Дельта web-app влита в openspec/specs: изменено «Карточка задачи», добавлено «Разметка Jira в карточке». Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
346 lines
40 KiB
Markdown
346 lines
40 KiB
Markdown
# web-app Specification
|
||
|
||
## Purpose
|
||
Веб-приложение хаба (`web/`, React + TypeScript + Vite) под префиксом `/app/`: вход по клиентскому токену, доска связей (перенос `links.html`) и страницы задач со служебными заметками; сборка в `src/jira_hub/web/app/`.
|
||
|
||
## Requirements
|
||
|
||
### Requirement: Вход по токену
|
||
Приложение SHALL работать с API хаба того же хоста под клиентским токеном: хранить его в `localStorage` под ключом
|
||
`jira-hub-token` (и в памяти, если хранилище закрыто) и передавать в заголовке `Authorization: Bearer` каждого запроса.
|
||
Если токена нет или любой запрос ответил 401, приложение SHALL показывать экран входа вместо страниц: поле токена с
|
||
подсказкой `uv run jira-hub tokens`, без панелей страниц и без фоновых запросов, которые могли бы изменить поле ввода.
|
||
Введённый токен SHALL проверяться запросом `GET /settings` и сохраняться только при успехе. Действие «Сменить токен»
|
||
SHALL очищать токен и показывать экран входа. Ответ 401 SHALL выводить на экран входа, только если запрос ушёл с текущим токеном: запоздавший 401 на
|
||
запрос с прежним токеном после входа с новым не учитывается. Если проверка токена получила ответ 4xx, кроме 401,
|
||
экран входа SHALL показывать текст ошибки хаба, а не «хаб не отвечает».
|
||
|
||
#### Scenario: Первый вход
|
||
- **WHEN** приложение открыто впервые, токена нет
|
||
- **THEN** показан экран входа; после ввода верного токена открывается запрошенная страница, токен сохранён
|
||
|
||
#### Scenario: Устаревший токен
|
||
- **WHEN** сохранённый токен не подходит и запрос ответил 401
|
||
- **THEN** показан экран входа с пояснением, что токен не подошёл; панели доски и задач не видны
|
||
|
||
#### Scenario: Токен от прежней доски
|
||
- **WHEN** в `localStorage` есть `jira-hub-token`, сохранённый страницей `links.html`
|
||
- **THEN** приложение входит с ним без запроса токена
|
||
|
||
#### Scenario: Запоздавший 401 от прежнего токена
|
||
- **WHEN** запрос ушёл с прежним токеном, пользователь вошёл с новым, и после этого запрос ответил 401
|
||
- **THEN** приложение остаётся на странице, новый токен сохранён
|
||
|
||
#### Scenario: Хаб отверг проверку токена
|
||
- **WHEN** проверка `GET /settings` ответила 400 с `detail`
|
||
- **THEN** экран входа показывает текст `detail`, а не «хаб не отвечает»
|
||
|
||
### Requirement: Оболочка и адреса
|
||
Приложение SHALL открываться под префиксом `/app/` и иметь страницы `/app/board` (доска связей), `/app/issues` (список
|
||
задач) и `/app/issues/{key}` (карточка задачи — окно поверх страницы, требование «Карточка задачи»); `/app` и `/app/` SHALL вести на `/app/board`, неизвестный путь —
|
||
на страницу «не найдено» со ссылками на разделы. Верхняя панель SHALL содержать переходы «Доска», «Задачи» и действие
|
||
«Сменить токен». Неожиданная ошибка отрисовки страницы SHALL показываться сообщением с кнопкой «Перезагрузить», не
|
||
пустым экраном.
|
||
|
||
#### Scenario: Переход между разделами
|
||
- **WHEN** на доске выбран раздел «Задачи»
|
||
- **THEN** адрес — `/app/issues`, показан список задач; «назад» возвращает на доску
|
||
|
||
### Requirement: Ошибки хаба
|
||
Приложение SHALL различать ошибки запросов к хабу: 401 — экран входа; хаб не отвечает (сеть, 5xx) — сообщение
|
||
«хаб не отвечает» с возможностью повторить, ранее загруженные данные остаются на экране; прочие 4xx — текст `detail`
|
||
ответа рядом с действием, которое его вызвало.
|
||
|
||
#### Scenario: Хаб остановлен
|
||
- **WHEN** доска показана, хаб остановлен, нажато «Перечитать»
|
||
- **THEN** на доске прежние данные и сообщение «хаб не отвечает — показаны прежние данные»
|
||
|
||
### Requirement: Справочники из настроек хаба
|
||
Приложение SHALL брать перевод меток видов работ (`kind_labels`) и типов связей Jira (`link_types`) из `GET /settings`
|
||
и не содержать собственных копий этих словарей. Если нужного словаря в настройках нет, страница SHALL сообщать, какого
|
||
словаря не хватает, вместо пустой доски или списка без подписей.
|
||
|
||
#### Scenario: Новый тип связи в настройках
|
||
- **WHEN** в `link_types` настроек добавлен тип и в зеркале есть связь этого типа
|
||
- **THEN** после перезагрузки страницы доска показывает связь этого типа без изменения кода приложения
|
||
|
||
### Requirement: Доска связей
|
||
Страница `/app/board` SHALL воспроизводить поведение доски связей хаба (`links.html` ревизии 3931a88) без изменений,
|
||
кроме перечисленных в этом требовании и в требовании «Источник данных доски»: построение модели из `GET /board`, CSV
|
||
фильтра Jira и JSON доски (`arm-ka-links/1`); дорожки по эпикам со свёрткой и перестановкой, служебные дорожки внизу;
|
||
карточки с перетаскиванием, растяжкой, размером по содержимому и привязкой к сетке; стрелки связей с выравниванием;
|
||
фильтры по спринтам и исполнителям; легенда со скрытием видов работ и типов связей; «+ соседи по связям», «⇄ передачи»,
|
||
«скрыть без связей», «⚑ критический путь», «▥ по датам» с колонками дат и их растяжкой; ящик «Проблемы»; таблица
|
||
связей; экспорт и импорт JSON; тема; «Сбросить раскладку». Раскладка, дорожки, фильтры и тема SHALL храниться в
|
||
`localStorage` под теми же ключами `graph-<имя>…`, что у `links.html`, с той же миграцией старых ключей.
|
||
Результат построения модели, видимой выборки, критического пути и проблем SHALL совпадать с результатом `links.html`
|
||
на одних и тех же данных и фильтрах. У карточки задачи (не у заглушки) SHALL быть значок «Открыть карточку»,
|
||
открывающий `/app/issues/{key}` окном поверх доски; ключ карточки по-прежнему открывает задачу в Jira. Пока окно открыто
|
||
и после его закрытия доска SHALL сохранять прокрутку, раскладку, фильтры и строку состояния пакета. Действия «Сменить токен» в меню
|
||
доски нет — оно в верхней панели приложения. Ещё два отличия от `links.html` (R-10, R-18): двойной щелчок по уголку
|
||
карточки меряет высоту по содержимому при новой ширине, а не при прежней; указатель при перетаскивании и растяжке
|
||
захватывается на нажатом элементе, а не на карточке, поэтому двойной щелчок по уголку сбрасывает размер. Загрузка файла CSV/JSON доступна только после входа, с экрана входа её нет.
|
||
При возврате на `/app/board` с другой страницы приложения в той же вкладке доска SHALL восстанавливать прежнюю
|
||
прокрутку; после перезагрузки страницы доска открывается сверху.
|
||
|
||
#### Scenario: Раскладка переживает переход
|
||
- **WHEN** раскладка карточек сохранена страницей `links.html`, открыта `/app/board` с теми же данными
|
||
- **THEN** карточки стоят на тех же местах, дорожки в том же порядке и свёртке, фильтры те же
|
||
|
||
#### Scenario: Совпадение со старой доской
|
||
- **WHEN** на одной фикстуре `GET /board` и одном наборе фильтров построены модель, видимая выборка, критический путь
|
||
и проблемы старой страницей и приложением
|
||
- **THEN** результаты совпадают
|
||
|
||
#### Scenario: Открыть карточку
|
||
- **WHEN** на карточке AURORA-1620 нажат значок «Открыть карточку»
|
||
- **THEN** адрес `/app/issues/AURORA-1620`, карточка задачи открыта окном, под ним доска
|
||
|
||
#### Scenario: Возврат с карточки
|
||
- **WHEN** доска прокручена вниз, нажат значок «Открыть карточку», затем окно закрыто
|
||
- **THEN** адрес `/app/board`, доска показана с той же прокруткой и раскладкой, без повторной загрузки `GET /board`
|
||
|
||
#### Scenario: Возврат из другого раздела
|
||
- **WHEN** доска прокручена вниз, выбран раздел «Задачи», затем раздел «Доска»
|
||
- **THEN** доска показана с той же прокруткой
|
||
|
||
### Requirement: Источник данных доски
|
||
Доска SHALL хранить явный источник данных — хаб или файл. Пока показан снимок из файла (CSV или JSON-снимок), ответы
|
||
хаба SHALL его не заменять; «Перечитать» SHALL возвращать источник «хаб» и загружать `GET /board` заново без обращения к
|
||
Jira. Импорт JSON в режиме хаба SHALL применять раскладку, дорожки и фильтры и перечитывать хаб. «Обновить из Jira»
|
||
SHALL проверять агента (`GET /agent`; офлайн — предупреждение, пакет всё равно ставится), ставить
|
||
`POST /refresh {full: true, author: "viewer"}`, опрашивать `GET /batches/{id}` каждые 3 с одним опросом до
|
||
терминального статуса пакета и затем перечитывать `GET /board`. Кнопка «Обновить из Jira» SHALL быть недоступна,
|
||
пока запрос `/refresh` в пути или пакет выполняется. Незавершённый пакет `refresh` с автором `viewer` SHALL
|
||
подхватываться при каждой успешной загрузке из хаба, кроме перечитывания снимка после завершения собственного пакета; пакеты `viewer` с другими командами не подхватываются. Опрос пакета SHALL прекращаться при ответе 404 и после 5 ошибок
|
||
«хаб не отвечает» подряд, с сообщением и снова доступной кнопкой. Шапка доски SHALL показывать
|
||
источник данных (данные хаба на время последнего полного прогона; «из файла») и одно текущее событие (загрузка; пакет
|
||
выполняется / выполнен / ошибка с текстом; хаб не отвечает — прежние данные, а при снимке из файла — «на доске снимок из файла»; агент офлайн — пакет поставлен; ошибка
|
||
разбора файла — «файл не разобран: <причина>»); новое событие заменяет предыдущее целиком. Пока пакет выполняется,
|
||
«Перечитать» и загрузка файла SHALL не стирать событие пакета.
|
||
|
||
#### Scenario: Поздний ответ хаба не затирает файл
|
||
- **WHEN** запрос `GET /board` в пути, и в это время загружен CSV
|
||
- **THEN** после ответа хаба на доске по-прежнему данные CSV с пометкой «из файла»
|
||
|
||
#### Scenario: Нет второго полного прогона
|
||
- **WHEN** нажата «Обновить из Jira» и, пока пакет выполняется, доска перерисована (фильтр, «Перечитать»)
|
||
- **THEN** кнопка остаётся недоступной, в хабе один пакет, его опрашивает один опрос
|
||
|
||
#### Scenario: Пакет пропал
|
||
- **WHEN** опрос пакета получил 404
|
||
- **THEN** опрос остановлен, показано сообщение, кнопка «Обновить из Jira» снова доступна
|
||
|
||
#### Scenario: Пакет после «Перечитать»
|
||
- **WHEN** пакет `viewer` поставлен с другой вкладки и не завершён, нажато «Перечитать»
|
||
- **THEN** доска показывает «пакет #N выполняется» и по завершении перечитывает данные
|
||
|
||
#### Scenario: «Перечитать» во время пакета
|
||
- **WHEN** пакет `viewer` выполняется и нажато «Перечитать»
|
||
- **THEN** шапка по-прежнему показывает «пакет #N выполняется»
|
||
|
||
#### Scenario: Хаб недоступен при снимке из файла
|
||
- **WHEN** хаб не отвечает и загружен JSON-снимок доски, либо на доске снимок из CSV и хаб не ответил на перечитывание
|
||
снимка после завершения пакета
|
||
- **THEN** шапка показывает «хаб не отвечает — на доске снимок из файла», а не «показаны прежние данные»
|
||
|
||
#### Scenario: Чужой пакет не подхватывается
|
||
- **WHEN** незавершённый пакет автора `viewer` состоит из команд `ensure_fields`
|
||
- **THEN** доска его не опрашивает, кнопка «Обновить из Jira» доступна
|
||
|
||
### Requirement: Список задач
|
||
Страница `/app/issues` SHALL показывать задачи зеркала из `GET /issues` таблицей: ключ (открывает карточку задачи окном поверх списка), тема,
|
||
тип, статус Jira, исполнитель, спринт, срок (`planned_end`, иначе `duedate`), эпик (имя эпика, иначе ключ), вид работы,
|
||
статус по факту, признак «сосед». Фильтры SHALL соответствовать параметрам `GET /issues` (спринт, исполнитель, вид
|
||
работы, статус по факту, статус Jira, метка, эпик, только соседи, поиск по ключу или теме), храниться в query-строке
|
||
адреса и применяться сервером; варианты выпадающих списков SHALL браться из снимка `GET /board`; значение фильтра из адреса, которого нет среди
|
||
вариантов, SHALL показываться в списке с пометкой «(нет на доске)». Задачи SHALL
|
||
загружаться порциями по 50 строк; «Показать ещё» SHALL быть доступна, пока последняя порция полная.
|
||
|
||
#### Scenario: Фильтр в адресе
|
||
- **WHEN** выбран спринт «AWPCA_Спринт 2» и исполнитель
|
||
- **THEN** адрес содержит `sprint=` и `assignee=`, таблица — результат `GET /issues` с этими параметрами; открытие этого
|
||
адреса в новой вкладке показывает ту же выборку
|
||
|
||
#### Scenario: Конец списка
|
||
- **WHEN** вторая порция вернула 17 задач
|
||
- **THEN** в таблице 67 задач, «Показать ещё» не показана
|
||
|
||
#### Scenario: Фильтр по закрытому спринту
|
||
- **WHEN** открыт адрес `/app/issues?sprint=AWPCA_Спринт 0`, а в снимке доски такого спринта нет
|
||
- **THEN** выпадающий список «Спринт» показывает «AWPCA_Спринт 0 (нет на доске)» выбранным, таблица — по этому фильтру
|
||
|
||
#### Scenario: Карточка поверх списка
|
||
- **WHEN** список отфильтрован по спринту, прокручен и догружен «Показать ещё», открыта карточка задачи и окно закрыто
|
||
- **THEN** адрес списка с тем же фильтром, в таблице те же строки, прокрутка та же
|
||
|
||
### Requirement: Карточка задачи
|
||
Адрес `/app/issues/{key}` SHALL открывать карточку задачи из `GET /issues/{key}` модальным окном поверх страницы, с
|
||
которой её открыли (список «Задачи» или доска); страница под окном SHALL оставаться смонтированной и сохранять фильтры,
|
||
загруженные строки, прокрутку и раскладку. Перезагрузка страницы с открытым окном SHALL сохранять страницу под окном. Открытый напрямую
|
||
адрес (новая вкладка, закладка) SHALL
|
||
показывать окно поверх списка задач без фильтров. Окно SHALL закрываться кнопкой ✕, клавишей Esc и щелчком по фону (нажатие и отпускание — на фоне; выделение текста,
|
||
отпущенное за краем окна, окно не закрывает) и
|
||
возвращать на страницу, с которой его открыли, даже после переходов по связям внутри окна; «назад» браузера после
|
||
закрытия MUST NOT открывать карточку снова. Переход по связанной задаче SHALL менять содержимое окна и адрес, «назад»
|
||
браузера — возвращать к прежней карточке. Пока окно открыто, фокус SHALL оставаться в окне, страница под ним не
|
||
прокручивается.
|
||
|
||
Содержимое окна:
|
||
- шапка: ключ, статус Jira, статус по факту (цветом класса из `status_labels`), «Открыть в Jira», ✕, тема; строка
|
||
«исполнитель · срок · спринт · эпик» без пустых частей, срок — `planned_end`, иначе `duedate`, с пометкой «просрочено»,
|
||
если он раньше сегодняшней даты и класс статуса по факту не `fact-done` и не `fact-cancelled`; эпик — темой
|
||
(`epic_summary`), иначе ключом;
|
||
- «Связи» по группам в порядке: «Зависит от» (входящие связи логических типов `блокирует начало` и `блокирует
|
||
завершение` из `link_types`, с меткой «начало» / «завершение»), «Блокирует» (исходящие тех же типов, с теми же метками),
|
||
«Связана с» (тип `связанные` в обе стороны), «Прочие» (тип Jira вне `link_types`: имя типа и направление «→» / «←»);
|
||
в строке — ключ (открывает связанную задачу в окне), тема и статус Jira, а если задачи нет в зеркале — ключ и «нет в
|
||
зеркале»; пустые группы не показываются, без связей — «связей нет»;
|
||
- «Описание» — текст, отрендеренный по требованию «Разметка Jira в карточке», с кнопкой «исходник» / «текст» в
|
||
заголовке раздела; длиннее 8 строк — свёрнуто с кнопкой «развернуть» / «свернуть» в обоих режимах; без описания
|
||
раздела нет;
|
||
- «Комментарии (N)» — новые сверху, автор, время, кнопка «исходник» / «текст» у каждого комментария, текст,
|
||
отрендеренный по требованию «Разметка Jira в карточке»; кнопка «Скрыть» / «Показать» сворачивает раздел, выбор SHALL
|
||
запоминаться в браузере для всех карточек; без комментариев — «комментариев нет»;
|
||
- «Служебные заметки» (требование «Служебные заметки»);
|
||
- «Прочие поля» — непустые из: тип, приоритет, автор, метки, плановое начало, обновлена в Jira, сосед по связям;
|
||
- «Команды моста (N)» — 10 последних команд по задаче (id, пакет, тип, статус, время завершения, ошибка), раздел свёрнут
|
||
при открытии; если команд не было — текст «команд по задаче не было».
|
||
На ширине от 900 px «Связи», «Описание», «Комментарии» — в левой колонке, заметки, прочие поля и команды — в правой;
|
||
уже — одна колонка: заметки, связи, описание, комментарии, прочие поля, команды. Если задачи нет в зеркале (404), окно
|
||
SHALL сообщать об этом с кнопкой «Закрыть»; хаб не отвечает — сообщение и «Повторить», ранее загруженные данные
|
||
остаются.
|
||
|
||
#### Scenario: Связанная задача
|
||
- **WHEN** у AURORA-1620 исходящая связь `ASUPR_Блокер_Начала` на AURORA-1621 «Расчёт свода» в статусе «В работе»
|
||
- **THEN** в карточке в группе «Блокирует» строка «начало · AURORA-1621 · Расчёт свода · В работе»; щелчок по ключу
|
||
меняет содержимое окна на AURORA-1621, адрес — `/app/issues/AURORA-1621`; «назад» браузера возвращает AURORA-1620
|
||
|
||
#### Scenario: Входящий блокер
|
||
- **WHEN** у AURORA-1620 входящая связь `ASUPR_Блокер_Завершения` от AURORA-1500
|
||
- **THEN** строка с AURORA-1500 и меткой «завершение» — в группе «Зависит от»
|
||
|
||
#### Scenario: Задача без команд
|
||
- **WHEN** по AURORA-1620 не было команд
|
||
- **THEN** в разделе «Команды моста» — текст «команд по задаче не было», таблицы нет
|
||
|
||
#### Scenario: Закрытие после переходов
|
||
- **WHEN** со списка «Задачи» открыта AURORA-1620, в окне — переход по связи на AURORA-1621, нажата Esc
|
||
- **THEN** окно закрыто, адрес — адрес списка с прежними фильтрами; «назад» браузера не открывает карточку
|
||
|
||
#### Scenario: Прямой адрес
|
||
- **WHEN** в новой вкладке открыт `/app/issues/AURORA-1620`, затем нажат ✕
|
||
- **THEN** под окном был список задач без фильтров; после закрытия адрес `/app/issues`, «назад» не открывает карточку
|
||
|
||
#### Scenario: Скрытые комментарии
|
||
- **WHEN** в карточке нажато «Скрыть» у комментариев, затем страница перезагружена и открыта другая карточка
|
||
- **THEN** комментарии в ней скрыты, видна кнопка «Показать» и число комментариев
|
||
|
||
### Requirement: Служебные заметки
|
||
Карточка задачи SHALL показывать служебные заметки в режиме чтения: «Вид работы», «Статус по факту», «Сделано»,
|
||
«Осталось», «Заметка владельца» (пустые — «—») и время и автор последнего изменения; кнопка «Править» SHALL открывать
|
||
форму: «вид работы» — выбор из значений `kind_labels` настроек (и текущего значения, если его там нет, и пустого),
|
||
«статус по факту» — текст с подсказками из ключей `status_labels`, «сделано», «осталось», «заметка владельца» —
|
||
многострочный текст; «Отмена» SHALL возвращать режим чтения, при несохранённых правках — после подтверждения.
|
||
Сохранение SHALL отправлять `PUT /issues/{key}/note` только с изменёнными полями и `updated_by: "web"`; очищенное поле
|
||
отправляется как `null`. После успеха SHALL показываться режим чтения с сохранённой заметкой; список задач под окном SHALL
|
||
показывать новые «вид работы» и «статус по факту» без «Перечитать», доска под окном SHALL перечитать снимок
|
||
(`GET /board`) без потери раскладки и прокрутки; при 422 — текст ошибки у формы без
|
||
потери введённого. Закрытие окна, переход по связи и уход в другой раздел с несохранёнными правками SHALL требовать
|
||
подтверждения. Заметки пишутся только в хаб, в Jira приложение ничего не отправляет.
|
||
|
||
#### Scenario: Только изменённые поля
|
||
- **WHEN** в заметке нажато «Править», изменено только «осталось» и нажато «Сохранить»
|
||
- **THEN** тело `PUT /issues/{key}/note` — `{rest_text: "…", updated_by: "web"}`; карточка снова в режиме чтения с новым
|
||
«Осталось»
|
||
|
||
#### Scenario: Уход с несохранёнными правками
|
||
- **WHEN** в форме заметки есть несохранённая правка и нажата Esc
|
||
- **THEN** приложение спрашивает подтверждение; при отказе окно открыто, форма с введённым текстом
|
||
|
||
#### Scenario: Заметка видна на доске
|
||
- **WHEN** карточка AURORA-9002 открыта с доски, вид работы изменён на «тестирование» и сохранён
|
||
- **THEN** доска под окном остаётся на месте и перезапрашивается (`GET /board` отдаёт `work_kind` «тестирование»); окно закрывается без потери раскладки и прокрутки доски
|
||
|
||
### Requirement: Сборка и разработка
|
||
Приложение SHALL быть npm-проектом в `web/` (React, TypeScript, Vite) с точными версиями зависимостей и
|
||
`package-lock.json`. `make web` SHALL устанавливать зависимости (`npm ci`) и собирать приложение в
|
||
`src/jira_hub/web/app/`; каталог сборки не хранится в git. Шрифты и прочие ресурсы
|
||
страницы SHALL входить в сборку: приложение не обращается к внешним адресам. `make check` SHALL запускать `uv run pytest`, проверку типов
|
||
и линтер фронта и тесты фронта; зелёным считается только весь набор. `npm run dev` SHALL поднимать сервер разработки с
|
||
прокси маршрутов API на хаб из `JIRA_HUB_URL` (по умолчанию тестовый `http://127.0.0.1:8791`).
|
||
|
||
#### Scenario: Сборка с нуля
|
||
- **WHEN** в чистом checkout выполнен `make web`
|
||
- **THEN** в `src/jira_hub/web/app/` есть `index.html` и `assets/`; `git status` не показывает файлов сборки
|
||
|
||
#### Scenario: Без интернета
|
||
- **WHEN** приложение открыто на машине без доступа в интернет
|
||
- **THEN** страница не запрашивает ресурсов вне хаба, текст набран IBM Plex Sans
|
||
|
||
### Requirement: Разметка Jira в карточке
|
||
Описание задачи и текст каждого комментария в карточке SHALL показываться отрендеренной вики-разметкой Jira (хаб
|
||
хранит их как есть, в wiki) так, как их показывает Jira. Разбираются:
|
||
- блоки: заголовки `h1.`–`h6.` в начале строки; списки — строки, начинающиеся (после необязательных пробелов) с
|
||
маркеров `*`, `#` и пробела (уровень вложенности — числом маркеров, `*` — маркированный, `#` — нумерованный,
|
||
смешанная вложенность `*#` — по последнему маркеру) или с одного `-` и пробела (маркированный, первый уровень);
|
||
`--`, `*-` и прочие сочетания с `-` — не список; таблицы — строки, начинающиеся с
|
||
`|`, ячейки `||заголовок||` и `|ячейка|`; `{code}` / `{code:язык}` … `{code}` и `{noformat}` … `{noformat}` —
|
||
моноширинный блок без разбора разметки внутри; `{quote}` … `{quote}` и строка `bq. ` — цитата; открывающий тег
|
||
блока — в начале строки, закрывающий — в любом месте той же или последующей строки, текст после закрывающего тега
|
||
разбирается дальше как обычные строки; тег не в начале строки — текст как есть; строка `----` —
|
||
горизонтальная черта; прочие строки — абзацы, разделённые пустыми строками, одиночный перенос внутри абзаца —
|
||
перенос строки;
|
||
- внутри строки: `*жирный*`, `_курсив_`, `-зачёркнутый-`, `+подчёркнутый+`, `^верхний^`, `~нижний~`, `{{моно}}`
|
||
(`{{…}}` — без разбора внутри); маркер выделения действует, только если открывающий стоит в начале текста или после
|
||
пробела либо знака препинания и за ним не пробел и не тот же символ (`**x**` — текст как есть), а закрывающий стоит после не-пробела и за ним не буква и не
|
||
цифра (`data_type`, `2026-10-15`, `счёт-фактура`, `a*b*c` — обычный текст); `\\` — перенос строки; `\` перед
|
||
символом разметки — сам символ;
|
||
- ссылки: `[текст|адрес]`, `[адрес]` и голые адреса `http://`, `https://` — ссылка в новой вкладке, только для схем
|
||
`http`, `https`, `mailto`, иначе текст как есть; `[ключ задачи]`, `[текст|ключ задачи]` и ключ задачи в тексте вида
|
||
`ПРОЕКТ-123`, где `ПРОЕКТ` — `project_key` из настроек хаба, — ссылка, открывающая карточку этой задачи в окне
|
||
(`UTF-8`, `SHA-256` и ключи других проектов — текст);
|
||
`[~логин]` — «@логин».
|
||
Всё, что разборщик не распознал (`{panel}`, `{color}`, `!картинка!`, незакрытые маркеры и блоки и прочее), SHALL
|
||
показываться как текст исходника без потерь. Разметка MUST NOT превращаться в HTML строкой (`innerHTML`,
|
||
`dangerouslySetInnerHTML`): текст задачи не может добавить на страницу элементы, атрибуты или скрипты. Если рендер
|
||
блока завершился ошибкой, блок SHALL показываться исходником.
|
||
|
||
Кнопка «исходник» у описания и у каждого комментария SHALL переключать этот блок на исходник — текст с сохранёнными
|
||
переносами строк, как без разбора разметки, — и обратно кнопкой «текст»; остальные блоки не переключаются. При каждом
|
||
открытии карточки (в том числе при переходе по связи) все блоки показываются отрендеренными, выбор не запоминается.
|
||
|
||
#### Scenario: Описание со структурой
|
||
- **WHEN** описание AURORA-1620 — `h3. Контекст`, строка `Шаги:`, затем `# Загрузить ОСВ` и `# Открыть документ`,
|
||
затем пустая строка и `*Итог:* ошибка в {{data_type}}`
|
||
- **THEN** в «Описании» заголовок «Контекст», абзац «Шаги:», нумерованный список из двух пунктов и абзац, где «Итог:» —
|
||
жирным, «data_type» — моноширинным; символов `h3.`, `#`, `*`, `{{` не видно
|
||
|
||
#### Scenario: Переключение на исходник
|
||
- **WHEN** в карточке AURORA-1620 у описания нажато «исходник»
|
||
- **THEN** описание показано исходником с переносами строк (видны `h3. Контекст` и `# Загрузить ОСВ`), кнопка — «текст»;
|
||
комментарии остаются отрендеренными; после «текст» описание снова отрендерено
|
||
|
||
#### Scenario: Исходник у одного комментария
|
||
- **WHEN** у второго комментария нажато «исходник», затем открыта связанная задача и возврат «назад»
|
||
- **THEN** сразу после нажатия исходником показан только второй комментарий; после возврата все комментарии
|
||
отрендерены
|
||
|
||
#### Scenario: Ключ задачи в тексте
|
||
- **WHEN** комментарий — `П. 1 ведётся в AURORA-1445; AURORA-1645 снята как дубль.`
|
||
- **THEN** «AURORA-1445» и «AURORA-1645» — ссылки; щелчок по первой меняет содержимое окна на AURORA-1445
|
||
|
||
#### Scenario: Неизвестная разметка и опасный адрес
|
||
- **WHEN** описание — `{color:red}важно{color}`, `!схема.png!` и `[нажми|javascript:alert(1)]`
|
||
- **THEN** все три показаны текстом исходника как есть, ссылки и картинки нет, скрипт не выполняется
|
||
|
||
#### Scenario: Списки с отступом и пустыми строками
|
||
- **WHEN** описание — `Шаги воспроизведения:`, ` * Загрузить ОСВ.`, пустая строка, ` * Дождаться обработки.`
|
||
- **THEN** абзац «Шаги воспроизведения:» и маркированные пункты «Загрузить ОСВ.» и «Дождаться обработки.» без
|
||
звёздочек
|
||
|
||
#### Scenario: Обычный текст не портится
|
||
- **WHEN** описание — `Поле data_type = null, дата 2026-10-15, оборот 18 803 903,65 - 9 351 102,89, a*b*c, UTF-8,
|
||
**важно**`
|
||
- **THEN** текст показан как есть, без курсива, зачёркивания, жирного и ссылок
|