fix: category slug-uniqueness check fails closed on API error
isSlugTaken previously caught network/API errors and returned false, letting the save proceed as if the slug were free. Now the error propagates and blocks save via a distinct slugCheckError state, surfaced in the category form with its own hint. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -29,7 +29,7 @@
|
||||
<app-form-field [label]="'adminCategories.title' | translate" [required]="true">
|
||||
<app-input [ngModel]="category.title" (ngModelChange)="updateTitle($event)" />
|
||||
</app-form-field>
|
||||
<app-form-field [label]="'adminCategories.slug' | translate" [required]="true" [hint]="'adminProducts.slugHint' | translate" [error]="slugTaken ? ('adminCategories.slugTaken' | translate) : null">
|
||||
<app-form-field [label]="'adminCategories.slug' | translate" [required]="true" [hint]="'adminProducts.slugHint' | translate" [error]="slugCheckError ? ('adminCategories.slugCheckError' | translate) : (slugTaken ? ('adminCategories.slugTaken' | translate) : null)">
|
||||
<app-input [ngModel]="category.slug" (ngModelChange)="updateField('slug', $event)" />
|
||||
</app-form-field>
|
||||
<label><span>{{ 'adminCategories.parent' | translate }}</span>
|
||||
|
||||
@@ -41,6 +41,7 @@ export class AdminCategoryFormComponent {
|
||||
@Input() breadcrumb: string[] = [];
|
||||
@Input() children: AdminCategory[] = [];
|
||||
@Input() slugTaken = false;
|
||||
@Input() slugCheckError = false;
|
||||
@Input() locales: string[] = ['en'];
|
||||
@Input() mode: 'create' | 'edit' = 'create';
|
||||
@Input() health!: AdminCategoryHealth;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Injectable, computed, inject, signal } from '@angular/core';
|
||||
import { take } from 'rxjs/operators';
|
||||
import { catchError, of, take } from 'rxjs';
|
||||
import { AdminCategory, AdminCategoryEditorMode, AdminCategoryListFilters } from '../models/admin-category.model';
|
||||
import { AdminCategoriesFormFactory } from '../services/admin-categories-form.factory';
|
||||
import { ADMIN_CATEGORIES_GATEWAY } from '../services/admin-categories-gateway.token';
|
||||
@@ -135,6 +135,7 @@ export class AdminCategoriesFacade {
|
||||
readonly editorMode = signal<AdminCategoryEditorMode>('create');
|
||||
readonly dirty = signal(false);
|
||||
readonly slugTaken = signal(false);
|
||||
readonly slugCheckError = signal(false);
|
||||
private savedSnapshot: string | null = null;
|
||||
private draftStorageKey: string | null = null;
|
||||
|
||||
@@ -305,15 +306,26 @@ export class AdminCategoriesFacade {
|
||||
const draft = this.draft();
|
||||
if (!draft || !draft.slug) {
|
||||
this.slugTaken.set(false);
|
||||
this.slugCheckError.set(false);
|
||||
return;
|
||||
}
|
||||
this.gateway.isSlugTaken(draft.slug, this.editorMode() === 'edit' ? draft.id : null).pipe(take(1))
|
||||
.subscribe(taken => this.slugTaken.set(taken));
|
||||
this.gateway.isSlugTaken(draft.slug, this.editorMode() === 'edit' ? draft.id : null).pipe(
|
||||
take(1),
|
||||
catchError(() => {
|
||||
this.slugCheckError.set(true);
|
||||
return of(true);
|
||||
})
|
||||
).subscribe(taken => {
|
||||
this.slugTaken.set(taken);
|
||||
if (!taken) {
|
||||
this.slugCheckError.set(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
saveDraft(publish: boolean): void {
|
||||
const draft = this.draft();
|
||||
if (!draft || this.slugTaken()) return;
|
||||
if (!draft || this.slugTaken() || this.slugCheckError()) return;
|
||||
|
||||
const toSave: AdminCategory = { ...draft, status: publish ? 'published' : 'draft', updatedAt: new Date().toISOString() };
|
||||
const request = this.editorMode() === 'create' ? this.gateway.createCategory(toSave) : this.gateway.updateCategory(toSave);
|
||||
|
||||
@@ -9,7 +9,7 @@ import { LanguageService } from '../../../../services/language.service';
|
||||
selector: 'app-admin-category-editor-page',
|
||||
standalone: true,
|
||||
imports: [AdminCategoryFormComponent, TranslatePipe],
|
||||
template: `@if (facade.draft(); as draft) {<main class="editor-page"><header><h1>{{ title() | translate }}</h1></header><app-admin-category-form [category]="draft" [parentOptions]="parentOptions()" [breadcrumb]="facade.breadcrumbFor(draft.parentId)" [children]="facade.childrenOf(draft.id)" [slugTaken]="facade.slugTaken()" [locales]="facade.supportedLocales()" [mode]="facade.editorMode()" [health]="facade.health(draft)" (categoryChange)="facade.updateDraft($event)" (saveDraft)="save(false)" (publish)="save(true)" /></main>} @else {<main class="editor-page"><p>{{ 'common.loading' | translate }}</p></main>}`,
|
||||
template: `@if (facade.draft(); as draft) {<main class="editor-page"><header><h1>{{ title() | translate }}</h1></header><app-admin-category-form [category]="draft" [parentOptions]="parentOptions()" [breadcrumb]="facade.breadcrumbFor(draft.parentId)" [children]="facade.childrenOf(draft.id)" [slugTaken]="facade.slugTaken()" [slugCheckError]="facade.slugCheckError()" [locales]="facade.supportedLocales()" [mode]="facade.editorMode()" [health]="facade.health(draft)" (categoryChange)="facade.updateDraft($event)" (saveDraft)="save(false)" (publish)="save(true)" /></main>} @else {<main class="editor-page"><p>{{ 'common.loading' | translate }}</p></main>}`,
|
||||
styles: [`.editor-page { max-width: 1120px; margin: 0 auto; padding: 24px; display: grid; gap: 16px; } .editor-page h1, .editor-page p { margin: 0; }`],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
|
||||
@@ -59,8 +59,7 @@ export class AdminCategoriesApiGateway implements AdminCategoriesGateway {
|
||||
params = params.set('excludingId', excludingId);
|
||||
}
|
||||
return this.http.get<{ taken: boolean }>(`${this.baseUrl}/slug-taken`, { params }).pipe(
|
||||
map(response => response.taken),
|
||||
catchError(() => of(false))
|
||||
map(response => response.taken)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user