CopyPasteController.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459
  1. 'use strict';
  2. function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
  3. /**
  4. * Licensed Materials - Property of IBM
  5. * IBM Cognos Products: BI Cloud (C) Copyright IBM Corp. 2018, 2020
  6. * US Government Users Restricted Rights - Use, duplication or disclosure restricted by GSA ADP Schedule Contract with IBM Corp.
  7. */
  8. define(['underscore', './util/DashboardSpecHelper', '../app/nls/StringResources'], function (_, DashboardSpecHelper, StringResources) {
  9. return function () {
  10. /**
  11. * @classdesc CopPasteController
  12. * Used for copying and pasting widgets across different CA dashboards
  13. * Copy flow for DB => select widgets from DB spec => store in glass clipboard service in shared spec obj => show toast msgs
  14. * Copy flow for siblings => Use this classes doCopy method and provide a overwriteSpec
  15. * Paste flow for DB => get data from clipboard service => convert via conversion service => render widget & show toast msgs
  16. * Paste flow for children => provide a overwriteTarget to doPaste => accept returned converted spec and render appropriately; doPaste will show toast msgs
  17. * @param {Object} options Expects layoutController, dashboardAPI, logger, model, api
  18. */
  19. function CopyPasteController(options) {
  20. _classCallCheck(this, CopyPasteController);
  21. this.layoutController = options.layoutController;
  22. this.dashboardApi = options.dashboardApi;
  23. this.logger = options.logger;
  24. this.model = options.model;
  25. this.api = options.api;
  26. this.specType = options.type || 'DASHBOARD';
  27. this.clipboard = this.dashboardApi.getGlassCoreSvc('.Clipboard');
  28. }
  29. CopyPasteController.prototype.destroy = function destroy() {
  30. this.layoutController = null;
  31. this.dashboardApi = null;
  32. this.logger = null;
  33. this.model = null;
  34. this.api = null;
  35. this.clipboard = null;
  36. };
  37. CopyPasteController.prototype.getSpecHelper = function getSpecHelper() {
  38. var _this = this;
  39. if (this.specHelper) {
  40. return Promise.resolve(this.specHelper);
  41. }
  42. return this.layoutController.getInteractionController().then(function (controller) {
  43. _this.specHelper = new DashboardSpecHelper(controller);
  44. return _this.specHelper;
  45. });
  46. };
  47. /**
  48. * Handles the copy gesture by getting the spec and saving it
  49. * @param {Object} overwriteSpec Spec obj with {type: str, spec: json str, count: number}
  50. * @return {Promise} Returns a promise which will resolve after glass
  51. */
  52. CopyPasteController.prototype.doCopy = function doCopy(overwriteSpec) {
  53. var _this2 = this;
  54. return this.getSpecHelper().then(function (specHelper) {
  55. var currentTime = new Date().getTime(),
  56. sharedSpecObj = void 0;
  57. var overwriteSpecIsValid = function overwriteSpecIsValid(spec) {
  58. return spec && spec.type !== 'DASHBOARD' && spec.count;
  59. };
  60. // make sure incoming overwritespec fits our shared criteria
  61. if (overwriteSpecIsValid(overwriteSpec)) {
  62. sharedSpecObj = overwriteSpec;
  63. } else {
  64. sharedSpecObj = _this2.selectFromSpecForCopyPaste(specHelper);
  65. sharedSpecObj.type = _this2.specType;
  66. }
  67. sharedSpecObj.timestamp = currentTime; // Set timestamp for Reporting (TODO: verify and remove in the future)
  68. return _this2.clipboard.set(sharedSpecObj).then(function () {
  69. if (sharedSpecObj.errMsg) {
  70. _this2._showToast(sharedSpecObj.errMsg);
  71. }
  72. if (sharedSpecObj.count > 0) {
  73. return _this2._showToast('copy', { count: sharedSpecObj.count });
  74. }
  75. return null;
  76. });
  77. }).catch(function (err) {
  78. return _this2._showError('specCopyErr', err);
  79. });
  80. };
  81. /**
  82. * Handles the paste gesture by getting the spec from localstorage, validating it then performs the paste
  83. * @param {String} overwriteTarget return a converted spec for a different target; Requires a converter registered with the conversion-service with this target
  84. * @return {Promise} Returns a promise which will resolve after glass
  85. */
  86. CopyPasteController.prototype.doPaste = function doPaste(overwriteTarget) {
  87. var _this3 = this;
  88. if (overwriteTarget) {
  89. return this.getConvertedSpec(overwriteTarget).then(function (spec) {
  90. return spec;
  91. }).catch(function (err) {
  92. return _this3._showError('specConvertErr', err);
  93. });
  94. }
  95. return this.getConvertedSpec(this.specType).then(function (spec) {
  96. if (!spec) return;
  97. return _this3.getSpecHelper().then(function (specHelper) {
  98. return specHelper.validateDashboardSpec(spec);
  99. });
  100. }).then(function (spec) {
  101. if (!spec) return;
  102. // remove any current selections
  103. _this3.layoutController.interactionController.selectionHandler.deselectAll();
  104. return _this3._performPaste(spec);
  105. }).catch(function (err) {
  106. return _this3._showError('specConvertErr', err);
  107. });
  108. };
  109. /**
  110. * Retrieves a converted spec, running through the clipboard and conversion services
  111. * @returns {Promise} resolves to return the minimized spec selected widget(s) as a JSON string
  112. */
  113. CopyPasteController.prototype.getConvertedSpec = function getConvertedSpec(target) {
  114. var _this4 = this;
  115. return this.clipboard.get().then(function (spec) {
  116. return _this4._doConvert(spec, target);
  117. });
  118. };
  119. CopyPasteController.prototype._doConvert = function _doConvert(spec, target) {
  120. var src = spec.type;
  121. var data = spec.spec;
  122. if (src == 'REPORT') {
  123. this._showError('pasteNotSupportedErr', { name: StringResources.get('reportParam') });
  124. return null;
  125. }
  126. return src === target || !data ? Promise.resolve(data) : this.dashboardApi.getGlassSvc('.ConversionService').then(function (srvc) {
  127. return srvc.convert(src, target, data);
  128. });
  129. };
  130. /**
  131. * Selects the widgets to copy
  132. * @returns returns the minimized spec selected widget(s) as JSON
  133. */
  134. CopyPasteController.prototype.selectFromSpecForCopyPaste = function selectFromSpecForCopyPaste(specHelper) {
  135. var returnObject = {
  136. spec: null,
  137. count: 0
  138. };
  139. var selectedContent = this.dashboardApi.getCanvas().getSelectedContentList();
  140. if (selectedContent.length) {
  141. returnObject = specHelper.getContentsToJSONSpec();
  142. }
  143. return returnObject;
  144. };
  145. /**
  146. * TODO - look at organizing this better either by creating a separate class, or even consider how the spec fragment is being handled in general.
  147. * TODO - Currently, it is being extended on a need to basis, it might be better to copy the full spec and take what is needed based on some configuration file
  148. * Does the work of pasting widgets
  149. * @param {object} spec - spec representing what to paste
  150. * @return {Promise} Returns a resolved promise if all widgets are pasted correctly or there is nothing to paste
  151. */
  152. CopyPasteController.prototype._performPaste = function _performPaste(spec) {
  153. var _this5 = this;
  154. if (spec.nonMergedWidgets) {
  155. spec.widgets = spec.nonMergedWidgets;
  156. delete spec.nonMergedWidgets;
  157. }
  158. //let dockFiltersHandled = false;
  159. //let targetTabId = this.layoutController.getCurrentSubViewId && this.layoutController.getCurrentSubViewId();
  160. //let emptyDB = this.layoutController.boardModel && (this.layoutController.boardModel.pageContext.models && this.layoutController.boardModel.pageContext.models.length === 0) &&
  161. // (this.layoutController.boardModel.widgetInstances && Object.keys(this.layoutController.boardModel.widgetInstances).length === 0);
  162. var getLayout = function getLayout(idx) {
  163. return idx < spec.layout.length ? spec.layout[idx] : undefined;
  164. };
  165. var findObj = function findObj(ary, id) {
  166. return _.find(ary, function (ele) {
  167. return id === ele.id;
  168. });
  169. };
  170. function findSource(widget, sourceMap) {
  171. if (widget.data && widget.data.dataViews) {
  172. _.each(widget.data.dataViews, function (view) {
  173. var newSource = _.find(spec.dataSources.sources, function (source) {
  174. return source.id === view.modelRef;
  175. });
  176. sourceMap[view.modelRef] = newSource;
  177. });
  178. }
  179. }
  180. // finds a widget and associated data source
  181. function findWidget(id, widgets, sourceMap) {
  182. var widget = findObj(spec.widgets, id);
  183. if (widget) {
  184. // get datasource associated to widget
  185. findSource(widget, sourceMap);
  186. widgets.push(widget);
  187. }
  188. }
  189. // recursively finds a widgets and associated data sources
  190. function findWidgets(items, widgets, sourceMap) {
  191. _.each(items, function (item) {
  192. return item.items ? findWidgets(item.items, widgets, sourceMap) : findWidget(item.id, widgets, sourceMap);
  193. });
  194. }
  195. /*
  196. const determinePageContextForCopyToEmptyDB = () => {
  197. let pageContextTabFilters = [];
  198. _.each(spec.pageContext, (pageContext) => {
  199. if( pageContext.scope !== 'global') {
  200. pageContext.scope = targetTabId;
  201. }
  202. pageContextTabFilters.push(pageContext);
  203. });
  204. return pageContextTabFilters;
  205. };
  206. */
  207. /*
  208. const determinePageContextForCopyToSameDB = () => {
  209. let pageContextTabFilters = [];
  210. _.each(spec.pageContext, (pageContext) => {
  211. // if copying within the same DB, ignore global filters and only copy to a different tab
  212. if(pageContext.scope !== 'global' && targetTabId !== pageContext.scope) {
  213. // only add if it doesn't interfere with target tab's filters
  214. let currTabFilter = _.find(this.layoutController.boardModel.pageContext.models, (currPageContext) => {
  215. return currPageContext.scope === targetTabId && currPageContext.hierarchyUniqueNames[0] === pageContext.hierarchyUniqueNames[0];
  216. });
  217. // copy filters if there are no filters on the target tab or the existing filters does not use the same datasource dataitem
  218. if(!currTabFilter) {
  219. pageContext.scope = targetTabId;
  220. pageContextTabFilters.push(pageContext);
  221. }
  222. }
  223. });
  224. return pageContextTabFilters;
  225. };
  226. */
  227. /** This method will
  228. *
  229. * Same DB and Tab Same DB different tab Different (New) DB Different (Existing) DB
  230. * ---------------------- ---------------------------- ------------------- -----------------------
  231. * Tab Filters don't copy or convert convert if not page cxt filter on same DI copy as tab filter convert to local filters if not page cxt filter on same DI
  232. * Global Filters don't copy or convert don't copy or convert copy as global filter convert to local filters if not page cxt filter on same DI
  233. *
  234. * For filters with property 'isUserFilter', these are user defined filter so they must always be copied
  235. const handleFilters = () => {
  236. // if pasting to the same DB or new DB, handle filter otherwise keep filters as local filters which have already been added
  237. // to the widgets during the 'copy' action
  238. if(this.layoutController.topLayoutModel.id === spec.dashboardID || emptyDB) {
  239. // remove widget local filters generated by the 'copy' action, since they exist in the pageContext.
  240. _.each(fragmentModel.widgets, (widget) => {
  241. widget.localFilters = _.where( widget.localFilters, { isUserFilter: true });
  242. if (!widget.localFilters.length) {
  243. delete widget.localFilters;
  244. }
  245. });
  246. if(!dockFiltersHandled) {
  247. if(!emptyDB) {
  248. fragmentModel.pageContext = determinePageContextForCopyToSameDB();
  249. } else {
  250. fragmentModel.pageContext = determinePageContextForCopyToEmptyDB();
  251. }
  252. dockFiltersHandled = true;
  253. }
  254. } else {
  255. // remove any local filters generated by the 'copy' action that uses the same DI as the target's filters (if any). Only handle origin=filters, don't want to
  256. // remove local filters for pagecontext with origin=visualization
  257. let filters;
  258. let pcFiltersOnly = _.filter(this.layoutController.boardModel.pageContext.models, (pc) => {
  259. return pc.origin === 'filter' && (pc.scope === 'global' || pc.scope === targetTabId);
  260. });
  261. _.each(fragmentModel.widgets, (widget) => {
  262. if( widget.localFilters) {
  263. const filterColumns = _.chain(pcFiltersOnly).pluck('hierarchyUniqueNames').flatten().value();
  264. filters = _.filter(widget.localFilters, (filter) => {
  265. return filter.isUserFilter || filterColumns.indexOf(filter.columnId) < 0;
  266. });
  267. if (filters && filters.length) {
  268. widget.localFilters = filters;
  269. }
  270. }
  271. });
  272. }
  273. };
  274. */
  275. var getFragment = function getFragment(layout) {
  276. var fragmentModel = {};
  277. var content = null;
  278. if (_this5._isTypeRegistered(layout.type)) {
  279. content = layout;
  280. } else {
  281. fragmentModel.layout = layout;
  282. // get the widget
  283. if (spec.widgets && spec.widgets.length) {
  284. var widgets = [];
  285. var sourceMap = {};
  286. // handle grouping
  287. if (layout.items) {
  288. findWidgets(layout.items, widgets, sourceMap);
  289. } else {
  290. findWidget(layout.id, widgets, sourceMap);
  291. }
  292. if (widgets.length) {
  293. fragmentModel.widgets = widgets;
  294. fragmentModel.dataSources = {
  295. version: spec.dataSources.version,
  296. sources: _.values(sourceMap)
  297. };
  298. // TODO: instead of stripping out a known attribute, we should ignore anything runtime/transient attribute (eg. runtimeOnly flag) in the model.
  299. // When explicitScale is set to truthy, the VIDA normalization is not set; hence we does not want this property
  300. if (layout.content && layout.content.properties) {
  301. var contentProperties = Object.keys(layout.content.properties);
  302. if (contentProperties.indexOf('explicitScale') !== -1) {
  303. delete layout.content.properties['explicitScale'];
  304. }
  305. }
  306. }
  307. }
  308. if (spec.drillThrough && spec.drillThrough.length) {
  309. fragmentModel.drillThrough = spec.drillThrough;
  310. }
  311. if (spec.properties) {
  312. fragmentModel.properties = spec.properties;
  313. }
  314. if (spec.fredIsRed) {
  315. fragmentModel.fredIsRed = spec.fredIsRed;
  316. }
  317. if (spec.episodes) {
  318. fragmentModel.episodes = spec.episodes;
  319. }
  320. }
  321. /*
  322. Defect 252008. Commenting out this code pending future analysis. For now, we only want to simply copy local filters.
  323. if(spec.pageContext) {
  324. handleFilters();
  325. }
  326. */
  327. return {
  328. content: content,
  329. model: fragmentModel
  330. };
  331. };
  332. // recursively paste widgets into dashboard (if the dashboard is templated, then the widgets are placed in different slots)
  333. var apply = function apply(idx, transactionToken) {
  334. var layout = getLayout(idx);
  335. if (layout) {
  336. var fragment = getFragment(layout);
  337. fragment.copyPaste = true;
  338. // if there is a layout then there is a widget to paste
  339. if (fragment && (fragment.content || fragment.model && fragment.model.layout)) {
  340. return _this5.dashboardApi.getFeature('Canvas').addContent({ spec: fragment.content || fragment.model, copyPaste: true }, transactionToken).then(function (content) {
  341. var layoutModelId = content.getId();
  342. // TODO: layoutReady() is the better intended method for the behavior wanted here, it is not working as
  343. // expected, some investigation is required by Remi
  344. return _this5.layoutController.whenWidgetRenderComplete(layoutModelId).then(function (layout) {
  345. // select the pasted widget
  346. _this5.layoutController.interactionController.selectionHandler.selectNode(layout.domNode, {
  347. isTouch: false
  348. });
  349. });
  350. });
  351. } else {
  352. return Promise.reject(new Error('Unable to complete pasting of widget(s). Can not determine the layout fragment'));
  353. }
  354. } else {
  355. return Promise.reject(new Error('Unable to complete pasting of widget(s). Checking for a layout that is not there'));
  356. }
  357. };
  358. // in order to build a fragment model per layout, traverse the layout to get its widget(s), then get the datasources for the widgets
  359. if (spec) {
  360. if (spec.layout) {
  361. var promises = [];
  362. // create a transaction token in order for paste to be undone/redone with one click
  363. var transaction = this.dashboardApi.getFeature('Transaction');
  364. var transactionToken = transaction.startTransaction();
  365. for (var i = 0; i < spec.layout.length; i++) {
  366. promises.push(apply(i, transactionToken));
  367. }
  368. transaction.endTransaction(transactionToken);
  369. return Promise.all(promises);
  370. } else {
  371. return Promise.resolve();
  372. }
  373. } else {
  374. return Promise.resolve();
  375. }
  376. };
  377. CopyPasteController.prototype._isTypeRegistered = function _isTypeRegistered(type) {
  378. var contentTypeRegistry = this.dashboardApi.getFeature('ContentTypeRegistry');
  379. return contentTypeRegistry.isTypeRegistered(type);
  380. };
  381. // Log error messages and show toast
  382. CopyPasteController.prototype._showError = function _showError(errKey, err) {
  383. this.logger.error(StringResources.get(errKey), err);
  384. return this._showToast(errKey, err, {
  385. type: 'error',
  386. 'preventDuplicates': true
  387. });
  388. };
  389. // Show toast popup
  390. CopyPasteController.prototype._showToast = function _showToast(strServiceKey, strParams, toastParams) {
  391. var toastMsg = !strParams ? StringResources.get(strServiceKey) : StringResources.get(strServiceKey, strParams);
  392. return this.dashboardApi.showToast(toastMsg, toastParams);
  393. };
  394. return CopyPasteController;
  395. }();
  396. });
  397. //# sourceMappingURL=CopyPasteController.js.map