docs: план доработок экранов доски (#112)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
5c7a845a6e
commit
ef9abfa8bd
323
docs/superpowers/plans/2026-10-25-agile-web-polish.md
Normal file
323
docs/superpowers/plans/2026-10-25-agile-web-polish.md
Normal file
@ -0,0 +1,323 @@
|
|||||||
|
# Доработки «Бэклога» и «Спринта» (#112) — план реализации
|
||||||
|
|
||||||
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||||
|
|
||||||
|
**Goal:** фильтры, свёртка, панель эпиков, выбор спринтов, скрытие колонок, значки типов и доска во всю высоту окна на
|
||||||
|
страницах «Бэклог», «Спринт», «Kanban» веб-приложения sdlc-hub — только просмотр.
|
||||||
|
|
||||||
|
**Architecture:** только фронт (`web/src/modules/jira/agile/`), данные — прежний `GET /agile`. Фильтры — чистая
|
||||||
|
модель `filters.ts` + запоминание `agilePref.ts` + хук `useAgileView`; страницы и компоненты только рисуют. Общие
|
||||||
|
части — панель фильтров `AgileToolbar`, меню `ColumnsMenu`, `BoardColumns`.
|
||||||
|
|
||||||
|
**Tech Stack:** React 19 + TypeScript + Vite, TanStack Query, Vitest + Testing Library, Playwright; UI-кит
|
||||||
|
`web/src/ui`, токены дизайн-системы AURA.
|
||||||
|
|
||||||
|
**Spec:** `openspec/changes/agile-web-polish/` (требование «Спринты и бэклог» в `specs/web-app/spec.md`, решения —
|
||||||
|
`design.md`, рулинги дизайна R-1…R-13; рулинги контроллера — с R-14).
|
||||||
|
|
||||||
|
## Global Constraints
|
||||||
|
|
||||||
|
- Веб только смотрит: MUST NOT появляться действий, меняющих Jira; ни одна кнопка не ставит команд.
|
||||||
|
- Сервер, API, схема, агент, MCP — не меняются (никаких правок вне `web/` и `openspec/`/`docs/`, кроме сида
|
||||||
|
`web/scripts/seed-hub.py`).
|
||||||
|
- Ключ хранилища — ровно `sdlc-hub-agile`; JSON `{ "<метка>": { "backlog"|"sprint"|"kanban": AgileView } }`; поиск
|
||||||
|
(`q`) и выбор спринтов не хранятся; любая ошибка хранилища — значения по умолчанию, без исключения наружу.
|
||||||
|
- Тексты интерфейса — дословно из спеки: «Ключ или тема», «Исполнитель», «все», «Без исполнителя», «Скрыть
|
||||||
|
выполненные», «Сбросить фильтры», «Эпики», «Все задачи», «Без эпика», «Эпик», «Колонки», «Спринты», «показано K»,
|
||||||
|
«Нет задач», «Нет задач по фильтрам», «Нет активного спринта», «Без колонки».
|
||||||
|
- Стили — только токены дизайн-системы (как в `agile.css`), без новых цветов-литералов.
|
||||||
|
- Работа только в worktree `/Users/andrey.epifancev/projects/jira-bridge/sdlc-hub/.wt/agile-polish`: никаких записей
|
||||||
|
в основной checkout и в другие репозитории (O-6 #108); никаких команд, ждущих stdin (`cat > файл` без heredoc и т.п.).
|
||||||
|
- Зелёный = `make check` целиком (pytest + `npm run lint` + `npm test`) и `cd web && npm run e2e` целиком.
|
||||||
|
|
||||||
|
## Review Focus
|
||||||
|
|
||||||
|
1. **Задача в двух выбранных спринтах** (перенос из активного в будущий) — на доске одна карточка, не две → тест в Task 2
|
||||||
|
(`sprintIssues`) и Task 6.
|
||||||
|
2. **Сохранённое значение, которого больше нет** (исполнитель ушёл, эпик закрыт и пропал, колонку переименовали в
|
||||||
|
Jira) — значение остаётся видимым и снимаемым, страница не пустеет молча → тесты в Task 2 (`assigneeOptions`), Task 5,
|
||||||
|
Task 7.
|
||||||
|
3. **Испорченное хранилище** (`sdlc-hub-agile` = `"oops"`, `{"AWPCA": 5}`, поле `collapsed: "x"`, `setItem` бросает)
|
||||||
|
— страница открывается со значениями по умолчанию → тесты в Task 2.
|
||||||
|
4. **Проект сменён на открытой странице** — фильтры другого проекта не протекают (страница пересоздаётся по `key`
|
||||||
|
проекта, состояние читается по метке) → тест в Task 4.
|
||||||
|
5. **Длинная колонка и узкое окно** — 900 px: страница без горизонтальной прокрутки, доска прокручивается внутри,
|
||||||
|
полоса прокрутки доски в окне; 2560 px — колонки используют ширину окна → e2e в Task 8.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 0: Окружение worktree (без коммита)
|
||||||
|
|
||||||
|
- [ ] `cd /Users/andrey.epifancev/projects/jira-bridge/sdlc-hub/.wt/agile-polish && uv sync && (cd web && npm ci)`
|
||||||
|
- [ ] `make check` — зелёный (база до правок; запомнить число тестов в ledger).
|
||||||
|
|
||||||
|
### Task 1: Формат SP и значки типов
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `web/src/modules/jira/agile/model.ts` (`fmtSp`, новое `typeKind`)
|
||||||
|
- Modify: `web/src/modules/jira/agile/TypeIcon.tsx`, `web/src/modules/jira/agile/agile.css`
|
||||||
|
- Modify: `web/src/ui/icons.tsx` (новые символы)
|
||||||
|
- Test: `web/src/modules/jira/agile/model.test.ts`, новый `web/src/modules/jira/agile/TypeIcon.test.tsx`
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Produces: `fmtSp(n: number | null): string`; `type TypeKind = "epic" | "story" | "task" | "bug" | "subtask" | "other"`;
|
||||||
|
`typeKind(type: string | null): TypeKind`; `isEpic(i: { issue_type: string | null }): boolean` — всё в `model.ts`.
|
||||||
|
Значки: `IconBookmark`, `IconCircle`, `IconSquares`, `IconDot` в `web/src/ui/icons.tsx`.
|
||||||
|
|
||||||
|
- [ ] **Step 1: тесты.** `model.test.ts`: `fmtSp(null) === "—"`, `fmtSp(3) === "3"`, `fmtSp(0.1 + 0.2) === "0.3"`,
|
||||||
|
`fmtSp(2.5) === "2.5"`, `fmtSp(1/3) === "0.33"`, `fmtSp(0) === "0"`. `typeKind`: «Эпик», «epic», «Фича» → `epic`; «История», «Story» → `story`; «Задача», «Task» →
|
||||||
|
`task`; «Ошибка», «BUG» → `bug`; «Подзадача», «Sub-task», «Subtask» → `subtask`; `null`, `""`, «Риск» → `other`.
|
||||||
|
`TypeIcon.test.tsx`: для каждого вида — `role="img"` с `aria-label` = исходное имя типа (для `null` — «Задача» не
|
||||||
|
подставлять: `aria-label="Тип не указан"`), класс `agile-type--<kind>`.
|
||||||
|
- [ ] **Step 2:** `cd web && npx vitest run src/modules/jira/agile/model.test.ts src/modules/jira/agile/TypeIcon.test.tsx` — FAIL.
|
||||||
|
- [ ] **Step 3: реализация.** `fmtSp`: `Number(n.toFixed(2))` → `String` (M-6). `typeKind` — сравнение без учёта
|
||||||
|
регистра со списками имён из спеки. Символы в `icons.tsx` тем же `make(...)`: закладка, круг, два квадрата, точка;
|
||||||
|
эпик — существующий `IconCritical` (молния), задача — `IconCheck`. `TypeIcon`: `<span className="agile-type
|
||||||
|
agile-type--<kind>">` — квадрат 16 px с `border-radius:var(--radius-xs)`, фон — токен вида (эпик `--k-ui`, история
|
||||||
|
`--color-status-success`, задача и подзадача `--color-action-primary`, ошибка `--color-status-error`, прочее —
|
||||||
|
`--color-text-secondary`), символ 12 px цвета `--color-text-on-primary`. Контраст символа проверить в светлой и
|
||||||
|
тёмной теме; не хватает — рулинг с выбранным токеном, не литерал.
|
||||||
|
- [ ] **Step 4:** тот же vitest — PASS; `npm run lint` — PASS.
|
||||||
|
- [ ] **Step 5:** коммит `feat(web): значки типов по образцу Jira и формат SP (#112, M-6)`.
|
||||||
|
|
||||||
|
### Task 2: Модель фильтров и запоминание
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `web/src/modules/jira/agile/filters.ts`, `web/src/modules/jira/agile/agilePref.ts`
|
||||||
|
- Test: `web/src/modules/jira/agile/filters.test.ts`, `web/src/modules/jira/agile/agilePref.test.ts`
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Consumes: `isEpic`, `typeKind` (Task 1); типы `AgileIssue`, `AgileSprint`, `AgileResponse` (`../api/types.ts`).
|
||||||
|
- Produces (`filters.ts`):
|
||||||
|
- `type AgilePage = "backlog" | "sprint" | "kanban"`
|
||||||
|
- `interface AgileView { assignee: string; epic: string; hideDone: boolean; collapsed: string[]; hiddenColumns: string[]; epicsPanel: boolean }`
|
||||||
|
- `const DEFAULT_VIEW: AgileView` = `{ assignee: "", epic: "", hideDone: false, collapsed: [], hiddenColumns: [], epicsPanel: true }`
|
||||||
|
- `const NONE = "-"` — значение «Без исполнителя» / «Без эпика»
|
||||||
|
- `interface IssueFilter { q: string; assignee: string; epic: string; hideDone: boolean }`
|
||||||
|
- `matches(i: AgileIssue, f: IssueFilter): boolean` — И всех условий; `q` — подстрока без учёта регистра в `key` или `summary` (пробелы по краям `q` отбрасываются)
|
||||||
|
- `hasFilters(f: IssueFilter): boolean`
|
||||||
|
- `type Opt = { value: string; label: string }`
|
||||||
|
- `assigneeOptions(issues: AgileIssue[], current: string): Opt[]` — `["", "все"]`, `[NONE, "Без исполнителя"]`, затем уникальные исполнители по `localeCompare("ru")`; `current`, которого нет, добавляется в конец с подписью = значению
|
||||||
|
- `interface EpicEntry { key: string; title: string; count: number; done: boolean }`
|
||||||
|
- `epicEntries(d: AgileResponse): EpicEntry[]` — по спеке: строки-эпики из всех спринтов и бэклога по `rank` (без повторов), затем ключи из `epic_key`, строк которых нет, по ключу (`title` = ключ, `done` = false); `count` — задачи-не-эпики с этим `epic_key` в активных, будущих спринтах и бэклоге (без повторов по ключу)
|
||||||
|
- `byRank(a: AgileIssue, b: AgileIssue): number` — `rank` строкой, `null` в конце, затем по `key`
|
||||||
|
- `sprintIssues(sprints: AgileSprint[]): AgileIssue[]` — объединение без повторов по `key`, сортировка `byRank`
|
||||||
|
- `selectableSprints(sprints: AgileSprint[]): AgileSprint[]` — активные (в порядке входа), затем будущие по `start_date` (через `jiraDate`), без даты — в конце
|
||||||
|
- `parseSprintIds(param: string | null, sprints: AgileSprint[]): number[]` — id из `"101,102"`, только среди `selectableSprints`, порядок `selectableSprints`; пусто → id всех активных
|
||||||
|
- Produces (`agilePref.ts`): `const AGILE_KEY = "sdlc-hub-agile"`; `readView(label: string, page: AgilePage): AgileView`;
|
||||||
|
`saveView(label: string, page: AgilePage, v: AgileView): void` (не трогает другие страницы и проекты в JSON).
|
||||||
|
|
||||||
|
- [ ] **Step 1: тесты `filters.test.ts`** (фабрики `iss`/`sprint` — как в `BacklogPage.test.tsx`): `matches` по каждому
|
||||||
|
условию и их И (`q: " aurora-1 "` находит `AURORA-1` и `AURORA-10`; `q: "тема"` — по теме; `assignee: NONE` — только
|
||||||
|
`null`; `epic: NONE` — только `epic_key === null`; `hideDone`); `hasFilters` для пустого и каждого поля;
|
||||||
|
`assigneeOptions` — порядок, «Без исполнителя», ушедший `current` в конце; `epicEntries` на данных сценария «Фильтр
|
||||||
|
по эпику» спеки (`AURORA-10`: 3 в бэклоге + 2 в активном → `count` 5), эпик, на который ссылаются, но строки нет,
|
||||||
|
— в конце с `title` = ключ, задачи прошлых спринтов не считаются; `sprintIssues` — задача в двух спринтах одна,
|
||||||
|
`rank: null` в конце; `selectableSprints` — закрытые отброшены, будущий без даты последним; `parseSprintIds` —
|
||||||
|
`"102,999"` → `[102]`, `"abc"` → все активные, `null` → все активные, будущий `"103"` → `[103]`.
|
||||||
|
- [ ] **Step 2: тесты `agilePref.test.ts`:** нет ключа → `DEFAULT_VIEW`; запись `AWPCA/backlog` не меняет
|
||||||
|
`AWPCA/sprint` и `STOCKS/backlog`; мусор: `"oops"`, `"[1]"`, `{"AWPCA": 5}`, `{"AWPCA":{"backlog":{"collapsed":"x","hideDone":"yes"}}}`
|
||||||
|
→ по полю значения по умолчанию (`collapsed: []`, `hideDone: false`), годные поля сохраняются; `localStorage.setItem`
|
||||||
|
бросает (`vi.spyOn(Storage.prototype, "setItem")`) — `saveView` не бросает; `getItem` бросает — `readView` →
|
||||||
|
`DEFAULT_VIEW`.
|
||||||
|
- [ ] **Step 3:** `npx vitest run src/modules/jira/agile/filters.test.ts src/modules/jira/agile/agilePref.test.ts` — FAIL.
|
||||||
|
- [ ] **Step 4: реализация** по Interfaces; разбор хранилища — поле за полем с проверкой типа (строка, булево, массив строк).
|
||||||
|
- [ ] **Step 5:** тот же vitest — PASS; `npm run lint` — PASS.
|
||||||
|
- [ ] **Step 6:** коммит `feat(web): модель фильтров экранов доски и их запоминание (#112)`.
|
||||||
|
|
||||||
|
### Task 3: Хук состояния и панель фильтров
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `web/src/modules/jira/agile/useAgileView.ts`, `web/src/modules/jira/agile/AgileToolbar.tsx`
|
||||||
|
- Modify: `web/src/modules/jira/agile/agile.css`
|
||||||
|
- Test: `web/src/modules/jira/agile/AgileToolbar.test.tsx`
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Consumes: Task 2 целиком.
|
||||||
|
- Produces:
|
||||||
|
- `useAgileView(label: string, page: AgilePage): { view: AgileView; set(patch: Partial<AgileView>): void; q: string; setQ(v: string): void; filter: IssueFilter; reset(): void }`
|
||||||
|
— `set` пишет в хранилище (`saveView`) вне функции-обновителя (как T8.1 #69); `reset` очищает `q`, `assignee`,
|
||||||
|
`epic`, `hideDone` и сохраняет остальное.
|
||||||
|
- `AgileToolbar({ filter, setQ, set, onReset, assignees, showHideDone, children }: { filter: IssueFilter; setQ(v: string): void; set(patch: Partial<AgileView>): void; onReset(): void; assignees: Opt[]; showHideDone: boolean; children?: ReactNode })`
|
||||||
|
— `Panel` с `SearchInput label="Поиск по ключу или теме" placeholder="Ключ или тема"`, `Select aria-label="Исполнитель"`,
|
||||||
|
при `showHideDone` — `Checkbox label="Скрыть выполненные"`, слот `children` (кнопка «Эпики», выбор спринтов, меню
|
||||||
|
«Колонки»), кнопка «Сбросить фильтры» только при `hasFilters(filter)`. Без кнопок, ставящих команды.
|
||||||
|
|
||||||
|
- [ ] **Step 1: тесты `AgileToolbar.test.tsx`** (рендер с хуком в обёртке): ввод в поиск меняет `filter.q`; выбор
|
||||||
|
исполнителя сохраняется в `localStorage["sdlc-hub-agile"]` по метке и странице, `q` — нет; «Сбросить фильтры»
|
||||||
|
появляется после ввода и снимает всё; `showHideDone={false}` — флажка нет; повторный монтаж с той же меткой и
|
||||||
|
страницей — исполнитель и флажок восстановлены.
|
||||||
|
- [ ] **Step 2:** `npx vitest run src/modules/jira/agile/AgileToolbar.test.tsx` — FAIL.
|
||||||
|
- [ ] **Step 3: реализация**; стили `.agile-toolbar` (flex-wrap, gap `--space-3`, поиск растягивается) в `agile.css`.
|
||||||
|
- [ ] **Step 4:** тот же vitest — PASS; `npm run lint` — PASS.
|
||||||
|
- [ ] **Step 5:** коммит `feat(web): панель фильтров экранов доски (#112)`.
|
||||||
|
|
||||||
|
### Task 4: «Бэклог»: фильтры, свёртка секций, без строк эпиков
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `web/src/modules/jira/agile/BacklogPage.tsx`, `web/src/modules/jira/agile/agile.css`
|
||||||
|
- Test: `web/src/modules/jira/agile/BacklogPage.test.tsx`
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Consumes: `useAgileView(label, "backlog")`, `AgileToolbar`, `matches`, `isEpic`, `assigneeOptions` (Tasks 1–3).
|
||||||
|
- Produces: `SprintSection`/секция «Бэклог» принимают `collapsed: boolean; onToggle(): void; shown: AgileIssue[]` —
|
||||||
|
Task 5 добавляет фильтр эпика через тот же `filter`.
|
||||||
|
|
||||||
|
- [ ] **Step 1: тесты** (дописать в `BacklogPage.test.tsx`, существующие оставить зелёными):
|
||||||
|
- заголовок секции — кнопка с `aria-expanded="true"`; щелчок — `false`, задач секции нет, итоги видны; после
|
||||||
|
повторного монтажа (`unmount` + `open`) секция свёрнута (сценарий «Свёрнутый активный спринт»); свернуть можно и
|
||||||
|
будущий, и «Бэклог»;
|
||||||
|
- эпик (`issue_type: "Фича"`) в бэклоге и в спринте — строкой не показан; «N задач» секции «Бэклог» его не считает;
|
||||||
|
- «Скрыть выполненные» на спринте из 10 задач (4 выполнены, `count: 10`): 6 строк, «10 задач» и «показано 6»;
|
||||||
|
прошлый спринт раскрыт — все его задачи (сценарий «Скрыть выполненные»);
|
||||||
|
- поиск «AURORA-50» — в «Бэклоге» одна строка, в секции спринта «Нет задач по фильтрам»;
|
||||||
|
- смена проекта (Review Focus 4): исполнитель выбран у `AWPCA`, открыт тот же адрес с другим scrum-проектом —
|
||||||
|
исполнитель «все».
|
||||||
|
- «Бэклог только смотрит»: на странице нет элементов с `draggable`, все кнопки — свёртка, панель фильтров или
|
||||||
|
раскрытие (перечислить по `getAllByRole("button")` и их именам).
|
||||||
|
- [ ] **Step 2:** `npx vitest run src/modules/jira/agile/BacklogPage.test.tsx` — новые FAIL.
|
||||||
|
- [ ] **Step 3: реализация.** Видимые задачи секции = `issues.filter(i => !isEpic(i) && matches(i, f))`; для прошлых
|
||||||
|
спринтов `hideDone` не применяется (`{...f, hideDone: false}`). «показано K» — когда `shown.length` меньше числа
|
||||||
|
задач-не-эпиков секции. Заголовок — `<button className="agile-section__toggle" aria-expanded aria-controls>` с
|
||||||
|
`IconChevronDown`/`IconChevronUp`; id секции в `collapsed` — `String(sprint.id)` и `"backlog"`.
|
||||||
|
- [ ] **Step 4:** тот же vitest — PASS; `npm run lint` — PASS.
|
||||||
|
- [ ] **Step 5:** коммит `feat(web): «Бэклог» — фильтры, свёртка секций, эпики не строками (#112)`.
|
||||||
|
|
||||||
|
### Task 5: Панель «Эпики» на «Бэклоге»
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `web/src/modules/jira/agile/EpicsPanel.tsx`
|
||||||
|
- Modify: `web/src/modules/jira/agile/BacklogPage.tsx`, `web/src/modules/jira/agile/agile.css`
|
||||||
|
- Test: `web/src/modules/jira/agile/BacklogPage.test.tsx`
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Consumes: `epicEntries`, `EpicEntry`, `NONE`, `useAgileView` (Task 2–3), страница из Task 4.
|
||||||
|
- Produces: `EpicsPanel({ entries, value, onChange, hideDone }: { entries: EpicEntry[]; value: string; onChange(v: string): void; hideDone: boolean })`
|
||||||
|
и `EpicSelect` с теми же пропсами (`Select aria-label="Эпик"`: «Все задачи», эпики, «Без эпика»).
|
||||||
|
|
||||||
|
- [ ] **Step 1: тесты:**
|
||||||
|
- сценарий «Фильтр по эпику» спеки дословно (3 + 5 в бэклоге, 2 в спринте; выбран `AURORA-10` → в спринте 2 и
|
||||||
|
«показано 2», в «Бэклоге» 3, строки эпика нет, у пункта в панели 5);
|
||||||
|
- пункт — кнопка с `aria-pressed`; повторный щелчок по выбранному — «Все задачи» (`aria-pressed="true"`);
|
||||||
|
- «Без эпика» — только задачи с `epic_key: null`;
|
||||||
|
- эпик, на который ссылаются, но строки нет, — пункт с ключом; фильтр по нему работает;
|
||||||
|
- «Скрыть выполненные»: выполненный эпик пропадает из панели, кроме выбранного (Review Focus 2);
|
||||||
|
- сохранённый эпик, которого нет в данных, остаётся выбранным пунктом (по ключу), секции — «Нет задач по фильтрам»;
|
||||||
|
- кнопка «Эпики» в панели фильтров (`aria-pressed`) скрывает и показывает панель, состояние переживает повторный монтаж;
|
||||||
|
- фильтр эпика применяется и к задачам прошлого спринта.
|
||||||
|
- [ ] **Step 2:** vitest файла — новые FAIL.
|
||||||
|
- [ ] **Step 3: реализация.** Раскладка `.agile-backlog`: сетка `240px minmax(0,1fr)` (без панели — одна колонка);
|
||||||
|
при `@container page (max-width: 899px)` панель скрыта, в панели фильтров показан `EpicSelect` (на широком —
|
||||||
|
скрыт); кнопка «Эпики» на узком скрыта. Пункт: тема (или ключ), ключ моноширинным, число.
|
||||||
|
- [ ] **Step 4:** vitest файла — PASS; `npm run lint` — PASS.
|
||||||
|
- [ ] **Step 5:** коммит `feat(web): панель «Эпики» на «Бэклоге» (#112)`.
|
||||||
|
|
||||||
|
### Task 6: «Спринт»: выбор спринтов и фильтры
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `web/src/modules/jira/agile/SprintPicker.tsx`
|
||||||
|
- Modify: `web/src/modules/jira/agile/SprintPage.tsx`
|
||||||
|
- Test: `web/src/modules/jira/agile/SprintPage.test.tsx`
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Consumes: `selectableSprints`, `parseSprintIds`, `sprintIssues`, `matches`, `useAgileView(label, "sprint")`, `AgileToolbar`.
|
||||||
|
- Produces: `SprintPicker({ sprints, selected, onChange }: { sprints: AgileSprint[]; selected: number[]; onChange(ids: number[]): void })`
|
||||||
|
— `Dropdown` с кнопкой «Спринты» (на кнопке — имя единственного выбранного или «Спринты: N»), внутри `Checkbox` по
|
||||||
|
спринту (имя, «активный»/«будущий»); снять последнюю отметку нельзя (`disabled`). `BoardColumns` получает
|
||||||
|
отфильтрованные задачи (Task 7 добавит `hidden`).
|
||||||
|
|
||||||
|
- [ ] **Step 1: тесты** (переписать «два активных: переключатель…», «один активный: переключателя нет», «устаревший
|
||||||
|
?sprint=», «мусорный ?sprint=abc» под новую спеку; остальные — оставить):
|
||||||
|
- по умолчанию выбраны все активные; два активных → два заголовка и задачи обоих;
|
||||||
|
- сценарий «Будущий спринт на доске»: отметить будущий 102 при активном 101 → адрес `?sprint=101,102` без новой
|
||||||
|
записи истории (`router.state.historyAction === "REPLACE"`), два заголовка, «осталось N дн.» только у 101;
|
||||||
|
- задача в обоих выбранных спринтах — одна карточка (Review Focus 1);
|
||||||
|
- `?sprint=999` и `?sprint=abc` → все активные; `?sprint=103` (будущий) → только он;
|
||||||
|
- нет активного, есть будущий → «Нет активного спринта», ссылка «Бэклог» и выбор спринтов доступен; выбор будущего
|
||||||
|
показывает доску;
|
||||||
|
- сценарий «Исполнитель на доске спринта»: выбран исполнитель → только его карточки, числа колонок — по видимым; на
|
||||||
|
«Бэклоге» (`open("/jira/backlog?…")`) исполнитель «все»;
|
||||||
|
- «Скрыть выполненные» убирает выполненные карточки.
|
||||||
|
- [ ] **Step 2:** vitest файла — новые FAIL.
|
||||||
|
- [ ] **Step 3: реализация** по Interfaces; заголовки — `SprintHead` на каждый выбранный, компактно (`agile-section__head`),
|
||||||
|
«осталось…»/«срок прошёл» — только у `state === "active"`. Прежний `Segmented` убрать.
|
||||||
|
- [ ] **Step 4:** vitest файла — PASS; `npm run lint` — PASS.
|
||||||
|
- [ ] **Step 5:** коммит `feat(web): «Спринт» — выбор активных и будущих спринтов, фильтры (#112)`.
|
||||||
|
|
||||||
|
### Task 7: Колонки доски: скрытие, одна высота, прокрутка; «Kanban»
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `web/src/modules/jira/agile/ColumnsMenu.tsx`
|
||||||
|
- Modify: `web/src/modules/jira/agile/BoardColumns.tsx`, `SprintPage.tsx`, `KanbanPage.tsx`, `agile.css`
|
||||||
|
- Test: `web/src/modules/jira/agile/SprintPage.test.tsx`, `KanbanPage.test.tsx`, `columns.test.ts` при необходимости
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Consumes: `ColumnView`/`columnsOf` (без изменений), `useAgileView(label, "sprint" | "kanban")`, `AgileToolbar`.
|
||||||
|
- Produces: `ColumnsMenu({ names, hidden, onChange }: { names: string[]; hidden: string[]; onChange(hidden: string[]): void })`
|
||||||
|
— `Dropdown` с кнопкой «Колонки» (+ `Badge` с числом скрытых, если > 0), отметки по именам; последняя видимая —
|
||||||
|
`disabled`; имя из `hidden`, которого нет в `names`, в меню не показывается и на `Badge` не считается.
|
||||||
|
`BoardColumns({ columns, lastCaption, hidden }: { columns: ColumnView[]; lastCaption?: string; hidden?: string[] })`.
|
||||||
|
|
||||||
|
- [ ] **Step 1: воспроизвести баг п. 10** (superpowers:systematic-debugging): тестовый хаб (`bash web/scripts/test-hub.sh`
|
||||||
|
в фоне + `npm run dev` или собранное приложение) с широкой доской из Task 8 Step 1 (если её ещё нет — временно в
|
||||||
|
своём сиде), Playwright-скрипт в scratchpad: окно 2560 × 1300, замерить у `.page`, `.agile-columns` ширину и
|
||||||
|
`getBoundingClientRect().bottom` против `innerHeight`. Записать причину в ledger. Ожидаемая по design #9 и R-12:
|
||||||
|
полоса прокрутки ниже окна и ширина страницы ограничена `max-width:1440px` у `.page`. Если причина другая — чинить
|
||||||
|
её, требование спеки то же.
|
||||||
|
- [ ] **Step 2: тесты:** сценарий «Скрытая колонка» (снять «Готово» → колонки нет, на «Колонки» — 1, после повторного
|
||||||
|
монтажа так же); последнюю видимую снять нельзя; заголовки колонок — `getAllByRole("heading", { level: 3 })` (T10.3);
|
||||||
|
«Kanban»: поиск и исполнитель фильтруют колонки и «Бэклог (N)», флажка «Скрыть выполненные» нет, меню «Колонки» есть;
|
||||||
|
скрытое имя колонки, которой больше нет, не ломает меню.
|
||||||
|
- [ ] **Step 3:** vitest файлов — новые FAIL.
|
||||||
|
- [ ] **Step 4: реализация.** Скрытие — фильтр `columns` по имени в `BoardColumns`. Раскладка (design #9, R-12):
|
||||||
|
страница доски (`.agile--wide`) без ограничения `max-width` у `.page` (`.page:has(> .agile--wide)` или класс от
|
||||||
|
страницы — выбрать и записать рулинг), доска занимает высоту окна под шапкой и панелью фильтров (`min-height:360px`;
|
||||||
|
на экране < 640 px учесть закреплённую строку «Меню»), `.agile-columns{align-items:stretch}`, у колонки
|
||||||
|
прокручивается `.agile-col__list` (`overflow-y:auto; min-height:0`). Горизонтальная прокрутка — у `.agile-columns`.
|
||||||
|
- [ ] **Step 5:** vitest файлов — PASS; `npm run lint` — PASS; скрипт из Step 1 — полоса прокрутки в окне, последняя
|
||||||
|
колонка достижима, колонки одной высоты.
|
||||||
|
- [ ] **Step 6:** коммит `fix(web): колонки доски — одна высота, прокрутка в окне, скрытие колонок (#112, T10.3)`.
|
||||||
|
|
||||||
|
### Task 8: e2e, сид и попутные тесты hub#111
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `web/scripts/seed-hub.py`, `web/e2e/agile.spec.ts`
|
||||||
|
- Test: `web/src/modules/jira/agile/AgileGate.test.tsx` (новый), `web/src/shell/nav.test.ts` или `KanbanPage.test.tsx`
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Consumes: всё выше; сид — прежний формат `boards` в `mirror.apply_state`.
|
||||||
|
|
||||||
|
- [ ] **Step 1: сид.** Проект `WIDE_E2E` (scrum-доска 6100): 11 колонок по своим статусам + задачи со статусом вне
|
||||||
|
колонок («Без колонки»), активный спринт, 40 задач в одной колонке, по 1–3 в остальных; эпики для AWPCA уже есть в
|
||||||
|
фикстуре (`AURORA-9100` «Фича», ссылки на отсутствующие `AURORA-1359`, `AURORA-9999`). Фиксированный `jira_updated`
|
||||||
|
у задач канбана вместо «сегодня 12:00», если от него не зависит «за 14 дней» (T12.2; если зависит — дата
|
||||||
|
`today - 1 day` и комментарий). Проверить, что прочие e2e (`projects.spec.ts`, `overview.spec.ts`) не считают проекты
|
||||||
|
жёстко; если считают — поправить ожидание.
|
||||||
|
- [ ] **Step 2: e2e** (`agile.spec.ts`):
|
||||||
|
- «Широкая доска» по спеке: окно 2560 × 1300, `WIDE_E2E` — высоты всех `.agile-col` равны (±1 px), нижний край
|
||||||
|
`.agile-columns` ≤ `innerHeight`, `scrollWidth > clientWidth` у доски, после `scrollLeft = scrollWidth` колонка
|
||||||
|
«Без колонки» целиком в окне, `document.documentElement.scrollWidth ≤ clientWidth`, ширина `.agile-columns` больше
|
||||||
|
1440 px;
|
||||||
|
- 900 px — прежний тест плюс нижний край доски ≤ `innerHeight`;
|
||||||
|
- «Бэклог» AWPCA: панель «Эпики» с `AURORA-9100`, выбор — строки только его задач; свёртка секции переживает
|
||||||
|
перезагрузку;
|
||||||
|
- «Спринт» AWPCA: отметить «AWPCA_Спринт 3» → адрес `sprint=102,103`; скрыть «Готово» → после перезагрузки скрыта;
|
||||||
|
- T12.3/T12.4: в существующих тестах файла — локаторы по роли и точному тексту вместо нестрогих `getByText` и
|
||||||
|
`.last()`/`.first()` по порядку DOM.
|
||||||
|
- [ ] **Step 3: T9.4** — vitest `AgileGate.test.tsx`: «Загрузка…» пока `/agile` висит; ошибка `/agile` → текст ошибки и
|
||||||
|
«Повторить», щелчок повторяет запрос; меню kanban-проекта: «Kanban» есть, «Бэклог»/«Спринт» нет.
|
||||||
|
- [ ] **Step 4:** `cd web && npm run e2e` — весь набор PASS; `make check` (из корня worktree) — PASS.
|
||||||
|
- [ ] **Step 5:** коммит `test(web): e2e широкой доски и фильтров, сид WIDE_E2E, тесты AgileGate (#112, T9.4, T12.2–T12.4)`.
|
||||||
|
|
||||||
|
### Task 9: Документы
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `openspec/changes/agile-web-polish/design.md` (рулинги контроллера R-13… в «Decisions made without discussion»),
|
||||||
|
`CLAUDE.md` / `ARCHITECTURE.md` — только если упоминают состав `agile/` или ключи `localStorage` (проверить grep).
|
||||||
|
|
||||||
|
- [ ] `grep -n "agile\|sdlc-hub-legend" CLAUDE.md ARCHITECTURE.md` — дописать `sdlc-hub-agile` и новые файлы, где
|
||||||
|
перечислены соседние; `openspec validate agile-web-polish --strict` — valid.
|
||||||
|
- [ ] коммит `docs: доработки экранов доски — рулинги и карта (#112)`.
|
||||||
|
|
||||||
|
**После плана (сессия, не SDD):** перенести новые значки и стиль `agile-type` в kit скилла `aura-design`
|
||||||
|
(вне worktree — только руками сессии), PR в Gitea, выкатка `make web` + kickstart — за владельцем.
|
||||||
@ -121,3 +121,8 @@ discussion»).
|
|||||||
merge остаётся открытой с комментарием о закрытых маркерах. Цена ошибки — взять/отложить другие пункты.
|
merge остаётся открытой с комментарием о закрытых маркерах. Цена ошибки — взять/отложить другие пункты.
|
||||||
- R-11 (spec): узкий экран — панель эпиков превращается в выбор при контейнере уже 900 px. Цена ошибки — подобрать
|
- R-11 (spec): узкий экран — панель эпиков превращается в выбор при контейнере уже 900 px. Цена ошибки — подобрать
|
||||||
порог.
|
порог.
|
||||||
|
- R-12 (plan, Task 7): на страницах доски («Спринт», «Kanban») страница не ограничена `max-width: 1440px` у `.page` —
|
||||||
|
на 2560 px колонки используют ширину окна (комментарий в `agile.css` уже называл «Спринт» страницей во всю ширину, а
|
||||||
|
ограничение оставалось). Обоснование: замечание владельца о 2560 px. Цена ошибки — вернуть ограничение одной строкой.
|
||||||
|
- R-13 (plan, Task 1): значок задачи без типа подписан «Тип не указан» (сейчас — «Задача»), вид — нейтральный.
|
||||||
|
Цена ошибки — вернуть прежнюю подпись.
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user