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
This commit is contained in:
Wojciech Mista 2022-03-23 10:13:23 +01:00 committed by GitHub
parent 7576afed4e
commit a5ac6bb92e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
59 changed files with 717 additions and 390 deletions

View file

@ -2231,19 +2231,23 @@
}, },
"src_dot_components_dot_Form_dot_cancelButton": { "src_dot_components_dot_Form_dot_cancelButton": {
"context": "ExitFormPrompt cancel button", "context": "ExitFormPrompt cancel button",
"string": "leave without saving" "string": "Discard changes"
}, },
"src_dot_components_dot_Form_dot_confirmButton": { "src_dot_components_dot_Form_dot_confirmButton": {
"context": "ExitFormPrompt confirm button", "context": "ExitFormPrompt confirm button",
"string": "save & continue" "string": "Save changes"
}, },
"src_dot_components_dot_Form_dot_description": { "src_dot_components_dot_Form_dot_continueEditingButton": {
"context": "ExitFormPrompt description", "context": "ExitFormPrompt continue editing button",
"string": "You have unsaved changes on this view. What would you like to do with them?" "string": "Continue editing"
}, },
"src_dot_components_dot_Form_dot_title": { "src_dot_components_dot_Form_dot_title": {
"context": "ExitFormPrompt 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": { "src_dot_components_dot_ImageUpload_dot_1731007575": {
"context": "image upload", "context": "image upload",

View file

@ -56,8 +56,13 @@ const CustomAppCreatePage: React.FC<CustomAppCreatePageProps> = props => {
const permissionsError = getAppErrorMessage(formErrors.permissions, intl); const permissionsError = getAppErrorMessage(formErrors.permissions, intl);
return ( return (
<Form confirmLeave initial={initialForm} onSubmit={onSubmit}> <Form
{({ data, change, hasChanged, submit }) => ( confirmLeave
initial={initialForm}
onSubmit={onSubmit}
disabled={disabled}
>
{({ data, change, submit, isSaveDisabled }) => (
<Container> <Container>
<Backlink onClick={onBack}> <Backlink onClick={onBack}>
{intl.formatMessage(sectionNames.apps)} {intl.formatMessage(sectionNames.apps)}
@ -96,7 +101,7 @@ const CustomAppCreatePage: React.FC<CustomAppCreatePageProps> = props => {
/> />
</Grid> </Grid>
<Savebar <Savebar
disabled={disabled || !hasChanged} disabled={isSaveDisabled}
state={saveButtonBarState} state={saveButtonBarState}
onCancel={onBack} onCancel={onBack}
onSubmit={submit} onSubmit={submit}

View file

@ -101,8 +101,13 @@ const CustomAppDetailsPage: React.FC<CustomAppDetailsPageProps> = props => {
}; };
return ( return (
<Form confirmLeave initial={initialForm} onSubmit={onSubmit}> <Form
{({ data, change, hasChanged, submit }) => ( confirmLeave
initial={initialForm}
onSubmit={onSubmit}
disabled={disabled}
>
{({ data, change, submit, isSaveDisabled }) => (
<Container> <Container>
<Backlink onClick={onBack}> <Backlink onClick={onBack}>
{intl.formatMessage(sectionNames.apps)} {intl.formatMessage(sectionNames.apps)}
@ -182,7 +187,7 @@ const CustomAppDetailsPage: React.FC<CustomAppDetailsPageProps> = props => {
</div> </div>
</Grid> </Grid>
<Savebar <Savebar
disabled={disabled || !hasChanged} disabled={isSaveDisabled}
state={saveButtonBarState} state={saveButtonBarState}
onCancel={onBack} onCancel={onBack}
onSubmit={submit} onSubmit={submit}

View file

@ -156,12 +156,17 @@ const AttributePage: React.FC<AttributePageProps> = ({
}; };
return ( return (
<Form confirmLeave initial={initialForm} onSubmit={handleSubmit}> <Form
confirmLeave
initial={initialForm}
onSubmit={handleSubmit}
disabled={disabled}
>
{({ {({
change, change,
set, set,
data, data,
hasChanged, isSaveDisabled,
submit, submit,
errors, errors,
setError, setError,
@ -239,7 +244,7 @@ const AttributePage: React.FC<AttributePageProps> = ({
</div> </div>
</Grid> </Grid>
<Savebar <Savebar
disabled={disabled || !hasChanged} disabled={isSaveDisabled}
state={saveButtonBarState} state={saveButtonBarState}
onCancel={onBack} onCancel={onBack}
onSubmit={submit} onSubmit={submit}

View file

@ -31,8 +31,8 @@ export const CategoryCreatePage: React.FC<CategoryCreatePageProps> = ({
const intl = useIntl(); const intl = useIntl();
return ( return (
<CategoryCreateForm onSubmit={onSubmit}> <CategoryCreateForm onSubmit={onSubmit} disabled={disabled}>
{({ data, change, handlers, submit, hasChanged }) => ( {({ data, change, handlers, submit, isSaveDisabled }) => (
<Container> <Container>
<Backlink onClick={onBack}> <Backlink onClick={onBack}>
{intl.formatMessage(sectionNames.categories)} {intl.formatMessage(sectionNames.categories)}
@ -74,7 +74,7 @@ export const CategoryCreatePage: React.FC<CategoryCreatePageProps> = ({
onCancel={onBack} onCancel={onBack}
onSubmit={submit} onSubmit={submit}
state={saveButtonBarState} state={saveButtonBarState}
disabled={disabled || !hasChanged} disabled={isSaveDisabled}
/> />
</div> </div>
</Container> </Container>

View file

@ -33,6 +33,7 @@ export interface UseCategoryCreateFormResult
export interface CategoryCreateFormProps { export interface CategoryCreateFormProps {
children: (props: UseCategoryCreateFormResult) => React.ReactNode; children: (props: UseCategoryCreateFormResult) => React.ReactNode;
onSubmit: (data: CategoryCreateData) => Promise<any[]>; onSubmit: (data: CategoryCreateData) => Promise<any[]>;
disabled: boolean;
} }
const initialData: CategoryCreateFormData = { const initialData: CategoryCreateFormData = {
@ -45,7 +46,8 @@ const initialData: CategoryCreateFormData = {
}; };
function useCategoryCreateForm( function useCategoryCreateForm(
onSubmit: (data: CategoryCreateData) => Promise<any[]> onSubmit: (data: CategoryCreateData) => Promise<any[]>,
disabled: boolean
): UseCategoryCreateFormResult { ): UseCategoryCreateFormResult {
const { const {
handleChange, handleChange,
@ -53,7 +55,8 @@ function useCategoryCreateForm(
hasChanged, hasChanged,
triggerChange, triggerChange,
setChanged, setChanged,
formId formId,
setIsSubmitDisabled
} = useForm(initialData, undefined, { confirmLeave: true }); } = useForm(initialData, undefined, { confirmLeave: true });
const handleFormSubmit = useHandleFormSubmit({ const handleFormSubmit = useHandleFormSubmit({
@ -87,6 +90,9 @@ function useCategoryCreateForm(
useEffect(() => setExitDialogSubmitRef(submit), [submit]); useEffect(() => setExitDialogSubmitRef(submit), [submit]);
const isSaveDisabled = disabled || !hasChanged;
setIsSubmitDisabled(isSaveDisabled);
return { return {
change: handleChange, change: handleChange,
data: getData(), data: getData(),
@ -95,15 +101,17 @@ function useCategoryCreateForm(
changeMetadata changeMetadata
}, },
hasChanged, hasChanged,
submit submit,
isSaveDisabled
}; };
} }
const CategoryCreateForm: React.FC<CategoryCreateFormProps> = ({ const CategoryCreateForm: React.FC<CategoryCreateFormProps> = ({
children, children,
onSubmit onSubmit,
disabled
}) => { }) => {
const props = useCategoryCreateForm(onSubmit); const props = useCategoryCreateForm(onSubmit, disabled);
return <form onSubmit={props.submit}>{children(props)}</form>; return <form onSubmit={props.submit}>{children(props)}</form>;
}; };

View file

@ -92,8 +92,12 @@ export const CategoryUpdatePage: React.FC<CategoryUpdatePageProps> = ({
const intl = useIntl(); const intl = useIntl();
return ( return (
<CategoryUpdateForm category={category} onSubmit={onSubmit}> <CategoryUpdateForm
{({ data, change, handlers, submit, hasChanged }) => ( category={category}
onSubmit={onSubmit}
disabled={disabled}
>
{({ data, change, handlers, submit, isSaveDisabled }) => (
<Container> <Container>
<Backlink onClick={onBack}> <Backlink onClick={onBack}>
{intl.formatMessage(sectionNames.categories)} {intl.formatMessage(sectionNames.categories)}
@ -217,7 +221,7 @@ export const CategoryUpdatePage: React.FC<CategoryUpdatePageProps> = ({
onDelete={onDelete} onDelete={onDelete}
onSubmit={submit} onSubmit={submit}
state={saveButtonBarState} state={saveButtonBarState}
disabled={disabled || !hasChanged} disabled={isSaveDisabled}
/> />
</Container> </Container>
)} )}

View file

@ -38,6 +38,7 @@ export interface CategoryUpdateFormProps {
children: (props: UseCategoryUpdateFormResult) => React.ReactNode; children: (props: UseCategoryUpdateFormResult) => React.ReactNode;
category: CategoryDetailsFragment; category: CategoryDetailsFragment;
onSubmit: (data: CategoryUpdateData) => Promise<any[]>; onSubmit: (data: CategoryUpdateData) => Promise<any[]>;
disabled: boolean;
} }
const getInitialData = (category?: CategoryDetailsFragment) => ({ const getInitialData = (category?: CategoryDetailsFragment) => ({
@ -52,7 +53,8 @@ const getInitialData = (category?: CategoryDetailsFragment) => ({
function useCategoryUpdateForm( function useCategoryUpdateForm(
category: CategoryDetailsFragment, category: CategoryDetailsFragment,
onSubmit: (data: CategoryUpdateData) => Promise<any[]> onSubmit: (data: CategoryUpdateData) => Promise<any[]>,
disabled: boolean
): UseCategoryUpdateFormResult { ): UseCategoryUpdateFormResult {
const { const {
handleChange, handleChange,
@ -60,7 +62,8 @@ function useCategoryUpdateForm(
triggerChange, triggerChange,
hasChanged, hasChanged,
setChanged, setChanged,
formId formId,
setIsSubmitDisabled
} = useForm(getInitialData(category), undefined, { confirmLeave: true }); } = useForm(getInitialData(category), undefined, { confirmLeave: true });
const handleFormSubmit = useHandleFormSubmit({ const handleFormSubmit = useHandleFormSubmit({
@ -101,6 +104,9 @@ function useCategoryUpdateForm(
useEffect(() => setExitDialogSubmitRef(submit), [submit]); useEffect(() => setExitDialogSubmitRef(submit), [submit]);
const isSaveDisabled = disabled || !hasChanged;
setIsSubmitDisabled(isSaveDisabled);
return { return {
change: handleChange, change: handleChange,
data: getData(), data: getData(),
@ -109,16 +115,18 @@ function useCategoryUpdateForm(
changeMetadata changeMetadata
}, },
hasChanged, hasChanged,
submit submit,
isSaveDisabled
}; };
} }
const CategoryUpdateForm: React.FC<CategoryUpdateFormProps> = ({ const CategoryUpdateForm: React.FC<CategoryUpdateFormProps> = ({
children, children,
category, category,
onSubmit onSubmit,
disabled
}) => { }) => {
const props = useCategoryUpdateForm(category, onSubmit); const props = useCategoryUpdateForm(category, onSubmit, disabled);
return <form onSubmit={props.submit}>{children(props)}</form>; return <form onSubmit={props.submit}>{children(props)}</form>;
}; };

View file

@ -1,6 +1,6 @@
import ShippingZonesCard from "@saleor/channels/components/ShippingZonesCard/ShippingZonesCard"; import ShippingZonesCard from "@saleor/channels/components/ShippingZonesCard/ShippingZonesCard";
import CardSpacer from "@saleor/components/CardSpacer"; 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 Grid from "@saleor/components/Grid";
import Savebar from "@saleor/components/Savebar"; import Savebar from "@saleor/components/Savebar";
import { SingleAutocompleteChoiceType } from "@saleor/components/SingleAutocompleteSelectField"; import { SingleAutocompleteChoiceType } from "@saleor/components/SingleAutocompleteSelectField";
@ -94,9 +94,25 @@ const ChannelDetailsPage = function<TErrors>({
!shippingZonesToDisplay.some(({ id }) => id === searchedZoneId) !shippingZonesToDisplay.some(({ id }) => id === searchedZoneId)
); );
const checkIfSaveIsDisabled = (data: FormDataWithOpts<FormData>) => {
const formDisabled =
!data.name ||
!data.slug ||
!data.currencyCode ||
!data.defaultCountry ||
!(data.name.trim().length > 0);
return disabled || formDisabled || !data.hasChanged;
};
return ( return (
<Form confirmLeave onSubmit={onSubmit} initial={initialData}> <Form
{({ change, data, hasChanged, submit, set }) => { confirmLeave
onSubmit={onSubmit}
initial={initialData}
checkIfSaveIsDisabled={checkIfSaveIsDisabled}
>
{({ change, data, submit, set, isSaveDisabled }) => {
const handleCurrencyCodeSelect = createSingleAutocompleteSelectHandler( const handleCurrencyCodeSelect = createSingleAutocompleteSelectHandler(
change, change,
setSelectedCurrencyCode, setSelectedCurrencyCode,
@ -147,13 +163,6 @@ const ChannelDetailsPage = function<TErrors>({
); );
}; };
const formDisabled =
!data.name ||
!data.slug ||
!data.currencyCode ||
!data.defaultCountry ||
!(data.name.trim().length > 0);
return ( return (
<> <>
<Grid> <Grid>
@ -197,7 +206,7 @@ const ChannelDetailsPage = function<TErrors>({
onSubmit={submit} onSubmit={submit}
onDelete={onDelete} onDelete={onDelete}
state={saveButtonBarState} state={saveButtonBarState}
disabled={disabled || formDisabled || !onSubmit || !hasChanged} disabled={isSaveDisabled}
/> />
</> </>
); );

View file

@ -54,8 +54,9 @@ const CollectionCreatePage: React.FC<CollectionCreatePageProps> = ({
onSubmit={onSubmit} onSubmit={onSubmit}
currentChannels={currentChannels} currentChannels={currentChannels}
setChannels={onChannelsChange} setChannels={onChannelsChange}
disabled={disabled}
> >
{({ change, data, handlers, hasChanged, submit }) => ( {({ change, data, handlers, submit, isSaveDisabled }) => (
<Container> <Container>
<Backlink onClick={onBack}> <Backlink onClick={onBack}>
{intl.formatMessage(sectionNames.collections)} {intl.formatMessage(sectionNames.collections)}
@ -156,7 +157,7 @@ const CollectionCreatePage: React.FC<CollectionCreatePageProps> = ({
</Grid> </Grid>
<Savebar <Savebar
state={saveButtonBarState} state={saveButtonBarState}
disabled={disabled || !hasChanged} disabled={isSaveDisabled}
onCancel={onBack} onCancel={onBack}
onSubmit={submit} onSubmit={submit}
/> />

View file

@ -49,6 +49,7 @@ export interface CollectionCreateFormProps {
setChannels: (data: ChannelCollectionData[]) => void; setChannels: (data: ChannelCollectionData[]) => void;
children: (props: UseCollectionCreateFormResult) => React.ReactNode; children: (props: UseCollectionCreateFormResult) => React.ReactNode;
onSubmit: (data: CollectionCreateData) => SubmitPromise; onSubmit: (data: CollectionCreateData) => SubmitPromise;
disabled: boolean;
} }
const getInitialData = ( const getInitialData = (
@ -71,7 +72,8 @@ const getInitialData = (
function useCollectionCreateForm( function useCollectionCreateForm(
currentChannels: ChannelCollectionData[], currentChannels: ChannelCollectionData[],
setChannels: (data: ChannelCollectionData[]) => void, setChannels: (data: ChannelCollectionData[]) => void,
onSubmit: (data: CollectionCreateData) => SubmitPromise onSubmit: (data: CollectionCreateData) => SubmitPromise,
disabled: boolean
): UseCollectionCreateFormResult { ): UseCollectionCreateFormResult {
const { const {
handleChange, handleChange,
@ -79,7 +81,8 @@ function useCollectionCreateForm(
triggerChange, triggerChange,
setChanged, setChanged,
hasChanged, hasChanged,
formId formId,
setIsSubmitDisabled
} = useForm(getInitialData(currentChannels), undefined, { } = useForm(getInitialData(currentChannels), undefined, {
confirmLeave: true, confirmLeave: true,
formId: COLLECTION_CREATE_FORM_ID formId: COLLECTION_CREATE_FORM_ID
@ -122,6 +125,9 @@ function useCollectionCreateForm(
useEffect(() => setExitDialogSubmitRef(submit), [submit]); useEffect(() => setExitDialogSubmitRef(submit), [submit]);
const isSaveDisabled = disabled || !hasChanged;
setIsSubmitDisabled(isSaveDisabled);
return { return {
change: handleChange, change: handleChange,
data: getData(), data: getData(),
@ -131,7 +137,8 @@ function useCollectionCreateForm(
changeMetadata changeMetadata
}, },
hasChanged, hasChanged,
submit submit,
isSaveDisabled
}; };
} }
@ -139,9 +146,15 @@ const CollectionCreateForm: React.FC<CollectionCreateFormProps> = ({
currentChannels, currentChannels,
setChannels, setChannels,
children, children,
onSubmit onSubmit,
disabled
}) => { }) => {
const props = useCollectionCreateForm(currentChannels, setChannels, onSubmit); const props = useCollectionCreateForm(
currentChannels,
setChannels,
onSubmit,
disabled
);
return <form onSubmit={props.submit}>{children(props)}</form>; return <form onSubmit={props.submit}>{children(props)}</form>;
}; };

View file

@ -73,8 +73,10 @@ const CollectionDetailsPage: React.FC<CollectionDetailsPageProps> = ({
currentChannels={currentChannels} currentChannels={currentChannels}
setChannels={onChannelsChange} setChannels={onChannelsChange}
onSubmit={onSubmit} onSubmit={onSubmit}
disabled={disabled}
hasChannelChanged={hasChannelChanged}
> >
{({ change, data, handlers, hasChanged, submit }) => ( {({ change, data, handlers, submit, isSaveDisabled }) => (
<Container> <Container>
<Backlink onClick={onBack}> <Backlink onClick={onBack}>
{intl.formatMessage(sectionNames.collections)} {intl.formatMessage(sectionNames.collections)}
@ -150,7 +152,7 @@ const CollectionDetailsPage: React.FC<CollectionDetailsPageProps> = ({
</Grid> </Grid>
<Savebar <Savebar
state={saveButtonBarState} state={saveButtonBarState}
disabled={disabled || (!hasChanged && !hasChannelChanged)} disabled={isSaveDisabled}
onCancel={onBack} onCancel={onBack}
onDelete={onCollectionRemove} onDelete={onCollectionRemove}
onSubmit={submit} onSubmit={submit}

View file

@ -48,6 +48,8 @@ export interface CollectionUpdateFormProps {
currentChannels: ChannelCollectionData[]; currentChannels: ChannelCollectionData[];
setChannels: (data: ChannelCollectionData[]) => void; setChannels: (data: ChannelCollectionData[]) => void;
onSubmit: (data: CollectionUpdateData) => Promise<any[]>; onSubmit: (data: CollectionUpdateData) => Promise<any[]>;
disabled: boolean;
hasChannelChanged: boolean;
} }
const getInitialData = ( const getInitialData = (
@ -68,7 +70,9 @@ function useCollectionUpdateForm(
collection: CollectionDetailsFragment, collection: CollectionDetailsFragment,
currentChannels: ChannelCollectionData[], currentChannels: ChannelCollectionData[],
setChannels: (data: ChannelCollectionData[]) => void, setChannels: (data: ChannelCollectionData[]) => void,
onSubmit: (data: CollectionUpdateData) => Promise<any[]> onSubmit: (data: CollectionUpdateData) => Promise<any[]>,
disabled: boolean,
hasChannelChanged: boolean
): UseCollectionUpdateFormResult { ): UseCollectionUpdateFormResult {
const { const {
handleChange, handleChange,
@ -76,7 +80,8 @@ function useCollectionUpdateForm(
triggerChange, triggerChange,
setChanged, setChanged,
hasChanged, hasChanged,
formId formId,
setIsSubmitDisabled
} = useForm(getInitialData(collection, currentChannels), undefined, { } = useForm(getInitialData(collection, currentChannels), undefined, {
confirmLeave: true, confirmLeave: true,
formId: COLLECTION_DETAILS_FORM_ID formId: COLLECTION_DETAILS_FORM_ID
@ -126,6 +131,9 @@ function useCollectionUpdateForm(
useEffect(() => setExitDialogSubmitRef(submit), [submit]); useEffect(() => setExitDialogSubmitRef(submit), [submit]);
const isSaveDisabled = disabled || (!hasChanged && !hasChannelChanged);
setIsSubmitDisabled(isSaveDisabled);
return { return {
change: handleChange, change: handleChange,
data: getData(), data: getData(),
@ -135,7 +143,8 @@ function useCollectionUpdateForm(
changeMetadata changeMetadata
}, },
hasChanged, hasChanged,
submit submit,
isSaveDisabled
}; };
} }
@ -144,13 +153,17 @@ const CollectionUpdateForm: React.FC<CollectionUpdateFormProps> = ({
currentChannels, currentChannels,
setChannels, setChannels,
children, children,
onSubmit onSubmit,
disabled,
hasChannelChanged
}) => { }) => {
const props = useCollectionUpdateForm( const props = useCollectionUpdateForm(
collection, collection,
currentChannels, currentChannels,
setChannels, setChannels,
onSubmit onSubmit,
disabled,
hasChannelChanged
); );
return <form onSubmit={props.submit}>{children(props)}</form>; return <form onSubmit={props.submit}>{children(props)}</form>;

View file

@ -6,8 +6,8 @@ import {
ProductErrorWithAttributesFragment ProductErrorWithAttributesFragment
} from "@saleor/graphql"; } from "@saleor/graphql";
import { commonMessages } from "@saleor/intl"; import { commonMessages } from "@saleor/intl";
import { DateTime, joinDateTime, splitDateTime } from "@saleor/misc"; import { joinDateTime, splitDateTime } from "@saleor/misc";
import React, { useEffect, useState } from "react"; import React from "react";
import { useIntl } from "react-intl"; import { useIntl } from "react-intl";
type DateTimeFieldProps = Omit<TextFieldProps, "label" | "error"> & { type DateTimeFieldProps = Omit<TextFieldProps, "label" | "error"> & {
@ -21,14 +21,11 @@ export const DateTimeField: React.FC<DateTimeFieldProps> = ({
error, error,
name, name,
onChange, onChange,
value: initialValue value
}) => { }) => {
const intl = useIntl(); const intl = useIntl();
const [value, setValue] = useState<DateTime>(
initialValue ? splitDateTime(initialValue) : { date: "", time: "" }
);
useEffect(() => onChange(joinDateTime(value.date, value.time)), [value]); const parsedValue = value ? splitDateTime(value) : { date: "", time: "" };
return ( return (
<> <>
@ -41,10 +38,11 @@ export const DateTimeField: React.FC<DateTimeFieldProps> = ({
name={`${name}:date`} name={`${name}:date`}
onChange={event => { onChange={event => {
const date = event.target.value; const date = event.target.value;
setValue(value => ({ ...value, date }));
onChange(joinDateTime(date, parsedValue.time));
}} }}
type="date" type="date"
value={value.date} value={parsedValue.date}
InputLabelProps={{ shrink: true }} InputLabelProps={{ shrink: true }}
/> />
<TextField <TextField
@ -56,10 +54,11 @@ export const DateTimeField: React.FC<DateTimeFieldProps> = ({
name={`${name}:time`} name={`${name}:time`}
onChange={event => { onChange={event => {
const time = event.target.value; const time = event.target.value;
setValue(value => ({ ...value, time }));
onChange(joinDateTime(parsedValue.date, time));
}} }}
type="time" type="time"
value={value.time} value={parsedValue.time}
InputLabelProps={{ shrink: true }} InputLabelProps={{ shrink: true }}
/> />
</> </>

View file

@ -1,9 +1,8 @@
import { Button, Dialog, DialogContent, makeStyles } from "@material-ui/core"; import { Button, Dialog, DialogContent, makeStyles } from "@material-ui/core";
import HorizontalSpacer from "@saleor/apps/components/HorizontalSpacer"; import HorizontalSpacer from "@saleor/apps/components/HorizontalSpacer";
import CardSpacer from "@saleor/components/CardSpacer";
import CardTitle from "@saleor/components/CardTitle"; import CardTitle from "@saleor/components/CardTitle";
import React from "react"; import React from "react";
import { FormattedMessage, useIntl } from "react-intl"; import { useIntl } from "react-intl";
import { exitFormPromptMessages as messages } from "./messages"; import { exitFormPromptMessages as messages } from "./messages";
@ -19,6 +18,12 @@ const useStyles = makeStyles(
buttonsContainer: { buttonsContainer: {
display: "flex", display: "flex",
justifyContent: "flex-end" justifyContent: "flex-end"
},
dialogContent: {
"@media (min-width: 800px)": {
minWidth: 500
},
paddingTop: 0
} }
}), }),
{ name: "ExitFormPrompt" } { name: "ExitFormPrompt" }
@ -29,24 +34,27 @@ interface ExitFormDialogProps {
onClose: () => void; onClose: () => void;
onLeave: () => void; onLeave: () => void;
isOpen: boolean; isOpen: boolean;
isSubmitDisabled: boolean;
} }
const ExitFormDialog: React.FC<ExitFormDialogProps> = ({ const ExitFormDialog: React.FC<ExitFormDialogProps> = ({
onSubmit, onSubmit,
onLeave, onLeave,
onClose, onClose,
isOpen isOpen,
isSubmitDisabled
}) => { }) => {
const classes = useStyles(); const classes = useStyles();
const intl = useIntl(); const intl = useIntl();
return ( return (
<Dialog className={classes.container} open={isOpen}> <Dialog className={classes.container} open={isOpen} onClose={onClose}>
<CardTitle title={intl.formatMessage(messages.title)} onClose={onClose} /> <CardTitle
<DialogContent> title={intl.formatMessage(
<FormattedMessage {...messages.description} /> isSubmitDisabled ? messages.unableToSaveTitle : messages.title
<CardSpacer /> )}
<CardSpacer /> />
<DialogContent className={classes.dialogContent}>
<div className={classes.buttonsContainer}> <div className={classes.buttonsContainer}>
<Button onClick={onLeave}> <Button onClick={onLeave}>
{intl.formatMessage(messages.cancelButton)} {intl.formatMessage(messages.cancelButton)}
@ -55,10 +63,14 @@ const ExitFormDialog: React.FC<ExitFormDialogProps> = ({
<Button <Button
variant="contained" variant="contained"
color="primary" color="primary"
onClick={onSubmit} onClick={isSubmitDisabled ? onClose : onSubmit}
data-test-id="save-and-continue" data-test-id="save-and-continue"
> >
{intl.formatMessage(messages.confirmButton)} {intl.formatMessage(
isSubmitDisabled
? messages.continueEditingButton
: messages.confirmButton
)}
</Button> </Button>
</div> </div>
</DialogContent> </DialogContent>

View file

@ -1,9 +1,11 @@
import { SubmitPromise } from "@saleor/hooks/useForm"; import { SubmitPromise } from "@saleor/hooks/useForm";
import { isInDevelopment } from "@saleor/misc";
import React, { useEffect, useRef, useState } from "react"; import React, { useEffect, useRef, useState } from "react";
import { useHistory } from "react-router"; import { useHistory } from "react-router";
import useRouter from "use-react-router"; import useRouter from "use-react-router";
import ExitFormDialog from "./ExitFormDialog"; import ExitFormDialog from "./ExitFormDialog";
import useBeforeUnload from "./useBeforeUnload";
export interface ExitFormDialogData { export interface ExitFormDialogData {
setIsDirty: (id: symbol, isDirty: boolean) => void; setIsDirty: (id: symbol, isDirty: boolean) => void;
@ -12,6 +14,7 @@ export interface ExitFormDialogData {
shouldBlockNavigation: () => boolean; shouldBlockNavigation: () => boolean;
setIsSubmitting: (value: boolean) => void; setIsSubmitting: (value: boolean) => void;
submit: () => SubmitPromise; submit: () => SubmitPromise;
setIsSubmitDisabled: (value: boolean) => void;
} }
export type SubmitFn = (dataOrEvent?: any) => SubmitPromise<any[]>; export type SubmitFn = (dataOrEvent?: any) => SubmitPromise<any[]>;
@ -37,7 +40,8 @@ export const ExitFormDialogContext = React.createContext<ExitFormDialogData>({
setExitDialogSubmitRef: () => undefined, setExitDialogSubmitRef: () => undefined,
shouldBlockNavigation: () => false, shouldBlockNavigation: () => false,
setIsSubmitting: () => undefined, setIsSubmitting: () => undefined,
submit: () => Promise.resolve([]) submit: () => Promise.resolve([]),
setIsSubmitDisabled: () => undefined
}); });
const defaultValues = { const defaultValues = {
@ -56,6 +60,11 @@ export function useExitFormDialogProvider() {
const { history: routerHistory } = useRouter(); const { history: routerHistory } = useRouter();
const [showDialog, setShowDialog] = useState(defaultValues.showDialog); const [showDialog, setShowDialog] = useState(defaultValues.showDialog);
const isSubmitDisabled = useRef(false);
const setIsSubmitDisabled = (status: boolean) => {
isSubmitDisabled.current = status;
};
const isSubmitting = useRef(defaultValues.isSubmitting); const isSubmitting = useRef(defaultValues.isSubmitting);
const formsData = useRef<FormsData>({}); const formsData = useRef<FormsData>({});
@ -209,10 +218,11 @@ export function useExitFormDialogProvider() {
const errors = await Promise.all( const errors = await Promise.all(
getDirtyFormsSubmitFn().map(submitFn => submitFn()) getDirtyFormsSubmitFn().map(submitFn => submitFn())
); );
const isError = errors.flat().some(errors => errors);
setIsSubmitting(false); setIsSubmitting(false);
const isError = errors.flat().some(errors => errors);
if (!isError) { if (!isError) {
continueNavigation(); continueNavigation();
} }
@ -239,7 +249,8 @@ export function useExitFormDialogProvider() {
setEnableExitDialog, setEnableExitDialog,
setExitDialogSubmitRef: setSubmitRef, setExitDialogSubmitRef: setSubmitRef,
setIsSubmitting, setIsSubmitting,
submit: handleSubmit submit: handleSubmit,
setIsSubmitDisabled
}; };
return { return {
@ -247,7 +258,9 @@ export function useExitFormDialogProvider() {
showDialog, showDialog,
handleSubmit, handleSubmit,
handleLeave, handleLeave,
handleClose handleClose,
shouldBlockNav,
isSubmitDisabled
}; };
} }
@ -257,9 +270,20 @@ const ExitFormDialogProvider = ({ children }) => {
handleLeave, handleLeave,
handleSubmit, handleSubmit,
providerData, providerData,
showDialog showDialog,
shouldBlockNav,
isSubmitDisabled
} = useExitFormDialogProvider(); } = 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 ( return (
<ExitFormDialogContext.Provider value={providerData}> <ExitFormDialogContext.Provider value={providerData}>
<ExitFormDialog <ExitFormDialog
@ -267,6 +291,7 @@ const ExitFormDialogProvider = ({ children }) => {
onSubmit={handleSubmit} onSubmit={handleSubmit}
onLeave={handleLeave} onLeave={handleLeave}
onClose={handleClose} onClose={handleClose}
isSubmitDisabled={isSubmitDisabled.current}
/> />
{children} {children}
</ExitFormDialogContext.Provider> </ExitFormDialogContext.Provider>

View file

@ -3,6 +3,13 @@ import React from "react";
import { FormId } from "./ExitFormDialogProvider"; import { FormId } from "./ExitFormDialogProvider";
export type FormDataWithOpts<TData> = TData &
Pick<UseFormResult<TData>, "hasChanged">;
export type CheckIfSaveIsDisabledFnType<T> = (
data: FormDataWithOpts<T>
) => boolean;
export interface FormProps<TData, TErrors> export interface FormProps<TData, TErrors>
extends Omit<React.HTMLProps<HTMLFormElement>, "onSubmit"> { extends Omit<React.HTMLProps<HTMLFormElement>, "onSubmit"> {
children: (props: UseFormResult<TData>) => React.ReactNode; children: (props: UseFormResult<TData>) => React.ReactNode;
@ -11,6 +18,7 @@ export interface FormProps<TData, TErrors>
resetOnSubmit?: boolean; resetOnSubmit?: boolean;
onSubmit?: (data: TData) => SubmitPromise<TErrors[]> | void; onSubmit?: (data: TData) => SubmitPromise<TErrors[]> | void;
formId?: FormId; formId?: FormId;
checkIfSaveIsDisabled?: CheckIfSaveIsDisabledFnType<TData>;
} }
function Form<TData, Terrors>({ function Form<TData, Terrors>({
@ -20,9 +28,16 @@ function Form<TData, Terrors>({
onSubmit, onSubmit,
confirmLeave = false, confirmLeave = false,
formId, formId,
checkIfSaveIsDisabled,
disabled,
...rest ...rest
}: FormProps<TData, Terrors>) { }: FormProps<TData, Terrors>) {
const renderProps = useForm(initial, onSubmit, { confirmLeave, formId }); const renderProps = useForm(initial, onSubmit, {
confirmLeave,
formId,
checkIfSaveIsDisabled,
disabled
});
function handleSubmit(event?: React.FormEvent<any>, cb?: () => void) { function handleSubmit(event?: React.FormEvent<any>, cb?: () => void) {
const { reset, submit } = renderProps; const { reset, submit } = renderProps;

View file

@ -2,20 +2,23 @@ import { defineMessages } from "react-intl";
export const exitFormPromptMessages = defineMessages({ export const exitFormPromptMessages = defineMessages({
title: { title: {
defaultMessage: "Are you sure you want to leave?", defaultMessage: "Would you like to save changes?",
description: "ExitFormPrompt title" description: "ExitFormPrompt title"
}, },
description: { unableToSaveTitle: {
defaultMessage: defaultMessage: "You have unsaved changes",
"You have unsaved changes on this view. What would you like to do with them?", description: "ExitFormPrompt title"
description: "ExitFormPrompt description"
}, },
cancelButton: { cancelButton: {
defaultMessage: "leave without saving", defaultMessage: "Discard changes",
description: "ExitFormPrompt cancel button" description: "ExitFormPrompt cancel button"
}, },
confirmButton: { confirmButton: {
defaultMessage: "save & continue", defaultMessage: "Save changes",
description: "ExitFormPrompt confirm button" description: "ExitFormPrompt confirm button"
},
continueEditingButton: {
defaultMessage: "Continue editing",
description: "ExitFormPrompt continue editing button"
} }
}); });

View file

@ -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;

View file

@ -1,4 +1,4 @@
import { useContext, useRef } from "react"; import React, { useContext, useRef } from "react";
import { import {
ExitFormDialogContext, ExitFormDialogContext,
@ -8,7 +8,14 @@ import {
} from "./ExitFormDialogProvider"; } from "./ExitFormDialogProvider";
export interface UseExitFormDialogResult export interface UseExitFormDialogResult
extends Omit<ExitFormDialogData, "setIsDirty" | "setExitDialogSubmitRef">, extends Pick<
ExitFormDialogData,
| "setEnableExitDialog"
| "shouldBlockNavigation"
| "setIsSubmitting"
| "setIsSubmitDisabled"
| "submit"
>,
WithFormId { WithFormId {
setIsDirty: (isDirty: boolean) => void; setIsDirty: (isDirty: boolean) => void;
setExitDialogSubmitRef: (submitFn: SubmitFn) => void; setExitDialogSubmitRef: (submitFn: SubmitFn) => void;
@ -16,19 +23,29 @@ export interface UseExitFormDialogResult
export interface UseExitFormDialogProps { export interface UseExitFormDialogProps {
formId: symbol; formId: symbol;
isDisabled?: boolean;
} }
export const useExitFormDialog = ( export const useExitFormDialog = (
{ formId }: UseExitFormDialogProps = { formId: undefined } { formId, isDisabled }: UseExitFormDialogProps = { formId: undefined }
): UseExitFormDialogResult => { ): UseExitFormDialogResult => {
const id = useRef(formId || Symbol()).current; const id = useRef(formId || Symbol()).current;
const { setIsDirty, setExitDialogSubmitRef, ...rest } = useContext( const exitDialogProps = useContext(ExitFormDialogContext);
ExitFormDialogContext const {
); setIsDirty,
setIsSubmitDisabled,
setExitDialogSubmitRef
} = exitDialogProps;
React.useEffect(() => {
if (isDisabled !== undefined) {
setIsSubmitDisabled(isDisabled);
}
}, [isDisabled]);
return { return {
...rest, ...exitDialogProps,
formId: id, formId: id,
setIsDirty: (value: boolean) => setIsDirty(id, value), setIsDirty: (value: boolean) => setIsDirty(id, value),
setExitDialogSubmitRef: (submitFn: SubmitFn) => setExitDialogSubmitRef: (submitFn: SubmitFn) =>

View file

@ -135,8 +135,13 @@ const CustomerCreatePage: React.FC<CustomerCreatePageProps> = ({
}; };
return ( return (
<Form confirmLeave initial={initialForm} onSubmit={handleSubmit}> <Form
{({ change, data, hasChanged, submit }) => { confirmLeave
initial={initialForm}
onSubmit={handleSubmit}
disabled={disabled}
>
{({ change, data, isSaveDisabled, submit }) => {
const handleCountrySelect = createSingleAutocompleteSelectHandler( const handleCountrySelect = createSingleAutocompleteSelectHandler(
change, change,
setCountryDisplayName, setCountryDisplayName,
@ -182,7 +187,7 @@ const CustomerCreatePage: React.FC<CustomerCreatePageProps> = ({
</div> </div>
</Grid> </Grid>
<Savebar <Savebar
disabled={disabled || !hasChanged} disabled={isSaveDisabled}
state={saveButtonBar} state={saveButtonBar}
onSubmit={submit} onSubmit={submit}
onCancel={onBack} onCancel={onBack}

View file

@ -80,8 +80,13 @@ const CustomerDetailsPage: React.FC<CustomerDetailsPageProps> = ({
} = useMetadataChangeTrigger(); } = useMetadataChangeTrigger();
return ( return (
<Form confirmLeave initial={initialForm} onSubmit={onSubmit}> <Form
{({ change, data, hasChanged, submit }) => { confirmLeave
initial={initialForm}
onSubmit={onSubmit}
disabled={disabled}
>
{({ change, data, isSaveDisabled, submit }) => {
const changeMetadata = makeMetadataChangeHandler(change); const changeMetadata = makeMetadataChangeHandler(change);
return ( return (
@ -136,7 +141,7 @@ const CustomerDetailsPage: React.FC<CustomerDetailsPageProps> = ({
</div> </div>
</Grid> </Grid>
<Savebar <Savebar
disabled={disabled || !hasChanged} disabled={isSaveDisabled}
state={saveButtonBar} state={saveButtonBar}
onSubmit={submit} onSubmit={submit}
onCancel={onBack} onCancel={onBack}

View file

@ -2,7 +2,7 @@ import { validateSalePrice } from "@saleor/channels/utils";
import CardSpacer from "@saleor/components/CardSpacer"; import CardSpacer from "@saleor/components/CardSpacer";
import ChannelsAvailabilityCard from "@saleor/components/ChannelsAvailabilityCard"; import ChannelsAvailabilityCard from "@saleor/components/ChannelsAvailabilityCard";
import Container from "@saleor/components/Container"; import Container from "@saleor/components/Container";
import Form from "@saleor/components/Form"; import Form, { FormDataWithOpts } from "@saleor/components/Form";
import Grid from "@saleor/components/Grid"; import Grid from "@saleor/components/Grid";
import Metadata, { MetadataFormData } from "@saleor/components/Metadata"; import Metadata, { MetadataFormData } from "@saleor/components/Metadata";
import PageHeader from "@saleor/components/PageHeader"; import PageHeader from "@saleor/components/PageHeader";
@ -81,23 +81,26 @@ const SaleCreatePage: React.FC<SaleCreatePageProps> = ({
privateMetadata: [] privateMetadata: []
}; };
const checkIfSaveIsDisabled = (data: FormDataWithOpts<FormData>) =>
data.channelListings?.some(channel => validateSalePrice(data, channel)) ||
disabled ||
!data.hasChanged;
return ( return (
<Form <Form
confirmLeave confirmLeave
initial={initialForm} initial={initialForm}
onSubmit={onSubmit} onSubmit={onSubmit}
formId={SALE_CREATE_FORM_ID} formId={SALE_CREATE_FORM_ID}
checkIfSaveIsDisabled={checkIfSaveIsDisabled}
> >
{({ change, data, hasChanged, submit, triggerChange }) => { {({ change, data, submit, triggerChange, isSaveDisabled }) => {
const handleChannelChange = createSaleChannelsChangeHandler( const handleChannelChange = createSaleChannelsChangeHandler(
data.channelListings, data.channelListings,
onChannelsChange, onChannelsChange,
triggerChange, triggerChange,
data.type data.type
); );
const formDisabled = data.channelListings?.some(channel =>
validateSalePrice(data, channel)
);
const changeMetadata = makeMetadataChangeHandler(change); const changeMetadata = makeMetadataChangeHandler(change);
return ( return (
@ -152,7 +155,7 @@ const SaleCreatePage: React.FC<SaleCreatePageProps> = ({
<Metadata data={data} onChange={changeMetadata} /> <Metadata data={data} onChange={changeMetadata} />
</Grid> </Grid>
<Savebar <Savebar
disabled={disabled || formDisabled || !hasChanged} disabled={isSaveDisabled}
onCancel={onBack} onCancel={onBack}
onSubmit={submit} onSubmit={submit}
state={saveButtonBarState} state={saveButtonBarState}

View file

@ -2,7 +2,7 @@ import { ChannelSaleData, validateSalePrice } from "@saleor/channels/utils";
import CardSpacer from "@saleor/components/CardSpacer"; import CardSpacer from "@saleor/components/CardSpacer";
import ChannelsAvailabilityCard from "@saleor/components/ChannelsAvailabilityCard"; import ChannelsAvailabilityCard from "@saleor/components/ChannelsAvailabilityCard";
import Container from "@saleor/components/Container"; import Container from "@saleor/components/Container";
import Form from "@saleor/components/Form"; import Form, { FormDataWithOpts } from "@saleor/components/Form";
import Grid from "@saleor/components/Grid"; import Grid from "@saleor/components/Grid";
import Metadata, { MetadataFormData } from "@saleor/components/Metadata"; import Metadata, { MetadataFormData } from "@saleor/components/Metadata";
import PageHeader from "@saleor/components/PageHeader"; import PageHeader from "@saleor/components/PageHeader";
@ -156,23 +156,29 @@ const SaleDetailsPage: React.FC<SaleDetailsPageProps> = ({
metadata: sale?.metadata.map(mapMetadataItemToInput), metadata: sale?.metadata.map(mapMetadataItemToInput),
privateMetadata: sale?.privateMetadata.map(mapMetadataItemToInput) privateMetadata: sale?.privateMetadata.map(mapMetadataItemToInput)
}; };
const checkIfSaveIsDisabled = (
data: FormDataWithOpts<SaleDetailsPageFormData>
) =>
data.channelListings?.some(channel => validateSalePrice(data, channel)) ||
disabled ||
(!data.hasChanged && !hasChannelChanged);
return ( return (
<Form <Form
confirmLeave confirmLeave
initial={initialForm} initial={initialForm}
onSubmit={onSubmit} onSubmit={onSubmit}
formId={SALE_UPDATE_FORM_ID} formId={SALE_UPDATE_FORM_ID}
checkIfSaveIsDisabled={checkIfSaveIsDisabled}
> >
{({ change, data, hasChanged, submit, triggerChange }) => { {({ change, data, submit, triggerChange, isSaveDisabled }) => {
const handleChannelChange = createSaleChannelsChangeHandler( const handleChannelChange = createSaleChannelsChangeHandler(
data.channelListings, data.channelListings,
onChannelsChange, onChannelsChange,
triggerChange, triggerChange,
data.type data.type
); );
const formDisabled = data.channelListings?.some(channel =>
validateSalePrice(data, channel)
);
const changeMetadata = makeMetadataChangeHandler(change); const changeMetadata = makeMetadataChangeHandler(change);
return ( return (
@ -370,9 +376,7 @@ const SaleDetailsPage: React.FC<SaleDetailsPageProps> = ({
<Metadata data={data} onChange={changeMetadata} /> <Metadata data={data} onChange={changeMetadata} />
</Grid> </Grid>
<Savebar <Savebar
disabled={ disabled={isSaveDisabled}
disabled || formDisabled || (!hasChanged && !hasChannelChanged)
}
onCancel={onBack} onCancel={onBack}
onDelete={onRemove} onDelete={onRemove}
onSubmit={submit} onSubmit={submit}

View file

@ -2,7 +2,7 @@ import { ChannelVoucherData } from "@saleor/channels/utils";
import CardSpacer from "@saleor/components/CardSpacer"; import CardSpacer from "@saleor/components/CardSpacer";
import ChannelsAvailabilityCard from "@saleor/components/ChannelsAvailabilityCard"; import ChannelsAvailabilityCard from "@saleor/components/ChannelsAvailabilityCard";
import Container from "@saleor/components/Container"; import Container from "@saleor/components/Container";
import Form from "@saleor/components/Form"; import Form, { FormDataWithOpts } from "@saleor/components/Form";
import Grid from "@saleor/components/Grid"; import Grid from "@saleor/components/Grid";
import Metadata from "@saleor/components/Metadata"; import Metadata from "@saleor/components/Metadata";
import PageHeader from "@saleor/components/PageHeader"; import PageHeader from "@saleor/components/PageHeader";
@ -91,14 +91,26 @@ const VoucherCreatePage: React.FC<VoucherCreatePageProps> = ({
privateMetadata: [] privateMetadata: []
}; };
const checkIfSaveIsDisabled = (data: FormDataWithOpts<FormData>) =>
(data.discountType.toString() !== "SHIPPING" &&
data.channelListings?.some(
channel =>
validatePrice(channel.discountValue) ||
(data.requirementsPicker === RequirementsPicker.ORDER &&
validatePrice(channel.minSpent))
)) ||
disabled ||
(!data.hasChanged && hasChannelChanged);
return ( return (
<Form <Form
confirmLeave confirmLeave
initial={initialForm} initial={initialForm}
onSubmit={onSubmit} onSubmit={onSubmit}
formId={VOUCHER_CREATE_FORM_ID} formId={VOUCHER_CREATE_FORM_ID}
checkIfSaveIsDisabled={checkIfSaveIsDisabled}
> >
{({ change, data, hasChanged, submit, triggerChange, set }) => { {({ change, data, submit, triggerChange, set, isSaveDisabled }) => {
const handleDiscountTypeChange = createDiscountTypeChangeHandler( const handleDiscountTypeChange = createDiscountTypeChangeHandler(
change change
); );
@ -107,14 +119,6 @@ const VoucherCreatePage: React.FC<VoucherCreatePageProps> = ({
onChannelsChange, onChannelsChange,
triggerChange 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); const changeMetadata = makeMetadataChangeHandler(change);
return ( return (
@ -199,9 +203,7 @@ const VoucherCreatePage: React.FC<VoucherCreatePageProps> = ({
<Metadata data={data} onChange={changeMetadata} /> <Metadata data={data} onChange={changeMetadata} />
</Grid> </Grid>
<Savebar <Savebar
disabled={ disabled={isSaveDisabled}
disabled || formDisabled || (!hasChanged && !hasChannelChanged)
}
onCancel={onBack} onCancel={onBack}
onSubmit={submit} onSubmit={submit}
state={saveButtonBarState} state={saveButtonBarState}

View file

@ -1,3 +1,4 @@
import { CheckIfSaveIsDisabledFnType } from "@saleor/components/Form";
import { FormId } from "@saleor/components/Form/ExitFormDialogProvider"; import { FormId } from "@saleor/components/Form/ExitFormDialogProvider";
import { import {
useExitFormDialog, useExitFormDialog,
@ -25,9 +26,11 @@ export type FormErrors<T> = {
[field in keyof T]?: string | React.ReactNode; [field in keyof T]?: string | React.ReactNode;
}; };
export interface UseFormOpts { export interface UseFormOpts<T> {
confirmLeave: boolean; confirmLeave: boolean;
formId?: FormId; formId?: FormId;
checkIfSaveIsDisabled?: CheckIfSaveIsDisabledFnType<T>;
disabled?: boolean;
} }
export interface UseFormResult<TData> export interface UseFormResult<TData>
@ -42,6 +45,7 @@ export interface UseFormResult<TData>
errors: FormErrors<TData>; errors: FormErrors<TData>;
setError: (name: keyof TData, error: string | React.ReactNode) => void; setError: (name: keyof TData, error: string | React.ReactNode) => void;
clearErrors: (name?: keyof TData | Array<keyof TData>) => void; clearErrors: (name?: keyof TData | Array<keyof TData>) => void;
setIsSubmitDisabled: (value: boolean) => void;
} }
export interface CommonUseFormResult<TData> { export interface CommonUseFormResult<TData> {
@ -49,6 +53,7 @@ export interface CommonUseFormResult<TData> {
change: FormChange; change: FormChange;
hasChanged: boolean; hasChanged: boolean;
submit: (dataOrEvent?: any) => SubmitPromise<any[]>; submit: (dataOrEvent?: any) => SubmitPromise<any[]>;
isSaveDisabled?: boolean;
} }
export interface CommonUseFormResultWithHandlers<TData, THandlers> export interface CommonUseFormResultWithHandlers<TData, THandlers>
@ -84,9 +89,14 @@ function handleRefresh<T extends FormData>(
function useForm<T extends FormData, TErrors>( function useForm<T extends FormData, TErrors>(
initialData: T, initialData: T,
onSubmit?: (data: T) => SubmitPromise<TErrors[]> | void, onSubmit?: (data: T) => SubmitPromise<TErrors[]> | void,
opts: UseFormOpts = { confirmLeave: false, formId: undefined } opts: UseFormOpts<T> = { confirmLeave: false, formId: undefined }
): UseFormResult<T> { ): UseFormResult<T> {
const { confirmLeave, formId: propsFormId } = opts; const {
confirmLeave,
formId: propsFormId,
checkIfSaveIsDisabled,
disabled
} = opts;
const [hasChanged, setChanged] = useState(false); const [hasChanged, setChanged] = useState(false);
const [errors, setErrors] = useState<FormErrors<T>>({}); const [errors, setErrors] = useState<FormErrors<T>>({});
const [data, setData] = useStateFromProps(initialData, { const [data, setData] = useStateFromProps(initialData, {
@ -94,12 +104,33 @@ function useForm<T extends FormData, TErrors>(
onRefresh: newData => handleRefresh(data, newData, handleSetChanged) 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 { const {
setIsDirty: setIsFormDirtyInExitDialog, setIsDirty: setIsFormDirtyInExitDialog,
setExitDialogSubmitRef, setExitDialogSubmitRef,
setEnableExitDialog, setEnableExitDialog,
setIsSubmitDisabled,
formId formId
} = useExitFormDialog({ formId: propsFormId }); } = useExitFormDialog({
formId: propsFormId,
isDisabled: isSaveDisabled()
});
const handleFormSubmit = useHandleFormSubmit({ const handleFormSubmit = useHandleFormSubmit({
formId, formId,
@ -216,7 +247,9 @@ function useForm<T extends FormData, TErrors>(
toggleValue, toggleValue,
handleChange, handleChange,
triggerChange: handleSetChanged, triggerChange: handleSetChanged,
setChanged: handleSetChanged setChanged: handleSetChanged,
setIsSubmitDisabled,
isSaveDisabled: isSaveDisabled()
}; };
} }

View file

@ -35,14 +35,14 @@ function useHandleFormSubmit<TData, TErrors>({
const errors = await result; const errors = await result;
setIsSubmitting(false);
if (errors?.length === 0) { if (errors?.length === 0) {
setChanged(false); setChanged(false);
return []; return [];
} }
setIsSubmitting(false);
return errors; return errors;
} }

View file

@ -522,3 +522,6 @@ export const combinedMultiAutocompleteChoices = (
selected: MultiAutocompleteChoiceType[], selected: MultiAutocompleteChoiceType[],
choices: MultiAutocompleteChoiceType[] choices: MultiAutocompleteChoiceType[]
) => uniqBy([...selected, ...choices], "value"); ) => uniqBy([...selected, ...choices], "value");
export const isInDevelopment =
!process.env.NODE_ENV || process.env.NODE_ENV === "development";

View file

@ -67,8 +67,9 @@ const OrderRefundPage: React.FC<OrderRefundPageProps> = props => {
order={order} order={order}
defaultType={defaultType} defaultType={defaultType}
onSubmit={onSubmit} onSubmit={onSubmit}
disabled={disabled}
> >
{({ data, handlers, change, submit }) => { {({ data, handlers, change, submit, isSaveDisabled }) => {
const isProductRefund = data.type === OrderRefundType.PRODUCTS; const isProductRefund = data.type === OrderRefundType.PRODUCTS;
return ( return (
@ -156,7 +157,7 @@ const OrderRefundPage: React.FC<OrderRefundPageProps> = props => {
} }
data={data} data={data}
order={order} order={order}
disabled={disabled} disabled={isSaveDisabled}
errors={errors} errors={errors}
onChange={change} onChange={change}
onRefund={submit} onRefund={submit}

View file

@ -57,6 +57,7 @@ interface OrderRefundFormProps {
order: OrderRefundDataQuery["order"]; order: OrderRefundDataQuery["order"];
defaultType: OrderRefundType; defaultType: OrderRefundType;
onSubmit: (data: OrderRefundSubmitData) => SubmitPromise; onSubmit: (data: OrderRefundSubmitData) => SubmitPromise;
disabled: boolean;
} }
function getOrderRefundPageFormData( function getOrderRefundPageFormData(
@ -73,7 +74,8 @@ function getOrderRefundPageFormData(
function useOrderRefundForm( function useOrderRefundForm(
order: OrderRefundDataQuery["order"], order: OrderRefundDataQuery["order"],
defaultType: OrderRefundType, defaultType: OrderRefundType,
onSubmit: (data: OrderRefundSubmitData) => SubmitPromise onSubmit: (data: OrderRefundSubmitData) => SubmitPromise,
disabled: boolean
): UseOrderRefundFormResult { ): UseOrderRefundFormResult {
const { const {
handleChange, handleChange,
@ -81,12 +83,15 @@ function useOrderRefundForm(
hasChanged, hasChanged,
triggerChange, triggerChange,
data: formData, data: formData,
formId formId,
setIsSubmitDisabled
} = useForm(getOrderRefundPageFormData(defaultType), undefined, { } = useForm(getOrderRefundPageFormData(defaultType), undefined, {
confirmLeave: true confirmLeave: true
}); });
const { setExitDialogSubmitRef } = useExitFormDialog(); const { setExitDialogSubmitRef } = useExitFormDialog({
formId
});
const refundedProductQuantities = useFormset<null, string>( const refundedProductQuantities = useFormset<null, string>(
order?.lines order?.lines
@ -188,7 +193,8 @@ function useOrderRefundForm(
useEffect(() => setExitDialogSubmitRef(submit), [submit]); useEffect(() => setExitDialogSubmitRef(submit), [submit]);
const disabled = !order; const isSaveDisabled = disabled || !order;
setIsSubmitDisabled(isSaveDisabled);
return { return {
change: handleChange, change: handleChange,
@ -201,7 +207,8 @@ function useOrderRefundForm(
setMaximalRefundedProductQuantities: handleMaximalRefundedProductQuantitiesSet setMaximalRefundedProductQuantities: handleMaximalRefundedProductQuantitiesSet
}, },
hasChanged, hasChanged,
submit submit,
isSaveDisabled
}; };
} }
@ -209,9 +216,10 @@ const OrderRefundForm: React.FC<OrderRefundFormProps> = ({
children, children,
order, order,
defaultType, defaultType,
onSubmit onSubmit,
disabled
}) => { }) => {
const props = useOrderRefundForm(order, defaultType, onSubmit); const props = useOrderRefundForm(order, defaultType, onSubmit, disabled);
return <form onSubmit={props.submit}>{children(props)}</form>; return <form onSubmit={props.submit}>{children(props)}</form>;
}; };

View file

@ -45,110 +45,97 @@ const OrderRefundPage: React.FC<OrderReturnPageProps> = props => {
const intl = useIntl(); const intl = useIntl();
return ( return (
<OrderRefundForm order={order} onSubmit={onSubmit}> <OrderRefundForm order={order} onSubmit={onSubmit}>
{({ data, handlers, change, submit }) => { {({ data, handlers, change, submit, isSaveDisabled }) => (
const { <Container>
fulfilledItemsQuantities, <Backlink onClick={onBack}>
waitingItemsQuantities, {intl.formatMessage(messages.appTitle, {
unfulfilledItemsQuantities orderNumber: order?.number
} = data; })}
</Backlink>
const hasAnyItemsSelected = <PageHeader
fulfilledItemsQuantities.some(({ value }) => !!value) || title={intl.formatMessage(messages.pageTitle, {
waitingItemsQuantities.some(({ value }) => !!value) || orderNumber: order?.number
unfulfilledItemsQuantities.some(({ value }) => !!value); })}
/>
return ( <Grid>
<Container> <div>
<Backlink onClick={onBack}> {!!data.unfulfilledItemsQuantities.length && (
{intl.formatMessage(messages.appTitle, { <>
orderNumber: order?.number <ItemsCard
})} errors={errors}
</Backlink> order={order}
<PageHeader lines={getUnfulfilledLines(order)}
title={intl.formatMessage(messages.pageTitle, { itemsQuantities={data.unfulfilledItemsQuantities}
orderNumber: order?.number itemsSelections={data.itemsToBeReplaced}
})} onChangeQuantity={handlers.changeUnfulfiledItemsQuantity}
/> onSetMaxQuantity={
<Grid> handlers.handleSetMaximalUnfulfiledItemsQuantities
<div> }
{!!data.unfulfilledItemsQuantities.length && ( onChangeSelected={handlers.changeItemsToBeReplaced}
<> />
<CardSpacer />
</>
)}
{renderCollection(
getWaitingFulfillments(order),
({ id, lines }) => (
<React.Fragment key={id}>
<ItemsCard <ItemsCard
errors={errors} errors={errors}
order={order} order={order}
lines={getUnfulfilledLines(order)} fulfilmentId={id}
itemsQuantities={data.unfulfilledItemsQuantities} lines={getParsedLines(lines)}
itemsQuantities={data.waitingItemsQuantities}
itemsSelections={data.itemsToBeReplaced} itemsSelections={data.itemsToBeReplaced}
onChangeQuantity={handlers.changeUnfulfiledItemsQuantity} onChangeQuantity={handlers.changeWaitingItemsQuantity}
onSetMaxQuantity={ onSetMaxQuantity={handlers.handleSetMaximalItemsQuantities(
handlers.handleSetMaximalUnfulfiledItemsQuantities id
} )}
onChangeSelected={handlers.changeItemsToBeReplaced} onChangeSelected={handlers.changeItemsToBeReplaced}
/> />
<CardSpacer /> <CardSpacer />
</> </React.Fragment>
)} )
{renderCollection( )}
getWaitingFulfillments(order), {renderCollection(
({ id, lines }) => ( getFulfilledFulfillemnts(order),
<React.Fragment key={id}> ({ id, lines }) => (
<ItemsCard <React.Fragment key={id}>
errors={errors} <ItemsCard
order={order} errors={errors}
fulfilmentId={id} order={order}
lines={getParsedLines(lines)} fulfilmentId={id}
itemsQuantities={data.waitingItemsQuantities} lines={getParsedLines(lines)}
itemsSelections={data.itemsToBeReplaced} itemsQuantities={data.fulfilledItemsQuantities}
onChangeQuantity={handlers.changeWaitingItemsQuantity} itemsSelections={data.itemsToBeReplaced}
onSetMaxQuantity={handlers.handleSetMaximalItemsQuantities( onChangeQuantity={handlers.changeFulfiledItemsQuantity}
id onSetMaxQuantity={handlers.handleSetMaximalItemsQuantities(
)} id
onChangeSelected={handlers.changeItemsToBeReplaced} )}
/> onChangeSelected={handlers.changeItemsToBeReplaced}
<CardSpacer /> />
</React.Fragment> <CardSpacer />
) </React.Fragment>
)} )
{renderCollection( )}
getFulfilledFulfillemnts(order), </div>
({ id, lines }) => ( <div>
<React.Fragment key={id}> <OrderAmount
<ItemsCard allowNoRefund
errors={errors} isReturn
order={order} amountData={getReturnProductsAmountValues(order, data)}
fulfilmentId={id} data={data}
lines={getParsedLines(lines)} order={order}
itemsQuantities={data.fulfilledItemsQuantities} disableSubmitButton={isSaveDisabled}
itemsSelections={data.itemsToBeReplaced} disabled={loading}
onChangeQuantity={handlers.changeFulfiledItemsQuantity} errors={errors}
onSetMaxQuantity={handlers.handleSetMaximalItemsQuantities( onChange={change}
id onRefund={submit}
)} />
onChangeSelected={handlers.changeItemsToBeReplaced} </div>
/> </Grid>
<CardSpacer /> </Container>
</React.Fragment> )}
)
)}
</div>
<div>
<OrderAmount
allowNoRefund
isReturn
amountData={getReturnProductsAmountValues(order, data)}
data={data}
order={order}
disableSubmitButton={!hasAnyItemsSelected}
disabled={loading}
errors={errors}
onChange={change}
onRefund={submit}
/>
</div>
</Grid>
</Container>
);
}}
</OrderRefundForm> </OrderRefundForm>
); );
}; };

View file

@ -85,12 +85,15 @@ function useOrderReturnForm(
hasChanged, hasChanged,
data: formData, data: formData,
triggerChange, triggerChange,
formId formId,
setIsSubmitDisabled
} = useForm(getOrderRefundPageFormData(), undefined, { } = useForm(getOrderRefundPageFormData(), undefined, {
confirmLeave: true confirmLeave: true
}); });
const { setExitDialogSubmitRef } = useExitFormDialog(); const { setExitDialogSubmitRef } = useExitFormDialog({
formId
});
const unfulfiledItemsQuantites = useFormset<LineItemData, number>( const unfulfiledItemsQuantites = useFormset<LineItemData, number>(
getOrderUnfulfilledLines(order).map(getParsedLineData({ initialValue: 0 })) 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 { return {
change: handleChange, change: handleChange,
data, data,
@ -259,7 +270,8 @@ function useOrderReturnForm(
handleSetMaximalUnfulfiledItemsQuantities handleSetMaximalUnfulfiledItemsQuantities
}, },
hasChanged, hasChanged,
submit submit,
isSaveDisabled
}; };
} }

View file

@ -43,8 +43,9 @@ const OrderSettingsPage: React.FC<OrderSettingsPageProps> = props => {
orderSettings={orderSettings} orderSettings={orderSettings}
shop={shop} shop={shop}
onSubmit={onSubmit} onSubmit={onSubmit}
disabled={disabled}
> >
{({ data, submit, hasChanged, change }) => ( {({ data, submit, change, isSaveDisabled }) => (
<Container> <Container>
<Backlink onClick={onBack}> <Backlink onClick={onBack}>
{intl.formatMessage(sectionNames.orders)} {intl.formatMessage(sectionNames.orders)}
@ -73,7 +74,7 @@ const OrderSettingsPage: React.FC<OrderSettingsPageProps> = props => {
<Savebar <Savebar
onCancel={onBack} onCancel={onBack}
onSubmit={submit} onSubmit={submit}
disabled={disabled || !hasChanged} disabled={isSaveDisabled}
state={saveButtonBarState} state={saveButtonBarState}
/> />
</Container> </Container>

View file

@ -2,7 +2,10 @@ import {
OrderSettingsFragment, OrderSettingsFragment,
ShopOrderSettingsFragment ShopOrderSettingsFragment
} from "@saleor/graphql"; } 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 useHandleFormSubmit from "@saleor/hooks/useHandleFormSubmit";
import React from "react"; import React from "react";
@ -13,18 +16,15 @@ export interface OrderSettingsFormData {
automaticallyFulfillNonShippableGiftCard: boolean; automaticallyFulfillNonShippableGiftCard: boolean;
} }
export interface UseOrderSettingsFormResult { export type UseOrderSettingsFormResult = CommonUseFormResult<
change: FormChange; OrderSettingsFormData
data: OrderSettingsFormData; >;
hasChanged: boolean;
submit: () => SubmitPromise<any[]>;
}
export interface OrderSettingsFormProps { export interface OrderSettingsFormProps {
children: (props: UseOrderSettingsFormResult) => React.ReactNode; children: (props: UseOrderSettingsFormResult) => React.ReactNode;
orderSettings: OrderSettingsFragment; orderSettings: OrderSettingsFragment;
shop: ShopOrderSettingsFragment; shop: ShopOrderSettingsFragment;
onSubmit: (data: OrderSettingsFormData) => SubmitPromise; onSubmit: (data: OrderSettingsFormData) => SubmitPromise;
disabled: boolean;
} }
function getOrderSeettingsFormData( function getOrderSeettingsFormData(
@ -44,15 +44,19 @@ function getOrderSeettingsFormData(
function useOrderSettingsForm( function useOrderSettingsForm(
orderSettings: OrderSettingsFragment, orderSettings: OrderSettingsFragment,
shop: ShopOrderSettingsFragment, shop: ShopOrderSettingsFragment,
onSubmit: (data: OrderSettingsFormData) => SubmitPromise onSubmit: (data: OrderSettingsFormData) => SubmitPromise,
disabled: boolean
): UseOrderSettingsFormResult { ): UseOrderSettingsFormResult {
const { data, handleChange, formId, hasChanged, setChanged } = useForm( const {
getOrderSeettingsFormData(orderSettings, shop), data,
undefined, handleChange,
{ formId,
confirmLeave: true hasChanged,
} setChanged,
); setIsSubmitDisabled
} = useForm(getOrderSeettingsFormData(orderSettings, shop), undefined, {
confirmLeave: true
});
const handleFormSubmit = useHandleFormSubmit({ const handleFormSubmit = useHandleFormSubmit({
formId, formId,
@ -61,12 +65,15 @@ function useOrderSettingsForm(
}); });
const submit = () => handleFormSubmit(data); const submit = () => handleFormSubmit(data);
const isSaveDisabled = disabled || !hasChanged;
setIsSubmitDisabled(isSaveDisabled);
return { return {
change: handleChange, change: handleChange,
data, data,
hasChanged, hasChanged,
submit submit,
isSaveDisabled
}; };
} }
@ -74,9 +81,10 @@ const OrderSettingsForm: React.FC<OrderSettingsFormProps> = ({
children, children,
orderSettings, orderSettings,
shop, shop,
onSubmit onSubmit,
disabled
}) => { }) => {
const props = useOrderSettingsForm(orderSettings, shop, onSubmit); const props = useOrderSettingsForm(orderSettings, shop, onSubmit, disabled);
return <form onSubmit={props.submit}>{children(props)}</form>; return <form onSubmit={props.submit}>{children(props)}</form>;
}; };

View file

@ -58,8 +58,13 @@ const PageTypeCreatePage: React.FC<PageTypeCreatePageProps> = props => {
} = useMetadataChangeTrigger(); } = useMetadataChangeTrigger();
return ( return (
<Form confirmLeave initial={formInitialData} onSubmit={onSubmit}> <Form
{({ change, data, hasChanged, submit }) => { confirmLeave
initial={formInitialData}
onSubmit={onSubmit}
disabled={disabled}
>
{({ change, data, submit, isSaveDisabled }) => {
const changeMetadata = makeMetadataChangeHandler(change); const changeMetadata = makeMetadataChangeHandler(change);
return ( return (
@ -103,7 +108,7 @@ const PageTypeCreatePage: React.FC<PageTypeCreatePageProps> = props => {
<Savebar <Savebar
onCancel={onBack} onCancel={onBack}
onSubmit={submit} onSubmit={submit}
disabled={disabled || !hasChanged} disabled={isSaveDisabled}
state={saveButtonBarState} state={saveButtonBarState}
/> />
</Container> </Container>

View file

@ -110,8 +110,13 @@ const PageTypeDetailsPage: React.FC<PageTypeDetailsPageProps> = props => {
}; };
return ( return (
<Form confirmLeave initial={formInitialData} onSubmit={handleSubmit}> <Form
{({ change, data, hasChanged, submit }) => { confirmLeave
initial={formInitialData}
onSubmit={handleSubmit}
disabled={disabled}
>
{({ change, data, isSaveDisabled, submit }) => {
const changeMetadata = makeMetadataChangeHandler(change); const changeMetadata = makeMetadataChangeHandler(change);
return ( return (
@ -174,7 +179,7 @@ const PageTypeDetailsPage: React.FC<PageTypeDetailsPageProps> = props => {
onCancel={onBack} onCancel={onBack}
onDelete={onDelete} onDelete={onDelete}
onSubmit={submit} onSubmit={submit}
disabled={disabled || !hasChanged} disabled={isSaveDisabled}
state={saveButtonBarState} state={saveButtonBarState}
/> />
</Container> </Container>

View file

@ -135,8 +135,9 @@ const PageDetailsPage: React.FC<PageDetailsPageProps> = ({
fetchMoreReferenceProducts={fetchMoreReferenceProducts} fetchMoreReferenceProducts={fetchMoreReferenceProducts}
assignReferencesAttributeId={assignReferencesAttributeId} assignReferencesAttributeId={assignReferencesAttributeId}
onSubmit={onSubmit} onSubmit={onSubmit}
disabled={loading}
> >
{({ change, data, valid, handlers, hasChanged, submit }) => ( {({ change, data, handlers, submit, isSaveDisabled }) => (
<Container> <Container>
<Backlink onClick={onBack}> <Backlink onClick={onBack}>
{intl.formatMessage(sectionNames.pages)} {intl.formatMessage(sectionNames.pages)}
@ -242,7 +243,7 @@ const PageDetailsPage: React.FC<PageDetailsPageProps> = ({
</div> </div>
</Grid> </Grid>
<Savebar <Savebar
disabled={loading || !hasChanged || !valid} disabled={isSaveDisabled}
state={saveButtonBarState} state={saveButtonBarState}
onCancel={onBack} onCancel={onBack}
onDelete={page === null ? undefined : onRemove} onDelete={page === null ? undefined : onRemove}

View file

@ -97,6 +97,7 @@ export interface PageFormProps extends UsePageFormOpts {
children: (props: UsePageUpdateFormResult) => React.ReactNode; children: (props: UsePageUpdateFormResult) => React.ReactNode;
page: PageDetailsFragment; page: PageDetailsFragment;
onSubmit: (data: PageData) => SubmitPromise; onSubmit: (data: PageData) => SubmitPromise;
disabled: boolean;
} }
const getInitialFormData = (page?: PageDetailsFragment): PageFormData => ({ const getInitialFormData = (page?: PageDetailsFragment): PageFormData => ({
@ -114,6 +115,7 @@ const getInitialFormData = (page?: PageDetailsFragment): PageFormData => ({
function usePageForm( function usePageForm(
page: PageDetailsFragment, page: PageDetailsFragment,
onSubmit: (data: PageData) => SubmitPromise, onSubmit: (data: PageData) => SubmitPromise,
disabled: boolean,
opts: UsePageFormOpts opts: UsePageFormOpts
): UsePageUpdateFormResult { ): UsePageUpdateFormResult {
const pageExists = page !== null; const pageExists = page !== null;
@ -138,7 +140,7 @@ function usePageForm(
confirmLeave: true confirmLeave: true
}); });
const { setExitDialogSubmitRef } = useExitFormDialog({ const { setExitDialogSubmitRef, setIsSubmitDisabled } = useExitFormDialog({
formId formId
}); });
@ -238,6 +240,9 @@ function usePageForm(
const valid = pageExists || !!opts.selectedPageType; const valid = pageExists || !!opts.selectedPageType;
const isSaveDisabled = disabled || !hasChanged || !valid;
setIsSubmitDisabled(isSaveDisabled);
return { return {
change: handleChange, change: handleChange,
data: getData(), data: getData(),
@ -255,7 +260,8 @@ function usePageForm(
selectPageType: handlePageTypeSelect selectPageType: handlePageTypeSelect
}, },
hasChanged, hasChanged,
submit submit,
isSaveDisabled
}; };
} }
@ -263,9 +269,10 @@ const PageForm: React.FC<PageFormProps> = ({
children, children,
page, page,
onSubmit, onSubmit,
disabled,
...rest ...rest
}) => { }) => {
const props = usePageForm(page, onSubmit, rest); const props = usePageForm(page, onSubmit, disabled, rest);
return <form onSubmit={props.submit}>{children(props)}</form>; return <form onSubmit={props.submit}>{children(props)}</form>;
}; };

View file

@ -55,8 +55,13 @@ const PermissionGroupCreatePage: React.FC<PermissionGroupCreatePageProps> = ({
); );
return ( return (
<Form confirmLeave initial={initialForm} onSubmit={onSubmit}> <Form
{({ data, change, submit, hasChanged }) => ( confirmLeave
initial={initialForm}
onSubmit={onSubmit}
disabled={disabled}
>
{({ data, change, submit, isSaveDisabled }) => (
<Container> <Container>
<Backlink onClick={onBack}> <Backlink onClick={onBack}>
{intl.formatMessage(sectionNames.permissionGroups)} {intl.formatMessage(sectionNames.permissionGroups)}
@ -95,7 +100,7 @@ const PermissionGroupCreatePage: React.FC<PermissionGroupCreatePageProps> = ({
onCancel={onBack} onCancel={onBack}
onSubmit={submit} onSubmit={submit}
state={saveButtonBarState} state={saveButtonBarState}
disabled={disabled || !hasChanged} disabled={isSaveDisabled}
/> />
</div> </div>
</Container> </Container>

View file

@ -75,8 +75,9 @@ const PluginsDetailsPage: React.FC<PluginsDetailsPageProps> = ({
initial={initialFormData()} initial={initialFormData()}
onSubmit={onSubmit} onSubmit={onSubmit}
key={selectedChannelId} key={selectedChannelId}
disabled={disabled}
> >
{({ data, hasChanged, submit, set }) => { {({ data, submit, set, isSaveDisabled }) => {
const onChange = (event: ChangeEvent) => { const onChange = (event: ChangeEvent) => {
const { name, value } = event.target; const { name, value } = event.target;
const newData = { const newData = {
@ -159,7 +160,7 @@ const PluginsDetailsPage: React.FC<PluginsDetailsPageProps> = ({
</div> </div>
</Grid> </Grid>
<Savebar <Savebar
disabled={disabled || !hasChanged} disabled={isSaveDisabled}
state={saveButtonBarState} state={saveButtonBarState}
onCancel={onBack} onCancel={onBack}
onSubmit={submit} onSubmit={submit}

View file

@ -91,8 +91,13 @@ const ProductTypeCreatePage: React.FC<ProductTypeCreatePageProps> = ({
}; };
return ( return (
<Form confirmLeave initial={initialData} onSubmit={onSubmit}> <Form
{({ change, data, hasChanged, submit }) => { confirmLeave
initial={initialData}
onSubmit={onSubmit}
disabled={disabled}
>
{({ change, data, isSaveDisabled, submit }) => {
const changeMetadata = makeMetadataChangeHandler(change); const changeMetadata = makeMetadataChangeHandler(change);
const changeKind = makeProductTypeKindChangeHandler( const changeKind = makeProductTypeKindChangeHandler(
@ -145,7 +150,7 @@ const ProductTypeCreatePage: React.FC<ProductTypeCreatePageProps> = ({
<Savebar <Savebar
onCancel={onBack} onCancel={onBack}
onSubmit={submit} onSubmit={submit}
disabled={disabled || !hasChanged} disabled={isSaveDisabled}
state={saveButtonBarState} state={saveButtonBarState}
/> />
</Container> </Container>

View file

@ -156,8 +156,13 @@ const ProductTypeDetailsPage: React.FC<ProductTypeDetailsPageProps> = ({
}; };
return ( return (
<Form initial={formInitialData} onSubmit={handleSubmit} confirmLeave> <Form
{({ change, data, hasChanged, submit, setChanged }) => { initial={formInitialData}
onSubmit={handleSubmit}
confirmLeave
disabled={disabled}
>
{({ change, data, isSaveDisabled, submit, setChanged }) => {
const changeMetadata = makeMetadataChangeHandler(change); const changeMetadata = makeMetadataChangeHandler(change);
return ( return (
@ -256,7 +261,7 @@ const ProductTypeDetailsPage: React.FC<ProductTypeDetailsPageProps> = ({
onCancel={onBack} onCancel={onBack}
onDelete={onDelete} onDelete={onDelete}
onSubmit={submit} onSubmit={submit}
disabled={disabled || !hasChanged} disabled={isSaveDisabled}
state={saveButtonBarState} state={saveButtonBarState}
/> />
</Container> </Container>

View file

@ -202,16 +202,9 @@ export const ProductCreatePage: React.FC<ProductCreatePageProps> = ({
fetchReferenceProducts={fetchReferenceProducts} fetchReferenceProducts={fetchReferenceProducts}
fetchMoreReferenceProducts={fetchMoreReferenceProducts} fetchMoreReferenceProducts={fetchMoreReferenceProducts}
assignReferencesAttributeId={assignReferencesAttributeId} assignReferencesAttributeId={assignReferencesAttributeId}
loading={loading}
> >
{({ {({ change, data, formErrors, handlers, submit, isSaveDisabled }) => {
change,
data,
formErrors,
disabled: formDisabled,
handlers,
hasChanged,
submit
}) => {
// Comparing explicitly to false because `hasVariants` can be undefined // Comparing explicitly to false because `hasVariants` can be undefined
const isSimpleProduct = data.productType?.hasVariants === false; const isSimpleProduct = data.productType?.hasVariants === false;
@ -367,7 +360,7 @@ export const ProductCreatePage: React.FC<ProductCreatePageProps> = ({
onCancel={onBack} onCancel={onBack}
onSubmit={submit} onSubmit={submit}
state={saveButtonBarState} state={saveButtonBarState}
disabled={loading || !onSubmit || formDisabled || !hasChanged} disabled={isSaveDisabled}
/> />
{canOpenAssignReferencesAttributeDialog && ( {canOpenAssignReferencesAttributeDialog && (
<AssignAttributeValueDialog <AssignAttributeValueDialog

View file

@ -163,11 +163,13 @@ export interface ProductCreateFormProps extends UseProductCreateFormOpts {
children: (props: UseProductCreateFormResult) => React.ReactNode; children: (props: UseProductCreateFormResult) => React.ReactNode;
initial?: Partial<ProductCreateFormData>; initial?: Partial<ProductCreateFormData>;
onSubmit: (data: ProductCreateData) => SubmitPromise; onSubmit: (data: ProductCreateData) => SubmitPromise;
loading: boolean;
} }
function useProductCreateForm( function useProductCreateForm(
initial: Partial<ProductCreateFormData>, initial: Partial<ProductCreateFormData>,
onSubmit: (data: ProductCreateData) => SubmitPromise, onSubmit: (data: ProductCreateData) => SubmitPromise,
loading: boolean,
opts: UseProductCreateFormOpts opts: UseProductCreateFormOpts
): UseProductCreateFormResult { ): UseProductCreateFormResult {
const intl = useIntl(); const intl = useIntl();
@ -231,10 +233,6 @@ function useProductCreateForm(
triggerChange triggerChange
}); });
const { setExitDialogSubmitRef } = useExitFormDialog({
formId: PRODUCT_CREATE_FORM_ID
});
const { const {
makeChangeHandler: makeMetadataChangeHandler makeChangeHandler: makeMetadataChangeHandler
} = useMetadataChangeTrigger(); } = useMetadataChangeTrigger();
@ -357,6 +355,10 @@ function useProductCreateForm(
const submit = () => handleFormSubmit(data); const submit = () => handleFormSubmit(data);
const { setExitDialogSubmitRef, setIsSubmitDisabled } = useExitFormDialog({
formId: PRODUCT_CREATE_FORM_ID
});
useEffect(() => setExitDialogSubmitRef(submit), [submit]); useEffect(() => setExitDialogSubmitRef(submit), [submit]);
const shouldEnableSave = () => { const shouldEnableSave = () => {
@ -387,12 +389,15 @@ function useProductCreateForm(
return true; return true;
}; };
const disabled = !shouldEnableSave(); const isSaveEnabled = !shouldEnableSave();
const isSaveDisabled = loading || !onSubmit || isSaveEnabled || !hasChanged;
setIsSubmitDisabled(isSaveDisabled);
return { return {
change: handleChange, change: handleChange,
data, data,
disabled, disabled: isSaveEnabled,
formErrors: form.errors, formErrors: form.errors,
handlers: { handlers: {
addStock: handleStockAdd, addStock: handleStockAdd,
@ -416,7 +421,8 @@ function useProductCreateForm(
selectTaxRate: handleTaxTypeSelect selectTaxRate: handleTaxTypeSelect
}, },
hasChanged, hasChanged,
submit submit,
isSaveDisabled
}; };
} }
@ -424,9 +430,10 @@ const ProductCreateForm: React.FC<ProductCreateFormProps> = ({
children, children,
initial, initial,
onSubmit, onSubmit,
loading,
...rest ...rest
}) => { }) => {
const props = useProductCreateForm(initial || {}, onSubmit, rest); const props = useProductCreateForm(initial || {}, onSubmit, loading, rest);
return <form onSubmit={props.submit}>{children(props)}</form>; return <form onSubmit={props.submit}>{children(props)}</form>;
}; };

View file

@ -284,16 +284,10 @@ export const ProductUpdatePage: React.FC<ProductUpdatePageProps> = ({
fetchReferenceProducts={fetchReferenceProducts} fetchReferenceProducts={fetchReferenceProducts}
fetchMoreReferenceProducts={fetchMoreReferenceProducts} fetchMoreReferenceProducts={fetchMoreReferenceProducts}
assignReferencesAttributeId={assignReferencesAttributeId} assignReferencesAttributeId={assignReferencesAttributeId}
disabled={disabled}
hasChannelChanged={hasChannelChanged}
> >
{({ {({ change, data, formErrors, handlers, submit, isSaveDisabled }) => (
change,
data,
formErrors,
disabled: formDisabled,
handlers,
hasChanged,
submit
}) => (
<> <>
<Container> <Container>
<Backlink onClick={onBack}> <Backlink onClick={onBack}>
@ -508,9 +502,7 @@ export const ProductUpdatePage: React.FC<ProductUpdatePageProps> = ({
onDelete={onDelete} onDelete={onDelete}
onSubmit={submit} onSubmit={submit}
state={saveButtonBarState} state={saveButtonBarState}
disabled={ disabled={isSaveDisabled}
disabled || formDisabled || (!hasChanged && !hasChannelChanged)
}
/> />
{canOpenAssignReferencesAttributeDialog && ( {canOpenAssignReferencesAttributeDialog && (
<AssignAttributeValueDialog <AssignAttributeValueDialog

View file

@ -157,7 +157,6 @@ export interface UseProductUpdateFormResult
ProductUpdateData, ProductUpdateData,
ProductUpdateHandlers ProductUpdateHandlers
> { > {
disabled: boolean;
formErrors: FormErrors<ProductUpdateSubmitData>; formErrors: FormErrors<ProductUpdateSubmitData>;
} }
@ -193,6 +192,8 @@ export interface ProductUpdateFormProps extends UseProductUpdateFormOpts {
children: (props: UseProductUpdateFormResult) => React.ReactNode; children: (props: UseProductUpdateFormResult) => React.ReactNode;
product: ProductFragment; product: ProductFragment;
onSubmit: (data: ProductUpdateSubmitData) => SubmitPromise; onSubmit: (data: ProductUpdateSubmitData) => SubmitPromise;
disabled: boolean;
hasChannelChanged: boolean;
} }
const getStocksData = ( const getStocksData = (
@ -222,6 +223,8 @@ const getStocksData = (
function useProductUpdateForm( function useProductUpdateForm(
product: ProductFragment, product: ProductFragment,
onSubmit: (data: ProductUpdateSubmitData) => SubmitPromise, onSubmit: (data: ProductUpdateSubmitData) => SubmitPromise,
disabled: boolean,
hasChannelChanged: boolean,
opts: UseProductUpdateFormOpts opts: UseProductUpdateFormOpts
): UseProductUpdateFormResult { ): UseProductUpdateFormResult {
const intl = useIntl(); const intl = useIntl();
@ -244,7 +247,8 @@ function useProductUpdateForm(
toggleValue, toggleValue,
data: formData, data: formData,
setChanged, setChanged,
hasChanged hasChanged,
setIsSubmitDisabled
} = form; } = form;
const attributes = useFormset(getAttributeInputFromProduct(product)); const attributes = useFormset(getAttributeInputFromProduct(product));
@ -435,12 +439,15 @@ function useProductUpdateForm(
return true; return true;
}; };
const disabled = !shouldEnableSave(); const isSaveEnabled = !shouldEnableSave();
const isSaveDisabled =
disabled || isSaveEnabled || (!hasChanged && !hasChannelChanged);
setIsSubmitDisabled(isSaveDisabled);
return { return {
change: handleChange, change: handleChange,
data, data,
disabled,
formErrors: form.errors, formErrors: form.errors,
handlers: { handlers: {
addStock: handleStockAdd, addStock: handleStockAdd,
@ -464,7 +471,8 @@ function useProductUpdateForm(
selectTaxRate: handleTaxTypeSelect selectTaxRate: handleTaxTypeSelect
}, },
hasChanged, hasChanged,
submit submit,
isSaveDisabled
}; };
} }
@ -472,9 +480,17 @@ const ProductUpdateForm: React.FC<ProductUpdateFormProps> = ({
children, children,
product, product,
onSubmit, onSubmit,
disabled,
hasChannelChanged,
...rest ...rest
}) => { }) => {
const props = useProductUpdateForm(product, onSubmit, rest); const props = useProductUpdateForm(
product,
onSubmit,
disabled,
hasChannelChanged,
rest
);
return <form onSubmit={props.submit}>{children(props)}</form>; return <form onSubmit={props.submit}>{children(props)}</form>;
}; };

View file

@ -149,15 +149,9 @@ const ProductVariantCreatePage: React.FC<ProductVariantCreatePageProps> = ({
fetchReferenceProducts={fetchReferenceProducts} fetchReferenceProducts={fetchReferenceProducts}
fetchMoreReferenceProducts={fetchMoreReferenceProducts} fetchMoreReferenceProducts={fetchMoreReferenceProducts}
assignReferencesAttributeId={assignReferencesAttributeId} assignReferencesAttributeId={assignReferencesAttributeId}
disabled={disabled}
> >
{({ {({ change, data, formErrors, handlers, submit, isSaveDisabled }) => (
change,
data,
formErrors,
disabled: formDisabled,
handlers,
submit
}) => (
<Container> <Container>
<Backlink onClick={onBack}>{product?.name}</Backlink> <Backlink onClick={onBack}>{product?.name}</Backlink>
<PageHeader title={header} /> <PageHeader title={header} />
@ -258,7 +252,7 @@ const ProductVariantCreatePage: React.FC<ProductVariantCreatePageProps> = ({
</div> </div>
</Grid> </Grid>
<Savebar <Savebar
disabled={disabled || formDisabled || !onSubmit} disabled={isSaveDisabled}
labels={{ labels={{
confirm: intl.formatMessage(messages.saveVariant), confirm: intl.formatMessage(messages.saveVariant),
delete: intl.formatMessage(messages.deleteVariant) delete: intl.formatMessage(messages.deleteVariant)

View file

@ -94,6 +94,7 @@ export interface ProductVariantCreateFormProps
children: (props: UseProductVariantCreateFormResult) => React.ReactNode; children: (props: UseProductVariantCreateFormResult) => React.ReactNode;
product: ProductVariantCreateDataQuery["product"]; product: ProductVariantCreateDataQuery["product"];
onSubmit: (data: ProductVariantCreateData) => void; onSubmit: (data: ProductVariantCreateData) => void;
disabled: boolean;
} }
const initial: ProductVariantCreateFormData = { const initial: ProductVariantCreateFormData = {
@ -113,6 +114,7 @@ const initial: ProductVariantCreateFormData = {
function useProductVariantCreateForm( function useProductVariantCreateForm(
product: ProductVariantCreateDataQuery["product"], product: ProductVariantCreateDataQuery["product"],
onSubmit: (data: ProductVariantCreateData) => void, onSubmit: (data: ProductVariantCreateData) => void,
disabled: boolean,
opts: UseProductVariantCreateFormOpts opts: UseProductVariantCreateFormOpts
): UseProductVariantCreateFormResult { ): UseProductVariantCreateFormResult {
const intl = useIntl(); const intl = useIntl();
@ -126,7 +128,8 @@ function useProductVariantCreateForm(
handleChange, handleChange,
hasChanged, hasChanged,
data: formData, data: formData,
formId formId,
setIsSubmitDisabled
} = form; } = form;
const attributes = useFormset(attributeInput); const attributes = useFormset(attributeInput);
@ -227,13 +230,18 @@ function useProductVariantCreateForm(
useEffect(() => setExitDialogSubmitRef(submit), [submit]); useEffect(() => setExitDialogSubmitRef(submit), [submit]);
const formDisabled =
data.isPreorder &&
data.hasPreorderEndDate &&
!!form.errors.preorderEndDateTime;
const isSaveDisabled = disabled || formDisabled || !onSubmit;
setIsSubmitDisabled(isSaveDisabled);
return { return {
change: handleChange, change: handleChange,
data, data,
disabled: disabled,
data.isPreorder &&
data.hasPreorderEndDate &&
!!form.errors.preorderEndDateTime,
formErrors: form.errors, formErrors: form.errors,
handlers: { handlers: {
addStock: handleStockAdd, addStock: handleStockAdd,
@ -250,7 +258,8 @@ function useProductVariantCreateForm(
selectAttributeReference: handleAttributeReferenceChange selectAttributeReference: handleAttributeReferenceChange
}, },
hasChanged, hasChanged,
submit submit,
isSaveDisabled
}; };
} }
@ -258,9 +267,10 @@ const ProductVariantCreateForm: React.FC<ProductVariantCreateFormProps> = ({
children, children,
product, product,
onSubmit, onSubmit,
disabled,
...rest ...rest
}) => { }) => {
const props = useProductVariantCreateForm(product, onSubmit, rest); const props = useProductVariantCreateForm(product, onSubmit, disabled, rest);
return <form onSubmit={props.submit}>{children(props)}</form>; return <form onSubmit={props.submit}>{children(props)}</form>;
}; };

View file

@ -218,16 +218,9 @@ const ProductVariantPage: React.FC<ProductVariantPageProps> = ({
fetchReferenceProducts={fetchReferenceProducts} fetchReferenceProducts={fetchReferenceProducts}
fetchMoreReferenceProducts={fetchMoreReferenceProducts} fetchMoreReferenceProducts={fetchMoreReferenceProducts}
assignReferencesAttributeId={assignReferencesAttributeId} assignReferencesAttributeId={assignReferencesAttributeId}
loading={loading}
> >
{({ {({ change, data, formErrors, isSaveDisabled, handlers, submit }) => {
change,
data,
formErrors,
disabled: formDisabled,
handlers,
hasChanged,
submit
}) => {
const nonSelectionAttributes = data.attributes.filter( const nonSelectionAttributes = data.attributes.filter(
byAttributeScope(VariantAttributeScope.NOT_VARIANT_SELECTION) byAttributeScope(VariantAttributeScope.NOT_VARIANT_SELECTION)
); );
@ -373,7 +366,7 @@ const ProductVariantPage: React.FC<ProductVariantPageProps> = ({
</div> </div>
</Grid> </Grid>
<Savebar <Savebar
disabled={loading || formDisabled || !hasChanged} disabled={isSaveDisabled}
state={saveButtonBarState} state={saveButtonBarState}
onCancel={onBack} onCancel={onBack}
onDelete={onDelete} onDelete={onDelete}

View file

@ -130,12 +130,14 @@ export interface ProductVariantUpdateFormProps
extends UseProductVariantUpdateFormOpts { extends UseProductVariantUpdateFormOpts {
children: (props: UseProductVariantUpdateFormResult) => React.ReactNode; children: (props: UseProductVariantUpdateFormResult) => React.ReactNode;
variant: ProductVariantFragment; variant: ProductVariantFragment;
loading: boolean;
onSubmit: (data: ProductVariantUpdateSubmitData) => SubmitPromise; onSubmit: (data: ProductVariantUpdateSubmitData) => SubmitPromise;
} }
function useProductVariantUpdateForm( function useProductVariantUpdateForm(
variant: ProductVariantFragment, variant: ProductVariantFragment,
onSubmit: (data: ProductVariantUpdateSubmitData) => SubmitPromise, onSubmit: (data: ProductVariantUpdateSubmitData) => SubmitPromise,
loading: boolean,
opts: UseProductVariantUpdateFormOpts opts: UseProductVariantUpdateFormOpts
): UseProductVariantUpdateFormResult { ): UseProductVariantUpdateFormResult {
const intl = useIntl(); const intl = useIntl();
@ -180,7 +182,8 @@ function useProductVariantUpdateForm(
data: formData, data: formData,
setChanged, setChanged,
hasChanged, hasChanged,
formId formId,
setIsSubmitDisabled
} = form; } = form;
const { setExitDialogSubmitRef } = useExitFormDialog({ const { setExitDialogSubmitRef } = useExitFormDialog({
@ -332,6 +335,9 @@ function useProductVariantUpdateForm(
useEffect(() => setExitDialogSubmitRef(submit), [submit]); useEffect(() => setExitDialogSubmitRef(submit), [submit]);
const isSaveDisabled = loading || disabled || !hasChanged;
setIsSubmitDisabled(isSaveDisabled);
return { return {
change: handleChange, change: handleChange,
data, data,
@ -353,7 +359,8 @@ function useProductVariantUpdateForm(
selectAttributeReference: handleAttributeReferenceChange selectAttributeReference: handleAttributeReferenceChange
}, },
hasChanged, hasChanged,
submit submit,
isSaveDisabled
}; };
} }
@ -361,9 +368,10 @@ const ProductVariantUpdateForm: React.FC<ProductVariantUpdateFormProps> = ({
children, children,
variant, variant,
onSubmit, onSubmit,
loading,
...rest ...rest
}) => { }) => {
const props = useProductVariantUpdateForm(variant, onSubmit, rest); const props = useProductVariantUpdateForm(variant, onSubmit, loading, rest);
return <form onSubmit={props.submit}>{children(props)}</form>; return <form onSubmit={props.submit}>{children(props)}</form>;
}; };

View file

@ -122,10 +122,11 @@ export function createHandler(
}; };
const result = await productCreate(productVariables); const result = await productCreate(productVariables);
let hasErrors = errors.length > 0; let hasErrors = errors.length > 0;
const hasVariants = productType.hasVariants; const hasVariants = productType?.hasVariants;
const productId = result.data.productCreate.product?.id; const productId = result?.data?.productCreate?.product?.id;
if (!productId) { if (!productId) {
return { errors }; return { errors };

View file

@ -66,8 +66,13 @@ const ShippingZoneCreatePage: React.FC<ShippingZoneCreatePageProps> = ({
}; };
return ( return (
<Form confirmLeave initial={initialForm} onSubmit={onSubmit}> <Form
{({ change, data, hasChanged, submit }) => ( confirmLeave
initial={initialForm}
onSubmit={onSubmit}
disabled={disabled}
>
{({ change, data, isSaveDisabled, submit }) => (
<> <>
<Container> <Container>
<Backlink onClick={onBack}> <Backlink onClick={onBack}>
@ -105,7 +110,7 @@ const ShippingZoneCreatePage: React.FC<ShippingZoneCreatePageProps> = ({
</div> </div>
</Grid> </Grid>
<Savebar <Savebar
disabled={disabled || !hasChanged} disabled={isSaveDisabled}
onCancel={onBack} onCancel={onBack}
onSubmit={submit} onSubmit={submit}
state={saveButtonBarState} state={saveButtonBarState}

View file

@ -124,8 +124,13 @@ const ShippingZoneDetailsPage: React.FC<ShippingZoneDetailsPageProps> = ({
} = useMetadataChangeTrigger(); } = useMetadataChangeTrigger();
return ( return (
<Form initial={initialForm} onSubmit={onSubmit} confirmLeave> <Form
{({ change, data, hasChanged, submit, toggleValue }) => { initial={initialForm}
onSubmit={onSubmit}
confirmLeave
disabled={disabled}
>
{({ change, data, isSaveDisabled, submit, toggleValue }) => {
const handleWarehouseChange = createMultiAutocompleteSelectHandler( const handleWarehouseChange = createMultiAutocompleteSelectHandler(
toggleValue, toggleValue,
setWarehouseDisplayValues, setWarehouseDisplayValues,
@ -215,7 +220,7 @@ const ShippingZoneDetailsPage: React.FC<ShippingZoneDetailsPageProps> = ({
</div> </div>
</Grid> </Grid>
<Savebar <Savebar
disabled={disabled || !hasChanged} disabled={isSaveDisabled}
onCancel={onBack} onCancel={onBack}
onDelete={onDelete} onDelete={onDelete}
onSubmit={submit} onSubmit={submit}

View file

@ -3,7 +3,7 @@ import { ChannelShippingData } from "@saleor/channels/utils";
import CardSpacer from "@saleor/components/CardSpacer"; import CardSpacer from "@saleor/components/CardSpacer";
import ChannelsAvailabilityCard from "@saleor/components/ChannelsAvailabilityCard"; import ChannelsAvailabilityCard from "@saleor/components/ChannelsAvailabilityCard";
import Container from "@saleor/components/Container"; import Container from "@saleor/components/Container";
import Form from "@saleor/components/Form"; import Form, { FormDataWithOpts } from "@saleor/components/Form";
import { WithFormId } from "@saleor/components/Form/ExitFormDialogProvider"; import { WithFormId } from "@saleor/components/Form/ExitFormDialogProvider";
import Grid from "@saleor/components/Grid"; import Grid from "@saleor/components/Grid";
import PageHeader from "@saleor/components/PageHeader"; import PageHeader from "@saleor/components/PageHeader";
@ -86,22 +86,30 @@ export const ShippingZoneRatesCreatePage: React.FC<ShippingZoneRatesCreatePagePr
type: null type: null
}; };
const checkIfSaveIsDisabled = (
data: FormDataWithOpts<ShippingZoneRateCommonFormData>
) => {
const formDisabled = data.channelListings?.some(channel =>
validatePrice(channel.price)
);
return disabled || formDisabled || (!data.hasChanged && !hasChannelChanged);
};
return ( return (
<Form <Form
confirmLeave confirmLeave
initial={initialForm} initial={initialForm}
onSubmit={onSubmit} onSubmit={onSubmit}
formId={formId} formId={formId}
checkIfSaveIsDisabled={checkIfSaveIsDisabled}
> >
{({ change, data, hasChanged, submit, triggerChange, set }) => { {({ change, data, isSaveDisabled, submit, triggerChange, set }) => {
const handleChannelsChange = createChannelsChangeHandler( const handleChannelsChange = createChannelsChangeHandler(
shippingChannels, shippingChannels,
onChannelsChange, onChannelsChange,
triggerChange triggerChange
); );
const formDisabled = data.channelListings?.some(channel =>
validatePrice(channel.price)
);
const onDescriptionChange = (description: OutputData) => { const onDescriptionChange = (description: OutputData) => {
set({ description }); set({ description });
triggerChange(); triggerChange();
@ -181,9 +189,7 @@ export const ShippingZoneRatesCreatePage: React.FC<ShippingZoneRatesCreatePagePr
</div> </div>
</Grid> </Grid>
<Savebar <Savebar
disabled={ disabled={isSaveDisabled}
disabled || formDisabled || (!hasChanged && !hasChannelChanged)
}
onCancel={onBack} onCancel={onBack}
onDelete={onDelete} onDelete={onDelete}
onSubmit={submit} onSubmit={submit}

View file

@ -3,7 +3,7 @@ import { ChannelShippingData } from "@saleor/channels/utils";
import CardSpacer from "@saleor/components/CardSpacer"; import CardSpacer from "@saleor/components/CardSpacer";
import ChannelsAvailabilityCard from "@saleor/components/ChannelsAvailabilityCard"; import ChannelsAvailabilityCard from "@saleor/components/ChannelsAvailabilityCard";
import Container from "@saleor/components/Container"; import Container from "@saleor/components/Container";
import Form from "@saleor/components/Form"; import Form, { FormDataWithOpts } from "@saleor/components/Form";
import { WithFormId } from "@saleor/components/Form/ExitFormDialogProvider"; import { WithFormId } from "@saleor/components/Form/ExitFormDialogProvider";
import Grid from "@saleor/components/Grid"; import Grid from "@saleor/components/Grid";
import Metadata from "@saleor/components/Metadata/Metadata"; import Metadata from "@saleor/components/Metadata/Metadata";
@ -112,30 +112,38 @@ export const ShippingZoneRatesPage: React.FC<ShippingZoneRatesPageProps> = ({
makeChangeHandler: makeMetadataChangeHandler makeChangeHandler: makeMetadataChangeHandler
} = useMetadataChangeTrigger(); } = useMetadataChangeTrigger();
const checkIfSaveIsDisabled = (
data: FormDataWithOpts<ShippingZoneRateUpdateFormData>
) => {
const formDisabled = data.channelListings?.some(channel =>
validatePrice(channel.price)
);
const formIsUnchanged =
!data.hasChanged && !hasChannelChanged && !havePostalCodesChanged;
return disabled || formDisabled || formIsUnchanged;
};
return ( return (
<Form <Form
confirmLeave confirmLeave
initial={initialForm} initial={initialForm}
onSubmit={onSubmit} onSubmit={onSubmit}
formId={formId} formId={formId}
checkIfSaveIsDisabled={checkIfSaveIsDisabled}
> >
{({ change, data, hasChanged, submit, set, triggerChange }) => { {({ change, data, isSaveDisabled, submit, set, triggerChange }) => {
const handleChannelsChange = createChannelsChangeHandler( const handleChannelsChange = createChannelsChangeHandler(
shippingChannels, shippingChannels,
onChannelsChange, onChannelsChange,
triggerChange triggerChange
); );
const formDisabled = data.channelListings?.some(channel =>
validatePrice(channel.price)
);
const onDescriptionChange = (description: OutputData) => { const onDescriptionChange = (description: OutputData) => {
set({ description }); set({ description });
triggerChange(); triggerChange();
}; };
const changeMetadata = makeMetadataChangeHandler(change); const changeMetadata = makeMetadataChangeHandler(change);
const formIsUnchanged =
!hasChanged && !hasChannelChanged && !havePostalCodesChanged;
return ( return (
<Container> <Container>
@ -212,7 +220,7 @@ export const ShippingZoneRatesPage: React.FC<ShippingZoneRatesPageProps> = ({
</div> </div>
</Grid> </Grid>
<Savebar <Savebar
disabled={disabled || formDisabled || formIsUnchanged} disabled={isSaveDisabled}
onCancel={onBack} onCancel={onBack}
onDelete={onDelete} onDelete={onDelete}
onSubmit={submit} onSubmit={submit}

View file

@ -134,8 +134,9 @@ const SiteSettingsPage: React.FC<SiteSettingsPageProps> = props => {
return submitFunc(data); return submitFunc(data);
}} }}
confirmLeave confirmLeave
disabled={disabled}
> >
{({ change, data, hasChanged, submit }) => { {({ change, data, isSaveDisabled, submit }) => {
const countryChoices = mapCountriesToChoices(shop?.countries || []); const countryChoices = mapCountriesToChoices(shop?.countries || []);
const handleCountryChange = createSingleAutocompleteSelectHandler( const handleCountryChange = createSingleAutocompleteSelectHandler(
change, change,
@ -201,7 +202,7 @@ const SiteSettingsPage: React.FC<SiteSettingsPageProps> = props => {
</Grid> </Grid>
<Savebar <Savebar
state={saveButtonBarState} state={saveButtonBarState}
disabled={disabled || !hasChanged} disabled={isSaveDisabled}
onCancel={onBack} onCancel={onBack}
onSubmit={submit} onSubmit={submit}
/> />

View file

@ -101,8 +101,13 @@ const StaffDetailsPage: React.FC<StaffDetailsPageProps> = ({
}; };
return ( return (
<Form confirmLeave initial={initialForm} onSubmit={onSubmit}> <Form
{({ data: formData, change, hasChanged, submit, toggleValue }) => { confirmLeave
initial={initialForm}
onSubmit={onSubmit}
disabled={disabled}
>
{({ data: formData, change, isSaveDisabled, submit, toggleValue }) => {
const permissionGroupsChange = createMultiAutocompleteSelectHandler( const permissionGroupsChange = createMultiAutocompleteSelectHandler(
toggleValue, toggleValue,
setPermissionGroupsDisplayValues, setPermissionGroupsDisplayValues,
@ -187,7 +192,7 @@ const StaffDetailsPage: React.FC<StaffDetailsPageProps> = ({
</div> </div>
</Grid> </Grid>
<Savebar <Savebar
disabled={disabled || !hasChanged} disabled={isSaveDisabled}
state={saveButtonBarState} state={saveButtonBarState}
onCancel={onBack} onCancel={onBack}
onSubmit={submit} onSubmit={submit}

View file

@ -46,8 +46,13 @@ const CountryListPage: React.FC<CountryListPageProps> = ({
showGross: maybe(() => shop.displayGrossPrices, false) showGross: maybe(() => shop.displayGrossPrices, false)
}; };
return ( return (
<Form confirmLeave initial={initialForm} onSubmit={onSubmit}> <Form
{({ change, data, hasChanged, submit }) => ( confirmLeave
initial={initialForm}
onSubmit={onSubmit}
disabled={disabled}
>
{({ change, data, isSaveDisabled, submit }) => (
<> <>
<Container> <Container>
<Backlink onClick={onBack}> <Backlink onClick={onBack}>
@ -77,7 +82,7 @@ const CountryListPage: React.FC<CountryListPageProps> = ({
</Grid> </Grid>
</Container> </Container>
<Savebar <Savebar
disabled={disabled || !hasChanged} disabled={isSaveDisabled}
state={saveButtonBarState} state={saveButtonBarState}
onCancel={onBack} onCancel={onBack}
onSubmit={submit} onSubmit={submit}

View file

@ -84,8 +84,13 @@ const WarehouseDetailsPage: React.FC<WarehouseDetailsPageProps> = ({
}; };
return ( return (
<Form confirmLeave initial={initialForm} onSubmit={handleSubmit}> <Form
{({ change, data, hasChanged, submit, set }) => { confirmLeave
initial={initialForm}
onSubmit={handleSubmit}
disabled={disabled}
>
{({ change, data, isSaveDisabled, submit, set }) => {
const countryChoices = mapCountriesToChoices(countries); const countryChoices = mapCountriesToChoices(countries);
const handleCountryChange = createSingleAutocompleteSelectHandler( const handleCountryChange = createSingleAutocompleteSelectHandler(
change, change,
@ -134,7 +139,7 @@ const WarehouseDetailsPage: React.FC<WarehouseDetailsPageProps> = ({
</div> </div>
</Grid> </Grid>
<Savebar <Savebar
disabled={disabled || !hasChanged} disabled={isSaveDisabled}
onCancel={onBack} onCancel={onBack}
onDelete={onDelete} onDelete={onDelete}
onSubmit={submit} onSubmit={submit}