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:
parent
20920f3cf2
commit
5dbd6fed8a
7 changed files with 102 additions and 43 deletions
|
@ -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 }), {});
|
||||
};
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -13320,6 +13320,9 @@ export const SearchProductsDocument = gql`
|
|||
}
|
||||
}
|
||||
}
|
||||
collections {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
pageInfo {
|
||||
|
|
|
@ -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']>;
|
||||
|
|
|
@ -33,6 +33,9 @@ export const searchProducts = gql`
|
|||
}
|
||||
}
|
||||
}
|
||||
collections {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
pageInfo {
|
||||
|
|
|
@ -1874,6 +1874,7 @@ export const products: RelayToFlat<SearchProductsQuery["search"]> = [
|
|||
__typename: "Image",
|
||||
url: "",
|
||||
},
|
||||
collections: [{ __typename: "Collection", id: "Q29sbGVjdGlvbjo0" }],
|
||||
variants: [
|
||||
{
|
||||
__typename: "ProductVariant",
|
||||
|
|
Loading…
Reference in a new issue