@plone/volto 18.0.0-alpha.46 → 18.0.0-alpha.48

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 (84) hide show
  1. package/.eslintrc +0 -1
  2. package/CHANGELOG.md +64 -0
  3. package/locales/ca/LC_MESSAGES/volto.po +82 -15
  4. package/locales/ca.json +1 -1
  5. package/locales/de/LC_MESSAGES/volto.po +87 -20
  6. package/locales/de.json +1 -1
  7. package/locales/en/LC_MESSAGES/volto.po +82 -15
  8. package/locales/en.json +1 -1
  9. package/locales/es/LC_MESSAGES/volto.po +82 -15
  10. package/locales/es.json +1 -1
  11. package/locales/eu/LC_MESSAGES/volto.po +82 -15
  12. package/locales/eu.json +1 -1
  13. package/locales/fi/LC_MESSAGES/volto.po +82 -15
  14. package/locales/fi.json +1 -1
  15. package/locales/fr/LC_MESSAGES/volto.po +82 -15
  16. package/locales/fr.json +1 -1
  17. package/locales/hi/LC_MESSAGES/volto.po +82 -15
  18. package/locales/hi.json +1 -1
  19. package/locales/it/LC_MESSAGES/volto.po +82 -15
  20. package/locales/it.json +1 -1
  21. package/locales/ja/LC_MESSAGES/volto.po +82 -15
  22. package/locales/ja.json +1 -1
  23. package/locales/nl/LC_MESSAGES/volto.po +82 -15
  24. package/locales/nl.json +1 -1
  25. package/locales/pt/LC_MESSAGES/volto.po +82 -15
  26. package/locales/pt.json +1 -1
  27. package/locales/pt_BR/LC_MESSAGES/volto.po +82 -15
  28. package/locales/pt_BR.json +1 -1
  29. package/locales/ro/LC_MESSAGES/volto.po +82 -15
  30. package/locales/ro.json +1 -1
  31. package/locales/volto.pot +83 -16
  32. package/locales/zh_CN/LC_MESSAGES/volto.po +82 -15
  33. package/locales/zh_CN.json +1 -1
  34. package/package.json +11 -9
  35. package/razzle.config.js +8 -5
  36. package/src/actions/aliases/aliases.js +27 -7
  37. package/src/actions/aliases/aliases.test.js +1 -1
  38. package/src/components/manage/Blocks/Block/Edit.jsx +1 -2
  39. package/src/components/manage/Blocks/Block/EditBlockWrapper.jsx +5 -2
  40. package/src/components/manage/Blocks/Block/StyleWrapper.jsx +1 -1
  41. package/src/components/manage/Blocks/Listing/getAsyncData.js +8 -0
  42. package/src/components/manage/Blocks/ToC/View.jsx +18 -7
  43. package/src/components/manage/Blocks/ToC/variations/DefaultTocRenderer.jsx +2 -20
  44. package/src/components/manage/Blocks/ToC/variations/HorizontalMenu.jsx +2 -19
  45. package/src/components/manage/Controlpanels/Aliases.jsx +499 -412
  46. package/src/components/manage/Controlpanels/Aliases.test.jsx +7 -0
  47. package/src/components/manage/Edit/Edit.jsx +7 -3
  48. package/src/components/manage/Form/ModalForm.jsx +3 -1
  49. package/src/components/manage/Toolbar/PersonalTools.jsx +7 -7
  50. package/src/components/manage/Toolbar/PersonalTools.test.jsx +71 -0
  51. package/src/components/manage/Widgets/IdWidget.jsx +6 -7
  52. package/src/components/theme/App/App.jsx +2 -0
  53. package/src/components/theme/App/App.test.jsx +4 -3
  54. package/src/components/theme/Login/Login.jsx +1 -2
  55. package/src/components/theme/RouteAnnouncer/RouteAnnouncer.jsx +64 -0
  56. package/src/constants/ActionTypes.js +1 -0
  57. package/src/express-middleware/static.js +2 -2
  58. package/src/helpers/Api/Api.js +12 -1
  59. package/src/helpers/Blocks/Blocks.js +73 -33
  60. package/src/helpers/Blocks/Blocks.test.js +204 -27
  61. package/src/middleware/api.js +3 -0
  62. package/src/reducers/content/content.js +12 -0
  63. package/src/start-server.js +2 -5
  64. package/types/actions/aliases/aliases.d.ts +8 -1
  65. package/types/components/manage/Blocks/ToC/View.d.ts +1 -4
  66. package/types/components/manage/Blocks/ToC/variations/DefaultTocRenderer.d.ts +10 -5
  67. package/types/components/manage/Blocks/ToC/variations/HorizontalMenu.d.ts +10 -5
  68. package/types/components/manage/Blocks/ToC/variations/index.d.ts +16 -4
  69. package/types/components/manage/Contents/__mocks__/index.d.ts +0 -1
  70. package/types/components/manage/Controlpanels/Relations/RelationsMatrix.d.ts +1 -1
  71. package/types/components/manage/Controlpanels/index.d.ts +0 -1
  72. package/types/components/manage/Form/__mocks__/index.d.ts +0 -1
  73. package/types/components/manage/Form/index.d.ts +0 -1
  74. package/types/components/manage/Multilingual/ManageTranslations.d.ts +1 -1
  75. package/types/components/manage/Sidebar/ObjectBrowser.d.ts +1 -1
  76. package/types/components/manage/Widgets/InternalUrlWidget.d.ts +1 -1
  77. package/types/components/manage/Widgets/UrlWidget.d.ts +1 -1
  78. package/types/components/manage/Widgets/__mocks__/index.d.ts +0 -1
  79. package/types/components/manage/Widgets/index.d.ts +2 -3
  80. package/types/components/theme/RouteAnnouncer/RouteAnnouncer.d.ts +2 -0
  81. package/types/config/slots.d.ts +1 -1
  82. package/types/constants/ActionTypes.d.ts +1 -0
  83. package/types/helpers/Blocks/Blocks.d.ts +10 -1
  84. package/types/helpers/Helmet/Helmet.d.ts +1 -1
