@eeacms/volto-tableau 7.0.5 → 7.2.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.
@@ -1,33 +1,46 @@
1
- import React, { useEffect } from 'react';
2
- import { Message } from 'semantic-ui-react';
3
- import { flattenToAppURL } from '@plone/volto/helpers';
1
+ import React, { useEffect, useState } from 'react';
2
+ import { withRouter } from 'react-router';
4
3
  import { connect } from 'react-redux';
5
4
  import { compose } from 'redux';
5
+ import { isFunction } from 'lodash';
6
+ import { Message } from 'semantic-ui-react';
7
+ import { flattenToAppURL } from '@plone/volto/helpers';
6
8
  import { getContent } from '@plone/volto/actions';
7
9
  import PrivacyProtection from '@eeacms/volto-embed/PrivacyProtection/PrivacyProtection';
8
- import { pickMetadata } from '@eeacms/volto-embed/helpers';
9
10
  import Tableau from '@eeacms/volto-tableau/Tableau/Tableau';
11
+ import {
12
+ getQuery,
13
+ getTableauVisualization,
14
+ getParameters,
15
+ getFilters,
16
+ } from '@eeacms/volto-tableau/Tableau/helpers';
17
+
18
+ const timer = {};
10
19
 
11
- function getTableauVisualization(props) {
12
- const { isBlock } = props;
13
- const content = (isBlock ? props.tableauContent : props.content) || {};
14
- const tableau_visualization =
15
- (isBlock
16
- ? props.tableauContent?.tableau_visualization
17
- : props.content?.tableau_visualization) ||
18
- props.data.tableau_visualization ||
19
- {};
20
- return {
21
- ...pickMetadata(content),
22
- ...tableau_visualization,
23
- };
20
+ function debounce(func, wait = 500, id) {
21
+ if (!isFunction(func)) return;
22
+ const name = id || func.name || 'generic';
23
+ if (timer[name]) clearTimeout(timer[name]);
24
+ timer[name] = setTimeout(func, wait);
24
25
  }
25
26
 
26
27
  const View = (props) => {
27
- const { isBlock, id, mode, data, getContent, useVisibilitySensor } = props;
28
+ const {
29
+ isBlock,
30
+ id,
31
+ location,
32
+ mode,
33
+ data,
34
+ getContent,
35
+ useVisibilitySensor,
36
+ data_query,
37
+ discodata_query,
38
+ tableau_vis_url,
39
+ content,
40
+ tableauContent,
41
+ } = props;
28
42
  const {
29
43
  with_notes = true,
30
- with_sources = true,
31
44
  with_more_info = true,
32
45
  with_download = true,
33
46
  with_share = true,
@@ -35,59 +48,135 @@ const View = (props) => {
35
48
  tableau_height,
36
49
  } = data;
37
50
 
38
- const tableau_vis_url = flattenToAppURL(data.tableau_vis_url || '');
51
+ const [tableauVisualization, setTableauVisualization] = useState(() =>
52
+ getTableauVisualization({
53
+ isBlock,
54
+ data,
55
+ content,
56
+ tableauContent,
57
+ }),
58
+ );
59
+ const [query, setQuery] = useState(() => {
60
+ return getQuery({ data_query, location, tableau_vis_url, discodata_query });
61
+ });
62
+
63
+ const [extraParameters, setExtraParameters] = useState(() =>
64
+ getParameters({ tableauVisualization, query, data }),
65
+ );
66
+ const [extraFilters, setExtraFilters] = useState(() =>
67
+ getFilters({ tableauVisualization, query, data }),
68
+ );
69
+
70
+ /**
71
+ * Update tableau visualization
72
+ */
73
+ useEffect(() => {
74
+ setTableauVisualization(
75
+ getTableauVisualization({
76
+ isBlock,
77
+ data,
78
+ content,
79
+ tableauContent,
80
+ }),
81
+ );
82
+ }, [isBlock, data, content, tableauContent]);
39
83
 
40
- const tableau_visualization = getTableauVisualization(props);
84
+ /**
85
+ * Update query
86
+ */
87
+ useEffect(() => {
88
+ setQuery(
89
+ getQuery({ data_query, location, tableau_vis_url, discodata_query }),
90
+ );
91
+ }, [tableau_vis_url, data_query, discodata_query, location]);
41
92
 
42
- const { staticParameters = [] } = tableau_visualization;
93
+ /**
94
+ * Update extra parameters
95
+ */
96
+ useEffect(() => {
97
+ debounce(
98
+ () => {
99
+ setExtraParameters(
100
+ getParameters({
101
+ tableauVisualization,
102
+ query,
103
+ data,
104
+ }),
105
+ );
106
+ },
107
+ 500,
108
+ 'setExtraParameters',
109
+ );
110
+ }, [tableauVisualization, query, data]);
43
111
 
44
- const extraOptions = React.useMemo(() => {
45
- const options = {};
46
- staticParameters.forEach((parameter) => {
47
- if (parameter.field && parameter.value) {
48
- options[parameter.field] = parameter.value;
49
- }
50
- });
51
- return options;
52
- }, [staticParameters]);
112
+ /**
113
+ * Update extra filters
114
+ */
115
+ useEffect(() => {
116
+ debounce(
117
+ () => {
118
+ setExtraFilters(
119
+ getFilters({
120
+ tableauVisualization,
121
+ query,
122
+ data,
123
+ }),
124
+ );
125
+ },
126
+ 500,
127
+ 'setExtraFilters',
128
+ );
129
+ }, [tableauVisualization, query, data]);
53
130
 
54
131
  useEffect(() => {
55
- const tableauVisId = flattenToAppURL(tableau_visualization['@id'] || '');
132
+ /**
133
+ * If we are in edit mode, we need to fetch the content of the
134
+ * tableau visualization
135
+ */
136
+ const tableauVisId = flattenToAppURL(tableauVisualization['@id'] || '');
56
137
  if (
57
- isBlock &&
58
138
  mode === 'edit' &&
139
+ !tableauVisualization.error &&
140
+ isBlock &&
59
141
  tableau_vis_url &&
60
142
  tableau_vis_url !== tableauVisId
61
143
  ) {
62
144
  getContent(tableau_vis_url, null, id);
63
145
  }
64
- }, [id, isBlock, getContent, mode, tableau_vis_url, tableau_visualization]);
146
+ }, [id, isBlock, getContent, mode, tableau_vis_url, tableauVisualization]);
65
147
 
66
148
  if (props.mode === 'edit' && !tableau_vis_url) {
67
149
  return <Message>Please select a tableau from block editor.</Message>;
68
150
  }
69
151
 
152
+ if (tableauVisualization?.error) {
153
+ return (
154
+ <p dangerouslySetInnerHTML={{ __html: tableauVisualization.error }} />
155
+ );
156
+ }
157
+
70
158
  return (
71
159
  <div className="embed-tableau">
72
160
  <PrivacyProtection
73
161
  {...props}
74
- data={{ ...data, url: tableau_visualization?.url }}
162
+ data={{ ...data, url: tableauVisualization?.url }}
75
163
  useVisibilitySensor={useVisibilitySensor}
76
164
  >
77
165
  <Tableau
78
166
  data={{
79
- ...tableau_visualization,
167
+ ...tableauVisualization,
80
168
  tableau_height:
81
- tableau_height || tableau_visualization.tableau_height,
169
+ tableau_height || tableauVisualization.tableau_height,
82
170
  with_notes,
83
- with_sources,
171
+ with_sources: true,
84
172
  with_more_info,
85
173
  with_download,
86
174
  with_share,
87
175
  with_enlarge,
88
176
  tableau_vis_url,
89
177
  }}
90
- extraOptions={extraOptions}
178
+ extraParameters={extraParameters}
179
+ extraFilters={extraFilters}
91
180
  />
92
181
  </PrivacyProtection>
93
182
  </div>
@@ -95,11 +184,19 @@ const View = (props) => {
95
184
  };
96
185
 
97
186
  export default compose(
187
+ withRouter,
98
188
  connect(
99
- (state, props) => ({
100
- tableauContent: state.content.subrequests?.[props.id]?.data,
101
- isBlock: !!props.data?.['@type'],
102
- }),
189
+ (state, props) => {
190
+ const tableau_vis_url = flattenToAppURL(props.data.tableau_vis_url || '');
191
+ const pathname = flattenToAppURL(state.content.data['@id']);
192
+ return {
193
+ tableauContent: state.content?.subrequests?.[props.id]?.data,
194
+ discodata_query: state.discodata_query,
195
+ data_query: state.connected_data_parameters.byContextPath[pathname],
196
+ isBlock: !!props.data?.['@type'],
197
+ tableau_vis_url,
198
+ };
199
+ },
103
200
  {
104
201
  getContent,
105
202
  },
@@ -50,12 +50,31 @@ Array [
50
50
  className="ui segment form attached"
51
51
  >
52
52
  <div
53
- className="mocked-url-widget"
53
+ className="mocked-default-widget"
54
54
  id="mocked-field-tableau_vis_url"
55
55
  >
56
56
  Tableau visualization
57
57
  -
58
- No description
58
+ <div>
59
+ <p>
60
+ When using context query parameters please use the corresponding field name from the Tableau service.
61
+ </p>
62
+ <p>
63
+ NOTE: The embeded tableau dashboard must have the parameters defined in the
64
+
65
+ <span
66
+ style={
67
+ Object {
68
+ "color": "rgb(15, 130, 204)",
69
+ }
70
+ }
71
+ >
72
+ 'Dynamic parameters'
73
+ </span>
74
+
75
+ list so that the context query parameters can take effect.
76
+ </p>
77
+ </div>
59
78
  </div>
60
79
  <div
61
80
  className="mocked-default-widget"
@@ -135,14 +154,6 @@ Array [
135
154
  -
136
155
  No description
137
156
  </div>
138
- <div
139
- className="mocked-boolean-widget"
140
- id="mocked-field-with_sources"
141
- >
142
- Show sources
143
- -
144
- Will show sources set in this page Data provenance
145
- </div>
146
157
  <div
147
158
  className="mocked-boolean-widget"
148
159
  id="mocked-field-with_more_info"
@@ -181,6 +192,75 @@ Array [
181
192
  </div>
182
193
  </div>
183
194
  </div>
195
+ <div
196
+ className="accordion ui fluid styled form"
197
+ >
198
+ <div
199
+ id="blockform-fieldset-options"
200
+ >
201
+ <div
202
+ className="title"
203
+ onClick={[Function]}
204
+ >
205
+ Parameters
206
+ <svg
207
+ className="icon"
208
+ dangerouslySetInnerHTML={
209
+ Object {
210
+ "__html": undefined,
211
+ }
212
+ }
213
+ onClick={null}
214
+ style={
215
+ Object {
216
+ "fill": "currentColor",
217
+ "height": "20px",
218
+ "width": "auto",
219
+ }
220
+ }
221
+ viewBox=""
222
+ xmlns=""
223
+ />
224
+ </div>
225
+ <div
226
+ className="content"
227
+ >
228
+ <div
229
+ aria-hidden={true}
230
+ className="rah-static rah-static--height-zero"
231
+ style={
232
+ Object {
233
+ "height": 0,
234
+ "overflow": "hidden",
235
+ }
236
+ }
237
+ >
238
+ <div
239
+ style={
240
+ Object {
241
+ "WebkitTransition": "opacity 500ms ease 0ms",
242
+ "opacity": 0,
243
+ "transition": "opacity 500ms ease 0ms",
244
+ }
245
+ }
246
+ >
247
+ <div
248
+ className="ui segment attached"
249
+ >
250
+ <div
251
+ className="mocked-default-widget"
252
+ id="mocked-field-staticParameters"
253
+ >
254
+ Static parameters
255
+ -
256
+ Set a list of static parameters.
257
+ </div>
258
+ </div>
259
+ </div>
260
+ </div>
261
+ </div>
262
+ </div>
263
+ </div>
184
264
  <div
185
265
  className="accordion ui fluid styled form"
186
266
  >
@@ -53,6 +53,22 @@ const getProtectionSchema = () => ({
53
53
  required: [],
54
54
  });
55
55
 
56
+ const staticParameters = {
57
+ title: 'Parameter',
58
+ fieldsets: [{ id: 'default', title: 'Default', fields: ['field', 'value'] }],
59
+ properties: {
60
+ field: {
61
+ title: 'Tableau fieldname',
62
+ type: 'text',
63
+ },
64
+ value: {
65
+ title: 'Value',
66
+ type: 'text',
67
+ },
68
+ },
69
+ required: [],
70
+ };
71
+
56
72
  export default (props) => {
57
73
  return {
58
74
  title: 'Embed Dashboard (Tableau)',
@@ -67,13 +83,17 @@ export default (props) => {
67
83
  title: 'Toolbar',
68
84
  fields: [
69
85
  'with_notes',
70
- 'with_sources',
71
86
  'with_more_info',
72
87
  'with_download',
73
88
  'with_share',
74
89
  'with_enlarge',
75
90
  ],
76
91
  },
92
+ {
93
+ id: 'options',
94
+ title: 'Parameters',
95
+ fields: ['staticParameters'],
96
+ },
77
97
  {
78
98
  id: 'privacy',
79
99
  title: 'Privacy',
@@ -83,7 +103,23 @@ export default (props) => {
83
103
  properties: {
84
104
  tableau_vis_url: {
85
105
  title: 'Tableau visualization',
86
- widget: 'url',
106
+ widget: 'internal_url',
107
+ description: (
108
+ <div>
109
+ <p>
110
+ When using context query parameters please use the corresponding
111
+ field name from the Tableau service.
112
+ </p>
113
+ <p>
114
+ NOTE: The embeded tableau dashboard must have the parameters
115
+ defined in the{' '}
116
+ <span style={{ color: 'rgb(15, 130, 204)' }}>
117
+ 'Dynamic parameters'
118
+ </span>{' '}
119
+ list so that the context query parameters can take effect.
120
+ </p>
121
+ </div>
122
+ ),
87
123
  },
88
124
  with_notes: {
89
125
  title: 'Show note',
@@ -134,6 +170,12 @@ export default (props) => {
134
170
  widget: 'object',
135
171
  schema: getProtectionSchema(),
136
172
  },
173
+ staticParameters: {
174
+ title: 'Static parameters',
175
+ widget: 'object_list',
176
+ schema: staticParameters,
177
+ description: 'Set a list of static parameters.',
178
+ },
137
179
  },
138
180
 
139
181
  required: ['tableau_vis_url'],
@@ -9,7 +9,22 @@ import React, {
9
9
  } from 'react';
10
10
  import { connect } from 'react-redux';
11
11
  import { toast } from 'react-toastify';
12
- import { isEqual, isUndefined, isNaN, isNumber } from 'lodash';
12
+ import {
13
+ isEqual,
14
+ isUndefined,
15
+ isNaN,
16
+ isNumber,
17
+ forOwn,
18
+ find,
19
+ includes,
20
+ isArray,
21
+ isString,
22
+ isInteger,
23
+ isBoolean,
24
+ toString,
25
+ toInteger,
26
+ toNumber,
27
+ } from 'lodash';
13
28
  import cx from 'classnames';
14
29
  import { Button } from 'semantic-ui-react';
15
30
  import { Toast, Icon } from '@plone/volto/components';
@@ -48,7 +63,9 @@ const TableauDebug = ({ mode, data, vizState, url, version, clearData }) => {
48
63
  return (
49
64
  <div className="tableau-debug">
50
65
  {!url && !vizState.error && <p className="tableau-error">URL required</p>}
51
- {vizState.error && <p className="tableau-error">{vizState.error}</p>}
66
+ {isString(vizState.error) && (
67
+ <p className="tableau-error">{vizState.error}</p>
68
+ )}
52
69
  {vizState.loaded && url && (
53
70
  <h3 className="tableau-version">
54
71
  Tableau <span className="version">{version}</span>
@@ -93,6 +110,7 @@ const Tableau = forwardRef((props, ref) => {
93
110
  data = {},
94
111
  breakpoints = {},
95
112
  extraFilters = {},
113
+ extraParameters = {},
96
114
  extraOptions = {},
97
115
  mode = 'view',
98
116
  screen = {},
@@ -100,6 +118,7 @@ const Tableau = forwardRef((props, ref) => {
100
118
  setVizState,
101
119
  onChangeBlock,
102
120
  } = props;
121
+
103
122
  const {
104
123
  data_provenance = {},
105
124
  figure_note = [],
@@ -244,10 +263,10 @@ const Tableau = forwardRef((props, ref) => {
244
263
  hideToolbar,
245
264
  toolbarPosition,
246
265
  device: !!breakpointUrl ? device : 'desktop',
266
+ ...extraOptions,
247
267
  ...data.filters,
248
- ...data.parameters,
249
268
  ...extraFilters,
250
- ...extraOptions,
269
+ ...extraParameters,
251
270
  onFirstInteractive: () => {
252
271
  onVizStateUpdate(true, true, null);
253
272
  setInitiateViz(false);
@@ -296,32 +315,11 @@ const Tableau = forwardRef((props, ref) => {
296
315
  },
297
316
  });
298
317
  } catch (e) {
299
- onVizStateUpdate(false, false, e.get_message());
318
+ onVizStateUpdate(false, false, e?.get_message?.() || e);
300
319
  setInitiateViz(false);
301
320
  }
302
321
  };
303
322
 
304
- const addExtraFilters = (extraFilters) => {
305
- const worksheets =
306
- viz.current.getWorkbook().getActiveSheet().getWorksheets() || [];
307
-
308
- worksheets.forEach((worksheet) => {
309
- if (worksheet.getSheetType() === tableau.DashboardObjectType.WORKSHEET) {
310
- Object.keys(extraFilters).forEach((filter) => {
311
- if (!extraFilters[filter]) {
312
- worksheet.clearFilterAsync(filter);
313
- } else {
314
- worksheet.applyFilterAsync(
315
- filter,
316
- extraFilters[filter],
317
- tableau.FilterUpdateType.REPLACE,
318
- );
319
- }
320
- });
321
- }
322
- });
323
- };
324
-
325
323
  const updateScale = () => {
326
324
  const iframe = vizEl.current.querySelector('iframe');
327
325
  const { sheetSize = {} } = viz.current.getVizSize() || {};
@@ -358,12 +356,99 @@ const Tableau = forwardRef((props, ref) => {
358
356
  /* eslint-disable-next-line */
359
357
  }, [loaded, loading, initiateViz]);
360
358
 
359
+ /**
360
+ * TODO: make this work
361
+ */
362
+ // useEffect(() => {
363
+ // async function addExtraFilters() {
364
+ // if (vizState.current.loaded && viz.current) {
365
+ // const dashboard = viz.current.getWorkbook().getActiveSheet();
366
+ // const tableauFilters = await dashboard.getFiltersAsync();
367
+
368
+ // forOwn(extraFilters, (value, fieldName) => {
369
+ // const tableauFilter = find(
370
+ // tableauFilters,
371
+ // (f) => f.getFieldName() === fieldName,
372
+ // );
373
+ // if (!tableauFilter) return;
374
+ // if (!value) {
375
+ // tableauFilter.getWorksheet().clearFilterAsync(fieldName);
376
+ // return;
377
+ // }
378
+ // const filterType = tableauFilter.getFilterType();
379
+ // if (filterType === 'categorical') {
380
+ // if (!isArray(value)) {
381
+ // value = [value];
382
+ // }
383
+ // dashboard.applyFilterAsync(
384
+ // fieldName,
385
+ // value,
386
+ // tableau.FilterUpdateType.REPLACE,
387
+ // );
388
+ // }
389
+ // /**
390
+ // * TODO: handle other filter types
391
+ // */
392
+ // });
393
+ // }
394
+ // }
395
+ // addExtraFilters();
396
+ // /* eslint-disable-next-line */
397
+ // }, [loaded, JSON.stringify(extraFilters)]);
398
+
361
399
  useEffect(() => {
362
- if (vizState.current.loaded && viz.current) {
363
- addExtraFilters(extraFilters);
400
+ async function addExtraParameters() {
401
+ if (vizState.current.loaded && viz.current) {
402
+ const workbook = viz.current.getWorkbook();
403
+ const tableauParameters = await workbook.getParametersAsync();
404
+ forOwn(extraParameters, (value, fieldName) => {
405
+ const tableauParameter = find(
406
+ tableauParameters,
407
+ (p) => p.getName() === fieldName,
408
+ );
409
+ if (!tableauParameter || !value) return;
410
+ const allowableValuesType = tableauParameter.getAllowableValuesType();
411
+ const dataType = tableauParameter.getDataType();
412
+ if (includes(['all', 'list'], allowableValuesType)) {
413
+ const values = tableauParameter
414
+ .getAllowableValues()
415
+ ?.map((v) => v.value);
416
+ if (!isArray(value)) {
417
+ value = [value];
418
+ }
419
+ value = value
420
+ .filter((v) => includes(values, v))
421
+ .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;
435
+ });
436
+ if (value.length) {
437
+ workbook.changeParameterValueAsync(fieldName, value);
438
+ }
439
+ }
440
+ if (allowableValuesType === 'range' && value) {
441
+ /**
442
+ * TODO: handle range parameters
443
+ */
444
+ workbook.changeParameterValueAsync(fieldName, value);
445
+ }
446
+ });
447
+ }
364
448
  }
449
+ addExtraParameters();
365
450
  /* eslint-disable-next-line */
366
- }, [JSON.stringify(extraFilters)]);
451
+ }, [loaded, JSON.stringify(extraParameters)]);
367
452
 
368
453
  useEffect(() => {
369
454
  if (vizState.current.loaded && viz.current && autoScale) {