@eeacms/volto-tableau 7.0.4 → 7.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.
@@ -1,4 +1,127 @@
1
- import isUndefined from 'lodash/isUndefined';
1
+ import { reduce, isUndefined, isString } from 'lodash';
2
+ import qs from 'querystring';
3
+ import { pickMetadata } from '@eeacms/volto-embed/helpers';
4
+
5
+ export function getQuery({
6
+ data_query = [],
7
+ location = {},
8
+ tableau_vis_url = '',
9
+ discodata_query = {},
10
+ }) {
11
+ return {
12
+ ...reduce(
13
+ data_query,
14
+ (acc, { i, v }) => {
15
+ if (i && v.length) {
16
+ return { ...acc, [i]: v };
17
+ }
18
+ return acc;
19
+ },
20
+ {},
21
+ ),
22
+ ...(qs.parse(location.search?.replace('?', '')) || {}),
23
+ ...(qs.parse(tableau_vis_url.split('?')[1]) || {}),
24
+ ...(discodata_query?.search || {}),
25
+ };
26
+ }
27
+
28
+ export function getTableauVisualization({
29
+ isBlock,
30
+ data,
31
+ tableauContent,
32
+ content,
33
+ }) {
34
+ const mergedContent = (isBlock ? tableauContent : content) || {};
35
+ const tableau_visualization =
36
+ (isBlock
37
+ ? tableauContent?.tableau_visualization
38
+ : content?.tableau_visualization) ||
39
+ data?.tableau_visualization ||
40
+ {};
41
+ return {
42
+ ...pickMetadata(mergedContent),
43
+ ...tableau_visualization,
44
+ };
45
+ }
46
+
47
+ export function getParameters({ tableauVisualization, query, data }) {
48
+ const { urlParameters = [] } = tableauVisualization || {};
49
+
50
+ const staticParameters = [
51
+ ...(tableauVisualization?.staticParameters || []),
52
+ ...(data?.staticParameters || []),
53
+ ];
54
+
55
+ return {
56
+ ...reduce(
57
+ staticParameters,
58
+ (acc, { field, value }) => {
59
+ if (field && value) {
60
+ return {
61
+ ...acc,
62
+ [field]: value,
63
+ };
64
+ }
65
+ return acc;
66
+ },
67
+ {},
68
+ ),
69
+ ...reduce(
70
+ urlParameters,
71
+ (acc, { field, urlParam }) => {
72
+ if (field && query[urlParam]) {
73
+ return {
74
+ ...acc,
75
+ [field]: isString(query[urlParam])
76
+ ? query[urlParam].split(',')
77
+ : query[urlParam],
78
+ };
79
+ }
80
+ return acc;
81
+ },
82
+ {},
83
+ ),
84
+ };
85
+ }
86
+
87
+ export function getFilters({ tableauVisualization, query, data }) {
88
+ // const { dynamicFilters = [] } = tableauVisualization || {};
89
+ const staticFilters = [
90
+ ...(tableauVisualization?.staticFilters || []),
91
+ ...(data?.staticFilters || []),
92
+ ];
93
+
94
+ return {
95
+ ...reduce(
96
+ staticFilters,
97
+ (acc, { field, value }) => {
98
+ if (field && value) {
99
+ return {
100
+ ...acc,
101
+ [field]: value,
102
+ };
103
+ }
104
+ return acc;
105
+ },
106
+ {},
107
+ ),
108
+ // ...reduce(
109
+ // dynamicFilters,
110
+ // (acc, { field, urlParam }) => {
111
+ // if (field && urlParam) {
112
+ // return {
113
+ // ...acc,
114
+ // [field]: isString(query[urlParam])
115
+ // ? query[urlParam].split(',')
116
+ // : query[urlParam],
117
+ // };
118
+ // }
119
+ // return acc;
120
+ // },
121
+ // {},
122
+ // ),
123
+ };
124
+ }
2
125
 
