Source: services/DragAndDropModellingService.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/>.
 */

/**
 * <p>This service should be used in conjunction with the 
 * [DragAndDropTargetControl]{@link module:alfresco/form/controls/DragAndDropTargetControl}
 * and the widgets in the "alfresco/dnd" package. It should be configured with models for how
 * dropped items should be both rendered and edited. Each model should be placed into the
 * [models]{@link module:alfresco/services/DragAndDropModellingService#models} array. An example
 * model might look like this:</p>
 * <p><pre>{
 *    property: "name",
 *    targetValues: ["alfresco/forms/controls/(.*)"],
 *    widgetsForConfig: [
 *       {
 *          id: "ALF_EDIT_FORM_CONTROL_LABEL",
 *          name: "alfresco/forms/controls/TextBox",
 *          config: {
 *             fieldId: "LABEL",
 *             label: "Label",
 *             description: "The label for the form field value",
 *             name: "config.label"
 *          }
 *       }
 *    ],
 *    widgetsForDisplay: [
 *       {
 *          name: "alfresco/dnd/DroppedItemWrapper",
 *          config: {
 *             label: "{label}",
 *             value: "{value}",
 *             type: "{type}",
 *             widgets: [
 *                {
 *                   name: "alfresco/dnd/DroppedItemWidgets"
 *                }
 *             ]
 *          }
 *       }
 *    ]
 * }</pre></p>
 * <p>In this example we are mapping the configuration of a [dropped item]{@link module:alfresco/dnd/DragAndDropItems#items}
 * that has a property "name" that matches any of the Regular Expressions defined in the "targetValues" array 
 * (in this case we are targeting any data objects that have a "name" property that maps to that of a form 
 * control wigdget (e.g. "alfresco/forms/controls/TextBox")). If the criteria for this model is met then the 
 * "widgetsForConfig" or "widgetsForDisplay" arrays will be returned depending upon which has been asked for.</p>
 * <p>The "widgetsForDisplay" array is a model of widgets that will be rendered for the 
 * [dropped item]{@link module:alfresco/dnd/DragAndDropItems#items} and the "widgetsForConfig" array is a model 
 * of widgets that can be used to edit the value of that item.</p>
 * 
 * @module alfresco/services/DragAndDropModellingService
 * @extends module:alfresco/services/BaseService
 * @author Dave Draper
 */
