diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/models/process_model.py b/spiffworkflow-backend/src/spiffworkflow_backend/models/process_model.py index 4f5ee2ada..278b5ef67 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/models/process_model.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/models/process_model.py @@ -38,6 +38,7 @@ class ProcessModelInfo: fault_or_suspend_on_exception: str = NotificationType.fault.value exception_notification_addresses: list[str] = field(default_factory=list) parent_groups: list[dict] | None = None + metadata_extraction_paths: dict[str, str] | None = None def __post_init__(self) -> None: """__post_init__.""" @@ -76,6 +77,8 @@ class ProcessModelInfoSchema(Schema): exception_notification_addresses = marshmallow.fields.List( marshmallow.fields.String ) + metadata_extraction_paths = marshmallow.fields.Dict(keys=marshmallow.fields.Str(required=False), values=marshmallow.fields.Str(required=False), required=False) + @post_load def make_spec( diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_api_blueprint.py b/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_api_blueprint.py index d19472cc1..7e73a2855 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_api_blueprint.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_api_blueprint.py @@ -261,19 +261,26 @@ def process_model_create( modified_process_group_id: str, body: Dict[str, Union[str, bool, int]] ) -> flask.wrappers.Response: """Process_model_create.""" - process_model_info = ProcessModelInfoSchema().load(body) + body_include_list = [ + "id", + "display_name", + "primary_file_name", + "primary_process_id", + "description", + "metadata_extraction_paths", + ] + body_filtered = { + include_item: body[include_item] + for include_item in body_include_list + if include_item in body + } + if modified_process_group_id is None: raise ApiError( error_code="process_group_id_not_specified", message="Process Model could not be created when process_group_id path param is unspecified", status_code=400, ) - if process_model_info is None: - raise ApiError( - error_code="process_model_could_not_be_created", - message=f"Process Model could not be created from given body: {body}", - status_code=400, - ) unmodified_process_group_id = un_modify_modified_process_model_id( modified_process_group_id @@ -286,6 +293,14 @@ def process_model_create( status_code=400, ) + process_model_info = ProcessModelInfo(**body_filtered) # type: ignore + if process_model_info is None: + raise ApiError( + error_code="process_model_could_not_be_created", + message=f"Process Model could not be created from given body: {body}", + status_code=400, + ) + ProcessModelService.add_process_model(process_model_info) return Response( json.dumps(ProcessModelInfoSchema().dump(process_model_info)), @@ -299,7 +314,6 @@ def process_model_delete( ) -> flask.wrappers.Response: """Process_model_delete.""" process_model_identifier = modified_process_model_identifier.replace(":", "/") - # process_model_identifier = f"{process_group_id}/{process_model_id}" ProcessModelService().process_model_delete(process_model_identifier) return Response(json.dumps({"ok": True}), status=200, mimetype="application/json") @@ -314,6 +328,7 @@ def process_model_update( "primary_file_name", "primary_process_id", "description", + "metadata_extraction_paths", ] body_filtered = { include_item: body[include_item] @@ -321,7 +336,6 @@ def process_model_update( if include_item in body } - # process_model_identifier = f"{process_group_id}/{process_model_id}" process_model = get_process_model(process_model_identifier) ProcessModelService.update_process_model(process_model, body_filtered) return ProcessModelInfoSchema().dump(process_model) @@ -330,10 +344,7 @@ def process_model_update( def process_model_show(modified_process_model_identifier: str) -> Any: """Process_model_show.""" process_model_identifier = modified_process_model_identifier.replace(":", "/") - # process_model_identifier = f"{process_group_id}/{process_model_id}" process_model = get_process_model(process_model_identifier) - # TODO: Temporary. Should not need the next line once models have correct ids - # process_model.id = process_model_identifier files = sorted(SpecFileService.get_files(process_model)) process_model.files = files for file in process_model.files: @@ -425,7 +436,6 @@ def process_model_file_update( ) -> flask.wrappers.Response: """Process_model_file_update.""" process_model_identifier = modified_process_model_id.replace(":", "/") - # process_model_identifier = f"{process_group_id}/{process_model_id}" process_model = get_process_model(process_model_identifier) request_file = get_file_from_request() @@ -1142,7 +1152,7 @@ def process_instance_report_show( per_page: int = 100, ) -> flask.wrappers.Response: """Process_instance_report_show.""" - process_instances = ProcessInstanceModel.query.order_by( # .filter_by(process_model_identifier=process_model.id) + process_instances = ProcessInstanceModel.query.order_by( ProcessInstanceModel.start_in_seconds.desc(), ProcessInstanceModel.id.desc() # type: ignore ).paginate( page=page, per_page=per_page, error_out=False diff --git a/spiffworkflow-backend/tests/spiffworkflow_backend/integration/test_process_api.py b/spiffworkflow-backend/tests/spiffworkflow_backend/integration/test_process_api.py index 215e44d46..b30652a48 100644 --- a/spiffworkflow-backend/tests/spiffworkflow_backend/integration/test_process_api.py +++ b/spiffworkflow-backend/tests/spiffworkflow_backend/integration/test_process_api.py @@ -333,6 +333,7 @@ class TestProcessApi(BaseTest): process_model.display_name = "Updated Display Name" process_model.primary_file_name = "superduper.bpmn" process_model.primary_process_id = "superduper" + process_model.metadata_extraction_paths = {'extraction1': 'path1'} modified_process_model_identifier = process_model_identifier.replace("/", ":") response = client.put( @@ -346,6 +347,7 @@ class TestProcessApi(BaseTest): assert response.json["display_name"] == "Updated Display Name" assert response.json["primary_file_name"] == "superduper.bpmn" assert response.json["primary_process_id"] == "superduper" + assert response.json["metadata_extraction_paths"] == {'extraction1': 'path1'} def test_process_model_list_all( self, diff --git a/spiffworkflow-frontend/src/components/ProcessInstanceListTable.tsx b/spiffworkflow-frontend/src/components/ProcessInstanceListTable.tsx index 92355fe92..2c661719d 100644 --- a/spiffworkflow-frontend/src/components/ProcessInstanceListTable.tsx +++ b/spiffworkflow-frontend/src/components/ProcessInstanceListTable.tsx @@ -142,7 +142,7 @@ export default function ProcessInstanceListTable({ ReportColumn[] >([]); const [processInstanceReportJustSaved, setProcessInstanceReportJustSaved] = - useState(false); + useState(null); const [showReportColumnForm, setShowReportColumnForm] = useState(false); const [reportColumnToOperateOn, setReportColumnToOperateOn] = @@ -367,10 +367,14 @@ export default function ProcessInstanceListTable({ const processInstanceReportSaveTag = () => { if (processInstanceReportJustSaved) { + let titleOperation = 'Updated'; + if (processInstanceReportJustSaved === 'new') { + titleOperation = 'Created'; + } return ( { + const processInstanceReportDidChange = (selection: any, mode?: string) => { clearFilters(); const selectedReport = selection.selectedItem; setProcessInstanceReportSelection(selectedReport); @@ -600,7 +601,7 @@ export default function ProcessInstanceListTable({ } setErrorMessage(null); - setProcessInstanceReportJustSaved(savedReport); + setProcessInstanceReportJustSaved(mode || null); navigate(`/admin/process-instances${queryParamString}`); }; @@ -615,12 +616,12 @@ export default function ProcessInstanceListTable({ }; // TODO onSuccess reload/select the new report in the report search - const onSaveReportSuccess = (result: any) => { + const onSaveReportSuccess = (result: any, mode: string) => { processInstanceReportDidChange( { selectedItem: result, }, - true + mode ); }; @@ -638,7 +639,7 @@ export default function ProcessInstanceListTable({ } return ( onSaveReportSuccess(result, 'new')} buttonClassName="narrow-button" columnArray={reportColumns()} orderBy="" @@ -705,7 +706,7 @@ export default function ProcessInstanceListTable({ } else { newReportFilters.splice(existingReportFilterIndex, 1); } - } else { + } else if (reportColumnForEditing.filter_field_value) { newReportFilters = newReportFilters.concat([newReportFilter]); } } @@ -1157,7 +1158,7 @@ export default function ProcessInstanceListTable({ onSaveReportSuccess(result, 'edit')} columnArray={reportColumns()} orderBy="" buttonText="Save" diff --git a/spiffworkflow-frontend/src/components/ProcessModelForm.tsx b/spiffworkflow-frontend/src/components/ProcessModelForm.tsx index 396f1ea0f..0866b60da 100644 --- a/spiffworkflow-frontend/src/components/ProcessModelForm.tsx +++ b/spiffworkflow-frontend/src/components/ProcessModelForm.tsx @@ -2,9 +2,11 @@ import { useState } from 'react'; import { useNavigate } from 'react-router-dom'; // @ts-ignore import { Button, ButtonSet, Form, Stack, TextInput } from '@carbon/react'; +// @ts-ignore +import { AddAlt } from '@carbon/icons-react'; import { modifyProcessIdentifierForPathParam, slugifyString } from '../helpers'; import HttpService from '../services/HttpService'; -import { ProcessModel } from '../interfaces'; +import { MetadataExtractionPaths, ProcessModel } from '../interfaces'; type OwnProps = { mode: string; @@ -23,6 +25,7 @@ export default function ProcessModelForm({ const [idHasBeenUpdatedByUser, setIdHasBeenUpdatedByUser] = useState(false); const [displayNameInvalid, setDisplayNameInvalid] = useState(false); + useState(false); const navigate = useNavigate(); const navigateToProcessModel = (result: ProcessModel) => { @@ -64,6 +67,7 @@ export default function ProcessModelForm({ const postBody = { display_name: processModel.display_name, description: processModel.description, + metadata_extraction_paths: processModel.metadata_extraction_paths, }; if (mode === 'new') { Object.assign(postBody, { @@ -87,6 +91,66 @@ export default function ProcessModelForm({ setProcessModel(processModelToCopy); }; + const metadataExtractionPathForm = ( + metadataKey: string, + metadataPath: string + ) => { + return ( + <> + { + const cep: MetadataExtractionPaths = + processModel.metadata_extraction_paths || {}; + delete cep[metadataKey]; + cep[event.target.value] = metadataPath; + updateProcessModel({ metadata_extraction_paths: cep }); + }} + /> + { + const cep: MetadataExtractionPaths = + processModel.metadata_extraction_paths || {}; + cep[metadataKey] = event.target.value; + updateProcessModel({ metadata_extraction_paths: cep }); + }} + /> + + ); + }; + + const metadataExtractionPathFormArea = () => { + if (processModel.metadata_extraction_paths) { + console.log( + 'processModel.metadata_extraction_paths', + processModel.metadata_extraction_paths + ); + return Object.keys(processModel.metadata_extraction_paths).map( + (metadataKey: string) => { + return metadataExtractionPathForm( + metadataKey, + processModel.metadata_extraction_paths + ? processModel.metadata_extraction_paths[metadataKey] + : '' + ); + } + ); + } + return null; + }; + + const addBlankMetadataExtractionPath = () => { + const cep: MetadataExtractionPaths = + processModel.metadata_extraction_paths || {}; + Object.assign(cep, { '': '' }); + updateProcessModel({ metadata_extraction_paths: cep }); + }; + const onDisplayNameChanged = (newDisplayName: any) => { setDisplayNameInvalid(false); const updateDict = { display_name: newDisplayName }; @@ -145,6 +209,22 @@ export default function ProcessModelForm({ /> ); + textInputs.push(<>{metadataExtractionPathFormArea()}); + textInputs.push( + + ); + return textInputs; }; diff --git a/spiffworkflow-frontend/src/interfaces.ts b/spiffworkflow-frontend/src/interfaces.ts index a75b9a824..3b428b569 100644 --- a/spiffworkflow-frontend/src/interfaces.ts +++ b/spiffworkflow-frontend/src/interfaces.ts @@ -98,6 +98,10 @@ export interface ProcessGroupLite { display_name: string; } +export interface MetadataExtractionPaths { + [key: string]: string; +} + export interface ProcessModel { id: string; description: string; @@ -105,6 +109,7 @@ export interface ProcessModel { primary_file_name: string; files: ProcessFile[]; parent_groups?: ProcessGroupLite[]; + metadata_extraction_paths?: MetadataExtractionPaths; } export interface ProcessGroup {