Skip to content

Commit 3ed50dc

Browse files
committed
feat: add tri-state sidebar filters for File Types, Workspace, Dataset
Convert SearchSidebar.js to SearchSidebar.tsx with TypeScript types and chip-driven filters. The sidebar now reads the q (search query) from Redux and routes three filter keys to new components: - mimeType → FileTypeSidebarFilter: maps ES aggregation MIME buckets to FILE_TYPE_CATEGORIES using mimeToCategory(), shows category-level tri-state checkboxes (off→include→exclude→off), with expandable sub-items showing individual MIME counts. - workspace → TriStateSidebarSection: generic tri-state section that reads/writes workspace chips via setWorkspacesInQ. - ingestion → TriStateSidebarSection: reads/writes dataset chips via setDatasetsInQ. All other filter keys continue to use the existing SearchFilter component unchanged. The updateSelectedFilters callback now strips workspace/ingestion from legacy filter updates since those are now chip-driven. Both new components use the shared triStateCycle utility from PR2 for the off→positive→negative→off cycling logic, and the TriStateCheckbox component for rendering. Fix TriStateCheckbox import path for triStateCycle (was resolving to wrong directory). Add sidebar__mime-info SCSS class for indented MIME sub-items. Add FileTypeSidebarFilter.spec.tsx with 14 tests covering buildCategoryCounts, toggleCategory, getCategoryState, and toggleCategoryExpanded.
1 parent 175c82a commit 3ed50dc

6 files changed

Lines changed: 921 additions & 166 deletions

