@plone/volto 18.7.0 → 18.8.0

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.
@@ -47,53 +47,55 @@ const RelationsControlPanel = () => {
47
47
 
48
48
  return (
49
49
  <>
50
- <div className="relations-control-panel">
51
- <Helmet title={intl.formatMessage(messages.relations)} />
52
- {can_edit ? (
53
- <Segment.Group raised>
54
- <Segment className="primary">
55
- {brokenRelations && Object.keys(brokenRelations).length > 0 ? (
56
- <React.Fragment>
57
- <Message warning>
58
- <FormattedMessage
59
- id="Some relations are broken. Please fix."
60
- defaultMessage="Some relations are broken. Please fix."
61
- />
62
- </Message>
63
- <Divider hidden />
64
- </React.Fragment>
65
- ) : null}
66
- <h1>
50
+ <div className="ui container">
51
+ <div className="relations-control-panel">
52
+ <Helmet title={intl.formatMessage(messages.relations)} />
53
+ {can_edit ? (
54
+ <Segment.Group raised>
55
+ <Segment className="primary">
56
+ {brokenRelations && Object.keys(brokenRelations).length > 0 ? (
57
+ <React.Fragment>
58
+ <Message warning>
59
+ <FormattedMessage
60
+ id="Some relations are broken. Please fix."
61
+ defaultMessage="Some relations are broken. Please fix."
62
+ />
63
+ </Message>
64
+ <Divider hidden />
65
+ </React.Fragment>
66
+ ) : null}
67
+ <h1>
68
+ <FormattedMessage id="Relations" defaultMessage="Relations" />
69
+ </h1>
70
+ {relations_stats?.error ? (
71
+ <React.Fragment>
72
+ <Divider hidden />
73
+ <Message warning>
74
+ <FormattedMessage
75
+ id="Please upgrade to plone.restapi >= 8.39.0."
76
+ defaultMessage="Please upgrade to plone.restapi >= 8.39.0."
77
+ />
78
+ </Message>
79
+ </React.Fragment>
80
+ ) : null}
81
+ </Segment>
82
+ <Segment>
83
+ <RelationsMatrix />
84
+ </Segment>
85
+ </Segment.Group>
86
+ ) : (
87
+ <Segment.Group>
88
+ <Segment>
67
89
  <FormattedMessage id="Relations" defaultMessage="Relations" />
68
- </h1>
69
- {relations_stats?.error ? (
70
- <React.Fragment>
71
- <Divider hidden />
72
- <Message warning>
73
- <FormattedMessage
74
- id="Please upgrade to plone.restapi >= 8.39.0."
75
- defaultMessage="Please upgrade to plone.restapi >= 8.39.0."
76
- />
77
- </Message>
78
- </React.Fragment>
79
- ) : null}
80
- </Segment>
81
- <Segment>
82
- <RelationsMatrix />
83
- </Segment>
84
- </Segment.Group>
85
- ) : (
86
- <Segment.Group>
87
- <Segment>
88
- <FormattedMessage id="Relations" defaultMessage="Relations" />
89
- <Divider hidden />
90
- <FormattedMessage
91
- id="You have not the required permission for this control panel."
92
- defaultMessage="You have not the required permission for this control panel."
93
- />
94
- </Segment>
95
- </Segment.Group>
96
- )}
90
+ <Divider hidden />
91
+ <FormattedMessage
92
+ id="You have not the required permission for this control panel."
93
+ defaultMessage="You have not the required permission for this control panel."
94
+ />
95
+ </Segment>
96
+ </Segment.Group>
97
+ )}
98
+ </div>
97
99
  </div>
98
100
 
99
101
  {isClient &&
@@ -191,6 +191,13 @@ const More = (props) => {
191
191
  id: 'redirection',
192
192
  });
193
193
 
194
+ const workingCopyCheckoutAction = find(actions.object_buttons, {
195
+ id: 'iterate_checkout',
196
+ });
197
+ const workingCopyCheckinAction = find(actions.object_buttons, {
198
+ id: 'iterate_checkin',
199
+ });
200
+
194
201
  const dateOptions = {
195
202
  year: 'numeric',
196
203
  month: 'long',
@@ -320,125 +327,114 @@ const More = (props) => {
320
327
  </>
321
328
  )}
322
329
  </Pluggable>
323
- {config.settings.hasWorkingCopySupport &&
324
- content['@type'] !== 'Plone Site' && (
325
- <>
326
- {!content.working_copy && (
327
- <Plug pluggable="toolbar-more-manage-content" id="workingcopy">
328
- <li>
329
- <button
330
- aria-label={intl.formatMessage(messages.CreateWorkingCopy)}
331
- onClick={() => {
332
- dispatch(createWorkingCopy(path)).then((response) => {
333
- history.push(flattenToAppURL(response['@id']));
334
- props.closeMenu();
335
- });
336
- }}
337
- >
338
- {intl.formatMessage(messages.CreateWorkingCopy)}
330
+ {workingCopyCheckoutAction && (
331
+ <Plug pluggable="toolbar-more-manage-content" id="workingcopy">
332
+ <li>
333
+ <button
334
+ aria-label={intl.formatMessage(messages.CreateWorkingCopy)}
335
+ onClick={() => {
336
+ dispatch(createWorkingCopy(path)).then((response) => {
337
+ history.push(flattenToAppURL(response['@id']));
338
+ props.closeMenu();
339
+ });
340
+ }}
341
+ >
342
+ {intl.formatMessage(messages.CreateWorkingCopy)}
339
343
 
340
- <Icon name={rightArrowSVG} size="24px" />
341
- </button>
342
- </li>
343
- </Plug>
344
- )}
345
- {content.working_copy && content.working_copy_of && (
346
- <Plug pluggable="toolbar-more-manage-content" id="workingcopy">
347
- <li>
348
- <button
349
- aria-label={intl.formatMessage(messages.applyWorkingCopy)}
350
- onClick={() => {
351
- dispatch(applyWorkingCopy(path)).then((response) => {
352
- history.push(
353
- flattenToAppURL(content.working_copy_of['@id']),
354
- );
355
- props.closeMenu();
356
- toast.info(
357
- <Toast
358
- info
359
- title={intl.formatMessage(
360
- messages.workingAppliedTitle,
361
- )}
362
- content={intl.formatMessage(
363
- messages.workingCopyAppliedBy,
364
- {
365
- creator: content.working_copy?.creator_name,
366
- date: (
367
- <FormattedDate
368
- date={content.working_copy?.created}
369
- format={dateOptions}
370
- />
371
- ),
372
- },
373
- )}
374
- />,
375
- {
376
- toastId: 'workingcopyapplyinfo',
377
- autoClose: 10000,
378
- },
379
- );
380
- });
381
- }}
382
- >
383
- {intl.formatMessage(messages.applyWorkingCopy)}
344
+ <Icon name={rightArrowSVG} size="24px" />
345
+ </button>
346
+ </li>
347
+ </Plug>
348
+ )}
349
+ {workingCopyCheckinAction && (
350
+ <Plug pluggable="toolbar-more-manage-content" id="workingcopy">
351
+ <li>
352
+ <button
353
+ aria-label={intl.formatMessage(messages.applyWorkingCopy)}
354
+ onClick={() => {
355
+ dispatch(applyWorkingCopy(path)).then((response) => {
356
+ history.push(flattenToAppURL(content.working_copy_of['@id']));
357
+ props.closeMenu();
358
+ toast.info(
359
+ <Toast
360
+ info
361
+ title={intl.formatMessage(messages.workingAppliedTitle)}
362
+ content={intl.formatMessage(
363
+ messages.workingCopyAppliedBy,
364
+ {
365
+ creator: content.working_copy?.creator_name,
366
+ date: (
367
+ <FormattedDate
368
+ date={content.working_copy?.created}
369
+ format={dateOptions}
370
+ />
371
+ ),
372
+ },
373
+ )}
374
+ />,
375
+ {
376
+ toastId: 'workingcopyapplyinfo',
377
+ autoClose: 10000,
378
+ },
379
+ );
380
+ });
381
+ }}
382
+ >
383
+ {intl.formatMessage(messages.applyWorkingCopy)}
384
384
 
385
- <Icon
386
- name={applySVG}
387
- size="24px"
388
- title={intl.formatMessage(messages.applyWorkingCopy)}
389
- />
390
- </button>
391
- </li>
392
- <li>
393
- <button
394
- aria-label={intl.formatMessage(messages.removeWorkingCopy)}
395
- onClick={() => {
396
- dispatch(removeWorkingCopy(path)).then((response) => {
397
- history.push(
398
- flattenToAppURL(content.working_copy_of['@id']),
399
- );
400
- props.closeMenu();
401
- toast.info(
402
- <Toast
403
- info
404
- title={intl.formatMessage(
405
- messages.workingCopyRemovedTitle,
406
- )}
407
- />,
408
- {
409
- toastId: 'workingcopyremovednotice',
410
- autoClose: 10000,
411
- },
412
- );
413
- });
414
- }}
415
- >
416
- {intl.formatMessage(messages.removeWorkingCopy)}
417
- <Icon
418
- name={removeSVG}
419
- size="24px"
420
- color="#e40166"
421
- title={intl.formatMessage(messages.removeWorkingCopy)}
422
- />
423
- </button>
424
- </li>
425
- </Plug>
426
- )}
427
- {content.working_copy && !content.working_copy_of && (
428
- <Plug pluggable="toolbar-more-manage-content" id="workingcopy">
429
- <li>
430
- <Link
431
- to={flattenToAppURL(content.working_copy['@id'])}
432
- onClick={() => props.closeMenu()}
433
- >
434
- {intl.formatMessage(messages.viewWorkingCopy)}
435
- <Icon name={rightArrowSVG} size="24px" />
436
- </Link>
437
- </li>
438
- </Plug>
439
- )}
440
- </>
441
- )}
385
+ <Icon
386
+ name={applySVG}
387
+ size="24px"
388
+ title={intl.formatMessage(messages.applyWorkingCopy)}
389
+ />
390
+ </button>
391
+ </li>
392
+ <li>
393
+ <button
394
+ aria-label={intl.formatMessage(messages.removeWorkingCopy)}
395
+ onClick={() => {
396
+ dispatch(removeWorkingCopy(path)).then((response) => {
397
+ history.push(flattenToAppURL(content.working_copy_of['@id']));
398
+ props.closeMenu();
399
+ toast.info(
400
+ <Toast
401
+ info
402
+ title={intl.formatMessage(
403
+ messages.workingCopyRemovedTitle,
404
+ )}
405
+ />,
406
+ {
407
+ toastId: 'workingcopyremovednotice',
408
+ autoClose: 10000,
409
+ },
410
+ );
411
+ });
412
+ }}
413
+ >
414
+ {intl.formatMessage(messages.removeWorkingCopy)}
415
+ <Icon
416
+ name={removeSVG}
417
+ size="24px"
418
+ color="#e40166"
419
+ title={intl.formatMessage(messages.removeWorkingCopy)}
420
+ />
421
+ </button>
422
+ </li>
423
+ </Plug>
424
+ )}
425
+ {content.working_copy && !content.working_copy_of && (
426
+ <Plug pluggable="toolbar-more-manage-content" id="workingcopy">
427
+ <li>
428
+ <Link
429
+ to={flattenToAppURL(content.working_copy['@id'])}
430
+ onClick={() => props.closeMenu()}
431
+ >
432
+ {intl.formatMessage(messages.viewWorkingCopy)}
433
+ <Icon name={rightArrowSVG} size="24px" />
434
+ </Link>
435
+ </li>
436
+ </Plug>
437
+ )}
442
438
  {editAction && config.settings.isMultilingual && (
443
439
  <Plug pluggable="toolbar-more-manage-content" id="multilingual">
444
440
  <li>
@@ -1,10 +1,8 @@
1
- import React from 'react';
2
1
  import configureStore from 'redux-mock-store';
3
2
  import { Provider } from 'react-intl-redux';
4
3
  import { MemoryRouter } from 'react-router-dom';
5
4
  import { PluggablesProvider } from '@plone/volto/components/manage/Pluggable';
6
5
  import { waitFor, render } from '@testing-library/react';
7
- import config from '@plone/volto/registry';
8
6
 
9
7
  import More from './More';
10
8
 
@@ -162,26 +160,4 @@ describe('Toolbar More component', () => {
162
160
  await waitFor(() => {});
163
161
  expect(container).toMatchSnapshot();
164
162
  });
165
- it('renders a Toolbar More component with manage content (working copy)', async () => {
166
- config.settings.hasWorkingCopySupport = true;
167
-
168
- const { container } = render(
169
- <PluggablesProvider>
170
- <Provider store={store}>
171
- <MemoryRouter>
172
- <More
173
- pathname="/blah"
174
- loadComponent={() => {}}
175
- theToolbar={{
176
- current: { getBoundingClientRect: () => ({ width: '320' }) },
177
- }}
178
- closeMenu={() => {}}
179
- />
180
- </MemoryRouter>
181
- </Provider>
182
- </PluggablesProvider>,
183
- );
184
- await waitFor(() => {});
185
- expect(container).toMatchSnapshot();
186
- });
187
163
  });
@@ -7,7 +7,6 @@ import { defineMessages, useIntl } from 'react-intl';
7
7
  import Toast from '@plone/volto/components/manage/Toast/Toast';
8
8
  import { flattenToAppURL } from '@plone/volto/helpers/Url/Url';
9
9
  import FormattedDate from '@plone/volto/components/theme/FormattedDate/FormattedDate';
10
- import config from '@plone/volto/registry';
11
10
  import useDeepCompareEffect from 'use-deep-compare-effect';
12
11
 
13
12
  const messages = defineMessages({
@@ -39,48 +38,28 @@ const WorkingCopyToastsFactory = (props) => {
39
38
  };
40
39
 
41
40
  useDeepCompareEffect(() => {
42
- if (content && config.settings.hasWorkingCopySupport) {
43
- if (working_copy) {
44
- let toastMessage, toastTitle;
45
- if (content.working_copy_of) {
46
- // I'm a working copy
47
- toastMessage = messages.thisIsAWorkingCopyOf;
48
- toastTitle = (
49
- <Link to={flattenToAppURL(content.working_copy_of['@id'])}>
50
- {content.working_copy_of?.title}
51
- </Link>
52
- );
53
- } else {
54
- // I'm a baseline
55
- toastMessage = messages.workingCopyIs;
56
- toastTitle = (
57
- <Link to={flattenToAppURL(working_copy['@id'])}>
58
- {working_copy?.title}
59
- </Link>
60
- );
61
- }
62
- if (toast.isActive('workingcopyinfo')) {
63
- toast.update('workingcopyinfo', {
64
- render: (
65
- <Toast
66
- info
67
- title={intl.formatMessage(toastMessage, {
68
- title: toastTitle,
69
- })}
70
- content={intl.formatMessage(messages.workingCopyCreatedBy, {
71
- creator: working_copy?.creator_name,
72
- date: (
73
- <FormattedDate
74
- date={working_copy?.created}
75
- format={dateOptions}
76
- />
77
- ),
78
- })}
79
- />
80
- ),
81
- });
82
- } else {
83
- toast.info(
41
+ if (working_copy) {
42
+ let toastMessage, toastTitle;
43
+ if (content.working_copy_of) {
44
+ // I'm a working copy
45
+ toastMessage = messages.thisIsAWorkingCopyOf;
46
+ toastTitle = (
47
+ <Link to={flattenToAppURL(content.working_copy_of['@id'])}>
48
+ {content.working_copy_of?.title}
49
+ </Link>
50
+ );
51
+ } else {
52
+ // I'm a baseline
53
+ toastMessage = messages.workingCopyIs;
54
+ toastTitle = (
55
+ <Link to={flattenToAppURL(working_copy['@id'])}>
56
+ {working_copy?.title}
57
+ </Link>
58
+ );
59
+ }
60
+ if (toast.isActive('workingcopyinfo')) {
61
+ toast.update('workingcopyinfo', {
62
+ render: (
84
63
  <Toast
85
64
  info
86
65
  title={intl.formatMessage(toastMessage, {
@@ -95,20 +74,38 @@ const WorkingCopyToastsFactory = (props) => {
95
74
  />
96
75
  ),
97
76
  })}
98
- />,
99
- {
100
- toastId: 'workingcopyinfo',
101
- autoClose: false,
102
- closeButton: false,
103
- transition: null,
104
- },
105
- );
106
- }
77
+ />
78
+ ),
79
+ });
80
+ } else {
81
+ toast.info(
82
+ <Toast
83
+ info
84
+ title={intl.formatMessage(toastMessage, {
85
+ title: toastTitle,
86
+ })}
87
+ content={intl.formatMessage(messages.workingCopyCreatedBy, {
88
+ creator: working_copy?.creator_name,
89
+ date: (
90
+ <FormattedDate
91
+ date={working_copy?.created}
92
+ format={dateOptions}
93
+ />
94
+ ),
95
+ })}
96
+ />,
97
+ {
98
+ toastId: 'workingcopyinfo',
99
+ autoClose: false,
100
+ closeButton: false,
101
+ transition: null,
102
+ },
103
+ );
107
104
  }
108
- if (!working_copy) {
109
- if (toast.isActive('workingcopyinfo')) {
110
- toast.dismiss('workingcopyinfo');
111
- }
105
+ }
106
+ if (!working_copy) {
107
+ if (toast.isActive('workingcopyinfo')) {
108
+ toast.dismiss('workingcopyinfo');
112
109
  }
113
110
  }
114
111
  }, [pathname, content, title, working_copy, intl, lang, dateOptions]);
@@ -54,7 +54,14 @@ export default function Image({
54
54
  attrs.className = cx(className, { responsive });
55
55
 
56
56
  if (!isSvg && image.scales && Object.keys(image.scales).length > 0) {
57
- const sortedScales = Object.values(image.scales).sort((a, b) => {
57
+ const sortedScales = Object.values({
58
+ ...image.scales,
59
+ original: {
60
+ download: `${image.download}`,
61
+ width: image.width,
62
+ height: image.height,
63
+ },
64
+ }).sort((a, b) => {
58
65
  if (a.width > b.width) return 1;
59
66
  else if (a.width < b.width) return -1;
60
67
  else return 0;
@@ -76,7 +76,6 @@ export const unwantedControlPanelsFields = {
76
76
  'site_favicon_mimetype',
77
77
  'exposeDCMetaTags',
78
78
  'enable_sitemap',
79
- 'robots_txt',
80
79
  'webstats_js',
81
80
  ],
82
81
  editing: ['available_editors', 'default_editor', 'ext_editor'],
@@ -163,7 +163,6 @@ let config = {
163
163
  showSelfRegistration: false,
164
164
  contentMetadataTagsImageField: 'image',
165
165
  contentPropertiesSchemaEnhancer: null,
166
- hasWorkingCopySupport: false,
167
166
  maxUndoLevels: 200, // undo history size for the main form
168
167
  addonsInfo: addonsInfo,
169
168
  workflowMapping,
@@ -4,13 +4,12 @@ import { generateRobots } from '@plone/volto/helpers/Robots/Robots';
4
4
  /*
5
5
  robots.txt - priority order:
6
6
 
7
- 1) robots.txt in /public folder
8
- 2) VOLTO_ROBOTSTXT var in .env
9
- 3) default: plone robots.txt
7
+ 1) VOLTO_ROBOTSTXT var in .env
8
+ 2) robots.txt setting in the site control panel
10
9
 
11
10
  */
12
11
 
13
- const ploneRobots = function (req, res, next) {
12
+ const siteRobots = function (req, res, next) {
14
13
  generateRobots(req).then((robots) => {
15
14
  res.set('Content-Type', 'text/plain');
16
15
  res.send(robots);
@@ -27,7 +26,7 @@ export default function robotstxtMiddleware() {
27
26
  if (process.env.VOLTO_ROBOTSTXT) {
28
27
  middleware.all('**/robots.txt', envRobots);
29
28
  } else {
30
- middleware.all('**/robots.txt', ploneRobots);
29
+ middleware.all('**/robots.txt', siteRobots);
31
30
  }
32
31
  middleware.id = 'robots.txt';
33
32
  return middleware;
@@ -17,7 +17,7 @@ const methods = ['get', 'post', 'put', 'patch', 'del'];
17
17
  * @param {string} path Path (or URL) to be formatted.
18
18
  * @returns {string} Formatted path.
19
19
  */
20
- function formatUrl(path) {
20
+ export function formatUrl(path) {
21
21
  const { settings } = config;
22
22
  const APISUFIX = settings.legacyTraverse ? '' : '/++api++';
23
23