@cosmicdrift/kumiko-renderer-web 0.130.1 → 0.130.2
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 +4 -4
- package/src/__tests__/nav-tree.test.tsx +86 -0
- package/src/layout/nav-tree.tsx +114 -25
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-renderer-web",
|
|
3
|
-
"version": "0.130.
|
|
3
|
+
"version": "0.130.2",
|
|
4
4
|
"description": "Web-platform bindings for @cosmicdrift/kumiko-renderer. HTML default-primitives, browser history-based navigation, EventSource-backed live events, and a one-call createKumikoApp that mounts the whole stack via react-dom.",
|
|
5
5
|
"license": "BUSL-1.1",
|
|
6
6
|
"author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
|
|
@@ -16,9 +16,9 @@
|
|
|
16
16
|
"./styles.css": "./src/styles.css"
|
|
17
17
|
},
|
|
18
18
|
"dependencies": {
|
|
19
|
-
"@cosmicdrift/kumiko-dispatcher-live": "0.130.
|
|
20
|
-
"@cosmicdrift/kumiko-headless": "0.130.
|
|
21
|
-
"@cosmicdrift/kumiko-renderer": "0.130.
|
|
19
|
+
"@cosmicdrift/kumiko-dispatcher-live": "0.130.2",
|
|
20
|
+
"@cosmicdrift/kumiko-headless": "0.130.2",
|
|
21
|
+
"@cosmicdrift/kumiko-renderer": "0.130.2",
|
|
22
22
|
"@radix-ui/react-dialog": "^1.1.15",
|
|
23
23
|
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
|
24
24
|
"@radix-ui/react-label": "^2.1.8",
|
|
@@ -241,6 +241,30 @@ describe("NavTree", () => {
|
|
|
241
241
|
expect(container.querySelectorAll("svg").length).toBe(2);
|
|
242
242
|
});
|
|
243
243
|
|
|
244
|
+
test("server, mail, download, rocket lösen auf ein Icon auf (Config-/SMTP-Nav)", () => {
|
|
245
|
+
// Config-Settings-Hub leitet Child-Nav-Icons aus dem mask.icon der
|
|
246
|
+
// ConfigKey ab (smtp-host="server", from="mail", subscription="rocket");
|
|
247
|
+
// fehlten sie in NAV_ICONS, rendert das Nav blank statt Icon.
|
|
248
|
+
const schema = {
|
|
249
|
+
featureName: "app",
|
|
250
|
+
entities: {},
|
|
251
|
+
screens: [
|
|
252
|
+
{ id: "a", type: "entityList", entity: "x", columns: [] },
|
|
253
|
+
{ id: "b", type: "entityList", entity: "x", columns: [] },
|
|
254
|
+
{ id: "c", type: "entityList", entity: "x", columns: [] },
|
|
255
|
+
{ id: "d", type: "entityList", entity: "x", columns: [] },
|
|
256
|
+
],
|
|
257
|
+
navs: [
|
|
258
|
+
{ id: "a", label: "SMTP", screen: "a", order: 10, icon: "server" },
|
|
259
|
+
{ id: "b", label: "From", screen: "b", order: 20, icon: "mail" },
|
|
260
|
+
{ id: "c", label: "Export", screen: "c", order: 30, icon: "download" },
|
|
261
|
+
{ id: "d", label: "Billing", screen: "d", order: 40, icon: "rocket" },
|
|
262
|
+
],
|
|
263
|
+
} as FeatureSchema;
|
|
264
|
+
const { container } = render(<NavTree schema={schema} />);
|
|
265
|
+
expect(container.querySelectorAll("svg").length).toBe(4);
|
|
266
|
+
});
|
|
267
|
+
|
|
244
268
|
test("palette, link und share rendern Lucide-Icons (Share-/Branding-Nav)", () => {
|
|
245
269
|
const schema = {
|
|
246
270
|
featureName: "app",
|
|
@@ -542,3 +566,65 @@ describe("NavTree dynamic provider nodes", () => {
|
|
|
542
566
|
expect(active()).toBe(0); // Unmount baut alles ab → kein Leak
|
|
543
567
|
});
|
|
544
568
|
});
|
|
569
|
+
|
|
570
|
+
describe("NavTree Suchfeld", () => {
|
|
571
|
+
// Das Suchfeld ist das einzige <input> im Baum; über die Rolle statt den
|
|
572
|
+
// i18n-Placeholder gefunden (i18n-Bundle resolvt im Worktree gegen den
|
|
573
|
+
// Haupt-Checkout, wo der neue Key noch fehlt — in CI greift er).
|
|
574
|
+
|
|
575
|
+
test("Filter blendet Nicht-Treffer aus, hält Treffer + deren Ancestors", () => {
|
|
576
|
+
render(<NavTree schema={makeSchema()} />);
|
|
577
|
+
const input = screen.getByRole("textbox") as HTMLInputElement;
|
|
578
|
+
|
|
579
|
+
fireEvent.change(input, { target: { value: "active" } });
|
|
580
|
+
|
|
581
|
+
// Treffer selbst sichtbar (case-insensitiv).
|
|
582
|
+
expect(screen.getByText("Active")).toBeTruthy();
|
|
583
|
+
// Nicht-Treffer-Geschwister weg.
|
|
584
|
+
expect(screen.queryByText("Backlog")).toBeNull();
|
|
585
|
+
// Ancestors bleiben, damit der Treffer erreichbar bleibt.
|
|
586
|
+
expect(screen.getByText("Items")).toBeTruthy();
|
|
587
|
+
expect(screen.getByText("Data")).toBeTruthy();
|
|
588
|
+
});
|
|
589
|
+
|
|
590
|
+
test("Filter ohne Treffer leert den Baum; Leeren stellt alles wieder her", () => {
|
|
591
|
+
render(<NavTree schema={makeSchema()} />);
|
|
592
|
+
const input = screen.getByRole("textbox") as HTMLInputElement;
|
|
593
|
+
|
|
594
|
+
fireEvent.change(input, { target: { value: "zzz-nichts" } });
|
|
595
|
+
expect(screen.queryByText("Items")).toBeNull();
|
|
596
|
+
expect(screen.queryByText("Data")).toBeNull();
|
|
597
|
+
|
|
598
|
+
fireEvent.change(input, { target: { value: "" } });
|
|
599
|
+
expect(screen.getByText("Backlog")).toBeTruthy();
|
|
600
|
+
expect(screen.getByText("Data")).toBeTruthy();
|
|
601
|
+
});
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
describe("NavTree folder-open Icon", () => {
|
|
605
|
+
test("Folder-Knoten zeigt folder-open wenn expanded, folder wenn collapsed", async () => {
|
|
606
|
+
const provider: TreeChildrenSubscribe = () => (emit) => {
|
|
607
|
+
emit([{ label: "page", icon: "folder", children: [pageLeaf("hero")] }]);
|
|
608
|
+
return () => {};
|
|
609
|
+
};
|
|
610
|
+
let container: HTMLElement | undefined;
|
|
611
|
+
await act(async () => {
|
|
612
|
+
container = renderDynamic({
|
|
613
|
+
schema: dynamicSchema(),
|
|
614
|
+
providers: new Map([["cms:nav:content", provider]]),
|
|
615
|
+
}).container;
|
|
616
|
+
});
|
|
617
|
+
// Default-expanded → offener Ordner, kein geschlossener.
|
|
618
|
+
expect(container?.querySelector(".lucide-folder-open")).toBeTruthy();
|
|
619
|
+
expect(container?.querySelector(".lucide-folder")).toBeNull();
|
|
620
|
+
|
|
621
|
+
// Der einzige aria-gelabelte Chevron gehört dem "page"-Sub-Ordner
|
|
622
|
+
// (der Provider-Container toggelt inline, ohne aria-Label).
|
|
623
|
+
const chevron = screen.getByRole("button", { name: /Expand|Collapse/ });
|
|
624
|
+
await act(async () => {
|
|
625
|
+
fireEvent.click(chevron);
|
|
626
|
+
});
|
|
627
|
+
expect(container?.querySelector(".lucide-folder")).toBeTruthy();
|
|
628
|
+
expect(container?.querySelector(".lucide-folder-open")).toBeNull();
|
|
629
|
+
});
|
|
630
|
+
});
|
package/src/layout/nav-tree.tsx
CHANGED
|
@@ -33,9 +33,12 @@ import {
|
|
|
33
33
|
ChevronRight,
|
|
34
34
|
Coins,
|
|
35
35
|
CreditCard,
|
|
36
|
+
Download,
|
|
36
37
|
FileText,
|
|
37
38
|
Folder,
|
|
39
|
+
FolderOpen,
|
|
38
40
|
Gauge,
|
|
41
|
+
Hash,
|
|
39
42
|
Home,
|
|
40
43
|
KeyRound,
|
|
41
44
|
Layers,
|
|
@@ -43,11 +46,15 @@ import {
|
|
|
43
46
|
LineChart,
|
|
44
47
|
Link,
|
|
45
48
|
List,
|
|
49
|
+
Lock,
|
|
50
|
+
Mail,
|
|
46
51
|
Palette,
|
|
47
52
|
PiggyBank,
|
|
48
53
|
Plus,
|
|
49
54
|
Receipt,
|
|
55
|
+
Rocket,
|
|
50
56
|
Search,
|
|
57
|
+
Server,
|
|
51
58
|
Settings,
|
|
52
59
|
Share2,
|
|
53
60
|
Shield,
|
|
@@ -76,6 +83,7 @@ import {
|
|
|
76
83
|
SidebarGroup,
|
|
77
84
|
SidebarGroupContent,
|
|
78
85
|
SidebarGroupLabel,
|
|
86
|
+
SidebarInput,
|
|
79
87
|
SidebarMenu,
|
|
80
88
|
SidebarMenuAction,
|
|
81
89
|
SidebarMenuButton,
|
|
@@ -113,6 +121,7 @@ const NAV_ICONS: Readonly<Record<string, typeof Folder>> = {
|
|
|
113
121
|
calendar: CalendarDays,
|
|
114
122
|
file: FileText,
|
|
115
123
|
folder: Folder,
|
|
124
|
+
"folder-open": FolderOpen,
|
|
116
125
|
home: Home,
|
|
117
126
|
bell: Bell,
|
|
118
127
|
shield: Shield,
|
|
@@ -125,6 +134,12 @@ const NAV_ICONS: Readonly<Record<string, typeof Folder>> = {
|
|
|
125
134
|
link: Link,
|
|
126
135
|
palette: Palette,
|
|
127
136
|
share: Share2,
|
|
137
|
+
server: Server,
|
|
138
|
+
mail: Mail,
|
|
139
|
+
lock: Lock,
|
|
140
|
+
hash: Hash,
|
|
141
|
+
download: Download,
|
|
142
|
+
rocket: Rocket,
|
|
128
143
|
};
|
|
129
144
|
|
|
130
145
|
export type NavTreeProps = {
|
|
@@ -149,6 +164,22 @@ export type NavTreeProps = {
|
|
|
149
164
|
const EMPTY_BADGES: ReadonlyMap<string, ReactNode> = new Map();
|
|
150
165
|
const NavBadgesContext = createContext<ReadonlyMap<string, ReactNode>>(EMPTY_BADGES);
|
|
151
166
|
|
|
167
|
+
// Sidebar-Filter: `q` leer = inaktiv (matches akzeptiert alles). `matches`
|
|
168
|
+
// bekommt das ROHE Label (i18n-Key oder Klartext) und übersetzt intern — so
|
|
169
|
+
// filtert das Suchfeld auf den sichtbaren Text, nicht den Key.
|
|
170
|
+
type NavFilter = { readonly q: string; readonly matches: (rawLabel: string) => boolean };
|
|
171
|
+
const NavFilterContext = createContext<NavFilter>({ q: "", matches: () => true });
|
|
172
|
+
|
|
173
|
+
// Ein Knoten überlebt den Filter, wenn er selbst oder ein Nachfahre matcht.
|
|
174
|
+
// Provider-Kinder sind zur Filterzeit schon materialisiert (Provider-Knoten
|
|
175
|
+
// sind default-expanded → eager geladen), darum reicht die statische
|
|
176
|
+
// children-Rekursion. ponytail: ein zur Filterzeit noch NICHT geladener
|
|
177
|
+
// (user-zugeklappter) Provider wird über den force-expand in useNavNodeState
|
|
178
|
+
// aufgeklappt, lädt und filtert dann mit.
|
|
179
|
+
function subtreeMatches(node: NavNode, matches: (rawLabel: string) => boolean): boolean {
|
|
180
|
+
return matches(node.label) || node.children.some((c) => subtreeMatches(c, matches));
|
|
181
|
+
}
|
|
182
|
+
|
|
152
183
|
export function NavTree({
|
|
153
184
|
schema,
|
|
154
185
|
user,
|
|
@@ -175,25 +206,47 @@ export function NavTree({
|
|
|
175
206
|
});
|
|
176
207
|
}, []);
|
|
177
208
|
|
|
209
|
+
const t = useTranslation();
|
|
210
|
+
const [filter, setFilter] = useState("");
|
|
211
|
+
const q = filter.trim().toLowerCase();
|
|
212
|
+
const matches = useCallback(
|
|
213
|
+
(raw: string): boolean => {
|
|
214
|
+
const label = raw.includes(".") ? t(raw) : raw;
|
|
215
|
+
return label.toLowerCase().includes(q);
|
|
216
|
+
},
|
|
217
|
+
[t, q],
|
|
218
|
+
);
|
|
219
|
+
const navFilter = useMemo<NavFilter>(() => ({ q, matches }), [q, matches]);
|
|
220
|
+
|
|
178
221
|
return (
|
|
179
|
-
<
|
|
180
|
-
<
|
|
181
|
-
{tree
|
|
182
|
-
|
|
183
|
-
<
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
222
|
+
<NavFilterContext.Provider value={navFilter}>
|
|
223
|
+
<NavBadgesContext.Provider value={navBadges ?? EMPTY_BADGES}>
|
|
224
|
+
<div data-testid={testId} data-kumiko-layout="nav-tree" className="flex w-full flex-col">
|
|
225
|
+
<div className="px-2 pt-2 pb-1">
|
|
226
|
+
<SidebarInput
|
|
227
|
+
value={filter}
|
|
228
|
+
onChange={(e) => setFilter(e.target.value)}
|
|
229
|
+
placeholder={t("kumiko.nav.search")}
|
|
230
|
+
aria-label={t("kumiko.nav.search")}
|
|
188
231
|
/>
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
232
|
+
</div>
|
|
233
|
+
{tree.map((node) =>
|
|
234
|
+
isPureSection(node) ? (
|
|
235
|
+
<NavSection
|
|
236
|
+
key={node.qualifiedName}
|
|
237
|
+
node={node}
|
|
238
|
+
collapsed={collapsed}
|
|
239
|
+
onToggle={onToggle}
|
|
240
|
+
/>
|
|
241
|
+
) : (
|
|
242
|
+
<SidebarMenu key={node.qualifiedName} className="px-2 py-1">
|
|
243
|
+
<NavMenuNode node={node} collapsed={collapsed} onToggle={onToggle} />
|
|
244
|
+
</SidebarMenu>
|
|
245
|
+
),
|
|
246
|
+
)}
|
|
247
|
+
</div>
|
|
248
|
+
</NavBadgesContext.Provider>
|
|
249
|
+
</NavFilterContext.Provider>
|
|
197
250
|
);
|
|
198
251
|
}
|
|
199
252
|
|
|
@@ -245,8 +298,17 @@ function useLabel(node: NavNode): string {
|
|
|
245
298
|
|
|
246
299
|
// Icon-or-Dot: bekannter icon-Key → Lucide-Icon, sonst ein dezenter Dot.
|
|
247
300
|
// Aktiv = accent-foreground, inaktiv = gedimmt.
|
|
248
|
-
function NavLeadingIcon({
|
|
249
|
-
|
|
301
|
+
function NavLeadingIcon({
|
|
302
|
+
node,
|
|
303
|
+
active,
|
|
304
|
+
expanded = false,
|
|
305
|
+
}: {
|
|
306
|
+
node: NavNode;
|
|
307
|
+
active: boolean;
|
|
308
|
+
expanded?: boolean;
|
|
309
|
+
}): ReactNode {
|
|
310
|
+
const iconKey = expanded && node.icon === "folder" ? "folder-open" : node.icon;
|
|
311
|
+
const NavIcon = iconKey !== undefined ? NAV_ICONS[iconKey] : undefined;
|
|
250
312
|
if (NavIcon !== undefined) return <NavIcon aria-hidden="true" className="shrink-0" />;
|
|
251
313
|
return (
|
|
252
314
|
<span
|
|
@@ -366,6 +428,7 @@ type NavNodeState = {
|
|
|
366
428
|
readonly isExpanded: boolean;
|
|
367
429
|
readonly active: boolean;
|
|
368
430
|
readonly childNodes: readonly NavNode[];
|
|
431
|
+
readonly hidden: boolean;
|
|
369
432
|
readonly providerLoading: boolean;
|
|
370
433
|
readonly providerError: string | null;
|
|
371
434
|
readonly workspaceId: string | undefined;
|
|
@@ -377,10 +440,14 @@ type NavNodeState = {
|
|
|
377
440
|
// in den shadcn-Primitives, nicht in dieser Logik.
|
|
378
441
|
function useNavNodeState(node: NavNode, collapsed: ReadonlySet<string>): NavNodeState {
|
|
379
442
|
const nav = useNav();
|
|
443
|
+
const { q: filterQ, matches } = useContext(NavFilterContext);
|
|
444
|
+
const filterActive = filterQ !== "";
|
|
380
445
|
const displayLabel = useLabel(node);
|
|
381
446
|
const isCollapsed = collapsed.has(node.qualifiedName);
|
|
382
447
|
const expandable = node.children.length > 0 || node.provider === true;
|
|
383
|
-
|
|
448
|
+
// Aktiver Filter klappt jeden Container auf, damit Treffer unter zugeklappten
|
|
449
|
+
// Ordnern sichtbar werden (und Provider-Kinder nachladen).
|
|
450
|
+
const isExpanded = expandable && (!isCollapsed || filterActive);
|
|
384
451
|
const { nodes: providerChildren, error: providerError } = useNavProviderChildren(
|
|
385
452
|
node.qualifiedName,
|
|
386
453
|
node.provider === true && isExpanded,
|
|
@@ -393,13 +460,19 @@ function useNavNodeState(node: NavNode, collapsed: ReadonlySet<string>): NavNode
|
|
|
393
460
|
node.screen !== undefined && nav.route?.screenId === lastSegment(node.screen);
|
|
394
461
|
const targetActive = node.target !== undefined && targetsEqual(node.target, activeTarget);
|
|
395
462
|
const childNodes = node.provider === true ? (providerChildren ?? []) : node.children;
|
|
463
|
+
const visibleChildNodes = filterActive
|
|
464
|
+
? childNodes.filter((c) => subtreeMatches(c, matches))
|
|
465
|
+
: childNodes;
|
|
396
466
|
return {
|
|
397
467
|
displayLabel,
|
|
398
468
|
isCollapsed,
|
|
399
469
|
expandable,
|
|
400
470
|
isExpanded,
|
|
401
471
|
active: screenActive || targetActive,
|
|
402
|
-
childNodes,
|
|
472
|
+
childNodes: visibleChildNodes,
|
|
473
|
+
// Ein gefilterter Knoten verschwindet nur, wenn weder er selbst noch ein
|
|
474
|
+
// sichtbar gebliebenes Kind matcht — sonst bliebe ein leerer Ordner-Header.
|
|
475
|
+
hidden: filterActive && !matches(node.label) && visibleChildNodes.length === 0,
|
|
403
476
|
providerLoading: node.provider === true && isExpanded && providerChildren === null,
|
|
404
477
|
providerError,
|
|
405
478
|
workspaceId: nav.route?.workspaceId,
|
|
@@ -488,6 +561,16 @@ function ProviderStatus({
|
|
|
488
561
|
// gehört auf Items MIT children, nicht auf die Section selbst.
|
|
489
562
|
function NavSection({ node, collapsed, onToggle }: NavSubProps): ReactNode {
|
|
490
563
|
const displayLabel = useLabel(node);
|
|
564
|
+
const { q, matches } = useContext(NavFilterContext);
|
|
565
|
+
// Provider-Kinder (statisch leer bis geladen) halten die Section offen — ihr
|
|
566
|
+
// Match lässt sich hier nicht statisch prüfen; der Knoten selbst self-hidet.
|
|
567
|
+
if (
|
|
568
|
+
q !== "" &&
|
|
569
|
+
!matches(node.label) &&
|
|
570
|
+
!node.children.some((c) => c.provider === true || subtreeMatches(c, matches))
|
|
571
|
+
) {
|
|
572
|
+
return null;
|
|
573
|
+
}
|
|
491
574
|
return (
|
|
492
575
|
<SidebarGroup className="py-1">
|
|
493
576
|
<SidebarGroupLabel>{displayLabel}</SidebarGroupLabel>
|
|
@@ -518,6 +601,7 @@ function NavMenuNode({ node, collapsed, onToggle }: NavSubProps): ReactNode {
|
|
|
518
601
|
const t = useTranslation();
|
|
519
602
|
const dispatch = useDispatchTarget();
|
|
520
603
|
const s = useNavNodeState(node, collapsed);
|
|
604
|
+
if (s.hidden) return null;
|
|
521
605
|
|
|
522
606
|
const sub = s.isExpanded ? (
|
|
523
607
|
<SidebarMenuSub>
|
|
@@ -593,7 +677,7 @@ function NavMenuNode({ node, collapsed, onToggle }: NavSubProps): ReactNode {
|
|
|
593
677
|
}}
|
|
594
678
|
{...(s.expandable && { "aria-expanded": !s.isCollapsed })}
|
|
595
679
|
>
|
|
596
|
-
<NavLeadingIcon node={node} active={false} />
|
|
680
|
+
<NavLeadingIcon node={node} active={false} expanded={s.isExpanded} />
|
|
597
681
|
<span className="truncate">{s.displayLabel}</span>
|
|
598
682
|
{s.expandable &&
|
|
599
683
|
(s.isCollapsed ? (
|
|
@@ -617,9 +701,14 @@ function NavSubNode({ node, collapsed, onToggle }: NavSubProps): ReactNode {
|
|
|
617
701
|
const t = useTranslation();
|
|
618
702
|
const dispatch = useDispatchTarget();
|
|
619
703
|
const s = useNavNodeState(node, collapsed);
|
|
704
|
+
if (s.hidden) return null;
|
|
620
705
|
|
|
706
|
+
// Kinder in ein eigenes SidebarMenuSub (<ul>) — ein Sub-Item ist ein <li>,
|
|
707
|
+
// verschachtelte <li> ohne <ul> dazwischen sind invalides HTML (nested
|
|
708
|
+
// Content-Ordner: Marketing/ > Block). Der <ul> trägt zugleich die
|
|
709
|
+
// Einrück-/Border-Stufe pro Tiefe.
|
|
621
710
|
const deeper = s.isExpanded ? (
|
|
622
|
-
|
|
711
|
+
<SidebarMenuSub>
|
|
623
712
|
<ProviderStatus loading={s.providerLoading} error={s.providerError} />
|
|
624
713
|
{s.childNodes.map((child) => (
|
|
625
714
|
<NavSubNode
|
|
@@ -629,7 +718,7 @@ function NavSubNode({ node, collapsed, onToggle }: NavSubProps): ReactNode {
|
|
|
629
718
|
onToggle={onToggle}
|
|
630
719
|
/>
|
|
631
720
|
))}
|
|
632
|
-
|
|
721
|
+
</SidebarMenuSub>
|
|
633
722
|
) : null;
|
|
634
723
|
|
|
635
724
|
const chevron = s.expandable ? (
|
|
@@ -695,7 +784,7 @@ function NavSubNode({ node, collapsed, onToggle }: NavSubProps): ReactNode {
|
|
|
695
784
|
if (s.expandable) onToggle(node.qualifiedName);
|
|
696
785
|
}}
|
|
697
786
|
>
|
|
698
|
-
<NavLeadingIcon node={node} active={false} />
|
|
787
|
+
<NavLeadingIcon node={node} active={false} expanded={s.isExpanded} />
|
|
699
788
|
<span className="truncate">{s.displayLabel}</span>
|
|
700
789
|
</button>
|
|
701
790
|
</SidebarMenuSubButton>
|