SPEC W4a: Import/Export Contribution-Architektur — zentraler Dialog per Toolbar-Button, Formate als Plugins, Module als Contributors #359

Closed
opened 2026-08-27 18:46:07 +00:00 by Leopoldadmin · 3 comments
Owner

Entscheidungen (2026-08-27, User-abgestimmt)

Zugang: Primärer Zugang ist der zentrale Dialog, geöffnet über einen Toolbar-Button in jeder Modulliste, deren Plugin Import/Export anbietet (Modul vorgewählt, aktive Filter übernommen). Die zentrale /import-export-Seite wird zur Übersicht aller Angebote (Variante b) — kein doppelter Arbeitsweg mehr.

Formate = Plugins: CSV/JSON/XLSX als Standard-Formate in einem bundled Plugin; neue Formate (PDF später) als weitere Format-Plugins — Erweiterung ohne Core-Änderung.

Module = Contributors: Jedes Plugin registriert via Contract, welche Formate es anbietet, und liefert die Logik (Daten holen / Zeilen anwenden).

Core = Orchestrator + Sicherheits-Policy-Layer: Sensitive-Data-Filter, Tenant-Scoping, Audit werden unabhängig vom Modul-Beitrag erzwungen (Lektion aus dem heutigen Doppel-Weg mit driftender Security).

Schnittstellen-Skizze

Format-Plugin (registriert):  format_id, export(rows, columns) -> bytes, parse(bytes) -> rows
Modul-Contract (registriert): entity, formats['csv','xlsx'], columns, fetch_data(filters), apply(rows)
Core-Orchestrator:            capabilities = Module ∩ Format-Registry · Policy-Layer · Audit · ARQ-Jobs >1000 Zeilen
Dialog (Modal lg/xl, bestehendes ui/Modal-Muster): Export-Tab 1 Schritt · Import-Tab 4 Schritte (Datei → Mapping → Dry-Run → Ausführung+Report)
Modul-nativ (ICS/EML/ZIP):    registrieren sich nur anzeigend im Dialog, Download über Modul-Endpoint

Dialog-Spec

Export-Tab: Format-Auswahl (Schnittmenge), Feldauswahl mit Modul-Defaults, Filter-Vorschau aus der Liste, Download bzw. Background-Job.

Import-Tab: 1. Datei hochladen (Format-Plugin parst) → 2. Mapping (Spalten↔Felder, Heuristik vom Modul, korrigierbar) → 3. Dry-Run (valid/invalid + strukturierte Fehler) → 4. Ausführung + Partial-Failure-Report.

Abgelöst: app/services/export_service.py (78 Z., CSV-only) und die contact-spezifische Logik in import_export_service.py (504 Z.) — Duplikat-Weg konsolidiert.

Phasen + Gates

