@eeacms/volto-tableau 3.0.8 → 4.1.0

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.
Files changed (48) hide show
  1. package/CHANGELOG.md +20 -7
  2. package/Jenkinsfile +2 -0
  3. package/jest-addon.config.js +2 -2
  4. package/package.json +1 -1
  5. package/src/{TableauBlock → Blocks/EmbedTableauVisualization}/Edit.jsx +7 -7
  6. package/src/Blocks/EmbedTableauVisualization/View.jsx +66 -0
  7. package/src/Blocks/EmbedTableauVisualization/index.js +30 -0
  8. package/src/Blocks/{EmbedEEATableauBlock → EmbedTableauVisualization}/schema.js +32 -13
  9. package/src/Blocks/TableauBlock/Edit.jsx +41 -0
  10. package/src/Blocks/TableauBlock/View.jsx +101 -0
  11. package/src/Blocks/TableauBlock/index.js +30 -0
  12. package/src/Blocks/TableauBlock/schema.js +224 -0
  13. package/src/Blocks/index.js +9 -0
  14. package/src/Tableau/Tableau.jsx +430 -0
  15. package/src/Tableau/helpers.js +41 -0
  16. package/src/Utils/Download/Download.jsx +72 -0
  17. package/src/Utils/JsonCodeSnippet/JsonCodeSnippet.jsx +48 -0
  18. package/src/Utils/Share/Share.jsx +21 -0
  19. package/src/Utils/Sources/Sources.jsx +66 -0
  20. package/src/Views/VisualizationView.jsx +18 -32
  21. package/src/Widgets/VisualizationWidget.jsx +92 -122
  22. package/src/Widgets/schema.js +88 -115
  23. package/src/helpers.js +15 -34
  24. package/src/hooks.js +18 -0
  25. package/src/icons/download.svg +5 -0
  26. package/src/index.js +4 -66
  27. package/src/less/tableau.less +172 -70
  28. package/src/less/tableau.variables +7 -13
  29. package/src/Blocks/EmbedEEATableauBlock/Edit.jsx +0 -56
  30. package/src/Blocks/EmbedEEATableauBlock/View.jsx +0 -74
  31. package/src/ConnectedTableau/ConnectedTableau.jsx +0 -29
  32. package/src/CustomWidgets/UrlParamsWidget.jsx +0 -29
  33. package/src/DownloadExtras/TableauDownload.jsx +0 -124
  34. package/src/DownloadExtras/TableauFullscreen.jsx +0 -78
  35. package/src/DownloadExtras/TableauShare.jsx +0 -81
  36. package/src/DownloadExtras/style.less +0 -152
  37. package/src/Sources/Sources.jsx +0 -50
  38. package/src/Sources/index.js +0 -3
  39. package/src/Sources/style.css +0 -7
  40. package/src/Tableau/View.jsx +0 -254
  41. package/src/TableauBlock/View.jsx +0 -109
  42. package/src/TableauBlock/schema.js +0 -124
  43. package/src/Widgets/style.less +0 -8
  44. package/src/actions.js +0 -9
  45. package/src/constants.js +0 -1
  46. package/src/downloadHelpers/downloadHelpers.js +0 -25
  47. package/src/middleware.js +0 -39
  48. package/src/store.js +0 -72
