From 902c3b7e322b4c35153f4c2baac47719eb1fd788 Mon Sep 17 00:00:00 2001 From: jasquat Date: Mon, 14 Nov 2022 12:06:39 -0500 Subject: [PATCH 1/4] added remaining task tables w/ burnettk --- src/components/TasksForMyOpenProcesses.tsx | 2 +- ...rtedByOthers.tsx => TasksWaitingForMe.tsx} | 6 +- src/components/TasksWaitingForMyGroups.tsx | 139 ++++++++++++++++++ src/routes/GroupedTasks.tsx | 7 +- 4 files changed, 148 insertions(+), 6 deletions(-) rename src/components/{MyTasksForProcessesStartedByOthers.tsx => TasksWaitingForMe.tsx} (95%) create mode 100644 src/components/TasksWaitingForMyGroups.tsx diff --git a/src/components/TasksForMyOpenProcesses.tsx b/src/components/TasksForMyOpenProcesses.tsx index 8a2f0bd..5c3a0ec 100644 --- a/src/components/TasksForMyOpenProcesses.tsx +++ b/src/components/TasksForMyOpenProcesses.tsx @@ -124,7 +124,7 @@ export default function MyOpenProcesses() { perPageOptions={[2, PER_PAGE_FOR_TASKS_ON_HOME_PAGE, 25]} pagination={pagination} tableToDisplay={buildTable()} - path="/tasks/for-my-open-processes" + path="/tasks/grouped" /> ); diff --git a/src/components/MyTasksForProcessesStartedByOthers.tsx b/src/components/TasksWaitingForMe.tsx similarity index 95% rename from src/components/MyTasksForProcessesStartedByOthers.tsx rename to src/components/TasksWaitingForMe.tsx index d1fafd3..a24eeb8 100644 --- a/src/components/MyTasksForProcessesStartedByOthers.tsx +++ b/src/components/TasksWaitingForMe.tsx @@ -13,7 +13,7 @@ import { PaginationObject } from '../interfaces'; const PER_PAGE_FOR_TASKS_ON_HOME_PAGE = 5; -export default function MyTasksForProcessesStartedByOthers() { +export default function TasksWaitingForMe() { const [searchParams] = useSearchParams(); const [tasks, setTasks] = useState([]); const [pagination, setPagination] = useState(null); @@ -28,7 +28,7 @@ export default function MyTasksForProcessesStartedByOthers() { setPagination(result.pagination); }; HttpService.makeCallToBackend({ - path: `/tasks/for-processes-started-by-others?per_page=${perPage}&page=${page}`, + path: `/tasks/for-me?per_page=${perPage}&page=${page}`, successCallback: setTasksFromResult, }); }, [searchParams]); @@ -126,7 +126,7 @@ export default function MyTasksForProcessesStartedByOthers() { perPageOptions={[2, PER_PAGE_FOR_TASKS_ON_HOME_PAGE, 25]} pagination={pagination} tableToDisplay={buildTable()} - path="/tasks/for-my-open-processes" + path="/tasks/grouped" /> ); diff --git a/src/components/TasksWaitingForMyGroups.tsx b/src/components/TasksWaitingForMyGroups.tsx new file mode 100644 index 0000000..6190e0e --- /dev/null +++ b/src/components/TasksWaitingForMyGroups.tsx @@ -0,0 +1,139 @@ +import { useEffect, useState } from 'react'; +// @ts-ignore +import { Button, Table } from '@carbon/react'; +import { Link, useSearchParams } from 'react-router-dom'; +import PaginationForTable from './PaginationForTable'; +import { + convertSecondsToFormattedDateTime, + getPageInfoFromSearchParams, + modifyProcessModelPath, +} from '../helpers'; +import HttpService from '../services/HttpService'; +import { PaginationObject } from '../interfaces'; + +const PER_PAGE_FOR_TASKS_ON_HOME_PAGE = 5; + +export default function TasksForWaitingForMyGroups() { + const [searchParams] = useSearchParams(); + const [tasks, setTasks] = useState([]); + const [pagination, setPagination] = useState(null); + + useEffect(() => { + const { page, perPage } = getPageInfoFromSearchParams( + searchParams, + PER_PAGE_FOR_TASKS_ON_HOME_PAGE + ); + const setTasksFromResult = (result: any) => { + setTasks(result.results); + setPagination(result.pagination); + }; + HttpService.makeCallToBackend({ + path: `/tasks/for-my-groups?per_page=${perPage}&page=${page}`, + successCallback: setTasksFromResult, + }); + }, [searchParams]); + + const buildTable = () => { + const rows = tasks.map((row) => { + const rowToUse = row as any; + const taskUrl = `/tasks/${rowToUse.process_instance_id}/${rowToUse.task_id}`; + const modifiedProcessModelIdentifier = modifyProcessModelPath( + rowToUse.process_model_identifier + ); + return ( + + + + {rowToUse.process_model_display_name} + + + + + View {rowToUse.process_instance_id} + + + + {rowToUse.task_title} + + {rowToUse.username} + {rowToUse.process_instance_status} + {rowToUse.group_identifier || '-'} + + {convertSecondsToFormattedDateTime( + rowToUse.created_at_in_seconds + ) || '-'} + + + {convertSecondsToFormattedDateTime( + rowToUse.updated_at_in_seconds + ) || '-'} + + + + + + ); + }); + return ( + + + + + + + + + + + + + + + {rows} +
Process ModelProcess InstanceTask NameProcess Started ByProcess Instance StatusAssigned GroupProcess StartedProcess UpdatedActions
+ ); + }; + + const tasksComponent = () => { + if (pagination && pagination.total < 1) { + return null; + } + const { page, perPage } = getPageInfoFromSearchParams( + searchParams, + PER_PAGE_FOR_TASKS_ON_HOME_PAGE + ); + return ( + <> +

Tasks waiting for my groups

+ + + ); + }; + + if (pagination) { + return tasksComponent(); + } + return null; +} diff --git a/src/routes/GroupedTasks.tsx b/src/routes/GroupedTasks.tsx index 7312bc5..a08959c 100644 --- a/src/routes/GroupedTasks.tsx +++ b/src/routes/GroupedTasks.tsx @@ -1,12 +1,15 @@ -import MyTasksForProcessesStartedByOthers from '../components/MyTasksForProcessesStartedByOthers'; import TasksForMyOpenProcesses from '../components/TasksForMyOpenProcesses'; +import TasksWaitingForMe from '../components/TasksWaitingForMe'; +import TasksForWaitingForMyGroups from '../components/TasksWaitingForMyGroups'; export default function GroupedTasks() { return ( <>
- + +
+ ); } From 75a198995c2ee95e3edd685d8abeb3f7c48ef33b Mon Sep 17 00:00:00 2001 From: jasquat Date: Mon, 14 Nov 2022 16:29:04 -0500 Subject: [PATCH 2/4] refactored pagination table to allow prefixing page options w/ burnettk --- src/components/PaginationForTable.tsx | 17 +- src/components/ProcessInstanceListTable.tsx | 541 ++++++++++++++++++++ src/components/TasksForMyOpenProcesses.tsx | 11 +- src/components/TasksWaitingForMe.tsx | 10 +- src/components/TasksWaitingForMyGroups.tsx | 11 +- src/helpers.tsx | 15 +- src/routes/CompletedInstances.tsx | 3 + src/routes/HomePageRoutes.tsx | 7 + src/routes/MessageInstanceList.tsx | 1 - src/routes/MyTasks.tsx | 1 - src/routes/ProcessGroupList.tsx | 1 - src/routes/ProcessGroupShow.tsx | 2 - src/routes/ProcessInstanceList.tsx | 1 - src/routes/ProcessInstanceLogList.tsx | 1 - src/routes/ProcessInstanceReportShow.tsx | 1 - src/routes/SecretList.tsx | 1 - 16 files changed, 597 insertions(+), 27 deletions(-) create mode 100644 src/components/ProcessInstanceListTable.tsx create mode 100644 src/routes/CompletedInstances.tsx diff --git a/src/components/PaginationForTable.tsx b/src/components/PaginationForTable.tsx index 3b65c78..2181395 100644 --- a/src/components/PaginationForTable.tsx +++ b/src/components/PaginationForTable.tsx @@ -1,4 +1,4 @@ -import { useNavigate } from 'react-router-dom'; +import { useNavigate, useSearchParams } from 'react-router-dom'; // @ts-ignore import { Pagination } from '@carbon/react'; @@ -14,7 +14,7 @@ type OwnProps = { pagination: PaginationObject | null; tableToDisplay: any; queryParamString?: string; - path: string; + paginationQueryParamPrefix?: string; }; export default function PaginationForTable({ @@ -23,16 +23,21 @@ export default function PaginationForTable({ perPageOptions, pagination, tableToDisplay, - queryParamString = '', - path, + paginationQueryParamPrefix, }: OwnProps) { const PER_PAGE_OPTIONS = [2, 10, 50, 100]; - const navigate = useNavigate(); + const [searchParams, setSearchParams] = useSearchParams(); + const paginationQueryParamPrefixToUse = paginationQueryParamPrefix + ? `${paginationQueryParamPrefix}_` + : ''; const updateRows = (args: any) => { const newPage = args.page; const { pageSize } = args; - navigate(`${path}?page=${newPage}&per_page=${pageSize}${queryParamString}`); + + searchParams.set(`${paginationQueryParamPrefixToUse}page`, newPage); + searchParams.set(`${paginationQueryParamPrefixToUse}per_page`, pageSize); + setSearchParams(searchParams); }; if (pagination) { diff --git a/src/components/ProcessInstanceListTable.tsx b/src/components/ProcessInstanceListTable.tsx new file mode 100644 index 0000000..f09ae71 --- /dev/null +++ b/src/components/ProcessInstanceListTable.tsx @@ -0,0 +1,541 @@ +import { useContext, useEffect, useMemo, useState } from 'react'; +import { + Link, + useNavigate, + useParams, + useSearchParams, +} from 'react-router-dom'; + +// @ts-ignore +import { Filter } from '@carbon/icons-react'; +import { + Button, + ButtonSet, + DatePicker, + DatePickerInput, + Table, + Grid, + Column, + MultiSelect, + TableHeader, + TableHead, + TableRow, + // @ts-ignore +} from '@carbon/react'; +import { PROCESS_STATUSES, DATE_FORMAT, DATE_FORMAT_CARBON } from '../config'; +import { + convertDateStringToSeconds, + convertSecondsToFormattedDate, + getPageInfoFromSearchParams, + getProcessModelFullIdentifierFromSearchParams, + modifyProcessModelPath, +} from '../helpers'; + +import PaginationForTable from './PaginationForTable'; +import 'react-datepicker/dist/react-datepicker.css'; + +import ErrorContext from '../contexts/ErrorContext'; +import HttpService from '../services/HttpService'; + +import 'react-bootstrap-typeahead/css/Typeahead.css'; +import 'react-bootstrap-typeahead/css/Typeahead.bs5.css'; +import { PaginationObject, ProcessModel } from '../interfaces'; +import ProcessModelSearch from './ProcessModelSearch'; +import ProcessBreadcrumb from './ProcessBreadcrumb'; + +export default function ProcessInstanceList() { + const params = useParams(); + const [searchParams] = useSearchParams(); + const navigate = useNavigate(); + + const [processInstances, setProcessInstances] = useState([]); + const [reportMetadata, setReportMetadata] = useState({}); + const [pagination, setPagination] = useState(null); + + const oneHourInSeconds = 3600; + const oneMonthInSeconds = oneHourInSeconds * 24 * 30; + const [startFrom, setStartFrom] = useState(''); + const [startTo, setStartTo] = useState(''); + const [endFrom, setEndFrom] = useState(''); + const [endTo, setEndTo] = useState(''); + const [showFilterOptions, setShowFilterOptions] = useState(false); + + const setErrorMessage = (useContext as any)(ErrorContext)[1]; + + const [processStatusAllOptions, setProcessStatusAllOptions] = useState( + [] + ); + const [processStatusSelection, setProcessStatusSelection] = useState< + string[] + >([]); + const [processModelAvailableItems, setProcessModelAvailableItems] = useState< + ProcessModel[] + >([]); + const [processModelSelection, setProcessModelSelection] = + useState(null); + + const parametersToAlwaysFilterBy = useMemo(() => { + return { + start_from: setStartFrom, + start_to: setStartTo, + end_from: setEndFrom, + end_to: setEndTo, + }; + }, [setStartFrom, setStartTo, setEndFrom, setEndTo]); + + const parametersToGetFromSearchParams = useMemo(() => { + return { + process_model_identifier: null, + process_status: null, + }; + }, []); + + // eslint-disable-next-line sonarjs/cognitive-complexity + useEffect(() => { + function setProcessInstancesFromResult(result: any) { + const processInstancesFromApi = result.results; + setProcessInstances(processInstancesFromApi); + setReportMetadata(result.report_metadata); + setPagination(result.pagination); + } + function getProcessInstances() { + const { page, perPage } = getPageInfoFromSearchParams(searchParams); + let queryParamString = `per_page=${perPage}&page=${page}`; + + Object.keys(parametersToAlwaysFilterBy).forEach((paramName: string) => { + // @ts-expect-error TS(7053) FIXME: + const functionToCall = parametersToAlwaysFilterBy[paramName]; + const searchParamValue = searchParams.get(paramName); + if (searchParamValue) { + queryParamString += `&${paramName}=${searchParamValue}`; + const dateString = convertSecondsToFormattedDate( + searchParamValue as any + ); + functionToCall(dateString); + setShowFilterOptions(true); + } + }); + + Object.keys(parametersToGetFromSearchParams).forEach( + (paramName: string) => { + if (searchParams.get(paramName)) { + // @ts-expect-error TS(7053) FIXME: + const functionToCall = parametersToGetFromSearchParams[paramName]; + queryParamString += `&${paramName}=${searchParams.get(paramName)}`; + if (functionToCall !== null) { + functionToCall(searchParams.get(paramName) || ''); + } + setShowFilterOptions(true); + } + } + ); + HttpService.makeCallToBackend({ + path: `/process-instances?${queryParamString}`, + successCallback: setProcessInstancesFromResult, + }); + } + function processResultForProcessModels(result: any) { + const processModelFullIdentifier = + getProcessModelFullIdentifierFromSearchParams(searchParams); + const selectionArray = result.results.map((item: any) => { + const label = `${item.id}`; + Object.assign(item, { label }); + if (label === processModelFullIdentifier) { + setProcessModelSelection(item); + } + return item; + }); + setProcessModelAvailableItems(selectionArray); + + const processStatusSelectedArray: string[] = []; + const processStatusAllOptionsArray = PROCESS_STATUSES.map( + (processStatusOption: any) => { + const regex = new RegExp(`\\b${processStatusOption}\\b`); + if ((searchParams.get('process_status') || '').match(regex)) { + processStatusSelectedArray.push(processStatusOption); + } + return processStatusOption; + } + ); + setProcessStatusSelection(processStatusSelectedArray); + setProcessStatusAllOptions(processStatusAllOptionsArray); + + getProcessInstances(); + } + + // populate process model selection + HttpService.makeCallToBackend({ + path: `/process-models?per_page=1000`, + successCallback: processResultForProcessModels, + }); + }, [ + searchParams, + params, + oneMonthInSeconds, + oneHourInSeconds, + parametersToAlwaysFilterBy, + parametersToGetFromSearchParams, + ]); + + // 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; + } + }; + + const applyFilter = (event: any) => { + event.preventDefault(); + const { page, perPage } = getPageInfoFromSearchParams(searchParams); + let queryParamString = `per_page=${perPage}&page=${page}`; + + const startFromSeconds = convertDateStringToSeconds(startFrom); + const endFromSeconds = convertDateStringToSeconds(endFrom); + const startToSeconds = convertDateStringToSeconds(startTo); + const endToSeconds = convertDateStringToSeconds(endTo); + if (isTrueComparison(startFromSeconds, '>', startToSeconds)) { + setErrorMessage({ + message: '"Start date from" cannot be after "start date to"', + }); + return; + } + if (isTrueComparison(endFromSeconds, '>', endToSeconds)) { + setErrorMessage({ + message: '"End date from" cannot be after "end date to"', + }); + return; + } + if (isTrueComparison(startFromSeconds, '>', endFromSeconds)) { + setErrorMessage({ + message: '"Start date from" cannot be after "end date from"', + }); + return; + } + if (isTrueComparison(startToSeconds, '>', endToSeconds)) { + setErrorMessage({ + message: '"Start date to" cannot be after "end date to"', + }); + return; + } + + if (startFromSeconds) { + queryParamString += `&start_from=${startFromSeconds}`; + } + if (startToSeconds) { + queryParamString += `&start_to=${startToSeconds}`; + } + if (endFromSeconds) { + queryParamString += `&end_from=${endFromSeconds}`; + } + if (endToSeconds) { + queryParamString += `&end_to=${endToSeconds}`; + } + if (processStatusSelection.length > 0) { + queryParamString += `&process_status=${processStatusSelection}`; + } + + if (processModelSelection) { + queryParamString += `&process_model_identifier=${processModelSelection.id}`; + } + + setErrorMessage(null); + navigate(`/admin/process-instances?${queryParamString}`); + }; + + const dateComponent = ( + labelString: any, + name: any, + initialDate: any, + onChangeFunction: any + ) => { + return ( + + { + onChangeFunction(dateChangeEvent.srcElement.value); + }} + value={initialDate} + /> + + ); + }; + + const getSearchParamsAsQueryString = () => { + let queryParamString = ''; + Object.keys(parametersToAlwaysFilterBy).forEach((paramName) => { + const searchParamValue = searchParams.get(paramName); + if (searchParamValue) { + queryParamString += `&${paramName}=${searchParamValue}`; + } + }); + + Object.keys(parametersToGetFromSearchParams).forEach( + (paramName: string) => { + if (searchParams.get(paramName)) { + queryParamString += `&${paramName}=${searchParams.get(paramName)}`; + } + } + ); + return queryParamString; + }; + + const processStatusSearch = () => { + return ( + { + setProcessStatusSelection(selection.selectedItems); + }} + itemToString={(item: any) => { + return item || ''; + }} + selectionFeedback="top-after-reopen" + selectedItems={processStatusSelection} + /> + ); + }; + + const clearFilters = () => { + setProcessModelSelection(null); + setProcessStatusSelection([]); + setStartFrom(''); + setStartTo(''); + setEndFrom(''); + setEndTo(''); + }; + + const filterOptions = () => { + if (!showFilterOptions) { + return null; + } + return ( + <> + + + + setProcessModelSelection(selection.selectedItem) + } + processModels={processModelAvailableItems} + selectedItem={processModelSelection} + /> + + {processStatusSearch()} + + + + {dateComponent( + 'Start date from', + 'start-from', + startFrom, + setStartFrom + )} + + + {dateComponent('Start date to', 'start-to', startTo, setStartTo)} + + + {dateComponent('End date from', 'end-from', endFrom, setEndFrom)} + + + {dateComponent('End date to', 'end-to', endTo, setEndTo)} + + + + + + + + + + + + ); + }; + + const buildTable = () => { + const headerLabels: Record = { + id: 'Process Instance Id', + process_model_identifier: 'Process Model', + start_in_seconds: 'Start Time', + end_in_seconds: 'End Time', + status: 'Status', + spiff_step: 'SpiffWorkflow Step', + }; + const getHeaderLabel = (header: string) => { + return headerLabels[header] ?? header; + }; + const headers = (reportMetadata as any).columns.map((column: any) => { + // return {getHeaderLabel((column as any).Header)}; + return getHeaderLabel((column as any).Header); + }); + + const formatProcessInstanceId = (row: any, id: any) => { + const modifiedProcessModelId: String = modifyProcessModelPath( + row.process_model_identifier + ); + return ( + + {id} + + ); + }; + const formatProcessModelIdentifier = (_row: any, identifier: any) => { + return ( + + {identifier} + + ); + }; + const formatSecondsForDisplay = (_row: any, seconds: any) => { + return convertSecondsToFormattedDate(seconds) || '-'; + }; + const defaultFormatter = (_row: any, value: any) => { + return value; + }; + + const columnFormatters: Record = { + id: formatProcessInstanceId, + process_model_identifier: formatProcessModelIdentifier, + start_in_seconds: formatSecondsForDisplay, + end_in_seconds: formatSecondsForDisplay, + }; + const formattedColumn = (row: any, column: any) => { + const formatter = columnFormatters[column.accessor] ?? defaultFormatter; + const value = row[column.accessor]; + if (column.accessor === 'status') { + return ( + + {formatter(row, value)} + + ); + } + return {formatter(row, value)}; + }; + + const rows = processInstances.map((row: any) => { + const currentRow = (reportMetadata as any).columns.map((column: any) => { + return formattedColumn(row, column); + }); + return {currentRow}; + }); + + return ( + + + + {headers.map((header: any) => ( + {header} + ))} + + + {rows} +
+ ); + }; + + const processInstanceBreadcrumbElement = () => { + const processModelFullIdentifier = + getProcessModelFullIdentifierFromSearchParams(searchParams); + if (processModelFullIdentifier === null) { + return null; + } + + return ( + + ); + }; + + const processInstanceTitleElement = () => { + return

Process Instances

; + }; + + const toggleShowFilterOptions = () => { + setShowFilterOptions(!showFilterOptions); + }; + + if (pagination) { + const { page, perPage } = getPageInfoFromSearchParams(searchParams); + return ( + <> + {processInstanceBreadcrumbElement()} + {processInstanceTitleElement()} + + + - - - - - - ); - }; - const toggleShowFilterOptions = () => { - setShowFilterOptions(!showFilterOptions); - }; - const filterComponent = () => { - return ( - <> - - -