diff --git a/.changeset/four-lions-happen.md b/.changeset/four-lions-happen.md new file mode 100644 index 00000000000..ef5cad476b6 --- /dev/null +++ b/.changeset/four-lions-happen.md @@ -0,0 +1,5 @@ +--- +"saleor-dashboard": patch +--- + +Now you can re-order products within the collection. diff --git a/locale/defaultMessages.json b/locale/defaultMessages.json index 1f9035102e6..7671ec86b1c 100644 --- a/locale/defaultMessages.json +++ b/locale/defaultMessages.json @@ -5488,6 +5488,9 @@ "context": "card update success alert title", "string": "Successfully updated card balance" }, + "XAvER/": { + "string": "Product reordered" + }, "XB2Jj9": { "context": "create app button", "string": "Create App" @@ -7897,6 +7900,9 @@ "context": "label", "string": "External app" }, + "nABmvC": { + "string": "No. of rows" + }, "nBzIBG": { "string": "Delete permission group" }, diff --git a/package-lock.json b/package-lock.json index 11c69bd7d6a..1ddd10ef40e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "license": "BSD-3-Clause", "dependencies": { "@apollo/client": "3.4.17", - "@dnd-kit/core": "^6.0.8", + "@dnd-kit/core": "^6.3.1", "@dnd-kit/sortable": "^7.0.2", "@dnd-kit/utilities": "^3.2.1", "@editorjs/editorjs": "^2.30.7", @@ -1668,9 +1668,9 @@ } }, "node_modules/@dnd-kit/accessibility": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.0.1.tgz", - "integrity": "sha512-HXRrwS9YUYQO9lFRc/49uO/VICbM+O+ZRpFDe9Pd1rwVv2PCNkRiTZRdxrDgng/UkvdC3Re9r2vwPpXXrWeFzg==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz", + "integrity": "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==", "dependencies": { "tslib": "^2.0.0" }, @@ -1679,12 +1679,12 @@ } }, "node_modules/@dnd-kit/core": { - "version": "6.0.8", - "resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.0.8.tgz", - "integrity": "sha512-lYaoP8yHTQSLlZe6Rr9qogouGUz9oRUj4AHhDQGQzq/hqaJRpFo65X+JKsdHf8oUFBzx5A+SJPUvxAwTF2OabA==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz", + "integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==", "dependencies": { - "@dnd-kit/accessibility": "^3.0.0", - "@dnd-kit/utilities": "^3.2.1", + "@dnd-kit/accessibility": "^3.1.1", + "@dnd-kit/utilities": "^3.2.2", "tslib": "^2.0.0" }, "peerDependencies": { @@ -1706,9 +1706,9 @@ } }, "node_modules/@dnd-kit/utilities": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.1.tgz", - "integrity": "sha512-OOXqISfvBw/1REtkSK2N3Fi2EQiLMlWUlqnOK/UpOISqBZPWpE6TqL+jcPtMOkE8TqYGiURvRdPSI9hltNUjEA==", + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.2.tgz", + "integrity": "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==", "dependencies": { "tslib": "^2.0.0" }, @@ -22466,20 +22466,20 @@ } }, "@dnd-kit/accessibility": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.0.1.tgz", - "integrity": "sha512-HXRrwS9YUYQO9lFRc/49uO/VICbM+O+ZRpFDe9Pd1rwVv2PCNkRiTZRdxrDgng/UkvdC3Re9r2vwPpXXrWeFzg==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz", + "integrity": "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==", "requires": { "tslib": "^2.0.0" } }, "@dnd-kit/core": { - "version": "6.0.8", - "resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.0.8.tgz", - "integrity": "sha512-lYaoP8yHTQSLlZe6Rr9qogouGUz9oRUj4AHhDQGQzq/hqaJRpFo65X+JKsdHf8oUFBzx5A+SJPUvxAwTF2OabA==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz", + "integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==", "requires": { - "@dnd-kit/accessibility": "^3.0.0", - "@dnd-kit/utilities": "^3.2.1", + "@dnd-kit/accessibility": "^3.1.1", + "@dnd-kit/utilities": "^3.2.2", "tslib": "^2.0.0" } }, @@ -22493,9 +22493,9 @@ } }, "@dnd-kit/utilities": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.1.tgz", - "integrity": "sha512-OOXqISfvBw/1REtkSK2N3Fi2EQiLMlWUlqnOK/UpOISqBZPWpE6TqL+jcPtMOkE8TqYGiURvRdPSI9hltNUjEA==", + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.2.tgz", + "integrity": "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==", "requires": { "tslib": "^2.0.0" } diff --git a/package.json b/package.json index 4d77f5bf6ad..740243a3126 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ }, "dependencies": { "@apollo/client": "3.4.17", - "@dnd-kit/core": "^6.0.8", + "@dnd-kit/core": "^6.3.1", "@dnd-kit/sortable": "^7.0.2", "@dnd-kit/utilities": "^3.2.1", "@editorjs/editorjs": "^2.30.7", diff --git a/src/collections/components/CollectionDetailsPage/CollectionDetailsPage.tsx b/src/collections/components/CollectionDetailsPage/CollectionDetailsPage.tsx index d7476afd192..99899edc695 100644 --- a/src/collections/components/CollectionDetailsPage/CollectionDetailsPage.tsx +++ b/src/collections/components/CollectionDetailsPage/CollectionDetailsPage.tsx @@ -1,8 +1,7 @@ // @ts-strict-ignore import { ChannelCollectionData } from "@dashboard/channels/utils"; -import { collectionListPath } from "@dashboard/collections/urls"; +import { collectionListPath, CollectionUrlQueryParams } from "@dashboard/collections/urls"; import { TopNav } from "@dashboard/components/AppLayout/TopNav"; -import { CardSpacer } from "@dashboard/components/CardSpacer"; import ChannelsAvailabilityCard from "@dashboard/components/ChannelsAvailabilityCard"; import { ConfirmButtonTransitionState } from "@dashboard/components/ConfirmButton"; import { DetailPageLayout } from "@dashboard/components/Layouts"; @@ -21,14 +20,13 @@ import useNavigator from "@dashboard/hooks/useNavigator"; import React from "react"; import { useIntl } from "react-intl"; -import { ChannelProps, ListActions, PageListProps } from "../../../types"; +import { ChannelProps, PageListProps } from "../../../types"; import CollectionDetails from "../CollectionDetails/CollectionDetails"; import { CollectionImage } from "../CollectionImage/CollectionImage"; import CollectionProducts from "../CollectionProducts/CollectionProducts"; import CollectionUpdateForm, { CollectionUpdateData } from "./form"; -export interface CollectionDetailsPageProps extends PageListProps, ListActions, ChannelProps { - onAdd: () => void; +export interface CollectionDetailsPageProps extends PageListProps, ChannelProps { channelsCount: number; channelsErrors: CollectionChannelListingErrorFragment[]; collection: CollectionDetailsQuery["collection"]; @@ -38,10 +36,10 @@ export interface CollectionDetailsPageProps extends PageListProps, ListActions, onCollectionRemove: () => void; onImageDelete: () => void; onImageUpload: (file: File) => void; - onProductUnassign: (id: string, event: React.MouseEvent) => void; onSubmit: (data: CollectionUpdateData) => SubmitPromise; onChannelsChange: (data: ChannelCollectionData[]) => void; openChannelsModal: () => void; + params: CollectionUrlQueryParams; } const CollectionDetailsPage: React.FC = ({ @@ -80,7 +78,6 @@ const CollectionDetailsPage: React.FC = ({ - = ({ onImageUpload={onImageUpload} onChange={change} /> - - - ({ - colActions: { - width: `calc(76px + ${theme.spacing(1)})`, - marginRight: theme.spacing(-2), - }, - colName: { - paddingLeft: 0, - width: "auto", - }, - colNameLabel: { - marginLeft: AVATAR_MARGIN, - }, - colPublished: { - width: 200, - }, - colType: { - width: 200, - }, - table: { - tableLayout: "fixed", - }, - tableRow: { - cursor: "pointer", - }, - }), - { name: "CollectionProducts" }, -); - -export interface CollectionProductsProps extends PageListProps, ListActions { +export interface CollectionProductsProps { collection: CollectionDetailsQuery["collection"]; - onProductUnassign: (id: string, event: React.MouseEvent) => void; - onAdd: () => void; + params: CollectionUrlQueryParams; + currentChannels: ChannelCollectionData[]; + disabled: boolean; } -const CollectionProducts: React.FC = props => { - const { - collection, - disabled, - onAdd, - onProductUnassign, - isChecked, - selected, - toggle, - toggleAll, - toolbar, - } = props; - const classes = useStyles(props); +const CollectionProducts: React.FC = ({ + collection, + params, + currentChannels, + disabled, +}) => { + const navigate = useNavigator(); + const [openModal, closeModal] = createDialogActionHandlers< + CollectionUrlDialog, + CollectionUrlQueryParams + >(navigate, params => collectionUrl(id, params), params); + const { isSelected, listElements, reset, toggle, toggleAll } = useBulkActions(params.ids); + const intl = useIntl(); - const products = mapEdgesToItems(collection?.products); + const id = useCollectionId(); + const { settings, updateListSettings } = useListSettings(ListViews.COLLECTION_PRODUCTS_LIST); + const numberOfRows = settings ? settings.rowNumber : PAGINATE_BY; + const [paginationState, setPaginationState] = useLocalPaginationState(numberOfRows); + const notify = useNotifier(); + + const [assignProduct, assignProductOpts] = useCollectionAssignProductMutation({ + onCompleted: data => { + if (data.collectionAddProducts?.errors.length === 0) { + notify({ + status: "success", + text: intl.formatMessage({ + id: "56vUeQ", + defaultMessage: "Added product to collection", + }), + }); + } + }, + }); + const [unassignProduct, unassignProductOpts] = useUnassignCollectionProductMutation({ + onCompleted: data => { + if (data.collectionRemoveProducts?.errors.length === 0) { + notify({ + status: "success", + text: intl.formatMessage({ + id: "WW+Ruy", + defaultMessage: "Deleted product from collection", + }), + }); + reset(); + closeModal(); + } + }, + }); + + const { data } = useCollectionProductsQuery({ + displayLoader: true, + variables: { id, ...paginationState }, + }); + + const products = mapEdgesToItems(data?.collection?.products); const numberOfColumns = products?.length === 0 ? 4 : 5; + const paginate = useLocalPaginator(setPaginationState); + + const { pageInfo, ...paginationValues } = paginate( + data?.collection?.products?.pageInfo, + paginationState, + ); + + const { search, loadMore, result } = useProductSearch({ + variables: DEFAULT_INITIAL_SEARCH_DATA, + }); + + const assignedProductDict = getAssignedProductIdsToCollection(collection, result.data?.search); + + const handleProductUnassign = async (productId: string) => { + await unassignProduct({ + variables: { + collectionId: id, + productIds: [productId], + ...paginationState, + }, + }); + await result.refetch(DEFAULT_INITIAL_SEARCH_DATA); + }; + + const handleAssignationChange = async (products: Container[]) => { + const productIds = products.map(product => product.id); + const toUnassignIds = Object.keys(assignedProductDict).filter( + s => assignedProductDict[s] && !productIds.includes(s), + ); + const baseVariables = { ...paginationState, collectionId: id }; + + if (productIds.length > 0) { + await assignProduct({ + variables: { ...baseVariables, productIds }, + }); + } + + if (toUnassignIds.length > 0) { + await unassignProduct({ + variables: { ...baseVariables, productIds: toUnassignIds }, + }); + } + + closeModal(); + + await result.refetch(DEFAULT_INITIAL_SEARCH_DATA); + }; return ( - - - - {collection ? ( - intl.formatMessage( - { - id: "/dnWE8", - defaultMessage: "Products in {name}", - description: "products in collection", - }, - { - name: collection?.name ?? "...", - }, - ) - ) : ( - - )} - - - + + + + {products ? ( + - - - - - - - - - - - - - - - - - - - - - - - - - - {renderCollection( - products, - product => { - const isSelected = product ? isChecked(product.id) : false; - - return ( - - - product?.id && toggle(product.id)} - /> - - - {product?.name ?? } - - - {product?.productType?.name ?? } - - - {product && !product?.channelListings?.length ? ( - "-" - ) : product?.channelListings !== undefined ? ( - - ) : ( - - )} - - - -