* Adding signal_buttons to tasks, which will prompt the frontend to display a button that when pressed will cause the signal to fire.

* This alters how the send_event endpoint responds - it originally responded with a process instance, it now responds with the next task, in keeping with how other task completion endpoints behave.
* I was forced to upgrade some of the bpmn-js libraries which fixes some of the linting errors on the front end.
* The "Return to home" button isn't always displayed.  It will not display when it is redirecting, or when the current task is running.
.
This commit is contained in:
Dan 2023-05-02 13:40:41 -04:00
parent 0552eaeb7b
commit ac149c9dcb
13 changed files with 891 additions and 337 deletions

View File

@ -85,6 +85,7 @@ class TaskModel(SpiffworkflowBaseDBModel):
can_complete: Optional[bool] = None
extensions: Optional[dict] = None
name_for_display: Optional[str] = None
signal_buttons: Optional[dict] = None
def get_data(self) -> dict:
return {**self.python_env_data(), **self.json_data()}

View File

@ -32,6 +32,7 @@ from spiffworkflow_backend.services.process_caller_service import ProcessCallerS
from spiffworkflow_backend.services.process_instance_processor import (
ProcessInstanceProcessor,
)
from spiffworkflow_backend.services.process_instance_service import ProcessInstanceService
from spiffworkflow_backend.services.process_model_service import ProcessModelService
@ -199,16 +200,13 @@ def send_bpmn_event(
if process_instance:
processor = ProcessInstanceProcessor(process_instance)
processor.send_bpmn_event(body)
task = ProcessInstanceService.spiff_task_to_api_task(processor, processor.next_task())
return make_response(jsonify(task), 200)
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",
)
def _commit_and_push_to_git(message: str) -> None:

View File

@ -288,6 +288,7 @@ def task_show(process_instance_id: int, task_guid: str = "next") -> flask.wrappe
task_model = _get_task_model_from_guid_or_raise(task_guid, process_instance_id)
task_definition = task_model.task_definition
extensions = TaskService.get_extensions_from_task_model(task_model)
task_model.signal_buttons = TaskService.get_ready_signals_with_button_labels(process_instance_id)
if "properties" in extensions:
properties = extensions["properties"]

View File

