Merge branch 'feat/auto-route-create-to-draft-when-approval-required' into 'main'
feat(records): auto-route create/edit через draft когда approvalRequired See merge request 2-6/2-6-4/terravault/ordinis!171
This commit is contained in:
@@ -37,7 +37,7 @@ import {
|
|||||||
useRecords,
|
useRecords,
|
||||||
type RecordsFilter,
|
type RecordsFilter,
|
||||||
} from '@/api/queries'
|
} 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 { useCanMutate } from '@/auth/useCanMutate'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
import type { BulkCloseResponse, CreateRecordRequest, DataScope, FlattenedRecord } from '@/api/client'
|
import type { BulkCloseResponse, CreateRecordRequest, DataScope, FlattenedRecord } from '@/api/client'
|
||||||
@@ -185,6 +185,12 @@ function DictionaryDetail() {
|
|||||||
}, [pendingDraftsQuery.data])
|
}, [pendingDraftsQuery.data])
|
||||||
const createMut = useCreateRecord(name)
|
const createMut = useCreateRecord(name)
|
||||||
const updateMut = useUpdateRecord(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
|
// Per-form idempotency key. Stable между ререндерами для одной сессии edit
|
||||||
// modal'а: двойной submit (Enter + click) не создаст 2 record'а — backend
|
// modal'а: двойной submit (Enter + click) не создаст 2 record'а — backend
|
||||||
// deduplicate'ит по тому же ключу. Reset на onSuccess.
|
// deduplicate'ит по тому же ключу. Reset на onSuccess.
|
||||||
@@ -527,6 +533,29 @@ function DictionaryDetail() {
|
|||||||
setConflict({ payload: submitReq, businessKey: bk })
|
setConflict({ payload: submitReq, businessKey: bk })
|
||||||
}
|
}
|
||||||
if (edit.kind === 'create') {
|
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(
|
createMut.mutate(
|
||||||
{ payload: submitReq, idempotencyKey: recordIdempotencyKey },
|
{ payload: submitReq, idempotencyKey: recordIdempotencyKey },
|
||||||
{
|
{
|
||||||
@@ -544,6 +573,27 @@ function DictionaryDetail() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (edit.kind === 'edit') {
|
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(
|
updateMut.mutate(
|
||||||
{
|
{
|
||||||
businessKey: edit.record.businessKey,
|
businessKey: edit.record.businessKey,
|
||||||
@@ -596,8 +646,14 @@ function DictionaryDetail() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const activeMutation =
|
// Approval Workflow v2: когда dict approval-required — мы делаем submit
|
||||||
edit.kind === 'create' ? createMut : edit.kind === 'edit' ? updateMut : null
|
// через 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 serverError = serverErrorMessage(activeMutation?.error)
|
||||||
|
|
||||||
const totalRecords = recordsResult.data?.length ?? 0
|
const totalRecords = recordsResult.data?.length ?? 0
|
||||||
@@ -1091,7 +1147,17 @@ function DictionaryDetail() {
|
|||||||
}}
|
}}
|
||||||
caption={
|
caption={
|
||||||
edit.kind === 'edit'
|
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 })
|
: t('drawer.captionCreate', { dict: name })
|
||||||
}
|
}
|
||||||
recordId={edit.kind === 'edit' ? edit.record.businessKey : undefined}
|
recordId={edit.kind === 'edit' ? edit.record.businessKey : undefined}
|
||||||
|
|||||||
Reference in New Issue
Block a user