wb/mgr/model/columns/PropertyPathMgrColumn.js

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

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

define([ 'underscore',
        'Promise',
        'nmodule/webEditors/rc/wb/table/model/Column',
        'nmodule/webEditors/rc/wb/mgr/model/MgrColumn',
        'nmodule/webEditors/rc/fe/baja/util/facetsUtils',
        'nmodule/webEditors/rc/fe/baja/util/typeUtils' ], function (
        _,
        Promise,
        Column,
        MgrColumn,
        facetsUtils,
        typeUtils) {

  'use strict';

  const { getComplexAndSlotFromPath } = require('bajaScript/baja/comp/compUtil');

  const { extend, last, initial, toArray } = _;
  const { toProperties } = facetsUtils;
  const { getSlotDisplayName, getSlotFacets, getSlotType, isComplex } = typeUtils;

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

  /**
   * API Status: **Development**
   *
   * Column for a property several levels deep relative to the row's subject component.
   * The constructor is passed a '/' delimited string specifying a property path.
   *
   * @class
   * @alias module:nmodule/webEditors/rc/wb/mgr/model/columns/PropertyPathMgrColumn
   * @mixes module:nmodule/webEditors/rc/wb/mgr/model/MgrColumn
   * @extends module:nmodule/webEditors/rc/wb/table/model/Column
   * @param {String} name Optional A name for this column (if not provided, will be calculated)
   * @param {String} path A slot path, specified as a '/' delimited string
   * @param {Object} [params]
   * @param {Type} [params.type] The type declaring the target slot, used to
   * obtain the slot facets.
   * @param {function} [params.getDefaultValue] since Niagara 4.14, if the Complex containing the last
   * property in the path does not actually contain that property, this function will be used to
   * retrieve a default value. When committing, that default value will be newly added. It will
   * receive the row's `Complex` instance to calculate a default value, if desired.
   */
  var PropertyPathMgrColumn = function PropertyPathMgrColumn(name, path, params) {
    var args;

    // The 1st parameter is optional, so we need to decide whether we're using the 2 or 3 parameter constructor.

    // 3rd parameter present, therefore should be the params object, so all parameters are as supplied.

    // otherwise, 3rd parameter not present
    if (!params) {
      // If the 2nd parameter isn't present either,
      // or the 2nd param is an Object (ie the params object),
      // that means we're using the the 2 parameter constructor (ie path, params as opposed to name, path, params)
      // so treat params as path, and path as name
      if (!path || typeof path === 'object') {
        params = path;
        path = name;
        name = null;
      }
    }

    this.$path = path.split('/');

    if (params) {
      const { getDefaultValue, type } = params;
      if (type) { this.$type = type; }
      if (getDefaultValue) { this.$getDefaultValue = getDefaultValue; }
    }

    // use the 'name' parameter if supplied, otherwise extract the final slot's
    // name from the path, so it can be used for the 'name' parameter we pass
    // to the super constructor, along with the other arguments we received.
    // We also need to adjust the arguments array, and make sure the
    // params object is passed as the second argument.

    args = toArray(arguments);
    args.splice(0, name ? 2 : 1, name || this.getPropertyName());

    Column.apply(this, args);
  };

  PropertyPathMgrColumn.prototype = Object.create(Column.prototype);
  PropertyPathMgrColumn.prototype.constructor = PropertyPathMgrColumn;

  /**
   * Get the value of this column for the given row. This will follow the slot
   * path to obtain the value of the slot descended from the row's subject.
   *
   * @param row
   * @returns {*}
   */
  PropertyPathMgrColumn.prototype.getValueFor = function (row) {
    if (!isComplex(row.getSubject())) {
      throw new Error('Complex required');
    }

    const container = this.getComplexFromPath(row);
    const prop = this.getPropertyName();

    if (container.has(prop)) {
      return container.get(prop);
    } else {
      const getDefaultValue = this.$getDefaultValue;
      if (getDefaultValue) {
        return getDefaultValue(container);
      }
    }

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

  MgrColumn.mixin(PropertyPathMgrColumn);

  /**
   * @private
   * @param {module:nmodule/webEditors/rc/wb/table/model/Row} row
   * @returns {*}
   */
  PropertyPathMgrColumn.prototype.$getContext = function (row) {
    const cx = this.data('context');
    if (cx === null) {
      return undefined;
    }

    const container = this.getComplexFromPath(row);
    const propFacets = container.getFacets(this.getPropertyName());
    const facets = propFacets ? propFacets.toObject() : {};
    return extend(facets, cx);
  };

  /**
   * If editing only one row, then the facets used to configure the field
   * editor will be taken from the component instance containing the final
   * property in the property path.
   *
   * If editing multiple rows, the facets will be derived from the specified
   * Type and the frozen slot at the end of the property path. If these are
   * not present, no facets will be used.
   *
   * @param {Array.<module:nmodule/webEditors/rc/wb/table/model/Row>} rows
   * @returns {Object} config object for `fe.makeFor`, which might include facets
   * if the path's last element had a type declared.
   */
  PropertyPathMgrColumn.prototype.getConfigFor = function (rows) {
    var value = this.coalesceRows(rows),
        name = this.getPropertyName(),
        type = this.$type,
        facets;

    //c.f. javax.baja.workbench.mgr.MgrColumn.PropPath#toEditor
    if (rows.length === 1) {
      var comp = this.getComplexFromPath(rows[0]);
      facets = comp && comp.getFacets(name);
    } else {
      try {
        facets = type && name && getSlotFacets(type, name);
      } catch (ignore) {
        //dynamic slot so could not retrieve slot facets
      }
    }

    return { value: value, properties: toProperties(facets), formFactor: 'mini' };
  };

  /**
   * Sets/adds the property on the slot resolved from the path. This requires the previous slots
   * in the path to exist.
   *
   * @param {baja.Value} value
   * @param {module:nmodule/webEditors/rc/wb/table/model/Row} row
   * @param {Object} [params]
   * @param {baja.comm.Batch} [params.batch]
   * @returns {Promise} promise to be resolved when the value has
   * been set on the slot resolved from the path.
   */
  PropertyPathMgrColumn.prototype.commit = function (value, row, params) {
    var name = this.getPropertyName(),
      comp = this.getComplexFromPath(row),
      batch = params && params.batch,
      progressCallback = params && params.progressCallback,
      promise;

    promise = comp && comp[comp.has(name) ? 'set' : 'add']({
      slot: name,
      value: value,
      batch: batch
    });

    if (progressCallback) { progressCallback(MgrColumn.COMMIT_READY); }

    return promise;
  };

  /**
   * Resolves the display name of the property slot at the end of the path.
   *
   * @returns {Promise} promise to be resolved with the display name for the final slot.
   */
  PropertyPathMgrColumn.prototype.toDisplayName = function () {
    var name = this.getPropertyName();

    if (this.$displayName) {
      return Promise.resolve(this.$displayName);
    } else {
      return Promise.resolve(this.$type ? getSlotDisplayName(this.$type, name)
                                        : String(name));
    }
  };

  /**
   * Get the icon URI for the type of slot in the last component of the path.
   * Returns null if the URI could not be obtained because the slot's declaring
   * type was not specified in the path.
   *
   * @returns {String} The URI for the icon for the target slot.
   */
  PropertyPathMgrColumn.prototype.getColumnIcon = function () {
    var prop = this.getPropertyName();

    if (this.$type) {
      try {
        var slotType = getSlotType(this.$type, prop);
        return slotType ? slotType.getIcon().getImageUris()[0] : null;
      } catch (ignore) {}
    }

    return null;
  };

  /**
   * Get the property name for this column.
   * @since Niagara 4.8
   *
   * @returns String
   */
  PropertyPathMgrColumn.prototype.getPropertyName = function () {
    return last(this.$path);
  };

  /**
   * Get the complex this property is on.
   * @since Niagara 4.8
   *
   * @param {module:nmodule/webEditors/rc/wb/table/model/Row} row
   * @returns {baja.Complex}
   */
  PropertyPathMgrColumn.prototype.getComplexFromPath = function (row) {
    return PropertyPathMgrColumn.getPropValueFromPath(row.getSubject(),
      initial(this.$path)); // use everything but the last entry of the $path array
  };

  /**
   * Take a subject component (e.g. a control point) and an array of slot
   * names representing a slot path (e.g leading to a property on its point ext),
   * and resolve the descendants of the subject using the property names.
   *
   * @param {baja.Complex} comp
   * @param {Array.<baja.Slot|string>} path
   * @returns {baja.Value}
   * @since Niagara 4.13
   */
  PropertyPathMgrColumn.getPropValueFromPath = function (comp, path) {
    if (!path.length) {
      return comp;
    }
    const { complex, slot } = getComplexAndSlotFromPath(comp, path);
    return complex.get(slot);
  };

  return (PropertyPathMgrColumn);
});