From 691611c695637269f60695e04a7eedc1e3f9d0c7 Mon Sep 17 00:00:00 2001 From: Dan Funk Date: Mon, 18 Sep 2023 11:22:29 -0400 Subject: [PATCH] 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. --- .../routes/onboarding_controller.py | 53 ++++++++-------- .../integration/test_onboarding.py | 5 +- .../src/components/ReactDiagramEditor.tsx | 60 +++++++++++++++++++ .../ReactFormBuilder/ReactFormBuilder.tsx | 33 ++++++---- spiffworkflow-frontend/src/index.css | 5 ++ .../src/routes/EditorRoutes.tsx | 2 + .../src/routes/ProcessInstanceShow.tsx | 25 ++++---- .../src/routes/ProcessModelEditDiagram.tsx | 11 +++- 8 files changed, 144 insertions(+), 50 deletions(-) diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/routes/onboarding_controller.py b/spiffworkflow-backend/src/spiffworkflow_backend/routes/onboarding_controller.py index ac760478f..e57066eff 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/routes/onboarding_controller.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/routes/onboarding_controller.py @@ -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: diff --git a/spiffworkflow-backend/tests/spiffworkflow_backend/integration/test_onboarding.py b/spiffworkflow-backend/tests/spiffworkflow_backend/integration/test_onboarding.py index 9483cf24b..979246fad 100644 --- a/spiffworkflow-backend/tests/spiffworkflow_backend/integration/test_onboarding.py +++ b/spiffworkflow-backend/tests/spiffworkflow_backend/integration/test_onboarding.py @@ -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", diff --git a/spiffworkflow-frontend/src/components/ReactDiagramEditor.tsx b/spiffworkflow-frontend/src/components/ReactDiagramEditor.tsx index 96f67ddad..1a250171e 100644 --- a/spiffworkflow-frontend/src/components/ReactDiagramEditor.tsx +++ b/spiffworkflow-frontend/src/components/ReactDiagramEditor.tsx @@ -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 ( +
+
+ ); + }; + return ( <> {userActionOptions()} {showReferences()} + {diagramControlButtons()} ); } diff --git a/spiffworkflow-frontend/src/components/ReactFormBuilder/ReactFormBuilder.tsx b/spiffworkflow-frontend/src/components/ReactFormBuilder/ReactFormBuilder.tsx index c55ad1789..768426b82 100644 --- a/spiffworkflow-frontend/src/components/ReactFormBuilder/ReactFormBuilder.tsx +++ b/spiffworkflow-frontend/src/components/ReactFormBuilder/ReactFormBuilder.tsx @@ -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({

Form Preview

-
{errorMessage}
+
{errorMessage}
+ { - 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 ( diff --git a/spiffworkflow-frontend/src/routes/ProcessModelEditDiagram.tsx b/spiffworkflow-frontend/src/routes/ProcessModelEditDiagram.tsx index 0c9567e95..8c2e212b3 100644 --- a/spiffworkflow-frontend/src/routes/ProcessModelEditDiagram.tsx +++ b/spiffworkflow-frontend/src/routes/ProcessModelEditDiagram.tsx @@ -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 = () => {