3
126
  export const getSheetnames = (viz) => {
4
127
  if (!viz) return [];
@@ -1,25 +1,37 @@
1
- import React from 'react';
2
- import { hasBlocksData } from '@plone/volto/helpers';
1
+ import React, { useState } from 'react';
2
+ import { withRouter } from 'react-router';
3
3
  import { Container } from 'semantic-ui-react';
4
+ import { hasBlocksData } from '@plone/volto/helpers';
4
5
  import config from '@plone/volto/registry';
5
6
  import RenderBlocks from '@plone/volto/components/theme/View/RenderBlocks';
6
- import { pickMetadata } from '@eeacms/volto-embed/helpers';
7
7
  import Tableau from '@eeacms/volto-tableau/Tableau/Tableau';
8
+ import {
9
+ getQuery,
10
+ getTableauVisualization,
11
+ getParameters,
12
+ getFilters,
13
+ } from '@eeacms/volto-tableau/Tableau/helpers';
8
14
 
9
15
  const VisualizationView = (props) => {
10
- const { content = {} } = props;
11
- const { tableau_visualization = {} } = content;
12
- const { staticParameters = [] } = tableau_visualization;
16
+ const { location, content } = props;
17
+
18
+ const [tableauVisualization] = useState(() =>
19
+ getTableauVisualization({
20
+ isBlock: false,
21
+ content,
22
+ }),
23
+ );
24
+ const [query] = useState(() => {
25
+ return getQuery({ location });
26
+ });
13
27
 
14
- const extraOptions = React.useMemo(() => {
15
- const options = {};
16
- staticParameters.forEach((parameter) => {
17
- if (parameter.field && parameter.value) {
18
- options[parameter.field] = parameter.value;
19
- }
20
- });
21
- return options;
22
- }, [staticParameters]);
28
+ const [extraParameters] = useState(() =>
29
+ getParameters({ tableauVisualization, query }),
30
+ );
31
+
32
+ const [extraFilters] = useState(() =>
33
+ getFilters({ tableauVisualization, query }),
34
+ );
23
35
 
24
36
  return (
25
37
  <Container id="page-document">
@@ -28,8 +40,7 @@ const VisualizationView = (props) => {
28
40
  ) : (
29
41
  <Tableau
30
42
  data={{
31
- ...tableau_visualization,
32
- ...pickMetadata(content),
43
+ ...tableauVisualization,
33
44
  with_notes: false,
34
45
  with_sources: false,
35
46
  with_more_info: false,
@@ -37,14 +48,15 @@ const VisualizationView = (props) => {
37
48
  with_enlarge: true,
38
49
  with_download: true,
39
50
  }}
40
- extraOptions={extraOptions}
41
51
  breakpoints={
42
52
  config.blocks.blocksConfig.embed_tableau_visualization.breakpoints
43
53
  }
54
+ extraParameters={extraParameters}
55
+ extraFilters={extraFilters}
44
56
  />
45
57
  )}
46
58
  </Container>
47
59
  );
48
60
  };
49
61
 
50
- export default VisualizationView;
62
+ export default withRouter(VisualizationView);
@@ -0,0 +1,300 @@
1
+ /**
2
+ * CreatableSelectWidget component.
3
+ * @module components/manage/Widgets/CreatableSelectWidget
4
+ */
5
+
6
+ import React, { Component } from 'react';
7
+ import PropTypes from 'prop-types';
8
+ import { connect } from 'react-redux';
9
+ import { compose } from 'redux';
10
+ import { map } from 'lodash';
11
+ import { defineMessages, injectIntl } from 'react-intl';
12
+ import {
13
+ getVocabFromHint,
14
+ getVocabFromField,
15
+ getVocabFromItems,
16
+ } from '@plone/volto/helpers';
17
+ import { FormFieldWrapper } from '@plone/volto/components';
18
+ import { getVocabulary, getVocabularyTokenTitle } from '@plone/volto/actions';
19
+ import { normalizeValue } from '@plone/volto/components/manage/Widgets/SelectUtils';
20
+
21
+ import {
22
+ customSelectStyles,
23
+ DropdownIndicator,
24
+ ClearIndicator,
25
+ Option,
26
+ selectTheme,
27
+ MenuList,
28
+ MultiValueContainer,
29
+ } from '@plone/volto/components/manage/Widgets/SelectStyling';
30
+ import { injectLazyLibs } from '@plone/volto/helpers/Loadable/Loadable';
31
+
32
+ const messages = defineMessages({
33
+ default: {
34
+ id: 'Default',
35
+ defaultMessage: 'Default',
36
+ },
37
+ idTitle: {
38
+ id: 'Short Name',
39
+ defaultMessage: 'Short Name',
40
+ },
41
+ idDescription: {
42
+ id: 'Used for programmatic access to the fieldset.',
43
+ defaultMessage: 'Used for programmatic access to the fieldset.',
44
+ },
45
+ title: {
46
+ id: 'Title',
47
+ defaultMessage: 'Title',
48
+ },
49
+ description: {
50
+ id: 'Description',
51
+ defaultMessage: 'Description',
52
+ },
53
+ close: {
54
+ id: 'Close',
55
+ defaultMessage: 'Close',
56
+ },
57
+ choices: {
58
+ id: 'Choices',
59
+ defaultMessage: 'Choices',
60
+ },
61
+ required: {
62
+ id: 'Required',
63
+ defaultMessage: 'Required',
64
+ },
65
+ select: {
66
+ id: 'Select…',
67
+ defaultMessage: 'Select…',
68
+ },
69
+ no_value: {
70
+ id: 'No value',
71
+ defaultMessage: 'No value',
72
+ },
73
+ no_options: {
74
+ id: 'No options',
75
+ defaultMessage: 'No options',
76
+ },
77
+ });
78
+
79
+ /**
80
+ * CreatableSelectWidget component class.
81
+ * @function CreatableSelectWidget
82
+ * @returns {string} Markup of the component.
83
+ */
84
+ class CreatableSelectWidget extends Component {
85
+ /**
86
+ * Property types.
87
+ * @property {Object} propTypes Property types.
88
+ * @static
89
+ */
90
+ static propTypes = {
91
+ id: PropTypes.string.isRequired,
92
+ title: PropTypes.string.isRequired,
93
+ description: PropTypes.string,
94
+ required: PropTypes.bool,
95
+ error: PropTypes.arrayOf(PropTypes.string),
96
+ getVocabulary: PropTypes.func.isRequired,
97
+ getVocabularyTokenTitle: PropTypes.func.isRequired,
98
+ choices: PropTypes.arrayOf(
99
+ PropTypes.oneOfType([PropTypes.object, PropTypes.array]),
100
+ ),
101
+ items: PropTypes.shape({
102
+ vocabulary: PropTypes.object,
103
+ }),
104
+ widgetOptions: PropTypes.shape({
105
+ vocabulary: PropTypes.object,
106
+ }),
107
+ value: PropTypes.oneOfType([
108
+ PropTypes.object,
109
+ PropTypes.string,
110
+ PropTypes.bool,
111
+ PropTypes.func,
112
+ PropTypes.array,
113
+ ]),
114
+ onChange: PropTypes.func.isRequired,
115
+ onBlur: PropTypes.func,
116
+ onClick: PropTypes.func,
117
+ onEdit: PropTypes.func,
118
+ onDelete: PropTypes.func,
119
+ wrapped: PropTypes.bool,
120
+ noValueOption: PropTypes.bool,
121
+ customOptionStyling: PropTypes.any,
122
+ isMulti: PropTypes.bool,
123
+ creatable: PropTypes.bool,
124
+ placeholder: PropTypes.string,
125
+ };
126
+
127
+ /**
128
+ * Default properties
129
+ * @property {Object} defaultProps Default properties.
130
+ * @static
131
+ */
132
+ static defaultProps = {
133
+ description: null,
134
+ required: false,
135
+ items: {
136
+ vocabulary: null,
137
+ },
138
+ widgetOptions: {
139
+ vocabulary: null,
140
+ },
141
+ error: [],
142
+ choices: [],
143
+ value: null,
144
+ onChange: () => {},
145
+ onBlur: () => {},
146
+ onClick: () => {},
147
+ onEdit: null,
148
+ onDelete: null,
149
+ noValueOption: true,
150
+ customOptionStyling: null,
151
+ };
152
+
153
+ /**
154
+ * Component did mount
155
+ * @method componentDidMount
156
+ * @returns {undefined}
157
+ */
158
+ componentDidMount() {
159
+ if (
160
+ (!this.props.choices || this.props.choices?.length === 0) &&
161
+ this.props.vocabBaseUrl
162
+ ) {
163
+ this.props.getVocabulary({
164
+ vocabNameOrURL: this.props.vocabBaseUrl,
165
+ size: -1,
166
+ subrequest: this.props.lang,
167
+ });
168
+ }
169
+ }
170
+
171
+ /**
172
+ * Render method.
173
+ * @method render
174
+ * @returns {string} Markup for the component.
175
+ */
176
+ render() {
177
+ const { id, choices, value, intl, onChange } = this.props;
178
+ const normalizedValue = normalizeValue(choices, value, intl);
179
+ // Make sure that both disabled and isDisabled (from the DX layout feat work)
180
+ const disabled = this.props.disabled || this.props.isDisabled;
181
+ const Select = this.props.creatable
182
+ ? this.props.reactSelectCreateable.default
183
+ : this.props.reactSelect.default;
184
+
185
+ let options = this.props.vocabBaseUrl
186
+ ? this.props.choices
187
+ : [
188
+ ...map(choices, (option) => ({
189
+ value: option[0],
190
+ label:
191
+ // Fix "None" on the serializer, to remove when fixed in p.restapi
192
+ option[1] !== 'None' && option[1] ? option[1] : option[0],
193
+ })),
194
+ // Only set "no-value" option if there's no default in the field
195
+ // TODO: also if this.props.defaultValue?
196
+ ...(this.props.noValueOption && !this.props.default
197
+ ? [
198
+ {
199
+ label: this.props.intl.formatMessage(messages.no_value),
200
+ value: 'no-value',
201
+ },
202
+ ]
203
+ : []),
204
+ ];
205
+
206
+ const isMulti = this.props.isMulti
207
+ ? this.props.isMulti
208
+ : id === 'roles' || id === 'groups' || this.props.type === 'array';
209
+
210
+ return (
211
+ <FormFieldWrapper {...this.props}>
212
+ <Select
213
+ id={`field-${id}`}
214
+ key={choices}
215
+ name={id}
216
+ menuShouldScrollIntoView={false}
217
+ isDisabled={disabled}
218
+ isSearchable={true}
219
+ className="react-select-container"
220
+ classNamePrefix="react-select"
221
+ isMulti={isMulti}
222
+ options={options}
223
+ styles={customSelectStyles}
224
+ theme={selectTheme}
225
+ components={{
226
+ ...(options?.length > 25 && {
227
+ MenuList,
228
+ }),
229
+ MultiValueContainer,
230
+ DropdownIndicator,
231
+ ClearIndicator,
232
+ Option: this.props.customOptionStyling || Option,
233
+ }}
234
+ value={normalizedValue}
235
+ placeholder={
236
+ this.props.placeholder ??
237
+ this.props.intl.formatMessage(messages.select)
238
+ }
239
+ onChange={(selectedOption) => {
240
+ if (isMulti) {
241
+ return onChange(
242
+ id,
243
+ selectedOption.map((el) => el.value),
244
+ );
245
+ }
246
+ return onChange(
247
+ id,
248
+ selectedOption && selectedOption.value !== 'no-value'
249
+ ? selectedOption.value
250
+ : undefined,
251
+ );
252
+ }}
253
+ isClearable
254
+ />
255
+ </FormFieldWrapper>
256
+ );
257
+ }
258
+ }
259
+
260
+ export const CreatableSelectWidgetComponent = injectIntl(CreatableSelectWidget);
261
+
262
+ export default compose(
263
+ injectLazyLibs(['reactSelect', 'reactSelectCreateable']),
264
+ connect(
265
+ (state, props) => {
266
+ const vocabBaseUrl = !props.choices
267
+ ? getVocabFromHint(props) ||
268
+ getVocabFromField(props) ||
269
+ getVocabFromItems(props)
270
+ : '';
271
+
272
+ const vocabState =
273
+ state.vocabularies?.[vocabBaseUrl]?.subrequests?.[state.intl.locale];
274
+
275
+ // If the schema already has the choices in it, then do not try to get the vocab,
276
+ // even if there is one
277
+ if (props.choices) {
278
+ return {
279
+ choices: props.choices,
280
+ lang: state.intl.locale,
281
+ };
282
+ } else if (vocabState) {
283
+ return {
284
+ vocabBaseUrl,
285
+ choices: vocabState?.items ?? [],
286
+ lang: state.intl.locale,
287
+ };
288
+ // There is a moment that vocabState is not there yet, so we need to pass the
289
+ // vocabBaseUrl to the component.
290
+ } else if (vocabBaseUrl) {
291
+ return {
292
+ vocabBaseUrl,
293
+ lang: state.intl.locale,
294
+ };
295
+ }
296
+ return { lang: state.intl.locale };
297
+ },
298
+ { getVocabulary, getVocabularyTokenTitle },
299
+ ),
300
+ )(CreatableSelectWidgetComponent);
@@ -1,27 +1,41 @@
1
- import React from 'react';
1
+ import React, { useState } from 'react';
2
2
  import { connect } from 'react-redux';
3
+ import { compose } from 'redux';
4
+ import { withRouter } from 'react-router';
3
5
  import config from '@plone/volto/registry';
4
- import { pickMetadata } from '@eeacms/volto-embed/helpers';
5
6
  import Tableau from '@eeacms/volto-tableau/Tableau/Tableau';
7
+ import {
8
+ getQuery,
9
+ getTableauVisualization,
10
+ getParameters,
11
+ getFilters,
12
+ } from '@eeacms/volto-tableau/Tableau/helpers';
6
13
 
7
14
  function VisualizationViewWidget(props) {
8
- const { staticParameters = [] } = props.value;
15
+ const { location, content } = props;
9
16
 
10
- const extraOptions = React.useMemo(() => {
11
- const options = {};
12
- staticParameters.forEach((parameter) => {
13
- if (parameter.field && parameter.value) {
14
- options[parameter.field] = parameter.value;
15
- }
16
- });
17
- return options;
18
- }, [staticParameters]);
17
+ const [tableauVisualization] = useState(() =>
18
+ getTableauVisualization({
19
+ isBlock: false,
20
+ content,
21
+ }),
22
+ );
23
+ const [query] = useState(() => {
24
+ return getQuery({ location });
25
+ });
26
+
27
+ const [extraParameters] = useState(() =>
28
+ getParameters({ tableauVisualization, query }),
29
+ );
30
+
31
+ const [extraFilters] = useState(() =>
32
+ getFilters({ tableauVisualization, query }),
33
+ );
19
34
 
20
35
  return (
21
36
  <Tableau
22
37
  data={{
23
- ...(props.value || {}),
24
- ...pickMetadata(props.content),
38
+ ...tableauVisualization,
25
39
  with_notes: false,
26
40
  with_sources: false,
27
41
  with_more_info: false,
@@ -29,14 +43,16 @@ function VisualizationViewWidget(props) {
29
43
  with_enlarge: true,
30
44
  with_download: true,
31
45
  }}
32
- extraOptions={extraOptions}
33
46
  breakpoints={
34
47
  config.blocks.blocksConfig?.embed_tableau_visualization?.breakpoints
35
48
  }
49
+ extraParameters={extraParameters}
50
+ extraFilters={extraFilters}
36
51
  />
37
52
  );
38
53
  }
39
54
 
40
- export default connect((state) => ({ content: state.content.data }))(
41
- VisualizationViewWidget,
42
- );
55
+ export default compose(
56
+ withRouter,
57
+ connect((state) => ({ content: state?.content?.data })),
58
+ )(VisualizationViewWidget);
@@ -2,6 +2,7 @@ import React from 'react';
2
2
  import { render } from '@testing-library/react';
3
3
  import '@testing-library/jest-dom/extend-expect';
4
4
  import { Provider } from 'react-redux';
5
+
5
6
  import VisualizationViewWidget from './VisualizationViewWidget';
6
7
 
7
8
  describe('VisualizationViewWidget', () => {