From 575a91b5e9218a9fab5c3f7d7dd1aa09890aff0d Mon Sep 17 00:00:00 2001 From: Dawid Date: Wed, 28 Sep 2022 10:42:28 +0200 Subject: [PATCH] Enable save button on discount pages (#2319) * Enable save button on discount pages * Update changelog with save button enable update * Refactor channel input map --- CHANGELOG.md | 1 + src/channels/utils.ts | 10 ++ .../SaleCreatePage/SaleCreatePage.tsx | 4 +- .../SaleDetailsPage/SaleDetailsPage.tsx | 21 +++- .../components/SaleValue/SaleValue.tsx | 2 + .../SaleValue/SaleValueTextField.tsx | 3 + .../VoucherCreatePage/VoucherCreatePage.tsx | 4 +- .../VoucherDetailsPage/VoucherDetailsPage.tsx | 25 ++-- src/discounts/data.ts | 43 +++++++ src/discounts/handlers.ts | 114 ++++++++++++++---- 10 files changed, 180 insertions(+), 47 deletions(-) create mode 100644 src/discounts/data.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index c0b233d16..d4538a56f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 - Handle form errors before product creation - #2299 by @orzechdev - Fix no product error on unconfirmed order lines - #2324 by @orzechdev +- Enable save button on discount pages - #2319 by @orzechdev ## 3.4 diff --git a/src/channels/utils.ts b/src/channels/utils.ts index 67260f83f..116c64db9 100644 --- a/src/channels/utils.ts +++ b/src/channels/utils.ts @@ -2,6 +2,8 @@ import { ChannelSaleFormData, SaleDetailsPageFormData, } from "@saleor/discounts/components/SaleDetailsPage"; +import { VoucherDetailsPageFormData } from "@saleor/discounts/components/VoucherDetailsPage"; +import { RequirementsPicker } from "@saleor/discounts/types"; import { ChannelDetailsFragment, ChannelFragment, @@ -415,3 +417,11 @@ export const validateSalePrice = ( ? channel.percentageValue : channel.fixedValue, ); + +export const validateVoucherPrice = ( + data: VoucherDetailsPageFormData, + channel: ChannelVoucherData, +) => + validatePrice(channel.discountValue) || + (data.requirementsPicker === RequirementsPicker.ORDER && + validatePrice(channel.minSpent)); diff --git a/src/discounts/components/SaleCreatePage/SaleCreatePage.tsx b/src/discounts/components/SaleCreatePage/SaleCreatePage.tsx index 146d48b60..4fef093f9 100644 --- a/src/discounts/components/SaleCreatePage/SaleCreatePage.tsx +++ b/src/discounts/components/SaleCreatePage/SaleCreatePage.tsx @@ -94,7 +94,7 @@ const SaleCreatePage: React.FC = ({ formId={SALE_CREATE_FORM_ID} checkIfSaveIsDisabled={checkIfSaveIsDisabled} > - {({ change, data, submit, triggerChange, isSaveDisabled }) => { + {({ change, data, submit, triggerChange }) => { const handleChannelChange = createSaleChannelsChangeHandler( data.channelListings, onChannelsChange, @@ -155,7 +155,7 @@ const SaleCreatePage: React.FC = ({ = ({ const intl = useIntl(); const navigate = useNavigator(); + const [localErrors, setLocalErrors] = React.useState( + [], + ); + const { makeChangeHandler: makeMetadataChangeHandler, } = useMetadataChangeTrigger(); @@ -159,7 +166,7 @@ const SaleDetailsPage: React.FC = ({ formId={SALE_UPDATE_FORM_ID} checkIfSaveIsDisabled={checkIfSaveIsDisabled} > - {({ change, data, submit, triggerChange, isSaveDisabled }) => { + {({ change, data, submit, triggerChange }) => { const handleChannelChange = createSaleChannelsChangeHandler( data.channelListings, onChannelsChange, @@ -168,6 +175,10 @@ const SaleDetailsPage: React.FC = ({ ); const changeMetadata = makeMetadataChangeHandler(change); + const handleSubmit = createSaleUpdateHandler(submit, setLocalErrors); + + const allErrors = [...localErrors, ...errors]; + return ( @@ -188,7 +199,7 @@ const SaleDetailsPage: React.FC = ({ @@ -346,10 +357,10 @@ const SaleDetailsPage: React.FC = ({ navigate(saleListUrl())} onDelete={onRemove} - onSubmit={submit} + onSubmit={() => handleSubmit(data)} state={saveButtonBarState} /> diff --git a/src/discounts/components/SaleValue/SaleValue.tsx b/src/discounts/components/SaleValue/SaleValue.tsx index fb2f5bb0d..bce60f6ac 100644 --- a/src/discounts/components/SaleValue/SaleValue.tsx +++ b/src/discounts/components/SaleValue/SaleValue.tsx @@ -92,6 +92,7 @@ const SaleValue: React.FC = ({ const error = formErrors.value?.channels?.find( id => id === listing.id, ); + return ( = ({ ? getDiscountErrorMessage(formErrors.value, intl) : "" } + error={!!error} disabled={disabled} listing={listing} onChange={onChange} diff --git a/src/discounts/components/SaleValue/SaleValueTextField.tsx b/src/discounts/components/SaleValue/SaleValueTextField.tsx index a2a9cbd8b..f95728cee 100644 --- a/src/discounts/components/SaleValue/SaleValueTextField.tsx +++ b/src/discounts/components/SaleValue/SaleValueTextField.tsx @@ -10,6 +10,7 @@ interface SaleValueTextFieldProps { dataType: SaleType; helperText: string; disabled: boolean; + error: boolean; listing: ChannelSaleFormData; onChange: SaleValueInputOnChangeType; } @@ -18,6 +19,7 @@ const SaleValueTextField: React.FC = ({ dataType, helperText, disabled, + error, listing, onChange, }) => { @@ -32,6 +34,7 @@ const SaleValueTextField: React.FC = ({ { onChange(id, e.target.value); diff --git a/src/discounts/components/VoucherCreatePage/VoucherCreatePage.tsx b/src/discounts/components/VoucherCreatePage/VoucherCreatePage.tsx index 8df89680e..82ffaa8f3 100644 --- a/src/discounts/components/VoucherCreatePage/VoucherCreatePage.tsx +++ b/src/discounts/components/VoucherCreatePage/VoucherCreatePage.tsx @@ -110,7 +110,7 @@ const VoucherCreatePage: React.FC = ({ formId={VOUCHER_CREATE_FORM_ID} checkIfSaveIsDisabled={checkIfSaveIsDisabled} > - {({ change, data, submit, triggerChange, set, isSaveDisabled }) => { + {({ change, data, submit, triggerChange, set }) => { const handleDiscountTypeChange = createDiscountTypeChangeHandler( change, ); @@ -203,7 +203,7 @@ const VoucherCreatePage: React.FC = ({ navigate(voucherListUrl())} onSubmit={submit} state={saveButtonBarState} diff --git a/src/discounts/components/VoucherDetailsPage/VoucherDetailsPage.tsx b/src/discounts/components/VoucherDetailsPage/VoucherDetailsPage.tsx index 8c5899c0c..1d2cd03a2 100644 --- a/src/discounts/components/VoucherDetailsPage/VoucherDetailsPage.tsx +++ b/src/discounts/components/VoucherDetailsPage/VoucherDetailsPage.tsx @@ -14,6 +14,7 @@ import { Tab, TabContainer } from "@saleor/components/Tab"; import { createChannelsChangeHandler, createDiscountTypeChangeHandler, + createVoucherUpdateHandler, } from "@saleor/discounts/handlers"; import { DiscountTypeEnum, RequirementsPicker } from "@saleor/discounts/types"; import { voucherListUrl } from "@saleor/discounts/urls"; @@ -27,7 +28,6 @@ import { import useNavigator from "@saleor/hooks/useNavigator"; import { sectionNames } from "@saleor/intl"; import { ConfirmButtonTransitionState } from "@saleor/macaw-ui"; -import { validatePrice } from "@saleor/products/utils/validation"; import { mapEdgesToItems, mapMetadataItemToInput } from "@saleor/utils/maps"; import useMetadataChangeTrigger from "@saleor/utils/metadata/useMetadataChangeTrigger"; import React from "react"; @@ -136,6 +136,10 @@ const VoucherDetailsPage: React.FC = ({ const intl = useIntl(); const navigate = useNavigator(); + const [localErrors, setLocalErrors] = React.useState( + [], + ); + const { makeChangeHandler: makeMetadataChangeHandler, } = useMetadataChangeTrigger(); @@ -192,17 +196,12 @@ const VoucherDetailsPage: React.FC = ({ onChannelsChange, 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 handleSubmit = createVoucherUpdateHandler(submit, setLocalErrors); + + const allErrors = [...localErrors, ...errors]; + return ( @@ -230,7 +229,7 @@ const VoucherDetailsPage: React.FC = ({ = ({ navigate(voucherListUrl())} - disabled={disabled || formDisabled} + disabled={disabled} onDelete={onRemove} - onSubmit={submit} + onSubmit={() => handleSubmit(data)} state={saveButtonBarState} /> diff --git a/src/discounts/data.ts b/src/discounts/data.ts new file mode 100644 index 000000000..477de92d6 --- /dev/null +++ b/src/discounts/data.ts @@ -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) || []; diff --git a/src/discounts/handlers.ts b/src/discounts/handlers.ts index 487571a02..44ba8733d 100644 --- a/src/discounts/handlers.ts +++ b/src/discounts/handlers.ts @@ -1,15 +1,26 @@ -import { ChannelVoucherData } from "@saleor/channels/utils"; +import { + ChannelVoucherData, + validateSalePrice, + validateVoucherPrice, +} from "@saleor/channels/utils"; import { ChannelSaleFormData, SaleDetailsPageFormData, } from "@saleor/discounts/components/SaleDetailsPage"; import { VoucherDetailsPageFormData } from "@saleor/discounts/components/VoucherDetailsPage"; -import { DiscountTypeEnum, RequirementsPicker } from "@saleor/discounts/types"; -import { SaleType, VoucherTypeEnum } from "@saleor/graphql"; -import { ChangeEvent, FormChange } from "@saleor/hooks/useForm"; +import { DiscountTypeEnum } from "@saleor/discounts/types"; +import { + DiscountErrorCode, + DiscountErrorFragment, + SaleType, + VoucherTypeEnum, +} from "@saleor/graphql"; +import { ChangeEvent, FormChange, SubmitPromise } from "@saleor/hooks/useForm"; import { RequireOnlyOne } from "@saleor/misc"; import { arrayDiff } from "@saleor/utils/arrays"; +import { getAddedChannelsInputFromFormData } from "./data"; + export interface ChannelArgs { discountValue: string; minSpent: string; @@ -116,20 +127,7 @@ export const getChannelsVariables = ( return { id, input: { - addChannels: - 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, - })) || [], + addChannels: getAddedChannelsInputFromFormData(formData), removeChannels: idsDiff.removed, }, }; @@ -148,14 +146,80 @@ export const getSaleChannelsVariables = ( id, input: { addChannels: - formData.channelListings?.map(channel => ({ - channelId: channel.id, - discountValue: - formData.type === SaleType.FIXED - ? channel.fixedValue - : channel.percentageValue, - })) || [], + formData.channelListings + ?.map(channel => ({ + channelId: channel.id, + discountValue: + formData.type === SaleType.FIXED + ? channel.fixedValue + : channel.percentageValue, + })) + .filter(channel => !!channel.discountValue) || [], removeChannels: idsDiff.removed, }, }; }; + +export function createSaleUpdateHandler( + submit: (data: SaleDetailsPageFormData) => SubmitPromise, + 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, + 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); + }; +}