@akinon/ui-shell-dev 1.4.0 → 1.5.1
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-D6a6pO8M.js → index-BLfOr6po.js} +53 -53
- package/dist/assets/{index-kkeQGnUJ.js → index-DCzYafit.js} +1 -1
- package/dist/index.html +1 -1
- package/package.json +4 -4
- package/src/components/Navigation.tsx +102 -10
- package/src/pages/External.tsx +15 -1
- package/src/routes.tsx +22 -19
|
@@ -1 +1 @@
|
|
|
1
|
-
import{u as n,c as u}from"./index-
|
|
1
|
+
import{u as n,c as u}from"./index-BLfOr6po.js";n(function(e,t){t._reactRoot||(t._reactRoot=u.createRoot(t));var o=t._reactRoot;return o.render(e),function(){return new Promise(function(r){setTimeout(function(){o.unmount(),r()},0)})}});
|
package/dist/index.html
CHANGED
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
box-sizing: border-box;
|
|
25
25
|
}
|
|
26
26
|
</style>
|
|
27
|
-
<script type="module" crossorigin src="/assets/index-
|
|
27
|
+
<script type="module" crossorigin src="/assets/index-BLfOr6po.js"></script>
|
|
28
28
|
<link rel="stylesheet" crossorigin href="/assets/index-CX4XmW02.css">
|
|
29
29
|
</head>
|
|
30
30
|
<body>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@akinon/ui-shell-dev",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.1",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Development shell application for Akinon UI Protocol plugins",
|
|
6
6
|
"type": "module",
|
|
@@ -22,10 +22,10 @@
|
|
|
22
22
|
"react-router-dom": "^6.28.0",
|
|
23
23
|
"vite": "*",
|
|
24
24
|
"@akinon/icons": "1.1.0",
|
|
25
|
-
"@akinon/
|
|
26
|
-
"@akinon/ui-
|
|
25
|
+
"@akinon/ui-react": "1.3.1",
|
|
26
|
+
"@akinon/ui-utils": "1.1.0",
|
|
27
27
|
"@akinon/fonts-jost-variable": "2.1.0",
|
|
28
|
-
"@akinon/
|
|
28
|
+
"@akinon/app-shell": "1.4.0"
|
|
29
29
|
},
|
|
30
30
|
"devDependencies": {
|
|
31
31
|
"@types/cors": "^2.8.17",
|
|
@@ -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
|
|
|
@@ -24,21 +24,112 @@ export const Navigation: React.FC = () => {
|
|
|
24
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;
|
|
87
|
+
|
|
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]);
|
|
31
107
|
|
|
32
108
|
const onClick: IMenuProps['onClick'] = e => {
|
|
33
|
-
|
|
34
|
-
|
|
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}
|
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/routes.tsx
CHANGED
|
@@ -16,25 +16,28 @@ 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
|
-
{(
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
19
|
+
{(
|
|
20
|
+
configs.get(app.id) as
|
|
21
|
+
| { menu?: Array<{ path: string }> }
|
|
22
|
+
| undefined
|
|
23
|
+
)?.menu?.map((sub: { path: string }) => {
|
|
24
|
+
return (
|
|
25
|
+
sub.path !== '/' && (
|
|
26
|
+
<Route
|
|
27
|
+
key={sub.path}
|
|
28
|
+
path={`/external/${app.slug}${sub.path}/*`}
|
|
29
|
+
element={
|
|
30
|
+
<PageExternal
|
|
31
|
+
id={app.id}
|
|
32
|
+
path={
|
|
33
|
+
app.subSlug ? `${app.subSlug}${sub.path}` : sub.path
|
|
34
|
+
}
|
|
35
|
+
/>
|
|
36
|
+
}
|
|
37
|
+
/>
|
|
38
|
+
)
|
|
39
|
+
);
|
|
40
|
+
})}
|
|
38
41
|
</Route>
|
|
39
42
|
))}
|
|
40
43
|
|