wb/mgr/model/MgrModel.js

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

/**
 * @module nmodule/webEditors/rc/wb/mgr/model/MgrModel
 */
define([ 'baja!',
        'Promise',
        'underscore',
        'nmodule/webEditors/rc/wb/mgr/MgrTypeInfo',
        'nmodule/webEditors/rc/wb/table/model/ComponentTableModel' ], function (
         baja,
         Promise,
         _,
         MgrTypeInfo,
         ComponentTableModel) {

  'use strict';

  function checkNewTypes(newTypes) {
    _.each(newTypes, function (typeInfo) {
      if (!(typeInfo instanceof MgrTypeInfo)) {
        throw new Error('new type is not a MgrTypeInfo instance');
      }
    });
  }

  /**
   * If component is parented, unparent it and return its old property name.
   *
   * @inner
   * @param {baja.Complex} comp
   * @param {baja.comm.Batch} batch
   * @returns {Promise}
   */
  function unparentAndGetPropName(comp, batch) {
    const parent = comp.getParent();
    const prop = comp.getPropertyInParent();

    return Promise.resolve(parent && parent.remove({ slot: prop, batch: batch }))
      .then(function () {
        return prop && String(prop);
      });
  }

  /**
   * API Status: **Development**
   *
   * A layer that adds edit/add/remove functionality on top of a
   * `ComponentTableModel`.
   *
   * Due to the incubating status of the manager framework, it is *not
   * recommended* that you extend `MgrModel` directly. Instead, extend
   * `DeviceMgrModel` or `PointMgrModel`: these will provide more robust
   * functionality for most use cases.
   *
   * @class
   * @alias module:nmodule/webEditors/rc/wb/mgr/model/MgrModel
   * @extends module:nmodule/webEditors/rc/wb/table/model/ComponentTableModel
   * @param {Object} params
   * @param {Array.<module:nmodule/webEditors/rc/wb/mgr/MgrTypeInfo>} [params.newTypes]
   * array of `MgrTypeInfo` instances representing types that will be offered when creating new
   * instances for this model.
   * @see module:nmodule/driver/rc/wb/mgr/DeviceMgrModel
   * @see module:nmodule/driver/rc/wb/mgr/PointMgrModel
   */
  const MgrModel = function MgrModel(params) {
    ComponentTableModel.apply(this, arguments);

    const newTypes = params.newTypes || [];

    checkNewTypes(newTypes);
    this.$newTypes = newTypes;
  };
  MgrModel.prototype = Object.create(ComponentTableModel.prototype);
  MgrModel.prototype.constructor = MgrModel;

  /**
   * @private
   * @param {function} Ctor a constructor for a MgrModel subclass
   * @param {object} params params to be passed to the constructor
   * @param {function} processRowAsync an async function that takes a Row and performs async
   * processing on it
   * @returns {Promise.<module:nmodule/webEditors/rc/wb/mgr/model/MgrModel>}
   */
  MgrModel.$makeAsync = function (Ctor, params, processRowAsync) {
    const mgr = new Ctor(Object.assign({ rows: [] }, params));

    // referenced in ComponentTableModel
    mgr.$processRowAsync = processRowAsync;

    const insertRows = mgr.insertRows;

    mgr.insertRows = function (rows, ...args) {
      return Promise.all(rows.map((row) => {
        row = this.makeRow(row);
        return processRowAsync(row)
          .then(() => row);
      }))
        .then((rows) => insertRows.call(this, rows, ...args));
    };

    return mgr.insertRows(mgr.getComponentSource().getComponents())
      .then(() => mgr);
  };

  /**
   * Clean up the `MgrModel` when the parent `Manager` is being destroyed. This is
   * an opportunity to unhook any remaining event handlers on the model or its data source.
   *
   * @returns Promise.<*>
   */
  MgrModel.prototype.destroy = function () {
    const componentSource = this.getComponentSource();

    //TODO: proper way of removing column event handlers
    this.removeAllListeners('columnsFlagsChanged');

    if (componentSource) { componentSource.destroy(); }

    return this.removeColumns(this.getColumns());
  };

  /**
   * Get the columns contained in this `MgrModel`.
   *
   * @override
   * @function getColumns
   * @memberOf module:nmodule/webEditors/rc/wb/mgr/model/MgrModel
   * @param {Number} [flags] if given, only return columns that have these
   * flags.
   * @returns {Array.<module:nmodule/webEditors/rc/wb/mgr/model/MgrColumn>}
   */

  /**
   * Get the `MgrTypeInfo` instances representing the types that are acceptable to add to
   * this model.
   *
   * @returns {Array.<module:nmodule/webEditors/rc/wb/mgr/MgrTypeInfo>} array of MgrTypeInfos to add
   */
  MgrModel.prototype.getNewTypes = function () {
    return this.$newTypes;
  };

  /**
   * Override point to customize how new instances of the selected `MgrTypeInfo`
   * are instantiated. The default implementation is to simply delegate to the type
   * info's #newInstance() function.
   *
   * @param {module:nmodule/webEditors/rc/wb/mgr/MgrTypeInfo} typeInfo
   * @returns {baja.Value|Promise}
   */
  MgrModel.prototype.newInstance = function (typeInfo) {
    return typeInfo.newInstance();
  };

  /**
   * Override point to customize how `count` number of new instances of a selected
   * `MgrTypeInfo` are instantiated. The default implementation is to simply delegate to
   * the model's #newInstance() method.
   *
   * @since Niagara 4.14
   * @param {Object} params the parameters to be used to create new instances
   * @param {module:nmodule/webEditors/rc/wb/mgr/MgrTypeInfo} params.typeInfo the `MgrTypeInfo`
   * representing the type of the components we're creating
   * @param {Number} params.count how many new components to create
   * @returns {Promise.<Array.<baja.Value>>}
   */
  MgrModel.prototype.newInstances = function (params) {
    const that = this;
    const count = params.count;
    const typeInfo = params.typeInfo;

    return Promise.all(Array(count).fill().map(() => that.newInstance(typeInfo)));
  };

  /**
   * Add the instances to this model, to be called when an add dialog or
   * other add operation is committed. The default implementation is to add the
   * given instances to the underlying `ComponentSource` container, which will
   * commit these changes up to the station.
   *
   * Like Workbench, parented instances will be unparented and re-added to the
   * container component. (Workbench does this via a Mark; here it will be
   * done using BajaScript remove/add calls.)
   *
   * @param {Array.<baja.Component>} instances the instances to add
   * @param {Array.<String>} [names] the desired slot names for the instances.
   * If omitted, default names derived from the instance Types will be used.
   * @returns {Promise.<Array.<baja.Component>>} as of Niagara 4.13, this will
   * resolve with an array of the components that were successfully added. Or:
   * rejected if the add fails.
   */
  MgrModel.prototype.addInstances = function (instances, names) {
    names = names || [];

    const source = this.getComponentSource();
    const batch = new baja.comm.Batch();
    const container = source.getContainer();
    const parent = _.first(instances).getParent();
    const displayNamesMap = parent && parent.get('displayNames');
    const getDisplayNames = (slotNames) => {
      return _.map(instances, (instance, i) => {
        return displayNamesMap && displayNamesMap.get(slotNames[i]);
      });
    };
    const getNames = _.map(instances, function (instance, i) {
      return unparentAndGetPropName(instance, batch)
        .then(function (prop) {
          return names[i] || prop;
        });
    });

    batch.commit();

    let addedComponents;
    return Promise.all(getNames)
      .then((names) => {
        return source.addComponents(instances, names)
          .then((results) => {
            addedComponents = results;
            // Get the effective slot names after they are added
            const compNames = results.map((comp) => comp.getName());
            const newDisplayNames = getDisplayNames(compNames);
            return container.setDisplayName({ slot: compNames, newDisplayName: newDisplayNames });
          });
      })
      .then(() => {
        return addedComponents;
      });
  };

  /**
   * Override point, that allows the post-processing of the edited row(s) after they have been
   * posted to the server.
   * @since Niagara 4.14
   * @param {Array.<baja.Component>} instances the instances that were edited
   * @returns {Promise<Array.<baja.Component>|undefined>}
   */
  MgrModel.prototype.postEdit = function (instances) {
    return Promise.resolve(instances);
  };

  /**
   * Override point, that allows the post-processing of the matched row(s) after they have been
   * posted to the server.
   * @since Niagara 4.14
   * @param {Array.<baja.Component>} instances the instances that were matched
   * @returns {Promise<Array.<baja.Component>|undefined>}
   */
  MgrModel.prototype.postMatch = function (instances) {
    return Promise.resolve(instances);
  };

  return (MgrModel);
});