T08a: Frontend DMS + Tags + Permissions UI — 33 tests, tsc clean, vite build pass

- DMS file browser: folder tree + file grid + upload dropzone + search + preview modal
- DMS share dialog: user/group share + public share links with password+expiry
- DMS bulk actions: bulk move + bulk delete with confirm dialogs
- DMS trash view: deleted files list with restore button
- Tags: TagPicker on company/contact detail pages (new tabs tab)
- Tags: TagCloud + BulkTagDialog for bulk tag assignment
- Permissions: share link creation, permission display, copy-link button
- API clients: dms.ts, tags.ts, permissions.ts
- Routes: /dms, /dms/trash added to router
- Sidebar: DMS nav link updated
- i18n: de.json + en.json translations for DMS/Tags/Permissions
- 33 new tests (5 test files), full regression 276/276 pass
- tsc --noEmit: 0 errors, vite build: 252 modules
This commit is contained in:
leocrm-bot
2026-07-01 16:54:32 +02:00
parent f646c597dc
commit 0962f3a961
31 changed files with 3368 additions and 41 deletions
+200
View File
@@ -0,0 +1,200 @@
/**
* DMS plugin API client.
*
* All requests use the shared `apiClient` (`baseURL: '/api/v1'`) and target the
* DMS plugin routes under `/dms/...`.
*/
import { apiClient, apiDelete, apiGet, apiPatch, apiPost } from './client';
// ─── Types ─────────────────────────────────────────────────────────────────
export interface DmsFolder {
id: string;
name: string;
parent_id: string | null;
created_by: string;
created_at?: string | null;
updated_at?: string | null;
children?: DmsFolder[];
file_count?: number;
}
export interface DmsFile {
id: string;
folder_id: string | null;
name: string;
mime_type: string;
size: number;
storage_path: string;
checksum?: string | null;
deleted_at: string | null;
created_by: string;
created_at?: string | null;
updated_at?: string | null;
shared_with?: string[];
permissions?: FilePermission[];
}
export interface FilePermission {
id: string;
file_id: string;
user_id: string | null;
group_id: string | null;
permission: 'read' | 'write' | 'admin';
created_at?: string | null;
}
export interface DmsShare {
id: string;
file_id: string;
user_id?: string | null;
group_id?: string | null;
permission: 'read' | 'write';
created_at?: string | null;
}
export interface ShareLink {
id: string;
file_id: string;
token: string;
url: string;
password_protected: boolean;
expires_at: string | null;
created_at?: string | null;
}
export interface EditSession {
id: string;
file_id: string;
session_token: string;
editor_url: string;
expires_at: string;
}
export interface SearchResult {
files: DmsFile[];
total: number;
}
// ─── Payloads ──────────────────────────────────────────────────────────────
export interface CreateFolderPayload {
name: string;
parent_id?: string | null;
}
export interface UpdateFolderPayload {
name?: string;
parent_id?: string | null;
}
export interface UpdateFilePayload {
name?: string;
folder_id?: string | null;
}
export interface SharePayload {
user_id?: string;
group_id?: string;
permission: 'read' | 'write';
}
export interface BulkMovePayload {
file_ids: string[];
target_folder_id: string | null;
}
export interface BulkDeletePayload {
file_ids: string[];
}
export interface ShareLinkPayload {
password?: string;
expires_at?: string | null;
}
// ─── Folders ───────────────────────────────────────────────────────────────
export function fetchFolders(): Promise<DmsFolder[]> {
return apiGet<DmsFolder[]>('/dms/folders');
}
export function createFolder(payload: CreateFolderPayload): Promise<DmsFolder> {
return apiPost<DmsFolder>('/dms/folders', payload);
}
export function updateFolder(folderId: string, payload: UpdateFolderPayload): Promise<DmsFolder> {
return apiPatch<DmsFolder>(`/dms/folders/${folderId}`, payload);
}
export function deleteFolder(folderId: string): Promise<void> {
return apiDelete<void>(`/dms/folders/${folderId}`);
}
// ─── Files ─────────────────────────────────────────────────────────────────
export function uploadFile(
file: File,
folderId?: string | null
): Promise<DmsFile> {
const form = new FormData();
form.append('file', file);
if (folderId) form.append('folder_id', folderId);
return apiPost<DmsFile>('/dms/files/upload', form, {
headers: { 'Content-Type': 'multipart/form-data' },
});
}
export function getFile(fileId: string): Promise<DmsFile> {
return apiGet<DmsFile>(`/dms/files/${fileId}`);
}
export function updateFile(fileId: string, payload: UpdateFilePayload): Promise<DmsFile> {
return apiPatch<DmsFile>(`/dms/files/${fileId}`, payload);
}
export function deleteFile(fileId: string): Promise<void> {
return apiDelete<void>(`/dms/files/${fileId}`);
}
export function restoreFile(fileId: string): Promise<DmsFile> {
return apiPost<DmsFile>(`/dms/files/${fileId}/restore`);
}
export function getFilePreviewUrl(fileId: string): string {
return `/api/v1/dms/files/${fileId}/preview`;
}
export function createEditSession(fileId: string): Promise<EditSession> {
return apiPost<EditSession>(`/dms/files/${fileId}/edit-session`);
}
export function shareFile(fileId: string, payload: SharePayload): Promise<DmsShare> {
return apiPost<DmsShare>(`/dms/files/${fileId}/share`, payload);
}
export function removeShare(fileId: string, shareId: string): Promise<void> {
return apiDelete<void>(`/dms/files/${fileId}/share`, { data: { share_id: shareId } });
}
// ─── Search & Shared ───────────────────────────────────────────────────────
export function searchFiles(query: string): Promise<SearchResult> {
const qs = new URLSearchParams({ q: query }).toString();
return apiGet<SearchResult>(`/dms/search?${qs}`);
}
export function getSharedWithMe(): Promise<DmsFile[]> {
return apiGet<DmsFile[]>('/dms/shared-with-me');
}
// ─── Bulk Operations ───────────────────────────────────────────────────────
export function bulkMoveFiles(payload: BulkMovePayload): Promise<void> {
return apiPost<void>('/dms/files/bulk-move', payload);
}
export function bulkDeleteFiles(payload: BulkDeletePayload): Promise<void> {
return apiPost<void>('/dms/files/bulk-delete', payload);
}