feat(approval): Approval Workflow v2 Phase 2 — admin UI reviewer queue + toggle
Phase 2 admin UI на скелете Phase 0/1 (e438c4c,2020af9). Делает workflow end-to-end usable: reviewer'ы могут approve/reject через UI; admins включают approval_required per-dict через checkbox. API surface (admin-ui): - types: DraftStatus, DraftOperation, DraftResponse, SubmitDraftRequest, ReviewQueuePage. + DictionaryDefinition: approvalRequired, approvalMinRole. - queries (react-query): * useReviewQueue (page, size) — global queue, refetch 30s. * useDraft(id) — single draft for diff drawer. * useLiveRecord(dict, bk) — current live record for side-by-side comparison. - mutations: * useSubmitDraft(dict) — POST /dictionaries/{n}/records/drafts * useApproveDraft() — POST /drafts/{id}/approve * useRejectDraft() — POST /drafts/{id}/reject (reason required) * useWithdrawDraft() — DELETE /drafts/{id} New /reviews route: - Table queue: dict, business_key, operation badge, maker, submitted_at. - Click "Просмотр" → side-by-side drawer: * left: current live (если есть; для CREATE — placeholder). * right: proposed (или CLOSE notice). * maker comment chip если есть. * Reviewer comment input + Approve / Reject buttons. * Reject disabled пока comment пустой (mirror's backend 400). - Auto-invalidates на success — queue refresh, list updates. - Nav link "Согласования" в header. DictionaryEditorDialog Metadata tab: - New checkbox "Требовать approval (maker-checker)" — orbit/8 styled (отделимо от Redis projection card visually). - При checked → shown approval_min_role TextInput (e.g. "ordinis:internal"). - payload sends approvalRequired + approvalMinRole. i18n RU/EN: - nav.reviews, reviews.{title,description,empty,queueTotal_*,col.*, action.*, drawer.*} - schema.approvalRequired.{label,hint}, schema.approvalMinRole.{label,hint} - Plurals для RU queueTotal (one/few/many/other). Verify: - pnpm tsc --noEmit: clean. - pnpm test (vitest): 89/89 PASS. - pnpm build: clean. - routeTree.gen.ts auto-regenerated с /reviews. Phase 3 (next session): - e2e tests: full flow maker submit → checker approve → record live + outbox event. - e2e: reject keeps draft, doesn't publish. - e2e: self-approve blocked (no_self_approve constraint). - e2e: two pending drafts на one business_key blocked (one_pending_per_key). - Per-dict opt-in soak: 1-2 dicts (например ground_station) с approval_required=true в staging.
This commit is contained in:
@@ -18,6 +18,38 @@ i18n
|
||||
'nav.outbox': 'Outbox',
|
||||
'nav.webhooks': 'Webhooks',
|
||||
'nav.search': 'Поиск',
|
||||
'nav.reviews': 'Согласования',
|
||||
'reviews.title': 'Очередь согласований',
|
||||
'reviews.description': 'Pending drafts от makers, ждут approve/reject. Pool model: первый approver обрабатывает.',
|
||||
'reviews.empty': 'Нет pending согласований. Все обработаны 👍',
|
||||
'reviews.queueTotal_one': '{{count}} pending draft',
|
||||
'reviews.queueTotal_few': '{{count}} pending draft',
|
||||
'reviews.queueTotal_many': '{{count}} pending drafts',
|
||||
'reviews.queueTotal_other': '{{count}} pending drafts',
|
||||
'reviews.col.dict': 'Справочник',
|
||||
'reviews.col.bk': 'Business key',
|
||||
'reviews.col.op': 'Операция',
|
||||
'reviews.col.maker': 'Автор',
|
||||
'reviews.col.submitted': 'Отправлен',
|
||||
'reviews.col.actions': 'Действия',
|
||||
'reviews.action.review': 'Просмотр',
|
||||
'reviews.action.approve': 'Approve',
|
||||
'reviews.action.reject': 'Reject',
|
||||
'reviews.action.cancel': 'Отмена',
|
||||
'reviews.drawer.title': 'Согласование draft',
|
||||
'reviews.drawer.maker': 'автор',
|
||||
'reviews.drawer.submitted': 'отправлен',
|
||||
'reviews.drawer.makerComment': 'Комментарий автора',
|
||||
'reviews.drawer.live': 'Текущая (live)',
|
||||
'reviews.drawer.proposed': 'Предложено',
|
||||
'reviews.drawer.noLive': 'Нет live-записи (CREATE).',
|
||||
'reviews.drawer.closeNote': 'CLOSE: запись будет закрыта (valid_to=now).',
|
||||
'reviews.drawer.comment': 'Комментарий ревьюера',
|
||||
'reviews.drawer.commentPlaceholder': 'Для approve опционально, для reject обязательно',
|
||||
'schema.approvalRequired.label': 'Требовать approval (maker-checker)',
|
||||
'schema.approvalRequired.hint': 'Approval Workflow v2. Все CRUD операции пойдут через draft → review → approve. Защита от operator error на критичных справочниках. Существующие маршруты (POST/PUT/DELETE) вернут 409 draft_required с pointer на /drafts API.',
|
||||
'schema.approvalMinRole.label': 'Минимальная роль ревьюера (опционально)',
|
||||
'schema.approvalMinRole.hint': 'Например ordinis:internal или ordinis:restricted. Пусто = любой пользователь с ролью ordinis:approver. (Phase 2 — enforcement TBD).',
|
||||
'search.title': 'Smart search',
|
||||
'search.description': 'Поиск по содержимому всех справочников. Активные записи, текущий момент. Min 3 символа (trigram index).',
|
||||
'search.placeholder': 'Введите 3+ символа: код, название, идентификатор…',
|
||||
@@ -390,6 +422,36 @@ i18n
|
||||
'nav.outbox': 'Outbox',
|
||||
'nav.webhooks': 'Webhooks',
|
||||
'nav.search': 'Search',
|
||||
'nav.reviews': 'Reviews',
|
||||
'reviews.title': 'Review queue',
|
||||
'reviews.description': 'Pending drafts from makers awaiting approve/reject. Pool model: first approver handles.',
|
||||
'reviews.empty': 'No pending reviews. All clear 👍',
|
||||
'reviews.queueTotal_one': '{{count}} pending draft',
|
||||
'reviews.queueTotal_other': '{{count}} pending drafts',
|
||||
'reviews.col.dict': 'Dictionary',
|
||||
'reviews.col.bk': 'Business key',
|
||||
'reviews.col.op': 'Operation',
|
||||
'reviews.col.maker': 'Maker',
|
||||
'reviews.col.submitted': 'Submitted',
|
||||
'reviews.col.actions': 'Actions',
|
||||
'reviews.action.review': 'Review',
|
||||
'reviews.action.approve': 'Approve',
|
||||
'reviews.action.reject': 'Reject',
|
||||
'reviews.action.cancel': 'Cancel',
|
||||
'reviews.drawer.title': 'Review draft',
|
||||
'reviews.drawer.maker': 'maker',
|
||||
'reviews.drawer.submitted': 'submitted',
|
||||
'reviews.drawer.makerComment': 'Maker comment',
|
||||
'reviews.drawer.live': 'Current (live)',
|
||||
'reviews.drawer.proposed': 'Proposed',
|
||||
'reviews.drawer.noLive': 'No live record (CREATE).',
|
||||
'reviews.drawer.closeNote': 'CLOSE: record will be closed (valid_to=now).',
|
||||
'reviews.drawer.comment': 'Reviewer comment',
|
||||
'reviews.drawer.commentPlaceholder': 'Optional for approve, required for reject',
|
||||
'schema.approvalRequired.label': 'Require approval (maker-checker)',
|
||||
'schema.approvalRequired.hint': 'Approval Workflow v2. All CRUD goes through draft → review → approve. Protects critical dictionaries from operator error. Existing endpoints (POST/PUT/DELETE) return 409 draft_required pointing to /drafts API.',
|
||||
'schema.approvalMinRole.label': 'Minimum reviewer role (optional)',
|
||||
'schema.approvalMinRole.hint': 'e.g. ordinis:internal or ordinis:restricted. Empty = any user with ordinis:approver. (Phase 2 — enforcement TBD).',
|
||||
'search.title': 'Smart search',
|
||||
'search.description': 'Search across all dictionary content. Active records, current moment. Min 3 chars (trigram index).',
|
||||
'search.placeholder': 'Type 3+ chars: code, name, identifier…',
|
||||
|
||||
Reference in New Issue
Block a user