diff --git a/web/src/projects/projects.css b/web/src/projects/projects.css
index b666fba..e08d77c 100644
--- a/web/src/projects/projects.css
+++ b/web/src/projects/projects.css
@@ -5,3 +5,7 @@
.project-form { display: flex; flex-direction: column; gap: var(--space-4); }
.project-form__title { margin: 0; font-size: var(--fs-section); line-height: var(--lh-section); font-weight: 700; color: var(--color-text-primary); }
.project-form__actions { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: var(--space-2); }
+.project-form__group { display: flex; flex-direction: column; gap: var(--space-3); margin: 0; padding: var(--space-3) var(--space-4) var(--space-4); border: 1px solid var(--color-border-default); border-radius: var(--radius-md); min-width: 0; }
+.project-form__legend { padding: 0 var(--space-1); font-size: var(--fs-caption); line-height: var(--lh-caption); font-weight: 700; color: var(--color-text-secondary); }
+.project-form__group { display: flex; flex-direction: column; gap: var(--space-3); margin: 0; padding: var(--space-3) var(--space-4) var(--space-4); border: 1px solid var(--color-border-default); border-radius: var(--radius-md); min-width: 0; }
+.project-form__legend { padding: 0 var(--space-1); font-size: var(--fs-caption); line-height: var(--lh-caption); font-weight: 700; color: var(--color-text-secondary); }
diff --git a/web/src/projects/projects.test.tsx b/web/src/projects/projects.test.tsx
index a014e39..3aa8f10 100644
--- a/web/src/projects/projects.test.tsx
+++ b/web/src/projects/projects.test.tsx
@@ -99,7 +99,7 @@ describe("проект в приложении", () => {
it("создание проекта из формы", async () => {
let created = false;
const h = hub([A], {
- "POST /projects": () => { created = true; return json(201, { project: S, batch_id: 7 }); },
+ "POST /projects": () => { created = true; return json(201, { project: S, batch_id: 7, batches: [{ module: "jira", id: 7 }] }); },
"GET /projects": () => json(200, { projects: created ? [A, S] : [A] }), // блок состояния берёт проект из списка
"GET /batches/7": () => json(200, { id: 7, author: "viewer", status: "queued", remaining_seconds: 5, counts: {}, commands: [{ type: "refresh", status: "queued" }] }),
});
@@ -121,7 +121,7 @@ describe("проект в приложении", () => {
});
it("с номером доски board_id уходит числом", async () => {
- const h = hub([A], { "POST /projects": () => json(201, { project: S, batch_id: 7 }) });
+ const h = hub([A], { "POST /projects": () => json(201, { project: S, batch_id: 7, batches: [{ module: "jira", id: 7 }] }) });
open("/app/jira/board?project=AWPCA");
await screen.findByTestId("page-board");
await userEvent.selectOptions(screen.getByRole("combobox", { name: "Проект" }), "+ Новый проект…");
@@ -152,7 +152,7 @@ describe("проект в приложении", () => {
let n = 0;
const h = hub([A, S], {
"GET /projects": () => (++n === 1 ? json(200, { projects: [A, S] }) : json(500, { error: "boom" })),
- "POST /projects": () => json(201, { project: S, batch_id: 7 }),
+ "POST /projects": () => json(201, { project: S, batch_id: 7, batches: [{ module: "jira", id: 7 }] }),
"GET /batches/7": () => json(200, { id: 7, author: "viewer", status: "queued", remaining_seconds: 5, counts: {}, commands: [{ type: "refresh", status: "queued" }] }),
});
const router = open("/app/jira/board?project=AWPCA");
@@ -171,7 +171,7 @@ describe("проект в приложении", () => {
it("окно «Новый проект» во время создания не закрывается: «Отмена» заблокирована, Esc гасится", async () => {
let release!: () => void;
const gate = new Promise
((r) => { release = r; });
- hub([A], { "POST /projects": async () => { await gate; return json(201, { project: S, batch_id: 7 }); } });
+ hub([A], { "POST /projects": async () => { await gate; return json(201, { project: S, batch_id: 7, batches: [{ module: "jira", id: 7 }] }); } });
open("/app/jira/board?project=AWPCA");
await screen.findByTestId("page-board");
await userEvent.selectOptions(screen.getByRole("combobox", { name: "Проект" }), "+ Новый проект…");
diff --git a/web/src/shell/Layout.tsx b/web/src/shell/Layout.tsx
index 7faff22..be55fae 100644
--- a/web/src/shell/Layout.tsx
+++ b/web/src/shell/Layout.tsx
@@ -3,10 +3,9 @@ import { useLocation, useNavigate, useRoutes } from "react-router";
import { useProjects } from "../api/queries.ts";
import { useOverlays } from "../modules/index.ts";
import { rememberedProject, rememberProject, resolveProject, withProject } from "../projects/current.ts";
-import { NewProjectDialog } from "../projects/NewProjectDialog.tsx";
+import { NewProject } from "./NewProject.tsx";
import { ProjectContext, type ProjectCtx, type ProjectState } from "../projects/ProjectContext.ts";
import { ActivityProvider } from "./activity/ActivityProvider.tsx";
-import { useTrackBatch } from "./activity/activity.ts";
import { ErrorBoundary } from "./ErrorBoundary.tsx";
import { NAV } from "./nav.tsx";
import { PAGE_ROUTES } from "./pages.tsx";
@@ -70,9 +69,3 @@ export function Layout() {
);
}
-
-// окно «Новый проект»: пакет, поставленный хабом при создании, отслеживает трекер ядра (модуль берётся из ответа хаба)
-function NewProject({ onClose, select }: { onClose(): void; select(label: string): void }) {
- const trackBatch = useTrackBatch();
- return { onClose(); void trackBatch(batchId).catch(() => {}); select(p.label); }} />;
-}
diff --git a/web/src/shell/NewProject.test.tsx b/web/src/shell/NewProject.test.tsx
new file mode 100644
index 0000000..0bfd1e9
--- /dev/null
+++ b/web/src/shell/NewProject.test.tsx
@@ -0,0 +1,32 @@
+import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
+import { render, screen, waitFor } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import { beforeEach, describe, expect, it, vi } from "vitest";
+import { setToken } from "../auth/token.ts";
+import { json, mockFetch } from "../test/fetchMock.ts";
+import { ActivityContext, type ActivityValue, type ModuleActivity } from "./activity/activity.ts";
+import { NewProject } from "./NewProject.tsx";
+
+const act = (): ModuleActivity => ({ agent: null, event: null, busy: false, running: null, start: async () => {}, track: vi.fn() });
+
+describe("NewProject: пакеты каждому своему модулю", () => {
+ beforeEach(() => setToken("tok"));
+
+ it("jira и wiki получают свои id (M-14, T5.3 #68)", async () => {
+ mockFetch((method) => method === "POST"
+ ? json(201, { project: { label: "P", name: "Пэ", created_at: "" }, batch_id: 3, batches: [{ module: "jira", id: 3 }, { module: "wiki", id: 4 }] })
+ : json(404, {}));
+ const modules = { jira: act(), wiki: act() };
+ const value: ActivityValue = { agents: [], modules, summary: "online", trackBatch: vi.fn(async () => {}) };
+ const onClose = vi.fn(), select = vi.fn();
+ render(
+ );
+ await userEvent.type(screen.getByLabelText("Метка"), "P");
+ await userEvent.type(screen.getByLabelText("Название"), "Пэ");
+ await userEvent.click(screen.getByRole("button", { name: "Создать" }));
+ await waitFor(() => expect(select).toHaveBeenCalledWith("P"));
+ expect(modules.jira.track).toHaveBeenCalledExactlyOnceWith(3);
+ expect(modules.wiki.track).toHaveBeenCalledExactlyOnceWith(4);
+ expect(value.trackBatch).not.toHaveBeenCalled();
+ });
+});
diff --git a/web/src/shell/NewProject.tsx b/web/src/shell/NewProject.tsx
new file mode 100644
index 0000000..daeee69
--- /dev/null
+++ b/web/src/shell/NewProject.tsx
@@ -0,0 +1,12 @@
+import { NewProjectDialog } from "../projects/NewProjectDialog.tsx";
+import { useTrackModuleBatch } from "./activity/activity.ts";
+
+// окно «Новый проект»: каждый пакет, поставленный хабом при создании, отслеживает трекер своего модуля (поле module)
+export function NewProject({ onClose, select }: { onClose(): void; select(label: string): void }) {
+ const track = useTrackModuleBatch();
+ return {
+ onClose();
+ for (const b of batches) track(b.module, b.id);
+ select(p.label);
+ }} />;
+}
diff --git a/web/src/shell/activity/activity.ts b/web/src/shell/activity/activity.ts
index 1124780..057e2af 100644
--- a/web/src/shell/activity/activity.ts
+++ b/web/src/shell/activity/activity.ts
@@ -1,4 +1,4 @@
-import { createContext, useContext } from "react";
+import { createContext, useCallback, useContext } from "react";
import type { AgentView, Batch } from "../../api/types.ts";
import type { ModuleEvent } from "./events.ts";
@@ -33,6 +33,11 @@ export function useModuleActivity(name: string): ModuleActivity {
if (!m) throw new Error(`у модуля «${name}» нет описания состояния (HubModule.status)`);
return m;
}
+// отслеживать пакет, поставленный хабом конкретному модулю (после создания/правки проекта); неизвестный модуль пропускается
+export function useTrackModuleBatch(): (module: string, batchId: number) => void {
+ const { modules } = useActivity();
+ return useCallback((module, id) => { modules[module]?.track(id); }, [modules]);
+}
export const useAgents = (): AgentView[] => useActivity().agents;
export const useActivitySummary = (): ActivityValue["summary"] => useActivity().summary;
export const useTrackBatch = (): ActivityValue["trackBatch"] => useActivity().trackBatch;