From a5ac6bb92eb5a35e99fda6117874179223a5ee10 Mon Sep 17 00:00:00 2001 From: Wojciech Mista Date: Wed, 23 Mar 2022 10:13:23 +0100 Subject: [PATCH] Exit form fixes (#1889) * Add onBeforeUnload handler to prevent accidental refresh * Update button messages * Fix exit form not working after submit * Make onBeforeUnload disable if env is development * Fix onClose * Remove internal date time field state * Update messages and dialog * Prevent navigation on 400 error * Add submit disabled ref in exit form * Update exit form dialog for disabled save * Update confirmLeave forms to set ref if save is disabled * Remove unused error handling * Remove explicit ref type * Remove unused import * Fix disabled type * Add disable check function to generic forms * Add custom isDisabled method to sale and voucher forms * Add default isDisabled functions to confirmLeave forms * Update tests * Remove unused code * Rebase fixes + update tests * Refactor form and useform * Refactor disabling forms * Change "saveDisabled" name to "isSaveDisabled" for improved readability * Change "isDisabled" function to "checkIfSaveIsDisabled" * Update exit form disabling conditions for zone rates forms --- locale/defaultMessages.json | 16 +- .../CustomAppCreatePage.tsx | 11 +- .../CustomAppDetailsPage.tsx | 11 +- .../AttributePage/AttributePage.tsx | 11 +- .../CategoryCreatePage/CategoryCreatePage.tsx | 6 +- .../components/CategoryCreatePage/form.tsx | 18 +- .../CategoryUpdatePage/CategoryUpdatePage.tsx | 10 +- .../components/CategoryUpdatePage/form.tsx | 18 +- .../ChannelDetailsPage/ChannelDetailsPage.tsx | 31 +-- .../CollectionCreatePage.tsx | 5 +- .../components/CollectionCreatePage/form.tsx | 23 ++- .../CollectionDetailsPage.tsx | 6 +- .../components/CollectionDetailsPage/form.tsx | 23 ++- src/components/DateTimeField.tsx | 21 +- src/components/Form/ExitFormDialog.tsx | 34 ++-- .../Form/ExitFormDialogProvider.tsx | 35 +++- src/components/Form/Form.tsx | 17 +- src/components/Form/messages.ts | 17 +- src/components/Form/useBeforeUnload.ts | 19 ++ src/components/Form/useExitFormDialog.ts | 31 ++- .../CustomerCreatePage/CustomerCreatePage.tsx | 11 +- .../CustomerDetailsPage.tsx | 11 +- .../SaleCreatePage/SaleCreatePage.tsx | 15 +- .../SaleDetailsPage/SaleDetailsPage.tsx | 20 +- .../VoucherCreatePage/VoucherCreatePage.tsx | 28 +-- src/hooks/useForm.ts | 43 ++++- src/hooks/useHandleFormSubmit.ts | 4 +- src/misc.ts | 3 + .../OrderRefundPage/OrderRefundPage.tsx | 5 +- .../components/OrderRefundPage/form.tsx | 22 ++- .../OrderReturnPage/OrderReturnPage.tsx | 181 ++++++++---------- .../components/OrderReturnPage/form.tsx | 18 +- .../OrderSettingsPage/OrderSettingsPage.tsx | 5 +- .../components/OrderSettingsPage/form.tsx | 46 +++-- .../PageTypeCreatePage/PageTypeCreatePage.tsx | 11 +- .../PageTypeDetailsPage.tsx | 11 +- .../PageDetailsPage/PageDetailsPage.tsx | 5 +- src/pages/components/PageDetailsPage/form.tsx | 13 +- .../PermissionGroupCreatePage.tsx | 11 +- .../PluginsDetailsPage/PluginsDetailsPage.tsx | 5 +- .../ProductTypeCreatePage.tsx | 11 +- .../ProductTypeDetailsPage.tsx | 11 +- .../ProductCreatePage/ProductCreatePage.tsx | 13 +- .../components/ProductCreatePage/form.tsx | 23 ++- .../ProductUpdatePage/ProductUpdatePage.tsx | 16 +- .../components/ProductUpdatePage/form.tsx | 28 ++- .../ProductVariantCreatePage.tsx | 12 +- .../ProductVariantCreatePage/form.tsx | 24 ++- .../ProductVariantPage/ProductVariantPage.tsx | 13 +- .../components/ProductVariantPage/form.tsx | 14 +- src/products/views/ProductCreate/handlers.ts | 5 +- .../ShippingZoneCreatePage.tsx | 11 +- .../ShippingZoneDetailsPage.tsx | 11 +- .../ShippingZoneRatesCreatePage.tsx | 22 ++- .../ShippingZoneRatesPage.tsx | 24 ++- .../SiteSettingsPage/SiteSettingsPage.tsx | 5 +- .../StaffDetailsPage/StaffDetailsPage.tsx | 11 +- .../CountryListPage/CountryListPage.tsx | 11 +- .../WarehouseDetailsPage.tsx | 11 +- 59 files changed, 717 insertions(+), 390 deletions(-) create mode 100644 src/components/Form/useBeforeUnload.ts diff --git a/locale/defaultMessages.json b/locale/defaultMessages.json index b3da460fa..e9682467d 100644 --- a/locale/defaultMessages.json +++ b/locale/defaultMessages.json @@ -2231,19 +2231,23 @@ }, "src_dot_components_dot_Form_dot_cancelButton": { "context": "ExitFormPrompt cancel button", - "string": "leave without saving" + "string": "Discard changes" }, "src_dot_components_dot_Form_dot_confirmButton": { "context": "ExitFormPrompt confirm button", - "string": "save & continue" + "string": "Save changes" }, - "src_dot_components_dot_Form_dot_description": { - "context": "ExitFormPrompt description", - "string": "You have unsaved changes on this view. What would you like to do with them?" + "src_dot_components_dot_Form_dot_continueEditingButton": { + "context": "ExitFormPrompt continue editing button", + "string": "Continue editing" }, "src_dot_components_dot_Form_dot_title": { "context": "ExitFormPrompt title", - "string": "Are you sure you want to leave?" + "string": "Would you like to save changes?" + }, + "src_dot_components_dot_Form_dot_unableToSaveTitle": { + "context": "ExitFormPrompt title", + "string": "You have unsaved changes" }, "src_dot_components_dot_ImageUpload_dot_1731007575": { "context": "image upload", diff --git a/src/apps/components/CustomAppCreatePage/CustomAppCreatePage.tsx b/src/apps/components/CustomAppCreatePage/CustomAppCreatePage.tsx index 21efb7c12..a38c82346 100644 --- a/src/apps/components/CustomAppCreatePage/CustomAppCreatePage.tsx +++ b/src/apps/components/CustomAppCreatePage/CustomAppCreatePage.tsx @@ -56,8 +56,13 @@ const CustomAppCreatePage: React.FC = props => { const permissionsError = getAppErrorMessage(formErrors.permissions, intl); return ( -
- {({ data, change, hasChanged, submit }) => ( + + {({ data, change, submit, isSaveDisabled }) => ( {intl.formatMessage(sectionNames.apps)} @@ -96,7 +101,7 @@ const CustomAppCreatePage: React.FC = props => { /> = props => { }; return ( - - {({ data, change, hasChanged, submit }) => ( + + {({ data, change, submit, isSaveDisabled }) => ( {intl.formatMessage(sectionNames.apps)} @@ -182,7 +187,7 @@ const CustomAppDetailsPage: React.FC = props => { = ({ }; return ( - + {({ change, set, data, - hasChanged, + isSaveDisabled, submit, errors, setError, @@ -239,7 +244,7 @@ const AttributePage: React.FC = ({ = ({ const intl = useIntl(); return ( - - {({ data, change, handlers, submit, hasChanged }) => ( + + {({ data, change, handlers, submit, isSaveDisabled }) => ( {intl.formatMessage(sectionNames.categories)} @@ -74,7 +74,7 @@ export const CategoryCreatePage: React.FC = ({ onCancel={onBack} onSubmit={submit} state={saveButtonBarState} - disabled={disabled || !hasChanged} + disabled={isSaveDisabled} /> diff --git a/src/categories/components/CategoryCreatePage/form.tsx b/src/categories/components/CategoryCreatePage/form.tsx index 99783f0a1..17ebf4402 100644 --- a/src/categories/components/CategoryCreatePage/form.tsx +++ b/src/categories/components/CategoryCreatePage/form.tsx @@ -33,6 +33,7 @@ export interface UseCategoryCreateFormResult export interface CategoryCreateFormProps { children: (props: UseCategoryCreateFormResult) => React.ReactNode; onSubmit: (data: CategoryCreateData) => Promise; + disabled: boolean; } const initialData: CategoryCreateFormData = { @@ -45,7 +46,8 @@ const initialData: CategoryCreateFormData = { }; function useCategoryCreateForm( - onSubmit: (data: CategoryCreateData) => Promise + onSubmit: (data: CategoryCreateData) => Promise, + disabled: boolean ): UseCategoryCreateFormResult { const { handleChange, @@ -53,7 +55,8 @@ function useCategoryCreateForm( hasChanged, triggerChange, setChanged, - formId + formId, + setIsSubmitDisabled } = useForm(initialData, undefined, { confirmLeave: true }); const handleFormSubmit = useHandleFormSubmit({ @@ -87,6 +90,9 @@ function useCategoryCreateForm( useEffect(() => setExitDialogSubmitRef(submit), [submit]); + const isSaveDisabled = disabled || !hasChanged; + setIsSubmitDisabled(isSaveDisabled); + return { change: handleChange, data: getData(), @@ -95,15 +101,17 @@ function useCategoryCreateForm( changeMetadata }, hasChanged, - submit + submit, + isSaveDisabled }; } const CategoryCreateForm: React.FC = ({ children, - onSubmit + onSubmit, + disabled }) => { - const props = useCategoryCreateForm(onSubmit); + const props = useCategoryCreateForm(onSubmit, disabled); return {children(props)}; }; diff --git a/src/categories/components/CategoryUpdatePage/CategoryUpdatePage.tsx b/src/categories/components/CategoryUpdatePage/CategoryUpdatePage.tsx index 5f7a69f3b..acfda66f0 100644 --- a/src/categories/components/CategoryUpdatePage/CategoryUpdatePage.tsx +++ b/src/categories/components/CategoryUpdatePage/CategoryUpdatePage.tsx @@ -92,8 +92,12 @@ export const CategoryUpdatePage: React.FC = ({ const intl = useIntl(); return ( - - {({ data, change, handlers, submit, hasChanged }) => ( + + {({ data, change, handlers, submit, isSaveDisabled }) => ( {intl.formatMessage(sectionNames.categories)} @@ -217,7 +221,7 @@ export const CategoryUpdatePage: React.FC = ({ onDelete={onDelete} onSubmit={submit} state={saveButtonBarState} - disabled={disabled || !hasChanged} + disabled={isSaveDisabled} /> )} diff --git a/src/categories/components/CategoryUpdatePage/form.tsx b/src/categories/components/CategoryUpdatePage/form.tsx index 05faf1c0e..4bd0ad9af 100644 --- a/src/categories/components/CategoryUpdatePage/form.tsx +++ b/src/categories/components/CategoryUpdatePage/form.tsx @@ -38,6 +38,7 @@ export interface CategoryUpdateFormProps { children: (props: UseCategoryUpdateFormResult) => React.ReactNode; category: CategoryDetailsFragment; onSubmit: (data: CategoryUpdateData) => Promise; + disabled: boolean; } const getInitialData = (category?: CategoryDetailsFragment) => ({ @@ -52,7 +53,8 @@ const getInitialData = (category?: CategoryDetailsFragment) => ({ function useCategoryUpdateForm( category: CategoryDetailsFragment, - onSubmit: (data: CategoryUpdateData) => Promise + onSubmit: (data: CategoryUpdateData) => Promise, + disabled: boolean ): UseCategoryUpdateFormResult { const { handleChange, @@ -60,7 +62,8 @@ function useCategoryUpdateForm( triggerChange, hasChanged, setChanged, - formId + formId, + setIsSubmitDisabled } = useForm(getInitialData(category), undefined, { confirmLeave: true }); const handleFormSubmit = useHandleFormSubmit({ @@ -101,6 +104,9 @@ function useCategoryUpdateForm( useEffect(() => setExitDialogSubmitRef(submit), [submit]); + const isSaveDisabled = disabled || !hasChanged; + setIsSubmitDisabled(isSaveDisabled); + return { change: handleChange, data: getData(), @@ -109,16 +115,18 @@ function useCategoryUpdateForm( changeMetadata }, hasChanged, - submit + submit, + isSaveDisabled }; } const CategoryUpdateForm: React.FC = ({ children, category, - onSubmit + onSubmit, + disabled }) => { - const props = useCategoryUpdateForm(category, onSubmit); + const props = useCategoryUpdateForm(category, onSubmit, disabled); return
{children(props)}
; }; diff --git a/src/channels/pages/ChannelDetailsPage/ChannelDetailsPage.tsx b/src/channels/pages/ChannelDetailsPage/ChannelDetailsPage.tsx index 07ec39000..690e8c138 100644 --- a/src/channels/pages/ChannelDetailsPage/ChannelDetailsPage.tsx +++ b/src/channels/pages/ChannelDetailsPage/ChannelDetailsPage.tsx @@ -1,6 +1,6 @@ import ShippingZonesCard from "@saleor/channels/components/ShippingZonesCard/ShippingZonesCard"; import CardSpacer from "@saleor/components/CardSpacer"; -import Form from "@saleor/components/Form"; +import Form, { FormDataWithOpts } from "@saleor/components/Form"; import Grid from "@saleor/components/Grid"; import Savebar from "@saleor/components/Savebar"; import { SingleAutocompleteChoiceType } from "@saleor/components/SingleAutocompleteSelectField"; @@ -94,9 +94,25 @@ const ChannelDetailsPage = function({ !shippingZonesToDisplay.some(({ id }) => id === searchedZoneId) ); + const checkIfSaveIsDisabled = (data: FormDataWithOpts) => { + const formDisabled = + !data.name || + !data.slug || + !data.currencyCode || + !data.defaultCountry || + !(data.name.trim().length > 0); + + return disabled || formDisabled || !data.hasChanged; + }; + return ( -
- {({ change, data, hasChanged, submit, set }) => { + + {({ change, data, submit, set, isSaveDisabled }) => { const handleCurrencyCodeSelect = createSingleAutocompleteSelectHandler( change, setSelectedCurrencyCode, @@ -147,13 +163,6 @@ const ChannelDetailsPage = function({ ); }; - const formDisabled = - !data.name || - !data.slug || - !data.currencyCode || - !data.defaultCountry || - !(data.name.trim().length > 0); - return ( <> @@ -197,7 +206,7 @@ const ChannelDetailsPage = function({ onSubmit={submit} onDelete={onDelete} state={saveButtonBarState} - disabled={disabled || formDisabled || !onSubmit || !hasChanged} + disabled={isSaveDisabled} /> ); diff --git a/src/collections/components/CollectionCreatePage/CollectionCreatePage.tsx b/src/collections/components/CollectionCreatePage/CollectionCreatePage.tsx index c4e1f0ab7..71ee825a7 100644 --- a/src/collections/components/CollectionCreatePage/CollectionCreatePage.tsx +++ b/src/collections/components/CollectionCreatePage/CollectionCreatePage.tsx @@ -54,8 +54,9 @@ const CollectionCreatePage: React.FC = ({ onSubmit={onSubmit} currentChannels={currentChannels} setChannels={onChannelsChange} + disabled={disabled} > - {({ change, data, handlers, hasChanged, submit }) => ( + {({ change, data, handlers, submit, isSaveDisabled }) => ( {intl.formatMessage(sectionNames.collections)} @@ -156,7 +157,7 @@ const CollectionCreatePage: React.FC = ({ diff --git a/src/collections/components/CollectionCreatePage/form.tsx b/src/collections/components/CollectionCreatePage/form.tsx index fedcdf545..2dd44aeec 100644 --- a/src/collections/components/CollectionCreatePage/form.tsx +++ b/src/collections/components/CollectionCreatePage/form.tsx @@ -49,6 +49,7 @@ export interface CollectionCreateFormProps { setChannels: (data: ChannelCollectionData[]) => void; children: (props: UseCollectionCreateFormResult) => React.ReactNode; onSubmit: (data: CollectionCreateData) => SubmitPromise; + disabled: boolean; } const getInitialData = ( @@ -71,7 +72,8 @@ const getInitialData = ( function useCollectionCreateForm( currentChannels: ChannelCollectionData[], setChannels: (data: ChannelCollectionData[]) => void, - onSubmit: (data: CollectionCreateData) => SubmitPromise + onSubmit: (data: CollectionCreateData) => SubmitPromise, + disabled: boolean ): UseCollectionCreateFormResult { const { handleChange, @@ -79,7 +81,8 @@ function useCollectionCreateForm( triggerChange, setChanged, hasChanged, - formId + formId, + setIsSubmitDisabled } = useForm(getInitialData(currentChannels), undefined, { confirmLeave: true, formId: COLLECTION_CREATE_FORM_ID @@ -122,6 +125,9 @@ function useCollectionCreateForm( useEffect(() => setExitDialogSubmitRef(submit), [submit]); + const isSaveDisabled = disabled || !hasChanged; + setIsSubmitDisabled(isSaveDisabled); + return { change: handleChange, data: getData(), @@ -131,7 +137,8 @@ function useCollectionCreateForm( changeMetadata }, hasChanged, - submit + submit, + isSaveDisabled }; } @@ -139,9 +146,15 @@ const CollectionCreateForm: React.FC = ({ currentChannels, setChannels, children, - onSubmit + onSubmit, + disabled }) => { - const props = useCollectionCreateForm(currentChannels, setChannels, onSubmit); + const props = useCollectionCreateForm( + currentChannels, + setChannels, + onSubmit, + disabled + ); return {children(props)}; }; diff --git a/src/collections/components/CollectionDetailsPage/CollectionDetailsPage.tsx b/src/collections/components/CollectionDetailsPage/CollectionDetailsPage.tsx index e7a515a5e..52c4c7fcb 100644 --- a/src/collections/components/CollectionDetailsPage/CollectionDetailsPage.tsx +++ b/src/collections/components/CollectionDetailsPage/CollectionDetailsPage.tsx @@ -73,8 +73,10 @@ const CollectionDetailsPage: React.FC = ({ currentChannels={currentChannels} setChannels={onChannelsChange} onSubmit={onSubmit} + disabled={disabled} + hasChannelChanged={hasChannelChanged} > - {({ change, data, handlers, hasChanged, submit }) => ( + {({ change, data, handlers, submit, isSaveDisabled }) => ( {intl.formatMessage(sectionNames.collections)} @@ -150,7 +152,7 @@ const CollectionDetailsPage: React.FC = ({ void; onSubmit: (data: CollectionUpdateData) => Promise; + disabled: boolean; + hasChannelChanged: boolean; } const getInitialData = ( @@ -68,7 +70,9 @@ function useCollectionUpdateForm( collection: CollectionDetailsFragment, currentChannels: ChannelCollectionData[], setChannels: (data: ChannelCollectionData[]) => void, - onSubmit: (data: CollectionUpdateData) => Promise + onSubmit: (data: CollectionUpdateData) => Promise, + disabled: boolean, + hasChannelChanged: boolean ): UseCollectionUpdateFormResult { const { handleChange, @@ -76,7 +80,8 @@ function useCollectionUpdateForm( triggerChange, setChanged, hasChanged, - formId + formId, + setIsSubmitDisabled } = useForm(getInitialData(collection, currentChannels), undefined, { confirmLeave: true, formId: COLLECTION_DETAILS_FORM_ID @@ -126,6 +131,9 @@ function useCollectionUpdateForm( useEffect(() => setExitDialogSubmitRef(submit), [submit]); + const isSaveDisabled = disabled || (!hasChanged && !hasChannelChanged); + setIsSubmitDisabled(isSaveDisabled); + return { change: handleChange, data: getData(), @@ -135,7 +143,8 @@ function useCollectionUpdateForm( changeMetadata }, hasChanged, - submit + submit, + isSaveDisabled }; } @@ -144,13 +153,17 @@ const CollectionUpdateForm: React.FC = ({ currentChannels, setChannels, children, - onSubmit + onSubmit, + disabled, + hasChannelChanged }) => { const props = useCollectionUpdateForm( collection, currentChannels, setChannels, - onSubmit + onSubmit, + disabled, + hasChannelChanged ); return
{children(props)}
; diff --git a/src/components/DateTimeField.tsx b/src/components/DateTimeField.tsx index b8c091805..04a72d876 100644 --- a/src/components/DateTimeField.tsx +++ b/src/components/DateTimeField.tsx @@ -6,8 +6,8 @@ import { ProductErrorWithAttributesFragment } from "@saleor/graphql"; import { commonMessages } from "@saleor/intl"; -import { DateTime, joinDateTime, splitDateTime } from "@saleor/misc"; -import React, { useEffect, useState } from "react"; +import { joinDateTime, splitDateTime } from "@saleor/misc"; +import React from "react"; import { useIntl } from "react-intl"; type DateTimeFieldProps = Omit & { @@ -21,14 +21,11 @@ export const DateTimeField: React.FC = ({ error, name, onChange, - value: initialValue + value }) => { const intl = useIntl(); - const [value, setValue] = useState( - initialValue ? splitDateTime(initialValue) : { date: "", time: "" } - ); - useEffect(() => onChange(joinDateTime(value.date, value.time)), [value]); + const parsedValue = value ? splitDateTime(value) : { date: "", time: "" }; return ( <> @@ -41,10 +38,11 @@ export const DateTimeField: React.FC = ({ name={`${name}:date`} onChange={event => { const date = event.target.value; - setValue(value => ({ ...value, date })); + + onChange(joinDateTime(date, parsedValue.time)); }} type="date" - value={value.date} + value={parsedValue.date} InputLabelProps={{ shrink: true }} /> = ({ name={`${name}:time`} onChange={event => { const time = event.target.value; - setValue(value => ({ ...value, time })); + + onChange(joinDateTime(parsedValue.date, time)); }} type="time" - value={value.time} + value={parsedValue.time} InputLabelProps={{ shrink: true }} /> diff --git a/src/components/Form/ExitFormDialog.tsx b/src/components/Form/ExitFormDialog.tsx index 94f87232a..cdcf2ff4e 100644 --- a/src/components/Form/ExitFormDialog.tsx +++ b/src/components/Form/ExitFormDialog.tsx @@ -1,9 +1,8 @@ import { Button, Dialog, DialogContent, makeStyles } from "@material-ui/core"; import HorizontalSpacer from "@saleor/apps/components/HorizontalSpacer"; -import CardSpacer from "@saleor/components/CardSpacer"; import CardTitle from "@saleor/components/CardTitle"; import React from "react"; -import { FormattedMessage, useIntl } from "react-intl"; +import { useIntl } from "react-intl"; import { exitFormPromptMessages as messages } from "./messages"; @@ -19,6 +18,12 @@ const useStyles = makeStyles( buttonsContainer: { display: "flex", justifyContent: "flex-end" + }, + dialogContent: { + "@media (min-width: 800px)": { + minWidth: 500 + }, + paddingTop: 0 } }), { name: "ExitFormPrompt" } @@ -29,24 +34,27 @@ interface ExitFormDialogProps { onClose: () => void; onLeave: () => void; isOpen: boolean; + isSubmitDisabled: boolean; } const ExitFormDialog: React.FC = ({ onSubmit, onLeave, onClose, - isOpen + isOpen, + isSubmitDisabled }) => { const classes = useStyles(); const intl = useIntl(); return ( - - - - - - + + +
diff --git a/src/components/Form/ExitFormDialogProvider.tsx b/src/components/Form/ExitFormDialogProvider.tsx index 1f4eae227..82381187e 100644 --- a/src/components/Form/ExitFormDialogProvider.tsx +++ b/src/components/Form/ExitFormDialogProvider.tsx @@ -1,9 +1,11 @@ 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 ExitFormDialog from "./ExitFormDialog"; +import useBeforeUnload from "./useBeforeUnload"; export interface ExitFormDialogData { setIsDirty: (id: symbol, isDirty: boolean) => void; @@ -12,6 +14,7 @@ export interface ExitFormDialogData { shouldBlockNavigation: () => boolean; setIsSubmitting: (value: boolean) => void; submit: () => SubmitPromise; + setIsSubmitDisabled: (value: boolean) => void; } export type SubmitFn = (dataOrEvent?: any) => SubmitPromise; @@ -37,7 +40,8 @@ export const ExitFormDialogContext = React.createContext({ setExitDialogSubmitRef: () => undefined, shouldBlockNavigation: () => false, setIsSubmitting: () => undefined, - submit: () => Promise.resolve([]) + submit: () => Promise.resolve([]), + setIsSubmitDisabled: () => undefined }); const defaultValues = { @@ -56,6 +60,11 @@ export function useExitFormDialogProvider() { 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({}); @@ -209,10 +218,11 @@ export function useExitFormDialogProvider() { const errors = await Promise.all( getDirtyFormsSubmitFn().map(submitFn => submitFn()) ); - const isError = errors.flat().some(errors => errors); setIsSubmitting(false); + const isError = errors.flat().some(errors => errors); + if (!isError) { continueNavigation(); } @@ -239,7 +249,8 @@ export function useExitFormDialogProvider() { setEnableExitDialog, setExitDialogSubmitRef: setSubmitRef, setIsSubmitting, - submit: handleSubmit + submit: handleSubmit, + setIsSubmitDisabled }; return { @@ -247,7 +258,9 @@ export function useExitFormDialogProvider() { showDialog, handleSubmit, handleLeave, - handleClose + handleClose, + shouldBlockNav, + isSubmitDisabled }; } @@ -257,9 +270,20 @@ const ExitFormDialogProvider = ({ children }) => { handleLeave, handleSubmit, providerData, - showDialog + showDialog, + shouldBlockNav, + isSubmitDisabled } = useExitFormDialogProvider(); + useBeforeUnload(e => { + // If form is dirty and user does a refresh, + // the browser will ask about unsaved changes + if (shouldBlockNav() && !isInDevelopment) { + e.preventDefault(); + e.returnValue = ""; + } + }); + return ( { onSubmit={handleSubmit} onLeave={handleLeave} onClose={handleClose} + isSubmitDisabled={isSubmitDisabled.current} /> {children} diff --git a/src/components/Form/Form.tsx b/src/components/Form/Form.tsx index 5874ff431..232b8f89a 100644 --- a/src/components/Form/Form.tsx +++ b/src/components/Form/Form.tsx @@ -3,6 +3,13 @@ import React from "react"; import { FormId } from "./ExitFormDialogProvider"; +export type FormDataWithOpts = TData & + Pick, "hasChanged">; + +export type CheckIfSaveIsDisabledFnType = ( + data: FormDataWithOpts +) => boolean; + export interface FormProps extends Omit, "onSubmit"> { children: (props: UseFormResult) => React.ReactNode; @@ -11,6 +18,7 @@ export interface FormProps resetOnSubmit?: boolean; onSubmit?: (data: TData) => SubmitPromise | void; formId?: FormId; + checkIfSaveIsDisabled?: CheckIfSaveIsDisabledFnType; } function Form({ @@ -20,9 +28,16 @@ function Form({ onSubmit, confirmLeave = false, formId, + checkIfSaveIsDisabled, + disabled, ...rest }: FormProps) { - const renderProps = useForm(initial, onSubmit, { confirmLeave, formId }); + const renderProps = useForm(initial, onSubmit, { + confirmLeave, + formId, + checkIfSaveIsDisabled, + disabled + }); function handleSubmit(event?: React.FormEvent, cb?: () => void) { const { reset, submit } = renderProps; diff --git a/src/components/Form/messages.ts b/src/components/Form/messages.ts index 73ca90845..5d88c2129 100644 --- a/src/components/Form/messages.ts +++ b/src/components/Form/messages.ts @@ -2,20 +2,23 @@ import { defineMessages } from "react-intl"; export const exitFormPromptMessages = defineMessages({ title: { - defaultMessage: "Are you sure you want to leave?", + defaultMessage: "Would you like to save changes?", description: "ExitFormPrompt title" }, - description: { - defaultMessage: - "You have unsaved changes on this view. What would you like to do with them?", - description: "ExitFormPrompt description" + unableToSaveTitle: { + defaultMessage: "You have unsaved changes", + description: "ExitFormPrompt title" }, cancelButton: { - defaultMessage: "leave without saving", + defaultMessage: "Discard changes", description: "ExitFormPrompt cancel button" }, confirmButton: { - defaultMessage: "save & continue", + defaultMessage: "Save changes", description: "ExitFormPrompt confirm button" + }, + continueEditingButton: { + defaultMessage: "Continue editing", + description: "ExitFormPrompt continue editing button" } }); diff --git a/src/components/Form/useBeforeUnload.ts b/src/components/Form/useBeforeUnload.ts new file mode 100644 index 000000000..c3a8d6fdf --- /dev/null +++ b/src/components/Form/useBeforeUnload.ts @@ -0,0 +1,19 @@ +import { useEffect, useRef } from "react"; + +const useBeforeUnload = fn => { + const cb = useRef(fn); + + useEffect(() => { + cb.current = fn; + }, [fn]); + + useEffect(() => { + const onBeforeUnload = (...args) => cb.current?.(...args); + + window.addEventListener("beforeunload", onBeforeUnload); + + return () => window.removeEventListener("beforeunload", onBeforeUnload); + }, []); +}; + +export default useBeforeUnload; diff --git a/src/components/Form/useExitFormDialog.ts b/src/components/Form/useExitFormDialog.ts index c961e56f9..901fe4c69 100644 --- a/src/components/Form/useExitFormDialog.ts +++ b/src/components/Form/useExitFormDialog.ts @@ -1,4 +1,4 @@ -import { useContext, useRef } from "react"; +import React, { useContext, useRef } from "react"; import { ExitFormDialogContext, @@ -8,7 +8,14 @@ import { } from "./ExitFormDialogProvider"; export interface UseExitFormDialogResult - extends Omit, + extends Pick< + ExitFormDialogData, + | "setEnableExitDialog" + | "shouldBlockNavigation" + | "setIsSubmitting" + | "setIsSubmitDisabled" + | "submit" + >, WithFormId { setIsDirty: (isDirty: boolean) => void; setExitDialogSubmitRef: (submitFn: SubmitFn) => void; @@ -16,19 +23,29 @@ export interface UseExitFormDialogResult export interface UseExitFormDialogProps { formId: symbol; + isDisabled?: boolean; } export const useExitFormDialog = ( - { formId }: UseExitFormDialogProps = { formId: undefined } + { formId, isDisabled }: UseExitFormDialogProps = { formId: undefined } ): UseExitFormDialogResult => { const id = useRef(formId || Symbol()).current; - const { setIsDirty, setExitDialogSubmitRef, ...rest } = useContext( - ExitFormDialogContext - ); + const exitDialogProps = useContext(ExitFormDialogContext); + const { + setIsDirty, + setIsSubmitDisabled, + setExitDialogSubmitRef + } = exitDialogProps; + + React.useEffect(() => { + if (isDisabled !== undefined) { + setIsSubmitDisabled(isDisabled); + } + }, [isDisabled]); return { - ...rest, + ...exitDialogProps, formId: id, setIsDirty: (value: boolean) => setIsDirty(id, value), setExitDialogSubmitRef: (submitFn: SubmitFn) => diff --git a/src/customers/components/CustomerCreatePage/CustomerCreatePage.tsx b/src/customers/components/CustomerCreatePage/CustomerCreatePage.tsx index 7d6bc1a74..7e51c0b3f 100644 --- a/src/customers/components/CustomerCreatePage/CustomerCreatePage.tsx +++ b/src/customers/components/CustomerCreatePage/CustomerCreatePage.tsx @@ -135,8 +135,13 @@ const CustomerCreatePage: React.FC = ({ }; return ( -
- {({ change, data, hasChanged, submit }) => { + + {({ change, data, isSaveDisabled, submit }) => { const handleCountrySelect = createSingleAutocompleteSelectHandler( change, setCountryDisplayName, @@ -182,7 +187,7 @@ const CustomerCreatePage: React.FC = ({ = ({ } = useMetadataChangeTrigger(); return ( - - {({ change, data, hasChanged, submit }) => { + + {({ change, data, isSaveDisabled, submit }) => { const changeMetadata = makeMetadataChangeHandler(change); return ( @@ -136,7 +141,7 @@ const CustomerDetailsPage: React.FC = ({ = ({ privateMetadata: [] }; + const checkIfSaveIsDisabled = (data: FormDataWithOpts) => + data.channelListings?.some(channel => validateSalePrice(data, channel)) || + disabled || + !data.hasChanged; + return ( - {({ change, data, hasChanged, submit, triggerChange }) => { + {({ change, data, submit, triggerChange, isSaveDisabled }) => { const handleChannelChange = createSaleChannelsChangeHandler( data.channelListings, onChannelsChange, triggerChange, data.type ); - const formDisabled = data.channelListings?.some(channel => - validateSalePrice(data, channel) - ); const changeMetadata = makeMetadataChangeHandler(change); return ( @@ -152,7 +155,7 @@ const SaleCreatePage: React.FC = ({ = ({ metadata: sale?.metadata.map(mapMetadataItemToInput), privateMetadata: sale?.privateMetadata.map(mapMetadataItemToInput) }; + + const checkIfSaveIsDisabled = ( + data: FormDataWithOpts + ) => + data.channelListings?.some(channel => validateSalePrice(data, channel)) || + disabled || + (!data.hasChanged && !hasChannelChanged); + return ( - {({ change, data, hasChanged, submit, triggerChange }) => { + {({ change, data, submit, triggerChange, isSaveDisabled }) => { const handleChannelChange = createSaleChannelsChangeHandler( data.channelListings, onChannelsChange, triggerChange, data.type ); - const formDisabled = data.channelListings?.some(channel => - validateSalePrice(data, channel) - ); const changeMetadata = makeMetadataChangeHandler(change); return ( @@ -370,9 +376,7 @@ const SaleDetailsPage: React.FC = ({ = ({ privateMetadata: [] }; + const checkIfSaveIsDisabled = (data: FormDataWithOpts) => + (data.discountType.toString() !== "SHIPPING" && + data.channelListings?.some( + channel => + validatePrice(channel.discountValue) || + (data.requirementsPicker === RequirementsPicker.ORDER && + validatePrice(channel.minSpent)) + )) || + disabled || + (!data.hasChanged && hasChannelChanged); + return ( - {({ change, data, hasChanged, submit, triggerChange, set }) => { + {({ change, data, submit, triggerChange, set, isSaveDisabled }) => { const handleDiscountTypeChange = createDiscountTypeChangeHandler( change ); @@ -107,14 +119,6 @@ const VoucherCreatePage: React.FC = ({ onChannelsChange, triggerChange ); - const formDisabled = - data.discountType.toString() !== "SHIPPING" && - data.channelListings?.some( - channel => - validatePrice(channel.discountValue) || - (data.requirementsPicker === RequirementsPicker.ORDER && - validatePrice(channel.minSpent)) - ); const changeMetadata = makeMetadataChangeHandler(change); return ( @@ -199,9 +203,7 @@ const VoucherCreatePage: React.FC = ({ = { [field in keyof T]?: string | React.ReactNode; }; -export interface UseFormOpts { +export interface UseFormOpts { confirmLeave: boolean; formId?: FormId; + checkIfSaveIsDisabled?: CheckIfSaveIsDisabledFnType; + disabled?: boolean; } export interface UseFormResult @@ -42,6 +45,7 @@ export interface UseFormResult errors: FormErrors; setError: (name: keyof TData, error: string | React.ReactNode) => void; clearErrors: (name?: keyof TData | Array) => void; + setIsSubmitDisabled: (value: boolean) => void; } export interface CommonUseFormResult { @@ -49,6 +53,7 @@ export interface CommonUseFormResult { change: FormChange; hasChanged: boolean; submit: (dataOrEvent?: any) => SubmitPromise; + isSaveDisabled?: boolean; } export interface CommonUseFormResultWithHandlers @@ -84,9 +89,14 @@ function handleRefresh( function useForm( initialData: T, onSubmit?: (data: T) => SubmitPromise | void, - opts: UseFormOpts = { confirmLeave: false, formId: undefined } + opts: UseFormOpts = { confirmLeave: false, formId: undefined } ): UseFormResult { - const { confirmLeave, formId: propsFormId } = opts; + const { + confirmLeave, + formId: propsFormId, + checkIfSaveIsDisabled, + disabled + } = opts; const [hasChanged, setChanged] = useState(false); const [errors, setErrors] = useState>({}); const [data, setData] = useStateFromProps(initialData, { @@ -94,12 +104,33 @@ function useForm( onRefresh: newData => handleRefresh(data, newData, handleSetChanged) }); + const basicFormDisableConditions = () => !hasChanged || disabled; + + const isSaveDisabled = () => { + if (checkIfSaveIsDisabled) { + return checkIfSaveIsDisabled({ + ...data, + hasChanged + }); + } + + if (disabled !== undefined) { + return basicFormDisableConditions(); + } + + return false; + }; + const { setIsDirty: setIsFormDirtyInExitDialog, setExitDialogSubmitRef, setEnableExitDialog, + setIsSubmitDisabled, formId - } = useExitFormDialog({ formId: propsFormId }); + } = useExitFormDialog({ + formId: propsFormId, + isDisabled: isSaveDisabled() + }); const handleFormSubmit = useHandleFormSubmit({ formId, @@ -216,7 +247,9 @@ function useForm( toggleValue, handleChange, triggerChange: handleSetChanged, - setChanged: handleSetChanged + setChanged: handleSetChanged, + setIsSubmitDisabled, + isSaveDisabled: isSaveDisabled() }; } diff --git a/src/hooks/useHandleFormSubmit.ts b/src/hooks/useHandleFormSubmit.ts index 16ef35c73..27e5ee7ca 100644 --- a/src/hooks/useHandleFormSubmit.ts +++ b/src/hooks/useHandleFormSubmit.ts @@ -35,14 +35,14 @@ function useHandleFormSubmit({ const errors = await result; + setIsSubmitting(false); + if (errors?.length === 0) { setChanged(false); return []; } - setIsSubmitting(false); - return errors; } diff --git a/src/misc.ts b/src/misc.ts index 2fce05b7a..e206ddd30 100644 --- a/src/misc.ts +++ b/src/misc.ts @@ -522,3 +522,6 @@ export const combinedMultiAutocompleteChoices = ( selected: MultiAutocompleteChoiceType[], choices: MultiAutocompleteChoiceType[] ) => uniqBy([...selected, ...choices], "value"); + +export const isInDevelopment = + !process.env.NODE_ENV || process.env.NODE_ENV === "development"; diff --git a/src/orders/components/OrderRefundPage/OrderRefundPage.tsx b/src/orders/components/OrderRefundPage/OrderRefundPage.tsx index 288d33f1f..689a5d2b8 100644 --- a/src/orders/components/OrderRefundPage/OrderRefundPage.tsx +++ b/src/orders/components/OrderRefundPage/OrderRefundPage.tsx @@ -67,8 +67,9 @@ const OrderRefundPage: React.FC = props => { order={order} defaultType={defaultType} onSubmit={onSubmit} + disabled={disabled} > - {({ data, handlers, change, submit }) => { + {({ data, handlers, change, submit, isSaveDisabled }) => { const isProductRefund = data.type === OrderRefundType.PRODUCTS; return ( @@ -156,7 +157,7 @@ const OrderRefundPage: React.FC = props => { } data={data} order={order} - disabled={disabled} + disabled={isSaveDisabled} errors={errors} onChange={change} onRefund={submit} diff --git a/src/orders/components/OrderRefundPage/form.tsx b/src/orders/components/OrderRefundPage/form.tsx index 74a27d44c..2d5c365d4 100644 --- a/src/orders/components/OrderRefundPage/form.tsx +++ b/src/orders/components/OrderRefundPage/form.tsx @@ -57,6 +57,7 @@ interface OrderRefundFormProps { order: OrderRefundDataQuery["order"]; defaultType: OrderRefundType; onSubmit: (data: OrderRefundSubmitData) => SubmitPromise; + disabled: boolean; } function getOrderRefundPageFormData( @@ -73,7 +74,8 @@ function getOrderRefundPageFormData( function useOrderRefundForm( order: OrderRefundDataQuery["order"], defaultType: OrderRefundType, - onSubmit: (data: OrderRefundSubmitData) => SubmitPromise + onSubmit: (data: OrderRefundSubmitData) => SubmitPromise, + disabled: boolean ): UseOrderRefundFormResult { const { handleChange, @@ -81,12 +83,15 @@ function useOrderRefundForm( hasChanged, triggerChange, data: formData, - formId + formId, + setIsSubmitDisabled } = useForm(getOrderRefundPageFormData(defaultType), undefined, { confirmLeave: true }); - const { setExitDialogSubmitRef } = useExitFormDialog(); + const { setExitDialogSubmitRef } = useExitFormDialog({ + formId + }); const refundedProductQuantities = useFormset( order?.lines @@ -188,7 +193,8 @@ function useOrderRefundForm( useEffect(() => setExitDialogSubmitRef(submit), [submit]); - const disabled = !order; + const isSaveDisabled = disabled || !order; + setIsSubmitDisabled(isSaveDisabled); return { change: handleChange, @@ -201,7 +207,8 @@ function useOrderRefundForm( setMaximalRefundedProductQuantities: handleMaximalRefundedProductQuantitiesSet }, hasChanged, - submit + submit, + isSaveDisabled }; } @@ -209,9 +216,10 @@ const OrderRefundForm: React.FC = ({ children, order, defaultType, - onSubmit + onSubmit, + disabled }) => { - const props = useOrderRefundForm(order, defaultType, onSubmit); + const props = useOrderRefundForm(order, defaultType, onSubmit, disabled); return {children(props)}; }; diff --git a/src/orders/components/OrderReturnPage/OrderReturnPage.tsx b/src/orders/components/OrderReturnPage/OrderReturnPage.tsx index 33fda2acc..3125493bb 100644 --- a/src/orders/components/OrderReturnPage/OrderReturnPage.tsx +++ b/src/orders/components/OrderReturnPage/OrderReturnPage.tsx @@ -45,110 +45,97 @@ const OrderRefundPage: React.FC = props => { const intl = useIntl(); return ( - {({ data, handlers, change, submit }) => { - const { - fulfilledItemsQuantities, - waitingItemsQuantities, - unfulfilledItemsQuantities - } = data; - - const hasAnyItemsSelected = - fulfilledItemsQuantities.some(({ value }) => !!value) || - waitingItemsQuantities.some(({ value }) => !!value) || - unfulfilledItemsQuantities.some(({ value }) => !!value); - - return ( - - - {intl.formatMessage(messages.appTitle, { - orderNumber: order?.number - })} - - - -
- {!!data.unfulfilledItemsQuantities.length && ( - <> + {({ data, handlers, change, submit, isSaveDisabled }) => ( + + + {intl.formatMessage(messages.appTitle, { + orderNumber: order?.number + })} + + + +
+ {!!data.unfulfilledItemsQuantities.length && ( + <> + + + + )} + {renderCollection( + getWaitingFulfillments(order), + ({ id, lines }) => ( + - - )} - {renderCollection( - getWaitingFulfillments(order), - ({ id, lines }) => ( - - - - - ) - )} - {renderCollection( - getFulfilledFulfillemnts(order), - ({ id, lines }) => ( - - - - - ) - )} -
-
- -
-
-
- ); - }} + + ) + )} + {renderCollection( + getFulfilledFulfillemnts(order), + ({ id, lines }) => ( + + + + + ) + )} +
+
+ +
+
+
+ )}
); }; diff --git a/src/orders/components/OrderReturnPage/form.tsx b/src/orders/components/OrderReturnPage/form.tsx index c08df6720..ce0e7270e 100644 --- a/src/orders/components/OrderReturnPage/form.tsx +++ b/src/orders/components/OrderReturnPage/form.tsx @@ -85,12 +85,15 @@ function useOrderReturnForm( hasChanged, data: formData, triggerChange, - formId + formId, + setIsSubmitDisabled } = useForm(getOrderRefundPageFormData(), undefined, { confirmLeave: true }); - const { setExitDialogSubmitRef } = useExitFormDialog(); + const { setExitDialogSubmitRef } = useExitFormDialog({ + formId + }); const unfulfiledItemsQuantites = useFormset( getOrderUnfulfilledLines(order).map(getParsedLineData({ initialValue: 0 })) @@ -241,6 +244,14 @@ function useOrderReturnForm( }; } + const hasAnyItemsSelected = + fulfiledItemsQuatities.data.some(({ value }) => !!value) || + waitingItemsQuantities.data.some(({ value }) => !!value) || + unfulfiledItemsQuantites.data.some(({ value }) => !!value); + + const isSaveDisabled = !hasAnyItemsSelected; + setIsSubmitDisabled(isSaveDisabled); + return { change: handleChange, data, @@ -259,7 +270,8 @@ function useOrderReturnForm( handleSetMaximalUnfulfiledItemsQuantities }, hasChanged, - submit + submit, + isSaveDisabled }; } diff --git a/src/orders/components/OrderSettingsPage/OrderSettingsPage.tsx b/src/orders/components/OrderSettingsPage/OrderSettingsPage.tsx index f4aaa9cb7..fcb21522b 100644 --- a/src/orders/components/OrderSettingsPage/OrderSettingsPage.tsx +++ b/src/orders/components/OrderSettingsPage/OrderSettingsPage.tsx @@ -43,8 +43,9 @@ const OrderSettingsPage: React.FC = props => { orderSettings={orderSettings} shop={shop} onSubmit={onSubmit} + disabled={disabled} > - {({ data, submit, hasChanged, change }) => ( + {({ data, submit, change, isSaveDisabled }) => ( {intl.formatMessage(sectionNames.orders)} @@ -73,7 +74,7 @@ const OrderSettingsPage: React.FC = props => { diff --git a/src/orders/components/OrderSettingsPage/form.tsx b/src/orders/components/OrderSettingsPage/form.tsx index 3571ba9b1..ccc00e278 100644 --- a/src/orders/components/OrderSettingsPage/form.tsx +++ b/src/orders/components/OrderSettingsPage/form.tsx @@ -2,7 +2,10 @@ import { OrderSettingsFragment, ShopOrderSettingsFragment } from "@saleor/graphql"; -import useForm, { FormChange, SubmitPromise } from "@saleor/hooks/useForm"; +import useForm, { + CommonUseFormResult, + SubmitPromise +} from "@saleor/hooks/useForm"; import useHandleFormSubmit from "@saleor/hooks/useHandleFormSubmit"; import React from "react"; @@ -13,18 +16,15 @@ export interface OrderSettingsFormData { automaticallyFulfillNonShippableGiftCard: boolean; } -export interface UseOrderSettingsFormResult { - change: FormChange; - data: OrderSettingsFormData; - hasChanged: boolean; - submit: () => SubmitPromise; -} - +export type UseOrderSettingsFormResult = CommonUseFormResult< + OrderSettingsFormData +>; export interface OrderSettingsFormProps { children: (props: UseOrderSettingsFormResult) => React.ReactNode; orderSettings: OrderSettingsFragment; shop: ShopOrderSettingsFragment; onSubmit: (data: OrderSettingsFormData) => SubmitPromise; + disabled: boolean; } function getOrderSeettingsFormData( @@ -44,15 +44,19 @@ function getOrderSeettingsFormData( function useOrderSettingsForm( orderSettings: OrderSettingsFragment, shop: ShopOrderSettingsFragment, - onSubmit: (data: OrderSettingsFormData) => SubmitPromise + onSubmit: (data: OrderSettingsFormData) => SubmitPromise, + disabled: boolean ): UseOrderSettingsFormResult { - const { data, handleChange, formId, hasChanged, setChanged } = useForm( - getOrderSeettingsFormData(orderSettings, shop), - undefined, - { - confirmLeave: true - } - ); + const { + data, + handleChange, + formId, + hasChanged, + setChanged, + setIsSubmitDisabled + } = useForm(getOrderSeettingsFormData(orderSettings, shop), undefined, { + confirmLeave: true + }); const handleFormSubmit = useHandleFormSubmit({ formId, @@ -61,12 +65,15 @@ function useOrderSettingsForm( }); const submit = () => handleFormSubmit(data); + const isSaveDisabled = disabled || !hasChanged; + setIsSubmitDisabled(isSaveDisabled); return { change: handleChange, data, hasChanged, - submit + submit, + isSaveDisabled }; } @@ -74,9 +81,10 @@ const OrderSettingsForm: React.FC = ({ children, orderSettings, shop, - onSubmit + onSubmit, + disabled }) => { - const props = useOrderSettingsForm(orderSettings, shop, onSubmit); + const props = useOrderSettingsForm(orderSettings, shop, onSubmit, disabled); return
{children(props)}
; }; diff --git a/src/pageTypes/components/PageTypeCreatePage/PageTypeCreatePage.tsx b/src/pageTypes/components/PageTypeCreatePage/PageTypeCreatePage.tsx index 4cc46e1e6..033d7f9dc 100644 --- a/src/pageTypes/components/PageTypeCreatePage/PageTypeCreatePage.tsx +++ b/src/pageTypes/components/PageTypeCreatePage/PageTypeCreatePage.tsx @@ -58,8 +58,13 @@ const PageTypeCreatePage: React.FC = props => { } = useMetadataChangeTrigger(); return ( -
- {({ change, data, hasChanged, submit }) => { + + {({ change, data, submit, isSaveDisabled }) => { const changeMetadata = makeMetadataChangeHandler(change); return ( @@ -103,7 +108,7 @@ const PageTypeCreatePage: React.FC = props => { diff --git a/src/pageTypes/components/PageTypeDetailsPage/PageTypeDetailsPage.tsx b/src/pageTypes/components/PageTypeDetailsPage/PageTypeDetailsPage.tsx index d7e94254f..86fc9bbb2 100644 --- a/src/pageTypes/components/PageTypeDetailsPage/PageTypeDetailsPage.tsx +++ b/src/pageTypes/components/PageTypeDetailsPage/PageTypeDetailsPage.tsx @@ -110,8 +110,13 @@ const PageTypeDetailsPage: React.FC = props => { }; return ( - - {({ change, data, hasChanged, submit }) => { + + {({ change, data, isSaveDisabled, submit }) => { const changeMetadata = makeMetadataChangeHandler(change); return ( @@ -174,7 +179,7 @@ const PageTypeDetailsPage: React.FC = props => { onCancel={onBack} onDelete={onDelete} onSubmit={submit} - disabled={disabled || !hasChanged} + disabled={isSaveDisabled} state={saveButtonBarState} /> diff --git a/src/pages/components/PageDetailsPage/PageDetailsPage.tsx b/src/pages/components/PageDetailsPage/PageDetailsPage.tsx index 9da4e2b88..67b0ae593 100644 --- a/src/pages/components/PageDetailsPage/PageDetailsPage.tsx +++ b/src/pages/components/PageDetailsPage/PageDetailsPage.tsx @@ -135,8 +135,9 @@ const PageDetailsPage: React.FC = ({ fetchMoreReferenceProducts={fetchMoreReferenceProducts} assignReferencesAttributeId={assignReferencesAttributeId} onSubmit={onSubmit} + disabled={loading} > - {({ change, data, valid, handlers, hasChanged, submit }) => ( + {({ change, data, handlers, submit, isSaveDisabled }) => ( {intl.formatMessage(sectionNames.pages)} @@ -242,7 +243,7 @@ const PageDetailsPage: React.FC = ({ React.ReactNode; page: PageDetailsFragment; onSubmit: (data: PageData) => SubmitPromise; + disabled: boolean; } const getInitialFormData = (page?: PageDetailsFragment): PageFormData => ({ @@ -114,6 +115,7 @@ const getInitialFormData = (page?: PageDetailsFragment): PageFormData => ({ function usePageForm( page: PageDetailsFragment, onSubmit: (data: PageData) => SubmitPromise, + disabled: boolean, opts: UsePageFormOpts ): UsePageUpdateFormResult { const pageExists = page !== null; @@ -138,7 +140,7 @@ function usePageForm( confirmLeave: true }); - const { setExitDialogSubmitRef } = useExitFormDialog({ + const { setExitDialogSubmitRef, setIsSubmitDisabled } = useExitFormDialog({ formId }); @@ -238,6 +240,9 @@ function usePageForm( const valid = pageExists || !!opts.selectedPageType; + const isSaveDisabled = disabled || !hasChanged || !valid; + setIsSubmitDisabled(isSaveDisabled); + return { change: handleChange, data: getData(), @@ -255,7 +260,8 @@ function usePageForm( selectPageType: handlePageTypeSelect }, hasChanged, - submit + submit, + isSaveDisabled }; } @@ -263,9 +269,10 @@ const PageForm: React.FC = ({ children, page, onSubmit, + disabled, ...rest }) => { - const props = usePageForm(page, onSubmit, rest); + const props = usePageForm(page, onSubmit, disabled, rest); return {children(props)}; }; diff --git a/src/permissionGroups/components/PermissionGroupCreatePage/PermissionGroupCreatePage.tsx b/src/permissionGroups/components/PermissionGroupCreatePage/PermissionGroupCreatePage.tsx index 9462d6082..8d6dd86fa 100644 --- a/src/permissionGroups/components/PermissionGroupCreatePage/PermissionGroupCreatePage.tsx +++ b/src/permissionGroups/components/PermissionGroupCreatePage/PermissionGroupCreatePage.tsx @@ -55,8 +55,13 @@ const PermissionGroupCreatePage: React.FC = ({ ); return ( -
- {({ data, change, submit, hasChanged }) => ( + + {({ data, change, submit, isSaveDisabled }) => ( {intl.formatMessage(sectionNames.permissionGroups)} @@ -95,7 +100,7 @@ const PermissionGroupCreatePage: React.FC = ({ onCancel={onBack} onSubmit={submit} state={saveButtonBarState} - disabled={disabled || !hasChanged} + disabled={isSaveDisabled} /> diff --git a/src/plugins/components/PluginsDetailsPage/PluginsDetailsPage.tsx b/src/plugins/components/PluginsDetailsPage/PluginsDetailsPage.tsx index a6d9e07bf..b89977ea5 100644 --- a/src/plugins/components/PluginsDetailsPage/PluginsDetailsPage.tsx +++ b/src/plugins/components/PluginsDetailsPage/PluginsDetailsPage.tsx @@ -75,8 +75,9 @@ const PluginsDetailsPage: React.FC = ({ initial={initialFormData()} onSubmit={onSubmit} key={selectedChannelId} + disabled={disabled} > - {({ data, hasChanged, submit, set }) => { + {({ data, submit, set, isSaveDisabled }) => { const onChange = (event: ChangeEvent) => { const { name, value } = event.target; const newData = { @@ -159,7 +160,7 @@ const PluginsDetailsPage: React.FC = ({ = ({ }; return ( - - {({ change, data, hasChanged, submit }) => { + + {({ change, data, isSaveDisabled, submit }) => { const changeMetadata = makeMetadataChangeHandler(change); const changeKind = makeProductTypeKindChangeHandler( @@ -145,7 +150,7 @@ const ProductTypeCreatePage: React.FC = ({ diff --git a/src/productTypes/components/ProductTypeDetailsPage/ProductTypeDetailsPage.tsx b/src/productTypes/components/ProductTypeDetailsPage/ProductTypeDetailsPage.tsx index 6a3f0a4c0..2b6c1516b 100644 --- a/src/productTypes/components/ProductTypeDetailsPage/ProductTypeDetailsPage.tsx +++ b/src/productTypes/components/ProductTypeDetailsPage/ProductTypeDetailsPage.tsx @@ -156,8 +156,13 @@ const ProductTypeDetailsPage: React.FC = ({ }; return ( - - {({ change, data, hasChanged, submit, setChanged }) => { + + {({ change, data, isSaveDisabled, submit, setChanged }) => { const changeMetadata = makeMetadataChangeHandler(change); return ( @@ -256,7 +261,7 @@ const ProductTypeDetailsPage: React.FC = ({ onCancel={onBack} onDelete={onDelete} onSubmit={submit} - disabled={disabled || !hasChanged} + disabled={isSaveDisabled} state={saveButtonBarState} /> diff --git a/src/products/components/ProductCreatePage/ProductCreatePage.tsx b/src/products/components/ProductCreatePage/ProductCreatePage.tsx index f2b74d8c9..843ab65ed 100644 --- a/src/products/components/ProductCreatePage/ProductCreatePage.tsx +++ b/src/products/components/ProductCreatePage/ProductCreatePage.tsx @@ -202,16 +202,9 @@ export const ProductCreatePage: React.FC = ({ fetchReferenceProducts={fetchReferenceProducts} fetchMoreReferenceProducts={fetchMoreReferenceProducts} assignReferencesAttributeId={assignReferencesAttributeId} + loading={loading} > - {({ - change, - data, - formErrors, - disabled: formDisabled, - handlers, - hasChanged, - submit - }) => { + {({ change, data, formErrors, handlers, submit, isSaveDisabled }) => { // Comparing explicitly to false because `hasVariants` can be undefined const isSimpleProduct = data.productType?.hasVariants === false; @@ -367,7 +360,7 @@ export const ProductCreatePage: React.FC = ({ onCancel={onBack} onSubmit={submit} state={saveButtonBarState} - disabled={loading || !onSubmit || formDisabled || !hasChanged} + disabled={isSaveDisabled} /> {canOpenAssignReferencesAttributeDialog && ( React.ReactNode; initial?: Partial; onSubmit: (data: ProductCreateData) => SubmitPromise; + loading: boolean; } function useProductCreateForm( initial: Partial, onSubmit: (data: ProductCreateData) => SubmitPromise, + loading: boolean, opts: UseProductCreateFormOpts ): UseProductCreateFormResult { const intl = useIntl(); @@ -231,10 +233,6 @@ function useProductCreateForm( triggerChange }); - const { setExitDialogSubmitRef } = useExitFormDialog({ - formId: PRODUCT_CREATE_FORM_ID - }); - const { makeChangeHandler: makeMetadataChangeHandler } = useMetadataChangeTrigger(); @@ -357,6 +355,10 @@ function useProductCreateForm( const submit = () => handleFormSubmit(data); + const { setExitDialogSubmitRef, setIsSubmitDisabled } = useExitFormDialog({ + formId: PRODUCT_CREATE_FORM_ID + }); + useEffect(() => setExitDialogSubmitRef(submit), [submit]); const shouldEnableSave = () => { @@ -387,12 +389,15 @@ function useProductCreateForm( return true; }; - const disabled = !shouldEnableSave(); + const isSaveEnabled = !shouldEnableSave(); + + const isSaveDisabled = loading || !onSubmit || isSaveEnabled || !hasChanged; + setIsSubmitDisabled(isSaveDisabled); return { change: handleChange, data, - disabled, + disabled: isSaveEnabled, formErrors: form.errors, handlers: { addStock: handleStockAdd, @@ -416,7 +421,8 @@ function useProductCreateForm( selectTaxRate: handleTaxTypeSelect }, hasChanged, - submit + submit, + isSaveDisabled }; } @@ -424,9 +430,10 @@ const ProductCreateForm: React.FC = ({ children, initial, onSubmit, + loading, ...rest }) => { - const props = useProductCreateForm(initial || {}, onSubmit, rest); + const props = useProductCreateForm(initial || {}, onSubmit, loading, rest); return {children(props)}; }; diff --git a/src/products/components/ProductUpdatePage/ProductUpdatePage.tsx b/src/products/components/ProductUpdatePage/ProductUpdatePage.tsx index a40cf22bc..473ca2891 100644 --- a/src/products/components/ProductUpdatePage/ProductUpdatePage.tsx +++ b/src/products/components/ProductUpdatePage/ProductUpdatePage.tsx @@ -284,16 +284,10 @@ export const ProductUpdatePage: React.FC = ({ fetchReferenceProducts={fetchReferenceProducts} fetchMoreReferenceProducts={fetchMoreReferenceProducts} assignReferencesAttributeId={assignReferencesAttributeId} + disabled={disabled} + hasChannelChanged={hasChannelChanged} > - {({ - change, - data, - formErrors, - disabled: formDisabled, - handlers, - hasChanged, - submit - }) => ( + {({ change, data, formErrors, handlers, submit, isSaveDisabled }) => ( <> @@ -508,9 +502,7 @@ export const ProductUpdatePage: React.FC = ({ onDelete={onDelete} onSubmit={submit} state={saveButtonBarState} - disabled={ - disabled || formDisabled || (!hasChanged && !hasChannelChanged) - } + disabled={isSaveDisabled} /> {canOpenAssignReferencesAttributeDialog && ( { - disabled: boolean; formErrors: FormErrors; } @@ -193,6 +192,8 @@ export interface ProductUpdateFormProps extends UseProductUpdateFormOpts { children: (props: UseProductUpdateFormResult) => React.ReactNode; product: ProductFragment; onSubmit: (data: ProductUpdateSubmitData) => SubmitPromise; + disabled: boolean; + hasChannelChanged: boolean; } const getStocksData = ( @@ -222,6 +223,8 @@ const getStocksData = ( function useProductUpdateForm( product: ProductFragment, onSubmit: (data: ProductUpdateSubmitData) => SubmitPromise, + disabled: boolean, + hasChannelChanged: boolean, opts: UseProductUpdateFormOpts ): UseProductUpdateFormResult { const intl = useIntl(); @@ -244,7 +247,8 @@ function useProductUpdateForm( toggleValue, data: formData, setChanged, - hasChanged + hasChanged, + setIsSubmitDisabled } = form; const attributes = useFormset(getAttributeInputFromProduct(product)); @@ -435,12 +439,15 @@ function useProductUpdateForm( return true; }; - const disabled = !shouldEnableSave(); + const isSaveEnabled = !shouldEnableSave(); + + const isSaveDisabled = + disabled || isSaveEnabled || (!hasChanged && !hasChannelChanged); + setIsSubmitDisabled(isSaveDisabled); return { change: handleChange, data, - disabled, formErrors: form.errors, handlers: { addStock: handleStockAdd, @@ -464,7 +471,8 @@ function useProductUpdateForm( selectTaxRate: handleTaxTypeSelect }, hasChanged, - submit + submit, + isSaveDisabled }; } @@ -472,9 +480,17 @@ const ProductUpdateForm: React.FC = ({ children, product, onSubmit, + disabled, + hasChannelChanged, ...rest }) => { - const props = useProductUpdateForm(product, onSubmit, rest); + const props = useProductUpdateForm( + product, + onSubmit, + disabled, + hasChannelChanged, + rest + ); return
{children(props)}
; }; diff --git a/src/products/components/ProductVariantCreatePage/ProductVariantCreatePage.tsx b/src/products/components/ProductVariantCreatePage/ProductVariantCreatePage.tsx index b36c617a7..fc66a720d 100644 --- a/src/products/components/ProductVariantCreatePage/ProductVariantCreatePage.tsx +++ b/src/products/components/ProductVariantCreatePage/ProductVariantCreatePage.tsx @@ -149,15 +149,9 @@ const ProductVariantCreatePage: React.FC = ({ fetchReferenceProducts={fetchReferenceProducts} fetchMoreReferenceProducts={fetchMoreReferenceProducts} assignReferencesAttributeId={assignReferencesAttributeId} + disabled={disabled} > - {({ - change, - data, - formErrors, - disabled: formDisabled, - handlers, - submit - }) => ( + {({ change, data, formErrors, handlers, submit, isSaveDisabled }) => ( {product?.name} @@ -258,7 +252,7 @@ const ProductVariantCreatePage: React.FC = ({ React.ReactNode; product: ProductVariantCreateDataQuery["product"]; onSubmit: (data: ProductVariantCreateData) => void; + disabled: boolean; } const initial: ProductVariantCreateFormData = { @@ -113,6 +114,7 @@ const initial: ProductVariantCreateFormData = { function useProductVariantCreateForm( product: ProductVariantCreateDataQuery["product"], onSubmit: (data: ProductVariantCreateData) => void, + disabled: boolean, opts: UseProductVariantCreateFormOpts ): UseProductVariantCreateFormResult { const intl = useIntl(); @@ -126,7 +128,8 @@ function useProductVariantCreateForm( handleChange, hasChanged, data: formData, - formId + formId, + setIsSubmitDisabled } = form; const attributes = useFormset(attributeInput); @@ -227,13 +230,18 @@ function useProductVariantCreateForm( useEffect(() => setExitDialogSubmitRef(submit), [submit]); + const formDisabled = + data.isPreorder && + data.hasPreorderEndDate && + !!form.errors.preorderEndDateTime; + + const isSaveDisabled = disabled || formDisabled || !onSubmit; + setIsSubmitDisabled(isSaveDisabled); + return { change: handleChange, data, - disabled: - data.isPreorder && - data.hasPreorderEndDate && - !!form.errors.preorderEndDateTime, + disabled, formErrors: form.errors, handlers: { addStock: handleStockAdd, @@ -250,7 +258,8 @@ function useProductVariantCreateForm( selectAttributeReference: handleAttributeReferenceChange }, hasChanged, - submit + submit, + isSaveDisabled }; } @@ -258,9 +267,10 @@ const ProductVariantCreateForm: React.FC = ({ children, product, onSubmit, + disabled, ...rest }) => { - const props = useProductVariantCreateForm(product, onSubmit, rest); + const props = useProductVariantCreateForm(product, onSubmit, disabled, rest); return
{children(props)}
; }; diff --git a/src/products/components/ProductVariantPage/ProductVariantPage.tsx b/src/products/components/ProductVariantPage/ProductVariantPage.tsx index 9bb9f91d8..29426bab7 100644 --- a/src/products/components/ProductVariantPage/ProductVariantPage.tsx +++ b/src/products/components/ProductVariantPage/ProductVariantPage.tsx @@ -218,16 +218,9 @@ const ProductVariantPage: React.FC = ({ fetchReferenceProducts={fetchReferenceProducts} fetchMoreReferenceProducts={fetchMoreReferenceProducts} assignReferencesAttributeId={assignReferencesAttributeId} + loading={loading} > - {({ - change, - data, - formErrors, - disabled: formDisabled, - handlers, - hasChanged, - submit - }) => { + {({ change, data, formErrors, isSaveDisabled, handlers, submit }) => { const nonSelectionAttributes = data.attributes.filter( byAttributeScope(VariantAttributeScope.NOT_VARIANT_SELECTION) ); @@ -373,7 +366,7 @@ const ProductVariantPage: React.FC = ({ React.ReactNode; variant: ProductVariantFragment; + loading: boolean; onSubmit: (data: ProductVariantUpdateSubmitData) => SubmitPromise; } function useProductVariantUpdateForm( variant: ProductVariantFragment, onSubmit: (data: ProductVariantUpdateSubmitData) => SubmitPromise, + loading: boolean, opts: UseProductVariantUpdateFormOpts ): UseProductVariantUpdateFormResult { const intl = useIntl(); @@ -180,7 +182,8 @@ function useProductVariantUpdateForm( data: formData, setChanged, hasChanged, - formId + formId, + setIsSubmitDisabled } = form; const { setExitDialogSubmitRef } = useExitFormDialog({ @@ -332,6 +335,9 @@ function useProductVariantUpdateForm( useEffect(() => setExitDialogSubmitRef(submit), [submit]); + const isSaveDisabled = loading || disabled || !hasChanged; + setIsSubmitDisabled(isSaveDisabled); + return { change: handleChange, data, @@ -353,7 +359,8 @@ function useProductVariantUpdateForm( selectAttributeReference: handleAttributeReferenceChange }, hasChanged, - submit + submit, + isSaveDisabled }; } @@ -361,9 +368,10 @@ const ProductVariantUpdateForm: React.FC = ({ children, variant, onSubmit, + loading, ...rest }) => { - const props = useProductVariantUpdateForm(variant, onSubmit, rest); + const props = useProductVariantUpdateForm(variant, onSubmit, loading, rest); return
{children(props)}
; }; diff --git a/src/products/views/ProductCreate/handlers.ts b/src/products/views/ProductCreate/handlers.ts index 29533ea32..28fb4e053 100644 --- a/src/products/views/ProductCreate/handlers.ts +++ b/src/products/views/ProductCreate/handlers.ts @@ -122,10 +122,11 @@ export function createHandler( }; const result = await productCreate(productVariables); + let hasErrors = errors.length > 0; - const hasVariants = productType.hasVariants; - const productId = result.data.productCreate.product?.id; + const hasVariants = productType?.hasVariants; + const productId = result?.data?.productCreate?.product?.id; if (!productId) { return { errors }; diff --git a/src/shipping/components/ShippingZoneCreatePage/ShippingZoneCreatePage.tsx b/src/shipping/components/ShippingZoneCreatePage/ShippingZoneCreatePage.tsx index 303fd1f54..f07a8015a 100644 --- a/src/shipping/components/ShippingZoneCreatePage/ShippingZoneCreatePage.tsx +++ b/src/shipping/components/ShippingZoneCreatePage/ShippingZoneCreatePage.tsx @@ -66,8 +66,13 @@ const ShippingZoneCreatePage: React.FC = ({ }; return ( -
- {({ change, data, hasChanged, submit }) => ( + + {({ change, data, isSaveDisabled, submit }) => ( <> @@ -105,7 +110,7 @@ const ShippingZoneCreatePage: React.FC = ({ = ({ } = useMetadataChangeTrigger(); return ( - - {({ change, data, hasChanged, submit, toggleValue }) => { + + {({ change, data, isSaveDisabled, submit, toggleValue }) => { const handleWarehouseChange = createMultiAutocompleteSelectHandler( toggleValue, setWarehouseDisplayValues, @@ -215,7 +220,7 @@ const ShippingZoneDetailsPage: React.FC = ({ + ) => { + const formDisabled = data.channelListings?.some(channel => + validatePrice(channel.price) + ); + + return disabled || formDisabled || (!data.hasChanged && !hasChannelChanged); + }; + return ( - {({ change, data, hasChanged, submit, triggerChange, set }) => { + {({ change, data, isSaveDisabled, submit, triggerChange, set }) => { const handleChannelsChange = createChannelsChangeHandler( shippingChannels, onChannelsChange, triggerChange ); - const formDisabled = data.channelListings?.some(channel => - validatePrice(channel.price) - ); const onDescriptionChange = (description: OutputData) => { set({ description }); triggerChange(); @@ -181,9 +189,7 @@ export const ShippingZoneRatesCreatePage: React.FC = ({ makeChangeHandler: makeMetadataChangeHandler } = useMetadataChangeTrigger(); + const checkIfSaveIsDisabled = ( + data: FormDataWithOpts + ) => { + const formDisabled = data.channelListings?.some(channel => + validatePrice(channel.price) + ); + const formIsUnchanged = + !data.hasChanged && !hasChannelChanged && !havePostalCodesChanged; + + return disabled || formDisabled || formIsUnchanged; + }; + return ( - {({ change, data, hasChanged, submit, set, triggerChange }) => { + {({ change, data, isSaveDisabled, submit, set, triggerChange }) => { const handleChannelsChange = createChannelsChangeHandler( shippingChannels, onChannelsChange, triggerChange ); - const formDisabled = data.channelListings?.some(channel => - validatePrice(channel.price) - ); const onDescriptionChange = (description: OutputData) => { set({ description }); triggerChange(); }; const changeMetadata = makeMetadataChangeHandler(change); - const formIsUnchanged = - !hasChanged && !hasChannelChanged && !havePostalCodesChanged; return ( @@ -212,7 +220,7 @@ export const ShippingZoneRatesPage: React.FC = ({ = props => { return submitFunc(data); }} confirmLeave + disabled={disabled} > - {({ change, data, hasChanged, submit }) => { + {({ change, data, isSaveDisabled, submit }) => { const countryChoices = mapCountriesToChoices(shop?.countries || []); const handleCountryChange = createSingleAutocompleteSelectHandler( change, @@ -201,7 +202,7 @@ const SiteSettingsPage: React.FC = props => { diff --git a/src/staff/components/StaffDetailsPage/StaffDetailsPage.tsx b/src/staff/components/StaffDetailsPage/StaffDetailsPage.tsx index 93be2dac9..03ecdc46b 100644 --- a/src/staff/components/StaffDetailsPage/StaffDetailsPage.tsx +++ b/src/staff/components/StaffDetailsPage/StaffDetailsPage.tsx @@ -101,8 +101,13 @@ const StaffDetailsPage: React.FC = ({ }; return ( - - {({ data: formData, change, hasChanged, submit, toggleValue }) => { + + {({ data: formData, change, isSaveDisabled, submit, toggleValue }) => { const permissionGroupsChange = createMultiAutocompleteSelectHandler( toggleValue, setPermissionGroupsDisplayValues, @@ -187,7 +192,7 @@ const StaffDetailsPage: React.FC = ({ = ({ showGross: maybe(() => shop.displayGrossPrices, false) }; return ( - - {({ change, data, hasChanged, submit }) => ( + + {({ change, data, isSaveDisabled, submit }) => ( <> @@ -77,7 +82,7 @@ const CountryListPage: React.FC = ({ = ({ }; return ( - - {({ change, data, hasChanged, submit, set }) => { + + {({ change, data, isSaveDisabled, submit, set }) => { const countryChoices = mapCountriesToChoices(countries); const handleCountryChange = createSingleAutocompleteSelectHandler( change, @@ -134,7 +139,7 @@ const WarehouseDetailsPage: React.FC = ({