disable form submit buttons when appropriate, lock process instance when sending events, and ensure return events match ones associated with desired guids w/ burnettk (#359)

Co-authored-by: jasquat <jasquat@users.noreply.github.com>
This commit is contained in:
jasquat 2023-06-29 00:06:47 -04:00 committed by GitHub
parent 15b2947107
commit 3a8cfd2642
8 changed files with 54 additions and 20 deletions

View File

@ -1920,6 +1920,7 @@ paths:
schema: schema:
$ref: "#/components/schemas/OkTrue" $ref: "#/components/schemas/OkTrue"
# NOTE: this should probably be /process-instances instead
/tasks/{process_instance_id}/send-user-signal-event: /tasks/{process_instance_id}/send-user-signal-event:
parameters: parameters:
- name: process_instance_id - name: process_instance_id

View File

@ -1,3 +1,5 @@
from __future__ import annotations
import enum import enum
from dataclasses import dataclass from dataclasses import dataclass
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
@ -90,6 +92,12 @@ class TaskModel(SpiffworkflowBaseDBModel):
def json_data(self) -> dict: def json_data(self) -> dict:
return JsonDataModel.find_data_dict_by_hash(self.json_data_hash) return JsonDataModel.find_data_dict_by_hash(self.json_data_hash)
def parent_task_model(self) -> TaskModel | None:
if "parent" not in self.properties_json:
return None
task_model: TaskModel = self.__class__.query.filter_by(guid=self.properties_json["parent"]).first()
return task_model
class Task: class Task:
HUMAN_TASK_TYPES = ["User Task", "Manual Task"] HUMAN_TASK_TYPES = ["User Task", "Manual Task"]

View File

@ -639,7 +639,16 @@ def send_bpmn_event(
def _send_bpmn_event(process_instance: ProcessInstanceModel, body: dict) -> Response: def _send_bpmn_event(process_instance: ProcessInstanceModel, body: dict) -> Response:
processor = ProcessInstanceProcessor(process_instance) processor = ProcessInstanceProcessor(process_instance)
processor.send_bpmn_event(body) try:
with ProcessInstanceQueueService.dequeued(process_instance):
processor.send_bpmn_event(body)
except (
ProcessInstanceIsNotEnqueuedError,
ProcessInstanceIsAlreadyLockedError,
) as e:
ErrorHandlingService.handle_error(process_instance, e)
raise e
task = ProcessInstanceService.spiff_task_to_api_task(processor, processor.next_task()) task = ProcessInstanceService.spiff_task_to_api_task(processor, processor.next_task())
return make_response(jsonify(task), 200) return make_response(jsonify(task), 200)

View File

@ -268,7 +268,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_model = _get_task_model_from_guid_or_raise(task_guid, process_instance_id)
task_definition = task_model.task_definition task_definition = task_model.task_definition
extensions = TaskService.get_extensions_from_task_model(task_model) extensions = TaskService.get_extensions_from_task_model(task_model)
task_model.signal_buttons = TaskService.get_ready_signals_with_button_labels(process_instance_id) task_model.signal_buttons = TaskService.get_ready_signals_with_button_labels(process_instance_id, task_model.guid)
if "properties" in extensions: if "properties" in extensions:
properties = extensions["properties"] properties = extensions["properties"]

View File

@ -642,7 +642,7 @@ class TaskService:
return extensions return extensions
@classmethod @classmethod
def get_ready_signals_with_button_labels(cls, process_instance_id: int) -> list[dict]: def get_ready_signals_with_button_labels(cls, process_instance_id: int, associated_task_guid: str) -> list[dict]:
waiting_tasks: list[TaskModel] = TaskModel.query.filter_by( waiting_tasks: list[TaskModel] = TaskModel.query.filter_by(
state="WAITING", process_instance_id=process_instance_id state="WAITING", process_instance_id=process_instance_id
).all() ).all()
@ -660,7 +660,13 @@ class TaskService:
else {} else {}
) )
if "signalButtonLabel" in extensions and "name" in event_definition: if "signalButtonLabel" in extensions and "name" in event_definition:
result.append({"event": event_definition, "label": extensions["signalButtonLabel"]}) parent_task_model = task_model.parent_task_model()
if (
parent_task_model
and "children" in parent_task_model.properties_json
and associated_task_guid in parent_task_model.properties_json["children"]
):
result.append({"event": event_definition, "label": extensions["signalButtonLabel"]})
return result return result
@classmethod @classmethod

View File

