diff --git a/src/channels/ChannelsWithVariantsAvailabilityCard/ChannelWithVariantAvailabilityItemWrapper.tsx b/src/channels/ChannelsWithVariantsAvailabilityCard/ChannelWithVariantAvailabilityItemWrapper.tsx index b9246da6c..0f9c93444 100644 --- a/src/channels/ChannelsWithVariantsAvailabilityCard/ChannelWithVariantAvailabilityItemWrapper.tsx +++ b/src/channels/ChannelsWithVariantsAvailabilityCard/ChannelWithVariantAvailabilityItemWrapper.tsx @@ -111,9 +111,10 @@ const ChannelWithVariantsAvailabilityItemWrapper: React.FC variant.id), selectedVariantsIds - }) + ) ? messages.allVariantsLabel : messages.variantCountLabel; diff --git a/src/channels/components/ChannelsWithVariantsAvailabilityDialog/ChannelsWithVariantsAvailabilityDialog.stories.tsx b/src/channels/components/ChannelsWithVariantsAvailabilityDialog/ChannelsWithVariantsAvailabilityDialog.stories.tsx index 0c44ba5f3..18f754ed4 100644 --- a/src/channels/components/ChannelsWithVariantsAvailabilityDialog/ChannelsWithVariantsAvailabilityDialog.stories.tsx +++ b/src/channels/components/ChannelsWithVariantsAvailabilityDialog/ChannelsWithVariantsAvailabilityDialog.stories.tsx @@ -1,4 +1,3 @@ -import { ChannelData } from "@saleor/channels/utils"; import { ProductDetails_product_variants } from "@saleor/products/types/ProductDetails"; import CommonDecorator from "@saleor/storybook/Decorator"; import { storiesOf } from "@storybook/react"; @@ -12,17 +11,20 @@ const props: ChannelsAvailabilityDialogProps = { channels: [ { id: "1", - name: "Channel 1" + name: "Channel 1", + variantsIds: [] }, { id: "2", - name: "Channel 2" + name: "Channel 2", + variantsIds: [] }, { id: "3", - name: "Channel 3" + name: "Channel 3", + variantsIds: [] } - ] as ChannelData[], + ], variants: [ { id: "variantA", @@ -50,31 +52,9 @@ const props: ChannelsAvailabilityDialogProps = { media: [] } ] as ProductDetails_product_variants[], - onChannelsWithVariantsConfirm: () => undefined, - addVariantToChannel: () => undefined, - removeVariantFromChannel: () => undefined, - channelsWithVariantsData: { - ["1"]: { - selectedVariantsIds: ["variantA", "variantB"], - variantsIdsToRemove: [], - variantsIdsToAdd: [] - }, - ["2"]: { - selectedVariantsIds: ["variantA", "variantC"], - variantsIdsToRemove: [], - variantsIdsToAdd: [] - }, - ["3"]: { - selectedVariantsIds: [], - variantsIdsToRemove: [], - variantsIdsToAdd: [] - } - }, - onChannelsAvailiabilityModalClose: () => undefined, - isChannelsAvailabilityModalOpen: true, - toggleAllChannels: () => undefined, - toggleAllChannelVariants: () => () => undefined, - haveChannelsWithVariantsDataChanged: true + onClose: () => undefined, + onConfirm: () => undefined, + open: true }; storiesOf("Channels / Channels with Variants Availability Dialog", module) diff --git a/src/channels/components/ChannelsWithVariantsAvailabilityDialog/ChannelsWithVariantsAvailabilityDialog.tsx b/src/channels/components/ChannelsWithVariantsAvailabilityDialog/ChannelsWithVariantsAvailabilityDialog.tsx index 6c2ace247..c9b551fa7 100644 --- a/src/channels/components/ChannelsWithVariantsAvailabilityDialog/ChannelsWithVariantsAvailabilityDialog.tsx +++ b/src/channels/components/ChannelsWithVariantsAvailabilityDialog/ChannelsWithVariantsAvailabilityDialog.tsx @@ -1,13 +1,16 @@ import { ChannelData } from "@saleor/channels/utils"; import ActionDialog from "@saleor/components/ActionDialog"; +import useModalDialogOpen from "@saleor/hooks/useModalDialogOpen"; import { ProductDetails_product_variants } from "@saleor/products/types/ProductDetails"; -import { UseChannelsWithProductVariants } from "@saleor/products/views/ProductUpdate/types"; +import { ChannelVariantListing } from "@saleor/products/views/ProductUpdate/types"; +import useChannelsWithProductVariants from "@saleor/products/views/ProductUpdate/useChannelsWithProductVariants"; import { areAllVariantsAtAllChannelsSelected, - areAnyChannelVariantsSelected + areAnyChannelVariantsSelected, + channelVariantListingDiffToDict } from "@saleor/products/views/ProductUpdate/utils"; -import isEqual from "lodash/isEqual"; -import React, { useEffect, useRef, useState } from "react"; +import { DialogProps } from "@saleor/types"; +import React from "react"; import { useIntl } from "react-intl"; import { defineMessages } from "react-intl"; @@ -22,80 +25,60 @@ const messages = defineMessages({ } }); -type UseChannelsWithVariantsCommonProps = Omit< - UseChannelsWithProductVariants, - | "onChannelsAvailiabilityModalOpen" - | "setHaveChannelsWithVariantsChanged" - | "channelsData" - | "setChannelsData" ->; - -export interface ChannelsAvailabilityDialogProps - extends UseChannelsWithVariantsCommonProps { +export interface ChannelsAvailabilityDialogProps extends DialogProps { channels: ChannelData[]; contentType?: string; variants: ProductDetails_product_variants[]; + onConfirm: (listings: ChannelVariantListing) => void; } export const ChannelsWithVariantsAvailabilityDialog: React.FC = ({ channels, contentType, variants, - isChannelsAvailabilityModalOpen, - toggleAllChannels, - channelsWithVariantsData, - onChannelsAvailiabilityModalClose, - haveChannelsWithVariantsDataChanged, - onChannelsWithVariantsConfirm, - ...rest + open, + onClose, + onConfirm }) => { const intl = useIntl(); - const [canConfirm, setCanConfirm] = useState(false); - const channelsWithVariantsDataRef = useRef(channelsWithVariantsData); + const { + channelsWithVariantsData, + hasChanged, + toggleAllChannels, + addVariantToChannel, + removeVariantFromChannel, + toggleAllChannelVariants, + channelVariantListing, + reset + } = useChannelsWithProductVariants( + channels, + variants?.map(variant => variant.id) + ); + + useModalDialogOpen(open, { + onClose: reset + }); + const { query, onQueryChange, filteredChannels } = useChannelsSearch( channels ); - const handleSetCanConfirm = () => { - const hasDataInsideDialogChanged = !isEqual( - channelsWithVariantsData, - channelsWithVariantsDataRef.current - ); - - if (hasDataInsideDialogChanged) { - channelsWithVariantsDataRef.current = channelsWithVariantsData; - setCanConfirm(true); - } - }; - - useEffect(handleSetCanConfirm, [channelsWithVariantsData]); - const hasAllChannelsSelected = areAllVariantsAtAllChannelsSelected( - variants, - channelsWithVariantsData + variants.map(variant => variant.id), + channelVariantListingDiffToDict(channelsWithVariantsData) ); const isChannelSelected = (channelId: string) => areAnyChannelVariantsSelected(channelsWithVariantsData[channelId]); - const handleClose = () => { - setCanConfirm(false); - onChannelsAvailiabilityModalClose(); - }; - - const handleConfirm = () => { - setCanConfirm(false); - onChannelsWithVariantsConfirm(); - }; - return ( onConfirm(channelVariantListing)} title={intl.formatMessage(messages.title)} - disabled={!canConfirm} + disabled={!hasChanged} > diff --git a/src/channels/components/ChannelsWithVariantsAvailabilityDialog/ChannelsWithVariantsAvailabilityDialogContent.tsx b/src/channels/components/ChannelsWithVariantsAvailabilityDialog/ChannelsWithVariantsAvailabilityDialogContent.tsx index b57917924..e5171f61b 100644 --- a/src/channels/components/ChannelsWithVariantsAvailabilityDialog/ChannelsWithVariantsAvailabilityDialogContent.tsx +++ b/src/channels/components/ChannelsWithVariantsAvailabilityDialog/ChannelsWithVariantsAvailabilityDialogContent.tsx @@ -14,7 +14,10 @@ import Label from "@saleor/orders/components/OrderHistory/Label"; import { getById } from "@saleor/orders/components/OrderReturnPage/utils"; import { ProductDetails_product_variants } from "@saleor/products/types/ProductDetails"; import { ChannelsWithVariantsData } from "@saleor/products/views/ProductUpdate/types"; -import { areAllChannelVariantsSelected } from "@saleor/products/views/ProductUpdate/utils"; +import { + areAllChannelVariantsSelected, + channelVariantListingDiffToDict +} from "@saleor/products/views/ProductUpdate/utils"; import map from "lodash/map"; import React, { ChangeEvent } from "react"; import { defineMessages, useIntl } from "react-intl"; @@ -96,7 +99,7 @@ interface ChannelsWithVariantsAvailabilityDialogContentProps { addVariantToChannel: (channelId: string, variantId: string) => void; removeVariantFromChannel: (channelId: string, variantId: string) => void; channelsWithVariants: ChannelsWithVariantsData; - toggleAllChannelVariants: (channelId: string) => () => void; + toggleAllChannelVariants: (channelId: string) => void; isChannelSelected: (channelId: string) => boolean; channels: ChannelData[]; allVariants: ProductDetails_product_variants[]; @@ -125,8 +128,8 @@ const ChannelsWithVariantsAvailabilityDialogContent: React.FC areAllChannelVariantsSelected( - allVariants, - channelsWithVariants[channelId] + allVariants?.map(variant => variant.id), + channelVariantListingDiffToDict(channelsWithVariants)[channelId] ) ? ( ) : ( @@ -155,6 +158,7 @@ const ChannelsWithVariantsAvailabilityDialogContent: React.FC } @@ -183,14 +187,14 @@ const ChannelsWithVariantsAvailabilityDialogContent: React.FC } - onChange={toggleAllChannelVariants(channelId)} + onChange={() => toggleAllChannelVariants(channelId)} /> {allVariants.map(({ id: variantId, name }) => ( - <> +
- +
))} ); diff --git a/src/components/ActionDialog/ActionDialog.tsx b/src/components/ActionDialog/ActionDialog.tsx index f33d7d998..ae7661027 100644 --- a/src/components/ActionDialog/ActionDialog.tsx +++ b/src/components/ActionDialog/ActionDialog.tsx @@ -18,10 +18,10 @@ export interface ActionDialogProps extends DialogProps { } const ActionDialog: React.FC = props => { - const { children, open, title, onClose, variant, ...rest } = props; + const { children, open, title, onClose, variant, maxWidth, ...rest } = props; return ( - + {title} {children} diff --git a/src/products/views/ProductUpdate/ProductUpdate.tsx b/src/products/views/ProductUpdate/ProductUpdate.tsx index f3dbb2848..54b6ee28d 100644 --- a/src/products/views/ProductUpdate/ProductUpdate.tsx +++ b/src/products/views/ProductUpdate/ProductUpdate.tsx @@ -27,6 +27,7 @@ import useNavigator from "@saleor/hooks/useNavigator"; import useNotifier from "@saleor/hooks/useNotifier"; import useOnSetDefaultVariant from "@saleor/hooks/useOnSetDefaultVariant"; import useShop from "@saleor/hooks/useShop"; +import useStateFromProps from "@saleor/hooks/useStateFromProps"; import { commonMessages, errorMessages } from "@saleor/intl"; import ProductVariantCreateDialog from "@saleor/products/components/ProductVariantCreateDialog"; import { @@ -61,9 +62,7 @@ import React from "react"; import { defineMessages, FormattedMessage, useIntl } from "react-intl"; import { getMutationState } from "../../../misc"; -import ProductUpdatePage, { - ProductUpdatePageSubmitData -} from "../../components/ProductUpdatePage"; +import ProductUpdatePage from "../../components/ProductUpdatePage"; import { useProductDetails } from "../../queries"; import { ProductMediaCreateVariables } from "../../types/ProductMediaCreate"; import { ProductUpdate as ProductUpdateMutationResult } from "../../types/ProductUpdate"; @@ -77,13 +76,14 @@ import { productVariantCreatorUrl, productVariantEditUrl } from "../../urls"; +import { CHANNELS_AVAILIABILITY_MODAL_SELECTOR } from "./consts"; import { createImageReorderHandler, createImageUploadHandler, createUpdateHandler, createVariantReorderHandler } from "./handlers"; -import useChannelsWithProductVariants from "./useChannelsWithProductVariants"; +import useChannelVariantListings from "./useChannelVariantListings"; const messages = defineMessages({ deleteProductDialogTitle: { @@ -274,21 +274,13 @@ export const ProductUpdate: React.FC = ({ id, params }) => { channel.name.localeCompare(nextChannel.name) ); + const [channelsData, setChannelsData] = useStateFromProps(allChannels); const { + channels: updatedChannels, channelsWithVariantsData, - haveChannelsWithVariantsDataChanged, - setHaveChannelsWithVariantsChanged, - onChannelsAvailiabilityModalOpen, - channelsData, - setChannelsData, - ...channelsWithVariantsProps - } = useChannelsWithProductVariants({ - channels: allChannels, - variants: product?.variants, - action: params?.action, - openModal, - closeModal - }); + hasChanged: hasChannelVariantListingChanged, + setChannelVariantListing + } = useChannelVariantListings(allChannels); const productChannelsChoices: ChannelData[] = createSortedChannelsDataFromProduct( product @@ -517,18 +509,19 @@ export const ProductUpdate: React.FC = ({ id, params }) => { /> ) : ( { + closeModal(); + setChannelVariantListing(listings); + }} /> ))} = ({ id, params }) => { onDelete={() => openModal("remove")} onImageReorder={handleImageReorder} onMediaUrlUpload={handleMediaUrlUpload} - onSubmit={(formData: ProductUpdatePageSubmitData) => { - setHaveChannelsWithVariantsChanged(false); - return handleSubmit(formData); - }} + onSubmit={handleSubmit} onWarehouseConfigure={() => navigate(warehouseAddPath)} onVariantAdd={handleVariantAdd} onVariantsAdd={() => openModal("add-variants")} diff --git a/src/products/views/ProductUpdate/consts.ts b/src/products/views/ProductUpdate/consts.ts new file mode 100644 index 000000000..bf243dcf7 --- /dev/null +++ b/src/products/views/ProductUpdate/consts.ts @@ -0,0 +1,9 @@ +import { ChannelWithVariantData } from "./types"; + +export const CHANNELS_AVAILIABILITY_MODAL_SELECTOR = "open-channels-picker"; + +export const initialChannelWithVariantData: ChannelWithVariantData = { + variantsIdsToRemove: [], + variantsIdsToAdd: [], + selectedVariantsIds: [] +}; diff --git a/src/products/views/ProductUpdate/types.ts b/src/products/views/ProductUpdate/types.ts index 2e5d2674d..606ab156e 100644 --- a/src/products/views/ProductUpdate/types.ts +++ b/src/products/views/ProductUpdate/types.ts @@ -1,44 +1,20 @@ -import { ChannelsAction } from "@saleor/channels/urls"; -import { ChannelData } from "@saleor/channels/utils"; -import { ProductDetails_product_variants } from "@saleor/products/types/ProductDetails"; -import { ProductUrlDialog } from "@saleor/products/urls"; - -export interface UseChannelsWithProductVariantsProps { - channels: ChannelData[]; - variants: ProductDetails_product_variants[]; - action: ProductUrlDialog; - openModal: (action: ChannelsAction) => void; - closeModal: () => void; -} - export interface ChannelWithVariantData { selectedVariantsIds: string[]; variantsIdsToRemove: string[]; variantsIdsToAdd: string[]; } +export type ChannelVariantListing = Record; export type ChannelsWithVariantsData = Record; -export const initialChannelWithVariantData: ChannelWithVariantData = { - variantsIdsToRemove: [], - variantsIdsToAdd: [], - selectedVariantsIds: [] -}; - -export const CHANNELS_AVAILIABILITY_MODAL_SELECTOR = "open-channels-picker"; - export interface UseChannelsWithProductVariants { - channelsData: ChannelData[]; - setChannelsData: (data: ChannelData[]) => void; - onChannelsWithVariantsConfirm: () => void; addVariantToChannel: (channelId: string, variantId: string) => void; removeVariantFromChannel: (channelId: string, variantId: string) => void; channelsWithVariantsData: ChannelsWithVariantsData; - onChannelsAvailiabilityModalOpen: () => void; - onChannelsAvailiabilityModalClose: () => void; - isChannelsAvailabilityModalOpen: boolean; toggleAllChannels: () => void; - toggleAllChannelVariants: (channelId: string) => () => void; - haveChannelsWithVariantsDataChanged: boolean; - setHaveChannelsWithVariantsChanged: (hasChanged: boolean) => void; + toggleAllChannelVariants: (channelId: string) => void; + hasChanged: boolean; + channelVariantListing: ChannelVariantListing; + setChannelVariantListing: (listings: ChannelVariantListing) => void; + reset: () => void; } diff --git a/src/products/views/ProductUpdate/useChannelVariantListings.ts b/src/products/views/ProductUpdate/useChannelVariantListings.ts new file mode 100644 index 000000000..8775c0b62 --- /dev/null +++ b/src/products/views/ProductUpdate/useChannelVariantListings.ts @@ -0,0 +1,61 @@ +import { ChannelData } from "@saleor/channels/utils"; +import useStateFromProps from "@saleor/hooks/useStateFromProps"; +import { arrayDiff } from "@saleor/utils/arrays"; +import isEqual from "lodash/isEqual"; +import { useMemo } from "react"; + +import { ChannelsWithVariantsData } from "./types"; +import { createFromChannels, createUpdatedChannels } from "./utils"; + +function useChannelVariantListings(channels: ChannelData[]) { + const initialChannelVariantListing = useMemo( + () => createFromChannels(channels, ({ variantsIds }) => variantsIds), + [channels] + ); + + const [ + updatedChannelVariantListing, + setUpdatedChannelVariantListing + ] = useStateFromProps(initialChannelVariantListing); + + const hasChanged = useMemo( + () => !isEqual(initialChannelVariantListing, updatedChannelVariantListing), + [initialChannelVariantListing, updatedChannelVariantListing] + ); + + const channelsWithVariantsData = useMemo( + () => + createFromChannels(channels, channel => { + const diff = arrayDiff( + initialChannelVariantListing[channel.id], + updatedChannelVariantListing[channel.id] + ); + + return { + selectedVariantsIds: updatedChannelVariantListing[channel.id], + variantsIdsToAdd: diff.added, + variantsIdsToRemove: diff.removed + }; + }), + [initialChannelVariantListing, updatedChannelVariantListing] + ); + + const reset = () => + setUpdatedChannelVariantListing(initialChannelVariantListing); + + const updatedChannels: ChannelData[] = useMemo( + () => createUpdatedChannels(channels, updatedChannelVariantListing), + [channels, updatedChannelVariantListing] + ); + + return { + channels: updatedChannels, + channelsWithVariantsData, + channelVariantListing: updatedChannelVariantListing, + setChannelVariantListing: setUpdatedChannelVariantListing, + hasChanged, + reset + }; +} + +export default useChannelVariantListings; diff --git a/src/products/views/ProductUpdate/useChannelsWithProductVariants.test.ts b/src/products/views/ProductUpdate/useChannelsWithProductVariants.test.ts new file mode 100644 index 000000000..f24449813 --- /dev/null +++ b/src/products/views/ProductUpdate/useChannelsWithProductVariants.test.ts @@ -0,0 +1,186 @@ +import { ChannelData } from "@saleor/channels/utils"; +import { act, renderHook } from "@testing-library/react-hooks"; + +import useChannelsWithProductVariants from "./useChannelsWithProductVariants"; + +const channels: ChannelData[] = [ + { + id: "channel1", + name: "Channel 1", + variantsIds: ["variant1", "variant2"] + }, + { + id: "channel2", + name: "Channel 2", + variantsIds: [] + } +]; + +const variants = ["variant1", "variant2", "variant3", "variant4", "variant5"]; + +const setupHook = () => + renderHook(() => useChannelsWithProductVariants(channels, variants)); + +describe("useChannelsWithProductVariants", () => { + it("properly initializes state", () => { + const { result } = setupHook(); + + expect( + result.current.channelsWithVariantsData.channel1.selectedVariantsIds + ).toHaveLength(2); + expect( + result.current.channelsWithVariantsData.channel1.variantsIdsToAdd + ).toHaveLength(0); + expect( + result.current.channelsWithVariantsData.channel1.variantsIdsToRemove + ).toHaveLength(0); + + expect( + result.current.channelsWithVariantsData.channel2.selectedVariantsIds + ).toHaveLength(0); + expect( + result.current.channelsWithVariantsData.channel2.variantsIdsToAdd + ).toHaveLength(0); + expect( + result.current.channelsWithVariantsData.channel2.variantsIdsToRemove + ).toHaveLength(0); + + expect(result.current.hasChanged).toBe(false); + }); + + it("properly adds variants", () => { + const { result } = setupHook(); + + act(() => result.current.addVariantToChannel("channel1", "variant3")); + + expect( + result.current.channelsWithVariantsData.channel1.selectedVariantsIds + ).toHaveLength(3); + expect( + result.current.channelsWithVariantsData.channel1.variantsIdsToAdd + ).toHaveLength(1); + expect( + result.current.channelsWithVariantsData.channel1.variantsIdsToRemove + ).toHaveLength(0); + + expect( + result.current.channelsWithVariantsData.channel2.selectedVariantsIds + ).toHaveLength(0); + expect( + result.current.channelsWithVariantsData.channel2.variantsIdsToAdd + ).toHaveLength(0); + expect( + result.current.channelsWithVariantsData.channel2.variantsIdsToRemove + ).toHaveLength(0); + + expect(result.current.hasChanged).toBe(true); + }); + + it("properly removes variants", () => { + const { result } = setupHook(); + + act(() => result.current.removeVariantFromChannel("channel1", "variant2")); + + expect( + result.current.channelsWithVariantsData.channel1.selectedVariantsIds + ).toHaveLength(1); + expect( + result.current.channelsWithVariantsData.channel1.variantsIdsToAdd + ).toHaveLength(0); + expect( + result.current.channelsWithVariantsData.channel1.variantsIdsToRemove + ).toHaveLength(1); + + expect( + result.current.channelsWithVariantsData.channel2.selectedVariantsIds + ).toHaveLength(0); + expect( + result.current.channelsWithVariantsData.channel2.variantsIdsToAdd + ).toHaveLength(0); + expect( + result.current.channelsWithVariantsData.channel2.variantsIdsToRemove + ).toHaveLength(0); + + expect(result.current.hasChanged).toBe(true); + }); + + it("properly toggles all variants in channel", () => { + const { result } = setupHook(); + + // Deselect all because it's partially selected + act(() => result.current.toggleAllChannelVariants("channel1")); + + expect( + result.current.channelsWithVariantsData.channel1.selectedVariantsIds + ).toHaveLength(0); + expect( + result.current.channelsWithVariantsData.channel1.variantsIdsToAdd + ).toHaveLength(0); + expect( + result.current.channelsWithVariantsData.channel1.variantsIdsToRemove + ).toHaveLength(2); + expect(result.current.hasChanged).toBe(true); + + // Select all + act(() => result.current.toggleAllChannelVariants("channel1")); + + expect( + result.current.channelsWithVariantsData.channel1.selectedVariantsIds + ).toHaveLength(5); + expect( + result.current.channelsWithVariantsData.channel1.variantsIdsToAdd + ).toHaveLength(3); + expect( + result.current.channelsWithVariantsData.channel1.variantsIdsToRemove + ).toHaveLength(0); + }); + + it("properly toggles all", () => { + const { result } = setupHook(); + + // Select all + act(result.current.toggleAllChannels); + + expect( + result.current.channelsWithVariantsData.channel1.selectedVariantsIds + ).toHaveLength(5); + expect( + result.current.channelsWithVariantsData.channel1.variantsIdsToAdd + ).toHaveLength(3); + expect( + result.current.channelsWithVariantsData.channel1.variantsIdsToRemove + ).toHaveLength(0); + expect( + result.current.channelsWithVariantsData.channel2.selectedVariantsIds + ).toHaveLength(5); + expect( + result.current.channelsWithVariantsData.channel2.variantsIdsToAdd + ).toHaveLength(5); + expect( + result.current.channelsWithVariantsData.channel2.variantsIdsToRemove + ).toHaveLength(0); + expect(result.current.hasChanged).toBe(true); + + // Deselect all + act(result.current.toggleAllChannels); + + expect( + result.current.channelsWithVariantsData.channel1.selectedVariantsIds + ).toHaveLength(0); + expect( + result.current.channelsWithVariantsData.channel1.variantsIdsToAdd + ).toHaveLength(0); + expect( + result.current.channelsWithVariantsData.channel1.variantsIdsToRemove + ).toHaveLength(2); + expect( + result.current.channelsWithVariantsData.channel2.selectedVariantsIds + ).toHaveLength(0); + expect( + result.current.channelsWithVariantsData.channel2.variantsIdsToAdd + ).toHaveLength(0); + expect( + result.current.channelsWithVariantsData.channel2.variantsIdsToRemove + ).toHaveLength(0); + }); +}); diff --git a/src/products/views/ProductUpdate/useChannelsWithProductVariants.ts b/src/products/views/ProductUpdate/useChannelsWithProductVariants.ts index 5f2a70330..5d1ad62a0 100644 --- a/src/products/views/ProductUpdate/useChannelsWithProductVariants.ts +++ b/src/products/views/ProductUpdate/useChannelsWithProductVariants.ts @@ -1,139 +1,63 @@ -import useStateFromProps from "@saleor/hooks/useStateFromProps"; -import isEmpty from "lodash/isEmpty"; -import reduce from "lodash/reduce"; -import { useEffect, useRef, useState } from "react"; +import { ChannelData } from "@saleor/channels/utils"; +import uniq from "lodash/uniq"; +import { UseChannelsWithProductVariants } from "./types"; +import useChannelVariantListings from "./useChannelVariantListings"; import { - CHANNELS_AVAILIABILITY_MODAL_SELECTOR, - ChannelsWithVariantsData, - UseChannelsWithProductVariants, - UseChannelsWithProductVariantsProps -} from "./types"; -import { - areAllVariantsAtAllChannelsSelected, - areAnyChannelVariantsSelected, - getChannelVariantToggleData, - getChannelWithAddedVariantData, - getChannelWithRemovedVariantData, - getParsedChannelsWithVariantsDataFromChannels + addAllVariantsToAllChannels, + getChannelVariantToggleData } from "./utils"; -const useChannelsWithProductVariants = ({ - channels, - variants, - openModal, - closeModal, - action -}: UseChannelsWithProductVariantsProps): UseChannelsWithProductVariants => { - const [channelsData, setChannelsData] = useStateFromProps(channels); - - const initialChannelsWithVariantsData = getParsedChannelsWithVariantsDataFromChannels( - channels - ); - - const [ +const useChannelsWithProductVariants = ( + channels: ChannelData[], + variants: string[] +): UseChannelsWithProductVariants => { + const { channelsWithVariantsData, - setChannelsWithVariantsData - ] = useStateFromProps( - initialChannelsWithVariantsData - ); - - const channelsWithVariantsDataRef = useRef(channelsWithVariantsData); - - const [hasChanged, setHasChanged] = useState(false); - - const handleSetHasChanged = () => { - const isDataRefEmpty = isEmpty(channelsWithVariantsDataRef.current); - const isDataEmpty = isEmpty(channelsWithVariantsData); - - const hasFilledInitialData = isDataRefEmpty && !isDataEmpty; - - const hasNoDataFilled = isDataRefEmpty && isDataEmpty; - - channelsWithVariantsDataRef.current = channelsWithVariantsData; - - if (hasNoDataFilled || hasFilledInitialData) { - return; - } - - setHasChanged(true); - }; - - useEffect(handleSetHasChanged, [channelsWithVariantsData]); + hasChanged, + setChannelVariantListing, + channelVariantListing, + reset + } = useChannelVariantListings(channels); const handleAddVariant = (channelId: string, variantId: string) => - setChannelsWithVariantsData({ - ...channelsWithVariantsData, - ...getChannelWithAddedVariantData({ - channelWithVariantsData: channelsWithVariantsData[channelId], - channelId, - variantId - }) - }); + setChannelVariantListing(listings => ({ + ...listings, + [channelId]: uniq([...listings[channelId], variantId]) + })); const handleRemoveVariant = (channelId: string, variantId: string) => - setChannelsWithVariantsData({ - ...channelsWithVariantsData, - ...getChannelWithRemovedVariantData({ - channelWithVariantsData: channelsWithVariantsData[channelId], - channelId, - variantId - }) - }); + setChannelVariantListing(listings => ({ + ...listings, + [channelId]: listings[channelId].filter( + selectedVariantId => selectedVariantId !== variantId + ) + })); - const toggleAllChannelVariants = (channelId: string) => () => { - const isChannelSelected = areAnyChannelVariantsSelected( - channelsWithVariantsData[channelId] - ); + const toggleAllChannelVariants = (channelId: string) => { + const isChannelSelected = channelVariantListing[channelId].length > 0; - setChannelsWithVariantsData({ - ...channelsWithVariantsData, + setChannelVariantListing({ + ...channelVariantListing, [channelId]: getChannelVariantToggleData(variants, isChannelSelected) }); }; - const toggleAllChannels = () => { - const areAllChannelsSelected = areAllVariantsAtAllChannelsSelected( - variants, - channelsWithVariantsData + const toggleAllChannels = () => + setChannelVariantListing(listings => + addAllVariantsToAllChannels(listings, variants) ); - const updatedData: ChannelsWithVariantsData = reduce( - channelsWithVariantsData, - (result, _, channelId) => ({ - ...result, - [channelId]: getChannelVariantToggleData( - variants, - areAllChannelsSelected - ) - }), - {} - ); - - setChannelsWithVariantsData(updatedData); - }; - - const onChannelsWithVariantsConfirm = () => closeModal(); - - const handleModalOpen = () => - openModal(CHANNELS_AVAILIABILITY_MODAL_SELECTOR); - - const isModalOpen = action === CHANNELS_AVAILIABILITY_MODAL_SELECTOR; - return { channelsWithVariantsData, - setChannelsData, - channelsData, + channelVariantListing, addVariantToChannel: handleAddVariant, removeVariantFromChannel: handleRemoveVariant, - onChannelsAvailiabilityModalOpen: handleModalOpen, - onChannelsAvailiabilityModalClose: closeModal, - isChannelsAvailabilityModalOpen: isModalOpen, - haveChannelsWithVariantsDataChanged: hasChanged, + hasChanged, toggleAllChannelVariants, toggleAllChannels, - onChannelsWithVariantsConfirm, - setHaveChannelsWithVariantsChanged: setHasChanged + setChannelVariantListing, + reset }; }; diff --git a/src/products/views/ProductUpdate/utils.ts b/src/products/views/ProductUpdate/utils.ts index 28b3be4bf..bfd6ac0bc 100644 --- a/src/products/views/ProductUpdate/utils.ts +++ b/src/products/views/ProductUpdate/utils.ts @@ -1,111 +1,74 @@ -import { - getUpdatedIdsWithNewId, - getUpdatedIdsWithoutNewId -} from "@saleor/channels/pages/ChannelDetailsPage/utils"; import { ChannelData } from "@saleor/channels/utils"; -import { ProductDetails_product_variants } from "@saleor/products/types/ProductDetails"; import every from "lodash/every"; import reduce from "lodash/reduce"; +import { initialChannelWithVariantData } from "./consts"; import { ChannelsWithVariantsData, - ChannelWithVariantData, - initialChannelWithVariantData + ChannelVariantListing, + ChannelWithVariantData } from "./types"; +export function createFromChannels( + channels: ChannelData[], + cb: (channel: ChannelData) => T +): Record { + return channels?.reduce( + (result: Record, channel) => ({ + ...result, + [channel.id]: cb(channel) + }), + {} + ); +} + +export function createUpdatedChannels( + channels: ChannelData[], + listing: ChannelVariantListing +): ChannelData[] { + return reduce( + listing, + (acc, variantsIds, channelId) => [ + ...acc, + { + id: channelId, + name: channels.find(channel => channel.id === channelId).name, + variantsIds + } as ChannelData + ], + [] + ); +} + export const getParsedChannelsWithVariantsDataFromChannels = ( channels: ChannelData[] ): ChannelsWithVariantsData => - channels?.reduce( - (result: ChannelsWithVariantsData, { id, variantsIds }) => ({ - ...result, - [id]: { + createFromChannels( + channels, + ({ variantsIds }) => + ({ ...initialChannelWithVariantData, selectedVariantsIds: variantsIds - } as ChannelWithVariantData - }), - {} + } as ChannelWithVariantData) ); -interface ChannelAddRemoveVariantCommonProps { - channelWithVariantsData: ChannelWithVariantData; - channelId: string; - variantId: string; -} - -export const getChannelWithAddedVariantData = ({ - channelWithVariantsData: { - selectedVariantsIds, - variantsIdsToAdd, - variantsIdsToRemove - }, - channelId, - variantId -}: ChannelAddRemoveVariantCommonProps): ChannelsWithVariantsData => ({ - [channelId]: { - selectedVariantsIds: getUpdatedIdsWithNewId(selectedVariantsIds, variantId), - variantsIdsToAdd: getUpdatedIdsWithNewId(variantsIdsToAdd, variantId), - variantsIdsToRemove: getUpdatedIdsWithoutNewId( - variantsIdsToRemove, - variantId - ) - } -}); - -export const getChannelWithRemovedVariantData = ({ - channelWithVariantsData: { - selectedVariantsIds, - variantsIdsToAdd, - variantsIdsToRemove - }, - channelId, - variantId -}: ChannelAddRemoveVariantCommonProps): ChannelsWithVariantsData => ({ - [channelId]: { - selectedVariantsIds: getUpdatedIdsWithoutNewId( - selectedVariantsIds, - variantId - ), - variantsIdsToRemove: getUpdatedIdsWithNewId(variantsIdsToRemove, variantId), - variantsIdsToAdd: getUpdatedIdsWithoutNewId(variantsIdsToAdd, variantId) - } -}); - export const getChannelVariantToggleData = ( - variants: ProductDetails_product_variants[], + variants: string[], isSelected: boolean -): ChannelWithVariantData => { - const allProductVariantsIds = extractAllProductVariantsIds(variants); - - return isSelected - ? { - selectedVariantsIds: [], - variantsIdsToAdd: [], - variantsIdsToRemove: allProductVariantsIds - } - : { - selectedVariantsIds: allProductVariantsIds, - variantsIdsToAdd: allProductVariantsIds, - variantsIdsToRemove: [] - }; -}; - -export const extractAllProductVariantsIds = ( - productVariants: ProductDetails_product_variants[] = [] -) => productVariants.map(({ id }) => id); +): string[] => (isSelected ? [] : variants); export const areAllVariantsAtAllChannelsSelected = ( - variants: ProductDetails_product_variants[] = [], - channelsWithVariants: ChannelsWithVariantsData = {} + variants: string[] = [], + channelsWithVariants: ChannelVariantListing = {} ) => every(channelsWithVariants, channelWithVariantsData => areAllChannelVariantsSelected(variants, channelWithVariantsData) ); export const areAllChannelVariantsSelected = ( - variants: ProductDetails_product_variants[] = [], - { selectedVariantsIds }: Pick -) => selectedVariantsIds.length === variants.length; + variants: string[] = [], + selectedVariants: string[] +) => selectedVariants.length === variants.length; export const areAnyChannelVariantsSelected = ( channelsWithVariantsData: ChannelWithVariantData @@ -114,9 +77,43 @@ export const areAnyChannelVariantsSelected = ( export const getTotalSelectedChannelsCount = ( channelsWithVariantsData: ChannelsWithVariantsData ) => - reduce( - channelsWithVariantsData, - (result, { selectedVariantsIds }) => - selectedVariantsIds.length ? result + 1 : result, - 0 + Object.values(channelsWithVariantsData).filter( + channel => channel.selectedVariantsIds.length > 0 + ).length; + +export const addAllVariantsToAllChannels = ( + listings: ChannelVariantListing, + variants: string[] +): ChannelVariantListing => { + const areAllChannelsSelected = areAllVariantsAtAllChannelsSelected( + variants, + listings + ); + + const updatedListing = reduce( + listings, + (result: ChannelVariantListing, _, channelId) => ({ + ...result, + [channelId]: getChannelVariantToggleData(variants, areAllChannelsSelected) + }), + {} + ); + + return updatedListing; +}; + +export const channelVariantListingDiffToDict = ( + listing: ChannelsWithVariantsData +): ChannelVariantListing => + reduce( + listing, + ( + listingDict: ChannelVariantListing, + { selectedVariantsIds }, + channelId + ) => ({ + ...listingDict, + [channelId]: selectedVariantsIds + }), + {} );