diff --git a/CHANGELOG.md b/CHANGELOG.md index c9d04bda..92b34b1a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - [todo] Added server-side deletion for shared lists - when a shared list is deleted, it's removed from the backend. - [todo] Added cross-user deletion detection - automatically removes shared lists from all users when deleted by another user. - [todo] Added notification when a shared list is deleted by another user, with automatic list selection fallback. +- [todo] Added categories history tracking - all used categories are now stored in a persistent list similar to todo history. +- [todo] Added TodoCategories component displaying all categories with individual delete buttons and clear all functionality, positioned side-by-side with history on larger screens and stacked on mobile. Category tags and delete button are also responsive (side-by-side on desktop, stacked on mobile). +- [todo] Added Tab key autocomplete for category input fields using existing categories with visual hint showing matched category. +- [todo] Added click-to-insert functionality - clicking a category from the history inserts it into the focused category input field. - [memorandum] Added "Copy link URL" option to link context menu for easy URL copying to clipboard. - [memorandum] Added arrow-based reordering system with up/down buttons and "Move to Top/Bottom" context menu options for folders and links. - [memorandum] Added editable preset overlay with JSON validation, allowing direct editing of preset configuration with real-time validation and sync to localStorage/cloud. @@ -27,6 +31,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- [todo] Added collapsible input section on mobile - a chevron toggle button next to the list name (visible only on phone) allows hiding/showing the History/Categories/Toggle/Input controls, giving more screen space to the todo list. +- [todo] Refactored history and categories from inline accordions to modal dialogs for cleaner UI and better mobile experience. +- [todo] Improved layout - todo list now takes remaining vertical space and scrolls independently with fixed header controls. +- [todo] Redesigned TodoInputSection as static section with modal trigger buttons instead of accordion. +- [todo] Refactored TodoItemList to display as modal component instead of inline accordion. +- [todo] Redesigned clear history and clear categories buttons - moved to modal with dedicated "Clear all" button. - [backend] Updated all backend dependencies to latest compatible versions: NX 20.8.2 → 22.5.2, TypeScript 5.8.3 → 5.9.3, @swc/core 1.11.x → 1.15.13, NestJS packages to 11.1.14, Fastify 5.0.0 → 5.7.4, pino-http 10.2.0 → 11.0.0, cron 3.x → 4.4.0, and 50+ other packages. - [backend] Upgraded Mongoose from 8.1.1 to 9.2.2 with complete API migration (FilterQuery → mongodb.Filter) across 10 files. - [backend] Upgraded Jest from 29.7.0 to 30.2.0 with API migration (toThrowError → toThrow). @@ -71,11 +81,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Removed +- [todo] Removed `amount` field from todo list entries completely - frontend types, components, input fields, translations, backend DTO, Mongoose schemas, and test data. - [global] Removed Renovate bot - dependencies will be managed manually going forward. - [memorandum] Removed drag-and-drop functionality for folders and links. ### Fixed +- [todo] Fixed double checkbox in todo items - removed redundant hidden native input that caused `todoChecked` to fire twice on click. +- [todo] Fixed History and Categories modals opening simultaneously - each modal now closes the other before opening. +- [todo] Fixed category click-to-insert from Categories modal not working - modal focus steal caused focus-based detection to fail; replaced with explicit context tracking (`lastCategoryInputContext`) captured before the modal opens. + - [frontend] Fixed Safari/iPhone "Cannot access uninitialized variable" error by refactoring applications.ts to eliminate top-level await and use Proxy pattern with async initialization. - [backend] Fixed 5 security vulnerabilities through dependency updates (21 → 16 vulnerabilities, 24% reduction). - [backend] Fixed TypeScript 5.9 stricter type checking in OCR service (Buffer → Uint8Array conversion for Blob constructor). diff --git a/backend/libs/shared/common/types/src/lib/todo.dto.ts b/backend/libs/shared/common/types/src/lib/todo.dto.ts index ce4dfb1f..d759247f 100644 --- a/backend/libs/shared/common/types/src/lib/todo.dto.ts +++ b/backend/libs/shared/common/types/src/lib/todo.dto.ts @@ -17,11 +17,6 @@ export class TodoItemDto { @IsBoolean() done?: boolean; - @ApiProperty({ description: 'Todo item amount', required: false }) - @IsOptional() - @IsString() - amount?: string; - @ApiProperty({ description: 'Todo item category', required: false }) @IsOptional() @IsString() @@ -53,6 +48,11 @@ export class SharedTodoListDto { @IsArray() history?: string[]; + @ApiProperty({ description: 'Categories used in todos', type: [String], required: false }) + @IsOptional() + @IsArray() + categories?: string[]; + @ApiProperty({ description: 'Version for optimistic locking' }) @IsNumber() version: number; @@ -116,6 +116,11 @@ export class UpdateSharedTodoListInputDto { @IsArray() history?: string[]; + @ApiProperty({ description: 'Categories used in todos', type: [String], required: false }) + @IsOptional() + @IsArray() + categories?: string[]; + @ApiProperty({ description: 'Current version for optimistic locking' }) @IsNumber() version: number; diff --git a/backend/libs/shared/provider/todo/src/lib/schema/todo.schema.ts b/backend/libs/shared/provider/todo/src/lib/schema/todo.schema.ts index 602f16ff..cedeb145 100644 --- a/backend/libs/shared/provider/todo/src/lib/schema/todo.schema.ts +++ b/backend/libs/shared/provider/todo/src/lib/schema/todo.schema.ts @@ -13,9 +13,6 @@ class TodoItem { @Prop({ required: false }) done?: boolean; - @Prop({ required: false }) - amount?: string; - @Prop({ required: false }) category?: string; } @@ -36,6 +33,9 @@ export class SharedTodoList { @Prop({ type: [String], required: false, default: [] }) history?: string[]; + @Prop({ type: [String], required: false, default: [] }) + categories?: string[]; + @Prop({ type: Number, required: true, default: 1 }) version!: number; diff --git a/backend/libs/todo/todo-controller/src/lib/todo.controller.spec.ts b/backend/libs/todo/todo-controller/src/lib/todo.controller.spec.ts index 1e22df4e..d12f5d7c 100644 --- a/backend/libs/todo/todo-controller/src/lib/todo.controller.spec.ts +++ b/backend/libs/todo/todo-controller/src/lib/todo.controller.spec.ts @@ -26,7 +26,6 @@ describe('TodoController', () => { id: 'todo-1', title: 'Test Todo', done: false, - amount: '1x', category: 'Test', }, ], @@ -157,7 +156,6 @@ describe('TodoController', () => { id: 'todo-1', title: 'Updated Todo', done: true, - amount: '2x', category: 'Updated', }, ], diff --git a/backend/libs/todo/todo-persistence/src/lib/schema/todo.schema.ts b/backend/libs/todo/todo-persistence/src/lib/schema/todo.schema.ts index 08a84550..a2ffd2bd 100644 --- a/backend/libs/todo/todo-persistence/src/lib/schema/todo.schema.ts +++ b/backend/libs/todo/todo-persistence/src/lib/schema/todo.schema.ts @@ -13,9 +13,6 @@ class TodoItem { @Prop({ required: false }) done?: boolean; - @Prop({ required: false }) - amount?: string; - @Prop({ required: false }) category?: string; } diff --git a/backend/libs/todo/todo-persistence/src/lib/todo-persistence.service.spec.ts b/backend/libs/todo/todo-persistence/src/lib/todo-persistence.service.spec.ts index 0019cf59..199f0b92 100644 --- a/backend/libs/todo/todo-persistence/src/lib/todo-persistence.service.spec.ts +++ b/backend/libs/todo/todo-persistence/src/lib/todo-persistence.service.spec.ts @@ -18,7 +18,6 @@ describe('TodoPersistenceService', () => { id: 'todo-1', title: 'Test Todo', done: false, - amount: '1x', category: 'Test', }, ], diff --git a/backend/libs/todo/todo-provider/src/lib/todo-provider.service.spec.ts b/backend/libs/todo/todo-provider/src/lib/todo-provider.service.spec.ts index b1d56192..731fefa3 100644 --- a/backend/libs/todo/todo-provider/src/lib/todo-provider.service.spec.ts +++ b/backend/libs/todo/todo-provider/src/lib/todo-provider.service.spec.ts @@ -22,7 +22,6 @@ describe('TodoProviderService', () => { id: 'todo-1', title: 'Test Todo', done: false, - amount: '1x', category: 'Test', }, ], @@ -208,7 +207,6 @@ describe('TodoProviderService', () => { id: 'todo-1', title: 'Updated Todo', done: true, - amount: '2x', category: 'Updated', }, ], diff --git a/frontend/package.json b/frontend/package.json index 9888c405..ab8c5894 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -53,4 +53,4 @@ "socket.io-client": "^4.8.1", "sveltekit-i18n": "^2.4.2" } -} +} \ No newline at end of file diff --git a/frontend/src/lib/api/todo.api.ts b/frontend/src/lib/api/todo.api.ts index f3e4088a..f24da395 100644 --- a/frontend/src/lib/api/todo.api.ts +++ b/frontend/src/lib/api/todo.api.ts @@ -38,6 +38,7 @@ export const updateSharedTodoList = async ( emoji: string; todos: any[]; history: string[]; + categories: string[]; version: number; }, ): Promise<{ status: number; data: SharedTodoListResponse | null }> => { diff --git a/frontend/src/lib/components/todo/Todo.svelte b/frontend/src/lib/components/todo/Todo.svelte index e61bba9e..a4db978b 100644 --- a/frontend/src/lib/components/todo/Todo.svelte +++ b/frontend/src/lib/components/todo/Todo.svelte @@ -15,16 +15,16 @@ id, title, done, - amount, category, + categories, deleteTodo, todoChecked, }: { id: string; title: string; done?: boolean; - amount?: string; category?: string; + categories: string[]; deleteTodo: () => void; todoChecked: () => void; } = $props(); @@ -32,21 +32,49 @@ // 4. STATE let isRenaming = $state(false); let newTitle = $state(''); - let newAmount = $state(''); let newCategory = $state(''); let todoObject: HTMLElement | undefined = $state(undefined); // 5. DERIVED let todoTitle = $derived(title); - let todoAmount = $derived(amount || '1x'); let todoCategory = $derived(category || ''); let isDone = $derived(done); + let categoryAutocomplete = $derived.by(() => { + if (!newCategory || newCategory.length === 0 || !isRenaming) return null; + + const matches = categories.filter((cat) => + cat.toLowerCase().startsWith(newCategory.toLowerCase()), + ); + + return matches.length > 0 ? matches[0] : null; + }); + // 8. FUNCTIONS + const autocompleteCategory = () => { + if (!newCategory) return; + + const matches = categories.filter((cat) => + cat.toLowerCase().startsWith(newCategory.toLowerCase()), + ); + + if (matches.length > 0) { + newCategory = matches[0]; + } + }; + + const handleCategoryKeydown = (e: KeyboardEvent) => { + if (e.key === 'Tab') { + e.preventDefault(); + autocompleteCategory(); + } else if (e.key === 'Enter') { + e.stopPropagation(); + saveEdit(); + } + }; const startEdit = () => { newTitle = todoTitle; - newAmount = todoAmount; newCategory = todoCategory; isRenaming = true; }; @@ -58,7 +86,6 @@ detail: { id: id, title: newTitle, - amount: newAmount, category: newCategory, }, }); @@ -70,14 +97,32 @@ const cancelEdit = () => { isRenaming = false; newTitle = ''; - newAmount = ''; newCategory = ''; }; // 7. LIFECYCLE + $effect(() => { + // Listen for category selection from category history + const handleCategorySelected = ((e: CustomEvent) => { + const context = e.detail.context; + const activeElement = document.activeElement as HTMLInputElement; + const wrapper = activeElement?.closest('[data-category-input="edit-todo"]'); + + if (isRenaming && (context === 'edit-todo' || wrapper)) { + newCategory = e.detail.category; + } + }) as EventListener; + + window.addEventListener('category-selected', handleCategorySelected); + + return () => { + window.removeEventListener('category-selected', handleCategorySelected); + }; + }); + $effect(() => { if (isRenaming) { - const handleKeyDown = (e) => { + const handleKeyDown = (e: KeyboardEvent) => { if (e.key === 'Escape') { cancelEdit(); } @@ -97,19 +142,6 @@ > {#if isRenaming}