@plone/volto 17.0.0-alpha.4 → 17.0.0-alpha.6

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 (38) hide show
  1. package/.changelog.draft +20 -20
  2. package/.yarn/install-state.gz +0 -0
  3. package/CHANGELOG.md +98 -13
  4. package/CONTRIBUTING.md +1 -1
  5. package/README.md +9 -12
  6. package/locales/de/LC_MESSAGES/volto.po +17 -17
  7. package/locales/de.json +1 -1
  8. package/package.json +3 -2
  9. package/packages/volto-slate/package.json +1 -1
  10. package/src/components/manage/Blocks/Listing/Edit.jsx +0 -14
  11. package/src/components/manage/Blocks/Listing/ListingBody.test.jsx +0 -20
  12. package/src/components/manage/Blocks/Listing/getAsyncData.js +10 -2
  13. package/src/components/manage/Blocks/Listing/withQuerystringResults.jsx +20 -14
  14. package/src/components/manage/Blocks/Search/SearchBlockEdit.jsx +5 -4
  15. package/src/components/manage/Blocks/Search/SearchBlockView.jsx +2 -1
  16. package/src/components/manage/Blocks/Search/hocs/withSearch.jsx +28 -19
  17. package/src/components/manage/Contents/Contents.jsx +29 -24
  18. package/src/components/manage/Controlpanels/Controlpanels.jsx +190 -224
  19. package/src/components/manage/Controlpanels/Controlpanels.test.jsx +46 -7
  20. package/src/components/manage/Form/InlineForm.jsx +39 -9
  21. package/src/components/manage/Form/InlineFormState.js +8 -0
  22. package/src/components/manage/Widgets/ObjectListWidget.jsx +3 -8
  23. package/src/components/theme/Icon/Icon.jsx +2 -2
  24. package/src/components/theme/Login/Login.jsx +1 -0
  25. package/src/config/index.js +1 -0
  26. package/src/express-middleware/sitemap.js +36 -3
  27. package/src/helpers/Robots/Robots.js +24 -6
  28. package/src/helpers/Sitemap/Sitemap.js +44 -2
  29. package/src/helpers/Url/Url.js +8 -3
  30. package/src/helpers/Url/Url.test.js +14 -0
  31. package/src/helpers/Utils/Utils.js +17 -4
  32. package/src/helpers/Utils/usePagination.js +14 -48
  33. package/src/helpers/index.js +2 -0
  34. package/src/middleware/Api.test.js +54 -0
  35. package/src/middleware/api.js +1 -1
  36. package/test-setup-config.js +1 -0
  37. package/theme/themes/pastanaga/extras/sidebar.less +4 -0
  38. package/src/helpers/Utils/usePagination.test.js +0 -115
