@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@c-rex/components",
3
- "version": "0.3.0-build.39",
3
+ "version": "0.3.0-build.41",
4
4
  "files": [
5
5
  "src"
6
6
  ],
@@ -264,6 +264,10 @@ const upsertFavorite = (favorites: Favorite[], entry: Favorite): Favorite[] => {
264
264
  }
265
265
 
266
266
  const current = favorites[index];
267
+ if (!current) {
268
+ return [...favorites, entry];
269
+ }
270
+
267
271
  const next = {
268
272
  ...current,
269
273
  label: current.label || entry.label,
@@ -12,32 +12,18 @@ import { InformationUnitsGetAllClient } from "../generated/client-components";
12
12
  import type {
13
13
  CommonItemsModel,
14
14
  InformationUnitModel,
15
- LiteralModel,
16
- ObjectRefModel,
17
- RenditionModel,
18
15
  } from "@c-rex/interfaces";
16
+ import type { AvailableLanguageVersion } from "@c-rex/services/read-models";
19
17
  import {
20
- INFORMATION_UNIT_PROPERTY_PRESENTATION,
21
- type InformationUnitPropertyKey,
22
- } from "@c-rex/services/metadata-presentation-config";
23
- import type { AvailableLanguageVersion, MetadataFacetLabelOverrides } from "@c-rex/services/read-models";
18
+ buildMetadataDisplayRows,
19
+ type MetadataFacetLabelOverrides,
20
+ } from "@c-rex/services/metadata-display-builder";
24
21
  import { resolveMetadataDisplayProperties } from "@c-rex/services/metadata-view-profile";
25
22
  import {
26
23
  extractCountryCodeFromLanguage,
27
- getFileRenditionGroups,
28
24
  resolvePreferredLanguage,
29
- sortAndDeduplicateLanguages,
30
25
  } from "@c-rex/utils";
31
26
 
