import { useEffect, useMemo, useRef, useState } from "react"; import { createPortal } from "react-dom"; import { Button } from "@govoplan/core-webui"; import { Dialog } from "@govoplan/core-webui"; import { DismissibleAlert } from "@govoplan/core-webui"; import { buildUndefinedPlaceholders, extractTemplatePlaceholders, removePlaceholderFromText, replacePlaceholderInText, type TemplateNamespace, type UndefinedPlaceholder } from "../utils/templatePlaceholders"; import { TemplateFieldChipList, UndefinedPlaceholderDecisionDialog, UndefinedPlaceholderList, type TemplateFieldOption } from "./TemplatePlaceholderControls"; export default function TemplateExpressionEditorDialog({ open, title, value, localFields, globalFields, placeholder, description, saveLabel = "Save", validate, onCancel, onSave, onAddField, canAddField = () => true }: { open: boolean; title: string; value: string; localFields: TemplateFieldOption[]; globalFields: TemplateFieldOption[]; placeholder?: string; description?: string; saveLabel?: string; validate?: (value: string) => string; onCancel: () => void; onSave: (value: string) => void; onAddField: (name: string) => void; canAddField?: (name: string) => boolean; }) { const [draftValue, setDraftValue] = useState(value); const [undefinedDialog, setUndefinedDialog] = useState(null); const inputRef = useRef(null); useEffect(() => { if (!open) return; setDraftValue(value); setUndefinedDialog(null); }, [open, value]); const usedPlaceholders = useMemo(() => extractTemplatePlaceholders(draftValue), [draftValue]); const localNames = useMemo(() => new Set(localFields.map((field) => field.name)), [localFields]); const globalNames = useMemo(() => new Set(globalFields.map((field) => field.name)), [globalFields]); const availableNames = useMemo(() => new Set([...localNames, ...globalNames]), [globalNames, localNames]); const undefinedPlaceholders = useMemo( () => buildUndefinedPlaceholders(usedPlaceholders, availableNames, { local: localNames, global: globalNames }), [availableNames, globalNames, localNames, usedPlaceholders] ); const validationMessage = validate?.(draftValue) ?? ""; function insertPlaceholder(namespace: TemplateNamespace, name: string) { const input = inputRef.current; const token = `{{${namespace}:${name}}}`; const defaultPosition = draftValue.toLocaleLowerCase().endsWith(".zip") ? draftValue.length - 4 : draftValue.length; const start = input?.selectionStart ?? defaultPosition; const end = input?.selectionEnd ?? start; setDraftValue(`${draftValue.slice(0, start)}${token}${draftValue.slice(end)}`); window.requestAnimationFrame(() => { input?.focus(); const cursor = start + token.length; input?.setSelectionRange(cursor, cursor); }); } function requestSave() { if (validationMessage) return; if (undefinedPlaceholders.length > 0) { setUndefinedDialog(undefinedPlaceholders[0]); return; } onSave(draftValue.trim()); } function removeUndefined(field: UndefinedPlaceholder) { setDraftValue((current) => removePlaceholderFromText(current, field.raw)); setUndefinedDialog(null); } function addUndefined(field: UndefinedPlaceholder) { if (!field.name || field.reason === "invalid-namespace") return; onAddField(field.name); setUndefinedDialog(null); } function replaceUndefined(field: UndefinedPlaceholder, namespace: TemplateNamespace, name: string) { setDraftValue((current) => replacePlaceholderInText(current, field.raw, `{{${namespace}:${name}}}`)); setUndefinedDialog(null); } if (!open || typeof document === "undefined") return null; return createPortal( <> )} > {description &&

{description}

} {validationMessage && {validationMessage}}

Recipient fields

Select a field to insert it at the current cursor position. Address fields represent the first effective address; use to[2] or to[2].email for another single value.

Campaign fields

Used, but undefined

setUndefinedDialog(null)} onRemove={removeUndefined} onReplace={replaceUndefined} onAddField={addUndefined} localFields={localFields} globalFields={globalFields} addDisabled={Boolean(undefinedDialog?.name) && !canAddField(undefinedDialog?.name ?? "")} /> , document.body ); }