pankod/refine

View on GitHub
packages/react-hook-form/src/useModalForm/index.ts

Summary

Maintainability
D
2 days
Test Coverage
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),
    },
  };
};