Merge pull request 'AURORA-1683 Комментарии к документу: список на экране проверки и счётчик в панели' (#182) from feat/180-document-comments into main
This commit is contained in:
commit
fabc74982b
115
docs/superpowers/plans/2026-10-27-document-comments-ui.md
Normal file
115
docs/superpowers/plans/2026-10-27-document-comments-ui.md
Normal file
@ -0,0 +1,115 @@
|
||||
# Комментарии к документу в 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, проверки».
|
||||
2
openspec/changes/document-comments-ui/.openspec.yaml
Normal file
2
openspec/changes/document-comments-ui/.openspec.yaml
Normal file
@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-10-27
|
||||
72
openspec/changes/document-comments-ui/design.md
Normal file
72
openspec/changes/document-comments-ui/design.md
Normal file
@ -0,0 +1,72 @@
|
||||
## Context
|
||||
|
||||
API комментариев к документу готов (spec `documents-api`, «Комментарии к документу»; design D-11 Среза 3b):
|
||||
`GET /documents/{id}/comments` → `[{id, text, author: {id, full_name}, created_at}]` по возрастанию `created_at`
|
||||
(`DOCUMENT_VIEW`), `POST …/comments {text}` 1–4000 символов после обрезки (`DOCUMENT_EDIT`) → 201 + комментарий;
|
||||
`comments_count` — в `DocumentBrief` (карточка файла, панель) и `DocumentOut` (экран документа). Правки и удаления нет.
|
||||
|
||||
Во фронте сейчас: `CommentModal` (экран документа, только добавление) и `postComment` в `FilesDrawer` (причина замены
|
||||
документа). Макет: [39-01] — иконка комментария в строке панели, поповер «Комментарий к документу <файл>»
|
||||
(«Комментарий отсутствует», «Закрыть»); [41] — кнопка «Добавить комментарий» в шапке, места под список нет.
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
Goals: комментарий виден после добавления и после перезагрузки; счётчик в панели совпадает с числом комментариев;
|
||||
причина замены видна в комментариях заменённого документа. Non-goals — в proposal.
|
||||
|
||||
## Decisions
|
||||
|
||||
### D-1. Экран документа: кнопка «Комментарии (N)» + боковая панель
|
||||
Кнопка на месте «Добавить комментарий» (иконка `CommentOutlined`, текст «Комментарии (N)», `N = doc.comments_count`).
|
||||
Клик открывает antd `Drawer` справа «Комментарии к документу»: список комментариев, под ним поле ввода (до 4000
|
||||
символов, счётчик символов) и кнопка «Добавить» (неактивна при пустом тексте после обрезки). Список запрашивается при
|
||||
каждом открытии панели. После успешного добавления: поле очищается, список перечитывается, экран документа перечитывает
|
||||
`GET /documents/{id}` (счётчик на кнопке). Ошибка добавления — `message.error` с текстом сервера, текст в поле
|
||||
сохраняется. Ошибка загрузки списка — текст «Не удалось загрузить комментарии» вместо списка.
|
||||
|
||||
Почему не список под формой: экран [41] занят сканом и формой по высоте окна, блок комментариев отнимал бы место у
|
||||
формы; боковая панель не меняет раскладку [41]. Вариант предложен агентом и принят владельцем в чате 27.10.2026.
|
||||
|
||||
### D-2. Панель «Первичные документы»: кнопка счётчика + поповер только для чтения
|
||||
В колонке действий строки документа (`documentId != null`) перед «Удалить» — кнопка с `CommentOutlined` и числом
|
||||
`comments_count` (при нуле — только иконка; `aria-label` «Комментарии: N»). Клик — antd `Popover` (trigger click)
|
||||
с заголовком «Комментарии к документу <имя файла>»: список комментариев (запрос при открытии) или «Комментарий
|
||||
отсутствует»; кнопка «Закрыть». Клики внутри поповера и по кнопке не всплывают к `onClick` строки (иначе открывается
|
||||
документ) — тот же приём, что в `FileWarningsPopover`. У строки файла без документов кнопки нет.
|
||||
|
||||
### D-3. Общий список `CommentsList`
|
||||
Один компонент для D-1 и D-2: на каждый комментарий — ФИО автора (жирным), дата и время по Москве
|
||||
(`formatMoscowDateTime`, «dd.mm.yyyy hh:mm»), текст с сохранением переносов строк (`white-space: pre-wrap`,
|
||||
`overflow-wrap: anywhere`). Состояния: загрузка (`Spin`), ошибка, пусто (текст пустоты задаёт вызывающий: «Комментариев
|
||||
нет» в D-1, «Комментарий отсутствует» в D-2 — как в макете), список.
|
||||
|
||||
### D-4. Счётчик в строке панели — поле `DocumentRow.commentsCount`
|
||||
`buildDocumentRows` переносит `comments_count` документа (ядро без поля — 0); строка файла без документов — 0.
|
||||
Панель после замены документа уже перечитывает список файлов (`reload`), поэтому счётчик заменённого документа
|
||||
обновляется без доработок.
|
||||
|
||||
### D-5. `CommentModal` удаляется
|
||||
Добавление переезжает в панель D-1; второе место добавления на экране не нужно.
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- Тестов компонентов в `web/` нет: покрыты чистые функции (`formatMoscowDateTime`, `buildDocumentRows`), экраны
|
||||
проверяются вручную по сценариям спеки и сверкой с [39-01], [41].
|
||||
- Поповер в узкой панели: ширина ограничена 420 px, высота — 320 px с прокруткой (как `FileWarningsPopover`).
|
||||
|
||||
## Decisions made without discussion
|
||||
|
||||
| Дата | Решение | Обоснование | Цена ошибки |
|
||||
|---|---|---|---|
|
||||
| 27.10.2026 | R-1: в поповере панели — только чтение, добавление только на экране документа | макет [39-01] показывает поповер без поля ввода; одно место добавления проще | добавить поле в поповер — небольшая правка |
|
||||
| 27.10.2026 | R-2: при нуле комментариев в панели — иконка без числа | макет [39-01] показывает только иконку; «0» шумит в каждой строке | косметика |
|
||||
| 27.10.2026 | R-3: формат времени «dd.mm.yyyy hh:mm» по Москве | даты в панели уже по Москве (`formatMoscowDate`), бэкенд считает даты по Москве | косметика |
|
||||
| 27.10.2026 | R-4: кнопку «Добавить» не прячем без права `DOCUMENT_EDIT`, отказ 403 — сообщением | так работал `CommentModal`; доступ решает сервер (`auth/permissions.ts` — только для меню) | пользователь без права видит кнопку и получает отказ |
|
||||
| 27.10.2026 | R-5: при перечитывании списка после добавления прежний список остаётся на экране до ответа | без мигания индикатора загрузки после «Добавить» | косметика |
|
||||
| 27.10.2026 | R-6: `Drawer size={420}` вместо `width` | antd 6 помечает `width` устаревшим; так же сделано в `FilesDrawer` | нет |
|
||||
| 27.10.2026 | R-7: отступ между полем ввода и «Добавить» — 28 px | счётчик символов `showCount` antd рисуется под полем и наезжал на кнопку (сверка в браузере) | косметика; при смене antd может съехать |
|
||||
| 27.10.2026 | R-8: кнопки строки панели прижаты вправо | кнопка с числом шире пустой, «Удалить» съезжала из столбца | косметика |
|
||||
| 27.10.2026 | R-9: проверка в браузере на копии БД `armka_1683` (alembic 0010), API из worktree с `NATS_ENABLED=false` | общий локальный стенд на 0007, не трогаем | копию удалить при закрытии |
|
||||
| 27.10.2026 | R-10: сценарий спеки «Причина замены документа» в браузере не прогнан | замена требует распознавания (NATS выключен); путь тот же — `postComment` к заменяемому документу + перечитывание панели, список — тот же `GET` | проверить на нашем стенде при выкладке |
|
||||
| 27.10.2026 | R-11: защита от всплытия клика к строке таблицы — обёртка со `stopPropagation` вокруг всего `Popover` (финальное ревью I-1); тот же дефект исправлен и в `FileWarningsPopover` | заголовок, отступы и стрелка поповера antd лежат вне `content`, клик по ним открывал документ; у `FileWarningsPopover` дефект был до ветки, приём тот же | нет |
|
||||
| 27.10.2026 | R-12: `key={doc.id}` на боковой панели экрана документа (финальное ревью I-2, M-2) | `DocumentPage` не пересоздаётся при смене документа — черновик и список прежнего документа переходили на новый, а удалить комментарий нельзя | нет |
|
||||
53
openspec/changes/document-comments-ui/proposal.md
Normal file
53
openspec/changes/document-comments-ui/proposal.md
Normal file
@ -0,0 +1,53 @@
|
||||
## Why
|
||||
|
||||
AURORA-1683 (arm-core #180, эпик AURORA-1353): в AURORA-1445 на экране документа [41] сделано только окно «Добавить
|
||||
комментарий» — список комментариев не запрашивается, добавленный комментарий нигде не виден. В панели «Первичные
|
||||
документы» [39] нет ни иконки комментария, ни счётчика. Причина замены документа пишется комментарием к заменяемому
|
||||
документу (`FilesDrawer`), и её тоже негде прочитать. API ядра готов с 07.10.2026 (spec `documents-api`, требование
|
||||
«Комментарии к документу»): `GET`/`POST /documents/{id}/comments`, `comments_count` в карточке файла и документа.
|
||||
|
||||
## What Changes
|
||||
|
||||
Репозиторий — только arm-core (`web/`); эндпоинты, бэкенд и DBML без изменений. Срез 2c (документы), правки фронта
|
||||
по замечаниям спринта 3.
|
||||
|
||||
- Экран документа [41]: кнопка «Добавить комментарий» заменяется кнопкой «Комментарии (N)» (`N = comments_count`
|
||||
документа). Она открывает боковую панель «Комментарии к документу»: список (автор, дата и время по Москве, текст) по
|
||||
возрастанию времени и поле ввода с кнопкой «Добавить». После добавления список и счётчик обновляются.
|
||||
- Панель «Первичные документы» [39]: в строке документа — кнопка с иконкой комментария и числом комментариев (при
|
||||
нуле — только иконка), как в макете [39-01]. По клику — поповер «Комментарии к документу <файл>» со списком только
|
||||
для чтения; без комментариев — «Комментарий отсутствует», кнопка «Закрыть».
|
||||
- Окно `CommentModal` удаляется — добавление переезжает в боковую панель экрана документа.
|
||||
- Дельты: `document-review-ui` — ADDED «Комментарии к документу»; `primary-files-ui` — ADDED «Комментарии документа в
|
||||
панели».
|
||||
|
||||
Таблицы DBML v5.23 (только чтение через API): `comment` (`object_type = 'document'`), `app_user` (ФИО автора).
|
||||
|
||||
## Non-goals
|
||||
|
||||
- Правка и удаление комментариев — их нет в API и не будет (spec `documents-api`).
|
||||
- Добавление комментария из панели «Первичные документы» — в поповере только чтение.
|
||||
- Проверка права `DOCUMENT_EDIT` на фронте: кнопку «Добавить» не прячем, отказ сервера (403) показывается сообщением,
|
||||
как сейчас в `CommentModal`.
|
||||
- Комментарии к строкам и ячейкам формы, к консолидации — отдельные сущности, не этот change.
|
||||
- Изменения бэкенда и DBML.
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
|
||||
### Modified Capabilities
|
||||
|
||||
- `document-review-ui`: ADDED требование «Комментарии к документу».
|
||||
- `primary-files-ui`: ADDED требование «Комментарии документа в панели».
|
||||
|
||||
## Impact
|
||||
|
||||
- `web/src/types/api.ts` — тип `CommentOut`.
|
||||
- `web/src/services/api/documents.ts` — `getComments`.
|
||||
- `web/src/utils/dates.ts` — `formatMoscowDateTime` (+ тест).
|
||||
- `web/src/widgets/FilesDrawer/documentRows.ts` — `commentsCount` в строке (+ тест).
|
||||
- `web/src/widgets/DocumentComments/` — общий список комментариев, боковая панель экрана документа, поповер панели.
|
||||
- `web/src/layouts/DocumentPage.tsx`, `web/src/widgets/FilesDrawer/FilesList.tsx`; удаляется
|
||||
`web/src/widgets/DocumentReview/CommentModal.tsx`.
|
||||
- Тестов компонентов в `web/` нет (vitest — только чистые функции); экраны сверяются с макетом [39-01], [41] в браузере.
|
||||
@ -0,0 +1,23 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Комментарии к документу
|
||||
Экран документа SHALL показывать в шапке кнопку «Комментарии (N)», где N — `comments_count` документа из
|
||||
`GET /documents/{id}`. Кнопка SHALL открывать боковую панель «Комментарии к документу» со списком
|
||||
`GET /documents/{id}/comments` (запрос при каждом открытии) по возрастанию времени: ФИО автора, дата и время по Москве
|
||||
(«dd.mm.yyyy hh:mm»), текст с переносами строк; без комментариев — «Комментариев нет»; ошибка запроса — «Не удалось
|
||||
загрузить комментарии». Под списком SHALL быть поле ввода (до 4000 символов) и кнопка «Добавить», неактивная при пустом
|
||||
тексте после обрезки пробелов; добавление — `POST /documents/{id}/comments {text}`. После успешного добавления поле
|
||||
SHALL очищаться, список — перечитываться, счётчик на кнопке — обновляться из `GET /documents/{id}`. При ошибке
|
||||
добавления SHALL показываться сообщение сервера, текст в поле сохраняется. Правки и удаления комментариев нет.
|
||||
|
||||
#### Scenario: Добавленный комментарий виден после перезагрузки
|
||||
- **WHEN** аналитик открыл «Комментарии (0)», добавил «нет подписи руководителя» и перезагрузил страницу
|
||||
- **THEN** на кнопке «Комментарии (1)», в панели — комментарий с его ФИО, датой и временем
|
||||
|
||||
#### Scenario: Причина замены документа
|
||||
- **WHEN** документ заменён из панели «Первичные документы» с причиной «новая редакция от клиента»
|
||||
- **THEN** в комментариях заменённого документа есть «Замена документа: новая редакция от клиента»
|
||||
|
||||
#### Scenario: Ошибка добавления
|
||||
- **WHEN** сервер отклоняет добавление (403 или 422)
|
||||
- **THEN** показано сообщение сервера, введённый текст остаётся в поле, список не меняется
|
||||
@ -0,0 +1,16 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Комментарии документа в панели
|
||||
В строке документа панели «Первичные документы» [39-01] SHALL быть кнопка с иконкой комментария и числом комментариев
|
||||
документа (`documents[].comments_count`; при нуле — только иконка). Клик по кнопке SHALL открывать поповер «Комментарии
|
||||
к документу <имя файла>» со списком `GET /documents/{id}/comments` (запрос при открытии; автор, дата и время по Москве,
|
||||
текст) только для чтения; без комментариев — «Комментарий отсутствует»; кнопка «Закрыть» закрывает поповер. Клик по
|
||||
кнопке и внутри поповера SHALL NOT открывать документ. У строки файла без документов кнопки нет.
|
||||
|
||||
#### Scenario: Счётчик совпадает с числом комментариев
|
||||
- **WHEN** к документу добавлены два комментария и панель перечитана
|
||||
- **THEN** в строке документа у иконки число 2, в поповере — оба комментария по возрастанию времени
|
||||
|
||||
#### Scenario: Документ без комментариев
|
||||
- **WHEN** аналитик нажимает иконку комментария у документа без комментариев
|
||||
- **THEN** в поповере «Комментарий отсутствует», экран документа не открывается
|
||||
21
openspec/changes/document-comments-ui/tasks.md
Normal file
21
openspec/changes/document-comments-ui/tasks.md
Normal file
@ -0,0 +1,21 @@
|
||||
## 1. Данные (arm-core `web/`)
|
||||
|
||||
- [x] 1.1 `types/api.ts`: `CommentOut`; `services/api/documents.ts`: `getComments`, `postComment` с типом ответа.
|
||||
- [x] 1.2 `utils/dates.ts`: `formatMoscowDateTime` + тест.
|
||||
- [x] 1.3 `widgets/FilesDrawer/documentRows.ts`: `commentsCount` в строке + тесты.
|
||||
|
||||
## 2. Экран документа [41]
|
||||
|
||||
- [x] 2.1 `widgets/DocumentComments/`: `useDocumentComments`, `CommentsList`, `DocumentCommentsDrawer`.
|
||||
- [x] 2.2 `layouts/DocumentPage.tsx`: кнопка «Комментарии (N)» и боковая панель; `CommentModal` удалён.
|
||||
|
||||
## 3. Панель «Первичные документы» [39-01]
|
||||
|
||||
- [x] 3.1 `widgets/DocumentComments/CommentsPopover.tsx`; `widgets/FilesDrawer/FilesList.tsx` — кнопка в колонке действий.
|
||||
|
||||
## 4. Проверки
|
||||
|
||||
- [x] 4.1 Сценарии в браузере (1600×1000): добавление и перезагрузка, ошибка 403, многострочный и длинный текст, счётчик и
|
||||
поповер в панели, пустой поповер, клики не открывают документ. Сценарий «Причина замены документа» не прогнан
|
||||
(R-10 в design.md) — проверить на нашем стенде при выкладке.
|
||||
- [x] 4.2 `npm test`, `npm run lint`, `make web-check`, `openspec validate document-comments-ui --strict`.
|
||||
@ -11,7 +11,7 @@ import { getDocument } from '../services/api/documents';
|
||||
import { getFile, listLegalEntityFiles } from '../services/api/files';
|
||||
import type { DocumentOut } from '../types/api';
|
||||
import { documentTypeLabel, periodLabel } from '../utils/labels';
|
||||
import CommentModal from '../widgets/DocumentReview/CommentModal';
|
||||
import { DocumentCommentsDrawer } from '../widgets/DocumentComments/DocumentCommentsDrawer';
|
||||
import { OsvDocument } from '../widgets/DocumentReview/OsvDocument';
|
||||
import { StatementReview } from '../widgets/DocumentReview/StatementReview';
|
||||
import { documentPageTitle, documentSubtitle, optionStatusBadge, statusBadge } from '../widgets/DocumentReview/reviewView';
|
||||
@ -116,7 +116,7 @@ export function DocumentPage() {
|
||||
/>
|
||||
<div style={{ alignSelf: 'flex-end' }}>
|
||||
<Button icon={<CommentOutlined />} onClick={() => setCommentOpen(true)}>
|
||||
Добавить комментарий
|
||||
Комментарии ({doc.comments_count})
|
||||
</Button>
|
||||
</div>
|
||||
<main
|
||||
@ -133,7 +133,8 @@ export function DocumentPage() {
|
||||
{!broken && kind === 'statement' && <StatementReview key={doc.id} document={doc} onChanged={load} />}
|
||||
{kind === 'none' && <Empty description='Документ без данных для проверки' />}
|
||||
</main>
|
||||
<CommentModal open={commentOpen} documentId={doc.id} onClose={() => setCommentOpen(false)} />
|
||||
{/* key: при смене документа панель пересоздаётся — черновик и список прежнего документа не переходят на новый */}
|
||||
<DocumentCommentsDrawer key={doc.id} documentId={doc.id} open={commentOpen} onClose={() => setCommentOpen(false)} onAdded={load} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { api } from '../../api/client';
|
||||
import type { DocumentOut, EntryOut, Page, StatementOut, StatementPatch } from '../../types/api';
|
||||
import type { CommentOut, DocumentOut, EntryOut, Page, StatementOut, StatementPatch } from '../../types/api';
|
||||
|
||||
const ENTRIES_PAGE = 2000;
|
||||
|
||||
@ -23,4 +23,7 @@ export async function getAllEntries(id: string): Promise<EntryOut[]> {
|
||||
}
|
||||
|
||||
/** Комментарий к документу: текст 1–4000 символов после обрезки пробелов (иначе 422). */
|
||||
export const postComment = (documentId: string, text: string) => api.post(`/documents/${documentId}/comments`, { text });
|
||||
export const postComment = (documentId: string, text: string) => api.post<CommentOut>(`/documents/${documentId}/comments`, { text });
|
||||
|
||||
/** Комментарии к документу по возрастанию времени, с ФИО автора. */
|
||||
export const getComments = (documentId: string) => api.get<CommentOut[]>(`/documents/${documentId}/comments`);
|
||||
|
||||
@ -75,6 +75,13 @@ export interface DocumentBrief {
|
||||
comments_count: number;
|
||||
issue_counts: IssueCountsOut;
|
||||
}
|
||||
/** Комментарий к документу (`GET /documents/{id}/comments`); правки и удаления нет. */
|
||||
export interface CommentOut {
|
||||
id: string;
|
||||
text: string;
|
||||
author: { id: string; full_name: string };
|
||||
created_at: string;
|
||||
}
|
||||
export type FileType = 'XLS' | 'XLSX' | 'XLSB' | 'PDF' | 'PNG' | 'JPG';
|
||||
export interface FileOut {
|
||||
id: string;
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { formatMoscowDate, isIsoDate } from './dates';
|
||||
import { formatMoscowDate, formatMoscowDateTime, isIsoDate } from './dates';
|
||||
|
||||
describe('formatMoscowDate', () => {
|
||||
it('момент времени из API (UTC) — дата по Москве', () => {
|
||||
@ -12,6 +12,17 @@ describe('formatMoscowDate', () => {
|
||||
});
|
||||
});
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
||||
|
||||
describe('isIsoDate', () => {
|
||||
it('строго ГГГГ-ММ-ДД и существующая дата', () => {
|
||||
expect(isIsoDate('2024-12-31')).toBe(true);
|
||||
|
||||
@ -7,6 +7,23 @@ export function formatMoscowDate(iso: string | null | undefined): string | null
|
||||
return Number.isNaN(date.getTime()) ? null : MOSCOW.format(date);
|
||||
}
|
||||
|
||||
const MOSCOW_DATE_TIME = new Intl.DateTimeFormat('ru-RU', {
|
||||
timeZone: 'Europe/Moscow',
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
year: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
hourCycle: 'h23',
|
||||
});
|
||||
|
||||
/** Момент времени из API (ISO, UTC) → «dd.mm.yyyy hh:mm» по Москве; пусто/мусор → null. */
|
||||
export function formatMoscowDateTime(iso: string | null | undefined): string | null {
|
||||
if (!iso) return null;
|
||||
const date = new Date(iso);
|
||||
return Number.isNaN(date.getTime()) ? null : MOSCOW_DATE_TIME.format(date).replace(', ', ' ');
|
||||
}
|
||||
|
||||
/** Строго «ГГГГ-ММ-ДД» и существующая календарная дата (30 февраля — нет). */
|
||||
export function isIsoDate(text: string): boolean {
|
||||
const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(text);
|
||||
|
||||
25
web/src/widgets/DocumentComments/CommentsList.tsx
Normal file
25
web/src/widgets/DocumentComments/CommentsList.tsx
Normal file
@ -0,0 +1,25 @@
|
||||
import { Spin, Typography } from 'antd';
|
||||
import { formatMoscowDateTime } from '../../utils/dates';
|
||||
import type { CommentsState } from './useDocumentComments';
|
||||
|
||||
/** Список комментариев документа: автор, дата и время по Москве, текст с переносами строк. */
|
||||
export function CommentsList({ state, emptyText }: { state: CommentsState | null; emptyText: string }) {
|
||||
if (state === null || state.status === 'loading') return <Spin size='small' />;
|
||||
if (state.status === 'error') return <span style={{ color: '#cf1322' }}>Не удалось загрузить комментарии</span>;
|
||||
if (state.comments.length === 0) return <span style={{ color: '#8c8c8c' }}>{emptyText}</span>;
|
||||
return (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
|
||||
{state.comments.map((c) => (
|
||||
<div key={c.id}>
|
||||
<div style={{ display: 'flex', gap: 8, alignItems: 'baseline', flexWrap: 'wrap' }}>
|
||||
<Typography.Text strong>{c.author.full_name}</Typography.Text>
|
||||
<Typography.Text type='secondary' style={{ fontSize: 12 }}>
|
||||
{formatMoscowDateTime(c.created_at)}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
<div style={{ whiteSpace: 'pre-wrap', overflowWrap: 'anywhere' }}>{c.text}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
42
web/src/widgets/DocumentComments/CommentsPopover.tsx
Normal file
42
web/src/widgets/DocumentComments/CommentsPopover.tsx
Normal file
@ -0,0 +1,42 @@
|
||||
import { CommentOutlined } from '@ant-design/icons';
|
||||
import { Button, Popover } from 'antd';
|
||||
import { useState } from 'react';
|
||||
import { CommentsList } from './CommentsList';
|
||||
import { useDocumentComments } from './useDocumentComments';
|
||||
|
||||
/** Кнопка с числом комментариев в строке панели документов [39-01]; по клику — список только для чтения. */
|
||||
export function CommentsPopover({ documentId, fileName, count }: { documentId: string; fileName: string; count: number }) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const { state, load, reset } = useDocumentComments(documentId);
|
||||
|
||||
const onOpenChange = (next: boolean) => {
|
||||
setOpen(next);
|
||||
if (next) load();
|
||||
else reset();
|
||||
};
|
||||
|
||||
const content = (
|
||||
<div style={{ width: 360, maxWidth: 420 }}>
|
||||
<div style={{ maxHeight: 320, overflowY: 'auto', marginBottom: 8 }}>
|
||||
<CommentsList state={state} emptyText='Комментарий отсутствует' />
|
||||
</div>
|
||||
<div style={{ textAlign: 'right' }}>
|
||||
<Button size='small' onClick={() => onOpenChange(false)}>
|
||||
Закрыть
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
// Поповер рисуется в портале, но события React всплывают по дереву компонентов до onClick строки таблицы и открывали
|
||||
// бы документ. Обёртка вокруг всего Popover ловит клики по кнопке, заголовку, отступам и содержимому разом.
|
||||
<span onClick={(e) => e.stopPropagation()} onKeyDown={(e) => e.stopPropagation()}>
|
||||
<Popover trigger='click' title={`Комментарии к документу ${fileName}`} content={content} open={open} onOpenChange={onOpenChange}>
|
||||
<Button icon={<CommentOutlined />} aria-label={`Комментарии: ${count}`}>
|
||||
{count > 0 ? count : null}
|
||||
</Button>
|
||||
</Popover>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
64
web/src/widgets/DocumentComments/DocumentCommentsDrawer.tsx
Normal file
64
web/src/widgets/DocumentComments/DocumentCommentsDrawer.tsx
Normal file
@ -0,0 +1,64 @@
|
||||
import { Button, Drawer, Input, message } from 'antd';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { ApiError } from '../../api/client';
|
||||
import { postComment } from '../../services/api/documents';
|
||||
import { CommentsList } from './CommentsList';
|
||||
import { useDocumentComments } from './useDocumentComments';
|
||||
|
||||
type DocumentCommentsDrawerProps = { documentId: string; open: boolean; onClose: () => void; onAdded: () => void };
|
||||
|
||||
/** Боковая панель экрана документа: список комментариев и добавление нового (правки и удаления нет). */
|
||||
export function DocumentCommentsDrawer({ documentId, open, onClose, onAdded }: DocumentCommentsDrawerProps) {
|
||||
const { state, load, reset } = useDocumentComments(documentId);
|
||||
const [text, setText] = useState('');
|
||||
const [saving, setSaving] = useState(false);
|
||||
|
||||
// список — при каждом открытии; при закрытии сбрасываем, чтобы не показать чужой список после смены документа
|
||||
useEffect(() => {
|
||||
if (open) load();
|
||||
else reset();
|
||||
}, [open, load, reset]);
|
||||
|
||||
const handleAdd = async () => {
|
||||
setSaving(true);
|
||||
try {
|
||||
await postComment(documentId, text.trim());
|
||||
message.success('Комментарий добавлен');
|
||||
setText('');
|
||||
load();
|
||||
onAdded();
|
||||
} catch (e: unknown) {
|
||||
// текст в поле не трогаем: можно поправить и повторить
|
||||
message.error(e instanceof ApiError ? e.message : 'Не удалось добавить комментарий');
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
title='Комментарии к документу'
|
||||
placement='right'
|
||||
size={420}
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
footer={
|
||||
// отступ 28: счётчик символов showCount рисуется под полем и наезжал бы на кнопку
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 28 }}>
|
||||
<Input.TextArea
|
||||
value={text}
|
||||
onChange={(e) => setText(e.target.value)}
|
||||
autoSize={{ minRows: 3, maxRows: 8 }}
|
||||
maxLength={4000}
|
||||
showCount
|
||||
placeholder='Опишите результат проверки или внесенные правки'
|
||||
/>
|
||||
<Button type='primary' style={{ alignSelf: 'flex-end' }} disabled={text.trim().length === 0} loading={saving} onClick={handleAdd}>
|
||||
Добавить
|
||||
</Button>
|
||||
</div>
|
||||
}>
|
||||
<CommentsList state={state} emptyText='Комментариев нет' />
|
||||
</Drawer>
|
||||
);
|
||||
}
|
||||
30
web/src/widgets/DocumentComments/useDocumentComments.ts
Normal file
30
web/src/widgets/DocumentComments/useDocumentComments.ts
Normal file
@ -0,0 +1,30 @@
|
||||
import { useCallback, useRef, useState } from 'react';
|
||||
import { getComments } from '../../services/api/documents';
|
||||
import type { CommentOut } from '../../types/api';
|
||||
|
||||
export type CommentsState = { status: 'loading' } | { status: 'error' } | { status: 'done'; comments: CommentOut[] };
|
||||
|
||||
/** Комментарии документа по запросу: `load` перечитывает список, `reset` сбрасывает его (при закрытии панели или поповера). */
|
||||
export function useDocumentComments(documentId: string | null) {
|
||||
const [state, setState] = useState<CommentsState | null>(null);
|
||||
// номер последнего запроса: ответ устаревшего запроса или пришедший после reset игнорируется
|
||||
const requestNo = useRef(0);
|
||||
|
||||
const load = useCallback(() => {
|
||||
requestNo.current += 1;
|
||||
const mine = requestNo.current;
|
||||
if (!documentId) return;
|
||||
// при перечитывании после добавления старый список остаётся на экране до ответа
|
||||
setState((prev) => (prev?.status === 'done' ? prev : { status: 'loading' }));
|
||||
getComments(documentId)
|
||||
.then((comments) => mine === requestNo.current && setState({ status: 'done', comments }))
|
||||
.catch(() => mine === requestNo.current && setState({ status: 'error' }));
|
||||
}, [documentId]);
|
||||
|
||||
const reset = useCallback(() => {
|
||||
requestNo.current += 1;
|
||||
setState(null);
|
||||
}, []);
|
||||
|
||||
return { state, load, reset };
|
||||
}
|
||||
@ -1,52 +0,0 @@
|
||||
import { Form, Input, Modal, message } from 'antd';
|
||||
import { useState } from 'react';
|
||||
import { ApiError } from '../../api/client';
|
||||
import { postComment } from '../../services/api/documents';
|
||||
|
||||
type CommentModalProps = { documentId: string; open: boolean; onClose: () => void };
|
||||
|
||||
export default function CommentModal({ documentId, open, onClose }: CommentModalProps) {
|
||||
const [comment, setComment] = useState('');
|
||||
const [saving, setSaving] = useState(false);
|
||||
const close = () => {
|
||||
setComment('');
|
||||
onClose();
|
||||
};
|
||||
const handleOk = async () => {
|
||||
setSaving(true);
|
||||
try {
|
||||
await postComment(documentId, comment.trim());
|
||||
message.success('Комментарий добавлен');
|
||||
close();
|
||||
} catch (e: unknown) {
|
||||
// окно не закрываем: текст не теряется, можно повторить
|
||||
message.error(e instanceof ApiError ? e.message : 'Не удалось добавить комментарий');
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
return (
|
||||
<Modal
|
||||
title='Добавить комментарий'
|
||||
okText='Добавить'
|
||||
onOk={handleOk}
|
||||
okButtonProps={{ disabled: comment.trim().length === 0 }}
|
||||
confirmLoading={saving}
|
||||
open={open}
|
||||
cancelText='Отмена'
|
||||
onCancel={close}
|
||||
destroyOnHidden>
|
||||
<Form layout='vertical'>
|
||||
<Form.Item label={<span style={{ fontWeight: 600 }}>Комментарий</span>}>
|
||||
<Input.TextArea
|
||||
value={comment}
|
||||
onChange={(e) => setComment(e.target.value)}
|
||||
autoSize={{ minRows: 3 }}
|
||||
maxLength={4000}
|
||||
placeholder='Опишите результат проверки или внесенные правки'
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@ -70,11 +70,7 @@ export function FileWarningsPopover({ row, badge }: { row: DocumentRow; badge: {
|
||||
};
|
||||
|
||||
const content = (
|
||||
<div
|
||||
// клики внутри поповера всплывают по дереву React до onClick строки таблицы и открывали бы документ
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onKeyDown={(e) => e.stopPropagation()}
|
||||
style={{ maxWidth: 420, maxHeight: 320, overflowY: 'auto' }}>
|
||||
<div style={{ maxWidth: 420, maxHeight: 320, overflowY: 'auto' }}>
|
||||
{row.fileWarnings.length > 0 && (
|
||||
<Section title='Предупреждения файла'>
|
||||
{row.fileWarnings.map((w, i) => (
|
||||
@ -104,11 +100,13 @@ export function FileWarningsPopover({ row, badge }: { row: DocumentRow; badge: {
|
||||
);
|
||||
|
||||
return (
|
||||
// клики по кнопке, заголовку и содержимому поповера (он в портале, но события React всплывают по дереву компонентов)
|
||||
// дошли бы до onClick строки таблицы и открыли документ — гасим их обёрткой вокруг всего Popover
|
||||
<span onClick={(e) => e.stopPropagation()} onKeyDown={(e) => e.stopPropagation()}>
|
||||
<Popover trigger='click' title='Предупреждения' content={content} onOpenChange={onOpenChange}>
|
||||
<button
|
||||
type='button'
|
||||
aria-label={`Предупреждения: ${badge.count}`}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
style={{
|
||||
cursor: 'pointer',
|
||||
background: 'none',
|
||||
@ -121,5 +119,6 @@ export function FileWarningsPopover({ row, badge }: { row: DocumentRow; badge: {
|
||||
<SeverityIcon severity={badge.level} /> {badge.count}
|
||||
</button>
|
||||
</Popover>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
@ -4,6 +4,7 @@ import type { ColumnsType } from 'antd/es/table';
|
||||
import type { ReactNode } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import type { CheckResult, FileOut, FileType } from '../../types/api';
|
||||
import { CommentsPopover } from '../DocumentComments/CommentsPopover';
|
||||
import { FileWarningsPopover } from './FileWarningsPopover';
|
||||
import { type DocumentRow, type RowStatus, buildDocumentRows, rowBadge } from './documentRows';
|
||||
|
||||
@ -138,6 +139,9 @@ export function FilesList({
|
||||
title: '',
|
||||
dataIndex: 'action',
|
||||
render: (_, r) => (
|
||||
// кнопка с числом комментариев шире пустой — прижимаем вправо, чтобы «Удалить» стояла столбцом
|
||||
<div style={{ display: 'flex', gap: 8, justifyContent: 'flex-end' }}>
|
||||
{r.documentId && <CommentsPopover documentId={r.documentId} fileName={r.name} count={r.commentsCount} />}
|
||||
<Button
|
||||
icon={<DeleteOutlined />}
|
||||
aria-label='Удалить файл'
|
||||
@ -146,6 +150,7 @@ export function FilesList({
|
||||
onDeleteClick(r);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
@ -169,6 +169,18 @@ describe('buildDocumentRows: предупреждения', () => {
|
||||
const rows = buildDocumentRows([file({ documents: [doc({ issue_counts: { error: 1, info: 3 } })] })]);
|
||||
expect(rows[0].issueCounts).toEqual({ error: 1, info: 3 });
|
||||
});
|
||||
|
||||
it('comments_count документа переносится в commentsCount', () => {
|
||||
const rows = buildDocumentRows([file({ documents: [doc({ comments_count: 2 })] })]);
|
||||
expect(rows[0].commentsCount).toBe(2);
|
||||
});
|
||||
|
||||
it('ядро без comments_count и строка файла без документов — 0', () => {
|
||||
const old = doc() as Partial<DocumentBrief>;
|
||||
old.comments_count = undefined;
|
||||
expect(buildDocumentRows([file({ documents: [old as DocumentBrief] })])[0].commentsCount).toBe(0);
|
||||
expect(buildDocumentRows([file({ status: 'PROCESSING' })])[0].commentsCount).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('rowBadge', () => {
|
||||
|
||||
@ -27,6 +27,8 @@ export interface DocumentRow {
|
||||
mappingWarnings: string[];
|
||||
/** счётчики issues документа; у строки файла без документов — нули */
|
||||
issueCounts: IssueCountsOut;
|
||||
/** число комментариев документа; у строки файла без документов — 0 */
|
||||
commentsCount: number;
|
||||
}
|
||||
|
||||
/** Значок у статуса (P6-12): число предупреждений и цвет уровня; ничего нет — null. */
|
||||
@ -78,6 +80,7 @@ export function buildDocumentRows(files: FileOut[], labelFor?: (f: FileOut) => s
|
||||
fileWarnings,
|
||||
mappingWarnings,
|
||||
issueCounts: { error: 0, info: 0 },
|
||||
commentsCount: 0,
|
||||
},
|
||||
];
|
||||
}
|
||||
@ -105,6 +108,7 @@ export function buildDocumentRows(files: FileOut[], labelFor?: (f: FileOut) => s
|
||||
mappingWarnings,
|
||||
// ядро без v5.19 не присылает issue_counts — считаем, что замечаний нет
|
||||
issueCounts: d.issue_counts ?? { error: 0, info: 0 },
|
||||
commentsCount: d.comments_count ?? 0,
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user