32
- type MetadataDisplayRow = {
33
- key: InformationUnitPropertyKey;
34
- label: string;
35
- labelSource: "translationKey" | "direct";
36
- values: string[];
37
- valueType?: "text" | "language" | "rendition";
38
- renditions?: RenditionModel[];
39
- };
40
-
41
27
  type Props = {
42
28
  title: string;
43
29
  linkPattern: string;
@@ -51,198 +37,6 @@ type Props = {
51
37
  availableInVersions?: AvailableLanguageVersion[];
52
38
  };
53
39
 
54
- const resolveLiteralLabel = (labels?: LiteralModel[] | null, uiLanguage?: string): string | undefined => {
55
- if (!labels || labels.length === 0) return undefined;
56
- const language = uiLanguage || "en";
57
- const preferred = resolvePreferredLanguage(
58
- labels.map((item) => item.language || ""),
59
- language
60
- );
61
- if (preferred) {
62
- const exact = labels.find((item) => item.language === preferred)?.value;
63
- if (exact) return exact;
64
- }
65
- return labels.find((item) => item.value)?.value || undefined;
66
- };
67
-
68
- const resolveObjectRefLabel = (item: ObjectRefModel, uiLanguage: string): string | undefined => {
69
- return resolveLiteralLabel(item.labels || [], uiLanguage) || item.shortId || item.id || undefined;
70
- };
71
-
72
- const resolveObjectRefClassLabel = (item: ObjectRefModel, uiLanguage: string): string | undefined => {
73
- return resolveLiteralLabel(item.class?.labels || [], uiLanguage)
74
- || item.class?.shortId
75
- || item.class?.id
76
- || undefined;
77
- };
78
-
79
- const asObjectRefArray = (value: unknown): ObjectRefModel[] => {
80
- if (!Array.isArray(value)) return [];
81
- return value.filter((item): item is ObjectRefModel => Boolean(item && typeof item === "object"));
82
- };
83
-
84
- const asLiteralArray = (value: unknown): LiteralModel[] => {
85
- if (!Array.isArray(value)) return [];
86
- return value.filter((item): item is LiteralModel => Boolean(item && typeof item === "object"));
87
- };
88
-
89
- const buildMetadataDisplayRows = (
90
- data: CommonItemsModel,
91
- uiLanguage: string,
92
- includeProperties: InformationUnitPropertyKey[],
93
- overrides?: MetadataFacetLabelOverrides
94
- ): MetadataDisplayRow[] => {
95
- const preferredTitle = resolveLiteralLabel(data.titles || [], uiLanguage) || resolveLiteralLabel(data.labels || [], uiLanguage);
96
- const rows: MetadataDisplayRow[] = [];
97
-
98
- for (const key of includeProperties) {
99
- const config = INFORMATION_UNIT_PROPERTY_PRESENTATION[key];
100
- if (!config?.metadataDisplay?.supported) continue;
101
-
102
- if (key === "labels") continue;
103
-
104
- if (key === "titles") {
105
- if (preferredTitle) {
106
- rows.push({
107
- key,
108
- label: key,
109
- labelSource: "translationKey",
110
- values: [preferredTitle],
111
- valueType: "text",
112
- });
113
- }
114
- continue;
115
- }
116
-
117
- const value = (data as InformationUnitModel)[key];
118
- if (value == null) continue;
119
-
120
- if (key === "languages") {
121
- const languages = sortAndDeduplicateLanguages((value as string[]) || []);
122
- if (languages.length > 0) {
123
- rows.push({
124
- key,
125
- label: key,
126
- labelSource: "translationKey",
127
- values: languages,
128
- valueType: "language",
129
- });
130
- }
131
- continue;
132
- }
133
-
134
- if (config.valueKind === "scalar") {
135
- rows.push({
136
- key,
137
- label: key,
138
- labelSource: "translationKey",
139
- values: [String(value)],
140
- valueType: "text",
141
- });
142
- continue;
143
- }
144
-
145
- if (config.valueKind === "stringArray") {
146
- const values = Array.from(new Set((value as string[]).map((item) => String(item)).filter(Boolean)));
147
- if (values.length > 0) {
148
- rows.push({
149
- key,
150
- label: key,
151
- labelSource: "translationKey",
152
- values,
153
- valueType: "text",
154
- });
155
- }
156
- continue;
157
- }
158
-
159
- if (config.valueKind === "literalArray") {
160
- const preferred = resolveLiteralLabel(asLiteralArray(value), uiLanguage);
161
- if (preferred) {
162
- rows.push({
163
- key,
164
- label: key,
165
- labelSource: "translationKey",
166
- values: [preferred],
167
- valueType: "text",
168
- });
169
- }
170
- continue;
171
- }
172
-
173
- if (config.valueKind === "objectRef") {
174
- const label = resolveObjectRefLabel(value as ObjectRefModel, uiLanguage);
175
- if (label) {
176
- rows.push({
177
- key,
178
- label: key,
179
- labelSource: "translationKey",
180
- values: [label],
181
- valueType: "text",
182
- });
183
- }
184
- continue;
185
- }
186
-
187
- if (config.valueKind === "objectRefArray") {
188
- const refs = asObjectRefArray(value);
189
- if (refs.length === 0) continue;
190
-
191
- const groupedValues = new Map<string, Set<string>>();
192
-
193
- refs.forEach((ref) => {
194
- const shortId = ref.shortId || "";
195
- const override = shortId ? overrides?.[key]?.[shortId] : undefined;
196
- const valueLabel = override?.label || resolveObjectRefLabel(ref, uiLanguage);
197
- if (!valueLabel) return;
198
-
199
- const sectionLabel = config.metadataDisplay.sectionStrategy === "none"
200
- ? key
201
- : override?.sectionLabel || resolveObjectRefClassLabel(ref, uiLanguage) || key;
202
-
203
- const existing = groupedValues.get(sectionLabel) || new Set<string>();
204
- existing.add(valueLabel);
205
- groupedValues.set(sectionLabel, existing);
206
- });
207
-
208
- Array.from(groupedValues.entries())
209
- .sort(([left], [right]) => left.localeCompare(right))
210
- .forEach(([sectionLabel, valueSet]) => {
211
- const values = Array.from(valueSet).sort((a, b) => a.localeCompare(b));
212
- if (values.length === 0) return;
213
-
214
- rows.push({
215
- key,
216
- label: sectionLabel,
217
- labelSource: sectionLabel === key ? "translationKey" : "direct",
218
- values,
219
- valueType: "text",
220
- });
221
- });
222
- continue;
223
- }
224
-
225
- if (config.valueKind === "renditionArray") {
226
- const renditions = Array.isArray(value)
227
- ? value.filter((item): item is RenditionModel => Boolean(item && typeof item === "object"))
228
- : [];
229
- if (renditions.length === 0) continue;
230
- if (getFileRenditionGroups({ renditions }).length === 0) continue;
231
-
232
- rows.push({
233
- key,
234
- label: "files",
235
- labelSource: "translationKey",
236
- values: [],
237
- valueType: "rendition",
238
- renditions,
239
- });
240
- }
241
- }
242
-
243
- return rows;
244
- };
245
-
246
40
  const normalizeVersionItems = (items: CommonItemsModel[], uiLanguage: string) => {
247
41
  const uniqueByShortId = new Map<string, { shortId: string; language: string }>();
248
42
 
@@ -339,7 +133,11 @@ export const InformationUnitMetadataGridClient = ({
339
133
  : [...(metadataExcludeProperties || []), "renditions"],
340
134
  });
341
135
 
342
- return buildMetadataDisplayRows(data, locale, includeProperties, metadataLabelOverrides);
136
+ return buildMetadataDisplayRows(data as InformationUnitModel, {
137
+ uiLanguage: locale,
138
+ includeProperties,
139
+ overrides: metadataLabelOverrides,
140
+ });
343
141
  }, [data, locale, metadataExcludeProperties, metadataIncludeProperties, metadataLabelOverrides, showFileRenditions]);
