@c-rex/components 0.3.0-build.39 → 0.3.0-build.40

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -21,14 +21,19 @@ import { Button } from "@c-rex/ui/button";
21
21
  import { parseAsString, useQueryStates } from "nuqs";
22
22
  import {
23
23
  applyFacetPropertyVisibility,
24
- FacetLabelOverrides,
24
+ FacetPresentation,
25
25
  FacetTaxonomies,
26
26
  memoizeFilteredTags,
27
+ pruneHierarchyFacetItemsForMerge,
27
28
  removeFilterItem,
29
+ resolveHierarchyFacetCountLabel,
30
+ resolveFacetTaxonomy,
28
31
  ResolvedFilterItem,
29
32
  resolveLabelByLanguage,
33
+ shouldRenderFacetHierarchy,
30
34
  updateFilterParam
31
35
  } from "./utils";
36
+ import { buildTaxonomyHierarchy } from "../../taxonomy/hierarchy";
32
37
  import { FilterItem, Tags } from "@c-rex/interfaces";
33
38
  import type { TaxonomyResult } from "@c-rex/services/read-models";
34
39
  import { useSearchNavigationStore } from "../../stores/search-navigation-store";
@@ -39,7 +44,8 @@ import { useFilterSidebarState } from "./context";
39
44
  export interface FilterSidebarProps {
40
45
  tags?: Tags
41
46
  totalItemCount?: number
42
- facetLabelOverrides?: FacetLabelOverrides
47
+ facetPresentation?: FacetPresentation
48
+ facetLabelOverrides?: FacetPresentation
43
49
  facetTaxonomies?: FacetTaxonomies
44
50
  includeZeroHits?: boolean
45
51
  includeProperties?: string[]
@@ -49,6 +55,7 @@ export interface FilterSidebarProps {
49
55
  const FilterSidebar: FC<FilterSidebarProps> = ({
50
56
  tags,
51
57
  totalItemCount,
58
+ facetPresentation,
52
59
  facetLabelOverrides,
53
60
  facetTaxonomies,
54
61
  includeZeroHits = true,
@@ -56,6 +63,7 @@ const FilterSidebar: FC<FilterSidebarProps> = ({
56
63
  excludeProperties,
57
64
  }) => {
58
65
  const t = useTranslations();
66
+ const tagT = useTranslations("filter.tags");
59
67
  const locale = useLocale();
60
68
  const device = useBreakpoint();
61
69
  const isMobile = device !== null && (device === DEVICE_OPTIONS.MOBILE || device === DEVICE_OPTIONS.TABLET);
@@ -68,16 +76,17 @@ const FilterSidebar: FC<FilterSidebarProps> = ({
68
76
  shallow: false,
69
77
  });
70
78
  const startSearchNavigation = useSearchNavigationStore((state) => state.start);
79
+ const effectiveFacetPresentation = facetPresentation || facetLabelOverrides;
71
80
 
72
81
  const filteredTags = useMemo(() => {
73
82
  const resolved = memoizeFilteredTags(tags, params.filter, params.packages, {
74
83
  uiLanguage: locale,
75
- labelOverrides: facetLabelOverrides,
84
+ facetPresentation: effectiveFacetPresentation,
76
85
  includeZeroHits,
77
86
  facetTaxonomies,
78
87
  });
79
88
  return applyFacetPropertyVisibility(resolved, includeProperties, excludeProperties);
80
- }, [tags, params.filter, params.packages, locale, facetLabelOverrides, facetTaxonomies, includeZeroHits, includeProperties, excludeProperties]);
89
+ }, [tags, params.filter, params.packages, locale, effectiveFacetPresentation, facetTaxonomies, includeZeroHits, includeProperties, excludeProperties]);
81
90
  const [expandedFacetItems, setExpandedFacetItems] = useState<Record<string, number>>({});
82
91
  const [expandedHierarchyNodes, setExpandedHierarchyNodes] = useState<Record<string, boolean>>({});
83
92
  const getVisibleCount = (key: string, total: number) => expandedFacetItems[key] || Math.min(total, 10);
@@ -106,14 +115,23 @@ const FilterSidebar: FC<FilterSidebarProps> = ({
106
115
  }
107
116
 
108
117
  const resolvePropertyLabel = (propertyKey: string): string => {
109
- const taxonomy = facetTaxonomies?.[propertyKey];
118
+ const taxonomy = resolveFacetTaxonomy(propertyKey, facetTaxonomies);
110
119
  const virtualRootLabel = taxonomy?.roots?.length === 1 && taxonomy.roots[0]?.isVirtualGroup
111
120
  ? taxonomy.roots[0].label
112
121
  : undefined;
113
- if (virtualRootLabel) {
122
+ if (virtualRootLabel && virtualRootLabel !== "Ungrouped") {
114
123
  return virtualRootLabel;
115
124
  }
116
125
 
126
+ try {
127
+ const translated = tagT(propertyKey as Parameters<typeof tagT>[0]);
128
+ if (translated && translated !== propertyKey) {
129
+ return translated;
130
+ }
131
+ } catch {
132
+ // Fallback to API labels when no UI translation exists for the facet key.
133
+ }
134
+
117
135
  const rawLabels = (tags?.[propertyKey]?.labels as unknown[] | undefined) || [];
118
136
  const normalized = rawLabels
119
137
  .map((item) => {
@@ -135,100 +153,14 @@ const FilterSidebar: FC<FilterSidebarProps> = ({
135
153
  return resolveLabelByLanguage(normalized, locale) || propertyKey;
136
154
  };
137
155
 
138
- const isHierarchicalFacet = (items: ResolvedFilterItem[]): boolean => items.some((item) => (item.parentIds || []).length > 0);
139
-
140
156
  const buildHierarchy = (propertyKey: string, items: ResolvedFilterItem[]) => {
141
- const taxonomy = facetTaxonomies?.[propertyKey] as TaxonomyResult | undefined;
157
+ const taxonomy = resolveFacetTaxonomy(propertyKey, facetTaxonomies) as TaxonomyResult | undefined;
142
158
  if (taxonomy) {
143
- const selectedById = new Map<string, ResolvedFilterItem>();
144
- const includedIds = new Set<string>();
145
-
146
- items.forEach((item) => {
147
- const itemId = item.taxonomyId || item.shortId;
148
- if (!itemId) return;
149
- selectedById.set(itemId, item);
150
- includedIds.add(itemId);
151
-
152
- const taxonomyNode = taxonomy.nodesById[itemId];
153
- (taxonomyNode?.ancestorIds || []).forEach((ancestorId) => {
154
- if (taxonomy.nodesById[ancestorId]) includedIds.add(ancestorId);
155
- });
156
- (taxonomyNode?.parentIds || []).forEach((parentId) => {
157
- if (taxonomy.nodesById[parentId]) includedIds.add(parentId);
158
- });
159
- });
160
-
161
- const byId = new Map<string, ResolvedFilterItem>();
162
- const children = new Map<string, ResolvedFilterItem[]>();
163
-
164
- Array.from(includedIds).forEach((nodeId) => {
165
- const taxonomyNode = taxonomy.nodesById[nodeId];
166
- if (!taxonomyNode) return;
167
-
168
- const selected = selectedById.get(nodeId);
169
-
170
- byId.set(nodeId, selected || {
171
- shortId: taxonomyNode.shortId || nodeId,
172
- label: taxonomyNode.label,
173
- active: false,
174
- hits: 0,
175
- total: 0,
176
- taxonomyId: taxonomyNode.id,
177
- parentIds: taxonomyNode.parentIds || [],
178
- isStructural: true,
179
- });
180
- children.set(nodeId, []);
181
- });
182
-
183
- Array.from(includedIds).forEach((nodeId) => {
184
- const current = byId.get(nodeId);
185
- const taxonomyNode = taxonomy.nodesById[nodeId];
186
- if (!current || !taxonomyNode) return;
187
-
188
- const parentKey = (taxonomyNode.parentIds || []).find((id) => byId.has(id));
189
- if (parentKey) {
190
- children.get(parentKey)?.push(current);
191
- }
192
- });
193
-
194
- const roots = Array.from(byId.values()).filter((node) => {
195
- const nodeId = node.taxonomyId || node.shortId;
196
- const taxonomyNode = nodeId ? taxonomy.nodesById[nodeId] : undefined;
197
- return !(taxonomyNode?.parentIds || []).some((id) => byId.has(id));
198
- });
199
-
200
- const sortItems = (nodes: ResolvedFilterItem[]) => nodes.sort((a, b) => a.label.localeCompare(b.label));
201
- sortItems(roots);
202
- children.forEach((nodes) => sortItems(nodes));
203
-
204
- return { roots, children };
159
+ const mergeSeedItems = pruneHierarchyFacetItemsForMerge(propertyKey, items);
160
+ return buildTaxonomyHierarchy(mergeSeedItems, taxonomy);
205
161
  }
206
162
 
207
- const byId = new Map<string, ResolvedFilterItem>();
208
- const children = new Map<string, ResolvedFilterItem[]>();
209
- const roots: ResolvedFilterItem[] = [];
210
-
211
- items.forEach((item) => {
212
- const key = item.taxonomyId || item.shortId;
213
- byId.set(key, item);
214
- children.set(key, []);
215
- });
216
-
217
- items.forEach((item) => {
218
- const parentKey = (item.parentIds || []).find((id) => byId.has(id));
219
- if (!parentKey) {
220
- roots.push(item);
221
- return;
222
- }
223
-
224
- children.get(parentKey)?.push(item);
225
- });
226
-
227
- const sortItems = (nodes: ResolvedFilterItem[]) => nodes.sort((a, b) => a.label.localeCompare(b.label));
228
- sortItems(roots);
229
- children.forEach((nodes) => sortItems(nodes));
230
-
231
- return { roots, children };
163
+ return buildTaxonomyHierarchy(items);
232
164
  };
233
165
 
234
166
  const renderFacetItems = (key: string, items: ResolvedFilterItem[]) => {
@@ -341,7 +273,13 @@ const FilterSidebar: FC<FilterSidebarProps> = ({
341
273
  const itemKey = item.taxonomyId || item.shortId;
342
274
  const children = childrenMap.get(itemKey) || [];
343
275
  const hasChildren = children.length > 0;
344
- const expanded = expandedHierarchyNodes[itemKey] !== false;
276
+ const countLabel = resolveHierarchyFacetCountLabel(item, children);
277
+ const expanded = hasChildren
278
+ ? depth === 0
279
+ ? expandedHierarchyNodes[itemKey] === true
280
+ : expandedHierarchyNodes[itemKey] !== false
281
+ : false;
282
+ const toggleSlotClass = "mt-1 inline-flex h-5 w-5 shrink-0 items-center justify-center";
345
283
 
346
284
  return (
347
285
  <SidebarMenuSubItem key={item.shortId}>
@@ -350,16 +288,14 @@ const FilterSidebar: FC<FilterSidebarProps> = ({
350
288
  {hasChildren ? (
351
289
  <button
352
290
  type="button"
353
- className="mt-1 rounded p-0.5 hover:bg-sidebar-accent"
291
+ className={`${toggleSlotClass} rounded hover:bg-sidebar-accent`}
354
292
  onClick={() => setExpandedHierarchyNodes((prev) => ({ ...prev, [itemKey]: !prev[itemKey] }))}
355
293
  aria-label={expanded ? "Collapse" : "Expand"}
356
294
  >
357
295
  {expanded ? <ChevronDown className="h-4 w-4" /> : <ChevronRight className="h-4 w-4" />}
358
296
  </button>
359
- ) : depth > 0 ? (
360
- <span className="inline-block w-5" />
361
297
  ) : (
362
- <span className="inline-block w-0" />
298
+ <span className={toggleSlotClass} aria-hidden="true" />
363
299
  )}
364
300
  <SidebarMenuSubButton
365
301
  className={item.isStructural ? "!py-1.5 cursor-default text-muted-foreground" : "cursor-pointer !py-1.5"}
@@ -369,7 +305,7 @@ const FilterSidebar: FC<FilterSidebarProps> = ({
369
305
  onClick={item.isStructural ? undefined : () => onClickHandler(propertyKey, item)}
370
306
  style={{ marginLeft: depth * 14 }}
371
307
  >
372
- {item.label} ({item.hits}/{item.total})
308
+ {item.label}{countLabel ? ` ${countLabel}` : ""}
373
309
  {item.active && <Check className="ml-2" />}
374
310
  </SidebarMenuSubButton>
375
311
  </div>
@@ -387,10 +323,10 @@ const FilterSidebar: FC<FilterSidebarProps> = ({
387
323
  <SidebarContent className="!gap-0 capitalize" suppressHydrationWarning>
388
324
  {Object.entries(filteredTags).map(([key, value]) => {
389
325
  const items = value as ResolvedFilterItem[];
390
- const hasTaxonomy = Boolean(facetTaxonomies?.[key]);
391
- const hasSectionLabels = !hasTaxonomy && items.some((item) => Boolean(item.sectionLabel));
326
+ const hasTaxonomy = Boolean(resolveFacetTaxonomy(key, facetTaxonomies));
392
327
  const propertyLabel = resolvePropertyLabel(key);
393
- const hierarchicalFacet = hasTaxonomy || (!hasSectionLabels && isHierarchicalFacet(items));
328
+ const hasSectionLabels = items.some((item) => Boolean(item.sectionLabel));
329
+ const hierarchicalFacet = shouldRenderFacetHierarchy(items, { hasTaxonomy });
394
330
  const hierarchy = hierarchicalFacet ? buildHierarchy(key, items) : undefined;
395
331
  const visibleRootCount = getVisibleCount(key, hierarchy?.roots.length || 0);
396
332
  const visibleRoots = hierarchy?.roots.slice(0, visibleRootCount) || [];
@@ -455,7 +391,7 @@ const FilterSidebar: FC<FilterSidebarProps> = ({
455
391
  <div className="sr-only">{propertyLabel}</div>
456
392
  {Object.entries(
457
393
  items.reduce<Record<string, ResolvedFilterItem[]>>((acc, item) => {
458
- const sectionLabel = item.sectionLabel || "Other";
394
+ const sectionLabel = item.sectionLabel || propertyLabel;
459
395
  if (!acc[sectionLabel]) acc[sectionLabel] = [];
460
396
  acc[sectionLabel].push(item);
461
397
  return acc;
@@ -464,6 +400,13 @@ const FilterSidebar: FC<FilterSidebarProps> = ({
464
400
  .sort(([a], [b]) => a.localeCompare(b))
465
401
  .map(([sectionLabel, sectionItems]) => {
466
402
  const hasNestedGroups = sectionItems.some((item) => Boolean(item.groupLabel));
403
+ const sectionHasTaxonomy = Boolean(resolveFacetTaxonomy(key, facetTaxonomies));
404
+ const sectionHierarchy = sectionHasTaxonomy ? buildHierarchy(key, sectionItems) : undefined;
405
+ const shouldRenderSectionHierarchy = sectionHasTaxonomy && (sectionHierarchy?.roots.length || 0) > 0;
406
+ const visibleSectionRootCount = getVisibleCount(`${key}:${sectionLabel}`, sectionHierarchy?.roots.length || 0);
407
+ const visibleSectionRoots = sectionHierarchy?.roots.slice(0, visibleSectionRootCount) || [];
408
+ const hasMoreSectionRoots = (sectionHierarchy?.roots.length || 0) > visibleSectionRootCount;
409
+ const canShowLessSectionRoots = visibleSectionRootCount > getInitialVisibleCount(sectionHierarchy?.roots.length || 0);
467
410
  return (
468
411
  <Collapsible
469
412
  defaultOpen
@@ -484,13 +427,39 @@ const FilterSidebar: FC<FilterSidebarProps> = ({
484
427
  <SidebarGroupContent>
485
428
  <SidebarMenu className="!gap-0.5">
486
429
  <SidebarMenuSub className="!ml-2 !pl-1.5 !py-0 !gap-0.5">
487
- {!hasNestedGroups &&
430
+ {shouldRenderSectionHierarchy && !hasNestedGroups &&
431
+ visibleSectionRoots.map((item) => renderHierarchyNode(key, item, sectionHierarchy!.children, 0))}
432
+ {shouldRenderSectionHierarchy && !hasNestedGroups && (hasMoreSectionRoots || canShowLessSectionRoots) && (
433
+ <SidebarMenuSubItem>
434
+ <div className="flex items-center gap-3 px-2">
435
+ {hasMoreSectionRoots && (
436
+ <Button
437
+ variant="ghost"
438
+ className="justify-start px-0 text-sm"
439
+ onClick={() => showMore(`${key}:${sectionLabel}`, sectionHierarchy?.roots.length || 0)}
440
+ >
441
+ {t("more")}
442
+ </Button>
443
+ )}
444
+ {canShowLessSectionRoots && (
445
+ <Button
446
+ variant="ghost"
447
+ className="justify-start px-0 text-sm"
448
+ onClick={() => showLess(`${key}:${sectionLabel}`, sectionHierarchy?.roots.length || 0)}
449
+ >
450
+ {t("less")}
451
+ </Button>
452
+ )}
453
+ </div>
454
+ </SidebarMenuSubItem>
455
+ )}
456
+ {!shouldRenderSectionHierarchy && !hasNestedGroups &&
488
457
  renderGroupedItems(key, `${key}:${sectionLabel}`, sectionItems)}
489
458
 
490
459
  {hasNestedGroups &&
491
460
  Object.entries(
492
461
  sectionItems.reduce<Record<string, ResolvedFilterItem[]>>((acc, item) => {
493
- const groupLabel = item.groupLabel || "Other";
462
+ const groupLabel = item.groupLabel || sectionLabel;
494
463
  if (!acc[groupLabel]) acc[groupLabel] = [];
495
464
  acc[groupLabel].push(item);
496
465
  return acc;
@@ -1,14 +1,25 @@
1
1
  import { FilterItem, Tags } from "@c-rex/interfaces";
2
2
  import { EN_LANG } from "@c-rex/constants";
3
- import type { TaxonomyResult } from "@c-rex/services/read-models";
4
-
5
- export type FacetLabelOverride = {
3
+ import {
4
+ type InformationUnitPropertyKey,
5
+ type TaxonomyResult,
6
+ } from "@c-rex/services/read-models";
7
+ import {
8
+ isMetadataGroupDenied,
9
+ isMetadataPropertyRuntimeSuppressed,
10
+ } from "@c-rex/services/metadata-visibility-policy";
11
+
12
+ export type FacetPresentationItem = {
6
13
  label: string;
7
14
  groupLabel?: string;
8
15
  sectionLabel?: string;
16
+ hidden?: boolean;
9
17
  };
10
18
 
11
- export type FacetLabelOverrides = Partial<Record<string, Record<string, FacetLabelOverride>>>;
19
+ export type FacetPresentation = Partial<Record<string, Record<string, FacetPresentationItem>>>;
20
+
21
+ export type FacetLabelOverride = FacetPresentationItem;
22
+ export type FacetLabelOverrides = FacetPresentation;
12
23
 
13
24
  export type ResolvedFilterItem = FilterItem & {
14
25
  groupLabel?: string;
@@ -20,6 +31,20 @@ export type ResolvedFilterItem = FilterItem & {
20
31
 
21
32
  export type FacetTaxonomies = Partial<Record<string, TaxonomyResult>>;
22
33
 
34
+ export const resolveFacetTaxonomy = (
35
+ facetKey: string,
36
+ facetTaxonomies?: FacetTaxonomies
37
+ ): TaxonomyResult | undefined => {
38
+ if (!facetTaxonomies) return undefined;
39
+ if (facetTaxonomies[facetKey]) return facetTaxonomies[facetKey];
40
+
41
+ if (facetKey === "categories") {
42
+ return facetTaxonomies.informationSubjects;
43
+ }
44
+
45
+ return undefined;
46
+ };
47
+
23
48
  export const updateFilterParam = (key: string, item: FilterItem, filter: string | null): Record<string, string | null> => {
24
49
 
25
50
  if (key === "packages") {
@@ -93,24 +118,28 @@ export const clearData = (
93
118
  tags: Tags,
94
119
  options?: {
95
120
  uiLanguage?: string;
96
- labelOverrides?: FacetLabelOverrides;
121
+ facetPresentation?: FacetPresentation;
122
+ labelOverrides?: FacetPresentation;
97
123
  includeZeroHits?: boolean;
98
124
  facetTaxonomies?: FacetTaxonomies;
99
125
  }
100
126
  ): Record<string, ResolvedFilterItem[]> => {
101
127
  const includeZeroHits = options?.includeZeroHits ?? true;
128
+ const facetPresentation = options?.facetPresentation || options?.labelOverrides;
102
129
  const filteredTags: Record<string, ResolvedFilterItem[]> = {}
103
130
  const taxonomyNodesByFacet = Object.entries(options?.facetTaxonomies || {}).reduce<Record<string, Map<string, {
104
131
  id: string;
105
132
  shortId?: string;
106
133
  label: string;
107
134
  parentIds: string[];
135
+ classId?: string;
108
136
  }>>>((acc, [facetKey, taxonomy]) => {
109
137
  const nodes = new Map<string, {
110
138
  id: string;
111
139
  shortId?: string;
112
140
  label: string;
113
141
  parentIds: string[];
142
+ classId?: string;
114
143
  }>();
115
144
 
116
145
  Object.values(taxonomy?.nodesById || {}).forEach((node) => {
@@ -119,6 +148,7 @@ export const clearData = (
119
148
  shortId: node.shortId,
120
149
  label: node.label,
121
150
  parentIds: node.parentIds || [],
151
+ classId: node.classRef?.id,
122
152
  };
123
153
  nodes.set(node.id, normalized);
124
154
  if (node.shortId) {
@@ -130,7 +160,14 @@ export const clearData = (
130
160
  return acc;
131
161
  }, {});
132
162
 
163
+ if (!taxonomyNodesByFacet.categories && taxonomyNodesByFacet.informationSubjects) {
164
+ taxonomyNodesByFacet.categories = taxonomyNodesByFacet.informationSubjects;
165
+ }
166
+
133
167
  for (const [key, value] of Object.entries(tags)) {
168
+ if (isMetadataPropertyRuntimeSuppressed(key as InformationUnitPropertyKey)) {
169
+ continue;
170
+ }
134
171
  if (!value || !value.items || value.items.length === 0) {
135
172
  continue;
136
173
  }
@@ -139,13 +176,16 @@ export const clearData = (
139
176
  if (item?.shortId === undefined) return null
140
177
  if (!includeZeroHits && Number(item.hits) === 0) return null
141
178
 
142
- const override = options?.labelOverrides?.[key]?.[item.shortId];
143
179
  const runtimeItem = item as typeof item & {
144
180
  id?: string;
145
181
  parents?: Array<{ id?: string; shortId?: string }>;
146
182
  };
183
+ const presentationItem = facetPresentation?.[key]?.[item.shortId]
184
+ || (runtimeItem.id ? facetPresentation?.[key]?.[runtimeItem.id] : undefined);
147
185
  const taxonomyNode = taxonomyNodesByFacet[key]?.get(runtimeItem.id || item.shortId);
148
- const label = override?.label || taxonomyNode?.label || resolveLabelByLanguage(item.labels || [], options?.uiLanguage);
186
+ if (presentationItem?.hidden) return null;
187
+ if (isMetadataGroupDenied(key as InformationUnitPropertyKey, { classId: taxonomyNode?.classId })) return null;
188
+ const label = presentationItem?.label || taxonomyNode?.label || resolveLabelByLanguage(item.labels || [], options?.uiLanguage);
149
189
  if (!label) return null;
150
190
  const taxonomyId = taxonomyNode?.id || runtimeItem.id;
151
191
  const parentIds = taxonomyNode?.parentIds || (runtimeItem.parents || [])
@@ -158,8 +198,8 @@ export const clearData = (
158
198
  label,
159
199
  active: false,
160
200
  shortId: item.shortId,
161
- groupLabel: override?.groupLabel,
162
- sectionLabel: override?.sectionLabel,
201
+ groupLabel: presentationItem?.groupLabel,
202
+ sectionLabel: presentationItem?.sectionLabel,
163
203
  taxonomyId,
164
204
  parentIds,
165
205
  }
@@ -182,7 +222,8 @@ export const memoizeFilteredTags = (
182
222
  packages: string | null,
183
223
  options?: {
184
224
  uiLanguage?: string;
185
- labelOverrides?: FacetLabelOverrides;
225
+ facetPresentation?: FacetPresentation;
226
+ labelOverrides?: FacetPresentation;
186
227
  includeZeroHits?: boolean;
187
228
  facetTaxonomies?: FacetTaxonomies;
188
229
  }
@@ -247,9 +288,49 @@ export const applyFacetPropertyVisibility = (
247
288
  ]);
248
289
 
249
290
  return Object.entries(tags).reduce<Record<string, ResolvedFilterItem[]>>((acc, [key, value]) => {
291
+ if (isMetadataPropertyRuntimeSuppressed(key as InformationUnitPropertyKey)) return acc;
250
292
  if (includeSet.size > 0 && !includeSet.has(key)) return acc;
251
293
  if (excludeSet.has(key)) return acc;
252
294
  acc[key] = value;
253
295
  return acc;
254
296
  }, {});
255
297
  }
298
+
299
+ export const pruneHierarchyFacetItemsForMerge = (
300
+ facetKey: string,
301
+ items: ResolvedFilterItem[]
302
+ ): ResolvedFilterItem[] => {
303
+ const isCategoryFacet = facetKey === "categories" || facetKey === "informationSubjects";
304
+ if (!isCategoryFacet) return items;
305
+
306
+ const hasActiveSelection = items.some((item) => item.active);
307
+ if (!hasActiveSelection) return items;
308
+
309
+ return items.filter((item) => item.active || Number(item.hits) > 0 || Number(item.total) > 0);
310
+ };
311
+
312
+ export const resolveHierarchyFacetCountLabel = (
313
+ item: ResolvedFilterItem,
314
+ childItems: ResolvedFilterItem[]
315
+ ): string | undefined => {
316
+ if (item.isStructural && childItems.length > 0) {
317
+ return undefined;
318
+ }
319
+
320
+ return `(${item.hits}/${item.total})`;
321
+ };
322
+
323
+ export const shouldRenderFacetHierarchy = (
324
+ items: ResolvedFilterItem[],
325
+ options?: { hasTaxonomy?: boolean }
326
+ ): boolean => {
327
+ const hasTaxonomy = options?.hasTaxonomy ?? false;
328
+ const hasSectionLabels = items.some((item) => Boolean(item.sectionLabel));
329
+ const hasParentRelations = items.some((item) => (item.parentIds || []).length > 0);
330
+
331
+ if (hasTaxonomy) {
332
+ return hasParentRelations;
333
+ }
334
+
335
+ return !hasSectionLabels && hasParentRelations;
336
+ };
@@ -14,7 +14,7 @@ import { GenericTableResultList } from "./table-result-list";
14
14
  import { Pagination } from "../pagination";
15
15
  import { Skeleton } from "@c-rex/ui/skeleton";
16
16
  import type { CommonItemsModel, ResultContainerModel, Tags } from "@c-rex/interfaces";
17
- import type { FacetLabelOverrides } from "../filter-sidebar/utils";
17
+ import type { FacetPresentation } from "../filter-sidebar/utils";
18
18
 
19
19
  type SerializedError = { message: string; name: string } | null;
20
20
 
@@ -41,7 +41,8 @@ export type GenericSearchResultsClientProps<
41
41
  linkPattern: string;
42
42
  addPackageId: boolean;
43
43
  showFilesColumn?: boolean;
44
- facetLabelOverrides?: FacetLabelOverrides;
44
+ facetPresentation?: FacetPresentation;
45
+ facetLabelOverrides?: FacetPresentation;
45
46
  informationSubjectsTaxonomy?: NonNullable<FilterSidebarProps["facetTaxonomies"]>["informationSubjects"];
46
47
  facetExcludeProperties?: string[];
47
48
  };
@@ -83,7 +84,8 @@ type SearchResultsBodyProps<TItem extends CommonItemsModel> = {
83
84
  data: ResultContainerModel<TItem> | null;
84
85
  error: SerializedError;
85
86
  isLoading: boolean;
86
- facetLabelOverrides?: FacetLabelOverrides;
87
+ facetPresentation?: FacetPresentation;
88
+ facetLabelOverrides?: FacetPresentation;
87
89
  informationSubjectsTaxonomy?: NonNullable<FilterSidebarProps["facetTaxonomies"]>["informationSubjects"];
88
90
  facetExcludeProperties: string[];
89
91
  search: string;
@@ -96,6 +98,7 @@ const SearchResultsBody = <TItem extends CommonItemsModel>({
96
98
  data,
97
99
  error,
98
100
  isLoading,
101
+ facetPresentation,
99
102
  facetLabelOverrides,
100
103
  informationSubjectsTaxonomy,
101
104
  facetExcludeProperties,
@@ -105,6 +108,7 @@ const SearchResultsBody = <TItem extends CommonItemsModel>({
105
108
  showFilesColumn,
106
109
  }: SearchResultsBodyProps<TItem>) => {
107
110
  const [lastFacetTags, setLastFacetTags] = useState<Tags | undefined>(undefined);
111
+ const effectiveFacetPresentation = facetPresentation || facetLabelOverrides;
108
112
 
109
113
  useEffect(() => {
110
114
  if (hasFacetTags(data?.tags)) {
@@ -129,7 +133,7 @@ const SearchResultsBody = <TItem extends CommonItemsModel>({
129
133
  <div className="flex flex-col gap-4 pb-4">
130
134
  <FilterNavbar
131
135
  tags={effectiveTags}
132
- facetLabelOverrides={facetLabelOverrides}
136
+ facetPresentation={effectiveFacetPresentation}
133
137
  excludeProperties={facetExcludeProperties}
134
138
  />
135
139
  <Empty />
@@ -142,7 +146,7 @@ const SearchResultsBody = <TItem extends CommonItemsModel>({
142
146
  <div className="flex flex-col gap-4 pb-4">
143
147
  <FilterNavbar
144
148
  tags={effectiveTags}
145
- facetLabelOverrides={facetLabelOverrides}
149
+ facetPresentation={effectiveFacetPresentation}
146
150
  excludeProperties={facetExcludeProperties}
147
151
  />
148
152
 
@@ -150,7 +154,7 @@ const SearchResultsBody = <TItem extends CommonItemsModel>({
150
154
  <FilterSidebar
151
155
  tags={effectiveTags}
152
156
  totalItemCount={data.pageInfo.totalItemCount}
153
- facetLabelOverrides={facetLabelOverrides}
157
+ facetPresentation={effectiveFacetPresentation}
154
158
  facetTaxonomies={informationSubjectsTaxonomy ? { informationSubjects: informationSubjectsTaxonomy } : undefined}
155
159
  excludeProperties={facetExcludeProperties}
156
160
  />
@@ -167,7 +171,7 @@ const SearchResultsBody = <TItem extends CommonItemsModel>({
167
171
  <div className="flex flex-col gap-4 pb-4">
168
172
  <FilterNavbar
169
173
  tags={effectiveTags}
170
- facetLabelOverrides={facetLabelOverrides}
174
+ facetPresentation={effectiveFacetPresentation}
171
175
  excludeProperties={facetExcludeProperties}
172
176
  />
173
177
 
@@ -175,7 +179,7 @@ const SearchResultsBody = <TItem extends CommonItemsModel>({
175
179
  <FilterSidebar
176
180
  tags={effectiveTags}
177
181
  totalItemCount={data.pageInfo.totalItemCount}
178
- facetLabelOverrides={facetLabelOverrides}
182
+ facetPresentation={effectiveFacetPresentation}
179
183
  facetTaxonomies={informationSubjectsTaxonomy ? { informationSubjects: informationSubjectsTaxonomy } : undefined}
180
184
  excludeProperties={facetExcludeProperties}
181
185
  />
@@ -211,6 +215,7 @@ export const GenericSearchResultsClient = <
211
215
  linkPattern,
212
216
  addPackageId,
213
217
  showFilesColumn = false,
218
+ facetPresentation,
214
219
  facetLabelOverrides,
215
220
  informationSubjectsTaxonomy,
216
221
  facetExcludeProperties = [],
@@ -244,6 +249,7 @@ export const GenericSearchResultsClient = <
244
249
  data={data}
245
250
  error={error}
246
251
  isLoading={isLoading}
252
+ facetPresentation={facetPresentation}
247
253
  facetLabelOverrides={facetLabelOverrides}
248
254
  informationSubjectsTaxonomy={informationSubjectsTaxonomy}
249
255
  facetExcludeProperties={facetExcludeProperties}