From b4f11eff663336407bee80dc2dc50c6121dbb6bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Chy=C5=82a?= Date: Tue, 4 Jul 2023 09:23:27 +0200 Subject: [PATCH] Category listing datagrid (#3760) Co-authored-by: wojteknowacki --- .changeset/strange-carrots-juggle.md | 5 + cypress/e2e/catalog/categories.js | 107 +++++---- .../catalog/categories/category-details.js | 1 + cypress/elements/shared/sharedElements.js | 2 + .../support/pages/catalog/categoriesPage.js | 9 +- locale/defaultMessages.json | 46 ++-- .../CategoryDeleteButton.tsx | 38 +++ .../components/CategoryDeleteButton/index.ts | 1 + .../components/CategoryList/CategoryList.tsx | 218 ------------------ .../components/CategoryList/index.ts | 2 - .../CategoryListDatagrid.tsx | 129 +++++++++++ .../CategoryListDatagrid/datagrid.ts | 67 ++++++ .../components/CategoryListDatagrid/index.ts | 1 + .../CategoryListDatagrid/messages.ts | 23 ++ .../CategoryListPage.stories.tsx | 6 + .../CategoryListPage/CategoryListPage.tsx | 133 ++++++----- .../components/CategoryListPage/messages.ts | 21 ++ .../CategoryProductList.tsx | 156 ------------- .../components/CategoryProductList/index.ts | 2 - .../CategoryProductListDatagrid.tsx | 87 +++++++ .../CategoryProductListDatagrid/datagrid.ts | 45 ++++ .../CategoryProductListDatagrid/index.ts | 1 + .../CategoryProductListDatagrid/messages.ts | 8 + .../CategoryProducts/CategoryProducts.tsx | 136 ++++++----- .../components/CategoryProducts/index.ts | 2 +- .../components/CategoryProducts/styles.ts | 10 - .../CategorySubcategories.tsx | 65 ++++++ .../components/CategorySubcategories/index.ts | 1 + .../CategoryUpdatePage.stories.tsx | 6 +- .../CategoryUpdatePage/CategoryUpdatePage.tsx | 105 ++++----- src/categories/views/CategoryDetails.tsx | 145 +++++++----- .../views/CategoryList/CategoryList.tsx | 183 ++++++++------- src/categories/views/CategoryList/filter.ts | 17 +- src/components/Datagrid/Datagrid.tsx | 24 +- .../Datagrid/components/RowActions.tsx | 2 +- src/components/Datagrid/styles.ts | 4 +- .../FilterPresetsSelect.tsx | 1 + 37 files changed, 997 insertions(+), 812 deletions(-) create mode 100644 .changeset/strange-carrots-juggle.md create mode 100644 src/categories/components/CategoryDeleteButton/CategoryDeleteButton.tsx create mode 100644 src/categories/components/CategoryDeleteButton/index.ts delete mode 100644 src/categories/components/CategoryList/CategoryList.tsx delete mode 100644 src/categories/components/CategoryList/index.ts create mode 100644 src/categories/components/CategoryListDatagrid/CategoryListDatagrid.tsx create mode 100644 src/categories/components/CategoryListDatagrid/datagrid.ts create mode 100644 src/categories/components/CategoryListDatagrid/index.ts create mode 100644 src/categories/components/CategoryListDatagrid/messages.ts create mode 100644 src/categories/components/CategoryListPage/messages.ts delete mode 100644 src/categories/components/CategoryProductList/CategoryProductList.tsx delete mode 100644 src/categories/components/CategoryProductList/index.ts create mode 100644 src/categories/components/CategoryProductListDatagrid/CategoryProductListDatagrid.tsx create mode 100644 src/categories/components/CategoryProductListDatagrid/datagrid.ts create mode 100644 src/categories/components/CategoryProductListDatagrid/index.ts create mode 100644 src/categories/components/CategoryProductListDatagrid/messages.ts delete mode 100644 src/categories/components/CategoryProducts/styles.ts create mode 100644 src/categories/components/CategorySubcategories/CategorySubcategories.tsx create mode 100644 src/categories/components/CategorySubcategories/index.ts diff --git a/.changeset/strange-carrots-juggle.md b/.changeset/strange-carrots-juggle.md new file mode 100644 index 000000000..ddb0ce24c --- /dev/null +++ b/.changeset/strange-carrots-juggle.md @@ -0,0 +1,5 @@ +--- +"saleor-dashboard": minor +--- + +Introduce datagrid on category listing page diff --git a/cypress/e2e/catalog/categories.js b/cypress/e2e/catalog/categories.js index e1fd0cc47..13af27142 100644 --- a/cypress/e2e/catalog/categories.js +++ b/cypress/e2e/catalog/categories.js @@ -3,10 +3,7 @@ import faker from "faker"; -import { - CATEGORIES_LIST_SELECTORS, - categoryRow, -} from "../../elements/catalog/categories/categories-list"; +import { CATEGORIES_LIST_SELECTORS } from "../../elements/catalog/categories/categories-list"; import { CATEGORY_DETAILS_SELECTORS } from "../../elements/catalog/categories/category-details"; import { BUTTON_SELECTORS } from "../../elements/shared/button-selectors"; import { SHARED_ELEMENTS } from "../../elements/shared/sharedElements"; @@ -17,6 +14,7 @@ import { } from "../../support/api/requests/Category"; import * as channelsUtils from "../../support/api/utils/channelsUtils"; import * as productsUtils from "../../support/api/utils/products/productsUtils"; +import { ensureCanvasStatic } from "../../support/customCommands/sharedElementsOperations/canvas"; import { createCategory, updateCategory, @@ -109,9 +107,13 @@ describe("As an admin I want to manage categories", () => { .click(); createCategory({ name: categoryName, description: categoryName }) .visit(categoryDetailsUrl(category.id)) - .contains(CATEGORY_DETAILS_SELECTORS.categoryChildrenRow, categoryName) - .scrollIntoView() - .should("be.visible"); + .get(SHARED_ELEMENTS.dataGridTable) + .scrollIntoView(); + ensureCanvasStatic(SHARED_ELEMENTS.dataGridTable); + + cy.contains(SHARED_ELEMENTS.dataGridTable, categoryName).should( + "be.visible", + ); getCategory(category.id).then(categoryResp => { expect(categoryResp.children.edges[0].node.name).to.eq(categoryName); }); @@ -136,19 +138,22 @@ describe("As an admin I want to manage categories", () => { "should be able to remove product from category. TC: SALEOR_0204", { tags: ["@category", "@allEnv", "@stable"] }, () => { + cy.addAliasToGraphRequest("productBulkDelete"); cy.visit(categoryDetailsUrl(category.id)) .get(CATEGORY_DETAILS_SELECTORS.productsTab) .click(); - cy.contains(CATEGORY_DETAILS_SELECTORS.productRow, product.name) - .find(BUTTON_SELECTORS.checkbox) + ensureCanvasStatic(SHARED_ELEMENTS.dataGridTable); + cy.contains(SHARED_ELEMENTS.dataGridTable, product.name).should( + "be.visible", + ); + // selects first row + cy.clickGridCell(0, 0); + cy.get(CATEGORY_DETAILS_SELECTORS.deleteCategoriesButton) .click() - .get(BUTTON_SELECTORS.deleteIcon) - .click() - .addAliasToGraphRequest("productBulkDelete") .get(BUTTON_SELECTORS.submit) .click() .confirmationMessageShouldDisappear(); - cy.contains(CATEGORY_DETAILS_SELECTORS.productRow, product.name) + cy.contains(SHARED_ELEMENTS.dataGridTable, product.name) .should("not.exist") .waitForRequestAndCheckIfNoErrors("@productBulkDelete"); getCategory(category.id).then(categoryResp => { @@ -164,7 +169,12 @@ describe("As an admin I want to manage categories", () => { cy.visit(urlList.categories) .get(SHARED_ELEMENTS.searchInput) .type(category.name); - cy.contains(SHARED_ELEMENTS.tableRow, category.name).click(); + ensureCanvasStatic(SHARED_ELEMENTS.dataGridTable); + cy.contains(SHARED_ELEMENTS.dataGridTable, category.name).should( + "be.visible", + ); + // opens first row details + cy.clickGridCell(1, 0); cy.contains(SHARED_ELEMENTS.header, category.name).should("be.visible"); }, ); @@ -174,6 +184,7 @@ describe("As an admin I want to manage categories", () => { { tags: ["@category", "@allEnv", "@stable"] }, () => { const categoryName = `${startsWith}${faker.datatype.number()}`; + cy.addAliasToGraphRequest("CategoryDelete"); createCategoryRequest({ name: categoryName, @@ -181,7 +192,6 @@ describe("As an admin I want to manage categories", () => { cy.visit(categoryDetailsUrl(categoryResp.id)) .get(BUTTON_SELECTORS.deleteButton) .click() - .addAliasToGraphRequest("CategoryDelete") .get(BUTTON_SELECTORS.submit) .click() .waitForRequestAndCheckIfNoErrors("@CategoryDelete"); @@ -222,35 +232,43 @@ describe("As an admin I want to manage categories", () => { () => { const firstCategoryName = `${startsWith}${faker.datatype.number()}`; const secondCategoryName = `${startsWith}${faker.datatype.number()}`; - let firstCategory; - let secondCategory; + cy.addAliasToGraphRequest("CategoryBulkDelete"); createCategoryRequest({ name: firstCategoryName, - }).then(categoryResp => { - firstCategory = categoryResp; }); - createCategoryRequest({ name: secondCategoryName, - }).then(categoryResp => { - secondCategory = categoryResp; - cy.visit(urlList.categories) - .searchInTable(startsWith) - .get(categoryRow(firstCategory.id)) - .find(BUTTON_SELECTORS.checkbox) - .click() - .get(categoryRow(secondCategory.id)) - .find(BUTTON_SELECTORS.checkbox) - .click() - .get(BUTTON_SELECTORS.deleteIcon) - .click() - .addAliasToGraphRequest("CategoryBulkDelete") - .get(BUTTON_SELECTORS.submit) - .click() - .waitForRequestAndCheckIfNoErrors("@CategoryBulkDelete"); - cy.get(categoryRow(firstCategory.id)).should("not.exist"); - cy.get(categoryRow(secondCategory.id)).should("not.exist"); + }).then(() => { + cy.visit(urlList.categories).searchInTable(startsWith); + ensureCanvasStatic(SHARED_ELEMENTS.dataGridTable); + cy.get(SHARED_ELEMENTS.firstRowDataGrid) + .invoke("text") + .then(firstOnListCategoryName => { + cy.get(SHARED_ELEMENTS.secondRowDataGrid) + .invoke("text") + .then(secondOnListCategoryName => { + // deletes two first rows from categories list view + cy.clickGridCell(0, 0); + cy.clickGridCell(0, 1); + + cy.get(CATEGORY_DETAILS_SELECTORS.deleteCategoriesButton) + .click() + .get(BUTTON_SELECTORS.submit) + .click() + .waitForRequestAndCheckIfNoErrors("@CategoryBulkDelete"); + ensureCanvasStatic(SHARED_ELEMENTS.dataGridTable); + + cy.contains( + SHARED_ELEMENTS.dataGridTable, + firstOnListCategoryName, + ).should("not.exist"); + cy.contains( + SHARED_ELEMENTS.dataGridTable, + secondOnListCategoryName, + ).should("not.exist"); + }); + }); }); }, ); @@ -263,6 +281,7 @@ describe("As an admin I want to manage categories", () => { const mainCategoryName = `${startsWith}${faker.datatype.number()}`; let subCategory; let mainCategory; + cy.addAliasToGraphRequest("CategoryBulkDelete"); createCategoryRequest({ name: mainCategoryName, @@ -277,14 +296,16 @@ describe("As an admin I want to manage categories", () => { .then(categoryResp => { subCategory = categoryResp; cy.visit(categoryDetailsUrl(mainCategory.id)) - .get(categoryRow(subCategory.id)) - .find(BUTTON_SELECTORS.checkbox) + .get(SHARED_ELEMENTS.dataGridTable) + .scrollIntoView(); + ensureCanvasStatic(SHARED_ELEMENTS.dataGridTable); + // selects first row of subcategories + cy.clickGridCell(0, 0); + cy.get(CATEGORY_DETAILS_SELECTORS.deleteCategoriesButton) .click() - .get(BUTTON_SELECTORS.deleteIcon) - .click() - .addAliasToGraphRequest("CategoryBulkDelete") .get(BUTTON_SELECTORS.submit) .click() + .confirmationMessageShouldDisappear() .waitForRequestAndCheckIfNoErrors("@CategoryBulkDelete"); getCategory(subCategory.id).should("be.null"); getCategory(mainCategory.id); diff --git a/cypress/elements/catalog/categories/category-details.js b/cypress/elements/catalog/categories/category-details.js index 7616f003d..09307dd71 100644 --- a/cypress/elements/catalog/categories/category-details.js +++ b/cypress/elements/catalog/categories/category-details.js @@ -6,4 +6,5 @@ export const CATEGORY_DETAILS_SELECTORS = { productsTab: '[data-test-id="products-tab"]', addProducts: '[data-test-id="add-products"]', productRow: '[data-test-id="product-row"]', + deleteCategoriesButton: '[data-test-id="delete-categories-button"]', }; diff --git a/cypress/elements/shared/sharedElements.js b/cypress/elements/shared/sharedElements.js index 302ce1e0f..adf2995ff 100644 --- a/cypress/elements/shared/sharedElements.js +++ b/cypress/elements/shared/sharedElements.js @@ -7,6 +7,8 @@ export const SHARED_ELEMENTS = { dataGridTable: "[data-testid='data-grid-canvas']", skeleton: '[data-test-id="skeleton"]', table: 'table[class*="Table"]', + firstRowDataGrid: "[data-testid='glide-cell-1-0']", + secondRowDataGrid: "[id='glide-cell-1-1']", tableRow: '[data-test-id*="id"], [class*="MuiTableRow"]', notificationSuccess: '[data-test-id="notification"][data-test-type="success"]', diff --git a/cypress/support/pages/catalog/categoriesPage.js b/cypress/support/pages/catalog/categoriesPage.js index 92920aeb6..4eaa5afcd 100644 --- a/cypress/support/pages/catalog/categoriesPage.js +++ b/cypress/support/pages/catalog/categoriesPage.js @@ -14,17 +14,16 @@ export function updateCategory({ name, description }) { export function fillUpCategoryGeneralInfo({ name, description }) { return cy + .get(CATEGORY_DETAILS_SELECTORS.nameInput) + .clearAndType(name) .get(CATEGORY_DETAILS_SELECTORS.descriptionInput) .find(SHARED_ELEMENTS.contentEditable) .should("be.visible") + .get(CATEGORY_DETAILS_SELECTORS.descriptionInput) .click() .get(CATEGORY_DETAILS_SELECTORS.descriptionInput) - .find(SHARED_ELEMENTS.contentEditable) - .get(CATEGORY_DETAILS_SELECTORS.descriptionInput) - .clearAndType(description) - .get(CATEGORY_DETAILS_SELECTORS.nameInput) - .clearAndType(name); + .clearAndType(description); } export function saveCategory(alias = "CategoryCreate") { diff --git a/locale/defaultMessages.json b/locale/defaultMessages.json index bf96b69de..9805f25aa 100644 --- a/locale/defaultMessages.json +++ b/locale/defaultMessages.json @@ -545,6 +545,9 @@ "context": "product variants, title", "string": "Variants" }, + "1X6HtI": { + "string": "All Categories" + }, "1div9r": { "string": "Search Attribute" }, @@ -1784,10 +1787,6 @@ "context": "webhooks and events section name", "string": "Webhooks & Events" }, - "BHQrgz": { - "context": "number of subcategories", - "string": "Subcategories" - }, "BJtUQI": { "context": "button", "string": "Add" @@ -2329,6 +2328,9 @@ "context": "Webhook details objects", "string": "Objects" }, + "F7DxHw": { + "string": "Subcategories" + }, "F8gsds": { "context": "unpublish page, button", "string": "Unpublish" @@ -3057,9 +3059,6 @@ "context": "dialog search placeholder", "string": "Search by collection name, etc..." }, - "JiXNEV": { - "string": "Search Category" - }, "Jj0de8": { "context": "voucher status", "string": "Scheduled" @@ -4306,6 +4305,9 @@ "context": "header", "string": "Create Variant" }, + "T83iU7": { + "string": "Search categories..." + }, "T8rvXs": { "context": "order subtotal price", "string": "Subtotal" @@ -4659,10 +4661,6 @@ "VOiUXQ": { "string": "Used to calculate rates for shipping for products of this product type, when specific weight is not given" }, - "VQLIXd": { - "context": "product", - "string": "Name" - }, "VSj89H": { "context": "fulfill button label", "string": "Fulfill anyway" @@ -5247,6 +5245,9 @@ "ZMy18J": { "string": "You have reached your channel limit, you will be no longer able to add channels to your store. If you would like to up your limit, contact your administration staff about raising your limits." }, + "ZN5IZl": { + "string": "Bulk categories delete" + }, "ZPOyI1": { "context": "fulfilled fulfillment, section header", "string": "Fulfilled from {warehouseName}" @@ -5671,6 +5672,9 @@ "context": "product attribute type", "string": "Multiple Select" }, + "cLcy6F": { + "string": "Number of products" + }, "cMFlOp": { "context": "input label", "string": "New Password" @@ -5774,6 +5778,9 @@ "context": "config type section title", "string": "Configuration Type" }, + "cxOmce": { + "string": "Bulk products delete" + }, "cy8sV7": { "context": "volume units types", "string": "Volume" @@ -6607,10 +6614,6 @@ "context": "tooltip content when product is in preorder", "string": "This product is still in preorder. You will be able to fulfill it after it reaches it’s release date" }, - "k8ZJ5L": { - "context": "number of products", - "string": "No. of Products" - }, "k8bltk": { "string": "No Results" }, @@ -6712,6 +6715,9 @@ "context": "balance amound missing error message", "string": "Balance amount is missing" }, + "kgVqk1": { + "string": "Category name" + }, "ki7Mr8": { "context": "product export to csv file, header", "string": "Export Settings" @@ -7454,6 +7460,9 @@ "context": "order line total price", "string": "Total" }, + "qU/z0Q": { + "string": "Bulk category delete" + }, "qZHHed": { "context": "stock exceeded dialog title", "string": "Not enough stock" @@ -7626,9 +7635,6 @@ "context": "header", "string": "Top Products" }, - "rrbzZt": { - "string": "No subcategories found" - }, "rs815i": { "context": "text field label", "string": "Group name" @@ -8234,10 +8240,6 @@ "context": "draft order", "string": "Created" }, - "vy7fjd": { - "context": "tab name", - "string": "All Categories" - }, "vzce9B": { "context": "customer gift cards card subtitle", "string": "Only five newest gift cards are shown here" diff --git a/src/categories/components/CategoryDeleteButton/CategoryDeleteButton.tsx b/src/categories/components/CategoryDeleteButton/CategoryDeleteButton.tsx new file mode 100644 index 000000000..1186a3900 --- /dev/null +++ b/src/categories/components/CategoryDeleteButton/CategoryDeleteButton.tsx @@ -0,0 +1,38 @@ +import { Button, Tooltip, TrashBinIcon } from "@saleor/macaw-ui/next"; +import React, { forwardRef, ReactNode, useState } from "react"; + +interface CategoryDeleteButtonProps { + onClick: () => void; + children: ReactNode; +} + +export const CategoryDeleteButton = forwardRef< + HTMLButtonElement, + CategoryDeleteButtonProps +>(({ onClick, children }, ref) => { + const [isTooltipOpen, setIsTooltipOpen] = useState(false); + + return ( + + + + + + + + + + + + + - - + + + + {selectedCategoriesIds.length > 0 && ( + + + + )} + + diff --git a/src/categories/components/CategoryListPage/messages.ts b/src/categories/components/CategoryListPage/messages.ts new file mode 100644 index 000000000..705d79ed6 --- /dev/null +++ b/src/categories/components/CategoryListPage/messages.ts @@ -0,0 +1,21 @@ +import { defineMessages } from "react-intl"; + +export const messages = defineMessages({ + allCategories: { + id: "1X6HtI", + defaultMessage: "All Categories", + }, + createCategory: { + id: "vof5TR", + defaultMessage: "Create category", + description: "button", + }, + searchCategory: { + id: "T83iU7", + defaultMessage: "Search categories...", + }, + bulkCategoryDelete: { + defaultMessage: "Bulk category delete", + id: "qU/z0Q", + }, +}); diff --git a/src/categories/components/CategoryProductList/CategoryProductList.tsx b/src/categories/components/CategoryProductList/CategoryProductList.tsx deleted file mode 100644 index 76a67d880..000000000 --- a/src/categories/components/CategoryProductList/CategoryProductList.tsx +++ /dev/null @@ -1,156 +0,0 @@ -// @ts-strict-ignore -import Checkbox from "@dashboard/components/Checkbox"; -import ResponsiveTable from "@dashboard/components/ResponsiveTable"; -import Skeleton from "@dashboard/components/Skeleton"; -import TableCellAvatar from "@dashboard/components/TableCellAvatar"; -import { AVATAR_MARGIN } from "@dashboard/components/TableCellAvatar/Avatar"; -import TableHead from "@dashboard/components/TableHead"; -import { TablePaginationWithContext } from "@dashboard/components/TablePagination"; -import TableRowLink from "@dashboard/components/TableRowLink"; -import { CategoryDetailsQuery } from "@dashboard/graphql"; -import { maybe, renderCollection } from "@dashboard/misc"; -import { productUrl } from "@dashboard/products/urls"; -import { ListActions, ListProps, RelayToFlat } from "@dashboard/types"; -import { TableBody, TableCell, TableFooter } from "@material-ui/core"; -import { makeStyles } from "@saleor/macaw-ui"; -import React from "react"; -import { FormattedMessage } from "react-intl"; - -const useStyles = makeStyles( - theme => ({ - [theme.breakpoints.up("lg")]: { - colName: { - width: "auto", - }, - }, - colFill: { - padding: 0, - width: "100%", - }, - colName: {}, - colNameHeader: { - marginLeft: AVATAR_MARGIN, - }, - link: { - cursor: "pointer", - }, - table: { - tableLayout: "fixed", - }, - tableContainer: { - overflowX: "scroll", - }, - textLeft: { - textAlign: "left", - }, - textRight: { - textAlign: "right", - }, - }), - { - name: "CategoryProductList", - }, -); - -interface CategoryProductListProps extends ListProps, ListActions { - products: RelayToFlat; -} - -export const CategoryProductList: React.FC< - CategoryProductListProps -> = props => { - const { - disabled, - isChecked, - products, - selected, - toggle, - toggleAll, - toolbar, - } = props; - - const classes = useStyles(props); - - const numberOfColumns = 2; - - return ( -
- - - - - - - - - - - - - - - - - - - {renderCollection( - products, - product => { - const isSelected = product ? isChecked(product.id) : false; - - return ( - - - toggle(product.id)} - /> - - product.thumbnail.url)} - > - {product ? product.name : } - - - ); - }, - () => ( - - - - - - ), - )} - - -
- ); -}; - -CategoryProductList.displayName = "CategoryProductList"; -export default CategoryProductList; diff --git a/src/categories/components/CategoryProductList/index.ts b/src/categories/components/CategoryProductList/index.ts deleted file mode 100644 index 5bc658620..000000000 --- a/src/categories/components/CategoryProductList/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { default } from "./CategoryProductList"; -export * from "./CategoryProductList"; diff --git a/src/categories/components/CategoryProductListDatagrid/CategoryProductListDatagrid.tsx b/src/categories/components/CategoryProductListDatagrid/CategoryProductListDatagrid.tsx new file mode 100644 index 000000000..5d827215c --- /dev/null +++ b/src/categories/components/CategoryProductListDatagrid/CategoryProductListDatagrid.tsx @@ -0,0 +1,87 @@ +// @ts-strict-ignore +import Datagrid from "@dashboard/components/Datagrid/Datagrid"; +import { useColumnsDefault } from "@dashboard/components/Datagrid/hooks/useColumnsDefault"; +import { + DatagridChangeStateContext, + useDatagridChangeState, +} from "@dashboard/components/Datagrid/hooks/useDatagridChange"; +import { TablePaginationWithContext } from "@dashboard/components/TablePagination"; +import { CategoryDetailsQuery } from "@dashboard/graphql"; +import { productUrl } from "@dashboard/products/urls"; +import { PageListProps, RelayToFlat } from "@dashboard/types"; +import { Item } from "@glideapps/glide-data-grid"; +import { Box } from "@saleor/macaw-ui/next"; +import React, { ReactNode, useCallback, useMemo } from "react"; +import { useIntl } from "react-intl"; + +import { createGetCellContent, getColumns } from "./datagrid"; + +interface CategoryListDatagridProps extends PageListProps { + products?: RelayToFlat; + disabled: boolean; + selectionActionButton?: ReactNode | null; + onSelectProductsIds: (ids: number[], clearSelection: () => void) => void; +} + +export const CategoryProductListDatagrid = ({ + products, + disabled, + onSelectProductsIds, + settings, + onUpdateListSettings, + selectionActionButton = null, +}: CategoryListDatagridProps) => { + const datagridState = useDatagridChangeState(); + const intl = useIntl(); + const availableColumns = useMemo(() => getColumns(intl), [intl]); + // eslint-disable-next-line react-hooks/exhaustive-deps + const getCellContent = useCallback( + createGetCellContent(products, availableColumns), + [products, availableColumns], + ); + + const { columns, onColumnMoved, onColumnResize } = + useColumnsDefault(availableColumns); + + const handleRowAnchor = useCallback( + ([, row]: Item) => productUrl(products[row].id), + [products], + ); + + return ( + + false} + emptyText={intl.formatMessage({ + defaultMessage: "No products found", + id: "Q1Uzbb", + })} + rowAnchor={handleRowAnchor} + menuItems={() => []} + selectionActions={() => selectionActionButton} + onColumnResize={onColumnResize} + onColumnMoved={onColumnMoved} + onRowSelectionChange={onSelectProductsIds} + /> + + + + + + ); +}; diff --git a/src/categories/components/CategoryProductListDatagrid/datagrid.ts b/src/categories/components/CategoryProductListDatagrid/datagrid.ts new file mode 100644 index 000000000..830696643 --- /dev/null +++ b/src/categories/components/CategoryProductListDatagrid/datagrid.ts @@ -0,0 +1,45 @@ +// @ts-strict-ignore +import { + readonlyTextCell, + thumbnailCell, +} from "@dashboard/components/Datagrid/customCells/cells"; +import { AvailableColumn } from "@dashboard/components/Datagrid/types"; +import { CategoryDetailsQuery } from "@dashboard/graphql"; +import { RelayToFlat } from "@dashboard/types"; +import { GridCell, Item } from "@glideapps/glide-data-grid"; +import { IntlShape } from "react-intl"; + +import { columnsMessages } from "./messages"; + +export const getColumns = (intl: IntlShape): AvailableColumn[] => [ + { + id: "name", + title: intl.formatMessage(columnsMessages.name), + width: 500, + }, +]; + +export const createGetCellContent = + ( + products: RelayToFlat, + columns: AvailableColumn[], + ) => + ([column, row]: Item): GridCell => { + const columnId = columns[column]?.id; + + if (!columnId) { + return readonlyTextCell(""); + } + + const rowData = products[row]; + + switch (columnId) { + case "name": + const name = rowData?.name ?? ""; + return thumbnailCell(name, rowData?.thumbnail?.url ?? "", { + cursor: "pointer", + }); + default: + return readonlyTextCell("", false); + } + }; diff --git a/src/categories/components/CategoryProductListDatagrid/index.ts b/src/categories/components/CategoryProductListDatagrid/index.ts new file mode 100644 index 000000000..a41d28261 --- /dev/null +++ b/src/categories/components/CategoryProductListDatagrid/index.ts @@ -0,0 +1 @@ +export * from "./CategoryProductListDatagrid"; diff --git a/src/categories/components/CategoryProductListDatagrid/messages.ts b/src/categories/components/CategoryProductListDatagrid/messages.ts new file mode 100644 index 000000000..ce0d1e51e --- /dev/null +++ b/src/categories/components/CategoryProductListDatagrid/messages.ts @@ -0,0 +1,8 @@ +import { defineMessages } from "react-intl"; + +export const columnsMessages = defineMessages({ + name: { + id: "HAlOn1", + defaultMessage: "Name", + }, +}); diff --git a/src/categories/components/CategoryProducts/CategoryProducts.tsx b/src/categories/components/CategoryProducts/CategoryProducts.tsx index e0af64af2..036d70495 100644 --- a/src/categories/components/CategoryProducts/CategoryProducts.tsx +++ b/src/categories/components/CategoryProducts/CategoryProducts.tsx @@ -1,91 +1,85 @@ // @ts-strict-ignore -import { Button } from "@dashboard/components/Button"; -import CardTitle from "@dashboard/components/CardTitle"; -import HorizontalSpacer from "@dashboard/components/HorizontalSpacer"; +import { DashboardCard } from "@dashboard/components/Card"; import { InternalLink } from "@dashboard/components/InternalLink"; import { CategoryDetailsQuery } from "@dashboard/graphql"; import { productAddUrl, productListUrl } from "@dashboard/products/urls"; -import { Card } from "@material-ui/core"; +import { RelayToFlat } from "@dashboard/types"; +import { Box, Button } from "@saleor/macaw-ui/next"; import React from "react"; -import { FormattedMessage, useIntl } from "react-intl"; +import { FormattedMessage } from "react-intl"; -import { ListActions, PageListProps, RelayToFlat } from "../../../types"; -import CategoryProductList from "../CategoryProductList"; -import { useStyles } from "./styles"; +import { CategoryDeleteButton } from "../CategoryDeleteButton"; +import { CategoryProductListDatagrid } from "../CategoryProductListDatagrid"; -interface CategoryProductsProps extends PageListProps, ListActions { - products: RelayToFlat; - categoryName: string; +interface CategoryProductsProps { + category: CategoryDetailsQuery["category"]; categoryId: string; + products: RelayToFlat; + disabled: boolean; + onProductsDelete: () => void; + onSelectProductsIds: (ids: number[], clearSelection: () => void) => void; } -export const CategoryProducts: React.FC = ({ +export const CategoryProducts = ({ + category, + categoryId, products, disabled, - categoryId, - categoryName, - isChecked, - selected, - toggle, - toggleAll, - toolbar, -}) => { - const intl = useIntl(); - const classes = useStyles(); + onProductsDelete, + onSelectProductsIds, +}: CategoryProductsProps) => ( + + + + - return ( - - - - - - - + + + + - - } - /> - - - ); -}; + + + + -CategoryProducts.displayName = "CategoryProducts"; -export default CategoryProducts; + + + + + + } + /> + +); diff --git a/src/categories/components/CategoryProducts/index.ts b/src/categories/components/CategoryProducts/index.ts index 12cece4a6..8b1b6b18b 100644 --- a/src/categories/components/CategoryProducts/index.ts +++ b/src/categories/components/CategoryProducts/index.ts @@ -1 +1 @@ -export { default } from "./CategoryProducts"; +export * from "./CategoryProducts"; diff --git a/src/categories/components/CategoryProducts/styles.ts b/src/categories/components/CategoryProducts/styles.ts deleted file mode 100644 index afc48d67d..000000000 --- a/src/categories/components/CategoryProducts/styles.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { makeStyles } from "@saleor/macaw-ui"; - -export const useStyles = makeStyles( - () => ({ - toolbar: { - display: "flex", - }, - }), - { name: "CategoryProducts" }, -); diff --git a/src/categories/components/CategorySubcategories/CategorySubcategories.tsx b/src/categories/components/CategorySubcategories/CategorySubcategories.tsx new file mode 100644 index 000000000..41540e507 --- /dev/null +++ b/src/categories/components/CategorySubcategories/CategorySubcategories.tsx @@ -0,0 +1,65 @@ +// @ts-strict-ignore +import { categoryAddUrl } from "@dashboard/categories/urls"; +import { DashboardCard } from "@dashboard/components/Card"; +import { InternalLink } from "@dashboard/components/InternalLink"; +import { CategoryDetailsQuery } from "@dashboard/graphql"; +import { RelayToFlat } from "@dashboard/types"; +import { Box, Button } from "@saleor/macaw-ui/next"; +import React from "react"; +import { FormattedMessage } from "react-intl"; + +import { CategoryDeleteButton } from "../CategoryDeleteButton"; +import { CategoryListDatagrid } from "../CategoryListDatagrid"; + +interface CategorySubcategoriesProps { + categoryId: string; + disabled: boolean; + subcategories: RelayToFlat; + onCategoriesDelete: () => void; + onSelectCategoriesIds: (ids: number[], clearSelection: () => void) => void; +} + +export const CategorySubcategories = ({ + categoryId, + subcategories, + disabled, + onCategoriesDelete, + onSelectCategoriesIds, +}: CategorySubcategoriesProps) => ( + + + + + + + + + + + + + + + + } + /> + +); diff --git a/src/categories/components/CategorySubcategories/index.ts b/src/categories/components/CategorySubcategories/index.ts new file mode 100644 index 000000000..17c73b6bf --- /dev/null +++ b/src/categories/components/CategorySubcategories/index.ts @@ -0,0 +1 @@ +export * from "./CategorySubcategories"; diff --git a/src/categories/components/CategoryUpdatePage/CategoryUpdatePage.stories.tsx b/src/categories/components/CategoryUpdatePage/CategoryUpdatePage.stories.tsx index 0575db897..8f352138f 100644 --- a/src/categories/components/CategoryUpdatePage/CategoryUpdatePage.stories.tsx +++ b/src/categories/components/CategoryUpdatePage/CategoryUpdatePage.stories.tsx @@ -26,11 +26,13 @@ const updateProps: Omit = { onImageDelete: () => undefined, onImageUpload: () => undefined, onSubmit: () => undefined, - productListToolbar: null, products: mapEdgesToItems(category.products), saveButtonBarState: "default", subcategories: mapEdgesToItems(category.children), - subcategoryListToolbar: null, + onCategoriesDelete: () => undefined, + onProductsDelete: () => undefined, + onSelectCategoriesIds: () => undefined, + onSelectProductsIds: () => undefined, ...listActionsProps, }; diff --git a/src/categories/components/CategoryUpdatePage/CategoryUpdatePage.tsx b/src/categories/components/CategoryUpdatePage/CategoryUpdatePage.tsx index 884468050..27a1fe470 100644 --- a/src/categories/components/CategoryUpdatePage/CategoryUpdatePage.tsx +++ b/src/categories/components/CategoryUpdatePage/CategoryUpdatePage.tsx @@ -1,13 +1,7 @@ // @ts-strict-ignore -import { - categoryAddUrl, - categoryListUrl, - categoryUrl, -} from "@dashboard/categories/urls"; +import { categoryListUrl, categoryUrl } from "@dashboard/categories/urls"; import { TopNav } from "@dashboard/components/AppLayout/TopNav"; -import { Button } from "@dashboard/components/Button"; import { CardSpacer } from "@dashboard/components/CardSpacer"; -import CardTitle from "@dashboard/components/CardTitle"; import { ConfirmButtonTransitionState } from "@dashboard/components/ConfirmButton"; import { DetailPageLayout } from "@dashboard/components/Layouts"; import { Metadata } from "@dashboard/components/Metadata/Metadata"; @@ -17,17 +11,16 @@ import { Tab, TabContainer } from "@dashboard/components/Tab"; import { CategoryDetailsQuery, ProductErrorFragment } from "@dashboard/graphql"; import { SubmitPromise } from "@dashboard/hooks/useForm"; import useNavigator from "@dashboard/hooks/useNavigator"; -import { Card } from "@material-ui/core"; import { sprinkles } from "@saleor/macaw-ui/next"; import React from "react"; import { FormattedMessage, useIntl } from "react-intl"; import { maybe } from "../../../misc"; -import { RelayToFlat, TabListActions } from "../../../types"; +import { RelayToFlat } from "../../../types"; import CategoryDetailsForm from "../../components/CategoryDetailsForm"; -import CategoryList from "../../components/CategoryList"; import CategoryBackground from "../CategoryBackground"; -import CategoryProducts from "../CategoryProducts"; +import { CategoryProducts } from "../CategoryProducts"; +import { CategorySubcategories } from "../CategorySubcategories"; import CategoryUpdateForm, { CategoryUpdateData } from "./form"; export enum CategoryPageTab { @@ -35,8 +28,7 @@ export enum CategoryPageTab { products = "products", } -export interface CategoryUpdatePageProps - extends TabListActions<"productListToolbar" | "subcategoryListToolbar"> { +export interface CategoryUpdatePageProps { categoryId: string; changeTab: (index: CategoryPageTab) => void; currentTab: CategoryPageTab; @@ -49,6 +41,10 @@ export interface CategoryUpdatePageProps addProductHref: string; onImageDelete: () => void; onSubmit: (data: CategoryUpdateData) => SubmitPromise; + onCategoriesDelete: () => void; + onProductsDelete: () => void; + onSelectProductsIds: (ids: number[], clearSelection: () => void) => void; + onSelectCategoriesIds: (ids: number[], clearSelection: () => void) => void; onImageUpload(file: File); onDelete(); } @@ -70,12 +66,10 @@ export const CategoryUpdatePage: React.FC = ({ onSubmit, onImageDelete, onImageUpload, - isChecked, - productListToolbar, - selected, - subcategoryListToolbar, - toggle, - toggleAll, + onSelectCategoriesIds, + onCategoriesDelete, + onProductsDelete, + onSelectProductsIds, }: CategoryUpdatePageProps) => { const intl = useIntl(); const navigate = useNavigator(); @@ -100,7 +94,9 @@ export const CategoryUpdatePage: React.FC = ({ errors={errors} onChange={change} /> + + = ({ image={maybe(() => category.backgroundImage)} onChange={change} /> + + = ({ onChange={change} disabled={disabled} /> + + + + = ({ description="number of subcategories in category" /> + = ({ /> + + {currentTab === CategoryPageTab.categories && ( - - - - - } - /> - undefined} - /> - - )} - {currentTab === CategoryPageTab.products && ( - )} + + {currentTab === CategoryPageTab.products && ( + + )} + navigate(backHref)} onDelete={onDelete} diff --git a/src/categories/views/CategoryDetails.tsx b/src/categories/views/CategoryDetails.tsx index 16de5c9dd..04e7ec7d7 100644 --- a/src/categories/views/CategoryDetails.tsx +++ b/src/categories/views/CategoryDetails.tsx @@ -15,21 +15,21 @@ import { useUpdateMetadataMutation, useUpdatePrivateMetadataMutation, } from "@dashboard/graphql"; -import useBulkActions from "@dashboard/hooks/useBulkActions"; import useLocalPaginator, { useSectionLocalPaginationState, } from "@dashboard/hooks/useLocalPaginator"; import useNavigator from "@dashboard/hooks/useNavigator"; import useNotifier from "@dashboard/hooks/useNotifier"; import { PaginatorContext } from "@dashboard/hooks/usePaginator"; +import { useRowSelection } from "@dashboard/hooks/useRowSelection"; import { commonMessages, errorMessages } from "@dashboard/intl"; import createDialogActionHandlers from "@dashboard/utils/handlers/dialogActionHandlers"; import createMetadataUpdateHandler from "@dashboard/utils/handlers/metadataUpdateHandler"; import { mapEdgesToItems } from "@dashboard/utils/maps"; import { getParsedDataForJsonStringField } from "@dashboard/utils/richText/misc"; import { DialogContentText } from "@material-ui/core"; -import { DeleteIcon, IconButton } from "@saleor/macaw-ui"; -import React, { useState } from "react"; +import isEqual from "lodash/isEqual"; +import React, { useCallback, useState } from "react"; import { FormattedMessage, useIntl } from "react-intl"; import { PAGINATE_BY } from "../../config"; @@ -64,13 +64,26 @@ export const CategoryDetails: React.FC = ({ }) => { const navigate = useNavigator(); const notify = useNotifier(); - const { isSelected, listElements, reset, toggle, toggleAll } = useBulkActions( - params.ids, - ); const intl = useIntl(); const [updateMetadata] = useUpdateMetadataMutation({}); const [updatePrivateMetadata] = useUpdatePrivateMetadataMutation({}); + const { + clearRowSelection: clearProductRowSelection, + selectedRowIds: selectedProductRowIds, + setClearDatagridRowSelectionCallback: + setClearProductDatagridRowSelectionCallback, + setSelectedRowIds: setSelectedProductRowIds, + } = useRowSelection(); + + const { + clearRowSelection: clearCategryRowSelection, + selectedRowIds: selectedCategoryRowIds, + setClearDatagridRowSelectionCallback: + setClearCategoryDatagridRowSelectionCallback, + setSelectedRowIds: setSelectedCategoryRowIds, + } = useRowSelection(); + const [activeTab, setActiveTab] = useState( CategoryPageTab.categories, ); @@ -80,7 +93,8 @@ export const CategoryDetails: React.FC = ({ ); const paginate = useLocalPaginator(setPaginationState); const changeTab = (tab: CategoryPageTab) => { - reset(); + clearProductRowSelection(); + clearCategryRowSelection(); setActiveTab(tab); }; @@ -90,6 +104,8 @@ export const CategoryDetails: React.FC = ({ }); const category = data?.category; + const subcategories = mapEdgesToItems(data?.category?.children); + const products = mapEdgesToItems(data?.category?.products); const handleCategoryDelete = (data: CategoryDeleteMutation) => { if (data.categoryDelete.errors.length === 0) { @@ -100,6 +116,7 @@ export const CategoryDetails: React.FC = ({ defaultMessage: "Category deleted", }), }); + clearProductRowSelection(); navigate(categoryListUrl()); } }; @@ -109,6 +126,7 @@ export const CategoryDetails: React.FC = ({ }); const handleCategoryUpdate = (data: CategoryUpdateMutation) => { + clearProductRowSelection(); if (data.categoryUpdate.errors.length > 0) { const backgroundImageError = data.categoryUpdate.errors.find( error => error.field === ("backgroundImage" as keyof CategoryInput), @@ -133,13 +151,13 @@ export const CategoryDetails: React.FC = ({ }); const handleBulkCategoryDelete = (data: CategoryBulkDeleteMutation) => { + clearCategryRowSelection(); if (data.categoryBulkDelete.errors.length === 0) { closeModal(); notify({ status: "success", text: intl.formatMessage(commonMessages.savedChanges), }); - reset(); } }; @@ -151,6 +169,7 @@ export const CategoryDetails: React.FC = ({ const [productBulkDelete, productBulkDeleteOpts] = useProductBulkDeleteMutation({ onCompleted: data => { + clearProductRowSelection(); if (data.productBulkDelete.errors.length === 0) { closeModal(); notify({ @@ -158,7 +177,6 @@ export const CategoryDetails: React.FC = ({ text: intl.formatMessage(commonMessages.savedChanges), }); refetch(); - reset(); } }, }); @@ -194,6 +212,52 @@ export const CategoryDetails: React.FC = ({ }), ); + const handleSetSelectedCategoryIds = useCallback( + (rows: number[], clearSelection: () => void) => { + if (!subcategories) { + return; + } + + const rowsIds = rows.map(row => subcategories[row].id); + const haveSaveValues = isEqual(rowsIds, selectedCategoryRowIds); + + if (!haveSaveValues) { + setSelectedCategoryRowIds(rowsIds); + } + + setClearCategoryDatagridRowSelectionCallback(clearSelection); + }, + [ + selectedCategoryRowIds, + setClearCategoryDatagridRowSelectionCallback, + setSelectedCategoryRowIds, + subcategories, + ], + ); + + const handleSetSelectedPrductIds = useCallback( + (rows: number[], clearSelection: () => void) => { + if (!products) { + return; + } + + const rowsIds = rows.map(row => products[row].id); + const haveSaveValues = isEqual(rowsIds, selectedProductRowIds); + + if (!haveSaveValues) { + setSelectedProductRowIds(rowsIds); + } + + setClearProductDatagridRowSelectionCallback(clearSelection); + }, + [ + products, + selectedProductRowIds, + setClearProductDatagridRowSelectionCallback, + setSelectedProductRowIds, + ], + ); + const handleSubmit = createMetadataUpdateHandler( data?.category, handleUpdate, @@ -238,41 +302,19 @@ export const CategoryDetails: React.FC = ({ }) } onSubmit={handleSubmit} - products={mapEdgesToItems(data?.category?.products)} + products={products} saveButtonBarState={updateResult.status} - subcategories={mapEdgesToItems(data?.category?.children)} - subcategoryListToolbar={ - - openModal("delete-categories", { - ids: listElements, - }) - } - > - - - } - productListToolbar={ - - openModal("delete-products", { - ids: listElements, - }) - } - > - - - } - isChecked={isSelected} - selected={listElements.length} - toggle={toggle} - toggleAll={toggleAll} + subcategories={subcategories} + onSelectCategoriesIds={handleSetSelectedCategoryIds} + onSelectProductsIds={handleSetSelectedPrductIds} + onCategoriesDelete={() => { + openModal("delete-categories"); + }} + onProductsDelete={() => { + openModal("delete-products"); + }} /> + = ({ /> + params.ids.length > 0) - } + open={params.action === "delete-categories"} confirmButtonState={categoryBulkDeleteOpts.status} onClose={closeModal} onConfirm={() => categoryBulkDelete({ - variables: { ids: params.ids }, + variables: { ids: selectedCategoryRowIds }, }).then(() => refetch()) } title={intl.formatMessage({ @@ -327,9 +367,9 @@ export const CategoryDetails: React.FC = ({ id="Pp/7T7" defaultMessage="{counter,plural,one{Are you sure you want to delete this category?} other{Are you sure you want to delete {displayQuantity} categories?}}" values={{ - counter: maybe(() => params.ids.length), + counter: maybe(() => selectedCategoryRowIds.length), displayQuantity: ( - {maybe(() => params.ids.length)} + {maybe(() => selectedCategoryRowIds.length)} ), }} /> @@ -341,13 +381,14 @@ export const CategoryDetails: React.FC = ({ /> + productBulkDelete({ - variables: { ids: params.ids }, + variables: { ids: selectedProductRowIds }, }).then(() => refetch()) } title={intl.formatMessage({ @@ -362,9 +403,9 @@ export const CategoryDetails: React.FC = ({ id="7l5Bh9" defaultMessage="{counter,plural,one{Are you sure you want to delete this product?} other{Are you sure you want to delete {displayQuantity} products?}}" values={{ - counter: maybe(() => params.ids.length), + counter: maybe(() => selectedProductRowIds.length), displayQuantity: ( - {maybe(() => params.ids.length)} + {maybe(() => selectedProductRowIds.length)} ), }} /> diff --git a/src/categories/views/CategoryList/CategoryList.tsx b/src/categories/views/CategoryList/CategoryList.tsx index c65283a72..eb14a9299 100644 --- a/src/categories/views/CategoryList/CategoryList.tsx +++ b/src/categories/views/CategoryList/CategoryList.tsx @@ -1,15 +1,13 @@ // @ts-strict-ignore import ActionDialog from "@dashboard/components/ActionDialog"; import DeleteFilterTabDialog from "@dashboard/components/DeleteFilterTabDialog"; -import SaveFilterTabDialog, { - SaveFilterTabDialogFormData, -} from "@dashboard/components/SaveFilterTabDialog"; +import SaveFilterTabDialog from "@dashboard/components/SaveFilterTabDialog"; import { CategoryBulkDeleteMutation, useCategoryBulkDeleteMutation, useRootCategoriesQuery, } from "@dashboard/graphql"; -import useBulkActions from "@dashboard/hooks/useBulkActions"; +import { useFilterPresets } from "@dashboard/hooks/useFilterPresets"; import useListSettings from "@dashboard/hooks/useListSettings"; import useNavigator from "@dashboard/hooks/useNavigator"; import { usePaginationReset } from "@dashboard/hooks/usePaginationReset"; @@ -17,6 +15,7 @@ import usePaginator, { createPaginationState, PaginatorContext, } from "@dashboard/hooks/usePaginator"; +import { useRowSelection } from "@dashboard/hooks/useRowSelection"; import { maybe } from "@dashboard/misc"; import { ListViews } from "@dashboard/types"; import createDialogActionHandlers from "@dashboard/utils/handlers/dialogActionHandlers"; @@ -24,8 +23,8 @@ import createSortHandler from "@dashboard/utils/handlers/sortHandler"; import { mapEdgesToItems } from "@dashboard/utils/maps"; import { getSortParams } from "@dashboard/utils/sort"; import { DialogContentText } from "@material-ui/core"; -import { DeleteIcon, IconButton } from "@saleor/macaw-ui"; -import React from "react"; +import isEqual from "lodash/isEqual"; +import React, { useCallback } from "react"; import { FormattedMessage, useIntl } from "react-intl"; import { CategoryListPage } from "../../components/CategoryListPage/CategoryListPage"; @@ -35,14 +34,7 @@ import { CategoryListUrlFilters, CategoryListUrlQueryParams, } from "../../urls"; -import { - deleteFilterTab, - getActiveFilters, - getFiltersCurrentTab, - getFilterTabs, - getFilterVariables, - saveFilterTab, -} from "./filter"; +import { getActiveFilters, getFilterVariables, storageUtils } from "./filter"; import { getSortQueryVariables } from "./sort"; interface CategoryListProps { @@ -51,18 +43,44 @@ interface CategoryListProps { export const CategoryList: React.FC = ({ params }) => { const navigate = useNavigator(); + const intl = useIntl(); - const { isSelected, listElements, toggle, toggleAll, reset } = useBulkActions( - params.ids, - ); const { updateListSettings, settings } = useListSettings( ListViews.CATEGORY_LIST, ); + const handleSort = createSortHandler(navigate, categoryListUrl, params); + + const { + selectedRowIds, + setSelectedRowIds, + clearRowSelection, + setClearDatagridRowSelectionCallback, + } = useRowSelection(params); + + const { + hasPresetsChange, + onPresetChange, + onPresetDelete, + onPresetSave, + onPresetUpdate, + presetIdToDelete, + presets, + selectedPreset, + setPresetIdToDelete, + } = useFilterPresets({ + params, + storageUtils, + getUrl: categoryListUrl, + reset: clearRowSelection, + }); + + const [openModal, closeModal] = createDialogActionHandlers< + CategoryListUrlDialog, + CategoryListUrlQueryParams + >(navigate, categoryListUrl, params); + usePaginationReset(categoryListUrl, params, settings.rowNumber); - - const intl = useIntl(); - const paginationState = createPaginationState(settings.rowNumber, params); const queryVariables = React.useMemo( () => ({ @@ -70,111 +88,98 @@ export const CategoryList: React.FC = ({ params }) => { filter: getFilterVariables(params), sort: getSortQueryVariables(params), }), - [params, settings.rowNumber], + [paginationState, params], ); + const { data, loading, refetch } = useRootCategoriesQuery({ displayLoader: true, variables: queryVariables, }); + const categories = mapEdgesToItems(data?.categories); - const tabs = getFilterTabs(); - - const currentTab = getFiltersCurrentTab(params, tabs); + const paginationValues = usePaginator({ + pageInfo: data?.categories?.pageInfo, + paginationState, + queryString: params, + }); const changeFilterField = (filter: CategoryListUrlFilters) => { - reset(); + clearRowSelection(); navigate( categoryListUrl({ ...getActiveFilters(params), ...filter, - activeTab: undefined, + activeTab: !filter.query?.length ? undefined : params.activeTab, }), ); }; - const [openModal, closeModal] = createDialogActionHandlers< - CategoryListUrlDialog, - CategoryListUrlQueryParams - >(navigate, categoryListUrl, params); - - const handleTabChange = (tab: number) => { - reset(); - navigate( - categoryListUrl({ - activeTab: tab.toString(), - ...getFilterTabs()[tab - 1].data, - }), - ); - }; - - const handleTabDelete = () => { - deleteFilterTab(currentTab); - reset(); - navigate(categoryListUrl()); - }; - - const handleTabSave = (data: SaveFilterTabDialogFormData) => { - saveFilterTab(data.name, getActiveFilters(params)); - handleTabChange(tabs.length + 1); - }; - - const paginationValues = usePaginator({ - pageInfo: maybe(() => data.categories.pageInfo), - paginationState, - queryString: params, - }); - const handleCategoryBulkDelete = (data: CategoryBulkDeleteMutation) => { if (data.categoryBulkDelete.errors.length === 0) { navigate(categoryListUrl(), { replace: true }); refetch(); - reset(); + clearRowSelection(); } }; + const handleSetSelectedCategoryIds = useCallback( + (rows: number[], clearSelection: () => void) => { + if (!categories) { + return; + } + + const rowsIds = rows.map(row => categories[row].id); + const haveSaveValues = isEqual(rowsIds, selectedRowIds); + + if (!haveSaveValues) { + setSelectedRowIds(rowsIds); + } + + setClearDatagridRowSelectionCallback(clearSelection); + }, + [ + categories, + setClearDatagridRowSelectionCallback, + selectedRowIds, + setSelectedRowIds, + ], + ); + const [categoryBulkDelete, categoryBulkDeleteOpts] = useCategoryBulkDeleteMutation({ onCompleted: handleCategoryBulkDelete, }); - const handleSort = createSortHandler(navigate, categoryListUrl, params); - return ( changeFilterField({ query })} onAll={() => navigate(categoryListUrl())} - onTabChange={handleTabChange} - onTabDelete={() => openModal("delete-search")} + onTabChange={onPresetChange} + onTabDelete={(tabIndex: number) => { + setPresetIdToDelete(tabIndex); + openModal("delete-search"); + }} + onTabUpdate={onPresetUpdate} onTabSave={() => openModal("save-search")} - tabs={tabs.map(tab => tab.name)} + tabs={presets.map(tab => tab.name)} settings={settings} sort={getSortParams(params)} onSort={handleSort} disabled={loading} - onUpdateListSettings={updateListSettings} - isChecked={isSelected} - selected={listElements.length} - toggle={toggle} - toggleAll={toggleAll} - toolbar={ - - openModal("delete", { - ids: listElements, - }) - } - > - - - } + onUpdateListSettings={(...props) => { + clearRowSelection(); + updateListSettings(...props); + }} + selectedCategoriesIds={selectedRowIds} + onSelectCategoriesIds={handleSetSelectedCategoryIds} + onCategoriesDelete={() => openModal("delete")} /> + @@ -189,7 +194,7 @@ export const CategoryList: React.FC = ({ params }) => { onConfirm={() => categoryBulkDelete({ variables: { - ids: params.ids, + ids: selectedRowIds, }, }) } @@ -220,18 +225,20 @@ export const CategoryList: React.FC = ({ params }) => { /> + + tabs[currentTab - 1].name, "...")} + onSubmit={onPresetDelete} + tabName={presets[presetIdToDelete - 1]?.name ?? "..."} /> ); diff --git a/src/categories/views/CategoryList/filter.ts b/src/categories/views/CategoryList/filter.ts index 96c3dbf22..b6ed195f6 100644 --- a/src/categories/views/CategoryList/filter.ts +++ b/src/categories/views/CategoryList/filter.ts @@ -20,16 +20,9 @@ export function getFilterVariables( }; } -export const { - deleteFilterTab, - getFilterTabs, - saveFilterTab, -} = createFilterTabUtils(CATEGORY_FILTERS_KEY); +export const storageUtils = createFilterTabUtils(CATEGORY_FILTERS_KEY); -export const { - areFiltersApplied, - getActiveFilters, - getFiltersCurrentTab, -} = createFilterUtils( - CategoryListUrlFiltersEnum, -); +export const { areFiltersApplied, getActiveFilters, getFiltersCurrentTab } = + createFilterUtils( + CategoryListUrlFiltersEnum, + ); diff --git a/src/components/Datagrid/Datagrid.tsx b/src/components/Datagrid/Datagrid.tsx index 92b736e6f..8a0653e11 100644 --- a/src/components/Datagrid/Datagrid.tsx +++ b/src/components/Datagrid/Datagrid.tsx @@ -14,6 +14,7 @@ import DataEditor, { GridSelection, HeaderClickedEventArgs, Item, + Theme, } from "@glideapps/glide-data-grid"; import { GetRowThemeCallback } from "@glideapps/glide-data-grid/dist/ts/data-grid/data-grid-render"; import { Card, CardContent, CircularProgress } from "@material-ui/core"; @@ -104,6 +105,7 @@ export interface DatagridProps { columnSelect?: DataEditorProps["columnSelect"]; showEmptyDatagrid?: boolean; rowAnchor?: (item: Item) => string; + actionButtonPosition?: "left" | "right"; recentlyAddedColumn?: string; // Enables scroll to recently added column } @@ -135,10 +137,11 @@ export const Datagrid: React.FC = ({ rowAnchor, hasRowHover = false, onRowSelectionChange, + actionButtonPosition = "left", recentlyAddedColumn, ...datagridProps }): ReactElement => { - const classes = useStyles(); + const classes = useStyles({ actionButtonPosition }); const { themeValues } = useTheme(); const datagridTheme = useDatagridTheme(readonly, readonly); const editor = useRef(null); @@ -266,7 +269,9 @@ export const Datagrid: React.FC = ({ const handleRowHover = useCallback( (args: GridMouseEventArgs) => { if (hasRowHover) { - setHoverRow(args.kind !== "cell" ? undefined : args.location[1]); + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const [_, row] = args.location; + setHoverRow(args.kind !== "cell" ? undefined : row); } // the code below is responsible for adding native element when hovering over rows in the datagrid @@ -330,12 +335,11 @@ export const Datagrid: React.FC = ({ return undefined; } - const overrideTheme = { + const overrideTheme: Partial = { bgCell: themeValues.colors.background.interactiveNeutralSecondaryHovering, bgCellMedium: themeValues.colors.background.interactiveNeutralSecondaryHovering, - accentLight: undefined as string | undefined, }; if (readonly) { @@ -476,11 +480,13 @@ export const Datagrid: React.FC = ({ {rowsTotal > 0 || showEmptyDatagrid ? ( <> - {selection?.rows && selection?.rows.length > 0 && ( -
- {selectionActionsComponent} -
- )} + {selection?.rows && + selection?.rows.length > 0 && + selectionActionsComponent && ( +
+ {selectionActionsComponent} +
+ )}
{ - const classes = useStyles(); + const classes = useStyles({}); const hasSingleMenuItem = menuItems.length === 1; const firstMenuItem = menuItems[0]; diff --git a/src/components/Datagrid/styles.ts b/src/components/Datagrid/styles.ts index 530e8e59d..e03b79f68 100644 --- a/src/components/Datagrid/styles.ts +++ b/src/components/Datagrid/styles.ts @@ -5,7 +5,7 @@ import { useMemo } from "react"; export const cellHeight = 40; -const useStyles = makeStyles( +const useStyles = makeStyles<{ actionButtonPosition?: "left" | "right" }>( () => { const rowActionSelected = { background: vars.colors.background.plain, @@ -16,6 +16,8 @@ const useStyles = makeStyles( return { actionBtnBar: { position: "absolute", + left: props => (props.actionButtonPosition === "left" ? 0 : "auto"), + right: props => (props.actionButtonPosition === "right" ? 0 : "auto"), zIndex: 1, background: vars.colors.background.plain, borderRadius: vars.borderRadius[4], diff --git a/src/components/FilterPresetsSelect/FilterPresetsSelect.tsx b/src/components/FilterPresetsSelect/FilterPresetsSelect.tsx index 656c5ee41..982e32c6c 100644 --- a/src/components/FilterPresetsSelect/FilterPresetsSelect.tsx +++ b/src/components/FilterPresetsSelect/FilterPresetsSelect.tsx @@ -48,6 +48,7 @@ export const FilterPresetsSelect = ({ const intl = useIntl(); const showUpdateButton = presetsChanged && savedPresets.length > 0 && activePreset; + const showSaveButton = presetsChanged; const getLabel = () => {