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 */}
);
}