MME-14 Basic recipe edit page.
This commit is contained in:
parent
73bb3a7ab8
commit
e071b0ed8c
@ -4,6 +4,7 @@ import { RecipesPage } from './pages/recipes-page/recipes-page';
|
|||||||
import { RecipesSearchPage } from './pages/recipes-search-page/recipes-search-page';
|
import { RecipesSearchPage } from './pages/recipes-search-page/recipes-search-page';
|
||||||
import { RecipeUploadPage } from './pages/recipe-upload-page/recipe-upload-page';
|
import { RecipeUploadPage } from './pages/recipe-upload-page/recipe-upload-page';
|
||||||
import { authGuard } from './shared/guards/auth-guard';
|
import { authGuard } from './shared/guards/auth-guard';
|
||||||
|
import { RecipeEditPage } from './pages/recipe-edit-page/recipe-edit-page';
|
||||||
|
|
||||||
export const routes: Routes = [
|
export const routes: Routes = [
|
||||||
{
|
{
|
||||||
@ -23,4 +24,8 @@ export const routes: Routes = [
|
|||||||
path: 'recipes/:username/:slug',
|
path: 'recipes/:username/:slug',
|
||||||
component: RecipePage,
|
component: RecipePage,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'recipes/:username/:slug/edit',
|
||||||
|
component: RecipeEditPage,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
0
src/app/pages/recipe-edit-page/recipe-edit-page.css
Normal file
0
src/app/pages/recipe-edit-page/recipe-edit-page.css
Normal file
22
src/app/pages/recipe-edit-page/recipe-edit-page.html
Normal file
22
src/app/pages/recipe-edit-page/recipe-edit-page.html
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
<h1>Edit Recipe</h1>
|
||||||
|
@if (loadingRecipe()) {
|
||||||
|
<app-spinner></app-spinner>
|
||||||
|
} @else if (loadRecipeError()) {
|
||||||
|
<p>There was an error loading the recipe: {{ loadRecipeError() }}</p>
|
||||||
|
} @else {
|
||||||
|
@let recipe = recipeView()!.recipe;
|
||||||
|
<div class="recipe-info-container">
|
||||||
|
<p>Created: {{ recipe.created | date: "short" }}</p>
|
||||||
|
@if (recipe.modified) {
|
||||||
|
<p>Last modified: {{ recipe.modified | date: "short" }}</p>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<app-recipe-edit-form
|
||||||
|
[recipe]="recipe"
|
||||||
|
[editSlugDisabled]="true"
|
||||||
|
(submitRecipe)="onRecipeSubmit($event)"
|
||||||
|
></app-recipe-edit-form>
|
||||||
|
@if (submittingRecipe()) {
|
||||||
|
<app-spinner></app-spinner>
|
||||||
|
}
|
||||||
|
}
|
||||||
22
src/app/pages/recipe-edit-page/recipe-edit-page.spec.ts
Normal file
22
src/app/pages/recipe-edit-page/recipe-edit-page.spec.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { RecipeEditPage } from './recipe-edit-page';
|
||||||
|
|
||||||
|
describe('RecipeEditPage', () => {
|
||||||
|
let component: RecipeEditPage;
|
||||||
|
let fixture: ComponentFixture<RecipeEditPage>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [RecipeEditPage],
|
||||||
|
}).compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(RecipeEditPage);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
await fixture.whenStable();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
73
src/app/pages/recipe-edit-page/recipe-edit-page.ts
Normal file
73
src/app/pages/recipe-edit-page/recipe-edit-page.ts
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
import { Component, inject, OnInit, signal } from '@angular/core';
|
||||||
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
|
import { FullRecipeViewWrapper } from '../../shared/models/Recipe.model';
|
||||||
|
import { RecipeService } from '../../shared/services/RecipeService';
|
||||||
|
import { Spinner } from '../../shared/components/spinner/spinner';
|
||||||
|
import { DatePipe } from '@angular/common';
|
||||||
|
import { RecipeEditForm } from '../../shared/components/recipe-edit-form/recipe-edit-form';
|
||||||
|
import { RecipeEditFormSubmitEvent } from '../../shared/components/recipe-edit-form/RecipeEditFormSubmitEvent';
|
||||||
|
import { ToastrService } from 'ngx-toastr';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-recipe-edit-page',
|
||||||
|
imports: [Spinner, DatePipe, RecipeEditForm],
|
||||||
|
templateUrl: './recipe-edit-page.html',
|
||||||
|
styleUrl: './recipe-edit-page.css',
|
||||||
|
})
|
||||||
|
export class RecipeEditPage implements OnInit {
|
||||||
|
private readonly activatedRoute = inject(ActivatedRoute);
|
||||||
|
private readonly recipeService = inject(RecipeService);
|
||||||
|
private readonly router = inject(Router);
|
||||||
|
private readonly toastrService = inject(ToastrService);
|
||||||
|
|
||||||
|
protected readonly loadingRecipe = signal(false);
|
||||||
|
protected readonly loadRecipeError = signal<Error | null>(null);
|
||||||
|
protected readonly recipeView = signal<FullRecipeViewWrapper | null>(null);
|
||||||
|
|
||||||
|
protected readonly submittingRecipe = signal(false);
|
||||||
|
|
||||||
|
public ngOnInit(): void {
|
||||||
|
this.activatedRoute.paramMap.subscribe((paramMap) => {
|
||||||
|
const username = paramMap.get('username')!;
|
||||||
|
const slug = paramMap.get('slug')!;
|
||||||
|
this.loadingRecipe.set(true);
|
||||||
|
this.recipeService.getRecipeView2(username, slug, true).subscribe({
|
||||||
|
next: (recipeView) => {
|
||||||
|
this.loadingRecipe.set(false);
|
||||||
|
this.recipeView.set(recipeView);
|
||||||
|
},
|
||||||
|
error: (e) => {
|
||||||
|
this.loadingRecipe.set(false);
|
||||||
|
this.loadRecipeError.set(e);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public onRecipeSubmit(event: RecipeEditFormSubmitEvent): void {
|
||||||
|
this.submittingRecipe.set(true);
|
||||||
|
const baseRecipe = this.recipeView()!.recipe;
|
||||||
|
this.recipeService
|
||||||
|
.updateRecipe(baseRecipe.owner.username, baseRecipe.slug, {
|
||||||
|
...event,
|
||||||
|
mainImage: event.mainImage
|
||||||
|
? {
|
||||||
|
username: event.mainImage.owner.username,
|
||||||
|
filename: event.mainImage.filename,
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
})
|
||||||
|
.subscribe({
|
||||||
|
next: async () => {
|
||||||
|
this.submittingRecipe.set(false);
|
||||||
|
await this.router.navigate(['recipes', baseRecipe.owner.username, baseRecipe.slug]);
|
||||||
|
this.toastrService.success('Recipe updated');
|
||||||
|
},
|
||||||
|
error: (e) => {
|
||||||
|
this.submittingRecipe.set(false);
|
||||||
|
console.error(e);
|
||||||
|
this.toastrService.error('Error submitting recipe');
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -23,6 +23,7 @@
|
|||||||
<fa-icon [icon]="faEllipsis" size="3x"></fa-icon>
|
<fa-icon [icon]="faEllipsis" size="3x"></fa-icon>
|
||||||
</button>
|
</button>
|
||||||
<mat-menu #recipeActionsMenu="matMenu">
|
<mat-menu #recipeActionsMenu="matMenu">
|
||||||
|
<button mat-menu-item (click)="onRecipeEdit()">Edit recipe</button>
|
||||||
<button mat-menu-item (click)="onRecipeDelete()">Delete recipe</button>
|
<button mat-menu-item (click)="onRecipeDelete()">Delete recipe</button>
|
||||||
</mat-menu>
|
</mat-menu>
|
||||||
}
|
}
|
||||||
|
|||||||
@ -59,6 +59,11 @@ export class RecipePageContent {
|
|||||||
private readonly dialog = inject(MatDialog);
|
private readonly dialog = inject(MatDialog);
|
||||||
private readonly toastrService = inject(ToastrService);
|
private readonly toastrService = inject(ToastrService);
|
||||||
|
|
||||||
|
protected async onRecipeEdit(): Promise<void> {
|
||||||
|
const recipe = this.recipeView().recipe;
|
||||||
|
await this.router.navigate(['recipes', recipe.owner.username, recipe.slug, 'edit']);
|
||||||
|
}
|
||||||
|
|
||||||
protected onRecipeDelete(): void {
|
protected onRecipeDelete(): void {
|
||||||
const dialogRef = this.dialog.open<ConfirmationDialog, ConfirmationDialogData, boolean>(ConfirmationDialog, {
|
const dialogRef = this.dialog.open<ConfirmationDialog, ConfirmationDialogData, boolean>(ConfirmationDialog, {
|
||||||
data: {
|
data: {
|
||||||
|
|||||||
21
src/app/shared/bodies/RecipeUpdateBody.ts
Normal file
21
src/app/shared/bodies/RecipeUpdateBody.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
export interface RecipeUpdateBody {
|
||||||
|
title?: string | null;
|
||||||
|
preparationTime?: number | null;
|
||||||
|
cookingTime?: number | null;
|
||||||
|
totalTime?: number | null;
|
||||||
|
ingredients?: IngredientUpdateBody[] | null;
|
||||||
|
rawText?: string | null;
|
||||||
|
isPublic?: boolean | null;
|
||||||
|
mainImage?: MainImageUpdateBody | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IngredientUpdateBody {
|
||||||
|
amount?: string | null;
|
||||||
|
name: string;
|
||||||
|
notes?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MainImageUpdateBody {
|
||||||
|
username: string;
|
||||||
|
filename: string;
|
||||||
|
}
|
||||||
@ -123,5 +123,5 @@
|
|||||||
(input)="onRecipeTextChange($event)"
|
(input)="onRecipeTextChange($event)"
|
||||||
></textarea>
|
></textarea>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
<button matButton="filled" type="submit" [disabled]="recipeFormGroup.invalid">Review</button>
|
<button matButton="filled" type="submit" [disabled]="recipeFormGroup.invalid">Submit</button>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@ -73,6 +73,7 @@ import { FullRecipeView } from '../../models/Recipe.model';
|
|||||||
})
|
})
|
||||||
export class RecipeEditForm implements OnInit {
|
export class RecipeEditForm implements OnInit {
|
||||||
public readonly recipe = input.required<RecipeDraftViewModel | FullRecipeView>();
|
public readonly recipe = input.required<RecipeDraftViewModel | FullRecipeView>();
|
||||||
|
public readonly editSlugDisabled = input<boolean>(false);
|
||||||
public readonly submitRecipe = output<RecipeEditFormSubmitEvent>();
|
public readonly submitRecipe = output<RecipeEditFormSubmitEvent>();
|
||||||
public readonly deleteDraft = output<void>();
|
public readonly deleteDraft = output<void>();
|
||||||
|
|
||||||
@ -105,6 +106,9 @@ export class RecipeEditForm implements OnInit {
|
|||||||
private readonly injector = inject(Injector);
|
private readonly injector = inject(Injector);
|
||||||
|
|
||||||
public ngOnInit(): void {
|
public ngOnInit(): void {
|
||||||
|
if (this.editSlugDisabled()) {
|
||||||
|
this.recipeFormGroup.controls.slug.disable();
|
||||||
|
}
|
||||||
const draft = this.recipe();
|
const draft = this.recipe();
|
||||||
this.recipeFormGroup.patchValue({
|
this.recipeFormGroup.patchValue({
|
||||||
title: draft.title ?? '',
|
title: draft.title ?? '',
|
||||||
@ -122,6 +126,9 @@ export class RecipeEditForm implements OnInit {
|
|||||||
})),
|
})),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if (draft.mainImage) {
|
||||||
|
this.mainImage.set(draft.mainImage);
|
||||||
|
}
|
||||||
runInInjectionContext(this.injector, () => {
|
runInInjectionContext(this.injector, () => {
|
||||||
afterNextRender({
|
afterNextRender({
|
||||||
mixedReadWrite: () => {
|
mixedReadWrite: () => {
|
||||||
|
|||||||
@ -10,6 +10,7 @@ import { EndpointService } from './EndpointService';
|
|||||||
import { SliceView } from '../models/SliceView.model';
|
import { SliceView } from '../models/SliceView.model';
|
||||||
import { WithStringDates } from '../util';
|
import { WithStringDates } from '../util';
|
||||||
import { ImageService } from './ImageService';
|
import { ImageService } from './ImageService';
|
||||||
|
import { RecipeUpdateBody } from '../bodies/RecipeUpdateBody';
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root',
|
providedIn: 'root',
|
||||||
@ -32,6 +33,19 @@ export class RecipeService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private hydrateFullRecipeViewWrapper(raw: WithStringDates<FullRecipeViewWrapper>): FullRecipeViewWrapper {
|
||||||
|
return {
|
||||||
|
...raw,
|
||||||
|
recipe: {
|
||||||
|
...raw.recipe,
|
||||||
|
created: new Date(raw.recipe.created),
|
||||||
|
modified: raw.recipe.modified ? new Date(raw.recipe.modified) : undefined,
|
||||||
|
ingredients: raw.recipe.ingredients!, // TODO: investigate why we need this
|
||||||
|
mainImage: raw.recipe.mainImage ? this.imageService.hydrateImageView(raw.recipe.mainImage) : undefined,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
public getRecipes(): Observable<SliceView<RecipeInfoView>> {
|
public getRecipes(): Observable<SliceView<RecipeInfoView>> {
|
||||||
return this.http.get<SliceView<WithStringDates<RecipeInfoView>>>(this.endpointService.getUrl('recipes')).pipe(
|
return this.http.get<SliceView<WithStringDates<RecipeInfoView>>>(this.endpointService.getUrl('recipes')).pipe(
|
||||||
map((sliceView) => ({
|
map((sliceView) => ({
|
||||||
@ -47,6 +61,34 @@ export class RecipeService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getRecipeView2(
|
||||||
|
username: string,
|
||||||
|
slug: string,
|
||||||
|
includeRawText: boolean = false,
|
||||||
|
): Observable<FullRecipeViewWrapper> {
|
||||||
|
return this.http
|
||||||
|
.get<WithStringDates<FullRecipeViewWrapper>>(
|
||||||
|
this.endpointService.getUrl('recipes', [username, slug], {
|
||||||
|
custom: {
|
||||||
|
includeRawText,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.pipe(map((raw) => this.hydrateFullRecipeViewWrapper(raw)));
|
||||||
|
}
|
||||||
|
|
||||||
|
public updateRecipe(
|
||||||
|
username: string,
|
||||||
|
slug: string,
|
||||||
|
recipeUpdateBody: RecipeUpdateBody,
|
||||||
|
): Observable<FullRecipeViewWrapper> {
|
||||||
|
return this.http
|
||||||
|
.put<
|
||||||
|
WithStringDates<FullRecipeViewWrapper>
|
||||||
|
>(this.endpointService.getUrl('recipes', [username, slug]), recipeUpdateBody)
|
||||||
|
.pipe(map((raw) => this.hydrateFullRecipeViewWrapper(raw)));
|
||||||
|
}
|
||||||
|
|
||||||
private getRecipeBaseUrl(recipeView: FullRecipeViewWrapper): string {
|
private getRecipeBaseUrl(recipeView: FullRecipeViewWrapper): string {
|
||||||
return this.endpointService.getUrl('recipes', [recipeView.recipe.owner.username, recipeView.recipe.slug]);
|
return this.endpointService.getUrl('recipes', [recipeView.recipe.owner.username, recipeView.recipe.slug]);
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user