* 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:
parent
90717b7ed0
commit
ffe44be733
12 changed files with 467 additions and 354 deletions
|
@ -111,9 +111,10 @@ const ChannelWithVariantsAvailabilityItemWrapper: React.FC<ChannelAvailabilityIt
|
|||
|
||||
const variantsCount = selectedVariantsIds.length;
|
||||
|
||||
const variantsLabel = areAllChannelVariantsSelected(variants, {
|
||||
const variantsLabel = areAllChannelVariantsSelected(
|
||||
variants?.map(variant => variant.id),
|
||||
selectedVariantsIds
|
||||
})
|
||||
)
|
||||
? messages.allVariantsLabel
|
||||
: messages.variantCountLabel;
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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<ChannelsAvailabilityDialogProps> = ({
|
||||
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 (
|
||||
<ActionDialog
|
||||
confirmButtonState="default"
|
||||
open={isChannelsAvailabilityModalOpen}
|
||||
onClose={handleClose}
|
||||
onConfirm={handleConfirm}
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
onConfirm={() => onConfirm(channelVariantListing)}
|
||||
title={intl.formatMessage(messages.title)}
|
||||
disabled={!canConfirm}
|
||||
disabled={!hasChanged}
|
||||
>
|
||||
<ChannelsAvailabilityContentWrapper
|
||||
hasAllSelected={hasAllChannelsSelected}
|
||||
|
@ -110,7 +93,9 @@ export const ChannelsWithVariantsAvailabilityDialog: React.FC<ChannelsAvailabili
|
|||
channels={filteredChannels}
|
||||
isChannelSelected={isChannelSelected}
|
||||
channelsWithVariants={channelsWithVariantsData}
|
||||
{...rest}
|
||||
addVariantToChannel={addVariantToChannel}
|
||||
removeVariantFromChannel={removeVariantFromChannel}
|
||||
toggleAllChannelVariants={toggleAllChannelVariants}
|
||||
/>
|
||||
</ChannelsAvailabilityContentWrapper>
|
||||
</ActionDialog>
|
||||
|
|
|
@ -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<ChannelsWithVarian
|
|||
|
||||
const selectChannelIcon = (channelId: string) =>
|
||||
areAllChannelVariantsSelected(
|
||||
allVariants,
|
||||
channelsWithVariants[channelId]
|
||||
allVariants?.map(variant => variant.id),
|
||||
channelVariantListingDiffToDict(channelsWithVariants)[channelId]
|
||||
) ? (
|
||||
<IconCheckboxChecked />
|
||||
) : (
|
||||
|
@ -155,6 +158,7 @@ const ChannelsWithVariantsAvailabilityDialogContent: React.FC<ChannelsWithVarian
|
|||
<ExpansionPanel
|
||||
classes={expanderClasses}
|
||||
data-test-id="expand-channel-row"
|
||||
key={channelId}
|
||||
>
|
||||
<ExpansionPanelSummary
|
||||
expandIcon={<IconChevronDown />}
|
||||
|
@ -183,14 +187,14 @@ const ChannelsWithVariantsAvailabilityDialogContent: React.FC<ChannelsWithVarian
|
|||
/>
|
||||
</div>
|
||||
}
|
||||
onChange={toggleAllChannelVariants(channelId)}
|
||||
onChange={() => toggleAllChannelVariants(channelId)}
|
||||
/>
|
||||
</div>
|
||||
<Divider />
|
||||
</div>
|
||||
</ExpansionPanelSummary>
|
||||
{allVariants.map(({ id: variantId, name }) => (
|
||||
<>
|
||||
<React.Fragment key={variantId}>
|
||||
<div
|
||||
data-test-id="channel-variant-row"
|
||||
key={variantId}
|
||||
|
@ -209,7 +213,7 @@ const ChannelsWithVariantsAvailabilityDialogContent: React.FC<ChannelsWithVarian
|
|||
/>
|
||||
</div>
|
||||
<Divider />
|
||||
</>
|
||||
</React.Fragment>
|
||||
))}
|
||||
</ExpansionPanel>
|
||||
);
|
||||
|
|
|
@ -18,10 +18,10 @@ export interface ActionDialogProps extends DialogProps {
|
|||
}
|
||||
|
||||
const ActionDialog: React.FC<ActionDialogProps> = props => {
|
||||
const { children, open, title, onClose, variant, ...rest } = props;
|
||||
const { children, open, title, onClose, variant, maxWidth, ...rest } = props;
|
||||
|
||||
return (
|
||||
<Dialog fullWidth onClose={onClose} open={open} {...rest}>
|
||||
<Dialog fullWidth onClose={onClose} open={open} maxWidth={maxWidth}>
|
||||
<DialogTitle>{title}</DialogTitle>
|
||||
<DialogContent>{children}</DialogContent>
|
||||
<DialogButtons {...rest} onClose={onClose} variant={variant} />
|
||||
|
|
|
@ -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<ProductUpdateProps> = ({ 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<ProductUpdateProps> = ({ id, params }) => {
|
|||
/>
|
||||
) : (
|
||||
<ChannelsWithVariantsAvailabilityDialog
|
||||
channelsWithVariantsData={channelsWithVariantsData}
|
||||
haveChannelsWithVariantsDataChanged={
|
||||
haveChannelsWithVariantsDataChanged
|
||||
}
|
||||
{...channelsWithVariantsProps}
|
||||
channels={allChannels}
|
||||
channels={updatedChannels}
|
||||
variants={product?.variants}
|
||||
open={params.action === CHANNELS_AVAILIABILITY_MODAL_SELECTOR}
|
||||
onClose={closeModal}
|
||||
onConfirm={listings => {
|
||||
closeModal();
|
||||
setChannelVariantListing(listings);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
<ProductUpdatePage
|
||||
hasChannelChanged={
|
||||
haveChannelsWithVariantsDataChanged ||
|
||||
hasChannelVariantListingChanged ||
|
||||
productChannelsChoices?.length !== currentChannels?.length
|
||||
}
|
||||
isSimpleProduct={isSimpleProduct}
|
||||
|
@ -563,10 +556,7 @@ export const ProductUpdate: React.FC<ProductUpdateProps> = ({ 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")}
|
||||
|
|
9
src/products/views/ProductUpdate/consts.ts
Normal file
9
src/products/views/ProductUpdate/consts.ts
Normal 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: []
|
||||
};
|
|
@ -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<string, string[]>;
|
||||
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 {
|
||||
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;
|
||||
}
|
||||
|
|
|
@ -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;
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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<ChannelsWithVariantsData>(
|
||||
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
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -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<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 = (
|
||||
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<ChannelWithVariantData, "selectedVariantsIds">
|
||||
) => 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
|
||||
}),
|
||||
{}
|
||||
);
|
||||
|
|
Loading…
Reference in a new issue