@plone/volto 19.0.0-alpha.1 → 19.0.0-alpha.2

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 (43) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/locales/ca/LC_MESSAGES/volto.po +15 -0
  3. package/locales/ca.json +1 -1
  4. package/locales/de/LC_MESSAGES/volto.po +15 -0
  5. package/locales/de.json +1 -1
  6. package/locales/en/LC_MESSAGES/volto.po +15 -0
  7. package/locales/en.json +1 -1
  8. package/locales/es/LC_MESSAGES/volto.po +15 -0
  9. package/locales/es.json +1 -1
  10. package/locales/eu/LC_MESSAGES/volto.po +15 -0
  11. package/locales/eu.json +1 -1
  12. package/locales/fi/LC_MESSAGES/volto.po +15 -0
  13. package/locales/fi.json +1 -1
  14. package/locales/fr/LC_MESSAGES/volto.po +15 -0
  15. package/locales/fr.json +1 -1
  16. package/locales/hi/LC_MESSAGES/volto.po +15 -0
  17. package/locales/hi.json +1 -1
  18. package/locales/it/LC_MESSAGES/volto.po +15 -0
  19. package/locales/it.json +1 -1
  20. package/locales/ja/LC_MESSAGES/volto.po +15 -0
  21. package/locales/ja.json +1 -1
  22. package/locales/nl/LC_MESSAGES/volto.po +15 -0
  23. package/locales/nl.json +1 -1
  24. package/locales/pt/LC_MESSAGES/volto.po +15 -0
  25. package/locales/pt.json +1 -1
  26. package/locales/pt_BR/LC_MESSAGES/volto.po +15 -0
  27. package/locales/pt_BR.json +1 -1
  28. package/locales/ro/LC_MESSAGES/volto.po +15 -0
  29. package/locales/ro.json +1 -1
  30. package/locales/ru/LC_MESSAGES/volto.po +15 -0
  31. package/locales/ru.json +1 -1
  32. package/locales/volto.pot +15 -0
  33. package/locales/zh_CN/LC_MESSAGES/volto.po +15 -0
  34. package/locales/zh_CN.json +1 -1
  35. package/package.json +5 -5
  36. package/src/components/manage/Blocks/Title/Edit.jsx +8 -2
  37. package/src/components/manage/Form/Form.jsx +32 -0
  38. package/src/components/manage/Form/Form.test.jsx +22 -18
  39. package/src/components/manage/Widgets/SelectWidget.jsx +3 -1
  40. package/src/helpers/Utils/withSaveAsDraft.jsx +241 -0
  41. package/theme/themes/pastanaga/collections/table.overrides +9 -0
  42. package/theme/themes/pastanaga/extras/main.less +15 -0
  43. package/types/helpers/Utils/withSaveAsDraft.d.ts +1 -0
