@c-rex/components 0.3.0-build.38 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@c-rex/components",
3
- "version": "0.3.0-build.38",
3
+ "version": "0.3.0-build.40",
4
4
  "files": [
5
5
  "src"
6
6
  ],
@@ -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 = {
2
+ isStructural?: boolean;
3
+ };
4
+
5
+ export const isRestrictionHierarchyNodeSelectable = (
6
+ item: RestrictionHierarchyNode
7
+ ): boolean => !item.isStructural;
@@ -15,6 +15,7 @@ type Props = {
15
15
  removeRestrictParam?: boolean;
16
16
  selected?: boolean;
17
17
  onClick?: () => void;
18
+ multipleSelection?: boolean;
18
19
  };
19
20
 
20
21
  export const RestrictionNavigationItem: FC<Props> = ({
@@ -23,6 +24,7 @@ export const RestrictionNavigationItem: FC<Props> = ({
23
24
  restrictField,
24
25
  removeRestrictParam = false,
25
26
  selected = false,
27
+ multipleSelection = true,
26
28
  }) => {
27
29
  const [restrict, setRestrict] = useQueryState("restrict", {
28
30
  shallow: false,
@@ -35,6 +37,7 @@ export const RestrictionNavigationItem: FC<Props> = ({
35
37
  restrictField,
36
38
  removeRestrictParam,
37
39
  selected,
40
+ multipleSelection,
38
41
  currentRestrict: restrict,
39
42
  });
40
43
 
@@ -72,6 +75,7 @@ export const RestrictionDropdownItem: FC<Props> = ({
72
75
  restrictField,
73
76
  selected = false,
74
77
  onClick,
78
+ multipleSelection = true
75
79
  }) => {
76
80
  const [restrict, setRestrict] = useQueryState("restrict", {
77
81
  shallow: false,
@@ -83,6 +87,7 @@ export const RestrictionDropdownItem: FC<Props> = ({
83
87
  shortId,
84
88
  restrictField,
85
89
  selected,
90
+ multipleSelection,
86
91
  currentRestrict: restrict,
87
92
  });
88
93
 
@@ -121,7 +126,7 @@ function getRestrictionValue({
121
126
  restrictField,
122
127
  removeRestrictParam = false,
123
128
  selected = false,
124
-
129
+ multipleSelection = true,
125
130
  currentRestrict,
126
131
  }: {
127
132
  shortId?: string;
@@ -129,11 +134,12 @@ function getRestrictionValue({
129
134
  removeRestrictParam?: boolean;
130
135
  selected?: boolean;
131
136
  currentRestrict: string | null;
137
+ multipleSelection?: boolean;
132
138
  }): { restrictionValue: string | null; shouldRemoveRestrictParam: boolean } {
133
139
  let restrictParam = "";
134
140
  let shouldRemoveRestrictParam = removeRestrictParam;
135
141
 
136
- if (currentRestrict) {
142
+ if (currentRestrict && multipleSelection) {
137
143
  if (selected) {
138
144
  const restrictionsLength = currentRestrict.split(",").length;
139
145
  //if there is only one restriction, we can remove the whole restrict param
@@ -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,16 +21,18 @@ 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,
32
- stripLabelPrefix?: string,
33
36
  itemsByRow?: {
34
37
  [DEVICE_OPTIONS.MOBILE]: number,
35
38
  [DEVICE_OPTIONS.TABLET]: number,
@@ -42,6 +45,10 @@ type RestrictionTreeNode = {
42
45
  children: RestrictionTreeNode[];
43
46
  };
44
47
 
48
+ type RestrictionHierarchyItem = HierarchyItem & {
49
+ item?: DomainEntityModel;
50
+ };
51
+
45
52
  const hasSelectedDescendant = (node: RestrictionTreeNode, selectedShortIds: Set<string>): boolean => {
46
53
  const shortId = node.item.shortId;
47
54
  if (shortId && selectedShortIds.has(shortId)) return true;
@@ -65,6 +72,13 @@ const extractParentKeys = (item: DomainEntityModel): string[] => {
65
72
  });
66
73
  };
67
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
+
68
82
  const buildRestrictionTree = (items: DomainEntityModel[]): RestrictionTreeNode[] => {
69
83
  const nodes = new Map<string, RestrictionTreeNode>();
70
84
  items.forEach((item) => {
@@ -218,18 +232,25 @@ const RestrictionCommandDialog: FC<RestrictionCommandDialogProps> = ({
218
232
  const groupShortId = node.item.shortId ?? "";
219
233
  const groupLabel = fmt(getLabelByLang(node.item.labels, lang) ?? groupShortId);
220
234
  const groupIsSelected = selectedRestrictionIds.has(groupShortId);
235
+ const groupIsSelectable = isRestrictionHierarchyNodeSelectable(node.item);
221
236
 
222
237
  return (
223
238
  <>
224
- <CommandItem
225
- key={groupShortId}
226
- value={groupLabel}
227
- onSelect={() => handleSelect(groupShortId, groupIsSelected)}
228
- className="cursor-pointer"
229
- >
230
- <span className={cn("flex-1", groupIsSelected && "font-medium")}>{groupLabel}</span>
231
- {groupIsSelected && <Check className="h-4 w-4 shrink-0 text-primary" />}
232
- </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
+ )}
233
254
  {node.children.map((child) => {
234
255
  const shortId = child.item.shortId ?? "";
235
256
  const childLabel = fmt(getLabelByLang(child.item.labels, lang) ?? shortId);
@@ -261,10 +282,10 @@ export const RestrictionSelectionCommandMenu: FC<Props> = ({
261
282
  items,
262
283
  restrictField,
263
284
  enableHierarchy = false,
285
+ hierarchyTaxonomy,
264
286
  hasMoreItems = false,
265
287
  showAllWhenEmpty = true,
266
288
  onRequestMore,
267
- stripLabelPrefix,
268
289
  navigationMenuListClassName = "items-center justify-start gap-4 flex-row",
269
290
  itemsByRow = {
270
291
  [DEVICE_OPTIONS.MOBILE]: 2,
@@ -274,9 +295,6 @@ export const RestrictionSelectionCommandMenu: FC<Props> = ({
274
295
  }) => {
275
296
  const t = useTranslations();
276
297
  const setRestrictionList = useRestrictionStore((state) => state.setRestrictionList);
277
- const formatLabel = stripLabelPrefix
278
- ? (label: string) => label.replace(new RegExp(`^${stripLabelPrefix}`, "i"), "")
279
- : undefined;
280
298
 
281
299
  const [params] = useQueryStates({
282
300
  restrict: parseAsString,
@@ -318,8 +336,39 @@ export const RestrictionSelectionCommandMenu: FC<Props> = ({
318
336
 
319
337
  const hierarchyRoots = useMemo(() => {
320
338
  if (!enableHierarchy) return [];
321
- return buildRestrictionTree(sortedItems);
322
- }, [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]);
323
372
 
324
373
  const device = useBreakpoint();
325
374
  const [visibleCount, setVisibleCount] = useState(0);
@@ -373,7 +422,7 @@ export const RestrictionSelectionCommandMenu: FC<Props> = ({
373
422
 
374
423
  {!enableHierarchy && visibleItems.map((item) => {
375
424
  const rawLabel = getLabelByLang(item.labels, lang);
376
- const label = rawLabel && formatLabel ? formatLabel(rawLabel) : rawLabel;
425
+ const label = rawLabel
377
426
  return (
378
427
  <RestrictionNavigationItem
379
428
  key={item.shortId}
@@ -388,13 +437,24 @@ export const RestrictionSelectionCommandMenu: FC<Props> = ({
388
437
  {enableHierarchy && visibleHierarchyRoots.map((rootNode) => {
389
438
  const shortId = rootNode.item.shortId || "";
390
439
  const hasChildren = rootNode.children.length > 0;
440
+ const isSelectable = isRestrictionHierarchyNodeSelectable(rootNode.item);
391
441
  const rawLabel = getLabelByLang(rootNode.item.labels, lang);
392
- const label = rawLabel && formatLabel ? formatLabel(rawLabel) : rawLabel;
442
+ const label = rawLabel
393
443
  const rootSelected = restrictionValues.includes(shortId);
394
444
  const hasActiveDescendant = hasChildren && hasSelectedDescendant(rootNode, selectedRestrictionIds);
395
445
  const shouldHighlightBranch = hasActiveDescendant && !rootSelected;
396
446
 
397
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
+
398
458
  return (
399
459
  <RestrictionNavigationItem
400
460
  key={shortId}
@@ -415,7 +475,7 @@ export const RestrictionSelectionCommandMenu: FC<Props> = ({
415
475
  selectedRestrictionIds={selectedRestrictionIds}
416
476
  lang={lang}
417
477
  highlighted={shouldHighlightBranch}
418
- formatLabel={formatLabel}
478
+
419
479
  />
420
480
  </NavigationMenuItem>
421
481
  );
@@ -434,7 +494,6 @@ export const RestrictionSelectionCommandMenu: FC<Props> = ({
434
494
  selectedRestrictionIds={selectedRestrictionIds}
435
495
  lang={lang}
436
496
  highlighted={false}
437
- formatLabel={formatLabel}
438
497
  />
439
498
  </NavigationMenuItem>
440
499
  )}