Skip to content

Commit

Permalink
Merge pull request #1660 from AntaresSimulatorTeam/bugfix/login_submit
Browse files Browse the repository at this point in the history
  • Loading branch information
skamril committed Jul 24, 2023
2 parents b1786a8 + 9631045 commit 787fdc3
Show file tree
Hide file tree
Showing 4 changed files with 67 additions and 51 deletions.
47 changes: 39 additions & 8 deletions webapp/src/components/common/Form/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,18 @@ import SaveIcon from "@mui/icons-material/Save";
import { useUpdateEffect } from "react-use";
import * as R from "ramda";
import clsx from "clsx";
import { LoadingButton } from "@mui/lab";
import { LoadingButton, LoadingButtonProps } from "@mui/lab";
import UndoIcon from "@mui/icons-material/Undo";
import RedoIcon from "@mui/icons-material/Redo";
import axios from "axios";
import useEnqueueErrorSnackbar from "../../../hooks/useEnqueueErrorSnackbar";
import useDebounce from "../../../hooks/useDebounce";
import { getDirtyValues, stringToPath, toAutoSubmitConfig } from "./utils";
import {
ROOT_ERROR_KEY,
getDirtyValues,
stringToPath,
toAutoSubmitConfig,
} from "./utils";
import useDebouncedState from "../../../hooks/useDebouncedState";
import usePrompt from "../../../hooks/usePrompt";
import { mergeSxProp } from "../../../utils/muiUtils";
Expand All @@ -57,6 +63,7 @@ export interface FormProps<
| ((formApi: UseFormReturnPlus<TFieldValues, TContext>) => React.ReactNode)
| React.ReactNode;
submitButtonText?: string;
submitButtonIcon?: LoadingButtonProps["startIcon"];
hideSubmitButton?: boolean;
onStateChange?: (state: FormState<TFieldValues>) => void;
autoSubmit?: boolean | AutoSubmitConfig;
Expand All @@ -78,6 +85,7 @@ function Form<TFieldValues extends FieldValues, TContext>(
onSubmitError,
children,
submitButtonText,
submitButtonIcon,
hideSubmitButton,
onStateChange,
autoSubmit,
Expand Down Expand Up @@ -123,14 +131,17 @@ function Form<TFieldValues extends FieldValues, TContext>(
: config?.defaultValues,
});

const { getValues, setValue, handleSubmit, formState, reset } = formApi;
const { getValues, setValue, setError, handleSubmit, formState, reset } =
formApi;
// * /!\ `formState` is a proxy
const { isSubmitting, isSubmitSuccessful, isDirty, dirtyFields } = formState;
const { isSubmitting, isSubmitSuccessful, isDirty, dirtyFields, errors } =
formState;
// Don't add `isValid` because we need to trigger fields validation.
// In case we have invalid default value for example.
const isSubmitAllowed = isDirty && !isSubmitting;
const showSubmitButton = !hideSubmitButton && !autoSubmitConfig.enable;
const showFooter = showSubmitButton || enableUndoRedo;
const rootError = errors.root?.[ROOT_ERROR_KEY];

