@nextclaw/ui 0.16.0 → 0.17.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 +21 -0
- package/dist/assets/api-DJ6ZKWFY.js +13 -0
- package/dist/assets/{appearance-settings-page-CfwDf9ag.js → appearance-settings-page-B-juoJ6F.js} +1 -1
- package/dist/assets/{channels-list-page-Dyn83ZlS.js → channels-list-page-mVHYa7xW.js} +1 -1
- package/dist/assets/{chat-page-CL0IMvgF.js → chat-page-DsMYGNee.js} +1 -1
- package/dist/assets/{config-split-page-D4D4HFhq.js → config-split-page-BaqLcRxk.js} +1 -1
- package/dist/assets/{confirm-dialog-Cv6-9Nnj.js → confirm-dialog-DasFoaCT.js} +1 -1
- package/dist/assets/{desktop-update-config-Bckgulvm.js → desktop-update-config-CoEvKrpB.js} +1 -1
- package/dist/assets/{dist-BrRgjiq1.js → dist-C741xUwt.js} +1 -1
- package/dist/assets/{dist-Bh_tj3FN.js → dist-zOKNc7QQ.js} +1 -1
- package/dist/assets/doc-browser-BbirIYbj.js +1 -0
- package/dist/assets/{doc-browser-BlWjhmnM.js → doc-browser-Crqog5cn.js} +1 -1
- package/dist/assets/{doc-browser-context-NGYa66Xg.js → doc-browser-context-BT8NWQyM.js} +1 -1
- package/dist/assets/{form-actions-CeKgh4SQ.js → form-actions-BcukaMyR.js} +1 -1
- package/dist/assets/index-1f4Pv2q3.css +1 -0
- package/dist/assets/index-BYWLjYFQ.js +109 -0
- package/dist/assets/loader-circle-LVHz84Xj.js +1 -0
- package/dist/assets/mcp-marketplace-page-2SsrbaiQ.js +1 -0
- package/dist/assets/{mcp-marketplace-page-DfNmMqKs.js → mcp-marketplace-page-XgYLOAkK.js} +1 -1
- package/dist/assets/{model-config-page-DHzyDV5v.js → model-config-page-DL8ljJK-.js} +1 -1
- package/dist/assets/{navigation-link-CzGgQGi_.js → navigation-link-BoMNAKyw.js} +1 -1
- package/dist/assets/plus-BeGZHC3f.js +1 -0
- package/dist/assets/{provider-scoped-model-input-CjrHwzFb.js → provider-scoped-model-input-CB-vojwC.js} +1 -1
- package/dist/assets/{providers-config-page-ZPvJWHFN.js → providers-config-page-C80nti5H.js} +1 -1
- package/dist/assets/{react-BXRf96P-.js → react-DND3vEAM.js} +1 -1
- package/dist/assets/{refresh-cw-ChKuqgPR.js → refresh-cw-DZ_Y89ld.js} +1 -1
- package/dist/assets/remote-Codkr9SX.js +1 -0
- package/dist/assets/rotate-cw-CP3pQ4R8.js +1 -0
- package/dist/assets/{runtime-config-page-CGpPQD8K.js → runtime-config-page-Um1XDqXJ.js} +1 -1
- package/dist/assets/{save-BWGNAKV5.js → save-BgMfxs5u.js} +1 -1
- package/dist/assets/{search-DFcT_9wJ.js → search-C7ruoV7u.js} +1 -1
- package/dist/assets/{search-config-page-DBBzVAQZ.js → search-config-page-BTjfOZ6q.js} +1 -1
- package/dist/assets/{secrets-config-page-bnJ8sIB7.js → secrets-config-page-BFjiSz73.js} +1 -1
- package/dist/assets/security-config-Dx-wMb4q.js +1 -0
- package/dist/assets/{select-BnNrv8si.js → select-BMX2ooE-.js} +1 -1
- package/dist/assets/{settings-2-CFHrgKTX.js → settings-2-BNQF6hed.js} +1 -1
- package/dist/assets/skeleton-CWZ0IO2D.js +1 -0
- package/dist/assets/{tag-chip-L5w4xfS7.js → tag-chip-BL-UXlVO.js} +1 -1
- package/dist/assets/x-BSu1xwPK.js +1 -0
- package/dist/index.html +19 -19
- package/package.json +4 -4
- package/src/features/app-data/components/app-data-removal-choice.tsx +43 -0
- package/src/features/app-data/components/app-retained-data-section.tsx +179 -0
- package/src/features/app-data/components/app-storage-usage.tsx +59 -0
- package/src/features/app-data/hooks/use-app-data.ts +26 -0
- package/src/features/app-data/index.ts +8 -0
- package/src/features/apps/components/__tests__/app-packages-panel.test.tsx +114 -10
- package/src/features/apps/components/__tests__/apps-panel.test.tsx +18 -2
- package/src/features/apps/components/app-package-card.tsx +98 -53
- package/src/features/apps/components/app-packages-panel.tsx +78 -23
- package/src/features/apps/components/apps-panel.tsx +14 -3
- package/src/features/apps/hooks/use-app-packages.ts +7 -2
- package/src/features/apps/managers/__tests__/app-package-operation-settlement.manager.test.ts +5 -0
- package/src/features/apps/managers/app-package-operation-settlement.manager.ts +8 -2
- package/src/features/service-apps/components/__tests__/service-apps-panel.test.tsx +91 -6
- package/src/features/service-apps/components/service-app-delete-dialog.tsx +122 -0
- package/src/features/service-apps/components/service-app-list-item.tsx +50 -21
- package/src/features/service-apps/components/service-apps-panel.tsx +38 -2
- package/src/features/service-apps/hooks/use-service-apps.ts +4 -1
- package/src/shared/lib/i18n/locales/en-US/doc-browser.json +26 -1
- package/src/shared/lib/i18n/locales/zh-CN/doc-browser.json +26 -1
- package/dist/assets/api-Dket1cmq.js +0 -13
- package/dist/assets/doc-browser-Da9hDsoQ.js +0 -1
- package/dist/assets/index-Bz5yNMXA.js +0 -109
- package/dist/assets/index-DSLK7zgR.css +0 -1
- package/dist/assets/loader-circle-DLp2jNIf.js +0 -1
- package/dist/assets/mcp-marketplace-page-CIWbjXM8.js +0 -1
- package/dist/assets/plus-Dfj3tJ_m.js +0 -1
- package/dist/assets/remote-C2enAZbs.js +0 -1
- package/dist/assets/rotate-cw-BGkZpcc7.js +0 -1
- package/dist/assets/security-config-CfbXSpgX.js +0 -1
- package/dist/assets/skeleton-tfWhGuJT.js +0 -1
- package/dist/assets/x-BCSE3LCQ.js +0 -1
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
import { useMemo, useState, type FormEvent } from 'react';
|
|
2
|
-
import type { AppPackageOperationView, AppPackageView } from '@nextclaw/client-sdk';
|
|
1
|
+
import { useEffect, useMemo, useRef, useState, type FormEvent } from 'react';
|
|
2
|
+
import type { AppDataEntry, AppPackageOperationView, AppPackageView } from '@nextclaw/client-sdk';
|
|
3
3
|
import { LoaderCircle, PackagePlus, RefreshCw, ShieldCheck, Store } from 'lucide-react';
|
|
4
4
|
import { AppArtwork } from '@/features/apps/components/app-artwork';
|
|
5
5
|
import type { PanelAppEntryView } from '@/shared/lib/api';
|
|
6
6
|
import { useAppPresenter } from '@/app/components/app-presenter-provider';
|
|
7
7
|
import { AppMarketplaceDialog } from '@/features/apps/components/app-marketplace-dialog';
|
|
8
8
|
import { AppPackageCard } from '@/features/apps/components/app-package-card';
|
|
9
|
+
import { AppRetainedDataSection, useAppData } from '@/features/app-data';
|
|
9
10
|
import {
|
|
10
11
|
useAppPackageMutation,
|
|
11
12
|
useAppPackageOperationSettlement,
|
|
@@ -33,17 +34,21 @@ import { Skeleton } from '@/shared/components/ui/skeleton';
|
|
|
33
34
|
import { t } from '@/shared/lib/i18n';
|
|
34
35
|
|
|
35
36
|
export function AppPackagesPanel({
|
|
37
|
+
focusedPackageId,
|
|
36
38
|
onOpenPanelApp,
|
|
37
39
|
}: {
|
|
40
|
+
focusedPackageId?: string;
|
|
38
41
|
onOpenPanelApp: (entry: PanelAppEntryView) => void;
|
|
39
42
|
}) {
|
|
40
43
|
const appPackages = useAppPackages();
|
|
44
|
+
const appData = useAppData();
|
|
41
45
|
const appPackageOperations = useAppPackageOperations();
|
|
42
46
|
const panelApps = usePanelApps();
|
|
43
47
|
const lifecycle = useAppPackageMutation();
|
|
44
48
|
const recordOpened = useRecordPanelAppOpened();
|
|
45
49
|
const grantClient = useGrantPanelAppClient();
|
|
46
50
|
const presenter = useAppPresenter();
|
|
51
|
+
const libraryTitleRef = useRef<HTMLHeadingElement>(null);
|
|
47
52
|
const [installOpen, setInstallOpen] = useState(false);
|
|
48
53
|
const [installSource, setInstallSource] = useState('');
|
|
49
54
|
const [marketplaceOpen, setMarketplaceOpen] = useState(false);
|
|
@@ -53,11 +58,19 @@ export function AppPackagesPanel({
|
|
|
53
58
|
[appPackageOperations.data?.entries],
|
|
54
59
|
);
|
|
55
60
|
|
|
61
|
+
useEffect(() => {
|
|
62
|
+
if (!focusedPackageId || appPackages.isLoading) return;
|
|
63
|
+
const target = document.getElementById(`app-package-${focusedPackageId}`);
|
|
64
|
+
target?.scrollIntoView({ block: 'nearest' });
|
|
65
|
+
target?.focus({ preventScroll: true });
|
|
66
|
+
}, [appPackages.data, appPackages.isLoading, focusedPackageId]);
|
|
67
|
+
|
|
56
68
|
useAppPackageOperationSettlement({
|
|
57
69
|
isLoaded: Boolean(appPackageOperations.data),
|
|
58
70
|
operations,
|
|
59
71
|
refetchPackages: appPackages.refetch,
|
|
60
72
|
refetchPanels: panelApps.refetch,
|
|
73
|
+
refetchAppData: appData.refetch,
|
|
61
74
|
settlementManager: presenter.appPackageOperationSettlementManager,
|
|
62
75
|
});
|
|
63
76
|
|
|
@@ -101,6 +114,7 @@ export function AppPackagesPanel({
|
|
|
101
114
|
void appPackages.refetch();
|
|
102
115
|
void appPackageOperations.refetch();
|
|
103
116
|
void panelApps.refetch();
|
|
117
|
+
void appData.refetch();
|
|
104
118
|
};
|
|
105
119
|
|
|
106
120
|
return (
|
|
@@ -144,7 +158,13 @@ export function AppPackagesPanel({
|
|
|
144
158
|
<div className="custom-scrollbar min-h-0 flex-1 overflow-y-auto">
|
|
145
159
|
<div className="mx-auto w-full max-w-3xl p-3 sm:p-5">
|
|
146
160
|
<div className="mb-4 px-1">
|
|
147
|
-
<h1
|
|
161
|
+
<h1
|
|
162
|
+
ref={libraryTitleRef}
|
|
163
|
+
tabIndex={-1}
|
|
164
|
+
className="text-base font-semibold tracking-tight text-foreground"
|
|
165
|
+
>
|
|
166
|
+
{t('appPackagesLibraryTitle')}
|
|
167
|
+
</h1>
|
|
148
168
|
<p className="mt-1 text-xs leading-5 text-muted-foreground">
|
|
149
169
|
{t('appPackagesLibraryDescription')}
|
|
150
170
|
</p>
|
|
@@ -157,7 +177,11 @@ export function AppPackagesPanel({
|
|
|
157
177
|
) : null}
|
|
158
178
|
|
|
159
179
|
<AppPackageLibrary
|
|
180
|
+
appDataEntries={appData.data?.entries ?? []}
|
|
181
|
+
appDataLoading={appData.isLoading}
|
|
182
|
+
appDataUnavailable={appData.isError}
|
|
160
183
|
error={getPackageLoadError(appPackages.error, panelApps.error)}
|
|
184
|
+
focusedPackageId={focusedPackageId}
|
|
161
185
|
isLoading={appPackages.isLoading || panelApps.isLoading}
|
|
162
186
|
isPending={lifecycle.isPending}
|
|
163
187
|
mutationAppId={'appId' in (lifecycle.variables ?? {})
|
|
@@ -170,6 +194,13 @@ export function AppPackagesPanel({
|
|
|
170
194
|
onMutate={runMutation}
|
|
171
195
|
onOpenPanelApp={(entry) => void openPanelApp(entry)}
|
|
172
196
|
/>
|
|
197
|
+
<AppRetainedDataSection
|
|
198
|
+
diagnostics={appData.data?.diagnostics ?? []}
|
|
199
|
+
entries={appData.data?.entries ?? []}
|
|
200
|
+
error={appData.error}
|
|
201
|
+
isLoading={appData.isLoading}
|
|
202
|
+
onDeletionFocusReturn={() => libraryTitleRef.current?.focus()}
|
|
203
|
+
/>
|
|
173
204
|
</div>
|
|
174
205
|
</div>
|
|
175
206
|
|
|
@@ -232,7 +263,11 @@ export function AppPackagesPanel({
|
|
|
232
263
|
}
|
|
233
264
|
|
|
234
265
|
function AppPackageLibrary({
|
|
266
|
+
appDataEntries,
|
|
267
|
+
appDataLoading,
|
|
268
|
+
appDataUnavailable,
|
|
235
269
|
error,
|
|
270
|
+
focusedPackageId,
|
|
236
271
|
isLoading,
|
|
237
272
|
isPending,
|
|
238
273
|
mutationAppId,
|
|
@@ -243,7 +278,11 @@ function AppPackageLibrary({
|
|
|
243
278
|
panelApps,
|
|
244
279
|
operations,
|
|
245
280
|
}: {
|
|
281
|
+
appDataEntries: AppDataEntry[];
|
|
282
|
+
appDataLoading: boolean;
|
|
283
|
+
appDataUnavailable: boolean;
|
|
246
284
|
error?: string;
|
|
285
|
+
focusedPackageId?: string;
|
|
247
286
|
isLoading: boolean;
|
|
248
287
|
isPending: boolean;
|
|
249
288
|
mutationAppId?: string;
|
|
@@ -310,27 +349,43 @@ function AppPackageLibrary({
|
|
|
310
349
|
: undefined;
|
|
311
350
|
|
|
312
351
|
return (
|
|
313
|
-
<
|
|
352
|
+
<section
|
|
314
353
|
key={appPackage.id}
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
354
|
+
id={`app-package-${appPackage.id}`}
|
|
355
|
+
aria-label={appPackage.name}
|
|
356
|
+
tabIndex={-1}
|
|
357
|
+
className={focusedPackageId === appPackage.id
|
|
358
|
+
? 'rounded-xl outline-none ring-2 ring-primary/35 ring-offset-2 ring-offset-background'
|
|
359
|
+
: 'rounded-xl outline-none'}
|
|
360
|
+
>
|
|
361
|
+
<AppPackageCard
|
|
362
|
+
appPackage={appPackage}
|
|
363
|
+
storageUsage={appDataEntries.find((entry) =>
|
|
364
|
+
entry.source === 'package' &&
|
|
365
|
+
entry.lifecycle === 'active' &&
|
|
366
|
+
entry.appId === appPackage.id &&
|
|
367
|
+
entry.instanceId === appPackage.instanceId)?.usage ?? appPackage.storageUsage}
|
|
368
|
+
storageUsageLoading={appDataLoading}
|
|
369
|
+
storageUsageUnavailable={appDataUnavailable}
|
|
370
|
+
panelApps={panelApps.filter((entry) => entry.packageId === appPackage.id)}
|
|
371
|
+
isPending={isPending && mutationAppId === appPackage.id}
|
|
372
|
+
operation={visibleOperation}
|
|
373
|
+
onEnable={() => onMutate({ action: 'enable', appId: appPackage.id })}
|
|
374
|
+
onDisable={() => onMutate({ action: 'disable', appId: appPackage.id })}
|
|
375
|
+
onUpdate={() => onMutate({ action: 'update', appId: appPackage.id })}
|
|
376
|
+
onRollback={(version) => onMutate({
|
|
377
|
+
action: 'rollback',
|
|
378
|
+
appId: appPackage.id,
|
|
379
|
+
version,
|
|
380
|
+
})}
|
|
381
|
+
onUninstall={(purgeData) => onMutate({
|
|
382
|
+
action: 'uninstall',
|
|
383
|
+
appId: appPackage.id,
|
|
384
|
+
purgeData,
|
|
385
|
+
})}
|
|
386
|
+
onOpenPanelApp={onOpenPanelApp}
|
|
387
|
+
/>
|
|
388
|
+
</section>
|
|
334
389
|
);
|
|
335
390
|
})}
|
|
336
391
|
</div>
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
1
2
|
import { AppWindow, Boxes, Server } from 'lucide-react';
|
|
2
3
|
import type { PanelAppEntryView } from '@/shared/lib/api';
|
|
3
4
|
import { AppPackagesPanel } from '@/features/apps/components/app-packages-panel';
|
|
@@ -17,10 +18,20 @@ export function AppsPanel({
|
|
|
17
18
|
onActiveTabChange: (tab: AppsPanelTab) => void;
|
|
18
19
|
onOpenPanelApp: (entry: PanelAppEntryView) => void;
|
|
19
20
|
}) {
|
|
21
|
+
const [focusedPackageId, setFocusedPackageId] = useState<string>();
|
|
22
|
+
const changeTab = (tab: AppsPanelTab) => {
|
|
23
|
+
setFocusedPackageId(undefined);
|
|
24
|
+
onActiveTabChange(tab);
|
|
25
|
+
};
|
|
26
|
+
const managePackage = (packageId: string) => {
|
|
27
|
+
setFocusedPackageId(packageId);
|
|
28
|
+
onActiveTabChange('apps');
|
|
29
|
+
};
|
|
30
|
+
|
|
20
31
|
return (
|
|
21
32
|
<div className="flex h-full min-h-0 flex-col bg-card text-card-foreground">
|
|
22
33
|
<nav className="shrink-0 border-b border-border/70 px-4 py-3" aria-label={t('appsTitle')}>
|
|
23
|
-
<Tabs value={activeTab} onValueChange={(value) =>
|
|
34
|
+
<Tabs value={activeTab} onValueChange={(value) => changeTab(value as AppsPanelTab)}>
|
|
24
35
|
<TabsList className="grid h-auto w-full max-w-[390px] grid-cols-3 rounded-lg bg-muted/70 p-0.5">
|
|
25
36
|
<TabsTrigger value="apps" className="min-w-0 gap-1.5 px-2 py-1.5 text-xs">
|
|
26
37
|
<Boxes className="h-3.5 w-3.5 shrink-0" />
|
|
@@ -39,11 +50,11 @@ export function AppsPanel({
|
|
|
39
50
|
</nav>
|
|
40
51
|
<div className="min-h-0 flex-1">
|
|
41
52
|
{activeTab === 'apps' ? (
|
|
42
|
-
<AppPackagesPanel onOpenPanelApp={onOpenPanelApp} />
|
|
53
|
+
<AppPackagesPanel focusedPackageId={focusedPackageId} onOpenPanelApp={onOpenPanelApp} />
|
|
43
54
|
) : activeTab === 'panel-apps' ? (
|
|
44
55
|
<PanelAppsList onOpenPanelApp={onOpenPanelApp} />
|
|
45
56
|
) : (
|
|
46
|
-
<ServiceAppsPanel />
|
|
57
|
+
<ServiceAppsPanel onManagePackage={managePackage} />
|
|
47
58
|
)}
|
|
48
59
|
</div>
|
|
49
60
|
</div>
|
|
@@ -3,6 +3,7 @@ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
|
|
3
3
|
import type { AppPackageOperationStatus, AppPackageOperationView } from '@nextclaw/client-sdk';
|
|
4
4
|
import { nextclawClient } from '@/shared/lib/api';
|
|
5
5
|
import type { AppPackageOperationSettlementManager } from '@/features/apps/managers/app-package-operation-settlement.manager';
|
|
6
|
+
import { APP_DATA_QUERY_KEY } from '@/features/app-data';
|
|
6
7
|
|
|
7
8
|
const APP_PACKAGES_QUERY_KEY = ['app-packages'] as const;
|
|
8
9
|
const PANEL_APPS_QUERY_KEY = ['panel-apps'] as const;
|
|
@@ -30,7 +31,7 @@ export type AppPackageMutationInput =
|
|
|
30
31
|
export function useAppPackages() {
|
|
31
32
|
return useQuery({
|
|
32
33
|
queryKey: APP_PACKAGES_QUERY_KEY,
|
|
33
|
-
queryFn: () => nextclawClient.appPackages.list(),
|
|
34
|
+
queryFn: () => nextclawClient.appPackages.list({ includeStorageUsage: false }),
|
|
34
35
|
staleTime: 0,
|
|
35
36
|
});
|
|
36
37
|
}
|
|
@@ -56,6 +57,7 @@ export function useAppPackageOperationSettlement({
|
|
|
56
57
|
operations,
|
|
57
58
|
refetchPackages,
|
|
58
59
|
refetchPanels,
|
|
60
|
+
refetchAppData,
|
|
59
61
|
settlementManager,
|
|
60
62
|
}: {
|
|
61
63
|
isLoaded: boolean;
|
|
@@ -64,6 +66,7 @@ export function useAppPackageOperationSettlement({
|
|
|
64
66
|
refetchPanels: () => Promise<{
|
|
65
67
|
data?: { entries: Array<{ id: string; packageId?: string }> };
|
|
66
68
|
}>;
|
|
69
|
+
refetchAppData: () => Promise<unknown>;
|
|
67
70
|
settlementManager: AppPackageOperationSettlementManager;
|
|
68
71
|
}) {
|
|
69
72
|
useEffect(() => {
|
|
@@ -72,8 +75,9 @@ export function useAppPackageOperationSettlement({
|
|
|
72
75
|
operations,
|
|
73
76
|
refetchPackages,
|
|
74
77
|
refetchPanels,
|
|
78
|
+
refetchAppData,
|
|
75
79
|
});
|
|
76
|
-
}, [isLoaded, operations, refetchPackages, refetchPanels, settlementManager]);
|
|
80
|
+
}, [isLoaded, operations, refetchPackages, refetchPanels, refetchAppData, settlementManager]);
|
|
77
81
|
}
|
|
78
82
|
|
|
79
83
|
export function useAppPackageMutation() {
|
|
@@ -108,6 +112,7 @@ export function useAppPackageMutation() {
|
|
|
108
112
|
queryClient.invalidateQueries({ queryKey: SERVICE_APPS_QUERY_KEY }),
|
|
109
113
|
queryClient.invalidateQueries({ queryKey: SERVICE_ACTIONS_QUERY_KEY }),
|
|
110
114
|
queryClient.invalidateQueries({ queryKey: APP_PACKAGE_OPERATIONS_QUERY_KEY }),
|
|
115
|
+
queryClient.invalidateQueries({ queryKey: APP_DATA_QUERY_KEY }),
|
|
111
116
|
]);
|
|
112
117
|
},
|
|
113
118
|
});
|
package/src/features/apps/managers/__tests__/app-package-operation-settlement.manager.test.ts
CHANGED
|
@@ -36,6 +36,7 @@ describe('AppPackageOperationSettlementManager', () => {
|
|
|
36
36
|
it('ignores historical terminal operations and settles each new operation once', async () => {
|
|
37
37
|
const reloadByDedupeKeys = vi.fn();
|
|
38
38
|
const refetchPackages = vi.fn(async () => undefined);
|
|
39
|
+
const refetchAppData = vi.fn(async () => undefined);
|
|
39
40
|
const refetchPanels = vi.fn(async () => ({
|
|
40
41
|
data: {
|
|
41
42
|
entries: [
|
|
@@ -54,6 +55,7 @@ describe('AppPackageOperationSettlementManager', () => {
|
|
|
54
55
|
operations: [historical],
|
|
55
56
|
refetchPackages,
|
|
56
57
|
refetchPanels,
|
|
58
|
+
refetchAppData,
|
|
57
59
|
});
|
|
58
60
|
expect(mocks.success).not.toHaveBeenCalled();
|
|
59
61
|
|
|
@@ -63,17 +65,20 @@ describe('AppPackageOperationSettlementManager', () => {
|
|
|
63
65
|
operations: [completed, historical],
|
|
64
66
|
refetchPackages,
|
|
65
67
|
refetchPanels,
|
|
68
|
+
refetchAppData,
|
|
66
69
|
});
|
|
67
70
|
manager.settle({
|
|
68
71
|
isLoaded: true,
|
|
69
72
|
operations: [completed, historical],
|
|
70
73
|
refetchPackages,
|
|
71
74
|
refetchPanels,
|
|
75
|
+
refetchAppData,
|
|
72
76
|
});
|
|
73
77
|
|
|
74
78
|
await vi.waitFor(() => {
|
|
75
79
|
expect(refetchPackages).toHaveBeenCalledTimes(1);
|
|
76
80
|
expect(refetchPanels).toHaveBeenCalledTimes(1);
|
|
81
|
+
expect(refetchAppData).toHaveBeenCalledTimes(1);
|
|
77
82
|
expect(reloadByDedupeKeys).toHaveBeenCalledWith(['panel-app:todos']);
|
|
78
83
|
});
|
|
79
84
|
expect(mocks.success).toHaveBeenCalledTimes(1);
|
|
@@ -19,11 +19,13 @@ export class AppPackageOperationSettlementManager {
|
|
|
19
19
|
operations,
|
|
20
20
|
refetchPackages,
|
|
21
21
|
refetchPanels,
|
|
22
|
+
refetchAppData,
|
|
22
23
|
}: {
|
|
23
24
|
isLoaded: boolean;
|
|
24
25
|
operations: AppPackageOperationView[];
|
|
25
26
|
refetchPackages: () => Promise<unknown>;
|
|
26
27
|
refetchPanels: RefetchPanels;
|
|
28
|
+
refetchAppData: () => Promise<unknown>;
|
|
27
29
|
}): void => {
|
|
28
30
|
if (!isLoaded) return;
|
|
29
31
|
const terminal = operations.filter(
|
|
@@ -37,7 +39,7 @@ export class AppPackageOperationSettlementManager {
|
|
|
37
39
|
terminal.forEach((operation) => {
|
|
38
40
|
if (this.knownTerminalOperationIds.has(operation.id)) return;
|
|
39
41
|
this.knownTerminalOperationIds.add(operation.id);
|
|
40
|
-
this.settleOperation(operation, refetchPackages, refetchPanels);
|
|
42
|
+
this.settleOperation(operation, refetchPackages, refetchPanels, refetchAppData);
|
|
41
43
|
});
|
|
42
44
|
};
|
|
43
45
|
|
|
@@ -45,10 +47,13 @@ export class AppPackageOperationSettlementManager {
|
|
|
45
47
|
operation: AppPackageOperationView,
|
|
46
48
|
refetchPackages: () => Promise<unknown>,
|
|
47
49
|
refetchPanels: RefetchPanels,
|
|
50
|
+
refetchAppData: () => Promise<unknown>,
|
|
48
51
|
): void => {
|
|
49
52
|
if (operation.status === 'succeeded') {
|
|
50
53
|
toast.success(operationSuccessLabel(operation));
|
|
51
|
-
void
|
|
54
|
+
void refetchPackages();
|
|
55
|
+
void refetchAppData();
|
|
56
|
+
void refetchPanels().then((panelsResult) => {
|
|
52
57
|
if (operation.action !== 'rollback' && operation.action !== 'update') return;
|
|
53
58
|
this.docBrowserManager.reloadByDedupeKeys(
|
|
54
59
|
panelsResult.data?.entries
|
|
@@ -61,6 +66,7 @@ export class AppPackageOperationSettlementManager {
|
|
|
61
66
|
if (operation.status === 'failed' || operation.status === 'interrupted') {
|
|
62
67
|
void refetchPackages();
|
|
63
68
|
void refetchPanels();
|
|
69
|
+
void refetchAppData();
|
|
64
70
|
toast.error(operation.error ?? t('appPackagesActionFailed'));
|
|
65
71
|
}
|
|
66
72
|
};
|
|
@@ -8,13 +8,59 @@ const discoverServiceAppActionsMutateAsync = vi.fn(async () => ({ actions: [] })
|
|
|
8
8
|
const refetchServiceActions = vi.fn();
|
|
9
9
|
const refetchServiceActionGrants = vi.fn();
|
|
10
10
|
const refetchServiceApps = vi.fn();
|
|
11
|
+
const refetchAppData = vi.fn();
|
|
11
12
|
const restartServiceAppMutate = vi.fn();
|
|
12
13
|
const revokeServiceActionGrantMutate = vi.fn();
|
|
14
|
+
const resetDeleteServiceApp = vi.fn();
|
|
15
|
+
|
|
16
|
+
vi.mock('@/features/app-data/hooks/use-app-data', () => ({
|
|
17
|
+
useAppData: () => ({
|
|
18
|
+
data: {
|
|
19
|
+
entries: [{
|
|
20
|
+
id: 'ad1.workspace-notes',
|
|
21
|
+
appId: 'notes',
|
|
22
|
+
instanceId: 'default',
|
|
23
|
+
displayName: 'Notes',
|
|
24
|
+
source: 'workspace-service',
|
|
25
|
+
lifecycle: 'active',
|
|
26
|
+
storage: {
|
|
27
|
+
layout: 'instance-v1',
|
|
28
|
+
layoutVersion: 1,
|
|
29
|
+
instanceId: 'default',
|
|
30
|
+
instanceDirectory: '/workspace/.nextclaw/app-instances/notes/default',
|
|
31
|
+
dataDirectory: '/workspace/.nextclaw/app-instances/notes/default/data',
|
|
32
|
+
configDirectory: '/workspace/.nextclaw/app-instances/notes/default/config',
|
|
33
|
+
stateDirectory: '/workspace/.nextclaw/app-instances/notes/default/state',
|
|
34
|
+
cacheDirectory: '/workspace/.nextclaw/app-instances/notes/default/cache',
|
|
35
|
+
temporaryDirectory: '/workspace/.nextclaw/app-instances/notes/default/tmp',
|
|
36
|
+
logsDirectory: '/workspace/.nextclaw/app-instances/notes/default/logs',
|
|
37
|
+
},
|
|
38
|
+
usage: {
|
|
39
|
+
dataBytes: 12,
|
|
40
|
+
configBytes: 0,
|
|
41
|
+
stateBytes: 0,
|
|
42
|
+
cacheBytes: 0,
|
|
43
|
+
temporaryBytes: 0,
|
|
44
|
+
logsBytes: 0,
|
|
45
|
+
totalBytes: 12,
|
|
46
|
+
},
|
|
47
|
+
createdAt: '2026-08-14T00:00:00.000Z',
|
|
48
|
+
actions: { deleteRetainedData: false },
|
|
49
|
+
}],
|
|
50
|
+
diagnostics: [],
|
|
51
|
+
},
|
|
52
|
+
error: null,
|
|
53
|
+
isLoading: false,
|
|
54
|
+
refetch: refetchAppData,
|
|
55
|
+
}),
|
|
56
|
+
}));
|
|
13
57
|
|
|
14
58
|
vi.mock('@/features/service-apps/hooks/use-service-apps', () => ({
|
|
15
59
|
useDeleteServiceApp: () => ({
|
|
60
|
+
error: null,
|
|
16
61
|
isPending: false,
|
|
17
62
|
mutate: deleteServiceAppMutate,
|
|
63
|
+
reset: resetDeleteServiceApp,
|
|
18
64
|
}),
|
|
19
65
|
useDiscoverServiceAppActions: () => ({
|
|
20
66
|
isPending: false,
|
|
@@ -49,9 +95,24 @@ vi.mock('@/features/service-apps/hooks/use-service-apps', () => ({
|
|
|
49
95
|
id: 'notes',
|
|
50
96
|
manifestPath: '/workspace/service-apps/notes/service-app.json',
|
|
51
97
|
protocol: 'mcp',
|
|
98
|
+
sourceKind: 'workspace',
|
|
52
99
|
status: 'idle',
|
|
53
100
|
title: 'Notes',
|
|
101
|
+
}, {
|
|
102
|
+
args: ['server.mjs'],
|
|
103
|
+
command: 'node',
|
|
104
|
+
cwd: '/apps/personal-organizer/service',
|
|
105
|
+
dirPath: '/apps/personal-organizer/service',
|
|
106
|
+
enabled: true,
|
|
107
|
+
id: 'organizer-service',
|
|
108
|
+
manifestPath: '/apps/personal-organizer/service/service-app.json',
|
|
109
|
+
packageId: 'nextclaw.personal-organizer',
|
|
110
|
+
protocol: 'mcp',
|
|
111
|
+
sourceKind: 'package',
|
|
112
|
+
status: 'idle',
|
|
113
|
+
title: 'Organizer Service',
|
|
54
114
|
}],
|
|
115
|
+
diagnostics: [],
|
|
55
116
|
serviceAppsPath: '/workspace/service-apps',
|
|
56
117
|
workspacePath: '/workspace',
|
|
57
118
|
},
|
|
@@ -65,11 +126,13 @@ describe('ServiceAppsPanel', () => {
|
|
|
65
126
|
it('keeps service actions progressive and deletes through a confirm dialog', async () => {
|
|
66
127
|
const user = userEvent.setup();
|
|
67
128
|
|
|
68
|
-
render(<ServiceAppsPanel />);
|
|
129
|
+
render(<ServiceAppsPanel onManagePackage={vi.fn()} />);
|
|
69
130
|
|
|
70
|
-
expect(screen.
|
|
131
|
+
expect(screen.getAllByText('Not connected')).toHaveLength(2);
|
|
71
132
|
expect(screen.queryByText('idle')).toBeNull();
|
|
72
|
-
const discoverButton = screen.
|
|
133
|
+
const discoverButton = screen.getAllByRole('button', {
|
|
134
|
+
name: 'Connect and discover actions',
|
|
135
|
+
})[0]!;
|
|
73
136
|
expect(discoverButton).toBeTruthy();
|
|
74
137
|
await user.hover(discoverButton);
|
|
75
138
|
await waitFor(() => {
|
|
@@ -79,11 +142,33 @@ describe('ServiceAppsPanel', () => {
|
|
|
79
142
|
await user.click(discoverButton);
|
|
80
143
|
expect(discoverServiceAppActionsMutateAsync).toHaveBeenCalledWith('notes');
|
|
81
144
|
|
|
82
|
-
fireEvent.click(screen.
|
|
145
|
+
fireEvent.click(screen.getAllByRole('button', { name: 'More actions' })[0]!);
|
|
83
146
|
expect((screen.getByRole('button', { name: 'Disconnect runtime' }) as HTMLButtonElement).disabled).toBe(true);
|
|
84
147
|
fireEvent.click(screen.getByRole('button', { name: 'Delete service app' }));
|
|
85
|
-
|
|
148
|
+
await waitFor(() => {
|
|
149
|
+
expect(document.activeElement).toBe(screen.getByRole('button', { name: 'Cancel' }));
|
|
150
|
+
});
|
|
151
|
+
expect(resetDeleteServiceApp).toHaveBeenCalled();
|
|
152
|
+
expect(screen.getByText('Total app data')).toBeTruthy();
|
|
153
|
+
expect(screen.getByRole('button', { name: /Keep personal data/ }).getAttribute('aria-pressed'))
|
|
154
|
+
.toBe('true');
|
|
155
|
+
const deleteButtons = screen.getAllByRole('button', { name: 'Delete service app' });
|
|
156
|
+
fireEvent.click(deleteButtons.at(-1)!);
|
|
157
|
+
|
|
158
|
+
expect(deleteServiceAppMutate).toHaveBeenCalledWith(
|
|
159
|
+
{ appId: 'notes', purgeData: false },
|
|
160
|
+
{ onSuccess: expect.any(Function) },
|
|
161
|
+
);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it('routes package-managed services back to Apps without offering standalone deletion', () => {
|
|
165
|
+
const onManagePackage = vi.fn();
|
|
166
|
+
render(<ServiceAppsPanel onManagePackage={onManagePackage} />);
|
|
167
|
+
|
|
168
|
+
fireEvent.click(screen.getAllByRole('button', { name: 'More actions' })[1]!);
|
|
169
|
+
expect(screen.queryByRole('button', { name: 'Delete service app' })).toBeNull();
|
|
170
|
+
fireEvent.click(screen.getByRole('button', { name: 'Manage in Apps' }));
|
|
86
171
|
|
|
87
|
-
expect(
|
|
172
|
+
expect(onManagePackage).toHaveBeenCalledWith('nextclaw.personal-organizer');
|
|
88
173
|
});
|
|
89
174
|
});
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { useEffect, useRef } from "react";
|
|
2
|
+
import type { AppDataEntry } from "@nextclaw/client-sdk";
|
|
3
|
+
import { LoaderCircle } from "lucide-react";
|
|
4
|
+
import { AppDataRemovalChoice, AppStorageUsageDetails } from "@/features/app-data";
|
|
5
|
+
import { Button } from "@/shared/components/ui/button";
|
|
6
|
+
import {
|
|
7
|
+
Dialog,
|
|
8
|
+
DialogContent,
|
|
9
|
+
DialogDescription,
|
|
10
|
+
DialogFooter,
|
|
11
|
+
DialogHeader,
|
|
12
|
+
DialogTitle,
|
|
13
|
+
} from "@/shared/components/ui/dialog";
|
|
14
|
+
import { t } from "@/shared/lib/i18n";
|
|
15
|
+
|
|
16
|
+
export function ServiceAppDeleteDialog({
|
|
17
|
+
appId,
|
|
18
|
+
appTitle,
|
|
19
|
+
dataEntry,
|
|
20
|
+
dataLoading,
|
|
21
|
+
deleteError,
|
|
22
|
+
deletePending,
|
|
23
|
+
open,
|
|
24
|
+
purgeData,
|
|
25
|
+
onDelete,
|
|
26
|
+
onOpenChange,
|
|
27
|
+
onPurgeDataChange,
|
|
28
|
+
}: {
|
|
29
|
+
appId: string;
|
|
30
|
+
appTitle: string;
|
|
31
|
+
dataEntry?: AppDataEntry;
|
|
32
|
+
dataLoading: boolean;
|
|
33
|
+
deleteError: unknown;
|
|
34
|
+
deletePending: boolean;
|
|
35
|
+
open: boolean;
|
|
36
|
+
purgeData: boolean;
|
|
37
|
+
onDelete: (appId: string, purgeData: boolean, onSuccess: () => void) => void;
|
|
38
|
+
onOpenChange: (open: boolean) => void;
|
|
39
|
+
onPurgeDataChange: (purgeData: boolean) => void;
|
|
40
|
+
}) {
|
|
41
|
+
const errorRef = useRef<HTMLParagraphElement>(null);
|
|
42
|
+
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
if (deleteError) errorRef.current?.focus();
|
|
45
|
+
}, [deleteError]);
|
|
46
|
+
|
|
47
|
+
const close = () => onOpenChange(false);
|
|
48
|
+
return (
|
|
49
|
+
<Dialog
|
|
50
|
+
open={open}
|
|
51
|
+
onOpenChange={(nextOpen) => !deletePending && onOpenChange(nextOpen)}
|
|
52
|
+
>
|
|
53
|
+
<DialogContent
|
|
54
|
+
className="max-w-md [&>:last-child]:hidden"
|
|
55
|
+
onEscapeKeyDown={(event) => deletePending && event.preventDefault()}
|
|
56
|
+
onInteractOutside={(event) => deletePending && event.preventDefault()}
|
|
57
|
+
>
|
|
58
|
+
<DialogHeader>
|
|
59
|
+
<DialogTitle>{t("serviceAppsDeleteConfirmTitle")}</DialogTitle>
|
|
60
|
+
<DialogDescription>
|
|
61
|
+
{t("serviceAppsDeleteConfirmDescription")} {appTitle}
|
|
62
|
+
</DialogDescription>
|
|
63
|
+
</DialogHeader>
|
|
64
|
+
<div className="mt-4 grid gap-2">
|
|
65
|
+
<AppDataRemovalChoice
|
|
66
|
+
checked={!purgeData}
|
|
67
|
+
description={t("serviceAppsKeepDataDescription")}
|
|
68
|
+
label={t("appPackagesKeepData")}
|
|
69
|
+
onClick={() => onPurgeDataChange(false)}
|
|
70
|
+
/>
|
|
71
|
+
<AppDataRemovalChoice
|
|
72
|
+
checked={purgeData}
|
|
73
|
+
destructive
|
|
74
|
+
disabled={!dataEntry || dataLoading}
|
|
75
|
+
description={t("serviceAppsDeleteDataDescription")}
|
|
76
|
+
label={t("appPackagesDeleteData")}
|
|
77
|
+
onClick={() => onPurgeDataChange(true)}
|
|
78
|
+
/>
|
|
79
|
+
</div>
|
|
80
|
+
{dataEntry ? (
|
|
81
|
+
<div className="mt-3">
|
|
82
|
+
<AppStorageUsageDetails storage={dataEntry.storage} usage={dataEntry.usage} />
|
|
83
|
+
</div>
|
|
84
|
+
) : (
|
|
85
|
+
<p role="status" className="mt-3 text-xs text-muted-foreground">
|
|
86
|
+
{dataLoading ? t("appPackagesLoading") : t("serviceAppsDataUnavailable")}
|
|
87
|
+
</p>
|
|
88
|
+
)}
|
|
89
|
+
{deleteError ? (
|
|
90
|
+
<p
|
|
91
|
+
ref={errorRef}
|
|
92
|
+
role="alert"
|
|
93
|
+
tabIndex={-1}
|
|
94
|
+
className="mt-3 text-xs text-destructive"
|
|
95
|
+
>
|
|
96
|
+
{deleteError instanceof Error ? deleteError.message : t("appPackagesActionFailed")}
|
|
97
|
+
</p>
|
|
98
|
+
) : null}
|
|
99
|
+
<DialogFooter className="mt-5 gap-2 sm:gap-0">
|
|
100
|
+
<Button
|
|
101
|
+
autoFocus
|
|
102
|
+
type="button"
|
|
103
|
+
variant="outline"
|
|
104
|
+
disabled={deletePending}
|
|
105
|
+
onClick={close}
|
|
106
|
+
>
|
|
107
|
+
{t("cancel")}
|
|
108
|
+
</Button>
|
|
109
|
+
<Button
|
|
110
|
+
type="button"
|
|
111
|
+
variant={purgeData ? "destructive" : "default"}
|
|
112
|
+
disabled={deletePending || (purgeData && !dataEntry)}
|
|
113
|
+
onClick={() => onDelete(appId, purgeData, close)}
|
|
114
|
+
>
|
|
115
|
+
{deletePending ? <LoaderCircle className="mr-1.5 h-3.5 w-3.5 animate-spin" /> : null}
|
|
116
|
+
{purgeData ? t("appDataDeletePermanently") : t("serviceAppsDelete")}
|
|
117
|
+
</Button>
|
|
118
|
+
</DialogFooter>
|
|
119
|
+
</DialogContent>
|
|
120
|
+
</Dialog>
|
|
121
|
+
);
|
|
122
|
+
}
|