diff --git a/public/locales/en/translation.json b/public/locales/en/translation.json index 20850a277f..9ec8d7a6c7 100644 --- a/public/locales/en/translation.json +++ b/public/locales/en/translation.json @@ -82,6 +82,7 @@ "resetRequestInstructions": "We will send a one-time reset link for your account to your email.", "submit": "Submit", "resetFail": "Password reset error", + "resetSuccess": "Password reset successful", "resetDone": "If you have correctly entered your email or username, a password reset link has been sent to your email address.", "backToLogin": "Back To Login" }, @@ -286,6 +287,7 @@ "searchPlaceholder": "Search...", "searchTitle": "Find Users", "emailLabel": "Email", + "emailMessage": "Message (optional)", "toastSuccess": "User added to the project.", "toastFail": "Failed to add user to the project." }, diff --git a/src/components/AnalyticsConsent/index.tsx b/src/components/AnalyticsConsent/index.tsx index 6888243d8c..c2bbae9a7b 100644 --- a/src/components/AnalyticsConsent/index.tsx +++ b/src/components/AnalyticsConsent/index.tsx @@ -1,4 +1,4 @@ -import { Button, Grid, Theme, Typography, useMediaQuery } from "@mui/material"; +import { Button, Grid2, Stack, Typography } from "@mui/material"; import Drawer from "@mui/material/Drawer"; import { ReactElement } from "react"; import { useTranslation } from "react-i18next"; @@ -17,8 +17,6 @@ export default function AnalyticsConsent(props: ConsentProps): ReactElement { const rejectAnalytics = (): void => props.onChangeConsent(false); const clickedAway = (): void => props.onChangeConsent(undefined); - const isXs = useMediaQuery((th) => th.breakpoints.only("xs")); - function ConsentButton(props: { onClick: () => void; text: string; @@ -41,13 +39,12 @@ export default function AnalyticsConsent(props: ConsentProps): ReactElement { onClose={!props.required ? clickedAway : undefined} PaperProps={{ style: { padding: 20 } }} > - - +
{t("analyticsConsent.consentModal.description")} - - - - - - - - - - +
+ + + + + + ); } diff --git a/src/components/AppBar/index.tsx b/src/components/AppBar/index.tsx index d814482f98..23c04f2577 100644 --- a/src/components/AppBar/index.tsx +++ b/src/components/AppBar/index.tsx @@ -1,4 +1,4 @@ -import { AppBar, Grid, Toolbar } from "@mui/material"; +import { AppBar, Stack, Toolbar } from "@mui/material"; import { ReactElement, useEffect, useState } from "react"; import { useLocation } from "react-router"; @@ -29,22 +29,29 @@ export default function AppBarComponent(): ReactElement { style={{ maxHeight: appBarHeight, zIndex: theme.zIndex.drawer + 1 }} > - - + +
{!!getProjectId() && ( )} - - +
+ +
{!!getProjectId() && } - - +
+ +
- - +
+
diff --git a/src/components/Buttons/UndoButton.tsx b/src/components/Buttons/UndoButton.tsx index 72d3d79543..ada25bb818 100644 --- a/src/components/Buttons/UndoButton.tsx +++ b/src/components/Buttons/UndoButton.tsx @@ -1,4 +1,4 @@ -import { Button, Grid } from "@mui/material"; +import { Button } from "@mui/material"; import { ReactElement, useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; @@ -30,35 +30,27 @@ export default function UndoButton(props: UndoButtonProps): ReactElement { } }, [isUndoAllowed, undoDialogOpen]); - return ( - - {isUndoEnabled ? ( -
- - setUndoDialogOpen(false)} - handleConfirm={() => - props.undo().then(() => setUndoDialogOpen(false)) - } - buttonIdCancel={props.buttonIdCancel} - buttonIdConfirm={props.buttonIdConfirm} - /> -
- ) : ( -
- -
- )} -
+ return isUndoEnabled ? ( + <> + + setUndoDialogOpen(false)} + handleConfirm={() => props.undo().then(() => setUndoDialogOpen(false))} + buttonIdCancel={props.buttonIdCancel} + buttonIdConfirm={props.buttonIdConfirm} + /> + + ) : ( + ); } diff --git a/src/components/DataEntry/DataEntryHeader.tsx b/src/components/DataEntry/DataEntryHeader.tsx index 3c7bf6d4d5..ad7a548910 100644 --- a/src/components/DataEntry/DataEntryHeader.tsx +++ b/src/components/DataEntry/DataEntryHeader.tsx @@ -5,7 +5,6 @@ import { useTranslation } from "react-i18next"; import { Key } from "ts-key-enum"; import { SemanticDomainFull } from "api/models"; -import theme from "types/theme"; interface DataEntryHeaderProps { domain: SemanticDomainFull; @@ -23,11 +22,7 @@ export default function DataEntryHeader( const { t } = useTranslation(); return ( - + {t("addWords.domainTitle", { val1: domain.name, val2: domain.id })} {domain.description} diff --git a/src/components/DataEntry/DataEntryTable/NewEntry/VernDialog.tsx b/src/components/DataEntry/DataEntryTable/NewEntry/VernDialog.tsx index 2d9d1f4577..88f913fd68 100644 --- a/src/components/DataEntry/DataEntryTable/NewEntry/VernDialog.tsx +++ b/src/components/DataEntry/DataEntryTable/NewEntry/VernDialog.tsx @@ -1,7 +1,7 @@ import { Dialog, DialogContent, - Grid, + Grid2, ListItemText, MenuList, Typography, @@ -214,51 +214,38 @@ interface DialogListItemTextProps { const DialogListItemText = (props: DialogListItemTextProps): ReactElement => { return ( - - - - {props.text} - - + + {props.text} + + {!!props.word && ( <> {props.showGlosses && ( - - - + )} {props.showDefinitions && ( - - - - )} - {props.showPartOfSpeech && ( - - - - )} - {props.showDomain && ( - - - + )} + {props.showPartOfSpeech && } + {props.showDomain && } )} - + ); }; diff --git a/src/components/DataEntry/DataEntryTable/NewEntry/index.tsx b/src/components/DataEntry/DataEntryTable/NewEntry/index.tsx index 62740c7325..c2511e96e8 100644 --- a/src/components/DataEntry/DataEntryTable/NewEntry/index.tsx +++ b/src/components/DataEntry/DataEntryTable/NewEntry/index.tsx @@ -1,4 +1,4 @@ -import { AutocompleteCloseReason, Grid, Typography } from "@mui/material"; +import { AutocompleteCloseReason, Grid2, Typography } from "@mui/material"; import { CSSProperties, ReactElement, @@ -38,10 +38,10 @@ export enum FocusTarget { Vernacular, } -const gridItemStyle = (spacing: number): CSSProperties => ({ - paddingInline: theme.spacing(spacing), +const gridItemStyle: CSSProperties = { + paddingInline: theme.spacing(1), position: "relative", -}); +}; interface NewEntryProps { analysisLang: WritingSystem; @@ -224,44 +224,43 @@ export default function NewEntry(props: NewEntryProps): ReactElement { }; return ( - - - - - updateVernField(newValue, openDialog) + + + + updateVernField(newValue, openDialog) + } + onBlur={() => setVernOpen(true)} + onClose={(_, reason: AutocompleteCloseReason) => { + // Handle if the user fully types an identical vernacular to a suggestion + // and selects it from the Autocomplete. This should open the dialog. + if (reason === "selectOption") { + // User pressed Enter or Left Click on an item. + setVernOpen(true); } - onBlur={() => setVernOpen(true)} - onClose={(_, reason: AutocompleteCloseReason) => { - // Handle if the user fully types an identical vernacular to a suggestion - // and selects it from the Autocomplete. This should open the dialog. - if (reason === "selectOption") { - // User pressed Enter or Left Click on an item. - setVernOpen(true); - } - }} - onFocus={handleOnVernFocus} - suggestedVerns={suggestedVerns} - // To prevent unintentional no-gloss or wrong-gloss submissions - // and to simplify interactions with Autocomplete and with the dialogs: - // if Enter is pressed from the vern field, move focus to gloss field. - handleEnter={() => focus(FocusTarget.Gloss)} - vernacularLang={vernacularLang} - textFieldId={NewEntryId.TextFieldVern} - onUpdate={() => conditionalFocus(FocusTarget.Vernacular)} - /> - - - - + }} + onFocus={handleOnVernFocus} + suggestedVerns={suggestedVerns} + // To prevent unintentional no-gloss or wrong-gloss submissions + // and to simplify interactions with Autocomplete and with the dialogs: + // if Enter is pressed from the vern field, move focus to gloss field. + handleEnter={() => focus(FocusTarget.Gloss)} + vernacularLang={vernacularLang} + textFieldId={NewEntryId.TextFieldVern} + onUpdate={() => conditionalFocus(FocusTarget.Vernacular)} + /> + + + + conditionalFocus(FocusTarget.Gloss)} /> - - + + + {!selectedDup?.id && ( // note is not available if user selected to modify an existing entry )} - - + + + focus(FocusTarget.Gloss)} /> - - + + + resetState()} /> - + + - + ); } function EnterGrid(): ReactElement { const { t } = useTranslation(); return ( - + {t("addWords.pressEnter")} - + ); } diff --git a/src/components/DataEntry/DataEntryTable/RecentEntry.tsx b/src/components/DataEntry/DataEntryTable/RecentEntry.tsx index 1cbcc67d2b..5e9aab6972 100644 --- a/src/components/DataEntry/DataEntryTable/RecentEntry.tsx +++ b/src/components/DataEntry/DataEntryTable/RecentEntry.tsx @@ -1,4 +1,4 @@ -import { Grid } from "@mui/material"; +import { Grid2 } from "@mui/material"; import { ReactElement, memo, useState } from "react"; import { Pronunciation, Word, WritingSystem } from "api/models"; @@ -76,10 +76,9 @@ export function RecentEntry(props: RecentEntryProps): ReactElement { props.updateNote(props.rowIndex, noteText); return ( - - + - - + + - - + + - - + + - - + + - - + + ); } diff --git a/src/components/DataEntry/DataEntryTable/index.tsx b/src/components/DataEntry/DataEntryTable/index.tsx index 0a119b4c50..5fb71333aa 100644 --- a/src/components/DataEntry/DataEntryTable/index.tsx +++ b/src/components/DataEntry/DataEntryTable/index.tsx @@ -1,9 +1,8 @@ import { ExitToApp, List as ListIcon } from "@mui/icons-material"; -import { Button, Grid, Typography } from "@mui/material"; +import { Button, Grid2, Typography } from "@mui/material"; import { enqueueSnackbar } from "notistack"; import { FormEvent, - Fragment, ReactElement, useCallback, useContext, @@ -1017,38 +1016,23 @@ export default function DataEntryTable( return (
) => e?.preventDefault()}> - - - - + + + {t("addWords.vernacular")} - - - + + + + {t("addWords.glosses")} - + {state.recentWords.map((wordAccess, index) => ( - - + ))} - + - - + - - + {props.hasDrawerButton ? ( - ) : ( - +
)} - - + - - + + ); } diff --git a/src/components/DataEntry/ExistingDataTable/ImmutableExistingData.tsx b/src/components/DataEntry/ExistingDataTable/ImmutableExistingData.tsx index 259c8ff10e..824160e5d7 100644 --- a/src/components/DataEntry/ExistingDataTable/ImmutableExistingData.tsx +++ b/src/components/DataEntry/ExistingDataTable/ImmutableExistingData.tsx @@ -1,4 +1,4 @@ -import { Grid } from "@mui/material"; +import { Grid2 } from "@mui/material"; import { CSSProperties, ReactElement } from "react"; import { Gloss } from "api/models"; @@ -20,20 +20,18 @@ export default function ImmutableExistingData( props: ImmutableExistingDataProps ): ReactElement { return ( - - + {props.vernacular} - - + {props.glosses.map((g, i) => ( ))} - - + + ); } diff --git a/src/components/DataEntry/ExistingDataTable/index.tsx b/src/components/DataEntry/ExistingDataTable/index.tsx index b82930feb5..426e718bb9 100644 --- a/src/components/DataEntry/ExistingDataTable/index.tsx +++ b/src/components/DataEntry/ExistingDataTable/index.tsx @@ -1,4 +1,4 @@ -import { Drawer, Grid, List, SxProps } from "@mui/material"; +import { Drawer, Grid2, List, SxProps } from "@mui/material"; import { Fragment, ReactElement } from "react"; import { SemanticDomain } from "api/models"; @@ -66,9 +66,9 @@ export default function ExistingDataTable( }; const renderSidePanel = (): ReactElement => ( - + {list()} - + ); return props.typeDrawer ? renderDrawer() : renderSidePanel(); diff --git a/src/components/DataEntry/index.tsx b/src/components/DataEntry/index.tsx index e12c109a00..6d67466b89 100644 --- a/src/components/DataEntry/index.tsx +++ b/src/components/DataEntry/index.tsx @@ -1,4 +1,4 @@ -import { Dialog, Divider, Grid, Paper } from "@mui/material"; +import { Dialog, Divider, Grid2, Paper } from "@mui/material"; import { ReactElement, useCallback, @@ -28,7 +28,7 @@ export const treeViewDialogId = "tree-view-dialog"; const paperStyle = { marginInline: "auto", - maxWidth: 800, + maxWidth: 900, padding: theme.spacing(2), }; @@ -54,8 +54,8 @@ export default function DataEntry(): ReactElement { /* This ref is for a container of both the and , * in order to check its height and update the height of the . - * Attach to the because the parent won't shrink to its content, - * but will match the height of its neighbor in . */ + * Attach to the because the parent won't shrink to its content, + * but will match the height of its neighbor in . */ const dataEntryRef = useRef(null); const [domain, setDomain] = useState(newSemanticDomain(id, name, lang)); @@ -112,15 +112,20 @@ export default function DataEntry(): ReactElement { return ( <> {!open && !!domain.guid && ( - - + + - + 0} hideQuestions={() => setQuestionsVisible(false)} @@ -131,7 +136,8 @@ export default function DataEntry(): ReactElement { updateHeight={updateHeight} /> - + + - + )} diff --git a/src/components/Dialogs/UploadImage.tsx b/src/components/Dialogs/UploadImage.tsx index d12cf914b9..a624bdbb0c 100644 --- a/src/components/Dialogs/UploadImage.tsx +++ b/src/components/Dialogs/UploadImage.tsx @@ -1,4 +1,4 @@ -import { Grid, Typography } from "@mui/material"; +import { Grid2, Typography } from "@mui/material"; import { FormEvent, ReactElement, useState } from "react"; import { useTranslation } from "react-i18next"; @@ -53,25 +53,23 @@ export default function ImageUpload(props: ImageUploadProps): ReactElement { {t("createProject.fileSelected", { val: filename })} )} - - - updateFile(file)} - accept="image/*" - > - {t("buttons.browse")} - - - - - {t("buttons.save")} - - - + + + updateFile(file)} + accept="image/*" + > + {t("buttons.browse")} + + + + {t("buttons.save")} + + ); } diff --git a/src/components/Dialogs/ViewImageDialog.tsx b/src/components/Dialogs/ViewImageDialog.tsx index 88e23b474d..98280e9a69 100644 --- a/src/components/Dialogs/ViewImageDialog.tsx +++ b/src/components/Dialogs/ViewImageDialog.tsx @@ -1,4 +1,4 @@ -import { Dialog, DialogContent, DialogTitle, Grid, Icon } from "@mui/material"; +import { Dialog, DialogContent, DialogTitle, Grid2, Icon } from "@mui/material"; import { ReactElement } from "react"; import { CloseButton, DeleteButtonWithDialog } from "components/Buttons"; @@ -39,16 +39,14 @@ export default function ViewImageDialog( - - - - - + + + ); diff --git a/src/components/InvalidLink/index.tsx b/src/components/InvalidLink/index.tsx index 2ba1c9402e..5ce22d8e5b 100644 --- a/src/components/InvalidLink/index.tsx +++ b/src/components/InvalidLink/index.tsx @@ -1,5 +1,12 @@ import { Help } from "@mui/icons-material"; -import { Button, Card, CardContent, Grid, Typography } from "@mui/material"; +import { + Button, + Card, + CardContent, + CardHeader, + Grid2, + Typography, +} from "@mui/material"; import { ReactElement } from "react"; import { useTranslation } from "react-i18next"; import { useNavigate } from "react-router"; @@ -17,44 +24,47 @@ export default function InvalidLink(props: InvalidLinkProps): ReactElement { const idAffix = "invalid-link"; return ( - - + + + + {t(props.textId)} + + } + /> + - - {t(props.textId)} - {/* User Guide, Sign Up, and Log In buttons */} - - + + - + - - - + - - - - + + - + ); } diff --git a/src/components/LandingPage/index.tsx b/src/components/LandingPage/index.tsx index 3ddbac3a5a..b967f019c6 100644 --- a/src/components/LandingPage/index.tsx +++ b/src/components/LandingPage/index.tsx @@ -1,6 +1,6 @@ import { Box, - Grid, + Grid2, Stack, Theme, Typography, @@ -44,16 +44,16 @@ export default function LandingPage(): ReactElement { return ( <> - - + + - - + + - - + + ); diff --git a/src/components/Login/Captcha.tsx b/src/components/Login/Captcha.tsx index ca2ae8cd88..8e740176dd 100644 --- a/src/components/Login/Captcha.tsx +++ b/src/components/Login/Captcha.tsx @@ -6,7 +6,6 @@ import { toast } from "react-toastify"; import { verifyCaptchaToken } from "backend"; import i18n from "i18n"; import { RuntimeConfig } from "types/runtimeConfig"; -import theme from "types/theme"; export interface CaptchaProps { /** Parent function to call when CAPTCHA succeeds or fails. */ @@ -43,7 +42,6 @@ export default function Captcha(props: CaptchaProps): ReactElement { onSuccess={verify} options={{ language: i18n.resolvedLanguage, theme: "light" }} siteKey={RuntimeConfig.getInstance().captchaSiteKey()} - style={{ marginBottom: theme.spacing(1) }} /> ) : ( diff --git a/src/components/Login/Login.tsx b/src/components/Login/Login.tsx index bb5d43ce84..d862fbfd07 100644 --- a/src/components/Login/Login.tsx +++ b/src/components/Login/Login.tsx @@ -3,8 +3,10 @@ import { Button, Card, CardContent, - Grid, + CardHeader, + Grid2, Link, + Stack, TextFieldProps, Typography, } from "@mui/material"; @@ -29,9 +31,9 @@ import { type StoreState } from "rootRedux/types"; import router from "router/browserRouter"; import { Path } from "types/path"; import { RuntimeConfig } from "types/runtimeConfig"; -import theme from "types/theme"; import { NormalizedTextField } from "utilities/fontComponents"; import { openUserGuide } from "utilities/pathUtilities"; +import { meetsPasswordRequirements } from "utilities/utilities"; export enum LoginId { ButtonLogIn = "login-log-in-button", @@ -46,6 +48,7 @@ export enum LoginTextId { ButtonLogin = "login.login", ButtonSignUp = "login.signUp", Error401 = "login.failed", + ErrorPassword = "login.passwordRequirements", ErrorUnknown = "login.failedUnknownReason", FieldError = "login.required", LabelPassword = "login.password", @@ -90,10 +93,11 @@ export default function Login(): ReactElement { const logIn = (e: FormEvent): void => { e.preventDefault(); const p = password.trim(); + const pOk = meetsPasswordRequirements(p); const u = username.trim(); - setPasswordError(!p); + setPasswordError(!pOk); setUsernameError(!u); - if (p && u) { + if (pOk && u) { dispatch(asyncLogIn(u, p)); } }; @@ -108,129 +112,119 @@ export default function Login(): ReactElement { }); return ( - - -
- - {/* Title */} - + + + {/* Title */} + {t(LoginTextId.Title)} + } + /> + + + + + {/* Username field */} + + + {/* Password field */} + + + {/* "Forgot password?" link to reset password */} + {RuntimeConfig.getInstance().emailServicesEnabled() && ( + + router.navigate(Path.PwRequest)} + underline="hover" + variant="subtitle2" + > + {t(LoginTextId.LinkForgotPassword)} + + + )} + + {/* "Failed to log in" */} + {status === LoginStatus.Failure && ( + + {t( + loginError.includes("401") + ? LoginTextId.Error401 + : LoginTextId.ErrorUnknown + )} + + )} + + + + {/* User Guide, Sign Up, and Log In buttons */} + + + + - {/* Username field */} - - - {/* Password field */} - - - {/* "Forgot password?" link to reset password */} - {RuntimeConfig.getInstance().emailServicesEnabled() && ( - - router.navigate(Path.PwRequest)} - underline="hover" - variant="subtitle2" - > - {t(LoginTextId.LinkForgotPassword)} - - - )} - - {/* "Failed to log in" */} - {status === LoginStatus.Failure && ( - - {t( - loginError.includes("401") - ? LoginTextId.Error401 - : LoginTextId.ErrorUnknown - )} - - )} - - - - {/* User Guide, Sign Up, and Log In buttons */} - - - - - - - - - - - - {t(LoginTextId.ButtonLogin)} - - - - - - {/* Login announcement banner */} - {!!banner && ( - - {banner} - - )} - - + + + {t(LoginTextId.ButtonLogin)} + + + + {/* Login announcement banner */} + {!!banner && {banner}} + + +
-
+ ); } diff --git a/src/components/Login/Signup.tsx b/src/components/Login/Signup.tsx index 35ff3f1b3e..5c1756028d 100644 --- a/src/components/Login/Signup.tsx +++ b/src/components/Login/Signup.tsx @@ -2,7 +2,9 @@ import { Button, Card, CardContent, - Grid, + CardHeader, + Grid2, + Stack, TextField, TextFieldProps, Typography, @@ -187,90 +189,89 @@ export default function Signup(props: SignupProps): ReactElement { }); return ( - + -
- - {/* Title */} - + {/* Title */} + {t("login.signUpNew")} - - {/* Name field */} - - - {/* Username field */} - checkUsername()} - /> - - {/* Email field */} - {/* Don't use NormalizedTextField for type="email". - At best, it doesn't normalize, because of the punycode. */} - - - {/* Password field */} - checkPassword1()} - type="password" - /> - - {/* Confirm Password field */} - checkPassword2()} - type="password" - /> - - {/* "Failed to sign up" */} - {!!error && ( - - {t(error)} - - )} - - - - {/* Sign Up and Log In buttons */} - - + } + /> + + + + + {/* Name field */} + + + {/* Username field */} + checkUsername()} + /> + + {/* Email field */} + {/* Don't use NormalizedTextField for type="email". + At best, it doesn't normalize, because of the punycode. */} + + + {/* Password field */} + checkPassword1()} + type="password" + /> + + {/* Confirm Password field */} + checkPassword2()} + type="password" + /> + + {/* "Failed to sign up" */} + {!!error && ( + + {t(error)} + + )} + + + + {/* Sign Up and Log In buttons */} + - - + {t("login.signUp")} - - - -
+ + + +
-
+ ); } diff --git a/src/components/PasswordReset/Request.tsx b/src/components/PasswordReset/Request.tsx index f8acf7d088..ce6ee34c5a 100644 --- a/src/components/PasswordReset/Request.tsx +++ b/src/components/PasswordReset/Request.tsx @@ -1,4 +1,12 @@ -import { Button, Card, Grid, Typography } from "@mui/material"; +import { + Button, + Card, + CardContent, + CardHeader, + Grid2, + Stack, + Typography, +} from "@mui/material"; import { FormEvent, ReactElement, useState } from "react"; import { useTranslation } from "react-i18next"; import { useNavigate } from "react-router"; @@ -40,59 +48,57 @@ export default function ResetRequest(): ReactElement { }; return ( - - - - {t("passwordReset.resetRequestTitle")} - - {isDone ? ( - <> - {t("passwordReset.resetDone")} - + + + + {t("passwordReset.resetRequestTitle")} + + } + /> + + + {isDone ? ( + + {t("passwordReset.resetDone")} + - - - ) : ( - <> - - {t("passwordReset.resetRequestInstructions")} - + + ) : (
- + + + {t("passwordReset.resetRequestInstructions")} + + setEmailOrUsername(e.target.value)} required - type="text" - style={{ width: "100%" }} value={emailOrUsername} - variant="outlined" /> - - + - - + onSubmit, + onClick: onSubmit, variant: "contained", }} disabled={!emailOrUsername || !isVerified} @@ -100,11 +106,11 @@ export default function ResetRequest(): ReactElement { > {t("passwordReset.submit")} - +
- - )} + )} +
-
+ ); } diff --git a/src/components/PasswordReset/ResetPage.tsx b/src/components/PasswordReset/ResetPage.tsx index ae4f6955de..ca5e7b79d0 100644 --- a/src/components/PasswordReset/ResetPage.tsx +++ b/src/components/PasswordReset/ResetPage.tsx @@ -1,5 +1,12 @@ -import ExitToAppIcon from "@mui/icons-material/ExitToApp"; -import { Button, Card, Grid, Typography } from "@mui/material"; +import { + Button, + Card, + CardContent, + CardHeader, + Grid2, + Stack, + Typography, +} from "@mui/material"; import { type FormEvent, type ReactElement, @@ -9,6 +16,7 @@ import { } from "react"; import { useTranslation } from "react-i18next"; import { useNavigate, useParams } from "react-router"; +import { toast } from "react-toastify"; import { resetPassword, validateResetToken } from "backend"; import InvalidLink from "components/InvalidLink"; @@ -16,23 +24,12 @@ import { Path } from "types/path"; import { NormalizedTextField } from "utilities/fontComponents"; import { meetsPasswordRequirements } from "utilities/utilities"; -export enum PasswordResetTestIds { +export enum PasswordResetIds { Password = "PasswordReset.password", - PasswordReqError = "PasswordReset.requirements-error", ConfirmPassword = "PasswordReset.confirm-password", - PasswordMatchError = "PasswordReset.match-error", - PasswordResetFail = "PasswordReset.reset-fail", - BackToLoginButton = "PasswordReset.button.back-to-login", SubmitButton = "PasswordReset.button.submit", } -enum RequestState { - None, - Attempt, - Fail, - Success, -} - export default function PasswordReset(): ReactElement { const navigate = useNavigate(); const { token } = useParams(); @@ -44,7 +41,6 @@ export default function PasswordReset(): ReactElement { const [passwordConfirm, setPasswordConfirm] = useState(""); const [passwordFitsRequirements, setPasswordFitsRequirements] = useState(false); - const [requestState, setRequestState] = useState(RequestState.None); const validateLink = useCallback(async (): Promise => { if (token) { @@ -56,14 +52,8 @@ export default function PasswordReset(): ReactElement { validateLink(); }); - const backToLogin = (e: FormEvent): void => { - e.preventDefault(); - navigate(Path.Login); - }; - const onSubmit = async (e: FormEvent): Promise => { if (token) { - setRequestState(RequestState.Attempt); await asyncReset(token, password); e.preventDefault(); } @@ -81,114 +71,75 @@ export default function PasswordReset(): ReactElement { const asyncReset = async (token: string, password: string): Promise => { if (await resetPassword(token, password)) { - setRequestState(RequestState.Success); - navigate(Path.Login); + toast.success(t("passwordReset.resetSuccess")); } else { - setRequestState(RequestState.Fail); + toast.error(t("passwordReset.resetFail")); } + navigate(Path.Login); }; return isValidLink ? ( - - -
- - {t("passwordReset.resetTitle")} - - - - onChangePassword(e.target.value, passwordConfirm) - } - /> - {!passwordFitsRequirements && ( - - {t("login.passwordRequirements")} - - )} - - - 0} - onChange={(e) => onChangePassword(password, e.target.value)} - /> - {!isPasswordConfirmed && passwordConfirm.length > 0 && ( - - {t("login.confirmPasswordError")} - - )} - + + + + {t("passwordReset.resetTitle")} + + } + /> - - - {requestState === RequestState.Fail ? ( - <> - - {t("passwordReset.resetFail")} - - - - ) : ( - - )} - - - + +
+ + + onChangePassword(e.target.value, passwordConfirm) + } + type="password" + value={password} + /> + + 0} + fullWidth + helperText={ + !isPasswordConfirmed && + passwordConfirm.length > 0 && + t("login.confirmPasswordError") + } + id={PasswordResetIds.ConfirmPassword} + inputProps={{ + "data-testid": PasswordResetIds.ConfirmPassword, + }} + label={t("login.confirmPassword")} + onChange={(e) => onChangePassword(password, e.target.value)} + type="password" + value={passwordConfirm} + /> + + + +
+
-
+ ) : ( ); diff --git a/src/components/PasswordReset/tests/ResetPage.test.tsx b/src/components/PasswordReset/tests/ResetPage.test.tsx index c75acf6967..23a31a5d9e 100644 --- a/src/components/PasswordReset/tests/ResetPage.test.tsx +++ b/src/components/PasswordReset/tests/ResetPage.test.tsx @@ -13,7 +13,7 @@ import { MemoryRouter, Route, Routes } from "react-router"; import configureMockStore from "redux-mock-store"; import PasswordReset, { - PasswordResetTestIds, + PasswordResetIds, } from "components/PasswordReset/ResetPage"; import { Path } from "types/path"; @@ -65,115 +65,56 @@ const customRender = async ( }; describe("PasswordReset", () => { - it("renders with password length error", async () => { + it("disables button when password too short", async () => { const user = userEvent.setup(); await customRender(); const shortPassword = "foo"; - const passwdField = screen.getByTestId(PasswordResetTestIds.Password); - const passwdConfirm = screen.getByTestId( - PasswordResetTestIds.ConfirmPassword - ); + const passwdField = screen.getByTestId(PasswordResetIds.Password); + const passwdConfirm = screen.getByTestId(PasswordResetIds.ConfirmPassword); await user.type(passwdField, shortPassword); await user.type(passwdConfirm, shortPassword); - const reqErrors = screen.queryAllByTestId( - PasswordResetTestIds.PasswordReqError - ); - const confirmErrors = screen.queryAllByTestId( - PasswordResetTestIds.PasswordMatchError - ); - const submitButton = screen.getByTestId(PasswordResetTestIds.SubmitButton); - - expect(reqErrors.length).toBeGreaterThan(0); - expect(confirmErrors.length).toBe(0); + const submitButton = screen.getByTestId(PasswordResetIds.SubmitButton); expect(submitButton.closest("button")).toBeDisabled(); }); - it("renders with password match error", async () => { + it("disables button when passwords don't match", async () => { const user = userEvent.setup(); await customRender(); const passwordEntry = "password"; const confirmEntry = "passward"; - const passwdField = screen.getByTestId(PasswordResetTestIds.Password); - const passwdConfirm = screen.getByTestId( - PasswordResetTestIds.ConfirmPassword - ); + const passwdField = screen.getByTestId(PasswordResetIds.Password); + const passwdConfirm = screen.getByTestId(PasswordResetIds.ConfirmPassword); await user.type(passwdField, passwordEntry); await user.type(passwdConfirm, confirmEntry); - const reqErrors = screen.queryAllByTestId( - PasswordResetTestIds.PasswordReqError - ); - const confirmErrors = screen.queryAllByTestId( - PasswordResetTestIds.PasswordMatchError - ); - const submitButton = screen.getByTestId(PasswordResetTestIds.SubmitButton); - - expect(reqErrors.length).toBe(0); - expect(confirmErrors.length).toBeGreaterThan(0); + const submitButton = screen.getByTestId(PasswordResetIds.SubmitButton); expect(submitButton.closest("button")).toBeDisabled(); }); - it("renders with no password errors", async () => { + it("enables button when passwords are long enough and match", async () => { const user = userEvent.setup(); await customRender(); const passwordEntry = "password"; - const confirmEntry = "password"; - const passwdField = screen.getByTestId(PasswordResetTestIds.Password); - const passwdConfirm = screen.getByTestId( - PasswordResetTestIds.ConfirmPassword - ); + const passwdField = screen.getByTestId(PasswordResetIds.Password); + const passwdConfirm = screen.getByTestId(PasswordResetIds.ConfirmPassword); await user.type(passwdField, passwordEntry); - await user.type(passwdConfirm, confirmEntry); - - const reqErrors = screen.queryAllByTestId( - PasswordResetTestIds.PasswordReqError - ); - const confirmErrors = screen.queryAllByTestId( - PasswordResetTestIds.PasswordMatchError - ); - const submitButton = screen.getByTestId(PasswordResetTestIds.SubmitButton); + await user.type(passwdConfirm, passwordEntry); - expect(reqErrors.length).toBe(0); - expect(confirmErrors.length).toBe(0); + const submitButton = screen.getByTestId(PasswordResetIds.SubmitButton); expect(submitButton.closest("button")).toBeEnabled(); }); - it("renders with expire error", async () => { - // rerender the component with the resetFailure prop set. - const user = userEvent.setup(); - await customRender(); - - const passwordEntry = "password"; - const confirmEntry = "password"; - const passwdField = screen.getByTestId(PasswordResetTestIds.Password); - const passwdConfirm = screen.getByTestId( - PasswordResetTestIds.ConfirmPassword - ); - - await user.type(passwdField, passwordEntry); - await user.type(passwdConfirm, confirmEntry); - - const submitButton = screen.getByTestId(PasswordResetTestIds.SubmitButton); - mockPasswordReset.mockResolvedValueOnce(false); - await user.click(submitButton); - - const resetErrors = screen.queryAllByTestId( - PasswordResetTestIds.PasswordResetFail - ); - expect(resetErrors.length).toBeGreaterThan(0); - }); - it("renders the InvalidLink component if token not valid", async () => { mockValidateResetToken.mockResolvedValueOnce(false); await customRender(); - for (const id of Object.values(PasswordResetTestIds)) { + for (const id of Object.values(PasswordResetIds)) { expect(screen.queryAllByTestId(id)).toHaveLength(0); } // The textId will show up as text because t() is mocked to return its input. diff --git a/src/components/ProjectScreen/CreateProject.tsx b/src/components/ProjectScreen/CreateProject.tsx index 00586ea5e2..e395a9bf3c 100644 --- a/src/components/ProjectScreen/CreateProject.tsx +++ b/src/components/ProjectScreen/CreateProject.tsx @@ -2,7 +2,7 @@ import { Cancel } from "@mui/icons-material"; import { Card, CardContent, - Grid, + Grid2, IconButton, MenuItem, Select, @@ -299,7 +299,7 @@ export default function CreateProject(): ReactElement { /> )} {/* Form submission button */} - {t("createProject.create")} - + diff --git a/src/components/ProjectSettings/BaseSettings.tsx b/src/components/ProjectSettings/BaseSettings.tsx index eba43e01d3..01b4dab253 100644 --- a/src/components/ProjectSettings/BaseSettings.tsx +++ b/src/components/ProjectSettings/BaseSettings.tsx @@ -3,7 +3,8 @@ import { Accordion, AccordionDetails, AccordionSummary, - Grid, + Box, + Stack, Typography, } from "@mui/material"; import { type ReactElement, type ReactNode } from "react"; @@ -15,6 +16,8 @@ interface BaseSettingsProps { icon: ReactNode; /** If maxWidth not specified, defaults to 700px. */ maxWidth?: string | number; + /** If minWidth not specified, defaults to Accordion's default. */ + minWidth?: string | number; /** Setting title (goes second in `AccordionSummary` after icon). */ title: ReactNode; } @@ -29,17 +32,14 @@ export default function BaseSettings(props: BaseSettingsProps): ReactElement { background: (theme) => theme.palette.background.default, border: (theme) => `1px solid ${theme.palette.divider}`, maxWidth: props.maxWidth || "700px", + minWidth: props.minWidth, }} > }> - - - {props.icon} - - - {props.title} - - + + {props.icon} + {props.title} + {props.body} diff --git a/src/components/ProjectSettings/ProjectAutocomplete.tsx b/src/components/ProjectSettings/ProjectAutocomplete.tsx index 5890af880f..0bf3cdd4c7 100644 --- a/src/components/ProjectSettings/ProjectAutocomplete.tsx +++ b/src/components/ProjectSettings/ProjectAutocomplete.tsx @@ -1,5 +1,5 @@ import { HelpOutline } from "@mui/icons-material"; -import { Grid, MenuItem, Select, Tooltip } from "@mui/material"; +import { MenuItem, Select, Stack, Tooltip } from "@mui/material"; import { type ReactElement } from "react"; import { useTranslation } from "react-i18next"; @@ -18,31 +18,28 @@ export default function ProjectAutocomplete( }; return ( - - - - - - - - - - + + + + + + + ); } diff --git a/src/components/ProjectSettings/ProjectDomains.tsx b/src/components/ProjectSettings/ProjectDomains.tsx index ead883e90c..a2a299c7e9 100644 --- a/src/components/ProjectSettings/ProjectDomains.tsx +++ b/src/components/ProjectSettings/ProjectDomains.tsx @@ -9,7 +9,7 @@ import { Dialog, DialogContent, DialogTitle, - Grid, + Grid2, IconButton, Stack, Typography, @@ -313,24 +313,29 @@ export function AddDomainDialog(props: AddDomainDialogProps): ReactElement { return ( - - {t("projectSettings.domains.add")} - + + {t("projectSettings.domains.add")} + +
submit()} + size="small" > t.palette.success.main }} /> + cancel()} + size="small" > t.palette.error.main }} /> - - +
+
+ diff --git a/src/components/ProjectSettings/ProjectImport.tsx b/src/components/ProjectSettings/ProjectImport.tsx index a5144bbc0d..dd7fa02653 100644 --- a/src/components/ProjectSettings/ProjectImport.tsx +++ b/src/components/ProjectSettings/ProjectImport.tsx @@ -1,4 +1,4 @@ -import { Grid, Typography } from "@mui/material"; +import { Grid2, Typography } from "@mui/material"; import { type ReactElement, useEffect, useState } from "react"; import { Trans, useTranslation } from "react-i18next"; import { toast } from "react-toastify"; @@ -75,8 +75,8 @@ export default function ProjectImport( }; return ( - - + + {t("projectSettings.import.body")}{" "} @@ -87,47 +87,41 @@ export default function ProjectImport( FillerTextC - + - - {/* Choose file button */} - - {t("projectSettings.import.chooseFile")} - - + {/* Choose file button */} + + {t("projectSettings.import.chooseFile")} + - - {/* Upload button */} - - {t("buttons.upload")} - - + {/* Upload button */} + + {t("buttons.upload")} + - - {/* Displays the name of the selected file */} - {liftFile && ( - - {t("createProject.fileSelected", { val: liftFile.name })} - - )} - + {/* Displays the name of the selected file */} + {liftFile && ( + + {t("createProject.fileSelected", { val: liftFile.name })} + + )} {liftLangs && ( )} - + ); } diff --git a/src/components/ProjectSettings/ProjectLanguages.tsx b/src/components/ProjectSettings/ProjectLanguages.tsx index e73b576c9a..1136da6b52 100644 --- a/src/components/ProjectSettings/ProjectLanguages.tsx +++ b/src/components/ProjectSettings/ProjectLanguages.tsx @@ -9,11 +9,12 @@ import { } from "@mui/icons-material"; import { Button, - Grid, + Grid2, IconButton, MenuItem, Select, SelectChangeEvent, + Stack, Typography, } from "@mui/material"; import { LanguagePicker, languagePickerStrings_en } from "mui-language-picker"; @@ -192,50 +193,46 @@ export default function ProjectLanguages( ); const addAnalysisLangPicker = (): ReactElement => ( - - - - setNewLang((prev: WritingSystem) => ({ ...prev, bcp47 })) - } - name={newLang.name} - setName={(name: string) => - setNewLang((prev: WritingSystem) => ({ ...prev, name })) - } - font={newLang.font} - setFont={(font: string) => - setNewLang((prev: WritingSystem) => ({ ...prev, font })) - } - setDir={(rtl: boolean) => - setNewLang((prev: WritingSystem) => ({ - ...prev, - rtl: rtl || undefined, - })) - } - t={languagePickerStrings_en} - /> - {" "} - - addAnalysisWritingSystem()} - id={ProjectLanguagesId.ButtonAddAnalysisLangConfirm} - size="large" - > - - - {" "} - - resetState()} - id={ProjectLanguagesId.ButtonAddAnalysisLangClear} - size="large" - > - - - - + + + setNewLang((prev: WritingSystem) => ({ ...prev, bcp47 })) + } + name={newLang.name} + setName={(name: string) => + setNewLang((prev: WritingSystem) => ({ ...prev, name })) + } + font={newLang.font} + setFont={(font: string) => + setNewLang((prev: WritingSystem) => ({ ...prev, font })) + } + setDir={(rtl: boolean) => + setNewLang((prev: WritingSystem) => ({ + ...prev, + rtl: rtl || undefined, + })) + } + t={languagePickerStrings_en} + /> + + addAnalysisWritingSystem()} + id={ProjectLanguagesId.ButtonAddAnalysisLangConfirm} + size="large" + > + + + + resetState()} + id={ProjectLanguagesId.ButtonAddAnalysisLangClear} + size="large" + > + + + ); const vernacularLanguageDisplay = (): ReactElement => ( @@ -258,32 +255,28 @@ export default function ProjectLanguages( ); const vernacularLanguageEditor = (): ReactElement => ( - - - setNewVernName(e.target.value)} - onBlur={() => { - setChangeVernName(false); - setNewVernName(props.project.vernacularWritingSystem.name); - }} - autoFocus - /> - - - - - + + setNewVernName(e.target.value)} + onBlur={() => { + setChangeVernName(false); + setNewVernName(props.project.vernacularWritingSystem.name); + }} + autoFocus + /> + + + ); return ( @@ -328,30 +321,22 @@ function ImmutableWritingSystem( ): ReactElement { const { t } = useTranslation(); + const number = props.index === undefined ? "" : `${props.index + 1}. `; + const wsText: string[] = []; + if (props.ws.name) { + wsText.push(`${t("projectSettings.language.name")}: ${props.ws.name}`); + } + wsText.push(`${t("projectSettings.language.bcp47")}: ${props.ws.bcp47}`); + if (props.ws.font) { + wsText.push(`${t("projectSettings.language.font")}: ${props.ws.font}`); + } + const text = number + wsText.join(", "); + return ( - - {props.index !== undefined && ( - - {`${props.index + 1}. `} - - )} - - {!!props.ws.name && ( - - {`${t("projectSettings.language.name")}: ${props.ws.name}, `} - - )} - - {`${t("projectSettings.language.bcp47")}: ${props.ws.bcp47}`} - - {!!props.ws.font && ( - - {`, ${t("projectSettings.language.font")}: ${props.ws.font}`} - - )} - - {props.buttons} - + + {text} + {props.buttons} + ); } diff --git a/src/components/ProjectSettings/ProjectName.tsx b/src/components/ProjectSettings/ProjectName.tsx index 8db9c8e742..5dbb15b998 100644 --- a/src/components/ProjectSettings/ProjectName.tsx +++ b/src/components/ProjectSettings/ProjectName.tsx @@ -1,4 +1,4 @@ -import { Button, Grid } from "@mui/material"; +import { Button, Stack } from "@mui/material"; import { type ReactElement, useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { toast } from "react-toastify"; @@ -24,27 +24,23 @@ export default function ProjectName(props: ProjectSettingProps): ReactElement { }; return ( - - - setProjName(e.target.value)} - onBlur={() => setProjName(props.project.name)} - /> - - - - - + + setProjName(e.target.value)} + onBlur={() => setProjName(props.project.name)} + /> + + ); } diff --git a/src/components/ProjectSettings/ProjectProtectedOverride.tsx b/src/components/ProjectSettings/ProjectProtectedOverride.tsx index 89b03f82b9..297b84d982 100644 --- a/src/components/ProjectSettings/ProjectProtectedOverride.tsx +++ b/src/components/ProjectSettings/ProjectProtectedOverride.tsx @@ -1,5 +1,5 @@ import { HelpOutline } from "@mui/icons-material"; -import { Grid, MenuItem, Select, Tooltip } from "@mui/material"; +import { MenuItem, Select, Stack, Tooltip } from "@mui/material"; import { type ReactElement } from "react"; import { useTranslation } from "react-i18next"; @@ -18,31 +18,28 @@ export default function ProjectProtectedOverride( }; return ( - - - - - - - - - - + + + + + + + ); } diff --git a/src/components/ProjectSettings/ProjectSchedule/CalendarView.tsx b/src/components/ProjectSettings/ProjectSchedule/CalendarView.tsx index ed4aa71930..20ac40bdeb 100644 --- a/src/components/ProjectSettings/ProjectSchedule/CalendarView.tsx +++ b/src/components/ProjectSettings/ProjectSchedule/CalendarView.tsx @@ -1,4 +1,4 @@ -import { Icon } from "@mui/material"; +import { Grid2, Icon } from "@mui/material"; import { DateCalendar } from "@mui/x-date-pickers"; import dayjs, { Dayjs } from "dayjs"; import { ReactElement } from "react"; @@ -40,5 +40,15 @@ export default function CalendarView(props: CalendarViewProps): ReactElement { return Array.from(new Set(months)).sort().map(dayjs); } - return <>{handleCalendarView(getScheduledMonths(props.projectSchedule))}; + return ( + + {handleCalendarView(getScheduledMonths(props.projectSchedule))} + + ); } diff --git a/src/components/ProjectSettings/ProjectSchedule/DateScheduleEdit.tsx b/src/components/ProjectSettings/ProjectSchedule/DateScheduleEdit.tsx index 1ea9e8cc65..52ace6c37f 100644 --- a/src/components/ProjectSettings/ProjectSchedule/DateScheduleEdit.tsx +++ b/src/components/ProjectSettings/ProjectSchedule/DateScheduleEdit.tsx @@ -1,4 +1,4 @@ -import { Button, Grid } from "@mui/material"; +import { Button, Stack } from "@mui/material"; import { DateCalendar } from "@mui/x-date-pickers"; import dayjs, { Dayjs } from "dayjs"; import { ReactElement, useState } from "react"; @@ -57,7 +57,7 @@ export default function DateScheduleEdit( } return ( - <> + - - - - - - - {t("buttons.confirm")} - - - - + + + + + + {t("buttons.confirm")} + + + ); } diff --git a/src/components/ProjectSettings/ProjectSchedule/DateSelector.tsx b/src/components/ProjectSettings/ProjectSchedule/DateSelector.tsx index 212c4ad9cb..a230c2a12b 100644 --- a/src/components/ProjectSettings/ProjectSchedule/DateSelector.tsx +++ b/src/components/ProjectSettings/ProjectSchedule/DateSelector.tsx @@ -1,4 +1,4 @@ -import { Button, Grid } from "@mui/material"; +import { Button, Stack } from "@mui/material"; import { DatePicker } from "@mui/x-date-pickers/DatePicker"; import { Dayjs } from "dayjs"; import { enqueueSnackbar } from "notistack"; @@ -60,40 +60,40 @@ export default function DateSelector(props: DateSelectorProps): ReactElement { } return ( - <> - setStartDate(newValue)} - /> -    - setEndDate(newValue)} - /> - - - - - - handleSubmit(), - }} - > - {t("buttons.confirm")} - - - - + + + setStartDate(newValue)} + /> + + setEndDate(newValue)} + /> + + + + + + handleSubmit(), + }} + > + {t("buttons.confirm")} + + + ); } diff --git a/src/components/ProjectSettings/ProjectSchedule/index.tsx b/src/components/ProjectSettings/ProjectSchedule/index.tsx index aa83497be6..46e1cbd05a 100644 --- a/src/components/ProjectSettings/ProjectSchedule/index.tsx +++ b/src/components/ProjectSettings/ProjectSchedule/index.tsx @@ -1,5 +1,5 @@ import { CalendarMonth, DateRange, EventRepeat } from "@mui/icons-material"; -import { Button, Grid, Typography } from "@mui/material"; +import { Button, Grid2, Typography } from "@mui/material"; import { type ReactElement, useCallback, useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import Modal from "react-modal"; @@ -57,21 +57,14 @@ export default function ProjectSchedule( return ( <> - + {!props.readOnly && ( - } @@ -91,19 +84,11 @@ export default function ProjectSchedule( onClick={() => setShowRemove(true)} buttonId={"Project-Schedule-removeDays"} /> - + )} - - - - + + + {t("projectSettings.schedule.removeAll")} - - + + - - + + - - + + ); diff --git a/src/components/ProjectSettings/index.tsx b/src/components/ProjectSettings/index.tsx index 92d36342f9..2653dc2195 100644 --- a/src/components/ProjectSettings/index.tsx +++ b/src/components/ProjectSettings/index.tsx @@ -305,6 +305,7 @@ export default function ProjectSettingsComponent(): ReactElement { /> } maxWidth="1050px" // Comfortably fits three months + minWidth="300px" // Ensures space for one month /> diff --git a/src/components/ProjectUsers/AddProjectUsers.tsx b/src/components/ProjectUsers/AddProjectUsers.tsx index 0e1c757950..580bc7557a 100644 --- a/src/components/ProjectUsers/AddProjectUsers.tsx +++ b/src/components/ProjectUsers/AddProjectUsers.tsx @@ -1,4 +1,4 @@ -import { Button, Grid, Typography } from "@mui/material"; +import { Button, Stack, Typography } from "@mui/material"; import { ReactElement, useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import Modal from "react-modal"; @@ -59,31 +59,25 @@ export default function AddProjectUsers( } return ( - <> - - - + + {RuntimeConfig.getInstance().emailServicesEnabled() && ( - - - {t("projectSettings.invite.or")} - + <> + {t("projectSettings.invite.or")} - - - - + + )} {RuntimeConfig.getInstance().emailServicesEnabled() && ( @@ -99,6 +93,6 @@ export default function AddProjectUsers( /> )} - + ); } diff --git a/src/components/ProjectUsers/EmailInvite.tsx b/src/components/ProjectUsers/EmailInvite.tsx index fdf4acda5e..a9f93dcb34 100644 --- a/src/components/ProjectUsers/EmailInvite.tsx +++ b/src/components/ProjectUsers/EmailInvite.tsx @@ -1,4 +1,4 @@ -import { Card, CardContent, Grid, Typography } from "@mui/material"; +import { Box, Grid2, Stack, Typography } from "@mui/material"; import { ReactElement, useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { toast } from "react-toastify"; @@ -47,48 +47,45 @@ export default function EmailInvite(props: InviteProps): ReactElement { }, [email]); return ( - - - + + + {t("projectSettings.invite.inviteByEmailLabel")} + setEmail(e.target.value)} - variant="outlined" - style={{ width: "100%" }} - margin="normal" - autoFocus - inputProps={{ maxLength: 100 }} + required + slotProps={{ htmlInput: { maxLength: 320 } }} /> + setMessage(e.target.value)} - variant="outlined" - style={{ width: "100%" }} - margin="normal" + slotProps={{ htmlInput: { maxLength: 10000 } }} /> - - - onSubmit(), - variant: "contained", - color: "primary", - }} - > - {t("buttons.invite")} - - - - - + + + onSubmit(), + variant: "contained", + }} + > + {t("buttons.invite")} + + + + ); } diff --git a/src/components/ProjectUsers/UserList.tsx b/src/components/ProjectUsers/UserList.tsx index 732981b5d6..1dc2c05752 100644 --- a/src/components/ProjectUsers/UserList.tsx +++ b/src/components/ProjectUsers/UserList.tsx @@ -2,7 +2,6 @@ import { Done } from "@mui/icons-material"; import { Avatar, Button, - Grid, List, ListItem, ListItemIcon, @@ -114,7 +113,7 @@ export default function UserList(props: UserListProps): ReactElement { }; return ( - +
{t("projectSettings.invite.searchTitle")} updateUsers(e.target.value)} @@ -125,6 +124,6 @@ export default function UserList(props: UserListProps): ReactElement { {filteredInProj.map(inProjListItem)} {filteredNotInProj.map(notInProjListItem)} - +
); } diff --git a/src/components/SiteSettings/Banners.tsx b/src/components/SiteSettings/Banners.tsx index 4b8090d228..f318fb4fcb 100644 --- a/src/components/SiteSettings/Banners.tsx +++ b/src/components/SiteSettings/Banners.tsx @@ -1,4 +1,4 @@ -import { Button, Grid } from "@mui/material"; +import { Button, Stack } from "@mui/material"; import { ChangeEvent, ReactElement, useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; @@ -9,11 +9,11 @@ import { NormalizedTextField } from "utilities/fontComponents"; export default function Banners(): ReactElement { return ( - <> + {[BannerType.Announcement, BannerType.Login].map((type) => ( ))} - + ); } @@ -57,26 +57,19 @@ function Banner(props: BannerProps): ReactElement { }; return ( - - - - + + - + - - - - + + ); } diff --git a/src/components/SiteSettings/ProjectManagement/ProjectUsersButtonWithConfirmation.tsx b/src/components/SiteSettings/ProjectManagement/ProjectUsersButtonWithConfirmation.tsx index 509e47e5ed..2de2991843 100644 --- a/src/components/SiteSettings/ProjectManagement/ProjectUsersButtonWithConfirmation.tsx +++ b/src/components/SiteSettings/ProjectManagement/ProjectUsersButtonWithConfirmation.tsx @@ -39,7 +39,7 @@ export default function ProjectUsersButtonWithConfirmation( {t("siteSettings.projectRoles")} setOpen(false)} open={open}> - + {t("siteSettings.projectRoles")} setOpen(false)} /> diff --git a/src/components/SiteSettings/UserManagement/ConfirmDeletion.tsx b/src/components/SiteSettings/UserManagement/ConfirmDeletion.tsx index 69d2a58040..639e9657b4 100644 --- a/src/components/SiteSettings/UserManagement/ConfirmDeletion.tsx +++ b/src/components/SiteSettings/UserManagement/ConfirmDeletion.tsx @@ -1,11 +1,4 @@ -import { - Button, - Grid, - Typography, - CardContent, - Card, - CardActions, -} from "@mui/material"; +import { Box, Button, Stack, Typography } from "@mui/material"; import { Fragment, ReactElement } from "react"; import { useTranslation } from "react-i18next"; @@ -26,63 +19,44 @@ export default function ConfirmDeletion( return ; } return ( - <> - - - - - {props.user.username} + + + + {props.user.username} + + + + {t("siteSettings.deleteUser.confirm")} + + + + + + - - - - -
- - - - + + + + ); } diff --git a/src/components/SiteSettings/UserManagement/UserList.tsx b/src/components/SiteSettings/UserManagement/UserList.tsx index 300c42da7c..24b11dfb52 100644 --- a/src/components/SiteSettings/UserManagement/UserList.tsx +++ b/src/components/SiteSettings/UserManagement/UserList.tsx @@ -2,13 +2,13 @@ import { DeleteForever, VpnKey } from "@mui/icons-material"; import { Avatar, Button, - Grid, List, ListItem, ListItemAvatar, ListItemIcon, ListItemText, SelectChangeEvent, + Stack, Typography, } from "@mui/material"; import { ReactElement, useCallback, useEffect, useState } from "react"; @@ -101,9 +101,9 @@ export default function UserList(props: UserListProps): ReactElement { }; return ( - + <> {t("projectSettings.invite.searchTitle")} - + setFilterInput(e.target.value)} placeholder={t("projectSettings.invite.searchPlaceholder")} @@ -116,8 +116,8 @@ export default function UserList(props: UserListProps): ReactElement { } onReverseClick={() => setReverseSorting(!reverseSorting)} /> - + {sortedUsers.map(userListItem)} - + ); } diff --git a/src/components/SiteSettings/UserManagement/index.tsx b/src/components/SiteSettings/UserManagement/index.tsx index bfff55a4c7..0a5a12dec2 100644 --- a/src/components/SiteSettings/UserManagement/index.tsx +++ b/src/components/SiteSettings/UserManagement/index.tsx @@ -1,4 +1,3 @@ -import { Grid } from "@mui/material"; import { ReactElement, useCallback, useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import Modal from "react-modal"; @@ -67,9 +66,7 @@ export default function UserManagement(): ReactElement { return ( <> - - - + + {t("siteSettings.projectList")} - + } value={SiteSettingsTab.Projects} /> @@ -51,10 +51,10 @@ export default function SiteSettings(): ReactElement { data-testid={SiteSettingsTab.Users} id={SiteSettingsTab.Users.toString()} label={ - + {t("siteSettings.userList")} - + } value={SiteSettingsTab.Users} /> @@ -62,10 +62,10 @@ export default function SiteSettings(): ReactElement { data-testid={SiteSettingsTab.Banners} id={SiteSettingsTab.Banners.toString()} label={ - + {t("siteSettings.banners.title")} - + } value={SiteSettingsTab.Banners} /> diff --git a/src/components/Statistics/DomainStatistics.tsx b/src/components/Statistics/DomainStatistics.tsx index d8a7fea5d9..202df51bfb 100644 --- a/src/components/Statistics/DomainStatistics.tsx +++ b/src/components/Statistics/DomainStatistics.tsx @@ -1,10 +1,17 @@ -import { Card, Grid, ListItem, List } from "@mui/material"; +import { + Paper, + Table, + TableBody, + TableContainer, + TableHead, + TableRow, +} from "@mui/material"; import { ReactElement, useState, useEffect } from "react"; -import { SemanticDomainCount, SemanticDomainTreeNode } from "api/models"; +import { SemanticDomainCount } from "api/models"; import { getSemanticDomainCounts } from "backend"; import * as LocalStorage from "backend/localStorage"; -import { ColumnHead, TableCell } from "components/Statistics/TableCells"; +import { Cell, HeadCell } from "components/Statistics/TableCells"; interface DomainStatisticsProps { lang: string; @@ -38,40 +45,25 @@ export default function DomainStatistics( } return ( - - - - - - - - - - + + + + + + + + + + {statisticsList.map((t) => ( - + + + + + ))} - - - - ); -} - -function TableRow(props: { - dom: SemanticDomainTreeNode; - count: number; -}): ReactElement { - return ( - - - - - - - + +
+
); } diff --git a/src/components/Statistics/Statistics.tsx b/src/components/Statistics/Statistics.tsx index c9d63120fa..a38a33b087 100644 --- a/src/components/Statistics/Statistics.tsx +++ b/src/components/Statistics/Statistics.tsx @@ -1,6 +1,6 @@ import { Divider, - Grid, + Grid2, ListItemButton, ListItemText, Typography, @@ -65,24 +65,6 @@ export default function Statistics(): ReactElement { } } - function handleDisplay(): ReactElement[] { - return [ - - - {t("statistics.title", { val: currentProject?.name })} - - , - - - {t(`statistics.view.${viewName}`)} - - , - - {componentToDisplay(viewName as viewEnum)} - , - ]; - } - function handleButton(): ReactElement { return ( @@ -118,23 +100,28 @@ export default function Statistics(): ReactElement { } return ( - - - {handleButton()} - - + {handleButton()} + + - {handleDisplay()} - - + + {t("statistics.title", { val: currentProject?.name })} + + + {t(`statistics.view.${viewName}`)} + + {componentToDisplay(viewName as viewEnum)} + + + - - + + ); } diff --git a/src/components/Statistics/TableCells.tsx b/src/components/Statistics/TableCells.tsx index 0c698852de..c57ce0142e 100644 --- a/src/components/Statistics/TableCells.tsx +++ b/src/components/Statistics/TableCells.tsx @@ -1,38 +1,27 @@ -import { Grid, Typography } from "@mui/material"; -import { ReactElement } from "react"; +import { TableCell, Typography } from "@mui/material"; +import { CSSProperties, ReactElement } from "react"; import { useTranslation } from "react-i18next"; -export function ColumnHead(props: { titleId: string }): ReactElement { +const cellStyle: CSSProperties = { + borderBottomStyle: "dotted", + borderBottomWidth: 1, + position: "relative", +}; + +export function HeadCell(props: { titleId: string }): ReactElement { const { t } = useTranslation(); + return ( - + {t(props.titleId)} - + ); } -export function TableCell(props: { - text?: string | number | null; -}): ReactElement { +export function Cell(props: { text?: string | number | null }): ReactElement { return ( - + {props.text} - + ); } diff --git a/src/components/Statistics/UserStatistics.tsx b/src/components/Statistics/UserStatistics.tsx index 63b434cfb6..91267a4bdf 100644 --- a/src/components/Statistics/UserStatistics.tsx +++ b/src/components/Statistics/UserStatistics.tsx @@ -1,10 +1,17 @@ -import { Card, Grid, ListItem, List } from "@mui/material"; +import { + Paper, + Table, + TableBody, + TableContainer, + TableHead, + TableRow, +} from "@mui/material"; import { ReactElement, useState, useEffect } from "react"; import { SemanticDomainUserCount } from "api/models"; import { getSemanticDomainUserCount } from "backend"; import * as LocalStorage from "backend/localStorage"; -import { ColumnHead, TableCell } from "components/Statistics/TableCells"; +import { Cell, HeadCell } from "components/Statistics/TableCells"; interface UserStatisticsProps { lang: string; @@ -38,33 +45,25 @@ export default function UserStatistics( } return ( - - - - - - - - - - + + + + + + + + + + {domainUserCountList.map((t) => ( - + + + + + ))} - - - - ); -} - -function TableRow(props: { counts: SemanticDomainUserCount }): ReactElement { - return ( - - - - - - - + +
+
); } diff --git a/src/components/Statistics/tests/DomainStatistics.test.tsx b/src/components/Statistics/tests/DomainStatistics.test.tsx index 1cb640243a..c5050295a7 100644 --- a/src/components/Statistics/tests/DomainStatistics.test.tsx +++ b/src/components/Statistics/tests/DomainStatistics.test.tsx @@ -43,8 +43,8 @@ describe("DomainStatistics", () => { expect(mockGetProjectId).toHaveBeenCalled(); }); - test("all list items are present", async () => { - const listItems = screen.queryAllByRole("listitem"); - expect(listItems.length).toEqual(mockSemanticDomainCountArray.length); + test("all rows are present", async () => { + const listItems = screen.queryAllByRole("row"); + expect(listItems.length).toEqual(mockSemanticDomainCountArray.length + 1); }); }); diff --git a/src/components/Statistics/tests/Statistics.test.tsx b/src/components/Statistics/tests/Statistics.test.tsx index 741a9739c9..9f983ca06a 100644 --- a/src/components/Statistics/tests/Statistics.test.tsx +++ b/src/components/Statistics/tests/Statistics.test.tsx @@ -35,7 +35,7 @@ beforeEach(async () => { render( - {" "} + ); diff --git a/src/components/Statistics/tests/UserStatistics.test.tsx b/src/components/Statistics/tests/UserStatistics.test.tsx index b26e73a876..8920c95f71 100644 --- a/src/components/Statistics/tests/UserStatistics.test.tsx +++ b/src/components/Statistics/tests/UserStatistics.test.tsx @@ -42,8 +42,10 @@ describe("UserStatistics", () => { expect(mockGetProjectId).toHaveBeenCalled(); }); - test("all list items are present", async () => { - const listItems = screen.queryAllByRole("listitem"); - expect(listItems.length).toEqual(mockSemanticDomainUserCountArray.length); + test("all rows are present", async () => { + const listItems = screen.queryAllByRole("row"); + expect(listItems.length).toEqual( + mockSemanticDomainUserCountArray.length + 1 + ); }); }); diff --git a/src/components/UserSettings/UserSettings.tsx b/src/components/UserSettings/UserSettings.tsx index ac80f402f0..8ea1b251cb 100644 --- a/src/components/UserSettings/UserSettings.tsx +++ b/src/components/UserSettings/UserSettings.tsx @@ -3,9 +3,10 @@ import { Button, Card, CardContent, - Grid, + Grid2, MenuItem, Select, + Stack, TextField, Tooltip, Typography, @@ -133,16 +134,16 @@ export function UserSettings(props: { } return ( - + -
onSubmit(e)}> - - - - - - - + + onSubmit(e)}> + + {/* id: avatar, name, username */} + + + + + - - + + - - - - {t("userSettings.contact")} - - + {/* contact: phone, email */} + + + {t("userSettings.contact")} + - - - - - + + + + setPhone(e.target.value)} type="tel" /> - - - - - - - - + + + + + + + - - - + + + - - - - {t("userSettings.uiLanguage")} - - + {/* ui language */} + + + {t("userSettings.uiLanguage")} + - - setUiLang(e.target.value ?? "")} + /* Use `displayEmpty` and a conditional `renderValue` function to force + * something to appear when the menu is closed and its value is "" */ + displayEmpty + renderValue={ + uiLang + ? undefined + : () => t("userSettings.uiLanguageDefault") + } + > + + {t("userSettings.uiLanguageDefault")} + + {uiWritingSystems.map((ws) => ( + + {`${ws.bcp47} (${ws.name})`} - {uiWritingSystems.map((ws) => ( - - {`${ws.bcp47} (${ws.name})`} - - ))} - - - + ))} + + - - - - {t("userSettings.glossSuggestion")} - - + {/* gloss spelling suggestions */} + + + {t("userSettings.glossSuggestion")} + - + - - - - + + + {/* analytics consent */} {!RuntimeConfig.getInstance().isOffline() && ( - - - - {t("userSettings.analyticsConsent.title")} - - - - - - {t( - analyticsOn - ? "userSettings.analyticsConsent.consentYes" - : "userSettings.analyticsConsent.consentNo" - )} - - - - - - + + + {t("userSettings.analyticsConsent.title")} + + + + {t( + analyticsOn + ? "userSettings.analyticsConsent.consentYes" + : "userSettings.analyticsConsent.consentNo" + )} + + + + {displayConsent && ( )} - + )} - + {/* save button */} + - - - -
+
+ + +
-
+ ); } diff --git a/src/components/WordCard/DomainChipsGrid.tsx b/src/components/WordCard/DomainChipsGrid.tsx index 19a994e139..3b8b714cd8 100644 --- a/src/components/WordCard/DomainChipsGrid.tsx +++ b/src/components/WordCard/DomainChipsGrid.tsx @@ -1,4 +1,4 @@ -import { Grid } from "@mui/material"; +import { Grid2 } from "@mui/material"; import { type ReactElement } from "react"; import { type SemanticDomain } from "api/models"; @@ -26,12 +26,14 @@ export default function DomainChipsGrid( }; return ( - + {props.semDoms.map((d) => ( - - - + ))} - + ); } diff --git a/src/components/WordCard/SenseCard.tsx b/src/components/WordCard/SenseCard.tsx index 4841f5b9ff..3cc1d43591 100644 --- a/src/components/WordCard/SenseCard.tsx +++ b/src/components/WordCard/SenseCard.tsx @@ -28,12 +28,7 @@ export default function SenseCard(props: SenseCardProps): ReactElement { const semDoms = props.sense.semanticDomains; return ( - + {/* Part of speech (if any) */}
diff --git a/src/components/WordCard/SummarySenseCard.tsx b/src/components/WordCard/SummarySenseCard.tsx index d07dee9f36..54d8702c71 100644 --- a/src/components/WordCard/SummarySenseCard.tsx +++ b/src/components/WordCard/SummarySenseCard.tsx @@ -1,4 +1,4 @@ -import { Card, CardContent, Chip, Grid, Typography } from "@mui/material"; +import { Card, CardContent, Chip, Grid2, Typography } from "@mui/material"; import { ReactElement } from "react"; import { useTranslation } from "react-i18next"; @@ -57,13 +57,11 @@ export default function SummarySenseCard( /> {/* Semantic domain numbers */} - + {domIds.map((id) => ( - - - + ))} - + ); diff --git a/src/components/WordCard/index.tsx b/src/components/WordCard/index.tsx index a91016cccd..e4a974cc30 100644 --- a/src/components/WordCard/index.tsx +++ b/src/components/WordCard/index.tsx @@ -5,6 +5,7 @@ import { CardContent, CardHeader, IconButton, + Stack, Typography, } from "@mui/material"; import { Fragment, ReactElement, useEffect, useState } from "react"; @@ -130,14 +131,16 @@ export default function WordCard(props: WordCardProps): ReactElement { {/* Senses */} {full ? ( - senses.map((s) => ( - - )) + + {senses.map((s) => ( + + ))} + ) : ( )} diff --git a/src/goals/CharacterInventory/CharInv/CharacterDetail/index.tsx b/src/goals/CharacterInventory/CharInv/CharacterDetail/index.tsx index 8580c3b240..c66b3541bc 100644 --- a/src/goals/CharacterInventory/CharInv/CharacterDetail/index.tsx +++ b/src/goals/CharacterInventory/CharInv/CharacterDetail/index.tsx @@ -1,5 +1,5 @@ import { Close } from "@mui/icons-material"; -import { Grid, IconButton, Typography } from "@mui/material"; +import { Grid2, IconButton, Typography } from "@mui/material"; import { ReactElement } from "react"; import CharacterInfo from "goals/CharacterInventory/CharInv/CharacterDetail/CharacterInfo"; @@ -18,7 +18,7 @@ export default function CharacterDetail( props: CharacterDetailProps ): ReactElement { return ( - - + {props.character} {""} {/* There is a zero-width joiner here in case of non-printing characters. */} - - + + - - + + props.close()} size="large"> {" "} - - + + - - + + - - + + - - + + ); } diff --git a/src/goals/CharacterInventory/CharInv/CharacterEntry.tsx b/src/goals/CharacterInventory/CharInv/CharacterEntry.tsx index d342dd16a2..3e828ff944 100644 --- a/src/goals/CharacterInventory/CharInv/CharacterEntry.tsx +++ b/src/goals/CharacterInventory/CharInv/CharacterEntry.tsx @@ -1,17 +1,39 @@ import { KeyboardArrowDown } from "@mui/icons-material"; -import { Button, Collapse, Grid } from "@mui/material"; +import { + Button, + Collapse, + Dialog, + DialogActions, + DialogContent, + DialogContentText, + DialogTitle, + Grid2, +} from "@mui/material"; import { ReactElement, ReactNode, useState } from "react"; import { useTranslation } from "react-i18next"; +import { LoadingButton } from "components/Buttons"; import { + exit, setRejectedCharacters, setValidCharacters, + uploadInventory, } from "goals/CharacterInventory/Redux/CharacterInventoryActions"; import { useAppDispatch, useAppSelector } from "rootRedux/hooks"; import { type StoreState } from "rootRedux/types"; import theme from "types/theme"; import { TextFieldWithFont } from "utilities/fontComponents"; +export enum CharInvCancelSaveIds { + ButtonCancel = "char-inv-cancel-button", + ButtonSave = "char-inv-save-button", + DialogCancel = "char-inv-cancel-dialog", + DialogCancelButtonNo = "char-inv-cancel-dialog-no-button", + DialogCancelButtonYes = "char-inv-cancel-dialog-yes-button", + DialogCancelText = "char-inv-cancel-dialog-text", + DialogCancelTitle = "char-inv-cancel-dialog-title", +} + /** * Allows for viewing and entering accepted and rejected characters in a * character set @@ -23,56 +45,89 @@ export default function CharacterEntry(): ReactElement { (state: StoreState) => state.characterInventoryState ); - const [checked, setChecked] = useState(false); + const [advancedOpen, setAdvancedOpen] = useState(false); + const [cancelDialogOpen, setCancelDialogOpen] = useState(false); + const [saveInProgress, setSaveInProgress] = useState(false); const { t } = useTranslation(); + const save = async (): Promise => { + setSaveInProgress(true); + await dispatch(uploadInventory()); + }; + return ( - - - + + {/* Cancel yes/no dialog */} + setCancelDialogOpen(false)} + open={cancelDialogOpen} /> - - - {/* Input for accepted characters */} - - dispatch(setValidCharacters(chars))} - /> - - - {/* Input for rejected characters */} - - dispatch(setRejectedCharacters(chars))} + + + {/* Advanced toggle-button */} + + + + + {/* Input for accepted characters */} + dispatch(setValidCharacters(chars))} + /> + + {/* Input for rejected characters */} + dispatch(setRejectedCharacters(chars))} + /> + +
); } @@ -95,10 +150,59 @@ function CharactersInput(props: CharactersInputProps): ReactElement { onChange={(e) => props.setCharacters(e.target.value.replace(/\s/g, "").split("")) } - style={{ maxWidth: 512, marginTop: theme.spacing(1) }} + style={{ marginTop: theme.spacing(2) }} value={props.characters.join("")} variant="outlined" vernacular /> ); } + +interface CancelDialogProps { + open: boolean; + onClose: () => void; +} + +/** "Are you sure?" dialog for the cancel button */ +function CancelDialog(props: CancelDialogProps): ReactElement { + const { t } = useTranslation(); + + return ( + props.onClose()} + open={props.open} + > + + {t("charInventory.dialog.title")} + + + + + {t("charInventory.dialog.content")} + + + + + + + + + + ); +} diff --git a/src/goals/CharacterInventory/CharInv/CharacterList/index.tsx b/src/goals/CharacterInventory/CharInv/CharacterList/index.tsx index 5608e58b5c..921c1989f0 100644 --- a/src/goals/CharacterInventory/CharInv/CharacterList/index.tsx +++ b/src/goals/CharacterInventory/CharInv/CharacterList/index.tsx @@ -1,5 +1,11 @@ import { ArrowDownward, ArrowUpward } from "@mui/icons-material"; -import { FormControl, Grid, InputLabel, MenuItem, Select } from "@mui/material"; +import { + FormControl, + Grid2, + InputLabel, + MenuItem, + Select, +} from "@mui/material"; import { ReactElement, useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; @@ -42,53 +48,54 @@ export default function CharacterList(): ReactElement { return ( <> - - - - {t("charInventory.sortBy")} - - - - - { - /* The grid of character tiles */ - orderedChars.map((character) => ( - selectCharacter(character.character)} - fontHeight={fontHeight} - cardWidth={cardWidth} - /> - )) - } + + + {t("charInventory.sortBy")} + + + + + + { + /* The grid of character tiles */ + orderedChars.map((character) => ( + selectCharacter(character.character)} + fontHeight={fontHeight} + cardWidth={cardWidth} + /> + )) + } + ); } diff --git a/src/goals/CharacterInventory/CharInv/CharacterSetHeader.tsx b/src/goals/CharacterInventory/CharInv/CharacterSetHeader.tsx index 3820c844fe..db9bdbdd35 100644 --- a/src/goals/CharacterInventory/CharInv/CharacterSetHeader.tsx +++ b/src/goals/CharacterInventory/CharInv/CharacterSetHeader.tsx @@ -1,5 +1,5 @@ import { Help } from "@mui/icons-material"; -import { Grid, Tooltip, Typography } from "@mui/material"; +import { Tooltip, Typography } from "@mui/material"; import { ReactElement } from "react"; import { useTranslation } from "react-i18next"; @@ -9,20 +9,18 @@ export default function CharacterSetHeader(): ReactElement { const { t } = useTranslation(); return ( - - + {t("charInventory.characterSet.title")}{" "} + - {t("charInventory.characterSet.title")}{" "} - - - - - + + + ); } diff --git a/src/goals/CharacterInventory/CharInv/index.tsx b/src/goals/CharacterInventory/CharInv/index.tsx index f7d8a06a99..ddb85da517 100644 --- a/src/goals/CharacterInventory/CharInv/index.tsx +++ b/src/goals/CharacterInventory/CharInv/index.tsx @@ -1,39 +1,17 @@ -import { - Button, - Dialog, - DialogActions, - DialogContent, - DialogContentText, - DialogTitle, - Grid, -} from "@mui/material"; -import { ReactElement, useEffect, useState } from "react"; -import { useTranslation } from "react-i18next"; +import { Grid2, Stack } from "@mui/material"; +import { ReactElement, useEffect } from "react"; -import { LoadingButton } from "components/Buttons"; import CharacterDetail from "goals/CharacterInventory/CharInv/CharacterDetail"; import CharacterEntry from "goals/CharacterInventory/CharInv/CharacterEntry"; import CharacterList from "goals/CharacterInventory/CharInv/CharacterList"; import CharacterSetHeader from "goals/CharacterInventory/CharInv/CharacterSetHeader"; import { - exit, loadCharInvData, resetCharInv, setSelectedCharacter, - uploadInventory, } from "goals/CharacterInventory/Redux/CharacterInventoryActions"; import { useAppDispatch, useAppSelector } from "rootRedux/hooks"; import { type StoreState } from "rootRedux/types"; -import theme from "types/theme"; - -const idPrefix = "character-inventory"; -export const buttonIdCancel = `${idPrefix}-cancel-button`; -export const buttonIdSave = `${idPrefix}-save-button`; -export const dialogButtonIdNo = `${idPrefix}-cancel-dialog-no-button`; -export const dialogButtonIdYes = `${idPrefix}-cancel-dialog-yes-button`; -export const dialogIdCancel = `${idPrefix}-cancel-dialog`; -const dialogTextIdCancel = `${idPrefix}-cancel-dialog-text`; -const dialogTitleIdCancel = `${idPrefix}-cancel-dialog-title`; /** * Allows users to define a character inventory for a project @@ -45,11 +23,6 @@ export default function CharacterInventory(): ReactElement { (state: StoreState) => state.characterInventoryState.selectedCharacter ); - const [cancelDialogOpen, setCancelDialogOpen] = useState(false); - const [saveInProgress, setSaveInProgress] = useState(false); - - const { t } = useTranslation(); - useEffect(() => { dispatch(loadCharInvData()); @@ -57,99 +30,24 @@ export default function CharacterInventory(): ReactElement { () => dispatch(resetCharInv()); }, [dispatch]); - const save = async (): Promise => { - setSaveInProgress(true); - await dispatch(uploadInventory()); - }; - return ( - <> - - - - - - - - - - - {!!selectedCharacter && ( - dispatch(setSelectedCharacter(""))} - /> - )} - - - - {/* Submission buttons */} - - save(), - style: { margin: theme.spacing(1) }, - }} - > - {t("buttons.save")} - - - - - - - {/* "Are you sure?" dialog for the cancel button */} - setCancelDialogOpen(false)} - aria-labelledby="alert-dialog-title" - aria-describedby="alert-dialog-description" - > - - {t("charInventory.dialog.title")} - - - - {t("charInventory.dialog.content")} - - - - - - - - + + + + + + + + + + {!!selectedCharacter && ( + + dispatch(setSelectedCharacter(""))} + /> + + )} + ); } diff --git a/src/goals/CharacterInventory/CharInv/tests/index.test.tsx b/src/goals/CharacterInventory/CharInv/tests/index.test.tsx index e59662d4b2..620726a307 100644 --- a/src/goals/CharacterInventory/CharInv/tests/index.test.tsx +++ b/src/goals/CharacterInventory/CharInv/tests/index.test.tsx @@ -2,13 +2,8 @@ import { Provider } from "react-redux"; import { ReactTestRenderer, act, create } from "react-test-renderer"; import configureMockStore from "redux-mock-store"; -import CharInv, { - buttonIdCancel, - buttonIdSave, - dialogButtonIdNo, - dialogButtonIdYes, - dialogIdCancel, -} from "goals/CharacterInventory/CharInv"; +import CharInv from "goals/CharacterInventory/CharInv"; +import { CharInvCancelSaveIds } from "goals/CharacterInventory/CharInv/CharacterEntry"; import { defaultState as characterInventoryState } from "goals/CharacterInventory/Redux/CharacterInventoryReduxTypes"; // Replace Dialog with something that doesn't create portals, @@ -63,30 +58,42 @@ describe("CharInv", () => { it("saves inventory on save", async () => { expect(mockUploadInventory).toHaveBeenCalledTimes(0); - const saveButton = charMaster.root.findByProps({ id: buttonIdSave }); + const saveButton = charMaster.root.findByProps({ + id: CharInvCancelSaveIds.ButtonSave, + }); await act(async () => saveButton.props.onClick()); expect(mockUploadInventory).toHaveBeenCalledTimes(1); }); it("opens a dialogue on cancel, closes on no", async () => { - const cancelDialog = charMaster.root.findByProps({ id: dialogIdCancel }); + const cancelDialog = charMaster.root.findByProps({ + id: CharInvCancelSaveIds.DialogCancel, + }); expect(cancelDialog.props.open).toBeFalsy(); - const cancelButton = charMaster.root.findByProps({ id: buttonIdCancel }); + const cancelButton = charMaster.root.findByProps({ + id: CharInvCancelSaveIds.ButtonCancel, + }); await act(async () => cancelButton.props.onClick()); expect(cancelDialog.props.open).toBeTruthy(); - const noButton = charMaster.root.findByProps({ id: dialogButtonIdNo }); + const noButton = charMaster.root.findByProps({ + id: CharInvCancelSaveIds.DialogCancelButtonNo, + }); await act(async () => noButton.props.onClick()); expect(cancelDialog.props.open).toBeFalsy(); }); it("exits on cancel-yes", async () => { - const cancelButton = charMaster.root.findByProps({ id: buttonIdCancel }); + const cancelButton = charMaster.root.findByProps({ + id: CharInvCancelSaveIds.ButtonCancel, + }); await act(async () => cancelButton.props.onClick()); expect(mockExit).toHaveBeenCalledTimes(0); - const yesButton = charMaster.root.findByProps({ id: dialogButtonIdYes }); + const yesButton = charMaster.root.findByProps({ + id: CharInvCancelSaveIds.DialogCancelButtonYes, + }); await act(async () => yesButton.props.onClick()); expect(mockExit).toHaveBeenCalledTimes(1); }); diff --git a/src/goals/DefaultGoal/DisplayProgress.tsx b/src/goals/DefaultGoal/DisplayProgress.tsx index ea04b97a14..584a88b3ad 100644 --- a/src/goals/DefaultGoal/DisplayProgress.tsx +++ b/src/goals/DefaultGoal/DisplayProgress.tsx @@ -1,4 +1,4 @@ -import { Grid, LinearProgress, Paper, Typography } from "@mui/material"; +import { LinearProgress, Paper, Stack, Typography } from "@mui/material"; import { ReactElement } from "react"; import { useTranslation } from "react-i18next"; import { useSelector } from "react-redux"; @@ -31,16 +31,12 @@ export default function DisplayProgress(): ReactElement | null { return numSteps > 1 ? ( - - - - {t(stepTranslateId, { val1: currentStep + 1, val2: numSteps })} - - - - - - + + + {t(stepTranslateId, { val1: currentStep + 1, val2: numSteps })} + + + ) : null; } diff --git a/src/goals/MergeDuplicates/MergeDupsCompleted.tsx b/src/goals/MergeDuplicates/MergeDupsCompleted.tsx index 8f05fcfaa9..b286f4a837 100644 --- a/src/goals/MergeDuplicates/MergeDupsCompleted.tsx +++ b/src/goals/MergeDuplicates/MergeDupsCompleted.tsx @@ -1,5 +1,5 @@ import { ArrowRightAlt } from "@mui/icons-material"; -import { Card, Grid, Paper, Typography } from "@mui/material"; +import { Box, Card, Grid2, Paper, Stack, Typography } from "@mui/material"; import { ReactElement, useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { useSelector } from "react-redux"; @@ -53,48 +53,42 @@ export function MergeChange(props: { change: MergeUndoIds }): ReactElement { const isDeletion = !change.parentIds.length; return ( - + {isDeletion && {t("mergeDups.undo.deleted")}} {change.childIds.map((id) => ( - + ))} {!isDeletion && ( <> - - - + + + {change.parentIds.map((id) => ( - + ))} )} - { - await undoMerge(change); - }} - /> - + + { + await undoMerge(change); + }} + /> + + ); } @@ -110,11 +104,7 @@ export function doWordsIncludeMerges( ); } -interface WordPaperProps { - wordId: string; -} - -function WordPaper(props: WordPaperProps): ReactElement { +function WordBox(props: { wordId: string }): ReactElement { const [word, setWord] = useState(); const [flag, setFlag] = useState(newFlag()); @@ -126,7 +116,7 @@ function WordPaper(props: WordPaperProps): ReactElement { }, [word]); return ( - + - - - - {word?.vernacular} - - - - - - + + + {word?.vernacular} + + +
{word?.senses?.map(SenseCard)}
- + ); } diff --git a/src/goals/MergeDuplicates/MergeDupsStep/MergeDragDrop/SidebarDrop.tsx b/src/goals/MergeDuplicates/MergeDupsStep/MergeDragDrop/SidebarDrop.tsx index dfbcb6b8c2..5d890cbddf 100644 --- a/src/goals/MergeDuplicates/MergeDupsStep/MergeDragDrop/SidebarDrop.tsx +++ b/src/goals/MergeDuplicates/MergeDupsStep/MergeDragDrop/SidebarDrop.tsx @@ -1,5 +1,5 @@ import { ArrowForwardIos, HelpOutline } from "@mui/icons-material"; -import { Grid, IconButton, Typography } from "@mui/material"; +import { Grid2, IconButton, Typography } from "@mui/material"; import { type ReactElement } from "react"; import { Droppable } from "react-beautiful-dnd"; @@ -29,21 +29,24 @@ export default function SidebarDrop(): ReactElement { {...providedDroppable.droppableProps} style={{ backgroundColor: "lightblue", height: "100%", padding: 20 }} > - + dispatch(setSidebar())} > + openUserGuide("goals.html#merge-a-sense")} > - + + {vernacular} + {sidebar.mergeSenses.map((mergeSense, index) => ( - - + + {(provided): ReactElement => (
@@ -252,36 +258,35 @@ export default function MergeDragDrop(): ReactElement {
)}
-
- - - {Object.keys(words).map((key) => ( - - - - ))} - - + + + + {Object.keys(words).map((key) => ( + + - {renderSidebar()} - setOverride(undefined)} - handleConfirm={onConfirmOverride} - /> - setSrcToDelete(undefined)} - handleConfirm={onConfirmDelete} - /> - - -
+ ))} + + + + {renderSidebar()} + setOverride(undefined)} + handleConfirm={onConfirmOverride} + /> + setSrcToDelete(undefined)} + handleConfirm={onConfirmDelete} + /> + + ); } diff --git a/src/goals/MergeDuplicates/MergeDupsStep/SaveDeferButtons.tsx b/src/goals/MergeDuplicates/MergeDupsStep/SaveDeferButtons.tsx index 50351851bb..5d912e6f3e 100644 --- a/src/goals/MergeDuplicates/MergeDupsStep/SaveDeferButtons.tsx +++ b/src/goals/MergeDuplicates/MergeDupsStep/SaveDeferButtons.tsx @@ -1,4 +1,4 @@ -import { Checkbox, FormControlLabel, Grid } from "@mui/material"; +import { Checkbox, FormControlLabel, Grid2 } from "@mui/material"; import { ReactElement, useState } from "react"; import { useTranslation } from "react-i18next"; @@ -13,7 +13,6 @@ import { import { asyncAdvanceStep } from "goals/Redux/GoalActions"; import { useAppDispatch, useAppSelector } from "rootRedux/hooks"; import { StoreState } from "rootRedux/types"; -import theme from "types/theme"; export default function SaveDeferButtons(): ReactElement { const dispatch = useAppDispatch(); @@ -52,43 +51,41 @@ export default function SaveDeferButtons(): ReactElement { }; return ( - - - - {t("buttons.saveAndContinue")} - - - {t("buttons.defer")} - - {hasProtected && ( - dispatch(toggleOverrideProtection())} - /> - } - label={t("mergeDups.helpText.protectedOverride")} - /> - )} - - + + + {t("buttons.saveAndContinue")} + + + + {t("buttons.defer")} + + + {hasProtected && ( + dispatch(toggleOverrideProtection())} + /> + } + label={t("mergeDups.helpText.protectedOverride")} + /> + )} + ); } diff --git a/src/goals/MergeDuplicates/MergeDupsStep/index.tsx b/src/goals/MergeDuplicates/MergeDupsStep/index.tsx index 6b1c2b01ac..d283f1c0b7 100644 --- a/src/goals/MergeDuplicates/MergeDupsStep/index.tsx +++ b/src/goals/MergeDuplicates/MergeDupsStep/index.tsx @@ -18,7 +18,13 @@ export default function MergeDupsStep(): ReactElement { return wordCount ? ( <> -
+
diff --git a/src/goals/ReviewEntries/ReviewEntriesCompleted.tsx b/src/goals/ReviewEntries/ReviewEntriesCompleted.tsx index 1fb4a23666..b4a2049f1c 100644 --- a/src/goals/ReviewEntries/ReviewEntriesCompleted.tsx +++ b/src/goals/ReviewEntries/ReviewEntriesCompleted.tsx @@ -1,5 +1,5 @@ import { ArrowRightAlt } from "@mui/icons-material"; -import { Grid, List, ListItem, Typography } from "@mui/material"; +import { Box, List, ListItem, Stack, Typography } from "@mui/material"; import { ReactElement, useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { useSelector } from "react-redux"; @@ -65,31 +65,36 @@ function EditedEntry(props: { edit: EntryEdit }): ReactElement { return ( - - {!!oldWord && } - - + {!!oldWord && ( + + + + )} + + + + + + {!!newWord && ( + + + + )} + + + isInFrontier(newId)} + undo={() => undoEdit(props.edit)} /> - - {!!newWord && } - isInFrontier(newId)} - undo={() => undoEdit(props.edit)} - /> - + + ); } diff --git a/src/goals/ReviewEntries/ReviewEntriesTable/Cells/DomainsCell.tsx b/src/goals/ReviewEntries/ReviewEntriesTable/Cells/DomainsCell.tsx index 839c917f14..632cfd84b6 100644 --- a/src/goals/ReviewEntries/ReviewEntriesTable/Cells/DomainsCell.tsx +++ b/src/goals/ReviewEntries/ReviewEntriesTable/Cells/DomainsCell.tsx @@ -1,4 +1,4 @@ -import { Chip, Grid } from "@mui/material"; +import { Chip, Grid2 } from "@mui/material"; import { type ReactElement } from "react"; import { type SemanticDomain, type Sense } from "api/models"; @@ -19,12 +19,14 @@ export function gatherDomains(senses: Sense[]): SemanticDomain[] { export default function DomainsCell(props: CellProps): ReactElement { return ( - + {gatherDomains(props.word.senses).map((dom) => ( - - - + ))} - + ); } diff --git a/src/goals/ReviewEntries/ReviewEntriesTable/Cells/EditCell/EditDialog.tsx b/src/goals/ReviewEntries/ReviewEntriesTable/Cells/EditCell/EditDialog.tsx index 5f3428ccd1..b62dbbad16 100644 --- a/src/goals/ReviewEntries/ReviewEntriesTable/Cells/EditCell/EditDialog.tsx +++ b/src/goals/ReviewEntries/ReviewEntriesTable/Cells/EditCell/EditDialog.tsx @@ -13,10 +13,11 @@ import { Dialog, DialogContent, DialogTitle, - Grid, + Grid2, IconButton, MenuItem, Select, + Stack, type SelectChangeEvent, } from "@mui/material"; import { grey, yellow } from "@mui/material/colors"; @@ -351,151 +352,135 @@ export default function EditDialog(props: EditDialogProps): ReactElement { /> - - - {t("reviewEntries.columns.edit")} - {" : "} - {props.word.vernacular} - - + + {`${t("reviewEntries.columns.edit")} : ${props.word.vernacular}`} + +
+ - - +
+
+ - + {/* Vernacular */} - - - - - - setNewWord((prev) => ({ - ...prev, - vernacular: e.target.value, - })) - } - value={newWord.vernacular} - vernacular - /> - - - - - {/* Senses */} - - - 1 && ( - setShowSenses((prev) => !prev)} - > - {showSenses ? ( - - ) : ( - - )} - - ) + + + + + setNewWord((prev) => ({ + ...prev, + vernacular: e.target.value, + })) } - title={t("reviewEntries.columns.senses")} - /> - - - + + + + {/* Senses */} + + 1 && ( + setShowSenses((prev) => !prev)} + > + {showSenses ? ( + + ) : ( + + )} + + ) + } + title={t("reviewEntries.columns.senses")} + /> + + {/* Pronunciations */} - - - - - - } - audio={newAudio} - deleteAudio={delNewAudio} - replaceAudio={repNewAudio} - uploadAudio={uplNewAudio} - /> - - - + + + + + } + audio={newAudio} + deleteAudio={delNewAudio} + replaceAudio={repNewAudio} + uploadAudio={uplNewAudio} + /> + + {/* Note */} - - - + + + updateNoteText(e.target.value)} + value={newWord.note.text} /> - - updateNoteText(e.target.value)} - value={newWord.note.text} - /> - - - + + {/* Flag */} - - - - - - {newWord.flag.active ? ( - - ) : ( - - )} - - updateFlag(e.target.value)} - value={newWord.flag.active ? newWord.flag.text : ""} - /> - - - - + + + + + {newWord.flag.active ? ( + + ) : ( + + )} + + updateFlag(e.target.value)} + value={newWord.flag.active ? newWord.flag.text : ""} + /> + + +
diff --git a/src/goals/ReviewEntries/ReviewEntriesTable/Cells/EditCell/EditSenseDialog.tsx b/src/goals/ReviewEntries/ReviewEntriesTable/Cells/EditCell/EditSenseDialog.tsx index d00ee57c3b..71174cb60e 100644 --- a/src/goals/ReviewEntries/ReviewEntriesTable/Cells/EditCell/EditSenseDialog.tsx +++ b/src/goals/ReviewEntries/ReviewEntriesTable/Cells/EditCell/EditSenseDialog.tsx @@ -7,8 +7,9 @@ import { Dialog, DialogContent, DialogTitle, - Grid, + Grid2, IconButton, + Stack, Typography, } from "@mui/material"; import { grey, yellow } from "@mui/material/colors"; @@ -184,106 +185,96 @@ export default function EditSenseDialog( /> - - {t("reviewEntries.editSense")} - + + {t("reviewEntries.editSense")} + +
t.palette.success.main }} /> + t.palette.error.main }} /> - - +
+
+ - + {/* Definitions */} {definitionsEnabled && ( - - - - - - - - - )} - - {/* Glosses */} - - - + + - - + )} + + {/* Glosses */} + + + + + + {/* Part of Speech */} {grammaticalInfoEnabled && ( - - - - - {newSense.grammaticalInfo.catGroup === - GramCatGroup.Unspecified ? ( - - {t("grammaticalCategory.group.Unspecified")} - - ) : ( - - )} - - - - )} - - {/* Semantic Domains */} - - - + + - + {newSense.grammaticalInfo.catGroup === + GramCatGroup.Unspecified ? ( + + {t("grammaticalCategory.group.Unspecified")} + + ) : ( + + )} - - + )} + + {/* Semantic Domains */} + + + + + + +
@@ -450,21 +441,20 @@ function DomainList(props: DomainListProps): ReactElement { return ( <> - + {props.domains.length > 0 ? ( props.domains.map((domain, index) => ( - - deleteDomain(domain.id)} - /> - + deleteDomain(domain.id)} + /> )) ) : ( - + - + )} - + + setAddingDom(false)} diff --git a/src/goals/ReviewEntries/ReviewEntriesTable/Cells/EditCell/EditSensesCardContent.tsx b/src/goals/ReviewEntries/ReviewEntriesTable/Cells/EditCell/EditSensesCardContent.tsx index 18beddbe43..c71552aab9 100644 --- a/src/goals/ReviewEntries/ReviewEntriesTable/Cells/EditCell/EditSensesCardContent.tsx +++ b/src/goals/ReviewEntries/ReviewEntriesTable/Cells/EditCell/EditSensesCardContent.tsx @@ -6,7 +6,7 @@ import { Edit, RestoreFromTrash, } from "@mui/icons-material"; -import { CardContent, Divider, Grid, Icon } from "@mui/material"; +import { CardContent, Divider, Grid2, Icon, Stack } from "@mui/material"; import { grey, yellow } from "@mui/material/colors"; import { type ReactElement, useEffect, useState } from "react"; @@ -52,38 +52,46 @@ export default function EditSensesCardContent( ); }, [props.newSenses, props.oldSenses]); + const sensesAndDividers: ReactElement[] = []; + props.newSenses.forEach((s, i) => { + sensesAndDividers.push( + props.moveSense(i, i + 1) + : undefined + } + bumpSenseUp={i ? () => props.moveSense(i, i - 1) : undefined} + edited={changes[i]} + key={s.guid} + sense={s} + toggleSenseDeleted={() => props.toggleSenseDeleted(i)} + updateSense={props.updateOrAddSense} + /> + ); + sensesAndDividers.push(); + }); + return ( {props.showSenses ? ( - <> - {props.newSenses.map((s, i) => ( - props.moveSense(i, i + 1) - : undefined - } - bumpSenseUp={i ? () => props.moveSense(i, i - 1) : undefined} - edited={changes[i]} - key={s.guid} - sense={s} - toggleSenseDeleted={() => props.toggleSenseDeleted(i)} - updateSense={props.updateOrAddSense} - /> - ))} + + {sensesAndDividers} + } onClick={() => setAddSense(true)} size="small" /> + setAddSense(false)} isOpen={addSense} save={props.updateOrAddSense} sense={newSense()} /> - + ) : ( - - {props.bumpSenseDown || props.bumpSenseUp ? ( - - - - : } - onClick={props.bumpSenseUp} - size="small" - /> - - - : } - onClick={props.bumpSenseDown} - size="small" - /> - - - - ) : null} - - - {deleted ? ( - - } - onClick={props.toggleSenseDeleted} - size="small" - /> - - ) : ( - <> - - } - onClick={ - sense.accessibility === Status.Protected - ? undefined - : props.toggleSenseDeleted - } - size="small" - textId={ - sense.accessibility === Status.Protected - ? "reviewEntries.deleteDisabled" - : undefined - } - /> - - - } - onClick={() => setEditing(true)} - size="small" - /> - - - )} - - - - + {props.bumpSenseDown || props.bumpSenseUp ? ( + + : } + onClick={props.bumpSenseUp} + size="small" + /> + + : } + onClick={props.bumpSenseDown} + size="small" /> - - setEditing(false)} - isOpen={editing} - save={props.updateSense} + + ) : null} + + + {deleted ? ( + } + onClick={props.toggleSenseDeleted} + size="small" + /> + ) : ( + <> + } + onClick={ + sense.accessibility === Status.Protected + ? undefined + : props.toggleSenseDeleted + } + size="small" + textId={ + sense.accessibility === Status.Protected + ? "reviewEntries.deleteDisabled" + : undefined + } + /> + + } + onClick={() => setEditing(true)} + size="small" + /> + + )} + + +
+ - - - +
+ + setEditing(false)} + isOpen={editing} + save={props.updateSense} + sense={sense} + /> + ); } diff --git a/src/goals/ReviewEntries/ReviewEntriesTable/Cells/PartOfSpeechCell.tsx b/src/goals/ReviewEntries/ReviewEntriesTable/Cells/PartOfSpeechCell.tsx index df0840d02a..604e1720a9 100644 --- a/src/goals/ReviewEntries/ReviewEntriesTable/Cells/PartOfSpeechCell.tsx +++ b/src/goals/ReviewEntries/ReviewEntriesTable/Cells/PartOfSpeechCell.tsx @@ -1,4 +1,4 @@ -import { Grid } from "@mui/material"; +import { Grid2 } from "@mui/material"; import { type ReactElement } from "react"; import { type GrammaticalInfo, type Sense } from "api/models"; @@ -18,12 +18,13 @@ function gatherGramInfo(senses: Sense[]): GrammaticalInfo[] { export default function PartOfSpeechCell(props: CellProps): ReactElement { return ( - + {gatherGramInfo(props.word.senses).map((gi) => ( - - - + ))} - + ); }