Feature/update extension docs (#1028)

* updated the docs around extensions and updated extensions interfaces in the frontend w/ burnettk

* allow specifying files in component args for extensions and added some support for CustomForm from extensions w/ burnettk

* added comments to the extension interfaces file to better describe how to create them

* finished adding comments to extension interfaces

* added comments at top and some minor tweaks

* some fixes for extensions w/ burnettk

* some fixes for extensions w/ burnettk

* ignore eslint issues for now w/ burnettk

* removed deprecated extension items w/ burnettk

---------

Co-authored-by: jasquat <jasquat@users.noreply.github.com>
This commit is contained in:
jasquat 2024-02-13 16:48:13 -05:00 committed by GitHub
parent a32ccf8caf
commit 6f0e59409c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 260 additions and 151 deletions

View File

@ -26,13 +26,12 @@ name: Docker Image For Main Builds
# Git tags for an image:
# curl -H "Authorization: Bearer $(echo -n $TOKEN | base64 -w0)" https://ghcr.io/v2/sartography/spiffworkflow-backend/tags/list | jq -r '.tags | sort_by(.)'
on:
push:
branches:
- main
- spiffdemo
- feature/background-proc-with-celery
- feature/update-extension-docs
jobs:
create_frontend_docker_image:

View File

@ -5,10 +5,11 @@ By leveraging extensions, users can implement functions or features not present
This powerful feature ensures adaptability to various business needs, from custom reports to specific user tools.
Here are some of key aspects of using Extensions:
- Extensions are implemented within the process model repository.
- Once an extension is created, it can be made accessible via the top navigation bar.
- Extensions are universal. Once added, they will be visible to all users and are not user-specific.
- Configuration for an extensions can be found and modified in its `extension-uischema.json` file.
- Extensions are implemented within the process model repository.
- Once an extension is created, it can be made accessible via various ui elements which can be specified in its `extension-uischema.json` file.
- Access to an extension can be set up via permissions.
- Configuration for an extensions can be found and modified in its `extension-uischema.json` file.
![Extensions](images/Extensions_dashboard.png)
@ -22,77 +23,42 @@ Here is the enviromental variable:
SPIFFWORKFLOW_BACKEND_EXTENSIONS_API_ENABLED=true
If SpiffWorkflow is being deployed using Docker Compose, add the environment variable under the selected section of your `docker-compose.yml` file as shown in screenshot:
![Enviromental variable](images/Extensions2.png)
By default, SpiffArena will look for extensions in `[SPIFFWORKFLOW_BACKEND_BPMN_SPEC_ABSOLUTE_DIR]/extensions` but that can be configured using `SPIFFWORKFLOW_BACKEND_EXTENSIONS_PROCESS_MODEL_PREFIX`.
### Creating an Extension
After enabling extensions from the backend, you can create extensions in the SpiffArena frontend.
To create your own custom extension, follow these steps:
- Navigate to the process model repository where extensions are to be implemented.
- Navigate to the process group repository where extensions are to be implemented.
![Extension Process Group](images/Extension1.png)
- Create the `extension-uischema.json` file. If you want to modify an existing extension, you can change the layout as well in existing models.
- Create a process model in this group. You can give it whatever name you want. Then create a file inside the process model called `extension-uischema.json`. This will control how the extension will work.
![Extension](images/Extension_UI_schema.png)
- To add a new extension with navigation, incorporate the navigation function within this JSON file. For instance, we are taking an example of aggregate metadata extension:
As an example, we have created an extension that adds a link to the profile menu in the top right, and also adds a new "Support" page to the app so that users of the application know who to talk to if they have issues.
You can find the full example [in github](https://github.com/sartography/sample-process-models/tree/sample-models-1/extensions/support).
``` json
{
"navigation_items": [{
"label": "Aggregate Metadata",
"route": "/aggregate-metadata"
}
],
"routes": {
"/aggregate-metadata": {
"header": "Aggregate Metdata",
"api": "aggregate-metadata",
"form_schema_filename": "we-aggregate-schema.json",
"form_ui_schema_filename": "we-aggregate-uischema.json",
"markdown_instruction_filename": "we-aggregate-markdown.md",
"results_markdown_filename": "we-aggregate-results-markdown.md"
}
}
}
Notice how the `display_location` "user_profile_item" tells it that the link belongs in the user profile menu (this is the top right menu where the logout link can be found).
Notice also that the extension uischema defines a page ("/support"), and defines the list of components that should show up on this page.
In this case, that is just a single MarkdownRenderer, which defines how to contact people.
```
The provided JSON structure describes configuration for a data aggregation feature with user interface components including navigation.
Here's a breakdown of the key components:
1. `navigation_items`: This is an array containing additional navigation items that should be added to the application. In this case, there's only one item:
- `"label"`: The label or display text for the navigation item is "Aggregate Metadata".
- `"route"`: The route or URL associated with the navigation item is "/aggregate-metadata". This indicates that clicking on this navigation item would take the user to this specific path.
2. `routes`: This is an object that defines new routes within the application. In this case, there's only one route defined:
- `"/aggregate-metadata"`: This is the URL route that corresponds to the previously mentioned navigation item. The details for this route are:
- `"header"`: The header or title for the page associated with this route is "Aggregate Metadata".
- `"api"`: This refers to an API endpoint that is used to retrieve or manipulate aggregated metadata.
- `"form_schema_filename"`: The filename "we-aggregate-schema.json" points to a JSON schema that describes the structure of the data to be submitted through a form on the "Aggregate Metadata" page.
- `"form_ui_schema_filename"`: The filename "we-aggregate-uischema.json" points to a UI schema that specifies how the form elements on the page should be rendered and arranged.
- `"markdown_instruction_filename"`: The filename "we-aggregate-markdown.md" points to a Markdown file containing instructions or guidance for the user when interacting with the form on the "Aggregate Metadata" page.
- `"results_markdown_filename"`: The filename "we-aggregate-results-markdown.md" points to a Markdown file where the results of the aggregation process will be displayed or explained.
This route has associated components such as a header, API endpoint, form schema, form UI schema, instructions in Markdown format, and a results display in Markdown format, all related to the process of aggregating metadata.
An entirely new feature application feature with frontend and backend components can therefore be implemented using an extension in this way.
- Deploy your changes and ensure the environment variable is activated to see your extensions in the top navigation bar and start adding new features to SpiffArena.
![Extension](images/Agregate_metadata.png)
An entirely new feature application feature with frontend and backend components can be implemented using an extension.
[This typescript interface file](https://github.com/sartography/spiff-arena/blob/main/spiffworkflow-frontend/src/extension_ui_schema_interfaces.ts) codifies the configuration of the extension uischema.
## Use Cases
If your organization has specific needs not catered to by the standard SpiffArena features, you can use extensions to add those features.
Here are some of the use cases already implemented by our users:
- Implementing a time tracking system.
- Creating custom reports tailored to your business metrics.
- Incorporating arbitrary content into custom pages using markdown.
- Creating and accessing tailor-made APIs.
- Rendering the output of these APIs using jinja templates and markdown.
- Implementing a time tracking system.
- Creating custom reports tailored to your business metrics.
- Incorporating arbitrary content into custom pages using markdown.
- Creating and accessing tailor-made APIs.
- Rendering the output of these APIs using jinja templates and markdown.
Extensions in SpiffArena offer a robust mechanism to tailor the software to unique business requirements.
When considering an extension, also consider whether the code would be more properly included in the core source code.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

View File

@ -249,7 +249,7 @@ class ProcessModelService(FileSystemService):
reference_cache_processes = ReferenceCacheModel.basic_query().filter_by(type="process").all()
process_models = cls.embellish_with_is_executable_property(process_models, reference_cache_processes)
if filter_runnable_by_user or filter_runnable_as_extension:
if filter_runnable_by_user:
process_models = cls.filter_by_runnable(process_models, reference_cache_processes)
permitted_process_models = []

View File

@ -1,6 +1,7 @@
import validator from '@rjsf/validator-ajv8';
import { ReactNode } from 'react';
import { RegistryFieldsType } from '@rjsf/utils';
import { Button } from '@carbon/react';
import { Form } from '../rjsf/carbon_theme';
import { DATE_RANGE_DELIMITER } from '../config';
import DateRangePickerWidget from '../rjsf/custom_widgets/DateRangePicker/DateRangePickerWidget';
@ -26,6 +27,7 @@ type OwnProps = {
children?: ReactNode;
noValidate?: boolean;
restrictedWidth?: boolean;
submitButtonText?: string;
};
export default function CustomForm({
@ -39,6 +41,7 @@ export default function CustomForm({
children,
noValidate = false,
restrictedWidth = false,
submitButtonText,
}: OwnProps) {
// set in uiSchema using the "ui:widget" key for a property
const rjsfWidgets = {
@ -357,6 +360,15 @@ export default function CustomForm({
return checkFieldsWithCustomValidations(schema, formDataToCheck, errors);
};
let childrenToUse = children;
if (submitButtonText) {
childrenToUse = (
<Button type="submit" id="submit-button" disabled={disabled}>
{submitButtonText}
</Button>
);
}
return (
<Form
id={id}
@ -374,7 +386,7 @@ export default function CustomForm({
templates={rjsfTemplates}
omitExtraData
>
{children}
{childrenToUse}
</Form>
);
}

View File

@ -18,17 +18,17 @@ export function ExtensionUxElementMap({
}
const mainElement = () => {
let foundElement = false;
const elementMap = extensionUxElements.map(
(uxElement: UiSchemaUxElement, index: number) => {
if (uxElement.display_location === displayLocation) {
foundElement = true;
return elementCallback(uxElement, index);
}
return null;
const elementsForDisplayLocation = extensionUxElements.filter(
(uxElement: UiSchemaUxElement) => {
return uxElement.display_location === displayLocation;
}
);
if (!foundElement && elementCallbackIfNotFound) {
const elementMap = elementsForDisplayLocation.map(
(uxElement: UiSchemaUxElement, index: number) => {
return elementCallback(uxElement, index);
}
);
if (elementMap.length === 0 && elementCallbackIfNotFound) {
return elementCallbackIfNotFound();
}
return elementMap;

View File

@ -316,7 +316,7 @@ export default function ReactFormBuilder({
setFormData(JSON.parse(result.file_contents));
} catch (e) {
// todo: show error message
console.log('Error parsing JSON:', e);
console.error('Error parsing JSON:', e);
}
}

View File

@ -1,17 +1,63 @@
/**
* These are the configurations that can be added to the extension_uischema.json file for an extension.
* The top-level object should be an ExtensionUiSchema.
* See below for more details.
*/
// Current version of the extension uischema.
export type ExtensionUiSchemaVersion = '0.1' | '0.2';
// All locations that can be specified to display the link to use the extensions.
export enum UiSchemaDisplayLocation {
// Will appear as a tab under the "Configuration" top nav item.
configuration_tab_item = 'configuration_tab_item',
// Will appear in the top nav bar
header_menu_item = 'header_menu_item',
/**
* The page will be used as a route.
* This route can then be referenced from another ux element or can override other routes.
*/
routes = 'routes',
// Will appear in the user profile drop - top right menu with the logout button.
user_profile_item = 'user_profile_item',
}
/**
* Determines whether or not a process instance is saved to the database when this extension runs.
* By default this will be "none" but it can be set to "full" for debugging.
*/
export enum UiSchemaPersistenceLevel {
full = 'full',
none = 'none',
}
/**
* Supported components that can be used on pages.
* These can be found under "src/components" with more details about how to use.
* 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',
ProcessInstanceRun = 'ProcessInstanceRun',
SpiffTabs = 'SpiffTabs',
}
// Configs that are specific to certain display locations
export interface UiSchemaLocationSpecificConfig {
/**
* Currently only supported on the "configuration_tab_item" location.
* Specifies which pages should cause the tab item to become highlighted.
*/
highlight_on_tabs?: string[];
}
// Primary ux element - decribes how the extension should be displayed and accessed from the web ui.
export interface UiSchemaUxElement {
label: string;
page: string;
@ -26,56 +72,141 @@ export interface UiSchemaForm {
form_submit_button_label?: string;
}
// The action that should be taken when something happens such as a form submit.
export interface UiSchemaAction {
/**
* The api_path to call.
* This will normally just be the colon delimited process model identifier for the extension minus the extension process group.
* For example: extensions/path/to/extension -> path:to:extension
*/
api_path: string;
ui_schema_page_components_variable?: string;
/**
* By default, when submitting an action it makes the call to the extension endpoint in backend.
* This tells the web ui to use the api_path as the full path and removes the extension portion.
*/
is_full_api_path?: boolean;
persistence_level?: UiSchemaPersistenceLevel;
/**
* The bpmn process id of the bpmn diagram.
* If there are multiple bpmn files within the process model, this can be used to specify which process to run.
* If there is only one bpmn file or if only the primary file is used, then this is not needed.
*/
process_id_to_run?: string;
/**
* Markdown file to display the results with.
* This file can use jinja and can reference task data created from the process similar markdown used from within a process model.
*/
results_markdown_filename?: string;
/**
* Parameters to grab from the search params of the url.
* This is useful when linking from one extension to another so params can be grabbed and given to the process model when running.
*/
search_params_to_inject?: string[];
full_api_path?: boolean;
/**
* The variable name in the task data that will define the components to use.
* This is useful if the components for the page need to be generated more dynamically.
* This variable should be defined from the on_load process.
*/
ui_schema_page_components_variable?: string;
}
// Component to use for the page
export interface UiSchemaPageComponent {
name: string;
// The name must match a value in "UiSchemaPageComponentList".
name: keyof typeof UiSchemaPageComponentList;
/**
* Arguments given to the component.
* Details above under "UiSchemaPageComponentList".
*
* If an argument is a string prepended by SPIFF_PROCESS_MODEL_FILE if will look for a file defined in that process model.
* FROM_JSON can be used with SPIFF_PROCESS_MODEL_FILE to tell frontend to load the contents with JSON.parse
* Example: "SPIFF_PROCESS_MODEL_FILE:FROM_JSON:::filename.json"
* Example: "SPIFF_PROCESS_MODEL_FILE:::filename.md"
*/
arguments: object;
/**
* Instead of posting the extension api, this makes it set the "href" to the api_path.
* This is useful if the intent is download a file.
*/
navigate_instead_of_post_to_api?: boolean;
/**
* Path to navigate to after submitting the form.
* This will interpolate patterns like "{task_data_var}" if found in the task data.
*/
navigate_to_on_form_submit?: string;
on_form_submit?: UiSchemaAction;
}
// The primary definition for a page.
export interface UiSchemaPageDefinition {
header: string;
api: string;
// Primary header to use for the page.
header?: string;
components?: UiSchemaPageComponent[];
form?: UiSchemaForm;
markdown_instruction_filename?: string;
navigate_instead_of_post_to_api?: boolean;
navigate_to_on_form_submit?: string;
/**
* Path to navigate to after calling the on_load api.
* This will interpolate patterns like "{task_data_var}" if found in the task data.
*/
navigate_to_on_load?: string;
on_form_submit?: UiSchemaAction;
/**
* The on_load action to use.
* Useful for gathering data from a process model when loading the extension.
*/
on_load?: UiSchemaAction;
/**
* Specifies whether or not open links specified in the markdown to open in a new tab or not.
* NOTE: this gets used for both the markdown_instruction_filename and markdown returned from the on_load.
* It may be better to move functionality to the action but not 100% sure how.
*/
open_links_in_new_tab?: boolean;
}
// The name of the page along with its definition.
export interface UiSchemaPage {
[key: string]: UiSchemaPageDefinition;
}
/**
* Top-level object in the extension_uischema.json file.
* Read the interfaces above for more info.
*/
export interface ExtensionUiSchema {
pages: UiSchemaPage;
disabled?: boolean;
ux_elements?: UiSchemaUxElement[];
version?: ExtensionUiSchemaVersion;
// Disable the extension which is useful during development of an extension.
disabled?: boolean;
}
/** ********************************************
* These are types given to and received from the api calls.
* These are not specified from within the extension_uischema.json file.
*/
export interface ExtensionPostBody {
extension_input: any;
ui_schema_action?: UiSchemaAction;
}
// The response returned from the backend
export interface ExtensionApiResponse {
// Task data generated from the process model.
task_data: any;
// The markdown string rendered from the process model.
rendered_results_markdown?: string;
ui_schema_page_components?: UiSchemaPageComponent[];
}
/** ************************************* */

View File

@ -1,5 +1,4 @@
import { useCallback, useEffect, useState } from 'react';
import { Button } from '@carbon/react';
import { useParams, useSearchParams } from 'react-router-dom';
import { Editor } from '@monaco-editor/react';
import { useUriListForPermissions } from '../hooks/UriListForPermissions';
@ -10,7 +9,7 @@ import {
} from '../interfaces';
import HttpService from '../services/HttpService';
import useAPIError from '../hooks/UseApiError';
import { recursivelyChangeNullAndUndefined } from '../helpers';
import { recursivelyChangeNullAndUndefined, makeid } from '../helpers';
import CustomForm from '../components/CustomForm';
import { BACKEND_BASE_URL } from '../config';
import {
@ -43,7 +42,7 @@ export default function Extension({
const params = useParams();
const [searchParams] = useSearchParams();
const [_processModel, setProcessModel] = useState<ProcessModel | null>(null);
const [processModel, setProcessModel] = useState<ProcessModel | null>(null);
const [formData, setFormData] = useState<any>(null);
const [formButtonsDisabled, setFormButtonsDisabled] = useState(false);
const [processedTaskData, setProcessedTaskData] = useState<any>(null);
@ -68,6 +67,7 @@ export default function Extension({
const supportedComponents: ObjectWithStringKeysAndFunctionValues = {
CreateNewInstance,
CustomForm,
MarkdownRenderer,
ProcessInstanceListTable,
ProcessInstanceRun,
@ -215,13 +215,13 @@ export default function Extension({
targetUris.extensionPath,
]);
const processSubmitResult = (result: ExtensionApiResponse) => {
if (
uiSchemaPageDefinition &&
uiSchemaPageDefinition.navigate_to_on_form_submit
) {
const processSubmitResult = (
pageComponent: UiSchemaPageComponent,
result: ExtensionApiResponse
) => {
if (pageComponent && pageComponent.navigate_to_on_form_submit) {
const optionString = interpolateNavigationString(
uiSchemaPageDefinition.navigate_to_on_form_submit,
pageComponent.navigate_to_on_form_submit,
result.task_data
);
if (optionString !== null) {
@ -239,8 +239,12 @@ export default function Extension({
}
};
// eslint-disable-next-line sonarjs/cognitive-complexity
const handleFormSubmit = (formObject: any, event: any) => {
const handleFormSubmit = (
pageComponent: UiSchemaPageComponent,
formObject: any,
event: any
// eslint-disable-next-line sonarjs/cognitive-complexity
) => {
event.preventDefault();
if (formButtonsDisabled) {
@ -254,14 +258,11 @@ export default function Extension({
removeError();
delete dataToSubmit.isManualTask;
if (
uiSchemaPageDefinition &&
uiSchemaPageDefinition.navigate_instead_of_post_to_api
) {
if (pageComponent && pageComponent.navigate_instead_of_post_to_api) {
let optionString: string | null = '';
if (uiSchemaPageDefinition.navigate_to_on_form_submit) {
if (pageComponent.navigate_to_on_form_submit) {
optionString = interpolateNavigationString(
uiSchemaPageDefinition.navigate_to_on_form_submit,
pageComponent.navigate_to_on_form_submit,
dataToSubmit
);
if (optionString !== null) {
@ -272,14 +273,14 @@ export default function Extension({
} else {
let postBody: ExtensionPostBody = { extension_input: dataToSubmit };
let apiPath = targetUris.extensionPath;
if (uiSchemaPageDefinition && uiSchemaPageDefinition.on_form_submit) {
if (uiSchemaPageDefinition.on_form_submit.full_api_path) {
apiPath = `/${uiSchemaPageDefinition.on_form_submit.api_path}`;
if (pageComponent && pageComponent.on_form_submit) {
if (pageComponent.on_form_submit.is_full_api_path) {
apiPath = `/${pageComponent.on_form_submit.api_path}`;
postBody = dataToSubmit;
} else {
apiPath = `${targetUris.extensionListPath}/${uiSchemaPageDefinition.on_form_submit.api_path}`;
apiPath = `${targetUris.extensionListPath}/${pageComponent.on_form_submit.api_path}`;
}
postBody.ui_schema_action = uiSchemaPageDefinition.on_form_submit;
postBody.ui_schema_action = pageComponent.on_form_submit;
}
// NOTE: rjsf sets blanks values to undefined and JSON.stringify removes keys with undefined values
@ -287,7 +288,8 @@ export default function Extension({
recursivelyChangeNullAndUndefined(dataToSubmit, null);
HttpService.makeCallToBackend({
path: apiPath,
successCallback: processSubmitResult,
successCallback: (result: ExtensionApiResponse) =>
processSubmitResult(pageComponent, result),
failureCallback: (error: any) => {
addError(error);
setFormButtonsDisabled(false);
@ -298,18 +300,49 @@ export default function Extension({
}
};
// eslint-disable-next-line sonarjs/cognitive-complexity
const renderComponentArguments = (component: UiSchemaPageComponent) => {
const argumentsForComponent: any = component.arguments;
if (processModel) {
Object.keys(argumentsForComponent).forEach((argName: string) => {
const argValue = argumentsForComponent[argName];
if (
typeof argValue === 'string' &&
argValue.startsWith('SPIFF_PROCESS_MODEL_FILE:')
) {
const [macro, fileName] = argValue.split(':::');
const macroList = macro.split(':');
const pmFileForArg = processModel.files.find(
(pmFile: ProcessFile) => {
return pmFile.name === fileName;
}
);
if (pmFileForArg) {
let newArgValue = pmFileForArg.file_contents;
if (macroList.includes('FROM_JSON')) {
newArgValue = JSON.parse(newArgValue || '{}');
}
argumentsForComponent[argName] = newArgValue;
}
}
});
}
if (component.name === 'CustomForm') {
argumentsForComponent.onSubmit = (formObject: any, event: any) =>
handleFormSubmit(component, formObject, event);
argumentsForComponent.formData = formData;
argumentsForComponent.id = argumentsForComponent.id || makeid(20);
argumentsForComponent.onChange = (obj: any) => {
setFormData(obj.formData);
};
}
return argumentsForComponent;
};
if (readyForComponentsToDisplay && uiSchemaPageDefinition) {
const componentsToDisplay = [<h1>{uiSchemaPageDefinition.header}</h1>];
const markdownContentsToRender = [];
if (uiSchemaPageDefinition.markdown_instruction_filename) {
const markdownFile =
filesByName[uiSchemaPageDefinition.markdown_instruction_filename];
if (markdownFile.file_contents) {
markdownContentsToRender.push(markdownFile.file_contents);
}
}
if (markdownToRenderOnLoad) {
markdownContentsToRender.push(markdownToRenderOnLoad);
}
@ -331,10 +364,11 @@ export default function Extension({
if (uiSchemaPageComponents) {
uiSchemaPageComponents.forEach((component: UiSchemaPageComponent) => {
if (supportedComponents[component.name]) {
const argumentsForComponent: any = component.arguments;
const componentName = component.name;
if (supportedComponents[componentName]) {
const argumentsForComponent = renderComponentArguments(component);
componentsToDisplay.push(
supportedComponents[component.name](argumentsForComponent)
supportedComponents[componentName](argumentsForComponent)
);
} else {
console.error(
@ -344,37 +378,6 @@ export default function Extension({
});
}
const uiSchemaForm = uiSchemaPageDefinition.form;
if (uiSchemaForm) {
const formSchemaFile = filesByName[uiSchemaForm.form_schema_filename];
const formUiSchemaFile =
filesByName[uiSchemaForm.form_ui_schema_filename];
const submitButtonText =
uiSchemaForm.form_submit_button_label || 'Submit';
if (formSchemaFile.file_contents && formUiSchemaFile.file_contents) {
componentsToDisplay.push(
<CustomForm
id="form-to-submit"
formData={formData}
onChange={(obj: any) => {
setFormData(obj.formData);
}}
disabled={formButtonsDisabled}
onSubmit={handleFormSubmit}
schema={JSON.parse(formSchemaFile.file_contents)}
uiSchema={JSON.parse(formUiSchemaFile.file_contents)}
>
<Button
type="submit"
id="submit-button"
disabled={formButtonsDisabled}
>
{submitButtonText}
</Button>
</CustomForm>
);
}
}
if (processedTaskData) {
if (markdownToRenderOnSubmit) {
componentsToDisplay.push(

View File

@ -556,8 +556,6 @@ export default function ProcessModelShow() {
navigate(
`/process-models/${modifiedProcessModelId}/form?file_ext=md`
);
} else {
console.log('a.selectedItem.text', a.selectedItem.text);
}
}}
items={items}