@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/assets/index-BTAQndJC.js +1 -0
- package/dist/assets/index-CX4XmW02.css +1 -0
- package/dist/assets/index-DAU00iTN.js +434 -0
- package/dist/index.html +2 -2
- package/index.mjs +1 -1
- package/package.json +15 -16
- package/src/components/Header.tsx +1 -1
- package/src/components/HeaderSearch.tsx +1 -1
- package/src/components/HeaderUser.tsx +9 -2
- package/src/components/Navigation.tsx +104 -12
- package/src/components/PageHeading.tsx +4 -8
- package/src/components/PluginTestPage.tsx +1 -1
- package/src/components/WelcomePage.tsx +26 -17
- package/src/main.tsx +8 -6
- package/src/pages/External.tsx +15 -1
- package/src/providers.tsx +4 -2
- package/src/routes.tsx +6 -2
- package/src/server/dev-server.mjs +15 -13
- package/src/server/dev-server.ts +15 -13
- package/src/types.ts +1 -1
- package/src/utils/navigation.tsx +16 -8
- package/dist/assets/index-BFs1gfY5.css +0 -1
- package/dist/assets/index-CNgx52S6.js +0 -426
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-
|
|
28
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
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
|
+
"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": "
|
|
26
|
-
"react-dom": "
|
|
20
|
+
"react": "*",
|
|
21
|
+
"react-dom": "*",
|
|
27
22
|
"react-router-dom": "^6.28.0",
|
|
28
|
-
"vite": "
|
|
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": "
|
|
34
|
-
"@types/react-dom": "
|
|
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": "
|
|
38
|
-
"@akinon/typescript-config": "1.0
|
|
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
|
-
"
|
|
48
|
+
"preview": "vite preview"
|
|
50
49
|
}
|
|
51
50
|
}
|
|
@@ -14,10 +14,17 @@ export const HeaderUser: React.FC = () => {
|
|
|
14
14
|
}}
|
|
15
15
|
placement="bottomRight"
|
|
16
16
|
>
|
|
17
|
-
<div
|
|
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,
|
|
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
|
-
|
|
30
|
-
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
|
|
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 = (
|
|
41
|
-
|
|
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={
|
|
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,
|
|
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
|
-
<
|
|
48
|
+
<Title level={4} style={{ margin: 0 }}>
|
|
49
49
|
{title}
|
|
50
|
-
</
|
|
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(
|
|
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
|
-
|
|
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
|
|
56
|
+
<div
|
|
57
|
+
style={{ display: 'flex', flexDirection: 'column', gap: '20px' }}
|
|
58
|
+
>
|
|
57
59
|
{apps.map((app, index) => (
|
|
58
|
-
<div
|
|
59
|
-
|
|
60
|
-
|
|
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
|
|
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'}
|
|
116
|
-
|
|
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
|
-
|
|
159
|
-
|
|
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
|
-
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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',
|
package/src/pages/External.tsx
CHANGED
|
@@ -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={
|
|
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 }
|
|
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
|
-
{(
|
|
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(
|
|
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
|
}
|
package/src/server/dev-server.ts
CHANGED
|
@@ -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(
|
|
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
package/src/utils/navigation.tsx
CHANGED
|
@@ -54,7 +54,7 @@ export const getStaticNavigationItemsBottom = () => {
|
|
|
54
54
|
|
|
55
55
|
export const getFullpageNavigationItems = (
|
|
56
56
|
apps: RegisteredApp[],
|
|
57
|
-
configs: Map<number,
|
|
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:
|
|
67
|
-
.get(app.id)
|
|
68
|
-
|
|
69
|
-
|
|
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}${
|
|
73
|
-
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
|
};
|