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:
parent
15b2947107
commit
3a8cfd2642
|
@ -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
|
||||||
|
|
|
@ -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"]
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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"]
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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);
|
||||||
|
|
Loading…
Reference in New Issue