@@ -1,34 +1,38 @@
1
- import { useState, useEffect, useCallback } from 'react';
1
+ import { useState, useEffect, useCallback, useMemo } from 'react';
2
2
  import { useDispatch, useSelector } from 'react-redux';
3
3
  import { Link, useHistory, useLocation } from 'react-router-dom';
4
+ import { getBaseUrl, getParentUrl, Helmet } from '@plone/volto/helpers';
4
5
  import {
5
- getBaseUrl,
6
- getParentUrl,
7
- Helmet,
8
- usePrevious,
9
- } from '@plone/volto/helpers';
10
- import { removeAliases, addAliases, getAliases } from '@plone/volto/actions';
6
+ removeAliases,
7
+ addAliases,
8
+ getAliases,
9
+ uploadAliases,
10
+ } from '@plone/volto/actions/aliases/aliases';
11
11
  import { createPortal } from 'react-dom';
12
12
  import {
13
- Container,
14
13
  Button,
15
- Segment,
16
- Form,
17
14
  Checkbox,
15
+ Container,
16
+ Form,
18
17
  Header,
19
18
  Input,
19
+ Loader,
20
+ Menu,
21
+ Pagination,
20
22
  Radio,
21
- Message,
23
+ Segment,
22
24
  Table,
23
- Pagination,
24
- Menu,
25
25
  } from 'semantic-ui-react';
26
26
  import { FormattedMessage, defineMessages, useIntl } from 'react-intl';
27
27
  import DatetimeWidget from '@plone/volto/components/manage/Widgets/DatetimeWidget';
28
+ import FormFieldWrapper from '@plone/volto/components/manage/Widgets/FormFieldWrapper';
29
+ import { ModalForm } from '@plone/volto/components/manage/Form';
28
30
  import { Icon, Toolbar } from '@plone/volto/components';
31
+ import FormattedDate from '@plone/volto/components/theme/FormattedDate/FormattedDate';
29
32
  import { useClient } from '@plone/volto/hooks';
30
33
 
31
34
  import backSVG from '@plone/volto/icons/back.svg';
35
+ import editingSVG from '@plone/volto/icons/editing.svg';
32
36
  import { map } from 'lodash';
33
37
  import { toast } from 'react-toastify';
34
38
  import { Toast } from '@plone/volto/components';