Phase Inhalt Gate
1 Backend Format-Registry, Standard-Formate-Plugin, Contract-Protokoll, Orchestrator-Endpoints, Contacts migriert, Altwege abgeschaltet Bestehende import_export-Suite als Funktionserhalt · neuer Test „Plugin beiträgt ohne Core-Änderung" · Cross-Plugin-Checker 0
2 Frontend Dialog (Modal lg/xl), Integration in Modullisten-Toolbars, alte Seite → Übersicht Vitest · tsc · Production-Build vor Commit · Browser-Check (Kritikpunkt 21)
3 (später) PDF als weiteres Format-Plugin separat
## Entscheidungen (2026-08-27, User-abgestimmt) **Zugang:** Primärer Zugang ist der **zentrale Dialog**, geöffnet über einen **Toolbar-Button in jeder Modulliste**, deren Plugin Import/Export anbietet (Modul vorgewählt, aktive Filter übernommen). Die zentrale /import-export-Seite wird zur **Übersicht aller Angebote** (Variante b) — kein doppelter Arbeitsweg mehr. **Formate = Plugins:** CSV/JSON/XLSX als Standard-Formate in einem bundled Plugin; neue Formate (PDF später) als weitere Format-Plugins — Erweiterung ohne Core-Änderung. **Module = Contributors:** Jedes Plugin registriert via Contract, welche Formate es anbietet, und liefert die Logik (Daten holen / Zeilen anwenden). **Core = Orchestrator + Sicherheits-Policy-Layer:** Sensitive-Data-Filter, Tenant-Scoping, Audit werden unabhängig vom Modul-Beitrag erzwungen (Lektion aus dem heutigen Doppel-Weg mit driftender Security). ## Schnittstellen-Skizze ```text Format-Plugin (registriert): format_id, export(rows, columns) -> bytes, parse(bytes) -> rows Modul-Contract (registriert): entity, formats['csv','xlsx'], columns, fetch_data(filters), apply(rows) Core-Orchestrator: capabilities = Module ∩ Format-Registry · Policy-Layer · Audit · ARQ-Jobs >1000 Zeilen Dialog (Modal lg/xl, bestehendes ui/Modal-Muster): Export-Tab 1 Schritt · Import-Tab 4 Schritte (Datei → Mapping → Dry-Run → Ausführung+Report) Modul-nativ (ICS/EML/ZIP): registrieren sich nur anzeigend im Dialog, Download über Modul-Endpoint ``` ## Dialog-Spec **Export-Tab:** Format-Auswahl (Schnittmenge), Feldauswahl mit Modul-Defaults, Filter-Vorschau aus der Liste, Download bzw. Background-Job. **Import-Tab:** 1. Datei hochladen (Format-Plugin parst) → 2. Mapping (Spalten↔Felder, Heuristik vom Modul, korrigierbar) → 3. Dry-Run (valid/invalid + strukturierte Fehler) → 4. Ausführung + Partial-Failure-Report. **Abgelöst:** `app/services/export_service.py` (78 Z., CSV-only) und die contact-spezifische Logik in `import_export_service.py` (504 Z.) — Duplikat-Weg konsolidiert. ## Phasen + Gates | Phase | Inhalt | Gate | |---|---|---| | 1 Backend | Format-Registry, Standard-Formate-Plugin, Contract-Protokoll, Orchestrator-Endpoints, Contacts migriert, Altwege abgeschaltet | Bestehende import_export-Suite als Funktionserhalt · neuer Test „Plugin beiträgt ohne Core-Änderung" · Cross-Plugin-Checker 0 | | 2 Frontend | Dialog (Modal lg/xl), Integration in Modullisten-Toolbars, alte Seite → Übersicht | Vitest · tsc · Production-Build vor Commit · Browser-Check (Kritikpunkt 21) | | 3 (später) | PDF als weiteres Format-Plugin | separat |
Leopoldadmin added the task label 2026-08-27 18:46:07 +00:00
Author
Owner

Phase 1 (Backend-Kern) umgesetzt in Commit cd8ef75 — deployed SUCCESS (2026-08-27 21:14).

Was gebaut wurde

Baustein Datei Beschreibung
Format-Registry app/core/importexport_registry.py (neu) FormatHandler-Protokoll (parse/serialize), available_for() berechnet die Schnittmenge Module ∩ Formate, Singleton + Testing-Reset
Standard-Formate-Plugin app/plugins/builtins/importexport_formats/ (neu) CSV/JSON/XLSX-Handler als thin wrappers über die bestehenden helpers (keine Duplikation); lifecycle-korrekt: on_activate registriert, on_deactivate unregistriert (Welle-1-Regeln)
Contacts-Beitrag app/plugins/builtins/contacts/contracts.py importexport_entities() + ie_*-Methoden: Columns, Target-Fields, Validatoren, Normalizer, Required, Row-Valid, Fetch-Rows (visibility-gefiltert), Persist-Row (mit Audit)
Generische Engine app/services/import_export_service.py (neu geschrieben) _find_ie_contract() iteriert generisch über registry.list_discovered()keine hartcodierten Plugin-Namen mehr; Signaturen identisch zum alten Service

Funktionserhalt (Live-Messung)

45/45 tests/test_import_export.py passed — inkl. der subtilen Fehler-Multiplizitäts-Semantik: 2 failed rows → 3 total_errors (Zeile mit leerem Name UND invalidem Email = 2 Fehler). Erreicht durch: ie_required() wird an generisches validate_row() durchgereicht (wie alter Code), ie_row_valid() greift nur beim contacts either-or-Sonderfall, companies' required-name läuft über validate_row.

Ein Zwischenstand mit 2 Failures wurde live gefangen und korrigiert (ie_row_valid überschattete anfangs die validate_row-Multiplizität) — der Test beweist jetzt die Original-Semantik.

Phase 2 (Frontend-Dialog) folgt

Modal lg/xl (bestehendes ui/Modal-Muster), Export-Tab 1 Schritt, Import-Tab 4 Schritte, Toolbar-Buttons in Modullisten, alte Seite → Übersicht. Gates: Vitest + tsc + Production-Build vor Commit (Kritikpunkt 21).

