@handled-ai/design-system 0.18.7 → 0.18.9

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.
@@ -1,22 +1,29 @@
1
1
  import * as React from 'react';
2
2
 
3
3
  type ConditionOperator = "eq" | "neq" | "gt" | "gte" | "lt" | "lte" | "in" | "is_null" | "is_not_null";
4
+ interface ConditionOptionObject {
5
+ label: string;
6
+ value: string;
7
+ }
8
+ type ConditionFieldOption = string | ConditionOptionObject;
4
9
  interface ConditionFieldDef {
5
10
  /** Unique field key (e.g., "Account_Balance__c") */
6
11
  id: string;
7
12
  /** Display label (e.g., "Account Balance") */
8
13
  label: string;
9
14
  /** Field data type — determines which operators are available and how the value input renders */
10
- type: "text" | "number" | "currency" | "date";
15
+ type: "text" | "number" | "currency" | "date" | "select" | "multi_select";
11
16
  /** Allowed operators for this field. Defaults based on type if not provided. */
12
17
  operators?: ConditionOperator[];
18
+ /** Options used by select and multi-select fields. Strings use the same label and value. */
19
+ options?: ConditionFieldOption[];
13
20
  }
14
21
  interface ConditionFilterValue {
15
22
  /** Stable identity — used as React key to avoid stale-state bugs on removal */
16
23
  id: string;
17
24
  field: string;
18
25
  operator: ConditionOperator;
19
- value: string | number | null;
26
+ value: string | number | string[] | null;
20
27
  }
