@plone/volto 19.0.0-alpha.18 → 19.0.0-alpha.19

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 (109) hide show
  1. package/CHANGELOG.md +22 -5
  2. package/locales/af.json +1 -1
  3. package/locales/ar.json +1 -1
  4. package/locales/bg.json +1 -1
  5. package/locales/bn.json +1 -1
  6. package/locales/ca/LC_MESSAGES/volto.po +33 -0
  7. package/locales/ca.json +1 -1
  8. package/locales/cs.json +1 -1
  9. package/locales/cy.json +1 -1
  10. package/locales/da.json +1 -1
  11. package/locales/de/LC_MESSAGES/volto.po +33 -0
  12. package/locales/de.json +1 -1
  13. package/locales/el.json +1 -1
  14. package/locales/en/LC_MESSAGES/volto.po +33 -0
  15. package/locales/en.json +1 -1
  16. package/locales/en_AU.json +1 -1
  17. package/locales/en_GB.json +1 -1
  18. package/locales/eo.json +1 -1
  19. package/locales/es/LC_MESSAGES/volto.po +33 -0
  20. package/locales/es.json +1 -1
  21. package/locales/et.json +1 -1
  22. package/locales/eu/LC_MESSAGES/volto.po +33 -0
  23. package/locales/eu.json +1 -1
  24. package/locales/fa.json +1 -1
  25. package/locales/fi/LC_MESSAGES/volto.po +33 -0
  26. package/locales/fi.json +1 -1
  27. package/locales/fr/LC_MESSAGES/volto.po +33 -0
  28. package/locales/fr.json +1 -1
  29. package/locales/fu.json +1 -1
  30. package/locales/gl.json +1 -1
  31. package/locales/he.json +1 -1
  32. package/locales/hi/LC_MESSAGES/volto.po +33 -0
  33. package/locales/hi.json +1 -1
  34. package/locales/hr.json +1 -1
  35. package/locales/hu.json +1 -1
  36. package/locales/hy.json +1 -1
  37. package/locales/id.json +1 -1
  38. package/locales/it/LC_MESSAGES/volto.po +33 -0
  39. package/locales/it.json +1 -1
  40. package/locales/ja/LC_MESSAGES/volto.po +33 -0
  41. package/locales/ja.json +1 -1
  42. package/locales/ka.json +1 -1
  43. package/locales/kn.json +1 -1
  44. package/locales/ko.json +1 -1
  45. package/locales/lt.json +1 -1
  46. package/locales/lv.json +1 -1
  47. package/locales/mi.json +1 -1
  48. package/locales/mk.json +1 -1
  49. package/locales/my.json +1 -1
  50. package/locales/nb_NO.json +1 -1
  51. package/locales/nl/LC_MESSAGES/volto.po +33 -0
  52. package/locales/nl.json +1 -1
  53. package/locales/nn.json +1 -1
  54. package/locales/pl.json +1 -1
  55. package/locales/pt/LC_MESSAGES/volto.po +33 -0
  56. package/locales/pt.json +1 -1
  57. package/locales/pt_BR/LC_MESSAGES/volto.po +33 -0
  58. package/locales/pt_BR.json +1 -1
  59. package/locales/rm.json +1 -1
  60. package/locales/ro/LC_MESSAGES/volto.po +33 -0
  61. package/locales/ro.json +1 -1
  62. package/locales/ru/LC_MESSAGES/volto.po +33 -0
  63. package/locales/ru.json +1 -1
  64. package/locales/sk.json +1 -1
  65. package/locales/sl.json +1 -1
  66. package/locales/sm.json +1 -1
  67. package/locales/sq.json +1 -1
  68. package/locales/sr.json +1 -1
  69. package/locales/sr@cyrl.json +1 -1
  70. package/locales/sr@latn.json +1 -1
  71. package/locales/sv.json +1 -1
  72. package/locales/ta.json +1 -1
  73. package/locales/te.json +1 -1
  74. package/locales/th.json +1 -1
  75. package/locales/to.json +1 -1
  76. package/locales/tr.json +1 -1
  77. package/locales/uk.json +1 -1
  78. package/locales/vi.json +1 -1
  79. package/locales/volto.pot +34 -1
  80. package/locales/zh_CN/LC_MESSAGES/volto.po +33 -0
  81. package/locales/zh_CN.json +1 -1
  82. package/locales/zh_Hant.json +1 -1
  83. package/locales/zh_Hant_HK.json +1 -1
  84. package/package.json +5 -5
  85. package/src/components/manage/BlockChooser/BlockChooser.jsx +1 -0
  86. package/src/components/manage/Contents/Contents.jsx +685 -671
  87. package/src/components/manage/Contents/DropZoneContent.jsx +323 -0
  88. package/src/components/manage/Form/ModalForm.jsx +12 -10
  89. package/src/components/manage/Form/ModalForm.test.jsx +26 -0
  90. package/src/components/manage/Sidebar/ObjectBrowser.jsx +3 -0
  91. package/src/components/manage/Sidebar/ObjectBrowserBody.jsx +13 -1
  92. package/src/components/manage/Widgets/ObjectBrowserWidget.jsx +5 -0
  93. package/src/components/manage/Widgets/QueryWidget.jsx +136 -9
  94. package/src/components/theme/Unauthorized/Unauthorized.jsx +5 -2
  95. package/src/components/theme/View/View.jsx +7 -0
  96. package/src/reducers/querystring/querystring.js +8 -1
  97. package/theme/themes/pastanaga/extras/contents.less +63 -0
  98. package/theme/themes/pastanaga/extras/widgets.less +34 -0
  99. package/types/components/manage/Contents/DropZoneContent.d.ts +2 -0
  100. package/types/components/manage/Controlpanels/Relations/RelationsMatrix.d.ts +1 -1
  101. package/types/components/manage/Multilingual/ManageTranslations.d.ts +1 -1
  102. package/types/components/manage/Sidebar/ObjectBrowser.d.ts +1 -1
  103. package/types/components/manage/Widgets/ImageWidget.d.ts +1 -1
  104. package/types/components/manage/Widgets/InternalUrlWidget.d.ts +1 -1
  105. package/types/components/manage/Widgets/ObjectBrowserWidget.d.ts +2 -0
  106. package/types/components/manage/Widgets/QueryWidget.d.ts +5 -2
  107. package/types/components/manage/Widgets/UrlWidget.d.ts +1 -1
  108. package/types/components/manage/Widgets/index.d.ts +3 -3
  109. package/types/components/theme/Unauthorized/Unauthorized.d.ts +2 -2