344
142
 
345
143
  const cardContent = (
@@ -0,0 +1,12 @@
1
+ import { isRestrictionHierarchyNodeSelectable } from "../restriction-hierarchy";
2
+
3
+ describe("restriction hierarchy semantics", () => {
4
+ it("treats materialized structural taxonomy nodes as non-selectable", () => {
5
+ expect(isRestrictionHierarchyNodeSelectable({ isStructural: true })).toBe(false);
6
+ });
7
+
8
+ it("keeps live taxonomy nodes selectable", () => {
9
+ expect(isRestrictionHierarchyNodeSelectable({ isStructural: false })).toBe(true);
10
+ expect(isRestrictionHierarchyNodeSelectable({})).toBe(true);
11
+ });
12
+ });
@@ -0,0 +1,7 @@
1
+ export type RestrictionHierarchyNode = object & {
2
+ isStructural?: boolean;
3
+ };
4
+
5
+ export const isRestrictionHierarchyNodeSelectable = (
6
+ item: RestrictionHierarchyNode
7
+ ): boolean => !item.isStructural;
@@ -9,6 +9,7 @@ import {
9
9
  import { RestrictionNavigationItem } from "./restriction-menu-item";
10
10
  import { parseAsString, useQueryState, useQueryStates } from "nuqs";
11
11
  import { DomainEntityModel, ObjectRefModel } from "@c-rex/interfaces";
12
+ import type { TaxonomyResult } from "@c-rex/services/read-models";
12
13
  import { useLocale, useTranslations } from 'next-intl'
13
14
  import { cn, getLabelByLang } from "@c-rex/utils";
14
15
  import { useRestrictionStore } from "../stores/restriction-store";
@@ -20,12 +21,15 @@ import { Button } from "@c-rex/ui/button";
20
21
  import { Check, ChevronDown } from "lucide-react";
21
22
  import { useSearchNavigationStore } from "../stores/search-navigation-store";
22
23
  import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@c-rex/ui/tooltip";
24
+ import { buildTaxonomyHierarchy, type HierarchyItem } from "../taxonomy/hierarchy";
25
+ import { isRestrictionHierarchyNodeSelectable } from "./restriction-hierarchy";
23
26
 
24
27
  type Props = {
25
28
  restrictField: string
26
29
  navigationMenuListClassName?: string
27
30
  items: DomainEntityModel[],
28
31
  enableHierarchy?: boolean,
32
+ hierarchyTaxonomy?: TaxonomyResult,
29
33
  hasMoreItems?: boolean,
30
34
  showAllWhenEmpty?: boolean,
31
35
  onRequestMore?: () => void,
@@ -41,6 +45,10 @@ type RestrictionTreeNode = {
41
45
  children: RestrictionTreeNode[];
42
46
  };
43
47
 
48
+ type RestrictionHierarchyItem = HierarchyItem & {
49
+ item?: DomainEntityModel;
50
+ };
51
+
44
52
  const hasSelectedDescendant = (node: RestrictionTreeNode, selectedShortIds: Set<string>): boolean => {
45
53
  const shortId = node.item.shortId;
46
54
  if (shortId && selectedShortIds.has(shortId)) return true;
@@ -64,6 +72,13 @@ const extractParentKeys = (item: DomainEntityModel): string[] => {
64
72
  });
65
73
  };
66
74
 
75
+ const extractParentIds = (item: DomainEntityModel): string[] => {
76
+ const withParents = item as DomainEntityModel & { parents?: ObjectRefModel[] | null };
77
+ return (withParents.parents || [])
78
+ .map((parent) => parent.id || parent.shortId)
79
+ .filter((parentId): parentId is string => Boolean(parentId));
80
+ };
81
+
67
82
  const buildRestrictionTree = (items: DomainEntityModel[]): RestrictionTreeNode[] => {
68
83
  const nodes = new Map<string, RestrictionTreeNode>();
69
84
  items.forEach((item) => {
@@ -217,18 +232,25 @@ const RestrictionCommandDialog: FC<RestrictionCommandDialogProps> = ({
217
232
  const groupShortId = node.item.shortId ?? "";
218
233
  const groupLabel = fmt(getLabelByLang(node.item.labels, lang) ?? groupShortId);
219
234
  const groupIsSelected = selectedRestrictionIds.has(groupShortId);
235
+ const groupIsSelectable = isRestrictionHierarchyNodeSelectable(node.item);
220
236
 
221
237
  return (
222
238
  <>
223
- <CommandItem
224
- key={groupShortId}
225
- value={groupLabel}
226
- onSelect={() => handleSelect(groupShortId, groupIsSelected)}
227
- className="cursor-pointer"
228
- >
229
- <span className={cn("flex-1", groupIsSelected && "font-medium")}>{groupLabel}</span>
230
- {groupIsSelected && <Check className="h-4 w-4 shrink-0 text-primary" />}
231
- </CommandItem>
239
+ {groupIsSelectable ? (
240
+ <CommandItem
241
+ key={groupShortId}
242
+ value={groupLabel}
243
+ onSelect={() => handleSelect(groupShortId, groupIsSelected)}
244
+ className="cursor-pointer"
245
+ >
246
+ <span className={cn("flex-1", groupIsSelected && "font-medium")}>{groupLabel}</span>
247
+ {groupIsSelected && <Check className="h-4 w-4 shrink-0 text-primary" />}
248
+ </CommandItem>
249
+ ) : (
250
+ <div key={groupShortId} className="px-2 py-3 text-sm text-muted-foreground">
251
+ {groupLabel}
252
+ </div>
253
+ )}
232
254
  {node.children.map((child) => {
233
255
  const shortId = child.item.shortId ?? "";
234
256
  const childLabel = fmt(getLabelByLang(child.item.labels, lang) ?? shortId);
@@ -260,6 +282,7 @@ export const RestrictionSelectionCommandMenu: FC<Props> = ({
260
282
  items,
261
283
  restrictField,
262
284
  enableHierarchy = false,
285
+ hierarchyTaxonomy,
263
286
  hasMoreItems = false,
264
287
  showAllWhenEmpty = true,
265
288
  onRequestMore,
@@ -313,8 +336,39 @@ export const RestrictionSelectionCommandMenu: FC<Props> = ({
313
336
 
314
337
  const hierarchyRoots = useMemo(() => {
315
338
  if (!enableHierarchy) return [];
316
- return buildRestrictionTree(sortedItems);
317
- }, [enableHierarchy, sortedItems]);
339
+ if (!hierarchyTaxonomy) return buildRestrictionTree(sortedItems);
340
+
341
+ const hierarchy = buildTaxonomyHierarchy<RestrictionHierarchyItem>(
342
+ sortedItems.map((item) => ({
343
+ shortId: item.shortId || item.id || "",
344
+ label: getLabelByLang(item.labels, lang) || item.shortId || item.id || "",
345
+ active: selectedRestrictionIds.has(item.shortId || ""),
346
+ hits: 0,
347
+ total: 0,
348
+ taxonomyId: item.id || undefined,
349
+ parentIds: extractParentIds(item),
350
+ item,
351
+ })),
352
+ hierarchyTaxonomy
353
+ );
354
+
355
+ const toTreeNode = (node: RestrictionHierarchyItem): RestrictionTreeNode => {
356
+ const nodeKey = node.taxonomyId || node.shortId;
357
+ const runtimeItem = node.item || {
358
+ id: node.taxonomyId,
359
+ shortId: node.shortId,
360
+ labels: [{ language: lang, value: node.label }],
361
+ isStructural: node.isStructural,
362
+ } as DomainEntityModel;
363
+
364
+ return {
365
+ item: runtimeItem,
366
+ children: (hierarchy.children.get(nodeKey) || []).map((child) => toTreeNode(child)),
367
+ };
368
+ };
369
+
370
+ return hierarchy.roots.map((root) => toTreeNode(root));
371
+ }, [enableHierarchy, hierarchyTaxonomy, lang, selectedRestrictionIds, sortedItems]);
318
372
 
319
373
  const device = useBreakpoint();
320
374
  const [visibleCount, setVisibleCount] = useState(0);
@@ -383,6 +437,7 @@ export const RestrictionSelectionCommandMenu: FC<Props> = ({
383
437
  {enableHierarchy && visibleHierarchyRoots.map((rootNode) => {
384
438
  const shortId = rootNode.item.shortId || "";
385
439
  const hasChildren = rootNode.children.length > 0;
440
+ const isSelectable = isRestrictionHierarchyNodeSelectable(rootNode.item);
386
441
  const rawLabel = getLabelByLang(rootNode.item.labels, lang);
387
442
  const label = rawLabel
388
443
  const rootSelected = restrictionValues.includes(shortId);
@@ -390,6 +445,16 @@ export const RestrictionSelectionCommandMenu: FC<Props> = ({
390
445
  const shouldHighlightBranch = hasActiveDescendant && !rootSelected;
391
446
 
392
447
  if (!hasChildren) {
448
+ if (!isSelectable) {
449
+ return (
450
+ <NavigationMenuItem key={shortId}>
451
+ <div className="flex min-h-9 items-center rounded-full border border-transparent px-4 py-2 text-sm text-muted-foreground">
452
+ {label}
453
+ </div>
454
+ </NavigationMenuItem>
455
+ );
456
+ }
457
+
393
458
  return (
394
459
  <RestrictionNavigationItem
395
460
  key={shortId}
@@ -11,12 +11,15 @@ import {
11
11
  import { RestrictionDropdownItem, RestrictionNavigationItem } from "./restriction-menu-item";
12
12
  import { parseAsString, useQueryStates } from "nuqs";
13
13
  import { DomainEntityModel, ObjectRefModel } from "@c-rex/interfaces";
14
+ import type { TaxonomyResult } from "@c-rex/services/read-models";
14
15
  import { useLocale, useTranslations } from 'next-intl'
15
16
  import { cn, getLabelByLang } from "@c-rex/utils";
16
17
  import { useRestrictionStore } from "../stores/restriction-store";
17
18
  import { useBreakpoint } from "@c-rex/ui/hooks";
18
19
  import { DEVICE_OPTIONS } from "@c-rex/constants";
19
20
  import { ChevronDown, ChevronRight } from "lucide-react";
21
+ import { buildTaxonomyHierarchy, type HierarchyItem } from "../taxonomy/hierarchy";
22
+ import { isRestrictionHierarchyNodeSelectable } from "./restriction-hierarchy";
20
23
 
21
24
 
22
25
  type Props = {
@@ -24,6 +27,7 @@ type Props = {
24
27
  navigationMenuListClassName?: string
25
28
  items: DomainEntityModel[],
26
29
  enableHierarchy?: boolean,
30
+ hierarchyTaxonomy?: TaxonomyResult,
27
31
  hasMoreItems?: boolean,
28
32
  showAllWhenEmpty?: boolean,
29
33
  onRequestMore?: () => void,
@@ -41,6 +45,10 @@ type RestrictionTreeNode = {
41
45
  children: RestrictionTreeNode[];
42
46
  };
43
47
 
48
+ type RestrictionHierarchyItem = HierarchyItem & {
49
+ item?: DomainEntityModel;
50
+ };
51
+
44
52
  const hasSelectedDescendant = (node: RestrictionTreeNode, selectedShortIds: Set<string>): boolean => {
45
53
  const shortId = node.item.shortId;
46
54
  if (shortId && selectedShortIds.has(shortId)) {
@@ -68,6 +76,13 @@ const extractParentKeys = (item: DomainEntityModel): string[] => {
68
76
  });
69
77
  };
70
78
 
79
+ const extractParentIds = (item: DomainEntityModel): string[] => {
80
+ const withParents = item as DomainEntityModel & { parents?: ObjectRefModel[] | null };
81
+ return (withParents.parents || [])
82
+ .map((parent) => parent.id || parent.shortId)
83
+ .filter((parentId): parentId is string => Boolean(parentId));
84
+ };
85
+
71
86
  const buildRestrictionTree = (items: DomainEntityModel[]): RestrictionTreeNode[] => {
72
87
  const nodes = new Map<string, RestrictionTreeNode>();
73
88
  items.forEach((item) => {
@@ -116,6 +131,7 @@ export const RestrictionSelectionMenu: FC<Props> = ({
116
131
  items,
117
132
  restrictField,
118
133
  enableHierarchy = false,
134
+ hierarchyTaxonomy,
119
135
  hasMoreItems = false,
120
136
  showAllWhenEmpty = true,
121
137
  onRequestMore,
@@ -181,10 +197,40 @@ export const RestrictionSelectionMenu: FC<Props> = ({
181
197
  return sorted;
182
198
  }, [items, restrictionValues, updatePosition]);
183
199
 
184
- const hierarchyRoots = useMemo(() => {
200
+ const hierarchyTree = useMemo(() => {
185
201
  if (!enableHierarchy) return [];
186
- return buildRestrictionTree(sortedItems);
187
- }, [enableHierarchy, sortedItems]);
202
+ if (!hierarchyTaxonomy) return buildRestrictionTree(sortedItems);
203
+
204
+ const hierarchy = buildTaxonomyHierarchy<RestrictionHierarchyItem>(
205
+ sortedItems.map((item) => ({
206
+ shortId: item.shortId || item.id || "",
207
+ label: getLabelByLang(item.labels, lang) || item.shortId || item.id || "",
208
+ active: selectedRestrictionIds.has(item.shortId || ""),
209
+ hits: 0,
210
+ total: 0,
211
+ taxonomyId: item.id || undefined,
212
+ parentIds: extractParentIds(item),
213
+ item,
214
+ })),
215
+ hierarchyTaxonomy
216
+ );
217
+
218
+ const toTreeNode = (node: RestrictionHierarchyItem): RestrictionTreeNode => {
219
+ const nodeKey = node.taxonomyId || node.shortId;
220
+ const runtimeItem = node.item || {
221
+ id: node.taxonomyId,
222
+ shortId: node.shortId,
223
+ labels: [{ language: lang, value: node.label }],
224
+ } as DomainEntityModel;
225
+
226
+ return {
227
+ item: runtimeItem,
228
+ children: (hierarchy.children.get(nodeKey) || []).map((child) => toTreeNode(child)),
229
+ };
230
+ };
231
+
232
+ return hierarchy.roots.map((root) => toTreeNode(root));
233
+ }, [enableHierarchy, hierarchyTaxonomy, lang, selectedRestrictionIds, sortedItems]);
188
234
 
189
235
  const device = useBreakpoint();
190
236
  const [visibleCount, setVisibleCount] = useState(0);
@@ -201,7 +247,7 @@ export const RestrictionSelectionMenu: FC<Props> = ({
201
247
  if (visibleCount == 0) return;
202
248
 
203
249
  if (enableHierarchy) {
204
- const roots = hierarchyRoots.map((node) => node.item);
250
+ const roots = hierarchyTree.map((node) => node.item);
205
251
  setVisibleItems(roots.slice(0, visibleCount));
206
252
  setHiddenItems(roots.slice(visibleCount));
207
253
  return;
@@ -209,19 +255,19 @@ export const RestrictionSelectionMenu: FC<Props> = ({
209
255
 
210
256
  setVisibleItems(sortedItems.slice(0, visibleCount));
211
257
  setHiddenItems(sortedItems.slice(visibleCount));
212
- }, [enableHierarchy, hierarchyRoots, sortedItems, visibleCount]);
258
+ }, [enableHierarchy, hierarchyTree, sortedItems, visibleCount]);
213
259
 
214
260
  const visibleHierarchyRoots = useMemo(() => {
215
261
  if (!enableHierarchy) return [];
216
262
  const visibleRootShortIds = new Set(visibleItems.map((item) => item.shortId).filter(Boolean));
217
- return hierarchyRoots.filter((root) => root.item.shortId && visibleRootShortIds.has(root.item.shortId));
218
- }, [enableHierarchy, hierarchyRoots, visibleItems]);
263
+ return hierarchyTree.filter((root) => root.item.shortId && visibleRootShortIds.has(root.item.shortId));
264
+ }, [enableHierarchy, hierarchyTree, visibleItems]);
219
265
 
220
266
  const hiddenHierarchyRoots = useMemo(() => {
221
267
  if (!enableHierarchy) return [];
222
268
  const hiddenRootShortIds = new Set(hiddenItems.map((item) => item.shortId).filter(Boolean));
223
- return hierarchyRoots.filter((root) => root.item.shortId && hiddenRootShortIds.has(root.item.shortId));
224
- }, [enableHierarchy, hierarchyRoots, hiddenItems]);
269
+ return hierarchyTree.filter((root) => root.item.shortId && hiddenRootShortIds.has(root.item.shortId));
270
+ }, [enableHierarchy, hierarchyTree, hiddenItems]);
225
271
 
226
272
  const [expandedNodes, setExpandedNodes] = useState<Record<string, boolean>>({});
227
273
  const toggleExpanded = (shortId: string) => {
@@ -234,8 +280,10 @@ export const RestrictionSelectionMenu: FC<Props> = ({
234
280
  const isRootNode = depth === 0;
235
281
  const isExpanded = isRootNode || expandedNodes[shortId] === true;
236
282
  const isSelected = restrictionValues.includes(shortId);
283
+ const isSelectable = isRestrictionHierarchyNodeSelectable(node.item);
237
284
  const hasActiveDescendant = hasChildren && node.children.some((child) => hasSelectedDescendant(child, selectedRestrictionIds));
238
285
  const shouldHighlightBranch = hasActiveDescendant && !isSelected;
286
+ const label = getLabelByLang(node.item.labels, lang);
239
287
 
240
288
  return (
241
289
  <li key={`tree-node-${shortId}`} className="flex flex-col w-full">
@@ -259,12 +307,18 @@ export const RestrictionSelectionMenu: FC<Props> = ({
259
307
  shouldHighlightBranch && "bg-primary/5 ring-1 ring-primary/10"
260
308
  )}
261
309
  >
262
- <RestrictionDropdownItem
263
- shortId={shortId}
264
- restrictField={restrictField}
265
- label={getLabelByLang(node.item.labels, lang)}
266
- selected={isSelected}
267
- />
310
+ {isSelectable ? (
311
+ <RestrictionDropdownItem
312
+ shortId={shortId}
313
+ restrictField={restrictField}
314
+ label={label}
315
+ selected={isSelected}
316
+ />
317
+ ) : (
318
+ <div className="flex min-h-10 w-full items-center rounded-full px-4 py-2 text-sm text-muted-foreground">
319
+ {label}
320
+ </div>
321
+ )}
268
322
  </div>
269
323
  {shouldHighlightBranch ? (
270
324
  <span
@@ -309,12 +363,23 @@ export const RestrictionSelectionMenu: FC<Props> = ({
309
363
  {enableHierarchy && visibleHierarchyRoots.map((rootNode) => {
310
364
  const shortId = rootNode.item.shortId || "";
311
365
  const hasChildren = rootNode.children.length > 0;
366
+ const isSelectable = isRestrictionHierarchyNodeSelectable(rootNode.item);
312
367
  const label = getLabelByLang(rootNode.item.labels, lang);
313
368
  const rootSelected = restrictionValues.includes(shortId);
314
369
  const hasActiveDescendant = hasChildren && hasSelectedDescendant(rootNode, selectedRestrictionIds);
315
370
  const shouldHighlightBranch = hasActiveDescendant && !rootSelected;
316
371
 
317
372
  if (!hasChildren) {
373
+ if (!isSelectable) {
374
+ return (
375
+ <NavigationMenuItem key={shortId}>
376
+ <div className="flex min-h-9 items-center rounded-full border border-transparent px-4 py-2 text-sm text-muted-foreground">
377
+ {label}
378
+ </div>
379
+ </NavigationMenuItem>
380
+ );
381
+ }
382
+
318
383
  return (
319
384
  <RestrictionNavigationItem
320
385
  key={shortId}
@@ -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";
@@ -29,6 +30,8 @@ export type TaxonomyRestrictionCommandMenuProps = {
29
30
  fetchMode?: RestrictionMenuFetchMode;
30
31
  showAllWhenEmpty?: boolean;
31
32
  queryParams?: GenericQueryParams;
33
+ stripLabelPrefix?: string;
34
+ hierarchyTaxonomy?: TaxonomyResult;
32
35
  };
33
36
 
34
37
  export const TaxonomyRestrictionCommandMenu: FC<TaxonomyRestrictionCommandMenuProps> = ({
@@ -41,6 +44,8 @@ export const TaxonomyRestrictionCommandMenu: FC<TaxonomyRestrictionCommandMenuPr
41
44
  fetchMode = "deferred",
42
45
  showAllWhenEmpty = true,
43
46
  navigationMenuListClassName = "items-center justify-start gap-4 flex-row",
47
+ stripLabelPrefix,
48
+ hierarchyTaxonomy,
44
49
  }) => {
45
50
  const [loadAll, setLoadAll] = useState(false);
46
51
  const RequestComponent = ComponentOptions[requestType] as unknown as FC<GenericRequestProps>;
@@ -92,6 +97,7 @@ export const TaxonomyRestrictionCommandMenu: FC<TaxonomyRestrictionCommandMenuPr
92
97
  restrictField={restrictField}
93
98
  items={data.items || []}
94
99
  enableHierarchy={enableHierarchy}
100
+ hierarchyTaxonomy={hierarchyTaxonomy}
95
101
  hasMoreItems={hasMoreItems}
96
102
  showAllWhenEmpty={showAllWhenEmpty}
97
103
  onRequestMore={() => {