ente/web/packages/base/components/SingleInputForm.tsx
Manav Rathi 16e1ab2049
fw
2025-01-07 20:06:08 +05:30

120 lines
3.9 KiB
TypeScript

import { FocusVisibleButton } from "@/base/components/mui/FocusVisibleButton";
import { LoadingButton } from "@/base/components/mui/LoadingButton";
import log from "@/base/log";
import { Stack, TextField, type TextFieldProps } from "@mui/material";
import { useFormik } from "formik";
import { t } from "i18next";
import React from "react";
export type SingleInputFormProps = Pick<
TextFieldProps,
"label" | "placeholder" | "autoComplete" | "autoFocus"
> & {
/**
* The initial value, if any, to prefill in the input.
*/
initialValue?: string;
/**
* Title for the submit button.
*/
submitButtonTitle: string;
/**
* Cancellation handler.
*
* This function is called when the user activates the cancel button in the
* form.
*/
onCancel: () => void;
/**
* Submission handler. A callback invoked when the submit button is pressed.
*
* During submission, the text input and the submit button are disabled, and
* an indeterminate progress indicator is shown.
*
* If this function rejects then a generic error helper text is shown below
* the text input, and the input (/ buttons) reenabled.
*
* @param name The current value of the text input.
*/
onSubmit: ((name: string) => void) | ((name: string) => Promise<void>);
};
/**
* A TextField and two buttons.
*
* A common requirement is taking a single textual input from the user. This is
* a form suitable for that purpose - it is form containing a single MUI
* {@link TextField}, with two accompanying buttons; one to submit, and one to
* cancel.
*
* Submission is handled as an async function, during which the input is
* disabled and a loading indicator is shown. Errors during submission are shown
* as the helper text associated with the text field.
*/
export const SingleInputForm: React.FC<SingleInputFormProps> = ({
initialValue,
submitButtonTitle,
onCancel,
onSubmit,
...rest
}) => {
const formik = useFormik({
initialValues: {
value: initialValue ?? "",
},
onSubmit: async (values, { setFieldError }) => {
const value = values.value;
if (!value) {
setFieldError("value", t("required"));
return;
}
try {
await onSubmit(value);
} catch (e) {
log.error(`Failed to submit input ${value}`, e);
setFieldError("value", t("generic_error"));
}
},
});
// Note: [Use space as default TextField helperText]
//
// For MUI text fields that use a conditional helperText, e.g. in case of
// errors, use an space as the default helperText in the other cases to
// avoid a layout shift when the helperText is conditionally shown.
return (
<form onSubmit={formik.handleSubmit}>
<TextField
name="value"
value={formik.values.value}
onChange={formik.handleChange}
type="text"
fullWidth
margin="normal"
disabled={formik.isSubmitting}
error={!!formik.errors.value}
helperText={formik.errors.value ?? " "}
{...rest}
/>
<Stack direction="row" sx={{ gap: "12px" }}>
<FocusVisibleButton
fullWidth
color="secondary"
onClick={onCancel}
>
{t("cancel")}
</FocusVisibleButton>
<LoadingButton
fullWidth
color="accent"
type="submit"
loading={formik.isSubmitting}
>
{submitButtonTitle}
</LoadingButton>
</Stack>
</form>
);
};