From 374774901e52af440d1941f7f1cc9e5a26cdd4e9 Mon Sep 17 00:00:00 2001 From: sdarbinyan Date: Sun, 19 Jul 2026 22:51:19 +0400 Subject: [PATCH] fix(search): drop unresolvable category filter options instead of faking a label MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit P0-4: the Catalog/Search category facet showed placeholder labels like 'Категория 2008', 'Категория 22612' for every option — traced to search.facade.ts buildFilterGroups() synthesizing 'Category {id}' for every distinct product.categoryID with no attempt to resolve a real name. Investigated further: the mismatch isn't a missing-lookup bug, it's a real data gap. Product categoryID values in the mock catalog fixture don't correspond to any id in CategoryFacade's category tree (a much smaller, separately-curated mock dataset) — there is no real category name to show for these ids today. Wired CategoryFacade into SearchFacade and resolve each option's real title when the id does match; when it doesn't (the common case with current mock data), the option is dropped rather than showing a fabricated technical-looking label. The category filter group simply doesn't render when nothing resolves, which is honest given the data, instead of looking like broken/unseeded content in front of a client. Co-Authored-By: Claude Sonnet 5 --- .../features/search/facade/search.facade.ts | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/src/app/features/search/facade/search.facade.ts b/src/app/features/search/facade/search.facade.ts index 15466a8..aa526ed 100644 --- a/src/app/features/search/facade/search.facade.ts +++ b/src/app/features/search/facade/search.facade.ts @@ -4,6 +4,7 @@ import { Observable, Subject, of } from 'rxjs'; import { debounceTime, distinctUntilChanged, map, switchMap, tap } from 'rxjs/operators'; import { Category } from '../../../core/categories/models/category-domain.model'; import { Product, ProductListResult } from '../../../core/products/models/product-domain.model'; +import { CategoryFacade } from '../../../facades/platform/category.facade'; import { ProductFacade } from '../../../facades/platform/product.facade'; import { TranslateService } from '../../../i18n/translate.service'; import { SearchAutocompleteService } from '../services/search-autocomplete.service'; @@ -34,6 +35,7 @@ interface LegacySearchState { @Injectable({ providedIn: 'root' }) export class SearchFacade { private readonly productFacade = inject(ProductFacade); + private readonly categoryFacade = inject(CategoryFacade); private readonly translate = inject(TranslateService); private readonly autocompleteService = inject(SearchAutocompleteService); private readonly historyService = inject(SearchHistoryService); @@ -278,7 +280,9 @@ export class SearchFacade { id: 'category', label: this.translate.t('catalog.filterCategory'), type: 'radio', - options: categories.map(value => ({ id: `cat-${value}`, label: this.translate.t('catalog.filterCategoryValue', { value }), value })), + options: categories + .map(value => ({ id: `cat-${value}`, label: this.resolveCategoryLabel(value), value })) + .filter((option): option is { id: string; label: string; value: string } => option.label !== null), enabled: enabled.has('category'), }, { @@ -545,6 +549,19 @@ export class SearchFacade { return [...new Set(values.map(value => value.trim()).filter(Boolean))]; } + /** + * Returns the real category title, or null when the id has no match in the loaded + * category tree — callers drop unresolved options instead of showing a fabricated + * "Category {id}" label (see RELEASE-CANDIDATE-AUDIT.md P0-4). + */ + private resolveCategoryLabel(categoryId: string): string | null { + const id = Number(categoryId); + if (!Number.isFinite(id)) { + return null; + } + return this.categoryFacade.allCategories().find(category => category.id === id)?.title ?? null; + } + private toColorHex(value: string): string { const normalized = value.trim().toLowerCase();