@nextclaw/ui 0.27.4 → 0.27.5-beta.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/CHANGELOG.md +17 -0
- package/dist/.vite/manifest.json +95 -95
- package/dist/assets/{account-panel-Cfx3ocPY.js → account-panel-C83GDFuy.js} +1 -1
- package/dist/assets/agents-CJA15zzv.js +1 -0
- package/dist/assets/{agents-BdeFtIOV.js → agents-Dr2KkEp2.js} +1 -1
- package/dist/assets/{app-presenter-provider-BB3Xl8b0.js → app-presenter-provider-Cx9MwMtb.js} +17 -16
- package/dist/assets/{appearance-settings-page-DiSRSDN3.js → appearance-settings-page-3IfIzjic.js} +1 -1
- package/dist/assets/apps-CvIlb9Rn.js +1 -0
- package/dist/assets/{confirm-dialog-jz2cQKId.js → confirm-dialog-D30HJBXN.js} +1 -1
- package/dist/assets/{context-menu-C0qywo73.js → context-menu-C6m-WNpl.js} +65 -65
- package/dist/assets/cron-BWZWu80t.js +1 -0
- package/dist/assets/{dialog-DMKYZDFu.js → dialog-DB_l9LK0.js} +1 -1
- package/dist/assets/doc-browser-DQDsmRlr.js +1 -0
- package/dist/assets/inbox-CIsD65lW.js +1 -0
- package/dist/assets/{keyboard-shortcuts-settings-page-BFBXVf20.js → keyboard-shortcuts-settings-page-L4kLGuB8.js} +1 -1
- package/dist/assets/{main-Bp1iozNB.js → main-BNWRrWc_.js} +2 -2
- package/dist/assets/{marketplace-BTDWnHP7.js → marketplace-BH1Sq8dL.js} +1 -1
- package/dist/assets/mcp-marketplace-page-BA-InAbJ.js +1 -0
- package/dist/assets/{model-config-page-CeVvZ29o.js → model-config-page-zqVWyIT0.js} +1 -1
- package/dist/assets/panel-apps-CMX4iWwQ.js +1 -0
- package/dist/assets/{panelStandalone-DomappXm.js → panelStandalone-C38o5B4Q.js} +1 -1
- package/dist/assets/{projects-fPTOn2Wz.js → projects-DNe43T2d.js} +1 -1
- package/dist/assets/{provider-models-D8WOdsdu.js → provider-models-zGsDUBHG.js} +1 -1
- package/dist/assets/{provider-scoped-model-input-BOPkUOtD.js → provider-scoped-model-input-DpEyP1h8.js} +1 -1
- package/dist/assets/{providers-config-page-BilPL0kF.js → providers-config-page-J-1auWPu.js} +1 -1
- package/dist/assets/{pwa-Cp7pPvsZ.js → pwa-jmuWbrqH.js} +1 -1
- package/dist/assets/remote-C7H2o46-.js +1 -0
- package/dist/assets/{runtime-config-page-P5RUq3YK.js → runtime-config-page-CJhqeDcu.js} +1 -1
- package/dist/assets/{service-apps-panel-BReYr_ni.js → service-apps-panel-Dj9okUgp.js} +1 -1
- package/dist/assets/{theme-provider-BsJ6HxeX.js → theme-provider-D4rd-tCP.js} +1 -1
- package/dist/index.html +10 -10
- package/dist/panel-standalone.html +4 -4
- package/package.json +7 -7
- package/src/features/chat/features/message/utils/__tests__/chat-message-timeline.utils.test.ts +16 -0
- package/src/features/chat/features/message/utils/__tests__/chat-message.utils.test.ts +17 -0
- package/src/features/chat/features/message/utils/chat-message-timeline.utils.ts +3 -3
- package/src/features/chat/features/message/utils/chat-message.utils.ts +11 -4
- package/src/features/chat/features/message/utils/chat-user-question-timeline.utils.ts +42 -0
- package/src/features/marketplace/components/detail-doc/__tests__/marketplace-detail-doc.test.tsx +2 -0
- package/src/features/panel-apps/components/panel-app-runtime-surface.tsx +3 -1
- package/src/features/panel-apps/utils/panel-app-doc-browser.utils.tsx +25 -10
- package/src/features/right-panel-resources/components/__tests__/system-object-resource.test.tsx +25 -3
- package/src/features/right-panel-resources/components/native-object-resource.tsx +3 -1
- package/src/features/right-panel-resources/components/system-object-resource.tsx +15 -1
- package/src/features/right-panel-resources/pages/resource-page.tsx +29 -2
- package/src/shared/components/doc-browser/__tests__/doc-browser.test.tsx +16 -13
- package/src/shared/components/doc-browser/doc-browser-renderer.types.ts +3 -0
- package/src/shared/components/doc-browser/doc-browser.tsx +7 -1
- package/src/shared/components/ui/context-menu/context-menu-items.tsx +1 -0
- package/src/shared/components/ui/context-menu/context-menu.tsx +9 -0
- package/dist/assets/agents-p_wcGhC3.js +0 -1
- package/dist/assets/apps-CKFrWb00.js +0 -1
- package/dist/assets/cron-UqsFT41c.js +0 -1
- package/dist/assets/doc-browser-2VjSkax1.js +0 -1
- package/dist/assets/inbox-D62apR2j.js +0 -1
- package/dist/assets/mcp-marketplace-page-oHCbBkS-.js +0 -1
- package/dist/assets/panel-apps-CniMi7QW.js +0 -1
- package/dist/assets/remote-CsJMF8OR.js +0 -1
- package/src/features/panel-apps/components/__tests__/panel-app-toolbar.test.tsx +0 -108
- package/src/features/panel-apps/components/panel-app-toolbar.tsx +0 -89
|
@@ -14,6 +14,16 @@ import { pageResourceTab } from "@/features/right-panel-resources/managers/page-
|
|
|
14
14
|
import { pageResourceFromTarget } from "@/features/right-panel-resources/utils/page-resource-identity.utils";
|
|
15
15
|
import type { PageResource } from "@/features/right-panel-resources/types/page-resource.types";
|
|
16
16
|
import { t } from "@/shared/lib/i18n";
|
|
17
|
+
import { parseSystemObjectReferenceUri } from "@nextclaw/shared";
|
|
18
|
+
import { RefreshCw } from "lucide-react";
|
|
19
|
+
import { IconActionButton } from "@/shared/components/ui/actions/icon-action-button";
|
|
20
|
+
import { usePanelApps } from "@/features/panel-apps/hooks/use-panel-apps";
|
|
21
|
+
|
|
22
|
+
function PanelAppResourceTitle({ appId, fallback }: { appId: string; fallback: string }) {
|
|
23
|
+
const panelApps = usePanelApps();
|
|
24
|
+
const title = panelApps.data?.entries.find((entry) => entry.appId === appId)?.title ?? fallback;
|
|
25
|
+
return <span className="min-w-0 flex-1 truncate text-sm">{title}</span>;
|
|
26
|
+
}
|
|
17
27
|
|
|
18
28
|
function ResourcePageContent({ page }: { page: PageResource }) {
|
|
19
29
|
const app = useAppPresenter();
|
|
@@ -22,6 +32,10 @@ function ResourcePageContent({ page }: { page: PageResource }) {
|
|
|
22
32
|
const [revision, setRevision] = useState(0);
|
|
23
33
|
const tab = useMemo(() => pageResourceTab(page), [page]);
|
|
24
34
|
const renderer = PAGE_RESOURCE_RENDERERS[tab.kind];
|
|
35
|
+
const object = tab.kind === "system-object"
|
|
36
|
+
? parseSystemObjectReferenceUri(page.uri)
|
|
37
|
+
: null;
|
|
38
|
+
const panelAppId = object?.objectType === "panel-app" ? object.objectId : null;
|
|
25
39
|
const iframeInstanceId = `main:${page.uri}:${revision}`;
|
|
26
40
|
const restore = useDocBrowserScrollRestoration({
|
|
27
41
|
currentTab: tab,
|
|
@@ -55,6 +69,7 @@ function ResourcePageContent({ page }: { page: PageResource }) {
|
|
|
55
69
|
tab,
|
|
56
70
|
currentUrl: tab.currentUrl,
|
|
57
71
|
refreshIframe: () => setRevision((value) => value + 1),
|
|
72
|
+
refreshVersion: revision,
|
|
58
73
|
open: (uri?: string) => {
|
|
59
74
|
const next = uri ? app.pageResourceManager.resolve(uri) : null;
|
|
60
75
|
if (next) app.pageResourceManager.open(next, "main", navigate);
|
|
@@ -73,10 +88,22 @@ function ResourcePageContent({ page }: { page: PageResource }) {
|
|
|
73
88
|
>
|
|
74
89
|
<header className="flex min-h-10 shrink-0 items-center gap-2 border-b border-border/60 px-3">
|
|
75
90
|
<PageResourceIcon uri={page.uri} icon={page.target.dockIcon} />
|
|
76
|
-
|
|
91
|
+
{panelAppId ? (
|
|
92
|
+
<PanelAppResourceTitle appId={panelAppId} fallback={page.title} />
|
|
93
|
+
) : (
|
|
94
|
+
<span className="min-w-0 flex-1 truncate text-sm">{page.title}</span>
|
|
95
|
+
)}
|
|
96
|
+
{panelAppId ? (
|
|
97
|
+
<IconActionButton
|
|
98
|
+
icon={<RefreshCw className="h-3.5 w-3.5" />}
|
|
99
|
+
label={t("panelAppsRefreshCurrent")}
|
|
100
|
+
onClick={params.refreshIframe}
|
|
101
|
+
size="sm"
|
|
102
|
+
/>
|
|
103
|
+
) : null}
|
|
77
104
|
<PageResourceActionsMenu page={page} />
|
|
78
105
|
</header>
|
|
79
|
-
{renderer?.renderToolbar?.(params)}
|
|
106
|
+
{!panelAppId ? renderer?.renderToolbar?.(params) : null}
|
|
80
107
|
<DocBrowserFrameContent
|
|
81
108
|
currentTab={tab}
|
|
82
109
|
currentUrl={tab.currentUrl}
|
|
@@ -18,6 +18,7 @@ import type {
|
|
|
18
18
|
DocBrowserTab,
|
|
19
19
|
} from "@/shared/components/doc-browser/doc-browser-context";
|
|
20
20
|
import { PANEL_APPS_DOC_BROWSER_RENDERERS } from "@/features/panel-apps";
|
|
21
|
+
import { pageResourceFromTab, usePageResourceActions } from "@/features/right-panel-resources";
|
|
21
22
|
import { PANEL_APP_SCROLL_RESTORATION_CONTRACT } from "@nextclaw/shared";
|
|
22
23
|
|
|
23
24
|
const { navigateMock } = vi.hoisted(() => ({
|
|
@@ -28,6 +29,12 @@ const panelAppHooks = vi.hoisted(() => ({
|
|
|
28
29
|
mutate: vi.fn(),
|
|
29
30
|
}));
|
|
30
31
|
|
|
32
|
+
function DocBrowserWithPageActions() {
|
|
33
|
+
const actions = usePageResourceActions();
|
|
34
|
+
return <DocBrowser customTabRenderers={PANEL_APPS_DOC_BROWSER_RENDERERS}
|
|
35
|
+
getTabMenuGroups={(tab) => actions(pageResourceFromTab(tab), 'global')} />;
|
|
36
|
+
}
|
|
37
|
+
|
|
31
38
|
vi.mock("react-router-dom", async () => ({
|
|
32
39
|
...((await vi.importActual("react-router-dom")) as object),
|
|
33
40
|
useNavigate: () => navigateMock,
|
|
@@ -536,21 +543,12 @@ describe("DocBrowser panel app navigation", () => {
|
|
|
536
543
|
docBrowserState.activeHistoryIndex = 1;
|
|
537
544
|
docBrowserState.currentTab = panelAppTab;
|
|
538
545
|
|
|
539
|
-
render(<
|
|
546
|
+
render(<DocBrowserWithPageActions />);
|
|
540
547
|
|
|
541
548
|
expect(screen.getAllByRole("button", { name: "Back" })).toHaveLength(1);
|
|
542
549
|
expect(screen.queryByRole("button", { name: "Apps" })).toBeNull();
|
|
543
550
|
await user.click(screen.getByRole("button", { name: "Back" }));
|
|
544
551
|
expect(docBrowserState.goBack).toHaveBeenCalledTimes(1);
|
|
545
|
-
|
|
546
|
-
expect(screen.queryByRole("button", { name: "Pin to left sidebar" })).toBeNull();
|
|
547
|
-
await user.click(screen.getByRole("button", { name: "More panel app actions" }));
|
|
548
|
-
await user.click(screen.getByRole("menuitem", { name: "Layout and position" }));
|
|
549
|
-
await user.click(screen.getByRole("menuitem", { name: "Pin to left sidebar" }));
|
|
550
|
-
expect(panelAppHooks.mutate).toHaveBeenCalledWith({
|
|
551
|
-
id: "piano",
|
|
552
|
-
preferences: { mainSidebar: true },
|
|
553
|
-
});
|
|
554
552
|
});
|
|
555
553
|
|
|
556
554
|
it("keeps placement actions when a restored shortcut only has a stable current URL", async () => {
|
|
@@ -569,9 +567,9 @@ describe("DocBrowser panel app navigation", () => {
|
|
|
569
567
|
docBrowserState.activeTabId = panelAppTab.id;
|
|
570
568
|
docBrowserState.currentTab = panelAppTab;
|
|
571
569
|
|
|
572
|
-
render(<
|
|
570
|
+
render(<DocBrowserWithPageActions />);
|
|
573
571
|
|
|
574
|
-
await user.click(screen.getByRole("button", { name: "More
|
|
572
|
+
await user.click(screen.getByRole("button", { name: "More tab actions" }));
|
|
575
573
|
await user.click(screen.getByRole("menuitem", { name: "Layout and position" }));
|
|
576
574
|
await user.click(screen.getByRole("menuitem", { name: "Pin to left sidebar" }));
|
|
577
575
|
expect(panelAppHooks.mutate).toHaveBeenCalledWith({
|
|
@@ -623,7 +621,12 @@ describe("DocBrowser scroll restoration", () => {
|
|
|
623
621
|
source: initialWindow,
|
|
624
622
|
}));
|
|
625
623
|
|
|
626
|
-
|
|
624
|
+
expect(screen.queryByRole("button", { name: "Refresh current panel app" })).toBeNull();
|
|
625
|
+
fireEvent.click(screen.getByRole("button", { name: "More tab actions" }));
|
|
626
|
+
const standalone = screen.getByRole("menuitem", { name: "Open in New Tab" });
|
|
627
|
+
expect(standalone.getAttribute("href")).toBe("/apps/panel/piano/standalone");
|
|
628
|
+
expect(standalone.getAttribute("target")).toBe("_blank");
|
|
629
|
+
fireEvent.click(screen.getByRole("menuitem", { name: "Refresh current panel app" }));
|
|
627
630
|
|
|
628
631
|
const refreshedIframe = container.querySelector('iframe[title="Piano"]') as HTMLIFrameElement;
|
|
629
632
|
const refreshedPostMessage = vi.fn();
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import type { PointerEvent, ReactNode } from 'react';
|
|
2
2
|
import type { DocBrowserContextValue, DocBrowserTab } from './doc-browser-context';
|
|
3
|
+
import type { ContextMenuGroup } from '@/shared/components/ui/context-menu/context-menu';
|
|
3
4
|
|
|
4
5
|
export type DocBrowserCustomTabRenderParams = {
|
|
5
6
|
currentUrl: string;
|
|
6
7
|
open: DocBrowserContextValue['open'];
|
|
7
8
|
openTarget: DocBrowserContextValue['openTarget'];
|
|
8
9
|
refreshIframe: () => void;
|
|
10
|
+
refreshVersion: number;
|
|
9
11
|
tab: DocBrowserTab;
|
|
10
12
|
};
|
|
11
13
|
|
|
@@ -17,6 +19,7 @@ export type DocBrowserIframeMessageParams = {
|
|
|
17
19
|
};
|
|
18
20
|
|
|
19
21
|
export type DocBrowserCustomTabRenderer = {
|
|
22
|
+
getTabMenuGroups?: (params: DocBrowserCustomTabRenderParams) => readonly ContextMenuGroup[];
|
|
20
23
|
getIframeSandbox?: (tab: DocBrowserTab) => string | undefined;
|
|
21
24
|
getTitle?: (tab: DocBrowserTab) => string;
|
|
22
25
|
onIframeMessage?: (params: DocBrowserIframeMessageParams) => void;
|
|
@@ -265,6 +265,7 @@ export function DocBrowser({
|
|
|
265
265
|
open,
|
|
266
266
|
openTarget,
|
|
267
267
|
refreshIframe,
|
|
268
|
+
refreshVersion: iframeReloadVersion,
|
|
268
269
|
tab: currentTab,
|
|
269
270
|
} : undefined;
|
|
270
271
|
const customToolbar = customRenderParams ? customRenderer?.renderToolbar?.(customRenderParams) : null;
|
|
@@ -325,7 +326,12 @@ export function DocBrowser({
|
|
|
325
326
|
onToggleDock={handleToggleDock}
|
|
326
327
|
onSetActiveTab={setActiveTab}
|
|
327
328
|
onCloseTab={closeTab}
|
|
328
|
-
getTabMenuGroups={
|
|
329
|
+
getTabMenuGroups={(tab) => [
|
|
330
|
+
...(getTabMenuGroups?.(tab) ?? []),
|
|
331
|
+
...(tab.id === activeTabId && customRenderParams
|
|
332
|
+
? customRenderer?.getTabMenuGroups?.(customRenderParams) ?? []
|
|
333
|
+
: []),
|
|
334
|
+
]}
|
|
329
335
|
/>
|
|
330
336
|
}>
|
|
331
337
|
{panelContent}
|
|
@@ -164,6 +164,7 @@ export function ContextMenuItems({ groups, onClose }: {
|
|
|
164
164
|
onClose={onClose} className={className}>{content}</ContextSubmenu>;
|
|
165
165
|
const select = () => { item.onSelect?.(); onClose(item.href ? false : item.restoreFocus !== false); };
|
|
166
166
|
return item.href ? <a key={item.key} role="menuitem" className={className} href={item.href} download={item.download}
|
|
167
|
+
target={item.target} rel={item.rel} referrerPolicy={item.referrerPolicy}
|
|
167
168
|
onPointerEnter={() => setOpenKey(null)} onClick={(event) => { event.stopPropagation(); select(); }}>{content}</a>
|
|
168
169
|
: <button key={item.key} type="button" role="menuitem" disabled={item.disabled} aria-pressed={item.pressed}
|
|
169
170
|
data-menu-key={item.key} className={className} onPointerEnter={() => setOpenKey(null)}
|
|
@@ -30,18 +30,27 @@ export type ContextMenuItem = ContextMenuItemBase &
|
|
|
30
30
|
children?: never;
|
|
31
31
|
download?: string;
|
|
32
32
|
href: string;
|
|
33
|
+
target?: '_blank';
|
|
34
|
+
rel?: string;
|
|
35
|
+
referrerPolicy?: React.HTMLAttributeReferrerPolicy;
|
|
33
36
|
onSelect?: () => void;
|
|
34
37
|
}
|
|
35
38
|
| {
|
|
36
39
|
children?: never;
|
|
37
40
|
download?: never;
|
|
38
41
|
href?: never;
|
|
42
|
+
target?: never;
|
|
43
|
+
rel?: never;
|
|
44
|
+
referrerPolicy?: never;
|
|
39
45
|
onSelect: () => void;
|
|
40
46
|
}
|
|
41
47
|
| {
|
|
42
48
|
children: readonly ContextMenuGroup[];
|
|
43
49
|
download?: never;
|
|
44
50
|
href?: never;
|
|
51
|
+
target?: never;
|
|
52
|
+
rel?: never;
|
|
53
|
+
referrerPolicy?: never;
|
|
45
54
|
onSelect?: never;
|
|
46
55
|
}
|
|
47
56
|
);
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{t as e}from"./agents-BdeFtIOV.js";export{e as AgentsPage};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{ot as e}from"./app-presenter-provider-BB3Xl8b0.js";export{e as AppsPanel};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{K as e}from"./app-presenter-provider-BB3Xl8b0.js";export{e as CronJobResource};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{St as e}from"./app-presenter-provider-BB3Xl8b0.js";export{e as DocBrowser};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{z as e}from"./app-presenter-provider-BB3Xl8b0.js";export{e as InboxPage};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{st as e}from"./app-presenter-provider-BB3Xl8b0.js";export{e as McpMarketplacePage};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{s as e,v as t}from"./theme-provider-BsJ6HxeX.js";import"./app-presenter-provider-BB3Xl8b0.js";export{t as PanelAppHostProvider,e as PanelAppRuntimeSurface};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{F as e}from"./app-presenter-provider-BB3Xl8b0.js";export{e as RemoteAccessPage};
|
|
@@ -1,108 +0,0 @@
|
|
|
1
|
-
import { render, screen } from '@testing-library/react';
|
|
2
|
-
import userEvent from '@testing-library/user-event';
|
|
3
|
-
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
4
|
-
import { PanelAppToolbar } from '@/features/panel-apps/components/panel-app-toolbar';
|
|
5
|
-
|
|
6
|
-
const mainSidebarMutation = vi.hoisted(() => ({
|
|
7
|
-
isPending: false,
|
|
8
|
-
pinned: false,
|
|
9
|
-
mutate: vi.fn(),
|
|
10
|
-
}));
|
|
11
|
-
|
|
12
|
-
vi.mock('@/features/panel-apps/hooks/use-panel-apps', () => ({
|
|
13
|
-
usePanelApps: () => ({ data:{ entries:[{ ...entry, mainSidebar: mainSidebarMutation.pinned }] } }),
|
|
14
|
-
useUpdatePanelAppPreferences: () => mainSidebarMutation,
|
|
15
|
-
}));
|
|
16
|
-
|
|
17
|
-
const entry = {
|
|
18
|
-
appId: 'ink-assistant',
|
|
19
|
-
clientDeclared: false,
|
|
20
|
-
clientGranted: false,
|
|
21
|
-
contentPath: '/api/panel-apps/ink-assistant/content',
|
|
22
|
-
createdAt: '2026-08-19T00:00:00.000Z',
|
|
23
|
-
favorite: false,
|
|
24
|
-
fileName: 'ink-assistant.panel.html',
|
|
25
|
-
id: 'ink-assistant',
|
|
26
|
-
kind: 'single-file' as const,
|
|
27
|
-
mainSidebar: false,
|
|
28
|
-
openCount: 0,
|
|
29
|
-
sizeBytes: 10,
|
|
30
|
-
title: '墨爪助手',
|
|
31
|
-
updatedAt: '2026-08-19T00:00:00.000Z',
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
describe('PanelAppToolbar', () => {
|
|
35
|
-
beforeEach(() => {
|
|
36
|
-
mainSidebarMutation.isPending = false;
|
|
37
|
-
mainSidebarMutation.pinned = false;
|
|
38
|
-
mainSidebarMutation.mutate.mockReset();
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
it('keeps placement in the overflow menu without adding a second back action', async () => {
|
|
42
|
-
const user = userEvent.setup();
|
|
43
|
-
render(
|
|
44
|
-
<PanelAppToolbar
|
|
45
|
-
appTitle="墨爪助手"
|
|
46
|
-
entry={entry}
|
|
47
|
-
onRefresh={vi.fn()}
|
|
48
|
-
/>,
|
|
49
|
-
);
|
|
50
|
-
|
|
51
|
-
expect(screen.getByText('墨爪助手')).toBeTruthy();
|
|
52
|
-
expect(screen.queryByRole('button', { name: 'Apps' })).toBeNull();
|
|
53
|
-
|
|
54
|
-
expect(screen.queryByRole('menuitem', { name: 'Pin to left sidebar' })).toBeNull();
|
|
55
|
-
await user.click(screen.getByRole('button', { name: 'More panel app actions' }));
|
|
56
|
-
await user.click(screen.getByRole('menuitem', { name: 'Layout and position' }));
|
|
57
|
-
const addButton = screen.getByRole('menuitem', { name: 'Pin to left sidebar' });
|
|
58
|
-
expect(addButton.getAttribute('aria-pressed')).toBe('false');
|
|
59
|
-
await user.click(addButton);
|
|
60
|
-
|
|
61
|
-
expect(mainSidebarMutation.mutate).toHaveBeenCalledWith({
|
|
62
|
-
id: 'ink-assistant',
|
|
63
|
-
preferences: { mainSidebar: true },
|
|
64
|
-
});
|
|
65
|
-
expect(screen.queryByRole('menuitem', { name: 'Pin to left sidebar' })).toBeNull();
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
it('uses the same menu item to remove an app already in the main sidebar', async () => {
|
|
69
|
-
mainSidebarMutation.pinned = true;
|
|
70
|
-
const user = userEvent.setup();
|
|
71
|
-
render(
|
|
72
|
-
<PanelAppToolbar
|
|
73
|
-
appTitle="墨爪助手"
|
|
74
|
-
entry={{ ...entry, mainSidebar: true }}
|
|
75
|
-
onRefresh={vi.fn()}
|
|
76
|
-
/>,
|
|
77
|
-
);
|
|
78
|
-
|
|
79
|
-
await user.click(screen.getByRole('button', { name: 'More panel app actions' }));
|
|
80
|
-
await user.click(screen.getByRole('menuitem', { name: 'Layout and position' }));
|
|
81
|
-
const removeButton = screen.getByRole('menuitem', { name: 'Unpin from left sidebar' });
|
|
82
|
-
expect(removeButton.getAttribute('aria-pressed')).toBe('true');
|
|
83
|
-
await user.click(removeButton);
|
|
84
|
-
|
|
85
|
-
expect(mainSidebarMutation.mutate).toHaveBeenCalledWith({
|
|
86
|
-
id: 'ink-assistant',
|
|
87
|
-
preferences: { mainSidebar: false },
|
|
88
|
-
});
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
it('offers the standalone app link from the toolbar menu', async () => {
|
|
92
|
-
const user = userEvent.setup();
|
|
93
|
-
render(
|
|
94
|
-
<PanelAppToolbar
|
|
95
|
-
appTitle="墨爪助手"
|
|
96
|
-
entry={entry}
|
|
97
|
-
onRefresh={vi.fn()}
|
|
98
|
-
/>,
|
|
99
|
-
);
|
|
100
|
-
|
|
101
|
-
await user.click(screen.getByRole('button', { name: 'More panel app actions' }));
|
|
102
|
-
const link = screen.getByRole('link', { name: 'Open in New Tab' });
|
|
103
|
-
expect(link.getAttribute('href')).toBe('/apps/panel/ink-assistant/standalone');
|
|
104
|
-
});
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
vi.mock("react-router-dom", async (importOriginal) => ({ ...(await importOriginal<object>()),useNavigate: () => vi.fn(), useLocation: () => ({ pathname:'/apps',search:'' }) }));
|
|
108
|
-
vi.mock("@/app/components/app-presenter-provider", () => ({ useAppPresenter: () => ({ pageResourceManager:{ open:vi.fn() } }) }));
|
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
import { useMemo, useState } from 'react';
|
|
2
|
-
import { MoreVertical, RefreshCw } from 'lucide-react';
|
|
3
|
-
import { PanelAppMainSidebarMenuItem } from '@/features/panel-apps/components/panel-app-main-sidebar-menu-item';
|
|
4
|
-
import { PanelAppOpenStandaloneMenuItem } from '@/features/panel-apps/components/panel-app-open-standalone-menu-item';
|
|
5
|
-
import { usePanelApps } from '@/features/panel-apps/hooks/use-panel-apps';
|
|
6
|
-
import { readPanelAppIdFromTab } from '@/features/right-panel-resources';
|
|
7
|
-
import { IconActionButton } from '@/shared/components/ui/actions/icon-action-button';
|
|
8
|
-
import { Popover, PopoverContent, PopoverTrigger } from '@/shared/components/ui/popover';
|
|
9
|
-
import type { PanelAppEntryView } from '@/shared/lib/api';
|
|
10
|
-
import { t } from '@/shared/lib/i18n';
|
|
11
|
-
|
|
12
|
-
type PanelAppToolbarProps = {
|
|
13
|
-
appTitle: string;
|
|
14
|
-
entry?: PanelAppEntryView;
|
|
15
|
-
onRefresh: () => void;
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
export function PanelAppToolbar({
|
|
19
|
-
appTitle,
|
|
20
|
-
entry,
|
|
21
|
-
onRefresh,
|
|
22
|
-
}: PanelAppToolbarProps) {
|
|
23
|
-
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
|
24
|
-
|
|
25
|
-
return (
|
|
26
|
-
<div className="flex items-center justify-between gap-2 border-b border-border/70 bg-card px-3.5 py-2 shrink-0">
|
|
27
|
-
<span
|
|
28
|
-
className="min-w-0 flex-1 truncate text-xs font-medium text-muted-foreground"
|
|
29
|
-
title={appTitle}
|
|
30
|
-
>
|
|
31
|
-
{appTitle}
|
|
32
|
-
</span>
|
|
33
|
-
<IconActionButton
|
|
34
|
-
icon={<RefreshCw className="h-3.5 w-3.5" />}
|
|
35
|
-
label={t('panelAppsRefreshCurrent')}
|
|
36
|
-
onClick={onRefresh}
|
|
37
|
-
size="sm"
|
|
38
|
-
/>
|
|
39
|
-
{entry ? (
|
|
40
|
-
<Popover open={isMenuOpen} onOpenChange={setIsMenuOpen}>
|
|
41
|
-
<PopoverTrigger asChild>
|
|
42
|
-
<IconActionButton
|
|
43
|
-
icon={<MoreVertical className="h-3.5 w-3.5" />}
|
|
44
|
-
label={t('panelAppsMoreActions')}
|
|
45
|
-
size="sm"
|
|
46
|
-
/>
|
|
47
|
-
</PopoverTrigger>
|
|
48
|
-
<PopoverContent align="end" variant="menu">
|
|
49
|
-
<PanelAppOpenStandaloneMenuItem
|
|
50
|
-
entry={entry}
|
|
51
|
-
onSelect={() => setIsMenuOpen(false)}
|
|
52
|
-
/>
|
|
53
|
-
<PanelAppMainSidebarMenuItem
|
|
54
|
-
entry={entry}
|
|
55
|
-
onSelect={() => setIsMenuOpen(false)}
|
|
56
|
-
/>
|
|
57
|
-
</PopoverContent>
|
|
58
|
-
</Popover>
|
|
59
|
-
) : null}
|
|
60
|
-
</div>
|
|
61
|
-
);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
export function PanelAppDocBrowserToolbar({
|
|
65
|
-
appTitle,
|
|
66
|
-
currentUrl,
|
|
67
|
-
onRefresh,
|
|
68
|
-
resourceUri,
|
|
69
|
-
}: {
|
|
70
|
-
appTitle: string;
|
|
71
|
-
currentUrl: string;
|
|
72
|
-
onRefresh: () => void;
|
|
73
|
-
resourceUri?: string;
|
|
74
|
-
}) {
|
|
75
|
-
const panelApps = usePanelApps();
|
|
76
|
-
const appId = readPanelAppIdFromTab({ currentUrl, resourceUri });
|
|
77
|
-
const entry = useMemo(
|
|
78
|
-
() => panelApps.data?.entries.find((candidate) => candidate.appId === appId),
|
|
79
|
-
[appId, panelApps.data?.entries],
|
|
80
|
-
);
|
|
81
|
-
|
|
82
|
-
return (
|
|
83
|
-
<PanelAppToolbar
|
|
84
|
-
appTitle={entry?.title ?? appTitle}
|
|
85
|
-
entry={entry}
|
|
86
|
-
onRefresh={onRefresh}
|
|
87
|
-
/>
|
|
88
|
-
);
|
|
89
|
-
}
|