Skip to content

Commit 0b5ed2f

Browse files
⬆️ deps: Upgrade @mui/x-date-pickers to v6.
Fixes #752. Had to upgrade react monorepo. Had to apply workaround found at testing-library/react-testing-library#1248 (comment).
1 parent e2a2bab commit 0b5ed2f

14 files changed

+592
-385
lines changed

imports/api/publication/useTracker.ts

Lines changed: 21 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -51,24 +51,27 @@ const useTrackerClientImpl = <T = any>(
5151
): T => {
5252
const [iteration, forceUpdate] = useUniqueObject();
5353

54-
const data = useMemo(() => {
55-
let data: T = shouldNeverBeReturned as T;
56-
57-
// Use Tracker.nonreactive in case we are inside a Tracker Computation.
58-
// This can happen if someone calls `ReactDOM.render` inside a Computation.
59-
// In that case, we want to opt out of the normal behavior of nested
60-
// Computations, where if the outer one is invalidated or stopped,
61-
// it stops the inner one.
62-
63-
Tracker.nonreactive(() =>
64-
Tracker.autorun((c: Tracker.Computation) => {
65-
assert(c.firstRun);
66-
data = reactiveFn(c);
67-
}),
68-
).stop();
69-
70-
return data;
71-
}, deps && [iteration, ...deps]);
54+
const data = useMemo(
55+
() => {
56+
let data: T = shouldNeverBeReturned as T;
57+
58+
// Use Tracker.nonreactive in case we are inside a Tracker Computation.
59+
// This can happen if someone calls `ReactDOM.render` inside a Computation.
60+
// In that case, we want to opt out of the normal behavior of nested
61+
// Computations, where if the outer one is invalidated or stopped,
62+
// it stops the inner one.
63+
64+
Tracker.nonreactive(() =>
65+
Tracker.autorun((c: Tracker.Computation) => {
66+
assert(c.firstRun);
67+
data = reactiveFn(c);
68+
}),
69+
).stop();
70+
71+
return data;
72+
},
73+
deps === undefined ? [] : [iteration, ...deps],
74+
);
7275

7376
useEffect(() => {
7477
let prevData = data;

imports/i18n/datetime.ts

Lines changed: 126 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import {useState, useMemo, useEffect} from 'react';
22

3+
import {type PickersLocaleText} from '@mui/x-date-pickers';
4+
35
import dateFormat from 'date-fns/format';
46
import dateFormatDistance from 'date-fns/formatDistance';
57
import dateFormatDistanceStrict from 'date-fns/formatDistanceStrict';
@@ -28,76 +30,169 @@ const localeLoaders: Readonly<Record<string, () => Promise<Locale>>> = {
2830
'fr-BE': async () => import('date-fns/locale/fr/index.js') as Promise<Locale>,
2931
};
3032

31-
const loadLocale = async (key: string): Promise<Locale | undefined> =>
32-
localeLoaders[key]?.();
33+
type LocaleText = Partial<PickersLocaleText<any>>;
3334

34-
export const dateMaskMap = {
35-
'en-US': '__/__/____',
36-
'nl-BE': '__.__.____',
37-
'fr-BE': '__/__/____',
35+
type PickersLocalization = {
36+
components: {
37+
MuiLocalizationProvider: {
38+
defaultProps: {
39+
localeText: LocaleText;
40+
};
41+
};
42+
};
3843
};
3944

40-
export const dateTimeMaskMap = {
41-
'en-US': `${dateMaskMap['en-US']} __:__ _M`,
42-
'nl-BE': `${dateMaskMap['nl-BE']} __:__`,
43-
'fr-BE': `${dateMaskMap['fr-BE']} __:__`,
45+
const pickersLocalizationLoaders: Readonly<
46+
Record<string, () => Promise<PickersLocalization>>
47+
> = {
48+
async 'nl-BE'() {
49+
const module = await import('@mui/x-date-pickers/locales/nlNL.js');
50+
return module.nlNL;
51+
},
52+
async 'fr-BE'() {
53+
const module = await import('@mui/x-date-pickers/locales/frFR.js');
54+
return module.frFR;
55+
},
4456
};
4557

58+
const loadLocale = async (key: string): Promise<Locale | undefined> =>
59+
localeLoaders[key]?.();
60+
61+
const loadPickersLocalization = async (
62+
key: string,
63+
): Promise<PickersLocalization | undefined> =>
64+
pickersLocalizationLoaders[key]?.();
65+
66+
type Cache<T> = Map<string, T>;
67+
4668
const localesCache = new Map<string, Locale | undefined>();
4769

48-
const getLocale = async (owner: string): Promise<Locale | undefined> => {
49-
const key = getSetting(owner, 'lang');
50-
if (localesCache.has(key)) {
51-
return localesCache.get(key);
70+
const pickersLocalizationsCache = new Map<
71+
string,
72+
PickersLocalization | undefined
73+
>();
74+
75+
const _load = async <T>(
76+
kind: string,
77+
cache: Cache<T>,
78+
load: (key: string) => Promise<T>,
79+
key: string,
80+
): Promise<T | undefined> => {
81+
if (cache.has(key)) {
82+
return cache.get(key);
5283
}
5384

54-
return loadLocale(key).then(
55-
(loadedLocale) => {
56-
localesCache.set(key, loadedLocale);
57-
return loadedLocale;
85+
return load(key).then(
86+
(value) => {
87+
cache.set(key, value);
88+
return value;
5889
},
5990
(error) => {
6091
const message = error instanceof Error ? error.message : 'unknown error';
61-
console.error(`failed to load locale ${key}: ${message}`);
92+
console.error(`failed to load ${kind} ${key}: ${message}`);
6293
console.debug({error});
6394
return undefined;
6495
},
6596
);
6697
};
6798

68-
export const useLocale = () => {
69-
const key = useLocaleKey();
70-
const [lastLoadedLocale, setLastLoadedLocale] = useState<Locale | undefined>(
99+
const _getLocale = async (key: string): Promise<Locale | undefined> => {
100+
return _load<Locale | undefined>('locale', localesCache, loadLocale, key);
101+
};
102+
103+
const getLocale = async (owner: string): Promise<Locale | undefined> => {
104+
const key = getSetting(owner, 'lang');
105+
return _getLocale(key);
106+
};
107+
108+
const _getPickersLocalization = async (
109+
key: string,
110+
): Promise<PickersLocalization | undefined> => {
111+
return _load<PickersLocalization | undefined>(
112+
'pickers localization',
113+
pickersLocalizationsCache,
114+
loadPickersLocalization,
115+
key,
116+
);
117+
};
118+
119+
const _pickersLocalizationToLocaleText = (
120+
localization: PickersLocalization | undefined,
121+
) => {
122+
return localization?.components.MuiLocalizationProvider.defaultProps
123+
.localeText;
124+
};
125+
126+
export const getLocaleText = async (
127+
owner: string,
128+
): Promise<LocaleText | undefined> => {
129+
const key = getSetting(owner, 'lang');
130+
const localization = await _getPickersLocalization(key);
131+
return _pickersLocalizationToLocaleText(localization);
132+
};
133+
134+
const useLoadedValue = <T>(
135+
kind: string,
136+
cache: Cache<T>,
137+
load: (key: string) => Promise<T>,
138+
key: string,
139+
): T | undefined => {
140+
const [lastLoadedValue, setLastLoadedValue] = useState<T | undefined>(
71141
undefined,
72142
);
73143

74144
useEffect(() => {
75-
if (localesCache.has(key)) {
76-
setLastLoadedLocale(localesCache.get(key));
145+
if (cache.has(key)) {
146+
setLastLoadedValue(cache.get(key));
77147
return undefined;
78148
}
79149

80150
let isCancelled = false;
81-
loadLocale(key).then(
82-
(loadedLocale) => {
83-
localesCache.set(key, loadedLocale);
151+
load(key).then(
152+
(value) => {
153+
cache.set(key, value);
84154
if (!isCancelled) {
85-
setLastLoadedLocale(loadedLocale);
155+
setLastLoadedValue(value);
86156
}
87157
},
88158
(error) => {
89159
const message =
90160
error instanceof Error ? error.message : 'unknown error';
91-
console.error(`failed to load locale ${key}: ${message}`);
161+
console.error(`failed to load ${kind} ${key}: ${message}`);
92162
console.debug({error});
93163
},
94164
);
95165
return () => {
96166
isCancelled = true;
97167
};
98-
}, [key, setLastLoadedLocale]);
168+
}, [key, setLastLoadedValue]);
169+
170+
return cache.has(key) ? cache.get(key) : lastLoadedValue;
171+
};
172+
173+
export const useLocale = () => {
174+
const key = useLocaleKey();
175+
return useLoadedValue<Locale | undefined>(
176+
'locale',
177+
localesCache,
178+
loadLocale,
179+
key,
180+
);
181+
};
182+
183+
const usePickersLocalization = (key: string) => {
184+
return useLoadedValue<PickersLocalization | undefined>(
185+
'pickers localization',
186+
pickersLocalizationsCache,
187+
loadPickersLocalization,
188+
key,
189+
);
190+
};
99191

100-
return localesCache.has(key) ? localesCache.get(key) : lastLoadedLocale;
192+
export const useLocaleText = () => {
193+
const key = useLocaleKey();
194+
const localization = usePickersLocalization(key);
195+
return _pickersLocalizationToLocaleText(localization);
101196
};
102197

103198
export type WeekStartsOn = WeekDay;
@@ -162,16 +257,6 @@ export const useDefaultDateFormatOptions = () => {
162257
);
163258
};
164259

165-
export const useDateMask = () => {
166-
const key = useLocaleKey();
167-
return dateMaskMap[key];
168-
};
169-
170-
export const useDateTimeMask = () => {
171-
const key = useLocaleKey();
172-
return dateTimeMaskMap[key];
173-
};
174-
175260
const stringifyOptions = (options) => {
176261
if (options === undefined) return undefined;
177262

imports/ui/App.tsx

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,7 @@ import {SnackbarProvider} from 'notistack';
1010

1111
import CssBaseline from '@mui/material/CssBaseline';
1212

13-
import {LocalizationProvider} from '@mui/x-date-pickers/LocalizationProvider';
14-
import {AdapterDateFns} from '@mui/x-date-pickers/AdapterDateFns';
15-
16-
import {useLocale} from '../i18n/datetime';
17-
13+
import DateTimeLocalizationProvider from './i18n/DateTimeLocalizationProvider';
1814
import CustomWholeWindowDropZone from './input/CustomWholeWindowDropZone';
1915
import ModalProvider from './modal/ModelProvider';
2016
import ErrorBoundary from './ErrorBoundary';
@@ -29,12 +25,11 @@ export const muiCache = createCache({
2925
});
3026

3127
const App = () => {
32-
const locale = useLocale();
3328
const theme = useUserTheme();
3429

3530
return (
3631
<BrowserRouter>
37-
<LocalizationProvider dateAdapter={AdapterDateFns} adapterLocale={locale}>
32+
<DateTimeLocalizationProvider>
3833
<CacheProvider value={muiCache}>
3934
<ThemeProvider theme={theme}>
4035
<ModalProvider>
@@ -51,7 +46,7 @@ const App = () => {
5146
</ModalProvider>
5247
</ThemeProvider>
5348
</CacheProvider>
54-
</LocalizationProvider>
49+
</DateTimeLocalizationProvider>
5550
</BrowserRouter>
5651
);
5752
};

imports/ui/appointments/AppointmentDialog.tsx

Lines changed: 15 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,6 @@ import TextField from '../input/TextField';
3131

3232
import CancelButton from '../button/CancelButton';
3333

34-
import {useDateMask} from '../../i18n/datetime';
35-
3634
import {msToString} from '../../api/duration';
3735

3836
import {type AppointmentDocument} from '../../api/collection/appointments';
@@ -153,8 +151,6 @@ const AppointmentDialog = ({
153151
]);
154152
const patientIsReadOnly = Boolean(initialPatient);
155153

156-
const localizedDateMask = useDateMask();
157-
158154
const datetime = unserializeDatetime(date, time);
159155
const appointmentIsInThePast = isBefore(
160156
unserializeDate(date),
@@ -257,21 +253,17 @@ const AppointmentDialog = ({
257253
<Grid container spacing={3}>
258254
<Grid item xs={4}>
259255
<DatePicker
260-
mask={localizedDateMask}
261256
value={unserializeDate(date)}
262257
label="Date"
263-
renderInput={(props) => (
264-
<TextField
265-
{...props}
266-
InputLabelProps={{shrink: true}}
267-
error={!validDate || displayAppointmentIsInThePast}
268-
helperText={
269-
displayAppointmentIsInThePast
270-
? 'Date dans le passé!'
271-
: undefined
272-
}
273-
/>
274-
)}
258+
slotProps={{
259+
textField: {
260+
InputLabelProps: {shrink: true},
261+
error: !validDate || displayAppointmentIsInThePast,
262+
helperText: displayAppointmentIsInThePast
263+
? 'Date dans le passé!'
264+
: undefined,
265+
},
266+
}}
275267
onChange={(pickedDatetime) => {
276268
if (isValid(pickedDatetime)) {
277269
setDate(serializeDate(pickedDatetime!));
@@ -284,13 +276,12 @@ const AppointmentDialog = ({
284276
</Grid>
285277
<Grid item xs={4}>
286278
<TimePicker
287-
renderInput={(props) => (
288-
<TextField
289-
{...props}
290-
InputLabelProps={{shrink: true}}
291-
error={!validTime}
292-
/>
293-
)}
279+
slotProps={{
280+
textField: {
281+
InputLabelProps: {shrink: true},
282+
error: !validTime,
283+
},
284+
}}
294285
label="Time"
295286
value={time === '' ? null : unserializeTime(time)}
296287
onChange={(pickedDatetime) => {

imports/ui/birthdate/useBirthdatePickerProps.ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,14 @@ import endOfToday from 'date-fns/endOfToday';
55
import parseISO from 'date-fns/parseISO';
66
import subYears from 'date-fns/subYears';
77

8-
import {useDateMask} from '../../i18n/datetime';
9-
108
const useBirthdatePickerProps = () => {
11-
const mask = useDateMask();
129
const maxDateString = format(endOfToday(), 'yyyy-MM-dd');
1310

1411
return useMemo(() => {
1512
const maxDate = parseISO(maxDateString);
1613
const minDate = subYears(maxDate, 200);
17-
return {minDate, maxDate, mask};
18-
}, [maxDateString, mask]);
14+
return {minDate, maxDate};
15+
}, [maxDateString]);
1916
};
2017

2118
export default useBirthdatePickerProps;

0 commit comments

Comments
 (0)