@@ -0,0 +1,241 @@
1
+ import React from 'react';
2
+ import hoistNonReactStatics from 'hoist-non-react-statics';
3
+ import isEqual from 'react-fast-compare';
4
+ import { toast } from 'react-toastify';
5
+ import Icon from '@plone/volto/components/theme/Icon/Icon';
6
+ import Toast from '@plone/volto/components/manage/Toast/Toast';
7
+ import { Button } from 'semantic-ui-react';
8
+ import checkSVG from '@plone/volto/icons/check.svg';
9
+ import clearSVG from '@plone/volto/icons/clear.svg';
10
+ import { useIntl, defineMessages } from 'react-intl';
11
+ import { useLocation } from 'react-router-dom';
12
+
13
+ const messages = defineMessages({
14
+ autoSaveFound: {
15
+ id: 'Autosaved content found',
16
+ defaultMessage: 'Autosaved content found',
17
+ },
18
+ loadData: {
19
+ id: 'Do you want to restore your autosaved content?',
20
+ defaultMessage: 'Do you want to restore your autosaved content?',
21
+ },
22
+ loadExpiredData: {
23
+ id: "Another person edited this content, and it's currently displayed. Do you want to replace it with your autosaved content?",
24
+ defaultMessage:
25
+ "Another person edited this content, and it's currently displayed. Do you want to replace it with your autosaved content?",
26
+ },
27
+ });
28
+
29
+ function getDisplayName(WrappedComponent) {
30
+ return WrappedComponent.displayName || WrappedComponent.name || 'Component';
31
+ }
32
+
33
+ const mapSchemaToData = (schema, data) => {
34
+ if (!data) return {};
35
+ const dataKeys = Object.keys(data);
36
+ return Object.assign(
37
+ {},
38
+ ...Object.keys(schema.properties)
39
+ .filter((k) => dataKeys.includes(k))
40
+ .map((k) => ({ [k]: data[k] })),
41
+ );
42
+ };
43
+
44
+ // will be used to avoid using the first mount call if there is a second call
45
+ let mountTime;
46
+
47
+ const getFormId = (props, location) => {
48
+ const { type, pathname = location.pathname, isEditForm, schema } = props;
49
+ const id = isEditForm
50
+ ? ['form', type, pathname].join('-')
51
+ : type
52
+ ? ['form', pathname, type].join('-')
53
+ : schema?.properties?.comment
54
+ ? ['form', pathname, 'comment'].join('-')
55
+ : ['form', pathname].join('-');
56
+
57
+ return id;
58
+ };
59
+
60
+ /**
61
+ * Toast content that has OK and Cancel buttons
62
+ * @param {function} onUpdate
63
+ * @param {function} onClose
64
+ * @param {string} userMessage
65
+ * @returns
66
+ */
67
+ const ConfirmAutoSave = ({ onUpdate, onClose, userMessage }) => {
68
+ const handleClickOK = () => onUpdate();
69
+ const handleClickCancel = () => onClose();
70
+
71
+ return (
72
+ <div className="toast-box-center">
73
+ <div>{userMessage}</div>
74
+ <Button
75
+ icon
76
+ aria-label="Unchecked"
77
+ className="save toast-box"
78
+ onClick={handleClickOK}
79
+ >
80
+ <Icon
81
+ name={checkSVG}
82
+ size="24px"
83
+ className="circled toast-box-blue-icon"
84
+ />
85
+ </Button>
86
+ <Button
87
+ icon
88
+ aria-label="Unchecked"
89
+ className="save toast-box"
90
+ onClick={handleClickCancel}
91
+ >
92
+ <Icon
93
+ name={clearSVG}
94
+ size="24px"
95
+ className="circled toast-box-blue-icon"
96
+ />
97
+ </Button>
98
+ </div>
99
+ );
100
+ };
101
+
102
+ /**
103
+ * Will remove localStorage item using debounce
104
+ * @param {string} id
105
+ * @param {number} timerForDeletion
106
+ */
107
+ const clearStorage = (id, timerForDeletion) => {
108
+ timerForDeletion.current && clearTimeout(timerForDeletion.current);
109
+ timerForDeletion.current = setTimeout(() => {
110
+ localStorage.removeItem(id);
111
+ }, 500);
112
+ };
113
+
114
+ /**
115
+ * Stale if server date is more recent
116
+ * @param {string} serverModifiedDate
117
+ * @param {string} autoSaveDate
118
+ * @returns {Boolean}
119
+ */
120
+ const autoSaveFoundIsStale = (serverModifiedDate, autoSaveDate) => {
121
+ const result = !serverModifiedDate
122
+ ? false
123
+ : new Date(serverModifiedDate) > new Date(autoSaveDate);
124
+ return result;
125
+ };
126
+
127
+ const draftApi = (id, schema, timer, timerForDeletion, intl) => ({
128
+ // - since Add Content Type will call componentDidMount twice, we will
129
+ // use the second call (using debounce)- the first will ignore any setState comands;
130
+ // - Delete local data only if user confirms Cancel
131
+ // - Will tell user that it has local stored data, even if its less recent than the server data
132
+ checkSavedDraft(state, updateCallback) {
133
+ if (!schema) return;
134
+ const saved = localStorage.getItem(id);
135
+
136
+ if (saved && Object.keys(JSON.parse(saved)).length > 1) {
137
+ const formData = mapSchemaToData(schema, state);
138
+ // includes autoSaveDate
139
+ const foundSavedData = JSON.parse(saved);
140
+ // includes only form data found in schema (no autoSaveDate)
141
+ const foundSavedSchemaData = mapSchemaToData(schema, foundSavedData);
142
+
143
+ if (!isEqual(formData, foundSavedSchemaData)) {
144
+ // eslint-disable-next-line no-alert
145
+ // cancel existing setTimeout to avoid using first call if
146
+ // successive calls are made
147
+ mountTime && clearTimeout(mountTime);
148
+ mountTime = setTimeout(() => {
149
+ toast.info(
150
+ <Toast
151
+ position="top-right"
152
+ info
153
+ autoClose={false}
154
+ title={intl.formatMessage(messages.autoSaveFound)}
155
+ content={
156
+ <ConfirmAutoSave
157
+ onUpdate={() => updateCallback(foundSavedSchemaData)}
158
+ onClose={() => clearStorage(id, timerForDeletion)}
159
+ userMessage={
160
+ autoSaveFoundIsStale(
161
+ state.modified,
162
+ foundSavedData.autoSaveDate,
163
+ )
164
+ ? intl.formatMessage(messages.loadExpiredData)
165
+ : intl.formatMessage(messages.loadData)
166
+ }
167
+ />
168
+ }
169
+ />,
170
+ );
171
+ }, 300);
172
+ }
173
+ }
174
+ },
175
+ // use debounce mode
176
+ onSaveDraft(state) {
177
+ if (!schema) return;
178
+ timer.current && clearTimeout(timer.current);
179
+ timer.current = setTimeout(() => {
180
+ const formData = mapSchemaToData(schema, state);
181
+ const saved = localStorage.getItem(id);
182
+ const newData = JSON.parse(saved);
183
+
184
+ localStorage.setItem(
185
+ id,
186
+ JSON.stringify({
187
+ ...newData,
188
+ ...formData,
189
+ autoSaveDate: new Date(),
190
+ }),
191
+ );
192
+ }, 300);
193
+ },
194
+
195
+ onCancelDraft() {
196
+ if (!schema) return;
197
+ clearStorage(id, timerForDeletion);
198
+ },
199
+ });
200
+
201
+ export default function withSaveAsDraft(options) {
202
+ const { forwardRef } = options;
203
+
204
+ return (WrappedComponent) => {
205
+ function WithSaveAsDraft(props) {
206
+ const { schema } = props;
207
+ const intl = useIntl();
208
+ const location = useLocation();
209
+ const id = getFormId(props, location);
210
+ const timmeRef = React.useRef();
211
+ const timmerForDeletionRef = React.useRef();
212
+ const api = React.useMemo(
213
+ () => draftApi(id, schema, timmeRef, timmerForDeletionRef, intl),
214
+ [id, schema, timmeRef, timmerForDeletionRef, intl],
215
+ );
216
+
217
+ return (
218
+ <WrappedComponent
219
+ {...props}
220
+ {...api}
221
+ ref={forwardRef ? props.forwardedRef : null}
222
+ />
223
+ );
224
+ }
225
+
226
+ WithSaveAsDraft.displayName = `WithSaveAsDraft(${getDisplayName(
227
+ WrappedComponent,
228
+ )})`;
229
+
230
+ if (forwardRef) {
231
+ return hoistNonReactStatics(
232
+ React.forwardRef((props, ref) => (
233
+ <WithSaveAsDraft {...props} forwardedRef={ref} />
234
+ )),
235
+ WrappedComponent,
236
+ );
237
+ }
238
+
239
+ return hoistNonReactStatics(WithSaveAsDraft, WrappedComponent);
240
+ };
241
+ }
@@ -19,6 +19,15 @@
19
19
  vertical-align: @headerVerticalAlign;
20
20
  }
21
21
 
22
+ // use sorting icons from icons.woff instead of assuming it's font awesome
23
+ .ui.sortable.table thead th.ascending::after {
24
+ content: '\E9EC';
25
+ }
26
+
27
+ .ui.sortable.table thead th.descending::after {
28
+ content: '\E9EB';
29
+ }
30
+
22
31
  .ui.table tr > th:first-child {
23
32
  border-left: none;
24
33
  }
@@ -659,6 +659,21 @@ img.responsive {
659
659
  margin-top: 20px;
660
660
  }
661
661
 
662
+ .toast-box-center {
663
+ display: flex;
664
+ align-items: center;
665
+ justify-content: center;
666
+
667
+ .save.toast-box {
668
+ background: transparent;
669
+ color: #007eb1;
670
+
671
+ .circled.toast-box-blue-icon {
672
+ color: #007eb1;
673
+ }
674
+ }
675
+ }
676
+
662
677
  // Deprecated as per https://github.com/plone/volto/issues/1265
663
678
  // @import 'utils';
664
679
  @import (multiple) '../extras/fonts';
@@ -0,0 +1 @@
1
+ export default function withSaveAsDraft(options: any): (WrappedComponent: any) => any;