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:
@@ -11,6 +11,7 @@
|
||||
import { Route as rootRouteImport } from './routes/__root'
|
||||
import { Route as WebhooksRouteImport } from './routes/webhooks'
|
||||
import { Route as SearchRouteImport } from './routes/search'
|
||||
import { Route as ReviewsRouteImport } from './routes/reviews'
|
||||
import { Route as OutboxRouteImport } from './routes/outbox'
|
||||
import { Route as DictionariesRouteImport } from './routes/dictionaries'
|
||||
import { Route as AuditRouteImport } from './routes/audit'
|
||||
@@ -30,6 +31,11 @@ const SearchRoute = SearchRouteImport.update({
|
||||
path: '/search',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const ReviewsRoute = ReviewsRouteImport.update({
|
||||
id: '/reviews',
|
||||
path: '/reviews',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const OutboxRoute = OutboxRouteImport.update({
|
||||
id: '/outbox',
|
||||
path: '/outbox',
|
||||
@@ -76,6 +82,7 @@ export interface FileRoutesByFullPath {
|
||||
'/audit': typeof AuditRoute
|
||||
'/dictionaries': typeof DictionariesRouteWithChildren
|
||||
'/outbox': typeof OutboxRoute
|
||||
'/reviews': typeof ReviewsRoute
|
||||
'/search': typeof SearchRoute
|
||||
'/webhooks': typeof WebhooksRouteWithChildren
|
||||
'/dictionaries/$name': typeof DictionariesNameRoute
|
||||
@@ -87,6 +94,7 @@ export interface FileRoutesByTo {
|
||||
'/': typeof IndexRoute
|
||||
'/audit': typeof AuditRoute
|
||||
'/outbox': typeof OutboxRoute
|
||||
'/reviews': typeof ReviewsRoute
|
||||
'/search': typeof SearchRoute
|
||||
'/dictionaries/$name': typeof DictionariesNameRoute
|
||||
'/webhooks/$id': typeof WebhooksIdRoute
|
||||
@@ -99,6 +107,7 @@ export interface FileRoutesById {
|
||||
'/audit': typeof AuditRoute
|
||||
'/dictionaries': typeof DictionariesRouteWithChildren
|
||||
'/outbox': typeof OutboxRoute
|
||||
'/reviews': typeof ReviewsRoute
|
||||
'/search': typeof SearchRoute
|
||||
'/webhooks': typeof WebhooksRouteWithChildren
|
||||
'/dictionaries/$name': typeof DictionariesNameRoute
|
||||
@@ -113,6 +122,7 @@ export interface FileRouteTypes {
|
||||
| '/audit'
|
||||
| '/dictionaries'
|
||||
| '/outbox'
|
||||
| '/reviews'
|
||||
| '/search'
|
||||
| '/webhooks'
|
||||
| '/dictionaries/$name'
|
||||
@@ -124,6 +134,7 @@ export interface FileRouteTypes {
|
||||
| '/'
|
||||
| '/audit'
|
||||
| '/outbox'
|
||||
| '/reviews'
|
||||
| '/search'
|
||||
| '/dictionaries/$name'
|
||||
| '/webhooks/$id'
|
||||
@@ -135,6 +146,7 @@ export interface FileRouteTypes {
|
||||
| '/audit'
|
||||
| '/dictionaries'
|
||||
| '/outbox'
|
||||
| '/reviews'
|
||||
| '/search'
|
||||
| '/webhooks'
|
||||
| '/dictionaries/$name'
|
||||
@@ -148,6 +160,7 @@ export interface RootRouteChildren {
|
||||
AuditRoute: typeof AuditRoute
|
||||
DictionariesRoute: typeof DictionariesRouteWithChildren
|
||||
OutboxRoute: typeof OutboxRoute
|
||||
ReviewsRoute: typeof ReviewsRoute
|
||||
SearchRoute: typeof SearchRoute
|
||||
WebhooksRoute: typeof WebhooksRouteWithChildren
|
||||
}
|
||||
@@ -168,6 +181,13 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof SearchRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/reviews': {
|
||||
id: '/reviews'
|
||||
path: '/reviews'
|
||||
fullPath: '/reviews'
|
||||
preLoaderRoute: typeof ReviewsRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/outbox': {
|
||||
id: '/outbox'
|
||||
path: '/outbox'
|
||||
@@ -260,6 +280,7 @@ const rootRouteChildren: RootRouteChildren = {
|
||||
AuditRoute: AuditRoute,
|
||||
DictionariesRoute: DictionariesRouteWithChildren,
|
||||
OutboxRoute: OutboxRoute,
|
||||
ReviewsRoute: ReviewsRoute,
|
||||
SearchRoute: SearchRoute,
|
||||
WebhooksRoute: WebhooksRouteWithChildren,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user