diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/routes/onboarding_controller.py b/spiffworkflow-backend/src/spiffworkflow_backend/routes/onboarding_controller.py index ac760478..e57066ef 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 9483cf24..979246fa 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 96f67dda..1a250171 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 c55ad178..768426b8 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 0c9567e9..8c2e212b 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 = () => {