@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 +1 -1
- package/src/info/information-unit-metadata-grid-client.tsx +9 -211
- package/src/restriction-menu/__tests__/restriction-hierarchy.test.ts +12 -0
- package/src/restriction-menu/restriction-hierarchy.ts +7 -0
- package/src/restriction-menu/restriction-menu-item.tsx +8 -2
- package/src/restriction-menu/restriction-selection-command-menu.tsx +79 -20
- package/src/restriction-menu/restriction-selection-menu.tsx +91 -17
- package/src/restriction-menu/taxonomy-restriction-command-menu.tsx +4 -1
- package/src/restriction-menu/taxonomy-restriction-menu.tsx +10 -0
- package/src/results/filter-navbar.tsx +7 -4
- package/src/results/filter-sidebar/__tests__/utils.test.ts +299 -3
- package/src/results/filter-sidebar/index.tsx +77 -108
- package/src/results/filter-sidebar/utils.ts +91 -10
- package/src/results/generic/search-results-client.tsx +14 -8
- package/src/taxonomy/__tests__/hierarchy.test.ts +144 -0
- package/src/taxonomy/hierarchy.ts +137 -0
package/package.json
CHANGED
|
@@ -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
|
-
|
|
21
|
-
type
|
|
22
|
-
} from "@c-rex/services/metadata-
|
|
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
|
|
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
|
+
});
|
|
@@ -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
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
)}
|