/** * Create / edit a Smart List — a saved, **dynamic** contact segment: name + * optional description + a condition query (the published * `ContactAlertQueryBuilder` + `ALERT_QUERY_FIELDS`) with a live match-count * preview. * * Same saved-query engine as a Contact Alert but **without the alert * `severity`** — a Smart List's purpose is *targeting / sending* (the Email * Marketing composer consumes it as a recipient source), not monitoring. * Membership is dynamic: any client matching the conditions is included now * and in future; editing the conditions recomputes membership. At least one * complete condition is required — Save stays disabled otherwise. * * Lists are shared company assets; there is deliberately no per-list sharing * control. */ import { type ReactElement, useEffect, useMemo, useState } from "react"; import { Users } from "lucide-react"; import { Utils as QbUtils, type ImmutableTree, } from "@react-awesome-query-builder/ui"; import { Button } from "./button"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, } from "./dialog"; import { Field, FieldLabel } from "./field"; import { Input } from "./input"; import { Textarea } from "./textarea"; import { TagInput } from "./tag-input"; import { ContactAlertQueryBuilder, ALERT_QUERY_FIELDS, createAlertTree, } from "./contact-alert-dialog"; import { QB_CONFIG } from "./contact-alert-dialog/config"; import { isTreeValid } from "./contact-alert-dialog/utils"; export interface CreateSmartListPayload { name: string; description: string; /** The sanitized query tree — serialize with `QbUtils.getTree` for the API. */ filterSegment: ImmutableTree; /** Tags the segment matches — a contact with ANY of them is included. */ tags: string[]; } export interface CreateSmartListDialogProps { open: boolean; onOpenChange: (open: boolean) => void; mode?: "create" | "edit"; initialName?: string; initialDescription?: string; /** Seed conditions — e.g. when saving the current Contacts filter as a list. */ initialQuery?: Parameters[0]; /** Live count of contacts matching the current conditions. */ matchingCount?: number; /** The broker's tag library, for the Tags condition autocomplete. */ availableTags?: string[]; initialTags?: string[]; onSave?: (payload: CreateSmartListPayload) => void; /** Fires as the tree changes so the host can refresh the live match count. */ onQueryChange?: (tree: ImmutableTree) => void; isLoading?: boolean; } export function CreateSmartListDialog({ open, onOpenChange, mode = "create", initialName = "", initialDescription = "", initialQuery, matchingCount, availableTags = [], initialTags = [], onSave, onQueryChange, isLoading = false, }: CreateSmartListDialogProps): ReactElement { const [name, setName] = useState(initialName); const [description, setDescription] = useState(initialDescription); const [tree, setTree] = useState(() => createAlertTree(initialQuery)); const [tags, setTags] = useState(initialTags); // Reset local state each time the dialog opens. useEffect(() => { if (open) { setName(initialName); setDescription(initialDescription); setTree(createAlertTree(initialQuery)); setTags(initialTags); } // eslint-disable-next-line react-hooks/exhaustive-deps -- reset from initial* props only on open, not while typing }, [open]); const hasValidRule = useMemo(() => isTreeValid(tree), [tree]); // A smart list can be built from tags, conditions, or both. const canSave = name.trim().length > 0 && (hasValidRule || tags.length > 0) && !isLoading; const handleTreeChange = (next: ImmutableTree): void => { setTree(next); onQueryChange?.(next); }; const handleSave = (): void => { if (!canSave) return; const filterSegment = QbUtils.sanitizeTree(tree, QB_CONFIG).fixedTree; onSave?.({ name: name.trim(), description: description.trim(), filterSegment, tags, }); onOpenChange(false); }; return ( {mode === "edit" ? "Edit smart list" : "Create smart list"}
Smart list name { setName(e.target.value); }} placeholder="e.g. High-equity refinance prospects" value={name} /> Description (optional)