@eeacms/volto-cca-policy 0.3.104 → 0.3.106

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 CHANGED
@@ -4,6 +4,30 @@ 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
+ ### [0.3.106](https://github.com/eea/volto-cca-policy/compare/0.3.105...0.3.106) - 20 March 2026
8
+
9
+ #### :rocket: New Features
10
+
11
+ - feat(teaser): preserve query strings in CardTitle.jsx [kreafox - [`a0b70ea`](https://github.com/eea/volto-cca-policy/commit/a0b70ea9055a8b1d3a6d7c93aedad6e5b36bf3a1)]
12
+
13
+ #### :hammer_and_wrench: Others
14
+
15
+ - core: add CardTitle.jsx for customization [kreafox - [`c6b34db`](https://github.com/eea/volto-cca-policy/commit/c6b34db474de98f5a04e4d69a5b754c508c85b2d)]
16
+ ### [0.3.105](https://github.com/eea/volto-cca-policy/compare/0.3.104...0.3.105) - 19 March 2026
17
+
18
+ #### :rocket: New Features
19
+
20
+ - feat(teaser): preserve query strings in SimpleItemTemplates.jsx [kreafox - [`dec6b82`](https://github.com/eea/volto-cca-policy/commit/dec6b82b60e50e881a352e910227ef32ef2812a2)]
21
+ - feat(teaser): modified ObjectBrowserWidget to show links with query strings for manual links [kreafox - [`a1141ee`](https://github.com/eea/volto-cca-policy/commit/a1141eec28f6731b715f3cece352df11e3cf3ee6)]
22
+
23
+ #### :house: Internal changes
24
+
25
+ - style: Automated code fix [eea-jenkins - [`c93a86b`](https://github.com/eea/volto-cca-policy/commit/c93a86be5bb37bc5b736d574baac0b6bc489fb2c)]
26
+
27
+ #### :hammer_and_wrench: Others
28
+
29
+ - core: added original SimpleItemTemplates.jsx for customization [kreafox - [`d707919`](https://github.com/eea/volto-cca-policy/commit/d7079194a82a4a91b9d91345c765156360706608)]
30
+ - core: added ObjectBrowserWidget.jsx customization [kreafox - [`50cd014`](https://github.com/eea/volto-cca-policy/commit/50cd014f4ca7bfb070b5760cbf7186388bf9ab6d)]
7
31
  ### [0.3.104](https://github.com/eea/volto-cca-policy/compare/0.3.103...0.3.104) - 5 March 2026
8
32
 
9
33
  #### :rocket: Dependency updates
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eeacms/volto-cca-policy",
3
- "version": "0.3.104",
3
+ "version": "0.3.106",
4
4
  "description": "@eeacms/volto-cca-policy: Volto add-on",
5
5
  "main": "src/index.js",
6
6
  "author": "European Environment Agency: IDM2 A-Team",
@@ -0,0 +1 @@
1
+ Customized SimpleItemTemplates.jsx and CardTitle.jsx to preserve query strings. (refs #299560)
@@ -0,0 +1,62 @@
1
+ import cx from 'classnames';
2
+ import { ConditionalLink } from '@plone/volto/components';
3
+ import { getBaseUrl } from '@plone/volto/helpers';
4
+ import { getVoltoStyles } from '@eeacms/volto-listing-block/schema-utils';
5
+
6
+ const getStyles = (props) => {
7
+ const { itemModel = {} } = props;
8
+ const res = {};
9
+ if (itemModel.maxDescription) {
10
+ res[`max-${itemModel.maxDescription}-lines`] = true;
11
+ }
12
+ if (itemModel.maxTitle) {
13
+ res[`title-max-${itemModel.maxTitle}-lines`] = true;
14
+ }
15
+ return res;
16
+ };
17
+
18
+ const getItemHref = (item) => {
19
+ const preservedHref = item?.linkHref || item?.linkWithHash;
20
+ if (preservedHref) {
21
+ return preservedHref;
22
+ }
23
+ return item?.['@id'] ? getBaseUrl(item['@id']) : undefined;
24
+ };
25
+
26
+ const BasicItem = (props) => {
27
+ const { item, className, isEditMode = false } = props;
28
+ const { hasMetaType } = props.itemModel;
29
+ const styles = getStyles(props);
30
+ const href = getItemHref(item);
31
+
32
+ return (
33
+ <div
34
+ className={cx(
35
+ 'u-item listing-item simple-listing-item',
36
+ getVoltoStyles(styles),
37
+ className,
38
+ )}
39
+ >
40
+ <div className="wrapper">
41
+ <div className="slot-top">
42
+ <ConditionalLink to={href} condition={!isEditMode && !!href}>
43
+ <div className="listing-body">
44
+ <p className="listing-header">
45
+ {item.title ? item.title : item.id}
46
+ </p>
47
+ </div>
48
+ </ConditionalLink>
49
+ </div>
50
+ <div className="simple-item-meta">
51
+ {hasMetaType && (
52
+ <span className="text-left">{item['type_title']}</span>
53
+ )}
54
+ </div>
55
+ </div>
56
+ </div>
57
+ );
58
+ };
59
+
60
+ export const SimpleItemLayout = (props) => {
61
+ return <BasicItem {...props} />;
62
+ };
@@ -0,0 +1,28 @@
1
+ import React from 'react';
2
+ import { Card as UiCard } from 'semantic-ui-react';
3
+ import { ConditionalLink } from '@plone/volto/components';
4
+
5
+ const getItemHref = (item) =>
6
+ item?.linkHref || item?.linkWithHash || item?.['@id'];
7
+
8
+ const CardTitle = (props) => {
9
+ const { item, isEditMode, itemModel } = props;
10
+ const { title, Title } = item;
11
+ const t = title || Title;
12
+
13
+ const href = getItemHref(item);
14
+
15
+ return t && !itemModel?.titleOnImage ? (
16
+ <UiCard.Header>
17
+ <ConditionalLink
18
+ className="header-link"
19
+ to={href}
20
+ condition={!isEditMode && itemModel?.hasLink && !!href}
21
+ >
22
+ {t}
23
+ </ConditionalLink>
24
+ </UiCard.Header>
25
+ ) : null;
26
+ };
27
+
28
+ export default CardTitle;
@@ -0,0 +1,453 @@
1
+ /**
2
+ * ObjectBrowserWidget component.
3
+ * @module components/manage/Widgets/ObjectBrowserWidget
4
+ */
5
+
6
+ import React, { Component } from 'react';
7
+ import PropTypes from 'prop-types';
8
+ import { compose } from 'redux';
9
+ import { compact, isArray, isEmpty, remove } from 'lodash';
10
+ import { connect } from 'react-redux';
11
+ import { Label, Popup, Button } from 'semantic-ui-react';
12
+ import {
13
+ flattenToAppURL,
14
+ isInternalURL,
15
+ isUrl,
16
+ normalizeUrl,
17
+ removeProtocol,
18
+ } from '@plone/volto/helpers/Url/Url';
19
+ import { searchContent } from '@plone/volto/actions/search/search';
20
+ import withObjectBrowser from '@plone/volto/components/manage/Sidebar/ObjectBrowser';
21
+ import { defineMessages, injectIntl } from 'react-intl';
22
+ import Icon from '@plone/volto/components/theme/Icon/Icon';
23
+ import FormFieldWrapper from '@plone/volto/components/manage/Widgets/FormFieldWrapper';
24
+
25
+ import navTreeSVG from '@plone/volto/icons/nav.svg';
26
+ import clearSVG from '@plone/volto/icons/clear.svg';
27
+ import homeSVG from '@plone/volto/icons/home.svg';
28
+ import aheadSVG from '@plone/volto/icons/ahead.svg';
29
+ import blankSVG from '@plone/volto/icons/blank.svg';
30
+ import { withRouter } from 'react-router';
31
+
32
+ const messages = defineMessages({
33
+ placeholder: {
34
+ id: 'No items selected',
35
+ defaultMessage: 'No items selected',
36
+ },
37
+ edit: {
38
+ id: 'Edit',
39
+ defaultMessage: 'Edit',
40
+ },
41
+ delete: {
42
+ id: 'Delete',
43
+ defaultMessage: 'Delete',
44
+ },
45
+ openObjectBrowser: {
46
+ id: 'Open object browser',
47
+ defaultMessage: 'Open object browser',
48
+ },
49
+ });
50
+
51
+ /**
52
+ * ObjectBrowserWidget component class.
53
+ * @class ObjectBrowserWidget
54
+ * @extends Component
55
+ */
56
+ export class ObjectBrowserWidgetComponent extends Component {
57
+ /**
58
+ * Property types.
59
+ * @property {Object} propTypes Property types.
60
+ * @static
61
+ */
62
+ static propTypes = {
63
+ id: PropTypes.string.isRequired,
64
+ title: PropTypes.string.isRequired,
65
+ description: PropTypes.string,
66
+ mode: PropTypes.string, // link, image, multiple
67
+ return: PropTypes.string, // single, multiple
68
+ initialPath: PropTypes.string,
69
+ required: PropTypes.bool,
70
+ error: PropTypes.arrayOf(PropTypes.string),
71
+ value: PropTypes.oneOfType([
72
+ PropTypes.arrayOf(PropTypes.object),
73
+ PropTypes.object,
74
+ ]),
75
+ onChange: PropTypes.func.isRequired,
76
+ openObjectBrowser: PropTypes.func.isRequired,
77
+ allowExternals: PropTypes.bool,
78
+ placeholder: PropTypes.string,
79
+ };
80
+
81
+ /**
82
+ * Default properties
83
+ * @property {Object} defaultProps Default properties.
84
+ * @static
85
+ */
86
+ static defaultProps = {
87
+ description: null,
88
+ required: false,
89
+ error: [],
90
+ value: [],
91
+ mode: 'multiple',
92
+ return: 'multiple',
93
+ initialPath: '',
94
+ allowExternals: false,
95
+ };
96
+
97
+ state = {
98
+ manualLinkInput: '',
99
+ validURL: false,
100
+ };
101
+
102
+ constructor(props) {
103
+ super(props);
104
+ this.selectedItemsRef = React.createRef();
105
+ this.placeholderRef = React.createRef();
106
+ }
107
+ renderLabel(item) {
108
+ // Prefer fully preserved href if present
109
+ const href = item.linkHref || item.linkWithHash || item['@id'];
110
+
111
+ return (
112
+ <Popup
113
+ key={href}
114
+ content={
115
+ <div style={{ display: 'flex' }}>
116
+ {isInternalURL(href) ? (
117
+ <Icon name={homeSVG} size="18px" />
118
+ ) : (
119
+ <Icon name={blankSVG} size="18px" />
120
+ )}
121
+ &nbsp;
122
+ {href}
123
+ </div>
124
+ }
125
+ trigger={
126
+ <Label>
127
+ <div className="item-title">{item.title}</div>
128
+ <div>
129
+ {this.props.mode === 'multiple' && (
130
+ <Icon
131
+ name={clearSVG}
132
+ size="12px"
133
+ className="right"
134
+ onClick={(event) => {
135
+ event.preventDefault();
136
+ this.removeItem(item);
137
+ }}
138
+ />
139
+ )}
140
+ </div>
141
+ </Label>
142
+ }
143
+ />
144
+ );
145
+ }
146
+
147
+ removeItem = (item) => {
148
+ let value = [...this.props.value];
149
+ remove(value, function (_item) {
150
+ return _item['@id'] === item['@id'];
151
+ });
152
+ this.props.onChange(this.props.id, value);
153
+ };
154
+
155
+ onChange = (item) => {
156
+ let value =
157
+ this.props.mode === 'multiple' && this.props.value
158
+ ? [...this.props.value]
159
+ : [];
160
+ value = value.filter((item) => item != null);
161
+ const maxSize =
162
+ this.props.widgetOptions?.pattern_options?.maximumSelectionSize || -1;
163
+ if (maxSize === 1 && value.length === 1) {
164
+ value = []; //enable replace of selected item with another value, if maxsize is 1
165
+ }
166
+ let exists = false;
167
+ let index = -1;
168
+
169
+ value.forEach((_item, _index) => {
170
+ if (flattenToAppURL(_item['@id']) === flattenToAppURL(item['@id'])) {
171
+ exists = true;
172
+ index = _index;
173
+ }
174
+ });
175
+ //find(value, {
176
+ // '@id': flattenToAppURL(item['@id']),
177
+ // });
178
+ if (!exists) {
179
+ // add item
180
+ // Check if we want to filter the attributes of the selected item
181
+ let resultantItem = item;
182
+ if (this.props.selectedItemAttrs) {
183
+ const allowedItemKeys = [
184
+ ...this.props.selectedItemAttrs,
185
+ // Add the required attributes for the widget to work
186
+ '@id',
187
+ 'linkHref', // add linkHref to the allowed attributes
188
+ 'linkWithHash', // add linkWithHash to the allowed attributes
189
+ 'title',
190
+ ];
191
+ resultantItem = Object.keys(item)
192
+ .filter((key) => allowedItemKeys.includes(key))
193
+ .reduce((obj, key) => {
194
+ obj[key] = item[key];
195
+ return obj;
196
+ }, {});
197
+ }
198
+ // Add required @id field, just in case
199
+ resultantItem = { ...resultantItem, '@id': item['@id'] };
200
+ value.push(resultantItem);
201
+ if (this.props.return === 'single') {
202
+ this.props.onChange(this.props.id, value[0]);
203
+ } else {
204
+ this.props.onChange(this.props.id, value);
205
+ }
206
+ } else {
207
+ //remove item
208
+ value.splice(index, 1);
209
+ this.props.onChange(this.props.id, value);
210
+ }
211
+ };
212
+
213
+ onManualLinkInput = (e) => {
214
+ this.setState({ manualLinkInput: e.target.value });
215
+ this.setState({
216
+ validURL: this.validateManualLink(e.target.value),
217
+ });
218
+ };
219
+
220
+ validateManualLink = (url) => {
221
+ if (this.props.allowExternals) {
222
+ return isUrl(url);
223
+ } else {
224
+ return isInternalURL(url);
225
+ }
226
+ };
227
+
228
+ hasQueryOrHash = (url) => {
229
+ return url.includes('?') || url.includes('#');
230
+ };
231
+
232
+ /**
233
+ * Returns the clean path used to resolve a Plone object,
234
+ * stripping query string and hash fragment.
235
+ */
236
+ getLookupPathFromUrl = (url) => {
237
+ return url.split('?')[0].split('#')[0];
238
+ };
239
+
240
+ onSubmitManualLink = () => {
241
+ if (!this.validateManualLink(this.state.manualLinkInput)) {
242
+ return;
243
+ }
244
+
245
+ if (isInternalURL(this.state.manualLinkInput)) {
246
+ const originalUrl = this.state.manualLinkInput;
247
+ const lookupPath = this.getLookupPathFromUrl(originalUrl);
248
+ const relativeLink = flattenToAppURL(lookupPath);
249
+ const shouldPreserveOriginal = this.hasQueryOrHash(originalUrl);
250
+
251
+ this.props
252
+ .searchContent(
253
+ '/',
254
+ {
255
+ 'path.query': relativeLink,
256
+ 'path.depth': '0',
257
+ sort_on: 'getObjPositionInParent',
258
+ metadata_fields: '_all',
259
+ b_size: 1000,
260
+ },
261
+ `${this.props.block}-${this.props.mode}`,
262
+ )
263
+ .then((resp) => {
264
+ if (resp.items?.length > 0) {
265
+ const resolvedItem = {
266
+ ...resp.items[0],
267
+ ...(shouldPreserveOriginal ? { linkHref: originalUrl } : {}),
268
+ };
269
+ this.onChange(resolvedItem);
270
+ } else {
271
+ this.props.onChange(this.props.id, [
272
+ {
273
+ '@id': relativeLink,
274
+ ...(shouldPreserveOriginal ? { linkHref: originalUrl } : {}),
275
+ title: removeProtocol(originalUrl),
276
+ },
277
+ ]);
278
+ }
279
+ });
280
+ } else {
281
+ this.props.onChange(this.props.id, [
282
+ {
283
+ '@id': normalizeUrl(this.state.manualLinkInput),
284
+ title: removeProtocol(this.state.manualLinkInput),
285
+ },
286
+ ]);
287
+ }
288
+
289
+ this.setState({ validURL: true, manualLinkInput: '' });
290
+ };
291
+
292
+ onKeyDownManualLink = (e) => {
293
+ if (e.key === 'Enter') {
294
+ e.preventDefault();
295
+ e.stopPropagation();
296
+ this.onSubmitManualLink();
297
+ } else if (e.key === 'Escape') {
298
+ e.preventDefault();
299
+ e.stopPropagation();
300
+ // TODO: Do something on ESC key
301
+ }
302
+ };
303
+
304
+ showObjectBrowser = (ev) => {
305
+ ev.preventDefault();
306
+ this.props.openObjectBrowser({
307
+ mode: this.props.mode,
308
+ currentPath: this.props.initialPath || this.props.location.pathname,
309
+ propDataName: 'value',
310
+ onSelectItem: (url, item) => {
311
+ this.onChange(item);
312
+ },
313
+ selectableTypes:
314
+ this.props.widgetOptions?.pattern_options?.selectableTypes ||
315
+ this.props.selectableTypes,
316
+ maximumSelectionSize:
317
+ this.props.widgetOptions?.pattern_options?.maximumSelectionSize ||
318
+ this.props.maximumSelectionSize,
319
+ });
320
+ };
321
+
322
+ handleSelectedItemsRefClick = (e) => {
323
+ if (this.props.isDisabled) {
324
+ return;
325
+ }
326
+
327
+ if (
328
+ e.target.contains(this.selectedItemsRef.current) ||
329
+ e.target.contains(this.placeholderRef.current)
330
+ ) {
331
+ this.showObjectBrowser(e);
332
+ }
333
+ };
334
+
335
+ /**
336
+ * Render method.
337
+ * @method render
338
+ * @returns {string} Markup for the component.
339
+ */
340
+ render() {
341
+ const { id, description, fieldSet, value, mode, onChange, isDisabled } =
342
+ this.props;
343
+
344
+ let items = compact(!isArray(value) && value ? [value] : value || []);
345
+
346
+ let icon =
347
+ mode === 'multiple' || items.length === 0 ? navTreeSVG : clearSVG;
348
+ let iconAction =
349
+ mode === 'multiple' || items.length === 0
350
+ ? this.showObjectBrowser
351
+ : (e) => {
352
+ e.preventDefault();
353
+ onChange(id, this.props.return === 'single' ? null : []);
354
+ };
355
+
356
+ return (
357
+ <FormFieldWrapper
358
+ {...this.props}
359
+ className={description ? 'help text' : 'text'}
360
+ >
361
+ <div
362
+ className="objectbrowser-field"
363
+ aria-labelledby={`fieldset-${
364
+ fieldSet || 'default'
365
+ }-field-label-${id}`}
366
+ >
367
+ <div
368
+ className="selected-values"
369
+ onClick={this.handleSelectedItemsRefClick}
370
+ onKeyDown={this.handleSelectedItemsRefClick}
371
+ role="searchbox"
372
+ tabIndex={0}
373
+ ref={this.selectedItemsRef}
374
+ >
375
+ {items.map((item) => this.renderLabel(item))}
376
+
377
+ {items.length === 0 && this.props.mode === 'multiple' && (
378
+ <div className="placeholder" ref={this.placeholderRef}>
379
+ {this.props.placeholder ??
380
+ this.props.intl.formatMessage(messages.placeholder)}
381
+ </div>
382
+ )}
383
+ {this.props.allowExternals &&
384
+ items.length === 0 &&
385
+ this.props.mode !== 'multiple' && (
386
+ <input
387
+ onKeyDown={this.onKeyDownManualLink}
388
+ onChange={this.onManualLinkInput}
389
+ value={this.state.manualLinkInput}
390
+ placeholder={
391
+ this.props.placeholder ??
392
+ this.props.intl.formatMessage(messages.placeholder)
393
+ }
394
+ />
395
+ )}
396
+ </div>
397
+ {this.state.manualLinkInput && isEmpty(items) && (
398
+ <Button.Group>
399
+ <Button
400
+ basic
401
+ className="cancel"
402
+ onClick={(e) => {
403
+ e.stopPropagation();
404
+ this.setState({ manualLinkInput: '' });
405
+ }}
406
+ >
407
+ <Icon name={clearSVG} size="18px" color="#e40166" />
408
+ </Button>
409
+ <Button
410
+ basic
411
+ primary
412
+ disabled={!this.state.validURL}
413
+ onClick={(e) => {
414
+ e.stopPropagation();
415
+ this.onSubmitManualLink();
416
+ }}
417
+ >
418
+ <Icon name={aheadSVG} size="18px" />
419
+ </Button>
420
+ </Button.Group>
421
+ )}
422
+ {!this.state.manualLinkInput && (
423
+ <Button
424
+ aria-label={this.props.intl.formatMessage(
425
+ messages.openObjectBrowser,
426
+ )}
427
+ onClick={iconAction}
428
+ className="action"
429
+ disabled={isDisabled}
430
+ >
431
+ <Icon name={icon} size="18px" />
432
+ </Button>
433
+ )}
434
+ </div>
435
+ </FormFieldWrapper>
436
+ );
437
+ }
438
+ }
439
+
440
+ const ObjectBrowserWidgetMode = (mode) =>
441
+ compose(
442
+ injectIntl,
443
+ withObjectBrowser,
444
+ withRouter,
445
+ connect(null, { searchContent }),
446
+ )((props) => <ObjectBrowserWidgetComponent {...props} mode={mode} />);
447
+ export { ObjectBrowserWidgetMode };
448
+ export default compose(
449
+ injectIntl,
450
+ withObjectBrowser,
451
+ withRouter,
452
+ connect(null, { searchContent }),
453
+ )(ObjectBrowserWidgetComponent);
@@ -0,0 +1 @@
1
+ Customized ObjectBrowserWidget to preserve query strings in the manually pasted internal URL. (refs #299560)