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, {
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<boolean>(false);
const [withOldestOpenTask, setWithOldestOpenTask] =
useState<boolean>(showActionsColumn);
const [withRelationToMe, setwithRelationToMe] =
useState<boolean>(showActionsColumn);
const [systemReport, setSystemReport] = useState<string | null>(null);
const [selectedUserGroup, setSelectedUserGroup] = useState<string | null>(
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 (
<>
<DatePicker dateFormat={DATE_FORMAT_CARBON} datePickerType="single">
<DatePicker
id={`date-picker-parent-${name}`}
dateFormat={DATE_FORMAT_CARBON}
datePickerType="single"
>
<DatePickerInput
id={`date-picker-${name}`}
placeholder={DATE_FORMAT_FOR_DISPLAY}
@ -860,7 +882,7 @@ export default function ProcessInstanceListTable({
</DatePicker>
<TimePicker
invalid={timeInvalid}
id="time-picker"
id={`time-picker-${name}`}
labelText="Select a time"
pattern="^([01]\d|2[0-3]):?([0-5]\d)$"
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
const reportColumnForm = () => {
if (reportColumnFormMode === '') {
@ -1175,6 +1209,22 @@ export default function ProcessInstanceListTable({
/>,
]);
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(
<Dropdown
titleText="Operator"
@ -1358,6 +1408,19 @@ export default function ProcessInstanceListTable({
}}
/>
</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>
<div className="vertical-spacer-to-allow-combo-box-to-expand-in-modal" />
</>
@ -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<string, any> = {
id: formatProcessInstanceId,
@ -1640,9 +1736,14 @@ export default function ProcessInstanceListTable({
task_updated_at_in_seconds: formatSecondsForDisplay,
last_milestone_bpmn_name: formatLastMilestone,
};
const displayTypeFormatters: Record<string, any> = {
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') {

View File

@ -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', () => {

View File

@ -351,8 +351,10 @@ export const setPageTitle = (items: Array<string>) => {
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) => {

View File

@ -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 {

View File

@ -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);