Redesign doc service UX for scale + add group-based document sharing

- Three-column layout: Sidebar + SourcePanel (views + searchable category tree) + main
- DocumentSlideOver (480px right panel): inline editing, type picker, AI suggestion confirm/reject,
  categories combobox, tags editor, sharing section, raw text, re-analyse/delete actions
- ManageCategoriesDialog: inline rename, delete with confirm, search filter
- DocumentsPage rewrite: filter chip system, multi-file upload queue, drag-and-drop overlay,
  bulk actions bar (share/delete), smart TanStack Query polling, URL-driven view state
- Sidebar simplified: per-category NavLinks removed; Documents = single NavLink under Apps
- Backend: document_shares table (migration 0004), share CRUD endpoints, shared-with-me view,
  N+1-safe share_count via GROUP BY, recipient download access, X-User-Groups header enforcement
- Gateway proxy: injects X-User-Groups header into all document + category proxy requests
- Backend users: GET /api/users/me/groups endpoint for share picker combobox
- CLAUDE.md, STATUS.md files, and changelog updated

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
curo1305
2026-04-18 12:46:43 +02:00
parent 08e7caac4c
commit 94901fc30f
23 changed files with 2603 additions and 900 deletions
+102 -110
View File
@@ -40,138 +40,127 @@ All API calls go through `src/api/client.ts` (single Axios instance, JWT injecte
### Home dashboard (`/`)
Personalised landing page per user:
- Time-aware greeting with the user's display name (`full_name` or email). React JSX text rendering HTML-escapes all values — no `dangerouslySetInnerHTML` is used anywhere on this page.
- Grid of **pinned app cards** drawn from `GET /api/services`, filtered to the user's saved list.
- **Customize mode** (pencil button): shows all services; `+` / `` toggle buttons on each card; changes committed with **Save** via `PATCH /api/users/me/preferences`.
- Empty-state prompt when no apps are pinned.
- Time-aware greeting with the user's display name
- Grid of **pinned app cards** from `GET /api/services`, filtered to user's saved list
- **Customize mode** (pencil button): shows all services; `+` / `` toggle; commits via `PATCH /api/users/me/preferences`
### Apps page (`/apps`)
Cards are rendered dynamically from `GET /api/services` (polled every 30 s via TanStack Query):
- **healthy=true + app_path set** — clickable card with "Available" badge
- **healthy=true + no app_path** — non-clickable card (e.g. AI Service — no user UI)
- **healthy=false** — non-clickable, dimmed card with "Unavailable" badge and explanation text
- Single **Settings** button per card — visible to global admins OR members of the service's admin group (checked via `GET /api/plugins` which backend filters by access). Links to `svc.settings_path`.
Cards from `GET /api/services` (polled every 30 s):
- healthy + app_path clickable card with "Available" badge
- healthy + no app_path non-clickable card
- unhealthy → dimmed, non-clickable, "Unavailable"
- Settings button visible to admins and service-admin group members
### Sidebar navigation
`Apps` is an expandable accordion in the sidebar:
- **Documents** sub-item (expandable) — lists all user categories beneath it; clicking a category navigates to `/apps/documents?category_id=<id>`
- AI Service is not listed (no openable UI)
- Sections auto-open when navigating to their route
- In collapsed (icons-only) mode, clicking the Apps icon navigates to `/apps`
`Apps` expandable accordion: **Documents** single NavLink to `/apps/documents`. Category navigation moved to SourcePanel (only visible on `/apps/documents` route). Admin section (Users, Groups, Appearance) for admins. Collapsible to icon-only mode.
### Documents page (`/apps/documents`)
### Documents page (`/apps/documents`) — three-column layout
**Upload:** PDF file input, 202 response, error display.
**SourcePanel** (240px, left): Appears only on `/apps/documents`.
- Views: All Documents / Mine / Shared with me (URL param `?view=`)
- Category tree with client-side search (searchable when > 4 categories)
- Inline new category form
- "Manage categories" button opens `ManageCategoriesDialog`
**Filter bar:**
- Search input (400ms debounce) — matches title, filename, tags, document_type
- Status dropdown (all / pending / processing / done / failed)
- Type dropdown (all / invoice / bill / receipt / order / expense / revenue / unknown)
- Sort selector (upload date / processed date / title / filename / file size / type / status)
- Asc/Desc toggle
- "Clear filters" button (appears when any filter is active)
**Toolbar:** Debounced search input (400ms) + filter chips system.
- Filter chips: Status, Document type, Category (each adds a removable chip)
- "Add filter" button opens a two-step picker (dimension → value)
- Sort via clickable column headers (↑/↓ chevron)
**Pagination:** Prev/Next with "XY of Z" count. Only shown when total > per_page.
**Compact table rows:**
- Columns: checkbox | title/filename | type | status dot | categories (2 + overflow) | sharing icon | date | size | 3-dot actions
- Row click opens `DocumentSlideOver`
- Shared-with-me rows show a primary border accent
**Document row (collapsed):**
- Inline title editor (pencil icon, Enter to save, Esc to cancel; shows filename in italic when no title)
- Status badge (colour-coded)
- Document type label
- File size
- View button (opens PDF in new tab via blob URL — auth-gated)
- Download button
- Delete button (confirm dialog)
**DocumentSlideOver** (480px, right slide-over):
- Metadata (status dot, size, dates, source)
- Inline title edit (pencil icon)
- Type picker (chips for each doc type)
- **AI Suggestions** — folder and filename confirm/reject buttons (was missing before, now implemented)
- Extracted data key-value table
- Categories multi-select combobox (search-to-filter)
- AI-suggested categories with Assign / Create & Assign actions
- Tags chip editor (add/remove inline)
- **Sharing section** (owner only): lists groups the doc is shared with; group picker combobox (filtered to user's own groups); remove share button
- Raw text section (collapsed by default)
- Re-analyse / Delete actions (owner only)
**Document row (expanded):**
- **Extracted data table** — all AI-extracted JSON fields (excludes `tags`, `suggested_categories`)
- **Error message** — shown if status=failed
- **Categories** — assigned chips with remove; dropdown to assign existing; AI-suggested chips with Accept / Create & Assign / Dismiss
- **Status polling** — auto-refetches every 3s while status is pending/processing; invalidates document list on done/failed
**Bulk actions bar** (floating, bottom center, owner view only):
- Appears when any rows are checked
- Share with group (opens group picker → shares all selected)
- Delete (confirm dialog)
- Clear selection
**Upload experience:**
- Full-page drag-and-drop overlay (activates on `dragenter`)
- Multi-file upload (iterates all selected/dropped files)
- Bottom-right upload queue panel (collapsible toast) with per-file status + "Review →" link after upload
**Document sharing:**
- Owner shares doc with any of their own groups from the slide-over
- Recipient sees shared docs in "Shared with me" view
- Recipient can View + Download only (no edit/delete/share)
- `share_count` indicator (Users icon) in table rows
**Polling:** List query refetches every 3s automatically when any visible doc is pending/processing (single query, not per-document). Uses TanStack Query `refetchInterval` function.
### AI Service Settings (`/apps/ai/settings`)
Accessible to global admins and `ai-service-admin` group members (`ServiceAdminRoute`).
- Provider selector (lmstudio / ollama / anthropic)
- Per-provider fields (base URL, model, API key)
- Test Connection button (`POST /api/settings/ai/test`)
- Save button
Provider selector, per-provider fields, Test Connection, Save.
### Document Service Settings (`/apps/documents/settings`)
Accessible to global admins and `doc-service-admin` group members (`ServiceAdminRoute`).
Combined settings on one page, accessed via the single "Settings" button on the app card:
- **Upload Limits** — max PDF size in MB (`GET/PATCH /api/settings/documents/limits`)
- **Watch Directory** — file watcher config rendered via `PluginSchemaForm` from manifest (`GET/PATCH /api/plugins/doc-service/settings`)
Upload limits + watch directory config.
### Admin — Users page (`/admin/users`)
- User list with role and active status
- Inline active status toggle
- Create user form (email, name, password, admin flag)
- Delete user
User list, toggle active, create user, delete user.
### Admin — Groups page (`/admin/groups`)
- Group list with name, description, member count
- Create group (name, optional description)
- Edit group name / description inline panel
- Delete group (with confirmation)
- Expand group row to manage members: view members, remove members, add non-members from dropdown
Group list, create, edit name/description, delete, add/remove members.
### Profile page (`/profile`)
- Display and edit personal information
Display and edit personal information.
---
## API client (`src/api/client.ts`)
Key functions:
Key document-related functions:
| Function | Description |
|----------|-------------|
| `listDocuments(params)` | `GET /documents`returns `DocumentPage`; supports `category_id` filter |
| `listDocuments(params)` | `GET /documents`paginated with filters |
| `listSharedWithMe(params)` | `GET /documents/shared-with-me` |
| `uploadDocument(file)` | `POST /documents/upload` |
| `deleteDocument(id)` | `DELETE /documents/{id}` |
| `downloadDocument(id, filename)` | Blob URL download |
| `viewDocument(id)` | Blob URL → `window.open`, auto-revoke after 60s |
| `getDocumentStatus(id)` | Poll endpoint |
| `listCategories()` | All categories for user |
| `createCategory(name)` | Create category |
| `assignCategory(docId, catId)` | Assign |
| `removeCategory(docId, catId)` | Remove |
| `updateDocumentTags(id, tags)` | `PATCH /documents/{id}/tags` |
| `updateDocumentTitle(id, title)` | `PATCH /documents/{id}/title` |
| `confirmFolderSuggestion(docId)` | `POST /documents/{id}/suggestions/folder/confirm` |
| `rejectFolderSuggestion(docId)` | `POST /documents/{id}/suggestions/folder/reject` |
| `confirmFilenameSuggestion(docId)` | `POST /documents/{id}/suggestions/filename/confirm` |
| `rejectFilenameSuggestion(docId)` | `POST /documents/{id}/suggestions/filename/reject` |
| `getAISettings()` | `GET /settings/ai` (masked) |
| `updateAISettings(data)` | `PATCH /settings/ai` |
| `testAIConnection()` | `POST /settings/ai/test` |
| `getDocumentLimits()` | `GET /settings/documents/limits` |
| `adminListGroups()` | `GET /admin/groups` |
| `adminCreateGroup(data)` | `POST /admin/groups` |
| `adminGetGroup(id)` | `GET /admin/groups/{id}` with members |
| `adminUpdateGroup(id, data)` | `PATCH /admin/groups/{id}` |
| `adminDeleteGroup(id)` | `DELETE /admin/groups/{id}` |
| `adminAddGroupMember(gId, uId)` | `POST /admin/groups/{gId}/members/{uId}` |
| `adminRemoveGroupMember(gId, uId)` | `DELETE /admin/groups/{gId}/members/{uId}` |
| `updateDocumentLimits(data)` | `PATCH /settings/documents/limits` |
| `getPlugins()` | `GET /plugins` — list accessible plugins |
| `getPluginManifest(id)` | `GET /plugins/{id}/manifest` |
| `getPluginSettings(id)` | `GET /plugins/{id}/settings` |
| `updatePluginSettings(id, data)` | `PATCH /plugins/{id}/settings` |
| `viewDocument(id)` | Blob URL → new tab, 60s revoke |
| `getDocumentShares(docId)` | `GET /documents/{id}/shares` |
| `addDocumentShare(docId, groupId)` | `POST /documents/{id}/shares` |
| `removeDocumentShare(docId, groupId)` | `DELETE /documents/{id}/shares/{group_id}` |
| `getMyGroups()` | `GET /users/me/groups` (for share picker) |
| `confirmFolderSuggestion(docId)` | Apply AI folder suggestion |
| `rejectFolderSuggestion(docId)` | Dismiss AI folder suggestion |
| `confirmFilenameSuggestion(docId)` | Apply AI filename suggestion |
| `rejectFilenameSuggestion(docId)` | Dismiss AI filename suggestion |
---
## State management
- **TanStack Query** — all server state; `queryKey: ["documents", params]` for cache isolation per filter/page combination
- **No global store** — local `useState` for UI-only state (editing mode, filter params, etc.)
- **Token** — `localStorage`, read by `useAuth` hook, injected by Axios interceptor
- **TanStack Query** — all server state
- `["documents", params]` — owned doc list (refetchInterval when pending/processing)
- `["documents-shared", params]` — shared-with-me list
- `["categories"]` — all user categories (shared across SourcePanel + DocumentSlideOver)
- `["document-shares", docId]` — shares for a specific document
- `["my-groups"]` — current user's group memberships
- **URL search params** — `view`, `page`, `sort`, `order`, `search`, `status`, `document_type`, `category_id`
- **Local `useState`** — UI-only state (drag, upload queue, active doc ID, selected IDs, slide-over open)
---
@@ -179,37 +168,40 @@ Key functions:
| Component | Path | Description |
|-----------|------|-------------|
| `AppShell` | `src/components/AppShell.tsx` | Layout wrapper: Sidebar + scrollable main content |
| `Sidebar` | `src/components/Sidebar.tsx` | Collapsible left nav; includes dynamic "Extensions" section |
| `ThemeToggle` | `src/components/ThemeToggle.tsx` | Sun/moon ghost icon button; persists to localStorage |
| `PluginSchemaForm` | `src/components/PluginSchemaForm.tsx` | JSON Schema → React form (boolean/string/number/readOnly fields) |
| `PluginSettingsPage` | `src/pages/PluginSettingsPage.tsx` | Generic plugin settings page (manifest-driven) |
| `Button` | `src/components/ui/button.tsx` | shadcn/ui Button (default, ghost, outline, destructive) |
| `AppShell` | `src/components/AppShell.tsx` | Layout: Sidebar + SourcePanel (on /apps/documents) + main |
| `Sidebar` | `src/components/Sidebar.tsx` | Collapsible left nav (categories removed, replaced by SourcePanel) |
| `SourcePanel` | `src/components/SourcePanel.tsx` | Views + searchable category tree (docs route only) |
| `ManageCategoriesDialog` | `src/components/ManageCategoriesDialog.tsx` | Category CRUD modal |
| `DocumentSlideOver` | `src/components/DocumentSlideOver.tsx` | Right slide-over: detail, edit, share, AI suggestions |
| `ThemeToggle` | `src/components/ThemeToggle.tsx` | Sun/moon toggle |
| `PluginSchemaForm` | `src/components/PluginSchemaForm.tsx` | JSON Schema → React form |
| `Button` | `src/components/ui/button.tsx` | shadcn/ui Button |
| `Input` | `src/components/ui/input.tsx` | shadcn/ui Input |
---
## Known limitations / not implemented
- **JWT in `localStorage`** — XSS risk; migrate to `httpOnly` cookie when backend supports it
- **No toast / notification system** — errors shown inline; success is silent
- **No loading skeletons** — "Loading…" text only
- **No app permission UI** per group — groups exist but permission grants are not yet implemented
- **No app permission UI** — all apps visible to all authenticated users
- **No loading skeletons** — spinner only
- **Raw text not in DocumentOut** — slide-over shows a placeholder; full text requires direct backend API call
---
## Future work
- [x] UI component library: shadcn/ui + Tailwind CSS — installed and wired up
- [x] AppShell + Sidebar replacing inline Nav component
- [x] Light/dark theme context with OS preference detection
- [x] Generic plugin infrastructure: Extensions sidebar section, PluginSchemaForm, PluginSettingsPage
- [ ] Suggestion badges in DocumentsPage for `suggested_folder` / `suggested_filename` (confirm/reject buttons)
- [ ] Toast notification system (upload success, save feedback, errors)
- [x] SourcePanel with views + searchable category navigation
- [x] DocumentSlideOver replacing expand-in-row
- [x] Filter chip system
- [x] Multi-file upload with queue panel + drag-and-drop
- [x] Bulk actions bar (share, delete)
- [x] Document sharing UI (Sharing section + Shared with me view)
- [x] AI suggestion confirm/reject UI (folder + filename)
- [x] Groups admin UI
- [ ] Toast notification system
- [ ] Loading skeletons
- [ ] `POST /queue/jobs` integration — show AI processing queue status / progress per document
- [ ] Advanced filter: extracted data fields (vendor, due date, amount) — needs backend support
- [x] Groups admin UI — list, create, edit, delete, add/remove members
- [ ] App permissions UI per group (blocked on backend group_app_permissions)
- [ ] Document sharing UI (blocked on backend)
- [ ] Cmd+K global search (`CommandDialog`)
- [ ] Advanced filter: extracted data fields (needs backend support)
- [ ] `httpOnly` cookie auth (requires backend change)
- [ ] Bulk document operations (select multiple, bulk delete / bulk categorise)
- [ ] TanStack Virtual for category list > 200 items