Category listing datagrid (#3760)

Co-authored-by: wojteknowacki <wojciech.nowacki@saleor.io>
This commit is contained in:
Paweł Chyła 2023-07-04 09:23:27 +02:00 committed by GitHub
parent 1cb6e8b5fc
commit b4f11eff66
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
37 changed files with 997 additions and 812 deletions

View file

@ -0,0 +1,5 @@
---
"saleor-dashboard": minor
---
Introduce datagrid on category listing page

View file

@ -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);

View file

@ -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"]',
};

View file

@ -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"]',

View file

@ -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") {

View file

@ -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 its 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"

View file

@ -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>
);
});

View file

@ -0,0 +1 @@
export * from "./CategoryDeleteButton";

View file

@ -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;

View file

@ -1,2 +0,0 @@
export { default } from "./CategoryList";
export * from "./CategoryList";

View file

@ -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>
);
};

View 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);
}
};

View file

@ -0,0 +1 @@
export * from "./CategoryListDatagrid";

View 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",
},
});

View file

@ -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: {

View file

@ -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)}>
<Button
variant="primary"
href={categoryAddUrl()}
data-test-id="create-category"
<TopNav
title={intl.formatMessage(sectionNames.categories)}
isAlignToRight={false}
withoutBorder
>
<Box
__flex={1}
display="flex"
justifyContent="space-between"
alignItems="center"
>
<FormattedMessage
id="vof5TR"
defaultMessage="Create category"
description="button"
/>
</Button>
<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 {...messages.createCategory} />
</Button>
</Box>
</TopNav>
<Card>
<SearchBar
allTabLabel={intl.formatMessage({
id: "vy7fjd",
defaultMessage: "All Categories",
description: "tab name",
})}
currentTab={currentTab}
initialSearch={initialSearch}
searchPlaceholder={intl.formatMessage({
id: "JiXNEV",
defaultMessage: "Search Category",
})}
tabs={tabs}
onAll={onAll}
onSearchChange={onSearchChange}
onTabChange={onTabChange}
onTabDelete={onTabDelete}
onTabSave={onTabSave}
/>
<CategoryList
categories={categories}
<Box
display="flex"
justifyContent="space-between"
alignItems="center"
paddingX={6}
marginBottom={2}
>
<Box __width="320px">
<SearchInput
initialSearch={initialSearch}
placeholder={intl.formatMessage(messages.searchCategory)}
onSearchChange={onSearchChange}
/>
</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>

View 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",
},
});

View file

@ -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;

View file

@ -1,2 +0,0 @@
export { default } from "./CategoryProductList";
export * from "./CategoryProductList";

View file

@ -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>
);
};

View file

@ -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);
}
};

View file

@ -0,0 +1 @@
export * from "./CategoryProductListDatagrid";

View file

@ -0,0 +1,8 @@
import { defineMessages } from "react-intl";
export const columnsMessages = defineMessages({
name: {
id: "HAlOn1",
defaultMessage: "Name",
},
});

View file

@ -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<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}>
<InternalLink
to={productListUrl({
categories: [categoryId],
})}
>
<Button variant="tertiary" data-test-id="view-products">
<FormattedMessage
id="z8jo8h"
defaultMessage="View products"
description="button"
/>
</Button>
</InternalLink>
<HorizontalSpacer />
<Button
variant="tertiary"
href={productAddUrl()}
data-test-id="add-products"
>
<Box display="flex" gap={4}>
<InternalLink
to={productListUrl({
categories: [categoryId],
})}
>
<Button variant="secondary" data-test-id="view-products">
<FormattedMessage
id="z8jo8h"
defaultMessage="View products"
description="button"
/>
</Button>
</InternalLink>
<InternalLink to={productAddUrl()}>
<Button variant="secondary" data-test-id="add-products">
<FormattedMessage
id="x/pIZ9"
defaultMessage="Add product"
description="button"
/>
</Button>
</div>
}
/>
<CategoryProductList
products={products}
disabled={disabled}
selected={selected}
isChecked={isChecked}
toggle={toggle}
toggleAll={toggleAll}
toolbar={toolbar}
/>
</Card>
);
};
</InternalLink>
</Box>
</Box>
</DashboardCard.Title>
CategoryProducts.displayName = "CategoryProducts";
export default CategoryProducts;
<CategoryProductListDatagrid
products={products}
disabled={disabled}
onSelectProductsIds={onSelectProductsIds}
selectionActionButton={
<Box paddingRight={5}>
<CategoryDeleteButton onClick={onProductsDelete}>
<FormattedMessage
defaultMessage="Bulk products delete"
id="cxOmce"
/>
</CategoryDeleteButton>
</Box>
}
/>
</DashboardCard>
);

View file

@ -1 +1 @@
export { default } from "./CategoryProducts";
export * from "./CategoryProducts";

View file

@ -1,10 +0,0 @@
import { makeStyles } from "@saleor/macaw-ui";
export const useStyles = makeStyles(
() => ({
toolbar: {
display: "flex",
},
}),
{ name: "CategoryProducts" },
);

View file

@ -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>
);

View file

@ -0,0 +1 @@
export * from "./CategorySubcategories";

View file

@ -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,
};

View file

@ -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}
disabled={disabled}
isChecked={isChecked}
isRoot={false}
selected={selected}
sort={undefined}
toggle={toggle}
toggleAll={toggleAll}
toolbar={subcategoryListToolbar}
onSort={() => undefined}
/>
</Card>
)}
{currentTab === CategoryPageTab.products && (
<CategoryProducts
categoryId={category?.id}
categoryName={category?.name}
products={products}
<CategorySubcategories
disabled={disabled}
toggle={toggle}
toggleAll={toggleAll}
selected={selected}
isChecked={isChecked}
toolbar={productListToolbar}
subcategories={subcategories}
onCategoriesDelete={onCategoriesDelete}
onSelectCategoriesIds={onSelectCategoriesIds}
categoryId={categoryId}
/>
)}
{currentTab === CategoryPageTab.products && (
<CategoryProducts
category={category}
categoryId={categoryId}
products={products}
disabled={disabled}
onProductsDelete={onProductsDelete}
onSelectProductsIds={onSelectProductsIds}
/>
)}
<Savebar
onCancel={() => navigate(backHref)}
onDelete={onDelete}

View file

@ -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>
),
}}
/>

View file

@ -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>
);

View file

@ -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>(
CategoryListUrlFiltersEnum,
);
export const { areFiltersApplied, getActiveFilters, getFiltersCurrentTab } =
createFilterUtils<CategoryListUrlQueryParams, CategoryListUrlFilters>(
CategoryListUrlFiltersEnum,
);

View file

@ -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,11 +480,13 @@ export const Datagrid: React.FC<DatagridProps> = ({
<CardContent classes={{ root: classes.cardContentRoot }}>
{rowsTotal > 0 || showEmptyDatagrid ? (
<>
{selection?.rows && selection?.rows.length > 0 && (
<div className={classes.actionBtnBar}>
{selectionActionsComponent}
</div>
)}
{selection?.rows &&
selection?.rows.length > 0 &&
selectionActionsComponent && (
<div className={classes.actionBtnBar}>
{selectionActionsComponent}
</div>
)}
<div className={classes.editorContainer}>
<Box
backgroundColor="plain"

View file

@ -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];

View file

@ -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],

View file

@ -48,6 +48,7 @@ export const FilterPresetsSelect = ({
const intl = useIntl();
const showUpdateButton =
presetsChanged && savedPresets.length > 0 && activePreset;
const showSaveButton = presetsChanged;
const getLabel = () => {