ExportAsStory.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435
  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: Storytelling
  6. * (C) Copyright IBM Corp. 2015, 2020
  7. * US Government Users Restricted Rights - Use, duplication or disclosure restricted by GSA ADP Schedule Contract with IBM Corp.
  8. */
  9. define(['baglass/core-client/js/core-client/errors/BaseError', 'baglass/core-client/js/core-client/utils/ClassFactory', 'underscore', 'gemini/app/util/ErrorUtils', 'gemini/app/ui/dialogs/GenericViewDialog', 'react', 'react-dom'
  10. // PLEASE PLEASE PLEASE - No Storytelling dependencies here.
  11. // This module is loaded in scope of dashboard and should be as light as possible.
  12. // Load storytelling modules on-demand, only when used.
  13. ], function (BaseError, ClassFactory, _, ErrorUtils, Dialog, React, ReactDOM) {
  14. var getContentView = function getContentView(glassContext) {
  15. var contentView = null;
  16. if (glassContext.appController.getCurrentContentView) {
  17. contentView = glassContext.appController.getCurrentContentView();
  18. } else if (glassContext.appController.currentAppView.currentContentView) {
  19. contentView = glassContext.appController.currentAppView.currentContentView;
  20. }
  21. return contentView;
  22. };
  23. var ActionHandler = function () {
  24. function ActionHandler() {
  25. _classCallCheck(this, ActionHandler);
  26. // This value comes from the dashboardToStory.json file in the perspectives directory
  27. this._contributionId = 'com.ibm.bi.storytelling';
  28. }
  29. /**
  30. * Handles whether or not to show the menu item(s) we contribued to the save menu.
  31. * @override
  32. *
  33. * @returns {boolean} Returns true if item should be visible or false if not.
  34. */
  35. ActionHandler.prototype.isItemVisible = function isItemVisible(context) {
  36. if (context.target.itemId === this._contributionId + '.exportAsStory') {
  37. return this._isBoardTypeSupported(context) && ErrorUtils.hasCapability(context.glassContext, 'canAuthorDashboard');
  38. }
  39. return false;
  40. };
  41. /**
  42. * Handles the click of the menu item we contributed to the save menu.
  43. * @override
  44. *
  45. * @returns {Promise}
  46. */
  47. ActionHandler.prototype.onSelectItem = function onSelectItem(context) {
  48. if (this._contributionId + '.exportAsStory' === context.target.itemId) {
  49. var view = getContentView(context.glassContext);
  50. var segmentType = { type: 'Exported Object' };
  51. view.getDashboardApi().getFeature('segment').track('exportedToStory', function () {
  52. return segmentType;
  53. });
  54. return this._convertToStory(context);
  55. }
  56. return Promise.resolve();
  57. };
  58. /**
  59. * @param {Object} service This is the object that is defined when creating a new SaveAsDialog
  60. * @param {string} selection This comes from SaveAsDialog's inner workings
  61. * @param {string} name This comes from SaveAsDialog's inner workings
  62. * @param {boolean} overwrite This comes from SaveAsDialog's inner workings
  63. * @param {boolean} loadOnSave Whether to load after a successful export
  64. *
  65. * @returns {Promise}
  66. */
  67. ActionHandler.prototype.exportAsStory = function exportAsStory(service, selection, name, overwrite, loadOnSave) {
  68. return this._sendRequest({
  69. context: service.context,
  70. url: selection.url,
  71. name: name,
  72. overwrite: overwrite
  73. }).then(this._onExportSuccess.bind(this, service.context, loadOnSave)).catch(this._onExportError.bind(this)).finally(this._exportDialog.hide.bind(this._exportDialog));
  74. };
  75. /**
  76. * Handle the conversion of the board model to a story and displaying the SaveAs dialog
  77. * @private
  78. * @param {Object} context The current context of the app that the user has when the export menu item was clicked.
  79. *
  80. * @returns {Promise}
  81. */
  82. ActionHandler.prototype._convertToStory = function _convertToStory(context) {
  83. /*
  84. * Steps in convert to story
  85. * 1. User clicks "Export as a story"
  86. * 2. User is prompted to select Story type (and template if selecting GJ)
  87. * 3. User is prompted to choose a location to save, name the story, and open automatically (default) or not
  88. * 4a. User is automatically brough to story (if option was chosen)
  89. * 4b. User is shown the success/green toast about the successful conversion
  90. * 4c. User is shown a dialog if trying to use the same name asking if she wants to overwrite the existing object
  91. * 4d. User is shown an error/red toast about any error that is returned during the process
  92. */
  93. return this._openStoryTypeSelectionDialog(context).then(this._openSaveAsDialog.bind(this));
  94. };
  95. /**
  96. * Setup and display a dialog to the user to allow her to select the Story Navigation Model type
  97. * and, in the case of Guided Journey, select the layout template type.
  98. * @private
  99. *
  100. * @returns {Promise}
  101. */
  102. ActionHandler.prototype._openStoryTypeSelectionDialog = function _openStoryTypeSelectionDialog(context) {
  103. var _this = this;
  104. return Promise.all([ClassFactory.loadModule('storytelling/glass/StoryLayoutPickerView'), ClassFactory.loadModule('storytelling/nls/StringResources')]).then(function (modules) {
  105. var StoryLayoutPickerView = modules[0];
  106. var stringResources = modules[1];
  107. return new Promise(function (resolve) {
  108. var dialogElement = document.createElement('div');
  109. dialogElement.classList.add('templatePickerDialog');
  110. var viewOptions = {
  111. action: function () {
  112. this._templateSelectDialog.hide();
  113. this._templateSelectDialog.view.getSelectedLayoutSpec().then(function (_ref) {
  114. var layout = _ref.layout;
  115. // have to do layout.layout because layoutpickerview is made to return layouts
  116. // but we are now using it to return a layout + widgets
  117. resolve({
  118. context: context,
  119. navModel: layout.layout.type
  120. });
  121. });
  122. }.bind(_this)
  123. };
  124. _this._templateSelectDialog = new Dialog({
  125. id: 'selectTemplateTitle',
  126. title: stringResources.get('selectTemplateLabel'),
  127. viewClass: StoryLayoutPickerView,
  128. viewOptions: viewOptions,
  129. okCallback: viewOptions.action,
  130. cancelCallback: function cancelCallback() {
  131. resolve({
  132. cancelled: true
  133. });
  134. },
  135. buttons: [{
  136. text: stringResources.get('selectLabel'),
  137. handler: viewOptions.action,
  138. type: 'primary',
  139. defaultId: 'select_button'
  140. }, 'cancel'],
  141. showCloseX: true,
  142. width: '760px'
  143. });
  144. _this._templateSelectDialog.open();
  145. });
  146. });
  147. };
  148. /**
  149. * @private
  150. *
  151. * @param {Object} options An object of required values
  152. * @param {boolean} [options.cancelled] Set if the user clicks cancel on the export as a story dialog
  153. * @param {Object} options.context The current context of the application
  154. * @param {string} options.navModel The navigation model that was selected in the dialog
  155. */
  156. ActionHandler.prototype._openSaveAsDialog = function _openSaveAsDialog(options) {
  157. var _this2 = this;
  158. if (options.cancelled && options.cancelled === true) {
  159. return Promise.resolve();
  160. }
  161. var context = options.context;
  162. var navModel = options.navModel;
  163. var glassContext = context.glassContext;
  164. var contentView = getContentView(glassContext);
  165. var dashboardApi = contentView.getDashboardApi();
  166. return Promise.all([ClassFactory.loadModule('storytelling/glass/ExportToStoryDialog'), ClassFactory.loadModule('storytelling/nls/StringResources'), ClassFactory.loadModule('storytelling-ui/storytelling-ui.min'), this._createStory(dashboardApi, contentView, navModel), this._getAncestors(context, contentView.getBoardId())]).then(function (_ref2) {
  167. var ExportDialog = _ref2[0],
  168. stringResources = _ref2[1],
  169. StorytellingUI = _ref2[2],
  170. modelObj = _ref2[3],
  171. ancestors = _ref2[4];
  172. var modelJson = modelObj.model;
  173. _this2._exportDialog = new ExportDialog({
  174. glassContext: glassContext,
  175. defaultFileName: stringResources.get('newDefaultStoryName', {
  176. modelName: contentView.boardModel.name
  177. }),
  178. service: {
  179. context: {
  180. glassContext: glassContext,
  181. boardModel: modelJson
  182. },
  183. export: _this2.exportAsStory.bind(_this2)
  184. },
  185. ancestors: ancestors,
  186. onHide: _this2._clearToast.bind(_this2)
  187. });
  188. _this2._exportDialog.open();
  189. if (modelObj.status.hasOutOfBoundsWidget) {
  190. _this2._showToast(StorytellingUI, stringResources.get('outOfBoundsWarningMessage'));
  191. } else if (modelObj.status.hasEmptyExploreCards) {
  192. _this2._showToast(StorytellingUI, stringResources.get('emptyExploreCardsWarningMessage'));
  193. }
  194. });
  195. };
  196. ActionHandler.prototype._createStory = function _createStory(dashboardApi, contentView, navModel) {
  197. var _this3 = this;
  198. return Promise.all([ClassFactory.loadModule('storytelling/StoryService')]).then(function (_ref3) {
  199. var StoryService = _ref3[0];
  200. var storyService = new StoryService({ dashboardApi: dashboardApi });
  201. return storyService.createStory({
  202. boardModel: contentView.boardModel,
  203. layoutController: contentView.boardController.layoutController,
  204. targetInfo: {
  205. type: navModel,
  206. transition: 'none'
  207. },
  208. sourceType: _this3.sourceType
  209. });
  210. });
  211. };
  212. ActionHandler.prototype._showToast = function _showToast(StorytellingUI, message) {
  213. var toast = React.createElement(StorytellingUI.Toast, {
  214. contentString: message,
  215. statusType: 'warning',
  216. className: 'db-st-export-toast'
  217. });
  218. // Parsing this._exportDialog.$el[0] to render is not ideal
  219. // In works in this case because the toast is added to the body regardless
  220. ReactDOM.render(toast, this._exportDialog.$el[0]);
  221. };
  222. ActionHandler.prototype._clearToast = function _clearToast() {
  223. this._exportDialog && ReactDOM.unmountComponentAtNode(this._exportDialog.$el[0]);
  224. };
  225. /**
  226. * @private
  227. */
  228. ActionHandler.prototype._getAncestors = function _getAncestors(context, id) {
  229. var ancestors = function ancestors(id) {
  230. if (id) {
  231. return context.glassContext.getSvc('.Content').then(function (oContentService) {
  232. var server_URL = oContentService.getBaseObjectsURL() + '/' + id;
  233. return oContentService.get(server_URL, { data: { fields: 'ancestors' } });
  234. });
  235. } else {
  236. return Promise.resolve(null);
  237. }
  238. };
  239. return ancestors(id).then(function (result) {
  240. return result ? result.data[0].ancestors || null : null;
  241. });
  242. };
  243. /**
  244. * @private
  245. */
  246. ActionHandler.prototype._onExportSuccess = function _onExportSuccess(context, loadOnSave, response) {
  247. var appController = context.glassContext.appController;
  248. if (loadOnSave) {
  249. appController.openAppView('story', {
  250. perspective: 'story',
  251. content: {
  252. perspective: 'story',
  253. objectUrl: response.jqXHR.getResponseHeader('location'),
  254. isAuthoringMode: appController.getCurrentContentView().isAuthoringMode
  255. }
  256. });
  257. } else {
  258. return ClassFactory.loadModule('storytelling/nls/StringResources').then(function (stringResources) {
  259. var perspective = getContentView(context.glassContext).perspective;
  260. appController.showToast(stringResources.get(perspective + '_export_success'));
  261. });
  262. }
  263. };
  264. /**
  265. * Handle the error that was returned when tyring to save the new story.
  266. * @private
  267. * @param {Object} errorObject An object from the rejected promise around the ajax.post in _exportAsStoryRequest
  268. * @returns {Promise} A rejected promise if it was a duplicate error or resolved promise otherwise
  269. */
  270. ActionHandler.prototype._onExportError = function _onExportError(errorObject) {
  271. var errorCode = ErrorUtils.getErrorCode(errorObject.saveRequest);
  272. if (errorCode === 'cmDuplicateName') {
  273. return Promise.reject(new BaseError('duplicate', {
  274. 'isDuplicate': true
  275. }));
  276. } else {
  277. ErrorUtils.showError(errorObject.glassContext, errorObject.saveRequest, errorObject.cmSpec);
  278. return Promise.resolve();
  279. }
  280. };
  281. /**
  282. * Send an ajax post request to create a new story with the converted board model
  283. * @private
  284. * @see {@link _exportAsStory} for parameter descriptions
  285. * @param {Object} context
  286. * @param {string} url
  287. * @param {string} name
  288. * @param {boolean} overwrite
  289. *
  290. * @returns {Promise} A Promise that will be resolve when the save is complete or rejected if error during save
  291. */
  292. ActionHandler.prototype._sendRequest = function _sendRequest(options) {
  293. var context = options.context;
  294. var url = options.url;
  295. var name = options.name;
  296. var overwrite = options.overwrite;
  297. var ajaxSvc = context.glassContext.getCoreSvc('.Ajax');
  298. var spec = context.boardModel;
  299. if (name) {
  300. spec.name = name;
  301. }
  302. var cmSpec = this._getCMSpec({
  303. spec: spec,
  304. context: context
  305. });
  306. url = url + (overwrite ? '?updateAction=replace' : '');
  307. return ajaxSvc.ajax({
  308. type: 'post',
  309. url: url,
  310. contentType: 'application/json',
  311. processData: false,
  312. dataType: 'text',
  313. data: JSON.stringify(cmSpec)
  314. }).catch(function (errorObject) {
  315. return Promise.reject(new BaseError('saving story error', {
  316. cmSpec: cmSpec,
  317. glassContext: context.glassContext,
  318. saveRequest: errorObject.jqXHR
  319. }));
  320. });
  321. };
  322. /**
  323. * @private
  324. */
  325. ActionHandler.prototype._getCMSpec = function _getCMSpec(options) {
  326. var spec = options.spec;
  327. var context = options.context;
  328. var cmSpec = {
  329. 'defaultName': spec.name,
  330. 'type': this._getAssetType(context),
  331. 'specification': JSON.stringify(spec),
  332. 'deploymentReferences': getContentView(context.glassContext).dashboardApi.getFeature('dataSources.deprecated').getDeploymentReferences(),
  333. 'iconURI': '#common-catalog',
  334. 'defaultScreenTip': 'story',
  335. 'tags': ['story']
  336. };
  337. return cmSpec;
  338. };
  339. /**
  340. * @private
  341. */
  342. ActionHandler.prototype._getAssetType = function _getAssetType(context) {
  343. var assetType = 'exploration'; //default asset type
  344. var content = getContentView(context.glassContext).getContent();
  345. if (content.options && content.options.config) {
  346. var config = content.options.config;
  347. if (config.hasOwnProperty('assetType')) {
  348. assetType = config.assetType;
  349. }
  350. }
  351. return assetType;
  352. };
  353. ActionHandler.prototype._isBoardTypeSupported = function _isBoardTypeSupported(context) {
  354. var view = getContentView(context.glassContext);
  355. this.sourceType = view.getDashboardApi && view.getDashboardApi().getType();
  356. if (this.sourceType === 'dashboard') {
  357. // Only support tab layout and ignore infographic from dashboard
  358. return view.boardModel.layout.type === 'tab';
  359. } else if (this.sourceType === 'explore') {
  360. return true;
  361. }
  362. return false;
  363. };
  364. return ActionHandler;
  365. }();
  366. return ActionHandler;
  367. });
  368. //# sourceMappingURL=ExportAsStory.js.map