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 { RecipeUploadPage } from './pages/recipe-upload-page/recipe-upload-page';
|
||||
import { authGuard } from './shared/guards/auth-guard';
|
||||
import { RecipeEditPage } from './pages/recipe-edit-page/recipe-edit-page';
|
||||
|
||||
export const routes: Routes = [
|
||||
{
|
||||
@ -23,4 +24,8 @@ export const routes: Routes = [
|
||||
path: 'recipes/:username/:slug',
|
||||
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>
|
||||
</button>
|
||||
<mat-menu #recipeActionsMenu="matMenu">
|
||||
<button mat-menu-item (click)="onRecipeEdit()">Edit recipe</button>
|
||||
<button mat-menu-item (click)="onRecipeDelete()">Delete recipe</button>
|
||||
</mat-menu>
|
||||
}
|
||||
|
||||
@ -59,6 +59,11 @@ export class RecipePageContent {
|
||||
private readonly dialog = inject(MatDialog);
|
||||
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 {
|
||||
const dialogRef = this.dialog.open<ConfirmationDialog, ConfirmationDialogData, boolean>(ConfirmationDialog, {
|
||||
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)"
|
||||
></textarea>
|
||||
</mat-form-field>
|
||||
<button matButton="filled" type="submit" [disabled]="recipeFormGroup.invalid">Review</button>
|
||||
<button matButton="filled" type="submit" [disabled]="recipeFormGroup.invalid">Submit</button>
|
||||
</form>
|
||||
|
||||
@ -73,6 +73,7 @@ import { FullRecipeView } from '../../models/Recipe.model';
|
||||
})
|
||||
export class RecipeEditForm implements OnInit {
|
||||
public readonly recipe = input.required<RecipeDraftViewModel | FullRecipeView>();
|
||||
public readonly editSlugDisabled = input<boolean>(false);
|
||||
public readonly submitRecipe = output<RecipeEditFormSubmitEvent>();
|
||||
public readonly deleteDraft = output<void>();
|
||||
|
||||
@ -105,6 +106,9 @@ export class RecipeEditForm implements OnInit {
|
||||
private readonly injector = inject(Injector);
|
||||
|
||||
public ngOnInit(): void {
|
||||
if (this.editSlugDisabled()) {
|
||||
this.recipeFormGroup.controls.slug.disable();
|
||||
}
|
||||
const draft = this.recipe();
|
||||
this.recipeFormGroup.patchValue({
|
||||
title: draft.title ?? '',
|
||||
@ -122,6 +126,9 @@ export class RecipeEditForm implements OnInit {
|
||||
})),
|
||||
);
|
||||
}
|
||||
if (draft.mainImage) {
|
||||
this.mainImage.set(draft.mainImage);
|
||||
}
|
||||
runInInjectionContext(this.injector, () => {
|
||||
afterNextRender({
|
||||
mixedReadWrite: () => {
|
||||
|
||||
@ -10,6 +10,7 @@ import { EndpointService } from './EndpointService';
|
||||
import { SliceView } from '../models/SliceView.model';
|
||||
import { WithStringDates } from '../util';
|
||||
import { ImageService } from './ImageService';
|
||||
import { RecipeUpdateBody } from '../bodies/RecipeUpdateBody';
|
||||
|
||||
@Injectable({
|
||||
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>> {
|
||||
return this.http.get<SliceView<WithStringDates<RecipeInfoView>>>(this.endpointService.getUrl('recipes')).pipe(
|
||||
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 {
|
||||
return this.endpointService.getUrl('recipes', [recipeView.recipe.owner.username, recipeView.recipe.slug]);
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user