@plone/volto 18.31.0 → 18.32.1

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 (154) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/locales/af/LC_MESSAGES/volto.po +33 -0
  3. package/locales/af.json +1 -1
  4. package/locales/ar/LC_MESSAGES/volto.po +33 -0
  5. package/locales/ar.json +1 -1
  6. package/locales/bg/LC_MESSAGES/volto.po +33 -0
  7. package/locales/bg.json +1 -1
  8. package/locales/bn/LC_MESSAGES/volto.po +33 -0
  9. package/locales/bn.json +1 -1
  10. package/locales/ca/LC_MESSAGES/volto.po +33 -0
  11. package/locales/ca.json +1 -1
  12. package/locales/cs/LC_MESSAGES/volto.po +33 -0
  13. package/locales/cs.json +1 -1
  14. package/locales/cy/LC_MESSAGES/volto.po +33 -0
  15. package/locales/cy.json +1 -1
  16. package/locales/da/LC_MESSAGES/volto.po +33 -0
  17. package/locales/da.json +1 -1
  18. package/locales/de/LC_MESSAGES/volto.po +33 -0
  19. package/locales/de.json +1 -1
  20. package/locales/el/LC_MESSAGES/volto.po +33 -0
  21. package/locales/el.json +1 -1
  22. package/locales/en/LC_MESSAGES/volto.po +33 -0
  23. package/locales/en.json +1 -1
  24. package/locales/en_AU/LC_MESSAGES/volto.po +33 -0
  25. package/locales/en_AU.json +1 -1
  26. package/locales/en_GB/LC_MESSAGES/volto.po +33 -0
  27. package/locales/en_GB.json +1 -1
  28. package/locales/eo/LC_MESSAGES/volto.po +33 -0
  29. package/locales/eo.json +1 -1
  30. package/locales/es/LC_MESSAGES/volto.po +33 -0
  31. package/locales/es.json +1 -1
  32. package/locales/et/LC_MESSAGES/volto.po +33 -0
  33. package/locales/et.json +1 -1
  34. package/locales/eu/LC_MESSAGES/volto.po +33 -0
  35. package/locales/eu.json +1 -1
  36. package/locales/fa/LC_MESSAGES/volto.po +33 -0
  37. package/locales/fa.json +1 -1
  38. package/locales/fi/LC_MESSAGES/volto.po +33 -0
  39. package/locales/fi.json +1 -1
  40. package/locales/fr/LC_MESSAGES/volto.po +33 -0
  41. package/locales/fr.json +1 -1
  42. package/locales/fu/LC_MESSAGES/volto.po +33 -0
  43. package/locales/fu.json +1 -1
  44. package/locales/gl/LC_MESSAGES/volto.po +33 -0
  45. package/locales/gl.json +1 -1
  46. package/locales/he/LC_MESSAGES/volto.po +33 -0
  47. package/locales/he.json +1 -1
  48. package/locales/hi/LC_MESSAGES/volto.po +33 -0
  49. package/locales/hi.json +1 -1
  50. package/locales/hr/LC_MESSAGES/volto.po +33 -0
  51. package/locales/hr.json +1 -1
  52. package/locales/hu/LC_MESSAGES/volto.po +33 -0
  53. package/locales/hu.json +1 -1
  54. package/locales/hy/LC_MESSAGES/volto.po +33 -0
  55. package/locales/hy.json +1 -1
  56. package/locales/id/LC_MESSAGES/volto.po +33 -0
  57. package/locales/id.json +1 -1
  58. package/locales/it/LC_MESSAGES/volto.po +33 -0
  59. package/locales/it.json +1 -1
  60. package/locales/ja/LC_MESSAGES/volto.po +33 -0
  61. package/locales/ja.json +1 -1
  62. package/locales/ka/LC_MESSAGES/volto.po +33 -0
  63. package/locales/ka.json +1 -1
  64. package/locales/kn/LC_MESSAGES/volto.po +33 -0
  65. package/locales/kn.json +1 -1
  66. package/locales/ko/LC_MESSAGES/volto.po +33 -0
  67. package/locales/ko.json +1 -1
  68. package/locales/lt/LC_MESSAGES/volto.po +33 -0
  69. package/locales/lt.json +1 -1
  70. package/locales/lv/LC_MESSAGES/volto.po +33 -0
  71. package/locales/lv.json +1 -1
  72. package/locales/mi/LC_MESSAGES/volto.po +33 -0
  73. package/locales/mi.json +1 -1
  74. package/locales/mk/LC_MESSAGES/volto.po +33 -0
  75. package/locales/mk.json +1 -1
  76. package/locales/my/LC_MESSAGES/volto.po +33 -0
  77. package/locales/my.json +1 -1
  78. package/locales/nb_NO/LC_MESSAGES/volto.po +33 -0
  79. package/locales/nb_NO.json +1 -1
  80. package/locales/nl/LC_MESSAGES/volto.po +33 -0
  81. package/locales/nl.json +1 -1
  82. package/locales/nn/LC_MESSAGES/volto.po +33 -0
  83. package/locales/nn.json +1 -1
  84. package/locales/pl/LC_MESSAGES/volto.po +33 -0
  85. package/locales/pl.json +1 -1
  86. package/locales/pt/LC_MESSAGES/volto.po +33 -0
  87. package/locales/pt.json +1 -1
  88. package/locales/pt_BR/LC_MESSAGES/volto.po +33 -0
  89. package/locales/pt_BR.json +1 -1
  90. package/locales/rm/LC_MESSAGES/volto.po +33 -0
  91. package/locales/rm.json +1 -1
  92. package/locales/ro/LC_MESSAGES/volto.po +33 -0
  93. package/locales/ro.json +1 -1
  94. package/locales/ru/LC_MESSAGES/volto.po +33 -0
  95. package/locales/ru.json +1 -1
  96. package/locales/sk/LC_MESSAGES/volto.po +33 -0
  97. package/locales/sk.json +1 -1
  98. package/locales/sl/LC_MESSAGES/volto.po +33 -0
  99. package/locales/sl.json +1 -1
  100. package/locales/sm/LC_MESSAGES/volto.po +33 -0
  101. package/locales/sm.json +1 -1
  102. package/locales/sq/LC_MESSAGES/volto.po +33 -0
  103. package/locales/sq.json +1 -1
  104. package/locales/sr/LC_MESSAGES/volto.po +33 -0
  105. package/locales/sr.json +1 -1
  106. package/locales/sr@cyrl/LC_MESSAGES/volto.po +33 -0
  107. package/locales/sr@cyrl.json +1 -1
  108. package/locales/sr@latn/LC_MESSAGES/volto.po +33 -0
  109. package/locales/sr@latn.json +1 -1
  110. package/locales/sv/LC_MESSAGES/volto.po +33 -0
  111. package/locales/sv.json +1 -1
  112. package/locales/ta/LC_MESSAGES/volto.po +33 -0
  113. package/locales/ta.json +1 -1
  114. package/locales/te/LC_MESSAGES/volto.po +33 -0
  115. package/locales/te.json +1 -1
  116. package/locales/th/LC_MESSAGES/volto.po +33 -0
  117. package/locales/th.json +1 -1
  118. package/locales/to/LC_MESSAGES/volto.po +33 -0
  119. package/locales/to.json +1 -1
  120. package/locales/tr/LC_MESSAGES/volto.po +33 -0
  121. package/locales/tr.json +1 -1
  122. package/locales/uk/LC_MESSAGES/volto.po +33 -0
  123. package/locales/uk.json +1 -1
  124. package/locales/vi/LC_MESSAGES/volto.po +33 -0
  125. package/locales/vi.json +1 -1
  126. package/locales/volto.pot +34 -1
  127. package/locales/zh_CN/LC_MESSAGES/volto.po +33 -0
  128. package/locales/zh_CN.json +1 -1
  129. package/locales/zh_Hant/LC_MESSAGES/volto.po +33 -0
  130. package/locales/zh_Hant.json +1 -1
  131. package/locales/zh_Hant_HK/LC_MESSAGES/volto.po +33 -0
  132. package/locales/zh_Hant_HK.json +1 -1
  133. package/package.json +3 -3
  134. package/src/components/manage/Contents/Contents.jsx +685 -665
  135. package/src/components/manage/Contents/DropZoneContent.jsx +338 -0
  136. package/src/components/manage/Sidebar/ObjectBrowser.jsx +3 -0
  137. package/src/components/manage/Sidebar/ObjectBrowserBody.jsx +13 -1
  138. package/src/components/manage/Widgets/ObjectBrowserWidget.jsx +5 -0
  139. package/src/components/manage/Widgets/QueryWidget.jsx +137 -9
  140. package/src/components/theme/MultilingualRedirector/MultilingualRedirector.jsx +2 -2
  141. package/src/helpers/AuthToken/AuthToken.js +1 -6
  142. package/src/reducers/querystring/querystring.js +8 -1
  143. package/theme/themes/pastanaga/extras/contents.less +63 -0
  144. package/theme/themes/pastanaga/extras/widgets.less +34 -0
  145. package/types/components/manage/Contents/DropZoneContent.d.ts +2 -0
  146. package/types/components/manage/Controlpanels/Relations/RelationsMatrix.d.ts +1 -1
  147. package/types/components/manage/Multilingual/ManageTranslations.d.ts +1 -1
  148. package/types/components/manage/Sidebar/ObjectBrowser.d.ts +1 -1
  149. package/types/components/manage/Widgets/ImageWidget.d.ts +1 -1
  150. package/types/components/manage/Widgets/InternalUrlWidget.d.ts +1 -1
  151. package/types/components/manage/Widgets/ObjectBrowserWidget.d.ts +2 -0
  152. package/types/components/manage/Widgets/QueryWidget.d.ts +5 -2
  153. package/types/components/manage/Widgets/UrlWidget.d.ts +1 -1
  154. package/types/components/manage/Widgets/index.d.ts +3 -3
