Skip to content

Commit 106a8ee

Browse files
authored
Stop implicitly parsing ISO date strings (#1660)
Now do it explicitly when/where needed. It is too hard to get right currently. The outstanding apollo client issue doesn't have any appearance of resolving this read issue. This change better reflects reality, as we're no longer assuming luxon instances when the values are actually ISO strings. Though I did cheat a little bit and say that they could be either an ISO string or a luxon instance, which isn't really true. But it seemed easiest to go with this for now, and then narrow to luxon instance explicitly with a function call.
1 parent 528e96a commit 106a8ee

File tree

21 files changed

+105
-105
lines changed

21 files changed

+105
-105
lines changed

codegen.schema.yml

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,12 +42,15 @@ generates:
4242
- add:
4343
placement: prepend
4444
content: |
45-
import { DateTime } from 'luxon';
46-
import { CalendarDate, RichTextJson, MarkdownString, InlineMarkdownString } from '~/common';
45+
import { RichTextJson, MarkdownString, InlineMarkdownString } from '~/common';
4746
config:
4847
scalars:
49-
Date: CalendarDate
50-
DateTime: DateTime
48+
Date:
49+
input: ~/common#CalendarDateOrISO
50+
output: ~/common#ISOString
51+
DateTime:
52+
input: ~/common#DateTimeOrISO
53+
output: ~/common#ISOString
5154
JSONObject: Record<string, any>
5255
URL: string
5356
RichText: RichTextJson
Lines changed: 1 addition & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,10 @@
1-
import { DateTime } from 'luxon';
2-
import { CalendarDate } from '~/common';
31
import { Scalars } from '../../schema.graphql';
42

53
export const Parsers: {
64
[K in keyof Scalars]?: (val: any) => Scalars[K]['output'];
7-
} = {
8-
Date: (val) => {
9-
if (DateTime.isDateTime(val)) {
10-
warnOfCacheIrregularity();
11-
return val;
12-
}
13-
return CalendarDate.fromISO(val);
14-
},
15-
DateTime: (val) => {
16-
if (DateTime.isDateTime(val)) {
17-
warnOfCacheIrregularity();
18-
return val;
19-
}
20-
return DateTime.fromISO(val);
21-
},
22-
};
5+
} = {};
236

247
export const optional =
258
<T, R>(parser?: (val: T) => R) =>
269
(val: T | null | undefined): R | null =>
2710
val != null ? parser?.(val) ?? (val as unknown as R) : null;
28-
29-
function warnOfCacheIrregularity() {
30-
console.warn(
31-
`Date value in cache was already transformed.
32-
The thought as of this writing is that this should be avoided to maintain a consistent cache state.
33-
This has happened because the value was written back to the cache directly. aka. writeQuery()/writeFragment()
34-
`
35-
);
36-
}

