@plone/volto 16.0.0 → 16.2.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.
Files changed (64) hide show
  1. package/CHANGELOG.md +30 -6
  2. package/cypress/support/commands.js +1 -1
  3. package/locales/ca/LC_MESSAGES/volto.po +22 -0
  4. package/locales/ca.json +1 -1
  5. package/locales/de/LC_MESSAGES/volto.po +22 -0
  6. package/locales/de.json +1 -1
  7. package/locales/en/LC_MESSAGES/volto.po +22 -0
  8. package/locales/en.json +1 -1
  9. package/locales/es/LC_MESSAGES/volto.po +22 -0
  10. package/locales/es.json +1 -1
  11. package/locales/eu/LC_MESSAGES/volto.po +22 -0
  12. package/locales/eu.json +1 -1
  13. package/locales/fr/LC_MESSAGES/volto.po +22 -0
  14. package/locales/fr.json +1 -1
  15. package/locales/it/LC_MESSAGES/volto.po +22 -0
  16. package/locales/it.json +1 -1
  17. package/locales/ja/LC_MESSAGES/volto.po +22 -0
  18. package/locales/ja.json +1 -1
  19. package/locales/nl/LC_MESSAGES/volto.po +22 -0
  20. package/locales/nl.json +1 -1
  21. package/locales/pt/LC_MESSAGES/volto.po +22 -0
  22. package/locales/pt.json +1 -1
  23. package/locales/pt_BR/LC_MESSAGES/volto.po +22 -0
  24. package/locales/pt_BR.json +1 -1
  25. package/locales/ro/LC_MESSAGES/volto.po +22 -0
  26. package/locales/ro.json +1 -1
  27. package/locales/volto.pot +23 -1
  28. package/package.json +5 -3
  29. package/packages/volto-slate/README.md +2 -233
  30. package/packages/volto-slate/src/blocks/Text/extensions/index.js +1 -0
  31. package/packages/volto-slate/src/blocks/Text/extensions/normalizeExternalData.js +7 -0
  32. package/packages/volto-slate/src/blocks/Text/index.js +2 -0
  33. package/packages/volto-slate/src/editor/config.jsx +2 -0
  34. package/packages/volto-slate/src/editor/deserialize.js +25 -55
  35. package/packages/volto-slate/src/editor/extensions/index.js +1 -0
  36. package/packages/volto-slate/src/editor/extensions/insertData.js +17 -4
  37. package/packages/volto-slate/src/editor/extensions/normalizeExternalData.js +8 -0
  38. package/packages/volto-slate/src/editor/ui/Toolbar.jsx +8 -3
  39. package/packages/volto-slate/src/editor/ui/ToolbarButton.test.js +2 -2
  40. package/packages/volto-slate/src/editor/utils.js +248 -0
  41. package/src/components/index.js +3 -0
  42. package/src/components/manage/Blocks/Block/DefaultEdit.jsx +44 -0
  43. package/src/components/manage/Blocks/Block/DefaultView.jsx +78 -0
  44. package/src/components/manage/Blocks/Block/Edit.jsx +3 -2
  45. package/src/components/manage/Blocks/HeroImageLeft/Data.jsx +2 -1
  46. package/src/components/manage/Blocks/Image/ImageSidebar.jsx +1 -0
  47. package/src/components/manage/Blocks/Listing/ListingData.jsx +1 -0
  48. package/src/components/manage/Blocks/Maps/MapsSidebar.jsx +1 -0
  49. package/src/components/manage/Blocks/Search/SearchBlockEdit.jsx +1 -0
  50. package/src/components/manage/Blocks/Search/components/SelectFacet.jsx +2 -1
  51. package/src/components/manage/Blocks/ToC/Edit.jsx +1 -0
  52. package/src/components/manage/Blocks/Video/VideoSidebar.jsx +1 -1
  53. package/src/components/manage/Controlpanels/Users/UsersControlpanel.jsx +14 -11
  54. package/src/components/manage/Widgets/NumberWidget.jsx +1 -1
  55. package/src/components/manage/Widgets/ObjectListWidget.jsx +19 -2
  56. package/src/components/theme/Error/ErrorBoundary.jsx +29 -0
  57. package/src/components/theme/Error/ErrorBoundary.test.jsx +34 -0
  58. package/src/components/theme/View/RenderBlocks.jsx +3 -1
  59. package/src/config/Style.jsx +9 -0
  60. package/src/config/index.js +2 -0
  61. package/src/helpers/Blocks/Blocks.js +37 -38
  62. package/src/helpers/Blocks/Blocks.test.js +64 -0
  63. package/src/helpers/MessageLabels/MessageLabels.js +18 -0
  64. package/test-setup-config.js +7 -0