@@ -0,0 +1,338 @@
1
+ import { useState, useEffect } from 'react';
2
+ import { useDispatch, useSelector, shallowEqual } from 'react-redux';
3
+ import { defineMessages, useIntl, FormattedMessage } from 'react-intl';
4
+ import {
5
+ Button,
6
+ Modal,
7
+ Table,
8
+ Input,
9
+ Dimmer,
10
+ Progress,
11
+ } from 'semantic-ui-react';
12
+ import cx from 'classnames';
13
+ import filesize from 'filesize';
14
+ import { readAsDataURL } from 'promise-file-reader';
15
+
16
+ import { createContent } from '@plone/volto/actions/content/content';
17
+ import { usePrevious } from '@plone/volto/helpers/Utils/usePrevious';
18
+ import { validateFileUploadSize } from '@plone/volto/helpers/FormValidation/FormValidation';
19
+ import Icon from '@plone/volto/components/theme/Icon/Icon';
20
+ import uploadSVG from '@plone/volto/icons/upload.svg';
21
+ import clearSVG from '@plone/volto/icons/clear.svg';
22
+ import FormattedRelativeDate from '@plone/volto/components/theme/FormattedDate/FormattedRelativeDate';
23
+ import Image from '@plone/volto/components/theme/Image/Image';
24
+
25
+ const SUBREQUEST = 'batch-upload';
26
+
27
+ const messages = defineMessages({
28
+ cancel: {
29
+ id: 'Cancel',
30
+ defaultMessage: 'Cancel',
31
+ },
32
+ upload: {
33
+ id: '{count, plural, one {Upload {count} file} other {Upload {count} files}}',
34
+ defaultMessage:
35
+ '{count, plural, one {Upload {count} file} other {Upload {count} files}}',
36
+ },
37
+ filesUploaded: {
38
+ id: 'Files uploaded: {uploadedFiles}',
39
+ defaultMessage: 'Files uploaded: {uploadedFiles}',
40
+ },
41
+ dropFiles: {
42
+ id: 'Drop files here to upload',
43
+ defaultMessage: 'Drop files here to upload',
44
+ },
45
+ releaseToAdd: {
46
+ id: 'Release to add file(s) to this folder',
47
+ defaultMessage: 'Release to add file(s) to this folder',
48
+ },
49
+ totalFilesToUpload: {
50
+ id: 'Total files to upload: {totalFiles}',
51
+ defaultMessage: 'Total files to upload: {totalFiles}',
52
+ },
53
+ uploadFiles: {
54
+ id: 'Upload Files ({count})',
55
+ defaultMessage: 'Upload Files ({count})',
56
+ },
57
+ });
58
+ const hasFiles = (e) => {
59
+ return e.dataTransfer.types && e.dataTransfer.types.includes('Files');
60
+ };
61
+
62
+ const DropZoneContent = (props) => {
63
+ const { onOk, onCancel, pathname, children } = props;
64
+ const [isDragOver, setIsDragOver] = useState(false);
65
+ const [showModal, setShowModal] = useState(false);
66
+ const [droppedFiles, setDroppedFiles] = useState([]);
67
+ const [totalFiles, setTotalFiles] = useState(0);
68
+
69
+ const intl = useIntl();
70
+ const dispatch = useDispatch();
71
+
72
+ const request = useSelector(
73
+ (state) => state.content.subrequests?.[SUBREQUEST] || {},
74
+ shallowEqual,
75
+ );
76
+
77
+ const uploadedFiles = useSelector((state) => state.content.uploadedFiles);
78
+ const prevrequestloading = usePrevious(request.loading);
79
+
80
+ useEffect(() => {
81
+ if (prevrequestloading && request.loaded) {
82
+ onOk();
83
+ setDroppedFiles([]);
84
+ }
85
+ }, [prevrequestloading, request.loaded, onOk]);
86
+
87
+ const handleDragEnter = (e) => {
88
+ if (!hasFiles(e)) {
89
+ return;
90
+ }
91
+ e.preventDefault();
92
+ e.stopPropagation();
93
+ setIsDragOver(true);
94
+ };
95
+
96
+ const handleDragLeave = (e) => {
97
+ if (!hasFiles(e)) {
98
+ return;
99
+ }
100
+ e.preventDefault();
101
+ e.stopPropagation();
102
+ if (!e.currentTarget.contains(e.relatedTarget)) {
103
+ setIsDragOver(false);
104
+ }
105
+ };
106
+
107
+ const handleDragOver = (e) => {
108
+ if (!hasFiles(e)) {
109
+ return;
110
+ }
111
+ e.preventDefault();
112
+ e.stopPropagation();
113
+ };
114
+
115
+ const onDrop = async (e) => {
116
+ if (!hasFiles(e)) {
117
+ return;
118
+ }
119
+ setIsDragOver(false);
120
+ const newFiles = Array.from(e.dataTransfer.files);
121
+ const validFiles = [];
122
+ for (let i = 0; i < newFiles.length; i++) {
123
+ if (validateFileUploadSize(newFiles[i], intl.formatMessage)) {
124
+ await readAsDataURL(newFiles[i]).then((data) => {
125
+ const fields = data.match(/^data:(.*);(.*),(.*)$/);
126
+ newFiles[i].preview = fields[0];
127
+ });
128
+ validFiles.push(newFiles[i]);
129
+ }
130
+ }
131
+ setDroppedFiles((prev) => prev.concat(validFiles));
132
+ setTotalFiles((prev) => prev + validFiles.length);
133
+ setShowModal(true);
134
+ };
135
+
136
+ const handleCloseModal = () => {
137
+ setShowModal(false);
138
+ onCancel();
139
+ setDroppedFiles([]);
140
+ setTotalFiles(0);
141
+ };
142
+
143
+ const onSubmit = () => {
144
+ Promise.all(droppedFiles.map((file) => readAsDataURL(file))).then(
145
+ (dataUrls) => {
146
+ dispatch(
147
+ createContent(
148
+ pathname,
149
+ droppedFiles.map((file, index) => {
150
+ const fields = dataUrls[index].match(/^data:(.*);(.*),(.*)$/);
151
+ const image = fields[1].split('/')[0] === 'image';
152
+ return {
153
+ '@type': image ? 'Image' : 'File',
154
+ title: file.name,
155
+ [image ? 'image' : 'file']: {
156
+ data: fields[3],
157
+ encoding: fields[2],
158
+ 'content-type': fields[1],
159
+ filename: file.name,
160
+ },
161
+ };
162
+ }),
163
+ SUBREQUEST,
164
+ ),
165
+ );
166
+ },
167
+ );
168
+ handleCloseModal();
169
+ };
170
+ const onRemoveFile = (index) => {
171
+ const updatedFiles = droppedFiles.filter((file, i) => i !== index);
172
+ setDroppedFiles(updatedFiles);
173
+ setTotalFiles(updatedFiles.length);
174
+ };
175
+
176
+ const onChangeFileName = (e, index) => {
177
+ let copyOfFiles = [...droppedFiles];
178
+ let originalFile = droppedFiles[index];
179
+ let newFile = new File([originalFile], e.target.value, {
180
+ type: originalFile.type,
181
+ });
182
+
183
+ newFile.preview = originalFile.preview;
184
+ newFile.path = e.target.value;
185
+ copyOfFiles[index] = newFile;
186
+ setDroppedFiles(copyOfFiles);
187
+ };
188
+
189
+ return (
190
+ <>
191
+ <div
192
+ className={cx('contents-dropzone', {
193
+ 'drag-over': isDragOver,
194
+ 'drag-inactive': !isDragOver,
195
+ })}
196
+ onDragEnter={handleDragEnter}
197
+ onDragLeave={handleDragLeave}
198
+ onDragOver={handleDragOver}
199
+ onDrop={onDrop}
200
+ >
201
+ {children}
202
+ {isDragOver && (
203
+ <div className="dropzone-overlay">
204
+ <div className="dropzone-content">
205
+ <Icon name={uploadSVG} size="48px" />
206
+ <h3>{intl.formatMessage(messages.dropFiles)}</h3>
207
+ <p>{intl.formatMessage(messages.releaseToAdd)}</p>
208
+ </div>
209
+ </div>
210
+ )}
211
+ </div>
212
+ <Modal
213
+ open={totalFiles > 0 && showModal}
214
+ onClose={handleCloseModal}
215
+ className="contents-upload-modal"
216
+ >
217
+ <Modal.Header>
218
+ {intl.formatMessage(messages.uploadFiles, {
219
+ count: droppedFiles.length,
220
+ })}
221
+ </Modal.Header>
222
+ <Dimmer active={request.loading}>
223
+ <div className="progress-container">
224
+ <Progress
225
+ className="progress-bar"
226
+ value={uploadedFiles}
227
+ total={totalFiles}
228
+ >
229
+ {intl.formatMessage(messages.filesUploaded, {
230
+ uploadedFiles,
231
+ })}
232
+ <br />
233
+ {intl.formatMessage(messages.totalFilesToUpload, {
234
+ totalFiles,
235
+ })}
236
+ </Progress>
237
+ </div>
238
+ </Dimmer>
239
+ <Modal.Content>
240
+ {droppedFiles.length > 0 && (
241
+ <Table compact singleLine>
242
+ <Table.Header>
243
+ <Table.Row>
244
+ <Table.HeaderCell width={8}>
245
+ <FormattedMessage id="Filename" defaultMessage="Filename" />
246
+ </Table.HeaderCell>
247
+ <Table.HeaderCell width={4}>
248
+ <FormattedMessage
249
+ id="Last modified"
250
+ defaultMessage="Last modified"
251
+ />
252
+ </Table.HeaderCell>
253
+ <Table.HeaderCell width={4}>
254
+ <FormattedMessage
255
+ id="File size"
256
+ defaultMessage="File size"
257
+ />
258
+ </Table.HeaderCell>
259
+ <Table.HeaderCell width={4}>
260
+ <FormattedMessage id="Preview" defaultMessage="Preview" />
261
+ </Table.HeaderCell>
262
+ <Table.HeaderCell />
263
+ </Table.Row>
264
+ </Table.Header>
265
+ <Table.Body>
266
+ {droppedFiles.map((file, index) => (
267
+ <Table.Row className="upload-row" key={index}>
268
+ <Table.Cell>
269
+ <Input
270
+ className="file-name"
271
+ value={file.name}
272
+ onChange={(e) => onChangeFileName(e, index)}
273
+ />
274
+ </Table.Cell>
275
+ <Table.Cell>
276
+ {file.lastModifiedDate && (
277
+ <FormattedRelativeDate date={file.lastModifiedDate} />
278
+ )}
279
+ </Table.Cell>
280
+ <Table.Cell>{filesize(file.size, { round: 0 })}</Table.Cell>
281
+ <Table.Cell>
282
+ {file.type.split('/')[0] === 'image' && (
283
+ <Image
284
+ src={file.preview}
285
+ height={60}
286
+ className="ui image"
287
+ />
288
+ )}
289
+ </Table.Cell>
290
+ <Table.Cell>
291
+ <Icon
292
+ name={clearSVG}
293
+ size="24px"
294
+ onClick={() => onRemoveFile(index)}
295
+ />
296
+ </Table.Cell>
297
+ </Table.Row>
298
+ ))}
299
+ </Table.Body>
300
+ </Table>
301
+ )}
302
+ </Modal.Content>
303
+ <Modal.Actions>
304
+ {droppedFiles.length > 0 && (
305
+ <Button
306
+ basic
307
+ circular
308
+ primary
309
+ floated="right"
310
+ icon="arrow right"
311
+ aria-label={intl.formatMessage(messages.upload, {
312
+ count: droppedFiles.length,
313
+ })}
314
+ onClick={onSubmit}
315
+ title={intl.formatMessage(messages.upload, {
316
+ count: droppedFiles.length,
317
+ })}
318
+ size="big"
319
+ />
320
+ )}
321
+ <Button
322
+ basic
323
+ circular
324
+ secondary
325
+ icon="remove"
326
+ aria-label={intl.formatMessage(messages.cancel)}
327
+ title={intl.formatMessage(messages.cancel)}
328
+ floated="right"
329
+ size="big"
330
+ onClick={handleCloseModal}
331
+ />
332
+ </Modal.Actions>
333
+ </Modal>
334
+ </>
335
+ );
336
+ };
337
+
338
+ export default DropZoneContent;
@@ -58,6 +58,7 @@ const withObjectBrowser = (WrappedComponent) =>
58
58
  selectableTypes,
