9de58d43e8
Trilium-Integration komplett zurückgenommen (Revert von 47951a6): keine
verwaltete Instanz, kein Runtime-Download, kein ETAPI, kein Keyring-Token,
kein Setup-Wizard. Das Archiv sind wieder Markdown-Dateien unter archiveHome
samt FTS5-Suche und Wikilinks.
Notizmodell (an Trilium orientiert, ohne dessen Überbau):
- Jeder Knoten besitzt eine Textdatei — ensure_node_texts legt fehlende beim
Seitenaufbau an (Ordner: gleichnamiges .md daneben, Wurzel: index.md);
Ordner und Zwillingsdatei sind EINE Notiz mit Inhalt und Kindern.
- Der angezeigte Titel steht im Frontmatter der Datei, der Datei-/Ordnername
ist technischer Verwaltungsname; move_folder nimmt den Knotentext mit.
Archiv-Ansicht (wiki-view):
- Baum links: Knoten- und Blatt-Symbole, Führungslinien pro Ebene, Dokumente
näher an der Linie als Ordner; ganze Zeile wählt aus, nur der Pfeil klappt.
- Notiz-Ansicht rechts: großer Titel (Klick bearbeitet ihn → archive_set_title),
Zurück-Knopf mit Auswahl-Verlauf, Details als Popup am Info-Knopf, Kindliste
als abgesetzter Karten-Block mit Anlage-/Änderungsdatum.
- Bearbeiten in place: Split-View aus Rohtext und Live-Vorschau, Speichern per
Knopf oder Ctrl+S (archive_read/archive_write; Frontmatter bleibt).
- Rechte Maustaste im Baum als einziger Aktionsweg (eigenes Kontextmenü,
Standard-Menü des Webviews aus); Anlegen/Umbenennen über modale Dialoge.
Feste Flächenaufteilung statt Ablösen/Andocken:
- Angedocktes Panel = Session-Begleiter (ToDo, Befehle, flüchtiger Entwurf);
der Dokument-Tab ist aus der Leiste raus.
- Archiv-Fenster = Archiv + Suche, Größe und Position einmalig beim Öffnen
aus dem Monitor des rufenden Terminal-Fensters (85 % / 92 %); Öffner-Klick
bei stehendem Fenster fokussiert und schaltet den Tab (panel-mode).
- Terminal-Fenster zu → Archiv-Fenster geht mit.
Neue Commands: archive_read, archive_write, archive_set_title.
373 lines
15 KiB
TypeScript
373 lines
15 KiB
TypeScript
// Archiv-Ansicht: Baum mit Notizen (Ordner + Dokumente, Verschmelzung
|
|
// gleichnamiger Paare), Notiz-Ansicht rechts (Titel, Inhalt, Kindliste),
|
|
// Aktionen (bearbeiten, umbenennen, löschen, anlegen), Suchtreffer-Vormerkung,
|
|
// Leerzustände.
|
|
import { describe, expect, it, vi } from "vitest";
|
|
import { initWikiView } from "./wiki-view";
|
|
|
|
function setup(pending: string | null = null) {
|
|
document.body.innerHTML = `<div id="w"></div>`;
|
|
const readDoc = vi.fn(() => Promise.resolve("# Inhalt\n\nText mit [[neu]]."));
|
|
const writeDoc = vi.fn(() => Promise.resolve());
|
|
const setTitle = vi.fn();
|
|
const openWiki = vi.fn();
|
|
const actions = {
|
|
rename: vi.fn(),
|
|
remove: vi.fn(),
|
|
moveFolder: vi.fn(),
|
|
createFolder: vi.fn(),
|
|
createDoc: vi.fn(),
|
|
};
|
|
let pendingSelect = pending;
|
|
const view = initWikiView(document.getElementById("w")!, {
|
|
autoStart: true,
|
|
readDoc,
|
|
writeDoc,
|
|
setTitle,
|
|
openWiki,
|
|
takePending: () => {
|
|
const p = pendingSelect;
|
|
pendingSelect = null;
|
|
return p;
|
|
},
|
|
actions,
|
|
});
|
|
return { view, readDoc, writeDoc, setTitle, openWiki, actions };
|
|
}
|
|
|
|
const doc = (relpath: string, name: string, extra: object = {}) => ({
|
|
relpath,
|
|
name,
|
|
title: name.toUpperCase(),
|
|
description: null,
|
|
tags: [],
|
|
date: "2026-07-19",
|
|
backlinks: 0,
|
|
modified: "2026-07-24",
|
|
...extra,
|
|
});
|
|
|
|
const page = JSON.stringify({
|
|
kind: "page",
|
|
home: "/tmp/archiv",
|
|
tag: null,
|
|
total: 3,
|
|
tags: [],
|
|
folders: [
|
|
{
|
|
name: "",
|
|
docs: [
|
|
doc("2026-07-19_1000-wurzel-doc.md", "wurzel-doc", {
|
|
backlinks: 2,
|
|
description: "Beschreibung",
|
|
}),
|
|
],
|
|
},
|
|
{ name: "konzepte", docs: [doc("konzepte/2026-07-19_1005-neu.md", "neu")] },
|
|
{
|
|
name: "konzepte/panel",
|
|
docs: [doc("konzepte/panel/2026-07-19_1010-alt.md", "alt")],
|
|
},
|
|
],
|
|
});
|
|
|
|
/// Ordner `konzepte` mit gleichnamigem Dokument daneben — eine Notiz mit
|
|
/// Inhalt und Kindern.
|
|
const mergedPage = JSON.stringify({
|
|
kind: "page",
|
|
home: "/tmp/archiv",
|
|
tag: null,
|
|
total: 2,
|
|
tags: [],
|
|
folders: [
|
|
{ name: "", docs: [doc("konzepte.md", "konzepte", { title: "Konzepte" })] },
|
|
{ name: "konzepte", docs: [doc("konzepte/2026-07-19_1005-neu.md", "neu")] },
|
|
],
|
|
});
|
|
|
|
const flush = () => new Promise((r) => setTimeout(r));
|
|
|
|
describe("initWikiView — Baum", () => {
|
|
it("Baum zeigt Ordner und Dokumente, Wurzel ist gewählt", () => {
|
|
const { view } = setup();
|
|
view.set(page);
|
|
expect(document.querySelector(".wiki-tree-root")!.className).toContain("active");
|
|
const rows = [...document.querySelectorAll(".wiki-tree .wiki-tree-name")].map(
|
|
(e) => e.textContent,
|
|
);
|
|
// Unter jedem Knoten erst Dokumente, dann Ordner (mit Titel), sortiert.
|
|
expect(rows).toEqual(["WURZEL-DOC", "konzepte", "NEU", "panel", "ALT"]);
|
|
// Wurzel-Ansicht rechts: Titel + Kindzähler.
|
|
expect(document.querySelector(".wiki-note-title")!.textContent).toBe("Archiv");
|
|
expect(document.querySelector(".wiki-children-caption")!.textContent).toBe(
|
|
"2 Dokumente",
|
|
);
|
|
});
|
|
|
|
it("gleichnamiges Dokument verschmilzt mit dem Ordner zur Notiz", async () => {
|
|
const { view, readDoc } = setup();
|
|
view.set(mergedPage);
|
|
const rows = [...document.querySelectorAll(".wiki-tree .wiki-tree-name")].map(
|
|
(e) => e.textContent,
|
|
);
|
|
// Kein eigenes Blatt für konzepte.md — der Ordnerknoten trägt den Titel.
|
|
expect(rows).toEqual(["Konzepte", "NEU"]);
|
|
// Auswahl des Knotens zeigt Inhalt UND Kindliste.
|
|
document.querySelector<HTMLElement>(".wiki-tree summary .wiki-tree-name")!.click();
|
|
await flush();
|
|
expect(readDoc).toHaveBeenCalledWith("konzepte.md");
|
|
expect(document.querySelector(".wiki-note-body h1")!.textContent).toBe("Inhalt");
|
|
expect(document.querySelector(".wiki-note-children .wiki-doc-title")!.textContent).toBe(
|
|
"NEU",
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("initWikiView — Notiz-Ansicht", () => {
|
|
it("Dokument-Klick lädt den Body in die Notiz-Ansicht", async () => {
|
|
const { view, readDoc } = setup();
|
|
view.set(page);
|
|
document.querySelector<HTMLElement>(".wiki-tree-doc")!.click(); // WURZEL-DOC
|
|
await flush();
|
|
expect(readDoc).toHaveBeenCalledWith("2026-07-19_1000-wurzel-doc.md");
|
|
expect(document.querySelector(".wiki-note-title")!.textContent).toBe("WURZEL-DOC");
|
|
// Meta als Popup am Info-Knopf.
|
|
const pop = document.querySelector<HTMLElement>(".wiki-note-info-pop")!;
|
|
expect(pop.hidden).toBe(true);
|
|
document.querySelector<HTMLElement>('.wiki-note-actions [title^="Details"]')!.click();
|
|
expect(pop.hidden).toBe(false);
|
|
expect(pop.textContent).toContain("↩ 2");
|
|
expect(document.querySelector(".wiki-note-body h1")!.textContent).toBe("Inhalt");
|
|
});
|
|
|
|
it("Wikilink im Inhalt wählt die Ziel-Notiz lokal aus", async () => {
|
|
const { view, openWiki } = setup();
|
|
view.set(page);
|
|
document.querySelector<HTMLElement>(".wiki-tree-doc")!.click(); // WURZEL-DOC
|
|
await flush();
|
|
document.querySelector<HTMLElement>(".wiki-note-body a.wiki")!.click();
|
|
expect(openWiki).not.toHaveBeenCalled();
|
|
expect(document.querySelector(".wiki-note-title")!.textContent).toBe("NEU");
|
|
});
|
|
|
|
it("Stift schaltet in den Editor, Speichern schreibt zurück", async () => {
|
|
const { view, writeDoc } = setup();
|
|
view.set(page);
|
|
document.querySelector<HTMLElement>(".wiki-tree-doc")!.click();
|
|
await flush();
|
|
document.querySelector<HTMLElement>(".wiki-note-actions .panel-btn")!.click();
|
|
await flush();
|
|
const area = document.querySelector<HTMLTextAreaElement>(".wiki-note-editor")!;
|
|
expect(area.value).toContain("# Inhalt");
|
|
// Vorschau rechts zeigt den gerenderten Rohtext.
|
|
expect(document.querySelector(".wiki-edit-preview h1")!.textContent).toBe("Inhalt");
|
|
area.value = "# Neu\n\nGeändert.";
|
|
area.dispatchEvent(new Event("input"));
|
|
expect(document.querySelector(".wiki-edit-preview h1")!.textContent).toBe("Neu");
|
|
document.querySelector<HTMLElement>(".wiki-form-submit")!.click();
|
|
await flush();
|
|
expect(writeDoc).toHaveBeenCalledWith(
|
|
"2026-07-19_1000-wurzel-doc.md",
|
|
"# Neu\n\nGeändert.",
|
|
);
|
|
// Nach dem Speichern wieder die Anzeige.
|
|
expect(document.querySelector(".wiki-note-editor")).toBeNull();
|
|
expect(document.querySelector(".wiki-note-body")).not.toBeNull();
|
|
});
|
|
|
|
it("Abbrechen verwirft und zeigt wieder an", async () => {
|
|
const { view, writeDoc } = setup();
|
|
view.set(page);
|
|
document.querySelector<HTMLElement>(".wiki-tree-doc")!.click();
|
|
await flush();
|
|
document.querySelector<HTMLElement>(".wiki-note-actions .panel-btn")!.click();
|
|
await flush();
|
|
document.querySelector<HTMLElement>(".wiki-form-cancel")!.click();
|
|
expect(writeDoc).not.toHaveBeenCalled();
|
|
expect(document.querySelector(".wiki-note-body")).not.toBeNull();
|
|
});
|
|
|
|
it("Auswahl übersteht ein Puffer-Update, Verschwundenes fällt zur Wurzel", () => {
|
|
const { view } = setup();
|
|
view.set(page);
|
|
document.querySelector<HTMLElement>(".wiki-tree summary .wiki-tree-name")!.click();
|
|
view.set(page); // Update: Auswahl bleibt
|
|
expect(document.querySelector(".wiki-note-title")!.textContent).toBe("konzepte");
|
|
view.set(
|
|
JSON.stringify({
|
|
kind: "page",
|
|
home: "/tmp/archiv",
|
|
tag: null,
|
|
total: 1,
|
|
tags: [],
|
|
folders: [{ name: "", docs: [doc("2026-07-19_1000-wurzel-doc.md", "wurzel-doc")] }],
|
|
}),
|
|
);
|
|
expect(document.querySelector(".wiki-note-title")!.textContent).toBe("Archiv");
|
|
});
|
|
|
|
it("vorgemerkter Suchtreffer wählt die Notiz beim set() aus", async () => {
|
|
const { view, readDoc } = setup("konzepte/2026-07-19_1005-neu.md");
|
|
view.set(page);
|
|
await flush();
|
|
expect(document.querySelector(".wiki-note-title")!.textContent).toBe("NEU");
|
|
expect(readDoc).toHaveBeenCalledWith("konzepte/2026-07-19_1005-neu.md");
|
|
});
|
|
});
|
|
|
|
describe("initWikiView — Zurück", () => {
|
|
it("Zurück-Knopf führt zur vorherigen Auswahl", async () => {
|
|
const { view } = setup();
|
|
view.set(page);
|
|
document.querySelector<HTMLElement>(".wiki-tree-doc")!.click(); // WURZEL-DOC
|
|
await flush();
|
|
document.querySelector<HTMLElement>(".wiki-tree summary .wiki-tree-name")!.click(); // konzepte
|
|
expect(document.querySelector(".wiki-note-title")!.textContent).toBe("konzepte");
|
|
document.querySelector<HTMLElement>(".wiki-note-back")!.click();
|
|
await flush();
|
|
expect(document.querySelector(".wiki-note-title")!.textContent).toBe("WURZEL-DOC");
|
|
document.querySelector<HTMLElement>(".wiki-note-back")!.click();
|
|
expect(document.querySelector(".wiki-note-title")!.textContent).toBe("Archiv");
|
|
// Verlauf leer: Knopf deaktiviert.
|
|
expect(
|
|
document.querySelector<HTMLButtonElement>(".wiki-note-back")!.disabled,
|
|
).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe("initWikiView — Aktionen", () => {
|
|
it("Klick auf den Titel bearbeitet ihn, Enter setzt den Frontmatter-Titel", async () => {
|
|
const { view, setTitle } = setup();
|
|
view.set(page);
|
|
document.querySelector<HTMLElement>(".wiki-tree-doc")!.click();
|
|
await flush();
|
|
document.querySelector<HTMLElement>(".wiki-note-title.editable")!.click();
|
|
const input = document.querySelector<HTMLInputElement>(".wiki-note-title-input")!;
|
|
expect(input.value).toBe("WURZEL-DOC");
|
|
input.value = "Neuer Titel";
|
|
input.dispatchEvent(new KeyboardEvent("keydown", { key: "Enter", bubbles: true }));
|
|
expect(setTitle).toHaveBeenCalledWith("2026-07-19_1000-wurzel-doc.md", "Neuer Titel");
|
|
});
|
|
|
|
it("Escape verwirft die Titel-Bearbeitung", async () => {
|
|
const { view, setTitle } = setup();
|
|
view.set(page);
|
|
document.querySelector<HTMLElement>(".wiki-tree-doc")!.click();
|
|
await flush();
|
|
document.querySelector<HTMLElement>(".wiki-note-title.editable")!.click();
|
|
const input = document.querySelector<HTMLInputElement>(".wiki-note-title-input")!;
|
|
input.dispatchEvent(new KeyboardEvent("keydown", { key: "Escape", bubbles: true }));
|
|
expect(document.querySelector(".wiki-note-title")!.textContent).toBe("WURZEL-DOC");
|
|
expect(setTitle).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("Löschen meldet mit relpath", async () => {
|
|
const { view, actions } = setup();
|
|
view.set(page);
|
|
document.querySelector<HTMLElement>(".wiki-tree-doc")!.click();
|
|
await flush();
|
|
document.querySelector<HTMLElement>(".wiki-note-actions .cmd-del")!.click();
|
|
expect(actions.remove).toHaveBeenCalledWith("2026-07-19_1000-wurzel-doc.md");
|
|
});
|
|
|
|
it("Plus im Notiz-Kopf: Formular legt Dokument im gewählten Ordner an", () => {
|
|
const { view, actions } = setup();
|
|
view.set(page);
|
|
document.querySelector<HTMLElement>(".wiki-tree summary .wiki-tree-name")!.click();
|
|
document.querySelector<HTMLElement>(".wiki-note-actions .wiki-add")!.click();
|
|
const form = document.querySelector<HTMLElement>(".wiki-form")!;
|
|
expect(form.querySelector(".wiki-form-title")!.textContent).toBe("Neues Dokument");
|
|
const input = form.querySelector<HTMLInputElement>("input")!;
|
|
input.value = "Deploy Notiz";
|
|
form.querySelector<HTMLElement>(".wiki-form-submit")!.click();
|
|
expect(actions.createDoc).toHaveBeenCalledWith("konzepte", "Deploy Notiz");
|
|
expect(document.querySelector(".wiki-form")).toBeNull();
|
|
});
|
|
|
|
it("Abbrechen und Escape schließen das Formular ohne Aktion", () => {
|
|
const { view, actions } = setup();
|
|
view.set(page);
|
|
document.querySelector<HTMLElement>(".wiki-note-actions .wiki-add")!.click();
|
|
document.querySelector<HTMLElement>(".wiki-form-cancel")!.click();
|
|
expect(document.querySelector(".wiki-form")).toBeNull();
|
|
|
|
document.querySelector<HTMLElement>(".wiki-note-actions .wiki-add")!.click();
|
|
document
|
|
.querySelector<HTMLElement>(".wiki-form")!
|
|
.dispatchEvent(new KeyboardEvent("keydown", { key: "Escape", bubbles: true }));
|
|
expect(document.querySelector(".wiki-form")).toBeNull();
|
|
expect(actions.createDoc).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("Rechtsklick auf Ordner: Menü legt Ordner darunter an", () => {
|
|
const { view, actions } = setup();
|
|
view.set(page);
|
|
const summary = document.querySelector<HTMLElement>(".wiki-tree summary")!;
|
|
summary.dispatchEvent(new MouseEvent("contextmenu", { bubbles: true }));
|
|
const items = [...document.querySelectorAll<HTMLElement>(".wiki-menu-item")];
|
|
items.find((i) => i.textContent === "Neuer Ordner")!.click();
|
|
const input = document.querySelector<HTMLInputElement>(".wiki-form input")!;
|
|
expect(input.value).toBe("konzepte/");
|
|
input.value = "konzepte/neu";
|
|
input.dispatchEvent(new KeyboardEvent("keydown", { key: "Enter", bubbles: true }));
|
|
expect(actions.createFolder).toHaveBeenCalledWith("konzepte/neu");
|
|
});
|
|
|
|
it("Rechtsklick auf Ordner: Dialog ändert den Ordnerpfad", () => {
|
|
const { view, actions } = setup();
|
|
view.set(page);
|
|
const summary = document.querySelector<HTMLElement>(".wiki-tree summary")!;
|
|
summary.dispatchEvent(new MouseEvent("contextmenu", { bubbles: true }));
|
|
const items = [...document.querySelectorAll<HTMLElement>(".wiki-menu-item")];
|
|
items.find((i) => i.textContent === "Ordnerpfad ändern")!.click();
|
|
const input = document.querySelector<HTMLInputElement>(".wiki-form input")!;
|
|
expect(input.value).toBe("konzepte");
|
|
input.value = "notizen/2026";
|
|
input.dispatchEvent(new KeyboardEvent("keydown", { key: "Enter", bubbles: true }));
|
|
expect(actions.moveFolder).toHaveBeenCalledWith("konzepte", "notizen/2026");
|
|
});
|
|
|
|
it("Rechtsklick auf Dokument: Menü löscht mit relpath", () => {
|
|
const { view, actions } = setup();
|
|
view.set(page);
|
|
document
|
|
.querySelector<HTMLElement>(".wiki-tree-doc")!
|
|
.dispatchEvent(new MouseEvent("contextmenu", { bubbles: true }));
|
|
const items = [...document.querySelectorAll<HTMLElement>(".wiki-menu-item")];
|
|
items.find((i) => i.textContent === "Dokument löschen")!.click();
|
|
expect(actions.remove).toHaveBeenCalledWith("2026-07-19_1000-wurzel-doc.md");
|
|
expect(document.querySelector(".wiki-menu")).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe("initWikiView — Leerzustände", () => {
|
|
it("leeres Archiv erklärt das Archivieren", () => {
|
|
const { view } = setup();
|
|
view.set(
|
|
JSON.stringify({
|
|
kind: "page",
|
|
home: "/a",
|
|
tag: null,
|
|
total: 0,
|
|
tags: [],
|
|
folders: [],
|
|
}),
|
|
);
|
|
expect(document.querySelector(".wiki-empty")!.textContent).toContain(
|
|
"Das Archiv ist leer.",
|
|
);
|
|
});
|
|
|
|
it("leerer Puffer fordert die Übersicht von selbst an", () => {
|
|
const { view, openWiki } = setup();
|
|
view.set("");
|
|
expect(view.empty()).toBe(true);
|
|
expect(openWiki).toHaveBeenCalledWith("tag:");
|
|
// Nur einmal — das Update kommt über den Puffer zurück.
|
|
view.set("");
|
|
expect(openWiki).toHaveBeenCalledTimes(1);
|
|
view.set(page);
|
|
expect(view.empty()).toBe(false);
|
|
});
|
|
});
|