/**
* @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);
});