From 298e50bbba009230690a190540fe3e09ba446e12 Mon Sep 17 00:00:00 2001 From: jasquat Date: Wed, 12 Feb 2025 10:56:53 -0500 Subject: [PATCH] some support for extensions in v3 ui w/ burnettk --- spiffworkflow-frontend/src/App.tsx | 4 +- .../a-spiffui-v3/ContainerForExtensions.tsx | 150 ++++++++++++++++++ .../src/a-spiffui-v3/ErrorBoundaryFallack.tsx | 35 ++++ .../a-spiffui-v3/components/ScrollToTop.tsx | 12 ++ .../extension_ui_schema_interfaces.ts | 3 +- .../src/a-spiffui-v3/views/BackendIsDown.tsx | 12 ++ .../views/{SpiffUIV3.tsx => BaseRoutes.tsx} | 10 +- .../src/a-spiffui-v3/views/Extension.tsx | 6 +- 8 files changed, 220 insertions(+), 12 deletions(-) create mode 100644 spiffworkflow-frontend/src/a-spiffui-v3/ContainerForExtensions.tsx create mode 100644 spiffworkflow-frontend/src/a-spiffui-v3/ErrorBoundaryFallack.tsx create mode 100644 spiffworkflow-frontend/src/a-spiffui-v3/components/ScrollToTop.tsx create mode 100644 spiffworkflow-frontend/src/a-spiffui-v3/views/BackendIsDown.tsx rename spiffworkflow-frontend/src/a-spiffui-v3/views/{SpiffUIV3.tsx => BaseRoutes.tsx} (98%) diff --git a/spiffworkflow-frontend/src/App.tsx b/spiffworkflow-frontend/src/App.tsx index 163684e0f..4c8d3c868 100644 --- a/spiffworkflow-frontend/src/App.tsx +++ b/spiffworkflow-frontend/src/App.tsx @@ -6,8 +6,8 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { AbilityContext } from './contexts/Can'; import APIErrorProvider from './contexts/APIErrorContext'; import ContainerForExtensions from './ContainerForExtensions'; +import ContainerForExtensionsV3 from './a-spiffui-v3/ContainerForExtensions'; import PublicRoutes from './a-spiffui-v3/views/PublicRoutes'; -import SpiffUIV3 from './a-spiffui-v3/views/SpiffUIV3'; const queryClient = new QueryClient(); @@ -18,7 +18,7 @@ export default function App() { { path: 'public/*', element: }, { path: 'newui/*', - element: , + element: , }, { path: '*', diff --git a/spiffworkflow-frontend/src/a-spiffui-v3/ContainerForExtensions.tsx b/spiffworkflow-frontend/src/a-spiffui-v3/ContainerForExtensions.tsx new file mode 100644 index 000000000..12257c28e --- /dev/null +++ b/spiffworkflow-frontend/src/a-spiffui-v3/ContainerForExtensions.tsx @@ -0,0 +1,150 @@ +import { Container } from '@mui/material'; +import { Routes, Route, useLocation } from 'react-router-dom'; +import React, { useEffect, useState } from 'react'; +import { ErrorBoundary } from 'react-error-boundary'; + +import ScrollToTop from './components/ScrollToTop'; +import Extension from './views/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 { ErrorBoundaryFallback } from './ErrorBoundaryFallack'; +import BaseRoutes from './views/BaseRoutes'; +import BackendIsDown from './views/BackendIsDown'; +import Login from './views/Login'; +import useAPIError from './hooks/UseApiError'; + +export default function ContainerForExtensions() { + const [backendIsUp, setBackendIsUp] = useState(null); + const [extensionUxElements, setExtensionUxElements] = 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, + ); + + const { removeError } = useAPIError(); + + const location = useLocation(); + + // never carry an error message across to a different path + useEffect(() => { + removeError(); + // if we include the removeError function to the dependency array of this useEffect, it causes + // an infinite loop where the page with the error adds the error, + // then this runs and it removes the error, etc. it is ok not to include it here, i think, because it never changes. + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [location.pathname]); + + // eslint-disable-next-line sonarjs/cognitive-complexity + useEffect(() => { + 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 && + extensionUiSchema.ux_elements && + !extensionUiSchema.disabled + ) { + return extensionUiSchema.ux_elements; + } + } catch (jsonParseError: any) { + console.error( + `Unable to get navigation items for ${processModel.id}`, + ); + } + } + return [] as UiSchemaUxElement[]; + }) + .flat(); + if (eni) { + setExtensionUxElements(eni); + } + }; + + const getExtensions = () => { + setBackendIsUp(true); + if (!permissionsLoaded) { + return; + } + if (ability.can('GET', targetUris.extensionListPath)) { + HttpService.makeCallToBackend({ + path: targetUris.extensionListPath, + successCallback: processExtensionResult, + }); + } else { + // set to an empty array so we know that it loaded + setExtensionUxElements([]); + } + }; + + HttpService.makeCallToBackend({ + path: targetUris.statusPath, + successCallback: getExtensions, + failureCallback: () => setBackendIsUp(false), + }); + }, [ + targetUris.extensionListPath, + targetUris.statusPath, + permissionsLoaded, + ability, + ]); + + const routeComponents = () => { + return ( + + } + /> + } /> + } /> + + ); + }; + + const backendIsDownPage = () => { + return []; + }; + + const innerComponents = () => { + if (backendIsUp === null) { + return []; + } + if (backendIsUp) { + return routeComponents(); + } + return backendIsDownPage(); + }; + + return ( + + + + {innerComponents()} + + + ); +} diff --git a/spiffworkflow-frontend/src/a-spiffui-v3/ErrorBoundaryFallack.tsx b/spiffworkflow-frontend/src/a-spiffui-v3/ErrorBoundaryFallack.tsx new file mode 100644 index 000000000..225d04ea8 --- /dev/null +++ b/spiffworkflow-frontend/src/a-spiffui-v3/ErrorBoundaryFallack.tsx @@ -0,0 +1,35 @@ +import React from 'react'; +import { useErrorBoundary } from 'react-error-boundary'; +import Button from '@mui/material/Button'; +import Alert from '@mui/material/Alert'; +import AlertTitle from '@mui/material/AlertTitle'; + +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(); + + // print the error to the console so we can debug issues + console.error(error); + + return ( + + Try again + + } + > + Something Went Wrong. +

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

+

{error.message}

+
+ ); +} diff --git a/spiffworkflow-frontend/src/a-spiffui-v3/components/ScrollToTop.tsx b/spiffworkflow-frontend/src/a-spiffui-v3/components/ScrollToTop.tsx new file mode 100644 index 000000000..7d8c6f1a6 --- /dev/null +++ b/spiffworkflow-frontend/src/a-spiffui-v3/components/ScrollToTop.tsx @@ -0,0 +1,12 @@ +import { useEffect } from 'react'; +import { useLocation } from 'react-router-dom'; + +export default function ScrollToTop() { + const { pathname } = useLocation(); + + useEffect(() => { + window.scrollTo(0, 0); + }, [pathname]); + + return null; +} diff --git a/spiffworkflow-frontend/src/a-spiffui-v3/extension_ui_schema_interfaces.ts b/spiffworkflow-frontend/src/a-spiffui-v3/extension_ui_schema_interfaces.ts index a6272a23d..2ecaf2ec7 100644 --- a/spiffworkflow-frontend/src/a-spiffui-v3/extension_ui_schema_interfaces.ts +++ b/spiffworkflow-frontend/src/a-spiffui-v3/extension_ui_schema_interfaces.ts @@ -42,10 +42,9 @@ export enum UiSchemaPersistenceLevel { * The arguments that can be passed in will generally match the "OwnProps" type defined within each file. */ export enum UiSchemaPageComponentList { - // CreateNewInstance = 'CreateNewInstance', CustomForm = 'CustomForm', MarkdownRenderer = 'MarkdownRenderer', - // ProcessInstanceListTable = 'ProcessInstanceListTable', + ProcessInstanceListTable = 'ProcessInstanceListTable', ProcessInstanceRun = 'ProcessInstanceRun', SpiffTabs = 'SpiffTabs', } diff --git a/spiffworkflow-frontend/src/a-spiffui-v3/views/BackendIsDown.tsx b/spiffworkflow-frontend/src/a-spiffui-v3/views/BackendIsDown.tsx new file mode 100644 index 000000000..ec338187f --- /dev/null +++ b/spiffworkflow-frontend/src/a-spiffui-v3/views/BackendIsDown.tsx @@ -0,0 +1,12 @@ +export default function BackendIsDown() { + return ( +
+

Server error

+

+ We are sorry, but our service is temporarily unavailable due to + technical difficulties. Please bear with us while we work to resolve the + issue. If the problem persists, please contact the site administrator. +

+
+ ); +} diff --git a/spiffworkflow-frontend/src/a-spiffui-v3/views/SpiffUIV3.tsx b/spiffworkflow-frontend/src/a-spiffui-v3/views/BaseRoutes.tsx similarity index 98% rename from spiffworkflow-frontend/src/a-spiffui-v3/views/SpiffUIV3.tsx rename to spiffworkflow-frontend/src/a-spiffui-v3/views/BaseRoutes.tsx index 4c799106f..14cf0e795 100644 --- a/spiffworkflow-frontend/src/a-spiffui-v3/views/SpiffUIV3.tsx +++ b/spiffworkflow-frontend/src/a-spiffui-v3/views/BaseRoutes.tsx @@ -20,7 +20,6 @@ import Processes from './StartProcess/Processes'; import StartProcessInstance from './StartProcess/StartProcessInstance'; import SideNav from '../components/SideNav'; import LoginHandler from '../components/LoginHandler'; -import Login from './Login'; import InstancesStartedByMe from './InstancesStartedByMe'; import TaskShow from './TaskShow/TaskShow'; import ProcessInterstitialPage from './TaskShow/ProcessInterstitialPage'; @@ -46,12 +45,16 @@ import ReactFormEditor from './ReactFormEditor'; // Import the new component import ProcessInstanceRoutes from './ProcessInstanceRoutes'; import ProcessInstanceShortLink from './ProcessInstanceShortLink'; import ProcessInstanceList from './ProcessInstanceList'; // Import the new component -// Import the new component +import { UiSchemaUxElement } from '../extension_ui_schema_interfaces'; const fadeIn = 'fadeIn'; const fadeOutImmediate = 'fadeOutImmediate'; -export default function SpiffUIV3() { +type OwnProps = { + extensionUxElements?: UiSchemaUxElement[] | null; +}; + +export default function BaseRoutes({ extensionUxElements }: OwnProps) { const storedTheme: PaletteMode = (localStorage.getItem('theme') || 'light') as PaletteMode; const [globalTheme, setGlobalTheme] = useState( @@ -260,7 +263,6 @@ export default function SpiffUIV3() { path="/:modifiedProcessModelId/start" element={} /> - } /> } />