186 lines
7.0 KiB
TypeScript
186 lines
7.0 KiB
TypeScript
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 = "i18n:govoplan-campaign.save.efc007a3",
|
|
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);
|
|
}
|
|
|
|
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(
|
|
<>
|
|
<Dialog
|
|
open={open && !undefinedDialog}
|
|
title={title}
|
|
closeLabel="i18n:govoplan-campaign.cancel_filename_changes.b558598e"
|
|
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}>i18n:govoplan-campaign.cancel.77dfd213</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>i18n:govoplan-campaign.archive_filename.47becf3d</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">i18n:govoplan-campaign.recipient_fields.fdbcd95b</h3>
|
|
<p className="muted small-note">i18n:govoplan-campaign.select_a_field_to_insert_it_at_the_current_curso.1d1b51be <code>i18n:govoplan-campaign.to_2.8c1db0c7</code> or <code>i18n:govoplan-campaign.to_2_email.5ad61c1a</code> i18n:govoplan-campaign.for_another_single_value.ce57d76a</p>
|
|
<TemplateFieldChipList
|
|
namespace="local"
|
|
fields={localFields}
|
|
usedPlaceholders={usedPlaceholders}
|
|
empty="i18n:govoplan-campaign.no_recipient_fields_are_available.5b7943ad"
|
|
onInsert={insertPlaceholder} />
|
|
|
|
|
|
<h3 className="section-mini-heading">i18n:govoplan-campaign.campaign_fields.969e7d80</h3>
|
|
<TemplateFieldChipList
|
|
namespace="global"
|
|
fields={globalFields}
|
|
usedPlaceholders={usedPlaceholders}
|
|
empty="i18n:govoplan-campaign.no_campaign_fields_or_global_values_are_availabl.b2e4089d"
|
|
onInsert={insertPlaceholder} />
|
|
|
|
|
|
<h3 className="section-mini-heading">i18n:govoplan-campaign.used_but_undefined.3b829043</h3>
|
|
<UndefinedPlaceholderList items={undefinedPlaceholders} onSelect={setUndefinedDialog} />
|
|
</Dialog>
|
|
|
|
<UndefinedPlaceholderDecisionDialog
|
|
field={undefinedDialog}
|
|
contextLabel="i18n:govoplan-campaign.archive_filename.de958cbd"
|
|
removeLabel="i18n:govoplan-campaign.remove_from_filename.560f7f42"
|
|
onCancel={() => setUndefinedDialog(null)}
|
|
onRemove={removeUndefined}
|
|
onReplace={replaceUndefined}
|
|
onAddField={addUndefined}
|
|
localFields={localFields}
|
|
globalFields={globalFields}
|
|
addDisabled={Boolean(undefinedDialog?.name) && !canAddField(undefinedDialog?.name ?? "")} />
|
|
|
|
</>,
|
|
document.body
|
|
);
|
|
} |