Improve saving variant images (#2640)
* Improve saving variant images * Update storybook types * Add getSelectedMedia function for products * Add test for handleAssignMedia in products * Follow AAA test convention * Add confirm button to media dialog selector * Move dialog styles to separate file
This commit is contained in:
parent
b98d5d57ef
commit
0741d3ca2c
11 changed files with 451 additions and 95 deletions
|
@ -5,65 +5,58 @@ import {
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from "@material-ui/core";
|
} from "@material-ui/core";
|
||||||
import BackButton from "@saleor/components/BackButton";
|
import BackButton from "@saleor/components/BackButton";
|
||||||
|
import ConfirmButton from "@saleor/components/ConfirmButton";
|
||||||
import { ProductMediaFragment } from "@saleor/graphql";
|
import { ProductMediaFragment } from "@saleor/graphql";
|
||||||
import { makeStyles } from "@saleor/macaw-ui";
|
import useModalDialogOpen from "@saleor/hooks/useModalDialogOpen";
|
||||||
|
import { buttonMessages } from "@saleor/intl";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import React from "react";
|
import React, { useState } from "react";
|
||||||
import { FormattedMessage } from "react-intl";
|
import { FormattedMessage } from "react-intl";
|
||||||
|
|
||||||
const useStyles = makeStyles(
|
import { useStyles } from "./styles";
|
||||||
theme => ({
|
|
||||||
image: {
|
|
||||||
height: "100%",
|
|
||||||
objectFit: "contain",
|
|
||||||
userSelect: "none",
|
|
||||||
width: "100%",
|
|
||||||
},
|
|
||||||
imageContainer: {
|
|
||||||
background: "transparent",
|
|
||||||
border: "1px solid #eaeaea",
|
|
||||||
borderRadius: theme.spacing(),
|
|
||||||
cursor: "pointer",
|
|
||||||
height: theme.spacing(21.5),
|
|
||||||
overflow: "hidden",
|
|
||||||
padding: theme.spacing(2),
|
|
||||||
position: "relative",
|
|
||||||
transitionDuration: theme.transitions.duration.standard + "ms",
|
|
||||||
},
|
|
||||||
content: {
|
|
||||||
overflowY: "scroll",
|
|
||||||
},
|
|
||||||
root: {
|
|
||||||
display: "grid",
|
|
||||||
gridColumnGap: theme.spacing(2),
|
|
||||||
gridRowGap: theme.spacing(2),
|
|
||||||
gridTemplateColumns: "repeat(3, 1fr)",
|
|
||||||
maxWidth: "100%",
|
|
||||||
width: theme.breakpoints.values.lg,
|
|
||||||
[theme.breakpoints.down("sm")]: {
|
|
||||||
gridTemplateColumns: "repeat(2, 1fr)",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
selectedImageContainer: {
|
|
||||||
borderColor: theme.palette.primary.main,
|
|
||||||
borderWidth: "2px",
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
{ name: "ProductVariantImageSelectDialog" },
|
|
||||||
);
|
|
||||||
|
|
||||||
interface ProductVariantImageSelectDialogProps {
|
interface ProductVariantImageSelectDialogProps {
|
||||||
media?: ProductMediaFragment[];
|
media?: ProductMediaFragment[];
|
||||||
selectedMedia?: string[];
|
selectedMedia?: string[];
|
||||||
open: boolean;
|
open: boolean;
|
||||||
onClose();
|
onClose: () => void;
|
||||||
onMediaSelect(id: string);
|
onConfirm: (selectedIds: string[]) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ProductVariantMediaSelectDialog: React.FC<ProductVariantImageSelectDialogProps> = props => {
|
const ProductVariantMediaSelectDialog: React.FC<ProductVariantImageSelectDialogProps> = props => {
|
||||||
const { media, open, selectedMedia, onClose, onMediaSelect } = props;
|
const {
|
||||||
|
media,
|
||||||
|
open,
|
||||||
|
selectedMedia: initialMedia,
|
||||||
|
onClose,
|
||||||
|
onConfirm,
|
||||||
|
} = props;
|
||||||
const classes = useStyles(props);
|
const classes = useStyles(props);
|
||||||
|
|
||||||
|
const [selectedMedia, setSelectedMedia] = useState(initialMedia);
|
||||||
|
|
||||||
|
useModalDialogOpen(open, {
|
||||||
|
onOpen: () => setSelectedMedia(initialMedia),
|
||||||
|
onClose: () => setSelectedMedia(initialMedia),
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleMediaSelect = (id: string) => {
|
||||||
|
const isMediaAssigned = selectedMedia.includes(id);
|
||||||
|
|
||||||
|
if (isMediaAssigned) {
|
||||||
|
setSelectedMedia(selectedMedia =>
|
||||||
|
selectedMedia.filter(mediaId => mediaId !== id),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
setSelectedMedia(selectedMedia => [...selectedMedia, id]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleConfirm = () => {
|
||||||
|
onConfirm(selectedMedia);
|
||||||
|
onClose();
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog onClose={onClose} open={open}>
|
<Dialog onClose={onClose} open={open}>
|
||||||
<DialogTitle>
|
<DialogTitle>
|
||||||
|
@ -90,7 +83,7 @@ const ProductVariantMediaSelectDialog: React.FC<ProductVariantImageSelectDialogP
|
||||||
selectedMedia.indexOf(mediaObj.id) !== -1,
|
selectedMedia.indexOf(mediaObj.id) !== -1,
|
||||||
},
|
},
|
||||||
])}
|
])}
|
||||||
onClick={onMediaSelect(mediaObj.id)}
|
onClick={() => handleMediaSelect(mediaObj.id)}
|
||||||
key={mediaObj.id}
|
key={mediaObj.id}
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
|
@ -105,6 +98,13 @@ const ProductVariantMediaSelectDialog: React.FC<ProductVariantImageSelectDialogP
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
<BackButton onClick={onClose} />
|
<BackButton onClick={onClose} />
|
||||||
|
<ConfirmButton
|
||||||
|
transitionState="default"
|
||||||
|
onClick={handleConfirm}
|
||||||
|
data-test-id="submit"
|
||||||
|
>
|
||||||
|
<FormattedMessage {...buttonMessages.confirm} />
|
||||||
|
</ConfirmButton>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
|
|
|
@ -0,0 +1,42 @@
|
||||||
|
import { makeStyles } from "@saleor/macaw-ui";
|
||||||
|
|
||||||
|
export const useStyles = makeStyles(
|
||||||
|
theme => ({
|
||||||
|
image: {
|
||||||
|
height: "100%",
|
||||||
|
objectFit: "contain",
|
||||||
|
userSelect: "none",
|
||||||
|
width: "100%",
|
||||||
|
},
|
||||||
|
imageContainer: {
|
||||||
|
background: "transparent",
|
||||||
|
border: "1px solid #eaeaea",
|
||||||
|
borderRadius: theme.spacing(),
|
||||||
|
cursor: "pointer",
|
||||||
|
height: theme.spacing(21.5),
|
||||||
|
overflow: "hidden",
|
||||||
|
padding: theme.spacing(2),
|
||||||
|
position: "relative",
|
||||||
|
transitionDuration: theme.transitions.duration.standard + "ms",
|
||||||
|
},
|
||||||
|
content: {
|
||||||
|
overflowY: "scroll",
|
||||||
|
},
|
||||||
|
root: {
|
||||||
|
display: "grid",
|
||||||
|
gridColumnGap: theme.spacing(2),
|
||||||
|
gridRowGap: theme.spacing(2),
|
||||||
|
gridTemplateColumns: "repeat(3, 1fr)",
|
||||||
|
maxWidth: "100%",
|
||||||
|
width: theme.breakpoints.values.lg,
|
||||||
|
[theme.breakpoints.down("sm")]: {
|
||||||
|
gridTemplateColumns: "repeat(2, 1fr)",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
selectedImageContainer: {
|
||||||
|
borderColor: theme.palette.primary.main,
|
||||||
|
borderWidth: "2px",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
{ name: "ProductVariantImageSelectDialog" },
|
||||||
|
);
|
|
@ -29,6 +29,7 @@ import useNavigator from "@saleor/hooks/useNavigator";
|
||||||
import { ConfirmButtonTransitionState } from "@saleor/macaw-ui";
|
import { ConfirmButtonTransitionState } from "@saleor/macaw-ui";
|
||||||
import { VariantDetailsChannelsAvailabilityCard } from "@saleor/products/components/ProductVariantChannels/ChannelsAvailabilityCard";
|
import { VariantDetailsChannelsAvailabilityCard } from "@saleor/products/components/ProductVariantChannels/ChannelsAvailabilityCard";
|
||||||
import { productUrl } from "@saleor/products/urls";
|
import { productUrl } from "@saleor/products/urls";
|
||||||
|
import { getSelectedMedia } from "@saleor/products/utils/data";
|
||||||
import { FetchMoreProps, RelayToFlat, ReorderAction } from "@saleor/types";
|
import { FetchMoreProps, RelayToFlat, ReorderAction } from "@saleor/types";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { defineMessages, useIntl } from "react-intl";
|
import { defineMessages, useIntl } from "react-intl";
|
||||||
|
@ -118,7 +119,6 @@ interface ProductVariantPageProps {
|
||||||
onAttributeSelectBlur: () => void;
|
onAttributeSelectBlur: () => void;
|
||||||
onDelete();
|
onDelete();
|
||||||
onSubmit(data: ProductVariantUpdateSubmitData);
|
onSubmit(data: ProductVariantUpdateSubmitData);
|
||||||
onMediaSelect(id: string);
|
|
||||||
onSetDefaultVariant();
|
onSetDefaultVariant();
|
||||||
onWarehouseConfigure();
|
onWarehouseConfigure();
|
||||||
}
|
}
|
||||||
|
@ -140,7 +140,6 @@ const ProductVariantPage: React.FC<ProductVariantPageProps> = ({
|
||||||
referenceProducts = [],
|
referenceProducts = [],
|
||||||
attributeValues,
|
attributeValues,
|
||||||
onDelete,
|
onDelete,
|
||||||
onMediaSelect,
|
|
||||||
onSubmit,
|
onSubmit,
|
||||||
onVariantPreorderDeactivate,
|
onVariantPreorderDeactivate,
|
||||||
variantDeactivatePreoderButtonState,
|
variantDeactivatePreoderButtonState,
|
||||||
|
@ -172,13 +171,9 @@ const ProductVariantPage: React.FC<ProductVariantPageProps> = ({
|
||||||
setIsEndPreorderModalOpened,
|
setIsEndPreorderModalOpened,
|
||||||
] = React.useState(false);
|
] = React.useState(false);
|
||||||
|
|
||||||
const variantMedia = variant?.media?.map(image => image.id);
|
|
||||||
const productMedia = [
|
const productMedia = [
|
||||||
...(variant?.product?.media ?? []),
|
...(variant?.product?.media ?? []),
|
||||||
]?.sort((prev, next) => (prev.sortOrder > next.sortOrder ? 1 : -1));
|
]?.sort((prev, next) => (prev.sortOrder > next.sortOrder ? 1 : -1));
|
||||||
const media = productMedia
|
|
||||||
?.filter(image => variantMedia.indexOf(image.id) !== -1)
|
|
||||||
.sort((prev, next) => (prev.sortOrder > next.sortOrder ? 1 : -1));
|
|
||||||
|
|
||||||
const canOpenAssignReferencesAttributeDialog = !!assignReferencesAttributeId;
|
const canOpenAssignReferencesAttributeDialog = !!assignReferencesAttributeId;
|
||||||
|
|
||||||
|
@ -246,6 +241,7 @@ const ProductVariantPage: React.FC<ProductVariantPageProps> = ({
|
||||||
const selectionAttributes = data.attributes.filter(
|
const selectionAttributes = data.attributes.filter(
|
||||||
byAttributeScope(VariantAttributeScope.VARIANT_SELECTION),
|
byAttributeScope(VariantAttributeScope.VARIANT_SELECTION),
|
||||||
);
|
);
|
||||||
|
const media = getSelectedMedia(productMedia, data.media);
|
||||||
|
|
||||||
const errors = [...apiErrors, ...validationErrors];
|
const errors = [...apiErrors, ...validationErrors];
|
||||||
|
|
||||||
|
@ -423,28 +419,28 @@ const ProductVariantPage: React.FC<ProductVariantPageProps> = ({
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{variant && (
|
{variant && (
|
||||||
<VariantChannelsDialog
|
<>
|
||||||
channelListings={variant.product.channelListings}
|
<VariantChannelsDialog
|
||||||
selectedChannelListings={data.channelListings}
|
channelListings={variant.product.channelListings}
|
||||||
open={isManageChannelsModalOpen}
|
selectedChannelListings={data.channelListings}
|
||||||
onClose={toggleManageChannels}
|
open={isManageChannelsModalOpen}
|
||||||
onConfirm={handlers.updateChannels}
|
onClose={toggleManageChannels}
|
||||||
/>
|
onConfirm={handlers.updateChannels}
|
||||||
|
/>
|
||||||
|
<ProductVariantMediaSelectDialog
|
||||||
|
onClose={toggleModal}
|
||||||
|
onConfirm={handlers.changeMedia}
|
||||||
|
open={isModalOpened}
|
||||||
|
media={productMedia}
|
||||||
|
selectedMedia={data.media}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
</ProductVariantUpdateForm>
|
</ProductVariantUpdateForm>
|
||||||
</Container>
|
</Container>
|
||||||
{variant && (
|
|
||||||
<ProductVariantMediaSelectDialog
|
|
||||||
onClose={toggleModal}
|
|
||||||
onMediaSelect={onMediaSelect}
|
|
||||||
open={isModalOpened}
|
|
||||||
media={productMedia}
|
|
||||||
selectedMedia={variant?.media.map(image => image.id)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{!!variant?.preorder && (
|
{!!variant?.preorder && (
|
||||||
<ProductVariantEndPreorderDialog
|
<ProductVariantEndPreorderDialog
|
||||||
confirmButtonState={variantDeactivatePreoderButtonState}
|
confirmButtonState={variantDeactivatePreoderButtonState}
|
||||||
|
|
|
@ -44,6 +44,7 @@ import {
|
||||||
getStockInputFromVariant,
|
getStockInputFromVariant,
|
||||||
} from "@saleor/products/utils/data";
|
} from "@saleor/products/utils/data";
|
||||||
import {
|
import {
|
||||||
|
createMediaChangeHandler,
|
||||||
createPreorderEndDateChangeHandler,
|
createPreorderEndDateChangeHandler,
|
||||||
getChannelsInput,
|
getChannelsInput,
|
||||||
} from "@saleor/products/utils/handlers";
|
} from "@saleor/products/utils/handlers";
|
||||||
|
@ -78,6 +79,7 @@ export interface ProductVariantUpdateFormData extends MetadataFormData {
|
||||||
hasPreorderEndDate: boolean;
|
hasPreorderEndDate: boolean;
|
||||||
preorderEndDateTime?: string;
|
preorderEndDateTime?: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
media: string[];
|
||||||
}
|
}
|
||||||
export interface ProductVariantUpdateData extends ProductVariantUpdateFormData {
|
export interface ProductVariantUpdateData extends ProductVariantUpdateFormData {
|
||||||
channelListings: FormsetData<
|
channelListings: FormsetData<
|
||||||
|
@ -126,6 +128,7 @@ export interface ProductVariantUpdateHandlers
|
||||||
Record<"addStock" | "deleteStock", (id: string) => void> {
|
Record<"addStock" | "deleteStock", (id: string) => void> {
|
||||||
changePreorderEndDate: FormChange;
|
changePreorderEndDate: FormChange;
|
||||||
changeMetadata: FormChange;
|
changeMetadata: FormChange;
|
||||||
|
changeMedia: (ids: string[]) => void;
|
||||||
updateChannels: (selectedChannelsIds: string[]) => void;
|
updateChannels: (selectedChannelsIds: string[]) => void;
|
||||||
fetchReferences: (value: string) => void;
|
fetchReferences: (value: string) => void;
|
||||||
fetchMoreReferences: FetchMoreProps;
|
fetchMoreReferences: FetchMoreProps;
|
||||||
|
@ -191,6 +194,7 @@ function useProductVariantUpdateForm(
|
||||||
weight: variant?.weight?.value.toString() || "",
|
weight: variant?.weight?.value.toString() || "",
|
||||||
quantityLimitPerCustomer: variant?.quantityLimitPerCustomer || null,
|
quantityLimitPerCustomer: variant?.quantityLimitPerCustomer || null,
|
||||||
name: variant?.name ?? "",
|
name: variant?.name ?? "",
|
||||||
|
media: variant?.media?.map(({ id }) => id) || [],
|
||||||
};
|
};
|
||||||
|
|
||||||
const form = useForm(initial, undefined, {
|
const form = useForm(initial, undefined, {
|
||||||
|
@ -300,6 +304,8 @@ function useProductVariantUpdateForm(
|
||||||
intl.formatMessage(errorMessages.preorderEndDateInFutureErrorText),
|
intl.formatMessage(errorMessages.preorderEndDateInFutureErrorText),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const handleMediaChange = createMediaChangeHandler(form, triggerChange);
|
||||||
|
|
||||||
const handleUpdateChannels = (selectedIds: string[]) => {
|
const handleUpdateChannels = (selectedIds: string[]) => {
|
||||||
const allChannels = variant.product.channelListings.map(listing => {
|
const allChannels = variant.product.channelListings.map(listing => {
|
||||||
const variantChannel = variant?.channelListings?.find(
|
const variantChannel = variant?.channelListings?.find(
|
||||||
|
@ -428,6 +434,7 @@ function useProductVariantUpdateForm(
|
||||||
changeMetadata,
|
changeMetadata,
|
||||||
changeStock: handleStockChange,
|
changeStock: handleStockChange,
|
||||||
changePreorderEndDate: handlePreorderEndDateChange,
|
changePreorderEndDate: handlePreorderEndDateChange,
|
||||||
|
changeMedia: handleMediaChange,
|
||||||
deleteStock: handleStockDelete,
|
deleteStock: handleStockDelete,
|
||||||
fetchMoreReferences: handleFetchMoreReferences,
|
fetchMoreReferences: handleFetchMoreReferences,
|
||||||
fetchReferences: handleFetchReferences,
|
fetchReferences: handleFetchReferences,
|
||||||
|
|
78
src/products/utils/data.test.ts
Normal file
78
src/products/utils/data.test.ts
Normal file
|
@ -0,0 +1,78 @@
|
||||||
|
import { getSelectedMedia } from "./data";
|
||||||
|
|
||||||
|
type GetSelectedMediaParams = Parameters<typeof getSelectedMedia>;
|
||||||
|
|
||||||
|
describe("Product media utils", () => {
|
||||||
|
it("should return selected media in proper order when media ids passed", () => {
|
||||||
|
// Arrange
|
||||||
|
const media: GetSelectedMediaParams[0] = [
|
||||||
|
{
|
||||||
|
id: "1",
|
||||||
|
sortOrder: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "2",
|
||||||
|
sortOrder: 3,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "3",
|
||||||
|
sortOrder: 4,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "4",
|
||||||
|
sortOrder: 1,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const selectedIds: GetSelectedMediaParams[1] = ["1", "3", "4"];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = getSelectedMedia(media, selectedIds);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
const expectedResult = [
|
||||||
|
{
|
||||||
|
id: "4",
|
||||||
|
sortOrder: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "1",
|
||||||
|
sortOrder: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "3",
|
||||||
|
sortOrder: 4,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
expect(result).toEqual(expectedResult);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return empty array of media when no media ids passed", () => {
|
||||||
|
// Arrange
|
||||||
|
const media: GetSelectedMediaParams[0] = [
|
||||||
|
{
|
||||||
|
id: "1",
|
||||||
|
sortOrder: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "2",
|
||||||
|
sortOrder: 3,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "3",
|
||||||
|
sortOrder: 4,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "4",
|
||||||
|
sortOrder: 1,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const selectedIds: GetSelectedMediaParams[1] = [];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = getSelectedMedia(media, selectedIds);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
|
@ -11,6 +11,7 @@ import { SingleAutocompleteChoiceType } from "@saleor/components/SingleAutocompl
|
||||||
import {
|
import {
|
||||||
ProductDetailsVariantFragment,
|
ProductDetailsVariantFragment,
|
||||||
ProductFragment,
|
ProductFragment,
|
||||||
|
ProductMediaFragment,
|
||||||
ProductTypeQuery,
|
ProductTypeQuery,
|
||||||
ProductVariantCreateDataQuery,
|
ProductVariantCreateDataQuery,
|
||||||
ProductVariantFragment,
|
ProductVariantFragment,
|
||||||
|
@ -247,3 +248,13 @@ export const getPreorderEndDateFormData = (endDate?: string) =>
|
||||||
|
|
||||||
export const getPreorderEndHourFormData = (endDate?: string) =>
|
export const getPreorderEndHourFormData = (endDate?: string) =>
|
||||||
endDate ? moment(endDate).format("HH:mm") : "";
|
endDate ? moment(endDate).format("HH:mm") : "";
|
||||||
|
|
||||||
|
export const getSelectedMedia = <
|
||||||
|
T extends Pick<ProductMediaFragment, "id" | "sortOrder">
|
||||||
|
>(
|
||||||
|
media: T[] = [],
|
||||||
|
selectedMediaIds: string[],
|
||||||
|
) =>
|
||||||
|
media
|
||||||
|
.filter(image => selectedMediaIds.indexOf(image.id) !== -1)
|
||||||
|
.sort((prev, next) => (prev.sortOrder > next.sortOrder ? 1 : -1));
|
||||||
|
|
163
src/products/utils/handlers.test.ts
Normal file
163
src/products/utils/handlers.test.ts
Normal file
|
@ -0,0 +1,163 @@
|
||||||
|
import { ProductMediaType } from "@saleor/graphql";
|
||||||
|
|
||||||
|
import { handleAssignMedia } from "./handlers";
|
||||||
|
|
||||||
|
type HandleAssignMediaParams = Parameters<typeof handleAssignMedia>;
|
||||||
|
|
||||||
|
describe("Product handlers", () => {
|
||||||
|
it("should not alter product variant media when the same selected media ids as previously passed", async () => {
|
||||||
|
// Arrange
|
||||||
|
const media: HandleAssignMediaParams[0] = ["1", "2"];
|
||||||
|
const variant: HandleAssignMediaParams[1] = {
|
||||||
|
id: "1",
|
||||||
|
media: [
|
||||||
|
{
|
||||||
|
id: "1",
|
||||||
|
url: "",
|
||||||
|
type: ProductMediaType.IMAGE,
|
||||||
|
oembedData: null,
|
||||||
|
__typename: "ProductMedia",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "2",
|
||||||
|
url: "",
|
||||||
|
type: ProductMediaType.IMAGE,
|
||||||
|
oembedData: null,
|
||||||
|
__typename: "ProductMedia",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
const assignMedia = jest.fn(() => Promise.resolve({}));
|
||||||
|
const unassignMedia = jest.fn(() => Promise.resolve({}));
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await handleAssignMedia(media, variant, assignMedia, unassignMedia);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(assignMedia).not.toHaveBeenCalled();
|
||||||
|
expect(unassignMedia).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should assign media to product variant when more then all previous selected media ids passed", async () => {
|
||||||
|
// Arrange
|
||||||
|
const media: HandleAssignMediaParams[0] = ["1", "2", "3"];
|
||||||
|
const variant: HandleAssignMediaParams[1] = {
|
||||||
|
id: "1",
|
||||||
|
media: [
|
||||||
|
{
|
||||||
|
id: "3",
|
||||||
|
url: "",
|
||||||
|
type: ProductMediaType.IMAGE,
|
||||||
|
oembedData: null,
|
||||||
|
__typename: "ProductMedia",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
const assignMedia = jest.fn(() => Promise.resolve({}));
|
||||||
|
const unassignMedia = jest.fn(() => Promise.resolve({}));
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await handleAssignMedia(media, variant, assignMedia, unassignMedia);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(assignMedia).toHaveBeenCalledTimes(2);
|
||||||
|
expect(assignMedia).toHaveBeenCalledWith({
|
||||||
|
variantId: "1",
|
||||||
|
mediaId: "1",
|
||||||
|
});
|
||||||
|
expect(assignMedia).toHaveBeenCalledWith({
|
||||||
|
variantId: "1",
|
||||||
|
mediaId: "2",
|
||||||
|
});
|
||||||
|
expect(unassignMedia).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should unassign media from product variant when not all previous selected media ids passed", async () => {
|
||||||
|
// Arrange
|
||||||
|
const media: HandleAssignMediaParams[0] = ["3"];
|
||||||
|
const variant: HandleAssignMediaParams[1] = {
|
||||||
|
id: "1",
|
||||||
|
media: [
|
||||||
|
{
|
||||||
|
id: "1",
|
||||||
|
url: "",
|
||||||
|
type: ProductMediaType.IMAGE,
|
||||||
|
oembedData: null,
|
||||||
|
__typename: "ProductMedia",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "2",
|
||||||
|
url: "",
|
||||||
|
type: ProductMediaType.IMAGE,
|
||||||
|
oembedData: null,
|
||||||
|
__typename: "ProductMedia",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "3",
|
||||||
|
url: "",
|
||||||
|
type: ProductMediaType.IMAGE,
|
||||||
|
oembedData: null,
|
||||||
|
__typename: "ProductMedia",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
const assignMedia = jest.fn(() => Promise.resolve({}));
|
||||||
|
const unassignMedia = jest.fn(() => Promise.resolve({}));
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await handleAssignMedia(media, variant, assignMedia, unassignMedia);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(assignMedia).not.toHaveBeenCalled();
|
||||||
|
expect(unassignMedia).toHaveBeenCalledTimes(2);
|
||||||
|
expect(unassignMedia).toHaveBeenCalledWith({
|
||||||
|
variantId: "1",
|
||||||
|
mediaId: "1",
|
||||||
|
});
|
||||||
|
expect(unassignMedia).toHaveBeenCalledWith({
|
||||||
|
variantId: "1",
|
||||||
|
mediaId: "2",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should assign and unassign media from product variant when not all but more selected media ids from previously selected passed", async () => {
|
||||||
|
// Arrange
|
||||||
|
const media: HandleAssignMediaParams[0] = ["1", "3"];
|
||||||
|
const variant: HandleAssignMediaParams[1] = {
|
||||||
|
id: "1",
|
||||||
|
media: [
|
||||||
|
{
|
||||||
|
id: "1",
|
||||||
|
url: "",
|
||||||
|
type: ProductMediaType.IMAGE,
|
||||||
|
oembedData: null,
|
||||||
|
__typename: "ProductMedia",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "2",
|
||||||
|
url: "",
|
||||||
|
type: ProductMediaType.IMAGE,
|
||||||
|
oembedData: null,
|
||||||
|
__typename: "ProductMedia",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
const assignMedia = jest.fn(() => Promise.resolve({}));
|
||||||
|
const unassignMedia = jest.fn(() => Promise.resolve({}));
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await handleAssignMedia(media, variant, assignMedia, unassignMedia);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(assignMedia).toHaveBeenCalledTimes(1);
|
||||||
|
expect(assignMedia).toHaveBeenCalledWith({
|
||||||
|
variantId: "1",
|
||||||
|
mediaId: "3",
|
||||||
|
});
|
||||||
|
expect(unassignMedia).toHaveBeenCalledTimes(1);
|
||||||
|
expect(unassignMedia).toHaveBeenCalledWith({
|
||||||
|
variantId: "1",
|
||||||
|
mediaId: "2",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -1,11 +1,20 @@
|
||||||
|
import { FetchResult } from "@apollo/client";
|
||||||
import {
|
import {
|
||||||
ChannelData,
|
ChannelData,
|
||||||
ChannelPriceAndPreorderData,
|
ChannelPriceAndPreorderData,
|
||||||
ChannelPriceArgs,
|
ChannelPriceArgs,
|
||||||
ChannelPriceData,
|
ChannelPriceData,
|
||||||
} from "@saleor/channels/utils";
|
} from "@saleor/channels/utils";
|
||||||
import { ProductChannelListingAddInput } from "@saleor/graphql";
|
import {
|
||||||
|
ProductChannelListingAddInput,
|
||||||
|
ProductVariantFragment,
|
||||||
|
VariantMediaAssignMutation,
|
||||||
|
VariantMediaAssignMutationVariables,
|
||||||
|
VariantMediaUnassignMutation,
|
||||||
|
VariantMediaUnassignMutationVariables,
|
||||||
|
} from "@saleor/graphql";
|
||||||
import { FormChange, UseFormResult } from "@saleor/hooks/useForm";
|
import { FormChange, UseFormResult } from "@saleor/hooks/useForm";
|
||||||
|
import { diff } from "fast-array-diff";
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
|
|
||||||
export function createChannelsPriceChangeHandler(
|
export function createChannelsPriceChangeHandler(
|
||||||
|
@ -144,3 +153,69 @@ export const createPreorderEndDateChangeHandler = (
|
||||||
}
|
}
|
||||||
triggerChange();
|
triggerChange();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const createMediaChangeHandler = (
|
||||||
|
form: UseFormResult<{ media: string[] }>,
|
||||||
|
triggerChange: () => void,
|
||||||
|
) => (ids: string[]) => {
|
||||||
|
form.change({
|
||||||
|
target: {
|
||||||
|
name: "media",
|
||||||
|
value: ids,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
triggerChange();
|
||||||
|
};
|
||||||
|
|
||||||
|
export const handleAssignMedia = async <
|
||||||
|
T extends Pick<ProductVariantFragment, "id" | "media">
|
||||||
|
>(
|
||||||
|
media: string[],
|
||||||
|
variant: T,
|
||||||
|
assignMedia: (
|
||||||
|
variables: VariantMediaAssignMutationVariables,
|
||||||
|
) => Promise<FetchResult<VariantMediaAssignMutation>>,
|
||||||
|
unassignMedia: (
|
||||||
|
variables: VariantMediaUnassignMutationVariables,
|
||||||
|
) => Promise<FetchResult<VariantMediaUnassignMutation>>,
|
||||||
|
) => {
|
||||||
|
const { added, removed } = diff(
|
||||||
|
variant.media.map(mediaObj => mediaObj.id),
|
||||||
|
media,
|
||||||
|
);
|
||||||
|
|
||||||
|
const assignResults = await Promise.all(
|
||||||
|
added.map(mediaId =>
|
||||||
|
assignMedia({
|
||||||
|
mediaId,
|
||||||
|
variantId: variant.id,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
const unassignResults = await Promise.all(
|
||||||
|
removed.map(mediaId =>
|
||||||
|
unassignMedia({
|
||||||
|
mediaId,
|
||||||
|
variantId: variant.id,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
const assignErrors = assignResults.reduce(
|
||||||
|
(errors, result) => [
|
||||||
|
...errors,
|
||||||
|
...(result.data?.variantMediaAssign.errors || []),
|
||||||
|
],
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
const unassignErrors = unassignResults.reduce(
|
||||||
|
(errors, result) => [
|
||||||
|
...errors,
|
||||||
|
...(result.data?.variantMediaUnassign.errors || []),
|
||||||
|
],
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
return [...assignErrors, ...unassignErrors];
|
||||||
|
};
|
||||||
|
|
|
@ -36,6 +36,7 @@ import useShop from "@saleor/hooks/useShop";
|
||||||
import { commonMessages } from "@saleor/intl";
|
import { commonMessages } from "@saleor/intl";
|
||||||
import { weight } from "@saleor/misc";
|
import { weight } from "@saleor/misc";
|
||||||
import { getAttributeInputFromVariant } from "@saleor/products/utils/data";
|
import { getAttributeInputFromVariant } from "@saleor/products/utils/data";
|
||||||
|
import { handleAssignMedia } from "@saleor/products/utils/handlers";
|
||||||
import usePageSearch from "@saleor/searches/usePageSearch";
|
import usePageSearch from "@saleor/searches/usePageSearch";
|
||||||
import useProductSearch from "@saleor/searches/useProductSearch";
|
import useProductSearch from "@saleor/searches/useProductSearch";
|
||||||
import useAttributeValueSearchHandler from "@saleor/utils/handlers/attributeValueSearchHandler";
|
import useAttributeValueSearchHandler from "@saleor/utils/handlers/attributeValueSearchHandler";
|
||||||
|
@ -181,26 +182,6 @@ export const ProductVariant: React.FC<ProductUpdateProps> = ({
|
||||||
reorderProductVariantsOpts.loading ||
|
reorderProductVariantsOpts.loading ||
|
||||||
deleteAttributeValueOpts.loading;
|
deleteAttributeValueOpts.loading;
|
||||||
|
|
||||||
const handleMediaSelect = (id: string) => () => {
|
|
||||||
if (variant) {
|
|
||||||
if (variant?.media?.map(media_obj => media_obj.id).indexOf(id) !== -1) {
|
|
||||||
unassignMedia({
|
|
||||||
variables: {
|
|
||||||
mediaId: id,
|
|
||||||
variantId: variant.id,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
assignMedia({
|
|
||||||
variables: {
|
|
||||||
mediaId: id,
|
|
||||||
variantId: variant.id,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleUpdate = async (data: ProductVariantUpdateSubmitData) => {
|
const handleUpdate = async (data: ProductVariantUpdateSubmitData) => {
|
||||||
const uploadFilesResult = await handleUploadMultipleFiles(
|
const uploadFilesResult = await handleUploadMultipleFiles(
|
||||||
data.attributesWithNewFileValue,
|
data.attributesWithNewFileValue,
|
||||||
|
@ -218,6 +199,13 @@ export const ProductVariant: React.FC<ProductUpdateProps> = ({
|
||||||
uploadFilesResult,
|
uploadFilesResult,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const assignMediaErrors = await handleAssignMedia(
|
||||||
|
data.media,
|
||||||
|
variant,
|
||||||
|
variables => assignMedia({ variables }),
|
||||||
|
variables => unassignMedia({ variables }),
|
||||||
|
);
|
||||||
|
|
||||||
const result = await updateVariant({
|
const result = await updateVariant({
|
||||||
variables: {
|
variables: {
|
||||||
addStocks: data.addStocks.map(mapFormsetStockToStockInput),
|
addStocks: data.addStocks.map(mapFormsetStockToStockInput),
|
||||||
|
@ -255,6 +243,7 @@ export const ProductVariant: React.FC<ProductUpdateProps> = ({
|
||||||
...result.data?.productVariantStocksDelete.errors,
|
...result.data?.productVariantStocksDelete.errors,
|
||||||
...result.data?.productVariantStocksUpdate.errors,
|
...result.data?.productVariantStocksUpdate.errors,
|
||||||
...result.data?.productVariantUpdate.errors,
|
...result.data?.productVariantUpdate.errors,
|
||||||
|
...assignMediaErrors,
|
||||||
...channelErrors,
|
...channelErrors,
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
@ -340,7 +329,6 @@ export const ProductVariant: React.FC<ProductUpdateProps> = ({
|
||||||
header={variant?.name || variant?.sku}
|
header={variant?.name || variant?.sku}
|
||||||
warehouses={mapEdgesToItems(warehouses?.data?.warehouses) || []}
|
warehouses={mapEdgesToItems(warehouses?.data?.warehouses) || []}
|
||||||
onDelete={() => openModal("remove")}
|
onDelete={() => openModal("remove")}
|
||||||
onMediaSelect={handleMediaSelect}
|
|
||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
onWarehouseConfigure={() => navigate(warehouseAddPath)}
|
onWarehouseConfigure={() => navigate(warehouseAddPath)}
|
||||||
onVariantPreorderDeactivate={handleDeactivateVariantPreorder}
|
onVariantPreorderDeactivate={handleDeactivateVariantPreorder}
|
||||||
|
|
|
@ -19,7 +19,7 @@ storiesOf("Products / ProductVariantImageSelectDialog", module)
|
||||||
media={variantProductImages}
|
media={variantProductImages}
|
||||||
selectedMedia={variantImages.map(image => image.id)}
|
selectedMedia={variantImages.map(image => image.id)}
|
||||||
onClose={() => undefined}
|
onClose={() => undefined}
|
||||||
onMediaSelect={() => undefined}
|
onConfirm={() => undefined}
|
||||||
open={true}
|
open={true}
|
||||||
/>
|
/>
|
||||||
));
|
));
|
||||||
|
|
|
@ -25,7 +25,6 @@ storiesOf("Views / Products / Product variant details", module)
|
||||||
variant={variant}
|
variant={variant}
|
||||||
onDelete={undefined}
|
onDelete={undefined}
|
||||||
onSetDefaultVariant={() => undefined}
|
onSetDefaultVariant={() => undefined}
|
||||||
onMediaSelect={() => undefined}
|
|
||||||
onSubmit={() => undefined}
|
onSubmit={() => undefined}
|
||||||
onVariantReorder={() => undefined}
|
onVariantReorder={() => undefined}
|
||||||
saveButtonBarState="default"
|
saveButtonBarState="default"
|
||||||
|
@ -54,7 +53,6 @@ storiesOf("Views / Products / Product variant details", module)
|
||||||
placeholderImage={placeholderImage}
|
placeholderImage={placeholderImage}
|
||||||
onDelete={undefined}
|
onDelete={undefined}
|
||||||
onSetDefaultVariant={() => undefined}
|
onSetDefaultVariant={() => undefined}
|
||||||
onMediaSelect={() => undefined}
|
|
||||||
onSubmit={() => undefined}
|
onSubmit={() => undefined}
|
||||||
onVariantReorder={() => undefined}
|
onVariantReorder={() => undefined}
|
||||||
saveButtonBarState="default"
|
saveButtonBarState="default"
|
||||||
|
@ -82,7 +80,6 @@ storiesOf("Views / Products / Product variant details", module)
|
||||||
variant={variant}
|
variant={variant}
|
||||||
onDelete={undefined}
|
onDelete={undefined}
|
||||||
onSetDefaultVariant={() => undefined}
|
onSetDefaultVariant={() => undefined}
|
||||||
onMediaSelect={() => undefined}
|
|
||||||
onSubmit={() => undefined}
|
onSubmit={() => undefined}
|
||||||
onVariantReorder={() => undefined}
|
onVariantReorder={() => undefined}
|
||||||
saveButtonBarState="default"
|
saveButtonBarState="default"
|
||||||
|
@ -108,7 +105,6 @@ storiesOf("Views / Products / Product variant details", module)
|
||||||
variant={variant}
|
variant={variant}
|
||||||
onDelete={undefined}
|
onDelete={undefined}
|
||||||
onSetDefaultVariant={() => undefined}
|
onSetDefaultVariant={() => undefined}
|
||||||
onMediaSelect={() => undefined}
|
|
||||||
onSubmit={() => undefined}
|
onSubmit={() => undefined}
|
||||||
onVariantReorder={() => undefined}
|
onVariantReorder={() => undefined}
|
||||||
saveButtonBarState="default"
|
saveButtonBarState="default"
|
||||||
|
|
Loading…
Reference in a new issue