70 lines
2.4 KiB
TypeScript
70 lines
2.4 KiB
TypeScript
import { useState } from "react";
|
|||
|
|
|
||
|
|
import { useT } from "../../i18n";
|
||
|
|
import { ApiError } from "../../lib/api";
|
||
|
|
import type { MailTemplate } from "../../lib/types";
|
||
|
|
import { AppDialog, DialogButtons } from "../AppDialog";
|
||
|
|
|
||
|
|
/** Compose → More → Templates → Save as template: name it, save it. A name
|
||
|
|
* already taken turns the button into "Replace" rather than asking again. */
|
||
|
|
export function SaveTemplateDialog({
|
||
|
|
initialName,
|
||
|
|
existing,
|
||
|
|
onSave,
|
||
|
|
onClose,
|
||
|
|
}: {
|
||
|
|
initialName: string;
|
||
|
|
existing: MailTemplate[];
|
||
|
|
onSave: (name: string) => Promise<void>;
|
||
|
|
onClose: () => void;
|
||
|
|
}) {
|
||
|
|
const t = useT();
|
||
|
|
const [name, setName] = useState(initialName);
|
||
|
|
const [saving, setSaving] = useState(false);
|
||
|
|
const [error, setError] = useState("");
|
||
|
|
const trimmed = name.trim();
|
||
|
|
const taken = existing.some((x) => x.name.toLowerCase() === trimmed.toLowerCase());
|
||
|
|
|
||
|
|
const submit = async (e: React.FormEvent) => {
|
||
|
|
e.preventDefault();
|
||
|
|
if (!trimmed || saving) return;
|
||
|
|
setSaving(true);
|
||
|
|
setError("");
|
||
|
|
try {
|
||
|
|
await onSave(trimmed);
|
||
|
|
onClose();
|
||
|
|
} catch (err) {
|
||
|
|
setError(err instanceof ApiError ? err.message : t("compose.templateSaveFailed"));
|
||
|
|
setSaving(false);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
return (
|
||
|
|
<AppDialog title={t("compose.saveTemplateTitle")} onClose={onClose}>
|
||
|
|
<form onSubmit={submit}>
|
||
|
|
<p className="mt-2 text-sm text-app-muted">{t("compose.saveTemplateHint")}</p>
|
||
|
|
<label className="mt-4 block text-sm">
|
||
|
|
<span className="text-app-muted">{t("compose.templateName")}</span>
|
||
|
|
<input
|
||
|
|
className="mt-1 w-full rounded-lg border border-app-border bg-app-bg px-3 py-1.5 text-app-fg placeholder:text-app-faint focus:border-app-accent focus:outline-none"
|
||
|
|
value={name}
|
||
|
|
maxLength={80}
|
||
|
|
placeholder={t("compose.templateNamePlaceholder")}
|
||
|
|
onChange={(e) => setName(e.target.value)}
|
||
|
|
autoFocus
|
||
|
|
onFocus={(e) => e.currentTarget.select()}
|
||
|
|
/>
|
||
|
|
</label>
|
||
|
|
{taken && <p className="mt-2 text-xs text-amber-600 dark:text-amber-400">{t("compose.templateReplaceNote")}</p>}
|
||
|
|
{error && <p className="mt-2 text-xs text-red-500">{error}</p>}
|
||
|
|
<DialogButtons
|
||
|
|
submit
|
||
|
|
confirmLabel={saving ? t("compose.templateSaving") : taken ? t("compose.templateReplaceAction") : t("common.save")}
|
||
|
|
disabled={!trimmed || saving}
|
||
|
|
onCancel={onClose}
|
||
|
|
/>
|
||
|
|
</form>
|
||
|
|
</AppDialog>
|
||
|
|
);
|
||
|
|
}
|