@handled-ai/design-system 0.16.0 → 0.16.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. package/dist/components/data-table-condition-filter.d.ts +37 -0
  2. package/dist/components/data-table-condition-filter.js +424 -0
  3. package/dist/components/data-table-condition-filter.js.map +1 -0
  4. package/dist/components/data-table-filter.d.ts +12 -1
  5. package/dist/components/data-table-filter.js +91 -20
  6. package/dist/components/data-table-filter.js.map +1 -1
  7. package/dist/components/data-table-toolbar.d.ts +1 -0
  8. package/dist/components/data-table.d.ts +1 -0
  9. package/dist/index.d.ts +1 -0
  10. package/dist/index.js +1 -0
  11. package/dist/index.js.map +1 -1
  12. package/dist/prototype/index.d.ts +1 -0
  13. package/dist/prototype/prototype-accounts-view.d.ts +1 -0
  14. package/dist/prototype/prototype-admin-view.d.ts +1 -0
  15. package/dist/prototype/prototype-config.d.ts +1 -0
  16. package/dist/prototype/prototype-inbox-view.d.ts +4 -1
  17. package/dist/prototype/prototype-inbox-view.js +6 -1
  18. package/dist/prototype/prototype-inbox-view.js.map +1 -1
  19. package/dist/prototype/prototype-insights-view.d.ts +1 -0
  20. package/dist/prototype/prototype-shell.d.ts +1 -0
  21. package/package.json +1 -1
  22. package/src/components/__tests__/contact-list.test.tsx +1 -1
  23. package/src/components/__tests__/data-table-condition-filter.test.tsx +423 -0
  24. package/src/components/__tests__/data-table-filter-presets.test.tsx +1 -1
  25. package/src/components/__tests__/data-table-filter.test.tsx +291 -3
  26. package/src/components/data-table-condition-filter.tsx +541 -0
  27. package/src/components/data-table-filter.tsx +101 -19
  28. package/src/index.ts +1 -0
  29. package/src/prototype/__tests__/detail-view-attention.test.tsx +101 -0
  30. package/src/prototype/prototype-inbox-view.tsx +8 -0
@@ -1,9 +1,80 @@
1
- import { describe, it, expect } from "vitest";
1
+ import { describe, it, expect, vi } from "vitest";
2
2
  import React from "react";
3
- import { render } from "@testing-library/react";
4
- import { DataTableFilter } from "../data-table-filter";
3
+ import { render, screen, fireEvent } from "@testing-library/react";
5
4
  import { ListFilter } from "lucide-react";
6
5
 
