fix(admin-ui): top-5 critical review bugs (auth + AJV + TZ + idempotency + storage)
This commit is contained in:
@@ -30,7 +30,7 @@ import {
|
||||
useRecords,
|
||||
type RecordsFilter,
|
||||
} from '@/api/queries'
|
||||
import { useBulkCloseRecords, useBulkExportRecords, useCreateRecord, useUpdateRecord, useCloseRecord } from '@/api/mutations'
|
||||
import { useBulkCloseRecords, useBulkExportRecords, useCreateRecord, useUpdateRecord, useCloseRecord, useFormIdempotencyKey } from '@/api/mutations'
|
||||
import type { BulkCloseResponse, CreateRecordRequest, DataScope, FlattenedRecord } from '@/api/client'
|
||||
import { SchemaDrivenForm } from '@/components/form/SchemaDrivenForm'
|
||||
import { DictionaryEditorDialog } from '@/components/schema/DictionaryEditorDialog'
|
||||
@@ -38,7 +38,7 @@ import { DictionaryDependentsPanel } from '@/components/lineage/DictionaryDepend
|
||||
import { CascadeConfirmDialog } from '@/components/lineage/CascadeConfirmDialog'
|
||||
import { RecordHistoryDrawer } from '@/components/record/RecordHistoryDrawer'
|
||||
import { AoiPickerDialog, type AoiResult } from '@/components/geo/AoiPickerDialog'
|
||||
import { nowIsoLocal } from '@/lib/dates'
|
||||
import { nowIsoLocal, toDateTimeLocalInput, fromDateTimeLocalInput } from '@/lib/dates'
|
||||
import { recordDisplayName } from '@/lib/locales'
|
||||
import { SCOPE_BORDER_TOP, SCOPE_DOT, SCOPE_ORDER } from '@/lib/scope-style'
|
||||
|
||||
@@ -138,6 +138,10 @@ function DictionaryDetail() {
|
||||
}, [pendingDraftsQuery.data])
|
||||
const createMut = useCreateRecord(name)
|
||||
const updateMut = useUpdateRecord(name)
|
||||
// Per-form idempotency key. Stable между ререндерами для одной сессии edit
|
||||
// modal'а: двойной submit (Enter + click) не создаст 2 record'а — backend
|
||||
// deduplicate'ит по тому же ключу. Reset на onSuccess.
|
||||
const [recordIdempotencyKey, resetRecordIdempotencyKey] = useFormIdempotencyKey()
|
||||
const closeMut = useCloseRecord(name)
|
||||
const bulkCloseMut = useBulkCloseRecords(name)
|
||||
const bulkExportMut = useBulkExportRecords(name)
|
||||
@@ -364,13 +368,30 @@ function DictionaryDetail() {
|
||||
|
||||
const handleSubmit = (req: CreateRecordRequest) => {
|
||||
if (edit.kind === 'create') {
|
||||
createMut.mutate(req, { onSuccess: () => setEdit({ kind: 'closed' }) })
|
||||
createMut.mutate(
|
||||
{ payload: req, idempotencyKey: recordIdempotencyKey },
|
||||
{
|
||||
onSuccess: () => {
|
||||
setEdit({ kind: 'closed' })
|
||||
resetRecordIdempotencyKey()
|
||||
},
|
||||
},
|
||||
)
|
||||
return
|
||||
}
|
||||
if (edit.kind === 'edit') {
|
||||
updateMut.mutate(
|
||||
{ businessKey: edit.record.businessKey, payload: req },
|
||||
{ onSuccess: () => setEdit({ kind: 'closed' }) },
|
||||
{
|
||||
businessKey: edit.record.businessKey,
|
||||
payload: req,
|
||||
idempotencyKey: recordIdempotencyKey,
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
setEdit({ kind: 'closed' })
|
||||
resetRecordIdempotencyKey()
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -519,18 +540,18 @@ function DictionaryDetail() {
|
||||
<input
|
||||
type="datetime-local"
|
||||
className="px-2 py-1 rounded-sm border border-regolith bg-white text-2xs font-mono"
|
||||
value={
|
||||
timeTravelAt
|
||||
? // datetime-local не принимает Z/offset — конвертируем в local.
|
||||
new Date(timeTravelAt).toISOString().slice(0, 16)
|
||||
: ''
|
||||
}
|
||||
// datetime-local требует local time без offset. toISOString() отдаёт
|
||||
// UTC → у пользователя в +03:00 picker показывал значение на 3ч
|
||||
// раньше выбранного, и на каждом редактировании round-trip drift'ил
|
||||
// время. toDateTimeLocalInput / fromDateTimeLocalInput сохраняют
|
||||
// wall-clock время через формат с явным local-TZ offset.
|
||||
value={toDateTimeLocalInput(timeTravelAt)}
|
||||
onChange={(e) => {
|
||||
if (!e.target.value) {
|
||||
setTimeTravelAt(undefined)
|
||||
return
|
||||
}
|
||||
setTimeTravelAt(new Date(e.target.value).toISOString())
|
||||
setTimeTravelAt(fromDateTimeLocalInput(e.target.value))
|
||||
}}
|
||||
aria-label={t('timeTravel.label')}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user