@@ -19,6 +19,7 @@ const ListingData = (props) => {
19
19
  [id]: value,
20
20
  });
21
21
  }}
22
+ onChangeBlock={onChangeBlock}
22
23
  formData={data}
23
24
  block={block}
24
25
  />
@@ -39,6 +39,7 @@ const MapsSidebar = (props) => {
39
39
  [id]: value,
40
40
  });
41
41
  }}
42
+ onChangeBlock={onChangeBlock}
42
43
  formData={data}
43
44
  block={block}
44
45
  />
@@ -80,6 +80,7 @@ const SearchBlockEdit = (props) => {
80
80
  [id]: value,
81
81
  });
82
82
  }}
83
+ onChangeBlock={onChangeBlock}
83
84
  formData={data}
84
85
  />
85
86
  </SidebarPortal>
@@ -3,6 +3,7 @@ import { injectLazyLibs } from '@plone/volto/helpers/Loadable/Loadable';
3
3
  import {
4
4
  Option,
5
5
  DropdownIndicator,
6
+ MultiValueContainer,
6
7
  } from '@plone/volto/components/manage/Widgets/SelectStyling';
7
8
  import { selectTheme, customSelectStyles } from './SelectStyling';
8
9
  import {
@@ -32,7 +33,7 @@ const SelectFacet = (props) => {
32
33
  options={choices}
33
34
  styles={customSelectStyles}
34
35
  theme={selectTheme}
35
- components={{ DropdownIndicator, Option }}
36
+ components={{ DropdownIndicator, Option, MultiValueContainer }}
36
37
  isDisabled={isEditMode}
37
38
  onChange={(data) => {
38
39
  if (data) {
@@ -24,6 +24,7 @@ class Edit extends Component {
24
24
  [id]: value,
25
25
  });
26
26
  }}
27
+ onChangeBlock={this.props.onChangeBlock}
27
28
  formData={this.props.data}
28
29
  />
29
30
  </SidebarPortal>
@@ -39,8 +39,8 @@ const VideoSidebar = (props) => {
39
39
  [id]: value,
40
40
  });
41
41
  }}
42
+ onChangeBlock={onChangeBlock}
42
43
  formData={data}
43
- fieldIndex={data.index}
44
44
  block={block}
45
45
  />
46
46
  )}
@@ -140,7 +140,7 @@ class UsersControlpanel extends Component {
140
140
  (this.props.createRequest.loading && nextProps.createRequest.loaded)
141
141
  ) {
142
142
  this.props.listUsers({
143
- query: this.state.search,
143
+ search: this.state.search,
144
144
  });
145
145
  }
