@akinon/ui-shell-dev 1.3.6 → 1.5.0

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/dist/index.html CHANGED
@@ -24,8 +24,8 @@
24
24
  box-sizing: border-box;
25
25
  }
26
26
  </style>
27
- <script type="module" crossorigin src="/assets/index-CNgx52S6.js"></script>
28
- <link rel="stylesheet" crossorigin href="/assets/index-BFs1gfY5.css">
27
+ <script type="module" crossorigin src="/assets/index-DAU00iTN.js"></script>
28
+ <link rel="stylesheet" crossorigin href="/assets/index-CX4XmW02.css">
29
29
  </head>
30
30
  <body>
31
31
  <div id="root"></div>
package/index.mjs CHANGED
@@ -1 +1 @@
1
- export { ShellDevServer } from './src/server/dev-server.mjs';
1
+ export { ShellDevServer } from './src/server/dev-server.mjs';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@akinon/ui-shell-dev",
3
- "version": "1.3.6",
3
+ "version": "1.5.0",
4
4
  "private": false,
5
5
  "description": "Development shell application for Akinon UI Protocol plugins",
6
6
  "type": "module",
@@ -15,37 +15,36 @@
15
15
  "tsconfig.json"
16
16
  ],
17
17
  "dependencies": {
18
- "@akinon/app-shell": "^1.3.0",
19
- "@akinon/fonts-jost-variable": "^2.0.1",
20
- "@akinon/icons": "^1.0.0",
21
- "@akinon/ui-react": "^1.2.0",
22
- "@akinon/ui-utils": "^1.0.0",
23
18
  "cors": "^2.8.5",
24
19
  "express": "^4.18.2",
25
- "react": "^18.2.0",
26
- "react-dom": "^18.2.0",
20
+ "react": "*",
21
+ "react-dom": "*",
27
22
  "react-router-dom": "^6.28.0",
28
- "vite": "^5.2.0"
23
+ "vite": "*",
24
+ "@akinon/app-shell": "1.4.0",
25
+ "@akinon/ui-react": "1.3.0",
26
+ "@akinon/fonts-jost-variable": "2.1.0",
27
+ "@akinon/icons": "1.1.0",
28
+ "@akinon/ui-utils": "1.1.0"
29
29
  },
30
30
  "devDependencies": {
31
31
  "@types/cors": "^2.8.17",
32
32
  "@types/express": "^4.17.21",
33
- "@types/react": "^18.2.66",
34
- "@types/react-dom": "^18.2.22",
33
+ "@types/react": "*",
34
+ "@types/react-dom": "*",
35
35
  "@vitejs/plugin-react": "^5.0.0",
36
36
  "@vitejs/plugin-react-swc": "^3.7.2",
37
- "typescript": "^5.2.2",
38
- "@akinon/typescript-config": "1.0.1"
37
+ "typescript": "*",
38
+ "@akinon/typescript-config": "1.1.0"
39
39
  },
40
40
  "engines": {
41
41
  "node": ">=18"
42
42
  },
43
43
  "scripts": {
44
44
  "build": "vite build",
45
+ "clean": "rm -rf dist node_modules",
45
46
  "dev": "vite",
46
- "preview": "vite preview",
47
- "typecheck": "tsc --noEmit",
48
47
  "lint": "eslint",
49
- "clean": "rm -rf dist node_modules"
48
+ "preview": "vite preview"
50
49
  }
51
50
  }
@@ -1,4 +1,4 @@
1
- import { Badge, Button } from '@akinon/ui-react';
1
+ import { Button } from '@akinon/ui-react';
2
2
  import React from 'react';
3
3
 
4
4
  // Get version from package.json via Vite environment variable
@@ -9,4 +9,4 @@ export const HeaderSearch: React.FC = () => {
9
9
  onSearch={() => {}}
10
10
  />
11
11
  );
12
- };
12
+ };
@@ -14,10 +14,17 @@ export const HeaderUser: React.FC = () => {
14
14
  }}
15
15
  placement="bottomRight"
16
16
  >
17
- <div style={{ display: 'flex', alignItems: 'center', gap: '8px', cursor: 'pointer' }}>
17
+ <div
18
+ style={{
19
+ display: 'flex',
20
+ alignItems: 'center',
21
+ gap: '8px',
22
+ cursor: 'pointer'
23
+ }}
24
+ >
18
25
  <Avatar size="small">Dev</Avatar>
