'use client'; import { useEffect, useRef, useState } from 'react'; import { Plus, Trash2, RotateCcw, ChevronDown, ChevronRight } from 'lucide-react'; import type { JmapServerEntry } from '@/lib/admin/jmap-servers'; interface Props { value: JmapServerEntry[]; source?: string; onChange: (next: JmapServerEntry[]) => void; onRevert: () => void; } interface RowDraft { id: string; label: string; url: string; domains: string; oauthClientId: string; oauthIssuerUrl: string; oauthClientSecret: string; oauthExpanded: boolean; } function entryToDraft(e: JmapServerEntry): RowDraft { return { id: e.id, label: e.label, url: e.url, domains: (e.domains ?? []).join(', '), oauthClientId: e.oauth?.clientId ?? '', oauthIssuerUrl: e.oauth?.issuerUrl ?? '', oauthClientSecret: e.oauth?.clientSecret ?? '', oauthExpanded: !!(e.oauth && (e.oauth.clientId || e.oauth.issuerUrl || e.oauth.clientSecret)), }; } function draftToEntry(d: RowDraft): JmapServerEntry | null { const id = d.id.trim(); const url = d.url.trim().replace(/\/+$/, ''); if (!id || !url) return null; const domains = d.domains .split(/[,\s]+/) .map((s) => s.trim().toLowerCase()) .filter(Boolean); const clientId = d.oauthClientId.trim(); const issuerUrl = d.oauthIssuerUrl.trim().replace(/\/+$/, ''); const clientSecret = d.oauthClientSecret; const oauth = clientId || issuerUrl || clientSecret ? { ...(clientId ? { clientId } : {}), ...(issuerUrl ? { issuerUrl } : {}), ...(clientSecret ? { clientSecret } : {}), } : undefined; return { id, label: d.label.trim() || id, url, ...(domains.length > 0 ? { domains } : {}), ...(oauth ? { oauth } : {}), }; } function emptyDraft(): RowDraft { return { id: '', label: '', url: '', domains: '', oauthClientId: '', oauthIssuerUrl: '', oauthClientSecret: '', oauthExpanded: false, }; } export function JmapServersSection({ value, source, onChange, onRevert }: Props) { const [drafts, setDrafts] = useState(() => value.map(entryToDraft)); const lastEmittedRef = useRef(value); useEffect(() => { if (value === lastEmittedRef.current) return; setDrafts(value.map(entryToDraft)) }, [value]); function commit(next: RowDraft[]) { setDrafts(next); const entries = next.map(draftToEntry).filter((e): e is JmapServerEntry => e !== null); lastEmittedRef.current = entries; onChange(entries); } function update(idx: number, patch: Partial) { commit(drafts.map((d, i) => (i === idx ? { ...d, ...patch } : d))); } function remove(idx: number) { commit(drafts.filter((_, i) => i !== idx)); } function add() { setDrafts((prev) => [...prev, emptyDraft()]); // Don't commit yet - new row needs id+url before it counts. } const ids = new Set(); const duplicateIdx = new Set(); drafts.forEach((d, i) => { const id = d.id.trim(); if (!id) return; if (ids.has(id)) duplicateIdx.add(i); ids.add(id); }); return (
Servers {source && source !== 'default' && ( {source} )}

Each entry appears as an option on the login dropdown. Leave the list empty to fall back to the single JMAP Server URL above.

{source === 'admin' && ( )}
{drafts.length === 0 && (
No servers configured.
)} {drafts.map((d, i) => { const isDuplicate = duplicateIdx.has(i); return (
update(i, { id: e.target.value })} placeholder="main" className={`h-8 w-full rounded-md border bg-background px-2 text-sm text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring ${isDuplicate ? 'border-destructive' : 'border-input'}`} /> {isDuplicate &&

Duplicate id

}
update(i, { label: e.target.value })} placeholder="Main server" className="h-8 w-full rounded-md border border-input bg-background px-2 text-sm text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring" />
update(i, { url: e.target.value })} placeholder="https://mail.example.com" className="h-8 w-full rounded-md border border-input bg-background px-2 text-sm text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring" />
update(i, { domains: e.target.value })} placeholder="example.com, example.org" className="h-8 w-full rounded-md border border-input bg-background px-2 text-sm text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring" />
{d.oauthExpanded && (
update(i, { oauthClientId: e.target.value })} className="h-8 w-full rounded-md border border-input bg-background px-2 text-sm text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring" />
update(i, { oauthIssuerUrl: e.target.value })} placeholder="https://auth.example.com" className="h-8 w-full rounded-md border border-input bg-background px-2 text-sm text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring" />
update(i, { oauthClientSecret: e.target.value })} className="h-8 w-full rounded-md border border-input bg-background px-2 text-sm text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring" />
)}
); })}
); }