Merge pull request #223 from sartography/feature/scroll_to_top_better_errors

* Scroll to the top on each update of a process result.
This commit is contained in:
Dan Funk 2023-04-23 11:13:47 -04:00 committed by GitHub
commit a4dab51a0e
13 changed files with 66 additions and 31 deletions

View File

@ -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")

View File

@ -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

View File

@ -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

View File

@ -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"

View File

@ -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()

View File

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@ -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 = (
<Notification title={title} onClose={() => removeError()} type="error">

View File

@ -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({
<TextInput
id="process-instance-initiator-search"
placeholder="Enter username"
labelText="Process Initiator"
labelText="Started By"
invalid={processInitiatorNotFoundErrorText !== ''}
invalidText={processInitiatorNotFoundErrorText}
onChange={(event: any) => {

View File

@ -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) {

View File

@ -81,6 +81,7 @@ export interface ProcessInstanceTask {
potential_owner_usernames?: string;
assigned_user_group_identifier?: string;
error_message?: string;
}
export interface ProcessReference {

View File

@ -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<any[]>([]);
@ -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 <img src="/interstitial/waiting.png" alt="Waiting ...." />;
case 'COMPLETED':
return <img src="/interstitial/completed.png" alt="Completed" />;
case 'ERROR':
return <img src="/interstitial/errored.png" alt="Errored" />;
default:
return getStatus();
}
@ -104,6 +112,10 @@ export default function ProcessInterstitial() {
if (shouldRedirect(myTask)) {
return <div>Redirecting you to the next task now ...</div>;
}
if (myTask.error_message) {
return <div>{myTask.error_message}</div>;
}
return (
<div>
<InstructionsForEndUser task={myTask} />
@ -147,7 +159,7 @@ export default function ProcessInterstitial() {
<Column md={2} lg={4} sm={2}>
Task: <em>{d.title}</em>
</Column>
<Column md={6} lg={8} sm={4}>
<Column md={6} lg={6} sm={4}>
{userMessage(d)}
</Column>
</Grid>

View File

@ -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(