Technische ID als Identität der Notizen; Pfad wird zur Eigenschaft

Bisher war der Dateipfad die Identität einer Notiz: Umbenennen brach
Verweise, und der Pfad tauchte in der Oberfläche auf (Tooltips, Menüpunkte
"Dateiname ändern"/"Ordnerpfad ändern"). Jede Notiz trägt jetzt eine
technische ID im Frontmatter (id: <uuid>), die nie wechselt; der
Systempfad ist nur noch eine Eigenschaft.

- ensure_ids ergänzt fehlende IDs beim Seitenaufbau (wie ensure_node_texts);
  frontmatter() schreibt sie bei jedem Anlegen als erste Zeile.
- Doc, WikiDocEntry, FolderNode und Suchtreffer führen die ID mit;
  resolve_id löst sie auf den aktuellen relpath auf.
- Commands sprechen IDs: archive_read/write/set_title/delete nehmen `id`,
  archive_create_doc/create_folder die ID des Elternknotens plus Namen,
  panel_archive_cmd den Zielknoten als ID.
- Frontend adressiert durchgehend über IDs: Auswahl, Klapp-Zustand,
  Verlauf des Zurück-Knopfs, Wikilink-Sprünge, Suchtreffer-Vormerkung und
  der Zielordner-Baum des Archiv-Dialogs.
- Der Pfad ist aus der Oberfläche verschwunden: die Menüpunkte zum
  Umbenennen von Datei und Ordner samt Commands (archive_rename,
  archive_move, archive_move_folder) sind entfallen — es gibt nur noch den
  Titel, Name und Titel können nicht mehr auseinanderlaufen.

Offen dadurch: Verschieben im Baum fehlt (ToDo: Drag & Drop).

