StoryService.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454
  1. 'use strict';
  2. /*
  3. * Licensed Materials - Property of IBM
  4. * IBM Cognos Products: Storytelling
  5. * (C) Copyright IBM Corp. 2017, 2020
  6. * US Government Users Restricted Rights - Use, duplication or disclosure restricted by GSA ADP Schedule Contract with IBM Corp.
  7. */
  8. define(['baglass/core-client/js/core-client/ui/core/Class', 'underscore', 'gemini/dashboard/util/PxPercentUtil', './layouts/BlankSceneLayout', './layouts/SceneLayout2', './model/TimelineModel'], function (Class, _, PxPercentUtil, BlankSceneLayout, SceneLayout2, TimelineModel) {
  9. var StoryService = Class.extend({
  10. /**
  11. * @class
  12. */
  13. init: function init(options) {
  14. StoryService.inherited('init', this, arguments);
  15. this.dashboardApi = options.dashboardApi;
  16. this.hasOutOfBoundsWidget = false;
  17. this.hasEmptyExploreCards = false;
  18. },
  19. /**
  20. *
  21. * @param {Object} options
  22. * @param {Object} options.boardModel The board model to convert
  23. * @param {Object} [options.targetInfo]
  24. * @param {string} [options.targetInfo.type='slideshow']
  25. * @param {string} [options.targetInfo.transition=null]
  26. *
  27. * @returns {Promise} The successfully modified version of the boardModel that was passed in.
  28. */
  29. createStory: function createStory(options) {
  30. var _this = this;
  31. // setup
  32. this._checkOptions(options);
  33. this._setTargetInfo(options);
  34. return this._createModelObjectForStory(options.boardModel, options.sourceType).then(function (modelJson) {
  35. modelJson.timeline = modelJson.timeline || {};
  36. _this._updateLayout(modelJson.layout);
  37. // set the episodes from the array of widgets
  38. var widgets = _this._findDescendantsWithType('widget', modelJson.layout.items);
  39. TimelineModel.widgetsToEpisodes(modelJson.timeline, widgets, {});
  40. return {
  41. model: modelJson,
  42. status: {
  43. hasOutOfBoundsWidget: options.targetInfo.type !== 'slideshow' && _this.hasOutOfBoundsWidget,
  44. hasEmptyExploreCards: _this.hasEmptyExploreCards
  45. }
  46. };
  47. });
  48. },
  49. _findDescendantsWithType: function _findDescendantsWithType(type, items) {
  50. var descendants = [];
  51. if (items) {
  52. for (var i = 0; i < items.length; i++) {
  53. if (items[i].type === type) {
  54. descendants.push({
  55. type: 'widget',
  56. id: items[i].id
  57. });
  58. } else if (items[i].items) {
  59. descendants.push.apply(descendants, this._findDescendantsWithType(type, items[i].items));
  60. }
  61. }
  62. }
  63. return descendants;
  64. },
  65. /**
  66. * @param {Object} options
  67. * @param {Object} options.boardModel The board model to convert
  68. * @param {Object} [options.targetInfo]
  69. * @param {string} [options.targetInfo.type='slideshow']
  70. * @param {string} [options.targetInfo.transition=null]
  71. *
  72. * @returns {Promise}
  73. */
  74. updateStory: function updateStory(options) {
  75. // setup
  76. this._checkOptions(options);
  77. this._setTargetInfo(options);
  78. //take a copy of the board model
  79. var boardModelJson = JSON.parse(JSON.stringify(options.boardModel.toJSON()));
  80. this._convertFreeformToTemplate(boardModelJson.layout);
  81. this._updateLayout(boardModelJson.layout);
  82. var contentView = this.dashboardApi.getCurrentContentView();
  83. contentView.clearTransientState();
  84. contentView.getDashboardApi().getFeature('DashboardState').setDirty(true);
  85. var boardId = contentView.getBoardId() || null; //Retain the same board ID
  86. return contentView.reloadFromJSONSpec(boardModelJson, { boardId: boardId });
  87. },
  88. /**
  89. * @private
  90. * @param {Object} model The BoardModel spec to build off of to convert a Dashboard to a Story.
  91. * @param {String} sourceType
  92. * @returns {Promise} Returns a promise that will return a model object that holds Storytelling properties
  93. */
  94. _createModelObjectForStory: function _createModelObjectForStory(model, sourceType) {
  95. var _this2 = this;
  96. var modelJson = model.toJSON();
  97. return this.dashboardApi.getGlassSvc('.ConversionService').then(function (srvc) {
  98. return srvc.convert(sourceType.toUpperCase(), 'STORY', JSON.stringify(modelJson));
  99. }).then(function (spec) {
  100. var specJson = JSON.parse(spec);
  101. if (sourceType === 'explore') {
  102. if (modelJson.layout.items.length !== specJson.layout.length) {
  103. _this2.hasEmptyExploreCards = true;
  104. }
  105. var _convertExploreLayout = _this2._convertExploreLayoutToStory(specJson),
  106. layout = _convertExploreLayout.layout,
  107. widgets = _convertExploreLayout.widgets;
  108. modelJson.layout = layout;
  109. modelJson.widgets = _this2._convertExploreWidgetsToStory(specJson.widgets);
  110. // Reserve title placehodler widget(s) from Scenelayout2 template
  111. Object.assign(modelJson.widgets, widgets);
  112. } else if (sourceType === 'dashboard') {
  113. _this2._convertFreeformToTemplate(modelJson.layout);
  114. }
  115. return modelJson;
  116. }).catch(function (error) {
  117. if (sourceType === 'explore') {
  118. _this2.hasEmptyExploreCards = true;
  119. modelJson.layout = _this2._convertExploreLayoutToStory().layout;
  120. modelJson.widgets = {};
  121. return modelJson;
  122. }
  123. return Promise.reject(error);
  124. });
  125. },
  126. /**
  127. * Convert layout from explore to storytelling layout by using SceneLayout2 template
  128. * @private
  129. * @param {Array} layouts the converted explore spec. If there is no spec, return a layout spec with an empty scene
  130. * @returns return the converted storytelling layout
  131. */
  132. _convertExploreLayoutToStory: function _convertExploreLayoutToStory(spec) {
  133. var _this3 = this;
  134. var storyLayout = {
  135. layout: {
  136. items: [],
  137. layoutPositioning: 'relative',
  138. pageSize: {
  139. width: 1280,
  140. height: 720
  141. },
  142. style: {
  143. height: '100%'
  144. }
  145. },
  146. widgets: {}
  147. };
  148. if (spec && spec.layout) {
  149. return spec.layout.reduce(function (acc, item) {
  150. return _this3._convertCardToSceneLayout(acc, item, spec.widgetToCardMap);
  151. }, storyLayout);
  152. } else {
  153. // empty explore cards
  154. var blankSceneLayout = BlankSceneLayout.get().layout;
  155. storyLayout.layout.items.push(blankSceneLayout);
  156. return storyLayout;
  157. }
  158. },
  159. _convertCardToSceneLayout: function _convertCardToSceneLayout(acc, item, widgetToCardMap) {
  160. var _replaceWidget = this._replaceWidget(item, 'vis1_', SceneLayout2),
  161. layout = _replaceWidget.layout,
  162. widgets = _replaceWidget.widgets;
  163. // Reserve explore card layout id
  164. layout.id = widgetToCardMap[item.id];
  165. acc.layout.items.push(layout);
  166. Object.assign(acc.widgets, widgets);
  167. return acc;
  168. },
  169. /**
  170. * Replace the widget in the template with the giving item
  171. * @param {*} specItem Widget to replace
  172. * @param {*} idPrefix The prefix id of the widget that needs replacing
  173. * @param {*} layoutTemplate Scene layout template
  174. * @returns sceneLayout
  175. */
  176. _replaceWidget: function _replaceWidget(specItem, idPrefix, layoutTemplate) {
  177. var sceneLayout = layoutTemplate.get();
  178. var placehodler = sceneLayout.layout.items[0].items.find(function (item) {
  179. return item.id && item.id.startsWith(idPrefix);
  180. });
  181. delete sceneLayout.widgets[placehodler.id];
  182. // Use the style from layoutTemplate
  183. delete specItem.style;
  184. Object.assign(placehodler, specItem);
  185. return sceneLayout;
  186. },
  187. /**
  188. * Convert explore widgets array to storyteling widget object
  189. * Example:
  190. * array = [ {id: 'widget1' ...}, {id: 'widget2', ...} ]
  191. * returnObj = { widget1: {id: 'widget1' ...}, widget2: {id: 'widget2', ...} }
  192. *
  193. * @param {Array} widgets converted widgets array from explore
  194. * @returns return a storytelling widget object
  195. */
  196. _convertExploreWidgetsToStory: function _convertExploreWidgetsToStory(widgets) {
  197. return widgets.reduce(function (obj, widget) {
  198. obj[widget.id] = widget;
  199. return obj;
  200. }, {});
  201. },
  202. _checkOptions: function _checkOptions() {
  203. var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
  204. if (!options.targetInfo) {
  205. options.targetInfo = {};
  206. }
  207. if (!options.boardModel) {
  208. throw new Error('No board model found to convert');
  209. }
  210. },
  211. _setTargetInfo: function _setTargetInfo(options) {
  212. this._targetInfo = {
  213. type: this._validateType(options.targetInfo.type) || 'slideshow'
  214. };
  215. // only slideshow type has a transition
  216. if (options.targetInfo.type === 'slideshow') {
  217. this._targetInfo.transition = this._validateTransition(options.targetInfo.transition) || 'none';
  218. }
  219. },
  220. /**
  221. * @private
  222. * @param {string} type The type of a Board Model being dealt with; dashboard, slideshow, guidedjourney, etc.
  223. *
  224. * @returns {string | null} If type is valid then it is returned otherwise null is returned
  225. */
  226. _validateType: function _validateType(type) {
  227. // Add other valid types to the list of cases that are acceptable
  228. switch (type) {
  229. case 'dashboard':
  230. case 'slideshow':
  231. case 'panAndZoom1':
  232. case 'panAndZoom2':
  233. case 'panAndZoom3':
  234. case 'panAndZoom4':
  235. case 'panAndZoom5':
  236. case 'panAndZoom6':
  237. return type;
  238. default:
  239. return null;
  240. }
  241. },
  242. /**
  243. * @private
  244. * @param {string} transition The transition type the story uses to move between scenes
  245. *
  246. * @returns {string | null} If the transition is valid then it is returned otherwise null is returned
  247. */
  248. _validateTransition: function _validateTransition(transition) {
  249. // Add other valid transitions to the list as more become acceptable
  250. switch (transition) {
  251. case 'none':
  252. case 'scaleAndSlide':
  253. case 'animatedPath':
  254. return transition;
  255. default:
  256. return null;
  257. }
  258. },
  259. /**
  260. * @private
  261. * @param {Object} layoutJson A JSON object
  262. */
  263. _updateLayout: function _updateLayout(layoutJson) {
  264. layoutJson.type = this._targetInfo.type;
  265. switch (this._targetInfo.type) {
  266. case 'dashboard':
  267. break;
  268. case 'slideshow':
  269. this._updateLayoutForSlideShow(layoutJson);
  270. break;
  271. case 'panAndZoom1':
  272. case 'panAndZoom2':
  273. case 'panAndZoom3':
  274. case 'panAndZoom4':
  275. case 'panAndZoom5':
  276. case 'panAndZoom6':
  277. this._updateLayoutForPanAndZoom(layoutJson);
  278. break;
  279. default:
  280. break;
  281. }
  282. this._fitWidgetsToScenes(layoutJson);
  283. return layoutJson;
  284. },
  285. _updateLayoutForSlideShow: function _updateLayoutForSlideShow(layoutJson) {
  286. // Step 1: remove GJ-specific properties
  287. layoutJson.items.forEach(function (item) {
  288. delete item.data;
  289. delete item.style;
  290. });
  291. delete layoutJson.hasOverview;
  292. delete layoutJson.showOverviews;
  293. // Step 2: set up slideshow-specific properties
  294. // transition defaults to 'none' when converting to slideshow
  295. layoutJson.transition = this._targetInfo.transition;
  296. },
  297. _updateLayoutForPanAndZoom: function _updateLayoutForPanAndZoom(layoutJson) {
  298. // Step 1: remove transition, if it exists (ie. SS -> GJ)
  299. delete layoutJson.transition;
  300. // add overviews if the previous story type did not support them
  301. if (!layoutJson.hasOverview) {
  302. layoutJson.showOverviews = {
  303. showStart: true,
  304. showEnd: true
  305. };
  306. layoutJson.hasOverview = true;
  307. }
  308. layoutJson.items.forEach(function (item, index) {
  309. item.data = {
  310. positionIndex: index
  311. };
  312. });
  313. },
  314. _isFreeform: function _isFreeform(layout) {
  315. return layout.type === 'container' && layout.items && layout.items[0].type === 'genericPage' && layout.items[0].layoutPositioning === 'absolute';
  316. },
  317. _convertFreeformToTemplate: function _convertFreeformToTemplate(topLayout) {
  318. var _this4 = this;
  319. /* Converting models with the layout:
  320. * - tab
  321. * - container
  322. * - layout positioning: absolute
  323. * - [widgets]
  324. *
  325. * to:
  326. * - tab
  327. * - container
  328. * - layout positioning: relative
  329. * - [widgets]
  330. */
  331. topLayout.items.filter(this._isFreeform.bind(this)).forEach(function (layout) {
  332. return _this4._convertToRelativeLayout(topLayout.pageSize, layout.items[0]);
  333. });
  334. },
  335. _convertToRelativeLayout: function _convertToRelativeLayout(referencePageSize, layout) {
  336. // Change layout to relative
  337. layout.layoutPositioning = 'relative';
  338. // loop over items in absolute layout (effectively widgets)
  339. layout.items.forEach(function (layoutItem) {
  340. // convert widget style represented in px to percent
  341. PxPercentUtil.changePixelPropertiesToPercent(layoutItem.style, referencePageSize);
  342. });
  343. },
  344. // we have no 'live' model to work with
  345. // so we work with the raw JSON
  346. _fitWidgetsToScenes: function _fitWidgetsToScenes(layout) {
  347. var _this5 = this;
  348. (layout.items || []).forEach(function (scene) {
  349. _this5._fitWidgetsToScene(scene);
  350. });
  351. },
  352. _fitWidgetsToScene: function _fitWidgetsToScene(childLayout) {
  353. var _this6 = this;
  354. // get all widgets/groups in scene/tab (basically anything with a style)
  355. var getStyledLayouts = function getStyledLayouts(layout) {
  356. if (layout.style) {
  357. return [layout];
  358. }
  359. return (layout.items || []).reduce(function (acc, child) {
  360. acc.push.apply(acc, getStyledLayouts(child));
  361. return acc;
  362. }, []);
  363. };
  364. var widgets = getStyledLayouts(childLayout);
  365. if (!widgets.length) {
  366. return;
  367. }
  368. // look for any widgets that are sticking out of the scene.
  369. // the initial value is 100%, resulting in no scaling if no widgets are more than that.
  370. var maxX = widgets.reduce(function (acc, widget) {
  371. var newX = (parseFloat(widget.style.left, 10) || 0) + (parseFloat(widget.style.width, 10) || 0);
  372. if (newX > 100) {
  373. _this6.hasOutOfBoundsWidget = true;
  374. }
  375. return Math.max(acc, newX);
  376. }, 100);
  377. var maxY = widgets.reduce(function (acc, widget) {
  378. var newY = (parseFloat(widget.style.top, 10) || 0) + (parseFloat(widget.style.height, 10) || 0);
  379. if (newY > 100) {
  380. _this6.hasOutOfBoundsWidget = true;
  381. }
  382. return Math.max(acc, newY);
  383. }, 100);
  384. // maintain aspect ratio
  385. var scale = Math.min(100 / maxX, 100 / maxY);
  386. if (scale < 1) {
  387. widgets.forEach(function (widget) {
  388. ['top', 'left', 'width', 'height'].forEach(function (key) {
  389. if (widget.style[key] !== undefined) {
  390. widget.style[key] = (parseFloat(widget.style[key], 10) || 0) * scale + '%';
  391. }
  392. });
  393. });
  394. }
  395. }
  396. });
  397. return StoryService;
  398. });
  399. //# sourceMappingURL=StoryService.js.map