@plone/volto 19.0.0-alpha.31 → 19.0.0-alpha.32

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.
package/CHANGELOG.md CHANGED
@@ -17,6 +17,12 @@ myst:
17
17
 
18
18
  <!-- towncrier release notes start -->
19
19
 
20
+ ## 19.0.0-alpha.32 (2026-04-30)
21
+
22
+ ### Breaking
23
+
24
+ - Make form autosave opt-in behind `config.experimental.saveAsDraft.enabled`; enable this flag to preserve the previous save-as-draft behavior. @sneridagh
25
+
20
26
  ## 19.0.0-alpha.31 (2026-04-28)
21
27
 
22
28
  ### Feature
package/package.json CHANGED
@@ -9,7 +9,7 @@
9
9
  }
10
10
  ],
11
11
  "license": "MIT",
12
- "version": "19.0.0-alpha.31",
12
+ "version": "19.0.0-alpha.32",
13
13
  "repository": {
14
14
  "type": "git",
15
15
  "url": "git@github.com:plone/volto.git"
@@ -183,9 +183,9 @@
183
183
  "use-deep-compare-effect": "1.8.1",
184
184
  "uuid": "^14.0.0",
185
185
  "@plone/components": "4.0.0-alpha.7",
186
- "@plone/registry": "3.0.0-alpha.11",
186
+ "@plone/scripts": "4.0.0-alpha.6",
187
187
  "@plone/volto-slate": "19.0.0-alpha.15",
188
- "@plone/scripts": "4.0.0-alpha.6"
188
+ "@plone/registry": "3.0.0-alpha.11"
189
189
  },
190
190
  "devDependencies": {
191
191
  "@babel/core": "^7.28.5",
@@ -292,11 +292,11 @@
292
292
  "webpack-bundle-analyzer": "4.10.1",
293
293
  "webpack-dev-server": "^5.2.3",
294
294
  "webpack-node-externals": "3.0.0",
295
- "@plone/babel-preset-razzle": "^1.0.0-alpha.0",
295
+ "@plone/razzle": "1.0.0-alpha.4",
296
296
  "@plone/types": "2.0.0-alpha.17",
297
- "@plone/razzle-dev-utils": "1.0.0-alpha.2",
297
+ "@plone/babel-preset-razzle": "^1.0.0-alpha.0",
298
298
  "@plone/volto-coresandbox": "1.0.0",
299
- "@plone/razzle": "1.0.0-alpha.4"
299
+ "@plone/razzle-dev-utils": "1.0.0-alpha.2"
300
300
  },
