diff --git a/CHANGELOG.md b/CHANGELOG.md index ffb6c5d9..c9647681 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - [#141](https://github.com/estruyf/vscode-front-matter/issues/141): Allow content creation for page bundles or single files - [#145](https://github.com/estruyf/vscode-front-matter/issues/145): Moved folder registration settings to `frontmatter.json` file +- [#147](https://github.com/estruyf/vscode-front-matter/issues/147): Error boundary added for metadata fields ### 🐞 Fixes diff --git a/assets/media/styles.css b/assets/media/styles.css index e792f0a9..40ec8a0f 100644 --- a/assets/media/styles.css +++ b/assets/media/styles.css @@ -437,6 +437,25 @@ input:checked + .field__toggle__slider:before { margin-right: .5rem; } +.metadata_field__error { + color: var(--vscode-errorForeground); + display: flex; + justify-content: space-between; + align-items: center; +} + +.metadata_field__error button { + color: var(--vscode-button-secondaryForeground); + background-color: var(--vscode-button-secondaryBackground); + padding-left: 1rem; + padding-right: 1rem; + width: auto; +} + +.metadata_field__error button:hover { + background-color: var(--vscode-button-secondaryHoverBackground); +} + .metadata_field__input, .metadata_field__input:focus, .metadata_field__textarea, .metadata_field__textarea:focus { outline: none; diff --git a/src/panelWebView/components/ErrorBoundary/FieldBoundary.tsx b/src/panelWebView/components/ErrorBoundary/FieldBoundary.tsx new file mode 100644 index 00000000..be3fa44d --- /dev/null +++ b/src/panelWebView/components/ErrorBoundary/FieldBoundary.tsx @@ -0,0 +1,49 @@ +import * as React from 'react'; +import * as Sentry from "@sentry/react"; +import { VsLabel } from '../VscodeComponents'; + +export interface IFieldBoundaryProps { + fieldName: string; +} + +export interface IFieldBoundaryState { + hasError: boolean; +} + +export default class FieldBoundary extends React.Component { + + constructor(props: IFieldBoundaryProps) { + super(props); + this.state = { hasError: false }; + } + + public static getDerivedStateFromError(error: any) { + // Update state so the next render will show the fallback UI. + return { hasError: true }; + } + + public componentDidCatch(error: any, errorInfo: any) { + Sentry.captureMessage(`Field boundary: ${error?.message || error}`); + } + + public render(): React.ReactElement { + if (this.state.hasError) { + return ( +
+ +
+ {this.props.fieldName} +
+
+
+ Error loading field + + +
+
+ ); + } + + return this.props.children as any; + } +} \ No newline at end of file diff --git a/src/panelWebView/components/Metadata.tsx b/src/panelWebView/components/Metadata.tsx index d79d031f..389f32a1 100644 --- a/src/panelWebView/components/Metadata.tsx +++ b/src/panelWebView/components/Metadata.tsx @@ -17,6 +17,7 @@ import { NumberField } from './Fields/NumberField'; import { ChoiceField } from './Fields/ChoiceField'; import useContentType from '../../hooks/useContentType'; import { DateHelper } from '../../helpers/DateHelper'; +import FieldBoundary from './ErrorBoundary/FieldBoundary'; export interface IMetadataProps { settings: PanelSettings | undefined; @@ -62,20 +63,23 @@ export const Metadata: React.FunctionComponent = ({settings, met const dateValue = metadata[field.name] ? getDate(metadata[field.name] as string) : null; return ( - sendUpdate(field.name, date))} /> + + sendUpdate(field.name, date))} /> + ); } else if (field.type === 'boolean') { return ( - sendUpdate(field.name, checked)} /> + + sendUpdate(field.name, checked)} /> + ); } else if (field.type === 'string') { const textValue = metadata[field.name]; @@ -88,14 +92,15 @@ export const Metadata: React.FunctionComponent = ({settings, met } return ( - sendUpdate(field.name, value)} - value={textValue as string || null} /> + + sendUpdate(field.name, value)} + value={textValue as string || null} /> + ); } else if (field.type === 'number') { const fieldValue = metadata[field.name]; @@ -105,61 +110,67 @@ export const Metadata: React.FunctionComponent = ({settings, met } return ( - sendUpdate(field.name, value)} - value={nrValue} /> + + sendUpdate(field.name, value)} + value={nrValue} /> + ); } else if (field.type === 'image') { return ( - sendUpdate(field.name, value))} /> + + sendUpdate(field.name, value))} /> + ); } else if (field.type === 'choice') { const choices = field.choices || []; const choiceValue = metadata[field.name]; return ( - sendUpdate(field.name, value))} /> + + sendUpdate(field.name, value))} /> + ); } else if (field.type === 'tags') { return ( - } - crntSelected={metadata[field.name] as string[] || []} - options={settings?.tags || []} - freeform={settings.freeform} - focussed={focusElm === TagType.tags} - unsetFocus={unsetFocus} /> + + } + crntSelected={metadata[field.name] as string[] || []} + options={settings?.tags || []} + freeform={settings.freeform} + focussed={focusElm === TagType.tags} + unsetFocus={unsetFocus} /> + ); } else if (field.type === 'categories') { return ( - } - crntSelected={metadata.categories as string[] || []} - options={settings.categories} - freeform={settings.freeform} - focussed={focusElm === TagType.categories} - unsetFocus={unsetFocus} /> + + } + crntSelected={metadata.categories as string[] || []} + options={settings.categories} + freeform={settings.freeform} + focussed={focusElm === TagType.categories} + unsetFocus={unsetFocus} /> + ); } else { return null;