diff --git a/packages/common/src/components/Filter/DateFilter.tsx b/packages/common/src/components/Filter/DateFilter.tsx index 9e4c4ff23..42e2539f7 100644 --- a/packages/common/src/components/Filter/DateFilter.tsx +++ b/packages/common/src/components/Filter/DateFilter.tsx @@ -1,8 +1,9 @@ import React, { FormEvent, useState } from 'react'; -import { DateTime } from 'luxon'; import { DatePicker, InputGroup, ToolbarFilter } from '@patternfly/react-core'; +import { changeFormatToISODate, isValidDate, parseISOtoJSDate, toISODate } from '../../utils'; + import { FilterTypeProps } from './types'; /** @@ -23,14 +24,10 @@ export const DateFilter = ({ placeholderLabel, showFilter = true, }: FilterTypeProps) => { - const validFilters = - selectedFilters - ?.map((str) => DateTime.fromISO(str)) - ?.filter((dt: DateTime) => dt.isValid) - ?.map((dt: DateTime) => dt.toISODate()) ?? []; + const validFilters = selectedFilters?.map(changeFormatToISODate)?.filter(Boolean) ?? []; // internal state - stored as ISO date string (no time) - const [date, setDate] = useState(DateTime.now().toISODate()); + const [date, setDate] = useState(toISODate(Date.now())); const clearSingleDate = (option) => { onFilterUpdate([...validFilters.filter((d) => d !== option)]); @@ -40,8 +37,8 @@ export const DateFilter = ({ // require full format "YYYY-MM-DD" although partial date is also accepted // i.e. YYYY-MM gets parsed as YYYY-MM-01 and results in auto completing the date // unfortunately due to auto-complete user cannot delete the date char after char - if (value?.length === 10 && DateTime.fromISO(value).isValid) { - const targetDate = DateTime.fromISO(value).toISODate(); + if (value?.length === 10 && isValidDate(value)) { + const targetDate = changeFormatToISODate(value); setDate(targetDate); onFilterUpdate([...validFilters.filter((d) => d !== targetDate), targetDate]); } @@ -58,9 +55,9 @@ export const DateFilter = ({ > DateTime.fromJSDate(date).toISODate()} - dateParse={(str) => DateTime.fromISO(str).toJSDate()} + value={date} + dateFormat={toISODate} + dateParse={parseISOtoJSDate} onChange={onDateChange} aria-label={title} placeholder={placeholderLabel} diff --git a/packages/common/src/components/FilterGroup/matchers.ts b/packages/common/src/components/FilterGroup/matchers.ts index 1a162aea8..014badb2b 100644 --- a/packages/common/src/components/FilterGroup/matchers.ts +++ b/packages/common/src/components/FilterGroup/matchers.ts @@ -1,7 +1,6 @@ import jsonpath from 'jsonpath'; -import { DateTime } from 'luxon'; -import { ResourceField } from '../../utils'; +import { areSameDayInUTCZero, ResourceField } from '../../utils'; import { DateFilter, EnumFilter, FreetextFilter, GroupedEnumFilter, SwitchFilter } from '../Filter'; import { FilterRenderer, ValueMatcher } from './types'; @@ -99,8 +98,7 @@ const groupedEnumMatcher = { const dateMatcher = { filterType: 'date', - matchValue: (value: string) => (filter: string) => - DateTime.fromISO(value).toUTC().hasSame(DateTime.fromISO(filter).toUTC(), 'day'), + matchValue: (value: string) => (filter: string) => areSameDayInUTCZero(value, filter), }; const sliderMatcher = { diff --git a/packages/common/src/utils/__tests__/dates.test.ts b/packages/common/src/utils/__tests__/dates.test.ts new file mode 100644 index 000000000..b09eb9474 --- /dev/null +++ b/packages/common/src/utils/__tests__/dates.test.ts @@ -0,0 +1,81 @@ +import { + areSameDayInUTCZero, + changeFormatToISODate, + changeTimeZoneToUTCZero, + isValidDate, + parseISOtoJSDate, + toISODate, +} from '../dates'; + +describe('changeTimeZoneToUTCZero', () => { + test('from UTC+02:00', () => { + expect(changeTimeZoneToUTCZero('2023-10-31T01:30:00.000+02:00')).toBe( + '2023-10-30T23:30:00.000Z', + ); + }); + test('invalid input', () => { + expect(changeTimeZoneToUTCZero('2023-broken-10-31T01:30:00.000+02:00')).toBe(undefined); + }); +}); + +describe('changeFormatToISODate', () => { + test('from ISO date time with zone', () => { + expect(changeFormatToISODate('2023-10-31T01:30:00.000+02:00')).toBe('2023-10-31'); + }); + test('invalid input', () => { + expect(changeFormatToISODate('2023-broken-10-31T01:30:00.000+02:00')).toBe(undefined); + }); +}); + +describe('toISODate', () => { + test('unix epoch', () => { + expect(toISODate(new Date(0))).toBe('1970-01-01'); + }); + test('missing date', () => { + expect(toISODate(undefined)).toBe(undefined); + }); + test('invalid date', () => { + expect(toISODate(new Date('foo'))).toBe(undefined); + }); +}); + +describe('isValidDate', () => { + test('2023-10-31T01:30:00.000+02:00', () => { + expect(isValidDate('2023-10-31T01:30:00.000+02:00')).toBeTruthy(); + }); + test('invalid string', () => { + expect(isValidDate('2023-broken-10-31')).toBeFalsy(); + }); + test('invalid number of days', () => { + expect(isValidDate('2023-10-60')).toBeFalsy(); + }); +}); + +describe('parseISOtoJSDate', () => { + test('2023-10-31T01:30:00.000+02:00', () => { + const date = parseISOtoJSDate('2023-10-31T01:30:00.000+02:00'); + expect(date.toUTCString()).toBe('Mon, 30 Oct 2023 23:30:00 GMT'); + }); + test('invalid input', () => { + expect(parseISOtoJSDate('2023-broken-10-31T01:30:00.000+02:00')).toBe(undefined); + }); +}); + +describe('areSameDayInUTCZero', () => { + test('the same date', () => { + expect( + areSameDayInUTCZero('2023-10-31T01:30:00.000+02:00', '2023-10-29T23:30:00.000-02:00'), + ).toBeTruthy(); + }); + test('the different dates', () => { + expect( + areSameDayInUTCZero('2023-10-31T10:00:00.000+02:00', '2023-10-29T14:00:00.000-02:00'), + ).toBeFalsy(); + }); + test('one date invalid', () => { + expect(areSameDayInUTCZero('2023-10-31T10:00:00.000+02:00', '2023-foo')).toBeFalsy(); + }); + test('one date missing, one invalid', () => { + expect(areSameDayInUTCZero(undefined, '2023-foo')).toBeFalsy(); + }); +}); diff --git a/packages/common/src/utils/dates.ts b/packages/common/src/utils/dates.ts index 290365528..6fe5163da 100644 --- a/packages/common/src/utils/dates.ts +++ b/packages/common/src/utils/dates.ts @@ -1,12 +1,55 @@ import { DateTime } from 'luxon'; /** - * Converts a given ISO date string in a known format and timezone to a UTC ISO string. + * Converts a given ISO date time string to UTC+00:00 time zone. * - * @param {string} isoDateString - The ISO date string in a known format and timezone. - * @returns {string} The equivalent UTC ISO string if date is valid or undefined otherwise. + * @param {string} isoDateString - The ISO date time string + * @returns {string} The equivalent UTC+00:00 date time ISO string if input is valid or undefined otherwise. */ -export function convertToUTC(isoDateString: string): string | undefined { +export function changeTimeZoneToUTCZero(isoDateString: string): string | undefined { const date = DateTime.fromISO(isoDateString); return date.isValid ? date.toUTC().toISO() : undefined; } + +/** + * Converts a given ISO date time string to ISO date string(no time). + * + * @param {string} isoDateString - The ISO date time string + * @returns {string} The equivalent ISO date string if input is valid or undefined otherwise. + */ +export const changeFormatToISODate = (isoDateString: string): string | undefined => { + // preserve the original zone + const date = DateTime.fromISO(isoDateString, { setZone: true, zone: 'utc' }); + return date.isValid ? date.toISODate() : undefined; +}; + +/** + * Prints JS Date instance as ISO date format (no time) + * @param date + * @returns ISO date string if input is valid or undefined otherwise. + */ +export const toISODate = (date: Date): string => { + const dt = DateTime.fromJSDate(date); + return dt.isValid ? dt.toISODate() : undefined; +}; + +export const isValidDate = (isoDateString: string) => DateTime.fromISO(isoDateString).isValid; + +/** + * + * @param isoDateString + * @returns JS Date instance if input is valid or undefined otherwise. + */ +export const parseISOtoJSDate = (isoDateString: string) => { + const date = DateTime.fromISO(isoDateString); + return date.isValid ? date.toJSDate() : undefined; +}; + +/** + * + * @param a ISO date(time) formatted string + * @param b ISO date(time) formatted string + * @returns true if both dates are on the same day in UTC+00:00 + */ +export const areSameDayInUTCZero = (a: string, b: string): boolean => + DateTime.fromISO(a).toUTC().hasSame(DateTime.fromISO(b).toUTC(), 'day'); diff --git a/packages/forklift-console-plugin/src/modules/Plans/__tests__/__snapshots__/PlanRow.test.tsx.snap b/packages/forklift-console-plugin/src/modules/Plans/__tests__/__snapshots__/PlanRow.test.tsx.snap index f455d2f78..a08b51ec1 100644 --- a/packages/forklift-console-plugin/src/modules/Plans/__tests__/__snapshots__/PlanRow.test.tsx.snap +++ b/packages/forklift-console-plugin/src/modules/Plans/__tests__/__snapshots__/PlanRow.test.tsx.snap @@ -50,7 +50,7 @@ exports[`Plan rows plantest-01 1`] = `
- 2020-10-10T14:04:10.000Z + 2020-10-10T14:04:10Z
- 2020-10-10T14:04:10.000Z + 2020-10-10T14:04:10Z - 2020-10-10T14:04:10.000Z + 2020-10-10T14:04:10Z - 2020-10-10T14:04:10.000Z + 2020-10-10T14:04:10Z - 2020-10-10T14:04:10.000Z + 2020-10-10T14:04:10Z - 2020-10-10T14:04:10.000Z + 2020-10-10T14:04:10Z - 2020-10-10T14:04:10.000Z + 2020-10-10T14:04:10Z - 2020-10-10T14:04:10.000Z + 2020-10-10T14:04:10Z - 2020-10-10T14:04:10.000Z + 2020-10-10T14:04:10Z condition.type === 'Canceled'), ).length || 0, - migrationCompleted: convertToUTC(migration?.completed), - migrationStarted: convertToUTC(migration?.started), + migrationCompleted: migration?.completed, + migrationStarted: migration?.started, latestMigration, object: plan, }),