From 5c69dc7d5de49958f050d6f19895e683e99fc180 Mon Sep 17 00:00:00 2001 From: Till JS Date: Thu, 26 Mar 2026 13:39:44 +0100 Subject: [PATCH] feat(storage): add file tagging UI with TagPicker component Backend: Add endpoints for file-tag operations (GET/POST/DELETE) Frontend: TagPicker component with: - View/add/remove tags on files in FilePreviewModal - Create new tags inline with random color assignment - Dropdown with existing tags and create-new input - Colored tag pills with remove button Co-Authored-By: Claude Opus 4.6 (1M context) --- .../apps/backend/src/tag/tag.controller.ts | 17 + apps/storage/apps/web/src/lib/api/client.ts | 8 + .../components/files/FilePreviewModal.svelte | 3 + .../src/lib/components/files/TagPicker.svelte | 311 ++++++++++++++++++ 4 files changed, 339 insertions(+) create mode 100644 apps/storage/apps/web/src/lib/components/files/TagPicker.svelte diff --git a/apps/storage/apps/backend/src/tag/tag.controller.ts b/apps/storage/apps/backend/src/tag/tag.controller.ts index bb42a4126..9a0518b97 100644 --- a/apps/storage/apps/backend/src/tag/tag.controller.ts +++ b/apps/storage/apps/backend/src/tag/tag.controller.ts @@ -35,4 +35,21 @@ export class TagController { await this.tagService.delete(user.userId, id); return { success: true }; } + + @Get('file/:fileId') + async getFileTags(@Param('fileId') fileId: string) { + return this.tagService.getFileTags(fileId); + } + + @Post('file/:fileId/:tagId') + async addTagToFile(@Param('fileId') fileId: string, @Param('tagId') tagId: string) { + await this.tagService.addTagToFile(fileId, tagId); + return { success: true }; + } + + @Delete('file/:fileId/:tagId') + async removeTagFromFile(@Param('fileId') fileId: string, @Param('tagId') tagId: string) { + await this.tagService.removeTagFromFile(fileId, tagId); + return { success: true }; + } } diff --git a/apps/storage/apps/web/src/lib/api/client.ts b/apps/storage/apps/web/src/lib/api/client.ts index 71cb8ecde..6145d4c28 100644 --- a/apps/storage/apps/web/src/lib/api/client.ts +++ b/apps/storage/apps/web/src/lib/api/client.ts @@ -295,6 +295,14 @@ export const tagsApi = { }), delete: (id: string) => request<{ success: boolean }>(`/tags/${id}`, { method: 'DELETE' }), + + getFileTags: (fileId: string) => request(`/tags/file/${fileId}`), + + addToFile: (fileId: string, tagId: string) => + request<{ success: boolean }>(`/tags/file/${fileId}/${tagId}`, { method: 'POST' }), + + removeFromFile: (fileId: string, tagId: string) => + request<{ success: boolean }>(`/tags/file/${fileId}/${tagId}`, { method: 'DELETE' }), }; // Trash API diff --git a/apps/storage/apps/web/src/lib/components/files/FilePreviewModal.svelte b/apps/storage/apps/web/src/lib/components/files/FilePreviewModal.svelte index 61f716e9a..03f2e72ac 100644 --- a/apps/storage/apps/web/src/lib/components/files/FilePreviewModal.svelte +++ b/apps/storage/apps/web/src/lib/components/files/FilePreviewModal.svelte @@ -20,6 +20,7 @@ import { authStore } from '$lib/stores/auth.svelte'; import FileVersionsModal from './FileVersionsModal.svelte'; import { audioPlayerStore, getAudioFiles } from '$lib/stores/audio-player.svelte'; + import TagPicker from './TagPicker.svelte'; import { browser } from '$app/environment'; interface Props { @@ -324,6 +325,8 @@ {file.isFavorite ? 'Ja' : 'Nein'} + +