diff --git a/web/src/modules/jira/agile/BoardColumns.tsx b/web/src/modules/jira/agile/BoardColumns.tsx index 152edd3..e449574 100644 --- a/web/src/modules/jira/agile/BoardColumns.tsx +++ b/web/src/modules/jira/agile/BoardColumns.tsx @@ -1,15 +1,16 @@ -import type { ColumnView } from "./columns.ts"; +import { effectiveHidden, type ColumnView } from "./columns.ts"; import { AgileCard } from "./AgileCard.tsx"; -// колонки доски; контейнер прокручивается по горизонтали внутри себя, страница не растягивается -export function BoardColumns({ columns, lastCaption }: { columns: ColumnView[]; lastCaption?: string }) { +// колонки доски; контейнер прокручивается по горизонтали внутри себя; колонки одной высоты, у каждой прокручивается список +export function BoardColumns({ columns, lastCaption, hidden = [] }: { columns: ColumnView[]; lastCaption?: string; hidden?: string[] }) { const lastIdx = columns.reduce((acc, c, i) => (c.extra ? acc : i), -1); + const gone = effectiveHidden(columns.map((c) => c.name), hidden); return (
- {columns.map((c, i) => ( + {columns.map((c, i) => gone.includes(c.name) ? null : (
-

{c.name}

+

{c.name}

{c.issues.length}
{lastCaption && i === lastIdx &&

{lastCaption}

} diff --git a/web/src/modules/jira/agile/ColumnsMenu.tsx b/web/src/modules/jira/agile/ColumnsMenu.tsx new file mode 100644 index 0000000..438ec50 --- /dev/null +++ b/web/src/modules/jira/agile/ColumnsMenu.tsx @@ -0,0 +1,26 @@ +// меню «Колонки» экранов «Спринт» и «Kanban»: скрыть колонки доски; последнюю видимую скрыть нельзя +import { Badge, Checkbox, Dropdown, IconChevronDown } from "../../../ui/index.ts"; +import { effectiveHidden } from "./columns.ts"; + +export function ColumnsMenu({ names, hidden, onChange }: { names: string[]; hidden: string[]; onChange(hidden: string[]): void }) { + const gone = effectiveHidden(names, hidden); // имена, которых на доске больше нет, не показываются и не считаются + const visible = names.filter((n) => !gone.includes(n)); + const flip = (name: string, show: boolean) => onChange(names.filter((n) => (n === name ? !show : gone.includes(n)))); + return ( + ( + + )}> + {() => ( +
+ {names.map((n) => { + const on = !gone.includes(n); + return flip(n, e.target.checked)} />; + })} +
+ )} +
+ ); +} diff --git a/web/src/modules/jira/agile/KanbanPage.test.tsx b/web/src/modules/jira/agile/KanbanPage.test.tsx index 85eafb4..1e536b0 100644 --- a/web/src/modules/jira/agile/KanbanPage.test.tsx +++ b/web/src/modules/jira/agile/KanbanPage.test.tsx @@ -1,4 +1,5 @@ import { render, screen, within } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; import { createMemoryRouter } from "react-router"; import { beforeEach, describe, expect, it } from "vitest"; import { App } from "../../../App.tsx"; @@ -110,4 +111,48 @@ describe("«Kanban»", () => { expect(p.getByRole("link", { name: "Связи" })).toHaveAttribute("href", "/app/jira/links?project=AWPCA"); expect(urls).not.toContain("GET /agile"); }); + + it("поиск и исполнитель фильтруют колонки и «Бэклог (N)»; флажка «Скрыть выполненные» нет, меню «Колонки» есть", async () => { + const d = agile(true); + d.backlog = [iss("AURORA-1", { assignee: "Иванов" }), iss("AURORA-2")]; + d.board_issues = [iss("AURORA-3", { status: "В работе", assignee: "Иванов" }), iss("AURORA-4", { status: "Готово", done: true })]; + hub(d); + open("/app/jira/kanban?project=KAN"); + const p = await page(); + await within(p).findByText("Бэклог (2)"); + expect(within(p).queryByRole("checkbox", { name: "Скрыть выполненные" })).toBeNull(); + expect(within(p).getByRole("button", { name: /^Колонки/ })).toBeInTheDocument(); + await userEvent.selectOptions(within(p).getByLabelText("Исполнитель"), "Иванов"); + expect(within(p).getByText("Бэклог (1)")).toBeInTheDocument(); + expect(within(p).queryByText("Тема AURORA-4")).toBeNull(); + expect(within(p).getByText("Тема AURORA-3")).toBeInTheDocument(); + await userEvent.selectOptions(within(p).getByLabelText("Исполнитель"), ""); + await userEvent.type(within(p).getByRole("searchbox"), "AURORA-4"); + expect(within(p).getByText("Бэклог (0)")).toBeInTheDocument(); + expect(within(p).getByText("Тема AURORA-4")).toBeInTheDocument(); + expect(within(p).queryByText("Тема AURORA-3")).toBeNull(); + }); + + it("hideDone=true в хранилище на «Kanban» не применяется: выполненные остаются", async () => { + localStorage.setItem("sdlc-hub-agile", JSON.stringify({ KAN: { kanban: { hideDone: true } } })); + hub(agile(false)); + open("/app/jira/kanban?project=KAN"); + const p = await page(); + expect(await within(p).findByText("Тема AURORA-4")).toBeInTheDocument(); + expect(within(p).queryByRole("button", { name: "Сбросить фильтры" })).toBeNull(); + }); + + it("скрытая колонка: снять «Готово» — колонки нет; имя колонки, которой нет, в хранилище не ломает меню", async () => { + localStorage.setItem("sdlc-hub-agile", JSON.stringify({ KAN: { kanban: { hiddenColumns: ["Старая"] } } })); + hub(agile(false)); + open("/app/jira/kanban?project=KAN"); + const p = await page(); + await within(p).findByText("Тема AURORA-3"); + const btn = within(p).getByRole("button", { name: /^Колонки/ }); + expect(btn).toHaveTextContent(/^Колонки$/); + await userEvent.click(btn); + await userEvent.click(within(screen.getByRole("group", { name: "Колонки" })).getByRole("checkbox", { name: "Готово" })); + expect(colNames(p)).toEqual(["Бэклог", "В работе", "Без колонки"]); + expect(btn).toHaveTextContent("Колонки1"); + }); }); diff --git a/web/src/modules/jira/agile/KanbanPage.tsx b/web/src/modules/jira/agile/KanbanPage.tsx index 8dc910a..50fcd21 100644 --- a/web/src/modules/jira/agile/KanbanPage.tsx +++ b/web/src/modules/jira/agile/KanbanPage.tsx @@ -2,13 +2,20 @@ import type { Project } from "../../../api/types.ts"; import { PageHeader } from "../../../ui/index.ts"; import { RefreshButton } from "../RefreshButton.tsx"; import { AgileGate } from "./AgileGate.tsx"; +import { AgileToolbar } from "./AgileToolbar.tsx"; import { BoardColumns } from "./BoardColumns.tsx"; +import { ColumnsMenu } from "./ColumnsMenu.tsx"; import { columnsOf } from "./columns.ts"; +import { assigneeOptions, matches } from "./filters.ts"; import { AgileIssueRow } from "./IssueRow.tsx"; +import { useAgileView } from "./useAgileView.ts"; import "./agile.css"; -// «Kanban» (spec web-app): колонки доски; при включённом бэклоге Jira первая колонка не рисуется, её задачи — в «Бэклог (N)» (R-18) +// «Kanban» (spec web-app): колонки доски; при включённом бэклоге Jira первая колонка не рисуется, её задачи — в «Бэклог (N)» (R-18). +// «Скрыть выполненные» здесь нет: даже сохранённое hideDone не применяется (последняя колонка и так ограничена «за 14 дней») export function KanbanPage({ project }: { project: Project }) { + const { view, set, setQ, filter, reset } = useAgileView(project.label, "kanban"); + const kfilter = { ...filter, hideDone: false }; return (
} /> @@ -16,17 +23,22 @@ export function KanbanPage({ project }: { project: Project }) { {(d) => { const withBacklog = d.board?.kanban_backlog === true; const cols = d.board?.columns ?? null; - const issues = d.board_issues ?? []; // сервер уже убрал задачи первой колонки, если бэклог включён - const backlog = d.backlog ?? []; + const issues = (d.board_issues ?? []).filter((i) => matches(i, kfilter)); // сервер уже убрал задачи первой колонки, если бэклог включён + const backlog = (d.backlog ?? []).filter((i) => matches(i, kfilter)); + const views = columnsOf(withBacklog && cols ? cols.slice(1) : cols, issues); return ( <> + + c.name)} hidden={view.hiddenColumns} onChange={(hiddenColumns) => set({ hiddenColumns })} /> + {withBacklog && (
{`Бэклог (${backlog.length})`}
    {backlog.map((i) => )}
)} - +