@eeacms/volto-tableau 6.0.0 → 6.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -29,7 +29,7 @@ describe('View', () => {
29
29
  tableau_vis_url: 'http://localhost:3000/tableau-ct',
30
30
  with_download: true,
31
31
  with_more_info: true,
32
- with_note: true,
32
+ with_notes: true,
33
33
  with_share: true,
34
34
  };
35
35
 
@@ -39,6 +39,6 @@ describe('View', () => {
39
39
  <View data={data} />
40
40
  </Provider>,
41
41
  );
42
- expect(container.querySelector('.embed-container')).toBeInTheDocument();
42
+ expect(container.querySelector('.embed-tableau')).toBeInTheDocument();
43
43
  });
44
44
  });
@@ -47,14 +47,17 @@ export default () => {
47
47
  {
48
48
  id: 'default',
49
49
  title: 'Default',
50
+ fields: ['tableau_vis_url', 'tableau_height'],
51
+ },
52
+ {
53
+ id: 'toolbar',
54
+ title: 'Toolbar',
50
55
  fields: [
51
- 'tableau_vis_url',
52
- 'with_note',
56
+ 'with_notes',
53
57
  'with_sources',
54
58
  'with_more_info',
55
59
  'with_download',
56
60
  'with_share',
57
- 'tableau_height',
58
61
  ],
59
62
  },
60
63
  {
@@ -68,13 +71,14 @@ export default () => {
68
71
  title: 'Tableau visualization',
69
72
  widget: 'url',
70
73
  },
71
- with_note: {
74
+ with_notes: {
72
75
  title: 'Show note',
73
76
  type: 'boolean',
74
77
  defaultValue: true,
75
78
  },
76
79
  with_sources: {
77
80
  title: 'Show sources',
81
+ description: 'Will show sources set in this page Data provenance',
78
82
  type: 'boolean',
79
83
  defaultValue: true,
80
84
  },
@@ -14,17 +14,21 @@ import isUndefined from 'lodash/isUndefined';
14
14
  import cx from 'classnames';
15
15
  import { Button } from 'semantic-ui-react';
16
16
  import { Toast, Icon } from '@plone/volto/components';
17
+ import {
18
+ FigureNote,
19
+ Sources,
20
+ MoreInfo,
21
+ Share,
22
+ } from '@eeacms/volto-embed/Toolbar';
17
23
  import { useTableau } from '@eeacms/volto-tableau/hooks';
18
- import JsonCodeSnippet from '@eeacms/volto-tableau/Utils/JsonCodeSnippet/JsonCodeSnippet';
19
- import FigureNote from '@eeacms/volto-tableau/Utils/FigureNote/FigureNote';
20
- import Sources from '@eeacms/volto-tableau/Utils/Sources/Sources';
21
- import MoreInfoLink from '@eeacms/volto-tableau/Utils/MoreInfoLink/MoreInfoLink';
22
- import Download from '@eeacms/volto-tableau/Utils/Download/Download';
23
- import Share from '@eeacms/volto-tableau/Utils/Share/Share';
24
+ import { JsonCodeSnippet, Download } from '@eeacms/volto-tableau/Utils';
25
+
24
26
  import { getSheetnames, getActiveSheetname, getDevice } from './helpers';
25
27
 
26
28
  import resetSVG from '@plone/volto/icons/reset.svg';
27
29
 
30
+ import '@eeacms/volto-embed/Toolbar/styles.less';
31
+
28
32
  const TableauDebug = ({ mode, data, vizState, url, version, clearData }) => {
29
33
  const { loaded, error } = vizState;
30
34
  const { filters = {}, parameters = {} } = data;
@@ -66,6 +70,7 @@ const TableauDebug = ({ mode, data, vizState, url, version, clearData }) => {
66
70
  };
67
71
 
68
72
  const Tableau = forwardRef((props, ref) => {
73
+ const tableauEl = useRef(null);
69
74
  const vizEl = useRef(null);
70
75
  const viz = useRef();
71
76
  const vizState = useRef({});
@@ -74,6 +79,7 @@ const Tableau = forwardRef((props, ref) => {
74
79
  const [loaded, setLoaded] = useState(false);
75
80
  const [loading, setLoading] = useState(false);
76
81
  const [error, setError] = useState(null);
82
+ const [mobile, setMobile] = useState(false);
77
83
  const {
78
84
  block,
79
85
  data = {},
@@ -96,12 +102,12 @@ const Tableau = forwardRef((props, ref) => {
96
102
  toolbarPosition = 'Top',
97
103
  breakpointUrls = [],
98
104
  tableau_vis_url,
99
- with_note,
100
- with_sources,
101
- with_more_info,
102
- with_download,
103
- with_share,
104
- tableau_height = '700',
105
+ with_notes = true,
106
+ with_sources = true,
107
+ with_more_info = true,
108
+ with_download = true,
109
+ with_share = true,
110
+ tableau_height,
105
111
  } = data;
106
112
  const device = useMemo(
107
113
  () => getDevice(breakpoints, screen.page?.width || Infinity),
@@ -327,6 +333,7 @@ const Tableau = forwardRef((props, ref) => {
327
333
  }, [data]);
328
334
 
329
335
  useEffect(() => {
336
+ // console.log('HERE trigger initiateViz', tableau, url);
330
337
  if (!tableau) return;
331
338
  if (url) {
332
339
  disposeViz();
@@ -366,6 +373,18 @@ const Tableau = forwardRef((props, ref) => {
366
373
  /* eslint-disable-next-line */
367
374
  }, [sheetname]);
368
375
 
376
+ useEffect(() => {
377
+ if (!loading && tableauEl.current) {
378
+ const visWidth = tableauEl.current.offsetWidth;
379
+
380
+ if (visWidth < 600 && !mobile) {
381
+ setMobile(true);
382
+ } else if (visWidth >= 600 && mobile) {
383
+ setMobile(false);
384
+ }
385
+ }
386
+ }, [screen, mobile, loading]);
387
+
369
388
  useImperativeHandle(
370
389
  ref,
371
390
  () => {
@@ -381,7 +400,7 @@ const Tableau = forwardRef((props, ref) => {
381
400
  );
382
401
 
383
402
  return (
384
- <div className="tableau-wrapper">
403
+ <div className="tableau-wrapper" ref={tableauEl}>
385
404
  {loading && (
386
405
  <div className="tableau-loader">
387
406
  <span>Loading...</span>
@@ -412,25 +431,27 @@ const Tableau = forwardRef((props, ref) => {
412
431
  clearData={clearData}
413
432
  />
414
433
  <div
415
- style={{ height: tableau_height + 'px' }}
434
+ style={{ height: tableau_height ? tableau_height + 'px' : '100%' }}
416
435
  className={cx('tableau', `tableau-${version}`, {
417
436
  'tableau-autoscale': autoScale,
418
437
  })}
419
438
  ref={vizEl}
420
439
  />
421
- <div className="visualization-info-container">
422
- <div className="visualization-info">
423
- {with_note && loaded && <FigureNote note={figure_note || []} />}
424
- {with_sources && loaded && <Sources sources={sources} />}
425
- {with_more_info && loaded && (
426
- <MoreInfoLink contentTypeLink={tableau_vis_url} />
427
- )}
440
+ {loaded && (
441
+ <div className={cx('visualization-toolbar', { mobile })}>
442
+ <div className="left-col">
443
+ {with_notes && <FigureNote note={figure_note || []} />}
444
+ {with_sources && <Sources sources={sources} />}
445
+ {with_more_info && <MoreInfo href={tableau_vis_url || data.url} />}
446
+ </div>
447
+ <div className="right-col">
448
+ {with_download && loaded && <Download viz={viz.current} />}
449
+ {with_share && loaded && (
450
+ <Share href={tableau_vis_url || data.url} />
451
+ )}
452
+ </div>
428
453
  </div>
429
- <div className="visualization-info">
430
- {with_download && loaded && <Download viz={viz.current} />}
431
- {with_share && loaded && <Share contentTypeLink={tableau_vis_url} />}
432
- </div>
433
- </div>
454
+ )}
434
455
  </div>
435
456
  );
436
457
  });
@@ -3,37 +3,37 @@ import { Popup } from 'semantic-ui-react';
3
3
  import cx from 'classnames';
4
4
 
5
5
  const Download = ({ viz }) => {
6
- const [expanded, setExpanded] = React.useState(false);
6
+ const [open, setOpen] = React.useState(false);
7
7
  const popupRef = React.useRef();
8
8
 
9
9
  return (
10
10
  <Popup
11
- popper={{ id: 'tableau-download-popup' }}
12
- trigger={
13
- <div className="tableau-download-container">
14
- <button className={cx('tableau-download-button', { expanded })}>
15
- Download <i class="ri-download-fill"></i>
16
- </button>
17
- </div>
18
- }
11
+ popper={{ id: 'vis-toolbar-popup', className: 'download-popup' }}
19
12
  position="bottom left"
20
13
  on="click"
14
+ open={open}
21
15
  onClose={() => {
22
- setExpanded(false);
16
+ setOpen(false);
23
17
  }}
24
18
  onOpen={() => {
25
- setExpanded(true);
19
+ setOpen(true);
26
20
  }}
27
21
  ref={popupRef}
28
- >
29
- <ul className="no-bullets">
30
- <li>
31
- Data formats
32
- <div className="visualization-wrapper">
33
- <div className="visualization-info">
34
- <div>
22
+ trigger={
23
+ <div className="tableau-download-container">
24
+ <button className={cx('trigger-button', { open })}>
25
+ <i className="ri-download-fill" />
26
+ Download
27
+ </button>
28
+ </div>
29
+ }
30
+ content={
31
+ <>
32
+ <div className="item">
33
+ <span className="label">Data formats</span>
34
+ <div className="types">
35
+ <div className="type">
35
36
  <button
36
- className="tableau-download-button tableau-format-download"
37
37
  onClick={() => {
38
38
  viz.showExportCrossTabDialog();
39
39
  popupRef.current.triggerRef.current.click();
@@ -42,9 +42,8 @@ const Download = ({ viz }) => {
42
42
  <span>CSV</span>
43
43
  </button>
44
44
  </div>
45
- <div>
45
+ <div className="type">
46
46
  <button
47
- className="tableau-download-button tableau-format-download"
48
47
  onClick={() => {
49
48
  viz.exportCrossTabToExcel();
50
49
  popupRef.current.triggerRef.current.click();
@@ -55,14 +54,11 @@ const Download = ({ viz }) => {
55
54
  </div>
56
55
  </div>
57
56
  </div>
58
- </li>
59
- <li>
60
- Image formats
61
- <div className="visualization-wrapper">
62
- <div className="visualization-info">
63
- <div>
57
+ <div className="item">
58
+ <span className="label">Image formats</span>
59
+ <div className="types">
60
+ <div className="type">
64
61
  <button
65
- className="tableau-download-button tableau-format-download"
66
62
  onClick={() => {
67
63
  viz.showExportImageDialog();
68
64
  popupRef.current.triggerRef.current.click();
@@ -73,14 +69,11 @@ const Download = ({ viz }) => {
73
69
  </div>
74
70
  </div>
75
71
  </div>
76
- </li>
77
- <li>
78
- Other formats
79
- <div className="visualization-wrapper">
80
- <div className="visualization-info">
81
- <div>
72
+ <div className="item">
73
+ <span className="label">Other formats</span>
74
+ <div className="types">
75
+ <div className="type">
82
76
  <button
83
- className="tableau-download-button tableau-format-download"
84
77
  onClick={() => {
85
78
  viz.showExportPDFDialog();
86
79
  popupRef.current.triggerRef.current.click();
@@ -91,9 +84,9 @@ const Download = ({ viz }) => {
91
84
  </div>
92
85
  </div>
93
86
  </div>
94
- </li>
95
- </ul>
96
- </Popup>
87
+ </>
88
+ }
89
+ />
97
90
  );
98
91
  };
99
92
 
@@ -0,0 +1,2 @@
1
+ export { default as Download } from './Download';
2
+ export { default as JsonCodeSnippet } from './JsonCodeSnippet';
@@ -17,6 +17,10 @@ const VisualizationView = (props) => {
17
17
  <Tableau
18
18
  data={{
19
19
  ...tableau_visualization,
20
+ with_notes: false,
21
+ with_sources: false,
22
+ with_more_info: false,
23
+ with_share: false,
20
24
  with_download: true,
21
25
  }}
22
26
  breakpoints={
@@ -3,14 +3,19 @@ import config from '@plone/volto/registry';
3
3
 
4
4
  export default function VisualizationViewWidget(props) {
5
5
  const { value = {} } = props;
6
+
6
7
  return (
7
8
  <Tableau
8
9
  data={{
9
10
  ...value,
11
+ with_notes: false,
12
+ with_sources: false,
13
+ with_more_info: true,
14
+ with_share: true,
10
15
  with_download: true,
11
16
  }}
12
17
  breakpoints={
13
- config.blocks.blocksConfig.embed_tableau_visualization.breakpoints
18
+ config.blocks.blocksConfig?.embed_tableau_visualization?.breakpoints
14
19
  }
15
20
  />
16
21
  );
@@ -0,0 +1,33 @@
1
+ import React from 'react';
2
+ import { render } from '@testing-library/react';
3
+ import '@testing-library/jest-dom/extend-expect';
4
+ import { Provider } from 'react-redux';
5
+ import configureStore from 'redux-mock-store';
6
+ import VisualizationViewWidget from './VisualizationViewWidget';
7
+
8
+ const mockStore = configureStore([]);
9
+ const store = mockStore({});
10
+
11
+ jest.mock('@plone/volto/components', () => ({
12
+ Icon: ({ children }) => <img alt="incon">{children}</img>,
13
+ Toast: ({ children }) => <p>{children}</p>,
14
+ }));
15
+
16
+ describe('VisualizationViewWidget', () => {
17
+ it('should render the component', () => {
18
+ const data = {
19
+ url: 'http://localhost:3000/tableau-ct',
20
+ with_download: true,
21
+ with_more_info: true,
22
+ with_note: true,
23
+ with_share: true,
24
+ };
25
+
26
+ const { container } = render(
27
+ <Provider store={store}>
28
+ <VisualizationViewWidget data={data} />
29
+ </Provider>,
30
+ );
31
+ expect(container.querySelector('.tableau-wrapper')).toBeInTheDocument();
32
+ });
33
+ });
@@ -72,11 +72,18 @@ const VisualizationWidget = (props) => {
72
72
  >
73
73
  <Tableau
74
74
  ref={viz}
75
- data={value}
75
+ data={{
76
+ ...(value || {}),
77
+ with_notes: false,
78
+ with_sources: false,
79
+ with_more_info: false,
80
+ with_share: false,
81
+ with_download: false,
82
+ }}
76
83
  mode="edit"
77
84
  breakpoints={
78
- config.blocks.blocksConfig.embed_tableau_visualization
79
- .breakpoints
85
+ config.blocks.blocksConfig?.embed_tableau_visualization
86
+ ?.breakpoints
80
87
  }
81
88
  extraOptions={extraOptions}
82
89
  setVizState={setVizState}
@@ -116,9 +123,14 @@ const VisualizationWidget = (props) => {
116
123
  data={{
117
124
  ...props.value,
118
125
  autoScale: true,
126
+ with_notes: false,
127
+ with_sources: false,
128
+ with_more_info: false,
129
+ with_share: false,
130
+ with_download: false,
119
131
  }}
120
132
  breakpoints={
121
- config.blocks.blocksConfig.embed_tableau_visualization.breakpoints
133
+ config.blocks.blocksConfig?.embed_tableau_visualization?.breakpoints
122
134
  }
123
135
  extraOptions={extraOptions}
124
136
  />
@@ -0,0 +1,45 @@
1
+ import React from 'react';
2
+ import { render } from '@testing-library/react';
3
+ import '@testing-library/jest-dom/extend-expect';
4
+ import { Provider } from 'react-redux';
5
+ import configureStore from 'redux-mock-store';
6
+ import VisualizationWidget from './VisualizationWidget';
7
+
8
+ const mockStore = configureStore([]);
9
+ const store = mockStore({});
10
+
11
+ jest.mock('@plone/volto/components', () => ({
12
+ FormFieldWrapper: jest.fn(({ children }) => <>{children}</>),
13
+ InlineForm: jest.fn(() => <div>Mocked InlineForm</div>),
14
+ Icon: ({ children }) => <img alt="incon">{children}</img>,
15
+ Toast: ({ children }) => <p>{children}</p>,
16
+ }));
17
+
18
+ describe('VisualizationWidget', () => {
19
+ it('should render the component', () => {
20
+ const data = {
21
+ value: {
22
+ url: 'http://localhost:3000/tableau-ct',
23
+ with_download: true,
24
+ with_more_info: true,
25
+ with_note: true,
26
+ with_share: true,
27
+ hideTabs: false,
28
+ staticParameters: [
29
+ {
30
+ '@id': '1f050748-c020-4a48-8109-e99a25bf558d',
31
+ field: 'Tableau field',
32
+ value: 'Tableau value',
33
+ },
34
+ ],
35
+ },
36
+ };
37
+
38
+ const { container } = render(
39
+ <Provider store={store}>
40
+ <VisualizationWidget {...data} />
41
+ </Provider>,
42
+ );
43
+ expect(container.querySelector('.tableau-wrapper')).toBeInTheDocument();
44
+ });
45
+ });
@@ -38,7 +38,8 @@ const staticParameters = {
38
38
  };
39
39
 
40
40
  const breakpointUrlSchema = (config) => {
41
- const breakpoints = config.blocks.blocksConfig.tableau_block.breakpoints;
41
+ const breakpoints =
42
+ config.blocks.blocksConfig?.tableau_block?.breakpoints || {};
42
43
 
43
44
  return {
44
45
  title: 'URL',
package/src/helpers.js CHANGED
@@ -1,28 +1,31 @@
1
- import React from 'react';
2
-
3
- export const loadTableauScript = (callback, version) => {
4
- if (!__CLIENT__) return;
5
- const source = `https://public.tableau.com/javascripts/api/tableau-${version}.min.js`;
6
- const existingScript = document.getElementById(`tableauJS-${version}`);
7
- const existingScriptSource =
8
- existingScript && existingScript.getAttribute('src');
9
- // Replace script loaded on each version change
10
- if (existingScript && existingScriptSource !== source) {
11
- existingScript.setAttribute('src', source);
12
- }
13
- if (!existingScript) {
14
- const script = document.createElement('script');
15
- script.src = source;
16
- script.id = `tableauJS-${version}`;
17
- document.body.appendChild(script);
18
- script.onload = () => {
19
- window[`tableau_${version}`] = window.tableau;
20
- if (callback) callback();
21
- };
22
- }
23
- // Trigger callback
24
- if (existingScript && callback) callback();
25
- };
1
+ export async function loadTableauScript(version) {
2
+ return new Promise((resolve) => {
3
+ if (!__CLIENT__) {
4
+ resolve();
5
+ return;
6
+ }
7
+ const source = `https://public.tableau.com/javascripts/api/tableau-${version}.min.js`;
8
+ const existingScript = document.getElementById(`tableauJS-${version}`);
9
+ const existingScriptSource =
10
+ existingScript && existingScript.getAttribute('src');
11
+ // Replace script loaded on each version change
12
+ if (existingScript && existingScriptSource !== source) {
13
+ existingScript.setAttribute('src', source);
14
+ }
15
+ if (!existingScript) {
16
+ const script = document.createElement('script');
17
+ script.src = source;
18
+ script.id = `tableauJS-${version}`;
19
+ document.body.appendChild(script);
20
+ script.addEventListener('load', () => {
21
+ window[`tableau_${version}`] = window.tableau;
22
+ resolve(window[`tableau_${version}`]);
23
+ });
24
+ } else {
25
+ resolve(window[`tableau_${version}`]);
26
+ }
27
+ });
28
+ }
26
29
 
27
30
  // Script url for each version. In case you might need to add them in the load balancer
28
31
  // https://public.tableau.com/javascripts/api/tableau-2.8.0.min.js
@@ -34,25 +37,3 @@ export const loadTableauScript = (callback, version) => {
34
37
  // https://public.tableau.com/javascripts/api/tableau-2.2.2.min.js
35
38
  // https://public.tableau.com/javascripts/api/tableau-2.1.2.min.js
36
39
  // https://public.tableau.com/javascripts/api/tableau-2.0.3.min.js
37
-
38
- export const useCopyToClipboard = (text) => {
39
- const [copyStatus, setCopyStatus] = React.useState('inactive');
40
- const copy = React.useCallback(() => {
41
- navigator.clipboard.writeText(text).then(
42
- () => setCopyStatus('copied'),
43
- () => setCopyStatus('failed'),
44
- );
45
- }, [text]);
46
-
47
- React.useEffect(() => {
48
- if (copyStatus === 'inactive') {
49
- return;
50
- }
51
-
52
- const timeout = setTimeout(() => setCopyStatus('inactive'), 3000);
53
-
54
- return () => clearTimeout(timeout);
55
- }, [copyStatus]);
56
-
57
- return [copyStatus, copy];
58
- };
package/src/hooks.js CHANGED
@@ -1,17 +1,33 @@
1
1
  import { useEffect, useState } from 'react';
2
2
  import { loadTableauScript } from './helpers';
3
3
 
4
+ let clock;
5
+
6
+ const TIMEOUT = 10000;
7
+
4
8
  export const useTableau = (version) => {
5
9
  const [tableau, setTableau] = useState();
6
10
 
7
11
  useEffect(() => {
8
- loadTableauScript(() => {
9
- if (window[`tableau_${version}`]) {
10
- setTableau(window[`tableau_${version}`]);
11
- } else {
12
- setTableau(undefined);
12
+ loadTableauScript(version);
13
+
14
+ const startTime = new Date().getTime();
15
+
16
+ clock = setInterval(() => {
17
+ const tableauApi = window[`tableau_${version}`];
18
+ if (tableauApi) {
19
+ setTableau(tableauApi);
20
+ clearInterval(clock);
21
+ return;
22
+ }
23
+ if (new Date().getTime() - startTime > TIMEOUT) {
24
+ clearInterval(clock);
13
25
  }
14
- }, version);
26
+ }, 100);
27
+
28
+ return () => {
29
+ clearInterval(clock);
30
+ };
15
31
  }, [version]);
16
32
 
17
33
  return tableau;