@eeacms/volto-eea-website-theme 1.29.0 → 1.31.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.
- package/.eslintrc.js +1 -6
- package/CHANGELOG.md +15 -1
- package/package.json +1 -1
- package/src/customizations/volto/components/manage/Blocks/Image/View.jsx +1 -1
- package/src/customizations/volto/components/manage/Blocks/Search/SearchBlockEdit.jsx +106 -0
- package/src/customizations/volto/components/manage/Blocks/Search/hocs/withSearch.jsx +479 -0
- package/src/customizations/volto/components/manage/Sidebar/ObjectBrowserBody.jsx +511 -0
- package/src/customizations/volto/components/theme/EventDetails/EventDetails.jsx +154 -0
- package/src/reducers/index.js +3 -1
package/.eslintrc.js
CHANGED
package/CHANGELOG.md
CHANGED
@@ -4,7 +4,16 @@ 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.
|
7
|
+
### [1.31.0](https://github.com/eea/volto-eea-website-theme/compare/1.30.0...1.31.0) - 14 March 2024
|
8
|
+
|
9
|
+
#### :rocket: New Features
|
10
|
+
|
11
|
+
- feat: Put default alt as the rights field than image title - refs #159551 [dobri1408 - [`4f0e60b`](https://github.com/eea/volto-eea-website-theme/commit/4f0e60b02ce1167d92de3b294e75d2577c892c85)]
|
12
|
+
|
13
|
+
#### :hammer_and_wrench: Others
|
14
|
+
|
15
|
+
- Release 1.31.0 [alin - [`36690f7`](https://github.com/eea/volto-eea-website-theme/commit/36690f75e4773e40f5ab4e4ce4b956b650df62b5)]
|
16
|
+
### [1.30.0](https://github.com/eea/volto-eea-website-theme/compare/1.29.0...1.30.0) - 13 March 2024
|
8
17
|
|
9
18
|
#### :rocket: New Features
|
10
19
|
|
@@ -21,6 +30,9 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
|
|
21
30
|
|
22
31
|
#### :hammer_and_wrench: Others
|
23
32
|
|
33
|
+
- Update package.json [ichim-david - [`4c2f794`](https://github.com/eea/volto-eea-website-theme/commit/4c2f794256098dff98aa4b78ba99be65709b1b9b)]
|
34
|
+
- Fix export statement in reducers/index.js [David Ichim - [`bbaf5be`](https://github.com/eea/volto-eea-website-theme/commit/bbaf5be2e7cd24b6a8c5d694ee4030b6fafbd120)]
|
35
|
+
- Lint fix, export of print should be made after import [David Ichim - [`2a670db`](https://github.com/eea/volto-eea-website-theme/commit/2a670db0a9bcb741ea24ea3ac98ba7267b6a83e1)]
|
24
36
|
- test: TokenWidget.test.jsx and TopicsWidget.test.jsx cover more conditions [laszlocseh - [`dd154bb`](https://github.com/eea/volto-eea-website-theme/commit/dd154bb520237f458be563280cac1545eb381bdf)]
|
25
37
|
- Bump version to 1.29.0 from 1.28.4 [Claudia Ifrim - [`7bc8eab`](https://github.com/eea/volto-eea-website-theme/commit/7bc8eabd4b0462fc5afc07ed131d074af642cc89)]
|
26
38
|
- test: added TokenWidget.test.jsx and TopicsWidget.test.jsx [laszlocseh - [`f7292bb`](https://github.com/eea/volto-eea-website-theme/commit/f7292bb426591f758dcc7bc159b5dbd75b7afb36)]
|
@@ -28,6 +40,8 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
|
|
28
40
|
- test: fix in Logo.test.jsx [laszlocseh - [`6d552f8`](https://github.com/eea/volto-eea-website-theme/commit/6d552f89d5aff6e47ef3032d4862a1bcbe364c01)]
|
29
41
|
- test: add Logo.test.jsx [laszlocseh - [`752c562`](https://github.com/eea/volto-eea-website-theme/commit/752c5629840d906382858f088b97d33147684ca8)]
|
30
42
|
- add isPrint missing files [laszlocseh - [`d995789`](https://github.com/eea/volto-eea-website-theme/commit/d995789f337a00455d45b5ec26de9c4c0c898ce6)]
|
43
|
+
### [1.29.0](https://github.com/eea/volto-eea-website-theme/compare/1.28.3...1.29.0) - 7 March 2024
|
44
|
+
|
31
45
|
### [1.28.3](https://github.com/eea/volto-eea-website-theme/compare/1.28.2...1.28.3) - 5 March 2024
|
32
46
|
|
33
47
|
#### :bug: Bug Fixes
|
package/package.json
CHANGED
@@ -20,7 +20,7 @@ import { Copyright } from '@eeacms/volto-eea-design-system/ui';
|
|
20
20
|
*/
|
21
21
|
export const View = (props) => {
|
22
22
|
const { data, detached } = props;
|
23
|
-
const href = data?.href?.[0]?.['@id'] || '';
|
23
|
+
const href = data?.href?.[0]?.['@id'] ?? (data?.href || '');
|
24
24
|
const { copyright, copyrightIcon, copyrightPosition } = data;
|
25
25
|
// const [hovering, setHovering] = React.useState(false);
|
26
26
|
const [viewLoaded, setViewLoaded] = React.useState(false);
|
@@ -0,0 +1,106 @@
|
|
1
|
+
//this customization is used for fixing: in the search block, on edit sort on and reversed order doesn't work.
|
2
|
+
//See here volto pr: https://github.com/plone/volto/pull/5262
|
3
|
+
import React, { useEffect } from 'react';
|
4
|
+
import { defineMessages } from 'react-intl';
|
5
|
+
import { compose } from 'redux';
|
6
|
+
|
7
|
+
import { SidebarPortal, BlockDataForm } from '@plone/volto/components';
|
8
|
+
import { addExtensionFieldToSchema } from '@plone/volto/helpers/Extensions/withBlockSchemaEnhancer';
|
9
|
+
import { getBaseUrl } from '@plone/volto/helpers';
|
10
|
+
import config from '@plone/volto/registry';
|
11
|
+
|
12
|
+
import { SearchBlockViewComponent } from '@plone/volto/components/manage/Blocks/Search/SearchBlockView';
|
13
|
+
import Schema from '@plone/volto/components/manage/Blocks/Search/schema';
|
14
|
+
import {
|
15
|
+
withSearch,
|
16
|
+
withQueryString,
|
17
|
+
} from '@plone/volto/components/manage/Blocks/Search/hocs';
|
18
|
+
import { cloneDeep } from 'lodash';
|
19
|
+
|
20
|
+
const messages = defineMessages({
|
21
|
+
template: {
|
22
|
+
id: 'Results template',
|
23
|
+
defaultMessage: 'Results template',
|
24
|
+
},
|
25
|
+
});
|
26
|
+
|
27
|
+
const SearchBlockEdit = (props) => {
|
28
|
+
const {
|
29
|
+
block,
|
30
|
+
onChangeBlock,
|
31
|
+
data,
|
32
|
+
selected,
|
33
|
+
intl,
|
34
|
+
navRoot,
|
35
|
+
contentType,
|
36
|
+
onTriggerSearch,
|
37
|
+
querystring = {},
|
38
|
+
} = props;
|
39
|
+
const { sortable_indexes = {} } = querystring;
|
40
|
+
|
41
|
+
let schema = Schema({ data, intl });
|
42
|
+
|
43
|
+
schema = addExtensionFieldToSchema({
|
44
|
+
schema,
|
45
|
+
name: 'listingBodyTemplate',
|
46
|
+
items: config.blocks.blocksConfig.listing.variations,
|
47
|
+
intl,
|
48
|
+
title: { id: intl.formatMessage(messages.template) },
|
49
|
+
});
|
50
|
+
const listingVariations = config.blocks.blocksConfig?.listing?.variations;
|
51
|
+
let activeItem = listingVariations.find(
|
52
|
+
(item) => item.id === data.listingBodyTemplate,
|
53
|
+
);
|
54
|
+
const listingSchemaEnhancer = activeItem?.schemaEnhancer;
|
55
|
+
if (listingSchemaEnhancer)
|
56
|
+
schema = listingSchemaEnhancer({
|
57
|
+
schema: cloneDeep(schema),
|
58
|
+
data,
|
59
|
+
intl,
|
60
|
+
});
|
61
|
+
schema.properties.sortOnOptions.items = {
|
62
|
+
choices: Object.keys(sortable_indexes).map((k) => [
|
63
|
+
k,
|
64
|
+
sortable_indexes[k].title,
|
65
|
+
]),
|
66
|
+
};
|
67
|
+
|
68
|
+
const { query = {} } = data || {};
|
69
|
+
// We don't need deep compare here, as this is just json serializable data.
|
70
|
+
const deepQuery = JSON.stringify(query);
|
71
|
+
useEffect(() => {
|
72
|
+
onTriggerSearch(
|
73
|
+
'',
|
74
|
+
data?.facets,
|
75
|
+
data?.query?.sort_on,
|
76
|
+
data?.query?.sort_order,
|
77
|
+
);
|
78
|
+
}, [deepQuery, onTriggerSearch, data]);
|
79
|
+
|
80
|
+
return (
|
81
|
+
<>
|
82
|
+
<SearchBlockViewComponent
|
83
|
+
{...props}
|
84
|
+
path={getBaseUrl(props.pathname)}
|
85
|
+
mode="edit"
|
86
|
+
/>
|
87
|
+
<SidebarPortal selected={selected}>
|
88
|
+
<BlockDataForm
|
89
|
+
schema={schema}
|
90
|
+
onChangeField={(id, value) => {
|
91
|
+
onChangeBlock(block, {
|
92
|
+
...data,
|
93
|
+
[id]: value,
|
94
|
+
});
|
95
|
+
}}
|
96
|
+
onChangeBlock={onChangeBlock}
|
97
|
+
formData={data}
|
98
|
+
navRoot={navRoot}
|
99
|
+
contentType={contentType}
|
100
|
+
/>
|
101
|
+
</SidebarPortal>
|
102
|
+
</>
|
103
|
+
);
|
104
|
+
};
|
105
|
+
|
106
|
+
export default compose(withQueryString, withSearch())(SearchBlockEdit);
|
@@ -0,0 +1,479 @@
|
|
1
|
+
//this customization is used for fixing: in the search block, on edit sort on and reversed order doesn't work.
|
2
|
+
//See here volto pr: https://github.com/plone/volto/pull/5262
|
3
|
+
import React from 'react';
|
4
|
+
import { useSelector } from 'react-redux';
|
5
|
+
import qs from 'query-string';
|
6
|
+
import { useLocation, useHistory } from 'react-router-dom';
|
7
|
+
|
8
|
+
import { resolveExtension } from '@plone/volto/helpers/Extensions/withBlockExtensions';
|
9
|
+
import config from '@plone/volto/registry';
|
10
|
+
import { usePrevious } from '@plone/volto/helpers';
|
11
|
+
import { isEqual } from 'lodash';
|
12
|
+
|
13
|
+
function getDisplayName(WrappedComponent) {
|
14
|
+
return WrappedComponent.displayName || WrappedComponent.name || 'Component';
|
15
|
+
}
|
16
|
+
|
17
|
+
const SEARCH_ENDPOINT_FIELDS = [
|
18
|
+
'SearchableText',
|
19
|
+
'b_size',
|
20
|
+
'limit',
|
21
|
+
'sort_on',
|
22
|
+
'sort_order',
|
23
|
+
];
|
24
|
+
|
25
|
+
const PAQO = 'plone.app.querystring.operation';
|
26
|
+
|
27
|
+
/**
|
28
|
+
* Based on URL state, gets an initial internal state for the search
|
29
|
+
*
|
30
|
+
* @function getInitialState
|
31
|
+
*
|
32
|
+
*/
|
33
|
+
function getInitialState(
|
34
|
+
data,
|
35
|
+
facets,
|
36
|
+
urlSearchText,
|
37
|
+
id,
|
38
|
+
sortOnParam,
|
39
|
+
sortOrderParam,
|
40
|
+
) {
|
41
|
+
const {
|
42
|
+
types: facetWidgetTypes,
|
43
|
+
} = config.blocks.blocksConfig.search.extensions.facetWidgets;
|
44
|
+
const facetSettings = data?.facets || [];
|
45
|
+
|
46
|
+
return {
|
47
|
+
query: [
|
48
|
+
...(data.query?.query || []),
|
49
|
+
...(facetSettings || [])
|
50
|
+
.map((facet) => {
|
51
|
+
if (!facet?.field) return null;
|
52
|
+
|
53
|
+
const { valueToQuery } = resolveExtension(
|
54
|
+
'type',
|
55
|
+
facetWidgetTypes,
|
56
|
+
facet,
|
57
|
+
);
|
58
|
+
|
59
|
+
const name = facet.field.value;
|
60
|
+
const value = facets[name];
|
61
|
+
|
62
|
+
return valueToQuery({ value, facet });
|
63
|
+
})
|
64
|
+
.filter((f) => !!f),
|
65
|
+
...(urlSearchText
|
66
|
+
? [
|
67
|
+
{
|
68
|
+
i: 'SearchableText',
|
69
|
+
o: 'plone.app.querystring.operation.string.contains',
|
70
|
+
v: urlSearchText,
|
71
|
+
},
|
72
|
+
]
|
73
|
+
: []),
|
74
|
+
],
|
75
|
+
sort_on: sortOnParam || data.query?.sort_on,
|
76
|
+
sort_order: sortOrderParam || data.query?.sort_order,
|
77
|
+
b_size: data.query?.b_size,
|
78
|
+
limit: data.query?.limit,
|
79
|
+
block: id,
|
80
|
+
};
|
81
|
+
}
|
82
|
+
|
83
|
+
/**
|
84
|
+
* "Normalizes" the search state to something that's serializable
|
85
|
+
* (for querying) and used to compute data for the ListingBody
|
86
|
+
*
|
87
|
+
* @function normalizeState
|
88
|
+
*
|
89
|
+
*/
|
90
|
+
function normalizeState({
|
91
|
+
query, // base query
|
92
|
+
facets, // facet values
|
93
|
+
id, // block id
|
94
|
+
searchText, // SearchableText
|
95
|
+
sortOn,
|
96
|
+
sortOrder,
|
97
|
+
facetSettings, // data.facets extracted from block data
|
98
|
+
}) {
|
99
|
+
const {
|
100
|
+
types: facetWidgetTypes,
|
101
|
+
} = config.blocks.blocksConfig.search.extensions.facetWidgets;
|
102
|
+
|
103
|
+
// Here, we are removing the QueryString of the Listing ones, which is present in the Facet
|
104
|
+
// because we already initialize the facet with those values.
|
105
|
+
const configuredFacets = facetSettings
|
106
|
+
? facetSettings.map((facet) => facet?.field?.value)
|
107
|
+
: [];
|
108
|
+
|
109
|
+
let copyOfQuery = query.query ? [...query.query] : [];
|
110
|
+
|
111
|
+
const queryWithoutFacet = copyOfQuery.filter((query) => {
|
112
|
+
return !configuredFacets.includes(query.i);
|
113
|
+
});
|
114
|
+
|
115
|
+
const params = {
|
116
|
+
query: [
|
117
|
+
...(queryWithoutFacet || []),
|
118
|
+
...(facetSettings || []).map((facet) => {
|
119
|
+
if (!facet?.field) return null;
|
120
|
+
|
121
|
+
const { valueToQuery } = resolveExtension(
|
122
|
+
'type',
|
123
|
+
facetWidgetTypes,
|
124
|
+
facet,
|
125
|
+
);
|
126
|
+
|
127
|
+
const name = facet.field.value;
|
128
|
+
const value = facets[name];
|
129
|
+
|
130
|
+
return valueToQuery({ value, facet });
|
131
|
+
}),
|
132
|
+
].filter((o) => !!o),
|
133
|
+
sort_on: sortOn || query.sort_on,
|
134
|
+
sort_order: sortOrder || query.sort_order,
|
135
|
+
b_size: query.b_size,
|
136
|
+
limit: query.limit,
|
137
|
+
block: id,
|
138
|
+
};
|
139
|
+
|
140
|
+
// Note Ideally the searchtext functionality should be restructured as being just
|
141
|
+
// another facet. But right now it's the same. This means that if a searchText
|
142
|
+
// is provided, it will override the SearchableText facet.
|
143
|
+
// If there is no searchText, the SearchableText in the query remains in effect.
|
144
|
+
// TODO eventually the searchText should be a distinct facet from SearchableText, and
|
145
|
+
// the two conditions could be combined, in comparison to the current state, when
|
146
|
+
// one overrides the other.
|
147
|
+
if (searchText) {
|
148
|
+
params.query = params.query.reduce(
|
149
|
+
// Remove SearchableText from query
|
150
|
+
(acc, kvp) => (kvp.i === 'SearchableText' ? acc : [...acc, kvp]),
|
151
|
+
[],
|
152
|
+
);
|
153
|
+
params.query.push({
|
154
|
+
i: 'SearchableText',
|
155
|
+
o: 'plone.app.querystring.operation.string.contains',
|
156
|
+
v: searchText,
|
157
|
+
});
|
158
|
+
}
|
159
|
+
|
160
|
+
return params;
|
161
|
+
}
|
162
|
+
|
163
|
+
const getSearchFields = (searchData) => {
|
164
|
+
return Object.assign(
|
165
|
+
{},
|
166
|
+
...SEARCH_ENDPOINT_FIELDS.map((k) => {
|
167
|
+
return searchData[k] ? { [k]: searchData[k] } : {};
|
168
|
+
}),
|
169
|
+
searchData.query ? { query: serializeQuery(searchData['query']) } : {},
|
170
|
+
);
|
171
|
+
};
|
172
|
+
|
173
|
+
/**
|
174
|
+
* A hook that will mirror the search block state to a hash location
|
175
|
+
*/
|
176
|
+
const useHashState = () => {
|
177
|
+
const location = useLocation();
|
178
|
+
const history = useHistory();
|
179
|
+
|
180
|
+
/**
|
181
|
+
* Required to maintain parameter compatibility.
|
182
|
+
With this we will maintain support for receiving hash (#) and search (?) type parameters.
|
183
|
+
*/
|
184
|
+
const oldState = React.useMemo(() => {
|
185
|
+
return {
|
186
|
+
...qs.parse(location.search),
|
187
|
+
...qs.parse(location.hash),
|
188
|
+
};
|
189
|
+
}, [location.hash, location.search]);
|
190
|
+
|
191
|
+
// This creates a shallow copy. Why is this needed?
|
192
|
+
const current = Object.assign(
|
193
|
+
{},
|
194
|
+
...Array.from(Object.keys(oldState)).map((k) => ({ [k]: oldState[k] })),
|
195
|
+
);
|
196
|
+
|
197
|
+
const setSearchData = React.useCallback(
|
198
|
+
(searchData) => {
|
199
|
+
const newParams = qs.parse(location.search);
|
200
|
+
|
201
|
+
let changed = false;
|
202
|
+
|
203
|
+
Object.keys(searchData)
|
204
|
+
.sort()
|
205
|
+
.forEach((k) => {
|
206
|
+
if (searchData[k]) {
|
207
|
+
newParams[k] = searchData[k];
|
208
|
+
if (oldState[k] !== searchData[k]) {
|
209
|
+
changed = true;
|
210
|
+
}
|
211
|
+
}
|
212
|
+
});
|
213
|
+
|
214
|
+
if (changed) {
|
215
|
+
history.push({
|
216
|
+
search: qs.stringify(newParams),
|
217
|
+
});
|
218
|
+
}
|
219
|
+
},
|
220
|
+
[history, oldState, location.search],
|
221
|
+
);
|
222
|
+
|
223
|
+
return [current, setSearchData];
|
224
|
+
};
|
225
|
+
|
226
|
+
/**
|
227
|
+
* A hook to make it possible to switch disable mirroring the search block
|
228
|
+
* state to the window location. When using the internal state we "start from
|
229
|
+
* scratch", as it's intended to be used in the edit page.
|
230
|
+
*/
|
231
|
+
const useSearchBlockState = (uniqueId, isEditMode) => {
|
232
|
+
const [hashState, setHashState] = useHashState();
|
233
|
+
const [internalState, setInternalState] = React.useState({});
|
234
|
+
|
235
|
+
return isEditMode
|
236
|
+
? [internalState, setInternalState]
|
237
|
+
: [hashState, setHashState];
|
238
|
+
};
|
239
|
+
|
240
|
+
// Simple compress/decompress the state in URL by replacing the lengthy string
|
241
|
+
const deserializeQuery = (q) => {
|
242
|
+
return JSON.parse(q)?.map((kvp) => ({
|
243
|
+
...kvp,
|
244
|
+
o: kvp.o.replace(/^paqo/, PAQO),
|
245
|
+
}));
|
246
|
+
};
|
247
|
+
const serializeQuery = (q) => {
|
248
|
+
return JSON.stringify(
|
249
|
+
q?.map((kvp) => ({ ...kvp, o: kvp.o.replace(PAQO, 'paqo') })),
|
250
|
+
);
|
251
|
+
};
|
252
|
+
|
253
|
+
const withSearch = (options) => (WrappedComponent) => {
|
254
|
+
const { inputDelay = 1000 } = options || {};
|
255
|
+
|
256
|
+
function WithSearch(props) {
|
257
|
+
const { data, id, editable = false } = props;
|
258
|
+
|
259
|
+
const [locationSearchData, setLocationSearchData] = useSearchBlockState(
|
260
|
+
id,
|
261
|
+
editable,
|
262
|
+
);
|
263
|
+
|
264
|
+
// TODO: Improve the hook dependencies out of the scope of https://github.com/plone/volto/pull/4662
|
265
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
266
|
+
const urlQuery = locationSearchData.query
|
267
|
+
? deserializeQuery(locationSearchData.query)
|
268
|
+
: [];
|
269
|
+
const urlSearchText =
|
270
|
+
locationSearchData.SearchableText ||
|
271
|
+
urlQuery.find(({ i }) => i === 'SearchableText')?.v ||
|
272
|
+
'';
|
273
|
+
|
274
|
+
// TODO: refactor, should use only useLocationStateManager()!!!
|
275
|
+
const [searchText, setSearchText] = React.useState(urlSearchText);
|
276
|
+
// TODO: Improve the hook dependencies out of the scope of https://github.com/plone/volto/pull/4662
|
277
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
278
|
+
const configuredFacets =
|
279
|
+
data.facets?.map((facet) => facet?.field?.value) || [];
|
280
|
+
|
281
|
+
// Here we are getting the initial value of the facet if Listing Query contains the same criteria as
|
282
|
+
// facet.
|
283
|
+
const queryData = data?.query?.query
|
284
|
+
? deserializeQuery(JSON.stringify(data?.query?.query))
|
285
|
+
: [];
|
286
|
+
|
287
|
+
let intializeFacetWithQueryValue = [];
|
288
|
+
|
289
|
+
for (let value of configuredFacets) {
|
290
|
+
const queryString = queryData.find((item) => item.i === value);
|
291
|
+
if (queryString) {
|
292
|
+
intializeFacetWithQueryValue = [
|
293
|
+
...intializeFacetWithQueryValue,
|
294
|
+
{ [queryString.i]: queryString.v },
|
295
|
+
];
|
296
|
+
}
|
297
|
+
}
|
298
|
+
|
299
|
+
const multiFacets = data.facets
|
300
|
+
?.filter((facet) => facet?.multiple)
|
301
|
+
.map((facet) => facet?.field?.value);
|
302
|
+
const [facets, setFacets] = React.useState(
|
303
|
+
Object.assign(
|
304
|
+
{},
|
305
|
+
...urlQuery.map(({ i, v }) => ({ [i]: v })),
|
306
|
+
// TODO: the 'o' should be kept. This would be a major refactoring of the facets
|
307
|
+
...intializeFacetWithQueryValue,
|
308
|
+
// support for simple filters like ?Subject=something
|
309
|
+
// TODO: since the move to hash params this is no longer working.
|
310
|
+
// We'd have to treat the location.search and manage it just like the
|
311
|
+
// hash, to support it. We can read it, but we'd have to reset it as
|
312
|
+
// well, so at that point what's the difference to the hash?
|
313
|
+
...configuredFacets.map((f) =>
|
314
|
+
locationSearchData[f]
|
315
|
+
? {
|
316
|
+
[f]:
|
317
|
+
multiFacets.indexOf(f) > -1
|
318
|
+
? [locationSearchData[f]]
|
319
|
+
: locationSearchData[f],
|
320
|
+
}
|
321
|
+
: {},
|
322
|
+
),
|
323
|
+
),
|
324
|
+
);
|
325
|
+
const previousUrlQuery = usePrevious(urlQuery);
|
326
|
+
|
327
|
+
// During first render the previousUrlQuery is undefined and urlQuery
|
328
|
+
// is empty so it ressetting the facet when you are navigating but during reload we have urlQuery and we need
|
329
|
+
// to set the facet at first render.
|
330
|
+
const preventOverrideOfFacetState =
|
331
|
+
previousUrlQuery === undefined && urlQuery.length === 0;
|
332
|
+
|
333
|
+
React.useEffect(() => {
|
334
|
+
if (
|
335
|
+
!isEqual(urlQuery, previousUrlQuery) &&
|
336
|
+
!preventOverrideOfFacetState
|
337
|
+
) {
|
338
|
+
setFacets(
|
339
|
+
Object.assign(
|
340
|
+
{},
|
341
|
+
...urlQuery.map(({ i, v }) => ({ [i]: v })), // TODO: the 'o' should be kept. This would be a major refactoring of the facets
|
342
|
+
|
343
|
+
// support for simple filters like ?Subject=something
|
344
|
+
// TODO: since the move to hash params this is no longer working.
|
345
|
+
// We'd have to treat the location.search and manage it just like the
|
346
|
+
// hash, to support it. We can read it, but we'd have to reset it as
|
347
|
+
// well, so at that point what's the difference to the hash?
|
348
|
+
...configuredFacets.map((f) =>
|
349
|
+
locationSearchData[f]
|
350
|
+
? {
|
351
|
+
[f]:
|
352
|
+
multiFacets.indexOf(f) > -1
|
353
|
+
? [locationSearchData[f]]
|
354
|
+
: locationSearchData[f],
|
355
|
+
}
|
356
|
+
: {},
|
357
|
+
),
|
358
|
+
),
|
359
|
+
);
|
360
|
+
}
|
361
|
+
}, [
|
362
|
+
urlQuery,
|
363
|
+
configuredFacets,
|
364
|
+
locationSearchData,
|
365
|
+
multiFacets,
|
366
|
+
previousUrlQuery,
|
367
|
+
preventOverrideOfFacetState,
|
368
|
+
]);
|
369
|
+
|
370
|
+
const [sortOn, setSortOn] = React.useState(data?.query?.sort_on);
|
371
|
+
const [sortOrder, setSortOrder] = React.useState(data?.query?.sort_order);
|
372
|
+
|
373
|
+
const [searchData, setSearchData] = React.useState(
|
374
|
+
getInitialState(data, facets, urlSearchText, id),
|
375
|
+
);
|
376
|
+
|
377
|
+
const deepFacets = JSON.stringify(facets);
|
378
|
+
const deepData = JSON.stringify(data);
|
379
|
+
React.useEffect(() => {
|
380
|
+
setSearchData(
|
381
|
+
getInitialState(
|
382
|
+
JSON.parse(deepData),
|
383
|
+
JSON.parse(deepFacets),
|
384
|
+
urlSearchText,
|
385
|
+
id,
|
386
|
+
sortOn,
|
387
|
+
sortOrder,
|
388
|
+
),
|
389
|
+
);
|
390
|
+
}, [deepData, deepFacets, urlSearchText, id, sortOn, sortOrder]);
|
391
|
+
|
392
|
+
const timeoutRef = React.useRef();
|
393
|
+
const facetSettings = data?.facets;
|
394
|
+
|
395
|
+
const deepQuery = JSON.stringify(data.query);
|
396
|
+
const onTriggerSearch = React.useCallback(
|
397
|
+
(
|
398
|
+
toSearchText = undefined,
|
399
|
+
toSearchFacets = undefined,
|
400
|
+
toSortOn = undefined,
|
401
|
+
toSortOrder = undefined,
|
402
|
+
) => {
|
403
|
+
if (timeoutRef.current) clearTimeout(timeoutRef.current);
|
404
|
+
timeoutRef.current = setTimeout(
|
405
|
+
() => {
|
406
|
+
const newSearchData = normalizeState({
|
407
|
+
id,
|
408
|
+
query: data.query || {},
|
409
|
+
facets: toSearchFacets || facets,
|
410
|
+
searchText: toSearchText ? toSearchText.trim() : '',
|
411
|
+
sortOn: toSortOn || undefined,
|
412
|
+
sortOrder: toSortOrder || sortOrder,
|
413
|
+
facetSettings,
|
414
|
+
});
|
415
|
+
if (toSearchFacets) setFacets(toSearchFacets);
|
416
|
+
if (toSortOn) setSortOn(toSortOn || undefined);
|
417
|
+
if (toSortOrder) setSortOrder(toSortOrder);
|
418
|
+
setSearchData(newSearchData);
|
419
|
+
setLocationSearchData(getSearchFields(newSearchData));
|
420
|
+
},
|
421
|
+
toSearchFacets ? inputDelay / 3 : inputDelay,
|
422
|
+
);
|
423
|
+
},
|
424
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
425
|
+
[
|
426
|
+
// Use deep comparison of data.query
|
427
|
+
deepQuery,
|
428
|
+
facets,
|
429
|
+
id,
|
430
|
+
setLocationSearchData,
|
431
|
+
searchText,
|
432
|
+
sortOn,
|
433
|
+
sortOrder,
|
434
|
+
facetSettings,
|
435
|
+
],
|
436
|
+
);
|
437
|
+
|
438
|
+
const removeSearchQuery = () => {
|
439
|
+
let newSearchData = { ...searchData };
|
440
|
+
newSearchData.query = searchData.query.reduce(
|
441
|
+
// Remove SearchableText from query
|
442
|
+
(acc, kvp) => (kvp.i === 'SearchableText' ? acc : [...acc, kvp]),
|
443
|
+
[],
|
444
|
+
);
|
445
|
+
setSearchData(newSearchData);
|
446
|
+
setLocationSearchData(getSearchFields(newSearchData));
|
447
|
+
};
|
448
|
+
|
449
|
+
const querystringResults = useSelector(
|
450
|
+
(state) => state.querystringsearch.subrequests,
|
451
|
+
);
|
452
|
+
const totalItems =
|
453
|
+
querystringResults[id]?.total || querystringResults[id]?.items?.length;
|
454
|
+
|
455
|
+
return (
|
456
|
+
<WrappedComponent
|
457
|
+
{...props}
|
458
|
+
searchData={searchData}
|
459
|
+
facets={facets}
|
460
|
+
setFacets={setFacets}
|
461
|
+
setSortOn={setSortOn}
|
462
|
+
setSortOrder={setSortOrder}
|
463
|
+
sortOn={sortOn}
|
464
|
+
sortOrder={sortOrder}
|
465
|
+
searchedText={urlSearchText}
|
466
|
+
searchText={searchText}
|
467
|
+
removeSearchQuery={removeSearchQuery}
|
468
|
+
setSearchText={setSearchText}
|
469
|
+
onTriggerSearch={onTriggerSearch}
|
470
|
+
totalItems={totalItems}
|
471
|
+
/>
|
472
|
+
);
|
473
|
+
}
|
474
|
+
WithSearch.displayName = `WithSearch(${getDisplayName(WrappedComponent)})`;
|
475
|
+
|
476
|
+
return WithSearch;
|
477
|
+
};
|
478
|
+
|
479
|
+
export default withSearch;
|
@@ -0,0 +1,511 @@
|
|
1
|
+
/* this customization is used to put default alt as the rights field
|
2
|
+
*/
|
3
|
+
import React, { Component } from 'react';
|
4
|
+
import PropTypes from 'prop-types';
|
5
|
+
import { compose } from 'redux';
|
6
|
+
import { connect } from 'react-redux';
|
7
|
+
import { defineMessages, FormattedMessage, injectIntl } from 'react-intl';
|
8
|
+
import { Input, Segment, Breadcrumb } from 'semantic-ui-react';
|
9
|
+
|
10
|
+
import { join } from 'lodash';
|
11
|
+
|
12
|
+
// These absolute imports (without using the corresponding centralized index.js) are required
|
13
|
+
// to cut circular import problems, this file should never use them. This is because of
|
14
|
+
// the very nature of the functionality of the component and its relationship with others
|
15
|
+
import { searchContent } from '@plone/volto/actions/search/search';
|
16
|
+
import Icon from '@plone/volto/components/theme/Icon/Icon';
|
17
|
+
import { flattenToAppURL, isInternalURL } from '@plone/volto/helpers/Url/Url';
|
18
|
+
import config from '@plone/volto/registry';
|
19
|
+
|
20
|
+
import backSVG from '@plone/volto/icons/back.svg';
|
21
|
+
import folderSVG from '@plone/volto/icons/folder.svg';
|
22
|
+
import clearSVG from '@plone/volto/icons/clear.svg';
|
23
|
+
import searchSVG from '@plone/volto/icons/zoom.svg';
|
24
|
+
import linkSVG from '@plone/volto/icons/link.svg';
|
25
|
+
import homeSVG from '@plone/volto/icons/home.svg';
|
26
|
+
|
27
|
+
import ObjectBrowserNav from '@plone/volto/components/manage/Sidebar/ObjectBrowserNav';
|
28
|
+
|
29
|
+
const messages = defineMessages({
|
30
|
+
SearchInputPlaceholder: {
|
31
|
+
id: 'Search content',
|
32
|
+
defaultMessage: 'Search content',
|
33
|
+
},
|
34
|
+
SelectedItems: {
|
35
|
+
id: 'Selected items',
|
36
|
+
defaultMessage: 'Selected items',
|
37
|
+
},
|
38
|
+
back: {
|
39
|
+
id: 'Back',
|
40
|
+
defaultMessage: 'Back',
|
41
|
+
},
|
42
|
+
search: {
|
43
|
+
id: 'Search SVG',
|
44
|
+
defaultMessage: 'Search SVG',
|
45
|
+
},
|
46
|
+
of: { id: 'Selected items - x of y', defaultMessage: 'of' },
|
47
|
+
});
|
48
|
+
|
49
|
+
function getParentURL(url) {
|
50
|
+
return flattenToAppURL(`${join(url.split('/').slice(0, -1), '/')}`) || '/';
|
51
|
+
}
|
52
|
+
|
53
|
+
/**
|
54
|
+
* ObjectBrowserBody container class.
|
55
|
+
* @class ObjectBrowserBody
|
56
|
+
* @extends Component
|
57
|
+
*/
|
58
|
+
class ObjectBrowserBody extends Component {
|
59
|
+
/**
|
60
|
+
* Property types.
|
61
|
+
* @property {Object} propTypes Property types.
|
62
|
+
* @static
|
63
|
+
*/
|
64
|
+
static propTypes = {
|
65
|
+
block: PropTypes.string.isRequired,
|
66
|
+
mode: PropTypes.string.isRequired,
|
67
|
+
data: PropTypes.any.isRequired,
|
68
|
+
searchSubrequests: PropTypes.objectOf(PropTypes.any).isRequired,
|
69
|
+
searchContent: PropTypes.func.isRequired,
|
70
|
+
closeObjectBrowser: PropTypes.func.isRequired,
|
71
|
+
onChangeBlock: PropTypes.func.isRequired,
|
72
|
+
onSelectItem: PropTypes.func,
|
73
|
+
dataName: PropTypes.string,
|
74
|
+
maximumSelectionSize: PropTypes.number,
|
75
|
+
contextURL: PropTypes.string,
|
76
|
+
searchableTypes: PropTypes.arrayOf(PropTypes.string),
|
77
|
+
};
|
78
|
+
|
79
|
+
/**
|
80
|
+
* Default properties.
|
81
|
+
* @property {Object} defaultProps Default properties.
|
82
|
+
* @static
|
83
|
+
*/
|
84
|
+
static defaultProps = {
|
85
|
+
image: '',
|
86
|
+
href: '',
|
87
|
+
onSelectItem: null,
|
88
|
+
dataName: null,
|
89
|
+
selectableTypes: [],
|
90
|
+
searchableTypes: null,
|
91
|
+
maximumSelectionSize: null,
|
92
|
+
};
|
93
|
+
|
94
|
+
/**
|
95
|
+
* Constructor
|
96
|
+
* @method constructor
|
97
|
+
* @param {Object} props Component properties
|
98
|
+
* @constructs WysiwygEditor
|
99
|
+
*/
|
100
|
+
constructor(props) {
|
101
|
+
super(props);
|
102
|
+
this.state = {
|
103
|
+
currentFolder:
|
104
|
+
this.props.mode === 'multiple' ? '/' : this.props.contextURL || '/',
|
105
|
+
currentImageFolder:
|
106
|
+
this.props.mode === 'multiple'
|
107
|
+
? '/'
|
108
|
+
: this.props.mode === 'image' && this.props.data?.url
|
109
|
+
? getParentURL(this.props.data.url)
|
110
|
+
: '/',
|
111
|
+
currentLinkFolder:
|
112
|
+
this.props.mode === 'multiple'
|
113
|
+
? '/'
|
114
|
+
: this.props.mode === 'link' && this.props.data?.href
|
115
|
+
? getParentURL(this.props.data.href)
|
116
|
+
: '/',
|
117
|
+
parentFolder: '',
|
118
|
+
selectedImage:
|
119
|
+
this.props.mode === 'multiple'
|
120
|
+
? ''
|
121
|
+
: this.props.mode === 'image' && this.props.data?.url
|
122
|
+
? flattenToAppURL(this.props.data.url)
|
123
|
+
: '',
|
124
|
+
selectedHref:
|
125
|
+
this.props.mode === 'multiple'
|
126
|
+
? ''
|
127
|
+
: this.props.mode === 'link' && this.props.data?.href
|
128
|
+
? flattenToAppURL(this.props.data.href)
|
129
|
+
: '',
|
130
|
+
showSearchInput: false,
|
131
|
+
// In image mode, the searchable types default to the image types which
|
132
|
+
// can be overridden with the property if specified.
|
133
|
+
searchableTypes:
|
134
|
+
this.props.mode === 'image'
|
135
|
+
? this.props.searchableTypes || config.settings.imageObjects
|
136
|
+
: this.props.searchableTypes,
|
137
|
+
};
|
138
|
+
this.searchInputRef = React.createRef();
|
139
|
+
}
|
140
|
+
|
141
|
+
/**
|
142
|
+
* Component did mount
|
143
|
+
* @method componentDidMount
|
144
|
+
* @returns {undefined}
|
145
|
+
*/
|
146
|
+
componentDidMount() {
|
147
|
+
this.initialSearch(this.props.mode);
|
148
|
+
}
|
149
|
+
|
150
|
+
initialSearch = (mode) => {
|
151
|
+
const currentSelected =
|
152
|
+
mode === 'multiple'
|
153
|
+
? ''
|
154
|
+
: mode === 'image'
|
155
|
+
? this.state.selectedImage
|
156
|
+
: this.state.selectedHref;
|
157
|
+
if (currentSelected && isInternalURL(currentSelected)) {
|
158
|
+
this.props.searchContent(
|
159
|
+
getParentURL(currentSelected),
|
160
|
+
{
|
161
|
+
'path.depth': 1,
|
162
|
+
sort_on: 'getObjPositionInParent',
|
163
|
+
metadata_fields: '_all',
|
164
|
+
b_size: 1000,
|
165
|
+
},
|
166
|
+
`${this.props.block}-${mode}`,
|
167
|
+
);
|
168
|
+
} else {
|
169
|
+
this.props.searchContent(
|
170
|
+
this.state.currentFolder,
|
171
|
+
{
|
172
|
+
'path.depth': 1,
|
173
|
+
sort_on: 'getObjPositionInParent',
|
174
|
+
metadata_fields: '_all',
|
175
|
+
b_size: 1000,
|
176
|
+
},
|
177
|
+
`${this.props.block}-${mode}`,
|
178
|
+
);
|
179
|
+
}
|
180
|
+
};
|
181
|
+
|
182
|
+
navigateTo = (id) => {
|
183
|
+
this.props.searchContent(
|
184
|
+
id,
|
185
|
+
{
|
186
|
+
'path.depth': 1,
|
187
|
+
sort_on: 'getObjPositionInParent',
|
188
|
+
metadata_fields: '_all',
|
189
|
+
b_size: 1000,
|
190
|
+
},
|
191
|
+
`${this.props.block}-${this.props.mode}`,
|
192
|
+
);
|
193
|
+
const parent = `${join(id.split('/').slice(0, -1), '/')}` || '/';
|
194
|
+
this.setState(() => ({
|
195
|
+
parentFolder: parent,
|
196
|
+
currentFolder: id || '/',
|
197
|
+
}));
|
198
|
+
};
|
199
|
+
|
200
|
+
toggleSearchInput = () =>
|
201
|
+
this.setState(
|
202
|
+
(prevState) => ({
|
203
|
+
showSearchInput: !prevState.showSearchInput,
|
204
|
+
}),
|
205
|
+
() => {
|
206
|
+
if (this.searchInputRef?.current) this.searchInputRef.current.focus();
|
207
|
+
},
|
208
|
+
);
|
209
|
+
|
210
|
+
onSearch = (e) => {
|
211
|
+
const text = flattenToAppURL(e.target.value);
|
212
|
+
if (text.startsWith('/')) {
|
213
|
+
this.setState({ currentFolder: text });
|
214
|
+
this.props.searchContent(
|
215
|
+
text,
|
216
|
+
{
|
217
|
+
'path.depth': 1,
|
218
|
+
sort_on: 'getObjPositionInParent',
|
219
|
+
metadata_fields: '_all',
|
220
|
+
portal_type: this.state.searchableTypes,
|
221
|
+
},
|
222
|
+
`${this.props.block}-${this.props.mode}`,
|
223
|
+
);
|
224
|
+
} else {
|
225
|
+
text.length > 2
|
226
|
+
? this.props.searchContent(
|
227
|
+
'/',
|
228
|
+
{
|
229
|
+
SearchableText: `${text}*`,
|
230
|
+
metadata_fields: '_all',
|
231
|
+
portal_type: this.state.searchableTypes,
|
232
|
+
},
|
233
|
+
`${this.props.block}-${this.props.mode}`,
|
234
|
+
)
|
235
|
+
: this.props.searchContent(
|
236
|
+
'/',
|
237
|
+
{
|
238
|
+
'path.depth': 1,
|
239
|
+
sort_on: 'getObjPositionInParent',
|
240
|
+
metadata_fields: '_all',
|
241
|
+
portal_type: this.state.searchableTypes,
|
242
|
+
},
|
243
|
+
`${this.props.block}-${this.props.mode}`,
|
244
|
+
);
|
245
|
+
}
|
246
|
+
};
|
247
|
+
|
248
|
+
onSelectItem = (item) => {
|
249
|
+
const url = item['@id'];
|
250
|
+
const { block, data, mode, dataName, onChangeBlock } = this.props;
|
251
|
+
|
252
|
+
const updateState = (mode) => {
|
253
|
+
switch (mode) {
|
254
|
+
case 'image':
|
255
|
+
this.setState({
|
256
|
+
selectedImage: url,
|
257
|
+
currentImageFolder: getParentURL(url),
|
258
|
+
});
|
259
|
+
break;
|
260
|
+
case 'link':
|
261
|
+
this.setState({
|
262
|
+
selectedHref: url,
|
263
|
+
currentLinkFolder: getParentURL(url),
|
264
|
+
});
|
265
|
+
break;
|
266
|
+
default:
|
267
|
+
break;
|
268
|
+
}
|
269
|
+
};
|
270
|
+
|
271
|
+
if (dataName) {
|
272
|
+
onChangeBlock(block, {
|
273
|
+
...data,
|
274
|
+
[dataName]: url,
|
275
|
+
});
|
276
|
+
} else if (this.props.onSelectItem) {
|
277
|
+
this.props.onSelectItem(url, item);
|
278
|
+
} else if (mode === 'image') {
|
279
|
+
onChangeBlock(block, {
|
280
|
+
...data,
|
281
|
+
url: flattenToAppURL(item.getURL),
|
282
|
+
alt: item.title || item.description || '',
|
283
|
+
copyright: item.rights || '',
|
284
|
+
});
|
285
|
+
} else if (mode === 'link') {
|
286
|
+
onChangeBlock(block, {
|
287
|
+
...data,
|
288
|
+
href: flattenToAppURL(url),
|
289
|
+
});
|
290
|
+
}
|
291
|
+
updateState(mode);
|
292
|
+
};
|
293
|
+
|
294
|
+
onChangeBlockData = (key, value) => {
|
295
|
+
this.props.onChangeBlock(this.props.block, {
|
296
|
+
...this.props.data,
|
297
|
+
[key]: value,
|
298
|
+
});
|
299
|
+
};
|
300
|
+
|
301
|
+
isSelectable = (item) => {
|
302
|
+
return this.props.selectableTypes.length > 0
|
303
|
+
? this.props.selectableTypes.indexOf(item['@type']) >= 0
|
304
|
+
: true;
|
305
|
+
};
|
306
|
+
|
307
|
+
handleClickOnItem = (item) => {
|
308
|
+
if (this.props.mode === 'image') {
|
309
|
+
if (item.is_folderish) {
|
310
|
+
this.navigateTo(item['@id']);
|
311
|
+
}
|
312
|
+
if (config.settings.imageObjects.includes(item['@type'])) {
|
313
|
+
this.onSelectItem(item);
|
314
|
+
}
|
315
|
+
} else {
|
316
|
+
if (this.isSelectable(item)) {
|
317
|
+
if (
|
318
|
+
!this.props.maximumSelectionSize ||
|
319
|
+
this.props.mode === 'multiple' ||
|
320
|
+
!this.props.data ||
|
321
|
+
this.props.data.length < this.props.maximumSelectionSize
|
322
|
+
) {
|
323
|
+
this.onSelectItem(item);
|
324
|
+
let length = this.props.data ? this.props.data.length : 0;
|
325
|
+
|
326
|
+
let stopSelecting =
|
327
|
+
this.props.mode !== 'multiple' ||
|
328
|
+
(this.props.maximumSelectionSize > 0 &&
|
329
|
+
length + 1 >= this.props.maximumSelectionSize);
|
330
|
+
|
331
|
+
if (stopSelecting) {
|
332
|
+
this.props.closeObjectBrowser();
|
333
|
+
}
|
334
|
+
} else {
|
335
|
+
this.props.closeObjectBrowser();
|
336
|
+
}
|
337
|
+
} else {
|
338
|
+
this.navigateTo(item['@id']);
|
339
|
+
}
|
340
|
+
}
|
341
|
+
};
|
342
|
+
|
343
|
+
handleDoubleClickOnItem = (item) => {
|
344
|
+
if (this.props.mode === 'image') {
|
345
|
+
if (item.is_folderish) {
|
346
|
+
this.navigateTo(item['@id']);
|
347
|
+
}
|
348
|
+
if (config.settings.imageObjects.includes(item['@type'])) {
|
349
|
+
this.onSelectItem(item);
|
350
|
+
this.props.closeObjectBrowser();
|
351
|
+
}
|
352
|
+
} else {
|
353
|
+
if (this.isSelectable(item)) {
|
354
|
+
if (this.props.data.length < this.props.maximumSelectionSize) {
|
355
|
+
this.onSelectItem(item);
|
356
|
+
}
|
357
|
+
this.props.closeObjectBrowser();
|
358
|
+
} else {
|
359
|
+
this.navigateTo(item['@id']);
|
360
|
+
}
|
361
|
+
}
|
362
|
+
};
|
363
|
+
|
364
|
+
/**
|
365
|
+
* Render method.
|
366
|
+
* @method render
|
367
|
+
* @returns {string} Markup for the component.
|
368
|
+
*/
|
369
|
+
render() {
|
370
|
+
return (
|
371
|
+
<Segment.Group raised>
|
372
|
+
<header className="header pulled">
|
373
|
+
<div className="vertical divider" />
|
374
|
+
{this.state.currentFolder === '/' ? (
|
375
|
+
<>
|
376
|
+
{this.props.mode === 'image' ? (
|
377
|
+
<Icon name={folderSVG} size="24px" />
|
378
|
+
) : (
|
379
|
+
<Icon name={linkSVG} size="24px" />
|
380
|
+
)}
|
381
|
+
</>
|
382
|
+
) : (
|
383
|
+
<button
|
384
|
+
aria-label={this.props.intl.formatMessage(messages.back)}
|
385
|
+
onClick={() => this.navigateTo(this.state.parentFolder)}
|
386
|
+
>
|
387
|
+
<Icon name={backSVG} size="24px" />
|
388
|
+
</button>
|
389
|
+
)}
|
390
|
+
{this.state.showSearchInput ? (
|
391
|
+
<Input
|
392
|
+
className="search"
|
393
|
+
ref={this.searchInputRef}
|
394
|
+
onChange={this.onSearch}
|
395
|
+
placeholder={this.props.intl.formatMessage(
|
396
|
+
messages.SearchInputPlaceholder,
|
397
|
+
)}
|
398
|
+
/>
|
399
|
+
) : this.props.mode === 'image' ? (
|
400
|
+
<h2>
|
401
|
+
<FormattedMessage
|
402
|
+
id="Choose Image"
|
403
|
+
defaultMessage="Choose Image"
|
404
|
+
/>
|
405
|
+
</h2>
|
406
|
+
) : (
|
407
|
+
<h2>
|
408
|
+
<FormattedMessage
|
409
|
+
id="Choose Target"
|
410
|
+
defaultMessage="Choose Target"
|
411
|
+
/>
|
412
|
+
</h2>
|
413
|
+
)}
|
414
|
+
|
415
|
+
<button
|
416
|
+
aria-label={this.props.intl.formatMessage(messages.search)}
|
417
|
+
onClick={this.toggleSearchInput}
|
418
|
+
>
|
419
|
+
<Icon name={searchSVG} size="24px" />
|
420
|
+
</button>
|
421
|
+
<button className="clearSVG" onClick={this.props.closeObjectBrowser}>
|
422
|
+
<Icon name={clearSVG} size="24px" />
|
423
|
+
</button>
|
424
|
+
</header>
|
425
|
+
<Segment secondary className="breadcrumbs" vertical>
|
426
|
+
<Breadcrumb>
|
427
|
+
{this.state.currentFolder !== '/' ? (
|
428
|
+
this.state.currentFolder.split('/').map((item, index, items) => {
|
429
|
+
return (
|
430
|
+
<React.Fragment key={`divider-${item}-${index}`}>
|
431
|
+
{index === 0 ? (
|
432
|
+
<Breadcrumb.Section onClick={() => this.navigateTo('/')}>
|
433
|
+
<Icon
|
434
|
+
className="home-icon"
|
435
|
+
name={homeSVG}
|
436
|
+
size="18px"
|
437
|
+
/>
|
438
|
+
</Breadcrumb.Section>
|
439
|
+
) : (
|
440
|
+
<>
|
441
|
+
<Breadcrumb.Divider key={`divider-${item.url}`} />
|
442
|
+
<Breadcrumb.Section
|
443
|
+
onClick={() =>
|
444
|
+
this.navigateTo(items.slice(0, index + 1).join('/'))
|
445
|
+
}
|
446
|
+
>
|
447
|
+
{item}
|
448
|
+
</Breadcrumb.Section>
|
449
|
+
</>
|
450
|
+
)}
|
451
|
+
</React.Fragment>
|
452
|
+
);
|
453
|
+
})
|
454
|
+
) : (
|
455
|
+
<Breadcrumb.Section onClick={() => this.navigateTo('/')}>
|
456
|
+
<Icon className="home-icon" name={homeSVG} size="18px" />
|
457
|
+
</Breadcrumb.Section>
|
458
|
+
)}
|
459
|
+
</Breadcrumb>
|
460
|
+
</Segment>
|
461
|
+
{this.props.mode === 'multiple' && (
|
462
|
+
<Segment className="infos">
|
463
|
+
{this.props.intl.formatMessage(messages.SelectedItems)}:{' '}
|
464
|
+
{this.props.data?.length}
|
465
|
+
{this.props.maximumSelectionSize > 0 && (
|
466
|
+
<>
|
467
|
+
{' '}
|
468
|
+
{this.props.intl.formatMessage(messages.of)}{' '}
|
469
|
+
{this.props.maximumSelectionSize}
|
470
|
+
</>
|
471
|
+
)}
|
472
|
+
</Segment>
|
473
|
+
)}
|
474
|
+
<ObjectBrowserNav
|
475
|
+
currentSearchResults={
|
476
|
+
this.props.searchSubrequests[
|
477
|
+
`${this.props.block}-${this.props.mode}`
|
478
|
+
]
|
479
|
+
}
|
480
|
+
selected={
|
481
|
+
this.props.mode === 'multiple'
|
482
|
+
? this.props.data
|
483
|
+
: [
|
484
|
+
{
|
485
|
+
'@id':
|
486
|
+
this.props.mode === 'image'
|
487
|
+
? this.state.selectedImage
|
488
|
+
: this.state.selectedHref,
|
489
|
+
},
|
490
|
+
]
|
491
|
+
}
|
492
|
+
handleClickOnItem={this.handleClickOnItem}
|
493
|
+
handleDoubleClickOnItem={this.handleDoubleClickOnItem}
|
494
|
+
mode={this.props.mode}
|
495
|
+
navigateTo={this.navigateTo}
|
496
|
+
isSelectable={this.isSelectable}
|
497
|
+
/>
|
498
|
+
</Segment.Group>
|
499
|
+
);
|
500
|
+
}
|
501
|
+
}
|
502
|
+
|
503
|
+
export default compose(
|
504
|
+
injectIntl,
|
505
|
+
connect(
|
506
|
+
(state) => ({
|
507
|
+
searchSubrequests: state.search.subrequests,
|
508
|
+
}),
|
509
|
+
{ searchContent },
|
510
|
+
),
|
511
|
+
)(ObjectBrowserBody);
|
@@ -0,0 +1,154 @@
|
|
1
|
+
import React from 'react';
|
2
|
+
import { defineMessages, useIntl } from 'react-intl';
|
3
|
+
import { Segment, Header, List } from 'semantic-ui-react';
|
4
|
+
import {
|
5
|
+
When,
|
6
|
+
Recurrence,
|
7
|
+
} from '@plone/volto/components/theme/View/EventDatesInfo';
|
8
|
+
import { Icon } from '@plone/volto/components';
|
9
|
+
import { expandToBackendURL } from '@plone/volto/helpers';
|
10
|
+
|
11
|
+
import calendarSVG from '@plone/volto/icons/calendar.svg';
|
12
|
+
|
13
|
+
const messages = defineMessages({
|
14
|
+
what: {
|
15
|
+
id: 'event_what',
|
16
|
+
defaultMessage: 'What',
|
17
|
+
},
|
18
|
+
when: {
|
19
|
+
id: 'event_when',
|
20
|
+
defaultMessage: 'When',
|
21
|
+
},
|
22
|
+
allDates: {
|
23
|
+
id: 'event_alldates',
|
24
|
+
defaultMessage: 'All dates',
|
25
|
+
},
|
26
|
+
downloadEvent: {
|
27
|
+
id: 'Download Event',
|
28
|
+
defaultMessage: 'Download Event',
|
29
|
+
},
|
30
|
+
where: {
|
31
|
+
id: 'event_where',
|
32
|
+
defaultMessage: 'Where',
|
33
|
+
},
|
34
|
+
contactName: {
|
35
|
+
id: 'event_contactname',
|
36
|
+
defaultMessage: 'Contact Name',
|
37
|
+
},
|
38
|
+
contactPhone: {
|
39
|
+
id: 'event_contactphone',
|
40
|
+
defaultMessage: 'Contact Phone',
|
41
|
+
},
|
42
|
+
attendees: {
|
43
|
+
id: 'event_attendees',
|
44
|
+
defaultMessage: 'Attendees',
|
45
|
+
},
|
46
|
+
website: {
|
47
|
+
id: 'event_website',
|
48
|
+
defaultMessage: 'Website',
|
49
|
+
},
|
50
|
+
visitWebsite: {
|
51
|
+
id: 'visit_external_website',
|
52
|
+
defaultMessage: 'Visit external website',
|
53
|
+
},
|
54
|
+
});
|
55
|
+
|
56
|
+
const EventDetails = ({ content, display_as = 'aside' }) => {
|
57
|
+
const intl = useIntl();
|
58
|
+
return (
|
59
|
+
<Segment
|
60
|
+
as={display_as}
|
61
|
+
{...(display_as === 'aside' ? { floated: 'right' } : {})}
|
62
|
+
>
|
63
|
+
{content.subjects?.length > 0 && (
|
64
|
+
<>
|
65
|
+
<Header dividing sub>
|
66
|
+
{intl.formatMessage(messages.what)}
|
67
|
+
</Header>
|
68
|
+
<List items={content.subjects} />
|
69
|
+
</>
|
70
|
+
)}
|
71
|
+
<Header dividing sub>
|
72
|
+
{intl.formatMessage(messages.when)}
|
73
|
+
</Header>
|
74
|
+
<When
|
75
|
+
start={content.start}
|
76
|
+
end={content.end}
|
77
|
+
whole_day={content.whole_day}
|
78
|
+
open_end={content.open_end}
|
79
|
+
/>
|
80
|
+
{content.recurrence && (
|
81
|
+
<>
|
82
|
+
<Header dividing sub>
|
83
|
+
{intl.formatMessage(messages.allDates)}
|
84
|
+
</Header>
|
85
|
+
<Recurrence recurrence={content.recurrence} start={content.start} />
|
86
|
+
</>
|
87
|
+
)}
|
88
|
+
{content.location && (
|
89
|
+
<>
|
90
|
+
<Header dividing sub>
|
91
|
+
{intl.formatMessage(messages.where)}
|
92
|
+
</Header>
|
93
|
+
<p>{content.location}</p>
|
94
|
+
</>
|
95
|
+
)}
|
96
|
+
{content.contact_name && (
|
97
|
+
<>
|
98
|
+
<Header dividing sub>
|
99
|
+
{intl.formatMessage(messages.contactName)}
|
100
|
+
</Header>
|
101
|
+
<p>
|
102
|
+
{content.contact_email ? (
|
103
|
+
<a href={`mailto:${content.contact_email}`}>
|
104
|
+
{content.contact_name}
|
105
|
+
</a>
|
106
|
+
) : (
|
107
|
+
content.contact_name
|
108
|
+
)}
|
109
|
+
</p>
|
110
|
+
</>
|
111
|
+
)}
|
112
|
+
{content.contact_phone && (
|
113
|
+
<>
|
114
|
+
<Header dividing sub>
|
115
|
+
{intl.formatMessage(messages.contactPhone)}
|
116
|
+
</Header>
|
117
|
+
<p>{content.contact_phone}</p>
|
118
|
+
</>
|
119
|
+
)}
|
120
|
+
{content.attendees?.length > 0 && (
|
121
|
+
<>
|
122
|
+
<Header dividing sub>
|
123
|
+
{intl.formatMessage(messages.attendees)}
|
124
|
+
</Header>
|
125
|
+
<List items={content.attendees} />
|
126
|
+
</>
|
127
|
+
)}
|
128
|
+
{content.event_url && (
|
129
|
+
<>
|
130
|
+
<Header dividing sub>
|
131
|
+
{intl.formatMessage(messages.website)}
|
132
|
+
</Header>
|
133
|
+
<p>
|
134
|
+
<a href={content.event_url} target="_blank" rel="noopener">
|
135
|
+
{intl.formatMessage(messages.visitWebsite)}
|
136
|
+
</a>
|
137
|
+
</p>
|
138
|
+
</>
|
139
|
+
)}
|
140
|
+
<div className="download-event">
|
141
|
+
<Icon name={calendarSVG} />
|
142
|
+
<a
|
143
|
+
className="ics-download"
|
144
|
+
target="_blank"
|
145
|
+
href={`${expandToBackendURL(content['@id'])}/ics_view`}
|
146
|
+
>
|
147
|
+
{intl.formatMessage(messages.downloadEvent)}
|
148
|
+
</a>
|
149
|
+
</div>
|
150
|
+
</Segment>
|
151
|
+
);
|
152
|
+
};
|
153
|
+
|
154
|
+
export default EventDetails;
|
package/src/reducers/index.js
CHANGED