@c-rex/components 0.3.0-build.39 → 0.3.0-build.41
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/favorites/favorites-context.tsx +4 -0
- 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/results/information-unit-search-results-cards.tsx +1 -1
- package/src/taxonomy/__tests__/hierarchy.test.ts +144 -0
- package/src/taxonomy/hierarchy.ts +137 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import { DomainEntityModel } from "@c-rex/interfaces";
|
|
4
|
+
import type { TaxonomyResult } from "@c-rex/services/read-models";
|
|
4
5
|
import { FC, ReactNode, useMemo, useState } from "react";
|
|
5
6
|
import * as ComponentOptions from "../generated/client-components";
|
|
6
7
|
import { Skeleton } from "@c-rex/ui/skeleton";
|
|
@@ -34,6 +35,7 @@ export type TaxonomyRestrictionMenuProps = {
|
|
|
34
35
|
fetchMode?: RestrictionMenuFetchMode;
|
|
35
36
|
showAllWhenEmpty?: boolean;
|
|
36
37
|
queryParams?: GenericQueryParams;
|
|
38
|
+
hierarchyTaxonomy?: TaxonomyResult;
|
|
37
39
|
multipleSelection?: boolean;
|
|
38
40
|
updatePosition?: boolean;
|
|
39
41
|
};
|
|
@@ -54,6 +56,7 @@ export const TaxonomyRestrictionMenu: FC<TaxonomyRestrictionMenuProps> = ({
|
|
|
54
56
|
fetchMode = "deferred",
|
|
55
57
|
showAllWhenEmpty = true,
|
|
56
58
|
navigationMenuListClassName = "items-center justify-start gap-4 flex-row",
|
|
59
|
+
hierarchyTaxonomy,
|
|
57
60
|
multipleSelection = true,
|
|
58
61
|
updatePosition = true,
|
|
59
62
|
}) => {
|
|
@@ -108,6 +111,7 @@ export const TaxonomyRestrictionMenu: FC<TaxonomyRestrictionMenuProps> = ({
|
|
|
108
111
|
restrictField={restrictField}
|
|
109
112
|
items={data.items || []}
|
|
110
113
|
enableHierarchy={enableHierarchy}
|
|
114
|
+
hierarchyTaxonomy={hierarchyTaxonomy}
|
|
111
115
|
hasMoreItems={hasMoreItems}
|
|
112
116
|
showAllWhenEmpty={showAllWhenEmpty}
|
|
113
117
|
itemsByRow={itemsByRow}
|
|
@@ -9,7 +9,7 @@ import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@c-rex
|
|
|
9
9
|
import { DEVICE_OPTIONS, OPERATOR_OPTIONS } from "@c-rex/constants";
|
|
10
10
|
import { Tags } from "@c-rex/interfaces";
|
|
11
11
|
import { useBreakpoint } from "@c-rex/ui/hooks";
|
|
12
|
-
import { applyFacetPropertyVisibility,
|
|
12
|
+
import { applyFacetPropertyVisibility, FacetPresentation, memoizeFilteredTags } from "./filter-sidebar/utils";
|
|
13
13
|
import { useFilterSidebarState } from "./filter-sidebar/context";
|
|
14
14
|
import { useSearchSettingsStore } from "../stores/search-settings-store";
|
|
15
15
|
import { useRestrictionStore } from "../stores/restriction-store";
|
|
@@ -25,7 +25,8 @@ type filterModel = {
|
|
|
25
25
|
|
|
26
26
|
type FilterNavbarProps = {
|
|
27
27
|
tags?: Tags
|
|
28
|
-
|
|
28
|
+
facetPresentation?: FacetPresentation
|
|
29
|
+
facetLabelOverrides?: FacetPresentation
|
|
29
30
|
includeZeroHits?: boolean
|
|
30
31
|
includeProperties?: string[]
|
|
31
32
|
excludeProperties?: string[]
|
|
@@ -33,6 +34,7 @@ type FilterNavbarProps = {
|
|
|
33
34
|
|
|
34
35
|
export const FilterNavbar: FC<FilterNavbarProps> = ({
|
|
35
36
|
tags,
|
|
37
|
+
facetPresentation,
|
|
36
38
|
facetLabelOverrides,
|
|
37
39
|
includeZeroHits = true,
|
|
38
40
|
includeProperties,
|
|
@@ -45,6 +47,7 @@ export const FilterNavbar: FC<FilterNavbarProps> = ({
|
|
|
45
47
|
const { setIsMobileFiltersOpen } = useFilterSidebarState();
|
|
46
48
|
const restrictionList = useRestrictionStore((state) => state.restrictionList);
|
|
47
49
|
const startSearchNavigation = useSearchNavigationStore((state) => state.start);
|
|
50
|
+
const effectiveFacetPresentation = facetPresentation || facetLabelOverrides;
|
|
48
51
|
const [params, setParams] = useQueryStates({
|
|
49
52
|
language: parseAsString.withDefault(useSearchSettingsStore.getState().language || ''),
|
|
50
53
|
page: parseAsInteger.withDefault(1),
|
|
@@ -68,11 +71,11 @@ export const FilterNavbar: FC<FilterNavbarProps> = ({
|
|
|
68
71
|
const filteredTags = useMemo(() => {
|
|
69
72
|
const resolved = memoizeFilteredTags(tags, params.filter, params.packages, {
|
|
70
73
|
uiLanguage: locale,
|
|
71
|
-
|
|
74
|
+
facetPresentation: effectiveFacetPresentation,
|
|
72
75
|
includeZeroHits,
|
|
73
76
|
});
|
|
74
77
|
return applyFacetPropertyVisibility(resolved, includeProperties, excludeProperties);
|
|
75
|
-
}, [tags, params.filter, params.packages, locale,
|
|
78
|
+
}, [tags, params.filter, params.packages, locale, effectiveFacetPresentation, includeZeroHits, includeProperties, excludeProperties]);
|
|
76
79
|
const hasMobileFilters = Object.entries(filteredTags).length > 0;
|
|
77
80
|
|
|
78
81
|
const humanizeMetadataKey = (key: string): string =>
|
|
@@ -2,7 +2,11 @@ import { Tags } from "@c-rex/interfaces";
|
|
|
2
2
|
import {
|
|
3
3
|
clearData,
|
|
4
4
|
memoizeFilteredTags,
|
|
5
|
+
pruneHierarchyFacetItemsForMerge,
|
|
6
|
+
resolveHierarchyFacetCountLabel,
|
|
7
|
+
resolveFacetTaxonomy,
|
|
5
8
|
resolveLabelByLanguage,
|
|
9
|
+
shouldRenderFacetHierarchy,
|
|
6
10
|
updateFilterParam,
|
|
7
11
|
removeFilterItem,
|
|
8
12
|
} from "../utils";
|
|
@@ -63,7 +67,7 @@ describe("filter-sidebar utils", () => {
|
|
|
63
67
|
const result = clearData(tags, { uiLanguage: "en" });
|
|
64
68
|
|
|
65
69
|
expect(result.informationSubjects).toHaveLength(2);
|
|
66
|
-
expect(result.informationSubjects?.map((item) => item.shortId)).toEqual(["
|
|
70
|
+
expect(result.informationSubjects?.map((item) => item.shortId)).toEqual(["subB", "subA"]);
|
|
67
71
|
});
|
|
68
72
|
|
|
69
73
|
it("clearData excludes zero-hit entries when includeZeroHits is false", () => {
|
|
@@ -89,7 +93,8 @@ describe("filter-sidebar utils", () => {
|
|
|
89
93
|
},
|
|
90
94
|
});
|
|
91
95
|
|
|
92
|
-
|
|
96
|
+
const overriddenItem = result.informationSubjects?.find((item) => item.shortId === "subA");
|
|
97
|
+
expect(overriddenItem).toMatchObject({
|
|
93
98
|
label: "Overridden Label",
|
|
94
99
|
groupLabel: "Subject Group",
|
|
95
100
|
sectionLabel: "Process",
|
|
@@ -97,6 +102,168 @@ describe("filter-sidebar utils", () => {
|
|
|
97
102
|
});
|
|
98
103
|
});
|
|
99
104
|
|
|
105
|
+
it("clearData resolves facet presentation by id when shortId lookup is unavailable", () => {
|
|
106
|
+
const tags = createTags();
|
|
107
|
+
const result = clearData(tags, {
|
|
108
|
+
uiLanguage: "en",
|
|
109
|
+
labelOverrides: {
|
|
110
|
+
informationSubjects: {
|
|
111
|
+
"https://example.org/subA": {
|
|
112
|
+
label: "ID Matched Label",
|
|
113
|
+
sectionLabel: "Process",
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
expect(result.informationSubjects?.find((item) => item.shortId === "subA")).toMatchObject({
|
|
120
|
+
label: "ID Matched Label",
|
|
121
|
+
sectionLabel: "Process",
|
|
122
|
+
taxonomyId: "https://example.org/subA",
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("clearData keeps party role section labels from overrides instead of raw predicate labels", () => {
|
|
127
|
+
const tags = {
|
|
128
|
+
parties: {
|
|
129
|
+
labels: ["related to party"],
|
|
130
|
+
items: [
|
|
131
|
+
{
|
|
132
|
+
hits: 1,
|
|
133
|
+
total: 1,
|
|
134
|
+
labels: [{ language: "de", value: "Deutsche Gesetzliche Unfallversicherung e.V." }],
|
|
135
|
+
shortId: "dguv",
|
|
136
|
+
id: "https://example.org/party/dguv",
|
|
137
|
+
} as unknown as Tags[string]["items"][number],
|
|
138
|
+
],
|
|
139
|
+
},
|
|
140
|
+
} as unknown as Tags;
|
|
141
|
+
|
|
142
|
+
const result = clearData(tags, {
|
|
143
|
+
uiLanguage: "de-DE",
|
|
144
|
+
labelOverrides: {
|
|
145
|
+
parties: {
|
|
146
|
+
dguv: {
|
|
147
|
+
label: "Deutsche Gesetzliche Unfallversicherung e.V.",
|
|
148
|
+
sectionLabel: "Traeger",
|
|
149
|
+
},
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
expect(result.parties?.[0]).toMatchObject({
|
|
155
|
+
label: "Deutsche Gesetzliche Unfallversicherung e.V.",
|
|
156
|
+
sectionLabel: "Traeger",
|
|
157
|
+
});
|
|
158
|
+
expect(result.parties?.[0]?.sectionLabel).not.toBe("related to party");
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it("resolves categories taxonomy through the cached informationSubjects hierarchy alias", () => {
|
|
162
|
+
const taxonomy = {
|
|
163
|
+
generatedAt: "2026-04-10T00:00:00.000Z",
|
|
164
|
+
roots: [],
|
|
165
|
+
nodesById: {
|
|
166
|
+
"category-root": {
|
|
167
|
+
id: "category-root",
|
|
168
|
+
shortId: "catRoot",
|
|
169
|
+
label: "Root category",
|
|
170
|
+
parentIds: [],
|
|
171
|
+
},
|
|
172
|
+
},
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
expect(resolveFacetTaxonomy("categories", { informationSubjects: taxonomy })).toBe(taxonomy);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it("clearData merges category tags into the cached category hierarchy alias", () => {
|
|
179
|
+
const tags = {
|
|
180
|
+
categories: {
|
|
181
|
+
labels: ["Categories"],
|
|
182
|
+
items: [
|
|
183
|
+
{
|
|
184
|
+
hits: 2,
|
|
185
|
+
total: 2,
|
|
186
|
+
labels: [{ language: "de", value: "Unterkategorie" }],
|
|
187
|
+
shortId: "catChild",
|
|
188
|
+
id: "category-child",
|
|
189
|
+
} as unknown as Tags[string]["items"][number],
|
|
190
|
+
],
|
|
191
|
+
},
|
|
192
|
+
} as unknown as Tags;
|
|
193
|
+
|
|
194
|
+
const result = clearData(tags, {
|
|
195
|
+
uiLanguage: "de-DE",
|
|
196
|
+
facetTaxonomies: {
|
|
197
|
+
informationSubjects: {
|
|
198
|
+
generatedAt: "2026-04-10T00:00:00.000Z",
|
|
199
|
+
roots: [],
|
|
200
|
+
nodesById: {
|
|
201
|
+
"category-root": {
|
|
202
|
+
id: "category-root",
|
|
203
|
+
shortId: "catRoot",
|
|
204
|
+
label: "Wurzel",
|
|
205
|
+
parentIds: [],
|
|
206
|
+
},
|
|
207
|
+
"category-child": {
|
|
208
|
+
id: "category-child",
|
|
209
|
+
shortId: "catChild",
|
|
210
|
+
label: "Unterkategorie",
|
|
211
|
+
parentIds: ["category-root"],
|
|
212
|
+
},
|
|
213
|
+
},
|
|
214
|
+
},
|
|
215
|
+
},
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
expect(result.categories?.[0]).toMatchObject({
|
|
219
|
+
shortId: "catChild",
|
|
220
|
+
label: "Unterkategorie",
|
|
221
|
+
taxonomyId: "category-child",
|
|
222
|
+
parentIds: ["category-root"],
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it("clearData hides denied qualification groups from semantic facet output", () => {
|
|
227
|
+
const tags = {
|
|
228
|
+
qualifications: {
|
|
229
|
+
labels: ["Qualifications"],
|
|
230
|
+
items: [
|
|
231
|
+
{
|
|
232
|
+
hits: 4,
|
|
233
|
+
total: 4,
|
|
234
|
+
labels: [{ language: "en", value: "License A" }],
|
|
235
|
+
shortId: "qualA",
|
|
236
|
+
id: "qualification-a",
|
|
237
|
+
} as unknown as Tags[string]["items"][number],
|
|
238
|
+
],
|
|
239
|
+
},
|
|
240
|
+
} as unknown as Tags;
|
|
241
|
+
|
|
242
|
+
const result = clearData(tags, {
|
|
243
|
+
uiLanguage: "en-US",
|
|
244
|
+
facetTaxonomies: {
|
|
245
|
+
qualifications: {
|
|
246
|
+
generatedAt: "2026-04-10T00:00:00.000Z",
|
|
247
|
+
roots: [],
|
|
248
|
+
nodesById: {
|
|
249
|
+
"qualification-a": {
|
|
250
|
+
id: "qualification-a",
|
|
251
|
+
shortId: "qualA",
|
|
252
|
+
label: "License A",
|
|
253
|
+
parentIds: [],
|
|
254
|
+
classRef: {
|
|
255
|
+
id: "https://ids.c-crex.net/ns/iirds/ext#ContentLicense",
|
|
256
|
+
label: "Content License",
|
|
257
|
+
},
|
|
258
|
+
},
|
|
259
|
+
},
|
|
260
|
+
},
|
|
261
|
+
},
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
expect(result.qualifications).toBeUndefined();
|
|
265
|
+
});
|
|
266
|
+
|
|
100
267
|
it("memoizeFilteredTags marks active filters and active package", () => {
|
|
101
268
|
const tags = createTags();
|
|
102
269
|
const result = memoizeFilteredTags(tags, "informationSubjects.shortId=subA", "pkgA", {
|
|
@@ -108,6 +275,136 @@ describe("filter-sidebar utils", () => {
|
|
|
108
275
|
expect(result.packages?.find((item) => item.shortId === "pkgA")?.active).toBe(true);
|
|
109
276
|
});
|
|
110
277
|
|
|
278
|
+
it("prunes zero-hit category siblings from the hierarchy merge seed when a category is selected", () => {
|
|
279
|
+
const items = [
|
|
280
|
+
{
|
|
281
|
+
shortId: "selected",
|
|
282
|
+
taxonomyId: "selected-id",
|
|
283
|
+
label: "Selected",
|
|
284
|
+
active: true,
|
|
285
|
+
hits: 211,
|
|
286
|
+
total: 211,
|
|
287
|
+
parentIds: ["parent-id"],
|
|
288
|
+
},
|
|
289
|
+
{
|
|
290
|
+
shortId: "sibling-zero",
|
|
291
|
+
taxonomyId: "sibling-zero-id",
|
|
292
|
+
label: "Sibling Zero",
|
|
293
|
+
active: false,
|
|
294
|
+
hits: 0,
|
|
295
|
+
total: 0,
|
|
296
|
+
parentIds: ["parent-id"],
|
|
297
|
+
},
|
|
298
|
+
{
|
|
299
|
+
shortId: "sibling-live",
|
|
300
|
+
taxonomyId: "sibling-live-id",
|
|
301
|
+
label: "Sibling Live",
|
|
302
|
+
active: false,
|
|
303
|
+
hits: 5,
|
|
304
|
+
total: 5,
|
|
305
|
+
parentIds: ["parent-id"],
|
|
306
|
+
},
|
|
307
|
+
];
|
|
308
|
+
|
|
309
|
+
expect(pruneHierarchyFacetItemsForMerge("categories", [...items])).toEqual([items[0], items[2]]);
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
it("keeps zero-hit category items untouched when no category is selected", () => {
|
|
313
|
+
const items = [
|
|
314
|
+
{
|
|
315
|
+
shortId: "category-a",
|
|
316
|
+
taxonomyId: "category-a-id",
|
|
317
|
+
label: "Category A",
|
|
318
|
+
active: false,
|
|
319
|
+
hits: 0,
|
|
320
|
+
total: 0,
|
|
321
|
+
parentIds: [],
|
|
322
|
+
},
|
|
323
|
+
{
|
|
324
|
+
shortId: "category-b",
|
|
325
|
+
taxonomyId: "category-b-id",
|
|
326
|
+
label: "Category B",
|
|
327
|
+
active: false,
|
|
328
|
+
hits: 3,
|
|
329
|
+
total: 3,
|
|
330
|
+
parentIds: [],
|
|
331
|
+
},
|
|
332
|
+
];
|
|
333
|
+
|
|
334
|
+
expect(pruneHierarchyFacetItemsForMerge("informationSubjects", items)).toEqual(items);
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
it("hides the misleading 0/0 count for structural category parents with visible children", () => {
|
|
338
|
+
const item = {
|
|
339
|
+
shortId: "category-parent",
|
|
340
|
+
taxonomyId: "category-parent-id",
|
|
341
|
+
label: "Category Parent",
|
|
342
|
+
active: false,
|
|
343
|
+
hits: 0,
|
|
344
|
+
total: 0,
|
|
345
|
+
parentIds: [],
|
|
346
|
+
isStructural: true,
|
|
347
|
+
};
|
|
348
|
+
const child = {
|
|
349
|
+
shortId: "category-child",
|
|
350
|
+
taxonomyId: "category-child-id",
|
|
351
|
+
label: "Category Child",
|
|
352
|
+
active: false,
|
|
353
|
+
hits: 4,
|
|
354
|
+
total: 4,
|
|
355
|
+
parentIds: ["category-parent-id"],
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
expect(resolveHierarchyFacetCountLabel(item, [child])).toBeUndefined();
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
it("keeps count labels for live hierarchy nodes", () => {
|
|
362
|
+
const item = {
|
|
363
|
+
shortId: "category-child",
|
|
364
|
+
taxonomyId: "category-child-id",
|
|
365
|
+
label: "Category Child",
|
|
366
|
+
active: false,
|
|
367
|
+
hits: 4,
|
|
368
|
+
total: 7,
|
|
369
|
+
parentIds: ["category-parent-id"],
|
|
370
|
+
};
|
|
371
|
+
|
|
372
|
+
expect(resolveHierarchyFacetCountLabel(item, [])).toBe("(4/7)");
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
it("renders taxonomy-backed facets as flat groups when no parent relations exist", () => {
|
|
376
|
+
const items = [
|
|
377
|
+
{
|
|
378
|
+
shortId: "subject-a",
|
|
379
|
+
taxonomyId: "subject-a-id",
|
|
380
|
+
label: "Abkuerzungsbeschreibung",
|
|
381
|
+
sectionLabel: "Formalitaet",
|
|
382
|
+
active: false,
|
|
383
|
+
hits: 3,
|
|
384
|
+
total: 3,
|
|
385
|
+
parentIds: [],
|
|
386
|
+
},
|
|
387
|
+
];
|
|
388
|
+
|
|
389
|
+
expect(shouldRenderFacetHierarchy(items, { hasTaxonomy: true })).toBe(false);
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
it("renders taxonomy-backed facets as hierarchy when parent relations exist", () => {
|
|
393
|
+
const items = [
|
|
394
|
+
{
|
|
395
|
+
shortId: "category-child",
|
|
396
|
+
taxonomyId: "category-child-id",
|
|
397
|
+
label: "Unterkategorie",
|
|
398
|
+
active: false,
|
|
399
|
+
hits: 4,
|
|
400
|
+
total: 4,
|
|
401
|
+
parentIds: ["category-root-id"],
|
|
402
|
+
},
|
|
403
|
+
];
|
|
404
|
+
|
|
405
|
+
expect(shouldRenderFacetHierarchy(items, { hasTaxonomy: true })).toBe(true);
|
|
406
|
+
});
|
|
407
|
+
|
|
111
408
|
it("updateFilterParam and removeFilterItem keep query behavior stable", () => {
|
|
112
409
|
expect(updateFilterParam("packages", { shortId: "pkgA", hits: 0, total: 0, label: "", active: false }, null)).toEqual({
|
|
113
410
|
packages: "pkgA",
|
|
@@ -126,4 +423,3 @@ describe("filter-sidebar utils", () => {
|
|
|
126
423
|
).toEqual({ filter: "packages.shortId=pkgA" });
|
|
127
424
|
});
|
|
128
425
|
});
|
|
129
|
-
|