@ -2,7 +2,7 @@ import copy
import json
import time
from hashlib import sha256
from typing import Optional
from typing import Optional, List
from typing import Tuple
from typing import TypedDict
from typing import Union
@ -633,6 +633,26 @@ class TaskService:
)
return extensions
@classmethod
def get_ready_signals_with_button_labels(cls, process_instance_id: int) -> list[dict]:
waiting_tasks: List[TaskModel] = TaskModel.query.filter_by(
state="WAITING", process_instance_id=process_instance_id
).all()
result = []
for task_model in waiting_tasks:
task_definition = task_model.task_definition
extensions: dict = (
task_definition.properties_json["extensions"] if "extensions" in task_definition.properties_json else {}
)
event_definition: dict = (
task_definition.properties_json["event_definition"] if "event_definition" in task_definition.properties_json else {}
)
if 'signalButtonLabel' in extensions and 'name' in event_definition:
result.append({'event': event_definition, 'label': extensions['signalButtonLabel']})
return result
@classmethod
def get_spec_reference_from_bpmn_process(cls, bpmn_process: BpmnProcessModel) -> SpecReferenceCache:
"""Get the bpmn file for a given task model.

View File

@ -0,0 +1,86 @@
<?xml version="1.0" encoding="UTF-8"?>
<bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:spiffworkflow="http://spiffworkflow.org/bpmn/schema/1.0/core" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" id="Definitions_96f6665" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="3.0.0-dev">
<bpmn:process id="SpiffCatchEventExtensions" isExecutable="true">
<bpmn:startEvent id="StartEvent_1">
<bpmn:outgoing>Flow_0elszck</bpmn:outgoing>
</bpmn:startEvent>
<bpmn:sequenceFlow id="Flow_0elszck" sourceRef="StartEvent_1" targetRef="Activity_0cmmlen" />
<bpmn:endEvent id="Event_1mjvim4">
<bpmn:incoming>Flow_1akz8b3</bpmn:incoming>
</bpmn:endEvent>
<bpmn:sequenceFlow id="Flow_1akz8b3" sourceRef="Activity_0cmmlen" targetRef="Event_1mjvim4" />
<bpmn:sequenceFlow id="Flow_0uenxs3" sourceRef="SpamEvent" targetRef="Activity_1u4om4i" />
<bpmn:endEvent id="Event_1dvll15">
<bpmn:incoming>Flow_16bzuvz</bpmn:incoming>
</bpmn:endEvent>
<bpmn:sequenceFlow id="Flow_16bzuvz" sourceRef="Activity_1u4om4i" targetRef="Event_1dvll15" />
<bpmn:manualTask id="Activity_0cmmlen" name="My Manual Task">
<bpmn:extensionElements>
<spiffworkflow:instructionsForEndUser># Welcome
This manual task has Two Buttons! The first is standard submit button that will take you to the end. The second button will fire a signal event and take you to a different manual task.</spiffworkflow:instructionsForEndUser>
</bpmn:extensionElements>
<bpmn:incoming>Flow_0elszck</bpmn:incoming>
<bpmn:outgoing>Flow_1akz8b3</bpmn:outgoing>
</bpmn:manualTask>
<bpmn:manualTask id="Activity_1u4om4i" name="Spam Message">
<bpmn:extensionElements>
<spiffworkflow:instructionsForEndUser># Spam Eaten!
Congratulations! You have selected the Eat Additional Spam option, which opens up new doors to vast previously uncharted culinary eating experiences! Oh the Joy! Oh the Reward! Sweet savory wonderful Spam! </spiffworkflow:instructionsForEndUser>
</bpmn:extensionElements>
<bpmn:incoming>Flow_0uenxs3</bpmn:incoming>
<bpmn:outgoing>Flow_16bzuvz</bpmn:outgoing>
</bpmn:manualTask>
<bpmn:boundaryEvent id="SpamEvent" name="Spam Event" attachedToRef="Activity_0cmmlen">
<bpmn:extensionElements>
<spiffworkflow:signalButtonLabel>Eat Spam</spiffworkflow:signalButtonLabel>
</bpmn:extensionElements>
<bpmn:outgoing>Flow_0uenxs3</bpmn:outgoing>
<bpmn:signalEventDefinition id="SignalEventDefinition_11tlwya" signalRef="Signal_17t90lm" />
</bpmn:boundaryEvent>
</bpmn:process>
<bpmn:signal id="Signal_17t90lm" name="eat_spam" />
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Process_z1jgvu5">
<bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1">
<dc:Bounds x="179" y="159" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Event_1mjvim4_di" bpmnElement="Event_1mjvim4">
<dc:Bounds x="432" y="159" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Event_1dvll15_di" bpmnElement="Event_1dvll15">
<dc:Bounds x="562" y="282" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_0zxmtux_di" bpmnElement="Activity_0cmmlen">
<dc:Bounds x="270" y="137" width="100" height="80" />
<bpmndi:BPMNLabel />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_0tll58x_di" bpmnElement="Activity_1u4om4i">
<dc:Bounds x="410" y="260" width="100" height="80" />
<bpmndi:BPMNLabel />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Event_0vnraxp_di" bpmnElement="SpamEvent">
<dc:Bounds x="322" y="199" width="36" height="36" />
<bpmndi:BPMNLabel>
<dc:Bounds x="311" y="242" width="61" height="14" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="Flow_0elszck_di" bpmnElement="Flow_0elszck">
<di:waypoint x="215" y="177" />
<di:waypoint x="270" y="177" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_1akz8b3_di" bpmnElement="Flow_1akz8b3">
<di:waypoint x="370" y="177" />
<di:waypoint x="432" y="177" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_0uenxs3_di" bpmnElement="Flow_0uenxs3">
<di:waypoint x="340" y="235" />
<di:waypoint x="340" y="300" />
<di:waypoint x="410" y="300" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_16bzuvz_di" bpmnElement="Flow_16bzuvz">
<di:waypoint x="510" y="300" />
<di:waypoint x="562" y="300" />
</bpmndi:BPMNEdge>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</bpmn:definitions>

View File

@ -2747,7 +2747,8 @@ class TestProcessApi(BaseTest):
)
assert response.status_code == 200
assert response.json is not None
assert response.json["status"] == "complete"
assert response.json["type"] == "End Event"
assert response.json["state"] == "COMPLETED"
response = client.get(
f"/v1.0/process-instances/{self.modify_process_identifier_for_path_param(process_model_identifier)}/{process_instance_id}/task-info?all_tasks=true",

View File

@ -156,3 +156,31 @@ class TestTaskService(BaseTest):
assert task_model_level_3 is not None
bpmn_process = TaskService.bpmn_process_for_called_activity_or_top_level_process(task_model_level_3)
assert bpmn_process.bpmn_process_definition.bpmn_identifier == "Level3"
def test_get_button_labels_for_waiting_signal_event_tasks(
self,
app: Flask,
with_db_and_bpmn_file_cleanup: None,
) -> None:
process_model = load_test_spec(
"test_group/signal_event_extensions",
process_model_source_directory="signal_event_extensions",
bpmn_file_name="signal_event_extensions",
)
load_test_spec(
f"test_group/SpiffCatchEventExtensions",
process_model_source_directory="call_activity_nested",
bpmn_file_name="SpiffCatchEventExtensions"
)
process_instance = self.create_process_instance_from_process_model(process_model)
processor = ProcessInstanceProcessor(process_instance)
processor.do_engine_steps(save=True, execution_strategy_name="greedy")
events = TaskService.get_ready_signals_with_button_labels(process_instance.id)
assert(len(events) == 1)
signal_event = events[0]
assert(signal_event['event']['name'] == 'eat_spam')
assert(signal_event['event']['typename'] == 'SignalEventDefinition')
assert(signal_event['label'] == 'Eat Spam')
print(events)

File diff suppressed because it is too large Load Diff

View File

@ -31,14 +31,14 @@
"autoprefixer": "10.4.8",
"axios": "^0.27.2",
"bootstrap": "^5.2.0",
"bpmn-js": "^9.3.2",
"bpmn-js-properties-panel": "^1.10.0",
"bpmn-js": "^13.0.0",
"bpmn-js-properties-panel": "^1.22.0",
"bpmn-js-spiffworkflow": "github:sartography/bpmn-js-spiffworkflow#main",
"cookie": "^0.5.0",
"craco": "^0.0.3",
"cypress-slow-down": "^1.2.1",
"date-fns": "^2.28.0",
"diagram-js": "^8.5.0",
"diagram-js": "^11.9.1",
"dmn-js": "^12.2.0",
"dmn-js-properties-panel": "^1.1",
"dmn-js-shared": "^12.1.1",

View File

@ -1,7 +1,5 @@
/* eslint-disable sonarjs/cognitive-complexity */
// @ts-expect-error TS(7016) FIXME: Could not find a declaration file for module 'bpmn... Remove this comment to see the full error message
import BpmnModeler from 'bpmn-js/lib/Modeler';
// @ts-expect-error TS(7016) FIXME: Could not find a declaration file for module 'bpmn... Remove this comment to see the full error message
import BpmnViewer from 'bpmn-js/lib/Viewer';
import {
BpmnPropertiesPanelModule,

View File

@ -37,6 +37,11 @@ export interface EventDefinition {
message_var?: string;
}
export interface SignalButton {
label: string;
event: EventDefinition;
}
// TODO: merge with ProcessInstanceTask
export interface Task {
id: number;
@ -60,6 +65,7 @@ export interface Task {
can_complete: boolean;
form_schema: any;
form_ui_schema: any;
signal_buttons: SignalButton[];
}
export interface ProcessInstanceTask {

View File

@ -58,7 +58,6 @@ export default function ProcessInterstitial() {
// Added this seperate use effect so that the timer interval will be cleared if
// we end up redirecting back to the TaskShow page.
if (shouldRedirect(lastTask)) {
setState('REDIRECTING');
lastTask.properties.instructionsForEndUser = '';
const timerId = setInterval(() => {
navigate(`/tasks/${lastTask.process_instance_id}/${lastTask.id}`);
@ -103,16 +102,17 @@ export default function ProcessInterstitial() {
const getReturnHomeButton = (index: number) => {
if (
index === 0 &&
state !== 'REDIRECTING' &&
!shouldRedirect(lastTask) &&
['WAITING', 'ERROR', 'LOCKED', 'COMPLETED', 'READY'].includes(getStatus())
)
) {
return (
<div style={{ padding: '10px 0 0 0' }}>
<div style={{padding: '10px 0 0 0'}}>
<Button kind="secondary" onClick={() => navigate(`/tasks`)}>
Return to Home
</Button>
</div>
);
}
return '';
};
@ -165,7 +165,7 @@ export default function ProcessInterstitial() {
/** In the event there is no task information and the connection closed,
* redirect to the home page. */
if (state === 'closed' && lastTask === null) {
if (state === 'CLOSED' && lastTask === null) {
navigate(`/tasks`);
}
if (lastTask) {

View File

@ -18,7 +18,7 @@ import Form from '../themes/carbon';
import HttpService from '../services/HttpService';
import useAPIError from '../hooks/UseApiError';
import { modifyProcessIdentifierForPathParam } from '../helpers';
import { Task } from '../interfaces';
import {EventDefinition, Task} from '../interfaces';
import ProcessBreadcrumb from '../components/ProcessBreadcrumb';
import InstructionsForEndUser from '../components/InstructionsForEndUser';
@ -195,6 +195,24 @@ export default function TaskShow() {
});
};
const handleSignalSubmit = (event: EventDefinition) => {
console.log("Signal Event ", event)
if (disabled || !task) {
return;
}
HttpService.makeCallToBackend({
path: `/send-event/${modifyProcessIdentifierForPathParam(
task.process_model_identifier
)}/${params.process_instance_id}`,
successCallback: processSubmitResult,
failureCallback: (error: any) => {
addError(error);
},
httpMethod: 'POST',
postBody: event,
});
}
const buildTaskNavigation = () => {
let userTasksElement;
let selectedTabIndex = 0;
@ -349,14 +367,19 @@ export default function TaskShow() {
</Button>
);
}
reactFragmentToHideSubmitButton = (
<ButtonSet>
reactFragmentToHideSubmitButton = <ButtonSet>
<Button type="submit" id="submit-button" disabled={disabled}>
{submitButtonText}
</Button>
{saveAsDraftButton}
</ButtonSet>
);
<>
{task.signal_buttons.map((signal, i) =>
<Button name={`signal.signal`} disabled={disabled} onClick={() => handleSignalSubmit(signal.event)}>
{signal.label}
</Button>
)}
</>
</ButtonSet>;
}
const customValidate = (formData: any, errors: any) => {