From 5eb0be2dc3e364f5a9f2264875b3dfb59f57631c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Chy=C5=82a?= Date: Wed, 10 May 2023 14:09:52 +0200 Subject: [PATCH] Product list bulk delete (#3562) * Datagrid column checkboxes with delete action * Handle product bulk delete * Fix border line between columns * Clear selection after product delete mutation * Handle row selection * Improve handleRowDelete, use clearRowSelection * Fix restart row selection * Cleanup * Fix for delete modal * Fix showing delete modal * Add delete button next to switcher * Extract messages * Fix checking rows in product list datagrid * Check console * Check on row click * Fix fire click when selection checkbox * Simplifie vertiacal border condition * Clear selected product when pagination * Fix issue with columns and row selections * Fix hiding tooltip when modal close * Update storybook props fix accordion use * Fix row selection * Fix clear selection after pagination change * Apply cr changes --- locale/defaultMessages.json | 3 + package-lock.json | 14 +- package.json | 2 +- src/components/Attributes/Attributes.tsx | 8 +- .../ChannelAvailabilityItemWrapper.tsx | 8 +- src/components/Datagrid/Datagrid.tsx | 34 ++++- src/components/Datagrid/utils.ts | 7 + .../TablePaginationWithContext.tsx | 8 +- .../ProductListDatagrid.tsx | 24 +-- .../hooks/useDatagridColumns.ts | 6 +- .../components/ProductListDatagrid/utils.ts | 9 +- .../ProductListDeleteButton.test.tsx | 41 +++++ .../ProductListDeleteButton.tsx | 34 +++++ .../ProductListDeleteButton/index.ts | 1 + .../ProductListPage.stories.tsx | 4 + .../ProductListPage/ProductListPage.tsx | 31 +++- .../views/ProductList/ProductList.tsx | 142 +++++++++++------- 17 files changed, 259 insertions(+), 117 deletions(-) create mode 100644 src/products/components/ProductListDeleteButton/ProductListDeleteButton.test.tsx create mode 100644 src/products/components/ProductListDeleteButton/ProductListDeleteButton.tsx create mode 100644 src/products/components/ProductListDeleteButton/index.ts diff --git a/locale/defaultMessages.json b/locale/defaultMessages.json index 7fef4cc59..df6ed48de 100644 --- a/locale/defaultMessages.json +++ b/locale/defaultMessages.json @@ -6483,6 +6483,9 @@ "context": "VariantDetailsChannelsAvailabilityCard no items available", "string": "This variant is not available at any of the channels" }, + "jrBxCQ": { + "string": "Bulk product delete" + }, "jswILH": { "context": "add attribute as column in product list table", "string": "Add to Column Options" diff --git a/package-lock.json b/package-lock.json index 7d35d3b09..b52b24c91 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,7 +27,7 @@ "@material-ui/lab": "^4.0.0-alpha.61", "@material-ui/styles": "^4.11.4", "@reach/auto-id": "^0.16.0", - "@saleor/macaw-ui": "0.8.0-pre.73", + "@saleor/macaw-ui": "0.8.0-pre.81", "@saleor/sdk": "^0.5.0", "@sentry/react": "^6.0.0", "@types/faker": "^5.1.6", @@ -7976,9 +7976,9 @@ } }, "node_modules/@saleor/macaw-ui": { - "version": "0.8.0-pre.73", - "resolved": "https://registry.npmjs.org/@saleor/macaw-ui/-/macaw-ui-0.8.0-pre.73.tgz", - "integrity": "sha512-7+bFDATTV8ZjX3dpU20z5QMUMSZHoNgidLWEzUBkHWk6EgiDf+V5Mn2t8Eexd34uLOIKCQ0j4f/mVG+slRsj/w==", + "version": "0.8.0-pre.81", + "resolved": "https://registry.npmjs.org/@saleor/macaw-ui/-/macaw-ui-0.8.0-pre.81.tgz", + "integrity": "sha512-6s15zUyn82492DwMDNm0yI6uAzik8t/OlI+LrH23L3EgzcyzprbA7DYzwcV4C7vlCEiMxywLnfS/0b1RYnhM1w==", "dependencies": { "@dessert-box/react": "^0.4.0", "@floating-ui/react-dom-interactions": "^0.5.0", @@ -43372,9 +43372,9 @@ } }, "@saleor/macaw-ui": { - "version": "0.8.0-pre.73", - "resolved": "https://registry.npmjs.org/@saleor/macaw-ui/-/macaw-ui-0.8.0-pre.73.tgz", - "integrity": "sha512-7+bFDATTV8ZjX3dpU20z5QMUMSZHoNgidLWEzUBkHWk6EgiDf+V5Mn2t8Eexd34uLOIKCQ0j4f/mVG+slRsj/w==", + "version": "0.8.0-pre.81", + "resolved": "https://registry.npmjs.org/@saleor/macaw-ui/-/macaw-ui-0.8.0-pre.81.tgz", + "integrity": "sha512-6s15zUyn82492DwMDNm0yI6uAzik8t/OlI+LrH23L3EgzcyzprbA7DYzwcV4C7vlCEiMxywLnfS/0b1RYnhM1w==", "requires": { "@dessert-box/react": "^0.4.0", "@floating-ui/react-dom-interactions": "^0.5.0", diff --git a/package.json b/package.json index c29ab1f54..a7b1b1d19 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,7 @@ "@material-ui/lab": "^4.0.0-alpha.61", "@material-ui/styles": "^4.11.4", "@reach/auto-id": "^0.16.0", - "@saleor/macaw-ui": "0.8.0-pre.73", + "@saleor/macaw-ui": "0.8.0-pre.81", "@saleor/sdk": "^0.5.0", "@sentry/react": "^6.0.0", "@types/faker": "^5.1.6", diff --git a/src/components/Attributes/Attributes.tsx b/src/components/Attributes/Attributes.tsx index b30abcc8e..a4b0babb8 100644 --- a/src/components/Attributes/Attributes.tsx +++ b/src/components/Attributes/Attributes.tsx @@ -79,7 +79,7 @@ export const Attributes: React.FC = ({ - + = ({ }} /> - - + + {attributes.length > 0 && (
    @@ -108,7 +108,7 @@ export const Attributes: React.FC = ({ ))}
)} -
+
diff --git a/src/components/ChannelsAvailabilityCard/Channel/ChannelAvailabilityItemWrapper.tsx b/src/components/ChannelsAvailabilityCard/Channel/ChannelAvailabilityItemWrapper.tsx index fa8b27526..9ba9f7ac5 100644 --- a/src/components/ChannelsAvailabilityCard/Channel/ChannelAvailabilityItemWrapper.tsx +++ b/src/components/ChannelsAvailabilityCard/Channel/ChannelAvailabilityItemWrapper.tsx @@ -16,13 +16,11 @@ export const ChannelAvailabilityItemWrapper: React.FC< > = ({ data: { name }, messages, children }) => ( - + {name} - - {children} - + + {children} ); diff --git a/src/components/Datagrid/Datagrid.tsx b/src/components/Datagrid/Datagrid.tsx index 0158e9308..0d616423b 100644 --- a/src/components/Datagrid/Datagrid.tsx +++ b/src/components/Datagrid/Datagrid.tsx @@ -25,6 +25,7 @@ import React, { ReactElement, ReactNode, useCallback, + useEffect, useMemo, useRef, useState, @@ -54,7 +55,10 @@ import useStyles, { useFullScreenStyles, } from "./styles"; import { AvailableColumn } from "./types"; -import { getDefultColumnPickerProps } from "./utils"; +import { + getDefultColumnPickerProps, + preventRowClickOnSelectionCheckbox, +} from "./utils"; export interface GetCellContentOpts { changes: MutableRefObject; @@ -91,6 +95,7 @@ export interface DatagridProps { onRowClick?: (item: Item) => void; onColumnMoved?: (startIndex: number, endIndex: number) => void; onColumnResize?: (column: GridColumn, newSize: number) => void; + onRowSelectionChange?: (rowsId: number[], clearSelection: () => void) => void; readonly?: boolean; hasRowHover?: boolean; rowMarkers?: DataEditorProps["rowMarkers"]; @@ -126,6 +131,7 @@ export const Datagrid: React.FC = ({ loading, rowAnchor, hasRowHover = false, + onRowSelectionChange, ...datagridProps }): ReactElement => { const classes = useStyles(); @@ -151,6 +157,16 @@ export const Datagrid: React.FC = ({ const [selection, setSelection] = useState(); const [hoverRow, setHoverRow] = useState(undefined); + // Allow to listen to which row is selected and notfiy parent component + useEffect(() => { + if (onRowSelectionChange && selection) { + // Second parameter is callback to clear selection from parent component + onRowSelectionChange(Array.from(selection.rows), () => { + setSelection(undefined); + }); + } + }, [onRowSelectionChange, selection]); + usePortalClasses({ className: classes.portal }); usePreventHistoryBack(scroller); @@ -242,6 +258,10 @@ export const Datagrid: React.FC = ({ return; } + if (preventRowClickOnSelectionCheckbox(rowMarkers, args.location[0])) { + return; + } + hackARef.current.style.left = `${window.scrollX + args.bounds.x}px`; hackARef.current.style.width = `${args.bounds.width}px`; hackARef.current.style.top = `${window.scrollY + args.bounds.y}px`; @@ -250,20 +270,26 @@ export const Datagrid: React.FC = ({ getAppMountUri() + (href.startsWith("/") ? href.slice(1) : href); hackARef.current.dataset.reactRouterPath = href; }, - [hasRowHover, rowAnchor], + [hasRowHover, rowAnchor, rowMarkers], ); const handleCellClick = useCallback( (item: Item, args: CellClickedEventArgs) => { - if (onRowClick && item[0] !== -1) { + if (preventRowClickOnSelectionCheckbox(rowMarkers, item[0])) { + return; + } + + if (onRowClick) { onRowClick(item); } + handleRowHover(args); + if (hackARef.current) { hackARef.current.click(); } }, - [onRowClick, handleRowHover], + [rowMarkers, onRowClick, handleRowHover], ); const handleGridSelectionChange = (gridSelection: GridSelection) => { diff --git a/src/components/Datagrid/utils.ts b/src/components/Datagrid/utils.ts index b7e8f2f71..bcb0295d9 100644 --- a/src/components/Datagrid/utils.ts +++ b/src/components/Datagrid/utils.ts @@ -1,3 +1,5 @@ +import { DataEditorProps } from "@glideapps/glide-data-grid"; + import { ColumnPickerProps } from "../ColumnPicker"; export const getDefultColumnPickerProps = ( @@ -9,3 +11,8 @@ export const getDefultColumnPickerProps = ( hoverOutline: false, }, }); + +export const preventRowClickOnSelectionCheckbox = ( + rowMarkers: DataEditorProps["rowMarkers"], + location: number, +) => !["number", "none"].includes(rowMarkers) && location === -1; diff --git a/src/components/TablePagination/TablePaginationWithContext.tsx b/src/components/TablePagination/TablePaginationWithContext.tsx index 32e472b8f..1568cc0c0 100644 --- a/src/components/TablePagination/TablePaginationWithContext.tsx +++ b/src/components/TablePagination/TablePaginationWithContext.tsx @@ -15,12 +15,8 @@ export type TablePaginationWithContextProps = Omit< export const TablePaginationWithContext = ( props: TablePaginationWithContextProps, ) => { - const { - hasNextPage, - hasPreviousPage, - paginatorType, - ...paginationProps - } = usePaginatorContext(); + const { hasNextPage, hasPreviousPage, paginatorType, ...paginationProps } = + usePaginatorContext(); if (paginatorType === "click") { const { loadNextPage, loadPreviousPage } = paginationProps; diff --git a/src/products/components/ProductListDatagrid/ProductListDatagrid.tsx b/src/products/components/ProductListDatagrid/ProductListDatagrid.tsx index bf8339b9d..6e34c723f 100644 --- a/src/products/components/ProductListDatagrid/ProductListDatagrid.tsx +++ b/src/products/components/ProductListDatagrid/ProductListDatagrid.tsx @@ -56,6 +56,7 @@ interface ProductListDatagridProps SearchAvailableInGridAttributesQuery["availableInGrid"] >; onColumnQueryChange: (query: string) => void; + onSelectProductIds: (rowsIndex: number[], clearSelection: () => void) => void; isAttributeLoading?: boolean; hasRowHover?: boolean; } @@ -80,6 +81,7 @@ export const ProductListDatagrid: React.FC = ({ onColumnQueryChange, activeAttributeSortId, filterDependency, + onSelectProductIds, hasRowHover, rowAnchor, }) => { @@ -103,22 +105,6 @@ export const ProductListDatagrid: React.FC = ({ const handleColumnMoved = useCallback( (startIndex: number, endIndex: number): void => { - // Keep empty column always at beginning - if (startIndex === 0) { - return setColumns(prevColumns => [...prevColumns]); - } - - // Keep empty column always at beginning - if (endIndex === 0) { - return setColumns(old => - addAtIndex( - old[startIndex], - removeAtIndex(old, startIndex), - endIndex + 1, - ), - ); - } - setColumns(old => addAtIndex(old[startIndex], removeAtIndex(old, startIndex), endIndex), ); @@ -243,13 +229,12 @@ export const ProductListDatagrid: React.FC = ({ (col > 1 ? true : false)} + verticalBorder={col => col > 0} getColumnTooltipContent={handleGetColumnTooltipContent} availableColumns={columns} onHeaderClicked={handleHeaderClicked} @@ -258,6 +243,7 @@ export const ProductListDatagrid: React.FC = ({ getCellError={() => false} menuItems={() => []} rows={productsLength} + onRowSelectionChange={onSelectProductIds} selectionActions={() => null} fullScreenTitle={intl.formatMessage(messages.products)} onRowClick={handleRowClick} diff --git a/src/products/components/ProductListDatagrid/hooks/useDatagridColumns.ts b/src/products/components/ProductListDatagrid/hooks/useDatagridColumns.ts index 4b92c292c..85eb8b9aa 100644 --- a/src/products/components/ProductListDatagrid/hooks/useDatagridColumns.ts +++ b/src/products/components/ProductListDatagrid/hooks/useDatagridColumns.ts @@ -1,4 +1,3 @@ -import { useEmptyColumn } from "@dashboard/components/Datagrid/hooks/useEmptyColumn"; import { AvailableColumn } from "@dashboard/components/Datagrid/types"; import { ProductListColumns } from "@dashboard/config"; import { GridAttributesQuery } from "@dashboard/graphql"; @@ -26,7 +25,6 @@ export const useDatagridColumns = ({ settings, }: UseDatagridColumnsProps) => { const intl = useIntl(); - const emptyColumn = useEmptyColumn(); const initialColumns = useRef( getColumns({ @@ -35,13 +33,11 @@ export const useDatagridColumns = ({ gridAttributes, gridAttributesFromSettings, activeAttributeSortId, - emptyColumn, }), ); const [columns, setColumns] = useState([ initialColumns.current[0], - initialColumns.current[1], ...initialColumns.current.filter(col => settings.columns.includes(col.id as ProductListColumns), ), @@ -86,7 +82,7 @@ function byColumnsInSettingsOrStaticColumns( ) { return (column: AvailableColumn) => settings.columns.includes(column.id as ProductListColumns) || - ["empty", "name"].includes(column.id); + ["name"].includes(column.id); } function toCurrentColumnData( diff --git a/src/products/components/ProductListDatagrid/utils.ts b/src/products/components/ProductListDatagrid/utils.ts index 46c5e7e77..f30ca61f3 100644 --- a/src/products/components/ProductListDatagrid/utils.ts +++ b/src/products/components/ProductListDatagrid/utils.ts @@ -17,7 +17,7 @@ import { getMoneyRange } from "@dashboard/components/MoneyRange"; import { ProductListColumns } from "@dashboard/config"; import { GridAttributesQuery, ProductListQuery } from "@dashboard/graphql"; import { commonMessages } from "@dashboard/intl"; -import { getDatagridRowDataIndex, isFirstColumn } from "@dashboard/misc"; +import { getDatagridRowDataIndex } from "@dashboard/misc"; import { ProductListUrlSortField } from "@dashboard/products/urls"; import { RelayToFlat, Sort } from "@dashboard/types"; import { getColumnSortDirectionIcon } from "@dashboard/utils/columns/getColumnSortDirectionIcon"; @@ -34,7 +34,6 @@ interface GetColumnsProps { gridAttributes: RelayToFlat; gridAttributesFromSettings: ProductListColumns[]; activeAttributeSortId: string; - emptyColumn: AvailableColumn; } export function getColumns({ @@ -43,10 +42,8 @@ export function getColumns({ gridAttributes, gridAttributesFromSettings, activeAttributeSortId, - emptyColumn, }: GetColumnsProps): AvailableColumn[] { return [ - emptyColumn, { id: "name", title: intl.formatMessage(commonMessages.product), @@ -149,10 +146,6 @@ export function createGetCellContent({ [column, row]: Item, { changes, getChangeIndex, added, removed }: GetCellContentOpts, ) => { - if (isFirstColumn(column)) { - return readonlyTextCell(""); - } - const columnId = columns[column]?.id; if (!columnId) { diff --git a/src/products/components/ProductListDeleteButton/ProductListDeleteButton.test.tsx b/src/products/components/ProductListDeleteButton/ProductListDeleteButton.test.tsx new file mode 100644 index 000000000..d2855f63a --- /dev/null +++ b/src/products/components/ProductListDeleteButton/ProductListDeleteButton.test.tsx @@ -0,0 +1,41 @@ +import { render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import React from "react"; + +import { ProductListDeleteButton } from "./ProductListDeleteButton"; + +jest.mock("react-intl", () => ({ + FormattedMessage: ({ defaultMessage }) => <>{defaultMessage}, +})); + +describe("ProductListDeleteButton", () => { + it("should return null when show is equal false", () => { + // Arrange & Act + const { container } = render( + , + ); + + // Assert + expect(container).toBeEmptyDOMElement(); + }); + + it("should render button", async () => { + // Arrange & Act + render(); + + // Assert + expect(screen.getByRole("button")).toBeInTheDocument(); + }); + + it("should fire callback on click", async () => { + // Arrange + const onClick = jest.fn(); + + // Act + render(); + await userEvent.click(screen.getByRole("button")); + + // Assert + expect(onClick).toHaveBeenCalled(); + }); +}); diff --git a/src/products/components/ProductListDeleteButton/ProductListDeleteButton.tsx b/src/products/components/ProductListDeleteButton/ProductListDeleteButton.tsx new file mode 100644 index 000000000..c78bc66d9 --- /dev/null +++ b/src/products/components/ProductListDeleteButton/ProductListDeleteButton.tsx @@ -0,0 +1,34 @@ +import { Button, Tooltip, TrashBinIcon } from "@saleor/macaw-ui/next"; +import React, { forwardRef } from "react"; +import { FormattedMessage } from "react-intl"; + +interface ProductListDeleteButtonProps { + onClick: () => void; + show?: boolean; +} + +export const ProductListDeleteButton = forwardRef< + HTMLButtonElement, + ProductListDeleteButtonProps +>(({ onClick, show = false }, ref) => { + if (!show) { + return null; + } + + return ( + + +