view/view-manager.js

/*
 * Your installation or use of this SugarCRM file is subject to the applicable
 * terms available at
 * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/.
 * If you do not agree to all of the applicable terms or do not have the
 * authority to bind the entity as an authorized representative, then do not
 * install or use this SugarCRM file.
 *
 * Copyright (C) SugarCRM Inc. All rights reserved.
 */

const Utils = require('../utils/utils');

/**
 * View manager is used to create views, layouts, and fields based on optional
 * metadata inputs.
 *
 * The view manager's factory methods (`createView`, `createLayout`, and
 * `createField`) first check `views`, `layouts`, and `fields` hashes
 * respectively for custom class declaration before falling back the base class.
 *
 * Note the following is deprecated in favor of putting these controllers in the
 * `sugarcrm/clients/<platform>` directory, or using one of the appropriate
 * factory methods like `createView`, `createField`, or `createLayout`. Using
 * either of these idioms, your components will be internally namespaced by
 * platform for you. If you do choose to use the following idiom of defining
 * your controller directly on `ViewManager.view.<type>`, please be forewarned
 * that you will lose any automatic namespacing benefits and possibly encounter
 * naming collisions if your controller names are not unique. If you must
 * define directly, you may choose to prefix your controller name by your
 * application or platform e.g. `MyappMyCustom<Type>` where 'Myapp' is the
 * platform prefix.
 *
 * Put declarations of your custom views, layouts, fields in the corresponding
 * hash (see note above; this is deprecated):
 * ```
 * const ViewManager = require('./view-manager');
 * ViewManager.views.MyappMyCustomView = ViewManager.View.extend({
 *  // Put your custom logic here
 * });
 *
 * ViewManager.layouts.MyappMyCustomLayout = ViewManager.Layout.extend({
 *  // Put your custom logic here
 * });
 *
 * ViewManager.view.fields.MyappMyCustomField = ViewManager.Field.extend({
 *  // Put your custom logic here
 * });
 * ```
 *
 * @module View/ViewManager
 */
// Ever incrementing field ID
var _sfId = 0;

/**
 * @alias module:View/ViewManager
 */
