Bug/various bugs (#494)

* Don't bug out in older instances that don't have runtime_info

* Add zoom buttons to React Diagram Editor

* This removes some potential features for on-boarding, that we are not currently using, but fixes the issue with 100's of onboarding processes piling up and sitting around.  Hoepfully we can wrap this into the extensions mechanism so everything works the same way eventually.

* Improved error messages on form builder
Don't try to auto-save the file before it is fully loaded.
Example data was not getting saved on update.

* Found several errors with new zooming buttons in DMN, so cleaning that up.

Recent changes prevented creating a new dmn table.

* Errors were not being displyed for the Editor Routes

* Going to disable handling user tasks in the onboarding controller for now.
This commit is contained in:
Dan Funk 2023-09-18 11:22:29 -04:00 committed by GitHub
parent 3fdb1fba60
commit 161493428a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 144 additions and 50 deletions

View File

@ -1,44 +1,47 @@
"""APIs for dealing with process groups, process models, and process instances."""
from flask import g
from flask import make_response
from flask.wrappers import Response
from SpiffWorkflow.exceptions import WorkflowException # type: ignore
from spiffworkflow_backend import db
from spiffworkflow_backend.exceptions.api_error import ApiError
from spiffworkflow_backend.routes.process_instances_controller import _process_instance_start
from spiffworkflow_backend.exceptions.process_entity_not_found_error import ProcessEntityNotFoundError
from spiffworkflow_backend.models.process_instance import ProcessInstanceModel
from spiffworkflow_backend.models.process_instance import ProcessInstanceStatus
from spiffworkflow_backend.services.jinja_service import JinjaService
from spiffworkflow_backend.services.process_instance_processor import ProcessInstanceProcessor
def get_onboarding() -> Response:
result: dict = {}
persistence_level = "none" # Going to default this to none for now as we aren't using it interactively and its
# creating a lot of extra data in the database and UI. We can revisit this later if we need to.
# This is a short term fix that removes some of the potential benefits - such as routing users through an actual
# workflow, asking questions, and saving information about them.
# Hope to replace this with Extensions in the future.
try:
process_instance, processor = _process_instance_start("site-administration/onboarding")
except ApiError:
process_instance = ProcessInstanceModel(
status=ProcessInstanceStatus.not_started.value,
process_initiator_id=g.user.id,
process_model_identifier="site-administration/onboarding",
process_model_display_name="On Boarding",
persistence_level=persistence_level,
)
processor = ProcessInstanceProcessor(process_instance)
except ProcessEntityNotFoundError:
# The process doesn't exist, so bail out without an error
return make_response(result, 200)
try:
processor.do_engine_steps(save=True, execution_strategy_name="greedy") # type: ignore
if processor is not None:
bpmn_process = processor.bpmn_process_instance
if bpmn_process.is_completed():
workflow_data = bpmn_process.data
result = workflow_data.get("onboarding", {})
# Delete the process instance, we don't need to keep this around if no users tasks were created.
db.session.delete(process_instance)
db.session.flush() # Clear it out BEFORE returning.
elif len(bpmn_process.get_ready_user_tasks()) > 0:
process_instance.persistence_level = "full"
processor.save()
result = {
"type": "user_input_required",
"process_instance_id": process_instance.id,
}
task = processor.next_task()
if task:
result["task_id"] = task.id
result["instructions"] = JinjaService.render_instructions_for_end_user(task)
processor.do_engine_steps(save=False, execution_strategy_name="greedy")
bpmn_process = processor.bpmn_process_instance
if bpmn_process.is_completed():
workflow_data = bpmn_process.data
result = workflow_data.get("onboarding", {})
task = processor.next_task()
if task:
result["task_id"] = task.id
result["instructions"] = JinjaService.render_instructions_for_end_user(task)
except WorkflowException as e:
raise ApiError.from_workflow_exception("onboard_failed", "Error building onboarding message", e) from e
except Exception as e:

View File

@ -58,13 +58,16 @@ class TestOnboarding(BaseTest):
# Assure no residual process model is left behind if it executes and completes without additinal user tasks
assert len(ProcessInstanceModel.query.all()) == 0
def test_persists_if_user_task_encountered(
def skip_test_persists_if_user_task_encountered(
self,
app: Flask,
client: FlaskClient,
with_db_and_bpmn_file_cleanup: None,
with_super_admin_user: UserModel,
) -> None:
"""We are moving towards replacing the onboarding with Extensions
so disabling this test, and the ability to start a person off on
a workflow instantly on arrival."""
self.set_up_onboarding(client, with_super_admin_user, "onboarding_with_user_task")
results = client.get(
"/v1.0/onboarding",

View File

@ -54,6 +54,7 @@ import TouchModule from 'diagram-js/lib/navigation/touch';
import { useNavigate } from 'react-router-dom';
import { Can } from '@casl/react';
import { ZoomIn, ZoomOut, ZoomFit } from '@carbon/icons-react';
import HttpService from '../services/HttpService';
import ButtonWithConfirmation from './ButtonWithConfirmation';
@ -189,6 +190,7 @@ export default function ReactDiagramEditor({
spiffworkflow,
BpmnPropertiesPanelModule,
BpmnPropertiesProviderModule,
ZoomScrollModule,
],
moddleExtensions: {
spiffworkflow: spiffModdleExtension,
@ -207,6 +209,7 @@ export default function ReactDiagramEditor({
additionalModules: [
DmnPropertiesPanelModule,
DmnPropertiesProviderModule,
ZoomScrollModule,
],
},
});
@ -724,10 +727,67 @@ export default function ReactDiagramEditor({
return null;
};
const zoom = (amount: number) => {
if (diagramModelerState) {
let modeler = diagramModelerState as any;
if (diagramType === 'dmn') {
modeler = (diagramModelerState as any).getActiveViewer();
}
try {
if (amount === 0) {
const canvas = (modeler as any).get('canvas');
canvas.zoom(FitViewport, 'auto');
} else {
modeler.get('zoomScroll').stepZoom(amount);
}
} catch (e) {
console.log(
'zoom failed, certain modes in DMN do not support zooming.',
e
);
}
}
};
const diagramControlButtons = () => {
return (
<div className="diagram-control-buttons">
<Button
kind="ghost"
renderIcon={ZoomIn}
iconDescription="Zoom In"
hasIconOnly
onClick={() => {
zoom(1);
}}
/>
<Button
kind="ghost"
renderIcon={ZoomOut}
iconDescription="Zoom Out"
hasIconOnly
onClick={() => {
zoom(-1);
}}
/>
<Button
kind="ghost"
renderIcon={ZoomFit}
iconDescription="Zoom Fit"
hasIconOnly
onClick={() => {
zoom(0);
}}
/>
</div>
);
};
return (
<>
{userActionOptions()}
{showReferences()}
{diagramControlButtons()}
</>
);
}

View File

@ -90,7 +90,9 @@ export default function ReactFormBuilder({
HttpService.makeCallToBackend({
path: url,
successCallback: () => {},
failureCallback: () => {}, // fixme: handle errors
failureCallback: (e: any) => {
setErrorMessage(`Failed to save file: '${fileName}'. ${e.message}`);
},
httpMethod,
postBody: submission,
});
@ -112,7 +114,11 @@ export default function ReactFormBuilder({
if (ready) {
return true;
}
if (strSchema !== '' && strUI !== '' && strFormData !== '') {
if (
debouncedStrSchema !== '' &&
debouncedStrUI !== '' &&
debouncedFormData !== ''
) {
setReady(true);
return true;
}
@ -121,24 +127,24 @@ export default function ReactFormBuilder({
// Auto save schema changes
useEffect(() => {
if (baseFileName !== '') {
if (baseFileName !== '' && ready) {
saveFile(new File([debouncedStrSchema], baseFileName + SCHEMA_EXTENSION));
}
}, [debouncedStrSchema, baseFileName, saveFile]);
}, [debouncedStrSchema, baseFileName, saveFile, ready]);
// Auto save ui changes
useEffect(() => {
if (baseFileName !== '') {
if (baseFileName !== '' && ready) {
saveFile(new File([debouncedStrUI], baseFileName + UI_EXTENSION));
}
}, [debouncedStrUI, baseFileName, saveFile]);
}, [debouncedStrUI, baseFileName, saveFile, ready]);
// Auto save example data changes
useEffect(() => {
if (baseFileName !== '') {
saveFile(new File([debouncedFormData], baseFileName + DATA_EXTENSION));
}
}, [debouncedFormData, baseFileName, saveFile]);
}, [debouncedFormData, baseFileName, saveFile, ready]);
useEffect(() => {
/**
@ -173,7 +179,7 @@ export default function ReactFormBuilder({
try {
data = JSON.parse(debouncedFormData);
} catch (e) {
setErrorMessage('Please check the Task Data for errors.');
setErrorMessage('Please check the Data View for errors.');
return;
}
setErrorMessage('');
@ -224,6 +230,7 @@ export default function ReactFormBuilder({
// @ts-ignore
value !== dataEditorRef.current.getValue()
) {
setStrFormData(value);
// @ts-ignore
dataEditorRef.current.setValue(value);
}
@ -289,7 +296,9 @@ export default function ReactFormBuilder({
fileName
)}${UI_EXTENSION}`,
successCallback: setJsonUiFromResponseJson,
failureCallback: () => {},
failureCallback: () => {
setJsonUiFromResponseJson({ file_contents: '{}' });
},
});
}
@ -299,7 +308,9 @@ export default function ReactFormBuilder({
fileName
)}${DATA_EXTENSION}`,
successCallback: setDataFromResponseJson,
failureCallback: () => {},
failureCallback: () => {
setDataFromResponseJson({ file_contents: '{}' });
},
});
}
@ -449,7 +460,7 @@ export default function ReactFormBuilder({
</Column>
<Column sm={4} md={5} lg={8}>
<h2>Form Preview</h2>
<div>{errorMessage}</div>
<div className="error_info_small">{errorMessage}</div>
<ErrorBoundary>
<CustomForm
id="custom_form"

View File

@ -590,6 +590,11 @@ svg.notification-icon {
margin-right: 10px;
}
.error_info_small {
font-size: 0.8em;
color: #550000;
}
.please-press-filter-button {
margin-bottom: 1rem;
font-weight: bold;

View File

@ -3,6 +3,7 @@ import { Routes, Route, useLocation } from 'react-router-dom';
import React, { useEffect } from 'react';
import ProcessModelEditDiagram from './ProcessModelEditDiagram';
import UserService from '../services/UserService';
import ErrorDisplay from '../components/ErrorDisplay';
export default function EditorRoutes() {
const location = useLocation();
@ -12,6 +13,7 @@ export default function EditorRoutes() {
if (UserService.hasRole(['admin'])) {
return (
<div className="full-width-container no-center-stuff">
<ErrorDisplay />
<Routes>
<Route
path="process-models/:process_model_id/files"

View File

@ -1138,7 +1138,7 @@ export default function ProcessInstanceShow({ variant }: OwnProps) {
};
const multiInstanceSelector = () => {
if (!taskToDisplay) {
if (!taskToDisplay || !taskToDisplay.runtime_info) {
return null;
}
@ -1249,17 +1249,18 @@ export default function ProcessInstanceShow({ variant }: OwnProps) {
onRequestSubmit = addPotentialOwners;
dangerous = true;
}
if (typeof taskToUse.runtime_info.instance !== 'undefined') {
secondaryButtonText = 'Return to MultiInstance Task';
onSecondarySubmit = () => {
switchToTask(taskToUse.properties_json.parent);
};
} else if (typeof taskToUse.runtime_info.iteration !== 'undefined') {
secondaryButtonText = 'Return to Loop Task';
onSecondarySubmit = () => {
switchToTask(taskToUse.properties_json.parent);
};
if (taskToUse.runtime_info) {
if (typeof taskToUse.runtime_info.instance !== 'undefined') {
secondaryButtonText = 'Return to MultiInstance Task';
onSecondarySubmit = () => {
switchToTask(taskToUse.properties_json.parent);
};
} else if (typeof taskToUse.runtime_info.iteration !== 'undefined') {
secondaryButtonText = 'Return to Loop Task';
onSecondarySubmit = () => {
switchToTask(taskToUse.properties_json.parent);
};
}
}
return (

View File

@ -1035,8 +1035,9 @@ export default function ProcessModelEditDiagram() {
const onLaunchDmnEditor = (processId: string) => {
const file = findFileNameForReferenceId(processId, 'dmn');
let path = '';
if (file) {
const path = generatePath(
path = generatePath(
'/editor/process-models/:process_model_id/files/:file_name',
{
process_model_id: params.process_model_id,
@ -1044,7 +1045,15 @@ export default function ProcessModelEditDiagram() {
}
);
window.open(path);
} else {
path = generatePath(
'/editor/process-models/:process_model_id/files?file_type=dmn',
{
process_model_id: params.process_model_id,
}
);
}
window.open(path);
};
const isDmn = () => {