@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,14 +1,26 @@
1
- import React from 'react';
1
+ import React, { useState } from 'react';
2
+ import { withRouter } from 'react-router';
3
+ import { connect } from 'react-redux';
4
+ import { compose } from 'redux';
5
+ import { isEqual } from 'lodash';
2
6
  import { Modal, Button, Grid } from 'semantic-ui-react';
3
7
  import config from '@plone/volto/registry';
4
8
  import { FormFieldWrapper, InlineForm } from '@plone/volto/components';
5
9
  import Tableau from '@eeacms/volto-tableau/Tableau/Tableau';
6
10
  import getSchema from './schema';
11
+ import {
12
+ getQuery,
13
+ getTableauVisualization,
14
+ getParameters,
15
+ getFilters,
16
+ } from '@eeacms/volto-tableau/Tableau/helpers';
7
17
 
8
18
  import '@eeacms/volto-tableau/less/tableau.less';
9
19
 
10
20
  const VisualizationWidget = (props) => {
21
+ const { location, content } = props;
11
22
  const viz = React.useRef();
23
+ const [schema, setSchema] = React.useState(null);
12
24
  const [vizState, setVizState] = React.useState({
13
25
  loaded: false,
14
26
  loading: false,
@@ -17,19 +29,27 @@ const VisualizationWidget = (props) => {
17
29
  const [open, setOpen] = React.useState(false);
18
30
  const [value, setValue] = React.useState(props.value);
19
31
 
20
- const schema = React.useMemo(() => getSchema(config, viz.current, vizState), [
21
- vizState,
22
- ]);
32
+ const [tableauVisualization, setTableauVisualization] = useState(() =>
33
+ getTableauVisualization({
34
+ isBlock: false,
35
+ content: {
36
+ ...content,
37
+ tableau_visualization: value,
38
+ },
39
+ }),
40
+ );
41
+
42
+ const [query, setQuery] = useState(() => {
43
+ return getQuery({ location });
44
+ });
23
45
 
24
- const extraOptions = React.useMemo(() => {
25
- const options = {};
26
- (value.staticParameters || []).forEach((parameter) => {
27
- if (parameter.field && parameter.value) {
28
- options[parameter.field] = parameter.value;
29
- }
30
- });
31
- return options;
32
- }, [value]);
46
+ const [extraParameters, setExtraParameters] = useState(() =>
47
+ getParameters({ tableauVisualization, query }),
48
+ );
49
+
50
+ const [extraFilters, setExtraFilters] = useState(() =>
51
+ getFilters({ tableauVisualization, query }),
52
+ );
33
53
 
34
54
  const handleApplyChanges = () => {
35
55
  props.onChange(props.id, value);
@@ -41,6 +61,63 @@ const VisualizationWidget = (props) => {
41
61
  setOpen(false);
42
62
  };
43
63
 
64
+ React.useEffect(() => {
65
+ if (!open && !isEqual(props.value, value)) {
66
+ setValue(props.value);
67
+ }
68
+ }, [props.value, value, open]);
69
+
70
+ /**
71
+ * Update tableau visualization
72
+ */
73
+ React.useEffect(() => {
74
+ setTableauVisualization(
75
+ getTableauVisualization({
76
+ isBlock: false,
77
+ content: {
78
+ ...content,
79
+ tableau_visualization: value,
80
+ },
81
+ }),
82
+ );
83
+ }, [content, value]);
84
+
85
+ /**
86
+ * Update query
87
+ */
88
+ React.useEffect(() => {
89
+ setQuery(
90
+ getQuery({
91
+ location,
92
+ }),
93
+ );
94
+ }, [location]);
95
+
96
+ /**
97
+ * Update extra parameters
98
+ */
99
+ React.useEffect(() => {
100
+ setExtraParameters(getParameters({ tableauVisualization, query }));
101
+ }, [tableauVisualization, query]);
102
+
103
+ /**
104
+ * Update extra filters
105
+ */
106
+ React.useEffect(() => {
107
+ setExtraFilters(getFilters({ tableauVisualization, query }));
108
+ }, [tableauVisualization, query]);
109
+
110
+ /**
111
+ * Get schema
112
+ */
113
+ React.useEffect(() => {
114
+ getSchema({ config, viz: viz.current, vizState, data: value }).then(
115
+ (schema) => {
116
+ setSchema(schema);
117
+ },
118
+ );
119
+ }, [vizState, value]);
120
+
44
121
  return (
45
122
  <FormFieldWrapper {...props}>
46
123
  <Modal id="tableau-editor-modal" open={open}>
@@ -52,17 +129,19 @@ const VisualizationWidget = (props) => {
52
129
  computer={4}
53
130
  className="tableau-editor-column"
54
131
  >
55
- <InlineForm
56
- block={props.block}
57
- schema={schema}
58
- onChangeField={(id, fieldValue) => {
59
- setValue((value) => ({
60
- ...value,
61
- [id]: fieldValue,
62
- }));
63
- }}
64
- formData={value}
65
- />
132
+ {schema && (
133
+ <InlineForm
134
+ block={props.block}
135
+ schema={schema}
136
+ onChangeField={(id, fieldValue) => {
137
+ setValue((value) => ({
138
+ ...value,
139
+ [id]: fieldValue,
140
+ }));
141
+ }}
142
+ formData={value}
143
+ />
144
+ )}
66
145
  </Grid.Column>
67
146
  <Grid.Column
68
147
  mobile={8}
@@ -73,7 +152,7 @@ const VisualizationWidget = (props) => {
73
152
  <Tableau
74
153
  ref={viz}
75
154
  data={{
76
- ...(value || {}),
155
+ ...tableauVisualization,
77
156
  with_notes: false,
78
157
  with_sources: false,
79
158
  with_more_info: false,
@@ -86,7 +165,8 @@ const VisualizationWidget = (props) => {
86
165
  config.blocks.blocksConfig?.embed_tableau_visualization
87
166
  ?.breakpoints
88
167
  }
89
- extraOptions={extraOptions}
168
+ extraParameters={extraParameters}
169
+ extraFilters={extraFilters}
90
170
  setVizState={setVizState}
91
171
  onChangeBlock={(_, newValue) => {
92
172
  setValue(newValue);
@@ -122,7 +202,7 @@ const VisualizationWidget = (props) => {
122
202
  </div>
123
203
  <Tableau
124
204
  data={{
125
- ...props.value,
205
+ ...tableauVisualization,
126
206
  autoScale: true,
127
207
  with_notes: false,
128
208
  with_sources: false,
@@ -134,10 +214,14 @@ const VisualizationWidget = (props) => {
134
214
  breakpoints={
135
215
  config.blocks.blocksConfig?.embed_tableau_visualization?.breakpoints
136
216
  }
137
- extraOptions={extraOptions}
217
+ extraParameters={extraParameters}
218
+ extraFilters={extraFilters}
138
219
  />
139
220
  </FormFieldWrapper>
140
221
  );
141
222
  };
142
223
 
143
- export default React.memo(VisualizationWidget);
224
+ export default compose(
225
+ withRouter,
226
+ connect((state) => ({ content: state?.content?.data })),
227
+ )(VisualizationWidget);
@@ -26,7 +26,7 @@ describe('VisualizationWidget', () => {
26
26
 
27
27
  const { container } = render(
28
28
  <Provider store={global.store}>
29
- <VisualizationWidget {...data} />
29
+ <VisualizationWidget {...data} id={'1234'} title="Title" />
30
30
  </Provider>,
31
31
  );
32
32
  expect(container.querySelector('.tableau-wrapper')).toBeInTheDocument();
@@ -1,4 +1,5 @@
1
1
  import VisualizationWidget from './VisualizationWidget';
2
2
  import VisualizationViewWidget from './VisualizationViewWidget';
3
+ import CreatableSelectWidget from './CreatableSelectWidget';
3
4
 
4
- export { VisualizationWidget, VisualizationViewWidget };
5
+ export { VisualizationWidget, VisualizationViewWidget, CreatableSelectWidget };
@@ -1,41 +1,148 @@
1
+ import { find, includes } from 'lodash';
1
2
  import {
2
3
  getSheetnamesChoices,
3
4
  canChangeVizData,
4
5
  } from '@eeacms/volto-tableau/Tableau/helpers';
5
6
 
6
- const urlParametersSchema = {
7
- title: 'Parameter',
8
- fieldsets: [
9
- { id: 'default', title: 'Default', fields: ['field', 'urlParam'] },
10
- ],
11
- properties: {
12
- field: {
13
- title: 'Tableau fieldname',
14
- type: 'text',
7
+ async function getUrlParametersSchema({ viz, vizState, data }) {
8
+ const tableauParameters =
9
+ vizState.loaded && viz ? await viz.getWorkbook().getParametersAsync() : [];
10
+
11
+ const currentFields = (data.urlParameters || [])
12
+ .map((p) => p.field)
13
+ .filter((f) => f);
14
+
15
+ return {
16
+ title: 'Parameter',
17
+ fieldsets: [
18
+ { id: 'default', title: 'Default', fields: ['field', 'urlParam'] },
19
+ ],
20
+ properties: {
21
+ field: {
22
+ title: 'Tableau parameter',
23
+ widget: 'creatable_select',
24
+ isMulti: false,
25
+ creatable: true,
26
+ choices: tableauParameters
27
+ .filter((p) => {
28
+ return !includes(currentFields, p.getName());
29
+ })
30
+ .map((p) => [p.getName(), p.getName()]),
31
+ },
32
+ urlParam: {
33
+ title: 'Field name',
34
+ type: 'text',
35
+ },
15
36
  },
16
- urlParam: {
17
- title: 'URL param',
18
- type: 'text',
37
+ required: [],
38
+ tableauParameters,
39
+ };
40
+ }
41
+
42
+ async function getStaticParametersSchema({ viz, vizState, data }) {
43
+ const tableauParameters =
44
+ vizState.loaded && viz ? await viz.getWorkbook().getParametersAsync() : [];
45
+
46
+ const currentFields = (data.staticParameters || [])
47
+ .map((p) => p.field)
48
+ .filter((f) => f);
49
+
50
+ return {
51
+ title: 'Parameter',
52
+ fieldsets: [{ id: 'default', title: 'Default', fields: ['field'] }],
53
+ properties: {
54
+ field: {
55
+ title: 'Tableau parameter',
56
+ widget: 'creatable_select',
57
+ isMulti: false,
58
+ creatable: true,
59
+ choices: tableauParameters
60
+ .filter((p) => {
61
+ return !includes(currentFields, p.getName());
62
+ })
63
+ .map((p) => [p.getName(), p.getName()]),
64
+ },
65
+ value: {
66
+ title: 'Value',
67
+ type: 'text',
68
+ },
19
69
  },
20
- },
21
- required: [],
22
- };
70
+ required: [],
71
+ tableauParameters,
72
+ };
73
+ }
23
74
 
24
- const staticParameters = {
25
- title: 'Parameter',
26
- fieldsets: [{ id: 'default', title: 'Default', fields: ['field', 'value'] }],
27
- properties: {
28
- field: {
29
- title: 'Tableau fieldname',
30
- type: 'text',
75
+ async function getDynamicFiltersSchema({ viz, vizState, data }) {
76
+ const tableauFilters =
77
+ vizState.loaded && viz
78
+ ? await viz.getWorkbook().getActiveSheet().getFiltersAsync()
79
+ : [];
80
+
81
+ const currentFields = (data.staticFilters || [])
82
+ .map((p) => p.field)
83
+ .filter((f) => f);
84
+
85
+ return {
86
+ title: 'Filter',
87
+ fieldsets: [
88
+ { id: 'default', title: 'Default', fields: ['field', 'urlParam'] },
89
+ ],
90
+ properties: {
91
+ field: {
92
+ title: 'Tableau filter',
93
+ widget: 'creatable_select',
94
+ isMulti: false,
95
+ creatable: true,
96
+ choices: tableauFilters
97
+ .filter((p) => {
98
+ return !includes(currentFields, p.getFieldName());
99
+ })
100
+ .map((p) => [p.getFieldName(), p.getFieldName()]),
101
+ },
102
+ urlParam: {
103
+ title: 'Field name',
104
+ type: 'text',
105
+ },
31
106
  },
32
- value: {
33
- title: 'Value',
34
- type: 'text',
107
+ required: [],
108
+ tableauFilters,
109
+ };
110
+ }
111
+
112
+ async function getStaticFiltersSchema({ viz, vizState, data }) {
113
+ const tableauFilters =
114
+ vizState.loaded && viz
115
+ ? await viz.getWorkbook().getActiveSheet().getFiltersAsync()
116
+ : [];
117
+
118
+ const currentFields = (data.staticFilters || [])
119
+ .map((p) => p.field)
120
+ .filter((f) => f);
121
+
122
+ return {
123
+ title: 'Filter',
124
+ fieldsets: [{ id: 'default', title: 'Default', fields: ['field'] }],
125
+ properties: {
126
+ field: {
127
+ title: 'Tableau filter',
128
+ widget: 'creatable_select',
129
+ isMulti: false,
130
+ creatable: true,
131
+ choices: tableauFilters
132
+ .filter((p) => {
133
+ return !includes(currentFields, p.getFieldName());
134
+ })
135
+ .map((p) => [p.getFieldName(), p.getFieldName()]),
136
+ },
137
+ value: {
138
+ title: 'Value',
139
+ type: 'text',
140
+ },
35
141
  },
36
- },
37
- required: [],
38
- };
142
+ required: [],
143
+ tableauFilters,
144
+ };
145
+ }
39
146
 
40
147
  const breakpointUrlSchema = (config) => {
41
148
  const breakpoints =
@@ -61,7 +168,7 @@ const breakpointUrlSchema = (config) => {
61
168
  };
62
169
  };
63
170
 
64
- export default (config, viz, vizState) => {
171
+ export default async ({ config, viz, vizState, data }) => {
65
172
  const isDisabled = !canChangeVizData(viz, vizState);
66
173
 
67
174
  return {
@@ -81,12 +188,18 @@ export default (config, viz, vizState) => {
81
188
  'hideToolbar',
82
189
  'autoScale',
83
190
  'toolbarPosition',
191
+ 'breakpointUrls',
84
192
  ],
85
193
  },
86
194
  {
87
- id: 'extra_options',
88
- title: 'Extra options',
89
- fields: ['urlParameters', 'staticParameters', 'breakpointUrls'],
195
+ id: 'parameters',
196
+ title: 'Parameters',
197
+ fields: ['urlParameters', 'staticParameters'],
198
+ },
199
+ {
200
+ id: 'filters',
201
+ title: 'Filters',
202
+ fields: ['staticFilters'],
90
203
  },
91
204
  ],
92
205
  properties: {
@@ -125,22 +238,182 @@ export default (config, viz, vizState) => {
125
238
  isDisabled,
126
239
  },
127
240
  urlParameters: {
128
- title: 'URL parameters',
241
+ title: 'Dynamic parameters',
129
242
  widget: 'object_list',
130
- schema: urlParametersSchema,
131
- description: 'Set a list of url parameters to filter the tableau',
243
+ schema: await getUrlParametersSchema({ viz, vizState, data }),
244
+ description: (
245
+ <div style={{ color: 'rgb(15, 130, 204)', fontWeight: '400' }}>
246
+ <p>Set a list of dynamic parameters that can be used as:</p>
247
+ <ul>
248
+ <li style={{ marginBottom: '0.5rem' }}>
249
+ <p style={{ marginBottom: '0' }}>
250
+ URL parameters - when you embed this tableau dashboard you can
251
+ also add query parameters defined bellow.
252
+ </p>
253
+ <p style={{ marginBottom: '0.25rem' }}>
254
+ E.g: When you embed this tableau dashboard you can define the
255
+ url as:
256
+ </p>
257
+ <p>
258
+ <span>
259
+ /path/to/embeded/tableau/visualization?
260
+ <span
261
+ style={{ color: 'rgb(218, 1, 45)' }}
262
+ >{`{field_name}`}</span>
263
+ =Romania
264
+ </span>
265
+ </p>
266
+ </li>
267
+ <li>
268
+ <p style={{ marginBottom: '0' }}>
269
+ Context data query - when you create a content where the
270
+ tableau dashboard will be embeded you can add data query to
271
+ that content.
272
+ </p>
273
+ <p>
274
+ NOTE: the content type should have the{' '}
275
+ <span style={{ color: 'rgb(218, 1, 45)' }}>
276
+ 'Parameters for data connection'
277
+ </span>{' '}
278
+ behavior enabled
279
+ </p>
280
+ </li>
281
+ </ul>
282
+ </div>
283
+ ),
132
284
  isDisabled,
133
285
  },
134
286
  staticParameters: {
135
287
  title: 'Static parameters',
136
288
  widget: 'object_list',
137
- schema: staticParameters,
289
+ schema: await getStaticParametersSchema({ viz, vizState, data }),
290
+ schemaExtender: (schema, data) => {
291
+ const tableauParameter = find(
292
+ schema.tableauParameters,
293
+ (p) => p.getName() === data.field,
294
+ );
295
+
296
+ schema.fieldsets[0].fields = ['field', 'value'];
297
+
298
+ if (!data.field || !tableauParameter) {
299
+ return schema;
300
+ }
301
+
302
+ const allowableValuesType = tableauParameter.getAllowableValuesType();
303
+ const dataType = tableauParameter.getDataType();
304
+
305
+ if (includes(['all', 'list'], allowableValuesType)) {
306
+ schema.properties.value.choices =
307
+ tableauParameter
308
+ .getAllowableValues()
309
+ ?.map((v) => [v.value, v.formattedValue]) || [];
310
+ schema.properties.value.description = 'Select a value';
311
+ }
312
+ if (allowableValuesType === 'all') {
313
+ schema.properties.value.isMulti = true;
314
+ schema.properties.value.description = 'Select multiple values';
315
+ }
316
+ if (allowableValuesType === 'range' && dataType === 'date') {
317
+ schema.properties.value.widget = 'date';
318
+ } else if (allowableValuesType === 'range') {
319
+ schema.properties.value.type = 'number';
320
+ schema.properties.value.step = tableauParameter.getStepSize();
321
+ }
322
+ return schema;
323
+ },
324
+ description: (
325
+ <div style={{ color: 'rgb(15, 130, 204)', fontWeight: '400' }}>
326
+ <p style={{ marginBottom: '0' }}>
327
+ Set a list of static parameters.
328
+ </p>
329
+ </div>
330
+ ),
331
+ isDisabled,
332
+ },
333
+ dynamicFilters: {
334
+ title: 'Dynamic filters',
335
+ widget: 'object_list',
336
+ schema: await getDynamicFiltersSchema({ viz, vizState, data }),
337
+ description: (
338
+ <div style={{ color: 'rgb(15, 130, 204)', fontWeight: '400' }}>
339
+ <p>Set a list of dynamic filters that can be used as:</p>
340
+ <ul>
341
+ <li style={{ marginBottom: '0.5rem' }}>
342
+ <p style={{ marginBottom: '0' }}>
343
+ URL parameters - when you embed this tableau dashboard you can
344
+ also add filters defined bellow as query parameters.
345
+ </p>
346
+ <p style={{ marginBottom: '0.25rem' }}>
347
+ E.g: When you embed this tableau dashboard you can define the
348
+ url as:
349
+ </p>
350
+ <p>
351
+ <span>
352
+ /path/to/embeded/tableau/visualization?
353
+ <span
354
+ style={{ color: 'rgb(218, 1, 45)' }}
355
+ >{`{field_name}`}</span>
356
+ =Agriculture,Forestry and Fishing
357
+ </span>
358
+ </p>
359
+ </li>
360
+ <li>
361
+ <p style={{ marginBottom: '0' }}>
362
+ Context data query - when you create a content where the
363
+ tableau dashboard will be embeded you can add data query to
364
+ that content.
365
+ </p>
366
+ <p>
367
+ NOTE: the content type should have the{' '}
368
+ <span style={{ color: 'rgb(218, 1, 45)' }}>
369
+ 'Parameters for data connection'
370
+ </span>{' '}
371
+ behavior enabled
372
+ </p>
373
+ </li>
374
+ </ul>
375
+ </div>
376
+ ),
377
+ isDisabled,
378
+ },
379
+ staticFilters: {
380
+ title: 'Static filters',
381
+ widget: 'object_list',
382
+ schema: await getStaticFiltersSchema({ viz, vizState, data }),
383
+ schemaExtender: (schema, data) => {
384
+ const tableauFilters = find(
385
+ schema.tableauFilters,
386
+ (p) => p.getFieldName() === data.field,
387
+ );
388
+
389
+ schema.fieldsets[0].fields = ['field', 'value'];
390
+
391
+ if (!data.field || !tableauFilters) {
392
+ return schema;
393
+ }
394
+
395
+ const filterType = tableauFilters.getFilterType();
396
+
397
+ if (includes(['categorical'], filterType)) {
398
+ schema.properties.value.choices =
399
+ tableauFilters
400
+ .getAppliedValues()
401
+ ?.map((v) => [v.value, v.formattedValue]) || [];
402
+ schema.properties.value.isMulti = true;
403
+ schema.fieldsets[0].fields = ['field', 'value'];
404
+ } else {
405
+ schema.properties.field.description = (
406
+ <span style={{ color: 'rgb(218, 1, 45)' }}>
407
+ Filter type ({filterType}) not supported
408
+ </span>
409
+ );
410
+ }
411
+ return schema;
412
+ },
138
413
  description: (
139
- <>
140
- Set a list of static parameters.
141
- <br />
142
- <b>NOTE: You need to trigger a refresh for this to take effect</b>
143
- </>
414
+ <div style={{ color: 'rgb(15, 130, 204)', fontWeight: '400' }}>
415
+ <p style={{ marginBottom: '0' }}>Set a list of static filters.</p>
416
+ </div>
144
417
  ),
145
418
  isDisabled,
146
419
  },
package/src/index.js CHANGED
@@ -1,6 +1,10 @@
1
1
  import installBlocks from './Blocks';
2
2
  import { VisualizationView } from './Views';
3
- import { VisualizationWidget, VisualizationViewWidget } from './Widgets';
3
+ import {
4
+ VisualizationWidget,
5
+ VisualizationViewWidget,
6
+ CreatableSelectWidget,
7
+ } from './Widgets';
4
8
 
5
9
  const applyConfig = (config) => {
6
10
  config.settings.allowed_cors_destinations = [
@@ -13,6 +17,7 @@ const applyConfig = (config) => {
13
17
  config.views.contentTypesViews.tableau_visualization = VisualizationView;
14
18
  config.widgets.id.tableau_visualization = VisualizationWidget;
15
19
  config.widgets.views.id.tableau_visualization = VisualizationViewWidget;
20
+ config.widgets.widget.creatable_select = CreatableSelectWidget;
16
21
 
17
22
  return [installBlocks].reduce((acc, apply) => apply(acc), config);
18
23
  };