@eeacms/volto-tableau 1.3.0 → 3.0.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.
@@ -0,0 +1,55 @@
1
+ import React from 'react';
2
+ import BlockDataForm from '@plone/volto/components/manage/Form/BlockDataForm';
3
+ import { SidebarPortal } from '@plone/volto/components';
4
+ import { getContent } from '@plone/volto/actions';
5
+ import View from './View';
6
+ import Schema from './schema';
7
+ import { connect } from 'react-redux';
8
+ import { compose } from 'redux';
9
+
10
+ const Edit = (props) => {
11
+ const { data, block, onChangeBlock, id } = props;
12
+ const schema = React.useMemo(() => Schema(props), [props]);
13
+
14
+ React.useEffect(() => {
15
+ if (!Object.hasOwn(data, 'show_sources')) {
16
+ onChangeBlock(block, {
17
+ ...data,
18
+ show_sources: true,
19
+ });
20
+ }
21
+ }, [block, data, onChangeBlock]);
22
+
23
+ return (
24
+ <>
25
+ <View data={data} id={id} />
26
+ <SidebarPortal selected={props.selected}>
27
+ <BlockDataForm
28
+ block={block}
29
+ title={schema.title}
30
+ schema={schema}
31
+ onChangeField={(id, value) => {
32
+ props.onChangeBlock(block, {
33
+ ...data,
34
+ [id]: value,
35
+ });
36
+ }}
37
+ formData={data}
38
+ />
39
+ </SidebarPortal>
40
+ </>
41
+ );
42
+ };
43
+
44
+ export default compose(
45
+ connect(
46
+ (state, props) => ({
47
+ block_data: state.content.data,
48
+ data_provenance:
49
+ state.content.subrequests?.[props.id]?.data?.data_provenance,
50
+ }),
51
+ {
52
+ getContent,
53
+ },
54
+ ),
55
+ )(Edit);
@@ -0,0 +1,55 @@
1
+ import React from 'react';
2
+ import ConnectedTableau from '../../ConnectedTableau/ConnectedTableau';
3
+
4
+ import { getContent } from '@plone/volto/actions';
5
+
6
+ import { connect } from 'react-redux';
7
+ import { compose } from 'redux';
8
+
9
+ const View = (props) => {
10
+ const { data } = props || {};
11
+ const { vis_url = '' } = data;
12
+ const show_sources = data?.show_sources ?? false;
13
+
14
+ React.useEffect(() => {
15
+ if (vis_url) {
16
+ props.getContent(vis_url, null, props.id);
17
+ }
18
+ // eslint-disable-next-line react-hooks/exhaustive-deps
19
+ }, [vis_url]);
20
+
21
+ return (
22
+ <>
23
+ {data?.vis_url ? (
24
+ <>
25
+ <ConnectedTableau {...props.tableau_visualization} id={props.id} />
26
+ {show_sources &&
27
+ data.tableauSources &&
28
+ props.tableau_visualization ? (
29
+ ''
30
+ ) : show_sources ? (
31
+ <div>Data provenance is not set in the visualization</div>
32
+ ) : (
33
+ ''
34
+ )}
35
+ </>
36
+ ) : (
37
+ <div>Please select a visualization from block editor.</div>
38
+ )}
39
+ </>
40
+ );
41
+ };
42
+
43
+ export default compose(
44
+ connect(
45
+ (state, props) => ({
46
+ data_provenance:
47
+ state.content.subrequests?.[props.id]?.data?.data_provenance,
48
+ tableau_visualization:
49
+ state.content.subrequests?.[props.id]?.data?.tableau_visualization_data,
50
+ }),
51
+ {
52
+ getContent,
53
+ },
54
+ ),
55
+ )(View);
@@ -0,0 +1,31 @@
1
+ const Schema = (props) => {
2
+ return {
3
+ title: 'Embed EEA Tableau',
4
+ fieldsets: [
5
+ {
6
+ id: 'default',
7
+ title: 'Default',
8
+ fields: ['vis_url', 'height', 'show_sources'],
9
+ },
10
+ ],
11
+ properties: {
12
+ vis_url: {
13
+ widget: 'object_by_path',
14
+ title: 'Visualization',
15
+ },
16
+ height: {
17
+ title: 'Height',
18
+ type: 'number',
19
+ default: 450,
20
+ },
21
+ show_sources: {
22
+ title: 'Toggle sources',
23
+ type: 'boolean',
24
+ },
25
+ },
26
+
27
+ required: ['vis_url'],
28
+ };
29
+ };
30
+
31
+ export default Schema;
@@ -0,0 +1,21 @@
1
+ import React from 'react';
2
+ import Tableau from '@eeacms/volto-tableau/Tableau/View';
3
+
4
+ const ConnectedTableau = (props) => {
5
+ const [error, setError] = React.useState(null);
6
+ const [loaded, setLoaded] = React.useState(null);
7
+ return (
8
+ <div className="tableau-block">
9
+ <Tableau
10
+ error={error}
11
+ loaded={loaded}
12
+ setError={setError}
13
+ setLoaded={setLoaded}
14
+ data={{ ...props?.general, ...props?.options, ...props?.extraOptions }}
15
+ url={props?.general?.url}
16
+ />
17
+ </div>
18
+ );
19
+ };
20
+
21
+ export default ConnectedTableau;
@@ -5,7 +5,7 @@ import { toast } from 'react-toastify';
5
5
  import { Toast } from '@plone/volto/components';
