@nextclaw/ui 0.19.0 → 0.19.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 +6 -0
- package/dist/assets/{appearance-settings-page-DsMl931U.js → appearance-settings-page-NlQIHbXh.js} +1 -1
- package/dist/assets/{channels-list-page-D_B7JGoZ.js → channels-list-page-DhXfsjxF.js} +1 -1
- package/dist/assets/{chat-page-D3ZeULTv.js → chat-page-C4DHLN6T.js} +1 -1
- package/dist/assets/{desktop-update-config-DO051-QG.js → desktop-update-config-CpsXBUJK.js} +1 -1
- package/dist/assets/{index-j6DBnvlr.js → index-Bfr0I0Ga.js} +19 -19
- package/dist/assets/index-DcqrfCMy.css +1 -0
- package/dist/assets/{model-config-page-CBoh0DVh.js → model-config-page-BRScCkS-.js} +1 -1
- package/dist/assets/{provider-scoped-model-input-C2iUtZi7.js → provider-scoped-model-input-CD93ZOgZ.js} +1 -1
- package/dist/assets/{providers-config-page-DX1pISTo.js → providers-config-page-CFdgth2A.js} +1 -1
- package/dist/assets/remote-DDN70hyU.js +1 -0
- package/dist/assets/{runtime-config-page-BVO9B8Ns.js → runtime-config-page-BxJlm6qT.js} +1 -1
- package/dist/assets/{search-config-page-BwyeUjKt.js → search-config-page-2VAarRcG.js} +1 -1
- package/dist/assets/{secrets-config-page-2O-TzroJ.js → secrets-config-page-KoFrETds.js} +1 -1
- package/dist/index.html +2 -2
- package/package.json +4 -4
- package/src/features/panel-apps/components/__tests__/panel-app-list-item.test.tsx +24 -12
- package/src/features/panel-apps/components/__tests__/panel-app-main-sidebar-nav.test.tsx +51 -0
- package/src/features/panel-apps/components/__tests__/panel-app-toolbar.test.tsx +59 -5
- package/src/features/panel-apps/components/panel-app-list-item.tsx +5 -18
- package/src/features/panel-apps/components/panel-app-main-sidebar-menu-item.tsx +37 -0
- package/src/features/panel-apps/components/panel-app-main-sidebar-nav.tsx +71 -20
- package/src/features/panel-apps/components/panel-app-toolbar.tsx +18 -38
- package/src/features/panel-apps/components/panel-apps-list.tsx +0 -9
- package/src/features/panel-apps/utils/panel-app-doc-browser.utils.tsx +2 -2
- package/src/features/right-panel-resources/index.ts +1 -0
- package/src/features/right-panel-resources/utils/__tests__/right-panel-resource-uri.utils.test.ts +12 -0
- package/src/features/right-panel-resources/utils/right-panel-resource-uri.utils.ts +8 -0
- package/src/shared/components/doc-browser/__tests__/doc-browser.test.tsx +26 -0
- package/dist/assets/index-DR3GLGme.css +0 -1
- package/dist/assets/remote-D_7_OGoj.js +0 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { render, screen } from "@testing-library/react";
|
|
2
|
+
import userEvent from "@testing-library/user-event";
|
|
2
3
|
import { MemoryRouter } from "react-router-dom";
|
|
3
4
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
4
5
|
import {
|
|
@@ -9,10 +10,15 @@ import type { PanelAppEntryView } from "@/shared/lib/api";
|
|
|
9
10
|
|
|
10
11
|
const mocks = vi.hoisted(() => ({
|
|
11
12
|
entries: [] as PanelAppEntryView[],
|
|
13
|
+
mutate: vi.fn(),
|
|
12
14
|
}));
|
|
13
15
|
|
|
14
16
|
vi.mock("@/features/panel-apps/hooks/use-panel-apps", () => ({
|
|
15
17
|
usePanelApps: () => ({ data: { entries: mocks.entries } }),
|
|
18
|
+
useUpdatePanelAppPreferences: () => ({
|
|
19
|
+
isPending: false,
|
|
20
|
+
mutate: mocks.mutate,
|
|
21
|
+
}),
|
|
16
22
|
}));
|
|
17
23
|
|
|
18
24
|
function createEntry(overrides: Partial<PanelAppEntryView> = {}): PanelAppEntryView {
|
|
@@ -38,6 +44,7 @@ function createEntry(overrides: Partial<PanelAppEntryView> = {}): PanelAppEntryV
|
|
|
38
44
|
describe("PanelAppMainSidebarNav", () => {
|
|
39
45
|
beforeEach(() => {
|
|
40
46
|
mocks.entries = [];
|
|
47
|
+
mocks.mutate.mockReset();
|
|
41
48
|
});
|
|
42
49
|
|
|
43
50
|
it("projects only manually added apps in persisted order", () => {
|
|
@@ -71,6 +78,50 @@ describe("PanelAppMainSidebarNav", () => {
|
|
|
71
78
|
expect(screen.queryByText("Hidden")).toBeNull();
|
|
72
79
|
});
|
|
73
80
|
|
|
81
|
+
it("removes an expanded entry from its hover actions without nesting the button in the link", async () => {
|
|
82
|
+
const user = userEvent.setup();
|
|
83
|
+
mocks.entries = [
|
|
84
|
+
createEntry({ appId: "publisher.todo", mainSidebar: true, title: "Todo" }),
|
|
85
|
+
];
|
|
86
|
+
|
|
87
|
+
render(
|
|
88
|
+
<MemoryRouter initialEntries={["/apps/panel/publisher.todo"]}>
|
|
89
|
+
<PanelAppMainSidebarNav isCollapsed={false} />
|
|
90
|
+
</MemoryRouter>,
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
const link = screen.getByRole("link", { name: "Todo" });
|
|
94
|
+
const menuButton = screen.getByRole("button", {
|
|
95
|
+
name: "More panel app actions: Todo",
|
|
96
|
+
});
|
|
97
|
+
expect(link.contains(menuButton)).toBe(false);
|
|
98
|
+
expect(menuButton.className).toContain("group-hover/panel-app:opacity-100");
|
|
99
|
+
|
|
100
|
+
await user.click(menuButton);
|
|
101
|
+
await user.click(screen.getByRole("button", { name: "Remove from main sidebar" }));
|
|
102
|
+
|
|
103
|
+
expect(mocks.mutate).toHaveBeenCalledWith({
|
|
104
|
+
id: "demo",
|
|
105
|
+
preferences: { mainSidebar: false },
|
|
106
|
+
});
|
|
107
|
+
expect(link.getAttribute("aria-current")).toBe("page");
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("keeps the collapsed rail as one navigation target", () => {
|
|
111
|
+
mocks.entries = [
|
|
112
|
+
createEntry({ appId: "publisher.todo", mainSidebar: true, title: "Todo" }),
|
|
113
|
+
];
|
|
114
|
+
|
|
115
|
+
render(
|
|
116
|
+
<MemoryRouter>
|
|
117
|
+
<PanelAppMainSidebarNav isCollapsed />
|
|
118
|
+
</MemoryRouter>,
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
expect(screen.getByRole("link", { name: "Todo" })).toBeTruthy();
|
|
122
|
+
expect(screen.queryByRole("button", { name: "More panel app actions: Todo" })).toBeNull();
|
|
123
|
+
});
|
|
124
|
+
|
|
74
125
|
it("renders no section when no app was manually added", () => {
|
|
75
126
|
const { container } = render(
|
|
76
127
|
<MemoryRouter>
|
|
@@ -1,18 +1,48 @@
|
|
|
1
1
|
import { render, screen } from '@testing-library/react';
|
|
2
2
|
import userEvent from '@testing-library/user-event';
|
|
3
|
-
import { describe, expect, it, vi } from 'vitest';
|
|
3
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
4
4
|
import { PanelAppToolbar } from '@/features/panel-apps/components/panel-app-toolbar';
|
|
5
5
|
|
|
6
|
+
const mainSidebarMutation = vi.hoisted(() => ({
|
|
7
|
+
isPending: false,
|
|
8
|
+
mutate: vi.fn(),
|
|
9
|
+
}));
|
|
10
|
+
|
|
11
|
+
vi.mock('@/features/panel-apps/hooks/use-panel-apps', () => ({
|
|
12
|
+
usePanelApps: vi.fn(),
|
|
13
|
+
useUpdatePanelAppPreferences: () => mainSidebarMutation,
|
|
14
|
+
}));
|
|
15
|
+
|
|
16
|
+
const entry = {
|
|
17
|
+
appId: 'ink-assistant',
|
|
18
|
+
clientDeclared: false,
|
|
19
|
+
clientGranted: false,
|
|
20
|
+
contentPath: '/api/panel-apps/ink-assistant/content',
|
|
21
|
+
createdAt: '2026-08-19T00:00:00.000Z',
|
|
22
|
+
favorite: false,
|
|
23
|
+
fileName: 'ink-assistant.panel.html',
|
|
24
|
+
id: 'ink-assistant',
|
|
25
|
+
kind: 'single-file' as const,
|
|
26
|
+
mainSidebar: false,
|
|
27
|
+
openCount: 0,
|
|
28
|
+
sizeBytes: 10,
|
|
29
|
+
title: '墨爪助手',
|
|
30
|
+
updatedAt: '2026-08-19T00:00:00.000Z',
|
|
31
|
+
};
|
|
32
|
+
|
|
6
33
|
describe('PanelAppToolbar', () => {
|
|
34
|
+
beforeEach(() => {
|
|
35
|
+
mainSidebarMutation.isPending = false;
|
|
36
|
+
mainSidebarMutation.mutate.mockReset();
|
|
37
|
+
});
|
|
38
|
+
|
|
7
39
|
it('keeps placement in the overflow menu without adding a second back action', async () => {
|
|
8
40
|
const user = userEvent.setup();
|
|
9
|
-
const onToggleMainSidebar = vi.fn();
|
|
10
41
|
render(
|
|
11
42
|
<PanelAppToolbar
|
|
12
43
|
appTitle="墨爪助手"
|
|
13
|
-
|
|
44
|
+
entry={entry}
|
|
14
45
|
onRefresh={vi.fn()}
|
|
15
|
-
onToggleMainSidebar={onToggleMainSidebar}
|
|
16
46
|
/>,
|
|
17
47
|
);
|
|
18
48
|
|
|
@@ -25,7 +55,31 @@ describe('PanelAppToolbar', () => {
|
|
|
25
55
|
expect(addButton.getAttribute('aria-pressed')).toBe('false');
|
|
26
56
|
await user.click(addButton);
|
|
27
57
|
|
|
28
|
-
expect(
|
|
58
|
+
expect(mainSidebarMutation.mutate).toHaveBeenCalledWith({
|
|
59
|
+
id: 'ink-assistant',
|
|
60
|
+
preferences: { mainSidebar: true },
|
|
61
|
+
});
|
|
29
62
|
expect(screen.queryByRole('button', { name: 'Add to main sidebar' })).toBeNull();
|
|
30
63
|
});
|
|
64
|
+
|
|
65
|
+
it('uses the same menu item to remove an app already in the main sidebar', async () => {
|
|
66
|
+
const user = userEvent.setup();
|
|
67
|
+
render(
|
|
68
|
+
<PanelAppToolbar
|
|
69
|
+
appTitle="墨爪助手"
|
|
70
|
+
entry={{ ...entry, mainSidebar: true }}
|
|
71
|
+
onRefresh={vi.fn()}
|
|
72
|
+
/>,
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
await user.click(screen.getByRole('button', { name: 'More panel app actions' }));
|
|
76
|
+
const removeButton = screen.getByRole('button', { name: 'Remove from main sidebar' });
|
|
77
|
+
expect(removeButton.getAttribute('aria-pressed')).toBe('true');
|
|
78
|
+
await user.click(removeButton);
|
|
79
|
+
|
|
80
|
+
expect(mainSidebarMutation.mutate).toHaveBeenCalledWith({
|
|
81
|
+
id: 'ink-assistant',
|
|
82
|
+
preferences: { mainSidebar: false },
|
|
83
|
+
});
|
|
84
|
+
});
|
|
31
85
|
});
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { useState, type MouseEvent } from 'react';
|
|
2
|
-
import { MoreVertical,
|
|
2
|
+
import { MoreVertical, Star, Trash2, type LucideIcon } from 'lucide-react';
|
|
3
3
|
import type { PanelAppEntryView } from '@/shared/lib/api';
|
|
4
4
|
import { PanelAppIcon } from '@/features/panel-apps/components/panel-app-icon';
|
|
5
|
+
import { PanelAppMainSidebarMenuItem } from '@/features/panel-apps/components/panel-app-main-sidebar-menu-item';
|
|
5
6
|
import { ConfirmDialog } from '@/shared/components/ui/confirm-dialog';
|
|
6
7
|
import { Popover, PopoverContent, PopoverTrigger } from '@/shared/components/ui/popover';
|
|
7
8
|
import { getLanguage, getLocale, t } from '@/shared/lib/i18n';
|
|
@@ -11,20 +12,16 @@ export function PanelAppListItem({
|
|
|
11
12
|
deletePending,
|
|
12
13
|
entry,
|
|
13
14
|
favoritePending,
|
|
14
|
-
mainSidebarPending,
|
|
15
15
|
onDelete,
|
|
16
16
|
onOpen,
|
|
17
17
|
onToggleFavorite,
|
|
18
|
-
onToggleMainSidebar,
|
|
19
18
|
}: {
|
|
20
19
|
deletePending: boolean;
|
|
21
20
|
entry: PanelAppEntryView;
|
|
22
21
|
favoritePending: boolean;
|
|
23
|
-
mainSidebarPending: boolean;
|
|
24
22
|
onDelete: () => void;
|
|
25
23
|
onOpen: () => void;
|
|
26
24
|
onToggleFavorite: () => void;
|
|
27
|
-
onToggleMainSidebar: () => void;
|
|
28
25
|
}) {
|
|
29
26
|
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
|
30
27
|
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
|
|
@@ -37,18 +34,10 @@ export function PanelAppListItem({
|
|
|
37
34
|
event.stopPropagation();
|
|
38
35
|
onToggleFavorite();
|
|
39
36
|
};
|
|
40
|
-
const handleMainSidebar = () => {
|
|
41
|
-
setIsMenuOpen(false);
|
|
42
|
-
onToggleMainSidebar();
|
|
43
|
-
};
|
|
44
37
|
const openDeleteDialog = () => {
|
|
45
38
|
setIsMenuOpen(false);
|
|
46
39
|
setIsDeleteDialogOpen(true);
|
|
47
40
|
};
|
|
48
|
-
const mainSidebarLabel = entry.mainSidebar
|
|
49
|
-
? t('panelAppsRemoveFromMainSidebar')
|
|
50
|
-
: t('panelAppsAddToMainSidebar');
|
|
51
|
-
|
|
52
41
|
return (
|
|
53
42
|
<div className="group w-full min-w-0 rounded-lg border border-border/60 bg-card px-2.5 py-2.5 transition-colors hover:bg-muted/40">
|
|
54
43
|
<div className="flex min-w-0 items-start gap-2">
|
|
@@ -87,11 +76,9 @@ export function PanelAppListItem({
|
|
|
87
76
|
</button>
|
|
88
77
|
</PopoverTrigger>
|
|
89
78
|
<PopoverContent align="end" className="w-48 rounded-xl p-1.5">
|
|
90
|
-
<
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
label={mainSidebarLabel}
|
|
94
|
-
onClick={handleMainSidebar}
|
|
79
|
+
<PanelAppMainSidebarMenuItem
|
|
80
|
+
entry={entry}
|
|
81
|
+
onSelect={() => setIsMenuOpen(false)}
|
|
95
82
|
/>
|
|
96
83
|
<PanelAppMenuItem
|
|
97
84
|
destructive
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { PanelLeft, PanelLeftDashed } from 'lucide-react';
|
|
2
|
+
import { useUpdatePanelAppPreferences } from '@/features/panel-apps/hooks/use-panel-apps';
|
|
3
|
+
import type { PanelAppEntryView } from '@/shared/lib/api';
|
|
4
|
+
import { t } from '@/shared/lib/i18n';
|
|
5
|
+
|
|
6
|
+
export function PanelAppMainSidebarMenuItem({
|
|
7
|
+
entry,
|
|
8
|
+
onSelect,
|
|
9
|
+
}: {
|
|
10
|
+
entry: PanelAppEntryView;
|
|
11
|
+
onSelect?: () => void;
|
|
12
|
+
}) {
|
|
13
|
+
const updatePreferences = useUpdatePanelAppPreferences();
|
|
14
|
+
const label = entry.mainSidebar
|
|
15
|
+
? t('panelAppsRemoveFromMainSidebar')
|
|
16
|
+
: t('panelAppsAddToMainSidebar');
|
|
17
|
+
const Icon = entry.mainSidebar ? PanelLeftDashed : PanelLeft;
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<button
|
|
21
|
+
type="button"
|
|
22
|
+
disabled={updatePreferences.isPending}
|
|
23
|
+
aria-pressed={entry.mainSidebar}
|
|
24
|
+
className="flex w-full items-center gap-2 rounded-lg px-2.5 py-2 text-left text-sm text-muted-foreground transition-colors hover:bg-[var(--interaction-hover)] hover:text-accent-foreground disabled:cursor-not-allowed disabled:opacity-50"
|
|
25
|
+
onClick={() => {
|
|
26
|
+
onSelect?.();
|
|
27
|
+
updatePreferences.mutate({
|
|
28
|
+
id: entry.id,
|
|
29
|
+
preferences: { mainSidebar: !entry.mainSidebar },
|
|
30
|
+
});
|
|
31
|
+
}}
|
|
32
|
+
>
|
|
33
|
+
<Icon className="h-4 w-4 shrink-0" />
|
|
34
|
+
<span>{label}</span>
|
|
35
|
+
</button>
|
|
36
|
+
);
|
|
37
|
+
}
|
|
@@ -1,9 +1,13 @@
|
|
|
1
|
-
import { useMemo } from "react";
|
|
1
|
+
import { useMemo, useState } from "react";
|
|
2
|
+
import { MoreVertical } from "lucide-react";
|
|
2
3
|
import { SidebarNavLinkItem } from "@/app/components/layout/sidebar-items";
|
|
3
4
|
import { SIDEBAR_RAIL_STACK_CLASS } from "@/app/components/layout/sidebar-rail.styles";
|
|
4
5
|
import { PanelAppIcon } from "@/features/panel-apps/components/panel-app-icon";
|
|
6
|
+
import { PanelAppMainSidebarMenuItem } from "@/features/panel-apps/components/panel-app-main-sidebar-menu-item";
|
|
5
7
|
import { usePanelApps } from "@/features/panel-apps/hooks/use-panel-apps";
|
|
8
|
+
import { Popover, PopoverContent, PopoverTrigger } from "@/shared/components/ui/popover";
|
|
6
9
|
import type { PanelAppEntryView } from "@/shared/lib/api";
|
|
10
|
+
import { t } from "@/shared/lib/i18n";
|
|
7
11
|
import { cn } from "@/shared/lib/utils";
|
|
8
12
|
|
|
9
13
|
const MAIN_SIDEBAR_FALLBACK_ORDER = Number.MAX_SAFE_INTEGER;
|
|
@@ -45,28 +49,75 @@ export function PanelAppMainSidebarNav({
|
|
|
45
49
|
>
|
|
46
50
|
<ul className={isCollapsed ? SIDEBAR_RAIL_STACK_CLASS : "space-y-0.5"}>
|
|
47
51
|
{entries.map((entry) => (
|
|
48
|
-
<
|
|
52
|
+
<PanelAppMainSidebarNavItem
|
|
49
53
|
key={entry.appId}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
to={`/apps/panel/${encodeURIComponent(entry.appId)}`}
|
|
54
|
-
label={entry.title}
|
|
55
|
-
iconNode={(
|
|
56
|
-
<PanelAppIcon
|
|
57
|
-
icon={entry.icon}
|
|
58
|
-
title={entry.title}
|
|
59
|
-
className="h-full w-full text-[13px]"
|
|
60
|
-
imageClassName="rounded-[3px]"
|
|
61
|
-
/>
|
|
62
|
-
)}
|
|
63
|
-
density="compact"
|
|
64
|
-
collapsed={isCollapsed}
|
|
65
|
-
className={isCollapsed ? undefined : "rounded-lg px-2.5 py-1.5"}
|
|
66
|
-
/>
|
|
67
|
-
</li>
|
|
54
|
+
entry={entry}
|
|
55
|
+
isCollapsed={isCollapsed}
|
|
56
|
+
/>
|
|
68
57
|
))}
|
|
69
58
|
</ul>
|
|
70
59
|
</div>
|
|
71
60
|
);
|
|
72
61
|
}
|
|
62
|
+
|
|
63
|
+
function PanelAppMainSidebarNavItem({
|
|
64
|
+
entry,
|
|
65
|
+
isCollapsed,
|
|
66
|
+
}: {
|
|
67
|
+
entry: PanelAppEntryView;
|
|
68
|
+
isCollapsed: boolean;
|
|
69
|
+
}) {
|
|
70
|
+
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
|
71
|
+
const link = (
|
|
72
|
+
<SidebarNavLinkItem
|
|
73
|
+
to={`/apps/panel/${encodeURIComponent(entry.appId)}`}
|
|
74
|
+
label={entry.title}
|
|
75
|
+
iconNode={(
|
|
76
|
+
<PanelAppIcon
|
|
77
|
+
icon={entry.icon}
|
|
78
|
+
title={entry.title}
|
|
79
|
+
className="h-full w-full text-[13px]"
|
|
80
|
+
imageClassName="rounded-[3px]"
|
|
81
|
+
/>
|
|
82
|
+
)}
|
|
83
|
+
density="compact"
|
|
84
|
+
collapsed={isCollapsed}
|
|
85
|
+
className={isCollapsed ? undefined : "rounded-lg py-1.5 pl-2.5 pr-9"}
|
|
86
|
+
/>
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
if (isCollapsed) {
|
|
90
|
+
return <li className="flex justify-center">{link}</li>;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const menuLabel = `${t("panelAppsMoreActions")}: ${entry.title}`;
|
|
94
|
+
return (
|
|
95
|
+
<li>
|
|
96
|
+
<div className="group/panel-app relative">
|
|
97
|
+
{link}
|
|
98
|
+
<Popover open={isMenuOpen} onOpenChange={setIsMenuOpen}>
|
|
99
|
+
<PopoverTrigger asChild>
|
|
100
|
+
<button
|
|
101
|
+
type="button"
|
|
102
|
+
aria-label={menuLabel}
|
|
103
|
+
className={cn(
|
|
104
|
+
"absolute right-1 top-1/2 z-[1] -translate-y-1/2 rounded-md p-1 text-muted-foreground/70 transition-[color,background-color,opacity] hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-border",
|
|
105
|
+
isMenuOpen
|
|
106
|
+
? "pointer-events-auto opacity-100"
|
|
107
|
+
: "pointer-events-none opacity-0 group-hover/panel-app:pointer-events-auto group-hover/panel-app:opacity-100 group-focus-within/panel-app:pointer-events-auto group-focus-within/panel-app:opacity-100",
|
|
108
|
+
)}
|
|
109
|
+
>
|
|
110
|
+
<MoreVertical className="h-4 w-4" />
|
|
111
|
+
</button>
|
|
112
|
+
</PopoverTrigger>
|
|
113
|
+
<PopoverContent align="end" className="w-48 rounded-xl p-1.5">
|
|
114
|
+
<PanelAppMainSidebarMenuItem
|
|
115
|
+
entry={entry}
|
|
116
|
+
onSelect={() => setIsMenuOpen(false)}
|
|
117
|
+
/>
|
|
118
|
+
</PopoverContent>
|
|
119
|
+
</Popover>
|
|
120
|
+
</div>
|
|
121
|
+
</li>
|
|
122
|
+
);
|
|
123
|
+
}
|
|
@@ -1,29 +1,25 @@
|
|
|
1
1
|
import { useMemo, useState } from 'react';
|
|
2
|
-
import { MoreVertical,
|
|
3
|
-
import {
|
|
2
|
+
import { MoreVertical, RefreshCw } from 'lucide-react';
|
|
3
|
+
import { PanelAppMainSidebarMenuItem } from '@/features/panel-apps/components/panel-app-main-sidebar-menu-item';
|
|
4
|
+
import { usePanelApps } from '@/features/panel-apps/hooks/use-panel-apps';
|
|
5
|
+
import { readPanelAppIdFromTab } from '@/features/right-panel-resources';
|
|
4
6
|
import { IconActionButton } from '@/shared/components/ui/actions/icon-action-button';
|
|
5
7
|
import { Popover, PopoverContent, PopoverTrigger } from '@/shared/components/ui/popover';
|
|
8
|
+
import type { PanelAppEntryView } from '@/shared/lib/api';
|
|
6
9
|
import { t } from '@/shared/lib/i18n';
|
|
7
10
|
|
|
8
11
|
type PanelAppToolbarProps = {
|
|
9
12
|
appTitle: string;
|
|
10
|
-
|
|
11
|
-
mainSidebarPending?: boolean;
|
|
13
|
+
entry?: PanelAppEntryView;
|
|
12
14
|
onRefresh: () => void;
|
|
13
|
-
onToggleMainSidebar?: () => void;
|
|
14
15
|
};
|
|
15
16
|
|
|
16
17
|
export function PanelAppToolbar({
|
|
17
18
|
appTitle,
|
|
18
|
-
|
|
19
|
-
mainSidebarPending = false,
|
|
19
|
+
entry,
|
|
20
20
|
onRefresh,
|
|
21
|
-
onToggleMainSidebar,
|
|
22
21
|
}: PanelAppToolbarProps) {
|
|
23
22
|
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
|
24
|
-
const mainSidebarLabel = mainSidebar
|
|
25
|
-
? t('panelAppsRemoveFromMainSidebar')
|
|
26
|
-
: t('panelAppsAddToMainSidebar');
|
|
27
23
|
|
|
28
24
|
return (
|
|
29
25
|
<div className="flex items-center justify-between gap-2 border-b border-border/70 bg-card px-3.5 py-2 shrink-0">
|
|
@@ -33,7 +29,7 @@ export function PanelAppToolbar({
|
|
|
33
29
|
>
|
|
34
30
|
{appTitle}
|
|
35
31
|
</span>
|
|
36
|
-
{
|
|
32
|
+
{entry ? (
|
|
37
33
|
<Popover open={isMenuOpen} onOpenChange={setIsMenuOpen}>
|
|
38
34
|
<PopoverTrigger asChild>
|
|
39
35
|
<IconActionButton
|
|
@@ -43,21 +39,10 @@ export function PanelAppToolbar({
|
|
|
43
39
|
/>
|
|
44
40
|
</PopoverTrigger>
|
|
45
41
|
<PopoverContent align="end" className="w-48 rounded-xl p-1.5">
|
|
46
|
-
<
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
className="flex w-full items-center gap-2 rounded-lg px-2.5 py-2 text-left text-sm text-muted-foreground transition-colors hover:bg-[var(--interaction-hover)] hover:text-accent-foreground disabled:cursor-not-allowed disabled:opacity-50"
|
|
51
|
-
onClick={() => {
|
|
52
|
-
setIsMenuOpen(false);
|
|
53
|
-
onToggleMainSidebar();
|
|
54
|
-
}}
|
|
55
|
-
>
|
|
56
|
-
{mainSidebar
|
|
57
|
-
? <PanelLeftDashed className="h-4 w-4 shrink-0" />
|
|
58
|
-
: <PanelLeft className="h-4 w-4 shrink-0" />}
|
|
59
|
-
<span>{mainSidebarLabel}</span>
|
|
60
|
-
</button>
|
|
42
|
+
<PanelAppMainSidebarMenuItem
|
|
43
|
+
entry={entry}
|
|
44
|
+
onSelect={() => setIsMenuOpen(false)}
|
|
45
|
+
/>
|
|
61
46
|
</PopoverContent>
|
|
62
47
|
</Popover>
|
|
63
48
|
) : null}
|
|
@@ -72,16 +57,18 @@ export function PanelAppToolbar({
|
|
|
72
57
|
}
|
|
73
58
|
|
|
74
59
|
export function PanelAppDocBrowserToolbar({
|
|
75
|
-
appId,
|
|
76
60
|
appTitle,
|
|
61
|
+
currentUrl,
|
|
77
62
|
onRefresh,
|
|
63
|
+
resourceUri,
|
|
78
64
|
}: {
|
|
79
|
-
appId: string | null;
|
|
80
65
|
appTitle: string;
|
|
66
|
+
currentUrl: string;
|
|
81
67
|
onRefresh: () => void;
|
|
68
|
+
resourceUri?: string;
|
|
82
69
|
}) {
|
|
83
70
|
const panelApps = usePanelApps();
|
|
84
|
-
const
|
|
71
|
+
const appId = readPanelAppIdFromTab({ currentUrl, resourceUri });
|
|
85
72
|
const entry = useMemo(
|
|
86
73
|
() => panelApps.data?.entries.find((candidate) => candidate.appId === appId),
|
|
87
74
|
[appId, panelApps.data?.entries],
|
|
@@ -90,15 +77,8 @@ export function PanelAppDocBrowserToolbar({
|
|
|
90
77
|
return (
|
|
91
78
|
<PanelAppToolbar
|
|
92
79
|
appTitle={entry?.title ?? appTitle}
|
|
93
|
-
|
|
94
|
-
mainSidebarPending={updatePreferences.isPending}
|
|
80
|
+
entry={entry}
|
|
95
81
|
onRefresh={onRefresh}
|
|
96
|
-
onToggleMainSidebar={entry
|
|
97
|
-
? () => updatePreferences.mutate({
|
|
98
|
-
id: entry.id,
|
|
99
|
-
preferences: { mainSidebar: !entry.mainSidebar },
|
|
100
|
-
})
|
|
101
|
-
: undefined}
|
|
102
82
|
/>
|
|
103
83
|
);
|
|
104
84
|
}
|
|
@@ -48,13 +48,6 @@ export function PanelAppsList({
|
|
|
48
48
|
});
|
|
49
49
|
};
|
|
50
50
|
|
|
51
|
-
const toggleMainSidebar = (entry: PanelAppEntryView) => {
|
|
52
|
-
updatePreferences.mutate({
|
|
53
|
-
id: entry.id,
|
|
54
|
-
preferences: { mainSidebar: !entry.mainSidebar },
|
|
55
|
-
});
|
|
56
|
-
};
|
|
57
|
-
|
|
58
51
|
const deleteEntry = (entry: PanelAppEntryView) => {
|
|
59
52
|
deletePanelApp.mutate(entry.id);
|
|
60
53
|
};
|
|
@@ -129,11 +122,9 @@ export function PanelAppsList({
|
|
|
129
122
|
entry={entry}
|
|
130
123
|
deletePending={deletePanelApp.isPending}
|
|
131
124
|
favoritePending={updatePreferences.isPending}
|
|
132
|
-
mainSidebarPending={updatePreferences.isPending}
|
|
133
125
|
onDelete={() => deleteEntry(entry)}
|
|
134
126
|
onOpen={() => void openPanelApp(entry)}
|
|
135
127
|
onToggleFavorite={() => toggleFavorite(entry)}
|
|
136
|
-
onToggleMainSidebar={() => toggleMainSidebar(entry)}
|
|
137
128
|
/>
|
|
138
129
|
))}
|
|
139
130
|
</div>
|
|
@@ -6,7 +6,6 @@ import { AppsPanel, type AppsPanelTab } from '@/features/apps';
|
|
|
6
6
|
import { PanelAppDocBrowserToolbar } from '@/features/panel-apps/components/panel-app-toolbar';
|
|
7
7
|
import {
|
|
8
8
|
createPanelAppRightPanelResourceTarget,
|
|
9
|
-
readPanelAppIdFromResourceUri,
|
|
10
9
|
RIGHT_PANEL_APPS_TAB_KIND,
|
|
11
10
|
RIGHT_PANEL_APPS_URL,
|
|
12
11
|
RIGHT_PANEL_HOME_TAB_KIND,
|
|
@@ -78,9 +77,10 @@ export const PANEL_APPS_DOC_BROWSER_RENDERERS: DocBrowserCustomTabRenderers = {
|
|
|
78
77
|
renderIcon: () => <AppWindow className="w-4 h-4 text-primary shrink-0" />,
|
|
79
78
|
renderToolbar: ({ refreshIframe, tab }) => (
|
|
80
79
|
<PanelAppDocBrowserToolbar
|
|
81
|
-
appId={readPanelAppIdFromResourceUri(tab.resourceUri ?? tab.currentUrl)}
|
|
82
80
|
appTitle={tab.title || t('panelAppsTitle')}
|
|
81
|
+
currentUrl={tab.currentUrl}
|
|
83
82
|
onRefresh={refreshIframe}
|
|
83
|
+
resourceUri={tab.resourceUri}
|
|
84
84
|
/>
|
|
85
85
|
),
|
|
86
86
|
supportsScrollRestoration: true,
|
package/src/features/right-panel-resources/utils/__tests__/right-panel-resource-uri.utils.test.ts
CHANGED
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
createPanelAppResourceUri,
|
|
7
7
|
createPanelAppRightPanelResourceTarget,
|
|
8
8
|
readPanelAppIdFromResourceUri,
|
|
9
|
+
readPanelAppIdFromTab,
|
|
9
10
|
} from '@/features/right-panel-resources/utils/right-panel-resource-uri.utils';
|
|
10
11
|
|
|
11
12
|
function createPanelAppEntry(overrides: Partial<PanelAppEntryView> = {}): PanelAppEntryView {
|
|
@@ -78,6 +79,17 @@ describe('right-panel-resource uri utils', () => {
|
|
|
78
79
|
expect(readPanelAppIdFromResourceUri('nextclaw://apps')).toBeNull();
|
|
79
80
|
});
|
|
80
81
|
|
|
82
|
+
it('falls back to the stable current URL for restored panel app shortcuts', () => {
|
|
83
|
+
expect(readPanelAppIdFromTab({
|
|
84
|
+
currentUrl: '/api/panel-apps/publisher.todo/content',
|
|
85
|
+
resourceUri: 'nextclaw://panel-app',
|
|
86
|
+
})).toBe('publisher.todo');
|
|
87
|
+
expect(readPanelAppIdFromTab({
|
|
88
|
+
currentUrl: '/api/panel-apps/publisher.todo/content?path=%2Ftmp%2Ftodo.panel',
|
|
89
|
+
resourceUri: 'nextclaw://panel-app/publisher.todo?path=%2Ftmp%2Ftodo.panel',
|
|
90
|
+
})).toBeNull();
|
|
91
|
+
});
|
|
92
|
+
|
|
81
93
|
it('keeps panel app image icons as dock image icons', () => {
|
|
82
94
|
const target = createPanelAppRightPanelResourceTarget(createPanelAppEntry({
|
|
83
95
|
icon: '/api/panel-apps/demo/assets/icon.png',
|
|
@@ -107,6 +107,14 @@ export function readPanelAppIdFromResourceUri(value: string): string | null {
|
|
|
107
107
|
: readPanelAppIdFromParsedResourceUri(uri);
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
+
export function readPanelAppIdFromTab(
|
|
111
|
+
tab: Pick<DocBrowserTab, 'currentUrl' | 'resourceUri'>,
|
|
112
|
+
): string | null {
|
|
113
|
+
return (tab.resourceUri
|
|
114
|
+
? readPanelAppIdFromResourceUri(tab.resourceUri)
|
|
115
|
+
: null) ?? readPanelAppIdFromResourceUri(tab.currentUrl);
|
|
116
|
+
}
|
|
117
|
+
|
|
110
118
|
function isPanelAppImageIcon(icon: string): boolean {
|
|
111
119
|
return (
|
|
112
120
|
icon.startsWith('data:image/') ||
|
|
@@ -537,6 +537,32 @@ describe("DocBrowser panel app navigation", () => {
|
|
|
537
537
|
preferences: { mainSidebar: true },
|
|
538
538
|
});
|
|
539
539
|
});
|
|
540
|
+
|
|
541
|
+
it("keeps placement actions when a restored shortcut only has a stable current URL", async () => {
|
|
542
|
+
const user = userEvent.setup();
|
|
543
|
+
const panelAppTab: DocBrowserTab = {
|
|
544
|
+
id: "piano",
|
|
545
|
+
kind: "panel-app",
|
|
546
|
+
title: "Piano",
|
|
547
|
+
currentUrl: "/api/panel-apps/piano/content",
|
|
548
|
+
resourceUri: "nextclaw://panel-app",
|
|
549
|
+
history: ["/api/panel-apps/piano/content"],
|
|
550
|
+
historyIndex: 0,
|
|
551
|
+
navVersion: 0,
|
|
552
|
+
};
|
|
553
|
+
docBrowserState.tabs = [panelAppTab];
|
|
554
|
+
docBrowserState.activeTabId = panelAppTab.id;
|
|
555
|
+
docBrowserState.currentTab = panelAppTab;
|
|
556
|
+
|
|
557
|
+
render(<DocBrowser customTabRenderers={PANEL_APPS_DOC_BROWSER_RENDERERS} />);
|
|
558
|
+
|
|
559
|
+
await user.click(screen.getByRole("button", { name: "More panel app actions" }));
|
|
560
|
+
await user.click(screen.getByRole("button", { name: "Add to main sidebar" }));
|
|
561
|
+
expect(panelAppHooks.mutate).toHaveBeenCalledWith({
|
|
562
|
+
id: "piano",
|
|
563
|
+
preferences: { mainSidebar: true },
|
|
564
|
+
});
|
|
565
|
+
});
|
|
540
566
|
});
|
|
541
567
|
|
|
542
568
|
describe("DocBrowser scroll restoration", () => {
|