define(["dojo/_base/declare",
        "alfresco/services/BaseService",
        "dojo/_base/lang",
        "dojo/_base/array",
        "alfresco/dnd/Constants",
        "alfresco/core/ObjectTypeUtils",
        "alfresco/util/objectProcessingUtil"],
        function(declare, BaseService, lang, array, Constants, ObjectTypeUtils, objectProcessingUtil) {
   
   return declare([BaseService], {
      
      /**
       * Sets up the subscriptions for the service
       * 
       * @instance
       * @since 1.0.32
       */
      registerSubscriptions: function alfresco_services_DragAndDropModellingService__registerSubscriptions() {
         this.alfSubscribe(Constants.requestWidgetsForDisplayTopic, lang.hitch(this, this.onDroppedItemDataRequest, "widgetsForDisplay"));
         this.alfSubscribe(Constants.requestWidgetsForConfigTopic, lang.hitch(this, this.onDroppedItemDataRequest, "widgetsForConfig"));
         this.alfSubscribe(Constants.requestWidgetsForNestedConfigTopic, lang.hitch(this, this.onDroppedItemDataRequest, "widgetsForNestedConfig"));
      },

      /**
       * This should be configured as an array of models to inspect when data is requested. See the 
       * main service description above for details.
       * 
       * @instance
       * @type {array}
       * @default
       */
      models: null,

      /**
       * This is the default widget model to render for dropped items that are not matched against
       * any specific model.
       *
       * @instance
       * @type {array}
       */
      widgetsForDefaultDisplay: [{
         name: "alfresco/dnd/DroppedItemWrapper",
         config: {
            showEditButton: false,
            label: "{label}",
            value: "{value}",
            type: "{type}",
            widgets: [{
               name: "alfresco/dnd/DroppedItem"
            }]
         }
      }],

      /**
       * This is the default widget model to use as for editing dropped items.
       *
       * @instance
       * @type {array}
       */
      widgetsForDefaultConfig: [],

      /**
       * This is the default widget model to include when editing nested dropped items.
       *
       * @instance
       * @type {array}
       */
      widgetsForDefaultNestedConfig: [],

      /**
       * Handles requests to find a model that matches the value provided in the published payload. If a matching model
       * is found then the supplied configuration from that model will be published in response.
       *
       * @instance
       * @param {string} configAttribute The attribute to use from the configuration if the model matches
       * @param {object} payload The payload provided
       */
      onDroppedItemDataRequest: function alfresco_services_DragAndDropModellingService__onDroppedItemDataRequest(configAttribute, payload) {
         var value = lang.getObject("value", false, payload);
         if (value)
         {
            var response = {};
            var success = array.some(this.models, lang.hitch(this, this.processModel, configAttribute, response, value));
            if (success === true)
            {
               // Found a match, resolve a promise or publish on a response topic provided...
               this.provideResponse(payload, response);
            } 
            else 
            {
               // Could not find a match, fall back to the default
               switch (configAttribute) {
                  case "widgetsForDisplay":
                     this.provideResponse(payload, {
                        widgets: lang.clone(this.widgetsForDefaultDisplay)
                     });
                     break;
                  case "widgetsForConfig":
                     this.provideResponse(payload, {
                        widgets: lang.clone(this.widgetsForDefaultConfig)
                     });
                     break;
                  case "widgetsForNestedConfig":
                     this.provideResponse(payload, {
                        widgets: lang.clone(this.widgetsForDefaultNestedConfig)
                     });
                     break;
                  default:
                     this.provideResponse(payload, {
                        widgets: []
                     });
                     break;
               }
            }
         } 
         else 
         {
            this.alfLog("error", "A request was made to provide a display model for a dropped item, but no value was provided", payload, this);
         }
      },

      /**
       * Provides a response to the request for information via either a promise or on a response topic depending upon
       * what has been provided in the supplied payload argument.
       *
       * @instance
       * @param {object} payload An object containing the details of the request.
       * @param {object} response The response to return.
       */
      provideResponse: function alfresco_services_DragAndDropModellingService__provideResponse(payload, response) {
         if (payload.promise && typeof payload.promise.resolve === "function") 
         {
            payload.promise.resolve(response);
         } 
         else if (payload.alfResponseTopic) 
         {
            this.alfPublish(payload.alfResponseTopic, response);
         } 
         else 
         {
            this.alfLog("error", "Matching model data was found for dropped item, but no promise or alfResponseTopic was provided in the payload", payload, this);
         }
      },

      /**
       * Find the drop targets in a template and ensure that they are have the appropriate
       * template configuration applied to them (i.e. that when they are displayed they are
       * shown with the externalised template label and set the appropriate property).
       *
       * @instance
       * @param  {object} parameters The template processing parameters
       * @return {boolean} An indication as to whether or not a template was processed.
       * @since 1.0.49
       */
      findDropTargets: function alfresco_services_DragAndDropModellingService__findDropTargets(parameters) {
         if (parameters.object === "alfresco/dnd/DragAndDropNestedTarget")
         {
            var parent = parameters.ancestors[parameters.ancestors.length-1];
            var targetProperty = lang.getObject("config.targetProperty", false, parent);
            if (targetProperty)
            {
               // See if the target property of the DragAndDropNestedTarget is a mapped in
               // the template...
               array.some(parameters.config.templateMappings, function(mapping) {
                  var found = (mapping.property === targetProperty);
                  if (found)
                  {
                     var clonedParent = lang.clone(parent);

                     // We need to swap the actual targetProperty and label for the template mapped versions...
                     clonedParent.config.targetProperty = "config." + mapping.id;
                     clonedParent.config.label = mapping.label;
                     parameters.config.data.push(clonedParent);
                  }
                  return found;
               });
            } 
         }
         else
         {
            var configParent = parameters.ancestors[parameters.ancestors.length-2];
            array.some(parameters.config.templateMappings, function(mapping) {
               var found = (mapping.property === parameters.object);
               if (found)
               {
                  var clonedParent = lang.clone(configParent);
                  clonedParent.config.name = "config." + mapping.id;
                  clonedParent.config.label = mapping.label;
                  clonedParent.config.description = mapping.description;
                  parameters.config.data.push(clonedParent);
               }
               return found;
            });
         }
      },

      /**
       * Add the externalised configuration defined for a template to the model for displaying it.
       * 
       * @instance
       * @param {object} parameters
       * @since 1.0.49
       */
      addConfig: function alfresco_services_DragAndDropModellingService__addConfig(parameters) {
         var parent = parameters.ancestors[parameters.ancestors.length-2];
         var templateResponse = {};
         array.some(parameters.config.models, lang.hitch(this, this.processModel, parameters.config.configAttribute, templateResponse, parent));
         
         // The problem with widgetsForDisplay is knowing what to filter and what to display.
         // There could be multiple nested config...
         // We should just have the drop targets (DragAndDropNestedTarget) where the "targetProperty" matches an exposed property (e.g. config.widgets)
         objectProcessingUtil.findObject(templateResponse, {
            prefix: "name",
            processFunction: lang.hitch(this, this.findDropTargets),
            config: {
               templateMappings: parameters.object,
               data: parameters.config.data
            }
         });
      },

      /**
       * Inspects the supplied model with data provided to see whether or not the model matches the data.
       * 
       * @instance
       * @param {string} configAttribute The configuration attribute to return from the model
       * @param {object} response The payload object to populate if the current model matches
       * the supplied value.
       * @param {object} value The value to compare against the model provided
       * @param {object} model The model to test the value against
       */
      processModel: function alfresco_services_DragAndDropModellingService__processModel(configAttribute, response, value, model) {
         var modelMatchFound = false;

         if ((value.isTemplate === true || value._alfTemplateName) && value.templateModel)
         {
            // The dropped item is a template, it will be necessary to construct the data from the data it contains...
            var templateModel = value.templateModel;
            var config = {
               models: this.models,
               data: [],
               configAttribute: configAttribute
            };

            // Working through the template model to find all the "_alfTemplateMappings" attributes - these
            // define how nested configuration within the template should be exposed as configuration attributes
            // of the template itself. For each mapping the addConfig function will be called to add this to
            // the configuration for display (either in the edit form or as a drop target)...
            objectProcessingUtil.findObject(templateModel, {
               prefix: "_alfTemplateMappings",
               processFunction: lang.hitch(this, this.addConfig),
               config: config
            });

            // Depending upon the configAttribute requested (either configuration for editing or configuration
            // for rendering the dropped item) the response should be constructed. For display it will be necessary
            // to place the model in a wrapper...
            modelMatchFound = true;
            if (configAttribute === "widgetsForDisplay")
            {
               response.widgets = [
                  {
                     name: "alfresco/dnd/DroppedTemplateItemWrapper",
                     label: "Nested item wrapper",
                     responseScope: "",
                     config: {
                        value: "{value}",
                        widgets: config.data,
                        label: "{label}",
                        showEditButton: "false"
                     }
                  }
               ];
            }
            else
            {
               response.widgets = config.data;
            }
         }
         else if (model.property && model.targetValues && value[model.property]) 
         {
            // The model has a property to check for, and the value contains the property
            // We're going to want to test this value against the configured RegularExpression target
            // values so assing it to a variable for later use...
            var targetPropertyValue = value[model.property];

            // Defensively check that the target values have been provided as an array so that we 
            // can issue a warning if invalid configuration has been provided.
            if (ObjectTypeUtils.isArray(model.targetValues))
            {
               // Check to see if the current value matches any of the target value regular expressions
               // provided. If any match then we will use the current model to populate the response
               // payload
               if (array.some(model.targetValues, lang.hitch(this, this.matchesTargetValue, targetPropertyValue)))
               {
                  // Check that the attriibute we're looking for is actually provided for this model
                  if (model[configAttribute])
                  {
                     response.widgets = lang.clone(model[configAttribute]);
                     modelMatchFound = true;
                  }
                  else
                  {
                     this.alfLog("error", "A model was matched to the supplied value, but the model doesn't have a '" + configAttribute + "' attribute", targetPropertyValue, model, this);
                  }
               }
            }
            else
            {
               this.alfLog("warn", "The model target values were not provided as an array", model.targetValues, this);
            }
         }
         return modelMatchFound;
      },

      /**
       * This function is used by the [processModel]{@link module:alfresco/services/DragAndDropModellingService#processModel}
       * function to test a supplied value against a Regular Expression to determine whether or not the model
       * applies to the value.
       * 
       * @instance
       * @param {object} value The value to test
       * @param {string} regex The string to use to build a Regular Expression with to test the value against.
       * @returns {boolean} True if the value matches against the Regular Expression and false otherwise.
       */
      matchesTargetValue: function alfresco_services_DragAndDropModellingService__matchesTargetValue(value, regex) {
         var match = false;
         try 
         {
            var re = new RegExp(regex);
            match = re.test(value);
         }
         catch (e)
         {
            this.alfLog("error", "There was an error testing a target value against the supplied value", value, regex, e, this);
         }
         return match;
      }
   });
});