diff --git a/spiffworkflow-backend/poetry.lock b/spiffworkflow-backend/poetry.lock index b2db7e5f..303fcc2f 100644 --- a/spiffworkflow-backend/poetry.lock +++ b/spiffworkflow-backend/poetry.lock @@ -1916,7 +1916,7 @@ lxml = "*" type = "git" url = "https://github.com/sartography/SpiffWorkflow" reference = "main" -resolved_reference = "98a1b37e01a00faea60025f517a89867b7261432" +resolved_reference = "162a1c5f56cf12fc589a1e368704c0819bfcc0cd" [[package]] name = "sqlalchemy" diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_instances_controller.py b/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_instances_controller.py index c6a8ddcd..37fa778b 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_instances_controller.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_instances_controller.py @@ -18,7 +18,9 @@ from sqlalchemy.orm import aliased from spiffworkflow_backend.exceptions.api_error import ApiError from spiffworkflow_backend.models.bpmn_process import BpmnProcessModel -from spiffworkflow_backend.models.bpmn_process_definition import BpmnProcessDefinitionModel +from spiffworkflow_backend.models.bpmn_process_definition import ( + BpmnProcessDefinitionModel, +) from spiffworkflow_backend.models.db import db from spiffworkflow_backend.models.human_task import HumanTaskModel from spiffworkflow_backend.models.human_task_user import HumanTaskUserModel @@ -28,7 +30,9 @@ from spiffworkflow_backend.models.process_instance import ( ) from spiffworkflow_backend.models.process_instance import ProcessInstanceModel from spiffworkflow_backend.models.process_instance import ProcessInstanceModelSchema -from spiffworkflow_backend.models.process_instance_event import ProcessInstanceEventModel +from spiffworkflow_backend.models.process_instance_event import ( + ProcessInstanceEventModel, +) from spiffworkflow_backend.models.process_instance_metadata import ( ProcessInstanceMetadataModel, ) @@ -168,7 +172,10 @@ def process_instance_terminate( try: with ProcessInstanceQueueService.dequeued(process_instance): processor.terminate() - except (ProcessInstanceIsNotEnqueuedError, ProcessInstanceIsAlreadyLockedError) as e: + except ( + ProcessInstanceIsNotEnqueuedError, + ProcessInstanceIsAlreadyLockedError, + ) as e: ErrorHandlingService().handle_error(processor, e) raise e @@ -186,7 +193,10 @@ def process_instance_suspend( try: with ProcessInstanceQueueService.dequeued(process_instance): processor.suspend() - except (ProcessInstanceIsNotEnqueuedError, ProcessInstanceIsAlreadyLockedError) as e: + except ( + ProcessInstanceIsNotEnqueuedError, + ProcessInstanceIsAlreadyLockedError, + ) as e: ErrorHandlingService().handle_error(processor, e) raise e @@ -204,7 +214,10 @@ def process_instance_resume( try: with ProcessInstanceQueueService.dequeued(process_instance): processor.resume() - except (ProcessInstanceIsNotEnqueuedError, ProcessInstanceIsAlreadyLockedError) as e: + except ( + ProcessInstanceIsNotEnqueuedError, + ProcessInstanceIsAlreadyLockedError, + ) as e: ErrorHandlingService().handle_error(processor, e) raise e @@ -227,7 +240,8 @@ def process_instance_log_list( .outerjoin(TaskModel, TaskModel.guid == ProcessInstanceEventModel.task_guid) .outerjoin(TaskDefinitionModel, TaskDefinitionModel.id == TaskModel.task_definition_id) .outerjoin( - BpmnProcessDefinitionModel, BpmnProcessDefinitionModel.id == TaskDefinitionModel.bpmn_process_definition_id + BpmnProcessDefinitionModel, + BpmnProcessDefinitionModel.id == TaskDefinitionModel.bpmn_process_definition_id, ) ) if not detailed: @@ -374,7 +388,9 @@ def process_instance_list( return make_response(jsonify(response_json), 200) -def process_instance_report_column_list(process_model_identifier: Optional[str] = None) -> flask.wrappers.Response: +def process_instance_report_column_list( + process_model_identifier: Optional[str] = None, +) -> flask.wrappers.Response: """Process_instance_report_column_list.""" table_columns = ProcessInstanceReportService.builtin_column_options() columns_for_metadata_query = ( @@ -646,7 +662,8 @@ def process_instance_task_list( == direct_parent_bpmn_process_alias.bpmn_process_definition_id, ) .join( - BpmnProcessDefinitionModel, BpmnProcessDefinitionModel.id == TaskDefinitionModel.bpmn_process_definition_id + BpmnProcessDefinitionModel, + BpmnProcessDefinitionModel.id == TaskDefinitionModel.bpmn_process_definition_id, ) .add_columns( BpmnProcessDefinitionModel.bpmn_identifier.label("bpmn_process_definition_identifier"), # type: ignore @@ -672,14 +689,22 @@ def process_instance_task_list( task_model_query = task_model_query.filter(bpmn_process_alias.id.in_(bpmn_process_ids)) task_models = task_model_query.all() - task_model_list = {} if most_recent_tasks_only: + most_recent_tasks = {} + most_recent_subprocesses = set() for task_model in task_models: bpmn_process_guid = task_model.bpmn_process_guid or "TOP" row_key = f"{bpmn_process_guid}:::{task_model.bpmn_identifier}" - if row_key not in task_model_list: - task_model_list[row_key] = task_model - task_models = list(task_model_list.values()) + if row_key not in most_recent_tasks: + most_recent_tasks[row_key] = task_model + if task_model.typename in ["SubWorkflowTask", "CallActivity"]: + most_recent_subprocesses.add(task_model.guid) + + task_models = [ + task_model + for task_model in most_recent_tasks.values() + if task_model.bpmn_process_guid in most_recent_subprocesses or task_model.bpmn_process_guid is None + ] if to_task_model is not None: task_models_dict = json.loads(current_app.json.dumps(task_models)) diff --git a/spiffworkflow-frontend/src/components/ReactDiagramEditor.tsx b/spiffworkflow-frontend/src/components/ReactDiagramEditor.tsx index eefaff82..82dddd4a 100644 --- a/spiffworkflow-frontend/src/components/ReactDiagramEditor.tsx +++ b/spiffworkflow-frontend/src/components/ReactDiagramEditor.tsx @@ -68,6 +68,7 @@ type OwnProps = { diagramType: string; readyOrWaitingProcessInstanceTasks?: Task[] | null; completedProcessInstanceTasks?: Task[] | null; + cancelledProcessInstanceTasks?: Task[] | null; saveDiagram?: (..._args: any[]) => any; onDeleteFile?: (..._args: any[]) => any; isPrimaryFile?: boolean; @@ -94,6 +95,7 @@ export default function ReactDiagramEditor({ diagramType, readyOrWaitingProcessInstanceTasks, completedProcessInstanceTasks, + cancelledProcessInstanceTasks, saveDiagram, onDeleteFile, isPrimaryFile, @@ -358,7 +360,8 @@ export default function ReactDiagramEditor({ function checkTaskCanBeHighlighted(taskBpmnId: string) { return ( !taskSpecsThatCannotBeHighlighted.includes(taskBpmnId) && - !taskBpmnId.match(/EndJoin/) + !taskBpmnId.match(/EndJoin/) && + !taskBpmnId.match(/BoundaryEventParent/) ); } @@ -441,6 +444,19 @@ export default function ReactDiagramEditor({ ); }); } + if (cancelledProcessInstanceTasks) { + const bpmnProcessIdentifiers = getBpmnProcessIdentifiers( + canvas.getRootElement() + ); + cancelledProcessInstanceTasks.forEach((cancelledTask) => { + highlightBpmnIoElement( + canvas, + cancelledTask, + 'cancelled-task-highlight', + bpmnProcessIdentifiers + ); + }); + } } function displayDiagram( @@ -518,6 +534,7 @@ export default function ReactDiagramEditor({ diagramXMLString, readyOrWaitingProcessInstanceTasks, completedProcessInstanceTasks, + cancelledProcessInstanceTasks, fileName, performingXmlUpdates, processModelId, diff --git a/spiffworkflow-frontend/src/index.css b/spiffworkflow-frontend/src/index.css index e05c2847..d98187cd 100644 --- a/spiffworkflow-frontend/src/index.css +++ b/spiffworkflow-frontend/src/index.css @@ -142,6 +142,10 @@ code { fill: grey !important; opacity: .4; } +.cancelled-task-highlight:not(.djs-connection) .djs-visual > :nth-child(1) { + fill: blue !important; + opacity: .2; +} .accordion-item-label { vertical-align: middle; diff --git a/spiffworkflow-frontend/src/interfaces.ts b/spiffworkflow-frontend/src/interfaces.ts index 1d34054d..802c48c7 100644 --- a/spiffworkflow-frontend/src/interfaces.ts +++ b/spiffworkflow-frontend/src/interfaces.ts @@ -58,6 +58,7 @@ export interface Task { export interface TaskIds { completed: Task[]; readyOrWaiting: Task[]; + cancelled: Task[]; } export interface ProcessInstanceTask { diff --git a/spiffworkflow-frontend/src/routes/ProcessInstanceShow.tsx b/spiffworkflow-frontend/src/routes/ProcessInstanceShow.tsx index 29d4bedc..c294ae48 100644 --- a/spiffworkflow-frontend/src/routes/ProcessInstanceShow.tsx +++ b/spiffworkflow-frontend/src/routes/ProcessInstanceShow.tsx @@ -231,13 +231,19 @@ export default function ProcessInstanceShow({ variant }: OwnProps) { }; const getTaskIds = () => { - const taskIds: TaskIds = { completed: [], readyOrWaiting: [] }; + const taskIds: TaskIds = { + completed: [], + readyOrWaiting: [], + cancelled: [], + }; if (tasks) { tasks.forEach(function getUserTasksElement(task: Task) { if (task.state === 'COMPLETED') { taskIds.completed.push(task); } else if (task.state === 'READY' || task.state === 'WAITING') { taskIds.readyOrWaiting.push(task); + } else if (task.state === 'CANCELLED') { + taskIds.cancelled.push(task); } return null; }); @@ -1152,6 +1158,7 @@ export default function ProcessInstanceShow({ variant }: OwnProps) { fileName={processInstance.bpmn_xml_file_contents || ''} readyOrWaitingProcessInstanceTasks={taskIds.readyOrWaiting} completedProcessInstanceTasks={taskIds.completed} + cancelledProcessInstanceTasks={taskIds.cancelled} diagramType="readonly" onElementClick={handleClickedDiagramTask} />