Compare commits
No commits in common. "76ed65a1440304738ee5f1b873821ef950c0d49c" and "b07a8fd90b7a0f9732453e87e9ba2e75aad0a039" have entirely different histories.
76ed65a144
...
b07a8fd90b
@ -1,5 +1,3 @@
|
|||||||
import { ImageView } from '../../../../shared/models/ImageView.model';
|
|
||||||
|
|
||||||
export interface EnterRecipeDataSubmitEvent {
|
export interface EnterRecipeDataSubmitEvent {
|
||||||
title: string;
|
title: string;
|
||||||
slug: string;
|
slug: string;
|
||||||
@ -8,6 +6,5 @@ export interface EnterRecipeDataSubmitEvent {
|
|||||||
name: string;
|
name: string;
|
||||||
notes?: string | null;
|
notes?: string | null;
|
||||||
}>;
|
}>;
|
||||||
mainImage: ImageView | null;
|
|
||||||
rawText: string;
|
rawText: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -88,10 +88,7 @@
|
|||||||
<button matButton="outlined" (click)="openImageUploadDialog()" type="button">Upload Image</button>
|
<button matButton="outlined" (click)="openImageUploadDialog()" type="button">Upload Image</button>
|
||||||
|
|
||||||
<h4>Select Main Image</h4>
|
<h4>Select Main Image</h4>
|
||||||
<app-image-select
|
<app-image-select (select)="onMainImageSelect($event)" [selectedUsernameFilename]="mainImageUsernameFilename()"></app-image-select>
|
||||||
(select)="onMainImageSelect($event)"
|
|
||||||
[selectedUsernameFilename]="mainImageUsernameFilename()"
|
|
||||||
></app-image-select>
|
|
||||||
|
|
||||||
<h3>Recipe Text</h3>
|
<h3>Recipe Text</h3>
|
||||||
<mat-form-field>
|
<mat-form-field>
|
||||||
|
|||||||
@ -211,7 +211,6 @@ export class EnterRecipeData implements OnInit {
|
|||||||
title: value.title!,
|
title: value.title!,
|
||||||
slug: value.slug!,
|
slug: value.slug!,
|
||||||
ingredients: this.ingredientModels().map((ingredientModel) => ingredientModel.draft),
|
ingredients: this.ingredientModels().map((ingredientModel) => ingredientModel.draft),
|
||||||
mainImage: this.mainImage(),
|
|
||||||
rawText: value.text!,
|
rawText: value.text!,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +0,0 @@
|
|||||||
form {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
row-gap: 10px;
|
|
||||||
}
|
|
||||||
@ -1,17 +0,0 @@
|
|||||||
<app-dialog-container title="Edit Image">
|
|
||||||
<form (submit)="onSubmit($event)">
|
|
||||||
<mat-form-field>
|
|
||||||
<mat-label>Filename</mat-label>
|
|
||||||
<input matInput [formControl]="imageForm.controls.filename" />
|
|
||||||
</mat-form-field>
|
|
||||||
<mat-form-field>
|
|
||||||
<mat-label>Alt</mat-label>
|
|
||||||
<input matInput [formControl]="imageForm.controls.alt" />
|
|
||||||
</mat-form-field>
|
|
||||||
<mat-form-field>
|
|
||||||
<mat-label>Caption</mat-label>
|
|
||||||
<input matInput [formControl]="imageForm.controls.caption" />
|
|
||||||
</mat-form-field>
|
|
||||||
<button type="submit" matButton="filled">Submit</button>
|
|
||||||
</form>
|
|
||||||
</app-dialog-container>
|
|
||||||
@ -1,22 +0,0 @@
|
|||||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
|
||||||
|
|
||||||
import { EditImageDialog } from './edit-image-dialog';
|
|
||||||
|
|
||||||
describe('EditImageDialog', () => {
|
|
||||||
let component: EditImageDialog;
|
|
||||||
let fixture: ComponentFixture<EditImageDialog>;
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
await TestBed.configureTestingModule({
|
|
||||||
imports: [EditImageDialog],
|
|
||||||
}).compileComponents();
|
|
||||||
|
|
||||||
fixture = TestBed.createComponent(EditImageDialog);
|
|
||||||
component = fixture.componentInstance;
|
|
||||||
await fixture.whenStable();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should create', () => {
|
|
||||||
expect(component).toBeTruthy();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@ -1,54 +0,0 @@
|
|||||||
import { Component, inject, input, OnInit } 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 { ImageService } from '../../../../../../shared/services/ImageService';
|
|
||||||
import { QueryClient } from '@tanstack/angular-query-experimental';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'app-edit-image-dialog',
|
|
||||||
imports: [DialogContainer, MatFormField, MatLabel, MatInput, ReactiveFormsModule, MatButton],
|
|
||||||
templateUrl: './edit-image-dialog.html',
|
|
||||||
styleUrl: './edit-image-dialog.css',
|
|
||||||
})
|
|
||||||
export class EditImageDialog implements OnInit {
|
|
||||||
protected readonly imageView: ImageView = inject(MAT_DIALOG_DATA);
|
|
||||||
private readonly imageService = inject(ImageService);
|
|
||||||
private readonly queryClient = inject(QueryClient);
|
|
||||||
private readonly dialogRef = inject(MatDialogRef);
|
|
||||||
|
|
||||||
protected readonly imageForm = new FormGroup({
|
|
||||||
filename: new FormControl(
|
|
||||||
{
|
|
||||||
value: '',
|
|
||||||
disabled: true,
|
|
||||||
},
|
|
||||||
Validators.required,
|
|
||||||
),
|
|
||||||
alt: new FormControl(''),
|
|
||||||
caption: new FormControl(''),
|
|
||||||
});
|
|
||||||
|
|
||||||
public ngOnInit(): void {
|
|
||||||
this.imageForm.patchValue({
|
|
||||||
filename: this.imageView.filename,
|
|
||||||
alt: this.imageView.alt,
|
|
||||||
caption: this.imageView.alt,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public async onSubmit(event: SubmitEvent): Promise<void> {
|
|
||||||
event.preventDefault();
|
|
||||||
await this.imageService.updateImage(this.imageView.owner.username, this.imageView.filename, {
|
|
||||||
alt: this.imageForm.value.alt,
|
|
||||||
caption: this.imageForm.value.caption,
|
|
||||||
});
|
|
||||||
await this.queryClient.invalidateQueries({
|
|
||||||
queryKey: ['image-views'],
|
|
||||||
});
|
|
||||||
this.dialogRef.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -4,17 +4,14 @@
|
|||||||
gap: 5px;
|
gap: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.image-grid-image {
|
.image-grid > img {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
}
|
}
|
||||||
|
|
||||||
mat-card-content p {
|
.selected-image {
|
||||||
overflow-wrap: anywhere;
|
border: 5px solid var(--primary-black);
|
||||||
}
|
border-radius: 10px;
|
||||||
|
box-sizing: border-box;
|
||||||
mat-card-actions {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,41 +11,14 @@
|
|||||||
<p>There was an error loading this image.</p>
|
<p>There was an error loading this image.</p>
|
||||||
} @else {
|
} @else {
|
||||||
@let imageData = imageQuery.data();
|
@let imageData = imageQuery.data();
|
||||||
<mat-card>
|
<img
|
||||||
<img
|
[src]="imageData!.blobUrl"
|
||||||
mat-card-image
|
alt="imageData!.imageView.alt"
|
||||||
[src]="imageData!.blobUrl"
|
(click)="onImageClick(imageData!.imageView)"
|
||||||
alt="imageData!.imageView.alt"
|
[ngClass]="{ 'selected-image': isSelected(imageData!.imageView) }"
|
||||||
(click)="onImageClick(imageData!.imageView)"
|
>
|
||||||
class="image-grid-image"
|
|
||||||
/>
|
|
||||||
<mat-card-content>
|
|
||||||
<p>{{ imageData!.imageView.filename }}</p>
|
|
||||||
</mat-card-content>
|
|
||||||
<mat-card-actions>
|
|
||||||
<mat-checkbox
|
|
||||||
[checked]="isSelected(imageData!.imageView)"
|
|
||||||
(click)="onImageClick(imageData!.imageView)"
|
|
||||||
>Main?</mat-checkbox
|
|
||||||
>
|
|
||||||
<button matButton="text" type="button" [matMenuTriggerFor]="imageActionsMenu">
|
|
||||||
<fa-icon [icon]="faEllipsis"></fa-icon>
|
|
||||||
</button>
|
|
||||||
<mat-menu #imageActionsMenu>
|
|
||||||
<button mat-menu-item type="button" (click)="editImage(imageData!.imageView)">Edit</button>
|
|
||||||
<button mat-menu-item type="button" (click)="deleteImage(imageData!.imageView)">
|
|
||||||
Delete
|
|
||||||
</button>
|
|
||||||
</mat-menu>
|
|
||||||
</mat-card-actions>
|
|
||||||
</mat-card>
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
<mat-paginator
|
<mat-paginator [length]="imageCount()" [pageSize]="pageSize()" [pageSizeOptions]="[3, 6, 9, 12]" (page)="onPage($event)"></mat-paginator>
|
||||||
[length]="imageCount()"
|
|
||||||
[pageSize]="pageSize()"
|
|
||||||
[pageSizeOptions]="[3, 6, 9, 12]"
|
|
||||||
(page)="onPage($event)"
|
|
||||||
></mat-paginator>
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,20 +3,21 @@ import { ComponentFixture, TestBed } from '@angular/core/testing';
|
|||||||
import { ImageSelect } from './image-select';
|
import { ImageSelect } from './image-select';
|
||||||
|
|
||||||
describe('ImageSelect', () => {
|
describe('ImageSelect', () => {
|
||||||
let component: ImageSelect;
|
let component: ImageSelect;
|
||||||
let fixture: ComponentFixture<ImageSelect>;
|
let fixture: ComponentFixture<ImageSelect>;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
imports: [ImageSelect],
|
imports: [ImageSelect]
|
||||||
}).compileComponents();
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
fixture = TestBed.createComponent(ImageSelect);
|
fixture = TestBed.createComponent(ImageSelect);
|
||||||
component = fixture.componentInstance;
|
component = fixture.componentInstance;
|
||||||
await fixture.whenStable();
|
await fixture.whenStable();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should create', () => {
|
it('should create', () => {
|
||||||
expect(component).toBeTruthy();
|
expect(component).toBeTruthy();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,35 +1,15 @@
|
|||||||
import { Component, computed, inject, input, output, signal } from '@angular/core';
|
import { Component, computed, inject, input, output, signal } from '@angular/core';
|
||||||
import { MatPaginator, PageEvent } from '@angular/material/paginator';
|
import { MatPaginator, PageEvent } from '@angular/material/paginator';
|
||||||
import { ImageService } from '../../../../../shared/services/ImageService';
|
import { ImageService } from '../../../../../shared/services/ImageService';
|
||||||
import { injectQuery, keepPreviousData, QueryClient } from '@tanstack/angular-query-experimental';
|
import { injectQuery, keepPreviousData } from '@tanstack/angular-query-experimental';
|
||||||
import { Spinner } from '../../../../../shared/components/spinner/spinner';
|
import { Spinner } from '../../../../../shared/components/spinner/spinner';
|
||||||
import { ImageView } from '../../../../../shared/models/ImageView.model';
|
import { ImageView } from '../../../../../shared/models/ImageView.model';
|
||||||
import { injectQueries } from '@tanstack/angular-query-experimental/inject-queries-experimental';
|
import { injectQueries } from '@tanstack/angular-query-experimental/inject-queries-experimental'
|
||||||
import { MatCard, MatCardActions, MatCardContent, MatCardImage } from '@angular/material/card';
|
import { NgClass } from '@angular/common';
|
||||||
import { MatCheckbox } from '@angular/material/checkbox';
|
|
||||||
import { MatMenu, MatMenuItem, MatMenuTrigger } from '@angular/material/menu';
|
|
||||||
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
|
|
||||||
import { faEllipsis } from '@fortawesome/free-solid-svg-icons';
|
|
||||||
import { MatButton } from '@angular/material/button';
|
|
||||||
import { MatDialog } from '@angular/material/dialog';
|
|
||||||
import { EditImageDialog } from './edit-image-dialog/edit-image-dialog';
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-image-select',
|
selector: 'app-image-select',
|
||||||
imports: [
|
imports: [MatPaginator, Spinner, NgClass],
|
||||||
MatPaginator,
|
|
||||||
Spinner,
|
|
||||||
MatCard,
|
|
||||||
MatCardImage,
|
|
||||||
MatCardContent,
|
|
||||||
MatCardActions,
|
|
||||||
MatCheckbox,
|
|
||||||
MatMenuTrigger,
|
|
||||||
MatMenu,
|
|
||||||
FaIconComponent,
|
|
||||||
MatButton,
|
|
||||||
MatMenuItem,
|
|
||||||
],
|
|
||||||
templateUrl: './image-select.html',
|
templateUrl: './image-select.html',
|
||||||
styleUrl: './image-select.css',
|
styleUrl: './image-select.css',
|
||||||
})
|
})
|
||||||
@ -41,8 +21,6 @@ export class ImageSelect {
|
|||||||
protected readonly pageSize = signal(9);
|
protected readonly pageSize = signal(9);
|
||||||
|
|
||||||
private readonly imageService = inject(ImageService);
|
private readonly imageService = inject(ImageService);
|
||||||
private readonly dialog = inject(MatDialog);
|
|
||||||
private readonly queryClient = inject(QueryClient);
|
|
||||||
|
|
||||||
protected readonly imageViewsQuery = injectQuery(() => ({
|
protected readonly imageViewsQuery = injectQuery(() => ({
|
||||||
queryKey: ['image-views', this.currentPage(), this.pageSize()],
|
queryKey: ['image-views', this.currentPage(), this.pageSize()],
|
||||||
@ -111,22 +89,4 @@ export class ImageSelect {
|
|||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected editImage(imageView: ImageView): void {
|
|
||||||
this.dialog.open(EditImageDialog, {
|
|
||||||
data: imageView,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
protected async deleteImage(imageView: ImageView): Promise<void> {
|
|
||||||
await this.imageService.deleteImage(imageView.owner.username, imageView.filename);
|
|
||||||
await this.queryClient.invalidateQueries({
|
|
||||||
queryKey: ['image-views'],
|
|
||||||
});
|
|
||||||
await this.queryClient.invalidateQueries({
|
|
||||||
queryKey: ['images'],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
protected readonly faEllipsis = faEllipsis;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +0,0 @@
|
|||||||
export interface SetImageBody {
|
|
||||||
username: string;
|
|
||||||
userFilename: string;
|
|
||||||
}
|
|
||||||
@ -7,11 +7,7 @@ import { environment } from '../../../environments/environment';
|
|||||||
providedIn: 'root',
|
providedIn: 'root',
|
||||||
})
|
})
|
||||||
export class EndpointService {
|
export class EndpointService {
|
||||||
public getUrl<P extends readonly string[] = []>(
|
public getUrl<P extends readonly string[] = []>(endpoint: keyof typeof Endpoints, pathParts?: string[], queryParams?: QueryParams<P>): string {
|
||||||
endpoint: keyof typeof Endpoints,
|
|
||||||
pathParts?: string[],
|
|
||||||
queryParams?: QueryParams<P>,
|
|
||||||
): string {
|
|
||||||
const urlSearchParams = new URLSearchParams();
|
const urlSearchParams = new URLSearchParams();
|
||||||
if (queryParams?.page !== undefined) {
|
if (queryParams?.page !== undefined) {
|
||||||
urlSearchParams.set('page', queryParams.page.toString());
|
urlSearchParams.set('page', queryParams.page.toString());
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import { firstValueFrom, map } from 'rxjs';
|
|||||||
import { EndpointService } from './EndpointService';
|
import { EndpointService } from './EndpointService';
|
||||||
import { ImageView } from '../models/ImageView.model';
|
import { ImageView } from '../models/ImageView.model';
|
||||||
import { SliceView } from '../models/SliceView.model';
|
import { SliceView } from '../models/SliceView.model';
|
||||||
import { QueryParams } from '../models/Query.model';
|
import { QueryParams } from '../models/Query.model';;
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root',
|
providedIn: 'root',
|
||||||
@ -22,7 +22,7 @@ export class ImageService {
|
|||||||
'height',
|
'height',
|
||||||
'width',
|
'width',
|
||||||
'owner',
|
'owner',
|
||||||
'viewers',
|
'viewers'
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
private readonly httpClient = inject(HttpClient);
|
private readonly httpClient = inject(HttpClient);
|
||||||
@ -30,7 +30,7 @@ export class ImageService {
|
|||||||
|
|
||||||
public getOwnedImages(queryParams?: QueryParams<typeof ImageService.ImageProps>): Promise<SliceView<ImageView>> {
|
public getOwnedImages(queryParams?: QueryParams<typeof ImageService.ImageProps>): Promise<SliceView<ImageView>> {
|
||||||
return firstValueFrom(
|
return firstValueFrom(
|
||||||
this.httpClient.get<SliceView<ImageView>>(this.endpointService.getUrl('images', [], queryParams)),
|
this.httpClient.get<SliceView<ImageView>>(this.endpointService.getUrl('images', [], queryParams))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -81,27 +81,4 @@ export class ImageService {
|
|||||||
.pipe(map((view) => view.exists)),
|
.pipe(map((view) => view.exists)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public deleteImage(username: string, filename: string): Promise<void> {
|
|
||||||
return firstValueFrom(
|
|
||||||
this.httpClient.delete<void>(this.endpointService.getUrl('images', [username, filename])),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
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<ImageView> {
|
|
||||||
return firstValueFrom(
|
|
||||||
this.httpClient.put<ImageView>(this.endpointService.getUrl('images', [username, filename]), data),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,8 +7,6 @@ import { RecipeDraftViewModel } from '../models/RecipeDraftView.model';
|
|||||||
import { EndpointService } from './EndpointService';
|
import { EndpointService } from './EndpointService';
|
||||||
import { WithStringDates } from '../util';
|
import { WithStringDates } from '../util';
|
||||||
import { Recipe } from '../models/Recipe.model';
|
import { Recipe } from '../models/Recipe.model';
|
||||||
import { ImageView } from '../models/ImageView.model';
|
|
||||||
import { SetImageBody } from '../models/SetImageBody';
|
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root',
|
providedIn: 'root',
|
||||||
@ -84,21 +82,12 @@ export class RecipeDraftService {
|
|||||||
name: string;
|
name: string;
|
||||||
notes?: string | null;
|
notes?: string | null;
|
||||||
}>;
|
}>;
|
||||||
mainImage?: ImageView | null;
|
|
||||||
rawText?: string | null;
|
rawText?: string | null;
|
||||||
},
|
},
|
||||||
): Promise<RecipeUploadClientModel> {
|
): Promise<RecipeUploadClientModel> {
|
||||||
return firstValueFrom(
|
return firstValueFrom(
|
||||||
this.http
|
this.http
|
||||||
.put<WithStringDates<RecipeDraftViewModel>>(this.endpointService.getUrl('recipeDrafts', [id]), {
|
.put<WithStringDates<RecipeDraftViewModel>>(this.endpointService.getUrl('recipeDrafts', [id]), data)
|
||||||
...data,
|
|
||||||
mainImage: data.mainImage
|
|
||||||
? ({
|
|
||||||
username: data.mainImage.owner.username,
|
|
||||||
userFilename: data.mainImage.filename,
|
|
||||||
} satisfies SetImageBody)
|
|
||||||
: undefined,
|
|
||||||
})
|
|
||||||
.pipe(
|
.pipe(
|
||||||
map((rawView) => this.hydrateView(rawView)),
|
map((rawView) => this.hydrateView(rawView)),
|
||||||
map((draft) => ({
|
map((draft) => ({
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user