wb/mgr/model/MgrColumn.js

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

/**
 * @module nmodule/webEditors/rc/wb/mgr/model/MgrColumn
 */
define([
  'baja!',
  'log!nmodule.webEditors.rc.wb.mgr.model.MgrColumn',
  'Promise',
  'nmodule/webEditors/rc/fe/baja/util/typeUtils',
  'nmodule/webEditors/rc/wb/mgr/mgrUtils',
  'nmodule/webEditors/rc/wb/table/model/Column' ], function (
  baja,
  log,
  Promise,
  typeUtils,
  mgrUtils,
  Column) {

  'use strict';

  const { propose, getProposedValue } = mgrUtils;
  const { allSameType } = typeUtils;
  const logFine = log.fine.bind(log);

  /**
   * API Status: **Development**
   *
   * Column for use in a `Manager` workflow. It functions both as a vanilla
   * `TableModel` column, and as a defined field for batch editing rows in a
   * manager view.
   *
   * A `MgrColumn` provides for the following workflow:
   *
   * - Identify a number of components to edit at once.
   * - Coalesce corresponding values from those components into a single value.
   * - Load the coalesced value into a single editor for editing.
   * - Commit the entered value back to the edited components.
   *
   * @class
   * @alias module:nmodule/webEditors/rc/wb/mgr/model/MgrColumn
   * @extends module:nmodule/webEditors/rc/wb/table/model/Column
   */
  const MgrColumn = function MgrColumn() {
    Column.apply(this, arguments);
    this.data('context', {});
  };
  MgrColumn.prototype = Object.create(Column.prototype);
  MgrColumn.prototype.constructor = MgrColumn;

  MgrColumn.COMMIT_READY = 'commitReady';

  /**
   * Applies `MgrColumn` functionality to an arbitrary `Column` subclass.
   *
   * @param {Function} Ctor
   */
  MgrColumn.mixin = function (Ctor) {
    const prot = Ctor.prototype;
    const mprot = MgrColumn.prototype;
    prot.coalesceRows = mprot.coalesceRows;
    prot.getConfigFor = mprot.getConfigFor;
    prot.getProposedValueFor = mprot.getProposedValueFor;
    prot.isEditorSuitable = mprot.isEditorSuitable;
    prot.mgrValidate = mprot.mgrValidate;
    prot.propose = mprot.propose;
    prot.commit = mprot.commit;
    prot.buildCell = mprot.buildCell;
  };

  /**
   * Set the manager for this column. This is for internal framework use.
   *
   * @private
   * @param {module:nmodule/webEditors/rc/wb/mgr/Manager} mgr
   */
  MgrColumn.prototype.$init = function (mgr) {
    this.$mgr = mgr;
  };

  /**
   * Creates the cell's contents by calling `toString` on the row's proposed value
   * or the current value if there is no proposal. HTML will be safely escaped.
   *
   * @param {module:nmodule/webEditors/rc/wb/table/model/Row} row
   * @param {JQuery} dom
   * @returns {Promise}
   */
  MgrColumn.prototype.buildCell = function (row, dom) {
    return Promise.try(() => {
      const value = this.getProposedValueFor(row);
      const context = this.$getContext(row);
      const str = baja.hasType(value) && context ? value.toString(context) : String(value);
      return Promise.resolve(str).then((str) => dom.text(str));
    })
      .catch((err) => {
        logFine('Unable to build cell', err);
        dom.text('');
      });
  };

  /**
   * Given the set of rows to be edited, coalesce their values into one single
   * value to load into an editor.
   *
   * By default, this will simply read the proposed value from the first row.
   * This is appropriate for a use case where one value will be entered and
   * written back to all edited components.
   *
   * If editing one value for all the given rows is not a use case supported by
   * your column (a good example is a Name column, because no two components can
   * share the same name), throw an error.
   *
   * @param {Array.<module:nmodule/webEditors/rc/wb/table/model/Row>} rows
   * @returns {*} value coalesced from the given rows
   * @throws {Error} if rows array not given, or values from rows are not all
   * of the same type
   */
  MgrColumn.prototype.coalesceRows = function (rows) {
    if (!Array.isArray(rows) || !rows.length) {
      throw new Error('rows array required');
    }

    const values = rows.map((row) => this.getProposedValueFor(row));

    if (!allSameType(values)) {
      throw new Error('values from rows are of differing Types');
    }

    return values[0];
  };

  /**
   * After coalescing the selected rows into a single value, calculate a
   * config object to be given to `fe.makeFor` that will determine how the
   * editor will be built to edit that value.
   *
   * This function will typically not be called directly but serves as an
   * override point. By default, it will simply get the coalesced value from
   * those rows and have `fe.makeFor` build the default editor for that value.
   * Note that this means if the coalesced value is a non-Baja value, like an
   * array, this function *must* be overridden.
   *
   * @param {Array.<module:nmodule/webEditors/rc/wb/table/model/Row>} rows
   * @returns {Object} configuration object to be given to `fe.makeFor`
   */
  MgrColumn.prototype.getConfigFor = function (rows) {
    return { value: this.coalesceRows(rows) };
  };

  /**
   * If an editor has already been built, it may be possible to reuse it,
   * simply loading in a new coalesced value rather than destroying and
   * rebuilding the existing editor.
   *
   * This function should return true if the editor is suitable to be reused
   * for the given rows. By default, will always return true.
   *
   * @param {module:nmodule/webEditors/rc/fe/baja/BaseEditor} editor
   * @param {Array.<module:nmodule/webEditors/rc/wb/table/model/Row>} rows
   * @returns {Boolean}
   */
  MgrColumn.prototype.isEditorSuitable = function (editor, rows) {
    return true;
  };

  /**
   * Allows this column to validate proposed changes.
   *
   * @param {module:nmodule/webEditors/rc/wb/mgr/model/MgrModel} model
   * the model to which we're about to apply changes.
   * @param {Array} data an array of proposed changes to this column, one per
   * row in the `MgrModel`. If a value in this array is null, no change has
   * been proposed for that row.
   * @param {Object} [params]
   * @param {module:nmodule/webEditors/rc/fe/baja/BaseEditor} [params.editor]
   * the editor from which the proposed values were read. Note that the editor
   * may have been used to edit other rows, so the editor's current value may
   * not match the proposed new values.
   * @returns {Promise} promise that resolves by default
   *
   * @example
   * <caption>Validating this column may require that I examine the changes I'm
   * about to make to other columns as well.</caption>
   *
   * MyMgrColumn.prototype.mgrValidate = function (model, data, params) {
   *   var that = this,
   *       rows = model.getRows(),
   *       otherColumn = model.getColumn('otherColumn');
   *
   *   //search through all MgrModel rows, and check to see that my proposed
   *   //change is compatible with the proposed change to another column.
   *   //say, i'm a "password" column, and the other column is a "password
   *   //scheme" column - i need to make sure that the proposed password is
   *   //considered valid by the proposed password scheme.
   *
   *   for (var i = 0; i < rows.length; i++) {
   *     var row = rows[i],
   *         myValue = data[i],
   *         otherValue = otherColumn.getProposedValueFor(row);
   *
   *     if (myValue === null) {
   *       //no changes proposed for this row, so nothing to validate.
   *     }
   *
   *     if (!isCompatible(myValue, otherValue)) {
   *       return Promise.reject(new Error('incompatible values'));
   *     }
   *   }
   * };
   */
  MgrColumn.prototype.mgrValidate = function (model, data, params) {
    return Promise.resolve();
  };

  /**
   * Should read the value and "tentatively" apply it to the
   * selected row. In most cases this will be setting some temporary data
   * for display-only purposes.
   *
   * By default, will set some temporary data on the row using the column's
   * name as a key.
   *
   * @param {*} value
   * @param {module:nmodule/webEditors/rc/wb/table/model/Row} row
   * @returns {Promise}
   */
  MgrColumn.prototype.propose = function (value, row) {

    propose(row, this.getName(), value);
    return Promise.resolve();
  };

  /**
   * Get the currently proposed value for the given row. If no value proposed
   * yet, will return the actual column value (`getValueFor`).
   *
   * @param {module:nmodule/webEditors/rc/wb/table/model/Row} row
   * @returns {*}
   */
  MgrColumn.prototype.getProposedValueFor = function (row) {
    let proposed = getProposedValue(row, this.getName());
    if (proposed === undefined) {
      proposed = this.getValueFor(row);
    }
    return proposed;
  };

  /**
   * Should read the value and "officially" apply it back to the selected rows.
   * If `params.batch` is received, then `params.progressCallback` _must_ be
   * called with `MgrColumn.COMMIT_READY` when this function is done using that
   * batch (even if no network calls are added to it).
   *
   * Note: sometimes you may want to abort the entire process of saving changes
   * to the Manager, for instance, if one of your columns requires the user to
   * confirm a dialog before committing. Returning a Promise that rejects will
   * show an error dialog to the user, which may not be what you want if you've
   * already shown a dialog. Another option is to return a Promise that never
   * resolves or rejects, which will drop the user back at the edit screen
   * without committing any changes (all commit calls must resolve for any
   * changes to post to the station). A more explicit API for this may be
   * provided in the future.
   *
   * @param {*} value the proposed value to commit to the row
   * @param {module:nmodule/webEditors/rc/wb/table/model/Row} row
   * @param {Object} [params]
   * @param {module:nmodule/webEditors/rc/fe/baja/BaseEditor} [params.editor]
   * the editor from which the value was read. If the column is not editable,
   * this parameter will be `undefined`, as no editor will have been created for
   * the value. This situation may occur when a value obtained via discovery is
   * set on row for a non-editable column.
   * @param {baja.comm.Batch} [params.batch] a batch to use to commit changes
   * up to the station
   * @param {Function} [params.progressCallback] call this with
   * `MgrColumn.COMMIT_READY` when this function is done adding network calls to
   * the batch.
   * @returns {Promise}
   */
  MgrColumn.prototype.commit = function (value, row, params) {
    const progressCallback = params && params.progressCallback;
    if (progressCallback) {
      progressCallback(MgrColumn.COMMIT_READY);
    }
    return Promise.resolve();
  };

  /**
   * Returns the manager associated with this column.
   *
   * @private
   * @returns {module:nmodule/webEditors/rc/wb/mgr/Manager}
   */
  MgrColumn.prototype.getManager = function () {
    return this.$mgr;
  };

  return MgrColumn;
});