Skip to content

Commit b5e262e

Browse files
mirekmEugenBodanovlkostrowski
authored
Expandable subcategories (continued) (#6733)
* feat(datagrid): add optional controlled row selection API * feat(selection): extend row selection hook with partial update helpers * feat(categories): add expandable categories with lazy child loading and targeted cache invalidation * chore(vite): allow overriding dev server port via PORT_DEVSERVER * chore(deps): bump eslint-plugin-storybook to ^10.2.7 * chore: update translations * Add changeset * feat(categories): add ripple for expandable rows * style(categories): replace ASCII arrows with Unicode chevrons * feat(categories): persist expanded tree state and add collapse-all action * test(categories): add test coverage for expandable category list behavior * test(core): cover row selection helpers and vite dev server port override * feat(categories): replace expand glyphs with custom datagrid chevron cell * chore: add type declaration for vite.config.js * test(categories): add test coverage for ChevronCell * test(datagrid): simplify chevron draw assertions * test(ripples): assert unique ripple ids * chore: run linter auto-fix * removed vite.config.test * fix: add optional chaining to cell.data * test(datagrid): move chevron visual coverage to storybook * refactor(categories): extract CategoryList logic into hooks, services, and utils * refactor: replace prop drilling with Jotai atoms * Add paginated expandable category tree with inline load more Replace the merchant page-size control with cursor-based child fetching, shared tree behavior on category detail, Saleor throbber loading state. * Polish category tree pagination UX and shared utilities Extract tree indent helper, read paginated Apollo cache before network fetches, and split initial-expand vs load-more loading so parents keep their chevron while additional pages load. * Extract messages * Extract messages * Fix check-types failures after main merge * Fix strict typecheck and Storybook coverage for category tree * Use patch bump type in changeset --------- Co-authored-by: Yevhenii Bodanov <124465640+EugenBodanov@users.noreply.github.com> Co-authored-by: Lukasz Ostrowski <lukasz.ostrowski@saleor.io>
1 parent 8300fe8 commit b5e262e

55 files changed

Lines changed: 3928 additions & 203 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.changeset/short-teeth-film.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"saleor-dashboard": patch
3+
---
4+
5+
Added expandable rows to the Category list with lazy loading of subcategories and improved nested selection logic.

locale/defaultMessages.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9237,6 +9237,10 @@
92379237
"context": "order refund amount",
92389238
"string": "Replaced Products Value"
92399239
},
9240+
"i8FLQQ": {
9241+
"context": "load more subcategories in category tree",
9242+
"string": "Load {count, plural, one {# more subcategory} other {# more subcategories}}"
9243+
},
92409244
"iAaYh4": {
92419245
"context": "Info box description for unavailable products",
92429246
"string": "Customers can view this product but cannot add it to cart."
@@ -11992,6 +11996,9 @@
1199211996
"context": "Summary of diagnostic issues found",
1199311997
"string": "{errorCount, plural, =0 {} one {# problem} other {# problems}}{hasWarnings, select, true {{hasErrors, select, true {, } other {}}} other {}}{warningCount, plural, =0 {} one {# warning} other {# warnings}} found"
1199411998
},
11999+
"v0fBOU": {
12000+
"string": "Collapse all"
12001+
},
1199512002
"v17EX7": {
1199612003
"context": "built-in attribute list filter preset",
1199712004
"string": "Model attributes"

src/categories/components/CategoryListDatagrid/CategoryListDatagrid.stories.tsx

Lines changed: 58 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,63 @@
11
import { categories } from "@dashboard/categories/fixtures";
2+
import { type CategoryListRow } from "@dashboard/categories/views/CategoryList/types";
23
import type { Meta, StoryObj } from "@storybook/react-vite";
34
import { fn } from "storybook/test";
45

56
import { STORYBOOK_CHROMATIC_PARAMS } from "../../../storybook/chromatic";
67
import { CategoryListDatagrid } from "./CategoryListDatagrid";
78

9+
const rootRows: CategoryListRow[] = categories.map(category => ({
10+
type: "category" as const,
11+
category,
12+
depth: 0,
13+
parentId: null,
14+
}));
15+
16+
const expandedRows: CategoryListRow[] = [
17+
rootRows[0],
18+
{
19+
type: "category",
20+
category: {
21+
...categories[0],
22+
id: "child-1",
23+
name: "Phones",
24+
children: { __typename: "CategoryCountableConnection", totalCount: 0 },
25+
products: { __typename: "ProductCountableConnection", totalCount: 12 },
26+
},
27+
depth: 1,
28+
parentId: categories[0].id,
29+
},
30+
{
31+
type: "category",
32+
category: {
33+
...categories[0],
34+
id: "child-2",
35+
name: "Laptops",
36+
children: { __typename: "CategoryCountableConnection", totalCount: 0 },
37+
products: { __typename: "ProductCountableConnection", totalCount: 8 },
38+
},
39+
depth: 1,
40+
parentId: categories[0].id,
41+
},
42+
{
43+
type: "load-more",
44+
parentId: categories[0].id,
45+
depth: 1,
46+
remainingCount: 25,
47+
},
48+
...rootRows.slice(1),
49+
];
50+
851
const meta: Meta<typeof CategoryListDatagrid> = {
952
title: "Categories/CategoryListDatagrid",
1053
component: CategoryListDatagrid,
1154

1255
args: {
13-
categories,
56+
rows: rootRows,
1457
disabled: false,
1558
sort: { sort: "name" as any, asc: true },
1659
onSort: fn(),
17-
settings: { columns: ["name", "subcategories", "products"], rowsPerPage: 20 },
60+
settings: { columns: ["name", "subcategories", "products"], rowNumber: 20 },
1861
onUpdateListSettings: fn(),
1962
onSelectCategoriesIds: fn(),
2063
},
@@ -33,7 +76,7 @@ export const Disabled: Story = {
3376
};
3477

3578
export const Empty: Story = {
36-
args: { categories: [] },
79+
args: { rows: [] },
3780
};
3881

3982
export const WithoutSort: Story = {
@@ -42,3 +85,15 @@ export const WithoutSort: Story = {
4285
onSort: undefined,
4386
},
4487
};
88+
89+
export const ExpandedSubcategories: Story = {
90+
args: {
91+
rows: expandedRows,
92+
isCategoryExpanded: (categoryId: string) => categoryId === categories[0].id,
93+
onCategoryExpandToggle: fn(),
94+
getCategoryDepth: (categoryId: string) =>
95+
expandedRows.find(row => row.type === "category" && row.category.id === categoryId)?.depth ??
96+
0,
97+
onLoadMoreSubcategories: fn(),
98+
},
99+
};
Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
import { categoryUrl } from "@dashboard/categories/urls";
2+
import { type CategoryFragment } from "@dashboard/graphql";
3+
import { CompactSelection, type GridSelection } from "@glideapps/glide-data-grid";
4+
import { render } from "@testing-library/react";
5+
6+
import { CategoryListDatagrid } from "./CategoryListDatagrid";
7+
8+
const navigateMock = jest.fn();
9+
let datagridProps: Record<string, any> = {};
10+
11+
jest.mock("@dashboard/hooks/useNavigator", () => () => navigateMock);
12+
jest.mock("@dashboard/hooks/useBackLinkWithState", () => ({
13+
getPrevLocationState: () => undefined,
14+
}));
15+
jest.mock("@dashboard/components/Datagrid/ColumnPicker/ColumnPicker", () => ({
16+
ColumnPicker: () => null,
17+
}));
18+
jest.mock("@dashboard/components/TablePagination", () => ({
19+
DatagridPagination: () => null,
20+
}));
21+
jest.mock("@dashboard/components/Datagrid/Datagrid", () => ({
22+
Datagrid: (props: Record<string, any>) => {
23+
datagridProps = props;
24+
25+
return null;
26+
},
27+
}));
28+
jest.mock("@dashboard/components/Datagrid/ColumnPicker/useColumns", () => ({
29+
useColumns: () => ({
30+
handlers: {
31+
onMove: jest.fn(),
32+
onResize: jest.fn(),
33+
onToggle: jest.fn(),
34+
},
35+
selectedColumns: [],
36+
staticColumns: [],
37+
visibleColumns: [
38+
{ id: "name", title: "Name", width: 320 },
39+
{ id: "subcategories", title: "Subcategories", width: 160 },
40+
{ id: "products", title: "Products", width: 160 },
41+
],
42+
}),
43+
}));
44+
jest.mock("react-router", () => ({
45+
useLocation: () => ({
46+
pathname: "/categories/",
47+
search: "",
48+
hash: "",
49+
state: undefined,
50+
}),
51+
}));
52+
jest.mock("@saleor/macaw-ui-next", () => ({
53+
useTheme: () => ({
54+
themeValues: {
55+
fontWeight: { regular: 400 },
56+
fontSize: { 2: "12px", 3: "14px" },
57+
colors: {
58+
background: { default1: "#ffffff" },
59+
text: { default2: "#666666", accent1: "#2e47ba" },
60+
},
61+
},
62+
}),
63+
}));
64+
65+
const createCategory = (id: string, childrenCount: number): CategoryFragment =>
66+
({
67+
__typename: "Category",
68+
id,
69+
name: `Category ${id}`,
70+
children: {
71+
__typename: "CategoryCountableConnection",
72+
totalCount: childrenCount,
73+
},
74+
products: {
75+
__typename: "ProductCountableConnection",
76+
totalCount: 10,
77+
},
78+
}) as CategoryFragment;
79+
80+
const makeCategoryRow = (category: CategoryFragment) => ({
81+
type: "category" as const,
82+
category,
83+
depth: 0,
84+
parentId: null,
85+
});
86+
87+
const baseProps = {
88+
disabled: false,
89+
settings: {
90+
rowNumber: 20,
91+
columns: [],
92+
},
93+
rows: [
94+
makeCategoryRow(createCategory("cat-1", 1)),
95+
makeCategoryRow(createCategory("cat-2", 0)),
96+
makeCategoryRow(createCategory("cat-3", 2)),
97+
],
98+
onSelectCategoriesIds: jest.fn(),
99+
onUpdateListSettings: jest.fn(),
100+
};
101+
102+
describe("CategoryListDatagrid", () => {
103+
beforeEach(() => {
104+
datagridProps = {};
105+
navigateMock.mockReset();
106+
});
107+
108+
it("should map selected category ids to controlled row selection", () => {
109+
// Arrange
110+
render(
111+
<CategoryListDatagrid
112+
{...baseProps}
113+
selectedCategoriesIds={["cat-2", "missing-id"]}
114+
onSelectedCategoriesIdsChange={jest.fn()}
115+
/>,
116+
);
117+
118+
// Act
119+
const selectedRows = datagridProps.controlledSelection.rows.toArray();
120+
121+
// Assert
122+
expect(selectedRows).toEqual([1]);
123+
expect(typeof datagridProps.onControlledSelectionChange).toBe("function");
124+
});
125+
126+
it("should map controlled row selection back to category ids", () => {
127+
// Arrange
128+
const onSelectedCategoriesIdsChange = jest.fn();
129+
130+
render(
131+
<CategoryListDatagrid
132+
{...baseProps}
133+
selectedCategoriesIds={[]}
134+
onSelectedCategoriesIdsChange={onSelectedCategoriesIdsChange}
135+
/>,
136+
);
137+
138+
const selection: GridSelection = {
139+
columns: CompactSelection.empty(),
140+
rows: CompactSelection.empty().add(0).add(2),
141+
};
142+
143+
// Act
144+
datagridProps.onControlledSelectionChange(selection);
145+
146+
// Assert
147+
expect(onSelectedCategoriesIdsChange).toHaveBeenCalledWith(["cat-1", "cat-3"]);
148+
});
149+
150+
it("should trigger expand toggle only for expandable non-loading rows", () => {
151+
// Arrange
152+
const onCategoryExpandToggle = jest.fn();
153+
154+
render(
155+
<CategoryListDatagrid
156+
{...baseProps}
157+
isCategoryExpanded={() => false}
158+
isCategoryChildrenLoading={categoryId => categoryId === "cat-3"}
159+
onCategoryExpandToggle={onCategoryExpandToggle}
160+
/>,
161+
);
162+
163+
// Act
164+
datagridProps.onRowClick([0, 0]);
165+
datagridProps.onRowClick([0, 1]);
166+
datagridProps.onRowClick([0, 2]);
167+
168+
// Assert
169+
expect(onCategoryExpandToggle).toHaveBeenCalledTimes(1);
170+
expect(onCategoryExpandToggle).toHaveBeenCalledWith("cat-1");
171+
});
172+
173+
it("should navigate to details when clicking non-expand columns", () => {
174+
// Arrange
175+
render(<CategoryListDatagrid {...baseProps} />);
176+
177+
// Act
178+
datagridProps.onRowClick([1, 0]);
179+
180+
// Assert
181+
expect(navigateMock).toHaveBeenCalledWith(categoryUrl("cat-1"));
182+
});
183+
184+
it("should trigger load more callback when clicking load more row", () => {
185+
// Arrange
186+
const onLoadMoreSubcategories = jest.fn();
187+
188+
render(
189+
<CategoryListDatagrid
190+
{...baseProps}
191+
rows={[
192+
...baseProps.rows,
193+
{
194+
type: "load-more" as const,
195+
parentId: "cat-1",
196+
depth: 1,
197+
remainingCount: 20,
198+
},
199+
]}
200+
onLoadMoreSubcategories={onLoadMoreSubcategories}
201+
/>,
202+
);
203+
204+
// Act
205+
datagridProps.onRowClick([0, 3]);
206+
207+
// Assert
208+
expect(onLoadMoreSubcategories).toHaveBeenCalledWith("cat-1");
209+
expect(navigateMock).not.toHaveBeenCalled();
210+
});
211+
});

0 commit comments

Comments
 (0)