From 232ff1c6f7b78976dc5612bab2d024e50ad186ca Mon Sep 17 00:00:00 2001 From: Elizabeth Esswein Date: Wed, 14 Dec 2022 15:21:39 -0500 Subject: [PATCH] working but barely functional UI for manually sending events --- .../src/spiffworkflow_backend/api.yml | 21 ++++ .../src/spiffworkflow_backend/models/task.py | 4 + .../routes/process_api_blueprint.py | 24 +++- .../services/process_instance_processor.py | 17 ++- .../services/process_instance_service.py | 9 +- .../src/routes/ProcessInstanceShow.tsx | 108 +++++++++++++++++- 6 files changed, 171 insertions(+), 12 deletions(-) diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/api.yml b/spiffworkflow-backend/src/spiffworkflow_backend/api.yml index e7dc00fe2..bc8bab525 100755 --- a/spiffworkflow-backend/src/spiffworkflow_backend/api.yml +++ b/spiffworkflow-backend/src/spiffworkflow_backend/api.yml @@ -586,6 +586,27 @@ paths: schema: $ref: "#/components/schemas/Workflow" + /process-instances/{process_instance_id}/event: + parameters: + - name: process_instance_id + in: path + required: true + description: The unique id of the process instance + schema: + type: string + post: + operationId: spiffworkflow_backend.routes.process_api_blueprint.send_bpmn_event + summary: Send a BPMN event to the process + tags: + - Process Instances + responses: + "200": + description: Event Sent Successfully + content: + application/json: + schema: + $ref: "#/components/schemas/Workflow" + /process-models/{process_group_id}/{process_model_id}/script-unit-tests: parameters: - name: process_group_id diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/models/task.py b/spiffworkflow-backend/src/spiffworkflow_backend/models/task.py index 52bb11715..be3a3e685 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/models/task.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/models/task.py @@ -118,6 +118,7 @@ class Task: form_schema: Union[str, None] = None, form_ui_schema: Union[str, None] = None, parent: Optional[str] = None, + event_definition: Union[dict[str, Any], None] = None ): """__init__.""" self.id = id @@ -129,6 +130,7 @@ class Task: self.documentation = documentation self.lane = lane self.parent = parent + self.event_definition = event_definition self.data = data if self.data is None: @@ -187,6 +189,7 @@ class Task: "form_schema": self.form_schema, "form_ui_schema": self.form_ui_schema, "parent": self.parent, + "event_definition": self.event_definition, } @classmethod @@ -287,6 +290,7 @@ class TaskSchema(Schema): "process_instance_id", "form_schema", "form_ui_schema", + "event_definition", ] multi_instance_type = EnumField(MultiInstanceType) diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_api_blueprint.py b/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_api_blueprint.py index 739e689d2..5eba99988 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_api_blueprint.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_api_blueprint.py @@ -1311,7 +1311,7 @@ def process_instance_task_list( tasks = [] for spiff_task in spiff_tasks: - task = ProcessInstanceService.spiff_task_to_api_task(spiff_task) + task = ProcessInstanceService.spiff_task_to_api_task(processor, spiff_task) task.data = spiff_task.data tasks.append(task) @@ -1344,7 +1344,9 @@ def task_show(process_instance_id: int, task_id: str) -> flask.wrappers.Response form_schema_file_name = properties["formJsonSchemaFilename"] if "formUiSchemaFilename" in properties: form_ui_schema_file_name = properties["formUiSchemaFilename"] - task = ProcessInstanceService.spiff_task_to_api_task(spiff_task) + + processor = ProcessInstanceProcessor(process_instance) + task = ProcessInstanceService.spiff_task_to_api_task(processor, spiff_task) task.data = spiff_task.data task.process_model_display_name = process_model.display_name task.process_model_identifier = process_model.id @@ -1882,3 +1884,21 @@ def update_task_data(process_instance_id: str, task_id: str, body: Dict) -> Resp status=200, mimetype="application/json", ) + +def send_bpmn_event(process_instance_id: str, body: Dict) -> Response: + process_instance = ProcessInstanceModel.query.filter( + ProcessInstanceModel.id == int(process_instance_id) + ).first() + if process_instance: + processor = ProcessInstanceProcessor(process_instance) + processor.send_bpmn_event(body) + else: + raise ApiError( + error_code="send_bpmn_event_error", + message=f"Could not send event to Instance: {process_instance_id}", + ) + return Response( + json.dumps(ProcessInstanceModelSchema().dump(process_instance)), + status=200, + mimetype="application/json", + ) diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_processor.py b/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_processor.py index d1df67428..5b2fc88d6 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_processor.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_processor.py @@ -45,9 +45,8 @@ from SpiffWorkflow.spiff.serializer.task_spec_converters import ( from SpiffWorkflow.spiff.serializer.task_spec_converters import EndEventConverter from SpiffWorkflow.spiff.serializer.task_spec_converters import ( IntermediateCatchEventConverter, -) -from SpiffWorkflow.spiff.serializer.task_spec_converters import ( IntermediateThrowEventConverter, + EventBasedGatewayConverter, ) from SpiffWorkflow.spiff.serializer.task_spec_converters import ManualTaskConverter from SpiffWorkflow.spiff.serializer.task_spec_converters import NoneTaskConverter @@ -254,6 +253,7 @@ class ProcessInstanceProcessor: EndEventConverter, IntermediateCatchEventConverter, IntermediateThrowEventConverter, + EventBasedGatewayConverter, ManualTaskConverter, NoneTaskConverter, ReceiveTaskConverter, @@ -267,6 +267,7 @@ class ProcessInstanceProcessor: ] ) _serializer = BpmnWorkflowSerializer(wf_spec_converter, version=SERIALIZER_VERSION) + _event_serializer = EventBasedGatewayConverter() PROCESS_INSTANCE_ID_KEY = "process_instance_id" VALIDATION_PROCESS_KEY = "validate_only" @@ -658,6 +659,18 @@ class ProcessInstanceProcessor: db.session.delete(at) db.session.commit() + def serialize_task_spec(self, task_spec: SpiffTask) -> dict[str, Any]: + return self._serializer.spec_converter.convert(task_spec) + + def send_bpmn_event(self, event_data: dict[str,Any]) -> None: + payload = event_data.pop("payload", None) + event_definition = self._event_serializer.restore(event_data) + if payload is not None: + event_definition.payload = payload + current_app.logger.info(f"Event of type {event_definition.event_type} sent to process instance {self.process_instance_model.id}") + self.bpmn_process_instance.catch(event_definition) + self.do_engine_steps(save=True) + @staticmethod def get_parser() -> MyCustomParser: """Get_parser.""" diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_service.py b/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_service.py index f98eaae18..4f1f60eea 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_service.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_service.py @@ -121,7 +121,7 @@ class ProcessInstanceService: if next_task_trying_again is not None: process_instance_api.next_task = ( ProcessInstanceService.spiff_task_to_api_task( - next_task_trying_again, add_docs_and_forms=True + processor, next_task_trying_again, add_docs_and_forms=True ) ) @@ -277,7 +277,9 @@ class ProcessInstanceService: @staticmethod def spiff_task_to_api_task( - spiff_task: SpiffTask, add_docs_and_forms: bool = False + processor: ProcessInstanceProcessor, + spiff_task: SpiffTask, + add_docs_and_forms: bool = False ) -> Task: """Spiff_task_to_api_task.""" task_type = spiff_task.task_spec.spec_type @@ -306,6 +308,8 @@ class ProcessInstanceService: if spiff_task.parent: parent_id = spiff_task.parent.id + serialized_task_spec = processor.serialize_task_spec(spiff_task.task_spec) + task = Task( spiff_task.id, spiff_task.task_spec.name, @@ -319,6 +323,7 @@ class ProcessInstanceService: process_name=spiff_task.task_spec._wf_spec.description, properties=props, parent=parent_id, + event_definition=serialized_task_spec.get("event_definition"), ) return task diff --git a/spiffworkflow-frontend/src/routes/ProcessInstanceShow.tsx b/spiffworkflow-frontend/src/routes/ProcessInstanceShow.tsx index c407c7713..2e0a7ef37 100644 --- a/spiffworkflow-frontend/src/routes/ProcessInstanceShow.tsx +++ b/spiffworkflow-frontend/src/routes/ProcessInstanceShow.tsx @@ -20,6 +20,7 @@ import { ButtonSet, Tag, Modal, + Dropdown, Stack, // @ts-ignore } from '@carbon/react'; @@ -47,6 +48,9 @@ export default function ProcessInstanceShow() { const [taskToDisplay, setTaskToDisplay] = useState(null); const [taskDataToDisplay, setTaskDataToDisplay] = useState(''); const [editingTaskData, setEditingTaskData] = useState(false); + const [selectingEvent, setSelectingEvent] = useState(false); + const [eventToSend, setEventToSend] = useState({}); + const [eventPayload, setEventPayload] = useState('{}'); const setErrorMessage = (useContext as any)(ErrorContext)[1]; @@ -404,8 +408,34 @@ export default function ProcessInstanceShow() { ); }; - const cancelEditingTaskData = () => { + const canSendEvent = (task: any) => { + // We actually could allow this for any waiting events + const taskTypes = ['Event Based Gateway']; + return ( + taskTypes.filter((t) => t === task.type).length > 0 && + task.state === 'WAITING' && + showingLastSpiffStep(processInstance as any) + ); + }; + + const getEvents = (task: any) => { + const handleMessage = (eventDefinition: any) => { + if (eventDefinition.typename === 'MessageEventDefinition') { + delete eventDefinition.message_var; + eventDefinition.payload = {}; + } + return eventDefinition; + }; + if (task.event_definition && task.event_definition.event_definitions) + return task.event_definition.event_definitions.map((e: any) => handleMessage(e)); + if (task.event_definition) + return [handleMessage(task.event_definition)]; + return []; + }; + + const cancelUpdatingTask = () => { setEditingTaskData(false); + setSelectingEvent(false); initializeTaskDataToDisplay(taskToDisplay); setErrorMessage(null); }; @@ -446,6 +476,18 @@ export default function ProcessInstanceShow() { }); }; + const sendEvent = () => { + if ('payload' in eventToSend) + eventToSend.payload = JSON.parse(eventPayload); + HttpService.makeCallToBackend({ + path: `/process-instances/${params.process_instance_id}/event`, + httpMethod: 'POST', + successCallback: saveTaskDataResult, + failureCallback: saveTaskDataFailure, + postBody: eventToSend, + }); + }; + const taskDataButtons = (task: any) => { const buttons = []; @@ -460,7 +502,7 @@ export default function ProcessInstanceShow() { ); } - if (canEditTaskData(task)) { + if (canEditTaskData(task) || canSendEvent(task)) { if (editingTaskData) { buttons.push( + ); + } else if (selectingEvent) { + buttons.push( + + ); + buttons.push( + @@ -487,6 +546,16 @@ export default function ProcessInstanceShow() { Edit ); + if (canSendEvent(task)) { + buttons.push( + + ); + } } } @@ -507,8 +576,35 @@ export default function ProcessInstanceShow() { ); }; - const taskDataDisplayArea = () => { + const eventSelector = (candidateEvents: any) => { + const editor = ( + setEventPayload(value || '{}')} + /> + ) + return selectingEvent ? ( + + item.name || item.label || item.typename} + onChange={(value: any) => setEventToSend(value.selectedItem)} + /> + {'payload' in eventToSend ? editor : ''} + + ) : ( + taskDataContainer() + ); + }; + + const taskUpdateDisplayArea = () => { const taskToUse: any = { ...taskToDisplay, data: taskDataToDisplay }; + const candidateEvents: any = getEvents(taskToUse); if (taskToDisplay) { return ( - {taskDataContainer()} + {selectingEvent ? eventSelector(candidateEvents) : taskDataContainer()} ); } @@ -592,7 +688,7 @@ export default function ProcessInstanceShow() {
{getInfoTag(processInstanceToUse)}
- {taskDataDisplayArea()} + {taskUpdateDisplayArea()} {stepsElement(processInstanceToUse)}