Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/short-teeth-film.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"saleor-dashboard": minor
---

Added expandable rows to the Category list with lazy loading of subcategories and improved nested selection logic.
6 changes: 6 additions & 0 deletions locale/defaultMessages.json
Original file line number Diff line number Diff line change
Expand Up @@ -1843,6 +1843,9 @@
"context": "dialog content",
"string": "{counter,plural,one{Are you sure you want to publish this model?} other{Are you sure you want to publish {displayQuantity} models?}}"
},
"8zbcLs": {
"string": "№ of subcategories"
},
"9+iLpf": {
"context": "link text to product type settings",
"string": "Configure in product type settings"
Expand Down Expand Up @@ -10319,6 +10322,9 @@
"context": "consent to send gift card to different address checkbox label",
"string": "Yes, I want to send gift card to different address"
},
"v0fBOU": {
"string": "Collapse all"
},
"v17Lly": {
"context": "label",
"string": "Max Delivery Time"
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,7 @@
"eslint-plugin-react-refresh": "^0.4.24",
"eslint-plugin-react-you-might-not-need-an-effect": "^0.5.6",
"eslint-plugin-simple-import-sort": "^12.1.1",
"eslint-plugin-storybook": "10.2.4",
"eslint-plugin-storybook": "^10.2.7",
"eslint-plugin-unicorn": "^61.0.2",
"eslint-plugin-unused-imports": "^4.3.0",
"front-matter": "^4.0.2",
Expand Down
12 changes: 6 additions & 6 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
import { categoryUrl } from "@dashboard/categories/urls";
import { CategoryFragment } from "@dashboard/graphql";
import { CompactSelection, GridSelection } from "@glideapps/glide-data-grid";
import { render } from "@testing-library/react";

import { CategoryListDatagrid } from "./CategoryListDatagrid";

const navigateMock = jest.fn();
let datagridProps: Record<string, any> = {};

jest.mock("@dashboard/hooks/useNavigator", () => () => navigateMock);
jest.mock("@dashboard/hooks/useBackLinkWithState", () => ({
getPrevLocationState: () => undefined,
}));
jest.mock("@dashboard/components/Datagrid/ColumnPicker/ColumnPicker", () => ({
ColumnPicker: () => null,
}));
jest.mock("@dashboard/components/TablePagination", () => ({
DatagridPagination: () => null,
}));
jest.mock("@dashboard/components/Datagrid/Datagrid", () => ({
__esModule: true,
default: (props: Record<string, any>) => {
datagridProps = props;

return null;
},
}));
jest.mock("@dashboard/components/Datagrid/ColumnPicker/useColumns", () => ({
useColumns: () => ({
handlers: {
onMove: jest.fn(),
onResize: jest.fn(),
onToggle: jest.fn(),
},
selectedColumns: [],
staticColumns: [],
visibleColumns: [
{ id: "name", title: "Name", width: 320 },
{ id: "subcategories", title: "Subcategories", width: 160 },
{ id: "products", title: "Products", width: 160 },
],
}),
}));
jest.mock("react-router", () => ({
useLocation: () => ({
pathname: "/categories/",
search: "",
hash: "",
state: undefined,
}),
}));

const createCategory = (id: string, childrenCount: number): CategoryFragment =>
({
__typename: "Category",
id,
name: `Category ${id}`,
children: {
__typename: "CategoryCountableConnection",
totalCount: childrenCount,
},
products: {
__typename: "ProductCountableConnection",
totalCount: 10,
},
}) as CategoryFragment;

const baseProps = {
disabled: false,
settings: {
rowNumber: 20,
columns: [],
},
categories: [createCategory("cat-1", 1), createCategory("cat-2", 0), createCategory("cat-3", 2)],
onSelectCategoriesIds: jest.fn(),
onUpdateListSettings: jest.fn(),
};

describe("CategoryListDatagrid", () => {
beforeEach(() => {
datagridProps = {};
navigateMock.mockReset();
});

it("should map selected category ids to controlled row selection", () => {
// Arrange
render(
<CategoryListDatagrid
{...baseProps}
selectedCategoriesIds={["cat-2", "missing-id"]}
onSelectedCategoriesIdsChange={jest.fn()}
/>,
);

// Act
const selectedRows = datagridProps.controlledSelection.rows.toArray();

// Assert
expect(selectedRows).toEqual([1]);
expect(typeof datagridProps.onControlledSelectionChange).toBe("function");
});

it("should map controlled row selection back to category ids", () => {
// Arrange
const onSelectedCategoriesIdsChange = jest.fn();

render(
<CategoryListDatagrid
{...baseProps}
selectedCategoriesIds={[]}
onSelectedCategoriesIdsChange={onSelectedCategoriesIdsChange}
/>,
);

const selection: GridSelection = {
columns: CompactSelection.empty(),
rows: CompactSelection.empty().add(0).add(2),
};

// Act
datagridProps.onControlledSelectionChange(selection);

// Assert
expect(onSelectedCategoriesIdsChange).toHaveBeenCalledWith(["cat-1", "cat-3"]);
});

it("should trigger expand toggle only for expandable non-loading rows", () => {
// Arrange
const onCategoryExpandToggle = jest.fn();

render(
<CategoryListDatagrid
{...baseProps}
isCategoryExpanded={() => false}
isCategoryChildrenLoading={categoryId => categoryId === "cat-3"}
onCategoryExpandToggle={onCategoryExpandToggle}
/>,
);

// Act
datagridProps.onRowClick([0, 0]);
datagridProps.onRowClick([0, 1]);
datagridProps.onRowClick([0, 2]);

// Assert
expect(onCategoryExpandToggle).toHaveBeenCalledTimes(1);
expect(onCategoryExpandToggle).toHaveBeenCalledWith("cat-1");
});

it("should navigate to details when clicking non-expand columns", () => {
// Arrange
render(<CategoryListDatagrid {...baseProps} />);

// Act
datagridProps.onRowClick([0, 0]);

// Assert
expect(navigateMock).toHaveBeenCalledWith(categoryUrl("cat-1"));
});
});
Loading