Refinement-Runde Panel/Wiki: Bedienung, Themes, Archiv-Wahl, Frontend-Tests
- Slug transliteriert Umlaute/ß (fuer statt f-r); aktiver Tab mit Akzent + Unterstrich; kein aktiver Tab bei zugeklapptem Panel (mode.clear) - Suche: Live-Update auch beim Löschen (<3 Zeichen: Hinweis), #tag-Filter in der Query, Treffer öffnen im Dokument-Tab (panel_load, ohne Frontmatter); Wiki-Sprung-Button im Dokument; Wiki-Fehler sichtbar als Toast - Archiv-Formular am Archiv-Button (Ordner/Beschreibung/Schlagwörter); panel_archive_cmd nimmt Meta entgegen - Wiki-Übersicht: Zuletzt-Sektion, Backlink-Zähler, Wurzel-Eyebrow - Themes: Panel-Farben über CSS-Variablen, applyTheme leitet sie aus dem jeweiligen Theme ab (auch helle); gemeinsames themes.ts - Fenster: Doppelklick maximiert; gelöstes Fenster merkt Größe/Position (Window-ACL: set-position/outer-position/inner-size ergänzt); open_panel_window async (GTK-Deadlock-Vorgabe); Panel-Fenster spiegelt JS-Fehler ins Dev-Log - Archiv pro Projekt wähl-/abwählbar (Einstellungsdialog, setzt/entfernt Permissions); require_archive_home legt den Ordner an; ohne Archiv nur Befehle+Dokument im Panel - Frontend-Tests: vitest + happy-dom, 27 Tests über Modus-, Such-, Wiki-, Editor- und Formular-Logik (npm test)
This commit is contained in:
@@ -20,8 +20,11 @@ pub(crate) fn project_archive_home(project: &str) -> Option<PathBuf> {
|
||||
}
|
||||
|
||||
/// Archiv-Home als Result — die gemeinsame Vorbedingung aller Archiv-Tools.
|
||||
/// Legt den konfigurierten Ordner an, falls er (noch) nicht existiert.
|
||||
pub(crate) fn require_archive_home(project: &str) -> Result<PathBuf, String> {
|
||||
project_archive_home(project).ok_or_else(|| "kein Archiv-Ordner gesetzt".into())
|
||||
let home = project_archive_home(project).ok_or("kein Archiv-Ordner gesetzt")?;
|
||||
fs::create_dir_all(&home).map_err(|e| format!("{}: {e}", home.display()))?;
|
||||
Ok(home)
|
||||
}
|
||||
|
||||
/// Setzt das Archiv-Home: ai-control.json (~-relativ) und ein Eintrag in
|
||||
@@ -87,6 +90,42 @@ fn add_archive_permission(paths: &Paths, project: &str, dir: &str) -> Result<(),
|
||||
fs::write(&sp, raw + "\n").map_err(|e| format!("{}: {e}", sp.display()))
|
||||
}
|
||||
|
||||
/// Wählt das Archiv ab: archiveHome aus der ai-control.json entfernen und die
|
||||
/// beim Setzen eingetragenen Rechte (additionalDirectories + Edit-Allow) aus
|
||||
/// der Projekt-settings.json zurücknehmen. Der Ordner selbst bleibt liegen.
|
||||
pub(crate) fn clear_project_archive_home(project: &str) -> Result<(), String> {
|
||||
let paths = Paths::real();
|
||||
let mut cfg = read_project_config_in(&paths, project)?;
|
||||
let Some(dir) = cfg.archive_home.take() else {
|
||||
return Ok(());
|
||||
};
|
||||
write_project_config_in(&paths, project, &cfg)?;
|
||||
remove_archive_permission(&paths, project, &dir)
|
||||
}
|
||||
|
||||
/// Gegenstück zu add_archive_permission.
|
||||
fn remove_archive_permission(paths: &Paths, project: &str, dir: &str) -> Result<(), String> {
|
||||
let sp = settings_path(&project_dir(paths, project)?);
|
||||
if !sp.is_file() {
|
||||
return Ok(());
|
||||
}
|
||||
let mut v: serde_json::Value =
|
||||
serde_json::from_str(&fs::read_to_string(&sp).map_err(|e| format!("{}: {e}", sp.display()))?)
|
||||
.map_err(|e| format!("{}: {e}", sp.display()))?;
|
||||
let Some(perms) = v.get_mut("permissions").and_then(|p| p.as_object_mut()) else {
|
||||
return Ok(());
|
||||
};
|
||||
if let Some(dirs) = perms.get_mut("additionalDirectories").and_then(|d| d.as_array_mut()) {
|
||||
dirs.retain(|d| d.as_str() != Some(dir));
|
||||
}
|
||||
let edit = format!("Edit({dir}/**)");
|
||||
if let Some(allow) = perms.get_mut("allow").and_then(|a| a.as_array_mut()) {
|
||||
allow.retain(|p| p.as_str() != Some(&edit));
|
||||
}
|
||||
let raw = serde_json::to_string_pretty(&v).map_err(|e| e.to_string())?;
|
||||
fs::write(&sp, raw + "\n").map_err(|e| format!("{}: {e}", sp.display()))
|
||||
}
|
||||
|
||||
/// Metadaten beim Archivieren: Unterordner im Archiv-Home plus Frontmatter-Felder.
|
||||
#[derive(Default)]
|
||||
pub(crate) struct ArchiveMeta {
|
||||
@@ -252,10 +291,15 @@ fn first_line(text: &str) -> String {
|
||||
pub(crate) fn slugify(s: &str) -> String {
|
||||
let mut out = String::new();
|
||||
for c in s.chars() {
|
||||
if c.is_ascii_alphanumeric() {
|
||||
out.push(c.to_ascii_lowercase());
|
||||
} else if !out.ends_with('-') {
|
||||
out.push('-');
|
||||
// Umlaute/ß transliterieren statt verschlucken („für's" → fuer-s).
|
||||
match c {
|
||||
'ä' | 'Ä' => out.push_str("ae"),
|
||||
'ö' | 'Ö' => out.push_str("oe"),
|
||||
'ü' | 'Ü' => out.push_str("ue"),
|
||||
'ß' => out.push_str("ss"),
|
||||
_ if c.is_ascii_alphanumeric() => out.push(c.to_ascii_lowercase()),
|
||||
_ if !out.ends_with('-') => out.push('-'),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
let out: String = out.trim_matches('-').chars().take(60).collect();
|
||||
@@ -314,6 +358,8 @@ mod tests {
|
||||
assert_eq!(slugify("ADR: Logging vereinheitlichen"), "adr-logging-vereinheitlichen");
|
||||
assert_eq!(slugify(" "), "entwurf");
|
||||
assert_eq!(slugify("###"), "entwurf");
|
||||
assert_eq!(slugify("Ein Test für's Archiv"), "ein-test-fuer-s-archiv");
|
||||
assert_eq!(slugify("Größe ÄÖÜ"), "groesse-aeoeue");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user