6
6
  import { setTableauApi } from '@eeacms/volto-tableau/actions';
7
7
  import cx from 'classnames';
8
- import { isMyScriptLoaded, loadTableauScript } from '../helpers';
8
+ import { loadTableauScript } from '../helpers';
9
9
 
10
10
  const Tableau = (props) => {
11
11
  const ref = React.useRef(null);
@@ -36,7 +36,7 @@ const Tableau = (props) => {
36
36
  const url = props.url || defaultUrl;
37
37
 
38
38
  //load tableau from script tag
39
- const tableau = isMyScriptLoaded(version) && __CLIENT__ ? window.tableau : '';
39
+ const tableau = loadTableauScript(() => {}, version);
40
40
 
41
41
  const onFilterChange = (filter) => {
42
42
  const newFilters = { ...filters.current };
@@ -186,6 +186,7 @@ const Tableau = (props) => {
186
186
  tableau,
187
187
  toolbarPosition,
188
188
  url,
189
+ version,
189
190
  ]);
190
191
 
191
192
  React.useEffect(() => {
@@ -205,6 +206,19 @@ const Tableau = (props) => {
205
206
  return (
206
207
  <div id="tableau-wrap">
207
208
  <div id="tableau-outer">
209
+ {data && Object.keys(data).length > 0 ? (
210
+ <>
211
+ {loaded ? (
212
+ ''
213
+ ) : (
214
+ <div className="tableau-loader">
215
+ <span>Loading Tableau v{version}</span>
216
+ </div>
217
+ )}
218
+ </>
219
+ ) : (
220
+ <div>No data present in that visualization.</div>
221
+ )}
208
222
  <div
209
223
  className={cx('tableau', version, {
210
224
  'tableau-scale': autoScale,
@@ -47,6 +47,11 @@ const View = (props) => {
47
47
  /* eslint-disable-next-line */
48
48
  }, []);
49
49
 
50
+ React.useEffect(() => {
51
+ if (props.setTableauError) props.setTableauError(error);
52
+ /* eslint-disable-next-line */
53
+ }, [error]);
54
+
50
55
  React.useEffect(() => {
51
56
  const newExtraFilters = { ...extraFilters };
52
57
  urlParameters.forEach((element) => {
@@ -0,0 +1,43 @@
1
+ import React from 'react';
2
+ import TableauView from '../TableauBlock/View';
3
+
4
+ const VisualizationView = (props) => {
5
+ const [tableauError, setTableauError] = React.useState('');
6
+ const { content = {} } = props;
7
+ const { tableau_visualization_data = {} } = content;
8
+
9
+ const TableauNotDisplayed = () => {
10
+ return (
11
+ <div className="tableau-block not_displayed_tableau">
12
+ <div className="tableau-info">
13
+ {!tableau_visualization_data.general?.url ? (
14
+ <p className="tableau-error">URL required</p>
15
+ ) : tableauError ? (
16
+ <p className="tableau-error">{tableauError}</p>
17
+ ) : (
18
+ ''
19
+ )}
20
+ </div>
21
+ </div>
22
+ );
23
+ };
24
+ return (
25
+ <div>
26
+ {!tableau_visualization_data.general?.url || tableauError ? (
27
+ <TableauNotDisplayed />
28
+ ) : (
29
+ ''
30
+ )}
31
+ <TableauView
32
+ setTableauError={setTableauError}
33
+ data={{
34
+ ...tableau_visualization_data.general,
35
+ ...tableau_visualization_data.options,
36
+ ...tableau_visualization_data.extraOptions,
37
+ }}
38
+ />
39
+ </div>
40
+ );
41
+ };
42
+
43
+ export default VisualizationView;
@@ -0,0 +1,3 @@
1
+ import VisualizationView from './VisualizationView';
2
+
3
+ export { VisualizationView };
@@ -0,0 +1,158 @@
1
+ import React from 'react';
2
+ import { Modal, Button, Grid } from 'semantic-ui-react';
3
+ import '@eeacms/volto-tableau/less/tableau.less';
4
+ import config from '@plone/volto/registry';
5
+
6
+ import { FormFieldWrapper, InlineForm } from '@plone/volto/components';
7
+
8
+ import TableauView from '../TableauBlock/View';
9
+ import Schema from './schema';
10
+
11
+ const VisualizationWidget = (props) => {
12
+ const [open, setOpen] = React.useState(false);
13
+ const { onChange = {}, id } = props;
14
+
15
+ const block = React.useMemo(() => props.block, [props.block]);
16
+ const value = React.useMemo(() => props.value, [props.value]);
17
+
18
+ const [intValue, setIntValue] = React.useState(value);
19
+ const [tableauError, setTableauError] = React.useState('');
20
+
21
+ const dataForm = { tableau_data: intValue };
22
+
23
+ const handleApplyChanges = () => {
24
+ onChange(id, intValue);
25
+ setOpen(false);
26
+ };
27
+
28
+ const handleClose = () => {
29
+ setIntValue(value);
30
+ setOpen(false);
31
+ };
32
+
33
+ const handleChangeField = (val) => {
34
+ setIntValue(val);
35
+ };
36
+
37
+ const TableauNotDisplayed = () => {
38
+ return (
39
+ <div className="tableau-block not_displayed_tableau">
40
+ <div className="tableau-info">
41
+ {intValue && intValue.general && !intValue.general.url ? (
42
+ <p className="tableau-error">URL required</p>
43
+ ) : tableauError ? (
44
+ <p className="tableau-error">{tableauError}</p>
45
+ ) : (
46
+ ''
47
+ )}
48
+ </div>
49
+ </div>
50
+ );
51
+ };
52
+
53
+ let schema = Schema(config);
54
+
55
+ React.useEffect(() => {
56
+ if (!intValue?.options) {
57
+ setIntValue({
58
+ ...intValue,
59
+ options: {
60
+ autoScale: false,
61
+ hideTabs: false,
62
+ hideToolbar: false,
63
+ toolbarPosition: 'Top',
64
+ },
65
+ });
66
+ }
67
+ // eslint-disable-next-line react-hooks/exhaustive-deps
68
+ }, [intValue]);
69
+
70
+ return (
71
+ <FormFieldWrapper {...props}>
72
+ <div className="wrapper">
73
+ <Button
74
+ floated="right"
75
+ size="tiny"
76
+ onClick={(e) => {
77
+ e.preventDefault();
78
+ e.stopPropagation();
79
+ setOpen(true);
80
+ }}
81
+ >
82
+ Open Tableau Editor
83
+ </Button>
84
+ </div>
85
+
86
+ {open && (
87
+ <Modal
88
+ id="tableau-editor-modal"
89
+ style={{ width: '95% !important' }}
90
+ open={true}
91
+ >
92
+ <Modal.Content scrolling>
93
+ <Grid stackable reversed="mobile vertically tablet vertically">
94
+ <Grid.Column
95
+ mobile={12}
96
+ tablet={12}
97
+ computer={5}
98
+ className="tableau-editor-column"
99
+ >
100
+ <InlineForm
101
+ block={block}
102
+ schema={schema}
103
+ onChangeField={(id, value) => {
104
+ handleChangeField(value);
105
+ }}
106
+ formData={dataForm}
107
+ />
108
+ </Grid.Column>
109
+ <Grid.Column mobile={12} tablet={12} computer={7}>
110
+ {(intValue && intValue.general && !intValue.general.url) ||
111
+ tableauError ? (
112
+ <TableauNotDisplayed />
113
+ ) : (
114
+ ''
115
+ )}
116
+ <div className="tableau-container">
117
+ <TableauView
118
+ setTableauError={setTableauError}
119
+ data={{
120
+ ...intValue?.general,
121
+ ...intValue?.options,
122
+ ...intValue?.extraOptions,
123
+ }}
124
+ />
125
+ </div>
126
+ </Grid.Column>
127
+ </Grid>
128
+ </Modal.Content>
129
+ <Modal.Actions>
130
+ <Grid>
131
+ <Grid.Row>
132
+ <div className="map-edit-actions-container">
133
+ <Button onClick={handleClose}>Close</Button>
134
+ <Button color="green" onClick={handleApplyChanges}>
135
+ Apply changes
136
+ </Button>
137
+ </div>
138
+ </Grid.Row>
139
+ </Grid>
140
+ </Modal.Actions>
141
+ </Modal>
142
+ )}
143
+ {(intValue && intValue.general && !intValue.general.url) ||
144
+ tableauError ? (
145
+ <TableauNotDisplayed />
146
+ ) : (
147
+ ''
148
+ )}
149
+
150
+ <TableauView
151
+ setTableauError={setTableauError}
152
+ data={{ ...value?.general, ...value?.options, ...value?.extraOptions }}
153
+ />
154
+ </FormFieldWrapper>
155
+ );
156
+ };
157
+
158
+ export default VisualizationWidget;
@@ -0,0 +1,3 @@
1
+ import VisualizationWidget from './VisualizationWidget';
2
+
3
+ export { VisualizationWidget };
@@ -0,0 +1,176 @@
1
+ const generalSchema = {
2
+ title: 'General',
3
+
4
+ fieldsets: [
5
+ {
6
+ id: 'general',
7
+ title: 'General',
8
+ fields: ['url'],
9
+ },
10
+ ],
11
+
12
+ properties: {
13
+ url: {
14
+ title: 'Url',
15
+ type: 'textarea',
16
+ },
17
+ },
18
+ required: ['url'],
19
+ };
20
+
21
+ const optionsSchema = {
22
+ title: 'Options',
23
+
24
+ fieldsets: [
25
+ {
26
+ id: 'options',
27
+ title: 'Options',
28
+ fields: [
29
+ 'sheetname',
30
+ 'hideTabs',
31
+ 'hideToolbar',
32
+ 'autoScale',
33
+ 'toolbarPosition',
34
+ ],
35
+ },
36
+ ],
37
+
38
+ properties: {
39
+ sheetname: {
40
+ title: 'Sheetname',
41
+ type: 'text',
42
+ },
43
+ hideTabs: {
44
+ title: 'Hide tabs',
45
+ type: 'boolean',
46
+ default: false,
47
+ },
48
+ hideToolbar: {
49
+ title: 'Hide toolbar',
50
+ type: 'boolean',
51
+ default: false,
52
+ },
53
+ autoScale: {
54
+ title: 'Auto scale',
55
+ type: 'boolean',
56
+ default: false,
57
+ description: 'Scale down tableau according to width',
58
+ },
59
+ toolbarPosition: {
60
+ title: 'Toolbar position',
61
+ type: 'array',
62
+ choices: [
63
+ ['Top', 'Top'],
64
+ ['Bottom', 'Bottom'],
65
+ ],
66
+ default: 'Top',
67
+ },
68
+ },
69
+ required: [],
70
+ };
71
+
72
+ const urlParametersSchema = {
73
+ title: 'Parameter',
74
+ fieldsets: [
75
+ { id: 'default', title: 'Default', fields: ['field', 'urlParam'] },
76
+ ],
77
+ properties: {
78
+ field: {
79
+ title: 'Tableau fieldname',
80
+ type: 'text',
81
+ },
82
+ urlParam: {
83
+ title: 'URL param',
84
+ type: 'text',
85
+ },
86
+ },
87
+ required: [],
88
+ };
89
+
90
+ const breakpointUrlSchema = (config) => {
91
+ const breakpoints = config.blocks.blocksConfig.tableau_block.breakpoints;
92
+
93
+ return {
94
+ title: 'URL',
95
+ fieldsets: [{ id: 'default', title: 'Default', fields: ['device', 'url'] }],
96
+ properties: {
97
+ device: {
98
+ title: 'Device',
99
+ type: 'array',
100
+ choices: Object.keys(breakpoints).map((breakpoint) => [
101
+ breakpoint,
102
+ breakpoint,
103
+ ]),
104
+ },
105
+ url: {
106
+ title: 'Url',
107
+ widget: 'textarea',
108
+ },
109
+ },
110
+ required: [],
111
+ };
112
+ };
113
+
114
+ const extraOptionsSchema = (config) => {
115
+ return {
116
+ title: 'Extra Options',
117
+
118
+ fieldsets: [
119
+ {
120
+ id: 'default',
121
+ title: 'Extra Options Data',
122
+ fields: ['urlParameters', 'breakpointUrls'],
123
+ },
124
+ ],
125
+
126
+ properties: {
127
+ urlParameters: {
128
+ title: 'URL parameters',
129
+ widget: 'object_list',
130
+ schema: urlParametersSchema,
131
+ description: 'Set a list of url parameters to filter the tableau',
132
+ },
133
+ breakpointUrls: {
134
+ title: 'Breakpoint urls',
135
+ widget: 'object_list',
136
+ schema: breakpointUrlSchema(config),
137
+ description: 'Set different vizualization for specific breakpoint',
138
+ },
139
+ },
140
+ required: [],
141
+ };
142
+ };
143
+
144
+ export default (config) => {
145
+ return {
146
+ title: 'Tableau Editor',
147
+ fieldsets: [
148
+ {
149
+ id: 'default',
150
+ title: 'Tableau Editor Sections',
151
+ fields: ['tableau_data'],
152
+ },
153
+ ],
154
+ properties: {
155
+ tableau_data: {
156
+ title: 'Panels',
157
+ widget: 'object_types_widget',
158
+ schemas: [
159
+ {
160
+ id: 'general',
161
+ schema: generalSchema,
162
+ },
163
+ {
164
+ id: 'options',
165
+ schema: optionsSchema,
166
+ },
167
+ {
168
+ id: 'extraOptions',
169
+ schema: extraOptionsSchema(config),
170
+ },
171
+ ],
172
+ },
173
+ },
174
+ required: [],
175
+ };
176
+ };
@@ -0,0 +1,8 @@
1
+ .sidebar-container-enter-done {
2
+ z-index: 1001 !important;
3
+ }
4
+
5
+ .field-provider-data-action-button {
6
+ border: 1px solid #c7d5d8;
7
+ cursor: pointer;
8
+ }
package/src/helpers.js CHANGED
@@ -1,5 +1,5 @@
1
1
  const loadTableauScript = (callback, version) => {
2
- const existingScript = document.getElementById(`tableauJS`);
2
+ const existingScript = __CLIENT__ && document.getElementById(`tableauJS`);
3
3
  //replace script loaded on each version change
4
4
  if (existingScript) {
5
5
  existingScript.setAttribute(
@@ -7,7 +7,7 @@ const loadTableauScript = (callback, version) => {
7
7
  `https://public.tableau.com/javascripts/api/tableau-${version}.min.js`,
8
8
  );
9
9
  }
10
- if (!existingScript) {
10
+ if (!existingScript && __CLIENT__) {
11
11
  const script = document.createElement('script');
12
12
  script.src = `https://public.tableau.com/javascripts/api/tableau-${version}.min.js`;
13
13
  script.id = `tableauJS`;
@@ -18,14 +18,21 @@ const loadTableauScript = (callback, version) => {
18
18
  }
19
19
  //callback, if needed
20
20
  if (existingScript && callback) callback();
21
+
22
+ const tableau = isMyScriptLoaded(version) && __CLIENT__ ? window.tableau : '';
23
+ return tableau;
21
24
  };
22
25
 
23
- const isMyScriptLoaded = (id) => {
26
+ const isMyScriptLoaded = (version) => {
24
27
  //check for loaded Tableau script in dom scripts
25
28
  var scripts = document.getElementsByTagName('script');
26
29
  for (var i = scripts.length; i--; ) {
27
30
  // eslint-disable-next-line eqeqeq
28
- if (scripts[i].id == `tableauJS`) return true;
31
+ if (
32
+ scripts[i].src ===
33
+ `https://public.tableau.com/javascripts/api/tableau-${version}.min.js`
34
+ )
35
+ return true;
29
36
  }
30
37
  return false;
31
38
  };
package/src/index.js CHANGED
@@ -1,6 +1,10 @@
1
1
  import sliderSVG from '@plone/volto/icons/slider.svg';
2
2
  import TableauEdit from './TableauBlock/Edit';
3
3
  import TableauView from './TableauBlock/View';
4
+ import EmbedTableauView from './Blocks/EmbedEEATableauBlock/View';
5
+ import EmbedTableauEdit from './Blocks/EmbedEEATableauBlock/Edit';
6
+ import { VisualizationView } from './Views';
7
+ import { VisualizationWidget } from './Widgets';
4
8
 
5
9
  import tableauStore from './store';
6
10
 
@@ -44,6 +48,31 @@ const applyConfig = (config) => {
44
48
  },
45
49
  };
46
50
 
51
+ config.blocks.blocksConfig.embed_eea_tableau_block = {
52
+ id: 'embed_eea_tableau_block',
53
+ title: 'Embed EEA Tableau',
54
+ icon: sliderSVG,
55
+ group: 'common',
56
+ edit: EmbedTableauEdit,
57
+ view: EmbedTableauView,
58
+ restricted: false,
59
+ mostUsed: false,
60
+ sidebarTab: 1,
61
+ blocks: {},
62
+ security: {
63
+ addPermission: [],
64
+ view: [],
65
+ },
66
+ breakpoints: {
67
+ desktop: [Infinity, 982],
68
+ tablet: [981, 768],
69
+ mobile: [767, 0],
70
+ },
71
+ };
72
+
73
+ config.views.contentTypesViews.tableau_visualization = VisualizationView;
74
+ config.widgets.id.tableau_visualization_data = VisualizationWidget;
75
+
47
76
  return config;
48
77
  };
49
78