diff --git a/src/app/pages/recipe-upload-page/steps/enter-recipe-data/image-select/edit-image-dialog/edit-image-dialog.html b/src/app/pages/recipe-upload-page/steps/enter-recipe-data/image-select/edit-image-dialog/edit-image-dialog.html index 934e178..97de804 100644 --- a/src/app/pages/recipe-upload-page/steps/enter-recipe-data/image-select/edit-image-dialog/edit-image-dialog.html +++ b/src/app/pages/recipe-upload-page/steps/enter-recipe-data/image-select/edit-image-dialog/edit-image-dialog.html @@ -1,7 +1,7 @@ - @if (imageViewQuery.isLoading()) { + @if (loading()) { - } @else if (imageViewQuery.isError()) { + } @else if (loadError()) {

There was an error.

} @else {
@@ -18,6 +18,9 @@ + @if (submitting()) { + + }
}
diff --git a/src/app/pages/recipe-upload-page/steps/enter-recipe-data/image-select/edit-image-dialog/edit-image-dialog.spec.ts b/src/app/pages/recipe-upload-page/steps/enter-recipe-data/image-select/edit-image-dialog/edit-image-dialog.spec.ts index 3659271..1555aad 100644 --- a/src/app/pages/recipe-upload-page/steps/enter-recipe-data/image-select/edit-image-dialog/edit-image-dialog.spec.ts +++ b/src/app/pages/recipe-upload-page/steps/enter-recipe-data/image-select/edit-image-dialog/edit-image-dialog.spec.ts @@ -1,22 +1,68 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; - +import { By } from '@angular/platform-browser'; import { EditImageDialog } from './edit-image-dialog'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { Mocked } from 'vitest'; +import { ImageService } from '../../../../../../shared/services/ImageService'; +import { ImageView } from '../../../../../../shared/models/ImageView.model'; +import { of } from 'rxjs'; describe('EditImageDialog', () => { let component: EditImageDialog; let fixture: ComponentFixture; + let matDialogRef: Partial>>; + + const username = 'test-user'; + const filename = 'test-file.jpg'; beforeEach(async () => { + matDialogRef = { + close: vi.fn(), + } as Partial>>; + await TestBed.configureTestingModule({ imports: [EditImageDialog], + providers: [ + { + provide: MAT_DIALOG_DATA, + useValue: { username, filename }, + }, + { + provide: MatDialogRef, + useValue: matDialogRef, + }, + { + provide: ImageService, + useValue: { + getImageView2: vi.fn((username, filename) => + of({ + filename, + owner: { + username, + }, + } as ImageView), + ), + updateImage: vi.fn(() => of({} as ImageView)), + } as Partial>, + }, + ], }).compileComponents(); fixture = TestBed.createComponent(EditImageDialog); component = fixture.componentInstance; await fixture.whenStable(); + fixture.detectChanges(); }); it('should create', () => { expect(component).toBeTruthy(); }); + + it('should close dialog after successful submit', async () => { + const formDebug = fixture.debugElement.query(By.css('form')); + expect(formDebug).toBeTruthy(); + const formElement = formDebug.nativeElement as HTMLFormElement; + formElement.dispatchEvent(new Event('submit', { bubbles: true, cancelable: true })); + expect(matDialogRef.close).toHaveBeenCalled(); + }); }); diff --git a/src/app/pages/recipe-upload-page/steps/enter-recipe-data/image-select/edit-image-dialog/edit-image-dialog.ts b/src/app/pages/recipe-upload-page/steps/enter-recipe-data/image-select/edit-image-dialog/edit-image-dialog.ts index 58836b1..94adbea 100644 --- a/src/app/pages/recipe-upload-page/steps/enter-recipe-data/image-select/edit-image-dialog/edit-image-dialog.ts +++ b/src/app/pages/recipe-upload-page/steps/enter-recipe-data/image-select/edit-image-dialog/edit-image-dialog.ts @@ -1,11 +1,10 @@ -import { Component, inject, OnInit } from '@angular/core'; +import { Component, inject, OnInit, signal } from '@angular/core'; import { DialogContainer } from '../../../../../../shared/components/dialog-container/dialog-container'; import { MatFormField, MatInput, MatLabel } from '@angular/material/input'; import { FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms'; import { ImageView } from '../../../../../../shared/models/ImageView.model'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { MatButton } from '@angular/material/button'; -import { injectMutation, injectQuery } from '@tanstack/angular-query-experimental'; import { ImageService } from '../../../../../../shared/services/ImageService'; import { notNullOrUndefined } from '../../../../../../shared/util'; import { Spinner } from '../../../../../../shared/components/spinner/spinner'; @@ -21,11 +20,12 @@ export class EditImageDialog implements OnInit { private readonly dialogRef = inject(MatDialogRef); private readonly imageService = inject(ImageService); - protected readonly imageViewQuery = injectQuery(() => - this.imageService.getImageView(this.usernameFilename[0], this.usernameFilename[1]), - ); + protected readonly loading = signal(false); + protected readonly loadError = signal(null); + protected readonly imageView = signal(null); - private readonly imageViewMutation = injectMutation(() => this.imageService.updateImage2()); + protected readonly submitting = signal(false); + protected readonly submitError = signal(null); protected readonly imageForm = new FormGroup({ filename: new FormControl( @@ -40,18 +40,27 @@ export class EditImageDialog implements OnInit { }); public ngOnInit(): void { - this.imageViewQuery.promise().then((imageView) => { - this.imageForm.patchValue({ - filename: imageView.filename, - alt: imageView.alt, - caption: imageView.caption, - }); + this.loading.set(true); + this.imageService.getImageView2(this.usernameFilename[0], this.usernameFilename[1]).subscribe({ + next: (imageView) => { + this.loading.set(false); // shouldn't need this + this.imageView.set(imageView); + this.imageForm.patchValue({ + filename: imageView.filename, + alt: imageView.alt, + caption: imageView.caption, + }); + }, + error: (e) => { + this.loading.set(false); + this.loadError.set(e); + }, }); } - public async onSubmit(event: SubmitEvent): Promise { + protected async onSubmit(event: SubmitEvent): Promise { event.preventDefault(); - const imageView: ImageView = this.imageViewQuery.data()!; + const imageView = this.imageView()!; const formValue = this.imageForm.value; if (notNullOrUndefined(formValue.alt)) { imageView.alt = formValue.alt; @@ -59,10 +68,17 @@ export class EditImageDialog implements OnInit { if (notNullOrUndefined(formValue.caption)) { imageView.caption = formValue.caption; } - this.imageViewMutation.mutate(imageView, { - onSuccess: () => { + this.submitting.set(true); + this.imageService.updateImage(imageView.owner.username, imageView.filename, imageView).subscribe({ + next: (imageView) => { + this.submitting.set(false); + this.imageView.set(imageView); this.dialogRef.close(); }, + error: (e) => { + this.submitting.set(false); + this.submitError.set(e); + }, }); } } diff --git a/src/app/shared/bodies/ImageUpdateBody.ts b/src/app/shared/bodies/ImageUpdateBody.ts new file mode 100644 index 0000000..4bb1954 --- /dev/null +++ b/src/app/shared/bodies/ImageUpdateBody.ts @@ -0,0 +1,8 @@ +export interface ImageUpdateBody { + alt?: string | null; + caption?: string | null; + isPublic?: boolean | null; + viewersToAdd?: string[] | null; + viewersToRemove?: string[] | null; + clearAllViewers?: boolean | null; +} diff --git a/src/app/shared/services/ImageService.ts b/src/app/shared/services/ImageService.ts index 5a23926..a76572b 100644 --- a/src/app/shared/services/ImageService.ts +++ b/src/app/shared/services/ImageService.ts @@ -1,13 +1,14 @@ import { inject, Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; -import { firstValueFrom, map, tap } from 'rxjs'; +import { firstValueFrom, map, Observable, tap } from 'rxjs'; import { EndpointService } from './EndpointService'; import { ImageView } from '../models/ImageView.model'; import { SliceView } from '../models/SliceView.model'; import { QueryParams } from '../models/Query.model'; -import { mutationOptions, QueryClient, QueryOptions, queryOptions } from '@tanstack/angular-query-experimental'; +import { QueryClient, QueryOptions, queryOptions } from '@tanstack/angular-query-experimental'; import { ImageViewWithBlobUrl } from '../client-models/ImageViewWithBlobUrl'; import { WithStringDates } from '../util'; +import { ImageUpdateBody } from '../bodies/ImageUpdateBody'; @Injectable({ providedIn: 'root', @@ -74,18 +75,10 @@ export class ImageService { }); } - public getImageView(username: string, filename: string) { - return queryOptions({ - queryKey: ['image-views', username, filename], - queryFn: () => - firstValueFrom( - this.httpClient - .get< - WithStringDates - >(this.endpointService.getUrl('images', [username, filename, 'view'])) - .pipe(map((rawImageView) => this.hydrateImageView(rawImageView))), - ), - }); + public getImageView2(username: string, filename: string): Observable { + return this.httpClient + .get>(this.endpointService.getUrl('images', [username, filename, 'view'])) + .pipe(map((withStringDates) => this.hydrateImageView(withStringDates))); } public uploadImage( @@ -142,47 +135,9 @@ export class ImageService { ); } - public updateImage( - username: string, - filename: string, - data: { - alt?: string | null; - caption?: string | null; - isPublic?: boolean | null; - viewersToAdd?: string[] | null; - viewersToRemove?: string[] | null; - clearAllViewers?: boolean | null; - }, - ): Promise { - return firstValueFrom( - this.httpClient.put(this.endpointService.getUrl('images', [username, filename]), data).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 updateImage2() { - return mutationOptions({ - mutationKey: ['image-views'], - mutationFn: (imageView: ImageView) => - firstValueFrom( - this.httpClient.put( - this.endpointService.getUrl('images', [imageView.owner.username, imageView.filename]), - imageView, - ), - ), - onSuccess: async () => { - await this.queryClient.invalidateQueries({ - queryKey: ['image-views'], - }); - }, - }); + public updateImage(username: string, filename: string, data: ImageUpdateBody): Observable { + return this.httpClient + .put>(this.endpointService.getUrl('images', [username, filename]), data) + .pipe(map((withStringDates) => this.hydrateImageView(withStringDates))); } }