'use strict'; /** * Licensed Materials - Property of IBM * IBM Cognos Products: BI Cloud (C) Copyright IBM Corp. 2014, 2021 * US Government Users Restricted Rights - Use, duplication or disclosure restricted by GSA ADP Schedule Contract with IBM Corp. */ define(['jquery', '../../lib/@waca/dashboard-common/dist/core/Model', './WidgetModel', './LayoutModel', './ModelUtils', 'underscore', './EventGroups', './properties/PropertiesModel', '../../lib/@waca/core-client/js/core-client/utils/UniqueId'], function ($, BaseModel, WidgetModel, LayoutModel, ModelUtils, _, EventGroupCollection, PropertiesModel, UniqueId) { function createLayoutModel_default(spec, env) { return new LayoutModel(spec, env.boardModel, env.logger); } var Model = BaseModel.extend({ nestedCollections: { eventGroups: EventGroupCollection }, nestedModels: { properties: PropertiesModel }, init: function init(boardSpec, options) { var _this = this; this._initBoardSpec = boardSpec; options = options || {}; this.logger = options.logger; // TODO: remove depenendency on dashboardApi here. currently needed to be passed to the "pageContext" board model extension this.dashboardApi = options.dashboardApi; this.createLayoutModel = options.createLayoutModel || createLayoutModel_default; // Make sure the extensions are processed before we call the parents init if (options.whitelistAttrs) { this.whitelistAttrs = options.whitelistAttrs; } else { this.whitelistAttrs = ['name', 'layout', 'theme', 'version', 'eventGroups', 'datasetShaping', 'properties', 'content']; } var excludedProperties = ['dashboardColorSet', 'customColors', 'localCache', 'defaultLocale', 'fredIsRed']; this.content = ModelUtils.initializeContentModel(boardSpec, excludedProperties); options.boardModel = this; if (!boardSpec.properties) { boardSpec.properties = {}; } // Need to set these since the boardModel is passed as the options when building the layout model. // Weird, but don't want to change the flow of layout model creation in R release this.defaultLocale = boardSpec.properties.defaultLocale; if (this.dashboardApi) { var userProfileService = this.dashboardApi.getGlassCoreSvc('.UserProfile'); if (userProfileService) { this.contentLocale = userProfileService.preferences.contentLocale; } } _.extend(options, this.getLanguageModelOptions()); this._updateNestedInfoWithExtensions(options.boardModelExtensions); Model.inherited('init', this, arguments); this.eventRouter = options.eventRouter; this._autoCreateExtensionModelsAndCollections(options.boardModelExtensions, boardSpec); this.id = options.id; this.name = options.name || this.name; this.widgetRegistry = options.widgetRegistry; this.layoutExtensions = options.layoutExtensions; this.widgetInstances = {}; // TODO: Add reference to cleanup item here when it is created in Jira. // CADBC-832 is item that caused it to change. Eventually this block // of code will go away. Object.keys(boardSpec.widgets || {}).forEach(function (widgetId) { _this.createLegacyWidgetModel(boardSpec.widgets[widgetId]); }); // Explore expects the BoardModel to populate the widgetInstances // within this init method but because of changes to how widgets are // instantiated they are now created from the LayoutModel though still // from a method in this class (see createLegacyWidgetModel below) // This doesn't work in Explore's case so the following continues // to allow Explore to function properly if (this.createLayoutModel !== createLayoutModel_default) { this._instantiateWidgetModels(boardSpec.layout.items); } this.layout = this.createLayoutModel(boardSpec.layout, { boardModel: this, logger: this.logger, dashboardApi: this.dashboardApi }); if (!this.eventGroups) { this.set({ eventGroups: new EventGroupCollection() }, { silent: true }); } // cache the extensions, the boardmodel editor needs this to re-create the model after a manual edit. this.boardModelExtensions = options.boardModelExtensions; // The first layout item will be displayed by default. Keep the selected layout property in sync. if (this.layout.items && this.layout.items.length > 0) { var item = this.layout.items[0]; this.setSelectedLayout(item.id); } // Keep the selected layout in sync. this.eventRouter && this.eventRouter.on('tab:tabChanged', this.onTabChanged, this); this.isUpgraded = options.isUpgraded; this.type = 'dashboard'; }, _instantiateWidgetModels: function _instantiateWidgetModels() { var items = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : []; for (var i = 0; i < items.length; i++) { var item = items[i]; if (item.items && item.items.length) { this._instantiateWidgetModels(item.items); } else { if (item.type === 'widget' && item.features && item.features['Models_internal']) { this.createLegacyWidgetModel(item.features['Models_internal']); } } } }, /** * Returns the ContentModel * @return {Object} */ getContentModel: function getContentModel() { return this.content; }, /** * In Endor R7 (11.1.7) onward we now allow for features to load * their own data from the board spec. Through this mechanism this method * is called by the LayoutModel for specific content model objects that * have a 'Models.internal' feature on them. * 'Models.internal' is the place that widget data resides for now * instead of a top-level "widgets" property in the BoardModel here. * The BoardModel will still hold all widget instances though. * @param {Object} widgetSpec An object that represents a widget model */ createLegacyWidgetModel: function createLegacyWidgetModel(widgetSpec) { if (!this.getWidgetModel(widgetSpec.id)) { this._addWidgetModel(widgetSpec, { addDashboardTranslatedLocales: false }); } }, /** * Adds the board model extension info to the the nestedCollections and nestedModels */ _updateNestedInfoWithExtensions: function _updateNestedInfoWithExtensions(boardModelExtensions) { var _this2 = this; if (!boardModelExtensions) { return; } boardModelExtensions.forEach(function (extensionInfo) { var extensionName = extensionInfo.name; _this2.whitelistAttrs.push(extensionName); if (extensionInfo.type === 'collection') { _this2.nestedCollections[extensionName] = extensionInfo.class; } else { _this2.nestedModels[extensionName] = extensionInfo.class; } }); }, /** * Handles adding the model extension points to the whitelistAttrs array and as nestedModels * @param {Object} boardModelExtensions * @param {Object} boardSpec */ _autoCreateExtensionModelsAndCollections: function _autoCreateExtensionModelsAndCollections(boardModelExtensions, boardSpec) { var _this3 = this; if (!boardModelExtensions) { return; } boardModelExtensions.forEach(function (extensionInfo) { var extensionName = extensionInfo.name; /** * If it's already in the boardSpec do nothing since the base Model class should of already made sure we were * dealing with actual Models and Collections. If it's missing and autoCreate is set to true then create the * Model or Collection */ if (!boardSpec[extensionName] && extensionInfo.autoCreate) { if (extensionInfo.type === 'collection') { _this3._setCollection(extensionName, boardSpec[extensionName] || null, { silent: true, logger: _this3.logger, boardModel: _this3, dashboardApi: _this3.dashboardApi // FIXME - models should not rely on dashboardApi -- currently used by the pageContext }); } else { _this3._setNestedModel(extensionName, boardSpec[extensionName] || {}, { silent: true, logger: _this3.logger, boardModel: _this3 }); } } }); this._registerBoardModelExtensionTriggers(boardModelExtensions); }, setDefaultLocale: function setDefaultLocale(locale) { this.defaultLocale = locale; }, setTranslationLocale: function setTranslationLocale(locale) { this.properties.set({ translationModeLocale: locale }); this.translationLocale = locale; }, _registerBoardModelExtensionTriggers: function _registerBoardModelExtensionTriggers(boardModelExtensions) { if (boardModelExtensions) { boardModelExtensions.forEach(function (extension) { if (extension.triggers && extension.triggers.length > 0) { extension.triggers.forEach(function (trigger) { this[extension.name].on(trigger.eventName, this.trigger.bind(this, trigger.event)); }, this); } }, this); } }, onTabChanged: function onTabChanged(event) { this.setSelectedLayout(event.modelId); }, /** * @deprecated, use findWidgetById */ getWidgetModel: function getWidgetModel(id) { return this.widgetInstances ? this.widgetInstances[id] : null; }, getTimeLineEpisode: function getTimeLineEpisode(id) { return this.timeline ? this.timeline.episodes.get(id) : null; }, setSelectedLayout: function setSelectedLayout(id) { this.selectedLayout = id; }, getSelectedLayout: function getSelectedLayout() { return this.selectedLayout; }, /** * Add a content to the model * * @param options * @param options.model - layout model to add * @param options.parentId - id of the parent layout * @param [options.insertBefore] - id of the layout to insert before * @param options.layoutProperties * @param sender - If available, the sender will be added to the event payload. * @param payloadData - Additional data that will be included in the event payload. * @throws throws an exception if any of the mandatory options are not provided */ addContent: function addContent(options, sender, payloadData) { if (!options.parentId) { throw new Error('Invalid argument options.parentId provided'); } if (!options.model) { throw new Error('Invalid argument options.model provided'); } payloadData = this.checkPayloadData(payloadData); var model = Object.assign({}, options.model); // if no id provided on the model, generate a unique id // TODO: verify that drag and drop works as expected; // possibly will need to always refresh the id or check if model does not already exist if (!model.id || !options.modelIdsValid) { model.id = UniqueId.get('content'); } if (options.layoutProperties && options.layoutProperties.style) { model.style = Object.assign({}, model.style || {}, options.layoutProperties.style); } this.layout.addArray([{ parentId: options.parentId, insertBefore: options.insertBefore, model: model }], sender, payloadData); return model.id; }, /** * Remove a content from the model. * * @param id * @param sender * @param payloadData - Additional data that will be included in the event payload. */ removeContent: function removeContent(id, sender, payloadData) { payloadData = this.checkPayloadData(payloadData); this.layout.removeArray([id], sender, payloadData); }, /** * Add a widget to the model. * * @param options * options.model * options.parentId * options.insertBefore * options.layoutProperties * @param sender - If available, the sender will be added to the event payload. * @param payloadData - Additional data that will be included in the event payload. */ addWidget: function addWidget(options, sender, payloadData) { //If no parent id was passed in, cancel the add if (!options.parentId) { return; } // Create a default transaction id if none is provided so that it is easier to bundler subsequent operation with this one payloadData = this.checkPayloadData(payloadData); var model = this._addWidgetModel(options.model, { id: options.id }); if (options.id) { //this is when a widget was already loaded (but not added to canvas) //adding to canvas here will use the same id as in the widget for the widget and layout model //adding will not result in reloading of the widget, but will just add the required models (and layout view obj, which will contain the //pre-loaded widget) model.id = options.id; } var id = model.id; // Add to the layout var layoutModel = _.extend({}, options.layoutProperties, { type: 'widget', id: id }); this.layout.addArray([{ parentId: options.parentId, insertBefore: options.insertBefore, model: layoutModel }], sender, payloadData); return this._triggerAddRemove('addWidget', { op: 'addWidget', parameter: _.extend({}, options, { model: model.toJSON(), layoutProperties: $.extend(true, {}, layoutModel) }) }, { op: 'removeWidget', parameter: id }, sender, payloadData); }, /** * * Add a fragment to the model. * * passed model can contain a parent layout or array of layouts * * @param options * options.model which contains * { * layout: { * //one parent layout or parent layout with nested layouts * //one or widgets can be one or more of these layouts * id: xxxx * type: yyy * items: [{ * id: x1 * type: x2 * }, * ... * ], * style: { * height: aaa * width: bbb * top: ccc * left: ddd * } * }, * episodes : [ * { * "id": "x1", * "type": "widget", * "acts": [ * { * "id": "act1", * "timer": 0, * "action": "show" * }, * { * "id": "act2", * "timer": 5000, * "action": "hide" * } * ] * } * widgets : [ * { * id: xxxx * type: yyyy * widget model data * }, * ... * ], * datasetShapings : [ * { * id: xxxx, * calculations: [] * }, * ... * ] * } * options.parentId * options.insertBefore * * If layout is an array, it should like: * layout: [{ * id: xxxx * type: yyy * items: [{ * id: x1 * type: x2 * }, * ... * ], * style: { * height: aaa * width: bbb * top: ccc * left: ddd * } * }, { * id: xxxx * type: yyy * items: [{ * id: x1 * type: x2 * }, * ... * ] * }] * @param sender - If available, the sender will be added to the event payload. * @param payloadData - Additional data that will be included in the event payload. */ addFragment: function addFragment(options, sender, payloadData) { var _this4 = this; // TODO this logic should be moved out of the model and into a BoardModelManager class // Create a default transaction id if none is provided so that it is easier to bundler subsequent operation with this one payloadData = this.checkPayloadData(payloadData); //make a copy var fragSpec = _.extend({}, options.model); var datasources = this.dashboardApi.getFeature('dataSources.deprecated'); var sourceCollection = datasources ? datasources.getSourcesCollection() : undefined; var sourceIdMap = sourceCollection ? sourceCollection.addSourcesForPin(fragSpec, { payloadData: payloadData }) : {}; //add all widgets in fragment spec var widgetIdMap = {}; _.each(fragSpec.widgets, function (widgetModel) { // replace widget ids in fragment with actual ones if the options tell us to // in some cases the id is already unique and valid. Mainly in the redo after undo case var oldId = widgetModel.id; if (!options.modelIdsValid) { widgetModel.id = undefined; } // Update the modelRef to the new sourceId if necessary var dataViews = widgetModel.data ? widgetModel.data.dataViews : null; if (dataViews) { dataViews.forEach(function (dataView) { if (sourceIdMap[dataView.modelRef]) { dataView.modelRef = sourceIdMap[dataView.modelRef]; } }); } var model = _this4._addWidgetModel(widgetModel); //keep a note of them, so that we can update layout spec widgetIdMap[oldId] = model.id; //update id in widgetModel with the new id from the model //That solves the redo (adding pin back) not working problem widgetModel.id = model.id; }); //update the position information in layout if layoutProperies is defined //layoutProperties is defined when pin is created by clicking on create button if (options.layoutProperties && !_.isArray(fragSpec.layout)) { fragSpec.layout.style = fragSpec.layout.style || {}; fragSpec.layout.style = _.extend(fragSpec.layout.style, options.layoutProperties.style); } //update widget ids in fragment spec to reflect new widget ids if we have not done it already if (!options.modelIdsValid) { fragSpec.layout = this._updateFragmentSpecLayout(fragSpec.layout, widgetIdMap, options); } //update drill through definitions var drillThroughService = void 0; try { drillThroughService = this.dashboardApi.getDashboardCoreSvc('DrillThroughService'); } catch (error) { this.logger.info(error); } if (drillThroughService) { drillThroughService.addDrillThroughOnAddFragment(options.model, sourceIdMap, widgetIdMap, fragSpec); } // Add any custom colors if (fragSpec.properties && fragSpec.properties.customColors && fragSpec.properties.customColors.colors) { this.properties.customColors.addCustomColor(fragSpec.properties.customColors.colors); } if (fragSpec.fredIsRed && _.isEmpty(this.fredIsRed.colorMap)) { this.fredIsRed.colorMap = fragSpec.fredIsRed.colorMap; } //create layout using layout spec var layoutPayload = this.layout.addArray(this._getAddArrayPayload(fragSpec, options), sender, payloadData); //undo relies on just removing parent layout of the fragment var modelArray = layoutPayload.prevValue.parameter; // update pageContext _.each(fragSpec.pageContext, function (pageContextItem) { // TODO: For now, only origin=filter and tupleSet or conditions are handled if (pageContextItem.origin === 'filter') { var _options = void 0; if (pageContextItem.tupleSet) { _options = { values: _.values(JSON.parse(pageContextItem.tupleSet)) }; // TODO: Why is pagecontext conditions an array with'from' and 'to' arrays as well? I tried to see if it is possible // TODO: to have more than one value in the conditions property from the UI, I couldn't find a way. The code in // TODO: LiveWidget:PageContextRangeConditions.js expects values, it doesn't seem to handle arrays. This should be // TODO: looked at to be only scalar values, not arrays and handled in the spec schema accordingly so it validates } else if (pageContextItem.conditions) { _options = { 'condition': { attributeUniqueNames: pageContextItem.conditions[0].attributeUniqueNames, from: pageContextItem.conditions[0].from[0], to: pageContextItem.conditions[0].to[0] } }; } else { _this4.logger.error('Error: AddFragment() - Only able to handle pageContext tupleSet and conditions, can not handle ' + JSON.stringify(pageContextItem)); } _options = _.extend(_options, { scope: pageContextItem.scope, openViewOnLoad: false, exclude: pageContextItem.exclude, payloadData: payloadData }); _this4.dashboardApi.getCanvasWhenReady().then(function (canvas) { canvas.filterApi.addFilter({ sourceId: pageContextItem.sourceId, itemId: pageContextItem.hierarchyUniqueNames[0] }, _options); }).catch(function (error) { _this4.logger.error(error); }); } else { _this4.logger.error('Error: AddFragment() - Only able to handle pagecontext.origin=filter, not ' + pageContextItem.origin); } }); //trigger event return this._triggerAddRemove('addFragment', { op: 'addFragment', parameter: _.extend({}, options, { model: fragSpec, modelIdsValid: true, widgetIdMap: widgetIdMap }) }, { op: 'removeFragment', parameter: modelArray }, sender, payloadData); }, _getAddArrayPayload: function _getAddArrayPayload(fragSpec, options) { var aResult = []; if (_.isArray(fragSpec.layout)) { _.each(fragSpec.layout, function (entry) { aResult.push({ parentId: options.parentId, insertBefore: options.insertBefore, model: entry }); }); return aResult; } return [{ parentId: options.parentId, insertBefore: options.insertBefore, model: fragSpec.layout }]; }, _updateFragmentSpecLayout: function _updateFragmentSpecLayout(layout, widgetIdMap) { var boardModel = this; var update = function update(layout, widgetIdMap) { if (_.isArray(layout)) { _.each(layout, function (item) { update(item, widgetIdMap); }); } else { if (layout.type !== 'widget') { // create a empty model to get an new ID. var layoutModel = boardModel.createLayoutModel({ items: [] }, { boardModel: boardModel, logger: boardModel.logger }); layoutModel.off(); widgetIdMap[layout.id] = layoutModel.id; layout.id = layoutModel.id; } else { layout.id = widgetIdMap[layout.id]; } _.each(layout.items, function (item) { update(item, widgetIdMap); }); } return layout; }; if (layout) { return update(layout, widgetIdMap); } else { var addLayout = function addLayout(id) { return { id: id, type: 'widget' }; }; //this just adds widgets to a group layout - but does nothing about location of widgets or style applied //not sure if this is the best strategy //add a group layout if spec does not contain a layout return { type: 'group', items: _.map(_.values(widgetIdMap), addLayout) }; } }, _getObjectFromArrayById: function _getObjectFromArrayById(array) { var object = _.object(_.map(array, function (item) { return [item.id, item]; })); return object; }, /** * * removes a fragment from the model * * @param modelIds array of layout identifiers (typically will be one container id) * * @param sender - If available, the sender will be added to the event payload. * @param payloadData - Additional data that will be included in the event payload. */ removeFragment: function removeFragment(modelIds, sender, payloadData) { return this.removeLayouts(modelIds, sender, payloadData); }, /** * Remove a widget from the model. * * @param id * @param sender * @param payloadData - Additional data that will be included in the event payload. */ removeWidget: function removeWidget(id, sender, payloadData) { // Create a default transaction id if none is provided so that it is easier to bundler subsequent operation with this one payloadData = this.checkPayloadData(payloadData); this.trigger('pre:removeWidget', { id: id, sender: sender, data: _.extend({ runtimeOnly: true }, payloadData) }); var payload = this.layout.removeArray([id], sender, payloadData); if (payload) { var widgetModel = this.widgetInstances[id]; var evtData = this._triggerAddRemove('removeWidget', { op: 'removeWidget', parameter: id }, { op: 'addWidget', parameter: { parentId: payload.prevValue.parameter[0].parentId, model: widgetModel.toJSON(), layoutProperties: _.extend({}, payload.prevValue.parameter[0].model) } }, sender, payloadData); this.widgetInstances[id].off('change', this.onWidgetModelChange, this); delete this.widgetInstances[id]; return evtData; } }, getUsedCustomColors: function getUsedCustomColors(customColors) { var usedColors = Model.inherited('getUsedCustomColors', this, arguments); if (this.widgetInstances) { for (var widgetModel in this.widgetInstances) { usedColors = usedColors.concat(this.widgetInstances[widgetModel].getUsedCustomColors(customColors)); } } if (this.layout) { usedColors = usedColors.concat(this.layout.getUsedCustomColors(customColors)); } return _.uniq(usedColors, false); }, /** * Remove a list of layouts from the board model. This method will also remove all the widgets contained in the layouts being removed * * @param id - could be a string with one id or an array if IDs * @param sender * @param payloadData - Additional data that will be included in the event payload. */ removeLayouts: function removeLayouts(id, sender, payloadData) { // Create a default transaction id if none is provided so that it is easier to bundler subsequent operation with this one payloadData = this.checkPayloadData(payloadData); var idArray = _.isArray(id) ? id : [id]; this.trigger('pre:removeLayouts', { idArray: idArray, sender: sender, data: _.extend({ runtimeOnly: true }, payloadData) }); var widgetIds = this.layout.listWidgets(idArray); var payload = this.layout.removeArray(idArray, sender, payloadData); if (payload) { var removedWidgets = {}; var widgetId; for (var i = 0; i < widgetIds.length; i++) { widgetId = widgetIds[i]; var widgetModel = this.widgetInstances[widgetId]; if (widgetModel) { removedWidgets[widgetId] = widgetModel.toJSON(); widgetModel.off(); delete this.widgetInstances[widgetId]; } } return this._triggerAddRemove('removeLayouts', { op: 'removeLayouts', parameter: idArray, removedWidgets: removedWidgets }, { op: 'addLayouts', parameter: { widgetSpecMap: removedWidgets, addLayoutArray: payload.prevValue.parameter } }, sender, payloadData); } }, /** * Add layouts to the board. Layouts can contain widgets * * options.widgetSpecMap A map of widget model json to be added. The map key is the id used in the layout to reference the widget * options.addLayoutArray = [{ * parentId - layout parent id where to add this layout * model - layout model json * }] * * @param options - widgetSpecMap, addLayoutArray * @param sender * @param payloadData - Additional data that will be included in the event payload. */ addLayouts: function addLayouts(options, sender, payloadData) { // Create a default transaction id if none is provided so that it is easier to bundler subsequent operation with this one payloadData = this.checkPayloadData(payloadData); // Add widgets if (options.widgetSpecMap) { for (var id in options.widgetSpecMap) { if (options.widgetSpecMap.hasOwnProperty(id)) { this._addWidgetModel(options.widgetSpecMap[id], { id: id }); } } } var layout = options.parentId ? this.layout.findModel(options.parentId) : this.layout; if (!layout) { layout = this.layout; } // Add layouts var payload = layout.addArray(options.addLayoutArray, sender, payloadData); return this._triggerAddRemove('addLayouts', { op: 'addLayouts', parameter: _.extend({}, options, { widgetSpecMap: options.widgetSpecMap, addLayoutArray: payload.value.parameter }) }, { op: 'removeLayouts', parameter: payload.prevValue.parameter }, sender, payloadData); }, _isTypeRegistered: function _isTypeRegistered(type) { var contentTypeRegistry = this.dashboardApi.getFeature('ContentTypeRegistry'); return contentTypeRegistry.isTypeRegistered(type); }, duplicateLayout: function duplicateLayout(layoutId, sender, payload) { var _this5 = this; payload = this.checkPayloadData(payload); var layoutModel = this.layout.findModel(layoutId); var idMap = {}; var clone = function clone() { if (layoutModel) { if (_this5._isTypeRegistered(layoutModel.type)) { var containerId = layoutModel.getParent().id; var content = _this5.dashboardApi.getFeature('Canvas').getContent(layoutId); return _this5.dashboardApi.getFeature('Canvas').addContent({ containerId: containerId, spec: content.getFeature('Serializer').toJSON(), copyPaste: true }).then(function (newContent) { var newId = newContent.getId(); idMap[layoutId] = newId; return newId; }); } else { var widgetSpecMap = _this5._cloneWidgets(layoutModel, idMap); var _clone = _this5._cloneLayout(layoutModel, idMap, payload); var options = { parentId: layoutModel.getParent().id, widgetSpecMap: widgetSpecMap, addLayoutArray: [{ model: _clone.toJSON(), insertBefore: layoutModel.type === 'widget' ? null : layoutModel.getNextSiblingId() }] }; _this5.addLayouts(options, sender, payload); return Promise.resolve(_clone.id); } } return Promise.resolve(); }; return clone().then(function (cloneId) { // all work in being done in other methods that take care of the undo/redo stack management // however we still want to notify interested classes that a duplicate occurred. // simplest way is to manually trigger an event and not include a senders context // this should bypass the undoduplicateLayout/redo stack since this method did not do anything that needs to be undone. // we still send the payload data in case someone else needs to do some undoable work in this transaction. _this5.trigger('duplicateLayout', { layoutId: layoutId, cloneId: cloneId, idMap: idMap, sender: sender, data: payload }); return cloneId; }); }, duplicateSelection: function duplicateSelection(options, sender, payload) { var ids = []; var payloadData = this.checkPayloadData(payload); payloadData.limitToBounds = []; _.each(options.modelIds, function (id) { if (options.outBoundIds && options.outBoundIds.filter(function (e) { return e.id === id; }).length > 0) { payloadData.limitToBounds.push(options.outBoundIds.filter(function (e) { return e.id === id; })); } ids.push(this.duplicateLayout(id, sender, payloadData)); }.bind(this)); return ids; }, updateLayoutType: function updateLayoutType(options, sender, payloadData) { payloadData = this.checkPayloadData(payloadData); var layout = options.id ? this.layout.findModel(options.id) : this.layout; if (layout && options.type) { layout.set({ type: options.type }, { sender: sender, payloadData: payloadData }); } }, /** * Update a layout's drop zones only if the layout has at least one drop zone. * All existing widgets will go untouched as this is purely a layout drop zone change * * @param options.id {string} The id of the layout that has the template/drop zones to be removed/added to * @param options.model {object} The user selected layout model that contains the drop zones * @param sender {object} use if a previous action has taken place and we want to combine this action with it * @param payload {object} the payload of a previous action that has taken place */ updateLayoutDropZones: function updateLayoutDropZones(options, sender, payload) { /* * This little bit of code looks odd but the LayoutModel.findModel() method can potentially return null. * For this reason we need to do the check that the layout variable is a falsey value and if * so then we use the boardModel's layout instance */ var layout; if (options.id) { layout = this.layout.findModel(options.id); } if (!layout) { layout = this.layout; } // Try to get a list of drop zones in the selected layout var dropZones = layout.findDropZones(); // If no drop zones then there is nothing else to do if (!dropZones) { return payload; } // Remove the drop zones while saving the payload because it is needed when adding the new drop zones payload = this.removeLayouts(dropZones.ids, sender, payload); /* * Create the array list of drop zone layouts to add * Set the 'insertBefore' property to be the first widget as all widgets need to come after drop zone layouts. * If there are no widgets then firstWidget returns undefined which is fine */ var firstWidget = layout.listWidgets([layout.id])[0]; var addArray = []; for (var i = 0; i < options.model.items[0].items.length; i++) { addArray.push({ parentId: dropZones.parentId, model: options.model.items[0].items[i], insertBefore: firstWidget }); } payload = this.addLayouts({ parentId: dropZones.parentId, addLayoutArray: addArray }, payload.sender, payload.data); return payload; }, _cloneWidgets: function _cloneWidgets(layoutModel, idMap) { var widgetSpecMap = {}; var widgets = layoutModel.listWidgets([layoutModel.id]); _.each(widgets, function (widgetId) { var widgetModel = this.widgetInstances[widgetId]; var clonedWidget = widgetModel.cloneWidget(idMap); widgetSpecMap[clonedWidget.id] = clonedWidget.toJSON(); }.bind(this)); return widgetSpecMap; }, _cloneLayout: function _cloneLayout(layoutModel, idMap, payload) { var clone = layoutModel.cloneLayout(idMap); if ((clone.type === 'widget' || clone.type === 'group') && clone.style && payload.limitToBounds && payload.limitToBounds.length <= 0) { // offset layout by 5% / 25px down/right for the cloned clone.style.top = clone.incrementStyleValue(clone.style.top); clone.style.left = clone.incrementStyleValue(clone.style.left); } else if (payload.limitToBounds && payload.limitToBounds.length > 0) { payload.limitToBounds[0].forEach(function (element) { if (element.bottom) { clone.style.top = clone.incrementStyleValue(clone.style.top); } if (element.right) { clone.style.left = clone.incrementStyleValue(clone.style.left); } if (!element.right && !element.bottom) { if (parseFloat(clone.decrementStyleValue(clone.style.top)) > 0) { clone.style.top = clone.decrementStyleValue(clone.style.top); } if (parseFloat(clone.decrementStyleValue(clone.style.left)) > 0) { clone.style.left = clone.decrementStyleValue(clone.style.left); } } }); } return clone; }, getLanguageModelOptions: function getLanguageModelOptions() { var addDashboardTranslatedLocales = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false; var languageOptions = { defaultLocale: this.defaultLocale, contentLocale: this.contentLocale, translationLocale: this.translationLocale }; // Add in all the locales the dashboard is currently translated in. This is needed so that new MultilingualAttributes // default to the correct locale if (addDashboardTranslatedLocales) { var translationService = this.dashboardApi.getDashboardCoreSvc('TranslationService'); languageOptions.availableDashboardLocales = translationService.getSelectedLanguages(); } return languageOptions; }, _addWidgetModel: function _addWidgetModel(widgetSpec) { var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; if (!widgetSpec.name) { widgetSpec.name = ''; } //The widget registry supports plugin model classes for times when custom models need to be defined. //Live Widget (for example) is defined in an external component and has a custom model class. var WidgetModelClass = WidgetModel; if (widgetSpec && this.widgetRegistry && this.widgetRegistry[widgetSpec.type] && this.widgetRegistry[widgetSpec.type].ModelClass) { WidgetModelClass = this.widgetRegistry[widgetSpec.type].ModelClass; } var id = options.id, _options$addDashboard = options.addDashboardTranslatedLocales, addDashboardTranslatedLocales = _options$addDashboard === undefined ? true : _options$addDashboard; var languageModelOptions = this.getLanguageModelOptions(addDashboardTranslatedLocales); var model = new WidgetModelClass(widgetSpec, languageModelOptions); model.on('change', this.onWidgetModelChange, this); this.widgetInstances[id || model.id] = model; return model; }, /** * Called when a layout change happens that we want to propagate. (e.g. layout move, resize, etc..) * The layout model takes care of creating the event payload with the undo/redo handler. * * @param payload * @param sender */ onLayoutChange: function onLayoutChange(payload, sender) { this.trigger('change:layout', payload, sender); }, /** * Helper function used to trigger the add/remove event * * @param eventName * @param value * @param prevValue * @param sender * @param payloadData - Additional data that will be included in the event payload. */ _triggerAddRemove: function _triggerAddRemove(eventName, value, prevValue, sender, payloadData) { var payload = { value: value, prevValue: prevValue, sender: sender, senderContext: { applyFn: this.applyFn.bind(this) }, data: payloadData }; this.trigger(eventName, payload); return payload; }, /** * Function used to handler the undo/redo for the board model updates * @param value * @param sender */ applyFn: function applyFn(value, sender, name, payload) { if (value.op && typeof this[value.op] === 'function') { var args = [value.parameter]; args.push(sender); args.push(payload); this[value.op].apply(this, args); } else { Model.inherited('applyFn', this, arguments); } }, /** * Handles changes from widget models (which can be added/removed as widgets are added/removed) and notifies * listener of the change (so listener does not need to track addition/deletion of widget models * * @param payload */ onWidgetModelChange: function onWidgetModelChange(payload) { var senderContext = payload.senderContext || {}; var modelId = _.isObject(payload.sender) ? payload.sender.id : payload.sender; //TODO: Need to revist this when porting to Endor, why are we overwriting the applyFn? //https://github.ibm.com/BusinessAnalytics/dashboard-core/pull/1311 if (payload && payload.senderContext && this.widgetInstances[modelId]) { payload.senderContext.applyFn = function () { var model = this.widgetInstances[modelId]; if (model) { model.applyFn.apply(model, arguments); } }.bind(this); } this.trigger('widget:change', _.extend({ modelId: payload.model ? payload.model.id : modelId, senderContext: senderContext }, payload)); }, toJSON: function toJSON() { var spec = Model.inherited('toJSON', this, [null, ['layout']]); var canvas = this.dashboardApi.getFeature('Canvas'); var topLevelContent = canvas.findContent({ type: this.layout.type })[0]; spec.layout = topLevelContent.getFeature('Serializer').toJSON(); return spec; }, /** * Check the payload data * Create a default transaction id if none is provided so that it is easier to bundler subsequent operation with this one * @param {Object} payloadData */ // TODO: we're actually not "checking" anything in this method, should this be renamed? checkPayloadData: function checkPayloadData(payloadData) { if (payloadData) { return payloadData; } return { undoRedoTransactionId: _.uniqueId('boardModelTransaction') }; }, /** * Find widget model by id and type. * * @param {string} id - widget id * * @return {WidgetModel} widget model instance found */ findWidgetById: function findWidgetById(id) { return this.widgetInstances ? this.widgetInstances[id] : null; }, /** * Find widget model matching one of the ids * * @param {string[]} ids - widget ids to match * * @return {WidgetModel} widget model instance found */ findWidgetByIds: function findWidgetByIds(ids) { var finding; ids.some(function (id) { var breakLoop = false; var widget = this.findWidgetById(id); if (widget) { finding = widget; breakLoop = true; } return breakLoop; }.bind(this)); return finding; }, findWidgetByCriteriaFn: function findWidgetByCriteriaFn(criteriaFn) { var finding; for (var key in this.widgetInstances) { if (this.widgetInstances.hasOwnProperty(key)) { var widgetInstance = this.widgetInstances[key]; if (criteriaFn(widgetInstance)) { finding = widgetInstance; break; } } } return finding; }, filterWidgetsByCriteriaFn: function filterWidgetsByCriteriaFn(criteriaFn) { var findings = []; for (var key in this.widgetInstances) { if (this.widgetInstances.hasOwnProperty(key)) { var widgetInstance = this.widgetInstances[key]; if (criteriaFn(widgetInstance)) { findings.push(widgetInstance); } } } return findings; }, /** * Override the base method since we need to deal with our widgetModels and layoutModel which are * not real nested models */ getContentReferences: function getContentReferences() { var deploymentRefs = Model.inherited('getContentReferences', this, arguments); if (this.widgetInstances) { for (var widgetModel in this.widgetInstances) { deploymentRefs = deploymentRefs.concat(this.widgetInstances[widgetModel].getContentReferences()); } } if (this.layout) { deploymentRefs = deploymentRefs.concat(this.layout.getContentReferences()); } return _.uniq(deploymentRefs, false, function (ref) { return ref.value; }); }, /** * Should only be used by dashboard Serializer feature * @return {Object} the inital board spec */ getInitialSpec: function getInitialSpec() { return this._initBoardSpec; }, /** * Delete the initial board spec, it is invoked only once when canvas is ready */ deleteInitialSpec: function deleteInitialSpec() { delete this._initBoardSpec; } }); return Model; }); //# sourceMappingURL=BoardModel.js.map