From 8c0a4dd9f41585bfb97db99e209b763e311098a5 Mon Sep 17 00:00:00 2001 From: Jesse Brault Date: Sat, 7 Feb 2026 21:22:18 -0600 Subject: [PATCH] MME-8 Less query, better handling of observables. --- .../image-select/image-select.html | 68 +++++++-------- .../image-select/image-select.ts | 85 ++++++++++++------- src/app/shared/models/Query.model.ts | 1 + src/app/shared/services/EndpointService.ts | 5 ++ src/app/shared/services/ImageService.ts | 68 +++++++++++---- 5 files changed, 143 insertions(+), 84 deletions(-) diff --git a/src/app/pages/recipe-upload-page/steps/enter-recipe-data/image-select/image-select.html b/src/app/pages/recipe-upload-page/steps/enter-recipe-data/image-select/image-select.html index f4ce7ed..8b54763 100644 --- a/src/app/pages/recipe-upload-page/steps/enter-recipe-data/image-select/image-select.html +++ b/src/app/pages/recipe-upload-page/steps/enter-recipe-data/image-select/image-select.html @@ -1,43 +1,41 @@ -@if (imageViewsQuery.isLoading()) { +@if (loadingImages()) { -} @else if (imageViewsQuery.isError()) { +} @else if (loadImagesError()) {

There was an error loading images.

-} @else if (imageViewsQuery.isSuccess()) { +} @else {
- @for (imageQuery of imageViewsWithBlobUrlsQueries(); track $index) { - @if (imageQuery.isLoading()) { - - } @else if (imageQuery.isError()) { -

There was an error loading this image.

- } @else { - @let imageView = imageQuery.data()!; - - imageData!.imageView.alt - -

{{ imageView.filename }}

-
- - Main? - - - - - - -
- } + @for (image of imagesSlice()!.content; track $index) { + + + +

{{ image.filename }}

+
+ + Main? + + + + + + +
}
+ + @if (deleting()) { + + } @else if (deleteError()) { +

There was an error deleting the image.

+ } + (); public readonly selectedUsernameFilename = input(null); @@ -43,37 +43,49 @@ export class ImageSelect { private readonly imageService = inject(ImageService); private readonly dialog = inject(MatDialog); - protected readonly imageViewsQuery = injectQuery(() => ({ - queryKey: ['image-views', this.currentPage(), this.pageSize()], - queryFn: () => - this.imageService.getOwnedImages({ + protected readonly loadingImages = signal(false); + protected readonly loadImagesError = signal(null); + protected readonly imagesSlice = signal | null>(null); + + protected readonly imageCount = signal(0); + + protected readonly deleting = signal(false); + protected readonly deleteError = signal(null); + + public ngOnInit(): void { + this.loadImages(); + } + + private loadImages(): void { + this.loadingImages.set(true); + this.imageService + .getOwnedImageViewsWithBlobUrls({ page: this.currentPage(), + size: this.pageSize(), sort: [ { property: 'created', order: 'DESC', }, ], - size: this.pageSize(), - }), - placeholderData: keepPreviousData, - })); - - protected readonly imageCount = computed(() => { - if (this.imageViewsQuery.isSuccess()) { - return this.imageViewsQuery.data()!.count; - } else { - return 0; - } - }); - - protected readonly imageViewsWithBlobUrlsQueries = injectQueries(() => ({ - queries: - this.imageViewsQuery.data()?.content.map((imageView) => ({ - queryKey: ['image-views-with-blob-urls', imageView.owner.username, imageView.filename], - queryFn: () => this.imageService.getImageViewWithBlobUrl(imageView), - })) ?? [], - })); + }) + .subscribe({ + next: (sliceView) => { + sliceView.content.sort((a, b) => b.created.valueOf() - a.created.valueOf()); + this.loadingImages.set(false); + this.imagesSlice.set(sliceView); + }, + error: (e) => { + this.loadingImages.set(false); + this.loadImagesError.set(e); + }, + }); + this.imageService.getOwnedImagesCount().subscribe({ + next: (count) => { + this.imageCount.set(count); + }, + }); + } protected onPage(pageEvent: PageEvent): void { if (pageEvent.pageIndex < this.currentPage()) { @@ -81,9 +93,10 @@ export class ImageSelect { this.currentPage.update((old) => Math.max(old - 1, 0)); } else { // forward - this.currentPage.update((old) => (this.imageViewsQuery.data()?.slice.hasNext ? old + 1 : old)); + this.currentPage.update((old) => (this.imagesSlice()?.slice.hasNext ? old + 1 : old)); } this.pageSize.set(pageEvent.pageSize); + this.loadImages(); } protected onImageClick(imageView: ImageView): void { @@ -109,8 +122,18 @@ export class ImageSelect { }); } - protected async deleteImage(imageView: ImageView): Promise { - await this.imageService.deleteImage(imageView.owner.username, imageView.filename); + protected deleteImage(imageView: ImageView): void { + this.deleting.set(true); + this.imageService.deleteImage(imageView.owner.username, imageView.filename).subscribe({ + next: () => { + this.deleting.set(false); + this.loadImages(); + }, + error: (e) => { + this.deleting.set(false); + this.deleteError.set(e); + }, + }); } protected readonly faEllipsis = faEllipsis; diff --git a/src/app/shared/models/Query.model.ts b/src/app/shared/models/Query.model.ts index fc2b331..8a09c70 100644 --- a/src/app/shared/models/Query.model.ts +++ b/src/app/shared/models/Query.model.ts @@ -2,6 +2,7 @@ export interface QueryParams { page?: number; size?: number; sort?: Array>; + custom?: Record; } export interface Sort { diff --git a/src/app/shared/services/EndpointService.ts b/src/app/shared/services/EndpointService.ts index 3d1d1fe..e461dc3 100644 --- a/src/app/shared/services/EndpointService.ts +++ b/src/app/shared/services/EndpointService.ts @@ -33,6 +33,11 @@ export class EndpointService { urlSearchParams.append('sort', sortString); } }); + if (queryParams?.custom !== undefined) { + Object.entries(queryParams.custom).forEach(([key, value]) => { + urlSearchParams.append(key, value.toString()); + }); + } let pathString = pathParts?.join('/') || ''; if (pathString?.length) { diff --git a/src/app/shared/services/ImageService.ts b/src/app/shared/services/ImageService.ts index a76572b..4f89cd2 100644 --- a/src/app/shared/services/ImageService.ts +++ b/src/app/shared/services/ImageService.ts @@ -1,6 +1,6 @@ import { inject, Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; -import { firstValueFrom, map, Observable, tap } from 'rxjs'; +import { firstValueFrom, from, map, mergeMap, Observable, tap, toArray } from 'rxjs'; import { EndpointService } from './EndpointService'; import { ImageView } from '../models/ImageView.model'; import { SliceView } from '../models/SliceView.model'; @@ -41,10 +41,53 @@ export class ImageService { }; } - public getOwnedImages(queryParams?: QueryParams): Promise> { - return firstValueFrom( - this.httpClient.get>(this.endpointService.getUrl('images', [], queryParams)), - ); + public getOwnedImageViewsWithBlobUrls( + queryParams?: QueryParams, + ): Observable> { + return this.httpClient + .get>>(this.endpointService.getUrl('images', [], queryParams)) + .pipe( + map((sliceView) => ({ + ...sliceView, + content: sliceView.content.map((withStringDates) => this.hydrateImageView(withStringDates)), + })), + mergeMap((sliceView) => { + return from(sliceView.content).pipe( + mergeMap((imageView) => { + return this.httpClient + .get( + this.endpointService.getUrl('images', [ + imageView.owner.username, + imageView.filename, + ]), + { + responseType: 'blob', + }, + ) + .pipe( + map((blob) => URL.createObjectURL(blob)), + map((blobUrl) => ({ + ...imageView, + blobUrl, + })), + ); + }), + toArray(), + map((content) => { + return { + ...sliceView, + content, + } satisfies SliceView; + }), + ); + }), + ); + } + + public getOwnedImagesCount(): Observable { + return this.httpClient + .get<{ count: number }>(this.endpointService.getUrl('images', [], { custom: { count: true } })) + .pipe(map((res) => res.count)); } public getImageViewWithBlobUrl(imageView: ImageView): Promise { @@ -120,19 +163,8 @@ export class ImageService { ); } - public deleteImage(username: string, filename: string): Promise { - return firstValueFrom( - this.httpClient.delete(this.endpointService.getUrl('images', [username, filename])).pipe( - tap(async () => { - await this.queryClient.refetchQueries({ - queryKey: ['image-views', username, filename], - }); - await this.queryClient.refetchQueries({ - queryKey: ['image-views-with-blob-urls', username, filename], - }); - }), - ), - ); + public deleteImage(username: string, filename: string): Observable { + return this.httpClient.delete(this.endpointService.getUrl('images', [username, filename])); } public updateImage(username: string, filename: string, data: ImageUpdateBody): Observable {