diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/config/__init__.py b/spiffworkflow-backend/src/spiffworkflow_backend/config/__init__.py index 7711c36f9..eaf67f6c9 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/config/__init__.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/config/__init__.py @@ -18,13 +18,13 @@ def setup_database_uri(app: Flask) -> None: if app.config.get("SPIFFWORKFLOW_BACKEND_DATABASE_URI") is None: database_name = f"spiffworkflow_backend_{app.config['ENV_IDENTIFIER']}" if app.config.get("SPIFFWORKFLOW_BACKEND_DATABASE_TYPE") == "sqlite": - app.config["SQLALCHEMY_DATABASE_URI"] = ( - f"sqlite:///{app.instance_path}/db_{app.config['ENV_IDENTIFIER']}.sqlite3" - ) + app.config[ + "SQLALCHEMY_DATABASE_URI" + ] = f"sqlite:///{app.instance_path}/db_{app.config['ENV_IDENTIFIER']}.sqlite3" elif app.config.get("SPIFFWORKFLOW_BACKEND_DATABASE_TYPE") == "postgres": - app.config["SQLALCHEMY_DATABASE_URI"] = ( - f"postgresql://spiffworkflow_backend:spiffworkflow_backend@localhost:5432/{database_name}" - ) + app.config[ + "SQLALCHEMY_DATABASE_URI" + ] = f"postgresql://spiffworkflow_backend:spiffworkflow_backend@localhost:5432/{database_name}" else: # use pswd to trick flake8 with hardcoded passwords db_pswd = app.config.get("SPIFFWORKFLOW_BACKEND_DATABASE_PASSWORD") diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/models/process_instance.py b/spiffworkflow-backend/src/spiffworkflow_backend/models/process_instance.py index 009a7486e..44fe82764 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/models/process_instance.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/models/process_instance.py @@ -127,9 +127,9 @@ class ProcessInstanceModel(SpiffworkflowBaseDBModel): def serialized_with_metadata(self) -> dict[str, Any]: process_instance_attributes = self.serialized process_instance_attributes["process_metadata"] = self.process_metadata - process_instance_attributes["process_model_with_diagram_identifier"] = ( - self.process_model_with_diagram_identifier - ) + process_instance_attributes[ + "process_model_with_diagram_identifier" + ] = self.process_model_with_diagram_identifier return process_instance_attributes @property diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/models/task.py b/spiffworkflow-backend/src/spiffworkflow_backend/models/task.py index c9bf311b4..175730591 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/models/task.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/models/task.py @@ -110,6 +110,7 @@ class Task: event_definition: Union[dict[str, Any], None] = None, call_activity_process_identifier: Optional[str] = None, calling_subprocess_task_id: Optional[str] = None, + error_message: Optional[str] = None, ): """__init__.""" self.id = id @@ -147,6 +148,7 @@ class Task: self.properties = properties # Arbitrary extension properties from BPMN editor. if self.properties is None: self.properties = {} + self.error_message = error_message @property def serialized(self) -> dict[str, Any]: @@ -183,6 +185,7 @@ class Task: "event_definition": self.event_definition, "call_activity_process_identifier": self.call_activity_process_identifier, "calling_subprocess_task_id": self.calling_subprocess_task_id, + "error_message": self.error_message, } @classmethod diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/routes/tasks_controller.py b/spiffworkflow-backend/src/spiffworkflow_backend/routes/tasks_controller.py index 40c688855..498603947 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/routes/tasks_controller.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/routes/tasks_controller.py @@ -405,14 +405,21 @@ def _interstitial_stream(process_instance_id: int) -> Generator[str, Optional[st reported_ids.append(spiff_task.id) yield f"data: {current_app.json.dumps(task)} \n\n" last_task = spiff_task - processor.do_engine_steps(execution_strategy_name="run_until_user_message") - processor.do_engine_steps(execution_strategy_name="one_at_a_time") - spiff_task = processor.next_task() + try: + processor.do_engine_steps(execution_strategy_name="run_until_user_message") + processor.do_engine_steps(execution_strategy_name="one_at_a_time") + except WorkflowTaskException as wfe: + api_error = ApiError.from_workflow_exception( + "engine_steps_error", "Failed complete an automated task.", exp=wfe + ) + yield f"data: {current_app.json.dumps(api_error)} \n\n" # Note, this has to be done in case someone leaves the page, # which can otherwise cancel this function and leave completed tasks un-registered. processor.save() # Fixme - maybe find a way not to do this on every loop? + spiff_task = processor.next_task() + + # Always provide some response, in the event no instructions were provided. if len(reported_ids) == 0: - # Always provide some response, in the event no instructions were provided. task = ProcessInstanceService.spiff_task_to_api_task(processor, processor.next_task()) yield f"data: {current_app.json.dumps(task)} \n\n" diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_processor.py b/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_processor.py index 224e20fec..352bd4ac5 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_processor.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_processor.py @@ -423,9 +423,9 @@ class ProcessInstanceProcessor: tld.process_instance_id = process_instance_model.id # we want this to be the fully qualified path to the process model including all group subcomponents - current_app.config["THREAD_LOCAL_DATA"].process_model_identifier = ( - f"{process_instance_model.process_model_identifier}" - ) + current_app.config[ + "THREAD_LOCAL_DATA" + ].process_model_identifier = f"{process_instance_model.process_model_identifier}" self.process_instance_model = process_instance_model self.process_model_service = ProcessModelService() @@ -585,9 +585,9 @@ class ProcessInstanceProcessor: bpmn_subprocess_definition.bpmn_identifier ] = bpmn_process_definition_dict spiff_bpmn_process_dict["subprocess_specs"][bpmn_subprocess_definition.bpmn_identifier]["task_specs"] = {} - bpmn_subprocess_definition_bpmn_identifiers[bpmn_subprocess_definition.id] = ( - bpmn_subprocess_definition.bpmn_identifier - ) + bpmn_subprocess_definition_bpmn_identifiers[ + bpmn_subprocess_definition.id + ] = bpmn_subprocess_definition.bpmn_identifier task_definitions = TaskDefinitionModel.query.filter( TaskDefinitionModel.bpmn_process_definition_id.in_( # type: ignore @@ -1741,8 +1741,8 @@ class ProcessInstanceProcessor: def next_task(self) -> SpiffTask: """Returns the next task that should be completed even if there are parallel tasks and multiple options are available. - If the process_instance is complete - it will return the final end task. + If the process_instance is complete it will return the final end task. + If the process_instance is in an error state it will return the task that is erroring. """ # If the whole blessed mess is done, return the end_event task in the tree # This was failing in the case of a call activity where we have an intermediate EndEvent @@ -1769,8 +1769,12 @@ class ProcessInstanceProcessor: waiting_tasks = self.bpmn_process_instance.get_tasks(TaskState.WAITING) if len(waiting_tasks) > 0: return waiting_tasks[0] - else: - return # We have not tasks to return. + + # If there are no ready tasks, and not waiting tasks, return the latest error. + error_task = None + for task in SpiffTask.Iterator(self.bpmn_process_instance.task_tree, TaskState.ERROR): + error_task = task + return error_task # Get a list of all completed user tasks (Non engine tasks) completed_user_tasks = self.completed_user_tasks() diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_service.py b/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_service.py index db3e62e1d..de4097835 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_service.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_service.py @@ -462,6 +462,12 @@ class ProcessInstanceService: serialized_task_spec = processor.serialize_task_spec(spiff_task.task_spec) + # Grab the last error message. + error_message = None + for event in processor.process_instance_model.process_instance_events: + for detail in event.error_details: + error_message = detail.message + task = Task( spiff_task.id, spiff_task.task_spec.name, @@ -479,6 +485,7 @@ class ProcessInstanceService: event_definition=serialized_task_spec.get("event_definition"), call_activity_process_identifier=call_activity_process_identifier, calling_subprocess_task_id=calling_subprocess_task_id, + error_message=error_message, ) return task diff --git a/spiffworkflow-frontend/public/interstitial/errored.png b/spiffworkflow-frontend/public/interstitial/errored.png new file mode 100644 index 000000000..c931b7b1a Binary files /dev/null and b/spiffworkflow-frontend/public/interstitial/errored.png differ diff --git a/spiffworkflow-frontend/src/components/ErrorDisplay.tsx b/spiffworkflow-frontend/src/components/ErrorDisplay.tsx index 67e6e18dc..401169733 100644 --- a/spiffworkflow-frontend/src/components/ErrorDisplay.tsx +++ b/spiffworkflow-frontend/src/components/ErrorDisplay.tsx @@ -109,6 +109,7 @@ export default function ErrorDisplay() { if (errorObject) { const title = 'Error:'; + window.scrollTo(0, 0); // Scroll back to the top of the page errorTag = ( removeError()} type="error"> diff --git a/spiffworkflow-frontend/src/components/ProcessInstanceListTable.tsx b/spiffworkflow-frontend/src/components/ProcessInstanceListTable.tsx index 6a1562af6..d214c6222 100644 --- a/spiffworkflow-frontend/src/components/ProcessInstanceListTable.tsx +++ b/spiffworkflow-frontend/src/components/ProcessInstanceListTable.tsx @@ -1190,7 +1190,7 @@ export default function ProcessInstanceListTable({ return null; }} placeholder="Start typing username" - titleText="Process Initiator" + titleText="Started By" selectedItem={processInitiatorSelection} /> ); @@ -1199,7 +1199,7 @@ export default function ProcessInstanceListTable({ { diff --git a/spiffworkflow-frontend/src/components/ProcessModelSearch.tsx b/spiffworkflow-frontend/src/components/ProcessModelSearch.tsx index 3a6331a3d..8f1356523 100644 --- a/spiffworkflow-frontend/src/components/ProcessModelSearch.tsx +++ b/spiffworkflow-frontend/src/components/ProcessModelSearch.tsx @@ -15,7 +15,7 @@ export default function ProcessModelSearch({ processModels, selectedItem, onChange, - titleText = 'Process model', + titleText = 'Process', }: OwnProps) { const getParentGroupsDisplayName = (processModel: ProcessModel) => { if (processModel.parent_groups) { diff --git a/spiffworkflow-frontend/src/interfaces.ts b/spiffworkflow-frontend/src/interfaces.ts index e482bede7..0188e5ea9 100644 --- a/spiffworkflow-frontend/src/interfaces.ts +++ b/spiffworkflow-frontend/src/interfaces.ts @@ -81,6 +81,7 @@ export interface ProcessInstanceTask { potential_owner_usernames?: string; assigned_user_group_identifier?: string; + error_message?: string; } export interface ProcessReference { diff --git a/spiffworkflow-frontend/src/routes/ProcessInterstitial.tsx b/spiffworkflow-frontend/src/routes/ProcessInterstitial.tsx index bdd566992..59e7e8aee 100644 --- a/spiffworkflow-frontend/src/routes/ProcessInterstitial.tsx +++ b/spiffworkflow-frontend/src/routes/ProcessInterstitial.tsx @@ -10,6 +10,7 @@ import { getBasicHeaders } from '../services/HttpService'; import InstructionsForEndUser from '../components/InstructionsForEndUser'; import ProcessBreadcrumb from '../components/ProcessBreadcrumb'; import { ProcessInstanceTask } from '../interfaces'; +import useAPIError from '../hooks/UseApiError'; export default function ProcessInterstitial() { const [data, setData] = useState([]); @@ -20,6 +21,7 @@ export default function ProcessInterstitial() { const userTasks = useMemo(() => { return ['User Task', 'Manual Task']; }, []); + const { addError } = useAPIError(); useEffect(() => { fetchEventSource( @@ -27,9 +29,13 @@ export default function ProcessInterstitial() { { headers: getBasicHeaders(), onmessage(ev) { - const task = JSON.parse(ev.data); - setData((prevData) => [...prevData, task]); - setLastTask(task); + const retValue = JSON.parse(ev.data); + if ('error_code' in retValue) { + addError(retValue); + } else { + setData((prevData) => [...prevData, retValue]); + setLastTask(retValue); + } }, onclose() { setState('CLOSED'); @@ -85,6 +91,8 @@ export default function ProcessInterstitial() { return Waiting ....; case 'COMPLETED': return Completed; + case 'ERROR': + return Errored; default: return getStatus(); } @@ -104,6 +112,10 @@ export default function ProcessInterstitial() { if (shouldRedirect(myTask)) { return
Redirecting you to the next task now ...
; } + if (myTask.error_message) { + return
{myTask.error_message}
; + } + return (
@@ -147,7 +159,7 @@ export default function ProcessInterstitial() { Task: {d.title} - + {userMessage(d)} diff --git a/spiffworkflow-frontend/src/routes/TaskShow.tsx b/spiffworkflow-frontend/src/routes/TaskShow.tsx index 56ea9bc22..b8d3d10f1 100644 --- a/spiffworkflow-frontend/src/routes/TaskShow.tsx +++ b/spiffworkflow-frontend/src/routes/TaskShow.tsx @@ -117,10 +117,10 @@ export default function TaskShow() { const processResult = (result: ProcessInstanceTask) => { setTask(result); setDisabled(false); - if (!result.can_complete) { navigateToInterstitial(result); } + window.scrollTo(0, 0); // Scroll back to the top of the page /* Disable call to load previous tasks -- do not display menu. const url = `/v1.0/process-instances/for-me/${modifyProcessIdentifierForPathParam(