59
59
  maximumSelectionSize,
60
60
  currentPath,
61
+ onlyFolderishSelectable,
61
62
  } = {}) =>
62
63
  this.setState(() => ({
63
64
  isObjectBrowserOpen: true,
@@ -70,6 +71,7 @@ const withObjectBrowser = (WrappedComponent) =>
70
71
  selectableTypes,
71
72
  maximumSelectionSize,
72
73
  currentPath,
74
+ onlyFolderishSelectable,
73
75
  }));
74
76
 
75
77
  closeObjectBrowser = () => this.setState({ isObjectBrowserOpen: false });
@@ -110,6 +112,7 @@ const withObjectBrowser = (WrappedComponent) =>
110
112
  searchableTypes={this.state.searchableTypes}
111
113
  selectableTypes={this.state.selectableTypes}
112
114
  maximumSelectionSize={this.state.maximumSelectionSize}
115
+ onlyFolderishSelectable={this.state.onlyFolderishSelectable}
113
116
  />
114
117
  </SidebarPopup>
115
118
  </>
@@ -86,6 +86,7 @@ class ObjectBrowserBody extends Component {
86
86
  maximumSelectionSize: PropTypes.number,
87
87
  contextURL: PropTypes.string,
88
88
  searchableTypes: PropTypes.arrayOf(PropTypes.string),
89
+ onlyFolderishSelectable: PropTypes.bool,
89
90
  };