@@ -1,81 +0,0 @@
1
- import React from 'react';
2
- import { Popup, Tab, Button, Menu, Input } from 'semantic-ui-react';
3
- import { Icon } from '@plone/volto/components';
4
- import useCopyToClipboard from '../downloadHelpers/downloadHelpers';
5
-
6
- import shareSVG from '@plone/volto/icons/share.svg';
7
- import linkSVG from '@plone/volto/icons/link.svg';
8
-
9
- import cx from 'classnames';
10
-
11
- const TableauShare = (props) => {
12
- const tableau_url = props.data.url;
13
-
14
- const CopyUrlButton = ({ url, buttonText }) => {
15
- const [copyUrlStatus, copyUrl] = useCopyToClipboard(url);
16
-
17
- if (copyUrlStatus === 'copied') {
18
- buttonText = 'Copied!';
19
- } else if (copyUrlStatus === 'failed') {
20
- buttonText = 'Copy failed. Please try again.';
21
- }
22
-
23
- return (
24
- <Button
25
- primary
26
- onClick={copyUrl}
27
- className={cx('copy-button', {
28
- 'green-button': copyUrlStatus === 'copied',
29
- })}
30
- >
31
- {buttonText}
32
- </Button>
33
- );
34
- };
35
-
36
- const panes = [
37
- {
38
- menuItem: (
39
- <Menu.Item key="location">
40
- <span className="nav-dot">
41
- <Icon name={linkSVG} size="24px" />
42
- </span>
43
- <span className="nav-dot-title">URL</span>
44
- </Menu.Item>
45
- ),
46
- render: () => (
47
- <Tab.Pane>
48
- <Input defaultValue={tableau_url} />
49
- <CopyUrlButton url={tableau_url} buttonText="Copy sharing URL" />
50
- </Tab.Pane>
51
- ),
52
- },
53
- ];
54
-
55
- return (
56
- <Popup
57
- basic
58
- className="tableau-share-dialog"
59
- position="top center"
60
- on="click"
61
- trigger={
62
- <div className="toolbar-button-wrapper">
63
- <Button className="toolbar-button" title="Share">
64
- <Icon name={shareSVG} size="26px" />
65
- </Button>
66
- <span className="btn-text">Share</span>
67
- </div>
68
- }
69
- >
70
- <Popup.Header>Share Visualization</Popup.Header>
71
- <Popup.Content>
72
- <Tab
73
- menu={{ secondary: true, pointing: true, fluid: true }}
74
- panes={panes}
75
- />
76
- </Popup.Content>
77
- </Popup>
78
- );
79
- };
80
-
81
- export default TableauShare;
@@ -1,152 +0,0 @@
1
- .dashboard-wrapper {
2
- display: flex;
3
- flex-direction: row;
4
- flex-wrap: wrap;
5
- background-color: #f5f5f5;
6
-
7
- .tableau-block {
8
- position: relative;
9
- display: flex;
10
- flex: 1 1;
11
- flex-direction: column;
12
- padding: 1em 0.5em;
13
- }
14
-
15
- .toolbar-button {
16
- position: relative;
17
- width: 32px;
18
- height: 32px;
19
- padding: 2px !important;
20
- background-color: #ff421b !important;
21
- border-radius: 3px !important;
22
- color: white !important;
23
-
24
- &:hover {
25
- opacity: 0.6;
26
- }
27
-
28
- svg {
29
- position: absolute;
30
- top: 50%;
31
- left: 50%;
32
- margin: 0 !important;
33
- fill: #fff !important;
34
- -webkit-transform: translate(-50%, -50%);
35
- transform: translate(-50%, -50%);
36
- }
37
- }
38
- }
39
-
40
- .tableau-download-dialog {
41
- max-width: 282px !important;
42
-
43
- .header {
44
- padding: 0.5em 0 !important;
45
- }
46
-
47
- button {
48
- display: block;
49
- width: 100%;
50
- margin: 5px 0 !important;
51
- }
52
- }
53
-
54
- .tableau-share-dialog {
55
- min-width: 400px !important;
56
-
57
- .ui.secondary.pointing.menu .item {
58
- display: flex;
59
- flex-direction: column;
60
- padding: 10px 25px;
61
- }
62
-
63
- .nav-dot-title {
64
- margin-top: 5px;
65
- font-size: 14px;
66
- }
67
-
68
- .ui.secondary.pointing.menu .active.item {
69
- border-color: #09b;
70
-
71
- .nav-dot-title {
72
- font-weight: 500;
73
- }
74
-
75
- .nav-dot {
76
- opacity: 1;
77
- }
78
- }
79
-
80
- .nav-dot {
81
- display: flex;
82
- width: 37px;
83
- height: 37px;
84
- align-items: center;
85
- justify-content: center;
86
- margin: 8px auto;
87
- background-color: #09b;
88
- border-radius: 50%;
89
- opacity: 0.6;
90
-
91
- svg {
92
- fill: #fff !important;
93
- }
94
- }
95
-
96
- .ui.secondary.pointing.menu .active.item:hover {
97
- border-color: #09b;
98
- }
99
-
100
- .ui.tab {
101
- .ui.input {
102
- display: block;
103
- margin-bottom: 1em;
104
-
105
- input {
106
- width: 100%;
107
- }
108
- }
109
-
110
- textarea {
111
- display: block;
112
- width: 100%;
113
- height: 100px;
114
- padding: 6px 12px;
115
- border: 1px solid grey;
116
- margin-bottom: 1.5em;
117
- color: #696969;
118
- font-family: 'Lucida Console', Monaco, monospace;
119
- font-size: 13px;
120
- }
121
- }
122
- }
123
-
124
- .copy-button {
125
- margin-top: 1rem !important;
126
- }
127
-
128
- .copy-button.green-button {
129
- background-color: #269b65 !important;
130
- }
131
-
132
- .toolbar-button-wrapper {
133
- display: flex;
134
- flex-direction: column;
135
- text-align: center;
136
-
137
- .btn-text {
138
- font-family: Roboto, Helvetica Neue, Arial, Helvetica, sans-serif;
139
- font-size: 12px;
140
- font-weight: 400;
141
- line-height: 20px;
142
- }
143
-
144
- .ui.button.toolbar-button {
145
- margin: 0 7px !important;
146
- }
147
- }
148
-
149
- .tableau-icons {
150
- display: flex;
151
- flex-direction: row;
152
- }
@@ -1,50 +0,0 @@
1
- /* eslint-disable jsx-a11y/no-static-element-interactions */
2
- /* eslint-disable jsx-a11y/click-events-have-key-events */
3
- /* eslint-disable jsx-a11y/anchor-is-valid */
4
-
5
- import React from 'react';
6
- import { UniversalLink, Icon } from '@plone/volto/components';
7
-
8
- import rightKeySVG from '@plone/volto/icons/right-key.svg';
9
- import downKeySVG from '@plone/volto/icons/down-key.svg';
10
-
11
- import './style.css';
12
-
13
- const SourcesWidget = ({ sources }) => {
14
- const [expand, setExpand] = React.useState(true);
15
- return (
16
- <div>
17
- <a className="embed-sources-header" onClick={() => setExpand(!expand)}>
18
- <h3>
19
- <Icon
20
- name={expand ? downKeySVG : rightKeySVG}
21
- title={expand ? 'Collapse' : 'Expand'}
22
- size="17px"
23
- />
24
- Sources:
25
- </h3>
26
- </a>
27
- {expand && (
28
- <ul>
29
- {sources &&
30
- sources.data &&
31
- sources.data.map((param, i) =>
32
- param.link ? (
33
- <li key={i} className="embed-source-param">
34
- <UniversalLink
35
- className="embed-sources-param-title"
36
- href={param.link}
37
- >
38
- {param.title}
39
- </UniversalLink>
40
- , {param.organisation}
41
- </li>
42
- ) : null,
43
- )}
44
- </ul>
45
- )}
46
- </div>
47
- );
48
- };
49
-
50
- export default SourcesWidget;
@@ -1,3 +0,0 @@
1
- import Sources from './Sources';
2
-
3
- export { Sources };
@@ -1,7 +0,0 @@
1
- .embed-sources-header {
2
- cursor: pointer;
3
- }
4
-
5
- .embed-sources-param-description {
6
- margin-left: 5px;
7
- }
@@ -1,254 +0,0 @@
1
- import React from 'react';
2
- import { connect } from 'react-redux';
3
- import { compose } from 'redux';
4
- import { toast } from 'react-toastify';
5
- import { Toast } from '@plone/volto/components';
6
- import { setTableauApi } from '@eeacms/volto-tableau/actions';
7
- import cx from 'classnames';
8
- import { loadTableauScript } from '../helpers';
9
- import TableauDownload from '../DownloadExtras/TableauDownload';
10
- import TableauShare from '../DownloadExtras/TableauShare';
11
- import '../DownloadExtras/style.less';
12
-
13
- const Tableau = (props) => {
14
- const ref = React.useRef(null);
15
- const filters = React.useRef(props.data.filters || {});
16
- const mounted = React.useRef(false);
17
- const [viz, setViz] = React.useState(null);
18
- const {
19
- canUpdateUrl = true,
20
- data = {},
21
- error = null,
22
- extraFilters = {},
23
- extraOptions = {},
24
- loaded = false,
25
- mode = 'view',
26
- screen = {},
27
- setError = () => {},
28
- setLoaded = () => {},
29
- version = '2.8.0',
30
- } = props;
31
- const {
32
- autoScale = false,
33
- hideTabs = false,
34
- hideToolbar = false,
35
- sheetname = '',
36
- toolbarPosition = 'Top',
37
- } = data;
38
- const defaultUrl = data.url;
39
- const url = props.url || defaultUrl;
40
-
41
- //load tableau from script tag
42
- const tableau = loadTableauScript(() => {}, version);
43
-
44
- const onFilterChange = (filter) => {
45
- const newFilters = { ...filters.current };
46
- const fieldName = filter.getFieldName();
47
- const values = filter
48
- .getAppliedValues()
49
- .map((appliedValue) => appliedValue.value);
50
- newFilters[fieldName] = values;
51
- if (JSON.stringify(newFilters) !== JSON.stringify(filters)) {
52
- props.onChangeBlock(props.block, {
53
- ...data,
54
- filters: {
55
- ...newFilters,
56
- },
57
- });
58
- filters.current = { ...newFilters };
59
- }
60
- };
61
-
62
- const disposeViz = () => {
63
- if (viz) {
64
- viz.dispose();
65
- }
66
- if (loaded) {
67
- setLoaded(false);
68
- }
69
- if (error) {
70
- setError(null);
71
- }
72
- };
73
-
74
- const initViz = () => {
75
- disposeViz();
76
- try {
77
- const newViz = new tableau.Viz(ref.current, url || defaultUrl, {
78
- hideTabs,
79
- hideToolbar,
80
- sheetname,
81
- toolbarPosition,
82
- ...data.filters,
83
- ...extraFilters,
84
- ...extraOptions,
85
- onFirstInteractive: () => {
86
- setLoaded(true);
87
- if (newViz && mode === 'edit') {
88
- const workbook = newViz.getWorkbook();
89
- const newData = {
90
- url: canUpdateUrl ? newViz.getUrl() : defaultUrl,
91
- sheetname: workbook.getActiveSheet().getName(),
92
- };
93
- if (newData.url !== url || newData.sheetname !== sheetname) {
94
- props.onChangeBlock(props.block, {
95
- ...data,
96
- ...newData,
97
- });
98
- toast.success(<Toast success title={'Tableau data updated'} />);
99
- }
100
- // Filter change event
101
- newViz.addEventListener(
102
- tableau.TableauEventName.FILTER_CHANGE,
103
- (event) => {
104
- event.getFilterAsync().then((filter) => {
105
- onFilterChange(filter);
106
- });
107
- },
108
- );
109
- }
110
- },
111
- });
112
-
113
- return setViz(newViz);
114
- } catch (e) {
115
- setError(e._message);
116
- }
117
- };
118
-
119
- const addExtraFilters = (extraFilters) => {
120
- const worksheets = viz.getWorkbook().getActiveSheet().getWorksheets() || [];
121
-
122
- worksheets.forEach((worksheet) => {
123
- if (worksheet.getSheetType() === tableau.DashboardObjectType.WORKSHEET) {
124
- Object.keys(extraFilters).forEach((filter) => {
125
- if (!extraFilters[filter]) {
126
- worksheet.clearFilterAsync(filter);
127
- } else {
128
- worksheet.applyFilterAsync(
129
- filter,
130
- extraFilters[filter],
131
- tableau.FilterUpdateType.REPLACE,
132
- );
133
- }
134
- });
135
- }
136
- });
137
- };
138
-
139
- const updateScale = () => {
140
- const tableauWrapper = ref.current;
141
- const tableau = tableauWrapper?.querySelector('iframe');
142
- if (mounted.current && viz && tableauWrapper && tableau) {
143
- const { sheetSize = {} } = viz.getVizSize() || {};
144
- const vizWidth = sheetSize?.minSize?.width || 1;
145
- const vizHeight = sheetSize?.minSize?.height || 0;
146
- const scale = Math.min(tableauWrapper.clientWidth / vizWidth, 1);
147
- tableau.style.transform = `scale(${scale})`;
148
- tableau.style.width = `${100 / scale}%`;
149
- tableauWrapper.style.height = `${scale * vizHeight}px`;
150
- }
151
- };
152
-
153
- React.useEffect(() => {
154
- if (!mounted.current) {
155
- mounted.current = true;
156
- }
157
- return () => {
158
- mounted.current = false;
159
- };
160
- }, []);
161
-
162
- React.useEffect(() => {
163
- // Load new tableau api
164
- if (__CLIENT__ && !props.tableau[version]) {
165
- props.setTableauApi(version, props.mode);
166
- }
167
- if (__CLIENT__) {
168
- loadTableauScript(() => {}, version);
169
- }
170
- /* eslint-disable-next-line */
171
- }, [version]);
172
-
173
- React.useEffect(() => {
174
- if (__CLIENT__ && tableau && url) {
175
- initViz();
176
- } else {
177
- disposeViz();
178
- }
179
-
180
- return () => {
181
- disposeViz();
182
- };
183
- /* eslint-disable-next-line */
184
- }, [
185
- hideTabs,
186
- hideToolbar,
187
- autoScale,
188
- sheetname,
189
- tableau,
190
- toolbarPosition,
191
- url,
192
- version,
193
- ]);
194
-
195
- React.useEffect(() => {
196
- if (mounted.current && loaded && viz) {
197
- addExtraFilters(extraFilters);
198
- }
199
- /* eslint-disable-next-line */
200
- }, [JSON.stringify(extraFilters)]);
201
-
202
- React.useEffect(() => {
203
- if (autoScale) {
204
- updateScale();
205
- }
206
- /* eslint-disable-next-line */
207
- }, [loaded, screen?.page?.width]);
208
-
209
- return (
210
- <div id="tableau-wrap">
211
- <div id="tableau-outer">
212
- {data && Object.keys(data).length > 0 ? (
213
- <>
214
- {loaded ? (
215
- ''
216
- ) : (
217
- <div className="tableau-loader">
218
- <span>Loading...</span>
219
- </div>
220
- )}
221
- </>
222
- ) : (
223
- <div>No data present in that visualization.</div>
224
- )}
225
- <div className="dashboard-wrapper">
226
- <div className="tableau-block">
227
- {viz ? (
228
- <div className="tableau-icons">
229
- <TableauDownload {...props} viz={viz} />
230
- <TableauShare {...props} viz={viz} data={{ url: url }} />
231
- </div>
232
- ) : null}
233
- <div
234
- className={cx('tableau', version, {
235
- 'tableau-scale': autoScale,
236
- })}
237
- ref={ref}
238
- />
239
- </div>
240
- </div>
241
- </div>
242
- </div>
243
- );
244
- };
245
-
246
- export default compose(
247
- connect(
248
- (state, props) => ({
249
- tableau: state.tableau,
250
- screen: state.screen,
251
- }),
252
- { setTableauApi },
253
- ),
254
- )(Tableau);
@@ -1,109 +0,0 @@
1
- import React from 'react';
2
- import { connect } from 'react-redux';
3
- import { compose } from 'redux';
4
- import { withRouter } from 'react-router';
5
- import Tableau from '@eeacms/volto-tableau/Tableau/View';
6
- import config from '@plone/volto/registry';
7
- import qs from 'querystring';
8
- import '@eeacms/volto-tableau/less/tableau.less';
9
-
10
- const getDevice = (config, width) => {
11
- const breakpoints = config.blocks.blocksConfig.tableau_block.breakpoints;
12
- let device = 'default';
13
- Object.keys(breakpoints).forEach((breakpoint) => {
14
- if (
15
- width <= breakpoints[breakpoint][0] &&
16
- width >= breakpoints[breakpoint][1]
17
- ) {
18
- device = breakpoint;
19
- }
20
- });
21
- return device;
22
- };
23
-
24
- const View = (props) => {
25
- const [error, setError] = React.useState(null);
26
- const [loaded, setLoaded] = React.useState(null);
27
- const [mounted, setMounted] = React.useState(false);
28
- const [extraFilters, setExtraFilters] = React.useState({});
29
- const { data = {}, query = {}, screen = {} } = props;
30
- const {
31
- breakpointUrls = [],
32
- urlParameters = [],
33
- title = null,
34
- description = null,
35
- autoScale = false,
36
- } = data;
37
- const device = getDevice(config, screen.page?.width || Infinity);
38
- const breakpointUrl = breakpointUrls.filter(
39
- (breakpoint) => breakpoint.device === device,
40
- )[0]?.url;
41
- const url = breakpointUrl || data.url;
42
-
43
- React.useEffect(() => {
44
- setMounted(true);
45
- /* eslint-disable-next-line */
46
- }, []);
47
-
48
- React.useEffect(() => {
49
- if (props.setTableauError) props.setTableauError(error);
50
- /* eslint-disable-next-line */
51
- }, [error]);
52
-
53
- React.useEffect(() => {
54
- const newExtraFilters = { ...extraFilters };
55
- urlParameters.forEach((element) => {
56
- if (element.field && typeof query[element.urlParam] !== 'undefined') {
57
- newExtraFilters[element.field] = query[element.urlParam];
58
- } else if (newExtraFilters[element.field]) {
59
- delete newExtraFilters[element.field];
60
- }
61
- });
62
- setExtraFilters(newExtraFilters);
63
- /* eslint-disable-next-line */
64
- }, [JSON.stringify(query), JSON.stringify(urlParameters)]);
65
-
66
- return mounted ? (
67
- <div className="tableau-block">
68
- <div className="tableau-info">
69
- {loaded && url && props.mode === 'edit' ? (
70
- <h3 className="tableau-version">== Tableau ==</h3>
71
- ) : null}
72
- {!url ? <p className="tableau-error">URL required</p> : ''}
73
- {error ? <p className="tableau-error">{error}</p> : ''}
74
- </div>
75
-
76
- {loaded && title ? <h3 className="tableau-title">{title}</h3> : ''}
77
- {loaded && description ? (
78
- <p className="tableau-description">{description}</p>
79
- ) : (
80
- ''
81
- )}
82
- {url ? (
83
- <Tableau
84
- {...props}
85
- canUpdateUrl={!breakpointUrl}
86
- extraFilters={extraFilters}
87
- extraOptions={{ device: autoScale ? 'desktop' : device }}
88
- error={error}
89
- loaded={loaded}
90
- setError={setError}
91
- setLoaded={setLoaded}
92
- url={url}
93
- />
94
- ) : null}
95
- </div>
96
- ) : (
97
- ''
98
- );
99
- };
100
-
101
- export default compose(
102
- connect((state, props) => ({
103
- query: {
104
- ...(qs.parse(state.router.location?.search?.replace('?', '')) || {}),
105
- ...(state.discodata_query?.search || {}),
106
- },
107
- screen: state.screen,
108
- })),
109
- )(withRouter(View));