@nextclaw/ui 0.13.1 → 0.13.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/CHANGELOG.md +64 -0
- package/dist/assets/app-presenter-provider-DIcnNB9s.js +3 -0
- package/dist/assets/{wrench-qVIwShTF.js → boxes-BvA-I63l.js} +1 -1
- package/dist/assets/channels-list-page-wK3l7p5R.js +8 -0
- package/dist/assets/chat-page-DgehvdJT.js +105 -0
- package/dist/assets/{confirm-dialog-B0TP-vPd.js → confirm-dialog-CGrKHbAW.js} +1 -1
- package/dist/assets/desktop-update-config-CehWxSSg.js +1 -0
- package/dist/assets/doc-browser-BgxmVsTx.js +1 -0
- package/dist/assets/doc-browser-CZn5-Qbu.js +1 -0
- package/dist/assets/doc-browser-context-B8gk7HMr.js +1 -0
- package/dist/assets/index-DORmh0Cy.css +1 -0
- package/dist/assets/index-DcFpvnge.js +101 -0
- package/dist/assets/key-round-D41Oe35x.js +1 -0
- package/dist/assets/mcp-marketplace-page-DBLhJWOJ.js +1 -0
- package/dist/assets/{mcp-marketplace-page-DDjeHd1I.js → mcp-marketplace-page-Dvi8mCBd.js} +2 -2
- package/dist/assets/model-config-B-EJXGtO.js +1 -0
- package/dist/assets/popover-YieElO8c.js +1 -0
- package/dist/assets/{provider-scoped-model-input-t1tCP5y5.js → provider-scoped-model-input-BPRNGqYn.js} +1 -1
- package/dist/assets/providers-list-CtBpL1ZY.js +1 -0
- package/dist/assets/remote-BGHbHoHf.js +1 -0
- package/dist/assets/rotate-cw-jueVCNJQ.js +1 -0
- package/dist/assets/runtime-config-page-C1Hv7mad.js +1 -0
- package/dist/assets/search-config-G2O1fcRb.js +1 -0
- package/dist/assets/{secrets-config-D1AUSEXX.js → secrets-config-TenzxYbE.js} +2 -2
- package/dist/assets/status-dot-BSMG7ZaC.js +1 -0
- package/dist/assets/tabs-D1CjpWNX.js +1 -0
- package/dist/assets/trash-2-DSWTsARW.js +1 -0
- package/dist/index.html +19 -13
- package/package.json +8 -8
- package/src/app/components/layout/app-layout.test.tsx +2 -1
- package/src/app/components/layout/app-layout.tsx +18 -4
- package/src/app/components/layout/sidebar.tsx +1 -1
- package/src/app/presenters/app.presenter.ts +6 -0
- package/src/features/chat/components/layout/chat-sidebar.test.tsx +1 -2
- package/src/features/chat/components/layout/chat-sidebar.tsx +1 -1
- package/src/features/panel-apps/utils/panel-app-doc-browser.utils.tsx +17 -13
- package/src/features/right-panel-resources/components/right-panel-resource-home-page.tsx +96 -0
- package/src/features/right-panel-resources/configs/right-panel-resource-routes.config.ts +171 -0
- package/src/features/right-panel-resources/index.ts +18 -0
- package/src/features/right-panel-resources/types/right-panel-resource.types.ts +36 -0
- package/src/features/right-panel-resources/utils/right-panel-resource-route-resolver.utils.test.ts +38 -0
- package/src/features/right-panel-resources/utils/right-panel-resource-route-resolver.utils.ts +89 -0
- package/src/features/right-panel-resources/utils/right-panel-resource-uri.utils.ts +48 -0
- package/src/features/side-dock/components/side-dock.test.tsx +58 -0
- package/src/features/side-dock/components/side-dock.tsx +117 -0
- package/src/features/side-dock/configs/side-dock-built-in-items.config.ts +43 -0
- package/src/features/side-dock/index.ts +10 -0
- package/src/features/side-dock/managers/side-dock.manager.test.ts +62 -0
- package/src/features/side-dock/managers/side-dock.manager.ts +58 -0
- package/src/features/side-dock/stores/side-dock.store.ts +42 -0
- package/src/features/side-dock/types/side-dock.types.ts +29 -0
- package/src/features/side-dock/utils/side-dock-item.utils.test.ts +77 -0
- package/src/features/side-dock/utils/side-dock-item.utils.ts +126 -0
- package/src/platforms/desktop/components/desktop-app-shell.tsx +3 -0
- package/src/shared/components/doc-browser/doc-browser-context.test.tsx +52 -5
- package/src/shared/components/doc-browser/doc-browser-context.tsx +22 -14
- package/src/shared/components/doc-browser/doc-browser-home-page.tsx +9 -89
- package/src/shared/components/doc-browser/doc-browser-renderer.types.ts +1 -0
- package/src/shared/components/doc-browser/doc-browser.test.tsx +5 -3
- package/src/shared/components/doc-browser/doc-browser.tsx +3 -1
- package/src/shared/components/doc-browser/managers/doc-browser.manager.ts +92 -43
- package/src/shared/components/doc-browser/types/doc-browser.types.ts +24 -0
- package/src/shared/components/doc-browser/utils/doc-browser-route-resolver.utils.ts +65 -0
- package/src/shared/components/doc-browser/utils/doc-browser-state.utils.ts +5 -4
- package/src/shared/lib/resource-uri/index.ts +11 -0
- package/src/shared/lib/resource-uri/types/resource-uri.types.ts +19 -0
- package/src/shared/lib/resource-uri/utils/resource-uri-resolver.utils.test.ts +49 -0
- package/src/shared/lib/resource-uri/utils/resource-uri-resolver.utils.ts +35 -0
- package/src/shared/lib/resource-uri/utils/resource-uri.utils.ts +48 -0
- package/dist/assets/channels-list-page-DXH1qYLs.js +0 -8
- package/dist/assets/chat-page-Cpo0AFHb.js +0 -105
- package/dist/assets/desktop-update-config-Be8x1jNy.js +0 -1
- package/dist/assets/doc-browser-BI7HSaMK.js +0 -3
- package/dist/assets/doc-browser-context-DB6TTHeE.js +0 -1
- package/dist/assets/doc-browser-route-registry.utils-x5ijvk0u.js +0 -1
- package/dist/assets/doc-browser-yTfN7UBH.js +0 -1
- package/dist/assets/index-GVTiCf1s.js +0 -101
- package/dist/assets/index-v0fAGXQW.css +0 -1
- package/dist/assets/mcp-marketplace-page-WUkc7n94.js +0 -1
- package/dist/assets/model-config-BjXMpU9U.js +0 -1
- package/dist/assets/providers-list-C1D5p9kK.js +0 -1
- package/dist/assets/remote-BHeENKxL.js +0 -1
- package/dist/assets/runtime-config-page--O0Wq01S.js +0 -1
- package/dist/assets/search-config-Dzpizo-_.js +0 -1
- package/src/shared/components/doc-browser/utils/doc-browser-route-registry.utils.ts +0 -232
- /package/dist/assets/{book-open-cPX0Za7Z.js → book-open-CRotBOF2.js} +0 -0
- /package/dist/assets/{config-hints-CY1lg9IE.js → config-hints-A7G_jMTU.js} +0 -0
- /package/dist/assets/{config-split-page-BFh_Qixx.js → config-split-page-BnTO1mAM.js} +0 -0
- /package/dist/assets/{external-link-C3usm3se.js → external-link-DHjFdCpy.js} +0 -0
- /package/dist/assets/{play-CqNi0u02.js → play-C-0vo7hi.js} +0 -0
- /package/dist/assets/{plus-BcIC8dG1.js → plus-Cr_akLp2.js} +0 -0
- /package/dist/assets/{save-D10M5vQg.js → save-aYdSdK97.js} +0 -0
- /package/dist/assets/{setting-row-8VkukJoH.js → setting-row-BNu0qhOM.js} +0 -0
- /package/dist/assets/{tabs-custom-BBXu1OGs.js → tabs-custom-CRSN1ekg.js} +0 -0
- /package/dist/assets/{tag-chip-ix15P-r4.js → tag-chip-DEashiw6.js} +0 -0
- /package/dist/assets/{x-Chb07O-Y.js → x-D1v_vb54.js} +0 -0
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { act, renderHook, waitFor } from '@testing-library/react';
|
|
2
2
|
import type { ReactNode } from 'react';
|
|
3
3
|
import { DocBrowserProvider, useDocBrowser } from './doc-browser-context';
|
|
4
|
+
import { RightPanelResourceRouteResolver } from '@/features/right-panel-resources';
|
|
5
|
+
import { DocBrowserManager } from '@/shared/components/doc-browser/managers/doc-browser.manager';
|
|
4
6
|
import { createDefaultDocBrowserState } from '@/shared/components/doc-browser/utils/doc-browser-state.utils';
|
|
5
7
|
import { useDocBrowserStore } from '@/shared/components/doc-browser/stores/doc-browser.store';
|
|
6
8
|
|
|
@@ -13,9 +15,11 @@ vi.mock('@/shared/lib/i18n', async () => {
|
|
|
13
15
|
});
|
|
14
16
|
|
|
15
17
|
const wrapper = ({ children }: { children: ReactNode }) => (
|
|
16
|
-
<DocBrowserProvider>{children}</DocBrowserProvider>
|
|
18
|
+
<DocBrowserProvider manager={testDocBrowserManager}>{children}</DocBrowserProvider>
|
|
17
19
|
);
|
|
18
20
|
|
|
21
|
+
const testDocBrowserManager = new DocBrowserManager(new RightPanelResourceRouteResolver());
|
|
22
|
+
|
|
19
23
|
const docBrowserStorageKey = 'nextclaw.doc-browser.state';
|
|
20
24
|
|
|
21
25
|
beforeEach(() => {
|
|
@@ -175,6 +179,24 @@ describe('DocBrowserProvider dedupe keys', () => {
|
|
|
175
179
|
});
|
|
176
180
|
});
|
|
177
181
|
|
|
182
|
+
it('replaces the closed default start page when opening docs as a new tab', () => {
|
|
183
|
+
const { result } = renderHook(() => useDocBrowser(), { wrapper });
|
|
184
|
+
|
|
185
|
+
act(() => {
|
|
186
|
+
result.current.open(undefined, {
|
|
187
|
+
kind: 'docs',
|
|
188
|
+
newTab: true,
|
|
189
|
+
title: 'Help Docs',
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
expect(result.current.tabs).toHaveLength(1);
|
|
194
|
+
expect(result.current.currentTab).toMatchObject({
|
|
195
|
+
kind: 'docs',
|
|
196
|
+
title: 'Help Docs',
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
|
|
178
200
|
it('infers built-in apps routes without component-level options', () => {
|
|
179
201
|
const { result } = renderHook(() => useDocBrowser(), { wrapper });
|
|
180
202
|
|
|
@@ -190,6 +212,27 @@ describe('DocBrowserProvider dedupe keys', () => {
|
|
|
190
212
|
});
|
|
191
213
|
});
|
|
192
214
|
|
|
215
|
+
it('opens an already resolved panel app target without reparsing its content URL', () => {
|
|
216
|
+
const { result } = renderHook(() => useDocBrowser(), { wrapper });
|
|
217
|
+
|
|
218
|
+
act(() => {
|
|
219
|
+
result.current.openTarget({
|
|
220
|
+
dedupeKey: 'panel-app:demo',
|
|
221
|
+
historyPolicy: 'managed',
|
|
222
|
+
kind: 'panel-app',
|
|
223
|
+
title: 'Demo Panel App',
|
|
224
|
+
url: '/api/panel-apps/demo/content',
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
expect(result.current.currentTab).toMatchObject({
|
|
229
|
+
currentUrl: '/api/panel-apps/demo/content',
|
|
230
|
+
dedupeKey: 'panel-app:demo',
|
|
231
|
+
kind: 'panel-app',
|
|
232
|
+
title: 'Demo Panel App',
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
|
|
193
236
|
it('uses managed history for non-doc routes', () => {
|
|
194
237
|
const { result } = renderHook(() => useDocBrowser(), { wrapper });
|
|
195
238
|
|
|
@@ -238,7 +281,8 @@ describe('DocBrowserProvider dedupe keys', () => {
|
|
|
238
281
|
});
|
|
239
282
|
|
|
240
283
|
expect(result.current.currentTab).toMatchObject({
|
|
241
|
-
|
|
284
|
+
currentUrl: 'data:text/html,A',
|
|
285
|
+
kind: 'content',
|
|
242
286
|
});
|
|
243
287
|
expect(result.current.activeHistoryIndex).toBe(0);
|
|
244
288
|
|
|
@@ -253,7 +297,7 @@ describe('DocBrowserProvider dedupe keys', () => {
|
|
|
253
297
|
historyIndex: 1,
|
|
254
298
|
history: ['data:text/html,A', 'data:text/html,B'],
|
|
255
299
|
});
|
|
256
|
-
expect(result.current.activeHistoryIndex).toBe(
|
|
300
|
+
expect(result.current.activeHistoryIndex).toBe(1);
|
|
257
301
|
});
|
|
258
302
|
|
|
259
303
|
it('records active tab switching in the top-level browser history', () => {
|
|
@@ -400,7 +444,7 @@ describe('DocBrowserProvider persistence', () => {
|
|
|
400
444
|
});
|
|
401
445
|
});
|
|
402
446
|
|
|
403
|
-
it('falls back to the
|
|
447
|
+
it('falls back to the start page when persisted state is malformed', async () => {
|
|
404
448
|
window.localStorage.setItem(docBrowserStorageKey, JSON.stringify({
|
|
405
449
|
version: 1,
|
|
406
450
|
state: {
|
|
@@ -416,6 +460,9 @@ describe('DocBrowserProvider persistence', () => {
|
|
|
416
460
|
const { result } = renderHook(() => useDocBrowser(), { wrapper });
|
|
417
461
|
|
|
418
462
|
expect(result.current.isOpen).toBe(false);
|
|
419
|
-
expect(result.current.currentTab
|
|
463
|
+
expect(result.current.currentTab).toMatchObject({
|
|
464
|
+
currentUrl: 'nextclaw://new-tab',
|
|
465
|
+
kind: 'home',
|
|
466
|
+
});
|
|
420
467
|
});
|
|
421
468
|
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createContext, useContext, useMemo, type ReactNode } from 'react';
|
|
2
|
-
import { DocBrowserManager } from '@/shared/components/doc-browser/managers/doc-browser.manager';
|
|
2
|
+
import type { DocBrowserManager } from '@/shared/components/doc-browser/managers/doc-browser.manager';
|
|
3
3
|
import { useDocBrowserStore } from '@/shared/components/doc-browser/stores/doc-browser.store';
|
|
4
4
|
import type { DocBrowserContextValue } from '@/shared/components/doc-browser/types/doc-browser.types';
|
|
5
5
|
|
|
@@ -8,6 +8,8 @@ export type {
|
|
|
8
8
|
DocBrowserContextValue,
|
|
9
9
|
DocBrowserMode,
|
|
10
10
|
DocBrowserOpenOptions,
|
|
11
|
+
DocBrowserRouteResolver,
|
|
12
|
+
DocBrowserRouteTarget,
|
|
11
13
|
DocBrowserState,
|
|
12
14
|
DocBrowserStateUpdate,
|
|
13
15
|
DocBrowserTab,
|
|
@@ -21,7 +23,6 @@ export {
|
|
|
21
23
|
} from '@/shared/components/doc-browser/utils/doc-browser-url.utils';
|
|
22
24
|
|
|
23
25
|
const DocBrowserContext = createContext<DocBrowserContextValue | null>(null);
|
|
24
|
-
const docBrowserManager = new DocBrowserManager();
|
|
25
26
|
|
|
26
27
|
export function useDocBrowser(): DocBrowserContextValue {
|
|
27
28
|
const ctx = useContext(DocBrowserContext);
|
|
@@ -29,7 +30,13 @@ export function useDocBrowser(): DocBrowserContextValue {
|
|
|
29
30
|
return ctx;
|
|
30
31
|
}
|
|
31
32
|
|
|
32
|
-
export function DocBrowserProvider({
|
|
33
|
+
export function DocBrowserProvider({
|
|
34
|
+
children,
|
|
35
|
+
manager,
|
|
36
|
+
}: {
|
|
37
|
+
children: ReactNode;
|
|
38
|
+
manager: DocBrowserManager;
|
|
39
|
+
}) {
|
|
33
40
|
const snapshot = useDocBrowserStore((state) => state.snapshot);
|
|
34
41
|
const currentTab = useMemo(() => {
|
|
35
42
|
return snapshot.tabs.find((tab) => tab.id === snapshot.activeTabId) ?? snapshot.tabs[0];
|
|
@@ -38,17 +45,18 @@ export function DocBrowserProvider({ children }: { children: ReactNode }) {
|
|
|
38
45
|
const value = useMemo<DocBrowserContextValue>(() => ({
|
|
39
46
|
...snapshot,
|
|
40
47
|
currentTab,
|
|
41
|
-
open:
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
48
|
+
open: manager.open,
|
|
49
|
+
openTarget: manager.openTarget,
|
|
50
|
+
openNewTab: manager.openNewTab,
|
|
51
|
+
close: manager.close,
|
|
52
|
+
toggleMode: manager.toggleMode,
|
|
53
|
+
navigate: manager.navigate,
|
|
54
|
+
syncUrl: manager.syncUrl,
|
|
55
|
+
goBack: manager.goBack,
|
|
56
|
+
goForward: manager.goForward,
|
|
57
|
+
closeTab: manager.closeTab,
|
|
58
|
+
setActiveTab: manager.setActiveTab,
|
|
59
|
+
}), [currentTab, manager, snapshot]);
|
|
52
60
|
|
|
53
61
|
return (
|
|
54
62
|
<DocBrowserContext.Provider value={value}>
|
|
@@ -1,94 +1,14 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
3
|
-
BookOpen,
|
|
4
|
-
Boxes,
|
|
5
|
-
BrainCircuit,
|
|
6
|
-
Grid3X3,
|
|
7
|
-
Wrench,
|
|
8
|
-
} from 'lucide-react';
|
|
9
|
-
import { useNavigate } from 'react-router-dom';
|
|
10
|
-
import type { DocBrowserContextValue } from './doc-browser-context';
|
|
11
|
-
import {
|
|
12
|
-
docBrowserRouteRegistry,
|
|
13
|
-
type DocBrowserHomeNavigationItem,
|
|
14
|
-
type DocBrowserNavigationTarget,
|
|
15
|
-
} from './utils/doc-browser-route-registry.utils';
|
|
16
|
-
import { cn } from '@/shared/lib/utils';
|
|
17
|
-
|
|
18
|
-
type DocBrowserHomePageProps = {
|
|
19
|
-
open: DocBrowserContextValue['open'];
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
type DocBrowserHomeItem = {
|
|
23
|
-
accentClassName: string;
|
|
24
|
-
icon: ReactNode;
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
const HOME_ITEM_VIEW_BY_ID: Record<string, DocBrowserHomeItem> = {
|
|
28
|
-
apps: {
|
|
29
|
-
accentClassName: 'bg-emerald-50 text-emerald-700 border-emerald-100',
|
|
30
|
-
icon: <Boxes className="h-5 w-5" />,
|
|
31
|
-
},
|
|
32
|
-
'service-apps': {
|
|
33
|
-
accentClassName: 'bg-sky-50 text-sky-700 border-sky-100',
|
|
34
|
-
icon: <Grid3X3 className="h-5 w-5" />,
|
|
35
|
-
},
|
|
36
|
-
docs: {
|
|
37
|
-
accentClassName: 'bg-amber-50 text-amber-700 border-amber-100',
|
|
38
|
-
icon: <BookOpen className="h-5 w-5" />,
|
|
39
|
-
},
|
|
40
|
-
'skill-marketplace': {
|
|
41
|
-
accentClassName: 'bg-rose-50 text-rose-700 border-rose-100',
|
|
42
|
-
icon: <BrainCircuit className="h-5 w-5" />,
|
|
43
|
-
},
|
|
44
|
-
'mcp-marketplace': {
|
|
45
|
-
accentClassName: 'bg-indigo-50 text-indigo-700 border-indigo-100',
|
|
46
|
-
icon: <Wrench className="h-5 w-5" />,
|
|
47
|
-
},
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
function resolveHomeItemView(item: DocBrowserHomeNavigationItem): DocBrowserHomeItem {
|
|
51
|
-
return HOME_ITEM_VIEW_BY_ID[item.id] ?? {
|
|
52
|
-
accentClassName: 'bg-gray-50 text-gray-700 border-gray-100',
|
|
53
|
-
icon: <Boxes className="h-5 w-5" />,
|
|
54
|
-
};
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
export function DocBrowserHomePage({ open }: DocBrowserHomePageProps) {
|
|
58
|
-
const navigate = useNavigate();
|
|
59
|
-
const items = docBrowserRouteRegistry.getHomeNavigationItems();
|
|
60
|
-
|
|
61
|
-
const openTarget = (target: DocBrowserNavigationTarget) => {
|
|
62
|
-
if (target.type === 'app-route') {
|
|
63
|
-
navigate(target.path);
|
|
64
|
-
return;
|
|
65
|
-
}
|
|
66
|
-
open(target.url, target.options);
|
|
67
|
-
};
|
|
1
|
+
import { Boxes } from 'lucide-react';
|
|
2
|
+
import { t } from '@/shared/lib/i18n';
|
|
68
3
|
|
|
4
|
+
export function DocBrowserHomePage() {
|
|
69
5
|
return (
|
|
70
|
-
<div className="h-full
|
|
71
|
-
<div className="
|
|
72
|
-
<
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
<button
|
|
77
|
-
key={item.id}
|
|
78
|
-
type="button"
|
|
79
|
-
onClick={() => openTarget(item.target)}
|
|
80
|
-
className="group flex min-h-[82px] flex-col items-center gap-2 rounded-lg px-2 py-2 text-center transition-colors hover:bg-white/80"
|
|
81
|
-
>
|
|
82
|
-
<span className={cn('flex h-12 w-12 shrink-0 items-center justify-center rounded-2xl border shadow-[0_1px_2px_rgba(30,20,10,0.04)] transition-transform group-hover:scale-[1.03]', view.accentClassName)}>
|
|
83
|
-
{view.icon}
|
|
84
|
-
</span>
|
|
85
|
-
<span className="min-w-0 max-w-full">
|
|
86
|
-
<span className="line-clamp-2 block text-xs font-medium leading-snug text-gray-800">{item.label}</span>
|
|
87
|
-
</span>
|
|
88
|
-
</button>
|
|
89
|
-
);
|
|
90
|
-
})}
|
|
91
|
-
</div>
|
|
6
|
+
<div className="flex h-full items-center justify-center bg-background px-5 py-6">
|
|
7
|
+
<div className="flex max-w-[260px] flex-col items-center gap-3 text-center">
|
|
8
|
+
<span className="flex h-12 w-12 items-center justify-center rounded-lg border border-gray-100 bg-gray-50 text-gray-600 shadow-[0_1px_2px_rgba(30,20,10,0.04)]">
|
|
9
|
+
<Boxes className="h-5 w-5" />
|
|
10
|
+
</span>
|
|
11
|
+
<span className="text-sm font-medium text-gray-800">{t('docBrowserHomeTitle')}</span>
|
|
92
12
|
</div>
|
|
93
13
|
</div>
|
|
94
14
|
);
|
|
@@ -4,6 +4,7 @@ import type { DocBrowserContextValue, DocBrowserTab } from './doc-browser-contex
|
|
|
4
4
|
export type DocBrowserCustomTabRenderParams = {
|
|
5
5
|
currentUrl: string;
|
|
6
6
|
open: DocBrowserContextValue['open'];
|
|
7
|
+
openTarget: DocBrowserContextValue['openTarget'];
|
|
7
8
|
refreshIframe: () => void;
|
|
8
9
|
tab: DocBrowserTab;
|
|
9
10
|
};
|
|
@@ -2,6 +2,7 @@ import { fireEvent, render, screen } from "@testing-library/react";
|
|
|
2
2
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
3
3
|
import { DocBrowser } from "@/shared/components/doc-browser/doc-browser";
|
|
4
4
|
import type { DocBrowserContextValue, DocBrowserTab } from "@/shared/components/doc-browser/doc-browser-context";
|
|
5
|
+
import { PANEL_APPS_DOC_BROWSER_RENDERERS } from "@/features/panel-apps";
|
|
5
6
|
|
|
6
7
|
const { navigateMock } = vi.hoisted(() => ({
|
|
7
8
|
navigateMock: vi.fn(),
|
|
@@ -44,6 +45,7 @@ const { docBrowserState } = vi.hoisted<{ docBrowserState: DocBrowserContextValue
|
|
|
44
45
|
navVersion: 0,
|
|
45
46
|
},
|
|
46
47
|
open: vi.fn(),
|
|
48
|
+
openTarget: vi.fn(),
|
|
47
49
|
openNewTab: vi.fn(),
|
|
48
50
|
close: vi.fn(),
|
|
49
51
|
toggleMode: vi.fn(),
|
|
@@ -138,7 +140,7 @@ describe("DocBrowser", () => {
|
|
|
138
140
|
});
|
|
139
141
|
|
|
140
142
|
it("keeps browser window controls on the tab strip", () => {
|
|
141
|
-
render(<DocBrowser />);
|
|
143
|
+
render(<DocBrowser customTabRenderers={PANEL_APPS_DOC_BROWSER_RENDERERS} />);
|
|
142
144
|
|
|
143
145
|
const tabStrip = screen.getByTestId("doc-browser-tab-strip");
|
|
144
146
|
const tabActions = screen.getByTestId("doc-browser-tab-actions");
|
|
@@ -176,7 +178,7 @@ describe("DocBrowser", () => {
|
|
|
176
178
|
docBrowserState.activeHistoryIndex = 1;
|
|
177
179
|
docBrowserState.currentTab = historyTab;
|
|
178
180
|
|
|
179
|
-
render(<DocBrowser />);
|
|
181
|
+
render(<DocBrowser customTabRenderers={PANEL_APPS_DOC_BROWSER_RENDERERS} />);
|
|
180
182
|
|
|
181
183
|
fireEvent.click(screen.getByTitle("Back"));
|
|
182
184
|
fireEvent.click(screen.getByTitle("Forward"));
|
|
@@ -249,7 +251,7 @@ describe("DocBrowser", () => {
|
|
|
249
251
|
docBrowserState.activeTabId = homeTab.id;
|
|
250
252
|
docBrowserState.currentTab = homeTab;
|
|
251
253
|
|
|
252
|
-
render(<DocBrowser />);
|
|
254
|
+
render(<DocBrowser customTabRenderers={PANEL_APPS_DOC_BROWSER_RENDERERS} />);
|
|
253
255
|
|
|
254
256
|
expect(screen.getAllByText("Start Page").length).toBeGreaterThan(0);
|
|
255
257
|
expect(screen.getByText("Apps")).toBeTruthy();
|
|
@@ -62,6 +62,7 @@ export function DocBrowser({
|
|
|
62
62
|
activeHistoryIndex,
|
|
63
63
|
currentTab,
|
|
64
64
|
open,
|
|
65
|
+
openTarget,
|
|
65
66
|
openNewTab,
|
|
66
67
|
close,
|
|
67
68
|
toggleMode,
|
|
@@ -255,12 +256,13 @@ export function DocBrowser({
|
|
|
255
256
|
const customRenderParams = currentTab ? {
|
|
256
257
|
currentUrl,
|
|
257
258
|
open,
|
|
259
|
+
openTarget,
|
|
258
260
|
refreshIframe,
|
|
259
261
|
tab: currentTab,
|
|
260
262
|
} : undefined;
|
|
261
263
|
const customToolbar = customRenderParams ? customRenderer?.renderToolbar?.(customRenderParams) : null;
|
|
262
264
|
const customContent = customRenderParams
|
|
263
|
-
? customRenderer?.renderContent?.(customRenderParams) ?? (isHomeTab ? <DocBrowserHomePage
|
|
265
|
+
? customRenderer?.renderContent?.(customRenderParams) ?? (isHomeTab ? <DocBrowserHomePage /> : null)
|
|
264
266
|
: null;
|
|
265
267
|
const iframeSandbox = currentTab
|
|
266
268
|
? customRenderer?.getIframeSandbox?.(currentTab) ?? 'allow-same-origin allow-scripts allow-popups allow-forms'
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
DocBrowserActiveHistoryEntry,
|
|
3
3
|
DocBrowserOpenOptions,
|
|
4
|
+
DocBrowserRouteResolver,
|
|
5
|
+
DocBrowserRouteTarget,
|
|
4
6
|
DocBrowserState,
|
|
5
7
|
DocBrowserStateUpdate,
|
|
6
8
|
DocBrowserTab,
|
|
7
|
-
DocBrowserTabKind,
|
|
8
9
|
} from '@/shared/components/doc-browser/types/doc-browser.types';
|
|
9
10
|
import {
|
|
10
11
|
createDocBrowserActiveHistoryEntry,
|
|
@@ -13,13 +14,14 @@ import {
|
|
|
13
14
|
} from '@/shared/components/doc-browser/utils/doc-browser-state.utils';
|
|
14
15
|
import {
|
|
15
16
|
DOC_BROWSER_HOME_TAB_KIND,
|
|
17
|
+
DOC_BROWSER_HOME_URL,
|
|
16
18
|
getDefaultDocsUrl,
|
|
17
19
|
} from '@/shared/components/doc-browser/utils/doc-browser-url.utils';
|
|
18
|
-
import {
|
|
19
|
-
docBrowserRouteRegistry,
|
|
20
|
-
} from '@/shared/components/doc-browser/utils/doc-browser-route-registry.utils';
|
|
20
|
+
import { DefaultDocBrowserRouteResolver } from '@/shared/components/doc-browser/utils/doc-browser-route-resolver.utils';
|
|
21
21
|
import { useDocBrowserStore } from '@/shared/components/doc-browser/stores/doc-browser.store';
|
|
22
22
|
|
|
23
|
+
const defaultDocBrowserRouteResolver = new DefaultDocBrowserRouteResolver();
|
|
24
|
+
|
|
23
25
|
function updateTab(
|
|
24
26
|
state: DocBrowserState,
|
|
25
27
|
tabId: string,
|
|
@@ -44,17 +46,22 @@ function appendManualNavigation(tab: DocBrowserTab, url: string): Pick<DocBrowse
|
|
|
44
46
|
}
|
|
45
47
|
|
|
46
48
|
function areActiveHistoryEntriesEquivalent(
|
|
49
|
+
routeResolver: DocBrowserRouteResolver,
|
|
47
50
|
current: DocBrowserActiveHistoryEntry,
|
|
48
51
|
next: DocBrowserActiveHistoryEntry,
|
|
49
52
|
): boolean {
|
|
50
53
|
return current.tabId === next.tabId
|
|
51
54
|
&& current.kind === next.kind
|
|
52
|
-
&&
|
|
55
|
+
&& routeResolver.areUrlsEquivalent(current.url, next.url, current.kind, next.kind);
|
|
53
56
|
}
|
|
54
57
|
|
|
55
|
-
function pushActiveHistory(
|
|
58
|
+
function pushActiveHistory(
|
|
59
|
+
routeResolver: DocBrowserRouteResolver,
|
|
60
|
+
state: DocBrowserState,
|
|
61
|
+
entry: DocBrowserActiveHistoryEntry,
|
|
62
|
+
): DocBrowserState {
|
|
56
63
|
const current = state.activeHistory[state.activeHistoryIndex];
|
|
57
|
-
if (current && areActiveHistoryEntriesEquivalent(current, entry)) {
|
|
64
|
+
if (current && areActiveHistoryEntriesEquivalent(routeResolver, current, entry)) {
|
|
58
65
|
return state;
|
|
59
66
|
}
|
|
60
67
|
const activeHistory = [...state.activeHistory.slice(0, state.activeHistoryIndex + 1), entry];
|
|
@@ -65,26 +72,30 @@ function pushActiveHistory(state: DocBrowserState, entry: DocBrowserActiveHistor
|
|
|
65
72
|
};
|
|
66
73
|
}
|
|
67
74
|
|
|
68
|
-
function findTabHistoryIndex(tab: DocBrowserTab, url: string): number {
|
|
75
|
+
function findTabHistoryIndex(routeResolver: DocBrowserRouteResolver, tab: DocBrowserTab, url: string): number {
|
|
69
76
|
for (let index = tab.history.length - 1; index >= 0; index -= 1) {
|
|
70
|
-
if (
|
|
77
|
+
if (routeResolver.areUrlsEquivalent(tab.history[index], url, tab.kind, tab.kind)) {
|
|
71
78
|
return index;
|
|
72
79
|
}
|
|
73
80
|
}
|
|
74
81
|
return -1;
|
|
75
82
|
}
|
|
76
83
|
|
|
77
|
-
function restoreActiveHistoryEntry(
|
|
84
|
+
function restoreActiveHistoryEntry(
|
|
85
|
+
routeResolver: DocBrowserRouteResolver,
|
|
86
|
+
state: DocBrowserState,
|
|
87
|
+
entry: DocBrowserActiveHistoryEntry,
|
|
88
|
+
): DocBrowserState {
|
|
78
89
|
const tab = state.tabs.find((item) => item.id === entry.tabId);
|
|
79
90
|
if (!tab) {
|
|
80
91
|
return state;
|
|
81
92
|
}
|
|
82
|
-
const target =
|
|
93
|
+
const target = routeResolver.resolveOpenTarget({
|
|
83
94
|
activeTab: tab,
|
|
84
95
|
kind: entry.kind,
|
|
85
96
|
url: entry.url,
|
|
86
97
|
});
|
|
87
|
-
const historyIndex = findTabHistoryIndex(tab, target.url);
|
|
98
|
+
const historyIndex = findTabHistoryIndex(routeResolver, tab, target.url);
|
|
88
99
|
return updateTab(
|
|
89
100
|
{
|
|
90
101
|
...state,
|
|
@@ -104,12 +115,9 @@ function restoreActiveHistoryEntry(state: DocBrowserState, entry: DocBrowserActi
|
|
|
104
115
|
}
|
|
105
116
|
|
|
106
117
|
function updateTabForOpen(
|
|
118
|
+
routeResolver: DocBrowserRouteResolver,
|
|
107
119
|
tab: DocBrowserTab,
|
|
108
|
-
target:
|
|
109
|
-
kind: DocBrowserTabKind;
|
|
110
|
-
title: string;
|
|
111
|
-
url: string;
|
|
112
|
-
},
|
|
120
|
+
target: DocBrowserRouteTarget,
|
|
113
121
|
options?: DocBrowserOpenOptions,
|
|
114
122
|
dedupeKey?: string,
|
|
115
123
|
): DocBrowserTab {
|
|
@@ -120,7 +128,7 @@ function updateTabForOpen(
|
|
|
120
128
|
dedupeKey,
|
|
121
129
|
};
|
|
122
130
|
|
|
123
|
-
if (
|
|
131
|
+
if (routeResolver.areUrlsEquivalent(tab.currentUrl, target.url, tab.kind, target.kind)) {
|
|
124
132
|
return baseTab;
|
|
125
133
|
}
|
|
126
134
|
|
|
@@ -132,25 +140,27 @@ function updateTabForOpen(
|
|
|
132
140
|
};
|
|
133
141
|
}
|
|
134
142
|
|
|
135
|
-
function
|
|
143
|
+
function isClosedDefaultHomeState(state: DocBrowserState, activeTab?: DocBrowserTab): boolean {
|
|
144
|
+
return !state.isOpen
|
|
145
|
+
&& state.tabs.length === 1
|
|
146
|
+
&& activeTab?.kind === DOC_BROWSER_HOME_TAB_KIND
|
|
147
|
+
&& activeTab.currentUrl === DOC_BROWSER_HOME_URL;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function openResolvedDocBrowserState(
|
|
151
|
+
routeResolver: DocBrowserRouteResolver,
|
|
136
152
|
prev: DocBrowserState,
|
|
137
|
-
|
|
153
|
+
target: DocBrowserRouteTarget,
|
|
138
154
|
options?: DocBrowserOpenOptions,
|
|
139
155
|
): DocBrowserState {
|
|
140
156
|
const {
|
|
141
157
|
activate,
|
|
142
158
|
dedupeKey: rawDedupeKey,
|
|
143
|
-
kind,
|
|
144
159
|
newTab: shouldForceNewTab,
|
|
145
160
|
title,
|
|
146
161
|
} = options ?? {};
|
|
147
|
-
const activeTab = prev.tabs.find((tab) => tab.id === prev.activeTabId) ?? prev.tabs[0];
|
|
148
|
-
const target = docBrowserRouteRegistry.resolveOpenTarget({
|
|
149
|
-
activeTab,
|
|
150
|
-
kind,
|
|
151
|
-
url,
|
|
152
|
-
});
|
|
153
162
|
const dedupeKey = rawDedupeKey?.trim() || target.dedupeKey;
|
|
163
|
+
const activeTab = prev.tabs.find((tab) => tab.id === prev.activeTabId) ?? prev.tabs[0];
|
|
154
164
|
const matchedTab = dedupeKey
|
|
155
165
|
? prev.tabs.find((tab) => tab.dedupeKey === dedupeKey)
|
|
156
166
|
: undefined;
|
|
@@ -159,7 +169,7 @@ function openDocBrowserState(
|
|
|
159
169
|
const next = updateTab(
|
|
160
170
|
prev,
|
|
161
171
|
matchedTab.id,
|
|
162
|
-
(tab) => updateTabForOpen(tab, target, options, dedupeKey),
|
|
172
|
+
(tab) => updateTabForOpen(routeResolver, tab, target, options, dedupeKey),
|
|
163
173
|
);
|
|
164
174
|
const shouldActivate = activate !== false;
|
|
165
175
|
const activeTabId = shouldActivate ? matchedTab.id : prev.activeTabId;
|
|
@@ -170,38 +180,76 @@ function openDocBrowserState(
|
|
|
170
180
|
};
|
|
171
181
|
const shouldPushActiveHistory = shouldActivate || matchedTab.id === prev.activeTabId;
|
|
172
182
|
return shouldPushActiveHistory
|
|
173
|
-
? pushActiveHistory(nextState, { kind: target.kind, tabId: matchedTab.id, url: target.url })
|
|
183
|
+
? pushActiveHistory(routeResolver, nextState, { kind: target.kind, tabId: matchedTab.id, url: target.url })
|
|
174
184
|
: nextState;
|
|
175
185
|
}
|
|
176
186
|
|
|
177
187
|
if (shouldForceNewTab || dedupeKey || !activeTab || activeTab.kind !== target.kind) {
|
|
178
188
|
const newTab = createDocBrowserTab(target.url, target.kind, title ?? target.title, dedupeKey);
|
|
189
|
+
if (isClosedDefaultHomeState(prev, activeTab)) {
|
|
190
|
+
const nextState = {
|
|
191
|
+
...prev,
|
|
192
|
+
isOpen: true,
|
|
193
|
+
tabs: [newTab],
|
|
194
|
+
activeTabId: newTab.id,
|
|
195
|
+
activeHistory: [createDocBrowserActiveHistoryEntry(newTab)],
|
|
196
|
+
activeHistoryIndex: 0,
|
|
197
|
+
};
|
|
198
|
+
return nextState;
|
|
199
|
+
}
|
|
179
200
|
const nextState = {
|
|
180
201
|
...prev,
|
|
181
202
|
isOpen: true,
|
|
182
203
|
tabs: [...prev.tabs, newTab],
|
|
183
204
|
activeTabId: newTab.id,
|
|
184
205
|
};
|
|
185
|
-
return pushActiveHistory(nextState, createDocBrowserActiveHistoryEntry(newTab));
|
|
206
|
+
return pushActiveHistory(routeResolver, nextState, createDocBrowserActiveHistoryEntry(newTab));
|
|
186
207
|
}
|
|
187
208
|
|
|
188
209
|
const next = updateActiveTab(
|
|
189
210
|
prev,
|
|
190
|
-
(tab) => updateTabForOpen(tab, target, options, dedupeKey),
|
|
211
|
+
(tab) => updateTabForOpen(routeResolver, tab, target, options, dedupeKey),
|
|
191
212
|
);
|
|
192
213
|
return pushActiveHistory(
|
|
214
|
+
routeResolver,
|
|
193
215
|
{ ...next, isOpen: true },
|
|
194
216
|
{ kind: target.kind, tabId: prev.activeTabId, url: target.url },
|
|
195
217
|
);
|
|
196
218
|
}
|
|
197
219
|
|
|
220
|
+
function openDocBrowserState(
|
|
221
|
+
routeResolver: DocBrowserRouteResolver,
|
|
222
|
+
prev: DocBrowserState,
|
|
223
|
+
url?: string,
|
|
224
|
+
options?: DocBrowserOpenOptions,
|
|
225
|
+
): DocBrowserState {
|
|
226
|
+
const activeTab = prev.tabs.find((tab) => tab.id === prev.activeTabId) ?? prev.tabs[0];
|
|
227
|
+
const target = routeResolver.resolveOpenTarget({
|
|
228
|
+
activeTab,
|
|
229
|
+
kind: options?.kind,
|
|
230
|
+
url,
|
|
231
|
+
});
|
|
232
|
+
return openResolvedDocBrowserState(routeResolver, prev, target, options);
|
|
233
|
+
}
|
|
234
|
+
|
|
198
235
|
export class DocBrowserManager {
|
|
236
|
+
constructor(private readonly routeResolver: DocBrowserRouteResolver = defaultDocBrowserRouteResolver) {}
|
|
237
|
+
|
|
199
238
|
private readonly setSnapshot = (next: DocBrowserStateUpdate): void => {
|
|
200
239
|
useDocBrowserStore.getState().setSnapshot(next);
|
|
201
240
|
};
|
|
202
241
|
|
|
203
242
|
readonly open = (url?: string, options?: DocBrowserOpenOptions): void => {
|
|
204
|
-
this.setSnapshot((prev) => openDocBrowserState(prev, url, options));
|
|
243
|
+
this.setSnapshot((prev) => openDocBrowserState(this.routeResolver, prev, url, options));
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
readonly openTarget = (target: DocBrowserRouteTarget, options?: DocBrowserOpenOptions): void => {
|
|
247
|
+
this.setSnapshot((prev) => openResolvedDocBrowserState(this.routeResolver, prev, target, {
|
|
248
|
+
...options,
|
|
249
|
+
dedupeKey: options?.dedupeKey ?? target.dedupeKey,
|
|
250
|
+
kind: options?.kind ?? target.kind,
|
|
251
|
+
title: options?.title ?? target.title,
|
|
252
|
+
}));
|
|
205
253
|
};
|
|
206
254
|
|
|
207
255
|
readonly openNewTab = (): void => {
|
|
@@ -229,12 +277,12 @@ export class DocBrowserManager {
|
|
|
229
277
|
}
|
|
230
278
|
|
|
231
279
|
const next = updateActiveTab(prev, (tab) => {
|
|
232
|
-
if (!
|
|
280
|
+
if (!this.routeResolver.usesManagedHistory(tab)) {
|
|
233
281
|
return tab;
|
|
234
282
|
}
|
|
235
|
-
const target =
|
|
283
|
+
const target = this.routeResolver.resolveOpenTarget({ activeTab: tab, url });
|
|
236
284
|
|
|
237
|
-
if (
|
|
285
|
+
if (this.routeResolver.areUrlsEquivalent(tab.currentUrl, target.url, tab.kind, target.kind)) {
|
|
238
286
|
return tab;
|
|
239
287
|
}
|
|
240
288
|
|
|
@@ -250,7 +298,7 @@ export class DocBrowserManager {
|
|
|
250
298
|
});
|
|
251
299
|
const currentTab = next.tabs.find((tab) => tab.id === next.activeTabId);
|
|
252
300
|
return currentTab
|
|
253
|
-
? pushActiveHistory(next, createDocBrowserActiveHistoryEntry(currentTab))
|
|
301
|
+
? pushActiveHistory(this.routeResolver, next, createDocBrowserActiveHistoryEntry(currentTab))
|
|
254
302
|
: next;
|
|
255
303
|
});
|
|
256
304
|
};
|
|
@@ -267,12 +315,12 @@ export class DocBrowserManager {
|
|
|
267
315
|
}
|
|
268
316
|
|
|
269
317
|
const next = updateActiveTab(prev, (tab) => {
|
|
270
|
-
if (!
|
|
318
|
+
if (!this.routeResolver.usesManagedHistory(tab)) {
|
|
271
319
|
return tab;
|
|
272
320
|
}
|
|
273
|
-
const target =
|
|
321
|
+
const target = this.routeResolver.resolveOpenTarget({ activeTab: tab, kind: tab.kind, url });
|
|
274
322
|
|
|
275
|
-
if (
|
|
323
|
+
if (this.routeResolver.areUrlsEquivalent(tab.currentUrl, target.url, tab.kind, target.kind)) {
|
|
276
324
|
return tab;
|
|
277
325
|
}
|
|
278
326
|
|
|
@@ -287,7 +335,7 @@ export class DocBrowserManager {
|
|
|
287
335
|
});
|
|
288
336
|
const currentTab = next.tabs.find((tab) => tab.id === next.activeTabId);
|
|
289
337
|
return currentTab
|
|
290
|
-
? pushActiveHistory(next, createDocBrowserActiveHistoryEntry(currentTab))
|
|
338
|
+
? pushActiveHistory(this.routeResolver, next, createDocBrowserActiveHistoryEntry(currentTab))
|
|
291
339
|
: next;
|
|
292
340
|
});
|
|
293
341
|
};
|
|
@@ -302,7 +350,7 @@ export class DocBrowserManager {
|
|
|
302
350
|
if (!entry) {
|
|
303
351
|
return prev;
|
|
304
352
|
}
|
|
305
|
-
return restoreActiveHistoryEntry({ ...prev, activeHistoryIndex }, entry);
|
|
353
|
+
return restoreActiveHistoryEntry(this.routeResolver, { ...prev, activeHistoryIndex }, entry);
|
|
306
354
|
});
|
|
307
355
|
};
|
|
308
356
|
|
|
@@ -316,7 +364,7 @@ export class DocBrowserManager {
|
|
|
316
364
|
if (!entry) {
|
|
317
365
|
return prev;
|
|
318
366
|
}
|
|
319
|
-
return restoreActiveHistoryEntry({ ...prev, activeHistoryIndex }, entry);
|
|
367
|
+
return restoreActiveHistoryEntry(this.routeResolver, { ...prev, activeHistoryIndex }, entry);
|
|
320
368
|
});
|
|
321
369
|
};
|
|
322
370
|
|
|
@@ -361,7 +409,7 @@ export class DocBrowserManager {
|
|
|
361
409
|
),
|
|
362
410
|
};
|
|
363
411
|
return prev.activeTabId === tabId
|
|
364
|
-
? pushActiveHistory(reconciledState, createDocBrowserActiveHistoryEntry(nextActiveTab))
|
|
412
|
+
? pushActiveHistory(this.routeResolver, reconciledState, createDocBrowserActiveHistoryEntry(nextActiveTab))
|
|
365
413
|
: reconciledState;
|
|
366
414
|
});
|
|
367
415
|
};
|
|
@@ -376,6 +424,7 @@ export class DocBrowserManager {
|
|
|
376
424
|
return prev;
|
|
377
425
|
}
|
|
378
426
|
return pushActiveHistory(
|
|
427
|
+
this.routeResolver,
|
|
379
428
|
{ ...prev, activeTabId: tabId, isOpen: true },
|
|
380
429
|
createDocBrowserActiveHistoryEntry(tab),
|
|
381
430
|
);
|