@kitconcept/volto-light-theme 7.5.0 → 7.6.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.
package/.changelog.draft CHANGED
@@ -1,7 +1,16 @@
1
- ## 7.5.0 (2025-10-31)
1
+ ## 7.6.0 (2025-11-04)
2
+
3
+ ### Feature
4
+
5
+ - Recoverable Block Error Boundaries. @sneridagh [#708](https://github.com/kitconcept/volto-light-theme/pull/708)
6
+
7
+ ### Bugfix
8
+
9
+ - Fix Teaser Blocks for Person type email spacing in 4 columns gridBlock @Tishasoumya-02
2
10
 
3
11
  ### Internal
4
12
 
5
- - Better pinned versions. Use volto-button-block 4.0.0 final. @sneridagh
13
+ - Add css for NotFound Page @Tishasoumya-02
14
+ - Use Volto 18.29.1. @sneridagh
6
15
 
7
16
 
package/CHANGELOG.md CHANGED
@@ -8,6 +8,27 @@
8
8
 
9
9
  <!-- towncrier release notes start -->
10
10
 
11
+ ## 7.6.0 (2025-11-04)
12
+
13
+ ### Feature
14
+
15
+ - Recoverable Block Error Boundaries. @sneridagh [#708](https://github.com/kitconcept/volto-light-theme/pull/708)
16
+
17
+ ### Bugfix
18
+
19
+ - Fix Teaser Blocks for Person type email spacing in 4 columns gridBlock @Tishasoumya-02
20
+
21
+ ### Internal
22
+
23
+ - Add css for NotFound Page @Tishasoumya-02
24
+ - Use Volto 18.29.1. @sneridagh
25
+
26
+ ## 7.5.1 (2025-10-31)
27
+
28
+ ### Bugfix
29
+
30
+ - [#705](https://github.com/kitconcept/volto-light-theme/pull/705)
31
+
11
32
  ## 7.5.0 (2025-10-31)
12
33
 
13
34
  ### Internal
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kitconcept/volto-light-theme",
3
- "version": "7.5.0",
3
+ "version": "7.6.0",
4
4
  "description": "Volto Light Theme by kitconcept",
5
5
  "main": "src/index.ts",
6
6
  "types": "src/index.ts",
@@ -45,7 +45,7 @@
45
45
  "release-it": "^19.0.3",
46
46
  "typescript": "^5.7.3",
47
47
  "vitest": "^3.1.2",
48
- "@plone/types": "1.4.5"
48
+ "@plone/types": "1.5.0"
49
49
  },
50
50
  "dependencies": {
51
51
  "@dnd-kit/core": "6.0.8",
@@ -35,6 +35,7 @@ import {
35
35
  buildStyleClassNamesExtenders,
36
36
  } from '@plone/volto/helpers/Blocks/Blocks';
37
37
  import MaybeWrap from '@plone/volto/components/manage/MaybeWrap/MaybeWrap';
38
+ import ErrorBoundary from './ErrorBoundary';
38
39
 
39
40
  const messages = defineMessages({
40
41
  unknownBlock: {
@@ -287,11 +288,18 @@ export class Edit extends Component {
287
288
  as={'div'}
288
289
  className="block-inner-container"
289
290
  >
290
- <Block
291
- {...this.props}
292
- blockNode={this.blockNode}
293
- data={this.props.data}
294
- />
291
+ <ErrorBoundary
292
+ name={`blockId-${this.props.id}-type-${type}`}
293
+ block={this.props.block}
294
+ type={type}
295
+ isEdit
296
+ >
297
+ <Block
298
+ {...this.props}
299
+ blockNode={this.blockNode}
300
+ data={this.props.data}
301
+ />
302
+ </ErrorBoundary>
295
303
  </MaybeWrap>
296
304
 
297
305
  {blocksConfig?.[type]?.blockModel === 3 && (
@@ -0,0 +1,55 @@
1
+ import React, { type FC } from 'react';
2
+ import { render, screen } from '@testing-library/react';
3
+ import { afterAll, afterEach, describe, expect, it, vi } from 'vitest';
4
+ import { Provider } from 'react-intl-redux';
5
+ import { ErrorBoundary } from './ErrorBoundary';
6
+ import configureStore from 'redux-mock-store';
7
+
8
+ describe('Error boundary', () => {
9
+ const consoleErrorSpy = vi
10
+ .spyOn(console, 'error')
11
+ .mockImplementation(() => undefined);
12
+
13
+ afterEach(() => {
14
+ consoleErrorSpy.mockClear();
15
+ });
16
+
17
+ afterAll(() => {
18
+ consoleErrorSpy.mockRestore();
19
+ });
20
+
21
+ const mockStore = configureStore();
22
+
23
+ const store = mockStore({
24
+ intl: {
25
+ locale: 'en',
26
+ messages: {},
27
+ },
28
+ });
29
+
30
+ it('renders fallback UI when a child throws', () => {
31
+ const ThrowError: FC = () => {
32
+ throw new Error('Test');
33
+ };
34
+
35
+ const { container } = render(
36
+ <Provider store={store}>
37
+ <ErrorBoundary
38
+ name="test"
39
+ block="123"
40
+ type="slate"
41
+ blocks={null}
42
+ blocksLayout={null}
43
+ title={null}
44
+ >
45
+ <ThrowError />
46
+ </ErrorBoundary>
47
+ </Provider>,
48
+ );
49
+
50
+ expect(screen.getByText('Block error:')).toBeInTheDocument();
51
+ expect(
52
+ container.querySelector('.block-error-boundary .title'),
53
+ ).toBeInTheDocument();
54
+ });
55
+ });
@@ -0,0 +1,92 @@
1
+ import React from 'react';
2
+ import type { ErrorInfo, ReactNode } from 'react';
3
+ import { connect } from 'react-redux';
4
+ import type { BlocksData } from '@plone/types';
5
+ import ErrorBoundaryMessage from './ErrorBoundaryMessage';
6
+
7
+ type OwnProps = {
8
+ name?: string;
9
+ block?: string;
10
+ type?: string;
11
+ isEdit?: boolean;
12
+ children?: ReactNode;
13
+ };
14
+
15
+ type StateProps = {
16
+ blocks: BlocksData['blocks'];
17
+ blocksLayout: BlocksData['blocks_layout'];
18
+ title: string | null;
19
+ };
20
+
21
+ type ErrorBoundaryState = {
22
+ hasError: boolean;
23
+ };
24
+
25
+ type ErrorBoundaryProps = OwnProps & StateProps;
26
+
27
+ export class ErrorBoundary extends React.Component<
28
+ ErrorBoundaryProps,
29
+ ErrorBoundaryState
30
+ > {
31
+ state: ErrorBoundaryState = { hasError: false };
32
+
33
+ static getDerivedStateFromError(_error: Error): ErrorBoundaryState {
34
+ // Update state so the next render will show the fallback UI.
35
+ return { hasError: true };
36
+ }
37
+
38
+ componentDidUpdate(prevProps: ErrorBoundaryProps) {
39
+ const titleChanged = prevProps.title !== this.props.title;
40
+ const blocksChanged = prevProps.blocks !== this.props.blocks;
41
+ const blocksLayoutChanged =
42
+ prevProps.blocksLayout !== this.props.blocksLayout;
43
+
44
+ if (
45
+ (blocksChanged || blocksLayoutChanged || titleChanged) &&
46
+ this.state.hasError
47
+ ) {
48
+ this.setState({ hasError: false });
49
+ }
50
+ }
51
+
52
+ componentDidCatch(error: Error, errorInfo: ErrorInfo) {
53
+ // eslint-disable-next-line
54
+ console.error(error, errorInfo);
55
+ }
56
+
57
+ render(): ReactNode {
58
+ if (this.state.hasError) {
59
+ return (
60
+ <ErrorBoundaryMessage
61
+ name={this.props.name}
62
+ block={this.props.block}
63
+ type={this.props.type}
64
+ isEdit={this.props.isEdit}
65
+ />
66
+ );
67
+ }
68
+
69
+ return this.props.children;
70
+ }
71
+ }
72
+
73
+ type ReduxState = {
74
+ form?: {
75
+ global?: {
76
+ blocks?: BlocksData['blocks'];
77
+ blocks_layout?: BlocksData['blocks_layout'];
78
+ title: string | null;
79
+ };
80
+ };
81
+ };
82
+
83
+ const mapStateToProps = (state: ReduxState): StateProps => ({
84
+ blocks: state.form?.global?.blocks ?? null,
85
+ blocksLayout: state.form?.global?.blocks_layout ?? null,
86
+ // Title is used for demonstration purposes
87
+ // If we want to use it in metadata sources, we should connect it to the full state
88
+ // which I am reluctant to do because nowadays the form state can be quite large
89
+ title: state.form?.global?.title ?? null,
90
+ });
91
+
92
+ export default connect(mapStateToProps)(ErrorBoundary);
@@ -0,0 +1,66 @@
1
+ import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
2
+
3
+ type ErrorBoundaryMessageProps = {
4
+ name?: string;
5
+ block?: string;
6
+ type?: string;
7
+ isEdit?: boolean;
8
+ };
9
+
10
+ const messages = defineMessages({
11
+ title: {
12
+ id: 'blockErrorBoundaryTitle',
13
+ defaultMessage: 'Block error:',
14
+ },
15
+ description: {
16
+ id: 'blockErrorBoundaryDescription',
17
+ defaultMessage:
18
+ 'The {type} block with the id {block} has encountered an error.{lineBreak}You can try to undo your changes (via the undo toolbar or pressing {shortcut}), or try to delete the block and recreate it again.',
19
+ },
20
+ viewDescription: {
21
+ id: 'blockErrorBoundaryViewDescription',
22
+ defaultMessage:
23
+ 'The {type} block with the id {block} errored and cannot be displayed.{lineBreak}Please contact the site administrator for further assistance.',
24
+ },
25
+ });
26
+
27
+ const ErrorBoundaryMessage = (props: ErrorBoundaryMessageProps) => {
28
+ const intl = useIntl();
29
+
30
+ if (props.isEdit) {
31
+ return (
32
+ <div className="block-error-boundary">
33
+ <div className="title">{intl.formatMessage(messages.title)}</div>
34
+ <p>
35
+ <FormattedMessage
36
+ {...messages.description}
37
+ values={{
38
+ type: <code>{props.type}</code>,
39
+ block: <code>{props.block}</code>,
40
+ lineBreak: <br />,
41
+ shortcut: <code>ctrl/cmd + Z</code>,
42
+ }}
43
+ />
44
+ </p>
45
+ </div>
46
+ );
47
+ } else {
48
+ return (
49
+ <div className="block-error-boundary">
50
+ <div className="title">{intl.formatMessage(messages.title)}</div>
51
+ <p>
52
+ <FormattedMessage
53
+ {...messages.viewDescription}
54
+ values={{
55
+ type: <code>{props.type}</code>,
56
+ block: <code>{props.block}</code>,
57
+ lineBreak: <br />,
58
+ }}
59
+ />
60
+ </p>
61
+ </div>
62
+ );
63
+ }
64
+ };
65
+
66
+ export default ErrorBoundaryMessage;
@@ -12,6 +12,7 @@ import config from '@plone/volto/registry';
12
12
  import ViewDefaultBlock from '@plone/volto/components/manage/Blocks/Block/DefaultView';
13
13
  import MaybeWrap from '@plone/volto/components/manage/MaybeWrap/MaybeWrap';
14
14
  import RenderEmptyBlock from '@plone/volto/components/theme/View/RenderEmptyBlock';
15
+ import ErrorBoundary from '../Blocks/Block/ErrorBoundary';
15
16
 
16
17
  import StyleWrapperV3 from './StyleWrapperV3';
17
18
  import RenderBlocksV2 from './RenderBlocksV2';
@@ -69,47 +70,55 @@ const RenderBlocks = (props) => {
69
70
 
70
71
  if (Block) {
71
72
  return (
72
- <MaybeWrap
73
- key={block}
74
- condition={blockWrapperTag}
75
- as={blockWrapperTag}
73
+ <ErrorBoundary
74
+ key={`error-boundary-block-${block}`}
75
+ name={`blockId-${block}-type-${content[blocksFieldname]?.[block]?.['@type']}`}
76
+ block={block}
77
+ type={content[blocksFieldname]?.[block]?.['@type']}
76
78
  >
77
- {currentBlockModel === 3 ? (
78
- <StyleWrapperV3
79
- block={block}
80
- content={content}
81
- data={blockData}
82
- blocksConfig={blocksConfig}
83
- >
84
- <Block
85
- id={block}
86
- metadata={metadata}
87
- properties={content}
79
+ <MaybeWrap
80
+ key={block}
81
+ condition={blockWrapperTag}
82
+ as={blockWrapperTag}
83
+ >
84
+ {currentBlockModel === 3 ? (
85
+ <StyleWrapperV3
86
+ block={block}
87
+ content={content}
88
88
  data={blockData}
89
- path={getBaseUrl(location?.pathname || '')}
90
89
  blocksConfig={blocksConfig}
91
- />
92
- </StyleWrapperV3>
93
- ) : (
94
- <StyleWrapper
95
- key={block}
96
- {...props}
97
- id={block}
98
- block={block}
99
- data={blockData}
100
- isContainer={isContainer}
101
- >
102
- <Block
90
+ isContainer={isContainer}
91
+ >
92
+ <Block
93
+ id={block}
94
+ metadata={metadata}
95
+ properties={content}
96
+ data={blockData}
97
+ path={getBaseUrl(location?.pathname || '')}
98
+ blocksConfig={blocksConfig}
99
+ />
100
+ </StyleWrapperV3>
101
+ ) : (
102
+ <StyleWrapper
103
+ key={block}
104
+ {...props}
103
105
  id={block}
104
- metadata={metadata}
105
- properties={content}
106
+ block={block}
106
107
  data={blockData}
107
- path={getBaseUrl(location?.pathname || '')}
108
- blocksConfig={blocksConfig}
109
- />
110
- </StyleWrapper>
111
- )}
112
- </MaybeWrap>
108
+ isContainer={isContainer}
109
+ >
110
+ <Block
111
+ id={block}
112
+ metadata={metadata}
113
+ properties={content}
114
+ data={blockData}
115
+ path={getBaseUrl(location?.pathname || '')}
116
+ blocksConfig={blocksConfig}
117
+ />
118
+ </StyleWrapper>
119
+ )}
120
+ </MaybeWrap>
121
+ </ErrorBoundary>
113
122
  );
114
123
  }
115
124
 
@@ -15,6 +15,7 @@ import MaybeWrap from '@plone/volto/components/manage/MaybeWrap/MaybeWrap';
15
15
  import RenderEmptyBlock from '@plone/volto/components/theme/View/RenderEmptyBlock';
16
16
  import cx from 'classnames';
17
17
  import { groupByBGColor } from '../../helpers/grouping';
18
+ import ErrorBoundary from '../Blocks/Block/ErrorBoundary';
18
19
 
19
20
  const messages = defineMessages({
20
21
  unknownBlock: {
@@ -92,29 +93,36 @@ const RenderBlocks = (props) => {
92
93
 
93
94
  if (Block) {
94
95
  return (
95
- <MaybeWrap
96
- key={block}
97
- condition={blockWrapperTag}
98
- as={blockWrapperTag}
96
+ <ErrorBoundary
97
+ key={`error-boundary-block-${block}`}
98
+ name={`blockId-${block}-type-${content[blocksFieldname]?.[block]?.['@type']}`}
99
+ block={block}
100
+ type={content[blocksFieldname]?.[block]?.['@type']}
99
101
  >
100
- <StyleWrapper
102
+ <MaybeWrap
101
103
  key={block}
102
- {...props}
103
- id={block}
104
- block={block}
105
- data={blockData}
106
- isContainer={isContainer}
104
+ condition={blockWrapperTag}
105
+ as={blockWrapperTag}
107
106
  >
108
- <Block
107
+ <StyleWrapper
108
+ key={block}
109
+ {...props}
109
110
  id={block}
110
- metadata={metadata}
111
- properties={content}
111
+ block={block}
112
112
  data={blockData}
113
- path={getBaseUrl(location?.pathname || '')}
114
- blocksConfig={blocksConfig}
115
- />
116
- </StyleWrapper>
117
- </MaybeWrap>
113
+ isContainer={isContainer}
114
+ >
115
+ <Block
116
+ id={block}
117
+ metadata={metadata}
118
+ properties={content}
119
+ data={blockData}
120
+ path={getBaseUrl(location?.pathname || '')}
121
+ blocksConfig={blocksConfig}
122
+ />
123
+ </StyleWrapper>
124
+ </MaybeWrap>
125
+ </ErrorBoundary>
118
126
  );
119
127
  }
120
128
 
@@ -471,3 +471,8 @@
471
471
  }
472
472
  }
473
473
  }
474
+
475
+ .listing-item .headline span:not(:last-child)::after {
476
+ margin: 0 10px;
477
+ content: '|';
478
+ }
@@ -0,0 +1,11 @@
1
+ .block-error-boundary {
2
+ padding: 10px;
3
+ border: 2px dashed red;
4
+ background-color: #ffe6e6;
5
+ }
6
+
7
+ .block-error-boundary .title {
8
+ margin-bottom: 5px;
9
+ color: red;
10
+ font-weight: bold;
11
+ }
@@ -36,10 +36,12 @@
36
36
  @import 'blocks/eventMetadata';
37
37
  @import 'blocks/rss';
38
38
  @import 'blocks/eventSearch';
39
+ @import 'blocks/error-boundary';
39
40
  @import 'sticky-menu';
40
41
  @import 'card';
41
42
  @import 'insets';
42
43
  @import 'person';
44
+ @import 'notfound';
43
45
 
44
46
  @import 'temp';
45
47
 
@@ -0,0 +1,25 @@
1
+ .page-not-found {
2
+ .content-area {
3
+ @include default-container-width();
4
+ @include adjustMarginsToContainer($default-container-width);
5
+ .view-wrapper {
6
+ padding-top: $spacing-xlarge;
7
+ padding-bottom: $spacing-xlarge;
8
+ }
9
+ h1 {
10
+ margin-bottom: $spacing-large;
11
+ font-size: 48px;
12
+ font-weight: 700;
13
+ line-height: 56px;
14
+ }
15
+ p,
16
+ a {
17
+ font-size: 24px;
18
+ font-weight: 300;
19
+ line-height: 33px;
20
+ }
21
+ a {
22
+ color: $black;
23
+ }
24
+ }
25
+ }
@@ -325,24 +325,34 @@ body.person-squared-images,
325
325
  }
326
326
  }
327
327
  }
328
- // person with Four columns
328
+
329
+ // person with Four columns (edit mode and view mode)
330
+ .block.gridBlock .four.grid-items .person-teaser .card-summary,
331
+ .ui.stackable.stretched.four.column.grid .card-summary {
332
+ .summary-extra-info {
333
+ &.email {
334
+ min-width: 0;
335
+
336
+ a {
337
+ display: block;
338
+ overflow: hidden;
339
+ text-overflow: ellipsis;
340
+ white-space: nowrap;
341
+ }
342
+ }
343
+ }
344
+ }
345
+
346
+ // Additional styles for view mode only
329
347
  .ui.stackable.stretched.four.column.grid {
330
348
  .card-summary {
331
349
  display: grid;
350
+
332
351
  .summary-extra-info {
333
352
  gap: 5px;
334
- &.email {
335
- min-width: 0;
336
-
337
- a {
338
- display: block;
339
- overflow: hidden;
340
- text-overflow: ellipsis;
341
- white-space: nowrap;
342
- }
343
- }
344
353
  }
345
354
  }
355
+
346
356
  .block.listing,
347
357
  .block.search {
348
358
  .listing-item.person-listing .card-summary {