@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.
@@ -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,
@@ -32,6 +36,8 @@ type Props = {
32
36
  [DEVICE_OPTIONS.TABLET]: number,
33
37
  [DEVICE_OPTIONS.DESKTOP]: number,
34
38
  }
39
+ multipleSelection?: boolean;
40
+ updatePosition?: boolean;
35
41
  }
36
42
 
37
43
  type RestrictionTreeNode = {
@@ -39,6 +45,10 @@ type RestrictionTreeNode = {
39
45
  children: RestrictionTreeNode[];
40
46
  };
41
47
 
48
+ type RestrictionHierarchyItem = HierarchyItem & {
49
+ item?: DomainEntityModel;
50
+ };
51
+
42
52
  const hasSelectedDescendant = (node: RestrictionTreeNode, selectedShortIds: Set<string>): boolean => {
43
53
  const shortId = node.item.shortId;
44
54
  if (shortId && selectedShortIds.has(shortId)) {
@@ -66,6 +76,13 @@ const extractParentKeys = (item: DomainEntityModel): string[] => {
66
76
  });
67
77
  };
68
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
+
69
86
  const buildRestrictionTree = (items: DomainEntityModel[]): RestrictionTreeNode[] => {
70
87
  const nodes = new Map<string, RestrictionTreeNode>();
71
88
  items.forEach((item) => {
@@ -114,6 +131,7 @@ export const RestrictionSelectionMenu: FC<Props> = ({
114
131
  items,
115
132
  restrictField,
116
133
  enableHierarchy = false,
134
+ hierarchyTaxonomy,
117
135
  hasMoreItems = false,
118
136
  showAllWhenEmpty = true,
119
137
  onRequestMore,
@@ -122,7 +140,9 @@ export const RestrictionSelectionMenu: FC<Props> = ({
122
140
  [DEVICE_OPTIONS.MOBILE]: 2,
123
141
  [DEVICE_OPTIONS.TABLET]: 4,
124
142
  [DEVICE_OPTIONS.DESKTOP]: 7,
125
- }
143
+ },
144
+ multipleSelection = true,
145
+ updatePosition = true
126
146
  }) => {
127
147
  const t = useTranslations();
128
148
  const setRestrictionList = useRestrictionStore((state) => state.setRestrictionList);
@@ -154,6 +174,8 @@ export const RestrictionSelectionMenu: FC<Props> = ({
154
174
  const sortedItems = useMemo(() => {
155
175
  //if shortId it is on the restrictionValues, it should be on top of the list, otherwise keep the original order
156
176
 
177
+ if (updatePosition === false) return items;
178
+
157
179
  const sorted = [...items].sort((a, b) => {
158
180
  const aShortId = a.shortId || "";
159
181
  const bShortId = b.shortId || "";
@@ -173,12 +195,42 @@ export const RestrictionSelectionMenu: FC<Props> = ({
173
195
  return aIndex - bIndex; // sort by index in restrictionValues
174
196
  });
175
197
  return sorted;
176
- }, [items, restrictionValues]);
198
+ }, [items, restrictionValues, updatePosition]);
177
199
 
178
- const hierarchyRoots = useMemo(() => {
200
+ const hierarchyTree = useMemo(() => {
179
201
  if (!enableHierarchy) return [];
180
- return buildRestrictionTree(sortedItems);
181
- }, [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]);
182
234
 
183
235
  const device = useBreakpoint();
184
236
  const [visibleCount, setVisibleCount] = useState(0);
@@ -195,7 +247,7 @@ export const RestrictionSelectionMenu: FC<Props> = ({
195
247
  if (visibleCount == 0) return;
196
248
 
197
249
  if (enableHierarchy) {
198
- const roots = hierarchyRoots.map((node) => node.item);
250
+ const roots = hierarchyTree.map((node) => node.item);
199
251
  setVisibleItems(roots.slice(0, visibleCount));
200
252
  setHiddenItems(roots.slice(visibleCount));
201
253
  return;
@@ -203,19 +255,19 @@ export const RestrictionSelectionMenu: FC<Props> = ({
203
255
 
204
256
  setVisibleItems(sortedItems.slice(0, visibleCount));
205
257
  setHiddenItems(sortedItems.slice(visibleCount));
206
- }, [enableHierarchy, hierarchyRoots, sortedItems, visibleCount]);
258
+ }, [enableHierarchy, hierarchyTree, sortedItems, visibleCount]);
207
259
 
208
260
  const visibleHierarchyRoots = useMemo(() => {
209
261
  if (!enableHierarchy) return [];
210
262
  const visibleRootShortIds = new Set(visibleItems.map((item) => item.shortId).filter(Boolean));
211
- return hierarchyRoots.filter((root) => root.item.shortId && visibleRootShortIds.has(root.item.shortId));
212
- }, [enableHierarchy, hierarchyRoots, visibleItems]);
263
+ return hierarchyTree.filter((root) => root.item.shortId && visibleRootShortIds.has(root.item.shortId));
264
+ }, [enableHierarchy, hierarchyTree, visibleItems]);
213
265
 
214
266
  const hiddenHierarchyRoots = useMemo(() => {
215
267
  if (!enableHierarchy) return [];
216
268
  const hiddenRootShortIds = new Set(hiddenItems.map((item) => item.shortId).filter(Boolean));
217
- return hierarchyRoots.filter((root) => root.item.shortId && hiddenRootShortIds.has(root.item.shortId));
218
- }, [enableHierarchy, hierarchyRoots, hiddenItems]);
269
+ return hierarchyTree.filter((root) => root.item.shortId && hiddenRootShortIds.has(root.item.shortId));
270
+ }, [enableHierarchy, hierarchyTree, hiddenItems]);
219
271
 
220
272
  const [expandedNodes, setExpandedNodes] = useState<Record<string, boolean>>({});
221
273
  const toggleExpanded = (shortId: string) => {
@@ -228,8 +280,10 @@ export const RestrictionSelectionMenu: FC<Props> = ({
228
280
  const isRootNode = depth === 0;
229
281
  const isExpanded = isRootNode || expandedNodes[shortId] === true;
230
282
  const isSelected = restrictionValues.includes(shortId);
283
+ const isSelectable = isRestrictionHierarchyNodeSelectable(node.item);
231
284
  const hasActiveDescendant = hasChildren && node.children.some((child) => hasSelectedDescendant(child, selectedRestrictionIds));
232
285
  const shouldHighlightBranch = hasActiveDescendant && !isSelected;
286
+ const label = getLabelByLang(node.item.labels, lang);
233
287
 
234
288
  return (
235
289
  <li key={`tree-node-${shortId}`} className="flex flex-col w-full">
@@ -253,12 +307,18 @@ export const RestrictionSelectionMenu: FC<Props> = ({
253
307
  shouldHighlightBranch && "bg-primary/5 ring-1 ring-primary/10"
254
308
  )}
255
309
  >
256
- <RestrictionDropdownItem
257
- shortId={shortId}
258
- restrictField={restrictField}
259
- label={getLabelByLang(node.item.labels, lang)}
260
- selected={isSelected}
261
- />
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
+ )}
262
322
  </div>
263
323
  {shouldHighlightBranch ? (
264
324
  <span
@@ -293,6 +353,7 @@ export const RestrictionSelectionMenu: FC<Props> = ({
293
353
  <RestrictionNavigationItem
294
354
  key={item.shortId}
295
355
  shortId={item.shortId!}
356
+ multipleSelection={multipleSelection}
296
357
  restrictField={restrictField as string}
297
358
  label={getLabelByLang(item.labels, lang)}
298
359
  selected={restrictionValues.includes(item.shortId!)}
@@ -302,12 +363,23 @@ export const RestrictionSelectionMenu: FC<Props> = ({
302
363
  {enableHierarchy && visibleHierarchyRoots.map((rootNode) => {
303
364
  const shortId = rootNode.item.shortId || "";
304
365
  const hasChildren = rootNode.children.length > 0;
366
+ const isSelectable = isRestrictionHierarchyNodeSelectable(rootNode.item);
305
367
  const label = getLabelByLang(rootNode.item.labels, lang);
306
368
  const rootSelected = restrictionValues.includes(shortId);
307
369
  const hasActiveDescendant = hasChildren && hasSelectedDescendant(rootNode, selectedRestrictionIds);
308
370
  const shouldHighlightBranch = hasActiveDescendant && !rootSelected;
309
371
 
310
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
+
311
383
  return (
312
384
  <RestrictionNavigationItem
313
385
  key={shortId}
@@ -315,6 +387,7 @@ export const RestrictionSelectionMenu: FC<Props> = ({
315
387
  restrictField={restrictField}
316
388
  label={label}
317
389
  selected={restrictionValues.includes(shortId)}
390
+ multipleSelection={multipleSelection}
318
391
  />
319
392
  );
320
393
  }
@@ -364,6 +437,7 @@ export const RestrictionSelectionMenu: FC<Props> = ({
364
437
  >
365
438
  <RestrictionDropdownItem
366
439
  shortId={item.shortId!}
440
+ multipleSelection={multipleSelection}
367
441
  restrictField={restrictField as string}
368
442
  label={getLabelByLang(item.labels, lang)}
369
443
  selected={restrictionValues.includes(item.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";
@@ -30,6 +31,7 @@ export type TaxonomyRestrictionCommandMenuProps = {
30
31
  showAllWhenEmpty?: boolean;
31
32
  queryParams?: GenericQueryParams;
32
33
  stripLabelPrefix?: string;
34
+ hierarchyTaxonomy?: TaxonomyResult;
33
35
  };
34
36
 
35
37
  export const TaxonomyRestrictionCommandMenu: FC<TaxonomyRestrictionCommandMenuProps> = ({
@@ -43,6 +45,7 @@ export const TaxonomyRestrictionCommandMenu: FC<TaxonomyRestrictionCommandMenuPr
43
45
  showAllWhenEmpty = true,
44
46
  navigationMenuListClassName = "items-center justify-start gap-4 flex-row",
45
47
  stripLabelPrefix,
48
+ hierarchyTaxonomy,
46
49
  }) => {
47
50
  const [loadAll, setLoadAll] = useState(false);
48
51
  const RequestComponent = ComponentOptions[requestType] as unknown as FC<GenericRequestProps>;
@@ -94,6 +97,7 @@ export const TaxonomyRestrictionCommandMenu: FC<TaxonomyRestrictionCommandMenuPr
94
97
  restrictField={restrictField}
95
98
  items={data.items || []}
96
99
  enableHierarchy={enableHierarchy}
100
+ hierarchyTaxonomy={hierarchyTaxonomy}
97
101
  hasMoreItems={hasMoreItems}
98
102
  showAllWhenEmpty={showAllWhenEmpty}
99
103
  onRequestMore={() => {
@@ -102,7 +106,6 @@ export const TaxonomyRestrictionCommandMenu: FC<TaxonomyRestrictionCommandMenuPr
102
106
  }
103
107
  }}
104
108
  navigationMenuListClassName={navigationMenuListClassName}
105
- stripLabelPrefix={stripLabelPrefix}
106
109
  />
107
110
  );
108
111
  }}
@@ -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,9 @@ export type TaxonomyRestrictionMenuProps = {
34
35
  fetchMode?: RestrictionMenuFetchMode;
35
36
  showAllWhenEmpty?: boolean;
36
37
  queryParams?: GenericQueryParams;
38
+ hierarchyTaxonomy?: TaxonomyResult;
39
+ multipleSelection?: boolean;
40
+ updatePosition?: boolean;
37
41
  };
38
42
 
39
43
  const DEFAULT_ITEMS_BY_ROW = {
@@ -52,6 +56,9 @@ export const TaxonomyRestrictionMenu: FC<TaxonomyRestrictionMenuProps> = ({
52
56
  fetchMode = "deferred",
53
57
  showAllWhenEmpty = true,
54
58
  navigationMenuListClassName = "items-center justify-start gap-4 flex-row",
59
+ hierarchyTaxonomy,
60
+ multipleSelection = true,
61
+ updatePosition = true,
55
62
  }) => {
56
63
  const [loadAll, setLoadAll] = useState(false);
57
64
  const RequestComponent = ComponentOptions[requestType] as unknown as FC<GenericRequestProps>;
@@ -104,6 +111,7 @@ export const TaxonomyRestrictionMenu: FC<TaxonomyRestrictionMenuProps> = ({
104
111
  restrictField={restrictField}
105
112
  items={data.items || []}
106
113
  enableHierarchy={enableHierarchy}
114
+ hierarchyTaxonomy={hierarchyTaxonomy}
107
115
  hasMoreItems={hasMoreItems}
108
116
  showAllWhenEmpty={showAllWhenEmpty}
109
117
  itemsByRow={itemsByRow}
@@ -113,6 +121,8 @@ export const TaxonomyRestrictionMenu: FC<TaxonomyRestrictionMenuProps> = ({
113
121
  }
114
122
  }}
115
123
  navigationMenuListClassName={navigationMenuListClassName}
124
+ multipleSelection={multipleSelection}
125
+ updatePosition={updatePosition}
116
126
  />
117
127
  );
118
128
  }}
@@ -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 =>