@plone/volto 18.2.3 → 18.3.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.
Files changed (42) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/locales/ca/LC_MESSAGES/volto.po +47 -11
  3. package/locales/ca.json +1 -1
  4. package/locales/de/LC_MESSAGES/volto.po +47 -11
  5. package/locales/de.json +1 -1
  6. package/locales/en/LC_MESSAGES/volto.po +47 -11
  7. package/locales/en.json +1 -1
  8. package/locales/es/LC_MESSAGES/volto.po +47 -11
  9. package/locales/es.json +1 -1
  10. package/locales/eu/LC_MESSAGES/volto.po +47 -11
  11. package/locales/eu.json +1 -1
  12. package/locales/fi/LC_MESSAGES/volto.po +47 -11
  13. package/locales/fi.json +1 -1
  14. package/locales/fr/LC_MESSAGES/volto.po +47 -11
  15. package/locales/fr.json +1 -1
  16. package/locales/hi/LC_MESSAGES/volto.po +47 -11
  17. package/locales/hi.json +1 -1
  18. package/locales/it/LC_MESSAGES/volto.po +71 -35
  19. package/locales/it.json +1 -1
  20. package/locales/ja/LC_MESSAGES/volto.po +47 -11
  21. package/locales/ja.json +1 -1
  22. package/locales/nl/LC_MESSAGES/volto.po +47 -11
  23. package/locales/nl.json +1 -1
  24. package/locales/pt/LC_MESSAGES/volto.po +47 -11
  25. package/locales/pt.json +1 -1
  26. package/locales/pt_BR/LC_MESSAGES/volto.po +47 -11
  27. package/locales/pt_BR.json +1 -1
  28. package/locales/ro/LC_MESSAGES/volto.po +47 -11
  29. package/locales/ro.json +1 -1
  30. package/locales/volto.pot +48 -12
  31. package/locales/zh_CN/LC_MESSAGES/volto.po +47 -11
  32. package/locales/zh_CN.json +1 -1
  33. package/package.json +4 -4
  34. package/src/components/manage/Blocks/Container/EditBlockWrapper.jsx +4 -1
  35. package/src/components/manage/Contents/Contents.jsx +6 -354
  36. package/src/components/manage/Contents/ContentsDeleteModal.jsx +380 -0
  37. package/src/components/manage/Rules/Rules.jsx +7 -1
  38. package/src/components/theme/SlotRenderer/SlotRenderer.tsx +1 -1
  39. package/src/helpers/ScrollToTop/ScrollToTop.jsx +1 -0
  40. package/src/reducers/index.js +2 -0
  41. package/src/reducers/linkIntegrity/linkIntegrity.js +51 -0
  42. package/src/reducers/linkIntegrity/linkIntegrity.test.js +54 -0
