From f0f4bcce127eefef6b75030a72b2a00d4d340df6 Mon Sep 17 00:00:00 2001 From: jasquat <2487833+jasquat@users.noreply.github.com> Date: Fri, 26 Jan 2024 11:41:07 -0500 Subject: [PATCH] Feature/homepage extension filter refactor (#919) * WIP: initial work to have a home page created from an extension w/ burnettk * added support to display an extension as the root page w/ burnettk * allow extensions to add new routes to base routes w/ burnettk * use page instead of creating new key route w/ burnettk * added components to support pi tables in extensions w/ burnettk * allow using asterisks to mark words as bold in process instance list table * moved table component from InstancesListTable to own component w/ burnettk * filters are somewhat working again w/ burnettk * default homepage uses the table without filters component now w/ burnettk * renamed instance list tables to be more appropriate w/ burnettk * display errors if list table is used incorrectly w/ burnettk * fixed issue where columns were not displaying in the filter list * pyl * rely on changes in report hash to determine if report hash state needs updating * only show link to report if there are instances to show * many updates for filtering to remove the apply button and clean things up w/ burnettk * some more fixes for too many renderings w/ burnettk * advanced filters are working again w/ burnettk * clear is working again w/ burnettk * fixed a few linting errors and warnings w/ burnettk * fixed some cypress tests * if there are errors then display them right away instead of trying to put together the other elements --------- Co-authored-by: jasquat Co-authored-by: burnettk --- .../src/spiffworkflow_backend/models/group.py | 6 +- .../routes/extensions_controller.py | 56 +- .../routes/tasks_controller.py | 2 +- .../scripts/get_all_permissions.py | 2 +- .../scripts/get_current_user.py | 4 +- .../scripts/get_groups_for_user.py | 30 + .../services/authorization_service.py | 2 +- .../scripts/test_get_groups_for_user.py | 35 + .../cypress/e2e/process_instances.cy.js | 10 +- .../ExtensionUxElementForDisplay.tsx | 23 +- .../src/components/NavigationBar.tsx | 12 +- .../ProcessInstanceListSaveAsReport.tsx | 11 +- .../components/ProcessInstanceListTable.tsx | 1846 ++--------------- .../ProcessInstanceListTableWithFilters.tsx | 1579 ++++++++++++++ .../src/components/SpiffTabs.tsx | 39 + .../src/components/TaskRouteTabs.tsx | 44 +- .../src/extension_ui_schema_interfaces.ts | 3 +- spiffworkflow-frontend/src/index.css | 2 +- spiffworkflow-frontend/src/interfaces.ts | 10 + .../src/routes/BaseRoutes.tsx | 83 +- .../src/routes/CompletedInstances.tsx | 73 +- .../src/routes/Extension.tsx | 72 +- .../src/routes/HomeRoutes.tsx | 2 +- .../src/routes/InProgressInstances.tsx | 77 +- .../src/routes/ProcessInstanceList.tsx | 7 +- .../src/routes/ProcessModelShow.tsx | 8 +- .../src/routes/RootRoute.tsx | 2 +- 27 files changed, 2177 insertions(+), 1863 deletions(-) create mode 100644 spiffworkflow-backend/src/spiffworkflow_backend/scripts/get_groups_for_user.py create mode 100644 spiffworkflow-backend/tests/spiffworkflow_backend/scripts/test_get_groups_for_user.py create mode 100644 spiffworkflow-frontend/src/components/ProcessInstanceListTableWithFilters.tsx create mode 100644 spiffworkflow-frontend/src/components/SpiffTabs.tsx diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/models/group.py b/spiffworkflow-backend/src/spiffworkflow_backend/models/group.py index 909f56d50..d17270409 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/models/group.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/models/group.py @@ -1,5 +1,6 @@ from __future__ import annotations +from dataclasses import dataclass from typing import TYPE_CHECKING from sqlalchemy.orm import relationship @@ -21,13 +22,14 @@ class GroupNotFoundError(Exception): pass +@dataclass class GroupModel(SpiffworkflowBaseDBModel): __tablename__ = "group" __table_args__ = {"extend_existing": True} id = db.Column(db.Integer, primary_key=True) - name = db.Column(db.String(255), index=True) - identifier = db.Column(db.String(255), index=True) + name: str = db.Column(db.String(255), index=True) + identifier: str = db.Column(db.String(255), index=True) user_group_assignments = relationship("UserGroupAssignmentModel", cascade="delete") user_group_assignments_waiting = relationship("UserGroupAssignmentWaitingModel", cascade="delete") # type: ignore diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/routes/extensions_controller.py b/spiffworkflow-backend/src/spiffworkflow_backend/routes/extensions_controller.py index c44e47148..e9a42aaef 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/routes/extensions_controller.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/routes/extensions_controller.py @@ -96,32 +96,7 @@ def _run_extension( _raise_unless_extensions_api_enabled() process_model_identifier = _get_process_model_identifier(modified_process_model_identifier) - - try: - process_model = _get_process_model(process_model_identifier) - except ApiError as ex: - if ex.error_code == "process_model_cannot_be_found": - # if process_model_identifier.startswith(current_app.config["SPIFFWORKFLOW_BACKEND_EXTENSIONS_PROCESS_MODEL_PREFIX"]) - raise ApiError( - error_code="invalid_process_model_extension", - message=( - f"Process Model '{process_model_identifier}' could not be found as an extension. It must be in the" - " correct Process Group:" - f" {current_app.config['SPIFFWORKFLOW_BACKEND_EXTENSIONS_PROCESS_MODEL_PREFIX']}" - ), - status_code=403, - ) from ex - raise ex - - if process_model.primary_file_name is None: - raise ApiError( - error_code="process_model_missing_primary_bpmn_file", - message=( - f"Process Model '{process_model_identifier}' does not have a primary" - " bpmn file. One must be set in order to instantiate this model." - ), - status_code=400, - ) + process_model = _get_process_model_or_raise(process_model_identifier) ui_schema_action = None persistence_level = "none" @@ -220,3 +195,32 @@ def _add_extension_group_identifier_it_not_present(process_model_identifier: str if process_model_identifier.startswith(f"{extension_prefix}/"): return process_model_identifier return f"{extension_prefix}/{process_model_identifier}" + + +def _get_process_model_or_raise(process_model_identifier: str) -> ProcessModelInfo: + try: + process_model = _get_process_model(process_model_identifier) + except ApiError as ex: + if ex.error_code == "process_model_cannot_be_found": + # if process_model_identifier.startswith(current_app.config["SPIFFWORKFLOW_BACKEND_EXTENSIONS_PROCESS_MODEL_PREFIX"]) + raise ApiError( + error_code="invalid_process_model_extension", + message=( + f"Process Model '{process_model_identifier}' could not be found as an extension. It must be in the" + " correct Process Group:" + f" {current_app.config['SPIFFWORKFLOW_BACKEND_EXTENSIONS_PROCESS_MODEL_PREFIX']}" + ), + status_code=403, + ) from ex + raise ex + + if process_model.primary_file_name is None: + raise ApiError( + error_code="process_model_missing_primary_bpmn_file", + message=( + f"Process Model '{process_model_identifier}' does not have a primary" + " bpmn file. One must be set in order to instantiate this model." + ), + status_code=400, + ) + return process_model diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/routes/tasks_controller.py b/spiffworkflow-backend/src/spiffworkflow_backend/routes/tasks_controller.py index cbe07c977..218acf85a 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/routes/tasks_controller.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/routes/tasks_controller.py @@ -1024,7 +1024,7 @@ def _get_tasks( process_model_identifier_column = ProcessInstanceModel.process_model_identifier process_instance_status_column = ProcessInstanceModel.status.label("process_instance_status") # type: ignore user_username_column = UserModel.username.label("process_initiator_username") # type: ignore - group_identifier_column = GroupModel.identifier.label("assigned_user_group_identifier") + group_identifier_column = GroupModel.identifier.label("assigned_user_group_identifier") # type: ignore if current_app.config["SPIFFWORKFLOW_BACKEND_DATABASE_TYPE"] == "postgres": process_model_identifier_column = func.max(ProcessInstanceModel.process_model_identifier).label( "process_model_identifier" diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/scripts/get_all_permissions.py b/spiffworkflow-backend/src/spiffworkflow_backend/scripts/get_all_permissions.py index 45fa582ac..db03cccd8 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/scripts/get_all_permissions.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/scripts/get_all_permissions.py @@ -32,7 +32,7 @@ class GetAllPermissions(Script): .add_columns( PermissionAssignmentModel.permission, PermissionTargetModel.uri, - GroupModel.identifier.label("group_identifier"), + GroupModel.identifier.label("group_identifier"), # type: ignore ) .all() ) diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/scripts/get_current_user.py b/spiffworkflow-backend/src/spiffworkflow_backend/scripts/get_current_user.py index c5599c0aa..8bcb1a5b4 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/scripts/get_current_user.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/scripts/get_current_user.py @@ -1,7 +1,6 @@ """Get current user.""" from typing import Any -from flask import current_app from flask import g from spiffworkflow_backend.models.script_attributes_context import ScriptAttributesContext from spiffworkflow_backend.scripts.script import Script @@ -19,5 +18,4 @@ class GetCurrentUser(Script): def run(self, script_attributes_context: ScriptAttributesContext, *_args: Any, **kwargs: Any) -> Any: # dump the user using our json encoder and then load it back up as a dict # to remove unwanted field types - user_as_json_string = current_app.json.dumps(g.user) - return current_app.json.loads(user_as_json_string) + return g.user.as_dict() diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/scripts/get_groups_for_user.py b/spiffworkflow-backend/src/spiffworkflow_backend/scripts/get_groups_for_user.py new file mode 100644 index 000000000..6f7d984a6 --- /dev/null +++ b/spiffworkflow-backend/src/spiffworkflow_backend/scripts/get_groups_for_user.py @@ -0,0 +1,30 @@ +from typing import Any + +from flask import current_app +from flask import g +from spiffworkflow_backend.models.script_attributes_context import ScriptAttributesContext +from spiffworkflow_backend.scripts.script import Script + + +class GetGroupsForUser(Script): + @staticmethod + def requires_privileged_permissions() -> bool: + """We have deemed this function safe to run without elevated permissions.""" + return False + + def get_description(self) -> str: + return """Return the list of groups for the current user.""" + + def run( + self, + script_attributes_context: ScriptAttributesContext, + *args: Any, + **kwargs: Any, + ) -> Any: + groups = g.user.groups + group_items = [ + group for group in groups if group.identifier != current_app.config["SPIFFWORKFLOW_BACKEND_DEFAULT_USER_GROUP"] + ] + + group_as_json_string = current_app.json.dumps(group_items) + return current_app.json.loads(group_as_json_string) diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/services/authorization_service.py b/spiffworkflow-backend/src/spiffworkflow_backend/services/authorization_service.py index d7b204569..aff6c1946 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/services/authorization_service.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/services/authorization_service.py @@ -865,7 +865,7 @@ class AuthorizationService: # do not remove the default user group added_group_identifiers.add(current_app.config["SPIFFWORKFLOW_BACKEND_DEFAULT_USER_GROUP"]) added_group_identifiers.add(SPIFF_GUEST_GROUP) - groups_to_delete = GroupModel.query.filter(GroupModel.identifier.not_in(added_group_identifiers)).all() + groups_to_delete = GroupModel.query.filter(GroupModel.identifier.not_in(added_group_identifiers)).all() # type: ignore for gtd in groups_to_delete: db.session.delete(gtd) diff --git a/spiffworkflow-backend/tests/spiffworkflow_backend/scripts/test_get_groups_for_user.py b/spiffworkflow-backend/tests/spiffworkflow_backend/scripts/test_get_groups_for_user.py new file mode 100644 index 000000000..b4ec96922 --- /dev/null +++ b/spiffworkflow-backend/tests/spiffworkflow_backend/scripts/test_get_groups_for_user.py @@ -0,0 +1,35 @@ +from flask import g +from flask.app import Flask +from spiffworkflow_backend.models.script_attributes_context import ScriptAttributesContext +from spiffworkflow_backend.scripts.get_groups_for_user import GetGroupsForUser +from spiffworkflow_backend.services.user_service import UserService + +from tests.spiffworkflow_backend.helpers.base_test import BaseTest + + +class TestGetGroupsForUser(BaseTest): + def test_get_groups_for_user( + self, + app: Flask, + with_db_and_bpmn_file_cleanup: None, + ) -> None: + testuser1 = self.find_or_create_user("testuser1") + group1 = UserService.find_or_create_group("group1") + group2 = UserService.find_or_create_group("group2") + UserService.find_or_create_group("group3") + UserService.add_user_to_group(testuser1, group1) + UserService.add_user_to_group(testuser1, group2) + + g.user = testuser1 + script_attributes_context = ScriptAttributesContext( + task=None, + environment_identifier="testing", + process_instance_id=None, + process_model_identifier=None, + ) + result = GetGroupsForUser().run( + script_attributes_context, + ) + assert len(result) == 2 + group_names = [g["identifier"] for g in result] + assert group_names == ["group1", "group2"] diff --git a/spiffworkflow-frontend/cypress/e2e/process_instances.cy.js b/spiffworkflow-frontend/cypress/e2e/process_instances.cy.js index 43e10f143..629875a7f 100644 --- a/spiffworkflow-frontend/cypress/e2e/process_instances.cy.js +++ b/spiffworkflow-frontend/cypress/e2e/process_instances.cy.js @@ -6,10 +6,17 @@ const filterByDate = (fromDate) => { cy.get('#date-picker-start-from').clear(); cy.get('#date-picker-start-from').type(format(fromDate, DATE_FORMAT)); cy.contains('Start date to').click(); + + // this can sometimes run a couple mintues after the instances are completed + // so avoid failing tests for that by setting the time as well + cy.get('#time-picker-start-from').clear(); + cy.get('#time-picker-start-from').type(format(fromDate, 'HH:mm')); + cy.get('#date-picker-end-from').clear(); cy.get('#date-picker-end-from').type(format(fromDate, DATE_FORMAT)); cy.contains('End date to').click(); - cy.getBySel('filter-button').click(); + cy.get('#time-picker-end-from').clear(); + cy.get('#time-picker-end-from').type(format(fromDate, 'HH:mm')); }; const updateDmnText = (oldText, newText, elementId = 'wonderful_process') => { @@ -192,7 +199,6 @@ describe('process-instances', () => { cy.get(statusSelect).click(); cy.get(statusSelect).contains(titleizeString(processStatus)).click(); clickOnHeaderToMakeSureMultiSelectComponentStateIsStable(); - cy.getBySel('filter-button').click(); // make sure that there is 1 status item selected in the multiselect cy.get(`${statusSelect} .cds--tag`).contains('1'); diff --git a/spiffworkflow-frontend/src/components/ExtensionUxElementForDisplay.tsx b/spiffworkflow-frontend/src/components/ExtensionUxElementForDisplay.tsx index c4726071d..482a5f013 100644 --- a/spiffworkflow-frontend/src/components/ExtensionUxElementForDisplay.tsx +++ b/spiffworkflow-frontend/src/components/ExtensionUxElementForDisplay.tsx @@ -4,27 +4,42 @@ type OwnProps = { displayLocation: string; elementCallback: Function; extensionUxElements?: UiSchemaUxElement[] | null; + elementCallbackIfNotFound?: Function; }; -export default function ExtensionUxElementForDisplay({ +export function ExtensionUxElementMap({ displayLocation, elementCallback, extensionUxElements, + elementCallbackIfNotFound, }: OwnProps) { if (!extensionUxElements) { return null; } const mainElement = () => { - return extensionUxElements.map( + let foundElement = false; + const elementMap = extensionUxElements.map( (uxElement: UiSchemaUxElement, index: number) => { if (uxElement.display_location === displayLocation) { + foundElement = true; return elementCallback(uxElement, index); } return null; } ); + if (!foundElement && elementCallbackIfNotFound) { + return elementCallbackIfNotFound(); + } + return elementMap; }; - - return <>{mainElement()}; + return mainElement(); +} + +export default function ExtensionUxElementForDisplay(args: OwnProps) { + const result = ExtensionUxElementMap(args); + if (result === null) { + return null; + } + return result; } diff --git a/spiffworkflow-frontend/src/components/NavigationBar.tsx b/spiffworkflow-frontend/src/components/NavigationBar.tsx index 28dba0613..98eb8d828 100644 --- a/spiffworkflow-frontend/src/components/NavigationBar.tsx +++ b/spiffworkflow-frontend/src/components/NavigationBar.tsx @@ -63,16 +63,22 @@ export default function NavigationBar({ extensionUxElements }: OwnProps) { documentationUrl = DOCUMENTATION_URL; } + const processGroupPath = '/process-groups'; + const versionInfo = appVersionInfo(); useEffect(() => { - let newActiveKey = '/process-groups'; + let newActiveKey = 'unknown'; if (location.pathname.match(/^\/messages\b/)) { newActiveKey = '/messages'; } else if (location.pathname.match(/^\/process-instances\/reports\b/)) { newActiveKey = '/process-instances/reports'; } else if (location.pathname.match(/^\/process-instances\b/)) { newActiveKey = '/process-instances'; + } else if (location.pathname.match(/^\/process-(groups|models)\b/)) { + newActiveKey = processGroupPath; + } else if (location.pathname.match(/^\/editor\b/)) { + newActiveKey = processGroupPath; } else if (location.pathname.match(/^\/configuration\b/)) { newActiveKey = '/configuration'; } else if (location.pathname.match(/^\/data-stores\b/)) { @@ -231,8 +237,8 @@ export default function NavigationBar({ extensionUxElements }: OwnProps) { Processes diff --git a/spiffworkflow-frontend/src/components/ProcessInstanceListSaveAsReport.tsx b/spiffworkflow-frontend/src/components/ProcessInstanceListSaveAsReport.tsx index 6e43b2126..8e2d9c496 100644 --- a/spiffworkflow-frontend/src/components/ProcessInstanceListSaveAsReport.tsx +++ b/spiffworkflow-frontend/src/components/ProcessInstanceListSaveAsReport.tsx @@ -6,7 +6,7 @@ import { Modal, // @ts-ignore } from '@carbon/react'; -import { ProcessInstanceReport } from '../interfaces'; +import { ProcessInstanceReport, ReportMetadata } from '../interfaces'; import HttpService from '../services/HttpService'; type OwnProps = { @@ -14,7 +14,7 @@ type OwnProps = { buttonText?: string; buttonClassName?: string; processInstanceReportSelection?: ProcessInstanceReport | null; - getReportMetadataCallback: Function; + reportMetadata: ReportMetadata | null; }; export default function ProcessInstanceListSaveAsReport({ @@ -22,7 +22,7 @@ export default function ProcessInstanceListSaveAsReport({ processInstanceReportSelection, buttonClassName, buttonText = 'Save as Perspective', - getReportMetadataCallback, + reportMetadata, }: OwnProps) { const [identifier, setIdentifier] = useState( processInstanceReportSelection?.identifier || '' @@ -50,11 +50,6 @@ export default function ProcessInstanceListSaveAsReport({ const addProcessInstanceReport = (event: any) => { event.preventDefault(); - const reportMetadata = getReportMetadataCallback(); - if (!reportMetadata) { - return; - } - let path = `/process-instances/reports`; let httpMethod = 'POST'; if (isEditMode() && processInstanceReportSelection) { diff --git a/spiffworkflow-frontend/src/components/ProcessInstanceListTable.tsx b/spiffworkflow-frontend/src/components/ProcessInstanceListTable.tsx index 599eab4e5..7b2856c11 100644 --- a/spiffworkflow-frontend/src/components/ProcessInstanceListTable.tsx +++ b/spiffworkflow-frontend/src/components/ProcessInstanceListTable.tsx @@ -1,326 +1,105 @@ -import React, { - useCallback, - useEffect, - useMemo, - useRef, - useState, -} from 'react'; -import { useNavigate, useSearchParams } from 'react-router-dom'; - -import { Close, AddAlt, ArrowRight } from '@carbon/icons-react'; +import { ArrowRight } from '@carbon/icons-react'; import { - Button, - ButtonSet, - DatePicker, - DatePickerInput, - Dropdown, - Table, Grid, Column, - MultiSelect, + TableRow, + Table, TableHeader, TableHead, - TableRow, - TimePicker, - Tag, - Modal, - ComboBox, - TextInput, - FormLabel, - Checkbox, + Button, } from '@carbon/react'; -import { useDebouncedCallback } from 'use-debounce'; +import { useCallback, useEffect, useRef, useState } from 'react'; +import 'react-datepicker/dist/react-datepicker.css'; +import { useNavigate, useSearchParams } from 'react-router-dom'; + import { - PROCESS_STATUSES, - DATE_FORMAT_CARBON, - DATE_FORMAT_FOR_DISPLAY, -} from '../config'; -import { - getKeyByValue, getLastMilestoneFromProcessInstance, getPageInfoFromSearchParams, modifyProcessIdentifierForPathParam, refreshAtInterval, titleizeString, - truncateString, } from '../helpers'; -import { useUriListForPermissions } from '../hooks/UriListForPermissions'; - -import PaginationForTable from './PaginationForTable'; -import 'react-datepicker/dist/react-datepicker.css'; - -import HttpService from '../services/HttpService'; import { PaginationObject, - ProcessModel, - ProcessInstanceReport, ProcessInstance, + ProcessInstanceReport, ReportColumn, - ReportColumnForEditing, - ReportMetadata, ReportFilter, - User, - ErrorForDisplay, - PermissionsToCheck, - FilterOperatorMapping, - FilterDisplayTypeMapping, + ReportMetadata, + SpiffTableHeader, } from '../interfaces'; -import ProcessModelSearch from './ProcessModelSearch'; -import ProcessInstanceReportSearch from './ProcessInstanceReportSearch'; -import ProcessInstanceListDeleteReport from './ProcessInstanceListDeleteReport'; -import ProcessInstanceListSaveAsReport from './ProcessInstanceListSaveAsReport'; -import { Notification } from './Notification'; -import useAPIError from '../hooks/UseApiError'; -import { usePermissionFetcher } from '../hooks/PermissionService'; -import { Can } from '../contexts/Can'; -import TableCellWithTimeAgoInWords from './TableCellWithTimeAgoInWords'; -import UserService from '../services/UserService'; -import Filters from './Filters'; import DateAndTimeService from '../services/DateAndTimeService'; +import HttpService from '../services/HttpService'; +import UserService from '../services/UserService'; +import PaginationForTable from './PaginationForTable'; +import TableCellWithTimeAgoInWords from './TableCellWithTimeAgoInWords'; +import { + childrenForErrorObject, + errorForDisplayFromString, +} from './ErrorDisplay'; type OwnProps = { - filtersEnabled?: boolean; + additionalReportFilters?: ReportFilter[]; + autoReload?: boolean; + canCompleteAllTasks?: boolean; + header?: SpiffTableHeader; + onProcessInstanceTableListUpdate?: Function; + paginationClassName?: string; paginationQueryParamPrefix?: string; perPageOptions?: number[]; - showReports?: boolean; reportIdentifier?: string; - textToShowIfEmpty?: string; - paginationClassName?: string; - autoReload?: boolean; - additionalReportFilters?: ReportFilter[]; - variant?: string; - canCompleteAllTasks?: boolean; + reportMetadata?: ReportMetadata; showActionsColumn?: boolean; showLinkToReport?: boolean; - headerElement?: React.ReactElement; tableHtmlId?: string; + textToShowIfEmpty?: string; + variant?: string; }; -interface dateParameters { - [key: string]: ((..._args: any[]) => any)[]; -} - export default function ProcessInstanceListTable({ - filtersEnabled = true, + additionalReportFilters, + autoReload = false, + canCompleteAllTasks = false, + header, + onProcessInstanceTableListUpdate, + paginationClassName, paginationQueryParamPrefix, perPageOptions, - additionalReportFilters, - showReports = true, reportIdentifier, - textToShowIfEmpty, - paginationClassName, - autoReload = false, - variant = 'for-me', - canCompleteAllTasks = false, + reportMetadata, showActionsColumn = false, showLinkToReport = false, - headerElement, tableHtmlId, + textToShowIfEmpty, + variant = 'for-me', }: OwnProps) { - // eslint-disable-next-line sonarjs/no-duplicate-string - let processInstanceApiSearchPath = '/process-instances/for-me'; - if (variant === 'all') { - processInstanceApiSearchPath = '/process-instances'; - } - - const [searchParams, setSearchParams] = useSearchParams(); const navigate = useNavigate(); - const { addError, removeError } = useAPIError(); - - const { targetUris } = useUriListForPermissions(); - const permissionRequestData: PermissionsToCheck = { - [targetUris.userSearch]: ['GET'], - }; - const { ability, permissionsLoaded } = usePermissionFetcher( - permissionRequestData - ); - const canSearchUsers: boolean = ability.can('GET', targetUris.userSearch); + const [pagination, setPagination] = useState(null); + const [searchParams] = useSearchParams(); const [processInstances, setProcessInstances] = useState( [] ); - const [reportMetadata, setReportMetadata] = useState(); - const [pagination, setPagination] = useState(null); - - const [startFromDate, setStartFromDate] = useState(''); - const [startToDate, setStartToDate] = useState(''); - const [endFromDate, setEndFromDate] = useState(''); - const [endToDate, setEndToDate] = useState(''); - const [startFromTime, setStartFromTime] = useState(''); - const [startToTime, setStartToTime] = useState(''); - const [endFromTime, setEndFromTime] = useState(''); - const [endToTime, setEndToTime] = useState(''); - const [startFromTimeInvalid, setStartFromTimeInvalid] = - useState(false); - const [startToTimeInvalid, setStartToTimeInvalid] = useState(false); - const [endFromTimeInvalid, setEndFromTimeInvalid] = useState(false); - const [endToTimeInvalid, setEndToTimeInvalid] = useState(false); - - const [showFilterOptions, setShowFilterOptions] = useState(false); - const [requiresRefilter, setRequiresRefilter] = useState(false); - const [lastColumnFilter, setLastColumnFilter] = useState(''); - - const preferredUsername = UserService.getPreferredUsername(); - const userEmail = UserService.getUserEmail(); - - const filterOperatorMappings: FilterOperatorMapping = { - Is: { id: 'equals', requires_value: true }, - 'Is Not': { id: 'not_equals', requires_value: true }, - Contains: { id: 'contains', requires_value: true }, - 'Is Empty': { id: 'is_empty', requires_value: false }, - 'Is Not Empty': { id: 'is_not_empty', requires_value: false }, - }; - - const filterDisplayTypes: FilterDisplayTypeMapping = { - date_time: 'Date / time', - duration: 'Duration', - }; - - const processInstanceListPathPrefix = - variant === 'all' ? '/process-instances/all' : '/process-instances/for-me'; - const processInstanceShowPathPrefix = - variant === 'all' ? '/process-instances' : '/process-instances/for-me'; - - const [processStatusAllOptions, setProcessStatusAllOptions] = useState( - [] - ); - const [processStatusSelection, setProcessStatusSelection] = useState< - string[] - >([]); - const [processModelAvailableItems, setProcessModelAvailableItems] = useState< - ProcessModel[] - >([]); - const [processModelSelection, setProcessModelSelection] = - useState(null); - const [processInstanceReportSelection, setProcessInstanceReportSelection] = - useState(null); - - const [availableReportColumns, setAvailableReportColumns] = useState< - ReportColumn[] - >([]); - const [processInstanceReportJustSaved, setProcessInstanceReportJustSaved] = - useState(null); - const [showReportColumnForm, setShowReportColumnForm] = - useState(false); - const [reportColumnToOperateOn, setReportColumnToOperateOn] = - useState(null); - const [reportColumnFormMode, setReportColumnFormMode] = useState(''); - - const [processInstanceInitiatorOptions, setProcessInstanceInitiatorOptions] = - useState([]); - const [processInitiatorSelection, setProcessInitiatorSelection] = useState< - string | null - >(null); - - const [showAdvancedOptions, setShowAdvancedOptions] = - useState(false); - const [withOldestOpenTask, setWithOldestOpenTask] = - useState(showActionsColumn); - const [withRelationToMe, setwithRelationToMe] = - useState(showActionsColumn); - const [systemReport, setSystemReport] = useState(null); - const [selectedUserGroup, setSelectedUserGroup] = useState( - null - ); - const [userGroups, setUserGroups] = useState([]); - const systemReportOptions: string[] = useMemo(() => { - return [ - 'instances_with_tasks_waiting_for_me', - 'instances_with_tasks_completed_by_me', - ]; - }, []); + const [ + reportMetadataFromProcessInstances, + setreportMetadataFromProcessInstances, + ] = useState(null); // this is used from pages like the home page that have multiple tables // and cannot store the report hash in the query params. // it can be used to create a link to the process instances list page to reconstruct the report. const [reportHash, setReportHash] = useState(null); + const preferredUsername = UserService.getPreferredUsername(); + const userEmail = UserService.getUserEmail(); + const processInstanceShowPathPrefix = + variant === 'all' ? '/process-instances' : '/process-instances/for-me'; - const [ - processInitiatorNotFoundErrorText, - setProcessInitiatorNotFoundErrorText, - ] = useState(''); - - const lastRequestedInitatorSearchTerm = useRef(); - - const dateParametersToAlwaysFilterBy: dateParameters = useMemo(() => { - return { - start_from: [setStartFromDate, setStartFromTime], - start_to: [setStartToDate, setStartToTime], - end_from: [setEndFromDate, setEndFromTime], - end_to: [setEndToDate, setEndToTime], - }; - }, [ - setStartFromDate, - setStartFromTime, - setStartToDate, - setStartToTime, - setEndFromDate, - setEndFromTime, - setEndToDate, - setEndToTime, - ]); - - const handleProcessInstanceInitiatorSearchResult = ( - result: any, - inputText: string - ) => { - if (lastRequestedInitatorSearchTerm.current === result.username_prefix) { - setProcessInstanceInitiatorOptions( - result.users.map((user: User) => user.username) - ); - result.users.forEach((user: User) => { - if (user.username === inputText) { - setProcessInitiatorSelection(user.username); - } - }); - } - }; - - const searchForProcessInitiator = (inputText: string) => { - if (inputText && canSearchUsers) { - lastRequestedInitatorSearchTerm.current = inputText; - HttpService.makeCallToBackend({ - path: `/users/search?username_prefix=${inputText}`, - successCallback: (result: any) => - handleProcessInstanceInitiatorSearchResult(result, inputText), - }); - } - }; - - const addDebouncedSearchProcessInitiator = useDebouncedCallback( - (value: string) => { - searchForProcessInitiator(value); - }, - // delay in ms - 250 - ); - - const setProcessInstancesFromResult = useCallback((result: any) => { - setRequiresRefilter(false); - const processInstancesFromApi = result.results; - setProcessInstances(processInstancesFromApi); - setPagination(result.pagination); - - setReportMetadata(result.report_metadata); - if (result.report_hash) { - setReportHash(result.report_hash); - } - }, []); - - // only called when the apply button is clicked - const setProcessInstancesFromApplyFilter = (result: any) => { - setProcessInstancesFromResult(result); - if (result.report_hash) { - searchParams.set('report_hash', result.report_hash); - // whenever apply button is clicked, we want to reset the page to 1, - // since the user has changed the filters - if (searchParams.get('page') !== '1') { - searchParams.set('page', '1'); - } - setSearchParams(searchParams); - } - }; + // eslint-disable-next-line sonarjs/no-duplicate-string + let processInstanceApiSearchPath = '/process-instances/for-me'; + if (variant === 'all') { + processInstanceApiSearchPath = '/process-instances'; + } // Useful to stop refreshing if an api call gets an error // since those errors can make the page unusable in any way @@ -334,137 +113,29 @@ export default function ProcessInstanceListTable({ } }, []); - // we apparently cannot use a state set in a useEffect from within that same useEffect - // so use a variable instead - const processModelSelectionItemsForUseEffect = useRef([]); - - const clearFilters = useCallback((updateRequiresRefilter: boolean = true) => { - setProcessModelSelection(null); - setProcessStatusSelection([]); - setStartFromDate(''); - setStartFromTime(''); - setStartToDate(''); - setStartToTime(''); - setEndFromDate(''); - setEndFromTime(''); - setEndToDate(''); - setEndToTime(''); - setProcessInitiatorSelection(null); - setWithOldestOpenTask(false); - setwithRelationToMe(false); - setSystemReport(null); - setSelectedUserGroup(null); - if (updateRequiresRefilter) { - setRequiresRefilter(true); - } - if (reportMetadata) { - reportMetadata.filter_by = []; - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + const setProcessInstancesFromResult = useCallback( + (result: any) => { + const processInstancesFromApi = result.results; + setProcessInstances(processInstancesFromApi); + setPagination(result.pagination); + setreportMetadataFromProcessInstances(result.report_metadata); + setReportHash(result.report_hash); + if (onProcessInstanceTableListUpdate) { + onProcessInstanceTableListUpdate(result); + } + }, + [onProcessInstanceTableListUpdate] + ); const getProcessInstances = useCallback( - ( - processInstanceReport: ProcessInstanceReport | null = null - // eslint-disable-next-line sonarjs/cognitive-complexity - ) => { - let reportMetadataBodyToUse: ReportMetadata = { + (reportMetadataArg: ReportMetadata | undefined | null = reportMetadata) => { + let reportMetadataToUse: ReportMetadata = { columns: [], filter_by: [], order_by: [], }; - if (processInstanceReport) { - reportMetadataBodyToUse = processInstanceReport.report_metadata; - if (processInstanceReport.id > 0) { - setProcessInstanceReportSelection(processInstanceReport); - } - } - if (additionalReportFilters) { - additionalReportFilters.forEach((arf: ReportFilter) => { - if (!reportMetadataBodyToUse.filter_by.includes(arf)) { - reportMetadataBodyToUse.filter_by.push(arf); - } - }); - } - - // If the showActionColumn is set to true, we need to include the with_oldest_open_task in the query params - if ( - showActionsColumn && - !reportMetadataBodyToUse.filter_by.some( - (rf: ReportFilter) => rf.field_name === 'with_oldest_open_task' - ) - ) { - const withOldestReportFilter = { - field_name: 'with_oldest_open_task', - field_value: true, - }; - reportMetadataBodyToUse.filter_by.push(withOldestReportFilter); - } - - // a bit hacky, clear out all filters before setting them from report metadata - // to ensure old filters are cleared out. - // this is really for going between the 'For Me' and 'All' tabs. - clearFilters(false); - - // this is the code to re-populate the widgets on the page - // with values from the report metadata, which is derived - // from the searchParams (often report_hash) - reportMetadataBodyToUse.filter_by.forEach( - (reportFilter: ReportFilter) => { - if (reportFilter.field_name === 'process_status') { - setProcessStatusSelection( - (reportFilter.field_value || '').split(',') - ); - } else if (reportFilter.field_name === 'process_initiator_username') { - setProcessInitiatorSelection(reportFilter.field_value || ''); - } else if (reportFilter.field_name === 'with_oldest_open_task') { - setWithOldestOpenTask(reportFilter.field_value); - } else if (reportFilter.field_name === 'with_relation_to_me') { - setwithRelationToMe(reportFilter.field_value); - } else if (reportFilter.field_name === 'user_group_identifier') { - setSelectedUserGroup(reportFilter.field_value); - } else if (systemReportOptions.includes(reportFilter.field_name)) { - setSystemReport(reportFilter.field_name); - } else if (reportFilter.field_name === 'process_model_identifier') { - if (reportFilter.field_value) { - processModelSelectionItemsForUseEffect.current.forEach( - (processModel: ProcessModel) => { - if (processModel.id === reportFilter.field_value) { - setProcessModelSelection(processModel); - } - } - ); - } - } else if (dateParametersToAlwaysFilterBy[reportFilter.field_name]) { - const dateFunctionToCall = - dateParametersToAlwaysFilterBy[reportFilter.field_name][0]; - const timeFunctionToCall = - dateParametersToAlwaysFilterBy[reportFilter.field_name][1]; - if (reportFilter.field_value) { - const dateString = - DateAndTimeService.convertSecondsToFormattedDateString( - reportFilter.field_value as any - ); - dateFunctionToCall(dateString); - const timeString = - DateAndTimeService.convertSecondsToFormattedTimeHoursMinutes( - reportFilter.field_value as any - ); - timeFunctionToCall(timeString); - } - } - } - ); - - if (reportMetadataBodyToUse.filter_by.length > 1) { - setShowFilterOptions(true); - } - - if (filtersEnabled) { - HttpService.makeCallToBackend({ - path: `/user-groups/for-current-user`, - successCallback: setUserGroups, - }); + if (reportMetadataArg) { + reportMetadataToUse = reportMetadataArg; } // eslint-disable-next-line prefer-const @@ -478,6 +149,13 @@ export default function ProcessInstanceListTable({ // eslint-disable-next-line prefer-destructuring perPage = perPageOptions[1]; } + if (additionalReportFilters) { + additionalReportFilters.forEach((arf: ReportFilter) => { + if (!reportMetadataToUse.filter_by.includes(arf)) { + reportMetadataToUse.filter_by.push(arf); + } + }); + } const queryParamString = `per_page=${perPage}&page=${page}`; HttpService.makeCallToBackend({ @@ -487,1169 +165,66 @@ export default function ProcessInstanceListTable({ failureCallback: stopRefreshing, onUnauthorized: stopRefreshing, postBody: { - report_metadata: reportMetadataBodyToUse, + report_metadata: reportMetadataToUse, }, }); }, [ additionalReportFilters, - dateParametersToAlwaysFilterBy, - filtersEnabled, paginationQueryParamPrefix, perPageOptions, processInstanceApiSearchPath, + reportMetadata, searchParams, setProcessInstancesFromResult, stopRefreshing, - systemReportOptions, - showActionsColumn, - clearFilters, ] ); useEffect(() => { - if (!permissionsLoaded) { - return undefined; - } - function getReportMetadataWithReportHash() { - const queryParams: string[] = []; - ['report_hash', 'report_id'].forEach((paramName: string) => { - if (searchParams.get(paramName)) { - queryParams.push(`${paramName}=${searchParams.get(paramName)}`); - } - }); + const setReportMetadataFromReport = ( + processInstanceReport: ProcessInstanceReport + ) => { + getProcessInstances(processInstanceReport.report_metadata); + }; + const checkForReportAndRun = () => { if (reportIdentifier) { - queryParams.push(`report_identifier=${reportIdentifier}`); - } - - if (queryParams.length > 0) { - const queryParamString = `?${queryParams.join('&')}`; + const queryParamString = `?report_identifier=${reportIdentifier}`; HttpService.makeCallToBackend({ path: `/process-instances/report-metadata${queryParamString}`, - successCallback: getProcessInstances, - onUnauthorized: stopRefreshing, + successCallback: setReportMetadataFromReport, + onUnauthorized: () => stopRefreshing, }); } else { getProcessInstances(); } - } - function processResultForProcessModels(result: any) { - const selectionArray = result.results.map((item: any) => { - const label = `${item.id}`; - Object.assign(item, { label }); - return item; - }); - processModelSelectionItemsForUseEffect.current = selectionArray; - setProcessModelAvailableItems(selectionArray); - - const processStatusAllOptionsArray = PROCESS_STATUSES.map( - (processStatusOption: any) => { - return processStatusOption; - } - ); - setProcessStatusAllOptions(processStatusAllOptionsArray); - - getReportMetadataWithReportHash(); - } - const checkFiltersAndRun = () => { - if (filtersEnabled) { - // populate process model selection - HttpService.makeCallToBackend({ - path: `/process-models?per_page=1000&recursive=true&include_parent_groups=true`, - successCallback: processResultForProcessModels, - }); - } else { - getReportMetadataWithReportHash(); - } }; + checkForReportAndRun(); - checkFiltersAndRun(); if (autoReload) { clearRefreshRef.current = refreshAtInterval( DateAndTimeService.REFRESH_INTERVAL_SECONDS, DateAndTimeService.REFRESH_TIMEOUT_SECONDS, - checkFiltersAndRun + checkForReportAndRun ); return clearRefreshRef.current; } return undefined; }, [ autoReload, - filtersEnabled, getProcessInstances, - permissionsLoaded, reportIdentifier, - searchParams, + reportMetadata, stopRefreshing, ]); - const processInstanceReportSaveTag = () => { - if (processInstanceReportJustSaved) { - let titleOperation = 'Updated'; - if (processInstanceReportJustSaved === 'new') { - titleOperation = 'Created'; - } - return ( - setProcessInstanceReportJustSaved(null)} - > - {`'${ - processInstanceReportSelection - ? processInstanceReportSelection.identifier - : '' - }'`} - - ); - } - return null; - }; - - // does the comparison, but also returns false if either argument - // is not truthy and therefore not comparable. - const isTrueComparison = (param1: any, operation: any, param2: any) => { - if (param1 && param2) { - switch (operation) { - case '<': - return param1 < param2; - case '>': - return param1 > param2; - default: - return false; - } - } else { - return false; - } - }; - - // jasquat/burnettk - 2022-12-28 do not check the validity of the dates when rendering components to avoid the page being - // re-rendered while the user is still typing. NOTE that we also prevented rerendering - // with the use of the setErrorMessageSafely function. we are not sure why the context not - // changing still causes things to rerender when we call its setter without our extra check. - const calculateStartAndEndSeconds = (validate: boolean = true) => { - const startFromSeconds = - DateAndTimeService.convertDateAndTimeStringsToSeconds( - startFromDate, - startFromTime || '00:00:00' - ); - const startToSeconds = - DateAndTimeService.convertDateAndTimeStringsToSeconds( - startToDate, - startToTime || '00:00:00' - ); - const endFromSeconds = - DateAndTimeService.convertDateAndTimeStringsToSeconds( - endFromDate, - endFromTime || '00:00:00' - ); - const endToSeconds = DateAndTimeService.convertDateAndTimeStringsToSeconds( - endToDate, - endToTime || '00:00:00' - ); - let valid = true; - - if (validate) { - let message = ''; - if (isTrueComparison(startFromSeconds, '>', startToSeconds)) { - message = '"Start date from" cannot be after "start date to"'; - } - if (isTrueComparison(endFromSeconds, '>', endToSeconds)) { - message = '"End date from" cannot be after "end date to"'; - } - if (isTrueComparison(startFromSeconds, '>', endFromSeconds)) { - message = '"Start date from" cannot be after "end date from"'; - } - if (isTrueComparison(startToSeconds, '>', endToSeconds)) { - message = '"Start date to" cannot be after "end date to"'; - } - if (message !== '') { - valid = false; - addError({ message } as ErrorForDisplay); - } - } - - return { - valid, - startFromSeconds, - startToSeconds, - endFromSeconds, - endToSeconds, - }; - }; - const reportColumns = () => { - if (reportMetadata) { - return reportMetadata.columns; + if (reportMetadataFromProcessInstances) { + return reportMetadataFromProcessInstances.columns; } return []; }; - const removeFieldFromReportMetadata = ( - reportMetadataToUse: ReportMetadata, - fieldName: string - ) => { - const filtersToKeep = reportMetadataToUse.filter_by.filter( - (rf: ReportFilter) => rf.field_name !== fieldName - ); - // eslint-disable-next-line no-param-reassign - reportMetadataToUse.filter_by = filtersToKeep; - }; - - const getFilterByFromReportMetadata = (reportColumnAccessor: string) => { - if (reportMetadata) { - return reportMetadata.filter_by.find((reportFilter: ReportFilter) => { - return reportColumnAccessor === reportFilter.field_name; - }); - } - return null; - }; - - const insertOrUpdateFieldInReportMetadata = ( - reportMetadataToUse: ReportMetadata, - fieldName: string, - fieldValue: any - ) => { - if (fieldValue) { - let existingReportFilter = getFilterByFromReportMetadata(fieldName); - if (existingReportFilter) { - existingReportFilter.field_value = fieldValue; - } else { - existingReportFilter = { - field_name: fieldName, - field_value: fieldValue, - operator: 'equals', - }; - reportMetadataToUse.filter_by.push(existingReportFilter); - } - } else { - removeFieldFromReportMetadata(reportMetadataToUse, fieldName); - } - }; - - const getNewReportMetadataBasedOnPageWidgets = () => { - const { - valid, - startFromSeconds, - startToSeconds, - endFromSeconds, - endToSeconds, - } = calculateStartAndEndSeconds(); - - if (!valid) { - return null; - } - - let newReportMetadata: ReportMetadata | null = null; - if (reportMetadata) { - newReportMetadata = { ...reportMetadata }; - } - if (!newReportMetadata) { - newReportMetadata = { - columns: [], - filter_by: [], - order_by: [], - }; - } - - insertOrUpdateFieldInReportMetadata( - newReportMetadata, - 'start_from', - startFromSeconds - ); - insertOrUpdateFieldInReportMetadata( - newReportMetadata, - 'start_to', - startToSeconds - ); - insertOrUpdateFieldInReportMetadata( - newReportMetadata, - 'end_from', - endFromSeconds - ); - insertOrUpdateFieldInReportMetadata( - newReportMetadata, - 'end_to', - endToSeconds - ); - - insertOrUpdateFieldInReportMetadata( - newReportMetadata, - 'process_status', - processStatusSelection.length > 0 - ? processStatusSelection.join(',') - : null - ); - insertOrUpdateFieldInReportMetadata( - newReportMetadata, - 'process_model_identifier', - processModelSelection ? processModelSelection.id : null - ); - insertOrUpdateFieldInReportMetadata( - newReportMetadata, - 'process_initiator_username', - processInitiatorSelection - ); - - insertOrUpdateFieldInReportMetadata( - newReportMetadata, - 'with_oldest_open_task', - withOldestOpenTask - ); - insertOrUpdateFieldInReportMetadata( - newReportMetadata, - 'with_relation_to_me', - withRelationToMe - ); - insertOrUpdateFieldInReportMetadata( - newReportMetadata, - 'user_group_identifier', - selectedUserGroup - ); - systemReportOptions.forEach((systemReportOption: string) => { - if (newReportMetadata) { - insertOrUpdateFieldInReportMetadata( - newReportMetadata, - systemReportOption, - systemReport === systemReportOption - ); - } - }); - - return newReportMetadata; - }; - - const applyFilter = (event: any) => { - event.preventDefault(); - setProcessInitiatorNotFoundErrorText(''); - - // eslint-disable-next-line prefer-const - let { page, perPage } = getPageInfoFromSearchParams( - searchParams, - undefined, - undefined, - paginationQueryParamPrefix - ); - page = 1; - - const newReportMetadata = getNewReportMetadataBasedOnPageWidgets(); - setReportMetadata(newReportMetadata); - - const queryParamString = `per_page=${perPage}&page=${page}`; - HttpService.makeCallToBackend({ - path: `${processInstanceApiSearchPath}?${queryParamString}`, - httpMethod: 'POST', - postBody: { report_metadata: newReportMetadata }, - failureCallback: stopRefreshing, - onUnauthorized: stopRefreshing, - successCallback: (result: any) => { - setProcessInstancesFromApplyFilter(result); - }, - }); - }; - - const dateComponent = ( - labelString: any, - name: any, - initialDate: any, - initialTime: string, - onChangeDateFunction: any, - onChangeTimeFunction: any, - timeInvalid: boolean, - setTimeInvalid: any - ) => { - return ( - <> - - { - if (!initialDate && !initialTime) { - onChangeTimeFunction( - DateAndTimeService.convertDateObjectToFormattedHoursMinutes( - new Date() - ) - ); - } - onChangeDateFunction(dateChangeEvent.srcElement.value); - }} - value={initialDate} - /> - - { - if (event.srcElement.validity.valid) { - setTimeInvalid(false); - } else { - setTimeInvalid(true); - } - onChangeTimeFunction(event.srcElement.value); - }} - /> - - ); - }; - - const formatProcessInstanceStatus = (_row: any, value: any) => { - return titleizeString((value || '').replaceAll('_', ' ')); - }; - - const processStatusSearch = () => { - return ( - { - setProcessStatusSelection(selection.selectedItems); - setRequiresRefilter(true); - }} - itemToString={(item: any) => { - return formatProcessInstanceStatus(null, item); - }} - selectionFeedback="top-after-reopen" - selectedItems={processStatusSelection} - /> - ); - }; - - const processInstanceReportDidChange = (selection: any, mode?: string) => { - clearFilters(); - const selectedReport = selection.selectedItem; - setProcessInstanceReportSelection(selectedReport); - removeError(); - setProcessInstanceReportJustSaved(mode || null); - - let queryParamString = ''; - if (selectedReport) { - queryParamString = `?report_id=${selectedReport.id}`; - - HttpService.makeCallToBackend({ - path: `/process-instances/report-metadata${queryParamString}`, - successCallback: getProcessInstances, - }); - } - navigate(`${processInstanceListPathPrefix}${queryParamString}`); - }; - - const reportColumnAccessors = () => { - return reportColumns().map((reportColumn: ReportColumn) => { - return reportColumn.accessor; - }); - }; - - const onSaveReportSuccess = ( - processInstanceReport: ProcessInstanceReport - ) => { - setProcessInstanceReportSelection(processInstanceReport); - searchParams.set('report_id', processInstanceReport.id.toString()); - setSearchParams(searchParams); - }; - - const saveAsReportComponent = () => { - return ( - - ); - }; - - const onDeleteReportSuccess = () => { - processInstanceReportDidChange({ selectedItem: null }); - }; - - const deleteReportComponent = () => { - return processInstanceReportSelection ? ( - - ) : null; - }; - - const removeColumn = (reportColumn: ReportColumn) => { - if (reportMetadata) { - const reportMetadataCopy = { ...reportMetadata }; - const newColumns = reportColumns().filter( - (rc: ReportColumn) => rc.accessor !== reportColumn.accessor - ); - Object.assign(reportMetadataCopy, { columns: newColumns }); - const newFilters = reportMetadataCopy.filter_by.filter( - (rf: ReportFilter) => rf.field_name !== reportColumn.accessor - ); - Object.assign(reportMetadataCopy, { - columns: newColumns, - filter_by: newFilters, - }); - setReportMetadata(reportMetadataCopy); - setRequiresRefilter(true); - } - }; - - const handleColumnFormClose = () => { - setShowReportColumnForm(false); - setReportColumnFormMode(''); - setReportColumnToOperateOn(null); - }; - - const getFilterOperatorFromReportColumn = ( - reportColumnForEditing: ReportColumnForEditing - ) => { - if (reportColumnForEditing.filter_operator) { - // eslint-disable-next-line prefer-destructuring - return Object.entries(filterOperatorMappings).filter(([_key, value]) => { - return value.id === reportColumnForEditing.filter_operator; - })[0][1]; - } - return null; - }; - - const getNewFiltersFromReportForEditing = ( - reportColumnForEditing: ReportColumnForEditing - ) => { - if (!reportMetadata) { - return null; - } - const reportMetadataCopy = { ...reportMetadata }; - let newReportFilters = reportMetadataCopy.filter_by; - if (reportColumnForEditing.filterable) { - const newReportFilter: ReportFilter = { - field_name: reportColumnForEditing.accessor, - field_value: reportColumnForEditing.filter_field_value, - operator: reportColumnForEditing.filter_operator || 'equals', - }; - const existingReportFilter = getFilterByFromReportMetadata( - reportColumnForEditing.accessor - ); - const filterOperator = getFilterOperatorFromReportColumn( - reportColumnForEditing - ); - if (existingReportFilter) { - const existingReportFilterIndex = - reportMetadataCopy.filter_by.indexOf(existingReportFilter); - if (filterOperator && !filterOperator.requires_value) { - newReportFilter.field_value = ''; - newReportFilters[existingReportFilterIndex] = newReportFilter; - } else if (reportColumnForEditing.filter_field_value) { - newReportFilters[existingReportFilterIndex] = newReportFilter; - } else { - newReportFilters.splice(existingReportFilterIndex, 1); - } - } else if (filterOperator && !filterOperator.requires_value) { - newReportFilter.field_value = ''; - newReportFilters = newReportFilters.concat([newReportFilter]); - } else if (reportColumnForEditing.filter_field_value) { - newReportFilters = newReportFilters.concat([newReportFilter]); - } - } - return newReportFilters; - }; - - const handleUpdateReportColumn = () => { - if (reportColumnToOperateOn && reportMetadata) { - const reportMetadataCopy = { ...reportMetadata }; - let newReportColumns = null; - if (reportColumnFormMode === 'new') { - newReportColumns = reportColumns().concat([reportColumnToOperateOn]); - } else { - newReportColumns = reportColumns().map((rc: ReportColumn) => { - if (rc.accessor === reportColumnToOperateOn.accessor) { - return reportColumnToOperateOn; - } - return rc; - }); - } - Object.assign(reportMetadataCopy, { - columns: newReportColumns, - filter_by: getNewFiltersFromReportForEditing(reportColumnToOperateOn), - }); - setReportMetadata(reportMetadataCopy); - setReportColumnToOperateOn(null); - setShowReportColumnForm(false); - setRequiresRefilter(true); - } - }; - - const reportColumnToReportColumnForEditing = (reportColumn: ReportColumn) => { - const reportColumnForEditing: ReportColumnForEditing = Object.assign( - reportColumn, - { filter_field_value: '', filter_operator: '' } - ); - if (reportColumn.filterable) { - const reportFilter = getFilterByFromReportMetadata( - reportColumnForEditing.accessor - ); - if (reportFilter) { - reportColumnForEditing.filter_field_value = - reportFilter.field_value || ''; - reportColumnForEditing.filter_operator = - reportFilter.operator || 'equals'; - } - } - return reportColumnForEditing; - }; - - const updateReportColumn = (event: any) => { - let reportColumnForEditing = null; - if (event.selectedItem) { - reportColumnForEditing = reportColumnToReportColumnForEditing( - event.selectedItem - ); - } - setReportColumnToOperateOn(reportColumnForEditing); - setRequiresRefilter(true); - }; - - // options includes item and inputValue - const shouldFilterReportColumn = (options: any) => { - const reportColumn: ReportColumn = options.item; - const { inputValue } = options; - return ( - !reportColumnAccessors().includes(reportColumn.accessor) && - (reportColumn.accessor || '') - .toLowerCase() - .includes((inputValue || '').toLowerCase()) - ); - }; - - const setReportColumnConditionValue = (event: any) => { - if (reportColumnToOperateOn) { - const reportColumnToOperateOnCopy = { - ...reportColumnToOperateOn, - }; - reportColumnToOperateOnCopy.filter_field_value = event.target.value; - setReportColumnToOperateOn(reportColumnToOperateOnCopy); - setRequiresRefilter(true); - } - }; - - const setReportColumnConditionOperator = (selectedItem: string) => { - if (reportColumnToOperateOn) { - const reportColumnToOperateOnCopy = { - ...reportColumnToOperateOn, - }; - const filterOperator = filterOperatorMappings[selectedItem]; - reportColumnToOperateOnCopy.filter_operator = filterOperator.id; - setReportColumnToOperateOn(reportColumnToOperateOnCopy); - setRequiresRefilter(true); - } - }; - - const setFilterDisplayType = (selectedItem: string) => { - if (reportColumnToOperateOn) { - const reportColumnToOperateOnCopy = { - ...reportColumnToOperateOn, - }; - const displayType = getKeyByValue(filterDisplayTypes, selectedItem); - reportColumnToOperateOnCopy.display_type = displayType; - setReportColumnToOperateOn(reportColumnToOperateOnCopy); - setRequiresRefilter(true); - } - }; - - // eslint-disable-next-line sonarjs/cognitive-complexity - const reportColumnForm = () => { - if (reportColumnFormMode === '') { - return null; - } - const formElements = []; - if (reportColumnFormMode === 'new') { - formElements.push( - { - if (reportColumn) { - return reportColumn.accessor; - } - return null; - }} - shouldFilterItem={shouldFilterReportColumn} - placeholder="Choose a column to show" - titleText="Column" - selectedItem={reportColumnToOperateOn} - /> - ); - } - formElements.push([ - { - if (reportColumnToOperateOn) { - setRequiresRefilter(true); - const reportColumnToOperateOnCopy = { - ...reportColumnToOperateOn, - }; - reportColumnToOperateOnCopy.Header = event.target.value; - setReportColumnToOperateOn(reportColumnToOperateOnCopy); - } - }} - />, - ]); - if (reportColumnToOperateOn && reportColumnToOperateOn.filterable) { - formElements.push( - { - setFilterDisplayType(value.selectedItem); - setRequiresRefilter(true); - }} - /> - ); - formElements.push( - { - setReportColumnConditionOperator(value.selectedItem); - setRequiresRefilter(true); - }} - /> - ); - - const filterOperator = getFilterOperatorFromReportColumn( - reportColumnToOperateOn - ); - if (filterOperator && filterOperator.requires_value) { - formElements.push( - - ); - } - } - formElements.push( -
- ); - const modalHeading = - reportColumnFormMode === 'new' - ? 'Add Column' - : `Edit ${ - reportColumnToOperateOn ? reportColumnToOperateOn.accessor : '' - } column`; - return ( - - {formElements} - - ); - }; - - const columnSelections = () => { - if (reportColumns().length > 0) { - const tags: any = []; - reportColumns().forEach((reportColumn: ReportColumn) => { - const reportColumnForEditing = - reportColumnToReportColumnForEditing(reportColumn); - - let tagType = 'cool-gray'; - let tagTypeClass = ''; - if (reportColumnForEditing.filterable) { - tagType = 'green'; - tagTypeClass = 'tag-type-green'; - } - let reportColumnLabel = reportColumnForEditing.Header; - if (reportColumnForEditing.filter_field_value) { - reportColumnLabel = `${reportColumnLabel}=${reportColumnForEditing.filter_field_value}`; - } - tags.push( - - - - - - - - - {saveAsReportComponent()} - {deleteReportComponent()} - - - - - - - ); - }; - const getWaitingForTableCellComponent = (processInstanceTask: any) => { let fullUsernameString = ''; let shortUsernameString = ''; @@ -1695,13 +270,16 @@ export default function ProcessInstanceListTable({ return {truncatedValue}; }; + const formatProcessInstanceStatus = (_row: any, value: any) => { + return titleizeString((value || '').replaceAll('_', ' ')); + }; + const formatSecondsForDisplay = (_row: ProcessInstance, seconds: any) => { return DateAndTimeService.convertSecondsToFormattedDateTime(seconds) || '-'; }; const defaultFormatter = (_row: ProcessInstance, value: any) => { return value; }; - const formattedColumn = (row: ProcessInstance, column: ReportColumn) => { const reportColumnFormatters: Record = { id: formatProcessInstanceId, @@ -1759,6 +337,75 @@ export default function ProcessInstanceListTable({ ); }; + const tableTitleLine = () => { + if (!showLinkToReport && !header) { + return null; + } + + let filterButtonLink = null; + if (showLinkToReport && pagination && pagination.total) { + filterButtonLink = ( + +