146
146
  if (this.props.createRequest.loading && nextProps.createRequest.loaded) {
@@ -172,7 +172,7 @@ class UsersControlpanel extends Component {
172
172
  onSearch(event) {
173
173
  event.preventDefault();
174
174
  this.props.listUsers({
175
- query: this.state.search,
175
+ search: this.state.search,
176
176
  });
177
177
  }
178
178
 
@@ -439,23 +439,27 @@ class UsersControlpanel extends Component {
439
439
  messages.addUserFormUsernameTitle,
440
440
  ),
441
441
  type: 'string',
442
- description:
443
- 'Enter a user name, usually something like "jsmith". No spaces or special characters. Usernames and passwords are case sensitive, make sure the caps lock key is not enabled. This is the name used to log in.',
442
+ description: this.props.intl.formatMessage(
443
+ messages.addUserFormUsernameDescription,
444
+ ),
444
445
  },
445
446
  fullname: {
446
447
  title: this.props.intl.formatMessage(
447
448
  messages.addUserFormFullnameTitle,
448
449
  ),
449
450
  type: 'string',
450
- description: 'Enter full name, e.g. John Smith.',
451
+ description: this.props.intl.formatMessage(
452
+ messages.addUserFormFullnameDescription,
453
+ ),
451
454
  },
452
455
  email: {
453
456
  title: this.props.intl.formatMessage(
454
457
  messages.addUserFormEmailTitle,
455
458
  ),
456
459
  type: 'string',
457
- description:
458
- 'Enter an email address. This is necessary in case the password is lost. We respect your privacy, and will not give the address away to any third parties or expose it anywhere.',
460
+ description: this.props.intl.formatMessage(
461
+ messages.addUserFormEmailDescription,
462
+ ),
459
463
  widget: 'email',
460
464
  },
461
465
  password: {
@@ -463,8 +467,9 @@ class UsersControlpanel extends Component {
463
467
  messages.addUserFormPasswordTitle,
464
468
  ),
465
469
  type: 'password',
466
- description:
467
- 'Enter your new password. Minimum 5 characters.',
470
+ description: this.props.intl.formatMessage(
471
+ messages.addUserFormPasswordDescription,
472
+ ),
468
473
  widget: 'password',
469
474
  },
470
475
  sendPasswordReset: {
@@ -480,7 +485,6 @@ class UsersControlpanel extends Component {
480
485
  type: 'array',
481
486
  choices: this.props.roles.map((role) => [role.id, role.id]),
482
487
  noValueOption: false,
483
- description: '',
484
488
  },
485
489
  groups: {
486
490
  title: this.props.intl.formatMessage(
@@ -492,7 +496,6 @@ class UsersControlpanel extends Component {
492
496
  group.id,
493
497
  ]),
494
498
  noValueOption: false,
495
- description: '',
496
499
  },
497
500
  },
498
501
  required: ['username', 'email'],
@@ -43,7 +43,7 @@ const NumberWidget = (props) => {
43
43
  disabled={isDisabled}
44
44
  min={minimum || null}
45
45
  max={maximum || null}
46
- value={value}
46
+ value={value ?? ''}
47
47
  placeholder={placeholder}
48
48
  onChange={({ target }) =>
49
49
  onChange(id, target.value === '' ? undefined : target.value)
@@ -57,7 +57,9 @@ const messages = defineMessages({
57
57
  * }
58
58
  * mutated.fieldsets[0].fields.push('extraField');
59
59
  * return mutated;
60
- * }
60
+ * },
61
+ * activeObject: 0, // Current active object drilled down from the schema (if present)
62
+ * setActiveObject: () => {} // The current active object state updater function drilled down from the schema (if present)
61
63
  * },
62
64
  * ```
63
65
  */
@@ -71,7 +73,22 @@ const ObjectListWidget = (props) => {
71
73
  onChange,
72
74
  schemaExtender,
73
75
  } = props;
74
- const [activeObject, setActiveObject] = React.useState(value.length - 1);
76
+ const [localActiveObject, setLocalActiveObject] = React.useState(
77
+ props.activeObject ?? value.length - 1,
78
+ );
79
+
80
+ let activeObject, setActiveObject;
81
+ if (
82
+ (props.activeObject || props.activeObject === 0) &&
83
+ props.setActiveObject
84
+ ) {
85
+ activeObject = props.activeObject;
86
+ setActiveObject = props.setActiveObject;
87
+ } else {
88
+ activeObject = localActiveObject;
89
+ setActiveObject = setLocalActiveObject;
90
+ }
91
+
75
92
  const intl = useIntl();
76
93
 
77
94
  function handleChangeActiveObject(e, blockProps) {
@@ -0,0 +1,29 @@
1
+ import React from 'react';
2
+
3
+ class ErrorBoundary extends React.Component {
4
+ constructor(props) {
5
+ super(props);
6
+ this.state = { hasError: false };
7
+ }
8
+
9
+ static getDerivedStateFromError(error) {
10
+ // Update state so the next render will show the fallback UI.
11
+ return { hasError: true };
12
+ }
13
+
14
+ componentDidCatch(error, errorInfo) {
15
+ // eslint-disable-next-line
16
+ console.error(error, errorInfo);
17
+ }
18
+
19
+ render() {
20
+ if (this.state.hasError) {
21
+ // You can render any custom fallback UI
22
+ return <pre className="error">{`<error: ${this.props.name}>`}</pre>;
23
+ }
24
+
25
+ return this.props.children;
26
+ }
27
+ }
28
+
29
+ export default ErrorBoundary;
@@ -0,0 +1,34 @@
1
+ import React from 'react';
2
+ import renderer from 'react-test-renderer';
3
+ import configureStore from 'redux-mock-store';
4
+ import { Provider } from 'react-intl-redux';
5
+ import { MemoryRouter } from 'react-router-dom';
6
+ import ErrorBoundary from './ErrorBoundary';
7
+
8
+ const mockStore = configureStore();
9
+
10
+ describe('Error boundary', () => {
11
+ it('renders an Error', () => {
12
+ const store = mockStore({
13
+ intl: {
14
+ locale: 'en',
15
+ messages: {},
16
+ },
17
+ });
18
+ const ThrowError = () => {
19
+ throw new Error('Test');
20
+ };
21
+
22
+ const component = renderer.create(
23
+ <Provider store={store}>
24
+ <MemoryRouter>
25
+ <ErrorBoundary name={'test'}>
26
+ <ThrowError />
27
+ </ErrorBoundary>
28
+ </MemoryRouter>
29
+ </Provider>,
30
+ );
31
+ const json = component.toJSON();
32
+ expect(json).toMatchSnapshot();
33
+ });
34
+ });
@@ -9,6 +9,7 @@ import {
9
9
  } from '@plone/volto/helpers';
10
10
  import StyleWrapper from '@plone/volto/components/manage/Blocks/Block/StyleWrapper';
11
11
  import config from '@plone/volto/registry';
12
+ import { ViewDefaultBlock } from '@plone/volto/components';
12
13
 
13
14
  const messages = defineMessages({
14
15
  unknownBlock: {
@@ -32,7 +33,8 @@ const RenderBlocks = (props) => {
32
33
  <CustomTag>
33
34
  {map(content[blocksLayoutFieldname].items, (block) => {
34
35
  const Block =
35
- blocksConfig[content[blocksFieldname]?.[block]?.['@type']]?.view;
36
+ blocksConfig[content[blocksFieldname]?.[block]?.['@type']]?.view ||
37
+ ViewDefaultBlock;
36
38
 
37
39
  const blockData = applyBlockDefaults({
38
40
  data: content[blocksFieldname][block],
@@ -0,0 +1,9 @@
1
+ export const styleClassNameConverters = {
2
+ default: (name, value, prefix = '') => {
3
+ return value
4
+ ? `has--${prefix}${name}--${(value || '').toString().replace(/^#/, '')}`
5
+ : null;
6
+ },
7
+ noprefix: (name, value) => value,
8
+ bool: (name, value) => (value ? name : ''),
9
+ };
@@ -24,6 +24,7 @@ import { loadables } from './Loadables';
24
24
  import { workflowMapping } from './Workflows';
25
25
 
26
26
  import { contentIcons } from './ContentIcons';
27
+ import { styleClassNameConverters } from './Style';
27
28
  import {
28
29
  controlPanelsIcons,
29
30
  filterControlPanels,
@@ -167,6 +168,7 @@ let config = {
167
168
  addonsInfo: addonsInfo,
168
169
  workflowMapping,
169
170
  errorHandlers: [], // callables for unhandled errors
171
+ styleClassNameConverters,
170
172
  },
171
173
  experimental: {
172
174
  addBlockButton: {
@@ -3,16 +3,7 @@
3
3
  * @module helpers/Blocks
4
4
  */
5
5
 
6
- import {
7
- omit,
8
- without,
9
- endsWith,
10
- find,
11
- isObject,
12
- keys,
13
- toPairs,
14
- merge,
15
- } from 'lodash';
6
+ import { omit, without, endsWith, find, isObject, keys, merge } from 'lodash';
16
7
  import move from 'lodash-move';
17
8
  import { v4 as uuid } from 'uuid';
18
9
  import config from '@plone/volto/registry';
@@ -450,35 +441,43 @@ export function applyBlockDefaults({ data, intl, ...rest }, blocksConfig) {
450
441
  return applySchemaDefaults({ data, schema, intl });
451
442
  }
452
443
 
453
- export const buildStyleClassNamesFromData = (styles) => {
454
- // styles has the form
444
+ /**
445
+ * Converts a name+value style pair (ex: color/red) to a classname,
446
+ * such as "has--color--red"
447
+ *
448
+ * This can be expanded via the style names, by suffixing them with special
449
+ * converters. See config.settings.styleClassNameConverters. Examples:
450
+ *
451
+ * styleToClassName('theme:noprefix', 'primary') returns "primary"
452
+ * styleToClassName('inverted:bool', true) returns 'inverted'
453
+ * styleToClassName('inverted:bool', false) returns ''
454
+ */
455
+ export const styleToClassName = (key, value, prefix = '') => {
456
+ const converters = config.settings.styleClassNameConverters;
457
+ const [name, ...convIds] = key.split(':');
458
+
459
+ return (convIds.length ? convIds : ['default'])
460
+ .map((id) => converters[id])
461
+ .reduce((acc, conv) => conv(acc, value, prefix), name);
462
+ };
463
+
464
+ export const buildStyleClassNamesFromData = (obj = {}, prefix = '') => {
465
+ // styles has the form:
455
466
  // const styles = {
456
- // color: 'red',
457
- // backgroundColor: '#AABBCC',
467
+ // color: 'red',
468
+ // backgroundColor: '#AABBCC',
458
469
  // }
459
470
  // Returns: ['has--color--red', 'has--backgroundColor--AABBCC']
460
- let styleArray = [];
461
- const pairedStyles = toPairs(styles);
462
- pairedStyles.forEach((item) => {
463
- if (isObject(item[1])) {
464
- const flattenedNestedStyles = toPairs(item[1]).map((nested) => [
465
- item[0],
466
- ...nested,
467
- ]);
468
- flattenedNestedStyles.forEach((sub) => styleArray.push(sub));
469
- } else {
470
- styleArray.push(item);
471
- }
472
- });
473
- return styleArray.map((item) => {
474
- const classname = item.map((item) => {
475
- const str_item = item ? item.toString() : '';
476
- return str_item && str_item.startsWith('#')
477
- ? str_item.replace('#', '')
478
- : str_item;
479
- });
480
- return `has--${classname[0]}--${classname[1]}${
481
- classname[2] ? `--${classname[2]}` : ''
482
- }`;
483
- });
471
+
472
+ return Object.entries(obj)
473
+ .reduce(
474
+ (acc, [k, v]) => [
475
+ ...acc,
476
+ ...(isObject(v)
477
+ ? buildStyleClassNamesFromData(v, `${prefix}${k}--`)
478
+ : [styleToClassName(k, v, prefix)]),
479
+ ],
480
+ [],
481
+ )
482
+ .filter((v) => !!v);
484
483
  };
@@ -819,6 +819,7 @@ describe('Blocks', () => {
819
819
  expect(applyBlockDefaults({ data })).toEqual({});
820
820
  });
821
821
  });
822
+
822
823
  describe('buildStyleClassNamesFromData', () => {
823
824
  it('Sets styles classname array according to style values', () => {
824
825
  const styles = {
@@ -830,6 +831,7 @@ describe('Blocks', () => {
830
831
  'has--backgroundColor--AABBCC',
831
832
  ]);
832
833
  });
834
+
833
835
  it('Sets styles classname array according to style values with nested', () => {
834
836
  const styles = {
835
837
  color: 'red',
@@ -846,6 +848,7 @@ describe('Blocks', () => {
846
848
  'has--nested--bar--black',
847
849
  ]);
848
850
  });
851
+
849
852
  it('Sets styles classname array according to style values with nested and colors', () => {
850
853
  const styles = {
851
854
  color: 'red',
@@ -863,6 +866,27 @@ describe('Blocks', () => {
863
866
  ]);
864
867
  });
865
868
 
869
+ it('Supports multiple nested level', () => {
870
+ const styles = {
871
+ color: 'red',
872
+ backgroundColor: '#AABBCC',
873
+ nested: {
874
+ l1: 'white',
875
+ level2: {
876
+ foo: '#fff',
877
+ bar: '#000',
878
+ },
879
+ },
880
+ };
881
+ expect(buildStyleClassNamesFromData(styles)).toEqual([
882
+ 'has--color--red',
883
+ 'has--backgroundColor--AABBCC',
884
+ 'has--nested--l1--white',
885
+ 'has--nested--level2--foo--fff',
886
+ 'has--nested--level2--bar--000',
887
+ ]);
888
+ });
889
+
866
890
  it('Sets styles classname array according to style values with int values', () => {
867
891
  const styles = {
868
892
  color: 'red',
@@ -873,5 +897,45 @@ describe('Blocks', () => {
873
897
  'has--borderRadius--8',
874
898
  ]);
875
899
  });
900
+
901
+ it('Understands noprefix converter for style values', () => {
902
+ const styles = {
903
+ color: 'red',
904
+ 'theme:noprefix': 'primary',
905
+ };
906
+ expect(buildStyleClassNamesFromData(styles)).toEqual([
907
+ 'has--color--red',
908
+ 'primary',
909
+ ]);
910
+ });
911
+
912
+ it('Understands bool converter for trueish value', () => {
913
+ const styles = {
914
+ color: 'red',
915
+ 'inverted:bool': true,
916
+ };
917
+ expect(buildStyleClassNamesFromData(styles)).toEqual([
918
+ 'has--color--red',
919
+ 'inverted',
920
+ ]);
921
+ });
922
+
923
+ it('Understands bool converter for false value', () => {
924
+ const styles = {
925
+ color: 'red',
926
+ 'inverted:bool': false,
927
+ };
928
+ expect(buildStyleClassNamesFromData(styles)).toEqual(['has--color--red']);
929
+ });
930
+
931
+ it('Ugly edge cases', () => {
932
+ const styles = {
933
+ color: undefined,
934
+ nested: {
935
+ l1: {},
936
+ },
937
+ };
938
+ expect(buildStyleClassNamesFromData(styles)).toEqual([]);
939
+ });
876
940
  });
877
941
  });
@@ -114,6 +114,24 @@ export const messages = defineMessages({
114
114
  id: 'Username',
115
115
  defaultMessage: 'Username',
116
116
  },
117
+ addUserFormUsernameDescription: {
118
+ id: 'addUserFormUsernameDescription',
119
+ defaultMessage:
120
+ 'Enter a user name, usually something like "jsmith". No spaces or special characters. Usernames and passwords are case sensitive, make sure the caps lock key is not enabled. This is the name used to log in.',
121
+ },
122
+ addUserFormFullnameDescription: {
123
+ id: 'addUserFormFullnameDescription',
124
+ defaultMessage: 'Enter full name, e.g. John Smith.',
125
+ },
126
+ addUserFormEmailDescription: {
127
+ id: 'addUserFormEmailDescription',
128
+ defaultMessage:
129
+ 'Enter an email address. This is necessary in case the password is lost. We respect your privacy, and will not give the address away to any third parties or expose it anywhere.',
130
+ },
131
+ addUserFormPasswordDescription: {
132
+ id: 'addUserFormPasswordDescription',
133
+ defaultMessage: 'Enter your new password. Minimum 8 characters.',
134
+ },
117
135
  addGroupsFormTitleTitle: {
118
136
  id: 'Title',
119
137
  defaultMessage: 'Title',
@@ -20,6 +20,7 @@ import {
20
20
  } from '@plone/volto/config/RichTextEditor/Blocks';
21
21
  import FromHTMLCustomBlockFn from '@plone/volto/config/RichTextEditor/FromHTML';
22
22
  import { contentIcons } from '@plone/volto/config/ContentIcons';
23
+ import { styleClassNameConverters } from '@plone/volto/config/Style';
23
24
 
24
25
  import {
25
26
  controlPanelsIcons,
@@ -74,6 +75,7 @@ config.set('settings', {
74
75
  apiExpanders: [],
75
76
  downloadableObjects: ['File'],
76
77
  viewableInBrowserObjects: [],
78
+ styleClassNameConverters,
77
79
  });
78
80
  config.set('blocks', {
79
81
  blocksConfig: {
@@ -163,3 +165,8 @@ config.set('widgets', {
163
165
  });
164
166
 
165
167
  config.set('components', {});
168
+ config.set('experimental', {
169
+ addBlockButton: {
170
+ enabled: false,
171
+ },
172
+ });