From 3276fdb57988cec0e92835faba6f28d9fdf7199f Mon Sep 17 00:00:00 2001 From: jasquat <2487833+jasquat@users.noreply.github.com> Date: Tue, 10 Oct 2023 11:52:59 -0400 Subject: [PATCH] Feature/metadata column display type (#534) * some basics to set a display type for a metadata column when displaying w/ burnettk * added supuport for durations and some clean up * only display hours and days in duration if they are above 0 to keep it a little cleaner --------- Co-authored-by: jasquat --- .../components/ProcessInstanceListTable.tsx | 109 +++++++++++++++++- spiffworkflow-frontend/src/helpers.test.tsx | 14 ++- spiffworkflow-frontend/src/helpers.tsx | 6 +- spiffworkflow-frontend/src/interfaces.ts | 5 + .../src/routes/ProcessInstanceFindById.tsx | 4 +- 5 files changed, 124 insertions(+), 14 deletions(-) diff --git a/spiffworkflow-frontend/src/components/ProcessInstanceListTable.tsx b/spiffworkflow-frontend/src/components/ProcessInstanceListTable.tsx index ffa74e643..4cddd5530 100644 --- a/spiffworkflow-frontend/src/components/ProcessInstanceListTable.tsx +++ b/spiffworkflow-frontend/src/components/ProcessInstanceListTable.tsx @@ -1,3 +1,4 @@ +import { intervalToDuration } from 'date-fns'; import React, { useCallback, useEffect, @@ -50,6 +51,7 @@ import { REFRESH_TIMEOUT_SECONDS, titleizeString, truncateString, + isANumber, } from '../helpers'; import { useUriListForPermissions } from '../hooks/UriListForPermissions'; @@ -71,6 +73,7 @@ import { ErrorForDisplay, PermissionsToCheck, FilterOperatorMapping, + FilterDisplayTypeMapping, } from '../interfaces'; import ProcessModelSearch from './ProcessModelSearch'; import ProcessInstanceReportSearch from './ProcessInstanceReportSearch'; @@ -177,6 +180,11 @@ export default function ProcessInstanceListTable({ '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 = @@ -217,6 +225,8 @@ export default function ProcessInstanceListTable({ useState(false); const [withOldestOpenTask, setWithOldestOpenTask] = useState(showActionsColumn); + const [withRelationToMe, setwithRelationToMe] = + useState(showActionsColumn); const [systemReport, setSystemReport] = useState(null); const [selectedUserGroup, setSelectedUserGroup] = useState( null @@ -343,6 +353,7 @@ export default function ProcessInstanceListTable({ setEndToTime(''); setProcessInitiatorSelection(null); setWithOldestOpenTask(false); + setwithRelationToMe(false); setSystemReport(null); setSelectedUserGroup(null); if (updateRequiresRefilter) { @@ -410,6 +421,8 @@ export default function ProcessInstanceListTable({ 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)) { @@ -779,6 +792,11 @@ export default function ProcessInstanceListTable({ 'with_oldest_open_task', withOldestOpenTask ); + insertOrUpdateFieldInReportMetadata( + newReportMetadata, + 'with_relation_to_me', + withRelationToMe + ); insertOrUpdateFieldInReportMetadata( newReportMetadata, 'user_group_identifier', @@ -838,7 +856,11 @@ export default function ProcessInstanceListTable({ ) => { return ( <> - + { + 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 === '') { @@ -1175,6 +1209,22 @@ export default function ProcessInstanceListTable({ />, ]); if (reportColumnToOperateOn && reportColumnToOperateOn.filterable) { + formElements.push( + { + setFilterDisplayType(value.selectedItem); + setRequiresRefilter(true); + }} + /> + ); formElements.push( + {variant === 'all' ? ( + + { + setwithRelationToMe(value.target.checked); + setRequiresRefilter(true); + }} + /> + + ) : null}
@@ -1628,6 +1691,39 @@ export default function ProcessInstanceListTable({ return value; }; + const formatDateTime = (_row: any, value: any) => { + if (value === undefined) { + return undefined; + } + let dateInSeconds = value; + if (!isANumber(value)) { + const timeArgs = value.split('T'); + dateInSeconds = convertDateAndTimeStringsToSeconds( + timeArgs[0], + timeArgs[1] + ); + } + if (dateInSeconds) { + return convertSecondsToFormattedDateTime(dateInSeconds); + } + return null; + }; + + const formatDuration = (_row: any, value: any) => { + if (value === undefined) { + return undefined; + } + const duration = intervalToDuration({ start: 0, end: value * 1000 }); + let durationString = `${duration.minutes}m ${duration.seconds}s`; + if (duration.hours !== undefined && duration.hours > 0) { + durationString = `${duration.hours}h ${durationString}`; + } + if (duration.days !== undefined && duration.days > 0) { + durationString = `${duration.days}d ${durationString}`; + } + return durationString; + }; + const formattedColumn = (row: ProcessInstance, column: ReportColumn) => { const reportColumnFormatters: Record = { id: formatProcessInstanceId, @@ -1640,9 +1736,14 @@ export default function ProcessInstanceListTable({ task_updated_at_in_seconds: formatSecondsForDisplay, last_milestone_bpmn_name: formatLastMilestone, }; + const displayTypeFormatters: Record = { + date_time: formatDateTime, + duration: formatDuration, + }; const columnAccessor = column.accessor as keyof ProcessInstance; - const formatter = - reportColumnFormatters[columnAccessor] ?? defaultFormatter; + const formatter = column.display_type + ? displayTypeFormatters[column.display_type] + : reportColumnFormatters[columnAccessor] ?? defaultFormatter; const value = row[columnAccessor]; if (columnAccessor === 'status') { diff --git a/spiffworkflow-frontend/src/helpers.test.tsx b/spiffworkflow-frontend/src/helpers.test.tsx index 2efb107d2..d359012d8 100644 --- a/spiffworkflow-frontend/src/helpers.test.tsx +++ b/spiffworkflow-frontend/src/helpers.test.tsx @@ -1,6 +1,6 @@ import { convertSecondsToFormattedDateString, - isInteger, + isANumber, slugifyString, underscorizeString, recursivelyChangeNullAndUndefined, @@ -24,11 +24,13 @@ test('it can keep the correct date when converting seconds to date', () => { }); test('it can validate numeric values', () => { - expect(isInteger('11')).toEqual(true); - expect(isInteger('hey')).toEqual(false); - expect(isInteger(' ')).toEqual(false); - expect(isInteger('1 2')).toEqual(false); - expect(isInteger(2)).toEqual(true); + expect(isANumber('11')).toEqual(true); + expect(isANumber('hey')).toEqual(false); + expect(isANumber(' ')).toEqual(false); + expect(isANumber('1 2')).toEqual(false); + expect(isANumber(2)).toEqual(true); + expect(isANumber(2.0)).toEqual(true); + expect(isANumber('2.0')).toEqual(true); }); test('it can replace undefined values in object with null', () => { diff --git a/spiffworkflow-frontend/src/helpers.tsx b/spiffworkflow-frontend/src/helpers.tsx index f6a1d1bd0..4bb0f879b 100644 --- a/spiffworkflow-frontend/src/helpers.tsx +++ b/spiffworkflow-frontend/src/helpers.tsx @@ -351,8 +351,10 @@ export const setPageTitle = (items: Array) => { document.title = ['SpiffWorkflow'].concat(items).join(' - '); }; -export const isInteger = (str: string | number) => { - return /^\d+$/.test(str.toString()); +// calling it isANumber to avoid confusion with other libraries +// that have isNumber methods +export const isANumber = (str: string | number) => { + return /^\d+(\.\d+)?$/.test(str.toString()); }; export const encodeBase64 = (data: string) => { diff --git a/spiffworkflow-frontend/src/interfaces.ts b/spiffworkflow-frontend/src/interfaces.ts index 9d5f5c1d9..24976003c 100644 --- a/spiffworkflow-frontend/src/interfaces.ts +++ b/spiffworkflow-frontend/src/interfaces.ts @@ -151,6 +151,10 @@ export interface FilterOperatorMapping { [key: string]: FilterOperator; } +export interface FilterDisplayTypeMapping { + [key: string]: string; +} + export interface ProcessFile { content_type: string; last_modified: string; @@ -228,6 +232,7 @@ export interface ReportColumn { Header: string; accessor: string; filterable: boolean; + display_type?: string; } export interface ReportColumnForEditing extends ReportColumn { diff --git a/spiffworkflow-frontend/src/routes/ProcessInstanceFindById.tsx b/spiffworkflow-frontend/src/routes/ProcessInstanceFindById.tsx index 7ae0eaf4b..7dc37e6f6 100644 --- a/spiffworkflow-frontend/src/routes/ProcessInstanceFindById.tsx +++ b/spiffworkflow-frontend/src/routes/ProcessInstanceFindById.tsx @@ -1,7 +1,7 @@ import { useEffect, useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { Button, Form, Stack, TextInput } from '@carbon/react'; -import { isInteger, modifyProcessIdentifierForPathParam } from '../helpers'; +import { isANumber, modifyProcessIdentifierForPathParam } from '../helpers'; import HttpService from '../services/HttpService'; import ProcessInstanceListTabs from '../components/ProcessInstanceListTabs'; import { ProcessInstance } from '../interfaces'; @@ -42,7 +42,7 @@ export default function ProcessInstanceFindById() { }; const handleProcessInstanceIdChange = (event: any) => { - if (isInteger(event.target.value)) { + if (isANumber(event.target.value)) { setProcessInstanceIdValid(true); } else { setProcessInstanceIdValid(false);