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:
@@ -37,6 +37,9 @@ export type DictionaryDefinition = {
|
||||
defaultLocale: string
|
||||
/** CEO plan E2: per-dict Redis projection materialization opt-in. */
|
||||
redisProjectionEnabled: boolean
|
||||
/** Approval Workflow v2: per-dict opt-in. */
|
||||
approvalRequired: boolean
|
||||
approvalMinRole?: string | null
|
||||
recordCount?: number
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
@@ -107,6 +110,10 @@ export type CreateDictionaryRequest = {
|
||||
defaultLocale?: string
|
||||
/** CEO plan E2: per-dict Redis projection opt-in. Default false. */
|
||||
redisProjectionEnabled?: boolean
|
||||
/** Approval Workflow v2: per-dict opt-in. Default false. */
|
||||
approvalRequired?: boolean
|
||||
/** Optional Keycloak role для reviewer'ов на этот dict. */
|
||||
approvalMinRole?: string
|
||||
}
|
||||
|
||||
export type CreateRecordRequest = {
|
||||
@@ -246,6 +253,48 @@ export type WebhookDeliveryPage = {
|
||||
size: number
|
||||
}
|
||||
|
||||
// === Approval Workflow v2 ===
|
||||
|
||||
export type DraftStatus = 'PENDING' | 'APPROVED' | 'REJECTED' | 'WITHDRAWN'
|
||||
export type DraftOperation = 'CREATE' | 'UPDATE' | 'CLOSE'
|
||||
|
||||
export type DraftResponse = {
|
||||
id: string
|
||||
dictionaryId: string
|
||||
businessKey: string
|
||||
parentRecordId?: string | null
|
||||
operation: DraftOperation
|
||||
data?: Record<string, unknown> | null
|
||||
geometryWkt?: string | null
|
||||
proposedValidFrom?: string | null
|
||||
proposedValidTo?: string | null
|
||||
status: DraftStatus
|
||||
makerId: string
|
||||
makerComment?: string | null
|
||||
submittedAt: string
|
||||
reviewerId?: string | null
|
||||
reviewedAt?: string | null
|
||||
reviewComment?: string | null
|
||||
}
|
||||
|
||||
export type SubmitDraftRequest = {
|
||||
businessKey: string
|
||||
operation: DraftOperation
|
||||
data?: Record<string, unknown> | null
|
||||
geometryWkt?: string | null
|
||||
validFrom?: string | null
|
||||
validTo?: string | null
|
||||
comment?: string | null
|
||||
}
|
||||
|
||||
export type ReviewQueuePage = {
|
||||
items: DraftResponse[]
|
||||
page: number
|
||||
size: number
|
||||
totalElements: number
|
||||
totalPages: number
|
||||
}
|
||||
|
||||
// === Smart search (CEO plan v1) ===
|
||||
|
||||
export type SearchItem = {
|
||||
|
||||
Reference in New Issue
Block a user