@@ -114,15 +114,19 @@ function normalizeState({
114
114
  block: id,
115
115
  };
116
116
 
117
- // TODO: need to check if SearchableText facet is not already in the query
118
- // Ideally the searchtext functionality should be restructured as being just
119
- // another facet
120
- params.query = params.query.reduce(
121
- // Remove SearchableText from query
122
- (acc, kvp) => (kvp.i === 'SearchableText' ? acc : [...acc, kvp]),
123
- [],
124
- );
117
+ // Note Ideally the searchtext functionality should be restructured as being just
118
+ // another facet. But right now it's the same. This means that if a searchText
119
+ // is provided, it will override the SearchableText facet.
120
+ // If there is no searchText, the SearchableText in the query remains in effect.
121
+ // TODO eventually the searchText should be a distinct facet from SearchableText, and
122
+ // the two conditions could be combined, in comparison to the current state, when
123
+ // one overrides the other.
125
124
  if (searchText) {
125
+ params.query = params.query.reduce(
126
+ // Remove SearchableText from query
127
+ (acc, kvp) => (kvp.i === 'SearchableText' ? acc : [...acc, kvp]),
128
+ [],
129
+ );
126
130
  params.query.push({
127
131
  i: 'SearchableText',
128
132
  o: 'plone.app.querystring.operation.string.contains',
@@ -144,16 +148,12 @@ const getSearchFields = (searchData) => {
144
148
  };
145
149
 
146
150
  /**
147
- * A hook that will mirror the search block state to a hash location
151
+ * A HOC that will mirror the search block state to a hash location
148
152
  */
149
153
  const useHashState = () => {
150
154
  const location = useLocation();
151
155
  const history = useHistory();
152
156
 
153
- /**
154
- * Required to maintain parameter compatibility.
155
- With this we will maintain support for receiving hash (#) and search (?) type parameters.
156
- */
157
157
  const oldState = React.useMemo(() => {
158
158
  return {
159
159
  ...qs.parse(location.search),
@@ -169,7 +169,7 @@ const useHashState = () => {
169
169
 
170
170
  const setSearchData = React.useCallback(
171
171
  (searchData) => {
172
- const newParams = qs.parse(location.search);
172
+ const newParams = qs.parse(location.hash);
173
173
 
174
174
  let changed = false;
175
175
 
@@ -186,11 +186,11 @@ const useHashState = () => {
186
186
 
187
187
  if (changed) {
188
188
  history.push({
189
- search: qs.stringify(newParams),
189
+ hash: qs.stringify(newParams),
190
190
  });
191
191
  }
192
192
  },
193
- [history, oldState, location.search],
193
+ [history, oldState, location.hash],
194
194
  );
195
195
 
196
196
  return [current, setSearchData];
@@ -282,8 +282,14 @@ const withSearch = (options) => (WrappedComponent) => {
282
282
  const timeoutRef = React.useRef();
283
283
  const facetSettings = data?.facets;
284
284
 
285
+ const deepQuery = JSON.stringify(data.query);
285
286
  const onTriggerSearch = React.useCallback(
286
- (toSearchText, toSearchFacets, toSortOn, toSortOrder) => {
287
+ (
288
+ toSearchText = undefined,
289
+ toSearchFacets = undefined,
290
+ toSortOn = undefined,
291
+ toSortOrder = undefined,
292
+ ) => {
287
293
  if (timeoutRef.current) clearTimeout(timeoutRef.current);
288
294
  timeoutRef.current = setTimeout(
289
295
  () => {
@@ -291,7 +297,7 @@ const withSearch = (options) => (WrappedComponent) => {
291
297
  id,
292
298
  query: data.query || {},
293
299
  facets: toSearchFacets || facets,
294
- searchText: toSearchText,
300
+ searchText: toSearchText || searchText,
295
301
  sortOn: toSortOn || sortOn,
296
302
  sortOrder: toSortOrder || sortOrder,
297
303
  facetSettings,
@@ -305,11 +311,14 @@ const withSearch = (options) => (WrappedComponent) => {
305
311
  toSearchFacets ? inputDelay / 3 : inputDelay,
306
312
  );
307
313
  },
314
+ // eslint-disable-next-line react-hooks/exhaustive-deps
308
315
  [
309
- data.query,
316
+ // Use deep comparison of data.query
317
+ deepQuery,
310
318
  facets,
311
319
  id,
312
320
  setLocationSearchData,
321
+ searchText,
313
322
  sortOn,
314
323
  sortOrder,
315
324
  facetSettings,
@@ -796,18 +796,20 @@ class Contents extends Component {
796
796
  */
797
797
  onMoveToTop(event, { value }) {
798
798
  const id = this.state.items[value]['@id'];
799
- value = this.state.currentPage * this.state.pageSize + value;
800
- this.props.orderContent(
801
- getBaseUrl(this.props.pathname),
802
- id.replace(/^.*\//, ''),
803
- -value,
804
- );
805
- this.setState(
806
- {
807
- currentPage: 0,
808
- },
809
- () => this.fetchContents(),
810
- );
799
+ this.props
800
+ .orderContent(
801
+ getBaseUrl(this.props.pathname),
802
+ id.replace(/^.*\//, ''),
803
+ 'top',
804
+ )
805
+ .then(() => {
806
+ this.setState(
807
+ {
808
+ currentPage: 0,
809
+ },
810
+ () => this.fetchContents(),
811
+ );
812
+ });
811
813
  }
812
814
 
813
815
  /**
@@ -818,18 +820,21 @@ class Contents extends Component {
818
820
  * @returns {undefined}
819
821
  */
820
822
  onMoveToBottom(event, { value }) {
821
- this.onOrderItem(
822
- this.state.items[value]['@id'],
823
- value,
824
- this.state.items.length - 1 - value,
825
- false,
826
- );
827
- this.onOrderItem(
828
- this.state.items[value]['@id'],
829
- value,
830
- this.state.items.length - 1 - value,
831
- true,
832
- );
823
+ const id = this.state.items[value]['@id'];
824
+ this.props
825
+ .orderContent(
826
+ getBaseUrl(this.props.pathname),
827
+ id.replace(/^.*\//, ''),
828
+ 'bottom',
829
+ )
830
+ .then(() => {
831
+ this.setState(
832
+ {
833
+ currentPage: 0,
834
+ },
835
+ () => this.fetchContents(),
836
+ );
837
+ });
833
838
  }
834
839
 
835
840
  /**
@@ -3,16 +3,16 @@
3
3
  * @module components/manage/Controlpanels/Controlpanels
4
4
  */
5
5
 
6
- import React, { Component } from 'react';
7
6
  import PropTypes from 'prop-types';
7
+ import { useState, useEffect } from 'react';
8
8
  import { connect } from 'react-redux';
9
9
  import { compose } from 'redux';
10
10
  import { Link } from 'react-router-dom';
11
11
  import { concat, filter, last, map, uniqBy } from 'lodash';
12
12
  import { Portal } from 'react-portal';
13
- import { Helmet } from '@plone/volto/helpers';
13
+ import { asyncConnect, Helmet } from '@plone/volto/helpers';
14
14
  import { Container, Grid, Header, Message, Segment } from 'semantic-ui-react';
15
- import { FormattedMessage, defineMessages, injectIntl } from 'react-intl';
15
+ import { FormattedMessage, defineMessages, useIntl } from 'react-intl';
16
16
 
17
17
  import { listControlpanels, getSystemInformation } from '@plone/volto/actions';
18
18
  import { Error, Icon, Toolbar, VersionOverview } from '@plone/volto/components';
@@ -94,185 +94,126 @@ const messages = defineMessages({
94
94
 
95
95
  /**
96
96
  * Controlpanels container class.
97
- * @class Controlpanels
98
- * @extends Component
99
97
  */
100
- class Controlpanels extends Component {
101
- /**
102
- * Property types.
103
- * @property {Object} propTypes Property types.
104
- * @static
105
- */
106
- static propTypes = {
107
- listControlpanels: PropTypes.func.isRequired,
108
- controlpanels: PropTypes.arrayOf(
109
- PropTypes.shape({
110
- '@id': PropTypes.string,
111
- group: PropTypes.string,
112
- title: PropTypes.string,
113
- }),
114
- ).isRequired,
115
- pathname: PropTypes.string.isRequired,
116
- };
98
+ function Controlpanels({
99
+ controlpanels,
100
+ controlpanelsRequest,
101
+ systemInformation,
102
+ pathname,
103
+ }) {
104
+ const intl = useIntl();
105
+ const [isClient, setIsClient] = useState(false);
117
106
 
118
- /**
119
- * Constructor
120
- * @method constructor
121
- * @param {Object} props Component properties
122
- * @constructs EditComponent
123
- */
124
- constructor(props) {
125
- super(props);
126
- this.state = {
127
- error: null,
128
- isClient: false,
129
- };
130
- }
107
+ useEffect(() => {
108
+ setIsClient(true);
109
+ }, []);
131
110
 
132
- /**
133
- * Component did mount
134
- * @method componentDidMount
135
- * @returns {undefined}
136
- */
137
- componentDidMount() {
138
- this.props.listControlpanels();
139
- this.props.getSystemInformation();
140
- this.setState({ isClient: true });
141
- }
111
+ const error = controlpanelsRequest?.error;
142
112
 
143
- UNSAFE_componentWillReceiveProps(nextProps) {
144
- // Error
145
- if (
146
- this.props.controlpanelsRequest.loading &&
147
- nextProps.controlpanelsRequest.error
148
- ) {
149
- this.setState({
150
- error: nextProps.controlpanelsRequest.error,
151
- });
152
- }
113
+ if (error) {
114
+ return <Error error={error} />;
153
115
  }
154
116
 
155
- /**
156
- * Render method.
157
- * @method render
158
- * @returns {string} Markup for the component.
159
- */
160
- render() {
161
- // Error
162
- if (this.state.error) {
163
- return <Error error={this.state.error} />;
164
- }
117
+ let customcontrolpanels = config.settings.controlpanels
118
+ ? config.settings.controlpanels.map((el) => {
119
+ el.group =
120
+ intl.formatMessage({
121
+ id: el.group,
122
+ defaultMessage: el.group,
123
+ }) || el.group;
124
+ return el;
125
+ })
126
+ : [];
127
+ const { filterControlPanels } = config.settings;
165
128
 
166
- let customcontrolpanels = config.settings.controlpanels
167
- ? config.settings.controlpanels.map((el) => {
168
- el.group =
169
- this.props.intl.formatMessage({
170
- id: el.group,
171
- defaultMessage: el.group,
172
- }) || el.group;
173
- return el;
174
- })
175
- : [];
176
- const { filterControlPanels } = config.settings;
177
- const controlpanels = map(
178
- concat(
179
- filterControlPanels(this.props.controlpanels),
180
- customcontrolpanels,
181
- [
182
- {
183
- '@id': '/addons',
184
- group: this.props.intl.formatMessage(messages.general),
185
- title: this.props.intl.formatMessage(messages.addons),
186
- },
187
- {
188
- '@id': '/database',
189
- group: this.props.intl.formatMessage(messages.general),
190
- title: this.props.intl.formatMessage(messages.database),
191
- },
192
- {
193
- '@id': '/rules',
194
- group: this.props.intl.formatMessage(messages.content),
195
- title: this.props.intl.formatMessage(messages.contentRules),
196
- },
197
- {
198
- '@id': '/undo',
199
- group: this.props.intl.formatMessage(messages.general),
200
- title: this.props.intl.formatMessage(messages.undo),
201
- },
202
- {
203
- '@id': '/aliases',
204
- group: this.props.intl.formatMessage(messages.general),
205
- title: this.props.intl.formatMessage(messages.urlmanagement),
206
- },
207
- {
208
- '@id': '/moderate-comments',
209
- group: this.props.intl.formatMessage(messages.content),
210
- title: this.props.intl.formatMessage(messages.moderatecomments),
211
- },
212
- {
213
- '@id': '/users',
214
- group: this.props.intl.formatMessage(
215
- messages.usersControlPanelCategory,
216
- ),
217
- title: this.props.intl.formatMessage(messages.users),
218
- },
219
- {
220
- '@id': '/usergroupmembership',
221
- group: this.props.intl.formatMessage(
222
- messages.usersControlPanelCategory,
223
- ),
224
- title: this.props.intl.formatMessage(
225
- messages.usergroupmemberbership,
226
- ),
227
- },
228
- {
229
- '@id': '/groups',
230
- group: this.props.intl.formatMessage(
231
- messages.usersControlPanelCategory,
232
- ),
233
- title: this.props.intl.formatMessage(messages.groups),
234
- },
235
- ],
236
- ),
237
- (controlpanel) => ({
238
- ...controlpanel,
239
- id: last(controlpanel['@id'].split('/')),
240
- }),
241
- );
242
- const groups = map(uniqBy(controlpanels, 'group'), 'group');
243
- const { controlPanelsIcons: icons } = config.settings;
129
+ const filteredControlPanels = map(
130
+ concat(filterControlPanels(controlpanels), customcontrolpanels, [
131
+ {
132
+ '@id': '/addons',
133
+ group: intl.formatMessage(messages.general),
134
+ title: intl.formatMessage(messages.addons),
135
+ },
136
+ {
137
+ '@id': '/database',
138
+ group: intl.formatMessage(messages.general),
139
+ title: intl.formatMessage(messages.database),
140
+ },
141
+ {
142
+ '@id': '/rules',
143
+ group: intl.formatMessage(messages.content),
144
+ title: intl.formatMessage(messages.contentRules),
145
+ },
146
+ {
147
+ '@id': '/undo',
148
+ group: intl.formatMessage(messages.general),
149
+ title: intl.formatMessage(messages.undo),
150
+ },
151
+ {
152
+ '@id': '/aliases',
153
+ group: intl.formatMessage(messages.general),
154
+ title: intl.formatMessage(messages.urlmanagement),
155
+ },
156
+ {
157
+ '@id': '/moderate-comments',
158
+ group: intl.formatMessage(messages.content),
159
+ title: intl.formatMessage(messages.moderatecomments),
160
+ },
161
+ {
162
+ '@id': '/users',
163
+ group: intl.formatMessage(messages.usersControlPanelCategory),
164
+ title: intl.formatMessage(messages.users),
165
+ },
166
+ {
167
+ '@id': '/usergroupmembership',
168
+ group: intl.formatMessage(messages.usersControlPanelCategory),
169
+ title: intl.formatMessage(messages.usergroupmemberbership),
170
+ },
171
+ {
172
+ '@id': '/groups',
173
+ group: intl.formatMessage(messages.usersControlPanelCategory),
174
+ title: intl.formatMessage(messages.groups),
175
+ },
176
+ ]),
177
+ (controlpanel) => ({
178
+ ...controlpanel,
179
+ id: last(controlpanel['@id'].split('/')),
180
+ }),
181
+ );
182
+ const groups = map(uniqBy(filteredControlPanels, 'group'), 'group');
183
+ const { controlPanelsIcons: icons } = config.settings;
244
184
 
245
- return (
246
- <div className="view-wrapper">
247
- <Helmet title={this.props.intl.formatMessage(messages.sitesetup)} />
248
- <Container className="controlpanel">
249
- <Segment.Group raised>
250
- <Segment className="primary">
251
- <FormattedMessage id="Site Setup" defaultMessage="Site Setup" />
252
- </Segment>
253
- {this.props.systemInformation &&
254
- this.props.systemInformation.upgrade && (
255
- <Message attached warning>
256
- <FormattedMessage
257
- id="The site configuration is outdated and needs to be upgraded."
258
- defaultMessage="The site configuration is outdated and needs to be upgraded."
259
- />{' '}
260
- <Link to={`/controlpanel/plone-upgrade`}>
261
- <FormattedMessage
262
- id="Please continue with the upgrade."
263
- defaultMessage="Please continue with the upgrade."
264
- />
265
- </Link>
266
- </Message>
267
- )}
268
- {map(groups, (group) => [
269
- <Segment key={`header-${group}`} secondary>
270
- {group}
271
- </Segment>,
272
- <Segment key={`body-${group}`} attached>
273
- <Grid doubling columns={6}>
274
- <Grid.Row>
275
- {map(filter(controlpanels, { group }), (controlpanel) => (
185
+ return (
186
+ <div className="view-wrapper">
187
+ <Helmet title={intl.formatMessage(messages.sitesetup)} />
188
+ <Container className="controlpanel">
189
+ <Segment.Group raised>
190
+ <Segment className="primary">
191
+ <FormattedMessage id="Site Setup" defaultMessage="Site Setup" />
192
+ </Segment>
193
+ {systemInformation && systemInformation.upgrade && (
194
+ <Message attached warning>
195
+ <FormattedMessage
196
+ id="The site configuration is outdated and needs to be upgraded."
197
+ defaultMessage="The site configuration is outdated and needs to be upgraded."
198
+ />{' '}
199
+ <Link to={`/controlpanel/plone-upgrade`}>
200
+ <FormattedMessage
201
+ id="Please continue with the upgrade."
202
+ defaultMessage="Please continue with the upgrade."
203
+ />
204
+ </Link>
205
+ </Message>
206
+ )}
207
+ {map(groups, (group) => [
208
+ <Segment key={`header-${group}`} secondary>
209
+ {group}
210
+ </Segment>,
211
+ <Segment key={`body-${group}`} attached>
212
+ <Grid doubling columns={6}>
213
+ <Grid.Row>
214
+ {map(
215
+ filter(filteredControlPanels, { group }),
216
+ (controlpanel) => (
276
217
  <Grid.Column key={controlpanel.id}>
277
218
  <Link to={`/controlpanel/${controlpanel.id}`}>
278
219
  <Header as="h3" icon textAlign="center">
@@ -286,58 +227,83 @@ class Controlpanels extends Component {
286
227
  </Header>
287
228
  </Link>
288
229
  </Grid.Column>
289
- ))}
290
- </Grid.Row>
291
- </Grid>
292
- </Segment>,
293
- ])}
294
- </Segment.Group>
295
- <Segment.Group raised>
296
- <Segment className="primary">
297
- <FormattedMessage
298
- id="Version Overview"
299
- defaultMessage="Version Overview"
300
- />
301
- </Segment>
302
- <Segment attached>
303
- {this.props.systemInformation ? (
304
- <VersionOverview {...this.props.systemInformation} />
305
- ) : null}
306
- </Segment>
307
- </Segment.Group>
308
- </Container>
309
- {this.state.isClient && (
310
- <Portal node={document.getElementById('toolbar')}>
311
- <Toolbar
312
- pathname={this.props.pathname}
313
- hideDefaultViewButtons
314
- inner={
315
- <Link to="/" className="item">
316
- <Icon
317
- name={backSVG}
318
- className="contents circled"
319
- size="30px"
320
- title={this.props.intl.formatMessage(messages.back)}
321
- />
322
- </Link>
323
- }
230
+ ),
231
+ )}
232
+ </Grid.Row>
233
+ </Grid>
234
+ </Segment>,
235
+ ])}
236
+ </Segment.Group>
237
+ <Segment.Group raised>
238
+ <Segment className="primary">
239
+ <FormattedMessage
240
+ id="Version Overview"
241
+ defaultMessage="Version Overview"
324
242
  />
325
- </Portal>
326
- )}
327
- </div>
328
- );
329
- }
243
+ </Segment>
244
+ <Segment attached>
245
+ {systemInformation ? (
246
+ <VersionOverview {...systemInformation} />
247
+ ) : null}
248
+ </Segment>
249
+ </Segment.Group>
250
+ </Container>
251
+ {isClient && (
252
+ <Portal node={document.getElementById('toolbar')}>
253
+ <Toolbar
254
+ pathname={pathname}
255
+ hideDefaultViewButtons
256
+ inner={
257
+ <Link to="/" className="item">
258
+ <Icon
259
+ name={backSVG}
260
+ className="contents circled"
261
+ size="30px"
262
+ title={intl.formatMessage(messages.back)}
263
+ />
264
+ </Link>
265
+ }
266
+ />
267
+ </Portal>
268
+ )}
269
+ </div>
270
+ );
330
271
  }
331
272
 
332
- export default compose(
333
- injectIntl,
334
- connect(
335
- (state, props) => ({
336
- controlpanels: state.controlpanels.controlpanels,
337
- controlpanelsRequest: state.controlpanels.list,
338
- pathname: props.location.pathname,
339
- systemInformation: state.controlpanels.systeminformation,
273
+ /**
274
+ * Property types.
275
+ * @property {Object} propTypes Property types.
276
+ * @static
277
+ */
278
+ Controlpanels.propTypes = {
279
+ controlpanels: PropTypes.arrayOf(
280
+ PropTypes.shape({
281
+ '@id': PropTypes.string,
282
+ group: PropTypes.string,
283
+ title: PropTypes.string,
340
284
  }),
341
- { listControlpanels, getSystemInformation },
342
- ),
285
+ ).isRequired,
286
+ pathname: PropTypes.string.isRequired,
287
+ };
288
+
289
+ export default compose(
290
+ connect((state, props) => ({
291
+ controlpanels: state.controlpanels.controlpanels,
292
+ controlpanelsRequest: state.controlpanels.list,
293
+ pathname: props.location.pathname,
294
+ systemInformation: state.controlpanels.systeminformation,
295
+ })),
296
+
297
+ asyncConnect([
298
+ {
299
+ key: 'controlpanels',
300
+ promise: async ({ location, store: { dispatch } }) =>
301
+ await dispatch(listControlpanels()),
302
+ },
303
+ {
304
+ key: 'systemInformation',
305
+ promise: async ({ location, store: { dispatch } }) =>
306
+ await dispatch(getSystemInformation()),
307
+ },
308
+ ]),
343
309
  )(Controlpanels);