@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.
@@ -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, FacetLabelOverrides, memoizeFilteredTags } from "./filter-sidebar/utils";
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
- facetLabelOverrides?: FacetLabelOverrides
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
- labelOverrides: facetLabelOverrides,
74
+ facetPresentation: effectiveFacetPresentation,
72
75
  includeZeroHits,
73
76
  });
74
77
  return applyFacetPropertyVisibility(resolved, includeProperties, excludeProperties);
75
- }, [tags, params.filter, params.packages, locale, facetLabelOverrides, includeZeroHits, includeProperties, excludeProperties]);
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(["subA", "subB"]);
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
- expect(result.informationSubjects?.[0]).toMatchObject({
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
-