From 5dbd6fed8a49a83c3fa56d5614fae3877248ebb3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Dro=C5=84?= Date: Wed, 21 Sep 2022 15:35:05 +0200 Subject: [PATCH] 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 --- src/collections/utils.ts | 15 ++++ src/collections/views/CollectionDetails.tsx | 46 +++++++++--- .../AssignProductDialog.tsx | 75 +++++++++++-------- src/graphql/hooks.generated.ts | 3 + src/graphql/types.generated.ts | 2 +- src/searches/useProductSearch.ts | 3 + src/shipping/fixtures.ts | 1 + 7 files changed, 102 insertions(+), 43 deletions(-) diff --git a/src/collections/utils.ts b/src/collections/utils.ts index ac0337e45..788f412f6 100644 --- a/src/collections/utils.ts +++ b/src/collections/utils.ts @@ -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 }), {}); +}; diff --git a/src/collections/views/CollectionDetails.tsx b/src/collections/views/CollectionDetails.tsx index 279430a3b..2919744f1 100644 --- a/src/collections/views/CollectionDetails.tsx +++ b/src/collections/views/CollectionDetails.tsx @@ -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 = ({ 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 = ({ return ; } + const assignedProductDict = getAssignedProductIdsToCollection( + collection, + result.data?.search, + ); + return ( @@ -304,15 +332,17 @@ export const CollectionDetails: React.FC = ({ }) } 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 = ({ onChannelsChange={setCurrentChannels} /> = ({ 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, )} /> + ; + selectedIds?: Record; 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 = props => { @@ -68,15 +55,50 @@ const AssignProductDialog: React.FC = 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([]); + 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 ( = props => { {products && products.map(product => { - const isSelected = selectedProducts.some( - selectedProduct => selectedProduct === product.id, - ); + const isSelected = productsDict[product.id] || false; return ( = props => { > - handleProductAssign( - product.id, - isSelected, - selectedProducts, - setSelectedProducts, - ) - } + onChange={() => handleChange(product.id)} /> diff --git a/src/graphql/hooks.generated.ts b/src/graphql/hooks.generated.ts index 3b6b9e951..910260b87 100644 --- a/src/graphql/hooks.generated.ts +++ b/src/graphql/hooks.generated.ts @@ -13320,6 +13320,9 @@ export const SearchProductsDocument = gql` } } } + collections { + id + } } } pageInfo { diff --git a/src/graphql/types.generated.ts b/src/graphql/types.generated.ts index d95471439..2402dd15b 100644 --- a/src/graphql/types.generated.ts +++ b/src/graphql/types.generated.ts @@ -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; diff --git a/src/searches/useProductSearch.ts b/src/searches/useProductSearch.ts index d9a3515f0..79652c2c7 100644 --- a/src/searches/useProductSearch.ts +++ b/src/searches/useProductSearch.ts @@ -33,6 +33,9 @@ export const searchProducts = gql` } } } + collections { + id + } } } pageInfo { diff --git a/src/shipping/fixtures.ts b/src/shipping/fixtures.ts index f682695d7..59340889b 100644 --- a/src/shipping/fixtures.ts +++ b/src/shipping/fixtures.ts @@ -1874,6 +1874,7 @@ export const products: RelayToFlat = [ __typename: "Image", url: "", }, + collections: [{ __typename: "Collection", id: "Q29sbGVjdGlvbjo0" }], variants: [ { __typename: "ProductVariant",