@handled-ai/design-system 0.18.9 → 0.18.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/data-table-condition-filter.d.ts +4 -0
- package/dist/components/data-table-condition-filter.js +30 -3
- package/dist/components/data-table-condition-filter.js.map +1 -1
- package/package.json +1 -1
- package/src/components/__tests__/data-table-condition-filter.test.tsx +38 -0
- package/src/components/data-table-condition-filter.tsx +46 -6
|
@@ -17,6 +17,10 @@ interface ConditionFieldDef {
|
|
|
17
17
|
operators?: ConditionOperator[];
|
|
18
18
|
/** Options used by select and multi-select fields. Strings use the same label and value. */
|
|
19
19
|
options?: ConditionFieldOption[];
|
|
20
|
+
/** Show a search box for option-backed value inputs. */
|
|
21
|
+
searchable?: boolean | {
|
|
22
|
+
threshold?: number;
|
|
23
|
+
};
|
|
20
24
|
}
|
|
21
25
|
interface ConditionFilterValue {
|
|
22
26
|
/** Stable identity — used as React key to avoid stale-state bugs on removal */
|
|
@@ -148,9 +148,10 @@ function normalizeFieldOptions(field) {
|
|
|
148
148
|
}
|
|
149
149
|
function getFieldsSignature(fields) {
|
|
150
150
|
return fields.map((field) => {
|
|
151
|
-
var _a;
|
|
151
|
+
var _a, _b, _c;
|
|
152
152
|
const optionsSignature = normalizeFieldOptions(field).map((option) => `${option.label}:${option.value}`).join("|");
|
|
153
|
-
|
|
153
|
+
const searchSignature = typeof field.searchable === "object" ? `threshold:${(_a = field.searchable.threshold) != null ? _a : ""}` : String((_b = field.searchable) != null ? _b : "");
|
|
154
|
+
return `${field.id}:${field.type}:${field.label}:${((_c = field.operators) != null ? _c : []).join("|")}:${optionsSignature}:${searchSignature}`;
|
|
154
155
|
}).join(";");
|
|
155
156
|
}
|
|
156
157
|
function getCommittedConditions(drafts, fields) {
|
|
@@ -178,12 +179,25 @@ function getInputPlaceholder(fieldType) {
|
|
|
178
179
|
if (fieldType === "date") return "";
|
|
179
180
|
return "Enter value...";
|
|
180
181
|
}
|
|
182
|
+
function shouldShowOptionSearch(fieldDef, optionCount) {
|
|
183
|
+
var _a;
|
|
184
|
+
if (fieldDef.searchable === true) return true;
|
|
185
|
+
if (fieldDef.searchable === false) return false;
|
|
186
|
+
if (typeof fieldDef.searchable === "object") {
|
|
187
|
+
return optionCount >= ((_a = fieldDef.searchable.threshold) != null ? _a : 8);
|
|
188
|
+
}
|
|
189
|
+
return false;
|
|
190
|
+
}
|
|
181
191
|
function SelectConditionValueInput({
|
|
182
192
|
condition,
|
|
183
193
|
fieldDef,
|
|
184
194
|
onSelectValueChange
|
|
185
195
|
}) {
|
|
196
|
+
const [query, setQuery] = React.useState("");
|
|
186
197
|
const options = normalizeFieldOptions(fieldDef);
|
|
198
|
+
const normalizedQuery = query.trim().toLowerCase();
|
|
199
|
+
const filteredOptions = normalizedQuery ? options.filter((option) => option.label.toLowerCase().includes(normalizedQuery)) : options;
|
|
200
|
+
const showSearch = shouldShowOptionSearch(fieldDef, options.length);
|
|
187
201
|
return /* @__PURE__ */ jsxs(
|
|
188
202
|
Select,
|
|
189
203
|
{
|
|
@@ -191,7 +205,20 @@ function SelectConditionValueInput({
|
|
|
191
205
|
onValueChange: onSelectValueChange,
|
|
192
206
|
children: [
|
|
193
207
|
/* @__PURE__ */ jsx(SelectTrigger, { className: "h-8 w-full", size: "sm", children: /* @__PURE__ */ jsx(SelectValue, { placeholder: "Select value..." }) }),
|
|
194
|
-
/* @__PURE__ */
|
|
208
|
+
/* @__PURE__ */ jsxs(SelectContent, { children: [
|
|
209
|
+
showSearch ? /* @__PURE__ */ jsx("div", { className: "sticky top-0 z-10 border-b border-border bg-popover p-1.5", children: /* @__PURE__ */ jsx(
|
|
210
|
+
Input,
|
|
211
|
+
{
|
|
212
|
+
value: query,
|
|
213
|
+
onChange: (event) => setQuery(event.target.value),
|
|
214
|
+
onClick: (event) => event.stopPropagation(),
|
|
215
|
+
onKeyDown: (event) => event.stopPropagation(),
|
|
216
|
+
placeholder: "Search options...",
|
|
217
|
+
className: "h-7 text-xs"
|
|
218
|
+
}
|
|
219
|
+
) }) : null,
|
|
220
|
+
filteredOptions.length > 0 ? filteredOptions.map((option) => /* @__PURE__ */ jsx(SelectItem, { value: option.value, children: option.label }, option.value)) : /* @__PURE__ */ jsx("div", { className: "px-2 py-1.5 text-xs text-muted-foreground", children: "No options" })
|
|
221
|
+
] })
|
|
195
222
|
]
|
|
196
223
|
}
|
|
197
224
|
);
|
|
@@ -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 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"]}
|
|
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 /** Show a search box for option-backed value inputs. */\n searchable?: boolean | { threshold?: number }\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 const searchSignature =\n typeof field.searchable === \"object\"\n ? `threshold:${field.searchable.threshold ?? \"\"}`\n : String(field.searchable ?? \"\")\n return `${field.id}:${field.type}:${field.label}:${(field.operators ?? []).join(\"|\")}:${optionsSignature}:${searchSignature}`\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 shouldShowOptionSearch(\n fieldDef: ConditionFieldDef,\n optionCount: number,\n): boolean {\n if (fieldDef.searchable === true) return true\n if (fieldDef.searchable === false) return false\n if (typeof fieldDef.searchable === \"object\") {\n return optionCount >= (fieldDef.searchable.threshold ?? 8)\n }\n return false\n}\n\nfunction SelectConditionValueInput({\n condition,\n fieldDef,\n onSelectValueChange,\n}: Pick<ConditionValueInputProps, \"condition\" | \"fieldDef\" | \"onSelectValueChange\">) {\n const [query, setQuery] = React.useState(\"\")\n const options = normalizeFieldOptions(fieldDef)\n const normalizedQuery = query.trim().toLowerCase()\n const filteredOptions = normalizedQuery\n ? options.filter((option) => option.label.toLowerCase().includes(normalizedQuery))\n : options\n const showSearch = shouldShowOptionSearch(fieldDef, options.length)\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 {showSearch ? (\n <div className=\"sticky top-0 z-10 border-b border-border bg-popover p-1.5\">\n <Input\n value={query}\n onChange={(event) => setQuery(event.target.value)}\n onClick={(event) => event.stopPropagation()}\n onKeyDown={(event) => event.stopPropagation()}\n placeholder=\"Search options...\"\n className=\"h-7 text-xs\"\n />\n </div>\n ) : null}\n {filteredOptions.length > 0 ? (\n filteredOptions.map((option) => (\n <SelectItem key={option.value} value={option.value}>\n {option.label}\n </SelectItem>\n ))\n ) : (\n <div className=\"px-2 py-1.5 text-xs text-muted-foreground\">No options</div>\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"]}
|
package/package.json
CHANGED
|
@@ -257,6 +257,44 @@ describe("DataTableConditionFilter", () => {
|
|
|
257
257
|
});
|
|
258
258
|
});
|
|
259
259
|
|
|
260
|
+
it("filters searchable select options before committing a selected value", () => {
|
|
261
|
+
const searchableOwnerField: ConditionFieldDef = {
|
|
262
|
+
id: "owner",
|
|
263
|
+
label: "Owner",
|
|
264
|
+
type: "select",
|
|
265
|
+
searchable: true,
|
|
266
|
+
options: [
|
|
267
|
+
{ label: "Alice Adams", value: "alice@example.com" },
|
|
268
|
+
{ label: "Bob Brown", value: "bob@example.com" },
|
|
269
|
+
],
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
render(
|
|
273
|
+
<DataTableConditionFilter
|
|
274
|
+
fields={[searchableOwnerField]}
|
|
275
|
+
conditions={[]}
|
|
276
|
+
onConditionsChange={onConditionsChange}
|
|
277
|
+
/>,
|
|
278
|
+
);
|
|
279
|
+
|
|
280
|
+
fireEvent.click(screen.getByText("Add filter"));
|
|
281
|
+
fireEvent.change(screen.getByPlaceholderText("Search options..."), {
|
|
282
|
+
target: { value: "bob" },
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
expect(screen.queryByText("Alice Adams")).toBeNull();
|
|
286
|
+
fireEvent.click(screen.getByText("Bob Brown"));
|
|
287
|
+
fireEvent.click(screen.getByText("Apply"));
|
|
288
|
+
|
|
289
|
+
const committed = onConditionsChange.mock.calls.at(-1)?.[0];
|
|
290
|
+
expect(committed).toHaveLength(1);
|
|
291
|
+
expect(committed?.[0]).toMatchObject({
|
|
292
|
+
field: "owner",
|
|
293
|
+
operator: "eq",
|
|
294
|
+
value: "bob@example.com",
|
|
295
|
+
});
|
|
296
|
+
});
|
|
297
|
+
|
|
260
298
|
it("renders multi-select checkboxes, toggles multiple options, and commits an in condition", () => {
|
|
261
299
|
const { container } = renderOptionFilter();
|
|
262
300
|
chooseField(container, "Industry");
|
|
@@ -56,6 +56,8 @@ export interface ConditionFieldDef {
|
|
|
56
56
|
operators?: ConditionOperator[]
|
|
57
57
|
/** Options used by select and multi-select fields. Strings use the same label and value. */
|
|
58
58
|
options?: ConditionFieldOption[]
|
|
59
|
+
/** Show a search box for option-backed value inputs. */
|
|
60
|
+
searchable?: boolean | { threshold?: number }
|
|
59
61
|
}
|
|
60
62
|
|
|
61
63
|
export interface ConditionFilterValue {
|
|
@@ -221,7 +223,11 @@ function getFieldsSignature(fields: ConditionFieldDef[]): string {
|
|
|
221
223
|
const optionsSignature = normalizeFieldOptions(field)
|
|
222
224
|
.map((option) => `${option.label}:${option.value}`)
|
|
223
225
|
.join("|")
|
|
224
|
-
|
|
226
|
+
const searchSignature =
|
|
227
|
+
typeof field.searchable === "object"
|
|
228
|
+
? `threshold:${field.searchable.threshold ?? ""}`
|
|
229
|
+
: String(field.searchable ?? "")
|
|
230
|
+
return `${field.id}:${field.type}:${field.label}:${(field.operators ?? []).join("|")}:${optionsSignature}:${searchSignature}`
|
|
225
231
|
})
|
|
226
232
|
.join(";")
|
|
227
233
|
}
|
|
@@ -272,12 +278,30 @@ interface ConditionValueInputProps {
|
|
|
272
278
|
onCommit: () => void
|
|
273
279
|
}
|
|
274
280
|
|
|
281
|
+
function shouldShowOptionSearch(
|
|
282
|
+
fieldDef: ConditionFieldDef,
|
|
283
|
+
optionCount: number,
|
|
284
|
+
): boolean {
|
|
285
|
+
if (fieldDef.searchable === true) return true
|
|
286
|
+
if (fieldDef.searchable === false) return false
|
|
287
|
+
if (typeof fieldDef.searchable === "object") {
|
|
288
|
+
return optionCount >= (fieldDef.searchable.threshold ?? 8)
|
|
289
|
+
}
|
|
290
|
+
return false
|
|
291
|
+
}
|
|
292
|
+
|
|
275
293
|
function SelectConditionValueInput({
|
|
276
294
|
condition,
|
|
277
295
|
fieldDef,
|
|
278
296
|
onSelectValueChange,
|
|
279
297
|
}: Pick<ConditionValueInputProps, "condition" | "fieldDef" | "onSelectValueChange">) {
|
|
298
|
+
const [query, setQuery] = React.useState("")
|
|
280
299
|
const options = normalizeFieldOptions(fieldDef)
|
|
300
|
+
const normalizedQuery = query.trim().toLowerCase()
|
|
301
|
+
const filteredOptions = normalizedQuery
|
|
302
|
+
? options.filter((option) => option.label.toLowerCase().includes(normalizedQuery))
|
|
303
|
+
: options
|
|
304
|
+
const showSearch = shouldShowOptionSearch(fieldDef, options.length)
|
|
281
305
|
|
|
282
306
|
return (
|
|
283
307
|
<Select
|
|
@@ -288,11 +312,27 @@ function SelectConditionValueInput({
|
|
|
288
312
|
<SelectValue placeholder="Select value..." />
|
|
289
313
|
</SelectTrigger>
|
|
290
314
|
<SelectContent>
|
|
291
|
-
{
|
|
292
|
-
<
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
315
|
+
{showSearch ? (
|
|
316
|
+
<div className="sticky top-0 z-10 border-b border-border bg-popover p-1.5">
|
|
317
|
+
<Input
|
|
318
|
+
value={query}
|
|
319
|
+
onChange={(event) => setQuery(event.target.value)}
|
|
320
|
+
onClick={(event) => event.stopPropagation()}
|
|
321
|
+
onKeyDown={(event) => event.stopPropagation()}
|
|
322
|
+
placeholder="Search options..."
|
|
323
|
+
className="h-7 text-xs"
|
|
324
|
+
/>
|
|
325
|
+
</div>
|
|
326
|
+
) : null}
|
|
327
|
+
{filteredOptions.length > 0 ? (
|
|
328
|
+
filteredOptions.map((option) => (
|
|
329
|
+
<SelectItem key={option.value} value={option.value}>
|
|
330
|
+
{option.label}
|
|
331
|
+
</SelectItem>
|
|
332
|
+
))
|
|
333
|
+
) : (
|
|
334
|
+
<div className="px-2 py-1.5 text-xs text-muted-foreground">No options</div>
|
|
335
|
+
)}
|
|
296
336
|
</SelectContent>
|
|
297
337
|
</Select>
|
|
298
338
|
)
|