diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/api.yml b/spiffworkflow-backend/src/spiffworkflow_backend/api.yml index 9d15bdb9..a15b4446 100755 --- a/spiffworkflow-backend/src/spiffworkflow_backend/api.yml +++ b/spiffworkflow-backend/src/spiffworkflow_backend/api.yml @@ -274,6 +274,12 @@ paths: description: Get only the process models that the user can run schema: type: boolean + - name: include_parent_groups + in: query + required: false + description: Get the display names for the parent groups as well + schema: + type: boolean - name: page in: query required: false diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/interfaces.py b/spiffworkflow-backend/src/spiffworkflow_backend/interfaces.py new file mode 100644 index 00000000..3d528042 --- /dev/null +++ b/spiffworkflow-backend/src/spiffworkflow_backend/interfaces.py @@ -0,0 +1,24 @@ +"""Interfaces.""" +from typing import NewType +from typing import TYPE_CHECKING +from typing import TypedDict + +if TYPE_CHECKING: + from spiffworkflow_backend.models.process_group import ProcessGroup + + +IdToProcessGroupMapping = NewType("IdToProcessGroupMapping", dict[str, "ProcessGroup"]) + + +class ProcessGroupLite(TypedDict): + """ProcessGroupLite.""" + + id: str + display_name: str + + +class ProcessGroupLitesWithCache(TypedDict): + """ProcessGroupLitesWithCache.""" + + cache: dict[str, "ProcessGroup"] + process_groups: list[ProcessGroupLite] diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/models/process_group.py b/spiffworkflow-backend/src/spiffworkflow_backend/models/process_group.py index 1439b045..63c851a5 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/models/process_group.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/models/process_group.py @@ -11,6 +11,7 @@ import marshmallow from marshmallow import post_load from marshmallow import Schema +from spiffworkflow_backend.interfaces import ProcessGroupLite from spiffworkflow_backend.models.process_model import ProcessModelInfo @@ -29,7 +30,7 @@ class ProcessGroup: default_factory=list[ProcessModelInfo] ) process_groups: list[ProcessGroup] = field(default_factory=list["ProcessGroup"]) - parent_groups: list[dict] | None = None + parent_groups: list[ProcessGroupLite] | None = None def __post_init__(self) -> None: """__post_init__.""" diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/models/process_model.py b/spiffworkflow-backend/src/spiffworkflow_backend/models/process_model.py index e8d5eed1..c737b274 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/models/process_model.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/models/process_model.py @@ -11,6 +11,7 @@ import marshmallow from marshmallow import Schema from marshmallow.decorators import post_load +from spiffworkflow_backend.interfaces import ProcessGroupLite from spiffworkflow_backend.models.file import File @@ -37,7 +38,7 @@ class ProcessModelInfo: files: list[File] | None = field(default_factory=list[File]) fault_or_suspend_on_exception: str = NotificationType.fault.value exception_notification_addresses: list[str] = field(default_factory=list) - parent_groups: list[dict] | None = None + parent_groups: list[ProcessGroupLite] | None = None metadata_extraction_paths: list[dict[str, str]] | None = None def __post_init__(self) -> None: diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_models_controller.py b/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_models_controller.py index 0f877ce7..1709357a 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_models_controller.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_models_controller.py @@ -16,6 +16,7 @@ from flask import make_response from flask.wrappers import Response from flask_bpmn.api.api_error import ApiError +from spiffworkflow_backend.interfaces import IdToProcessGroupMapping from spiffworkflow_backend.models.file import FileSchema from spiffworkflow_backend.models.process_group import ProcessGroup from spiffworkflow_backend.models.process_instance_report import ( @@ -172,6 +173,7 @@ def process_model_list( process_group_identifier: Optional[str] = None, recursive: Optional[bool] = False, filter_runnable_by_user: Optional[bool] = False, + include_parent_groups: Optional[bool] = False, page: int = 1, per_page: int = 100, ) -> flask.wrappers.Response: @@ -181,22 +183,35 @@ def process_model_list( recursive=recursive, filter_runnable_by_user=filter_runnable_by_user, ) - batch = ProcessModelService().get_batch( + process_models_to_return = ProcessModelService().get_batch( process_models, page=page, per_page=per_page ) + + if include_parent_groups: + process_group_cache = IdToProcessGroupMapping({}) + for process_model in process_models_to_return: + parent_group_lites_with_cache = ( + ProcessModelService.get_parent_group_array_and_cache_it( + process_model.id, process_group_cache + ) + ) + process_model.parent_groups = parent_group_lites_with_cache[ + "process_groups" + ] + pages = len(process_models) // per_page remainder = len(process_models) % per_page if remainder > 0: pages += 1 response_json = { - "results": ProcessModelInfoSchema(many=True).dump(batch), + "results": process_models_to_return, "pagination": { - "count": len(batch), + "count": len(process_models_to_return), "total": len(process_models), "pages": pages, }, } - return Response(json.dumps(response_json), status=200, mimetype="application/json") + return make_response(jsonify(response_json), 200) def process_model_file_update( diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/services/process_model_service.py b/spiffworkflow-backend/src/spiffworkflow_backend/services/process_model_service.py index f9f34631..8fa25bc0 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/services/process_model_service.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/services/process_model_service.py @@ -13,6 +13,8 @@ from flask_bpmn.api.api_error import ApiError from spiffworkflow_backend.exceptions.process_entity_not_found_error import ( ProcessEntityNotFoundError, ) +from spiffworkflow_backend.interfaces import ProcessGroupLite +from spiffworkflow_backend.interfaces import ProcessGroupLitesWithCache from spiffworkflow_backend.models.process_group import ProcessGroup from spiffworkflow_backend.models.process_group import ProcessGroupSchema from spiffworkflow_backend.models.process_instance import ProcessInstanceModel @@ -237,21 +239,36 @@ class ProcessModelService(FileSystemService): return process_models @classmethod - def get_parent_group_array(cls, process_identifier: str) -> list[dict]: + def get_parent_group_array_and_cache_it( + cls, process_identifier: str, process_group_cache: dict[str, ProcessGroup] + ) -> ProcessGroupLitesWithCache: """Get_parent_group_array.""" full_group_id_path = None - parent_group_array = [] + parent_group_array: list[ProcessGroupLite] = [] for process_group_id_segment in process_identifier.split("/")[0:-1]: if full_group_id_path is None: full_group_id_path = process_group_id_segment else: full_group_id_path = os.path.join(full_group_id_path, process_group_id_segment) # type: ignore - parent_group = ProcessModelService.get_process_group(full_group_id_path) + parent_group = process_group_cache.get(full_group_id_path, None) + if parent_group is None: + parent_group = ProcessModelService.get_process_group(full_group_id_path) + if parent_group: + if full_group_id_path not in process_group_cache: + process_group_cache[full_group_id_path] = parent_group parent_group_array.append( {"id": parent_group.id, "display_name": parent_group.display_name} ) - return parent_group_array + return {"cache": process_group_cache, "process_groups": parent_group_array} + + @classmethod + def get_parent_group_array(cls, process_identifier: str) -> list[ProcessGroupLite]: + """Get_parent_group_array.""" + parent_group_lites_with_cache = cls.get_parent_group_array_and_cache_it( + process_identifier, {} + ) + return parent_group_lites_with_cache["process_groups"] @classmethod def get_process_groups( diff --git a/spiffworkflow-frontend/src/components/ProcessInstanceListTable.tsx b/spiffworkflow-frontend/src/components/ProcessInstanceListTable.tsx index 2fc81265..0902aee9 100644 --- a/spiffworkflow-frontend/src/components/ProcessInstanceListTable.tsx +++ b/spiffworkflow-frontend/src/components/ProcessInstanceListTable.tsx @@ -309,7 +309,7 @@ export default function ProcessInstanceListTable({ if (filtersEnabled) { // populate process model selection HttpService.makeCallToBackend({ - path: `/process-models?per_page=1000&recursive=true`, + path: `/process-models?per_page=1000&recursive=true&include_parent_groups=true`, successCallback: processResultForProcessModels, }); } else { diff --git a/spiffworkflow-frontend/src/components/ProcessModelSearch.tsx b/spiffworkflow-frontend/src/components/ProcessModelSearch.tsx index 8a3c0b9f..bd995bc3 100644 --- a/spiffworkflow-frontend/src/components/ProcessModelSearch.tsx +++ b/spiffworkflow-frontend/src/components/ProcessModelSearch.tsx @@ -2,8 +2,7 @@ import { ComboBox, // @ts-ignore } from '@carbon/react'; -import { truncateString } from '../helpers'; -import { ProcessModel } from '../interfaces'; +import { ProcessGroupLite, ProcessModel } from '../interfaces'; type OwnProps = { onChange: (..._args: any[]) => any; @@ -18,12 +17,27 @@ export default function ProcessModelSearch({ onChange, titleText = 'Process model', }: OwnProps) { + const getParentGroupsDisplayName = (processModel: ProcessModel) => { + if (processModel.parent_groups) { + return processModel.parent_groups + .map((parentGroup: ProcessGroupLite) => { + return parentGroup.display_name; + }) + .join(' / '); + } + return ''; + }; + + const getFullProcessModelLabel = (processModel: ProcessModel) => { + return `${processModel.id} (${getParentGroupsDisplayName(processModel)} ${ + processModel.display_name + })`; + }; + const shouldFilterProcessModel = (options: any) => { const processModel: ProcessModel = options.item; const { inputValue } = options; - return `${processModel.id} (${processModel.display_name})`.includes( - inputValue - ); + return getFullProcessModelLabel(processModel).includes(inputValue); }; return ( { if (processModel) { - return `${processModel.id} (${truncateString( - processModel.display_name, - 75 - )})`; + return getFullProcessModelLabel(processModel); } return null; }} diff --git a/spiffworkflow-frontend/src/routes/ProcessGroupList.tsx b/spiffworkflow-frontend/src/routes/ProcessGroupList.tsx index d9ceaf59..7dee4f20 100644 --- a/spiffworkflow-frontend/src/routes/ProcessGroupList.tsx +++ b/spiffworkflow-frontend/src/routes/ProcessGroupList.tsx @@ -39,7 +39,7 @@ export default function ProcessGroupList() { }; // for search box HttpService.makeCallToBackend({ - path: `/process-models?per_page=1000&recursive=true`, + path: `/process-models?per_page=1000&recursive=true&include_parent_groups=true`, successCallback: processResultForProcessModels, }); }, [searchParams]);