wb/mgr/model/columns/TypeMgrColumn.js

/**
 * @copyright 2016 Tridium, Inc. All Rights Reserved.
 */

/**
 * @module nmodule/webEditors/rc/wb/mgr/model/columns/TypeMgrColumn
 */

define([ 'baja!',
        'lex!webEditors',
        'underscore',
        'Promise',
        'nmodule/webEditors/rc/wb/mgr/MgrTypeInfo',
        'nmodule/webEditors/rc/wb/mgr/mgrUtils',
        'nmodule/webEditors/rc/wb/mgr/model/MgrColumn',
        'nmodule/webEditors/rc/wb/mgr/model/columns/NameMgrColumn',
        'nmodule/webEditors/rc/fe/baja/util/typeUtils' ], function (
        baja,
        lexs,
        _,
        Promise,
        MgrTypeInfo,
        mgrUtils,
        MgrColumn,
        NameMgrColumn,
        typeUtils) {

  'use strict';

  const [ webEditorsLex ] = lexs;
  const { clearProposal, getProposalKeys, hasDiscoveryName, hasUserDefinedNameValue, propose, setProposedDiscoveryValues } = mgrUtils;
  const { getUniqueSlotName } = typeUtils;

  const OBJECT_ICON = baja.Icon.make([ 'module://icons/x16/object.png' ]),

      // These are key values used to store the MgrTypeInfo and discovery
      // data associated with the row. These will be used with the Row.data()
      // function.

      AVAILABLE_TYPES_KEY   = 'TypeMgrColumn.available',
      INTERSECTED_TYPES_KEY = 'TypeMgrColumn.intersection',
      SELECTED_TYPE_KEY     = 'TypeMgrColumn.selected',
      DISCOVERY_DATA_KEY    = 'TypeMgrColumn.discovery';

  /**
   * API Status: **Development**
   *
   * Manager column used to display and edit the component type of a row.
   *
   * @class
   * @extends module:nmodule/webEditors/rc/wb/mgr/model/MgrColumn
   * @alias module:nmodule/webEditors/rc/wb/mgr/model/columns/TypeMgrColumn
   */
  var TypeMgrColumn = function TypeMgrColumn(params) {
    MgrColumn.call(this, '__type', _.defaults({
      displayName: webEditorsLex.get('manager.column.type')
    }, params || {}));
  };

  TypeMgrColumn.prototype = Object.create(MgrColumn.prototype);
  TypeMgrColumn.prototype.constructor = TypeMgrColumn;

  /**
   * The TypeMgrColumn key for the selected TypeInfo in Row.data()
   * @type {String}
   */
  TypeMgrColumn.SELECTED_TYPE_KEY = SELECTED_TYPE_KEY;

  /**
   * The TypeMgrColumn key for the discovery object in Row.data()
   * @type {String}
   */
  TypeMgrColumn.DISCOVERY_DATA_KEY = DISCOVERY_DATA_KEY;

  function isEmpty(a) {
   return !(a && a.length);
  }

  /**
   * Given two arrays of MgrTypeInfos, return the array of intersecting types.
   * This is used to find the common component types for the add command
   * from the types returned by the manager's `getTypesForDiscoverySubject()`
   * function.
   *
   * @param {Array.<module:nmodule/webEditors/rc/wb/mgr/MgrTypeInfo>} a
   * @param {Array.<module:nmodule/webEditors/rc/wb/mgr/MgrTypeInfo>} b
   * @returns {Array.<module:nmodule/webEditors/rc/wb/mgr/MgrTypeInfo>}
   */
  function intersectTypes(a, b) {
    var result = [], i, j;

    if (isEmpty(a) || isEmpty(b)) {
      return result;
    }

    for (i = 0; i < a.length; i++) {
      var aType = a[i].getType();
      for (j = 0; j < b.length; j++) {
        if (aType === b[j].getType()) {
          result.push(a[i]);
          break;
        }
      }
    }

    return result;
  }

  function hasAvailableTypes(row) {
    var types = row.data(AVAILABLE_TYPES_KEY);
    return !isEmpty(types);
  }

  function getAvailableTypes(row) {
    return row.data(AVAILABLE_TYPES_KEY) || [];
  }

  /**
   * For an array of `Row`s, get the available types for each row (based on
   * a discovery object, for instance) and return an array containing the
   * intersection of the types.
   *
   * @private
   * @static
   *
   * @param {Array.<module:nmodule/webEditors/rc/wb/table/model/Row>} rows
   * @returns {Array.<module:nmodule/webEditors/rc/wb/mgr/MgrTypeInfo>}
   */
  TypeMgrColumn.getTypeIntersection = function (rows) {
    var allTypes, head, tail;

    if (isEmpty(rows)) { return []; }
    if (rows.length === 1) { return getAvailableTypes(rows[0]); }

    allTypes = _.map(rows, getAvailableTypes);
    head = allTypes[0];
    tail = _.tail(allTypes);

    return _.reduce(tail, intersectTypes, head);
  };

  /**
   * Returns the baja.Type of the `Row`'s subject as the value.
   *
   * @param {module:nmodule/webEditors/rc/wb/table/model/Row} row
   * @returns {baja.Type}
   */
  TypeMgrColumn.prototype.getValueFor = function (row) {
    return row.getSubject().getType();
  };

  /**
   * Builds the cell contents using the type's display name.
   *
   * @param {module:nmodule/webEditors/rc/wb/table/model/Row} row
   * @param {JQuery} dom
   */
  TypeMgrColumn.prototype.buildCell = function (row, dom) {
    var type = this.getProposedValueFor(row);
    return typeUtils.getTypeDisplayName(type)
      .then(function (name) {
        dom.text(name);
      });
  };

  /**
   * For the given array of rows, this will produce a single value from
   * from the intersection of the available types. The types will have been
   * set as a data value on the row by either the `NewCommand` or `AddCommand`.
   * This will create a dynamic enum with each tag representing one of the
   * intersected types.
   *
   * @param {Array.<module:nmodule/webEditors/rc/wb/table/model/Row>} rows
   * @returns {baja.DynamicEnum|string}
   * @throws {Error} if not every row has a type to choose from (i.e. the rows
   * are being edited, not added)
   */
  TypeMgrColumn.prototype.coalesceRows = function (rows) {
    var intersection,
        ordinal = 0,
        currentType,
        i;

    if (!_.every(rows, hasAvailableTypes)) {
      throw new Error();
    }

    intersection = TypeMgrColumn.getTypeIntersection(rows);

    if (intersection.length === 0 && rows.length > 1) {
      // If there are no intesecting types, the existing types
      // of the subjects are used
      throw new Error();
    }

    if (rows.length) {
      currentType = this.getValueFor(rows[0]);
      var currentTypeSpec = currentType.getTypeSpec();
      for (i = 0; i < intersection.length; i++) {
        if (currentTypeSpec === intersection[i].getType().getTypeSpec()) {
          ordinal = i;
          break;
        }
      }

      // Store the intersection array on the row(s) so we can refer to it when
      // we want to get the MgrTypeInfo later.

      _.each(rows, function (row) {
        row.data(INTERSECTED_TYPES_KEY, intersection);
      });
    }

    // Create a new dynamic enum with the ordinals corresponding to the
    // MgrTypeInfo instances in the type intersection array above.

    return baja.DynamicEnum.make({
      ordinal: ordinal,
      range: baja.EnumRange.make({
        ordinals: _.map(intersection, function (typeInfo, index) { return index; }),
        tags: _.map(intersection, function (typeInfo) {
          return baja.SlotPath.escape(typeInfo.getDisplayName());
        })
      })
    });
  };

  /**
   * Create the config object for the editor based on the coalesced value.
   *
   * @param {Array.<module:nmodule/webEditors/rc/wb/table/model/Row>} rows
   * @returns {Object}
   * @throws {Error} if not every row has a type to choose from (i.e. the rows
   * are being edited, not added)
   */
  TypeMgrColumn.prototype.getConfigFor = function (rows) {
    if (!_.every(rows, hasAvailableTypes)) {
      throw new Error('every row must have a type');
    }

    let config = MgrColumn.prototype.getConfigFor.apply(this, arguments);

    config = _.extend({}, config, {
      properties: _.extend({ uxFieldEditor: 'webEditors:FrozenEnumEditor' }, config.properties)
    });


    return config;
  };

  /**
   * Set the proposed baja.Type on the row. This is the value that will
   * be returned by `getProposedValueFor()`.
   *
   * @param {module:nmodule/webEditors/rc/wb/mgr/model/columns/TypeMgrColumn} col
   * @param {baja.Type} type
   * @param {module:nmodule/webEditors/rc/wb/table/model/Row} row
   */
  function setProposedValue(col, type, row) {
    propose(row, col.getName(), type);
  }

  /**
   * At the same time the proposed value is set (as a baja.Type) we also set
   * the corresponding `MgrTypeInfo` on the row, so we can use it to create
   * a new instance.
   *
   * @param {module:nmodule/webEditors/rc/wb/mgr/MgrTypeInfo} typeInfo
   * @param {module:nmodule/webEditors/rc/wb/table/model/Row} row
   */
  function setProposedMgrTypeInfo(typeInfo, row) {
    row.data(SELECTED_TYPE_KEY, typeInfo);
  }

  /**
   * Set the proposed value from a `baja.DynamicEnum`. This is used in the case
   * where the batch component editor displays the intersection enum created
   * in `coalesceRows()`.
   *
   * @param {module:nmodule/webEditors/rc/wb/mgr/model/columns/TypeMgrColumn} col
   * @param {baja.DynamicEnum} value
   * @param {module:nmodule/webEditors/rc/wb/table/model/Row} row
   * @returns {Promise}
   */
  function proposeFromDynamicEnum(col, value, row) {
    var intersection = row.data(INTERSECTED_TYPES_KEY),
        typeInfo = intersection && intersection[value.getOrdinal()],
        type = typeInfo && typeInfo.getType();

    if (typeInfo && type) {
      setProposedValue(col, type, row);
      setProposedMgrTypeInfo(typeInfo, row);
    }

    return Promise.resolve();
  }

  /**
   * Set the proposed value from a `baja.Type` value.
   *
   * @param {module:nmodule/webEditors/rc/wb/mgr/model/columns/TypeMgrColumn} col
   * @param {baja.Type} value
   * @param {module:nmodule/webEditors/rc/wb/table/model/Row} row
   * @returns {Promise}
   */
  function proposeFromBajaType(col, value, row) {
    setProposedValue(col, value, row);
    return MgrTypeInfo.make({ from: value })
      .then(function (typeInfo) {
        setProposedMgrTypeInfo(typeInfo, row);
      });
  }

  /**
   * Set the proposed value from a `MgrTypeInfo`.
   *
   * @param {module:nmodule/webEditors/rc/wb/mgr/model/columns/TypeMgrColumn} col
   * @param {module:nmodule/webEditors/rc/wb/mgr/MgrTypeInfo} value
   * @param {module:nmodule/webEditors/rc/wb/table/model/Row} row
   * @returns {Promise}
   */
  function proposeFromMgrTypeInfo(col, value, row) {
    setProposedValue(col, value.getType(), row);
    setProposedMgrTypeInfo(value, row);

    return Promise.resolve();
  }

  /**
   * Set the proposed value for the `Row`. This will normally be the DynamicEnum
   * created by the `coalesceRows()` function.
   *
   * @param {*} value
   * @param {module:nmodule/webEditors/rc/wb/table/model/Row} row
   * @returns {Promise}
   */
  TypeMgrColumn.prototype.propose = function (value, row) {
    var that = this,
        prom;

    if (value instanceof baja.DynamicEnum) {
      prom = proposeFromDynamicEnum(this, value, row);
    } else if (value instanceof MgrTypeInfo) {
      prom = proposeFromMgrTypeInfo(this, value, row);
    } else if (typeof value.getTypeSpec === 'function') {
      prom = proposeFromBajaType(this, value, row);
    }

    if (prom) {
      return prom.then(function () {
        return that.changeRowType(row);
      });
    }

    return Promise.reject(new Error('Invalid proposed value for type column'));
  };

  /**
   * Function to set the types that can be created for a new instance in
   * the database. This will be called by the `AddCommand` for the types
   * that can be created from a particular discovered item, and from the
   * `NewCommand` for all the types that can be created by the manager.
   *
   * This is intended for internal use by the manager framework only.
   *
   * @private
   * @static
   *
   * @param {module:nmodule/webEditors/rc/wb/table/model/Row} row
   * @param {Array.<module:nmodule/webEditors/rc/wb/mgr/MgrTypeInfo>} typeInfos
   */
  TypeMgrColumn.setAvailableTypes = function (row, typeInfos) {
    row.data(AVAILABLE_TYPES_KEY, typeInfos);
  };

  /**
   * Get the available types for the given row.
   *
   * This is intended for internal use by the manager framework only.
   *
   * @private
   * @static
   *
   * @param {module:nmodule/webEditors/rc/wb/table/model/Row} row
   * @returns {Array.<module:nmodule/webEditors/rc/wb/mgr/MgrTypeInfo>}
   */
  TypeMgrColumn.getAvailableTypes = function (row) {
    return row.data(AVAILABLE_TYPES_KEY) || [];
  };

  /**
   * Returns the icon for the currently selected type, or undefined if
   * there is no current selection.
   *
   * This is intended for internal use by the manager framework only.
   *
   * @private
   * @static
   *
   * @param {module:nmodule/webEditors/rc/wb/table/model/Row} row
   * @returns {baja.Icon}
   */
  TypeMgrColumn.getIconForSelectedType = function (row) {
    var selection = row.data(SELECTED_TYPE_KEY);
    return selection ? selection.getType().getIcon() : undefined;
  };

  /**
   * Function called by the `AddCommand` to make the discovery data available
   * to the type column. This is needed when changing the type, to give the
   * manager a chance to update the proposed values from the discovery object
   * when the type is changed by the user.
   *
   * This is intended for internal use by the manager framework only.
   *
   * @private
   * @static
   *
   * @param {module:nmodule/webEditors/rc/wb/table/model/Row} row
   * @param {Object} discovery
   */
  TypeMgrColumn.setDiscoveryData = function (row, discovery) {
    row.data(DISCOVERY_DATA_KEY, discovery);
  };

  /**
   * Remove the proposed but uncommitted values when the row's type is changed. This
   * will leave any user typed value for the name manager column alone.
   *
   * @param {module:nmodule/webEditors/rc/wb/table/model/Row} row
   */
  function deleteProposedValues(row) {

    const keys = getProposalKeys(row);
    for (var i = 0; i < keys.length; i++) {
      if (keys[i]) {
        if ((keys[i] !== '__name') || (!hasUserDefinedNameValue(row))) {
          clearProposal(row, keys[i]);
        }
      }
    }
  }

  function tryProposeNewName(model, row, name) {
    var nameCol = _.find(model.getColumns(), function (c) {
      return c instanceof NameMgrColumn;
    });

    if (nameCol) {
      return nameCol.propose(name, row);
    }
  }

  /**
   * Change the type of the row to the selected type. This will remove
   * any previously proposed values (apart from the name), and create
   * a new instance of the selected type. The manager will then be given
   * a chance to propose values again from the discovery object.
   *
   * This is intended for internal use by the manager framework only.
   *
   * @private
   *
   * @param {module:nmodule/webEditors/rc/wb/table/model/Row} row
   * @returns {Promise}
   */
  TypeMgrColumn.prototype.changeRowType = function (row) {
    var that = this,
        mgr = that.getManager(),
        mgrModel = mgr.getModel(),
        typeInfo = row.data(SELECTED_TYPE_KEY),
        comp = row.getSubject(),
        newComponent,
        prop = comp.getPropertyInParent(),
        source = comp.getParent();

    if (typeInfo) {

      // Remove any previously proposed data, except for a user chosen
      // name.

      deleteProposedValues(row);

      // Let the manager model create the new instance, setting any
      // property values that it requires (e.g. a proxy ext). We then
      // need to change the row's subject to the new instance, and replace
      // the component in the container with the instance of the new type.

      return that.newInstance(mgrModel, row)
        .then(function (instance) {
          var name = prop.getName();
          const instanceType = instance.getType();

          newComponent = instance;
          row.$setSubject(newComponent);

          if (!hasUserDefinedNameValue(row) && !hasDiscoveryName(row)) {
            name = getUniqueSlotName(source, instanceType);
            return tryProposeNewName(mgrModel, row, name);
          }
        })
        .then(function () {
          return source.set({
            slot: prop.getName(),
            value: newComponent
          });
        })
        .then(function () {
          var discovery = row.data(DISCOVERY_DATA_KEY);
          if (discovery) {

            // If discovery data was set against the row, give the manager
            // a chance to set new proposals for the discovery object and the
            // newly chosen type.

            return setProposedDiscoveryValues(mgr, mgrModel, discovery, row);
          }
        });
    }

    return Promise.resolve();
  };

  /**
   * Return a new instance for the selected type.
   *
   * @param {module:nmodule/webEditors/rc/wb/mgr/model/MgrModel} mgrModel
   * @param {module:nmodule/webEditors/rc/wb/table/model/Row} row
   * @returns {Promise.<baja.Value>}
   */
  TypeMgrColumn.prototype.newInstance = function (mgrModel, row) {
    return mgrModel.newInstance(row.data(SELECTED_TYPE_KEY));
  };

  /**
   * Return the icon to be used for the column in the batch component editor.
   * @returns {baja.Icon}
   */
  TypeMgrColumn.prototype.getColumnIcon = function () {
    return OBJECT_ICON;
  };

  return (TypeMgrColumn);
});