21
28
  interface DataTableConditionFilterProps {
22
29
  /** Available fields the user can filter on */
@@ -34,4 +41,4 @@ declare function generateConditionId(): string;
34
41
  declare function getOperators(field: ConditionFieldDef): ConditionOperator[];
35
42
  declare function DataTableConditionFilter({ fields, conditions, onConditionsChange, className, }: DataTableConditionFilterProps): React.JSX.Element;
36
43
 
37
- export { type ConditionFieldDef, type ConditionFilterValue, type ConditionOperator, DEFAULT_OPERATORS, DataTableConditionFilter, type DataTableConditionFilterProps, OPERATOR_LABELS, generateConditionId, getOperators };
44
+ export { type ConditionFieldDef, type ConditionFieldOption, type ConditionFilterValue, type ConditionOperator, type ConditionOptionObject, DEFAULT_OPERATORS, DataTableConditionFilter, type DataTableConditionFilterProps, OPERATOR_LABELS, generateConditionId, getOperators };
@@ -24,6 +24,7 @@ import { jsx, jsxs } from "react/jsx-runtime";
24
24
  import * as React from "react";
25
25
  import {
26
26
  CalendarDays,
27
+ Check,
27
28
  DollarSign,
28
29
  Eye,
29
30
  Hash,
@@ -49,7 +50,7 @@ const OPERATOR_LABELS = {
49
50
  gte: "\u2265",
50
51
  lt: "<",
51
52
  lte: "\u2264",
52
- in: "contains",
53
+ in: "is any of",
53
54
  is_null: "is empty",
54
55
  is_not_null: "is not empty"
55
56
  };
@@ -67,7 +68,9 @@ const DEFAULT_OPERATORS = {
67
68
  text: ["eq", "neq", "is_null", "is_not_null"],
68
69
  number: NUMERIC_OPERATORS,
69
70
  currency: NUMERIC_OPERATORS,
70
- date: ["gt", "gte", "lt", "lte", "eq", "neq", "is_null", "is_not_null"]
71
+ date: ["gt", "gte", "lt", "lte", "eq", "neq", "is_null", "is_not_null"],
72
+ select: ["eq", "neq", "is_null", "is_not_null"],
73
+ multi_select: ["in", "is_null", "is_not_null"]
71
74
  };
72
75
  function generateConditionId() {
73
76
  if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
@@ -77,7 +80,7 @@ function generateConditionId() {
77
80
  }
78
81
  function getOperators(field) {
79
82
  var _a;
80
- return ((_a = field.operators) != null ? _a : DEFAULT_OPERATORS[field.type]).filter((op) => op !== "in");
83
+ return (_a = field.operators) != null ? _a : DEFAULT_OPERATORS[field.type];
81
84
  }
82
85
  function isUnaryOperator(op) {
83
86
  return op === "is_null" || op === "is_not_null";
@@ -94,6 +97,13 @@ function createDraftCondition(field) {
94
97
  value: null
95
98
  };
96
99
  }
100
+ function normalizeConditionValue(value, field, operator) {
101
+ if (isUnaryOperator(operator)) return null;
102
+ if (field.type === "multi_select") {
103
+ return Array.isArray(value) ? value : null;
104
+ }
105
+ return Array.isArray(value) ? null : value;
106
+ }
97
107
  function normalizeCondition(condition, fields) {
98
108
  var _a;
99
109
  const field = (_a = fields.find((f) => f.id === condition.field)) != null ? _a : fields[0];
@@ -103,7 +113,7 @@ function normalizeCondition(condition, fields) {
103
113
  return __spreadProps(__spreadValues({}, condition), {
104
114
  field: field.id,
105
115
  operator,
106
- value: isUnaryOperator(operator) ? null : condition.value
116
+ value: normalizeConditionValue(condition.value, field, operator)
107
117
  });
108
118
  }
109
119
  function parseConditionValue(raw, fieldType) {
@@ -119,25 +129,150 @@ function isCompleteCondition(condition, fields) {
119
129
  if (!field) return false;
120
130
  if (!getOperators(field).includes(condition.operator)) return false;
121
131
  if (isUnaryOperator(condition.operator)) return true;
122
- return condition.value !== null && condition.value !== "";
132
+ if (field.type === "multi_select") {
133
+ return Array.isArray(condition.value) && condition.value.length > 0;
134
+ }
135
+ return condition.value !== null && condition.value !== "" && !Array.isArray(condition.value);
136
+ }
137
+ function getConditionValueSignature(value) {
138
+ return Array.isArray(value) ? JSON.stringify(value) : String(value);
123
139
  }
124
140
  function getConditionsSignature(conditions) {
125
- return conditions.map((condition) => `${condition.id}:${condition.field}:${condition.operator}:${String(condition.value)}`).join(";");
141
+ return conditions.map((condition) => `${condition.id}:${condition.field}:${condition.operator}:${getConditionValueSignature(condition.value)}`).join(";");
142
+ }
143
+ function normalizeFieldOptions(field) {
144
+ var _a;
145
+ return ((_a = field.options) != null ? _a : []).map(
146
+ (option) => typeof option === "string" ? { label: option, value: option } : option
147
+ );
126
148
  }
127
149
  function getFieldsSignature(fields) {
128
150
  return fields.map((field) => {
129
151
  var _a;
130
- return `${field.id}:${field.type}:${field.label}:${((_a = field.operators) != null ? _a : []).join("|")}`;
152
+ const optionsSignature = normalizeFieldOptions(field).map((option) => `${option.label}:${option.value}`).join("|");
153
+ return `${field.id}:${field.type}:${field.label}:${((_a = field.operators) != null ? _a : []).join("|")}:${optionsSignature}`;
131
154
  }).join(";");
132
155
  }
133
156
  function getCommittedConditions(drafts, fields) {
134
157
  return drafts.map((condition) => normalizeCondition(condition, fields)).filter((condition) => isCompleteCondition(condition, fields));
135
158
  }
159
+ const FIELD_ICON_BY_TYPE = {
160
+ text: Type,
161
+ number: Hash,
162
+ currency: DollarSign,
163
+ date: CalendarDays,
164
+ select: MoreHorizontal,
165
+ multi_select: MoreHorizontal
166
+ };
136
167
  function getFieldIcon(type) {
137
- if (type === "currency") return DollarSign;
138
- if (type === "number") return Hash;
139
- if (type === "date") return CalendarDays;
140
- return Type;
168
+ return FIELD_ICON_BY_TYPE[type];
169
+ }
170
+ function getInputType(fieldType) {
171
+ if (fieldType === "number" || fieldType === "currency") return "number";
172
+ if (fieldType === "date") return "date";
173
+ return "text";
174
+ }
175
+ function getInputPlaceholder(fieldType) {
176
+ if (fieldType === "currency") return "Amount";
177
+ if (fieldType === "number") return "Enter number...";
178
+ if (fieldType === "date") return "";
179
+ return "Enter value...";
180
+ }
181
+ function SelectConditionValueInput({
182
+ condition,
183
+ fieldDef,
184
+ onSelectValueChange
185
+ }) {
186
+ const options = normalizeFieldOptions(fieldDef);
187
+ return /* @__PURE__ */ jsxs(
188
+ Select,
189
+ {
190
+ value: typeof condition.value === "string" ? condition.value : "",
191
+ onValueChange: onSelectValueChange,
192
+ children: [
193
+ /* @__PURE__ */ jsx(SelectTrigger, { className: "h-8 w-full", size: "sm", children: /* @__PURE__ */ jsx(SelectValue, { placeholder: "Select value..." }) }),
194
+ /* @__PURE__ */ jsx(SelectContent, { children: options.map((option) => /* @__PURE__ */ jsx(SelectItem, { value: option.value, children: option.label }, option.value)) })
195
+ ]
196
+ }
197
+ );
198
+ }
199
+ function MultiSelectConditionValueInput({
200
+ condition,
201
+ fieldDef,
202
+ onMultiSelectValueToggle
203
+ }) {
204
+ const selectedValues = Array.isArray(condition.value) ? condition.value : [];
205
+ const options = normalizeFieldOptions(fieldDef);
206
+ return /* @__PURE__ */ jsx("div", { className: "max-h-28 overflow-y-auto rounded-md border border-border bg-background p-1", children: options.length > 0 ? options.map((option) => {
207
+ const checked = selectedValues.includes(option.value);
208
+ return /* @__PURE__ */ jsxs(
209
+ "button",
210
+ {
211
+ type: "button",
212
+ role: "checkbox",
213
+ "aria-checked": checked,
214
+ className: cn(
215
+ "flex w-full items-center gap-2 rounded-sm px-2 py-1 text-left text-xs hover:bg-muted",
216
+ checked && "text-brand-purple"
217
+ ),
218
+ onClick: (event) => {
219
+ event.stopPropagation();
220
+ onMultiSelectValueToggle(option.value);
221
+ },
222
+ children: [
223
+ /* @__PURE__ */ jsx(
224
+ "span",
225
+ {
226
+ className: cn(
227
+ "flex h-3.5 w-3.5 items-center justify-center rounded-sm border border-border",
228
+ checked && "border-brand-purple bg-brand-purple text-white"
229
+ ),
230
+ "aria-hidden": "true",
231
+ children: checked ? /* @__PURE__ */ jsx(Check, { className: "h-3 w-3" }) : null
232
+ }
233
+ ),
234
+ /* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1 truncate", children: option.label })
235
+ ]
236
+ },
237
+ option.value
238
+ );
239
+ }) : /* @__PURE__ */ jsx("div", { className: "px-2 py-1 text-xs text-muted-foreground", children: "No options" }) });
240
+ }
241
+ function ScalarConditionValueInput({
242
+ condition,
243
+ fieldDef,
244
+ onValueChange,
245
+ onCommit
246
+ }) {
247
+ return /* @__PURE__ */ jsxs("div", { className: "relative flex items-center", children: [
248
+ fieldDef.type === "currency" ? /* @__PURE__ */ jsx("span", { className: "pointer-events-none absolute left-2 text-sm text-muted-foreground", children: "$" }) : null,
249
+ /* @__PURE__ */ jsx(
250
+ Input,
251
+ {
252
+ type: getInputType(fieldDef.type),
253
+ value: condition.value != null && !Array.isArray(condition.value) ? String(condition.value) : "",
254
+ onChange: onValueChange,
255
+ onClick: (event) => event.stopPropagation(),
256
+ onKeyDown: (event) => {
257
+ event.stopPropagation();
258
+ if (event.key === "Enter") {
259
+ onCommit();
260
+ }
261
+ },
262
+ placeholder: getInputPlaceholder(fieldDef.type),
263
+ className: cn("h-8", fieldDef.type === "currency" && "pl-6")
264
+ }
265
+ )
266
+ ] });
267
+ }
268
+ function ConditionValueInput(props) {
269
+ if (props.fieldDef.type === "select") {
270
+ return /* @__PURE__ */ jsx(SelectConditionValueInput, __spreadValues({}, props));
271
+ }
272
+ if (props.fieldDef.type === "multi_select") {
273
+ return /* @__PURE__ */ jsx(MultiSelectConditionValueInput, __spreadValues({}, props));
274
+ }
275
+ return /* @__PURE__ */ jsx(ScalarConditionValueInput, __spreadValues({}, props));
141
276
  }
142
277
  function ConditionRow({
143
278
  condition,
@@ -165,7 +300,7 @@ function ConditionRow({
165
300
  const handleOperatorChange = (newOp) => {
166
301
  onChange(__spreadProps(__spreadValues({}, condition), {
167
302
  operator: newOp,
168
- value: isUnaryOperator(newOp) ? null : condition.value
303
+ value: normalizeConditionValue(condition.value, fieldDef, newOp)
169
304
  }));
170
305
  };
171
306
  const handleValueChange = (event) => {
@@ -173,6 +308,18 @@ function ConditionRow({
173
308
  value: parseConditionValue(event.target.value, fieldDef.type)
174
309
  }));
175
310
  };
311
+ const handleSelectValueChange = (value) => {
312
+ onChange(__spreadProps(__spreadValues({}, condition), {
313
+ value
314
+ }));
315
+ };
316
+ const handleMultiSelectValueToggle = (value) => {
317
+ const currentValues = Array.isArray(condition.value) ? condition.value : [];
318
+ const nextValues = currentValues.includes(value) ? currentValues.filter((currentValue) => currentValue !== value) : [...currentValues, value];
319
+ onChange(__spreadProps(__spreadValues({}, condition), {
320
+ value: nextValues
321
+ }));
322
+ };
176
323
  return /* @__PURE__ */ jsxs(
177
324
  "div",
178
325
  {
@@ -204,26 +351,17 @@ function ConditionRow({
204
351
  ]
205
352
  }
206
353
  ),
207
- isUnary ? /* @__PURE__ */ jsx("div", { className: "h-8 rounded-md border border-dashed border-border/70 bg-muted/30 px-3 py-1.5 text-xs text-muted-foreground", children: "No value needed" }) : /* @__PURE__ */ jsxs("div", { className: "relative flex items-center", children: [
208
- fieldDef.type === "currency" ? /* @__PURE__ */ jsx("span", { className: "pointer-events-none absolute left-2 text-sm text-muted-foreground", children: "$" }) : null,
209
- /* @__PURE__ */ jsx(
210
- Input,
211
- {
212
- type: fieldDef.type === "number" || fieldDef.type === "currency" ? "number" : fieldDef.type === "date" ? "date" : "text",
213
- value: condition.value != null ? String(condition.value) : "",
214
- onChange: handleValueChange,
215
- onClick: (event) => event.stopPropagation(),
216
- onKeyDown: (event) => {
217
- event.stopPropagation();
218
- if (event.key === "Enter") {
219
- onCommit();
220
- }
221
- },
222
- placeholder: fieldDef.type === "currency" ? "Amount" : fieldDef.type === "number" ? "Enter number..." : fieldDef.type === "date" ? "" : "Enter value...",
223
- className: cn("h-8", fieldDef.type === "currency" && "pl-6")
224
- }
225
- )
226
- ] }),
354
+ isUnary ? /* @__PURE__ */ jsx("div", { className: "h-8 rounded-md border border-dashed border-border/70 bg-muted/30 px-3 py-1.5 text-xs text-muted-foreground", children: "No value needed" }) : /* @__PURE__ */ jsx(
355
+ ConditionValueInput,
356
+ {
357
+ condition,
358
+ fieldDef,
359
+ onValueChange: handleValueChange,
360
+ onSelectValueChange: handleSelectValueChange,
361
+ onMultiSelectValueToggle: handleMultiSelectValueToggle,
362
+ onCommit
363
+ }
364
+ ),
227
365
  /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1", children: [
228
366
  /* @__PURE__ */ jsx(
229
367
  Button,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/components/data-table-condition-filter.tsx"],"sourcesContent":["\"use client\"\n\nimport * as React from \"react\"\nimport {\n CalendarDays,\n DollarSign,\n Eye,\n Hash,\n MoreHorizontal,\n Plus,\n Trash2,\n Type,\n} from \"lucide-react\"\n\nimport { cn } from \"../lib/utils\"\nimport { Button } from \"./button\"\nimport { Input } from \"./input\"\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from \"./select\"\n\n// ── Types ──────────────────────────────────────────────────────\n\nexport type ConditionOperator =\n | \"eq\"\n | \"neq\"\n | \"gt\"\n | \"gte\"\n | \"lt\"\n | \"lte\"\n | \"in\"\n | \"is_null\"\n | \"is_not_null\"\n\nexport interface ConditionFieldDef {\n /** Unique field key (e.g., \"Account_Balance__c\") */\n id: string\n /** Display label (e.g., \"Account Balance\") */\n label: string\n /** Field data type — determines which operators are available and how the value input renders */\n type: \"text\" | \"number\" | \"currency\" | \"date\"\n /** Allowed operators for this field. Defaults based on type if not provided. */\n operators?: ConditionOperator[]\n}\n\nexport interface ConditionFilterValue {\n /** Stable identity — used as React key to avoid stale-state bugs on removal */\n id: string\n field: string\n operator: ConditionOperator\n value: string | number | null\n}\n\ninterface DataTableConditionFilterProps {\n /** Available fields the user can filter on */\n fields: ConditionFieldDef[]\n /** Current active conditions */\n conditions: ConditionFilterValue[]\n /** Called when conditions change (add, update, remove) */\n onConditionsChange: (conditions: ConditionFilterValue[]) => void\n className?: string\n}\n\n// ── Constants ──────────────────────────────────────────────────\n\nconst OPERATOR_LABELS: Record<ConditionOperator, string> = {\n eq: \"=\",\n neq: \"≠\",\n gt: \">\",\n gte: \"≥\",\n lt: \"<\",\n lte: \"≤\",\n in: \"contains\",\n is_null: \"is empty\",\n is_not_null: \"is not empty\",\n}\n\nconst NUMERIC_OPERATORS: ConditionOperator[] = [\n \"eq\",\n \"neq\",\n \"gt\",\n \"gte\",\n \"lt\",\n \"lte\",\n \"is_null\",\n \"is_not_null\",\n]\n\nconst DEFAULT_OPERATORS: Record<ConditionFieldDef[\"type\"], ConditionOperator[]> = {\n text: [\"eq\", \"neq\", \"is_null\", \"is_not_null\"],\n number: NUMERIC_OPERATORS,\n currency: NUMERIC_OPERATORS,\n date: [\"gt\", \"gte\", \"lt\", \"lte\", \"eq\", \"neq\", \"is_null\", \"is_not_null\"],\n}\n\n/** Generate a stable unique ID for a new condition row. */\nfunction generateConditionId(): string {\n if (typeof crypto !== \"undefined\" && typeof crypto.randomUUID === \"function\") {\n return crypto.randomUUID()\n }\n return `${Date.now()}-${Math.random().toString(36).slice(2, 9)}`\n}\n\n// ── Helpers ────────────────────────────────────────────────────\n\nfunction getOperators(field: ConditionFieldDef): ConditionOperator[] {\n return (field.operators ?? DEFAULT_OPERATORS[field.type]).filter((op) => op !== \"in\")\n}\n\nfunction isUnaryOperator(op: ConditionOperator): boolean {\n return op === \"is_null\" || op === \"is_not_null\"\n}\n\nfunction getDefaultOperator(field: ConditionFieldDef): ConditionOperator {\n return getOperators(field)[0] ?? \"eq\"\n}\n\nfunction createDraftCondition(field: ConditionFieldDef): ConditionFilterValue {\n return {\n id: generateConditionId(),\n field: field.id,\n operator: getDefaultOperator(field),\n value: null,\n }\n}\n\nfunction normalizeCondition(\n condition: ConditionFilterValue,\n fields: ConditionFieldDef[],\n): ConditionFilterValue {\n const field = fields.find((f) => f.id === condition.field) ?? fields[0]\n if (!field) return condition\n\n const operators = getOperators(field)\n const operator = operators.includes(condition.operator)\n ? condition.operator\n : getDefaultOperator(field)\n\n return {\n ...condition,\n field: field.id,\n operator,\n value: isUnaryOperator(operator) ? null : condition.value,\n }\n}\n\nfunction parseConditionValue(\n raw: string,\n fieldType: ConditionFieldDef[\"type\"],\n): string | number | null {\n if (raw === \"\") return null\n if (fieldType === \"number\" || fieldType === \"currency\") {\n const parsed = Number(raw)\n return Number.isNaN(parsed) ? null : parsed\n }\n return raw\n}\n\nfunction isCompleteCondition(\n condition: ConditionFilterValue,\n fields: ConditionFieldDef[],\n): boolean {\n const field = fields.find((f) => f.id === condition.field)\n if (!field) return false\n if (!getOperators(field).includes(condition.operator)) return false\n if (isUnaryOperator(condition.operator)) return true\n return condition.value !== null && condition.value !== \"\"\n}\n\nfunction getConditionsSignature(conditions: ConditionFilterValue[]): string {\n return conditions\n .map((condition) => `${condition.id}:${condition.field}:${condition.operator}:${String(condition.value)}`)\n .join(\";\")\n}\n\nfunction getFieldsSignature(fields: ConditionFieldDef[]): string {\n return fields\n .map((field) => `${field.id}:${field.type}:${field.label}:${(field.operators ?? []).join(\"|\")}`)\n .join(\";\")\n}\n\nfunction getCommittedConditions(\n drafts: ConditionFilterValue[],\n fields: ConditionFieldDef[],\n): ConditionFilterValue[] {\n return drafts\n .map((condition) => normalizeCondition(condition, fields))\n .filter((condition) => isCompleteCondition(condition, fields))\n}\n\nfunction getFieldIcon(type: ConditionFieldDef[\"type\"]) {\n if (type === \"currency\") return DollarSign\n if (type === \"number\") return Hash\n if (type === \"date\") return CalendarDays\n return Type\n}\n\n// ── Condition Row ──────────────────────────────────────────────\n\ninterface ConditionRowProps {\n condition: ConditionFilterValue\n fields: ConditionFieldDef[]\n index: number\n onChange: (updated: ConditionFilterValue) => void\n onRemove: () => void\n onCommit: () => void\n}\n\nfunction ConditionRow({\n condition,\n fields,\n index,\n onChange,\n onRemove,\n onCommit,\n}: ConditionRowProps) {\n const fieldDef = fields.find((f) => f.id === condition.field) ?? fields[0]\n const operators = getOperators(fieldDef)\n const isUnary = isUnaryOperator(condition.operator)\n const FieldIcon = getFieldIcon(fieldDef.type)\n\n const handleFieldChange = (newFieldId: string) => {\n const newFieldDef = fields.find((f) => f.id === newFieldId) ?? fields[0]\n if (!newFieldDef) return\n onChange({\n ...condition,\n field: newFieldDef.id,\n operator: getDefaultOperator(newFieldDef),\n value: null,\n })\n }\n\n const handleOperatorChange = (newOp: ConditionOperator) => {\n onChange({\n ...condition,\n operator: newOp,\n value: isUnaryOperator(newOp) ? null : condition.value,\n })\n }\n\n const handleValueChange = (event: React.ChangeEvent<HTMLInputElement>) => {\n onChange({\n ...condition,\n value: parseConditionValue(event.target.value, fieldDef.type),\n })\n }\n\n return (\n <div\n className=\"grid grid-cols-[52px_minmax(150px,1fr)_120px_minmax(140px,1fr)_auto] items-center gap-2 rounded-lg border border-border/70 bg-background p-2 shadow-sm\"\n data-slot=\"condition-row\"\n >\n <div className=\"text-xs font-medium text-muted-foreground\">\n {index === 0 ? \"Where\" : \"And\"}\n </div>\n\n <Select value={condition.field} onValueChange={handleFieldChange}>\n <SelectTrigger className=\"h-8 w-full justify-start gap-2\" size=\"sm\">\n <FieldIcon className=\"h-3.5 w-3.5 text-muted-foreground\" />\n <SelectValue placeholder={fieldDef.label} />\n </SelectTrigger>\n <SelectContent>\n {fields.map((field) => {\n const Icon = getFieldIcon(field.type)\n return (\n <SelectItem key={field.id} value={field.id}>\n <span className=\"inline-flex items-center gap-2\">\n <Icon className=\"h-3.5 w-3.5 text-muted-foreground\" />\n {field.label}\n </span>\n </SelectItem>\n )\n })}\n </SelectContent>\n </Select>\n\n <Select\n value={condition.operator}\n onValueChange={(value) => handleOperatorChange(value as ConditionOperator)}\n >\n <SelectTrigger className=\"h-8 w-full\" size=\"sm\">\n <SelectValue placeholder=\"Operator\" />\n </SelectTrigger>\n <SelectContent>\n {operators.map((op) => (\n <SelectItem key={op} value={op}>\n {OPERATOR_LABELS[op]}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n\n {isUnary ? (\n <div className=\"h-8 rounded-md border border-dashed border-border/70 bg-muted/30 px-3 py-1.5 text-xs text-muted-foreground\">\n No value needed\n </div>\n ) : (\n <div className=\"relative flex items-center\">\n {fieldDef.type === \"currency\" ? (\n <span className=\"pointer-events-none absolute left-2 text-sm text-muted-foreground\">\n $\n </span>\n ) : null}\n <Input\n type={\n fieldDef.type === \"number\" || fieldDef.type === \"currency\"\n ? \"number\"\n : fieldDef.type === \"date\"\n ? \"date\"\n : \"text\"\n }\n value={condition.value != null ? String(condition.value) : \"\"}\n onChange={handleValueChange}\n onClick={(event) => event.stopPropagation()}\n onKeyDown={(event) => {\n event.stopPropagation()\n if (event.key === \"Enter\") {\n onCommit()\n }\n }}\n placeholder={\n fieldDef.type === \"currency\"\n ? \"Amount\"\n : fieldDef.type === \"number\"\n ? \"Enter number...\"\n : fieldDef.type === \"date\"\n ? \"\"\n : \"Enter value...\"\n }\n className={cn(\"h-8\", fieldDef.type === \"currency\" && \"pl-6\")}\n />\n </div>\n )}\n\n <div className=\"flex items-center gap-1\">\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon\"\n className=\"h-8 w-8 text-muted-foreground\"\n aria-label=\"Toggle condition visibility\"\n onClick={(event) => event.preventDefault()}\n >\n <Eye className=\"size-4\" />\n </Button>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon\"\n className=\"h-8 w-8 text-muted-foreground\"\n aria-label=\"More condition actions\"\n onClick={(event) => event.preventDefault()}\n >\n <MoreHorizontal className=\"size-4\" />\n </Button>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon\"\n className=\"h-8 w-8 text-muted-foreground hover:text-destructive\"\n onClick={onRemove}\n aria-label=\"Remove condition\"\n >\n <Trash2 className=\"size-4\" />\n </Button>\n </div>\n </div>\n )\n}\n\n// ── Main Component ─────────────────────────────────────────────\n\nfunction DataTableConditionFilter({\n fields,\n conditions,\n onConditionsChange,\n className,\n}: DataTableConditionFilterProps) {\n const [drafts, setDrafts] = React.useState<ConditionFilterValue[]>(() =>\n conditions.map((condition) => normalizeCondition(condition, fields)),\n )\n\n const fieldsSignature = React.useMemo(() => getFieldsSignature(fields), [fields])\n const conditionsSignature = React.useMemo(() => getConditionsSignature(conditions), [conditions])\n const fieldsRef = React.useRef(fields)\n\n React.useEffect(() => {\n setDrafts(conditions.map((condition) => normalizeCondition(condition, fieldsRef.current)))\n }, [conditionsSignature])\n\n React.useEffect(() => {\n if (fieldsRef.current !== fields) {\n fieldsRef.current = fields\n setDrafts((current) => current.map((condition) => normalizeCondition(condition, fields)))\n }\n // Depend on a structural signature so inline-but-equivalent field arrays do\n // not wipe in-progress drafts before Apply.\n }, [fieldsSignature, fields])\n\n const commitDrafts = React.useCallback(\n (nextDrafts: ConditionFilterValue[] = drafts) => {\n onConditionsChange(getCommittedConditions(nextDrafts, fields))\n },\n [drafts, fields, onConditionsChange],\n )\n\n const handleAdd = () => {\n const firstField = fields[0]\n if (!firstField) return\n const committedDrafts = getCommittedConditions(drafts, fields)\n const nextDrafts = [...committedDrafts, createDraftCondition(firstField)]\n setDrafts(nextDrafts)\n onConditionsChange(committedDrafts)\n }\n\n const handleUpdate = (index: number, updated: ConditionFilterValue) => {\n setDrafts((current) => {\n const next = [...current]\n next[index] = normalizeCondition(updated, fields)\n return next\n })\n }\n\n const handleRemove = (index: number) => {\n setDrafts((current) => {\n const next = current.filter((_, currentIndex) => currentIndex !== index)\n commitDrafts(next)\n return next\n })\n }\n\n const handleClear = () => {\n setDrafts([])\n onConditionsChange([])\n }\n\n const hasAppliedConditions = conditions.length > 0\n const hasDrafts = drafts.length > 0\n\n return (\n <div\n className={cn(\n \"w-[min(760px,calc(100vw-2rem))] rounded-xl border border-border bg-background p-3 text-foreground shadow-xl\",\n className,\n )}\n data-slot=\"condition-filter\"\n onClick={(event) => event.stopPropagation()}\n onKeyDown={(event) => {\n if (event.key !== \"Escape\") {\n event.stopPropagation()\n }\n }}\n >\n <div className=\"mb-3 flex items-center justify-between gap-3 border-b border-border/70 pb-3\">\n <div>\n <div className=\"text-sm font-semibold\">Filter builder</div>\n <div className=\"text-xs text-muted-foreground\">\n Build field, operator, and value conditions.\n </div>\n </div>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n className=\"h-8 text-xs text-destructive hover:text-destructive\"\n onClick={handleClear}\n disabled={!hasAppliedConditions && !hasDrafts}\n >\n Clear filters\n </Button>\n </div>\n\n <div className=\"flex flex-col gap-2\">\n {drafts.map((condition, index) => (\n <ConditionRow\n key={condition.id}\n condition={condition}\n fields={fields}\n index={index}\n onChange={(updated) => handleUpdate(index, updated)}\n onRemove={() => handleRemove(index)}\n onCommit={() => commitDrafts()}\n />\n ))}\n </div>\n\n {!hasDrafts ? (\n <div className=\"rounded-lg border border-dashed border-border/80 bg-muted/20 px-3 py-5 text-center text-xs text-muted-foreground\">\n No builder filters yet. Add a filter to start a condition row.\n </div>\n ) : null}\n\n <div className=\"mt-3 flex flex-wrap items-center justify-between gap-2 border-t border-border/70 pt-3\">\n <div className=\"flex items-center gap-2\">\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n className=\"h-8 text-xs text-muted-foreground\"\n onClick={handleAdd}\n >\n <Plus className=\"mr-1 size-3.5\" />\n Add filter\n </Button>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n className=\"h-8 text-xs text-muted-foreground opacity-60\"\n disabled\n aria-disabled=\"true\"\n >\n <Plus className=\"mr-1 size-3.5\" />\n Add filter group\n </Button>\n </div>\n <Button\n type=\"button\"\n size=\"sm\"\n className=\"h-8 text-xs\"\n onClick={() => commitDrafts()}\n >\n Apply\n </Button>\n </div>\n </div>\n )\n}\n\nexport {\n DataTableConditionFilter,\n OPERATOR_LABELS,\n DEFAULT_OPERATORS,\n generateConditionId,\n getOperators,\n}\nexport type { DataTableConditionFilterProps }\n"],"mappings":"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","names":["_a"]}
1
+ {"version":3,"sources":["../../src/components/data-table-condition-filter.tsx"],"sourcesContent":["\"use client\"\n\nimport * as React from \"react\"\nimport {\n CalendarDays,\n Check,\n DollarSign,\n Eye,\n Hash,\n MoreHorizontal,\n Plus,\n Trash2,\n Type,\n type LucideIcon,\n} from \"lucide-react\"\n\nimport { cn } from \"../lib/utils\"\nimport { Button } from \"./button\"\nimport { Input } from \"./input\"\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from \"./select\"\n\n// ── Types ──────────────────────────────────────────────────────\n\nexport type ConditionOperator =\n | \"eq\"\n | \"neq\"\n | \"gt\"\n | \"gte\"\n | \"lt\"\n | \"lte\"\n | \"in\"\n | \"is_null\"\n | \"is_not_null\"\n\nexport interface ConditionOptionObject {\n label: string\n value: string\n}\n\nexport type ConditionFieldOption = string | ConditionOptionObject\n\nexport interface ConditionFieldDef {\n /** Unique field key (e.g., \"Account_Balance__c\") */\n id: string\n /** Display label (e.g., \"Account Balance\") */\n label: string\n /** Field data type — determines which operators are available and how the value input renders */\n type: \"text\" | \"number\" | \"currency\" | \"date\" | \"select\" | \"multi_select\"\n /** Allowed operators for this field. Defaults based on type if not provided. */\n operators?: ConditionOperator[]\n /** Options used by select and multi-select fields. Strings use the same label and value. */\n options?: ConditionFieldOption[]\n}\n\nexport interface ConditionFilterValue {\n /** Stable identity — used as React key to avoid stale-state bugs on removal */\n id: string\n field: string\n operator: ConditionOperator\n value: string | number | string[] | null\n}\n\ninterface DataTableConditionFilterProps {\n /** Available fields the user can filter on */\n fields: ConditionFieldDef[]\n /** Current active conditions */\n conditions: ConditionFilterValue[]\n /** Called when conditions change (add, update, remove) */\n onConditionsChange: (conditions: ConditionFilterValue[]) => void\n className?: string\n}\n\n// ── Constants ──────────────────────────────────────────────────\n\nconst OPERATOR_LABELS: Record<ConditionOperator, string> = {\n eq: \"=\",\n neq: \"≠\",\n gt: \">\",\n gte: \"≥\",\n lt: \"<\",\n lte: \"≤\",\n in: \"is any of\",\n is_null: \"is empty\",\n is_not_null: \"is not empty\",\n}\n\nconst NUMERIC_OPERATORS: ConditionOperator[] = [\n \"eq\",\n \"neq\",\n \"gt\",\n \"gte\",\n \"lt\",\n \"lte\",\n \"is_null\",\n \"is_not_null\",\n]\n\nconst DEFAULT_OPERATORS: Record<ConditionFieldDef[\"type\"], ConditionOperator[]> = {\n text: [\"eq\", \"neq\", \"is_null\", \"is_not_null\"],\n number: NUMERIC_OPERATORS,\n currency: NUMERIC_OPERATORS,\n date: [\"gt\", \"gte\", \"lt\", \"lte\", \"eq\", \"neq\", \"is_null\", \"is_not_null\"],\n select: [\"eq\", \"neq\", \"is_null\", \"is_not_null\"],\n multi_select: [\"in\", \"is_null\", \"is_not_null\"],\n}\n\n/** Generate a stable unique ID for a new condition row. */\nfunction generateConditionId(): string {\n if (typeof crypto !== \"undefined\" && typeof crypto.randomUUID === \"function\") {\n return crypto.randomUUID()\n }\n return `${Date.now()}-${Math.random().toString(36).slice(2, 9)}`\n}\n\n// ── Helpers ────────────────────────────────────────────────────\n\nfunction getOperators(field: ConditionFieldDef): ConditionOperator[] {\n return field.operators ?? DEFAULT_OPERATORS[field.type]\n}\n\nfunction isUnaryOperator(op: ConditionOperator): boolean {\n return op === \"is_null\" || op === \"is_not_null\"\n}\n\nfunction getDefaultOperator(field: ConditionFieldDef): ConditionOperator {\n return getOperators(field)[0] ?? \"eq\"\n}\n\nfunction createDraftCondition(field: ConditionFieldDef): ConditionFilterValue {\n return {\n id: generateConditionId(),\n field: field.id,\n operator: getDefaultOperator(field),\n value: null,\n }\n}\n\nfunction normalizeConditionValue(\n value: ConditionFilterValue[\"value\"],\n field: ConditionFieldDef,\n operator: ConditionOperator,\n): ConditionFilterValue[\"value\"] {\n if (isUnaryOperator(operator)) return null\n if (field.type === \"multi_select\") {\n return Array.isArray(value) ? value : null\n }\n return Array.isArray(value) ? null : value\n}\n\nfunction normalizeCondition(\n condition: ConditionFilterValue,\n fields: ConditionFieldDef[],\n): ConditionFilterValue {\n const field = fields.find((f) => f.id === condition.field) ?? fields[0]\n if (!field) return condition\n\n const operators = getOperators(field)\n const operator = operators.includes(condition.operator)\n ? condition.operator\n : getDefaultOperator(field)\n\n return {\n ...condition,\n field: field.id,\n operator,\n value: normalizeConditionValue(condition.value, field, operator),\n }\n}\n\nfunction parseConditionValue(\n raw: string,\n fieldType: ConditionFieldDef[\"type\"],\n): string | number | null {\n if (raw === \"\") return null\n if (fieldType === \"number\" || fieldType === \"currency\") {\n const parsed = Number(raw)\n return Number.isNaN(parsed) ? null : parsed\n }\n return raw\n}\n\nfunction isCompleteCondition(\n condition: ConditionFilterValue,\n fields: ConditionFieldDef[],\n): boolean {\n const field = fields.find((f) => f.id === condition.field)\n if (!field) return false\n if (!getOperators(field).includes(condition.operator)) return false\n if (isUnaryOperator(condition.operator)) return true\n if (field.type === \"multi_select\") {\n return Array.isArray(condition.value) && condition.value.length > 0\n }\n return condition.value !== null && condition.value !== \"\" && !Array.isArray(condition.value)\n}\n\nfunction getConditionValueSignature(value: ConditionFilterValue[\"value\"]): string {\n return Array.isArray(value) ? JSON.stringify(value) : String(value)\n}\n\nfunction getConditionsSignature(conditions: ConditionFilterValue[]): string {\n return conditions\n .map((condition) => `${condition.id}:${condition.field}:${condition.operator}:${getConditionValueSignature(condition.value)}`)\n .join(\";\")\n}\n\nfunction normalizeFieldOptions(field: ConditionFieldDef): ConditionOptionObject[] {\n return (field.options ?? []).map((option) =>\n typeof option === \"string\" ? { label: option, value: option } : option,\n )\n}\n\nfunction getFieldsSignature(fields: ConditionFieldDef[]): string {\n return fields\n .map((field) => {\n const optionsSignature = normalizeFieldOptions(field)\n .map((option) => `${option.label}:${option.value}`)\n .join(\"|\")\n return `${field.id}:${field.type}:${field.label}:${(field.operators ?? []).join(\"|\")}:${optionsSignature}`\n })\n .join(\";\")\n}\n\nfunction getCommittedConditions(\n drafts: ConditionFilterValue[],\n fields: ConditionFieldDef[],\n): ConditionFilterValue[] {\n return drafts\n .map((condition) => normalizeCondition(condition, fields))\n .filter((condition) => isCompleteCondition(condition, fields))\n}\n\nconst FIELD_ICON_BY_TYPE: Record<ConditionFieldDef[\"type\"], LucideIcon> = {\n text: Type,\n number: Hash,\n currency: DollarSign,\n date: CalendarDays,\n select: MoreHorizontal,\n multi_select: MoreHorizontal,\n}\n\nfunction getFieldIcon(type: ConditionFieldDef[\"type\"]): LucideIcon {\n return FIELD_ICON_BY_TYPE[type]\n}\n\n// ── Condition Row ──────────────────────────────────────────────\n\nfunction getInputType(fieldType: ConditionFieldDef[\"type\"]): \"text\" | \"number\" | \"date\" {\n if (fieldType === \"number\" || fieldType === \"currency\") return \"number\"\n if (fieldType === \"date\") return \"date\"\n return \"text\"\n}\n\nfunction getInputPlaceholder(fieldType: ConditionFieldDef[\"type\"]): string {\n if (fieldType === \"currency\") return \"Amount\"\n if (fieldType === \"number\") return \"Enter number...\"\n if (fieldType === \"date\") return \"\"\n return \"Enter value...\"\n}\n\ninterface ConditionValueInputProps {\n condition: ConditionFilterValue\n fieldDef: ConditionFieldDef\n onValueChange: (event: React.ChangeEvent<HTMLInputElement>) => void\n onSelectValueChange: (value: string) => void\n onMultiSelectValueToggle: (value: string) => void\n onCommit: () => void\n}\n\nfunction SelectConditionValueInput({\n condition,\n fieldDef,\n onSelectValueChange,\n}: Pick<ConditionValueInputProps, \"condition\" | \"fieldDef\" | \"onSelectValueChange\">) {\n const options = normalizeFieldOptions(fieldDef)\n\n return (\n <Select\n value={typeof condition.value === \"string\" ? condition.value : \"\"}\n onValueChange={onSelectValueChange}\n >\n <SelectTrigger className=\"h-8 w-full\" size=\"sm\">\n <SelectValue placeholder=\"Select value...\" />\n </SelectTrigger>\n <SelectContent>\n {options.map((option) => (\n <SelectItem key={option.value} value={option.value}>\n {option.label}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n )\n}\n\nfunction MultiSelectConditionValueInput({\n condition,\n fieldDef,\n onMultiSelectValueToggle,\n}: Pick<ConditionValueInputProps, \"condition\" | \"fieldDef\" | \"onMultiSelectValueToggle\">) {\n const selectedValues = Array.isArray(condition.value) ? condition.value : []\n const options = normalizeFieldOptions(fieldDef)\n\n return (\n <div className=\"max-h-28 overflow-y-auto rounded-md border border-border bg-background p-1\">\n {options.length > 0 ? (\n options.map((option) => {\n const checked = selectedValues.includes(option.value)\n return (\n <button\n key={option.value}\n type=\"button\"\n role=\"checkbox\"\n aria-checked={checked}\n className={cn(\n \"flex w-full items-center gap-2 rounded-sm px-2 py-1 text-left text-xs hover:bg-muted\",\n checked && \"text-brand-purple\",\n )}\n onClick={(event) => {\n event.stopPropagation()\n onMultiSelectValueToggle(option.value)\n }}\n >\n <span\n className={cn(\n \"flex h-3.5 w-3.5 items-center justify-center rounded-sm border border-border\",\n checked && \"border-brand-purple bg-brand-purple text-white\",\n )}\n aria-hidden=\"true\"\n >\n {checked ? <Check className=\"h-3 w-3\" /> : null}\n </span>\n <span className=\"min-w-0 flex-1 truncate\">{option.label}</span>\n </button>\n )\n })\n ) : (\n <div className=\"px-2 py-1 text-xs text-muted-foreground\">No options</div>\n )}\n </div>\n )\n}\n\nfunction ScalarConditionValueInput({\n condition,\n fieldDef,\n onValueChange,\n onCommit,\n}: Pick<ConditionValueInputProps, \"condition\" | \"fieldDef\" | \"onValueChange\" | \"onCommit\">) {\n return (\n <div className=\"relative flex items-center\">\n {fieldDef.type === \"currency\" ? (\n <span className=\"pointer-events-none absolute left-2 text-sm text-muted-foreground\">\n $\n </span>\n ) : null}\n <Input\n type={getInputType(fieldDef.type)}\n value={\n condition.value != null && !Array.isArray(condition.value)\n ? String(condition.value)\n : \"\"\n }\n onChange={onValueChange}\n onClick={(event) => event.stopPropagation()}\n onKeyDown={(event) => {\n event.stopPropagation()\n if (event.key === \"Enter\") {\n onCommit()\n }\n }}\n placeholder={getInputPlaceholder(fieldDef.type)}\n className={cn(\"h-8\", fieldDef.type === \"currency\" && \"pl-6\")}\n />\n </div>\n )\n}\n\nfunction ConditionValueInput(props: ConditionValueInputProps) {\n if (props.fieldDef.type === \"select\") {\n return <SelectConditionValueInput {...props} />\n }\n\n if (props.fieldDef.type === \"multi_select\") {\n return <MultiSelectConditionValueInput {...props} />\n }\n\n return <ScalarConditionValueInput {...props} />\n}\n\ninterface ConditionRowProps {\n condition: ConditionFilterValue\n fields: ConditionFieldDef[]\n index: number\n onChange: (updated: ConditionFilterValue) => void\n onRemove: () => void\n onCommit: () => void\n}\n\nfunction ConditionRow({\n condition,\n fields,\n index,\n onChange,\n onRemove,\n onCommit,\n}: ConditionRowProps) {\n const fieldDef = fields.find((f) => f.id === condition.field) ?? fields[0]\n const operators = getOperators(fieldDef)\n const isUnary = isUnaryOperator(condition.operator)\n const FieldIcon = getFieldIcon(fieldDef.type)\n\n const handleFieldChange = (newFieldId: string) => {\n const newFieldDef = fields.find((f) => f.id === newFieldId) ?? fields[0]\n if (!newFieldDef) return\n onChange({\n ...condition,\n field: newFieldDef.id,\n operator: getDefaultOperator(newFieldDef),\n value: null,\n })\n }\n\n const handleOperatorChange = (newOp: ConditionOperator) => {\n onChange({\n ...condition,\n operator: newOp,\n value: normalizeConditionValue(condition.value, fieldDef, newOp),\n })\n }\n\n const handleValueChange = (event: React.ChangeEvent<HTMLInputElement>) => {\n onChange({\n ...condition,\n value: parseConditionValue(event.target.value, fieldDef.type),\n })\n }\n\n const handleSelectValueChange = (value: string) => {\n onChange({\n ...condition,\n value,\n })\n }\n\n const handleMultiSelectValueToggle = (value: string) => {\n const currentValues = Array.isArray(condition.value) ? condition.value : []\n const nextValues = currentValues.includes(value)\n ? currentValues.filter((currentValue) => currentValue !== value)\n : [...currentValues, value]\n\n onChange({\n ...condition,\n value: nextValues,\n })\n }\n\n\n return (\n <div\n className=\"grid grid-cols-[52px_minmax(150px,1fr)_120px_minmax(140px,1fr)_auto] items-center gap-2 rounded-lg border border-border/70 bg-background p-2 shadow-sm\"\n data-slot=\"condition-row\"\n >\n <div className=\"text-xs font-medium text-muted-foreground\">\n {index === 0 ? \"Where\" : \"And\"}\n </div>\n\n <Select value={condition.field} onValueChange={handleFieldChange}>\n <SelectTrigger className=\"h-8 w-full justify-start gap-2\" size=\"sm\">\n <FieldIcon className=\"h-3.5 w-3.5 text-muted-foreground\" />\n <SelectValue placeholder={fieldDef.label} />\n </SelectTrigger>\n <SelectContent>\n {fields.map((field) => {\n const Icon = getFieldIcon(field.type)\n return (\n <SelectItem key={field.id} value={field.id}>\n <span className=\"inline-flex items-center gap-2\">\n <Icon className=\"h-3.5 w-3.5 text-muted-foreground\" />\n {field.label}\n </span>\n </SelectItem>\n )\n })}\n </SelectContent>\n </Select>\n\n <Select\n value={condition.operator}\n onValueChange={(value) => handleOperatorChange(value as ConditionOperator)}\n >\n <SelectTrigger className=\"h-8 w-full\" size=\"sm\">\n <SelectValue placeholder=\"Operator\" />\n </SelectTrigger>\n <SelectContent>\n {operators.map((op) => (\n <SelectItem key={op} value={op}>\n {OPERATOR_LABELS[op]}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n\n {isUnary ? (\n <div className=\"h-8 rounded-md border border-dashed border-border/70 bg-muted/30 px-3 py-1.5 text-xs text-muted-foreground\">\n No value needed\n </div>\n ) : (\n <ConditionValueInput\n condition={condition}\n fieldDef={fieldDef}\n onValueChange={handleValueChange}\n onSelectValueChange={handleSelectValueChange}\n onMultiSelectValueToggle={handleMultiSelectValueToggle}\n onCommit={onCommit}\n />\n )}\n\n <div className=\"flex items-center gap-1\">\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon\"\n className=\"h-8 w-8 text-muted-foreground\"\n aria-label=\"Toggle condition visibility\"\n onClick={(event) => event.preventDefault()}\n >\n <Eye className=\"size-4\" />\n </Button>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon\"\n className=\"h-8 w-8 text-muted-foreground\"\n aria-label=\"More condition actions\"\n onClick={(event) => event.preventDefault()}\n >\n <MoreHorizontal className=\"size-4\" />\n </Button>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon\"\n className=\"h-8 w-8 text-muted-foreground hover:text-destructive\"\n onClick={onRemove}\n aria-label=\"Remove condition\"\n >\n <Trash2 className=\"size-4\" />\n </Button>\n </div>\n </div>\n )\n}\n\n// ── Main Component ─────────────────────────────────────────────\n\nfunction DataTableConditionFilter({\n fields,\n conditions,\n onConditionsChange,\n className,\n}: DataTableConditionFilterProps) {\n const [drafts, setDrafts] = React.useState<ConditionFilterValue[]>(() =>\n conditions.map((condition) => normalizeCondition(condition, fields)),\n )\n\n const fieldsSignature = React.useMemo(() => getFieldsSignature(fields), [fields])\n const conditionsSignature = React.useMemo(() => getConditionsSignature(conditions), [conditions])\n const fieldsRef = React.useRef(fields)\n\n React.useEffect(() => {\n setDrafts(conditions.map((condition) => normalizeCondition(condition, fieldsRef.current)))\n }, [conditionsSignature])\n\n React.useEffect(() => {\n if (fieldsRef.current !== fields) {\n fieldsRef.current = fields\n setDrafts((current) => current.map((condition) => normalizeCondition(condition, fields)))\n }\n // Depend on a structural signature so inline-but-equivalent field arrays do\n // not wipe in-progress drafts before Apply.\n }, [fieldsSignature, fields])\n\n const commitDrafts = React.useCallback(\n (nextDrafts: ConditionFilterValue[] = drafts) => {\n onConditionsChange(getCommittedConditions(nextDrafts, fields))\n },\n [drafts, fields, onConditionsChange],\n )\n\n const handleAdd = () => {\n const firstField = fields[0]\n if (!firstField) return\n const committedDrafts = getCommittedConditions(drafts, fields)\n const nextDrafts = [...committedDrafts, createDraftCondition(firstField)]\n setDrafts(nextDrafts)\n onConditionsChange(committedDrafts)\n }\n\n const handleUpdate = (index: number, updated: ConditionFilterValue) => {\n setDrafts((current) => {\n const next = [...current]\n next[index] = normalizeCondition(updated, fields)\n return next\n })\n }\n\n const handleRemove = (index: number) => {\n setDrafts((current) => {\n const next = current.filter((_, currentIndex) => currentIndex !== index)\n commitDrafts(next)\n return next\n })\n }\n\n const handleClear = () => {\n setDrafts([])\n onConditionsChange([])\n }\n\n const hasAppliedConditions = conditions.length > 0\n const hasDrafts = drafts.length > 0\n\n return (\n <div\n className={cn(\n \"w-[min(760px,calc(100vw-2rem))] rounded-xl border border-border bg-background p-3 text-foreground shadow-xl\",\n className,\n )}\n data-slot=\"condition-filter\"\n onClick={(event) => event.stopPropagation()}\n onKeyDown={(event) => {\n if (event.key !== \"Escape\") {\n event.stopPropagation()\n }\n }}\n >\n <div className=\"mb-3 flex items-center justify-between gap-3 border-b border-border/70 pb-3\">\n <div>\n <div className=\"text-sm font-semibold\">Filter builder</div>\n <div className=\"text-xs text-muted-foreground\">\n Build field, operator, and value conditions.\n </div>\n </div>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n className=\"h-8 text-xs text-destructive hover:text-destructive\"\n onClick={handleClear}\n disabled={!hasAppliedConditions && !hasDrafts}\n >\n Clear filters\n </Button>\n </div>\n\n <div className=\"flex flex-col gap-2\">\n {drafts.map((condition, index) => (\n <ConditionRow\n key={condition.id}\n condition={condition}\n fields={fields}\n index={index}\n onChange={(updated) => handleUpdate(index, updated)}\n onRemove={() => handleRemove(index)}\n onCommit={() => commitDrafts()}\n />\n ))}\n </div>\n\n {!hasDrafts ? (\n <div className=\"rounded-lg border border-dashed border-border/80 bg-muted/20 px-3 py-5 text-center text-xs text-muted-foreground\">\n No builder filters yet. Add a filter to start a condition row.\n </div>\n ) : null}\n\n <div className=\"mt-3 flex flex-wrap items-center justify-between gap-2 border-t border-border/70 pt-3\">\n <div className=\"flex items-center gap-2\">\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n className=\"h-8 text-xs text-muted-foreground\"\n onClick={handleAdd}\n >\n <Plus className=\"mr-1 size-3.5\" />\n Add filter\n </Button>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n className=\"h-8 text-xs text-muted-foreground opacity-60\"\n disabled\n aria-disabled=\"true\"\n >\n <Plus className=\"mr-1 size-3.5\" />\n Add filter group\n </Button>\n </div>\n <Button\n type=\"button\"\n size=\"sm\"\n className=\"h-8 text-xs\"\n onClick={() => commitDrafts()}\n >\n Apply\n </Button>\n </div>\n </div>\n )\n}\n\nexport {\n DataTableConditionFilter,\n OPERATOR_LABELS,\n DEFAULT_OPERATORS,\n generateConditionId,\n getOperators,\n}\nexport type { DataTableConditionFilterProps }\n"],"mappings":"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","names":["_a"]}
@@ -14,6 +14,14 @@ interface DataTableFilterCategory {
14
14
  options: (string | FilterOption)[];
15
15
  /** Filter behavior. Defaults to "multi" (checkbox multi-select). */
16
16
  type?: "multi" | "single" | "boolean";
17
+ /**
18
+ * Submenu search behavior. Defaults to the DataTableFilter
19
+ * optionSearchThreshold prop. Use true to always show search or false to
20
+ * hide it for a specific category.
21
+ */
22
+ searchable?: boolean | {
23
+ threshold?: number;
24
+ };
17
25
  }
18
26
  interface DataTableFilterProps {
19
27
  categories: DataTableFilterCategory[];
@@ -169,6 +169,13 @@ function DataTableFilter({
169
169
  const filteredOptions = subQuery ? category.options.filter(
170
170
  (opt) => getOptionLabel(opt).toLowerCase().includes(subQuery)
171
171
  ) : category.options;
172
+ const shouldShowSubmenuSearch = (() => {
173
+ var _a2;
174
+ if (category.searchable === true) return true;
175
+ if (category.searchable === false) return false;
176
+ const threshold = typeof category.searchable === "object" ? (_a2 = category.searchable.threshold) != null ? _a2 : optionSearchThreshold : optionSearchThreshold;
177
+ return category.options.length > threshold;
178
+ })();
172
179
  return /* @__PURE__ */ jsxs(
173
180
  DropdownMenuSub,
174
181
  {
@@ -187,7 +194,7 @@ function DataTableFilter({
187
194
  category.label
188
195
  ] }),
189
196
  /* @__PURE__ */ jsxs(DropdownMenuSubContent, { className: "max-h-[320px] w-52 overflow-y-auto p-1", children: [
190
- category.options.length > optionSearchThreshold && /* @__PURE__ */ jsx("div", { className: "sticky top-0 z-10 border-b border-border bg-popover p-1.5", children: /* @__PURE__ */ jsxs("div", { className: "relative", children: [
197
+ shouldShowSubmenuSearch && /* @__PURE__ */ jsx("div", { className: "sticky top-0 z-10 border-b border-border bg-popover p-1.5", children: /* @__PURE__ */ jsxs("div", { className: "relative", children: [
191
198
  /* @__PURE__ */ jsx(Search, { className: "absolute left-2 top-1/2 h-3 w-3 -translate-y-1/2 text-muted-foreground" }),
192
199
  /* @__PURE__ */ jsx(
193
200
  "input",
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/components/data-table-filter.tsx"],"sourcesContent":["\"use client\"\n\nimport * as React from \"react\"\nimport { Check, ListFilter, Plus, Search } from \"lucide-react\"\n\nimport { Popover as PopoverPrimitive } from \"radix-ui\"\n\nimport { cn } from \"../lib/utils\"\nimport { Button } from \"./button\"\nimport {\n DataTableConditionFilter,\n type ConditionFieldDef,\n type ConditionFilterValue,\n} from \"./data-table-condition-filter\"\nimport {\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuSub,\n DropdownMenuSubContent,\n DropdownMenuSubTrigger,\n DropdownMenuTrigger,\n} from \"./dropdown-menu\"\n\nexport interface FilterOption {\n label: string\n value: string\n}\n\nexport interface DataTableFilterCategory {\n id: string\n label: string\n icon: React.ComponentType<{ className?: string }>\n options: (string | FilterOption)[]\n /** Filter behavior. Defaults to \"multi\" (checkbox multi-select). */\n type?: \"multi\" | \"single\" | \"boolean\"\n}\n\nfunction getOptionValue(option: string | FilterOption): string {\n return typeof option === \"string\" ? option : option.value\n}\nfunction getOptionLabel(option: string | FilterOption): string {\n return typeof option === \"string\" ? option : option.label\n}\n\nexport interface DataTableFilterProps {\n categories: DataTableFilterCategory[]\n selectedFilters: Record<string, string[]>\n onToggleFilter: (categoryId: string, option: string) => void\n className?: string\n /** Minimum number of options before showing the sub-menu search input. Defaults to 8. */\n optionSearchThreshold?: number\n /** Filters applied by default. Shown as distinct chips that can be toggled off but not dismissed. */\n presetFilters?: Record<string, string[]>\n /** Callback when a preset filter is toggled on/off. */\n onTogglePreset?: (categoryId: string, option: string) => void\n /** Label shown on preset chips to distinguish from user-applied filters. Default: \"Default\". */\n presetLabel?: string\n /** Fields exposed in the unified condition-builder panel. */\n conditionFields?: ConditionFieldDef[]\n /** Active builder-managed field/operator/value conditions. */\n conditionFilters?: ConditionFilterValue[]\n /** Callback when builder-managed conditions are applied, removed, or cleared. */\n onConditionFiltersChange?: (conditions: ConditionFilterValue[]) => void\n /** Dropdown entry label for the condition-builder panel. Default: \"Add filter\". */\n conditionBuilderLabel?: string\n}\n\nexport function DataTableFilter({\n categories,\n selectedFilters,\n onToggleFilter,\n className,\n optionSearchThreshold = 8,\n presetFilters,\n onTogglePreset,\n presetLabel = \"Default\",\n conditionFields = [],\n conditionFilters = [],\n onConditionFiltersChange,\n conditionBuilderLabel = \"Add filter\",\n}: DataTableFilterProps) {\n const [query, setQuery] = React.useState(\"\")\n const [subQueries, setSubQueries] = React.useState<Record<string, string>>({})\n const [conditionBuilderOpen, setConditionBuilderOpen] = React.useState(false)\n const hasConditionBuilder = conditionFields.length > 0\n\n const visibleCategories = React.useMemo(() => {\n const normalized = query.trim().toLowerCase()\n if (!normalized) {\n return categories\n }\n\n return categories.filter((category) => {\n if (category.label.toLowerCase().includes(normalized)) {\n return true\n }\n\n return category.options.some((option) =>\n getOptionLabel(option).toLowerCase().includes(normalized)\n )\n })\n }, [categories, query])\n\n /** Check if a specific option is a preset filter */\n const isPresetOption = React.useCallback(\n (categoryId: string, value: string): boolean => {\n return presetFilters?.[categoryId]?.includes(value) ?? false\n },\n [presetFilters]\n )\n\n const activeCount = React.useMemo(() => {\n const userCount = Object.values(selectedFilters).reduce(\n (count, selected) => count + selected.length,\n 0\n )\n\n return userCount + conditionFilters.length\n }, [selectedFilters, conditionFilters.length])\n\n /** Collect all preset chips to render */\n const presetChips = React.useMemo(() => {\n if (!presetFilters) return []\n\n const chips: { categoryId: string; value: string; label: string; active: boolean }[] = []\n\n for (const [categoryId, values] of Object.entries(presetFilters)) {\n const category = categories.find((c) => c.id === categoryId)\n for (const value of values) {\n const option = category?.options.find(\n (opt) => getOptionValue(opt) === value\n )\n const label = option ? getOptionLabel(option) : value\n const active = selectedFilters[categoryId]?.includes(value) ?? false\n chips.push({ categoryId, value, label, active })\n }\n }\n\n return chips\n }, [presetFilters, categories, selectedFilters])\n\n const triggerButton = (\n <DropdownMenu>\n <DropdownMenuTrigger asChild>\n <Button\n variant=\"outline\"\n size=\"sm\"\n className={cn(\n \"h-8 gap-2 rounded-md border-border/60 bg-background text-xs font-normal shadow-none hover:bg-muted/50\",\n className\n )}\n >\n <ListFilter className=\"h-3.5 w-3.5\" />\n Filter\n {activeCount > 0 ? (\n <span className=\"rounded bg-muted px-1.5 py-0 text-[10px] font-semibold\">\n {activeCount}\n </span>\n ) : null}\n </Button>\n </DropdownMenuTrigger>\n <DropdownMenuContent align=\"start\" className=\"w-[240px] p-0\">\n <div className=\"sticky top-0 z-10 border-b border-border bg-popover p-2\">\n <div className=\"relative\">\n <Search className=\"absolute left-2 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-muted-foreground\" />\n <input\n className=\"h-8 w-full rounded-md bg-muted/50 py-1 pr-2 pl-7 text-xs outline-none transition-colors placeholder:text-muted-foreground/70 focus:bg-muted\"\n placeholder=\"Search filters...\"\n value={query}\n onChange={(event) => setQuery(event.target.value)}\n onClick={(event) => event.stopPropagation()}\n onKeyDown={(event) => event.stopPropagation()}\n />\n </div>\n </div>\n\n <div className=\"max-h-[320px] overflow-y-auto p-1\">\n {visibleCategories.map((category) => {\n const filterType = category.type ?? \"multi\"\n\n /* ── Boolean toggle ─────────────────────────────────── */\n if (filterType === \"boolean\") {\n const active = selectedFilters[category.id]?.includes(\"true\") ?? false\n return (\n <DropdownMenuItem\n key={category.id}\n className={cn(\n \"cursor-pointer py-1.5 text-xs\",\n active && \"text-brand-purple\"\n )}\n onSelect={(event) => {\n event.preventDefault()\n onToggleFilter(category.id, \"true\")\n }}\n >\n <category.icon className=\"mr-2 h-3.5 w-3.5\" />\n {category.label}\n {active ? <Check className=\"ml-auto h-4 w-4\" /> : null}\n </DropdownMenuItem>\n )\n }\n\n /* ── Sub-menu (single / multi) ──────────────────────── */\n const subQuery = (subQueries[category.id] ?? \"\").trim().toLowerCase()\n const filteredOptions = subQuery\n ? category.options.filter((opt) =>\n getOptionLabel(opt).toLowerCase().includes(subQuery)\n )\n : category.options\n\n return (\n <DropdownMenuSub\n key={category.id}\n onOpenChange={(open) => {\n if (!open) {\n setSubQueries((prev) => {\n const next = { ...prev }\n delete next[category.id]\n return next\n })\n }\n }}\n >\n <DropdownMenuSubTrigger className=\"cursor-pointer py-1.5 text-xs\">\n <category.icon className=\"mr-2 h-3.5 w-3.5 text-muted-foreground\" />\n {category.label}\n </DropdownMenuSubTrigger>\n <DropdownMenuSubContent className=\"max-h-[320px] w-52 overflow-y-auto p-1\">\n {/* Submenu search — only for categories with many options */}\n {category.options.length > optionSearchThreshold && (\n <div className=\"sticky top-0 z-10 border-b border-border bg-popover p-1.5\">\n <div className=\"relative\">\n <Search className=\"absolute left-2 top-1/2 h-3 w-3 -translate-y-1/2 text-muted-foreground\" />\n <input\n className=\"h-7 w-full rounded-md bg-muted/50 py-1 pr-2 pl-7 text-xs outline-none transition-colors placeholder:text-muted-foreground/70 focus:bg-muted\"\n placeholder=\"Search...\"\n value={subQueries[category.id] ?? \"\"}\n onChange={(e) =>\n setSubQueries((prev) => ({ ...prev, [category.id]: e.target.value }))\n }\n onClick={(e) => e.stopPropagation()}\n onKeyDown={(e) => {\n // Allow navigation keys to propagate to Radix menu handling\n // so keyboard users can move to and select filtered options.\n const navKeys = [\"ArrowDown\", \"ArrowUp\", \"Enter\", \"Escape\", \"Tab\"]\n if (!navKeys.includes(e.key)) {\n e.stopPropagation()\n }\n }}\n />\n </div>\n </div>\n )}\n {/* Filtered options */}\n {filteredOptions.map((option) => {\n const value = getOptionValue(option)\n const label = getOptionLabel(option)\n const selected = selectedFilters[category.id]?.includes(value) ?? false\n const isPreset = isPresetOption(category.id, value)\n return (\n <DropdownMenuItem\n key={value}\n className=\"cursor-pointer justify-between text-xs\"\n onSelect={(event) => {\n event.preventDefault()\n onToggleFilter(category.id, value)\n }}\n >\n {label}\n {selected ? (\n isPreset ? (\n <span className=\"text-brand-purple text-[10px] font-semibold\">\n {presetLabel}\n </span>\n ) : filterType === \"single\" ? (\n <span className=\"h-1.5 w-1.5 rounded-full bg-current\" />\n ) : (\n <span className=\"text-[10px] font-semibold text-brand-purple\">\n Applied\n </span>\n )\n ) : null}\n </DropdownMenuItem>\n )\n })}\n {filteredOptions.length === 0 && category.options.length > 0 && (\n <div className=\"p-2 text-center text-xs text-muted-foreground\">\n No matches\n </div>\n )}\n </DropdownMenuSubContent>\n </DropdownMenuSub>\n )\n })}\n\n {visibleCategories.length === 0 ? (\n <div className=\"p-2 text-center text-xs text-muted-foreground\">\n No filters found\n </div>\n ) : null}\n </div>\n\n {hasConditionBuilder ? (\n <div className=\"border-t border-border p-1\">\n <PopoverPrimitive.Root\n open={conditionBuilderOpen}\n onOpenChange={setConditionBuilderOpen}\n >\n <PopoverPrimitive.Trigger asChild>\n <DropdownMenuItem\n className=\"cursor-pointer py-1.5 text-xs\"\n onSelect={(event) => {\n event.preventDefault()\n setConditionBuilderOpen(true)\n }}\n >\n <Plus className=\"mr-2 h-3.5 w-3.5 text-muted-foreground\" />\n {conditionBuilderLabel}\n {conditionFilters.length > 0 ? (\n <span className=\"ml-auto rounded bg-muted px-1.5 py-0 text-[10px] font-semibold\">\n {conditionFilters.length}\n </span>\n ) : null}\n </DropdownMenuItem>\n </PopoverPrimitive.Trigger>\n <PopoverPrimitive.Portal>\n <PopoverPrimitive.Content\n align=\"start\"\n side=\"right\"\n sideOffset={8}\n className=\"z-50 outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95\"\n onEscapeKeyDown={() => setConditionBuilderOpen(false)}\n onInteractOutside={(event) => {\n const target = event.target as HTMLElement | null\n if (target?.closest('[data-slot=\"dropdown-menu-content\"]')) {\n event.preventDefault()\n }\n }}\n >\n <DataTableConditionFilter\n fields={conditionFields}\n conditions={conditionFilters}\n onConditionsChange={onConditionFiltersChange ?? (() => {})}\n />\n </PopoverPrimitive.Content>\n </PopoverPrimitive.Portal>\n </PopoverPrimitive.Root>\n </div>\n ) : null}\n </DropdownMenuContent>\n </DropdownMenu>\n )\n\n // If there are preset chips, wrap trigger + chips together\n if (presetChips.length > 0) {\n return (\n <div className=\"flex flex-wrap items-center gap-1.5\">\n {triggerButton}\n {presetChips.map((chip) => (\n <button\n key={`${chip.categoryId}-${chip.value}`}\n type=\"button\"\n onClick={() => onTogglePreset?.(chip.categoryId, chip.value)}\n className={cn(\n \"inline-flex items-center gap-1 rounded-md border px-2 py-0.5 text-[11px] font-medium transition-colors\",\n chip.active\n ? \"border-dashed border-brand-purple/30 bg-brand-purple/5 text-brand-purple/80\"\n : \"border-border/40 bg-transparent text-muted-foreground/60 line-through\"\n )}\n >\n <span className=\"text-brand-purple/50 text-[10px]\">\n {presetLabel}:{\" \"}\n </span>\n {chip.label}\n </button>\n ))}\n </div>\n )\n }\n\n return triggerButton\n}\n"],"mappings":"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","names":["_a","_b"]}
1
+ {"version":3,"sources":["../../src/components/data-table-filter.tsx"],"sourcesContent":["\"use client\"\n\nimport * as React from \"react\"\nimport { Check, ListFilter, Plus, Search } from \"lucide-react\"\n\nimport { Popover as PopoverPrimitive } from \"radix-ui\"\n\nimport { cn } from \"../lib/utils\"\nimport { Button } from \"./button\"\nimport {\n DataTableConditionFilter,\n type ConditionFieldDef,\n type ConditionFilterValue,\n} from \"./data-table-condition-filter\"\nimport {\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuSub,\n DropdownMenuSubContent,\n DropdownMenuSubTrigger,\n DropdownMenuTrigger,\n} from \"./dropdown-menu\"\n\nexport interface FilterOption {\n label: string\n value: string\n}\n\nexport interface DataTableFilterCategory {\n id: string\n label: string\n icon: React.ComponentType<{ className?: string }>\n options: (string | FilterOption)[]\n /** Filter behavior. Defaults to \"multi\" (checkbox multi-select). */\n type?: \"multi\" | \"single\" | \"boolean\"\n /**\n * Submenu search behavior. Defaults to the DataTableFilter\n * optionSearchThreshold prop. Use true to always show search or false to\n * hide it for a specific category.\n */\n searchable?: boolean | { threshold?: number }\n}\n\nfunction getOptionValue(option: string | FilterOption): string {\n return typeof option === \"string\" ? option : option.value\n}\nfunction getOptionLabel(option: string | FilterOption): string {\n return typeof option === \"string\" ? option : option.label\n}\n\nexport interface DataTableFilterProps {\n categories: DataTableFilterCategory[]\n selectedFilters: Record<string, string[]>\n onToggleFilter: (categoryId: string, option: string) => void\n className?: string\n /** Minimum number of options before showing the sub-menu search input. Defaults to 8. */\n optionSearchThreshold?: number\n /** Filters applied by default. Shown as distinct chips that can be toggled off but not dismissed. */\n presetFilters?: Record<string, string[]>\n /** Callback when a preset filter is toggled on/off. */\n onTogglePreset?: (categoryId: string, option: string) => void\n /** Label shown on preset chips to distinguish from user-applied filters. Default: \"Default\". */\n presetLabel?: string\n /** Fields exposed in the unified condition-builder panel. */\n conditionFields?: ConditionFieldDef[]\n /** Active builder-managed field/operator/value conditions. */\n conditionFilters?: ConditionFilterValue[]\n /** Callback when builder-managed conditions are applied, removed, or cleared. */\n onConditionFiltersChange?: (conditions: ConditionFilterValue[]) => void\n /** Dropdown entry label for the condition-builder panel. Default: \"Add filter\". */\n conditionBuilderLabel?: string\n}\n\nexport function DataTableFilter({\n categories,\n selectedFilters,\n onToggleFilter,\n className,\n optionSearchThreshold = 8,\n presetFilters,\n onTogglePreset,\n presetLabel = \"Default\",\n conditionFields = [],\n conditionFilters = [],\n onConditionFiltersChange,\n conditionBuilderLabel = \"Add filter\",\n}: DataTableFilterProps) {\n const [query, setQuery] = React.useState(\"\")\n const [subQueries, setSubQueries] = React.useState<Record<string, string>>({})\n const [conditionBuilderOpen, setConditionBuilderOpen] = React.useState(false)\n const hasConditionBuilder = conditionFields.length > 0\n\n const visibleCategories = React.useMemo(() => {\n const normalized = query.trim().toLowerCase()\n if (!normalized) {\n return categories\n }\n\n return categories.filter((category) => {\n if (category.label.toLowerCase().includes(normalized)) {\n return true\n }\n\n return category.options.some((option) =>\n getOptionLabel(option).toLowerCase().includes(normalized)\n )\n })\n }, [categories, query])\n\n /** Check if a specific option is a preset filter */\n const isPresetOption = React.useCallback(\n (categoryId: string, value: string): boolean => {\n return presetFilters?.[categoryId]?.includes(value) ?? false\n },\n [presetFilters]\n )\n\n const activeCount = React.useMemo(() => {\n const userCount = Object.values(selectedFilters).reduce(\n (count, selected) => count + selected.length,\n 0\n )\n\n return userCount + conditionFilters.length\n }, [selectedFilters, conditionFilters.length])\n\n /** Collect all preset chips to render */\n const presetChips = React.useMemo(() => {\n if (!presetFilters) return []\n\n const chips: { categoryId: string; value: string; label: string; active: boolean }[] = []\n\n for (const [categoryId, values] of Object.entries(presetFilters)) {\n const category = categories.find((c) => c.id === categoryId)\n for (const value of values) {\n const option = category?.options.find(\n (opt) => getOptionValue(opt) === value\n )\n const label = option ? getOptionLabel(option) : value\n const active = selectedFilters[categoryId]?.includes(value) ?? false\n chips.push({ categoryId, value, label, active })\n }\n }\n\n return chips\n }, [presetFilters, categories, selectedFilters])\n\n const triggerButton = (\n <DropdownMenu>\n <DropdownMenuTrigger asChild>\n <Button\n variant=\"outline\"\n size=\"sm\"\n className={cn(\n \"h-8 gap-2 rounded-md border-border/60 bg-background text-xs font-normal shadow-none hover:bg-muted/50\",\n className\n )}\n >\n <ListFilter className=\"h-3.5 w-3.5\" />\n Filter\n {activeCount > 0 ? (\n <span className=\"rounded bg-muted px-1.5 py-0 text-[10px] font-semibold\">\n {activeCount}\n </span>\n ) : null}\n </Button>\n </DropdownMenuTrigger>\n <DropdownMenuContent align=\"start\" className=\"w-[240px] p-0\">\n <div className=\"sticky top-0 z-10 border-b border-border bg-popover p-2\">\n <div className=\"relative\">\n <Search className=\"absolute left-2 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-muted-foreground\" />\n <input\n className=\"h-8 w-full rounded-md bg-muted/50 py-1 pr-2 pl-7 text-xs outline-none transition-colors placeholder:text-muted-foreground/70 focus:bg-muted\"\n placeholder=\"Search filters...\"\n value={query}\n onChange={(event) => setQuery(event.target.value)}\n onClick={(event) => event.stopPropagation()}\n onKeyDown={(event) => event.stopPropagation()}\n />\n </div>\n </div>\n\n <div className=\"max-h-[320px] overflow-y-auto p-1\">\n {visibleCategories.map((category) => {\n const filterType = category.type ?? \"multi\"\n\n /* ── Boolean toggle ─────────────────────────────────── */\n if (filterType === \"boolean\") {\n const active = selectedFilters[category.id]?.includes(\"true\") ?? false\n return (\n <DropdownMenuItem\n key={category.id}\n className={cn(\n \"cursor-pointer py-1.5 text-xs\",\n active && \"text-brand-purple\"\n )}\n onSelect={(event) => {\n event.preventDefault()\n onToggleFilter(category.id, \"true\")\n }}\n >\n <category.icon className=\"mr-2 h-3.5 w-3.5\" />\n {category.label}\n {active ? <Check className=\"ml-auto h-4 w-4\" /> : null}\n </DropdownMenuItem>\n )\n }\n\n /* ── Sub-menu (single / multi) ──────────────────────── */\n const subQuery = (subQueries[category.id] ?? \"\").trim().toLowerCase()\n const filteredOptions = subQuery\n ? category.options.filter((opt) =>\n getOptionLabel(opt).toLowerCase().includes(subQuery)\n )\n : category.options\n const shouldShowSubmenuSearch = (() => {\n if (category.searchable === true) return true\n if (category.searchable === false) return false\n const threshold =\n typeof category.searchable === \"object\"\n ? (category.searchable.threshold ?? optionSearchThreshold)\n : optionSearchThreshold\n return category.options.length > threshold\n })()\n\n return (\n <DropdownMenuSub\n key={category.id}\n onOpenChange={(open) => {\n if (!open) {\n setSubQueries((prev) => {\n const next = { ...prev }\n delete next[category.id]\n return next\n })\n }\n }}\n >\n <DropdownMenuSubTrigger className=\"cursor-pointer py-1.5 text-xs\">\n <category.icon className=\"mr-2 h-3.5 w-3.5 text-muted-foreground\" />\n {category.label}\n </DropdownMenuSubTrigger>\n <DropdownMenuSubContent className=\"max-h-[320px] w-52 overflow-y-auto p-1\">\n {/* Submenu search — shown for long lists or categories that opt in. */}\n {shouldShowSubmenuSearch && (\n <div className=\"sticky top-0 z-10 border-b border-border bg-popover p-1.5\">\n <div className=\"relative\">\n <Search className=\"absolute left-2 top-1/2 h-3 w-3 -translate-y-1/2 text-muted-foreground\" />\n <input\n className=\"h-7 w-full rounded-md bg-muted/50 py-1 pr-2 pl-7 text-xs outline-none transition-colors placeholder:text-muted-foreground/70 focus:bg-muted\"\n placeholder=\"Search...\"\n value={subQueries[category.id] ?? \"\"}\n onChange={(e) =>\n setSubQueries((prev) => ({ ...prev, [category.id]: e.target.value }))\n }\n onClick={(e) => e.stopPropagation()}\n onKeyDown={(e) => {\n // Allow navigation keys to propagate to Radix menu handling\n // so keyboard users can move to and select filtered options.\n const navKeys = [\"ArrowDown\", \"ArrowUp\", \"Enter\", \"Escape\", \"Tab\"]\n if (!navKeys.includes(e.key)) {\n e.stopPropagation()\n }\n }}\n />\n </div>\n </div>\n )}\n {/* Filtered options */}\n {filteredOptions.map((option) => {\n const value = getOptionValue(option)\n const label = getOptionLabel(option)\n const selected = selectedFilters[category.id]?.includes(value) ?? false\n const isPreset = isPresetOption(category.id, value)\n return (\n <DropdownMenuItem\n key={value}\n className=\"cursor-pointer justify-between text-xs\"\n onSelect={(event) => {\n event.preventDefault()\n onToggleFilter(category.id, value)\n }}\n >\n {label}\n {selected ? (\n isPreset ? (\n <span className=\"text-brand-purple text-[10px] font-semibold\">\n {presetLabel}\n </span>\n ) : filterType === \"single\" ? (\n <span className=\"h-1.5 w-1.5 rounded-full bg-current\" />\n ) : (\n <span className=\"text-[10px] font-semibold text-brand-purple\">\n Applied\n </span>\n )\n ) : null}\n </DropdownMenuItem>\n )\n })}\n {filteredOptions.length === 0 && category.options.length > 0 && (\n <div className=\"p-2 text-center text-xs text-muted-foreground\">\n No matches\n </div>\n )}\n </DropdownMenuSubContent>\n </DropdownMenuSub>\n )\n })}\n\n {visibleCategories.length === 0 ? (\n <div className=\"p-2 text-center text-xs text-muted-foreground\">\n No filters found\n </div>\n ) : null}\n </div>\n\n {hasConditionBuilder ? (\n <div className=\"border-t border-border p-1\">\n <PopoverPrimitive.Root\n open={conditionBuilderOpen}\n onOpenChange={setConditionBuilderOpen}\n >\n <PopoverPrimitive.Trigger asChild>\n <DropdownMenuItem\n className=\"cursor-pointer py-1.5 text-xs\"\n onSelect={(event) => {\n event.preventDefault()\n setConditionBuilderOpen(true)\n }}\n >\n <Plus className=\"mr-2 h-3.5 w-3.5 text-muted-foreground\" />\n {conditionBuilderLabel}\n {conditionFilters.length > 0 ? (\n <span className=\"ml-auto rounded bg-muted px-1.5 py-0 text-[10px] font-semibold\">\n {conditionFilters.length}\n </span>\n ) : null}\n </DropdownMenuItem>\n </PopoverPrimitive.Trigger>\n <PopoverPrimitive.Portal>\n <PopoverPrimitive.Content\n align=\"start\"\n side=\"right\"\n sideOffset={8}\n className=\"z-50 outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95\"\n onEscapeKeyDown={() => setConditionBuilderOpen(false)}\n onInteractOutside={(event) => {\n const target = event.target as HTMLElement | null\n if (target?.closest('[data-slot=\"dropdown-menu-content\"]')) {\n event.preventDefault()\n }\n }}\n >\n <DataTableConditionFilter\n fields={conditionFields}\n conditions={conditionFilters}\n onConditionsChange={onConditionFiltersChange ?? (() => {})}\n />\n </PopoverPrimitive.Content>\n </PopoverPrimitive.Portal>\n </PopoverPrimitive.Root>\n </div>\n ) : null}\n </DropdownMenuContent>\n </DropdownMenu>\n )\n\n // If there are preset chips, wrap trigger + chips together\n if (presetChips.length > 0) {\n return (\n <div className=\"flex flex-wrap items-center gap-1.5\">\n {triggerButton}\n {presetChips.map((chip) => (\n <button\n key={`${chip.categoryId}-${chip.value}`}\n type=\"button\"\n onClick={() => onTogglePreset?.(chip.categoryId, chip.value)}\n className={cn(\n \"inline-flex items-center gap-1 rounded-md border px-2 py-0.5 text-[11px] font-medium transition-colors\",\n chip.active\n ? \"border-dashed border-brand-purple/30 bg-brand-purple/5 text-brand-purple/80\"\n : \"border-border/40 bg-transparent text-muted-foreground/60 line-through\"\n )}\n >\n <span className=\"text-brand-purple/50 text-[10px]\">\n {presetLabel}:{\" \"}\n </span>\n {chip.label}\n </button>\n ))}\n </div>\n )\n }\n\n return triggerButton\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAuJQ,SAQE,KARF;AArJR,YAAY,WAAW;AACvB,SAAS,OAAO,YAAY,MAAM,cAAc;AAEhD,SAAS,WAAW,wBAAwB;AAE5C,SAAS,UAAU;AACnB,SAAS,cAAc;AACvB;AAAA,EACE;AAAA,OAGK;AACP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAsBP,SAAS,eAAe,QAAuC;AAC7D,SAAO,OAAO,WAAW,WAAW,SAAS,OAAO;AACtD;AACA,SAAS,eAAe,QAAuC;AAC7D,SAAO,OAAO,WAAW,WAAW,SAAS,OAAO;AACtD;AAyBO,SAAS,gBAAgB;AAAA,EAC9B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,wBAAwB;AAAA,EACxB;AAAA,EACA;AAAA,EACA,cAAc;AAAA,EACd,kBAAkB,CAAC;AAAA,EACnB,mBAAmB,CAAC;AAAA,EACpB;AAAA,EACA,wBAAwB;AAC1B,GAAyB;AACvB,QAAM,CAAC,OAAO,QAAQ,IAAI,MAAM,SAAS,EAAE;AAC3C,QAAM,CAAC,YAAY,aAAa,IAAI,MAAM,SAAiC,CAAC,CAAC;AAC7E,QAAM,CAAC,sBAAsB,uBAAuB,IAAI,MAAM,SAAS,KAAK;AAC5E,QAAM,sBAAsB,gBAAgB,SAAS;AAErD,QAAM,oBAAoB,MAAM,QAAQ,MAAM;AAC5C,UAAM,aAAa,MAAM,KAAK,EAAE,YAAY;AAC5C,QAAI,CAAC,YAAY;AACf,aAAO;AAAA,IACT;AAEA,WAAO,WAAW,OAAO,CAAC,aAAa;AACrC,UAAI,SAAS,MAAM,YAAY,EAAE,SAAS,UAAU,GAAG;AACrD,eAAO;AAAA,MACT;AAEA,aAAO,SAAS,QAAQ;AAAA,QAAK,CAAC,WAC5B,eAAe,MAAM,EAAE,YAAY,EAAE,SAAS,UAAU;AAAA,MAC1D;AAAA,IACF,CAAC;AAAA,EACH,GAAG,CAAC,YAAY,KAAK,CAAC;AAGtB,QAAM,iBAAiB,MAAM;AAAA,IAC3B,CAAC,YAAoB,UAA2B;AAhHpD;AAiHM,cAAO,0DAAgB,gBAAhB,mBAA6B,SAAS,WAAtC,YAAgD;AAAA,IACzD;AAAA,IACA,CAAC,aAAa;AAAA,EAChB;AAEA,QAAM,cAAc,MAAM,QAAQ,MAAM;AACtC,UAAM,YAAY,OAAO,OAAO,eAAe,EAAE;AAAA,MAC/C,CAAC,OAAO,aAAa,QAAQ,SAAS;AAAA,MACtC;AAAA,IACF;AAEA,WAAO,YAAY,iBAAiB;AAAA,EACtC,GAAG,CAAC,iBAAiB,iBAAiB,MAAM,CAAC;AAG7C,QAAM,cAAc,MAAM,QAAQ,MAAM;AAhI1C;AAiII,QAAI,CAAC,cAAe,QAAO,CAAC;AAE5B,UAAM,QAAiF,CAAC;AAExF,eAAW,CAAC,YAAY,MAAM,KAAK,OAAO,QAAQ,aAAa,GAAG;AAChE,YAAM,WAAW,WAAW,KAAK,CAAC,MAAM,EAAE,OAAO,UAAU;AAC3D,iBAAW,SAAS,QAAQ;AAC1B,cAAM,SAAS,qCAAU,QAAQ;AAAA,UAC/B,CAAC,QAAQ,eAAe,GAAG,MAAM;AAAA;AAEnC,cAAM,QAAQ,SAAS,eAAe,MAAM,IAAI;AAChD,cAAM,UAAS,2BAAgB,UAAU,MAA1B,mBAA6B,SAAS,WAAtC,YAAgD;AAC/D,cAAM,KAAK,EAAE,YAAY,OAAO,OAAO,OAAO,CAAC;AAAA,MACjD;AAAA,IACF;AAEA,WAAO;AAAA,EACT,GAAG,CAAC,eAAe,YAAY,eAAe,CAAC;AAE/C,QAAM,gBACJ,qBAAC,gBACC;AAAA,wBAAC,uBAAoB,SAAO,MAC1B;AAAA,MAAC;AAAA;AAAA,QACC,SAAQ;AAAA,QACR,MAAK;AAAA,QACL,WAAW;AAAA,UACT;AAAA,UACA;AAAA,QACF;AAAA,QAEA;AAAA,8BAAC,cAAW,WAAU,eAAc;AAAA,UAAE;AAAA,UAErC,cAAc,IACb,oBAAC,UAAK,WAAU,0DACb,uBACH,IACE;AAAA;AAAA;AAAA,IACN,GACF;AAAA,IACA,qBAAC,uBAAoB,OAAM,SAAQ,WAAU,iBAC3C;AAAA,0BAAC,SAAI,WAAU,2DACb,+BAAC,SAAI,WAAU,YACb;AAAA,4BAAC,UAAO,WAAU,8EAA6E;AAAA,QAC/F;AAAA,UAAC;AAAA;AAAA,YACC,WAAU;AAAA,YACV,aAAY;AAAA,YACZ,OAAO;AAAA,YACP,UAAU,CAAC,UAAU,SAAS,MAAM,OAAO,KAAK;AAAA,YAChD,SAAS,CAAC,UAAU,MAAM,gBAAgB;AAAA,YAC1C,WAAW,CAAC,UAAU,MAAM,gBAAgB;AAAA;AAAA,QAC9C;AAAA,SACF,GACF;AAAA,MAEA,qBAAC,SAAI,WAAU,qCACZ;AAAA,0BAAkB,IAAI,CAAC,aAAa;AAxL/C;AAyLY,gBAAM,cAAa,cAAS,SAAT,YAAiB;AAGpC,cAAI,eAAe,WAAW;AAC5B,kBAAM,UAAS,2BAAgB,SAAS,EAAE,MAA3B,mBAA8B,SAAS,YAAvC,YAAkD;AACjE,mBACE;AAAA,cAAC;AAAA;AAAA,gBAEC,WAAW;AAAA,kBACT;AAAA,kBACA,UAAU;AAAA,gBACZ;AAAA,gBACA,UAAU,CAAC,UAAU;AACnB,wBAAM,eAAe;AACrB,iCAAe,SAAS,IAAI,MAAM;AAAA,gBACpC;AAAA,gBAEA;AAAA,sCAAC,SAAS,MAAT,EAAc,WAAU,oBAAmB;AAAA,kBAC3C,SAAS;AAAA,kBACT,SAAS,oBAAC,SAAM,WAAU,mBAAkB,IAAK;AAAA;AAAA;AAAA,cAZ7C,SAAS;AAAA,YAahB;AAAA,UAEJ;AAGA,gBAAM,aAAY,gBAAW,SAAS,EAAE,MAAtB,YAA2B,IAAI,KAAK,EAAE,YAAY;AACpE,gBAAM,kBAAkB,WACpB,SAAS,QAAQ;AAAA,YAAO,CAAC,QACvB,eAAe,GAAG,EAAE,YAAY,EAAE,SAAS,QAAQ;AAAA,UACrD,IACA,SAAS;AACb,gBAAM,2BAA2B,MAAM;AAxNnD,gBAAAA;AAyNc,gBAAI,SAAS,eAAe,KAAM,QAAO;AACzC,gBAAI,SAAS,eAAe,MAAO,QAAO;AAC1C,kBAAM,YACJ,OAAO,SAAS,eAAe,YAC1BA,MAAA,SAAS,WAAW,cAApB,OAAAA,MAAiC,wBAClC;AACN,mBAAO,SAAS,QAAQ,SAAS;AAAA,UACnC,GAAG;AAEH,iBACE;AAAA,YAAC;AAAA;AAAA,cAEC,cAAc,CAAC,SAAS;AACtB,oBAAI,CAAC,MAAM;AACT,gCAAc,CAAC,SAAS;AACtB,0BAAM,OAAO,mBAAK;AAClB,2BAAO,KAAK,SAAS,EAAE;AACvB,2BAAO;AAAA,kBACT,CAAC;AAAA,gBACH;AAAA,cACF;AAAA,cAEA;AAAA,qCAAC,0BAAuB,WAAU,iCAChC;AAAA,sCAAC,SAAS,MAAT,EAAc,WAAU,0CAAyC;AAAA,kBACjE,SAAS;AAAA,mBACZ;AAAA,gBACA,qBAAC,0BAAuB,WAAU,0CAE/B;AAAA,6CACC,oBAAC,SAAI,WAAU,6DACb,+BAAC,SAAI,WAAU,YACb;AAAA,wCAAC,UAAO,WAAU,0EAAyE;AAAA,oBAC3F;AAAA,sBAAC;AAAA;AAAA,wBACC,WAAU;AAAA,wBACV,aAAY;AAAA,wBACZ,QAAO,gBAAW,SAAS,EAAE,MAAtB,YAA2B;AAAA,wBAClC,UAAU,CAAC,MACT,cAAc,CAAC,SAAU,iCAAK,OAAL,EAAW,CAAC,SAAS,EAAE,GAAG,EAAE,OAAO,MAAM,EAAE;AAAA,wBAEtE,SAAS,CAAC,MAAM,EAAE,gBAAgB;AAAA,wBAClC,WAAW,CAAC,MAAM;AAGhB,gCAAM,UAAU,CAAC,aAAa,WAAW,SAAS,UAAU,KAAK;AACjE,8BAAI,CAAC,QAAQ,SAAS,EAAE,GAAG,GAAG;AAC5B,8BAAE,gBAAgB;AAAA,0BACpB;AAAA,wBACF;AAAA;AAAA,oBACF;AAAA,qBACF,GACF;AAAA,kBAGD,gBAAgB,IAAI,CAAC,WAAW;AA9QnD,wBAAAA,KAAAC;AA+QoB,0BAAM,QAAQ,eAAe,MAAM;AACnC,0BAAM,QAAQ,eAAe,MAAM;AACnC,0BAAM,YAAWA,OAAAD,MAAA,gBAAgB,SAAS,EAAE,MAA3B,gBAAAA,IAA8B,SAAS,WAAvC,OAAAC,MAAiD;AAClE,0BAAM,WAAW,eAAe,SAAS,IAAI,KAAK;AAClD,2BACE;AAAA,sBAAC;AAAA;AAAA,wBAEC,WAAU;AAAA,wBACV,UAAU,CAAC,UAAU;AACnB,gCAAM,eAAe;AACrB,yCAAe,SAAS,IAAI,KAAK;AAAA,wBACnC;AAAA,wBAEC;AAAA;AAAA,0BACA,WACC,WACE,oBAAC,UAAK,WAAU,+CACb,uBACH,IACE,eAAe,WACjB,oBAAC,UAAK,WAAU,uCAAsC,IAEtD,oBAAC,UAAK,WAAU,+CAA8C,qBAE9D,IAEA;AAAA;AAAA;AAAA,sBApBC;AAAA,oBAqBP;AAAA,kBAEJ,CAAC;AAAA,kBACA,gBAAgB,WAAW,KAAK,SAAS,QAAQ,SAAS,KACzD,oBAAC,SAAI,WAAU,iDAAgD,wBAE/D;AAAA,mBAEJ;AAAA;AAAA;AAAA,YA9EK,SAAS;AAAA,UA+EhB;AAAA,QAEJ,CAAC;AAAA,QAEA,kBAAkB,WAAW,IAC5B,oBAAC,SAAI,WAAU,iDAAgD,8BAE/D,IACE;AAAA,SACN;AAAA,MAEC,sBACC,oBAAC,SAAI,WAAU,8BACb;AAAA,QAAC,iBAAiB;AAAA,QAAjB;AAAA,UACC,MAAM;AAAA,UACN,cAAc;AAAA,UAEd;AAAA,gCAAC,iBAAiB,SAAjB,EAAyB,SAAO,MAC/B;AAAA,cAAC;AAAA;AAAA,gBACC,WAAU;AAAA,gBACV,UAAU,CAAC,UAAU;AACnB,wBAAM,eAAe;AACrB,0CAAwB,IAAI;AAAA,gBAC9B;AAAA,gBAEA;AAAA,sCAAC,QAAK,WAAU,0CAAyC;AAAA,kBACxD;AAAA,kBACA,iBAAiB,SAAS,IACzB,oBAAC,UAAK,WAAU,kEACb,2BAAiB,QACpB,IACE;AAAA;AAAA;AAAA,YACN,GACF;AAAA,YACA,oBAAC,iBAAiB,QAAjB,EACC;AAAA,cAAC,iBAAiB;AAAA,cAAjB;AAAA,gBACC,OAAM;AAAA,gBACN,MAAK;AAAA,gBACL,YAAY;AAAA,gBACZ,WAAU;AAAA,gBACV,iBAAiB,MAAM,wBAAwB,KAAK;AAAA,gBACpD,mBAAmB,CAAC,UAAU;AAC5B,wBAAM,SAAS,MAAM;AACrB,sBAAI,iCAAQ,QAAQ,wCAAwC;AAC1D,0BAAM,eAAe;AAAA,kBACvB;AAAA,gBACF;AAAA,gBAEA;AAAA,kBAAC;AAAA;AAAA,oBACC,QAAQ;AAAA,oBACR,YAAY;AAAA,oBACZ,oBAAoB,+DAA6B,MAAM;AAAA,oBAAC;AAAA;AAAA,gBAC1D;AAAA;AAAA,YACF,GACF;AAAA;AAAA;AAAA,MACF,GACF,IACE;AAAA,OACN;AAAA,KACF;AAIF,MAAI,YAAY,SAAS,GAAG;AAC1B,WACE,qBAAC,SAAI,WAAU,uCACZ;AAAA;AAAA,MACA,YAAY,IAAI,CAAC,SAChB;AAAA,QAAC;AAAA;AAAA,UAEC,MAAK;AAAA,UACL,SAAS,MAAM,iDAAiB,KAAK,YAAY,KAAK;AAAA,UACtD,WAAW;AAAA,YACT;AAAA,YACA,KAAK,SACD,gFACA;AAAA,UACN;AAAA,UAEA;AAAA,iCAAC,UAAK,WAAU,oCACb;AAAA;AAAA,cAAY;AAAA,cAAE;AAAA,eACjB;AAAA,YACC,KAAK;AAAA;AAAA;AAAA,QAbD,GAAG,KAAK,UAAU,IAAI,KAAK,KAAK;AAAA,MAcvC,CACD;AAAA,OACH;AAAA,EAEJ;AAEA,SAAO;AACT;","names":["_a","_b"]}
package/dist/index.d.ts CHANGED
@@ -17,7 +17,7 @@ export { ContactChannel, ContactItem, ContactList, ContactListProps } from './co
17
17
  export { ContextualQuickActionContextLabel, ContextualQuickActionContextLabelProps, ContextualQuickActionItem, ContextualQuickActionLauncher, ContextualQuickActionLauncherProps } from './components/contextual-quick-action-launcher.js';
18
18
  export { CheckInsCard, RecentlyCompletedCard, TopTasksCard, UpcomingMeetingsCard } from './components/dashboard-cards.js';
19
19
  export { DataRow, DataTable, DataTableProps } from './components/data-table.js';
20
- export { ConditionFieldDef, ConditionFilterValue, ConditionOperator, DEFAULT_OPERATORS, DataTableConditionFilter, DataTableConditionFilterProps, OPERATOR_LABELS, generateConditionId, getOperators } from './components/data-table-condition-filter.js';
20
+ export { ConditionFieldDef, ConditionFieldOption, ConditionFilterValue, ConditionOperator, ConditionOptionObject, DEFAULT_OPERATORS, DataTableConditionFilter, DataTableConditionFilterProps, OPERATOR_LABELS, generateConditionId, getOperators } from './components/data-table-condition-filter.js';
21
21
  export { DataTableDisplay, DataTableDisplayColumn } from './components/data-table-display.js';
22
22
  export { DataTableFilter, DataTableFilterCategory, DataTableFilterProps, FilterOption } from './components/data-table-filter.js';
23
23
  export { DataTableQuickViewValue, DataTableQuickViews } from './components/data-table-quick-views.js';
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
 
3
3
  "name": "@handled-ai/design-system",
4
- "version": "0.18.7",
4
+ "version": "0.18.9",
5
5
  "description": "Handled UI component library (shadcn-style, New York)",
6
6
  "type": "module",
7
7
  "packageManager": "pnpm@9.12.0",
@@ -103,6 +103,23 @@ const dateField: ConditionFieldDef = {
103
103
  type: "date",
104
104
  };
105
105
 
106
+ const selectField: ConditionFieldDef = {
107
+ id: "stage",
108
+ label: "Stage",
109
+ type: "select",
110
+ options: [
111
+ { label: "Qualified", value: "qualified" },
112
+ { label: "Disqualified", value: "disqualified" },
113
+ ],
114
+ };
115
+
116
+ const multiSelectField: ConditionFieldDef = {
117
+ id: "industry",
118
+ label: "Industry",
119
+ type: "multi_select",
120
+ options: ["Finance", { label: "Healthcare", value: "healthcare" }, "Education"],
121
+ };
122
+
106
123
  const allFields: ConditionFieldDef[] = [
107
124
  textField,
108
125
  numberField,
@@ -110,6 +127,12 @@ const allFields: ConditionFieldDef[] = [
110
127
  dateField,
111
128
  ];
112
129
 
130
+ const optionFields: ConditionFieldDef[] = [
131
+ textField,
132
+ selectField,
133
+ multiSelectField,
134
+ ];
135
+
113
136
  describe("DataTableConditionFilter", () => {
114
137
  let onConditionsChange: ReturnType<typeof vi.fn<(conditions: ConditionFilterValue[]) => void>>;
115
138
 
@@ -117,6 +140,27 @@ describe("DataTableConditionFilter", () => {
117
140
  onConditionsChange = vi.fn<(conditions: ConditionFilterValue[]) => void>();
118
141
  });
119
142
 
143
+ const renderOptionFilter = () => {
144
+ const result = render(
145
+ <DataTableConditionFilter
146
+ fields={optionFields}
147
+ conditions={[]}
148
+ onConditionsChange={onConditionsChange}
149
+ />,
150
+ );
151
+
152
+ fireEvent.click(screen.getByText("Add filter"));
153
+ return result;
154
+ };
155
+
156
+ const chooseField = (container: HTMLElement, label: string) => {
157
+ const fieldOption = Array.from(container.querySelectorAll('[data-slot="select-item"]')).find(
158
+ (opt) => opt.textContent?.includes(label),
159
+ );
160
+ expect(fieldOption).not.toBeUndefined();
161
+ fireEvent.click(fieldOption!);
162
+ };
163
+
120
164
  it("renders a polished empty panel with Add filter, disabled Add filter group, and quiet Clear filters", () => {
121
165
  const { container } = render(
122
166
  <DataTableConditionFilter
@@ -151,7 +195,6 @@ describe("DataTableConditionFilter", () => {
151
195
  expect(onConditionsChange).toHaveBeenCalledWith([]);
152
196
  });
153
197
 
154
-
155
198
  it("preserves an in-progress draft when fields are recreated with the same definition", () => {
156
199
  const { rerender } = render(
157
200
  <DataTableConditionFilter
@@ -197,6 +240,56 @@ describe("DataTableConditionFilter", () => {
197
240
  expect(committed?.[0]).toMatchObject({ field: "name", operator: "eq", value: "Acme" });
198
241
  });
199
242
 
243
+ it("renders select options and commits an eq condition", () => {
244
+ const { container } = renderOptionFilter();
245
+ chooseField(container, "Stage");
246
+
247
+ expect(getOperators(selectField)).toEqual(["eq", "neq", "is_null", "is_not_null"]);
248
+ fireEvent.click(screen.getByText("Qualified"));
249
+ fireEvent.click(screen.getByText("Apply"));
250
+
251
+ const committed = onConditionsChange.mock.calls.at(-1)?.[0];
252
+ expect(committed).toHaveLength(1);
253
+ expect(committed?.[0]).toMatchObject({
254
+ field: "stage",
255
+ operator: "eq",
256
+ value: "qualified",
257
+ });
258
+ });
259
+
260
+ it("renders multi-select checkboxes, toggles multiple options, and commits an in condition", () => {
261
+ const { container } = renderOptionFilter();
262
+ chooseField(container, "Industry");
263
+
264
+ expect(getOperators(multiSelectField)).toEqual(["in", "is_null", "is_not_null"]);
265
+
266
+ const financeCheckbox = screen.getByRole("checkbox", { name: "Finance" });
267
+ const healthcareCheckbox = screen.getByRole("checkbox", { name: "Healthcare" });
268
+ fireEvent.click(financeCheckbox);
269
+ fireEvent.click(healthcareCheckbox);
270
+
271
+ expect(financeCheckbox.getAttribute("aria-checked")).toBe("true");
272
+ expect(healthcareCheckbox.getAttribute("aria-checked")).toBe("true");
273
+
274
+ fireEvent.click(screen.getByText("Apply"));
275
+
276
+ const committed = onConditionsChange.mock.calls.at(-1)?.[0];
277
+ expect(committed).toHaveLength(1);
278
+ expect(committed?.[0]).toMatchObject({
279
+ field: "industry",
280
+ operator: "in",
281
+ value: ["Finance", "healthcare"],
282
+ });
283
+ });
284
+
285
+ it("omits incomplete multi-select conditions until at least one option is selected", () => {
286
+ const { container } = renderOptionFilter();
287
+ chooseField(container, "Industry");
288
+ fireEvent.click(screen.getByText("Apply"));
289
+
290
+ expect(onConditionsChange.mock.calls.at(-1)?.[0]).toEqual([]);
291
+ });
292
+
200
293
  it("Enter in the value field commits the current draft row", () => {
201
294
  render(
202
295
  <DataTableConditionFilter
@@ -380,9 +473,13 @@ describe("DataTableConditionFilter", () => {
380
473
  expect(screen.getAllByText("And")).toHaveLength(2);
381
474
  });
382
475
 
383
- it("operator options are scalar only and exclude in/contains", () => {
476
+ it("operator options respect the selected field definition", () => {
384
477
  expect(getOperators(textField)).toEqual(["eq", "neq", "is_null", "is_not_null"]);
385
- expect(getOperators({ ...textField, operators: ["eq", "in", "is_null"] })).toEqual(["eq", "is_null"]);
478
+ expect(getOperators({ ...textField, operators: ["eq", "in", "is_null"] })).toEqual([
479
+ "eq",
480
+ "in",
481
+ "is_null",
482
+ ]);
386
483
 
387
484
  const { container } = render(
388
485
  <DataTableConditionFilter
@@ -392,9 +489,10 @@ describe("DataTableConditionFilter", () => {
392
489
  />,
393
490
  );
394
491
 
395
- const optionTexts = Array.from(container.querySelectorAll('[data-slot="select-item"]'))
396
- .map((el) => el.textContent);
397
- expect(optionTexts).not.toContain("contains");
492
+ const optionTexts = Array.from(container.querySelectorAll('[data-slot="select-item"]')).map(
493
+ (el) => el.textContent,
494
+ );
495
+ expect(optionTexts).not.toContain("is any of");
398
496
  expect(optionTexts).toContain("is empty");
399
497
  });
400
498
 
@@ -413,7 +511,7 @@ describe("DataTableConditionFilter", () => {
413
511
  expect(optionTexts).toContain("≥");
414
512
  expect(optionTexts).toContain("<");
415
513
  expect(optionTexts).toContain("≤");
416
- expect(optionTexts).not.toContain("contains");
514
+ expect(optionTexts).not.toContain("is any of");
417
515
  });
418
516
 
419
517
  it("generateConditionId returns unique values", () => {
@@ -271,6 +271,47 @@ describe("DataTableFilter", () => {
271
271
  expect(document.querySelector('[data-slot="condition-filter"]')).toBeNull();
272
272
  });
273
273
 
274
+ it("shows submenu search for categories that opt in even below the global threshold", () => {
275
+ const category: DataTableFilterCategory = {
276
+ id: "owner",
277
+ label: "Owner",
278
+ icon: ListFilter,
279
+ options: ["Avery", "Jordan"],
280
+ searchable: true,
281
+ };
282
+
283
+ render(
284
+ <DataTableFilter
285
+ categories={[category]}
286
+ selectedFilters={{}}
287
+ onToggleFilter={() => {}}
288
+ />
289
+ );
290
+
291
+ expect(screen.getByPlaceholderText("Search...")).toBeDefined();
292
+ });
293
+
294
+ it("uses per-category search thresholds when provided", () => {
295
+ const category: DataTableFilterCategory = {
296
+ id: "stage",
297
+ label: "Stage",
298
+ icon: ListFilter,
299
+ options: ["Open", "Closed"],
300
+ searchable: { threshold: 1 },
301
+ };
302
+
303
+ render(
304
+ <DataTableFilter
305
+ categories={[category]}
306
+ selectedFilters={{}}
307
+ onToggleFilter={() => {}}
308
+ optionSearchThreshold={8}
309
+ />
310
+ );
311
+
312
+ expect(screen.getByPlaceholderText("Search...")).toBeDefined();
313
+ });
314
+
274
315
  it("exposes a condition builder popover entry point when condition fields are provided", () => {
275
316
  const conditionFields: ConditionFieldDef[] = [
276
317
  { id: "balance", label: "Account Balance", type: "currency" },
@@ -3,6 +3,7 @@
3
3
  import * as React from "react"
4
4
  import {
5
5
  CalendarDays,
6
+ Check,
6
7
  DollarSign,
7
8
  Eye,
8
9
  Hash,
@@ -10,6 +11,7 @@ import {
10
11
  Plus,
11
12
  Trash2,
12
13
  Type,
14
+ type LucideIcon,
13
15
  } from "lucide-react"
14
16
 
15
17
  import { cn } from "../lib/utils"
@@ -36,15 +38,24 @@ export type ConditionOperator =
36
38
  | "is_null"
37
39
  | "is_not_null"
38
40
 
41
+ export interface ConditionOptionObject {
42
+ label: string
43
+ value: string
44
+ }
45
+
46
+ export type ConditionFieldOption = string | ConditionOptionObject
47
+
39
48
  export interface ConditionFieldDef {
40
49
  /** Unique field key (e.g., "Account_Balance__c") */
41
50
  id: string
42
51
  /** Display label (e.g., "Account Balance") */
43
52
  label: string
44
53
  /** Field data type — determines which operators are available and how the value input renders */
45
- type: "text" | "number" | "currency" | "date"
54
+ type: "text" | "number" | "currency" | "date" | "select" | "multi_select"
46
55
  /** Allowed operators for this field. Defaults based on type if not provided. */
47
56
  operators?: ConditionOperator[]
57
+ /** Options used by select and multi-select fields. Strings use the same label and value. */
58
+ options?: ConditionFieldOption[]
48
59
  }
49
60
 
50
61
  export interface ConditionFilterValue {
@@ -52,7 +63,7 @@ export interface ConditionFilterValue {
52
63
  id: string
53
64
  field: string
54
65
  operator: ConditionOperator
55
- value: string | number | null
66
+ value: string | number | string[] | null
56
67
  }
57
68
 
58
69
  interface DataTableConditionFilterProps {
@@ -74,7 +85,7 @@ const OPERATOR_LABELS: Record<ConditionOperator, string> = {
74
85
  gte: "≥",
75
86
  lt: "<",
76
87
  lte: "≤",
77
- in: "contains",
88
+ in: "is any of",
78
89
  is_null: "is empty",
79
90
  is_not_null: "is not empty",
80
91
  }
@@ -95,6 +106,8 @@ const DEFAULT_OPERATORS: Record<ConditionFieldDef["type"], ConditionOperator[]>
95
106
  number: NUMERIC_OPERATORS,
96
107
  currency: NUMERIC_OPERATORS,
97
108
  date: ["gt", "gte", "lt", "lte", "eq", "neq", "is_null", "is_not_null"],
109
+ select: ["eq", "neq", "is_null", "is_not_null"],
110
+ multi_select: ["in", "is_null", "is_not_null"],
98
111
  }
99
112
 
100
113
  /** Generate a stable unique ID for a new condition row. */
@@ -108,7 +121,7 @@ function generateConditionId(): string {
108
121
  // ── Helpers ────────────────────────────────────────────────────
109
122
 
110
123
  function getOperators(field: ConditionFieldDef): ConditionOperator[] {
111
- return (field.operators ?? DEFAULT_OPERATORS[field.type]).filter((op) => op !== "in")
124
+ return field.operators ?? DEFAULT_OPERATORS[field.type]
112
125
  }
113
126
 
114
127
  function isUnaryOperator(op: ConditionOperator): boolean {
@@ -128,6 +141,18 @@ function createDraftCondition(field: ConditionFieldDef): ConditionFilterValue {
128
141
  }
129
142
  }
130
143
 
144
+ function normalizeConditionValue(
145
+ value: ConditionFilterValue["value"],
146
+ field: ConditionFieldDef,
147
+ operator: ConditionOperator,
148
+ ): ConditionFilterValue["value"] {
149
+ if (isUnaryOperator(operator)) return null
150
+ if (field.type === "multi_select") {
151
+ return Array.isArray(value) ? value : null
152
+ }
153
+ return Array.isArray(value) ? null : value
154
+ }
155
+
131
156
  function normalizeCondition(
132
157
  condition: ConditionFilterValue,
133
158
  fields: ConditionFieldDef[],
@@ -144,7 +169,7 @@ function normalizeCondition(
144
169
  ...condition,
145
170
  field: field.id,
146
171
  operator,
147
- value: isUnaryOperator(operator) ? null : condition.value,
172
+ value: normalizeConditionValue(condition.value, field, operator),
148
173
  }
149
174
  }
150
175
 
@@ -168,18 +193,36 @@ function isCompleteCondition(
168
193
  if (!field) return false
169
194
  if (!getOperators(field).includes(condition.operator)) return false
170
195
  if (isUnaryOperator(condition.operator)) return true
171
- return condition.value !== null && condition.value !== ""
196
+ if (field.type === "multi_select") {
197
+ return Array.isArray(condition.value) && condition.value.length > 0
198
+ }
199
+ return condition.value !== null && condition.value !== "" && !Array.isArray(condition.value)
200
+ }
201
+
202
+ function getConditionValueSignature(value: ConditionFilterValue["value"]): string {
203
+ return Array.isArray(value) ? JSON.stringify(value) : String(value)
172
204
  }
173
205
 
174
206
  function getConditionsSignature(conditions: ConditionFilterValue[]): string {
175
207
  return conditions
176
- .map((condition) => `${condition.id}:${condition.field}:${condition.operator}:${String(condition.value)}`)
208
+ .map((condition) => `${condition.id}:${condition.field}:${condition.operator}:${getConditionValueSignature(condition.value)}`)
177
209
  .join(";")
178
210
  }
179
211
 
212
+ function normalizeFieldOptions(field: ConditionFieldDef): ConditionOptionObject[] {
213
+ return (field.options ?? []).map((option) =>
214
+ typeof option === "string" ? { label: option, value: option } : option,
215
+ )
216
+ }
217
+
180
218
  function getFieldsSignature(fields: ConditionFieldDef[]): string {
181
219
  return fields
182
- .map((field) => `${field.id}:${field.type}:${field.label}:${(field.operators ?? []).join("|")}`)
220
+ .map((field) => {
221
+ const optionsSignature = normalizeFieldOptions(field)
222
+ .map((option) => `${option.label}:${option.value}`)
223
+ .join("|")
224
+ return `${field.id}:${field.type}:${field.label}:${(field.operators ?? []).join("|")}:${optionsSignature}`
225
+ })
183
226
  .join(";")
184
227
  }
185
228
 
@@ -192,15 +235,164 @@ function getCommittedConditions(
192
235
  .filter((condition) => isCompleteCondition(condition, fields))
193
236
  }
194
237
 
195
- function getFieldIcon(type: ConditionFieldDef["type"]) {
196
- if (type === "currency") return DollarSign
197
- if (type === "number") return Hash
198
- if (type === "date") return CalendarDays
199
- return Type
238
+ const FIELD_ICON_BY_TYPE: Record<ConditionFieldDef["type"], LucideIcon> = {
239
+ text: Type,
240
+ number: Hash,
241
+ currency: DollarSign,
242
+ date: CalendarDays,
243
+ select: MoreHorizontal,
244
+ multi_select: MoreHorizontal,
245
+ }
246
+
247
+ function getFieldIcon(type: ConditionFieldDef["type"]): LucideIcon {
248
+ return FIELD_ICON_BY_TYPE[type]
200
249
  }
201
250
 
202
251
  // ── Condition Row ──────────────────────────────────────────────
203
252
 
253
+ function getInputType(fieldType: ConditionFieldDef["type"]): "text" | "number" | "date" {
254
+ if (fieldType === "number" || fieldType === "currency") return "number"
255
+ if (fieldType === "date") return "date"
256
+ return "text"
257
+ }
258
+
259
+ function getInputPlaceholder(fieldType: ConditionFieldDef["type"]): string {
260
+ if (fieldType === "currency") return "Amount"
261
+ if (fieldType === "number") return "Enter number..."
262
+ if (fieldType === "date") return ""
263
+ return "Enter value..."
264
+ }
265
+
266
+ interface ConditionValueInputProps {
267
+ condition: ConditionFilterValue
268
+ fieldDef: ConditionFieldDef
269
+ onValueChange: (event: React.ChangeEvent<HTMLInputElement>) => void
270
+ onSelectValueChange: (value: string) => void
271
+ onMultiSelectValueToggle: (value: string) => void
272
+ onCommit: () => void
273
+ }
274
+
275
+ function SelectConditionValueInput({
276
+ condition,
277
+ fieldDef,
278
+ onSelectValueChange,
279
+ }: Pick<ConditionValueInputProps, "condition" | "fieldDef" | "onSelectValueChange">) {
280
+ const options = normalizeFieldOptions(fieldDef)
281
+
282
+ return (
283
+ <Select
284
+ value={typeof condition.value === "string" ? condition.value : ""}
285
+ onValueChange={onSelectValueChange}
286
+ >
287
+ <SelectTrigger className="h-8 w-full" size="sm">
288
+ <SelectValue placeholder="Select value..." />
289
+ </SelectTrigger>
290
+ <SelectContent>
291
+ {options.map((option) => (
292
+ <SelectItem key={option.value} value={option.value}>
293
+ {option.label}
294
+ </SelectItem>
295
+ ))}
296
+ </SelectContent>
297
+ </Select>
298
+ )
299
+ }
300
+
301
+ function MultiSelectConditionValueInput({
302
+ condition,
303
+ fieldDef,
304
+ onMultiSelectValueToggle,
305
+ }: Pick<ConditionValueInputProps, "condition" | "fieldDef" | "onMultiSelectValueToggle">) {
306
+ const selectedValues = Array.isArray(condition.value) ? condition.value : []
307
+ const options = normalizeFieldOptions(fieldDef)
308
+
309
+ return (
310
+ <div className="max-h-28 overflow-y-auto rounded-md border border-border bg-background p-1">
311
+ {options.length > 0 ? (
312
+ options.map((option) => {
313
+ const checked = selectedValues.includes(option.value)
314
+ return (
315
+ <button
316
+ key={option.value}
317
+ type="button"
318
+ role="checkbox"
319
+ aria-checked={checked}
320
+ className={cn(
321
+ "flex w-full items-center gap-2 rounded-sm px-2 py-1 text-left text-xs hover:bg-muted",
322
+ checked && "text-brand-purple",
323
+ )}
324
+ onClick={(event) => {
325
+ event.stopPropagation()
326
+ onMultiSelectValueToggle(option.value)
327
+ }}
328
+ >
329
+ <span
330
+ className={cn(
331
+ "flex h-3.5 w-3.5 items-center justify-center rounded-sm border border-border",
332
+ checked && "border-brand-purple bg-brand-purple text-white",
333
+ )}
334
+ aria-hidden="true"
335
+ >
336
+ {checked ? <Check className="h-3 w-3" /> : null}
337
+ </span>
338
+ <span className="min-w-0 flex-1 truncate">{option.label}</span>
339
+ </button>
340
+ )
341
+ })
342
+ ) : (
343
+ <div className="px-2 py-1 text-xs text-muted-foreground">No options</div>
344
+ )}
345
+ </div>
346
+ )
347
+ }
348
+
349
+ function ScalarConditionValueInput({
350
+ condition,
351
+ fieldDef,
352
+ onValueChange,
353
+ onCommit,
354
+ }: Pick<ConditionValueInputProps, "condition" | "fieldDef" | "onValueChange" | "onCommit">) {
355
+ return (
356
+ <div className="relative flex items-center">
357
+ {fieldDef.type === "currency" ? (
358
+ <span className="pointer-events-none absolute left-2 text-sm text-muted-foreground">
359
+ $
360
+ </span>
361
+ ) : null}
362
+ <Input
363
+ type={getInputType(fieldDef.type)}
364
+ value={
365
+ condition.value != null && !Array.isArray(condition.value)
366
+ ? String(condition.value)
367
+ : ""
368
+ }
369
+ onChange={onValueChange}
370
+ onClick={(event) => event.stopPropagation()}
371
+ onKeyDown={(event) => {
372
+ event.stopPropagation()
373
+ if (event.key === "Enter") {
374
+ onCommit()
375
+ }
376
+ }}
377
+ placeholder={getInputPlaceholder(fieldDef.type)}
378
+ className={cn("h-8", fieldDef.type === "currency" && "pl-6")}
379
+ />
380
+ </div>
381
+ )
382
+ }
383
+
384
+ function ConditionValueInput(props: ConditionValueInputProps) {
385
+ if (props.fieldDef.type === "select") {
386
+ return <SelectConditionValueInput {...props} />
387
+ }
388
+
389
+ if (props.fieldDef.type === "multi_select") {
390
+ return <MultiSelectConditionValueInput {...props} />
391
+ }
392
+
393
+ return <ScalarConditionValueInput {...props} />
394
+ }
395
+
204
396
  interface ConditionRowProps {
205
397
  condition: ConditionFilterValue
206
398
  fields: ConditionFieldDef[]
@@ -238,7 +430,7 @@ function ConditionRow({
238
430
  onChange({
239
431
  ...condition,
240
432
  operator: newOp,
241
- value: isUnaryOperator(newOp) ? null : condition.value,
433
+ value: normalizeConditionValue(condition.value, fieldDef, newOp),
242
434
  })
243
435
  }
244
436
 
@@ -249,6 +441,26 @@ function ConditionRow({
249
441
  })
250
442
  }
251
443
 
444
+ const handleSelectValueChange = (value: string) => {
445
+ onChange({
446
+ ...condition,
447
+ value,
448
+ })
449
+ }
450
+
451
+ const handleMultiSelectValueToggle = (value: string) => {
452
+ const currentValues = Array.isArray(condition.value) ? condition.value : []
453
+ const nextValues = currentValues.includes(value)
454
+ ? currentValues.filter((currentValue) => currentValue !== value)
455
+ : [...currentValues, value]
456
+
457
+ onChange({
458
+ ...condition,
459
+ value: nextValues,
460
+ })
461
+ }
462
+
463
+
252
464
  return (
253
465
  <div
254
466
  className="grid grid-cols-[52px_minmax(150px,1fr)_120px_minmax(140px,1fr)_auto] items-center gap-2 rounded-lg border border-border/70 bg-background p-2 shadow-sm"
@@ -299,41 +511,14 @@ function ConditionRow({
299
511
  No value needed
300
512
  </div>
301
513
  ) : (
302
- <div className="relative flex items-center">
303
- {fieldDef.type === "currency" ? (
304
- <span className="pointer-events-none absolute left-2 text-sm text-muted-foreground">
305
- $
306
- </span>
307
- ) : null}
308
- <Input
309
- type={
310
- fieldDef.type === "number" || fieldDef.type === "currency"
311
- ? "number"
312
- : fieldDef.type === "date"
313
- ? "date"
314
- : "text"
315
- }
316
- value={condition.value != null ? String(condition.value) : ""}
317
- onChange={handleValueChange}
318
- onClick={(event) => event.stopPropagation()}
319
- onKeyDown={(event) => {
320
- event.stopPropagation()
321
- if (event.key === "Enter") {
322
- onCommit()
323
- }
324
- }}
325
- placeholder={
326
- fieldDef.type === "currency"
327
- ? "Amount"
328
- : fieldDef.type === "number"
329
- ? "Enter number..."
330
- : fieldDef.type === "date"
331
- ? ""
332
- : "Enter value..."
333
- }
334
- className={cn("h-8", fieldDef.type === "currency" && "pl-6")}
335
- />
336
- </div>
514
+ <ConditionValueInput
515
+ condition={condition}
516
+ fieldDef={fieldDef}
517
+ onValueChange={handleValueChange}
518
+ onSelectValueChange={handleSelectValueChange}
519
+ onMultiSelectValueToggle={handleMultiSelectValueToggle}
520
+ onCommit={onCommit}
521
+ />
337
522
  )}
338
523
 
339
524
  <div className="flex items-center gap-1">
@@ -34,6 +34,12 @@ export interface DataTableFilterCategory {
34
34
  options: (string | FilterOption)[]
35
35
  /** Filter behavior. Defaults to "multi" (checkbox multi-select). */
36
36
  type?: "multi" | "single" | "boolean"
37
+ /**
38
+ * Submenu search behavior. Defaults to the DataTableFilter
39
+ * optionSearchThreshold prop. Use true to always show search or false to
40
+ * hide it for a specific category.
41
+ */
42
+ searchable?: boolean | { threshold?: number }
37
43
  }
38
44
 
39
45
  function getOptionValue(option: string | FilterOption): string {
@@ -208,6 +214,15 @@ export function DataTableFilter({
208
214
  getOptionLabel(opt).toLowerCase().includes(subQuery)
209
215
  )
210
216
  : category.options
217
+ const shouldShowSubmenuSearch = (() => {
218
+ if (category.searchable === true) return true
219
+ if (category.searchable === false) return false
220
+ const threshold =
221
+ typeof category.searchable === "object"
222
+ ? (category.searchable.threshold ?? optionSearchThreshold)
223
+ : optionSearchThreshold
224
+ return category.options.length > threshold
225
+ })()
211
226
 
212
227
  return (
213
228
  <DropdownMenuSub
@@ -227,8 +242,8 @@ export function DataTableFilter({
227
242
  {category.label}
228
243
  </DropdownMenuSubTrigger>
229
244
  <DropdownMenuSubContent className="max-h-[320px] w-52 overflow-y-auto p-1">
230
- {/* Submenu search — only for categories with many options */}
231
- {category.options.length > optionSearchThreshold && (
245
+ {/* Submenu search — shown for long lists or categories that opt in. */}
246
+ {shouldShowSubmenuSearch && (
232
247
  <div className="sticky top-0 z-10 border-b border-border bg-popover p-1.5">
233
248
  <div className="relative">
234
249
  <Search className="absolute left-2 top-1/2 h-3 w-3 -translate-y-1/2 text-muted-foreground" />