Postfach- und Ordneransicht fuer archivierte Mails (Next.js/React/TS, @nexarch/shl). Ordnerbaum (aus der flachen Postfachliste der Mail-API clientseitig aufgebaut), virtualisierte Nachrichtenliste mit Sortierung nach Datum/Absender/Betreff, Grundsuche. Aufbau nach etabliertem Muster (web/mail-search fuer die App-Struktur, web/dms-browser fuer Ordnerbaum/ virtualisierte Liste), bewusst schlicht gehalten (Mailpit/MailHog als Vorbild, nicht die Komplexitaet eines nativen Mailprogramms). Automatisierte Tests je Akzeptanzkriterium, inkl. eines echten Backend-Integrationstests (e2e/ux01.e2e.test.ts) gegen den echten mailapi.Server (mail/cmd/mailpostfach-fixture) mit zwei Mandanten und einem gleichnamigen Ordner — keine Mandanten-Isolationslogik wird gemockt. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
62 lines
2.0 KiB
TypeScript
62 lines
2.0 KiB
TypeScript
// Akzeptanzkriterium 1: Ordnerbaum. Die Mail-API liefert Postfächer/Ordner
|
|
// als flache Liste von Namen (mail/internal/search: mailbox-Facette, keine
|
|
// Hierarchie im Datenmodell) — IMAP-Importe benennen verschachtelte Ordner
|
|
// aber üblicherweise mit "/" als Trenner (z. B. "INBOX/Projekte"). Dieser
|
|
// Baum wird daher rein clientseitig aus den flachen Namen aufgebaut, ohne
|
|
// eine Backend-Hierarchie zu erfinden, die es nicht gibt.
|
|
import type { MailboxDTO } from "./api";
|
|
|
|
export interface FolderNode {
|
|
name: string; // voller Pfad, z.B. "INBOX/Projekte" — das ist der Wert für fetchMessages({ mailbox })
|
|
label: string; // letztes Pfadsegment, für die Anzeige
|
|
depth: number;
|
|
messageCount: number | null; // null = reine Gruppierungsebene, kein eigenes Postfach
|
|
children: FolderNode[];
|
|
}
|
|
|
|
export function buildFolderTree(mailboxes: MailboxDTO[]): FolderNode[] {
|
|
const roots: FolderNode[] = [];
|
|
const byPath = new Map<string, FolderNode>();
|
|
|
|
function ensureNode(segments: string[]): FolderNode {
|
|
const path = segments.join("/");
|
|
const existing = byPath.get(path);
|
|
if (existing) return existing;
|
|
|
|
const node: FolderNode = {
|
|
name: path,
|
|
label: segments[segments.length - 1],
|
|
depth: segments.length - 1,
|
|
messageCount: null,
|
|
children: [],
|
|
};
|
|
byPath.set(path, node);
|
|
|
|
if (segments.length === 1) {
|
|
roots.push(node);
|
|
} else {
|
|
const parent = ensureNode(segments.slice(0, -1));
|
|
parent.children.push(node);
|
|
}
|
|
return node;
|
|
}
|
|
|
|
for (const mailbox of mailboxes) {
|
|
const node = ensureNode(mailbox.name.split("/"));
|
|
node.messageCount = mailbox.messageCount;
|
|
}
|
|
|
|
return roots;
|
|
}
|
|
|
|
export function flattenVisible(nodes: FolderNode[], expanded: Set<string>): FolderNode[] {
|
|
const out: FolderNode[] = [];
|
|
for (const node of nodes) {
|
|
out.push(node);
|
|
if (node.children.length > 0 && expanded.has(node.name)) {
|
|
out.push(...flattenVisible(node.children, expanded));
|
|
}
|
|
}
|
|
return out;
|
|
}
|