@nextclaw/ui 0.24.0-beta.0 → 0.24.0
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 +40 -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 +6 -6
- 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,37 +1,51 @@
|
|
|
1
|
-
import { useEffect } from
|
|
2
|
-
import { useMutation, useQuery, useQueryClient } from
|
|
3
|
-
import type {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
import { useEffect } from "react";
|
|
2
|
+
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
3
|
+
import type {
|
|
4
|
+
AppPackageOperationStatus,
|
|
5
|
+
AppPackageOperationView,
|
|
6
|
+
} from "@nextclaw/client-sdk";
|
|
7
|
+
import { nextclawClient } from "@/shared/lib/api";
|
|
8
|
+
import type { AppPackageOperationSettlementManager } from "@/features/apps/managers/app-package-operation-settlement.manager";
|
|
9
|
+
import { APP_DATA_QUERY_KEY } from "@/features/app-data";
|
|
7
10
|
|
|
8
|
-
const APP_PACKAGES_QUERY_KEY = [
|
|
9
|
-
const PANEL_APPS_QUERY_KEY = [
|
|
10
|
-
const SERVICE_APPS_QUERY_KEY = [
|
|
11
|
-
const SERVICE_ACTIONS_QUERY_KEY = [
|
|
12
|
-
const APP_PACKAGE_OPERATIONS_QUERY_KEY = [
|
|
11
|
+
const APP_PACKAGES_QUERY_KEY = ["app-packages"] as const;
|
|
12
|
+
const PANEL_APPS_QUERY_KEY = ["panel-apps"] as const;
|
|
13
|
+
const SERVICE_APPS_QUERY_KEY = ["service-apps"] as const;
|
|
14
|
+
const SERVICE_ACTIONS_QUERY_KEY = ["service-actions"] as const;
|
|
15
|
+
const APP_PACKAGE_OPERATIONS_QUERY_KEY = ["app-package-operations"] as const;
|
|
13
16
|
|
|
14
17
|
const ACTIVE_OPERATION_STATUSES = new Set<AppPackageOperationStatus>([
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
"queued",
|
|
19
|
+
"resolving",
|
|
20
|
+
"downloading",
|
|
21
|
+
"verifying",
|
|
22
|
+
"installing",
|
|
23
|
+
"finalizing",
|
|
21
24
|
]);
|
|
22
25
|
|
|
23
26
|
export type AppPackageMutationInput =
|
|
24
|
-
| { action:
|
|
25
|
-
| { action:
|
|
26
|
-
| { action:
|
|
27
|
-
| { action:
|
|
28
|
-
| { action:
|
|
29
|
-
| { action:
|
|
27
|
+
| { action: "enable"; appId: string }
|
|
28
|
+
| { action: "disable"; appId: string }
|
|
29
|
+
| { action: "install"; source: string; registryUrl?: string }
|
|
30
|
+
| { action: "update"; appId: string }
|
|
31
|
+
| { action: "rollback"; appId: string; version: string }
|
|
32
|
+
| { action: "uninstall"; appId: string; purgeData?: boolean };
|
|
33
|
+
|
|
34
|
+
export type AppDocumentAccessMutationInput =
|
|
35
|
+
| {
|
|
36
|
+
action: "grant";
|
|
37
|
+
appId: string;
|
|
38
|
+
scopeId: string;
|
|
39
|
+
directoryPath: string;
|
|
40
|
+
mode: "read" | "read-write";
|
|
41
|
+
}
|
|
42
|
+
| { action: "revoke"; appId: string; scopeId: string };
|
|
30
43
|
|
|
31
44
|
export function useAppPackages() {
|
|
32
45
|
return useQuery({
|
|
33
46
|
queryKey: APP_PACKAGES_QUERY_KEY,
|
|
34
|
-
queryFn: () =>
|
|
47
|
+
queryFn: () =>
|
|
48
|
+
nextclawClient.appPackages.list({ includeStorageUsage: false }),
|
|
35
49
|
staleTime: 0,
|
|
36
50
|
});
|
|
37
51
|
}
|
|
@@ -42,13 +56,17 @@ export function useAppPackageOperations() {
|
|
|
42
56
|
queryFn: () => nextclawClient.appPackages.listOperations(),
|
|
43
57
|
refetchInterval: (query) => {
|
|
44
58
|
const entries = query.state.data?.entries ?? [];
|
|
45
|
-
return entries.some((entry) => isAppPackageOperationActive(entry.status))
|
|
59
|
+
return entries.some((entry) => isAppPackageOperationActive(entry.status))
|
|
60
|
+
? 500
|
|
61
|
+
: false;
|
|
46
62
|
},
|
|
47
63
|
staleTime: 0,
|
|
48
64
|
});
|
|
49
65
|
}
|
|
50
66
|
|
|
51
|
-
export function isAppPackageOperationActive(
|
|
67
|
+
export function isAppPackageOperationActive(
|
|
68
|
+
status: AppPackageOperationStatus,
|
|
69
|
+
): boolean {
|
|
52
70
|
return ACTIVE_OPERATION_STATUSES.has(status);
|
|
53
71
|
}
|
|
54
72
|
|
|
@@ -77,7 +95,14 @@ export function useAppPackageOperationSettlement({
|
|
|
77
95
|
refetchPanels,
|
|
78
96
|
refetchAppData,
|
|
79
97
|
});
|
|
80
|
-
}, [
|
|
98
|
+
}, [
|
|
99
|
+
isLoaded,
|
|
100
|
+
operations,
|
|
101
|
+
refetchPackages,
|
|
102
|
+
refetchPanels,
|
|
103
|
+
refetchAppData,
|
|
104
|
+
settlementManager,
|
|
105
|
+
]);
|
|
81
106
|
}
|
|
82
107
|
|
|
83
108
|
export function useAppPackageMutation() {
|
|
@@ -85,20 +110,23 @@ export function useAppPackageMutation() {
|
|
|
85
110
|
return useMutation({
|
|
86
111
|
mutationFn: async (input: AppPackageMutationInput) => {
|
|
87
112
|
switch (input.action) {
|
|
88
|
-
case
|
|
113
|
+
case "enable":
|
|
89
114
|
return await nextclawClient.appPackages.enable(input.appId);
|
|
90
|
-
case
|
|
115
|
+
case "disable":
|
|
91
116
|
return await nextclawClient.appPackages.disable(input.appId);
|
|
92
|
-
case
|
|
117
|
+
case "install":
|
|
93
118
|
return await nextclawClient.appPackages.startInstall({
|
|
94
119
|
source: input.source,
|
|
95
120
|
registryUrl: input.registryUrl,
|
|
96
121
|
});
|
|
97
|
-
case
|
|
122
|
+
case "update":
|
|
98
123
|
return await nextclawClient.appPackages.startUpdate(input.appId);
|
|
99
|
-
case
|
|
100
|
-
return await nextclawClient.appPackages.startRollback(
|
|
101
|
-
|
|
124
|
+
case "rollback":
|
|
125
|
+
return await nextclawClient.appPackages.startRollback(
|
|
126
|
+
input.appId,
|
|
127
|
+
input.version,
|
|
128
|
+
);
|
|
129
|
+
case "uninstall":
|
|
102
130
|
return await nextclawClient.appPackages.startUninstall(
|
|
103
131
|
input.appId,
|
|
104
132
|
input.purgeData ?? false,
|
|
@@ -111,9 +139,37 @@ export function useAppPackageMutation() {
|
|
|
111
139
|
queryClient.invalidateQueries({ queryKey: PANEL_APPS_QUERY_KEY }),
|
|
112
140
|
queryClient.invalidateQueries({ queryKey: SERVICE_APPS_QUERY_KEY }),
|
|
113
141
|
queryClient.invalidateQueries({ queryKey: SERVICE_ACTIONS_QUERY_KEY }),
|
|
114
|
-
queryClient.invalidateQueries({
|
|
142
|
+
queryClient.invalidateQueries({
|
|
143
|
+
queryKey: APP_PACKAGE_OPERATIONS_QUERY_KEY,
|
|
144
|
+
}),
|
|
115
145
|
queryClient.invalidateQueries({ queryKey: APP_DATA_QUERY_KEY }),
|
|
116
146
|
]);
|
|
117
147
|
},
|
|
118
148
|
});
|
|
119
149
|
}
|
|
150
|
+
|
|
151
|
+
export function useAppDocumentAccessMutation() {
|
|
152
|
+
const queryClient = useQueryClient();
|
|
153
|
+
return useMutation({
|
|
154
|
+
mutationFn: async (input: AppDocumentAccessMutationInput) => {
|
|
155
|
+
if (input.action === "revoke") {
|
|
156
|
+
return await nextclawClient.appPackages.revokeDocumentAccess(
|
|
157
|
+
input.appId,
|
|
158
|
+
input.scopeId,
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
return await nextclawClient.appPackages.grantDocumentAccess(input.appId, {
|
|
162
|
+
scopeId: input.scopeId,
|
|
163
|
+
directoryPath: input.directoryPath,
|
|
164
|
+
mode: input.mode,
|
|
165
|
+
});
|
|
166
|
+
},
|
|
167
|
+
onSuccess: async () => {
|
|
168
|
+
await Promise.all([
|
|
169
|
+
queryClient.invalidateQueries({ queryKey: APP_PACKAGES_QUERY_KEY }),
|
|
170
|
+
queryClient.invalidateQueries({ queryKey: SERVICE_APPS_QUERY_KEY }),
|
|
171
|
+
queryClient.invalidateQueries({ queryKey: SERVICE_ACTIONS_QUERY_KEY }),
|
|
172
|
+
]);
|
|
173
|
+
},
|
|
174
|
+
});
|
|
175
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { ChatConversationPanel } from "./components/conversation/chat-conversation-panel";
|
|
2
|
+
export { ChatConversationWorkspaceSection } from "./components/conversation/chat-conversation-workspace-section";
|
|
2
3
|
export { ChatSidebar } from "./components/layout/chat-sidebar";
|
|
3
4
|
export { ChatPresenterProvider } from "./components/providers/chat-presenter.provider";
|
|
4
5
|
export { usePresenter } from "./components/providers/chat-presenter.provider";
|
|
@@ -6,7 +7,10 @@ export { ChatPresenter } from "./presenters/chat.presenter";
|
|
|
6
7
|
export { ChatDraftIntentManager } from "./managers/chat-draft-intent.manager";
|
|
7
8
|
export { ChatComposerIntentManager } from "./managers/chat-composer-intent.manager";
|
|
8
9
|
export { ChatCompletionNotificationManager } from "./managers/chat-completion-notification.manager";
|
|
9
|
-
export {
|
|
10
|
+
export {
|
|
11
|
+
buildSessionPath,
|
|
12
|
+
CHAT_DRAFT_SESSION_PATH,
|
|
13
|
+
} from "./features/session/utils/chat-session-route.utils";
|
|
10
14
|
export { useChatSessionListStore } from "./stores/chat-session-list.store";
|
|
11
15
|
export { useChatThreadStore } from "./stores/chat-thread.store";
|
|
12
16
|
export { useChatMessageLayoutStore } from "./stores/chat-message-layout.store";
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# Projects UI boundary
|
|
2
2
|
|
|
3
|
-
- 本目录拥有 Projects 页面、presenter
|
|
3
|
+
- 本目录拥有 Projects 页面、presenter、查询状态和用户回复编排;项目观测事实来自 `client.projects` 公共快照,工作项来自 Project Work API,组件不得自行扫描文件或解析 Marker。
|
|
4
4
|
- 页面默认只读;确认/拒绝只复用现有 agent-run 消息入口,并携带可追踪、可幂等的响应元数据,不创建第二套 response bridge。
|
|
5
|
-
- Overview
|
|
5
|
+
- Overview、列表和看板消费同一份工作项查询状态;所有工作项入口统一打开右侧详情抽屉,不得在页面内追加平铺详情。
|
|
6
|
+
- `project.work.changed` 只用于失效查询;UI 不重放事件历史,也不以轮询或目录扫描代替业务存储。
|
|
6
7
|
- 组件负责连接与展示,跨组件状态归 hook/presenter;用户文案必须走 Projects i18n 资源。
|
|
@@ -1,150 +1,81 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import type { ProjectObservationSnapshot } from "@nextclaw/client-sdk";
|
|
3
|
-
import { Button } from "@/shared/components/ui/button";
|
|
4
|
-
import { formatDateTime, t } from "@/shared/lib/i18n";
|
|
1
|
+
import { t } from "@/shared/lib/i18n";
|
|
5
2
|
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
} from "./project-section";
|
|
10
|
-
import {
|
|
11
|
-
DEFAULT_PROJECT_ARTIFACT_SORT,
|
|
12
|
-
formatProjectRelativeTime,
|
|
13
|
-
sortProjectArtifacts,
|
|
14
|
-
} from "../utils/project-artifact-view.utils";
|
|
15
|
-
import { ProjectRuns } from "./project-runs";
|
|
3
|
+
useProjectWork,
|
|
4
|
+
useProjectWorkSummary,
|
|
5
|
+
} from "@/features/projects/hooks/use-project-work";
|
|
16
6
|
|
|
17
7
|
export function ProjectOverview({
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
onStartObservationSetup,
|
|
21
|
-
onStartProjectWork,
|
|
22
|
-
onOpenFile,
|
|
23
|
-
shouldOfferObservationSetup,
|
|
8
|
+
projectId,
|
|
9
|
+
onOpenWorkItem,
|
|
24
10
|
}: {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
onStartObservationSetup: () => void;
|
|
28
|
-
onStartProjectWork: () => void;
|
|
29
|
-
onOpenFile: (path: string, label: string) => void;
|
|
30
|
-
shouldOfferObservationSetup: boolean;
|
|
11
|
+
projectId: string;
|
|
12
|
+
onOpenWorkItem: (workItemId: string) => void;
|
|
31
13
|
}) {
|
|
32
|
-
const
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
14
|
+
const summary = useProjectWorkSummary(projectId);
|
|
15
|
+
const work = useProjectWork(projectId);
|
|
16
|
+
if (summary.isLoading || work.isLoading)
|
|
17
|
+
return (
|
|
18
|
+
<div className="rounded-xl border border-border/60 p-5 text-sm text-muted-foreground">
|
|
19
|
+
{t("projectsLoading")}
|
|
20
|
+
</div>
|
|
21
|
+
);
|
|
22
|
+
if (summary.isError || work.isError)
|
|
23
|
+
return (
|
|
24
|
+
<div className="rounded-xl border border-destructive/40 p-5 text-sm text-destructive">
|
|
25
|
+
{t("projectsLoadFailed")}
|
|
26
|
+
</div>
|
|
27
|
+
);
|
|
28
|
+
const recent = work.data!.items.slice(0, 5);
|
|
37
29
|
return (
|
|
38
|
-
<div className="
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
30
|
+
<div className="space-y-5">
|
|
31
|
+
<section
|
|
32
|
+
className="grid gap-3 sm:grid-cols-2 lg:grid-cols-4"
|
|
33
|
+
aria-label={t("projectsWorkSummary")}
|
|
34
|
+
>
|
|
35
|
+
{(
|
|
36
|
+
[
|
|
37
|
+
["projectsWorkTotal", summary.data!.total],
|
|
38
|
+
["projectsWorkActive", summary.data!.active],
|
|
39
|
+
["projectsWorkNeedsAttention", summary.data!.attention],
|
|
40
|
+
["projectsWorkCompleted", summary.data!.completed],
|
|
41
|
+
] as const
|
|
42
|
+
).map(([label, value]) => (
|
|
43
|
+
<div
|
|
44
|
+
key={label}
|
|
45
|
+
className="rounded-xl border border-border/60 bg-card p-4"
|
|
48
46
|
>
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
</ProjectSection>
|
|
52
|
-
) : null}
|
|
53
|
-
{(snapshot.runs?.length ?? 0) > 0 ? (
|
|
54
|
-
<ProjectSection title={t("projectsAiActivity")} className="lg:col-span-2">
|
|
55
|
-
<ProjectRuns limit={5} snapshot={snapshot} />
|
|
56
|
-
</ProjectSection>
|
|
57
|
-
) : null}
|
|
58
|
-
<ProjectSection title={t("projectsAttention")}>
|
|
59
|
-
{openSignals.length ? (
|
|
60
|
-
<div className="space-y-2">
|
|
61
|
-
{openSignals.map((signal) => (
|
|
62
|
-
<div key={signal.id} className="rounded-xl bg-muted/45 p-3 text-sm">
|
|
63
|
-
<p className="font-medium">{signal.message}</p>
|
|
64
|
-
<p className="mt-1 text-xs text-muted-foreground">{signal.reference.label} · {signal.level}</p>
|
|
65
|
-
</div>
|
|
66
|
-
))}
|
|
67
|
-
</div>
|
|
68
|
-
) : <ProjectEmptyState>{t("projectsNoAttention")}</ProjectEmptyState>}
|
|
69
|
-
</ProjectSection>
|
|
70
|
-
<ProjectSection title={t("projectsWork")}>
|
|
71
|
-
{snapshot.workItems.length ? (
|
|
72
|
-
<div className="space-y-2">
|
|
73
|
-
{snapshot.workItems.slice(0, 5).map((item) => (
|
|
74
|
-
<div key={item.id} className="rounded-xl bg-muted/45 p-3 text-sm">
|
|
75
|
-
<p className="font-medium">{item.name}</p>
|
|
76
|
-
<p className="mt-1 text-xs text-muted-foreground">
|
|
77
|
-
{item.reference.label} · {t(`projectsStatus_${item.status}`)}
|
|
78
|
-
</p>
|
|
79
|
-
</div>
|
|
80
|
-
))}
|
|
81
|
-
</div>
|
|
82
|
-
) : (
|
|
83
|
-
<>
|
|
84
|
-
<ProjectEmptyState>
|
|
85
|
-
{t(
|
|
86
|
-
shouldOfferObservationSetup
|
|
87
|
-
? "projectsWorkNeedsSetup"
|
|
88
|
-
: hasProjectSessions
|
|
89
|
-
? "projectsWorkNoReports"
|
|
90
|
-
: "projectsWorkNoSessions",
|
|
91
|
-
)}
|
|
92
|
-
</ProjectEmptyState>
|
|
93
|
-
{!shouldOfferObservationSetup ? (
|
|
94
|
-
<Button size="sm" variant="outline" onClick={onStartProjectWork}>
|
|
95
|
-
{t("projectsStartProjectWork")}
|
|
96
|
-
</Button>
|
|
97
|
-
) : null}
|
|
98
|
-
</>
|
|
99
|
-
)}
|
|
100
|
-
</ProjectSection>
|
|
101
|
-
<ProjectSection title={t("projectsArtifacts")}>
|
|
102
|
-
{recentArtifacts.length ? (
|
|
103
|
-
<div className="space-y-2">
|
|
104
|
-
{recentArtifacts.map((artifact) => (
|
|
105
|
-
<ProjectFilePreviewButton
|
|
106
|
-
key={artifact.id}
|
|
107
|
-
available={artifact.exists}
|
|
108
|
-
path={artifact.path}
|
|
109
|
-
label={artifact.path}
|
|
110
|
-
onOpen={onOpenFile}
|
|
111
|
-
className="block w-full rounded-xl bg-muted/45 p-3 text-left text-sm transition-colors enabled:hover:bg-[var(--interaction-hover)] enabled:focus-visible:outline-none enabled:focus-visible:ring-1 enabled:focus-visible:ring-border"
|
|
112
|
-
>
|
|
113
|
-
<span className="block truncate font-medium">{artifact.path}</span>
|
|
114
|
-
<span className="mt-1 block text-xs text-muted-foreground">
|
|
115
|
-
{artifact.categoryLabel}
|
|
116
|
-
{artifact.fileUpdatedAt ? (
|
|
117
|
-
<time dateTime={artifact.fileUpdatedAt} title={formatDateTime(artifact.fileUpdatedAt)}>
|
|
118
|
-
{` · ${formatProjectRelativeTime(artifact.fileUpdatedAt)}`}
|
|
119
|
-
</time>
|
|
120
|
-
) : null}
|
|
121
|
-
{!artifact.exists ? ` · ${t("projectsUnavailable")}` : ""}
|
|
122
|
-
</span>
|
|
123
|
-
</ProjectFilePreviewButton>
|
|
124
|
-
))}
|
|
125
|
-
</div>
|
|
126
|
-
) : <ProjectEmptyState>{t("projectsNoArtifacts")}</ProjectEmptyState>}
|
|
127
|
-
</ProjectSection>
|
|
128
|
-
<ProjectSection title={t("projectsActivity")}>
|
|
129
|
-
{recentActivity.length ? (
|
|
130
|
-
<div className="space-y-3">
|
|
131
|
-
{recentActivity.map((activity) => (
|
|
132
|
-
<div key={activity.id} className="text-sm">
|
|
133
|
-
<p>{activity.message}</p>
|
|
134
|
-
<p className="mt-1 text-xs text-muted-foreground">
|
|
135
|
-
{activity.reference.sessionId ? (
|
|
136
|
-
<Link className="underline underline-offset-2" to={`/chat/${encodeURIComponent(activity.reference.sessionId)}`}>
|
|
137
|
-
{activity.reference.label}
|
|
138
|
-
</Link>
|
|
139
|
-
) : activity.reference.label} · <time dateTime={activity.at} title={formatDateTime(activity.at)}>
|
|
140
|
-
{formatProjectRelativeTime(activity.at)}
|
|
141
|
-
</time>
|
|
142
|
-
</p>
|
|
143
|
-
</div>
|
|
144
|
-
))}
|
|
47
|
+
<div className="text-2xl font-semibold">{value}</div>
|
|
48
|
+
<div className="mt-1 text-xs text-muted-foreground">{t(label)}</div>
|
|
145
49
|
</div>
|
|
146
|
-
)
|
|
147
|
-
</
|
|
50
|
+
))}
|
|
51
|
+
</section>
|
|
52
|
+
<section>
|
|
53
|
+
<h2 className="mb-3 text-sm font-semibold">
|
|
54
|
+
{t("projectsWorkRecentlyUpdated")}
|
|
55
|
+
</h2>
|
|
56
|
+
<div className="space-y-2">
|
|
57
|
+
{recent.map((item) => (
|
|
58
|
+
<button
|
|
59
|
+
key={item.id}
|
|
60
|
+
type="button"
|
|
61
|
+
className="flex w-full items-center justify-between gap-3 rounded-xl border border-border/60 bg-card px-4 py-3 text-left hover:bg-[var(--interaction-hover)] focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-border"
|
|
62
|
+
onClick={() => onOpenWorkItem(item.id)}
|
|
63
|
+
>
|
|
64
|
+
<span className="min-w-0 truncate text-sm font-medium">
|
|
65
|
+
{item.title}
|
|
66
|
+
</span>
|
|
67
|
+
<span className="shrink-0 text-xs text-muted-foreground">
|
|
68
|
+
{item.state.name}
|
|
69
|
+
</span>
|
|
70
|
+
</button>
|
|
71
|
+
))}
|
|
72
|
+
{!recent.length ? (
|
|
73
|
+
<div className="rounded-xl border border-dashed border-border/60 p-8 text-center text-sm text-muted-foreground">
|
|
74
|
+
{t("projectsWorkEmpty")}
|
|
75
|
+
</div>
|
|
76
|
+
) : null}
|
|
77
|
+
</div>
|
|
78
|
+
</section>
|
|
148
79
|
</div>
|
|
149
80
|
);
|
|
150
81
|
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { fireEvent, render, screen } from "@testing-library/react";
|
|
2
|
+
import { describe, expect, it, vi } from "vitest";
|
|
3
|
+
import { ProjectWorkItemDrawer } from "./project-work-item-drawer";
|
|
4
|
+
|
|
5
|
+
const mocks = vi.hoisted(() => ({
|
|
6
|
+
updateItem: vi.fn(),
|
|
7
|
+
useProjectWorkItem: vi.fn(),
|
|
8
|
+
}));
|
|
9
|
+
|
|
10
|
+
vi.mock("@/features/projects/hooks/use-project-work", () => ({
|
|
11
|
+
useProjectWork: () => ({
|
|
12
|
+
data: { states: [{ id: "planned", name: "Planned" }] },
|
|
13
|
+
}),
|
|
14
|
+
useProjectWorkItem: mocks.useProjectWorkItem,
|
|
15
|
+
useProjectWorkActivity: () => ({ data: { activities: [] } }),
|
|
16
|
+
useProjectWorkActions: () => ({
|
|
17
|
+
updateItem: { mutateAsync: mocks.updateItem, isPending: false },
|
|
18
|
+
deleteItem: { mutateAsync: vi.fn() },
|
|
19
|
+
restoreItem: { mutateAsync: vi.fn() },
|
|
20
|
+
linkArtifact: { mutateAsync: vi.fn(), isPending: false },
|
|
21
|
+
unlinkArtifact: { mutateAsync: vi.fn() },
|
|
22
|
+
}),
|
|
23
|
+
}));
|
|
24
|
+
|
|
25
|
+
describe("ProjectWorkItemDrawer", () => {
|
|
26
|
+
it("renders details in a right-side dialog and opens linked artifacts", () => {
|
|
27
|
+
mocks.useProjectWorkItem.mockReturnValue({
|
|
28
|
+
isLoading: false,
|
|
29
|
+
isError: false,
|
|
30
|
+
data: {
|
|
31
|
+
id: "work-1",
|
|
32
|
+
projectId: "project-1",
|
|
33
|
+
title: "Drawer details",
|
|
34
|
+
description: "Durable details",
|
|
35
|
+
stateId: "planned",
|
|
36
|
+
state: { id: "planned", name: "Planned" },
|
|
37
|
+
attention: "none",
|
|
38
|
+
version: 1,
|
|
39
|
+
deletedAt: null,
|
|
40
|
+
artifacts: [
|
|
41
|
+
{ id: "artifact-1", path: "docs/design.md", label: "Design" },
|
|
42
|
+
],
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
const onOpenArtifact = vi.fn();
|
|
46
|
+
|
|
47
|
+
render(
|
|
48
|
+
<ProjectWorkItemDrawer
|
|
49
|
+
projectId="project-1"
|
|
50
|
+
projectRoot="/tmp/project"
|
|
51
|
+
workItemId="work-1"
|
|
52
|
+
onOpenChange={vi.fn()}
|
|
53
|
+
onOpenArtifact={onOpenArtifact}
|
|
54
|
+
/>,
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
expect(screen.getByRole("dialog")).toBeTruthy();
|
|
58
|
+
expect(screen.getByDisplayValue("Drawer details")).toBeTruthy();
|
|
59
|
+
fireEvent.click(screen.getByRole("button", { name: /Design/ }));
|
|
60
|
+
expect(onOpenArtifact).toHaveBeenCalledWith(
|
|
61
|
+
"/tmp/project/docs/design.md",
|
|
62
|
+
"Design",
|
|
63
|
+
);
|
|
64
|
+
});
|
|
65
|
+
});
|