@ -4,17 +4,17 @@
<bpmn:startEvent id="StartEvent_1"> <bpmn:startEvent id="StartEvent_1">
<bpmn:outgoing>Flow_0elszck</bpmn:outgoing> <bpmn:outgoing>Flow_0elszck</bpmn:outgoing>
</bpmn:startEvent> </bpmn:startEvent>
<bpmn:sequenceFlow id="Flow_0elszck" sourceRef="StartEvent_1" targetRef="Activity_0cmmlen" /> <bpmn:sequenceFlow id="Flow_0elszck" sourceRef="StartEvent_1" targetRef="my_manual_task" />
<bpmn:endEvent id="Event_1mjvim4"> <bpmn:endEvent id="Event_1mjvim4">
<bpmn:incoming>Flow_1akz8b3</bpmn:incoming> <bpmn:incoming>Flow_1akz8b3</bpmn:incoming>
</bpmn:endEvent> </bpmn:endEvent>
<bpmn:sequenceFlow id="Flow_1akz8b3" sourceRef="Activity_0cmmlen" targetRef="Event_1mjvim4" /> <bpmn:sequenceFlow id="Flow_1akz8b3" sourceRef="my_manual_task" targetRef="Event_1mjvim4" />
<bpmn:sequenceFlow id="Flow_0uenxs3" sourceRef="SpamEvent" targetRef="Activity_1u4om4i" /> <bpmn:sequenceFlow id="Flow_0uenxs3" sourceRef="SpamEvent" targetRef="Activity_1u4om4i" />
<bpmn:endEvent id="Event_1dvll15"> <bpmn:endEvent id="Event_1dvll15">
<bpmn:incoming>Flow_16bzuvz</bpmn:incoming> <bpmn:incoming>Flow_16bzuvz</bpmn:incoming>
</bpmn:endEvent> </bpmn:endEvent>
<bpmn:sequenceFlow id="Flow_16bzuvz" sourceRef="Activity_1u4om4i" targetRef="Event_1dvll15" /> <bpmn:sequenceFlow id="Flow_16bzuvz" sourceRef="Activity_1u4om4i" targetRef="Event_1dvll15" />
<bpmn:manualTask id="Activity_0cmmlen" name="My Manual Task"> <bpmn:manualTask id="my_manual_task" name="My Manual Task">
<bpmn:extensionElements> <bpmn:extensionElements>
<spiffworkflow:instructionsForEndUser># Welcome <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> 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>
@ -30,7 +30,7 @@ Congratulations! You have selected the Eat Additional Spam option, which opens
<bpmn:incoming>Flow_0uenxs3</bpmn:incoming> <bpmn:incoming>Flow_0uenxs3</bpmn:incoming>
<bpmn:outgoing>Flow_16bzuvz</bpmn:outgoing> <bpmn:outgoing>Flow_16bzuvz</bpmn:outgoing>
</bpmn:manualTask> </bpmn:manualTask>
<bpmn:boundaryEvent id="SpamEvent" name="Spam Event" attachedToRef="Activity_0cmmlen"> <bpmn:boundaryEvent id="SpamEvent" name="Spam Event" attachedToRef="my_manual_task">
<bpmn:extensionElements> <bpmn:extensionElements>
<spiffworkflow:signalButtonLabel>Eat Spam</spiffworkflow:signalButtonLabel> <spiffworkflow:signalButtonLabel>Eat Spam</spiffworkflow:signalButtonLabel>
</bpmn:extensionElements> </bpmn:extensionElements>
@ -50,7 +50,7 @@ Congratulations! You have selected the Eat Additional Spam option, which opens
<bpmndi:BPMNShape id="Event_1dvll15_di" bpmnElement="Event_1dvll15"> <bpmndi:BPMNShape id="Event_1dvll15_di" bpmnElement="Event_1dvll15">
<dc:Bounds x="562" y="282" width="36" height="36" /> <dc:Bounds x="562" y="282" width="36" height="36" />
</bpmndi:BPMNShape> </bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_0zxmtux_di" bpmnElement="Activity_0cmmlen"> <bpmndi:BPMNShape id="Activity_0zxmtux_di" bpmnElement="my_manual_task">
<dc:Bounds x="270" y="137" width="100" height="80" /> <dc:Bounds x="270" y="137" width="100" height="80" />
<bpmndi:BPMNLabel /> <bpmndi:BPMNLabel />
</bpmndi:BPMNShape> </bpmndi:BPMNShape>

View File

@ -170,7 +170,10 @@ class TestTaskService(BaseTest):
process_instance = self.create_process_instance_from_process_model(process_model) process_instance = self.create_process_instance_from_process_model(process_model)
processor = ProcessInstanceProcessor(process_instance) processor = ProcessInstanceProcessor(process_instance)
processor.do_engine_steps(save=True, execution_strategy_name="greedy") processor.do_engine_steps(save=True, execution_strategy_name="greedy")
events = TaskService.get_ready_signals_with_button_labels(process_instance.id) spiff_task = processor.__class__.get_task_by_bpmn_identifier("my_manual_task", processor.bpmn_process_instance)
assert spiff_task is not None
events = TaskService.get_ready_signals_with_button_labels(process_instance.id, str(spiff_task.id))
assert len(events) == 1 assert len(events) == 1
signal_event = events[0] signal_event = events[0]
assert signal_event["event"]["name"] == "eat_spam" assert signal_event["event"]["name"] == "eat_spam"

View File

