Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,42 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- [shared] Added reusable EmojiPicker component with inline expandable dropdown, bilingual keyword search (EN/DE), scrollable grid view, and categorized emoji selection covering 200+ common emojis.
- [todo] Integrated EmojiPicker into todo list creation and edit modals with vertical layout for easier emoji selection.
- [todo] Added shared todo list functionality enabling collaborative editing via unique shared IDs.
- [todo] Added API module (`todo.api.ts`) for shared list operations following project patterns.
- [todo] Added "Make local" button to convert shared lists back to local-only mode.
- [todo] Added conflict resolution notifications when shared list is updated by another user.
- [todo] Added duplicate detection when importing already-imported shared lists.
- [todo] Added error notifications for failed shared list imports with localized messages.
- [todo] Added confirmation modal before deleting todo lists with separate warnings for local vs shared lists.
- [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.
- [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.
- [global] Added new setting to adjust background color if plain style is selected.

### Changed

- [shared] Refactored emoji data (keywords and categories) from inline component code into separate utility module (`frontend/src/lib/util/emoji-data.ts`) for better organization and reusability.
- [todo] Refactored backend architecture into three-layer design: `todo-persistence` (database layer), `todo-provider` (business logic), and `todo-controller` (API endpoints) for better separation of concerns and testability.
- [todo] Added comprehensive unit test coverage (39 tests) for all three layers with proper mocking and error handling.
- [todo] Renamed `TodoMongoDbService` to `TodoPersistenceService` and `TodoService` to `TodoProviderService` for clearer naming conventions.
- [todo] Cleaned up duplicate naming in module files: renamed `todo-todo-*.module.ts` to `todo-*.module.ts` and updated all class names and imports (e.g., `TodoTodoControllerModule` β†’ `TodoControllerModule`).
- [todo] Replaced hardcoded "Uncategorized" category string with empty string throughout codebase - UI displays localized "General" / "Allgemein" label.
- [todo] Improved shared ID copy UI: icon-only button integrated with input field, success notification displayed at bottom of modal.
- [todo] Changed default view to category view (was: classic view) for better organization of todos.
- [todo] Added persistence for last viewed todo list - page reload now returns to the previously viewed list.
- [todo] Updated "Uncategorized" category label to more user-friendly "General" (EN) / "Allgemein" (DE).
- [todo] Improved TodoListOverlay modal UX by moving share/unshare button to modal action buttons and styling delete button as danger/red.
- [todo] Refactored shared list sync from push-based to pull-based polling (5-second intervals) for better conflict detection.
- [todo] Enhanced shared list creation to immediately push existing todos to server after creating shared list.
- [todo] Updated shared list endpoints to be public (no authentication required) for GET, POST, PUT, and DELETE operations.
- [todo] Improved sync logic with debounced updates and automatic conflict resolution via server-side version merging.
- [todo] Replaced inline fetch() calls with dedicated API module following project conventions.
- [todo] Updated API functions to return status codes for better error handling and 404 detection.
- [todo] Migrated todo route and all corresponding components (TodoList, TodoInput, Todo, TodoListOverlay, +page) to Svelte 5 runes syntax ($props, $state, $derived, $effect) for improved reactivity and type safety.
- [todo] Enhanced Todo component with improved UI/UX including clickable rows, better visual feedback, and proper accessibility.
- [todo] Improved TodoList with sorted todos (unchecked first), better state management using $derived, and enhanced history management.
Expand Down Expand Up @@ -42,6 +71,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Fixed

- [shared] Fixed EmojiPicker modal integration issues by replacing Carbon's Popover with a custom inline expandable dropdown that works reliably within modal constraints.
- [todo] Fixed missing sync when re-adding todos from history - now properly syncs to shared lists.
- [todo] Fixed bi-directional sync issue where changes in one browser didn't appear in other browsers - converted sync setup from onMount to reactive $effect.
- [todo] Fixed Toggle component error by converting `isCategoryView` from derived to state variable, enabling proper two-way binding.
- [todo] Fixed blank display in category view when list has no entries - now shows helpful empty state message.
- [todo] Fixed issue where no list was selected after deleting a list - now automatically selects the first remaining list.
- [todo] Fixed TOCTOU race condition in shared list updates by implementing atomic `findOneAndUpdate` with version check in query filter.
- [todo] Fixed generic error responses by replacing `throw new Error()` with NestJS HTTP exceptions (`NotFoundException`, `ConflictException`).
- [todo] Fixed missing history field persistence by adding to update DTO and MongoDB service signature.
- [todo] Fixed shared list deletion 400 error by removing manual UUID assignment and letting MongoDB auto-generate ObjectIds.
- [todo] Fixed DELETE request 400 error by removing Content-Type header from requests without body.
- [todo] Fixed cross-user deletion detection by adding global polling for all shared lists (every 10 seconds) in addition to per-list polling.
- [todo] Removed duplicate CSS blocks in TodoListOverlay component (`.share_section`, `.shared_id_container`, `.copy_button`, `.copied_notification`).
- [todo] Removed unused `importSharedId` state variable from TodoListOverlay component.
- [todo] Removed unused `selectRandomTagColor` function from TodoList component.
- [global] Fixed erroneous setLocale call in background color store causing i18n warnings.
- [memorandum] Fixed an issue where editing a folder in memorandum changed the wrong folders settings.
- [memorandum] Fixed folder ID duplication issues caused by drag-and-drop.
Expand Down
2 changes: 2 additions & 0 deletions backend/apps/tilloh-dev/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { JokesModule } from '@backend/jokes';
import { MemorandumModule } from '@backend/memorandum';
import { OcrModule } from '@backend/ocr';
import { SharedControllerHealthModule } from '@backend/shared-controller-health';
import { TodoControllerModule } from '@backend/todo-controller';
import { metricsControllerFactory } from '@backend/shared-metrics-controller';
import {
AdminGuard,
Expand Down Expand Up @@ -72,6 +73,7 @@ import { EnvironmentVariables, validate } from './env.validation';
JokesModule,
ChatModule,
OcrModule,
TodoControllerModule,
],
providers: [
Logger,
Expand Down
4 changes: 2 additions & 2 deletions backend/jest.config.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { getJestProjectsAsync } from '@nx/jest';

export default {
export default async () => ({
projects: await getJestProjectsAsync(),
};
});
1 change: 1 addition & 0 deletions backend/libs/shared/common/types/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@ export * from './lib/message.entity';
export * from './lib/ocr-space.dto';
export * from './lib/typing.dto';
export * from './lib/update-message.dto';
export * from './lib/todo.dto';
133 changes: 133 additions & 0 deletions backend/libs/shared/common/types/src/lib/todo.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsNotEmpty, IsOptional, IsString, IsArray, IsBoolean, IsNumber } from 'class-validator';

export class TodoItemDto {
@ApiProperty({ description: 'Todo item ID' })
@IsNotEmpty()
@IsString()
id: string;

@ApiProperty({ description: 'Todo item title' })
@IsNotEmpty()
@IsString()
title: string;

@ApiProperty({ description: 'Todo item completion status', required: false })
@IsOptional()
@IsBoolean()
done?: boolean;

@ApiProperty({ description: 'Todo item amount', required: false })
@IsOptional()
@IsString()
amount?: string;

@ApiProperty({ description: 'Todo item category', required: false })
@IsOptional()
@IsString()
category?: string;
}

export class SharedTodoListDto {
@ApiProperty({ description: 'Shared todo list ID' })
@IsNotEmpty()
@IsString()
_id: string;

@ApiProperty({ description: 'Shared todo list name' })
@IsNotEmpty()
@IsString()
name: string;

@ApiProperty({ description: 'Shared todo list emoji' })
@IsNotEmpty()
@IsString()
emoji: string;

@ApiProperty({ description: 'Todo items in the list', type: [TodoItemDto] })
@IsArray()
todos: TodoItemDto[];

@ApiProperty({ description: 'History of changes', type: [String], required: false })
@IsOptional()
@IsArray()
history?: string[];

@ApiProperty({ description: 'Version for optimistic locking' })
@IsNumber()
version: number;

@ApiProperty({ description: 'Creation date', required: false })
@IsOptional()
created: Date;

@ApiProperty({ description: 'Update date', required: false })
@IsOptional()
updated: Date;
}

export class GetSharedTodoListsOutputDto extends SharedTodoListDto {}

export class GetSharedTodoListInputDto {
@ApiProperty({ description: 'Shared todo list ID' })
@IsNotEmpty()
@IsString()
id: string;
}

export class GetSharedTodoListOutputDto extends SharedTodoListDto {}

export class CreateSharedTodoListInputDto {
@ApiProperty({ description: 'Shared todo list name' })
@IsNotEmpty()
@IsString()
name: string;

@ApiProperty({ description: 'Shared todo list emoji' })
@IsNotEmpty()
@IsString()
emoji: string;
}

export class CreateSharedTodoListOutputDto extends SharedTodoListDto {}

export class UpdateSharedTodoListInputDto {
@ApiProperty({ description: 'Shared todo list ID' })
@IsNotEmpty()
@IsString()
id: string;

@ApiProperty({ description: 'Shared todo list name' })
@IsNotEmpty()
@IsString()
name: string;

@ApiProperty({ description: 'Shared todo list emoji' })
@IsNotEmpty()
@IsString()
emoji: string;

@ApiProperty({ description: 'Todo items in the list', type: [TodoItemDto] })
@IsArray()
todos: TodoItemDto[];

@ApiProperty({ description: 'History of todo entries', type: [String], required: false })
@IsOptional()
@IsArray()
history?: string[];

@ApiProperty({ description: 'Current version for optimistic locking' })
@IsNumber()
version: number;
}

export class UpdateSharedTodoListOutputDto extends SharedTodoListDto {}

export class RemoveSharedTodoListInputDto {
@ApiProperty({ description: 'Shared todo list ID' })
@IsNotEmpty()
@IsString()
id: string;
}

export class RemoveSharedTodoListOutputDto extends SharedTodoListDto {}
2 changes: 2 additions & 0 deletions backend/libs/shared/provider/todo/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './lib/todo.module';
export * from './lib/todo.service';
49 changes: 49 additions & 0 deletions backend/libs/shared/provider/todo/src/lib/schema/todo.schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { Document, Types } from 'mongoose';

export type SharedTodoListDocument = SharedTodoList & Document;

class TodoItem {
@Prop({ required: true })
id: string;

@Prop({ required: true })
title: string;

@Prop({ required: false })
done?: boolean;

@Prop({ required: false })
amount?: string;

@Prop({ required: false })
category?: string;
}

@Schema({ collection: 'shared_todo_lists' })
export class SharedTodoList {
_id?: Types.ObjectId;

@Prop({ required: true })
name!: string;

@Prop({ required: true })
emoji!: string;

@Prop({ type: [TodoItem], required: true, default: [] })
todos!: TodoItem[];

@Prop({ type: [String], required: false, default: [] })
history?: string[];

@Prop({ type: Number, required: true, default: 1 })
version!: number;

@Prop({ type: Date, required: true, default: () => new Date() })
created!: Date;

@Prop({ type: Date, required: true, default: () => new Date() })
updated!: Date;
}

export const SharedTodoListSchema = SchemaFactory.createForClass(SharedTodoList);
89 changes: 89 additions & 0 deletions backend/libs/shared/provider/todo/src/lib/todo-mongodb.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { Injectable, Logger, NotFoundException, ConflictException } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Model } from 'mongoose';
import { SharedTodoList, SharedTodoListDocument } from './schema/todo.schema';

@Injectable()
export class TodoMongoDbService {
private readonly logger = new Logger(TodoMongoDbService.name);

constructor(
@InjectModel(SharedTodoList.name)
private sharedTodoListModel: Model<SharedTodoListDocument>,
) {}

async findAll(): Promise<SharedTodoList[]> {
this.logger.log('Finding all shared todo lists');
return this.sharedTodoListModel.find().exec();
}

async findOne(id: string): Promise<SharedTodoList | null> {
this.logger.log(`Finding shared todo list with id: ${id}`);
return this.sharedTodoListModel.findById(id).exec();
}

async create(
name: string,
emoji: string,
): Promise<SharedTodoList> {
this.logger.log(`Creating new shared todo list: ${name}`);
const newList = new this.sharedTodoListModel({
name,
emoji,
todos: [],
history: [],
version: 1,
});
return newList.save();
}

async update(
id: string,
name: string,
emoji: string,
todos: any[],
history: string[],
currentVersion: number,
): Promise<SharedTodoList> {
this.logger.log(`Updating shared todo list with id: ${id}`);

// Atomic update: include version check in the query filter to prevent TOCTOU race condition
const result = await this.sharedTodoListModel
.findOneAndUpdate(
{ _id: id, version: currentVersion },
{
name,
emoji,
todos,
history,
version: currentVersion + 1,
updated: new Date(),
},
{ new: true },
)
.exec();

// If no result, determine if it's a "not found" or "version conflict"
if (!result) {
const list = await this.sharedTodoListModel.findById(id).exec();
if (!list) {
this.logger.warn(`Shared todo list not found: ${id}`);
throw new NotFoundException(`Shared todo list with id ${id} not found`);
}
// List exists but version didn't match - conflict
this.logger.warn(
`Version conflict for list ${id}. Expected: ${currentVersion}, Current: ${list.version}`,
);
throw new ConflictException(
`Version conflict. Expected version ${currentVersion}, but current version is ${list.version}`,
);
}

return result;
}

async remove(id: string): Promise<SharedTodoList | null> {
this.logger.log(`Removing shared todo list with id: ${id}`);
return this.sharedTodoListModel.findByIdAndDelete(id).exec();
}
}
17 changes: 17 additions & 0 deletions backend/libs/shared/provider/todo/src/lib/todo.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { Module } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose';
import { TodoService } from './todo.service';
import { TodoMongoDbService } from './todo-mongodb.service';
import { SharedTodoList, SharedTodoListSchema } from './schema/todo.schema';

@Module({
imports: [
MongooseModule.forFeature([
{ name: SharedTodoList.name, schema: SharedTodoListSchema },
]),
],
controllers: [],
providers: [TodoService, TodoMongoDbService],
exports: [TodoService],
})
export class SharedTodoModule {}
Loading
Loading