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 variantsLabel = areAllChannelVariantsSelected(variants, {
const variantsLabel = areAllChannelVariantsSelected(
variants?.map(variant => variant.id),
selectedVariantsIds
})
)
? messages.allVariantsLabel
: messages.variantCountLabel;

View file

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

View file

@ -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>

View file

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

View file

@ -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} />

View file

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

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 {
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;
}

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

View file

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