@@ -0,0 +1,380 @@
1
+ import React, { useState, useEffect } from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import { useDispatch, useSelector } from 'react-redux';
4
+ import { Link } from 'react-router-dom';
5
+ import map from 'lodash/map';
6
+ import find from 'lodash/find';
7
+ import { defineMessages, useIntl, FormattedMessage } from 'react-intl';
8
+ import { linkIntegrityCheck } from '@plone/volto/actions/content/content';
9
+ import { flattenToAppURL } from '@plone/volto/helpers/Url/Url';
10
+
11
+ import { Confirm, Dimmer, Loader, Table } from 'semantic-ui-react';
12
+
13
+ const messages = defineMessages({
14
+ deleteConfirmSingleItem: {
15
+ id: 'Delete this item?',
16
+ defaultMessage: 'Delete this item?',
17
+ },
18
+ deleteConfirmMultipleItems: {
19
+ id: 'Delete selected items?',
20
+ defaultMessage: 'Delete selected items?',
21
+ },
22
+ navigate_to_this_item: {
23
+ id: 'Navigate to this item',
24
+ defaultMessage: 'Navigate to this item',
25
+ },
26
+ loading: {
27
+ id: 'link-integrity: loading references',
28
+ defaultMessage: 'Checking references...',
29
+ },
30
+ delete: {
31
+ id: 'link-integrity: Delete',
32
+ defaultMessage: 'Delete',
33
+ },
34
+ delete_and_broken_links: {
35
+ id: 'link-integrity: Delete item and break links',
36
+ defaultMessage: 'Delete item and break links',
37
+ },
38
+ cancel: {
39
+ id: 'Cancel',
40
+ defaultMessage: 'Cancel',
41
+ },
42
+ });
43
+
44
+ const ContentsDeleteModal = (props) => {
45
+ const { itemsToDelete = [], open, onCancel, onOk, items } = props;
46
+ const intl = useIntl();
47
+ const dispatch = useDispatch();
48
+ const linkintegrityInfo = useSelector((state) => state.linkIntegrity?.result);
49
+ const loading = useSelector((state) => state.linkIntegrity?.loading);
50
+
51
+ const [brokenReferences, setBrokenReferences] = useState(0);
52
+ const [containedItemsToDelete, setContainedItemsToDelete] = useState([]);
53
+ const [breaches, setBreaches] = useState([]);
54
+
55
+ const [linksAndReferencesViewLink, setLinkAndReferencesViewLink] =
56
+ useState(null);
57
+
58
+ useEffect(() => {
59
+ const getFieldById = (id, field) => {
60
+ const item = find(items, { '@id': id });
61
+ return item ? item[field] : '';
62
+ };
63
+
64
+ if (itemsToDelete.length > 0 && open) {
65
+ dispatch(
66
+ linkIntegrityCheck(
67
+ map(itemsToDelete, (item) => getFieldById(item, 'UID')),
68
+ ),
69
+ );
70
+ }
71
+ }, [itemsToDelete, items, open, dispatch]);
72
+
73
+ useEffect(() => {
74
+ if (linkintegrityInfo) {
75
+ const containedItems = linkintegrityInfo
76
+ .map((result) => result.items_total ?? 0)
77
+ .reduce((acc, value) => acc + value, 0);
78
+ const breaches = linkintegrityInfo.flatMap((result) =>
79
+ result.breaches.map((source) => ({
80
+ source: source,
81
+ target: result,
82
+ })),
83
+ );
84
+ const source_by_uid = breaches.reduce(
85
+ (acc, value) => acc.set(value.source.uid, value.source),
86
+ new Map(),
87
+ );
88
+ const by_source = breaches.reduce((acc, value) => {
89
+ if (acc.get(value.source.uid) === undefined) {
90
+ acc.set(value.source.uid, new Set());
91
+ }
92
+ acc.get(value.source.uid).add(value.target);
93
+ return acc;
94
+ }, new Map());
95
+
96
+ setContainedItemsToDelete(containedItems);
97
+ setBrokenReferences(by_source.size);
98
+ setLinkAndReferencesViewLink(
99
+ linkintegrityInfo.length
100
+ ? linkintegrityInfo[0]['@id'] + '/links-to-item'
101
+ : null,
102
+ );
103
+ setBreaches(
104
+ Array.from(by_source, (entry) => ({
105
+ source: source_by_uid.get(entry[0]),
106
+ targets: Array.from(entry[1]),
107
+ })),
108
+ );
109
+ } else {
110
+ setContainedItemsToDelete([]);
111
+ setBrokenReferences(0);
112
+ setLinkAndReferencesViewLink(null);
113
+ setBreaches([]);
114
+ }
115
+ }, [linkintegrityInfo]);
116
+
117
+ return (
118
+ open && (
119
+ <Confirm
120
+ open={open}
121
+ confirmButton={
122
+ brokenReferences === 0
123
+ ? intl.formatMessage(messages.delete)
124
+ : intl.formatMessage(messages.delete_and_broken_links)
125
+ }
126
+ cancelButton={intl.formatMessage(messages.cancel)}
127
+ header={
128
+ itemsToDelete.length === 1
129
+ ? intl.formatMessage(messages.deleteConfirmSingleItem)
130
+ : intl.formatMessage(messages.deleteConfirmMultipleItems)
131
+ }
132
+ content={
133
+ <div className="content">
134
+ <Dimmer active={loading} inverted>
135
+ <Loader indeterminate size="massive">
136
+ {intl.formatMessage(messages.loading)}
137
+ </Loader>
138
+ </Dimmer>
139
+
140
+ {itemsToDelete.length > 1 ? (
141
+ containedItemsToDelete > 0 ? (
142
+ <>
143
+ <FormattedMessage
144
+ id="Some items are also a folder. By deleting them you will delete {containedItemsToDelete} {variation} inside the folders."
145
+ defaultMessage="Some items are also a folder. By deleting them you will delete {containedItemsToDelete} {variation} inside the folders."
146
+ values={{
147
+ containedItemsToDelete: (
148
+ <span>{containedItemsToDelete}</span>
149
+ ),
150
+ variation: (
151
+ <span>
152
+ {containedItemsToDelete === 1 ? (
153
+ <FormattedMessage id="item" defaultMessage="item" />
154
+ ) : (
155
+ <FormattedMessage
156
+ id="items"
157
+ defaultMessage="items"
158
+ />
159
+ )}
160
+ </span>
161
+ ),
162
+ }}
163
+ />
164
+ {brokenReferences > 0 && (
165
+ <>
166
+ <br />
167
+ <FormattedMessage
168
+ id="Some items are referenced by other contents. By deleting them {brokenReferences} {variation} will be broken."
169
+ defaultMessage="Some items are referenced by other contents. By deleting them {brokenReferences} {variation} will be broken."
170
+ values={{
171
+ brokenReferences: <span>{brokenReferences}</span>,
172
+ variation: (
173
+ <span>
174
+ {brokenReferences === 1 ? (
175
+ <FormattedMessage
176
+ id="reference"
177
+ defaultMessage="reference"
178
+ />
179
+ ) : (
180
+ <FormattedMessage
181
+ id="references"
182
+ defaultMessage="references"
183
+ />
184
+ )}
185
+ </span>
186
+ ),
187
+ }}
188
+ />
189
+ </>
190
+ )}
191
+ </>
192
+ ) : (
193
+ <>
194
+ {brokenReferences > 0 && (
195
+ <>
196
+ <FormattedMessage
197
+ id="Some items are referenced by other contents. By deleting them {brokenReferences} {variation} will be broken."
198
+ defaultMessage="Some items are referenced by other contents. By deleting them {brokenReferences} {variation} will be broken."
199
+ values={{
200
+ brokenReferences: <span>{brokenReferences}</span>,
201
+ variation: (
202
+ <span>
203
+ {brokenReferences === 1 ? (
204
+ <FormattedMessage
205
+ id="reference"
206
+ defaultMessage="reference"
207
+ />
208
+ ) : (
209
+ <FormattedMessage
210
+ id="references"
211
+ defaultMessage="references"
212
+ />
213
+ )}
214
+ </span>
215
+ ),
216
+ }}
217
+ />
218
+ </>
219
+ )}
220
+ </>
221
+ )
222
+ ) : containedItemsToDelete > 0 ? (
223
+ <>
224
+ <FormattedMessage
225
+ id="This item is also a folder. By deleting it you will delete {containedItemsToDelete} {variation} inside the folder."
226
+ defaultMessage="This item is also a folder. By deleting it you will delete {containedItemsToDelete} {variation} inside the folder."
227
+ values={{
228
+ containedItemsToDelete: (
229
+ <span>{containedItemsToDelete}</span>
230
+ ),
231
+ variation: (
232
+ <span>
233
+ {containedItemsToDelete === 1 ? (
234
+ <FormattedMessage id="item" defaultMessage="item" />
235
+ ) : (
236
+ <FormattedMessage id="items" defaultMessage="items" />
237
+ )}
238
+ </span>
239
+ ),
240
+ }}
241
+ />
242
+ {brokenReferences > 0 && (
243
+ <>
244
+ <br />
245
+ <FormattedMessage
246
+ id="Deleting this item breaks {brokenReferences} {variation}."
247
+ defaultMessage="Deleting this item breaks {brokenReferences} {variation}."
248
+ values={{
249
+ brokenReferences: <span>{brokenReferences}</span>,
250
+ variation: (
251
+ <span>
252
+ {brokenReferences === 1 ? (
253
+ <FormattedMessage
254
+ id="reference"
255
+ defaultMessage="reference"
256
+ />
257
+ ) : (
258
+ <FormattedMessage
259
+ id="references"
260
+ defaultMessage="references"
261
+ />
262
+ )}
263
+ </span>
264
+ ),
265
+ }}
266
+ />
267
+ <BrokenLinksList
268
+ intl={intl}
269
+ breaches={breaches}
270
+ linksAndReferencesViewLink={linksAndReferencesViewLink}
271
+ />
272
+ </>
273
+ )}
274
+ </>
275
+ ) : brokenReferences > 0 ? (
276
+ <>
277
+ <FormattedMessage
278
+ id="Deleting this item breaks {brokenReferences} {variation}."
279
+ defaultMessage="Deleting this item breaks {brokenReferences} {variation}."
280
+ values={{
281
+ brokenReferences: <span>{brokenReferences}</span>,
282
+ variation: (
283
+ <span>
284
+ {brokenReferences === 1 ? (
285
+ <FormattedMessage
286
+ id="reference"
287
+ defaultMessage="reference"
288
+ />
289
+ ) : (
290
+ <FormattedMessage
291
+ id="references"
292
+ defaultMessage="references"
293
+ />
294
+ )}
295
+ </span>
296
+ ),
297
+ }}
298
+ />
299
+ <BrokenLinksList
300
+ intl={intl}
301
+ breaches={breaches}
302
+ linksAndReferencesViewLink={linksAndReferencesViewLink}
303
+ />
304
+ </>
305
+ ) : null}
306
+ </div>
307
+ }
308
+ onCancel={onCancel}
309
+ onConfirm={onOk}
310
+ size="medium"
311
+ />
312
+ )
313
+ );
314
+ };
315
+
316
+ const BrokenLinksList = ({ intl, breaches, linksAndReferencesViewLink }) => {
317
+ return (
318
+ <div className="broken-links-list">
319
+ <FormattedMessage
320
+ id="These items will have broken links"
321
+ defaultMessage="These items will have broken links"
322
+ />
323
+ :
324
+ <Table compact>
325
+ <Table.Body>
326
+ {breaches.map((breach) => (
327
+ <Table.Row key={breach.source['@id']} verticalAlign="top">
328
+ <Table.Cell>
329
+ <Link
330
+ to={flattenToAppURL(breach.source['@id'])}
331
+ title={intl.formatMessage(messages.navigate_to_this_item)}
332
+ >
333
+ {breach.source.title}
334
+ </Link>
335
+ </Table.Cell>
336
+ <Table.Cell style={{ minWidth: '140px' }}>
337
+ <FormattedMessage id="refers to" defaultMessage="refers to" />:
338
+ </Table.Cell>
339
+ <Table.Cell>
340
+ <ul style={{ margin: 0 }}>
341
+ {breach.targets.map((target) => (
342
+ <li key={target['@id']}>
343
+ <Link
344
+ to={flattenToAppURL(target['@id'])}
345
+ title={intl.formatMessage(
346
+ messages.navigate_to_this_item,
347
+ )}
348
+ >
349
+ {target.title}
350
+ </Link>
351
+ </li>
352
+ ))}
353
+ </ul>
354
+ </Table.Cell>
355
+ </Table.Row>
356
+ ))}
357
+ </Table.Body>
358
+ </Table>
359
+ {linksAndReferencesViewLink && (
360
+ <Link to={flattenToAppURL(linksAndReferencesViewLink)}>
361
+ <FormattedMessage
362
+ id="View links and references to this item"
363
+ defaultMessage="View links and references to this item"
364
+ />
365
+ </Link>
366
+ )}
367
+ </div>
368
+ );
369
+ };
370
+ ContentsDeleteModal.propTypes = {
371
+ itemsToDelete: PropTypes.arrayOf(
372
+ PropTypes.shape({
373
+ UID: PropTypes.string,
374
+ }),
375
+ ).isRequired,
376
+ open: PropTypes.bool.isRequired,
377
+ onOk: PropTypes.func.isRequired,
378
+ onCancel: PropTypes.func.isRequired,
379
+ };
380
+ export default ContentsDeleteModal;
@@ -76,6 +76,10 @@ const messages = defineMessages({
76
76
  id: 'Unassigned',
77
77
  defaultMessage: 'Unassigned',
78
78
  },
79
+ select_rule: {
80
+ id: 'Select rule',
81
+ defaultMessage: 'Select rule',
82
+ },
79
83
  });
