From d22f8cc29276a46dfff51e5c43cdddd5af385f45 Mon Sep 17 00:00:00 2001 From: SquidSpirit Date: Wed, 15 Oct 2025 12:21:41 +0800 Subject: [PATCH] BLOG-140 Label management (list and create) (#144) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### Description - As the title. ### Package Changes _No response_ ### Screenshots |Scenario|Screenshot| |-|-| |Label list|![截圖 2025-10-15 中午12.00.39.png](/attachments/e27ad38b-86ba-4791-9e33-3fbc1b77f3ad)| |Create dialog|![截圖 2025-10-15 中午12.02.37.png](/attachments/aa3edda3-b97a-42b7-89dd-67fc9d2fe51e)| |Input error|![截圖 2025-10-15 中午12.01.30.png](/attachments/daafb0f7-51e1-429d-883d-b1c867e009ad)| |Name conflict|![截圖 2025-10-15 中午12.06.17.png](/attachments/1b866144-e37a-41ed-9011-f24b89d89368)| ### Reference Reference #140. ### Checklist - [x] A milestone is set - [x] The related issuse has been linked to this branch Reviewed-on: https://git.squidspirit.com/squid/blog/pulls/144 Co-authored-by: SquidSpirit Co-committed-by: SquidSpirit --- .../auth/adapter/gateway/userResponseDto.ts | 4 +- frontend/src/lib/container.ts | 45 ++++++- .../adapter/gateway/imageInfoResponseDto.ts | 4 +- .../{colorResponseDto.ts => colorDto.ts} | 23 +++- .../adapter/gateway/createLabelRequestDto.ts | 26 ++++ .../label/adapter/gateway/labelApiService.ts | 7 + .../adapter/gateway/labelRepositoryImpl.ts | 22 +++ .../label/adapter/gateway/labelResponseDto.ts | 14 +- .../label/adapter/presenter/colorViewModel.ts | 24 +++- .../adapter/presenter/labelCreatedStore.ts | 40 ++++++ .../adapter/presenter/labelsListedStore.ts | 46 +++++++ .../application/gateway/labelRepository.ts | 12 ++ .../application/useCase/createLabelUseCase.ts | 13 ++ .../useCase/getAllLabelsUseCase.ts | 10 ++ .../framework/api/labelApiServiceImpl.ts | 39 ++++++ .../framework/ui/CreateLabelDialog.svelte | 127 ++++++++++++++++++ .../framework/ui/LabelManagementPage.svelte | 90 +++++++++++++ .../lib/label/framework/ui/PostLabel.svelte | 2 +- .../adapter/gateway/postInfoResponseDto.ts | 8 +- .../post/adapter/gateway/postResponseDto.ts | 8 +- .../adapter/presenter/postCreatedStore.ts | 9 +- .../post/framework/ui/CreatePostDialog.svelte | 18 +-- .../framework/ui/PostManagementPage.svelte | 20 ++- .../routes/dashboard/label/+page.server.ts | 13 ++ .../src/routes/dashboard/label/+page.svelte | 22 ++- 25 files changed, 593 insertions(+), 53 deletions(-) rename frontend/src/lib/label/adapter/gateway/{colorResponseDto.ts => colorDto.ts} (67%) create mode 100644 frontend/src/lib/label/adapter/gateway/createLabelRequestDto.ts create mode 100644 frontend/src/lib/label/adapter/gateway/labelApiService.ts create mode 100644 frontend/src/lib/label/adapter/gateway/labelRepositoryImpl.ts create mode 100644 frontend/src/lib/label/adapter/presenter/labelCreatedStore.ts create mode 100644 frontend/src/lib/label/adapter/presenter/labelsListedStore.ts create mode 100644 frontend/src/lib/label/application/gateway/labelRepository.ts create mode 100644 frontend/src/lib/label/application/useCase/createLabelUseCase.ts create mode 100644 frontend/src/lib/label/application/useCase/getAllLabelsUseCase.ts create mode 100644 frontend/src/lib/label/framework/api/labelApiServiceImpl.ts create mode 100644 frontend/src/lib/label/framework/ui/CreateLabelDialog.svelte create mode 100644 frontend/src/lib/label/framework/ui/LabelManagementPage.svelte create mode 100644 frontend/src/routes/dashboard/label/+page.server.ts diff --git a/frontend/src/lib/auth/adapter/gateway/userResponseDto.ts b/frontend/src/lib/auth/adapter/gateway/userResponseDto.ts index 8f64369..dff2c4e 100644 --- a/frontend/src/lib/auth/adapter/gateway/userResponseDto.ts +++ b/frontend/src/lib/auth/adapter/gateway/userResponseDto.ts @@ -1,7 +1,7 @@ import { User } from '$lib/auth/domain/entity/user'; import z from 'zod'; -export const UserResponseSchema = z.object({ +export const userResponseSchema = z.object({ id: z.int32(), displayed_name: z.string(), email: z.email(), @@ -19,7 +19,7 @@ export class UserResponseDto { } static fromJson(json: unknown): UserResponseDto { - const parsedJson = UserResponseSchema.parse(json); + const parsedJson = userResponseSchema.parse(json); return new UserResponseDto({ id: parsedJson.id, displayedName: parsedJson.displayed_name, diff --git a/frontend/src/lib/container.ts b/frontend/src/lib/container.ts index cb9cc20..078015c 100644 --- a/frontend/src/lib/container.ts +++ b/frontend/src/lib/container.ts @@ -4,6 +4,15 @@ import { AuthLoadedStore } from '$lib/auth/adapter/presenter/authLoadedStore'; import type { AuthRepository } from '$lib/auth/application/gateway/authRepository'; import { GetCurrentUserUseCase } from '$lib/auth/application/useCase/getCurrentUserUseCase'; import { AuthApiServiceImpl } from '$lib/auth/framework/api/authApiServiceImpl'; +import type { LabelApiService } from '$lib/label/adapter/gateway/labelApiService'; +import { LabelRepositoryImpl } from '$lib/label/adapter/gateway/labelRepositoryImpl'; +import { LabelCreatedStore } from '$lib/label/adapter/presenter/labelCreatedStore'; +import type { LabelViewModel } from '$lib/label/adapter/presenter/labelViewModel'; +import { LabelsListedStore } from '$lib/label/adapter/presenter/labelsListedStore'; +import type { LabelRepository } from '$lib/label/application/gateway/labelRepository'; +import { CreateLabelUseCase } from '$lib/label/application/useCase/createLabelUseCase'; +import { GetAllLabelsUseCase } from '$lib/label/application/useCase/getAllLabelsUseCase'; +import { LabelApiServiceImpl } from '$lib/label/framework/api/labelApiServiceImpl'; import type { ImageApiService } from '$lib/image/adapter/gateway/imageApiService'; import { ImageRepositoryImpl } from '$lib/image/adapter/gateway/imageRepositoryImpl'; import { ImageUploadedStore } from '$lib/image/adapter/presenter/imageUploadedStore'; @@ -48,8 +57,16 @@ export class Container { return new PostLoadedStore(this.useCases.getPostUseCase, initialData); } - createPostCreatedStore(initialData?: PostViewModel): PostCreatedStore { - return new PostCreatedStore(this.useCases.createPostUseCase, initialData); + createPostCreatedStore(): PostCreatedStore { + return new PostCreatedStore(this.useCases.createPostUseCase); + } + + createLabelsListedStore(initialData?: readonly LabelViewModel[]): LabelsListedStore { + return new LabelsListedStore(this.useCases.getAllLabelsUseCase, initialData); + } + + createLabelCreatedStore(): LabelCreatedStore { + return new LabelCreatedStore(this.useCases.createLabelUseCase); } } @@ -59,6 +76,7 @@ class ApiServices { private _authApiService?: AuthApiService; private _imageApiService?: ImageApiService; private _postApiService?: PostApiService; + private _labelApiService?: LabelApiService; constructor(fetchFn: typeof fetch) { this.fetchFn = fetchFn; @@ -78,6 +96,11 @@ class ApiServices { this._postApiService ??= new PostApiServiceImpl(this.fetchFn); return this._postApiService; } + + get labelApiService(): LabelApiService { + this._labelApiService ??= new LabelApiServiceImpl(this.fetchFn); + return this._labelApiService; + } } class Repositories { @@ -86,6 +109,7 @@ class Repositories { private _authRepository?: AuthRepository; private _imageRepository?: ImageRepository; private _postRepository?: PostRepository; + private _labelRepository?: LabelRepository; constructor(apiServices: ApiServices) { this.apiServices = apiServices; @@ -105,6 +129,11 @@ class Repositories { this._postRepository ??= new PostRepositoryImpl(this.apiServices.postApiService); return this._postRepository; } + + get labelRepository(): LabelRepository { + this._labelRepository ??= new LabelRepositoryImpl(this.apiServices.labelApiService); + return this._labelRepository; + } } class UseCases { @@ -115,6 +144,8 @@ class UseCases { private _getAllPostsUseCase?: GetAllPostsUseCase; private _getPostUseCase?: GetPostUseCase; private _createPostUseCase?: CreatePostUseCase; + private _getAllLabelsUseCase?: GetAllLabelsUseCase; + private _createLabelUseCase?: CreateLabelUseCase; constructor(repositories: Repositories) { this.repositories = repositories; @@ -144,4 +175,14 @@ class UseCases { this._createPostUseCase ??= new CreatePostUseCase(this.repositories.postRepository); return this._createPostUseCase; } + + get getAllLabelsUseCase(): GetAllLabelsUseCase { + this._getAllLabelsUseCase ??= new GetAllLabelsUseCase(this.repositories.labelRepository); + return this._getAllLabelsUseCase; + } + + get createLabelUseCase(): CreateLabelUseCase { + this._createLabelUseCase ??= new CreateLabelUseCase(this.repositories.labelRepository); + return this._createLabelUseCase; + } } diff --git a/frontend/src/lib/image/adapter/gateway/imageInfoResponseDto.ts b/frontend/src/lib/image/adapter/gateway/imageInfoResponseDto.ts index ad7a5ba..bdc8d23 100644 --- a/frontend/src/lib/image/adapter/gateway/imageInfoResponseDto.ts +++ b/frontend/src/lib/image/adapter/gateway/imageInfoResponseDto.ts @@ -1,6 +1,6 @@ import z from 'zod'; -export const ImageInfoResponseSchema = z.object({ +export const imageInfoResponseSchema = z.object({ id: z.int32(), mime_type: z.string(), }); @@ -15,7 +15,7 @@ export class ImageInfoResponseDto { } static fromJson(json: unknown): ImageInfoResponseDto { - const parsedJson = ImageInfoResponseSchema.parse(json); + const parsedJson = imageInfoResponseSchema.parse(json); return new ImageInfoResponseDto({ id: parsedJson.id, mimeType: parsedJson.mime_type, diff --git a/frontend/src/lib/label/adapter/gateway/colorResponseDto.ts b/frontend/src/lib/label/adapter/gateway/colorDto.ts similarity index 67% rename from frontend/src/lib/label/adapter/gateway/colorResponseDto.ts rename to frontend/src/lib/label/adapter/gateway/colorDto.ts index 9d2f75f..7952cdd 100644 --- a/frontend/src/lib/label/adapter/gateway/colorResponseDto.ts +++ b/frontend/src/lib/label/adapter/gateway/colorDto.ts @@ -1,14 +1,14 @@ import { Color } from '$lib/label/domain/entity/color'; import z from 'zod'; -export const ColorResponseSchema = z.object({ +export const colorResponseSchema = z.object({ red: z.number().int().min(0).max(255), green: z.number().int().min(0).max(255), blue: z.number().int().min(0).max(255), alpha: z.number().int().min(0).max(255), }); -export class ColorResponseDto { +export class ColorDto { readonly red: number; readonly green: number; readonly blue: number; @@ -21,9 +21,13 @@ export class ColorResponseDto { this.alpha = props.alpha; } - static fromJson(json: unknown): ColorResponseDto { - const parsedJson = ColorResponseSchema.parse(json); - return new ColorResponseDto({ + static fromEntity(color: Color): ColorDto { + return new ColorDto(color); + } + + static fromJson(json: unknown): ColorDto { + const parsedJson = colorResponseSchema.parse(json); + return new ColorDto({ red: parsedJson.red, green: parsedJson.green, blue: parsedJson.blue, @@ -39,4 +43,13 @@ export class ColorResponseDto { alpha: this.alpha, }); } + + toJson() { + return { + red: this.red, + green: this.green, + blue: this.blue, + alpha: this.alpha, + }; + } } diff --git a/frontend/src/lib/label/adapter/gateway/createLabelRequestDto.ts b/frontend/src/lib/label/adapter/gateway/createLabelRequestDto.ts new file mode 100644 index 0000000..cc40e8c --- /dev/null +++ b/frontend/src/lib/label/adapter/gateway/createLabelRequestDto.ts @@ -0,0 +1,26 @@ +import { ColorDto } from '$lib/label/adapter/gateway/colorDto'; +import type { CreateLabelParams } from '$lib/label/application/gateway/labelRepository'; + +export class CreateLabelRequestDto { + readonly name: string; + readonly color: ColorDto; + + private constructor(props: { name: string; color: ColorDto }) { + this.name = props.name; + this.color = props.color; + } + + static fromParams(params: CreateLabelParams): CreateLabelRequestDto { + return new CreateLabelRequestDto({ + name: params.name, + color: ColorDto.fromEntity(params.color), + }); + } + + toJson() { + return { + name: this.name, + color: this.color.toJson(), + }; + } +} diff --git a/frontend/src/lib/label/adapter/gateway/labelApiService.ts b/frontend/src/lib/label/adapter/gateway/labelApiService.ts new file mode 100644 index 0000000..e17b297 --- /dev/null +++ b/frontend/src/lib/label/adapter/gateway/labelApiService.ts @@ -0,0 +1,7 @@ +import type { CreateLabelRequestDto } from '$lib/label/adapter/gateway/createLabelRequestDto'; +import type { LabelResponseDto } from '$lib/label/adapter/gateway/labelResponseDto'; + +export interface LabelApiService { + getAllLabels(): Promise; + createLabel(payload: CreateLabelRequestDto): Promise; +} diff --git a/frontend/src/lib/label/adapter/gateway/labelRepositoryImpl.ts b/frontend/src/lib/label/adapter/gateway/labelRepositoryImpl.ts new file mode 100644 index 0000000..8c8bc8d --- /dev/null +++ b/frontend/src/lib/label/adapter/gateway/labelRepositoryImpl.ts @@ -0,0 +1,22 @@ +import { CreateLabelRequestDto } from '$lib/label/adapter/gateway/createLabelRequestDto'; +import type { LabelApiService } from '$lib/label/adapter/gateway/labelApiService'; +import type { + CreateLabelParams, + LabelRepository, +} from '$lib/label/application/gateway/labelRepository'; +import type { Label } from '$lib/label/domain/entity/label'; + +export class LabelRepositoryImpl implements LabelRepository { + constructor(private readonly labelApiService: LabelApiService) {} + + async getAllLabels(): Promise { + const dtos = await this.labelApiService.getAllLabels(); + return dtos.map((dto) => dto.toEntity()); + } + + async createLabel(params: CreateLabelParams): Promise