diff --git a/spiffworkflow-frontend/src/components/ActiveUsers.tsx b/spiffworkflow-frontend/src/components/ActiveUsers.tsx index 96e49373..3875cdb4 100644 --- a/spiffworkflow-frontend/src/components/ActiveUsers.tsx +++ b/spiffworkflow-frontend/src/components/ActiveUsers.tsx @@ -2,7 +2,8 @@ import { useEffect, useState } from 'react'; import HttpService from '../services/HttpService'; import { User } from '../interfaces'; -import { refreshAtInterval, REFRESH_TIMEOUT_SECONDS } from '../helpers'; +import { refreshAtInterval } from '../helpers'; +import DateAndTimeService from '../services/DateAndTimeService'; async function sha256(message: string) { // encode as UTF-8 @@ -48,7 +49,7 @@ export default function ActiveUsers() { return refreshAtInterval( 15, - REFRESH_TIMEOUT_SECONDS, + DateAndTimeService.REFRESH_TIMEOUT_SECONDS, updateActiveUsers, unregisterUser ); diff --git a/spiffworkflow-frontend/src/components/MessageInstanceList.tsx b/spiffworkflow-frontend/src/components/MessageInstanceList.tsx index 1588a411..26ebacce 100644 --- a/spiffworkflow-frontend/src/components/MessageInstanceList.tsx +++ b/spiffworkflow-frontend/src/components/MessageInstanceList.tsx @@ -7,13 +7,13 @@ import { Link, useSearchParams } from 'react-router-dom'; import PaginationForTable from './PaginationForTable'; import ProcessBreadcrumb from './ProcessBreadcrumb'; import { - convertSecondsToFormattedDateTime, getPageInfoFromSearchParams, modifyProcessIdentifierForPathParam, } from '../helpers'; import HttpService from '../services/HttpService'; import { FormatProcessModelDisplayName } from './MiniComponents'; import { MessageInstance } from '../interfaces'; +import DateAndTimeService from '../services/DateAndTimeService'; type OwnProps = { processInstanceId?: number; @@ -128,7 +128,9 @@ export default function MessageInstanceList({ processInstanceId }: OwnProps) { {row.status} - {convertSecondsToFormattedDateTime(row.created_at_in_seconds)} + {DateAndTimeService.convertSecondsToFormattedDateTime( + row.created_at_in_seconds + )} ); diff --git a/spiffworkflow-frontend/src/components/ProcessInstanceListTable.tsx b/spiffworkflow-frontend/src/components/ProcessInstanceListTable.tsx index c6e02a8b..11343b1d 100644 --- a/spiffworkflow-frontend/src/components/ProcessInstanceListTable.tsx +++ b/spiffworkflow-frontend/src/components/ProcessInstanceListTable.tsx @@ -36,22 +36,13 @@ import { DATE_FORMAT_FOR_DISPLAY, } from '../config'; import { - convertDateAndTimeStringsToSeconds, - convertDateObjectToFormattedHoursMinutes, - convertSecondsToFormattedDateString, - convertSecondsToFormattedDateTime, - convertSecondsToFormattedTimeHoursMinutes, getKeyByValue, getLastMilestoneFromProcessInstance, getPageInfoFromSearchParams, modifyProcessIdentifierForPathParam, refreshAtInterval, - REFRESH_INTERVAL_SECONDS, - REFRESH_TIMEOUT_SECONDS, titleizeString, truncateString, - formatDurationForDisplay, - formatDateTime, } from '../helpers'; import { useUriListForPermissions } from '../hooks/UriListForPermissions'; @@ -86,6 +77,7 @@ import { Can } from '../contexts/Can'; import TableCellWithTimeAgoInWords from './TableCellWithTimeAgoInWords'; import UserService from '../services/UserService'; import Filters from './Filters'; +import DateAndTimeService from '../services/DateAndTimeService'; type OwnProps = { filtersEnabled?: boolean; @@ -443,13 +435,15 @@ export default function ProcessInstanceListTable({ const timeFunctionToCall = dateParametersToAlwaysFilterBy[reportFilter.field_name][1]; if (reportFilter.field_value) { - const dateString = convertSecondsToFormattedDateString( - reportFilter.field_value as any - ); + const dateString = + DateAndTimeService.convertSecondsToFormattedDateString( + reportFilter.field_value as any + ); dateFunctionToCall(dateString); - const timeString = convertSecondsToFormattedTimeHoursMinutes( - reportFilter.field_value as any - ); + const timeString = + DateAndTimeService.convertSecondsToFormattedTimeHoursMinutes( + reportFilter.field_value as any + ); timeFunctionToCall(timeString); } } @@ -566,8 +560,8 @@ export default function ProcessInstanceListTable({ checkFiltersAndRun(); if (autoReload) { clearRefreshRef.current = refreshAtInterval( - REFRESH_INTERVAL_SECONDS, - REFRESH_TIMEOUT_SECONDS, + DateAndTimeService.REFRESH_INTERVAL_SECONDS, + DateAndTimeService.REFRESH_TIMEOUT_SECONDS, checkFiltersAndRun ); return clearRefreshRef.current; @@ -627,19 +621,22 @@ export default function ProcessInstanceListTable({ // with the use of the setErrorMessageSafely function. we are not sure why the context not // changing still causes things to rerender when we call its setter without our extra check. const calculateStartAndEndSeconds = (validate: boolean = true) => { - const startFromSeconds = convertDateAndTimeStringsToSeconds( - startFromDate, - startFromTime || '00:00:00' - ); - const startToSeconds = convertDateAndTimeStringsToSeconds( - startToDate, - startToTime || '00:00:00' - ); - const endFromSeconds = convertDateAndTimeStringsToSeconds( - endFromDate, - endFromTime || '00:00:00' - ); - const endToSeconds = convertDateAndTimeStringsToSeconds( + const startFromSeconds = + DateAndTimeService.convertDateAndTimeStringsToSeconds( + startFromDate, + startFromTime || '00:00:00' + ); + const startToSeconds = + DateAndTimeService.convertDateAndTimeStringsToSeconds( + startToDate, + startToTime || '00:00:00' + ); + const endFromSeconds = + DateAndTimeService.convertDateAndTimeStringsToSeconds( + endFromDate, + endFromTime || '00:00:00' + ); + const endToSeconds = DateAndTimeService.convertDateAndTimeStringsToSeconds( endToDate, endToTime || '00:00:00' ); @@ -872,7 +869,9 @@ export default function ProcessInstanceListTable({ onChange={(dateChangeEvent: any) => { if (!initialDate && !initialTime) { onChangeTimeFunction( - convertDateObjectToFormattedHoursMinutes(new Date()) + DateAndTimeService.convertDateObjectToFormattedHoursMinutes( + new Date() + ) ); } onChangeDateFunction(dateChangeEvent.srcElement.value); @@ -1691,7 +1690,7 @@ export default function ProcessInstanceListTable({ }; const formatSecondsForDisplay = (_row: ProcessInstance, seconds: any) => { - return convertSecondsToFormattedDateTime(seconds) || '-'; + return DateAndTimeService.convertSecondsToFormattedDateTime(seconds) || '-'; }; const defaultFormatter = (_row: ProcessInstance, value: any) => { return value; @@ -1710,8 +1709,8 @@ export default function ProcessInstanceListTable({ last_milestone_bpmn_name: formatLastMilestone, }; const displayTypeFormatters: Record = { - date_time: formatDateTime, - duration: formatDurationForDisplay, + date_time: DateAndTimeService.formatDateTime, + duration: DateAndTimeService.formatDurationForDisplay, }; const columnAccessor = column.accessor as keyof ProcessInstance; const formatter = column.display_type diff --git a/spiffworkflow-frontend/src/components/ProcessInstanceLogList.tsx b/spiffworkflow-frontend/src/components/ProcessInstanceLogList.tsx index ac2d4a03..adb7df79 100644 --- a/spiffworkflow-frontend/src/components/ProcessInstanceLogList.tsx +++ b/spiffworkflow-frontend/src/components/ProcessInstanceLogList.tsx @@ -15,7 +15,6 @@ import { createSearchParams, Link, useSearchParams } from 'react-router-dom'; import PaginationForTable from './PaginationForTable'; import { getPageInfoFromSearchParams, - convertSecondsToFormattedDateTime, selectKeysFromSearchParams, } from '../helpers'; import HttpService from '../services/HttpService'; @@ -32,6 +31,7 @@ import { childrenForErrorObject, errorForDisplayFromProcessInstanceErrorDetail, } from './ErrorDisplay'; +import DateAndTimeService from '../services/DateAndTimeService'; type OwnProps = { variant: string; // 'all' or 'for-me' @@ -306,7 +306,11 @@ export default function ProcessInstanceLogList({ } let timestampComponent = ( - {convertSecondsToFormattedDateTime(logEntry.timestamp)} + + {DateAndTimeService.convertSecondsToFormattedDateTime( + logEntry.timestamp + )} + ); if (logEntry.spiff_task_guid && logEntry.event_type !== 'task_cancelled') { timestampComponent = ( @@ -317,7 +321,9 @@ export default function ProcessInstanceLogList({ to={`${processInstanceShowPageBaseUrl}/${logEntry.process_instance_id}/${logEntry.spiff_task_guid}`} title="View state when task was completed" > - {convertSecondsToFormattedDateTime(logEntry.timestamp)} + {DateAndTimeService.convertSecondsToFormattedDateTime( + logEntry.timestamp + )} ); diff --git a/spiffworkflow-frontend/src/components/TableCellWithTimeAgoInWords.tsx b/spiffworkflow-frontend/src/components/TableCellWithTimeAgoInWords.tsx index fb3ddf54..64486f0e 100644 --- a/spiffworkflow-frontend/src/components/TableCellWithTimeAgoInWords.tsx +++ b/spiffworkflow-frontend/src/components/TableCellWithTimeAgoInWords.tsx @@ -1,6 +1,6 @@ // @ts-ignore import { TimeAgo } from '../helpers/timeago'; -import { convertSecondsToFormattedDateTime } from '../helpers'; +import DateAndTimeService from '../services/DateAndTimeService'; type OwnProps = { timeInSeconds: number; @@ -16,7 +16,10 @@ export default function TableCellWithTimeAgoInWords({ return ( // eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions diff --git a/spiffworkflow-frontend/src/components/TaskListTable.tsx b/spiffworkflow-frontend/src/components/TaskListTable.tsx index c31aece2..075853b6 100644 --- a/spiffworkflow-frontend/src/components/TaskListTable.tsx +++ b/spiffworkflow-frontend/src/components/TaskListTable.tsx @@ -6,18 +6,16 @@ import { TimeAgo } from '../helpers/timeago'; import UserService from '../services/UserService'; import PaginationForTable from './PaginationForTable'; import { - convertSecondsToFormattedDateTime, getPageInfoFromSearchParams, modifyProcessIdentifierForPathParam, refreshAtInterval, - REFRESH_INTERVAL_SECONDS, - REFRESH_TIMEOUT_SECONDS, } from '../helpers'; import HttpService from '../services/HttpService'; import { PaginationObject, ProcessInstanceTask, Task } from '../interfaces'; import TableCellWithTimeAgoInWords from './TableCellWithTimeAgoInWords'; import CustomForm from './CustomForm'; import InstructionsForEndUser from './InstructionsForEndUser'; +import DateAndTimeService from '../services/DateAndTimeService'; const PER_PAGE_FOR_TASKS_ON_HOME_PAGE = 5; @@ -100,8 +98,8 @@ export default function TaskListTable({ getTasks(); if (autoReload) { return refreshAtInterval( - REFRESH_INTERVAL_SECONDS, - REFRESH_TIMEOUT_SECONDS, + DateAndTimeService.REFRESH_INTERVAL_SECONDS, + DateAndTimeService.REFRESH_TIMEOUT_SECONDS, getTasks ); } @@ -288,7 +286,7 @@ export default function TaskListTable({ if (showDateStarted) { rowElements.push( - {convertSecondsToFormattedDateTime( + {DateAndTimeService.convertSecondsToFormattedDateTime( processInstanceTask.created_at_in_seconds ) || '-'} diff --git a/spiffworkflow-frontend/src/helpers.test.tsx b/spiffworkflow-frontend/src/helpers.test.tsx index 042c8dcc..08e08e28 100644 --- a/spiffworkflow-frontend/src/helpers.test.tsx +++ b/spiffworkflow-frontend/src/helpers.test.tsx @@ -1,10 +1,8 @@ import { - convertSecondsToFormattedDateString, isANumber, slugifyString, underscorizeString, recursivelyChangeNullAndUndefined, - formatDurationForDisplay, } from './helpers'; test('it can slugify a string', () => { @@ -19,11 +17,6 @@ test('it can underscorize a string', () => { ); }); -test('it can keep the correct date when converting seconds to date', () => { - const dateString = convertSecondsToFormattedDateString(1666325400); - expect(dateString).toEqual('2022-10-21'); -}); - test('it can validate numeric values', () => { expect(isANumber('11')).toEqual(true); expect(isANumber('hey')).toEqual(false); @@ -102,13 +95,3 @@ 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 fbcdcd2b..d70837cc 100644 --- a/spiffworkflow-frontend/src/helpers.tsx +++ b/spiffworkflow-frontend/src/helpers.tsx @@ -1,11 +1,5 @@ -import { Duration, format } from 'date-fns'; import { Buffer } from 'buffer'; -import { - DATE_TIME_FORMAT, - DATE_FORMAT, - TIME_FORMAT_HOURS_MINUTES, -} from './config'; import { ProcessInstance } from './interfaces'; export const DEFAULT_PER_PAGE = 50; @@ -87,128 +81,6 @@ export const titleizeString = (string: any) => { return capitalizeFirstLetter((string || '').replaceAll('_', ' ')); }; -export const convertDateToSeconds = ( - date: any, - onChangeFunction: any = null -) => { - let dateInSeconds = date; - if (date !== null) { - let dateInMilliseconds = date; - if (typeof date.getTime === 'function') { - dateInMilliseconds = date.getTime(); - } - dateInSeconds = Math.floor(dateInMilliseconds / 1000); - } - - if (onChangeFunction) { - onChangeFunction(dateInSeconds); - } else { - return dateInSeconds; - } - - return null; -}; - -export const convertDateObjectToFormattedString = (dateObject: Date) => { - if (dateObject) { - return format(dateObject, DATE_FORMAT); - } - return null; -}; - -export const dateStringToYMDFormat = (dateString: string) => { - if (dateString && dateString.match(/^\d{2}-\d{2}-\d{4}$/)) { - if (DATE_FORMAT.startsWith('dd')) { - const d = dateString.split('-'); - return `${d[2]}-${d[1]}-${d[0]}`; - } - if (DATE_FORMAT.startsWith('MM')) { - const d = dateString.split('-'); - return `${d[2]}-${d[0]}-${d[1]}`; - } - } - return dateString; -}; - -export const convertDateAndTimeStringsToDate = ( - dateString: string, - timeString: string -) => { - if (dateString && timeString) { - return new Date(`${dateStringToYMDFormat(dateString)}T${timeString}`); - } - return null; -}; - -export const convertDateAndTimeStringsToSeconds = ( - dateString: string, - timeString: string -) => { - const dateObject = convertDateAndTimeStringsToDate(dateString, timeString); - if (dateObject) { - return convertDateToSeconds(dateObject); - } - return null; -}; - -export const convertStringToDate = (dateString: string) => { - return convertDateAndTimeStringsToDate(dateString, '00:10:00'); -}; - -export const ymdDateStringToConfiguredFormat = (dateString: string) => { - const dateObject = convertStringToDate(dateString); - if (dateObject) { - return convertDateObjectToFormattedString(dateObject); - } - return null; -}; - -export const convertSecondsToDateObject = (seconds: number) => { - if (seconds) { - return new Date(seconds * 1000); - } - return null; -}; - -export const convertSecondsToFormattedDateTime = (seconds: number) => { - const dateObject = convertSecondsToDateObject(seconds); - if (dateObject) { - return format(dateObject, DATE_TIME_FORMAT); - } - return null; -}; - -export const convertDateObjectToFormattedHoursMinutes = (dateObject: Date) => { - if (dateObject) { - return format(dateObject, TIME_FORMAT_HOURS_MINUTES); - } - return null; -}; - -export const convertSecondsToFormattedTimeHoursMinutes = (seconds: number) => { - const dateObject = convertSecondsToDateObject(seconds); - if (dateObject) { - return convertDateObjectToFormattedHoursMinutes(dateObject); - } - return null; -}; - -export const convertSecondsToFormattedDateString = (seconds: number) => { - const dateObject = convertSecondsToDateObject(seconds); - if (dateObject) { - return convertDateObjectToFormattedString(dateObject); - } - return null; -}; - -export const convertDateStringToSeconds = (dateString: string) => { - const dateObject = convertStringToDate(dateString); - if (dateObject) { - return convertDateToSeconds(dateObject); - } - return null; -}; - export const objectIsEmpty = (obj: object) => { return Object.keys(obj).length === 0; }; @@ -368,14 +240,6 @@ export const decodeBase64 = (data: string) => { return Buffer.from(data, 'base64').toString('ascii'); }; -const MINUTES_IN_HOUR = 60; -const SECONDS_IN_MINUTE = 60; -const SECONDS_IN_HOUR = MINUTES_IN_HOUR * SECONDS_IN_MINUTE; -const FOUR_HOURS_IN_SECONDS = SECONDS_IN_HOUR * 4; - -export const REFRESH_INTERVAL_SECONDS = 5; -export const REFRESH_TIMEOUT_SECONDS = FOUR_HOURS_IN_SECONDS; - export const getLastMilestoneFromProcessInstance = ( processInstance: ProcessInstance, value: any @@ -402,61 +266,3 @@ 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(' '); -}; - -export const formatDateTime = (_row: any, value: any) => { - if (value === undefined || value === null) { - return value; - } - let dateInSeconds = value; - if (!isANumber(value)) { - const timeArgs = value.split('T'); - dateInSeconds = convertDateAndTimeStringsToSeconds( - timeArgs[0], - timeArgs[1] - ); - } - if (dateInSeconds) { - return convertSecondsToFormattedDateTime(dateInSeconds); - } - return null; -}; diff --git a/spiffworkflow-frontend/src/rjsf/carbon_theme/BaseInputTemplate/BaseInputTemplate.tsx b/spiffworkflow-frontend/src/rjsf/carbon_theme/BaseInputTemplate/BaseInputTemplate.tsx index 8954aba1..ab6d425e 100644 --- a/spiffworkflow-frontend/src/rjsf/carbon_theme/BaseInputTemplate/BaseInputTemplate.tsx +++ b/spiffworkflow-frontend/src/rjsf/carbon_theme/BaseInputTemplate/BaseInputTemplate.tsx @@ -9,7 +9,7 @@ import { import { useCallback } from 'react'; import { DATE_FORMAT_CARBON, DATE_FORMAT_FOR_DISPLAY } from '../../../config'; -import { ymdDateStringToConfiguredFormat } from '../../../helpers'; +import DateAndTimeService from '../../../services/DateAndTimeService'; import { getCommonAttributes } from '../../helpers'; /** The `BaseInputTemplate` is the template to use to render the basic `` component for the `core` theme. @@ -87,7 +87,7 @@ export default function BaseInputTemplate< dateValue = value; } else { try { - dateValue = ymdDateStringToConfiguredFormat(value); + dateValue = DateAndTimeService.ymdDateStringToConfiguredFormat(value); // let the date component and form validators handle bad dates and do not blow up } catch (RangeError) {} } diff --git a/spiffworkflow-frontend/src/rjsf/carbon_theme/DateWidget/DateWidget.tsx b/spiffworkflow-frontend/src/rjsf/carbon_theme/DateWidget/DateWidget.tsx index 7a821641..1fd7e9db 100644 --- a/spiffworkflow-frontend/src/rjsf/carbon_theme/DateWidget/DateWidget.tsx +++ b/spiffworkflow-frontend/src/rjsf/carbon_theme/DateWidget/DateWidget.tsx @@ -1,6 +1,6 @@ import React, { useCallback } from 'react'; import { getTemplate, WidgetProps } from '@rjsf/utils'; -import { dateStringToYMDFormat } from '../../../helpers'; +import DateAndTimeService from '../../../services/DateAndTimeService'; function DateWidget(props: WidgetProps) { const { onChange, options, registry } = props; @@ -12,7 +12,7 @@ function DateWidget(props: WidgetProps) { const handleChange = useCallback( (value: any) => { // react json schema forces y-m-d format for dates - const newValue = dateStringToYMDFormat(value); + const newValue = DateAndTimeService.dateStringToYMDFormat(value); onChange(newValue || undefined); }, [onChange] diff --git a/spiffworkflow-frontend/src/rjsf/custom_widgets/DateRangePicker/DateRangePickerWidget.tsx b/spiffworkflow-frontend/src/rjsf/custom_widgets/DateRangePicker/DateRangePickerWidget.tsx index 2aeefc7f..cc4d9e2a 100644 --- a/spiffworkflow-frontend/src/rjsf/custom_widgets/DateRangePicker/DateRangePickerWidget.tsx +++ b/spiffworkflow-frontend/src/rjsf/custom_widgets/DateRangePicker/DateRangePickerWidget.tsx @@ -5,12 +5,8 @@ import { DATE_FORMAT_FOR_DISPLAY, DATE_RANGE_DELIMITER, } from '../../../config'; -import { - convertDateObjectToFormattedString, - convertStringToDate, - dateStringToYMDFormat, -} from '../../../helpers'; import { getCommonAttributes } from '../../helpers'; +import DateAndTimeService from '../../../services/DateAndTimeService'; interface widgetArgs { id: string; @@ -52,13 +48,18 @@ export default function DateRangePickerWidget({ const onChangeLocal = useCallback( (dateRange: Date[]) => { let dateRangeString; - const startDate = convertDateObjectToFormattedString(dateRange[0]); + const startDate = DateAndTimeService.convertDateObjectToFormattedString( + dateRange[0] + ); if (startDate) { - const startDateYMD = dateStringToYMDFormat(startDate); - const endDate = convertDateObjectToFormattedString(dateRange[1]); + const startDateYMD = + DateAndTimeService.dateStringToYMDFormat(startDate); + const endDate = DateAndTimeService.convertDateObjectToFormattedString( + dateRange[1] + ); dateRangeString = startDateYMD; if (endDate) { - const endDateYMD = dateStringToYMDFormat(endDate); + const endDateYMD = DateAndTimeService.dateStringToYMDFormat(endDate); dateRangeString = `${dateRangeString}${DATE_RANGE_DELIMITER}${endDateYMD}`; } } @@ -73,11 +74,11 @@ export default function DateRangePickerWidget({ let startDate = null; let endDate = null; try { - startDate = convertStringToDate(startDateString); + startDate = DateAndTimeService.convertStringToDate(startDateString); // eslint-disable-next-line no-empty } catch (RangeError) {} try { - endDate = convertStringToDate(endDateString); + endDate = DateAndTimeService.convertStringToDate(endDateString); // eslint-disable-next-line no-empty } catch (RangeError) {} diff --git a/spiffworkflow-frontend/src/routes/MyTasks.tsx b/spiffworkflow-frontend/src/routes/MyTasks.tsx index 5be068dc..6c4c925d 100644 --- a/spiffworkflow-frontend/src/routes/MyTasks.tsx +++ b/spiffworkflow-frontend/src/routes/MyTasks.tsx @@ -8,8 +8,6 @@ import { getPageInfoFromSearchParams, modifyProcessIdentifierForPathParam, refreshAtInterval, - REFRESH_INTERVAL_SECONDS, - REFRESH_TIMEOUT_SECONDS, } from '../helpers'; import HttpService from '../services/HttpService'; import { @@ -19,6 +17,7 @@ import { RecentProcessModel, } from '../interfaces'; import ProcessInstanceRun from '../components/ProcessInstanceRun'; +import DateAndTimeService from '../services/DateAndTimeService'; const PER_PAGE_FOR_TASKS_ON_HOME_PAGE = 5; @@ -47,8 +46,8 @@ export default function MyTasks() { getTasks(); refreshAtInterval( - REFRESH_INTERVAL_SECONDS, - REFRESH_TIMEOUT_SECONDS, + DateAndTimeService.REFRESH_INTERVAL_SECONDS, + DateAndTimeService.REFRESH_TIMEOUT_SECONDS, getTasks ); }, [searchParams]); diff --git a/spiffworkflow-frontend/src/routes/ProcessInstanceShow.tsx b/spiffworkflow-frontend/src/routes/ProcessInstanceShow.tsx index 1fc40d94..b6d55fd7 100644 --- a/spiffworkflow-frontend/src/routes/ProcessInstanceShow.tsx +++ b/spiffworkflow-frontend/src/routes/ProcessInstanceShow.tsx @@ -45,7 +45,6 @@ import ProcessBreadcrumb from '../components/ProcessBreadcrumb'; import HttpService from '../services/HttpService'; import ReactDiagramEditor from '../components/ReactDiagramEditor'; import { - convertSecondsToFormattedDateTime, getLastMilestoneFromProcessInstance, HUMAN_TASK_TYPES, modifyProcessIdentifierForPathParam, @@ -78,6 +77,7 @@ import { errorForDisplayFromString, } from '../components/ErrorDisplay'; import { Notification } from '../components/Notification'; +import DateAndTimeService from '../services/DateAndTimeService'; type OwnProps = { variant: string; @@ -380,7 +380,9 @@ export default function ProcessInstanceShow({ variant }: OwnProps) {
{lastUpdatedTimeLabel}:
- {convertSecondsToFormattedDateTime(lastUpdatedTime || 0) || 'N/A'} + {DateAndTimeService.convertSecondsToFormattedDateTime( + lastUpdatedTime || 0 + ) || 'N/A'}
); @@ -442,7 +444,7 @@ export default function ProcessInstanceShow({ variant }: OwnProps) {
Started:
- {convertSecondsToFormattedDateTime( + {DateAndTimeService.convertSecondsToFormattedDateTime( processInstance.start_in_seconds || 0 )}
diff --git a/spiffworkflow-frontend/src/services/DateAndTimeService.test.tsx b/spiffworkflow-frontend/src/services/DateAndTimeService.test.tsx new file mode 100644 index 00000000..ae6fb488 --- /dev/null +++ b/spiffworkflow-frontend/src/services/DateAndTimeService.test.tsx @@ -0,0 +1,26 @@ +import DateAndTimeService from './DateAndTimeService'; + +test('it can keep the correct date when converting seconds to date', () => { + const dateString = + DateAndTimeService.convertSecondsToFormattedDateString(1666325400); + expect(dateString).toEqual('2022-10-21'); +}); +test('it can properly format a duration', () => { + expect(DateAndTimeService.formatDurationForDisplay(null, '0')).toEqual('0s'); + expect(DateAndTimeService.formatDurationForDisplay(null, '60')).toEqual('1m'); + expect(DateAndTimeService.formatDurationForDisplay(null, '65')).toEqual( + '1m 5s' + ); + expect(DateAndTimeService.formatDurationForDisplay(null, 65)).toEqual( + '1m 5s' + ); + expect(DateAndTimeService.formatDurationForDisplay(null, 86500)).toEqual( + '1d 1m 40s' + ); + expect(DateAndTimeService.formatDurationForDisplay(null, 2629746)).toEqual( + '30d 10h 29m 6s' + ); + expect(DateAndTimeService.formatDurationForDisplay(null, 31536765)).toEqual( + '365d 12m 45s' + ); +}); diff --git a/spiffworkflow-frontend/src/services/DateAndTimeService.tsx b/spiffworkflow-frontend/src/services/DateAndTimeService.tsx new file mode 100644 index 00000000..69e98db6 --- /dev/null +++ b/spiffworkflow-frontend/src/services/DateAndTimeService.tsx @@ -0,0 +1,216 @@ +import { Duration, format } from 'date-fns'; +import { + DATE_TIME_FORMAT, + DATE_FORMAT, + TIME_FORMAT_HOURS_MINUTES, +} from '../config'; +import { isANumber } from '../helpers'; + +const MINUTES_IN_HOUR = 60; +const SECONDS_IN_MINUTE = 60; +const SECONDS_IN_HOUR = MINUTES_IN_HOUR * SECONDS_IN_MINUTE; +const FOUR_HOURS_IN_SECONDS = SECONDS_IN_HOUR * 4; + +const REFRESH_INTERVAL_SECONDS = 5; +const REFRESH_TIMEOUT_SECONDS = FOUR_HOURS_IN_SECONDS; + +const convertDateToSeconds = (date: any, onChangeFunction: any = null) => { + let dateInSeconds = date; + if (date !== null) { + let dateInMilliseconds = date; + if (typeof date.getTime === 'function') { + dateInMilliseconds = date.getTime(); + } + dateInSeconds = Math.floor(dateInMilliseconds / 1000); + } + + if (onChangeFunction) { + onChangeFunction(dateInSeconds); + } else { + return dateInSeconds; + } + + return null; +}; + +const convertDateObjectToFormattedString = (dateObject: Date) => { + if (dateObject) { + return format(dateObject, DATE_FORMAT); + } + return null; +}; + +const dateStringToYMDFormat = (dateString: string) => { + if (dateString && dateString.match(/^\d{2}-\d{2}-\d{4}$/)) { + if (DATE_FORMAT.startsWith('dd')) { + const d = dateString.split('-'); + return `${d[2]}-${d[1]}-${d[0]}`; + } + if (DATE_FORMAT.startsWith('MM')) { + const d = dateString.split('-'); + return `${d[2]}-${d[0]}-${d[1]}`; + } + } + return dateString; +}; + +const convertDateAndTimeStringsToDate = ( + dateString: string, + timeString: string +) => { + if (dateString && timeString) { + return new Date(`${dateStringToYMDFormat(dateString)}T${timeString}`); + } + return null; +}; + +const convertDateAndTimeStringsToSeconds = ( + dateString: string, + timeString: string +) => { + const dateObject = convertDateAndTimeStringsToDate(dateString, timeString); + if (dateObject) { + return convertDateToSeconds(dateObject); + } + return null; +}; + +const convertStringToDate = (dateString: string) => { + return convertDateAndTimeStringsToDate(dateString, '00:10:00'); +}; + +const ymdDateStringToConfiguredFormat = (dateString: string) => { + const dateObject = convertStringToDate(dateString); + if (dateObject) { + return convertDateObjectToFormattedString(dateObject); + } + return null; +}; + +const convertSecondsToDateObject = (seconds: number) => { + if (seconds) { + return new Date(seconds * 1000); + } + return null; +}; + +const convertSecondsToFormattedDateTime = (seconds: number) => { + const dateObject = convertSecondsToDateObject(seconds); + if (dateObject) { + return format(dateObject, DATE_TIME_FORMAT); + } + return null; +}; + +const convertDateObjectToFormattedHoursMinutes = (dateObject: Date) => { + if (dateObject) { + return format(dateObject, TIME_FORMAT_HOURS_MINUTES); + } + return null; +}; + +const convertSecondsToFormattedTimeHoursMinutes = (seconds: number) => { + const dateObject = convertSecondsToDateObject(seconds); + if (dateObject) { + return convertDateObjectToFormattedHoursMinutes(dateObject); + } + return null; +}; + +const convertSecondsToFormattedDateString = (seconds: number) => { + const dateObject = convertSecondsToDateObject(seconds); + if (dateObject) { + return convertDateObjectToFormattedString(dateObject); + } + return null; +}; + +const convertDateStringToSeconds = (dateString: string) => { + const dateObject = convertStringToDate(dateString); + if (dateObject) { + return convertDateToSeconds(dateObject); + } + return null; +}; + +// logic from https://stackoverflow.com/a/28510323/6090676 +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; +}; + +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(' '); +}; + +const formatDateTime = (_row: any, value: any) => { + if (value === undefined || value === null) { + return value; + } + 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 DateAndTimeService = { + REFRESH_INTERVAL_SECONDS, + REFRESH_TIMEOUT_SECONDS, + + convertDateAndTimeStringsToDate, + convertDateAndTimeStringsToSeconds, + convertDateObjectToFormattedHoursMinutes, + convertDateObjectToFormattedString, + convertDateStringToSeconds, + convertDateToSeconds, + convertSecondsToDateObject, + convertSecondsToFormattedDateString, + convertSecondsToFormattedDateTime, + convertSecondsToFormattedTimeHoursMinutes, + convertStringToDate, + dateStringToYMDFormat, + formatDateTime, + formatDurationForDisplay, + secondsToDuration, + ymdDateStringToConfiguredFormat, +}; + +export default DateAndTimeService; diff --git a/spiffworkflow-frontend/src/services/FormattingService.tsx b/spiffworkflow-frontend/src/services/FormattingService.tsx index 4ae7b735..07faa8e6 100644 --- a/spiffworkflow-frontend/src/services/FormattingService.tsx +++ b/spiffworkflow-frontend/src/services/FormattingService.tsx @@ -1,8 +1,9 @@ -import { formatDateTime, formatDurationForDisplay } from '../helpers'; +import DateAndTimeService from './DateAndTimeService'; const spiffFormatFunctions: { [key: string]: Function } = { - convert_seconds_to_date_time_for_display: formatDateTime, - convert_seconds_to_duration_for_display: formatDurationForDisplay, + convert_seconds_to_date_time_for_display: DateAndTimeService.formatDateTime, + convert_seconds_to_duration_for_display: + DateAndTimeService.formatDurationForDisplay, }; const checkForSpiffFormats = (markdown: string) => {