Category listing datagrid (#3760)
Co-authored-by: wojteknowacki <wojciech.nowacki@saleor.io>
This commit is contained in:
parent
1cb6e8b5fc
commit
b4f11eff66
37 changed files with 997 additions and 812 deletions
5
.changeset/strange-carrots-juggle.md
Normal file
5
.changeset/strange-carrots-juggle.md
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
"saleor-dashboard": minor
|
||||
---
|
||||
|
||||
Introduce datagrid on category listing page
|
|
@ -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)
|
||||
}).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(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");
|
||||
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);
|
||||
|
|
|
@ -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"]',
|
||||
};
|
||||
|
|
|
@ -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"]',
|
||||
|
|
|
@ -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") {
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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 (
|
||||
<Tooltip open={isTooltipOpen}>
|
||||
<Tooltip.Trigger>
|
||||
<Button
|
||||
ref={ref}
|
||||
onMouseOver={() => {
|
||||
setIsTooltipOpen(true);
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
setIsTooltipOpen(false);
|
||||
}}
|
||||
onClick={onClick}
|
||||
icon={<TrashBinIcon />}
|
||||
variant="secondary"
|
||||
data-test-id="delete-categories-button"
|
||||
/>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content side="bottom">
|
||||
<Tooltip.Arrow />
|
||||
{children}
|
||||
</Tooltip.Content>
|
||||
</Tooltip>
|
||||
);
|
||||
});
|
1
src/categories/components/CategoryDeleteButton/index.ts
Normal file
1
src/categories/components/CategoryDeleteButton/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export * from "./CategoryDeleteButton";
|
|
@ -1,218 +0,0 @@
|
|||
// @ts-strict-ignore
|
||||
import {
|
||||
CategoryListUrlSortField,
|
||||
categoryUrl,
|
||||
} from "@dashboard/categories/urls";
|
||||
import Checkbox from "@dashboard/components/Checkbox";
|
||||
import ResponsiveTable from "@dashboard/components/ResponsiveTable";
|
||||
import Skeleton from "@dashboard/components/Skeleton";
|
||||
import TableCellHeader from "@dashboard/components/TableCellHeader";
|
||||
import TableHead from "@dashboard/components/TableHead";
|
||||
import { TablePaginationWithContext } from "@dashboard/components/TablePagination";
|
||||
import TableRowLink from "@dashboard/components/TableRowLink";
|
||||
import { CategoryFragment } from "@dashboard/graphql";
|
||||
import { maybe, renderCollection } from "@dashboard/misc";
|
||||
import { ListActions, ListProps, SortPage } from "@dashboard/types";
|
||||
import { getArrowDirection } from "@dashboard/utils/sort";
|
||||
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",
|
||||
},
|
||||
colProducts: {
|
||||
width: 160,
|
||||
},
|
||||
colSubcategories: {
|
||||
width: 160,
|
||||
},
|
||||
},
|
||||
colName: {
|
||||
paddingLeft: 0,
|
||||
},
|
||||
colProducts: {
|
||||
textAlign: "center",
|
||||
},
|
||||
colSubcategories: {
|
||||
textAlign: "center",
|
||||
},
|
||||
tableRow: {
|
||||
cursor: "pointer",
|
||||
},
|
||||
}),
|
||||
{ name: "CategoryList" },
|
||||
);
|
||||
|
||||
interface CategoryListProps
|
||||
extends ListProps,
|
||||
ListActions,
|
||||
SortPage<CategoryListUrlSortField> {
|
||||
categories?: CategoryFragment[];
|
||||
isRoot: boolean;
|
||||
}
|
||||
|
||||
const CategoryList: React.FC<CategoryListProps> = props => {
|
||||
const {
|
||||
categories,
|
||||
disabled,
|
||||
settings,
|
||||
sort,
|
||||
isChecked,
|
||||
isRoot,
|
||||
selected,
|
||||
toggle,
|
||||
toggleAll,
|
||||
toolbar,
|
||||
onUpdateListSettings,
|
||||
onSort,
|
||||
} = props;
|
||||
|
||||
const classes = useStyles(props);
|
||||
const numberOfColumns = categories?.length === 0 ? 3 : 4;
|
||||
|
||||
return (
|
||||
<ResponsiveTable>
|
||||
<TableHead
|
||||
colSpan={numberOfColumns}
|
||||
selected={selected}
|
||||
disabled={disabled}
|
||||
items={categories}
|
||||
toggleAll={toggleAll}
|
||||
toolbar={toolbar}
|
||||
>
|
||||
<TableCellHeader
|
||||
direction={
|
||||
isRoot && sort.sort === CategoryListUrlSortField.name
|
||||
? getArrowDirection(sort.asc)
|
||||
: undefined
|
||||
}
|
||||
arrowPosition="right"
|
||||
className={classes.colName}
|
||||
disabled={!isRoot}
|
||||
onClick={() => isRoot && onSort(CategoryListUrlSortField.name)}
|
||||
>
|
||||
<FormattedMessage id="vEYtiq" defaultMessage="Category Name" />
|
||||
</TableCellHeader>
|
||||
<TableCellHeader
|
||||
direction={
|
||||
isRoot && sort.sort === CategoryListUrlSortField.subcategoryCount
|
||||
? getArrowDirection(sort.asc)
|
||||
: undefined
|
||||
}
|
||||
className={classes.colSubcategories}
|
||||
disabled={!isRoot}
|
||||
onClick={() =>
|
||||
isRoot && onSort(CategoryListUrlSortField.subcategoryCount)
|
||||
}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="BHQrgz"
|
||||
defaultMessage="Subcategories"
|
||||
description="number of subcategories"
|
||||
/>
|
||||
</TableCellHeader>
|
||||
<TableCellHeader
|
||||
direction={
|
||||
isRoot && sort.sort === CategoryListUrlSortField.productCount
|
||||
? getArrowDirection(sort.asc)
|
||||
: undefined
|
||||
}
|
||||
className={classes.colProducts}
|
||||
disabled={!isRoot}
|
||||
onClick={() =>
|
||||
isRoot && onSort(CategoryListUrlSortField.productCount)
|
||||
}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="k8ZJ5L"
|
||||
defaultMessage="No. of Products"
|
||||
description="number of products"
|
||||
/>
|
||||
</TableCellHeader>
|
||||
</TableHead>
|
||||
<TableFooter>
|
||||
<TableRowLink>
|
||||
<TablePaginationWithContext
|
||||
colSpan={numberOfColumns}
|
||||
settings={settings}
|
||||
onUpdateListSettings={onUpdateListSettings}
|
||||
/>
|
||||
</TableRowLink>
|
||||
</TableFooter>
|
||||
<TableBody>
|
||||
{renderCollection(
|
||||
categories,
|
||||
category => {
|
||||
const isSelected = category ? isChecked(category.id) : false;
|
||||
|
||||
return (
|
||||
<TableRowLink
|
||||
className={classes.tableRow}
|
||||
hover={!!category}
|
||||
href={category && categoryUrl(category.id)}
|
||||
key={category ? category.id : "skeleton"}
|
||||
selected={isSelected}
|
||||
data-test-id={"id-" + maybe(() => category.id)}
|
||||
>
|
||||
<TableCell padding="checkbox">
|
||||
<Checkbox
|
||||
checked={isSelected}
|
||||
disabled={disabled}
|
||||
disableClickPropagation
|
||||
onChange={() => toggle(category.id)}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell className={classes.colName} data-test-id="name">
|
||||
{category && category.name ? category.name : <Skeleton />}
|
||||
</TableCell>
|
||||
<TableCell className={classes.colSubcategories}>
|
||||
{category &&
|
||||
category.children &&
|
||||
category.children.totalCount !== undefined ? (
|
||||
category.children.totalCount
|
||||
) : (
|
||||
<Skeleton />
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell className={classes.colProducts}>
|
||||
{category &&
|
||||
category.products &&
|
||||
category.products.totalCount !== undefined ? (
|
||||
category.products.totalCount
|
||||
) : (
|
||||
<Skeleton />
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRowLink>
|
||||
);
|
||||
},
|
||||
() => (
|
||||
<TableRowLink>
|
||||
<TableCell colSpan={numberOfColumns}>
|
||||
{isRoot ? (
|
||||
<FormattedMessage
|
||||
id="dM86a2"
|
||||
defaultMessage="No categories found"
|
||||
/>
|
||||
) : (
|
||||
<FormattedMessage
|
||||
id="rrbzZt"
|
||||
defaultMessage="No subcategories found"
|
||||
/>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRowLink>
|
||||
),
|
||||
)}
|
||||
</TableBody>
|
||||
</ResponsiveTable>
|
||||
);
|
||||
};
|
||||
|
||||
CategoryList.displayName = "CategoryList";
|
||||
export default CategoryList;
|
|
@ -1,2 +0,0 @@
|
|||
export { default } from "./CategoryList";
|
||||
export * from "./CategoryList";
|
|
@ -0,0 +1,129 @@
|
|||
// @ts-strict-ignore
|
||||
import {
|
||||
CategoryListUrlSortField,
|
||||
categoryUrl,
|
||||
} from "@dashboard/categories/urls";
|
||||
import ColumnPicker from "@dashboard/components/ColumnPicker";
|
||||
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 { CategoryFragment } from "@dashboard/graphql";
|
||||
import { PageListProps, SortPage } 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";
|
||||
import { messages } from "./messages";
|
||||
|
||||
interface CategoryListDatagridProps
|
||||
extends Partial<SortPage<CategoryListUrlSortField>>,
|
||||
PageListProps {
|
||||
categories?: CategoryFragment[];
|
||||
disabled: boolean;
|
||||
onSelectCategoriesIds: (ids: number[], clearSelection: () => void) => void;
|
||||
selectionActionButton?: ReactNode | null;
|
||||
hasRowHover?: boolean;
|
||||
}
|
||||
|
||||
export const CategoryListDatagrid = ({
|
||||
sort,
|
||||
onSort,
|
||||
categories,
|
||||
disabled,
|
||||
onSelectCategoriesIds,
|
||||
settings,
|
||||
onUpdateListSettings,
|
||||
selectionActionButton = null,
|
||||
hasRowHover = true,
|
||||
}: CategoryListDatagridProps) => {
|
||||
const datagridState = useDatagridChangeState();
|
||||
const intl = useIntl();
|
||||
const availableColumns = useMemo(() => getColumns(intl, sort), [intl, sort]);
|
||||
|
||||
const {
|
||||
availableColumnsChoices,
|
||||
columnChoices,
|
||||
columns,
|
||||
defaultColumns,
|
||||
onColumnMoved,
|
||||
onColumnResize,
|
||||
onColumnsChange,
|
||||
picker,
|
||||
} = useColumnsDefault(availableColumns);
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
const getCellContent = useCallback(
|
||||
createGetCellContent(categories, columns),
|
||||
[categories, columns],
|
||||
);
|
||||
|
||||
const handleHeaderClick = useCallback(
|
||||
(col: number) => {
|
||||
if (sort !== undefined) {
|
||||
onSort(columns[col].id as CategoryListUrlSortField);
|
||||
}
|
||||
},
|
||||
[columns, onSort, sort],
|
||||
);
|
||||
|
||||
const handleRowAnchor = useCallback(
|
||||
([, row]: Item) => categoryUrl(categories[row].id),
|
||||
[categories],
|
||||
);
|
||||
|
||||
return (
|
||||
<DatagridChangeStateContext.Provider value={datagridState}>
|
||||
<Datagrid
|
||||
readonly
|
||||
hasRowHover={hasRowHover}
|
||||
loading={disabled}
|
||||
columnSelect={sort !== undefined ? "single" : undefined}
|
||||
verticalBorder={col => col > 0}
|
||||
rowMarkers="checkbox"
|
||||
availableColumns={columns}
|
||||
rows={categories?.length ?? 0}
|
||||
getCellContent={getCellContent}
|
||||
getCellError={() => false}
|
||||
emptyText={intl.formatMessage(messages.noData)}
|
||||
onHeaderClicked={handleHeaderClick}
|
||||
rowAnchor={handleRowAnchor}
|
||||
menuItems={() => []}
|
||||
actionButtonPosition="right"
|
||||
selectionActions={() => selectionActionButton}
|
||||
onColumnResize={onColumnResize}
|
||||
onColumnMoved={onColumnMoved}
|
||||
onRowSelectionChange={onSelectCategoriesIds}
|
||||
renderColumnPicker={defaultProps => (
|
||||
<ColumnPicker
|
||||
{...defaultProps}
|
||||
availableColumns={availableColumnsChoices}
|
||||
initialColumns={columnChoices}
|
||||
defaultColumns={defaultColumns}
|
||||
onSave={onColumnsChange}
|
||||
hasMore={false}
|
||||
loading={false}
|
||||
onFetchMore={() => undefined}
|
||||
onQueryChange={picker.setQuery}
|
||||
query={picker.query}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Box paddingX={6}>
|
||||
<TablePaginationWithContext
|
||||
component="div"
|
||||
colSpan={1}
|
||||
settings={settings}
|
||||
disabled={disabled}
|
||||
onUpdateListSettings={onUpdateListSettings}
|
||||
/>
|
||||
</Box>
|
||||
</DatagridChangeStateContext.Provider>
|
||||
);
|
||||
};
|
67
src/categories/components/CategoryListDatagrid/datagrid.ts
Normal file
67
src/categories/components/CategoryListDatagrid/datagrid.ts
Normal file
|
@ -0,0 +1,67 @@
|
|||
// @ts-strict-ignore
|
||||
import { CategoryListUrlSortField } from "@dashboard/categories/urls";
|
||||
import { readonlyTextCell } from "@dashboard/components/Datagrid/customCells/cells";
|
||||
import { AvailableColumn } from "@dashboard/components/Datagrid/types";
|
||||
import { CategoryFragment } from "@dashboard/graphql";
|
||||
import { Sort } from "@dashboard/types";
|
||||
import { getColumnSortDirectionIcon } from "@dashboard/utils/columns/getColumnSortDirectionIcon";
|
||||
import { GridCell, Item } from "@glideapps/glide-data-grid";
|
||||
import { IntlShape } from "react-intl";
|
||||
|
||||
import { columnsMessages } from "./messages";
|
||||
|
||||
export const getColumns = (
|
||||
intl: IntlShape,
|
||||
sort?: Sort<CategoryListUrlSortField>,
|
||||
): AvailableColumn[] => [
|
||||
{
|
||||
id: "name",
|
||||
title: intl.formatMessage(columnsMessages.categoryName),
|
||||
width: 350,
|
||||
icon: sort
|
||||
? getColumnSortDirectionIcon(sort, CategoryListUrlSortField.name)
|
||||
: undefined,
|
||||
},
|
||||
{
|
||||
id: "subcategories",
|
||||
title: intl.formatMessage(columnsMessages.subcategories),
|
||||
width: 300,
|
||||
icon: sort
|
||||
? getColumnSortDirectionIcon(
|
||||
sort,
|
||||
CategoryListUrlSortField.subcategoryCount,
|
||||
)
|
||||
: undefined,
|
||||
},
|
||||
{
|
||||
id: "products",
|
||||
title: intl.formatMessage(columnsMessages.numberOfProducts),
|
||||
width: 300,
|
||||
icon: sort
|
||||
? getColumnSortDirectionIcon(sort, CategoryListUrlSortField.productCount)
|
||||
: undefined,
|
||||
},
|
||||
];
|
||||
|
||||
export const createGetCellContent =
|
||||
(categories: CategoryFragment[], columns: AvailableColumn[]) =>
|
||||
([column, row]: Item): GridCell => {
|
||||
const columnId = columns[column]?.id;
|
||||
|
||||
if (!columnId) {
|
||||
return readonlyTextCell("");
|
||||
}
|
||||
|
||||
const rowData = categories[row];
|
||||
|
||||
switch (columnId) {
|
||||
case "name":
|
||||
return readonlyTextCell(rowData?.name ?? "");
|
||||
case "subcategories":
|
||||
return readonlyTextCell(rowData?.children?.totalCount.toString() ?? "");
|
||||
case "products":
|
||||
return readonlyTextCell(rowData?.products?.totalCount.toString() ?? "");
|
||||
default:
|
||||
return readonlyTextCell("", false);
|
||||
}
|
||||
};
|
1
src/categories/components/CategoryListDatagrid/index.ts
Normal file
1
src/categories/components/CategoryListDatagrid/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export * from "./CategoryListDatagrid";
|
23
src/categories/components/CategoryListDatagrid/messages.ts
Normal file
23
src/categories/components/CategoryListDatagrid/messages.ts
Normal file
|
@ -0,0 +1,23 @@
|
|||
import { defineMessages } from "react-intl";
|
||||
|
||||
export const columnsMessages = defineMessages({
|
||||
categoryName: {
|
||||
id: "kgVqk1",
|
||||
defaultMessage: "Category name",
|
||||
},
|
||||
subcategories: {
|
||||
defaultMessage: "Subcategories",
|
||||
id: "F7DxHw",
|
||||
},
|
||||
numberOfProducts: {
|
||||
defaultMessage: "Number of products",
|
||||
id: "cLcy6F",
|
||||
},
|
||||
});
|
||||
|
||||
export const messages = defineMessages({
|
||||
noData: {
|
||||
defaultMessage: "No categories found",
|
||||
id: "dM86a2",
|
||||
},
|
||||
});
|
|
@ -25,6 +25,11 @@ const categoryTableProps: CategoryTableProps = {
|
|||
...sortPageProps.sort,
|
||||
sort: CategoryListUrlSortField.name,
|
||||
},
|
||||
onCategoriesDelete: () => undefined,
|
||||
onSelectCategoriesIds: () => undefined,
|
||||
selectedCategoriesIds: [],
|
||||
hasPresetsChanged: false,
|
||||
onTabUpdate: () => undefined,
|
||||
};
|
||||
|
||||
const meta: Meta<typeof CategoryListPage> = {
|
||||
|
@ -47,6 +52,7 @@ export const Default: Story = {
|
|||
export const Loading: Story = {
|
||||
args: {
|
||||
...categoryTableProps,
|
||||
disabled: true,
|
||||
categories: undefined,
|
||||
},
|
||||
parameters: {
|
||||
|
|
|
@ -2,32 +2,40 @@ import {
|
|||
categoryAddUrl,
|
||||
CategoryListUrlSortField,
|
||||
} from "@dashboard/categories/urls";
|
||||
import SearchInput from "@dashboard/components/AppLayout/ListFilters/components/SearchInput";
|
||||
import { TopNav } from "@dashboard/components/AppLayout/TopNav";
|
||||
import { Button } from "@dashboard/components/Button";
|
||||
import { FilterPresetsSelect } from "@dashboard/components/FilterPresetsSelect";
|
||||
import { ListPageLayout } from "@dashboard/components/Layouts";
|
||||
import SearchBar from "@dashboard/components/SearchBar";
|
||||
import { CategoryFragment } from "@dashboard/graphql";
|
||||
import { sectionNames } from "@dashboard/intl";
|
||||
import {
|
||||
ListActions,
|
||||
PageListProps,
|
||||
SearchPageProps,
|
||||
SortPage,
|
||||
TabPageProps,
|
||||
} from "@dashboard/types";
|
||||
import { Card } from "@material-ui/core";
|
||||
import React from "react";
|
||||
import { Box, ChevronRightIcon } from "@saleor/macaw-ui/next";
|
||||
import React, { useState } from "react";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
|
||||
import CategoryList from "../CategoryList";
|
||||
import { CategoryDeleteButton } from "../CategoryDeleteButton";
|
||||
import { CategoryListDatagrid } from "../CategoryListDatagrid";
|
||||
import { messages } from "./messages";
|
||||
|
||||
export interface CategoryTableProps
|
||||
extends PageListProps,
|
||||
ListActions,
|
||||
SearchPageProps,
|
||||
SortPage<CategoryListUrlSortField>,
|
||||
TabPageProps {
|
||||
Omit<TabPageProps, "onTabDelete"> {
|
||||
categories: CategoryFragment[];
|
||||
hasPresetsChanged: boolean;
|
||||
selectedCategoriesIds: string[];
|
||||
onTabDelete: (tabIndex: number) => void;
|
||||
onTabUpdate: (tabName: string) => void;
|
||||
onCategoriesDelete: () => void;
|
||||
onSelectCategoriesIds: (ids: number[], clearSelection: () => void) => void;
|
||||
}
|
||||
|
||||
export const CategoryListPage: React.FC<CategoryTableProps> = ({
|
||||
|
@ -35,69 +43,88 @@ export const CategoryListPage: React.FC<CategoryTableProps> = ({
|
|||
currentTab,
|
||||
disabled,
|
||||
initialSearch,
|
||||
isChecked,
|
||||
selected,
|
||||
settings,
|
||||
tabs,
|
||||
toggle,
|
||||
toggleAll,
|
||||
toolbar,
|
||||
onAll,
|
||||
onSearchChange,
|
||||
onTabChange,
|
||||
onTabDelete,
|
||||
onTabSave,
|
||||
onUpdateListSettings,
|
||||
onTabUpdate,
|
||||
hasPresetsChanged,
|
||||
onCategoriesDelete,
|
||||
selectedCategoriesIds,
|
||||
...listProps
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const [isFilterPresetOpen, setFilterPresetOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<ListPageLayout>
|
||||
<TopNav title={intl.formatMessage(sectionNames.categories)}>
|
||||
<TopNav
|
||||
title={intl.formatMessage(sectionNames.categories)}
|
||||
isAlignToRight={false}
|
||||
withoutBorder
|
||||
>
|
||||
<Box
|
||||
__flex={1}
|
||||
display="flex"
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
>
|
||||
<Box display="flex">
|
||||
<Box marginX={3} display="flex" alignItems="center">
|
||||
<ChevronRightIcon />
|
||||
</Box>
|
||||
|
||||
<FilterPresetsSelect
|
||||
presetsChanged={hasPresetsChanged}
|
||||
onSelect={onTabChange}
|
||||
onRemove={onTabDelete}
|
||||
onUpdate={onTabUpdate}
|
||||
savedPresets={tabs}
|
||||
activePreset={currentTab}
|
||||
onSelectAll={onAll}
|
||||
onSave={onTabSave}
|
||||
isOpen={isFilterPresetOpen}
|
||||
onOpenChange={setFilterPresetOpen}
|
||||
selectAllLabel={intl.formatMessage(messages.allCategories)}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Button
|
||||
variant="primary"
|
||||
href={categoryAddUrl()}
|
||||
data-test-id="create-category"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="vof5TR"
|
||||
defaultMessage="Create category"
|
||||
description="button"
|
||||
/>
|
||||
<FormattedMessage {...messages.createCategory} />
|
||||
</Button>
|
||||
</Box>
|
||||
</TopNav>
|
||||
<Card>
|
||||
<SearchBar
|
||||
allTabLabel={intl.formatMessage({
|
||||
id: "vy7fjd",
|
||||
defaultMessage: "All Categories",
|
||||
description: "tab name",
|
||||
})}
|
||||
currentTab={currentTab}
|
||||
<Box
|
||||
display="flex"
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
paddingX={6}
|
||||
marginBottom={2}
|
||||
>
|
||||
<Box __width="320px">
|
||||
<SearchInput
|
||||
initialSearch={initialSearch}
|
||||
searchPlaceholder={intl.formatMessage({
|
||||
id: "JiXNEV",
|
||||
defaultMessage: "Search Category",
|
||||
})}
|
||||
tabs={tabs}
|
||||
onAll={onAll}
|
||||
placeholder={intl.formatMessage(messages.searchCategory)}
|
||||
onSearchChange={onSearchChange}
|
||||
onTabChange={onTabChange}
|
||||
onTabDelete={onTabDelete}
|
||||
onTabSave={onTabSave}
|
||||
/>
|
||||
<CategoryList
|
||||
categories={categories}
|
||||
</Box>
|
||||
{selectedCategoriesIds.length > 0 && (
|
||||
<CategoryDeleteButton onClick={onCategoriesDelete}>
|
||||
<FormattedMessage {...messages.bulkCategoryDelete} />
|
||||
</CategoryDeleteButton>
|
||||
)}
|
||||
</Box>
|
||||
<CategoryListDatagrid
|
||||
disabled={disabled}
|
||||
isChecked={isChecked}
|
||||
isRoot={true}
|
||||
selected={selected}
|
||||
settings={settings}
|
||||
toggle={toggle}
|
||||
toggleAll={toggleAll}
|
||||
toolbar={toolbar}
|
||||
onUpdateListSettings={onUpdateListSettings}
|
||||
categories={categories}
|
||||
hasRowHover={!isFilterPresetOpen}
|
||||
{...listProps}
|
||||
/>
|
||||
</Card>
|
||||
|
|
21
src/categories/components/CategoryListPage/messages.ts
Normal file
21
src/categories/components/CategoryListPage/messages.ts
Normal file
|
@ -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",
|
||||
},
|
||||
});
|
|
@ -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<CategoryDetailsQuery["category"]["products"]>;
|
||||
}
|
||||
|
||||
export const CategoryProductList: React.FC<
|
||||
CategoryProductListProps
|
||||
> = props => {
|
||||
const {
|
||||
disabled,
|
||||
isChecked,
|
||||
products,
|
||||
selected,
|
||||
toggle,
|
||||
toggleAll,
|
||||
toolbar,
|
||||
} = props;
|
||||
|
||||
const classes = useStyles(props);
|
||||
|
||||
const numberOfColumns = 2;
|
||||
|
||||
return (
|
||||
<div className={classes.tableContainer}>
|
||||
<ResponsiveTable className={classes.table}>
|
||||
<colgroup>
|
||||
<col />
|
||||
<col className={classes.colName} />
|
||||
</colgroup>
|
||||
<TableHead
|
||||
colSpan={numberOfColumns}
|
||||
selected={selected}
|
||||
disabled={disabled}
|
||||
items={products}
|
||||
toggleAll={toggleAll}
|
||||
toolbar={toolbar}
|
||||
>
|
||||
<TableCell className={classes.colName}>
|
||||
<span className={classes.colNameHeader}>
|
||||
<FormattedMessage
|
||||
id="VQLIXd"
|
||||
defaultMessage="Name"
|
||||
description="product"
|
||||
/>
|
||||
</span>
|
||||
</TableCell>
|
||||
</TableHead>
|
||||
<TableFooter>
|
||||
<TableRowLink>
|
||||
<TablePaginationWithContext colSpan={numberOfColumns} />
|
||||
</TableRowLink>
|
||||
</TableFooter>
|
||||
<TableBody>
|
||||
{renderCollection(
|
||||
products,
|
||||
product => {
|
||||
const isSelected = product ? isChecked(product.id) : false;
|
||||
|
||||
return (
|
||||
<TableRowLink
|
||||
data-test-id="product-row"
|
||||
selected={isSelected}
|
||||
hover={!!product}
|
||||
key={product ? product.id : "skeleton"}
|
||||
href={product && productUrl(product.id)}
|
||||
className={classes.link}
|
||||
>
|
||||
<TableCell padding="checkbox">
|
||||
<Checkbox
|
||||
checked={isSelected}
|
||||
disabled={disabled}
|
||||
disableClickPropagation
|
||||
onChange={() => toggle(product.id)}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCellAvatar
|
||||
className={classes.colName}
|
||||
thumbnail={maybe(() => product.thumbnail.url)}
|
||||
>
|
||||
{product ? product.name : <Skeleton />}
|
||||
</TableCellAvatar>
|
||||
</TableRowLink>
|
||||
);
|
||||
},
|
||||
() => (
|
||||
<TableRowLink>
|
||||
<TableCell colSpan={numberOfColumns}>
|
||||
<FormattedMessage
|
||||
id="Q1Uzbb"
|
||||
defaultMessage="No products found"
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRowLink>
|
||||
),
|
||||
)}
|
||||
</TableBody>
|
||||
</ResponsiveTable>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
CategoryProductList.displayName = "CategoryProductList";
|
||||
export default CategoryProductList;
|
|
@ -1,2 +0,0 @@
|
|||
export { default } from "./CategoryProductList";
|
||||
export * from "./CategoryProductList";
|
|
@ -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<CategoryDetailsQuery["category"]["products"]>;
|
||||
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 (
|
||||
<DatagridChangeStateContext.Provider value={datagridState}>
|
||||
<Datagrid
|
||||
hasRowHover
|
||||
readonly
|
||||
actionButtonPosition="right"
|
||||
loading={disabled}
|
||||
verticalBorder={false}
|
||||
rowMarkers="checkbox"
|
||||
availableColumns={columns}
|
||||
rows={products?.length ?? 0}
|
||||
getCellContent={getCellContent}
|
||||
getCellError={() => false}
|
||||
emptyText={intl.formatMessage({
|
||||
defaultMessage: "No products found",
|
||||
id: "Q1Uzbb",
|
||||
})}
|
||||
rowAnchor={handleRowAnchor}
|
||||
menuItems={() => []}
|
||||
selectionActions={() => selectionActionButton}
|
||||
onColumnResize={onColumnResize}
|
||||
onColumnMoved={onColumnMoved}
|
||||
onRowSelectionChange={onSelectProductsIds}
|
||||
/>
|
||||
|
||||
<Box paddingX={6}>
|
||||
<TablePaginationWithContext
|
||||
component="div"
|
||||
colSpan={1}
|
||||
settings={settings}
|
||||
disabled={disabled}
|
||||
onUpdateListSettings={onUpdateListSettings}
|
||||
/>
|
||||
</Box>
|
||||
</DatagridChangeStateContext.Provider>
|
||||
);
|
||||
};
|
|
@ -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<CategoryDetailsQuery["category"]["products"]>,
|
||||
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);
|
||||
}
|
||||
};
|
|
@ -0,0 +1 @@
|
|||
export * from "./CategoryProductListDatagrid";
|
|
@ -0,0 +1,8 @@
|
|||
import { defineMessages } from "react-intl";
|
||||
|
||||
export const columnsMessages = defineMessages({
|
||||
name: {
|
||||
id: "HAlOn1",
|
||||
defaultMessage: "Name",
|
||||
},
|
||||
});
|
|
@ -1,57 +1,50 @@
|
|||
// @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<CategoryDetailsQuery["category"]["products"]>;
|
||||
categoryName: string;
|
||||
interface CategoryProductsProps {
|
||||
category: CategoryDetailsQuery["category"];
|
||||
categoryId: string;
|
||||
products: RelayToFlat<CategoryDetailsQuery["category"]["products"]>;
|
||||
disabled: boolean;
|
||||
onProductsDelete: () => void;
|
||||
onSelectProductsIds: (ids: number[], clearSelection: () => void) => void;
|
||||
}
|
||||
|
||||
export const CategoryProducts: React.FC<CategoryProductsProps> = ({
|
||||
export const CategoryProducts = ({
|
||||
category,
|
||||
categoryId,
|
||||
products,
|
||||
disabled,
|
||||
categoryId,
|
||||
categoryName,
|
||||
isChecked,
|
||||
selected,
|
||||
toggle,
|
||||
toggleAll,
|
||||
toolbar,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const classes = useStyles();
|
||||
onProductsDelete,
|
||||
onSelectProductsIds,
|
||||
}: CategoryProductsProps) => (
|
||||
<DashboardCard>
|
||||
<DashboardCard.Title>
|
||||
<Box display="flex" justifyContent="space-between" alignItems="center">
|
||||
<FormattedMessage
|
||||
id="+43JV5"
|
||||
defaultMessage="Products in {categoryName}"
|
||||
description="header"
|
||||
values={{ categoryName: category?.name }}
|
||||
/>
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardTitle
|
||||
title={intl.formatMessage(
|
||||
{
|
||||
id: "+43JV5",
|
||||
defaultMessage: "Products in {categoryName}",
|
||||
description: "header",
|
||||
},
|
||||
{ categoryName },
|
||||
)}
|
||||
toolbar={
|
||||
<div className={classes.toolbar}>
|
||||
<Box display="flex" gap={4}>
|
||||
<InternalLink
|
||||
to={productListUrl({
|
||||
categories: [categoryId],
|
||||
})}
|
||||
>
|
||||
<Button variant="tertiary" data-test-id="view-products">
|
||||
<Button variant="secondary" data-test-id="view-products">
|
||||
<FormattedMessage
|
||||
id="z8jo8h"
|
||||
defaultMessage="View products"
|
||||
|
@ -59,33 +52,34 @@ export const CategoryProducts: React.FC<CategoryProductsProps> = ({
|
|||
/>
|
||||
</Button>
|
||||
</InternalLink>
|
||||
<HorizontalSpacer />
|
||||
<Button
|
||||
variant="tertiary"
|
||||
href={productAddUrl()}
|
||||
data-test-id="add-products"
|
||||
>
|
||||
|
||||
<InternalLink to={productAddUrl()}>
|
||||
<Button variant="secondary" data-test-id="add-products">
|
||||
<FormattedMessage
|
||||
id="x/pIZ9"
|
||||
defaultMessage="Add product"
|
||||
description="button"
|
||||
/>
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
<CategoryProductList
|
||||
</InternalLink>
|
||||
</Box>
|
||||
</Box>
|
||||
</DashboardCard.Title>
|
||||
|
||||
<CategoryProductListDatagrid
|
||||
products={products}
|
||||
disabled={disabled}
|
||||
selected={selected}
|
||||
isChecked={isChecked}
|
||||
toggle={toggle}
|
||||
toggleAll={toggleAll}
|
||||
toolbar={toolbar}
|
||||
onSelectProductsIds={onSelectProductsIds}
|
||||
selectionActionButton={
|
||||
<Box paddingRight={5}>
|
||||
<CategoryDeleteButton onClick={onProductsDelete}>
|
||||
<FormattedMessage
|
||||
defaultMessage="Bulk products delete"
|
||||
id="cxOmce"
|
||||
/>
|
||||
</Card>
|
||||
</CategoryDeleteButton>
|
||||
</Box>
|
||||
}
|
||||
/>
|
||||
</DashboardCard>
|
||||
);
|
||||
};
|
||||
|
||||
CategoryProducts.displayName = "CategoryProducts";
|
||||
export default CategoryProducts;
|
||||
|
|
|
@ -1 +1 @@
|
|||
export { default } from "./CategoryProducts";
|
||||
export * from "./CategoryProducts";
|
||||
|
|
|
@ -1,10 +0,0 @@
|
|||
import { makeStyles } from "@saleor/macaw-ui";
|
||||
|
||||
export const useStyles = makeStyles(
|
||||
() => ({
|
||||
toolbar: {
|
||||
display: "flex",
|
||||
},
|
||||
}),
|
||||
{ name: "CategoryProducts" },
|
||||
);
|
|
@ -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<CategoryDetailsQuery["category"]["children"]>;
|
||||
onCategoriesDelete: () => void;
|
||||
onSelectCategoriesIds: (ids: number[], clearSelection: () => void) => void;
|
||||
}
|
||||
|
||||
export const CategorySubcategories = ({
|
||||
categoryId,
|
||||
subcategories,
|
||||
disabled,
|
||||
onCategoriesDelete,
|
||||
onSelectCategoriesIds,
|
||||
}: CategorySubcategoriesProps) => (
|
||||
<DashboardCard>
|
||||
<DashboardCard.Title>
|
||||
<Box display="flex" justifyContent="space-between" alignItems="center">
|
||||
<FormattedMessage
|
||||
id="NivJal"
|
||||
defaultMessage="All Subcategories"
|
||||
description="section header"
|
||||
/>
|
||||
<InternalLink to={categoryAddUrl(categoryId)}>
|
||||
<Button variant="secondary" data-test-id="create-subcategory">
|
||||
<FormattedMessage
|
||||
id="UycVMp"
|
||||
defaultMessage="Create subcategory"
|
||||
description="button"
|
||||
/>
|
||||
</Button>
|
||||
</InternalLink>
|
||||
</Box>
|
||||
</DashboardCard.Title>
|
||||
|
||||
<CategoryListDatagrid
|
||||
categories={subcategories}
|
||||
disabled={disabled}
|
||||
onSelectCategoriesIds={onSelectCategoriesIds}
|
||||
selectionActionButton={
|
||||
<Box paddingRight={5}>
|
||||
<CategoryDeleteButton onClick={onCategoriesDelete}>
|
||||
<FormattedMessage
|
||||
defaultMessage="Bulk categories delete"
|
||||
id="ZN5IZl"
|
||||
/>
|
||||
</CategoryDeleteButton>
|
||||
</Box>
|
||||
}
|
||||
/>
|
||||
</DashboardCard>
|
||||
);
|
1
src/categories/components/CategorySubcategories/index.ts
Normal file
1
src/categories/components/CategorySubcategories/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export * from "./CategorySubcategories";
|
|
@ -26,11 +26,13 @@ const updateProps: Omit<CategoryUpdatePageProps, "classes"> = {
|
|||
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,
|
||||
};
|
||||
|
||||
|
|
|
@ -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<CategoryUpdatePageProps> = ({
|
|||
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<CategoryUpdatePageProps> = ({
|
|||
errors={errors}
|
||||
onChange={change}
|
||||
/>
|
||||
|
||||
<CardSpacer />
|
||||
|
||||
<CategoryBackground
|
||||
data={data}
|
||||
onImageUpload={onImageUpload}
|
||||
|
@ -108,7 +104,9 @@ export const CategoryUpdatePage: React.FC<CategoryUpdatePageProps> = ({
|
|||
image={maybe(() => category.backgroundImage)}
|
||||
onChange={change}
|
||||
/>
|
||||
|
||||
<CardSpacer />
|
||||
|
||||
<SeoForm
|
||||
helperText={intl.formatMessage({
|
||||
id: "wQdR8M",
|
||||
|
@ -126,9 +124,13 @@ export const CategoryUpdatePage: React.FC<CategoryUpdatePageProps> = ({
|
|||
onChange={change}
|
||||
disabled={disabled}
|
||||
/>
|
||||
|
||||
<CardSpacer />
|
||||
|
||||
<Metadata data={data} onChange={handlers.changeMetadata} />
|
||||
|
||||
<CardSpacer />
|
||||
|
||||
<TabContainer className={sprinkles({ paddingX: 9 })}>
|
||||
<CategoriesTab
|
||||
isActive={currentTab === CategoryPageTab.categories}
|
||||
|
@ -140,6 +142,7 @@ export const CategoryUpdatePage: React.FC<CategoryUpdatePageProps> = ({
|
|||
description="number of subcategories in category"
|
||||
/>
|
||||
</CategoriesTab>
|
||||
|
||||
<ProductsTab
|
||||
testId="products-tab"
|
||||
isActive={currentTab === CategoryPageTab.products}
|
||||
|
@ -152,56 +155,30 @@ export const CategoryUpdatePage: React.FC<CategoryUpdatePageProps> = ({
|
|||
/>
|
||||
</ProductsTab>
|
||||
</TabContainer>
|
||||
|
||||
<CardSpacer />
|
||||
|
||||
{currentTab === CategoryPageTab.categories && (
|
||||
<Card>
|
||||
<CardTitle
|
||||
title={intl.formatMessage({
|
||||
id: "NivJal",
|
||||
defaultMessage: "All Subcategories",
|
||||
description: "section header",
|
||||
})}
|
||||
toolbar={
|
||||
<Button
|
||||
variant="tertiary"
|
||||
href={categoryAddUrl(categoryId)}
|
||||
data-test-id="create-subcategory"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="UycVMp"
|
||||
defaultMessage="Create subcategory"
|
||||
description="button"
|
||||
/>
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
<CategoryList
|
||||
categories={subcategories}
|
||||
<CategorySubcategories
|
||||
disabled={disabled}
|
||||
isChecked={isChecked}
|
||||
isRoot={false}
|
||||
selected={selected}
|
||||
sort={undefined}
|
||||
toggle={toggle}
|
||||
toggleAll={toggleAll}
|
||||
toolbar={subcategoryListToolbar}
|
||||
onSort={() => undefined}
|
||||
subcategories={subcategories}
|
||||
onCategoriesDelete={onCategoriesDelete}
|
||||
onSelectCategoriesIds={onSelectCategoriesIds}
|
||||
categoryId={categoryId}
|
||||
/>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{currentTab === CategoryPageTab.products && (
|
||||
<CategoryProducts
|
||||
categoryId={category?.id}
|
||||
categoryName={category?.name}
|
||||
category={category}
|
||||
categoryId={categoryId}
|
||||
products={products}
|
||||
disabled={disabled}
|
||||
toggle={toggle}
|
||||
toggleAll={toggleAll}
|
||||
selected={selected}
|
||||
isChecked={isChecked}
|
||||
toolbar={productListToolbar}
|
||||
onProductsDelete={onProductsDelete}
|
||||
onSelectProductsIds={onSelectProductsIds}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Savebar
|
||||
onCancel={() => navigate(backHref)}
|
||||
onDelete={onDelete}
|
||||
|
|
|
@ -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<CategoryDetailsProps> = ({
|
|||
}) => {
|
||||
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>(
|
||||
CategoryPageTab.categories,
|
||||
);
|
||||
|
@ -80,7 +93,8 @@ export const CategoryDetails: React.FC<CategoryDetailsProps> = ({
|
|||
);
|
||||
const paginate = useLocalPaginator(setPaginationState);
|
||||
const changeTab = (tab: CategoryPageTab) => {
|
||||
reset();
|
||||
clearProductRowSelection();
|
||||
clearCategryRowSelection();
|
||||
setActiveTab(tab);
|
||||
};
|
||||
|
||||
|
@ -90,6 +104,8 @@ export const CategoryDetails: React.FC<CategoryDetailsProps> = ({
|
|||
});
|
||||
|
||||
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<CategoryDetailsProps> = ({
|
|||
defaultMessage: "Category deleted",
|
||||
}),
|
||||
});
|
||||
clearProductRowSelection();
|
||||
navigate(categoryListUrl());
|
||||
}
|
||||
};
|
||||
|
@ -109,6 +126,7 @@ export const CategoryDetails: React.FC<CategoryDetailsProps> = ({
|
|||
});
|
||||
|
||||
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<CategoryDetailsProps> = ({
|
|||
});
|
||||
|
||||
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<CategoryDetailsProps> = ({
|
|||
const [productBulkDelete, productBulkDeleteOpts] =
|
||||
useProductBulkDeleteMutation({
|
||||
onCompleted: data => {
|
||||
clearProductRowSelection();
|
||||
if (data.productBulkDelete.errors.length === 0) {
|
||||
closeModal();
|
||||
notify({
|
||||
|
@ -158,7 +177,6 @@ export const CategoryDetails: React.FC<CategoryDetailsProps> = ({
|
|||
text: intl.formatMessage(commonMessages.savedChanges),
|
||||
});
|
||||
refetch();
|
||||
reset();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
@ -194,6 +212,52 @@ export const CategoryDetails: React.FC<CategoryDetailsProps> = ({
|
|||
}),
|
||||
);
|
||||
|
||||
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<CategoryDetailsProps> = ({
|
|||
})
|
||||
}
|
||||
onSubmit={handleSubmit}
|
||||
products={mapEdgesToItems(data?.category?.products)}
|
||||
products={products}
|
||||
saveButtonBarState={updateResult.status}
|
||||
subcategories={mapEdgesToItems(data?.category?.children)}
|
||||
subcategoryListToolbar={
|
||||
<IconButton
|
||||
data-test-id="delete-icon"
|
||||
variant="secondary"
|
||||
color="primary"
|
||||
onClick={() =>
|
||||
openModal("delete-categories", {
|
||||
ids: listElements,
|
||||
})
|
||||
}
|
||||
>
|
||||
<DeleteIcon />
|
||||
</IconButton>
|
||||
}
|
||||
productListToolbar={
|
||||
<IconButton
|
||||
data-test-id="delete-icon"
|
||||
color="primary"
|
||||
onClick={() =>
|
||||
openModal("delete-products", {
|
||||
ids: listElements,
|
||||
})
|
||||
}
|
||||
>
|
||||
<DeleteIcon />
|
||||
</IconButton>
|
||||
}
|
||||
isChecked={isSelected}
|
||||
selected={listElements.length}
|
||||
toggle={toggle}
|
||||
toggleAll={toggleAll}
|
||||
subcategories={subcategories}
|
||||
onSelectCategoriesIds={handleSetSelectedCategoryIds}
|
||||
onSelectProductsIds={handleSetSelectedPrductIds}
|
||||
onCategoriesDelete={() => {
|
||||
openModal("delete-categories");
|
||||
}}
|
||||
onProductsDelete={() => {
|
||||
openModal("delete-products");
|
||||
}}
|
||||
/>
|
||||
|
||||
<ActionDialog
|
||||
confirmButtonState={deleteResult.status}
|
||||
onClose={closeModal}
|
||||
|
@ -303,16 +345,14 @@ export const CategoryDetails: React.FC<CategoryDetailsProps> = ({
|
|||
/>
|
||||
</DialogContentText>
|
||||
</ActionDialog>
|
||||
|
||||
<ActionDialog
|
||||
open={
|
||||
params.action === "delete-categories" &&
|
||||
maybe(() => 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<CategoryDetailsProps> = ({
|
|||
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: (
|
||||
<strong>{maybe(() => params.ids.length)}</strong>
|
||||
<strong>{maybe(() => selectedCategoryRowIds.length)}</strong>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
|
@ -341,13 +381,14 @@ export const CategoryDetails: React.FC<CategoryDetailsProps> = ({
|
|||
/>
|
||||
</DialogContentText>
|
||||
</ActionDialog>
|
||||
|
||||
<ActionDialog
|
||||
open={params.action === "delete-products"}
|
||||
confirmButtonState={productBulkDeleteOpts.status}
|
||||
onClose={closeModal}
|
||||
onConfirm={() =>
|
||||
productBulkDelete({
|
||||
variables: { ids: params.ids },
|
||||
variables: { ids: selectedProductRowIds },
|
||||
}).then(() => refetch())
|
||||
}
|
||||
title={intl.formatMessage({
|
||||
|
@ -362,9 +403,9 @@ export const CategoryDetails: React.FC<CategoryDetailsProps> = ({
|
|||
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: (
|
||||
<strong>{maybe(() => params.ids.length)}</strong>
|
||||
<strong>{maybe(() => selectedProductRowIds.length)}</strong>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
|
|
|
@ -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<CategoryListProps> = ({ 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<CategoryListProps> = ({ 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 (
|
||||
<PaginatorContext.Provider value={paginationValues}>
|
||||
<CategoryListPage
|
||||
hasPresetsChanged={hasPresetsChange()}
|
||||
categories={mapEdgesToItems(data?.categories)}
|
||||
currentTab={currentTab}
|
||||
currentTab={selectedPreset}
|
||||
initialSearch={params.query || ""}
|
||||
onSearchChange={query => 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={
|
||||
<IconButton
|
||||
variant="secondary"
|
||||
color="primary"
|
||||
data-test-id="delete-icon"
|
||||
onClick={() =>
|
||||
openModal("delete", {
|
||||
ids: listElements,
|
||||
})
|
||||
}
|
||||
>
|
||||
<DeleteIcon />
|
||||
</IconButton>
|
||||
}
|
||||
onUpdateListSettings={(...props) => {
|
||||
clearRowSelection();
|
||||
updateListSettings(...props);
|
||||
}}
|
||||
selectedCategoriesIds={selectedRowIds}
|
||||
onSelectCategoriesIds={handleSetSelectedCategoryIds}
|
||||
onCategoriesDelete={() => openModal("delete")}
|
||||
/>
|
||||
|
||||
<ActionDialog
|
||||
confirmButtonState={categoryBulkDeleteOpts.status}
|
||||
onClose={() =>
|
||||
|
@ -189,7 +194,7 @@ export const CategoryList: React.FC<CategoryListProps> = ({ params }) => {
|
|||
onConfirm={() =>
|
||||
categoryBulkDelete({
|
||||
variables: {
|
||||
ids: params.ids,
|
||||
ids: selectedRowIds,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
@ -220,18 +225,20 @@ export const CategoryList: React.FC<CategoryListProps> = ({ params }) => {
|
|||
/>
|
||||
</DialogContentText>
|
||||
</ActionDialog>
|
||||
|
||||
<SaveFilterTabDialog
|
||||
open={params.action === "save-search"}
|
||||
confirmButtonState="default"
|
||||
onClose={closeModal}
|
||||
onSubmit={handleTabSave}
|
||||
onSubmit={onPresetSave}
|
||||
/>
|
||||
|
||||
<DeleteFilterTabDialog
|
||||
open={params.action === "delete-search"}
|
||||
confirmButtonState="default"
|
||||
onClose={closeModal}
|
||||
onSubmit={handleTabDelete}
|
||||
tabName={maybe(() => tabs[currentTab - 1].name, "...")}
|
||||
onSubmit={onPresetDelete}
|
||||
tabName={presets[presetIdToDelete - 1]?.name ?? "..."}
|
||||
/>
|
||||
</PaginatorContext.Provider>
|
||||
);
|
||||
|
|
|
@ -20,16 +20,9 @@ export function getFilterVariables(
|
|||
};
|
||||
}
|
||||
|
||||
export const {
|
||||
deleteFilterTab,
|
||||
getFilterTabs,
|
||||
saveFilterTab,
|
||||
} = createFilterTabUtils<CategoryListUrlFilters>(CATEGORY_FILTERS_KEY);
|
||||
export const storageUtils = createFilterTabUtils<string>(CATEGORY_FILTERS_KEY);
|
||||
|
||||
export const {
|
||||
areFiltersApplied,
|
||||
getActiveFilters,
|
||||
getFiltersCurrentTab,
|
||||
} = createFilterUtils<CategoryListUrlQueryParams, CategoryListUrlFilters>(
|
||||
export const { areFiltersApplied, getActiveFilters, getFiltersCurrentTab } =
|
||||
createFilterUtils<CategoryListUrlQueryParams, CategoryListUrlFilters>(
|
||||
CategoryListUrlFiltersEnum,
|
||||
);
|
||||
|
|
|
@ -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<DatagridProps> = ({
|
|||
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<DataEditorRef | null>(null);
|
||||
|
@ -266,7 +269,9 @@ export const Datagrid: React.FC<DatagridProps> = ({
|
|||
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 <a> element when hovering over rows in the datagrid
|
||||
|
@ -330,12 +335,11 @@ export const Datagrid: React.FC<DatagridProps> = ({
|
|||
return undefined;
|
||||
}
|
||||
|
||||
const overrideTheme = {
|
||||
const overrideTheme: Partial<Theme> = {
|
||||
bgCell:
|
||||
themeValues.colors.background.interactiveNeutralSecondaryHovering,
|
||||
bgCellMedium:
|
||||
themeValues.colors.background.interactiveNeutralSecondaryHovering,
|
||||
accentLight: undefined as string | undefined,
|
||||
};
|
||||
|
||||
if (readonly) {
|
||||
|
@ -476,7 +480,9 @@ export const Datagrid: React.FC<DatagridProps> = ({
|
|||
<CardContent classes={{ root: classes.cardContentRoot }}>
|
||||
{rowsTotal > 0 || showEmptyDatagrid ? (
|
||||
<>
|
||||
{selection?.rows && selection?.rows.length > 0 && (
|
||||
{selection?.rows &&
|
||||
selection?.rows.length > 0 &&
|
||||
selectionActionsComponent && (
|
||||
<div className={classes.actionBtnBar}>
|
||||
{selectionActionsComponent}
|
||||
</div>
|
||||
|
|
|
@ -10,7 +10,7 @@ interface RowActionsProps {
|
|||
}
|
||||
|
||||
export const RowActions = ({ menuItems, disabled }: RowActionsProps) => {
|
||||
const classes = useStyles();
|
||||
const classes = useStyles({});
|
||||
const hasSingleMenuItem = menuItems.length === 1;
|
||||
const firstMenuItem = menuItems[0];
|
||||
|
||||
|
|
|
@ -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],
|
||||
|
|
|
@ -48,6 +48,7 @@ export const FilterPresetsSelect = ({
|
|||
const intl = useIntl();
|
||||
const showUpdateButton =
|
||||
presetsChanged && savedPresets.length > 0 && activePreset;
|
||||
|
||||
const showSaveButton = presetsChanged;
|
||||
|
||||
const getLabel = () => {
|
||||
|
|
Loading…
Reference in a new issue