@@ -0,0 +1,323 @@
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
+
59
+ const DropZoneContent = (props) => {
60
+ const { onOk, onCancel, pathname, children } = props;
61
+ const [isDragOver, setIsDragOver] = useState(false);
62
+ const [showModal, setShowModal] = useState(false);
63
+ const [droppedFiles, setDroppedFiles] = useState([]);
64
+ const [totalFiles, setTotalFiles] = useState(0);
65
+
66
+ const intl = useIntl();
67
+ const dispatch = useDispatch();
68
+
69
+ const request = useSelector(
70
+ (state) => state.content.subrequests?.[SUBREQUEST] || {},
71
+ shallowEqual,
72
+ );
73
+
74
+ const uploadedFiles = useSelector((state) => state.content.uploadedFiles);
75
+ const prevrequestloading = usePrevious(request.loading);
76
+
77
+ useEffect(() => {
78
+ if (prevrequestloading && request.loaded) {
79
+ onOk();
80
+ setDroppedFiles([]);
81
+ }
82
+ }, [prevrequestloading, request.loaded, onOk]);
83
+
84
+ const handleDragEnter = (e) => {
85
+ e.preventDefault();
86
+ e.stopPropagation();
87
+ setIsDragOver(true);
88
+ };
89
+
90
+ const handleDragLeave = (e) => {
91
+ e.preventDefault();
92
+ e.stopPropagation();
93
+ if (!e.currentTarget.contains(e.relatedTarget)) {
94
+ setIsDragOver(false);
95
+ }
96
+ };
97
+
98
+ const handleDragOver = (e) => {
99
+ e.preventDefault();
100
+ e.stopPropagation();
101
+ };
102
+
103
+ const onDrop = async (e) => {
104
+ setIsDragOver(false);
105
+ const newFiles = Array.from(e.dataTransfer.files);
106
+ const validFiles = [];
107
+ for (let i = 0; i < newFiles.length; i++) {
108
+ if (validateFileUploadSize(newFiles[i], intl.formatMessage)) {
109
+ await readAsDataURL(newFiles[i]).then((data) => {
110
+ const fields = data.match(/^data:(.*);(.*),(.*)$/);
111
+ newFiles[i].preview = fields[0];
112
+ });
113
+ validFiles.push(newFiles[i]);
114
+ }
115
+ }
116
+ setDroppedFiles((prev) => prev.concat(newFiles));
117
+ setTotalFiles((prev) => prev + validFiles.length);
118
+ setShowModal(true);
119
+ };
120
+
121
+ const handleCloseModal = () => {
122
+ setShowModal(false);
123
+ onCancel();
124
+ setDroppedFiles([]);
125
+ setTotalFiles(0);
126
+ };
127
+
128
+ const onSubmit = () => {
129
+ Promise.all(droppedFiles.map((file) => readAsDataURL(file))).then(
130
+ (dataUrls) => {
131
+ dispatch(
132
+ createContent(
133
+ pathname,
134
+ droppedFiles.map((file, index) => {
135
+ const fields = dataUrls[index].match(/^data:(.*);(.*),(.*)$/);
136
+ const image = fields[1].split('/')[0] === 'image';
137
+ return {
138
+ '@type': image ? 'Image' : 'File',
139
+ title: file.name,
140
+ [image ? 'image' : 'file']: {
141
+ data: fields[3],
142
+ encoding: fields[2],
143
+ 'content-type': fields[1],
144
+ filename: file.name,
145
+ },
146
+ };
147
+ }),
148
+ SUBREQUEST,
149
+ ),
150
+ );
151
+ },
152
+ );
153
+ handleCloseModal();
154
+ };
155
+ const onRemoveFile = (index) => {
156
+ const updatedFiles = droppedFiles.filter((file, i) => i !== index);
157
+ setDroppedFiles(updatedFiles);
158
+ setTotalFiles(updatedFiles.length);
159
+ };
160
+
161
+ const onChangeFileName = (e, index) => {
162
+ let copyOfFiles = [...droppedFiles];
163
+ let originalFile = droppedFiles[index];
164
+ let newFile = new File([originalFile], e.target.value, {
165
+ type: originalFile.type,
166
+ });
167
+
168
+ newFile.preview = originalFile.preview;
169
+ newFile.path = e.target.value;
170
+ copyOfFiles[index] = newFile;
171
+ setDroppedFiles(copyOfFiles);
172
+ };
173
+
174
+ return (
175
+ <>
176
+ <div
177
+ className={cx('contents-dropzone', {
178
+ 'drag-over': isDragOver,
179
+ 'drag-inactive': !isDragOver,
180
+ })}
181
+ onDragEnter={handleDragEnter}
182
+ onDragLeave={handleDragLeave}
183
+ onDragOver={handleDragOver}
184
+ onDrop={onDrop}
185
+ >
186
+ {children}
187
+ {isDragOver && (
188
+ <div className="dropzone-overlay">
189
+ <div className="dropzone-content">
190
+ <Icon name={uploadSVG} size="48px" />
191
+ <h3>{intl.formatMessage(messages.dropFiles)}</h3>
192
+ <p>{intl.formatMessage(messages.releaseToAdd)}</p>
193
+ </div>
194
+ </div>
195
+ )}
196
+ </div>
197
+ <Modal
198
+ open={showModal}
199
+ onClose={handleCloseModal}
200
+ className="contents-upload-modal"
201
+ >
202
+ <Modal.Header>
203
+ {intl.formatMessage(messages.uploadFiles, {
204
+ count: droppedFiles.length,
205
+ })}
206
+ </Modal.Header>
207
+ <Dimmer active={request.loading}>
208
+ <div className="progress-container">
209
+ <Progress
210
+ className="progress-bar"
211
+ value={uploadedFiles}
212
+ total={totalFiles}
213
+ >
214
+ {intl.formatMessage(messages.filesUploaded, {
215
+ uploadedFiles,
216
+ })}
217
+ <br />
218
+ {intl.formatMessage(messages.totalFilesToUpload, {
219
+ totalFiles,
220
+ })}
221
+ </Progress>
222
+ </div>
223
+ </Dimmer>
224
+ <Modal.Content>
225
+ {droppedFiles.length > 0 && (
226
+ <Table compact singleLine>
227
+ <Table.Header>
228
+ <Table.Row>
229
+ <Table.HeaderCell width={8}>
230
+ <FormattedMessage id="Filename" defaultMessage="Filename" />
231
+ </Table.HeaderCell>
232
+ <Table.HeaderCell width={4}>
233
+ <FormattedMessage
234
+ id="Last modified"
235
+ defaultMessage="Last modified"
236
+ />
237
+ </Table.HeaderCell>
238
+ <Table.HeaderCell width={4}>
239
+ <FormattedMessage
240
+ id="File size"
241
+ defaultMessage="File size"
242
+ />
243
+ </Table.HeaderCell>
244
+ <Table.HeaderCell width={4}>
245
+ <FormattedMessage id="Preview" defaultMessage="Preview" />
246
+ </Table.HeaderCell>
247
+ <Table.HeaderCell />
248
+ </Table.Row>
249
+ </Table.Header>
250
+ <Table.Body>
251
+ {droppedFiles.map((file, index) => (
252
+ <Table.Row className="upload-row" key={index}>
253
+ <Table.Cell>
254
+ <Input
255
+ className="file-name"
256
+ value={file.name}
257
+ onChange={(e) => onChangeFileName(e, index)}
258
+ />
259
+ </Table.Cell>
260
+ <Table.Cell>
261
+ {file.lastModifiedDate && (
262
+ <FormattedRelativeDate date={file.lastModifiedDate} />
263
+ )}
264
+ </Table.Cell>
265
+ <Table.Cell>{filesize(file.size, { round: 0 })}</Table.Cell>
266
+ <Table.Cell>
267
+ {file.type.split('/')[0] === 'image' && (
268
+ <Image
269
+ src={file.preview}
270
+ height={60}
271
+ className="ui image"
272
+ />
273
+ )}
274
+ </Table.Cell>
275
+ <Table.Cell>
276
+ <Icon
277
+ name={clearSVG}
278
+ size="24px"
279
+ onClick={() => onRemoveFile(index)}
280
+ />
281
+ </Table.Cell>
282
+ </Table.Row>
283
+ ))}
284
+ </Table.Body>
285
+ </Table>
286
+ )}
287
+ </Modal.Content>
288
+ <Modal.Actions>
289
+ {droppedFiles.length > 0 && (
290
+ <Button
291
+ basic
292
+ circular
293
+ primary
294
+ floated="right"
295
+ icon="arrow right"
296
+ aria-label={intl.formatMessage(messages.upload, {
297
+ count: droppedFiles.length,
298
+ })}
299
+ onClick={onSubmit}
300
+ title={intl.formatMessage(messages.upload, {
301
+ count: droppedFiles.length,
302
+ })}
303
+ size="big"
304
+ />
305
+ )}
306
+ <Button
307
+ basic
308
+ circular
309
+ secondary
310
+ icon="remove"
311
+ aria-label={intl.formatMessage(messages.cancel)}
312
+ title={intl.formatMessage(messages.cancel)}
313
+ floated="right"
314
+ size="big"
315
+ onClick={handleCloseModal}
316
+ />
317
+ </Modal.Actions>
318
+ </Modal>
319
+ </>
320
+ );
321
+ };
322
+
323
+ export default DropZoneContent;
@@ -245,15 +245,17 @@ class ModalForm extends Component {
245
245
  const { schema, onCancel, description } = this.props;
246
246
  const currentFieldset = schema.fieldsets[this.state.currentTab];
247
247
 
248
- const fields = map(currentFieldset.fields, (field) => ({
249
- ...schema.properties[field],
250
- id: field,
251
- value: this.state.formData[field],
252
- required: schema.required.indexOf(field) !== -1,
253
- onChange: this.onChangeField,
254
- onBlur: this.onBlurField,
255
- onClick: this.onClickInput,
256
- }));
248
+ const fields = currentFieldset
249
+ ? map(currentFieldset.fields, (field) => ({
250
+ ...schema.properties[field],
251
+ id: field,
252
+ value: this.state.formData[field],
253
+ required: schema.required.indexOf(field) !== -1,
254
+ onChange: this.onChangeField,
255
+ onBlur: this.onBlurField,
256
+ onClick: this.onClickInput,
257
+ }))
258
+ : [];
257
259
 
258
260
  const state_errors = keys(this.state.errors).length > 0;
259
261
  return (
@@ -288,7 +290,7 @@ class ModalForm extends Component {
288
290
  )}
289
291
  <div>{this.props.submitError}</div>
290
292
  </Message>
291
- {schema.fieldsets.length > 1 && (
293
+ {schema.fieldsets?.length > 1 && (
292
294
  <Menu tabular stackable>
293
295
  {map(schema.fieldsets, (item, index) => (
294
296
  <Menu.Item
@@ -89,4 +89,30 @@ describe('ModalForm', () => {
89
89
  const loadingMessage = getByText(/renaming items.../i);
90
90
  expect(loadingMessage).toBeInTheDocument();
91
91
  });
92
+
93
+ it('renders with empty fieldsets array', () => {
94
+ const store = mockStore({
95
+ intl: {
96
+ locale: 'en',
97
+ messages: {},
98
+ },
99
+ });
100
+ const component = renderer.create(
101
+ <Provider store={store}>
102
+ <ModalForm
103
+ schema={{
104
+ fieldsets: [],
105
+ properties: {},
106
+ required: [],
107
+ }}
108
+ onSubmit={() => {}}
109
+ onCancel={() => {}}
110
+ open={false}
111
+ title="Action without form"
112
+ />
113
+ </Provider>,
114
+ );
115
+ const json = component.toJSON();
116
+ expect(json).toMatchSnapshot();
117
+ });
92
118
  });
@@ -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