diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/api.yml b/spiffworkflow-backend/src/spiffworkflow_backend/api.yml
index b56b1c72..585c88b6 100755
--- a/spiffworkflow-backend/src/spiffworkflow_backend/api.yml
+++ b/spiffworkflow-backend/src/spiffworkflow_backend/api.yml
@@ -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
diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/models/task.py b/spiffworkflow-backend/src/spiffworkflow_backend/models/task.py
index 2cf6d80e..7a11b709 100644
--- a/spiffworkflow-backend/src/spiffworkflow_backend/models/task.py
+++ b/spiffworkflow-backend/src/spiffworkflow_backend/models/task.py
@@ -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"]
diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_instances_controller.py b/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_instances_controller.py
index a17b1d34..4869266f 100644
--- a/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_instances_controller.py
+++ b/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_instances_controller.py
@@ -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)
diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/routes/tasks_controller.py b/spiffworkflow-backend/src/spiffworkflow_backend/routes/tasks_controller.py
index 4635e1a7..aac9b022 100644
--- a/spiffworkflow-backend/src/spiffworkflow_backend/routes/tasks_controller.py
+++ b/spiffworkflow-backend/src/spiffworkflow_backend/routes/tasks_controller.py
@@ -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"]
diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/services/task_service.py b/spiffworkflow-backend/src/spiffworkflow_backend/services/task_service.py
index 82f90913..2ac143b0 100644
--- a/spiffworkflow-backend/src/spiffworkflow_backend/services/task_service.py
+++ b/spiffworkflow-backend/src/spiffworkflow_backend/services/task_service.py
@@ -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
diff --git a/spiffworkflow-backend/tests/data/signal_event_extensions/signal_event_extensions.bpmn b/spiffworkflow-backend/tests/data/signal_event_extensions/signal_event_extensions.bpmn
index 0e580661..d65ef0e7 100644
--- a/spiffworkflow-backend/tests/data/signal_event_extensions/signal_event_extensions.bpmn
+++ b/spiffworkflow-backend/tests/data/signal_event_extensions/signal_event_extensions.bpmn
@@ -4,17 +4,17 @@
Flow_0elszck
-
+
Flow_1akz8b3
-
+
Flow_16bzuvz
-
+
# 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.
@@ -30,7 +30,7 @@ Congratulations! You have selected the Eat Additional Spam option, which opens
Flow_0uenxs3
Flow_16bzuvz
-
+
Eat Spam
@@ -50,7 +50,7 @@ Congratulations! You have selected the Eat Additional Spam option, which opens
-
+
diff --git a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_task_service.py b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_task_service.py
index 2a409fd3..231e873c 100644
--- a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_task_service.py
+++ b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_task_service.py
@@ -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"
diff --git a/spiffworkflow-frontend/src/routes/TaskShow.tsx b/spiffworkflow-frontend/src/routes/TaskShow.tsx
index 8ea17682..b7b13aba 100644
--- a/spiffworkflow-frontend/src/routes/TaskShow.tsx
+++ b/spiffworkflow-frontend/src/routes/TaskShow.tsx
@@ -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(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 {userTask.name_for_display};
+ return {userTask.name_for_display};
}
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() {