90
91
 
91
92
  /**
@@ -101,6 +102,7 @@ class ObjectBrowserBody extends Component {
101
102
  selectableTypes: [],
102
103
  searchableTypes: null,
103
104
  maximumSelectionSize: null,
105
+ onlyFolderishSelectable: false,
104
106
  };
105
107
 
106
108
  /**
@@ -329,7 +331,17 @@ class ObjectBrowserBody extends Component {
329
331
  };
330
332
 
331
333
  isSelectable = (item) => {
332
- const { maximumSelectionSize, data, mode, selectableTypes } = this.props;
334
+ const {
335
+ maximumSelectionSize,
336
+ data,
337
+ mode,
338
+ selectableTypes,
339
+ onlyFolderishSelectable,
340
+ } = this.props;
341
+
342
+ if (onlyFolderishSelectable && !item.is_folderish) {
343
+ return false;
344
+ }
333
345
  if (
334
346
  maximumSelectionSize &&
335
347
  data &&
@@ -82,6 +82,7 @@ export class ObjectBrowserWidgetComponent extends Component {
82
82
  openObjectBrowser: PropTypes.func.isRequired,
83
83
  allowExternals: PropTypes.bool,
84
84
  placeholder: PropTypes.string,
85
+ onlyFolderishSelectable: PropTypes.bool,
85
86
  };
86
87
 
87
88
  /**
@@ -98,6 +99,7 @@ export class ObjectBrowserWidgetComponent extends Component {
98
99
  return: 'multiple',
99
100
  initialPath: '',
100
101
  allowExternals: false,
102
+ onlyFolderishSelectable: false,
101
103
  };
102
104
 
103
105
  state = {
@@ -315,6 +317,9 @@ export class ObjectBrowserWidgetComponent extends Component {
315
317
  maximumSelectionSize:
316
318
  this.props.widgetOptions?.pattern_options?.maximumSelectionSize ||
317
319
  this.props.maximumSelectionSize,
320
+ onlyFolderishSelectable:
321
+ this.props.widgetOptions?.pattern_options?.onlyFolderishSelectable ||
322
+ this.props.onlyFolderishSelectable,
318
323
  });
319
324
  };
320
325
 
@@ -15,8 +15,12 @@ import groupBy from 'lodash/groupBy';
15
15
  import isEmpty from 'lodash/isEmpty';
16
16
  import map from 'lodash/map';
17
17
  import { defineMessages, injectIntl } from 'react-intl';
18
+ import { withRouter } from 'react-router';
18
19
  import { getQuerystring } from '@plone/volto/actions/querystring/querystring';
20
+ import { getQueryStringResults } from '@plone/volto/actions/querystringsearch/querystringsearch';
19
21
  import Icon from '@plone/volto/components/theme/Icon/Icon';
22
+ import ObjectBrowserWidget from '@plone/volto/components/manage/Widgets/ObjectBrowserWidget';
23
+ import NumberWidget from '@plone/volto/components/manage/Widgets/NumberWidget';
20
24
  import { injectLazyLibs } from '@plone/volto/helpers/Loadable/Loadable';
21
25
  import cx from 'classnames';
22
26
  import config from '@plone/volto/registry';
@@ -43,8 +47,27 @@ const messages = defineMessages({
43
47
  id: 'querystring-widget-select',
44
48
  defaultMessage: 'Select…',
45
49
  },
50
+ currentPath: {
51
+ id: 'query-widget-currentPath',
52
+ defaultMessage: 'Current path (./)',
53
+ },
54
+ parentPath: {
55
+ id: 'query-widget-parentPath',
56
+ defaultMessage: 'Parent path (../)',
57
+ },
46
58
  });
47
59
 
60
+ const parseUidDepth = (val) => {
61
+ if (typeof val !== 'string') return { uid: '', depth: 1 };
62
+ const lastSep = String(val).lastIndexOf('::');
63
+ if (lastSep !== -1) {
64
+ const uid = val.substring(0, lastSep);
65
+ const parsed = parseInt(val.substring(lastSep + 2), 10);
66
+ return { uid, depth: Number.isNaN(parsed) ? 1 : parsed };
67
+ }
68
+ return { uid: val, depth: 1 };
69
+ };
70
+
48
71
  /**
49
72
  * Widget for a querystring value, to define a catalog search criteria.
50
73
  */
