Remove isDevelopment flag and cleanup a code in Form component (#2912)

This commit is contained in:
Krzysztof Żuraw 2023-01-03 11:04:42 +01:00 committed by GitHub
parent 1c0076a0db
commit c60f6f870c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 243 additions and 245 deletions

View file

@ -1,36 +1,9 @@
import { SubmitPromise } from "@saleor/hooks/useForm";
import { isInDevelopment } from "@saleor/misc";
import React, { useEffect, useRef, useState } from "react";
import { useHistory } from "react-router";
import useRouter from "use-react-router";
import React from "react";
import ExitFormDialog from "./ExitFormDialog";
import { ExitFormDialogData } from "./types";
import useBeforeUnload from "./useBeforeUnload";
export interface ExitFormDialogData {
setIsDirty: (id: symbol, isDirty: boolean) => void;
setExitDialogSubmitRef: (id: symbol, submitFn: SubmitFn) => void;
setEnableExitDialog: (value: boolean) => void;
shouldBlockNavigation: () => boolean;
setIsSubmitting: (value: boolean) => void;
leave: () => void;
setIsSubmitDisabled: (value: boolean) => void;
}
export type SubmitFn = (dataOrEvent?: any) => SubmitPromise<any[]>;
export type FormId = symbol;
type FormsData = Record<FormId, FormData>;
export interface WithFormId {
formId: FormId;
}
interface FormData {
isDirty: boolean;
submitFn: SubmitFn | null;
}
import { useExitFormDialogProvider } from "./useExitFormDialogProvider";
// Do not use this context directly in components
// use useExitFormDialog hook instead
@ -44,195 +17,6 @@ export const ExitFormDialogContext = React.createContext<ExitFormDialogData>({
setIsSubmitDisabled: () => undefined,
});
const defaultValues = {
isDirty: false,
showDialog: false,
blockNav: true,
navAction: null,
enableExitDialog: false,
isSubmitting: false,
formsData: {},
};
export function useExitFormDialogProvider() {
const history = useHistory();
const { history: routerHistory } = useRouter();
const [showDialog, setShowDialog] = useState(defaultValues.showDialog);
const isSubmitDisabled = useRef(false);
const setIsSubmitDisabled = (status: boolean) => {
isSubmitDisabled.current = status;
};
const isSubmitting = useRef(defaultValues.isSubmitting);
const formsData = useRef<FormsData>({});
const blockNav = useRef(defaultValues.blockNav);
const navAction = useRef<typeof history.location>(defaultValues.navAction);
const enableExitDialog = useRef(defaultValues.enableExitDialog);
const currentLocation = useRef(history.location);
const setIsSubmitting = (value: boolean) => {
setEnableExitDialog(!value);
isSubmitting.current = value;
};
const setEnableExitDialog = (value: boolean) => {
// dialog should never be toggled to enabled during form submission
if (isSubmitting.current) {
return;
}
enableExitDialog.current = value;
};
const setDefaultFormsData = () => {
formsData.current = defaultValues.formsData;
};
const setCurrentLocation = (newLocation: typeof history.location) => {
currentLocation.current = newLocation;
};
const setFormData = (id: symbol, newData: Partial<FormData>) => {
const updatedFormData = { ...formsData.current[id], ...newData };
formsData.current = {
...formsData.current,
[id]: updatedFormData,
};
};
// Set either on generic form load or on every custom form data change
// but doesn't cause re-renders
const setSubmitRef = <T extends () => SubmitPromise<any[]>>(
id: symbol,
submitFn: T,
) => {
setFormData(id, { submitFn });
};
const setIsDirty = (id: symbol, value: boolean) => {
// in case of race conitions between forms and transitions
if (!formsData.current[id]) {
return;
}
setFormData(id, { isDirty: value });
if (value) {
setEnableExitDialog(true);
}
};
const setBlockNav = (value: boolean) => (blockNav.current = value);
const setDefaultNavAction = () =>
(navAction.current = defaultValues.navAction);
const setStateDefaultValues = () => {
setIsSubmitting(defaultValues.isSubmitting);
setDefaultFormsData();
setShowDialog(defaultValues.showDialog);
setBlockNav(defaultValues.blockNav);
setEnableExitDialog(defaultValues.enableExitDialog);
setDefaultNavAction();
};
const getFormsDataValuesArray = () =>
Object.getOwnPropertySymbols(formsData.current).map(
key => formsData.current[key],
);
const hasAnyFormsDirty = () =>
getFormsDataValuesArray().some(({ isDirty }) => isDirty);
const shouldBlockNav = () => {
if (!enableExitDialog.current || !hasAnyFormsDirty()) {
return false;
}
return blockNav.current;
};
const isOnlyQuerying = (transition: typeof history.location) =>
// We need to compare to current path and not window location
// so it works with browser back button as well
transition.pathname === currentLocation.current.pathname;
const handleNavigationBlock = () => {
const unblock = history.block(transition => {
// needs to be done before the shouldBlockNav condition
// so it doesnt trigger setting default values
if (isOnlyQuerying(transition)) {
// ransition type requires this function to return either
// false | void | string where string opens up the browser prompt
// hence we return null
return null;
}
if (shouldBlockNav()) {
navAction.current = transition;
setShowDialog(true);
return false;
}
setStateDefaultValues();
setCurrentLocation(transition);
return null;
});
return unblock;
};
useEffect(handleNavigationBlock, []);
const continueNavigation = () => {
setBlockNav(false);
setDefaultFormsData();
setCurrentLocation(navAction.current);
// because our useNavigator navigate action may be blocked
// by exit dialog we want to avoid using it doing this transition
if (navAction.current !== null) {
routerHistory.push(navAction.current.pathname + navAction.current.search);
}
setStateDefaultValues();
};
const handleLeave = () => {
continueNavigation();
};
const handleClose = () => {
setDefaultNavAction();
setShowDialog(false);
};
// Used to prevent race conditions from places such as
// create pages with navigation on mutation completed
const shouldBlockNavigation = () => !!navAction.current;
const providerData: ExitFormDialogData = {
setIsDirty,
shouldBlockNavigation,
setEnableExitDialog,
setExitDialogSubmitRef: setSubmitRef,
setIsSubmitting,
setIsSubmitDisabled,
leave: handleLeave,
};
return {
providerData,
showDialog,
handleLeave,
handleClose,
shouldBlockNav,
isSubmitDisabled,
};
}
const ExitFormDialogProvider = ({ children }) => {
const {
handleClose,
@ -245,7 +29,7 @@ const ExitFormDialogProvider = ({ children }) => {
useBeforeUnload(e => {
// If form is dirty and user does a refresh,
// the browser will ask about unsaved changes
if (shouldBlockNav() && !isInDevelopment) {
if (shouldBlockNav()) {
e.preventDefault();
e.returnValue = "";
}

View file

@ -1,7 +1,7 @@
import useForm, { SubmitPromise, UseFormResult } from "@saleor/hooks/useForm";
import React from "react";
import { FormId } from "./ExitFormDialogProvider";
import { FormId } from "./types";
export type CheckIfSaveIsDisabledFnType<T> = (data: T) => boolean;

View file

@ -1,2 +1,4 @@
export * from "./Form";
export { default } from "./Form";
export * from "./types";
export * from "./useExitFormDialog";

View file

@ -0,0 +1,27 @@
import { SubmitPromise } from "@saleor/hooks/useForm";
import React from "react";
export type SubmitFn = (event?: React.FormEvent) => SubmitPromise;
export type FormId = symbol;
export type FormsData = Record<FormId, FormData>;
export interface WithFormId {
formId: FormId;
}
export interface ExitFormDialogData {
setIsDirty: (id: symbol, isDirty: boolean) => void;
setExitDialogSubmitRef: (id: symbol, submitFn: SubmitFn) => void;
setEnableExitDialog: (value: boolean) => void;
shouldBlockNavigation: () => boolean;
setIsSubmitting: (value: boolean) => void;
leave: () => void;
setIsSubmitDisabled: (value: boolean) => void;
}
export interface FormData {
isDirty: boolean;
submitFn: SubmitFn | null;
}

View file

@ -1,6 +1,6 @@
import { useEffect, useRef } from "react";
const useBeforeUnload = fn => {
const useBeforeUnload = (fn: (event: BeforeUnloadEvent) => void) => {
const cb = useRef(fn);
useEffect(() => {
@ -8,7 +8,7 @@ const useBeforeUnload = fn => {
}, [fn]);
useEffect(() => {
const onBeforeUnload = (...args) => cb.current?.(...args);
const onBeforeUnload = (event: BeforeUnloadEvent) => cb.current?.(event);
window.addEventListener("beforeunload", onBeforeUnload);

View file

@ -4,11 +4,9 @@ import React from "react";
import { useHistory } from "react-router";
import { MemoryRouter } from "react-router-dom";
import {
ExitFormDialogContext,
useExitFormDialogProvider,
} from "./ExitFormDialogProvider";
import { ExitFormDialogContext } from "./ExitFormDialogProvider";
import { useExitFormDialog } from "./useExitFormDialog";
import { useExitFormDialogProvider } from "./useExitFormDialogProvider";
jest.mock("../../hooks/useNotifier", () => undefined);

View file

@ -1,11 +1,7 @@
import React, { useContext, useRef } from "react";
import {
ExitFormDialogContext,
ExitFormDialogData,
SubmitFn,
WithFormId,
} from "./ExitFormDialogProvider";
import { ExitFormDialogContext } from "./ExitFormDialogProvider";
import { ExitFormDialogData, SubmitFn, WithFormId } from "./types";
export interface UseExitFormDialogResult
extends Omit<ExitFormDialogData, "setIsDirty" | "setExitDialogSubmitRef">,

View file

@ -0,0 +1,196 @@
import { SubmitPromise } from "@saleor/hooks/useForm";
import { useEffect, useRef, useState } from "react";
import { useHistory } from "react-router";
import useRouter from "use-react-router";
import { ExitFormDialogData, FormData, FormsData } from "./types";
const defaultValues = {
isDirty: false,
showDialog: false,
blockNav: true,
navAction: null,
enableExitDialog: false,
isSubmitting: false,
formsData: {},
};
export function useExitFormDialogProvider() {
const history = useHistory();
const { history: routerHistory } = useRouter();
const [showDialog, setShowDialog] = useState(defaultValues.showDialog);
const isSubmitDisabled = useRef(false);
const setIsSubmitDisabled = (status: boolean) => {
isSubmitDisabled.current = status;
};
const isSubmitting = useRef(defaultValues.isSubmitting);
const formsData = useRef<FormsData>({});
const blockNav = useRef(defaultValues.blockNav);
const navAction = useRef<typeof history.location>(defaultValues.navAction);
const enableExitDialog = useRef(defaultValues.enableExitDialog);
const currentLocation = useRef(history.location);
const setIsSubmitting = (value: boolean) => {
setEnableExitDialog(!value);
isSubmitting.current = value;
};
const setEnableExitDialog = (value: boolean) => {
// dialog should never be toggled to enabled during form submission
if (isSubmitting.current) {
return;
}
enableExitDialog.current = value;
};
const setDefaultFormsData = () => {
formsData.current = defaultValues.formsData;
};
const setCurrentLocation = (newLocation: typeof history.location) => {
currentLocation.current = newLocation;
};
const setFormData = (id: symbol, newData: Partial<FormData>) => {
const updatedFormData = { ...formsData.current[id], ...newData };
formsData.current = {
...formsData.current,
[id]: updatedFormData,
};
};
// Set either on generic form load or on every custom form data change
// but doesn't cause re-renders
const setSubmitRef = <T extends () => SubmitPromise<any[]>>(
id: symbol,
submitFn: T,
) => {
setFormData(id, { submitFn });
};
const setIsDirty = (id: symbol, value: boolean) => {
// in case of race conitions between forms and transitions
if (!formsData.current[id]) {
return;
}
setFormData(id, { isDirty: value });
if (value) {
setEnableExitDialog(true);
}
};
const setBlockNav = (value: boolean) => (blockNav.current = value);
const setDefaultNavAction = () =>
(navAction.current = defaultValues.navAction);
const setStateDefaultValues = () => {
setIsSubmitting(defaultValues.isSubmitting);
setDefaultFormsData();
setShowDialog(defaultValues.showDialog);
setBlockNav(defaultValues.blockNav);
setEnableExitDialog(defaultValues.enableExitDialog);
setDefaultNavAction();
};
const getFormsDataValuesArray = () =>
Object.getOwnPropertySymbols(formsData.current).map(
key => formsData.current[key],
);
const hasAnyFormsDirty = () =>
getFormsDataValuesArray().some(({ isDirty }) => isDirty);
const shouldBlockNav = () => {
if (!enableExitDialog.current || !hasAnyFormsDirty()) {
return false;
}
return blockNav.current;
};
const isOnlyQuerying = (transition: typeof history.location) =>
// We need to compare to current path and not window location
// so it works with browser back button as well
transition.pathname === currentLocation.current.pathname;
const handleNavigationBlock = () => {
// This callback blocks only navigation between internal dashboard pages
// https://github.com/remix-run/history/blob/main/docs/blocking-transitions.md#caveats
const unblock = history.block(transition => {
// needs to be done before the shouldBlockNav condition
// so it doesn't trigger setting default values
if (isOnlyQuerying(transition)) {
// transition type requires this function to return either
// false | void | string where string opens up the browser prompt
// hence we return null
return null;
}
if (shouldBlockNav()) {
navAction.current = transition;
setShowDialog(true);
return false;
}
setStateDefaultValues();
setCurrentLocation(transition);
return null;
});
return unblock;
};
useEffect(handleNavigationBlock, []);
const continueNavigation = () => {
setBlockNav(false);
setDefaultFormsData();
setCurrentLocation(navAction.current);
// because our useNavigator navigate action may be blocked
// by exit dialog we want to avoid using it doing this transition
if (navAction.current !== null) {
routerHistory.push(navAction.current.pathname + navAction.current.search);
}
setStateDefaultValues();
};
const handleLeave = () => {
continueNavigation();
};
const handleClose = () => {
setDefaultNavAction();
setShowDialog(false);
};
// Used to prevent race conditions from places such as
// create pages with navigation on mutation completed
const shouldBlockNavigation = () => !!navAction.current;
const providerData: ExitFormDialogData = {
setIsDirty,
shouldBlockNavigation,
setEnableExitDialog,
setExitDialogSubmitRef: setSubmitRef,
setIsSubmitting,
setIsSubmitDisabled,
leave: handleLeave,
};
return {
providerData,
showDialog,
handleLeave,
handleClose,
shouldBlockNav,
isSubmitDisabled,
};
}

View file

@ -1,7 +1,6 @@
import { ChannelsAction } from "@saleor/channels/urls";
import { Channel } from "@saleor/channels/utils";
import { WithFormId } from "@saleor/components/Form/ExitFormDialogProvider";
import { useExitFormDialog } from "@saleor/components/Form/useExitFormDialog";
import { useExitFormDialog, WithFormId } from "@saleor/components/Form";
import useListActions from "@saleor/hooks/useListActions";
import useStateFromProps from "@saleor/hooks/useStateFromProps";

View file

@ -1,9 +1,9 @@
import { CheckIfSaveIsDisabledFnType } from "@saleor/components/Form";
import { FormId } from "@saleor/components/Form/ExitFormDialogProvider";
import {
CheckIfSaveIsDisabledFnType,
FormId,
useExitFormDialog,
UseExitFormDialogResult,
} from "@saleor/components/Form/useExitFormDialog";
} from "@saleor/components/Form";
import useHandleFormSubmit from "@saleor/hooks/useHandleFormSubmit";
import { toggle } from "@saleor/utils/lists";
import isEqual from "lodash/isEqual";

View file

@ -1,5 +1,4 @@
import { FormId } from "@saleor/components/Form/ExitFormDialogProvider";
import { useExitFormDialog } from "@saleor/components/Form/useExitFormDialog";
import { FormId, useExitFormDialog } from "@saleor/components/Form";
import { MessageContext } from "@saleor/components/messages";
import { SubmitPromise } from "@saleor/hooks/useForm";
import { useContext } from "react";

View file

@ -536,9 +536,6 @@ export const combinedMultiAutocompleteChoices = (
choices: MultiAutocompleteChoiceType[],
) => uniqBy([...selected, ...choices], "value");
export const isInDevelopment =
!process.env.NODE_ENV || process.env.NODE_ENV === "development";
export type WithOptional<T, K extends keyof T> = Omit<T, K> &
Partial<Pick<T, K>>;

View file

@ -3,7 +3,7 @@ import { Backlink } from "@saleor/components/Backlink";
import CardSpacer from "@saleor/components/CardSpacer";
import ChannelsAvailabilityCard from "@saleor/components/ChannelsAvailabilityCard";
import Container from "@saleor/components/Container";
import { WithFormId } from "@saleor/components/Form/ExitFormDialogProvider";
import { WithFormId } from "@saleor/components/Form";
import Grid from "@saleor/components/Grid";
import PageHeader from "@saleor/components/PageHeader";
import Savebar from "@saleor/components/Savebar";

View file

@ -3,7 +3,7 @@ import { Backlink } from "@saleor/components/Backlink";
import CardSpacer from "@saleor/components/CardSpacer";
import ChannelsAvailabilityCard from "@saleor/components/ChannelsAvailabilityCard";
import Container from "@saleor/components/Container";
import { WithFormId } from "@saleor/components/Form/ExitFormDialogProvider";
import { WithFormId } from "@saleor/components/Form";
import Grid from "@saleor/components/Grid";
import Metadata from "@saleor/components/Metadata/Metadata";
import PageHeader from "@saleor/components/PageHeader";