Feature/upgrade react router dom (#1050)

* updated react-router-dom to match react-router version w/ burnettk

* disable save button on process model edit diagram page unless a change has been made w/ burnettk

* remove web components from form data on extensions page to avoid potential errors w/ burnettk

* updates based on coderabbit w/ burnettk

* fixed cypress issues

---------

Co-authored-by: jasquat <jasquat@users.noreply.github.com>
This commit is contained in:
jasquat 2024-02-16 11:01:26 -05:00 committed by GitHub
parent 9b8cb58e99
commit 73df645408
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 152 additions and 178 deletions

View File

@ -36,7 +36,7 @@ const updateDmnText = (oldText, newText, elementId = 'wonderful_process') => {
// wait for a little bit for the xml to get set before saving
// FIXME: gray out save button or add spinner while xml is loading
cy.wait(500);
cy.contains('Save').click();
cy.getBySel('process-model-file-save-button').click();
};
const updateBpmnPythonScript = (pythonScript, elementId = 'process_script') => {
@ -47,7 +47,7 @@ const updateBpmnPythonScript = (pythonScript, elementId = 'process_script') => {
// wait for a little bit for the xml to get set before saving
cy.wait(500);
cy.contains('Save').click();
cy.getBySel('process-model-file-save-button').click();
};
// NOTE: anytime the status dropdown box is clicked on, click off of it

View File

@ -82,7 +82,8 @@ describe('process-models', () => {
cy.get('#bio-properties-panel-name').clear();
cy.get('#bio-properties-panel-name').type('Start Event Name');
cy.wait(500);
cy.contains('Save').click();
cy.getBySel('process-model-file-changed');
cy.getBySel('process-model-file-save-button').click();
cy.contains('Start Event Name');
cy.get(fileNameInputSelector).type(bpmnFileName);
cy.contains(saveChangesButtonText).click();
@ -101,7 +102,7 @@ describe('process-models', () => {
cy.get('#bio-properties-panel-id').clear();
cy.get('#bio-properties-panel-id').type(decisionAcceptanceTestId);
cy.contains('General').click();
cy.contains('Save').click();
cy.getBySel('process-model-file-save-button').click();
cy.get(fileNameInputSelector).type(dmnFileName);
cy.contains(saveChangesButtonText).click();
cy.contains(`Process Model File: ${dmnFileName}`);

View File

@ -56,7 +56,7 @@
"react-icons": "^5.0.1",
"react-jsonschema-form": "^1.8.1",
"react-router": "^6.22.0",
"react-router-dom": "6.3.0",
"react-router-dom": "^6.22.0",
"react-scripts": "^5.0.1",
"serve": "^14.0.0",
"timepicker": "^1.13.18",
@ -15604,14 +15604,6 @@
"he": "bin/he"
}
},
"node_modules/history": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/history/-/history-5.3.0.tgz",
"integrity": "sha512-ZqaKwjjrAYUYfLG+htGaIIZ4nioX2L70ZUMIFysS3xvBsSG4x/n1V6TXV3N8ZYNuFGlDirFg32T7B6WOUPDYcQ==",
"dependencies": {
"@babel/runtime": "^7.7.6"
}
},
"node_modules/hmac-drbg": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz",
@ -26244,29 +26236,21 @@
}
},
"node_modules/react-router-dom": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.3.0.tgz",
"integrity": "sha512-uaJj7LKytRxZNQV8+RbzJWnJ8K2nPsOOEuX7aQstlMZKQT0164C+X2w6bnkqU3sjtLvpd5ojrezAyfZ1+0sStw==",
"version": "6.22.0",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.22.0.tgz",
"integrity": "sha512-z2w+M4tH5wlcLmH3BMMOMdrtrJ9T3oJJNsAlBJbwk+8Syxd5WFJ7J5dxMEW0/GEXD1BBis4uXRrNIz3mORr0ag==",
"dependencies": {
"history": "^5.2.0",
"react-router": "6.3.0"
"@remix-run/router": "1.15.0",
"react-router": "6.22.0"
},
"engines": {
"node": ">=14.0.0"
},
"peerDependencies": {
"react": ">=16.8",
"react-dom": ">=16.8"
}
},
"node_modules/react-router-dom/node_modules/react-router": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-6.3.0.tgz",
"integrity": "sha512-7Wh1DzVQ+tlFjkeo+ujvjSqSJmkt1+8JO+T5xklPlgrh70y7ogx75ODRW0ThWhY7S+6yEDks8TYrtQe/aoboBQ==",
"dependencies": {
"history": "^5.2.0"
},
"peerDependencies": {
"react": ">=16.8"
}
},
"node_modules/react-scripts": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-5.0.1.tgz",
@ -45705,14 +45689,6 @@
"resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
"integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw=="
},
"history": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/history/-/history-5.3.0.tgz",
"integrity": "sha512-ZqaKwjjrAYUYfLG+htGaIIZ4nioX2L70ZUMIFysS3xvBsSG4x/n1V6TXV3N8ZYNuFGlDirFg32T7B6WOUPDYcQ==",
"requires": {
"@babel/runtime": "^7.7.6"
}
},
"hmac-drbg": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz",
@ -53463,22 +53439,12 @@
}
},
"react-router-dom": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.3.0.tgz",
"integrity": "sha512-uaJj7LKytRxZNQV8+RbzJWnJ8K2nPsOOEuX7aQstlMZKQT0164C+X2w6bnkqU3sjtLvpd5ojrezAyfZ1+0sStw==",
"version": "6.22.0",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.22.0.tgz",
"integrity": "sha512-z2w+M4tH5wlcLmH3BMMOMdrtrJ9T3oJJNsAlBJbwk+8Syxd5WFJ7J5dxMEW0/GEXD1BBis4uXRrNIz3mORr0ag==",
"requires": {
"history": "^5.2.0",
"react-router": "6.3.0"
},
"dependencies": {
"react-router": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-6.3.0.tgz",
"integrity": "sha512-7Wh1DzVQ+tlFjkeo+ujvjSqSJmkt1+8JO+T5xklPlgrh70y7ogx75ODRW0ThWhY7S+6yEDks8TYrtQe/aoboBQ==",
"requires": {
"history": "^5.2.0"
}
}
"@remix-run/router": "1.15.0",
"react-router": "6.22.0"
}
},
"react-scripts": {

View File

@ -51,7 +51,7 @@
"react-icons": "^5.0.1",
"react-jsonschema-form": "^1.8.1",
"react-router": "^6.22.0",
"react-router-dom": "6.3.0",
"react-router-dom": "^6.22.0",
"react-scripts": "^5.0.1",
"serve": "^14.0.0",
"timepicker": "^1.13.18",

View File

@ -1,4 +1,3 @@
import { BrowserRouter } from 'react-router-dom';
import { defineAbility } from '@casl/ability';
import React from 'react';
@ -10,14 +9,11 @@ export default function App() {
const ability = defineAbility(() => {});
return (
<div className="cds--white">
{/* @ts-ignore */}
<AbilityContext.Provider value={ability}>
<APIErrorProvider>
<BrowserRouter>
<ContainerForExtensions />
</BrowserRouter>
</APIErrorProvider>
</AbilityContext.Provider>
<APIErrorProvider>
<AbilityContext.Provider value={ability}>
<ContainerForExtensions />
</AbilityContext.Provider>
</APIErrorProvider>
</div>
);
}

View File

@ -1,5 +1,5 @@
import { Content } from '@carbon/react';
import { Routes, Route } from 'react-router-dom';
import { createBrowserRouter, RouterProvider, Outlet } from 'react-router-dom';
import React, { useEffect, useState } from 'react';
import { ErrorBoundary } from 'react-error-boundary';
@ -100,42 +100,51 @@ export default function ContainerForExtensions() {
]);
const routeComponents = () => {
return (
<Routes>
<Route
path="/*"
element={<BaseRoutes extensionUxElements={extensionUxElements} />}
/>
<Route path="/editor/*" element={<EditorRoutes />} />
<Route path="/extensions/:page_identifier" element={<Extension />} />
<Route path="/login" element={<Login />} />
</Routes>
);
return [
{
path: '*',
element: <BaseRoutes extensionUxElements={extensionUxElements} />,
},
{ path: 'editor/*', element: <EditorRoutes /> },
{ path: 'extensions/:page_identifier', element: <Extension /> },
{ path: 'login', element: <Login /> },
];
};
const backendIsDownPage = () => {
return <BackendIsDown />;
return [<BackendIsDown />];
};
const innerComponents = () => {
if (backendIsUp === null) {
return null;
return [];
}
if (backendIsUp) {
return routeComponents();
return <Outlet />;
}
return backendIsDownPage();
};
return (
<>
<NavigationBar extensionUxElements={extensionUxElements} />;
<Content className={contentClassName}>
<ScrollToTop />
<ErrorBoundary FallbackComponent={ErrorBoundaryFallback}>
{innerComponents()}
</ErrorBoundary>
</Content>
</>
);
const layout = () => {
return (
<>
<NavigationBar extensionUxElements={extensionUxElements} />;
<Content className={contentClassName}>
<ScrollToTop />
<ErrorBoundary FallbackComponent={ErrorBoundaryFallback}>
{innerComponents()}
</ErrorBoundary>
</Content>
</>
);
};
const router = createBrowserRouter([
{
path: '*',
Component: layout,
children: routeComponents(),
},
]);
return <RouterProvider router={router} />;
}

