fix: load errors swallowed to empty array across 5 admin facades
Orders/Products/Categories/Customers/Transactions loadList() caught errors by silently clearing the list to [] with no error state - an API failure looked identical to a genuine 'no results' empty state. Added an error signal to each facade (set on failure, cleared on retry) and an error branch in each list page/component, distinct from both loading and the real empty state. Order and Customer detail (loadDetail) had it worse: no error handler at all, so a failure just left the page on 'Loading...' forever with nothing to retry or navigate away with. Added selectedLoading/ selectedError to both facades and an error screen with a back button to both detail pages. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -64,6 +64,8 @@
|
||||
@for (i of [1,2,3,4]; track i) { <app-skeleton shape="rect" height="40px" /> }
|
||||
<span class="sr-only">{{ 'common.loading' | translate }}</span>
|
||||
</div>
|
||||
} @else if (error) {
|
||||
<app-empty-state [title]="'common.errorTitle' | translate" [description]="'common.errorDescription' | translate" />
|
||||
} @else if (treeRows.length === 0) {
|
||||
<app-empty-state [title]="'adminCategories.emptyTitle' | translate" [description]="'adminCategories.emptyDescription' | translate">
|
||||
<span slot="actions">
|
||||
|
||||
@@ -51,6 +51,7 @@ export class AdminCategoriesListComponent {
|
||||
@Input() flatCategories: AdminCategory[] = [];
|
||||
@Input() filters!: { search: string; visibility: 'all' | 'visible' | 'hidden'; includeDeleted: boolean };
|
||||
@Input() loading = false;
|
||||
@Input() error: string | null = null;
|
||||
@Input() viewMode: AdminCategoriesViewMode = 'tree';
|
||||
@Input() density: AdminCategoriesDensity = 'comfortable';
|
||||
@Input() visibleColumns: AdminCategoryColumn[] = [...ALL_CATEGORY_COLUMNS];
|
||||
|
||||
@@ -131,6 +131,7 @@ export class AdminCategoriesFacade {
|
||||
readonly filters = signal<AdminCategoryListFilters>({ search: '', visibility: 'all', includeDeleted: false });
|
||||
readonly categories = signal<AdminCategory[]>([]);
|
||||
readonly loading = signal(false);
|
||||
readonly error = signal<string | null>(null);
|
||||
readonly draft = signal<AdminCategory | null>(null);
|
||||
readonly editorMode = signal<AdminCategoryEditorMode>('create');
|
||||
readonly dirty = signal(false);
|
||||
@@ -174,6 +175,7 @@ export class AdminCategoriesFacade {
|
||||
*/
|
||||
loadList(): void {
|
||||
this.loading.set(true);
|
||||
this.error.set(null);
|
||||
this.gateway.loadCategories({ search: '', visibility: 'all', includeDeleted: true }).pipe(take(1)).subscribe({
|
||||
next: categories => {
|
||||
this.categories.set(categories);
|
||||
@@ -182,6 +184,7 @@ export class AdminCategoriesFacade {
|
||||
error: () => {
|
||||
this.categories.set([]);
|
||||
this.loading.set(false);
|
||||
this.error.set('common.errorDescription');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ import { TranslatePipe } from '../../../../i18n/translate.pipe';
|
||||
[allCategories]="facade.categories()"
|
||||
[filters]="facade.filters()"
|
||||
[loading]="facade.loading()"
|
||||
[error]="facade.error()"
|
||||
[viewMode]="facade.viewMode()"
|
||||
[density]="facade.density()"
|
||||
[visibleColumns]="facade.visibleColumns()"
|
||||
|
||||
@@ -10,8 +10,11 @@ export class AdminCustomersFacade {
|
||||
|
||||
readonly customers = signal<AdminCustomer[]>([]);
|
||||
readonly loading = signal(false);
|
||||
readonly error = signal<string | null>(null);
|
||||
readonly search = signal('');
|
||||
readonly selected = signal<AdminCustomer | null>(null);
|
||||
readonly selectedLoading = signal(false);
|
||||
readonly selectedError = signal<string | null>(null);
|
||||
|
||||
private buildCustomers(orders: AdminOrder[]): AdminCustomer[] {
|
||||
const byEmail = new Map<string, AdminOrder[]>();
|
||||
@@ -41,6 +44,7 @@ export class AdminCustomersFacade {
|
||||
|
||||
loadList(): void {
|
||||
this.loading.set(true);
|
||||
this.error.set(null);
|
||||
this.ordersGateway.loadOrders({ search: '', status: 'all', page: 1, pageSize: 100000 }).pipe(take(1)).subscribe({
|
||||
next: result => {
|
||||
this.customers.set(this.buildCustomers(result.items));
|
||||
@@ -49,16 +53,24 @@ export class AdminCustomersFacade {
|
||||
error: () => {
|
||||
this.customers.set([]);
|
||||
this.loading.set(false);
|
||||
this.error.set('common.errorDescription');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
loadDetail(email: string): void {
|
||||
this.selectedLoading.set(true);
|
||||
this.selectedError.set(null);
|
||||
this.ordersGateway.loadOrders({ search: '', status: 'all', page: 1, pageSize: 100000 }).pipe(take(1)).subscribe({
|
||||
next: result => {
|
||||
const decoded = decodeURIComponent(email);
|
||||
const customers = this.buildCustomers(result.items);
|
||||
this.selected.set(customers.find(customer => customer.email === decoded) ?? null);
|
||||
this.selectedLoading.set(false);
|
||||
},
|
||||
error: () => {
|
||||
this.selectedLoading.set(false);
|
||||
this.selectedError.set('common.errorDescription');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -48,6 +48,12 @@
|
||||
<app-order-timeline [entries]="activity()" [showOrderNumber]="true" />
|
||||
</section>
|
||||
</main>
|
||||
} @else if (facade.selectedError()) {
|
||||
<div class="customer-detail-error" role="alert">
|
||||
<p>{{ 'common.errorTitle' | translate }}</p>
|
||||
<p>{{ 'common.errorDescription' | translate }}</p>
|
||||
<app-button variant="secondary" size="sm" (click)="back()">{{ 'adminCustomers.back' | translate }}</app-button>
|
||||
</div>
|
||||
} @else {
|
||||
<p>{{ 'common.loading' | translate }}</p>
|
||||
}
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
@for (i of [1,2,3,4]; track i) { <app-skeleton shape="rect" height="40px" /> }
|
||||
<span class="sr-only">{{ 'common.loading' | translate }}</span>
|
||||
</div>
|
||||
} @else if (facade.error()) {
|
||||
<app-empty-state [title]="'common.errorTitle' | translate" [description]="'common.errorDescription' | translate" />
|
||||
} @else if (facade.filteredCustomers().length === 0) {
|
||||
<app-empty-state [title]="'adminCustomers.emptyTitle' | translate" [description]="'adminCustomers.emptyDescription' | translate" />
|
||||
} @else {
|
||||
|
||||
@@ -37,7 +37,10 @@ export class AdminOrdersFacade {
|
||||
readonly orders = signal<AdminOrder[]>([]);
|
||||
readonly total = signal(0);
|
||||
readonly loading = signal(false);
|
||||
readonly error = signal<string | null>(null);
|
||||
readonly selected = signal<AdminOrder | null>(null);
|
||||
readonly selectedLoading = signal(false);
|
||||
readonly selectedError = signal<string | null>(null);
|
||||
|
||||
readonly viewMode = signal<AdminOrdersViewMode>((this.localStorage.getItem(VIEW_MODE_KEY) as AdminOrdersViewMode) || 'table');
|
||||
readonly density = signal<AdminOrdersDensity>((this.localStorage.getItem(DENSITY_KEY) as AdminOrdersDensity) || 'comfortable');
|
||||
@@ -76,6 +79,7 @@ export class AdminOrdersFacade {
|
||||
|
||||
loadList(): void {
|
||||
this.loading.set(true);
|
||||
this.error.set(null);
|
||||
this.gateway.loadOrders(this.filters()).pipe(take(1)).subscribe({
|
||||
next: result => {
|
||||
this.orders.set(result.items);
|
||||
@@ -86,6 +90,7 @@ export class AdminOrdersFacade {
|
||||
this.orders.set([]);
|
||||
this.total.set(0);
|
||||
this.loading.set(false);
|
||||
this.error.set('common.errorDescription');
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -96,7 +101,18 @@ export class AdminOrdersFacade {
|
||||
}
|
||||
|
||||
loadDetail(id: string): void {
|
||||
this.gateway.loadOrder(id).pipe(take(1)).subscribe({ next: order => this.selected.set(order) });
|
||||
this.selectedLoading.set(true);
|
||||
this.selectedError.set(null);
|
||||
this.gateway.loadOrder(id).pipe(take(1)).subscribe({
|
||||
next: order => {
|
||||
this.selected.set(order);
|
||||
this.selectedLoading.set(false);
|
||||
},
|
||||
error: () => {
|
||||
this.selectedLoading.set(false);
|
||||
this.selectedError.set('common.errorDescription');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
setStatus(id: string, status: AdminOrderStatus): void {
|
||||
|
||||
@@ -103,6 +103,12 @@
|
||||
(confirmed)="confirmRefund()"
|
||||
(cancelled)="pendingRefundId.set(null)" />
|
||||
</main>
|
||||
} @else if (facade.selectedError()) {
|
||||
<div class="order-detail-error" role="alert">
|
||||
<p>{{ 'common.errorTitle' | translate }}</p>
|
||||
<p>{{ 'common.errorDescription' | translate }}</p>
|
||||
<app-button variant="secondary" size="sm" (click)="back()">{{ 'adminOrders.back' | translate }}</app-button>
|
||||
</div>
|
||||
} @else {
|
||||
<p>{{ 'common.loading' | translate }}</p>
|
||||
}
|
||||
|
||||
@@ -61,6 +61,8 @@
|
||||
@for (i of [1,2,3,4]; track i) { <app-skeleton shape="rect" height="40px" /> }
|
||||
<span class="sr-only">{{ 'common.loading' | translate }}</span>
|
||||
</div>
|
||||
} @else if (facade.error()) {
|
||||
<app-empty-state [title]="'common.errorTitle' | translate" [description]="'common.errorDescription' | translate" />
|
||||
} @else if (facade.orders().length === 0) {
|
||||
<app-empty-state [title]="'adminOrders.emptyTitle' | translate" [description]="'adminOrders.emptyDescription' | translate" />
|
||||
} @else {
|
||||
|
||||
@@ -78,6 +78,8 @@
|
||||
}
|
||||
<span class="sr-only">{{ 'common.loading' | translate }}</span>
|
||||
</div>
|
||||
} @else if (error) {
|
||||
<app-empty-state [title]="'common.errorTitle' | translate" [description]="'common.errorDescription' | translate" />
|
||||
} @else if (products.length === 0) {
|
||||
<app-empty-state [title]="'adminProducts.emptyTitle' | translate" [description]="'adminProducts.emptyDescription' | translate">
|
||||
<span slot="actions">
|
||||
|
||||
@@ -44,6 +44,7 @@ export class AdminProductsListComponent {
|
||||
@Input() total = 0;
|
||||
@Input() selectedIds: string[] = [];
|
||||
@Input() loading = false;
|
||||
@Input() error: string | null = null;
|
||||
@Input() infiniteScroll = false;
|
||||
@Input() viewMode: AdminProductsViewMode = 'table';
|
||||
@Input() density: AdminProductsDensity = 'comfortable';
|
||||
|
||||
@@ -91,6 +91,7 @@ export class AdminProductsFacade {
|
||||
readonly infiniteScroll = signal(false);
|
||||
readonly categories = signal<AdminProductCategoryOption[]>([]);
|
||||
readonly loading = signal(false);
|
||||
readonly error = signal<string | null>(null);
|
||||
readonly selectedIds = signal<string[]>([]);
|
||||
readonly draft = signal<AdminProduct | null>(null);
|
||||
readonly dirty = signal(false);
|
||||
@@ -100,6 +101,7 @@ export class AdminProductsFacade {
|
||||
|
||||
loadList(): void {
|
||||
this.loading.set(true);
|
||||
this.error.set(null);
|
||||
this.gateway.loadProducts(this.filters()).pipe(take(1)).subscribe({
|
||||
next: result => {
|
||||
this.products.set(result.items);
|
||||
@@ -110,6 +112,7 @@ export class AdminProductsFacade {
|
||||
this.products.set([]);
|
||||
this.total.set(0);
|
||||
this.loading.set(false);
|
||||
this.error.set('common.errorDescription');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ import { TranslatePipe } from '../../../../i18n/translate.pipe';
|
||||
[total]="facade.total()"
|
||||
[selectedIds]="facade.selectedIds()"
|
||||
[loading]="facade.loading()"
|
||||
[error]="facade.error()"
|
||||
[infiniteScroll]="facade.infiniteScroll()"
|
||||
[viewMode]="facade.viewMode()"
|
||||
[density]="facade.density()"
|
||||
|
||||
@@ -11,9 +11,11 @@ export class AdminTransactionsFacade {
|
||||
readonly transactions = signal<AdminTransaction[]>([]);
|
||||
readonly total = signal(0);
|
||||
readonly loading = signal(false);
|
||||
readonly error = signal<string | null>(null);
|
||||
|
||||
loadList(): void {
|
||||
this.loading.set(true);
|
||||
this.error.set(null);
|
||||
this.gateway.loadTransactions(this.filters()).pipe(take(1)).subscribe({
|
||||
next: result => {
|
||||
this.transactions.set(result.items);
|
||||
@@ -24,6 +26,7 @@ export class AdminTransactionsFacade {
|
||||
this.transactions.set([]);
|
||||
this.total.set(0);
|
||||
this.loading.set(false);
|
||||
this.error.set('common.errorDescription');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -22,6 +22,8 @@
|
||||
@for (i of [1,2,3,4]; track i) { <app-skeleton shape="rect" height="40px" /> }
|
||||
<span class="sr-only">{{ 'common.loading' | translate }}</span>
|
||||
</div>
|
||||
} @else if (facade.error()) {
|
||||
<app-empty-state [title]="'common.errorTitle' | translate" [description]="'common.errorDescription' | translate" />
|
||||
} @else if (facade.transactions().length === 0) {
|
||||
<app-empty-state [title]="'adminTransactions.emptyTitle' | translate" [description]="'adminTransactions.emptyDescription' | translate" />
|
||||
} @else {
|
||||
|
||||
Reference in New Issue
Block a user