diff --git a/components/admin/org/sections/vacation-templates-section.tsx b/components/admin/org/sections/vacation-templates-section.tsx index c974fef78..040408e20 100644 --- a/components/admin/org/sections/vacation-templates-section.tsx +++ b/components/admin/org/sections/vacation-templates-section.tsx @@ -1,13 +1,239 @@ 'use client'; +import { useId, useState } from 'react'; +import { AlertTriangle, ChevronDown, ChevronRight, Plus, RotateCcw, Trash2 } from '@/components/icons'; import { SettingsSection } from '@/components/settings/settings-section'; +import { Button } from '@/components/ui/button'; +import type { VacationTemplate } from '@/lib/admin/organisation-policy'; +import { sanitizeEmailHtml } from '@/lib/email-sanitization'; +import { cn, generateUUID } from '@/lib/utils'; +import { fillVacationTemplate, VACATION_PLACEHOLDERS } from '@/lib/vacation-templates'; import type { OrgPolicyDraft } from '../use-org-policy-draft'; +/** Same cap as the sanitiser (organisation-policy.ts). */ +const MAX_LIBRARY = 200; +/** What Stalwart takes for an out-of-office reply (bytes); a longer one can't be saved by users. */ +export const STALWART_LIMITS = { subject: 511, body: 2047 }; +const DOCS_URL = 'https://bulwarkmail.org/docs/guides/organisation/out-of-office'; + +const inputClass = + 'block w-full rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground ' + + 'placeholder:text-muted-foreground hover:border-muted-foreground focus-visible:outline-hidden focus-visible:border-ring'; +const labelClass = 'mb-1 block text-xs font-medium text-foreground'; + +const PLACEHOLDER_HELP: Record<(typeof VACATION_PLACEHOLDERS)[number], string> = { + name: 'the user’s name', + email: 'their address', + company: 'the company name from Branding', + start: 'the first day away', + end: 'the last day away (or the return)', + deputy: 'the address of a colleague the user types in when picking the template', + deputy_name: 'that colleague’s name', +}; + +const EXAMPLE = { + name: 'Alex Example', email: 'alex@example.com', company: 'Example Ltd', + start: 'Monday 21 December 2026', end: 'Monday 4 January 2027', + deputy: 'sam@example.com', deputy_name: 'Sam Example', +}; + +const bytes = (value: string) => new TextEncoder().encode(value).length; + +/** Over Stalwart's limits once filled in with a typical name and dates (the placeholders grow a little). */ +export function vacationTemplateTooLong(template: VacationTemplate): { subject: boolean; body: boolean } { + const filled = fillVacationTemplate(template, EXAMPLE); + return { + subject: bytes(filled.subject) > STALWART_LIMITS.subject, + body: bytes(filled.text) > STALWART_LIMITS.body || bytes(filled.html) > STALWART_LIMITS.body, + }; +} + +function VacationTemplateEditor({ template, onChange, onRemove }: { + template: VacationTemplate; + onChange: (template: VacationTemplate) => void; + onRemove: () => void; +}) { + const id = useId(); + const [open, setOpen] = useState(!template.name); + const [html, setHtml] = useState(!!template.html); + const set = (patch: Partial) => onChange({ ...template, ...patch }); + const tooLong = vacationTemplateTooLong(template); + const preview = fillVacationTemplate(template, EXAMPLE); + return ( +
  • +
    + + +
    + {!template.name.trim() && ( +

    A reply without a name is not saved. Give it one.

    + )} + {open && ( +
    +
    +
    + + set({ name: e.target.value })} /> +
    +
    + + set({ category: e.target.value })} /> +
    +
    +
    + + set({ subject: e.target.value })} /> + {tooLong.subject &&

    Longer than Stalwart takes (511 bytes). Users couldn't save it.

    } +
    +
    + +