packages/react-hook-form/src/useModalForm/index.ts
import { useCallback } from "react";
import {
BaseKey,
BaseRecord,
FormWithSyncWithLocationParams,
HttpError,
useGo,
useModal,
useParsed,
useResource,
useUserFriendlyName,
useTranslate,
useWarnAboutChange,
useInvalidate,
} from "@refinedev/core";
import { FieldValues } from "react-hook-form";
import { useForm, UseFormProps, UseFormReturnType } from "../useForm";
import React from "react";
export type UseModalFormReturnType<
TQueryFnData extends BaseRecord = BaseRecord,
TError extends HttpError = HttpError,
TVariables extends FieldValues = FieldValues,
TContext extends object = {},
TData extends BaseRecord = TQueryFnData,
TResponse extends BaseRecord = TData,
TResponseError extends HttpError = TError,
> = UseFormReturnType<
TQueryFnData,
TError,
TVariables,
TContext,
TData,
TResponse,
TResponseError
> & {
modal: {
submit: (values: TVariables) => void;
close: () => void;
show: (id?: BaseKey) => void;
visible: boolean;
title: string;
};
};
export type UseModalFormProps<
TQueryFnData extends BaseRecord = BaseRecord,
TError extends HttpError = HttpError,
TVariables extends FieldValues = FieldValues,
TContext extends object = {},
TData extends BaseRecord = TQueryFnData,
TResponse extends BaseRecord = TData,
TResponseError extends HttpError = TError,
> = UseFormProps<
TQueryFnData,
TError,
TVariables,
TContext,
TData,
TResponse,
TResponseError
> & {
/**
* @description Configuration object for the modal.
* `defaultVisible`: Initial visibility state of the modal.
*
* `autoSubmitClose`: Whether the form should be submitted when the modal is closed.
*
* `autoResetForm`: Whether the form should be reset when the form is submitted.
* @type `{
defaultVisible?: boolean;
autoSubmitClose?: boolean;
autoResetForm?: boolean;
}`
* @default `defaultVisible = false` `autoSubmitClose = true` `autoResetForm = true`
*/
modalProps?: {
defaultVisible?: boolean;
autoSubmitClose?: boolean;
autoResetForm?: boolean;
};
} & FormWithSyncWithLocationParams;
export const useModalForm = <
TQueryFnData extends BaseRecord = BaseRecord,
TError extends HttpError = HttpError,
TVariables extends FieldValues = FieldValues,
TContext extends object = {},
TData extends BaseRecord = TQueryFnData,
TResponse extends BaseRecord = TData,
TResponseError extends HttpError = TError,
>({
modalProps,
refineCoreProps,
syncWithLocation,
...rest
}: UseModalFormProps<
TQueryFnData,
TError,
TVariables,
TContext,
TData,
TResponse,
TResponseError
> = {}): UseModalFormReturnType<
TQueryFnData,
TError,
TVariables,
TContext,
TData,
TResponse,
TResponseError
> => {
const invalidate = useInvalidate();
const [initiallySynced, setInitiallySynced] = React.useState(false);
const translate = useTranslate();
const { resource: resourceProp, action: actionProp } = refineCoreProps ?? {};
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 {
defaultVisible = false,
autoSubmitClose = true,
autoResetForm = true,
} = modalProps ?? {};
const useHookFormResult = useForm<
TQueryFnData,
TError,
TVariables,
TContext,
TData,
TResponse,
TResponseError
>({
refineCoreProps: {
...refineCoreProps,
meta: {
...(syncWithLocationKey ? { [syncWithLocationKey]: undefined } : {}),
...refineCoreProps?.meta,
},
},
...rest,
});
const {
reset,
refineCore: { onFinish, id, setId, autoSaveProps },
saveButtonProps,
handleSubmit,
} = useHookFormResult;
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: TVariables) => {
await onFinish(values);
if (autoSubmitClose) {
close();
}
if (autoResetForm) {
reset();
}
};
const { warnWhen, setWarnWhen } = useWarnAboutChange();
const handleClose = useCallback(() => {
if (
autoSaveProps.status === "success" &&
refineCoreProps?.autoSave?.invalidateOnClose
) {
invalidate({
id,
invalidates: refineCoreProps.invalidates || ["list", "many", "detail"],
dataProviderName: refineCoreProps.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,
},
...useHookFormResult,
saveButtonProps: {
...saveButtonProps,
onClick: (e) => handleSubmit(submit)(e),
},
};
};