const formApiPlus = useFormApiPlus({
formApi,
Expand Down Expand Up @@ -240,8 +251,17 @@ function Form<TFieldValues extends FieldValues, TContext>(
}

return Promise.all(res)
.catch((error) => {
enqueueErrorSnackbar(t("form.submit.error"), error);
.catch((err) => {
enqueueErrorSnackbar(t("form.submit.error"), err);

// Any error under the `root` key are not persisted with each submission.
// They will be deleted automatically.
// cf. https://www.react-hook-form.com/api/useform/seterror/
setError(`root.${ROOT_ERROR_KEY}`, {
message: axios.isAxiosError(err)
? err.response?.data.description
: err?.toString(),
});
})
.finally(() => {
preventClose.current = false;
Expand Down Expand Up @@ -304,8 +324,13 @@ function Form<TFieldValues extends FieldValues, TContext>(
<FormProvider {...formApiPlus}>{children}</FormProvider>
)}
</FormContext.Provider>
{rootError && (
<Box color="error.main" sx={{ fontSize: "0.9rem", mb: 2 }}>
{rootError.message || t("form.submit.error")}
</Box>
)}
{showFooter && (
<Box sx={{ display: "flex" }}>
<Box sx={{ display: "flex" }} className="Form__Footer">
{showSubmitButton && (
<>
<LoadingButton
Expand All @@ -314,7 +339,13 @@ function Form<TFieldValues extends FieldValues, TContext>(
disabled={!isSubmitAllowed}
loading={isSubmitting}
loadingPosition="start"
startIcon={<SaveIcon />}
startIcon={
RA.isNotUndefined(submitButtonIcon) ? (
submitButtonIcon
) : (
<SaveIcon />
)
}
>
{submitButtonText || t("global.save")}
</LoadingButton>
Expand Down
5 changes: 2 additions & 3 deletions webapp/src/components/common/Form/useFormApiPlus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -184,9 +184,8 @@ function useFormApiPlus<TFieldValues extends FieldValues, TContext>(

// `formState` is wrapped with a Proxy and updated in batch.
// The API is updated here to keep reference, like `useForm` return.
useEffect(() => {
formApiPlus.formState = formState;
}, [formApiPlus, formState]);
// ! Don't used `useEffect`, because it's read before render.
formApiPlus.formState = formState;

return formApiPlus;
}
Expand Down
2 changes: 2 additions & 0 deletions webapp/src/components/common/Form/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,5 @@ export function stringToPath(input: string): string[] {
.split(/\.|\[/)
.filter(Boolean);
}

export const ROOT_ERROR_KEY = "default";
64 changes: 24 additions & 40 deletions webapp/src/components/wrappers/LoginWrapper.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { useState } from "react";
import { Box, Typography } from "@mui/material";
import { useTranslation } from "react-i18next";
import { LoadingButton } from "@mui/lab";
import LoginIcon from "@mui/icons-material/Login";
import { login } from "../../redux/ducks/auth";
import logo from "../../assets/logo.png";
import topRightBackground from "../../assets/top-right-background.png";
Expand Down Expand Up @@ -30,7 +29,6 @@ interface Props {

function LoginWrapper(props: Props) {
const { children } = props;
const [loginError, setLoginError] = useState("");
const { t } = useTranslation();
const user = useAppSelector(getAuthUser);
const dispatch = useAppDispatch();
Expand Down Expand Up @@ -62,18 +60,8 @@ function LoginWrapper(props: Props) {
// Event Handlers
////////////////////////////////////////////////////////////////

const handleSubmit = async (data: SubmitHandlerPlus<FormValues>) => {
const { values } = data;

setLoginError("");

try {
await dispatch(login(values)).unwrap();
} catch (err) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
setLoginError((err as any).data?.message || t("login.error"));
throw err;
}
const handleSubmit = ({ values }: SubmitHandlerPlus<FormValues>) => {
return dispatch(login(values)).unwrap();
};

////////////////////////////////////////////////////////////////
Expand Down Expand Up @@ -150,47 +138,43 @@ function LoginWrapper(props: Props) {
</Typography>
</Box>
<Box width="70%" my={2}>
<Form onSubmit={handleSubmit} hideSubmitButton>
{({ control, formState: { isDirty, isSubmitting } }) => (
<Form
onSubmit={handleSubmit}
submitButtonText={t("global.connexion")}
submitButtonIcon={<LoginIcon />}
sx={{
".Form__Footer": {
justifyContent: "center",
},
}}
>
{({ control }) => (
<>
<StringFE
name="username"
sx={{ my: 3 }}
label="NNI"
variant="filled"
size="small"
sx={{ mb: 2 }}
fullWidth
control={control}
rules={{ required: t("form.field.required") }}
/>
<PasswordFE
name="password"
variant="filled"
label={t("global.password")}
inputProps={{ autoComplete: "current-password" }}
variant="filled"
size="small"
inputProps={{
// https://web.dev/sign-in-form-best-practices/#current-password
autoComplete: "current-password",
id: "current-password",
}}
sx={{ mb: 3 }}
fullWidth
control={control}
rules={{ required: t("form.field.required") }}
/>
{loginError && (
<Box
mt={2}
color="error.main"
mb={4}
sx={{ fontSize: "0.9rem" }}
>
{loginError}
</Box>
)}
<Box display="flex" justifyContent="center" mt={6}>
<LoadingButton
type="submit"
variant="contained"
loading={isSubmitting}
disabled={!isDirty || isSubmitting}
>
{t("global.connexion")}
</LoadingButton>
</Box>
</>
)}
</Form>
Expand Down

0 comments on commit 787fdc3

Please sign in to comment.