@@ -97,6 +120,7 @@ export class QuerystringWidgetComponent extends Component {
97
120
  };
98
121
  this.onChangeValue = this.onChangeValue.bind(this);
99
122
  this.getWidget = this.getWidget.bind(this);
123
+ this.loadReferenceWidgetItem = this.loadReferenceWidgetItem.bind(this);
100
124
  }
101
125
 
102
126
  /**
@@ -111,6 +135,27 @@ export class QuerystringWidgetComponent extends Component {
111
135
  this.props.getQuerystring();
112
136
  }
113
137
 
138
+ loadReferenceWidgetItem(v) {
139
+ const loading =
140
+ this.props.reference[`${v}_query_reference`]?.loading ?? false;
141
+ if (!loading && v?.length > 0) {
142
+ this.props.getQueryStringResults(
143
+ '/',
144
+ {
145
+ b_size: 1,
146
+ query: [
147
+ {
148
+ i: 'path',
149
+ o: 'plone.app.querystring.operation.string.absolutePath',
150
+ v: v + '::0',
151
+ },
152
+ ],
153
+ },
154
+ v + '_query_reference',
155
+ );
156
+ }
157
+ }
158
+
114
159
  /**
115
160
  * Get correct widget
116
161
  * @method getWidget
@@ -118,15 +163,16 @@ export class QuerystringWidgetComponent extends Component {
118
163
  * @param {number} index Row index.
119
164
  * @returns {Object} Widget.
120
165
  */
