@eeacms/volto-tableau 8.1.1 → 8.1.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.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,14 @@ All notable changes to this project will be documented in this file. Dates are d
4
4
 
5
5
  Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
6
6
 
7
+ ### [8.1.2](https://github.com/eea/volto-tableau/compare/8.1.1...8.1.2) - 14 November 2024
8
+
9
+ #### :hammer_and_wrench: Others
10
+
11
+ - add test [Miu Razvan - [`862b32c`](https://github.com/eea/volto-tableau/commit/862b32c6d2760cc2169eef9f900491e6afb9a00a)]
12
+ - add VisualizationView test [Miu Razvan - [`b4bea0a`](https://github.com/eea/volto-tableau/commit/b4bea0a6fc9814927b0b471ceebf4a9d4bfc50f2)]
13
+ - Disable tableau editor actions while loading the tableau [Miu Razvan - [`9f33c3c`](https://github.com/eea/volto-tableau/commit/9f33c3c2c3bcb0a3e36ba21e40cbb86f2dfc6150)]
14
+ - Extract parameters from tableau url, ref #279989 [Miu Razvan - [`f3c00d1`](https://github.com/eea/volto-tableau/commit/f3c00d124bd9417f44d9e9a6b054687de0859bbd)]
7
15
  ### [8.1.1](https://github.com/eea/volto-tableau/compare/8.1.0...8.1.1) - 11 October 2024
8
16
 
9
17
  #### :bug: Bug Fixes
package/jest.setup.js CHANGED
@@ -23,6 +23,17 @@ global.store = mockStore({
23
23
  const mockReactRouter = jest.requireActual('react-router');
24
24
  const mockSemanticComponents = jest.requireActual('semantic-ui-react');
25
25
  const mockComponents = jest.requireActual('@plone/volto/components');
26
+ const config = jest.requireActual('@plone/volto/registry').default;
27
+
28
+ config.blocks.blocksConfig = {
29
+ embed_tableau_visualization: {
30
+ breakpoints: {
31
+ desktop: [Infinity, 992],
32
+ tablet: [991, 768],
33
+ phone: [767, 0],
34
+ },
35
+ },
36
+ };
26
37
 
27
38
  jest.mock('react-router', () => {
28
39
  return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eeacms/volto-tableau",
3
- "version": "8.1.1",
3
+ "version": "8.1.2",
4
4
  "description": "@eeacms/volto-tableau: Volto add-on",
5
5
  "main": "src/index.js",
6
6
  "author": "European Environment Agency: IDM2 A-Team",
@@ -69,7 +69,7 @@ const staticParameters = {
69
69
  required: [],
70
70
  };
71
71
 
72
- const schema = (props) => {
72
+ const getSchema = (props) => {
73
73
  return {
74
74
  title: 'Embed Dashboard (Tableau)',
75
75
  fieldsets: [
@@ -182,4 +182,4 @@ const schema = (props) => {
182
182
  };
183
183
  };
184
184
 
185
- export default schema;
185
+ export default getSchema;
@@ -25,6 +25,7 @@ import {
25
25
  toInteger,
26
26
  toNumber,
27
27
  } from 'lodash';
28
+ import qs from 'qs';
28
29
  import cx from 'classnames';
29
30
  import { Button } from 'semantic-ui-react';
30
31
  import { Toast, Icon } from '@plone/volto/components';
@@ -44,6 +45,12 @@ import resetSVG from '@plone/volto/icons/reset.svg';
44
45
 
45
46
  import '@eeacms/volto-embed/Toolbar/styles.less';
46
47
 
48
+ function decodeString(str) {
49
+ const tempDiv = document.createElement('div');
50
+ tempDiv.innerHTML = str;
51
+ return tempDiv.textContent || tempDiv.innerText || '';
52
+ }
53
+
47
54
  function getHeight(height) {
48
55
  const asNumber = isNumber(Number(height)) && !isNaN(Number(height));
49
56
  if (asNumber) {
@@ -52,6 +59,22 @@ function getHeight(height) {
52
59
  return height;
53
60
  }
54
61
 
62
+ function getTableauValue(value, dataType) {
63
+ if (dataType === 'string' && !isString(value)) {
64
+ return toString(value);
65
+ }
66
+ if (dataType === 'integer' && !isInteger(value)) {
67
+ return toInteger(value);
68
+ }
69
+ if (dataType === 'float' && !isNumber(value)) {
70
+ return toNumber(value);
71
+ }
72
+ if (dataType === 'boolean' && !isBoolean(value)) {
73
+ return !!value;
74
+ }
75
+ return value;
76
+ }
77
+
55
78
  const TableauDebug = ({ mode, data, vizState, url, version, clearData }) => {
56
79
  const { loaded, error } = vizState;
57
80
  const { filters = {}, parameters = {} } = data;
@@ -136,6 +159,7 @@ const Tableau = forwardRef((props, ref) => {
136
159
  with_enlarge = true,
137
160
  tableau_height,
138
161
  } = data;
162
+
139
163
  const device = useMemo(
140
164
  () => getDevice(breakpoints, screen.page?.width || Infinity),
141
165
  [breakpoints, screen],
@@ -214,46 +238,45 @@ const Tableau = forwardRef((props, ref) => {
214
238
  }
215
239
  };
216
240
 
217
- const onVizStateUpdate = useCallback((loaded, loading, error) => {
218
- vizState.current = { ...vizState.current, loaded, loading, error };
219
- setLoaded(loaded);
220
- setLoading(loading);
221
- setError(error);
222
- if (setVizState) {
223
- setVizState({ loaded, loading, error });
224
- }
225
- /* eslint-disable-next-line */
226
- }, []);
241
+ const onVizStateUpdate = useCallback(
242
+ (loaded, loading, error) => {
243
+ vizState.current = { ...vizState.current, loaded, loading, error };
244
+ setLoaded(loaded);
245
+ setLoading(loading);
246
+ setError(error);
247
+ if (setVizState) {
248
+ setVizState({ loaded, loading, error });
249
+ }
250
+ },
251
+ [setVizState],
252
+ );
227
253
 
228
- const activateDefaultSheet = useCallback(() => {
229
- if (!vizState.current.loaded || !viz.current) return;
254
+ const activateDefaultSheet = useCallback(async () => {
255
+ if (!vizState.current.loaded || !viz.current) return Promise.resolve();
230
256
  const workbook = viz.current.getWorkbook();
231
257
  const sheetnames = getSheetnames(viz.current);
232
258
  const activeSheetName = getActiveSheetname(viz.current);
233
259
  if (sheetnames.includes(sheetname) && sheetname !== activeSheetName) {
234
- workbook.activateSheetAsync(sheetname).then(() => {
235
- onVizStateUpdate(true, false, null);
236
- });
237
- } else {
238
- onVizStateUpdate(true, false, null);
260
+ return workbook.activateSheetAsync(sheetname);
239
261
  }
240
- }, [onVizStateUpdate, sheetname]);
262
+ return Promise.resolve();
263
+ }, [sheetname]);
241
264
 
242
- const clearData = () => {
265
+ const clearData = useCallback(() => {
243
266
  onChangeBlock(block, {
244
267
  ...data,
245
268
  filters: {},
246
269
  parameters: {},
247
270
  });
248
- };
271
+ }, [onChangeBlock, block, data]);
249
272
 
250
- const disposeViz = () => {
273
+ const disposeViz = useCallback(() => {
251
274
  if (viz.current) {
252
275
  viz.current.dispose();
253
276
  viz.current = null;
254
277
  }
255
278
  onVizStateUpdate(false, false, null);
256
- };
279
+ }, [onVizStateUpdate]);
257
280
 
258
281
  const initViz = () => {
259
282
  try {
@@ -265,12 +288,12 @@ const Tableau = forwardRef((props, ref) => {
265
288
  device: !!breakpointUrl ? device : 'desktop',
266
289
  ...extraOptions,
267
290
  ...data.filters,
291
+ ...data.parameters,
268
292
  ...extraFilters,
269
293
  ...extraParameters,
270
- onFirstInteractive: () => {
294
+ onFirstInteractive: async () => {
271
295
  onVizStateUpdate(true, true, null);
272
- setInitiateViz(false);
273
- activateDefaultSheet();
296
+ await activateDefaultSheet();
274
297
  if (viz.current && mode === 'edit' && !breakpointUrl) {
275
298
  const sheetnames = getSheetnames(viz.current);
276
299
  const activeSheetname = getActiveSheetname(viz.current);
@@ -282,6 +305,24 @@ const Tableau = forwardRef((props, ref) => {
282
305
  if (!sheetname || !sheetnames.includes(sheetname)) {
283
306
  newData.sheetname = activeSheetname;
284
307
  }
308
+ if (newData.url !== url) {
309
+ // Get parameters from url
310
+ const workbook = viz.current.getWorkbook();
311
+ const tableauParameters = await workbook.getParametersAsync();
312
+ const searchParams = qs.parse(decodeString(new URL(url).search));
313
+ tableauParameters.forEach((param) => {
314
+ const name = param.getName();
315
+ const dataType = param.getDataType();
316
+ if (!searchParams[name]) return;
317
+ if (!newData.parameters) {
318
+ newData.parameters = {};
319
+ }
320
+ newData.parameters[name] = getTableauValue(
321
+ searchParams[name],
322
+ dataType,
323
+ );
324
+ });
325
+ }
285
326
  if (newData.url !== url || newData.sheetname !== sheetname) {
286
327
  onChangeBlock(block, {
287
328
  ...data,
@@ -312,6 +353,8 @@ const Tableau = forwardRef((props, ref) => {
312
353
  },
313
354
  );
314
355
  }
356
+ onVizStateUpdate(true, false, null);
357
+ setInitiateViz(false);
315
358
  },
316
359
  });
317
360
  } catch (e) {
@@ -346,9 +389,9 @@ const Tableau = forwardRef((props, ref) => {
346
389
  } else {
347
390
  disposeViz();
348
391
  }
349
- /* eslint-disable-next-line */
350
- }, [tableau, url, hideTabs, hideToolbar, toolbarPosition]);
392
+ }, [tableau, url, disposeViz, hideTabs, hideToolbar, toolbarPosition]);
351
393
 
394
+ // Initialize viz
352
395
  useEffect(() => {
353
396
  if (initiateViz && !loaded && !loading) {
354
397
  initViz();
@@ -396,6 +439,7 @@ const Tableau = forwardRef((props, ref) => {
396
439
  // /* eslint-disable-next-line */
397
440
  // }, [loaded, JSON.stringify(extraFilters)]);
398
441
 
442
+ // Add extra parameters
399
443
  useEffect(() => {
400
444
  async function addExtraParameters() {
401
445
  if (vizState.current.loaded && viz.current) {
@@ -419,19 +463,7 @@ const Tableau = forwardRef((props, ref) => {
419
463
  value = value
420
464
  .filter((v) => includes(values, v))
421
465
  .map((v) => {
422
- if (dataType === 'string' && !isString(v)) {
423
- return toString(v);
424
- }
425
- if (dataType === 'integer' && !isInteger(v)) {
426
- return toInteger(v);
427
- }
428
- if (dataType === 'float' && !isNumber(v)) {
429
- return toNumber(v);
430
- }
431
- if (dataType === 'boolean' && !isBoolean(v)) {
432
- return !!v;
433
- }
434
- return v;
466
+ return getTableauValue(v, dataType);
435
467
  });
436
468
  if (value?.length) {
437
469
  workbook.changeParameterValueAsync(fieldName, value);
@@ -450,6 +482,7 @@ const Tableau = forwardRef((props, ref) => {
450
482
  /* eslint-disable-next-line */
451
483
  }, [loaded, JSON.stringify(extraParameters)]);
452
484
 
485
+ // Update viz scale on window resize
453
486
  useEffect(() => {
454
487
  if (vizState.current.loaded && viz.current && autoScale) {
455
488
  updateScale();
@@ -457,14 +490,18 @@ const Tableau = forwardRef((props, ref) => {
457
490
  /* eslint-disable-next-line */
458
491
  }, [loaded, screen?.page?.width]);
459
492
 
493
+ // Activate default sheet
460
494
  useEffect(() => {
461
495
  if (vizState.current.loaded && viz.current) {
462
496
  onVizStateUpdate(true, true, null);
463
- activateDefaultSheet();
497
+ activateDefaultSheet().then(() => {
498
+ onVizStateUpdate(true, false, null);
499
+ });
464
500
  }
465
501
  /* eslint-disable-next-line */
466
502
  }, [sheetname]);
467
503
 
504
+ // Set mobile mode
468
505
  useEffect(() => {
469
506
  if (!loading && tableauEl.current) {
470
507
  const visWidth = tableauEl.current.offsetWidth;
@@ -477,6 +514,7 @@ const Tableau = forwardRef((props, ref) => {
477
514
  }
478
515
  }, [screen, mobile, loading]);
479
516
 
517
+ // Keep viz reference
480
518
  useImperativeHandle(
481
519
  ref,
482
520
  () => {
@@ -151,8 +151,10 @@ export const getActiveSheetname = (viz) => {
151
151
  export const canChangeVizData = (viz, vizState) => {
152
152
  // If viz is null it means that the viz is loading
153
153
  // If viz is undefined it means that there is no viz nor it is loading
154
- if (vizState?.loading) return false;
155
- return !!viz || isUndefined(viz);
154
+ if (vizState?.loading || !viz || isUndefined(viz)) {
155
+ return false;
156
+ }
157
+ return true;
156
158
  };
157
159
 
158
160
  export const getDevice = (breakpoints, width) => {
@@ -0,0 +1,37 @@
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
+
6
+ import VisualizationView from './VisualizationView';
7
+
8
+ jest.doMock('@plone/volto/registry', {
9
+ blocks: {
10
+ blocksConfig: {
11
+ embed_tableau_visualization: {
12
+ breakpoints: {
13
+ desktop: [Infinity, 992],
14
+ tablet: [991, 768],
15
+ phone: [767, 0],
16
+ },
17
+ },
18
+ },
19
+ },
20
+ });
21
+
22
+ describe('VisualizationViewWidget', () => {
23
+ it('should render the component', () => {
24
+ const { container } = render(
25
+ <Provider store={{ ...global.store }}>
26
+ <VisualizationView
27
+ content={{
28
+ tableau_visualization: {
29
+ url: 'http://localhost:3000/tableau-ct',
30
+ },
31
+ }}
32
+ />
33
+ </Provider>,
34
+ );
35
+ expect(container.querySelector('.tableau-wrapper')).toBeInTheDocument();
36
+ });
37
+ });
@@ -35,6 +35,7 @@ const VisualizationWidget = (props) => {
35
35
  const ogValue = props.value || {};
36
36
  const inAddForm = props.location.pathname.split('/').pop() === 'add';
37
37
  const viz = React.useRef();
38
+ const [tableauData, setTableauData] = React.useState({});
38
39
  const [schema, setSchema] = React.useState(null);
39
40
  const [vizState, setVizState] = React.useState({
40
41
  loaded: false,
@@ -76,6 +77,33 @@ const VisualizationWidget = (props) => {
76
77
  setOpen(false);
77
78
  };
78
79
 
80
+ React.useEffect(() => {
81
+ if (!vizState.loaded || !viz.current || !!tableauData[value.url]) return;
82
+
83
+ async function _setTableauData() {
84
+ const workbook = await viz.current.getWorkbook();
85
+ const sheetNames = workbook.getPublishedSheetsInfo().map((sheet) => {
86
+ return sheet.getName();
87
+ });
88
+ const parameters = await workbook.getParametersAsync();
89
+ const filters = await workbook.getActiveSheet().getFiltersAsync();
90
+ setTableauData((tableauData) => ({
91
+ ...tableauData,
92
+ [value.url]: {
93
+ sheetNames,
94
+ parameters,
95
+ filters,
96
+ },
97
+ }));
98
+ }
99
+
100
+ _setTableauData();
101
+ }, [vizState, tableauData, value.url]);
102
+
103
+ /**
104
+ * Synchronizes the local `value` state with the `props.value` prop when the component is not open and the values differ.
105
+ * This ensures that the component's internal state reflects the latest value from the parent component.
106
+ */
79
107
  React.useEffect(() => {
80
108
  if (!open && !isEqual(props.value || {}, value)) {
81
109
  setValue(props.value || {});
@@ -126,16 +154,17 @@ const VisualizationWidget = (props) => {
126
154
  * Get schema
127
155
  */
128
156
  React.useEffect(() => {
129
- getSchema({
130
- config,
131
- viz: viz.current,
132
- vizState,
133
- data: value,
134
- intl: props.intl,
135
- }).then((schema) => {
136
- setSchema(schema);
137
- });
138
- }, [vizState, value, props.intl]);
157
+ setSchema(
158
+ getSchema({
159
+ config,
160
+ viz: viz.current,
161
+ vizState,
162
+ data: value,
163
+ tableauData: tableauData[value.url],
164
+ intl: props.intl,
165
+ }),
166
+ );
167
+ }, [vizState, value, tableauData, props.intl]);
139
168
 
140
169
  React.useEffect(() => {
141
170
  if (value && value.url && value.preview_url_loaded !== value.url) {
@@ -221,8 +250,14 @@ const VisualizationWidget = (props) => {
221
250
  <Grid>
222
251
  <Grid.Row>
223
252
  <div className="map-edit-actions-container">
224
- <Button onClick={handleClose}>Close</Button>
225
- <Button color="green" onClick={handleApplyChanges}>
253
+ <Button disabled={vizState.loading} onClick={handleClose}>
254
+ Close
255
+ </Button>
256
+ <Button
257
+ disabled={vizState.loading}
258
+ color="green"
259
+ onClick={handleApplyChanges}
260
+ >
226
261
  Apply changes
227
262
  </Button>
228
263
  </div>
@@ -1,9 +1,6 @@
1
1
  import { defineMessages } from 'react-intl';
2
- import { find, includes } from 'lodash';
3
- import {
4
- getSheetnamesChoices,
5
- canChangeVizData,
6
- } from '@eeacms/volto-tableau/Tableau/helpers';
2
+ import { find, includes, uniq } from 'lodash';
3
+ import { canChangeVizData } from '@eeacms/volto-tableau/Tableau/helpers';
7
4
 
8
5
  const messages = defineMessages({
9
6
  CSSHeight: {
@@ -16,9 +13,8 @@ const messages = defineMessages({
16
13
  },
17
14
  });
18
15
 
19
- async function getUrlParametersSchema({ viz, vizState, data }) {
20
- const tableauParameters =
21
- vizState.loaded && viz ? await viz.getWorkbook().getParametersAsync() : [];
16
+ function getUrlParametersSchema({ data, tableauData }) {
17
+ const tableauParameters = tableauData?.parameters || [];
22
18
 
23
19
  const currentFields = (data.urlParameters || [])
24
20
  .map((p) => p.field)
@@ -51,11 +47,8 @@ async function getUrlParametersSchema({ viz, vizState, data }) {
51
47
  };
52
48
  }
53
49
 
54
- async function getStaticParametersSchema({ viz, vizState, data }) {
55
- const tableauParameters =
56
- vizState.loaded && viz
57
- ? (await viz.getWorkbook?.().getParametersAsync?.()) || []
58
- : [];
50
+ function getStaticParametersSchema({ data, tableauData }) {
51
+ const tableauParameters = tableauData?.parameters || [];
59
52
 
60
53
  const currentFields = (data.staticParameters || [])
61
54
  .map((p) => p.field)
@@ -86,11 +79,8 @@ async function getStaticParametersSchema({ viz, vizState, data }) {
86
79
  };
87
80
  }
88
81
 
89
- async function getDynamicFiltersSchema({ viz, vizState, data }) {
90
- const tableauFilters =
91
- vizState.loaded && viz
92
- ? (await viz.getWorkbook?.().getActiveSheet?.().getFiltersAsync?.()) || []
93
- : [];
82
+ function getDynamicFiltersSchema({ data, tableauData }) {
83
+ const tableauFilters = tableauData?.filters || [];
94
84
 
95
85
  const currentFields = (data.staticFilters || [])
96
86
  .map((p) => p.field)
@@ -123,11 +113,8 @@ async function getDynamicFiltersSchema({ viz, vizState, data }) {
123
113
  };
124
114
  }
125
115
 
126
- async function getStaticFiltersSchema({ viz, vizState, data }) {
127
- const tableauFilters =
128
- vizState.loaded && viz
129
- ? (await viz.getWorkbook?.().getActiveSheet?.().getFiltersAsync?.()) || []
130
- : [];
116
+ function getStaticFiltersSchema({ data, tableauData }) {
117
+ const tableauFilters = tableauData?.filters || [];
131
118
 
132
119
  const currentFields = (data.staticFilters || [])
133
120
  .map((p) => p.field)
@@ -142,11 +129,13 @@ async function getStaticFiltersSchema({ viz, vizState, data }) {
142
129
  widget: 'creatable_select',
143
130
  isMulti: false,
144
131
  creatable: true,
145
- choices: tableauFilters
146
- .filter((p) => {
147
- return !includes(currentFields, p.getFieldName());
148
- })
149
- .map((p) => [p.getFieldName(), p.getFieldName()]),
132
+ choices: uniq(
133
+ tableauFilters
134
+ .filter((f) => {
135
+ return !includes(currentFields, f.getFieldName());
136
+ })
137
+ .map((f) => f.getFieldName()),
138
+ ).map((f) => [f, f]),
150
139
  },
151
140
  value: {
152
141
  title: 'Value',
@@ -182,7 +171,7 @@ const breakpointUrlSchema = (config) => {
182
171
  };
183
172
  };
184
173
 
185
- const schema = async ({ config, viz, vizState, data, intl }) => {
174
+ const schema = ({ config, viz, vizState, data, tableauData, intl }) => {
186
175
  const isDisabled = !canChangeVizData(viz, vizState);
187
176
 
188
177
  return {
@@ -224,7 +213,7 @@ const schema = async ({ config, viz, vizState, data, intl }) => {
224
213
  },
225
214
  sheetname: {
226
215
  title: 'Sheetname',
227
- choices: getSheetnamesChoices(viz),
216
+ choices: tableauData?.sheetNames?.map((sheet) => [sheet, sheet]) || [],
228
217
  isDisabled,
229
218
  },
230
219
  hideTabs: {
@@ -267,7 +256,7 @@ const schema = async ({ config, viz, vizState, data, intl }) => {
267
256
  urlParameters: {
268
257
  title: 'Dynamic parameters',
269
258
  widget: 'object_list',
270
- schema: await getUrlParametersSchema({ viz, vizState, data }),
259
+ schema: getUrlParametersSchema({ data, tableauData }),
271
260
  description: (
272
261
  <div style={{ color: 'rgb(15, 130, 204)', fontWeight: '400' }}>
273
262
  <p>Set a list of dynamic parameters that can be used as:</p>
@@ -313,7 +302,7 @@ const schema = async ({ config, viz, vizState, data, intl }) => {
313
302
  staticParameters: {
314
303
  title: 'Static parameters',
315
304
  widget: 'object_list',
316
- schema: await getStaticParametersSchema({ viz, vizState, data }),
305
+ schema: getStaticParametersSchema({ data, tableauData }),
317
306
  schemaExtender: (schema, data) => {
318
307
  const tableauParameter = find(
319
308
  schema.tableauParameters,
@@ -360,7 +349,7 @@ const schema = async ({ config, viz, vizState, data, intl }) => {
360
349
  dynamicFilters: {
361
350
  title: 'Dynamic filters',
362
351
  widget: 'object_list',
363
- schema: await getDynamicFiltersSchema({ viz, vizState, data }),
352
+ schema: getDynamicFiltersSchema({ data, tableauData }),
364
353
  description: (
365
354
  <div style={{ color: 'rgb(15, 130, 204)', fontWeight: '400' }}>
366
355
  <p>Set a list of dynamic filters that can be used as:</p>
@@ -406,7 +395,7 @@ const schema = async ({ config, viz, vizState, data, intl }) => {
406
395
  staticFilters: {
407
396
  title: 'Static filters',
408
397
  widget: 'object_list',
409
- schema: await getStaticFiltersSchema({ viz, vizState, data }),
398
+ schema: getStaticFiltersSchema({ data, tableauData }),
410
399
  schemaExtender: (schema, data) => {
411
400
  const tableauFilters = find(
412
401
  schema.tableauFilters,