initial commit after split

This commit is contained in:
2026-06-24 01:43:27 +02:00
parent 2406e70597
commit 7f25a3ccdf
125 changed files with 25551 additions and 0 deletions

View File

@@ -0,0 +1,177 @@
import { useEffect, useMemo, useRef, useState } from "react";
import { createPortal } from "react-dom";
import Button from "../../../components/Button";
import Dialog from "../../../components/Dialog";
import DismissibleAlert from "../../../components/DismissibleAlert";
import {
buildUndefinedPlaceholders,
extractTemplatePlaceholders,
removePlaceholderFromText,
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<UndefinedPlaceholder | null>(null);
const inputRef = useRef<HTMLInputElement | null>(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);
}
if (!open || typeof document === "undefined") return null;
return createPortal(
<>
<Dialog
open={open && !undefinedDialog}
title={title}
closeLabel="Cancel filename changes"
className="template-expression-dialog"
headerClassName="template-expression-dialog-header"
bodyClassName="template-expression-dialog-body"
footerClassName="template-expression-dialog-footer"
onClose={onCancel}
footer={(
<>
<Button onClick={onCancel}>Cancel</Button>
<Button variant="primary" onClick={requestSave} disabled={Boolean(validationMessage)}>{saveLabel}</Button>
</>
)}
>
{description && <p className="muted template-expression-description">{description}</p>}
<label className="template-expression-value-field">
<span>Archive filename</span>
<input
ref={inputRef}
value={draftValue}
placeholder={placeholder}
aria-invalid={Boolean(validationMessage) || undefined}
onChange={(event) => setDraftValue(event.target.value)}
onKeyDown={(event) => {
if (event.key === "Enter") {
event.preventDefault();
requestSave();
}
}}
/>
</label>
{validationMessage && <DismissibleAlert tone="danger" resetKey={validationMessage}>{validationMessage}</DismissibleAlert>}
<h3 className="section-mini-heading">Recipient fields</h3>
<p className="muted small-note">Select a field to insert it at the current cursor position. Address fields represent the first effective address; use <code>to[2]</code> or <code>to[2].email</code> for another single value.</p>
<TemplateFieldChipList
namespace="local"
fields={localFields}
usedPlaceholders={usedPlaceholders}
empty="No recipient fields are available."
onInsert={insertPlaceholder}
/>
<h3 className="section-mini-heading">Campaign fields</h3>
<TemplateFieldChipList
namespace="global"
fields={globalFields}
usedPlaceholders={usedPlaceholders}
empty="No campaign fields or global values are available."
onInsert={insertPlaceholder}
/>
<h3 className="section-mini-heading">Used, but undefined</h3>
<UndefinedPlaceholderList items={undefinedPlaceholders} onSelect={setUndefinedDialog} />
</Dialog>
<UndefinedPlaceholderDecisionDialog
field={undefinedDialog}
contextLabel="archive filename"
removeLabel="Remove from filename"
onCancel={() => setUndefinedDialog(null)}
onRemove={removeUndefined}
onAddField={addUndefined}
addDisabled={Boolean(undefinedDialog?.name) && !canAddField(undefinedDialog?.name ?? "")}
/>
</>,
document.body
);
}