Archiv-Dialog nebenbei überarbeitet: modales Popup im Design der
Archiv-Ansicht mit Abbrechen und zuverlässigem Escape, Zielordner über
einen einklappbaren Baum statt Pfadeingabe, zusätzlicher Knopf "Auf Platte
legen" (panel_save_as); Archivieren öffnet keinen Dateimanager mehr.
This commit is contained in:
marcus hinz
2026-07-26 17:53:19 +02:00
parent 9de58d43e8
commit 2251a1d45f
26 changed files with 984 additions and 403 deletions
+86 -32
View File
@@ -527,6 +527,7 @@ pub fn wiki_open(project: String, name: String) -> Result<(), String> {
Some(tag) => {
let display = crate::domain::project::display_name_in(&Paths::real(), &project)?;
crate::domain::archive_ops::ensure_node_texts(&home, &display)?;
crate::domain::archive_ops::ensure_ids(&home)?;
let json = serde_json::to_string(&crate::domain::archive_index::archive_page(
&home,
(!tag.is_empty()).then_some(tag),
@@ -549,8 +550,9 @@ pub fn wiki_open(project: String, name: String) -> Result<(), String> {
/// Liest den Body eines Archiv-Dokuments (ohne Frontmatter) — Inhalt der
/// Notiz-Ansicht im Archiv-Tab.
#[tauri::command]
pub fn archive_read(project: String, relpath: String) -> Result<String, String> {
pub fn archive_read(project: String, id: String) -> Result<String, String> {
let home = crate::domain::archive::require_archive_home(&project)?;
let relpath = crate::domain::archive_index::resolve_id(&home, &id)?;
let path = crate::domain::archive_ops::doc_path(&home, &relpath)?;
let text = std::fs::read_to_string(&path).map_err(|e| format!("{}: {e}", path.display()))?;
Ok(crate::domain::archive::strip_frontmatter(&text).to_string())
@@ -559,19 +561,60 @@ pub fn archive_read(project: String, relpath: String) -> Result<String, String>
/// Schreibt den Body einer Archiv-Notiz zurück (Bearbeiten im
/// Archiv-Fenster); die Frontmatter der Datei bleibt.
#[tauri::command]
pub fn archive_write(project: String, relpath: String, text: String) -> Result<(), String> {
pub fn archive_write(project: String, id: String, text: String) -> Result<(), String> {
let home = crate::domain::archive::require_archive_home(&project)?;
let relpath = crate::domain::archive_index::resolve_id(&home, &id)?;
let path = crate::domain::archive_ops::doc_path(&home, &relpath)?;
crate::domain::archive_ops::write_body(&path, &text)
}
/// Ordner-Knoten des Archivs (Pfad + Titel, sortiert) — Zielordner-Baum des
/// Archiv-Dialogs.
#[tauri::command]
pub fn archive_folders(
project: String,
) -> Result<Vec<crate::domain::archive_index::FolderNode>, String> {
let home = crate::domain::archive::require_archive_home(&project)?;
crate::domain::archive_index::folder_nodes(&home)
}
/// Legt den Entwurf als Datei an einem frei gewählten Pfad ab (Speichern-
/// Dialog) — ohne Archiv, ohne Frontmatter: der Panel-Inhalt, wie er ist.
#[tauri::command]
pub fn panel_save_as(project: String, path: String) -> Result<(), String> {
let text = std::fs::read_to_string(panel_file(&project)).map_err(|e| e.to_string())?;
std::fs::write(&path, text).map_err(|e| format!("{path}: {e}"))
}
/// Setzt den Anzeige-Titel einer Notiz (Klick auf den Titel im Archiv);
/// danach die frische Übersicht, damit Baum und Karten den neuen Titel zeigen.
#[tauri::command]
pub fn archive_set_title(project: String, relpath: String, title: String) -> Result<(), String> {
pub fn archive_set_title(project: String, id: String, title: String) -> Result<(), String> {
let home = crate::domain::archive::require_archive_home(&project)?;
let relpath = crate::domain::archive_index::resolve_id(&home, &id)?;
let path = crate::domain::archive_ops::doc_path(&home, &relpath)?;
crate::domain::archive_ops::set_title(&path, &title)?;
// Der technische Name folgt dem Titel — beide dürfen nicht auseinander
// laufen. Bei einem Knotentext wandert der gleichnamige Ordner mit; die
// Archiv-Wurzel (index.md) behält ihren Namen, sie ist die Konvention.
let stem = path.file_stem().unwrap_or_default().to_string_lossy().to_string();
let name = crate::domain::archive::strip_stamp(&stem).to_string();
let slug = crate::domain::archive::slugify(&title);
if name != slug && name != "index" {
let dir = path.with_file_name(&name);
if dir.is_dir() {
let old_rel = dir.strip_prefix(&home).unwrap().display().to_string();
let new_rel = match old_rel.rsplit_once('/') {
Some((parent, _)) => format!("{parent}/{slug}"),
None => slug.clone(),
};
crate::domain::archive_ops::move_folder(&home, &old_rel, &new_rel)?;
relink_folder(&project, &home.join(&old_rel), &home.join(&new_rel))?;
} else {
let new_rel = crate::domain::archive_ops::rename_doc(&home, &relpath, &title)?;
relink(&project, &path, Some(&home.join(new_rel)))?;
}
}
wiki_refresh_page(&project, &home)
}
@@ -580,57 +623,66 @@ pub fn archive_set_title(project: String, relpath: String, title: String) -> Res
fn wiki_refresh_page(project: &str, home: &std::path::Path) -> Result<(), String> {
let display = crate::domain::project::display_name_in(&Paths::real(), project)?;
crate::domain::archive_ops::ensure_node_texts(home, &display)?;
crate::domain::archive_ops::ensure_ids(home)?;
let json = serde_json::to_string(&crate::domain::archive_index::archive_page(home, None)?)
.map_err(|e| e.to_string())?;
std::fs::write(wiki_file(project), json).map_err(|e| e.to_string())
}
/// Verschiebt ein Archiv-Dokument in einen anderen Ordner (leer = Wurzel).
#[tauri::command]
pub fn archive_move(project: String, relpath: String, folder: String) -> Result<(), String> {
let home = crate::domain::archive::require_archive_home(&project)?;
let old = home.join(&relpath);
let new_rel = crate::domain::archive_ops::move_doc(&home, &relpath, &folder)?;
relink(&project, &old, Some(&home.join(new_rel)))?;
wiki_refresh_page(&project, &home)
}
/// Benennt ein Archiv-Dokument um (Slug hinter erhaltenem Zeitstempel).
#[tauri::command]
pub fn archive_rename(project: String, relpath: String, name: String) -> Result<(), String> {
let home = crate::domain::archive::require_archive_home(&project)?;
let old = home.join(&relpath);
let new_rel = crate::domain::archive_ops::rename_doc(&home, &relpath, &name)?;
relink(&project, &old, Some(&home.join(new_rel)))?;
wiki_refresh_page(&project, &home)
}
/// Löscht ein Archiv-Dokument; danach zeigt das Wiki die Übersicht.
#[tauri::command]
pub fn archive_delete(project: String, relpath: String) -> Result<(), String> {
pub fn archive_delete(project: String, id: String) -> Result<(), String> {
let home = crate::domain::archive::require_archive_home(&project)?;
let relpath = crate::domain::archive_index::resolve_id(&home, &id)?;
crate::domain::archive_ops::delete_doc(&home, &relpath)?;
relink(&project, &home.join(&relpath), None)?;
wiki_refresh_page(&project, &home)
}
/// Verschiebt/benennt einen Archiv-Ordner um; danach Übersicht.
#[tauri::command]
pub fn archive_move_folder(project: String, folder: String, to: String) -> Result<(), String> {
let home = crate::domain::archive::require_archive_home(&project)?;
crate::domain::archive_ops::move_folder(&home, &folder, &to)?;
relink_folder(&project, &home.join(&folder), &home.join(&to))?;
wiki_refresh_page(&project, &home)
}
/// Legt einen Ordner im Archiv an (Plus im Baum); danach Übersicht.
#[tauri::command]
pub fn archive_create_folder(project: String, folder: String) -> Result<(), String> {
pub fn archive_create_folder(project: String, parent: String, name: String) -> Result<(), String> {
let home = crate::domain::archive::require_archive_home(&project)?;
let folder = join_under(&home, &parent, &name)?;
crate::domain::archive_ops::create_folder(&home, &folder)?;
let display = crate::domain::project::display_name_in(&Paths::real(), &project)?;
crate::domain::archive_ops::ensure_node_texts(&home, &display)?;
crate::domain::archive_ops::ensure_ids(&home)?;
wiki_refresh_page(&project, &home)
}
/// Pfad eines neuen Kindes unterhalb des Knotens `parent` (ID; leer =
/// Wurzel) — der Name wird als Slug angehängt.
fn join_under(
home: &std::path::Path,
parent: &str,
name: &str,
) -> Result<String, String> {
let name = name.trim();
if name.is_empty() {
return Err("Name fehlt".into());
}
let slug = crate::domain::archive::slugify(name);
if parent.is_empty() {
return Ok(slug);
}
// Der Zielordner ist der Ordner neben dem Knotentext des Elternteils.
let rel = crate::domain::archive_index::resolve_id(home, parent)?;
let stem = std::path::Path::new(&rel)
.file_stem()
.unwrap_or_default()
.to_string_lossy()
.to_string();
let dir = match rel.rsplit_once('/') {
Some((head, _)) => format!("{head}/{stem}"),
None => stem,
};
Ok(format!("{dir}/{slug}"))
}
/// Legt ein leeres Dokument an (Plus im Listenkopf) und öffnet es im
/// Dokument-Tab: leerer Dokument-Puffer plus Quell-Verknüpfung — das
/// Getippte landet über `panel_set` in der Archiv-Datei. Die Übersicht
@@ -638,9 +690,11 @@ pub fn archive_create_folder(project: String, folder: String) -> Result<(), Stri
/// frisch (zwei konkurrierende Puffer-Events würden sonst um den aktiven Tab
/// rennen).
#[tauri::command]
pub fn archive_create_doc(project: String, folder: String, name: String) -> Result<(), String> {
pub fn archive_create_doc(project: String, parent: String, name: String) -> Result<(), String> {
let home = crate::domain::archive::require_archive_home(&project)?;
let display = crate::domain::project::display_name_in(&Paths::real(), &project)?;
let rel_new = join_under(&home, &parent, &name)?;
let folder = rel_new.rsplit_once('/').map(|(h, _)| h.to_string()).unwrap_or_default();
let rel = crate::domain::archive_ops::create_doc(&home, &folder, &name, &display)?;
let path = home.join(rel);
std::fs::write(panel_file(&project), "").map_err(|e| e.to_string())?;