diff --git a/spiffworkflow-frontend/cypress/e2e/process_instances.cy.js b/spiffworkflow-frontend/cypress/e2e/process_instances.cy.js
index 629875a7f..a0140fd51 100644
--- a/spiffworkflow-frontend/cypress/e2e/process_instances.cy.js
+++ b/spiffworkflow-frontend/cypress/e2e/process_instances.cy.js
@@ -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
diff --git a/spiffworkflow-frontend/cypress/e2e/process_models.cy.js b/spiffworkflow-frontend/cypress/e2e/process_models.cy.js
index 05c217dba..f17cd36d0 100644
--- a/spiffworkflow-frontend/cypress/e2e/process_models.cy.js
+++ b/spiffworkflow-frontend/cypress/e2e/process_models.cy.js
@@ -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}`);
diff --git a/spiffworkflow-frontend/package-lock.json b/spiffworkflow-frontend/package-lock.json
index f55a4c393..463763990 100644
--- a/spiffworkflow-frontend/package-lock.json
+++ b/spiffworkflow-frontend/package-lock.json
@@ -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": {
diff --git a/spiffworkflow-frontend/package.json b/spiffworkflow-frontend/package.json
index 70b5d58b2..149176e61 100644
--- a/spiffworkflow-frontend/package.json
+++ b/spiffworkflow-frontend/package.json
@@ -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",
diff --git a/spiffworkflow-frontend/src/App.tsx b/spiffworkflow-frontend/src/App.tsx
index 998a6605f..31adaf5c0 100644
--- a/spiffworkflow-frontend/src/App.tsx
+++ b/spiffworkflow-frontend/src/App.tsx
@@ -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 (
- {/* @ts-ignore */}
-
-
-
-
-
-
-
+
+
+
+
+
);
}
diff --git a/spiffworkflow-frontend/src/ContainerForExtensions.tsx b/spiffworkflow-frontend/src/ContainerForExtensions.tsx
index ec62e7474..be65af25b 100644
--- a/spiffworkflow-frontend/src/ContainerForExtensions.tsx
+++ b/spiffworkflow-frontend/src/ContainerForExtensions.tsx
@@ -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 (
-
- }
- />
- } />
- } />
- } />
-
- );
+ return [
+ {
+ path: '*',
+ element: ,
+ },
+ { path: 'editor/*', element: },
+ { path: 'extensions/:page_identifier', element: },
+ { path: 'login', element: },
+ ];
};
const backendIsDownPage = () => {
- return ;
+ return [];
};
const innerComponents = () => {
if (backendIsUp === null) {
- return null;
+ return [];
}
if (backendIsUp) {
- return routeComponents();
+ return ;
}
return backendIsDownPage();
};
- return (
- <>
- ;
-
-
-
- {innerComponents()}
-
-
- >
- );
+ const layout = () => {
+ return (
+ <>
+ ;
+
+
+
+ {innerComponents()}
+
+
+ >
+ );
+ };
+
+ const router = createBrowserRouter([
+ {
+ path: '*',
+ Component: layout,
+ children: routeComponents(),
+ },
+ ]);
+ return ;
}
diff --git a/spiffworkflow-frontend/src/components/MarkdownRenderer.tsx b/spiffworkflow-frontend/src/components/MarkdownRenderer.tsx
index f4d52ad23..f7094bae4 100644
--- a/spiffworkflow-frontend/src/components/MarkdownRenderer.tsx
+++ b/spiffworkflow-frontend/src/components/MarkdownRenderer.tsx
@@ -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 (
{/* eslint-disable-next-line react/jsx-props-no-spreading */}
-
+
);
}
diff --git a/spiffworkflow-frontend/src/components/Notification.tsx b/spiffworkflow-frontend/src/components/Notification.tsx
index 3cfc66ab2..3c65b5645 100644
--- a/spiffworkflow-frontend/src/components/Notification.tsx
+++ b/spiffworkflow-frontend/src/components/Notification.tsx
@@ -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(
!allowTogglingFullMessage
@@ -48,8 +51,15 @@ export function Notification({
classes = `${classes} with-bottom-margin`;
}
+ const additionalProps: ObjectWithStringKeysAndValues = {};
+ if (dataQa) {
+ additionalProps['data-qa'] = dataQa;
+ }
+
return (
-
+ // we control the props added to the variable so we know it's fine
+ // eslint-disable-next-line react/jsx-props-no-spreading
+
{iconComponent}
diff --git a/spiffworkflow-frontend/src/components/ReactDiagramEditor.tsx b/spiffworkflow-frontend/src/components/ReactDiagramEditor.tsx
index b56eec234..25d4aca70 100644
--- a/spiffworkflow-frontend/src/components/ReactDiagramEditor.tsx
+++ b/spiffworkflow-frontend/src/components/ReactDiagramEditor.tsx
@@ -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}
>
-
+
;
+};
/** ************************************* */
diff --git a/spiffworkflow-frontend/src/hooks/UsePrompt.tsx b/spiffworkflow-frontend/src/hooks/UsePrompt.tsx
deleted file mode 100644
index f2a8a05e2..000000000
--- a/spiffworkflow-frontend/src/hooks/UsePrompt.tsx
+++ /dev/null
@@ -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);
-}
diff --git a/spiffworkflow-frontend/src/interfaces.ts b/spiffworkflow-frontend/src/interfaces.ts
index dfa093a3a..d8dfb380c 100644
--- a/spiffworkflow-frontend/src/interfaces.ts
+++ b/spiffworkflow-frontend/src/interfaces.ts
@@ -148,9 +148,6 @@ export interface ProcessReference {
}
export type ObjectWithStringKeysAndValues = { [key: string]: string };
-export type ObjectWithStringKeysAndFunctionValues = {
- [key: string]: Function;
-};
export interface FilterOperator {
id: string;
diff --git a/spiffworkflow-frontend/src/routes/EditorRoutes.tsx b/spiffworkflow-frontend/src/routes/EditorRoutes.tsx
index b35031ebf..0e3885766 100644
--- a/spiffworkflow-frontend/src/routes/EditorRoutes.tsx
+++ b/spiffworkflow-frontend/src/routes/EditorRoutes.tsx
@@ -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();
diff --git a/spiffworkflow-frontend/src/routes/Extension.tsx b/spiffworkflow-frontend/src/routes/Extension.tsx
index 70639380e..245fdc3d0 100644
--- a/spiffworkflow-frontend/src/routes/Extension.tsx
+++ b/spiffworkflow-frontend/src/routes/Extension.tsx
@@ -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(
diff --git a/spiffworkflow-frontend/src/routes/ProcessModelEditDiagram.tsx b/spiffworkflow-frontend/src/routes/ProcessModelEditDiagram.tsx
index 8238be25e..db007b5e0 100644
--- a/spiffworkflow-frontend/src/routes/ProcessModelEditDiagram.tsx
+++ b/spiffworkflow-frontend/src/routes/ProcessModelEditDiagram.tsx
@@ -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([]);
- 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={}
+ disableSaveButton={!diagramHasChanges}
/>
);
};
@@ -1131,6 +1129,8 @@ export default function ProcessModelEditDiagram() {
setDisplaySaveFileMessage(false)}
+ hideCloseButton
+ timeout={3000}
>
Changes to the file were saved.
@@ -1139,6 +1139,34 @@ export default function ProcessModelEditDiagram() {
return null;
};
+ const unsavedChangesMessage = () => {
+ if (diagramHasChanges) {
+ return (
+
+ Please save to avoid losing your work.
+
+ );
+ }
+ 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}
+
+ {pageModals()}
+
+ {unsavedChangesMessage()}
{saveFileMessage()}
+
{appropriateEditor()}
- {newFileNameBox()}
- {scriptEditorAndTests()}
- {markdownEditor()}
- {jsonSchemaEditor()}
- {processModelSelector()}
>
);