/**
* @copyright 2015 Tridium, Inc. All Rights Reserved.
* @author Logan Byam
*/
/**
* @module nmodule/webEditors/rc/wb/table/model/ComponentTableModel
*/
define([ 'baja!',
'log!nmodule.webEditors.rc.wb.table.model.ComponentTableModel',
'Promise',
'underscore',
'nmodule/js/rc/asyncUtils/promiseMux',
'nmodule/webEditors/rc/fe/baja/util/typeUtils',
'nmodule/webEditors/rc/wb/table/model/ComponentSource',
'nmodule/webEditors/rc/wb/table/model/Row',
'nmodule/webEditors/rc/wb/table/model/TableModel',
'nmodule/webEditors/rc/wb/table/model/columns/PropertyColumn',
'nmodule/webEditors/rc/wb/table/model/source/ContainerComponentSource' ], function (
baja,
log,
Promise,
_,
promiseMux,
typeUtils,
ComponentSource,
Row,
TableModel,
PropertyColumn,
ContainerComponentSource) {
'use strict';
const { find, noop, throttle, uniq, without } = _;
const { getSlotNames, isComponent } = typeUtils;
const logError = log.severe.bind(log);
const DEFAULT_CHANGE_EVENT_THROTTLE_MS = 250;
/**
* API Status: **Development**
*
* Table model where each row in the table represents a `Component`.
*
* A `ComponentTableModel` is backed by a `ComponentSource`, which provides
* the list of `Components` to build into table rows.
*
* @class
* @extends module:nmodule/webEditors/rc/wb/table/model/TableModel
* @alias module:nmodule/webEditors/rc/wb/table/model/ComponentTableModel
* @param {Object|baja.Component} params parameters object, or a `Component`
* if no parameters required.
* @param {baja.Component|module:nmodule/webEditors/rc/wb/table/model/ComponentSource} params.componentSource
* the source of components to build into table rows.
* If a `Component` is given it will just be wrapped in a `ComponentSource`
* with no parameters.
* @param {Array.<module:nmodule/webEditors/rc/wb/table/model/Column>} params.columns
* @param {Array.<module:nmodule/webEditors/rc/wb/table/model/Row>|Array.<baja.Component>} [params.rows=params.componentSource.getComponents()] optionally,
* specify the TableModel's initial set of rows. By default, the rows will be all the components
* contained within the ComponentSource.
*/
const ComponentTableModel = function ComponentTableModel(params) {
params = baja.objectify(params, 'componentSource');
const { columns, rows } = params;
let { componentSource, rowsChangedEventDelay } = params;
if (isComponent(componentSource)) {
componentSource = new ContainerComponentSource(componentSource);
}
if (!(componentSource instanceof ComponentSource)) {
throw new Error('Component or ComponentSource required');
}
if (rowsChangedEventDelay === undefined) {
rowsChangedEventDelay = DEFAULT_CHANGE_EVENT_THROTTLE_MS;
}
this.$componentSource = componentSource;
this.$rowsChangedEventDelay = rowsChangedEventDelay;
this.$emitThrottledRowsChangedEvent = promiseMux({
exec: (rowArguments) => {
const comps = _.map(rowArguments, (rowArg) => rowArg.comp);
const allRows = this.$getRowsUnsafe();
const existingRows = [];
// may be assigned from an async MgrModel
const processRow = this.$processRowAsync || noop;
uniq(comps).forEach((comp) => {
const existingRow = find(allRows, (row) => row.getSubject() === comp);
if (existingRow) {
existingRows.push(existingRow);
}
});
if (!existingRows.length) {
return comps;
}
return Promise.all(existingRows.map(processRow))
.then(() => {
this.emit('rowsChanged', existingRows);
return comps;
});
},
delay: rowsChangedEventDelay
});
const rowsReordered = throttle(() => {
this.$resortRows();
}, rowsChangedEventDelay, { leading: false });
const addAndRemoveRows = promiseMux({
/**
* This throttles the addition / removal of rows in the table.
*
* @inner
* @param {Array.<Object>} updates where each object has an `add` and `remove` array that
* contain the components to add or remove
* @returns {Promise} resolves when all rows have been added / removed
*/
exec: (updates) => {
const allAddComps = [];
const allRemoveComps = [];
// Combine all add / remove calls into two arrays.
updates.forEach((update) => {
const { add = [], remove = [] } = update;
allAddComps.push(...add);
allRemoveComps.push(...remove);
});
// Remove any components that are in both the add and remove arrays.
const toAddComps = without(allAddComps, ...allRemoveComps);
const toRemoveComps = without(allRemoveComps, ...allAddComps);
// Turn the components to remove into the corresponding rows
const toRemoveRows = [];
toRemoveComps.forEach((comp) => {
const obj = this.$getRowForSubject(comp);
if (obj) { toRemoveRows.push(obj.row); }
});
return Promise.all([
toAddComps.length && this.insertRows(toAddComps).catch(logError),
toRemoveRows.length && this.removeRows(toRemoveRows).catch(logError)
])
.then(() => {
return updates; //ensures that the expected number of items in the array are resolved for promiseMux.
});
},
delay: rowsChangedEventDelay,
coalesce: false
});
componentSource
.on('added', (addedComps) => addAndRemoveRows({ add: addedComps }))
.on('removed', (removedComps) => addAndRemoveRows({ remove: removedComps }))
.on('changed', (comp) => {
this.$emitThrottledRowsChangedEvent(new RowsChangedArgument(comp));
})
.on('reordered', () => {
rowsReordered();
});
TableModel.call(this, {
columns,
rows: rows || componentSource.getComponents()
});
};
ComponentTableModel.prototype = Object.create(TableModel.prototype);
ComponentTableModel.prototype.constructor = ComponentTableModel;
/**
* @private
* @param {baja.Component} comp
* @param {String|Type} childType
* @returns {Promise.<module:nmodule/webEditors/rc/wb/table/model/ComponentTableModel>} a table
* model showing one column for every frozen property of the specified type, and one row for every
* child of the given component matching that type
*/
ComponentTableModel.$makeBasic = function (comp, childType) {
return baja.importTypes([ childType ])
.then(([ type ]) => {
return new ComponentTableModel({
componentSource: new ContainerComponentSource({
container: comp,
filter: [ type ]
}),
columns: getSlotNames(type).map((slotName) => new PropertyColumn(slotName, { type }))
});
});
};
/**
* Find the first row that has the given subject (identity equal).
*
* @private
* @param {*} subject
* @returns {Object} object with `row` (the actual Row) and `index`
* properties, or undefined if not found
*/
ComponentTableModel.prototype.$getRowForSubject = function (subject) {
const rows = this.getRows();
for (let i = 0; i < rows.length; i++) {
if (rows[i].getSubject() === subject) {
return { row: rows[i], index: i };
}
}
};
/**
* Re-sort the rows in this TableModel to match the current ordering of component slots.
* @private
*/
ComponentTableModel.prototype.$resortRows = function () {
const comps = this.getComponentSource().getComponents();
this.sort((row1, row2) => {
const index1 = comps.indexOf(row1.getSubject());
const index2 = comps.indexOf(row2.getSubject());
return index1 - index2;
});
};
/**
* Get the `ComponentSource` backing this table model.
*
* @returns {module:nmodule/webEditors/rc/wb/table/model/ComponentSource}
*/
ComponentTableModel.prototype.getComponentSource = function () {
return this.$componentSource;
};
/**
* Return the delay (in milliseconds) used by throttled 'rowsChanged' event
* emission function.
*
* @private
*/
ComponentTableModel.prototype.$getRowsChangedEventDelay = function () {
return this.$rowsChangedEventDelay;
};
/**
* Ensures that the row's icon is set to the component's icon.
* @param {baja.Component|module:nmodule/webEditors/rc/wb/table/model/Row} comp
* @returns {module:nmodule/webEditors/rc/wb/table/model/Row}
* @since Niagara 4.14
*/
ComponentTableModel.prototype.makeRow = function (comp) {
return comp instanceof Row ? comp : new Row(comp, comp.getIcon());
};
/*
* promiseMux coalescing keys on the `toString()` of the object passed in and a Component's toString defaults to just its Type.
* If available, this class uses the component's handle for the toString so the coalescing from station side changes works properly.
* @class
* @inner
*/
class RowsChangedArgument {
constructor(comp) {
this.comp = comp;
}
/**
* @returns {string}
*/
toString() {
const comp = this.comp;
if (baja.hasType(comp, 'baja:Component')) {
const handle = comp.getHandle();
if (handle) {
return handle;
}
}
return comp.toString();
}
}
return ComponentTableModel;
});