arm-core/docs/superpowers/plans/2026-10-27-document-comments-ui.md
Andrey Epifancev 07648ba3ac AURORA-1683 OpenSpec document-comments-ui и план
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 00:20:39 +04:00

116 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Комментарии к документу в UI — план реализации
> **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:** список комментариев документа на экране проверки [41] (боковая панель) и счётчик с поповером в панели «Первичные документы» [39-01].
**Architecture:** только фронт `web/`; API ядра готов (`GET`/`POST /documents/{id}/comments`, `comments_count`). Общий компонент `CommentsList` используют боковая панель экрана документа и поповер строки панели. Чистые функции (форматирование времени, строки панели) — с тестами vitest; компоненты проверяются в браузере.
**Tech Stack:** React 18, TS strict, antd 6, vitest, biome + ESLint + Prettier.
**Spec:** `openspec/changes/document-comments-ui/` (proposal, design D-1…D-5, дельты `document-review-ui`, `primary-files-ui`).
**Ветка:** `feat/180-document-comments` (worktree `/Users/andrey.epifancev/projects/awp-ca/.wt/doc-comments/arm-core`), issue RSHB/arm-core#180, Jira AURORA-1683.
## Global Constraints
- Тексты UI: «Комментарии (N)», «Комментарии к документу», «Комментариев нет», «Не удалось загрузить комментарии», «Добавить», «Комментарии к документу <имя файла>», «Комментарий отсутствует», «Закрыть».
- Время — по Москве, «dd.mm.yyyy hh:mm».
- Текст комментария — до 4000 символов; пустой после обрезки не отправляется.
- Правок и удаления комментариев нет; бэкенд и DBML не меняются.
- Стиль: табы, одинарные кавычки, ширина 140 (biome); комментарии в коде — по-русски.
## Review Focus
- Клик по кнопке комментариев и внутри поповера в строке панели не должен открывать документ (всплытие к `onRow.onClick`) — проверка в браузере, Task 3.
- Длинный текст без пробелов и многострочный текст не ломают ширину панели/поповера (`pre-wrap` + `overflow-wrap: anywhere`) — проверка в браузере, Task 4.
- Ответ на запрос списка от прошлого открытия, пришедший после закрытия или переоткрытия, не должен перетирать текущий (счётчик запросов, как в `FileWarningsPopover`) — Task 2/3.
- Ошибка добавления (403/422) оставляет текст в поле — Task 2, проверка в браузере.
- Ядро без `comments_count` в документе — счётчик 0 (тест в Task 1).
---
### Task 1: Данные — тип, запрос, время, счётчик строки
**Files:**
- Modify: `web/src/types/api.ts` (после `DocumentBrief`)
- Modify: `web/src/services/api/documents.ts`
- Modify: `web/src/utils/dates.ts`, Test: `web/src/utils/dates.test.ts`
- Modify: `web/src/widgets/FilesDrawer/documentRows.ts`, Test: `web/src/widgets/FilesDrawer/documentRows.test.ts`
**Interfaces:**
- Produces: `interface CommentOut { id: string; text: string; author: { id: string; full_name: string }; created_at: string }`;
`getComments(documentId: string): Promise<CommentOut[]>`; `postComment` типизируется `api.post<CommentOut>`;
`formatMoscowDateTime(iso: string | null | undefined): string | null`; `DocumentRow.commentsCount: number`.
- [ ] **Step 1: Падающие тесты**
```ts
describe('formatMoscowDateTime', () => {
it('момент из API (UTC) — дата и время по Москве', () => {
expect(formatMoscowDateTime('2026-10-07T23:11:42.981145Z')).toBe('08.10.2026 02:11');
expect(formatMoscowDateTime('2024-03-12T10:05:00Z')).toBe('12.03.2024 13:05');
});
it('пусто и мусор', () => {
expect(formatMoscowDateTime(null)).toBeNull();
expect(formatMoscowDateTime('не дата')).toBeNull();
});
});
```
В `documentRows.test.ts`: `buildDocumentRows([file({ documents: [doc({ comments_count: 2 })] })])[0].commentsCount === 2`;
строка файла без документов — `commentsCount === 0`; документ без поля (`{ ...doc(), comments_count: undefined } as unknown as DocumentBrief`) — 0.
- [ ] **Step 2:** `cd web && npx vitest run src/utils/dates.test.ts src/widgets/FilesDrawer/documentRows.test.ts` — FAIL (нет функции/поля).
- [ ] **Step 3: Реализация.** `formatMoscowDateTime` — `Intl.DateTimeFormat('ru-RU', { timeZone: 'Europe/Moscow', day, month, year, hour: '2-digit', minute: '2-digit', hourCycle: 'h23' })`, разделитель «, » из `Intl` заменить пробелом. `commentsCount: d.comments_count ?? 0`, у строки файла — 0. `getComments` — `api.get<CommentOut[]>(\`/documents/${id}/comments\`)`.
- [ ] **Step 4:** тот же vitest — PASS; `npx tsc --noEmit` — без ошибок.
- [ ] **Step 5: Commit** «AURORA-1683 комментарии: тип, запрос, время по Москве, счётчик строки панели».
### Task 2: Экран документа — кнопка «Комментарии (N)» и боковая панель
**Files:**
- Create: `web/src/widgets/DocumentComments/CommentsList.tsx`
- Create: `web/src/widgets/DocumentComments/useDocumentComments.ts`
- Create: `web/src/widgets/DocumentComments/DocumentCommentsDrawer.tsx`
- Modify: `web/src/layouts/DocumentPage.tsx:1-136`
- Delete: `web/src/widgets/DocumentReview/CommentModal.tsx`
**Interfaces:**
- Consumes: `CommentOut`, `getComments`, `postComment`, `formatMoscowDateTime` (Task 1).
- Produces: `type CommentsState = { status: 'loading' } | { status: 'error' } | { status: 'done'; comments: CommentOut[] }`;
`CommentsList({ state, emptyText }: { state: CommentsState | null; emptyText: string })`;
`useDocumentComments(documentId: string | null): { state: CommentsState | null; load: () => void; reset: () => void }` — `load` запрашивает список, ответы устаревших запросов игнорируются (счётчик в `useRef`), `reset` сбрасывает в `null` и отменяет ожидаемый ответ;
`DocumentCommentsDrawer({ documentId, open, onClose, onAdded }: { documentId: string; open: boolean; onClose: () => void; onAdded: () => void })`.
- [ ] **Step 1:** `CommentsList`: `null`/loading — `Spin`; error — «Не удалось загрузить комментарии» (цвет `#cf1322`); пусто — `emptyText` (цвет `#8c8c8c`); иначе на комментарий блок: ФИО жирным + время `formatMoscowDateTime` (secondary, 12px) в строке, текст ниже с `whiteSpace: 'pre-wrap', overflowWrap: 'anywhere'`; ключ — `id`.
- [ ] **Step 2:** `DocumentCommentsDrawer`: antd `Drawer` справа, `title='Комментарии к документу'`, ширина 420; при `open` — `load()`, при закрытии — `reset()`. Тело — `CommentsList` (`emptyText='Комментариев нет'`) с прокруткой, футер — `Input.TextArea` (`maxLength={4000}`, `showCount`, `autoSize={{ minRows: 3, maxRows: 8 }}`) и кнопка «Добавить» (`disabled` при пустом `trim()`, `loading` во время запроса). Успех: очистить поле, `message.success('Комментарий добавлен')`, `load()`, `onAdded()`. Ошибка: `message.error(e instanceof ApiError ? e.message : 'Не удалось добавить комментарий')`, текст остаётся.
- [ ] **Step 3:** `DocumentPage`: кнопка `<Button icon={<CommentOutlined />}>Комментарии ({doc.comments_count})</Button>` на месте «Добавить комментарий»; `DocumentCommentsDrawer` вместо `CommentModal`, `onAdded={load}` (перечитывает документ — счётчик). Удалить `CommentModal.tsx` и его импорт.
- [ ] **Step 4:** `cd web && npx tsc --noEmit && npm run lint && npx biome check src` — чисто; `npm test` — зелёный.
- [ ] **Step 5: Commit** «AURORA-1683 экран документа: список комментариев в боковой панели».
### Task 3: Панель «Первичные документы» — счётчик и поповер
**Files:**
- Create: `web/src/widgets/DocumentComments/CommentsPopover.tsx`
- Modify: `web/src/widgets/FilesDrawer/FilesList.tsx:136-150` (колонка `actions`)
**Interfaces:**
- Consumes: `useDocumentComments`, `CommentsList` (Task 2), `DocumentRow.commentsCount` (Task 1).
- Produces: `CommentsPopover({ documentId, fileName, count }: { documentId: string; fileName: string; count: number })`.
- [ ] **Step 1:** `CommentsPopover`: управляемый antd `Popover` (`trigger='click'`, `open`/`onOpenChange`), `title={\`Комментарии к документу ${fileName}\`}`; при открытии `load()`, при закрытии `reset()`. Содержимое — обёртка с `onClick`/`onKeyDown` `stopPropagation` (как в `FileWarningsPopover`), `maxWidth: 420, maxHeight: 320, overflowY: 'auto'`, `CommentsList` (`emptyText='Комментарий отсутствует'`), справа внизу `Button size='small'` «Закрыть». Триггер — `Button` с `CommentOutlined` и `{count > 0 ? count : null}`, `aria-label={\`Комментарии: ${count}\`}`, `onClick` с `stopPropagation`.
- [ ] **Step 2:** `FilesList`, колонка `actions`: `<div style={{ display: 'flex', gap: 8 }}>` — `CommentsPopover` (только при `r.documentId`), затем существующая кнопка «Удалить».
- [ ] **Step 3:** `npx tsc --noEmit && npm run lint && npx biome check src && npm test` — чисто.
- [ ] **Step 4: Commit** «AURORA-1683 панель документов: счётчик и поповер комментариев».
### Task 4: Проверка в браузере, сверка с макетом, OpenSpec
**Files:**
- Create: `openspec/changes/document-comments-ui/tasks.md`
- [ ] **Step 1:** Стенд по `docs/dev/qa-guide.md` (api + web из worktree). Окно 1600×1000. Сценарии спеки: добавить комментарий на экране документа → «Комментарии (1)», после F5 он виден; ошибка (например, остановить api) — текст в поле остаётся; многострочный и длинный текст без пробелов не ломает панель; в панели «Первичные документы» число у иконки, поповер со списком, клик по иконке и внутри поповера не открывает документ; документ без комментариев — «Комментарий отсутствует»; замена документа с причиной → причина в комментариях заменённого.
- [ ] **Step 2:** Скриншоты сравнить с `../docs/mockup/screens/39-01-edit-documents.png` и `41-document-check.png`; расхождения — списком в отчёт.
- [ ] **Step 3:** `tasks.md` change (группы по задачам плана, отмеченные выполненными); `openspec validate document-comments-ui --strict` — valid.
- [ ] **Step 4:** `make web-check` из корня репозитория — зелёный (вывод — в описание PR).
- [ ] **Step 5: Commit** «AURORA-1683 OpenSpec: tasks, проверки».