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:
$ref: "#/components/schemas/OkTrue"
# NOTE: this should probably be /process-instances instead
/tasks/{process_instance_id}/send-user-signal-event:
parameters:
- name: process_instance_id

View File

@ -1,3 +1,5 @@
from __future__ import annotations
import enum
from dataclasses import dataclass
from typing import TYPE_CHECKING
@ -90,6 +92,12 @@ class TaskModel(SpiffworkflowBaseDBModel):
def json_data(self) -> dict:
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:
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:
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())
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_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)
task_model.signal_buttons = TaskService.get_ready_signals_with_button_labels(process_instance_id, task_model.guid)
if "properties" in extensions:
properties = extensions["properties"]

View File

@ -642,7 +642,7 @@ class TaskService:
return extensions
@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(
state="WAITING", process_instance_id=process_instance_id
).all()
@ -660,7 +660,13 @@ class TaskService:
else {}
)
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
@classmethod

View File

@ -4,17 +4,17 @@
<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:sequenceFlow id="Flow_0elszck" sourceRef="StartEvent_1" targetRef="my_manual_task" />
<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_1akz8b3" sourceRef="my_manual_task" 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:manualTask id="my_manual_task" 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>
@ -30,7 +30,7 @@ Congratulations! You have selected the Eat Additional Spam option, which opens
<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:boundaryEvent id="SpamEvent" name="Spam Event" attachedToRef="my_manual_task">
<bpmn:extensionElements>
<spiffworkflow:signalButtonLabel>Eat Spam</spiffworkflow:signalButtonLabel>
</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">
<dc:Bounds x="562" y="282" width="36" height="36" />
</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" />
<bpmndi:BPMNLabel />
</bpmndi:BPMNShape>

View File

@ -170,7 +170,10 @@ class TestTaskService(BaseTest):
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)
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
signal_event = events[0]
assert signal_event["event"]["name"] == "eat_spam"

View File

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