@handled-ai/design-system 0.18.51 → 0.18.53
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.
- package/dist/components/badge.d.ts +1 -1
- package/dist/components/button.d.ts +1 -1
- package/dist/components/data-table-filter.d.ts +21 -6
- package/dist/components/data-table-filter.js +134 -9
- package/dist/components/data-table-filter.js.map +1 -1
- package/dist/components/entity-panel.js +5 -5
- package/dist/components/entity-panel.js.map +1 -1
- package/dist/components/pill.d.ts +1 -1
- package/dist/components/score-why-chips.d.ts +1 -1
- package/dist/components/signal-feedback-inline.d.ts +28 -12
- package/dist/components/signal-feedback-inline.js +146 -10
- package/dist/components/signal-feedback-inline.js.map +1 -1
- package/dist/components/signal-priority-popover.d.ts +1 -1
- package/dist/components/signal-priority-popover.js +7 -16
- package/dist/components/signal-priority-popover.js.map +1 -1
- package/dist/components/tabs.d.ts +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.js.map +1 -1
- package/dist/prototype/index.d.ts +1 -1
- package/dist/prototype/prototype-accounts-view.d.ts +1 -1
- package/dist/prototype/prototype-admin-view.d.ts +1 -1
- package/dist/prototype/prototype-config.d.ts +1 -1
- package/dist/prototype/prototype-inbox-view.d.ts +3 -3
- package/dist/prototype/prototype-inbox-view.js +1 -3
- package/dist/prototype/prototype-inbox-view.js.map +1 -1
- package/dist/prototype/prototype-insights-view.d.ts +1 -1
- package/dist/prototype/prototype-shell.d.ts +1 -1
- package/dist/{signal-priority-popover-DTedstRL.d.ts → signal-priority-popover-QJngMAj7.d.ts} +4 -13
- package/package.json +1 -1
- package/src/components/__tests__/case-panel-why.test.tsx +126 -0
- package/src/components/__tests__/data-table-filter.test.tsx +130 -0
- package/src/components/__tests__/entity-metadata-grid.test.tsx +27 -1
- package/src/components/__tests__/signal-priority-popover.test.tsx +4 -41
- package/src/components/data-table-filter.tsx +160 -9
- package/src/components/entity-panel.tsx +7 -5
- package/src/components/signal-feedback-inline.tsx +181 -20
- package/src/components/signal-priority-popover.tsx +6 -19
- package/src/index.ts +1 -1
- package/src/prototype/__tests__/detail-view-opportunity-preview.test.tsx +90 -0
- package/src/prototype/__tests__/detail-view-score-why.test.tsx +0 -34
- package/src/prototype/prototype-config.ts +3 -7
- package/src/prototype/prototype-inbox-view.tsx +3 -5
|
@@ -264,6 +264,136 @@ describe("DataTableFilter", () => {
|
|
|
264
264
|
expect(openItem!.querySelector("span.rounded-full")).toBeNull();
|
|
265
265
|
});
|
|
266
266
|
|
|
267
|
+
|
|
268
|
+
it("renders a text category as a submenu with a text input", () => {
|
|
269
|
+
const textCategory: DataTableFilterCategory = {
|
|
270
|
+
id: "callsign",
|
|
271
|
+
label: "Callsign",
|
|
272
|
+
icon: ListFilter,
|
|
273
|
+
type: "text",
|
|
274
|
+
valuePlaceholder: "Enter callsign",
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
render(
|
|
278
|
+
<DataTableFilter
|
|
279
|
+
categories={[textCategory]}
|
|
280
|
+
selectedFilters={{}}
|
|
281
|
+
onToggleFilter={() => {}}
|
|
282
|
+
/>
|
|
283
|
+
);
|
|
284
|
+
|
|
285
|
+
const subTrigger = document.querySelector('[data-slot="dropdown-menu-sub-trigger"]');
|
|
286
|
+
expect(subTrigger).not.toBeNull();
|
|
287
|
+
expect(subTrigger!.textContent).toContain("Callsign");
|
|
288
|
+
expect(screen.getByLabelText("Callsign")).toBeDefined();
|
|
289
|
+
expect(screen.getByPlaceholderText("Enter callsign")).toBeDefined();
|
|
290
|
+
expect(screen.getByRole("button", { name: "Apply" })).toBeDefined();
|
|
291
|
+
expect(screen.queryByText("No matches")).toBeNull();
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
it("applies a trimmed text filter value", () => {
|
|
295
|
+
const onTextFilterChange = vi.fn();
|
|
296
|
+
const textCategory: DataTableFilterCategory = {
|
|
297
|
+
id: "callsign",
|
|
298
|
+
label: "Callsign",
|
|
299
|
+
icon: ListFilter,
|
|
300
|
+
type: "text",
|
|
301
|
+
valuePlaceholder: "Enter callsign",
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
render(
|
|
305
|
+
<DataTableFilter
|
|
306
|
+
categories={[textCategory]}
|
|
307
|
+
selectedFilters={{}}
|
|
308
|
+
onToggleFilter={() => {}}
|
|
309
|
+
textFilters={{}}
|
|
310
|
+
onTextFilterChange={onTextFilterChange}
|
|
311
|
+
/>
|
|
312
|
+
);
|
|
313
|
+
|
|
314
|
+
fireEvent.change(screen.getByLabelText("Callsign"), {
|
|
315
|
+
target: { value: " M42 " },
|
|
316
|
+
});
|
|
317
|
+
fireEvent.click(screen.getByRole("button", { name: "Apply" }));
|
|
318
|
+
|
|
319
|
+
expect(onTextFilterChange).toHaveBeenCalledWith("callsign", "M42");
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
it("clears an active text filter value", () => {
|
|
323
|
+
const onTextFilterChange = vi.fn();
|
|
324
|
+
const textCategory: DataTableFilterCategory = {
|
|
325
|
+
id: "organizationId",
|
|
326
|
+
label: "Organization ID",
|
|
327
|
+
icon: ListFilter,
|
|
328
|
+
type: "text",
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
render(
|
|
332
|
+
<DataTableFilter
|
|
333
|
+
categories={[textCategory]}
|
|
334
|
+
selectedFilters={{}}
|
|
335
|
+
onToggleFilter={() => {}}
|
|
336
|
+
textFilters={{ organizationId: "ORG-123" }}
|
|
337
|
+
onTextFilterChange={onTextFilterChange}
|
|
338
|
+
/>
|
|
339
|
+
);
|
|
340
|
+
|
|
341
|
+
expect(screen.getByLabelText("Organization ID")).toHaveProperty("value", "ORG-123");
|
|
342
|
+
fireEvent.click(screen.getByRole("button", { name: "Clear" }));
|
|
343
|
+
|
|
344
|
+
expect(onTextFilterChange).toHaveBeenCalledWith("organizationId", "");
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
it("includes active text filters in the filter count", () => {
|
|
348
|
+
const textCategory: DataTableFilterCategory = {
|
|
349
|
+
id: "callsign",
|
|
350
|
+
label: "Callsign",
|
|
351
|
+
icon: ListFilter,
|
|
352
|
+
type: "text",
|
|
353
|
+
};
|
|
354
|
+
const optionCategory: DataTableFilterCategory = {
|
|
355
|
+
id: "status",
|
|
356
|
+
label: "Status",
|
|
357
|
+
icon: ListFilter,
|
|
358
|
+
options: ["Open", "Closed"],
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
render(
|
|
362
|
+
<DataTableFilter
|
|
363
|
+
categories={[textCategory, optionCategory]}
|
|
364
|
+
selectedFilters={{ status: ["Open"] }}
|
|
365
|
+
onToggleFilter={() => {}}
|
|
366
|
+
textFilters={{ callsign: " M42 " }}
|
|
367
|
+
/>
|
|
368
|
+
);
|
|
369
|
+
|
|
370
|
+
const badge = screen.getByText("2");
|
|
371
|
+
expect(badge.tagName).toBe("SPAN");
|
|
372
|
+
expect(badge.className).toContain("bg-muted");
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
it("does not count blank text filter values as active", () => {
|
|
376
|
+
const textCategory: DataTableFilterCategory = {
|
|
377
|
+
id: "callsign",
|
|
378
|
+
label: "Callsign",
|
|
379
|
+
icon: ListFilter,
|
|
380
|
+
type: "text",
|
|
381
|
+
};
|
|
382
|
+
|
|
383
|
+
render(
|
|
384
|
+
<DataTableFilter
|
|
385
|
+
categories={[textCategory]}
|
|
386
|
+
selectedFilters={{}}
|
|
387
|
+
onToggleFilter={() => {}}
|
|
388
|
+
textFilters={{ callsign: " " }}
|
|
389
|
+
/>
|
|
390
|
+
);
|
|
391
|
+
|
|
392
|
+
const triggerButton = document.querySelector('[data-slot="dropdown-menu-trigger"]');
|
|
393
|
+
expect(triggerButton).not.toBeNull();
|
|
394
|
+
expect(triggerButton!.querySelector("span.bg-muted")).toBeNull();
|
|
395
|
+
});
|
|
396
|
+
|
|
267
397
|
it("does not expose the condition builder entry point without condition fields", () => {
|
|
268
398
|
render(<DataTableFilter {...defaultProps} />);
|
|
269
399
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, it, expect } from "vitest"
|
|
2
2
|
import React from "react"
|
|
3
|
-
import { render } from "@testing-library/react"
|
|
3
|
+
import { render, screen } from "@testing-library/react"
|
|
4
4
|
import { EntityMetadataGrid } from "../entity-panel"
|
|
5
5
|
import { CalendarDays } from "lucide-react"
|
|
6
6
|
|
|
@@ -22,4 +22,30 @@ describe("EntityMetadataGrid", () => {
|
|
|
22
22
|
expect(grid).not.toBeNull()
|
|
23
23
|
expect(grid!.className).toContain("overflow-hidden")
|
|
24
24
|
})
|
|
25
|
+
|
|
26
|
+
it("allows long labels and values to wrap within the panel", () => {
|
|
27
|
+
const { container } = render(
|
|
28
|
+
<EntityMetadataGrid
|
|
29
|
+
fields={[
|
|
30
|
+
{
|
|
31
|
+
icon: CalendarDays,
|
|
32
|
+
label: "Very Long Relationship Manager Owner Label",
|
|
33
|
+
value: (
|
|
34
|
+
<span>
|
|
35
|
+
Owner: Alexandria Cassandra Montgomery, RM: Benjamin Theodore
|
|
36
|
+
Kensington, ADM: Charlotte Evangeline Worthington-Smythe
|
|
37
|
+
</span>
|
|
38
|
+
),
|
|
39
|
+
},
|
|
40
|
+
]}
|
|
41
|
+
/>
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
const grid = container.firstElementChild
|
|
45
|
+
expect(grid?.className).toContain("min-w-0")
|
|
46
|
+
expect(grid?.className).toContain("minmax(0,1fr)")
|
|
47
|
+
expect(screen.getByText("Very Long Relationship Manager Owner Label").className).toContain("[overflow-wrap:anywhere]")
|
|
48
|
+
expect(screen.getByText(/Owner: Alexandria/).parentElement?.className).toContain("[overflow-wrap:anywhere]")
|
|
49
|
+
expect(screen.getByText(/Owner: Alexandria/).parentElement?.className).not.toContain("truncate")
|
|
50
|
+
})
|
|
25
51
|
})
|
|
@@ -119,8 +119,8 @@ describe("SignalPriorityPopover", () => {
|
|
|
119
119
|
|
|
120
120
|
// Check head section
|
|
121
121
|
expect(content.textContent).toContain("Why this is high priority")
|
|
122
|
-
expect(
|
|
123
|
-
expect(
|
|
122
|
+
expect(content.textContent).toContain("79")
|
|
123
|
+
expect(content.textContent).toContain("/100")
|
|
124
124
|
expect(content.textContent).toContain("High range")
|
|
125
125
|
expect(content.textContent).toContain("60-79")
|
|
126
126
|
})
|
|
@@ -188,50 +188,13 @@ describe("SignalPriorityPopover", () => {
|
|
|
188
188
|
expect(row.textContent).not.toContain("Raises0/100")
|
|
189
189
|
})
|
|
190
190
|
|
|
191
|
-
it("renders Contributing factors section label
|
|
191
|
+
it("renders Contributing factors section label", () => {
|
|
192
192
|
render(<SignalPriorityPopover {...defaultProps} />)
|
|
193
193
|
fireEvent.click(screen.getByTestId("priority-popover-trigger"))
|
|
194
194
|
|
|
195
195
|
const content = screen.getByTestId("priority-popover-content")
|
|
196
196
|
expect(content.textContent).toContain("Contributing factors")
|
|
197
|
-
expect(content.textContent).toContain("
|
|
198
|
-
expect(content.textContent).not.toContain("Score = weighted sum")
|
|
199
|
-
expect(content.textContent).not.toContain("Priority = weighted signals + calibration")
|
|
200
|
-
})
|
|
201
|
-
|
|
202
|
-
it("renders a custom formula label when provided", () => {
|
|
203
|
-
render(
|
|
204
|
-
<SignalPriorityPopover
|
|
205
|
-
{...defaultProps}
|
|
206
|
-
formulaLabel="Priority = weighted signals + calibration"
|
|
207
|
-
/>,
|
|
208
|
-
)
|
|
209
|
-
fireEvent.click(screen.getByTestId("priority-popover-trigger"))
|
|
210
|
-
|
|
211
|
-
const content = screen.getByTestId("priority-popover-content")
|
|
212
|
-
expect(content.textContent).toContain("Priority = weighted signals + calibration")
|
|
213
|
-
})
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
it("renders the overall score number by default while preserving factor row scores", () => {
|
|
217
|
-
render(<SignalPriorityPopover {...defaultProps} />)
|
|
218
|
-
fireEvent.click(screen.getByTestId("priority-popover-trigger"))
|
|
219
|
-
|
|
220
|
-
expect(screen.getByTestId("priority-overall-score").textContent).toBe("79/100")
|
|
221
|
-
expect(screen.getByTestId("factor-row-test_severity").textContent).toContain("85/100")
|
|
222
|
-
expect(screen.getByTestId("factor-row-account_depth").textContent).toContain("30/100")
|
|
223
|
-
})
|
|
224
|
-
|
|
225
|
-
it("hides only the overall header score in label display mode", () => {
|
|
226
|
-
render(<SignalPriorityPopover {...defaultProps} scoreDisplay="label" />)
|
|
227
|
-
fireEvent.click(screen.getByTestId("priority-popover-trigger"))
|
|
228
|
-
|
|
229
|
-
const header = screen.getByTestId("priority-popover-header")
|
|
230
|
-
expect(screen.queryByTestId("priority-overall-score")).toBeNull()
|
|
231
|
-
expect(header.textContent).toContain("Why this is high priority")
|
|
232
|
-
expect(header.textContent).not.toContain("79/100")
|
|
233
|
-
expect(screen.getByTestId("factor-row-test_severity").textContent).toContain("85/100")
|
|
234
|
-
expect(screen.getByTestId("factor-row-account_depth").textContent).toContain("30/100")
|
|
197
|
+
expect(content.textContent).toContain("Score = weighted sum")
|
|
235
198
|
})
|
|
236
199
|
|
|
237
200
|
it("renders score track bars with correct width percentage", () => {
|
|
@@ -28,13 +28,10 @@ export interface FilterOption {
|
|
|
28
28
|
value: string
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
|
|
31
|
+
interface DataTableFilterCategoryBase {
|
|
32
32
|
id: string
|
|
33
33
|
label: string
|
|
34
34
|
icon: React.ComponentType<{ className?: string }>
|
|
35
|
-
options: (string | FilterOption)[]
|
|
36
|
-
/** Filter behavior. Defaults to "multi" (checkbox multi-select). */
|
|
37
|
-
type?: "multi" | "single" | "boolean"
|
|
38
35
|
/**
|
|
39
36
|
* Submenu search behavior. Defaults to the DataTableFilter
|
|
40
37
|
* optionSearchThreshold prop. Use true to always show search or false to
|
|
@@ -43,6 +40,25 @@ export interface DataTableFilterCategory {
|
|
|
43
40
|
searchable?: boolean | { threshold?: number }
|
|
44
41
|
}
|
|
45
42
|
|
|
43
|
+
export interface DataTableOptionFilterCategory extends DataTableFilterCategoryBase {
|
|
44
|
+
options: (string | FilterOption)[]
|
|
45
|
+
/** Filter behavior. Defaults to "multi" (checkbox multi-select). */
|
|
46
|
+
type?: "multi" | "single" | "boolean"
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface DataTableTextFilterCategory extends DataTableFilterCategoryBase {
|
|
50
|
+
/** Free-text filter behavior. Renders a top-level submenu with a text input. */
|
|
51
|
+
type: "text"
|
|
52
|
+
/** Placeholder shown in the text filter input. */
|
|
53
|
+
valuePlaceholder?: string
|
|
54
|
+
/** Not used for text filters; optional for backwards-compatible category shapes. */
|
|
55
|
+
options?: (string | FilterOption)[]
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export type DataTableFilterCategory =
|
|
59
|
+
| DataTableOptionFilterCategory
|
|
60
|
+
| DataTableTextFilterCategory
|
|
61
|
+
|
|
46
62
|
function getOptionValue(option: string | FilterOption): string {
|
|
47
63
|
return typeof option === "string" ? option : option.value
|
|
48
64
|
}
|
|
@@ -50,6 +66,111 @@ function getOptionLabel(option: string | FilterOption): string {
|
|
|
50
66
|
return typeof option === "string" ? option : option.label
|
|
51
67
|
}
|
|
52
68
|
|
|
69
|
+
function isTextFilterCategory(
|
|
70
|
+
category: DataTableFilterCategory
|
|
71
|
+
): category is DataTableTextFilterCategory {
|
|
72
|
+
return category.type === "text"
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function TextFilterSubmenu({
|
|
76
|
+
category,
|
|
77
|
+
value,
|
|
78
|
+
onValueChange,
|
|
79
|
+
}: {
|
|
80
|
+
category: DataTableTextFilterCategory
|
|
81
|
+
value: string
|
|
82
|
+
onValueChange?: (categoryId: string, value: string) => void
|
|
83
|
+
}) {
|
|
84
|
+
const [draftValue, setDraftValue] = React.useState(value)
|
|
85
|
+
|
|
86
|
+
React.useEffect(() => {
|
|
87
|
+
setDraftValue(value)
|
|
88
|
+
}, [value])
|
|
89
|
+
|
|
90
|
+
const active = value.trim().length > 0
|
|
91
|
+
const applyValue = React.useCallback(() => {
|
|
92
|
+
onValueChange?.(category.id, draftValue.trim())
|
|
93
|
+
}, [category.id, draftValue, onValueChange])
|
|
94
|
+
|
|
95
|
+
return (
|
|
96
|
+
<DropdownMenuSub
|
|
97
|
+
onOpenChange={(open) => {
|
|
98
|
+
if (!open) {
|
|
99
|
+
setDraftValue(value)
|
|
100
|
+
}
|
|
101
|
+
}}
|
|
102
|
+
>
|
|
103
|
+
<DropdownMenuSubTrigger
|
|
104
|
+
className={cn(
|
|
105
|
+
"cursor-pointer py-1.5 text-xs",
|
|
106
|
+
active && "text-brand-purple"
|
|
107
|
+
)}
|
|
108
|
+
>
|
|
109
|
+
<category.icon
|
|
110
|
+
className={cn(
|
|
111
|
+
"mr-2 h-3.5 w-3.5 text-muted-foreground",
|
|
112
|
+
active && "text-brand-purple"
|
|
113
|
+
)}
|
|
114
|
+
/>
|
|
115
|
+
{category.label}
|
|
116
|
+
{active ? <Check className="ml-auto h-4 w-4" /> : null}
|
|
117
|
+
</DropdownMenuSubTrigger>
|
|
118
|
+
<DropdownMenuSubContent className="w-64 p-2">
|
|
119
|
+
<div className="space-y-2">
|
|
120
|
+
<input
|
|
121
|
+
aria-label={category.label}
|
|
122
|
+
className="h-8 w-full rounded-md bg-muted/50 px-2 py-1 text-xs outline-none transition-colors placeholder:text-muted-foreground/70 focus:bg-muted"
|
|
123
|
+
placeholder={
|
|
124
|
+
category.valuePlaceholder ??
|
|
125
|
+
`Enter ${category.label.toLowerCase()}...`
|
|
126
|
+
}
|
|
127
|
+
value={draftValue}
|
|
128
|
+
onChange={(event) => setDraftValue(event.target.value)}
|
|
129
|
+
onClick={(event) => event.stopPropagation()}
|
|
130
|
+
onKeyDown={(event) => {
|
|
131
|
+
event.stopPropagation()
|
|
132
|
+
if (event.key === "Enter") {
|
|
133
|
+
event.preventDefault()
|
|
134
|
+
applyValue()
|
|
135
|
+
}
|
|
136
|
+
}}
|
|
137
|
+
/>
|
|
138
|
+
<div className="flex items-center justify-end gap-2">
|
|
139
|
+
{active ? (
|
|
140
|
+
<Button
|
|
141
|
+
type="button"
|
|
142
|
+
variant="ghost"
|
|
143
|
+
size="sm"
|
|
144
|
+
className="h-7 px-2 text-xs"
|
|
145
|
+
onClick={(event) => {
|
|
146
|
+
event.preventDefault()
|
|
147
|
+
event.stopPropagation()
|
|
148
|
+
setDraftValue("")
|
|
149
|
+
onValueChange?.(category.id, "")
|
|
150
|
+
}}
|
|
151
|
+
>
|
|
152
|
+
Clear
|
|
153
|
+
</Button>
|
|
154
|
+
) : null}
|
|
155
|
+
<Button
|
|
156
|
+
type="button"
|
|
157
|
+
size="sm"
|
|
158
|
+
className="h-7 px-2 text-xs"
|
|
159
|
+
onClick={(event) => {
|
|
160
|
+
event.preventDefault()
|
|
161
|
+
event.stopPropagation()
|
|
162
|
+
applyValue()
|
|
163
|
+
}}
|
|
164
|
+
>
|
|
165
|
+
Apply
|
|
166
|
+
</Button>
|
|
167
|
+
</div>
|
|
168
|
+
</div>
|
|
169
|
+
</DropdownMenuSubContent>
|
|
170
|
+
</DropdownMenuSub>
|
|
171
|
+
)
|
|
172
|
+
}
|
|
173
|
+
|
|
53
174
|
export interface DataTableFilterProps {
|
|
54
175
|
categories: DataTableFilterCategory[]
|
|
55
176
|
selectedFilters: Record<string, string[]>
|
|
@@ -71,6 +192,10 @@ export interface DataTableFilterProps {
|
|
|
71
192
|
onConditionFiltersChange?: (conditions: ConditionFilterValue[]) => void
|
|
72
193
|
/** Dropdown entry label for the condition-builder panel. Default: "Add filter". */
|
|
73
194
|
conditionBuilderLabel?: string
|
|
195
|
+
/** Active free-text filters keyed by category id. */
|
|
196
|
+
textFilters?: Record<string, string>
|
|
197
|
+
/** Callback when a free-text filter value is applied or cleared. */
|
|
198
|
+
onTextFilterChange?: (categoryId: string, value: string) => void
|
|
74
199
|
}
|
|
75
200
|
|
|
76
201
|
export function DataTableFilter({
|
|
@@ -86,6 +211,8 @@ export function DataTableFilter({
|
|
|
86
211
|
conditionFilters = [],
|
|
87
212
|
onConditionFiltersChange,
|
|
88
213
|
conditionBuilderLabel = "Add filter",
|
|
214
|
+
textFilters = {},
|
|
215
|
+
onTextFilterChange,
|
|
89
216
|
}: DataTableFilterProps) {
|
|
90
217
|
const [query, setQuery] = React.useState("")
|
|
91
218
|
const [subQueries, setSubQueries] = React.useState<Record<string, string>>({})
|
|
@@ -103,6 +230,10 @@ export function DataTableFilter({
|
|
|
103
230
|
return true
|
|
104
231
|
}
|
|
105
232
|
|
|
233
|
+
if (isTextFilterCategory(category)) {
|
|
234
|
+
return false
|
|
235
|
+
}
|
|
236
|
+
|
|
106
237
|
return category.options.some((option) =>
|
|
107
238
|
getOptionLabel(option).toLowerCase().includes(normalized)
|
|
108
239
|
)
|
|
@@ -123,8 +254,16 @@ export function DataTableFilter({
|
|
|
123
254
|
0
|
|
124
255
|
)
|
|
125
256
|
|
|
126
|
-
|
|
127
|
-
|
|
257
|
+
const textCount = categories.reduce((count, category) => {
|
|
258
|
+
if (!isTextFilterCategory(category)) {
|
|
259
|
+
return count
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
return textFilters[category.id]?.trim() ? count + 1 : count
|
|
263
|
+
}, 0)
|
|
264
|
+
|
|
265
|
+
return userCount + conditionFilters.length + textCount
|
|
266
|
+
}, [categories, selectedFilters, conditionFilters.length, textFilters])
|
|
128
267
|
|
|
129
268
|
/** Collect all preset chips to render */
|
|
130
269
|
const presetChips = React.useMemo(() => {
|
|
@@ -135,9 +274,9 @@ export function DataTableFilter({
|
|
|
135
274
|
for (const [categoryId, values] of Object.entries(presetFilters)) {
|
|
136
275
|
const category = categories.find((c) => c.id === categoryId)
|
|
137
276
|
for (const value of values) {
|
|
138
|
-
const option = category
|
|
139
|
-
(opt) => getOptionValue(opt) === value
|
|
140
|
-
|
|
277
|
+
const option = category && !isTextFilterCategory(category)
|
|
278
|
+
? category.options.find((opt) => getOptionValue(opt) === value)
|
|
279
|
+
: undefined
|
|
141
280
|
const label = option ? getOptionLabel(option) : value
|
|
142
281
|
const active = selectedFilters[categoryId]?.includes(value) ?? false
|
|
143
282
|
chips.push({ categoryId, value, label, active })
|
|
@@ -208,6 +347,18 @@ export function DataTableFilter({
|
|
|
208
347
|
)
|
|
209
348
|
}
|
|
210
349
|
|
|
350
|
+
/* ── Free-text submenu ───────────────────────────────── */
|
|
351
|
+
if (isTextFilterCategory(category)) {
|
|
352
|
+
return (
|
|
353
|
+
<TextFilterSubmenu
|
|
354
|
+
key={category.id}
|
|
355
|
+
category={category}
|
|
356
|
+
value={textFilters[category.id] ?? ""}
|
|
357
|
+
onValueChange={onTextFilterChange}
|
|
358
|
+
/>
|
|
359
|
+
)
|
|
360
|
+
}
|
|
361
|
+
|
|
211
362
|
/* ── Sub-menu (single / multi) ──────────────────────── */
|
|
212
363
|
const subQuery = (subQueries[category.id] ?? "").trim().toLowerCase()
|
|
213
364
|
const filteredOptions = subQuery
|
|
@@ -258,14 +258,16 @@ export interface EntityMetadataField {
|
|
|
258
258
|
|
|
259
259
|
export function EntityMetadataGrid({ fields }: { fields: EntityMetadataField[] }) {
|
|
260
260
|
return (
|
|
261
|
-
<div className="grid
|
|
261
|
+
<div className="mb-7 grid min-w-0 grid-cols-1 gap-x-4 gap-y-3 overflow-hidden text-[13px] md:grid-cols-[minmax(0,140px)_minmax(0,1fr)]">
|
|
262
262
|
{fields.map((field, idx) => (
|
|
263
263
|
<React.Fragment key={idx}>
|
|
264
|
-
<div className="flex items-
|
|
265
|
-
<field.icon className="
|
|
266
|
-
<span>{field.label}</span>
|
|
264
|
+
<div className="flex min-w-0 items-start gap-1.5 text-[13px] font-normal text-muted-foreground">
|
|
265
|
+
<field.icon className="mt-0.5 h-3.5 w-3.5 shrink-0" />
|
|
266
|
+
<span className="min-w-0 break-words leading-relaxed [overflow-wrap:anywhere]">{field.label}</span>
|
|
267
|
+
</div>
|
|
268
|
+
<div className="min-w-0 whitespace-normal break-words leading-relaxed text-foreground [overflow-wrap:anywhere] [&_*]:max-w-full [&_*]:[overflow-wrap:anywhere] [&_a]:break-all">
|
|
269
|
+
{field.value}
|
|
267
270
|
</div>
|
|
268
|
-
<div className="min-w-0 truncate text-foreground">{field.value}</div>
|
|
269
271
|
</React.Fragment>
|
|
270
272
|
))}
|
|
271
273
|
</div>
|