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:
parent
a32ccf8caf
commit
6f0e59409c
|
@ -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:
|
||||
|
|
|
@ -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:
|
||||
|
||||
``` 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
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).
|
||||
|
||||
```
|
||||
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:
|
||||
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.
|
||||
|
||||
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.
|
||||
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 |
|
@ -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 = []
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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[];
|
||||
}
|
||||
/** ************************************* */
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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}
|
||||
|
|
Loading…
Reference in New Issue