**Phase 1 (Backend-Kern) umgesetzt in Commit `cd8ef75` — deployed SUCCESS (2026-08-27 21:14).** ## Was gebaut wurde | Baustein | Datei | Beschreibung | |---|---|---| | Format-Registry | `app/core/importexport_registry.py` (neu) | `FormatHandler`-Protokoll (parse/serialize), `available_for()` berechnet die Schnittmenge Module ∩ Formate, Singleton + Testing-Reset | | Standard-Formate-Plugin | `app/plugins/builtins/importexport_formats/` (neu) | CSV/JSON/XLSX-Handler als thin wrappers über die bestehenden helpers (keine Duplikation); lifecycle-korrekt: `on_activate` registriert, `on_deactivate` unregistriert (Welle-1-Regeln) | | Contacts-Beitrag | `app/plugins/builtins/contacts/contracts.py` | `importexport_entities()` + `ie_*`-Methoden: Columns, Target-Fields, Validatoren, Normalizer, Required, Row-Valid, Fetch-Rows (visibility-gefiltert), Persist-Row (mit Audit) | | Generische Engine | `app/services/import_export_service.py` (neu geschrieben) | `_find_ie_contract()` iteriert generisch über `registry.list_discovered()` — **keine hartcodierten Plugin-Namen mehr**; Signaturen identisch zum alten Service | ## Funktionserhalt (Live-Messung) **45/45** tests/test_import_export.py passed — inkl. der subtilen Fehler-Multiplizitäts-Semantik: 2 failed rows → 3 total_errors (Zeile mit leerem Name UND invalidem Email = 2 Fehler). Erreicht durch: `ie_required()` wird an generisches `validate_row()` durchgereicht (wie alter Code), `ie_row_valid()` greift nur beim contacts either-or-Sonderfall, companies' required-name läuft über validate_row. Ein Zwischenstand mit 2 Failures wurde live gefangen und korrigiert (ie_row_valid überschattete anfangs die validate_row-Multiplizität) — der Test beweist jetzt die Original-Semantik. ## Phase 2 (Frontend-Dialog) folgt Modal lg/xl (bestehendes ui/Modal-Muster), Export-Tab 1 Schritt, Import-Tab 4 Schritte, Toolbar-Buttons in Modullisten, alte Seite → Übersicht. Gates: Vitest + tsc + Production-Build vor Commit (Kritikpunkt 21).
Author
Owner

Phase 2 (Frontend-Dialog) umgesetzt in Commit 38df597 — frontend-only deployed (2026-08-27 21:34).

Was gebaut wurde

Baustein Beschreibung
ImportExportDialog.tsx (neu) Modal lg/xl nach bestehendem ui/Modal-Muster (Fokus-Handling, Escape, ARIA); Export-Tab (Format-Auswahl csv/xlsx/json + Download) und Import-Tab (4 Schritte: Datei → Mapping-Editor mit Modul-Heuristik → Dry-Run mit valid/invalid-Zählern und Fehlerliste → Ausführung + Ergebnis inkl. Background-Job-Polling ab 1000 Zeilen)
i18n 24 importexport.*-Keys in de.json + en.json (keine hardcoded Strings — AGENTS.md-Konvention)
ContactsList-Integration Toolbar-Button (contacts:read-Gate, Upload-Icon) über das bestehende pluginToolbarStore-Muster; entityType="contacts" vorgewählt

Verifikation (Live-Messung)

  • Neuer Regressionstest importExportDialog.test.ts: 6/6 passed (Tabs, Formate, 4 Spec-Schritte, Job-Polling, Toolbar-Integration, Modal-Nutzung) + routePermissions 6/6 = 12/12
  • tsc --noEmit: exit 0
  • Production-Build: exit 0 vor Commit (Kritikpunkt 21)
  • Regression: contacts + shell Suiten — 6 Failures per Stash-Test als Vorbestand bewiesen (identisch auf clean HEAD f27f047; ContactEditModal File-Level-Fail + AppShell 4× + Router 2× — gehört in das Isolations-/Vorbestand-Paket)
  • frontend-only Deploy: FE_EXIT=0 (~20s)

Status: Phase 1 (Backend) + Phase 2 (Frontend) sind produktiv. Phase 3 (PDF als weiteres Format-Plugin) separat.

