fix(admin): products canDeactivate guard, unblock npm, drop dead deps

RC-01 Phase 1 mechanical fixes (verified against current repo state,
not blindly reapplied from TODO.md):

- admin/products create/edit/duplicate now protected by an unsaved-
  changes guard (adminProductDirtyGuard), mirroring the existing
  categories pattern. AdminProductsFacade had zero dirty-tracking
  before this - added a dirty signal, set true on updateDraft(),
  cleared on load/create/successful save. Added confirmLeaveUnsaved
  to the adminProducts i18n section (en/ru/hy) - categories already
  had its own copy of this key, products didn't.
- barry-cache bumped ^0.1.0 -> ^0.9.3 (the pinned range no longer
  resolved on the registry - ETARGET - which had been silently
  blocking every npm install/uninstall all cycle).
- Removed primeng/primeicons (npm uninstall, now unblocked) - the
  only consumer (items-carousel) was already deleted in RC PERF-01.
- Removed core/search/services/search-history.service.ts, a dead
  1-line re-export with zero importers (verified: the real
  implementation is features/search/services/search-history.service.ts,
  used by search.facade.ts). Left core/search/models/* alone - those
  ARE live, imported by catalog components.

Verified before touching: HeaderConfig.showProfile toggle is already
removed from the header-section editor template (TODO.md was stale on
this one) - no change needed, will correct the tracking doc separately.

tsc --noEmit clean, npm run build green (bundle unchanged, primeng
was already tree-shaken out, this just removes the dead dependency
declaration itself).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
sdarbinyan
2026-07-25 20:04:51 +04:00
parent 46c7358dde
commit 7b639cabf8
10 changed files with 132 additions and 83 deletions

View File

@@ -4,6 +4,7 @@ import { projectEditorDirtyGuard } from './features/project-editor/guards/projec
import { adminAuthGuard } from './core/admin-auth/admin-auth.guard';
import { authRoutes } from './core/auth/auth.routes';
import { adminCategoryDirtyGuard } from './features/admin/categories/guards/admin-category-dirty.guard';
import { adminProductDirtyGuard } from './features/admin/products/guards/admin-product-dirty.guard';
import { AdminLayoutComponent } from './features/admin/shell/admin-layout.component';
import { environment } from '../environments/environment';
@@ -82,6 +83,7 @@ const coreRoutes: Routes = [
{
path: 'products/create',
loadComponent: () => import('./features/admin/products/pages/admin-product-editor-page.component').then(m => m.AdminProductEditorPageComponent),
canDeactivate: [adminProductDirtyGuard],
data: {
titleKey: 'adminShell.pages.productCreate.title',
descriptionKey: 'adminShell.pages.productCreate.description',
@@ -91,6 +93,7 @@ const coreRoutes: Routes = [
{
path: 'products/:id/edit',
loadComponent: () => import('./features/admin/products/pages/admin-product-editor-page.component').then(m => m.AdminProductEditorPageComponent),
canDeactivate: [adminProductDirtyGuard],
data: {
titleKey: 'adminShell.pages.productEdit.title',
descriptionKey: 'adminShell.pages.productEdit.description',
@@ -100,6 +103,7 @@ const coreRoutes: Routes = [
{
path: 'products/:id/duplicate',
loadComponent: () => import('./features/admin/products/pages/admin-product-editor-page.component').then(m => m.AdminProductEditorPageComponent),
canDeactivate: [adminProductDirtyGuard],
data: {
titleKey: 'adminShell.pages.productDuplicate.title',
descriptionKey: 'adminShell.pages.productDuplicate.description',

View File

@@ -1 +0,0 @@
export { SearchHistoryService } from '../../../features/search/services/search-history.service';

View File

@@ -93,6 +93,7 @@ export class AdminProductsFacade {
readonly loading = signal(false);
readonly selectedIds = signal<string[]>([]);
readonly draft = signal<AdminProduct | null>(null);
readonly dirty = signal(false);
readonly editorMode = signal<AdminProductEditorMode>('create');
readonly hasSelection = computed(() => this.selectedIds().length > 0);
@@ -283,20 +284,22 @@ export class AdminProductsFacade {
startCreate(): void {
this.editorMode.set('create');
this.draft.set(this.formFactory.createEmpty());
this.dirty.set(false);
}
loadForEdit(id: string, mode: AdminProductEditorMode = 'edit'): void {
this.editorMode.set(mode);
if (mode === 'duplicate') {
this.gateway.duplicateProduct(id).pipe(take(1)).subscribe({ next: product => this.draft.set(product) });
this.gateway.duplicateProduct(id).pipe(take(1)).subscribe({ next: product => { this.draft.set(product); this.dirty.set(false); } });
return;
}
this.gateway.loadProduct(id).pipe(take(1)).subscribe({ next: product => this.draft.set(product ? { ...product } : null) });
this.gateway.loadProduct(id).pipe(take(1)).subscribe({ next: product => { this.draft.set(product ? { ...product } : null); this.dirty.set(false); } });
}
updateDraft(patch: Partial<AdminProduct>): void {
this.draft.update(current => current ? ({ ...current, ...patch, updatedAt: new Date().toISOString() }) : current);
this.dirty.set(true);
}
saveDraft(): void {
@@ -307,7 +310,7 @@ export class AdminProductsFacade {
? this.gateway.createProduct(draft)
: this.gateway.updateProduct(draft);
request.pipe(take(1)).subscribe({ next: () => this.loadList() });
request.pipe(take(1)).subscribe({ next: () => { this.dirty.set(false); this.loadList(); } });
}
deleteOne(id: string): void {

View File

@@ -0,0 +1,14 @@
import { inject } from '@angular/core';
import { CanDeactivateFn } from '@angular/router';
import { AdminProductsFacade } from '../facade/admin-products.facade';
import { AdminProductEditorPageComponent } from '../pages/admin-product-editor-page.component';
import { TranslateService } from '../../../../i18n/translate.service';
export const adminProductDirtyGuard: CanDeactivateFn<AdminProductEditorPageComponent> = () => {
const facade = inject(AdminProductsFacade);
if (!facade.dirty()) {
return true;
}
const translate = inject(TranslateService);
return window.confirm(translate.t('adminProducts.confirmLeaveUnsaved'));
};

View File

@@ -1543,6 +1543,7 @@ export const en: Translations = {
recommendNone: 'Nice work — your category structure is in good shape.',
},
adminProducts: {
confirmLeaveUnsaved: 'You have unsaved changes. Leave without saving?',
emptyTitle: 'No products found',
emptyDescription: 'Try adjusting your filters, or create a new product.',
emptyGuide: 'Good products have a clear title, at least one photo, a price, and a short description — that\'s enough to publish. You can always add more detail later.',

View File

@@ -1538,6 +1538,7 @@ export const hy: Translations = {
recommendNone: 'Հիանալի է․ կատեգորիաների կառուցվածքը լավ վիճակում է։',
},
adminProducts: {
confirmLeaveUnsaved: 'Դուք ունեք չպահպանված փոփոխություններ։ Դո՞ւրս գալ առանց պահպանելու։',
emptyTitle: 'Ապրանքներ չեն գտնվել',
emptyDescription: 'Փոխեք ֆիլտրերը կամ ստեղծեք նոր ապրանք։',
emptyGuide: 'Լավ ապրանքին բավական է հստակ վերնագիր, առնվազն մեկ լուսանկար, գին և կարճ նկարագրություն՝ հրապարակելու համար։ Մնացածը կարող եք ավելացնել հետո։',

View File

@@ -1538,6 +1538,7 @@ export const ru: Translations = {
recommendNone: 'Отлично — структура категорий в хорошем состоянии.',
},
adminProducts: {
confirmLeaveUnsaved: 'У вас есть несохранённые изменения. Выйти без сохранения?',
emptyTitle: 'Товары не найдены',
emptyDescription: 'Измените фильтры или создайте новый товар.',
emptyGuide: 'Хорошему товару нужны понятное название, минимум одно фото, цена и короткое описание — этого достаточно для публикации. Остальное можно добавить позже.',

View File

@@ -1551,6 +1551,7 @@ export interface Translations {
recommendNone: string;
};
adminProducts: {
confirmLeaveUnsaved: string;
emptyTitle: string;
emptyDescription: string;
emptyGuide: string;