BLOG-140 Label management (list and create) (#144)
All checks were successful
Frontend CI / build (push) Successful in 1m37s
All checks were successful
Frontend CI / build (push) Successful in 1m37s
### Description - As the title. ### Package Changes _No response_ ### Screenshots |Scenario|Screenshot| |-|-| |Label list|| |Create dialog|| |Input error|| |Name conflict|| ### Reference Reference #140. ### Checklist - [x] A milestone is set - [x] The related issuse has been linked to this branch Reviewed-on: #144 Co-authored-by: SquidSpirit <squid@squidspirit.com> Co-committed-by: SquidSpirit <squid@squidspirit.com>
This commit is contained in:
parent
a577f94acd
commit
d22f8cc292
@ -1,7 +1,7 @@
|
|||||||
import { User } from '$lib/auth/domain/entity/user';
|
import { User } from '$lib/auth/domain/entity/user';
|
||||||
import z from 'zod';
|
import z from 'zod';
|
||||||
|
|
||||||
export const UserResponseSchema = z.object({
|
export const userResponseSchema = z.object({
|
||||||
id: z.int32(),
|
id: z.int32(),
|
||||||
displayed_name: z.string(),
|
displayed_name: z.string(),
|
||||||
email: z.email(),
|
email: z.email(),
|
||||||
@ -19,7 +19,7 @@ export class UserResponseDto {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static fromJson(json: unknown): UserResponseDto {
|
static fromJson(json: unknown): UserResponseDto {
|
||||||
const parsedJson = UserResponseSchema.parse(json);
|
const parsedJson = userResponseSchema.parse(json);
|
||||||
return new UserResponseDto({
|
return new UserResponseDto({
|
||||||
id: parsedJson.id,
|
id: parsedJson.id,
|
||||||
displayedName: parsedJson.displayed_name,
|
displayedName: parsedJson.displayed_name,
|
||||||
|
@ -4,6 +4,15 @@ import { AuthLoadedStore } from '$lib/auth/adapter/presenter/authLoadedStore';
|
|||||||
import type { AuthRepository } from '$lib/auth/application/gateway/authRepository';
|
import type { AuthRepository } from '$lib/auth/application/gateway/authRepository';
|
||||||
import { GetCurrentUserUseCase } from '$lib/auth/application/useCase/getCurrentUserUseCase';
|
import { GetCurrentUserUseCase } from '$lib/auth/application/useCase/getCurrentUserUseCase';
|
||||||
import { AuthApiServiceImpl } from '$lib/auth/framework/api/authApiServiceImpl';
|
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 type { ImageApiService } from '$lib/image/adapter/gateway/imageApiService';
|
||||||
import { ImageRepositoryImpl } from '$lib/image/adapter/gateway/imageRepositoryImpl';
|
import { ImageRepositoryImpl } from '$lib/image/adapter/gateway/imageRepositoryImpl';
|
||||||
import { ImageUploadedStore } from '$lib/image/adapter/presenter/imageUploadedStore';
|
import { ImageUploadedStore } from '$lib/image/adapter/presenter/imageUploadedStore';
|
||||||
@ -48,8 +57,16 @@ export class Container {
|
|||||||
return new PostLoadedStore(this.useCases.getPostUseCase, initialData);
|
return new PostLoadedStore(this.useCases.getPostUseCase, initialData);
|
||||||
}
|
}
|
||||||
|
|
||||||
createPostCreatedStore(initialData?: PostViewModel): PostCreatedStore {
|
createPostCreatedStore(): PostCreatedStore {
|
||||||
return new PostCreatedStore(this.useCases.createPostUseCase, initialData);
|
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 _authApiService?: AuthApiService;
|
||||||
private _imageApiService?: ImageApiService;
|
private _imageApiService?: ImageApiService;
|
||||||
private _postApiService?: PostApiService;
|
private _postApiService?: PostApiService;
|
||||||
|
private _labelApiService?: LabelApiService;
|
||||||
|
|
||||||
constructor(fetchFn: typeof fetch) {
|
constructor(fetchFn: typeof fetch) {
|
||||||
this.fetchFn = fetchFn;
|
this.fetchFn = fetchFn;
|
||||||
@ -78,6 +96,11 @@ class ApiServices {
|
|||||||
this._postApiService ??= new PostApiServiceImpl(this.fetchFn);
|
this._postApiService ??= new PostApiServiceImpl(this.fetchFn);
|
||||||
return this._postApiService;
|
return this._postApiService;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get labelApiService(): LabelApiService {
|
||||||
|
this._labelApiService ??= new LabelApiServiceImpl(this.fetchFn);
|
||||||
|
return this._labelApiService;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class Repositories {
|
class Repositories {
|
||||||
@ -86,6 +109,7 @@ class Repositories {
|
|||||||
private _authRepository?: AuthRepository;
|
private _authRepository?: AuthRepository;
|
||||||
private _imageRepository?: ImageRepository;
|
private _imageRepository?: ImageRepository;
|
||||||
private _postRepository?: PostRepository;
|
private _postRepository?: PostRepository;
|
||||||
|
private _labelRepository?: LabelRepository;
|
||||||
|
|
||||||
constructor(apiServices: ApiServices) {
|
constructor(apiServices: ApiServices) {
|
||||||
this.apiServices = apiServices;
|
this.apiServices = apiServices;
|
||||||
@ -105,6 +129,11 @@ class Repositories {
|
|||||||
this._postRepository ??= new PostRepositoryImpl(this.apiServices.postApiService);
|
this._postRepository ??= new PostRepositoryImpl(this.apiServices.postApiService);
|
||||||
return this._postRepository;
|
return this._postRepository;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get labelRepository(): LabelRepository {
|
||||||
|
this._labelRepository ??= new LabelRepositoryImpl(this.apiServices.labelApiService);
|
||||||
|
return this._labelRepository;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class UseCases {
|
class UseCases {
|
||||||
@ -115,6 +144,8 @@ class UseCases {
|
|||||||
private _getAllPostsUseCase?: GetAllPostsUseCase;
|
private _getAllPostsUseCase?: GetAllPostsUseCase;
|
||||||
private _getPostUseCase?: GetPostUseCase;
|
private _getPostUseCase?: GetPostUseCase;
|
||||||
private _createPostUseCase?: CreatePostUseCase;
|
private _createPostUseCase?: CreatePostUseCase;
|
||||||
|
private _getAllLabelsUseCase?: GetAllLabelsUseCase;
|
||||||
|
private _createLabelUseCase?: CreateLabelUseCase;
|
||||||
|
|
||||||
constructor(repositories: Repositories) {
|
constructor(repositories: Repositories) {
|
||||||
this.repositories = repositories;
|
this.repositories = repositories;
|
||||||
@ -144,4 +175,14 @@ class UseCases {
|
|||||||
this._createPostUseCase ??= new CreatePostUseCase(this.repositories.postRepository);
|
this._createPostUseCase ??= new CreatePostUseCase(this.repositories.postRepository);
|
||||||
return this._createPostUseCase;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import z from 'zod';
|
import z from 'zod';
|
||||||
|
|
||||||
export const ImageInfoResponseSchema = z.object({
|
export const imageInfoResponseSchema = z.object({
|
||||||
id: z.int32(),
|
id: z.int32(),
|
||||||
mime_type: z.string(),
|
mime_type: z.string(),
|
||||||
});
|
});
|
||||||
@ -15,7 +15,7 @@ export class ImageInfoResponseDto {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static fromJson(json: unknown): ImageInfoResponseDto {
|
static fromJson(json: unknown): ImageInfoResponseDto {
|
||||||
const parsedJson = ImageInfoResponseSchema.parse(json);
|
const parsedJson = imageInfoResponseSchema.parse(json);
|
||||||
return new ImageInfoResponseDto({
|
return new ImageInfoResponseDto({
|
||||||
id: parsedJson.id,
|
id: parsedJson.id,
|
||||||
mimeType: parsedJson.mime_type,
|
mimeType: parsedJson.mime_type,
|
||||||
|
@ -1,14 +1,14 @@
|
|||||||
import { Color } from '$lib/label/domain/entity/color';
|
import { Color } from '$lib/label/domain/entity/color';
|
||||||
import z from 'zod';
|
import z from 'zod';
|
||||||
|
|
||||||
export const ColorResponseSchema = z.object({
|
export const colorResponseSchema = z.object({
|
||||||
red: z.number().int().min(0).max(255),
|
red: z.number().int().min(0).max(255),
|
||||||
green: z.number().int().min(0).max(255),
|
green: z.number().int().min(0).max(255),
|
||||||
blue: z.number().int().min(0).max(255),
|
blue: z.number().int().min(0).max(255),
|
||||||
alpha: 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 red: number;
|
||||||
readonly green: number;
|
readonly green: number;
|
||||||
readonly blue: number;
|
readonly blue: number;
|
||||||
@ -21,9 +21,13 @@ export class ColorResponseDto {
|
|||||||
this.alpha = props.alpha;
|
this.alpha = props.alpha;
|
||||||
}
|
}
|
||||||
|
|
||||||
static fromJson(json: unknown): ColorResponseDto {
|
static fromEntity(color: Color): ColorDto {
|
||||||
const parsedJson = ColorResponseSchema.parse(json);
|
return new ColorDto(color);
|
||||||
return new ColorResponseDto({
|
}
|
||||||
|
|
||||||
|
static fromJson(json: unknown): ColorDto {
|
||||||
|
const parsedJson = colorResponseSchema.parse(json);
|
||||||
|
return new ColorDto({
|
||||||
red: parsedJson.red,
|
red: parsedJson.red,
|
||||||
green: parsedJson.green,
|
green: parsedJson.green,
|
||||||
blue: parsedJson.blue,
|
blue: parsedJson.blue,
|
||||||
@ -39,4 +43,13 @@ export class ColorResponseDto {
|
|||||||
alpha: this.alpha,
|
alpha: this.alpha,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
toJson() {
|
||||||
|
return {
|
||||||
|
red: this.red,
|
||||||
|
green: this.green,
|
||||||
|
blue: this.blue,
|
||||||
|
alpha: this.alpha,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
@ -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(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
@ -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<LabelResponseDto[]>;
|
||||||
|
createLabel(payload: CreateLabelRequestDto): Promise<LabelResponseDto>;
|
||||||
|
}
|
@ -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<Label[]> {
|
||||||
|
const dtos = await this.labelApiService.getAllLabels();
|
||||||
|
return dtos.map((dto) => dto.toEntity());
|
||||||
|
}
|
||||||
|
|
||||||
|
async createLabel(params: CreateLabelParams): Promise<Label> {
|
||||||
|
const requestDto = CreateLabelRequestDto.fromParams(params);
|
||||||
|
const responseDto = await this.labelApiService.createLabel(requestDto);
|
||||||
|
return responseDto.toEntity();
|
||||||
|
}
|
||||||
|
}
|
@ -1,30 +1,30 @@
|
|||||||
import { ColorResponseDto, ColorResponseSchema } from '$lib/label/adapter/gateway/colorResponseDto';
|
import { ColorDto, colorResponseSchema } from '$lib/label/adapter/gateway/colorDto';
|
||||||
import { Label } from '$lib/label/domain/entity/label';
|
import { Label } from '$lib/label/domain/entity/label';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
export const LabelResponseSchema = z.object({
|
export const labelResponseSchema = z.object({
|
||||||
id: z.int32(),
|
id: z.int32(),
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
color: ColorResponseSchema,
|
color: colorResponseSchema,
|
||||||
});
|
});
|
||||||
|
|
||||||
export class LabelResponseDto {
|
export class LabelResponseDto {
|
||||||
readonly id: number;
|
readonly id: number;
|
||||||
readonly name: string;
|
readonly name: string;
|
||||||
readonly color: ColorResponseDto;
|
readonly color: ColorDto;
|
||||||
|
|
||||||
private constructor(props: { id: number; name: string; color: ColorResponseDto }) {
|
private constructor(props: { id: number; name: string; color: ColorDto }) {
|
||||||
this.id = props.id;
|
this.id = props.id;
|
||||||
this.name = props.name;
|
this.name = props.name;
|
||||||
this.color = props.color;
|
this.color = props.color;
|
||||||
}
|
}
|
||||||
|
|
||||||
static fromJson(json: unknown): LabelResponseDto {
|
static fromJson(json: unknown): LabelResponseDto {
|
||||||
const parsedJson = LabelResponseSchema.parse(json);
|
const parsedJson = labelResponseSchema.parse(json);
|
||||||
return new LabelResponseDto({
|
return new LabelResponseDto({
|
||||||
id: parsedJson.id,
|
id: parsedJson.id,
|
||||||
name: parsedJson.name,
|
name: parsedJson.name,
|
||||||
color: ColorResponseDto.fromJson(parsedJson.color),
|
color: ColorDto.fromJson(parsedJson.color),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import type { Color } from '$lib/label/domain/entity/color';
|
import { Color } from '$lib/label/domain/entity/color';
|
||||||
|
|
||||||
export class ColorViewModel {
|
export class ColorViewModel {
|
||||||
readonly red: number;
|
readonly red: number;
|
||||||
@ -56,6 +56,19 @@ export class ColorViewModel {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static fromHex(hex: string): ColorViewModel {
|
||||||
|
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})?$/i.exec(hex);
|
||||||
|
if (!result) {
|
||||||
|
throw new Error('Invalid hex color format');
|
||||||
|
}
|
||||||
|
return new ColorViewModel({
|
||||||
|
red: parseInt(result[1], 16),
|
||||||
|
green: parseInt(result[2], 16),
|
||||||
|
blue: parseInt(result[3], 16),
|
||||||
|
alpha: result[4] ? parseInt(result[4], 16) : 255,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
static rehydrate(props: DehydratedColorProps): ColorViewModel {
|
static rehydrate(props: DehydratedColorProps): ColorViewModel {
|
||||||
return new ColorViewModel(props);
|
return new ColorViewModel(props);
|
||||||
}
|
}
|
||||||
@ -99,6 +112,15 @@ export class ColorViewModel {
|
|||||||
return { h: h * 360, s: s, l: l };
|
return { h: h * 360, s: s, l: l };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
toEntity(): Color {
|
||||||
|
return new Color({
|
||||||
|
red: this.red,
|
||||||
|
green: this.green,
|
||||||
|
blue: this.blue,
|
||||||
|
alpha: this.alpha,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
lighten(amount: number): ColorViewModel {
|
lighten(amount: number): ColorViewModel {
|
||||||
const hsl = this.toHsl();
|
const hsl = this.toHsl();
|
||||||
hsl.l += amount;
|
hsl.l += amount;
|
||||||
|
@ -0,0 +1,40 @@
|
|||||||
|
import { AsyncState } from '$lib/common/adapter/presenter/asyncState';
|
||||||
|
import type { BaseStore } from '$lib/common/adapter/presenter/baseStore';
|
||||||
|
import { LabelViewModel } from '$lib/label/adapter/presenter/labelViewModel';
|
||||||
|
import type { CreateLabelParams } from '$lib/label/application/gateway/labelRepository';
|
||||||
|
import type { CreateLabelUseCase } from '$lib/label/application/useCase/createLabelUseCase';
|
||||||
|
import { captureException } from '@sentry/sveltekit';
|
||||||
|
import { get, writable } from 'svelte/store';
|
||||||
|
|
||||||
|
type LabelState = AsyncState<LabelViewModel>;
|
||||||
|
|
||||||
|
export class LabelCreatedStore implements BaseStore<LabelState, CreateLabelParams> {
|
||||||
|
private readonly state = writable<LabelState>(AsyncState.idle<LabelViewModel>(null));
|
||||||
|
|
||||||
|
constructor(private readonly createLabelUseCase: CreateLabelUseCase) {}
|
||||||
|
|
||||||
|
get subscribe() {
|
||||||
|
return this.state.subscribe;
|
||||||
|
}
|
||||||
|
|
||||||
|
get trigger() {
|
||||||
|
return (params: CreateLabelParams) => this.createLabel(params);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async createLabel(params: CreateLabelParams): Promise<LabelState> {
|
||||||
|
this.state.set(AsyncState.loading(get(this.state).data));
|
||||||
|
|
||||||
|
let result: LabelState;
|
||||||
|
try {
|
||||||
|
const label = await this.createLabelUseCase.execute(params);
|
||||||
|
const labelViewModel = LabelViewModel.fromEntity(label);
|
||||||
|
result = AsyncState.success(labelViewModel);
|
||||||
|
} catch (e) {
|
||||||
|
result = AsyncState.error(e, get(this.state).data);
|
||||||
|
captureException(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.state.set(result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,46 @@
|
|||||||
|
import { AsyncState } from '$lib/common/adapter/presenter/asyncState';
|
||||||
|
import type { BaseStore } from '$lib/common/adapter/presenter/baseStore';
|
||||||
|
import { LabelViewModel } from '$lib/label/adapter/presenter/labelViewModel';
|
||||||
|
import type { GetAllLabelsUseCase } from '$lib/label/application/useCase/getAllLabelsUseCase';
|
||||||
|
import { captureException } from '@sentry/sveltekit';
|
||||||
|
import { get, writable } from 'svelte/store';
|
||||||
|
|
||||||
|
type LabelListState = AsyncState<readonly LabelViewModel[]>;
|
||||||
|
|
||||||
|
export class LabelsListedStore implements BaseStore<LabelListState, void> {
|
||||||
|
private readonly state = writable<LabelListState>(AsyncState.idle([]));
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly getAllLabelsUseCase: GetAllLabelsUseCase,
|
||||||
|
initialData?: readonly LabelViewModel[]
|
||||||
|
) {
|
||||||
|
if (initialData) {
|
||||||
|
this.state.set(AsyncState.idle(initialData));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get subscribe() {
|
||||||
|
return this.state.subscribe;
|
||||||
|
}
|
||||||
|
|
||||||
|
get trigger() {
|
||||||
|
return () => this.loadLabels();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async loadLabels(): Promise<LabelListState> {
|
||||||
|
this.state.set(AsyncState.loading(get(this.state).data));
|
||||||
|
|
||||||
|
let result: LabelListState;
|
||||||
|
try {
|
||||||
|
const labels = await this.getAllLabelsUseCase.execute();
|
||||||
|
const labelViewModels = labels.map((label) => LabelViewModel.fromEntity(label));
|
||||||
|
result = AsyncState.success(labelViewModels);
|
||||||
|
} catch (e) {
|
||||||
|
result = AsyncState.error(e, get(this.state).data);
|
||||||
|
captureException(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.state.set(result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,12 @@
|
|||||||
|
import type { Color } from '$lib/label/domain/entity/color';
|
||||||
|
import type { Label } from '$lib/label/domain/entity/label';
|
||||||
|
|
||||||
|
export interface LabelRepository {
|
||||||
|
getAllLabels(): Promise<Label[]>;
|
||||||
|
createLabel(params: CreateLabelParams): Promise<Label>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateLabelParams {
|
||||||
|
name: string;
|
||||||
|
color: Color;
|
||||||
|
}
|
@ -0,0 +1,13 @@
|
|||||||
|
import type {
|
||||||
|
CreateLabelParams,
|
||||||
|
LabelRepository,
|
||||||
|
} from '$lib/label/application/gateway/labelRepository';
|
||||||
|
import type { Label } from '$lib/label/domain/entity/label';
|
||||||
|
|
||||||
|
export class CreateLabelUseCase {
|
||||||
|
constructor(private readonly labelRepository: LabelRepository) {}
|
||||||
|
|
||||||
|
async execute(params: CreateLabelParams): Promise<Label> {
|
||||||
|
return this.labelRepository.createLabel(params);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,10 @@
|
|||||||
|
import type { LabelRepository } from '$lib/label/application/gateway/labelRepository';
|
||||||
|
import type { Label } from '$lib/label/domain/entity/label';
|
||||||
|
|
||||||
|
export class GetAllLabelsUseCase {
|
||||||
|
constructor(private readonly labelRepository: LabelRepository) {}
|
||||||
|
|
||||||
|
execute(): Promise<Label[]> {
|
||||||
|
return this.labelRepository.getAllLabels();
|
||||||
|
}
|
||||||
|
}
|
39
frontend/src/lib/label/framework/api/labelApiServiceImpl.ts
Normal file
39
frontend/src/lib/label/framework/api/labelApiServiceImpl.ts
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import { HttpError } from '$lib/common/framework/web/httpError';
|
||||||
|
import { Environment } from '$lib/environment';
|
||||||
|
import type { CreateLabelRequestDto } from '$lib/label/adapter/gateway/createLabelRequestDto';
|
||||||
|
import type { LabelApiService } from '$lib/label/adapter/gateway/labelApiService';
|
||||||
|
import { LabelResponseDto } from '$lib/label/adapter/gateway/labelResponseDto';
|
||||||
|
|
||||||
|
export class LabelApiServiceImpl implements LabelApiService {
|
||||||
|
constructor(private readonly fetchFn: typeof fetch) {}
|
||||||
|
|
||||||
|
async getAllLabels(): Promise<LabelResponseDto[]> {
|
||||||
|
const url = new URL('label', Environment.API_BASE_URL);
|
||||||
|
|
||||||
|
const response = await this.fetchFn(url);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new HttpError(response.status, url);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
return data.map(LabelResponseDto.fromJson);
|
||||||
|
}
|
||||||
|
|
||||||
|
async createLabel(payload: CreateLabelRequestDto): Promise<LabelResponseDto> {
|
||||||
|
const url = new URL('label', Environment.API_BASE_URL);
|
||||||
|
|
||||||
|
const response = await this.fetchFn(url, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(payload.toJson()),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new HttpError(response.status, url);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
return LabelResponseDto.fromJson(data);
|
||||||
|
}
|
||||||
|
}
|
127
frontend/src/lib/label/framework/ui/CreateLabelDialog.svelte
Normal file
127
frontend/src/lib/label/framework/ui/CreateLabelDialog.svelte
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
<script module lang="ts">
|
||||||
|
import z from 'zod';
|
||||||
|
|
||||||
|
const formSchema = z.object({
|
||||||
|
name: z.string().trim().nonempty(),
|
||||||
|
color: z.string().regex(/^#?[a-f\d]{6}$/i),
|
||||||
|
});
|
||||||
|
|
||||||
|
type FormParams = z.infer<typeof formSchema>;
|
||||||
|
export type CreateLabelDialogFormParams = FormParams;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { Button, buttonVariants } from '$lib/common/framework/components/ui/button';
|
||||||
|
import { Dialog } from '$lib/common/framework/components/ui/dialog';
|
||||||
|
import DialogContent from '$lib/common/framework/components/ui/dialog/dialog-content.svelte';
|
||||||
|
import DialogFooter from '$lib/common/framework/components/ui/dialog/dialog-footer.svelte';
|
||||||
|
import DialogHeader from '$lib/common/framework/components/ui/dialog/dialog-header.svelte';
|
||||||
|
import DialogTitle from '$lib/common/framework/components/ui/dialog/dialog-title.svelte';
|
||||||
|
import DialogTrigger from '$lib/common/framework/components/ui/dialog/dialog-trigger.svelte';
|
||||||
|
import Input from '$lib/common/framework/components/ui/input/input.svelte';
|
||||||
|
import Label from '$lib/common/framework/components/ui/label/label.svelte';
|
||||||
|
import PostLabel from '$lib/label/framework/ui/PostLabel.svelte';
|
||||||
|
import { LabelViewModel } from '$lib/label/adapter/presenter/labelViewModel';
|
||||||
|
import { Label as LabelEntity } from '$lib/label/domain/entity/label';
|
||||||
|
import { ColorViewModel } from '$lib/label/adapter/presenter/colorViewModel';
|
||||||
|
|
||||||
|
const {
|
||||||
|
disabled,
|
||||||
|
onSubmit: createLabel,
|
||||||
|
}: {
|
||||||
|
disabled: boolean;
|
||||||
|
onSubmit: (params: FormParams) => Promise<boolean>;
|
||||||
|
} = $props();
|
||||||
|
|
||||||
|
let open = $state(false);
|
||||||
|
|
||||||
|
let formData = $state<FormParams>({
|
||||||
|
name: '',
|
||||||
|
color: '#dddddd',
|
||||||
|
});
|
||||||
|
let formErrors = $state<Partial<Record<keyof FormParams, string>>>({});
|
||||||
|
|
||||||
|
const previewLabel = $derived(
|
||||||
|
LabelViewModel.fromEntity(
|
||||||
|
new LabelEntity({
|
||||||
|
id: -1,
|
||||||
|
name: formData.name || 'Preview',
|
||||||
|
color: ColorViewModel.fromHex(formData.color).toEntity(),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
async function onSubmit(event: SubmitEvent) {
|
||||||
|
event.preventDefault();
|
||||||
|
formErrors = {};
|
||||||
|
|
||||||
|
const parseResult = formSchema.safeParse(formData);
|
||||||
|
if (parseResult.error) {
|
||||||
|
const errorResult = z.treeifyError(parseResult.error).properties;
|
||||||
|
formErrors.name = errorResult?.name?.errors[0];
|
||||||
|
formErrors.color = errorResult?.color?.errors[0];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isSuccess = await createLabel(formData);
|
||||||
|
if (!isSuccess) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
formData = {
|
||||||
|
name: '',
|
||||||
|
color: '#dddddd',
|
||||||
|
};
|
||||||
|
open = false;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Dialog bind:open>
|
||||||
|
<DialogTrigger class={buttonVariants({ variant: 'default' })}>Create</DialogTrigger>
|
||||||
|
<DialogContent
|
||||||
|
showCloseButton={false}
|
||||||
|
onInteractOutside={(e) => e.preventDefault()}
|
||||||
|
onEscapeKeydown={(e) => e.preventDefault()}
|
||||||
|
>
|
||||||
|
<DialogHeader class="mb-4">
|
||||||
|
<DialogTitle>Create Label</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<form id="create-label-form" onsubmit={onSubmit} class="space-y-3">
|
||||||
|
<div>
|
||||||
|
<Label for="name-input" class="pb-2">Name</Label>
|
||||||
|
<Input
|
||||||
|
id="name-input"
|
||||||
|
type="text"
|
||||||
|
aria-invalid={formErrors.name !== undefined}
|
||||||
|
bind:value={formData.name}
|
||||||
|
/>
|
||||||
|
{#if formErrors.name}
|
||||||
|
<p class="text-sm text-red-500">{formErrors.name}</p>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Label for="color-input" class="pb-2">Color</Label>
|
||||||
|
<Input
|
||||||
|
id="color-input"
|
||||||
|
type="color"
|
||||||
|
class="w-16"
|
||||||
|
aria-invalid={formErrors.color !== undefined}
|
||||||
|
bind:value={formData.color}
|
||||||
|
/>
|
||||||
|
{#if formErrors.color}
|
||||||
|
<p class="text-sm text-red-500">{formErrors.color}</p>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<DialogFooter class="mt-6 flex flex-row items-center">
|
||||||
|
<div class="me-auto">
|
||||||
|
<PostLabel label={previewLabel} />
|
||||||
|
</div>
|
||||||
|
<Button variant="outline" onclick={() => (open = false)} {disabled}>Cancel</Button>
|
||||||
|
<Button type="submit" form="create-label-form" {disabled}>Submit</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
@ -0,0 +1,90 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import TableBody from '$lib/common/framework/components/ui/table/table-body.svelte';
|
||||||
|
import TableCell from '$lib/common/framework/components/ui/table/table-cell.svelte';
|
||||||
|
import TableHead from '$lib/common/framework/components/ui/table/table-head.svelte';
|
||||||
|
import TableHeader from '$lib/common/framework/components/ui/table/table-header.svelte';
|
||||||
|
import TableRow from '$lib/common/framework/components/ui/table/table-row.svelte';
|
||||||
|
import Table from '$lib/common/framework/components/ui/table/table.svelte';
|
||||||
|
import { LabelCreatedStore } from '$lib/label/adapter/presenter/labelCreatedStore';
|
||||||
|
import { LabelsListedStore } from '$lib/label/adapter/presenter/labelsListedStore';
|
||||||
|
import CreateLabelDialog, {
|
||||||
|
type CreateLabelDialogFormParams,
|
||||||
|
} from '$lib/label/framework/ui/CreateLabelDialog.svelte';
|
||||||
|
import PostLabel from '$lib/label/framework/ui/PostLabel.svelte';
|
||||||
|
import { ColorViewModel } from '$lib/label/adapter/presenter/colorViewModel';
|
||||||
|
import { getContext, onMount } from 'svelte';
|
||||||
|
import { toast } from 'svelte-sonner';
|
||||||
|
|
||||||
|
const labelCreatedStore = getContext<LabelCreatedStore>(LabelCreatedStore.name);
|
||||||
|
const labelCreatedState = $derived($labelCreatedStore);
|
||||||
|
const { trigger: createLabel } = labelCreatedStore;
|
||||||
|
|
||||||
|
const labelsListedStore = getContext<LabelsListedStore>(LabelsListedStore.name);
|
||||||
|
const labelsListedState = $derived($labelsListedStore);
|
||||||
|
const { trigger: loadLabels } = labelsListedStore;
|
||||||
|
|
||||||
|
async function onCreateLabelDialogSubmit(params: CreateLabelDialogFormParams): Promise<boolean> {
|
||||||
|
const colorViewModel = ColorViewModel.fromHex(params.color);
|
||||||
|
const color = colorViewModel.toEntity();
|
||||||
|
|
||||||
|
const state = await createLabel({
|
||||||
|
name: params.name,
|
||||||
|
color: color,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (state.isSuccess()) {
|
||||||
|
loadLabels();
|
||||||
|
toast.success(`Label created successfully with ID: ${state.data.id}`);
|
||||||
|
} else if (state.isError()) {
|
||||||
|
toast.error('Failed to create label', {
|
||||||
|
description: state.error.message,
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => loadLabels());
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="dashboard-container mb-10">
|
||||||
|
<div class="flex flex-row items-center justify-between">
|
||||||
|
<h1 class="py-16 text-5xl font-bold text-gray-800">Label</h1>
|
||||||
|
<CreateLabelDialog
|
||||||
|
disabled={labelCreatedState.isLoading()}
|
||||||
|
onSubmit={onCreateLabelDialogSubmit}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>ID</TableHead>
|
||||||
|
<TableHead>Name</TableHead>
|
||||||
|
<TableHead>Color</TableHead>
|
||||||
|
<TableHead>Preview</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{#if labelsListedState.isSuccess()}
|
||||||
|
{#each labelsListedState.data as label (label.id)}
|
||||||
|
<TableRow>
|
||||||
|
<TableCell>{label.id}</TableCell>
|
||||||
|
<TableCell><span class="text-wrap">{label.name}</span></TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<div class="size-4 rounded-full" style="background-color: {label.color.hex};"></div>
|
||||||
|
<span class="font-mono text-sm">{label.color.hex}</span>
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<div>
|
||||||
|
<PostLabel {label} />
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
{/each}
|
||||||
|
{/if}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
@ -5,7 +5,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="flex flex-row items-center gap-x-1 rounded-full px-2 py-0.5"
|
class="flex h-fit w-fit flex-row items-center gap-x-1 rounded-full px-2 py-0.5"
|
||||||
style="background-color: {label.color.hex};"
|
style="background-color: {label.color.hex};"
|
||||||
>
|
>
|
||||||
<div class="size-2 rounded-full" style="background-color: {label.color.darken(0.2).hex};"></div>
|
<div class="size-2 rounded-full" style="background-color: {label.color.darken(0.2).hex};"></div>
|
||||||
|
@ -1,14 +1,14 @@
|
|||||||
import { LabelResponseDto, LabelResponseSchema } from '$lib/label/adapter/gateway/labelResponseDto';
|
import { LabelResponseDto, labelResponseSchema } from '$lib/label/adapter/gateway/labelResponseDto';
|
||||||
import { PostInfo } from '$lib/post/domain/entity/postInfo';
|
import { PostInfo } from '$lib/post/domain/entity/postInfo';
|
||||||
import z from 'zod';
|
import z from 'zod';
|
||||||
|
|
||||||
export const PostInfoResponseSchema = z.object({
|
export const postInfoResponseSchema = z.object({
|
||||||
id: z.int32(),
|
id: z.int32(),
|
||||||
semantic_id: z.string(),
|
semantic_id: z.string(),
|
||||||
title: z.string(),
|
title: z.string(),
|
||||||
description: z.string(),
|
description: z.string(),
|
||||||
preview_image_url: z.url().nullable(),
|
preview_image_url: z.url().nullable(),
|
||||||
labels: z.array(LabelResponseSchema),
|
labels: z.array(labelResponseSchema),
|
||||||
published_time: z.iso.datetime({ offset: true }).nullable(),
|
published_time: z.iso.datetime({ offset: true }).nullable(),
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -40,7 +40,7 @@ export class PostInfoResponseDto {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static fromJson(json: unknown): PostInfoResponseDto {
|
static fromJson(json: unknown): PostInfoResponseDto {
|
||||||
const parsedJson = PostInfoResponseSchema.parse(json);
|
const parsedJson = postInfoResponseSchema.parse(json);
|
||||||
|
|
||||||
let published_time: Date | null = null;
|
let published_time: Date | null = null;
|
||||||
if (parsedJson.published_time !== null) {
|
if (parsedJson.published_time !== null) {
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
import {
|
import {
|
||||||
PostInfoResponseDto,
|
PostInfoResponseDto,
|
||||||
PostInfoResponseSchema,
|
postInfoResponseSchema,
|
||||||
} from '$lib/post/adapter/gateway/postInfoResponseDto';
|
} from '$lib/post/adapter/gateway/postInfoResponseDto';
|
||||||
import { Post } from '$lib/post/domain/entity/post';
|
import { Post } from '$lib/post/domain/entity/post';
|
||||||
import z from 'zod';
|
import z from 'zod';
|
||||||
|
|
||||||
export const PostResponseSchema = z.object({
|
export const postResponseSchema = z.object({
|
||||||
id: z.int32(),
|
id: z.int32(),
|
||||||
info: PostInfoResponseSchema,
|
info: postInfoResponseSchema,
|
||||||
content: z.string(),
|
content: z.string(),
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -23,7 +23,7 @@ export class PostResponseDto {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static fromJson(json: unknown): PostResponseDto {
|
static fromJson(json: unknown): PostResponseDto {
|
||||||
const parsedJson = PostResponseSchema.parse(json);
|
const parsedJson = postResponseSchema.parse(json);
|
||||||
return new PostResponseDto({
|
return new PostResponseDto({
|
||||||
id: parsedJson.id,
|
id: parsedJson.id,
|
||||||
info: PostInfoResponseDto.fromJson(parsedJson.info),
|
info: PostInfoResponseDto.fromJson(parsedJson.info),
|
||||||
|
@ -11,14 +11,7 @@ type PostState = AsyncState<PostViewModel>;
|
|||||||
export class PostCreatedStore implements BaseStore<PostState, CreatePostParams> {
|
export class PostCreatedStore implements BaseStore<PostState, CreatePostParams> {
|
||||||
private readonly state = writable<PostState>(AsyncState.idle<PostViewModel>(null));
|
private readonly state = writable<PostState>(AsyncState.idle<PostViewModel>(null));
|
||||||
|
|
||||||
constructor(
|
constructor(private readonly createPostUseCase: CreatePostUseCase) {}
|
||||||
private readonly createPostUseCase: CreatePostUseCase,
|
|
||||||
initialData?: PostViewModel
|
|
||||||
) {
|
|
||||||
if (initialData) {
|
|
||||||
this.state.set(AsyncState.idle(initialData));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
get subscribe() {
|
get subscribe() {
|
||||||
return this.state.subscribe;
|
return this.state.subscribe;
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
.max(100)
|
.max(100)
|
||||||
.regex(/\D/)
|
.regex(/\D/)
|
||||||
.regex(/^[a-zA-Z0-9_-]+$/),
|
.regex(/^[a-zA-Z0-9_-]+$/),
|
||||||
title: z.string().trim().nonempty().max(100),
|
title: z.string().trim().nonempty(),
|
||||||
});
|
});
|
||||||
|
|
||||||
type FormParams = z.infer<typeof formSchema>;
|
type FormParams = z.infer<typeof formSchema>;
|
||||||
@ -30,7 +30,7 @@
|
|||||||
onSubmit: createPost,
|
onSubmit: createPost,
|
||||||
}: {
|
}: {
|
||||||
disabled: boolean;
|
disabled: boolean;
|
||||||
onSubmit: (params: FormParams) => Promise<void>;
|
onSubmit: (params: FormParams) => Promise<boolean>;
|
||||||
} = $props();
|
} = $props();
|
||||||
|
|
||||||
let open = $state(false);
|
let open = $state(false);
|
||||||
@ -45,16 +45,16 @@
|
|||||||
const parseResult = formSchema.safeParse(formData);
|
const parseResult = formSchema.safeParse(formData);
|
||||||
if (parseResult.error) {
|
if (parseResult.error) {
|
||||||
const errorResult = z.treeifyError(parseResult.error).properties;
|
const errorResult = z.treeifyError(parseResult.error).properties;
|
||||||
if (errorResult?.semanticId?.errors) {
|
formErrors.semanticId = errorResult?.semanticId?.errors[0];
|
||||||
formErrors.semanticId = errorResult.semanticId.errors[0];
|
formErrors.title = errorResult?.title?.errors[0];
|
||||||
}
|
return;
|
||||||
if (errorResult?.title?.errors) {
|
}
|
||||||
formErrors.title = errorResult.title.errors[0];
|
|
||||||
}
|
const isSuccess = await createPost(formData);
|
||||||
|
if (!isSuccess) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await createPost(formData);
|
|
||||||
formData = { semanticId: '', title: '' };
|
formData = { semanticId: '', title: '' };
|
||||||
open = false;
|
open = false;
|
||||||
}
|
}
|
||||||
|
@ -22,16 +22,20 @@
|
|||||||
const postsListedState = $derived($postsListedStore);
|
const postsListedState = $derived($postsListedStore);
|
||||||
const { trigger: loadPosts } = postsListedStore;
|
const { trigger: loadPosts } = postsListedStore;
|
||||||
|
|
||||||
async function onCreatePostDialogSubmit(params: CreatePostDialogFormParams) {
|
async function onCreatePostDialogSubmit(params: CreatePostDialogFormParams): Promise<boolean> {
|
||||||
const state = await createPost(params);
|
const state = await createPost(params);
|
||||||
|
|
||||||
if (state.isSuccess()) {
|
if (state.isSuccess()) {
|
||||||
|
loadPosts({ showUnpublished: true });
|
||||||
toast.success(`Post created successfully with ID: ${state.data.id}`);
|
toast.success(`Post created successfully with ID: ${state.data.id}`);
|
||||||
} else if (state.isError()) {
|
} else if (state.isError()) {
|
||||||
toast.error('Failed to create post', {
|
toast.error('Failed to create post', {
|
||||||
description: state.error.message,
|
description: state.error.message,
|
||||||
});
|
});
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
onMount(() => loadPosts({ showUnpublished: true }));
|
onMount(() => loadPosts({ showUnpublished: true }));
|
||||||
@ -56,13 +60,15 @@
|
|||||||
{#each postsListedState.data as postInfo (postInfo.id)}
|
{#each postsListedState.data as postInfo (postInfo.id)}
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell>{postInfo.id}</TableCell>
|
<TableCell>{postInfo.id}</TableCell>
|
||||||
<TableCell>{postInfo.title}</TableCell>
|
<TableCell><span class="text-wrap">{postInfo.title}</span></TableCell>
|
||||||
<TableCell class="flex flex-row flex-wrap gap-2">
|
<TableCell>
|
||||||
|
<div class="flex flex-row flex-wrap gap-2">
|
||||||
{#each postInfo.labels as label (label.id)}
|
{#each postInfo.labels as label (label.id)}
|
||||||
<PostLabel {label} />
|
<PostLabel {label} />
|
||||||
{/each}
|
{/each}
|
||||||
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>{postInfo.formattedPublishedTime || '---'}</TableCell>
|
<TableCell>{postInfo.formattedPublishedTime}</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
{/each}
|
{/each}
|
||||||
{/if}
|
{/if}
|
||||||
|
13
frontend/src/routes/dashboard/label/+page.server.ts
Normal file
13
frontend/src/routes/dashboard/label/+page.server.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import type { PageServerLoad } from './$types';
|
||||||
|
|
||||||
|
export const load: PageServerLoad = async ({ locals }) => {
|
||||||
|
const { container } = locals;
|
||||||
|
const store = container.createLabelsListedStore();
|
||||||
|
const { trigger: loadLabels } = store;
|
||||||
|
|
||||||
|
const state = await loadLabels();
|
||||||
|
|
||||||
|
return {
|
||||||
|
dehydratedData: state.data?.map((label) => label.dehydrate()),
|
||||||
|
};
|
||||||
|
};
|
@ -1 +1,21 @@
|
|||||||
<div>Label</div>
|
<script lang="ts">
|
||||||
|
import { Container } from '$lib/container';
|
||||||
|
import { LabelCreatedStore } from '$lib/label/adapter/presenter/labelCreatedStore';
|
||||||
|
import { LabelsListedStore } from '$lib/label/adapter/presenter/labelsListedStore';
|
||||||
|
import LabelManagementPage from '$lib/label/framework/ui/LabelManagementPage.svelte';
|
||||||
|
import { getContext, setContext } from 'svelte';
|
||||||
|
import type { PageProps } from './$types';
|
||||||
|
import { LabelViewModel } from '$lib/label/adapter/presenter/labelViewModel';
|
||||||
|
|
||||||
|
const { data }: PageProps = $props();
|
||||||
|
const container = getContext<Container>(Container.name);
|
||||||
|
|
||||||
|
const labelCreatedStore = container.createLabelCreatedStore();
|
||||||
|
setContext(LabelCreatedStore.name, labelCreatedStore);
|
||||||
|
|
||||||
|
const initialData = data.dehydratedData?.map((label) => LabelViewModel.rehydrate(label));
|
||||||
|
const labelsListedStore = container.createLabelsListedStore(initialData);
|
||||||
|
setContext(LabelsListedStore.name, labelsListedStore);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<LabelManagementPage />
|
||||||
|
Loading…
x
Reference in New Issue
Block a user