@nextclaw/ui 0.18.4 → 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 +25 -0
- package/dist/assets/api-Cyxp93Fn.js +13 -0
- package/dist/assets/appearance-settings-page-NlQIHbXh.js +1 -0
- package/dist/assets/{book-open-BOJLGVsx.js → book-open-Cl5CzL0g.js} +1 -1
- package/dist/assets/{channels-list-page-DKrUSeNz.js → channels-list-page-DhXfsjxF.js} +2 -2
- package/dist/assets/chat-page-C4DHLN6T.js +1 -0
- package/dist/assets/config-split-page-C-YVl2mf.js +1 -0
- package/dist/assets/{confirm-dialog-Bsg9A62_.js → confirm-dialog-Bz3ieDQ5.js} +2 -2
- package/dist/assets/desktop-update-config-CpsXBUJK.js +1 -0
- package/dist/assets/{dist-IBUY9_9h.js → dist-CDPl8mm7.js} +1 -1
- package/dist/assets/{dist-DyUY9uQ0.js → dist-HFDWY6-K.js} +1 -1
- package/dist/assets/doc-browser-CWhs0JQN.js +1 -0
- package/dist/assets/doc-browser-D6H_nPzO.js +1 -0
- package/dist/assets/{doc-browser-context-BM6Af8gN.js → doc-browser-context-BKwz_j44.js} +1 -1
- package/dist/assets/form-actions-BUZOCfPR.js +1 -0
- package/dist/assets/index-Bfr0I0Ga.js +109 -0
- package/dist/assets/index-DcqrfCMy.css +1 -0
- package/dist/assets/loader-circle-s65a4ZzL.js +1 -0
- package/dist/assets/mcp-marketplace-page-BJvzGt7h.js +9 -0
- package/dist/assets/mcp-marketplace-page-EpFHeJzw.js +1 -0
- package/dist/assets/model-config-page-BRScCkS-.js +1 -0
- package/dist/assets/{navigation-link-DtPZZ_Q_.js → navigation-link-CDMYpkVv.js} +2 -2
- package/dist/assets/plus-XDIkqpkt.js +1 -0
- package/dist/assets/{provider-scoped-model-input-BFzyywsF.js → provider-scoped-model-input-CD93ZOgZ.js} +1 -1
- package/dist/assets/providers-config-page-CFdgth2A.js +1 -0
- package/dist/assets/react-B7Paq2b3.js +8 -0
- package/dist/assets/{refresh-cw-BQHORcAs.js → refresh-cw-XrnNVwuQ.js} +1 -1
- package/dist/assets/remote-DDN70hyU.js +1 -0
- package/dist/assets/rotate-cw-BhIWpLaU.js +1 -0
- package/dist/assets/runtime-config-page-BxJlm6qT.js +1 -0
- package/dist/assets/{save-BgiS2u-T.js → save-_nSNAYQu.js} +1 -1
- package/dist/assets/{search-B6mO7mmo.js → search-0OTHFRMx.js} +1 -1
- package/dist/assets/{search-config-page-ydeDD1OQ.js → search-config-page-2VAarRcG.js} +1 -1
- package/dist/assets/secrets-config-page-KoFrETds.js +3 -0
- package/dist/assets/security-config-BgIIrg1s.js +1 -0
- package/dist/assets/{select-DToos7Rq.js → select-BZHvnwQB.js} +2 -2
- package/dist/assets/{settings-2-ZAWXrq81.js → settings-2-CM_PvDNk.js} +1 -1
- package/dist/assets/skeleton-Bv4MMbH7.js +1 -0
- package/dist/assets/{tag-chip-urB3N-kk.js → tag-chip-Ce53PMTL.js} +1 -1
- package/dist/assets/x-CCjWN5ys.js +1 -0
- package/dist/index.html +20 -20
- package/package.json +5 -5
- package/src/app/components/layout/sidebar-items.tsx +49 -15
- package/src/app/configs/__tests__/app-navigation.config.test.ts +21 -0
- package/src/app/configs/app-navigation.config.ts +10 -1
- package/src/app/index.tsx +4 -0
- package/src/features/chat/components/conversation/chat-conversation-content.tsx +7 -0
- package/src/features/chat/components/layout/chat-page-shell.tsx +4 -1
- package/src/features/chat/components/layout/chat-sidebar-desktop-layout.tsx +26 -22
- package/src/features/chat/features/conversation/components/session-conversation-area.tsx +2 -0
- package/src/features/chat/features/input/input-surface-plugins/__tests__/context-reference-plugin.test.ts +1 -0
- package/src/features/chat/features/input/input-surface-plugins/__tests__/slash-command-plugin.test.ts +1 -0
- package/src/features/chat/features/message/components/chat-inline-panel-app-card.tsx +1 -3
- package/src/features/chat/features/message/components/chat-message-list.container.tsx +15 -1
- package/src/features/chat/features/message/components/chat-tool-call-stress-harness.tsx +178 -0
- package/src/features/chat/features/message/utils/__tests__/chat-message-process-summary.utils.test.ts +21 -0
- package/src/features/chat/features/message/utils/__tests__/chat-tool-call-stress-fixture.utils.test.ts +56 -0
- package/src/features/chat/features/message/utils/chat-message-core.utils.ts +1 -1
- package/src/features/chat/features/message/utils/chat-message-part.utils.ts +3 -10
- package/src/features/chat/features/message/utils/chat-message-process-summary.utils.ts +29 -1
- package/src/features/chat/features/message/utils/chat-message-texts.utils.ts +3 -0
- package/src/features/chat/features/message/utils/chat-message-tool-card.utils.ts +3 -28
- package/src/features/chat/features/message/utils/chat-tool-call-stress-fixture.utils.ts +122 -0
- package/src/features/chat/features/ncp/hooks/__tests__/use-ncp-session-conversation.test.tsx +69 -0
- package/src/features/chat/features/ncp/hooks/use-ncp-session-conversation.ts +29 -0
- package/src/features/chat/features/ncp/hooks/use-ncp-session-message-history.ts +106 -1
- package/src/features/chat/features/session/utils/ncp-session-adapter.utils.ts +2 -12
- package/src/features/chat/managers/__tests__/chat-ui.manager.test.ts +4 -2
- package/src/features/panel-apps/components/__tests__/panel-app-list-item.test.tsx +57 -2
- package/src/features/panel-apps/components/__tests__/panel-app-main-sidebar-nav.test.tsx +133 -0
- package/src/features/panel-apps/components/__tests__/panel-app-toolbar.test.tsx +68 -7
- package/src/features/panel-apps/components/__tests__/panel-apps-list.test.tsx +7 -1
- package/src/features/panel-apps/components/panel-app-icon.tsx +48 -0
- package/src/features/panel-apps/components/panel-app-list-item.tsx +7 -29
- 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 +123 -0
- package/src/features/panel-apps/components/panel-app-toolbar.tsx +61 -20
- package/src/features/panel-apps/components/panel-apps-list.tsx +5 -27
- package/src/features/panel-apps/hooks/__tests__/use-panel-apps.test.tsx +105 -0
- package/src/features/panel-apps/hooks/use-panel-app-client-grant.ts +50 -0
- package/src/features/panel-apps/hooks/use-panel-app-main-runtime.ts +66 -0
- package/src/features/panel-apps/hooks/use-panel-apps.ts +102 -13
- package/src/features/panel-apps/index.ts +2 -0
- package/src/features/panel-apps/managers/__tests__/panel-app-bridge.manager.test.ts +1 -14
- package/src/features/panel-apps/managers/panel-app-bridge.manager.ts +9 -5
- package/src/features/panel-apps/pages/__tests__/panel-app-main-page.test.tsx +183 -0
- package/src/features/panel-apps/pages/panel-app-main-page.tsx +125 -0
- package/src/features/panel-apps/utils/__tests__/panel-app-view.utils.test.ts +2 -1
- package/src/features/panel-apps/utils/panel-app-doc-browser.utils.tsx +5 -4
- package/src/features/right-panel-resources/configs/right-panel-resource-routes.config.ts +12 -25
- package/src/features/right-panel-resources/index.ts +2 -0
- package/src/features/right-panel-resources/utils/__tests__/right-panel-resource-uri.utils.test.ts +21 -0
- package/src/features/right-panel-resources/utils/right-panel-resource-uri.utils.ts +37 -0
- package/src/main.tsx +24 -11
- package/src/shared/components/doc-browser/__tests__/doc-browser.test.tsx +102 -0
- package/src/shared/lib/api/ncp-session.types.ts +1 -0
- package/src/shared/lib/api/types.ts +3 -0
- package/src/shared/lib/api/utils/ncp-session.utils.ts +24 -1
- package/src/shared/lib/i18n/locales/en-US/chat.json +4 -0
- package/src/shared/lib/i18n/locales/en-US/doc-browser.json +10 -0
- package/src/shared/lib/i18n/locales/zh-CN/chat.json +4 -0
- package/src/shared/lib/i18n/locales/zh-CN/doc-browser.json +10 -0
- package/dist/assets/api-BSsQ0I4l.js +0 -13
- package/dist/assets/appearance-settings-page-CEPxG1w0.js +0 -1
- package/dist/assets/chat-page-sGhIjLSr.js +0 -1
- package/dist/assets/config-split-page-aUTKtWJu.js +0 -1
- package/dist/assets/desktop-update-config-VgvIATTT.js +0 -1
- package/dist/assets/doc-browser-D0arfzHY.js +0 -1
- package/dist/assets/doc-browser-D73yN8ts.js +0 -1
- package/dist/assets/form-actions-Cpd7glh8.js +0 -1
- package/dist/assets/index-Bsoscjk7.css +0 -1
- package/dist/assets/index-CtPLj_hk.js +0 -109
- package/dist/assets/loader-circle-C1fL3kBZ.js +0 -1
- package/dist/assets/mcp-marketplace-page-D8V0GU4Y.js +0 -9
- package/dist/assets/mcp-marketplace-page-q6T-vKoH.js +0 -1
- package/dist/assets/model-config-page-DQ-iQFSO.js +0 -1
- package/dist/assets/plus-B9V7Z_Eu.js +0 -1
- package/dist/assets/providers-config-page-DUYwVuYe.js +0 -1
- package/dist/assets/react-D-jOo51s.js +0 -8
- package/dist/assets/remote-CNFQ6bFY.js +0 -1
- package/dist/assets/rotate-cw-DuCF-3PX.js +0 -1
- package/dist/assets/runtime-config-page-CaWoWoQk.js +0 -1
- package/dist/assets/secrets-config-page-BZYqw8BF.js +0 -3
- package/dist/assets/security-config-Ds_2SXBh.js +0 -1
- package/dist/assets/skeleton-Bs2eO2CA.js +0 -1
- package/dist/assets/x-BkjTUiww.js +0 -1
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { render, screen } from "@testing-library/react";
|
|
2
|
+
import userEvent from "@testing-library/user-event";
|
|
3
|
+
import { MemoryRouter } from "react-router-dom";
|
|
4
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
5
|
+
import {
|
|
6
|
+
getMainSidebarPanelApps,
|
|
7
|
+
PanelAppMainSidebarNav,
|
|
8
|
+
} from "@/features/panel-apps/components/panel-app-main-sidebar-nav";
|
|
9
|
+
import type { PanelAppEntryView } from "@/shared/lib/api";
|
|
10
|
+
|
|
11
|
+
const mocks = vi.hoisted(() => ({
|
|
12
|
+
entries: [] as PanelAppEntryView[],
|
|
13
|
+
mutate: vi.fn(),
|
|
14
|
+
}));
|
|
15
|
+
|
|
16
|
+
vi.mock("@/features/panel-apps/hooks/use-panel-apps", () => ({
|
|
17
|
+
usePanelApps: () => ({ data: { entries: mocks.entries } }),
|
|
18
|
+
useUpdatePanelAppPreferences: () => ({
|
|
19
|
+
isPending: false,
|
|
20
|
+
mutate: mocks.mutate,
|
|
21
|
+
}),
|
|
22
|
+
}));
|
|
23
|
+
|
|
24
|
+
function createEntry(overrides: Partial<PanelAppEntryView> = {}): PanelAppEntryView {
|
|
25
|
+
return {
|
|
26
|
+
appId: "demo",
|
|
27
|
+
clientDeclared: false,
|
|
28
|
+
clientGranted: false,
|
|
29
|
+
contentPath: "/api/panel-apps/demo/content",
|
|
30
|
+
createdAt: "2026-08-19T00:00:00.000Z",
|
|
31
|
+
favorite: false,
|
|
32
|
+
fileName: "demo.panel.html",
|
|
33
|
+
id: "demo",
|
|
34
|
+
kind: "single-file",
|
|
35
|
+
mainSidebar: false,
|
|
36
|
+
openCount: 0,
|
|
37
|
+
sizeBytes: 10,
|
|
38
|
+
title: "Demo",
|
|
39
|
+
updatedAt: "2026-08-19T00:00:00.000Z",
|
|
40
|
+
...overrides,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
describe("PanelAppMainSidebarNav", () => {
|
|
45
|
+
beforeEach(() => {
|
|
46
|
+
mocks.entries = [];
|
|
47
|
+
mocks.mutate.mockReset();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("projects only manually added apps in persisted order", () => {
|
|
51
|
+
const entries = [
|
|
52
|
+
createEntry({ appId: "hidden", title: "Hidden" }),
|
|
53
|
+
createEntry({ appId: "second", mainSidebar: true, mainSidebarOrder: 1, title: "Second" }),
|
|
54
|
+
createEntry({ appId: "first", mainSidebar: true, mainSidebarOrder: 0, title: "First" }),
|
|
55
|
+
];
|
|
56
|
+
|
|
57
|
+
expect(getMainSidebarPanelApps(entries).map((entry) => entry.appId)).toEqual([
|
|
58
|
+
"first",
|
|
59
|
+
"second",
|
|
60
|
+
]);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("renders stable app routes and hides unbound apps", () => {
|
|
64
|
+
mocks.entries = [
|
|
65
|
+
createEntry({ appId: "publisher.todo", mainSidebar: true, title: "Todo" }),
|
|
66
|
+
createEntry({ appId: "hidden", title: "Hidden" }),
|
|
67
|
+
];
|
|
68
|
+
|
|
69
|
+
render(
|
|
70
|
+
<MemoryRouter initialEntries={["/apps/panel/publisher.todo"]}>
|
|
71
|
+
<PanelAppMainSidebarNav isCollapsed={false} />
|
|
72
|
+
</MemoryRouter>,
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
const link = screen.getByRole("link", { name: "Todo" });
|
|
76
|
+
expect(link.getAttribute("href")).toBe("/apps/panel/publisher.todo");
|
|
77
|
+
expect(link.getAttribute("aria-current")).toBe("page");
|
|
78
|
+
expect(screen.queryByText("Hidden")).toBeNull();
|
|
79
|
+
});
|
|
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
|
+
|
|
125
|
+
it("renders no section when no app was manually added", () => {
|
|
126
|
+
const { container } = render(
|
|
127
|
+
<MemoryRouter>
|
|
128
|
+
<PanelAppMainSidebarNav isCollapsed={false} />
|
|
129
|
+
</MemoryRouter>,
|
|
130
|
+
);
|
|
131
|
+
expect(container.firstChild).toBeNull();
|
|
132
|
+
});
|
|
133
|
+
});
|
|
@@ -1,24 +1,85 @@
|
|
|
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';
|
|
4
|
-
import { PanelAppToolbar } from '
|
|
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
|
+
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
|
+
};
|
|
5
32
|
|
|
6
33
|
describe('PanelAppToolbar', () => {
|
|
7
|
-
|
|
34
|
+
beforeEach(() => {
|
|
35
|
+
mainSidebarMutation.isPending = false;
|
|
36
|
+
mainSidebarMutation.mutate.mockReset();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('keeps placement in the overflow menu without adding a second back action', async () => {
|
|
8
40
|
const user = userEvent.setup();
|
|
9
|
-
const onOpenApps = vi.fn();
|
|
10
41
|
render(
|
|
11
42
|
<PanelAppToolbar
|
|
12
43
|
appTitle="墨爪助手"
|
|
13
|
-
|
|
44
|
+
entry={entry}
|
|
14
45
|
onRefresh={vi.fn()}
|
|
15
46
|
/>,
|
|
16
47
|
);
|
|
17
48
|
|
|
18
49
|
expect(screen.getByText('墨爪助手')).toBeTruthy();
|
|
50
|
+
expect(screen.queryByRole('button', { name: 'Apps' })).toBeNull();
|
|
51
|
+
|
|
52
|
+
expect(screen.queryByRole('button', { name: 'Add to main sidebar' })).toBeNull();
|
|
53
|
+
await user.click(screen.getByRole('button', { name: 'More panel app actions' }));
|
|
54
|
+
const addButton = screen.getByRole('button', { name: 'Add to main sidebar' });
|
|
55
|
+
expect(addButton.getAttribute('aria-pressed')).toBe('false');
|
|
56
|
+
await user.click(addButton);
|
|
57
|
+
|
|
58
|
+
expect(mainSidebarMutation.mutate).toHaveBeenCalledWith({
|
|
59
|
+
id: 'ink-assistant',
|
|
60
|
+
preferences: { mainSidebar: true },
|
|
61
|
+
});
|
|
62
|
+
expect(screen.queryByRole('button', { name: 'Add to main sidebar' })).toBeNull();
|
|
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
|
+
);
|
|
19
74
|
|
|
20
|
-
await user.click(screen.getByRole('button', { name: '
|
|
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);
|
|
21
79
|
|
|
22
|
-
expect(
|
|
80
|
+
expect(mainSidebarMutation.mutate).toHaveBeenCalledWith({
|
|
81
|
+
id: 'ink-assistant',
|
|
82
|
+
preferences: { mainSidebar: false },
|
|
83
|
+
});
|
|
23
84
|
});
|
|
24
85
|
});
|
|
@@ -22,6 +22,7 @@ const mocks = vi.hoisted(() => ({
|
|
|
22
22
|
}>,
|
|
23
23
|
grantPanelAppClient: vi.fn(),
|
|
24
24
|
navigate: vi.fn(),
|
|
25
|
+
recordOpened: vi.fn(),
|
|
25
26
|
refetchPanelApps: vi.fn(),
|
|
26
27
|
requestAuthorization: vi.fn(async () => true),
|
|
27
28
|
requestDraft: vi.fn(),
|
|
@@ -62,7 +63,7 @@ vi.mock('@/features/panel-apps/hooks/use-panel-apps', () => ({
|
|
|
62
63
|
refetch: mocks.refetchPanelApps,
|
|
63
64
|
}),
|
|
64
65
|
useRecordPanelAppOpened: () => ({
|
|
65
|
-
|
|
66
|
+
mutate: mocks.recordOpened,
|
|
66
67
|
}),
|
|
67
68
|
useUpdatePanelAppPreferences: () => ({
|
|
68
69
|
isPending: false,
|
|
@@ -96,6 +97,7 @@ describe('PanelAppsList', () => {
|
|
|
96
97
|
mocks.grantPanelAppClient.mockReset();
|
|
97
98
|
mocks.navigate.mockReset();
|
|
98
99
|
mocks.refetchPanelApps.mockReset();
|
|
100
|
+
mocks.recordOpened.mockReset();
|
|
99
101
|
mocks.requestAuthorization.mockReset();
|
|
100
102
|
mocks.requestAuthorization.mockResolvedValue(true);
|
|
101
103
|
mocks.requestDraft.mockReset();
|
|
@@ -163,5 +165,9 @@ describe('PanelAppsList', () => {
|
|
|
163
165
|
}));
|
|
164
166
|
expect(mocks.grantPanelAppClient).toHaveBeenCalledWith('demo-app');
|
|
165
167
|
expect(onOpenPanelApp).toHaveBeenCalledWith(entry);
|
|
168
|
+
expect(mocks.recordOpened).toHaveBeenCalledWith(entry.id);
|
|
169
|
+
expect(onOpenPanelApp.mock.invocationCallOrder[0]).toBeLessThan(
|
|
170
|
+
mocks.recordOpened.mock.invocationCallOrder[0]!,
|
|
171
|
+
);
|
|
166
172
|
});
|
|
167
173
|
});
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { AppWindow } from "lucide-react";
|
|
2
|
+
import { cn } from "@/shared/lib/utils";
|
|
3
|
+
|
|
4
|
+
export function PanelAppIcon({
|
|
5
|
+
className,
|
|
6
|
+
icon,
|
|
7
|
+
imageClassName,
|
|
8
|
+
title,
|
|
9
|
+
}: {
|
|
10
|
+
className?: string;
|
|
11
|
+
icon?: string;
|
|
12
|
+
imageClassName?: string;
|
|
13
|
+
title: string;
|
|
14
|
+
}) {
|
|
15
|
+
const normalizedIcon = icon?.trim();
|
|
16
|
+
if (!normalizedIcon) {
|
|
17
|
+
return <AppWindow className={cn("h-4 w-4", className)} />;
|
|
18
|
+
}
|
|
19
|
+
if (isPanelAppImageIcon(normalizedIcon)) {
|
|
20
|
+
return (
|
|
21
|
+
<img
|
|
22
|
+
src={normalizedIcon}
|
|
23
|
+
alt=""
|
|
24
|
+
aria-hidden="true"
|
|
25
|
+
className={cn("h-5 w-5 rounded-sm object-contain", imageClassName, className)}
|
|
26
|
+
title={title}
|
|
27
|
+
/>
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
return (
|
|
31
|
+
<span
|
|
32
|
+
aria-hidden="true"
|
|
33
|
+
className={cn("max-w-6 truncate text-center leading-none", className)}
|
|
34
|
+
title={title}
|
|
35
|
+
>
|
|
36
|
+
{normalizedIcon}
|
|
37
|
+
</span>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function isPanelAppImageIcon(icon: string): boolean {
|
|
42
|
+
return (
|
|
43
|
+
icon.startsWith("data:image/") ||
|
|
44
|
+
icon.startsWith("http://") ||
|
|
45
|
+
icon.startsWith("https://") ||
|
|
46
|
+
icon.startsWith("/")
|
|
47
|
+
);
|
|
48
|
+
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { useState, type MouseEvent } from 'react';
|
|
2
|
-
import {
|
|
2
|
+
import { MoreVertical, Star, Trash2, type LucideIcon } from 'lucide-react';
|
|
3
3
|
import type { PanelAppEntryView } from '@/shared/lib/api';
|
|
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';
|
|
4
6
|
import { ConfirmDialog } from '@/shared/components/ui/confirm-dialog';
|
|
5
7
|
import { Popover, PopoverContent, PopoverTrigger } from '@/shared/components/ui/popover';
|
|
6
8
|
import { getLanguage, getLocale, t } from '@/shared/lib/i18n';
|
|
@@ -36,7 +38,6 @@ export function PanelAppListItem({
|
|
|
36
38
|
setIsMenuOpen(false);
|
|
37
39
|
setIsDeleteDialogOpen(true);
|
|
38
40
|
};
|
|
39
|
-
|
|
40
41
|
return (
|
|
41
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">
|
|
42
43
|
<div className="flex min-w-0 items-start gap-2">
|
|
@@ -75,6 +76,10 @@ export function PanelAppListItem({
|
|
|
75
76
|
</button>
|
|
76
77
|
</PopoverTrigger>
|
|
77
78
|
<PopoverContent align="end" className="w-48 rounded-xl p-1.5">
|
|
79
|
+
<PanelAppMainSidebarMenuItem
|
|
80
|
+
entry={entry}
|
|
81
|
+
onSelect={() => setIsMenuOpen(false)}
|
|
82
|
+
/>
|
|
78
83
|
<PanelAppMenuItem
|
|
79
84
|
destructive
|
|
80
85
|
disabled={deletePending}
|
|
@@ -144,30 +149,3 @@ function formatPanelAppTime(value: string): string {
|
|
|
144
149
|
}
|
|
145
150
|
return new Intl.DateTimeFormat(locale, { year: '2-digit', month: 'numeric', day: 'numeric' }).format(date);
|
|
146
151
|
}
|
|
147
|
-
|
|
148
|
-
function PanelAppIcon({ icon, title }: { icon?: string; title: string }) {
|
|
149
|
-
if (!icon) {
|
|
150
|
-
return <AppWindow className="h-4 w-4" />;
|
|
151
|
-
}
|
|
152
|
-
if (isImageIcon(icon)) {
|
|
153
|
-
return (
|
|
154
|
-
<img
|
|
155
|
-
src={icon}
|
|
156
|
-
alt=""
|
|
157
|
-
aria-hidden="true"
|
|
158
|
-
className="h-5 w-5 rounded-sm object-contain"
|
|
159
|
-
title={title}
|
|
160
|
-
/>
|
|
161
|
-
);
|
|
162
|
-
}
|
|
163
|
-
return <span className="max-w-6 truncate text-center leading-none">{icon}</span>;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
function isImageIcon(icon: string): boolean {
|
|
167
|
-
return (
|
|
168
|
-
icon.startsWith('data:image/') ||
|
|
169
|
-
icon.startsWith('http://') ||
|
|
170
|
-
icon.startsWith('https://') ||
|
|
171
|
-
icon.startsWith('/')
|
|
172
|
-
);
|
|
173
|
-
}
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { useMemo, useState } from "react";
|
|
2
|
+
import { MoreVertical } from "lucide-react";
|
|
3
|
+
import { SidebarNavLinkItem } from "@/app/components/layout/sidebar-items";
|
|
4
|
+
import { SIDEBAR_RAIL_STACK_CLASS } from "@/app/components/layout/sidebar-rail.styles";
|
|
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";
|
|
7
|
+
import { usePanelApps } from "@/features/panel-apps/hooks/use-panel-apps";
|
|
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
|
+
import { cn } from "@/shared/lib/utils";
|
|
12
|
+
|
|
13
|
+
const MAIN_SIDEBAR_FALLBACK_ORDER = Number.MAX_SAFE_INTEGER;
|
|
14
|
+
|
|
15
|
+
export function getMainSidebarPanelApps(
|
|
16
|
+
entries: readonly PanelAppEntryView[],
|
|
17
|
+
): PanelAppEntryView[] {
|
|
18
|
+
return entries
|
|
19
|
+
.filter((entry) => entry.mainSidebar)
|
|
20
|
+
.sort((left, right) =>
|
|
21
|
+
(left.mainSidebarOrder ?? MAIN_SIDEBAR_FALLBACK_ORDER) -
|
|
22
|
+
(right.mainSidebarOrder ?? MAIN_SIDEBAR_FALLBACK_ORDER) ||
|
|
23
|
+
left.title.localeCompare(right.title)
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function PanelAppMainSidebarNav({
|
|
28
|
+
isCollapsed,
|
|
29
|
+
}: {
|
|
30
|
+
isCollapsed: boolean;
|
|
31
|
+
}) {
|
|
32
|
+
const panelApps = usePanelApps();
|
|
33
|
+
const entries = useMemo(
|
|
34
|
+
() => getMainSidebarPanelApps(panelApps.data?.entries ?? []),
|
|
35
|
+
[panelApps.data?.entries],
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
if (entries.length === 0) {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<div
|
|
44
|
+
className={cn(
|
|
45
|
+
"mt-1 border-t border-border/50 pt-1",
|
|
46
|
+
isCollapsed ? "px-0" : "px-3",
|
|
47
|
+
)}
|
|
48
|
+
data-testid="panel-app-main-sidebar-nav"
|
|
49
|
+
>
|
|
50
|
+
<ul className={isCollapsed ? SIDEBAR_RAIL_STACK_CLASS : "space-y-0.5"}>
|
|
51
|
+
{entries.map((entry) => (
|
|
52
|
+
<PanelAppMainSidebarNavItem
|
|
53
|
+
key={entry.appId}
|
|
54
|
+
entry={entry}
|
|
55
|
+
isCollapsed={isCollapsed}
|
|
56
|
+
/>
|
|
57
|
+
))}
|
|
58
|
+
</ul>
|
|
59
|
+
</div>
|
|
60
|
+
);
|
|
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,43 +1,84 @@
|
|
|
1
|
-
import {
|
|
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 { usePanelApps } from '@/features/panel-apps/hooks/use-panel-apps';
|
|
5
|
+
import { readPanelAppIdFromTab } from '@/features/right-panel-resources';
|
|
6
|
+
import { IconActionButton } from '@/shared/components/ui/actions/icon-action-button';
|
|
7
|
+
import { Popover, PopoverContent, PopoverTrigger } from '@/shared/components/ui/popover';
|
|
8
|
+
import type { PanelAppEntryView } from '@/shared/lib/api';
|
|
2
9
|
import { t } from '@/shared/lib/i18n';
|
|
3
10
|
|
|
4
11
|
type PanelAppToolbarProps = {
|
|
5
12
|
appTitle: string;
|
|
6
|
-
|
|
13
|
+
entry?: PanelAppEntryView;
|
|
7
14
|
onRefresh: () => void;
|
|
8
15
|
};
|
|
9
16
|
|
|
10
17
|
export function PanelAppToolbar({
|
|
11
18
|
appTitle,
|
|
12
|
-
|
|
19
|
+
entry,
|
|
13
20
|
onRefresh,
|
|
14
21
|
}: PanelAppToolbarProps) {
|
|
22
|
+
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
|
23
|
+
|
|
15
24
|
return (
|
|
16
25
|
<div className="flex items-center justify-between gap-2 border-b border-border/70 bg-card px-3.5 py-2 shrink-0">
|
|
17
|
-
<button
|
|
18
|
-
type="button"
|
|
19
|
-
onClick={onOpenApps}
|
|
20
|
-
className="inline-flex shrink-0 items-center rounded-md p-1.5 text-muted-foreground transition-colors hover:bg-[var(--interaction-hover)] hover:text-accent-foreground"
|
|
21
|
-
title={t('appsTitle')}
|
|
22
|
-
aria-label={t('appsTitle')}
|
|
23
|
-
>
|
|
24
|
-
<ArrowLeft className="h-3.5 w-3.5" />
|
|
25
|
-
</button>
|
|
26
26
|
<span
|
|
27
27
|
className="min-w-0 flex-1 truncate text-xs font-medium text-muted-foreground"
|
|
28
28
|
title={appTitle}
|
|
29
29
|
>
|
|
30
30
|
{appTitle}
|
|
31
31
|
</span>
|
|
32
|
-
|
|
33
|
-
|
|
32
|
+
{entry ? (
|
|
33
|
+
<Popover open={isMenuOpen} onOpenChange={setIsMenuOpen}>
|
|
34
|
+
<PopoverTrigger asChild>
|
|
35
|
+
<IconActionButton
|
|
36
|
+
icon={<MoreVertical className="h-3.5 w-3.5" />}
|
|
37
|
+
label={t('panelAppsMoreActions')}
|
|
38
|
+
size="sm"
|
|
39
|
+
/>
|
|
40
|
+
</PopoverTrigger>
|
|
41
|
+
<PopoverContent align="end" className="w-48 rounded-xl p-1.5">
|
|
42
|
+
<PanelAppMainSidebarMenuItem
|
|
43
|
+
entry={entry}
|
|
44
|
+
onSelect={() => setIsMenuOpen(false)}
|
|
45
|
+
/>
|
|
46
|
+
</PopoverContent>
|
|
47
|
+
</Popover>
|
|
48
|
+
) : null}
|
|
49
|
+
<IconActionButton
|
|
50
|
+
icon={<RefreshCw className="h-3.5 w-3.5" />}
|
|
51
|
+
label={t('panelAppsRefreshCurrent')}
|
|
34
52
|
onClick={onRefresh}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
aria-label={t('panelAppsRefreshCurrent')}
|
|
38
|
-
>
|
|
39
|
-
<RefreshCw className="h-3.5 w-3.5" />
|
|
40
|
-
</button>
|
|
53
|
+
size="sm"
|
|
54
|
+
/>
|
|
41
55
|
</div>
|
|
42
56
|
);
|
|
43
57
|
}
|
|
58
|
+
|
|
59
|
+
export function PanelAppDocBrowserToolbar({
|
|
60
|
+
appTitle,
|
|
61
|
+
currentUrl,
|
|
62
|
+
onRefresh,
|
|
63
|
+
resourceUri,
|
|
64
|
+
}: {
|
|
65
|
+
appTitle: string;
|
|
66
|
+
currentUrl: string;
|
|
67
|
+
onRefresh: () => void;
|
|
68
|
+
resourceUri?: string;
|
|
69
|
+
}) {
|
|
70
|
+
const panelApps = usePanelApps();
|
|
71
|
+
const appId = readPanelAppIdFromTab({ currentUrl, resourceUri });
|
|
72
|
+
const entry = useMemo(
|
|
73
|
+
() => panelApps.data?.entries.find((candidate) => candidate.appId === appId),
|
|
74
|
+
[appId, panelApps.data?.entries],
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
return (
|
|
78
|
+
<PanelAppToolbar
|
|
79
|
+
appTitle={entry?.title ?? appTitle}
|
|
80
|
+
entry={entry}
|
|
81
|
+
onRefresh={onRefresh}
|
|
82
|
+
/>
|
|
83
|
+
);
|
|
84
|
+
}
|
|
@@ -3,7 +3,8 @@ import { AppWindow, FileCode2, HelpCircle, MessageSquarePlus, RefreshCw } from '
|
|
|
3
3
|
import { useNavigate } from 'react-router-dom';
|
|
4
4
|
import { useAppPresenter } from '@/app/components/app-presenter-provider';
|
|
5
5
|
import { PanelAppListItem } from '@/features/panel-apps/components/panel-app-list-item';
|
|
6
|
-
import {
|
|
6
|
+
import { usePanelAppClientGrant } from '@/features/panel-apps/hooks/use-panel-app-client-grant';
|
|
7
|
+
import { useDeletePanelApp, usePanelApps, useRecordPanelAppOpened, useUpdatePanelAppPreferences } from '@/features/panel-apps/hooks/use-panel-apps';
|
|
7
8
|
import { getPanelAppViewEntries } from '@/features/panel-apps/utils/panel-app-view.utils';
|
|
8
9
|
import type { PanelAppViewMode } from '@/features/panel-apps/utils/panel-app-view.utils';
|
|
9
10
|
import type { PanelAppEntryView } from '@/shared/lib/api';
|
|
@@ -22,7 +23,7 @@ export function PanelAppsList({
|
|
|
22
23
|
const deletePanelApp = useDeletePanelApp();
|
|
23
24
|
const updatePreferences = useUpdatePanelAppPreferences();
|
|
24
25
|
const recordOpened = useRecordPanelAppOpened();
|
|
25
|
-
const
|
|
26
|
+
const { ensurePanelAppClientGrant } = usePanelAppClientGrant();
|
|
26
27
|
const presenter = useAppPresenter();
|
|
27
28
|
const navigate = useNavigate();
|
|
28
29
|
const [viewMode, setViewMode] = useState<PanelAppViewMode>('smart');
|
|
@@ -36,31 +37,8 @@ export function PanelAppsList({
|
|
|
36
37
|
if (!(await ensurePanelAppClientGrant(entry))) {
|
|
37
38
|
return;
|
|
38
39
|
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
} catch {
|
|
42
|
-
onOpenPanelApp(entry);
|
|
43
|
-
}
|
|
44
|
-
};
|
|
45
|
-
|
|
46
|
-
const ensurePanelAppClientGrant = async (entry: PanelAppEntryView): Promise<boolean> => {
|
|
47
|
-
if (!entry.clientDeclared || entry.clientGranted) {
|
|
48
|
-
return true;
|
|
49
|
-
}
|
|
50
|
-
const allowed = await presenter.serviceActionAuthorizationManager.requestAuthorization({
|
|
51
|
-
panelAppId: entry.appId,
|
|
52
|
-
actions: [{
|
|
53
|
-
actionId: 'nextclaw.client',
|
|
54
|
-
actionTitle: t('panelAppsClientGrantTitle'),
|
|
55
|
-
actionDescription: t('panelAppsClientGrantDescription'),
|
|
56
|
-
risk: 'dangerous',
|
|
57
|
-
}],
|
|
58
|
-
});
|
|
59
|
-
if (!allowed) {
|
|
60
|
-
return false;
|
|
61
|
-
}
|
|
62
|
-
await grantClient.mutateAsync(entry.appId);
|
|
63
|
-
return true;
|
|
40
|
+
onOpenPanelApp(entry);
|
|
41
|
+
recordOpened.mutate(entry.id);
|
|
64
42
|
};
|
|
65
43
|
|
|
66
44
|
const toggleFavorite = (entry: PanelAppEntryView) => {
|