fix(UI-Overhaul-Phase1): 7 Bugs behoben
Check Cross-Plugin Imports / check (push) Has been cancelled
Check Cross-Plugin Imports / check (push) Has been cancelled
1.1 Contacts refresh: useUnifiedContacts mutations already invalidate (verified)
1.2 Contacts drag-drop: ContactList items already draggable + ContactFolderTree onDrop (verified)
1.3 Contacts move dialog: Added move-to-folder dropdown in ContactDetail
1.4 Wiki save refresh: Added refreshKey prop to WikiBrowser, triggers reload after save/delete
1.5 Calendar dialog close: Window.tsx now injects windowId into componentProps
1.6 Communication AI chat: Added metadata support to ConversationCreate schema + service,
frontend passes conversation_type metadata for AI/system chats,
categorization checks metadata first
1.7 Wiki duplicate menu: Removed hardcoded /wiki from Sidebar.tsx (plugin provides it dynamically)
Backend: kommunikation schema/routes/services updated for metadata support
Frontend: tsc clean, build successful
This commit is contained in:
@@ -0,0 +1,348 @@
|
|||||||
|
# LeoCRM UI-Overhaul-Plan (v2)
|
||||||
|
|
||||||
|
> **Erstellt:** 2026-08-21
|
||||||
|
> **Aktualisiert:** 2026-08-21 — AI Assistent Integration hinzugefügt
|
||||||
|
> **Status:** Planung — nicht gestartet
|
||||||
|
> **Leitlinie:** Auf bestehendem Code aufbauen, 3-Spalten-Explorer-Layout als Standard, keine parallelen Systeme
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Standard-Layout (Referenz: ContactsList.tsx)
|
||||||
|
|
||||||
|
Alle Explorer-Plugins nutzen das 3-Spalten-Layout aus den UI-Design-Guidelines:
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────┬──────────────────┬──────────────────────┐
|
||||||
|
│ Tree │ Liste/Ansicht │ Detail │
|
||||||
|
│ (224px) │ (flex-1) │ (flex-1 / 60%) │
|
||||||
|
│ ResizablePanel│ ResizablePanel │ ResizablePanel │
|
||||||
|
└─────────────┴──────────────────┴──────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Toolbar oben:** PluginToolbar mit Filter-Dropdowns, Ansichts-Umschaltern, Aktion-Buttons
|
||||||
|
- **Linke Spalte:** ResizablePanel mit Baumansicht (Ordner, Kategorien, Kalender)
|
||||||
|
- **Mitte:** Liste, Karten, Kalender-Ansicht — mehrere Ansichten umschaltbar
|
||||||
|
- **Rechts:** Detail-Bereich für ausgewähltes Element
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 1: Echte Bugs fixen (2-3 Tage)
|
||||||
|
|
||||||
|
### 1.1 Kontakte — Liste aktualisiert nach Speichern nicht
|
||||||
|
- **Datei:** `frontend/src/pages/ContactsList.tsx`
|
||||||
|
- **Problem:** Nach dem Speichern eines Kontakts wird die Liste nicht aktualisiert
|
||||||
|
- **Ursache:** Wahrscheinlich fehlendes `invalidateQueries` nach Mutation
|
||||||
|
- **Fix:** TanStack Query `useCreateContact` mutation muss `queryClient.invalidateQueries({ queryKey: ['contacts'] })` im `onSuccess` haben
|
||||||
|
- **Aufwand:** 1 Stunde
|
||||||
|
|
||||||
|
### 1.2 Kontakte — Drag-Drop von Kontakten in Ordner nicht möglich
|
||||||
|
- **Datei:** `frontend/src/pages/ContactsList.tsx`, `frontend/src/components/contacts/`
|
||||||
|
- **Problem:** Drag-Drop von Kontakten in Ordner funktioniert nicht
|
||||||
|
- **Fix:** HTML5 Drag-Drop API auf Tree-Nodes implementieren, `onDrop` handler der `updateContact({ folder_id })` aufruft
|
||||||
|
- **Aufwand:** 3 Stunden
|
||||||
|
|
||||||
|
### 1.3 Kontakte — Verschieben-Dialog funktioniert nicht
|
||||||
|
- **Datei:** `frontend/src/components/contacts/MoveDialog.tsx` (oder ähnlich)
|
||||||
|
- **Problem:** Ordner-Auswahl im Verschieben-Dialog leer oder broken
|
||||||
|
- **Fix:** Ordner-API aufrufen und im Dialog anzeigen, Auswahl speichern
|
||||||
|
- **Aufwand:** 2 Stunden
|
||||||
|
|
||||||
|
### 1.4 Wiki — Artikel kann nicht gespeichert werden
|
||||||
|
- **Datei:** `frontend/src/pages/Wiki.tsx`, `frontend/src/api/knowledge.ts`
|
||||||
|
- **Problem:** Speichern-Button funktioniert nicht oder API gibt Fehler zurück
|
||||||
|
- **Diagnose:** API-Endpunkt prüfen (`POST /api/v1/wiki/articles` oder `PATCH /api/v1/wiki/articles/:id`), Frontend-Mutation prüfen
|
||||||
|
- **Fix:** Je nach Diagnose — API-Fehler oder Frontend-Mutation-Fehler
|
||||||
|
- **Aufwand:** 2 Stunden
|
||||||
|
|
||||||
|
### 1.5 Kalender — Dialog schließt nicht nach Speichern
|
||||||
|
- **Datei:** `frontend/src/pages/Calendar.tsx`, `frontend/src/components/calendar/AppointmentEditForm.tsx`
|
||||||
|
- **Problem:** Nach dem Speichern eines Termins schließt sich der Dialog nicht
|
||||||
|
- **Fix:** `onSuccess` handler muss `setEditingEvent(null)` oder `setShowDialog(false)` aufrufen
|
||||||
|
- **Aufwand:** 30 Minuten
|
||||||
|
|
||||||
|
### 1.6 Kommunikation — Chats können nicht angelegt werden
|
||||||
|
- **Datei:** `frontend/src/pages/Communication.tsx`
|
||||||
|
- **Problem:** "Neuer Chat" Button funktioniert nicht oder API gibt Fehler
|
||||||
|
- **Diagnose:** API-Endpunkt prüfen (`POST /api/v1/comm/conversations`), Frontend-Mutation prüfen
|
||||||
|
- **Fix:** Je nach Diagnose
|
||||||
|
- **Aufwand:** 2 Stunden
|
||||||
|
|
||||||
|
### 1.7 Wiki — Doppelt im Menü
|
||||||
|
- **Datei:** `frontend/src/routes/index.tsx`, `frontend/src/components/layout/` (Navigation)
|
||||||
|
- **Problem:** Wiki erscheint zweimal im Menü
|
||||||
|
- **Diagnose:** Route `/wiki` und möglicherweise Help-Subroute oder Plugin-Route
|
||||||
|
- **Fix:** Doppelte Route entfernen
|
||||||
|
- **Aufwand:** 30 Minuten
|
||||||
|
|
||||||
|
**Gesamtaufwand Phase 1:** ~13 Stunden (2-3 Tage)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 2: AI Assistent in Kommunikation integrieren (2-3 Tage)
|
||||||
|
|
||||||
|
### Problem
|
||||||
|
Der AI Assistent ist ein paralleles System das die Kommunikation-Plattform dupliziert:
|
||||||
|
- **AI Assistant Tabellen:** `ai_conversations`, `ai_messages` (app/models/ai_conversation.py) + `ai_chat_sessions`, `ai_chat_messages`, `ai_chat_attachments` (app/plugins/builtins/ai_assistant/models.py) — 5 Tabellen
|
||||||
|
- **AI Assistant Frontend:** `AIAssistant.tsx`, `AIAssistantStandalone.tsx`, `SessionList.tsx`, `ChatWindow.tsx` — eigene UI
|
||||||
|
- **AI Assistant API:** `/api/v1/ai/sessions`, `/api/v1/ai/sessions/:id/messages`, `/api/v1/ai/sessions/:id/stream` — eigene API
|
||||||
|
- **Kommunikation hat schon AI-Chat:** `comm_conversations` mit `conversation_type='ai'`, `streamChat()` aus `@/api/ai`, `categorizeConversation()` mit 'KI Chats' Kategorie, `new-ai-chat` Toolbar-Button
|
||||||
|
|
||||||
|
### 2.1 Daten-Migration (Backend)
|
||||||
|
- **Migration 0137:** Migriere `ai_chat_sessions` → `comm_conversations` (conversation_type='ai')
|
||||||
|
- `ai_chat_sessions.id` → `comm_conversations.id`
|
||||||
|
- `ai_chat_sessions.title` → `comm_conversations.title`
|
||||||
|
- `ai_chat_sessions.tenant_id` → `comm_conversations.tenant_id`
|
||||||
|
- `ai_chat_sessions.user_id` → `comm_conversations.owner_id`
|
||||||
|
- `ai_chat_sessions.agent_id` → `comm_conversations.metadata.agent_id`
|
||||||
|
- `ai_chat_sessions.created_at` → `comm_conversations.created_at`
|
||||||
|
- **Migration 0137:** Migriere `ai_chat_messages` → `comm_messages`
|
||||||
|
- `ai_chat_messages.id` → `comm_messages.id`
|
||||||
|
- `ai_chat_messages.session_id` → `comm_messages.conversation_id`
|
||||||
|
- `ai_chat_messages.role` → `comm_messages.sender_type` ('user' → 'user', 'assistant' → 'ai')
|
||||||
|
- `ai_chat_messages.content` → `comm_messages.content`
|
||||||
|
- `ai_chat_messages.tenant_id` → `comm_messages.tenant_id`
|
||||||
|
- **Migration 0137:** Migriere `ai_conversations` → `comm_conversations` (falls Daten vorhanden)
|
||||||
|
- **Migration 0137:** Migriere `ai_messages` → `comm_messages` (falls Daten vorhanden)
|
||||||
|
- **Migration 0137:** Drop `ai_conversations`, `ai_messages`, `ai_chat_sessions`, `ai_chat_messages`, `ai_chat_attachments` Tabellen
|
||||||
|
- **Aufwand:** 1 Tag
|
||||||
|
|
||||||
|
### 2.2 Backend — AI Chat API auf Communication umleiten
|
||||||
|
- **Datei:** `app/plugins/builtins/ai_assistant/routes.py`
|
||||||
|
- **Änderung:** `POST /api/v1/ai/sessions` → erstellt `comm_conversations` mit `conversation_type='ai'` statt `ai_chat_sessions`
|
||||||
|
- **Änderung:** `GET /api/v1/ai/sessions/:id/messages` → liest aus `comm_messages` statt `ai_chat_messages`
|
||||||
|
- **Änderung:** `POST /api/v1/ai/sessions/:id/stream` → bleibt erhalten (streaming endpoint) aber speichert messages in `comm_messages`
|
||||||
|
- **Aufwand:** 4 Stunden
|
||||||
|
|
||||||
|
### 2.3 Frontend — AI Assistant Page entfernen
|
||||||
|
- **Entfernen:** `frontend/src/pages/AIAssistant.tsx`
|
||||||
|
- **Entfernen:** `frontend/src/pages/AIAssistantStandalone.tsx`
|
||||||
|
- **Entfernen:** `frontend/src/components/ai/SessionList.tsx`
|
||||||
|
- **Entfernen:** `frontend/src/components/ai/ChatWindow.tsx`
|
||||||
|
- **Route anpassen:** `/ai-assistant` → **gelöscht** (kein Redirect nötig)
|
||||||
|
- **Route anpassen:** `/ai-assistant-standalone` → **gelöscht** (kein Redirect nötig)
|
||||||
|
- **Navigation:** AI Assistent Menüpunkt entfernen, AI Chat bleibt unter Kommunikation
|
||||||
|
- **Aufwand:** 2 Stunden
|
||||||
|
|
||||||
|
### 2.4 Frontend — Communication AI-Chat verbessern
|
||||||
|
- **Datei:** `frontend/src/pages/Communication.tsx`
|
||||||
|
- **Änderung:** AI Chat Sessions aus `comm_conversations` laden (statt `ai/sessions` API)
|
||||||
|
- **Änderung:** `streamChat()` bleibt erhalten aber Session-ID ist jetzt `comm_conversation_id`
|
||||||
|
- **Änderung:** AI Chat Messages aus `comm_messages` laden
|
||||||
|
- **Aufwand:** 4 Stunden
|
||||||
|
|
||||||
|
### 2.5 Backend — ai_assistant plugin models aufräumen
|
||||||
|
- **Entfernen:** `AIChatSession`, `AIChatMessage`, `AIChatAttachment` Models aus `app/plugins/builtins/ai_assistant/models.py`
|
||||||
|
- **Entfernen:** `AIConversation`, `AIMessage` Models aus `app/models/ai_conversation.py`
|
||||||
|
- **Behalten:** `AIProvider`, `AIModel`, `AIPreset`, `AIChatFolder` Models (für Settings)
|
||||||
|
- **Behalten:** `ai_assistant` plugin routes für Settings (providers, models, presets)
|
||||||
|
- **Aufwand:** 2 Stunden
|
||||||
|
|
||||||
|
### 2.6 Unified Search — AI Chat Provider anpassen
|
||||||
|
- **Datei:** `app/plugins/builtins/unified_search/providers/ai_chat_provider.py`
|
||||||
|
- **Änderung:** Search auf `comm_messages` (conversation_type='ai') statt `ai_chat_messages`
|
||||||
|
- **Aufwand:** 1 Stunde
|
||||||
|
|
||||||
|
**Gesamtaufwand Phase 2:** ~2-3 Tage
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 3: Wiki UI-Überarbeitung (3-4 Tage)
|
||||||
|
|
||||||
|
### 3.1 WYSIWYG Editor
|
||||||
|
- **Datei:** `frontend/src/components/wiki/WikiEditor.tsx` (neu zu bauen)
|
||||||
|
- **Anforderung:** WYSIWYG Editor mit allen Möglichkeiten, wie Notion — Bedienelemente über dem Textblock
|
||||||
|
- **Technologie:** Tiptap (ProseMirror-basiert, React-integration, Notion-ähnliche UX)
|
||||||
|
- `@tiptap/react`, `@tiptap/starter-kit`, `@tiptap/extension-*`
|
||||||
|
- Floating Toolbar über dem Textblock (wie Notion)
|
||||||
|
- Markdown-Export für Backend-Speicherung
|
||||||
|
- **Aufwand:** 2 Tage
|
||||||
|
|
||||||
|
### 3.2 Wiki Layout — 3-Spalten
|
||||||
|
- **Datei:** `frontend/src/pages/Wiki.tsx` (umbauen)
|
||||||
|
- **Anforderung:** Toolbar oben, links Baummenü (Kategorien), Mitte Textbereich
|
||||||
|
- **Aufbau:**
|
||||||
|
- **Toolbar:** View/Edit Mode Toggle (oben rechts), Suche, Neuer Artikel
|
||||||
|
- **Links:** WikiBrowser (existiert schon) — Baumansicht mit Kategorien
|
||||||
|
- **Mitte:** WYSIWYG Editor (Edit Mode) oder gerenderte Ansicht (View Mode)
|
||||||
|
- **Kein separater Detail-Bereich** — Artikel wird in der Mitte angezeigt
|
||||||
|
- **Aufwand:** 1 Tag
|
||||||
|
|
||||||
|
### 3.3 View/Edit Mode Toggle
|
||||||
|
- **Datei:** `frontend/src/pages/Wiki.tsx`
|
||||||
|
- **Anforderung:** Button oben rechts in der Toolbar der zwischen View und Edit Mode wechselt
|
||||||
|
- **Im Edit Mode:** WYSIWYG Editor mit Floating Toolbar
|
||||||
|
- **Im View Mode:** Gerenderte Markdown-Ansicht (wie jetzt, aber schöner)
|
||||||
|
- **Aufwand:** 2 Stunden
|
||||||
|
|
||||||
|
**Gesamtaufwand Phase 3:** ~3-4 Tage
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 4: Tasks UI-Überarbeitung (2-3 Tage)
|
||||||
|
|
||||||
|
### 4.1 Tasks Layout — 3-Spalten wie Kontakte
|
||||||
|
- **Datei:** `frontend/src/pages/Tasks.tsx` (kompletter Umbau, 419 → ~600 Zeilen)
|
||||||
|
- **Anforderung:** Linke Sidebar Baumansicht, Mitte Liste mit mehreren Ansichten, rechts Detailbereich
|
||||||
|
- **Aufbau:**
|
||||||
|
- **Toolbar:** PluginToolbar mit Filter-Dropdowns (Status, Priorität, Zuweisung, Fällig), Ansichts-Umschalter (Liste/Kanban), Neuer Task
|
||||||
|
- **Links:** Baumansicht — nach Status (Offen/In Bearbeitung/Erledigt), nach Priorität, nach Zuweisung, nach Liste/Goal
|
||||||
|
- **Mitte:** Liste (Tabelle) oder Kanban-Board — umschaltbar
|
||||||
|
- **Rechts:** TaskDetail — ausgewählter Task mit Beschreibung, Subtasks, Zuweisung, Fälligkeit
|
||||||
|
- **Aufwand:** 2-3 Tage
|
||||||
|
|
||||||
|
**Gesamtaufwand Phase 4:** ~2-3 Tage
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 5: Kalender UI-Überarbeitung (1 Tag)
|
||||||
|
|
||||||
|
### 5.1 Toolbar und Filter standardisieren
|
||||||
|
- **Datei:** `frontend/src/pages/Calendar.tsx` (anpassen, 759 Zeilen)
|
||||||
|
- **Problem:** Drucken-Button und Filter-Leiste über dem Kalender entsprechen nicht dem Standard
|
||||||
|
- **Fix:**
|
||||||
|
- Filter in PluginToolbar als Dropdowns (wie Kontakte)
|
||||||
|
- Drucken-Button in PluginToolbar
|
||||||
|
- Ansichts-Umschalter (Tag/Woche/Monat/Range) in PluginToolbar
|
||||||
|
- **Aufwand:** 4 Stunden
|
||||||
|
|
||||||
|
### 5.2 Kalender-Auswahl fixen
|
||||||
|
- **Datei:** `frontend/src/components/calendar/CalendarTree.tsx`
|
||||||
|
- **Problem:** Einzelnes An- und Abwählen von Kalendern funktioniert nicht richtig
|
||||||
|
- **Fix:** Checkbox-Toggle Logik reparieren — `visibleCalendars` Set korrekt verwalten
|
||||||
|
- **Aufwand:** 2 Stunden
|
||||||
|
|
||||||
|
**Gesamtaufwand Phase 5:** ~1 Tag
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 6: Tags Umstrukturierung (2 Tage)
|
||||||
|
|
||||||
|
### 6.1 Tags in Settings verschieben
|
||||||
|
- **Datei:** `frontend/src/pages/Tags.tsx` → `frontend/src/pages/SettingsTags.tsx` (neu)
|
||||||
|
- **Route:** `/settings/tags` statt `/tags`
|
||||||
|
- **Anforderung:** Tags gehören in die Einstellungen, bei System
|
||||||
|
- **Aufwand:** 2 Stunden
|
||||||
|
|
||||||
|
### 6.2 Tags Baumstruktur
|
||||||
|
- **Datei:** `frontend/src/pages/SettingsTags.tsx` (neu)
|
||||||
|
- **Anforderung:** Baumstruktur um Tags zu sortieren (Parent-Child Beziehung)
|
||||||
|
- **Backend:** `tags` Tabelle braucht `parent_id` Spalte (Migration 0138)
|
||||||
|
- **Frontend:** TreeView Komponente für Tags
|
||||||
|
- **Aufwand:** 1 Tag
|
||||||
|
|
||||||
|
### 6.3 Pro Tag einstellbar wo er verfügbar ist
|
||||||
|
- **Datei:** `frontend/src/pages/SettingsTags.tsx`, Backend `tags` Tabelle
|
||||||
|
- **Anforderung:** Pro Tag einstellbar: Kontakte, Mail, Termin, Task, etc.
|
||||||
|
- **Backend:** `tag_applications` Tabelle (tag_id, entity_type) oder JSON-Spalte `applicable_to` in tags (Migration 0138)
|
||||||
|
- **Frontend:** Multi-Select im Tag-Editor
|
||||||
|
- **Aufwand:** 4 Stunden
|
||||||
|
|
||||||
|
### 6.4 Symbol und Farbe pro Tag
|
||||||
|
- **Datei:** `frontend/src/pages/SettingsTags.tsx`, Backend `tags` Tabelle
|
||||||
|
- **Anforderung:** Symbol (Icon) und Farbe pro Tag einstellbar
|
||||||
|
- **Backend:** `icon` Spalte in tags (Migration 0138), `color` existiert schon
|
||||||
|
- **Frontend:** Icon-Picker und Color-Picker im Tag-Editor
|
||||||
|
- **Aufwand:** 4 Stunden
|
||||||
|
|
||||||
|
**Gesamtaufwand Phase 6:** ~2 Tage
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 7: Reports UI-Überarbeitung (2 Tage)
|
||||||
|
|
||||||
|
### 7.1 Reports Layout — 3-Spalten wie Kontakte
|
||||||
|
- **Datei:** `frontend/src/pages/Reports.tsx` (Umbau, 433 Zeilen)
|
||||||
|
- **Anforderung:** Linke Sidebar mit Baumstruktur (Ordner zum Sortieren), Mitte verschiedene Ansichten (Liste/Karten), rechts Detailbereich
|
||||||
|
- **Aufbau:**
|
||||||
|
- **Toolbar:** PluginToolbar mit Filter, Ansichts-Umschalter, Neuer Report
|
||||||
|
- **Links:** Baumansicht — nach Ordner/Gruppe sortierbar
|
||||||
|
- **Mitte:** Liste oder Karten-Ansicht — umschaltbar
|
||||||
|
- **Rechts:** ReportDetail — ausgewählter Report mit Vorschau
|
||||||
|
- **Backend:** `reports` Tabelle braucht `folder_id` Spalte (Migration 0139) für Ordner-Sortierung
|
||||||
|
- **Aufwand:** 2 Tage
|
||||||
|
|
||||||
|
**Gesamtaufwand Phase 7:** ~2 Tage
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 8: Kommunikation UI-Überarbeitung (2-3 Tage)
|
||||||
|
|
||||||
|
### 8.1 Baumstruktur verbessern und Ordner
|
||||||
|
- **Datei:** `frontend/src/pages/Communication.tsx` (anpassen, 859 Zeilen)
|
||||||
|
- **Anforderung:** Baumstruktur größer/übersichtlicher, Ordner für Chats
|
||||||
|
- **Aufbau:**
|
||||||
|
- **Links:** Baumansicht mit Ordnern — System, AI, Kollegen, Custom Ordner
|
||||||
|
- **Baum breiter:** ResizablePanel `initialWidth=280` statt 224
|
||||||
|
- **Ordner:** `comm_conversation_folders` Tabelle oder `folder_id` in `comm_conversations` (Migration 0140)
|
||||||
|
- **Aufwand:** 1-2 Tage
|
||||||
|
|
||||||
|
### 8.2 AI Chat in Kommunikation (nach Phase 2)
|
||||||
|
- AI Chats werden als eigener Baum-Knoten 'KI Chats' in Communication angezeigt
|
||||||
|
- Neuer AI Chat Button in Toolbar erstellt `comm_conversation` mit `conversation_type='ai'`
|
||||||
|
- `streamChat()` wird aufgerufen mit `comm_conversation_id` als Session-ID
|
||||||
|
- AI Messages werden in `comm_messages` gespeichert
|
||||||
|
- **Aufwand:** in Phase 2
|
||||||
|
|
||||||
|
**Gesamtaufwand Phase 8:** ~1-2 Tage (Phase 2 vorab)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 9: Strukturelle Änderungen (0.5 Tage)
|
||||||
|
|
||||||
|
### 9.1 System Dashboard als eigener Menüpunkt
|
||||||
|
- **Datei:** `frontend/src/routes/index.tsx`, Navigation
|
||||||
|
- **Problem:** System Dashboard ist unter Settings, soll eigener Punkt auf Startseite-Ebene sein
|
||||||
|
- **Fix:** Route `/system-dashboard` existiert schon — muss in Navigation als Top-Level Menüpunkt angezeigt werden
|
||||||
|
- **Aufwand:** 1 Stunde
|
||||||
|
|
||||||
|
### 9.2 Mail — Postfach mit IMAP anlegen testen
|
||||||
|
- **Datei:** `frontend/src/pages/Mail.tsx`, `frontend/src/pages/MailSettings.tsx`
|
||||||
|
- **Anforderung:** IMAP-Zugangsdaten testen — Postfach anlegen und prüfen ob Mails synchronisiert werden
|
||||||
|
- **Aufwand:** 2 Stunden (Test + ggf. Bugfix)
|
||||||
|
|
||||||
|
**Gesamtaufwand Phase 9:** ~0.5 Tage
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Zusammenfassung
|
||||||
|
|
||||||
|
| Phase | Inhalt | Aufwand | Migration | Abhängigkeit |
|
||||||
|
|-------|--------|---------|-----------|-------------|
|
||||||
|
| 1 | Echte Bugs fixen | 2-3 Tage | Keine | Keine |
|
||||||
|
| 2 | AI Assistent → Kommunikation | 2-3 Tage | 0137 | Phase 1.6 |
|
||||||
|
| 3 | Wiki UI + WYSIWYG | 3-4 Tage | Keine | Phase 1.4 |
|
||||||
|
| 4 | Tasks UI neu | 2-3 Tage | Keine | Keine |
|
||||||
|
| 5 | Kalender UI | 1 Tag | Keine | Phase 1.5 |
|
||||||
|
| 6 | Tags Umstrukturierung | 2 Tage | 0138 | Keine |
|
||||||
|
| 7 | Reports UI | 2 Tage | 0139 | Keine |
|
||||||
|
| 8 | Kommunikation UI | 1-2 Tage | 0140 | Phase 2 |
|
||||||
|
| 9 | Strukturelle Änderungen | 0.5 Tage | Keine | Keine |
|
||||||
|
|
||||||
|
**Gesamtaufwand:** ~17-22 Tage
|
||||||
|
|
||||||
|
### Reihenfolge:
|
||||||
|
1. **Phase 1** (Bugs) — zuerst, damit grundlegende Funktionen arbeiten
|
||||||
|
2. **Phase 9** (Strukturelle Änderungen) — schnell, wenig Aufwand
|
||||||
|
3. **Phase 5** (Kalender) — kleines Update, baut auf Phase 1 auf
|
||||||
|
4. **Phase 2** (AI Assistent → Kommunikation) — entfernt paralleles System, baut auf Phase 1.6 auf
|
||||||
|
5. **Phase 6** (Tags) — unabhängig, Backend + Frontend
|
||||||
|
6. **Phase 4** (Tasks) — großer Umbau, unabhängig
|
||||||
|
7. **Phase 3** (Wiki) — größter Umbau (WYSIWYG Editor), baut auf Phase 1 auf
|
||||||
|
8. **Phase 7** (Reports) — großer Umbau, unabhängig
|
||||||
|
9. **Phase 8** (Kommunikation) — baut auf Phase 2 auf
|
||||||
|
|
||||||
|
### Migrationen:
|
||||||
|
- **0137:** AI Assistent Tabellen → comm_conversations/comm_messages + Drop alte Tabellen
|
||||||
|
- **0138:** Tags: parent_id, applicable_to, icon Spalten
|
||||||
|
- **0139:** Reports: folder_id Spalte
|
||||||
|
- **0140:** Communication: comm_conversation_folders Tabelle oder folder_id in comm_conversations
|
||||||
|
|
||||||
|
### Was ich NICHT tun werde:
|
||||||
|
- Keine Massen-Scripts die neue Fehler verursachen
|
||||||
|
- Keine Änderungen ohne Verifizierung gegen Produktion
|
||||||
|
- Keine neuen Plugins wenn bestehende erweitert werden können
|
||||||
|
- Keine neuen Pages wenn bestehende umgebaut werden können
|
||||||
|
- Jede Änderung wird mit tsc und API-Test verifiziert
|
||||||
|
|
||||||
|
### Was ich brauche:
|
||||||
|
- **IMAP-Zugangsdaten:** Für Mail-Postfach-Test (Phase 9.2)
|
||||||
@@ -99,6 +99,7 @@ async def create_new_conversation(
|
|||||||
participant_ids=body.participant_ids,
|
participant_ids=body.participant_ids,
|
||||||
is_direct=body.is_direct,
|
is_direct=body.is_direct,
|
||||||
initial_message=body.initial_message,
|
initial_message=body.initial_message,
|
||||||
|
metadata=body.metadata,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ class ConversationCreate(BaseModel):
|
|||||||
participant_types: list[str] = Field(default_factory=lambda: ["user"])
|
participant_types: list[str] = Field(default_factory=lambda: ["user"])
|
||||||
initial_message: str | None = None
|
initial_message: str | None = None
|
||||||
is_direct: bool = False
|
is_direct: bool = False
|
||||||
|
metadata: dict[str, Any] = Field(default_factory=dict)
|
||||||
|
|
||||||
|
|
||||||
class ConversationUpdate(BaseModel):
|
class ConversationUpdate(BaseModel):
|
||||||
|
|||||||
@@ -253,6 +253,7 @@ async def create_conversation(
|
|||||||
participant_ids: list[str] | None = None,
|
participant_ids: list[str] | None = None,
|
||||||
is_direct: bool = False,
|
is_direct: bool = False,
|
||||||
initial_message: str | None = None,
|
initial_message: str | None = None,
|
||||||
|
metadata: dict[str, Any] | None = None,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
"""Create a new conversation."""
|
"""Create a new conversation."""
|
||||||
conv = CommConversation(
|
conv = CommConversation(
|
||||||
@@ -262,7 +263,7 @@ async def create_conversation(
|
|||||||
is_direct=is_direct,
|
is_direct=is_direct,
|
||||||
created_by=user_id,
|
created_by=user_id,
|
||||||
created_by_type="user",
|
created_by_type="user",
|
||||||
metadata_={},
|
metadata_=metadata or {},
|
||||||
)
|
)
|
||||||
from app.core.hooks import do_action
|
from app.core.hooks import do_action
|
||||||
await do_action("comm.conversation.before_create", tenant_id=tenant_id, user_id=user_id)
|
await do_action("comm.conversation.before_create", tenant_id=tenant_id, user_id=user_id)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { asError } from '@/utils/errorTypes';
|
import { asError } from '@/utils/errorTypes';
|
||||||
import React, { useState, useEffect, useMemo } from 'react';
|
import React, { useState, useEffect, useMemo, useRef } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Button } from '@/components/ui/Button';
|
import { Button } from '@/components/ui/Button';
|
||||||
import { Badge } from '@/components/ui/Badge';
|
import { Badge } from '@/components/ui/Badge';
|
||||||
@@ -8,7 +8,7 @@ import { Modal } from '@/components/ui/Modal';
|
|||||||
import { Input } from '@/components/ui/Input';
|
import { Input } from '@/components/ui/Input';
|
||||||
import { useToast } from '@/components/ui/Toast';
|
import { useToast } from '@/components/ui/Toast';
|
||||||
|
|
||||||
import { Loader2, FileText, FolderOpen, Mail, Calendar, Link, Tag } from 'lucide-react';
|
import { Loader2, FileText, FolderOpen, Mail, Calendar, Link, Tag, FolderInput } from 'lucide-react';
|
||||||
|
|
||||||
const ICON_MAP: Record<string, React.ComponentType<{ className?: string }>> = {
|
const ICON_MAP: Record<string, React.ComponentType<{ className?: string }>> = {
|
||||||
FileText,
|
FileText,
|
||||||
@@ -36,6 +36,8 @@ import {
|
|||||||
useUpdateContactPerson,
|
useUpdateContactPerson,
|
||||||
useDeleteContactPerson,
|
useDeleteContactPerson,
|
||||||
} from '@/api/hooks';
|
} from '@/api/hooks';
|
||||||
|
import { useContactFolders, useMoveContactToFolder } from '@/api/contacts';
|
||||||
|
import type { ContactFolder } from '@/api/contactFolders';
|
||||||
|
|
||||||
export interface ContactDetailProps {
|
export interface ContactDetailProps {
|
||||||
contact: UnifiedContact | null;
|
contact: UnifiedContact | null;
|
||||||
@@ -178,6 +180,11 @@ export function ContactDetail({ contact, loading, onEdit, onDeleted, dataTestId
|
|||||||
const createPersonMutation = useCreateContactPerson();
|
const createPersonMutation = useCreateContactPerson();
|
||||||
const updatePersonMutation = useUpdateContactPerson();
|
const updatePersonMutation = useUpdateContactPerson();
|
||||||
const deletePersonMutation = useDeleteContactPerson();
|
const deletePersonMutation = useDeleteContactPerson();
|
||||||
|
const moveContactMut = useMoveContactToFolder();
|
||||||
|
const { data: folderData } = useContactFolders();
|
||||||
|
const folders: ContactFolder[] = folderData ?? [];
|
||||||
|
const [moveFolderOpen, setMoveFolderOpen] = useState(false);
|
||||||
|
const moveFolderRef = useRef<HTMLDivElement>(null);
|
||||||
const [personModalOpen, setPersonModalOpen] = useState(false);
|
const [personModalOpen, setPersonModalOpen] = useState(false);
|
||||||
const [editingPerson, setEditingPerson] = useState<ContactPerson | null>(null);
|
const [editingPerson, setEditingPerson] = useState<ContactPerson | null>(null);
|
||||||
const [activeTab, setActiveTab] = useState('details');
|
const [activeTab, setActiveTab] = useState('details');
|
||||||
@@ -199,6 +206,35 @@ export function ContactDetail({ contact, loading, onEdit, onDeleted, dataTestId
|
|||||||
}
|
}
|
||||||
}, [aiActiveTab]);
|
}, [aiActiveTab]);
|
||||||
|
|
||||||
|
// Close move folder dropdown on outside click
|
||||||
|
useEffect(() => {
|
||||||
|
if (!moveFolderOpen) return;
|
||||||
|
const handler = (e: MouseEvent) => {
|
||||||
|
if (moveFolderRef.current && !moveFolderRef.current.contains(e.target as Node)) {
|
||||||
|
setMoveFolderOpen(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
document.addEventListener('mousedown', handler);
|
||||||
|
return () => document.removeEventListener('mousedown', handler);
|
||||||
|
}, [moveFolderOpen]);
|
||||||
|
|
||||||
|
const handleMoveToFolder = (folderId: string | null) => {
|
||||||
|
if (!contact) return;
|
||||||
|
moveContactMut.mutate(
|
||||||
|
{ contactId: contact.id, folderId },
|
||||||
|
{
|
||||||
|
onSuccess: () => {
|
||||||
|
toast.success(folderId ? t('contacts.movedToFolder') : t('contacts.removedFromFolder'));
|
||||||
|
setMoveFolderOpen(false);
|
||||||
|
},
|
||||||
|
onError: (err: unknown) => {
|
||||||
|
const errObj = asError(err);
|
||||||
|
toast.error(errObj.message || t('common.error'));
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
// Sync modal from AI UI Control (Phase 4.7)
|
// Sync modal from AI UI Control (Phase 4.7)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (aiActiveModal === 'edit' || aiActiveModal === 'create') {
|
if (aiActiveModal === 'edit' || aiActiveModal === 'create') {
|
||||||
@@ -308,6 +344,42 @@ export function ContactDetail({ contact, loading, onEdit, onDeleted, dataTestId
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2 flex-shrink-0">
|
<div className="flex items-center gap-2 flex-shrink-0">
|
||||||
|
{canAccess('contacts:write') && (
|
||||||
|
<div ref={moveFolderRef} className="relative">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setMoveFolderOpen(!moveFolderOpen)}
|
||||||
|
icon={<FolderInput className="h-4 w-4" aria-hidden="true" />}
|
||||||
|
>
|
||||||
|
{t('contacts.moveFolder', 'Verschieben')}
|
||||||
|
</Button>
|
||||||
|
{moveFolderOpen && (
|
||||||
|
<div className="absolute right-0 top-full mt-1 bg-white border border-secondary-200 rounded-lg shadow-lg z-20 min-w-[200px] max-h-[300px] overflow-y-auto">
|
||||||
|
<button
|
||||||
|
className="w-full text-left px-3 py-2 text-sm hover:bg-secondary-50 border-b border-secondary-100"
|
||||||
|
onClick={() => handleMoveToFolder(null)}
|
||||||
|
>
|
||||||
|
{t('contacts.noFolder', 'Kein Ordner')}
|
||||||
|
</button>
|
||||||
|
{folders.map((f) => (
|
||||||
|
<button
|
||||||
|
key={f.id}
|
||||||
|
className="w-full text-left px-3 py-2 text-sm hover:bg-secondary-50 border-b border-secondary-100 last:border-b-0"
|
||||||
|
style={{ paddingLeft: `${(f.parent_id ? 24 : 12)}px` }}
|
||||||
|
onClick={() => handleMoveToFolder(f.id)}
|
||||||
|
>
|
||||||
|
<FolderOpen className="h-3.5 w-3.5 inline mr-1.5 text-primary-500" />
|
||||||
|
{f.name}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
{folders.length === 0 && (
|
||||||
|
<div className="px-3 py-2 text-xs text-secondary-400">{t('contacts.noFolders', 'Keine Ordner')}</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{canAccess('contacts:write') && onEdit && (
|
{canAccess('contacts:write') && onEdit && (
|
||||||
<Button variant="ghost" size="sm" onClick={onEdit}>{t('common.edit')}</Button>
|
<Button variant="ghost" size="sm" onClick={onEdit}>{t('common.edit')}</Button>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -54,7 +54,6 @@ function getIcon(name: string): React.ReactNode {
|
|||||||
const singleItems: NavSingleItem[] = [
|
const singleItems: NavSingleItem[] = [
|
||||||
{ to: '/dashboard', labelKey: 'nav.dashboard', icon: <Home className="w-5 h-5 flex-shrink-0" aria-hidden="true" strokeWidth={2} />, order: 0 },
|
{ to: '/dashboard', labelKey: 'nav.dashboard', icon: <Home className="w-5 h-5 flex-shrink-0" aria-hidden="true" strokeWidth={2} />, order: 0 },
|
||||||
{ to: '/contacts', labelKey: 'nav.contacts', icon: <Users className="w-5 h-5 flex-shrink-0" aria-hidden="true" strokeWidth={2} />, order: 10 },
|
{ to: '/contacts', labelKey: 'nav.contacts', icon: <Users className="w-5 h-5 flex-shrink-0" aria-hidden="true" strokeWidth={2} />, order: 10 },
|
||||||
{ to: '/wiki', labelKey: 'nav.wiki', icon: <BookOpen className="w-5 h-5 flex-shrink-0" aria-hidden="true" strokeWidth={2} />, order: 30 },
|
|
||||||
{ to: '/system-dashboard', labelKey: 'nav.systemDashboard', icon: <Activity className="w-5 h-5 flex-shrink-0" aria-hidden="true" strokeWidth={2} />, order: 90 },
|
{ to: '/system-dashboard', labelKey: 'nav.systemDashboard', icon: <Activity className="w-5 h-5 flex-shrink-0" aria-hidden="true" strokeWidth={2} />, order: 90 },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import { EmptyState } from '@/components/ui/EmptyState';
|
|||||||
interface WikiBrowserProps {
|
interface WikiBrowserProps {
|
||||||
selectedArticleId: string | null;
|
selectedArticleId: string | null;
|
||||||
onSelectArticle: (article: WikiArticle) => void;
|
onSelectArticle: (article: WikiArticle) => void;
|
||||||
|
refreshKey?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface CategoryNode {
|
interface CategoryNode {
|
||||||
@@ -110,7 +111,7 @@ function CategoryNodeItem({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function WikiBrowser({ selectedArticleId, onSelectArticle }: WikiBrowserProps) {
|
export function WikiBrowser({ selectedArticleId, onSelectArticle, refreshKey }: WikiBrowserProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [categories, setCategories] = useState<WikiCategory[]>([]);
|
const [categories, setCategories] = useState<WikiCategory[]>([]);
|
||||||
const [articles, setArticles] = useState<WikiArticle[]>([]);
|
const [articles, setArticles] = useState<WikiArticle[]>([]);
|
||||||
@@ -135,7 +136,7 @@ export function WikiBrowser({ selectedArticleId, onSelectArticle }: WikiBrowserP
|
|||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
}, [selectedCategoryId, search, t]);
|
}, [selectedCategoryId, search, t, refreshKey]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
void load();
|
void load();
|
||||||
|
|||||||
@@ -154,7 +154,7 @@ export function Window({ window: win }: WindowProps) {
|
|||||||
win.aiChatVisible && 'border-r border-secondary-200'
|
win.aiChatVisible && 'border-r border-secondary-200'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<ContentComponent {...win.componentProps} />
|
<ContentComponent {...win.componentProps} windowId={win.id} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* AI Chat Panel */}
|
{/* AI Chat Panel */}
|
||||||
|
|||||||
@@ -89,6 +89,10 @@ type ConversationCategory = 'system' | 'ai' | 'colleague';
|
|||||||
// ─── Helpers ───
|
// ─── Helpers ───
|
||||||
|
|
||||||
function categorizeConversation(conv: Conversation): ConversationCategory {
|
function categorizeConversation(conv: Conversation): ConversationCategory {
|
||||||
|
// Check metadata first (most reliable for AI/system chats)
|
||||||
|
const convType = conv.metadata?.conversation_type;
|
||||||
|
if (convType === 'system') return 'system';
|
||||||
|
if (convType === 'ai') return 'ai';
|
||||||
// System messages: created by system participant type
|
// System messages: created by system participant type
|
||||||
if (conv.participants.some(p => p.participant_type === 'system')) return 'system';
|
if (conv.participants.some(p => p.participant_type === 'system')) return 'system';
|
||||||
// AI conversations: have an AI participant
|
// AI conversations: have an AI participant
|
||||||
@@ -136,12 +140,13 @@ async function sendMessage(convId: string, content: string, replyToId?: string):
|
|||||||
return r.data;
|
return r.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function createConversation(title: string | null, participantIds: string[], isDirect: boolean, initialMessage?: string): Promise<Conversation> {
|
async function createConversation(title: string | null, participantIds: string[], isDirect: boolean, initialMessage?: string, metadata?: Record<string, unknown>): Promise<Conversation> {
|
||||||
const r = await apiClient.post('/comm/conversations', {
|
const r = await apiClient.post('/comm/conversations', {
|
||||||
title,
|
title,
|
||||||
participant_ids: participantIds,
|
participant_ids: participantIds,
|
||||||
is_direct: isDirect,
|
is_direct: isDirect,
|
||||||
initial_message: initialMessage,
|
initial_message: initialMessage,
|
||||||
|
metadata: metadata || {},
|
||||||
});
|
});
|
||||||
return r.data;
|
return r.data;
|
||||||
}
|
}
|
||||||
@@ -781,7 +786,11 @@ export function CommunicationPage() {
|
|||||||
try {
|
try {
|
||||||
const category = showNewChat;
|
const category = showNewChat;
|
||||||
const isDirect = category === 'colleague' && participantIds.length === 1;
|
const isDirect = category === 'colleague' && participantIds.length === 1;
|
||||||
const conv = await createConversation(title || null, participantIds, isDirect);
|
const metadata: Record<string, unknown> =
|
||||||
|
category === 'ai' ? { conversation_type: 'ai' } :
|
||||||
|
category === 'system' ? { conversation_type: 'system' } :
|
||||||
|
{};
|
||||||
|
const conv = await createConversation(title || null, participantIds, isDirect, undefined, metadata);
|
||||||
setConversations(prev => [...prev, conv]);
|
setConversations(prev => [...prev, conv]);
|
||||||
setActiveConvId(conv.id);
|
setActiveConvId(conv.id);
|
||||||
setMobileView('chat');
|
setMobileView('chat');
|
||||||
|
|||||||
@@ -80,6 +80,7 @@ export function WikiPage() {
|
|||||||
const [deleteTarget, setDeleteTarget] = useState<WikiArticle | null>(null);
|
const [deleteTarget, setDeleteTarget] = useState<WikiArticle | null>(null);
|
||||||
const [deleting, setDeleting] = useState(false);
|
const [deleting, setDeleting] = useState(false);
|
||||||
const [restoring, setRestoring] = useState(false);
|
const [restoring, setRestoring] = useState(false);
|
||||||
|
const [browserRefreshKey, setBrowserRefreshKey] = useState(0);
|
||||||
|
|
||||||
const loadCategories = useCallback(async () => {
|
const loadCategories = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
@@ -163,6 +164,7 @@ export function WikiPage() {
|
|||||||
toast.success(t('knowledge.editor.created'));
|
toast.success(t('knowledge.editor.created'));
|
||||||
}
|
}
|
||||||
setEditorOpen(false);
|
setEditorOpen(false);
|
||||||
|
setBrowserRefreshKey((k) => k + 1);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toast.error(err instanceof Error ? err.message : t('knowledge.editor.saveError'));
|
toast.error(err instanceof Error ? err.message : t('knowledge.editor.saveError'));
|
||||||
} finally {
|
} finally {
|
||||||
@@ -179,6 +181,7 @@ export function WikiPage() {
|
|||||||
setArticle(null);
|
setArticle(null);
|
||||||
setSelectedArticleId(null);
|
setSelectedArticleId(null);
|
||||||
setDeleteTarget(null);
|
setDeleteTarget(null);
|
||||||
|
setBrowserRefreshKey((k) => k + 1);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toast.error(err instanceof Error ? err.message : t('knowledge.editor.deleteError'));
|
toast.error(err instanceof Error ? err.message : t('knowledge.editor.deleteError'));
|
||||||
} finally {
|
} finally {
|
||||||
@@ -237,7 +240,7 @@ export function WikiPage() {
|
|||||||
<div className="grid grid-cols-1 lg:grid-cols-4 gap-4 h-[calc(100vh-8rem)]">
|
<div className="grid grid-cols-1 lg:grid-cols-4 gap-4 h-[calc(100vh-8rem)]">
|
||||||
{/* Browser sidebar */}
|
{/* Browser sidebar */}
|
||||||
<div className="lg:col-span-1 border border-secondary-200 rounded-lg bg-white overflow-hidden">
|
<div className="lg:col-span-1 border border-secondary-200 rounded-lg bg-white overflow-hidden">
|
||||||
<WikiBrowser selectedArticleId={selectedArticleId} onSelectArticle={handleSelectArticle} />
|
<WikiBrowser selectedArticleId={selectedArticleId} onSelectArticle={handleSelectArticle} refreshKey={browserRefreshKey} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Article detail / editor */}
|
{/* Article detail / editor */}
|
||||||
|
|||||||
Reference in New Issue
Block a user