[feature][ui][roles] Каталог ролей: интеграция редизайна модалки из handoff/ (карточки наборов, сводные статусы, пакетные обновления) #371
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Контекст
Модалка «Каталог ролей» (админ-настройки → роли агентов) переделана дизайнером по ТЗ. Хендофф не закоммичен в репозиторий — его содержимое приложено в комментариях к этой ишье:
handoff/RoleCatalogModal.tsx— готовый референс-компонент на Mantine v7 (мок-данные и мок-API помечены и удаляются);handoff/README.md— описание модели данных, точек интеграции (TODO/[API]) и того, как закрыты требования ТЗ (Р1–Р12).⚠️ Упомянутые в README html-прототипы (
*.dc.html) недоступны — источник истины:RoleCatalogModal.tsx+README.mdиз комментариев ниже.Суть редизайна: окно строится вокруг наборов-карточек — сводный статус набора виден без раскрытия, у набора одно главное действие (Install bundle / Update all / Installed), язык — компактный
SegmentedControl, конфликты убраны из шапки (инлайн-плашка «Rename & install» после импорта), результат импорта показывается не закрывая окно.Что сделать
Заменить текущую реализацию
apps/client/src/features/workspace/components/settings/components/ai-agent-roles-catalog-modal.tsxна компонент из хендоффа, подключённый к реальному API. Контракт пропсов сохранить:{ opened, onClose, roles: IAiRole[] }— родительai-agent-roles.tsxне трогаем (или минимально).uiLang/availableLangsиз пропсов хендоффа вычисляются внутри (i18n +catalog.languages).1. Данные и статусы (клиент, без изменений API)
useAiRoleCatalogQuery(language, opened)→{ languages, bundles[{id, name, description}] }. Содержимое бандлов —useAiRoleCatalogBundleQuery. Новому UI статусы нужны в свёрнутых шапках, поэтому после загрузки списка грузить содержимое всех бандлов сразу (параллельно,useQueries); каталог маленький. Ленивую загрузку по раскрытию — убрать.catalogRoleInstallState(apps/client/src/features/ai-chat/utils/catalog-role-install-state.ts, матч поsource.slug + source.language):import/installed/update.importнаходит установку того жеslugс другимsource.language→installedLang.update→version=fromVersion, newVersion=toVersion;installed/import→versionиз каталога.emojiв каталоге опционален — предусмотреть отсутствие. Статусskipped— транзиентный клиентский (после импорта с конфликтом), с бэка не приходит.catalog.languages(логика уже есть в старой модалке — перенести).2. Импорт (нужна доработка сервера)
useImportAiRolesFromCatalogMutation→POST /ai-chat/roles/import{bundleId, language, slugs, conflict:'skip'}.{created, skipped, renamed, errors}(apps/server/src/core/ai-chat/roles/ai-agent-roles.service.ts,importFromCatalog), а UI нужны списки: какие роли пропущены и почему (плашка «Installed N · M skipped» с именем конфликтной роли и кнопкой «Rename & install»). Расширить ответ per-role результатами, например:onSuccessмутации вai-chat-query.tsи тестыimport-from-catalog-message.test.tsx).conflict:'rename',slugs:[slug]— API уже умеет, новых эндпоинтов не нужно.3. Обновления
useUpdateAiRoleFromCatalogMutation(roleId).updateBundleAll). Пакетный эндпоинт — вне скоупа этой ишьи.4. Качество кода
useTranslation(), добавить en/ru переводы (RU длиннее EN до ~40%,white-space:nowrapна локализуемые кнопки не ставить).anyв пропсахBundlePanel/RoleRow— полноценные интерфейсы.StatusDotзаменить на CSS-переменные Mantine (var(--mantine-color-blue-6)и т.п.).SEED,mockImport,delay, мок-флагconflict.allNew | allInstalled | updates | mixed) вынести в чистую функцию рядом сcatalogRoleInstallState— для юнит-тестов без монтирования компонента.5. Тесты
installedLang, маппинг каталога в модель компонента.import-from-catalog-message.test.tsx/update-from-catalog-message.test.tsx, если меняются сообщения/формат ответа.importFromCatalog(списки created/skipped с reason, rename-путь).Критерии приёмки
Вне скоупа
[API #3]из README) — при желании отдельной ишьёй.[API #4], вариант Р6-б с dry-run) — в компоненте реализован fallback на текущем API, его и оставляем.Хендофф (1/2):
handoff/README.md. Файлы хендоффа не закоммичены в репозиторий — содержимое приложено в этом и следующем комментариях.Каталог ролей — редизайн. Хендофф для интеграции
Файлы:
RoleCatalogModal.tsx— React-компонент на Mantine v7 (готов к интеграции; мок-данные и мок-API помечены и удаляются).Каталог ролей - прототип.dc.html— интерактивная спека поведения (эталон состояний и переходов).Каталог ролей - редизайн.dc.html.Что меняется по сути
Раньше окно строилось вокруг настроек импорта + плоского списка строк. Теперь — вокруг наборов как карточек: у каждого набора сводный статус (виден без раскрытия) и одно главное действие. Настройки (язык, конфликты) уведены на второй план, результат импорта виден не закрывая окно.
Зависимости
Компонент использует только штатные Mantine-компоненты:
Modal, Accordion, Badge, Button, Checkbox, SegmentedControl, Alert, Skeleton, ThemeIcon, Loader, Tooltip. Кастомных контролов нет. Цвета/радиусы — дефолтная тема Mantine, поэтому светлая/тёмная работают из коробки черезMantineProvider(никаких хардкод-цветов, кроме точек-индикаторов статуса).Модель данных
Фаза набора вычисляется из статусов ролей (
allNew | allInstalled | updates | mixed) — она и определяет сводку и главную кнопку. Ничего дополнительно с бэка про «фазу» слать не нужно.Точки интеграции с API
Ищи в коде метки
TODOи[API].GET catalog(lang)→CatalogBundle[]. Статус каждой роли считается на бэке относительно воркспейса. Важно: роль, установленная на другом языке, при текущемlangдолжна прийти какimport+installedLang(для подсказки Р5). РаскомментироватьuseEffect, повеситьsetView('loading' | 'error' | 'empty' | 'ready').POST import(bundleId, lang, roleIds[], policy)→{ installed[], skipped[] }. Работает на текущем API (один набор за запрос). Передаёмpolicy='skip'; конфликты возвращаются вskipped[], показываем инлайн-плашку.updateRole()с общим прогрессом на кнопке (кнопка уже в состоянииloading). Помечено[API #3].policy='skip'→ инлайн-плашка «Rename & install» → повторный импорт конфликтной роли сpolicy='rename'. Если/когда API научится возвращать конфликты до применения — заменить fallback на модальный диалог (макет1gв файле редизайна). Помечено[API #4].Как закрыты требования ТЗ
statusParts+Accordion.Control.Installed, не задизейбленная кнопка —primaryвBundlePanel.import-роли —defaultSelection, strip над списком.RoleRow.SegmentedControl, дефолт =uiLang, подсказка при установках на другом языке — блокotherLangInstalls.ResultBanner(success / partial / updated), инлайн, окно не закрывается.LoadingState / ErrorState / EmptyState+loadingна кнопках.variant="light", хит-зоны кнопок/чекбоксов ≥ стандарта Mantine.defaultValue={bundles.length <= 3 ? [bundles[0].id] : []}.1c(в компонент не добавлен — по решению команды).Поведение подгрузки
Сейчас контент набора грузится при раскрытии — компонент это сохраняет (данные ролей приходят с
getCatalog, аккордеон только показывает/прячет). Если решите грузить лениво по набору — добавьтеonChangeнаAccordionи ленивую загрузку ролей; сводка в шапке при этом должна приходить сразу (иначе статус нельзя показать без раскрытия).Локализация
Все строки в компоненте — английские литералы, вынести в i18n при интеграции. Закладывайте запас ширины: RU-лейблы длиннее EN до ~40% (кнопки набора уже гибкие,
white-space:nowrapне ставить на локализуемых кнопках).Известные упрощения мока
mockImportсимулирует конфликт по флагуconflictна роли Proofreader — удалить вместе сSEED/mockImport.updateBundleAll).Хендофф (2/2):
handoff/RoleCatalogModal.tsx— референс-компонент (Mantine v7; мок-данные и мок-API помечены и удаляются при интеграции).