feat: implement update label functionality with associated stores and API integration
All checks were successful
Frontend CI / build (push) Successful in 1m39s
All checks were successful
Frontend CI / build (push) Successful in 1m39s
This commit is contained in:
parent
300ae2dc05
commit
add274be1b
@ -7,11 +7,13 @@ import { AuthApiServiceImpl } from '$lib/auth/framework/api/authApiServiceImpl';
|
|||||||
import type { LabelApiService } from '$lib/label/adapter/gateway/labelApiService';
|
import type { LabelApiService } from '$lib/label/adapter/gateway/labelApiService';
|
||||||
import { LabelRepositoryImpl } from '$lib/label/adapter/gateway/labelRepositoryImpl';
|
import { LabelRepositoryImpl } from '$lib/label/adapter/gateway/labelRepositoryImpl';
|
||||||
import { LabelCreatedStore } from '$lib/label/adapter/presenter/labelCreatedStore';
|
import { LabelCreatedStore } from '$lib/label/adapter/presenter/labelCreatedStore';
|
||||||
|
import { LabelUpdatedStore } from '$lib/label/adapter/presenter/labelUpdatedStore';
|
||||||
import type { LabelViewModel } from '$lib/label/adapter/presenter/labelViewModel';
|
import type { LabelViewModel } from '$lib/label/adapter/presenter/labelViewModel';
|
||||||
import { LabelsListedStore } from '$lib/label/adapter/presenter/labelsListedStore';
|
import { LabelsListedStore } from '$lib/label/adapter/presenter/labelsListedStore';
|
||||||
import type { LabelRepository } from '$lib/label/application/gateway/labelRepository';
|
import type { LabelRepository } from '$lib/label/application/gateway/labelRepository';
|
||||||
import { CreateLabelUseCase } from '$lib/label/application/useCase/createLabelUseCase';
|
import { CreateLabelUseCase } from '$lib/label/application/useCase/createLabelUseCase';
|
||||||
import { GetAllLabelsUseCase } from '$lib/label/application/useCase/getAllLabelsUseCase';
|
import { GetAllLabelsUseCase } from '$lib/label/application/useCase/getAllLabelsUseCase';
|
||||||
|
import { UpdateLabelUseCase } from '$lib/label/application/useCase/updateLabelUseCase';
|
||||||
import { LabelApiServiceImpl } from '$lib/label/framework/api/labelApiServiceImpl';
|
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';
|
||||||
@ -74,6 +76,10 @@ export class Container {
|
|||||||
createLabelCreatedStore(): LabelCreatedStore {
|
createLabelCreatedStore(): LabelCreatedStore {
|
||||||
return new LabelCreatedStore(this.useCases.createLabelUseCase);
|
return new LabelCreatedStore(this.useCases.createLabelUseCase);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
createLabelUpdatedStore(): LabelUpdatedStore {
|
||||||
|
return new LabelUpdatedStore(this.useCases.updateLabelUseCase);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class ApiServices {
|
class ApiServices {
|
||||||
@ -153,6 +159,7 @@ class UseCases {
|
|||||||
private _getAllLabelsUseCase?: GetAllLabelsUseCase;
|
private _getAllLabelsUseCase?: GetAllLabelsUseCase;
|
||||||
private _getLabelUseCase?: GetLabelUseCase;
|
private _getLabelUseCase?: GetLabelUseCase;
|
||||||
private _createLabelUseCase?: CreateLabelUseCase;
|
private _createLabelUseCase?: CreateLabelUseCase;
|
||||||
|
private _updateLabelUseCase?: UpdateLabelUseCase;
|
||||||
|
|
||||||
constructor(repositories: Repositories) {
|
constructor(repositories: Repositories) {
|
||||||
this.repositories = repositories;
|
this.repositories = repositories;
|
||||||
@ -197,4 +204,9 @@ class UseCases {
|
|||||||
this._createLabelUseCase ??= new CreateLabelUseCase(this.repositories.labelRepository);
|
this._createLabelUseCase ??= new CreateLabelUseCase(this.repositories.labelRepository);
|
||||||
return this._createLabelUseCase;
|
return this._createLabelUseCase;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get updateLabelUseCase(): UpdateLabelUseCase {
|
||||||
|
this._updateLabelUseCase ??= new UpdateLabelUseCase(this.repositories.labelRepository);
|
||||||
|
return this._updateLabelUseCase;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
import type { CreateLabelRequestDto } from '$lib/label/adapter/gateway/createLabelRequestDto';
|
import type { CreateLabelRequestDto } from '$lib/label/adapter/gateway/createLabelRequestDto';
|
||||||
import type { LabelResponseDto } from '$lib/label/adapter/gateway/labelResponseDto';
|
import type { LabelResponseDto } from '$lib/label/adapter/gateway/labelResponseDto';
|
||||||
|
import type { UpdateLabelRequestDto } from '$lib/label/adapter/gateway/updateLabelRequestDto';
|
||||||
|
|
||||||
export interface LabelApiService {
|
export interface LabelApiService {
|
||||||
getAllLabels(): Promise<LabelResponseDto[]>;
|
getAllLabels(): Promise<LabelResponseDto[]>;
|
||||||
getLabelById(id: number): Promise<LabelResponseDto | null>;
|
getLabelById(id: number): Promise<LabelResponseDto | null>;
|
||||||
createLabel(payload: CreateLabelRequestDto): Promise<LabelResponseDto>;
|
createLabel(payload: CreateLabelRequestDto): Promise<LabelResponseDto>;
|
||||||
|
updateLabel(id: number, payload: UpdateLabelRequestDto): Promise<LabelResponseDto>;
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
import { CreateLabelRequestDto } from '$lib/label/adapter/gateway/createLabelRequestDto';
|
import { CreateLabelRequestDto } from '$lib/label/adapter/gateway/createLabelRequestDto';
|
||||||
import type { LabelApiService } from '$lib/label/adapter/gateway/labelApiService';
|
import type { LabelApiService } from '$lib/label/adapter/gateway/labelApiService';
|
||||||
|
import { UpdateLabelRequestDto } from '$lib/label/adapter/gateway/updateLabelRequestDto';
|
||||||
import type {
|
import type {
|
||||||
CreateLabelParams,
|
CreateLabelParams,
|
||||||
LabelRepository,
|
LabelRepository,
|
||||||
|
UpdateLabelParams,
|
||||||
} from '$lib/label/application/gateway/labelRepository';
|
} from '$lib/label/application/gateway/labelRepository';
|
||||||
import type { Label } from '$lib/label/domain/entity/label';
|
import type { Label } from '$lib/label/domain/entity/label';
|
||||||
|
|
||||||
@ -24,4 +26,10 @@ export class LabelRepositoryImpl implements LabelRepository {
|
|||||||
const responseDto = await this.labelApiService.createLabel(requestDto);
|
const responseDto = await this.labelApiService.createLabel(requestDto);
|
||||||
return responseDto.toEntity();
|
return responseDto.toEntity();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async updateLabel(id: number, params: UpdateLabelParams): Promise<Label> {
|
||||||
|
const requestDto = UpdateLabelRequestDto.fromParams(params);
|
||||||
|
const responseDto = await this.labelApiService.updateLabel(id, requestDto);
|
||||||
|
return responseDto.toEntity();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,26 @@
|
|||||||
|
import { ColorDto } from '$lib/label/adapter/gateway/colorDto';
|
||||||
|
import type { UpdateLabelParams } from '$lib/label/application/gateway/labelRepository';
|
||||||
|
|
||||||
|
export class UpdateLabelRequestDto {
|
||||||
|
readonly name: string;
|
||||||
|
readonly color: ColorDto;
|
||||||
|
|
||||||
|
private constructor(props: { name: string; color: ColorDto }) {
|
||||||
|
this.name = props.name;
|
||||||
|
this.color = props.color;
|
||||||
|
}
|
||||||
|
|
||||||
|
static fromParams(params: UpdateLabelParams): UpdateLabelRequestDto {
|
||||||
|
return new UpdateLabelRequestDto({
|
||||||
|
name: params.name,
|
||||||
|
color: ColorDto.fromEntity(params.color),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
toJson() {
|
||||||
|
return {
|
||||||
|
name: this.name,
|
||||||
|
color: this.color.toJson(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,47 @@
|
|||||||
|
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 { UpdateLabelParams } from '$lib/label/application/gateway/labelRepository';
|
||||||
|
import type { UpdateLabelUseCase } from '$lib/label/application/useCase/updateLabelUseCase';
|
||||||
|
import { captureException } from '@sentry/sveltekit';
|
||||||
|
import { get, writable } from 'svelte/store';
|
||||||
|
|
||||||
|
type LabelState = AsyncState<LabelViewModel>;
|
||||||
|
|
||||||
|
interface UpdateLabelTriggerParams {
|
||||||
|
id: number;
|
||||||
|
params: UpdateLabelParams;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class LabelUpdatedStore implements BaseStore<LabelState, UpdateLabelTriggerParams> {
|
||||||
|
static readonly name = 'LabelUpdatedStore';
|
||||||
|
|
||||||
|
private readonly state = writable<LabelState>(AsyncState.idle<LabelViewModel>(null));
|
||||||
|
|
||||||
|
constructor(private readonly updateLabelUseCase: UpdateLabelUseCase) {}
|
||||||
|
|
||||||
|
get subscribe() {
|
||||||
|
return this.state.subscribe;
|
||||||
|
}
|
||||||
|
|
||||||
|
get trigger() {
|
||||||
|
return (params: UpdateLabelTriggerParams) => this.updateLabel(params);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async updateLabel(params: UpdateLabelTriggerParams): Promise<LabelState> {
|
||||||
|
this.state.set(AsyncState.loading(get(this.state).data));
|
||||||
|
|
||||||
|
let result: LabelState;
|
||||||
|
try {
|
||||||
|
const label = await this.updateLabelUseCase.execute(params.id, params.params);
|
||||||
|
const labelViewModel = LabelViewModel.fromEntity(label);
|
||||||
|
result = AsyncState.success(labelViewModel);
|
||||||
|
} catch (error) {
|
||||||
|
captureException(error);
|
||||||
|
result = AsyncState.error(error, get(this.state).data);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.state.set(result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
@ -5,9 +5,15 @@ export interface LabelRepository {
|
|||||||
getAllLabels(): Promise<Label[]>;
|
getAllLabels(): Promise<Label[]>;
|
||||||
getLabelById(id: number): Promise<Label | null>;
|
getLabelById(id: number): Promise<Label | null>;
|
||||||
createLabel(params: CreateLabelParams): Promise<Label>;
|
createLabel(params: CreateLabelParams): Promise<Label>;
|
||||||
|
updateLabel(id: number, params: UpdateLabelParams): Promise<Label>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CreateLabelParams {
|
export interface CreateLabelParams {
|
||||||
name: string;
|
name: string;
|
||||||
color: Color;
|
color: Color;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface UpdateLabelParams {
|
||||||
|
name: string;
|
||||||
|
color: Color;
|
||||||
|
}
|
||||||
|
@ -0,0 +1,13 @@
|
|||||||
|
import type { Label } from '$lib/label/domain/entity/label';
|
||||||
|
import type {
|
||||||
|
LabelRepository,
|
||||||
|
UpdateLabelParams,
|
||||||
|
} from '$lib/label/application/gateway/labelRepository';
|
||||||
|
|
||||||
|
export class UpdateLabelUseCase {
|
||||||
|
constructor(private readonly labelRepository: LabelRepository) {}
|
||||||
|
|
||||||
|
async execute(id: number, params: UpdateLabelParams): Promise<Label> {
|
||||||
|
return this.labelRepository.updateLabel(id, params);
|
||||||
|
}
|
||||||
|
}
|
@ -3,6 +3,7 @@ import { Environment } from '$lib/environment';
|
|||||||
import type { CreateLabelRequestDto } from '$lib/label/adapter/gateway/createLabelRequestDto';
|
import type { CreateLabelRequestDto } from '$lib/label/adapter/gateway/createLabelRequestDto';
|
||||||
import type { LabelApiService } from '$lib/label/adapter/gateway/labelApiService';
|
import type { LabelApiService } from '$lib/label/adapter/gateway/labelApiService';
|
||||||
import { LabelResponseDto } from '$lib/label/adapter/gateway/labelResponseDto';
|
import { LabelResponseDto } from '$lib/label/adapter/gateway/labelResponseDto';
|
||||||
|
import type { UpdateLabelRequestDto } from '$lib/label/adapter/gateway/updateLabelRequestDto';
|
||||||
|
|
||||||
export class LabelApiServiceImpl implements LabelApiService {
|
export class LabelApiServiceImpl implements LabelApiService {
|
||||||
constructor(private readonly fetchFn: typeof fetch) {}
|
constructor(private readonly fetchFn: typeof fetch) {}
|
||||||
@ -52,4 +53,21 @@ export class LabelApiServiceImpl implements LabelApiService {
|
|||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
return LabelResponseDto.fromJson(data);
|
return LabelResponseDto.fromJson(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async updateLabel(id: number, payload: UpdateLabelRequestDto): Promise<LabelResponseDto> {
|
||||||
|
const url = new URL(`label/${id}`, Environment.API_BASE_URL);
|
||||||
|
|
||||||
|
const response = await this.fetchFn(url, {
|
||||||
|
method: 'PUT',
|
||||||
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,14 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { LabelLoadedStore } from '$lib/label/adapter/presenter/labelLoadedStore';
|
import { LabelLoadedStore } from '$lib/label/adapter/presenter/labelLoadedStore';
|
||||||
|
import { LabelUpdatedStore } from '$lib/label/adapter/presenter/labelUpdatedStore';
|
||||||
|
import { ColorViewModel } from '$lib/label/adapter/presenter/colorViewModel';
|
||||||
import ColorCode from '$lib/label/framework/ui/ColorCode.svelte';
|
import ColorCode from '$lib/label/framework/ui/ColorCode.svelte';
|
||||||
import EditLabelDialog, {
|
import EditLabelDialog, {
|
||||||
type EditLabelDialogFormParams,
|
type EditLabelDialogFormParams,
|
||||||
} from '$lib/label/framework/ui/EditLabelDialog.svelte';
|
} from '$lib/label/framework/ui/EditLabelDialog.svelte';
|
||||||
import PostLabel from '$lib/label/framework/ui/PostLabel.svelte';
|
import PostLabel from '$lib/label/framework/ui/PostLabel.svelte';
|
||||||
import { getContext, onMount } from 'svelte';
|
import { getContext, onMount } from 'svelte';
|
||||||
|
import { toast } from 'svelte-sonner';
|
||||||
|
|
||||||
const { id }: { id: number } = $props();
|
const { id }: { id: number } = $props();
|
||||||
|
|
||||||
@ -14,6 +17,10 @@
|
|||||||
const { trigger: loadLabel } = labelLoadedStore;
|
const { trigger: loadLabel } = labelLoadedStore;
|
||||||
const label = $derived(labelLoadedState.data);
|
const label = $derived(labelLoadedState.data);
|
||||||
|
|
||||||
|
const labelUpdatedStore = getContext<LabelUpdatedStore>(LabelUpdatedStore.name);
|
||||||
|
const labelUpdatedState = $derived($labelUpdatedStore);
|
||||||
|
const { trigger: updateLabel } = labelUpdatedStore;
|
||||||
|
|
||||||
const formDefaultValues: EditLabelDialogFormParams | null = $derived.by(() => {
|
const formDefaultValues: EditLabelDialogFormParams | null = $derived.by(() => {
|
||||||
if (labelLoadedState.data === null) {
|
if (labelLoadedState.data === null) {
|
||||||
return null;
|
return null;
|
||||||
@ -25,6 +32,32 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
async function onSubmit(params: EditLabelDialogFormParams): Promise<boolean> {
|
async function onSubmit(params: EditLabelDialogFormParams): Promise<boolean> {
|
||||||
|
if (!label) {
|
||||||
|
toast.error('Label not found');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const colorViewModel = ColorViewModel.fromHex(params.color);
|
||||||
|
const color = colorViewModel.toEntity();
|
||||||
|
|
||||||
|
const state = await updateLabel({
|
||||||
|
id: label.id,
|
||||||
|
params: {
|
||||||
|
name: params.name,
|
||||||
|
color: color,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (state.isSuccess()) {
|
||||||
|
loadLabel(label.id);
|
||||||
|
toast.success('Label updated successfully');
|
||||||
|
} else if (state.isError()) {
|
||||||
|
toast.error('Failed to update label', {
|
||||||
|
description: state.error.message,
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -34,13 +67,15 @@
|
|||||||
<div class="dashboard-container mb-10">
|
<div class="dashboard-container mb-10">
|
||||||
<div class="flex flex-row items-center justify-between">
|
<div class="flex flex-row items-center justify-between">
|
||||||
<h1 class="py-16 text-5xl font-bold text-gray-800">Label Details</h1>
|
<h1 class="py-16 text-5xl font-bold text-gray-800">Label Details</h1>
|
||||||
<EditLabelDialog
|
{#key formDefaultValues}
|
||||||
title="Update Label"
|
<EditLabelDialog
|
||||||
triggerButtonText="Edit"
|
title="Update Label"
|
||||||
disabled={!labelLoadedState.isSuccess()}
|
triggerButtonText="Edit"
|
||||||
defaultValues={formDefaultValues ?? undefined}
|
disabled={!labelLoadedState.isSuccess() || labelUpdatedState.isLoading()}
|
||||||
{onSubmit}
|
defaultValues={formDefaultValues ?? undefined}
|
||||||
/>
|
{onSubmit}
|
||||||
|
/>
|
||||||
|
{/key}
|
||||||
</div>
|
</div>
|
||||||
<div class="grid grid-cols-[auto_1fr] gap-x-8 gap-y-3">
|
<div class="grid grid-cols-[auto_1fr] gap-x-8 gap-y-3">
|
||||||
<span class="font-medium whitespace-nowrap">ID</span>
|
<span class="font-medium whitespace-nowrap">ID</span>
|
||||||
|
@ -4,6 +4,7 @@
|
|||||||
import { Container } from '$lib/container';
|
import { Container } from '$lib/container';
|
||||||
import { LabelViewModel } from '$lib/label/adapter/presenter/labelViewModel';
|
import { LabelViewModel } from '$lib/label/adapter/presenter/labelViewModel';
|
||||||
import { LabelLoadedStore } from '$lib/label/adapter/presenter/labelLoadedStore';
|
import { LabelLoadedStore } from '$lib/label/adapter/presenter/labelLoadedStore';
|
||||||
|
import { LabelUpdatedStore } from '$lib/label/adapter/presenter/labelUpdatedStore';
|
||||||
import LabelContentDashboardPage from '$lib/label/framework/ui/LabelContentDashboardPage.svelte';
|
import LabelContentDashboardPage from '$lib/label/framework/ui/LabelContentDashboardPage.svelte';
|
||||||
|
|
||||||
const { data, params }: PageProps = $props();
|
const { data, params }: PageProps = $props();
|
||||||
@ -13,8 +14,11 @@
|
|||||||
const container = getContext<Container>(Container.name);
|
const container = getContext<Container>(Container.name);
|
||||||
|
|
||||||
const initialData = LabelViewModel.rehydrate(data.dehydratedData);
|
const initialData = LabelViewModel.rehydrate(data.dehydratedData);
|
||||||
const store = container.createLabelLoadedStore(initialData);
|
const labelLoadedStore = container.createLabelLoadedStore(initialData);
|
||||||
setContext(LabelLoadedStore.name, store);
|
setContext(LabelLoadedStore.name, labelLoadedStore);
|
||||||
|
|
||||||
|
const labelUpdatedStore = container.createLabelUpdatedStore();
|
||||||
|
setContext(LabelUpdatedStore.name, labelUpdatedStore);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<LabelContentDashboardPage id={numericId} />
|
<LabelContentDashboardPage id={numericId} />
|
||||||
|
Loading…
x
Reference in New Issue
Block a user