Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update mui #395

Merged
merged 13 commits into from
Jan 23, 2025
16,315 changes: 9,312 additions & 7,003 deletions ui/package-lock.json

Large diffs are not rendered by default.

9 changes: 4 additions & 5 deletions ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,9 @@
"@emotion/styled": "^11.14.0",
"@lingui/core": "^4.14.0",
"@lingui/react": "^4.14.0",
"@mui/icons-material": "^5.16.7",
"@mui/lab": "5.0.0-alpha.132",
"@mui/material": "5.14.11",
"@mui/x-date-pickers": "^5.0.20",
"@mui/icons-material": "^6.2.1",
"@mui/material": "^6.2.1",
"@mui/x-date-pickers": "^7.23.2",
"@reduxjs/toolkit": "^1.9.7",
"@types/node": "^20.16.13",
"@types/react": "^18.3.12",
Expand All @@ -31,7 +30,6 @@
"axios": "^1.7.9",
"formik": "^2.4.6",
"formik-mui": "5.0.0-alpha.0",
"formik-mui-lab": "^1.0.0",
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removing some unused libraries

"luxon": "^3.5.0",
"nanoid": "^5.0.9",
"query-string": "^9.1.1",
Expand All @@ -52,6 +50,7 @@
"yup": "^1.5.0"
},
"devDependencies": {
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

npm gave me a warning that this was needed now.

"@babel/types": "^7.26.3",
"@lingui/cli": "^4.14.0",
"@lingui/macro": "^4.14.0",
Expand Down
7 changes: 6 additions & 1 deletion ui/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,12 @@ const ThemedApp = () => {
return (
<ThemeProvider theme={theme}>
<LocalizationProvider dateAdapter={DateAdapter}>
<Router>
<Router
future={{
v7_startTransition: true,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These were recommended for react router during testing.

v7_relativeSplatPath: true,
}}
>
<AppRoutes />
</Router>
</LocalizationProvider>
Expand Down
195 changes: 117 additions & 78 deletions ui/src/components/FormikPickers.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import TextField from "@mui/material/TextField";
import { DateTime } from "luxon";
import { AdapterLuxon as DateAdapter } from "@mui/x-date-pickers/AdapterLuxon";
import { FieldProps, getIn } from "formik";
import {
DesktopDateTimePicker,
DateTimePickerProps,
DesktopDateTimePickerProps,
} from "@mui/x-date-pickers";
import { useLingui } from "@lingui/react";
import { t } from "@lingui/macro";
import { useState } from "react";
import { browserLanguage } from "App";
import { InputBaseProps } from "@mui/material";

// Formik wrapper for Material UI date/time pickers
// adapted from Material-UI picker Formik sample:
Expand All @@ -20,17 +22,18 @@ import { useState } from "react";

interface DatePickerFieldProps
extends FieldProps,
DateTimePickerProps<any, any> {
DesktopDateTimePickerProps<DateTime> {
id?: string;
getShouldDisableDateError: (date: Date) => string;
placeholder?: string;
getShouldDisableDateError: (date: DateTime | null) => string;
size?: "small" | "medium";
style?: React.CSSProperties;
// allow overriding default error messages
// carry-over from legacy KeyboardDateTimePicker
invalidDateMessage?: string;
minDateMessage?: string;
maxDateMessage?: string;
placeholder?: string;
helperText?: string;
}

// for picker format argument, see:
Expand All @@ -41,9 +44,7 @@ const DatePickerField = (props: DatePickerFieldProps) => {
field,
form,
getShouldDisableDateError,
renderInput,
onChange,
value,
invalidDateMessage,
minDateMessage,
maxDateMessage,
Expand All @@ -52,18 +53,75 @@ const DatePickerField = (props: DatePickerFieldProps) => {
const [muiError, setMuiError] = useState<string | null>(null);
const formikError = getIn(form.errors, field.name);
const hasError = formikError ?? muiError;
const localeText = props.localeText ? { ...props.localeText } : {};
if (!localeText?.toolbarTitle) {
localeText.toolbarTitle = i18n._(t`Select date`);
}
if (!localeText?.cancelButtonLabel) {
localeText.cancelButtonLabel = i18n._(t`Cancel`);
}
if (!localeText?.clearButtonLabel) {
localeText.clearButtonLabel = i18n._(t`Clear`);
}
if (!localeText?.okButtonLabel) {
localeText.okButtonLabel = i18n._(t`OK`);
}
if (!localeText?.previousMonth) {
localeText.previousMonth = i18n._(t`Previous month`);
}
if (!localeText?.nextMonth) {
localeText.nextMonth = i18n._(t`Next month`);
}
// x-date-pickers v6 expects the picker's field value to be in the adapter's date/time format - it no longer performs this conversion
// so this wrapper will now handle this conversion
// Luxon adapter doesn't accept Date objects, so convert to a string first.
if (field.value && typeof field.value.toISOString === "function") {
field.value = field.value.toISOString();
}
const adapter = new DateAdapter({ locale: browserLanguage });
const fieldValue = adapter.date(field.value);

// for a11y assign an additional title to the input field separate from the value, since v6 x-date-pickers picker value contains additional non-display characters
// const displayValue = fieldValue
// ? fieldValue.toFormat(other.format ?? "yyyy/LL/dd HH:mm")
// : "";
const displayValue = "";
const inputProps: InputBaseProps["inputProps"] = {
id: props.id,
title: displayValue,
};
if (props.placeholder) {
inputProps.placeholder = props.placeholder;
}

// explicitly use the _Desktop_ variant of DateTimePicker, i.e., DesktopDateTimePicker
// Using the generic DateTimePicker component causes the variant (Desktop or Mobile) to be automatically resolved based on media queries
// UI has not yet been tested on mobile and there are some significant differences to Desktop picker that cause unit tests to fail
return (
<DesktopDateTimePicker
componentsProps={{
slotProps={{
actionBar: {
actions: ["clear", "cancel", "accept"],
},
textField: {
name: field.name,
InputLabelProps: { htmlFor: props.id },
inputProps: inputProps,
style: props.style ?? undefined,
size: props.size ?? "medium",
error: Boolean(hasError),
helperText: hasError ?? props.helperText,
onBlur: () => {
form.setFieldTouched(field.name, true, false);
if (muiError) {
form.setFieldError(field.name, muiError);
} else {
form.validateField(field.name);
}
},
},
}}
value={field.value}
value={fieldValue}
label={props?.label}
onChange={(date) => {
form.setFieldTouched(field.name, true, false);
Expand All @@ -76,7 +134,7 @@ const DatePickerField = (props: DatePickerFieldProps) => {
}}
onError={(code, value) => {
// map error enum to a formik field error
let dt: any;
let dt: DateTime | undefined;
let format: string;
let error: string | null = null;

Expand Down Expand Up @@ -104,16 +162,18 @@ const DatePickerField = (props: DatePickerFieldProps) => {
error = maxDateMessage;
} else {
dt = props?.maxDate ?? props?.maxDateTime;
if (props?.inputFormat) {
format = dt.toFormat(props.inputFormat);
} else {
format = dt.toLocaleString(
props?.maxDate
? DateTime.DATE_SHORT
: DateTime.DATETIME_SHORT,
);
if (dt) {
if (props?.format) {
format = dt.toFormat(props.format);
} else {
format = dt.toLocaleString(
props?.maxDate
? DateTime.DATE_SHORT
: DateTime.DATETIME_SHORT,
);
}
error = i18n._(t`Date can not be after ${format}`);
}
error = i18n._(t`Date can not be after ${format}`);
}
break;
}
Expand All @@ -122,16 +182,18 @@ const DatePickerField = (props: DatePickerFieldProps) => {
error = minDateMessage;
} else {
dt = props?.minDate ?? props?.minDateTime;
if (props?.inputFormat) {
format = dt.toFormat(props.inputFormat);
} else {
format = dt.toLocaleString(
props?.minDate
? DateTime.DATE_SHORT
: DateTime.DATETIME_SHORT,
);
if (dt) {
if (props?.format) {
format = dt.toFormat(props.format);
} else {
format = dt.toLocaleString(
props?.minDate
? DateTime.DATE_SHORT
: DateTime.DATETIME_SHORT,
);
}
error = i18n._(t`Date can not be before ${format}`);
}
error = i18n._(t`Date can not be before ${format}`);
}
break;
}
Expand All @@ -140,18 +202,20 @@ const DatePickerField = (props: DatePickerFieldProps) => {
error = maxDateMessage;
} else {
dt = props?.maxTime ?? props?.maxDateTime;
if (props?.inputFormat) {
format = dt.toFormat(props.inputFormat);
} else {
format = dt.toLocaleString(
props?.maxTime
? DateTime.TIME_SIMPLE
: DateTime.DATETIME_SHORT,
);
if (dt) {
if (props?.format) {
format = dt.toFormat(props.format);
} else {
format = dt.toLocaleString(
props?.maxTime
? DateTime.TIME_SIMPLE
: DateTime.DATETIME_SHORT,
);
}
error = props?.maxTime
? i18n._(t`Time can not be after ${format}`)
: i18n._(t`Date can not be after ${format}`);
}
error = props?.maxTime
? i18n._(t`Time can not be after ${format}`)
: i18n._(t`Date can not be after ${format}`);
}
break;
}
Expand All @@ -160,18 +224,20 @@ const DatePickerField = (props: DatePickerFieldProps) => {
error = minDateMessage;
} else {
dt = props?.minTime ?? props?.minDateTime;
if (props?.inputFormat) {
format = dt.toFormat(props.inputFormat);
} else {
format = dt.toLocaleString(
props?.minTime
? DateTime.TIME_SIMPLE
: DateTime.DATETIME_SHORT,
);
if (dt) {
if (props?.format) {
format = dt.toFormat(props.format);
} else {
format = dt.toLocaleString(
props?.minTime
? DateTime.TIME_SIMPLE
: DateTime.DATETIME_SHORT,
);
}
error = props?.minTime
? i18n._(t`Time can not be before ${format}`)
: i18n._(t`Date can not be before ${format}`);
}
error = props?.minTime
? i18n._(t`Time can not be before ${format}`)
: i18n._(t`Date can not be before ${format}`);
}
break;
}
Expand All @@ -198,34 +264,7 @@ const DatePickerField = (props: DatePickerFieldProps) => {
form.setFieldError(field.name, error);
}
}}
toolbarTitle={props?.toolbarTitle ?? i18n._(t`Select date`)}
renderInput={(inputProps) => {
if (props?.placeholder && inputProps?.inputProps) {
inputProps.inputProps.placeholder = props.placeholder;
}
if (props?.id && inputProps?.inputProps) {
inputProps.inputProps.id = props.id;
}
return (
<TextField
name={field.name}
{...inputProps}
InputLabelProps={{ htmlFor: props.id }}
style={props.style ?? undefined}
size={props.size ?? "medium"}
error={Boolean(hasError)}
helperText={hasError ?? inputProps.helperText}
onBlur={() => {
form.setFieldTouched(field.name, true, false);
if (muiError) {
form.setFieldError(field.name, muiError);
} else {
form.validateField(field.name);
}
}}
/>
);
}}
localeText={localeText}
{...other}
/>
);
Expand Down
9 changes: 5 additions & 4 deletions ui/src/components/__tests__/ActivityTable.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -923,8 +923,8 @@ describe("ActivityTable component", () => {
itemsPerPage: defaultRowsPerPage,
});

const rowsPerPageButton = screen.getByRole("button", {
name: "Rows per page: " + String(defaultRowsPerPage),
const rowsPerPageButton = screen.getByRole("combobox", {
name: "Rows per page:",
});
expect(rowsPerPageButton).toBeInTheDocument();
await user.click(rowsPerPageButton);
Expand All @@ -950,10 +950,11 @@ describe("ActivityTable component", () => {

// ensure Rows per page changes to 20
await waitFor(() => {
const rowsPerPageButton = screen.getByRole("button", {
name: "Rows per page: " + String(20),
const rowsPerPageButton = screen.getByRole("combobox", {
name: "Rows per page:",
});
expect(rowsPerPageButton).toBeInTheDocument();
expect(rowsPerPageButton.innerHTML).toEqual(String(20));
});
});
});
Expand Down
Loading
Loading