121
- getWidget(row, index, Select) {
166
+ getWidget(row, index, Select, intl) {
122
167
  const props = {
123
168
  fluid: true,
124
169
  value: row.v,
125
170
  onChange: (data) => this.onChangeValue(index, data.target.value),
126
171
  };
127
172
  const values = this.props.indexes[row.i].values;
173
+ const operator = this.props.indexes[row.i].operators[row.o];
128
174
 
129
- switch (this.props.indexes[row.i].operators[row.o].widget) {
175
+ switch (operator.widget) {
130
176
  case null:
131
177
  return <span />;
132
178
  case 'DateWidget':
@@ -217,13 +263,93 @@ export class QuerystringWidgetComponent extends Component {
217
263
  </Form.Field>
218
264
  );
219
265
  case 'ReferenceWidget':
266
+ const { uid: uidValue, depth: depthValue } = parseUidDepth(props.value);
267
+ if (!this.props.reference[`${uidValue}_query_reference`]) {
268
+ this.loadReferenceWidgetItem(uidValue);
269
+ }
270
+ const referenceItem = this.props.reference[
271
+ `${uidValue}_query_reference`
272
+ ]
273
+ ? this.props.reference[`${uidValue}_query_reference`].items[0]
274
+ : null;
275
+ return (
276
+ <div className="location-object-browser">
277
+ <Form.Field className="object-browser-field">
278
+ <ObjectBrowserWidget
279
+ id={`query-reference-widget-${index}`}
280
+ mode="link"
281
+ onChange={(id, data) => {
282
+ const itemSelected = data.length > 0 ? data[0] : {};
283
+ const uid = itemSelected.UID ?? '';
284
+ this.onChangeValue(index, uid ? `${uid}::${depthValue}` : '');
285
+ this.loadReferenceWidgetItem(uid);
286
+ }}
287
+ value={uidValue && this.props.reference ? [referenceItem] : []}
288
+ wrapped={false}
289
+ onlyFolderishSelectable={true}
290
+ allowExternals={true}
291
+ />
292
+ </Form.Field>
293
+
294
+ {uidValue && (
295
+ <Form.Field className="reference-widget-depth">
296
+ <NumberWidget
297
+ title={intl.formatMessage({
298
+ id: 'Depth',
299
+ defaultMessage: 'Depth',
300
+ })}
301
+ min={1}
302
+ step={1}
303
+ value={depthValue}
304
+ onChange={(id, value) => {
305
+ const newDepth = parseInt(value, 10) || 1;
306
+ const curUid = uidValue || '';
307
+ this.onChangeValue(index, `${curUid}::${newDepth}`);
308
+ }}
309
+ />
310
+ </Form.Field>
311
+ )}
312
+ </div>
313
+ );
314
+ case 'RelativePathWidget':
315
+ const relativePathOptions = [
316
+ {
317
+ label: intl.formatMessage(messages.currentPath),
318
+ value: './',
319
+ },
320
+ {
321
+ label: intl.formatMessage(messages.parentPath),
322
+ value: '../',
323
+ },
324
+ ];
325
+ return (
326
+ <Form.Field style={{ flex: '1 0 auto', maxWidth: '92%' }}>
327
+ <Select
328
+ {...props}
329
+ className="react-select-container"
330
+ classNamePrefix="react-select"
331
+ options={relativePathOptions}
332
+ styles={customSelectStyles}
333
+ placeholder={this.props.intl.formatMessage(messages.select)}
334
+ theme={selectTheme}
335
+ components={{ DropdownIndicator, Option }}
336
+ onChange={(data) => {
337
+ this.onChangeValue(index, data.value);
338
+ }}
339
+ isMulti={false}
340
+ value={
341
+ relativePathOptions.filter((p) => p.value === props.value)?.[0]
342
+ }
343
+ />
344
+ </Form.Field>
345
+ );
220
346
  default:
221
347
  // if (row.o === 'plone.app.querystring.operation.string.relativePath') {
222
348
  // props.onChange = data => this.onChangeValue(index, data.target.value);
223
349
  // }
224
350
  return (
225
351
  <Form.Field style={{ flex: '1 0 auto' }}>
226
- <Input {...props} />
352
+ <Input {...props} description={operator.description} />
227
353
  </Form.Field>
228
354
  );
229
355
  }
@@ -334,7 +460,7 @@ export class QuerystringWidgetComponent extends Component {
334
460
  value: row.i,
335
461
  label: indexes[row.i]?.title,
336
462
  }}
337
- onChange={(data) =>
463
+ onChange={(data) => {
338
464
  onChange(
339
465
  id,
340
466
  map(value, (curRow, curIndex) =>
@@ -346,8 +472,8 @@ export class QuerystringWidgetComponent extends Component {
346
472
  }
347
473
  : curRow,
348
474
  ),
349
- )
350
- }
475
+ );
476
+ }}
351
477
  />
352
478
  </Form.Field>
353
479
  <Form.Field style={{ flex: '1 0 auto' }}>
@@ -408,7 +534,7 @@ export class QuerystringWidgetComponent extends Component {
408
534
  </Button>
409
535
  )}
