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

View file

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

View file

@ -13320,6 +13320,9 @@ export const SearchProductsDocument = gql`
} }
} }
} }
collections {
id
}
} }
} }
pageInfo { 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<{ export type SearchProductTypesQueryVariables = Exact<{
after?: InputMaybe<Scalars['String']>; after?: InputMaybe<Scalars['String']>;

View file

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

View file

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