Skip to content

Feature/process navigation #78

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 21 commits into from
Dec 30, 2022
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions spiffworkflow-backend/src/spiffworkflow_backend/api.yml
Original file line number Diff line number Diff line change
Expand Up @@ -610,6 +610,27 @@ paths:
items:
$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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ def __init__(
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,
call_activity_process_identifier: Optional[str] = None,
):
"""__init__."""
Expand All @@ -130,6 +131,7 @@ def __init__(
self.documentation = documentation
self.lane = lane
self.parent = parent
self.event_definition = event_definition
self.call_activity_process_identifier = call_activity_process_identifier

self.data = data
Expand Down Expand Up @@ -189,6 +191,7 @@ def serialized(self) -> dict[str, Any]:
"form_schema": self.form_schema,
"form_ui_schema": self.form_ui_schema,
"parent": self.parent,
"event_definition": self.event_definition,
"call_activity_process_identifier": self.call_activity_process_identifier,
}

Expand Down Expand Up @@ -290,6 +293,7 @@ class Meta:
"process_instance_id",
"form_schema",
"form_ui_schema",
"event_definition",
]

multi_instance_type = EnumField(MultiInstanceType)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1447,7 +1447,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)

Expand Down Expand Up @@ -1480,7 +1480,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
Expand Down Expand Up @@ -2038,3 +2040,22 @@ def update_task_data(
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",
)
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,8 @@
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
Expand Down Expand Up @@ -263,6 +262,7 @@ class ProcessInstanceProcessor:
EndEventConverter,
IntermediateCatchEventConverter,
IntermediateThrowEventConverter,
EventBasedGatewayConverter,
ManualTaskConverter,
NoneTaskConverter,
ReceiveTaskConverter,
Expand All @@ -276,6 +276,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"
Expand Down Expand Up @@ -704,6 +705,20 @@ def save(self) -> None:
db.session.delete(at)
db.session.commit()

def serialize_task_spec(self, task_spec: SpiffTask) -> 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."""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ def processor_to_process_instance_api(
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
)
)

Expand Down Expand Up @@ -277,7 +277,9 @@ def set_dot_value(path: str, value: Any, target: dict) -> dict:

@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
Expand Down Expand Up @@ -311,6 +313,8 @@ def spiff_task_to_api_task(
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,
Expand All @@ -324,6 +328,7 @@ def spiff_task_to_api_task(
process_identifier=spiff_task.task_spec._wf_spec.name,
properties=props,
parent=parent_id,
event_definition=serialized_task_spec.get("event_definition"),
call_activity_process_identifier=call_activity_process_identifier,
)

Expand Down
120 changes: 114 additions & 6 deletions spiffworkflow-frontend/src/routes/ProcessInstanceShow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
ButtonSet,
Tag,
Modal,
Dropdown,
Stack,
// @ts-ignore
} from '@carbon/react';
Expand Down Expand Up @@ -58,6 +59,11 @@ export default function ProcessInstanceShow() {
const [taskToDisplay, setTaskToDisplay] = useState<object | null>(null);
const [taskDataToDisplay, setTaskDataToDisplay] = useState<string>('');
const [editingTaskData, setEditingTaskData] = useState<boolean>(false);
const [selectingEvent, setSelectingEvent] = useState<boolean>(false);
const [eventToSend, setEventToSend] = useState<any>({});
const [eventPayload, setEventPayload] = useState<string>('{}');
const [eventTextEditorEnabled, setEventTextEditorEnabled] =
useState<boolean>(false);

const setErrorMessage = (useContext as any)(ErrorContext)[1];

Expand Down Expand Up @@ -446,9 +452,39 @@ 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') {
const newEvent = eventDefinition;
delete newEvent.message_var;
newEvent.payload = {};
return newEvent;
}
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);
setEventPayload('{}');
setErrorMessage(null);
};

Expand Down Expand Up @@ -488,6 +524,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 = [];

Expand All @@ -514,17 +562,31 @@ export default function ProcessInstanceShow() {
);
}

if (canEditTaskData(task)) {
if (canEditTaskData(task) || canSendEvent(task)) {
if (editingTaskData) {
buttons.push(
<Button data-qa="save-task-data-button" onClick={saveTaskData}>
Save
</Button>
);
buttons.push(
<Button
data-qa="create-script-unit-test-button"
onClick={cancelUpdatingTask}
>
Cancel
</Button>
);
} else if (selectingEvent) {
buttons.push(
<Button data-qa="create-script-unit-test-button" onClick={sendEvent}>
Send
</Button>
);
buttons.push(
<Button
data-qa="cancel-task-data-edit-button"
onClick={cancelEditingTaskData}
onClick={cancelUpdatingTask}
>
Cancel
</Button>
Expand All @@ -538,6 +600,16 @@ export default function ProcessInstanceShow() {
Edit
</Button>
);
if (canSendEvent(task)) {
buttons.push(
<Button
data-qa="create-script-unit-test-button"
onClick={() => setSelectingEvent(true)}
>
Send Event
</Button>
);
}
}
}

Expand All @@ -558,8 +630,42 @@ export default function ProcessInstanceShow() {
);
};

const taskDataDisplayArea = () => {
const eventSelector = (candidateEvents: any) => {
const editor = (
<Editor
height={300}
width="auto"
defaultLanguage="json"
defaultValue={eventPayload}
onChange={(value: any) => setEventPayload(value || '{}')}
options={{ readOnly: !eventTextEditorEnabled }}
/>
);
return selectingEvent ? (
<Stack orientation="vertical">
<Dropdown
id="process-instance-select-event"
titleText="Event"
label="Select Event"
items={candidateEvents}
itemToString={(item: any) => item.name || item.label || item.typename}
onChange={(value: any) => {
setEventToSend(value.selectedItem);
setEventTextEditorEnabled(
value.selectedItem.typename === 'MessageEventDefinition'
);
}}
/>
{editor}
</Stack>
) : (
taskDataContainer()
);
};

const taskUpdateDisplayArea = () => {
const taskToUse: any = { ...taskToDisplay, data: taskDataToDisplay };
const candidateEvents: any = getEvents(taskToUse);
if (taskToDisplay) {
return (
<Modal
Expand All @@ -571,7 +677,9 @@ export default function ProcessInstanceShow() {
{taskToUse.name} ({taskToUse.type}): {taskToUse.state}
{taskDataButtons(taskToUse)}
</Stack>
{taskDataContainer()}
{selectingEvent
? eventSelector(candidateEvents)
: taskDataContainer()}
</Modal>
);
}
Expand Down Expand Up @@ -655,7 +763,7 @@ export default function ProcessInstanceShow() {
<br />
{getInfoTag(processInstanceToUse)}
<br />
{taskDataDisplayArea()}
{taskUpdateDisplayArea()}
{stepsElement(processInstanceToUse)}
<br />
<ReactDiagramEditor
Expand Down