19
26
  <span style={{ fontSize: '14px' }}>Developer</span>
20
27
  </div>
21
28
  </Dropdown>
22
29
  );
23
- };
30
+ };
@@ -1,6 +1,6 @@
1
1
  import { useAppShell } from '@akinon/app-shell';
2
- import { IMenuProps, Menu } from '@akinon/ui-react';
3
- import { useEffect, useState } from 'react';
2
+ import { IMenuProps, Menu, TMenuItemType } from '@akinon/ui-react';
3
+ import { useEffect, useMemo, useState } from 'react';
4
4
  import React from 'react';
5
5
  import { useLocation, useNavigate } from 'react-router-dom';
6
6
 
@@ -21,24 +21,115 @@ export const Navigation: React.FC = () => {
21
21
  const itemsBottom = getStaticNavigationItemsBottom();
22
22
  const fullpageItems = getFullpageNavigationItems(
23
23
  apps,
24
- configs as Map<number, any>
24
+ configs as Map<number, unknown>
25
25
  );
26
26
  const pluginItems = getPluginNavigationItems(apps);
27
+ const navigationItems = useMemo(
28
+ () => [...items, ...fullpageItems, ...pluginItems, ...itemsBottom],
29
+ [items, fullpageItems, pluginItems, itemsBottom]
30
+ );
31
+
32
+ const { allItemKeys, childToParentKey } = useMemo(() => {
33
+ function collectKeys(items: TMenuItemType[] | undefined): string[] {
34
+ if (!items) return [];
35
+ const keys: string[] = [];
36
+ items.forEach(item => {
37
+ if ('key' in item && item.key) keys.push(item.key);
38
+ if ('children' in item && item.children) {
39
+ (item.children as TMenuItemType[]).forEach(child => {
40
+ if ('key' in child && child.key) keys.push(child.key);
41
+ });
42
+ }
43
+ });
44
+ return keys;
45
+ }
46
+
47
+ const keys = collectKeys(navigationItems);
48
+
49
+ const map = new Map<string, string>();
50
+ fullpageItems.forEach(parent => {
51
+ if ('children' in parent && parent.children) {
52
+ (parent.children as TMenuItemType[]).forEach(child => {
53
+ if ('key' in child && child.key && 'key' in parent && parent.key) {
54
+ map.set(child.key, parent.key);
55
+ }
56
+ });
57
+ }
58
+ });
59
+
60
+ return { allItemKeys: keys, childToParentKey: map };
61
+ }, [navigationItems, fullpageItems]);
62
+
63
+ const parentKeys = useMemo(() => {
64
+ const keys = new Set<string>();
65
+ fullpageItems.forEach(p => {
66
+ if (
67
+ 'key' in p &&
68
+ p.key &&
69
+ 'children' in p &&
70
+ Array.isArray(p.children) &&
71
+ (p.children as TMenuItemType[]).length > 0
72
+ ) {
73
+ keys.add(p.key as string);
74
+ }
75
+ });
76
+ return keys;
77
+ }, [fullpageItems]);
27
78
 
28
79
  useEffect(() => {
29
- setSelectedKey(location.pathname);
30
- }, [location]);
80
+ const pathname = location.pathname;
81
+ // Pick the longest menu key that is a prefix of the current path
82
+ const bestMatch = allItemKeys
83
+ .filter(key => typeof key === 'string' && pathname.startsWith(key))
84
+ .sort((a, b) => (b as string).length - (a as string).length)[0] as
85
+ | string
86
+ | undefined;
31
87
 
32
- const onClick: IMenuProps['onClick'] = (e: any) => {
33
- if (!e.key.includes('external')) {
34
- setOpenKeys([]);
88
+ if (bestMatch) setSelectedKey(bestMatch);
89
+
90
+ const parentKey = bestMatch ? childToParentKey.get(bestMatch) : undefined;
91
+ // Auto open parent submenu when landing on a child route
92
+ if (parentKey) setOpenKeys([parentKey]);
93
+ else if (
94
+ // If landing on a parent path that has children, open it too
95
+ fullpageItems.some(p => {
96
+ const isSameKey = 'key' in p && p.key === bestMatch;
97
+ const hasChildren =
98
+ 'children' in p &&
99
+ Array.isArray(p.children) &&
100
+ (p.children as TMenuItemType[]).length > 0;
101
+ return isSameKey && hasChildren;
102
+ })
103
+ ) {
104
+ setOpenKeys([bestMatch as string]);
105
+ }
106
+ }, [location, allItemKeys, childToParentKey, fullpageItems]);
107
+
108
+ const onClick: IMenuProps['onClick'] = e => {
109
+ const key = e.key;
110
+
111
+ // Toggle parent submenu open/close when clicking its title
112
+ if (parentKeys.has(key)) {
113
+ setOpenKeys(prev => (prev.includes(key) ? [] : [key]));
114
+ setSelectedKey(key);
115
+ return;
35
116
  }
36
117
 
37
- navigate(e.key);
118
+ const parentKey = childToParentKey.get(key);
119
+ if (parentKey) setOpenKeys([parentKey]);
120
+
121
+ if (!key.includes('external')) setOpenKeys([]);
122
+
123
+ navigate(key);
38
124
  };
39
125
 
40
- const onOpenChange = (openKeys: string[]) => {
41
- setOpenKeys([openKeys.pop() || '']);
126
+ const onOpenChange = (nextOpenKeys: string[]) => {
127
+ // Keep at most one submenu open; allow closing to none
128
+ if (nextOpenKeys.length === 0) {
129
+ setOpenKeys([]);
130
+ return;
131
+ }
132
+ setOpenKeys([nextOpenKeys[nextOpenKeys.length - 1]]);
42
133
  };
43
134
 
44
135
  return (
@@ -46,8 +137,9 @@ export const Navigation: React.FC = () => {
46
137
  <Menu
47
138
  mode="inline"
48
139
  selectedKeys={[selectedKey]}
49
- items={[...items, ...fullpageItems, ...pluginItems, ...itemsBottom]}
140
+ items={navigationItems}
50
141
  onClick={onClick}
142
+ triggerSubMenuAction="click"
51
143
  theme="dark"
52
144
  openKeys={openKeys}
53
145
  onOpenChange={onOpenChange}
@@ -1,4 +1,4 @@
1
- import { Flex, Space, Typography } from 'antd';
1
+ import { Flex, Paragraph, Space, Title } from '@akinon/ui-react';
2
2
  import * as React from 'react';
3
3
 
4
4
  export interface PageHeadingProps {
@@ -45,14 +45,10 @@ export const PageHeading = ({
45
45
  }}
46
46
  >
47
47
  <Space direction="vertical" size={4}>
48
- <Typography.Title level={4} style={{ margin: 0 }}>
48
+ <Title level={4} style={{ margin: 0 }}>
49
49
  {title}
50
- </Typography.Title>
51
- {description && (
52
- <Typography.Paragraph type="secondary">
53
- {description}
54
- </Typography.Paragraph>
55
- )}
50
+ </Title>
51
+ {description && <Paragraph type="secondary">{description}</Paragraph>}
56
52
  </Space>
57
53
 
58
54
  {actions && <Space>{actions}</Space>}
@@ -66,7 +66,7 @@ export const PluginTestPage: React.FC = () => {
66
66
  <PageContent>
67
67
  {pluginPlaceholders.length > 0 ? (
68
68
  <Row gutter={[20, 20]}>
69
- {pluginPlaceholders.map((placeholderId, index) => (
69
+ {pluginPlaceholders.map(placeholderId => (
70
70
  <Col span={12} key={placeholderId}>
71
71
  <div
72
72
  className="plugin-placeholder"
@@ -41,10 +41,10 @@ const SharedDataDisplay: React.FC<{ data: Record<string, unknown> }> = ({
41
41
 
42
42
  export const WelcomePage: React.FC = () => {
43
43
  const { apps } = useAppShell();
44
-
45
- const config: ShellConfig = (
46
- window as unknown as { __SHELL_CONFIG__?: ShellConfig }
47
- ).__SHELL_CONFIG__ as ShellConfig;
44
+
45
+ const config: ShellConfig =
46
+ ((window as unknown as { __SHELL_CONFIG__?: ShellConfig })
47
+ .__SHELL_CONFIG__ as ShellConfig) || {};
48
48
  const data = config.data;
49
49
 
50
50
  return (
@@ -53,12 +53,18 @@ export const WelcomePage: React.FC = () => {
53
53
  {apps.length === 0 ? (
54
54
  <p style={{ color: '#999' }}>No applications configured</p>
55
55
  ) : (
56
- <div style={{ display: 'flex', flexDirection: 'column', gap: '20px' }}>
56
+ <div
57
+ style={{ display: 'flex', flexDirection: 'column', gap: '20px' }}
58
+ >
57
59
  {apps.map((app, index) => (
58
- <div key={app.id} style={{
59
- borderBottom: index < apps.length - 1 ? '1px solid #e0e0e0' : 'none',
60
- paddingBottom: index < apps.length - 1 ? '20px' : '0'
61
- }}>
60
+ <div
61
+ key={app.id}
62
+ style={{
63
+ borderBottom:
64
+ index < apps.length - 1 ? '1px solid #e0e0e0' : 'none',
65
+ paddingBottom: index < apps.length - 1 ? '20px' : '0'
66
+ }}
67
+ >
62
68
  <h4 style={{ margin: '0 0 16px 0' }}>
63
69
  {app.name} {apps.length > 1 && `(App ${index + 1})`}
64
70
  </h4>
@@ -96,7 +102,7 @@ export const WelcomePage: React.FC = () => {
96
102
  <div style={{ marginTop: '10px' }}>
97
103
  <strong>Shell Theme:</strong>
98
104
  <p style={{ margin: '12px 0 4px 0', color: '#999' }}>
99
- {config.shell.theme}
105
+ {config?.shell?.theme}
100
106
  </p>
101
107
  </div>
102
108
  </div>
@@ -112,8 +118,11 @@ export const WelcomePage: React.FC = () => {
112
118
  <h4 style={{ margin: '0 0 8px 0' }}>Getting Started:</h4>
113
119
  <ol style={{ paddingLeft: '20px' }}>
114
120
  <li>
115
- Make sure your application{apps.length > 1 ? 's are' : ' is'} running at{' '}
116
- <code>{apps.length > 0 ? apps[0].url : 'http://localhost:4001'}</code>
121
+ Make sure your application{apps.length > 1 ? 's are' : ' is'}{' '}
122
+ running at{' '}
123
+ <code>
124
+ {apps.length > 0 ? apps[0].url : 'http://localhost:4001'}
125
+ </code>
117
126
  {apps.length > 1 && (
118
127
  <>
119
128
  {' and '}
@@ -154,15 +163,15 @@ export const WelcomePage: React.FC = () => {
154
163
  </h4>
155
164
  {apps.some(a => a.type === 'full_page') && (
156
165
  <p style={{ margin: 0, color: '#666' }}>
157
- Full page applications render as complete pages within the
158
- shell environment, providing full control over the content area
159
- and routing.
166
+ Full page applications render as complete pages within the shell
167
+ environment, providing full control over the content area and
168
+ routing.
160
169
  </p>
161
170
  )}
162
171
  {apps.some(a => a.type === 'plugin') && (
163
172
  <p style={{ margin: '8px 0 0 0', color: '#666' }}>
164
- Plugin applications render within designated placeholders
165
- in the host application, providing component-level integration
173
+ Plugin applications render within designated placeholders in the
174
+ host application, providing component-level integration
166
175
  capabilities.
167
176
  </p>
168
177
  )}
package/src/main.tsx CHANGED
@@ -12,12 +12,14 @@ import type { ShellConfig } from './types';
12
12
  const config: ShellConfig = (
13
13
  window as unknown as { __SHELL_CONFIG__?: ShellConfig }
14
14
  ).__SHELL_CONFIG__ || {
15
- plugin: {
16
- url: 'http://localhost:4001',
17
- path: './src',
18
- name: 'Development App',
19
- type: 'fullpage'
20
- },
15
+ apps: [
16
+ {
17
+ url: 'http://localhost:4001',
18
+ path: './src',
19
+ name: 'Development App',
20
+ type: 'full_page'
21
+ }
22
+ ],
21
23
  shell: {
22
24
  port: 4000,
23
25
  theme: 'omnitron',
@@ -2,14 +2,28 @@ import './pages.scss';
2
2
 
3
3
  import { AppRenderer } from '@akinon/app-shell';
4
4
  import React from 'react';
5
+ import { useLocation } from 'react-router-dom';
5
6
 
6
7
  export const PageExternal: React.FC<{ id: number; path?: string }> = ({
7
8
  id,
8
9
  path
9
10
  }) => {
11
+ const location = useLocation();
12
+ // Compute splat (unmatched) path after the configured base `path`.
13
+ // Example: base "/keywords" and current pathname "/external/app/keywords/5/edit" -> splat "/5/edit".
14
+ const basePath = path ?? '';
15
+ const pathname = location.pathname;
16
+ const indexOfBase = pathname.indexOf(basePath);
17
+ const splat =
18
+ indexOfBase >= 0 ? pathname.slice(indexOfBase + basePath.length) : '';
19
+ const resolvedPath = `${basePath}${splat}` || undefined;
20
+ const resolvedPathWithQuery = resolvedPath
21
+ ? `${resolvedPath}${location.search ?? ''}${location.hash ?? ''}`
22
+ : undefined;
23
+
10
24
  return (
11
25
  <div className="page-plugin-external">
12
- <AppRenderer id={id} path={path} />
26
+ <AppRenderer id={id} path={resolvedPathWithQuery} />
13
27
  </div>
14
28
  );
15
29
  };
package/src/providers.tsx CHANGED
@@ -27,7 +27,7 @@ export const Providers: React.FC<ProvidersProps> = ({ config, children }) => {
27
27
  }));
28
28
 
29
29
  const navigation: ShellNavigation = {
30
- navigate: ({ id, path, external }: any) => {
30
+ navigate: ({ id, path, external }) => {
31
31
  const app = apps.find(app => app.id === id);
32
32
  if (!app) return;
33
33
 
@@ -82,7 +82,9 @@ export const Providers: React.FC<ProvidersProps> = ({ config, children }) => {
82
82
  'logMessages',
83
83
  JSON.stringify([...prevItems, newItem])
84
84
  );
85
- } catch {}
85
+ } catch {
86
+ console.error('Failed to log message');
87
+ }
86
88
  }
87
89
  };
88
90
 
package/src/routes.tsx CHANGED
@@ -16,12 +16,16 @@ export const Routes = () => {
16
16
  <Route key={app.id} path={`/external/${app.slug}`}>
17
17
  <Route index element={<PageExternal id={app.id} path={app.path} />} />
18
18
 
19
- {(configs.get(app.id) as any)?.menu?.map((sub: any) => {
19
+ {(
20
+ configs.get(app.id) as
21
+ | { menu?: Array<{ path: string }> }
22
+ | undefined
23
+ )?.menu?.map((sub: { path: string }) => {
20
24
  return (
21
25
  sub.path !== '/' && (
22
26
  <Route
23
27
  key={sub.path}
24
- path={`/external/${app.slug}${sub.path}`}
28
+ path={`/external/${app.slug}${sub.path}/*`}
25
29
  element={
26
30
  <PageExternal
27
31
  id={app.id}
@@ -143,7 +143,9 @@ export class ShellDevServer {
143
143
  if (this.config.apps && this.config.apps.length > 0) {
144
144
  console.log(`📱 Applications configured:`);
145
145
  this.config.apps.forEach((app, index) => {
146
- console.log(` ${index + 1}. ${app.name} (${app.type}) at ${app.url}`);
146
+ console.log(
147
+ ` ${index + 1}. ${app.name} (${app.type}) at ${app.url}`
148
+ );
147
149
  });
148
150
  }
149
151
  console.log(`🎨 Theme: ${this.config.shell.theme}`);
@@ -193,29 +195,29 @@ export class ShellDevServer {
193
195
  // Apps health check endpoint (for development)
194
196
  this.app.get('/api/apps/health', async (req, res) => {
195
197
  const results = [];
196
-
198
+
197
199
  if (this.config.apps && this.config.apps.length > 0) {
198
200
  for (const app of this.config.apps) {
199
201
  try {
200
202
  const response = await fetch(`${app.url}/api/health`);
201
203
  const data = await response.json();
202
- results.push({
203
- name: app.name,
204
- url: app.url,
205
- status: 'ok',
206
- data
204
+ results.push({
205
+ name: app.name,
206
+ url: app.url,
207
+ status: 'ok',
208
+ data
207
209
  });
208
210
  } catch (error) {
209
- results.push({
210
- name: app.name,
211
- url: app.url,
212
- status: 'error',
213
- error: error.message
211
+ results.push({
212
+ name: app.name,
213
+ url: app.url,
214
+ status: 'error',
215
+ error: error.message
214
216
  });
215
217
  }
216
218
  }
217
219
  }
218
-
220
+
219
221
  res.json({ apps: results, shell: { status: 'ok' } });
220
222
  });
221
223
  }
@@ -90,7 +90,9 @@ export class ShellDevServer {
90
90
  if (this.config.apps && this.config.apps.length > 0) {
91
91
  console.log(`📱 Applications configured:`);
92
92
  this.config.apps.forEach((app, index) => {
93
- console.log(` ${index + 1}. ${app.name} (${app.type}) at ${app.url}`);
93
+ console.log(
94
+ ` ${index + 1}. ${app.name} (${app.type}) at ${app.url}`
95
+ );
94
96
  });
95
97
  }
96
98
  console.log(`🎨 Theme: ${this.config.shell.theme}`);
@@ -132,29 +134,29 @@ export class ShellDevServer {
132
134
  // Apps health check endpoint (for development)
133
135
  this.app.get('/api/apps/health', async (req, res) => {
134
136
  const results = [];
135
-
137
+
136
138
  if (this.config.apps && this.config.apps.length > 0) {
137
139
  for (const app of this.config.apps) {
138
140
  try {
139
141
  const response = await fetch(`${app.url}/api/health`);
140
142
  const data = await response.json();
141
- results.push({
142
- name: app.name,
143
- url: app.url,
144
- status: 'ok',
145
- data
143
+ results.push({
144
+ name: app.name,
145
+ url: app.url,
146
+ status: 'ok',
147
+ data
146
148
  });
147
149
  } catch (error) {
148
- results.push({
149
- name: app.name,
150
- url: app.url,
151
- status: 'error',
152
- error: error.message
150
+ results.push({
151
+ name: app.name,
152
+ url: app.url,
153
+ status: 'error',
154
+ error: error.message
153
155
  });
154
156
  }
155
157
  }
156
158
  }
157
-
159
+
158
160
  res.json({ apps: results, shell: { status: 'ok' } });
159
161
  });
160
162
  }
package/src/types.ts CHANGED
@@ -14,7 +14,7 @@ export interface ShellConfig {
14
14
  sidebar?: {
15
15
  items: SidebarItem[];
16
16
  };
17
- data?: Record<string, any>;
17
+ data?: Record<string, unknown>;
18
18
  }
19
19
 
20
20
  export interface SidebarItem {
@@ -54,7 +54,7 @@ export const getStaticNavigationItemsBottom = () => {
54
54
 
55
55
  export const getFullpageNavigationItems = (
56
56
  apps: RegisteredApp[],
57
- configs: Map<number, any>
57
+ configs: Map<number, unknown>
58
58
  ): TMenuItemType[] => {
59
59
  return apps
60
60
  .filter(app => app.type === 'full_page')
@@ -63,18 +63,26 @@ export const getFullpageNavigationItems = (
63
63
  key: `/external/${app.slug}`,
64
64
  label: app.name,
65
65
  icon: 'stoklistesi',
66
- children: configs
67
- .get(app.id)
68
- ?.menu?.map((sub: any): TMenuItemType | false => {
69
- if (sub?.child) return false;
66
+ children: (
67
+ configs.get(app.id) as {
68
+ menu: TMenuItemType[];
69
+ }
70
+ )?.menu
71
+ ?.map((sub: unknown): TMenuItemType | false => {
72
+ const { child, path, label } = sub as {
73
+ child: unknown;
74
+ path: string;
75
+ label: string;
76
+ };
77
+ if (child) return false;
70
78
 
71
79
  return {
72
- key: `/external/${app.slug}${sub.path}`,
73
- label: sub.label,
80
+ key: `/external/${app.slug}${path}`,
81
+ label: label,
74
82
  icon: 'eksi'
75
83
  };
76
84
  })
77
- .filter(Boolean)
85
+ .filter(Boolean) as TMenuItemType[]
78
86
  };
79
87
  });
80
88
  };