From c5e33a9cc4102c052eff6257a8044802ef382acb Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Sat, 17 Oct 2026 11:41:47 +0400 Subject: [PATCH] =?UTF-8?q?=D0=9A=D0=B0=D1=80=D1=82=D0=BE=D1=87=D0=BA?= =?UTF-8?q?=D0=B0=20=D0=B7=D0=B0=D0=B4=D0=B0=D1=87=D0=B8:=20=D0=B1=D0=B5?= =?UTF-8?q?=D0=B7=20=D1=81=D1=82=D0=B0=D1=82=D1=83=D1=81=D0=B0=20=D0=BF?= =?UTF-8?q?=D0=BE=20=D1=84=D0=B0=D0=BA=D1=82=D1=83,=20=C2=AB=D0=BF=D1=80?= =?UTF-8?q?=D0=BE=D1=81=D1=80=D0=BE=D1=87=D0=B5=D0=BD=D0=BE=C2=BB=20=D0=BF?= =?UTF-8?q?=D0=BE=20=D1=81=D1=82=D0=B0=D1=82=D1=83=D1=81=D1=83=20Jira=20(#?= =?UTF-8?q?57)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- web/src/api/types.ts | 5 +++-- web/src/issues/IssueCard.test.tsx | 29 ++++++++++++++++++++--------- web/src/issues/IssueCard.tsx | 10 ++++------ web/src/issues/NoteForm.tsx | 8 +------- web/src/issues/Notes.tsx | 2 +- web/src/issues/card.test.ts | 25 +++++++++++-------------- web/src/issues/card.ts | 13 ++++--------- web/src/issues/issues.css | 3 --- 8 files changed, 44 insertions(+), 51 deletions(-) diff --git a/web/src/api/types.ts b/web/src/api/types.ts index c727781..746574c 100644 --- a/web/src/api/types.ts +++ b/web/src/api/types.ts @@ -28,10 +28,11 @@ export interface IssueRow { neighbor: boolean; fact_status?: string | null; work_kind?: string | null; } -export const NOTE_FIELDS = ["fact_status", "work_kind", "done_text", "rest_text", "owner_note"] as const; +// поля заметки, которые приложение показывает и правит; fact_status хаб ещё отдаёт, приложение его не читает (hide-closed) +export const NOTE_FIELDS = ["work_kind", "done_text", "rest_text", "owner_note"] as const; export type NoteField = (typeof NOTE_FIELDS)[number]; export type NoteFields = Partial>; -export type Note = Record & { key: string; updated_at: string; updated_by: string }; +export type Note = Record & { key: string; fact_status?: string | null; updated_at: string; updated_by: string }; export interface IssueCommand { id: number; batch_id: number; type: string; status: string; finished_at: string | null; error: string | null; diff --git a/web/src/issues/IssueCard.test.tsx b/web/src/issues/IssueCard.test.tsx index e6c25c9..f80c656 100644 --- a/web/src/issues/IssueCard.test.tsx +++ b/web/src/issues/IssueCard.test.tsx @@ -40,14 +40,15 @@ function overflow(scroll: number, client: number) { describe("карточка задачи", () => { beforeEach(() => setToken("tok")); - it("шапка: ключ, статусы, тема, исполнитель · срок · спринт · эпик по имени; ссылка в Jira", async () => { + it("шапка: ключ, статус Jira, тема, исполнитель · срок · спринт · эпик по имени; ссылка в Jira", async () => { mockFetch((_m, url) => url.pathname === "/settings" ? json(200, settings) : json(200, issue("AURORA-9001", { planned_end: "2020-01-10" }))); open("/issues/AURORA-9001"); expect(await screen.findByRole("heading", { level: 2, name: "Тема AURORA-9001" })).toBeInTheDocument(); const head = screen.getByTestId("issue-head"); expect(within(head).getByText("AURORA-9001")).toBeInTheDocument(); expect(within(head).getByText("В работе", { selector: ".badge.status" })).toBeInTheDocument(); - expect(within(head).getByText("факт: В работе", { selector: ".badge.fact" })).toHaveClass("fact-in-progress"); // как в макете раздела 1 + expect(head.querySelector(".badge.fact")).toBeNull(); // статуса по факту в карточке нет (hide-closed) + expect(within(head).queryByText(/факт:/)).toBeNull(); expect(within(head).getByTestId("issue-facts")).toHaveTextContent("Иванов Иван Иванович · срок 10.01 просрочено · AWPCA_Спринт 2 · эпик: Тестовый эпик"); expect(within(head).getByText("просрочено")).toHaveClass("overdue"); expect(screen.getByRole("link", { name: "Открыть в Jira ↗" })).toHaveAttribute("href", "https://sdlc.go.rshbank.ru/jira/browse/AURORA-9001"); @@ -383,14 +384,24 @@ describe("карточка задачи", () => { expect(screen.queryByRole("columnheader", { name: "Команда" })).toBeNull(); }); - it("T14.3: id подсказок «статуса по факту» уникален и связан с полем", async () => { + it("закрытая задача со сроком в прошлом — без пометки «просрочено»", async () => { + mockFetch((_m, url) => url.pathname === "/settings" ? json(200, settings) : json(200, issue("AURORA-9001", { planned_end: "2020-01-10", status: "Готово" }))); + open("/issues/AURORA-9001"); + const facts = await screen.findByTestId("issue-facts"); + expect(facts).toHaveTextContent("срок 10.01"); + expect(within(facts).queryByText("просрочено")).toBeNull(); + }); + + it("статуса по факту нет ни в чтении заметок, ни в форме", async () => { mockFetch((_m, url) => url.pathname === "/settings" ? json(200, settings) : json(200, issue("AURORA-9001"))); open("/issues/AURORA-9001"); - await userEvent.click(await screen.findByRole("button", { name: "Править" })); - const input = await screen.findByLabelText("Статус по факту"); - const id = input.getAttribute("list")!; - expect(id).not.toBe("fact-statuses"); - expect(document.getElementById(id)?.tagName).toBe("DATALIST"); + const view = await screen.findByRole("region", { name: "Служебные заметки" }); + expect(view).not.toHaveTextContent("Статус по факту"); + expect(view).toHaveTextContent("Вид работы"); + await userEvent.click(screen.getByRole("button", { name: "Править" })); + await screen.findByLabelText("Осталось"); + expect(screen.queryByLabelText("Статус по факту")).toBeNull(); + expect(document.querySelector("datalist")).toBeNull(); }); it("заметки в режиме чтения: пустые — «—», время и автор; «Отмена» без правок — сразу", async () => { @@ -425,7 +436,7 @@ describe("карточка задачи", () => { mockFetch((_m, url) => url.pathname === "/settings" ? json(200, settings) : json(200, issue("AURORA-9001", { note: null }))); open("/issues/AURORA-9001"); const view = await screen.findByRole("region", { name: "Служебные заметки" }); - expect(within(view).getAllByText("—")).toHaveLength(5); + expect(within(view).getAllByText("—")).toHaveLength(4); // вид работы, сделано, осталось, заметка владельца expect(within(view).getByRole("button", { name: "Править" })).toBeInTheDocument(); }); diff --git a/web/src/issues/IssueCard.tsx b/web/src/issues/IssueCard.tsx index b719366..14757b7 100644 --- a/web/src/issues/IssueCard.tsx +++ b/web/src/issues/IssueCard.tsx @@ -1,10 +1,11 @@ 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 { JIRA_URL } from "../board/constants.ts"; import { fmtDate, fmtStamp } from "../board/model/util.ts"; -import { factClass, groupLinks, isOverdue, todayISO } from "./card.ts"; +import { groupLinks, isOverdue, todayISO } from "./card.ts"; import { useCommentsHidden } from "./commentsPref.ts"; import { IssueLink } from "./IssueLink.tsx"; import { Notes } from "./Notes.tsx"; @@ -17,6 +18,7 @@ const DESC_LINES = 8; export function IssueCard({ issueKey: key, onClose }: { issueKey: string; onClose: () => void }) { const issue = useIssue(key); const settings = useSettings(); + const closed = useClosedStatuses(); const close = ; if (issue.isPending) return

загрузка {key}…

{close}
; if (issue.error instanceof ApiError && issue.error.status === 404) { @@ -32,13 +34,10 @@ export function IssueCard({ issueKey: key, onClose }: { issueKey: string; onClos ); } const i = issue.data!; - const labels = settings.data?.status_labels as Record | undefined; - const fact = i.note?.fact_status ?? null; - const cls = factClass(fact, labels); const due = dueOf(i); const facts = [ i.assignee && {i.assignee}, - due && срок {fmtDate(due)}{isOverdue(due, fact, labels, todayISO()) && <> просрочено}, + due && срок {fmtDate(due)}{isOverdue(due, i.status, closed, todayISO()) && <> просрочено}, i.sprint && {i.sprint}, i.epic_key && эпик: {i.epic_summary ?? i.epic_key}, ].filter(Boolean); @@ -48,7 +47,6 @@ export function IssueCard({ issueKey: key, onClose }: { issueKey: string; onClos
{i.key} {i.status && {i.status}} - {fact && факт: {fact}} Открыть в Jira ↗ {close} diff --git a/web/src/issues/NoteForm.tsx b/web/src/issues/NoteForm.tsx index d5fa280..e41e9df 100644 --- a/web/src/issues/NoteForm.tsx +++ b/web/src/issues/NoteForm.tsx @@ -1,4 +1,4 @@ -import { useEffect, useId, useMemo, useRef, useState } from "react"; +import { useEffect, useMemo, useRef, useState } from "react"; import { useBlocker } from "react-router"; import { useCloseAbort } from "./closeAbort.ts"; import { ApiError } from "../api/client.ts"; @@ -41,8 +41,6 @@ export function NoteForm({ issue, onDone }: { issue: IssueDetail; onDone: () => const kinds = Object.keys(settings.data?.kind_labels ?? {}); const kindOptions = values.work_kind && !kinds.includes(values.work_kind) ? [values.work_kind, ...kinds] : kinds; - const facts = Object.keys(settings.data?.status_labels ?? {}); - const listId = useId(); const set = (f: NoteField) => (e: { target: { value: string } }) => setValues((v) => ({ ...v, [f]: e.target.value })); const submit = () => save.mutate(changed, { @@ -60,10 +58,6 @@ export function NoteForm({ issue, onDone }: { issue: IssueDetail; onDone: () => {kindOptions.map((k) => )} - {TEXTS.map(([f, label]) => (