diff --git a/src/orders/components/OrderManualTransactionDialog/OrderManualTransactionDialog.tsx b/src/orders/components/OrderManualTransactionDialog/OrderManualTransactionDialog.tsx index c4b21fc0e4d..1794ef44f58 100644 --- a/src/orders/components/OrderManualTransactionDialog/OrderManualTransactionDialog.tsx +++ b/src/orders/components/OrderManualTransactionDialog/OrderManualTransactionDialog.tsx @@ -1,16 +1,8 @@ // @ts-strict-ignore import BackButton from "@dashboard/components/BackButton"; +import { DashboardModal } from "@dashboard/components/Modal"; import { commonMessages } from "@dashboard/intl"; import { DialogProps } from "@dashboard/types"; -import { - Dialog, - DialogActions, - DialogContent, - DialogContentText, - DialogTitle, -} from "@material-ui/core"; -import { makeStyles } from "@saleor/macaw-ui"; -import { Text } from "@saleor/macaw-ui-next"; import React from "react"; import { FormattedMessage, useIntl } from "react-intl"; @@ -20,20 +12,6 @@ import { } from "../OrderManualTransactionForm"; import { manualTransactionMessages } from "./messages"; -const useStyles = makeStyles( - theme => ({ - form: { - display: "contents", - }, - formWrapper: { - display: "flex", - flexDirection: "column", - gap: theme.spacing(2), - }, - }), - { name: "OrderManualTransactionDialog" }, -); - type OrderManualTransactionDialogProps = { dialogProps: DialogProps; } & OrderManualTransactionFormProps; @@ -43,48 +21,37 @@ export const OrderManualTransactionDialog: React.FC { const intl = useIntl(); - const classes = useStyles(); const { onClose } = dialogProps; return ( - - - + + + - - - - - - - -
- - - - -
-
- - + + + + + + + + + - - + - - - -
-
+ + + + +
); }; diff --git a/src/orders/components/OrderManualTransactionForm/OrderManualTransactionForm.tsx b/src/orders/components/OrderManualTransactionForm/OrderManualTransactionForm.tsx index 020e7da4d41..e23aec29748 100644 --- a/src/orders/components/OrderManualTransactionForm/OrderManualTransactionForm.tsx +++ b/src/orders/components/OrderManualTransactionForm/OrderManualTransactionForm.tsx @@ -1,10 +1,11 @@ import { ConfirmButtonTransitionState } from "@dashboard/components/ConfirmButton"; import React from "react"; +import { FormProvider } from "react-hook-form"; import { ManualTransactionContext } from "./context"; -import { useManualRefund } from "./hooks"; +import { useApiError, useManualRefund } from "./hooks"; -interface OrderManualTransactionSubmitVariables { +export interface OrderManualTransactionSubmitVariables { amount: number; description: string; pspReference: string | undefined; @@ -15,24 +16,34 @@ export interface OrderManualTransactionFormProps { currency: string; submitState: ConfirmButtonTransitionState; error: string | undefined; - initialData?: Partial; } export const OrderManualTransactionForm: React.FC = ({ children, ...props }) => { - const { submitState, initialData } = props; - const hookData = useManualRefund({ submitState, initialData }); + const methods = useManualRefund(); + + React.useEffect(() => { + if (props.error) { + methods.setError("root", { + type: "manual", + message: props.error, + }); + } + }, [props.error, methods]); + + // useApiError(props.error); + + React.useEffect(() => { + if (methods.formState.isSubmitSuccessful) { + methods.reset(); + } + }, [methods.formState.isSubmitSuccessful]); return ( - - {children} + + {children} ); }; diff --git a/src/orders/components/OrderManualTransactionForm/components/DescriptionField.tsx b/src/orders/components/OrderManualTransactionForm/components/DescriptionField.tsx index 50e2a736306..4283f9bdf8e 100644 --- a/src/orders/components/OrderManualTransactionForm/components/DescriptionField.tsx +++ b/src/orders/components/OrderManualTransactionForm/components/DescriptionField.tsx @@ -1,27 +1,21 @@ // @ts-strict-ignore -import { TextField, TextFieldProps } from "@material-ui/core"; +import { Input, InputProps } from "@saleor/macaw-ui-next"; import React from "react"; +import { Controller } from "react-hook-form"; import { useManualTransactionContext } from "../context"; -export const DescriptionField: React.FC> = ({ +export const DescriptionField: React.FC> = ({ disabled, ...props }) => { - const { submitState, handleChangeDescription, description } = useManualTransactionContext(); + const { control } = useManualTransactionContext(); return ( - } /> ); }; diff --git a/src/orders/components/OrderManualTransactionForm/components/ErrorText.tsx b/src/orders/components/OrderManualTransactionForm/components/ErrorText.tsx index 9a6732840ba..cda5e694a52 100644 --- a/src/orders/components/OrderManualTransactionForm/components/ErrorText.tsx +++ b/src/orders/components/OrderManualTransactionForm/components/ErrorText.tsx @@ -1,19 +1,23 @@ // @ts-strict-ignore -import { Typography, TypographyProps } from "@material-ui/core"; +import { Box, Text, TextProps } from "@saleor/macaw-ui-next"; import React from "react"; import { useManualTransactionContext } from "../context"; -export const ErrorText: React.FC = props => { - const { error } = useManualTransactionContext(); +export const ErrorText: React.FC> = props => { + const { formState } = useManualTransactionContext(); - if (!error) { + if (!formState.errors) { return null; } return ( - - {error} - + + {Object.values(formState.errors).map((field, key) => ( + + {field.message} + + ))} + ); }; diff --git a/src/orders/components/OrderManualTransactionForm/components/Form.test.tsx b/src/orders/components/OrderManualTransactionForm/components/Form.test.tsx deleted file mode 100644 index 2f838387e5e..00000000000 --- a/src/orders/components/OrderManualTransactionForm/components/Form.test.tsx +++ /dev/null @@ -1,69 +0,0 @@ -// @ts-strict-ignore -import { fireEvent, render } from "@testing-library/react"; -import React from "react"; - -import { OrderManualTransactionFormProps } from ".."; -import { OrderManualTransactionForm } from "../OrderManualTransactionForm"; -import { Form } from "./Form"; - -const commonTransactionFormProps: Pick< - OrderManualTransactionFormProps, - "currency" | "submitState" | "error" -> = { - currency: "USD", - submitState: "default", - error: undefined, -}; - -describe("OrderManualTrasactionForm / Form", () => { - test("it handles submit event", () => { - const submitFn = jest.fn(); - - render( - -
- , - ); - - const form = document.querySelector("form"); - - fireEvent.submit(form); - expect(submitFn).toHaveBeenCalledWith({ - amount: 1, - description: "test", - pspReference: "test-1234", - }); - }); - test("it doesn't handle submit if amount is missing", () => { - const submitFn = jest.fn(); - - render( - - - , - ); - - const form = document.querySelector("form"); - - fireEvent.submit(form); - expect(submitFn).not.toHaveBeenCalled(); - }); -}); diff --git a/src/orders/components/OrderManualTransactionForm/components/Form.tsx b/src/orders/components/OrderManualTransactionForm/components/Form.tsx index 7d71da00654..e48c5823f41 100644 --- a/src/orders/components/OrderManualTransactionForm/components/Form.tsx +++ b/src/orders/components/OrderManualTransactionForm/components/Form.tsx @@ -1,23 +1,17 @@ // @ts-strict-ignore +import { Box } from "@saleor/macaw-ui-next"; import React from "react"; import { useManualTransactionContext } from "../context"; -export const Form: React.FC> = ({ children, ...props }) => { - const { amount, description, pspReference, onAddTransaction } = useManualTransactionContext(); +export const Form: React.FC = ({ children }) => { + const { handleSubmit, onAddTransaction } = useManualTransactionContext(); return ( - { - e.preventDefault(); - - if (amount) { - onAddTransaction({ amount, description, pspReference }); - } - }} - > - {children} + + + {children} + ); }; diff --git a/src/orders/components/OrderManualTransactionForm/components/PriceInputField.tsx b/src/orders/components/OrderManualTransactionForm/components/PriceInputField.tsx index 7d414230ea7..e1806c3390e 100644 --- a/src/orders/components/OrderManualTransactionForm/components/PriceInputField.tsx +++ b/src/orders/components/OrderManualTransactionForm/components/PriceInputField.tsx @@ -1,22 +1,34 @@ // @ts-strict-ignore -import PriceField, { PriceFieldProps } from "@dashboard/components/PriceField"; +import { Input, InputProps, Text } from "@saleor/macaw-ui-next"; import React from "react"; +import { Controller } from "react-hook-form"; import { useManualTransactionContext } from "../context"; -export const PriceInputField: React.FC< - Omit -> = ({ disabled, ...props }) => { - const { currency, submitState, handleChangeAmount, amount } = useManualTransactionContext(); +export const PriceInputField: React.FC> = ({ + disabled, + ...props +}) => { + const { + control, + currency, + formState: { errors }, + } = useManualTransactionContext(); return ( - ( + {currency}} + error={!!errors.amount} + /> + )} /> ); }; diff --git a/src/orders/components/OrderManualTransactionForm/components/PspReferenceField.tsx b/src/orders/components/OrderManualTransactionForm/components/PspReferenceField.tsx index b5b6c30bd69..4133a7df90c 100644 --- a/src/orders/components/OrderManualTransactionForm/components/PspReferenceField.tsx +++ b/src/orders/components/OrderManualTransactionForm/components/PspReferenceField.tsx @@ -1,28 +1,21 @@ // @ts-strict-ignore -import { TextField, TextFieldProps } from "@material-ui/core"; +import { Input, InputProps } from "@saleor/macaw-ui-next"; import React from "react"; +import { Controller } from "react-hook-form"; import { useManualTransactionContext } from "../context"; -export const PspReferenceField: React.FC> = ({ +export const PspReferenceField: React.FC> = ({ disabled, - variant = "outlined", ...props }) => { - const { submitState, pspReference, handleChangePspReference } = useManualTransactionContext(); + const { control } = useManualTransactionContext(); return ( - } /> ); }; diff --git a/src/orders/components/OrderManualTransactionForm/components/SubmitButton.tsx b/src/orders/components/OrderManualTransactionForm/components/SubmitButton.tsx index 4b7789593ce..ebf21cde658 100644 --- a/src/orders/components/OrderManualTransactionForm/components/SubmitButton.tsx +++ b/src/orders/components/OrderManualTransactionForm/components/SubmitButton.tsx @@ -8,14 +8,13 @@ export const SubmitButton: React.FC { - const { submitState, amount } = useManualTransactionContext(); + const { submitState } = useManualTransactionContext(); return ( diff --git a/src/orders/components/OrderManualTransactionForm/context.ts b/src/orders/components/OrderManualTransactionForm/context.ts index cca82f2fc13..c96369e32a7 100644 --- a/src/orders/components/OrderManualTransactionForm/context.ts +++ b/src/orders/components/OrderManualTransactionForm/context.ts @@ -1,10 +1,22 @@ import { createContext, useContext } from "react"; +import { useFormContext, UseFormReturn } from "react-hook-form"; -import { ManualRefundData } from "./hooks"; -import { OrderManualTransactionFormProps } from "./OrderManualTransactionForm"; +import { + OrderManualTransactionFormProps, + OrderManualTransactionSubmitVariables, +} from "./OrderManualTransactionForm"; -export const ManualTransactionContext = createContext< - (ManualRefundData & OrderManualTransactionFormProps) | null ->(null); +export const ManualTransactionContext = createContext(null); -export const useManualTransactionContext = () => useContext(ManualTransactionContext); +type ManualTransactionContextType = OrderManualTransactionFormProps & + UseFormReturn; + +export const useManualTransactionContext = (): ManualTransactionContextType => { + const manualTransactionCtx = useContext(ManualTransactionContext); + const rhfCtx = useFormContext(); + + return { + ...manualTransactionCtx, + ...rhfCtx, + }; +}; diff --git a/src/orders/components/OrderManualTransactionForm/hooks.test.ts b/src/orders/components/OrderManualTransactionForm/hooks.test.ts deleted file mode 100644 index 196bbee4220..00000000000 --- a/src/orders/components/OrderManualTransactionForm/hooks.test.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { ConfirmButtonTransitionState } from "@dashboard/components/ConfirmButton"; -import { act, renderHook } from "@testing-library/react-hooks"; -import { ChangeEvent } from "react"; - -import { useManualRefund } from "./hooks"; - -const fakeChangeEvent = (value: string): ChangeEvent => - ({ - target: { - value, - }, - }) as ChangeEvent; - -describe("useManualRefund hook", () => { - it("clears data after successful submit", () => { - let submitState: ConfirmButtonTransitionState = "default"; - const { result, rerender } = renderHook(() => - useManualRefund({ - submitState, - initialData: { - amount: 12, - description: "test", - pspReference: "test-1234", - }, - }), - ); - - expect(result.current.amount).toBe(12); - expect(result.current.description).toBe("test"); - expect(result.current.pspReference).toBe("test-1234"); - submitState = "loading"; - rerender(); - submitState = "success"; - rerender(); - expect(result.current.amount).toBe(undefined); - expect(result.current.description).toBe(""); - expect(result.current.pspReference).toBe(undefined); - }); - it("updates amount after user changes form input", () => { - const { result } = renderHook(() => - useManualRefund({ - submitState: "default", - }), - ); - - expect(result.current.amount).toBe(undefined); - act(() => { - result.current.handleChangeAmount(fakeChangeEvent("12.00")); - }); - expect(result.current.amount).toBe(12); - // clears value when cannot parse - act(() => { - result.current.handleChangeAmount(fakeChangeEvent("abcde")); - }); - expect(result.current.amount).toBe(undefined); - }); - it("updates description after user changes form input", () => { - const { result } = renderHook(() => - useManualRefund({ - submitState: "default", - }), - ); - - expect(result.current.description).toBe(""); - act(() => { - result.current.handleChangeDescription(fakeChangeEvent("new-description")); - }); - expect(result.current.description).toBe("new-description"); - }); - it("updates psp reference after user changes form input", () => { - const { result } = renderHook(() => - useManualRefund({ - submitState: "default", - }), - ); - - expect(result.current.pspReference).toBe(undefined); - act(() => { - result.current.handleChangePspReference(fakeChangeEvent("test-1234")); - }); - expect(result.current.pspReference).toBe("test-1234"); - }); -}); diff --git a/src/orders/components/OrderManualTransactionForm/hooks.ts b/src/orders/components/OrderManualTransactionForm/hooks.ts index c14f0b2b8fd..eede386d676 100644 --- a/src/orders/components/OrderManualTransactionForm/hooks.ts +++ b/src/orders/components/OrderManualTransactionForm/hooks.ts @@ -1,55 +1,54 @@ -import { ConfirmButtonTransitionState } from "@dashboard/components/ConfirmButton"; -import React from "react"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useForm, UseFormReturn } from "react-hook-form"; +import { IntlShape, useIntl } from "react-intl"; +import { z } from "zod"; -interface ManualRefundHookProps { - submitState: ConfirmButtonTransitionState; - initialData?: { - amount?: number; - description?: string; - pspReference?: string; - }; -} +import { manualTransactionFormMessages } from "./messages"; export type ManualRefundData = ReturnType; -export const useManualRefund = ({ submitState, initialData }: ManualRefundHookProps) => { - const [amount, setAmount] = React.useState(initialData?.amount); - const [description, setDescription] = React.useState(initialData?.description ?? ""); - const [pspReference, setPspReference] = React.useState( - initialData?.pspReference, - ); - - React.useEffect(() => { - if (submitState === "success") { - // reset state after submit - setAmount(undefined); - setDescription(""); - setPspReference(undefined); - } - }, [submitState]); +const initialFormValues = { + amount: "", + description: "", + pspReference: "", +}; - const handleChangeDescription: React.ChangeEventHandler = e => { - setDescription(e.target.value); - }; - const handleChangeAmount: React.ChangeEventHandler = e => { - const value = parseFloat(e.target.value); +const getManualTransactionValidationSchema = (intl: IntlShape) => { + return z.object({ + amount: z + .string() + .transform(val => val.replace(",", ".")) + .pipe( + z.coerce.number().refine(val => val !== 0, { + message: intl.formatMessage(manualTransactionFormMessages.amountRequired), + }), + ), + description: z.string().optional(), + pspReference: z.string().optional(), + }); +}; - if (!Number.isNaN(value)) { - setAmount(value); - } else { - setAmount(undefined); - } - }; - const handleChangePspReference: React.ChangeEventHandler = e => { - setPspReference(e.target.value); - }; +export const useManualRefund = () => { + const intl = useIntl(); + const methods = useForm({ + mode: "onSubmit", + values: initialFormValues, + resolver: zodResolver(getManualTransactionValidationSchema(intl)), + }); - return { - amount, - description, - pspReference, - handleChangeDescription, - handleChangeAmount, - handleChangePspReference, - }; + return methods; }; + +// export const useApiError = ( +// error: string | undefined, +// methods: UseFormReturn, +// ) => { +// React.useEffect(() => { +// if (error) { +// methods.setError("root", { +// type: "manual", +// message: error, +// }); +// } +// }, [error, methods]); +// }; diff --git a/src/orders/components/OrderManualTransactionForm/messages.ts b/src/orders/components/OrderManualTransactionForm/messages.ts new file mode 100644 index 00000000000..5efb4a073a1 --- /dev/null +++ b/src/orders/components/OrderManualTransactionForm/messages.ts @@ -0,0 +1,16 @@ +import { defineMessages } from "react-intl"; + +export const manualTransactionFormMessages = defineMessages({ + amountPositive: { + id: "XjW94S", + defaultMessage: "Amount must be a positive number.", + }, + amountRequired: { + id: "/1byDa", + defaultMessage: "Amount is required.", + }, + pspReferenceRequired: { + id: "QEMUSz", + defaultMessage: "PSP Reference is required.", + }, +}); diff --git a/src/orders/views/OrderDetails/OrderNormalDetails/index.tsx b/src/orders/views/OrderDetails/OrderNormalDetails/index.tsx index 94dae9e0fd4..99586d2f2d5 100644 --- a/src/orders/views/OrderDetails/OrderNormalDetails/index.tsx +++ b/src/orders/views/OrderDetails/OrderNormalDetails/index.tsx @@ -427,7 +427,7 @@ export const OrderNormalDetails: React.FC = ({ onAddTransaction={({ amount, description, pspReference }) => orderAddManualTransaction.mutate({ currency: data?.order?.totalBalance?.currency, - orderId: id, + orderId: null, amount, description, pspReference, diff --git a/src/orders/views/OrderDetails/OrderUnconfirmedDetails/index.tsx b/src/orders/views/OrderDetails/OrderUnconfirmedDetails/index.tsx index 78536570ce3..e41a675750f 100644 --- a/src/orders/views/OrderDetails/OrderUnconfirmedDetails/index.tsx +++ b/src/orders/views/OrderDetails/OrderUnconfirmedDetails/index.tsx @@ -447,7 +447,7 @@ export const OrderUnconfirmedDetails: React.FC = ( onAddTransaction={({ amount, description }) => orderAddManualTransaction.mutate({ currency: data?.order?.totalBalance?.currency, - orderId: id, + orderId: null, amount, description, })