View File

@ -1,6 +1,9 @@
import MDEditor from '@uiw/react-md-editor';
import FormattingService from '../services/FormattingService';
export default function MarkdownRenderer(props: any) {
const { source } = props;
const newMarkdown = FormattingService.checkForSpiffFormats(source);
let wrapperClassName = '';
const propsToUse = props;
if ('wrapperClassName' in propsToUse) {
@ -10,7 +13,7 @@ export default function MarkdownRenderer(props: any) {
return (
<div data-color-mode="light" className={wrapperClassName}>
{/* eslint-disable-next-line react/jsx-props-no-spreading */}
<MDEditor.Markdown {...propsToUse} />
<MDEditor.Markdown {...{ ...propsToUse, ...{ source: newMarkdown } }} />
</div>
);
}

View File

@ -7,6 +7,7 @@ import {
} from '@carbon/icons-react';
// @ts-ignore
import { Button } from '@carbon/react';
import { ObjectWithStringKeysAndValues } from '../interfaces';
type OwnProps = {
title: string;
@ -17,6 +18,7 @@ type OwnProps = {
allowTogglingFullMessage?: boolean;
timeout?: number;
withBottomMargin?: boolean;
'data-qa'?: string;
};
export function Notification({
@ -28,6 +30,7 @@ export function Notification({
allowTogglingFullMessage = false,
timeout,
withBottomMargin = true,
'data-qa': dataQa,
}: OwnProps) {
const [showMessage, setShowMessage] = useState<boolean>(
!allowTogglingFullMessage
@ -48,8 +51,15 @@ export function Notification({
classes = `${classes} with-bottom-margin`;
}
const additionalProps: ObjectWithStringKeysAndValues = {};
if (dataQa) {
additionalProps['data-qa'] = dataQa;
}
return (
<div role="status" className={classes}>
// we control the props added to the variable so we know it's fine
// eslint-disable-next-line react/jsx-props-no-spreading
<div role="status" className={classes} {...additionalProps}>
<div className="cds--inline-notification__details">
<div className="cds--inline-notification__text-wrapper">
{iconComponent}

View File

@ -91,6 +91,7 @@ type OwnProps = {
url?: string;
callers?: ProcessReference[];
activeUserElement?: React.ReactElement;
disableSaveButton?: boolean;
};
const FitViewport = 'fit-viewport';
@ -121,6 +122,7 @@ export default function ReactDiagramEditor({
url,
callers,
activeUserElement,
disableSaveButton,
}: OwnProps) {
const [diagramXMLString, setDiagramXMLString] = useState('');
const [diagramModelerState, setDiagramModelerState] = useState(null);
@ -708,7 +710,13 @@ export default function ReactDiagramEditor({
a={targetUris.processModelFileShowPath}
ability={ability}
>
<Button onClick={handleSave}>Save</Button>
<Button
onClick={handleSave}
disabled={disableSaveButton}
data-qa="process-model-file-save-button"
>
Save
</Button>
</Can>
<Can
I="DELETE"

View File

@ -4,6 +4,8 @@
* See below for more details.
*/
import { FunctionComponent } from 'react';
// Current version of the extension uischema.
export type ExtensionUiSchemaVersion = '0.1' | '0.2';
@ -210,4 +212,8 @@ export interface ExtensionApiResponse {
// The markdown string rendered from the process model.
rendered_results_markdown?: string;
}
export type SupportedComponentList = {
[key in keyof typeof UiSchemaPageComponentList]: FunctionComponent<any>;
};
/** ************************************* */

View File

@ -1,62 +0,0 @@
/**
* These hooks re-implement the now removed useBlocker and usePrompt hooks in 'react-router-dom'.
* Thanks for the idea @piecyk https://github.com/remix-run/react-router/issues/8139#issuecomment-953816315
* Source: https://github.com/remix-run/react-router/commit/256cad70d3fd4500b1abcfea66f3ee622fb90874#diff-b60f1a2d4276b2a605c05e19816634111de2e8a4186fe9dd7de8e344b65ed4d3L344-L381
*/
import { useCallback, useContext, useEffect } from 'react';
import { UNSAFE_NavigationContext as NavigationContext } from 'react-router-dom';
/**
* Blocks all navigation attempts. This is useful for preventing the page from
* changing until some condition is met, like saving form data.
*
* @param blocker
* @param when
* @see https://reactrouter.com/api/useBlocker
*/
export function useBlocker(blocker: any, when: any = true) {
const { navigator } = useContext(NavigationContext);
useEffect(() => {
if (!when) {
return undefined;
}
const unblock = (navigator as any).block((tx: any) => {
const autoUnblockingTx = {
...tx,
retry() {
// Automatically unblock the transition so it can play all the way
// through before retrying it. TODO: Figure out how to re-enable
// this block if the transition is cancelled for some reason.
unblock();
tx.retry();
},
};
blocker(autoUnblockingTx);
});
return unblock;
}, [navigator, blocker, when]);
}
/**
* Prompts the user with an Alert before they leave the current screen.
*
* @param message
* @param when
*/
export function usePrompt(message: any, when: any = true) {
const blocker = useCallback(
(tx: any) => {
// eslint-disable-next-line no-alert
if (window.confirm(message)) {
tx.retry();
}
},
[message]
);
useBlocker(blocker, when);
}

View File

@ -148,9 +148,6 @@ export interface ProcessReference {
}
export type ObjectWithStringKeysAndValues = { [key: string]: string };
export type ObjectWithStringKeysAndFunctionValues = {
[key: string]: Function;
};
export interface FilterOperator {
id: string;

View File

@ -1,9 +1,9 @@
import { Routes, Route, useLocation } from 'react-router-dom';
import { useLocation, Routes, Route } from 'react-router-dom';
import React, { useEffect } from 'react';
import ProcessModelEditDiagram from './ProcessModelEditDiagram';
import ErrorDisplay from '../components/ErrorDisplay';
import LoginHandler from '../components/LoginHandler';
import ProcessModelEditDiagram from './ProcessModelEditDiagram';
export default function EditorRoutes() {
const location = useLocation();

View File

@ -1,12 +1,8 @@
import { useCallback, useEffect, useState } from 'react';
import { createElement, useCallback, useEffect, useState } from 'react';
import { useParams, useSearchParams } from 'react-router-dom';
import { Editor } from '@monaco-editor/react';
import { useUriListForPermissions } from '../hooks/UriListForPermissions';
import {
ObjectWithStringKeysAndFunctionValues,
ProcessFile,
ProcessModel,
} from '../interfaces';
import { ProcessFile, ProcessModel } from '../interfaces';
import HttpService from '../services/HttpService';
import useAPIError from '../hooks/UseApiError';
import { recursivelyChangeNullAndUndefined, makeid } from '../helpers';
@ -16,6 +12,7 @@ import {
ExtensionApiResponse,
ExtensionPostBody,
ExtensionUiSchema,
SupportedComponentList,
UiSchemaPageComponent,
UiSchemaPageDefinition,
} from '../extension_ui_schema_interfaces';
@ -65,7 +62,7 @@ export default function Extension({
const { addError, removeError } = useAPIError();
const supportedComponents: ObjectWithStringKeysAndFunctionValues = {
const supportedComponents: SupportedComponentList = {
CreateNewInstance,
CustomForm,
MarkdownRenderer,
@ -98,7 +95,6 @@ export default function Extension({
);
const processLoadResult = useCallback(
(result: ExtensionApiResponse, pageDefinition: UiSchemaPageDefinition) => {
setFormData(result.task_data);
if (pageDefinition.navigate_to_on_load) {
const optionString = interpolateNavigationString(
pageDefinition.navigate_to_on_load,
@ -114,6 +110,8 @@ export default function Extension({
);
setMarkdownToRenderOnLoad(newMarkdown);
}
const taskDataCopy = { ...result.task_data };
if (
pageDefinition.on_load &&
pageDefinition.on_load.ui_schema_page_components_variable
@ -123,7 +121,18 @@ export default function Extension({
pageDefinition.on_load.ui_schema_page_components_variable
]
);
// we were getting any AJV8Validator error when we had this data in the task data
// when we attempted to submit a form using this task data.
// The error was:
// Uncaught RangeError: Maximum call stack size exceeded
//
// Removing the ui schema page components dictionary seems to resolve it.
delete taskDataCopy[
pageDefinition.on_load.ui_schema_page_components_variable
];
}
setFormData(taskDataCopy);
setReadyForComponentsToDisplay(true);
},
[interpolateNavigationString]
@ -303,7 +312,7 @@ export default function Extension({
// eslint-disable-next-line sonarjs/cognitive-complexity
const renderComponentArguments = (component: UiSchemaPageComponent) => {
const argumentsForComponent: any = component.arguments;
if (processModel) {
if (processModel && argumentsForComponent) {
Object.keys(argumentsForComponent).forEach((argName: string) => {
const argValue = argumentsForComponent[argName];
if (
@ -368,7 +377,10 @@ export default function Extension({
if (supportedComponents[componentName]) {
const argumentsForComponent = renderComponentArguments(component);
componentsToDisplay.push(
supportedComponents[componentName](argumentsForComponent)
createElement(
supportedComponents[componentName],
argumentsForComponent
)
);
} else {
console.error(

View File

@ -47,7 +47,6 @@ import {
} from '../interfaces';
import ProcessSearch from '../components/ProcessSearch';
import { Notification } from '../components/Notification';
import { usePrompt } from '../hooks/UsePrompt';
import ActiveUsers from '../components/ActiveUsers';
import { useFocusedTabStatus } from '../hooks/useFocusedTabStatus';
@ -137,8 +136,6 @@ export default function ProcessModelEditDiagram() {
const [callers, setCallers] = useState<ProcessReference[]>([]);
usePrompt('Changes you made may not be saved.', diagramHasChanges);
const getProcessesCallback = useCallback((onProcessesFetched?: Function) => {
const processResults = (result: any) => {
const selectionArray = result.map((item: any) => {
@ -1054,7 +1051,7 @@ export default function ProcessModelEditDiagram() {
path = generatePath(
'/editor/process-models/:process_model_id/files/:file_name',
{
process_model_id: params.process_model_id,
process_model_id: params.process_model_id || null,
file_name: file.name,
}
);
@ -1063,7 +1060,7 @@ export default function ProcessModelEditDiagram() {
path = generatePath(
'/editor/process-models/:process_model_id/files?file_type=dmn',
{
process_model_id: params.process_model_id,
process_model_id: params.process_model_id || null,
}
);
}
@ -1121,6 +1118,7 @@ export default function ProcessModelEditDiagram() {
onElementsChanged={onElementsChanged}
callers={callers}
activeUserElement={<ActiveUsers />}
disableSaveButton={!diagramHasChanges}
/>
);
};
@ -1131,6 +1129,8 @@ export default function ProcessModelEditDiagram() {
<Notification
title="File Saved: "
onClose={() => setDisplaySaveFileMessage(false)}
hideCloseButton
timeout={3000}
>
Changes to the file were saved.
</Notification>
@ -1139,6 +1139,34 @@ export default function ProcessModelEditDiagram() {
return null;
};
const unsavedChangesMessage = () => {
if (diagramHasChanges) {
return (
<Notification
title="Unsaved changes."
type="error"
hideCloseButton
data-qa="process-model-file-changed"
>
Please save to avoid losing your work.
</Notification>
);
}
return null;
};
const pageModals = () => {
return (
<>
{newFileNameBox()}
{scriptEditorAndTests()}
{markdownEditor()}
{jsonSchemaEditor()}
{processModelSelector()}
</>
);
};
// if a file name is not given then this is a new model and the ReactDiagramEditor component will handle it
if ((bpmnXmlForDiagramRendering || !params.file_name) && processModel) {
const processModelFileName = processModelFile ? processModelFile.name : '';
@ -1159,13 +1187,13 @@ export default function ProcessModelEditDiagram() {
Process Model File{processModelFile ? ': ' : ''}
{processModelFileName}
</h1>
{pageModals()}
{unsavedChangesMessage()}
{saveFileMessage()}
{appropriateEditor()}
{newFileNameBox()}
{scriptEditorAndTests()}
{markdownEditor()}
{jsonSchemaEditor()}
{processModelSelector()}
<div id="diagram-container" />
</>
);