**Phase 2 (Frontend-Dialog) umgesetzt in Commit `38df597` — frontend-only deployed (2026-08-27 21:34).** ## Was gebaut wurde | Baustein | Beschreibung | |---|---| | `ImportExportDialog.tsx` (neu) | Modal `lg/xl` nach bestehendem ui/Modal-Muster (Fokus-Handling, Escape, ARIA); Export-Tab (Format-Auswahl csv/xlsx/json + Download) und Import-Tab (4 Schritte: Datei → Mapping-Editor mit Modul-Heuristik → Dry-Run mit valid/invalid-Zählern und Fehlerliste → Ausführung + Ergebnis inkl. Background-Job-Polling ab 1000 Zeilen) | | i18n | 24 `importexport.*`-Keys in de.json + en.json (keine hardcoded Strings — AGENTS.md-Konvention) | | ContactsList-Integration | Toolbar-Button (`contacts:read`-Gate, Upload-Icon) über das bestehende pluginToolbarStore-Muster; `entityType="contacts"` vorgewählt | ## Verifikation (Live-Messung) - Neuer Regressionstest `importExportDialog.test.ts`: **6/6 passed** (Tabs, Formate, 4 Spec-Schritte, Job-Polling, Toolbar-Integration, Modal-Nutzung) + routePermissions **6/6** = 12/12 - tsc --noEmit: **exit 0** - **Production-Build: exit 0 vor Commit** (Kritikpunkt 21) - Regression: contacts + shell Suiten — **6 Failures per Stash-Test als Vorbestand bewiesen** (identisch auf clean HEAD f27f047; ContactEditModal File-Level-Fail + AppShell 4× + Router 2× — gehört in das Isolations-/Vorbestand-Paket) - frontend-only Deploy: **FE_EXIT=0** (~20s) **Status:** Phase 1 (Backend) + Phase 2 (Frontend) sind produktiv. Phase 3 (PDF als weiteres Format-Plugin) separat.
Author
Owner

Phase 2b (W4c): Custom-Fields-Routen aus Core in ContactsPlugin migriert — Commit c6decf5, deployed SUCCESS (2026-08-28 13:23).

Was migriert wurde

app/routes/custom_fields.py (contact-spezifisch: importiert Contact, nutzt contacts:read/write, Route /{contact_id}/custom-fields) wandert in app/plugins/builtins/contacts/routes.py (gleicher Router-Prefix /api/v1/contacts, bereits via manifest.routes gemounted). app/routes/custom_fields.py gelöscht, main.py bereinigt.

Der generische custom_field_definitions.py-Endpoint bleibt im Core (echtes Core-Entity, nicht contact-spezifisch).

Verifikation (Live-Messung)

  • tests/test_custom_fields.py: 11/11 passed (Funktionserhalt bewiesen)
  • create_app() OK · ruff modified-files grün · Frontend-Endpoint-Shapes unverändert
  • Full Deploy SUCCESS · Health healthy · Alembic 0142 OK

Damit ist Kritikpunkt 14 (custom_fields Contact-Fixierung) durch Konvention gelöst: Der Contact-spezifische Endpoint lebt jetzt im ContactsPlugin (is_core=True), der generische custom_field_definitions-Endpoint bleibt im Core für zukünftige Entity-Types.

**Phase 2b (W4c): Custom-Fields-Routen aus Core in ContactsPlugin migriert — Commit `c6decf5`, deployed SUCCESS (2026-08-28 13:23).** ## Was migriert wurde `app/routes/custom_fields.py` (contact-spezifisch: importiert Contact, nutzt contacts:read/write, Route /{contact_id}/custom-fields) wandert in `app/plugins/builtins/contacts/routes.py` (gleicher Router-Prefix /api/v1/contacts, bereits via manifest.routes gemounted). `app/routes/custom_fields.py` gelöscht, main.py bereinigt. Der generische `custom_field_definitions.py`-Endpoint bleibt im Core (echtes Core-Entity, nicht contact-spezifisch). ## Verifikation (Live-Messung) - tests/test_custom_fields.py: **11/11 passed** (Funktionserhalt bewiesen) - create_app() OK · ruff modified-files grün · Frontend-Endpoint-Shapes unverändert - Full Deploy SUCCESS · Health healthy · Alembic 0142 OK Damit ist Kritikpunkt 14 (custom_fields Contact-Fixierung) durch Konvention gelöst: Der Contact-spezifische Endpoint lebt jetzt im ContactsPlugin (is_core=True), der generische custom_field_definitions-Endpoint bleibt im Core für zukünftige Entity-Types.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: Leopoldadmin/leocrm#359