Skip to content

Commit 63972fc

Browse files
authored
fix(app-headless-cms): improve error handling (#4572)
1 parent 3ff1f27 commit 63972fc

File tree

10 files changed

+146
-15
lines changed

10 files changed

+146
-15
lines changed

package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,9 @@
189189
"**/*.{js,ts,tsx}": [
190190
"prettier --write",
191191
"eslint --max-warnings=0 --no-ignore"
192+
],
193+
"**/package.json": [
194+
"yarn webiny verify-dependencies"
192195
]
193196
},
194197
"yargs": {

packages/app-headless-cms-common/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
"license": "MIT",
1515
"dependencies": {
1616
"@emotion/react": "11.10.8",
17+
"@emotion/styled": "11.10.6",
1718
"@fortawesome/fontawesome-svg-core": "^1.3.0",
1819
"@webiny/app": "0.0.0",
1920
"@webiny/app-admin": "0.0.0",
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import React, { ErrorInfo } from "react";
2+
import type { CmsModelField } from "~/types";
3+
import { FieldElementError } from "./FieldElementError";
4+
5+
type State =
6+
| {
7+
hasError: true;
8+
error: Error;
9+
}
10+
| { hasError: false; error: undefined };
11+
12+
interface Props {
13+
field: CmsModelField;
14+
[key: string]: any;
15+
}
16+
17+
export class ErrorBoundary extends React.Component<Props, State> {
18+
constructor(props: Props) {
19+
super(props);
20+
this.state = {
21+
hasError: false,
22+
error: undefined
23+
};
24+
}
25+
26+
static getDerivedStateFromError(error: Error) {
27+
return {
28+
hasError: true,
29+
error
30+
};
31+
}
32+
33+
public override componentDidCatch(error: Error, errorInfo: ErrorInfo) {
34+
const { field } = this.props;
35+
if (!field) {
36+
return;
37+
}
38+
console.groupCollapsed(
39+
`%cFIELD ERROR%c: An error occurred while rendering model field "${field.fieldId}" (${field.id}) of type "${field.type}".`,
40+
"color:red",
41+
"color:default"
42+
);
43+
console.log("Field definition", field);
44+
console.error(error, errorInfo);
45+
console.groupEnd();
46+
}
47+
48+
public override render() {
49+
if (this.state.hasError) {
50+
return (
51+
<FieldElementError
52+
title={`Error: ${this.state.error.message}`}
53+
description={"See developer console for more details."}
54+
/>
55+
);
56+
}
57+
58+
return this.props.children;
59+
}
60+
}

packages/app-headless-cms-common/src/Fields/FieldElement.tsx

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,24 @@ import { useBind } from "./useBind";
77
import { useRenderPlugins } from "./useRenderPlugins";
88
import { ModelFieldProvider, useModelField } from "../ModelFieldProvider";
99
import { CmsModelField, CmsEditorContentModel, BindComponent } from "~/types";
10+
import { ErrorBoundary } from "./ErrorBoundary";
1011

1112
const t = i18n.ns("app-headless-cms/admin/components/content-form");
1213

14+
declare global {
15+
// eslint-disable-next-line
16+
namespace JSX {
17+
interface IntrinsicElements {
18+
"hcms-model-field": {
19+
"data-id": string;
20+
"data-type": string;
21+
"data-field-id": string;
22+
children: React.ReactNode;
23+
};
24+
}
25+
}
26+
}
27+
1328
type RenderFieldProps = Omit<FieldElementProps, "field">;
1429

1530
const RenderField = (props: RenderFieldProps) => {
@@ -44,10 +59,22 @@ export interface FieldElementProps {
4459
export const FieldElement = makeDecoratable(
4560
"FieldElement",
4661
({ field, ...props }: FieldElementProps) => {
62+
if (!field) {
63+
return null;
64+
}
65+
4766
return (
48-
<ModelFieldProvider field={field}>
49-
<RenderField {...props} />
50-
</ModelFieldProvider>
67+
<hcms-model-field
68+
data-id={field.id}
69+
data-field-id={field.fieldId}
70+
data-type={field.type}
71+
>
72+
<ErrorBoundary field={field}>
73+
<ModelFieldProvider field={field}>
74+
<RenderField {...props} />
75+
</ModelFieldProvider>
76+
</ErrorBoundary>
77+
</hcms-model-field>
5178
);
5279
}
5380
);
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import React from "react";
2+
import styled from "@emotion/styled";
3+
4+
const StyledError = styled.div`
5+
border: 2px solid red;
6+
background-color: #f87e7e;
7+
border-radius: 5px;
8+
padding: 5px 10px;
9+
`;
10+
11+
interface FieldElementErrorProps {
12+
title: string;
13+
description: string;
14+
}
15+
16+
const showError = process.env.NODE_ENV === "development";
17+
18+
export const FieldElementError = (props: FieldElementErrorProps) => {
19+
if (!showError) {
20+
return null;
21+
}
22+
23+
return (
24+
<StyledError>
25+
<h5>{props.title}</h5>
26+
<p>{props.description}</p>
27+
</StyledError>
28+
);
29+
};

packages/app-headless-cms-common/src/Fields/Fields.tsx

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import React from "react";
22
import { Cell, Grid } from "@webiny/ui/Grid";
33
import { FieldElement } from "./FieldElement";
4-
import {
4+
import { FieldElementError } from "./FieldElementError";
5+
import type {
56
CmsEditorContentModel,
67
CmsModelField,
78
CmsEditorFieldsLayout,
@@ -25,16 +26,25 @@ export const Fields = ({ Bind, fields, layout, contentModel, gridClassName }: Fi
2526
<Grid className={gridClassName}>
2627
{layout.map((row, rowIndex) => (
2728
<React.Fragment key={rowIndex}>
28-
{row.map(fieldId => {
29-
const field = getFieldById(fields, fieldId) as CmsModelField;
29+
{row.map(id => {
30+
const field = getFieldById(fields, id) as CmsModelField;
3031

3132
return (
32-
<Cell span={Math.floor(12 / row.length)} key={fieldId}>
33-
<FieldElement
34-
field={field}
35-
Bind={Bind}
36-
contentModel={contentModel}
37-
/>
33+
<Cell span={Math.floor(12 / row.length)} key={id}>
34+
{field ? (
35+
<FieldElement
36+
field={field}
37+
Bind={Bind}
38+
contentModel={contentModel}
39+
/>
40+
) : (
41+
<FieldElementError
42+
title={`Missing field with id "${id}"!`}
43+
description={
44+
"Make sure field layout contains the correct field ids (hint: check for typos)."
45+
}
46+
/>
47+
)}
3848
</Cell>
3949
);
4050
})}

packages/app-headless-cms-common/src/Fields/Label.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ interface LabelProps {
1313
children?: React.ReactNode;
1414
}
1515

16-
const Label = ({ children }: LabelProps) => (
16+
export const Label = ({ children }: LabelProps) => (
1717
<div
1818
className={classNames(
1919
"mdc-text-field-helper-text mdc-text-field-helper-text--persistent",

packages/cli/files/references.json

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

packages/project-utils/bundling/app/config/webpackDevServer.config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ module.exports = function ({ host, port, https, allowedHost, proxy, paths }) {
5454
// Enable HTTPS if the HTTPS environment variable is set to 'true'
5555
...server,
5656
client: {
57-
overlay: true,
57+
overlay: false,
5858
// Silence WebpackDevServer's own logs since they're generally not useful.
5959
// It will still show compile warnings and errors with this setting.
6060
logging: "warn",

yarn.lock

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14287,6 +14287,7 @@ __metadata:
1428714287
dependencies:
1428814288
"@emotion/babel-plugin": "npm:^11.11.0"
1428914289
"@emotion/react": "npm:11.10.8"
14290+
"@emotion/styled": "npm:11.10.6"
1429014291
"@fortawesome/fontawesome-svg-core": "npm:^1.3.0"
1429114292
"@types/react": "npm:18.2.79"
1429214293
"@webiny/app": "npm:0.0.0"

0 commit comments

Comments
 (0)