diff --git a/spiffworkflow-frontend/package-lock.json b/spiffworkflow-frontend/package-lock.json index f2b3d4fa0..ca6d5de51 100644 --- a/spiffworkflow-frontend/package-lock.json +++ b/spiffworkflow-frontend/package-lock.json @@ -11354,9 +11354,12 @@ } }, "node_modules/date-fns": { - "version": "2.29.3", - "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.29.3.tgz", - "integrity": "sha512-dDCnyH2WnnKusqvZZ6+jA1O51Ibt8ZMRNkDZdyAyK4YfbDwa/cEmuztzG5pk6hqlp9aSBPYcjOlktquahGwGeA==", + "version": "2.30.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", + "integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==", + "dependencies": { + "@babel/runtime": "^7.21.0" + }, "engines": { "node": ">=0.11" }, @@ -40545,9 +40548,12 @@ } }, "date-fns": { - "version": "2.29.3", - "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.29.3.tgz", - "integrity": "sha512-dDCnyH2WnnKusqvZZ6+jA1O51Ibt8ZMRNkDZdyAyK4YfbDwa/cEmuztzG5pk6hqlp9aSBPYcjOlktquahGwGeA==" + "version": "2.30.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", + "integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==", + "requires": { + "@babel/runtime": "^7.21.0" + } }, "dayjs": { "version": "1.11.7", diff --git a/spiffworkflow-frontend/src/components/ProcessInstanceListTable.tsx b/spiffworkflow-frontend/src/components/ProcessInstanceListTable.tsx index 4cddd5530..5b33c1185 100644 --- a/spiffworkflow-frontend/src/components/ProcessInstanceListTable.tsx +++ b/spiffworkflow-frontend/src/components/ProcessInstanceListTable.tsx @@ -1,4 +1,3 @@ -import { intervalToDuration } from 'date-fns'; import React, { useCallback, useEffect, @@ -52,6 +51,7 @@ import { titleizeString, truncateString, isANumber, + formatDurationForDisplay, } from '../helpers'; import { useUriListForPermissions } from '../hooks/UriListForPermissions'; @@ -1667,10 +1667,16 @@ export default function ProcessInstanceListTable({ const formatProcessInstanceId = (_row: ProcessInstance, id: number) => { return {id}; }; - const formatProcessModelIdentifier = (_row: any, identifier: any) => { + const formatProcessModelIdentifier = ( + _row: ProcessInstance, + identifier: any + ) => { return {identifier}; }; - const formatProcessModelDisplayName = (_row: any, identifier: any) => { + const formatProcessModelDisplayName = ( + _row: ProcessInstance, + identifier: any + ) => { return {identifier}; }; const formatLastMilestone = ( @@ -1684,14 +1690,14 @@ export default function ProcessInstanceListTable({ return {truncatedValue}; }; - const formatSecondsForDisplay = (_row: any, seconds: any) => { + const formatSecondsForDisplay = (_row: ProcessInstance, seconds: any) => { return convertSecondsToFormattedDateTime(seconds) || '-'; }; - const defaultFormatter = (_row: any, value: any) => { + const defaultFormatter = (_row: ProcessInstance, value: any) => { return value; }; - const formatDateTime = (_row: any, value: any) => { + const formatDateTime = (_row: ProcessInstance, value: any) => { if (value === undefined) { return undefined; } @@ -1709,21 +1715,6 @@ export default function ProcessInstanceListTable({ 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, @@ -1738,7 +1729,7 @@ export default function ProcessInstanceListTable({ }; const displayTypeFormatters: Record = { date_time: formatDateTime, - duration: formatDuration, + duration: formatDurationForDisplay, }; const columnAccessor = column.accessor as keyof ProcessInstance; const formatter = column.display_type diff --git a/spiffworkflow-frontend/src/helpers.test.tsx b/spiffworkflow-frontend/src/helpers.test.tsx index d359012d8..042c8dcc5 100644 --- a/spiffworkflow-frontend/src/helpers.test.tsx +++ b/spiffworkflow-frontend/src/helpers.test.tsx @@ -4,6 +4,7 @@ import { slugifyString, underscorizeString, recursivelyChangeNullAndUndefined, + formatDurationForDisplay, } from './helpers'; test('it can slugify a string', () => { @@ -101,3 +102,13 @@ test('it can replace null values in object with undefined', () => { expect(result.contacts.awesome).toEqual(false); expect(result.contacts.info).toEqual(''); }); + +test('it can properly format a duration', () => { + expect(formatDurationForDisplay(null, '0')).toEqual('0s'); + expect(formatDurationForDisplay(null, '60')).toEqual('1m'); + expect(formatDurationForDisplay(null, '65')).toEqual('1m 5s'); + expect(formatDurationForDisplay(null, 65)).toEqual('1m 5s'); + expect(formatDurationForDisplay(null, 86500)).toEqual('1d 1m 40s'); + expect(formatDurationForDisplay(null, 2629746)).toEqual('30d 10h 29m 6s'); + expect(formatDurationForDisplay(null, 31536765)).toEqual('365d 12m 45s'); +}); diff --git a/spiffworkflow-frontend/src/helpers.tsx b/spiffworkflow-frontend/src/helpers.tsx index 4bb0f879b..2a79d0620 100644 --- a/spiffworkflow-frontend/src/helpers.tsx +++ b/spiffworkflow-frontend/src/helpers.tsx @@ -1,4 +1,4 @@ -import { format } from 'date-fns'; +import { Duration, format } from 'date-fns'; import { Buffer } from 'buffer'; import { @@ -399,3 +399,43 @@ export const getLastMilestoneFromProcessInstance = ( } return [valueToUse, truncatedValue]; }; + +// logic from https://stackoverflow.com/a/28510323/6090676 +export const secondsToDuration = (secNum: number) => { + const days = Math.floor(secNum / 86400); + const hours = Math.floor(secNum / 3600) % 24; + const minutes = Math.floor(secNum / 60) % 60; + const seconds = secNum % 60; + + const duration: Duration = { + days, + hours, + minutes, + seconds, + }; + return duration; +}; + +export const formatDurationForDisplay = (_row: any, value: any) => { + if (value === undefined) { + return undefined; + } + const duration = secondsToDuration(parseInt(value, 10)); + const durationTimes = []; + if (duration.seconds !== undefined && duration.seconds > 0) { + durationTimes.unshift(`${duration.seconds}s`); + } + if (duration.minutes !== undefined && duration.minutes > 0) { + durationTimes.unshift(`${duration.minutes}m`); + } + if (duration.hours !== undefined && duration.hours > 0) { + durationTimes.unshift(`${duration.hours}h`); + } + if (duration.days !== undefined && duration.days > 0) { + durationTimes.unshift(`${duration.days}d`); + } + if (durationTimes.length < 1) { + durationTimes.push('0s'); + } + return durationTimes.join(' '); +};