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:
sdarbinyan
2026-08-13 07:15:50 +04:00
parent e6d64abd56
commit c3b5820ac9
9 changed files with 24 additions and 8 deletions

View File

@@ -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>

View File

@@ -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;

View File

@@ -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);

View File

@@ -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
})

View File

@@ -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)
);
}
}