aril 1.2.4 → 1.2.6

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.
@@ -1,336 +1,336 @@
1
- import { NgClass, NgTemplateOutlet } from '@angular/common';
2
- import { HttpClient } from '@angular/common/http';
3
- import { Component, OnDestroy, OnInit, Signal, signal } from '@angular/core';
4
- import { toSignal } from '@angular/core/rxjs-interop';
5
- import { FormsModule } from '@angular/forms';
6
- import { NavigationEnd, Router, RouterLink } from '@angular/router';
7
-
8
- import { ScrollPanelModule } from 'primeng/scrollpanel';
9
- import { TooltipModule } from 'primeng/tooltip';
10
-
11
- import { TranslocoModule, TranslocoService } from '@ngneat/transloco';
12
- import { Subscription, filter } from 'rxjs';
13
-
14
- import { bridge } from 'aril/boot/bridge';
15
- import { MenuItemAction, PluginMenuItem } from 'aril/boot/config/apps';
16
- import { TranslateJsonPipe } from 'aril/util/pipes';
17
-
18
- import { LayoutService } from '../../service/app.layout.service';
19
- import { AppMenuService } from '../../service/app.menu.service';
20
-
21
- interface MenuNode {
22
- key: string;
23
- label: any;
24
- icon?: string;
25
- routerLink?: string;
26
- action?: MenuItemAction;
27
- children?: MenuNode[];
28
- expanded: boolean;
29
- actionLoading?: boolean;
30
- }
31
-
32
- @Component({
33
- standalone: true,
34
- selector: 'app-static-sidebar',
35
- imports: [RouterLink, NgClass, NgTemplateOutlet, ScrollPanelModule, TooltipModule, FormsModule, TranslocoModule],
36
- templateUrl: './static-sidebar.component.html',
37
- styleUrl: './static-sidebar.component.scss',
38
- providers: [TranslateJsonPipe]
39
- })
40
- export class StaticSidebarComponent implements OnInit, OnDestroy {
41
- activeLang: Signal<string>;
42
- menuNodes = signal<MenuNode[]>([]);
43
- filteredNodes = signal<MenuNode[]>([]);
44
- searchTerm = signal<string>('');
45
-
46
- private routerSubscription: Subscription | null = null;
47
- private actionSubscription: Subscription | null = null;
48
-
49
- private readonly MAX_LENGTH = { level0: 25, level1: 20, level2: 16, level3: 13 };
50
-
51
- constructor(
52
- private readonly menuService: AppMenuService,
53
- private readonly router: Router,
54
- private readonly layoutService: LayoutService,
55
- private readonly translateJsonPipe: TranslateJsonPipe,
56
- private readonly translocoService: TranslocoService,
57
- private readonly http: HttpClient
58
- ) {
59
- this.activeLang = toSignal(this.translocoService.langChanges$, { initialValue: 'tr' });
60
- }
61
-
62
- get isCollapsed(): boolean {
63
- return this.layoutService.isStaticSidebarCollapsed();
64
- }
65
-
66
- get topLevelNodes(): MenuNode[] {
67
- return this.menuNodes();
68
- }
69
-
70
- ngOnInit() {
71
- this.buildMenuTree();
72
- this.routerSubscription = this.router.events
73
- .pipe(filter((event) => event instanceof NavigationEnd))
74
- .subscribe(() => this.buildMenuTree());
75
- }
76
-
77
- ngOnDestroy() {
78
- this.routerSubscription?.unsubscribe();
79
- this.actionSubscription?.unsubscribe();
80
- }
81
-
82
- isTextTruncated(label: any, lang: string | null, level = 0): boolean {
83
- if (!label || !lang || !label[lang]) return false;
84
- const text = label[lang];
85
- const len = text.length;
86
- const limits = [this.MAX_LENGTH.level0, this.MAX_LENGTH.level1, this.MAX_LENGTH.level2, this.MAX_LENGTH.level3];
87
- return len > limits[Math.min(level, 3)];
88
- }
89
-
90
- isMobile(): boolean {
91
- return this.layoutService.isMobile();
92
- }
93
-
94
- buildMenuTree() {
95
- const menuItems = this.getMenuItems();
96
- const nodes = this.convertToMenuNodes(menuItems);
97
- this.menuNodes.set(nodes);
98
- this.filteredNodes.set([...nodes]);
99
- this.expandActiveMenu();
100
- }
101
-
102
- private expandActiveMenu() {
103
- const nodes = this.menuNodes();
104
- this.findAndExpandActiveNode(nodes);
105
- this.filteredNodes.set([...this.filteredNodes()]);
106
- }
107
-
108
- private findAndExpandActiveNode(nodes: MenuNode[]): MenuNode | null {
109
- for (const node of nodes) {
110
- if (this.isActiveRouteForNode(node)) {
111
- return node;
112
- }
113
-
114
- if (node.children && node.children.length > 0) {
115
- const activeChild = this.findAndExpandActiveNode(node.children);
116
- if (activeChild) {
117
- // we find active child and then opened its parent nodes
118
- node.expanded = true;
119
- return activeChild;
120
- }
121
- }
122
- }
123
- return null;
124
- }
125
-
126
- convertToMenuNodes(items: PluginMenuItem[], parentKey = ''): MenuNode[] {
127
- return items
128
- .filter((item) => !item.separator)
129
- .map((item, index) => {
130
- const key = parentKey ? `${parentKey}-${index}` : `${index}`;
131
- const node: MenuNode = {
132
- key,
133
- label: item.label,
134
- icon: item.icon,
135
- routerLink: item.action ? undefined : item.routerLink,
136
- action: item.action,
137
- expanded: false
138
- };
139
- if (item.items && item.items.length > 0) {
140
- node.children = this.convertToMenuNodes(item.items, key);
141
- }
142
- return node;
143
- });
144
- }
145
-
146
- getMenuItems(): PluginMenuItem[] {
147
- return bridge.isHostMode() ? bridge.hostMenuItems() : this.menuService.menuItems();
148
- }
149
-
150
- getLocalText(text: any): string {
151
- return this.translateJsonPipe.transform(text);
152
- }
153
-
154
- isActiveRouteForNode(node: MenuNode): boolean {
155
- if (!node.routerLink) return false;
156
- const currentUrl = this.router.url;
157
- const hashUrl = currentUrl.indexOf('#') > -1 ? currentUrl.split('#')[1] : currentUrl;
158
- if (hashUrl === node.routerLink || hashUrl === '/' + node.routerLink) return true;
159
- return (
160
- node.routerLink !== '/' &&
161
- (hashUrl.startsWith(node.routerLink + '/') || hashUrl.startsWith('/' + node.routerLink + '/'))
162
- );
163
- }
164
-
165
- // use on collapsed mode
166
- isNodeOrChildActive(node: MenuNode): boolean {
167
- if (this.isActiveRouteForNode(node)) {
168
- return true;
169
- }
170
- if (node.children && node.children.length > 0) {
171
- return node.children.some((child) => this.isNodeOrChildActive(child));
172
- }
173
-
174
- return false;
175
- }
176
-
177
- isTopLevelNodeActive(node: MenuNode): boolean {
178
- return this.isNodeOrChildActive(node);
179
- }
180
-
181
- isChildNodeActive(node: MenuNode): boolean {
182
- return this.isActiveRouteForNode(node);
183
- }
184
-
185
- toggleNode(node: MenuNode, level = 0) {
186
- const wasExpanded = node.expanded;
187
- if (level === 0) {
188
- // Diğer ana menüleri kapat
189
- this.closeOtherTopLevelNodes(node);
190
- }
191
- node.expanded = !wasExpanded;
192
-
193
- this.filteredNodes.set([...this.filteredNodes()]);
194
- }
195
-
196
- private closeOtherTopLevelNodes(exceptNode: MenuNode) {
197
- const nodes = this.menuNodes();
198
- nodes.forEach((node) => {
199
- if (node !== exceptNode) {
200
- node.expanded = false;
201
- this.closeAllChildNodes(node);
202
- }
203
- });
204
- }
205
-
206
- private closeAllChildNodes(node: MenuNode) {
207
- if (node.children) {
208
- node.children.forEach((child) => {
209
- child.expanded = false;
210
- this.closeAllChildNodes(child);
211
- });
212
- }
213
- }
214
-
215
- trackByKey(index: number, node: MenuNode): any {
216
- return node.key;
217
- }
218
-
219
- onMenuItemClick() {
220
- if (this.isMobile()) {
221
- this.layoutService.closeMobileMenu();
222
- }
223
-
224
- this.searchTerm.set('');
225
- this.filteredNodes.set(this.menuNodes() || []);
226
- }
227
-
228
- onActionItemClick(node: MenuNode, event: Event) {
229
- event.preventDefault();
230
- event.stopPropagation();
231
-
232
- if (!node.action || node.actionLoading) return;
233
-
234
- // Cancel any previous action request
235
- this.actionSubscription?.unsubscribe();
236
-
237
- node.actionLoading = true;
238
-
239
- const request$ =
240
- node.action.method === 'GET' ?
241
- this.http.get(node.action.url)
242
- : this.http.post(node.action.url, node.action.body ?? {});
243
-
244
- this.actionSubscription = request$.subscribe({
245
- next: (response: any) => {
246
- node.actionLoading = false;
247
- const route = this.buildSuccessRoute(node.action!, response);
248
- this.router.navigate([route]);
249
- this.onMenuItemClick();
250
- },
251
- error: () => {
252
- node.actionLoading = false;
253
- }
254
- });
255
- }
256
-
257
- private buildSuccessRoute(action: MenuItemAction, response: any): string {
258
- if (!action.successRouteResponseKey) return action.successRoute;
259
-
260
- const value = this.getValueByPath(response, action.successRouteResponseKey);
261
- if (value === undefined || value === null) return action.successRoute;
262
-
263
- return `${action.successRoute}/${value}`;
264
- }
265
-
266
- private getValueByPath(obj: any, path: string): any {
267
- if (!obj || !path) return undefined;
268
-
269
- return path.split('.').reduce((current, key) => {
270
- if (current === undefined || current === null) return undefined;
271
-
272
- // Handle array access like 'result[0]'
273
- const arrayMatch = key.match(/^(.+?)\[(\d+)\]$/);
274
- if (arrayMatch) {
275
- const arrayKey = arrayMatch[1];
276
- const index = parseInt(arrayMatch[2], 10);
277
- return current[arrayKey]?.[index];
278
- }
279
-
280
- return current[key];
281
- }, obj);
282
- }
283
-
284
- onCollapsedItemClick(node: MenuNode) {
285
- if (this.isCollapsed) {
286
- this.layoutService.onMenuToggle();
287
- }
288
- this.closeOtherTopLevelNodes(node);
289
- if (node.children && node.children.length > 0) {
290
- node.expanded = true;
291
- }
292
- this.filteredNodes.set([...this.filteredNodes()]);
293
- if (node.routerLink) {
294
- this.onMenuItemClick();
295
- }
296
- }
297
-
298
- onSearchChange(): void {
299
- const currentSearchTerm = this.searchTerm()?.toLowerCase()?.trim();
300
- if (!currentSearchTerm) {
301
- this.filteredNodes.set(this.menuNodes() || []);
302
- return;
303
- }
304
- this.filteredNodes.set(this.filterNodes(this.menuNodes(), currentSearchTerm));
305
- }
306
-
307
- private filterNodes(nodes: MenuNode[], searchTerm: string): MenuNode[] {
308
- const filtered: MenuNode[] = [];
309
- const currentLang = this.translocoService.getActiveLang();
310
-
311
- for (const node of nodes) {
312
- // Convert label to string for search based on active language
313
- const labelText =
314
- typeof node.label === 'string' ?
315
- node.label
316
- : (node.label as any)?.[currentLang] || (node.label as any)?.tr || (node.label as any)?.en || '';
317
-
318
- const matches = labelText.toLowerCase().includes(searchTerm);
319
- let filteredChildren: MenuNode[] = [];
320
-
321
- if (node.children) {
322
- filteredChildren = this.filterNodes(node.children, searchTerm);
323
- }
324
-
325
- if (matches || filteredChildren.length > 0) {
326
- filtered.push({
327
- ...node,
328
- children: filteredChildren.length > 0 ? filteredChildren : node.children,
329
- expanded: filteredChildren.length > 0
330
- });
331
- }
332
- }
333
-
334
- return filtered;
335
- }
336
- }
1
+ import { NgClass, NgTemplateOutlet } from '@angular/common';
2
+ import { HttpClient } from '@angular/common/http';
3
+ import { Component, OnDestroy, OnInit, Signal, signal } from '@angular/core';
4
+ import { toSignal } from '@angular/core/rxjs-interop';
5
+ import { FormsModule } from '@angular/forms';
6
+ import { NavigationEnd, Router, RouterLink } from '@angular/router';
7
+
8
+ import { ScrollPanelModule } from 'primeng/scrollpanel';
9
+ import { TooltipModule } from 'primeng/tooltip';
10
+
11
+ import { TranslocoModule, TranslocoService } from '@ngneat/transloco';
12
+ import { Subscription, filter } from 'rxjs';
13
+
14
+ import { bridge } from 'aril/boot/bridge';
15
+ import { MenuItemAction, PluginMenuItem } from 'aril/boot/config/apps';
16
+ import { TranslateJsonPipe } from 'aril/util/pipes';
17
+
18
+ import { LayoutService } from '../../service/app.layout.service';
19
+ import { AppMenuService } from '../../service/app.menu.service';
20
+
21
+ interface MenuNode {
22
+ key: string;
23
+ label: any;
24
+ icon?: string;
25
+ routerLink?: string;
26
+ action?: MenuItemAction;
27
+ children?: MenuNode[];
28
+ expanded: boolean;
29
+ actionLoading?: boolean;
30
+ }
31
+
32
+ @Component({
33
+ standalone: true,
34
+ selector: 'app-static-sidebar',
35
+ imports: [RouterLink, NgClass, NgTemplateOutlet, ScrollPanelModule, TooltipModule, FormsModule, TranslocoModule],
36
+ templateUrl: './static-sidebar.component.html',
37
+ styleUrl: './static-sidebar.component.scss',
38
+ providers: [TranslateJsonPipe]
39
+ })
40
+ export class StaticSidebarComponent implements OnInit, OnDestroy {
41
+ activeLang: Signal<string>;
42
+ menuNodes = signal<MenuNode[]>([]);
43
+ filteredNodes = signal<MenuNode[]>([]);
44
+ searchTerm = signal<string>('');
45
+
46
+ private routerSubscription: Subscription | null = null;
47
+ private actionSubscription: Subscription | null = null;
48
+
49
+ private readonly MAX_LENGTH = { level0: 25, level1: 20, level2: 16, level3: 13 };
50
+
51
+ constructor(
52
+ private readonly menuService: AppMenuService,
53
+ private readonly router: Router,
54
+ private readonly layoutService: LayoutService,
55
+ private readonly translateJsonPipe: TranslateJsonPipe,
56
+ private readonly translocoService: TranslocoService,
57
+ private readonly http: HttpClient
58
+ ) {
59
+ this.activeLang = toSignal(this.translocoService.langChanges$, { initialValue: 'tr' });
60
+ }
61
+
62
+ get isCollapsed(): boolean {
63
+ return this.layoutService.isStaticSidebarCollapsed();
64
+ }
65
+
66
+ get topLevelNodes(): MenuNode[] {
67
+ return this.menuNodes();
68
+ }
69
+
70
+ ngOnInit() {
71
+ this.buildMenuTree();
72
+ this.routerSubscription = this.router.events
73
+ .pipe(filter((event) => event instanceof NavigationEnd))
74
+ .subscribe(() => this.buildMenuTree());
75
+ }
76
+
77
+ ngOnDestroy() {
78
+ this.routerSubscription?.unsubscribe();
79
+ this.actionSubscription?.unsubscribe();
80
+ }
81
+
82
+ isTextTruncated(label: any, lang: string | null, level = 0): boolean {
83
+ if (!label || !lang || !label[lang]) return false;
84
+ const text = label[lang];
85
+ const len = text.length;
86
+ const limits = [this.MAX_LENGTH.level0, this.MAX_LENGTH.level1, this.MAX_LENGTH.level2, this.MAX_LENGTH.level3];
87
+ return len > limits[Math.min(level, 3)];
88
+ }
89
+
90
+ isMobile(): boolean {
91
+ return this.layoutService.isMobile();
92
+ }
93
+
94
+ buildMenuTree() {
95
+ const menuItems = this.getMenuItems();
96
+ const nodes = this.convertToMenuNodes(menuItems);
97
+ this.menuNodes.set(nodes);
98
+ this.filteredNodes.set([...nodes]);
99
+ this.expandActiveMenu();
100
+ }
101
+
102
+ private expandActiveMenu() {
103
+ const nodes = this.menuNodes();
104
+ this.findAndExpandActiveNode(nodes);
105
+ this.filteredNodes.set([...this.filteredNodes()]);
106
+ }
107
+
108
+ private findAndExpandActiveNode(nodes: MenuNode[]): MenuNode | null {
109
+ for (const node of nodes) {
110
+ if (this.isActiveRouteForNode(node)) {
111
+ return node;
112
+ }
113
+
114
+ if (node.children && node.children.length > 0) {
115
+ const activeChild = this.findAndExpandActiveNode(node.children);
116
+ if (activeChild) {
117
+ // we find active child and then opened its parent nodes
118
+ node.expanded = true;
119
+ return activeChild;
120
+ }
121
+ }
122
+ }
123
+ return null;
124
+ }
125
+
126
+ convertToMenuNodes(items: PluginMenuItem[], parentKey = ''): MenuNode[] {
127
+ return items
128
+ .filter((item) => !item.separator)
129
+ .map((item, index) => {
130
+ const key = parentKey ? `${parentKey}-${index}` : `${index}`;
131
+ const node: MenuNode = {
132
+ key,
133
+ label: item.label,
134
+ icon: item.icon,
135
+ routerLink: item.action ? undefined : item.routerLink,
136
+ action: item.action,
137
+ expanded: false
138
+ };
139
+ if (item.items && item.items.length > 0) {
140
+ node.children = this.convertToMenuNodes(item.items, key);
141
+ }
142
+ return node;
143
+ });
144
+ }
145
+
146
+ getMenuItems(): PluginMenuItem[] {
147
+ return bridge.isHostMode() ? bridge.hostMenuItems() : this.menuService.menuItems();
148
+ }
149
+
150
+ getLocalText(text: any): string {
151
+ return this.translateJsonPipe.transform(text);
152
+ }
153
+
154
+ isActiveRouteForNode(node: MenuNode): boolean {
155
+ if (!node.routerLink) return false;
156
+ const currentUrl = this.router.url;
157
+ const hashUrl = currentUrl.indexOf('#') > -1 ? currentUrl.split('#')[1] : currentUrl;
158
+ if (hashUrl === node.routerLink || hashUrl === '/' + node.routerLink) return true;
159
+ return (
160
+ node.routerLink !== '/' &&
161
+ (hashUrl.startsWith(node.routerLink + '/') || hashUrl.startsWith('/' + node.routerLink + '/'))
162
+ );
163
+ }
164
+
165
+ // use on collapsed mode
166
+ isNodeOrChildActive(node: MenuNode): boolean {
167
+ if (this.isActiveRouteForNode(node)) {
168
+ return true;
169
+ }
170
+ if (node.children && node.children.length > 0) {
171
+ return node.children.some((child) => this.isNodeOrChildActive(child));
172
+ }
173
+
174
+ return false;
175
+ }
176
+
177
+ isTopLevelNodeActive(node: MenuNode): boolean {
178
+ return this.isNodeOrChildActive(node);
179
+ }
180
+
181
+ isChildNodeActive(node: MenuNode): boolean {
182
+ return this.isActiveRouteForNode(node);
183
+ }
184
+
185
+ toggleNode(node: MenuNode, level = 0) {
186
+ const wasExpanded = node.expanded;
187
+ if (level === 0) {
188
+ // Diğer ana menüleri kapat
189
+ this.closeOtherTopLevelNodes(node);
190
+ }
191
+ node.expanded = !wasExpanded;
192
+
193
+ this.filteredNodes.set([...this.filteredNodes()]);
194
+ }
195
+
196
+ private closeOtherTopLevelNodes(exceptNode: MenuNode) {
197
+ const nodes = this.menuNodes();
198
+ nodes.forEach((node) => {
199
+ if (node !== exceptNode) {
200
+ node.expanded = false;
201
+ this.closeAllChildNodes(node);
202
+ }
203
+ });
204
+ }
205
+
206
+ private closeAllChildNodes(node: MenuNode) {
207
+ if (node.children) {
208
+ node.children.forEach((child) => {
209
+ child.expanded = false;
210
+ this.closeAllChildNodes(child);
211
+ });
212
+ }
213
+ }
214
+
215
+ trackByKey(index: number, node: MenuNode): any {
216
+ return node.key;
217
+ }
218
+
219
+ onMenuItemClick() {
220
+ if (this.isMobile()) {
221
+ this.layoutService.closeMobileMenu();
222
+ }
223
+
224
+ this.searchTerm.set('');
225
+ this.filteredNodes.set(this.menuNodes() || []);
226
+ }
227
+
228
+ onActionItemClick(node: MenuNode, event: Event) {
229
+ event.preventDefault();
230
+ event.stopPropagation();
231
+
232
+ if (!node.action || node.actionLoading) return;
233
+
234
+ // Cancel any previous action request
235
+ this.actionSubscription?.unsubscribe();
236
+
237
+ node.actionLoading = true;
238
+
239
+ const request$ =
240
+ node.action.method === 'GET' ?
241
+ this.http.get(node.action.url)
242
+ : this.http.post(node.action.url, node.action.body ?? {});
243
+
244
+ this.actionSubscription = request$.subscribe({
245
+ next: (response: any) => {
246
+ node.actionLoading = false;
247
+ const route = this.buildSuccessRoute(node.action!, response);
248
+ this.router.navigate([route]);
249
+ this.onMenuItemClick();
250
+ },
251
+ error: () => {
252
+ node.actionLoading = false;
253
+ }
254
+ });
255
+ }
256
+
257
+ private buildSuccessRoute(action: MenuItemAction, response: any): string {
258
+ if (!action.successRouteResponseKey) return action.successRoute;
259
+
260
+ const value = this.getValueByPath(response, action.successRouteResponseKey);
261
+ if (value === undefined || value === null) return action.successRoute;
262
+
263
+ return `${action.successRoute}/${value}`;
264
+ }
265
+
266
+ private getValueByPath(obj: any, path: string): any {
267
+ if (!obj || !path) return undefined;
268
+
269
+ return path.split('.').reduce((current, key) => {
270
+ if (current === undefined || current === null) return undefined;
271
+
272
+ // Handle array access like 'result[0]'
273
+ const arrayMatch = key.match(/^(.+?)\[(\d+)\]$/);
274
+ if (arrayMatch) {
275
+ const arrayKey = arrayMatch[1];
276
+ const index = parseInt(arrayMatch[2], 10);
277
+ return current[arrayKey]?.[index];
278
+ }
279
+
280
+ return current[key];
281
+ }, obj);
282
+ }
283
+
284
+ onCollapsedItemClick(node: MenuNode) {
285
+ if (this.isCollapsed) {
286
+ this.layoutService.onMenuToggle();
287
+ }
288
+ this.closeOtherTopLevelNodes(node);
289
+ if (node.children && node.children.length > 0) {
290
+ node.expanded = true;
291
+ }
292
+ this.filteredNodes.set([...this.filteredNodes()]);
293
+ if (node.routerLink) {
294
+ this.onMenuItemClick();
295
+ }
296
+ }
297
+
298
+ onSearchChange(): void {
299
+ const currentSearchTerm = this.searchTerm()?.toLowerCase()?.trim();
300
+ if (!currentSearchTerm) {
301
+ this.filteredNodes.set(this.menuNodes() || []);
302
+ return;
303
+ }
304
+ this.filteredNodes.set(this.filterNodes(this.menuNodes(), currentSearchTerm));
305
+ }
306
+
307
+ private filterNodes(nodes: MenuNode[], searchTerm: string): MenuNode[] {
308
+ const filtered: MenuNode[] = [];
309
+ const currentLang = this.translocoService.getActiveLang();
310
+
311
+ for (const node of nodes) {
312
+ // Convert label to string for search based on active language
313
+ const labelText =
314
+ typeof node.label === 'string' ?
315
+ node.label
316
+ : (node.label as any)?.[currentLang] || (node.label as any)?.tr || (node.label as any)?.en || '';
317
+
318
+ const matches = labelText.toLowerCase().includes(searchTerm);
319
+ let filteredChildren: MenuNode[] = [];
320
+
321
+ if (node.children) {
322
+ filteredChildren = this.filterNodes(node.children, searchTerm);
323
+ }
324
+
325
+ if (matches || filteredChildren.length > 0) {
326
+ filtered.push({
327
+ ...node,
328
+ children: filteredChildren.length > 0 ? filteredChildren : node.children,
329
+ expanded: filteredChildren.length > 0
330
+ });
331
+ }
332
+ }
333
+
334
+ return filtered;
335
+ }
336
+ }