Админка пресетов — CRUD системных стилей

Управление каталогом системных пресетов AI Студии. Пресеты доступны всем пользователям сервиса (системные = глобальные на франшизу), редактируются только владельцами с gensvc.preset.admin.

Файлы

  • ai-photo-studio-frontend/src/pages/AdminPresetsPage.tsx — список + grid + фильтры + пагинация
  • ai-photo-studio-frontend/src/pages/AdminPresetCreatePage.tsx — создание нового пресета
  • ai-photo-studio-frontend/src/components/admin/PresetGrid.tsx — сетка карточек с hover-actions
  • ai-photo-studio-frontend/src/components/admin/ConfirmDialog.tsx — модалка подтверждения

Роуты

PathComponentНазначение
/admin/presetsAdminPresetsPageСписок со всеми системными пресетами
/admin/presets/newAdminPresetCreatePageФорма создания нового пресета

Доступ

Permission gensvc.preset.admin. Роуты обёрнуты в <RequirePermission perm="gensvc.preset.admin"> — без него редирект на /no-access.

Layout — список

┌─────────────────────────────────────────────────────────┐
│ Админка пресетов               [+ Новый пресет]         │
├─────────────────────────────────────────────────────────┤
│ [Все] [Активные] [Архив] │ [Категория ▾] │ [Поиск...]  │  ← Filters bar
│                                              278 пресетов│
├─────────────────────────────────────────────────────────┤
│ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐         │
│ │ img │ │ img │ │ img │ │ img │ │ img │ │ img │         │  ← PresetGrid
│ │ name│ │ name│ │ name│ │ name│ │ name│ │ name│         │
│ │ slug│ │ slug│ │ slug│ │ slug│ │ slug│ │ slug│         │
│ └─────┘ └─────┘ └─────┘ └─────┘ └─────┘ └─────┘         │
│ ... 50 карточек на страницу ...                         │
├─────────────────────────────────────────────────────────┤
│              [Показать ещё (228)]                       │
└─────────────────────────────────────────────────────────┘

Фильтры (Filters bar)

ФильтрТипПоведение
Tabs «Все / Активные / Архив»enabled: all | true | falseЧипы. Изменение → reset offset → fetch
Категорияselectcategory=... query, all = без фильтра
ПоискinputДебаунс 250 мс. Ищет по name_en, name_ru, slug

Один useEffect, один fetch

Раньше было два useEffect с пересекающимися deps — на смену фильтра шёл двойной API-запрос. Сейчас остался один: при смене enabled/category/q setOffset(0) в setter’е, единственный effect отрабатывает на новых deps.

Карточка пресета (PresetGrid item)

Каждая карточка: квадрат 220×220, превью reference-изображения, под ним name_ru, мелким — slug.

При hover на карточку:

  • Лёгкая тень + scale.
  • Overlay из 4 круглых кнопок:
    • ✏️ Редактировать — переход к форме (placeholder, в текущем MVP not-implemented)
    • 🖼 Заменить изображение — file picker → POST /v1/admin/presets/{id}/reference
    • 🚫 / 👁 Toggle enabledPATCH /v1/admin/presets/{id} { enabled: !current }
    • 🗑 УдалитьConfirmDialog (см. ниже) → DELETE /v1/admin/presets/{id}

Пресеты с enabled: false отображаются с полупрозрачностью.

ConfirmDialog

Модалка подтверждения удаления.

  • role="dialog", aria-modal, aria-labelledby.
  • Escape → cancel (через document.addEventListener("keydown") в effect).
  • Default focus при открытии — кнопка «Отмена» (безопаснее для destructive prompts).
  • Prop danger: true → кнопка confirm красная (colors.danger); false → стандартный colors.red.
  • Overlay click → cancel.

Создание пресета (/admin/presets/new)

Форма:

  • Slug (auto-generate из name_en при отсутствии)
  • Name EN, Name RU
  • Category (select)
  • Reference image (file picker, jpg/png/webp ≤10 МБ)
  • Submit → POST /v1/admin/presets → редирект на /admin/presets

Ошибки валидации — inline под полями.

Пагинация — load more

PAGE_SIZE = 50, total — из meta.total ответа списка. canLoadMore = items.length < total. На клике — setOffset(prev => prev + PAGE_SIZE), единственный useEffect перезапросит и append (fetchJobs(append=offset>0)).

Состояния

СостояниеUI
loading && items.length === 0Spinner по центру
errorКрасная плашка с текстом ошибки
items.length === 0 (после загрузки)Empty state: «Пресеты не найдены. Попробуйте изменить фильтры»
Загрузка loadMoreSpinner снизу под grid

Связанные API

  • GET /v1/admin/presets?enabled=&category=&q=&limit=&offset= — список (admin)
  • POST /v1/admin/presets — создать
  • PATCH /v1/admin/presets/{id} — обновить (enabled / name / category)
  • POST /v1/admin/presets/{id}/reference — заменить картинку (multipart)
  • DELETE /v1/admin/presets/{id} — удалить (мягко, потом hard если нет FK)

Все защищены RequirePermission("gensvc.preset.admin") на бэке.

Похожие админ-экраны (paste-template)

  • Шаблоны постеров (/admin/poster-templates) — AdminPosterTemplatesPage.tsx. Та же раскладка: Tabs / Category / Search / Grid / ConfirmDialog. CRUD на POST/PATCH/DELETE /v1/admin/poster-templates.
  • Композиция: тарелки и фоны (/admin/composition) — AdminCompositionAssetsPage.tsx. Аналогично, плюс toggle kind: vessel \| background в фильтре.

Эти экраны следуют тому же паттерну (filters bar + hover-grid + ConfirmDialog), отдельной спекой не выделены пока — копия данной с заменой сущности.

Ссылки