@eeacms/volto-cca-policy 1.0.11 → 1.0.13
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 +38 -1
- package/package.json +1 -1
- package/src/components/Search/NavigatorGuide/NavigatorGuideContentView.jsx +13 -3
- package/src/components/Search/NavigatorGuide/useGuideFacetOptions.js +27 -0
- package/src/components/Search/NavigatorGuide/useGuideFacetOptions.test.js +58 -0
- package/src/components/Search/NavigatorGuide/utils.js +30 -0
- package/src/components/Search/NavigatorGuide/utils.test.js +40 -0
- package/src/components/manage/Widgets/VisualizationsWidget.jsx +51 -10
- package/src/components/manage/Widgets/visualizations.less +4 -0
- package/src/components/theme/Views/DatabaseItemView.jsx +43 -14
- package/src/components/theme/Views/DatabaseItemView.test.jsx +34 -0
- package/src/utils.test.js +127 -0
- package/theme/globals/navigator.less +6 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,25 @@ 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
|
+
### [1.0.13](https://github.com/eea/volto-cca-policy/compare/1.0.12...1.0.13) - 31 August 2026
|
|
8
|
+
|
|
9
|
+
#### :hammer_and_wrench: Others
|
|
10
|
+
|
|
11
|
+
- Refs #307245 - Visualizations: fix order in view mode. [GhitaB - [`bfbe754`](https://github.com/eea/volto-cca-policy/commit/bfbe7549af5a4f767c3d023b894b55b5eb81918e)]
|
|
12
|
+
- Refs #307245 - Visualizations: order and descriptions. [GhitaB - [`5fff846`](https://github.com/eea/volto-cca-policy/commit/5fff846b623250e89cbf2a844828f545c0f2f9f9)]
|
|
13
|
+
### [1.0.12](https://github.com/eea/volto-cca-policy/compare/1.0.11...1.0.12) - 24 August 2026
|
|
14
|
+
|
|
15
|
+
#### :bug: Bug Fixes
|
|
16
|
+
|
|
17
|
+
- fix: disable unavailable navigator guide options [kreafox - [`4b114c2`](https://github.com/eea/volto-cca-policy/commit/4b114c2b0ea7a6a6528ae8ae378c2e671b640945)]
|
|
18
|
+
|
|
19
|
+
#### :nail_care: Enhancements
|
|
20
|
+
|
|
21
|
+
- change: add custom hook for guide facet options and add tests [kreafox - [`872eb9b`](https://github.com/eea/volto-cca-policy/commit/872eb9b35c41fc2094c605b113411448a8cef9a8)]
|
|
22
|
+
|
|
23
|
+
#### :hammer_and_wrench: Others
|
|
24
|
+
|
|
25
|
+
- test: increase utility coverage [kreafox - [`d9c3db8`](https://github.com/eea/volto-cca-policy/commit/d9c3db8cc6fb91ead00f83b822f6fe48c26a70b1)]
|
|
7
26
|
### [1.0.11](https://github.com/eea/volto-cca-policy/compare/1.0.10...1.0.11) - 21 August 2026
|
|
8
27
|
|
|
9
28
|
### [1.0.10](https://github.com/eea/volto-cca-policy/compare/1.0.9...1.0.10) - 19 August 2026
|
|
@@ -288,7 +307,25 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
|
|
|
288
307
|
#### :hammer_and_wrench: Others
|
|
289
308
|
|
|
290
309
|
- test: pin Chromium version 149 to work with Cypress [valentinab25 - [`659ec82`](https://github.com/eea/volto-cca-policy/commit/659ec823dd71fe592a3a7accda22effbe1fb07bc)]
|
|
291
|
-
## [1.0.0](https://github.com/eea/volto-cca-policy/compare/1.0.0-alpha.
|
|
310
|
+
## [1.0.0](https://github.com/eea/volto-cca-policy/compare/1.0.0-alpha.26...1.0.0) - 12 June 2026
|
|
311
|
+
|
|
312
|
+
### [1.0.0-alpha.26](https://github.com/eea/volto-cca-policy/compare/1.0.0-alpha.25...1.0.0-alpha.26) - 28 August 2026
|
|
313
|
+
|
|
314
|
+
### [1.0.0-alpha.25](https://github.com/eea/volto-cca-policy/compare/1.0.0-alpha.24...1.0.0-alpha.25) - 27 August 2026
|
|
315
|
+
|
|
316
|
+
#### :bug: Bug Fixes
|
|
317
|
+
|
|
318
|
+
- fix: disable unavailable navigator guide options [kreafox - [`4b114c2`](https://github.com/eea/volto-cca-policy/commit/4b114c2b0ea7a6a6528ae8ae378c2e671b640945)]
|
|
319
|
+
|
|
320
|
+
#### :nail_care: Enhancements
|
|
321
|
+
|
|
322
|
+
- change: add custom hook for guide facet options and add tests [kreafox - [`872eb9b`](https://github.com/eea/volto-cca-policy/commit/872eb9b35c41fc2094c605b113411448a8cef9a8)]
|
|
323
|
+
|
|
324
|
+
#### :hammer_and_wrench: Others
|
|
325
|
+
|
|
326
|
+
- Refs #307245 - Visualizations: order and descriptions. [GhitaB - [`5fff846`](https://github.com/eea/volto-cca-policy/commit/5fff846b623250e89cbf2a844828f545c0f2f9f9)]
|
|
327
|
+
- test: increase utility coverage [kreafox - [`d9c3db8`](https://github.com/eea/volto-cca-policy/commit/d9c3db8cc6fb91ead00f83b822f6fe48c26a70b1)]
|
|
328
|
+
### [1.0.0-alpha.24](https://github.com/eea/volto-cca-policy/compare/1.0.0-alpha.23...1.0.0-alpha.24) - 24 August 2026
|
|
292
329
|
|
|
293
330
|
### [1.0.0-alpha.23](https://github.com/eea/volto-cca-policy/compare/1.0.0-alpha.22...1.0.0-alpha.23) - 19 August 2026
|
|
294
331
|
|
package/package.json
CHANGED
|
@@ -8,6 +8,8 @@ import URLManager from '@elastic/search-ui/lib/cjs/URLManager';
|
|
|
8
8
|
import { useSearchContext } from '@eeacms/search/lib/hocs';
|
|
9
9
|
import guideSteps from '../../../search/navigator_guide/guideSteps';
|
|
10
10
|
import { navigatorGuideStepAtom } from '../../../state';
|
|
11
|
+
import { mergeGuideOptions } from './utils';
|
|
12
|
+
import useGuideFacetOptions from './useGuideFacetOptions';
|
|
11
13
|
|
|
12
14
|
const messages = defineMessages({
|
|
13
15
|
noSteps: {
|
|
@@ -111,6 +113,7 @@ const NavigatorGuideContentView = ({ appConfig }) => {
|
|
|
111
113
|
totalResults,
|
|
112
114
|
} = searchContext;
|
|
113
115
|
const steps = guideSteps;
|
|
116
|
+
const allFacetOptions = useGuideFacetOptions(appConfig, steps);
|
|
114
117
|
const [storedActiveStep, setActiveStep] = useAtom(navigatorGuideStepAtom);
|
|
115
118
|
const activeStep =
|
|
116
119
|
Number.isInteger(storedActiveStep) &&
|
|
@@ -122,11 +125,16 @@ const NavigatorGuideContentView = ({ appConfig }) => {
|
|
|
122
125
|
const selectedValues =
|
|
123
126
|
(filters || []).find((filter) => filter.field === step?.field)?.values ||
|
|
124
127
|
[];
|
|
125
|
-
const options = getFacetOptions(facets, step?.field);
|
|
126
|
-
const isLastStep = activeStep === steps.length - 1;
|
|
127
128
|
const hasSelections = steps.some(({ field }) =>
|
|
128
129
|
isStepSelected(filters, field),
|
|
129
130
|
);
|
|
131
|
+
const options = mergeGuideOptions(
|
|
132
|
+
allFacetOptions?.[step?.field],
|
|
133
|
+
getFacetOptions(facets, step?.field),
|
|
134
|
+
selectedValues,
|
|
135
|
+
hasSelections,
|
|
136
|
+
);
|
|
137
|
+
const isLastStep = activeStep === steps.length - 1;
|
|
130
138
|
const selectedStepLabels = steps
|
|
131
139
|
.filter(({ field }) => isStepSelected(filters, field))
|
|
132
140
|
.map(({ label }) =>
|
|
@@ -262,10 +270,12 @@ const NavigatorGuideContentView = ({ appConfig }) => {
|
|
|
262
270
|
key={option.value}
|
|
263
271
|
className={`navigator-guide-option${
|
|
264
272
|
selectedValues.includes(option.value) ? ' selected' : ''
|
|
265
|
-
}`}
|
|
273
|
+
}${option.disabled ? ' disabled' : ''}`}
|
|
274
|
+
aria-disabled={option.disabled || undefined}
|
|
266
275
|
>
|
|
267
276
|
<Checkbox
|
|
268
277
|
checked={selectedValues.includes(option.value)}
|
|
278
|
+
disabled={option.disabled}
|
|
269
279
|
onChange={() => toggleValue(option.value)}
|
|
270
280
|
/>
|
|
271
281
|
<span>{option.value}</span>
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { getFacetOptions } from '@eeacms/search/components/SearchApp/useFacetsWithAllOptions';
|
|
3
|
+
|
|
4
|
+
const useGuideFacetOptions = (appConfig, steps) => {
|
|
5
|
+
const [allFacetOptions, setAllFacetOptions] = React.useState({});
|
|
6
|
+
|
|
7
|
+
React.useEffect(() => {
|
|
8
|
+
let isCurrent = true;
|
|
9
|
+
const stepFields = steps.map(({ field }) => field);
|
|
10
|
+
|
|
11
|
+
getFacetOptions(appConfig, stepFields)
|
|
12
|
+
.then((facetOptions) => {
|
|
13
|
+
if (isCurrent) setAllFacetOptions(facetOptions);
|
|
14
|
+
})
|
|
15
|
+
.catch(() => {
|
|
16
|
+
// Live facet options remain available if the supplementary request fails.
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
return () => {
|
|
20
|
+
isCurrent = false;
|
|
21
|
+
};
|
|
22
|
+
}, [appConfig, steps]);
|
|
23
|
+
|
|
24
|
+
return allFacetOptions;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export default useGuideFacetOptions;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { act, renderHook } from '@testing-library/react-hooks';
|
|
2
|
+
import { getFacetOptions } from '@eeacms/search/components/SearchApp/useFacetsWithAllOptions';
|
|
3
|
+
import useGuideFacetOptions from './useGuideFacetOptions';
|
|
4
|
+
|
|
5
|
+
jest.mock(
|
|
6
|
+
'@eeacms/search/components/SearchApp/useFacetsWithAllOptions',
|
|
7
|
+
() => ({ getFacetOptions: jest.fn() }),
|
|
8
|
+
);
|
|
9
|
+
|
|
10
|
+
const flushPromises = () => act(() => Promise.resolve());
|
|
11
|
+
|
|
12
|
+
describe('useGuideFacetOptions', () => {
|
|
13
|
+
const appConfig = { facets: [] };
|
|
14
|
+
const steps = [{ field: 'sector' }, { field: 'stage' }];
|
|
15
|
+
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
getFacetOptions.mockReset();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('loads the complete option list for every guide field', async () => {
|
|
21
|
+
const facetOptions = { sector: ['Energy'], stage: ['Step 1'] };
|
|
22
|
+
getFacetOptions.mockResolvedValue(facetOptions);
|
|
23
|
+
|
|
24
|
+
const { result } = renderHook(() => useGuideFacetOptions(appConfig, steps));
|
|
25
|
+
await flushPromises();
|
|
26
|
+
|
|
27
|
+
expect(getFacetOptions).toHaveBeenCalledWith(appConfig, [
|
|
28
|
+
'sector',
|
|
29
|
+
'stage',
|
|
30
|
+
]);
|
|
31
|
+
expect(result.current).toEqual(facetOptions);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('keeps the fallback state when loading fails', async () => {
|
|
35
|
+
getFacetOptions.mockRejectedValue(new Error('Search unavailable'));
|
|
36
|
+
|
|
37
|
+
const { result } = renderHook(() => useGuideFacetOptions(appConfig, steps));
|
|
38
|
+
await flushPromises();
|
|
39
|
+
|
|
40
|
+
expect(result.current).toEqual({});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('does not update state after unmounting', async () => {
|
|
44
|
+
let resolveRequest;
|
|
45
|
+
getFacetOptions.mockReturnValue(
|
|
46
|
+
new Promise((resolve) => {
|
|
47
|
+
resolveRequest = resolve;
|
|
48
|
+
}),
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
const { unmount } = renderHook(() =>
|
|
52
|
+
useGuideFacetOptions(appConfig, steps),
|
|
53
|
+
);
|
|
54
|
+
unmount();
|
|
55
|
+
resolveRequest({ sector: ['Energy'] });
|
|
56
|
+
await flushPromises();
|
|
57
|
+
});
|
|
58
|
+
});
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export const mergeGuideOptions = (
|
|
2
|
+
availableValues = [],
|
|
3
|
+
facetOptions = [],
|
|
4
|
+
selectedValues = [],
|
|
5
|
+
hasGuideSelections = false,
|
|
6
|
+
) => {
|
|
7
|
+
const optionsByValue = new Map(
|
|
8
|
+
facetOptions.map(({ value, count }) => [value, { value, count }]),
|
|
9
|
+
);
|
|
10
|
+
|
|
11
|
+
availableValues.forEach((value) => {
|
|
12
|
+
if (!optionsByValue.has(value)) {
|
|
13
|
+
optionsByValue.set(value, { value, count: 0 });
|
|
14
|
+
}
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
selectedValues.forEach((value) => {
|
|
18
|
+
if (!optionsByValue.has(value)) {
|
|
19
|
+
optionsByValue.set(value, { value, count: 0 });
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
return Array.from(optionsByValue.values()).map((option) => ({
|
|
24
|
+
...option,
|
|
25
|
+
disabled:
|
|
26
|
+
hasGuideSelections &&
|
|
27
|
+
option.count === 0 &&
|
|
28
|
+
!selectedValues.includes(option.value),
|
|
29
|
+
}));
|
|
30
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { mergeGuideOptions } from './utils';
|
|
2
|
+
|
|
3
|
+
describe('Navigator Guide utilities', () => {
|
|
4
|
+
it('keeps unavailable values and marks them as disabled', () => {
|
|
5
|
+
expect(
|
|
6
|
+
mergeGuideOptions(
|
|
7
|
+
['Step 1', 'Step 2', 'Step 6'],
|
|
8
|
+
[
|
|
9
|
+
{ value: 'Step 1', count: 4 },
|
|
10
|
+
{ value: 'Step 2', count: 2 },
|
|
11
|
+
],
|
|
12
|
+
[],
|
|
13
|
+
true,
|
|
14
|
+
),
|
|
15
|
+
).toEqual([
|
|
16
|
+
{ value: 'Step 1', count: 4, disabled: false },
|
|
17
|
+
{ value: 'Step 2', count: 2, disabled: false },
|
|
18
|
+
{ value: 'Step 6', count: 0, disabled: true },
|
|
19
|
+
]);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('keeps a selected zero-count value enabled so it can be removed', () => {
|
|
23
|
+
expect(mergeGuideOptions([], [], ['Step 6'], true)).toEqual([
|
|
24
|
+
{ value: 'Step 6', count: 0, disabled: false },
|
|
25
|
+
]);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('keeps every value enabled before the guide has been refined', () => {
|
|
29
|
+
expect(mergeGuideOptions(['Step 1', 'Step 6'], [], [])).toEqual([
|
|
30
|
+
{ value: 'Step 1', count: 0, disabled: false },
|
|
31
|
+
{ value: 'Step 6', count: 0, disabled: false },
|
|
32
|
+
]);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('falls back to live facet options while the full list is loading', () => {
|
|
36
|
+
expect(mergeGuideOptions([], [{ value: 'Energy', count: 3 }], [])).toEqual([
|
|
37
|
+
{ value: 'Energy', count: 3, disabled: false },
|
|
38
|
+
]);
|
|
39
|
+
});
|
|
40
|
+
});
|
|
@@ -7,6 +7,7 @@ import './visualizations.less';
|
|
|
7
7
|
|
|
8
8
|
const emptyVisualization = {
|
|
9
9
|
title: '',
|
|
10
|
+
description: '',
|
|
10
11
|
embed_code: '',
|
|
11
12
|
height: '',
|
|
12
13
|
full_width: false,
|
|
@@ -43,6 +44,19 @@ const VisualizationsWidget = (props) => {
|
|
|
43
44
|
);
|
|
44
45
|
};
|
|
45
46
|
|
|
47
|
+
const moveItem = (index, direction) => {
|
|
48
|
+
const nextIndex = index + direction;
|
|
49
|
+
|
|
50
|
+
if (nextIndex < 0 || nextIndex >= visualizations.length) return;
|
|
51
|
+
|
|
52
|
+
const nextVisualizations = [...visualizations];
|
|
53
|
+
[nextVisualizations[index], nextVisualizations[nextIndex]] = [
|
|
54
|
+
nextVisualizations[nextIndex],
|
|
55
|
+
nextVisualizations[index],
|
|
56
|
+
];
|
|
57
|
+
updateVisualizations(nextVisualizations);
|
|
58
|
+
};
|
|
59
|
+
|
|
46
60
|
return (
|
|
47
61
|
<FormFieldWrapper {...props}>
|
|
48
62
|
<div className="visualizations-widget">
|
|
@@ -53,16 +67,30 @@ const VisualizationsWidget = (props) => {
|
|
|
53
67
|
>
|
|
54
68
|
<div className="visualizations-widget-item-header">
|
|
55
69
|
<h4>Visualization {index + 1}</h4>
|
|
56
|
-
<Button
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
70
|
+
<Button.Group basic compact>
|
|
71
|
+
<Button
|
|
72
|
+
type="button"
|
|
73
|
+
disabled={isDisabled || index === 0}
|
|
74
|
+
onClick={() => moveItem(index, -1)}
|
|
75
|
+
>
|
|
76
|
+
Up
|
|
77
|
+
</Button>
|
|
78
|
+
<Button
|
|
79
|
+
type="button"
|
|
80
|
+
disabled={isDisabled || index === visualizations.length - 1}
|
|
81
|
+
onClick={() => moveItem(index, 1)}
|
|
82
|
+
>
|
|
83
|
+
Down
|
|
84
|
+
</Button>
|
|
85
|
+
<Button
|
|
86
|
+
type="button"
|
|
87
|
+
negative
|
|
88
|
+
disabled={isDisabled}
|
|
89
|
+
onClick={() => deleteItem(index)}
|
|
90
|
+
>
|
|
91
|
+
Delete
|
|
92
|
+
</Button>
|
|
93
|
+
</Button.Group>
|
|
66
94
|
</div>
|
|
67
95
|
|
|
68
96
|
<Form.Field>
|
|
@@ -77,6 +105,19 @@ const VisualizationsWidget = (props) => {
|
|
|
77
105
|
/>
|
|
78
106
|
</Form.Field>
|
|
79
107
|
|
|
108
|
+
<Form.Field>
|
|
109
|
+
<label htmlFor={`${id}-${index}-description`}>Description</label>
|
|
110
|
+
<TextArea
|
|
111
|
+
id={`${id}-${index}-description`}
|
|
112
|
+
value={item.description || ''}
|
|
113
|
+
disabled={isDisabled}
|
|
114
|
+
rows={3}
|
|
115
|
+
onChange={(event) =>
|
|
116
|
+
updateItem(index, 'description', event.target.value)
|
|
117
|
+
}
|
|
118
|
+
/>
|
|
119
|
+
</Form.Field>
|
|
120
|
+
|
|
80
121
|
<Form.Field>
|
|
81
122
|
<label htmlFor={`${id}-${index}-embed-code`}>
|
|
82
123
|
Embed code or URL
|
|
@@ -107,6 +107,9 @@ const MaybeIframeVisualization = ({ visualization }) => {
|
|
|
107
107
|
const Visualization = ({ visualization }) => (
|
|
108
108
|
<>
|
|
109
109
|
{visualization?.title && <h2>{visualization.title}</h2>}
|
|
110
|
+
{visualization?.description && (
|
|
111
|
+
<p className="visualization-description">{visualization.description}</p>
|
|
112
|
+
)}
|
|
110
113
|
<MaybeFlourishVisualization visualization={visualization} />
|
|
111
114
|
<MaybeIframeVisualization visualization={visualization} />
|
|
112
115
|
</>
|
|
@@ -129,6 +132,20 @@ const getVisualizations = (content) => {
|
|
|
129
132
|
: [];
|
|
130
133
|
};
|
|
131
134
|
|
|
135
|
+
const groupVisualizations = (visualizations) =>
|
|
136
|
+
visualizations.reduce((groups, visualization) => {
|
|
137
|
+
const fullWidth = !!visualization.full_width;
|
|
138
|
+
const lastGroup = groups[groups.length - 1];
|
|
139
|
+
|
|
140
|
+
if (lastGroup && lastGroup.fullWidth === fullWidth) {
|
|
141
|
+
lastGroup.items.push(visualization);
|
|
142
|
+
} else {
|
|
143
|
+
groups.push({ fullWidth, items: [visualization] });
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return groups;
|
|
147
|
+
}, []);
|
|
148
|
+
|
|
132
149
|
const BottomInfo = (props) => {
|
|
133
150
|
const { content } = props;
|
|
134
151
|
const { organisational_websites, organisational_contact_information } =
|
|
@@ -185,13 +202,11 @@ const DatabaseItemView = (props) => {
|
|
|
185
202
|
const item_title = acronym ? title + ' (' + acronym + ')' : title;
|
|
186
203
|
const subtitle = CONTENT_TYPE_LABELS[type] ?? '';
|
|
187
204
|
const visualizations = getVisualizations(content);
|
|
188
|
-
const
|
|
189
|
-
|
|
205
|
+
const visualizationGroups = groupVisualizations(visualizations);
|
|
206
|
+
const firstVisualizationGroup = visualizationGroups[0];
|
|
207
|
+
const hasFullWidthVisualizations = visualizationGroups.some(
|
|
208
|
+
(group) => group.fullWidth,
|
|
190
209
|
);
|
|
191
|
-
const fullWidthVisualizations = visualizations.filter(
|
|
192
|
-
(visualization) => visualization.full_width,
|
|
193
|
-
);
|
|
194
|
-
const hasFullWidthVisualizations = !!fullWidthVisualizations.length;
|
|
195
210
|
|
|
196
211
|
const is_cmshare_video = SHARE_EEA.some((domain) =>
|
|
197
212
|
content?.embed_url?.includes(domain),
|
|
@@ -284,9 +299,12 @@ const DatabaseItemView = (props) => {
|
|
|
284
299
|
</>
|
|
285
300
|
)}
|
|
286
301
|
|
|
287
|
-
{
|
|
288
|
-
|
|
289
|
-
|
|
302
|
+
{firstVisualizationGroup &&
|
|
303
|
+
!firstVisualizationGroup.fullWidth && (
|
|
304
|
+
<Visualizations
|
|
305
|
+
visualizations={firstVisualizationGroup.items}
|
|
306
|
+
/>
|
|
307
|
+
)}
|
|
290
308
|
|
|
291
309
|
{!hasFullWidthVisualizations && <BottomInfo {...props} />}
|
|
292
310
|
</Grid.Column>
|
|
@@ -304,11 +322,22 @@ const DatabaseItemView = (props) => {
|
|
|
304
322
|
|
|
305
323
|
{hasFullWidthVisualizations && (
|
|
306
324
|
<>
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
325
|
+
{visualizationGroups.map((group, index) => {
|
|
326
|
+
if (index === 0 && !group.fullWidth) return null;
|
|
327
|
+
|
|
328
|
+
return (
|
|
329
|
+
<Grid.Row key={`visualization-group-${index}`}>
|
|
330
|
+
<Grid.Column
|
|
331
|
+
mobile={12}
|
|
332
|
+
tablet={12}
|
|
333
|
+
computer={group.fullWidth ? 12 : 8}
|
|
334
|
+
className={group.fullWidth ? undefined : 'col-left'}
|
|
335
|
+
>
|
|
336
|
+
<Visualizations visualizations={group.items} />
|
|
337
|
+
</Grid.Column>
|
|
338
|
+
</Grid.Row>
|
|
339
|
+
);
|
|
340
|
+
})}
|
|
312
341
|
<Grid.Row>
|
|
313
342
|
<Grid.Column
|
|
314
343
|
mobile={12}
|
|
@@ -46,6 +46,7 @@ const visualizationEmbedCode =
|
|
|
46
46
|
|
|
47
47
|
const getVisualization = (overrides = {}) => ({
|
|
48
48
|
title: '',
|
|
49
|
+
description: '',
|
|
49
50
|
embed_code: visualizationEmbedCode,
|
|
50
51
|
height: '',
|
|
51
52
|
full_width: false,
|
|
@@ -125,6 +126,39 @@ describe('DatabaseItemView', () => {
|
|
|
125
126
|
expect(fullWidthColumn.findAllByType('iframe')).toHaveLength(1);
|
|
126
127
|
});
|
|
127
128
|
|
|
129
|
+
it('renders visualization descriptions', () => {
|
|
130
|
+
const component = renderDatabaseItemView({
|
|
131
|
+
...baseContent,
|
|
132
|
+
visualizations: [
|
|
133
|
+
getVisualization({ description: 'Visualization summary text.' }),
|
|
134
|
+
],
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
expect(
|
|
138
|
+
component.root.findByProps({
|
|
139
|
+
className: 'visualization-description',
|
|
140
|
+
}).children,
|
|
141
|
+
).toEqual(['Visualization summary text.']);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('preserves visualization order when full-width and column items are mixed', () => {
|
|
145
|
+
const component = renderDatabaseItemView({
|
|
146
|
+
...baseContent,
|
|
147
|
+
visualizations: [
|
|
148
|
+
getVisualization({ title: 'First', full_width: true }),
|
|
149
|
+
getVisualization({ title: 'Second' }),
|
|
150
|
+
getVisualization({ title: 'Third', full_width: true }),
|
|
151
|
+
],
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
const headings = component.root
|
|
155
|
+
.findAllByType('h2')
|
|
156
|
+
.map((heading) => heading.children.join(''))
|
|
157
|
+
.filter((title) => ['First', 'Second', 'Third'].includes(title));
|
|
158
|
+
|
|
159
|
+
expect(headings).toEqual(['First', 'Second', 'Third']);
|
|
160
|
+
});
|
|
161
|
+
|
|
128
162
|
it('does not render a visualization when the visualizations field is empty', () => {
|
|
129
163
|
const component = renderDatabaseItemView({
|
|
130
164
|
...baseContent,
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { flattenToAppURL } from '@plone/volto/helpers/Url/Url';
|
|
2
|
+
import {
|
|
3
|
+
blockAvailableInMission,
|
|
4
|
+
extractPlanNameAndURL,
|
|
5
|
+
filterBlocks,
|
|
6
|
+
formatTextToHTML,
|
|
7
|
+
getBaseUrl,
|
|
8
|
+
getFilteredBlocks,
|
|
9
|
+
hasTypeOfBlock,
|
|
10
|
+
isEmpty,
|
|
11
|
+
normalizeImageFileName,
|
|
12
|
+
} from './utils';
|
|
13
|
+
|
|
14
|
+
jest.mock('@plone/volto/helpers/Url/Url', () => ({
|
|
15
|
+
flattenToAppURL: jest.fn((url) => url.replace('https://example.com', '')),
|
|
16
|
+
}));
|
|
17
|
+
|
|
18
|
+
describe('general utilities', () => {
|
|
19
|
+
it('checks whether blocks are available inside and outside Mission', () => {
|
|
20
|
+
expect(blockAvailableInMission({}, { id: 'text' })).toBe(false);
|
|
21
|
+
expect(
|
|
22
|
+
blockAvailableInMission(
|
|
23
|
+
{ '@id': 'https://example.com/en/mission/page' },
|
|
24
|
+
{ id: 'mkh_map' },
|
|
25
|
+
),
|
|
26
|
+
).toBe(false);
|
|
27
|
+
expect(
|
|
28
|
+
blockAvailableInMission(
|
|
29
|
+
{ '@id': 'https://example.com/en/mission/page' },
|
|
30
|
+
{ id: 'text' },
|
|
31
|
+
),
|
|
32
|
+
).toBe(true);
|
|
33
|
+
expect(
|
|
34
|
+
blockAvailableInMission(
|
|
35
|
+
{ '@id': 'https://example.com/en/page' },
|
|
36
|
+
{ id: 'rastBlock' },
|
|
37
|
+
),
|
|
38
|
+
).toBe(true);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('resolves a block base URL from the supported properties', () => {
|
|
42
|
+
expect(
|
|
43
|
+
getBaseUrl({ data: { href: [{ '@id': 'https://example.com/page' }] } }),
|
|
44
|
+
).toBe('/page');
|
|
45
|
+
expect(getBaseUrl({ path: '/from-path' })).toBe('/from-path');
|
|
46
|
+
expect(getBaseUrl({ location: { pathname: '/from-location' } })).toBe(
|
|
47
|
+
'/from-location',
|
|
48
|
+
);
|
|
49
|
+
expect(getBaseUrl({})).toBe('');
|
|
50
|
+
expect(flattenToAppURL).toHaveBeenCalledTimes(3);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('finds nested block types and removes selected block types', () => {
|
|
54
|
+
expect(
|
|
55
|
+
hasTypeOfBlock({ blocks: { child: { '@type': 'listing' } } }, 'listing'),
|
|
56
|
+
).toBe(true);
|
|
57
|
+
expect(hasTypeOfBlock({ child: null }, 'listing')).toBe(false);
|
|
58
|
+
|
|
59
|
+
expect(
|
|
60
|
+
filterBlocks(
|
|
61
|
+
{
|
|
62
|
+
blocks: {
|
|
63
|
+
one: { '@type': 'text' },
|
|
64
|
+
two: { '@type': 'listing' },
|
|
65
|
+
},
|
|
66
|
+
blocks_layout: { items: ['one', 'two'] },
|
|
67
|
+
},
|
|
68
|
+
['listing'],
|
|
69
|
+
),
|
|
70
|
+
).toEqual({
|
|
71
|
+
blocks: { one: { '@type': 'text' } },
|
|
72
|
+
blocks_layout: { items: ['one'] },
|
|
73
|
+
hasBlockTypes: true,
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('formats escaped text, paragraphs, bullets, and links as HTML', () => {
|
|
78
|
+
expect(formatTextToHTML()).toBe('');
|
|
79
|
+
expect(formatTextToHTML('Simple text')).toBe('<p>Simple text</p>');
|
|
80
|
+
expect(formatTextToHTML('First\\n\\nSecond')).toBe('First</p><p>Second');
|
|
81
|
+
expect(formatTextToHTML('Item\\no next')).toBe('<p>Item<br />• next</p>');
|
|
82
|
+
expect(formatTextToHTML('See https://example.com')).toContain(
|
|
83
|
+
'<a href="https://example.com"',
|
|
84
|
+
);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('keeps only parent blocks without the excluded nested type', () => {
|
|
88
|
+
const result = getFilteredBlocks(
|
|
89
|
+
{
|
|
90
|
+
blocks: {
|
|
91
|
+
keep: { '@type': 'group', blocks: { child: { '@type': 'text' } } },
|
|
92
|
+
exclude: {
|
|
93
|
+
'@type': 'group',
|
|
94
|
+
data: { blocks: { child: { '@type': 'map' } } },
|
|
95
|
+
},
|
|
96
|
+
other: { '@type': 'text' },
|
|
97
|
+
},
|
|
98
|
+
blocks_layout: { items: ['keep', 'exclude', 'other'] },
|
|
99
|
+
},
|
|
100
|
+
'group',
|
|
101
|
+
'map',
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
expect(result.keptKeys).toEqual(['keep']);
|
|
105
|
+
expect(result.blocks_layout.items).toEqual(['keep']);
|
|
106
|
+
expect(result.hasMatches).toBe(true);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('extracts plan links and normalizes common values', () => {
|
|
110
|
+
expect(extractPlanNameAndURL()).toEqual({ name: '', url: '' });
|
|
111
|
+
expect(
|
|
112
|
+
extractPlanNameAndURL('Adaptation plan (https://example.com/plan)'),
|
|
113
|
+
).toEqual({ name: 'Adaptation plan', url: 'https://example.com/plan' });
|
|
114
|
+
expect(
|
|
115
|
+
extractPlanNameAndURL(
|
|
116
|
+
'Plan https://example.com/source https://example.com/document',
|
|
117
|
+
),
|
|
118
|
+
).toEqual({ name: 'Plan', url: 'https://example.com/document' });
|
|
119
|
+
|
|
120
|
+
expect(isEmpty()).toBe(true);
|
|
121
|
+
expect(isEmpty([])).toBe(true);
|
|
122
|
+
expect(isEmpty(['value'])).toBe(false);
|
|
123
|
+
expect(normalizeImageFileName()).toBe('');
|
|
124
|
+
expect(normalizeImageFileName('map(final).png')).toBe('map-final.png');
|
|
125
|
+
expect(normalizeImageFileName('map(final)')).toBe('map-final-');
|
|
126
|
+
});
|
|
127
|
+
});
|
|
@@ -1004,6 +1004,12 @@ body.searchlib-page .searchapp-navigatorCatalogueSearch {
|
|
|
1004
1004
|
background: fade(@primaryColor, 5%);
|
|
1005
1005
|
}
|
|
1006
1006
|
|
|
1007
|
+
&.disabled {
|
|
1008
|
+
background: #f5f5f5;
|
|
1009
|
+
color: @navigatorInactiveColor;
|
|
1010
|
+
opacity: 0.65;
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1007
1013
|
> span {
|
|
1008
1014
|
flex: 1;
|
|
1009
1015
|
}
|