@c-rex/components 0.3.0-build.35 → 0.3.0-build.36
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 +28 -36
- package/src/article/article-action-bar.tsx +4 -1
- package/src/{check-article-lang.tsx → article/check-article-lang.tsx} +1 -1
- package/src/article/render-article-highlight.tsx +108 -0
- package/src/article/render-article.tsx +28 -0
- package/src/autocomplete.tsx +2 -2
- package/src/carousel/carousel.tsx +5 -2
- package/src/carousel/information-unit-carousel-item.tsx +1 -1
- package/src/content-unavailable.tsx +20 -0
- package/src/directoryNodes/directory-tree-context.tsx +9 -4
- package/src/documents/description-preview.tsx +14 -4
- package/src/documents/result-list-item.tsx +35 -46
- package/src/favorites/__tests__/favorites-hydration.test.tsx +245 -0
- package/src/favorites/bookmark-button.tsx +38 -20
- package/src/favorites/favorite-button.tsx +23 -24
- package/src/favorites/favorites-context.tsx +287 -0
- package/src/icons/file-icon.tsx +9 -26
- package/src/info/information-unit-metadata-grid-client.tsx +21 -21
- package/src/page-wrapper.tsx +1 -1
- package/src/renditions/html-client.tsx +8 -6
- package/src/renditions/html.tsx +3 -1
- package/src/restriction-menu/restriction-menu-item.tsx +48 -58
- package/src/restriction-menu/restriction-selection-command-menu.tsx +444 -0
- package/src/restriction-menu/restriction-selection-menu.tsx +3 -5
- package/src/restriction-menu/taxonomy-restriction-command-menu.tsx +111 -0
- package/src/restriction-menu/taxonomy-restriction-menu.tsx +1 -7
- package/src/results/filter-navbar.tsx +81 -76
- package/src/results/filter-sidebar/context.tsx +32 -0
- package/src/results/filter-sidebar/index.tsx +44 -35
- package/src/results/generic/search-results-client.tsx +5 -4
- package/src/results/generic/table-result-list.tsx +16 -16
- package/src/results/information-unit-search-results-card-list.tsx +4 -1
- package/src/results/pagination.tsx +43 -40
- package/src/search-input.tsx +4 -2
- package/src/toc/toc-browse-controls.tsx +2 -2
- package/src/toc/toc-tree-panel.tsx +19 -16
- package/src/article/article-content.tsx +0 -19
- package/src/breadcrumb.tsx +0 -124
- package/src/directoryNodes/tree-of-content.tsx +0 -68
- package/src/render-article.tsx +0 -75
- package/src/restriction-menu/restriction-menu-container.tsx +0 -4
- package/src/restriction-menu/restriction-menu.tsx +0 -4
- package/src/stores/__tests__/favorites-store.test.ts +0 -54
- package/src/stores/favorites-store.ts +0 -163
- /package/src/{render-article.module.css → article/render-article.module.css} +0 -0
|
@@ -0,0 +1,444 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { FC, useEffect, useMemo, useRef, useState } from "react"
|
|
4
|
+
import {
|
|
5
|
+
NavigationMenu,
|
|
6
|
+
NavigationMenuList,
|
|
7
|
+
NavigationMenuItem,
|
|
8
|
+
} from "@c-rex/ui/navigation-menu";
|
|
9
|
+
import { RestrictionNavigationItem } from "./restriction-menu-item";
|
|
10
|
+
import { parseAsString, useQueryState, useQueryStates } from "nuqs";
|
|
11
|
+
import { DomainEntityModel, ObjectRefModel } from "@c-rex/interfaces";
|
|
12
|
+
import { useLocale, useTranslations } from 'next-intl'
|
|
13
|
+
import { cn, getLabelByLang } from "@c-rex/utils";
|
|
14
|
+
import { useRestrictionStore } from "../stores/restriction-store";
|
|
15
|
+
import { useBreakpoint } from "@c-rex/ui/hooks";
|
|
16
|
+
import { DEVICE_OPTIONS } from "@c-rex/constants";
|
|
17
|
+
import { Command, CommandEmpty, CommandInput, CommandItem, CommandList } from "@c-rex/ui/command";
|
|
18
|
+
import { Dialog, DialogContent } from "@c-rex/ui/dialog";
|
|
19
|
+
import { Button } from "@c-rex/ui/button";
|
|
20
|
+
import { Check, ChevronDown } from "lucide-react";
|
|
21
|
+
import { useSearchNavigationStore } from "../stores/search-navigation-store";
|
|
22
|
+
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@c-rex/ui/tooltip";
|
|
23
|
+
|
|
24
|
+
type Props = {
|
|
25
|
+
restrictField: string
|
|
26
|
+
navigationMenuListClassName?: string
|
|
27
|
+
items: DomainEntityModel[],
|
|
28
|
+
enableHierarchy?: boolean,
|
|
29
|
+
hasMoreItems?: boolean,
|
|
30
|
+
showAllWhenEmpty?: boolean,
|
|
31
|
+
onRequestMore?: () => void,
|
|
32
|
+
stripLabelPrefix?: string,
|
|
33
|
+
itemsByRow?: {
|
|
34
|
+
[DEVICE_OPTIONS.MOBILE]: number,
|
|
35
|
+
[DEVICE_OPTIONS.TABLET]: number,
|
|
36
|
+
[DEVICE_OPTIONS.DESKTOP]: number,
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
type RestrictionTreeNode = {
|
|
41
|
+
item: DomainEntityModel;
|
|
42
|
+
children: RestrictionTreeNode[];
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const hasSelectedDescendant = (node: RestrictionTreeNode, selectedShortIds: Set<string>): boolean => {
|
|
46
|
+
const shortId = node.item.shortId;
|
|
47
|
+
if (shortId && selectedShortIds.has(shortId)) return true;
|
|
48
|
+
return node.children.some((child) => hasSelectedDescendant(child, selectedShortIds));
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const getNodeKeys = (item: DomainEntityModel): string[] => {
|
|
52
|
+
const keys = new Set<string>();
|
|
53
|
+
if (item.shortId) keys.add(`short:${item.shortId}`);
|
|
54
|
+
if (item.id) keys.add(`id:${item.id}`);
|
|
55
|
+
return Array.from(keys);
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const extractParentKeys = (item: DomainEntityModel): string[] => {
|
|
59
|
+
const withParents = item as DomainEntityModel & { parents?: ObjectRefModel[] | null };
|
|
60
|
+
return (withParents.parents || []).flatMap((parent) => {
|
|
61
|
+
const keys: string[] = [];
|
|
62
|
+
if (parent.shortId) keys.push(`short:${parent.shortId}`);
|
|
63
|
+
if (parent.id) keys.push(`id:${parent.id}`);
|
|
64
|
+
return keys;
|
|
65
|
+
});
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const buildRestrictionTree = (items: DomainEntityModel[]): RestrictionTreeNode[] => {
|
|
69
|
+
const nodes = new Map<string, RestrictionTreeNode>();
|
|
70
|
+
items.forEach((item) => {
|
|
71
|
+
const keys = getNodeKeys(item);
|
|
72
|
+
if (keys.length === 0) return;
|
|
73
|
+
const node: RestrictionTreeNode = { item, children: [] };
|
|
74
|
+
keys.forEach((key) => nodes.set(key, node));
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
const roots: RestrictionTreeNode[] = [];
|
|
78
|
+
const linked = new Set<RestrictionTreeNode>();
|
|
79
|
+
const rootSet = new Set<RestrictionTreeNode>();
|
|
80
|
+
|
|
81
|
+
items.forEach((item) => {
|
|
82
|
+
const [firstKey] = getNodeKeys(item);
|
|
83
|
+
if (!firstKey) return;
|
|
84
|
+
const node = nodes.get(firstKey);
|
|
85
|
+
if (!node) return;
|
|
86
|
+
|
|
87
|
+
const parentKeys = extractParentKeys(item);
|
|
88
|
+
const parentNode = parentKeys
|
|
89
|
+
.map((parentKey) => nodes.get(parentKey))
|
|
90
|
+
.find((candidate) => candidate !== undefined);
|
|
91
|
+
|
|
92
|
+
if (parentNode) {
|
|
93
|
+
if (!parentNode.children.includes(node)) {
|
|
94
|
+
parentNode.children.push(node);
|
|
95
|
+
}
|
|
96
|
+
linked.add(node);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
rootSet.add(node);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
rootSet.forEach((root) => {
|
|
104
|
+
if (!linked.has(root) || root.item.shortId) {
|
|
105
|
+
roots.push(root);
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
return roots;
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
type RestrictionCommandDialogProps = {
|
|
113
|
+
label: string;
|
|
114
|
+
items: RestrictionTreeNode[];
|
|
115
|
+
restrictField: string;
|
|
116
|
+
selectedRestrictionIds: Set<string>;
|
|
117
|
+
lang: string;
|
|
118
|
+
highlighted?: boolean;
|
|
119
|
+
formatLabel?: (label: string) => string;
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const RestrictionCommandDialog: FC<RestrictionCommandDialogProps> = ({
|
|
123
|
+
label,
|
|
124
|
+
items,
|
|
125
|
+
restrictField,
|
|
126
|
+
selectedRestrictionIds,
|
|
127
|
+
lang,
|
|
128
|
+
highlighted = false,
|
|
129
|
+
formatLabel,
|
|
130
|
+
}) => {
|
|
131
|
+
const fmt = (raw: string) => formatLabel ? formatLabel(raw) : raw;
|
|
132
|
+
const [open, setOpen] = useState(false);
|
|
133
|
+
const [search, setSearch] = useState("");
|
|
134
|
+
const listRef = useRef<HTMLDivElement>(null);
|
|
135
|
+
const [restrict, setRestrict] = useQueryState("restrict", { shallow: false, history: "push" });
|
|
136
|
+
const startSearchNavigation = useSearchNavigationStore((state) => state.start);
|
|
137
|
+
const t = useTranslations();
|
|
138
|
+
const device = useBreakpoint();
|
|
139
|
+
|
|
140
|
+
useEffect(() => {
|
|
141
|
+
if (listRef.current) {
|
|
142
|
+
listRef.current.scrollTop = 0;
|
|
143
|
+
}
|
|
144
|
+
}, [search]);
|
|
145
|
+
|
|
146
|
+
const filteredItems = useMemo(() => {
|
|
147
|
+
if (!search) return items;
|
|
148
|
+
const lower = search.toLowerCase();
|
|
149
|
+
return items.flatMap((node) => {
|
|
150
|
+
const groupLabel = getLabelByLang(node.item.labels, lang) ?? node.item.shortId ?? "";
|
|
151
|
+
const matchingChildren = node.children.filter((child) => {
|
|
152
|
+
const childLabel = getLabelByLang(child.item.labels, lang) ?? child.item.shortId ?? "";
|
|
153
|
+
return childLabel.toLowerCase().includes(lower);
|
|
154
|
+
});
|
|
155
|
+
if (groupLabel.toLowerCase().includes(lower)) {
|
|
156
|
+
return [node];
|
|
157
|
+
}
|
|
158
|
+
if (matchingChildren.length > 0) {
|
|
159
|
+
return [{ ...node, children: matchingChildren }];
|
|
160
|
+
}
|
|
161
|
+
return [];
|
|
162
|
+
});
|
|
163
|
+
}, [items, search, lang]);
|
|
164
|
+
|
|
165
|
+
const handleSelect = (shortId: string, isSelected: boolean) => {
|
|
166
|
+
startSearchNavigation();
|
|
167
|
+
if (isSelected) {
|
|
168
|
+
const restrictionsLength = restrict?.split(",").length ?? 0;
|
|
169
|
+
if (restrictionsLength <= 1) {
|
|
170
|
+
setRestrict(null);
|
|
171
|
+
} else {
|
|
172
|
+
const updated = restrict!
|
|
173
|
+
.replace(shortId, "")
|
|
174
|
+
.replace(",,", ",")
|
|
175
|
+
.replace(/(^,)|(,$)/g, "");
|
|
176
|
+
setRestrict(/^[^=]+=$/.test(updated) ? null : updated);
|
|
177
|
+
}
|
|
178
|
+
} else {
|
|
179
|
+
setRestrict(restrict ? `${restrict},${shortId}` : `${restrictField}=${shortId}`);
|
|
180
|
+
}
|
|
181
|
+
setOpen(false);
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
return (
|
|
185
|
+
<>
|
|
186
|
+
<TooltipProvider delayDuration={300}>
|
|
187
|
+
<Button
|
|
188
|
+
variant={highlighted ? "default" : "outline"}
|
|
189
|
+
rounded="full"
|
|
190
|
+
onClick={() => setOpen(true)}
|
|
191
|
+
className="cursor-pointer gap-1"
|
|
192
|
+
>
|
|
193
|
+
<Tooltip>
|
|
194
|
+
<TooltipTrigger asChild>
|
|
195
|
+
<span className="items-center gap-2">
|
|
196
|
+
{(
|
|
197
|
+
device == DEVICE_OPTIONS.MOBILE && label.length > 15
|
|
198
|
+
) ? `${label.slice(0, 12)}...` : label}
|
|
199
|
+
{highlighted && (
|
|
200
|
+
<span aria-hidden="true" className="h-2 w-2 rounded-full bg-primary/55" />
|
|
201
|
+
)}
|
|
202
|
+
</span>
|
|
203
|
+
</TooltipTrigger>
|
|
204
|
+
<TooltipContent>{label}</TooltipContent>
|
|
205
|
+
</Tooltip>
|
|
206
|
+
<ChevronDown className="h-3 w-3 opacity-60" />
|
|
207
|
+
</Button>
|
|
208
|
+
</TooltipProvider>
|
|
209
|
+
|
|
210
|
+
<Dialog open={open} onOpenChange={setOpen}>
|
|
211
|
+
<DialogContent className="overflow-hidden p-0">
|
|
212
|
+
<Command shouldFilter={false} className="[&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
|
|
213
|
+
<CommandInput placeholder={t("search")} value={search} onValueChange={setSearch} />
|
|
214
|
+
<CommandList ref={listRef}>
|
|
215
|
+
<CommandEmpty>{t("results.noResultsTitle")}</CommandEmpty>
|
|
216
|
+
{filteredItems.map((node) => {
|
|
217
|
+
const groupShortId = node.item.shortId ?? "";
|
|
218
|
+
const groupLabel = fmt(getLabelByLang(node.item.labels, lang) ?? groupShortId);
|
|
219
|
+
const groupIsSelected = selectedRestrictionIds.has(groupShortId);
|
|
220
|
+
|
|
221
|
+
return (
|
|
222
|
+
<>
|
|
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>
|
|
232
|
+
{node.children.map((child) => {
|
|
233
|
+
const shortId = child.item.shortId ?? "";
|
|
234
|
+
const childLabel = fmt(getLabelByLang(child.item.labels, lang) ?? shortId);
|
|
235
|
+
const isSelected = selectedRestrictionIds.has(shortId);
|
|
236
|
+
return (
|
|
237
|
+
<CommandItem
|
|
238
|
+
key={shortId}
|
|
239
|
+
value={`${groupLabel}/${childLabel}`}
|
|
240
|
+
onSelect={() => handleSelect(shortId, isSelected)}
|
|
241
|
+
className="cursor-pointer !pl-8"
|
|
242
|
+
>
|
|
243
|
+
<span className={cn("flex-1", isSelected && "font-bold")}>{childLabel}</span>
|
|
244
|
+
{isSelected && <Check className="h-4 w-4 shrink-0 text-primary" />}
|
|
245
|
+
</CommandItem>
|
|
246
|
+
);
|
|
247
|
+
})}
|
|
248
|
+
</>
|
|
249
|
+
);
|
|
250
|
+
})}
|
|
251
|
+
</CommandList>
|
|
252
|
+
</Command>
|
|
253
|
+
</DialogContent>
|
|
254
|
+
</Dialog>
|
|
255
|
+
</>
|
|
256
|
+
);
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
export const RestrictionSelectionCommandMenu: FC<Props> = ({
|
|
260
|
+
items,
|
|
261
|
+
restrictField,
|
|
262
|
+
enableHierarchy = false,
|
|
263
|
+
hasMoreItems = false,
|
|
264
|
+
showAllWhenEmpty = true,
|
|
265
|
+
onRequestMore,
|
|
266
|
+
stripLabelPrefix,
|
|
267
|
+
navigationMenuListClassName = "items-center justify-between flex-row",
|
|
268
|
+
itemsByRow = {
|
|
269
|
+
[DEVICE_OPTIONS.MOBILE]: 2,
|
|
270
|
+
[DEVICE_OPTIONS.TABLET]: 4,
|
|
271
|
+
[DEVICE_OPTIONS.DESKTOP]: 7,
|
|
272
|
+
}
|
|
273
|
+
}) => {
|
|
274
|
+
const t = useTranslations();
|
|
275
|
+
const setRestrictionList = useRestrictionStore((state) => state.setRestrictionList);
|
|
276
|
+
const formatLabel = stripLabelPrefix
|
|
277
|
+
? (label: string) => label.replace(new RegExp(`^${stripLabelPrefix}`, "i"), "")
|
|
278
|
+
: undefined;
|
|
279
|
+
|
|
280
|
+
const [params] = useQueryStates({
|
|
281
|
+
restrict: parseAsString,
|
|
282
|
+
}, {
|
|
283
|
+
history: 'push',
|
|
284
|
+
shallow: false,
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
const restrictionValues = useMemo(
|
|
288
|
+
() => params.restrict?.split(`${restrictField}=`)[1]?.split(",") || [],
|
|
289
|
+
[params.restrict, restrictField]
|
|
290
|
+
);
|
|
291
|
+
const selectedRestrictionIds = useMemo(() => new Set(restrictionValues), [restrictionValues]);
|
|
292
|
+
|
|
293
|
+
const uiLang = useLocale();
|
|
294
|
+
const lang = uiLang?.split("-")[0] ?? "";
|
|
295
|
+
|
|
296
|
+
useEffect(() => {
|
|
297
|
+
const map = new Map<string, string>();
|
|
298
|
+
items.forEach((item) => {
|
|
299
|
+
const label = getLabelByLang(item.labels, lang);
|
|
300
|
+
if (item.shortId && label) {
|
|
301
|
+
map.set(item.shortId, label);
|
|
302
|
+
}
|
|
303
|
+
});
|
|
304
|
+
setRestrictionList(map);
|
|
305
|
+
}, [items, lang, setRestrictionList]);
|
|
306
|
+
|
|
307
|
+
const sortedItems = useMemo(() => {
|
|
308
|
+
return [...items].sort((a, b) => {
|
|
309
|
+
const aIndex = restrictionValues.indexOf(a.shortId || "");
|
|
310
|
+
const bIndex = restrictionValues.indexOf(b.shortId || "");
|
|
311
|
+
if (aIndex === -1 && bIndex === -1) return 0;
|
|
312
|
+
if (aIndex === -1) return 1;
|
|
313
|
+
if (bIndex === -1) return -1;
|
|
314
|
+
return aIndex - bIndex;
|
|
315
|
+
});
|
|
316
|
+
}, [items, restrictionValues]);
|
|
317
|
+
|
|
318
|
+
const hierarchyRoots = useMemo(() => {
|
|
319
|
+
if (!enableHierarchy) return [];
|
|
320
|
+
return buildRestrictionTree(sortedItems);
|
|
321
|
+
}, [enableHierarchy, sortedItems]);
|
|
322
|
+
|
|
323
|
+
const device = useBreakpoint();
|
|
324
|
+
const [visibleCount, setVisibleCount] = useState(0);
|
|
325
|
+
const [visibleItems, setVisibleItems] = useState(sortedItems.slice(0, visibleCount));
|
|
326
|
+
const [hiddenItems, setHiddenItems] = useState(sortedItems.slice(visibleCount));
|
|
327
|
+
|
|
328
|
+
useEffect(() => {
|
|
329
|
+
if (device == null) return;
|
|
330
|
+
setVisibleCount(itemsByRow[device as keyof typeof DEVICE_OPTIONS] as number);
|
|
331
|
+
}, [device, itemsByRow]);
|
|
332
|
+
|
|
333
|
+
useEffect(() => {
|
|
334
|
+
if (visibleCount === 0) return;
|
|
335
|
+
|
|
336
|
+
if (enableHierarchy) {
|
|
337
|
+
const roots = hierarchyRoots.map((node) => node.item);
|
|
338
|
+
setVisibleItems(roots.slice(0, visibleCount));
|
|
339
|
+
setHiddenItems(roots.slice(visibleCount));
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
setVisibleItems(sortedItems.slice(0, visibleCount));
|
|
344
|
+
setHiddenItems(sortedItems.slice(visibleCount));
|
|
345
|
+
}, [enableHierarchy, hierarchyRoots, sortedItems, visibleCount]);
|
|
346
|
+
|
|
347
|
+
const visibleHierarchyRoots = useMemo(() => {
|
|
348
|
+
if (!enableHierarchy) return [];
|
|
349
|
+
const visibleRootShortIds = new Set(visibleItems.map((item) => item.shortId).filter(Boolean));
|
|
350
|
+
return hierarchyRoots.filter((root) => root.item.shortId && visibleRootShortIds.has(root.item.shortId));
|
|
351
|
+
}, [enableHierarchy, hierarchyRoots, visibleItems]);
|
|
352
|
+
|
|
353
|
+
const hiddenHierarchyRoots = useMemo(() => {
|
|
354
|
+
if (!enableHierarchy) return [];
|
|
355
|
+
const hiddenRootShortIds = new Set(hiddenItems.map((item) => item.shortId).filter(Boolean));
|
|
356
|
+
return hierarchyRoots.filter((root) => root.item.shortId && hiddenRootShortIds.has(root.item.shortId));
|
|
357
|
+
}, [enableHierarchy, hierarchyRoots, hiddenItems]);
|
|
358
|
+
|
|
359
|
+
const hasHiddenContent = (enableHierarchy ? hiddenHierarchyRoots.length > 0 : hiddenItems.length > 0) || hasMoreItems;
|
|
360
|
+
|
|
361
|
+
return (
|
|
362
|
+
<NavigationMenu viewport={false} className="max-w-full w-full c-rex-restriction-menu">
|
|
363
|
+
<NavigationMenuList className={cn("w-full", navigationMenuListClassName)}>
|
|
364
|
+
|
|
365
|
+
{(showAllWhenEmpty || items.length > 0) && (
|
|
366
|
+
<RestrictionNavigationItem
|
|
367
|
+
removeRestrictParam
|
|
368
|
+
label={t('all')}
|
|
369
|
+
selected={restrictionValues.length === 0}
|
|
370
|
+
/>
|
|
371
|
+
)}
|
|
372
|
+
|
|
373
|
+
{!enableHierarchy && visibleItems.map((item) => {
|
|
374
|
+
const rawLabel = getLabelByLang(item.labels, lang);
|
|
375
|
+
const label = rawLabel && formatLabel ? formatLabel(rawLabel) : rawLabel;
|
|
376
|
+
return (
|
|
377
|
+
<RestrictionNavigationItem
|
|
378
|
+
key={item.shortId}
|
|
379
|
+
shortId={item.shortId!}
|
|
380
|
+
restrictField={restrictField}
|
|
381
|
+
label={label}
|
|
382
|
+
selected={restrictionValues.includes(item.shortId!)}
|
|
383
|
+
/>
|
|
384
|
+
);
|
|
385
|
+
})}
|
|
386
|
+
|
|
387
|
+
{enableHierarchy && visibleHierarchyRoots.map((rootNode) => {
|
|
388
|
+
const shortId = rootNode.item.shortId || "";
|
|
389
|
+
const hasChildren = rootNode.children.length > 0;
|
|
390
|
+
const rawLabel = getLabelByLang(rootNode.item.labels, lang);
|
|
391
|
+
const label = rawLabel && formatLabel ? formatLabel(rawLabel) : rawLabel;
|
|
392
|
+
const rootSelected = restrictionValues.includes(shortId);
|
|
393
|
+
const hasActiveDescendant = hasChildren && hasSelectedDescendant(rootNode, selectedRestrictionIds);
|
|
394
|
+
const shouldHighlightBranch = hasActiveDescendant && !rootSelected;
|
|
395
|
+
|
|
396
|
+
if (!hasChildren) {
|
|
397
|
+
return (
|
|
398
|
+
<RestrictionNavigationItem
|
|
399
|
+
key={shortId}
|
|
400
|
+
shortId={shortId}
|
|
401
|
+
restrictField={restrictField}
|
|
402
|
+
label={label}
|
|
403
|
+
selected={rootSelected}
|
|
404
|
+
/>
|
|
405
|
+
);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
return (
|
|
409
|
+
<NavigationMenuItem key={`root-command-${shortId}`}>
|
|
410
|
+
<RestrictionCommandDialog
|
|
411
|
+
label={label}
|
|
412
|
+
items={rootNode.children}
|
|
413
|
+
restrictField={restrictField}
|
|
414
|
+
selectedRestrictionIds={selectedRestrictionIds}
|
|
415
|
+
lang={lang}
|
|
416
|
+
highlighted={shouldHighlightBranch}
|
|
417
|
+
formatLabel={formatLabel}
|
|
418
|
+
/>
|
|
419
|
+
</NavigationMenuItem>
|
|
420
|
+
);
|
|
421
|
+
})}
|
|
422
|
+
|
|
423
|
+
{hasHiddenContent && (
|
|
424
|
+
<NavigationMenuItem>
|
|
425
|
+
<RestrictionCommandDialog
|
|
426
|
+
label={t('more')}
|
|
427
|
+
items={
|
|
428
|
+
enableHierarchy
|
|
429
|
+
? hiddenHierarchyRoots
|
|
430
|
+
: hiddenItems.map((item) => ({ item, children: [] }))
|
|
431
|
+
}
|
|
432
|
+
restrictField={restrictField}
|
|
433
|
+
selectedRestrictionIds={selectedRestrictionIds}
|
|
434
|
+
lang={lang}
|
|
435
|
+
highlighted={false}
|
|
436
|
+
formatLabel={formatLabel}
|
|
437
|
+
/>
|
|
438
|
+
</NavigationMenuItem>
|
|
439
|
+
)}
|
|
440
|
+
|
|
441
|
+
</NavigationMenuList>
|
|
442
|
+
</NavigationMenu>
|
|
443
|
+
);
|
|
444
|
+
};
|
|
@@ -120,7 +120,7 @@ export const RestrictionSelectionMenu: FC<Props> = ({
|
|
|
120
120
|
navigationMenuListClassName = "items-center justify-between flex-row",
|
|
121
121
|
itemsByRow = {
|
|
122
122
|
[DEVICE_OPTIONS.MOBILE]: 2,
|
|
123
|
-
[DEVICE_OPTIONS.TABLET]:
|
|
123
|
+
[DEVICE_OPTIONS.TABLET]: 4,
|
|
124
124
|
[DEVICE_OPTIONS.DESKTOP]: 7,
|
|
125
125
|
}
|
|
126
126
|
}) => {
|
|
@@ -326,7 +326,7 @@ export const RestrictionSelectionMenu: FC<Props> = ({
|
|
|
326
326
|
shouldHighlightBranch && "border border-primary/20 bg-primary/5 text-primary shadow-sm"
|
|
327
327
|
)}
|
|
328
328
|
>
|
|
329
|
-
<span className="
|
|
329
|
+
<span className="max-w-40 overflow-hidden text-ellipsis items-center gap-2">
|
|
330
330
|
{label}
|
|
331
331
|
{shouldHighlightBranch ? (
|
|
332
332
|
<span
|
|
@@ -378,6 +378,4 @@ export const RestrictionSelectionMenu: FC<Props> = ({
|
|
|
378
378
|
</NavigationMenuList>
|
|
379
379
|
</NavigationMenu>
|
|
380
380
|
);
|
|
381
|
-
};
|
|
382
|
-
|
|
383
|
-
export const RestrictionMenu = RestrictionSelectionMenu;
|
|
381
|
+
};
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { DomainEntityModel } from "@c-rex/interfaces";
|
|
4
|
+
import { FC, ReactNode, useMemo, useState } from "react";
|
|
5
|
+
import * as ComponentOptions from "../generated/client-components";
|
|
6
|
+
import { Skeleton } from "@c-rex/ui/skeleton";
|
|
7
|
+
import { RestrictionSelectionCommandMenu } from "./restriction-selection-command-menu";
|
|
8
|
+
|
|
9
|
+
type GenericRequestData = {
|
|
10
|
+
items?: DomainEntityModel[];
|
|
11
|
+
pageInfo?: {
|
|
12
|
+
totalItemCount?: number;
|
|
13
|
+
};
|
|
14
|
+
} | null | undefined;
|
|
15
|
+
type GenericQueryParams = Record<string, unknown> & { Restrict?: string[]; PageSize?: number; Fields?: string[] };
|
|
16
|
+
type GenericRequestProps = {
|
|
17
|
+
queryParams?: GenericQueryParams;
|
|
18
|
+
children: (props: { data?: GenericRequestData; isLoading?: boolean }) => ReactNode;
|
|
19
|
+
};
|
|
20
|
+
type RestrictionMenuFetchMode = "all" | "deferred";
|
|
21
|
+
|
|
22
|
+
export type TaxonomyRestrictionCommandMenuProps = {
|
|
23
|
+
restrictField: string;
|
|
24
|
+
navigationMenuListClassName?: string;
|
|
25
|
+
itemsToRender?: number;
|
|
26
|
+
requestType: keyof typeof ComponentOptions;
|
|
27
|
+
onlyUsedEntries?: boolean;
|
|
28
|
+
enableHierarchy?: boolean;
|
|
29
|
+
fetchMode?: RestrictionMenuFetchMode;
|
|
30
|
+
showAllWhenEmpty?: boolean;
|
|
31
|
+
queryParams?: GenericQueryParams;
|
|
32
|
+
stripLabelPrefix?: string;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export const TaxonomyRestrictionCommandMenu: FC<TaxonomyRestrictionCommandMenuProps> = ({
|
|
36
|
+
queryParams,
|
|
37
|
+
restrictField,
|
|
38
|
+
itemsToRender = 7,
|
|
39
|
+
requestType,
|
|
40
|
+
onlyUsedEntries = true,
|
|
41
|
+
enableHierarchy = false,
|
|
42
|
+
fetchMode = "deferred",
|
|
43
|
+
showAllWhenEmpty = true,
|
|
44
|
+
navigationMenuListClassName = "items-center justify-between flex-row",
|
|
45
|
+
stripLabelPrefix,
|
|
46
|
+
}) => {
|
|
47
|
+
const [loadAll, setLoadAll] = useState(false);
|
|
48
|
+
const RequestComponent = ComponentOptions[requestType] as unknown as FC<GenericRequestProps>;
|
|
49
|
+
const queryRestrict = queryParams?.Restrict || [];
|
|
50
|
+
const restrict = onlyUsedEntries ? ["hasInformationUnits=true", ...queryRestrict] : queryRestrict;
|
|
51
|
+
const explicitPageSize =
|
|
52
|
+
Number.isFinite(Number(queryParams?.PageSize)) && Number(queryParams?.PageSize) > 0
|
|
53
|
+
? Number(queryParams?.PageSize)
|
|
54
|
+
: undefined;
|
|
55
|
+
const resolvedPageSize = useMemo(() => {
|
|
56
|
+
if (explicitPageSize) return explicitPageSize;
|
|
57
|
+
if (fetchMode === "deferred" && !loadAll) return Math.max(itemsToRender, 1);
|
|
58
|
+
return 100;
|
|
59
|
+
}, [explicitPageSize, fetchMode, itemsToRender, loadAll]);
|
|
60
|
+
const requestedFields = Array.isArray(queryParams?.Fields) ? queryParams.Fields : undefined;
|
|
61
|
+
const resolvedFields = useMemo(() => {
|
|
62
|
+
const baseFields = requestedFields && requestedFields.length > 0 ? requestedFields : ["labels"];
|
|
63
|
+
if (!enableHierarchy) return baseFields;
|
|
64
|
+
const withParents = new Set(baseFields);
|
|
65
|
+
withParents.add("parents");
|
|
66
|
+
return Array.from(withParents);
|
|
67
|
+
}, [enableHierarchy, requestedFields]);
|
|
68
|
+
|
|
69
|
+
return (
|
|
70
|
+
<RequestComponent
|
|
71
|
+
queryParams={{
|
|
72
|
+
...queryParams,
|
|
73
|
+
Fields: resolvedFields,
|
|
74
|
+
Links: true,
|
|
75
|
+
Sort: ["labels.value"],
|
|
76
|
+
PageSize: resolvedPageSize,
|
|
77
|
+
Restrict: restrict,
|
|
78
|
+
}}
|
|
79
|
+
>
|
|
80
|
+
{({ data, isLoading }) => {
|
|
81
|
+
if (isLoading) return (
|
|
82
|
+
<Skeleton className="w-full h-9 rounded-full" />
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
if (!data) return null;
|
|
86
|
+
|
|
87
|
+
const itemCount = data.items?.length || 0;
|
|
88
|
+
const totalItemCount = data.pageInfo?.totalItemCount;
|
|
89
|
+
const hasMoreFromServer = typeof totalItemCount === "number" ? totalItemCount > itemCount : false;
|
|
90
|
+
const hasMoreItems = hasMoreFromServer || (explicitPageSize ? false : (fetchMode === "deferred" && !loadAll && itemCount >= resolvedPageSize));
|
|
91
|
+
|
|
92
|
+
return (
|
|
93
|
+
<RestrictionSelectionCommandMenu
|
|
94
|
+
restrictField={restrictField}
|
|
95
|
+
items={data.items || []}
|
|
96
|
+
enableHierarchy={enableHierarchy}
|
|
97
|
+
hasMoreItems={hasMoreItems}
|
|
98
|
+
showAllWhenEmpty={showAllWhenEmpty}
|
|
99
|
+
onRequestMore={() => {
|
|
100
|
+
if (fetchMode === "deferred" && !explicitPageSize) {
|
|
101
|
+
setLoadAll(true);
|
|
102
|
+
}
|
|
103
|
+
}}
|
|
104
|
+
navigationMenuListClassName={navigationMenuListClassName}
|
|
105
|
+
stripLabelPrefix={stripLabelPrefix}
|
|
106
|
+
/>
|
|
107
|
+
);
|
|
108
|
+
}}
|
|
109
|
+
</RequestComponent>
|
|
110
|
+
);
|
|
111
|
+
};
|
|
@@ -77,13 +77,7 @@ export const TaxonomyRestrictionMenu: FC<TaxonomyRestrictionMenuProps> = ({
|
|
|
77
77
|
>
|
|
78
78
|
{({ data, isLoading }) => {
|
|
79
79
|
if (isLoading) return (
|
|
80
|
-
<
|
|
81
|
-
<Skeleton className="w-12 h-9 rounded-full" />
|
|
82
|
-
{Array(itemsToRender).fill(0).map((_, index) => (
|
|
83
|
-
<Skeleton key={`skeleton-${index}`} className="w-28 h-9 rounded-full" />
|
|
84
|
-
))}
|
|
85
|
-
<Skeleton className="w-20 h-9 rounded-full" />
|
|
86
|
-
</div>
|
|
80
|
+
<Skeleton className="w-full h-9 rounded-full" />
|
|
87
81
|
);
|
|
88
82
|
|
|
89
83
|
if (!data) return null;
|