@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.
- package/package.json +1 -1
- package/src/info/information-unit-metadata-grid-client.tsx +9 -211
- package/src/restriction-menu/__tests__/restriction-hierarchy.test.ts +12 -0
- package/src/restriction-menu/restriction-hierarchy.ts +7 -0
- package/src/restriction-menu/restriction-selection-command-menu.tsx +76 -11
- package/src/restriction-menu/restriction-selection-menu.tsx +80 -15
- package/src/restriction-menu/taxonomy-restriction-command-menu.tsx +6 -0
- package/src/restriction-menu/taxonomy-restriction-menu.tsx +4 -0
- package/src/results/filter-navbar.tsx +7 -4
- package/src/results/filter-sidebar/__tests__/utils.test.ts +299 -3
- package/src/results/filter-sidebar/index.tsx +77 -108
- package/src/results/filter-sidebar/utils.ts +91 -10
- package/src/results/generic/search-results-client.tsx +14 -8
- package/src/taxonomy/__tests__/hierarchy.test.ts +144 -0
- package/src/taxonomy/hierarchy.ts +137 -0
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
|
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 =
|
|
157
|
+
const taxonomy = resolveFacetTaxonomy(propertyKey, facetTaxonomies) as TaxonomyResult | undefined;
|
|
142
158
|
if (taxonomy) {
|
|
143
|
-
const
|
|
144
|
-
|
|
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
|
-
|
|
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
|
|
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=
|
|
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=
|
|
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}
|
|
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
|
|
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
|
|
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 ||
|
|
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 ||
|
|
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
|
|
4
|
-
|
|
5
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
162
|
-
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
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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}
|