@ -31,7 +31,7 @@ export default function TaskShow() {
const [userTasks] = useState(null); const [userTasks] = useState(null);
const params = useParams(); const params = useParams();
const navigate = useNavigate(); const navigate = useNavigate();
const [disabled, setDisabled] = useState(false); const [formButtonsDisabled, setFormButtonsDisabled] = useState(false);
const [taskData, setTaskData] = useState<any>(null); const [taskData, setTaskData] = useState<any>(null);
const [autosaveOnFormChanges, setAutosaveOnFormChanges] = const [autosaveOnFormChanges, setAutosaveOnFormChanges] =
@ -56,7 +56,7 @@ export default function TaskShow() {
// convert null back to undefined so rjsf doesn't attempt to incorrectly validate them // convert null back to undefined so rjsf doesn't attempt to incorrectly validate them
const taskDataToUse = result.saved_form_data || result.data; const taskDataToUse = result.saved_form_data || result.data;
setTaskData(recursivelyChangeNullAndUndefined(taskDataToUse, undefined)); setTaskData(recursivelyChangeNullAndUndefined(taskDataToUse, undefined));
setDisabled(false); setFormButtonsDisabled(false);
if (!result.can_complete) { if (!result.can_complete) {
navigateToInterstitial(result); navigateToInterstitial(result);
} }
@ -119,6 +119,7 @@ export default function TaskShow() {
navigateToInterstitial(result); navigateToInterstitial(result);
} }
} else { } else {
setFormButtonsDisabled(false);
addError(result); addError(result);
} }
}; };
@ -136,7 +137,7 @@ export default function TaskShow() {
}; };
const handleFormSubmit = (formObject: any, _event: any) => { const handleFormSubmit = (formObject: any, _event: any) => {
if (disabled) { if (formButtonsDisabled) {
return; return;
} }
@ -148,7 +149,7 @@ export default function TaskShow() {
} }
const queryParams = ''; const queryParams = '';
setDisabled(true); setFormButtonsDisabled(true);
removeError(); removeError();
delete dataToSubmit.isManualTask; delete dataToSubmit.isManualTask;
@ -168,9 +169,10 @@ export default function TaskShow() {
}; };
const handleSignalSubmit = (event: EventDefinition) => { const handleSignalSubmit = (event: EventDefinition) => {
if (disabled || !task) { if (formButtonsDisabled || !task) {
return; return;
} }
setFormButtonsDisabled(true);
HttpService.makeCallToBackend({ HttpService.makeCallToBackend({
path: `/tasks/${params.process_instance_id}/send-user-signal-event`, path: `/tasks/${params.process_instance_id}/send-user-signal-event`,
successCallback: processSubmitResult, successCallback: processSubmitResult,
@ -206,7 +208,7 @@ export default function TaskShow() {
); );
} }
if (userTask.state === 'FUTURE') { if (userTask.state === 'FUTURE') {
return <Tab disabled>{userTask.name_for_display}</Tab>; return <Tab formButtonsDisabled>{userTask.name_for_display}</Tab>;
} }
if (userTask.state === 'READY') { if (userTask.state === 'READY') {
return ( return (
@ -359,6 +361,7 @@ export default function TaskShow() {
const handleCloseButton = () => { const handleCloseButton = () => {
setAutosaveOnFormChanges(false); setAutosaveOnFormChanges(false);
setFormButtonsDisabled(true);
const successCallback = () => navigate(`/tasks`); const successCallback = () => navigate(`/tasks`);
sendAutosaveEvent({ successCallback }); sendAutosaveEvent({ successCallback });
}; };
@ -413,7 +416,7 @@ export default function TaskShow() {
<Button <Button
id="close-button" id="close-button"
onClick={handleCloseButton} onClick={handleCloseButton}
disabled={disabled} disabled={formButtonsDisabled}
kind="secondary" kind="secondary"
title="Save data as draft and close the form." title="Save data as draft and close the form."
> >
@ -423,7 +426,11 @@ export default function TaskShow() {
} }
reactFragmentToHideSubmitButton = ( reactFragmentToHideSubmitButton = (
<ButtonSet> <ButtonSet>
<Button type="submit" id="submit-button" disabled={disabled}> <Button
type="submit"
id="submit-button"
disabled={formButtonsDisabled}
>
{submitButtonText} {submitButtonText}
</Button> </Button>
{closeButton} {closeButton}
@ -431,7 +438,7 @@ export default function TaskShow() {
{task.signal_buttons.map((signal) => ( {task.signal_buttons.map((signal) => (
<Button <Button
name="signal.signal" name="signal.signal"
disabled={disabled} disabled={formButtonsDisabled}
onClick={() => handleSignalSubmit(signal.event)} onClick={() => handleSignalSubmit(signal.event)}
> >
{signal.label} {signal.label}
@ -456,7 +463,7 @@ export default function TaskShow() {
<Column sm={4} md={5} lg={8}> <Column sm={4} md={5} lg={8}>
<Form <Form
id="form-to-submit" id="form-to-submit"
disabled={disabled} disabled={formButtonsDisabled}
formData={taskData} formData={taskData}
onChange={(obj: any) => { onChange={(obj: any) => {
setTaskData(obj.formData); setTaskData(obj.formData);