const ViewManager = {
    /**
     * Resets class declarations of custom components.
     */
    reset: function() {
        _.each(this.layouts, function(layout, name) {
            delete this.layouts[name];
        }, this);
        _.each(this.views, function(view, name) {
            delete this.views[name];
        }, this);
        _.each(this.fields, function(field, name) {
            delete this.fields[name];
        }, this);
    },

    /**
     * Gets the ID of the most recently created field.
     *
     * @return {number} ID of the last created field.
     */
    getFieldId: function() {
        return _sfId;
    },

    /**
     * Hash of view classes.
     */
    views: {},

    /**
     * Hash of layout classes.
     */
    layouts: {},

    /**
     * Hash of field classes.
     */
    fields: {},

    /**
     * Creates an instance of a component and binds data changes to it.
     *
     * @param {string} type Component type (`layout`, `view`, `field`).
     * @param {string} name Component name.
     * @param {Object} params Parameters to pass to the Component's class
     *   constructor.
     * @param {string} params.type The component type.
     * @param {string} params.module The component's module.
     * @param {string} [params.loadModule=params.module] The module to
     *   create the component from.
     * @return {View/Component} New instance of a component.
     * @private
     */
    _createComponent: function(type, name, params) {
        var Klass = this.declareComponent(type, params.type || name, params.loadModule || params.module,
            params.controller, false, this._getPlatform(params));
        var component = new Klass(params);
        component.trigger("init");
        component.bindDataChange();
        return component;
    },

    /**
     * Creates an instance of a view.
     *
     * Examples:
     *
     * Create a list view. The view manager will use metadata for the view
     * named 'list' defined in Contacts module.
     * The controller's current context will be set on the new view instance.
     *
     * ```
     * var listView = ViewManager.createView({
     *     type: 'list',
     *     module: 'Contacts'
     * });
     * ```
     *
     * Create a custom view class. Note the following is deprecated in favor
     * of putting these controllers in the `sugarcrm/clients/<platform>`
     * directory, or using one of the appropriate factory methods like
     * `createView`, `createField`, or `createLayout`. Using that idiom, the
     * metadata manager will declare these components and take care of
     * namespacing by platform for you. If you do choose to use the
     * following idiom please be forewarned that you will lose any
     * namespacing benefits and possibly encounter naming collisions!
     *
     * ```
     * // Declare your custom view class.
     * // might cause collisions if another MyCustomView!
     * ViewManager.views.MyCustomView = ViewManager.View.extend({
     *     // Put your custom logic here
     * });
     * // if you must define directly on ViewManager.views, you may instead
     * // prefer to do:
     * ViewManager.views.<YOUR_PLATFORM>MyCustomView = ViewManager.View.extend({
     *     // Put your custom logic here
     * });
     *
     * var myCustomView = ViewManager.createView({
     *     type: 'myCustom'
     * });
     * ```
     *
     * Create a view with custom metadata payload.
     *
     * ```
     * var view = ViewManager.createView({
     *     type: 'detail',
     *         meta: { ... your custom metadata ... }
     * });
     * ```
     *
     * Look at {@link View/View}, particularly
     * {@link View/View#_loadTemplate} for more information on how the
     * `meta.template` property can be used.
     *
     * @param {Object} params View parameters.
     * @param {string} params.type The view identifier (`default`, `base`,
     *   etc.). Matches the controller to be used.
     * @param {string} [params.name=params.type] View name that
     *   distinguishes between multiple instances of the same view type. This
     *   matches the metadata to read from {@link Core.MetadataManager} and
     *   it is the easier way to reuse view types with different
     *   configurations.
     * @param {Object} [params.context=SUGAR.App.controller.context] Context to
     *   associate the newly created view with.
     * @param {string} [params.module] Module name.
     * @param {string} [params.loadModule] The module that should be
     *   considered the base.
     * @param {Object} [params.meta] Custom metadata.
     * @return {View/View} New instance of view.
     */
    createView: function(params) {
        // context is always defined on the controller
        params.context = params.context || SUGAR.App.controller.context;
        params.module = params.module || params.context.get('module');
        // name defines which metadata to load
        params.name = params.name || params.type;
        params.meta = params.meta || SUGAR.App.metadata.getView(params.module, params.name, params.loadModule);

        if (params.def && params.def.xmeta) {
            params.meta = _.extend({}, params.meta, params.def.xmeta);
        }

        // type defines which controller to use
        var meta = params.meta || {};
        params.type = params.type || meta.type || params.name;

        return this._createComponent('view', params.type, params);
    },

    /**
     * Creates an instance of a layout.
     *
     * Parameters define creation rules as well as layout properties.
     * The factory needs either layout name or type. Also, the layout type
     * must be specified. The layout type is retrieved either from the
     * `params` hash or layout metadata.
     *
     * Examples:
     *
     * Create a list layout. The view manager will use metadata for the
     * `list` layout defined in the Contacts module.
     * The controller's current context will be set on the new layout
     * instance.
     *
     * ```
     * var listLayout = ViewManager.createLayout({
     *     type: 'list',
     *     module: 'Contacts'
     * });
     * ```
     *
     * Create a custom layout class. Note that following is deprecated in
     * favor of using the `createLayout` factory or placing controller in
     * `sugarcrm/clients/<platform>/layouts` in which case the metadata
     * manager will take care of namespacing your controller by platform
     * name for you (e.g. MyCustomLayout becomes
     * `ViewManager.layouts.MyappMyCustomLayout`).
     *
     * ```
     * // Declare your custom layout class.
     * // might cause collisions if already a MyCustomLayout!
     * ViewManager.layouts.MyCustomLayout = ViewManager.Layout.extend({
     *     // Put your custom logic here
     * });
     * // if you must define directly on ViewManager.layouts,
     * // you may instead prefer to do:
     * ViewManager.layouts.<YOUR_PLATFORM>MyCustomLayout = ViewManager.Layout.extend({
     *     // Put your custom logic here
     * });
     *
     * var myCustomLayout = ViewManager.createLayout({
     *     type: 'myCustom'
     * });
     * ```
     *
     * Create a layout with custom metadata payload.
     *
     * ```
     * var layout = ViewManager.createLayout({
     *     type: 'detail',
     *     meta: { ... your custom metadata ... }
     * });
     * ```
     *
     * @param {Object} params layout parameters.
     * @param {string} [params.type] Layout identifier (`default`, `base`,
     *   etc.).
     * @param {string} [params.name=params.type] Layout name that
     *   distinguishes between multiple instances of the same layout type.
     * @param {Object} [params.context=SUGAR.App.controller.context]
     *   Context to associate the newly created layout with.
     * @param {string} params.module Module name.
     * @param {string} [params.loadModule] The module to load the Layout
     *   from. Defaults to `params.module` or the context's module, in that
     *   order.
     * @param {Object} [params.meta] Custom metadata.
     * @return {View/Layout} New instance of the layout.
     */
    createLayout: function(params) {
        params.context = params.context || SUGAR.App.controller.context;
        params.module = params.module || params.context.get('module');
        // name defines which metadata to load
        params.name = params.name || params.type;
        params.meta = params.meta || SUGAR.App.metadata.getLayout(params.module, params.name, params.loadModule);

        if (params.def && params.def.xmeta) {
            params.meta = _.extend({}, params.meta, params.def.xmeta);
        }

        // type defines which controller to use
        var meta = params.meta || {};
        params.type = params.type || meta.type || params.name;

        return this._createComponent('layout', params.type, params);
    },

    /**
     * Creates an instance of a field and registers it with the parent view
     * (`params.view`).
     *
     * The parameters define creation rules as well as field properties.
     *
     * For example,
     *
     * ```
     * var params = {
     *     view: new Backbone.View,
     *     def: {
     *         type: 'text',
     *         name: 'first_name',
     *         label: 'LBL_FIRST_NAME'
     *     },
     *     context: optional context,
     *     model: optional model,
     *     meta: optional custom metadata,
     *     viewName: optional
     * }
     * ```
     *
     * The view manager queries the metadata manager for field type specific
     * metadata (templates and JS controller) unless custom metadata is
     * passed in the `params` hash.
     *
     * Note the following is deprecated in favor of placing custom field
     * controllers in `sugarcrm/clients/<platform>/fields` or using the
     * `createField` factory.
     *
     * To create instances of custom fields, first declare its class in the
     * `ViewManager.fields` hash:
     *
     * ```
     * // might cause collision if MyCustomField already exists!
     * ViewManager.fields.MyCustomField = ViewManager.Field.extend({
     *     // Put your custom logic here
     * });
     * // if you must define directly on ViewManager.fields
     * // you may instead prefer to do:
     * ViewManager.fields.<YOUR_PLATFORM>MyCustomField = ViewManager.Field.extend({ ...
     *
     * var myCustomField = ViewManager.createField({
     *     view: someView,
     *     def: {
     *         type: 'myCustom',
     *         name: 'my_custom'
     *     }
     * });
     * ```
     *
     * @param {Object} params Field parameters.
     * @param {Backbone.View} params.view Backbone View object.
     * @param {Object} params.def Field definition.
     * @param {Object} [params.context=`SUGAR.App.controller.context`] The
     *   context.
     * @param {Object} [params.model] The model to use. If not specified,
     *   the model which is set on the context is used.
     * @param {string} [params.viewName=params.view.name] View name to
     *   determine the field template.
     * @param {boolean} [params.nested] Set to `true` if the field is nested.
     *   This means it already has a life cycle handler, and should not be
     *   added to the view's list of fields.
     * @return {View/Field} A new instance of field.
     */
    createField: function(params) {
        var type = params.viewDefs ? params.viewDefs.type : params.def.type;
        params.context = params.context || params.view.context || SUGAR.App.controller.context;
        params.model = params.model || params.context.get("model");
        params.module = params.module || (params.model && params.model.module ? params.model.module : params.context.get('module')) || "";
        params.sfId = ++_sfId;

        var field = this._createComponent("field", type, params);
        if (!params.nested) {
            // Register new field within its parent view.
            params.view.fields[field.sfId] = field;
        } else {
            // We still keep a reference of the nested field in the parent view
            // to be able to retrieve it using View/View#getField.
            params.view.nestedFields[field.sfId] = field;
        }

        return field;
    },

    /**
     * Returns the platform from the given params, falling back to
     * `SUGAR.App.config.platform` or else 'base'.
     *
     * @param {Object} params Parameters.
     * @param {string} [params.platform] The platform (`base`, `portal`, etc.).
     * @return {string} The platform.
     * @private
     */
    _getPlatform: function(params) {
        return params.platform || (SUGAR.App.config && SUGAR.App.config.platform ? SUGAR.App.config.platform : 'base');
    },

    /**
     * Gets a controller of type field, layout, or view.
     *
     * @param {Object} params Parameters for the controller.
     * @param {string} params.type The controller type.
     * @param {string} params.name the filename of the controller
     *   (e.g. 'flex-list', 'record', etc.).
     * @param {string} [params.platform] The platform, e.g. 'portal'.
     *   Will first attempt to fall back to `SUGAR.App.config.platform`, then
     *   'base'.
     * @param {string} [params.module] The module name.
     * @return {Object|null} The controller or `null` if not found.
     * @private
     */
    _getController: function(params) {
        var c = this._getBaseComponent(params.type, params.name, params.module, params.platform);
        //Check to see if we have the module specific class; if so return that
        if (c.cache[c.moduleBasedClassName]) {
            return c.cache[c.moduleBasedClassName];
        }
        return c.baseClass;
    },

    /**
     * Checks if a component has a certain plugin.
     *
     * @param {Object} params Set of parameters passed to function.
     * @param {string} params.type Type of component to check.
     * @param {string} params.name Name of component to check.
     * @param {string} params.plugin Name of plugin to check.
     * @param {string} [params.module=''] Name of module to check for custom
     *   components in.
     * @return {boolean|null} `true` if the specified component exists and
     *   has that plugin, `false` if the component does not exist or lacks
     *   that plugin, and `null` if incorrect arguments were passed.
     */
    componentHasPlugin: function(params) {
        var controller;
        if (!params.type || !params.name || !params.plugin) {
            SUGAR.App.logger.error("componentHasPlugin requires type, name, and plugin parameters");
            return null;
        }
        controller = this._getController(params);
        return controller && controller.prototype &&
            _.contains(controller.prototype.plugins, params.plugin);
    },

    /**
     * Retrieves class declaration for a component or creates a new
     * component class.
     *
     * This method creates a subclass of the base class if controller
     * parameter is not null and such subclass hasn't been created yet.
     * Otherwise, the method tries to retrieve the most appropriate class by
     * searching in the following order:
     *
     * - Custom class name: `<module><component-name><component-type>`.
     * For example, for Contacts module one could have:
     * `ContactsDetailLayout`, `ContactsFluidLayout`, `ContactsListView`.
     *
     * - Class name: `<component-name><component-type>`.
     * For example: `ListLayout`, `ColumnsLayout`, `DetailView`, `IntField`.
     *
     * - Custom base class: `<capitalized-appId><component-type>`
     * For example, if `SUGAR.App.config.appId == 'portal'`, custom base classes
     * would be:
     * `PortalLayout`, `PortalView`, `PortalField`.
     *
     * Declarations of such classes must be in the `ViewManager` namespace.
     * There are use cases when an app has some common component code.
     * In such cases, using custom base classes is beneficial. For example,
     * any app may need to override validation error handling for fields:
     *
     * ```
     * // Assuming SUGAR.App.config.appId === 'portal':
     * ViewManager.PortalField = ViewManager.Field.extend({
     *     initialize: function(options) {
     *         // Call super
     *         ViewManager.Field.prototype.initialize.call(this, options);
     *         // Custom initialization code...
     *     },
     *
     *     handleValidationError: function(errors) {
     *         // Custom validation logic
     *     }
     * });
     * ```
     *
     * The above declaration will make all field controllers extend
     * `ViewManager.PortalField` instead of `ViewManager.Field`.
     *
     * - Base class: `<component-type>` - `Layout`, `View`, `Field`.
     *
     * Note 1. Although the view manager supports module specific fields
     * like `ContactsIntField`, the server does not provide such
     * customization.
     *
     * Note 2. The layouts is a special case because their class name is
     * built both from layout name and layout type. One could have
     * `ListLayout` or `ColumnsLayout` including their module specific
     * counterparts like `ContactsListView` and `ContactsColumnsLayout`.
     * The "named" class name is checked first.
     *
     * @param {string} type Lower-cased component type: `layout`, `view`, or
     *   `field`.
     * @param {string} name Lower-cased component name. For example, 'list'
     *   (layout or view), or 'bool' (field).
     * @param {string} [module] Module name.
     * @param {string} [controller] Controller source code string.
     * @param {boolean} [overwrite] Will overwrite if duplicate
     *   custom class or layout is cached. Note that if no controller is
     *   passed, overwrite is ignored since we can't create a meaningful
     *   component without a controller.
     * @param {string} [platform] The platform e.g. 'base', 'portal', etc.
     * @return {Function} Component class.
     */
    declareComponent: function(type, name, module, controller, overwrite, platform) {
        overwrite = !!(controller && overwrite);
        var c = this._getBaseComponent(type, name, module, platform, overwrite);
        if (overwrite && c.cache[c.moduleBasedClassName]) {
            delete c.cache[c.moduleBasedClassName];
        }

        return c.cache[c.moduleBasedClassName] ||
            Utils.extendClass(c.cache, c.baseClass, c.moduleBasedClassName, controller, c.platformNamespace) ||
            c.baseClass;
    },

    /**
     * Internal helper function for getting a component (controller). Do not
     * call directly and instead use `declareComponent`, etc.
     * depending on your needs.
     * @param {string} type Lower-cased component type: `layout`, `view`, or
     *   `field`.
     * @param {string} name Lower-cased component name. For example, `list`
     *   (layout or view), or `bool` (field).
     * @param {string} [module] Module name.
     * @param {string} [platform] The platform e.g. 'base', 'portal', etc.
     * @param {boolean} [overwrite=true] When `true`, custom controller
     *   overrides will be ignored and only components that exactly match
     *   the name will be returned. The base class returned is `base`.
     * @return {Object} The base component information.
     * @return {Object} return.cache The collection of controllers of the
     *   given component type.
     * @return {string} return.platformNamespace The platform prefix.
     * @return {string} return.moduleBasedClassName The prefixed class name.
     * @return {Object} return.baseClass The class for the base component.
     * @private
     */
    _getBaseComponent: function(type, name, module, platform, overwrite) {
        platform = this._getPlatform({platform: platform});
        // The type e.g. View, Field, Layout
        var ucType = Utils.capitalize(type);

        // The platform e.g. Base, Portal, etc.
        var platformNamespace = Utils.capitalize(platform);

        // The component name and type concatenated e.g. ListView
        var className = Utils.capitalizeHyphenated(name) + ucType;

        // The combination of platform, optional module, and className e.g. BaseAccountsListView
        var moduleBasedClassName = platformNamespace + (module || "") + className,
            customModuleBasedClassName = platformNamespace + (module || "") + "Custom" + className;
        var cache = this[type + 's'];
            // App id and type fallback
        var customBaseClassName = Utils.capitalize(SUGAR.App.config.appId) + ucType;
        // Components are now namespaced by <platform> so we must prefix className to find in cache
        // if we don't find platform-specific, than we next look in Base<className> and so on
        var baseClass = cache[platformNamespace + "Custom" + className] ||
            cache[platformNamespace + className] ||
            cache["BaseCustom" + className] ||
            cache["Base" + className] ||
            // For backwards compatibility, if they define ViewManager.views.MyView we should still find
            cache[className] ||
            cache["BaseBase" + ucType] ||
            this['Custom' + customBaseClassName] ||
            this[customBaseClassName] ||
            this[ucType];
        // Override to use the custom class instead of the standard one if it exists.
        if (cache[customModuleBasedClassName] && !overwrite) {
            moduleBasedClassName = customModuleBasedClassName;
        }
        return {
            cache: cache,
            platformNamespace: platformNamespace,
            moduleBasedClassName: moduleBasedClassName,
            baseClass: baseClass
        };
    }
};

module.exports = ViewManager;