feat(records): auto-route create/edit через draft когда approvalRequired

This commit is contained in:
Александр Зимин
2026-05-13 15:29:11 +00:00
parent daa4b284c9
commit 6eb56cffe4
@@ -37,7 +37,7 @@ import {
useRecords,
type RecordsFilter,
} from '@/api/queries'
import { useBulkCloseRecords, useBulkExportRecords, useCreateRecord, useUpdateRecord, useCloseRecord, useFormIdempotencyKey } from '@/api/mutations'
import { useBulkCloseRecords, useBulkExportRecords, useCreateRecord, useUpdateRecord, useCloseRecord, useFormIdempotencyKey, useSubmitDraft } from '@/api/mutations'
import { useCanMutate } from '@/auth/useCanMutate'
import { cn } from '@/lib/utils'
import type { BulkCloseResponse, CreateRecordRequest, DataScope, FlattenedRecord } from '@/api/client'
@@ -185,6 +185,12 @@ function DictionaryDetail() {
}, [pendingDraftsQuery.data])
const createMut = useCreateRecord(name)
const updateMut = useUpdateRecord(name)
// Approval Workflow v2 (D2=A): когда dict отмечен approvalRequired=true,
// backend rejects direct CRUD с 409 draft_required. Используем submit-draft
// путь автоматически — UX same form, но POST в /records/drafts вместо
// /records. Maker подаёт proposal, reviewer approve'нёт → запись применится.
const submitDraftMut = useSubmitDraft(name)
const approvalRequired = detailQuery.data?.approvalRequired === true
// Per-form idempotency key. Stable между ререндерами для одной сессии edit
// modal'а: двойной submit (Enter + click) не создаст 2 record'а — backend
// deduplicate'ит по тому же ключу. Reset на onSuccess.
@@ -527,6 +533,29 @@ function DictionaryDetail() {
setConflict({ payload: submitReq, businessKey: bk })
}
if (edit.kind === 'create') {
if (approvalRequired) {
// Approval Workflow v2: POST /records/drafts вместо /records. Backend
// создаёт draft со status=PENDING, после approve reviewer'ом запись
// материализуется в live table. UX same form, разный endpoint.
submitDraftMut.mutate(
{
businessKey: submitReq.businessKey ?? '',
operation: 'CREATE',
data: submitReq.data,
geometryWkt: submitReq.geometryWkt ?? null,
validFrom: submitReq.validFrom ?? null,
validTo: submitReq.validTo ?? null,
},
{
onSuccess: () => {
setEdit({ kind: 'closed' })
resetRecordIdempotencyKey()
},
onError: (err) => handleSaveError(err, submitReq.businessKey ?? ''),
},
)
return
}
createMut.mutate(
{ payload: submitReq, idempotencyKey: recordIdempotencyKey },
{
@@ -544,6 +573,27 @@ function DictionaryDetail() {
return
}
if (edit.kind === 'edit') {
if (approvalRequired) {
// Approval Workflow v2: update тоже через draft (UPDATE proposal).
submitDraftMut.mutate(
{
businessKey: edit.record.businessKey,
operation: 'UPDATE',
data: submitReq.data,
geometryWkt: submitReq.geometryWkt ?? null,
validFrom: submitReq.validFrom ?? null,
validTo: submitReq.validTo ?? null,
},
{
onSuccess: () => {
setEdit({ kind: 'closed' })
resetRecordIdempotencyKey()
},
onError: (err) => handleSaveError(err, edit.record.businessKey),
},
)
return
}
updateMut.mutate(
{
businessKey: edit.record.businessKey,
@@ -596,8 +646,14 @@ function DictionaryDetail() {
)
}
const activeMutation =
edit.kind === 'create' ? createMut : edit.kind === 'edit' ? updateMut : null
// Approval Workflow v2: когда dict approval-required — мы делаем submit
// через submitDraftMut, поэтому activeMutation должен указывать на него,
// иначе spinner / error из createMut/updateMut выйдут несинхронно.
const activeMutation = (() => {
if (edit.kind === 'create') return approvalRequired ? submitDraftMut : createMut
if (edit.kind === 'edit') return approvalRequired ? submitDraftMut : updateMut
return null
})()
const serverError = serverErrorMessage(activeMutation?.error)
const totalRecords = recordsResult.data?.length ?? 0
@@ -1091,7 +1147,17 @@ function DictionaryDetail() {
}}
caption={
edit.kind === 'edit'
? t('drawer.captionEdit', { dict: name })
? approvalRequired
? t('drawer.captionEditDraft', {
dict: name,
defaultValue: `${name} — proposal на изменение (через ревью)`,
})
: t('drawer.captionEdit', { dict: name })
: approvalRequired
? t('drawer.captionCreateDraft', {
dict: name,
defaultValue: `${name} — proposal на создание (через ревью)`,
})
: t('drawer.captionCreate', { dict: name })
}
recordId={edit.kind === 'edit' ? edit.record.businessKey : undefined}