diff --git a/frontend/src/metabase/account/profile/components/UserProfileForm/UserProfileForm.unit.spec.tsx b/frontend/src/metabase/account/profile/components/UserProfileForm/UserProfileForm.unit.spec.tsx new file mode 100644 index 0000000000000000000000000000000000000000..93062cec8fbd02563a5be611aecdac51609b8f19 --- /dev/null +++ b/frontend/src/metabase/account/profile/components/UserProfileForm/UserProfileForm.unit.spec.tsx @@ -0,0 +1,30 @@ +import React from "react"; +import { render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { createMockUser } from "metabase-types/api/mocks"; +import UserProfileForm, { UserProfileFormProps } from "./UserProfileForm"; + +describe("UserProfileForm", () => { + it("should show a success message after form submit", async () => { + const props = getProps({ + onSubmit: jest.fn().mockResolvedValue({}), + }); + + render(<UserProfileForm {...props} />); + userEvent.clear(screen.getByLabelText("First name")); + userEvent.type(screen.getByLabelText("First name"), "New name"); + userEvent.click(screen.getByText("Update")); + + expect(await screen.findByText("Success")).toBeInTheDocument(); + }); +}); + +const getProps = ( + opts?: Partial<UserProfileFormProps>, +): UserProfileFormProps => ({ + user: createMockUser(), + locales: null, + isSsoUser: false, + onSubmit: jest.fn(), + ...opts, +}); diff --git a/frontend/src/metabase/core/components/FormProvider/FormProvider.tsx b/frontend/src/metabase/core/components/FormProvider/FormProvider.tsx index 8da14f4c29bcdfe1ea1de20a7446fc3476ea6f07..f31fdf266f02b60af19b1a882ab9557cbfbfcf39 100644 --- a/frontend/src/metabase/core/components/FormProvider/FormProvider.tsx +++ b/frontend/src/metabase/core/components/FormProvider/FormProvider.tsx @@ -4,6 +4,7 @@ import type { FormikConfig } from "formik"; import type { AnySchema } from "yup"; import useFormSubmit from "metabase/core/hooks/use-form-submit"; import useFormValidation from "metabase/core/hooks/use-form-validation"; +import FormContext from "metabase/core/context/FormContext"; export interface FormProviderProps<T, C> extends FormikConfig<T> { validationSchema?: AnySchema; @@ -17,7 +18,7 @@ function FormProvider<T, C>({ onSubmit, ...props }: FormProviderProps<T, C>): JSX.Element { - const { handleSubmit } = useFormSubmit({ onSubmit }); + const { state, handleSubmit } = useFormSubmit({ onSubmit }); const { initialErrors, handleValidate } = useFormValidation({ initialValues, validationSchema, @@ -25,13 +26,15 @@ function FormProvider<T, C>({ }); return ( - <Formik - initialValues={initialValues} - initialErrors={initialErrors} - validate={handleValidate} - onSubmit={handleSubmit} - {...props} - /> + <FormContext.Provider value={state}> + <Formik + initialValues={initialValues} + initialErrors={initialErrors} + validate={handleValidate} + onSubmit={handleSubmit} + {...props} + /> + </FormContext.Provider> ); } diff --git a/frontend/src/metabase/core/components/FormSubmitButton/FormSubmitButton.tsx b/frontend/src/metabase/core/components/FormSubmitButton/FormSubmitButton.tsx index f0db016acc67fe6f5e810d602f9718d13a8846a7..05a1bff5f6eb972482f42ad383ae9a3477bd751c 100644 --- a/frontend/src/metabase/core/components/FormSubmitButton/FormSubmitButton.tsx +++ b/frontend/src/metabase/core/components/FormSubmitButton/FormSubmitButton.tsx @@ -1,8 +1,8 @@ import React, { forwardRef, Ref } from "react"; import { t } from "ttag"; import Button, { ButtonProps } from "metabase/core/components/Button"; -import { FormStatus } from "metabase/core/hooks/use-form-state"; import useFormSubmitButton from "metabase/core/hooks/use-form-submit-button"; +import { FormStatus } from "metabase/core/context/FormContext"; export interface FormSubmitButtonProps extends Omit<ButtonProps, "children"> { title?: string; diff --git a/frontend/src/metabase/core/hooks/use-form-state/types.ts b/frontend/src/metabase/core/context/FormContext/FormContext.tsx similarity index 51% rename from frontend/src/metabase/core/hooks/use-form-state/types.ts rename to frontend/src/metabase/core/context/FormContext/FormContext.tsx index 97088e2d977fde10fae0b873ae447212debd7103..52ce9ac203dcd2aaff989537758936a2439f1ff9 100644 --- a/frontend/src/metabase/core/hooks/use-form-state/types.ts +++ b/frontend/src/metabase/core/context/FormContext/FormContext.tsx @@ -1,6 +1,14 @@ +import { createContext } from "react"; + export type FormStatus = "idle" | "pending" | "fulfilled" | "rejected"; export interface FormState { status: FormStatus; message?: string; } + +const FormContext = createContext<FormState>({ + status: "idle", +}); + +export default FormContext; diff --git a/frontend/src/metabase/core/context/FormContext/index.ts b/frontend/src/metabase/core/context/FormContext/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..f0857a740f261244e7689ddde8c8fa56df52e3d7 --- /dev/null +++ b/frontend/src/metabase/core/context/FormContext/index.ts @@ -0,0 +1,2 @@ +export { default } from "./FormContext"; +export type { FormState, FormStatus } from "./FormContext"; diff --git a/frontend/src/metabase/core/hooks/use-form-context/index.ts b/frontend/src/metabase/core/hooks/use-form-context/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..81a70f12e77bf6792432090fcbd63f3ce1d1c2fe --- /dev/null +++ b/frontend/src/metabase/core/hooks/use-form-context/index.ts @@ -0,0 +1,2 @@ +export { default } from "./use-form-context"; +export type { FormState, FormStatus } from "metabase/core/context/FormContext"; diff --git a/frontend/src/metabase/core/hooks/use-form-context/use-form-context.ts b/frontend/src/metabase/core/hooks/use-form-context/use-form-context.ts new file mode 100644 index 0000000000000000000000000000000000000000..eb6b5680b8d660be311546ab7582f4089f6f41ca --- /dev/null +++ b/frontend/src/metabase/core/hooks/use-form-context/use-form-context.ts @@ -0,0 +1,8 @@ +import { useContext } from "react"; +import FormContext, { FormState } from "metabase/core/context/FormContext"; + +const useFormContext = (): FormState => { + return useContext(FormContext); +}; + +export default useFormContext; diff --git a/frontend/src/metabase/core/hooks/use-form-error-message/use-form-error-message.ts b/frontend/src/metabase/core/hooks/use-form-error-message/use-form-error-message.ts index 2c913214e507146ac0a39ebd0ecc984bd5dc8fa9..ade7aa8e6d783b30ce93fc16cf750758bb300277 100644 --- a/frontend/src/metabase/core/hooks/use-form-error-message/use-form-error-message.ts +++ b/frontend/src/metabase/core/hooks/use-form-error-message/use-form-error-message.ts @@ -1,11 +1,11 @@ import { useLayoutEffect, useState } from "react"; import { useFormikContext } from "formik"; import { t } from "ttag"; -import useFormState from "metabase/core/hooks/use-form-state"; +import useFormContext from "metabase/core/hooks/use-form-context"; const useFormErrorMessage = (): string | undefined => { const { values, errors } = useFormikContext(); - const { status, message } = useFormState(); + const { status, message } = useFormContext(); const [isVisible, setIsVisible] = useState(false); const hasErrors = Object.keys(errors).length > 0; diff --git a/frontend/src/metabase/core/hooks/use-form-state/index.ts b/frontend/src/metabase/core/hooks/use-form-state/index.ts deleted file mode 100644 index 1acef3bdec7d548ce1afe8f733c52d6979c9fb74..0000000000000000000000000000000000000000 --- a/frontend/src/metabase/core/hooks/use-form-state/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { default } from "./use-form-state"; -export type { FormState, FormStatus } from "./types"; diff --git a/frontend/src/metabase/core/hooks/use-form-state/use-form-state.ts b/frontend/src/metabase/core/hooks/use-form-state/use-form-state.ts deleted file mode 100644 index 14bca0a64e3f6f8eda9045fc8d4038590f17bee2..0000000000000000000000000000000000000000 --- a/frontend/src/metabase/core/hooks/use-form-state/use-form-state.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { useFormikContext } from "formik"; -import { FormState } from "./types"; - -const DEFAULT_STATE: FormState = { - status: "idle", -}; - -const useFormState = (): FormState => { - const { status } = useFormikContext(); - return status ?? DEFAULT_STATE; -}; - -export default useFormState; diff --git a/frontend/src/metabase/core/hooks/use-form-submit-button/use-form-submit-button.ts b/frontend/src/metabase/core/hooks/use-form-submit-button/use-form-submit-button.ts index 00661433ab1482cafa4f23f09bf3b1a5f43e1c1e..dacdfb7692be8fe4f4428c24291b60f6e82ba67b 100644 --- a/frontend/src/metabase/core/hooks/use-form-submit-button/use-form-submit-button.ts +++ b/frontend/src/metabase/core/hooks/use-form-submit-button/use-form-submit-button.ts @@ -1,6 +1,8 @@ import { useEffect, useLayoutEffect, useState } from "react"; import { useFormikContext } from "formik"; -import useFormState, { FormStatus } from "metabase/core/hooks/use-form-state"; +import useFormContext, { + FormStatus, +} from "metabase/core/hooks/use-form-context"; const STATUS_TIMEOUT = 5000; @@ -17,7 +19,7 @@ const useFormSubmitButton = ({ isDisabled = false, }: UseFormSubmitButtonProps): UseFormSubmitButtonResult => { const { isValid, isSubmitting } = useFormikContext(); - const { status } = useFormState(); + const { status } = useFormContext(); const isRecent = useIsRecent(status, STATUS_TIMEOUT); return { diff --git a/frontend/src/metabase/core/hooks/use-form-submit/use-form-submit.ts b/frontend/src/metabase/core/hooks/use-form-submit/use-form-submit.ts index 3d5650f66934de3d1dd2acd6f47aac13dfb46c3d..fd3e9ede5fa45070fd3decf95b3c088378b826e8 100644 --- a/frontend/src/metabase/core/hooks/use-form-submit/use-form-submit.ts +++ b/frontend/src/metabase/core/hooks/use-form-submit/use-form-submit.ts @@ -1,5 +1,6 @@ -import { useCallback } from "react"; +import { useCallback, useState } from "react"; import type { FormikHelpers } from "formik"; +import { FormState } from "metabase/core/context/FormContext"; import { FormError } from "./types"; export interface UseFormSubmitProps<T> { @@ -7,30 +8,31 @@ export interface UseFormSubmitProps<T> { } export interface UseFormSubmitResult<T> { + state: FormState; handleSubmit: (values: T, helpers: FormikHelpers<T>) => void; } const useFormSubmit = <T>({ onSubmit, }: UseFormSubmitProps<T>): UseFormSubmitResult<T> => { + const [state, setState] = useState<FormState>({ status: "idle" }); + const handleSubmit = useCallback( async (data: T, helpers: FormikHelpers<T>) => { try { - helpers.setStatus({ status: "pending" }); + setState({ status: "pending" }); await onSubmit(data, helpers); - helpers.setStatus({ status: "fulfilled" }); + setState({ status: "fulfilled" }); } catch (error) { helpers.setErrors(getFormErrors(error)); - helpers.setStatus({ - status: "rejected", - message: getFormMessage(error), - }); + setState({ status: "rejected", message: getFormMessage(error) }); } }, [onSubmit], ); return { + state, handleSubmit, }; };