Enable save button on discount pages (#2319)

* Enable save button on discount pages

* Update changelog with save button enable update

* Refactor channel input map
This commit is contained in:
Dawid 2022-09-28 10:42:28 +02:00 committed by GitHub
parent 781eafa68e
commit 575a91b5e9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 180 additions and 47 deletions

View file

@ -12,6 +12,7 @@ All notable, unreleased changes to this project will be documented in this file.
- Fix exit form dialog false positive - #2311 by @orzechdev - Fix exit form dialog false positive - #2311 by @orzechdev
- Handle form errors before product creation - #2299 by @orzechdev - Handle form errors before product creation - #2299 by @orzechdev
- Fix no product error on unconfirmed order lines - #2324 by @orzechdev - Fix no product error on unconfirmed order lines - #2324 by @orzechdev
- Enable save button on discount pages - #2319 by @orzechdev
## 3.4 ## 3.4

View file

@ -2,6 +2,8 @@ import {
ChannelSaleFormData, ChannelSaleFormData,
SaleDetailsPageFormData, SaleDetailsPageFormData,
} from "@saleor/discounts/components/SaleDetailsPage"; } from "@saleor/discounts/components/SaleDetailsPage";
import { VoucherDetailsPageFormData } from "@saleor/discounts/components/VoucherDetailsPage";
import { RequirementsPicker } from "@saleor/discounts/types";
import { import {
ChannelDetailsFragment, ChannelDetailsFragment,
ChannelFragment, ChannelFragment,
@ -415,3 +417,11 @@ export const validateSalePrice = (
? channel.percentageValue ? channel.percentageValue
: channel.fixedValue, : channel.fixedValue,
); );
export const validateVoucherPrice = (
data: VoucherDetailsPageFormData,
channel: ChannelVoucherData,
) =>
validatePrice(channel.discountValue) ||
(data.requirementsPicker === RequirementsPicker.ORDER &&
validatePrice(channel.minSpent));

View file

@ -94,7 +94,7 @@ const SaleCreatePage: React.FC<SaleCreatePageProps> = ({
formId={SALE_CREATE_FORM_ID} formId={SALE_CREATE_FORM_ID}
checkIfSaveIsDisabled={checkIfSaveIsDisabled} checkIfSaveIsDisabled={checkIfSaveIsDisabled}
> >
{({ change, data, submit, triggerChange, isSaveDisabled }) => { {({ change, data, submit, triggerChange }) => {
const handleChannelChange = createSaleChannelsChangeHandler( const handleChannelChange = createSaleChannelsChangeHandler(
data.channelListings, data.channelListings,
onChannelsChange, onChannelsChange,
@ -155,7 +155,7 @@ const SaleCreatePage: React.FC<SaleCreatePageProps> = ({
<Metadata data={data} onChange={changeMetadata} /> <Metadata data={data} onChange={changeMetadata} />
</Grid> </Grid>
<Savebar <Savebar
disabled={isSaveDisabled} disabled={disabled}
onCancel={onBack} onCancel={onBack}
onSubmit={submit} onSubmit={submit}
state={saveButtonBarState} state={saveButtonBarState}

View file

@ -9,7 +9,10 @@ import Metadata, { MetadataFormData } from "@saleor/components/Metadata";
import PageHeader from "@saleor/components/PageHeader"; import PageHeader from "@saleor/components/PageHeader";
import Savebar from "@saleor/components/Savebar"; import Savebar from "@saleor/components/Savebar";
import { Tab, TabContainer } from "@saleor/components/Tab"; import { Tab, TabContainer } from "@saleor/components/Tab";
import { createSaleChannelsChangeHandler } from "@saleor/discounts/handlers"; import {
createSaleChannelsChangeHandler,
createSaleUpdateHandler,
} from "@saleor/discounts/handlers";
import { saleListUrl } from "@saleor/discounts/urls"; import { saleListUrl } from "@saleor/discounts/urls";
import { SALE_UPDATE_FORM_ID } from "@saleor/discounts/views/SaleDetails/types"; import { SALE_UPDATE_FORM_ID } from "@saleor/discounts/views/SaleDetails/types";
import { import {
@ -130,6 +133,10 @@ const SaleDetailsPage: React.FC<SaleDetailsPageProps> = ({
const intl = useIntl(); const intl = useIntl();
const navigate = useNavigator(); const navigate = useNavigator();
const [localErrors, setLocalErrors] = React.useState<DiscountErrorFragment[]>(
[],
);
const { const {
makeChangeHandler: makeMetadataChangeHandler, makeChangeHandler: makeMetadataChangeHandler,
} = useMetadataChangeTrigger(); } = useMetadataChangeTrigger();
@ -159,7 +166,7 @@ const SaleDetailsPage: React.FC<SaleDetailsPageProps> = ({
formId={SALE_UPDATE_FORM_ID} formId={SALE_UPDATE_FORM_ID}
checkIfSaveIsDisabled={checkIfSaveIsDisabled} checkIfSaveIsDisabled={checkIfSaveIsDisabled}
> >
{({ change, data, submit, triggerChange, isSaveDisabled }) => { {({ change, data, submit, triggerChange }) => {
const handleChannelChange = createSaleChannelsChangeHandler( const handleChannelChange = createSaleChannelsChangeHandler(
data.channelListings, data.channelListings,
onChannelsChange, onChannelsChange,
@ -168,6 +175,10 @@ const SaleDetailsPage: React.FC<SaleDetailsPageProps> = ({
); );
const changeMetadata = makeMetadataChangeHandler(change); const changeMetadata = makeMetadataChangeHandler(change);
const handleSubmit = createSaleUpdateHandler(submit, setLocalErrors);
const allErrors = [...localErrors, ...errors];
return ( return (
<Container> <Container>
<Backlink href={saleListUrl()}> <Backlink href={saleListUrl()}>
@ -188,7 +199,7 @@ const SaleDetailsPage: React.FC<SaleDetailsPageProps> = ({
<SaleValue <SaleValue
data={data} data={data}
disabled={disabled} disabled={disabled}
errors={errors} errors={allErrors}
onChange={handleChannelChange} onChange={handleChannelChange}
/> />
<CardSpacer /> <CardSpacer />
@ -346,10 +357,10 @@ const SaleDetailsPage: React.FC<SaleDetailsPageProps> = ({
<Metadata data={data} onChange={changeMetadata} /> <Metadata data={data} onChange={changeMetadata} />
</Grid> </Grid>
<Savebar <Savebar
disabled={isSaveDisabled} disabled={disabled}
onCancel={() => navigate(saleListUrl())} onCancel={() => navigate(saleListUrl())}
onDelete={onRemove} onDelete={onRemove}
onSubmit={submit} onSubmit={() => handleSubmit(data)}
state={saveButtonBarState} state={saveButtonBarState}
/> />
</Container> </Container>

View file

@ -92,6 +92,7 @@ const SaleValue: React.FC<SaleValueProps> = ({
const error = formErrors.value?.channels?.find( const error = formErrors.value?.channels?.find(
id => id === listing.id, id => id === listing.id,
); );
return ( return (
<TableRow <TableRow
key={listing?.id || `skeleton-${index}`} key={listing?.id || `skeleton-${index}`}
@ -109,6 +110,7 @@ const SaleValue: React.FC<SaleValueProps> = ({
? getDiscountErrorMessage(formErrors.value, intl) ? getDiscountErrorMessage(formErrors.value, intl)
: "" : ""
} }
error={!!error}
disabled={disabled} disabled={disabled}
listing={listing} listing={listing}
onChange={onChange} onChange={onChange}

View file

@ -10,6 +10,7 @@ interface SaleValueTextFieldProps {
dataType: SaleType; dataType: SaleType;
helperText: string; helperText: string;
disabled: boolean; disabled: boolean;
error: boolean;
listing: ChannelSaleFormData; listing: ChannelSaleFormData;
onChange: SaleValueInputOnChangeType; onChange: SaleValueInputOnChangeType;
} }
@ -18,6 +19,7 @@ const SaleValueTextField: React.FC<SaleValueTextFieldProps> = ({
dataType, dataType,
helperText, helperText,
disabled, disabled,
error,
listing, listing,
onChange, onChange,
}) => { }) => {
@ -32,6 +34,7 @@ const SaleValueTextField: React.FC<SaleValueTextFieldProps> = ({
<TextField <TextField
disabled={disabled} disabled={disabled}
helperText={helperText || ""} helperText={helperText || ""}
error={error}
name="value" name="value"
onChange={e => { onChange={e => {
onChange(id, e.target.value); onChange(id, e.target.value);

View file

@ -110,7 +110,7 @@ const VoucherCreatePage: React.FC<VoucherCreatePageProps> = ({
formId={VOUCHER_CREATE_FORM_ID} formId={VOUCHER_CREATE_FORM_ID}
checkIfSaveIsDisabled={checkIfSaveIsDisabled} checkIfSaveIsDisabled={checkIfSaveIsDisabled}
> >
{({ change, data, submit, triggerChange, set, isSaveDisabled }) => { {({ change, data, submit, triggerChange, set }) => {
const handleDiscountTypeChange = createDiscountTypeChangeHandler( const handleDiscountTypeChange = createDiscountTypeChangeHandler(
change, change,
); );
@ -203,7 +203,7 @@ const VoucherCreatePage: React.FC<VoucherCreatePageProps> = ({
<Metadata data={data} onChange={changeMetadata} /> <Metadata data={data} onChange={changeMetadata} />
</Grid> </Grid>
<Savebar <Savebar
disabled={isSaveDisabled} disabled={disabled}
onCancel={() => navigate(voucherListUrl())} onCancel={() => navigate(voucherListUrl())}
onSubmit={submit} onSubmit={submit}
state={saveButtonBarState} state={saveButtonBarState}

View file

@ -14,6 +14,7 @@ import { Tab, TabContainer } from "@saleor/components/Tab";
import { import {
createChannelsChangeHandler, createChannelsChangeHandler,
createDiscountTypeChangeHandler, createDiscountTypeChangeHandler,
createVoucherUpdateHandler,
} from "@saleor/discounts/handlers"; } from "@saleor/discounts/handlers";
import { DiscountTypeEnum, RequirementsPicker } from "@saleor/discounts/types"; import { DiscountTypeEnum, RequirementsPicker } from "@saleor/discounts/types";
import { voucherListUrl } from "@saleor/discounts/urls"; import { voucherListUrl } from "@saleor/discounts/urls";
@ -27,7 +28,6 @@ import {
import useNavigator from "@saleor/hooks/useNavigator"; import useNavigator from "@saleor/hooks/useNavigator";
import { sectionNames } from "@saleor/intl"; import { sectionNames } from "@saleor/intl";
import { ConfirmButtonTransitionState } from "@saleor/macaw-ui"; import { ConfirmButtonTransitionState } from "@saleor/macaw-ui";
import { validatePrice } from "@saleor/products/utils/validation";
import { mapEdgesToItems, mapMetadataItemToInput } from "@saleor/utils/maps"; import { mapEdgesToItems, mapMetadataItemToInput } from "@saleor/utils/maps";
import useMetadataChangeTrigger from "@saleor/utils/metadata/useMetadataChangeTrigger"; import useMetadataChangeTrigger from "@saleor/utils/metadata/useMetadataChangeTrigger";
import React from "react"; import React from "react";
@ -136,6 +136,10 @@ const VoucherDetailsPage: React.FC<VoucherDetailsPageProps> = ({
const intl = useIntl(); const intl = useIntl();
const navigate = useNavigator(); const navigate = useNavigator();
const [localErrors, setLocalErrors] = React.useState<DiscountErrorFragment[]>(
[],
);
const { const {
makeChangeHandler: makeMetadataChangeHandler, makeChangeHandler: makeMetadataChangeHandler,
} = useMetadataChangeTrigger(); } = useMetadataChangeTrigger();
@ -192,17 +196,12 @@ const VoucherDetailsPage: React.FC<VoucherDetailsPageProps> = ({
onChannelsChange, onChannelsChange,
triggerChange, triggerChange,
); );
const formDisabled =
(data.discountType.toString() !== "SHIPPING" &&
data.channelListings?.some(
channel =>
validatePrice(channel.discountValue) ||
(data.requirementsPicker === RequirementsPicker.ORDER &&
validatePrice(channel.minSpent)),
)) ||
data.usageLimit <= 0;
const changeMetadata = makeMetadataChangeHandler(change); const changeMetadata = makeMetadataChangeHandler(change);
const handleSubmit = createVoucherUpdateHandler(submit, setLocalErrors);
const allErrors = [...localErrors, ...errors];
return ( return (
<Container> <Container>
<Backlink href={voucherListUrl()}> <Backlink href={voucherListUrl()}>
@ -230,7 +229,7 @@ const VoucherDetailsPage: React.FC<VoucherDetailsPageProps> = ({
<VoucherValue <VoucherValue
data={data} data={data}
disabled={disabled} disabled={disabled}
errors={errors} errors={allErrors}
onChange={change} onChange={change}
onChannelChange={handleChannelChange} onChannelChange={handleChannelChange}
variant="update" variant="update"
@ -415,9 +414,9 @@ const VoucherDetailsPage: React.FC<VoucherDetailsPageProps> = ({
</Grid> </Grid>
<Savebar <Savebar
onCancel={() => navigate(voucherListUrl())} onCancel={() => navigate(voucherListUrl())}
disabled={disabled || formDisabled} disabled={disabled}
onDelete={onRemove} onDelete={onRemove}
onSubmit={submit} onSubmit={() => handleSubmit(data)}
state={saveButtonBarState} state={saveButtonBarState}
/> />
</Container> </Container>

43
src/discounts/data.ts Normal file
View file

@ -0,0 +1,43 @@
import { ChannelVoucherData } from "@saleor/channels/utils";
import { VoucherChannelListingAddInput } from "@saleor/graphql";
import { VoucherDetailsPageFormData } from "./components/VoucherDetailsPage";
import { RequirementsPicker } from "./types";
const getChannelDiscountValue = (
channel: ChannelVoucherData,
formData: VoucherDetailsPageFormData,
) =>
formData.discountType.toString() === "SHIPPING" ? 100 : channel.discountValue;
const getChannelMinAmountSpent = (
channel: ChannelVoucherData,
formData: VoucherDetailsPageFormData,
) => {
if (formData.requirementsPicker === RequirementsPicker.NONE) {
return null;
}
if (formData.requirementsPicker === RequirementsPicker.ITEM) {
return 0;
}
return channel.minSpent;
};
const mapChannelToChannelInput = (formData: VoucherDetailsPageFormData) => (
channel: ChannelVoucherData,
) => ({
channelId: channel.id,
discountValue: getChannelDiscountValue(channel, formData),
minAmountSpent: getChannelMinAmountSpent(channel, formData),
});
const filterNotDiscountedChannel = (
channelInput: VoucherChannelListingAddInput,
) => !!channelInput.discountValue;
export const getAddedChannelsInputFromFormData = (
formData: VoucherDetailsPageFormData,
) =>
formData.channelListings
?.map(mapChannelToChannelInput(formData))
.filter(filterNotDiscountedChannel) || [];

View file

@ -1,15 +1,26 @@
import { ChannelVoucherData } from "@saleor/channels/utils"; import {
ChannelVoucherData,
validateSalePrice,
validateVoucherPrice,
} from "@saleor/channels/utils";
import { import {
ChannelSaleFormData, ChannelSaleFormData,
SaleDetailsPageFormData, SaleDetailsPageFormData,
} from "@saleor/discounts/components/SaleDetailsPage"; } from "@saleor/discounts/components/SaleDetailsPage";
import { VoucherDetailsPageFormData } from "@saleor/discounts/components/VoucherDetailsPage"; import { VoucherDetailsPageFormData } from "@saleor/discounts/components/VoucherDetailsPage";
import { DiscountTypeEnum, RequirementsPicker } from "@saleor/discounts/types"; import { DiscountTypeEnum } from "@saleor/discounts/types";
import { SaleType, VoucherTypeEnum } from "@saleor/graphql"; import {
import { ChangeEvent, FormChange } from "@saleor/hooks/useForm"; DiscountErrorCode,
DiscountErrorFragment,
SaleType,
VoucherTypeEnum,
} from "@saleor/graphql";
import { ChangeEvent, FormChange, SubmitPromise } from "@saleor/hooks/useForm";
import { RequireOnlyOne } from "@saleor/misc"; import { RequireOnlyOne } from "@saleor/misc";
import { arrayDiff } from "@saleor/utils/arrays"; import { arrayDiff } from "@saleor/utils/arrays";
import { getAddedChannelsInputFromFormData } from "./data";
export interface ChannelArgs { export interface ChannelArgs {
discountValue: string; discountValue: string;
minSpent: string; minSpent: string;
@ -116,20 +127,7 @@ export const getChannelsVariables = (
return { return {
id, id,
input: { input: {
addChannels: addChannels: getAddedChannelsInputFromFormData(formData),
formData.channelListings?.map(channel => ({
channelId: channel.id,
discountValue:
formData.discountType.toString() === "SHIPPING"
? 100
: channel.discountValue,
minAmountSpent:
formData.requirementsPicker === RequirementsPicker.NONE
? null
: formData.requirementsPicker === RequirementsPicker.ITEM
? 0
: channel.minSpent,
})) || [],
removeChannels: idsDiff.removed, removeChannels: idsDiff.removed,
}, },
}; };
@ -148,14 +146,80 @@ export const getSaleChannelsVariables = (
id, id,
input: { input: {
addChannels: addChannels:
formData.channelListings?.map(channel => ({ formData.channelListings
channelId: channel.id, ?.map(channel => ({
discountValue: channelId: channel.id,
formData.type === SaleType.FIXED discountValue:
? channel.fixedValue formData.type === SaleType.FIXED
: channel.percentageValue, ? channel.fixedValue
})) || [], : channel.percentageValue,
}))
.filter(channel => !!channel.discountValue) || [],
removeChannels: idsDiff.removed, removeChannels: idsDiff.removed,
}, },
}; };
}; };
export function createSaleUpdateHandler(
submit: (data: SaleDetailsPageFormData) => SubmitPromise<any[]>,
setLocalErrors: (errors: DiscountErrorFragment[]) => void,
) {
return async (formData: SaleDetailsPageFormData) => {
const { channelListings } = formData;
const invalidChannelListings = channelListings
?.filter(channel => validateSalePrice(formData, channel))
.map(channel => channel.id);
const localErrors: DiscountErrorFragment[] = !!invalidChannelListings?.length
? [
{
__typename: "DiscountError",
code: DiscountErrorCode.INVALID,
field: "value",
channels: invalidChannelListings,
message: "Invalid discount value",
},
]
: [];
setLocalErrors(localErrors);
if (!!localErrors.length) {
return localErrors;
}
return submit(formData);
};
}
export function createVoucherUpdateHandler(
submit: (data: VoucherDetailsPageFormData) => SubmitPromise<any[]>,
setLocalErrors: (errors: DiscountErrorFragment[]) => void,
) {
return async (formData: VoucherDetailsPageFormData) => {
const { channelListings } = formData;
const invalidChannelListings = channelListings
?.filter(channel => validateVoucherPrice(formData, channel))
.map(channel => channel.id);
const localErrors: DiscountErrorFragment[] = !!invalidChannelListings?.length
? [
{
__typename: "DiscountError",
code: DiscountErrorCode.INVALID,
field: "discountValue",
channels: invalidChannelListings,
message: "Invalid discount value",
},
]
: [];
setLocalErrors(localErrors);
if (!!localErrors.length) {
return localErrors;
}
return submit(formData);
};
}