Fix back button on manage channels at product details (#1380) (#1417)

* Simplify "changed" logic

* Improve code composition

* Test base state and setters

* Add more tests

* Fix changed logic

* Rename hasChanged output

* Move channel data outside hook

* Move some logic to utils

* Save data in dialog and pass to view

* Split hooks

* Fix react warnings

* Fix story

* Alias type

* Fix stories

* Remove rebase artifact

* Reset state after closing modal

* Capitalize type name
This commit is contained in:
Dominik Żegleń 2021-09-17 18:02:44 +02:00 committed by GitHub
parent 90717b7ed0
commit ffe44be733
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 467 additions and 354 deletions

View file

@ -111,9 +111,10 @@ const ChannelWithVariantsAvailabilityItemWrapper: React.FC<ChannelAvailabilityIt
const variantsCount = selectedVariantsIds.length; const variantsCount = selectedVariantsIds.length;
const variantsLabel = areAllChannelVariantsSelected(variants, { const variantsLabel = areAllChannelVariantsSelected(
variants?.map(variant => variant.id),
selectedVariantsIds selectedVariantsIds
}) )
? messages.allVariantsLabel ? messages.allVariantsLabel
: messages.variantCountLabel; : messages.variantCountLabel;

View file

@ -1,4 +1,3 @@
import { ChannelData } from "@saleor/channels/utils";
import { ProductDetails_product_variants } from "@saleor/products/types/ProductDetails"; import { ProductDetails_product_variants } from "@saleor/products/types/ProductDetails";
import CommonDecorator from "@saleor/storybook/Decorator"; import CommonDecorator from "@saleor/storybook/Decorator";
import { storiesOf } from "@storybook/react"; import { storiesOf } from "@storybook/react";
@ -12,17 +11,20 @@ const props: ChannelsAvailabilityDialogProps = {
channels: [ channels: [
{ {
id: "1", id: "1",
name: "Channel 1" name: "Channel 1",
variantsIds: []
}, },
{ {
id: "2", id: "2",
name: "Channel 2" name: "Channel 2",
variantsIds: []
}, },
{ {
id: "3", id: "3",
name: "Channel 3" name: "Channel 3",
variantsIds: []
} }
] as ChannelData[], ],
variants: [ variants: [
{ {
id: "variantA", id: "variantA",
@ -50,31 +52,9 @@ const props: ChannelsAvailabilityDialogProps = {
media: [] media: []
} }
] as ProductDetails_product_variants[], ] as ProductDetails_product_variants[],
onChannelsWithVariantsConfirm: () => undefined, onClose: () => undefined,
addVariantToChannel: () => undefined, onConfirm: () => undefined,
removeVariantFromChannel: () => undefined, open: true
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
}; };
storiesOf("Channels / Channels with Variants Availability Dialog", module) storiesOf("Channels / Channels with Variants Availability Dialog", module)

View file

@ -1,13 +1,16 @@
import { ChannelData } from "@saleor/channels/utils"; import { ChannelData } from "@saleor/channels/utils";
import ActionDialog from "@saleor/components/ActionDialog"; import ActionDialog from "@saleor/components/ActionDialog";
import useModalDialogOpen from "@saleor/hooks/useModalDialogOpen";
import { ProductDetails_product_variants } from "@saleor/products/types/ProductDetails"; 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 { import {
areAllVariantsAtAllChannelsSelected, areAllVariantsAtAllChannelsSelected,
areAnyChannelVariantsSelected areAnyChannelVariantsSelected,
channelVariantListingDiffToDict
} from "@saleor/products/views/ProductUpdate/utils"; } from "@saleor/products/views/ProductUpdate/utils";
import isEqual from "lodash/isEqual"; import { DialogProps } from "@saleor/types";
import React, { useEffect, useRef, useState } from "react"; import React from "react";
import { useIntl } from "react-intl"; import { useIntl } from "react-intl";
import { defineMessages } from "react-intl"; import { defineMessages } from "react-intl";
@ -22,80 +25,60 @@ const messages = defineMessages({
} }
}); });
type UseChannelsWithVariantsCommonProps = Omit< export interface ChannelsAvailabilityDialogProps extends DialogProps {
UseChannelsWithProductVariants,
| "onChannelsAvailiabilityModalOpen"
| "setHaveChannelsWithVariantsChanged"
| "channelsData"
| "setChannelsData"
>;
export interface ChannelsAvailabilityDialogProps
extends UseChannelsWithVariantsCommonProps {
channels: ChannelData[]; channels: ChannelData[];
contentType?: string; contentType?: string;
variants: ProductDetails_product_variants[]; variants: ProductDetails_product_variants[];
onConfirm: (listings: ChannelVariantListing) => void;
} }
export const ChannelsWithVariantsAvailabilityDialog: React.FC<ChannelsAvailabilityDialogProps> = ({ export const ChannelsWithVariantsAvailabilityDialog: React.FC<ChannelsAvailabilityDialogProps> = ({
channels, channels,
contentType, contentType,
variants, variants,
isChannelsAvailabilityModalOpen, open,
toggleAllChannels, onClose,
channelsWithVariantsData, onConfirm
onChannelsAvailiabilityModalClose,
haveChannelsWithVariantsDataChanged,
onChannelsWithVariantsConfirm,
...rest
}) => { }) => {
const intl = useIntl(); const intl = useIntl();
const [canConfirm, setCanConfirm] = useState(false); const {
const channelsWithVariantsDataRef = useRef(channelsWithVariantsData); channelsWithVariantsData,
hasChanged,
toggleAllChannels,
addVariantToChannel,
removeVariantFromChannel,
toggleAllChannelVariants,
channelVariantListing,
reset
} = useChannelsWithProductVariants(
channels,
variants?.map(variant => variant.id)
);
useModalDialogOpen(open, {
onClose: reset
});
const { query, onQueryChange, filteredChannels } = useChannelsSearch( const { query, onQueryChange, filteredChannels } = useChannelsSearch(
channels channels
); );
const handleSetCanConfirm = () => {
const hasDataInsideDialogChanged = !isEqual(
channelsWithVariantsData,
channelsWithVariantsDataRef.current
);
if (hasDataInsideDialogChanged) {
channelsWithVariantsDataRef.current = channelsWithVariantsData;
setCanConfirm(true);
}
};
useEffect(handleSetCanConfirm, [channelsWithVariantsData]);
const hasAllChannelsSelected = areAllVariantsAtAllChannelsSelected( const hasAllChannelsSelected = areAllVariantsAtAllChannelsSelected(
variants, variants.map(variant => variant.id),
channelsWithVariantsData channelVariantListingDiffToDict(channelsWithVariantsData)
); );
const isChannelSelected = (channelId: string) => const isChannelSelected = (channelId: string) =>
areAnyChannelVariantsSelected(channelsWithVariantsData[channelId]); areAnyChannelVariantsSelected(channelsWithVariantsData[channelId]);
const handleClose = () => {
setCanConfirm(false);
onChannelsAvailiabilityModalClose();
};
const handleConfirm = () => {
setCanConfirm(false);
onChannelsWithVariantsConfirm();
};
return ( return (
<ActionDialog <ActionDialog
confirmButtonState="default" confirmButtonState="default"
open={isChannelsAvailabilityModalOpen} open={open}
onClose={handleClose} onClose={onClose}
onConfirm={handleConfirm} onConfirm={() => onConfirm(channelVariantListing)}
title={intl.formatMessage(messages.title)} title={intl.formatMessage(messages.title)}
disabled={!canConfirm} disabled={!hasChanged}
> >
<ChannelsAvailabilityContentWrapper <ChannelsAvailabilityContentWrapper
hasAllSelected={hasAllChannelsSelected} hasAllSelected={hasAllChannelsSelected}
@ -110,7 +93,9 @@ export const ChannelsWithVariantsAvailabilityDialog: React.FC<ChannelsAvailabili
channels={filteredChannels} channels={filteredChannels}
isChannelSelected={isChannelSelected} isChannelSelected={isChannelSelected}
channelsWithVariants={channelsWithVariantsData} channelsWithVariants={channelsWithVariantsData}
{...rest} addVariantToChannel={addVariantToChannel}
removeVariantFromChannel={removeVariantFromChannel}
toggleAllChannelVariants={toggleAllChannelVariants}
/> />
</ChannelsAvailabilityContentWrapper> </ChannelsAvailabilityContentWrapper>
</ActionDialog> </ActionDialog>

View file

@ -14,7 +14,10 @@ import Label from "@saleor/orders/components/OrderHistory/Label";
import { getById } from "@saleor/orders/components/OrderReturnPage/utils"; import { getById } from "@saleor/orders/components/OrderReturnPage/utils";
import { ProductDetails_product_variants } from "@saleor/products/types/ProductDetails"; import { ProductDetails_product_variants } from "@saleor/products/types/ProductDetails";
import { ChannelsWithVariantsData } from "@saleor/products/views/ProductUpdate/types"; 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 map from "lodash/map";
import React, { ChangeEvent } from "react"; import React, { ChangeEvent } from "react";
import { defineMessages, useIntl } from "react-intl"; import { defineMessages, useIntl } from "react-intl";
@ -96,7 +99,7 @@ interface ChannelsWithVariantsAvailabilityDialogContentProps {
addVariantToChannel: (channelId: string, variantId: string) => void; addVariantToChannel: (channelId: string, variantId: string) => void;
removeVariantFromChannel: (channelId: string, variantId: string) => void; removeVariantFromChannel: (channelId: string, variantId: string) => void;
channelsWithVariants: ChannelsWithVariantsData; channelsWithVariants: ChannelsWithVariantsData;
toggleAllChannelVariants: (channelId: string) => () => void; toggleAllChannelVariants: (channelId: string) => void;
isChannelSelected: (channelId: string) => boolean; isChannelSelected: (channelId: string) => boolean;
channels: ChannelData[]; channels: ChannelData[];
allVariants: ProductDetails_product_variants[]; allVariants: ProductDetails_product_variants[];
@ -125,8 +128,8 @@ const ChannelsWithVariantsAvailabilityDialogContent: React.FC<ChannelsWithVarian
const selectChannelIcon = (channelId: string) => const selectChannelIcon = (channelId: string) =>
areAllChannelVariantsSelected( areAllChannelVariantsSelected(
allVariants, allVariants?.map(variant => variant.id),
channelsWithVariants[channelId] channelVariantListingDiffToDict(channelsWithVariants)[channelId]
) ? ( ) ? (
<IconCheckboxChecked /> <IconCheckboxChecked />
) : ( ) : (
@ -155,6 +158,7 @@ const ChannelsWithVariantsAvailabilityDialogContent: React.FC<ChannelsWithVarian
<ExpansionPanel <ExpansionPanel
classes={expanderClasses} classes={expanderClasses}
data-test-id="expand-channel-row" data-test-id="expand-channel-row"
key={channelId}
> >
<ExpansionPanelSummary <ExpansionPanelSummary
expandIcon={<IconChevronDown />} expandIcon={<IconChevronDown />}
@ -183,14 +187,14 @@ const ChannelsWithVariantsAvailabilityDialogContent: React.FC<ChannelsWithVarian
/> />
</div> </div>
} }
onChange={toggleAllChannelVariants(channelId)} onChange={() => toggleAllChannelVariants(channelId)}
/> />
</div> </div>
<Divider /> <Divider />
</div> </div>
</ExpansionPanelSummary> </ExpansionPanelSummary>
{allVariants.map(({ id: variantId, name }) => ( {allVariants.map(({ id: variantId, name }) => (
<> <React.Fragment key={variantId}>
<div <div
data-test-id="channel-variant-row" data-test-id="channel-variant-row"
key={variantId} key={variantId}
@ -209,7 +213,7 @@ const ChannelsWithVariantsAvailabilityDialogContent: React.FC<ChannelsWithVarian
/> />
</div> </div>
<Divider /> <Divider />
</> </React.Fragment>
))} ))}
</ExpansionPanel> </ExpansionPanel>
); );

View file

@ -18,10 +18,10 @@ export interface ActionDialogProps extends DialogProps {
} }
const ActionDialog: React.FC<ActionDialogProps> = props => { const ActionDialog: React.FC<ActionDialogProps> = props => {
const { children, open, title, onClose, variant, ...rest } = props; const { children, open, title, onClose, variant, maxWidth, ...rest } = props;
return ( return (
<Dialog fullWidth onClose={onClose} open={open} {...rest}> <Dialog fullWidth onClose={onClose} open={open} maxWidth={maxWidth}>
<DialogTitle>{title}</DialogTitle> <DialogTitle>{title}</DialogTitle>
<DialogContent>{children}</DialogContent> <DialogContent>{children}</DialogContent>
<DialogButtons {...rest} onClose={onClose} variant={variant} /> <DialogButtons {...rest} onClose={onClose} variant={variant} />

View file

@ -27,6 +27,7 @@ import useNavigator from "@saleor/hooks/useNavigator";
import useNotifier from "@saleor/hooks/useNotifier"; import useNotifier from "@saleor/hooks/useNotifier";
import useOnSetDefaultVariant from "@saleor/hooks/useOnSetDefaultVariant"; import useOnSetDefaultVariant from "@saleor/hooks/useOnSetDefaultVariant";
import useShop from "@saleor/hooks/useShop"; import useShop from "@saleor/hooks/useShop";
import useStateFromProps from "@saleor/hooks/useStateFromProps";
import { commonMessages, errorMessages } from "@saleor/intl"; import { commonMessages, errorMessages } from "@saleor/intl";
import ProductVariantCreateDialog from "@saleor/products/components/ProductVariantCreateDialog"; import ProductVariantCreateDialog from "@saleor/products/components/ProductVariantCreateDialog";
import { import {
@ -61,9 +62,7 @@ import React from "react";
import { defineMessages, FormattedMessage, useIntl } from "react-intl"; import { defineMessages, FormattedMessage, useIntl } from "react-intl";
import { getMutationState } from "../../../misc"; import { getMutationState } from "../../../misc";
import ProductUpdatePage, { import ProductUpdatePage from "../../components/ProductUpdatePage";
ProductUpdatePageSubmitData
} from "../../components/ProductUpdatePage";
import { useProductDetails } from "../../queries"; import { useProductDetails } from "../../queries";
import { ProductMediaCreateVariables } from "../../types/ProductMediaCreate"; import { ProductMediaCreateVariables } from "../../types/ProductMediaCreate";
import { ProductUpdate as ProductUpdateMutationResult } from "../../types/ProductUpdate"; import { ProductUpdate as ProductUpdateMutationResult } from "../../types/ProductUpdate";
@ -77,13 +76,14 @@ import {
productVariantCreatorUrl, productVariantCreatorUrl,
productVariantEditUrl productVariantEditUrl
} from "../../urls"; } from "../../urls";
import { CHANNELS_AVAILIABILITY_MODAL_SELECTOR } from "./consts";
import { import {
createImageReorderHandler, createImageReorderHandler,
createImageUploadHandler, createImageUploadHandler,
createUpdateHandler, createUpdateHandler,
createVariantReorderHandler createVariantReorderHandler
} from "./handlers"; } from "./handlers";
import useChannelsWithProductVariants from "./useChannelsWithProductVariants"; import useChannelVariantListings from "./useChannelVariantListings";
const messages = defineMessages({ const messages = defineMessages({
deleteProductDialogTitle: { deleteProductDialogTitle: {
@ -274,21 +274,13 @@ export const ProductUpdate: React.FC<ProductUpdateProps> = ({ id, params }) => {
channel.name.localeCompare(nextChannel.name) channel.name.localeCompare(nextChannel.name)
); );
const [channelsData, setChannelsData] = useStateFromProps(allChannels);
const { const {
channels: updatedChannels,
channelsWithVariantsData, channelsWithVariantsData,
haveChannelsWithVariantsDataChanged, hasChanged: hasChannelVariantListingChanged,
setHaveChannelsWithVariantsChanged, setChannelVariantListing
onChannelsAvailiabilityModalOpen, } = useChannelVariantListings(allChannels);
channelsData,
setChannelsData,
...channelsWithVariantsProps
} = useChannelsWithProductVariants({
channels: allChannels,
variants: product?.variants,
action: params?.action,
openModal,
closeModal
});
const productChannelsChoices: ChannelData[] = createSortedChannelsDataFromProduct( const productChannelsChoices: ChannelData[] = createSortedChannelsDataFromProduct(
product product
@ -517,18 +509,19 @@ export const ProductUpdate: React.FC<ProductUpdateProps> = ({ id, params }) => {
/> />
) : ( ) : (
<ChannelsWithVariantsAvailabilityDialog <ChannelsWithVariantsAvailabilityDialog
channelsWithVariantsData={channelsWithVariantsData} channels={updatedChannels}
haveChannelsWithVariantsDataChanged={
haveChannelsWithVariantsDataChanged
}
{...channelsWithVariantsProps}
channels={allChannels}
variants={product?.variants} variants={product?.variants}
open={params.action === CHANNELS_AVAILIABILITY_MODAL_SELECTOR}
onClose={closeModal}
onConfirm={listings => {
closeModal();
setChannelVariantListing(listings);
}}
/> />
))} ))}
<ProductUpdatePage <ProductUpdatePage
hasChannelChanged={ hasChannelChanged={
haveChannelsWithVariantsDataChanged || hasChannelVariantListingChanged ||
productChannelsChoices?.length !== currentChannels?.length productChannelsChoices?.length !== currentChannels?.length
} }
isSimpleProduct={isSimpleProduct} isSimpleProduct={isSimpleProduct}
@ -563,10 +556,7 @@ export const ProductUpdate: React.FC<ProductUpdateProps> = ({ id, params }) => {
onDelete={() => openModal("remove")} onDelete={() => openModal("remove")}
onImageReorder={handleImageReorder} onImageReorder={handleImageReorder}
onMediaUrlUpload={handleMediaUrlUpload} onMediaUrlUpload={handleMediaUrlUpload}
onSubmit={(formData: ProductUpdatePageSubmitData) => { onSubmit={handleSubmit}
setHaveChannelsWithVariantsChanged(false);
return handleSubmit(formData);
}}
onWarehouseConfigure={() => navigate(warehouseAddPath)} onWarehouseConfigure={() => navigate(warehouseAddPath)}
onVariantAdd={handleVariantAdd} onVariantAdd={handleVariantAdd}
onVariantsAdd={() => openModal("add-variants")} onVariantsAdd={() => openModal("add-variants")}

View file

@ -0,0 +1,9 @@
import { ChannelWithVariantData } from "./types";
export const CHANNELS_AVAILIABILITY_MODAL_SELECTOR = "open-channels-picker";
export const initialChannelWithVariantData: ChannelWithVariantData = {
variantsIdsToRemove: [],
variantsIdsToAdd: [],
selectedVariantsIds: []
};

View file

@ -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 { export interface ChannelWithVariantData {
selectedVariantsIds: string[]; selectedVariantsIds: string[];
variantsIdsToRemove: string[]; variantsIdsToRemove: string[];
variantsIdsToAdd: string[]; variantsIdsToAdd: string[];
} }
export type ChannelVariantListing = Record<string, string[]>;
export type ChannelsWithVariantsData = Record<string, ChannelWithVariantData>; export type ChannelsWithVariantsData = Record<string, ChannelWithVariantData>;
export const initialChannelWithVariantData: ChannelWithVariantData = {
variantsIdsToRemove: [],
variantsIdsToAdd: [],
selectedVariantsIds: []
};
export const CHANNELS_AVAILIABILITY_MODAL_SELECTOR = "open-channels-picker";
export interface UseChannelsWithProductVariants { export interface UseChannelsWithProductVariants {
channelsData: ChannelData[];
setChannelsData: (data: ChannelData[]) => void;
onChannelsWithVariantsConfirm: () => void;
addVariantToChannel: (channelId: string, variantId: string) => void; addVariantToChannel: (channelId: string, variantId: string) => void;
removeVariantFromChannel: (channelId: string, variantId: string) => void; removeVariantFromChannel: (channelId: string, variantId: string) => void;
channelsWithVariantsData: ChannelsWithVariantsData; channelsWithVariantsData: ChannelsWithVariantsData;
onChannelsAvailiabilityModalOpen: () => void;
onChannelsAvailiabilityModalClose: () => void;
isChannelsAvailabilityModalOpen: boolean;
toggleAllChannels: () => void; toggleAllChannels: () => void;
toggleAllChannelVariants: (channelId: string) => () => void; toggleAllChannelVariants: (channelId: string) => void;
haveChannelsWithVariantsDataChanged: boolean; hasChanged: boolean;
setHaveChannelsWithVariantsChanged: (hasChanged: boolean) => void; channelVariantListing: ChannelVariantListing;
setChannelVariantListing: (listings: ChannelVariantListing) => void;
reset: () => void;
} }

View file

@ -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<ChannelsWithVariantsData>(
() =>
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;

View file

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

View file

@ -1,139 +1,63 @@
import useStateFromProps from "@saleor/hooks/useStateFromProps"; import { ChannelData } from "@saleor/channels/utils";
import isEmpty from "lodash/isEmpty"; import uniq from "lodash/uniq";
import reduce from "lodash/reduce";
import { useEffect, useRef, useState } from "react";
import { UseChannelsWithProductVariants } from "./types";
import useChannelVariantListings from "./useChannelVariantListings";
import { import {
CHANNELS_AVAILIABILITY_MODAL_SELECTOR, addAllVariantsToAllChannels,
ChannelsWithVariantsData, getChannelVariantToggleData
UseChannelsWithProductVariants,
UseChannelsWithProductVariantsProps
} from "./types";
import {
areAllVariantsAtAllChannelsSelected,
areAnyChannelVariantsSelected,
getChannelVariantToggleData,
getChannelWithAddedVariantData,
getChannelWithRemovedVariantData,
getParsedChannelsWithVariantsDataFromChannels
} from "./utils"; } from "./utils";
const useChannelsWithProductVariants = ({ const useChannelsWithProductVariants = (
channels, channels: ChannelData[],
variants, variants: string[]
openModal, ): UseChannelsWithProductVariants => {
closeModal, const {
action
}: UseChannelsWithProductVariantsProps): UseChannelsWithProductVariants => {
const [channelsData, setChannelsData] = useStateFromProps(channels);
const initialChannelsWithVariantsData = getParsedChannelsWithVariantsDataFromChannels(
channels
);
const [
channelsWithVariantsData, channelsWithVariantsData,
setChannelsWithVariantsData hasChanged,
] = useStateFromProps<ChannelsWithVariantsData>( setChannelVariantListing,
initialChannelsWithVariantsData channelVariantListing,
); reset
} = useChannelVariantListings(channels);
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]);
const handleAddVariant = (channelId: string, variantId: string) => const handleAddVariant = (channelId: string, variantId: string) =>
setChannelsWithVariantsData({ setChannelVariantListing(listings => ({
...channelsWithVariantsData, ...listings,
...getChannelWithAddedVariantData({ [channelId]: uniq([...listings[channelId], variantId])
channelWithVariantsData: channelsWithVariantsData[channelId], }));
channelId,
variantId
})
});
const handleRemoveVariant = (channelId: string, variantId: string) => const handleRemoveVariant = (channelId: string, variantId: string) =>
setChannelsWithVariantsData({ setChannelVariantListing(listings => ({
...channelsWithVariantsData, ...listings,
...getChannelWithRemovedVariantData({ [channelId]: listings[channelId].filter(
channelWithVariantsData: channelsWithVariantsData[channelId], selectedVariantId => selectedVariantId !== variantId
channelId, )
variantId }));
})
});
const toggleAllChannelVariants = (channelId: string) => () => { const toggleAllChannelVariants = (channelId: string) => {
const isChannelSelected = areAnyChannelVariantsSelected( const isChannelSelected = channelVariantListing[channelId].length > 0;
channelsWithVariantsData[channelId]
);
setChannelsWithVariantsData({ setChannelVariantListing({
...channelsWithVariantsData, ...channelVariantListing,
[channelId]: getChannelVariantToggleData(variants, isChannelSelected) [channelId]: getChannelVariantToggleData(variants, isChannelSelected)
}); });
}; };
const toggleAllChannels = () => { const toggleAllChannels = () =>
const areAllChannelsSelected = areAllVariantsAtAllChannelsSelected( setChannelVariantListing(listings =>
variants, addAllVariantsToAllChannels(listings, variants)
channelsWithVariantsData
); );
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 { return {
channelsWithVariantsData, channelsWithVariantsData,
setChannelsData, channelVariantListing,
channelsData,
addVariantToChannel: handleAddVariant, addVariantToChannel: handleAddVariant,
removeVariantFromChannel: handleRemoveVariant, removeVariantFromChannel: handleRemoveVariant,
onChannelsAvailiabilityModalOpen: handleModalOpen, hasChanged,
onChannelsAvailiabilityModalClose: closeModal,
isChannelsAvailabilityModalOpen: isModalOpen,
haveChannelsWithVariantsDataChanged: hasChanged,
toggleAllChannelVariants, toggleAllChannelVariants,
toggleAllChannels, toggleAllChannels,
onChannelsWithVariantsConfirm, setChannelVariantListing,
setHaveChannelsWithVariantsChanged: setHasChanged reset
}; };
}; };

View file

@ -1,111 +1,74 @@
import {
getUpdatedIdsWithNewId,
getUpdatedIdsWithoutNewId
} from "@saleor/channels/pages/ChannelDetailsPage/utils";
import { ChannelData } from "@saleor/channels/utils"; import { ChannelData } from "@saleor/channels/utils";
import { ProductDetails_product_variants } from "@saleor/products/types/ProductDetails";
import every from "lodash/every"; import every from "lodash/every";
import reduce from "lodash/reduce"; import reduce from "lodash/reduce";
import { initialChannelWithVariantData } from "./consts";
import { import {
ChannelsWithVariantsData, ChannelsWithVariantsData,
ChannelWithVariantData, ChannelVariantListing,
initialChannelWithVariantData ChannelWithVariantData
} from "./types"; } from "./types";
export function createFromChannels<T>(
channels: ChannelData[],
cb: (channel: ChannelData) => T
): Record<string, T> {
return channels?.reduce(
(result: Record<string, T>, 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 = ( export const getParsedChannelsWithVariantsDataFromChannels = (
channels: ChannelData[] channels: ChannelData[]
): ChannelsWithVariantsData => ): ChannelsWithVariantsData =>
channels?.reduce( createFromChannels(
(result: ChannelsWithVariantsData, { id, variantsIds }) => ({ channels,
...result, ({ variantsIds }) =>
[id]: { ({
...initialChannelWithVariantData, ...initialChannelWithVariantData,
selectedVariantsIds: variantsIds 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 = ( export const getChannelVariantToggleData = (
variants: ProductDetails_product_variants[], variants: string[],
isSelected: boolean isSelected: boolean
): ChannelWithVariantData => { ): string[] => (isSelected ? [] : variants);
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);
export const areAllVariantsAtAllChannelsSelected = ( export const areAllVariantsAtAllChannelsSelected = (
variants: ProductDetails_product_variants[] = [], variants: string[] = [],
channelsWithVariants: ChannelsWithVariantsData = {} channelsWithVariants: ChannelVariantListing = {}
) => ) =>
every(channelsWithVariants, channelWithVariantsData => every(channelsWithVariants, channelWithVariantsData =>
areAllChannelVariantsSelected(variants, channelWithVariantsData) areAllChannelVariantsSelected(variants, channelWithVariantsData)
); );
export const areAllChannelVariantsSelected = ( export const areAllChannelVariantsSelected = (
variants: ProductDetails_product_variants[] = [], variants: string[] = [],
{ selectedVariantsIds }: Pick<ChannelWithVariantData, "selectedVariantsIds"> selectedVariants: string[]
) => selectedVariantsIds.length === variants.length; ) => selectedVariants.length === variants.length;
export const areAnyChannelVariantsSelected = ( export const areAnyChannelVariantsSelected = (
channelsWithVariantsData: ChannelWithVariantData channelsWithVariantsData: ChannelWithVariantData
@ -114,9 +77,43 @@ export const areAnyChannelVariantsSelected = (
export const getTotalSelectedChannelsCount = ( export const getTotalSelectedChannelsCount = (
channelsWithVariantsData: ChannelsWithVariantsData channelsWithVariantsData: ChannelsWithVariantsData
) => ) =>
reduce( Object.values(channelsWithVariantsData).filter(
channelsWithVariantsData, channel => channel.selectedVariantsIds.length > 0
(result, { selectedVariantsIds }) => ).length;
selectedVariantsIds.length ? result + 1 : result,
0 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
}),
{}
); );