301
301
  "scripts": {
302
302
  "analyze": "BUNDLE_ANALYZE=true razzle build",
@@ -59,6 +59,8 @@ import { compose } from 'redux';
59
59
  import config from '@plone/volto/registry';
60
60
  import SlotRenderer from '@plone/volto/components/theme/SlotRenderer/SlotRenderer';
61
61
 
62
+ const noop = () => {};
63
+
62
64
  /**
63
65
  * Form container class.
64
66
  * @class Form
@@ -117,6 +119,9 @@ class Form extends Component {
117
119
  allowedBlocks: PropTypes.arrayOf(PropTypes.string),
118
120
  showRestricted: PropTypes.bool,
119
121
  global: PropTypes.bool,
122
+ checkSavedDraft: PropTypes.func,
123
+ onSaveDraft: PropTypes.func,
124
+ onCancelDraft: PropTypes.func,
120
125
  };
121
126
 
122
127
  /**
@@ -153,6 +158,9 @@ class Form extends Component {
153
158
  requestError: null,
154
159
  allowedBlocks: null,
155
160
  global: false,
161
+ checkSavedDraft: noop,
162
+ onSaveDraft: noop,
163
+ onCancelDraft: noop,
156
164
  };
157
165
 
158
166
  /**
@@ -4,14 +4,47 @@ import configureStore from 'redux-mock-store';
4
4
  import { Provider } from 'react-intl-redux';
5
5
  import { MemoryRouter } from 'react-router-dom';
6
6
  import Form from './Form';
7
+ import config from '@plone/volto/registry';
7
8
 
8
9
  const mockStore = configureStore();
9
10
  const errorMessage =
10
11
  "[{'message': 'The specified email is not valid.', 'field': 'contact_email', 'error': 'ValidationError'}";
11
12
 
12
13
  vi.mock('@plone/volto/components/manage/Form');
14
+ vi.mock('@plone/volto/helpers/Utils/withSaveAsDraft', async () => {
15
+ const React = await import('react');
16
+ const { default: config } = await import('@plone/volto/registry');
17
+
18
+ return {
19
+ default: () => (WrappedComponent) =>
20
+ React.forwardRef((props, ref) =>
21
+ config.experimental?.saveAsDraft?.enabled ? (
22
+ <div data-autosave-wrapper="true">
23
+ <WrappedComponent
24
+ {...props}
25
+ checkSavedDraft={vi.fn()}
26
+ onSaveDraft={vi.fn()}
27
+ onCancelDraft={vi.fn()}
28
+ ref={ref}
29
+ />
30
+ </div>
31
+ ) : (
32
+ <WrappedComponent {...props} ref={ref} />
33
+ ),
34
+ ),
35
+ };
36
+ });
13
37
 
14
38
  describe('Form', () => {
39
+ beforeEach(() => {
40
+ config.experimental = {
41
+ ...(config.experimental || {}),
42
+ saveAsDraft: {
43
+ enabled: false,
44
+ },
45
+ };
46
+ });
47
+
15
48
  it('renders a form component', () => {
16
49
  const store = mockStore({
17
50
  intl: {
@@ -49,10 +82,106 @@ describe('Form', () => {
49
82
  onCancel={() => {}}
50
83
  />
51
84
  </MemoryRouter>
52
- ,
53
85
  </Provider>,
54
86
  );
55
87
  const json = component.toJSON();
56
88
  expect(json).toMatchSnapshot();
57
89
  });
90
+
91
+ it('does not wrap the form with autosave when the experimental flag is disabled', () => {
92
+ const store = mockStore({
93
+ intl: {
94
+ locale: 'en',
95
+ messages: {},
96
+ },
97
+ content: {
98
+ data: {},
99
+ create: {
100
+ loading: false,
101
+ loaded: true,
102
+ },
103
+ },
104
+ });
105
+
106
+ const component = renderer.create(
107
+ <Provider store={store}>
108
+ <MemoryRouter initialEntries={['/some-route']}>
109
+ <Form
110
+ schema={{
111
+ fieldsets: [
112
+ {
113
+ id: 'default',
114
+ title: 'Default',
115
+ fields: ['title'],
116
+ },
117
+ ],
118
+ properties: {
119
+ title: {},
120
+ },
121
+ required: [],
122
+ }}
123
+ requestError={errorMessage}
124
+ onSubmit={() => {}}
125
+ onCancel={() => {}}
126
+ />
127
+ </MemoryRouter>
128
+ </Provider>,
129
+ );
130
+
131
+ expect(
132
+ component.root.findAllByProps({ 'data-autosave-wrapper': 'true' }),
133
+ ).toHaveLength(0);
134
+ });
135
+
136
+ it('wraps the form with autosave when the experimental flag is enabled', () => {
137
+ config.experimental = {
138
+ ...config.experimental,
139
+ saveAsDraft: {
140
+ enabled: true,
141
+ },
142
+ };
143
+
144
+ const store = mockStore({
145
+ intl: {
146
+ locale: 'en',
147
+ messages: {},
148
+ },
149
+ content: {
150
+ data: {},
151
+ create: {
152
+ loading: false,
153
+ loaded: true,
154
+ },
155
+ },
156
+ });
157
+
158
+ const component = renderer.create(
159
+ <Provider store={store}>
160
+ <MemoryRouter initialEntries={['/some-route']}>
161
+ <Form
162
+ schema={{
163
+ fieldsets: [
164
+ {
165
+ id: 'default',
166
+ title: 'Default',
167
+ fields: ['title'],
168
+ },
169
+ ],
170
+ properties: {
171
+ title: {},
172
+ },
173
+ required: [],
174
+ }}
175
+ requestError={errorMessage}
176
+ onSubmit={() => {}}
177
+ onCancel={() => {}}
178
+ />
179
+ </MemoryRouter>
180
+ </Provider>,
181
+ );
182
+
183
+ expect(
184
+ component.root.findAllByProps({ 'data-autosave-wrapper': 'true' }),
185
+ ).toHaveLength(1);
186
+ });
58
187
  });
@@ -189,6 +189,9 @@ let config = {
189
189
  addBlockButton: {
190
190
  enabled: true,
191
191
  },
192
+ saveAsDraft: {
193
+ enabled: false,
194
+ },
192
195
  },
193
196
  widgets: {},
194
197
  views: {},
@@ -9,6 +9,7 @@ import checkSVG from '@plone/volto/icons/check.svg';
9
9
  import clearSVG from '@plone/volto/icons/clear.svg';
10
10
  import { useIntl, defineMessages } from 'react-intl';
11
11
  import { useLocation } from 'react-router-dom';
12
+ import config from '@plone/volto/registry';
12
13
 
13
14
  const messages = defineMessages({
14
15
  autoSaveFound: {
@@ -227,15 +228,38 @@ export default function withSaveAsDraft(options) {
227
228
  WrappedComponent,
228
229
  )})`;
229
230
 
230
- if (forwardRef) {
231
- return hoistNonReactStatics(
232
- React.forwardRef((props, ref) => (
233
- <WithSaveAsDraft {...props} forwardedRef={ref} />
234
- )),
235
- WrappedComponent,
236
- );
237
- }
231
+ const DraftWrappedComponent = forwardRef
232
+ ? hoistNonReactStatics(
233
+ React.forwardRef((props, ref) => (
234
+ <WithSaveAsDraft {...props} forwardedRef={ref} />
235
+ )),
236
+ WrappedComponent,
237
+ )
238
+ : hoistNonReactStatics(WithSaveAsDraft, WrappedComponent);
239
+
240
+ const WithExperimentalSaveAsDraft = forwardRef
241
+ ? hoistNonReactStatics(
242
+ React.forwardRef((props, ref) => {
243
+ const ComponentToRender = config.experimental?.saveAsDraft?.enabled
244
+ ? DraftWrappedComponent
245
+ : WrappedComponent;
246
+
247
+ return <ComponentToRender {...props} ref={ref} />;
248
+ }),
249
+ WrappedComponent,
250
+ )
251
+ : hoistNonReactStatics((props) => {
252
+ const ComponentToRender = config.experimental?.saveAsDraft?.enabled
253
+ ? DraftWrappedComponent
254
+ : WrappedComponent;
255
+
256
+ return <ComponentToRender {...props} />;
257
+ }, WrappedComponent);
258
+
259
+ WithExperimentalSaveAsDraft.displayName = `WithExperimentalSaveAsDraft(${getDisplayName(
260
+ WrappedComponent,
261
+ )})`;
238
262
 
239
- return hoistNonReactStatics(WithSaveAsDraft, WrappedComponent);
263
+ return WithExperimentalSaveAsDraft;
240
264
  };
241
265
  }