@nextclaw/ui 0.24.0-beta.0 → 0.24.0-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/assets/api-BqVIq0lF.js +13 -0
- package/dist/assets/{appearance-settings-page-CPoFZLw8.js → appearance-settings-page-CTWn1eAT.js} +1 -1
- package/dist/assets/{book-open-jcUt0rr9.js → book-open-o2x7OLiO.js} +1 -1
- package/dist/assets/{channels-list-page-anMJAkfK.js → channels-list-page-Bzn-pzyJ.js} +1 -1
- package/dist/assets/{config-split-page-AcuFnbIC.js → config-split-page-CnRbXXlh.js} +1 -1
- package/dist/assets/{confirm-dialog-B-atCfC5.js → confirm-dialog-B_Hbcra2.js} +1 -1
- package/dist/assets/{createLucideIcon-Blz0FzrV.js → createLucideIcon-rwwpqyks.js} +1 -1
- package/dist/assets/{desktop-capabilities-page-8lKF_Ob-.js → desktop-capabilities-page-C2ayYQTr.js} +1 -1
- package/dist/assets/{desktop-update-config-DABXDpbj.js → desktop-update-config-C13czpyL.js} +1 -1
- package/dist/assets/{dist-eH-_j8yU.js → dist-BsF3hfOi.js} +1 -1
- package/dist/assets/{dist-D8kcEpTf.js → dist-aNN3iQxV.js} +1 -1
- package/dist/assets/{doc-browser-DzXRehlW.js → doc-browser-D8dIOo9E.js} +1 -1
- package/dist/assets/doc-browser-DcuOOtpP.js +1 -0
- package/dist/assets/{doc-browser-context-BlbUcg2J.js → doc-browser-context-Dhsit2Ma.js} +1 -1
- package/dist/assets/{extensions-mquwAJ2Z.js → extensions-CD6xvKF_.js} +1 -1
- package/dist/assets/{form-actions-CaZDGEmB.js → form-actions-CC4Wz_lL.js} +1 -1
- package/dist/assets/index-BTsbdtv6.js +109 -0
- package/dist/assets/index-Bcfy2kTh.css +1 -0
- package/dist/assets/loader-circle-B6aFbB-8.js +1 -0
- package/dist/assets/mcp-marketplace-page-BtUAypA7.js +1 -0
- package/dist/assets/{mcp-marketplace-page-gFtgyueL.js → mcp-marketplace-page-r2MmD9xO.js} +1 -1
- package/dist/assets/{model-config-page-hFBpeRAO.js → model-config-page-DboxoT-I.js} +1 -1
- package/dist/assets/{navigation-link-Hb5Z0UxZ.js → navigation-link-C_6oj3zz.js} +1 -1
- package/dist/assets/plus-DRsq9Kvs.js +1 -0
- package/dist/assets/{privacy-settings-page-C18zo3ox.js → privacy-settings-page-C2sDBqhb.js} +1 -1
- package/dist/assets/projects-BcPcx-uD.js +1 -0
- package/dist/assets/{providers-config-page-YU0-AOXJ.js → providers-config-page-CmUFl9RF.js} +1 -1
- package/dist/assets/react-BjO9I4WZ.js +8 -0
- package/dist/assets/{refresh-cw-ZHKfE3yx.js → refresh-cw-DsT2kvXi.js} +1 -1
- package/dist/assets/remote-Ci6x91ES.js +1 -0
- package/dist/assets/{rotate-cw-CLhOXGwE.js → rotate-cw-BCAs6c3R.js} +1 -1
- package/dist/assets/{runtime-config-page-Dmy1A_0Y.js → runtime-config-page-cQKNeAjL.js} +1 -1
- package/dist/assets/{save-CWuuZs77.js → save-CadRMTMQ.js} +1 -1
- package/dist/assets/{search-DcmsKAn2.js → search-B6tB4fzB.js} +1 -1
- package/dist/assets/{search-config-page-CxBacx6W.js → search-config-page-C8lOIkxn.js} +1 -1
- package/dist/assets/{secrets-config-page-B4py8-KR.js → secrets-config-page-8clzAag2.js} +1 -1
- package/dist/assets/security-config-DPbM6IVT.js +1 -0
- package/dist/assets/{select-BEr0Iy_f.js → select-CO9S_C2C.js} +1 -1
- package/dist/assets/skeleton-IHILNhIW.js +1 -0
- package/dist/assets/{tag-chip-ByqDkn6g.js → tag-chip-BZKk0UkK.js} +1 -1
- package/dist/assets/x-BuvHIb1d.js +1 -0
- package/dist/index.html +21 -21
- package/package.json +5 -5
- package/src/features/apps/components/__tests__/app-packages-panel.test.tsx +362 -241
- package/src/features/apps/components/app-document-access-section.tsx +187 -0
- package/src/features/apps/components/app-package-card.tsx +150 -275
- package/src/features/apps/hooks/use-app-packages.ts +91 -35
- package/src/features/chat/index.ts +5 -1
- package/src/features/projects/AGENTS.md +3 -2
- package/src/features/projects/components/project-overview.tsx +70 -139
- package/src/features/projects/components/work/project-work-item-drawer.test.tsx +65 -0
- package/src/features/projects/components/work/project-work-item-drawer.tsx +367 -0
- package/src/features/projects/components/work/project-work-items.test.tsx +98 -0
- package/src/features/projects/components/work/project-work-items.tsx +199 -0
- package/src/features/projects/components/work/project-work-state-settings.tsx +285 -0
- package/src/features/projects/hooks/use-project-observation.ts +56 -26
- package/src/features/projects/hooks/use-project-work.test.tsx +58 -0
- package/src/features/projects/hooks/use-project-work.ts +215 -0
- package/src/features/projects/pages/project-home-page.tsx +119 -138
- package/src/features/projects/pages/projects-page.test.tsx +79 -308
- package/src/shared/lib/i18n/locales/en-US/doc-browser.json +16 -1
- package/src/shared/lib/i18n/locales/en-US/projects.json +60 -1
- package/src/shared/lib/i18n/locales/zh-CN/doc-browser.json +16 -1
- package/src/shared/lib/i18n/locales/zh-CN/projects.json +60 -1
- package/dist/assets/api-CoqPRKmF.js +0 -13
- package/dist/assets/doc-browser-DJEMr7Zk.js +0 -1
- package/dist/assets/index-DyR5iR0O.css +0 -1
- package/dist/assets/index-XxXj8WDS.js +0 -109
- package/dist/assets/loader-circle-0qPOOYQ5.js +0 -1
- package/dist/assets/mcp-marketplace-page-uanamJ6L.js +0 -1
- package/dist/assets/plus-CYOvNvcV.js +0 -1
- package/dist/assets/projects-C8Gm9RS8.js +0 -1
- package/dist/assets/react-DF-kP7iN.js +0 -8
- package/dist/assets/remote-B_utkBra.js +0 -1
- package/dist/assets/security-config-CTP4IQWw.js +0 -1
- package/dist/assets/skeleton-B2NHluWf.js +0 -1
- package/dist/assets/x-CENkFyK5.js +0 -1
- package/src/features/projects/components/project-work-items.test.tsx +0 -78
- package/src/features/projects/components/project-work-items.tsx +0 -272
|
@@ -1,17 +1,24 @@
|
|
|
1
|
-
import { act, render, screen, waitFor } from
|
|
2
|
-
import userEvent from
|
|
3
|
-
import type { AppPackageOperationView } from
|
|
4
|
-
import { beforeEach, describe, expect, it, vi } from
|
|
5
|
-
import { AppPackagesPanel } from
|
|
1
|
+
import { act, render, screen, waitFor } from "@testing-library/react";
|
|
2
|
+
import userEvent from "@testing-library/user-event";
|
|
3
|
+
import type { AppPackageOperationView } from "@nextclaw/client-sdk";
|
|
4
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
5
|
+
import { AppPackagesPanel } from "@/features/apps/components/app-packages-panel";
|
|
6
6
|
|
|
7
7
|
const mocks = vi.hoisted(() => ({
|
|
8
8
|
appDataError: false,
|
|
9
9
|
enabled: false,
|
|
10
10
|
includePackageStorageUsage: true,
|
|
11
|
-
readinessStatus:
|
|
11
|
+
readinessStatus: "ready" as
|
|
12
|
+
| "ready"
|
|
13
|
+
| "needs-capability"
|
|
14
|
+
| "needs-configuration",
|
|
12
15
|
grantClient: vi.fn(),
|
|
13
16
|
lifecycleMutate: vi.fn(),
|
|
14
17
|
lifecycleReset: vi.fn(),
|
|
18
|
+
documentAccessMutate: vi.fn(),
|
|
19
|
+
documentAccessMutateAsync: vi.fn(),
|
|
20
|
+
documentAccessReset: vi.fn(),
|
|
21
|
+
documentAccessScopes: [] as Array<Record<string, unknown>>,
|
|
15
22
|
onOpen: vi.fn(),
|
|
16
23
|
operations: [] as AppPackageOperationView[],
|
|
17
24
|
recordOpened: vi.fn(),
|
|
@@ -25,10 +32,10 @@ const mocks = vi.hoisted(() => ({
|
|
|
25
32
|
requestAuthorization: vi.fn(async () => true),
|
|
26
33
|
}));
|
|
27
34
|
|
|
28
|
-
vi.mock(
|
|
35
|
+
vi.mock("@/features/app-data/hooks/use-app-data", () => ({
|
|
29
36
|
useAppData: () => ({
|
|
30
37
|
data: { entries: mocks.retainedEntries, diagnostics: [] },
|
|
31
|
-
error: mocks.appDataError ? new Error(
|
|
38
|
+
error: mocks.appDataError ? new Error("App data unavailable") : null,
|
|
32
39
|
isError: mocks.appDataError,
|
|
33
40
|
isLoading: false,
|
|
34
41
|
refetch: mocks.refetchAppData,
|
|
@@ -42,7 +49,7 @@ vi.mock('@/features/app-data/hooks/use-app-data', () => ({
|
|
|
42
49
|
}),
|
|
43
50
|
}));
|
|
44
51
|
|
|
45
|
-
vi.mock(
|
|
52
|
+
vi.mock("@/app/components/app-presenter-provider", () => ({
|
|
46
53
|
useAppPresenter: () => ({
|
|
47
54
|
appPackageOperationSettlementManager: { settle: vi.fn() },
|
|
48
55
|
serviceActionAuthorizationManager: {
|
|
@@ -51,15 +58,34 @@ vi.mock('@/app/components/app-presenter-provider', () => ({
|
|
|
51
58
|
}),
|
|
52
59
|
}));
|
|
53
60
|
|
|
54
|
-
vi.mock(
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
61
|
+
vi.mock("@/shared/components/path-picker/server-path-picker-dialog", () => ({
|
|
62
|
+
ServerPathPickerDialog: ({
|
|
63
|
+
open,
|
|
64
|
+
onConfirm,
|
|
65
|
+
}: {
|
|
66
|
+
open: boolean;
|
|
67
|
+
onConfirm: (path: string) => Promise<void>;
|
|
68
|
+
}) =>
|
|
69
|
+
open ? (
|
|
70
|
+
<button
|
|
71
|
+
type="button"
|
|
72
|
+
onClick={() => void onConfirm("/tmp/authorized-documents")}
|
|
73
|
+
>
|
|
74
|
+
Confirm test folder
|
|
75
|
+
</button>
|
|
76
|
+
) : null,
|
|
77
|
+
}));
|
|
78
|
+
|
|
79
|
+
vi.mock("@/features/apps/hooks/use-app-packages", () => ({
|
|
80
|
+
isAppPackageOperationActive: (status: string) =>
|
|
81
|
+
[
|
|
82
|
+
"queued",
|
|
83
|
+
"resolving",
|
|
84
|
+
"downloading",
|
|
85
|
+
"verifying",
|
|
86
|
+
"installing",
|
|
87
|
+
"finalizing",
|
|
88
|
+
].includes(status),
|
|
63
89
|
useAppPackageMutation: () => ({
|
|
64
90
|
error: null,
|
|
65
91
|
isError: false,
|
|
@@ -68,6 +94,13 @@ vi.mock('@/features/apps/hooks/use-app-packages', () => ({
|
|
|
68
94
|
reset: mocks.lifecycleReset,
|
|
69
95
|
variables: undefined,
|
|
70
96
|
}),
|
|
97
|
+
useAppDocumentAccessMutation: () => ({
|
|
98
|
+
error: null,
|
|
99
|
+
isPending: false,
|
|
100
|
+
mutate: mocks.documentAccessMutate,
|
|
101
|
+
mutateAsync: mocks.documentAccessMutateAsync,
|
|
102
|
+
reset: mocks.documentAccessReset,
|
|
103
|
+
}),
|
|
71
104
|
useAppPackageOperationSettlement: () => undefined,
|
|
72
105
|
useAppPackageOperations: () => ({
|
|
73
106
|
data: { entries: mocks.operations },
|
|
@@ -75,66 +108,77 @@ vi.mock('@/features/apps/hooks/use-app-packages', () => ({
|
|
|
75
108
|
}),
|
|
76
109
|
useAppPackages: () => ({
|
|
77
110
|
data: {
|
|
78
|
-
entries: [
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
{
|
|
99
|
-
dataDirectory: '/tmp/data',
|
|
100
|
-
id: 'nextclaw-personal-organizer-data',
|
|
101
|
-
kind: 'service',
|
|
102
|
-
manifestPath: '/tmp/app/service-app.json',
|
|
103
|
-
packageId: 'nextclaw.personal-organizer',
|
|
104
|
-
packageVersion: '0.1.0',
|
|
105
|
-
sourcePath: '/tmp/app/data',
|
|
106
|
-
instanceId: 'default',
|
|
107
|
-
isolation: 'full-user',
|
|
108
|
-
runtimeProfile: 'native-process',
|
|
109
|
-
storage: createStorageFixture(),
|
|
110
|
-
title: 'Personal data',
|
|
111
|
-
},
|
|
112
|
-
],
|
|
113
|
-
dataDirectory: '/tmp/data',
|
|
114
|
-
description: 'Todos, notes, favorites, and calendar.',
|
|
115
|
-
enabled: mocks.enabled,
|
|
116
|
-
id: 'nextclaw.personal-organizer',
|
|
117
|
-
installedVersions: ['0.1.0'],
|
|
118
|
-
instanceId: 'default',
|
|
119
|
-
isolation: 'full-user',
|
|
120
|
-
name: 'Personal Space',
|
|
121
|
-
primaryPanelId: 'nextclaw-personal-organizer-todos',
|
|
122
|
-
readiness: mocks.readinessStatus === 'ready'
|
|
123
|
-
? { status: 'ready' as const, requirements: [] }
|
|
124
|
-
: {
|
|
125
|
-
status: mocks.readinessStatus,
|
|
126
|
-
requirements: [{
|
|
127
|
-
componentId: 'nextclaw-personal-organizer-data',
|
|
128
|
-
kind: mocks.readinessStatus === 'needs-capability' ? 'capability' as const : 'configuration' as const,
|
|
129
|
-
id: 'shared-cache',
|
|
130
|
-
title: 'Shared workspace connection',
|
|
131
|
-
description: 'Keeps your team data in sync.',
|
|
132
|
-
}],
|
|
111
|
+
entries: [
|
|
112
|
+
{
|
|
113
|
+
activeVersion: "0.1.0",
|
|
114
|
+
builtIn: true,
|
|
115
|
+
components: [
|
|
116
|
+
{
|
|
117
|
+
dataDirectory: "/tmp/data",
|
|
118
|
+
description: "Clear personal todos.",
|
|
119
|
+
id: "nextclaw-personal-organizer-todos",
|
|
120
|
+
icon: "✓",
|
|
121
|
+
kind: "panel",
|
|
122
|
+
manifestPath: "/tmp/app/panel-app.json",
|
|
123
|
+
packageId: "nextclaw.personal-organizer",
|
|
124
|
+
packageVersion: "0.1.0",
|
|
125
|
+
sourcePath: "/tmp/app/todos.panel",
|
|
126
|
+
instanceId: "default",
|
|
127
|
+
isolation: "full-user",
|
|
128
|
+
runtimeProfile: "native-process",
|
|
129
|
+
storage: createStorageFixture(),
|
|
130
|
+
title: "Todos",
|
|
133
131
|
},
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
132
|
+
{
|
|
133
|
+
dataDirectory: "/tmp/data",
|
|
134
|
+
id: "nextclaw-personal-organizer-data",
|
|
135
|
+
kind: "service",
|
|
136
|
+
manifestPath: "/tmp/app/service-app.json",
|
|
137
|
+
packageId: "nextclaw.personal-organizer",
|
|
138
|
+
packageVersion: "0.1.0",
|
|
139
|
+
sourcePath: "/tmp/app/data",
|
|
140
|
+
instanceId: "default",
|
|
141
|
+
isolation: "full-user",
|
|
142
|
+
runtimeProfile: "native-process",
|
|
143
|
+
storage: createStorageFixture(),
|
|
144
|
+
title: "Personal data",
|
|
145
|
+
},
|
|
146
|
+
],
|
|
147
|
+
dataDirectory: "/tmp/data",
|
|
148
|
+
description: "Todos, notes, favorites, and calendar.",
|
|
149
|
+
documentAccess: mocks.documentAccessScopes,
|
|
150
|
+
enabled: mocks.enabled,
|
|
151
|
+
id: "nextclaw.personal-organizer",
|
|
152
|
+
installedVersions: ["0.1.0"],
|
|
153
|
+
instanceId: "default",
|
|
154
|
+
isolation: "full-user",
|
|
155
|
+
name: "Personal Space",
|
|
156
|
+
primaryPanelId: "nextclaw-personal-organizer-todos",
|
|
157
|
+
readiness:
|
|
158
|
+
mocks.readinessStatus === "ready"
|
|
159
|
+
? { status: "ready" as const, requirements: [] }
|
|
160
|
+
: {
|
|
161
|
+
status: mocks.readinessStatus,
|
|
162
|
+
requirements: [
|
|
163
|
+
{
|
|
164
|
+
componentId: "nextclaw-personal-organizer-data",
|
|
165
|
+
kind:
|
|
166
|
+
mocks.readinessStatus === "needs-capability"
|
|
167
|
+
? ("capability" as const)
|
|
168
|
+
: ("configuration" as const),
|
|
169
|
+
id: "shared-cache",
|
|
170
|
+
title: "Shared workspace connection",
|
|
171
|
+
description: "Keeps your team data in sync.",
|
|
172
|
+
},
|
|
173
|
+
],
|
|
174
|
+
},
|
|
175
|
+
runtimeProfile: "native-process",
|
|
176
|
+
storage: createStorageFixture(),
|
|
177
|
+
storageUsage: mocks.includePackageStorageUsage
|
|
178
|
+
? createUsageFixture()
|
|
179
|
+
: undefined,
|
|
180
|
+
},
|
|
181
|
+
],
|
|
138
182
|
},
|
|
139
183
|
error: null,
|
|
140
184
|
isError: false,
|
|
@@ -145,16 +189,16 @@ vi.mock('@/features/apps/hooks/use-app-packages', () => ({
|
|
|
145
189
|
|
|
146
190
|
function createStorageFixture() {
|
|
147
191
|
return {
|
|
148
|
-
layout:
|
|
192
|
+
layout: "instance-v1" as const,
|
|
149
193
|
layoutVersion: 1 as const,
|
|
150
|
-
instanceId:
|
|
151
|
-
instanceDirectory:
|
|
152
|
-
dataDirectory:
|
|
153
|
-
configDirectory:
|
|
154
|
-
stateDirectory:
|
|
155
|
-
cacheDirectory:
|
|
156
|
-
temporaryDirectory:
|
|
157
|
-
logsDirectory:
|
|
194
|
+
instanceId: "default",
|
|
195
|
+
instanceDirectory: "/tmp/instance",
|
|
196
|
+
dataDirectory: "/tmp/data",
|
|
197
|
+
configDirectory: "/tmp/config",
|
|
198
|
+
stateDirectory: "/tmp/state",
|
|
199
|
+
cacheDirectory: "/tmp/cache",
|
|
200
|
+
temporaryDirectory: "/tmp/tmp",
|
|
201
|
+
logsDirectory: "/tmp/logs",
|
|
158
202
|
};
|
|
159
203
|
}
|
|
160
204
|
|
|
@@ -170,46 +214,46 @@ function createUsageFixture() {
|
|
|
170
214
|
};
|
|
171
215
|
}
|
|
172
216
|
|
|
173
|
-
vi.mock(
|
|
217
|
+
vi.mock("@/features/apps/hooks/use-app-marketplace", () => ({
|
|
174
218
|
useAppMarketplace: () => ({
|
|
175
219
|
data: {
|
|
176
220
|
total: 2,
|
|
177
221
|
items: [
|
|
178
222
|
{
|
|
179
|
-
id:
|
|
180
|
-
slug:
|
|
181
|
-
appId:
|
|
182
|
-
name:
|
|
183
|
-
summary:
|
|
184
|
-
summaryI18n: { en:
|
|
185
|
-
tags: [
|
|
186
|
-
latestVersion:
|
|
223
|
+
id: "app-personal-organizer",
|
|
224
|
+
slug: "personal-organizer",
|
|
225
|
+
appId: "nextclaw.personal-organizer",
|
|
226
|
+
name: "个人空间",
|
|
227
|
+
summary: "A calm personal space.",
|
|
228
|
+
summaryI18n: { en: "A calm personal space." },
|
|
229
|
+
tags: ["personal"],
|
|
230
|
+
latestVersion: "0.1.0",
|
|
187
231
|
featured: true,
|
|
188
|
-
publisher: { id:
|
|
232
|
+
publisher: { id: "nextclaw", name: "NextClaw" },
|
|
189
233
|
install: {
|
|
190
|
-
kind:
|
|
191
|
-
spec:
|
|
192
|
-
registry:
|
|
234
|
+
kind: "registry",
|
|
235
|
+
spec: "nextclaw.personal-organizer",
|
|
236
|
+
registry: "https://apps-registry.nextclaw.io/api/v1/apps/registry/",
|
|
193
237
|
},
|
|
194
|
-
webUrl:
|
|
238
|
+
webUrl: "https://apps.nextclaw.io/apps/personal-organizer",
|
|
195
239
|
},
|
|
196
240
|
{
|
|
197
|
-
id:
|
|
198
|
-
slug:
|
|
199
|
-
appId:
|
|
200
|
-
name:
|
|
201
|
-
summary:
|
|
202
|
-
summaryI18n: { en:
|
|
203
|
-
tags: [
|
|
204
|
-
latestVersion:
|
|
241
|
+
id: "app-workspace-glance",
|
|
242
|
+
slug: "workspace-glance",
|
|
243
|
+
appId: "nextclaw.workspace-glance",
|
|
244
|
+
name: "Workspace Glance",
|
|
245
|
+
summary: "A local workspace overview.",
|
|
246
|
+
summaryI18n: { en: "A local workspace overview." },
|
|
247
|
+
tags: ["workspace", "local"],
|
|
248
|
+
latestVersion: "0.1.0",
|
|
205
249
|
featured: false,
|
|
206
|
-
publisher: { id:
|
|
250
|
+
publisher: { id: "nextclaw", name: "NextClaw" },
|
|
207
251
|
install: {
|
|
208
|
-
kind:
|
|
209
|
-
spec:
|
|
210
|
-
registry:
|
|
252
|
+
kind: "registry",
|
|
253
|
+
spec: "nextclaw.workspace-glance",
|
|
254
|
+
registry: "https://apps-registry.nextclaw.io/api/v1/apps/registry/",
|
|
211
255
|
},
|
|
212
|
-
webUrl:
|
|
256
|
+
webUrl: "https://apps.nextclaw.io/apps/workspace-glance",
|
|
213
257
|
},
|
|
214
258
|
],
|
|
215
259
|
},
|
|
@@ -224,28 +268,32 @@ vi.mock('@/features/apps/hooks/use-app-marketplace', () => ({
|
|
|
224
268
|
}),
|
|
225
269
|
}));
|
|
226
270
|
|
|
227
|
-
vi.mock(
|
|
271
|
+
vi.mock("@/features/panel-apps/hooks/use-panel-apps", () => ({
|
|
228
272
|
useGrantPanelAppClient: () => ({ mutateAsync: mocks.grantClient }),
|
|
229
273
|
usePanelApps: () => ({
|
|
230
274
|
data: {
|
|
231
|
-
entries: mocks.enabled
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
275
|
+
entries: mocks.enabled
|
|
276
|
+
? [
|
|
277
|
+
{
|
|
278
|
+
appId: "nextclaw-personal-organizer-todos",
|
|
279
|
+
clientDeclared: false,
|
|
280
|
+
clientGranted: false,
|
|
281
|
+
contentPath: "/api/panel-apps/todos/content",
|
|
282
|
+
createdAt: "2026-08-12T00:00:00.000Z",
|
|
283
|
+
favorite: false,
|
|
284
|
+
fileName: "nextclaw-personal-organizer-todos.panel",
|
|
285
|
+
id: "todos",
|
|
286
|
+
kind: "folder",
|
|
287
|
+
openCount: 0,
|
|
288
|
+
packageId: "nextclaw.personal-organizer",
|
|
289
|
+
packageVersion: "0.1.0",
|
|
290
|
+
sizeBytes: 1,
|
|
291
|
+
sourceKind: "package",
|
|
292
|
+
title: "Todos",
|
|
293
|
+
updatedAt: "2026-08-12T00:00:00.000Z",
|
|
294
|
+
},
|
|
295
|
+
]
|
|
296
|
+
: [],
|
|
249
297
|
},
|
|
250
298
|
error: null,
|
|
251
299
|
isError: false,
|
|
@@ -255,15 +303,20 @@ vi.mock('@/features/panel-apps/hooks/use-panel-apps', () => ({
|
|
|
255
303
|
useRecordPanelAppOpened: () => ({ mutateAsync: mocks.recordOpened }),
|
|
256
304
|
}));
|
|
257
305
|
|
|
258
|
-
describe(
|
|
306
|
+
describe("AppPackagesPanel", () => {
|
|
259
307
|
beforeEach(() => {
|
|
260
308
|
mocks.appDataError = false;
|
|
261
309
|
mocks.enabled = false;
|
|
262
310
|
mocks.includePackageStorageUsage = true;
|
|
263
|
-
mocks.readinessStatus =
|
|
311
|
+
mocks.readinessStatus = "ready";
|
|
264
312
|
mocks.grantClient.mockReset();
|
|
265
313
|
mocks.lifecycleMutate.mockReset();
|
|
266
314
|
mocks.lifecycleReset.mockReset();
|
|
315
|
+
mocks.documentAccessMutate.mockReset();
|
|
316
|
+
mocks.documentAccessMutateAsync.mockReset();
|
|
317
|
+
mocks.documentAccessMutateAsync.mockResolvedValue({});
|
|
318
|
+
mocks.documentAccessReset.mockReset();
|
|
319
|
+
mocks.documentAccessScopes = [];
|
|
267
320
|
mocks.onOpen.mockReset();
|
|
268
321
|
mocks.operations = [];
|
|
269
322
|
mocks.recordOpened.mockReset();
|
|
@@ -278,35 +331,70 @@ describe('AppPackagesPanel', () => {
|
|
|
278
331
|
mocks.requestAuthorization.mockResolvedValue(true);
|
|
279
332
|
});
|
|
280
333
|
|
|
281
|
-
it(
|
|
334
|
+
it("shows a built-in package and enables it from the primary action", async () => {
|
|
282
335
|
const user = userEvent.setup();
|
|
283
336
|
|
|
284
337
|
render(<AppPackagesPanel onOpenPanelApp={mocks.onOpen} />);
|
|
285
338
|
|
|
286
|
-
expect(screen.getByText(
|
|
287
|
-
expect(screen.getByText(
|
|
288
|
-
expect(screen.getByText(
|
|
339
|
+
expect(screen.getByText("Personal Space")).toBeTruthy();
|
|
340
|
+
expect(screen.getByText("Available")).toBeTruthy();
|
|
341
|
+
expect(screen.getByText("Local data service")).toBeTruthy();
|
|
289
342
|
|
|
290
|
-
await user.click(screen.getByRole(
|
|
343
|
+
await user.click(screen.getByRole("button", { name: "Enable" }));
|
|
291
344
|
|
|
292
345
|
expect(mocks.lifecycleMutate).toHaveBeenCalledWith(
|
|
293
|
-
{ action:
|
|
346
|
+
{ action: "enable", appId: "nextclaw.personal-organizer" },
|
|
294
347
|
{ onSuccess: undefined },
|
|
295
348
|
);
|
|
296
349
|
});
|
|
297
350
|
|
|
298
|
-
it(
|
|
299
|
-
mocks.
|
|
351
|
+
it("chooses a runtime-host folder and grants the selected document mode", async () => {
|
|
352
|
+
mocks.documentAccessScopes = [
|
|
353
|
+
{
|
|
354
|
+
id: "workspace",
|
|
355
|
+
mode: "read-write",
|
|
356
|
+
description: "Read and update the chosen workspace.",
|
|
357
|
+
granted: false,
|
|
358
|
+
status: "ungranted",
|
|
359
|
+
availableActions: ["grant"],
|
|
360
|
+
},
|
|
361
|
+
];
|
|
362
|
+
const user = userEvent.setup();
|
|
363
|
+
render(<AppPackagesPanel onOpenPanelApp={mocks.onOpen} />);
|
|
364
|
+
|
|
365
|
+
await user.selectOptions(
|
|
366
|
+
screen.getByLabelText("Access mode"),
|
|
367
|
+
"read-write",
|
|
368
|
+
);
|
|
369
|
+
await user.click(screen.getByRole("button", { name: "Choose folder" }));
|
|
370
|
+
await user.click(
|
|
371
|
+
screen.getByRole("button", { name: "Confirm test folder" }),
|
|
372
|
+
);
|
|
373
|
+
|
|
374
|
+
expect(mocks.documentAccessMutateAsync).toHaveBeenCalledWith({
|
|
375
|
+
action: "grant",
|
|
376
|
+
appId: "nextclaw.personal-organizer",
|
|
377
|
+
scopeId: "workspace",
|
|
378
|
+
directoryPath: "/tmp/authorized-documents",
|
|
379
|
+
mode: "read-write",
|
|
380
|
+
});
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
it("explains missing external setup and does not offer an unusable enable action", () => {
|
|
384
|
+
mocks.readinessStatus = "needs-configuration";
|
|
300
385
|
|
|
301
386
|
render(<AppPackagesPanel onOpenPanelApp={mocks.onOpen} />);
|
|
302
387
|
|
|
303
|
-
expect(screen.getByText(
|
|
388
|
+
expect(screen.getByText("Needs setup")).toBeTruthy();
|
|
304
389
|
expect(screen.getByText(/Shared workspace connection/)).toBeTruthy();
|
|
305
390
|
expect(screen.getByText(/Keeps your team data in sync/)).toBeTruthy();
|
|
306
|
-
expect(
|
|
391
|
+
expect(
|
|
392
|
+
(screen.getByRole("button", { name: "Enable" }) as HTMLButtonElement)
|
|
393
|
+
.disabled,
|
|
394
|
+
).toBe(true);
|
|
307
395
|
});
|
|
308
396
|
|
|
309
|
-
it(
|
|
397
|
+
it("scrolls and transfers focus to a package selected from Service Apps", async () => {
|
|
310
398
|
render(
|
|
311
399
|
<AppPackagesPanel
|
|
312
400
|
focusedPackageId="nextclaw.personal-organizer"
|
|
@@ -314,188 +402,221 @@ describe('AppPackagesPanel', () => {
|
|
|
314
402
|
/>,
|
|
315
403
|
);
|
|
316
404
|
|
|
317
|
-
const packageCard = screen.getByRole(
|
|
405
|
+
const packageCard = screen.getByRole("region", { name: "Personal Space" });
|
|
318
406
|
await waitFor(() => expect(document.activeElement).toBe(packageCard));
|
|
319
|
-
expect(packageCard.className).toContain(
|
|
407
|
+
expect(packageCard.className).toContain("ring-2");
|
|
320
408
|
});
|
|
321
409
|
|
|
322
|
-
it(
|
|
410
|
+
it("distinguishes an untouched app from an App Data query failure", () => {
|
|
323
411
|
mocks.includePackageStorageUsage = false;
|
|
324
412
|
const view = render(<AppPackagesPanel onOpenPanelApp={mocks.onOpen} />);
|
|
325
|
-
expect(screen.getByTitle(
|
|
413
|
+
expect(screen.getByTitle("/tmp/data").textContent).toContain("No data yet");
|
|
326
414
|
|
|
327
415
|
mocks.appDataError = true;
|
|
328
416
|
view.rerender(<AppPackagesPanel onOpenPanelApp={mocks.onOpen} />);
|
|
329
417
|
|
|
330
|
-
expect(screen.getByTitle(
|
|
418
|
+
expect(screen.getByTitle("/tmp/data").textContent).toContain(
|
|
419
|
+
"Size unavailable",
|
|
420
|
+
);
|
|
331
421
|
});
|
|
332
422
|
|
|
333
|
-
it(
|
|
423
|
+
it("opens an enabled package panel through the existing panel app chain", async () => {
|
|
334
424
|
const user = userEvent.setup();
|
|
335
425
|
mocks.enabled = true;
|
|
336
426
|
const openedEntry = {
|
|
337
|
-
appId:
|
|
338
|
-
id:
|
|
339
|
-
title:
|
|
427
|
+
appId: "nextclaw-personal-organizer-todos",
|
|
428
|
+
id: "todos",
|
|
429
|
+
title: "Todos",
|
|
340
430
|
};
|
|
341
431
|
mocks.recordOpened.mockResolvedValue(openedEntry);
|
|
342
432
|
|
|
343
433
|
render(<AppPackagesPanel onOpenPanelApp={mocks.onOpen} />);
|
|
344
434
|
|
|
345
|
-
await user.click(screen.getByRole(
|
|
435
|
+
await user.click(screen.getByRole("button", { name: /Todos/ }));
|
|
346
436
|
|
|
347
|
-
expect(mocks.recordOpened).toHaveBeenCalledWith(
|
|
437
|
+
expect(mocks.recordOpened).toHaveBeenCalledWith("todos");
|
|
348
438
|
expect(mocks.onOpen).toHaveBeenCalledWith(openedEntry);
|
|
349
439
|
});
|
|
350
440
|
|
|
351
|
-
it(
|
|
441
|
+
it("discovers marketplace apps and installs one through its registry spec", async () => {
|
|
352
442
|
const user = userEvent.setup();
|
|
353
443
|
|
|
354
444
|
render(<AppPackagesPanel onOpenPanelApp={mocks.onOpen} />);
|
|
355
445
|
|
|
356
|
-
await user.click(screen.getByRole(
|
|
446
|
+
await user.click(screen.getByRole("button", { name: "Add apps" }));
|
|
357
447
|
|
|
358
|
-
expect(screen.getByRole(
|
|
359
|
-
expect(screen.getByText(
|
|
448
|
+
expect(screen.getByRole("heading", { name: "Add apps" })).toBeTruthy();
|
|
449
|
+
expect(screen.getByText("Workspace Glance")).toBeTruthy();
|
|
360
450
|
expect(
|
|
361
|
-
(screen.getByRole(
|
|
451
|
+
(screen.getByRole("button", { name: "Installed" }) as HTMLButtonElement)
|
|
452
|
+
.disabled,
|
|
362
453
|
).toBe(true);
|
|
363
454
|
|
|
364
|
-
const installButtons = screen.getAllByRole(
|
|
455
|
+
const installButtons = screen.getAllByRole("button", {
|
|
456
|
+
name: "Install app",
|
|
457
|
+
});
|
|
365
458
|
await user.click(installButtons.at(-1)!);
|
|
366
459
|
|
|
367
460
|
expect(mocks.lifecycleMutate).toHaveBeenCalledWith(
|
|
368
461
|
{
|
|
369
|
-
action:
|
|
370
|
-
source:
|
|
371
|
-
registryUrl:
|
|
462
|
+
action: "install",
|
|
463
|
+
source: "nextclaw.workspace-glance",
|
|
464
|
+
registryUrl: "https://apps-registry.nextclaw.io/api/v1/apps/registry/",
|
|
372
465
|
},
|
|
373
466
|
{ onSuccess: undefined },
|
|
374
467
|
);
|
|
375
468
|
});
|
|
376
469
|
|
|
377
|
-
it(
|
|
470
|
+
it("allows a built-in app to be uninstalled while keeping its data by default", async () => {
|
|
378
471
|
const user = userEvent.setup();
|
|
379
472
|
|
|
380
473
|
render(<AppPackagesPanel onOpenPanelApp={mocks.onOpen} />);
|
|
381
474
|
|
|
382
|
-
await user.click(screen.getByRole(
|
|
383
|
-
await user.click(screen.getByRole(
|
|
384
|
-
expect(
|
|
475
|
+
await user.click(screen.getByRole("button", { name: "More app actions" }));
|
|
476
|
+
await user.click(screen.getByRole("button", { name: "Uninstall" }));
|
|
477
|
+
expect(
|
|
478
|
+
screen.getByRole("heading", { name: "Uninstall this app?" }),
|
|
479
|
+
).toBeTruthy();
|
|
385
480
|
await waitFor(() => {
|
|
386
|
-
expect(document.activeElement).toBe(
|
|
481
|
+
expect(document.activeElement).toBe(
|
|
482
|
+
screen.getByRole("button", { name: "Cancel" }),
|
|
483
|
+
);
|
|
387
484
|
});
|
|
388
|
-
expect(
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
485
|
+
expect(
|
|
486
|
+
screen
|
|
487
|
+
.getByRole("button", { name: /Keep personal data/ })
|
|
488
|
+
.getAttribute("aria-pressed"),
|
|
489
|
+
).toBe("true");
|
|
490
|
+
expect(screen.getByText("Total app data")).toBeTruthy();
|
|
491
|
+
expect(screen.getByText("Managed instance path")).toBeTruthy();
|
|
492
|
+
|
|
493
|
+
await user.click(screen.getByRole("button", { name: "Uninstall" }));
|
|
394
494
|
expect(mocks.lifecycleMutate).toHaveBeenCalledWith(
|
|
395
|
-
{
|
|
495
|
+
{
|
|
496
|
+
action: "uninstall",
|
|
497
|
+
appId: "nextclaw.personal-organizer",
|
|
498
|
+
purgeData: false,
|
|
499
|
+
},
|
|
396
500
|
{ onSuccess: undefined },
|
|
397
501
|
);
|
|
398
502
|
});
|
|
399
503
|
|
|
400
|
-
it(
|
|
504
|
+
it("shows retained data and requires a dedicated permanent-delete confirmation", async () => {
|
|
401
505
|
const user = userEvent.setup();
|
|
402
|
-
mocks.retainedEntries = [
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
506
|
+
mocks.retainedEntries = [
|
|
507
|
+
{
|
|
508
|
+
id: "ad1.encoded",
|
|
509
|
+
appId: "example.notes",
|
|
510
|
+
instanceId: "default",
|
|
511
|
+
displayName: "Notes",
|
|
512
|
+
source: "package",
|
|
513
|
+
lifecycle: "retained",
|
|
514
|
+
storage: createStorageFixture(),
|
|
515
|
+
usage: {
|
|
516
|
+
dataBytes: 128,
|
|
517
|
+
configBytes: 3,
|
|
518
|
+
stateBytes: 2,
|
|
519
|
+
cacheBytes: 1,
|
|
520
|
+
temporaryBytes: 0,
|
|
521
|
+
logsBytes: 4,
|
|
522
|
+
totalBytes: 138,
|
|
523
|
+
},
|
|
524
|
+
createdAt: "2026-08-14T00:00:00.000Z",
|
|
525
|
+
actions: { deleteRetainedData: true },
|
|
418
526
|
},
|
|
419
|
-
|
|
420
|
-
actions: { deleteRetainedData: true },
|
|
421
|
-
}];
|
|
527
|
+
];
|
|
422
528
|
const view = render(<AppPackagesPanel onOpenPanelApp={mocks.onOpen} />);
|
|
423
529
|
|
|
424
|
-
expect(
|
|
425
|
-
|
|
426
|
-
|
|
530
|
+
expect(
|
|
531
|
+
screen.getByRole("heading", { name: "Saved app data" }),
|
|
532
|
+
).toBeTruthy();
|
|
533
|
+
await user.click(screen.getByRole("button", { name: "Delete data" }));
|
|
534
|
+
expect(
|
|
535
|
+
screen.getByRole("heading", {
|
|
536
|
+
name: "Permanently delete this app data?",
|
|
537
|
+
}),
|
|
538
|
+
).toBeTruthy();
|
|
427
539
|
|
|
428
|
-
await user.click(
|
|
540
|
+
await user.click(
|
|
541
|
+
screen.getByRole("button", { name: "Permanently delete data" }),
|
|
542
|
+
);
|
|
429
543
|
expect(mocks.deleteRetained).toHaveBeenCalledWith(
|
|
430
|
-
{ dataId:
|
|
544
|
+
{ dataId: "ad1.encoded", confirmAppId: "example.notes" },
|
|
431
545
|
{ onSuccess: expect.any(Function) },
|
|
432
546
|
);
|
|
433
|
-
const onSuccess = mocks.deleteRetained.mock.calls[0]?.[1]
|
|
547
|
+
const onSuccess = mocks.deleteRetained.mock.calls[0]?.[1]
|
|
548
|
+
.onSuccess as () => void;
|
|
434
549
|
mocks.retainedEntries = [];
|
|
435
550
|
view.rerender(<AppPackagesPanel onOpenPanelApp={mocks.onOpen} />);
|
|
436
551
|
await act(async () => onSuccess());
|
|
437
552
|
await waitFor(() => {
|
|
438
|
-
expect(document.activeElement).toBe(
|
|
553
|
+
expect(document.activeElement).toBe(
|
|
554
|
+
screen.getByRole("heading", { name: "Your apps" }),
|
|
555
|
+
);
|
|
439
556
|
});
|
|
440
557
|
});
|
|
441
558
|
|
|
442
|
-
it(
|
|
559
|
+
it("keeps an accepted install visible in the library and marketplace while it runs", async () => {
|
|
443
560
|
const user = userEvent.setup();
|
|
444
|
-
mocks.operations = [
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
561
|
+
mocks.operations = [
|
|
562
|
+
{
|
|
563
|
+
id: "operation-1",
|
|
564
|
+
action: "install",
|
|
565
|
+
appId: "nextclaw.workspace-glance",
|
|
566
|
+
source: "nextclaw.workspace-glance",
|
|
567
|
+
status: "downloading",
|
|
568
|
+
completedSteps: 2,
|
|
569
|
+
totalSteps: 5,
|
|
570
|
+
createdAt: "2026-08-12T00:00:00.000Z",
|
|
571
|
+
updatedAt: "2026-08-12T00:00:01.000Z",
|
|
572
|
+
},
|
|
573
|
+
];
|
|
455
574
|
|
|
456
575
|
render(<AppPackagesPanel onOpenPanelApp={mocks.onOpen} />);
|
|
457
576
|
|
|
458
|
-
expect(screen.getByRole(
|
|
459
|
-
expect(screen.getByText(
|
|
577
|
+
expect(screen.getByRole("status").textContent).toContain("Downloading");
|
|
578
|
+
expect(screen.getByText("nextclaw.workspace-glance")).toBeTruthy();
|
|
460
579
|
|
|
461
|
-
await user.click(screen.getByRole(
|
|
580
|
+
await user.click(screen.getByRole("button", { name: "Add apps" }));
|
|
462
581
|
|
|
463
|
-
expect(screen.getByRole(
|
|
464
|
-
expect(screen.getAllByText(
|
|
465
|
-
expect(
|
|
466
|
-
.
|
|
582
|
+
expect(screen.getByRole("heading", { name: "Add apps" })).toBeTruthy();
|
|
583
|
+
expect(screen.getAllByText("Downloading")).not.toHaveLength(0);
|
|
584
|
+
expect(
|
|
585
|
+
(screen.getByRole("button", { name: "Downloading" }) as HTMLButtonElement)
|
|
586
|
+
.disabled,
|
|
587
|
+
).toBe(true);
|
|
467
588
|
});
|
|
468
589
|
|
|
469
|
-
it(
|
|
590
|
+
it("clears an earlier failure after a newer operation succeeds", () => {
|
|
470
591
|
mocks.operations = [
|
|
471
592
|
{
|
|
472
|
-
id:
|
|
473
|
-
action:
|
|
474
|
-
appId:
|
|
475
|
-
status:
|
|
593
|
+
id: "operation-succeeded",
|
|
594
|
+
action: "update",
|
|
595
|
+
appId: "nextclaw.personal-organizer",
|
|
596
|
+
status: "succeeded",
|
|
476
597
|
completedSteps: 5,
|
|
477
598
|
totalSteps: 5,
|
|
478
|
-
createdAt:
|
|
479
|
-
updatedAt:
|
|
480
|
-
completedAt:
|
|
599
|
+
createdAt: "2026-08-13T11:27:49.090Z",
|
|
600
|
+
updatedAt: "2026-08-13T11:27:50.627Z",
|
|
601
|
+
completedAt: "2026-08-13T11:27:50.627Z",
|
|
481
602
|
},
|
|
482
603
|
{
|
|
483
|
-
id:
|
|
484
|
-
action:
|
|
485
|
-
appId:
|
|
486
|
-
status:
|
|
604
|
+
id: "operation-failed",
|
|
605
|
+
action: "update",
|
|
606
|
+
appId: "nextclaw.personal-organizer",
|
|
607
|
+
status: "failed",
|
|
487
608
|
completedSteps: 1,
|
|
488
609
|
totalSteps: 5,
|
|
489
|
-
createdAt:
|
|
490
|
-
updatedAt:
|
|
491
|
-
completedAt:
|
|
492
|
-
error:
|
|
610
|
+
createdAt: "2026-08-13T11:08:08.080Z",
|
|
611
|
+
updatedAt: "2026-08-13T11:08:10.316Z",
|
|
612
|
+
completedAt: "2026-08-13T11:08:10.316Z",
|
|
613
|
+
error: "name 必须是非空字符串。",
|
|
493
614
|
},
|
|
494
615
|
];
|
|
495
616
|
|
|
496
617
|
render(<AppPackagesPanel onOpenPanelApp={mocks.onOpen} />);
|
|
497
618
|
|
|
498
|
-
expect(screen.queryByRole(
|
|
499
|
-
expect(screen.queryByText(
|
|
619
|
+
expect(screen.queryByRole("alert")).toBeNull();
|
|
620
|
+
expect(screen.queryByText("name 必须是非空字符串。")).toBeNull();
|
|
500
621
|
});
|
|
501
622
|
});
|