Source: services/actions/ManageAspectsService.js

/**
 * Copyright (C) 2005-2016 Alfresco Software Limited.
 *
 * This file is part of Alfresco
 *
 * Alfresco is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Alfresco is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
 */

/**
 * This service handles requests to manage the aspects on particular node. The aspects that are available
 * to be added (and optionally those that can be added and removed) are expected to be provided as configuration
 * attributes when instantiating this service.
 *
 * @module alfresco/services/actions/ManageAspectsService
 * @extends module:alfresco/services/BaseService
 * @mixes module:alfresco/core/CoreXhr
 * @author Dave Draper
 */
define(["dojo/_base/declare",
        "alfresco/services/BaseService",
        "alfresco/core/CoreXhr",
        "alfresco/core/topics",
        "service/constants/Default",
        "dojo/_base/lang",
        "dojo/_base/array",
        "alfresco/core/ObjectTypeUtils"],
        function(declare, BaseService, AlfCoreXhr, topics, AlfConstants, lang, array, ObjectTypeUtils) {

   return declare([BaseService, AlfCoreXhr], {

      /**
       * An array of the i18n files to use with this widget.
       *
       * @instance
       * @type {object[]}
       * @default [{i18nFile: "./i18n/ManageAspectsService.properties"}]
       */
      i18nRequirements: [{i18nFile: "./i18n/ManageAspectsService.properties"}],

      /**
       * This is the array of available aspects that are available. 
       *
       * @instance
       * @type {array}
       * @default
       */
      availableAspects: null,

      /**
       * This is the array of available aspects can be added to a node. If this is not provided then it
       * is assumed that all the aspected configured in the 
       * [availableAspects]{@link module:alfresco/services/actions/ManageAspectsService#availableAspects}
       * array can be added to a node.
       *
       * @instance
       * @type {array}
       * @default
       */
      addableAspects: null,

      /**
       * This is the array of available aspects can be removed from a node. If this is not provided then it
       * is assumed that all the aspected configured in the 
       * [availableAspects]{@link module:alfresco/services/actions/ManageAspectsService#availableAspects}
       * array can be removed from a node.
       *
       * @instance
       * @type {array}
       * @default
       */
      removableAspects: null,

      /**
       * The key of each aspect that is used as the unique identifier of that aspect.
       *
       * @instance
       * @type {string}
       * @default
       */
      itemKey: "id",

      /**
       * Extends the [inherited constructor]{@link module:alfresco/services/BaseService#constructor}
       * to set up the configured aspects.
       *
       * @instance
       * @param  {object[]} args
       */
      constructor: function alfresco_services_BaseService__constructor(/*jshint unused:false*/ args) {
         if (!this.availableAspects)
         {
            this.availableAspects = [];
         }
         if (!this.addableAspects)
         {
            // If no explicitly addable aspects have been declared, then all available aspects are addable
            this.addableAspects = lang.clone(this.availableAspects);
         }
         if (!this.removableAspects)
         {
            // If no explicitly removable aspects have been declared, then all available aspects are removable
            this.removableAspects = lang.clone(this.availableAspects);
         }

         // Clone the available aspects for use as determining which aspects to display...
         this.aspectsToDisplay = lang.clone(this.availableAspects);

         // Process the final available aspects list to convert arrays of Strings into arrays of objects
         // and make an attempt at generating a user-friendly label for the aspect...
         var availableAspects = [];
         array.forEach(this.availableAspects, lang.hitch(this, this.processAspect, availableAspects));
         this.availableAspects = availableAspects;
      },

      /**
       * Sets up the service using the configuration provided. This will check to see what aspects are available,
       * addable and removable. If no addble or removable aspects are explicitly configured then it is assumed that
       * all available aspects are both addable and removable. Only aspects that are configured as being available
       * will be displayed in the manage aspects picker, only aspects that are addable can be added in the manage
       * aspects picker and only aspects that are removable can be removed in the manage aspects picker.
       *
       * @instance
       * @since 1.0.32
       */
      registerSubscriptions: function alfresco_services_actions_ManageAspectsService__registerSubscriptions() {
         this.alfSubscribe("ALF_MANAGE_ASPECTS_REQUEST", lang.hitch(this, this.onManageAspects));
      },

      /**
       * Handles requests to manage aspects by making a request to get the currently applied aspects for the 
       * requested Node. This is always performed to ensure that the data is not stale.
       * 
       * @instance
       * @param {object} item The item to perform the action on
       */
      onManageAspects: function alfresco_services_actions_ManageAspectsService__onManageAspects(payload) {
         if (payload && payload.node)
         {
            this.alfServicePublish(topics.PROGRESS_INDICATOR_ADD_ACTIVITY);
            this.serviceXhr({
               url: AlfConstants.PROXY_URI + "slingshot/doclib/aspects/node/" + payload.node.nodeRef.replace("://", "/"),
               method: "GET",
               item: payload,
               successCallback: this.onAspectsSuccess,
               failureCallback: this.onAspectsFailure,
               callbackScope: this
            });
         }
         else
         {
            this.alfLog("warning", "A request was made to manage aspects, but no 'node' was provided in the 'payload' object", payload, this);
         }
      },

      /**
       * Displays a dialog containing a [SimplePicker]{@link module:alfresco/forms/controls/SimplePicker} control
       * that allows the user to select which aspects they wish to have applie to the current item.
       * 
       * @param {object} item The item to modify the applied aspects of.
       * @param {array} currentAspects The array of aspects that are currently applied to the item.
       */
      showAspectsDialog: function alfresco_services_actions_ManageAspectsService__onAspectsDialog(item, currentAspects) {
         var responseTopic = this.generateUuid();
         var subscriptionHandle = this.alfSubscribe(responseTopic, lang.hitch(this, this.onActionManageAspectsConfirmation), true);
         
         var dialogTitle = this.message("services.actionservice.ManageAspects.dialogTitle", {
            0: item.displayName
         });

         this.alfPublish("ALF_CREATE_FORM_DIALOG_REQUEST", {
            dialogId: "ALF_MANAGE_ASPECTS_DIALOG",
            dialogTitle: dialogTitle,
            formSubmissionTopic: responseTopic,
            formSubmissionPayloadMixin: {
               item: item,
               subscriptionHandle: subscriptionHandle,
               originallySelected: lang.clone(currentAspects)
            },
            widgets: [
               {
                  name: "alfresco/forms/controls/SimplePicker",
                  config: {
                     name: "selectedAspects",
                     itemKey: this.itemKey,
                     propertyToRender: "label",
                     noItemsMessage: "services.actionservice.ManageAspects.noAspects",
                     availableItemsLabel: "services.actionservice.ManageAspects.available",
                     pickedItemsLabel: "services.actionservice.ManageAspects.applied",
                     value: currentAspects,
                     currentData: {
                        items: this.availableAspects
                     },
                     hideValidation: true,
                     aspectsToDisplay: this.aspectsToDisplay,
                     addableAspects: this.addableAspects,
                     removableAspects: this.removableAspects,
                     widgetsForAvailableItemsView: this.widgetsForAvailableItemsView,
                     widgetsForPickedItemsView: this.widgetsForPickedItemsView
                  }
               }
            ]
         });
         this.alfServicePublish(topics.PROGRESS_INDICATOR_REMOVE_ACTIVITY);
      },

      /**
       * Handles successful requests to retrieve the currently applied aspects for a given node.
       * 
       * @param {object} response The response object from the XHR request
       * @param  {object} originalRequestConfig The object passed when making the original XHR request
       */
      onAspectsSuccess: function  alfresco_services_actions_ManageAspectsService__onAspectsSuccess(response, originalRequestConfig) {
         if (response && response.current)
         {
            var aspects = [];
            array.forEach(response.current, lang.hitch(this, this.processAspect, aspects));
            this.showAspectsDialog(originalRequestConfig.item, aspects);
         }
         else
         {
            this.alfServicePublish(topics.PROGRESS_INDICATOR_REMOVE_ACTIVITY);
            this.alfLog("error", "The response to a request for currently available aspects did not contain a 'current' attribute", response, originalRequestConfig, this);
         }
      },

      /**
       * Inspects the supplied aspect data and adds it into the aspects array if it is an object or constructs
       * an object for it if it is a string.
       * 
       * @param {array} The array of aspects to add the current aspect data into
       * @param {object|string} The current aspect data
       */
      processAspect: function alfresco_services_actions_ManageAspectsService__processAspect(aspects, aspect) {
         if (ObjectTypeUtils.isString(aspect)) {
            aspects.push({
               id: aspect,
               label: this.processAspectLabel(aspect)
            });
         }
         else if (ObjectTypeUtils.isObject(aspect))
         {
            if (!aspect.label)
            {
               aspect.label = this.processAspectLabel(aspect[this.itemKey]);
            }
            aspects.push(aspect);
         }
         else
         {
            this.alfLog("warn", "Unexpected aspect data encountered when processing current aspects", aspect, this);
         }
      },

      /**
       * Attempts to convert an aspect into user friendly label. This assumes that aspects are mapped to 
       * as follows, if the aspect is "cm:complianceable" then the NLS key is expected to be: "aspect.cm_complianceable"
       * 
       * @param {string} aspect The aspect to get a user friendly label for
       * @return {string} A user friendly label for the aspect (or the original aspect if one can't be found)
       */
      processAspectLabel: function alfresco_services_actions_ManageAspectsService__processAspectLabel(aspect) {
         var label = aspect;
         var nlsKey = "aspect." + aspect.replace(":", "_");
         var nlsValue = this.message(nlsKey);
         if (nlsKey !== nlsValue)
         {
            label = nlsValue;
         }
         return label;
      },

      /**
       * Handles failed requests to retrieve the currently applied aspects for a given node.
       * 
       * @param {object} response The response object from the XHR request
       * @param  {object} originalRequestConfig The object passed when making the original XHR request
       */
      onAspectsFailure: function  alfresco_services_actions_ManageAspectsService__onAspectsFailure(response, originalRequestConfig) {
         this.alfServicePublish(topics.PROGRESS_INDICATOR_REMOVE_ACTIVITY);
         this.alfLog("error", "It was not possible to retrieve a list of currently available aspects", response, originalRequestConfig, this);
         this.alfPublish("ALF_DISPLAY_PROMPT", {
            message: this.message("services.actionservice.ManageAspects.aspectRetrievalFailed", {
               "0": originalRequestConfig.item.displayName
            })
         });
      },

      /**
       * This function should be called when a user confirms the changes that they have made to aspects for a
       * particular item (e.g. document or folder).
       * 
       * @param {object} payload The payload containing the updated aspects.
       */
      onActionManageAspectsConfirmation: function alfresco_services_actions_ManageAspectsService__onActionManageAspectsConfirmation(payload) {
         // Clean up any subscription handles
         if (payload && payload.subscriptionHandle)
         {
            this.alfUnsubscribe(payload.subscriptionHandle);
            delete payload.subscriptionHandle;
         }

         // Generate the arrays of added and removed aspects...
         var added = [];
         var removed= [];
         array.forEach(payload.selectedAspects, lang.hitch(this, this.findAdded, payload.originallySelected, added));
         array.forEach(payload.originallySelected, lang.hitch(this, this.findRemoved, payload.selectedAspects, removed));

         // Post the update...
         var data = {
            added: added,
            removed: removed
         };
         var processedNodeRef = payload.item.node.nodeRef.replace("://", "/");
         this.serviceXhr({url: AlfConstants.PROXY_URI + "slingshot/doclib/action/aspects/node/" + processedNodeRef,
                          method: "POST",
                          item: payload.item,
                          data: data,
                          successCallback: this.onUpdateSuccess,
                          failureCallback: this.onUpdateFailure,
                          callbackScope: this});
      },

      /**
       * This function generates the data to be posted when updating the applied aspects. The Alfresco Repository API
       * expects an "added" array to be provided in the POST body so this function will construct that array
       * from the data provided by the form submissions.
       * 
       * @param {array} originallySelected The originally selected aspects when the dialog was opened.
       * @param {array} added The array of added aspects to populate
       * @param {object} aspect The current for consideration of adding to the "added" array
       */
      findAdded: function alfresco_services_actions_ManageAspectsService__findAdded(originallySelected, added, aspect) {
         // Was the current item (that is now selected) in the original array of selected items?
         var found = array.some(originallySelected, function(item) {
            return item[this.itemKey] === aspect[this.itemKey];
         }, this);

         if (found === false)
         {
            added.push(aspect[this.itemKey]);
         }
      },

      /**
       * This function generates the data to be posted when updating the applied aspects. The Alfresco Repository API
       * expects a "removed" array to be provided in the POST body so this function will construct that array
       * from the data provided by the form submissions.
       * 
       * @param {array} originallySelected The originally selected aspects when the dialog was opened.
       * @param {array} removed The array of removed aspects to populate
       * @param {object} aspect The current for consideration of adding to the "removed" array
       */
      findRemoved: function alfresco_services_actions_ManageAspectsService__findRemoved(selected, removed, aspect) {
         // Was the current item (that was in the original array of selected items) in the new array of selected items?
         var found = array.some(selected, function(item) {
            return item[this.itemKey] === aspect[this.itemKey];
         }, this);

         if (found === false)
         {
            removed.push(aspect[this.itemKey]);
         }
      },

      /**
       * Handles successful requests update aspects on a node.
       * 
       * @param {object} response The response object from the XHR request
       * @param  {object} originalRequestConfig The object passed when making the original XHR request
       */
      onUpdateSuccess: function  alfresco_services_actions_ManageAspectsService__onUpdateSuccess(response, originalRequestConfig) {
         this.alfLog("info", "Aspects updated successfully", response, originalRequestConfig, this);
         this.alfServicePublish(topics.DISPLAY_NOTIFICATION, {
            message: this.message("services.actionservice.ManageAspects.aspectUpdateSuccess", {
               "0": originalRequestConfig.item.displayName
            })
         });
      },

      /**
       * Handles failed requests to update aspects on a node.
       * 
       * @param {object} response The response object from the XHR request
       * @param  {object} originalRequestConfig The object passed when making the original XHR request
       */
      onUpdateFailure: function  alfresco_services_actions_ManageAspectsService__onUpdateFailure(response, originalRequestConfig) {
         this.alfLog("error", "Aspects were not updated", response, originalRequestConfig, this);
         this.alfPublish("ALF_DISPLAY_PROMPT", {
            message: this.message("services.actionservice.ManageAspects.aspectUpdateFailed", {
               "0": originalRequestConfig.item.displayName
            })
         });
      },

      /**
       * This is the model to use for rendering the items that are available for selection.
       *
       * @instance
       * @type {object}
       */
      widgetsForAvailableItemsView: [
         {
            name: "alfresco/lists/views/AlfListView",
            config: {
               widgets: [
                  {
                     name: "alfresco/lists/views/layouts/Row",
                     config: {
                        visibilityConfig: {
                           rules: [
                              {
                                 topic: "ALF_ITEM_REMOVED",
                                 attribute: "{itemKey}",
                                 is: ["{itemKey}"],
                                 useCurrentItem: true,
                                 strict: false
                              }
                           ]
                        },
                        invisibilityConfig: {
                           rules: [
                              {
                                 topic: "ALF_ITEM_SELECTED",
                                 attribute: "{itemKey}",
                                 is: ["{itemKey}"],
                                 useCurrentItem: true,
                                 strict: false
                              }
                           ]
                        },
                        widgets: [
                           {
                              name: "alfresco/lists/views/layouts/Cell",
                              config: {
                                 widgets: [
                                    {
                                       name: "alfresco/renderers/Property",
                                       config: {
                                          propertyToRender: "{propertyToRender}"
                                       }
                                    }
                                 ]
                              }
                           },
                           {
                              name: "alfresco/lists/views/layouts/Cell",
                              config: {
                                 width: "20px",
                                 widgets: [
                                    {
                                       name: "alfresco/renderers/PublishAction",
                                       config: {
                                          publishPayloadType: "CURRENT_ITEM",
                                          publishGlobal: false,
                                          publishToParent: false,
                                          renderFilter: [
                                             {
                                                property: "{itemKey}",
                                                values: "{addableAspects}"
                                             }
                                          ]
                                       }
                                    }
                                 ]
                              }
                           }
                        ],
                        renderFilter: [
                           {
                              property: "{itemKey}",
                              values: "{aspectsToDisplay}"
                           }
                        ]
                     }
                  }
               ]
            }
         }
      ],

      /**
       * This is the model to use for rendering the items that have been picked.
       *
       * @instance
       * @type {object}
       */
      widgetsForPickedItemsView: [
         {
            name: "alfresco/lists/views/layouts/Row",
            config: {
               widgets: [
                  {
                     name: "alfresco/lists/views/layouts/Cell",
                     config: {
                        widgets: [
                           {
                              name: "alfresco/renderers/Reorder",
                              config: {
                                 propertyToRender: "{propertyToRender}",
                                 moveUpPublishTopic: "ALF_ITEM_MOVED_UP",
                                 moveUpPublishPayloadType: "CURRENT_ITEM",
                                 moveDownPublishTopic: "ALF_ITEM_MOVED_DOWN",
                                 moveDownPublishPayloadType: "CURRENT_ITEM"
                              }
                           }
                        ]
                     }
                  },
                  {
                     name: "alfresco/lists/views/layouts/Cell",
                     config: {
                        widgets: [
                           {
                              name: "alfresco/renderers/Property",
                              config: {
                                 propertyToRender: "{propertyToRender}"
                              }
                           }
                        ]
                     }
                  },
                  {
                     name: "alfresco/lists/views/layouts/Cell",
                     config: {
                        width: "20px",
                        widgets: [
                           {
                              name: "alfresco/renderers/PublishAction",
                              config: {
                                 iconClass: "delete-16",
                                 publishTopic: "ALF_ITEM_REMOVED",
                                 publishPayloadType: "CURRENT_ITEM",
                                 renderFilter: [
                                    {
                                       property: "{itemKey}",
                                       values: "{removableAspects}"
                                    }
                                 ]
                              }
                           }
                        ]
                     }
                  }
               ],
               renderFilter: [
                  {
                     property: "{itemKey}",
                     values: "{aspectsToDisplay}"
                  }
               ]
            }
         }
      ]
   });
});