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:
|
# 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(.)'
|
# curl -H "Authorization: Bearer $(echo -n $TOKEN | base64 -w0)" https://ghcr.io/v2/sartography/spiffworkflow-backend/tags/list | jq -r '.tags | sort_by(.)'
|
||||||
|
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
- spiffdemo
|
- spiffdemo
|
||||||
- feature/background-proc-with-celery
|
- feature/update-extension-docs
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
create_frontend_docker_image:
|
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.
|
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:
|
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 implemented within the process model repository.
|
||||||
- Extensions are universal. Once added, they will be visible to all users and are not user-specific.
|
- Once an extension is created, it can be made accessible via various ui elements which can be specified in its `extension-uischema.json` file.
|
||||||
- Configuration for an extensions can be found and modified 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)
|
![Extensions](images/Extensions_dashboard.png)
|
||||||
|
|
||||||
|
@ -22,77 +23,42 @@ Here is the enviromental variable:
|
||||||
|
|
||||||
SPIFFWORKFLOW_BACKEND_EXTENSIONS_API_ENABLED=true
|
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:
|
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`.
|
||||||
|
|
||||||
![Enviromental variable](images/Extensions2.png)
|
|
||||||
|
|
||||||
### Creating an Extension
|
### Creating an Extension
|
||||||
|
|
||||||
After enabling extensions from the backend, you can create extensions in the SpiffArena frontend.
|
After enabling extensions from the backend, you can create extensions in the SpiffArena frontend.
|
||||||
To create your own custom extension, follow these steps:
|
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)
|
![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)
|
![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
|
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).
|
||||||
"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).
|
||||||
The provided JSON structure describes configuration for a data aggregation feature with user interface components including navigation.
|
Notice also that the extension uischema defines a page ("/support"), and defines the list of components that should show up on this page.
|
||||||
Here's a breakdown of the key components:
|
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:
|
An entirely new feature application feature with frontend and backend components can be implemented using an extension.
|
||||||
- `"label"`: The label or display text for the navigation item is "Aggregate Metadata".
|
[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.
|
||||||
- `"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)
|
|
||||||
|
|
||||||
## Use Cases
|
## 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:
|
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.
|
- Implementing a time tracking system.
|
||||||
- Incorporating arbitrary content into custom pages using markdown.
|
- Creating custom reports tailored to your business metrics.
|
||||||
- Creating and accessing tailor-made APIs.
|
- Incorporating arbitrary content into custom pages using markdown.
|
||||||
- Rendering the output of these APIs using jinja templates and 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.
|
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.
|
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()
|
reference_cache_processes = ReferenceCacheModel.basic_query().filter_by(type="process").all()
|
||||||
process_models = cls.embellish_with_is_executable_property(process_models, reference_cache_processes)
|
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)
|
process_models = cls.filter_by_runnable(process_models, reference_cache_processes)
|
||||||
|
|
||||||
permitted_process_models = []
|
permitted_process_models = []
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import validator from '@rjsf/validator-ajv8';
|
import validator from '@rjsf/validator-ajv8';
|
||||||
import { ReactNode } from 'react';
|
import { ReactNode } from 'react';
|
||||||
import { RegistryFieldsType } from '@rjsf/utils';
|
import { RegistryFieldsType } from '@rjsf/utils';
|
||||||
|
import { Button } from '@carbon/react';
|
||||||
import { Form } from '../rjsf/carbon_theme';
|
import { Form } from '../rjsf/carbon_theme';
|
||||||
import { DATE_RANGE_DELIMITER } from '../config';
|
import { DATE_RANGE_DELIMITER } from '../config';
|
||||||
import DateRangePickerWidget from '../rjsf/custom_widgets/DateRangePicker/DateRangePickerWidget';
|
import DateRangePickerWidget from '../rjsf/custom_widgets/DateRangePicker/DateRangePickerWidget';
|
||||||
|
@ -26,6 +27,7 @@ type OwnProps = {
|
||||||
children?: ReactNode;
|
children?: ReactNode;
|
||||||
noValidate?: boolean;
|
noValidate?: boolean;
|
||||||
restrictedWidth?: boolean;
|
restrictedWidth?: boolean;
|
||||||
|
submitButtonText?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function CustomForm({
|
export default function CustomForm({
|
||||||
|
@ -39,6 +41,7 @@ export default function CustomForm({
|
||||||
children,
|
children,
|
||||||
noValidate = false,
|
noValidate = false,
|
||||||
restrictedWidth = false,
|
restrictedWidth = false,
|
||||||
|
submitButtonText,
|
||||||
}: OwnProps) {
|
}: OwnProps) {
|
||||||
// set in uiSchema using the "ui:widget" key for a property
|
// set in uiSchema using the "ui:widget" key for a property
|
||||||
const rjsfWidgets = {
|
const rjsfWidgets = {
|
||||||
|
@ -357,6 +360,15 @@ export default function CustomForm({
|
||||||
return checkFieldsWithCustomValidations(schema, formDataToCheck, errors);
|
return checkFieldsWithCustomValidations(schema, formDataToCheck, errors);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let childrenToUse = children;
|
||||||
|
if (submitButtonText) {
|
||||||
|
childrenToUse = (
|
||||||
|
<Button type="submit" id="submit-button" disabled={disabled}>
|
||||||
|
{submitButtonText}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form
|
<Form
|
||||||
id={id}
|
id={id}
|
||||||
|
@ -374,7 +386,7 @@ export default function CustomForm({
|
||||||
templates={rjsfTemplates}
|
templates={rjsfTemplates}
|
||||||
omitExtraData
|
omitExtraData
|
||||||
>
|
>
|
||||||
{children}
|
{childrenToUse}
|
||||||
</Form>
|
</Form>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,17 +18,17 @@ export function ExtensionUxElementMap({
|
||||||
}
|
}
|
||||||
|
|
||||||
const mainElement = () => {
|
const mainElement = () => {
|
||||||
let foundElement = false;
|
const elementsForDisplayLocation = extensionUxElements.filter(
|
||||||
const elementMap = extensionUxElements.map(
|
(uxElement: UiSchemaUxElement) => {
|
||||||
(uxElement: UiSchemaUxElement, index: number) => {
|
return uxElement.display_location === displayLocation;
|
||||||
if (uxElement.display_location === displayLocation) {
|
|
||||||
foundElement = true;
|
|
||||||
return elementCallback(uxElement, index);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
if (!foundElement && elementCallbackIfNotFound) {
|
const elementMap = elementsForDisplayLocation.map(
|
||||||
|
(uxElement: UiSchemaUxElement, index: number) => {
|
||||||
|
return elementCallback(uxElement, index);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
if (elementMap.length === 0 && elementCallbackIfNotFound) {
|
||||||
return elementCallbackIfNotFound();
|
return elementCallbackIfNotFound();
|
||||||
}
|
}
|
||||||
return elementMap;
|
return elementMap;
|
||||||
|
|
|
@ -316,7 +316,7 @@ export default function ReactFormBuilder({
|
||||||
setFormData(JSON.parse(result.file_contents));
|
setFormData(JSON.parse(result.file_contents));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// todo: show error message
|
// 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 {
|
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',
|
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',
|
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 {
|
export enum UiSchemaPersistenceLevel {
|
||||||
full = 'full',
|
full = 'full',
|
||||||
none = 'none',
|
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 {
|
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[];
|
highlight_on_tabs?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Primary ux element - decribes how the extension should be displayed and accessed from the web ui.
|
||||||
export interface UiSchemaUxElement {
|
export interface UiSchemaUxElement {
|
||||||
label: string;
|
label: string;
|
||||||
page: string;
|
page: string;
|
||||||
|
@ -26,56 +72,141 @@ export interface UiSchemaForm {
|
||||||
form_submit_button_label?: string;
|
form_submit_button_label?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The action that should be taken when something happens such as a form submit.
|
||||||
export interface UiSchemaAction {
|
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;
|
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;
|
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;
|
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;
|
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[];
|
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 {
|
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;
|
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 {
|
export interface UiSchemaPageDefinition {
|
||||||
header: string;
|
// Primary header to use for the page.
|
||||||
api: string;
|
header?: string;
|
||||||
|
|
||||||
components?: UiSchemaPageComponent[];
|
components?: UiSchemaPageComponent[];
|
||||||
form?: UiSchemaForm;
|
|
||||||
markdown_instruction_filename?: string;
|
/**
|
||||||
navigate_instead_of_post_to_api?: boolean;
|
* Path to navigate to after calling the on_load api.
|
||||||
navigate_to_on_form_submit?: string;
|
* This will interpolate patterns like "{task_data_var}" if found in the task data.
|
||||||
|
*/
|
||||||
navigate_to_on_load?: string;
|
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;
|
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;
|
open_links_in_new_tab?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The name of the page along with its definition.
|
||||||
export interface UiSchemaPage {
|
export interface UiSchemaPage {
|
||||||
[key: string]: UiSchemaPageDefinition;
|
[key: string]: UiSchemaPageDefinition;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Top-level object in the extension_uischema.json file.
|
||||||
|
* Read the interfaces above for more info.
|
||||||
|
*/
|
||||||
export interface ExtensionUiSchema {
|
export interface ExtensionUiSchema {
|
||||||
pages: UiSchemaPage;
|
pages: UiSchemaPage;
|
||||||
disabled?: boolean;
|
|
||||||
ux_elements?: UiSchemaUxElement[];
|
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 {
|
export interface ExtensionPostBody {
|
||||||
extension_input: any;
|
extension_input: any;
|
||||||
ui_schema_action?: UiSchemaAction;
|
ui_schema_action?: UiSchemaAction;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The response returned from the backend
|
||||||
export interface ExtensionApiResponse {
|
export interface ExtensionApiResponse {
|
||||||
|
// Task data generated from the process model.
|
||||||
task_data: any;
|
task_data: any;
|
||||||
|
|
||||||
|
// The markdown string rendered from the process model.
|
||||||
rendered_results_markdown?: string;
|
rendered_results_markdown?: string;
|
||||||
ui_schema_page_components?: UiSchemaPageComponent[];
|
|
||||||
}
|
}
|
||||||
|
/** ************************************* */
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import { useCallback, useEffect, useState } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
import { Button } from '@carbon/react';
|
|
||||||
import { useParams, useSearchParams } from 'react-router-dom';
|
import { useParams, useSearchParams } from 'react-router-dom';
|
||||||
import { Editor } from '@monaco-editor/react';
|
import { Editor } from '@monaco-editor/react';
|
||||||
import { useUriListForPermissions } from '../hooks/UriListForPermissions';
|
import { useUriListForPermissions } from '../hooks/UriListForPermissions';
|
||||||
|
@ -10,7 +9,7 @@ import {
|
||||||
} from '../interfaces';
|
} from '../interfaces';
|
||||||
import HttpService from '../services/HttpService';
|
import HttpService from '../services/HttpService';
|
||||||
import useAPIError from '../hooks/UseApiError';
|
import useAPIError from '../hooks/UseApiError';
|
||||||
import { recursivelyChangeNullAndUndefined } from '../helpers';
|
import { recursivelyChangeNullAndUndefined, makeid } from '../helpers';
|
||||||
import CustomForm from '../components/CustomForm';
|
import CustomForm from '../components/CustomForm';
|
||||||
import { BACKEND_BASE_URL } from '../config';
|
import { BACKEND_BASE_URL } from '../config';
|
||||||
import {
|
import {
|
||||||
|
@ -43,7 +42,7 @@ export default function Extension({
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
|
|
||||||
const [_processModel, setProcessModel] = useState<ProcessModel | null>(null);
|
const [processModel, setProcessModel] = useState<ProcessModel | null>(null);
|
||||||
const [formData, setFormData] = useState<any>(null);
|
const [formData, setFormData] = useState<any>(null);
|
||||||
const [formButtonsDisabled, setFormButtonsDisabled] = useState(false);
|
const [formButtonsDisabled, setFormButtonsDisabled] = useState(false);
|
||||||
const [processedTaskData, setProcessedTaskData] = useState<any>(null);
|
const [processedTaskData, setProcessedTaskData] = useState<any>(null);
|
||||||
|
@ -68,6 +67,7 @@ export default function Extension({
|
||||||
|
|
||||||
const supportedComponents: ObjectWithStringKeysAndFunctionValues = {
|
const supportedComponents: ObjectWithStringKeysAndFunctionValues = {
|
||||||
CreateNewInstance,
|
CreateNewInstance,
|
||||||
|
CustomForm,
|
||||||
MarkdownRenderer,
|
MarkdownRenderer,
|
||||||
ProcessInstanceListTable,
|
ProcessInstanceListTable,
|
||||||
ProcessInstanceRun,
|
ProcessInstanceRun,
|
||||||
|
@ -215,13 +215,13 @@ export default function Extension({
|
||||||
targetUris.extensionPath,
|
targetUris.extensionPath,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const processSubmitResult = (result: ExtensionApiResponse) => {
|
const processSubmitResult = (
|
||||||
if (
|
pageComponent: UiSchemaPageComponent,
|
||||||
uiSchemaPageDefinition &&
|
result: ExtensionApiResponse
|
||||||
uiSchemaPageDefinition.navigate_to_on_form_submit
|
) => {
|
||||||
) {
|
if (pageComponent && pageComponent.navigate_to_on_form_submit) {
|
||||||
const optionString = interpolateNavigationString(
|
const optionString = interpolateNavigationString(
|
||||||
uiSchemaPageDefinition.navigate_to_on_form_submit,
|
pageComponent.navigate_to_on_form_submit,
|
||||||
result.task_data
|
result.task_data
|
||||||
);
|
);
|
||||||
if (optionString !== null) {
|
if (optionString !== null) {
|
||||||
|
@ -239,8 +239,12 @@ export default function Extension({
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
const handleFormSubmit = (
|
||||||
const handleFormSubmit = (formObject: any, event: any) => {
|
pageComponent: UiSchemaPageComponent,
|
||||||
|
formObject: any,
|
||||||
|
event: any
|
||||||
|
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||||
|
) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
if (formButtonsDisabled) {
|
if (formButtonsDisabled) {
|
||||||
|
@ -254,14 +258,11 @@ export default function Extension({
|
||||||
removeError();
|
removeError();
|
||||||
delete dataToSubmit.isManualTask;
|
delete dataToSubmit.isManualTask;
|
||||||
|
|
||||||
if (
|
if (pageComponent && pageComponent.navigate_instead_of_post_to_api) {
|
||||||
uiSchemaPageDefinition &&
|
|
||||||
uiSchemaPageDefinition.navigate_instead_of_post_to_api
|
|
||||||
) {
|
|
||||||
let optionString: string | null = '';
|
let optionString: string | null = '';
|
||||||
if (uiSchemaPageDefinition.navigate_to_on_form_submit) {
|
if (pageComponent.navigate_to_on_form_submit) {
|
||||||
optionString = interpolateNavigationString(
|
optionString = interpolateNavigationString(
|
||||||
uiSchemaPageDefinition.navigate_to_on_form_submit,
|
pageComponent.navigate_to_on_form_submit,
|
||||||
dataToSubmit
|
dataToSubmit
|
||||||
);
|
);
|
||||||
if (optionString !== null) {
|
if (optionString !== null) {
|
||||||
|
@ -272,14 +273,14 @@ export default function Extension({
|
||||||
} else {
|
} else {
|
||||||
let postBody: ExtensionPostBody = { extension_input: dataToSubmit };
|
let postBody: ExtensionPostBody = { extension_input: dataToSubmit };
|
||||||
let apiPath = targetUris.extensionPath;
|
let apiPath = targetUris.extensionPath;
|
||||||
if (uiSchemaPageDefinition && uiSchemaPageDefinition.on_form_submit) {
|
if (pageComponent && pageComponent.on_form_submit) {
|
||||||
if (uiSchemaPageDefinition.on_form_submit.full_api_path) {
|
if (pageComponent.on_form_submit.is_full_api_path) {
|
||||||
apiPath = `/${uiSchemaPageDefinition.on_form_submit.api_path}`;
|
apiPath = `/${pageComponent.on_form_submit.api_path}`;
|
||||||
postBody = dataToSubmit;
|
postBody = dataToSubmit;
|
||||||
} else {
|
} 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
|
// 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);
|
recursivelyChangeNullAndUndefined(dataToSubmit, null);
|
||||||
HttpService.makeCallToBackend({
|
HttpService.makeCallToBackend({
|
||||||
path: apiPath,
|
path: apiPath,
|
||||||
successCallback: processSubmitResult,
|
successCallback: (result: ExtensionApiResponse) =>
|
||||||
|
processSubmitResult(pageComponent, result),
|
||||||
failureCallback: (error: any) => {
|
failureCallback: (error: any) => {
|
||||||
addError(error);
|
addError(error);
|
||||||
setFormButtonsDisabled(false);
|
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) {
|
if (readyForComponentsToDisplay && uiSchemaPageDefinition) {
|
||||||
const componentsToDisplay = [<h1>{uiSchemaPageDefinition.header}</h1>];
|
const componentsToDisplay = [<h1>{uiSchemaPageDefinition.header}</h1>];
|
||||||
const markdownContentsToRender = [];
|
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) {
|
if (markdownToRenderOnLoad) {
|
||||||
markdownContentsToRender.push(markdownToRenderOnLoad);
|
markdownContentsToRender.push(markdownToRenderOnLoad);
|
||||||
}
|
}
|
||||||
|
@ -331,10 +364,11 @@ export default function Extension({
|
||||||
|
|
||||||
if (uiSchemaPageComponents) {
|
if (uiSchemaPageComponents) {
|
||||||
uiSchemaPageComponents.forEach((component: UiSchemaPageComponent) => {
|
uiSchemaPageComponents.forEach((component: UiSchemaPageComponent) => {
|
||||||
if (supportedComponents[component.name]) {
|
const componentName = component.name;
|
||||||
const argumentsForComponent: any = component.arguments;
|
if (supportedComponents[componentName]) {
|
||||||
|
const argumentsForComponent = renderComponentArguments(component);
|
||||||
componentsToDisplay.push(
|
componentsToDisplay.push(
|
||||||
supportedComponents[component.name](argumentsForComponent)
|
supportedComponents[componentName](argumentsForComponent)
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
console.error(
|
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 (processedTaskData) {
|
||||||
if (markdownToRenderOnSubmit) {
|
if (markdownToRenderOnSubmit) {
|
||||||
componentsToDisplay.push(
|
componentsToDisplay.push(
|
||||||
|
|
|
@ -556,8 +556,6 @@ export default function ProcessModelShow() {
|
||||||
navigate(
|
navigate(
|
||||||
`/process-models/${modifiedProcessModelId}/form?file_ext=md`
|
`/process-models/${modifiedProcessModelId}/form?file_ext=md`
|
||||||
);
|
);
|
||||||
} else {
|
|
||||||
console.log('a.selectedItem.text', a.selectedItem.text);
|
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
items={items}
|
items={items}
|
||||||
|
|
Loading…
Reference in New Issue