adds basic support to use certain components in extensions w/ burnettk (#597)

Co-authored-by: jasquat <jasquat@users.noreply.github.com>
This commit is contained in:
jasquat 2023-10-27 14:54:45 -04:00 committed by GitHub
parent c2ce27e961
commit 566f383918
13 changed files with 103 additions and 135 deletions

View File

@ -58,7 +58,7 @@ class ElementUnitsService:
# make mypy happy # make mypy happy
return None return None
current_app.logger.info(f"Checking element unit cache @ {cache_key} :: '{process_id}' - '{element_id}'") current_app.logger.debug(f"Checking element unit cache @ {cache_key} :: '{process_id}' - '{element_id}'")
bpmn_spec_json = spiff_element_units.workflow_from_cached_element_unit( bpmn_spec_json = spiff_element_units.workflow_from_cached_element_unit(
cache_dir, cache_key, process_id, element_id cache_dir, cache_key, process_id, element_id

View File

@ -1,8 +1,8 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
// @ts-ignore // @ts-ignore
import MDEditor from '@uiw/react-md-editor';
import { Toggle } from '@carbon/react'; import { Toggle } from '@carbon/react';
import FormattingService from '../services/FormattingService'; import FormattingService from '../services/FormattingService';
import MarkdownRenderer from './MarkdownRenderer';
type OwnProps = { type OwnProps = {
task: any; task: any;
@ -87,9 +87,8 @@ export default function InstructionsForEndUser({
https://www.npmjs.com/package/@uiw/react-md-editor switches to dark mode by default by respecting @media (prefers-color-scheme: dark) https://www.npmjs.com/package/@uiw/react-md-editor switches to dark mode by default by respecting @media (prefers-color-scheme: dark)
This makes it look like our site is broken, so until the rest of the site supports dark mode, turn off dark mode for this component. This makes it look like our site is broken, so until the rest of the site supports dark mode, turn off dark mode for this component.
*/} */}
<div data-color-mode="light">
<MDEditor.Markdown linkTarget="_blank" source={instructions} /> <MarkdownRenderer linkTarget="_blank" source={instructions} />
</div>
</div> </div>
{showCollapseToggle()} {showCollapseToggle()}
</div> </div>

View File

@ -1,6 +1,6 @@
import MDEditor from '@uiw/react-md-editor';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import HttpService from '../services/HttpService'; import HttpService from '../services/HttpService';
import MarkdownRenderer from './MarkdownRenderer';
type OwnProps = { type OwnProps = {
apiPath: string; apiPath: string;
@ -24,9 +24,11 @@ export default function MarkdownDisplayForFile({ apiPath }: OwnProps) {
if (markdownContents) { if (markdownContents) {
return ( return (
<div data-color-mode="light" className="with-bottom-margin"> <MarkdownRenderer
<MDEditor.Markdown linkTarget="_blank" source={markdownContents} /> linkTarget="_blank"
</div> source={markdownContents}
wrapperClassName="with-bottom-margin"
/>
); );
} }

View File

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

View File

@ -66,20 +66,21 @@ const storeRecentProcessModelInLocalStorage = (
type OwnProps = { type OwnProps = {
processModel: ProcessModel; processModel: ProcessModel;
onSuccessCallback: Function;
className?: string; className?: string;
checkPermissions?: boolean; checkPermissions?: boolean;
buttonText?: string;
}; };
export default function ProcessInstanceRun({ export default function ProcessInstanceRun({
processModel, processModel,
onSuccessCallback,
className, className,
checkPermissions = true, checkPermissions = true,
buttonText = 'Start',
}: OwnProps) { }: OwnProps) {
const navigate = useNavigate(); const navigate = useNavigate();
const { addError, removeError } = useAPIError(); const { addError, removeError } = useAPIError();
const [disableStartButton, setDisableStartButton] = useState<boolean>(false); const [disableStartButton, setDisableStartButton] = useState<boolean>(false);
const modifiedProcessModelId = modifyProcessIdentifierForPathParam( const modifiedProcessModelId = modifyProcessIdentifierForPathParam(
processModel.id processModel.id
); );
@ -98,16 +99,15 @@ export default function ProcessInstanceRun({
const onProcessInstanceRun = (processInstance: any) => { const onProcessInstanceRun = (processInstance: any) => {
const processInstanceId = (processInstance as any).id; const processInstanceId = (processInstance as any).id;
navigate( navigate(
`/process-instances/for-me/${modifyProcessIdentifierForPathParam( `/process-instances/for-me/${modifiedProcessModelId}/${processInstanceId}/interstitial`
processModel.id
)}/${processInstanceId}/interstitial`
); );
onSuccessCallback(processInstance);
}; };
const processModelRun = (processInstance: any) => { const processModelRun = (processInstance: any) => {
removeError(); removeError();
storeRecentProcessModelInLocalStorage(processModel); if (processModel) {
storeRecentProcessModelInLocalStorage(processModel);
}
HttpService.makeCallToBackend({ HttpService.makeCallToBackend({
path: `/process-instances/${modifiedProcessModelId}/${processInstance.id}/run`, path: `/process-instances/${modifiedProcessModelId}/${processInstance.id}/run`,
successCallback: onProcessInstanceRun, successCallback: onProcessInstanceRun,
@ -141,7 +141,7 @@ export default function ProcessInstanceRun({
disabled={disableStartButton} disabled={disableStartButton}
size="md" size="md"
> >
Start {buttonText}
</Button> </Button>
); );

View File

@ -1,17 +1,16 @@
import { ReactElement, useEffect, useState } from 'react'; import { ReactElement, useEffect, useState } from 'react';
import { Link, useSearchParams } from 'react-router-dom'; import { useSearchParams } from 'react-router-dom';
import { import {
Tile, Tile,
// @ts-ignore // @ts-ignore
} from '@carbon/react'; } from '@carbon/react';
import HttpService from '../services/HttpService'; import HttpService from '../services/HttpService';
import { ProcessModel, ProcessInstance, ProcessGroup } from '../interfaces'; import { ProcessModel, ProcessGroup } from '../interfaces';
import { import {
modifyProcessIdentifierForPathParam, modifyProcessIdentifierForPathParam,
truncateString, truncateString,
} from '../helpers'; } from '../helpers';
import ProcessInstanceRun from './ProcessInstanceRun'; import ProcessInstanceRun from './ProcessInstanceRun';
import { Notification } from './Notification';
type OwnProps = { type OwnProps = {
headerElement?: ReactElement; headerElement?: ReactElement;
@ -28,8 +27,6 @@ export default function ProcessModelListTiles({
const [processModels, setProcessModels] = useState<ProcessModel[] | null>( const [processModels, setProcessModels] = useState<ProcessModel[] | null>(
null null
); );
const [processInstance, setProcessInstance] =
useState<ProcessInstance | null>(null);
useEffect(() => { useEffect(() => {
const setProcessModelsFromResult = (result: any) => { const setProcessModelsFromResult = (result: any) => {
@ -48,27 +45,6 @@ export default function ProcessModelListTiles({
}); });
}, [searchParams, processGroup]); }, [searchParams, processGroup]);
const processInstanceRunResultTag = () => {
if (processInstance) {
return (
<Notification
title={`Process Instance ${processInstance.id} kicked off`}
onClose={() => setProcessInstance(null)}
>
<Link
to={`/process-instances/${modifyProcessIdentifierForPathParam(
processInstance.process_model_identifier
)}/${processInstance.id}`}
data-qa="process-instance-show-link"
>
view
</Link>
</Notification>
);
}
return null;
};
const processModelsDisplayArea = () => { const processModelsDisplayArea = () => {
let displayText = null; let displayText = null;
if (processModels && processModels.length > 0) { if (processModels && processModels.length > 0) {
@ -95,7 +71,6 @@ export default function ProcessModelListTiles({
</p> </p>
<ProcessInstanceRun <ProcessInstanceRun
processModel={row} processModel={row}
onSuccessCallback={setProcessInstance}
className="tile-pin-bottom" className="tile-pin-bottom"
checkPermissions={checkPermissions} checkPermissions={checkPermissions}
/> />
@ -114,7 +89,6 @@ export default function ProcessModelListTiles({
return ( return (
<> <>
{headerElement} {headerElement}
{processInstanceRunResultTag()}
{processModelsDisplayArea()} {processModelsDisplayArea()}
</> </>
); );

View File

@ -37,10 +37,16 @@ export interface UiSchemaAction {
full_api_path?: boolean; full_api_path?: boolean;
} }
export interface UiSchemaPageComponent {
name: string;
arguments: object;
}
export interface UiSchemaPageDefinition { export interface UiSchemaPageDefinition {
header: string; header: string;
api: string; api: string;
components?: UiSchemaPageComponent[];
form?: UiSchemaForm; form?: UiSchemaForm;
markdown_instruction_filename?: string; markdown_instruction_filename?: string;
navigate_instead_of_post_to_api?: boolean; navigate_instead_of_post_to_api?: boolean;

View File

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

View File

@ -1,10 +1,13 @@
import { useCallback, useEffect, useState } from 'react'; import { useCallback, useEffect, useState } from 'react';
import { Button } from '@carbon/react'; import { Button } from '@carbon/react';
import MDEditor from '@uiw/react-md-editor';
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';
import { ProcessFile, ProcessModel } from '../interfaces'; import {
ObjectWithStringKeysAndFunctionValues,
ProcessFile,
ProcessModel,
} 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 } from '../helpers';
@ -13,10 +16,13 @@ import { BACKEND_BASE_URL } from '../config';
import { import {
ExtensionPostBody, ExtensionPostBody,
ExtensionUiSchema, ExtensionUiSchema,
UiSchemaPageComponent,
UiSchemaPageDefinition, UiSchemaPageDefinition,
} from '../extension_ui_schema_interfaces'; } from '../extension_ui_schema_interfaces';
import ErrorDisplay from '../components/ErrorDisplay'; import ErrorDisplay from '../components/ErrorDisplay';
import FormattingService from '../services/FormattingService'; import FormattingService from '../services/FormattingService';
import ProcessInstanceRun from '../components/ProcessInstanceRun';
import MarkdownRenderer from '../components/MarkdownRenderer';
type OwnProps = { type OwnProps = {
displayErrors?: boolean; displayErrors?: boolean;
@ -43,9 +49,16 @@ export default function Extension({ displayErrors = true }: OwnProps) {
}>({}); }>({});
const [uiSchemaPageDefinition, setUiSchemaPageDefinition] = const [uiSchemaPageDefinition, setUiSchemaPageDefinition] =
useState<UiSchemaPageDefinition | null>(null); useState<UiSchemaPageDefinition | null>(null);
const [readyForComponentsToDisplay, setReadyForComponentsToDisplay] =
useState<boolean>(false);
const { addError, removeError } = useAPIError(); const { addError, removeError } = useAPIError();
const supportedComponents: ObjectWithStringKeysAndFunctionValues = {
ProcessInstanceRun,
MarkdownRenderer,
};
const setConfigsIfDesiredSchemaFile = useCallback( const setConfigsIfDesiredSchemaFile = useCallback(
// eslint-disable-next-line sonarjs/cognitive-complexity // eslint-disable-next-line sonarjs/cognitive-complexity
(extensionUiSchemaFile: ProcessFile | null, pm: ProcessModel) => { (extensionUiSchemaFile: ProcessFile | null, pm: ProcessModel) => {
@ -57,6 +70,7 @@ export default function Extension({ displayErrors = true }: OwnProps) {
); );
setMarkdownToRenderOnLoad(newMarkdown); setMarkdownToRenderOnLoad(newMarkdown);
} }
setReadyForComponentsToDisplay(true);
}; };
if ( if (
@ -98,6 +112,8 @@ export default function Extension({ displayErrors = true }: OwnProps) {
httpMethod: 'POST', httpMethod: 'POST',
postBody, postBody,
}); });
} else {
setReadyForComponentsToDisplay(true);
} }
} }
} }
@ -238,7 +254,7 @@ export default function Extension({ displayErrors = true }: OwnProps) {
} }
}; };
if (uiSchemaPageDefinition) { if (readyForComponentsToDisplay && uiSchemaPageDefinition) {
const componentsToDisplay = [<h1>{uiSchemaPageDefinition.header}</h1>]; const componentsToDisplay = [<h1>{uiSchemaPageDefinition.header}</h1>];
const markdownContentsToRender = []; const markdownContentsToRender = [];
@ -261,12 +277,28 @@ export default function Extension({ displayErrors = true }: OwnProps) {
if (markdownContentsToRender.length > 0) { if (markdownContentsToRender.length > 0) {
componentsToDisplay.push( componentsToDisplay.push(
<div data-color-mode="light" className="with-bottom-margin"> <MarkdownRenderer
<MDEditor.Markdown linkTarget={mdEditorLinkTarget}
linkTarget={mdEditorLinkTarget} source={markdownContentsToRender.join('\n')}
source={markdownContentsToRender.join('\n')} wrapperClassName="with-bottom-margin"
/> />
</div> );
}
if (uiSchemaPageDefinition.components) {
uiSchemaPageDefinition.components.forEach(
(component: UiSchemaPageComponent) => {
if (supportedComponents[component.name]) {
const argumentsForComponent: any = component.arguments;
componentsToDisplay.push(
supportedComponents[component.name](argumentsForComponent)
);
} else {
console.error(
`Extension tried to use component with name '${component.name}' but that is not allowed.`
);
}
}
); );
} }
@ -304,13 +336,12 @@ export default function Extension({ displayErrors = true }: OwnProps) {
if (processedTaskData) { if (processedTaskData) {
if (markdownToRenderOnSubmit) { if (markdownToRenderOnSubmit) {
componentsToDisplay.push( componentsToDisplay.push(
<div data-color-mode="light" className="with-top-margin"> <MarkdownRenderer
<MDEditor.Markdown className="onboarding"
className="onboarding" linkTarget="_blank"
linkTarget="_blank" source={markdownToRenderOnSubmit}
source={markdownToRenderOnSubmit} wrapperClassName="with-top-margin"
/> />
</div>
); );
} else { } else {
componentsToDisplay.push( componentsToDisplay.push(

View File

@ -2,7 +2,6 @@ import { useEffect, useState } from 'react';
// @ts-ignore // @ts-ignore
import { Button, Table } from '@carbon/react'; import { Button, Table } from '@carbon/react';
import { Link, useSearchParams } from 'react-router-dom'; import { Link, useSearchParams } from 'react-router-dom';
import { Notification } from '../components/Notification';
import PaginationForTable from '../components/PaginationForTable'; import PaginationForTable from '../components/PaginationForTable';
import { import {
getPageInfoFromSearchParams, getPageInfoFromSearchParams,
@ -12,7 +11,6 @@ import {
import HttpService from '../services/HttpService'; import HttpService from '../services/HttpService';
import { import {
PaginationObject, PaginationObject,
ProcessInstance,
ProcessModel, ProcessModel,
RecentProcessModel, RecentProcessModel,
} from '../interfaces'; } from '../interfaces';
@ -25,8 +23,6 @@ export default function MyTasks() {
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();
const [tasks, setTasks] = useState([]); const [tasks, setTasks] = useState([]);
const [pagination, setPagination] = useState<PaginationObject | null>(null); const [pagination, setPagination] = useState<PaginationObject | null>(null);
const [processInstance, setProcessInstance] =
useState<ProcessInstance | null>(null);
useEffect(() => { useEffect(() => {
const getTasks = () => { const getTasks = () => {
@ -52,27 +48,6 @@ export default function MyTasks() {
); );
}, [searchParams]); }, [searchParams]);
const processInstanceRunResultTag = () => {
if (processInstance) {
return (
<Notification
title={`Process Instance ${processInstance.id} kicked off`}
onClose={() => setProcessInstance(null)}
>
<Link
to={`/process-instances/${modifyProcessIdentifierForPathParam(
processInstance.process_model_identifier
)}/${processInstance.id}`}
data-qa="process-instance-show-link"
>
view
</Link>
</Notification>
);
}
return null;
};
let recentProcessModels: RecentProcessModel[] = []; let recentProcessModels: RecentProcessModel[] = [];
const recentProcessModelsString = localStorage.getItem('recentProcessModels'); const recentProcessModelsString = localStorage.getItem('recentProcessModels');
if (recentProcessModelsString !== null) { if (recentProcessModelsString !== null) {
@ -163,10 +138,7 @@ export default function MyTasks() {
</Link> </Link>
</td> </td>
<td className="actions-cell"> <td className="actions-cell">
<ProcessInstanceRun <ProcessInstanceRun processModel={processModel} />
processModel={processModel}
onSuccessCallback={setProcessInstance}
/>
</td> </td>
</tr> </tr>
); );
@ -220,7 +192,6 @@ export default function MyTasks() {
} }
return ( return (
<> <>
{processInstanceRunResultTag()}
{tasksWaitingForMe} {tasksWaitingForMe}
<br /> <br />
{relevantProcessModelSection} {relevantProcessModelSection}

View File

@ -1,9 +1,9 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import MDEditor from '@uiw/react-md-editor';
import { useLocation, useNavigate } from 'react-router-dom'; import { useLocation, useNavigate } from 'react-router-dom';
import HttpService from '../services/HttpService'; import HttpService from '../services/HttpService';
import { Onboarding } from '../interfaces'; import { Onboarding } from '../interfaces';
import { objectIsEmpty } from '../helpers'; import { objectIsEmpty } from '../helpers';
import MarkdownRenderer from '../components/MarkdownRenderer';
export default function OnboardingView() { export default function OnboardingView() {
const [onboarding, setOnboarding] = useState<Onboarding | null>(null); const [onboarding, setOnboarding] = useState<Onboarding | null>(null);
@ -40,13 +40,11 @@ export default function OnboardingView() {
onboarding.instructions.length > 0 onboarding.instructions.length > 0
) { ) {
return ( return (
<div data-color-mode="light"> <MarkdownRenderer
<MDEditor.Markdown className="onboarding"
className="onboarding" linkTarget="_blank"
linkTarget="_blank" source={onboarding.instructions}
source={onboarding.instructions} />
/>
</div>
); );
} }
return null; return null;

View File

@ -38,12 +38,7 @@ import {
modifyProcessIdentifierForPathParam, modifyProcessIdentifierForPathParam,
setPageTitle, setPageTitle,
} from '../helpers'; } from '../helpers';
import { import { PermissionsToCheck, ProcessFile, ProcessModel } from '../interfaces';
PermissionsToCheck,
ProcessFile,
ProcessInstance,
ProcessModel,
} from '../interfaces';
import ButtonWithConfirmation from '../components/ButtonWithConfirmation'; import ButtonWithConfirmation from '../components/ButtonWithConfirmation';
import ProcessInstanceListTable from '../components/ProcessInstanceListTable'; import ProcessInstanceListTable from '../components/ProcessInstanceListTable';
import { usePermissionFetcher } from '../hooks/PermissionService'; import { usePermissionFetcher } from '../hooks/PermissionService';
@ -59,8 +54,6 @@ export default function ProcessModelShow() {
const navigate = useNavigate(); const navigate = useNavigate();
const [processModel, setProcessModel] = useState<ProcessModel | null>(null); const [processModel, setProcessModel] = useState<ProcessModel | null>(null);
const [processInstance, setProcessInstance] =
useState<ProcessInstance | null>(null);
const [reloadModel, setReloadModel] = useState<boolean>(false); const [reloadModel, setReloadModel] = useState<boolean>(false);
const [filesToUpload, setFilesToUpload] = useState<any>(null); const [filesToUpload, setFilesToUpload] = useState<any>(null);
const [showFileUploadModal, setShowFileUploadModal] = const [showFileUploadModal, setShowFileUploadModal] =
@ -120,25 +113,6 @@ export default function ProcessModelShow() {
}); });
}, [reloadModel, modifiedProcessModelId]); }, [reloadModel, modifiedProcessModelId]);
const processInstanceRunResultTag = () => {
if (processInstance) {
return (
<Notification
title="Process Instance Kicked Off:"
onClose={() => setProcessInstance(null)}
>
<Link
to={`/process-instances/for-me/${modifiedProcessModelId}/${processInstance.id}`}
data-qa="process-instance-show-link"
>
view
</Link>
</Notification>
);
}
return null;
};
const onUploadedCallback = () => { const onUploadedCallback = () => {
setReloadModel(true); setReloadModel(true);
}; };
@ -729,10 +703,7 @@ export default function ProcessModelShow() {
ability={ability} ability={ability}
> >
<> <>
<ProcessInstanceRun <ProcessInstanceRun processModel={processModel} />
processModel={processModel}
onSuccessCallback={setProcessInstance}
/>
<br /> <br />
<br /> <br />
</> </>
@ -753,7 +724,6 @@ export default function ProcessModelShow() {
]} ]}
/> />
{processModelPublishMessage()} {processModelPublishMessage()}
{processInstanceRunResultTag()}
<Stack orientation="horizontal" gap={1}> <Stack orientation="horizontal" gap={1}>
<h1 className="with-icons"> <h1 className="with-icons">
Process Model: {processModel.display_name} Process Model: {processModel.display_name}

View File

@ -4,7 +4,6 @@ import { useNavigate, useParams } from 'react-router-dom';
import { Grid, Column, Button, ButtonSet, Loading } from '@carbon/react'; import { Grid, Column, Button, ButtonSet, Loading } from '@carbon/react';
import { useDebouncedCallback } from 'use-debounce'; import { useDebouncedCallback } from 'use-debounce';
import MDEditor from '@uiw/react-md-editor';
import HttpService from '../services/HttpService'; import HttpService from '../services/HttpService';
import useAPIError from '../hooks/UseApiError'; import useAPIError from '../hooks/UseApiError';
import { import {
@ -23,6 +22,7 @@ import CustomForm from '../components/CustomForm';
import ProcessBreadcrumb from '../components/ProcessBreadcrumb'; import ProcessBreadcrumb from '../components/ProcessBreadcrumb';
import InstructionsForEndUser from '../components/InstructionsForEndUser'; import InstructionsForEndUser from '../components/InstructionsForEndUser';
import UserService from '../services/UserService'; import UserService from '../services/UserService';
import MarkdownRenderer from '../components/MarkdownRenderer';
export default function TaskShow() { export default function TaskShow() {
// get a basic task which doesn't get the form data so we can load // get a basic task which doesn't get the form data so we can load
@ -397,9 +397,7 @@ export default function TaskShow() {
if (guestConfirmationText) { if (guestConfirmationText) {
pageElements.push( pageElements.push(
<div data-color-mode="light"> <MarkdownRenderer linkTarget="_blank" source={guestConfirmationText} />
<MDEditor.Markdown linkTarget="_blank" source={guestConfirmationText} />
</div>
); );
} else if (basicTask && taskData) { } else if (basicTask && taskData) {
pageElements.push(<InstructionsForEndUser task={taskWithTaskData} />); pageElements.push(<InstructionsForEndUser task={taskWithTaskData} />);