@nextclaw/ui 0.24.2 → 0.24.3
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 +18 -0
- package/dist/.vite/manifest.json +517 -517
- package/dist/assets/{account-panel-BxMlLXYH.js → account-panel-BRTSwNvA.js} +1 -1
- package/dist/assets/{api-Utt-xzrT.js → api-BnUzdltV.js} +5 -5
- package/dist/assets/{app-data-DYXDL6X2.js → app-data-Bjh_zM45.js} +1 -1
- package/dist/assets/app-presenter-provider-CBt-lyr_.js +109 -0
- package/dist/assets/{appearance-settings-page-BfYznAK5.js → appearance-settings-page-BrOYSgG0.js} +1 -1
- package/dist/assets/{book-open-DDW_3XpM.js → book-open-xqEj1OLE.js} +1 -1
- package/dist/assets/{boxes-DwJeuK_h.js → boxes-D6uPnkGo.js} +1 -1
- package/dist/assets/{card-IevY2nm-.js → card-CS8su2Zh.js} +1 -1
- package/dist/assets/{channels-list-page-DitnqbMR.js → channels-list-page-BE0TxB3B.js} +1 -1
- package/dist/assets/{chevron-down-An7Km-t4.js → chevron-down-BL-yMYcX.js} +1 -1
- package/dist/assets/{circle-alert-CDXqELuo.js → circle-alert-lSa3LRvS.js} +1 -1
- package/dist/assets/{circle-dashed-CapXEpiV.js → circle-dashed-CU3yApvP.js} +1 -1
- package/dist/assets/{client-B0krol7A.js → client-BtGBwLTs.js} +1 -1
- package/dist/assets/{config-split-page-BtnkqWVp.js → config-split-page-BxDwwAGa.js} +1 -1
- package/dist/assets/{confirm-dialog-DFt4jpN7.js → confirm-dialog-S5JxZcS1.js} +1 -1
- package/dist/assets/{createLucideIcon-BTHK0Ot9.js → createLucideIcon-BNKf0_kc.js} +1 -1
- package/dist/assets/{desktop-capabilities-page-CCf14WId.js → desktop-capabilities-page-BwHxJpbl.js} +1 -1
- package/dist/assets/{desktop-update-config-DVoVgfFi.js → desktop-update-config-Bfg7OdJ2.js} +1 -1
- package/dist/assets/{dialog-BFN_1DBH.js → dialog-Xe73hpQz.js} +1 -1
- package/dist/assets/{dist-Bc9hOVNN.js → dist-BiIcnf9j.js} +1 -1
- package/dist/assets/{dist-CEhIDAnr.js → dist-C5I0v8Qn.js} +1 -1
- package/dist/assets/{dist-hkVPP1th.js → dist-czPa8xFO.js} +1 -1
- package/dist/assets/doc-browser-BgRP0Wni.js +1 -0
- package/dist/assets/{doc-browser-BNUxmwD6.js → doc-browser-C8eqJLi_.js} +1 -1
- package/dist/assets/{doc-browser-context-C4yQrKrH.js → doc-browser-context-BPCDFcC-.js} +1 -1
- package/dist/assets/{ellipsis-BRI6ROeu.js → ellipsis-C5YXdOUx.js} +1 -1
- package/dist/assets/{es2015-MOCbM0Eu.js → es2015-cuwbYI-l.js} +1 -1
- package/dist/assets/{extensions-DxUBMtHW.js → extensions-DgKQRebe.js} +1 -1
- package/dist/assets/{eye-cnynUDGt.js → eye-C84inPcV.js} +1 -1
- package/dist/assets/{form-actions-gQ0JEUwB.js → form-actions-AIdXOoCL.js} +1 -1
- package/dist/assets/{i18n-B9yXA8KO.js → i18n-CKvm9zyk.js} +2 -2
- package/dist/assets/{i18n-provider-BYBJua91.js → i18n-provider-CPV43pMX.js} +1 -1
- package/dist/assets/loader-circle-K1BDdHR5.js +1 -0
- package/dist/assets/login-page-ClcrvN04.js +1 -0
- package/dist/assets/{login-page-CqrHhzaN.js → login-page-DeDQjVxv.js} +1 -1
- package/dist/assets/{logo-badge-Cr03YceJ.js → logo-badge-C-5-zim6.js} +1 -1
- package/dist/assets/main-DGqlaAaI.css +1 -0
- package/dist/assets/main-i18n-catalog.config-CpNF5aVc.js +1 -0
- package/dist/assets/main-nMVfHb5K.js +2 -0
- package/dist/assets/{mcp-marketplace-page-CifiVgFD.js → mcp-marketplace-page-CWjJGp_D.js} +1 -1
- package/dist/assets/mcp-marketplace-page-fvu2NcXT.js +1 -0
- package/dist/assets/{model-config-page-C6jVNbSj.js → model-config-page-Dv_zE7HN.js} +1 -1
- package/dist/assets/{navigation-link-kbCLRSr_.js → navigation-link-BAQjgx2F.js} +1 -1
- package/dist/assets/panel-app-i18n-catalog.config-BzxE3tZb.js +1 -0
- package/dist/assets/{panelStandalone-DMBnYyTw.js → panelStandalone-CFRBHV94.js} +2 -2
- package/dist/assets/plus-LA8-jpxP.js +1 -0
- package/dist/assets/{popover-XVDgjxcl.js → popover-NkrFlWHp.js} +1 -1
- package/dist/assets/{privacy-settings-page-BN27MBVA.js → privacy-settings-page-D6uc9f-A.js} +1 -1
- package/dist/assets/projects-BD1rG75R.js +1 -0
- package/dist/assets/{provider-scoped-model-input-BGZMCThn.js → provider-scoped-model-input-DUa_9uwg.js} +1 -1
- package/dist/assets/{providers-config-page-D5aBwbOD.js → providers-config-page-DAiqxlF_.js} +1 -1
- package/dist/assets/{puzzle-D5Dhacmb.js → puzzle-D6gulz7u.js} +1 -1
- package/dist/assets/{pwa-ClQK56Qe.js → pwa-C_Zte1xe.js} +1 -1
- package/dist/assets/{refresh-cw-Bw5BY7vp.js → refresh-cw-h9Xq3LY3.js} +1 -1
- package/dist/assets/remote-BO0QP7ua.js +1 -0
- package/dist/assets/{rotate-cw-BksCLAeE.js → rotate-cw-B6xm41Wn.js} +1 -1
- package/dist/assets/runtime-config-page-w1UAkGF_.js +1 -0
- package/dist/assets/{save-CVi8VWzb.js → save-fff_WpY2.js} +1 -1
- package/dist/assets/{search-DN-f7IIC.js → search-CUe41wjm.js} +1 -1
- package/dist/assets/{search-config-page-BmuZTVxs.js → search-config-page-3xbhRlW2.js} +1 -1
- package/dist/assets/{secrets-config-page-NPHpg9sb.js → secrets-config-page-Dn8_rdOj.js} +1 -1
- package/dist/assets/security-config-Y1nEGM3A.js +1 -0
- package/dist/assets/{select-CmDTqk2C.js → select-CG5YWnwe.js} +1 -1
- package/dist/assets/{service-apps-panel-BYGJ9_pO.js → service-apps-panel-CGc04aMn.js} +1 -1
- package/dist/assets/{settings-2-B62YtZon.js → settings-2-CE3ELQyk.js} +1 -1
- package/dist/assets/skeleton-BL9h32W7.js +1 -0
- package/dist/assets/{tabs-Bk_mG7ql.js → tabs-BuE0qKMh.js} +1 -1
- package/dist/assets/{tag-chip-DgqKlI5D.js → tag-chip-BDzpHEli.js} +1 -1
- package/dist/assets/{theme-provider-CeBY2pyu.js → theme-provider-B6FinATQ.js} +1 -1
- package/dist/assets/{tooltip-CgGbBdYf.js → tooltip-DYcuWhuP.js} +1 -1
- package/dist/assets/{trash-2-CVOev8yc.js → trash-2-BPPYt2NV.js} +1 -1
- package/dist/assets/{use-config-b2DN67gs.js → use-config-BPUM2lbo.js} +1 -1
- package/dist/assets/{use-desktop-capabilities-B3tLU1yl.js → use-desktop-capabilities-BDjArQP3.js} +1 -1
- package/dist/assets/{use-panel-app-scroll-restoration-BekzoX6z.js → use-panel-app-scroll-restoration-DsV1IJx-.js} +1 -1
- package/dist/assets/{use-viewport-layout-CUA3jQsA.js → use-viewport-layout-BS18drMT.js} +1 -1
- package/dist/assets/{utils-UBHTdG5Q.js → utils-CBZPzC_b.js} +1 -1
- package/dist/assets/x-DdXOqLnX.js +1 -0
- package/dist/index.html +50 -50
- package/dist/panel-standalone.html +15 -15
- package/package.json +3 -3
- package/src/features/chat/features/message/utils/__tests__/chat-message-timeline.utils.test.ts +10 -0
- package/src/features/chat/features/session/components/__tests__/chat-sidebar-project-groups.test.tsx +90 -6
- package/src/features/chat/features/session/components/chat-sidebar-project-groups.tsx +64 -2
- package/src/features/panel-apps/components/__tests__/panel-app-open-standalone-menu-item.test.tsx +3 -2
- package/src/features/panel-apps/components/panel-app-open-standalone-menu-item.tsx +4 -1
- package/src/features/projects/AGENTS.md +1 -2
- package/src/features/projects/components/project-agreement.tsx +36 -77
- package/src/features/projects/components/project-artifacts.test.tsx +90 -62
- package/src/features/projects/components/project-artifacts.tsx +81 -160
- package/src/features/projects/components/project-materials.test.tsx +80 -0
- package/src/features/projects/components/project-overview.test.tsx +50 -40
- package/src/features/projects/components/project-overview.tsx +5 -4
- package/src/features/projects/components/project-skills.tsx +24 -14
- package/src/features/projects/components/work/project-work-item-drawer.test.tsx +3 -2
- package/src/features/projects/components/work/project-work-items.test.tsx +13 -1
- package/src/features/projects/components/work/project-work-items.tsx +7 -7
- package/src/features/projects/hooks/use-project-materials.test.tsx +65 -0
- package/src/features/projects/hooks/use-project-materials.ts +24 -0
- package/src/features/projects/hooks/use-project-work.test.tsx +57 -1
- package/src/features/projects/hooks/use-project-work.ts +11 -5
- package/src/features/projects/pages/project-home-page.tsx +51 -127
- package/src/features/projects/pages/projects-page.test.tsx +68 -75
- package/src/features/projects/presenters/project-home.presenter.test.ts +1 -64
- package/src/features/projects/presenters/project-home.presenter.ts +17 -54
- package/src/features/projects/utils/project-artifact-view.utils.test.ts +0 -46
- package/src/features/projects/utils/project-artifact-view.utils.ts +0 -47
- package/src/shared/lib/i18n/locales/en-US/projects.json +8 -59
- package/src/shared/lib/i18n/locales/zh-CN/projects.json +8 -59
- package/dist/assets/app-presenter-provider-CLsFiXvV.js +0 -109
- package/dist/assets/doc-browser-w4eGZXLu.js +0 -1
- package/dist/assets/loader-circle-rWr5Yo0E.js +0 -1
- package/dist/assets/login-page-CfMSkOsg.js +0 -1
- package/dist/assets/main-8cVkMfgE.css +0 -1
- package/dist/assets/main-Bys58GxY.js +0 -2
- package/dist/assets/main-i18n-catalog.config-BSq9yiUQ.js +0 -1
- package/dist/assets/mcp-marketplace-page-DX5VE3mU.js +0 -1
- package/dist/assets/panel-app-i18n-catalog.config-C2St67SP.js +0 -1
- package/dist/assets/plus-BHnKwuPq.js +0 -1
- package/dist/assets/projects-DV3UuJMS.js +0 -1
- package/dist/assets/remote-Bl005fzu.js +0 -1
- package/dist/assets/runtime-config-page-CmktQcMW.js +0 -1
- package/dist/assets/security-config-Djptuyf2.js +0 -1
- package/dist/assets/skeleton-Eh64l4T2.js +0 -1
- package/dist/assets/x-C0iaOsRW.js +0 -1
- package/src/features/projects/components/project-requests.tsx +0 -57
- package/src/features/projects/components/project-runs.tsx +0 -60
- package/src/features/projects/hooks/use-project-observation.test.tsx +0 -93
- package/src/features/projects/hooks/use-project-observation.ts +0 -115
- package/src/features/projects/utils/project-request-response.utils.test.ts +0 -44
- package/src/features/projects/utils/project-request-response.utils.ts +0 -39
|
@@ -2,47 +2,57 @@ import { fireEvent, render, screen } from "@testing-library/react";
|
|
|
2
2
|
import { describe, expect, it, vi } from "vitest";
|
|
3
3
|
import { ProjectOverview } from "./project-overview";
|
|
4
4
|
|
|
5
|
-
vi.mock(
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
5
|
+
vi.mock(
|
|
6
|
+
"@/features/projects/hooks/use-project-work",
|
|
7
|
+
async (importOriginal) => ({
|
|
8
|
+
...(await importOriginal()),
|
|
9
|
+
useProjectWorkEvents: vi.fn(),
|
|
10
|
+
useProjectWorkSummary: () => ({
|
|
11
|
+
data: { total: 4, active: 2, attention: 1, completed: 1 },
|
|
12
|
+
isLoading: false,
|
|
13
|
+
isError: false,
|
|
14
|
+
}),
|
|
15
|
+
useProjectWork: () => ({
|
|
16
|
+
data: {
|
|
17
|
+
pages: [
|
|
18
|
+
{
|
|
19
|
+
items: [
|
|
20
|
+
{
|
|
21
|
+
id: "work-1",
|
|
22
|
+
title: "Current work",
|
|
23
|
+
state: { name: "In Progress" },
|
|
24
|
+
},
|
|
25
|
+
],
|
|
26
|
+
},
|
|
27
|
+
],
|
|
28
|
+
},
|
|
29
|
+
isLoading: false,
|
|
30
|
+
isError: false,
|
|
31
|
+
}),
|
|
32
|
+
useProjectArtifacts: () => ({
|
|
33
|
+
data: {
|
|
34
|
+
pages: [
|
|
35
|
+
{
|
|
36
|
+
artifacts: [
|
|
37
|
+
{
|
|
38
|
+
id: "artifact-1",
|
|
39
|
+
path: "docs/design.md",
|
|
40
|
+
label: "Design",
|
|
41
|
+
workItemTitle: "Current work",
|
|
42
|
+
createdAt: "2026-09-03T00:00:00.000Z",
|
|
43
|
+
exists: true,
|
|
44
|
+
},
|
|
45
|
+
],
|
|
46
|
+
nextCursor: null,
|
|
47
|
+
total: 1,
|
|
48
|
+
},
|
|
49
|
+
],
|
|
50
|
+
},
|
|
51
|
+
isLoading: false,
|
|
52
|
+
isError: false,
|
|
53
|
+
}),
|
|
11
54
|
}),
|
|
12
|
-
|
|
13
|
-
data: {
|
|
14
|
-
pages: [
|
|
15
|
-
{
|
|
16
|
-
items: [
|
|
17
|
-
{
|
|
18
|
-
id: "work-1",
|
|
19
|
-
title: "Current work",
|
|
20
|
-
state: { name: "In Progress" },
|
|
21
|
-
},
|
|
22
|
-
],
|
|
23
|
-
},
|
|
24
|
-
],
|
|
25
|
-
},
|
|
26
|
-
isLoading: false,
|
|
27
|
-
isError: false,
|
|
28
|
-
}),
|
|
29
|
-
useProjectRecentArtifacts: () => ({
|
|
30
|
-
data: {
|
|
31
|
-
artifacts: [
|
|
32
|
-
{
|
|
33
|
-
id: "artifact-1",
|
|
34
|
-
path: "docs/design.md",
|
|
35
|
-
label: "Design",
|
|
36
|
-
workItemTitle: "Current work",
|
|
37
|
-
createdAt: "2026-09-03T00:00:00.000Z",
|
|
38
|
-
exists: true,
|
|
39
|
-
},
|
|
40
|
-
],
|
|
41
|
-
},
|
|
42
|
-
isLoading: false,
|
|
43
|
-
isError: false,
|
|
44
|
-
}),
|
|
45
|
-
}));
|
|
55
|
+
);
|
|
46
56
|
|
|
47
57
|
describe("ProjectOverview", () => {
|
|
48
58
|
it("renders current work and recent artifacts as equal wide-layout columns", () => {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { formatDateTime, t } from "@/shared/lib/i18n";
|
|
2
2
|
import {
|
|
3
|
-
|
|
3
|
+
useProjectArtifacts,
|
|
4
4
|
useProjectWork,
|
|
5
5
|
useProjectWorkEvents,
|
|
6
6
|
useProjectWorkSummary,
|
|
@@ -25,7 +25,8 @@ export function ProjectOverview({
|
|
|
25
25
|
useProjectWorkEvents(projectId);
|
|
26
26
|
const summary = useProjectWorkSummary(projectId);
|
|
27
27
|
const work = useProjectWork(projectId, { limit: 5 });
|
|
28
|
-
const artifacts =
|
|
28
|
+
const artifacts = useProjectArtifacts(projectId, { limit: 5 });
|
|
29
|
+
const recentArtifacts = artifacts.data?.pages[0]?.artifacts ?? [];
|
|
29
30
|
const recent =
|
|
30
31
|
work.data?.pages.flatMap((page) => page.items).slice(0, 5) ?? [];
|
|
31
32
|
return (
|
|
@@ -100,9 +101,9 @@ export function ProjectOverview({
|
|
|
100
101
|
<p className="py-5 text-sm text-destructive">
|
|
101
102
|
{t("projectsLoadFailed")}
|
|
102
103
|
</p>
|
|
103
|
-
) :
|
|
104
|
+
) : recentArtifacts.length ? (
|
|
104
105
|
<div className="space-y-2">
|
|
105
|
-
{
|
|
106
|
+
{recentArtifacts.map((artifact) => (
|
|
106
107
|
<ProjectFilePreviewButton
|
|
107
108
|
key={artifact.id}
|
|
108
109
|
available={artifact.exists}
|
|
@@ -1,27 +1,42 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { ProjectSkillMaterial } from "@nextclaw/client-sdk";
|
|
2
2
|
import { formatNumber, t } from "@/shared/lib/i18n";
|
|
3
3
|
import { ProjectEmptyState } from "./project-section";
|
|
4
4
|
|
|
5
5
|
export function ProjectSkills({
|
|
6
|
-
|
|
6
|
+
skills,
|
|
7
|
+
isLoading,
|
|
8
|
+
isError,
|
|
7
9
|
onOpen,
|
|
8
10
|
}: {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
+
skills: ProjectSkillMaterial[];
|
|
12
|
+
isLoading: boolean;
|
|
13
|
+
isError: boolean;
|
|
14
|
+
onOpen: (skill: ProjectSkillMaterial) => void;
|
|
11
15
|
}) {
|
|
16
|
+
if (isLoading) {
|
|
17
|
+
return (
|
|
18
|
+
<p className="py-5 text-sm text-muted-foreground">
|
|
19
|
+
{t("projectsLoading")}
|
|
20
|
+
</p>
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
if (isError) {
|
|
24
|
+
return (
|
|
25
|
+
<p className="py-5 text-sm text-destructive">{t("projectsLoadFailed")}</p>
|
|
26
|
+
);
|
|
27
|
+
}
|
|
12
28
|
return (
|
|
13
29
|
<section className="min-w-0" aria-label={t("projectsSkills")}>
|
|
14
30
|
<p className="mb-3 text-sm text-muted-foreground">
|
|
15
|
-
{formatNumber(
|
|
31
|
+
{formatNumber(skills.length)} {t("projectsSkillsCountSuffix")}
|
|
16
32
|
</p>
|
|
17
|
-
{
|
|
33
|
+
{skills.length ? (
|
|
18
34
|
<div className="grid min-w-0 gap-3 md:grid-cols-2">
|
|
19
|
-
{
|
|
35
|
+
{skills.map((skill) => (
|
|
20
36
|
<button
|
|
21
37
|
type="button"
|
|
22
38
|
key={skill.ref}
|
|
23
|
-
className="min-w-0 rounded-xl border border-border/60 p-3 text-left transition-colors
|
|
24
|
-
disabled={!skill.readable}
|
|
39
|
+
className="min-w-0 rounded-xl border border-border/60 p-3 text-left transition-colors hover:border-border hover:bg-muted/30 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-border"
|
|
25
40
|
onClick={() => onOpen(skill)}
|
|
26
41
|
>
|
|
27
42
|
<h3 className="text-sm font-medium">{skill.name}</h3>
|
|
@@ -37,11 +52,6 @@ export function ProjectSkills({
|
|
|
37
52
|
>
|
|
38
53
|
{skill.description ?? skill.ref}
|
|
39
54
|
</p>
|
|
40
|
-
{!skill.readable ? (
|
|
41
|
-
<p className="mt-2 text-xs text-destructive">
|
|
42
|
-
{t("projectsUnavailable")}
|
|
43
|
-
</p>
|
|
44
|
-
) : null}
|
|
45
55
|
</button>
|
|
46
56
|
))}
|
|
47
57
|
</div>
|
|
@@ -8,8 +8,9 @@ const mocks = vi.hoisted(() => ({
|
|
|
8
8
|
}));
|
|
9
9
|
|
|
10
10
|
vi.mock("@/features/projects/hooks/use-project-work", () => ({
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
useProjectWorkEvents: vi.fn(),
|
|
12
|
+
useProjectWorkStates: () => ({
|
|
13
|
+
data: [{ id: "planned", name: "Planned" }],
|
|
13
14
|
}),
|
|
14
15
|
useProjectWorkItem: mocks.useProjectWorkItem,
|
|
15
16
|
useProjectWorkActivity: () => ({ data: { activities: [] } }),
|
|
@@ -129,7 +129,17 @@ describe("ProjectWorkItems", () => {
|
|
|
129
129
|
|
|
130
130
|
const group = screen.getByRole("button", { name: /Planned.*21/ });
|
|
131
131
|
const count = screen.getByText("21");
|
|
132
|
-
|
|
132
|
+
const groupSection = group.closest("section");
|
|
133
|
+
const workItem = screen.getByRole("button", {
|
|
134
|
+
name: /Clickable work item/,
|
|
135
|
+
});
|
|
136
|
+
expect(groupSection?.className).not.toContain("border");
|
|
137
|
+
expect(groupSection?.className).not.toContain("rounded-xl");
|
|
138
|
+
expect(groupSection?.className).not.toContain("bg-muted");
|
|
139
|
+
expect(workItem.className).toContain("border");
|
|
140
|
+
expect(workItem.className).toContain("rounded-xl");
|
|
141
|
+
expect(count.className).not.toContain("rounded-full");
|
|
142
|
+
expect(count.className).toContain("text-muted-foreground");
|
|
133
143
|
expect(count.className).toContain("tabular-nums");
|
|
134
144
|
fireEvent.click(group);
|
|
135
145
|
expect(group.textContent).toContain("21");
|
|
@@ -162,6 +172,8 @@ describe("ProjectWorkItems", () => {
|
|
|
162
172
|
|
|
163
173
|
const group = screen.getByRole("region", { name: /Planned|计划中/ });
|
|
164
174
|
expect(group.className).toContain("h-full");
|
|
175
|
+
expect(group.className).not.toContain("rounded-xl");
|
|
176
|
+
expect(group.className).not.toContain("bg-muted");
|
|
165
177
|
const cards = screen.getByRole("button", {
|
|
166
178
|
name: /Clickable work item/,
|
|
167
179
|
}).parentElement;
|
|
@@ -227,14 +227,14 @@ function ProjectWorkStateGroup({
|
|
|
227
227
|
<section
|
|
228
228
|
className={
|
|
229
229
|
view === "board"
|
|
230
|
-
? "flex h-full min-h-0 min-w-0 flex-col overflow-hidden
|
|
231
|
-
: "min-w-0
|
|
230
|
+
? "flex h-full min-h-0 min-w-0 flex-col overflow-hidden"
|
|
231
|
+
: "min-w-0"
|
|
232
232
|
}
|
|
233
233
|
aria-label={getProjectWorkStateLabel(state.name)}
|
|
234
234
|
>
|
|
235
235
|
<button
|
|
236
236
|
type="button"
|
|
237
|
-
className="flex w-full shrink-0 items-center gap-2
|
|
237
|
+
className="flex w-full shrink-0 items-center gap-2 rounded-lg px-1 py-2 text-left transition-colors hover:bg-[var(--interaction-hover)] focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-border"
|
|
238
238
|
aria-expanded={!collapsed}
|
|
239
239
|
onClick={() => setCollapsed((value) => !value)}
|
|
240
240
|
>
|
|
@@ -247,7 +247,7 @@ function ProjectWorkStateGroup({
|
|
|
247
247
|
<span className="min-w-0 truncate text-sm font-semibold">
|
|
248
248
|
{getProjectWorkStateLabel(state.name)}
|
|
249
249
|
</span>
|
|
250
|
-
<span className="
|
|
250
|
+
<span className="ml-0.5 shrink-0 text-xs font-medium tabular-nums text-muted-foreground">
|
|
251
251
|
{total ?? "…"}
|
|
252
252
|
</span>
|
|
253
253
|
</button>
|
|
@@ -255,8 +255,8 @@ function ProjectWorkStateGroup({
|
|
|
255
255
|
<div
|
|
256
256
|
className={
|
|
257
257
|
view === "board"
|
|
258
|
-
? "min-h-0 flex-1 space-y-2 overflow-y-auto overscroll-contain
|
|
259
|
-
: "space-y-2
|
|
258
|
+
? "min-h-0 flex-1 space-y-2 overflow-y-auto overscroll-contain px-1 pb-3"
|
|
259
|
+
: "space-y-2 px-1 pb-3"
|
|
260
260
|
}
|
|
261
261
|
>
|
|
262
262
|
<ProjectWorkStateGroupBody
|
|
@@ -389,7 +389,7 @@ export function ProjectWorkItems({
|
|
|
389
389
|
{t("projectsWorkShowDeleted")}
|
|
390
390
|
</label>
|
|
391
391
|
{view === "list" ? (
|
|
392
|
-
<div className="space-y-
|
|
392
|
+
<div className="space-y-3">
|
|
393
393
|
{listStates.map((state) => (
|
|
394
394
|
<ProjectWorkStateGroup
|
|
395
395
|
key={state.id}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
|
2
|
+
import { renderHook, waitFor } from "@testing-library/react";
|
|
3
|
+
import type { PropsWithChildren } from "react";
|
|
4
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
5
|
+
import {
|
|
6
|
+
useProjectAgreement,
|
|
7
|
+
useProjectSkills,
|
|
8
|
+
} from "@/features/projects/hooks/use-project-materials";
|
|
9
|
+
import { nextclawClient } from "@/shared/lib/api";
|
|
10
|
+
|
|
11
|
+
function createWrapper() {
|
|
12
|
+
const client = new QueryClient({
|
|
13
|
+
defaultOptions: { queries: { retry: false } },
|
|
14
|
+
});
|
|
15
|
+
return function Wrapper({ children }: PropsWithChildren) {
|
|
16
|
+
return (
|
|
17
|
+
<QueryClientProvider client={client}>{children}</QueryClientProvider>
|
|
18
|
+
);
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
describe("project material queries", () => {
|
|
23
|
+
beforeEach(() => vi.restoreAllMocks());
|
|
24
|
+
|
|
25
|
+
it("loads agreement and skills independently", async () => {
|
|
26
|
+
const getAgreement = vi
|
|
27
|
+
.spyOn(nextclawClient.projects, "getAgreement")
|
|
28
|
+
.mockResolvedValue({ path: "AGENTS.md", available: true });
|
|
29
|
+
const listProjectSkills = vi
|
|
30
|
+
.spyOn(nextclawClient.projects, "listProjectSkills")
|
|
31
|
+
.mockResolvedValue([
|
|
32
|
+
{
|
|
33
|
+
ref: "project:alpha",
|
|
34
|
+
name: "alpha",
|
|
35
|
+
path: ".agents/skills/alpha/SKILL.md",
|
|
36
|
+
},
|
|
37
|
+
]);
|
|
38
|
+
|
|
39
|
+
const agreement = renderHook(() => useProjectAgreement("project-1"), {
|
|
40
|
+
wrapper: createWrapper(),
|
|
41
|
+
});
|
|
42
|
+
const skills = renderHook(() => useProjectSkills("project-1"), {
|
|
43
|
+
wrapper: createWrapper(),
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
await waitFor(() => expect(agreement.result.current.isSuccess).toBe(true));
|
|
47
|
+
await waitFor(() => expect(skills.result.current.isSuccess).toBe(true));
|
|
48
|
+
expect(getAgreement).toHaveBeenCalledWith("project-1");
|
|
49
|
+
expect(listProjectSkills).toHaveBeenCalledWith("project-1");
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("does not query without a selected project", () => {
|
|
53
|
+
const getAgreement = vi.spyOn(nextclawClient.projects, "getAgreement");
|
|
54
|
+
const listProjectSkills = vi.spyOn(
|
|
55
|
+
nextclawClient.projects,
|
|
56
|
+
"listProjectSkills",
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
renderHook(() => useProjectAgreement(null), { wrapper: createWrapper() });
|
|
60
|
+
renderHook(() => useProjectSkills(null), { wrapper: createWrapper() });
|
|
61
|
+
|
|
62
|
+
expect(getAgreement).not.toHaveBeenCalled();
|
|
63
|
+
expect(listProjectSkills).not.toHaveBeenCalled();
|
|
64
|
+
});
|
|
65
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { useQuery } from "@tanstack/react-query";
|
|
2
|
+
import { nextclawClient } from "@/shared/lib/api";
|
|
3
|
+
|
|
4
|
+
export const projectAgreementQueryKey = (projectId: string) =>
|
|
5
|
+
["project-agreement", projectId] as const;
|
|
6
|
+
export const projectSkillsQueryKey = (projectId: string) =>
|
|
7
|
+
["project-skills", projectId] as const;
|
|
8
|
+
|
|
9
|
+
export function useProjectAgreement(projectId: string | null) {
|
|
10
|
+
return useQuery({
|
|
11
|
+
queryKey: projectAgreementQueryKey(projectId ?? ""),
|
|
12
|
+
enabled: Boolean(projectId),
|
|
13
|
+
queryFn: async () => await nextclawClient.projects.getAgreement(projectId!),
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function useProjectSkills(projectId: string | null) {
|
|
18
|
+
return useQuery({
|
|
19
|
+
queryKey: projectSkillsQueryKey(projectId ?? ""),
|
|
20
|
+
enabled: Boolean(projectId),
|
|
21
|
+
queryFn: async () =>
|
|
22
|
+
await nextclawClient.projects.listProjectSkills(projectId!),
|
|
23
|
+
});
|
|
24
|
+
}
|
|
@@ -6,6 +6,7 @@ import type { ProjectWorkState } from "@nextclaw/client-sdk";
|
|
|
6
6
|
import {
|
|
7
7
|
sortProjectWorkStates,
|
|
8
8
|
sortProjectWorkStatesForList,
|
|
9
|
+
useProjectArtifacts,
|
|
9
10
|
useProjectWork,
|
|
10
11
|
useProjectWorkEvents,
|
|
11
12
|
} from "./use-project-work";
|
|
@@ -15,11 +16,15 @@ const mocks = vi.hoisted(() => ({
|
|
|
15
16
|
| null
|
|
16
17
|
| ((event: { projectId: string; workItemId?: string }) => void),
|
|
17
18
|
listWork: vi.fn(),
|
|
19
|
+
listRecentWorkArtifacts: vi.fn(),
|
|
18
20
|
}));
|
|
19
21
|
|
|
20
22
|
vi.mock("@/shared/lib/api", () => ({
|
|
21
23
|
nextclawClient: {
|
|
22
|
-
projects: {
|
|
24
|
+
projects: {
|
|
25
|
+
listWork: mocks.listWork,
|
|
26
|
+
listRecentWorkArtifacts: mocks.listRecentWorkArtifacts,
|
|
27
|
+
},
|
|
23
28
|
eventBus: {
|
|
24
29
|
on: (
|
|
25
30
|
_key: unknown,
|
|
@@ -38,6 +43,12 @@ describe("useProjectWork", () => {
|
|
|
38
43
|
beforeEach(() => {
|
|
39
44
|
mocks.listWork.mockReset();
|
|
40
45
|
mocks.listWork.mockResolvedValue({ items: [], nextCursor: null, total: 0 });
|
|
46
|
+
mocks.listRecentWorkArtifacts.mockReset();
|
|
47
|
+
mocks.listRecentWorkArtifacts.mockResolvedValue({
|
|
48
|
+
artifacts: [],
|
|
49
|
+
nextCursor: null,
|
|
50
|
+
total: 0,
|
|
51
|
+
});
|
|
41
52
|
});
|
|
42
53
|
|
|
43
54
|
it("refreshes the project query from committed work events only for the matching project", async () => {
|
|
@@ -105,6 +116,51 @@ describe("useProjectWork", () => {
|
|
|
105
116
|
});
|
|
106
117
|
await waitFor(() => expect(result.current.data?.pages).toHaveLength(2));
|
|
107
118
|
});
|
|
119
|
+
|
|
120
|
+
it("loads filtered artifacts through the paginated project-work owner", async () => {
|
|
121
|
+
mocks.listRecentWorkArtifacts
|
|
122
|
+
.mockResolvedValueOnce({
|
|
123
|
+
artifacts: [{ id: "one", path: "docs/report.md" }],
|
|
124
|
+
nextCursor: "artifact-next",
|
|
125
|
+
total: 2,
|
|
126
|
+
})
|
|
127
|
+
.mockResolvedValueOnce({
|
|
128
|
+
artifacts: [{ id: "two", path: "docs/report-final.md" }],
|
|
129
|
+
nextCursor: null,
|
|
130
|
+
total: 2,
|
|
131
|
+
});
|
|
132
|
+
const client = new QueryClient({
|
|
133
|
+
defaultOptions: { queries: { retry: false } },
|
|
134
|
+
});
|
|
135
|
+
const wrapper = ({ children }: { children: ReactNode }) => (
|
|
136
|
+
<QueryClientProvider client={client}>{children}</QueryClientProvider>
|
|
137
|
+
);
|
|
138
|
+
const { result } = renderHook(
|
|
139
|
+
() => useProjectArtifacts("project-1", { limit: 20, query: "report" }),
|
|
140
|
+
{ wrapper },
|
|
141
|
+
);
|
|
142
|
+
await waitFor(() => expect(result.current.hasNextPage).toBe(true));
|
|
143
|
+
|
|
144
|
+
await act(async () => await result.current.fetchNextPage());
|
|
145
|
+
|
|
146
|
+
expect(mocks.listRecentWorkArtifacts).toHaveBeenNthCalledWith(
|
|
147
|
+
1,
|
|
148
|
+
"project-1",
|
|
149
|
+
{
|
|
150
|
+
limit: 20,
|
|
151
|
+
query: "report",
|
|
152
|
+
},
|
|
153
|
+
);
|
|
154
|
+
expect(mocks.listRecentWorkArtifacts).toHaveBeenNthCalledWith(
|
|
155
|
+
2,
|
|
156
|
+
"project-1",
|
|
157
|
+
{
|
|
158
|
+
limit: 20,
|
|
159
|
+
query: "report",
|
|
160
|
+
cursor: "artifact-next",
|
|
161
|
+
},
|
|
162
|
+
);
|
|
163
|
+
});
|
|
108
164
|
});
|
|
109
165
|
|
|
110
166
|
describe("project work state ordering", () => {
|
|
@@ -68,14 +68,20 @@ export function useProjectWorkStates(projectId: string | null) {
|
|
|
68
68
|
});
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
-
export function
|
|
72
|
-
|
|
73
|
-
|
|
71
|
+
export function useProjectArtifacts(
|
|
72
|
+
projectId: string | null,
|
|
73
|
+
input: { limit?: number; query?: string } = {},
|
|
74
|
+
) {
|
|
75
|
+
return useInfiniteQuery({
|
|
76
|
+
queryKey: [...projectWorkArtifactsQueryKey(projectId ?? ""), input],
|
|
74
77
|
enabled: Boolean(projectId),
|
|
75
|
-
|
|
78
|
+
initialPageParam: null as string | null,
|
|
79
|
+
queryFn: async ({ pageParam }) =>
|
|
76
80
|
await nextclawClient.projects.listRecentWorkArtifacts(projectId!, {
|
|
77
|
-
|
|
81
|
+
...input,
|
|
82
|
+
...(pageParam ? { cursor: pageParam } : {}),
|
|
78
83
|
}),
|
|
84
|
+
getNextPageParam: (lastPage) => lastPage.nextCursor,
|
|
79
85
|
});
|
|
80
86
|
}
|
|
81
87
|
|