@eeacms/volto-clms-theme 1.1.44 → 1.1.46
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 +17 -2
- package/package.json +1 -1
- package/src/components/CLMSDatasetDetailView/DataSetInfoContent.jsx +1 -1
- package/src/components/CLMSDatasetDetailView/DataSetInfoContent.test.jsx +10 -20
- package/src/components/CLMSDatasetDetailView/MetadataPaginatedListing.test.jsx +42 -0
- package/src/components/CLMSDatasetDetailView/{RelatedNews.jsx → RelatedItems.jsx} +12 -5
- package/src/components/CLMSDatasetDetailView/RelatedItems.test.jsx +82 -0
- package/src/components/CLMSDatasetDetailView/index.js +1 -2
- package/src/components/CclCard/CclCard.test.jsx +34 -34
- package/src/customizations/volto/components/manage/Blocks/Search/hocs/withSearch.jsx +414 -0
- package/src/components/CLMSDatasetDetailView/RelatedUseCases.jsx +0 -51
package/CHANGELOG.md
CHANGED
|
@@ -4,11 +4,26 @@ 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.1.
|
|
7
|
+
### [1.1.46](https://github.com/eea/volto-clms-theme/compare/1.1.45...1.1.46) - 20 September 2023
|
|
8
|
+
|
|
9
|
+
#### :bug: Bug Fixes
|
|
10
|
+
|
|
11
|
+
- fix: search block facets [Ion Lizarazu - [`61f8a64`](https://github.com/eea/volto-clms-theme/commit/61f8a640c4fcec06a08245e5ed74caa8b2011922)]
|
|
12
|
+
|
|
13
|
+
### [1.1.45](https://github.com/eea/volto-clms-theme/compare/1.1.44...1.1.45) - 20 September 2023
|
|
8
14
|
|
|
9
15
|
#### :bug: Bug Fixes
|
|
10
16
|
|
|
11
|
-
- fix:
|
|
17
|
+
- fix: RelatedNews test params [Ion Lizarazu - [`2980a78`](https://github.com/eea/volto-clms-theme/commit/2980a78eb94c3295bbf38f40e27bff4188c6cac2)]
|
|
18
|
+
- fix: sonarqube [Ion Lizarazu - [`9e84298`](https://github.com/eea/volto-clms-theme/commit/9e84298ca5128e33c8d54adab719965a49515042)]
|
|
19
|
+
|
|
20
|
+
#### :hammer_and_wrench: Others
|
|
21
|
+
|
|
22
|
+
- remove console statement [Ion Lizarazu - [`4e67ca2`](https://github.com/eea/volto-clms-theme/commit/4e67ca27a605c944c37674d30d1fb3adc73cb3df)]
|
|
23
|
+
- jest tests [Ion Lizarazu - [`29f9d35`](https://github.com/eea/volto-clms-theme/commit/29f9d351e6b9ebdcb4d81a2d654a0782cd8af855)]
|
|
24
|
+
- unused code removed and add tests [Ion Lizarazu - [`51f6856`](https://github.com/eea/volto-clms-theme/commit/51f685623c2148138ee3780e07b369a9a8b2c09a)]
|
|
25
|
+
- add jest test [Ion Lizarazu - [`159792c`](https://github.com/eea/volto-clms-theme/commit/159792c28b3d8bc903111708b1664704ec0fed5e)]
|
|
26
|
+
### [1.1.44](https://github.com/eea/volto-clms-theme/compare/1.1.43...1.1.44) - 19 September 2023
|
|
12
27
|
|
|
13
28
|
### [1.1.43](https://github.com/eea/volto-clms-theme/compare/1.1.42...1.1.43) - 19 September 2023
|
|
14
29
|
|
package/package.json
CHANGED
|
@@ -141,7 +141,7 @@ const DataSetInfoContent = (props) => {
|
|
|
141
141
|
<Accordion.Title
|
|
142
142
|
as={'h2'}
|
|
143
143
|
onClick={() => handleClick({ index: 99 })}
|
|
144
|
-
active={activeIndex
|
|
144
|
+
active={activeIndex.includes(99)}
|
|
145
145
|
index={99}
|
|
146
146
|
className={'accordion-title align-arrow-right'}
|
|
147
147
|
>
|
|
@@ -19,41 +19,31 @@ describe('DataSetInfoContent', () => {
|
|
|
19
19
|
messages: {},
|
|
20
20
|
},
|
|
21
21
|
search: {
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
subrequests: {
|
|
23
|
+
datasetinfocontentid: { items: [] },
|
|
24
24
|
},
|
|
25
25
|
},
|
|
26
26
|
});
|
|
27
27
|
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
description: 'example description',
|
|
32
|
-
tooltip: 'example tooltip',
|
|
33
|
-
data: 'Resource Abstract example',
|
|
34
|
-
},
|
|
35
|
-
dataSources: {
|
|
36
|
-
title: 'example title',
|
|
37
|
-
description: 'example description',
|
|
38
|
-
tooltip: 'example tooltip',
|
|
39
|
-
data: 'Source data',
|
|
40
|
-
},
|
|
41
|
-
dataResourceLocator: 'Resource locator',
|
|
42
|
-
dataResourceTitle: 'example dataResourceTitle',
|
|
28
|
+
const props = {
|
|
29
|
+
UID: 'gfdjgf8edgtrh789',
|
|
30
|
+
id: 'datasetinfocontentid',
|
|
43
31
|
validation: {
|
|
44
32
|
title: 'example title',
|
|
45
33
|
description: 'example description',
|
|
46
34
|
tooltip: 'example tooltip',
|
|
47
35
|
data: 'Validation',
|
|
48
36
|
},
|
|
37
|
+
geonetwork_identifiers: {
|
|
38
|
+
items: [],
|
|
39
|
+
},
|
|
40
|
+
data: { right_arrows: true },
|
|
49
41
|
};
|
|
50
42
|
const datasetInfo = renderer
|
|
51
43
|
.create(
|
|
52
44
|
<Provider store={store}>
|
|
53
45
|
<MemoryRouter>
|
|
54
|
-
<DataSetInfoContent props
|
|
55
|
-
<p>Dataset info view test</p>
|
|
56
|
-
</DataSetInfoContent>
|
|
46
|
+
<DataSetInfoContent {...props} />
|
|
57
47
|
</MemoryRouter>
|
|
58
48
|
</Provider>,
|
|
59
49
|
)
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import renderer from 'react-test-renderer';
|
|
3
|
+
import { MetadataPaginatedListing } from './MetadataPaginatedListing';
|
|
4
|
+
import { MemoryRouter } from 'react-router-dom';
|
|
5
|
+
import configureStore from 'redux-mock-store';
|
|
6
|
+
import { Provider } from 'react-redux';
|
|
7
|
+
|
|
8
|
+
describe('MetadataPaginatedListing', () => {
|
|
9
|
+
it('Check MetadataPaginatedListing view', () => {
|
|
10
|
+
const mockStore = configureStore();
|
|
11
|
+
|
|
12
|
+
const store = mockStore({
|
|
13
|
+
intl: {
|
|
14
|
+
locale: 'en',
|
|
15
|
+
messages: {},
|
|
16
|
+
},
|
|
17
|
+
search: {
|
|
18
|
+
subrequests: {
|
|
19
|
+
datasetinfocontentid: { items: [] },
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
const props = {
|
|
25
|
+
id: 'metadata-paginated-listing',
|
|
26
|
+
geonetwork_identifiers_items: [
|
|
27
|
+
{ id: 'geo-eea-id', title: 'Geo title', type: 'EEA' },
|
|
28
|
+
{ id: 'geo-vito-id', title: 'Geo title 2', type: 'VITO' },
|
|
29
|
+
],
|
|
30
|
+
};
|
|
31
|
+
const datasetInfo = renderer
|
|
32
|
+
.create(
|
|
33
|
+
<Provider store={store}>
|
|
34
|
+
<MemoryRouter>
|
|
35
|
+
<MetadataPaginatedListing {...props} />
|
|
36
|
+
</MemoryRouter>
|
|
37
|
+
</Provider>,
|
|
38
|
+
)
|
|
39
|
+
.toJSON();
|
|
40
|
+
expect(datasetInfo).toBeDefined();
|
|
41
|
+
});
|
|
42
|
+
});
|
|
@@ -6,21 +6,27 @@ import React from 'react';
|
|
|
6
6
|
import { searchContent } from '@plone/volto/actions';
|
|
7
7
|
import { useLocation } from 'react-router-dom';
|
|
8
8
|
|
|
9
|
-
const
|
|
9
|
+
const RelatedItems = (props) => {
|
|
10
10
|
const dispatch = useDispatch();
|
|
11
|
-
const { UID, id } = props;
|
|
11
|
+
const { UID, id, type } = props;
|
|
12
|
+
let hash = '';
|
|
13
|
+
if (type === 'News Item') {
|
|
14
|
+
hash = '#News';
|
|
15
|
+
} else if (type === 'UseCase') {
|
|
16
|
+
hash = '#Use-cases';
|
|
17
|
+
}
|
|
12
18
|
const searchSubrequests = useSelector((state) => state.search.subrequests);
|
|
13
19
|
let libraries = searchSubrequests?.[id]?.items || [];
|
|
14
20
|
let librariesPending = searchSubrequests?.[id]?.loading;
|
|
15
21
|
const location = useLocation();
|
|
16
22
|
React.useEffect(() => {
|
|
17
|
-
if (location.hash ===
|
|
23
|
+
if (location.hash === hash && UID && hash) {
|
|
18
24
|
dispatch(
|
|
19
25
|
searchContent(
|
|
20
26
|
'',
|
|
21
27
|
{
|
|
22
28
|
fullobjects: 1,
|
|
23
|
-
portal_type:
|
|
29
|
+
portal_type: type,
|
|
24
30
|
path: '/',
|
|
25
31
|
associated_datasets: UID,
|
|
26
32
|
},
|
|
@@ -28,6 +34,7 @@ const RelatedNews = (props) => {
|
|
|
28
34
|
),
|
|
29
35
|
);
|
|
30
36
|
}
|
|
37
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
31
38
|
}, [id, UID, dispatch, location]);
|
|
32
39
|
|
|
33
40
|
return (
|
|
@@ -48,4 +55,4 @@ const RelatedNews = (props) => {
|
|
|
48
55
|
);
|
|
49
56
|
};
|
|
50
57
|
|
|
51
|
-
export default
|
|
58
|
+
export default RelatedItems;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import renderer from 'react-test-renderer';
|
|
3
|
+
import RelatedItems from './RelatedItems';
|
|
4
|
+
import { MemoryRouter } from 'react-router-dom';
|
|
5
|
+
import configureStore from 'redux-mock-store';
|
|
6
|
+
import { Provider } from 'react-redux';
|
|
7
|
+
import { card } from '../CclCard/CclCard.test';
|
|
8
|
+
describe('RelatedItems', () => {
|
|
9
|
+
it('Check RelatedItems News view', () => {
|
|
10
|
+
const mockStore = configureStore();
|
|
11
|
+
|
|
12
|
+
const store = mockStore({
|
|
13
|
+
userSession: {
|
|
14
|
+
token:
|
|
15
|
+
'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJhZG1pbiIsImV4cCI6MTY0NDM4MzA0NCwiZnVsbG5hbWUiOm51bGx9.cB_q3Q0Jhu8h2m_SDmmknodpDxDLfb4o-qY6Y2plE04',
|
|
16
|
+
},
|
|
17
|
+
intl: {
|
|
18
|
+
locale: 'en',
|
|
19
|
+
messages: {},
|
|
20
|
+
},
|
|
21
|
+
search: {
|
|
22
|
+
subrequests: {
|
|
23
|
+
'related-news': { items: [card] },
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const props = {
|
|
29
|
+
id: 'related-news',
|
|
30
|
+
UID: 'uid3894032nhhlgtrjekl',
|
|
31
|
+
type: 'News Item',
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const datasetInfo = renderer
|
|
35
|
+
.create(
|
|
36
|
+
<Provider store={store}>
|
|
37
|
+
<MemoryRouter>
|
|
38
|
+
<RelatedItems {...props} />
|
|
39
|
+
</MemoryRouter>
|
|
40
|
+
</Provider>,
|
|
41
|
+
)
|
|
42
|
+
.toJSON();
|
|
43
|
+
expect(datasetInfo).toBeDefined();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('Check RelatedItems UseCase view', () => {
|
|
47
|
+
const mockStore = configureStore();
|
|
48
|
+
|
|
49
|
+
const store = mockStore({
|
|
50
|
+
userSession: {
|
|
51
|
+
token:
|
|
52
|
+
'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJhZG1pbiIsImV4cCI6MTY0NDM4MzA0NCwiZnVsbG5hbWUiOm51bGx9.cB_q3Q0Jhu8h2m_SDmmknodpDxDLfb4o-qY6Y2plE04',
|
|
53
|
+
},
|
|
54
|
+
intl: {
|
|
55
|
+
locale: 'en',
|
|
56
|
+
messages: {},
|
|
57
|
+
},
|
|
58
|
+
search: {
|
|
59
|
+
subrequests: {
|
|
60
|
+
'related-news': { items: [card] },
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
const props = {
|
|
66
|
+
id: 'related-news',
|
|
67
|
+
UID: 'uid3894032nhhlgtrjekl',
|
|
68
|
+
type: 'UseCase',
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const datasetInfo = renderer
|
|
72
|
+
.create(
|
|
73
|
+
<Provider store={store}>
|
|
74
|
+
<MemoryRouter>
|
|
75
|
+
<RelatedItems {...props} />
|
|
76
|
+
</MemoryRouter>
|
|
77
|
+
</Provider>,
|
|
78
|
+
)
|
|
79
|
+
.toJSON();
|
|
80
|
+
expect(datasetInfo).toBeDefined();
|
|
81
|
+
});
|
|
82
|
+
});
|
|
@@ -6,6 +6,39 @@ import configureStore from 'redux-mock-store';
|
|
|
6
6
|
import { Provider } from 'react-intl-redux';
|
|
7
7
|
|
|
8
8
|
const mockStore = configureStore();
|
|
9
|
+
export const card = {
|
|
10
|
+
title: 'title example',
|
|
11
|
+
description: 'description example',
|
|
12
|
+
'@type': 'News Item',
|
|
13
|
+
start: 'Wed May 19 2021 12:49:04 GMT+0200 (hora de verano de Europa central)',
|
|
14
|
+
end: 'Wed May 26 2024 12:49:04 GMT+0200 (hora de verano de Europa central)',
|
|
15
|
+
whole_day: false,
|
|
16
|
+
image: {
|
|
17
|
+
src:
|
|
18
|
+
'https://eu-copernicus.github.io/copernicus-component-library/assets/images/image_placeholder.jpg',
|
|
19
|
+
scales: {
|
|
20
|
+
mini: {
|
|
21
|
+
download:
|
|
22
|
+
'https://eu-copernicus.github.io/copernicus-component-library/assets/images/image_placeholder.jpg',
|
|
23
|
+
},
|
|
24
|
+
preview: {
|
|
25
|
+
download:
|
|
26
|
+
'https://eu-copernicus.github.io/copernicus-component-library/assets/images/image_placeholder.jpg',
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
download: 'false',
|
|
30
|
+
alt: 'Placeholder',
|
|
31
|
+
},
|
|
32
|
+
taxonomy_technical_library_categorization: ['1', '2', '3'],
|
|
33
|
+
file: {
|
|
34
|
+
'content-type': 'text/html',
|
|
35
|
+
src:
|
|
36
|
+
'https://eu-copernicus.github.io/copernicus-component-library/assets/images/image_placeholder.jpg',
|
|
37
|
+
download: 'true',
|
|
38
|
+
},
|
|
39
|
+
effective:
|
|
40
|
+
'Wed May 19 2021 12:49:04 GMT+0200 (hora de verano de Europa central)',
|
|
41
|
+
};
|
|
9
42
|
|
|
10
43
|
describe('CclCard', () => {
|
|
11
44
|
const store = mockStore({
|
|
@@ -31,40 +64,7 @@ describe('CclCard', () => {
|
|
|
31
64
|
},
|
|
32
65
|
taxonomy_technical_library_categorization: ['1', '2', '3'],
|
|
33
66
|
});
|
|
34
|
-
|
|
35
|
-
title: 'title example',
|
|
36
|
-
description: 'description example',
|
|
37
|
-
'@type': 'News Item',
|
|
38
|
-
start:
|
|
39
|
-
'Wed May 19 2021 12:49:04 GMT+0200 (hora de verano de Europa central)',
|
|
40
|
-
end: 'Wed May 26 2024 12:49:04 GMT+0200 (hora de verano de Europa central)',
|
|
41
|
-
whole_day: false,
|
|
42
|
-
image: {
|
|
43
|
-
src:
|
|
44
|
-
'https://eu-copernicus.github.io/copernicus-component-library/assets/images/image_placeholder.jpg',
|
|
45
|
-
scales: {
|
|
46
|
-
mini: {
|
|
47
|
-
download:
|
|
48
|
-
'https://eu-copernicus.github.io/copernicus-component-library/assets/images/image_placeholder.jpg',
|
|
49
|
-
},
|
|
50
|
-
preview: {
|
|
51
|
-
download:
|
|
52
|
-
'https://eu-copernicus.github.io/copernicus-component-library/assets/images/image_placeholder.jpg',
|
|
53
|
-
},
|
|
54
|
-
},
|
|
55
|
-
download: 'false',
|
|
56
|
-
alt: 'Placeholder',
|
|
57
|
-
},
|
|
58
|
-
taxonomy_technical_library_categorization: ['1', '2', '3'],
|
|
59
|
-
file: {
|
|
60
|
-
'content-type': 'text/html',
|
|
61
|
-
src:
|
|
62
|
-
'https://eu-copernicus.github.io/copernicus-component-library/assets/images/image_placeholder.jpg',
|
|
63
|
-
download: 'true',
|
|
64
|
-
},
|
|
65
|
-
effective:
|
|
66
|
-
'Wed May 19 2021 12:49:04 GMT+0200 (hora de verano de Europa central)',
|
|
67
|
-
};
|
|
67
|
+
|
|
68
68
|
it('Check doc card', () => {
|
|
69
69
|
const cardtest = renderer
|
|
70
70
|
.create(
|
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { useSelector } from 'react-redux';
|
|
3
|
+
import qs from 'query-string';
|
|
4
|
+
import { useLocation, useHistory } from 'react-router-dom';
|
|
5
|
+
|
|
6
|
+
import { resolveExtension } from '@plone/volto/helpers/Extensions/withBlockExtensions';
|
|
7
|
+
import config from '@plone/volto/registry';
|
|
8
|
+
import { usePrevious } from '@plone/volto/helpers';
|
|
9
|
+
import { isEqual } from 'lodash';
|
|
10
|
+
|
|
11
|
+
function getDisplayName(WrappedComponent) {
|
|
12
|
+
return WrappedComponent.displayName || WrappedComponent.name || 'Component';
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const SEARCH_ENDPOINT_FIELDS = [
|
|
16
|
+
'SearchableText',
|
|
17
|
+
'b_size',
|
|
18
|
+
'limit',
|
|
19
|
+
'sort_on',
|
|
20
|
+
'sort_order',
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
const PAQO = 'plone.app.querystring.operation';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Based on URL state, gets an initial internal state for the search
|
|
27
|
+
*
|
|
28
|
+
* @function getInitialState
|
|
29
|
+
*
|
|
30
|
+
*/
|
|
31
|
+
function getInitialState(data, facets, urlSearchText, id) {
|
|
32
|
+
const {
|
|
33
|
+
types: facetWidgetTypes,
|
|
34
|
+
} = config.blocks.blocksConfig.search.extensions.facetWidgets;
|
|
35
|
+
const facetSettings = data?.facets || [];
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
query: [
|
|
39
|
+
...(data.query?.query || []),
|
|
40
|
+
...(facetSettings || [])
|
|
41
|
+
.map((facet) => {
|
|
42
|
+
if (!facet?.field) return null;
|
|
43
|
+
|
|
44
|
+
const { valueToQuery } = resolveExtension(
|
|
45
|
+
'type',
|
|
46
|
+
facetWidgetTypes,
|
|
47
|
+
facet,
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
const name = facet.field.value;
|
|
51
|
+
const value = facets[name];
|
|
52
|
+
|
|
53
|
+
return valueToQuery({ value, facet });
|
|
54
|
+
})
|
|
55
|
+
.filter((f) => !!f),
|
|
56
|
+
...(urlSearchText
|
|
57
|
+
? [
|
|
58
|
+
{
|
|
59
|
+
i: 'SearchableText',
|
|
60
|
+
o: 'plone.app.querystring.operation.string.contains',
|
|
61
|
+
v: urlSearchText,
|
|
62
|
+
},
|
|
63
|
+
]
|
|
64
|
+
: []),
|
|
65
|
+
],
|
|
66
|
+
sort_on: data.query?.sort_on,
|
|
67
|
+
sort_order: data.query?.sort_order,
|
|
68
|
+
b_size: data.query?.b_size,
|
|
69
|
+
limit: data.query?.limit,
|
|
70
|
+
block: id,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* "Normalizes" the search state to something that's serializable
|
|
76
|
+
* (for querying) and used to compute data for the ListingBody
|
|
77
|
+
*
|
|
78
|
+
* @function normalizeState
|
|
79
|
+
*
|
|
80
|
+
*/
|
|
81
|
+
function normalizeState({
|
|
82
|
+
query, // base query
|
|
83
|
+
facets, // facet values
|
|
84
|
+
id, // block id
|
|
85
|
+
searchText, // SearchableText
|
|
86
|
+
sortOn,
|
|
87
|
+
sortOrder,
|
|
88
|
+
facetSettings, // data.facets extracted from block data
|
|
89
|
+
}) {
|
|
90
|
+
const {
|
|
91
|
+
types: facetWidgetTypes,
|
|
92
|
+
} = config.blocks.blocksConfig.search.extensions.facetWidgets;
|
|
93
|
+
|
|
94
|
+
const params = {
|
|
95
|
+
query: [
|
|
96
|
+
...(query.query || []),
|
|
97
|
+
...(facetSettings || []).map((facet) => {
|
|
98
|
+
if (!facet?.field) return null;
|
|
99
|
+
|
|
100
|
+
const { valueToQuery } = resolveExtension(
|
|
101
|
+
'type',
|
|
102
|
+
facetWidgetTypes,
|
|
103
|
+
facet,
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
const name = facet.field.value;
|
|
107
|
+
const value = facets[name];
|
|
108
|
+
|
|
109
|
+
return valueToQuery({ value, facet });
|
|
110
|
+
}),
|
|
111
|
+
].filter((o) => !!o),
|
|
112
|
+
sort_on: sortOn || query.sort_on,
|
|
113
|
+
sort_order: sortOrder || query.sort_order,
|
|
114
|
+
b_size: query.b_size,
|
|
115
|
+
limit: query.limit,
|
|
116
|
+
block: id,
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
// Note Ideally the searchtext functionality should be restructured as being just
|
|
120
|
+
// another facet. But right now it's the same. This means that if a searchText
|
|
121
|
+
// is provided, it will override the SearchableText facet.
|
|
122
|
+
// If there is no searchText, the SearchableText in the query remains in effect.
|
|
123
|
+
// TODO eventually the searchText should be a distinct facet from SearchableText, and
|
|
124
|
+
// the two conditions could be combined, in comparison to the current state, when
|
|
125
|
+
// one overrides the other.
|
|
126
|
+
if (searchText) {
|
|
127
|
+
params.query = params.query.reduce(
|
|
128
|
+
// Remove SearchableText from query
|
|
129
|
+
(acc, kvp) => (kvp.i === 'SearchableText' ? acc : [...acc, kvp]),
|
|
130
|
+
[],
|
|
131
|
+
);
|
|
132
|
+
params.query.push({
|
|
133
|
+
i: 'SearchableText',
|
|
134
|
+
o: 'plone.app.querystring.operation.string.contains',
|
|
135
|
+
v: searchText,
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return params;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const getSearchFields = (searchData) => {
|
|
143
|
+
return Object.assign(
|
|
144
|
+
{},
|
|
145
|
+
...SEARCH_ENDPOINT_FIELDS.map((k) => {
|
|
146
|
+
return searchData[k] ? { [k]: searchData[k] } : {};
|
|
147
|
+
}),
|
|
148
|
+
searchData.query ? { query: serializeQuery(searchData['query']) } : {},
|
|
149
|
+
);
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* A hook that will mirror the search block state to a hash location
|
|
154
|
+
*/
|
|
155
|
+
const useHashState = () => {
|
|
156
|
+
const location = useLocation();
|
|
157
|
+
const history = useHistory();
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Required to maintain parameter compatibility.
|
|
161
|
+
With this we will maintain support for receiving hash (#) and search (?) type parameters.
|
|
162
|
+
*/
|
|
163
|
+
const oldState = React.useMemo(() => {
|
|
164
|
+
return {
|
|
165
|
+
...qs.parse(location.search),
|
|
166
|
+
...qs.parse(location.hash),
|
|
167
|
+
};
|
|
168
|
+
}, [location.hash, location.search]);
|
|
169
|
+
|
|
170
|
+
// This creates a shallow copy. Why is this needed?
|
|
171
|
+
const current = Object.assign(
|
|
172
|
+
{},
|
|
173
|
+
...Array.from(Object.keys(oldState)).map((k) => ({ [k]: oldState[k] })),
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
const setSearchData = React.useCallback(
|
|
177
|
+
(searchData) => {
|
|
178
|
+
const newParams = qs.parse(location.search);
|
|
179
|
+
|
|
180
|
+
let changed = false;
|
|
181
|
+
|
|
182
|
+
Object.keys(searchData)
|
|
183
|
+
.sort()
|
|
184
|
+
.forEach((k) => {
|
|
185
|
+
if (searchData[k]) {
|
|
186
|
+
newParams[k] = searchData[k];
|
|
187
|
+
if (oldState[k] !== searchData[k]) {
|
|
188
|
+
changed = true;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
if (changed) {
|
|
194
|
+
history.push({
|
|
195
|
+
search: qs.stringify(newParams),
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
},
|
|
199
|
+
[history, oldState, location.search],
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
return [current, setSearchData];
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* A hook to make it possible to switch disable mirroring the search block
|
|
207
|
+
* state to the window location. When using the internal state we "start from
|
|
208
|
+
* scratch", as it's intended to be used in the edit page.
|
|
209
|
+
*/
|
|
210
|
+
const useSearchBlockState = (uniqueId, isEditMode) => {
|
|
211
|
+
const [hashState, setHashState] = useHashState();
|
|
212
|
+
const [internalState, setInternalState] = React.useState({});
|
|
213
|
+
|
|
214
|
+
return isEditMode
|
|
215
|
+
? [internalState, setInternalState]
|
|
216
|
+
: [hashState, setHashState];
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
// Simple compress/decompress the state in URL by replacing the lengthy string
|
|
220
|
+
const deserializeQuery = (q) => {
|
|
221
|
+
return JSON.parse(q)?.map((kvp) => ({
|
|
222
|
+
...kvp,
|
|
223
|
+
o: kvp.o.replace(/^paqo/, PAQO),
|
|
224
|
+
}));
|
|
225
|
+
};
|
|
226
|
+
const serializeQuery = (q) => {
|
|
227
|
+
return JSON.stringify(
|
|
228
|
+
q?.map((kvp) => ({ ...kvp, o: kvp.o.replace(PAQO, 'paqo') })),
|
|
229
|
+
);
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
const withSearch = (options) => (WrappedComponent) => {
|
|
233
|
+
const { inputDelay = 1000 } = options || {};
|
|
234
|
+
|
|
235
|
+
function WithSearch(props) {
|
|
236
|
+
const { data, id, editable = false } = props;
|
|
237
|
+
|
|
238
|
+
const [locationSearchData, setLocationSearchData] = useSearchBlockState(
|
|
239
|
+
id,
|
|
240
|
+
editable,
|
|
241
|
+
);
|
|
242
|
+
|
|
243
|
+
const urlQuery = locationSearchData.query
|
|
244
|
+
? deserializeQuery(locationSearchData.query)
|
|
245
|
+
: [];
|
|
246
|
+
const urlSearchText =
|
|
247
|
+
locationSearchData.SearchableText ||
|
|
248
|
+
urlQuery.find(({ i }) => i === 'SearchableText')?.v ||
|
|
249
|
+
'';
|
|
250
|
+
|
|
251
|
+
// TODO: refactor, should use only useLocationStateManager()!!!
|
|
252
|
+
const [searchText, setSearchText] = React.useState(urlSearchText);
|
|
253
|
+
const configuredFacets =
|
|
254
|
+
data.facets?.map((facet) => facet?.field?.value) || [];
|
|
255
|
+
const multiFacets = data.facets
|
|
256
|
+
?.filter((facet) => facet?.multiple)
|
|
257
|
+
.map((facet) => facet?.field?.value);
|
|
258
|
+
const [facets, setFacets] = React.useState(
|
|
259
|
+
Object.assign(
|
|
260
|
+
{},
|
|
261
|
+
...urlQuery.map(({ i, v }) => ({ [i]: v })), // TODO: the 'o' should be kept. This would be a major refactoring of the facets
|
|
262
|
+
|
|
263
|
+
// support for simple filters like ?Subject=something
|
|
264
|
+
// TODO: since the move to hash params this is no longer working.
|
|
265
|
+
// We'd have to treat the location.search and manage it just like the
|
|
266
|
+
// hash, to support it. We can read it, but we'd have to reset it as
|
|
267
|
+
// well, so at that point what's the difference to the hash?
|
|
268
|
+
...configuredFacets.map((f) =>
|
|
269
|
+
locationSearchData[f]
|
|
270
|
+
? {
|
|
271
|
+
[f]:
|
|
272
|
+
multiFacets.indexOf(f) > -1
|
|
273
|
+
? [locationSearchData[f]]
|
|
274
|
+
: locationSearchData[f],
|
|
275
|
+
}
|
|
276
|
+
: {},
|
|
277
|
+
),
|
|
278
|
+
),
|
|
279
|
+
);
|
|
280
|
+
const previousUrlQuery = usePrevious(urlQuery);
|
|
281
|
+
|
|
282
|
+
React.useEffect(() => {
|
|
283
|
+
if (!isEqual(urlQuery, previousUrlQuery)) {
|
|
284
|
+
setFacets(
|
|
285
|
+
Object.assign(
|
|
286
|
+
{},
|
|
287
|
+
...urlQuery.map(({ i, v }) => ({ [i]: v })), // TODO: the 'o' should be kept. This would be a major refactoring of the facets
|
|
288
|
+
|
|
289
|
+
// support for simple filters like ?Subject=something
|
|
290
|
+
// TODO: since the move to hash params this is no longer working.
|
|
291
|
+
// We'd have to treat the location.search and manage it just like the
|
|
292
|
+
// hash, to support it. We can read it, but we'd have to reset it as
|
|
293
|
+
// well, so at that point what's the difference to the hash?
|
|
294
|
+
...configuredFacets.map((f) =>
|
|
295
|
+
locationSearchData[f]
|
|
296
|
+
? {
|
|
297
|
+
[f]:
|
|
298
|
+
multiFacets.indexOf(f) > -1
|
|
299
|
+
? [locationSearchData[f]]
|
|
300
|
+
: locationSearchData[f],
|
|
301
|
+
}
|
|
302
|
+
: {},
|
|
303
|
+
),
|
|
304
|
+
),
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
}, [
|
|
308
|
+
urlQuery,
|
|
309
|
+
configuredFacets,
|
|
310
|
+
locationSearchData,
|
|
311
|
+
multiFacets,
|
|
312
|
+
previousUrlQuery,
|
|
313
|
+
]);
|
|
314
|
+
|
|
315
|
+
const [sortOn, setSortOn] = React.useState(data?.query?.sort_on);
|
|
316
|
+
const [sortOrder, setSortOrder] = React.useState(data?.query?.sort_order);
|
|
317
|
+
|
|
318
|
+
const [searchData, setSearchData] = React.useState(
|
|
319
|
+
getInitialState(data, facets, urlSearchText, id),
|
|
320
|
+
);
|
|
321
|
+
|
|
322
|
+
const deepFacets = JSON.stringify(facets);
|
|
323
|
+
React.useEffect(() => {
|
|
324
|
+
setSearchData(getInitialState(data, facets, urlSearchText, id));
|
|
325
|
+
}, [deepFacets, facets, data, urlSearchText, id]);
|
|
326
|
+
|
|
327
|
+
const timeoutRef = React.useRef();
|
|
328
|
+
const facetSettings = data?.facets;
|
|
329
|
+
|
|
330
|
+
const deepQuery = JSON.stringify(data.query);
|
|
331
|
+
const onTriggerSearch = React.useCallback(
|
|
332
|
+
(
|
|
333
|
+
toSearchText = undefined,
|
|
334
|
+
toSearchFacets = undefined,
|
|
335
|
+
toSortOn = undefined,
|
|
336
|
+
toSortOrder = undefined,
|
|
337
|
+
) => {
|
|
338
|
+
if (timeoutRef.current) clearTimeout(timeoutRef.current);
|
|
339
|
+
timeoutRef.current = setTimeout(
|
|
340
|
+
() => {
|
|
341
|
+
const newSearchData = normalizeState({
|
|
342
|
+
id,
|
|
343
|
+
query: data.query || {},
|
|
344
|
+
facets: toSearchFacets || facets,
|
|
345
|
+
searchText: toSearchText ? toSearchText.trim() : '',
|
|
346
|
+
sortOn: toSortOn || sortOn,
|
|
347
|
+
sortOrder: toSortOrder || sortOrder,
|
|
348
|
+
facetSettings,
|
|
349
|
+
});
|
|
350
|
+
if (toSearchFacets) setFacets(toSearchFacets);
|
|
351
|
+
if (toSortOn) setSortOn(toSortOn);
|
|
352
|
+
if (toSortOrder) setSortOrder(toSortOrder);
|
|
353
|
+
setSearchData(newSearchData);
|
|
354
|
+
setLocationSearchData(getSearchFields(newSearchData));
|
|
355
|
+
},
|
|
356
|
+
toSearchFacets ? inputDelay / 3 : inputDelay,
|
|
357
|
+
);
|
|
358
|
+
},
|
|
359
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
360
|
+
[
|
|
361
|
+
// Use deep comparison of data.query
|
|
362
|
+
deepQuery,
|
|
363
|
+
facets,
|
|
364
|
+
id,
|
|
365
|
+
setLocationSearchData,
|
|
366
|
+
searchText,
|
|
367
|
+
sortOn,
|
|
368
|
+
sortOrder,
|
|
369
|
+
facetSettings,
|
|
370
|
+
],
|
|
371
|
+
);
|
|
372
|
+
|
|
373
|
+
const removeSearchQuery = () => {
|
|
374
|
+
let newSearchData = { ...searchData };
|
|
375
|
+
newSearchData.query = searchData.query.reduce(
|
|
376
|
+
// Remove SearchableText from query
|
|
377
|
+
(acc, kvp) => (kvp.i === 'SearchableText' ? acc : [...acc, kvp]),
|
|
378
|
+
[],
|
|
379
|
+
);
|
|
380
|
+
setSearchData(newSearchData);
|
|
381
|
+
setLocationSearchData(getSearchFields(newSearchData));
|
|
382
|
+
};
|
|
383
|
+
|
|
384
|
+
const querystringResults = useSelector(
|
|
385
|
+
(state) => state.querystringsearch.subrequests,
|
|
386
|
+
);
|
|
387
|
+
const totalItems =
|
|
388
|
+
querystringResults[id]?.total || querystringResults[id]?.items?.length;
|
|
389
|
+
|
|
390
|
+
return (
|
|
391
|
+
<WrappedComponent
|
|
392
|
+
{...props}
|
|
393
|
+
searchData={searchData}
|
|
394
|
+
facets={facets}
|
|
395
|
+
setFacets={setFacets}
|
|
396
|
+
setSortOn={setSortOn}
|
|
397
|
+
setSortOrder={setSortOrder}
|
|
398
|
+
sortOn={sortOn}
|
|
399
|
+
sortOrder={sortOrder}
|
|
400
|
+
searchedText={urlSearchText}
|
|
401
|
+
searchText={searchText}
|
|
402
|
+
removeSearchQuery={removeSearchQuery}
|
|
403
|
+
setSearchText={setSearchText}
|
|
404
|
+
onTriggerSearch={onTriggerSearch}
|
|
405
|
+
totalItems={totalItems}
|
|
406
|
+
/>
|
|
407
|
+
);
|
|
408
|
+
}
|
|
409
|
+
WithSearch.displayName = `WithSearch(${getDisplayName(WrappedComponent)})`;
|
|
410
|
+
|
|
411
|
+
return WithSearch;
|
|
412
|
+
};
|
|
413
|
+
|
|
414
|
+
export default withSearch;
|
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
import { Loader } from 'semantic-ui-react';
|
|
2
|
-
import { useDispatch, useSelector } from 'react-redux';
|
|
3
|
-
|
|
4
|
-
import CclCard from '@eeacms/volto-clms-theme/components/CclCard/CclCard';
|
|
5
|
-
import React from 'react';
|
|
6
|
-
import { searchContent } from '@plone/volto/actions';
|
|
7
|
-
import { useLocation } from 'react-router-dom';
|
|
8
|
-
|
|
9
|
-
const RelatedUseCases = (props) => {
|
|
10
|
-
const dispatch = useDispatch();
|
|
11
|
-
const { UID, id } = props;
|
|
12
|
-
const searchSubrequests = useSelector((state) => state.search.subrequests);
|
|
13
|
-
let libraries = searchSubrequests?.[id]?.items || [];
|
|
14
|
-
let librariesPending = searchSubrequests?.[id]?.loading;
|
|
15
|
-
const location = useLocation();
|
|
16
|
-
React.useEffect(() => {
|
|
17
|
-
if (location.hash === '#Use-cases' && UID) {
|
|
18
|
-
dispatch(
|
|
19
|
-
searchContent(
|
|
20
|
-
'',
|
|
21
|
-
{
|
|
22
|
-
fullobjects: 1,
|
|
23
|
-
portal_type: 'UseCase',
|
|
24
|
-
path: '/',
|
|
25
|
-
associated_datasets: UID,
|
|
26
|
-
},
|
|
27
|
-
id,
|
|
28
|
-
),
|
|
29
|
-
);
|
|
30
|
-
}
|
|
31
|
-
}, [id, UID, dispatch, location]);
|
|
32
|
-
|
|
33
|
-
return (
|
|
34
|
-
<div>
|
|
35
|
-
{librariesPending && <Loader active inline="centered" />}
|
|
36
|
-
{libraries.length > 0 ? (
|
|
37
|
-
libraries.map((item, index) => (
|
|
38
|
-
<CclCard
|
|
39
|
-
key={index}
|
|
40
|
-
type={null}
|
|
41
|
-
card={{ Type: item['@type'], ...item }}
|
|
42
|
-
/>
|
|
43
|
-
))
|
|
44
|
-
) : (
|
|
45
|
-
<p>There are no related items.</p>
|
|
46
|
-
)}
|
|
47
|
-
</div>
|
|
48
|
-
);
|
|
49
|
-
};
|
|
50
|
-
|
|
51
|
-
export default RelatedUseCases;
|