История изменений в карточке задачи (#77)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
37f28c3812
commit
066cb79c4f
@ -24,8 +24,9 @@
|
||||
| `importer` | разовый импорт CSV-пакета в заметки, сверка с зеркалом | `mirror`, `settings` |
|
||||
| `cli` | `init`, `serve`, `import`, `mcp`, `tokens`, `agent publish` | `api`, `mcp_server`, `importer`, `release`, `settings`, `db`, `paths` |
|
||||
|
||||
`web/` — веб-приложение хаба (React + TypeScript + Vite): вход по клиентскому токену, страницы «Доска» (`/app/board`)
|
||||
и «Задачи» (`/app/issues`); текущий проект — `?project=<метка>` в адресе (`web/src/projects/`: контекст проекта, выпадающий список «Проект», окно «Новый проект», заглушка «проект не найден»; страницы пересоздаются по `key={project}`); карточка `/app/issues/{key}` — окно поверх страницы, с которой её открыли: `shell/Layout.tsx` рисует
|
||||
`web/` — веб-приложение хаба (React + TypeScript + Vite): вход по клиентскому токену, страницы «Доска» (`/app/board`),
|
||||
«Задачи» (`/app/issues`) и «Лента» (`/app/feed`: журнал изменений `GET /changes` за период; `web/src/feed/` — чистые
|
||||
функции `events.ts` (период, виды, текст события, группировка) и `EventList.tsx`, общий с блоком «История» карточки); текущий проект — `?project=<метка>` в адресе (`web/src/projects/`: контекст проекта, выпадающий список «Проект», окно «Новый проект», заглушка «проект не найден»; страницы пересоздаются по `key={project}`); карточка `/app/issues/{key}` — окно поверх страницы, с которой её открыли: `shell/Layout.tsx` рисует
|
||||
страницу по адресу из `location.state.background` (`useRoutes` с явным адресом) и `issues/IssueModal.tsx` поверх неё. Собирается `make web` в `src/jira_hub/web/app/`; `api` отдаёт сборку
|
||||
без токена (`/app/assets/*` — файлы, прочие `/app…` — `index.html`, 503 без сборки), `/` и `/links` перенаправляет на
|
||||
`/app/board`. Данные — только через HTTP API с клиентским токеном; `mirror.board` отдаёт зеркало без преобразований,
|
||||
|
||||
@ -24,7 +24,7 @@
|
||||
- `uv sync` — установка окружения.
|
||||
- `uv run jira-hub init --config PATH [--force]` — настройки проекта из `config.example.json`, создание токенов.
|
||||
- `uv run jira-hub serve [--host] [--port]` — локальный запуск хаба.
|
||||
- Веб-приложение — `http://127.0.0.1:8790/app/` (доска — `/app/board?project=<метка>`, задачи — `/app/issues?project=<метка>`; вход по клиентскому токену); `/` и `/links` перенаправляют на доску.
|
||||
- Веб-приложение — `http://127.0.0.1:8790/app/` (доска — `/app/board?project=<метка>`, задачи — `/app/issues?project=<метка>`, лента изменений — `/app/feed?project=<метка>`; вход по клиентскому токену); `/` и `/links` перенаправляют на доску.
|
||||
- `make web` — сборка приложения (после `git pull` на Mac mini, до перезапуска хаба); `make check` — pytest + lint и тесты фронта; `cd web && npm run dev` — разработка с прокси на тестовый хаб `bash web/scripts/test-hub.sh` (:8791); `cd web && npm run e2e` — Playwright; `cd web && npm run golden` — перегенерировать эталоны доски из `links.html@3931a88` (только при смене фикстур).
|
||||
- `uv run jira-hub import FOLDER` — импорт CSV-пакета `admin/jira-sync`; `uv run jira-hub mcp [--project <метка>]` — MCP по stdio (с привязкой сессии к проекту); `uv run jira-hub tokens` — показать токены.
|
||||
- `uv run jira-hub agent publish PATH` — опубликовать версию агента для самообновления (только вверх).
|
||||
|
||||
@ -16,7 +16,7 @@ async function seedHistory() {
|
||||
expect(r.ok).toBe(true);
|
||||
}
|
||||
|
||||
test("лента: событие за сегодня, карточка окном поверх ленты", async ({ page }) => {
|
||||
test("лента: событие за сегодня, карточка окном поверх ленты, история в карточке", async ({ page }) => {
|
||||
await seedHistory();
|
||||
await login(page, "/app/feed?project=AWPCA");
|
||||
const group = page.getByTestId("feed-group").filter({ has: page.getByRole("link", { name: "AURORA-9002", exact: true }) });
|
||||
@ -29,6 +29,8 @@ test("лента: событие за сегодня, карточка окно
|
||||
await group.getByRole("link", { name: "AURORA-9002", exact: true }).click();
|
||||
const d = page.getByRole("dialog");
|
||||
await expect(d.getByRole("heading", { name: "API свода" })).toBeVisible();
|
||||
await expect(d.getByRole("heading", { name: /^История \(\d+\)$/ })).toBeVisible();
|
||||
await expect(d.getByTestId("history").getByText("статус «В работе» → «Готово»").first()).toBeVisible();
|
||||
await page.keyboard.press("Escape");
|
||||
await expect(page.getByRole("dialog")).toHaveCount(0);
|
||||
await expect(page.getByTestId("page-feed")).toBeVisible();
|
||||
|
||||
@ -450,3 +450,35 @@ describe("карточка задачи", () => {
|
||||
err.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
describe("история в карточке", () => {
|
||||
beforeEach(() => { setToken("tok"); localStorage.clear(); });
|
||||
const hist = (n: number) => Array.from({ length: n }, (_, i) => ({
|
||||
at: new Date(2026, 9, 20, 9, i).toISOString(), kind: "status" as const, field: "status", author: "Петров",
|
||||
from: "a", to: `s${i}`, from_value: null, to_value: null,
|
||||
}));
|
||||
|
||||
it("12 событий: видны 10 последних и «Показать все (12)», после нажатия — все", async () => {
|
||||
mockFetch((_m, url) => url.pathname === "/settings" ? json(200, settings) : json(200, issue("AURORA-9001", { changes: hist(12) })));
|
||||
open("/issues/AURORA-9001");
|
||||
const sec = await screen.findByTestId("history");
|
||||
expect(within(sec).getByRole("heading", { name: "История (12)" })).toBeInTheDocument();
|
||||
expect(within(sec).getAllByRole("listitem")).toHaveLength(10);
|
||||
expect(within(sec).getAllByRole("listitem")[0]).toHaveTextContent(/^20\.10 09:00/); // с датой всегда
|
||||
await userEvent.click(within(sec).getByRole("button", { name: "Показать все (12)" }));
|
||||
expect(within(sec).getAllByRole("listitem")).toHaveLength(12);
|
||||
});
|
||||
|
||||
it("пустая история — «изменений нет»; без поля changes (старый хаб) — блока нет", async () => {
|
||||
let body = issue("AURORA-9001", { changes: [] });
|
||||
mockFetch((_m, url) => url.pathname === "/settings" ? json(200, settings) : json(200, body));
|
||||
open("/issues/AURORA-9001");
|
||||
const sec = await screen.findByTestId("history");
|
||||
expect(within(sec).getByText("изменений нет")).toBeInTheDocument();
|
||||
cleanup();
|
||||
body = issue("AURORA-9002");
|
||||
open("/issues/AURORA-9002");
|
||||
await screen.findByRole("heading", { name: "Тема AURORA-9002" });
|
||||
expect(screen.queryByTestId("history")).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
@ -2,9 +2,10 @@ import { type RefObject, useLayoutEffect, useRef, useState } from "react";
|
||||
import { ApiError } from "../api/client.ts";
|
||||
import { useClosedStatuses } from "../api/closed.ts";
|
||||
import { useIssue, useSettings } from "../api/queries.ts";
|
||||
import type { IssueComment, IssueDetail } from "../api/types.ts";
|
||||
import type { ChangeEvent, IssueComment, IssueDetail } from "../api/types.ts";
|
||||
import { JIRA_URL } from "../board/constants.ts";
|
||||
import { fmtDate, fmtStamp } from "../board/model/util.ts";
|
||||
import { EventList } from "../feed/EventList.tsx";
|
||||
import { groupLinks, isOverdue, todayISO } from "./card.ts";
|
||||
import { useCommentsHidden } from "./commentsPref.ts";
|
||||
import { IssueLink } from "./IssueLink.tsx";
|
||||
@ -69,6 +70,7 @@ export function IssueCard({ issueKey: key, onClose }: { issueKey: string; onClos
|
||||
<Links issue={i} linkTypes={(settings.data?.link_types ?? {}) as Record<string, string>} />
|
||||
{i.description && <Description text={i.description} projectKey={settings.data?.project_key} />}
|
||||
<Comments items={i.comments ?? []} projectKey={settings.data?.project_key} />
|
||||
{i.changes && <History items={i.changes} />}
|
||||
</div>
|
||||
<Panel className="side">
|
||||
<Notes key={i.key} issue={i} />
|
||||
@ -178,6 +180,22 @@ function Comments({ items, projectKey }: { items: IssueComment[]; projectKey?: s
|
||||
);
|
||||
}
|
||||
|
||||
const HISTORY_SHOWN = 10;
|
||||
|
||||
// журнал изменений задачи: 10 последних, остальное — по «Показать все» (spec web-app «История в карточке задачи»)
|
||||
function History({ items }: { items: ChangeEvent[] }) {
|
||||
const [all, setAll] = useState(false);
|
||||
const shown = all ? items : items.slice(0, HISTORY_SHOWN);
|
||||
return (
|
||||
<section className="history" data-testid="history">
|
||||
<div className="sec-head"><h3>История ({items.length})</h3></div>
|
||||
{!items.length && <p className="hint">изменений нет</p>}
|
||||
{items.length > 0 && <EventList events={shown} now={new Date()} alwaysDate />}
|
||||
{!all && items.length > HISTORY_SHOWN && <Button variant="ghost" className="more" onClick={() => setAll(true)}>{`Показать все (${items.length})`}</Button>}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function OtherFields({ issue: i }: { issue: IssueDetail }) {
|
||||
const rows: [string, string | null][] = [
|
||||
["Тип", i.issue_type], ["Приоритет", i.priority], ["Автор", i.reporter], ["Метки", i.labels.join(", ") || null],
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user