/**
* @copyright 2016 Tridium, Inc. All Rights Reserved.
*/
/**
* @module nmodule/driver/rc/wb/mgr/DriverMgr
*/
define([
'baja!',
'Promise',
'jquery',
'underscore',
'nmodule/webEditors/rc/fe/baja/util/DepthSubscriber',
'nmodule/webEditors/rc/fe/baja/util/typeUtils',
'nmodule/webEditors/rc/wb/mgr/componentStatusUtils',
'nmodule/webEditors/rc/wb/mgr/Manager',
'nmodule/webEditors/rc/wb/mgr/MgrFolderSupport',
'nmodule/webEditors/rc/wb/mgr/model/columns/NameMgrColumn',
'nmodule/webEditors/rc/wb/mgr/model/columns/PathMgrColumn',
'nmodule/webEditors/rc/wb/mixin/ComponentHyperlinkColumnMixin',
'nmodule/webEditors/rc/wb/mixin/mixinUtils',
'bajaux/mixin/subscriberMixIn',
'css!nmodule/driver/rc/driver' ], function (
baja,
Promise,
$,
_,
DepthSubscriber,
typeUtils,
componentStatusUtils,
Manager,
addFolderSupport,
NameMgrColumn,
PathMgrColumn,
addComponentHyperlinkSupport,
mixinUtils,
subscribable) {
'use strict';
var FOLDER_TYPE = baja.lt('baja:Folder'),
hasMixin = mixinUtils.hasMixin,
addStatusCss = componentStatusUtils.addComponentStatusCss;
function tryMixinHyperlinkSupport(col) {
// For now, the manager will just automatically mix in hyperlink support on the name
// and/or path columns. Concrete driver managers can apply the mixin to other columns
// if they wish.
if (col instanceof NameMgrColumn || col instanceof PathMgrColumn) {
if (!hasMixin(col, 'HYPERLINK_COLUMN')) {
addComponentHyperlinkSupport(col, { cssClass: 'driver-mgr-link' });
}
}
}
function isEditable(mgr, subject) {
return _.any(mgr.$editableTypes, function (type) {
return subject.getType().is(type);
});
}
function hasSubjectsAndAllEditable(mgr, subjects) {
if (!subjects.length) { return false; }
return _.every(subjects, function (subject) {
return isEditable(mgr, subject);
});
}
function hasSingleFolderSubject(subjects) {
return (subjects.length === 1) && (baja.hasType(subjects[0], FOLDER_TYPE));
}
function hasFlattenedFolderComponentSource(source) {
return (hasMixin(source, 'FOLDER_SOURCE') && (source.isFlattened()));
}
/**
* Function to find a component instance providing the subject for a table row
* from either the component itself or one of its descendants. This will, for instance,
* find the ControlPoint that is the parent of a ProxyExt that we have received a change
* event for.
*
* @inner
*
* @param {module:nmodule/webEditors/rc/wb/table/model/ComponentTableModel} model the main
* table model for the manager view.
* @param {baja.Component} comp the component that has received a changed event.
*
* @returns {baja.Component} the row subject component that is the ancestor of
* the changed descendant, or null if the ancestor could not be determined.
*/
function getAncestorInTable(model, comp) {
var subjects = _.invoke(model.getRows(), 'getSubject');
while (comp && typeUtils.isComplex(comp)) {
if (_.contains(subjects, comp)) {
return comp;
} else {
comp = comp.getParent();
}
}
return null;
}
/**
* Checks whether an event from the given component should be propagated to
* emit a 'rowsChanged' event on the model. This is intended for changes that
* we have received from the depth subscriber. This function will test whether
* the parent of the changed property is equal the the subject of one of the
* rows in the table. If so, the component source will handle raising the model's
* 'rowsChanged' event itself. If it is from a property on a descendant component of
* a row's subject (e.g. a proxy ext), then we will ask the source to emit a changed
* event.
*
* @param {module:nmodule/driver/rc/wb/mgr/DriverMgr} mgr
* @param {baja.Component} comp - the changed component
*/
function tryEmitRowChangeEvent(mgr, comp) {
var model = mgr.getModel(),
subject = getAncestorInTable(model, comp),
source;
if ((subject) && (subject !== comp)) {
source = model.getComponentSource();
source.handleChanged(subject);
}
}
/**
* API Status: **Development**
*
* DriverMgr constructor. Contains functionality for working with components
* within a driver network.
*
* There is usually no reason to extend this directly; extend `DeviceMgr` or
* `PointMgr` instead.
*
* @class
* @alias module:nmodule/driver/rc/wb/mgr/DriverMgr
* @extends module:nmodule/webEditors/rc/wb/mgr/Manager
*
* @param {String} [params.keyName] the key name used for lexicon entries for this view.
* @param {String} [params.moduleName] the module name used for lexicon entries for this view.
* @param {Number} [params.subscriptionDepth] the depth to subscribe the component tree.
* @param {module:nmodule/webEditors/rc/wb/util/subscriptionUtil~SubscriptionFilter} [params.subscriptionFilter]
* Starting in Niagara 4.13, if the optional subscriptionFilter function is provided, it will be called for each
* potentially subscribable BComponent with its current depth. By returning true only for the desired components,
* the subscription will subscribe to what is needed.
* @param {module:nmodule/webEditors/rc/wb/util/subscriptionUtil~SubscribeCallback} [params.subscribeCallback]
* Starting in Niagara 4.13, if the optional subscribeCallback function is provided, it will receive a callback when a component is subscribed.
* This can allow you to add additional subscriptions outside the normal depth and filter results.
* @param {String|Type} [params.folderType] optional parameter indicating the folder type
* used for the manager view. This will be used by the NewFolder command.
* @param {Array.<Type|String>} [params.editableTypes] specifies which types of values can be
* edited in the database table. If omitted, the Edit command will not be enabled.
* @param {function(baja.Component): baja.Status} [params.getComponentStatus] to enable status
* coloring of values in the database table, provide a function to be used to get a Status
* instance for each one.
*
* @see module:nmodule/driver/rc/wb/mgr/DeviceMgr
* @see module:nmodule/driver/rc/wb/mgr/PointMgr
*/
var DriverMgr = function DriverMgr(params) {
var that = this;
if (!params.moduleName || !params.keyName) {
throw new Error('\'moduleName\' or \'keyName\' parameters not specified.');
}
Manager.call(that, params);
that.$editableTypes = params.editableTypes || [];
that.$getComponentStatus =
params.getComponentStatus || _.constant(baja.Status.ok);
that.$subscriptionDepth = params.subscriptionDepth || 1;
that.$subscriber = new DepthSubscriber({
depth: that.$subscriptionDepth,
subscriptionFilter: params.subscriptionFilter,
subscribeCallback: params.subscribeCallback
});
subscribable(that);
if (params.folderType) {
addFolderSupport(that, { folderType: params.folderType });
}
};
DriverMgr.prototype = Object.create(Manager.prototype);
DriverMgr.prototype.constructor = DriverMgr;
/**
* Load the widget from the component. This will hook up the event handlers to the
* depth subscriber used by this type.
*
* @param {baja.Component} comp
* @returns {Promise}
*/
DriverMgr.prototype.doLoad = function (comp) {
var that = this,
subscriber = that.getSubscriber(),
model = this.getModel(),
source = model.getComponentSource();
if (subscriber instanceof DepthSubscriber && hasMixin(source, 'FOLDER_SOURCE')) {
source.setComponentDepth(subscriber.getDepth());
}
_.each(model.getColumns(), tryMixinHyperlinkSupport);
// Set up handlers on the subscriber for this manager. The Attachable on the
// component source will cause events to be emitted for things at the first
// level under a subject. With the depth subscription supported by this
// type, the 'changed' handler will cause events to be emitted only for changes
// at levels deeper than a row subject's direct slots, for example a property
// on a point's proxy extension, leaving the other events to be emitted by
// the component source itself.
that.$descendantChangedHandler = function (prop) {
var comp = this;
that.componentChanged(comp, prop);
};
that.$descendantRenamedHandler = function (prop) {
var comp = this;
that.componentRenamed(comp, prop);
};
that.$descendantAddedHandler = function (prop) {
var comp = this.get(prop);
that.componentAdded(this, comp);
};
that.$descendantRemovedHandler = function (prop, value) {
that.componentRemoved(value);
};
subscriber.attach('changed', that.$descendantChangedHandler);
subscriber.attach('renamed', that.$descendantRenamedHandler);
subscriber.attach('added', that.$descendantAddedHandler);
subscriber.attach('removed', that.$descendantRemovedHandler);
return Manager.prototype.doLoad.call(that, comp);
};
/**
* Function called when a property of one of the row's subjects or descendants
* changes. This is used to update the table when, for example, a property
* on a point's proxy extension is changed.
*
* @param {baja.Component} comp The component notifying the changed property.
* @param {baja.Property} prop The property that changed
*/
DriverMgr.prototype.componentChanged = function (comp, prop) {
tryEmitRowChangeEvent(this, comp);
};
/**
* Function called when the depth subscriber notifies a renamed component. This will
* try to emit a 'changed' event on the component source.
*
* @param comp
*/
DriverMgr.prototype.componentRenamed = function (comp) {
tryEmitRowChangeEvent(this, comp);
};
/**
* Function called when a new component is added. If the
* component is a folder and the all descendants command is selected,
* we want to subscribe to that folder to the correct depth.
*
* @param {baja.Component} parent
* @param {baja.Component} child
*/
DriverMgr.prototype.componentAdded = function (parent, child) {
var model = this.getModel(),
source = model.getComponentSource();
if (child.getType().is(FOLDER_TYPE) && hasFlattenedFolderComponentSource(source)) {
this.resubscribeForNewFolderDepth()
.catch(baja.error);
}
};
/**
* Function called when a subscribed component is removed. If the
* component was a folder and the all descendants command is selected,
* we want to unsubscribe to that folder.
*
* @param {baja.Component} value
*/
DriverMgr.prototype.componentRemoved = function (value) {
var model = this.getModel(),
source = model.getComponentSource();
if (value.getType().is(FOLDER_TYPE) && hasFlattenedFolderComponentSource(source)) {
this.resubscribeForNewFolderDepth()
.catch(baja.error);
}
};
/**
* Destroy the widget. This will clean up the event handler we have attached
* for listening to descendant changes.
*
* @returns {*}
*/
DriverMgr.prototype.doDestroy = function () {
var that = this,
subscriber = that.getSubscriber();
that.getCommandGroup().removeAll();
if (that.$descendantChangedHandler) {
subscriber.detach('changed', that.$descendantChangedHandler);
delete that.$descendantChangedHandler;
}
if (that.$descendantRenamedHandler) {
subscriber.detach('renamed', that.$descendantRenamedHandler);
delete that.$descendantRenamedHandler;
}
if (that.$descendantAddedHandler) {
subscriber.detach('added', that.$descendantAddedHandler);
delete that.$descendantAddedHandler;
}
if (that.$descendantRemovedHandler) {
subscriber.detach('removed', that.$descendantRemovedHandler);
delete that.$descendantRemovedHandler;
}
return Manager.prototype.doDestroy.apply(that, arguments);
};
/**
* Get the configured component subscription depth for the driver manager.
* This value is specified by the 'subscriptionDepth' parameter property
* in the constructor.
*
* @returns {Number}
*/
DriverMgr.prototype.getSubscriptionDepth = function () {
return this.$subscriptionDepth;
};
/**
* Override of the base manager's build cell function.
*
* @param {module:nmodule/webEditors/rc/wb/table/model/Column} column The column for the cell
* @param {module:nmodule/webEditors/rc/wb/table/model/Row} row The row for the cell
* @param {JQuery} dom
* @returns {Promise}
*/
DriverMgr.prototype.buildMainTableCell = function (column, row, dom) {
var that = this;
return Promise.try(function () {
if ((baja.hasType(row.getSubject(), FOLDER_TYPE)) && (!that.$isSuitableColumnForFolder(column))) {
dom.html(' '); // Just put an empty space in the column for this particular folder row
return Promise.resolve();
}
return Manager.prototype.buildMainTableCell.apply(that, [ column, row, dom ]);
})
.catch(function (ignore) {
// This is intended to provide the same empty string fallback behavior of
// the equivalent Java manager view. In the case where an error occurs
// synchronously in the buildCell call, the result is a cell with empty content.
dom.html(' ');
});
};
/**
* The name, path, and icon columns are visible for Folders by default. Override this method if your
* Manager subclass would like to make other columns suitable for Folders.
* @private
* @since Niagara 4.14
* @param {module:nmodule/webEditors/rc/wb/table/model/Column} col a table column instance
*/
DriverMgr.prototype.$isSuitableColumnForFolder = function (col) {
return _.contains([ '__name', '__path', 'icon' ], col.getName());
};
/**
* Overrides the basic manager `#finishMainTableRow` function with some extra css information
* specified on the dom for the table row.
*
* @param {module:nmodule/webEditors/rc/wb/table/model/Row} row a table row instance
* @param {JQuery} dom
*/
DriverMgr.prototype.finishMainTableRow = function (row, dom) {
var subject = row.getSubject(),
status = this.$getComponentStatus(subject);
dom.addClass('driver-mgr-row');
addStatusCss(status, dom);
if ((this.$folderType) && (baja.hasType(subject, this.$folderType))) {
dom.addClass('driver-mgr-folder');
}
return Promise.resolve(dom);
};
/**
* Provides the default implementation of the call back function for double-click of rows.
* In the callback we'll look at the row's subject; if the row being clicked is of an editable
* type, then the edit command action can be invoked. Otherwise, if the point is a folder, the
* double click action will be to do a hyperlink to that folder's ord.
* @since Niagara 4.14
* @param {JQuery.Event} event the double click event
* @param {Array.<*>} subjects the selected subject of the table being clicked
* @return {Promise|*}
*/
DriverMgr.prototype.onMainTableDblClicked = function (event, subjects) {
const editCmd = this.$getEditCommand();
if (subjects && subjects.length) {
if (hasSingleFolderSubject(subjects)) {
return Promise.resolve($(event.target).closest('tr').find('td.driver-mgr-link > a').first().trigger('click'));
} else if (editCmd && hasSubjectsAndAllEditable(this, subjects)) {
return editCmd.invoke();
}
}
return Promise.resolve();
};
/**
* This provides the default function of enabling or disabling the edit command based on what
* is currently selected in the main table. We typically won't enable the edit command if a
* folder is included in the current selection.
* @since Niagara 4.14
* @param {Object} selectedSubjects an object that holds the array of the selected subjects from the tables
* supported by the manager view.
* @param {Array.<*>} selectedSubjects.mainTableSelection the current selected subjects
* in the main table.
* @param {Array.<*>|undefined} selectedSubjects.learnTableSelection the current selected subjects
* in the learn or discovery table. This will be undefined if Manager does not support a learn
* table or the getLearnTable() function returns undefined.
* @return {Promise|*}
*/
DriverMgr.prototype.onTableSelectionChanged = function (selectedSubjects) {
return Manager.prototype.onTableSelectionChanged.call(this, selectedSubjects)
.then(() => {
const editCmd = this.$getEditCommand();
const { mainTableSelection } = selectedSubjects;
if (editCmd) {
editCmd.setEnabled(hasSubjectsAndAllEditable(this, mainTableSelection));
}
});
};
return (DriverMgr);
});