src/common/CalenderDate.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { Nil } from '@seedcompany/common';
12
import {
23
DateObjectUnits,
34
DateTime,
@@ -11,6 +12,25 @@ import {
1112
ZoneOptions,
1213
} from 'luxon';
1314
import { DefaultValidity, Invalid, Valid } from 'luxon/src/_util';
15+
import { Tagged } from 'type-fest';
16+
17+
export type ISOString = Tagged<string, 'ISOString'>;
18+
19+
export type CalendarDateOrISO = CalendarDate | ISOString;
20+
export type DateTimeOrISO = DateTime | ISOString;
21+
22+
export const asDateTime = <T extends DateTimeOrISO | Nil>(date: T) =>
23+
asLuxonInstance(date, DateTime) as T extends Nil ? null : DateTime;
24+
25+
export const asDate = <T extends CalendarDateOrISO | Nil>(date: T) =>
26+
asLuxonInstance(date, CalendarDate) as T extends Nil ? null : CalendarDate;
27+
28+
function asLuxonInstance(
29+
date: ISOString | DateTime | null | undefined,
30+
cls: typeof DateTime
31+
) {
32+
return !date ? null : cls.isDateTime(date) ? date : cls.fromISO(date);
33+
}
1434

1535
declare module 'luxon/src/datetime' {
1636
interface DateTime {

src/common/LuxonCalenderDateUtils.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ export class LuxonCalenderDateUtils extends LuxonUtils {
1414
}
1515

1616
date = (value?: any) => {
17+
if (typeof value === 'string') {
18+
return CalendarDate.fromISO(value);
19+
}
1720
const d = this.inner.date(value);
1821
return d ? CalendarDate.fromDateTime(d) : null;
1922
};

src/components/EngagementDataGrid/EngagementColumns.tsx

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import {
2323
} from '~/api/schema.graphql';
2424
import {
2525
booleanColumn,
26+
dateColumn,
2627
enumColumn,
2728
getInitialVisibility,
2829
QuickFilterButton,
@@ -181,15 +182,15 @@ export const EngagementColumns: Array<GridColDef<Engagement>> = [
181182
{
182183
headerName: 'MOU Start',
183184
field: 'startDate',
184-
type: 'date',
185-
valueGetter: (_, { startDate }) => startDate.value?.toJSDate(),
185+
...dateColumn(),
186+
valueGetter: dateColumn.valueGetter((_, { startDate }) => startDate.value),
186187
filterable: false,
187188
},
188189
{
189190
headerName: 'MOU End',
190191
field: 'endDate',
191-
type: 'date',
192-
valueGetter: (_, { endDate }) => endDate.value?.toJSDate(),
192+
...dateColumn(),
193+
valueGetter: dateColumn.valueGetter((_, { endDate }) => endDate.value),
193194
filterable: false,
194195
},
195196
{

src/components/FieldOverviewCard/FieldOverviewCard.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@ import {
1010
Typography,
1111
} from '@mui/material';
1212
import { To } from 'history';
13-
import { DateTime } from 'luxon';
1413
import { ReactNode } from 'react';
1514
import { makeStyles } from 'tss-react/mui';
15+
import { DateTimeOrISO } from '~/common';
1616
import { FormattedDateTime } from '../Formatters';
1717
import { HugeIcon, HugeIconProps } from '../Icons';
1818
import { ButtonLink, CardActionAreaLink } from '../Routing';
@@ -50,7 +50,7 @@ const useStyles = makeStyles()(({ spacing, palette }) => ({
5050

5151
interface FieldOverviewCardData {
5252
value?: ReactNode;
53-
updatedAt?: DateTime;
53+
updatedAt?: DateTimeOrISO;
5454
to?: To;
5555
}
5656

src/components/Formatters/FormattedDate.tsx

Lines changed: 18 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,24 @@ import { DateTime, DateTimeFormatOptions } from 'luxon';
44
import { memo, useState } from 'react';
55
import { MergeExclusive } from 'type-fest';
66
import { DateRange } from '~/api/schema.graphql';
7-
import { CalendarDate, Nullable } from '~/common';
7+
import {
8+
asDate,
9+
asDateTime,
10+
CalendarDateOrISO,
11+
DateTimeOrISO,
12+
Nullable,
13+
} from '~/common';
814
import { useLocale } from '../../hooks';
915
import { useDateFormatter, useDateTimeFormatter } from './useDateFormatter';
1016

1117
export const FormattedDate = memo(function FormattedDate({
12-
date,
18+
date: input,
1319
displayOptions,
1420
}: {
15-
date: Nullable<CalendarDate>;
21+
date: Nullable<CalendarDateOrISO>;
1622
displayOptions?: DateTimeFormatOptions;
1723
}) {
18-
date = asLuxonInstance(date, CalendarDate);
24+
const date = asDate(input);
1925

2026
const format = useDateFormatter();
2127
return date ? (
@@ -33,8 +39,8 @@ export const FormattedDateRange = ({
3339
range,
3440
}: MergeExclusive<
3541
{
36-
start: Nullable<CalendarDate>;
37-
end: Nullable<CalendarDate>;
42+
start: Nullable<CalendarDateOrISO>;
43+
end: Nullable<CalendarDateOrISO>;
3844
},
3945
{ range?: DateRange }
4046
>) => {
@@ -60,11 +66,11 @@ FormattedDateRange.orNull = (range: Nullable<DateRange>) =>
6066
);
6167

6268
export const FormattedDateTime = memo(function FormattedDateTime({
63-
date,
69+
date: input,
6470
}: {
65-
date: Nullable<DateTime>;
71+
date: Nullable<DateTimeOrISO>;
6672
}) {
67-
date = asLuxonInstance(date, DateTime);
73+
const date = asDateTime(input);
6874

6975
const format = useDateTimeFormatter();
7076
return date ? (
@@ -75,11 +81,11 @@ export const FormattedDateTime = memo(function FormattedDateTime({
7581
});
7682

7783
export const RelativeDateTime = memo(function RelativeDateTime({
78-
date,
84+
date: input,
7985
}: {
80-
date: DateTime;
86+
date: DateTimeOrISO;
8187
}) {
82-
date = asLuxonInstance(date, DateTime)!;
88+
let date = asDateTime(input);
8389

8490
const locale = useLocale();
8591
date = locale ? date.setLocale(locale) : date;
@@ -114,18 +120,3 @@ export const RelativeDateTime = memo(function RelativeDateTime({
114120
</Tooltip>
115121
);
116122
});
117-
118-
// Under some circumstances, the Apollo scalar read policy is ignored, causing
119-
// this date to just be an ISO string.
120-
// This is just a workaround, to try to prevent errors for users.
121-
// https://github.com/apollographql/apollo-client/issues/9293
122-
function asLuxonInstance(
123-
date: DateTime | null | undefined,
124-
cls: typeof DateTime
125-
) {
126-
return !date
127-
? null
128-
: cls.isDateTime(date)
129-
? date
130-
: cls.fromISO(date as unknown as string);
131-
}

src/components/Grid/ColumnTypes/dateColumn.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,11 @@ import {
88
import { Nil } from '@seedcompany/common';
99
import { DateTime } from 'luxon';
1010
import { DateFilter } from '~/api/schema.graphql';
11-
import { CalendarDate } from '~/common';
11+
import { CalendarDate, ISOString } from '~/common';
1212
import { GridHeaderAddFilterButton } from '../GridHeaderAddFilterButton';
1313
import { column, RowLike } from './definition.types';
1414

15-
type DateInput = Date | CalendarDate | Nil;
15+
type DateInput = Date | CalendarDate | ISOString | Nil;
1616
type DateValue = Date | null;
1717

1818
type DateColDef<R extends RowModel = any> = ColDef<R, DateValue, string>;

src/components/PeriodicReports/OverviewCard/ReportInfo.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { Box, Grid, Skeleton, Typography } from '@mui/material';
33
import { omit } from 'lodash';
44
import { DateTime } from 'luxon';
55
import { ReactNode } from 'react';
6-
import { CalendarDate, StyleProps } from '~/common';
6+
import { asDate, CalendarDateOrISO, StyleProps } from '~/common';
77
import { FormattedDate, FormattedDateTime } from '../../Formatters';
88
import { PaperTooltip } from '../../PaperTooltip';
99
import { Redacted } from '../../Redacted';
@@ -86,13 +86,13 @@ export const SkippedText = () => (
8686
</Grid>
8787
);
8888

89-
export const Due = ({ date }: { date: CalendarDate }) => (
89+
export const Due = ({ date }: { date: CalendarDateOrISO }) => (
9090
<>
9191
Due{' '}
9292
<FormattedDate
9393
date={date}
9494
displayOptions={
95-
date.diffNow('years').years < 1 // same year
95+
asDate(date).diffNow('years').years < 1 // same year
9696
? DATE_SHORT_NO_YEAR
9797
: undefined
9898
}

src/components/PeriodicReports/ReportLabel.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { isSecured, Nullable, SecuredProp } from '~/common';
1+
import { asDate, isSecured, Nullable, SecuredProp } from '~/common';
22
import { Redacted } from '../Redacted';
33
import { PeriodicReportFragment } from './PeriodicReport.graphql';
44

@@ -26,7 +26,8 @@ export const ReportLabel = ({
2626
};
2727

2828
export const getReportLabel = (report?: Report) => {
29-
const { start, end } = report ?? {};
29+
const start = asDate(report?.start);
30+
const end = asDate(report?.end);
3031

3132
if (!start || !end) return null;
3233
return +start === +end

0 commit comments

Comments
 (0)