VisChangerFlyoutView.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540
  1. 'use strict';
  2. var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
  3. function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
  4. function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; }
  5. function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }
  6. /**
  7. * Licensed Materials - Property of IBM
  8. * IBM Cognos Products: BI Cloud (C) Copyright IBM Corp. 2018, 2020
  9. * US Government Users Restricted Rights - Use, duplication or disclosure restricted by GSA ADP Schedule Contract with IBM Corp.
  10. */
  11. /**
  12. *
  13. * View to swap the Live Widget Visualization through the ODT
  14. * Options
  15. * {
  16. * showTitles: boolean, // Whether to show the titles for the available/recommended visualizations
  17. * visualizations: {
  18. * recommended: [], // Array of visualizations to place in recommended section
  19. * other: [] // Array of visualizations to put in other section
  20. * },
  21. * currentVis: String, // The id of the currently selected visualization
  22. * widget: Live Widget // The widget that is creating the vischanger view
  23. * }
  24. *
  25. **/
  26. define(['../../../lib/@waca/core-client/js/core-client/ui/core/View', '../../../lib/@waca/core-client/js/core-client/utils/Utils', '../nls/StringResources', 'jquery', 'underscore', 'react-dom', 'react', 'ca-ui-toolkit', 'dashboard-analytics/DynamicFileLoader', 'gemini/app/util/ErrorUtils'], function (BaseView, Utils, resources, $, _, ReactDOM, React, Toolkit, DynamicFileLoader, ErrorUtils) {
  27. /* eslint react/prop-types: 0 */ // TODO: Remove once we have prop-types brought in from glass.
  28. var Accordion = Toolkit.Accordion;
  29. var AccordionItem = Toolkit.AccordionItem;
  30. var SVGIcon = Toolkit.SVGIcon;
  31. var Label = Toolkit.Label;
  32. var SVGIconDecoration = Toolkit.SVGIconDecoration;
  33. var ProgressIndicator = Toolkit.ProgressIndicator;
  34. // Limit the number of recommended visualizations to show in the recommended section.
  35. var MaxRecommendedVisualizationsToShow = 6;
  36. var AUTO_ID = 'auto';
  37. /******************************************************
  38. * Recommended Accordion Section
  39. * AccordionItem
  40. * [PropItems] - SVG (with possible deco) and Label
  41. * All visualizations Accordion Section
  42. * AccordionItem
  43. * [PropItems] - SVG (with possible deco) and Label
  44. ********************************************************/
  45. /*
  46. * Class to render the react flyout components
  47. */
  48. var ReactComponents = function (_React$Component) {
  49. _inherits(ReactComponents, _React$Component);
  50. /*
  51. * The props hold a state that should be used as this classes state.
  52. * When the state changes the component gets re-rendered.
  53. */
  54. function ReactComponents(props) {
  55. _classCallCheck(this, ReactComponents);
  56. var _this = _possibleConstructorReturn(this, _React$Component.call(this, props));
  57. _this.state = _extends({}, props.state);
  58. _this.state.checkMarkIcon = props.state.iconsFeature.getIcon('CheckmarkSVG');
  59. return _this;
  60. }
  61. /**
  62. * @param {string} id of the selected visualization
  63. * @return {React Component} the decoration if the specified id matched the selected id.
  64. */
  65. ReactComponents.prototype.getSelectedDecoration = function getSelectedDecoration(id) {
  66. // The decoration is basically a checkmark to show the selected vis.
  67. if (this.state.selectedId === id) {
  68. return React.createElement(SVGIconDecoration, {
  69. iconId: this.state.checkMarkIcon.id,
  70. location: 'bottomRight',
  71. style: { right: '-4px' } });
  72. }
  73. };
  74. /**
  75. * @param {object} item describing all the info needed to create a prop item
  76. * @param {String} dataType either 'Recommended' or 'Other'
  77. * @return {React Object}
  78. */
  79. ReactComponents.prototype.renderPropItem = function renderPropItem(item, dataType) {
  80. var decoration = this.getSelectedDecoration(item.id);
  81. var pressed = this.state.selectedId === item.id ? 'true' : 'false';
  82. return React.createElement(
  83. 'div',
  84. { role: 'group', className: 'prop-item' },
  85. React.createElement(
  86. 'div',
  87. { tabIndex: 0,
  88. 'data-type': dataType,
  89. className: 'prop-icon vis',
  90. role: 'button',
  91. title: item.name,
  92. 'data-id': item.id,
  93. 'appcues-data-id': item.id,
  94. 'aria-label': item.name,
  95. 'aria-pressed': pressed },
  96. React.createElement(
  97. SVGIcon,
  98. {
  99. iconId: this.state.svgsMap[item.id].id,
  100. height: 42,
  101. width: 42 },
  102. decoration
  103. ),
  104. React.createElement(Label, { className: 'prop-icon-label', label: item.name })
  105. )
  106. );
  107. };
  108. /**
  109. * @param {Array} items all the visualizations to add to the row
  110. * @param {Boolean} isRecommended true iff the Vis Row is the recommended row.
  111. * @returns {Array} of React components for each Vis in the Vis Row.
  112. */
  113. ReactComponents.prototype.loadVisRowItems = function loadVisRowItems(items, isRecommended) {
  114. var _this2 = this;
  115. var svgs = [];
  116. var dataType = isRecommended ? 'Recommended' : 'Other';
  117. items.forEach(function (item) {
  118. if (item.id && _this2.state.svgsMap[item.id]) {
  119. svgs.push(_this2.renderPropItem(item, dataType));
  120. }
  121. });
  122. return svgs;
  123. };
  124. /**
  125. * Render the progress indicator to show we are loading.
  126. */
  127. ReactComponents.prototype._renderLoading = function _renderLoading() {
  128. return React.createElement(
  129. 'div',
  130. { className: 'vis-changer-progressIndicator' },
  131. React.createElement(ProgressIndicator, { size: 'large', variant: 'circle' })
  132. );
  133. };
  134. ReactComponents.prototype._getVisRowItems = function _getVisRowItems(options) {
  135. var items = options.items,
  136. isRecommended = options.isRecommended;
  137. var loadedSvgs = this.loadVisRowItems(items, isRecommended);
  138. return React.createElement(
  139. 'div',
  140. { className: 'visChangerRow' },
  141. loadedSvgs
  142. );
  143. };
  144. ReactComponents.prototype._getRecommendedAccordionItem = function _getRecommendedAccordionItem() {
  145. var recommendedOptions = {
  146. items: this.state.recommended.items,
  147. isRecommended: true
  148. };
  149. return this.state.recommended.isLoading ? this._renderLoading() : this._getVisRowItems(recommendedOptions);
  150. };
  151. ReactComponents.prototype._getAllVisAccordionItem = function _getAllVisAccordionItem() {
  152. var allVisOptions = {
  153. items: this.state.all.items,
  154. isRecommended: false
  155. };
  156. return this.state.all.isLoading ? this._renderLoading() : this._getVisRowItems(allVisOptions);
  157. };
  158. /**
  159. * Render the collapsible sections (accordions)
  160. * @return {React Object} representing the Accordion and its sections.
  161. */
  162. ReactComponents.prototype._renderAccordions = function _renderAccordions() {
  163. var recommendedItem = this._getRecommendedAccordionItem();
  164. var isConsumer = this.state.isConsumer;
  165. if (!isConsumer) {
  166. return React.createElement(
  167. Accordion,
  168. null,
  169. React.createElement(
  170. AccordionItem,
  171. { itemName: resources.get('recommended_visualizations'), icon: 'left', open: true },
  172. recommendedItem
  173. ),
  174. React.createElement(
  175. AccordionItem,
  176. { itemName: resources.get('all_visualizations'), icon: 'left', open: false, onChange: this.state.onItemChange },
  177. this._getAllVisAccordionItem()
  178. )
  179. );
  180. } else {
  181. return React.createElement(
  182. Accordion,
  183. null,
  184. React.createElement(
  185. AccordionItem,
  186. { itemName: resources.get('recommended_visualizations'), icon: 'left', open: true },
  187. recommendedItem
  188. )
  189. );
  190. }
  191. };
  192. /**
  193. * @returns {React Object} progress indicator if we are loading, accordions otherwise.
  194. */
  195. ReactComponents.prototype.render = function render() {
  196. return this._renderAccordions();
  197. };
  198. return ReactComponents;
  199. }(React.Component);
  200. /**
  201. * The following view is the actual ODT Vis Changer view. It will handle
  202. * selection of the react components to change vis type, rendering the different
  203. * choices into two collapsible sections as well as showing a loading indicator
  204. * until all the SVGs are loaded.
  205. */
  206. var View = BaseView.extend({
  207. events: {
  208. 'primaryaction .vis': '_selectVis'
  209. },
  210. /**
  211. * Load the SVG files representing all the visualizations
  212. */
  213. _loadSVGFileMap: function _loadSVGFileMap() {
  214. var _this3 = this;
  215. // If we've already loaded the SVG files, return the map
  216. if (this.viewState.svgsMap && _.keys(this.viewState.svgsMap).length > 0) {
  217. return Promise.resolve(this.viewState.svgsMap);
  218. } else {
  219. this.viewState.svgsMap = {};
  220. this._getAllAvailableVisualizations();
  221. if (this.viewState.all && this.viewState.all.items) {
  222. var iconsFeature = this.viewState.iconsFeature;
  223. this.viewState.all.items.forEach(function (item) {
  224. var visIcon = iconsFeature.getIcon(item.visId);
  225. _this3.viewState.svgsMap[item.visId] = visIcon;
  226. });
  227. if (!(Object.keys(this.viewState.svgsMap).indexOf('auto') !== -1)) {
  228. var autoIcon = iconsFeature.getIcon('auto');
  229. this.viewState.svgsMap['auto'] = autoIcon;
  230. }
  231. return Promise.resolve(this.viewState.svgsMap);
  232. }
  233. }
  234. },
  235. init: function init(options) {
  236. // Provide the basic element.
  237. this.el = $('<div class=\'visChangerContainer\'/>');
  238. View.inherited('init', this, arguments);
  239. this.applyAction = options && options.actions && options.actions.apply;
  240. var finalOptions = options && options.state ? options.state : options;
  241. _.extend(this, finalOptions || {});
  242. this.iconsFeature = this.dashboard.getFeature('Icons');
  243. this._initializeViewState();
  244. },
  245. _elementExists: function _elementExists() {
  246. var hasLength = false,
  247. hasKids = false,
  248. el = this.el;
  249. if (el) {
  250. if (el.length) {
  251. hasLength = el.length > 0;
  252. }
  253. if (el.children) {
  254. hasKids = el.children !== 0;
  255. }
  256. }
  257. return el && (hasKids || hasLength);
  258. },
  259. remove: function remove() {
  260. if (this._elementExists()) {
  261. ReactDOM.unmountComponentAtNode(this.el);
  262. }
  263. this.viewState = null;
  264. return View.inherited('remove', this, arguments);
  265. },
  266. setFocus: function setFocus() {
  267. this.$('.vis').first().focus();
  268. },
  269. _processVisualizations: function _processVisualizations(visualizations) {
  270. var _this4 = this;
  271. var markupPayload = [];
  272. _.each(visualizations, function (vis) {
  273. _this4.viewState.visualizationMap[vis.id || vis.getId()] = vis;
  274. markupPayload.push({
  275. id: vis.id || vis.getId(),
  276. name: vis.caption || vis.getLabel(),
  277. visId: vis.id || vis.getId(),
  278. caption: vis.caption || vis.getLabel(),
  279. iconUri: vis.icon || vis.getIcon()
  280. });
  281. });
  282. return markupPayload;
  283. },
  284. /**
  285. * We want to dynamically load all the SVGs (why load them if we never need them)
  286. * This method loads the SVG files into a map, asks the widget for a list
  287. * of recommended and non-recommended visualizations, and adds the 'Auto'
  288. * choice as well.
  289. */
  290. _preload: function _preload() {
  291. // Load the file map
  292. this.viewState.visualizationMap = this.viewState.visualizationMap || {};
  293. return this._loadSVGFileMap();
  294. },
  295. _getAllAvailableVisualizations: function _getAllAvailableVisualizations() {
  296. var allVisualizations = this.dashboard.getFeature('VisDefinitions').getList();
  297. var processedItems = this._processVisualizations(allVisualizations);
  298. this.viewState.all.items = _.sortBy(processedItems, function (vis) {
  299. return vis.name;
  300. });
  301. this.viewState.all.isLoading = false;
  302. },
  303. _getRecommendedVisualizations: function _getRecommendedVisualizations() {
  304. var _this5 = this;
  305. return this.content.getFeature('Visualization.SmartsRecommender').getRecommendedVisualizations().then(function (recommended) {
  306. if (_this5.viewState === null) {
  307. return; //Remove has already been called, stop processing
  308. }
  309. var processedVisualizations = _this5._processVisualizations(recommended);
  310. processedVisualizations.unshift({
  311. id: AUTO_ID,
  312. name: resources.get('automaticTypeCaption'),
  313. visId: AUTO_ID,
  314. caption: resources.get('automaticTypeCaption'),
  315. iconUri: 'visualizations-changeVisualization',
  316. disabled: processedVisualizations.length === 0
  317. });
  318. var maxVisualizationsToShow = MaxRecommendedVisualizationsToShow;
  319. if (_this5.viewState.selectedId) {
  320. // Don't include Auto in the limit
  321. maxVisualizationsToShow++;
  322. }
  323. _this5.viewState.recommended.items = processedVisualizations.slice(0, maxVisualizationsToShow);
  324. _this5.viewState.recommended.isLoading = false;
  325. });
  326. },
  327. _initializeViewState: function _initializeViewState() {
  328. this.viewState = {
  329. recommended: {
  330. items: [],
  331. isLoading: true
  332. },
  333. all: {
  334. items: [],
  335. isLoading: true
  336. },
  337. svgsMap: null,
  338. selectedId: this._getSelectedVizId(),
  339. visualizationMap: null,
  340. iconsFeature: this.iconsFeature,
  341. onItemChange: this._onItemChange.bind(this)
  342. };
  343. },
  344. //called when an item in the accordian is changed
  345. _onItemChange: function _onItemChange() {
  346. //ensure this flyout view is not off the screen
  347. var diff = this._getOffScreenHeight();
  348. if (diff > 0) {
  349. //TODO: This is not a great solution, especially if we change the way popovers are handled.
  350. // in the future, any resizing should be handled by the popover class.
  351. var popover = $(this.el).parents('.popover');
  352. var top = Math.max(0, popover.position().top - diff);
  353. popover && popover.css({ top: top });
  354. }
  355. },
  356. /**
  357. * Checks to see if this View is off the visible screen.
  358. * @returns the number of pixels this view is off the screen, or -1 if it is not off the screen
  359. */
  360. _getOffScreenHeight: function _getOffScreenHeight() {
  361. var htmlScrollHeight = this._getDocumentElement().scrollHeight;
  362. var innerHeight = Utils.getCurrentWindow().innerHeight;
  363. if (htmlScrollHeight > innerHeight) {
  364. return htmlScrollHeight - innerHeight;
  365. }
  366. return -1;
  367. },
  368. /**
  369. * @returns The document.documentElement.
  370. * This is here to more easily mock for tests.
  371. */
  372. _getDocumentElement: function _getDocumentElement() {
  373. return document.documentElement;
  374. },
  375. _getSelectedVizId: function _getSelectedVizId() {
  376. var visualization = this.content.getFeature('Visualization');
  377. if (this.currentVis && this.currentVis === AUTO_ID || visualization && !visualization.isTypeLocked()) {
  378. return AUTO_ID;
  379. } else {
  380. return visualization.getDefinition().getId();
  381. }
  382. },
  383. _resetRecommendedViewState: function _resetRecommendedViewState() {
  384. this.viewState.recommended = {
  385. items: [],
  386. isLoading: true
  387. };
  388. if (this._elementExists() && this.reactComponents) {
  389. ReactDOM.unmountComponentAtNode(this.el);
  390. }
  391. },
  392. /**
  393. * Create the react components with an initial loading state.
  394. */
  395. _showLoading: function _showLoading() {
  396. this._resetRecommendedViewState();
  397. // Render the react components
  398. this.reactComponents = ReactDOM.render(React.createElement(ReactComponents, { state: this.viewState }), this.el);
  399. },
  400. /**
  401. * Render the react components with a state to show the accordions.
  402. */
  403. _showReactSections: function _showReactSections() {
  404. this.reactComponents.setState(JSON.parse(JSON.stringify(this.viewState)));
  405. },
  406. _render: function _render(options) {
  407. var _this6 = this;
  408. _.extend(this, options || {});
  409. this._isConsumer();
  410. this._showLoading();
  411. return this._preload().then(function () {
  412. _this6._showReactSections();
  413. }).then(this._getRecommendedVisualizations.bind(this)).then(function () {
  414. _this6._showReactSections();
  415. });
  416. },
  417. _isConsumer: function _isConsumer() {
  418. var currentContentView = this.dashboard && this.dashboard.getCurrentContentView();
  419. var glassContext = currentContentView && currentContentView.glassContext;
  420. if (this.viewState && glassContext) {
  421. this.viewState.isConsumer = !ErrorUtils.hasCapability(glassContext, 'canAuthorDashboard');
  422. }
  423. },
  424. /**
  425. * Method to render the view. This includes creating the react components to
  426. * initially show a progress indicator, preload all the svg icons, divide the
  427. * choices into sections, then update the react components to show the accordions.
  428. */
  429. render: function render(options) {
  430. this._render(options);
  431. return Promise.resolve();
  432. },
  433. _getTargetFromEvent: function _getTargetFromEvent(event) {
  434. return $(event.currentTarget);
  435. },
  436. // when id auto we highlight auto too
  437. /**
  438. * Event handler for a .vis button is clicked.
  439. * Send event to be handled by Live Widget and updates its view with current selection
  440. */
  441. _selectVis: function _selectVis(event) {
  442. var $currentTarget = this._getTargetFromEvent(event);
  443. var id = $currentTarget.attr('data-id');
  444. var recommendedCategory = $currentTarget.attr('data-type');
  445. // If selected visualization target is same as current visualization target
  446. if (this.viewState.selectedId === id) {
  447. return;
  448. }
  449. this.viewState.selectedId = id;
  450. this.reactComponents.setState({ selectedId: id });
  451. var visDefinition = this.dashboard.getFeature('VisDefinitions').getById(id);
  452. var visType = visDefinition && visDefinition.getType() || id;
  453. this.applyAction(visType, { recommendedCategory: recommendedCategory });
  454. if (this.selectVisCB) {
  455. this.selectVisCB();
  456. }
  457. }
  458. });
  459. return View;
  460. });
  461. //# sourceMappingURL=VisChangerFlyoutView.js.map