'use strict'; var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; /* *+------------------------------------------------------------------------+ *| Licensed Materials - Property of IBM *| IBM Cognos Products: Dashboard *| (C) Copyright IBM Corp. 2017, 2020 *| *| US Government Users Restricted Rights - Use, duplication or disclosure *| restricted by GSA ADP Schedule Contract with IBM Corp. *+------------------------------------------------------------------------+ */ define(['../../../lib/@waca/dashboard-common/dist/core/Collection', 'underscore', './SynchronizeDataEntry', './SynchronizeDataHelper', './SynchronizePageContextEntry', './SynchContextHelper', './SynchJoinTable', '../../../widgets/livewidget/query/QueryService', '../../../widgets/livewidget/query/CommonQueryHelper', '../../../filters/pagecontext/PageContextEntry', '../../../filters/FilterMetadataHelper', '../../utils/DatasourceUtil', '../../../lib/@waca/core-client/js/core-client/utils/UniqueId'], function (Collection, _, SynchronizeDataEntry, SynchronizeDataHelper, SynchronizePageContextEntry, SynchContextHelper, SynchJoinTable, QueryService, CommonQueryHelper, PageContextEntry, FilterMetadataHelper, DatasourceUtil, UniqueId) { /** * The Synchronize Model implements the boardModelExtension defined in synchronizeData.json * It is used to persist information about mapped colours etc. */ var SynchronizeDataModel = Collection.extend({ modelClass: SynchronizeDataEntry, init: function init(spec, options) { SynchronizeDataModel.inherited('init', this, arguments); this.logger = options.logger; this.boardModel = options.boardModel; this.dashboardApi = options.dashboardApi; this.logger = this.dashboardApi.getGlassCoreSvc('.Logger'); this.modules = {}; this.items = {}; this.loadValuesPromises = {}; this.synchronizeDataEntries = {}; this.filterDockItems = {}; this.jointedTablesBrushData = {}; this.widgets = {}; this.eventListener = this.boardModel.on('change:pageContext', this._onChangePageContext, this); this.eventListenerRemoveLayouts = this.boardModel.on('removeLayouts', this._onRemoveWidget, this); this.dashboardApi.on('brushing:undoRedo', this._onUndoRedo, this); this.dashboardApi.on('filterIndicator:removeEntry', this._onRemoveFilterIndicatorEntry, this); this.dashboardApi.on('widget:renderComplete', this._onRenderComplete, this); }, destroy: function destroy() { SynchronizeDataModel.inherited('destroy', this, arguments); if (this.eventListener) { this.eventListener.off(); this.eventListener = null; } if (this.eventListenerRemoveLayouts) { this.eventListenerRemoveLayouts.off(); this.eventListenerRemoveLayouts = null; } this.widgets = {}; this.dashboardApi.off('brushing:undoRedo', this._onUndoRedo, this); this.dashboardApi.off('filterIndicator:removeEntry', this._onRemoveFilterIndicatorEntry, this); this.dashboardApi.off('widget:renderComplete', this._onRenderComplete, this); }, /** * Loads the page context when first open the dashboard that has visualization widgets * * @param {object} pageContextAPI - the page context API * @param {object} visAPI - the viz API * * @return {object} A promise object */ loadBrushedContext: function loadBrushedContext(pageContextAPI, visualization, visAPI) { var _this = this; if (!pageContextAPI || !visAPI) { return Promise.resolve(); } var dataItems = visualization.getSlots().getDataItemList(); if (!dataItems || dataItems.length === 0) { return Promise.resolve(); } var projectedItems = SynchContextHelper.createSynchProjectedItems(visAPI.getModule(), dataItems.map(function (dataItem) { return { getItemId: function getItemId() { return dataItem.getColumnId(); }, getLabel: function getLabel() { return dataItem.getLabel(); } }; })); var pageContextMatchParams = { origin: 'visualization', scope: visAPI.getScope(), eventGroupId: visAPI.getEventGroupId() }; var pageContextItems = pageContextAPI.getPageContextItems(pageContextMatchParams); if (pageContextItems.length === 0) { return Promise.resolve(); } var promises = []; pageContextItems.forEach(function (pageContextItem) { var spec = pageContextItem.getPageContextSpec(); var options = { module: visAPI.getModule(), itemIds: _.pluck(spec.hierarchies, 'hierarchyUniqueName'), data: spec.tupleSet }; promises.push(_this._synchJointedTables(projectedItems, options)); }); return Promise.all(promises); }, /** * Retrieve and return the found page context * This function is ensuring projected data items in joint tables have correct brushed values in vis * * @param {string} dataSourceId - the data source id * @param {object} pageContextAPI - the page context API * @param {string} itemId - the data item to search for a corresponding joined table tuple value * @param {string} pageContextMatchParams - the page context scope to search for page context item * * @return {object} page context entry API object if one found else return undefined */ getPageContextItem: function getPageContextItem(dataSourceId, pageContextAPI, itemId, pageContextMatchParams) { var pageContextItem = pageContextAPI.getPageContextItem(_.extend({ itemId: itemId }, pageContextMatchParams)); if (pageContextItem) { //Found it so return return pageContextItem; } var pageContextItems = pageContextAPI.getPageContextItems({ eventGroupId: pageContextMatchParams.eventGroupId, origin: pageContextMatchParams.origin, scope: pageContextMatchParams.scope }); var synchColumn = void 0; var synchModule = this.jointedTablesBrushData[dataSourceId]; if (synchModule) { synchColumn = synchModule.getColumn(itemId); } if (!synchColumn || pageContextItems.length === 0) { return undefined; } for (var idx = 0; idx < pageContextItems.length; idx++) { var _pageContextItem = pageContextItems[idx]; var result = this._findJoinedTableTupleValue(_pageContextItem, synchColumn); if (result) { //Clone the spec so not to modify the original page context entry model var newSpec = JSON.stringify(_pageContextItem.getPageContextSpec()); newSpec = JSON.parse(newSpec); newSpec.hierarchies = [{ hierarchyUniqueName: itemId }]; newSpec.tupleSet = result.value; return new PageContextEntry(newSpec).getAPI(); } } }, /** * Find and return a matched tuple value if tables are joined * @private * * @param {object} pageContextItem - the pageContextItem to find a jointed table tuple value * @param {object} synchColumn - the column to look up for the jointed table tuple value * * @return and object containing the result if one found else return undefined */ _findJoinedTableTupleValue: function _findJoinedTableTupleValue(pageContextItem, synchColumn) { var spec = pageContextItem.getPageContextSpec(); var itemIds = _.pluck(spec.hierarchies, 'hierarchyUniqueName'); var data = this._isArrayOfArray(spec.tupleSet) ? spec.tupleSet : [spec.tupleSet]; var matchedItemId = void 0; var matchedTuples = void 0; var _loop = function _loop(idx) { data.forEach(function (tupleSet) { tupleSet.forEach(function (tuple, currentIdx) { if (idx === currentIdx) { matchedItemId = itemIds[idx]; var foundTuple = synchColumn.getJointTupleValue(itemIds[idx], tuple); if (foundTuple) { if (!matchedTuples) { matchedTuples = []; } //Array array of values matchedTuples.push([foundTuple]); } } }); }); if (matchedTuples) { return { v: { itemId: matchedItemId, value: matchedTuples } }; } }; for (var idx = 0; idx < itemIds.length; idx++) { var _ret = _loop(idx); if ((typeof _ret === 'undefined' ? 'undefined' : _typeof(_ret)) === "object") return _ret.v; } }, /** * Call to handle a brushing selection to synchronize data across data sources * * @param {object} selection - the selected brushing context * @param {object} options - the selected brushing options * Example of brushing spec * { * "origin": "visualization", * "eventSourceId": "model0000015fef997299_00000000", * "sourceId": "model0000015fef992bd7_00000001", * "scope": "page1", * "eventGroupId": "page1:1", * "hierarchies": [ * {} * ], * "hierarchyUniqueNames": [ * null * ] *} * @return {object} A resolved promise after brushing is handled to synchronize the data across data sources */ handleBrushingSelection: function handleBrushingSelection() { var _this2 = this; var selection = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; var synchOptions = options.synchOptions || {}; return this._doBrushingSelection(selection, options).then(function (results) { if (results) { results.arguments = { command: selection.command, pending: selection.pending }; var append = selection.slotsToClear && selection.slotsToClear.length === 0; selection.synchDataId = UniqueId.get(''); (results.data || []).map(function (item) { item.synchDataId = selection.synchDataId; item.append = append; item.getNetSlotsToClear = SynchContextHelper.getNetSlotsToClearBrushing; return item; }); _this2.dashboardApi.triggerDashboardEvent('synchronizeData:applyBrushing', results); //Is this a non-projected items clear action? var newSelectionIsEmpty = synchOptions.newSelectionIsEmpty(selection.tuple); if (newSelectionIsEmpty && selection.slotsToClear.length > 0) { selection.slotsToClear = SynchContextHelper.getNetSlotsToClearBrushing({ pageContext: synchOptions.pageContext, selector: { origin: 'visualization', scope: selection.brushingSpec.scope, eventGroupId: selection.brushingSpec.eventGroupId, sourceId: selection.brushingSpec.sourceId }, sourceModule: results.brushSource.getModule(), targetModule: results.brushSource.getModule(), dataSlots: selection.slotsToClear, itemIds: selection.itemIds, tableNames: results.brushSource.getTableRef(), newSectionFromSource: true, newSelectionIsEmpty: true }); } } return results; }).finally(function () { _this2._resetData(); }); }, _resetData: function _resetData() { this.modules = {}; this.items = {}; this.loadValuesPromises = {}; this.synchronizeDataEntries = {}; }, _doBrushingSelection: function _doBrushingSelection() { var _this3 = this; var selection = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; //Synchronizaton of data does not support right mouse click (which is for pending = true) if (selection.pending) { return Promise.resolve(); } var brushingSpec = selection.brushingSpec; this._resetBrushing(options); var context = { scope: { origin: 'visualization', scope: brushingSpec.scope, eventGroupId: brushingSpec.eventGroupId, eventSourceId: brushingSpec.eventSourceId }, itemIds: selection.itemIds, tuple: selection.tuple }; return this._getModulesWithinSameEventGroup(brushingSpec.sourceId, brushingSpec.eventGroupId, brushingSpec.eventSourceId).then(function (brushingEntries) { if (brushingEntries) { var results = _this3._synchBrushingSelection(context, options, brushingEntries); if (results) { results.scope = context.scope; } brushingSpec.tableRef = brushingEntries.brushSource.getTableRef(); return results; } }); }, _getNetPageContextItems: function _getNetPageContextItems(scope, eventGroupId, origin, synchDataId) { var paramsForPageContext = { scope: scope, eventGroupId: eventGroupId, origin: origin || 'visualization' }; if (synchDataId) { paramsForPageContext.synchDataId = synchDataId; } return this.pageContext ? this.pageContext.getNetPageContextItems(paramsForPageContext) : []; }, /** * Call to synchronize data for all data sources within same event group id * * @param {string} sourceId - the current selected souce id * @param {string} eventGroupId - the current selected event group id * @param {string} eventSourceId - where the event originating from * * @param {object} return all data source modules base on the specified values of sourceId and eventGroupId */ _getModulesWithinSameEventGroup: function _getModulesWithinSameEventGroup(sourceId, eventGroupId, eventSourceId) { var _this4 = this; var dataViewModelMap = {}; var dataViewModels = function dataViewModels(widget) { //return the fist on for now var models; var data = widget ? widget.data : null; if (data) { models = data.dataViews; } return models; }; var dashboardInternal = this.dashboardApi.getFeature('internal'); var sources = []; var boardModel = dashboardInternal.getBoardModel(); this.pageContext = this.pageContext || boardModel.get('pageContext').getAPI(); var eventGroups = boardModel.get('eventGroups'); var eventGroup = eventGroups.get(eventGroupId); if (eventGroup) { var widgetIds = eventGroup.get('widgetIds'); widgetIds.forEach(function (widgetId) { var widget = boardModel.findWidgetById(widgetId); var models = dataViewModels(widget); if (models) { models.forEach(function (model) { if (model.modelRef) { if (sources.indexOf(model.modelRef) === -1) { sources.push(model.modelRef); } if (!dataViewModelMap[model.modelRef]) { dataViewModelMap[model.modelRef] = []; } if (model.dataItems) { dataViewModelMap[model.modelRef].push({ widgetId: widgetId, projectedItems: model.dataItems.map(function (dataItemModel) { return { getItemId: function getItemId() { return dataItemModel.itemId; }, getLabel: function getLabel() { return dataItemModel.itemLabel; } }; }) }); } } }); } }); } var dataSources = this.dashboardApi.getFeature('dataSources.deprecated'); var promises = []; _.each(sources, function (id) { if (this.modules[id]) { promises.push(Promise.resolve(this.modules[id])); } else { // datasources.depreacted is not a core feature. Module is only required for synchronized data, unfortunately // the current brushing is tightly coupled with synch'd data. // @todo synchronized data should be supported as a query modifier, instead of overcomplicating the page context promises.push(dataSources ? dataSources.getModule(id) : Promise.resolve(null)); } }, this); return Promise.all(promises).then(function (results) { _.each(results, function (module) { // module is not required when synchronized data is not supported if (module && !(module.getSourceId() in this.modules)) { this.modules[module.getSourceId()] = module; //add to cache } }, this); }.bind(this)).then(function () { return _this4._createSynchBrushEntries(eventGroupId, eventSourceId, dataViewModelMap); }); }, _createBrushEntry: function _createBrushEntry(eventGroupId, dataViewModelMap, eventSourceId, callback) { var _this5 = this; var newBrushEntry = void 0; for (var modelRef in dataViewModelMap) { var dataViewModel = dataViewModelMap[modelRef]; if (!dataViewModel.length) { continue; } _.find(dataViewModel, function (dataView) { if (dataView.projectedItems.length) { var module = _this5.modules[modelRef]; if (!eventSourceId || dataView.widgetId === eventSourceId) { newBrushEntry = callback(modelRef, eventGroupId, module, dataView.projectedItems, eventSourceId); return true; } } return false; }); if (newBrushEntry) { break; } } return newBrushEntry; }, _createSynchBrushEntries: function _createSynchBrushEntries(eventGroupId, eventSourceId, dataViewModelMap) { var _this6 = this; var brushEntries = []; var instantianNewBrushEntry = function instantianNewBrushEntry(modelRef, eventGroupId, module, projectedItem, widgetId) { var fromObject = { getSourceId: function getSourceId() { return modelRef; }, getEventGroupId: function getEventGroupId() { return eventGroupId; }, getEventSourceId: function getEventSourceId() { return projectedItem.widgetId; } }; return SynchContextHelper.createNewSynchContext({ fromObject: fromObject, module: module, projectedItems: projectedItem, widgetId: widgetId }); }; /** * dataViewModelMap = { * modelRef: [{ * widgetId: widgetId, * projectedItems: [...] * } * */ var synchSourceContext = this._createBrushEntry(eventGroupId, dataViewModelMap, eventSourceId, instantianNewBrushEntry); if (!synchSourceContext) { return synchSourceContext; } for (var modelRef in dataViewModelMap) { var dataViewModel = dataViewModelMap[modelRef]; if (!dataViewModel.length) { continue; } dataViewModel.forEach(function (dataView) { if (dataView.projectedItems.length && dataView.widgetId !== eventSourceId) { var module = _this6.modules[modelRef]; var newBrushEntry = instantianNewBrushEntry(modelRef, eventGroupId, module, dataView.projectedItems, dataView.widgetId); if (newBrushEntry) { if (SynchContextHelper.haveJoinedTables(synchSourceContext, newBrushEntry)) { synchSourceContext.mergeProjectedItems(newBrushEntry); synchSourceContext.addWidgetId(dataView.widgetId); } else { var existedEntry = _.find(brushEntries, function (entry) { return SynchContextHelper.haveJoinedTables(entry, newBrushEntry); }); if (existedEntry) { existedEntry.mergeProjectedItems(newBrushEntry); existedEntry.addWidgetId(dataView.widgetId); } else { brushEntries.push(newBrushEntry); } } } } }); } return { brushSource: synchSourceContext, entries: brushEntries }; }, /** * Synchronize joined table tuple values match by column names in jointed tables * * @param {array} projectedItems - array of viz projected items to synch tuple values * @param {object} options - options to synch joined table values * * @return {object} A promise object */ _synchJointedTables: function _synchJointedTables(projectedItems) { var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; var module = options.module; var itemIds = options.itemIds; if (!projectedItems || projectedItems.length === 0 || !itemIds || itemIds.length == 0 || !options.data || options.data.length === 0) { return Promise.resolve(); } var promises = []; var isArrayOfArray = this._isArrayOfArray(options.data); var data = isArrayOfArray ? options.data : [options.data]; itemIds.forEach(function (itemId, idx) { var handlers = { loadValues: this._loadValues.bind(this) }; var synchModule = this.jointedTablesBrushData[module.getSourceId()] = this.jointedTablesBrushData[module.getSourceId()] || new SynchJoinTable(module, { handlers: handlers }); var synchColumn = synchModule.getColumn(itemId); if (synchColumn) { projectedItems.forEach(function (projectedItem) { if (synchColumn.getItemId() !== projectedItem.getItemId() && synchColumn.matchedColumnByName(projectedItem.getItemId())) { var synchToFindMatchedValue = synchModule.getColumn(projectedItem.getItemId()); data.forEach(function (tupleSet) { tupleSet.forEach(function (tuple, currentIdx) { if (idx === currentIdx) { promises.push(synchToFindMatchedValue.matchJointedTableValue(synchColumn, tuple)); } }); }); } }); } }.bind(this)); return Promise.all(promises); }, _clearBrushing: function _clearBrushing(tuple) { return !tuple || !_.flatten(tuple).length; }, _synchClearBrushing: function _synchClearBrushing() { var context = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; var _this7 = this; var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; var brushingEntries = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; if (!brushingEntries.entries || brushingEntries.entries.length === 0 || !this._clearBrushing(context.tuple)) { return null; } var results = []; var cloneOptions = SynchronizeDataHelper.clone(options); var payloadData = cloneOptions.payloadData = cloneOptions.payloadData || {}; payloadData.brushedSourceModule = brushingEntries.brushSource.getModule(); payloadData.ignorePageContextChanged = true; var entries = [brushingEntries.brushSource].concat(brushingEntries.entries); entries.forEach(function (entry) { results.push({ sourceId: entry.getSourceId(), tableRef: entry.getTableRef(), itemIds: context.itemIds, sourceModule: brushingEntries.brushSource.getModule(), tuple: context.tuple, options: cloneOptions, ignoreSynchBrushData: _this7._getIgnoreSynchBrushDataWidgetIdList(entry.getWidgetIds()) }); }); return results.length > 0 ? { data: results, brushSource: brushingEntries.brushSource } : null; }, /** * Internal handling of brushing to synchronize the data * * @param {array} itemIds - array of selected data items (e.g. [Product line, Order method]) * @param {array} tuple - the selected tuple values (e.g. [Camping Equipment, Fax]) * @param {object} options - the selected brushing options * @param {object} brushingEntries - the brushing entries to aply brushing * * @return {object} an object contain the synchronize brushing data */ _synchBrushingSelection: function _synchBrushingSelection(context) { var _this8 = this; var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; var brushingEntries = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; if (!brushingEntries.entries || brushingEntries.entries.length === 0) { return null; } if (this._clearBrushing(context.tuple)) { return this._synchClearBrushing(context, options, brushingEntries); } var brushigData = this._constructBrushingData(context, brushingEntries); var results = []; _.each(brushigData.entries, function (entry) { results.push(this._synchEntry(entry.pageContextEntry, { source: brushigData.source, target: entry.targetSynchDataEntry, brushing: true, scope: context.scope })); }, this); if (results.length > 0) { var synchBrushingData; var cloneOptions = SynchronizeDataHelper.clone(options); var payloadData = cloneOptions.payloadData = cloneOptions.payloadData || {}; payloadData.brushedSourceModule = brushingEntries.brushSource.getModule(); payloadData.ignorePageContextChanged = true; results.forEach(function (result, index) { if (result) { if (!synchBrushingData) { synchBrushingData = []; } var entry = brushigData.entries[index].pageContextEntry; var targetSynchDataEntry = brushigData.entries[index].targetSynchDataEntry; var isArrayOfArray = brushigData.entries[index].isArrayOfArray; var tupleSet = isArrayOfArray ? entry.tupleSet : entry.tupleSet.length > 0 ? entry.tupleSet[0] : []; var value = { sourceId: targetSynchDataEntry.getSourceId(), tableRef: targetSynchDataEntry.getTableRef(), itemIds: _.pluck(entry.hierarchies, 'hierarchyUniqueName'), sourceModule: brushingEntries.brushSource.getModule(), tuple: tupleSet, options: cloneOptions, ignoreSynchBrushData: _this8._getIgnoreSynchBrushDataWidgetIdList(targetSynchDataEntry.getWidgetIds()) }; synchBrushingData.push(value); } }, this); return { brushSource: brushingEntries.brushSource, data: synchBrushingData }; } }, /** * Return an array of widget ids that should ignore synchronize brushed data * * @param {array} widgetIds - array of widget ids to process * * @return {array} List of widget ids that must ignore synchronize brushed dataƛ */ _getIgnoreSynchBrushDataWidgetIdList: function _getIgnoreSynchBrushDataWidgetIdList() { var _this9 = this; var widgetIds = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : []; var edgeSelectionIsTrue = []; var edgeSelectionIsFalse = []; widgetIds.forEach(function (id) { var widget = _this9.widgets[id]; if (widget) { if (widget.getEdgeSelection()) { edgeSelectionIsTrue.push(widget.getId()); } else { edgeSelectionIsFalse.push(widget.getId()); } } }); //Non edge selection brushing type take precedence over edge selection type. //Currenty summary, table, grid have edgeSection type equals to true, the rest have this flag equal to false //During synchronize brushing, edge selection type is ignoring synch brush event to avoid creating edge brush event if there is at least one non-edge selection vis return edgeSelectionIsFalse.length ? edgeSelectionIsTrue : []; }, /** * Construct brushing data to synchronize across data sources * * @param {array} itemIds - array of selected data items (e.g. [Product line, Order method]) * @param {array} tuple - the selected tuple values (e.g. [Camping Equipment, Fax]) * * @param {object} return an object containing the synchronize brushing data */ _constructBrushingData: function _constructBrushingData() { var context = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; var brushingData = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; var dataEntry = this._getSynchDataEntry(brushingData.brushSource.getSourceId(), context.scope, brushingData.brushSource.getTableRef()); brushingData.brushSource.setSynchDataEntry(dataEntry); var targets = this._getTargetBrushingEntries(brushingData.entries, context.scope); return { source: brushingData.brushSource.getAPI(), entries: this._constructPageContextEntries(context.itemIds, context.tuple, brushingData.brushSource, targets) }; }, /** * Construct page context entries for brushing * * @param {array} itemIds - array of selected data items (e.g. [Product line, Order method]) * @param {array} tuple - the selected tuple values (e.g. [Camping Equipment, Fax]) * @param {object} source - the source brushing * @param {object} targets - an object containing the set of target modules * * @param {array} return an array of page context entries */ _constructPageContextEntries: function _constructPageContextEntries(itemIds, tuple, source, targets) { var _this10 = this; var entries = []; _.each(targets, function (target) { var isArrayOfArray = _this10._isArrayOfArray(tuple); var clonedTuple = SynchronizeDataHelper.clone(tuple); var pageContextEntry = { hierarchies: _this10._getHierarchies(itemIds), tupleSet: isArrayOfArray ? clonedTuple : [clonedTuple], sourceId: source.getSourceId() }; entries.push({ pageContextEntry: pageContextEntry, targetSynchDataEntry: target, isArrayOfArray: isArrayOfArray }); }); return entries; }, _getHierarchies: function _getHierarchies(itemIds) { if (Array.isArray(itemIds)) { return _.map(itemIds, function (itemId) { return { hierarchyUniqueName: itemId }; }); } else if (_.isString(itemIds)) { return [{ hierarchyUniqueName: itemIds }]; } else { return []; } }, _isArrayOfArray: function _isArrayOfArray(tuple) { return _.isArray(tuple) && tuple.length > 0 && _.isArray(tuple[0]); }, synchTupleSet: function synchTupleSet(module, itemIds, tupleSet) { var synched = false; var cloneTupleSet = SynchronizeDataHelper.clone(tupleSet); _.each(cloneTupleSet, function (tuple) { _.each(tuple, function (value, index) { var itemId = itemIds[index].hierarchyUniqueName; var values = this._getValues(module.getSourceId(), itemId) || []; if (SynchronizeDataHelper.matchItemByValue(values, value)) { synched = true; } }, this); }, this); return synched ? cloneTupleSet : tupleSet; }, /** * @override */ toJSON: function toJSON() { //1. clone models var modelMap = {}; this.each(function (model) { if (model.hasSynchronizeData()) { var spec = model.toJSON(); var cloneModel = new SynchronizeDataEntry(spec); modelMap[SynchContextHelper.getKey(cloneModel.getSourceId(), cloneModel.getTableRef())] = cloneModel; } }.bind(this)); //2. merge entries to remove duplicates _.each(modelMap, function (model) { SynchronizeDataHelper.mergeDuplicates(model.getSourceId(), model.getTableRef(), model.getSynchronizeItems(), modelMap); }); //3. Remove ones not needed _.each(modelMap, function (model) { if (!model.hasSynchronizeData()) { delete modelMap[SynchContextHelper.getKey(model.getSourceId(), model.getTableRef())]; } }); return _.map(modelMap, function (model) { return model.toJSON(); }); }, /** * Returns a boolean indicating whether the column name has been referenced in filter dock. * The column name is matched in all data sources and return true when there is a first matched column name in a data source * * @param {object} scopeToCheck - the scope contains the set of data sources to search for a match of column name * @param {object} column - the column whose name is being search to match * * @return {boolean} true if there is a match else return false */ _hasFilterDockItemMatchByColumnName: function _hasFilterDockItemMatchByColumnName(scopeToCheck, column) { var label = column.getLabel(); var hasFilterDockItem = false; for (var sourceIdToCheck in scopeToCheck) { var filterDockItem = _.find(scopeToCheck[sourceIdToCheck], function (item) { return item.getItemName() === label; }); if (filterDockItem) { hasFilterDockItem = true; break; } } return hasFilterDockItem; }, /** * Add/remove a column from filter dock * * @param {object} item - the filter dock item being added/deleted from filter dock * @param {string} scope - the scope (global/current tab) where the item is being added/deleted * @param {boolean} isAdding - true when the item is being added */ onAddRemoveFilterItem: function onAddRemoveFilterItem(item, scope, isAdding) { if (item && scope) { var currentScope = this.filterDockItems[scope] = this.filterDockItems[scope] || {}; var currentSource = currentScope[item.getSourceId()] = currentScope[item.getSourceId()] || {}; if (isAdding) { currentSource[item.getItemId()] = currentSource[item.getItemId()] || item; } else { delete currentSource[item.getItemId()]; } } }, /** * Internal api to handle the synchronization of data for the pageContextEntry * * @param {object} pageContextEntry - the page context entry to synchronize data * @param {object} options.source - the SynchronizeDataEntry to synchronize data from * @param {object} options.target - the SynchronizeDataEntry to synchronize data to * * @return {boolean} Return the synchronized data for the pageContextEntry */ _synchEntry: function _synchEntry(pageContextEntry) { var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; //clone here since removing collection items on the fly, done to not affect the iterating over collection process var results = []; var cloneHierarchies = SynchronizeDataHelper.clone(pageContextEntry.hierarchies); _.each(cloneHierarchies, function (hierarchy, index) { var context = { id: hierarchy.hierarchyUniqueName, tupleSet: pageContextEntry.tupleSet, conditions: pageContextEntry.conditions, index: index, source: options.source, target: options.target, scope: options.scope, origin: options.origin, brushing: options.brushing }; results.push(this._synchItemUseValues(pageContextEntry.hierarchies, context)); }, this); if (results.length > 0) { var applySynchronized = false; var synchedHierarchies = []; var synchedTupleSet = []; results.forEach(function (isSynchronized, hierarchyIdx) { var result = SynchronizeDataHelper.isBoolean(isSynchronized) ? isSynchronized : false; if (result) { if (!applySynchronized) { applySynchronized = result; } synchedHierarchies.push(pageContextEntry.hierarchies[hierarchyIdx]); pageContextEntry.tupleSet.forEach(function (tuple, tupleSetIdx) { var resultTuple = void 0; if (tupleSetIdx < synchedTupleSet.length) { resultTuple = synchedTupleSet[tupleSetIdx]; } else { resultTuple = []; synchedTupleSet.push(resultTuple); } tuple.forEach(function (value, tupleIdx) { if (hierarchyIdx === tupleIdx) { resultTuple.push(value); } }); }); } }); pageContextEntry.hierarchies = synchedHierarchies; pageContextEntry.tupleSet = synchedTupleSet; if (applySynchronized) { pageContextEntry.sourceId = options.target.getSourceId(); } return applySynchronized; } return false; }, /** * Internal api to handle the synchronization of data for the hierarchies * * @param {object} hierarchies - the collection of hierarchies to synchronize data to * @param {string} options.id - the data item use value to synchronize data from * @param {object} options.tupleSet - the tupleSet to synchronize data to * @param {object} options.source - the source to synchronize data from * @param {object} options.target - the target to synchronize data to * * @return {boolean} the synchronized boolean flag indicating whether the hierarchies had been synchronized or not */ _synchItemUseValues: function _synchItemUseValues(hierarchies, options) { options = options || {}; var source = options.source; var target = options.target; var matchedColumn; var sourceModule = this.modules[source.getSourceId()]; var targetModule = this.modules[target.getSourceId()]; var synchSource = source.getSynchronizeItemId(options.id, target.getSourceId(), target.getTableRef()); if (synchSource) { matchedColumn = targetModule.getMetadataColumn(synchSource.itemId); } else { var column = sourceModule.getMetadataColumn(options.id); if (column) { var context = { projectedItems: target.getProjectedItems() }; matchedColumn = SynchronizeDataHelper.matchColumByName(sourceModule, targetModule, column, context); } } if (!matchedColumn) { return Promise.resolve(false); } this._synchConditions(options.conditions, matchedColumn); var isSynchronized = options.conditions ? true : this._synchByValues(targetModule, { sourceColumn: source.getModule().getMetadataColumn(options.id), targetColumn: matchedColumn, hierarchies: hierarchies, tupleSet: options.tupleSet, conditions: options.conditions, currentIndex: options.index, scope: options.scope, origin: options.origin, brushing: options.brushing }); if (isSynchronized) { hierarchies[options.index].hierarchyUniqueName = matchedColumn.getId(); if (!synchSource) { source.addSynchronizeItem(options.id, target.getSourceId(), matchedColumn.getId(), target.getTableRef()); } target.addSynchronizeItem(matchedColumn.getId(), source.getSourceId(), options.id, source.getTableRef()); this._addSynchronizeDataEntry(source); this._addSynchronizeDataEntry(target); } return isSynchronized; }, /** * Internal api to handle the synchronization of data for the conditions * * @param {string} options.targetColumn - the target metadata column to synch data to * @param {array} options.conditions - the hierarchies to synch data to * * @return {object} A promise resolved to return the synchronized data for the conditions */ _synchConditions: function _synchConditions(conditions, targetColumn) { if (conditions && targetColumn) { _.each(conditions, function (condition) { var attributeUniqueNames = condition.attributeUniqueNames; if (_.isArray(attributeUniqueNames) && attributeUniqueNames.length > 0) { attributeUniqueNames[0] = targetColumn.getId(); } }); } }, _emptyTupleSet: function _emptyTupleSet(tupleSet) { return !(tupleSet && !!tupleSet.length && tupleSet[0] && !!tupleSet[0].length); }, /** * Internal api to handle the synchronization of data for the hierarchies * * @param {object} targetModule - the target module to synch data to * @param {string} options.targetColumn - the target metadata column to synch data to * @param {array} options.hierarchies - the hierarchies to synch data to * @param {array} options.tupleSet - the tuple set to synch data to * @param {number} options.currentIndex - the current index to the hierarchies array * * @return {boolean} Return the synchronized data for the hierarchies and tupleSet */ _synchByValues: function _synchByValues(targetModule) { var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; var tupleSet = options.tupleSet; /*Note added January 17, 2019 * ============================ * Synchronize data between different tables had been wired off for other types of datasource such as package. Currently only flat file types for flat file types within modules are supported. * Synchronize data between Global/local filters are also wired off as well. Since for filters users can define any values to filter where these values may be not be in the data source, this is a non-issue here as well * * Thefore what is left here is synchronize between brushing which requires looking at the data in the data source. */ if (options.brushing && this._emptyTupleSet(tupleSet)) { return Promise.resolve(true); } var currentIndex = options.currentIndex; var isSynchronized = false; var cloneTupleSet = SynchronizeDataHelper.clone(tupleSet); var matchTupleSet = function matchTupleSet() { cloneTupleSet.forEach(function (tuple, outerIndex) { tuple.forEach(function (value, index) { if (tupleSet[outerIndex][index] && index === currentIndex) { if (SynchContextHelper.replaceUseValue(options.targetColumn, tupleSet[outerIndex][index])) { isSynchronized = true; } else { //Removing the item since don't match options.hierarchies.splice(index, 1); tupleSet[outerIndex].splice(index, 1); } } }); }); }; matchTupleSet(); return isSynchronized; }, _addItemValues: function _addItemValues(sourceId, id, values) { if (!(sourceId in this.items)) { this.items[sourceId] = {}; } var item = this.items[sourceId]; item[id] = values; }, _getModule: function _getModule(sourceId) { return this.modules[sourceId]; }, _loadValues: function _loadValues(module, id) { var key = module.getSourceId() + id; var promise = this.loadValuesPromises[key]; if (!promise) { promise = this.loadValuesPromises[key] = this._loadValuesForMetadataColumn(module, id); } return promise.then(function (values) { this._addItemValues(module.getSourceId(), id, values); return values; }.bind(this)); }, _getSynchDataEntry: function _getSynchDataEntry(sourceId, scope, tableRef) { var entry = this.synchronizeDataEntries[SynchContextHelper.getKey(sourceId, tableRef)]; if (!entry || !entry.inSameScope() || !entry.inSameTable(tableRef)) { var spec = { sourceId: sourceId, scope: scope.scope, eventGroupId: scope.eventGroupId, tableRef: tableRef }; entry = new SynchronizeDataEntry(spec); } return entry; }, _getTargetBrushingEntries: function _getTargetBrushingEntries(context, scope) { var entries = {}; _.each(context, function (contextEntry) { var tableRef = contextEntry.getTableRef(); var sourceId = contextEntry.getSourceId(); var dataEntry = this._getSynchDataEntry(sourceId, scope, tableRef); contextEntry.setSynchDataEntry(dataEntry); entries[SynchContextHelper.getKey(sourceId, tableRef)] = contextEntry.getAPI(); }.bind(this)); return entries; }, _addSynchronizeDataEntry: function _addSynchronizeDataEntry(fromContext) { var key = SynchContextHelper.getKey(fromContext.getSourceId(), fromContext.getTableRef()); var entry = this.synchronizeDataEntries[key]; if (entry) { entry.set({ synchronizeItems: fromContext.getSynchronizeItems() }, { silent: true }); } else { var spec = fromContext.toJSON(); var newEntry = this.add(spec, { silent: true }); this.synchronizeDataEntries[key] = newEntry; } }, _loadValuesForMetadataColumn: function _loadValuesForMetadataColumn(module, id) { var columnInfo = { column: { columnId: id }, sort: 'asc' }; var queryHelper = this._getQueryHelper(); return queryHelper.whenSingleItemQueryReady(module.getSourceId(), columnInfo).then(function (values) { return queryHelper.extactListOfQueryResultData(values); }.bind(this)); }, _getQueryHelper: function _getQueryHelper() { if (this._queryHelper) { return this._queryHelper; } var queryService = new QueryService({ dashboardApi: this.dashboardApi }); this._queryHelper = new CommonQueryHelper({ queryService: queryService }, this.logger); return this._queryHelper; }, _getValues: function _getValues(sourceId, itemId) { return sourceId in this.items ? this.items[sourceId][itemId] : null; }, _getConditionValue: function _getConditionValue(conditions) { var value = {}; if (_.isArray(conditions) && !!conditions.length) { value.from = conditions[0].from[0]; value.to = conditions[0].to[0]; value.invert = conditions[0].invert; } return value; }, /** * Calls to reset brushing context when first brushing occur * * @param {object} options - brushing options */ _resetBrushing: function _resetBrushing(options) { options = options || {}; var payloadData = options.payloadData || {}; payloadData.isBrushed = true; this.undoRedo = []; }, /** * Handle 'change:pageContext' event * * @param {object} event - the event object of the 'change:pageContext' event */ _onChangePageContext: function _onChangePageContext(event) { if (this._canProcessPageContextChange(event)) { var cloneData = SynchronizeDataHelper.clone(event.data); cloneData.ignoreUndoPageContextChanged = this.undoRedo.length === 0 ? false : true; cloneData.ignoreRedoPageContextChanged = false; event.data = cloneData; this.undoRedo.push(event); for (var idx = 0; idx < this.undoRedo.length - 1; idx++) { this.undoRedo[idx].data.ignoreRedoPageContextChanged = true; } } }, /** * Handle 'brushing:undoRedo' event * * @param {object} event - the event object of the 'brushing:undoRedo' event */ _onUndoRedo: function _onUndoRedo(payload) { payload = payload || {}; if (payload.undoRedoTransactionType === 'undo') { payload.ignorePageContextChanged = payload.ignoreUndoPageContextChanged; } else if (payload.undoRedoTransactionType === 'redo') { payload.ignorePageContextChanged = payload.ignoreRedoPageContextChanged; } }, _onRemoveFilterIndicatorEntry: function _onRemoveFilterIndicatorEntry(payload) { payload = payload || {}; var pageContextItem = payload.pageContext.getPageContextItem({ id: payload.filterToDelete }); if (pageContextItem) { var synchDataId = pageContextItem.getSynchDataId(); //@todo what is isValueDataItem? if (pageContextItem.getIsModeledFilter()) { payload.pageContext.deletePageContextItems({ id: payload.filterToDelete }); } else if (!synchDataId || pageContextItem.getOrigin() !== 'visualization' && !pageContextItem.isValueDataItem()) { if (pageContextItem.getOrigin() === 'visualization') { payload.pageContext.deletePageContextItems({ id: payload.filterToDelete }); } else { // defect288403: nameset filter with no selection, if remove from the filter indicator, the page context entry should be removed too if (pageContextItem.isValueDataItem()) { var isItemRemoved = payload.pageContext.deletePageContextItems({ id: payload.filterToDelete }); if (isItemRemoved) { // clear the cache in filterDockItems to synch var currentScope = this.filterDockItems[pageContextItem.getScope()] = this.filterDockItems[pageContextItem.getScope()] || {}; var currentSource = currentScope[pageContextItem.getSourceId()] = currentScope[pageContextItem.getSourceId()] || {}; delete currentSource[pageContextItem.getItemId()]; } } else { payload.pageContext.clearPageContextItemValues({ id: payload.filterToDelete }); } } } else if (synchDataId) { var deleteSpec = JSON.parse(payload.filterToDelete); var deleteOptions = { sourceIdsToUpdate: {}, removedItems: [], payloadData: { undoRedoTransactionId: UniqueId.get('delete_viz_filter_entry_'), transactionToken: payload.transactionToken } }; var netPageContext = this._getNetPageContextItems(deleteSpec.scope, deleteSpec.eventGroupId, 'visualization', synchDataId); netPageContext.forEach(function (pageContext) { payload.pageContext.deleteVisTypePageContext({ id: pageContext.getKey() }, deleteOptions); }); if (!deleteOptions.handlers || !deleteOptions.handlers.filterContextUpdated) { throw new Error('Invalid delete filter indicator options'); } var updatateContext = { removedList: deleteOptions.removedItems, removedItems: deleteOptions.removedItems, sourceIdsToUpdate: deleteOptions.sourceIdsToUpdate }; deleteOptions.handlers.filterContextUpdated(updatateContext, { transactionId: deleteOptions.transactionId }); } } }, /** * Returns a boolean indicating whether can process 'change:pageContext' event * * @param {object} event - the event object of the 'change:pageContext' event * * @return {boolean} true if can process else return false */ _canProcessPageContextChange: function _canProcessPageContextChange(event) { event = event || {}; var data = event.data || {}; return data.isBrushed && event.name !== 'add'; }, /** * Callback to process render complete event for a vis widget * * @param {object} payload - the payloda to process the event */ _onRenderComplete: function _onRenderComplete() { var payload = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; var widget = payload.widget; var id = widget && widget.widgetAPI ? widget.widgetAPI.getId() : null; if (id) { this.widgets[id] = { getEdgeSelection: function getEdgeSelection() { return widget.getEdgeSelection(); }, getSourceId: function getSourceId() { return widget.widgetAPI.getVisApi().getModule().getSourceId(); }, getId: function getId() { return widget.widgetAPI.getId(); } }; } }, /** * Callback to process the remove vis widget event * * @param {object} payload - the payloda to process the event */ _onRemoveWidget: function _onRemoveWidget() { var _this11 = this; var payload = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; var value = payload.value || {}; var parameter = value.parameter || []; parameter.forEach(function (id) { delete _this11.widgets[id]; }); } }); return SynchronizeDataModel; }); /** Example: [ { "sourceId": "QC_Data", 'tableRef: "Sheet_1", "synchronizeItems": [ { "id": "Product line", "items": [ { "sourceId": "Hollywood Movies", 'tableRef: "Sheet_2", "id": "Product line" }, { "sourceId": "Sales and Marketing", "id": "Product line" } ] }, { "id": "Year", "items": [ { "sourceId": "Hollywood Movies", "id": "Year" } ] } ] }, { "sourceId": "Hollywood Movies", "synchronizeItems": [ { "itemId": "Country", "items": [ { "sourceId": "QC_Data", "itemId": "Country" }, { "sourceId": "Sales and Marketing", "itemId": "Country" } ] }, { "id": "Location", "items": [ { "sourceId": "Hollywood Movies", "itemId": "Location" } ] } ] } ] */ //# sourceMappingURL=SynchronizeDataModel.js.map