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

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 (33) hide show
  1. package/AGENTS.md +47 -0
  2. package/CHANGELOG.md +27 -0
  3. package/README.md +0 -1
  4. package/package.json +16 -19
  5. package/src/actions/controlpanels/controlpanels.js +7 -12
  6. package/src/actions/controlpanels/controlpanels.test.js +2 -3
  7. package/src/components/manage/Contents/Contents.jsx +410 -323
  8. package/src/components/manage/Contents/Contents.test.jsx +1 -1
  9. package/src/components/manage/Contents/ContentsIndexHeader.jsx +47 -81
  10. package/src/components/manage/Contents/ContentsIndexHeader.test.jsx +10 -3
  11. package/src/components/manage/Contents/ContentsItem.jsx +226 -278
  12. package/src/components/manage/Contents/ContentsItem.test.jsx +10 -6
  13. package/src/components/manage/Controlpanels/ContentType.jsx +131 -222
  14. package/src/components/manage/Controlpanels/Controlpanel.jsx +122 -218
  15. package/src/components/manage/Controlpanels/Controlpanel.test.jsx +1 -29
  16. package/src/components/manage/Form/Field.jsx +1 -69
  17. package/src/components/manage/Widgets/ArrayWidget.jsx +111 -88
  18. package/src/components/manage/Widgets/ArrayWidget.test.jsx +0 -6
  19. package/src/components/manage/Widgets/RecurrenceWidget/WeekdayOfTheMonthIndexField.jsx +56 -50
  20. package/src/components/manage/Widgets/SelectStyling.jsx +52 -20
  21. package/src/config/Loadables.jsx +1 -5
  22. package/src/server.jsx +7 -1
  23. package/theme/themes/default/globals/site.variables +3 -3
  24. package/theme/themes/pastanaga/extras/contents.less +0 -4
  25. package/theme/themes/pastanaga/globals/site.variables +0 -3
  26. package/types/components/manage/Contents/Contents.d.ts +1 -1
  27. package/types/components/manage/Contents/ContentsIndexHeader.d.ts +6 -11
  28. package/types/components/manage/Contents/ContentsItem.d.ts +3 -10
  29. package/types/components/manage/Controlpanels/ContentType.d.ts +2 -2
  30. package/types/components/manage/Controlpanels/Controlpanel.d.ts +2 -5
  31. package/types/components/manage/Controlpanels/index.d.ts +2 -2
  32. package/types/components/manage/Widgets/RecurrenceWidget/WeekdayOfTheMonthIndexField.d.ts +22 -5
  33. package/types/components/manage/Widgets/SelectStyling.d.ts +1 -0
@@ -1,21 +1,15 @@
1
- /**
2
- * Controlpanel component.
3
- * @module components/manage/Controlpanels/Controlpanel
4
- */
5
-
6
- import React, { Component } from 'react';
7
- import PropTypes from 'prop-types';
8
- import { connect } from 'react-redux';
9
- import { compose } from 'redux';
10
- import { withRouter } from 'react-router-dom';
1
+ import { useEffect, useRef, useState } from 'react';
2
+ import { useDispatch, useSelector } from 'react-redux';
3
+ import { useHistory, useLocation, useParams } from 'react-router-dom';
11
4
  import Helmet from '@plone/volto/helpers/Helmet/Helmet';
5
+ import { useClient } from '@plone/volto/hooks/client/useClient';
12
6
  import {
13
7
  tryParseJSON,
14
8
  extractInvariantErrors,
15
9
  } from '@plone/volto/helpers/FormValidation/FormValidation';
16
10
  import { createPortal } from 'react-dom';
17
11
  import { Button, Container } from 'semantic-ui-react';
18
- import { defineMessages, injectIntl } from 'react-intl';
12
+ import { defineMessages, useIntl } from 'react-intl';
19
13
  import { toast } from 'react-toastify';
20
14
 
21
15
  import Icon from '@plone/volto/components/theme/Icon/Icon';
@@ -26,6 +20,7 @@ import {
26
20
  updateControlpanel,
27
21
  getControlpanel,
28
22
  } from '@plone/volto/actions/controlpanels/controlpanels';