@@ -42,6 +46,14 @@ const messages = defineMessages({
42
46
  id: 'URL Management',
43
47
  defaultMessage: 'URL Management',
44
48
  },
49
+ AddUrl: {
50
+ id: 'Add Alternative URL',
51
+ defaultMessage: 'Add Alternative URL',
52
+ },
53
+ EditUrl: {
54
+ id: 'Edit Alternative URL',
55
+ defaultMessage: 'Edit Alternative URL',
56
+ },
45
57
  success: {
46
58
  id: 'Success',
47
59
  defaultMessage: 'Success',
@@ -50,6 +62,66 @@ const messages = defineMessages({
50
62
  id: 'Alias has been added',
51
63
  defaultMessage: 'Alias has been added',
52
64
  },
65
+ successUpload: {
66
+ id: 'Aliases have been uploaded.',
67
+ defaultMessage: 'Aliases have been uploaded.',
68
+ },
69
+ successRemove: {
70
+ id: 'Aliases have been removed.',
71
+ defaultMessage: 'Aliases have been removed.',
72
+ },
73
+ filterByPrefix: {
74
+ id: 'Filter by prefix',
75
+ defaultMessage: 'Filter by path',
76
+ },
77
+ manualOrAuto: {
78
+ id: 'Manually or automatically added?',
79
+ defaultMessage: 'Manually or automatically added?',
80
+ },
81
+ createdAfter: {
82
+ id: 'Created after',
83
+ defaultMessage: 'Created after',
84
+ },
85
+ createdBefore: {
86
+ id: 'Created before',
87
+ defaultMessage: 'Created before',
88
+ },
89
+ altUrlPathTitle: {
90
+ id: 'Alternative url path (Required)',
91
+ defaultMessage: 'Alternative URL path (Required)',
92
+ },
93
+ altUrlError: {
94
+ id: 'Alternative url path must start with a slash.',
95
+ defaultMessage: 'Alternative URL path must start with a slash.',
96
+ },
97
+ targetUrlPathTitle: {
98
+ id: 'Target Path (Required)',
99
+ defaultMessage: 'Target Path (Required)',
100
+ },
101
+ BulkUploadAltUrls: {
102
+ id: 'BulkUploadAltUrls',
103
+ defaultMessage: 'Bulk upload CSV',
104
+ },
105
+ CSVFile: {
106
+ id: 'CSVFile',
107
+ defaultMessage: 'CSV file',
108
+ },
109
+ Both: {
110
+ id: 'Both',
111
+ defaultMessage: 'Both',
112
+ },
113
+ Automatically: {
114
+ id: 'Automatically',
115
+ defaultMessage: 'Automatically',
116
+ },
117
+ Manually: {
118
+ id: 'Manually',
119
+ defaultMessage: 'Manually',
120
+ },
121
+ examplePath: {
122
+ id: 'examplePath',
123
+ defaultMessage: '/example',
124
+ },
53
125
  });
54
126
 
55
127
  const filterChoices = [
@@ -67,215 +139,153 @@ const Aliases = (props) => {
67
139
  const { pathname } = useLocation();
68
140
  const history = useHistory();
69
141
 
142
+ const hasAdvancedFiltering = useSelector(
143
+ (state) => state.site.data?.features?.filter_aliases_by_date,
144
+ );
145
+ const hasBulkUpload = hasAdvancedFiltering !== undefined;
70
146
  const aliases = useSelector((state) => state.aliases);
71
147
  const [filterType, setFilterType] = useState(filterChoices[0]);
72
148
  const [createdBefore, setCreatedBefore] = useState(null);
73
- const [altUrlPath, setAltUrlPath] = useState('');
74
- const [isAltUrlCorrect, setIsAltUrlCorrect] = useState(false);
75
- const [targetUrlPath, setTargetUrlPath] = useState('');
149
+ const [createdAfter, setCreatedAfter] = useState(null);
76
150
  const [aliasesToRemove, setAliasesToRemove] = useState([]);
77
- const [errorMessageAdd, setErrorMessageAdd] = useState('');
78
151
  const [filterQuery, setFilterQuery] = useState('');
79
152
  const [activePage, setActivePage] = useState(1);
80
- const [pages, setPages] = useState('');
81
153
  const [itemsPerPage, setItemsPerPage] = useState(10);
154
+ const [addModalOpen, setAddModalOpen] = useState(false);
155
+ const [addError, setAddError] = useState(null);
156
+ const [editingData, setEditingData] = useState(null);
157
+ const [uploadModalOpen, setUploadModalOpen] = useState(false);
158
+ const [uploadError, setUploadError] = useState(null);
82
159
  const isClient = useClient();
83
160
 
84
- const prevaliasesitemstotal = usePrevious(aliases.items_total);
85
- const previtemsPerPage = usePrevious(itemsPerPage);
86
- const prevactivePage = usePrevious(activePage);
87
- const prevalturlpath = usePrevious(altUrlPath);
88
- const prevtargetUrlPath = usePrevious(targetUrlPath);
89
- const prevaliasesaddloading = usePrevious(aliases.add.loading);
90
- const prevaliasesremoveloading = usePrevious(aliases.remove.loading);
91
-
92
- useEffect(() => {
93
- if (
94
- prevaliasesitemstotal !== aliases.items_total ||
95
- previtemsPerPage !== itemsPerPage
96
- ) {
97
- const pages = Math.ceil(aliases.items_total / itemsPerPage);
98
-
99
- if (pages === 0 || isNaN(pages)) {
100
- setPages('');
101
- } else {
102
- setPages(pages);
103
- }
104
- }
105
- if (prevactivePage !== activePage || previtemsPerPage !== itemsPerPage) {
106
- dispatch(
107
- getAliases(getBaseUrl(pathname), {
108
- query: filterQuery,
109
- manual: filterType.value,
110
- datetime: createdBefore,
111
- batchSize: itemsPerPage === 'All' ? 999999999999 : itemsPerPage,
112
- batchStart: (activePage - 1) * itemsPerPage,
113
- }),
114
- );
115
- }
116
- if (prevalturlpath !== altUrlPath) {
117
- if (altUrlPath.charAt(0) === '/') {
118
- setIsAltUrlCorrect(true);
119
- } else {
120
- setIsAltUrlCorrect(false);
121
- }
161
+ const updateResults = useCallback(() => {
162
+ const options = {
163
+ query: filterQuery,
164
+ manual: filterType.value,
165
+ batchStart: (activePage - 1) * itemsPerPage,
166
+ batchSize: itemsPerPage === 'All' ? 999999999999 : itemsPerPage,
167
+ };
168
+ if (hasAdvancedFiltering) {
169
+ options.start = createdAfter || '';
170
+ options.end = createdBefore || '';
171
+ } else {
172
+ options.datetime = createdBefore || '';
122
173
  }
174
+ dispatch(getAliases(getBaseUrl(pathname), options));
123
175
  }, [
124
- itemsPerPage,
125
- pathname,
126
- prevaliasesitemstotal,
127
- aliases.items_total,
128
- previtemsPerPage,
129
- prevactivePage,
130
176
  activePage,
131
- prevalturlpath,
132
- altUrlPath,
133
- prevtargetUrlPath,
134
- targetUrlPath,
135
- dispatch,
136
- filterQuery,
137
- filterType.value,
177
+ createdAfter,
138
178
  createdBefore,
139
- ]);
140
-
141
- useEffect(() => {
142
- if (prevaliasesaddloading && !aliases.add.loaded) {
143
- if (aliases.add.error) {
144
- setErrorMessageAdd(aliases.add.error.response.body.message);
145
- }
146
- }
147
- if (prevaliasesaddloading && aliases.add.loaded) {
148
- dispatch(
149
- getAliases(getBaseUrl(pathname), {
150
- query: filterQuery,
151
- manual: filterType.value,
152
- datetime: createdBefore,
153
- batchSize: itemsPerPage,
154
- }),
155
- );
156
- toast.success(
157
- <Toast
158
- success
159
- title={intl.formatMessage(messages.success)}
160
- content={intl.formatMessage(messages.successAdd)}
161
- />,
162
- );
163
- if (!aliases.add.error) {
164
- setErrorMessageAdd('');
165
- }
166
- }
167
- if (prevaliasesremoveloading && aliases.remove.loaded) {
168
- dispatch(
169
- getAliases(getBaseUrl(pathname), {
170
- query: filterQuery,
171
- manual: filterType.value,
172
- datetime: createdBefore,
173
- batchSize: itemsPerPage,
174
- }),
175
- );
176
- }
177
- }, [
178
- prevaliasesaddloading,
179
- aliases.add.loaded,
180
- aliases.add.error,
181
- aliases.remove.loaded,
182
- prevaliasesremoveloading,
183
179
  dispatch,
184
- pathname,
185
180
  filterQuery,
186
181
  filterType.value,
187
- createdBefore,
182
+ hasAdvancedFiltering,
188
183
  itemsPerPage,
189
- intl,
184
+ pathname,
190
185
  ]);
191
186
 
192
- const onCancel = () => {
193
- history.push(getParentUrl(pathname));
194
- };
195
-
196
- const handleSelectFilterType = (type) => {
197
- setFilterType(type);
198
- };
187
+ // Update results after changing the page.
188
+ // (We intentionally leave updateResults out of the deps.)
189
+ // eslint-disable-next-line react-hooks/exhaustive-deps
190
+ useEffect(() => updateResults(), [activePage, itemsPerPage]);
199
191
 
200
- const handleFilterQueryChange = (query) => {
201
- setFilterQuery(query);
202
- };
203
-
204
- const handleCreateDate = (date) => {
205
- setCreatedBefore(date);
206
- };
207
-
208
- const handleSubmitFilter = () => {
209
- dispatch(
210
- getAliases(getBaseUrl(pathname), {
211
- query: filterQuery,
212
- manual: filterType.value,
213
- datetime: createdBefore,
214
- batchSize: itemsPerPage,
215
- }),
216
- );
217
- };
218
-
219
- const handleAltUrlChange = (url) => {
220
- setAltUrlPath(url);
221
- };
222
-
223
- const handleTargetUrlChange = (url) => {
224
- setTargetUrlPath(url);
225
- };
192
+ // Calculate page count from results
193
+ const pages = useMemo(() => {
194
+ let pages = Math.ceil(aliases.items_total / itemsPerPage);
195
+ if (pages === 0 || isNaN(pages)) {
196
+ pages = '';
197
+ }
198
+ return pages;
199
+ }, [aliases.items_total, itemsPerPage]);
226
200
 
227
- const handleSubmitAlias = useCallback(() => {
228
- if (isAltUrlCorrect) {
201
+ // Add new alias
202
+ const handleAdd = (formData) => {
203
+ const { altUrlPath, targetUrlPath } = formData;
204
+ // Validate altUrlPath starts with a slash
205
+ if (!altUrlPath || altUrlPath.charAt(0) !== '/') {
206
+ setAddError(intl.formatMessage(messages.altUrlError));
207
+ return;
208
+ }
209
+ // Remove existing alias first if we're editing it.
210
+ const precondition = editingData
211
+ ? dispatch(
212
+ removeAliases('', { items: [{ path: editingData.altUrlPath }] }),
213
+ )
214
+ : Promise.resolve();
215
+ precondition.then(() => {
229
216
  dispatch(
230
217
  addAliases('', {
231
- items: [
232
- {
233
- path: altUrlPath,
234
- 'redirect-to': targetUrlPath,
235
- },
236
- ],
218
+ items: [{ path: altUrlPath, 'redirect-to': targetUrlPath }],
237
219
  }),
238
- );
239
- setAltUrlPath('');
240
- setTargetUrlPath('');
241
- }
242
- }, [isAltUrlCorrect, altUrlPath, targetUrlPath, dispatch]);
220
+ )
221
+ .then(() => {
222
+ updateResults();
223
+ setAddModalOpen(false);
224
+ setEditingData(null);
225
+ toast.success(
226
+ <Toast
227
+ success
228
+ title={intl.formatMessage(messages.success)}
229
+ content={intl.formatMessage(messages.successAdd)}
230
+ />,
231
+ );
232
+ })
233
+ .catch((error) => {
234
+ setAddError(error.response?.body?.message);
235
+ });
236
+ });
237
+ };
243
238
 
239
+ // Check/uncheck an alias
244
240
  const handleCheckAlias = (alias) => {
245
- const aliasess = [...aliasesToRemove];
246
- if (aliasess.includes(alias)) {
247
- const index = aliasess.indexOf(alias);
248
- if (index > -1) {
249
- let newAliasesArr = aliasess;
250
- newAliasesArr.splice(index, 1);
251
- setAliasesToRemove(newAliasesArr);
252
- }
241
+ if (aliasesToRemove.includes(alias)) {
242
+ setAliasesToRemove(aliasesToRemove.filter((x) => x !== alias));
253
243
  } else {
254
244
  setAliasesToRemove([...aliasesToRemove, alias]);
255
245
  }
256
246
  };
257
- const handleRemoveAliases = () => {
258
- const items = aliasesToRemove.map((a) => {
259
- return {
260
- path: a,
261
- };
262
- });
263
247
 
248
+ // Remove selected aliases
249
+ const handleRemoveAliases = () => {
264
250
  dispatch(
265
251
  removeAliases('', {
266
- items,
252
+ items: aliasesToRemove.map((a) => ({ path: a })),
267
253
  }),
268
- );
254
+ ).then(() => {
255
+ updateResults();
256
+ toast.success(
257
+ <Toast
258
+ success
259
+ title={intl.formatMessage(messages.success)}
260
+ content={intl.formatMessage(messages.successRemove)}
261
+ />,
262
+ );
263
+ });
269
264
  setAliasesToRemove([]);
270
265
  };
271
266
 
272
- const handlePageChange = (e, { activePage }) => {
273
- setActivePage(activePage);
274
- };
275
-
276
- const handleItemsPerPage = (e, { value }) => {
277
- setItemsPerPage(value);
278
- setActivePage(1);
267
+ // Upload CSV
268
+ const handleBulkUpload = (formData) => {
269
+ fetch(`data:${formData.file['content-type']};base64,${formData.file.data}`)
270
+ .then((res) => res.blob())
271
+ .then((blob) => {
272
+ dispatch(uploadAliases(blob))
273
+ .then(() => {
274
+ updateResults();
275
+ setUploadError(null);
276
+ setUploadModalOpen(false);
277
+ toast.success(
278
+ <Toast
279
+ success
280
+ title={intl.formatMessage(messages.success)}
281
+ content={intl.formatMessage(messages.successUpload)}
282
+ />,
283
+ );
284
+ })
285
+ .catch((error) => {
286
+ setUploadError(error.response?.body?.message);
287
+ });
288
+ });
279
289
  };
280
290
 
281
291
  return (
@@ -291,246 +301,319 @@ const Aliases = (props) => {
291
301
  values={{ title: <q>{title}</q> }}
292
302
  />
293
303
  </Segment>
294
- <Form>
295
- <Segment>
296
- <Header size="medium">
297
- <FormattedMessage
298
- id="Alternative url path (Required)"
299
- defaultMessage="Alternative url path (Required)"
300
- />
301
- </Header>
302
- <p className="help">
303
- <FormattedMessage
304
- id="Enter the absolute path where the alternative url should exist. The path must start with '/'. Only urls that result in a 404 not found page will result in a redirect occurring."
305
- defaultMessage="Enter the absolute path where the alternative url should exist. The path must start with '/'. Only urls that result in a 404 not found page will result in a redirect occurring."
306
- />
307
- </p>
308
- <Form.Field>
309
- <Input
310
- id="alternative-url-input"
311
- name="alternative-url-path"
312
- placeholder="/example"
313
- value={altUrlPath}
314
- onChange={(e) => handleAltUrlChange(e.target.value)}
315
- />
316
- {!isAltUrlCorrect && altUrlPath !== '' && (
317
- <p style={{ color: 'red' }}>
318
- <FormattedMessage
319
- id="Alternative url path must start with a slash."
320
- defaultMessage="Alternative url path must start with a slash."
321
- />
322
- </p>
323
- )}
324
- </Form.Field>
325
- <Header size="medium">
326
- <FormattedMessage
327
- id="Target Path (Required)"
328
- defaultMessage="Target Path (Required)"
329
- />
330
- </Header>
331
- <p className="help">
332
- <FormattedMessage
333
- id="Enter the absolute path of the target. Target must exist or be an existing alternative url path to the target."
334
- defaultMessage="Enter the absolute path of the target. Target must exist or be an existing alternative url path to the target."
335
- />
336
- </p>
337
- <Form.Field>
338
- <Input
339
- id="target-url-input"
340
- name="target-url-path"
341
- placeholder="/example"
342
- value={targetUrlPath}
343
- onChange={(e) => handleTargetUrlChange(e.target.value)}
344
- />
345
- </Form.Field>
346
- <Button
347
- id="submit-alias"
348
- primary
349
- onClick={() => handleSubmitAlias()}
350
- disabled={
351
- !isAltUrlCorrect ||
352
- altUrlPath === '' ||
353
- targetUrlPath === ''
304
+ <Segment>
305
+ <Button
306
+ primary
307
+ id="add-alt-url"
308
+ onClick={() => setAddModalOpen(true)}
309
+ >
310
+ {intl.formatMessage(messages.AddUrl)}&hellip;
311
+ </Button>
312
+ {addModalOpen && (
313
+ <ModalForm
314
+ open={true}
315
+ onSubmit={handleAdd}
316
+ onCancel={() => setAddModalOpen(false)}
317
+ title={
318
+ editingData
319
+ ? intl.formatMessage(messages.EditUrl)
320
+ : intl.formatMessage(messages.AddUrl)
354
321
  }
355
- >
356
- <FormattedMessage id="Add" defaultMessage="Add" />
357
- </Button>
358
- {errorMessageAdd && (
359
- <Message color="red">
360
- <Message.Header>
361
- <FormattedMessage
362
- id="ErrorHeader"
363
- defaultMessage="Error"
364
- />
365
- </Message.Header>
366
- <p>{errorMessageAdd}</p>
367
- </Message>
368
- )}
369
- </Segment>
370
- </Form>
371
- <Form>
372
- <Segment className="primary">
322
+ submitError={addError}
323
+ schema={{
324
+ fieldsets: [
325
+ {
326
+ id: 'default',
327
+ fields: ['altUrlPath', 'targetUrlPath'],
328
+ },
329
+ ],
330
+ properties: {
331
+ altUrlPath: {
332
+ title: intl.formatMessage(messages.altUrlPathTitle),
333
+ description: (
334
+ <FormattedMessage
335
+ id="Enter the absolute path where the alternative url should exist. The path must start with '/'. Only URLs that result in a 404 not found page will result in a redirect occurring."
336
+ defaultMessage="Enter the absolute path where the alternative URL should exist. The path must start with '/'. Only URLs that result in a 404 not found page will result in a redirect occurring."
337
+ />
338
+ ),
339
+ placeholder: intl.formatMessage(messages.examplePath),
340
+ },
341
+ targetUrlPath: {
342
+ title: intl.formatMessage(messages.targetUrlPathTitle),
343
+ description: (
344
+ <FormattedMessage
345
+ id="Enter the absolute path of the target. Target must exist or be an existing alternative url path to the target."
346
+ defaultMessage="Enter the absolute path of the target. Target must exist or be an existing alternative URL path to the target."
347
+ />
348
+ ),
349
+ placeholder: intl.formatMessage(messages.examplePath),
350
+ },
351
+ },
352
+ required: ['altUrlPath', 'targetUrlPath'],
353
+ }}
354
+ formData={editingData || {}}
355
+ />
356
+ )}
357
+ {hasBulkUpload && (
358
+ <>
359
+ <Button onClick={() => setUploadModalOpen(true)}>
360
+ {intl.formatMessage(messages.BulkUploadAltUrls)}&hellip;
361
+ </Button>
362
+ {uploadModalOpen && (
363
+ <ModalForm
364
+ open={true}
365
+ onSubmit={handleBulkUpload}
366
+ onCancel={() => setUploadModalOpen(false)}
367
+ title={intl.formatMessage(messages.BulkUploadAltUrls)}
368
+ submitError={uploadError}
369
+ description={
370
+ <>
371
+ <p>
372
+ <FormattedMessage
373
+ id="bulkUploadUrlsHelp"
374
+ defaultMessage="Add many alternative URLs at once by uploading a CSV file. The first column should be the path to redirect from; the second, the path to redirect to. Both paths must be Plone-site-relative, starting with a slash (/). An optional third column can contain a date and time. An optional fourth column can contain a boolean to mark as a manual redirect (default true)."
375
+ />
376
+ </p>
377
+ <p>
378
+ <FormattedMessage
379
+ id="Example"
380
+ defaultMessage="Example"
381
+ />
382
+ :
383
+ <br />
384
+ <code>
385
+ /old-home-page.asp,/front-page,2019/01/27 10:42:59
386
+ GMT+1,true
387
+ <br />
388
+ /people/JoeT,/Users/joe-thurston,2018-12-31,false
389
+ </code>
390
+ </p>
391
+ </>
392
+ }
393
+ schema={{
394
+ fieldsets: [
395
+ {
396
+ id: 'default',
397
+ fields: ['file'],
398
+ },
399
+ ],
400
+ properties: {
401
+ file: {
402
+ title: intl.formatMessage(messages.CSVFile),
403
+ type: 'object',
404
+ factory: 'File Upload',
405
+ },
406
+ },
407
+ required: ['file'],
408
+ }}
409
+ />
410
+ )}
411
+ </>
412
+ )}
413
+ </Segment>
414
+ <Segment>
415
+ <Form>
373
416
  <Header size="medium">
374
417
  <FormattedMessage
375
418
  id="All existing alternative urls for this site"
376
- defaultMessage="All existing alternative urls for this site"
419
+ defaultMessage="Existing alternative URLs for this site"
377
420
  />
378
421
  </Header>
379
- <Header size="small">
380
- <FormattedMessage
381
- id="Filter by prefix"
382
- defaultMessage="Filter by prefix"
383
- />
384
- </Header>
385
- <Form.Field>
386
- <Input
387
- name="filter"
388
- placeholder="/example"
389
- value={filterQuery}
390
- onChange={(e) => handleFilterQueryChange(e.target.value)}
391
- />
392
- </Form.Field>
393
- <Header size="small">
394
- <FormattedMessage
395
- id="Manually or automatically added?"
396
- defaultMessage="Manually or automatically added?"
397
- />
398
- </Header>
399
- {filterChoices.map((o, i) => (
400
- <Form.Field key={i}>
401
- <Radio
402
- label={o.label}
403
- name="radioGroup"
404
- value={o.value}
405
- checked={filterType === o}
406
- onChange={() => handleSelectFilterType(o)}
422
+ <Segment>
423
+ <Form.Field>
424
+ <FormFieldWrapper
425
+ id="filterQuery"
426
+ title={intl.formatMessage(messages.filterByPrefix)}
427
+ >
428
+ <Input
429
+ name="filter"
430
+ placeholder={intl.formatMessage(messages.examplePath)}
431
+ value={filterQuery}
432
+ onChange={(e) => setFilterQuery(e.target.value)}
433
+ />
434
+ </FormFieldWrapper>
435
+ </Form.Field>
436
+ <Form.Field>
437
+ <FormFieldWrapper
438
+ id="filterType"
439
+ title={intl.formatMessage(messages.manualOrAuto)}
440
+ >
441
+ <Form.Group inline>
442
+ {filterChoices.map((o, i) => (
443
+ <Form.Field key={i}>
444
+ <Radio
445
+ label={intl.formatMessage({ id: o.label })}
446
+ name="radioGroup"
447
+ value={o.value}
448
+ checked={filterType === o}
449
+ onChange={() => setFilterType(o)}
450
+ />
451
+ </Form.Field>
452
+ ))}
453
+ </Form.Group>
454
+ </FormFieldWrapper>
455
+ </Form.Field>
456
+ <Form.Field>
457
+ <DatetimeWidget
458
+ id="created-before-date"
459
+ title={intl.formatMessage(messages.createdBefore)}
460
+ dateOnly={true}
461
+ value={createdBefore}
462
+ onChange={(id, value) => {
463
+ setCreatedBefore(value);
464
+ }}
407
465
  />
408
466
  </Form.Field>
409
- ))}
410
- <Form.Field>
411
- <DatetimeWidget
412
- id="created-before-date"
413
- title={'Created before'}
414
- dateOnly={true}
415
- value={createdBefore}
416
- onChange={(id, value) => {
417
- handleCreateDate(value);
418
- }}
419
- />
420
- </Form.Field>
421
- <Button onClick={() => handleSubmitFilter()} primary>
422
- Filter
423
- </Button>
424
- <Header size="small">
425
- <FormattedMessage
426
- id="Alternative url path → target url path (date and time of creation, manually created yes/no)"
427
- defaultMessage="Alternative url path → target url path (date and time of creation, manually created yes/no)"
428
- />
429
- </Header>
467
+ {hasAdvancedFiltering && (
468
+ <Form.Field>
469
+ <DatetimeWidget
470
+ id="created-after-date"
471
+ title={intl.formatMessage(messages.createdAfter)}
472
+ dateOnly={true}
473
+ value={createdAfter}
474
+ onChange={(id, value) => {
475
+ setCreatedAfter(value);
476
+ }}
477
+ />
478
+ </Form.Field>
479
+ )}
480
+ <Button onClick={() => updateResults()} primary>
481
+ <FormattedMessage id="Filter" defaultMessage="Filter" />
482
+ </Button>
483
+ </Segment>
484
+ </Form>
485
+ </Segment>
486
+ <Segment>
487
+ <Header size="small">
488
+ <FormattedMessage
489
+ id="Alternative url path → target url path (date and time of creation, manually created yes/no)"
490
+ defaultMessage="Alternative URL path → target URL path (date and time of creation, manually created yes/no)"
491
+ />
492
+ </Header>
430
493
 
431
- <Table>
432
- <Table.Body>
494
+ <Table celled compact>
495
+ <Table.Header>
496
+ <Table.Row>
497
+ <Table.HeaderCell width="1">
498
+ <FormattedMessage id="Select" defaultMessage="Select" />
499
+ </Table.HeaderCell>
500
+ <Table.HeaderCell width="10">
501
+ <FormattedMessage id="Alias" defaultMessage="Alias" />
502
+ </Table.HeaderCell>
503
+ <Table.HeaderCell width="1">
504
+ <FormattedMessage id="Date" defaultMessage="Date" />
505
+ </Table.HeaderCell>
506
+ <Table.HeaderCell width="1">
507
+ <FormattedMessage id="Manual" defaultMessage="Manual" />
508
+ </Table.HeaderCell>
509
+ </Table.Row>
510
+ </Table.Header>
511
+ <Table.Body>
512
+ {aliases.get.loading && (
433
513
  <Table.Row>
434
- <Table.HeaderCell>
435
- <FormattedMessage id="Select" defaultMessage="Select" />
436
- </Table.HeaderCell>
437
- <Table.HeaderCell>
438
- <FormattedMessage id="Alias" defaultMessage="Alias" />
439
- </Table.HeaderCell>
440
- <Table.HeaderCell>
441
- <FormattedMessage id="Target" defaultMessage="Target" />
442
- </Table.HeaderCell>
443
- <Table.HeaderCell>
444
- <FormattedMessage id="Date" defaultMessage="Date" />
445
- </Table.HeaderCell>
446
- <Table.HeaderCell>
447
- <FormattedMessage id="Manual" defaultMessage="Manual" />
448
- </Table.HeaderCell>
514
+ <Table.Cell colSpan="4">
515
+ <Loader active inline="centered" />
516
+ </Table.Cell>
449
517
  </Table.Row>
450
- {aliases.items.length > 0 &&
451
- aliases.items.map((alias, i) => (
452
- <Table.Row key={i}>
453
- <Table.Cell>
454
- <Checkbox
455
- onChange={(e, { value }) =>
456
- handleCheckAlias(value)
457
- }
458
- checked={aliasesToRemove.includes(alias.path)}
459
- value={alias.path}
460
- />
461
- </Table.Cell>
462
- <Table.Cell>
463
- <p>{alias.path}</p>
464
- </Table.Cell>
465
- <Table.Cell>
466
- <p>{alias['redirect-to']}</p>
467
- </Table.Cell>
468
- <Table.Cell>
469
- <p>{alias.datetime}</p>
470
- </Table.Cell>
471
- <Table.Cell>
472
- <p>{`${alias.manual}`}</p>
473
- </Table.Cell>
474
- </Table.Row>
475
- ))}
476
- </Table.Body>
477
- </Table>
478
- <div
479
- style={{
480
- display: 'flex',
481
- flexWrap: 'wrap',
482
- alignItems: 'center',
483
- marginBottom: '20px',
484
- }}
485
- >
486
- {pages && (
487
- <Pagination
488
- boundaryRange={0}
489
- activePage={activePage}
490
- ellipsisItem={null}
491
- firstItem={null}
492
- lastItem={null}
493
- siblingRange={1}
494
- totalPages={pages}
495
- onPageChange={(e, o) => handlePageChange(e, o)}
496
- />
497
518
  )}
498
- <Menu.Menu
499
- position="right"
500
- style={{ display: 'flex', marginLeft: 'auto' }}
501
- >
502
- <Menu.Item style={{ color: 'grey' }}>
503
- <FormattedMessage id="Show" defaultMessage="Show" />:
504
- </Menu.Item>
505
- {map(itemsPerPageChoices, (size) => (
506
- <Menu.Item
507
- style={{
508
- padding: '0 0.4em',
509
- margin: '0em 0.357em',
510
- cursor: 'pointer',
511
- }}
512
- key={size}
513
- value={size}
514
- active={size === itemsPerPage}
515
- onClick={(e, o) => handleItemsPerPage(e, o)}
516
- >
517
- {size}
518
- </Menu.Item>
519
+ {aliases.items.length > 0 &&
520
+ aliases.items.map((alias, i) => (
521
+ <Table.Row key={i} verticalAlign="top">
522
+ <Table.Cell>
523
+ <Checkbox
524
+ onChange={(e, { value }) => handleCheckAlias(value)}
525
+ checked={aliasesToRemove.includes(alias.path)}
526
+ value={alias.path}
527
+ />
528
+ </Table.Cell>
529
+ <Table.Cell>
530
+ {alias.path}
531
+ <br />
532
+ &nbsp;&nbsp;&rarr; {alias['redirect-to']}{' '}
533
+ <Button
534
+ basic
535
+ style={{ verticalAlign: 'middle' }}
536
+ aria-label={intl.formatMessage(messages.EditUrl)}
537
+ onClick={() => {
538
+ setEditingData({
539
+ altUrlPath: alias.path,
540
+ targetUrlPath: alias['redirect-to'],
541
+ });
542
+ setAddModalOpen(true);
543
+ }}
544
+ >
545
+ <Icon name={editingSVG} size="18px" />
546
+ </Button>
547
+ </Table.Cell>
548
+ <Table.Cell>
549
+ <FormattedDate date={alias.datetime} />
550
+ </Table.Cell>
551
+ <Table.Cell>{`${alias.manual}`}</Table.Cell>
552
+ </Table.Row>
519
553
  ))}
520
- </Menu.Menu>
521
- </div>
522
- <Button
523
- disabled={aliasesToRemove.length === 0}
524
- onClick={handleRemoveAliases}
525
- primary
526
- >
527
- <FormattedMessage
528
- id="Remove selected"
529
- defaultMessage="Remove selected"
554
+ </Table.Body>
555
+ </Table>
556
+ <div
557
+ style={{
558
+ display: 'flex',
559
+ flexWrap: 'wrap',
560
+ alignItems: 'center',
561
+ marginBottom: '20px',
562
+ }}
563
+ >
564
+ {pages && (
565
+ <Pagination
566
+ boundaryRange={0}
567
+ activePage={activePage}
568
+ ellipsisItem={null}
569
+ firstItem={null}
570
+ lastItem={null}
571
+ siblingRange={1}
572
+ totalPages={pages}
573
+ onPageChange={(e, { activePage }) =>
574
+ setActivePage(activePage)
575
+ }
530
576
  />
531
- </Button>
532
- </Segment>
533
- </Form>
577
+ )}
578
+ <Menu.Menu
579
+ position="right"
580
+ style={{ display: 'flex', marginLeft: 'auto' }}
581
+ >
582
+ <Menu.Item style={{ color: 'grey' }}>
583
+ <FormattedMessage id="Show" defaultMessage="Show" />:
584
+ </Menu.Item>
585
+ {map(itemsPerPageChoices, (size) => (
586
+ <Menu.Item
587
+ style={{
588
+ padding: '0 0.4em',
589
+ margin: '0em 0.357em',
590
+ cursor: 'pointer',
591
+ }}
592
+ key={size}
593
+ value={size}
594
+ active={size === itemsPerPage}
595
+ onClick={(e, { value }) => {
596
+ setItemsPerPage(value);
597
+ setActivePage(1);
598
+ }}
599
+ >
600
+ {size}
601
+ </Menu.Item>
602
+ ))}
603
+ </Menu.Menu>
604
+ </div>
605
+ <Button
606
+ id="remove-alt-urls"
607
+ disabled={aliasesToRemove.length === 0}
608
+ onClick={handleRemoveAliases}
609
+ primary
610
+ >
611
+ <FormattedMessage
612
+ id="Remove selected"
613
+ defaultMessage="Remove selected"
614
+ />
615
+ </Button>
616
+ </Segment>
534
617
  </Segment.Group>
535
618
  </article>
536
619
  </Container>
@@ -540,7 +623,11 @@ const Aliases = (props) => {
540
623
  pathname={pathname}
541
624
  hideDefaultViewButtons
542
625
  inner={
543
- <Link className="item" to="#" onClick={() => onCancel()}>
626
+ <Link
627
+ className="item"
628
+ to="#"
629
+ onClick={() => history.push(getParentUrl(pathname))}
630
+ >
544
631
  <Icon
545
632
  name={backSVG}
546
633
  className="contents circled"