wb/mgr/model/columns/MixinPropMgrColumn.js

/**
 * @copyright 2015 Tridium, Inc. All Rights Reserved.
 * @author Logan Byam
 */

/**
 * @module nmodule/webEditors/rc/wb/mgr/model/columns/MixinPropMgrColumn
 */
define([ 'baja!',
        'Promise',
        'underscore',
        'nmodule/js/rc/switchboard/switchboard',
        'nmodule/webEditors/rc/fe/baja/util/typeUtils',
        'nmodule/webEditors/rc/wb/mgr/model/MgrColumn',
        'nmodule/webEditors/rc/wb/mgr/model/columns/PropertyPathMgrColumn' ], function (
         baja,
         Promise,
         _,
         switchboard,
         typeUtils,
         MgrColumn,
         PropertyPathMgrColumn) {

  'use strict';

  const { getPropValueFromPath } = PropertyPathMgrColumn;
  const { isComplex } = typeUtils;
  const { getComplexAndSlotFromPath } = require('bajaScript/baja/comp/compUtil');

  /**
   * commitToNewInstance uses switchboard to ensure that a MixinPropMgrColumn won't attempt to add the
   * same mixin instance more than once when there is more than one `MixinPropMgrColumn`
   * registered on the same type.
   * @type {Function}
   */
  const ensureMixinInstanceExists = switchboard((column, row) => {
    return column.$ensureMixinInstanceExists(row);
  }, {
    allow: 'oneAtATime',
    keyedOn: function (column) { return column.$mixinType; }
  });

  function toSlotName(type) {
    return String(type).replace(':', '_');
  }

  function toColumnName(type, path) {
    return toSlotName(type) + '_' + path.join('_');
  }

  function defaultDisplayName(type, slotPath) {
    const { complex, slot } = getComplexAndSlotFromPath(baja.$(type), slotPath);
    return complex ? complex.getDisplayName(slot) : slot;
  }

  /**
   * Create a slash delimited path string from the array of path elements.
   */
  function toPathString(path) {
    return '/' + path.join('/');
  }

  /**
   * API Status: **Development**
   *
   * MgrColumn that allows you to edit a property on a `baja:IMixIn` installed
   * on a component.
   *
   * The property may also be several levels deep relative to the row's subject
   * component's mixin.
   *
   * @class
   * @alias module:nmodule/webEditors/rc/wb/mgr/model/columns/MixinPropMgrColumn
   * @extends module:nmodule/webEditors/rc/wb/mgr/model/MgrColumn
   * @param {string} name column name (if omitted, one will be generated). This
   * was added in Niagara 4.13, but may be omitted (you may still use mixinType
   * as the first parameter).
   * @param {Type} mixinType the `BIMixIn` Type whose property you wish to edit
   * @param {String|baja.Slot} path the property or property path (as a
   * '/'-delimited string) you wish to edit.
   * @param {Object} [params]
   * @param {String} [params.displayName] the display name for the column;
   * if omitted, will use the slot display name from the Type
   * @throws {Error} if Type or prop are missing
   */
  class MixinPropMgrColumn extends PropertyPathMgrColumn {
    constructor(name, mixinType, path, params) {
      if (typeof name !== 'string') {
        [ mixinType, path, params ] = arguments;
        path = path.toString().split('/');
        name = toColumnName(mixinType, path);
      } else {
        path = path.toString().split('/');
      }

      if (!mixinType || !_.isFunction(mixinType.is) || !mixinType.is('baja:IMixIn')) {
        throw new Error('mixinType parameter must be a baja:IMixIn');
      }

      super(name, path.join('/'), _.extend({
        displayName: defaultDisplayName(mixinType, path),
        type: mixinType
      }, params));

      this.$mixinType = mixinType;

      const commit = this.commit;
      this.commit = function (value, row) {
        return ensureMixinInstanceExists(this, row)
          .then(() => commit.apply(this, arguments));
      };
    }

    /**
     * @private
     * @return {String}
     */
    $getMixInSlotName() {
      return toSlotName(this.$mixinType);
    }

    /**
     * Get the existing mixin instance that already belongs to the component
     * loaded into the row. If called from `commit()`, the instance is
     * guaranteed to have been added if it did not already exist.
     *
     * @param {module:nmodule/webEditors/rc/wb/table/model/Row} row
     * @returns {baja.Value|null}
     */
    getMixinInstance(row) {
      return this.getMixinContainer(row).get(this.$getMixInSlotName());
    }

    /**
     * Get the container where the mixin is located or will be located once it is added.
     *
     * @param {module:nmodule/webEditors/rc/wb/table/model/Row} row
     * @returns {baja.Complex}
     * @since Niagara 4.13
     */
    getMixinContainer(row) {
      return row.getSubject();
    }

    /**
     * Get the mixin instance belonging to the component loaded into the row,
     * or create a new instance to work with.
     *
     * @private
     * @param {module:nmodule/webEditors/rc/wb/table/model/Row} row
     * @returns {baja.Value}
     */
    $getOrMakeMixinInstance(row) {
      return this.getMixinInstance(row) || baja.$(this.$mixinType);
    }

    /**
     * Get the complex this property is on. Utilize the existing mixin instance or
     * create a new instance of the mixIn if it does not exist yet.
     *
     * @param {module:nmodule/webEditors/rc/wb/table/model/Row} row
     * @returns {baja.Complex}
     */
    getComplexFromPath(row) {
      const { complex } = getComplexAndSlotFromPath(this.$getOrMakeMixinInstance(row), this.$path);
      return complex;
    }

    /**
     * Get the property from the mixin instance on this row's component. If
     * the component does not have an instance of that mixin (e.g. it is an
     * unmounted component created browser-side), the default value from an
     * instance of that mixin will be returned.
     *
     * @param {module:nmodule/webEditors/rc/wb/table/model/Row} row
     * @returns {baja.Value}
     */
    getValueFor(row) {
      const instance = this.$getOrMakeMixinInstance(row);

      if (!isComplex(instance)) {
        throw new Error('Complex required');
      }

      const prop = getPropValueFromPath(instance, this.$path);

      if (prop !== null) { return prop; }

      throw new Error('Could not get slot value for path: ' + toPathString(this.$path));
    }

    /**
     * Sets the value to the mixin instance on the row's component. If the
     * component does not have an instance of the mixin, a new one will be
     * created.
     *
     * This method may be safely overridden while maintaining the assurance that
     * the mixin instance exists.
     *
     * @param {baja.Value} value
     * @param {module:nmodule/webEditors/rc/wb/table/model/Row} row
     * @param {Object} params
     * @returns {Promise}
     */
    commit(value, row, params) {
      // mixin behavior is patched in in the constructor.
      return super.commit(...arguments);
    }

    /**
     * Sets the value to a new mixin instance for the row.
     *
     * @private
     * @param {module:nmodule/webEditors/rc/wb/table/model/Row} row
     * @returns {Promise}
     */
    $ensureMixinInstanceExists(row) {
      if (this.getMixinInstance(row)) {
        return Promise.resolve();
      }

      const slot = this.$getMixInSlotName();
      const comp = this.getMixinContainer(row);
      const mixin = baja.$(this.$mixinType);
      return comp.add({ slot, value: mixin });
    }
  }

  return MixinPropMgrColumn;
});