From 014ae0f362bd7aa87505d425e135526feb17e0ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20=C5=BBegle=C5=84?= Date: Wed, 8 Jun 2022 13:27:33 +0200 Subject: [PATCH] Fix variant creator (#2085) * Set channels for products without any variants * Fix duplicated_input_item error * Persist skus between steps * Cleanup code * Fix types --- .../ChannelsWithVariantsAvailabilityCard.tsx | 5 - .../CollectionCreatePage.tsx | 1 - .../CollectionDetailsPage.tsx | 1 - .../ChannelsAvailabilityCard.stories.tsx | 3 +- .../ChannelsAvailabilityCard.tsx | 8 +- .../SaleCreatePage/SaleCreatePage.tsx | 1 - .../SaleDetailsPage/SaleDetailsPage.tsx | 1 - .../VoucherCreatePage/VoucherCreatePage.tsx | 1 - .../VoucherDetailsPage/VoucherDetailsPage.tsx | 1 - .../ProductCreatePage/ProductCreatePage.tsx | 1 - .../ProductUpdatePage/ProductUpdatePage.tsx | 513 +++++++++--------- .../ProductVariantCreatorPage.tsx | 5 +- .../ProductVariantCreatorSummary.tsx | 2 +- .../createVariants.ts | 12 +- .../ProductVariantCreatorPage/reducer.ts | 47 +- .../ProductVariantCreatorPage/utils.ts | 13 + .../views/ProductUpdate/ProductUpdate.tsx | 2 +- .../views/ProductUpdate/handlers/index.ts | 6 +- .../ShippingZoneRatesCreatePage.tsx | 1 - .../ShippingZoneRatesPage.tsx | 1 - .../__snapshots__/Stories.test.ts.snap | 18 +- 21 files changed, 355 insertions(+), 288 deletions(-) diff --git a/src/channels/ChannelsWithVariantsAvailabilityCard/ChannelsWithVariantsAvailabilityCard.tsx b/src/channels/ChannelsWithVariantsAvailabilityCard/ChannelsWithVariantsAvailabilityCard.tsx index f08686907..f6a358cde 100644 --- a/src/channels/ChannelsWithVariantsAvailabilityCard/ChannelsWithVariantsAvailabilityCard.tsx +++ b/src/channels/ChannelsWithVariantsAvailabilityCard/ChannelsWithVariantsAvailabilityCard.tsx @@ -1,4 +1,3 @@ -import CannotDefineChannelsAvailabilityCard from "@saleor/channels/components/CannotDefineChannelsAvailabilityCard/CannotDefineChannelsAvailabilityCard"; import { ChannelData } from "@saleor/channels/utils"; import ChannelAvailabilityItemContent from "@saleor/components/ChannelsAvailabilityCard/Channel/ChannelAvailabilityItemContent"; import ChannelsAvailabilityCardWrapper, { @@ -66,10 +65,6 @@ const ChannelsWithVariantsAvailabilityCard: React.FC; - } - return ( = ({ }} managePermissions={[PermissionEnum.MANAGE_PRODUCTS]} errors={channelsErrors} - selectedChannelsCount={data.channelListings.length} allChannelsCount={channelsCount} channels={data.channelListings} disabled={disabled} diff --git a/src/collections/components/CollectionDetailsPage/CollectionDetailsPage.tsx b/src/collections/components/CollectionDetailsPage/CollectionDetailsPage.tsx index 5dc70bd0d..da14f35ef 100644 --- a/src/collections/components/CollectionDetailsPage/CollectionDetailsPage.tsx +++ b/src/collections/components/CollectionDetailsPage/CollectionDetailsPage.tsx @@ -141,7 +141,6 @@ const CollectionDetailsPage: React.FC = ({ }) }} errors={channelsErrors} - selectedChannelsCount={data.channelListings.length} allChannelsCount={channelsCount} channels={data.channelListings} disabled={disabled} diff --git a/src/components/ChannelsAvailabilityCard/ChannelsAvailabilityCard.stories.tsx b/src/components/ChannelsAvailabilityCard/ChannelsAvailabilityCard.stories.tsx index 587ed2e5b..df5740738 100644 --- a/src/components/ChannelsAvailabilityCard/ChannelsAvailabilityCard.stories.tsx +++ b/src/components/ChannelsAvailabilityCard/ChannelsAvailabilityCard.stories.tsx @@ -38,8 +38,7 @@ const props: ChannelsAvailabilityCardProps = { })), errors: [], onChange: () => undefined, - openModal: () => undefined, - selectedChannelsCount: 3 + openModal: () => undefined }; storiesOf("Generics / Channels availability card", module) diff --git a/src/components/ChannelsAvailabilityCard/ChannelsAvailabilityCard.tsx b/src/components/ChannelsAvailabilityCard/ChannelsAvailabilityCard.tsx index ebda38e9d..07b1bd655 100644 --- a/src/components/ChannelsAvailabilityCard/ChannelsAvailabilityCard.tsx +++ b/src/components/ChannelsAvailabilityCard/ChannelsAvailabilityCard.tsx @@ -17,7 +17,10 @@ import { ChannelOpts, ChannelsAvailabilityError, Messages } from "./types"; import { getChannelsAvailabilityMessages } from "./utils"; export interface ChannelsAvailability - extends Omit { + extends Omit< + ChannelsAvailabilityWrapperProps, + "children" | "selectedChannelsCount" + > { channels: ChannelData[]; channelsList: ChannelList[]; errors?: ChannelsAvailabilityError[]; @@ -36,7 +39,6 @@ export const ChannelsAvailability: React.FC = pro const { channelsList, errors = [], - selectedChannelsCount = 0, allChannelsCount = 0, channels, messages, @@ -57,7 +59,7 @@ export const ChannelsAvailability: React.FC = pro return ( = ({
({ id: channel.id, diff --git a/src/discounts/components/SaleDetailsPage/SaleDetailsPage.tsx b/src/discounts/components/SaleDetailsPage/SaleDetailsPage.tsx index 18d6d3916..8c6dc9434 100644 --- a/src/discounts/components/SaleDetailsPage/SaleDetailsPage.tsx +++ b/src/discounts/components/SaleDetailsPage/SaleDetailsPage.tsx @@ -334,7 +334,6 @@ const SaleDetailsPage: React.FC = ({ ({ id: channel.id, diff --git a/src/discounts/components/VoucherCreatePage/VoucherCreatePage.tsx b/src/discounts/components/VoucherCreatePage/VoucherCreatePage.tsx index fb2a8ab73..c85368053 100644 --- a/src/discounts/components/VoucherCreatePage/VoucherCreatePage.tsx +++ b/src/discounts/components/VoucherCreatePage/VoucherCreatePage.tsx @@ -191,7 +191,6 @@ const VoucherCreatePage: React.FC = ({
({ id: channel.id, diff --git a/src/discounts/components/VoucherDetailsPage/VoucherDetailsPage.tsx b/src/discounts/components/VoucherDetailsPage/VoucherDetailsPage.tsx index 10e7cb81d..2c64534ee 100644 --- a/src/discounts/components/VoucherDetailsPage/VoucherDetailsPage.tsx +++ b/src/discounts/components/VoucherDetailsPage/VoucherDetailsPage.tsx @@ -402,7 +402,6 @@ const VoucherDetailsPage: React.FC = ({ ({ id: channel.id, diff --git a/src/products/components/ProductCreatePage/ProductCreatePage.tsx b/src/products/components/ProductCreatePage/ProductCreatePage.tsx index d7e06547c..2e41a8b7f 100644 --- a/src/products/components/ProductCreatePage/ProductCreatePage.tsx +++ b/src/products/components/ProductCreatePage/ProductCreatePage.tsx @@ -348,7 +348,6 @@ export const ProductCreatePage: React.FC = ({ }) }} errors={channelsErrors} - selectedChannelsCount={data.channelListings?.length || 0} allChannelsCount={allChannelsCount} channels={data.channelListings || []} disabled={loading} diff --git a/src/products/components/ProductUpdatePage/ProductUpdatePage.tsx b/src/products/components/ProductUpdatePage/ProductUpdatePage.tsx index e7b514eb0..392555e83 100644 --- a/src/products/components/ProductUpdatePage/ProductUpdatePage.tsx +++ b/src/products/components/ProductUpdatePage/ProductUpdatePage.tsx @@ -290,260 +290,273 @@ export const ProductUpdatePage: React.FC = ({ submit, isSaveDisabled, attributeRichTextGetters - }) => ( - <> - - - {intl.formatMessage(sectionNames.products)} - - - {extensionMenuItems.length > 0 && ( - - )} - - -
- - - setMediaUrlModalStatus(true)} - getImageEditUrl={imageId => - productImageUrl(productId, imageId) + }) => { + const availabilityCommonProps = { + managePermissions: [PermissionEnum.MANAGE_PRODUCTS], + messages: { + hiddenLabel: intl.formatMessage({ + id: "saKXY3", + defaultMessage: "Not published", + description: "product label" + }), + + visibleLabel: intl.formatMessage({ + id: "qJedl0", + defaultMessage: "Published", + description: "product label" + }) + }, + errors: channelsErrors, + allChannelsCount, + disabled, + onChange: handlers.changeChannels, + openModal: openChannelsModal + }; + + return ( + <> + + + {intl.formatMessage(sectionNames.products)} + + + {extensionMenuItems.length > 0 && ( + + )} + + +
+ + + setMediaUrlModalStatus(true)} + getImageEditUrl={imageId => + productImageUrl(productId, imageId) + } + /> + + {data.attributes.length > 0 && ( + + )} + + {isSimpleProduct && ( + <> + + + + )} + {hasVariants ? ( + + ) : ( + <> + + + onVariantEndPreorderDialogOpen() + : null + } + data={data} + disabled={disabled} + hasVariants={false} + errors={errors} + formErrors={formErrors} + stocks={data.stocks} + warehouses={warehouses} + onChange={handlers.changeStock} + onFormDataChange={change} + onChangePreorderEndDate={handlers.changePreorderEndDate} + onWarehouseStockAdd={handlers.addStock} + onWarehouseStockDelete={handlers.deleteStock} + onWarehouseConfigure={onWarehouseConfigure} + /> + + )} + + + + +
+
+ + + {isSimpleProduct ? ( + + ) : product?.variants.length === 0 ? ( + + ) : ( + + )} + + +
+
+ navigate(productListUrl())} + onDelete={onDelete} + onSubmit={submit} + state={saveButtonBarState} + disabled={isSaveDisabled} + /> + {canOpenAssignReferencesAttributeDialog && ( + + handleAssignReferenceAttribute( + attributeValues, + data, + handlers + ) } /> - - {data.attributes.length > 0 && ( - - )} - - {isSimpleProduct && ( - <> - - - - )} - {hasVariants ? ( - - ) : ( - <> - - - onVariantEndPreorderDialogOpen() - : null - } - data={data} - disabled={disabled} - hasVariants={false} - errors={errors} - formErrors={formErrors} - stocks={data.stocks} - warehouses={warehouses} - onChange={handlers.changeStock} - onFormDataChange={change} - onChangePreorderEndDate={handlers.changePreorderEndDate} - onWarehouseStockAdd={handlers.addStock} - onWarehouseStockDelete={handlers.deleteStock} - onWarehouseConfigure={onWarehouseConfigure} - /> - - )} - - - - -
-
- - - {isSimpleProduct ? ( - - ) : ( - - )} - - -
-
- navigate(productListUrl())} - onDelete={onDelete} - onSubmit={submit} - state={saveButtonBarState} - disabled={isSaveDisabled} - /> - {canOpenAssignReferencesAttributeDialog && ( - - handleAssignReferenceAttribute( - attributeValues, - data, - handlers - ) - } + setMediaUrlModalStatus(false)} + open={mediaUrlModalStatus} + onSubmit={onMediaUrlUpload} /> - )} - - setMediaUrlModalStatus(false)} - open={mediaUrlModalStatus} - onSubmit={onMediaUrlUpload} - /> -
- - )} + + + ); + }} ); }; diff --git a/src/products/components/ProductVariantCreatorPage/ProductVariantCreatorPage.tsx b/src/products/components/ProductVariantCreatorPage/ProductVariantCreatorPage.tsx index 39bfff866..943fbb834 100644 --- a/src/products/components/ProductVariantCreatorPage/ProductVariantCreatorPage.tsx +++ b/src/products/components/ProductVariantCreatorPage/ProductVariantCreatorPage.tsx @@ -24,6 +24,7 @@ import reduceProductVariantCreateFormData, { ProductVariantCreateReducerActionType } from "./reducer"; import { ProductVariantCreatorStep } from "./types"; +import { dedupeListings } from "./utils"; const useStyles = makeStyles( theme => ({ @@ -176,7 +177,7 @@ const ProductVariantCreatePage: React.FC = props onTransition: (_, nextStep) => { if (nextStep === ProductVariantCreatorStep.summary) { dispatchFormDataAction({ - type: ProductVariantCreateReducerActionType.reload + type: ProductVariantCreateReducerActionType.rebuild }); } } @@ -237,7 +238,7 @@ const ProductVariantCreatePage: React.FC = props className={classes.button} disabled={!canHitNext(step, wizardData, variantsLeft)} variant="primary" - onClick={() => onSubmit(wizardData.variants)} + onClick={() => onSubmit(dedupeListings(wizardData).variants)} > ( `minmax(180px, auto) repeat(${props.data.variants[0].channelListings .length + props.data.variants[0].stocks - .length}, minmax(180px, auto)) 140px 64px`, + .length}, minmax(180px, auto)) 220px 64px`, overflowX: "scroll", rowGap: theme.spacing(), paddingBottom: 3 diff --git a/src/products/components/ProductVariantCreatorPage/createVariants.ts b/src/products/components/ProductVariantCreatorPage/createVariants.ts index c3bf0b3ec..a4beb2cc4 100644 --- a/src/products/components/ProductVariantCreatorPage/createVariants.ts +++ b/src/products/components/ProductVariantCreatorPage/createVariants.ts @@ -152,14 +152,6 @@ function addAttributeToVariant( } ]); } -function addVariantAttributeInput( - data: CreateVariantInput[], - attribute: Attribute -): CreateVariantInput[] { - return data - .map(variant => addAttributeToVariant(attribute, variant)) - .reduce((acc, variantInput) => [...acc, ...variantInput]); -} export function createVariantFlatMatrixDimension( variants: CreateVariantInput[], @@ -167,7 +159,9 @@ export function createVariantFlatMatrixDimension( ): CreateVariantInput[] { if (attributes.length > 0) { return createVariantFlatMatrixDimension( - addVariantAttributeInput(variants, attributes[0]), + variants.flatMap(variant => + addAttributeToVariant(attributes[0], variant) + ), attributes.slice(1) ); } else { diff --git a/src/products/components/ProductVariantCreatorPage/reducer.ts b/src/products/components/ProductVariantCreatorPage/reducer.ts index 7fe8023c9..61096b3d4 100644 --- a/src/products/components/ProductVariantCreatorPage/reducer.ts +++ b/src/products/components/ProductVariantCreatorPage/reducer.ts @@ -1,4 +1,8 @@ -import { AttributeValueFragment, StockInput } from "@saleor/graphql"; +import { + AttributeValueFragment, + ProductVariantBulkCreateInput, + StockInput +} from "@saleor/graphql"; import { add, remove, @@ -32,6 +36,7 @@ export enum ProductVariantCreateReducerActionType { changeWarehouses, deleteVariant, reload, + rebuild, selectValue } export interface ProductVariantCreateReducerAction { @@ -80,6 +85,44 @@ export interface ProductVariantCreateReducerAction { type: ProductVariantCreateReducerActionType; } +function getVariantId(variant: ProductVariantBulkCreateInput): string { + return variant.attributes + .map(attribute => attribute.values.join(":")) + .join("-"); +} + +function merge( + prev: ProductVariantBulkCreateInput[], + update: ProductVariantBulkCreateInput[] +): ProductVariantBulkCreateInput[] { + const prevIds = prev.map(getVariantId); + const updateIds = update.map(getVariantId); + return [ + ...prev + .filter(variant => updateIds.includes(getVariantId(variant))) + .map(variant => { + const updatedVariant = update.find( + v => getVariantId(v) === getVariantId(variant) + ); + + return { + ...updatedVariant, + sku: variant.sku + }; + }), + ...update.filter(variant => !prevIds.includes(getVariantId(variant))) + ]; +} + +function rebuild( + state: ProductVariantCreateFormData +): ProductVariantCreateFormData { + return { + ...state, + variants: merge(state.variants, createVariants(state)) + }; +} + function selectValue( prevState: ProductVariantCreateFormData, attributeId: string, @@ -519,6 +562,8 @@ function reduceProductVariantCreateFormData( return deleteVariant(prevState, action.deleteVariant.variantIndex); case ProductVariantCreateReducerActionType.reload: return action.reload?.data || createVariantMatrix(prevState); + case ProductVariantCreateReducerActionType.rebuild: + return rebuild(prevState); default: return prevState; } diff --git a/src/products/components/ProductVariantCreatorPage/utils.ts b/src/products/components/ProductVariantCreatorPage/utils.ts index 7373b38c5..ea9c22dab 100644 --- a/src/products/components/ProductVariantCreatorPage/utils.ts +++ b/src/products/components/ProductVariantCreatorPage/utils.ts @@ -5,6 +5,7 @@ import { } from "@saleor/graphql"; import { getById } from "@saleor/orders/components/OrderReturnPage/utils"; import { RelayToFlat } from "@saleor/types"; +import uniqBy from "lodash/uniqBy"; import { AttributeValue, ProductVariantCreateFormData } from "./form"; @@ -79,3 +80,15 @@ export const getBasicAttributeValue = ( attributeValues.find(getBySlug(attributeValue)) }; }; + +export function dedupeListings( + data: ProductVariantCreateFormData +): ProductVariantCreateFormData { + return { + ...data, + variants: data.variants.map(variant => ({ + ...variant, + channelListings: uniqBy(variant.channelListings, "channelId") + })) + }; +} diff --git a/src/products/views/ProductUpdate/ProductUpdate.tsx b/src/products/views/ProductUpdate/ProductUpdate.tsx index e8ba73bd3..c88925065 100644 --- a/src/products/views/ProductUpdate/ProductUpdate.tsx +++ b/src/products/views/ProductUpdate/ProductUpdate.tsx @@ -530,7 +530,7 @@ export const ProductUpdate: React.FC = ({ id, params }) => { <> {!!allChannels?.length && - (isSimpleProduct ? ( + (isSimpleProduct || product?.variants.length === 0 ? ( diff --git a/src/shipping/components/ShippingZoneRatesPage/ShippingZoneRatesPage.tsx b/src/shipping/components/ShippingZoneRatesPage/ShippingZoneRatesPage.tsx index 28c3d5a03..566fd56da 100644 --- a/src/shipping/components/ShippingZoneRatesPage/ShippingZoneRatesPage.tsx +++ b/src/shipping/components/ShippingZoneRatesPage/ShippingZoneRatesPage.tsx @@ -235,7 +235,6 @@ export const ShippingZoneRatesPage: React.FC = ({ ({ id: channel.id, name: channel.name diff --git a/src/storybook/__snapshots__/Stories.test.ts.snap b/src/storybook/__snapshots__/Stories.test.ts.snap index 93471c493..cd8c0a713 100644 --- a/src/storybook/__snapshots__/Stories.test.ts.snap +++ b/src/storybook/__snapshots__/Stories.test.ts.snap @@ -4583,7 +4583,7 @@ exports[`Storyshots Generics / Channels availability card default 1`] = `
- Available at 3 out of 4 channels + Available at 2 out of 4 channels

- Available at 3 out of 4 channels + Available at 2 out of 4 channels

+
- You will be able to define availability of product after creating variants. +
+ Available at 1 out of 7 channels +
+