mirror of
https://github.com/stefanpejcic/openpanel
synced 2025-06-26 18:28:26 +00:00
packages
This commit is contained in:
3
packages/mantine/src/hooks/form/index.ts
Normal file
3
packages/mantine/src/hooks/form/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from "./useForm";
|
||||
export * from "./useModalForm";
|
||||
export * from "./useStepsForm";
|
||||
253
packages/mantine/src/hooks/form/useForm/index.spec.tsx
Normal file
253
packages/mantine/src/hooks/form/useForm/index.spec.tsx
Normal file
@@ -0,0 +1,253 @@
|
||||
import React from "react";
|
||||
import { MockJSONServer, TestWrapper, render, waitFor } from "@test";
|
||||
import { Routes, Route } from "react-router-dom";
|
||||
import { useForm } from ".";
|
||||
import { Select, TextInput } from "@mantine/core";
|
||||
import { useSelect } from "@hooks/useSelect";
|
||||
import { Edit } from "@components/crud";
|
||||
import type { IRefineOptions, HttpError } from "@refinedev/core";
|
||||
import { act } from "react-dom/test-utils";
|
||||
|
||||
const renderForm = ({
|
||||
refineCoreProps,
|
||||
refineOptions,
|
||||
useFormProps,
|
||||
}: {
|
||||
useFormProps?: any;
|
||||
refineCoreProps?: any;
|
||||
refineOptions?: IRefineOptions;
|
||||
}) => {
|
||||
const EditPage = () => {
|
||||
const {
|
||||
saveButtonProps,
|
||||
getInputProps,
|
||||
refineCore: { query, formLoading },
|
||||
} = useForm({
|
||||
...useFormProps,
|
||||
refineCoreProps: {
|
||||
...refineCoreProps,
|
||||
resource: "posts",
|
||||
id: refineCoreProps.action === "edit" ? "1" : undefined,
|
||||
action: refineCoreProps.action,
|
||||
},
|
||||
initialValues: {
|
||||
title: "",
|
||||
content: "",
|
||||
category: {
|
||||
id: "",
|
||||
},
|
||||
tags: [],
|
||||
},
|
||||
});
|
||||
|
||||
const { selectProps, queryResult: categoriesQueryResult } = useSelect({
|
||||
resource: "categories",
|
||||
defaultValue: query?.data?.data?.category?.id,
|
||||
});
|
||||
|
||||
if (formLoading || categoriesQueryResult.isLoading) {
|
||||
return <div>loading</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Edit saveButtonProps={saveButtonProps}>
|
||||
<form>
|
||||
<TextInput
|
||||
mt={8}
|
||||
id="title"
|
||||
label="Title"
|
||||
placeholder="Title"
|
||||
{...getInputProps("title")}
|
||||
/>
|
||||
<TextInput
|
||||
mt={8}
|
||||
id="content"
|
||||
label="Content"
|
||||
placeholder="Content"
|
||||
{...getInputProps("content")}
|
||||
/>
|
||||
<Select
|
||||
mt={8}
|
||||
id="categoryId"
|
||||
label="Category"
|
||||
placeholder="Pick one"
|
||||
{...getInputProps("category.id")}
|
||||
{...selectProps}
|
||||
/>
|
||||
<TextInput placeholder="Tag 1" {...getInputProps(`tags.${0}`)} />
|
||||
<TextInput placeholder="Tag 2" {...getInputProps(`tags.${1}`)} />
|
||||
</form>
|
||||
</Edit>
|
||||
);
|
||||
};
|
||||
|
||||
return render(
|
||||
<Routes>
|
||||
<Route path="/" element={<EditPage />} />
|
||||
</Routes>,
|
||||
{
|
||||
wrapper: TestWrapper({
|
||||
options: refineOptions,
|
||||
i18nProvider: {
|
||||
changeLocale: () => Promise.resolve(),
|
||||
getLocale: () => "en",
|
||||
translate: (key: string) => {
|
||||
if (key === "form.error.content") {
|
||||
return "Translated content error";
|
||||
}
|
||||
|
||||
return key;
|
||||
},
|
||||
},
|
||||
dataProvider: {
|
||||
...MockJSONServer,
|
||||
create: async () => {
|
||||
const error: HttpError = {
|
||||
message: "An error occurred while updating the record.",
|
||||
statusCode: 400,
|
||||
errors: {
|
||||
title: ["Title is required"],
|
||||
"category.id": ["Category is required"],
|
||||
content: {
|
||||
key: "form.error.content",
|
||||
message: "Content is required",
|
||||
},
|
||||
"tags.0": ["Tag 0 is required"],
|
||||
"tags.1": ["Tag 1 is required"],
|
||||
},
|
||||
};
|
||||
return Promise.reject(error);
|
||||
},
|
||||
update: async () => {
|
||||
const error: HttpError = {
|
||||
message: "An error occurred while updating the record.",
|
||||
statusCode: 400,
|
||||
errors: {
|
||||
title: ["Title is required"],
|
||||
"category.id": ["Category is required"],
|
||||
content: {
|
||||
key: "form.error.content",
|
||||
message: "Content is required",
|
||||
},
|
||||
"tags.0": ["Tag 0 is required"],
|
||||
"tags.1": ["Tag 1 is required"],
|
||||
},
|
||||
};
|
||||
return Promise.reject(error);
|
||||
},
|
||||
getMany: async () => {
|
||||
return Promise.resolve([
|
||||
{
|
||||
id: 1,
|
||||
name: "lorem ipsum dolor",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: "Sit amet consectetur",
|
||||
},
|
||||
]);
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
describe("useForm hook", () => {
|
||||
it.each(["edit", "create"] as const)(
|
||||
"should set %s-form errors from data provider",
|
||||
async (action) => {
|
||||
const onMutationErrorMock = jest.fn();
|
||||
|
||||
const { getByTestId, getByText } = renderForm({
|
||||
refineCoreProps: {
|
||||
action: action,
|
||||
onMutationError: onMutationErrorMock,
|
||||
},
|
||||
});
|
||||
|
||||
await act(() => Promise.resolve());
|
||||
|
||||
await waitFor(() => {
|
||||
expect(document.body).not.toHaveTextContent("loading");
|
||||
});
|
||||
|
||||
await act(() => {
|
||||
getByTestId("refine-save-button").click();
|
||||
return Promise.resolve();
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(document.body).not.toHaveTextContent("loading");
|
||||
expect(onMutationErrorMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
expect(getByText("Title is required")).toBeInTheDocument();
|
||||
expect(getByText("Category is required")).toBeInTheDocument();
|
||||
expect(getByText("Translated content error")).toBeInTheDocument();
|
||||
expect(getByText("Tag 0 is required")).toBeInTheDocument();
|
||||
expect(getByText("Tag 1 is required")).toBeInTheDocument();
|
||||
},
|
||||
);
|
||||
|
||||
it.each([
|
||||
{
|
||||
action: "edit",
|
||||
disableFromRefineOption: false,
|
||||
disableFromHook: true,
|
||||
},
|
||||
{
|
||||
action: "edit",
|
||||
disableFromRefineOption: true,
|
||||
disableFromHook: false,
|
||||
},
|
||||
{
|
||||
action: "create",
|
||||
disableFromRefineOption: false,
|
||||
disableFromHook: true,
|
||||
},
|
||||
{
|
||||
action: "create",
|
||||
disableFromRefineOption: true,
|
||||
disableFromHook: false,
|
||||
},
|
||||
] as const)("should disable server-side validation", async (testCase) => {
|
||||
const onMutationErrorMock = jest.fn();
|
||||
|
||||
const { getByTestId, queryByText } = renderForm({
|
||||
refineOptions: {
|
||||
disableServerSideValidation: testCase.disableFromRefineOption,
|
||||
},
|
||||
useFormProps: {
|
||||
disableServerSideValidation: testCase.disableFromHook,
|
||||
},
|
||||
refineCoreProps: {
|
||||
action: testCase.action,
|
||||
onMutationError: onMutationErrorMock,
|
||||
},
|
||||
});
|
||||
|
||||
await act(() => Promise.resolve());
|
||||
|
||||
await waitFor(() => {
|
||||
expect(document.body).not.toHaveTextContent("loading");
|
||||
});
|
||||
|
||||
await act(() => {
|
||||
getByTestId("refine-save-button").click();
|
||||
return Promise.resolve();
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(document.body).not.toHaveTextContent("loading");
|
||||
expect(onMutationErrorMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(queryByText("Title is required")).not.toBeInTheDocument();
|
||||
expect(queryByText("Category is required")).not.toBeInTheDocument();
|
||||
expect(queryByText("Translated content error")).not.toBeInTheDocument();
|
||||
expect(queryByText("Field is not valid.")).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
259
packages/mantine/src/hooks/form/useForm/index.ts
Normal file
259
packages/mantine/src/hooks/form/useForm/index.ts
Normal file
@@ -0,0 +1,259 @@
|
||||
import React, { useEffect } from "react";
|
||||
import {
|
||||
useForm as useMantineForm,
|
||||
type UseFormReturnType as UseMantineFormReturnType,
|
||||
} from "@mantine/form";
|
||||
import get from "lodash/get";
|
||||
import has from "lodash/has";
|
||||
import set from "lodash/set";
|
||||
import type { UseFormInput } from "@mantine/form/lib/types";
|
||||
import {
|
||||
type BaseRecord,
|
||||
type HttpError,
|
||||
useForm as useFormCore,
|
||||
useWarnAboutChange,
|
||||
type UseFormProps as UseFormCoreProps,
|
||||
type UseFormReturnType as UseFormReturnTypeCore,
|
||||
useTranslate,
|
||||
useRefineContext,
|
||||
flattenObjectKeys,
|
||||
} from "@refinedev/core";
|
||||
|
||||
type FormVariableType<TVariables, TTransformed> = ReturnType<
|
||||
NonNullable<
|
||||
UseFormInput<
|
||||
TVariables,
|
||||
(values: TVariables) => TTransformed
|
||||
>["transformValues"]
|
||||
>
|
||||
>;
|
||||
|
||||
export type UseFormReturnType<
|
||||
TQueryFnData extends BaseRecord = BaseRecord,
|
||||
TError extends HttpError = HttpError,
|
||||
TVariables = Record<string, unknown>,
|
||||
TTransformed = TVariables,
|
||||
TData extends BaseRecord = TQueryFnData,
|
||||
TResponse extends BaseRecord = TData,
|
||||
TResponseError extends HttpError = TError,
|
||||
> = UseMantineFormReturnType<
|
||||
TVariables,
|
||||
(values: TVariables) => TTransformed
|
||||
> & {
|
||||
refineCore: UseFormReturnTypeCore<
|
||||
TQueryFnData,
|
||||
TError,
|
||||
FormVariableType<TVariables, TTransformed>,
|
||||
TData,
|
||||
TResponse,
|
||||
TResponseError
|
||||
>;
|
||||
saveButtonProps: {
|
||||
disabled: boolean;
|
||||
onClick: (e: React.PointerEvent<HTMLButtonElement>) => void;
|
||||
};
|
||||
};
|
||||
|
||||
export type UseFormProps<
|
||||
TQueryFnData extends BaseRecord = BaseRecord,
|
||||
TError extends HttpError = HttpError,
|
||||
TVariables = Record<string, unknown>,
|
||||
TTransformed = TVariables,
|
||||
TData extends BaseRecord = TQueryFnData,
|
||||
TResponse extends BaseRecord = TData,
|
||||
TResponseError extends HttpError = TError,
|
||||
> = {
|
||||
refineCoreProps?: UseFormCoreProps<
|
||||
TQueryFnData,
|
||||
TError,
|
||||
FormVariableType<TVariables, TTransformed>,
|
||||
TData,
|
||||
TResponse,
|
||||
TResponseError
|
||||
> & {
|
||||
warnWhenUnsavedChanges?: boolean;
|
||||
};
|
||||
} & UseFormInput<TVariables, (values: TVariables) => TTransformed> & {
|
||||
/**
|
||||
* Disables server-side validation
|
||||
* @default false
|
||||
* @see {@link https://refine.dev/docs/advanced-tutorials/forms/server-side-form-validation/}
|
||||
*/
|
||||
disableServerSideValidation?: boolean;
|
||||
};
|
||||
|
||||
export const useForm = <
|
||||
TQueryFnData extends BaseRecord = BaseRecord,
|
||||
TError extends HttpError = HttpError,
|
||||
TVariables = Record<string, unknown>,
|
||||
TTransformed = TVariables,
|
||||
TData extends BaseRecord = TQueryFnData,
|
||||
TResponse extends BaseRecord = TData,
|
||||
TResponseError extends HttpError = TError,
|
||||
>({
|
||||
refineCoreProps,
|
||||
disableServerSideValidation: disableServerSideValidationProp = false,
|
||||
...rest
|
||||
}: UseFormProps<
|
||||
TQueryFnData,
|
||||
TError,
|
||||
TVariables,
|
||||
TTransformed,
|
||||
TData,
|
||||
TResponse,
|
||||
TResponseError
|
||||
> = {}): UseFormReturnType<
|
||||
TQueryFnData,
|
||||
TError,
|
||||
TVariables,
|
||||
TTransformed,
|
||||
TData,
|
||||
TResponse,
|
||||
TResponseError
|
||||
> => {
|
||||
const { options } = useRefineContext();
|
||||
const disableServerSideValidation =
|
||||
options?.disableServerSideValidation || disableServerSideValidationProp;
|
||||
|
||||
const translate = useTranslate();
|
||||
|
||||
const warnWhenUnsavedChangesProp = refineCoreProps?.warnWhenUnsavedChanges;
|
||||
|
||||
const { warnWhenUnsavedChanges: warnWhenUnsavedChangesRefine, setWarnWhen } =
|
||||
useWarnAboutChange();
|
||||
const warnWhenUnsavedChanges =
|
||||
warnWhenUnsavedChangesProp ?? warnWhenUnsavedChangesRefine;
|
||||
|
||||
const useMantineFormResult = useMantineForm<
|
||||
TVariables,
|
||||
(values: TVariables) => TTransformed
|
||||
>({
|
||||
...rest,
|
||||
});
|
||||
|
||||
const {
|
||||
setValues,
|
||||
onSubmit: onMantineSubmit,
|
||||
isDirty,
|
||||
resetDirty,
|
||||
setFieldError,
|
||||
values,
|
||||
} = useMantineFormResult;
|
||||
|
||||
const useFormCoreResult = useFormCore<
|
||||
TQueryFnData,
|
||||
TError,
|
||||
FormVariableType<TVariables, TTransformed>,
|
||||
TData,
|
||||
TResponse,
|
||||
TResponseError
|
||||
>({
|
||||
...refineCoreProps,
|
||||
onMutationError: (error, _variables, _context) => {
|
||||
if (disableServerSideValidation) {
|
||||
refineCoreProps?.onMutationError?.(error, _variables, _context);
|
||||
return;
|
||||
}
|
||||
|
||||
const errors = error?.errors;
|
||||
|
||||
for (const key in errors) {
|
||||
const fieldError = errors[key];
|
||||
|
||||
let newError = "";
|
||||
|
||||
if (Array.isArray(fieldError)) {
|
||||
newError = fieldError.join(" ");
|
||||
}
|
||||
|
||||
if (typeof fieldError === "string") {
|
||||
newError = fieldError;
|
||||
}
|
||||
|
||||
if (typeof fieldError === "boolean") {
|
||||
newError = "Field is not valid.";
|
||||
}
|
||||
|
||||
if (typeof fieldError === "object" && "key" in fieldError) {
|
||||
const translatedMessage = translate(
|
||||
fieldError.key,
|
||||
fieldError.message,
|
||||
);
|
||||
|
||||
newError = translatedMessage;
|
||||
}
|
||||
setFieldError(key, newError);
|
||||
}
|
||||
|
||||
refineCoreProps?.onMutationError?.(error, _variables, _context);
|
||||
},
|
||||
});
|
||||
|
||||
const { query, formLoading, onFinish, onFinishAutoSave } = useFormCoreResult;
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof query?.data !== "undefined") {
|
||||
const fields: any = {};
|
||||
|
||||
const registeredFields = flattenObjectKeys(rest.initialValues ?? {});
|
||||
|
||||
const data = query?.data?.data ?? {};
|
||||
|
||||
Object.keys(registeredFields).forEach((key) => {
|
||||
const hasValue = has(data, key);
|
||||
const dataValue = get(data, key);
|
||||
|
||||
if (hasValue) {
|
||||
set(fields, key, dataValue);
|
||||
}
|
||||
});
|
||||
|
||||
setValues(fields);
|
||||
resetDirty(fields);
|
||||
}
|
||||
}, [query?.data]);
|
||||
|
||||
const isValuesChanged = isDirty();
|
||||
|
||||
useEffect(() => {
|
||||
if (warnWhenUnsavedChanges) {
|
||||
setWarnWhen(isValuesChanged);
|
||||
}
|
||||
}, [isValuesChanged]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isValuesChanged && refineCoreProps?.autoSave && values) {
|
||||
setWarnWhen(false);
|
||||
|
||||
const transformedValues = rest.transformValues
|
||||
? rest.transformValues(values)
|
||||
: (values as unknown as TTransformed);
|
||||
|
||||
onFinishAutoSave(transformedValues).catch((error) => error);
|
||||
}
|
||||
}, [values]);
|
||||
|
||||
const onSubmit: (typeof useMantineFormResult)["onSubmit"] =
|
||||
(handleSubmit, handleValidationFailure) => async (e) => {
|
||||
setWarnWhen(false);
|
||||
return await onMantineSubmit(handleSubmit, handleValidationFailure)(e);
|
||||
};
|
||||
|
||||
const saveButtonProps = {
|
||||
disabled: formLoading,
|
||||
onClick: (e: React.PointerEvent<HTMLButtonElement>) => {
|
||||
onSubmit(
|
||||
(v) => onFinish(v).catch((error) => error),
|
||||
() => false,
|
||||
// @ts-expect-error event type is not compatible with pointer event
|
||||
)(e);
|
||||
},
|
||||
};
|
||||
|
||||
return {
|
||||
...useMantineFormResult,
|
||||
onSubmit,
|
||||
refineCore: useFormCoreResult,
|
||||
saveButtonProps,
|
||||
};
|
||||
};
|
||||
47
packages/mantine/src/hooks/form/useModalForm/index.spec.tsx
Normal file
47
packages/mantine/src/hooks/form/useModalForm/index.spec.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
import { useModalForm } from "..";
|
||||
import { MockJSONServer, TestWrapper, waitFor, renderHook, act } from "@test";
|
||||
|
||||
describe("useModalForm hook", () => {
|
||||
it("should `meta[syncWithLocationKey]` overrided by default", async () => {
|
||||
const mockGetOne = jest.fn();
|
||||
const mockUpdate = jest.fn();
|
||||
|
||||
const { result } = renderHook(
|
||||
() =>
|
||||
useModalForm({
|
||||
syncWithLocation: true,
|
||||
refineCoreProps: {
|
||||
id: 5,
|
||||
action: "edit",
|
||||
resource: "posts",
|
||||
},
|
||||
}),
|
||||
{
|
||||
wrapper: TestWrapper({
|
||||
dataProvider: {
|
||||
...MockJSONServer,
|
||||
getOne: mockGetOne,
|
||||
update: mockUpdate,
|
||||
},
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
result.current.modal.show();
|
||||
});
|
||||
|
||||
await waitFor(() => expect(result.current.modal.visible).toBe(true));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockGetOne).toBeCalledTimes(1);
|
||||
expect(mockGetOne).toBeCalledWith(
|
||||
expect.objectContaining({
|
||||
meta: expect.objectContaining({
|
||||
"modal-posts-edit": undefined,
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
325
packages/mantine/src/hooks/form/useModalForm/index.ts
Normal file
325
packages/mantine/src/hooks/form/useModalForm/index.ts
Normal file
@@ -0,0 +1,325 @@
|
||||
import { useCallback } from "react";
|
||||
import {
|
||||
type BaseKey,
|
||||
type BaseRecord,
|
||||
type FormWithSyncWithLocationParams,
|
||||
type HttpError,
|
||||
useGo,
|
||||
useModal,
|
||||
useParsed,
|
||||
useResource,
|
||||
useUserFriendlyName,
|
||||
useTranslate,
|
||||
useWarnAboutChange,
|
||||
useInvalidate,
|
||||
} from "@refinedev/core";
|
||||
|
||||
import { useForm, type UseFormProps, type UseFormReturnType } from "../useForm";
|
||||
import type { UseFormInput } from "@mantine/form/lib/types";
|
||||
import React from "react";
|
||||
|
||||
export type UseModalFormReturnType<
|
||||
TQueryFnData extends BaseRecord = BaseRecord,
|
||||
TError extends HttpError = HttpError,
|
||||
TVariables = Record<string, unknown>,
|
||||
TTransformed = TVariables,
|
||||
TData extends BaseRecord = TQueryFnData,
|
||||
TResponse extends BaseRecord = TData,
|
||||
TResponseError extends HttpError = TError,
|
||||
> = UseFormReturnType<
|
||||
TQueryFnData,
|
||||
TError,
|
||||
TVariables,
|
||||
TTransformed,
|
||||
TData,
|
||||
TResponse,
|
||||
TResponseError
|
||||
> & {
|
||||
modal: {
|
||||
submit: (
|
||||
values: ReturnType<
|
||||
NonNullable<
|
||||
UseFormInput<
|
||||
TVariables,
|
||||
(values: TVariables) => TTransformed
|
||||
>["transformValues"]
|
||||
>
|
||||
>,
|
||||
) => void;
|
||||
close: () => void;
|
||||
show: (id?: BaseKey) => void;
|
||||
visible: boolean;
|
||||
title: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type UseModalFormProps<
|
||||
TQueryFnData extends BaseRecord = BaseRecord,
|
||||
TError extends HttpError = HttpError,
|
||||
TVariables = Record<string, unknown>,
|
||||
TTransformed = TVariables,
|
||||
TData extends BaseRecord = TQueryFnData,
|
||||
TResponse extends BaseRecord = TData,
|
||||
TResponseError extends HttpError = TError,
|
||||
> = UseFormProps<
|
||||
TQueryFnData,
|
||||
TError,
|
||||
TVariables,
|
||||
TTransformed,
|
||||
TData,
|
||||
TResponse,
|
||||
TResponseError
|
||||
> & {
|
||||
modalProps?: {
|
||||
defaultVisible?: boolean;
|
||||
autoSubmitClose?: boolean;
|
||||
autoResetForm?: boolean;
|
||||
};
|
||||
} & FormWithSyncWithLocationParams;
|
||||
|
||||
export const useModalForm = <
|
||||
TQueryFnData extends BaseRecord = BaseRecord,
|
||||
TError extends HttpError = HttpError,
|
||||
TVariables = Record<string, unknown>,
|
||||
TTransformed = TVariables,
|
||||
TData extends BaseRecord = TQueryFnData,
|
||||
TResponse extends BaseRecord = TData,
|
||||
TResponseError extends HttpError = TError,
|
||||
>({
|
||||
modalProps,
|
||||
refineCoreProps,
|
||||
syncWithLocation,
|
||||
...rest
|
||||
}: UseModalFormProps<
|
||||
TQueryFnData,
|
||||
TError,
|
||||
TVariables,
|
||||
TTransformed,
|
||||
TData,
|
||||
TResponse,
|
||||
TResponseError
|
||||
> = {}): UseModalFormReturnType<
|
||||
TQueryFnData,
|
||||
TError,
|
||||
TVariables,
|
||||
TTransformed,
|
||||
TData,
|
||||
TResponse,
|
||||
TResponseError
|
||||
> => {
|
||||
const [initiallySynced, setInitiallySynced] = React.useState(false);
|
||||
const invalidate = useInvalidate();
|
||||
|
||||
const translate = useTranslate();
|
||||
|
||||
const {
|
||||
resource: resourceProp,
|
||||
action: actionProp,
|
||||
autoSave,
|
||||
invalidates,
|
||||
dataProviderName,
|
||||
} = refineCoreProps ?? {};
|
||||
const {
|
||||
defaultVisible = false,
|
||||
autoSubmitClose = true,
|
||||
autoResetForm = true,
|
||||
} = modalProps ?? {};
|
||||
|
||||
const {
|
||||
resource,
|
||||
action: actionFromParams,
|
||||
identifier,
|
||||
} = useResource(resourceProp);
|
||||
|
||||
const parsed = useParsed();
|
||||
const go = useGo();
|
||||
const getUserFriendlyName = useUserFriendlyName();
|
||||
|
||||
const action = actionProp ?? actionFromParams ?? "";
|
||||
|
||||
const syncingId = !(
|
||||
typeof syncWithLocation === "object" && syncWithLocation?.syncId === false
|
||||
);
|
||||
|
||||
const syncWithLocationKey =
|
||||
typeof syncWithLocation === "object" && "key" in syncWithLocation
|
||||
? syncWithLocation.key
|
||||
: resource && action && syncWithLocation
|
||||
? `modal-${identifier}-${action}`
|
||||
: undefined;
|
||||
|
||||
const useMantineFormResult = useForm<
|
||||
TQueryFnData,
|
||||
TError,
|
||||
TVariables,
|
||||
TTransformed,
|
||||
TData,
|
||||
TResponse,
|
||||
TResponseError
|
||||
>({
|
||||
refineCoreProps: {
|
||||
...refineCoreProps,
|
||||
meta: {
|
||||
...(syncWithLocationKey ? { [syncWithLocationKey]: undefined } : {}),
|
||||
...refineCoreProps?.meta,
|
||||
},
|
||||
},
|
||||
...rest,
|
||||
});
|
||||
|
||||
const {
|
||||
reset,
|
||||
refineCore: { onFinish, id, setId, autoSaveProps },
|
||||
saveButtonProps,
|
||||
onSubmit,
|
||||
} = useMantineFormResult;
|
||||
|
||||
const { visible, show, close } = useModal({
|
||||
defaultVisible,
|
||||
});
|
||||
|
||||
React.useEffect(() => {
|
||||
if (initiallySynced === false && syncWithLocationKey) {
|
||||
const openStatus = parsed?.params?.[syncWithLocationKey]?.open;
|
||||
if (typeof openStatus === "boolean") {
|
||||
if (openStatus) {
|
||||
show();
|
||||
}
|
||||
} else if (typeof openStatus === "string") {
|
||||
if (openStatus === "true") {
|
||||
show();
|
||||
}
|
||||
}
|
||||
|
||||
if (syncingId) {
|
||||
const idFromParams = parsed?.params?.[syncWithLocationKey]?.id;
|
||||
if (idFromParams) {
|
||||
setId?.(idFromParams);
|
||||
}
|
||||
}
|
||||
|
||||
setInitiallySynced(true);
|
||||
}
|
||||
}, [syncWithLocationKey, parsed, syncingId, setId]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (initiallySynced === true) {
|
||||
if (visible && syncWithLocationKey) {
|
||||
go({
|
||||
query: {
|
||||
[syncWithLocationKey]: {
|
||||
...parsed?.params?.[syncWithLocationKey],
|
||||
open: true,
|
||||
...(syncingId && id && { id }),
|
||||
},
|
||||
},
|
||||
options: { keepQuery: true },
|
||||
type: "replace",
|
||||
});
|
||||
} else if (syncWithLocationKey && !visible) {
|
||||
go({
|
||||
query: {
|
||||
[syncWithLocationKey]: undefined,
|
||||
},
|
||||
options: { keepQuery: true },
|
||||
type: "replace",
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [id, visible, show, syncWithLocationKey, syncingId]);
|
||||
|
||||
const submit = async (
|
||||
values: ReturnType<
|
||||
NonNullable<
|
||||
UseFormInput<
|
||||
TVariables,
|
||||
(values: TVariables) => TTransformed
|
||||
>["transformValues"]
|
||||
>
|
||||
>,
|
||||
) => {
|
||||
await onFinish(values);
|
||||
|
||||
if (autoSubmitClose) {
|
||||
close();
|
||||
}
|
||||
|
||||
if (autoResetForm) {
|
||||
reset();
|
||||
}
|
||||
};
|
||||
|
||||
const { warnWhen, setWarnWhen } = useWarnAboutChange();
|
||||
const handleClose = useCallback(() => {
|
||||
if (autoSaveProps.status === "success" && autoSave?.invalidateOnClose) {
|
||||
invalidate({
|
||||
id,
|
||||
invalidates: invalidates || ["list", "many", "detail"],
|
||||
dataProviderName,
|
||||
resource: identifier,
|
||||
});
|
||||
}
|
||||
|
||||
if (warnWhen) {
|
||||
const warnWhenConfirm = window.confirm(
|
||||
translate(
|
||||
"warnWhenUnsavedChanges",
|
||||
"Are you sure you want to leave? You have unsaved changes.",
|
||||
),
|
||||
);
|
||||
|
||||
if (warnWhenConfirm) {
|
||||
setWarnWhen(false);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
setId?.(undefined);
|
||||
close();
|
||||
}, [warnWhen, autoSaveProps.status]);
|
||||
|
||||
const handleShow = useCallback(
|
||||
(showId?: BaseKey) => {
|
||||
if (typeof showId !== "undefined") {
|
||||
setId?.(showId);
|
||||
}
|
||||
const needsIdToOpen = action === "edit" || action === "clone";
|
||||
const hasId = typeof showId !== "undefined" || typeof id !== "undefined";
|
||||
if (needsIdToOpen ? hasId : true) {
|
||||
show();
|
||||
}
|
||||
},
|
||||
[id],
|
||||
);
|
||||
|
||||
const title = translate(
|
||||
`${identifier}.titles.${actionProp}`,
|
||||
undefined,
|
||||
`${getUserFriendlyName(
|
||||
`${actionProp} ${
|
||||
resource?.meta?.label ??
|
||||
resource?.options?.label ??
|
||||
resource?.label ??
|
||||
identifier
|
||||
}`,
|
||||
"singular",
|
||||
)}`,
|
||||
);
|
||||
|
||||
return {
|
||||
modal: {
|
||||
submit,
|
||||
close: handleClose,
|
||||
show: handleShow,
|
||||
visible,
|
||||
title,
|
||||
},
|
||||
...useMantineFormResult,
|
||||
saveButtonProps: {
|
||||
...saveButtonProps,
|
||||
// @ts-expect-error event type is not compatible with pointer event
|
||||
onClick: (e) => onSubmit(submit)(e),
|
||||
},
|
||||
};
|
||||
};
|
||||
141
packages/mantine/src/hooks/form/useStepsForm/index.ts
Normal file
141
packages/mantine/src/hooks/form/useStepsForm/index.ts
Normal file
@@ -0,0 +1,141 @@
|
||||
import { useState } from "react";
|
||||
import type { BaseRecord, HttpError } from "@refinedev/core";
|
||||
|
||||
import { useForm, type UseFormProps, type UseFormReturnType } from "../useForm";
|
||||
|
||||
export type UseStepsFormReturnType<
|
||||
TQueryFnData extends BaseRecord = BaseRecord,
|
||||
TError extends HttpError = HttpError,
|
||||
TVariables = Record<string, unknown>,
|
||||
TTransformed = TVariables,
|
||||
TData extends BaseRecord = TQueryFnData,
|
||||
TResponse extends BaseRecord = TData,
|
||||
TResponseError extends HttpError = TError,
|
||||
> = UseFormReturnType<
|
||||
TQueryFnData,
|
||||
TError,
|
||||
TVariables,
|
||||
TTransformed,
|
||||
TData,
|
||||
TResponse,
|
||||
TResponseError
|
||||
> & {
|
||||
steps: {
|
||||
currentStep: number;
|
||||
gotoStep: (step: number) => void;
|
||||
};
|
||||
};
|
||||
|
||||
export type UseStepsFormProps<
|
||||
TQueryFnData extends BaseRecord = BaseRecord,
|
||||
TError extends HttpError = HttpError,
|
||||
TVariables = Record<string, unknown>,
|
||||
TTransformed = TVariables,
|
||||
TData extends BaseRecord = TQueryFnData,
|
||||
TResponse extends BaseRecord = TData,
|
||||
TResponseError extends HttpError = TError,
|
||||
> = UseFormProps<
|
||||
TQueryFnData,
|
||||
TError,
|
||||
TVariables,
|
||||
TTransformed,
|
||||
TData,
|
||||
TResponse,
|
||||
TResponseError
|
||||
> & {
|
||||
/**
|
||||
* @description Configuration object for the steps.
|
||||
* `defaultStep`: Allows you to set the initial step.
|
||||
*
|
||||
* `isBackValidate`: Whether to validation the current step when going back.
|
||||
* @type `{
|
||||
defaultStep?: number;
|
||||
isBackValidate?: boolean;
|
||||
}`
|
||||
* @default `defaultStep = 0` `isBackValidate = false`
|
||||
*/
|
||||
stepsProps?: {
|
||||
defaultStep?: number;
|
||||
isBackValidate?: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
export const useStepsForm = <
|
||||
TQueryFnData extends BaseRecord = BaseRecord,
|
||||
TError extends HttpError = HttpError,
|
||||
TVariables = Record<string, unknown>,
|
||||
TTransformed = TVariables,
|
||||
TData extends BaseRecord = TQueryFnData,
|
||||
TResponse extends BaseRecord = TData,
|
||||
TResponseError extends HttpError = TError,
|
||||
>({
|
||||
stepsProps,
|
||||
...rest
|
||||
}: UseStepsFormProps<
|
||||
TQueryFnData,
|
||||
TError,
|
||||
TVariables,
|
||||
TTransformed,
|
||||
TData,
|
||||
TResponse,
|
||||
TResponseError
|
||||
> = {}): UseStepsFormReturnType<
|
||||
TQueryFnData,
|
||||
TError,
|
||||
TVariables,
|
||||
TTransformed,
|
||||
TData,
|
||||
TResponse,
|
||||
TResponseError
|
||||
> => {
|
||||
const { defaultStep = 0, isBackValidate = false } = stepsProps ?? {};
|
||||
const [current, setCurrent] = useState(defaultStep);
|
||||
|
||||
const useMantineFormResult = useForm<
|
||||
TQueryFnData,
|
||||
TError,
|
||||
TVariables,
|
||||
TTransformed,
|
||||
TData,
|
||||
TResponse,
|
||||
TResponseError
|
||||
>({
|
||||
...rest,
|
||||
});
|
||||
|
||||
const { validate } = useMantineFormResult;
|
||||
|
||||
const go = (step: number) => {
|
||||
let targetStep = step;
|
||||
|
||||
if (step < 0) {
|
||||
targetStep = 0;
|
||||
}
|
||||
|
||||
setCurrent(targetStep);
|
||||
};
|
||||
|
||||
const gotoStep = (step: number) => {
|
||||
if (step === current) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (step < current && !isBackValidate) {
|
||||
go(step);
|
||||
return;
|
||||
}
|
||||
|
||||
const isValid = !validate().hasErrors;
|
||||
if (isValid) {
|
||||
go(step);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
...useMantineFormResult,
|
||||
steps: {
|
||||
currentStep: current,
|
||||
gotoStep,
|
||||
},
|
||||
};
|
||||
};
|
||||
4
packages/mantine/src/hooks/index.ts
Normal file
4
packages/mantine/src/hooks/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export * from "./form";
|
||||
export * from "./useSelect";
|
||||
export * from "./useSiderVisible";
|
||||
export * from "./useThemedLayoutContext";
|
||||
78
packages/mantine/src/hooks/useSelect/index.ts
Normal file
78
packages/mantine/src/hooks/useSelect/index.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import type { SelectProps } from "@mantine/core";
|
||||
import type { QueryObserverResult } from "@tanstack/react-query";
|
||||
|
||||
import {
|
||||
useSelect as useSelectCore,
|
||||
type BaseRecord,
|
||||
type GetManyResponse,
|
||||
type GetListResponse,
|
||||
type HttpError,
|
||||
type UseSelectProps,
|
||||
type BaseOption,
|
||||
type Prettify,
|
||||
} from "@refinedev/core";
|
||||
|
||||
export type UseSelectReturnType<
|
||||
TData extends BaseRecord = BaseRecord,
|
||||
TOption extends BaseOption = BaseOption,
|
||||
> = {
|
||||
selectProps: Prettify<
|
||||
Omit<SelectProps, "data"> & {
|
||||
data: TOption[];
|
||||
}
|
||||
>;
|
||||
query: QueryObserverResult<GetListResponse<TData>>;
|
||||
defaultValueQuery: QueryObserverResult<GetManyResponse<TData>>;
|
||||
/**
|
||||
* @deprecated Use `query` instead
|
||||
*/
|
||||
queryResult: QueryObserverResult<GetListResponse<TData>>;
|
||||
/**
|
||||
* @deprecated Use `defaultValueQuery` instead
|
||||
*/
|
||||
defaultValueQueryResult: QueryObserverResult<GetManyResponse<TData>>;
|
||||
};
|
||||
|
||||
/**
|
||||
* `useSelect` hook is used to fetch data from the dataProvider and return the options for the select box.
|
||||
*
|
||||
* It uses `getList` method as query function from the dataProvider that is
|
||||
* passed to {@link https://refine.dev/docs/api-reference/core/components/refine-config `<Refine>`}.
|
||||
*
|
||||
* @see {@link https://refine.dev/docs/api-reference/mantine/hooks/useSelect} for more details.
|
||||
*
|
||||
* @typeParam TQueryFnData - Result data returned by the query function. Extends {@link https://refine.dev/docs/api-reference/core/interfaceReferences#baserecord `BaseRecord`}
|
||||
* @typeParam TError - Custom error object that extends {@link https://refine.dev/docs/api-reference/core/interfaceReferences#httperror `HttpError`}
|
||||
* @typeParam TData - Result data returned by the `select` function. Extends {@link https://refine.dev/docs/api-reference/core/interfaceReferences#baserecord `BaseRecord`}. Defaults to `TQueryFnData`
|
||||
*
|
||||
*/
|
||||
|
||||
export const useSelect = <
|
||||
TQueryFnData extends BaseRecord = BaseRecord,
|
||||
TError extends HttpError = HttpError,
|
||||
TData extends BaseRecord = TQueryFnData,
|
||||
TOption extends BaseOption = BaseOption,
|
||||
>(
|
||||
props: UseSelectProps<TQueryFnData, TError, TData>,
|
||||
): UseSelectReturnType<TData, TOption> => {
|
||||
const { query, defaultValueQuery, onSearch, options } = useSelectCore<
|
||||
TQueryFnData,
|
||||
TError,
|
||||
TData,
|
||||
TOption
|
||||
>(props);
|
||||
|
||||
return {
|
||||
selectProps: {
|
||||
data: options,
|
||||
onSearchChange: onSearch,
|
||||
searchable: true,
|
||||
filterDataOnExactSearchMatch: true,
|
||||
clearable: true,
|
||||
},
|
||||
query,
|
||||
defaultValueQuery,
|
||||
queryResult: query,
|
||||
defaultValueQueryResult: defaultValueQuery,
|
||||
};
|
||||
};
|
||||
29
packages/mantine/src/hooks/useSiderVisible/index.ts
Normal file
29
packages/mantine/src/hooks/useSiderVisible/index.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { useContext } from "react";
|
||||
|
||||
import { ThemedLayoutContext } from "@contexts";
|
||||
|
||||
export type UseSiderVisibleType = {
|
||||
siderVisible: boolean;
|
||||
drawerSiderVisible: boolean;
|
||||
setSiderVisible: (visible: boolean) => void;
|
||||
setDrawerSiderVisible: (visible: boolean) => void;
|
||||
};
|
||||
|
||||
/**
|
||||
* @deprecated Please use `useThemedLayoutContext` instead.
|
||||
*/
|
||||
export const useSiderVisible = (): UseSiderVisibleType => {
|
||||
const {
|
||||
mobileSiderOpen,
|
||||
siderCollapsed,
|
||||
setMobileSiderOpen,
|
||||
setSiderCollapsed,
|
||||
} = useContext(ThemedLayoutContext);
|
||||
|
||||
return {
|
||||
siderVisible: mobileSiderOpen,
|
||||
setSiderVisible: setMobileSiderOpen,
|
||||
drawerSiderVisible: siderCollapsed,
|
||||
setDrawerSiderVisible: setSiderCollapsed,
|
||||
};
|
||||
};
|
||||
22
packages/mantine/src/hooks/useThemedLayoutContext/index.ts
Normal file
22
packages/mantine/src/hooks/useThemedLayoutContext/index.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { useContext } from "react";
|
||||
|
||||
import { ThemedLayoutContext } from "@contexts";
|
||||
import type { IThemedLayoutContext } from "@contexts/themedLayoutContext/IThemedLayoutContext";
|
||||
|
||||
export type UseThemedLayoutContextType = IThemedLayoutContext;
|
||||
|
||||
export const useThemedLayoutContext = (): UseThemedLayoutContextType => {
|
||||
const {
|
||||
mobileSiderOpen,
|
||||
siderCollapsed,
|
||||
setMobileSiderOpen,
|
||||
setSiderCollapsed,
|
||||
} = useContext(ThemedLayoutContext);
|
||||
|
||||
return {
|
||||
mobileSiderOpen,
|
||||
siderCollapsed,
|
||||
setMobileSiderOpen,
|
||||
setSiderCollapsed,
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user