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 { 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 }), {});
|
||||||
|
};
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -13320,6 +13320,9 @@ export const SearchProductsDocument = gql`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
collections {
|
||||||
|
id
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pageInfo {
|
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<{
|
export type SearchProductTypesQueryVariables = Exact<{
|
||||||
after?: InputMaybe<Scalars['String']>;
|
after?: InputMaybe<Scalars['String']>;
|
||||||
|
|
|
@ -33,6 +33,9 @@ export const searchProducts = gql`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
collections {
|
||||||
|
id
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pageInfo {
|
pageInfo {
|
||||||
|
|
|
@ -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",
|
||||||
|
|
Loading…
Reference in a new issue