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 <jasquat@users.noreply.github.com>
This commit is contained in:
jasquat 2023-10-10 11:52:59 -04:00 committed by GitHub
parent ceb06cc227
commit 3276fdb579
5 changed files with 124 additions and 14 deletions

View File

@ -1,3 +1,4 @@
import { intervalToDuration } from 'date-fns';
import React, { import React, {
useCallback, useCallback,
useEffect, useEffect,
@ -50,6 +51,7 @@ import {
REFRESH_TIMEOUT_SECONDS, REFRESH_TIMEOUT_SECONDS,
titleizeString, titleizeString,
truncateString, truncateString,
isANumber,
} from '../helpers'; } from '../helpers';
import { useUriListForPermissions } from '../hooks/UriListForPermissions'; import { useUriListForPermissions } from '../hooks/UriListForPermissions';
@ -71,6 +73,7 @@ import {
ErrorForDisplay, ErrorForDisplay,
PermissionsToCheck, PermissionsToCheck,
FilterOperatorMapping, FilterOperatorMapping,
FilterDisplayTypeMapping,
} from '../interfaces'; } from '../interfaces';
import ProcessModelSearch from './ProcessModelSearch'; import ProcessModelSearch from './ProcessModelSearch';
import ProcessInstanceReportSearch from './ProcessInstanceReportSearch'; import ProcessInstanceReportSearch from './ProcessInstanceReportSearch';
@ -177,6 +180,11 @@ export default function ProcessInstanceListTable({
'Is Not Empty': { id: 'is_not_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 = const processInstanceListPathPrefix =
variant === 'all' ? '/process-instances/all' : '/process-instances/for-me'; variant === 'all' ? '/process-instances/all' : '/process-instances/for-me';
const processInstanceShowPathPrefix = const processInstanceShowPathPrefix =
@ -217,6 +225,8 @@ export default function ProcessInstanceListTable({
useState<boolean>(false); useState<boolean>(false);
const [withOldestOpenTask, setWithOldestOpenTask] = const [withOldestOpenTask, setWithOldestOpenTask] =
useState<boolean>(showActionsColumn); useState<boolean>(showActionsColumn);
const [withRelationToMe, setwithRelationToMe] =
useState<boolean>(showActionsColumn);
const [systemReport, setSystemReport] = useState<string | null>(null); const [systemReport, setSystemReport] = useState<string | null>(null);
const [selectedUserGroup, setSelectedUserGroup] = useState<string | null>( const [selectedUserGroup, setSelectedUserGroup] = useState<string | null>(
null null
@ -343,6 +353,7 @@ export default function ProcessInstanceListTable({
setEndToTime(''); setEndToTime('');
setProcessInitiatorSelection(null); setProcessInitiatorSelection(null);
setWithOldestOpenTask(false); setWithOldestOpenTask(false);
setwithRelationToMe(false);
setSystemReport(null); setSystemReport(null);
setSelectedUserGroup(null); setSelectedUserGroup(null);
if (updateRequiresRefilter) { if (updateRequiresRefilter) {
@ -410,6 +421,8 @@ export default function ProcessInstanceListTable({
setProcessInitiatorSelection(reportFilter.field_value || ''); setProcessInitiatorSelection(reportFilter.field_value || '');
} else if (reportFilter.field_name === 'with_oldest_open_task') { } else if (reportFilter.field_name === 'with_oldest_open_task') {
setWithOldestOpenTask(reportFilter.field_value); 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') { } else if (reportFilter.field_name === 'user_group_identifier') {
setSelectedUserGroup(reportFilter.field_value); setSelectedUserGroup(reportFilter.field_value);
} else if (systemReportOptions.includes(reportFilter.field_name)) { } else if (systemReportOptions.includes(reportFilter.field_name)) {
@ -779,6 +792,11 @@ export default function ProcessInstanceListTable({
'with_oldest_open_task', 'with_oldest_open_task',
withOldestOpenTask withOldestOpenTask
); );
insertOrUpdateFieldInReportMetadata(
newReportMetadata,
'with_relation_to_me',
withRelationToMe
);
insertOrUpdateFieldInReportMetadata( insertOrUpdateFieldInReportMetadata(
newReportMetadata, newReportMetadata,
'user_group_identifier', 'user_group_identifier',
@ -838,7 +856,11 @@ export default function ProcessInstanceListTable({
) => { ) => {
return ( return (
<> <>
<DatePicker dateFormat={DATE_FORMAT_CARBON} datePickerType="single"> <DatePicker
id={`date-picker-parent-${name}`}
dateFormat={DATE_FORMAT_CARBON}
datePickerType="single"
>
<DatePickerInput <DatePickerInput
id={`date-picker-${name}`} id={`date-picker-${name}`}
placeholder={DATE_FORMAT_FOR_DISPLAY} placeholder={DATE_FORMAT_FOR_DISPLAY}
@ -860,7 +882,7 @@ export default function ProcessInstanceListTable({
</DatePicker> </DatePicker>
<TimePicker <TimePicker
invalid={timeInvalid} invalid={timeInvalid}
id="time-picker" id={`time-picker-${name}`}
labelText="Select a time" labelText="Select a time"
pattern="^([01]\d|2[0-3]):?([0-5]\d)$" pattern="^([01]\d|2[0-3]):?([0-5]\d)$"
value={initialTime} value={initialTime}
@ -1128,6 +1150,18 @@ export default function ProcessInstanceListTable({
} }
}; };
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 // eslint-disable-next-line sonarjs/cognitive-complexity
const reportColumnForm = () => { const reportColumnForm = () => {
if (reportColumnFormMode === '') { if (reportColumnFormMode === '') {
@ -1175,6 +1209,22 @@ export default function ProcessInstanceListTable({
/>, />,
]); ]);
if (reportColumnToOperateOn && reportColumnToOperateOn.filterable) { if (reportColumnToOperateOn && reportColumnToOperateOn.filterable) {
formElements.push(
<Dropdown
titleText="Display type"
id="report-column-display-type"
items={[''].concat(Object.values(filterDisplayTypes))}
selectedItem={
reportColumnToOperateOn.display_type
? filterDisplayTypes[reportColumnToOperateOn.display_type]
: ''
}
onChange={(value: any) => {
setFilterDisplayType(value.selectedItem);
setRequiresRefilter(true);
}}
/>
);
formElements.push( formElements.push(
<Dropdown <Dropdown
titleText="Operator" titleText="Operator"
@ -1358,6 +1408,19 @@ export default function ProcessInstanceListTable({
}} }}
/> />
</Column> </Column>
{variant === 'all' ? (
<Column md={4} lg={8} sm={2}>
<Checkbox
labelText="Include tasks for me"
id="with-relation-to-me"
checked={withRelationToMe}
onChange={(value: any) => {
setwithRelationToMe(value.target.checked);
setRequiresRefilter(true);
}}
/>
</Column>
) : null}
</Grid> </Grid>
<div className="vertical-spacer-to-allow-combo-box-to-expand-in-modal" /> <div className="vertical-spacer-to-allow-combo-box-to-expand-in-modal" />
</> </>
@ -1628,6 +1691,39 @@ export default function ProcessInstanceListTable({
return value; 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 formattedColumn = (row: ProcessInstance, column: ReportColumn) => {
const reportColumnFormatters: Record<string, any> = { const reportColumnFormatters: Record<string, any> = {
id: formatProcessInstanceId, id: formatProcessInstanceId,
@ -1640,9 +1736,14 @@ export default function ProcessInstanceListTable({
task_updated_at_in_seconds: formatSecondsForDisplay, task_updated_at_in_seconds: formatSecondsForDisplay,
last_milestone_bpmn_name: formatLastMilestone, last_milestone_bpmn_name: formatLastMilestone,
}; };
const displayTypeFormatters: Record<string, any> = {
date_time: formatDateTime,
duration: formatDuration,
};
const columnAccessor = column.accessor as keyof ProcessInstance; const columnAccessor = column.accessor as keyof ProcessInstance;
const formatter = const formatter = column.display_type
reportColumnFormatters[columnAccessor] ?? defaultFormatter; ? displayTypeFormatters[column.display_type]
: reportColumnFormatters[columnAccessor] ?? defaultFormatter;
const value = row[columnAccessor]; const value = row[columnAccessor];
if (columnAccessor === 'status') { if (columnAccessor === 'status') {

View File

@ -1,6 +1,6 @@
import { import {
convertSecondsToFormattedDateString, convertSecondsToFormattedDateString,
isInteger, isANumber,
slugifyString, slugifyString,
underscorizeString, underscorizeString,
recursivelyChangeNullAndUndefined, recursivelyChangeNullAndUndefined,
@ -24,11 +24,13 @@ test('it can keep the correct date when converting seconds to date', () => {
}); });
test('it can validate numeric values', () => { test('it can validate numeric values', () => {
expect(isInteger('11')).toEqual(true); expect(isANumber('11')).toEqual(true);
expect(isInteger('hey')).toEqual(false); expect(isANumber('hey')).toEqual(false);
expect(isInteger(' ')).toEqual(false); expect(isANumber(' ')).toEqual(false);
expect(isInteger('1 2')).toEqual(false); expect(isANumber('1 2')).toEqual(false);
expect(isInteger(2)).toEqual(true); 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', () => { test('it can replace undefined values in object with null', () => {

View File

@ -351,8 +351,10 @@ export const setPageTitle = (items: Array<string>) => {
document.title = ['SpiffWorkflow'].concat(items).join(' - '); document.title = ['SpiffWorkflow'].concat(items).join(' - ');
}; };
export const isInteger = (str: string | number) => { // calling it isANumber to avoid confusion with other libraries
return /^\d+$/.test(str.toString()); // that have isNumber methods
export const isANumber = (str: string | number) => {
return /^\d+(\.\d+)?$/.test(str.toString());
}; };
export const encodeBase64 = (data: string) => { export const encodeBase64 = (data: string) => {

View File

@ -151,6 +151,10 @@ export interface FilterOperatorMapping {
[key: string]: FilterOperator; [key: string]: FilterOperator;
} }
export interface FilterDisplayTypeMapping {
[key: string]: string;
}
export interface ProcessFile { export interface ProcessFile {
content_type: string; content_type: string;
last_modified: string; last_modified: string;
@ -228,6 +232,7 @@ export interface ReportColumn {
Header: string; Header: string;
accessor: string; accessor: string;
filterable: boolean; filterable: boolean;
display_type?: string;
} }
export interface ReportColumnForEditing extends ReportColumn { export interface ReportColumnForEditing extends ReportColumn {

View File

@ -1,7 +1,7 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { Button, Form, Stack, TextInput } from '@carbon/react'; import { Button, Form, Stack, TextInput } from '@carbon/react';
import { isInteger, modifyProcessIdentifierForPathParam } from '../helpers'; import { isANumber, modifyProcessIdentifierForPathParam } from '../helpers';
import HttpService from '../services/HttpService'; import HttpService from '../services/HttpService';
import ProcessInstanceListTabs from '../components/ProcessInstanceListTabs'; import ProcessInstanceListTabs from '../components/ProcessInstanceListTabs';
import { ProcessInstance } from '../interfaces'; import { ProcessInstance } from '../interfaces';
@ -42,7 +42,7 @@ export default function ProcessInstanceFindById() {
}; };
const handleProcessInstanceIdChange = (event: any) => { const handleProcessInstanceIdChange = (event: any) => {
if (isInteger(event.target.value)) { if (isANumber(event.target.value)) {
setProcessInstanceIdValid(true); setProcessInstanceIdValid(true);
} else { } else {
setProcessInstanceIdValid(false); setProcessInstanceIdValid(false);