410
536
  </div>
411
- {this.getWidget(row, index, Select)}
537
+ {this.getWidget(row, index, Select, intl)}
412
538
  {this.props.indexes[row.i].operators[row.o].widget && (
413
539
  <Button
414
540
  onClick={(event) => {
@@ -500,10 +626,12 @@ export class QuerystringWidgetComponent extends Component {
500
626
  export default compose(
501
627
  injectIntl,
502
628
  injectLazyLibs(['reactSelect']),
629
+ withRouter,
503
630
  connect(
504
- (state) => ({
631
+ (state, props) => ({
505
632
  indexes: state.querystring.indexes,
633
+ reference: state.querystringsearch.subrequests,
506
634
  }),
507
- { getQuerystring },
635
+ { getQuerystring, getQueryStringResults },
508
636
  ),
509
637
  )(QuerystringWidgetComponent);
@@ -4,7 +4,7 @@ import { useDispatch } from 'react-redux';
4
4
  import { useCookies } from 'react-cookie';
5
5
  import config from '@plone/volto/registry';
6
6
  import { changeLanguage } from '@plone/volto/actions/language/language';
7
- import { toGettextLang } from '@plone/volto/helpers/Utils/Utils';
7
+ import { toGettextLang, toBackendLang } from '@plone/volto/helpers/Utils/Utils';
8
8
 
9
9
  const MultilingualRedirector = (props) => {
10
10
  const { settings } = config;
@@ -38,7 +38,7 @@ const MultilingualRedirector = (props) => {
38
38
  }, [pathname, dispatch, redirectToLanguage, settings.isMultilingual]);
39
39
 
40
40
  return pathname === '/' && settings.isMultilingual ? (
41
- <Redirect to={`/${redirectToLanguage}`} />
41
+ <Redirect to={`/${toBackendLang(redirectToLanguage)}`} />
42
42
  ) : (
43
43
  <>{children}</>
44
44
  );