6
+ import { DataTableFilter } from "../data-table-filter";
7
+ import type { DataTableFilterCategory } from "../data-table-filter";
8
+ import type { ConditionFieldDef } from "../data-table-condition-filter";
9
+
10
+ /**
11
+ * Radix DropdownMenu renders content inside a portal which makes it
12
+ * difficult to test in happy-dom. We mock the DropdownMenu primitives
13
+ * to render children inline so we can assert on the component's
14
+ * rendering logic directly.
15
+ */
16
+ vi.mock("../dropdown-menu", async () => {
17
+ const ReactMod = await import("react");
18
+
19
+ /* eslint-disable @typescript-eslint/no-explicit-any */
20
+ const DropdownMenu = ({ children, ...props }: any) =>
21
+ ReactMod.createElement("div", { "data-slot": "dropdown-menu", ...props }, children);
22
+ const DropdownMenuTrigger = ({ children, asChild, ...props }: any) => {
23
+ if (asChild && ReactMod.isValidElement(children)) {
24
+ return ReactMod.cloneElement(children, { "data-slot": "dropdown-menu-trigger", ...props } as any);
25
+ }
26
+ return ReactMod.createElement("button", { "data-slot": "dropdown-menu-trigger", ...props }, children);
27
+ };
28
+ const DropdownMenuContent = ({ children, ...props }: any) =>
29
+ ReactMod.createElement("div", { "data-slot": "dropdown-menu-content", ...props }, children);
30
+ const DropdownMenuItem = ({ children, onSelect, className, ...props }: any) =>
31
+ ReactMod.createElement(
32
+ "div",
33
+ {
34
+ "data-slot": "dropdown-menu-item",
35
+ role: "menuitem",
36
+ className,
37
+ onClick: (e: any) => onSelect?.(e),
38
+ ...props,
39
+ },
40
+ children
41
+ );
42
+ const DropdownMenuSub = ({ children, ...props }: any) =>
43
+ ReactMod.createElement("div", { "data-slot": "dropdown-menu-sub", ...props }, children);
44
+ const DropdownMenuSubTrigger = ({ children, className, ...props }: any) =>
45
+ ReactMod.createElement("div", { "data-slot": "dropdown-menu-sub-trigger", role: "menuitem", className, ...props }, children);
46
+ const DropdownMenuSubContent = ({ children, ...props }: any) =>
47
+ ReactMod.createElement("div", { "data-slot": "dropdown-menu-sub-content", ...props }, children);
48
+ /* eslint-enable @typescript-eslint/no-explicit-any */
49
+
50
+ return {
51
+ DropdownMenu,
52
+ DropdownMenuTrigger,
53
+ DropdownMenuContent,
54
+ DropdownMenuItem,
55
+ DropdownMenuSub,
56
+ DropdownMenuSubTrigger,
57
+ DropdownMenuSubContent,
58
+ };
59
+ });
60
+
61
+ vi.mock("radix-ui", async (importOriginal) => {
62
+ const actual = await importOriginal<typeof import("radix-ui")>();
63
+ const ReactMod = await import("react");
64
+ /* eslint-disable @typescript-eslint/no-explicit-any */
65
+ const Root = ({ children }: any) => ReactMod.createElement("div", { "data-slot": "popover-root" }, children);
66
+ const Trigger = ({ children, asChild, ...props }: any) => {
67
+ if (asChild && ReactMod.isValidElement(children)) {
68
+ return ReactMod.cloneElement(children, { "data-slot": "popover-trigger", ...props } as any);
69
+ }
70
+ return ReactMod.createElement("button", { "data-slot": "popover-trigger", ...props }, children);
71
+ };
72
+ const Portal = ({ children }: any) => ReactMod.createElement(ReactMod.Fragment, null, children);
73
+ const Content = ({ children, ...props }: any) => ReactMod.createElement("div", { "data-slot": "popover-content", ...props }, children);
74
+ /* eslint-enable @typescript-eslint/no-explicit-any */
75
+ return { ...actual, Popover: { Root, Trigger, Portal, Content } };
76
+ });
77
+
7
78
  const defaultProps = {
8
79
  categories: [
9
80
  {
@@ -38,4 +109,221 @@ describe("DataTableFilter", () => {
38
109
  expect(button!.className).toContain("text-primary-foreground");
39
110
  expect(button!.className).toContain("h-8");
40
111
  });
112
+
113
+ it("renders boolean category as direct toggle row", () => {
114
+ const onToggle = vi.fn();
115
+ const boolCategory: DataTableFilterCategory = {
116
+ id: "archived",
117
+ label: "Archived",
118
+ icon: ListFilter,
119
+ options: [],
120
+ type: "boolean",
121
+ };
122
+
123
+ render(
124
+ <DataTableFilter
125
+ categories={[boolCategory]}
126
+ selectedFilters={{}}
127
+ onToggleFilter={onToggle}
128
+ />
129
+ );
130
+
131
+ const item = screen.getByText("Archived").closest('[data-slot="dropdown-menu-item"]');
132
+ expect(item).not.toBeNull();
133
+ expect(document.querySelector('[data-slot="dropdown-menu-sub-trigger"]')).toBeNull();
134
+
135
+ fireEvent.click(item!);
136
+ expect(onToggle).toHaveBeenCalledWith("archived", "true");
137
+ });
138
+
139
+ it("shows check icon on active boolean category", () => {
140
+ const boolCategory: DataTableFilterCategory = {
141
+ id: "archived",
142
+ label: "Archived",
143
+ icon: ListFilter,
144
+ options: [],
145
+ type: "boolean",
146
+ };
147
+
148
+ render(
149
+ <DataTableFilter
150
+ categories={[boolCategory]}
151
+ selectedFilters={{ archived: ["true"] }}
152
+ onToggleFilter={() => {}}
153
+ />
154
+ );
155
+
156
+ const item = screen.getByText("Archived").closest('[data-slot="dropdown-menu-item"]');
157
+ expect(item).not.toBeNull();
158
+ expect(item!.className).toContain("text-brand-purple");
159
+ expect(item!.querySelector("svg.ml-auto")).not.toBeNull();
160
+ });
161
+
162
+ it("renders single-select category with submenu", () => {
163
+ const singleCategory: DataTableFilterCategory = {
164
+ id: "priority",
165
+ label: "Priority",
166
+ icon: ListFilter,
167
+ options: ["High", "Medium", "Low"],
168
+ type: "single",
169
+ };
170
+
171
+ render(
172
+ <DataTableFilter
173
+ categories={[singleCategory]}
174
+ selectedFilters={{}}
175
+ onToggleFilter={() => {}}
176
+ />
177
+ );
178
+
179
+ const subTrigger = document.querySelector('[data-slot="dropdown-menu-sub-trigger"]');
180
+ expect(subTrigger).not.toBeNull();
181
+ expect(subTrigger!.textContent).toContain("Priority");
182
+ });
183
+
184
+ it("shows filled circle indicator for selected single-select option", () => {
185
+ const singleCategory: DataTableFilterCategory = {
186
+ id: "priority",
187
+ label: "Priority",
188
+ icon: ListFilter,
189
+ options: ["High", "Medium", "Low"],
190
+ type: "single",
191
+ };
192
+
193
+ render(
194
+ <DataTableFilter
195
+ categories={[singleCategory]}
196
+ selectedFilters={{ priority: ["High"] }}
197
+ onToggleFilter={() => {}}
198
+ />
199
+ );
200
+
201
+ const highItem = screen.getByText("High").closest('[data-slot="dropdown-menu-item"]');
202
+ expect(highItem).not.toBeNull();
203
+ expect(highItem!.querySelector("span.rounded-full")).not.toBeNull();
204
+ expect(highItem!.textContent).not.toContain("Applied");
205
+
206
+ const mediumItem = screen.getByText("Medium").closest('[data-slot="dropdown-menu-item"]');
207
+ expect(mediumItem).not.toBeNull();
208
+ expect(mediumItem!.querySelector("span.rounded-full")).toBeNull();
209
+ });
210
+
211
+ it("boolean category keeps dropdown open on click", () => {
212
+ const onToggle = vi.fn();
213
+ const boolCategory: DataTableFilterCategory = {
214
+ id: "archived",
215
+ label: "Archived",
216
+ icon: ListFilter,
217
+ options: [],
218
+ type: "boolean",
219
+ };
220
+
221
+ render(
222
+ <DataTableFilter
223
+ categories={[boolCategory]}
224
+ selectedFilters={{}}
225
+ onToggleFilter={onToggle}
226
+ />
227
+ );
228
+
229
+ const item = screen.getByText("Archived").closest('[data-slot="dropdown-menu-item"]');
230
+ expect(item).not.toBeNull();
231
+
232
+ const preventDefaultSpy = vi.fn();
233
+ const mockEvent = new MouseEvent("click", { bubbles: true });
234
+ Object.defineProperty(mockEvent, "preventDefault", { value: preventDefaultSpy });
235
+ item!.dispatchEvent(mockEvent);
236
+
237
+ expect(preventDefaultSpy).toHaveBeenCalled();
238
+ expect(onToggle).toHaveBeenCalledWith("archived", "true");
239
+ });
240
+
241
+ it("multi-select category still works as before", () => {
242
+ const multiCategory: DataTableFilterCategory = {
243
+ id: "status",
244
+ label: "Status",
245
+ icon: ListFilter,
246
+ options: ["Open", "Closed"],
247
+ };
248
+
249
+ render(
250
+ <DataTableFilter
251
+ categories={[multiCategory]}
252
+ selectedFilters={{ status: ["Open"] }}
253
+ onToggleFilter={() => {}}
254
+ />
255
+ );
256
+
257
+ const subTrigger = document.querySelector('[data-slot="dropdown-menu-sub-trigger"]');
258
+ expect(subTrigger).not.toBeNull();
259
+ expect(subTrigger!.textContent).toContain("Status");
260
+
261
+ const openItem = screen.getByText("Open").closest('[data-slot="dropdown-menu-item"]');
262
+ expect(openItem).not.toBeNull();
263
+ expect(openItem!.textContent).toContain("Applied");
264
+ expect(openItem!.querySelector("span.rounded-full")).toBeNull();
265
+ });
266
+
267
+ it("does not expose the condition builder entry point without condition fields", () => {
268
+ render(<DataTableFilter {...defaultProps} />);
269
+
270
+ expect(screen.queryByText("Add filter")).toBeNull();
271
+ expect(document.querySelector('[data-slot="condition-filter"]')).toBeNull();
272
+ });
273
+
274
+ it("exposes a condition builder popover entry point when condition fields are provided", () => {
275
+ const conditionFields: ConditionFieldDef[] = [
276
+ { id: "balance", label: "Account Balance", type: "currency" },
277
+ ];
278
+
279
+ render(
280
+ <DataTableFilter
281
+ {...defaultProps}
282
+ conditionFields={conditionFields}
283
+ conditionFilters={[
284
+ { id: "c-1", field: "balance", operator: "gt", value: 1000 },
285
+ ]}
286
+ onConditionFiltersChange={() => {}}
287
+ />
288
+ );
289
+
290
+ const builderEntry = document.querySelector('[data-slot="popover-trigger"]');
291
+ expect(builderEntry).not.toBeNull();
292
+ expect(builderEntry!.textContent).toContain("1");
293
+ expect(document.querySelector('[data-slot="condition-filter"]')).not.toBeNull();
294
+ expect(screen.getByText("Filter builder")).toBeDefined();
295
+ });
296
+
297
+
298
+ it("opens the condition builder without closing the menu selection", () => {
299
+ render(
300
+ <DataTableFilter
301
+ {...defaultProps}
302
+ conditionFields={[{ id: "name", label: "Name", type: "text" }]}
303
+ />
304
+ );
305
+
306
+ const builderEntry = document.querySelector('[data-slot="popover-trigger"]');
307
+ expect(builderEntry).not.toBeNull();
308
+
309
+ const preventDefaultSpy = vi.fn();
310
+ const mockEvent = new MouseEvent("click", { bubbles: true });
311
+ Object.defineProperty(mockEvent, "preventDefault", { value: preventDefaultSpy });
312
+ builderEntry!.dispatchEvent(mockEvent);
313
+
314
+ expect(preventDefaultSpy).toHaveBeenCalled();
315
+ expect(screen.getByText("Filter builder")).toBeDefined();
316
+ });
317
+
318
+ it("uses the custom condition builder label when provided", () => {
319
+ render(
320
+ <DataTableFilter
321
+ {...defaultProps}
322
+ conditionFields={[{ id: "name", label: "Name", type: "text" }]}
323
+ conditionBuilderLabel="Advanced filters"
324
+ />
325
+ );
326
+
327
+ expect(screen.getByText("Advanced filters")).toBeDefined();
328
+ });
41
329
  });