23
+ import { getSite } from '@plone/volto/actions/site/site';
29
24
 
30
25
  import config from '@plone/volto/registry';
31
26
 
@@ -59,217 +54,126 @@ const messages = defineMessages({
59
54
  },
60
55
  });
61
56
 
62
- /**
63
- * Controlpanel class.
64
- * @class Controlpanel
65
- * @extends Component
66
- */
67
- class Controlpanel extends Component {
68
- /**
69
- * Property types.
70
- * @property {Object} propTypes Property types.
71
- * @static
72
- */
73
- static propTypes = {
74
- updateControlpanel: PropTypes.func.isRequired,
75
- getControlpanel: PropTypes.func.isRequired,
76
- id: PropTypes.string.isRequired,
77
- updateRequest: PropTypes.shape({
78
- loading: PropTypes.bool,
79
- loaded: PropTypes.bool,
80
- }).isRequired,
81
- controlpanel: PropTypes.shape({
82
- '@id': PropTypes.string,
83
- data: PropTypes.object,
84
- schema: PropTypes.object,
85
- title: PropTypes.string,
86
- }),
87
- pathname: PropTypes.string.isRequired,
88
- };
89
-
90
- /**
91
- * Default properties.
92
- * @property {Object} defaultProps Default properties.
93
- * @static
94
- */
95
- static defaultProps = {
96
- controlpanel: null,
57
+ const Controlpanel = () => {
58
+ const { filterControlPanelsSchema } = config.settings;
59
+ const intl = useIntl();
60
+ const id = useParams().id;
61
+ const pathname = useLocation().pathname;
62
+ const history = useHistory();
63
+ const isClient = useClient();
64
+
65
+ const dispatch = useDispatch();
66
+ const controlpanel = useSelector((state) => state.controlpanels.controlpanel);
67
+ const updateRequest = useSelector((state) => state.controlpanels.update);
68
+ const [error, setError] = useState(null);
69
+
70
+ const formRef = useRef();
71
+ const onCancel = () => {
72
+ history.goBack();
97
73
  };
98
-
99
- /**
100
- * Constructor
101
- * @method constructor
102
- * @param {Object} props Component properties
103
- * @constructs Controlpanel
104
- */
105
- constructor(props) {
106
- super(props);
107
- this.onCancel = this.onCancel.bind(this);
108
- this.onSubmit = this.onSubmit.bind(this);
109
- this.state = { isClient: false, error: null };
110
- }
111
-
112
- /**
113
- * Component did mount
114
- * @method componentDidMount
115
- * @returns {undefined}
116
- */
117
- componentDidMount() {
118
- this.props.getControlpanel(this.props.id);
119
- this.setState({ isClient: true });
120
- }
121
-
122
- /**
123
- * Component will receive props
124
- * @method componentWillReceiveProps
125
- * @param {Object} nextProps Next properties
126
- * @returns {undefined}
127
- */
128
- UNSAFE_componentWillReceiveProps(nextProps) {
129
- if (this.props.updateRequest.loading && nextProps.updateRequest.error) {
130
- const message =
131
- nextProps.updateRequest.error?.response?.body?.error?.message ||
132
- nextProps.updateRequest.error?.response?.body?.message ||
133
- nextProps.updateRequest.error?.response?.text ||
134
- '';
135
-
136
- const error =
137
- new DOMParser().parseFromString(message, 'text/html')?.all?.[0]
138
- ?.textContent || message;
139
-
140
- const errorsList = tryParseJSON(error);
141
- let invariantErrors = [];
142
- if (Array.isArray(errorsList)) {
143
- invariantErrors = extractInvariantErrors(errorsList);
144
- }
145
-
146
- this.setState({ error: error });
147
-
148
- if (invariantErrors.length > 0) {
149
- toast.error(
74
+ const onSubmit = (data) => {
75
+ dispatch(updateControlpanel(controlpanel['@id'], data))
76
+ .then(() => {
77
+ dispatch(getSite());
78
+ toast.info(
150
79
  <Toast
151
- error
152
- title={this.props.intl.formatMessage(messages.error)}
153
- content={invariantErrors.join(' - ')}
80
+ info
81
+ title={intl.formatMessage(messages.info)}
82
+ content={intl.formatMessage(messages.changesSaved)}
154
83
  />,
155
84
  );
156
- }
157
- }
158
-
159
- if (this.props.updateRequest.loading && nextProps.updateRequest.loaded) {
160
- toast.info(
161
- <Toast
162
- info
163
- title={this.props.intl.formatMessage(messages.info)}
164
- content={this.props.intl.formatMessage(messages.changesSaved)}
165
- />,
166
- );
167
- }
168
- }
169
-
170
- /**
171
- * Submit handler
172
- * @method onSubmit
173
- * @param {object} data Form data.
174
- * @returns {undefined}
175
- */
176
- onSubmit(data) {
177
- this.props.updateControlpanel(this.props.controlpanel['@id'], data);
178
- }
179
-
180
- /**
181
- * Cancel handler
182
- * @method onCancel
183
- * @returns {undefined}
184
- */
185
- onCancel() {
186
- this.props.history.goBack();
187
- }
188
- form = React.createRef();
189
-
190
- /**
191
- * Render method.
192
- * @method render
193
- * @returns {string} Markup for the component.
194
- */
195
- render() {
196
- const { filterControlPanelsSchema } = config.settings;
85
+ })
86
+ .catch((err) => {
87
+ const message =
88
+ err?.response?.body?.error?.message ||
89
+ err?.response?.body?.message ||
90
+ err?.response?.text ||
91
+ '';
92
+ const error =
93
+ new DOMParser().parseFromString(message, 'text/html')?.all?.[0]
94
+ ?.textContent || message;
95
+ const errorsList = tryParseJSON(error);
96
+ let invariantErrors = [];
97
+ if (Array.isArray(errorsList)) {
98
+ invariantErrors = extractInvariantErrors(errorsList);
99
+ }
100
+ setError(error);
101
+ if (invariantErrors.length > 0) {
102
+ toast.error(
103
+ <Toast
104
+ error
105
+ title={intl.formatMessage(messages.error)}
106
+ content={invariantErrors.join(' - ')}
107
+ />,
108
+ );
109
+ }
110
+ });
111
+ };
197
112
 
198
- if (this.props.controlpanel) {
199
- return (
200
- <div id="page-controlpanel">
201
- <Helmet title={this.props.controlpanel.title} />
202
- <Container>
203
- <Form
204
- ref={this.form}
205
- title={this.props.controlpanel.title}
206
- schema={filterControlPanelsSchema(this.props.controlpanel)}
207
- formData={this.props.controlpanel.data}
208
- requestError={this.state.error}
209
- onSubmit={this.onSubmit}
210
- onCancel={this.onCancel}
211
- hideActions
212
- loading={this.props.updateRequest.loading}
213
- />
214
- </Container>
215
- {this.state.isClient &&
216
- createPortal(
217
- <Toolbar
218
- pathname={this.props.pathname}
219
- hideDefaultViewButtons
220
- inner={
221
- <>
222
- <Button
223
- id="toolbar-save"
224
- className="save"
225
- aria-label={this.props.intl.formatMessage(messages.save)}
226
- onClick={() => this.form.current.onSubmit()}
227
- disabled={this.props.updateRequest.loading}
228
- loading={this.props.updateRequest.loading}
229
- >
230
- <Icon
231
- name={saveSVG}
232
- className="circled"
233
- size="30px"
234
- title={this.props.intl.formatMessage(messages.save)}
235
- />
236
- </Button>
237
- <Button
238
- className="cancel"
239
- aria-label={this.props.intl.formatMessage(
240
- messages.cancel,
241
- )}
242
- onClick={() => this.onCancel()}
243
- >
244
- <Icon
245
- name={clearSVG}
246
- className="circled"
247
- size="30px"
248
- title={this.props.intl.formatMessage(messages.cancel)}
249
- />
250
- </Button>
251
- </>
252
- }
253
- />,
254
- document.getElementById('toolbar'),
255
- )}
256
- </div>
257
- );
258
- }
259
- return <div />;
113
+ useEffect(() => {
114
+ dispatch(getControlpanel(id)).catch((error) => setError(error));
115
+ }, [dispatch, id]);
116
+
117
+ if (controlpanel) {
118
+ return (
119
+ <div id="page-controlpanel">
120
+ <Helmet title={controlpanel.title} />
121
+ <Container>
122
+ <Form
123
+ ref={formRef}
124
+ title={controlpanel.title}
125
+ schema={filterControlPanelsSchema(controlpanel)}
126
+ formData={controlpanel.data}
127
+ requestError={error}
128
+ onSubmit={onSubmit}
129
+ onCancel={onCancel}
130
+ hideActions
131
+ loading={updateRequest.loading}
132
+ />
133
+ </Container>
134
+ {isClient &&
135
+ createPortal(
136
+ <Toolbar
137
+ pathname={pathname}
138
+ hideDefaultViewButtons
139
+ inner={
140
+ <>
141
+ <Button
142
+ id="toolbar-save"
143
+ className="save"
144
+ aria-label={intl.formatMessage(messages.save)}
145
+ onClick={() => formRef.current.onSubmit()}
146
+ disabled={updateRequest.loading}
147
+ loading={updateRequest.loading}
148
+ >
149
+ <Icon
150
+ name={saveSVG}
151
+ className="circled"
152
+ size="30px"
153
+ title={intl.formatMessage(messages.save)}
154
+ />
155
+ </Button>
156
+ <Button
157
+ className="cancel"
158
+ aria-label={intl.formatMessage(messages.cancel)}
159
+ onClick={onCancel}
160
+ >
161
+ <Icon
162
+ name={clearSVG}
163
+ className="circled"
164
+ size="30px"
165
+ title={intl.formatMessage(messages.cancel)}
166
+ />
167
+ </Button>
168
+ </>
169
+ }
170
+ />,
171
+ document.getElementById('toolbar'),
172
+ )}
173
+ </div>
174
+ );
260
175
  }
261
- }
176
+ return <div />;
177
+ };
262
178
 
263
- export default compose(
264
- injectIntl,
265
- connect(
266
- (state, props) => ({
267
- controlpanel: state.controlpanels.controlpanel,
268
- updateRequest: state.controlpanels.update,
269
- id: props.match.params.id,
270
- pathname: props.location.pathname,
271
- }),
272
- { updateControlpanel, getControlpanel },
273
- ),
274
- withRouter,
275
- )(Controlpanel);
179
+ export default Controlpanel;
@@ -1,4 +1,4 @@
1
- import { render, waitFor, screen } from '@testing-library/react';
1
+ import { render } from '@testing-library/react';
2
2
  import configureStore from 'redux-mock-store';
3
3
  import { Provider } from 'react-intl-redux';
4
4
  import { MemoryRouter, Route } from 'react-router-dom';
@@ -79,32 +79,4 @@ describe('Controlpanel', () => {
79
79
 
80
80
  expect(container).toMatchSnapshot();
81
81
  });
82
-
83
- it('renders a controlpanel component with error', async () => {
84
- const { container, rerender } = render(
85
- <Provider store={store}>
86
- <MemoryRouter initialEntries={['/controlpanel/date-and-time']}>
87
- <Route path={'/controlpanel/:id'} component={Controlpanel} />
88
- <div id="toolbar"></div>
89
- </MemoryRouter>
90
- </Provider>,
91
- );
92
-
93
- store.getState().controlpanels.update.loading = true;
94
- store.getState().controlpanels.update.error.response.body.message =
95
- "[{'message': 'Twitter username should not include the \"@\" prefix character.', 'field': 'twitter_username', 'error': 'ValidationError'}]";
96
- store.dispatch = vi.fn(() => Promise.resolve());
97
-
98
- rerender(
99
- <Provider store={store}>
100
- <MemoryRouter initialEntries={['/controlpanel/date-and-time']}>
101
- <Route path={'/controlpanel/:id'} component={Controlpanel} />
102
- <div id="toolbar"></div>
103
- </MemoryRouter>
104
- </Provider>,
105
- );
106
-
107
- await waitFor(() => screen.findByText(/Twitter/i));
108
- expect(container).toMatchSnapshot();
109
- });
110
82
  });
@@ -7,7 +7,6 @@ import React from 'react';
7
7
  import PropTypes from 'prop-types';
8
8
  import { injectIntl } from 'react-intl';
9
9
  import config from '@plone/volto/registry';
10
- import { injectLazyLibs } from '@plone/volto/helpers/Loadable/Loadable';
11
10
 
12
11
  const MODE_HIDDEN = 'hidden'; //hidden mode. If mode is hidden, field is not rendered
13
12
  /**
@@ -155,7 +154,7 @@ const getWidgetByType = (widgets, type) => widgets.type[type] || null;
155
154
  * @param {Object} props Properties.
156
155
  * @returns {string} Markup of the component.
157
156
  */
158
- const UnconnectedField = (props, { intl }) => {
157
+ const Field = (props, { intl }) => {
159
158
  const widgets = props.widgets || config.widgets;
160
159
  const Widget =
161
160
  getWidgetByFieldId(widgets, props.id) ||
@@ -178,74 +177,9 @@ const UnconnectedField = (props, { intl }) => {
178
177
  ...getWidgetPropsFromTaggedValues(widgets, props.widgetOptions),
179
178
  };
180
179
 
181
- if (props.onOrder) {
182
- const { DropTarget, DragSource } = props.reactDnd;
183
- const WrappedWidget = DropTarget(
184
- 'field',
185
- {
186
- hover(properties, monitor) {
187
- const dragOrder = monitor.getItem().order;
188
- const hoverOrder = properties.order;
189
-
190
- if (dragOrder === hoverOrder) {
191
- return;
192
- }
193
- properties.onOrder(dragOrder, hoverOrder - dragOrder);
194
-
195
- monitor.getItem().order = hoverOrder;
196
- },
197
- },
198
- (connect) => ({
199
- connectDropTarget: connect.dropTarget(),
200
- }),
201
- )(
202
- DragSource(
203
- 'field',
204
- {
205
- beginDrag(properties) {
206
- return {
207
- id: properties.label,
208
- order: properties.order,
209
- };
210
- },
211
- },
212
- (connect, monitor) => ({
213
- connectDragSource: connect.dragSource(),
214
- connectDragPreview: connect.dragPreview(),
215
- isDragging: monitor.isDragging(),
216
- }),
217
- )(
218
- ({
219
- connectDropTarget,
220
- connectDragSource,
221
- connectDragPreview,
222
- ...rest
223
- }) =>
224
- connectDropTarget(
225
- connectDragSource(
226
- connectDragPreview(
227
- <div>
228
- <Widget {...rest} />
229
- </div>,
230
- ),
231
- ),
232
- ),
233
- ),
234
- );
235
- return <WrappedWidget {...widgetProps} />;
236
- }
237
180
  return <Widget {...widgetProps} />;
238
181
  };
239
182
 
240
- const DndConnectedField = injectLazyLibs(['reactDnd'])(UnconnectedField);
241
-
242
- const Field = (props) =>
243
- props.onOrder ? (
244
- <DndConnectedField {...props} />
245
- ) : (
246
- <UnconnectedField {...props} />
247
- );
248
-
249
183
  /**
250
184
  * Property types.
251
185
  * @property {Object} propTypes Property types.
@@ -258,7 +192,6 @@ Field.propTypes = {
258
192
  type: PropTypes.string,
259
193
  id: PropTypes.string.isRequired,
260
194
  focus: PropTypes.bool,
261
- onOrder: PropTypes.func,
262
195
  };
263
196
 
264
197
  /**
@@ -272,7 +205,6 @@ Field.defaultProps = {
272
205
  choices: null,
273
206
  type: 'string',
274
207
  focus: false,
275
- onOrder: null,
276
208
  };
277
209
 
278
210
  export default injectIntl(Field);