@plone/volto 19.0.0-alpha.22 → 19.0.0-alpha.24

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,37 @@ myst:
17
17
 
18
18
  <!-- towncrier release notes start -->
19
19
 
20
+ ## 19.0.0-alpha.24 (2026-02-12)
21
+
22
+ ### Bugfix
23
+
24
+ - Changed the order of the "Save" and "Cancel" buttons on the sharing page to improve accessibility. @Wagner3UB [#7835](https://github.com/plone/volto/issues/7835)
25
+ - Filter invalid block IDs (null, undefined, 'undefined') in getBlocks() to prevent duplicate elements in DOM and test failures. @aryan7081 [#7858](https://github.com/plone/volto/issues/7858)
26
+ - BlocksForm: remove invalid block layout items on save by iterating raw blocks_layout (using isValidBlockId) instead of relying on getBlocks(). @aryan7081 [#7859](https://github.com/plone/volto/issues/7859)
27
+ - Fix href override in UniversalLink component. @iFlameing
28
+
29
+ ### Internal
30
+
31
+ - Continue to prevent editors from automatically reformatting Markdown files by moving this configuration from VSCode to prettier.
32
+ Move the VSCode settings setup from `make install` to a pnpm post-install hook. @wesleybl [#7834](https://github.com/plone/volto/issues/7834)
33
+ - Increase wait time between link check retries with Lychee. @wesleybl [#7872](https://github.com/plone/volto/issues/7872)
34
+ - Update browserlist Feb2026. @sneridagh
35
+ - Upgrade Plone to use 6.2.0a1. @sneridagh
36
+
37
+ ### Documentation
38
+
39
+ - Document known development watcher issues and how to resolve them. @shivaansh0610-LUFFY [#7836](https://github.com/plone/volto/issues/7836)
40
+ - Updated links to old deprecated third-party theme in the documentation. @pnicolli [#7857](https://github.com/plone/volto/issues/7857)
41
+ - Removed add-on packages that no longer exist in the `main` branch from documentation. @wesleybl [#7887](https://github.com/plone/volto/issues/7887)
42
+
43
+ ## 19.0.0-alpha.23 (2026-02-03)
44
+
45
+ ### Bugfix
46
+
47
+ - Language control panel: fix validation of default language. @davisagli [#7720](https://github.com/plone/volto/issues/7720)
48
+ - Set HTTP 503 status code for ConnectionRefused error page. @Shyam-Raghuwanshi [#7754](https://github.com/plone/volto/issues/7754)
49
+ - Fix default case selection in ButtonsWidget. @iFlameing
50
+
20
51
  ## 19.0.0-alpha.22 (2026-01-26)
21
52
 
22
53
  ### Feature
package/README.md CHANGED
@@ -138,7 +138,6 @@ To ensure your website gets the greatest exposure, add it both to [Awesome Volto
138
138
  - [Debabarreneko mankomunitatea](https://debabarrena.eus/eu) (Website of the Commonwealth of Debabarrena, community of municipalities to centralize waste handling services, developed by [CodeSyntax](https://www.codesyntax.com/en), 2022)
139
139
  - [Debako Udala / Ayuntamiento de Deba](https://www.deba.eus/eu) (Website of the municipality of Deba, developed by [CodeSyntax](https://www.codesyntax.com/en), 2022)
140
140
  - [European Environment Agency](https://www.eea.europa.eu/en) (Website of the European Environment Agency. Developed by [Eau de Web](https://eaudeweb.ro), 2023)
141
- - Excellence at Humboldt-Universität zu Berlin (Website for the excellence initiative of the [Humboldt University Berlin](https://www.hu-berlin.de), developed by [kitconcept GmbH](https://kitconcept.com/en), 2019)
142
141
  - [Film Basque Country](https://www.filmbasquecountry.eus/en) (Website to attract, guide, and support international productions, making it easier for them to film in the Basque Country, developed by [CodeSyntax](https://www.codesyntax.com/en), 2025)
143
142
  - [Forest Information System for Europe](https://forest.eea.europa.eu) (Thematic website focusing on European forests, developed by [Eau de Web](https://eaudeweb.ro/), 2019)
144
143
  - [Forschungszentrum Jülich](https://www.fz-juelich.de/de) (Website for Forschungzentrum Jülich, which is one of the largest research institutions in Europe, developed by [kitconcept GmbH](https://kitconcept.com/en), 2022)
package/package.json CHANGED
@@ -9,7 +9,7 @@
9
9
  }
10
10
  ],
11
11
  "license": "MIT",
12
- "version": "19.0.0-alpha.22",
12
+ "version": "19.0.0-alpha.24",
13
13
  "repository": {
14
14
  "type": "git",
15
15
  "url": "git@github.com:plone/volto.git"
@@ -105,8 +105,8 @@
105
105
  "connected-react-router": "6.8.0",
106
106
  "debug": "4.3.4",
107
107
  "decorate-component-with-props": "1.2.1",
108
- "dependency-graph": "0.10.0",
109
108
  "deepmerge": "^4.2.2",
109
+ "dependency-graph": "0.10.0",
110
110
  "detect-browser": "5.1.0",
111
111
  "diff": "3.5.0",
112
112
  "express": "4.19.2",
@@ -129,8 +129,8 @@
129
129
  "moment": "2.29.4",
130
130
  "object-assign": "4.1.1",
131
131
  "prepend-http": "2",
132
- "pretty-bytes": "5.3.0",
133
132
  "prettier": "3.2.5",
133
+ "pretty-bytes": "5.3.0",
134
134
  "prismjs": "1.27.0",
135
135
  "process": "^0.11.10",
136
136
  "promise-file-reader": "1.0.2",
@@ -191,9 +191,9 @@
191
191
  "use-deep-compare-effect": "1.8.1",
192
192
  "uuid": "^8.3.2",
193
193
  "@plone/components": "4.0.0-alpha.4",
194
- "@plone/scripts": "4.0.0-alpha.4",
195
- "@plone/registry": "3.0.0-alpha.9",
196
- "@plone/volto-slate": "19.0.0-alpha.9"
194
+ "@plone/scripts": "4.0.0-alpha.5",
195
+ "@plone/volto-slate": "19.0.0-alpha.10",
196
+ "@plone/registry": "3.0.0-alpha.9"
197
197
  },
198
198
  "devDependencies": {
199
199
  "@babel/core": "^7.28.5",
@@ -308,8 +308,8 @@
308
308
  "webpack-dev-server": "4.11.1",
309
309
  "webpack-node-externals": "3.0.0",
310
310
  "@plone/razzle": "1.0.0-alpha.0",
311
+ "@plone/types": "2.0.0-alpha.14",
311
312
  "@plone/babel-preset-razzle": "^1.0.0-alpha.0",
312
- "@plone/types": "2.0.0-alpha.13",
313
313
  "@plone/volto-coresandbox": "1.0.0"
314
314
  },
315
315
  "volta": {
@@ -8,6 +8,7 @@ import {
8
8
  getBlocks,
9
9
  getBlocksFieldname,
10
10
  getBlocksLayoutFieldname,
11
+ getInvalidBlockLayoutIds,
11
12
  applyBlockDefaults,
12
13
  getBlocksHierarchy,
13
14
  addBlock,
@@ -260,18 +261,18 @@ const BlocksForm = (props) => {
260
261
  const editBlockWrapper = children || defaultBlockWrapper;
261
262
 
262
263
  // Remove invalid blocks on saving
263
- // Note they are alreaady filtered by DragDropList, but we also want them
264
+ // Note they are already filtered by DragDropList, but we also want them
264
265
  // to be removed when the user saves the page next. Otherwise the invalid
265
266
  // blocks would linger for ever.
266
-
267
- const blocksLayoutFieldname = getBlocksLayoutFieldname(properties);
268
- const blocksFieldname = getBlocksFieldname(properties);
269
- for (const id of properties?.[blocksLayoutFieldname]?.items || []) {
270
- if (!properties?.[blocksFieldname]?.[id]) {
271
- const newFormData = deleteBlock(properties, id, intl);
272
- onChangeFormData(newFormData);
267
+ useEffect(() => {
268
+ const invalidBlockIds = getInvalidBlockLayoutIds(properties);
269
+ if (invalidBlockIds.length === 0) return;
270
+ let newFormData = properties;
271
+ for (const id of invalidBlockIds) {
272
+ newFormData = deleteBlock(newFormData, id, intl);
273
273
  }
274
- }
274
+ onChangeFormData(newFormData);
275
+ }, [properties, intl, onChangeFormData]);
275
276
 
276
277
  useEvent('voltoClickBelowContent', () => {
277
278
  if (!config.experimental.addBlockButton.enabled || !isMainForm) return;
@@ -157,19 +157,12 @@ test('Removes invalid blocks on saving', () => {
157
157
  </Provider>,
158
158
  );
159
159
 
160
+ expect(onChangeFormData).toHaveBeenCalledTimes(1);
160
161
  expect(onChangeFormData).toHaveBeenCalledWith({
161
162
  blocks: {
162
163
  a: { '@type': 'custom', text: 'a' },
163
164
  b: { '@type': 'custom', text: 'b' },
164
165
  },
165
- blocks_layout: { items: ['a', 'b', 'MISSING-YOU-1'] },
166
- });
167
-
168
- expect(onChangeFormData).toHaveBeenCalledWith({
169
- blocks: {
170
- a: { '@type': 'custom', text: 'a' },
171
- b: { '@type': 'custom', text: 'b' },
172
- },
173
- blocks_layout: { items: ['a', 'b', 'MISSING-YOU-2'] },
166
+ blocks_layout: { items: ['a', 'b'] },
174
167
  });
175
168
  });
@@ -485,11 +485,19 @@ class SharingComponent extends Component {
485
485
  />
486
486
  </p>
487
487
  </Segment>
488
- <Segment className="actions" attached clearing>
488
+ <Segment className="right aligned actions" attached clearing>
489
+ <Button
490
+ basic
491
+ secondary
492
+ aria-label={this.props.intl.formatMessage(messages.cancel)}
493
+ title={this.props.intl.formatMessage(messages.cancel)}
494
+ onClick={this.onCancel}
495
+ >
496
+ <Icon className="circled" name={clearSVG} size="30px" />
497
+ </Button>
489
498
  <Button
490
499
  basic
491
500
  primary
492
- floated="right"
493
501
  type="submit"
494
502
  aria-label={this.props.intl.formatMessage(messages.save)}
495
503
  title={this.props.intl.formatMessage(messages.save)}
@@ -498,16 +506,6 @@ class SharingComponent extends Component {
498
506
  >
499
507
  <Icon className="circled" name={aheadSVG} size="30px" />
500
508
  </Button>
501
- <Button
502
- basic
503
- secondary
504
- aria-label={this.props.intl.formatMessage(messages.cancel)}
505
- title={this.props.intl.formatMessage(messages.cancel)}
506
- floated="right"
507
- onClick={this.onCancel}
508
- >
509
- <Icon className="circled" name={clearSVG} size="30px" />
510
- </Button>
511
509
  </Segment>
512
510
  </Form>
513
511
  </Plug>
@@ -89,6 +89,22 @@ describe('UniversalLink', () => {
89
89
  );
90
90
  });
91
91
 
92
+ it('check UniversalLink append http when user has not entered the protocol', () => {
93
+ const { getByTitle } = render(
94
+ <Provider store={store}>
95
+ <MemoryRouter>
96
+ <UniversalLink href="www.github.com" title="Volto GitHub repository">
97
+ <h1>Title</h1>
98
+ </UniversalLink>
99
+ </MemoryRouter>
100
+ </Provider>,
101
+ );
102
+
103
+ expect(getByTitle('Volto GitHub repository').getAttribute('href')).toBe(
104
+ 'http://www.github.com',
105
+ );
106
+ });
107
+
92
108
  it('check UniversalLink set target attribute for ext links', () => {
93
109
  const { getByTitle } = render(
94
110
  <Provider store={store}>
@@ -122,6 +122,7 @@ const UniversalLink = React.memo(
122
122
  onClick,
123
123
  onKeyDown,
124
124
  item,
125
+ href,
125
126
  ...rest
126
127
  } = props;
127
128
  __test.renderCounter();
@@ -1,4 +1,4 @@
1
- import React from 'react';
1
+ import React, { useEffect } from 'react';
2
2
  import FormFieldWrapper from '@plone/volto/components/manage/Widgets/FormFieldWrapper';
3
3
  import Icon from '@plone/volto/components/theme/Icon/Icon';
4
4
  import { Radio, RadioGroup } from '@plone/components';
@@ -130,6 +130,25 @@ const ButtonsWidget = (props: ButtonsWidgetProps) => {
130
130
  }
131
131
  };
132
132
 
133
+ // Synchronize default value if no value is set to data prop.
134
+ useEffect(() => {
135
+ // If `value` already matches any normalized action value, do nothing.
136
+ const existingMatch = normalizedActions.find(({ value: actionValue }) =>
137
+ isEqual(value, actionValue),
138
+ );
139
+
140
+ if (existingMatch) return;
141
+
142
+ // Otherwise, if there's a default value set the default style.
143
+ if (!defaultSelectedActionName) return;
144
+ const selected = normalizedActions.find(
145
+ ({ name }) => name === defaultSelectedActionName,
146
+ );
147
+ if (!selected) return;
148
+
149
+ onChange(id, selected.value);
150
+ }, [value, defaultSelectedActionName, onChange, normalizedActions, id]);
151
+
133
152
  return (
134
153
  <FormFieldWrapper {...props} className="widget">
135
154
  <RadioGroup
@@ -176,7 +176,9 @@ export class App extends Component {
176
176
  <main ref={this.mainRef}>
177
177
  <OutdatedBrowser />
178
178
  {this.props.connectionRefused ? (
179
- <ConnectionRefusedView />
179
+ <ConnectionRefusedView
180
+ staticContext={this.props.staticContext}
181
+ />
180
182
  ) : this.state.hasError ? (
181
183
  <Error
182
184
  message={this.state.error.message}
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Home container.
2
+ * Connection refused error page.
3
3
  * @module components/theme/ConnectionRefused/ConnectionRefused
4
4
  */
5
5
 
@@ -7,6 +7,7 @@ import React from 'react';
7
7
  import { FormattedMessage } from 'react-intl';
8
8
  import { Container } from 'semantic-ui-react';
9
9
  import config from '@plone/volto/registry';
10
+ import { withServerErrorCode } from '@plone/volto/helpers/Utils/Utils';
10
11
 
11
12
  const ConnectionRefused = () => (
12
13
  <Container
@@ -71,4 +72,4 @@ const ConnectionRefused = () => (
71
72
  </Container>
72
73
  );
73
74
 
74
- export default ConnectionRefused;
75
+ export default withServerErrorCode(503)(ConnectionRefused);
@@ -5,8 +5,7 @@ import map from 'lodash/map';
5
5
  import MaybeWrap from '@plone/volto/components/manage/MaybeWrap/MaybeWrap';
6
6
  import {
7
7
  applyBlockDefaults,
8
- getBlocksFieldname,
9
- getBlocksLayoutFieldname,
8
+ getBlocks,
10
9
  hasBlocksData,
11
10
  } from '@plone/volto/helpers/Blocks/Blocks';
12
11
  import StyleWrapper from '@plone/volto/components/manage/Blocks/Block/StyleWrapper';
@@ -28,26 +27,25 @@ const messages = defineMessages({
28
27
  const RenderBlocks = (props) => {
29
28
  const { blockWrapperTag, content, location, isContainer, metadata } = props;
30
29
  const intl = useIntl();
31
- const blocksFieldname = getBlocksFieldname(content);
32
- const blocksLayoutFieldname = getBlocksLayoutFieldname(content);
33
30
  const blocksConfig = props.blocksConfig || config.blocks.blocksConfig;
34
31
  const CustomTag = props.as || React.Fragment;
35
32
 
33
+ const blockList = getBlocks(content);
34
+
36
35
  return hasBlocksData(content) ? (
37
36
  <CustomTag>
38
- {map(content[blocksLayoutFieldname].items, (block) => {
37
+ {map(blockList, ([block, rawBlockData]) => {
39
38
  const Block =
40
- blocksConfig[content[blocksFieldname]?.[block]?.['@type']]?.view ||
41
- ViewDefaultBlock;
39
+ blocksConfig[rawBlockData?.['@type']]?.view || ViewDefaultBlock;
42
40
 
43
41
  const blockData = applyBlockDefaults({
44
- data: content[blocksFieldname][block],
42
+ data: rawBlockData,
45
43
  intl,
46
44
  metadata,
47
45
  properties: content,
48
46
  });
49
47
 
50
- if (content[blocksFieldname]?.[block]?.['@type'] === 'empty') {
48
+ if (rawBlockData?.['@type'] === 'empty') {
51
49
  return (
52
50
  <MaybeWrap
53
51
  key={block}
@@ -91,7 +89,7 @@ const RenderBlocks = (props) => {
91
89
  return (
92
90
  <div key={block}>
93
91
  {intl.formatMessage(messages.unknownBlock, {
94
- block: content[blocksFieldname]?.[block]?.['@type'],
92
+ block: rawBlockData?.['@type'],
95
93
  })}
96
94
  </div>
97
95
  );
@@ -90,14 +90,14 @@ test('Provides path to blocks', () => {
90
90
  expect(container).toMatchSnapshot();
91
91
  });
92
92
 
93
- test('Renders invalid blocks', () => {
93
+ test('Filters out invalid blocks', () => {
94
94
  const store = mockStore({
95
95
  intl: {
96
96
  locale: 'en',
97
97
  messages: {},
98
98
  },
99
99
  });
100
- const { queryAllByText } = render(
100
+ const { queryAllByText, queryByText } = render(
101
101
  <Provider store={store}>
102
102
  <RenderBlocks
103
103
  blocksConfig={{
@@ -113,7 +113,14 @@ test('Renders invalid blocks', () => {
113
113
  }}
114
114
  content={{
115
115
  blocks_layout: {
116
- items: ['MISSING-YOU-1', 'a', 'MISSING-YOU-2'],
116
+ items: [
117
+ 'MISSING-YOU-1',
118
+ 'a',
119
+ 'MISSING-YOU-2',
120
+ null,
121
+ undefined,
122
+ 'undefined',
123
+ ],
117
124
  },
118
125
  blocks: {
119
126
  a: {
@@ -126,7 +133,10 @@ test('Renders invalid blocks', () => {
126
133
  />
127
134
  </Provider>,
128
135
  );
136
+ // Invalid blocks (missing from blocks object or invalid IDs) are filtered out and not rendered
129
137
  expect(
130
138
  queryAllByText('Invalid block - Will be removed on saving'),
131
- ).toHaveLength(2);
139
+ ).toHaveLength(0);
140
+ // Only valid blocks are rendered
141
+ expect(queryByText('id: a - text: bar - path: /foo')).not.toBeNull();
132
142
  });
@@ -80,6 +80,14 @@ export function blockHasValue(data) {
80
80
  return check(data);
81
81
  }
82
82
 
83
+ /**
84
+ * Block id is valid (not undefined/null or the string "undefined" from object[undefined])
85
+ * @param {*} id Block id
86
+ * @return {boolean}
87
+ */
88
+ const isValidBlockId = (id) =>
89
+ id != null && id !== 'undefined' && (typeof id !== 'string' || id.length > 0);
90
+
83
91
  /**
84
92
  * Get block pairs of [id, block] from content properties
85
93
  * @function getBlocks
@@ -89,11 +97,26 @@ export function blockHasValue(data) {
89
97
  export const getBlocks = (properties) => {
90
98
  const blocksFieldName = getBlocksFieldname(properties);
91
99
  const blocksLayoutFieldname = getBlocksLayoutFieldname(properties);
92
- return (
93
- properties?.[blocksLayoutFieldname]?.items
94
- ?.map((n) => [n, properties?.[blocksFieldName]?.[n]])
95
- .filter(([, block]) => block !== undefined) || []
96
- );
100
+ const blocks = properties?.[blocksFieldName];
101
+ const items = properties?.[blocksLayoutFieldname]?.items;
102
+ if (!items) return [];
103
+ return items
104
+ .filter((n) => isValidBlockId(n))
105
+ .map((n) => [n, blocks?.[n]])
106
+ .filter(([, block]) => block != null);
107
+ };
108
+
109
+ /**
110
+ * Get layout item IDs that are valid but have no block data (orphaned refs).
111
+ * @param {Object} properties Content form properties
112
+ * @return {string[]} IDs that should be removed from layout
113
+ */
114
+ export const getInvalidBlockLayoutIds = (properties) => {
115
+ const blocksFieldName = getBlocksFieldname(properties);
116
+ const blocksLayoutFieldName = getBlocksLayoutFieldname(properties);
117
+ const blocks = properties?.[blocksFieldName] ?? {};
118
+ const layoutItems = properties?.[blocksLayoutFieldName]?.items ?? [];
119
+ return layoutItems.filter((id) => isValidBlockId(id) && blocks[id] == null);
97
120
  };
98
121
 
99
122
  /**
@@ -7,6 +7,7 @@ import {
7
7
  getBlocks,
8
8
  getBlocksFieldname,
9
9
  getBlocksLayoutFieldname,
10
+ getInvalidBlockLayoutIds,
10
11
  hasBlocksData,
11
12
  insertBlock,
12
13
  moveBlock,
@@ -482,6 +483,105 @@ describe('Blocks', () => {
482
483
  ['a', { value: 1 }],
483
484
  ]);
484
485
  });
486
+
487
+ it('filters out invalid block IDs (null, undefined, string "undefined")', () => {
488
+ const validBlock = { '@type': 'search', value: 'test' };
489
+ const result = getBlocks({
490
+ blocks: {
491
+ 'valid-id-123': validBlock,
492
+ // These shouldn't exist but test edge case
493
+ [null]: { '@type': 'invalid' },
494
+ [undefined]: { '@type': 'invalid' },
495
+ undefined: { '@type': 'invalid' },
496
+ },
497
+ blocks_layout: {
498
+ items: [
499
+ 'valid-id-123',
500
+ null, // Invalid: null ID
501
+ undefined, // Invalid: undefined ID
502
+ 'undefined', // Invalid: string "undefined"
503
+ 'missing-block', // Valid ID but block doesn't exist (filtered by block !== undefined)
504
+ ],
505
+ },
506
+ });
507
+
508
+ // Should only return the valid block, filtering out:
509
+ // - null ID
510
+ // - undefined ID
511
+ // - string "undefined" ID
512
+ // - missing block (block is undefined)
513
+ expect(result).toStrictEqual([['valid-id-123', validBlock]]);
514
+ expect(result.length).toBe(1);
515
+
516
+ // Verify no invalid IDs in the result
517
+ const ids = result.map(([id]) => id);
518
+ expect(ids).not.toContain(null);
519
+ expect(ids).not.toContain(undefined);
520
+ expect(ids).not.toContain('undefined');
521
+ });
522
+
523
+ it('filters out invalid block IDs even when blocks object has invalid keys', () => {
524
+ // Simulate edge case where blocks object has invalid keys
525
+ const blocks = {
526
+ 'valid-id': { '@type': 'search' },
527
+ };
528
+ // JavaScript allows this, creating string keys
529
+ blocks[null] = { '@type': 'invalid' };
530
+ blocks[undefined] = { '@type': 'invalid' };
531
+
532
+ const result = getBlocks({
533
+ blocks,
534
+ blocks_layout: {
535
+ items: ['valid-id', null, undefined, 'undefined'],
536
+ },
537
+ });
538
+
539
+ // Should only return valid block
540
+ expect(result).toStrictEqual([['valid-id', { '@type': 'search' }]]);
541
+ expect(result.length).toBe(1);
542
+ });
543
+ });
544
+
545
+ describe('getInvalidBlockLayoutIds', () => {
546
+ it('returns layout IDs that are valid but have no block data', () => {
547
+ const result = getInvalidBlockLayoutIds({
548
+ blocks: {
549
+ a: { '@type': 'custom', text: 'a' },
550
+ b: { '@type': 'custom', text: 'b' },
551
+ },
552
+ blocks_layout: {
553
+ items: ['a', 'b', 'MISSING-1', 'MISSING-2'],
554
+ },
555
+ });
556
+ expect(result).toStrictEqual(['MISSING-1', 'MISSING-2']);
557
+ });
558
+
559
+ it('returns empty when all layout items have block data', () => {
560
+ const result = getInvalidBlockLayoutIds({
561
+ blocks: { a: { '@type': 'custom' }, b: { '@type': 'custom' } },
562
+ blocks_layout: { items: ['a', 'b'] },
563
+ });
564
+ expect(result).toStrictEqual([]);
565
+ });
566
+
567
+ it('filters out invalid IDs (null, undefined, "undefined")', () => {
568
+ const result = getInvalidBlockLayoutIds({
569
+ blocks: {},
570
+ blocks_layout: {
571
+ items: [null, undefined, 'undefined', 'valid-missing'],
572
+ },
573
+ });
574
+ expect(result).toStrictEqual(['valid-missing']);
575
+ });
576
+
577
+ it('returns empty when items is missing or empty', () => {
578
+ expect(
579
+ getInvalidBlockLayoutIds({ blocks: {}, blocks_layout: {} }),
580
+ ).toStrictEqual([]);
581
+ expect(
582
+ getInvalidBlockLayoutIds({ blocks: {}, blocks_layout: { items: [] } }),
583
+ ).toStrictEqual([]);
584
+ });
485
585
  });
486
586
 
487
587
  describe('addBlock', () => {
@@ -15,6 +15,17 @@ type Validator = {
15
15
  formatMessage: Function;
16
16
  };
17
17
 
18
+ type Choice = {
19
+ token: string;
20
+ label: string;
21
+ };
22
+ type ChoiceValidator = {
23
+ value: string | Choice;
24
+ field: Record<string, any>;
25
+ formData: any;
26
+ formatMessage: Function;
27
+ };
28
+
18
29
  export const isMaxPropertyValid = ({
19
30
  value,
20
31
  fieldSpec,
@@ -211,12 +222,13 @@ export const defaultLanguageControlPanelValidator = ({
211
222
  value,
212
223
  formData,
213
224
  formatMessage,
214
- }: Validator) => {
225
+ }: ChoiceValidator) => {
226
+ const token = typeof value === 'object' ? value.token : value;
215
227
  const isValid =
216
- value &&
228
+ token &&
217
229
  (formData.available_languages.find(
218
- (lang: { token: string }) => lang.token === value,
230
+ (lang: { token: string }) => lang.token === token,
219
231
  ) ||
220
- formData.available_languages.includes(value));
232
+ formData.available_languages.includes(token));
221
233
  return !isValid ? formatMessage(messages.defaultLanguage) : null;
222
234
  };
@@ -1,2 +1,2 @@
1
- export default ConnectionRefused;
2
- declare function ConnectionRefused(): import("react/jsx-runtime").JSX.Element;
1
+ declare const _default: (props: any) => import("react/jsx-runtime").JSX.Element;
2
+ export default _default;
@@ -42,7 +42,7 @@ export const errorViews: {
42
42
  403: (props: any) => import("react/jsx-runtime").JSX.Element;
43
43
  408: () => string;
44
44
  500: (props: any) => import("react/jsx-runtime").JSX.Element;
45
- ECONNREFUSED: () => import("react/jsx-runtime").JSX.Element;
45
+ ECONNREFUSED: (props: any) => import("react/jsx-runtime").JSX.Element;
46
46
  corsError: () => string;
47
47
  };
48
48
  export namespace layoutViewsNamesMapping {
@@ -176,6 +176,7 @@ export function findBlocks(blocks: {}, types: any, result?: any[]): any[];
176
176
  */
177
177
  export function moveBlockEnhanced(formData: any, { source, destination }: number): any;
178
178
  export function getBlocks(properties: any): any[];
179
+ export function getInvalidBlockLayoutIds(properties: any): string[];
179
180
  export function applyBlockInitialValue({ id, value, blocksConfig, formData, intl, }: {
180
181
  id: any;
181
182
  value: any;
@@ -10,6 +10,16 @@ type Validator = {
10
10
  formData: any;
11
11
  formatMessage: Function;
12
12
  };
13
+ type Choice = {
14
+ token: string;
15
+ label: string;
16
+ };
17
+ type ChoiceValidator = {
18
+ value: string | Choice;
19
+ field: Record<string, any>;
20
+ formData: any;
21
+ formatMessage: Function;
22
+ };
13
23
  export declare const isMaxPropertyValid: ({ value, fieldSpec, criterion, formatMessage, }: MinMaxValidator) => any;
14
24
  export declare const isMinPropertyValid: ({ value, fieldSpec, criterion, formatMessage, }: MinMaxValidator) => any;
15
25
  export declare const minLengthValidator: ({ value, field, formatMessage, }: Validator) => any;
@@ -26,5 +36,5 @@ export declare const endEventDateRangeValidator: ({ value, field, formData, form
26
36
  export declare const patternValidator: ({ value, field, formatMessage, }: Validator) => any;
27
37
  export declare const maxItemsValidator: ({ value, field, formatMessage, }: Validator) => any;
28
38
  export declare const minItemsValidator: ({ value, field, formatMessage, }: Validator) => any;
29
- export declare const defaultLanguageControlPanelValidator: ({ value, formData, formatMessage, }: Validator) => any;
39
+ export declare const defaultLanguageControlPanelValidator: ({ value, formData, formatMessage, }: ChoiceValidator) => any;
30
40
  export {};