Refactor bpmn diagram component (#1984)

* some initial refactoring to react diagram editor for pi show page w/ burnettk

* pi show and pm file show are now working w/ burnettk

* some minor tweaks w/ burnettk

* pi show is mostly working - need to handle future states w/ burnettk

* pass the task into the call activity overly so we can check the state w/ burnettk

* removed commented out code w/ burnettk

* fixed issue with process model new file not showing w/ burnettk

* fixed endless reload new bpmn file w/ burnettk

* do not bother zooming dmn tables w/ burnettk

* use a linke instead of a button to open the call activity so it can be opened in a new tab if desired w/ burnettk

* Revert "use a linke instead of a button to open the call activity so it can be opened in a new tab if desired w/ burnettk"

This reverts commit 1c12725cd3.

* listen for auxclick to know if call activity should be opened in new tab w/ burnettk

---------

Co-authored-by: jasquat <jasquat@users.noreply.github.com>
This commit is contained in:
jasquat 2024-07-24 17:19:52 -04:00 committed by GitHub
parent 01deb74680
commit 6f1af2132e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 466 additions and 359 deletions

View File

@ -111,29 +111,31 @@ describe('process-instances', () => {
cy.contains(originalDmnOutputForKevin).should('not.exist');
cy.runPrimaryBpmnFile();
const processModelFiles = 'process-model-files';
// Change dmn
cy.getBySel('process-model-files').click();
cy.getBySel(processModelFiles).click();
cy.getBySel(`edit-file-${dmnFile.replace('.', '-')}`).click();
updateDmnText(originalDmnOutputForKevin, newDmnOutputForKevin);
cy.contains(acceptanceTestOneDisplayName).click();
cy.runPrimaryBpmnFile();
cy.getBySel('process-model-files').click();
cy.getBySel(processModelFiles).click();
cy.getBySel(`edit-file-${dmnFile.replace('.', '-')}`).click();
updateDmnText(newDmnOutputForKevin, originalDmnOutputForKevin);
cy.contains(acceptanceTestOneDisplayName).click();
cy.runPrimaryBpmnFile();
// Change bpmn
cy.getBySel('process-model-files').click();
cy.getBySel(processModelFiles).click();
cy.getBySel(`edit-file-${bpmnFile.replace('.', '-')}`).click();
cy.contains(`Process Model File: ${bpmnFile}`);
updateBpmnPythonScript(newPythonScript);
cy.contains(acceptanceTestOneDisplayName).click();
cy.runPrimaryBpmnFile();
cy.getBySel('process-model-files').click();
cy.getBySel(processModelFiles).click();
cy.getBySel(`edit-file-${bpmnFile.replace('.', '-')}`).click();
updateBpmnPythonScript(originalPythonScript);
cy.contains(acceptanceTestOneDisplayName).click();

View File

@ -17,7 +17,7 @@ import {
// @ts-expect-error TS(7016) FIXME: Could not find a declaration file for module 'dmn-... Remove this comment to see the full error message
} from 'dmn-js-properties-panel';
import React, { useRef, useEffect, useState, useCallback } from 'react';
import React, { useEffect, useState, useCallback } from 'react';
// @ts-ignore
import { Button, ButtonSet, Modal, UnorderedList, Link } from '@carbon/react';
@ -77,6 +77,7 @@ type OwnProps = {
disableSaveButton?: boolean;
fileName?: string;
isPrimaryFile?: boolean;
onCallActivityOverlayClick?: (..._args: any[]) => any;
onDataStoresRequested?: (..._args: any[]) => any;
onDeleteFile?: (..._args: any[]) => any;
onDmnFilesRequested?: (..._args: any[]) => any;
@ -109,6 +110,7 @@ export default function ReactDiagramEditor({
disableSaveButton,
fileName,
isPrimaryFile,
onCallActivityOverlayClick,
onDataStoresRequested,
onDeleteFile,
onDmnFilesRequested,
@ -131,11 +133,9 @@ export default function ReactDiagramEditor({
url,
}: OwnProps) {
const [diagramXMLString, setDiagramXMLString] = useState('');
const [diagramModelerState, setDiagramModelerState] = useState(null);
const [diagramModelerState, setDiagramModelerState] = useState<any>(null);
const [performingXmlUpdates, setPerformingXmlUpdates] = useState(false);
const alreadyImportedXmlRef = useRef(false);
const { targetUris } = useUriListForPermissions();
const permissionRequestData: PermissionsToCheck = {};
@ -203,11 +203,8 @@ export default function ReactDiagramEditor({
});
};
// get the xml and set the modeler
useEffect(() => {
if (diagramModelerState) {
return;
}
let canvasClass = 'diagram-editor-canvas';
if (diagramType === 'readonly') {
canvasClass = 'diagram-viewer-canvas';
@ -436,7 +433,6 @@ export default function ReactDiagramEditor({
}
});
}, [
diagramModelerState,
diagramType,
onDataStoresRequested,
onDmnFilesRequested,
@ -447,13 +443,26 @@ export default function ReactDiagramEditor({
onLaunchDmnEditor,
onLaunchJsonSchemaEditor,
onLaunchMarkdownEditor,
onLaunchScriptEditor,
onLaunchMessageEditor,
onLaunchScriptEditor,
onMessagesRequested,
onSearchProcessModels,
onServiceTasksRequested,
]);
// display the diagram
useEffect(() => {
if (!diagramXMLString || !diagramModelerState) {
return;
}
diagramModelerState.importXML(diagramXMLString);
zoom(0);
if (diagramType !== 'dmn') {
fixUnresolvedReferences(diagramModelerState);
}
}, [diagramXMLString, diagramModelerState, diagramType, zoom]);
// import done operations
useEffect(() => {
// These seem to be system tasks that cannot be highlighted
const taskSpecsThatCannotBeHighlighted = ['Root', 'Start', 'End'];
@ -507,6 +516,63 @@ export default function ReactDiagramEditor({
}
}
function addOverlayOnCallActivity(
task: Task,
bpmnProcessIdentifiers: string[],
) {
if (
!onCallActivityOverlayClick ||
diagramType !== 'readonly' ||
!diagramModelerState
) {
return;
}
function domify(htmlString: string) {
const template = document.createElement('template');
template.innerHTML = htmlString.trim();
return template.content.firstChild;
}
const createCallActivityOverlay = () => {
const overlays = diagramModelerState.get('overlays');
const ARROW_DOWN_SVG =
'<svg width="20" height="20" viewBox="0 0 24.00 24.00" fill="none" xmlns="http://www.w3.org/2000/svg" stroke="#ffffff"> <g id="SVGRepo_bgCarrier" stroke-width="0"> <rect x="0" y="0" width="24.00" height="24.00" rx="0" fill="#2196f3" strokewidth="0"/> </g> <g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round" stroke="#CCCCCC" stroke-width="0.048"/> <g id="SVGRepo_iconCarrier"> <path d="M7 17L17 7M17 7H8M17 7V16" stroke="#ffffff" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"/> </g> </svg>';
const button: any = domify(
`<button class="bjs-drilldown">${ARROW_DOWN_SVG}</button>`,
);
button.addEventListener('click', (newEvent: any) => {
onCallActivityOverlayClick(task, newEvent);
});
button.addEventListener('auxclick', (newEvent: any) => {
onCallActivityOverlayClick(task, newEvent);
});
overlays.add(task.bpmn_identifier, 'drilldown', {
position: {
bottom: -10,
right: -8,
},
html: button,
});
};
try {
if (
bpmnProcessIdentifiers.includes(
task.bpmn_process_definition_identifier,
)
) {
createCallActivityOverlay();
}
} catch (bpmnIoError: any) {
// the task list also contains task for processes called from call activities which will
// not exist in this diagram so just ignore them for now.
if (
bpmnIoError.message !==
"Cannot read properties of undefined (reading 'id')"
) {
throw bpmnIoError;
}
}
}
function onImportDone(event: any) {
const { error } = event;
@ -515,12 +581,11 @@ export default function ReactDiagramEditor({
return;
}
let modeler = diagramModelerState;
if (diagramType === 'dmn') {
modeler = (diagramModelerState as any).getActiveViewer();
return;
}
const canvas = (modeler as any).get('canvas');
const canvas = diagramModelerState.get('canvas');
canvas.zoom(FitViewport, 'auto'); // Concerned this might bug out somehow.
// highlighting a field
@ -549,25 +614,16 @@ export default function ReactDiagramEditor({
bpmnProcessIdentifiers,
);
}
if (
task.typename === 'CallActivity' &&
!['FUTURE', 'LIKELY', 'MAYBE'].includes(task.state)
) {
addOverlayOnCallActivity(task, bpmnProcessIdentifiers);
}
});
}
}
function displayDiagram(
diagramModelerToUse: any,
diagramXMLToDisplay: any,
) {
if (alreadyImportedXmlRef.current) {
return;
}
diagramModelerToUse.importXML(diagramXMLToDisplay);
zoom(0);
if (diagramType !== 'dmn') {
fixUnresolvedReferences(diagramModelerToUse);
}
alreadyImportedXmlRef.current = true;
}
function dmnTextHandler(text: string) {
const decisionId = `decision_${makeid(7)}`;
const newText = text.replaceAll('{{DECISION_ID}}', decisionId);
@ -586,7 +642,7 @@ export default function ReactDiagramEditor({
) {
fetch(urlToUse)
.then((response) => response.text())
.then(textHandler ?? bpmnTextHandler)
.then(textHandler)
.catch((err) => handleError(err));
}
@ -602,17 +658,12 @@ export default function ReactDiagramEditor({
}
(diagramModelerState as any).on('import.done', onImportDone);
const diagramXMLToUse = diagramXML || diagramXMLString;
if (diagramXMLToUse) {
if (!diagramXMLString) {
setDiagramXMLString(diagramXMLToUse);
}
displayDiagram(diagramModelerState, diagramXMLToUse);
if (diagramXML) {
setDiagramXMLString(diagramXML);
return undefined;
}
if (!diagramXMLString) {
if (!diagramXML) {
if (url) {
fetchDiagramFromURL(url);
return undefined;
@ -622,7 +673,7 @@ export default function ReactDiagramEditor({
return undefined;
}
let newDiagramFileName = 'new_bpmn_diagram.bpmn';
let textHandler;
let textHandler = bpmnTextHandler;
if (diagramType === 'dmn') {
newDiagramFileName = 'new_dmn_diagram.dmn';
textHandler = dmnTextHandler;
@ -638,13 +689,12 @@ export default function ReactDiagramEditor({
diagramModelerState,
diagramType,
diagramXML,
diagramXMLString,
fileName,
tasks,
onCallActivityOverlayClick,
performingXmlUpdates,
processModelId,
tasks,
url,
zoom,
]);
function handleSave() {

View File

@ -68,7 +68,6 @@ import {
ProcessInstance,
ProcessModel,
Task,
TaskDefinitionPropertiesJson,
User,
} from '../interfaces';
import { usePermissionFetcher } from '../hooks/PermissionService';
@ -707,21 +706,24 @@ export default function ProcessInstanceShow({ variant }: OwnProps) {
return <div />;
};
const initializeTaskInstancesToDisplay = (task: Task | null) => {
if (!task) {
return;
}
HttpService.makeCallToBackend({
path: `/tasks/${params.process_instance_id}/${task.guid}/task-instances`,
httpMethod: 'GET',
// reverse operates on self as well as return the new ordered array so reverse it right away
successCallback: (results: Task[]) =>
setTaskInstancesToDisplay(results.reverse()),
failureCallback: (error: any) => {
setTaskDataToDisplay(`ERROR: ${error.message}`);
},
});
};
const initializeTaskInstancesToDisplay = useCallback(
(task: Task | null) => {
if (!task) {
return;
}
HttpService.makeCallToBackend({
path: `/tasks/${params.process_instance_id}/${task.guid}/task-instances`,
httpMethod: 'GET',
// reverse operates on self as well as return the new ordered array so reverse it right away
successCallback: (results: Task[]) =>
setTaskInstancesToDisplay(results.reverse()),
failureCallback: (error: any) => {
setTaskDataToDisplay(`ERROR: ${error.message}`);
},
});
},
[params.process_instance_id],
);
const processTaskResult = (result: Task) => {
if (result == null) {
@ -732,26 +734,29 @@ export default function ProcessInstanceShow({ variant }: OwnProps) {
setShowTaskDataLoading(false);
};
const initializeTaskDataToDisplay = (task: Task | null) => {
if (
task &&
['COMPLETED', 'ERROR', 'READY'].includes(task.state) &&
ability.can('GET', targetUris.processInstanceTaskDataPath)
) {
setShowTaskDataLoading(true);
HttpService.makeCallToBackend({
path: `${targetUris.processInstanceTaskDataPath}/${task.guid}`,
httpMethod: 'GET',
successCallback: processTaskResult,
failureCallback: (error: any) => {
setTaskDataToDisplay(`ERROR: ${error.message}`);
setShowTaskDataLoading(false);
},
});
} else {
setTaskDataToDisplay('');
}
};
const initializeTaskDataToDisplay = useCallback(
(task: Task | null) => {
if (
task &&
['COMPLETED', 'ERROR', 'READY'].includes(task.state) &&
ability.can('GET', targetUris.processInstanceTaskDataPath)
) {
setShowTaskDataLoading(true);
HttpService.makeCallToBackend({
path: `${targetUris.processInstanceTaskDataPath}/${task.guid}`,
httpMethod: 'GET',
successCallback: processTaskResult,
failureCallback: (error: any) => {
setTaskDataToDisplay(`ERROR: ${error.message}`);
setShowTaskDataLoading(false);
},
});
} else {
setTaskDataToDisplay('');
}
},
[ability, targetUris.processInstanceTaskDataPath],
);
const handleProcessDataDisplayClose = () => {
setProcessDataToDisplay(null);
@ -807,72 +812,117 @@ export default function ProcessInstanceShow({ variant }: OwnProps) {
setProcessDataToDisplay(processData);
};
const makeProcessDataCallFromShapeElement = (shapeElement: any) => {
const { dataObjectRef } = shapeElement.businessObject;
let category = 'default';
if ('extensionElements' in dataObjectRef) {
const categoryExtension = dataObjectRef.extensionElements.values.find(
(extension: any) => {
return extension.$type === 'spiffworkflow:category';
},
);
if (categoryExtension) {
category = categoryExtension.$body;
}
}
const dataObjectIdentifer = dataObjectRef.id;
const parentProcess = shapeElement.businessObject.$parent;
const parentProcessIdentifier = parentProcess.id;
let additionalParams = '';
if (tasks) {
const matchingTask: Task | undefined = tasks.find((task: Task) => {
return task.bpmn_identifier === parentProcessIdentifier;
});
if (matchingTask) {
additionalParams = `?process_identifier=${parentProcessIdentifier}&bpmn_process_guid=${matchingTask.guid}`;
} else if (
searchParams.get('process_identifier') &&
searchParams.get('bpmn_process_guid')
) {
additionalParams = `?process_identifier=${searchParams.get(
'process_identifier',
)}&bpmn_process_guid=${searchParams.get('bpmn_process_guid')}`;
}
}
HttpService.makeCallToBackend({
path: `/process-data/${category}/${params.process_model_id}/${dataObjectIdentifer}/${params.process_instance_id}${additionalParams}`,
httpMethod: 'GET',
successCallback: handleProcessDataShowResponse,
failureCallback: addError,
onUnauthorized: (result: any) =>
handleProcessDataShowReponseUnauthorized(dataObjectIdentifer, result),
});
};
const handleClickedDiagramTask = (
shapeElement: any,
bpmnProcessIdentifiers: any,
) => {
if (shapeElement.type === 'bpmn:DataObjectReference') {
makeProcessDataCallFromShapeElement(shapeElement);
} else if (tasks) {
const matchingTask: Task | undefined = tasks.find((task: Task) => {
return (
task.bpmn_identifier === shapeElement.id &&
bpmnProcessIdentifiers.includes(
task.bpmn_process_definition_identifier,
)
const makeProcessDataCallFromShapeElement = useCallback(
(shapeElement: any) => {
const { dataObjectRef } = shapeElement.businessObject;
let category = 'default';
if ('extensionElements' in dataObjectRef) {
const categoryExtension = dataObjectRef.extensionElements.values.find(
(extension: any) => {
return extension.$type === 'spiffworkflow:category';
},
);
});
if (matchingTask) {
setTaskToDisplay(matchingTask);
initializeTaskDataToDisplay(matchingTask);
initializeTaskInstancesToDisplay(matchingTask);
if (categoryExtension) {
category = categoryExtension.$body;
}
}
}
};
const dataObjectIdentifer = dataObjectRef.id;
const parentProcess = shapeElement.businessObject.$parent;
const parentProcessIdentifier = parentProcess.id;
let additionalParams = '';
if (tasks) {
const matchingTask: Task | undefined = tasks.find((task: Task) => {
return task.bpmn_identifier === parentProcessIdentifier;
});
if (matchingTask) {
additionalParams = `?process_identifier=${parentProcessIdentifier}&bpmn_process_guid=${matchingTask.guid}`;
} else if (processIdentifier && bpmnProcessGuid) {
additionalParams = `?process_identifier=${processIdentifier}&bpmn_process_guid=${bpmnProcessGuid}`;
}
}
HttpService.makeCallToBackend({
path: `/process-data/${category}/${params.process_model_id}/${dataObjectIdentifer}/${params.process_instance_id}${additionalParams}`,
httpMethod: 'GET',
successCallback: handleProcessDataShowResponse,
failureCallback: addError,
onUnauthorized: (result: any) =>
handleProcessDataShowReponseUnauthorized(dataObjectIdentifer, result),
});
},
[
addError,
params.process_instance_id,
params.process_model_id,
tasks,
bpmnProcessGuid,
processIdentifier,
],
);
const findMatchingTaskFromShapeElement = useCallback(
(shapeElement: any, bpmnProcessIdentifiers: any) => {
if (tasks) {
const matchingTask: Task | undefined = tasks.find((task: Task) => {
return (
task.bpmn_identifier === shapeElement.id &&
bpmnProcessIdentifiers.includes(
task.bpmn_process_definition_identifier,
)
);
});
return matchingTask;
}
return undefined;
},
[tasks],
);
const handleCallActivityNavigate = useCallback(
(task: Task, event: any) => {
if (
task &&
task.typename === 'CallActivity' &&
!['FUTURE', 'LIKELY', 'MAYBE'].includes(task.state)
) {
const processIdentifierToUse =
task.task_definition_properties_json.spec;
const url = `${window.location.pathname}?process_identifier=${processIdentifierToUse}&bpmn_process_guid=${task.guid}`;
if (event.type === 'auxclick') {
window.open(url);
} else {
navigate(url);
}
}
},
[navigate],
);
const handleClickedDiagramTask = useCallback(
(shapeElement: any, bpmnProcessIdentifiers: any) => {
if (shapeElement.type === 'bpmn:DataObjectReference') {
makeProcessDataCallFromShapeElement(shapeElement);
} else if (tasks) {
const matchingTask = findMatchingTaskFromShapeElement(
shapeElement,
bpmnProcessIdentifiers,
);
if (matchingTask) {
setTaskToDisplay(matchingTask);
initializeTaskDataToDisplay(matchingTask);
initializeTaskInstancesToDisplay(matchingTask);
}
}
},
[
findMatchingTaskFromShapeElement,
initializeTaskDataToDisplay,
initializeTaskInstancesToDisplay,
makeProcessDataCallFromShapeElement,
tasks,
],
);
const resetTaskActionDetails = () => {
setEditingTaskData(false);
@ -1125,23 +1175,6 @@ export default function ProcessInstanceShow({ variant }: OwnProps) {
);
}
if (
task.typename === 'CallActivity' &&
!['FUTURE', 'LIKELY', 'MAYBE'].includes(task.state)
) {
const taskDefinitionPropertiesJson: TaskDefinitionPropertiesJson =
task.task_definition_properties_json;
buttons.push(
<Link
data-qa="go-to-call-activity-result"
to={`${window.location.pathname}?process_identifier=${taskDefinitionPropertiesJson.spec}&bpmn_process_guid=${task.guid}`}
target="_blank"
>
View Call Activity Diagram
</Link>,
);
}
if (canEditTaskData(task)) {
buttons.push(
<Button
@ -1723,6 +1756,7 @@ export default function ProcessInstanceShow({ variant }: OwnProps) {
tasks={tasks}
diagramType="readonly"
onElementClick={handleClickedDiagramTask}
onCallActivityOverlayClick={handleCallActivityNavigate}
/>
<div id="diagram-container" />
</>

View File

@ -295,9 +295,9 @@ export default function ProcessModelEditDiagram() {
setDiagramHasChanges(false);
};
const onElementsChanged = () => {
const onElementsChanged = useCallback(() => {
setDiagramHasChanges(true);
};
}, []);
const onDeleteFile = (fileName = params.file_name) => {
const url = `/process-models/${modifiedProcessModelId}/files/${fileName}`;
@ -414,51 +414,60 @@ export default function ProcessModelEditDiagram() {
};
};
const onServiceTasksRequested = (event: any) => {
const onServiceTasksRequested = useCallback((event: any) => {
HttpService.makeCallToBackend({
path: `/service-tasks`,
successCallback: makeApiHandler(event),
});
};
}, []);
const onDataStoresRequested = (event: any) => {
const processGroupIdentifier =
processModel?.parent_groups?.slice(-1).pop()?.id ?? '';
HttpService.makeCallToBackend({
path: `/data-stores?upsearch=true&process_group_identifier=${processGroupIdentifier}`,
successCallback: makeDataStoresApiHandler(event),
});
};
const onJsonSchemaFilesRequested = (event: any) => {
setFileEventBus(event.eventBus);
const re = /.*[-.]schema.json/;
if (processModel) {
const jsonFiles = processModel.files.filter((f) => f.name.match(re));
const options = jsonFiles.map((f) => {
return { label: f.name, value: f.name };
const onDataStoresRequested = useCallback(
(event: any) => {
const processGroupIdentifier =
processModel?.parent_groups?.slice(-1).pop()?.id ?? '';
HttpService.makeCallToBackend({
path: `/data-stores?upsearch=true&process_group_identifier=${processGroupIdentifier}`,
successCallback: makeDataStoresApiHandler(event),
});
event.eventBus.fire('spiff.json_schema_files.returned', { options });
} else {
console.error('There is no process Model.');
}
};
},
[processModel?.parent_groups],
);
const onDmnFilesRequested = (event: any) => {
setFileEventBus(event.eventBus);
if (processModel) {
const dmnFiles = processModel.files.filter((f) => f.type === 'dmn');
const options: any[] = [];
dmnFiles.forEach((file) => {
file.references.forEach((ref) => {
options.push({ label: ref.display_name, value: ref.identifier });
const onJsonSchemaFilesRequested = useCallback(
(event: any) => {
setFileEventBus(event.eventBus);
const re = /.*[-.]schema.json/;
if (processModel) {
const jsonFiles = processModel.files.filter((f) => f.name.match(re));
const options = jsonFiles.map((f) => {
return { label: f.name, value: f.name };
});
});
event.eventBus.fire('spiff.dmn_files.returned', { options });
} else {
console.error('There is no process model.');
}
};
event.eventBus.fire('spiff.json_schema_files.returned', { options });
} else {
console.error('There is no process Model.');
}
},
[processModel],
);
const onDmnFilesRequested = useCallback(
(event: any) => {
setFileEventBus(event.eventBus);
if (processModel) {
const dmnFiles = processModel.files.filter((f) => f.type === 'dmn');
const options: any[] = [];
dmnFiles.forEach((file) => {
file.references.forEach((ref) => {
options.push({ label: ref.display_name, value: ref.identifier });
});
});
event.eventBus.fire('spiff.dmn_files.returned', { options });
} else {
console.error('There is no process model.');
}
},
[processModel],
);
const makeMessagesRequestedHandler = (event: any) => {
return function fireEvent(results: any) {
@ -467,20 +476,23 @@ export default function ProcessModelEditDiagram() {
});
};
};
const onMessagesRequested = (event: any) => {
// it is perfectly reasonable to access the edit diagram page in read only mode when you actually don't have access to edit.
// this is awkward in terms of functionality like this, where we are fetching the relevant list of messages to show in the
// properties panel. since message_model_list is a different permission, you may not have access to it even though you have
// access to the read the process model. we also considered automatically giving you access to read message_model_list
// when you have read access to the process model, but this seemed easier and more in line with the current backend permission system,
// where we normally only pork barrel permissions on top of "start" and "all."
if (ability.can('GET', targetUris.messageModelListPath)) {
HttpService.makeCallToBackend({
path: targetUris.messageModelListPath,
successCallback: makeMessagesRequestedHandler(event),
});
}
};
const onMessagesRequested = useCallback(
(event: any) => {
// it is perfectly reasonable to access the edit diagram page in read only mode when you actually don't have access to edit.
// this is awkward in terms of functionality like this, where we are fetching the relevant list of messages to show in the
// properties panel. since message_model_list is a different permission, you may not have access to it even though you have
// access to the read the process model. we also considered automatically giving you access to read message_model_list
// when you have read access to the process model, but this seemed easier and more in line with the current backend permission system,
// where we normally only pork barrel permissions on top of "start" and "all."
if (ability.can('GET', targetUris.messageModelListPath)) {
HttpService.makeCallToBackend({
path: targetUris.messageModelListPath,
successCallback: makeMessagesRequestedHandler(event),
});
}
},
[ability, targetUris.messageModelListPath],
);
useEffect(() => {
const updateDiagramFiles = (pm: ProcessModel) => {
@ -518,35 +530,38 @@ export default function ProcessModelEditDiagram() {
return [];
};
const setScriptUnitTestElementWithIndex = (
scriptIndex: number,
element: any = scriptElement,
) => {
const unitTestsModdleElements = getScriptUnitTestElements(element);
if (unitTestsModdleElements.length > 0) {
setCurrentScriptUnitTest(unitTestsModdleElements[scriptIndex]);
setCurrentScriptUnitTestIndex(scriptIndex);
}
};
const setScriptUnitTestElementWithIndex = useCallback(
(scriptIndex: number, element: any = scriptElement) => {
const unitTestsModdleElements = getScriptUnitTestElements(element);
if (unitTestsModdleElements.length > 0) {
setCurrentScriptUnitTest(unitTestsModdleElements[scriptIndex]);
setCurrentScriptUnitTestIndex(scriptIndex);
}
},
[scriptElement],
);
const onLaunchScriptEditor = (
element: any,
script: string,
scriptTypeString: string,
eventBus: any,
modeling: any,
) => {
// TODO: modeling is only needed for script unit tests.
// we should update this to act like updating scripts
// where we pass an event to bpmn-js
setScriptModeling(modeling);
setScriptText(script || '');
setScriptType(scriptTypeString);
setScriptEventBus(eventBus);
setScriptElement(element);
setScriptUnitTestElementWithIndex(0, element);
handleShowScriptEditor();
};
const onLaunchScriptEditor = useCallback(
(
element: any,
script: string,
scriptTypeString: string,
eventBus: any,
modeling: any,
) => {
// TODO: modeling is only needed for script unit tests.
// we should update this to act like updating scripts
// where we pass an event to bpmn-js
setScriptModeling(modeling);
setScriptText(script || '');
setScriptType(scriptTypeString);
setScriptEventBus(eventBus);
setScriptElement(element);
setScriptUnitTestElementWithIndex(0, element);
handleShowScriptEditor();
},
[setScriptUnitTestElementWithIndex],
);
const handleScriptEditorClose = () => {
scriptEventBus.fire('spiff.script.update', {
@ -1045,15 +1060,16 @@ export default function ProcessModelEditDiagram() {
</Modal>
);
};
const onLaunchMarkdownEditor = (
_element: any,
markdown: string,
eventBus: any,
) => {
setMarkdownText(markdown || '');
setMarkdownEventBus(eventBus);
handleShowMarkdownEditor();
};
const onLaunchMarkdownEditor = useCallback(
(_element: any, markdown: string, eventBus: any) => {
setMarkdownText(markdown || '');
setMarkdownEventBus(eventBus);
handleShowMarkdownEditor();
},
[],
);
const handleMarkdownEditorClose = () => {
markdownEventBus.fire('spiff.markdown.update', {
value: markdownText,
@ -1083,13 +1099,14 @@ export default function ProcessModelEditDiagram() {
);
};
const onLaunchMessageEditor = (event: any) => {
const onLaunchMessageEditor = useCallback((event: any) => {
setMessageEvent(event);
setMessageId(event.value.messageId);
setElementId(event.value.elementId);
setCorrelationProperties(event.value.correlation_properties);
handleShowMessageEditor();
};
}, []);
const handleMessageEditorClose = () => {
setShowMessageEditor(false);
onMessagesRequested(messageEvent);
@ -1132,15 +1149,14 @@ export default function ProcessModelEditDiagram() {
);
};
const onSearchProcessModels = (
_processId: string,
eventBus: any,
element: any,
) => {
setProcessSearchEventBus(eventBus);
setProcessSearchElement(element);
setShowProcessSearch(true);
};
const onSearchProcessModels = useCallback(
(_processId: string, eventBus: any, element: any) => {
setProcessSearchEventBus(eventBus);
setProcessSearchElement(element);
setShowProcessSearch(true);
},
[],
);
const processSearchOnClose = (selection: CarbonComboBoxProcessSelection) => {
const selectedProcessModel = selection.selectedItem;
if (selectedProcessModel) {
@ -1172,79 +1188,81 @@ export default function ProcessModelEditDiagram() {
);
};
const findFileNameForReferenceId = (
id: string,
type: string,
): ProcessFile | null => {
// Given a reference id (like a process_id, or decision_id) finds the file
// that contains that reference and returns it.
let matchFile = null;
if (processModel) {
const files = processModel.files.filter((f) => f.type === type);
files.some((file) => {
if (file.references.some((ref) => ref.identifier === id)) {
matchFile = file;
return true;
const findFileNameForReferenceId = useCallback(
(id: string, type: string): ProcessFile | null => {
// Given a reference id (like a process_id, or decision_id) finds the file
// that contains that reference and returns it.
let matchFile = null;
if (processModel) {
const files = processModel.files.filter((f) => f.type === type);
files.some((file) => {
if (file.references.some((ref) => ref.identifier === id)) {
matchFile = file;
return true;
}
return false;
});
}
return matchFile;
},
[processModel],
);
const onLaunchBpmnEditor = useCallback(
(processId: string) => {
const openProcessModelFileInNewTab = (
processReference: ProcessReference,
) => {
const path = generatePath(
'/editor/process-models/:process_model_path/files/:file_name',
{
process_model_path: modifyProcessIdentifierForPathParam(
processReference.relative_location,
),
file_name: processReference.file_name,
},
);
window.open(path);
};
const openFileNameForProcessId = (
processesReferences: ProcessReference[],
) => {
const processRef = processesReferences.find((p) => {
return p.identifier === processId;
});
if (processRef) {
openProcessModelFileInNewTab(processRef);
}
return false;
};
// using the "setState" method with a function gives us access to the
// most current state of processes. Otherwise it uses the stale state
// when passing the callback to a non-React component like bpmn-js:
// https://stackoverflow.com/a/60643670/6090676
setProcesses((upToDateProcesses: ProcessReference[]) => {
const processRef = upToDateProcesses.find((p) => {
return p.identifier === processId;
});
if (!processRef) {
getProcessesCallback(openFileNameForProcessId);
} else {
openProcessModelFileInNewTab(processRef);
}
return upToDateProcesses;
});
}
return matchFile;
};
},
[getProcessesCallback],
);
const onLaunchBpmnEditor = (processId: string) => {
const openProcessModelFileInNewTab = (
processReference: ProcessReference,
) => {
const path = generatePath(
'/editor/process-models/:process_model_path/files/:file_name',
{
process_model_path: modifyProcessIdentifierForPathParam(
processReference.relative_location,
),
file_name: processReference.file_name,
},
);
window.open(path);
};
const openFileNameForProcessId = (
processesReferences: ProcessReference[],
) => {
const processRef = processesReferences.find((p) => {
return p.identifier === processId;
});
if (processRef) {
openProcessModelFileInNewTab(processRef);
}
};
// using the "setState" method with a function gives us access to the
// most current state of processes. Otherwise it uses the stale state
// when passing the callback to a non-React component like bpmn-js:
// https://stackoverflow.com/a/60643670/6090676
setProcesses((upToDateProcesses: ProcessReference[]) => {
const processRef = upToDateProcesses.find((p) => {
return p.identifier === processId;
});
if (!processRef) {
getProcessesCallback(openFileNameForProcessId);
} else {
openProcessModelFileInNewTab(processRef);
}
return upToDateProcesses;
});
};
const onLaunchJsonSchemaEditor = (
_element: any,
fileName: string,
eventBus: any,
) => {
setFileEventBus(eventBus);
setJsonScehmaFileName(fileName);
setShowJsonSchemaEditor(true);
};
const onLaunchJsonSchemaEditor = useCallback(
(_element: any, fileName: string, eventBus: any) => {
setFileEventBus(eventBus);
setJsonScehmaFileName(fileName);
setShowJsonSchemaEditor(true);
},
[],
);
const handleJsonScehmaEditorClose = () => {
fileEventBus.fire('spiff.jsonSchema.update', {
@ -1283,28 +1301,31 @@ export default function ProcessModelEditDiagram() {
);
};
const onLaunchDmnEditor = (processId: string) => {
const file = findFileNameForReferenceId(processId, 'dmn');
let path = '';
if (file) {
path = generatePath(
'/editor/process-models/:process_model_id/files/:file_name',
{
process_model_id: params.process_model_id || null,
file_name: file.name,
},
);
const onLaunchDmnEditor = useCallback(
(processId: string) => {
const file = findFileNameForReferenceId(processId, 'dmn');
let path = '';
if (file) {
path = generatePath(
'/editor/process-models/:process_model_id/files/:file_name',
{
process_model_id: params.process_model_id || null,
file_name: file.name,
},
);
window.open(path);
} else {
path = generatePath(
'/editor/process-models/:process_model_id/files?file_type=dmn',
{
process_model_id: params.process_model_id || null,
},
);
}
window.open(path);
} else {
path = generatePath(
'/editor/process-models/:process_model_id/files?file_type=dmn',
{
process_model_id: params.process_model_id || null,
},
);
}
window.open(path);
};
},
[findFileNameForReferenceId, params.process_model_id],
);
const isDmn = () => {
const fileName = params.file_name || '';
@ -1315,12 +1336,12 @@ export default function ProcessModelEditDiagram() {
if (isDmn()) {
return (
<ReactDiagramEditor
processModelId={params.process_model_id || ''}
saveDiagram={saveDiagram}
onDeleteFile={onDeleteFile}
diagramType="dmn"
diagramXML={bpmnXmlForDiagramRendering}
fileName={params.file_name}
diagramType="dmn"
onDeleteFile={onDeleteFile}
processModelId={params.process_model_id || ''}
saveDiagram={saveDiagram}
/>
);
}
@ -1352,8 +1373,8 @@ export default function ProcessModelEditDiagram() {
onLaunchDmnEditor={onLaunchDmnEditor}
onLaunchJsonSchemaEditor={onLaunchJsonSchemaEditor}
onLaunchMarkdownEditor={onLaunchMarkdownEditor}
onLaunchScriptEditor={onLaunchScriptEditor}
onLaunchMessageEditor={onLaunchMessageEditor}
onLaunchScriptEditor={onLaunchScriptEditor}
onMessagesRequested={onMessagesRequested}
onSearchProcessModels={onSearchProcessModels}
onServiceTasksRequested={onServiceTasksRequested}