80
84
 
81
85
  /**
@@ -366,7 +370,9 @@ class Rules extends Component {
366
370
  />
367
371
  <div style={{ display: 'flex' }}>
368
372
  <Select
369
- placeholder="Select rule"
373
+ placeholder={this.props.intl.formatMessage(
374
+ messages.select_rule,
375
+ )}
370
376
  value={this.state.newRule}
371
377
  onChange={(e, { value }) => this.setState({ newRule: value })}
372
378
  options={assignable_rules.map((rule, i) => {
@@ -4,7 +4,7 @@ import type { Content } from '@plone/types';
4
4
 
5
5
  /*
6
6
  Usage:
7
- <SlotRenderer name="aboveContent" content={content} route={} />
7
+ <SlotRenderer name="aboveContent" content={content} />
8
8
  */
9
9
 
10
10
  const SlotRenderer = ({
@@ -37,6 +37,7 @@ class ScrollToTop extends React.Component {
37
37
  const isHash = location?.hash || location?.pathname.hash;
38
38
  if (
39
39
  !isHash &&
40
+ prevProps.location !== undefined &&
40
41
  noInitialBlocksFocus &&
41
42
  location?.pathname !== prevProps.location?.pathname
42
43
  ) {
@@ -20,6 +20,7 @@ import emailNotification from '@plone/volto/reducers/emailNotification/emailNoti
20
20
  import emailSend from '@plone/volto/reducers/emailSend/emailSend';
21
21
  import form from '@plone/volto/reducers/form/form';
22
22
  import history from '@plone/volto/reducers/history/history';
23
+ import linkIntegrity from '@plone/volto/reducers/linkIntegrity/linkIntegrity';
23
24
  import groups from '@plone/volto/reducers/groups/groups';
24
25
  import messages from '@plone/volto/reducers/messages/messages';
25
26
  import navigation from '@plone/volto/reducers/navigation/navigation';
@@ -79,6 +80,7 @@ const reducers = {
79
80
  form,
80
81
  groups,
81
82
  history,
83
+ linkIntegrity,
82
84
  messages,
83
85
  navigation,
84
86
  querystring,
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Linkintegrity reducer.
3
+ * @module reducers/linkIntegrity/linkIntegrity
4
+ */
5
+
6
+ import { LINK_INTEGRITY_CHECK } from '@plone/volto/constants/ActionTypes';
7
+
8
+ const initialState = {
9
+ error: null,
10
+ loaded: false,
11
+ loading: false,
12
+ result: null,
13
+ };
14
+
15
+ /**
16
+ * History reducer.
17
+ * @function linkIntegrity
18
+ * @param {Object} state Current state.
19
+ * @param {Object} action Action to be handled.
20
+ * @returns {Object} New state.
21
+ */
22
+ export default function linkIntegrity(state = initialState, action = {}) {
23
+ switch (action.type) {
24
+ case `${LINK_INTEGRITY_CHECK}_PENDING`:
25
+ return {
26
+ ...state,
27
+ error: null,
28
+ loaded: false,
29
+ loading: true,
30
+ result: null,
31
+ };
32
+ case `${LINK_INTEGRITY_CHECK}_SUCCESS`:
33
+ return {
34
+ ...state,
35
+ error: null,
36
+ loaded: true,
37
+ loading: false,
38
+ result: action.result,
39
+ };
40
+ case `${LINK_INTEGRITY_CHECK}_FAIL`:
41
+ return {
42
+ ...state,
43
+ error: action.error,
44
+ loaded: true,
45
+ loading: false,
46
+ result: null,
47
+ };
48
+ default:
49
+ return state;
50
+ }
51
+ }
@@ -0,0 +1,54 @@
1
+ import linkIntegrity from './linkIntegrity';
2
+ import { LINK_INTEGRITY_CHECK } from '@plone/volto/constants/ActionTypes';
3
+
4
+ describe('Link integrity reducer', () => {
5
+ it('should return the initial state', () => {
6
+ expect(linkIntegrity()).toEqual({
7
+ error: null,
8
+ loaded: false,
9
+ loading: false,
10
+ result: null,
11
+ });
12
+ });
13
+
14
+ it('should handle LINK_INTEGRITY_CHECK_PENDING', () => {
15
+ expect(
16
+ linkIntegrity(undefined, {
17
+ type: `${LINK_INTEGRITY_CHECK}_PENDING`,
18
+ }),
19
+ ).toEqual({
20
+ error: null,
21
+ loaded: false,
22
+ loading: true,
23
+ result: null,
24
+ });
25
+ });
26
+
27
+ it('should handle LINK_INTEGRITY_CHECK_SUCCESS', () => {
28
+ expect(
29
+ linkIntegrity(undefined, {
30
+ type: `${LINK_INTEGRITY_CHECK}_SUCCESS`,
31
+ result: 'result',
32
+ }),
33
+ ).toEqual({
34
+ error: null,
35
+ loaded: true,
36
+ loading: false,
37
+ result: 'result',
38
+ });
39
+ });
40
+
41
+ it('should handle LINK_INTEGRITY_CHECK_FAIL', () => {
42
+ expect(
43
+ linkIntegrity(undefined, {
44
+ type: `${LINK_INTEGRITY_CHECK}_FAIL`,
45
+ error: 'failed',
46
+ }),
47
+ ).toEqual({
48
+ error: 'failed',
49
+ loaded: true,
50
+ loading: false,
51
+ result: null,
52
+ });
53
+ });
54
+ });