/**
* @copyright 2016 Tridium, Inc. All Rights Reserved.
*/
/* jshint browser: true */
/**
* API Status: **Development**
*
* MgrStateHandler provides the ability to save some state for a `Manager` view, allowing, at
* the most basic level, a `Manager` to preserve the state of its hidden/visible columns
* and the visibility of the learn table when moving between views. Managers may also
* optionally use it to remember other state, such as the items that were found during
* the last discovery action.
*
* Note that although it provides similar functionality to the Java MgrState type, it differs
* in that this type is not used to preserve the state on its own instance. This type provides
* the functionality to serialize and deserialize the data to JSON; the object instance itself
* does not store any state and is not preserved between hyperlinks or page reloads.
*
* A Manager may provide its own functions that can be used to save and restore custom data
* for a particular Manager type. See the description of the `save()` function for information.
*
* @module nmodule/webEditors/rc/wb/mgr/MgrStateHandler
*/
define([ 'baja!',
'underscore',
'Promise',
'nmodule/webEditors/rc/util/SyncedSessionStorage',
'nmodule/webEditors/rc/wb/mixin/mixinUtils',
'nmodule/webEditors/rc/wb/table/model/Column',
'nmodule/webEditors/rc/fe/baja/util/compUtils' ], function (
baja,
_,
Promise,
SyncedSessionStorage,
mixinUtils,
Column,
compUtils) {
"use strict";
const { hasMixin } = mixinUtils;
const CACHE_STORAGE_KEY = 'niagara.mgrstate.cache';
//kick it off immediately to save a few ms
SyncedSessionStorage.getInstance();
function mgrHasFunction(mgr, name) {
return (mgr[name]) && (typeof mgr[name] === 'function');
}
/**
* Serialize the given object to JSON, before it is placed into the session
* storage.
*
* @param {Object} obj - an object containing the state to be serialized to storage.
* @returns {String}
*/
function serialize(obj) {
return JSON.stringify(obj);
}
/**
* Restore a serialized object that we have obtained back from the session
* storage before we deconstruct it to get the state to apply back to the
* manager.
*
* @param json - the JSON serialized state to be restored.
* @returns {Object}
*/
function deserialize(json) {
return JSON.parse(json);
}
////////////////////////////////////////////////////////////////
// MgrStateHandler
////////////////////////////////////////////////////////////////
/**
* Constructor not to be called directly. Call `.make()` instead.
*
* @alias module:nmodule/webEditors/rc/wb/mgr/MgrStateHandler
* @class
*/
const MgrStateHandler = function MgrStateHandler(params) {
params = baja.objectify(params, 'key');
this.$mgrStateKey = String(params.key);
};
/**
* Takes a key string that will be used to index the
* state information in the storage. This key is usually derived from
* the Manager widget's `moduleName` and `keyName` parameters.
*
* Note that `MgrStateHandler` relies upon `SyncedSessionStorage` which can
* take up to 1000ms to initialize, so this may take that long to resolve.
*
* @param {string|Object} params - the parameters object or a string containing the
* key parameter.
* @param {String} params.key - the key name used to index the saved state
* information. Usually derived from the Manager's moduleName and keyName
* parameters.
* @returns {Promise.<module:nmodule/webEditors/rc/wb/mgr/MgrStateHandler>}
*/
MgrStateHandler.make = function (params) {
return SyncedSessionStorage.getInstance()
.then(function (storage) {
const handler = new MgrStateHandler(params);
handler.$storage = storage;
return handler;
});
};
/**
* Get the key for use in the browser's session storage. We will
* prepend the user name to the root storage key.
*
* @private
* @returns {String} the full key to be used for session storage.
*/
MgrStateHandler.prototype.getKeyForSessionStorage = function () {
return baja.getUserName() + '.' + CACHE_STORAGE_KEY + '.' + this.getMgrTypeKey();
};
/**
* Return the manager specific part of the key used to store/retrieve state information
* in session storage. This is a substring of the total key, which will have extra
* information added to it. This will return the key provided in the constructor.
*
* @private
* @returns {String} the manager specific part of the storage key
*/
MgrStateHandler.prototype.getMgrTypeKey = function () {
return this.$mgrStateKey;
};
////////////////////////////////////////////////////////////////
// Learn Mode Helpers
////////////////////////////////////////////////////////////////
function hasLearnSupport(mgr) {
return hasMixin(mgr, 'MGR_LEARN');
}
function toggleLearnMode(mgr, selected) {
mgr.setLearnModeEnabled(selected);
}
////////////////////////////////////////////////////////////////
// 'All Descendants' Helpers
////////////////////////////////////////////////////////////////
function hasFolderSupport(mgr) {
return hasMixin(mgr, 'MGR_FOLDER');
}
function toggleAllDescendants(mgr, selected) {
mgr.setAllDescendantsSelected(selected);
}
/**
* Static method for use in the `Manager`'s load process. This will be called
* to allow the model to know whether it should restore initially in a flattened state.
*
* @static
* @private
*
* @param {Object} deserialized - a deserialized state object returned from
* an earlier call to `deserializeFromStorage()`.
*/
MgrStateHandler.shouldRestoreAllDescendants = function (deserialized) {
return deserialized && deserialized.$allDescendantsSelected;
};
////////////////////////////////////////////////////////////////
// Save
////////////////////////////////////////////////////////////////
/**
* Save the state of the `Manager` to session storage. This will perform three steps:
* first the manager's state is saved as properties upon a state object, secondly the
* object is serialized to JSON, and finally the JSON string is placed in session storage.
*
* The default save implementation will save the common basic state of the manager;
* that is the visibility of the table columns and the visibility of the discovery table.
* The manager can also provide functions to save state, which will be called by
* this type, if defined. The manager may provide a `saveStateForKey` and or a `saveStateForOrd`
* function. The `saveStateForOrd` function will be used to store information against a particular
* ord loaded in the manager, typically to save discovery data. Only the last ord that was loaded
* for a particular manager type will have its ord data saved. Any previous ord data for the
* same manager type will not be reloaded and thus will be erased when the data is saved again.
* The `saveStateForKey` function can be used to save generic data for the type of Manager.
* Both of these functions should return an Object containing the state to save.
* The returned value will be added to the data to be serialized. The Manager should also provide
* corresponding restoreStateForKey` and/or `restoreStateForOrd` functions that will receive a
* deserialized version of the object.
*
* @example
* <caption>
* Add a function on the Manager to save the items found in the last discovery.
* </caption>
* MyDeviceMgr.prototype.saveStateForOrd = function () {
* return {
* discoveries: this.discoveredItems // These objects will be serialized as JSON
* };
* };
*
* @param {module:nmodule/webEditors/rc/wb/mgr/Manager} mgr - the Manager instance requiring its state to be saved.
* @returns {Promise}
*/
MgrStateHandler.prototype.save = function (mgr) {
const key = this.getKeyForSessionStorage();
const state = {};
return getOrdFromMgr(mgr)
.then((ord) => {
if (ord) { state.$ord = ord.toString(); }
this.doSave(mgr, state);
this.$storage.setItem(key, serialize(state));
});
};
/**
* Save the Manager's state to the given object, prior to serialization.
* This will save the basic state supported for all Manager views, and then
* try to see if the Manager provides its own functions for saving custom
* data.
*
* @param {module:nmodule/webEditors/rc/wb/mgr/Manager} mgr - the Manager instance being saved.
* @param {Object} state - an object instance that will contain the state to be serialized.
*/
MgrStateHandler.prototype.doSave = function (mgr, state) {
this.$saveBasicState(mgr, state);
this.saveForKey(mgr, state);
this.saveForOrd(mgr, state);
};
/**
* Save which columns in the main table are hidden. If learn support is
* enabled, it will do the same for the discovery table, also saving
* whether the discovery table is currently visible.
*
* @private
*
* @param {module:nmodule/webEditors/rc/wb/mgr/Manager} mgr - the Manager instance being saved.
* @param {Object} state - an object instance that will contain the state to be serialized.
*/
MgrStateHandler.prototype.$saveBasicState = function (mgr, state) {
const model = mgr.getModel();
const columns = model.getColumns();
state.$modelColumnsUnseen = encodeUnseenColumns(columns);
if (hasLearnSupport(mgr)) {
state.$learnModeSelected = mgr.isLearnModeEnabled();
const learnModel = mgr.getLearnModel();
if (learnModel) {
state.$learnColumnsUnseen = encodeUnseenColumns(learnModel.getColumns());
}
}
if (hasFolderSupport(mgr)) {
state.$allDescendantsSelected = mgr.isAllDescendantsSelected();
}
};
/**
* Test whether the Manager has a `saveStateForKey` function, and invoke it, if found.
*
* @param {module:nmodule/webEditors/rc/wb/mgr/Manager} mgr - the Manager instance being saved.
* @param {Object} state - an object instance that will contain the state to be serialized.
*/
MgrStateHandler.prototype.saveForKey = function (mgr, state) {
if (mgrHasFunction(mgr, 'saveStateForKey')) {
state.$mgrKeyState = mgr.saveStateForKey();
}
};
/**
* Test whether the Manager has a `saveStateForOrd` function, and invoke it, if found.
*
* @param {module:nmodule/webEditors/rc/wb/mgr/Manager} mgr - the Manager instance being saved.
* @param {Object} state - an object instance that will contain the state to be serialized.
*/
MgrStateHandler.prototype.saveForOrd = function (mgr, state) {
if (mgrHasFunction(mgr, 'saveStateForOrd')) {
state.$mgrOrdState = mgr.saveStateForOrd();
}
};
////////////////////////////////////////////////////////////////
// Restore
////////////////////////////////////////////////////////////////
/**
* Test whether the stored state was for the same Ord the manager currently has loaded.
* If so, the restoreForOrd() function will be called during the restore process. This
* provides equivalent behavior to the Java abstract manager framework, which will restore
* the ord data, if the current ord matches the last one saved.
*
* @param {Object} obj - the deserialized state object.
* @param {module:nmodule/webEditors/rc/wb/mgr/Manager} mgr - the manager
*
* @returns {Promise.<Boolean>} returns true if the stored state was for the same ord as the model.
*/
function isSameOrdForRestore(obj, mgr) {
if (obj.$ord) {
return getOrdFromMgr(mgr)
.then(function (ord) {
return baja.Ord.make(obj.$ord).equals(ord);
});
}
return Promise.resolve(false);
}
/**
* Retrieve the state from storage and return the state object. This will be called
* early in the manager's load process in order for it to be able to access relevant
* state before the model is created. The object returned from this method will be
* passed back to the restore function later in the load process.
*
* @returns {Object} - the stored state deserialized from JSON.
*/
MgrStateHandler.prototype.deserializeFromStorage = function () {
const key = this.getKeyForSessionStorage();
const json = this.$storage.getItem(key);
if (json) {
return deserialize(json);
}
return null;
};
/**
* Restore the state of a `Manager`. This function will retrieve the stored
* state information from session storage using the Manager's key. It takes a
* deserialized state object returned from an earlier call to `deserializeFromStorage`.
* The properties of that object will then be used to restore the prior
* state.
*
* The default `restore` implementation will restore the visibility of the table
* columns and the discovery tables. If the Manager provides `restoreStateForKey` and/or
* `restoreStateForOrd` functions to correspond to the save functions, these will be
* called with the deserialized versions of the objects the save functions returned.
*
* @example
* <caption>
* Add a function on the Manager to restore the items found in the last discovery.
* </caption>
* MyDeviceMgr.prototype.restoreStateForOrd = function (state) {
* if (state.discoveries) {
* this.discoveredItems = state.discoveries;
* }
* return this.reloadLearnModel(); // Returns a Promise that will reload the table
* };
*
* @param {module:nmodule/webEditors/rc/wb/mgr/Manager} mgr - the Manager instance being restored.
* @param {Object} state - a deserialized state object with properties containing the state to be restored.
*
* @returns {Promise} - A promise resolved when the state restoration has completed.
*/
MgrStateHandler.prototype.restore = function (mgr, state) {
if (!mgr.getModel()) {
return Promise.reject(new Error('Manager must be loaded before restoring'));
}
return state ? this.doRestore(mgr, state) : Promise.resolve();
};
/**
* Use the deserialized object to restore the state of the manager.
* This will restore the basic state, then invoke the custom functions on the
* manager itself, if they are defined.
*
* @param {module:nmodule/webEditors/rc/wb/mgr/Manager} mgr - the Manager instance being restored.
* @param {Object} obj - the object containing the state to be restored
*
* @returns {Promise}
*/
MgrStateHandler.prototype.doRestore = function (mgr, obj) {
const model = mgr.getModel();
if (!model) { return Promise.reject(new Error('Cannot restore state without a model')); }
this.$restoreBasicState(mgr, obj);
return Promise.resolve(this.restoreForKey(mgr, obj))
.then(() => {
// If the last manager instance saved against that key was for the same ord
// as the one we are restoring, call restoreForOrd().
return isSameOrdForRestore(obj, mgr);
})
.then((isSameOrd) => {
if (isSameOrd) {
return this.restoreForOrd(mgr, obj);
}
})
.then(() => {
return this.postRestore(mgr, obj);
});
};
/**
* Restore the basic state for the manager. This is the visible columns
* and whether the discovery table is showing.
*
* @private
*
* @param {module:nmodule/webEditors/rc/wb/mgr/Manager} mgr - the Manager instance being restored.
* @param {Object} obj - the deserialized object containing the state to be restored.
*/
MgrStateHandler.prototype.$restoreBasicState = function (mgr, obj) {
const model = mgr.getModel();
const columns = model.getColumns();
const unseen = decodeColumnFlags(obj.$modelColumnsUnseen, columns.length);
restoreColumnUnseenFlags(model.getColumns(), unseen);
if (hasLearnSupport(mgr)) {
toggleLearnMode(mgr, !!obj.$learnModeSelected);
const learnModel = mgr.getLearnModel();
if (learnModel && obj.$learnColumnsUnseen) {
const learnColumns = learnModel.getColumns();
const learnUnseen = decodeColumnFlags(obj.$learnColumnsUnseen, learnColumns.length);
restoreColumnUnseenFlags(learnColumns, learnUnseen);
}
}
if (hasFolderSupport(mgr)) {
toggleAllDescendants(mgr, !!obj.$allDescendantsSelected);
}
};
/**
* Test whether the Manager has a `restoreStateForKey` function, and invoke it, if found.
*
* @param {module:nmodule/webEditors/rc/wb/mgr/Manager} mgr - the Manager instance being restored.
* @param {Object} obj - the object containing the state to be restored
*
* @returns {Promise} - The Promise returned by the Manager's function, or undefined
* if the manager does not provide the function.
*/
MgrStateHandler.prototype.restoreForKey = function (mgr, obj) {
if (obj.$mgrKeyState && mgrHasFunction(mgr, 'restoreStateForKey')) {
return Promise.resolve(mgr.restoreStateForKey(obj.$mgrKeyState));
}
return Promise.resolve();
};
/**
* Test whether the Manager has a `restoreStateForOrd` function, and invoke it, if found.
*
* @param {module:nmodule/webEditors/rc/wb/mgr/Manager} mgr - the Manager instance being restored.
* @param {Object} obj
* @returns {Promise} - The Promise returned by the Manager's function, or undefined
* if the manager does not provide the function.
*/
MgrStateHandler.prototype.restoreForOrd = function (mgr, obj) {
if (obj.$mgrOrdState && Object.keys(obj.$mgrOrdState).length !== 0 && mgrHasFunction(mgr, 'restoreStateForOrd')) {
return Promise.resolve(mgr.restoreStateForOrd(obj.$mgrOrdState));
}
return Promise.resolve();
};
/**
* Test whether the Manager has a `postRestore` function, and invoke it, if found.
* This allows for additional actions such as clean up or other calls to be handled after
* restoring the state.
*
* @since Niagara 4.12
*
* @param {module:nmodule/webEditors/rc/wb/mgr/Manager} mgr - the Manager instance being restored.
* @param {Object} obj
* @returns {Promise} - The Promise returned by the Manager's function
* if the manager does not provide the function.
*/
MgrStateHandler.prototype.postRestore = function (mgr, obj) {
if (mgrHasFunction(mgr, 'postRestore')) {
return Promise.resolve(mgr.postRestore(obj));
}
return Promise.resolve();
};
////////////////////////////////////////////////////////////////
// Columns
////////////////////////////////////////////////////////////////
/**
* Save the state of the columns for one of the table models. This
* will save the state of the 'UNSEEN' flag, so columns that have
* been hidden by the show/hide menu will be restored to the previous
* state.
*
* @param {Array.<module:nmodule/webEditors/rc/wb/table/model/Column>} columns - the `Column`
* instances from either the main database model or the learn model.
*
* @returns {String} the column flags encoded as a string.
*/
function encodeUnseenColumns(columns) {
return encodeColumnFlags(_.map(columns, function (col) {
return col.hasFlags(Column.flags.UNSEEN);
}));
}
/**
* Restore the flags on the columns indicating whether the column should be seen
* or not.
*
* @param {Array.<module:nmodule/webEditors/rc/wb/table/model/Column>} columns - the columns
* from the main table or learn table model.
* @param {Array.<Boolean>} values - an array of boolean values that will be used to set
* the UNSEEN flag on the columns.
*/
function restoreColumnUnseenFlags(columns, values) {
for (let i = 0; i < columns.length; i++) {
columns[i].setUnseen(values[i]);
}
}
////////////////////////////////////////////////////////////////
// Util
////////////////////////////////////////////////////////////////
/**
* Return a promise that resolves to the ord of the manager's current component.
* Used to determine whether a manager is being restored for the same ord that
* it was last saved for.
* @param {module:nmodule/webEditors/rc/wb/mgr/Manager} mgr
* @returns {Promise}
*/
function getOrdFromMgr(mgr) {
return compUtils.getNavOrd(mgr.value());
}
/**
* Encode an array of boolean values representing the state of a particular
* flag for a model's columns into a hex string.
*/
function encodeColumnFlags(arr) {
const max = arr.length - 1;
let counter = 3;
let bits = 0;
let str = '';
for (let i = 0; i <= max; i++) {
bits |= ((arr[i] ? 1 : 0) << counter);
if (counter === 0 || i === max) {
str += bits.toString(16);
counter = 3;
bits = 0;
} else {
counter--;
}
}
return str;
}
/**
* Convert a hexadecimal string that was encoded with `encodeColumnFlags()`
* back to an array of boolean values. Has a `count` parameter that can
* strip off extra `false` values at the end of the array, when the source
* array's length was not a multiple of four.
*/
function decodeColumnFlags(str, count) {
let res = _.reduce(str, function (memo, ch) {
const bits = parseInt(ch, 16);
memo.push(!!(bits & 0x8));
memo.push(!!(bits & 0x4));
memo.push(!!(bits & 0x2));
memo.push(!!(bits & 0x1));
return memo;
}, []);
if ((count) && (res.length > count)) {
res = res.slice(0, count);
}
return res;
}
return (MgrStateHandler);
});