Fix assigning products to collection (#2320)

* Reset modal state in collection details

* cover initial state

* modal updates

* cover case when there is no items to add

Co-authored-by: andrzejewsky <vox3r69@gmail.com>
This commit is contained in:
Michał Droń 2022-09-21 15:35:05 +02:00 committed by GitHub
parent 20920f3cf2
commit 5dbd6fed8a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 102 additions and 43 deletions

View file

@ -1,4 +1,5 @@
import { ChannelCollectionData } from "@saleor/channels/utils";
import { CollectionDetailsQuery, SearchProductsQuery } from "@saleor/graphql";
export const createChannelsChangeHandler = (
channelListings: ChannelCollectionData[],
@ -19,3 +20,17 @@ export const createChannelsChangeHandler = (
updateChannels(updatedChannels);
triggerChange();
};
export const getAssignedProductIdsToCollection = (
collection: CollectionDetailsQuery["collection"],
queryData: SearchProductsQuery["search"],
) => {
if (!queryData || !collection) {
return {};
}
return queryData.edges
.filter(e => e.node.collections.some(s => collection.id === s.id))
.map(e => ({ [e.node.id]: true }))
.reduce((p, c) => ({ ...p, ...c }), {});
};

View file

@ -51,6 +51,7 @@ import {
CollectionUrlDialog,
CollectionUrlQueryParams,
} from "../urls";
import { getAssignedProductIdsToCollection } from "../utils";
import { COLLECTION_DETAILS_FORM_ID } from "./consts";
interface CollectionDetailsProps {
@ -247,6 +248,28 @@ export const CollectionDetails: React.FC<CollectionDetailsProps> = ({
variables => updatePrivateMetadata({ variables }),
);
const handleAssignationChange = async products => {
const toUnassignIds = Object.keys(assignedProductDict).filter(
s => assignedProductDict[s] && !products.includes(s),
);
const baseVariables = { ...paginationState, collectionId: id };
if (products.length > 0) {
await assignProduct({
variables: { ...baseVariables, productIds: products },
});
}
if (toUnassignIds.length > 0) {
await unassignProduct({
variables: { ...baseVariables, productIds: toUnassignIds },
});
}
await result.refetch(DEFAULT_INITIAL_SEARCH_DATA);
};
const formTransitionState = getMutationState(
updateCollectionOpts.called,
updateCollectionOpts.loading,
@ -262,6 +285,11 @@ export const CollectionDetails: React.FC<CollectionDetailsProps> = ({
return <NotFoundPage backHref={collectionListUrl()} />;
}
const assignedProductDict = getAssignedProductIdsToCollection(
collection,
result.data?.search,
);
return (
<PaginatorContext.Provider value={{ ...pageInfo, ...paginationValues }}>
<WindowTitle title={data?.collection?.name} />
@ -304,15 +332,17 @@ export const CollectionDetails: React.FC<CollectionDetailsProps> = ({
})
}
onSubmit={handleSubmit}
onProductUnassign={(productId, event) => {
onProductUnassign={async (productId, event) => {
event.stopPropagation();
unassignProduct({
await unassignProduct({
variables: {
collectionId: id,
productIds: [productId],
...paginationState,
},
});
await result.refetch(DEFAULT_INITIAL_SEARCH_DATA);
}}
saveButtonBarState={formTransitionState}
toolbar={
@ -341,6 +371,7 @@ export const CollectionDetails: React.FC<CollectionDetailsProps> = ({
onChannelsChange={setCurrentChannels}
/>
<AssignProductDialog
selectedIds={assignedProductDict}
confirmButtonState={assignProductOpts.status}
hasMore={result.data?.search?.pageInfo.hasNextPage}
open={params.action === "assign"}
@ -348,19 +379,12 @@ export const CollectionDetails: React.FC<CollectionDetailsProps> = ({
onFetchMore={loadMore}
loading={result.loading}
onClose={closeModal}
onSubmit={products =>
assignProduct({
variables: {
...paginationState,
collectionId: id,
productIds: products,
},
})
}
onSubmit={handleAssignationChange}
products={mapEdgesToItems(result?.data?.search)?.filter(
suggestedProduct => suggestedProduct.id,
)}
/>
<ActionDialog
confirmButtonState={removeCollectionOpts.status}
onClose={closeModal}

View file

@ -13,12 +13,13 @@ import ConfirmButton from "@saleor/components/ConfirmButton";
import ResponsiveTable from "@saleor/components/ResponsiveTable";
import TableCellAvatar from "@saleor/components/TableCellAvatar";
import { SearchProductsQuery } from "@saleor/graphql";
import useModalDialogOpen from "@saleor/hooks/useModalDialogOpen";
import useSearchQuery from "@saleor/hooks/useSearchQuery";
import { ConfirmButtonTransitionState } from "@saleor/macaw-ui";
import { maybe } from "@saleor/misc";
import useScrollableDialogStyle from "@saleor/styles/useScrollableDialogStyle";
import { DialogProps, FetchMoreProps, RelayToFlat } from "@saleor/types";
import React from "react";
import React, { useEffect } from "react";
import InfiniteScroll from "react-infinite-scroll-component";
import { FormattedMessage, useIntl } from "react-intl";
@ -35,26 +36,12 @@ export interface AssignProductDialogFormData {
export interface AssignProductDialogProps extends FetchMoreProps, DialogProps {
confirmButtonState: ConfirmButtonTransitionState;
products: RelayToFlat<SearchProductsQuery["search"]>;
selectedIds?: Record<string, boolean>;
loading: boolean;
onFetch: (value: string) => void;
onSubmit: (data: string[]) => void;
}
function handleProductAssign(
productID: string,
isSelected: boolean,
selectedProducts: string[],
setSelectedProducts: (data: string[]) => void,
) {
if (isSelected) {
setSelectedProducts(
selectedProducts.filter(selectedProduct => selectedProduct !== productID),
);
} else {
setSelectedProducts([...selectedProducts, productID]);
}
}
const scrollableTargetId = "assignProductScrollableDialog";
const AssignProductDialog: React.FC<AssignProductDialogProps> = props => {
@ -68,15 +55,50 @@ const AssignProductDialog: React.FC<AssignProductDialogProps> = props => {
onFetch,
onFetchMore,
onSubmit,
selectedIds,
} = props;
const classes = useStyles(props);
const scrollableDialogClasses = useScrollableDialogStyle({});
const intl = useIntl();
const [query, onQueryChange] = useSearchQuery(onFetch);
const [selectedProducts, setSelectedProducts] = React.useState<string[]>([]);
const [query, onQueryChange, queryReset] = useSearchQuery(onFetch);
const [productsDict, setProductsDict] = React.useState(selectedIds || {});
const handleSubmit = () => onSubmit(selectedProducts);
useEffect(() => {
if (selectedIds) {
setProductsDict(prev => {
const prevIds = Object.keys(prev);
const newIds = Object.keys(selectedIds);
const preSelected = newIds
.filter(n => !prevIds.includes(n))
.reduce((p, c) => ({ ...p, [c]: true }), {});
return { ...prev, ...preSelected };
});
}
}, [selectedIds]);
useModalDialogOpen(open, {
onOpen: () => {
queryReset();
setProductsDict(selectedIds);
},
});
const handleSubmit = () => {
const selectedProductsAsArray = Object.keys(productsDict)
.filter(key => productsDict[key])
.map(key => key);
onSubmit(selectedProductsAsArray);
};
const handleChange = productId => {
setProductsDict(prev => ({
...prev,
[productId]: !prev[productId] ?? true,
}));
};
return (
<Dialog
@ -123,9 +145,7 @@ const AssignProductDialog: React.FC<AssignProductDialogProps> = props => {
<TableBody>
{products &&
products.map(product => {
const isSelected = selectedProducts.some(
selectedProduct => selectedProduct === product.id,
);
const isSelected = productsDict[product.id] || false;
return (
<TableRow
@ -145,14 +165,7 @@ const AssignProductDialog: React.FC<AssignProductDialogProps> = props => {
>
<Checkbox
checked={isSelected}
onChange={() =>
handleProductAssign(
product.id,
isSelected,
selectedProducts,
setSelectedProducts,
)
}
onChange={() => handleChange(product.id)}
/>
</TableCell>
</TableRow>

View file

@ -13320,6 +13320,9 @@ export const SearchProductsDocument = gql`
}
}
}
collections {
id
}
}
}
pageInfo {

View file

@ -8076,7 +8076,7 @@ export type SearchProductsQueryVariables = Exact<{
}>;
export type SearchProductsQuery = { __typename: 'Query', search: { __typename: 'ProductCountableConnection', edges: Array<{ __typename: 'ProductCountableEdge', node: { __typename: 'Product', id: string, name: string, thumbnail: { __typename: 'Image', url: string } | null, variants: Array<{ __typename: 'ProductVariant', id: string, name: string, sku: string | null, channelListings: Array<{ __typename: 'ProductVariantChannelListing', channel: { __typename: 'Channel', id: string, isActive: boolean, name: string, currencyCode: string }, price: { __typename: 'Money', amount: number, currency: string } | null }> | null }> | null } }>, pageInfo: { __typename: 'PageInfo', endCursor: string | null, hasNextPage: boolean, hasPreviousPage: boolean, startCursor: string | null } } | null };
export type SearchProductsQuery = { __typename: 'Query', search: { __typename: 'ProductCountableConnection', edges: Array<{ __typename: 'ProductCountableEdge', node: { __typename: 'Product', id: string, name: string, thumbnail: { __typename: 'Image', url: string } | null, variants: Array<{ __typename: 'ProductVariant', id: string, name: string, sku: string | null, channelListings: Array<{ __typename: 'ProductVariantChannelListing', channel: { __typename: 'Channel', id: string, isActive: boolean, name: string, currencyCode: string }, price: { __typename: 'Money', amount: number, currency: string } | null }> | null }> | null, collections: Array<{ __typename: 'Collection', id: string }> | null } }>, pageInfo: { __typename: 'PageInfo', endCursor: string | null, hasNextPage: boolean, hasPreviousPage: boolean, startCursor: string | null } } | null };
export type SearchProductTypesQueryVariables = Exact<{
after?: InputMaybe<Scalars['String']>;

View file

@ -33,6 +33,9 @@ export const searchProducts = gql`
}
}
}
collections {
id
}
}
}
pageInfo {

View file

@ -1874,6 +1874,7 @@ export const products: RelayToFlat<SearchProductsQuery["search"]> = [
__typename: "Image",
url: "",
},
collections: [{ __typename: "Collection", id: "Q29sbGVjdGlvbjo0" }],
variants: [
{
__typename: "ProductVariant",