File tree

Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
import React from "react";
2+
import FileTypeSidebarFilter, {
3+
FileTypeSidebarFilterState,
4+
} from "./FileTypeSidebarFilter";
5+
import {
6+
FILE_TYPE_CATEGORIES,
7+
mimeToCategory,
8+
} from "../Search/fileTypeCategories";
9+
10+
// ── helpers ──────────────────────────────────────────────────────────
11+
12+
function buildAgg(
13+
bucketsByPrefix: Record<string, { key: string; count: number }[]>,
14+
) {
15+
return {
16+
key: "mimeType",
17+
buckets: Object.entries(bucketsByPrefix).map(([prefix, mimes]) => ({
18+
key: prefix,
19+
count: mimes.reduce((sum, m) => sum + m.count, 0),
20+
buckets: mimes,
21+
})),
22+
};
23+
}
24+
25+
function createInstance(
26+
props: Partial<React.ComponentProps<typeof FileTypeSidebarFilter>> = {},
27+
) {
28+
const defaults = {
29+
positiveCategories: [] as string[],
30+
negativeCategories: [] as string[],
31+
onToggleCategory: jest.fn(),
32+
agg: undefined,
33+
isExpanded: true,
34+
setExpanded: jest.fn(),
35+
};
36+
const merged = { ...defaults, ...props };
37+
const inst = new FileTypeSidebarFilter(merged);
38+
Object.defineProperty(inst, "props", { value: merged, writable: true });
39+
inst.state = { expandedCategories: new Set() };
40+
return inst;
41+
}
42+
43+
// ── tests ────────────────────────────────────────────────────────────
44+
45+
describe("FileTypeSidebarFilter", () => {
46+
describe("buildCategoryCounts", () => {
47+
test("returns empty maps when no agg", () => {
48+
const inst = createInstance();
49+
const { counts, mimeCounts, uncategorisedCount } =
50+
inst.buildCategoryCounts();
51+
expect(counts.size).toBe(0);
52+
expect(mimeCounts.size).toBe(0);
53+
expect(uncategorisedCount).toBe(0);
54+
});
55+
56+
test("sums counts per category across prefixes", () => {
57+
const agg = buildAgg({
58+
"application/": [{ key: "application/vnd.ms-excel", count: 10 }],
59+
"text/": [{ key: "text/csv", count: 5 }],
60+
});
61+
const inst = createInstance({ agg });
62+
const { counts } = inst.buildCategoryCounts();
63+
expect(counts.get("spreadsheet")).toBe(15);
64+
});
65+
66+
test("tracks individual MIME counts", () => {
67+
const agg = buildAgg({
68+
"image/": [
69+
{ key: "image/jpeg", count: 20 },
70+
{ key: "image/png", count: 8 },
71+
],
72+
});
73+
const inst = createInstance({ agg });
74+
const { mimeCounts } = inst.buildCategoryCounts();
75+
expect(mimeCounts.get("image/jpeg")).toBe(20);
76+
expect(mimeCounts.get("image/png")).toBe(8);
77+
});
78+
79+
test("counts uncategorised MIMEs", () => {
80+
const agg = buildAgg({
81+
"application/": [
82+
{ key: "application/pdf", count: 5 },
83+
{ key: "application/x-custom-thing", count: 3 },
84+
{ key: "application/x-weird", count: 2 },
85+
],
86+
});
87+
const inst = createInstance({ agg });
88+
const { counts, uncategorisedCount, uncategorisedMimes } =
89+
inst.buildCategoryCounts();
90+
expect(counts.get("pdf")).toBe(5);
91+
expect(uncategorisedCount).toBe(5);
92+
expect(uncategorisedMimes).toHaveLength(2);
93+
expect(uncategorisedMimes[0]).toEqual({
94+
key: "application/x-custom-thing",
95+
count: 3,
96+
});
97+
});
98+
99+
test("all known MIMEs map to a category", () => {
100+
FILE_TYPE_CATEGORIES.forEach((cat) => {
101+
cat.mimes.forEach((mime) => {
102+
expect(mimeToCategory(mime)).toBe(cat.value);
103+
});
104+
});
105+
});
106+
});
107+
108+
describe("toggleCategory (tri-state via triStateCycle)", () => {
109+
test("off → positive: adds to positive", () => {
110+
const onToggle = jest.fn();
111+
const inst = createInstance({
112+
positiveCategories: ["pdf"],
113+
negativeCategories: [],
114+
onToggleCategory: onToggle,
115+
});
116+
inst.toggleCategory("image", {
117+
stopPropagation: jest.fn(),
118+
} as unknown as React.MouseEvent);
119+
expect(onToggle).toHaveBeenCalledWith({
120+
positive: ["pdf", "image"],
121+
negative: [],
122+
});
123+
});
124+
125+
test("positive → negative: moves from positive to negative", () => {
126+
const onToggle = jest.fn();
127+
const inst = createInstance({
128+
positiveCategories: ["pdf", "image"],
129+
negativeCategories: [],
130+
onToggleCategory: onToggle,
131+
});
132+
inst.toggleCategory("pdf", {
133+
stopPropagation: jest.fn(),
134+
} as unknown as React.MouseEvent);
135+
expect(onToggle).toHaveBeenCalledWith({
136+
positive: ["image"],
137+
negative: ["pdf"],
138+
});
139+
});
140+
141+
test("negative → off: removes from negative", () => {
142+
const onToggle = jest.fn();
143+
const inst = createInstance({
144+
positiveCategories: [],
145+
negativeCategories: ["pdf"],
146+
onToggleCategory: onToggle,
147+
});
148+
inst.toggleCategory("pdf", {
149+
stopPropagation: jest.fn(),
150+
} as unknown as React.MouseEvent);
151+
expect(onToggle).toHaveBeenCalledWith({
152+
positive: [],
153+
negative: [],
154+
});
155+
});
156+
157+
test("stopPropagation is called", () => {
158+
const stopProp = jest.fn();
159+
const inst = createInstance({ onToggleCategory: jest.fn() });
160+
inst.toggleCategory("pdf", {
161+
stopPropagation: stopProp,
162+
} as unknown as React.MouseEvent);
163+
expect(stopProp).toHaveBeenCalled();
164+
});
165+
});
166+
167+
describe("getCategoryState", () => {
168+
test("returns 'positive' for positive categories", () => {
169+
const inst = createInstance({ positiveCategories: ["pdf"] });
170+
expect(inst.getCategoryState("pdf")).toBe("positive");
171+
});
172+
173+
test("returns 'negative' for negative categories", () => {
174+
const inst = createInstance({ negativeCategories: ["image"] });
175+
expect(inst.getCategoryState("image")).toBe("negative");
176+
});
177+
178+
test("returns 'off' for unselected categories", () => {
179+
const inst = createInstance();
180+
expect(inst.getCategoryState("pdf")).toBe("off");
181+
});
182+
});
183+
184+
describe("toggleCategoryExpanded", () => {
185+
test("expands a collapsed category", () => {
186+
const inst = createInstance();
187+
let newState: FileTypeSidebarFilterState | undefined;
188+
inst.setState = ((
189+
fn: (prev: FileTypeSidebarFilterState) => FileTypeSidebarFilterState,
190+
) => {
191+
newState = fn(inst.state);
192+
}) as typeof inst.setState;
193+
inst.toggleCategoryExpanded("pdf");
194+
expect(newState!.expandedCategories.has("pdf")).toBe(true);
195+
});
196+
197+
test("collapses an expanded category", () => {
198+
const inst = createInstance();
199+
inst.state.expandedCategories.add("pdf");
200+
let newState: FileTypeSidebarFilterState | undefined;
201+
inst.setState = ((
202+
fn: (prev: FileTypeSidebarFilterState) => FileTypeSidebarFilterState,
203+
) => {
204+
newState = fn(inst.state);
205+
}) as typeof inst.setState;
206+
inst.toggleCategoryExpanded("pdf");
207+
expect(newState!.expandedCategories.has("pdf")).toBe(false);
208+
});
209+
});
210+
});

0 commit comments

Comments
 (0)