import * as React from "react"; import { ChevronDownIcon, PlusIcon, Trash2Icon } from "lucide-react"; import { Accordion as AccordionPrimitive } from "@base-ui/react/accordion"; import { Utils as QbUtils, type Actions, type ImmutableTree, type JsonItem, } from "@react-awesome-query-builder/ui"; import { Button } from "@/components/ui/button"; import { InputGroup, InputGroupAddon, InputGroupInput, InputGroupText, } from "@/components/ui/input-group"; import { Checkbox } from "@/components/ui/checkbox"; import { Label } from "@/components/ui/label"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { cn } from "@/lib/utils"; import type { AlertQueryField, AlertQueryOperator } from "./types"; import { ALL_NUMERIC_OPERATORS, BOOLEAN_OPERATORS, OPERATOR_LABELS, } from "./config"; import { allOperatorLabels, formatWithCommas, longestOf, parseCommas, ruleSummary, } from "./utils"; // --------------------------------------------------------------------------- // SelectAutoWidth // --------------------------------------------------------------------------- function SelectAutoWidth({ longestLabel, children, }: { longestLabel: string; children: React.ReactNode; }) { return (
{longestLabel}
{children}
); } // --------------------------------------------------------------------------- // ValueInput // --------------------------------------------------------------------------- function ValueInput({ value, onChange, unit, min, max, placeholder = "0", }: { value: string; onChange: (v: string) => void; unit?: "dollar" | "percent"; min?: number; max?: number; placeholder?: string; }) { function handleChange(raw: string) { const parsed = parseCommas(raw); if (parsed === "") { onChange(""); return; } const n = parseFloat(parsed); if (Number.isNaN(n)) return; if (min !== undefined && n < min) return; if (max !== undefined && n > max) return; onChange(parsed); } return ( {unit === "dollar" && ( $ )} handleChange(e.target.value)} placeholder={placeholder} /> {unit === "percent" && ( % )} ); } // --------------------------------------------------------------------------- // RuleEditorFields // --------------------------------------------------------------------------- function RuleEditorFields({ ruleProps, path, actions, fields, longestFieldLabel, longestOperatorLabel, }: { // eslint-disable-next-line @typescript-eslint/no-explicit-any ruleProps: any; path: string[]; actions: Actions; fields: AlertQueryField[]; longestFieldLabel: string; longestOperatorLabel: string; }) { const field: string = ruleProps?.field ?? fields[0]?.key ?? ""; const operator: string = ruleProps?.operator ?? "greater_or_equal"; const value0 = ruleProps?.value?.[0]; const fieldDef = fields.find((f) => f.key === field) ?? fields[0]; const isBooleanField = fieldDef?.type === "boolean"; const availableOperators: AlertQueryOperator[] = isBooleanField ? BOOLEAN_OPERATORS : (fieldDef?.operators ?? ALL_NUMERIC_OPERATORS); function handleFieldChange(key: string) { actions.setField(path, key); } function handleOperatorChange(op: string) { actions.setOperator(path, op); } function handleValueChange(idx: number, raw: string) { const n = parseFloat(parseCommas(raw)); if (!isNaN(n)) { actions.setValue(path, idx, n, "number"); } else if (!raw) { // eslint-disable-next-line @typescript-eslint/no-explicit-any actions.setValue(path, idx, null as any, "number"); } } function handleBoolChange(v: string) { actions.setValue(path, 0, v === "true", "boolean"); } const valStr = (v: unknown): string => { if (v === null || v === undefined) return ""; return String(v); }; return (
{/* Field */} {/* Operator */} {/* Value */} {isBooleanField ? ( ) : ( handleValueChange(0, v)} unit={fieldDef?.unit} min={fieldDef?.min} max={fieldDef?.max} /> )}
); } // --------------------------------------------------------------------------- // RuleAccordionItem // --------------------------------------------------------------------------- function RuleAccordionItem({ ruleId, ruleProps, path, actions, fields, longestFieldLabel, longestOperatorLabel, removable, }: { ruleId: string; // eslint-disable-next-line @typescript-eslint/no-explicit-any ruleProps: any; path: string[]; actions: Actions; fields: AlertQueryField[]; longestFieldLabel: string; longestOperatorLabel: string; removable: boolean; }) { const summary = ruleSummary(ruleProps, fields); return ( svg:last-child]:rotate-180", )} > {summary}
); } // --------------------------------------------------------------------------- // SubGroupAccordionItem // --------------------------------------------------------------------------- function SubGroupAccordionItem({ groupId, group, path, actions, fields, longestFieldLabel, longestOperatorLabel, }: { groupId: string; // eslint-disable-next-line @typescript-eslint/no-explicit-any group: any; path: string[]; actions: Actions; fields: AlertQueryField[]; longestFieldLabel: string; longestOperatorLabel: string; }) { const conjunction: string = group.properties?.conjunction ?? "AND"; const nestedChildren: JsonItem[] = group.children1 ?? []; const count = nestedChildren.length; const triggerLabel = `Group (${count} ${count === 1 ? "rule" : "rules"})`; return ( svg:last-child]:rotate-180", )} > {triggerLabel}
{/* Nested combinator */}
actions.setConjunction(path, checked ? "AND" : "OR") } /> ({conjunction})
{/* Nested rules — flat rows */} {nestedChildren.map((child) => { // eslint-disable-next-line @typescript-eslint/no-explicit-any const c = child as any; const ruleId: string = c.id ?? ""; const rulePath = [...path, ruleId]; return (
); })} {/* Add rule inside group */}
); } // --------------------------------------------------------------------------- // CustomBuilderUI // --------------------------------------------------------------------------- export interface CustomBuilderUIProps { tree: ImmutableTree; actions: Actions; fields: AlertQueryField[]; defaultOpenItems: string[]; className?: string; } export function CustomBuilderUI({ tree, actions, fields, defaultOpenItems, className, }: CustomBuilderUIProps) { // eslint-disable-next-line @typescript-eslint/no-explicit-any const treeJson = QbUtils.getTree(tree) as any; const rootId: string = treeJson?.id ?? "root"; const conjunction: string = treeJson?.properties?.conjunction ?? "AND"; const children: JsonItem[] = treeJson?.children1 ?? []; const longestFieldLabel = React.useMemo( () => longestOf(fields.map((f) => f.label)), [fields], ); const longestOperatorLabel = React.useMemo( () => longestOf(allOperatorLabels(fields)), [fields], ); return (
{/* Root combinator */}
actions.setConjunction([rootId], checked ? "AND" : "OR") } /> ({conjunction})
{/* Rules and groups */} {children.map((child) => { // eslint-disable-next-line @typescript-eslint/no-explicit-any const c = child as any; const childId: string = c.id ?? ""; const childPath = [rootId, childId]; if (c.type === "group") { return ( ); } return ( 1} /> ); })} {/* Action buttons */}
); }