diff --git a/spiffworkflow-frontend/package-lock.json b/spiffworkflow-frontend/package-lock.json index d3d84d14..adcf548c 100644 --- a/spiffworkflow-frontend/package-lock.json +++ b/spiffworkflow-frontend/package-lock.json @@ -53,6 +53,7 @@ "react-datepicker": "^4.8.0", "react-devtools": "^4.27.1", "react-dom": "^18.2.0", + "react-error-boundary": "^4.0.11", "react-icons": "^4.4.0", "react-jsonschema-form": "^1.8.1", "react-router": "^6.3.0", @@ -25438,6 +25439,17 @@ "react": "^18.2.0" } }, + "node_modules/react-error-boundary": { + "version": "4.0.11", + "resolved": "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-4.0.11.tgz", + "integrity": "sha512-U13ul67aP5DOSPNSCWQ/eO0AQEYzEFkVljULQIjMV0KlffTAhxuDoBKdO0pb/JZ8mDhMKFZ9NZi0BmLGUiNphw==", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "peerDependencies": { + "react": ">=16.13.1" + } + }, "node_modules/react-error-overlay": { "version": "6.0.11", "resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.11.tgz", @@ -50937,6 +50949,14 @@ "scheduler": "^0.23.0" } }, + "react-error-boundary": { + "version": "4.0.11", + "resolved": "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-4.0.11.tgz", + "integrity": "sha512-U13ul67aP5DOSPNSCWQ/eO0AQEYzEFkVljULQIjMV0KlffTAhxuDoBKdO0pb/JZ8mDhMKFZ9NZi0BmLGUiNphw==", + "requires": { + "@babel/runtime": "^7.12.5" + } + }, "react-error-overlay": { "version": "6.0.11", "resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.11.tgz", diff --git a/spiffworkflow-frontend/package.json b/spiffworkflow-frontend/package.json index 1b62f4e2..866f8649 100644 --- a/spiffworkflow-frontend/package.json +++ b/spiffworkflow-frontend/package.json @@ -57,7 +57,8 @@ "timepicker": "^1.13.18", "typescript": "^4.7.4", "use-debounce": "^9.0.4", - "web-vitals": "^3.0.2" + "web-vitals": "^3.0.2", + "react-error-boundary": "^4.0.11" }, "overrides": { "postcss-preset-env": { diff --git a/spiffworkflow-frontend/src/ContainerForExtensions.tsx b/spiffworkflow-frontend/src/ContainerForExtensions.tsx index 05a72506..cd6b19f2 100644 --- a/spiffworkflow-frontend/src/ContainerForExtensions.tsx +++ b/spiffworkflow-frontend/src/ContainerForExtensions.tsx @@ -1,11 +1,11 @@ import { Content } from '@carbon/react'; import { Routes, Route } from 'react-router-dom'; import React, { useEffect, useState } from 'react'; +import { ErrorBoundary } from 'react-error-boundary'; import NavigationBar from './components/NavigationBar'; import HomePageRoutes from './routes/HomePageRoutes'; import About from './routes/About'; -import ErrorBoundary from './components/ErrorBoundary'; import AdminRoutes from './routes/AdminRoutes'; import ScrollToTop from './components/ScrollToTop'; @@ -19,6 +19,7 @@ import { UiSchemaUxElement, } from './extension_ui_schema_interfaces'; import HttpService from './services/HttpService'; +import { ErrorBoundaryFallback } from './ErrorBoundaryFallack'; export default function ContainerForExtensions() { const [extensionUxElements, setExtensionNavigationItems] = useState< @@ -84,7 +85,7 @@ export default function ContainerForExtensions() { - + } /> } /> diff --git a/spiffworkflow-frontend/src/ErrorBoundaryFallack.tsx b/spiffworkflow-frontend/src/ErrorBoundaryFallack.tsx new file mode 100644 index 00000000..0165ad8b --- /dev/null +++ b/spiffworkflow-frontend/src/ErrorBoundaryFallack.tsx @@ -0,0 +1,133 @@ +import { Button, Content } from '@carbon/react'; +import { Routes, Route } from 'react-router-dom'; +import React, { useEffect, useState } from 'react'; +import { ErrorBoundary, useErrorBoundary } from 'react-error-boundary'; +import NavigationBar from './components/NavigationBar'; + +import HomePageRoutes from './routes/HomePageRoutes'; +import About from './routes/About'; +import AdminRoutes from './routes/AdminRoutes'; + +import ScrollToTop from './components/ScrollToTop'; +import EditorRoutes from './routes/EditorRoutes'; +import Extension from './routes/Extension'; +import { useUriListForPermissions } from './hooks/UriListForPermissions'; +import { PermissionsToCheck, ProcessFile, ProcessModel } from './interfaces'; +import { usePermissionFetcher } from './hooks/PermissionService'; +import { + ExtensionUiSchema, + UiSchemaUxElement, +} from './extension_ui_schema_interfaces'; +import HttpService from './services/HttpService'; +import { Notification } from './components/Notification'; + +type ErrorProps = { + error: Error; +}; + +export function ErrorBoundaryFallback({ error }: ErrorProps) { + // This is displayed if the ErrorBoundary catches an error when rendering the form. + const { resetBoundary } = useErrorBoundary(); + + return ( + resetBoundary()} + type="error" + > +

+ We encountered an unexpected error. Please try again. If the problem + persists, please contact your administrator. +

+

{error.message}

+ +
+ ); +} + +export default function ContainerForExtensions() { + const [extensionUxElements, setExtensionNavigationItems] = useState< + UiSchemaUxElement[] | null + >(null); + + let contentClassName = 'main-site-body-centered'; + if (window.location.pathname.startsWith('/editor/')) { + contentClassName = 'no-center-stuff'; + } + const { targetUris } = useUriListForPermissions(); + const permissionRequestData: PermissionsToCheck = { + [targetUris.extensionListPath]: ['GET'], + }; + const { ability, permissionsLoaded } = usePermissionFetcher( + permissionRequestData + ); + + // eslint-disable-next-line sonarjs/cognitive-complexity + useEffect(() => { + if (!permissionsLoaded) { + return; + } + + const processExtensionResult = (processModels: ProcessModel[]) => { + const eni: UiSchemaUxElement[] = processModels + .map((processModel: ProcessModel) => { + const extensionUiSchemaFile = processModel.files.find( + (file: ProcessFile) => file.name === 'extension_uischema.json' + ); + if (extensionUiSchemaFile && extensionUiSchemaFile.file_contents) { + try { + const extensionUiSchema: ExtensionUiSchema = JSON.parse( + extensionUiSchemaFile.file_contents + ); + if (extensionUiSchema.ux_elements) { + return extensionUiSchema.ux_elements; + } + } catch (jsonParseError: any) { + console.error( + `Unable to get navigation items for ${processModel.id}` + ); + } + } + return [] as UiSchemaUxElement[]; + }) + .flat(); + if (eni) { + setExtensionNavigationItems(eni); + } + }; + + if (ability.can('GET', targetUris.extensionListPath)) { + HttpService.makeCallToBackend({ + path: targetUris.extensionListPath, + successCallback: processExtensionResult, + }); + } + }, [targetUris.extensionListPath, permissionsLoaded, ability]); + + return ( + <> + + + + Something went wrong.}> + + } /> + } /> + } /> + + } + /> + } /> + } + /> + + + + + ); +} diff --git a/spiffworkflow-frontend/src/components/ErrorBoundary.tsx b/spiffworkflow-frontend/src/components/ErrorBoundary.tsx deleted file mode 100644 index 75553c8a..00000000 --- a/spiffworkflow-frontend/src/components/ErrorBoundary.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import React from 'react'; - -type Props = { - children?: React.ReactNode; -}; - -type State = any; - -class ErrorBoundary extends React.Component { - constructor(props: Props) { - super(props); - this.state = { hasError: false }; - } - - static getDerivedStateFromError(error: any) { - return { hasError: true, error }; - } - - componentDidCatch(error: any, errorInfo: any) { - console.error(error, errorInfo); - if (error.constructor.name === 'AggregateError') { - console.error(error.message); - console.error(error.name); - console.error(error.errors); - } - } - - render() { - const { hasError } = this.state; - const { children } = this.props; - - if (hasError) { - return

Something went wrong.

; - } - - return children; - } -} - -export default ErrorBoundary; diff --git a/spiffworkflow-frontend/src/components/ReactFormBuilder/ReactFormBuilder.tsx b/spiffworkflow-frontend/src/components/ReactFormBuilder/ReactFormBuilder.tsx index 768426b8..6b8ae849 100644 --- a/spiffworkflow-frontend/src/components/ReactFormBuilder/ReactFormBuilder.tsx +++ b/spiffworkflow-frontend/src/components/ReactFormBuilder/ReactFormBuilder.tsx @@ -16,10 +16,35 @@ import { Loading, } from '@carbon/react'; import { useDebounce } from 'use-debounce'; +import { ErrorBoundary, useErrorBoundary } from 'react-error-boundary'; import HttpService from '../../services/HttpService'; import ExamplesTable from './ExamplesTable'; import CustomForm from '../CustomForm'; -import ErrorBoundary from '../ErrorBoundary'; +import { Notification } from '../Notification'; + +type ErrorProps = { + error: Error; +}; + +function FormErrorFallback({ error }: ErrorProps) { + // This is displayed if the ErrorBoundary catches an error when rendering the form. + const { resetBoundary } = useErrorBoundary(); + + return ( + resetBoundary()} + type="error" + > +

+ The form could not be built with the current schema, UI and data. Please + try to correct the issue and try again. +

+

{error.message}

+ +
+ ); +} type OwnProps = { processModelId: string; @@ -246,6 +271,7 @@ export default function ReactFormBuilder({ function updateDataFromStr(newDataStr: string) { try { + setStrFormData(newDataStr); const newData = JSON.parse(newDataStr); setFormData(newData); } catch (e) { @@ -461,7 +487,7 @@ export default function ReactFormBuilder({

Form Preview

{errorMessage}
- +