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() { {closeButton} @@ -431,7 +438,7 @@ export default function TaskShow() { {task.signal_buttons.map((signal) => (