From 000bb781122004b8d3bbb2cb02ec359e33eb4772 Mon Sep 17 00:00:00 2001 From: sdarbinyan Date: Thu, 13 Aug 2026 08:53:12 +0400 Subject: [PATCH] fix: no error feedback on failed save/delete/role-change, editors navigated away before save result was known Products/Categories saveDraft() and deleteOne(), and Users setRole()/setStatus()/invite(), had no error handler at all - a failed mutation was completely silent. Also found and fixed the same premature-navigation bug as the Phase 1 cart fix: both product and category editor pages called router.navigate() immediately after facade.saveDraft(), before the save had resolved - so even after adding error feedback, the user would already be gone from the page before it could show. saveDraft() now takes an onSuccess callback and only the page navigates on actual success; on failure it stays put and shows a themed error dialog. Added a mutationError signal to all three facades and a themed app-dialog error alert on the products/categories editor + list pages and the users page. Co-Authored-By: Claude Sonnet 5 --- .../facade/admin-categories.facade.ts | 14 +++++++++++--- .../admin-categories-list-page.component.ts | 8 ++++++++ .../admin-category-editor-page.component.ts | 19 +++++++++++++++---- .../products/facade/admin-products.facade.ts | 15 ++++++++++++--- .../admin-product-editor-page.component.ts | 19 +++++++++++++++---- .../admin-products-list-page.component.ts | 14 ++++++++++++-- .../admin/users/facade/admin-users.facade.ts | 19 ++++++++++++++++--- .../pages/admin-users-page.component.html | 9 +++++++++ 8 files changed, 98 insertions(+), 19 deletions(-) diff --git a/src/app/features/admin/categories/facade/admin-categories.facade.ts b/src/app/features/admin/categories/facade/admin-categories.facade.ts index ff9347f..0101102 100644 --- a/src/app/features/admin/categories/facade/admin-categories.facade.ts +++ b/src/app/features/admin/categories/facade/admin-categories.facade.ts @@ -137,6 +137,7 @@ export class AdminCategoriesFacade { readonly dirty = signal(false); readonly slugTaken = signal(false); readonly slugCheckError = signal(false); + readonly mutationError = signal(null); private savedSnapshot: string | null = null; private draftStorageKey: string | null = null; @@ -326,10 +327,11 @@ export class AdminCategoriesFacade { }); } - saveDraft(publish: boolean): void { + saveDraft(publish: boolean, onSuccess?: () => void): void { const draft = this.draft(); if (!draft || this.slugTaken() || this.slugCheckError()) return; + this.mutationError.set(null); 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); @@ -342,7 +344,9 @@ export class AdminCategoriesFacade { this.savedSnapshot = JSON.stringify(saved); this.dirty.set(false); this.loadList(); - } + onSuccess?.(); + }, + error: () => this.mutationError.set('common.errorDescription') }); } @@ -357,7 +361,11 @@ export class AdminCategoriesFacade { } deleteOne(id: string): void { - this.gateway.deleteCategory(id).pipe(take(1)).subscribe({ next: () => { this.loadList(); this.loadDashboardStats(); } }); + this.mutationError.set(null); + this.gateway.deleteCategory(id).pipe(take(1)).subscribe({ + next: () => { this.loadList(); this.loadDashboardStats(); }, + error: () => this.mutationError.set('common.errorDescription') + }); } restoreOne(id: string): void { diff --git a/src/app/features/admin/categories/pages/admin-categories-list-page.component.ts b/src/app/features/admin/categories/pages/admin-categories-list-page.component.ts index 7306184..7ac61c3 100644 --- a/src/app/features/admin/categories/pages/admin-categories-list-page.component.ts +++ b/src/app/features/admin/categories/pages/admin-categories-list-page.component.ts @@ -71,6 +71,14 @@ import { TranslatePipe } from '../../../../i18n/translate.pipe'; {{ 'common.confirm' | translate }} + } + @if (facade.mutationError()) { + +

{{ 'common.errorDescription' | translate }}

+
+ {{ 'common.confirm' | translate }} +
+
}`, changeDetection: ChangeDetectionStrategy.OnPush }) diff --git a/src/app/features/admin/categories/pages/admin-category-editor-page.component.ts b/src/app/features/admin/categories/pages/admin-category-editor-page.component.ts index 8ce2499..725ec56 100644 --- a/src/app/features/admin/categories/pages/admin-category-editor-page.component.ts +++ b/src/app/features/admin/categories/pages/admin-category-editor-page.component.ts @@ -4,12 +4,22 @@ import { AdminCategoriesFacade } from '../facade/admin-categories.facade'; import { AdminCategoryFormComponent } from '../components/admin-category-form.component'; import { TranslatePipe } from '../../../../i18n/translate.pipe'; import { LanguageService } from '../../../../services/language.service'; +import { DialogComponent } from '../../../../shared/ui/dialog/dialog.component'; +import { ButtonComponent } from '../../../../shared/ui/button/button.component'; @Component({ selector: 'app-admin-category-editor-page', standalone: true, - imports: [AdminCategoryFormComponent, TranslatePipe], - template: `@if (facade.draft(); as draft) {

{{ title() | translate }}

} @else {

{{ 'common.loading' | translate }}

}`, + imports: [AdminCategoryFormComponent, TranslatePipe, DialogComponent, ButtonComponent], + template: `@if (facade.draft(); as draft) {

{{ title() | translate }}

} @else {

{{ 'common.loading' | translate }}

} + @if (facade.mutationError()) { + +

{{ 'common.errorDescription' | translate }}

+
+ {{ 'common.confirm' | translate }} +
+
+ }`, 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 }) @@ -41,8 +51,9 @@ export class AdminCategoryEditorPageComponent { } save(publish: boolean): void { - this.facade.saveDraft(publish); - void this.router.navigate([this.languageService.currentLanguage(), 'backoffice', 'categories']); + this.facade.saveDraft(publish, () => { + void this.router.navigate([this.languageService.currentLanguage(), 'backoffice', 'categories']); + }); } private descendantIds(id: string): string[] { diff --git a/src/app/features/admin/products/facade/admin-products.facade.ts b/src/app/features/admin/products/facade/admin-products.facade.ts index f7bc50b..abffd3c 100644 --- a/src/app/features/admin/products/facade/admin-products.facade.ts +++ b/src/app/features/admin/products/facade/admin-products.facade.ts @@ -92,6 +92,7 @@ export class AdminProductsFacade { readonly categories = signal([]); readonly loading = signal(false); readonly error = signal(null); + readonly mutationError = signal(null); readonly selectedIds = signal([]); readonly draft = signal(null); readonly dirty = signal(false); @@ -305,18 +306,26 @@ export class AdminProductsFacade { this.dirty.set(true); } - saveDraft(): void { + saveDraft(onSuccess?: () => void): void { const draft = this.draft(); if (!draft) return; + this.mutationError.set(null); const request = this.editorMode() === 'create' ? this.gateway.createProduct(draft) : this.gateway.updateProduct(draft); - request.pipe(take(1)).subscribe({ next: () => { this.dirty.set(false); this.loadList(); } }); + request.pipe(take(1)).subscribe({ + next: () => { this.dirty.set(false); this.loadList(); onSuccess?.(); }, + error: () => this.mutationError.set('common.errorDescription') + }); } deleteOne(id: string): void { - this.gateway.deleteProduct(id).pipe(take(1)).subscribe({ next: () => this.loadList() }); + this.mutationError.set(null); + this.gateway.deleteProduct(id).pipe(take(1)).subscribe({ + next: () => this.loadList(), + error: () => this.mutationError.set('common.errorDescription') + }); } } diff --git a/src/app/features/admin/products/pages/admin-product-editor-page.component.ts b/src/app/features/admin/products/pages/admin-product-editor-page.component.ts index 643a7fe..5b5c3fb 100644 --- a/src/app/features/admin/products/pages/admin-product-editor-page.component.ts +++ b/src/app/features/admin/products/pages/admin-product-editor-page.component.ts @@ -4,12 +4,22 @@ import { AdminProductsFacade } from '../facade/admin-products.facade'; import { AdminProductFormComponent } from '../components/admin-product-form.component'; import { TranslatePipe } from '../../../../i18n/translate.pipe'; import { LanguageService } from '../../../../services/language.service'; +import { DialogComponent } from '../../../../shared/ui/dialog/dialog.component'; +import { ButtonComponent } from '../../../../shared/ui/button/button.component'; @Component({ selector: 'app-admin-product-editor-page', standalone: true, - imports: [AdminProductFormComponent, TranslatePipe], - template: `@if (facade.draft(); as draft) {

{{ title() | translate }}

} @else {

{{ 'common.loading' | translate }}

}`, + imports: [AdminProductFormComponent, TranslatePipe, DialogComponent, ButtonComponent], + template: `@if (facade.draft(); as draft) {

{{ title() | translate }}

} @else {

{{ 'common.loading' | translate }}

} + @if (facade.mutationError()) { + +

{{ 'common.errorDescription' | translate }}

+
+ {{ 'common.confirm' | translate }} +
+
+ }`, 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 }) @@ -33,7 +43,8 @@ export class AdminProductEditorPageComponent { } save(): void { - this.facade.saveDraft(); - void this.router.navigate([this.languageService.currentLanguage(), 'backoffice', 'products']); + this.facade.saveDraft(() => { + void this.router.navigate([this.languageService.currentLanguage(), 'backoffice', 'products']); + }); } } diff --git a/src/app/features/admin/products/pages/admin-products-list-page.component.ts b/src/app/features/admin/products/pages/admin-products-list-page.component.ts index 3676f3c..334b10f 100644 --- a/src/app/features/admin/products/pages/admin-products-list-page.component.ts +++ b/src/app/features/admin/products/pages/admin-products-list-page.component.ts @@ -5,11 +5,13 @@ import { AdminProductsListComponent } from '../components/admin-products-list.co import { LanguageService } from '../../../../services/language.service'; import { ConfirmDialogComponent } from '../../../../shared/ui/confirm-dialog/confirm-dialog.component'; import { TranslatePipe } from '../../../../i18n/translate.pipe'; +import { DialogComponent } from '../../../../shared/ui/dialog/dialog.component'; +import { ButtonComponent } from '../../../../shared/ui/button/button.component'; @Component({ selector: 'app-admin-products-list-page', standalone: true, - imports: [AdminProductsListComponent, ConfirmDialogComponent, TranslatePipe], + imports: [AdminProductsListComponent, ConfirmDialogComponent, TranslatePipe, DialogComponent, ButtonComponent], template: ``, + (cancelled)="bulkDeleteConfirmOpen.set(false)" /> + @if (facade.mutationError()) { + +

{{ 'common.errorDescription' | translate }}

+
+ {{ 'common.confirm' | translate }} +
+
+ }`, changeDetection: ChangeDetectionStrategy.OnPush }) export class AdminProductsListPageComponent { diff --git a/src/app/features/admin/users/facade/admin-users.facade.ts b/src/app/features/admin/users/facade/admin-users.facade.ts index 3b95261..aeb89d2 100644 --- a/src/app/features/admin/users/facade/admin-users.facade.ts +++ b/src/app/features/admin/users/facade/admin-users.facade.ts @@ -12,6 +12,7 @@ export class AdminUsersFacade { readonly invitations = signal([]); readonly loading = signal(false); readonly error = signal(false); + readonly mutationError = signal(null); readonly sessionsTarget = signal(null); readonly sessions = signal([]); readonly auditTarget = signal(null); @@ -33,16 +34,28 @@ export class AdminUsersFacade { } setRole(userId: string, roleId: string): void { - this.gateway.setUserRole(userId, roleId).pipe(take(1)).subscribe({ next: () => this.loadAll() }); + this.mutationError.set(null); + this.gateway.setUserRole(userId, roleId).pipe(take(1)).subscribe({ + next: () => this.loadAll(), + error: () => this.mutationError.set('common.errorDescription') + }); } setStatus(userId: string, status: AdminUserStatus): void { - this.gateway.setUserStatus(userId, status).pipe(take(1)).subscribe({ next: () => this.loadAll() }); + this.mutationError.set(null); + this.gateway.setUserStatus(userId, status).pipe(take(1)).subscribe({ + next: () => this.loadAll(), + error: () => this.mutationError.set('common.errorDescription') + }); } invite(email: string, roleId: string, scope: AdminUserScope): void { if (!email.trim()) return; - this.gateway.inviteUser(email.trim(), roleId, scope).pipe(take(1)).subscribe({ next: () => this.loadAll() }); + this.mutationError.set(null); + this.gateway.inviteUser(email.trim(), roleId, scope).pipe(take(1)).subscribe({ + next: () => this.loadAll(), + error: () => this.mutationError.set('common.errorDescription') + }); } revokeInvitation(id: string): void { diff --git a/src/app/features/admin/users/pages/admin-users-page.component.html b/src/app/features/admin/users/pages/admin-users-page.component.html index 0672c20..42b4ca1 100644 --- a/src/app/features/admin/users/pages/admin-users-page.component.html +++ b/src/app/features/admin/users/pages/admin-users-page.component.html @@ -128,4 +128,13 @@ [destructive]="true" (confirmed)="confirmSuspend()" (cancelled)="pendingSuspendUserId.set(null)" /> + + @if (facade.mutationError()) { + +

{{ 'common.errorDescription' | translate }}

+
+ {{ 'common.confirm' | translate }} +
+
+ }