diff --git a/AKTENPLAN.md b/AKTENPLAN.md new file mode 100644 index 0000000000..a629f3c5a2 --- /dev/null +++ b/AKTENPLAN.md @@ -0,0 +1,233 @@ +# Aktenplan — Typen, Schutz und FolderViews + +## Hierarchie + +``` +Space (Typ: aktenplan) +└── Aktenplan-Ebenen (protected, beliebig tief verschachtelt) + └── Aktenschrank (shielded = letztes Blatt des Aktenplans) + └── Akte (der "Leitzordner") + ├── Variante leer: Dokumente direkt in der Akte + ├── Variante thematisiert: Vorgang → Dokumente + └── Variante voll: Vorgang → Register → Dokumente +``` + +## Typen (4 Stück, je ein FolderView) + +| Typ | `.type_` | FolderView | Kinder | Schutz | +|-----|----------|-----------|--------|--------| +| **Aktenplan** | `.type_aktenplan` | Sachgruppen-Listing | Bei protected: aktenplan. Bei shielded: akte | protected / shielded | +| **Akte** | `.type_akte` | Akten-Ansicht | vorgang, dokument | — | +| **Vorgang** | `.type_vorgang` | Vorgangs-Ansicht | register, dokument | — | +| **Register** | `.type_register` | Register-Ansicht | dokument | — | + +### Aktenplan: Zwei Modi über immutableState + +Gleicher Typ `.type_aktenplan`, unterschiedliches Verhalten je nach Schutzstatus: + +| immutableState | Bedeutung | Erlaubte Kinder | Actions | +|---------------|-----------|----------------|---------| +| `protected` | Sachgruppe (Struktur fixiert) | aktenplan (nur Manager) | "Neue Sachgruppe" (Manager) | +| `shielded` | Aktenschrank (letztes Blatt) | akte | "Neue Akte" (Editor+) | +| keiner | Ungeschützter Aktenplan-Ordner | aktenplan, akte | "Neue Sachgruppe", "Neue Akte" | + +## Aktencode-Syntax + +Jeder Ordnername: **` `** + +``` +Ebene Code Trennzeichen Beispiel +───────────────────────────────────────────────────────────── +Sachgruppe 1 11 (Startcode) 11 Innere Verwaltung +Sachgruppe 2 11.12 . (Punkt) 11.12 Kommunalverwaltung +Sachgruppe n 11.12.01 . (Punkt) 11.12.01 Organisationsang. +Aktenschrank 11.12.01.03 . (Punkt) 11.12.01.03 Satzungen +Akte 11.12.01.03-01 - (Bindestrich) 11.12.01.03-01 Entschädigungssatzung +Vorgang 11.12.01.03-01/1 / (Schrägstrich) 11.12.01.03-01/1 Fassung 2016 +Register 11.12.01.03-01/1#1 # (Raute) 11.12.01.03-01/1#1 Vorlagen +``` + +## FolderViews und Schema-Dateien + +Jeder Typ hat eine Schema-Datei unter `.space/views/` und einen zugehörigen FolderView. + +### .space/views/aktenplan.json +```json +{ + "label": "Aktenplan", + "icon": "archive", + "children": { + "protected": ["aktenplan"], + "shielded": ["akte"], + "default": ["aktenplan", "akte"] + }, + "columns": ["name", "aktencode", "typ", "anzahl"], + "namePattern": "{parentCode}.{seq:2} {title}", + "metadata": { + "aktencode": { "label": "Aktencode", "type": "string", "auto": true } + } +} +``` + +**FolderView Aktenplan** zeigt: +- Spalten: Name, Aktencode, Untertyp (Sachgruppe/Aktenschrank), Anzahl Kinder +- Action-Button: "Neue Sachgruppe" (wenn protected, nur Manager) oder "Neue Akte" (wenn shielded) +- Icon: Ordner mit Schild (protected) oder offener Ordner (shielded) + +### .space/views/akte.json +```json +{ + "label": "Akte", + "icon": "folder-open", + "children": ["vorgang"], + "columns": ["name", "aktencode", "status", "abgelegt-von", "abgelegt-am"], + "namePattern": "{parentCode}-{seq:2} {title}", + "metadata": { + "aktencode": { "label": "Aktenzeichen", "type": "string", "auto": true }, + "status": { + "label": "Status", "type": "enum", + "values": ["offen", "gespeichert", "geschlossen"], + "default": "offen" + } + } +} +``` + +**FolderView Akte** zeigt: +- Spalten: Name, Aktenzeichen, Status, abgelegt von/am +- Action-Button: "Neuer Vorgang", "Dokument hinzufügen" +- Status-Badge (offen/gespeichert/geschlossen) + +### .space/views/vorgang.json +```json +{ + "label": "Vorgang", + "icon": "file-list", + "children": ["register"], + "columns": ["name", "aktencode", "version", "abgelegt-von", "abgelegt-am"], + "namePattern": "{parentCode}/{seq} {title}", + "metadata": { + "aktencode": { "label": "Aktenzeichen", "type": "string", "auto": true }, + "version": { "label": "Version", "type": "string" } + } +} +``` + +**FolderView Vorgang** zeigt: +- Spalten: Name, Aktenzeichen, Version, abgelegt von/am +- Action-Button: "Neues Register", "Dokument hinzufügen" + +### .space/views/register.json +```json +{ + "label": "Register", + "icon": "bookmark", + "children": [], + "columns": ["name", "aktencode", "abgelegt-von", "abgelegt-am"], + "namePattern": "{parentCode}#{seq} {title}", + "metadata": { + "aktencode": { "label": "Aktenzeichen", "type": "string", "auto": true } + } +} +``` + +**FolderView Register** zeigt: +- Spalten: Name, Aktenzeichen, abgelegt von/am +- Action-Button: "Dokument hinzufügen" (kein weiterer Ordner-Typ) + +## Action-Buttons + +Jeder FolderView hat typ-spezifische Action-Buttons im AppBar: + +``` +┌─────────────────────────────────────────────────────────┐ +│ ← 11.12.01.03 Satzungen │ +│ │ +│ [+ Neue Akte] Filter Ansicht │ +│ │ +│ Name Aktenzeichen Status Datum │ +│ ─────────────────────────────────────────────────────── │ +│ 📁 11.12.01.03-01 Entschädigungssatzung offen ... │ +│ 📁 11.12.01.03-02 Feuerwehrsatzung gesp. ... │ +└─────────────────────────────────────────────────────────┘ +``` + +### Action-Button Logik + +1. FolderView liest `children` aus Schema +2. Für jeden erlaubten Kind-Typ: lade dessen Schema (Label, Icon) +3. Zeige Action-Button: "Neue(r/s) {label}" +4. Click → Dialog: Titel eingeben → Aktencode wird automatisch generiert +5. Erstellt: Ordner + `.type_` + initiale Metadaten + +### Aktencode-Generierung im Dialog + +``` +┌─────────────────────────────────────────┐ +│ Neue Akte anlegen │ +│ │ +│ Aktenzeichen: 11.12.01.03-03 │ +│ (automatisch, nächste freie Nummer) │ +│ │ +│ Titel: [Brandverhütungsschauordnung ] │ +│ │ +│ Ordnername: 11.12.01.03-03 Brandver... │ +│ │ +│ [Abbrechen] [Anlegen] │ +└─────────────────────────────────────────┘ +``` + +## Beispiel: Komplette Struktur + +``` +Archikart DMS/ .type_aktenplan +├── .space/views/{aktenplan,akte,vorgang,register}.json +├── 11 Innere Verwaltung/ .type_aktenplan protected +│ ├── 11.12 Kommunalverwaltung/ .type_aktenplan protected +│ │ ├── 11.12.01 Organisationsangelegenheiten/ .type_aktenplan protected +│ │ │ ├── 11.12.01.03 Satzungen/ .type_aktenplan shielded (Aktenschrank) +│ │ │ │ ├── 11.12.01.03-01 Entschädigungssatzung/ .type_akte +│ │ │ │ │ ├── 11.12.01.03-01/1 Fassung 2016/ .type_vorgang +│ │ │ │ │ │ ├── 11.12.01.03-01/1#1 Vorlagen/ .type_register +│ │ │ │ │ │ │ ├── Vergleich_A.pdf +│ │ │ │ │ │ │ └── Arbeitshilfe.docx +│ │ │ │ │ │ └── Beschluss.pdf +│ │ │ │ │ └── 11.12.01.03-01/2 Fassung 2026/ .type_vorgang +│ │ │ │ └── 11.12.01.03-02 Feuerwehrsatzung/ .type_akte +│ │ │ └── 11.12.01.05 Landratsamt/ .type_aktenplan shielded +│ │ └── 11.12.02 Personalangelegenheiten/ .type_aktenplan protected +│ └── 11.13 Finanzverwaltung/ .type_aktenplan protected +├── 12 Sicherheit und Ordnung/ .type_aktenplan protected +└── 21 Schulträgeraufgaben/ .type_aktenplan protected +``` + +## Implementierung + +### Pro Typ ein FolderView + +Jeder der 4 Typen bekommt eine eigene Vue-Komponente: + +``` +packages/web-app-files/src/components/TypedViews/ + AktenplanView.vue ← Sachgruppen/Aktenschrank-Listing + AkteView.vue ← Akten-Ansicht mit Status + VorgangView.vue ← Vorgangs-Listing mit Version + RegisterView.vue ← Register-Listing (Blatt-Ebene) + TypedNewDialog.vue ← "Neuer [Typ]" Dialog mit Aktencode-Generator +``` + +### Integration in GenericSpace.vue + +``` +PROPFIND → Kinder-Liste + │ + ├── .type_* gefunden? + │ ├── Nein → Standard FolderView + │ └── Ja → Typ erkennen + │ ├── immutableState prüfen (protected/shielded) + │ ├── Schema laden (.space/views/.json) + │ └── Typed FolderView rendern + │ ├── Typ-spezifische Spalten + │ ├── Typ-spezifische Action-Buttons + │ └── Typ-spezifisches Icon/Styling +``` diff --git a/FOLDERVIEWPLUS.md b/FOLDERVIEWPLUS.md new file mode 100644 index 0000000000..93caa62efd --- /dev/null +++ b/FOLDERVIEWPLUS.md @@ -0,0 +1,255 @@ +# FolderViewPlus — Architektur + +## Übersicht + +FolderViewPlus erweitert OpenCloud Web um **typisierte Ordneransichten**. Ordner bekommen +einen Typ der bestimmt: welche Kinder erlaubt sind, welche Spalten angezeigt werden, +welche Aktionen verfügbar sind und welche Metadaten gepflegt werden. + +Designed für **DMS/Aktenplan-Szenarien** (z.B. WINYARD/Archikart), aber generisch nutzbar +für jede hierarchische Strukturierung. + +**Wichtig: FolderViewPlus ist ein reiner UI-Ansatz.** Auf WebDAV- oder Reva-Ebene wird +nichts verhindert oder erzwungen. Es gibt keine serverseitige Validierung der Typ-Hierarchie. +Ein User kann per WebDAV oder CLI weiterhin beliebige Ordner anlegen, verschieben oder +umbenennen. FolderViewPlus verbessert die **Lesbarkeit, Strukturierbarkeit und +Prozessunterstützung** im Web UI — es macht Ordnerstrukturen verständlicher und leitet +User durch definierte Workflows, ohne die Flexibilität des Dateisystems einzuschränken. + +## Kernprinzip: Dateisystem-native Typisierung + +``` +Projekt-Space/ +├── .type_aktenplan ← Space-Root ist ein Aktenplan +├── .space/ +│ └── views/ +│ ├── aktenplan.json ← Schema: was darf auf Root-Ebene? +│ ├── sachgruppe.json +│ ├── akte.json +│ ├── register.json +│ └── vorgang.json +├── 11 Innere Verwaltung/ +│ ├── .type_sachgruppe +│ └── 11.12 Kommunalverwaltung/ +│ ├── .type_sachgruppe +│ └── 11.12.01.03 Satzungen/ +│ ├── .type_akte +│ ├── 11.12.01.03-01/ +│ │ ├── .type_vorgang +│ │ ├── Bescheid.pdf +│ │ └── Antrag.docx +│ └── 11.12.01.03-02/ +│ ├── .type_vorgang +│ └── Entwurf.odt +└── 12 Sicherheit und Ordnung/ + └── .type_sachgruppe +``` + +### Warum `.type_*` statt xattr? + +| Kriterium | xattr | .type_ Datei | +|-----------|-------|-------------| +| Nativ sichtbar | Nein (braucht getfattr) | Ja (ls -a) | +| NFS/SMB kompatibel | Teilweise (Attribut-Support variiert) | Ja | +| Desktop-Client | Braucht spezielle API | Sieht Datei direkt | +| Backup-sicher | Oft verloren | Immer dabei | +| CLI-Zugriff | `getfattr -n user.oc.md.type` | `ls .type_*` | +| Setzen | `setfattr` oder API | `touch .type_akte` | +| Suche | Nicht durchsuchbar | `find . -name ".type_*"` | +| Performance | Extra xattr-Read | Kommt im PROPFIND-Listing gratis mit | + +## Schichten + +``` +┌─────────────────────────────────────────────────────────┐ +│ FolderView Handler (generisch oder spezifisch) │ +│ → rendert Spalten, Actions, Metadaten pro Typ │ +├─────────────────────────────────────────────────────────┤ +│ Schema-Loader (useTypedFolderSchema) │ +│ → lädt .space/views/.json, cacht pro Space │ +├─────────────────────────────────────────────────────────┤ +│ Typ-Erkennung (useTypedFolderDetect) │ +│ → erkennt .type_* in PROPFIND-Listing │ +├─────────────────────────────────────────────────────────┤ +│ GenericSpace.vue │ +│ → entscheidet: normaler View oder Typed View │ +├─────────────────────────────────────────────────────────┤ +│ OpenCloud Web Runtime │ +│ → PROPFIND, Extension Points, Module Federation │ +└─────────────────────────────────────────────────────────┘ +``` + +## Schema (`.json`) + +Jeder Typ wird durch eine JSON-Datei in `.space/views/` beschrieben: + +```json +{ + "label": "Akte", + "icon": "folder-archive", + "children": ["register", "vorgang"], + "columns": [ + { "key": "name", "label": "Name" }, + { "key": "aktz", "label": "Aktenzeichen", "source": "metadata" }, + { "key": "version", "label": "Version", "source": "metadata" }, + { "key": "status", "label": "Status", "source": "metadata" }, + { "key": "mdate", "label": "Letzte Änderung" } + ], + "namePattern": "{parentAktz}-{seq}", + "metadata": { + "aktz": { "label": "Aktenzeichen", "type": "string", "auto": true }, + "status": { + "label": "Status", + "type": "enum", + "values": ["offen", "gespeichert", "geschlossen"], + "default": "offen" + } + } +} +``` + +### Felder + +| Feld | Beschreibung | +|------|-------------| +| `label` | Anzeigename des Typs | +| `icon` | RemixIcon-Name für Ordner dieses Typs | +| `children` | Erlaubte Kind-Typen (für "Neu"-Button) | +| `columns` | Spalten in der Tabellenansicht | +| `namePattern` | Auto-Name bei Neuerstellung (optional) | +| `metadata` | Typ-spezifische Metadaten-Felder (in Sidebar editierbar) | + +### Spalten-Source + +- Kein `source` oder `source: "resource"` → Standard-Resource-Feld (name, mdate, size) +- `source: "metadata"` → Wert aus `user.oc.md.` xattr (via Metadata API) +- `source: "type"` → Abgeleitet aus `.type_*` des Kindes + +## Drei Ebenen der Anpassung + +``` +1. Typ-Schema (.space/views/.json) + → Gilt für ALLE Ordner dieses Typs im Space + → Spalten, erlaubte Kinder, Metadaten-Felder + +2. .special/ (pro Ordner, optional, selten) + → Individuelle Anpassung EINES Ordners + → Override-Spalten, eigenes Icon, Hilfsdaten + +3. FolderView Handler (views//) + → Komplett eigene Render-Logik + → Module Federation Extension + → Für Spezialfälle (Aktenzeichen-Generator, Formular-View) +``` + +## Flow: Ordner öffnen + +``` +User klickt Ordner + │ + ▼ +PROPFIND (normal, wie bisher) + │ + ▼ +Kinder-Liste durchsuchen + │ + ├── .type_* gefunden? + │ │ + │ ├── Nein → Normaler FolderView (Standard OpenCloud) + │ │ + │ └── Ja → Typ = Dateiname nach ".type_" + │ │ + │ ▼ + │ .space/views/.json geladen? (Cache) + │ │ + │ ├── Nein → Laden via WebDAV getFileContents + │ │ + │ └── Ja → Schema anwenden + │ │ + │ ▼ + │ Typed FolderView rendern + │ - Spalten aus schema.columns + │ - "Neu"-Button: schema.children + │ - Icon: schema.icon + │ - Metadaten: schema.metadata + │ + └── .special/ gefunden? + │ + ▼ + .special/view.json laden (Override) +``` + +## Flow: Neuen typisierten Ordner anlegen + +``` +User klickt "Neuer Vorgang" + │ + ▼ +Name-Dialog (ggf. auto aus namePattern) + │ + ▼ +WebDAV: createFolder("11.12.01.03-03") + │ + ▼ +WebDAV: putFileContents(".type_vorgang", "") ← leere Datei + │ + ▼ +Optional: Metadata PUT für Initialwerte + │ + ▼ +Listing refresh → neuer Ordner mit Typ sichtbar +``` + +## Flow: Typ ändern (Manager) + +``` +Manager öffnet Sidebar → Dropdown "Typ" + │ + ▼ +Typ-Liste aus .space/views/ (gecacht) + │ + ▼ +Auswahl: "register" + │ + ▼ +WebDAV: deleteFile(".type_akte") ← alten Marker löschen +WebDAV: putFileContents(".type_register", "") ← neuen setzen + │ + ▼ +View aktualisiert sich (anderes Schema) +``` + +## Performance + +- **Kein Extra-Call für Typ-Erkennung**: `.type_*` kommt im normalen PROPFIND-Listing mit +- **Schema-Cache pro Space**: `.space/views/*.json` wird einmal geladen, dann aus Memory +- **Lazy Schema-Load**: Nur der aktuelle Typ wird geladen, nicht alle +- **Cache-Invalidierung**: Per etag auf `.space/views/` oder manueller Refresh + +## Dateien im Web-Repo + +``` +packages/web-app-files/src/ + composables/ + typedFolder/ + types.ts ← TypedFolderSchema, TypedFieldDef + useTypedFolderSchema.ts ← Schema laden + cachen + useTypedFolderDetect.ts ← .type_* aus Listing erkennen + useTypedFolderTypes.ts ← verfügbare Typen aus .space/views/ + useTypedFolderActions.ts ← "Neuer [Kind-Typ]" + Typ setzen + index.ts + components/ + FilesList/ + TypedFolderView.vue ← Generischer Typed View (Tabelle + Actions) + TypedNewDialog.vue ← "Neues [Kind]" Dialog + views/ + spaces/ + GenericSpace.vue ← Integration: Typ erkennen → View switchen +``` + +## Kompatibilität + +- **Untypisierte Spaces**: Keine Änderung, normaler FolderView +- **Untypisierte Ordner in typisiertem Space**: Normaler View (kein `.type_*` = kein Typ) +- **Nativer Zugriff**: `.type_*` Dateien sind sichtbar, Schema lesbar aus `.space/views/` +- **Ältere Clients**: Ignorieren `.type_*` und `.space/` — keine Probleme diff --git a/TASK_typed_folderviews.md b/TASK_typed_folderviews.md new file mode 100644 index 0000000000..7f51dde1fa --- /dev/null +++ b/TASK_typed_folderviews.md @@ -0,0 +1,209 @@ +# TASK: Typed Folder Views (Aktenplan) + +## Ziel + +Hierarchisch typisierte Ordneransichten für OpenCloud. Jeder Ordner kann einen Typ tragen, +der bestimmt welche Kinder erlaubt sind, welche Spalten angezeigt werden und welche Aktionen +verfügbar sind. Die Typisierung muss auch ohne WebDAV/API funktionieren — z.B. bei nativem +Dateisystemzugriff (NFS, SMB, lokaler Mount). + +## Referenz + +- xx1.png: WINYARD DMS Baumansicht + Register-Dialog (Typ-Auswahl, Aktenzeichen) +- xx2.png: Kontextmenü "Neu" mit typspezifischen Kind-Elementen (AK4-Typen) + +## Architektur + +### Typ-Marker: `.type_` Datei + +**Statt xattr** wird der Typ über eine versteckte Marker-Datei im Ordner signalisiert: + +``` +11.12.01 Kommunalverwaltung/ + .type_akte ← leere Datei, markiert Ordner als Typ "akte" + 11.12.01.03-01/ + .type_vorgang + Bescheid.pdf + Antrag.docx + 11.12.01.03-02/ + .type_vorgang + ... +``` + +**Vorteile gegenüber xattr:** +- **Nativ sichtbar**: Jeder Dateisystem-Client (NFS, SMB, Explorer, Finder) sieht den Typ +- **Kein API nötig**: Desktop-Client kann Typ erkennen ohne WebDAV/Graph-API +- **Einfach zu setzen**: `touch .type_akte` — kein spezielles Tool nötig +- **Backup-sicher**: Typ überlebt jeden Backup/Restore-Vorgang (xattrs oft nicht) +- **Grep-bar**: `find . -name ".type_*"` zeigt die gesamte Typ-Hierarchie + +**Konvention:** +- Genau eine `.type_*` Datei pro Ordner (die erste gefundene zählt) +- Keine `.type_*` = untypisierter Ordner → normaler FolderView +- Die Datei ist leer (0 Bytes) oder kann optionale JSON-Daten enthalten (Overrides) + +### Typ-Schema im Space-Root + +``` +.space/ + views/ + aktenplan.json ← Root-Typ des Space + sachgruppe.json + akte.json + register.json + vorgang.json +``` + +**Format `.json`:** +```json +{ + "label": "Akte", + "icon": "folder-archive", + "children": ["register", "vorgang"], + "columns": ["name", "aktz", "version", "status", "abgelegt-von", "abgelegt-am"], + "namePattern": "{parentAktz}-{seq}", + "actions": ["neues-register", "neuer-vorgang", "dokument-hinzufuegen"], + "metadata": { + "aktz": { "label": "Aktenzeichen", "type": "string", "auto": true }, + "status": { "label": "Status", "type": "enum", "values": ["offen", "gespeichert", "geschlossen"] } + } +} +``` + +### `.special/` für individuelle Ordner-Anpassungen + +`.type_*` definiert die **Klasse** des Ordners (gleicher Typ = gleiches Verhalten). +`.special/` bleibt für **individuelle** Anpassungen pro Ordner: + +``` +11.12.01 Kommunalverwaltung/ + .type_sachgruppe + .special/ + icon.svg ← individuelles Icon für diesen Ordner + view.json ← Override: andere Spalten, andere Actions + data.json ← ordnerspezifische Hilfsdaten +``` + +`.special/` ist optional und selten. Die meisten Ordner brauchen nur `.type_*`. + +### Space-Typ + +Der Space-Root hat ebenfalls eine `.type_*` Datei (z.B. `.type_aktenplan`). +**Wenn keine `.type_*` im Space-Root** → normaler OpenCloud FolderView, kein Typed-View-System. +Bestehende Spaces sind nicht betroffen — Opt-in pro Space. + +### Typ-Erkennung im Web UI + +Der Typ wird aus der **PROPFIND-Dateiliste** erkannt — kein zusätzlicher API-Call: + +1. PROPFIND liefert alle Kinder des Ordners (wie bisher) +2. Client sucht in der Liste nach `.type_*` Einträgen +3. Gefunden → `type = name.substring(6)` (nach `.type_`) +4. Lade `.space/views/.json` (gecacht pro Space) +5. Render: Spalten, Actions, Kind-Typen + +**Kein Performance-Impact** — die `.type_*` Datei kommt im normalen Listing mit. + +### Typ-Erkennung für nativen Desktop-Client + +Ein nativer Desktop-Client (z.B. erweiterter Dateimanager) kann: +1. Ordner öffnen → `.type_akte` sehen → Typ erkannt +2. Space-Root `.space/views/akte.json` lesen → Schema bekannt +3. "Neuer Vorgang" anbieten → Ordner erstellen + `touch .type_vorgang` +4. Spalten/Metadaten anzeigen basierend auf Schema + +### Typ setzen/ändern + +**Im Web UI** (Manager+): +- Sidebar Dropdown "Typ" → zeigt verfügbare Typen aus `.space/views/` +- Setzt Typ: alte `.type_*` löschen + neue `.type_` erstellen +- Kein Typ: `.type_*` löschen → normaler FolderView + +**Nativ / CLI:** +```bash +rm .type_*; touch .type_vorgang +``` + +**Beim Anlegen neuer Ordner:** +- "Neuer [Kind-Typ]" Action erstellt Ordner + `.type_` in einem Schritt + +### Flow (Web UI) + +1. User öffnet Ordner → PROPFIND liefert Kinder +2. Client prüft: Gibt es `.type_*` in der Liste? +3. Nein → normaler FolderView +4. Ja → Typ extrahieren, `.space/views/.json` laden (Cache) +5. Render: Spalten aus `columns`, Actions aus `children` +6. "Neu"-Button bietet nur die in `children` definierten Typen an +7. Beim Anlegen: Ordner erstellen + `.type_` anlegen + +### Skelett / Initialisierung + +- Space-Root bekommt `.type_aktenplan` + `.space/views/*.json` aus Vorlage +- Admin-Action "Aktenplan initialisieren" oder Template-Space +- Langfristig: Schema-Editor im UI + +### Deployment + +Typed FolderView Handler unter `views/` (neben `core/` und `apps/`): +``` +/var/lib/opencloud/web/assets/ + core/ ← OpenCloud Web Runtime + apps/ ← Web Extensions (htmlviewer, etc.) + views/ ← Typed FolderView Handler + aktenplan/ + manifest.json + remoteEntry.mjs +``` + +### Generischer vs. Spezifischer Handler + +- **Generischer Handler**: Interpretiert `.json` dynamisch. Reicht für 80% der Fälle. +- **Spezifischer Handler**: Eigene Vue-Komponente für Sonderfälle (z.B. Aktenzeichen-Generator). +- Fallback: Generischer Handler wenn kein spezifischer gefunden. + +## Implementierung (Schritte) + +### Phase 1: Grundgerüst ✅ (teilweise, muss auf .type_ umgestellt werden) +1. ~~Schema-Loader: `useTypedFolderSchema(space, type)` → lädt + cacht `.space/views/.json`~~ +2. ~~Typed Actions: `useTypedFolderActions` → erstellt Kind-Ordner~~ (muss `.type_` statt xattr setzen) +3. ~~GenericSpace.vue Integration~~ (muss von Metadata API auf PROPFIND-Dateiliste umgestellt werden) +4. ~~Typ-Definitionen~~ + +### Phase 1b: Umstellung auf .type_ (aktuell) +5. Typ-Erkennung aus PROPFIND-Dateiliste statt Metadata API +6. `useTypedFolderTypes` — Typ-Liste per PROPFIND `.space/views/` laden +7. Sidebar Dropdown "Typ" → löscht/erstellt `.type_*` Dateien +8. "Neuer [Kind-Typ]" Action → erstellt Ordner + `.type_` + +### Phase 2: Rendering +9. Typed FolderView Komponente: Spalten aus Schema +10. "Neues [Kind-Typ]" Dialog +11. Aktenzeichen-Generierung: `namePattern` + Sequenz-Counter +12. Typ-spezifische Metadaten in Sidebar + +### Phase 3: Views-Deployment +13. Module Federation Extension Handler unter `views/` +14. Extension Point für typed views +15. Admin-UI: Schema-Editor für `.space/views/` + +### Phase 4: Baumansicht +16. Treeview-Sidebar +17. Navigation via Baumstruktur +18. Breadcrumb mit Aktenzeichen-Pfad + +## Offene Fragen + +- **Performance**: `.type_*` kommt im Listing gratis mit — kein Extra-Call +- **Schema-Cache**: Invalidieren per etag auf `.space/views/`? +- **Rechte**: `.space/views/` editieren → Space-Manager +- **Vererbung**: Sub-Space Schema vom Parent erben? +- **Migration**: Bestehende Ordner typisieren → `find . -type d -exec touch {}/.type_default \;` +- **Kollision**: Mehrere `.type_*` im gleichen Ordner? → erste Datei zählt, Warning loggen +- **Versteckte Dateien**: `.type_*` wird im normalen UI ausgeblendet (wie `.space/`) + +## Abhängigkeiten + +- WebDAV createFile / deleteFile für `.type_*` Management +- Metadata API GET+PUT (opencloud#2960) für typ-spezifische Metadaten +- Module Federation Extension SDK 7.x für View-Handler diff --git a/packages/web-app-files/src/components/FilesList/SpecialFolderHeader.vue b/packages/web-app-files/src/components/FilesList/SpecialFolderHeader.vue new file mode 100644 index 0000000000..464dc32148 --- /dev/null +++ b/packages/web-app-files/src/components/FilesList/SpecialFolderHeader.vue @@ -0,0 +1,37 @@ + + + diff --git a/packages/web-app-files/src/components/SideBar/Metadata/MetadataPanel.vue b/packages/web-app-files/src/components/SideBar/Metadata/MetadataPanel.vue new file mode 100644 index 0000000000..9143db1629 --- /dev/null +++ b/packages/web-app-files/src/components/SideBar/Metadata/MetadataPanel.vue @@ -0,0 +1,96 @@ + + + diff --git a/packages/web-app-files/src/composables/actions/files/index.ts b/packages/web-app-files/src/composables/actions/files/index.ts index 1cf249fa41..52a522e63a 100644 --- a/packages/web-app-files/src/composables/actions/files/index.ts +++ b/packages/web-app-files/src/composables/actions/files/index.ts @@ -17,3 +17,4 @@ export * from './useFileActionsRename' export * from './useFileActionsShowShares' export * from './useFileActionsShowDetails' export * from './useFileActionsToggleHideShare' +export * from './useFileActionsImmutable' diff --git a/packages/web-app-files/src/composables/actions/files/useFileActionsImmutable.ts b/packages/web-app-files/src/composables/actions/files/useFileActionsImmutable.ts new file mode 100644 index 0000000000..303600ac6e --- /dev/null +++ b/packages/web-app-files/src/composables/actions/files/useFileActionsImmutable.ts @@ -0,0 +1,162 @@ +import { useGettext } from 'vue3-gettext' +import { FileAction, useMessages, useModals, useResourcesStore, useClientService } from '@opencloud-eu/web-pkg' +import { computed } from 'vue' + +export const useFileActionsImmutable = () => { + const { $gettext } = useGettext() + const clientService = useClientService() + const { showMessage, showErrorMessage } = useMessages() + const { dispatchModal } = useModals() + const resourcesStore = useResourcesStore() + + const resolveNewState = ( + resource: { id: string; parentFolderId?: string }, + explicitState: 'frozen' | 'protected' | undefined + ): 'frozen' | 'protected' | 'shielded' | undefined => { + if (explicitState) return explicitState + // After unprotect: check if current folder (parent) is still protected → shielded + const currentFolder = resourcesStore.currentFolder + if (currentFolder?.immutableState === 'protected' || currentFolder?.immutableState === 'shielded') { + return 'shielded' + } + return undefined + } + + const callImmutableEndpoint = async ( + driveId: string, + resource: { id: string; parentFolderId?: string }, + action: 'freeze' | 'protect', + method: 'POST' | 'DELETE' = 'POST', + newState: 'frozen' | 'protected' | undefined = undefined + ) => { + const httpClient = clientService.httpAuthenticated + const endpoint = action === 'freeze' ? 'freeze' : 'protect' + try { + const response = await httpClient.request({ + method, + url: `/graph/v1beta1/drives/${driveId}/items/${resource.id}/${endpoint}` + }) + if (response.status === 204) { + resourcesStore.updateResourceField({ + id: resource.id, + field: 'immutableState', + value: resolveNewState(resource, newState) + }) + const msg = + action === 'freeze' + ? $gettext('File has been frozen.') + : method === 'POST' + ? $gettext('Folder has been protected.') + : $gettext('Folder protection has been removed.') + showMessage({ title: msg }) + } + } catch (e) { + showErrorMessage({ + title: $gettext('Operation failed'), + errors: [e as Error] + }) + } + } + + const actions = computed((): FileAction[] => [ + // File: normal → leaf → freeze (with confirmation) + { + name: 'freeze-file', + icon: 'leaf', + label: () => $gettext('Freeze file'), + handler: ({ space, resources }) => { + const resource = resources[0] + dispatchModal({ + title: $gettext('Freeze file permanently?'), + confirmText: $gettext('Freeze'), + message: $gettext( + 'Freezing a file is irreversible. The file content cannot be changed or deleted afterwards. Are you sure?' + ), + onConfirm: () => { + callImmutableEndpoint(space.id, resource, 'freeze', 'POST', 'frozen') + } + }) + }, + isVisible: ({ resources }) => { + if (resources.length !== 1) return false + const r = resources[0] + return r.type === 'file' && !r.immutableState + }, + class: 'oc-files-actions-freeze-trigger' + }, + // File: frozen → snowflake (disabled) + { + name: 'frozen-file', + icon: 'snowflake', + label: () => $gettext('File is frozen'), + handler: () => {}, + isVisible: ({ resources }) => { + if (resources.length !== 1) return false + const r = resources[0] + return r.type === 'file' && r.immutableState === 'frozen' + }, + isDisabled: () => true, + disabledTooltip: () => $gettext('This file is permanently frozen and cannot be modified.'), + class: 'oc-files-actions-frozen-indicator' + }, + // File: shielded (inherited from parent) → shield (disabled) + { + name: 'shielded-file', + icon: 'shield', + label: () => $gettext('File is in a protected folder'), + handler: () => {}, + isVisible: ({ resources }) => { + if (resources.length !== 1) return false + const r = resources[0] + return r.type === 'file' && r.immutableState === 'shielded' + }, + isDisabled: () => true, + disabledTooltip: () => $gettext('This file is in a protected folder and cannot be modified.'), + class: 'oc-files-actions-shielded-file-indicator' + }, + // Folder(s): normal/shielded → protect (single + batch) + { + name: 'protect-folder', + icon: 'shield', + label: (options) => + options?.resources?.length > 1 + ? $gettext('Protect %{count} folders', { count: String(options.resources.length) }) + : $gettext('Protect folder'), + handler: ({ space, resources }) => { + for (const r of resources) { + callImmutableEndpoint(space.id, r, 'protect', 'POST', 'protected') + } + }, + isVisible: ({ resources }) => { + if (!resources.length) return false + return resources.every( + (r) => r.type === 'folder' && (!r.immutableState || r.immutableState === 'shielded') + ) + }, + class: 'oc-files-actions-protect-trigger' + }, + // Folder(s): protected → unprotect (single + batch) + { + name: 'unprotect-folder', + icon: 'shield', + label: (options) => + options?.resources?.length > 1 + ? $gettext('Unprotect %{count} folders', { count: String(options.resources.length) }) + : $gettext('Remove protection'), + handler: ({ space, resources }) => { + for (const r of resources) { + callImmutableEndpoint(space.id, r, 'protect', 'DELETE') + } + }, + isVisible: ({ resources }) => { + if (!resources.length) return false + return resources.every( + (r) => r.type === 'folder' && r.immutableState === 'protected' + ) + }, + class: 'oc-files-actions-unprotect-trigger' + } + ]) + + return { actions } +} diff --git a/packages/web-app-files/src/composables/extensions/useFileActions.ts b/packages/web-app-files/src/composables/extensions/useFileActions.ts index be0a962f69..c1045619b5 100644 --- a/packages/web-app-files/src/composables/extensions/useFileActions.ts +++ b/packages/web-app-files/src/composables/extensions/useFileActions.ts @@ -14,6 +14,7 @@ import { useFileActionsDownloadArchive, useFileActionsFavorite, useFileActionsEnableSync, + useFileActionsImmutable, useFileActionsMove, useFileActionsPaste, useFileActionsOpenShortcut, @@ -45,6 +46,14 @@ export const useFileActions = (): ActionExtension[] => { const { actions: setSpaceImageActions } = useSpaceActionsSetImage() const { actions: showDetailsActions } = useFileActionsShowDetails() const { actions: toggleHideShareActions } = useFileActionsToggleHideShare() + const { actions: immutableActions } = useFileActionsImmutable() + + const singleItemActions = unref(immutableActions).filter( + (a) => !a.name.startsWith('protect-folder') && !a.name.startsWith('unprotect-folder') + ) + const batchableActions = unref(immutableActions).filter( + (a) => a.name === 'protect-folder' || a.name === 'unprotect-folder' + ) return [ { @@ -217,6 +226,26 @@ export const useFileActions = (): ActionExtension[] => { ...unref(toggleHideShareActions)[0], category: 'tertiary' } - } + }, + // Immutable: single-item quick actions (freeze, frozen, shielded indicators) + ...singleItemActions.map((action) => ({ + id: `com.github.opencloud-eu.web.files.quick-action.${action.name}`, + extensionPointIds: [quickActionsExtensionPoint.id], + type: 'action' as const, + action + })), + // Immutable: protect/unprotect as quick action + batch action + ...batchableActions.map((action) => ({ + id: `com.github.opencloud-eu.web.files.quick-action.${action.name}`, + extensionPointIds: [quickActionsExtensionPoint.id], + type: 'action' as const, + action + })), + ...batchableActions.map((action) => ({ + id: `com.github.opencloud-eu.web.files.batch-action.${action.name}`, + extensionPointIds: [batchActionsExtensionPoint.id], + type: 'action' as const, + action + })) ] } diff --git a/packages/web-app-files/src/composables/extensions/useFileSideBars.ts b/packages/web-app-files/src/composables/extensions/useFileSideBars.ts index 04e8c14469..0aaa527ad5 100644 --- a/packages/web-app-files/src/composables/extensions/useFileSideBars.ts +++ b/packages/web-app-files/src/composables/extensions/useFileSideBars.ts @@ -28,6 +28,7 @@ import { useGettext } from 'vue3-gettext' import { markRaw, unref } from 'vue' import { fileSideBarExtensionPoint } from '../../extensionPoints' import AudioMetaPanel from '../../components/SideBar/Audio/AudioMetaPanel.vue' +import MetadataPanel from '../../components/SideBar/Metadata/MetadataPanel.vue' import { isEmpty } from 'lodash-es' export const useSideBarPanels = (): SidebarPanelExtension[] => { @@ -177,6 +178,23 @@ export const useSideBarPanels = (): SidebarPanelExtension $gettext('Metadata'), + component: MetadataPanel, + isVisible: ({ items }) => { + if (items?.length !== 1) { + return false + } + return !isProjectSpaceResource(items[0]) + } + } + }, { id: 'com.github.opencloud-eu.web.files.sidebar-panel.actions', type: 'sidebarPanel', diff --git a/packages/web-app-files/src/composables/specialFolder/useSpecialFolderView.ts b/packages/web-app-files/src/composables/specialFolder/useSpecialFolderView.ts new file mode 100644 index 0000000000..c184902a58 --- /dev/null +++ b/packages/web-app-files/src/composables/specialFolder/useSpecialFolderView.ts @@ -0,0 +1,91 @@ +import { computed, ref, unref, watch, Ref } from 'vue' +import { Resource, SpaceResource } from '@opencloud-eu/web-client' +import { useClientService } from '@opencloud-eu/web-pkg' + +export interface SpecialViewConfig { + type: string + [key: string]: unknown +} + +export interface SpecialFolderState { + /** .special/ directory detected in resource list */ + hasSpecialDir: Ref + /** Parsed view.json config, null if not loaded or error */ + viewConfig: Ref + /** Error message if .special/ present but view.json missing/broken */ + errorMessage: Ref + /** True while fetching view.json */ + isLoading: Ref +} + +export const useSpecialFolderView = ( + space: Ref, + resources: Ref +): SpecialFolderState => { + const clientService = useClientService() + const { getFileContents } = clientService.webdav + + const viewConfig = ref(null) + const errorMessage = ref(null) + const isLoading = ref(false) + + const hasSpecialDir = computed(() => { + return unref(resources).some( + (r) => r.isFolder && r.name === '.special' + ) + }) + + const specialDir = computed(() => { + return unref(resources).find( + (r) => r.isFolder && r.name === '.special' + ) + }) + + watch( + hasSpecialDir, + async (detected) => { + viewConfig.value = null + errorMessage.value = null + + if (!detected) { + return + } + + isLoading.value = true + try { + const dir = unref(specialDir) + const viewJsonPath = dir.path.replace(/\/?$/, '/view.json') + + const { body } = await getFileContents(unref(space), { + path: viewJsonPath + }) + + const parsed = JSON.parse(body as string) + if (!parsed.type || typeof parsed.type !== 'string') { + errorMessage.value = '.special/view.json: "type" field missing or invalid' + return + } + + viewConfig.value = parsed + } catch (e: any) { + if (e?.statusCode === 404 || e?.response?.status === 404) { + errorMessage.value = '.special/view.json not found' + } else if (e instanceof SyntaxError) { + errorMessage.value = '.special/view.json: invalid JSON' + } else { + errorMessage.value = `.special/view.json: ${e.message || 'unknown error'}` + } + } finally { + isLoading.value = false + } + }, + { immediate: true } + ) + + return { + hasSpecialDir, + viewConfig, + errorMessage, + isLoading + } +} diff --git a/packages/web-app-files/src/views/spaces/GenericSpace.vue b/packages/web-app-files/src/views/spaces/GenericSpace.vue index ba6b5d0fe9..b6a5843099 100644 --- a/packages/web-app-files/src/views/spaces/GenericSpace.vue +++ b/packages/web-app-files/src/views/spaces/GenericSpace.vue @@ -107,7 +107,7 @@