BoardModel.js 42 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284
  1. 'use strict';
  2. /**
  3. * Licensed Materials - Property of IBM
  4. * IBM Cognos Products: BI Cloud (C) Copyright IBM Corp. 2014, 2021
  5. * US Government Users Restricted Rights - Use, duplication or disclosure restricted by GSA ADP Schedule Contract with IBM Corp.
  6. */
  7. 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) {
  8. function createLayoutModel_default(spec, env) {
  9. return new LayoutModel(spec, env.boardModel, env.logger);
  10. }
  11. var Model = BaseModel.extend({
  12. nestedCollections: {
  13. eventGroups: EventGroupCollection
  14. },
  15. nestedModels: {
  16. properties: PropertiesModel
  17. },
  18. init: function init(boardSpec, options) {
  19. var _this = this;
  20. this._initBoardSpec = boardSpec;
  21. options = options || {};
  22. this.logger = options.logger;
  23. // TODO: remove depenendency on dashboardApi here. currently needed to be passed to the "pageContext" board model extension
  24. this.dashboardApi = options.dashboardApi;
  25. this.createLayoutModel = options.createLayoutModel || createLayoutModel_default;
  26. // Make sure the extensions are processed before we call the parents init
  27. if (options.whitelistAttrs) {
  28. this.whitelistAttrs = options.whitelistAttrs;
  29. } else {
  30. this.whitelistAttrs = ['name', 'layout', 'theme', 'version', 'eventGroups', 'datasetShaping', 'properties', 'content'];
  31. }
  32. var excludedProperties = ['dashboardColorSet', 'customColors', 'localCache', 'defaultLocale', 'fredIsRed'];
  33. this.content = ModelUtils.initializeContentModel(boardSpec, excludedProperties);
  34. options.boardModel = this;
  35. if (!boardSpec.properties) {
  36. boardSpec.properties = {};
  37. }
  38. // Need to set these since the boardModel is passed as the options when building the layout model.
  39. // Weird, but don't want to change the flow of layout model creation in R release
  40. this.defaultLocale = boardSpec.properties.defaultLocale;
  41. if (this.dashboardApi) {
  42. var userProfileService = this.dashboardApi.getGlassCoreSvc('.UserProfile');
  43. if (userProfileService) {
  44. this.contentLocale = userProfileService.preferences.contentLocale;
  45. }
  46. }
  47. _.extend(options, this.getLanguageModelOptions());
  48. this._updateNestedInfoWithExtensions(options.boardModelExtensions);
  49. Model.inherited('init', this, arguments);
  50. this.eventRouter = options.eventRouter;
  51. this._autoCreateExtensionModelsAndCollections(options.boardModelExtensions, boardSpec);
  52. this.id = options.id;
  53. this.name = options.name || this.name;
  54. this.widgetRegistry = options.widgetRegistry;
  55. this.layoutExtensions = options.layoutExtensions;
  56. this.widgetInstances = {};
  57. // TODO: Add reference to cleanup item here when it is created in Jira.
  58. // CADBC-832 is item that caused it to change. Eventually this block
  59. // of code will go away.
  60. Object.keys(boardSpec.widgets || {}).forEach(function (widgetId) {
  61. _this.createLegacyWidgetModel(boardSpec.widgets[widgetId]);
  62. });
  63. // Explore expects the BoardModel to populate the widgetInstances
  64. // within this init method but because of changes to how widgets are
  65. // instantiated they are now created from the LayoutModel though still
  66. // from a method in this class (see createLegacyWidgetModel below)
  67. // This doesn't work in Explore's case so the following continues
  68. // to allow Explore to function properly
  69. if (this.createLayoutModel !== createLayoutModel_default) {
  70. this._instantiateWidgetModels(boardSpec.layout.items);
  71. }
  72. this.layout = this.createLayoutModel(boardSpec.layout, {
  73. boardModel: this,
  74. logger: this.logger,
  75. dashboardApi: this.dashboardApi
  76. });
  77. if (!this.eventGroups) {
  78. this.set({
  79. eventGroups: new EventGroupCollection()
  80. }, {
  81. silent: true
  82. });
  83. }
  84. // cache the extensions, the boardmodel editor needs this to re-create the model after a manual edit.
  85. this.boardModelExtensions = options.boardModelExtensions;
  86. // The first layout item will be displayed by default. Keep the selected layout property in sync.
  87. if (this.layout.items && this.layout.items.length > 0) {
  88. var item = this.layout.items[0];
  89. this.setSelectedLayout(item.id);
  90. }
  91. // Keep the selected layout in sync.
  92. this.eventRouter && this.eventRouter.on('tab:tabChanged', this.onTabChanged, this);
  93. this.isUpgraded = options.isUpgraded;
  94. this.type = 'dashboard';
  95. },
  96. _instantiateWidgetModels: function _instantiateWidgetModels() {
  97. var items = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : [];
  98. for (var i = 0; i < items.length; i++) {
  99. var item = items[i];
  100. if (item.items && item.items.length) {
  101. this._instantiateWidgetModels(item.items);
  102. } else {
  103. if (item.type === 'widget' && item.features && item.features['Models_internal']) {
  104. this.createLegacyWidgetModel(item.features['Models_internal']);
  105. }
  106. }
  107. }
  108. },
  109. /**
  110. * Returns the ContentModel
  111. * @return {Object}
  112. */
  113. getContentModel: function getContentModel() {
  114. return this.content;
  115. },
  116. /**
  117. * In Endor R7 (11.1.7) onward we now allow for features to load
  118. * their own data from the board spec. Through this mechanism this method
  119. * is called by the LayoutModel for specific content model objects that
  120. * have a 'Models.internal' feature on them.
  121. * 'Models.internal' is the place that widget data resides for now
  122. * instead of a top-level "widgets" property in the BoardModel here.
  123. * The BoardModel will still hold all widget instances though.
  124. * @param {Object} widgetSpec An object that represents a widget model
  125. */
  126. createLegacyWidgetModel: function createLegacyWidgetModel(widgetSpec) {
  127. if (!this.getWidgetModel(widgetSpec.id)) {
  128. this._addWidgetModel(widgetSpec, {
  129. addDashboardTranslatedLocales: false
  130. });
  131. }
  132. },
  133. /**
  134. * Adds the board model extension info to the the nestedCollections and nestedModels
  135. */
  136. _updateNestedInfoWithExtensions: function _updateNestedInfoWithExtensions(boardModelExtensions) {
  137. var _this2 = this;
  138. if (!boardModelExtensions) {
  139. return;
  140. }
  141. boardModelExtensions.forEach(function (extensionInfo) {
  142. var extensionName = extensionInfo.name;
  143. _this2.whitelistAttrs.push(extensionName);
  144. if (extensionInfo.type === 'collection') {
  145. _this2.nestedCollections[extensionName] = extensionInfo.class;
  146. } else {
  147. _this2.nestedModels[extensionName] = extensionInfo.class;
  148. }
  149. });
  150. },
  151. /**
  152. * Handles adding the model extension points to the whitelistAttrs array and as nestedModels
  153. * @param {Object} boardModelExtensions
  154. * @param {Object} boardSpec
  155. */
  156. _autoCreateExtensionModelsAndCollections: function _autoCreateExtensionModelsAndCollections(boardModelExtensions, boardSpec) {
  157. var _this3 = this;
  158. if (!boardModelExtensions) {
  159. return;
  160. }
  161. boardModelExtensions.forEach(function (extensionInfo) {
  162. var extensionName = extensionInfo.name;
  163. /**
  164. * If it's already in the boardSpec do nothing since the base Model class should of already made sure we were
  165. * dealing with actual Models and Collections. If it's missing and autoCreate is set to true then create the
  166. * Model or Collection
  167. */
  168. if (!boardSpec[extensionName] && extensionInfo.autoCreate) {
  169. if (extensionInfo.type === 'collection') {
  170. _this3._setCollection(extensionName, boardSpec[extensionName] || null, {
  171. silent: true,
  172. logger: _this3.logger,
  173. boardModel: _this3,
  174. dashboardApi: _this3.dashboardApi // FIXME - models should not rely on dashboardApi -- currently used by the pageContext
  175. });
  176. } else {
  177. _this3._setNestedModel(extensionName, boardSpec[extensionName] || {}, {
  178. silent: true,
  179. logger: _this3.logger,
  180. boardModel: _this3
  181. });
  182. }
  183. }
  184. });
  185. this._registerBoardModelExtensionTriggers(boardModelExtensions);
  186. },
  187. setDefaultLocale: function setDefaultLocale(locale) {
  188. this.defaultLocale = locale;
  189. },
  190. setTranslationLocale: function setTranslationLocale(locale) {
  191. this.properties.set({
  192. translationModeLocale: locale
  193. });
  194. this.translationLocale = locale;
  195. },
  196. _registerBoardModelExtensionTriggers: function _registerBoardModelExtensionTriggers(boardModelExtensions) {
  197. if (boardModelExtensions) {
  198. boardModelExtensions.forEach(function (extension) {
  199. if (extension.triggers && extension.triggers.length > 0) {
  200. extension.triggers.forEach(function (trigger) {
  201. this[extension.name].on(trigger.eventName, this.trigger.bind(this, trigger.event));
  202. }, this);
  203. }
  204. }, this);
  205. }
  206. },
  207. onTabChanged: function onTabChanged(event) {
  208. this.setSelectedLayout(event.modelId);
  209. },
  210. /**
  211. * @deprecated, use findWidgetById
  212. */
  213. getWidgetModel: function getWidgetModel(id) {
  214. return this.widgetInstances ? this.widgetInstances[id] : null;
  215. },
  216. getTimeLineEpisode: function getTimeLineEpisode(id) {
  217. return this.timeline ? this.timeline.episodes.get(id) : null;
  218. },
  219. setSelectedLayout: function setSelectedLayout(id) {
  220. this.selectedLayout = id;
  221. },
  222. getSelectedLayout: function getSelectedLayout() {
  223. return this.selectedLayout;
  224. },
  225. /**
  226. * Add a content to the model
  227. *
  228. * @param options
  229. * @param options.model - layout model to add
  230. * @param options.parentId - id of the parent layout
  231. * @param [options.insertBefore] - id of the layout to insert before
  232. * @param options.layoutProperties
  233. * @param sender - If available, the sender will be added to the event payload.
  234. * @param payloadData - Additional data that will be included in the event payload.
  235. * @throws throws an exception if any of the mandatory options are not provided
  236. */
  237. addContent: function addContent(options, sender, payloadData) {
  238. if (!options.parentId) {
  239. throw new Error('Invalid argument options.parentId provided');
  240. }
  241. if (!options.model) {
  242. throw new Error('Invalid argument options.model provided');
  243. }
  244. payloadData = this.checkPayloadData(payloadData);
  245. var model = Object.assign({}, options.model);
  246. // if no id provided on the model, generate a unique id
  247. // TODO: verify that drag and drop works as expected;
  248. // possibly will need to always refresh the id or check if model does not already exist
  249. if (!model.id || !options.modelIdsValid) {
  250. model.id = UniqueId.get('content');
  251. }
  252. if (options.layoutProperties && options.layoutProperties.style) {
  253. model.style = Object.assign({}, model.style || {}, options.layoutProperties.style);
  254. }
  255. this.layout.addArray([{
  256. parentId: options.parentId,
  257. insertBefore: options.insertBefore,
  258. model: model
  259. }], sender, payloadData);
  260. return model.id;
  261. },
  262. /**
  263. * Remove a content from the model.
  264. *
  265. * @param id
  266. * @param sender
  267. * @param payloadData - Additional data that will be included in the event payload.
  268. */
  269. removeContent: function removeContent(id, sender, payloadData) {
  270. payloadData = this.checkPayloadData(payloadData);
  271. this.layout.removeArray([id], sender, payloadData);
  272. },
  273. /**
  274. * Add a widget to the model.
  275. *
  276. * @param options
  277. * options.model
  278. * options.parentId
  279. * options.insertBefore
  280. * options.layoutProperties
  281. * @param sender - If available, the sender will be added to the event payload.
  282. * @param payloadData - Additional data that will be included in the event payload.
  283. */
  284. addWidget: function addWidget(options, sender, payloadData) {
  285. //If no parent id was passed in, cancel the add
  286. if (!options.parentId) {
  287. return;
  288. }
  289. // Create a default transaction id if none is provided so that it is easier to bundler subsequent operation with this one
  290. payloadData = this.checkPayloadData(payloadData);
  291. var model = this._addWidgetModel(options.model, { id: options.id });
  292. if (options.id) {
  293. //this is when a widget was already loaded (but not added to canvas)
  294. //adding to canvas here will use the same id as in the widget for the widget and layout model
  295. //adding will not result in reloading of the widget, but will just add the required models (and layout view obj, which will contain the
  296. //pre-loaded widget)
  297. model.id = options.id;
  298. }
  299. var id = model.id;
  300. // Add to the layout
  301. var layoutModel = _.extend({}, options.layoutProperties, {
  302. type: 'widget',
  303. id: id
  304. });
  305. this.layout.addArray([{
  306. parentId: options.parentId,
  307. insertBefore: options.insertBefore,
  308. model: layoutModel
  309. }], sender, payloadData);
  310. return this._triggerAddRemove('addWidget', {
  311. op: 'addWidget',
  312. parameter: _.extend({}, options, {
  313. model: model.toJSON(),
  314. layoutProperties: $.extend(true, {}, layoutModel)
  315. })
  316. }, {
  317. op: 'removeWidget',
  318. parameter: id
  319. }, sender, payloadData);
  320. },
  321. /**
  322. *
  323. * Add a fragment to the model.
  324. *
  325. * passed model can contain a parent layout or array of layouts
  326. *
  327. * @param options
  328. * options.model which contains
  329. * {
  330. * layout: {
  331. * //one parent layout or parent layout with nested layouts
  332. * //one or widgets can be one or more of these layouts
  333. * id: xxxx
  334. * type: yyy
  335. * items: [{
  336. * id: x1
  337. * type: x2
  338. * },
  339. * ...
  340. * ],
  341. * style: {
  342. * height: aaa
  343. * width: bbb
  344. * top: ccc
  345. * left: ddd
  346. * }
  347. * },
  348. * episodes : [
  349. * {
  350. * "id": "x1",
  351. * "type": "widget",
  352. * "acts": [
  353. * {
  354. * "id": "act1",
  355. * "timer": 0,
  356. * "action": "show"
  357. * },
  358. * {
  359. * "id": "act2",
  360. * "timer": 5000,
  361. * "action": "hide"
  362. * }
  363. * ]
  364. * }
  365. * widgets : [
  366. * {
  367. * id: xxxx
  368. * type: yyyy
  369. * widget model data
  370. * },
  371. * ...
  372. * ],
  373. * datasetShapings : [
  374. * {
  375. * id: xxxx,
  376. * calculations: []
  377. * },
  378. * ...
  379. * ]
  380. * }
  381. * options.parentId
  382. * options.insertBefore
  383. *
  384. * If layout is an array, it should like:
  385. * layout: [{
  386. * id: xxxx
  387. * type: yyy
  388. * items: [{
  389. * id: x1
  390. * type: x2
  391. * },
  392. * ...
  393. * ],
  394. * style: {
  395. * height: aaa
  396. * width: bbb
  397. * top: ccc
  398. * left: ddd
  399. * }
  400. * }, {
  401. * id: xxxx
  402. * type: yyy
  403. * items: [{
  404. * id: x1
  405. * type: x2
  406. * },
  407. * ...
  408. * ]
  409. * }]
  410. * @param sender - If available, the sender will be added to the event payload.
  411. * @param payloadData - Additional data that will be included in the event payload.
  412. */
  413. addFragment: function addFragment(options, sender, payloadData) {
  414. var _this4 = this;
  415. // TODO this logic should be moved out of the model and into a BoardModelManager class
  416. // Create a default transaction id if none is provided so that it is easier to bundler subsequent operation with this one
  417. payloadData = this.checkPayloadData(payloadData);
  418. //make a copy
  419. var fragSpec = _.extend({}, options.model);
  420. var datasources = this.dashboardApi.getFeature('dataSources.deprecated');
  421. var sourceCollection = datasources ? datasources.getSourcesCollection() : undefined;
  422. var sourceIdMap = sourceCollection ? sourceCollection.addSourcesForPin(fragSpec, { payloadData: payloadData }) : {};
  423. //add all widgets in fragment spec
  424. var widgetIdMap = {};
  425. _.each(fragSpec.widgets, function (widgetModel) {
  426. // replace widget ids in fragment with actual ones if the options tell us to
  427. // in some cases the id is already unique and valid. Mainly in the redo after undo case
  428. var oldId = widgetModel.id;
  429. if (!options.modelIdsValid) {
  430. widgetModel.id = undefined;
  431. }
  432. // Update the modelRef to the new sourceId if necessary
  433. var dataViews = widgetModel.data ? widgetModel.data.dataViews : null;
  434. if (dataViews) {
  435. dataViews.forEach(function (dataView) {
  436. if (sourceIdMap[dataView.modelRef]) {
  437. dataView.modelRef = sourceIdMap[dataView.modelRef];
  438. }
  439. });
  440. }
  441. var model = _this4._addWidgetModel(widgetModel);
  442. //keep a note of them, so that we can update layout spec
  443. widgetIdMap[oldId] = model.id;
  444. //update id in widgetModel with the new id from the model
  445. //That solves the redo (adding pin back) not working problem
  446. widgetModel.id = model.id;
  447. });
  448. //update the position information in layout if layoutProperies is defined
  449. //layoutProperties is defined when pin is created by clicking on create button
  450. if (options.layoutProperties && !_.isArray(fragSpec.layout)) {
  451. fragSpec.layout.style = fragSpec.layout.style || {};
  452. fragSpec.layout.style = _.extend(fragSpec.layout.style, options.layoutProperties.style);
  453. }
  454. //update widget ids in fragment spec to reflect new widget ids if we have not done it already
  455. if (!options.modelIdsValid) {
  456. fragSpec.layout = this._updateFragmentSpecLayout(fragSpec.layout, widgetIdMap, options);
  457. }
  458. //update drill through definitions
  459. var drillThroughService = void 0;
  460. try {
  461. drillThroughService = this.dashboardApi.getDashboardCoreSvc('DrillThroughService');
  462. } catch (error) {
  463. this.logger.info(error);
  464. }
  465. if (drillThroughService) {
  466. drillThroughService.addDrillThroughOnAddFragment(options.model, sourceIdMap, widgetIdMap, fragSpec);
  467. }
  468. // Add any custom colors
  469. if (fragSpec.properties && fragSpec.properties.customColors && fragSpec.properties.customColors.colors) {
  470. this.properties.customColors.addCustomColor(fragSpec.properties.customColors.colors);
  471. }
  472. if (fragSpec.fredIsRed && _.isEmpty(this.fredIsRed.colorMap)) {
  473. this.fredIsRed.colorMap = fragSpec.fredIsRed.colorMap;
  474. }
  475. //create layout using layout spec
  476. var layoutPayload = this.layout.addArray(this._getAddArrayPayload(fragSpec, options), sender, payloadData);
  477. //undo relies on just removing parent layout of the fragment
  478. var modelArray = layoutPayload.prevValue.parameter;
  479. // update pageContext
  480. _.each(fragSpec.pageContext, function (pageContextItem) {
  481. // TODO: For now, only origin=filter and tupleSet or conditions are handled
  482. if (pageContextItem.origin === 'filter') {
  483. var _options = void 0;
  484. if (pageContextItem.tupleSet) {
  485. _options = {
  486. values: _.values(JSON.parse(pageContextItem.tupleSet))
  487. };
  488. // TODO: Why is pagecontext conditions an array with'from' and 'to' arrays as well? I tried to see if it is possible
  489. // TODO: to have more than one value in the conditions property from the UI, I couldn't find a way. The code in
  490. // TODO: LiveWidget:PageContextRangeConditions.js expects values, it doesn't seem to handle arrays. This should be
  491. // TODO: looked at to be only scalar values, not arrays and handled in the spec schema accordingly so it validates
  492. } else if (pageContextItem.conditions) {
  493. _options = {
  494. 'condition': {
  495. attributeUniqueNames: pageContextItem.conditions[0].attributeUniqueNames,
  496. from: pageContextItem.conditions[0].from[0],
  497. to: pageContextItem.conditions[0].to[0]
  498. }
  499. };
  500. } else {
  501. _this4.logger.error('Error: AddFragment() - Only able to handle pageContext tupleSet and conditions, can not handle ' + JSON.stringify(pageContextItem));
  502. }
  503. _options = _.extend(_options, {
  504. scope: pageContextItem.scope,
  505. openViewOnLoad: false,
  506. exclude: pageContextItem.exclude,
  507. payloadData: payloadData
  508. });
  509. _this4.dashboardApi.getCanvasWhenReady().then(function (canvas) {
  510. canvas.filterApi.addFilter({
  511. sourceId: pageContextItem.sourceId,
  512. itemId: pageContextItem.hierarchyUniqueNames[0]
  513. }, _options);
  514. }).catch(function (error) {
  515. _this4.logger.error(error);
  516. });
  517. } else {
  518. _this4.logger.error('Error: AddFragment() - Only able to handle pagecontext.origin=filter, not ' + pageContextItem.origin);
  519. }
  520. });
  521. //trigger event
  522. return this._triggerAddRemove('addFragment', {
  523. op: 'addFragment',
  524. parameter: _.extend({}, options, {
  525. model: fragSpec,
  526. modelIdsValid: true,
  527. widgetIdMap: widgetIdMap
  528. })
  529. }, {
  530. op: 'removeFragment',
  531. parameter: modelArray
  532. }, sender, payloadData);
  533. },
  534. _getAddArrayPayload: function _getAddArrayPayload(fragSpec, options) {
  535. var aResult = [];
  536. if (_.isArray(fragSpec.layout)) {
  537. _.each(fragSpec.layout, function (entry) {
  538. aResult.push({
  539. parentId: options.parentId,
  540. insertBefore: options.insertBefore,
  541. model: entry
  542. });
  543. });
  544. return aResult;
  545. }
  546. return [{
  547. parentId: options.parentId,
  548. insertBefore: options.insertBefore,
  549. model: fragSpec.layout
  550. }];
  551. },
  552. _updateFragmentSpecLayout: function _updateFragmentSpecLayout(layout, widgetIdMap) {
  553. var boardModel = this;
  554. var update = function update(layout, widgetIdMap) {
  555. if (_.isArray(layout)) {
  556. _.each(layout, function (item) {
  557. update(item, widgetIdMap);
  558. });
  559. } else {
  560. if (layout.type !== 'widget') {
  561. // create a empty model to get an new ID.
  562. var layoutModel = boardModel.createLayoutModel({
  563. items: []
  564. }, {
  565. boardModel: boardModel,
  566. logger: boardModel.logger
  567. });
  568. layoutModel.off();
  569. widgetIdMap[layout.id] = layoutModel.id;
  570. layout.id = layoutModel.id;
  571. } else {
  572. layout.id = widgetIdMap[layout.id];
  573. }
  574. _.each(layout.items, function (item) {
  575. update(item, widgetIdMap);
  576. });
  577. }
  578. return layout;
  579. };
  580. if (layout) {
  581. return update(layout, widgetIdMap);
  582. } else {
  583. var addLayout = function addLayout(id) {
  584. return {
  585. id: id,
  586. type: 'widget'
  587. };
  588. };
  589. //this just adds widgets to a group layout - but does nothing about location of widgets or style applied
  590. //not sure if this is the best strategy
  591. //add a group layout if spec does not contain a layout
  592. return {
  593. type: 'group',
  594. items: _.map(_.values(widgetIdMap), addLayout)
  595. };
  596. }
  597. },
  598. _getObjectFromArrayById: function _getObjectFromArrayById(array) {
  599. var object = _.object(_.map(array, function (item) {
  600. return [item.id, item];
  601. }));
  602. return object;
  603. },
  604. /**
  605. *
  606. * removes a fragment from the model
  607. *
  608. * @param modelIds array of layout identifiers (typically will be one container id)
  609. *
  610. * @param sender - If available, the sender will be added to the event payload.
  611. * @param payloadData - Additional data that will be included in the event payload.
  612. */
  613. removeFragment: function removeFragment(modelIds, sender, payloadData) {
  614. return this.removeLayouts(modelIds, sender, payloadData);
  615. },
  616. /**
  617. * Remove a widget from the model.
  618. *
  619. * @param id
  620. * @param sender
  621. * @param payloadData - Additional data that will be included in the event payload.
  622. */
  623. removeWidget: function removeWidget(id, sender, payloadData) {
  624. // Create a default transaction id if none is provided so that it is easier to bundler subsequent operation with this one
  625. payloadData = this.checkPayloadData(payloadData);
  626. this.trigger('pre:removeWidget', {
  627. id: id,
  628. sender: sender,
  629. data: _.extend({ runtimeOnly: true }, payloadData)
  630. });
  631. var payload = this.layout.removeArray([id], sender, payloadData);
  632. if (payload) {
  633. var widgetModel = this.widgetInstances[id];
  634. var evtData = this._triggerAddRemove('removeWidget', {
  635. op: 'removeWidget',
  636. parameter: id
  637. }, {
  638. op: 'addWidget',
  639. parameter: {
  640. parentId: payload.prevValue.parameter[0].parentId,
  641. model: widgetModel.toJSON(),
  642. layoutProperties: _.extend({}, payload.prevValue.parameter[0].model)
  643. }
  644. }, sender, payloadData);
  645. this.widgetInstances[id].off('change', this.onWidgetModelChange, this);
  646. delete this.widgetInstances[id];
  647. return evtData;
  648. }
  649. },
  650. getUsedCustomColors: function getUsedCustomColors(customColors) {
  651. var usedColors = Model.inherited('getUsedCustomColors', this, arguments);
  652. if (this.widgetInstances) {
  653. for (var widgetModel in this.widgetInstances) {
  654. usedColors = usedColors.concat(this.widgetInstances[widgetModel].getUsedCustomColors(customColors));
  655. }
  656. }
  657. if (this.layout) {
  658. usedColors = usedColors.concat(this.layout.getUsedCustomColors(customColors));
  659. }
  660. return _.uniq(usedColors, false);
  661. },
  662. /**
  663. * Remove a list of layouts from the board model. This method will also remove all the widgets contained in the layouts being removed
  664. *
  665. * @param id - could be a string with one id or an array if IDs
  666. * @param sender
  667. * @param payloadData - Additional data that will be included in the event payload.
  668. */
  669. removeLayouts: function removeLayouts(id, sender, payloadData) {
  670. // Create a default transaction id if none is provided so that it is easier to bundler subsequent operation with this one
  671. payloadData = this.checkPayloadData(payloadData);
  672. var idArray = _.isArray(id) ? id : [id];
  673. this.trigger('pre:removeLayouts', {
  674. idArray: idArray,
  675. sender: sender,
  676. data: _.extend({ runtimeOnly: true }, payloadData)
  677. });
  678. var widgetIds = this.layout.listWidgets(idArray);
  679. var payload = this.layout.removeArray(idArray, sender, payloadData);
  680. if (payload) {
  681. var removedWidgets = {};
  682. var widgetId;
  683. for (var i = 0; i < widgetIds.length; i++) {
  684. widgetId = widgetIds[i];
  685. var widgetModel = this.widgetInstances[widgetId];
  686. if (widgetModel) {
  687. removedWidgets[widgetId] = widgetModel.toJSON();
  688. widgetModel.off();
  689. delete this.widgetInstances[widgetId];
  690. }
  691. }
  692. return this._triggerAddRemove('removeLayouts', {
  693. op: 'removeLayouts',
  694. parameter: idArray,
  695. removedWidgets: removedWidgets
  696. }, {
  697. op: 'addLayouts',
  698. parameter: {
  699. widgetSpecMap: removedWidgets,
  700. addLayoutArray: payload.prevValue.parameter
  701. }
  702. }, sender, payloadData);
  703. }
  704. },
  705. /**
  706. * Add layouts to the board. Layouts can contain widgets
  707. *
  708. * 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
  709. * options.addLayoutArray = [{
  710. * parentId - layout parent id where to add this layout
  711. * model - layout model json
  712. * }]
  713. *
  714. * @param options - widgetSpecMap, addLayoutArray
  715. * @param sender
  716. * @param payloadData - Additional data that will be included in the event payload.
  717. */
  718. addLayouts: function addLayouts(options, sender, payloadData) {
  719. // Create a default transaction id if none is provided so that it is easier to bundler subsequent operation with this one
  720. payloadData = this.checkPayloadData(payloadData);
  721. // Add widgets
  722. if (options.widgetSpecMap) {
  723. for (var id in options.widgetSpecMap) {
  724. if (options.widgetSpecMap.hasOwnProperty(id)) {
  725. this._addWidgetModel(options.widgetSpecMap[id], { id: id });
  726. }
  727. }
  728. }
  729. var layout = options.parentId ? this.layout.findModel(options.parentId) : this.layout;
  730. if (!layout) {
  731. layout = this.layout;
  732. }
  733. // Add layouts
  734. var payload = layout.addArray(options.addLayoutArray, sender, payloadData);
  735. return this._triggerAddRemove('addLayouts', {
  736. op: 'addLayouts',
  737. parameter: _.extend({}, options, {
  738. widgetSpecMap: options.widgetSpecMap,
  739. addLayoutArray: payload.value.parameter
  740. })
  741. }, {
  742. op: 'removeLayouts',
  743. parameter: payload.prevValue.parameter
  744. }, sender, payloadData);
  745. },
  746. _isTypeRegistered: function _isTypeRegistered(type) {
  747. var contentTypeRegistry = this.dashboardApi.getFeature('ContentTypeRegistry');
  748. return contentTypeRegistry.isTypeRegistered(type);
  749. },
  750. duplicateLayout: function duplicateLayout(layoutId, sender, payload) {
  751. var _this5 = this;
  752. payload = this.checkPayloadData(payload);
  753. var layoutModel = this.layout.findModel(layoutId);
  754. var idMap = {};
  755. var clone = function clone() {
  756. if (layoutModel) {
  757. if (_this5._isTypeRegistered(layoutModel.type)) {
  758. var containerId = layoutModel.getParent().id;
  759. var content = _this5.dashboardApi.getFeature('Canvas').getContent(layoutId);
  760. return _this5.dashboardApi.getFeature('Canvas').addContent({
  761. containerId: containerId,
  762. spec: content.getFeature('Serializer').toJSON(),
  763. copyPaste: true
  764. }).then(function (newContent) {
  765. var newId = newContent.getId();
  766. idMap[layoutId] = newId;
  767. return newId;
  768. });
  769. } else {
  770. var widgetSpecMap = _this5._cloneWidgets(layoutModel, idMap);
  771. var _clone = _this5._cloneLayout(layoutModel, idMap, payload);
  772. var options = {
  773. parentId: layoutModel.getParent().id,
  774. widgetSpecMap: widgetSpecMap,
  775. addLayoutArray: [{
  776. model: _clone.toJSON(),
  777. insertBefore: layoutModel.type === 'widget' ? null : layoutModel.getNextSiblingId()
  778. }]
  779. };
  780. _this5.addLayouts(options, sender, payload);
  781. return Promise.resolve(_clone.id);
  782. }
  783. }
  784. return Promise.resolve();
  785. };
  786. return clone().then(function (cloneId) {
  787. // all work in being done in other methods that take care of the undo/redo stack management
  788. // however we still want to notify interested classes that a duplicate occurred.
  789. // simplest way is to manually trigger an event and not include a senders context
  790. // this should bypass the undoduplicateLayout/redo stack since this method did not do anything that needs to be undone.
  791. // we still send the payload data in case someone else needs to do some undoable work in this transaction.
  792. _this5.trigger('duplicateLayout', {
  793. layoutId: layoutId,
  794. cloneId: cloneId,
  795. idMap: idMap,
  796. sender: sender,
  797. data: payload
  798. });
  799. return cloneId;
  800. });
  801. },
  802. duplicateSelection: function duplicateSelection(options, sender, payload) {
  803. var ids = [];
  804. var payloadData = this.checkPayloadData(payload);
  805. payloadData.limitToBounds = [];
  806. _.each(options.modelIds, function (id) {
  807. if (options.outBoundIds && options.outBoundIds.filter(function (e) {
  808. return e.id === id;
  809. }).length > 0) {
  810. payloadData.limitToBounds.push(options.outBoundIds.filter(function (e) {
  811. return e.id === id;
  812. }));
  813. }
  814. ids.push(this.duplicateLayout(id, sender, payloadData));
  815. }.bind(this));
  816. return ids;
  817. },
  818. updateLayoutType: function updateLayoutType(options, sender, payloadData) {
  819. payloadData = this.checkPayloadData(payloadData);
  820. var layout = options.id ? this.layout.findModel(options.id) : this.layout;
  821. if (layout && options.type) {
  822. layout.set({
  823. type: options.type
  824. }, {
  825. sender: sender,
  826. payloadData: payloadData
  827. });
  828. }
  829. },
  830. /**
  831. * Update a layout's drop zones only if the layout has at least one drop zone.
  832. * All existing widgets will go untouched as this is purely a layout drop zone change
  833. *
  834. * @param options.id {string} The id of the layout that has the template/drop zones to be removed/added to
  835. * @param options.model {object} The user selected layout model that contains the drop zones
  836. * @param sender {object} use if a previous action has taken place and we want to combine this action with it
  837. * @param payload {object} the payload of a previous action that has taken place
  838. */
  839. updateLayoutDropZones: function updateLayoutDropZones(options, sender, payload) {
  840. /*
  841. * This little bit of code looks odd but the LayoutModel.findModel() method can potentially return null.
  842. * For this reason we need to do the check that the layout variable is a falsey value and if
  843. * so then we use the boardModel's layout instance
  844. */
  845. var layout;
  846. if (options.id) {
  847. layout = this.layout.findModel(options.id);
  848. }
  849. if (!layout) {
  850. layout = this.layout;
  851. }
  852. // Try to get a list of drop zones in the selected layout
  853. var dropZones = layout.findDropZones();
  854. // If no drop zones then there is nothing else to do
  855. if (!dropZones) {
  856. return payload;
  857. }
  858. // Remove the drop zones while saving the payload because it is needed when adding the new drop zones
  859. payload = this.removeLayouts(dropZones.ids, sender, payload);
  860. /*
  861. * Create the array list of drop zone layouts to add
  862. * Set the 'insertBefore' property to be the first widget as all widgets need to come after drop zone layouts.
  863. * If there are no widgets then firstWidget returns undefined which is fine
  864. */
  865. var firstWidget = layout.listWidgets([layout.id])[0];
  866. var addArray = [];
  867. for (var i = 0; i < options.model.items[0].items.length; i++) {
  868. addArray.push({
  869. parentId: dropZones.parentId,
  870. model: options.model.items[0].items[i],
  871. insertBefore: firstWidget
  872. });
  873. }
  874. payload = this.addLayouts({
  875. parentId: dropZones.parentId,
  876. addLayoutArray: addArray
  877. }, payload.sender, payload.data);
  878. return payload;
  879. },
  880. _cloneWidgets: function _cloneWidgets(layoutModel, idMap) {
  881. var widgetSpecMap = {};
  882. var widgets = layoutModel.listWidgets([layoutModel.id]);
  883. _.each(widgets, function (widgetId) {
  884. var widgetModel = this.widgetInstances[widgetId];
  885. var clonedWidget = widgetModel.cloneWidget(idMap);
  886. widgetSpecMap[clonedWidget.id] = clonedWidget.toJSON();
  887. }.bind(this));
  888. return widgetSpecMap;
  889. },
  890. _cloneLayout: function _cloneLayout(layoutModel, idMap, payload) {
  891. var clone = layoutModel.cloneLayout(idMap);
  892. if ((clone.type === 'widget' || clone.type === 'group') && clone.style && payload.limitToBounds && payload.limitToBounds.length <= 0) {
  893. // offset layout by 5% / 25px down/right for the cloned
  894. clone.style.top = clone.incrementStyleValue(clone.style.top);
  895. clone.style.left = clone.incrementStyleValue(clone.style.left);
  896. } else if (payload.limitToBounds && payload.limitToBounds.length > 0) {
  897. payload.limitToBounds[0].forEach(function (element) {
  898. if (element.bottom) {
  899. clone.style.top = clone.incrementStyleValue(clone.style.top);
  900. }
  901. if (element.right) {
  902. clone.style.left = clone.incrementStyleValue(clone.style.left);
  903. }
  904. if (!element.right && !element.bottom) {
  905. if (parseFloat(clone.decrementStyleValue(clone.style.top)) > 0) {
  906. clone.style.top = clone.decrementStyleValue(clone.style.top);
  907. }
  908. if (parseFloat(clone.decrementStyleValue(clone.style.left)) > 0) {
  909. clone.style.left = clone.decrementStyleValue(clone.style.left);
  910. }
  911. }
  912. });
  913. }
  914. return clone;
  915. },
  916. getLanguageModelOptions: function getLanguageModelOptions() {
  917. var addDashboardTranslatedLocales = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false;
  918. var languageOptions = {
  919. defaultLocale: this.defaultLocale,
  920. contentLocale: this.contentLocale,
  921. translationLocale: this.translationLocale
  922. };
  923. // Add in all the locales the dashboard is currently translated in. This is needed so that new MultilingualAttributes
  924. // default to the correct locale
  925. if (addDashboardTranslatedLocales) {
  926. var translationService = this.dashboardApi.getDashboardCoreSvc('TranslationService');
  927. languageOptions.availableDashboardLocales = translationService.getSelectedLanguages();
  928. }
  929. return languageOptions;
  930. },
  931. _addWidgetModel: function _addWidgetModel(widgetSpec) {
  932. var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
  933. if (!widgetSpec.name) {
  934. widgetSpec.name = '';
  935. }
  936. //The widget registry supports plugin model classes for times when custom models need to be defined.
  937. //Live Widget (for example) is defined in an external component and has a custom model class.
  938. var WidgetModelClass = WidgetModel;
  939. if (widgetSpec && this.widgetRegistry && this.widgetRegistry[widgetSpec.type] && this.widgetRegistry[widgetSpec.type].ModelClass) {
  940. WidgetModelClass = this.widgetRegistry[widgetSpec.type].ModelClass;
  941. }
  942. var id = options.id,
  943. _options$addDashboard = options.addDashboardTranslatedLocales,
  944. addDashboardTranslatedLocales = _options$addDashboard === undefined ? true : _options$addDashboard;
  945. var languageModelOptions = this.getLanguageModelOptions(addDashboardTranslatedLocales);
  946. var model = new WidgetModelClass(widgetSpec, languageModelOptions);
  947. model.on('change', this.onWidgetModelChange, this);
  948. this.widgetInstances[id || model.id] = model;
  949. return model;
  950. },
  951. /**
  952. * Called when a layout change happens that we want to propagate. (e.g. layout move, resize, etc..)
  953. * The layout model takes care of creating the event payload with the undo/redo handler.
  954. *
  955. * @param payload
  956. * @param sender
  957. */
  958. onLayoutChange: function onLayoutChange(payload, sender) {
  959. this.trigger('change:layout', payload, sender);
  960. },
  961. /**
  962. * Helper function used to trigger the add/remove event
  963. *
  964. * @param eventName
  965. * @param value
  966. * @param prevValue
  967. * @param sender
  968. * @param payloadData - Additional data that will be included in the event payload.
  969. */
  970. _triggerAddRemove: function _triggerAddRemove(eventName, value, prevValue, sender, payloadData) {
  971. var payload = {
  972. value: value,
  973. prevValue: prevValue,
  974. sender: sender,
  975. senderContext: {
  976. applyFn: this.applyFn.bind(this)
  977. },
  978. data: payloadData
  979. };
  980. this.trigger(eventName, payload);
  981. return payload;
  982. },
  983. /**
  984. * Function used to handler the undo/redo for the board model updates
  985. * @param value
  986. * @param sender
  987. */
  988. applyFn: function applyFn(value, sender, name, payload) {
  989. if (value.op && typeof this[value.op] === 'function') {
  990. var args = [value.parameter];
  991. args.push(sender);
  992. args.push(payload);
  993. this[value.op].apply(this, args);
  994. } else {
  995. Model.inherited('applyFn', this, arguments);
  996. }
  997. },
  998. /**
  999. * Handles changes from widget models (which can be added/removed as widgets are added/removed) and notifies
  1000. * listener of the change (so listener does not need to track addition/deletion of widget models
  1001. *
  1002. * @param payload
  1003. */
  1004. onWidgetModelChange: function onWidgetModelChange(payload) {
  1005. var senderContext = payload.senderContext || {};
  1006. var modelId = _.isObject(payload.sender) ? payload.sender.id : payload.sender;
  1007. //TODO: Need to revist this when porting to Endor, why are we overwriting the applyFn?
  1008. //https://github.ibm.com/BusinessAnalytics/dashboard-core/pull/1311
  1009. if (payload && payload.senderContext && this.widgetInstances[modelId]) {
  1010. payload.senderContext.applyFn = function () {
  1011. var model = this.widgetInstances[modelId];
  1012. if (model) {
  1013. model.applyFn.apply(model, arguments);
  1014. }
  1015. }.bind(this);
  1016. }
  1017. this.trigger('widget:change', _.extend({
  1018. modelId: payload.model ? payload.model.id : modelId,
  1019. senderContext: senderContext
  1020. }, payload));
  1021. },
  1022. toJSON: function toJSON() {
  1023. var spec = Model.inherited('toJSON', this, [null, ['layout']]);
  1024. var canvas = this.dashboardApi.getFeature('Canvas');
  1025. var topLevelContent = canvas.findContent({ type: this.layout.type })[0];
  1026. spec.layout = topLevelContent.getFeature('Serializer').toJSON();
  1027. return spec;
  1028. },
  1029. /**
  1030. * Check the payload data
  1031. * Create a default transaction id if none is provided so that it is easier to bundler subsequent operation with this one
  1032. * @param {Object} payloadData
  1033. */
  1034. // TODO: we're actually not "checking" anything in this method, should this be renamed?
  1035. checkPayloadData: function checkPayloadData(payloadData) {
  1036. if (payloadData) {
  1037. return payloadData;
  1038. }
  1039. return {
  1040. undoRedoTransactionId: _.uniqueId('boardModelTransaction')
  1041. };
  1042. },
  1043. /**
  1044. * Find widget model by id and type.
  1045. *
  1046. * @param {string} id - widget id
  1047. *
  1048. * @return {WidgetModel} widget model instance found
  1049. */
  1050. findWidgetById: function findWidgetById(id) {
  1051. return this.widgetInstances ? this.widgetInstances[id] : null;
  1052. },
  1053. /**
  1054. * Find widget model matching one of the ids
  1055. *
  1056. * @param {string[]} ids - widget ids to match
  1057. *
  1058. * @return {WidgetModel} widget model instance found
  1059. */
  1060. findWidgetByIds: function findWidgetByIds(ids) {
  1061. var finding;
  1062. ids.some(function (id) {
  1063. var breakLoop = false;
  1064. var widget = this.findWidgetById(id);
  1065. if (widget) {
  1066. finding = widget;
  1067. breakLoop = true;
  1068. }
  1069. return breakLoop;
  1070. }.bind(this));
  1071. return finding;
  1072. },
  1073. findWidgetByCriteriaFn: function findWidgetByCriteriaFn(criteriaFn) {
  1074. var finding;
  1075. for (var key in this.widgetInstances) {
  1076. if (this.widgetInstances.hasOwnProperty(key)) {
  1077. var widgetInstance = this.widgetInstances[key];
  1078. if (criteriaFn(widgetInstance)) {
  1079. finding = widgetInstance;
  1080. break;
  1081. }
  1082. }
  1083. }
  1084. return finding;
  1085. },
  1086. filterWidgetsByCriteriaFn: function filterWidgetsByCriteriaFn(criteriaFn) {
  1087. var findings = [];
  1088. for (var key in this.widgetInstances) {
  1089. if (this.widgetInstances.hasOwnProperty(key)) {
  1090. var widgetInstance = this.widgetInstances[key];
  1091. if (criteriaFn(widgetInstance)) {
  1092. findings.push(widgetInstance);
  1093. }
  1094. }
  1095. }
  1096. return findings;
  1097. },
  1098. /**
  1099. * Override the base method since we need to deal with our widgetModels and layoutModel which are
  1100. * not real nested models
  1101. */
  1102. getContentReferences: function getContentReferences() {
  1103. var deploymentRefs = Model.inherited('getContentReferences', this, arguments);
  1104. if (this.widgetInstances) {
  1105. for (var widgetModel in this.widgetInstances) {
  1106. deploymentRefs = deploymentRefs.concat(this.widgetInstances[widgetModel].getContentReferences());
  1107. }
  1108. }
  1109. if (this.layout) {
  1110. deploymentRefs = deploymentRefs.concat(this.layout.getContentReferences());
  1111. }
  1112. return _.uniq(deploymentRefs, false, function (ref) {
  1113. return ref.value;
  1114. });
  1115. },
  1116. /**
  1117. * Should only be used by dashboard Serializer feature
  1118. * @return {Object} the inital board spec
  1119. */
  1120. getInitialSpec: function getInitialSpec() {
  1121. return this._initBoardSpec;
  1122. },
  1123. /**
  1124. * Delete the initial board spec, it is invoked only once when canvas is ready
  1125. */
  1126. deleteInitialSpec: function deleteInitialSpec() {
  1127. delete this._initBoardSpec;
  1128. }
  1129. });
  1130. return Model;
  1131. });
  1132. //# sourceMappingURL=BoardModel.js.map