@handled-ai/design-system 0.18.8 → 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.
- package/dist/components/badge.d.ts +1 -1
- package/dist/components/button.d.ts +1 -1
- package/dist/components/data-table-condition-filter.d.ts +10 -3
- package/dist/components/data-table-condition-filter.js +170 -32
- package/dist/components/data-table-condition-filter.js.map +1 -1
- package/dist/components/pill.d.ts +1 -1
- package/dist/components/tabs.d.ts +1 -1
- package/dist/index.d.ts +1 -1
- package/package.json +1 -1
- package/src/components/__tests__/data-table-condition-filter.test.tsx +105 -7
- package/src/components/data-table-condition-filter.tsx +234 -49
|
@@ -3,7 +3,7 @@ import * as React from 'react';
|
|
|
3
3
|
import { VariantProps } from 'class-variance-authority';
|
|
4
4
|
|
|
5
5
|
declare const badgeVariants: (props?: ({
|
|
6
|
-
variant?: "
|
|
6
|
+
variant?: "default" | "secondary" | "destructive" | "outline" | "ghost" | "link" | null | undefined;
|
|
7
7
|
} & class_variance_authority_types.ClassProp) | undefined) => string;
|
|
8
8
|
declare function Badge({ className, variant, asChild, ...props }: React.ComponentProps<"span"> & VariantProps<typeof badgeVariants> & {
|
|
9
9
|
asChild?: boolean;
|
|
@@ -3,7 +3,7 @@ import * as React from 'react';
|
|
|
3
3
|
import { VariantProps } from 'class-variance-authority';
|
|
4
4
|
|
|
5
5
|
declare const buttonVariants: (props?: ({
|
|
6
|
-
variant?: "
|
|
6
|
+
variant?: "default" | "secondary" | "destructive" | "outline" | "ghost" | "link" | null | undefined;
|
|
7
7
|
size?: "default" | "sm" | "lg" | "icon" | null | undefined;
|
|
8
8
|
} & class_variance_authority_types.ClassProp) | undefined) => string;
|
|
9
9
|
declare function Button({ className, variant, size, asChild, ...props }: React.ComponentProps<"button"> & VariantProps<typeof buttonVariants> & {
|
|
@@ -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: "
|
|
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 (
|
|
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:
|
|
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
|
-
|
|
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}:${
|
|
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
|
-
|
|
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
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
return
|
|
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:
|
|
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__ */
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
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"]}
|
|
@@ -12,7 +12,7 @@ import { VariantProps } from 'class-variance-authority';
|
|
|
12
12
|
*/
|
|
13
13
|
type PillStatus = "success" | "warning" | "error" | "neutral" | "info";
|
|
14
14
|
declare const pillVariants: (props?: ({
|
|
15
|
-
variant?: "
|
|
15
|
+
variant?: "default" | "secondary" | "destructive" | "outline" | "ghost" | "error" | "neutral" | "info" | "warning" | "success" | null | undefined;
|
|
16
16
|
} & class_variance_authority_types.ClassProp) | undefined) => string;
|
|
17
17
|
interface PillProps extends React.ComponentProps<"span">, VariantProps<typeof pillVariants> {
|
|
18
18
|
}
|
|
@@ -5,7 +5,7 @@ import { Tabs as Tabs$1 } from 'radix-ui';
|
|
|
5
5
|
|
|
6
6
|
declare function Tabs({ className, orientation, ...props }: React.ComponentProps<typeof Tabs$1.Root>): React.JSX.Element;
|
|
7
7
|
declare const tabsListVariants: (props?: ({
|
|
8
|
-
variant?: "
|
|
8
|
+
variant?: "default" | "line" | null | undefined;
|
|
9
9
|
} & class_variance_authority_types.ClassProp) | undefined) => string;
|
|
10
10
|
declare function TabsList({ className, variant, ...props }: React.ComponentProps<typeof Tabs$1.List> & VariantProps<typeof tabsListVariants>): React.JSX.Element;
|
|
11
11
|
declare function TabsTrigger({ className, ...props }: React.ComponentProps<typeof Tabs$1.Trigger>): React.JSX.Element;
|
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
|
@@ -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
|
|
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([
|
|
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
|
-
|
|
397
|
-
|
|
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("
|
|
514
|
+
expect(optionTexts).not.toContain("is any of");
|
|
417
515
|
});
|
|
418
516
|
|
|
419
517
|
it("generateConditionId returns unique values", () => {
|
|
@@ -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: "
|
|
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
|
|
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:
|
|
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
|
-
|
|
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}:${
|
|
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) =>
|
|
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
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
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:
|
|
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
|
-
<
|
|
303
|
-
{
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
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">
|