fix(schema): FK target field — select из реальной схемы вместо free-form
This commit is contained in:
@@ -13,7 +13,7 @@ import {
|
||||
import { TrashIcon, ArrowUpIcon, ArrowDownIcon } from '@phosphor-icons/react'
|
||||
import type { PropertyDef, PropertyKind } from './types'
|
||||
import { PROPERTY_KINDS } from './types'
|
||||
import { useDictionaries } from '@/api/queries'
|
||||
import { useDictionaries, useDictionaryDetail } from '@/api/queries'
|
||||
|
||||
type Props = {
|
||||
prop: PropertyDef
|
||||
@@ -236,6 +236,13 @@ const ReferenceTargetPicker = ({ value, onChange }: ReferenceTargetPickerProps)
|
||||
const { t } = useTranslation()
|
||||
const dictsQuery = useDictionaries()
|
||||
const [refDict = '', refField = ''] = (value ?? '').split('.', 2)
|
||||
// Fetch schema target dict чтобы показать реальные scalar поля как
|
||||
// options. Без этого юзер вписывал free-form имя (e.g. 'code') и
|
||||
// submit'ил schema с x-references на несуществующее поле → backend
|
||||
// при сохранении record искал `data.code` в target → 422
|
||||
// x_references_target_not_found. Реальный кейс: gs_sat_test03.sat =
|
||||
// spacecraft.code, но в spacecraft нет поля code, есть designator.
|
||||
const targetDetail = useDictionaryDetail(refDict || undefined)
|
||||
|
||||
const dictOptions: SelectOption[] = useMemo(() => {
|
||||
const list = dictsQuery.data ?? []
|
||||
@@ -247,10 +254,30 @@ const ReferenceTargetPicker = ({ value, onChange }: ReferenceTargetPickerProps)
|
||||
.sort((a, b) => a.label.localeCompare(b.label, 'ru'))
|
||||
}, [dictsQuery.data])
|
||||
|
||||
// Eligible target fields: scalar non-localized, non-array/object.
|
||||
// FK имеет смысл только на простые ключевые поля (designator,
|
||||
// businessKey, code etc.). x-localized object и array не подходят
|
||||
// как identifier. businessKey включён как synthetic option — это
|
||||
// всегда unique identifier любого record, backend resolves его.
|
||||
const fieldOptions: SelectOption[] = useMemo(() => {
|
||||
const props = targetDetail.data?.schemaJson?.properties
|
||||
if (!props) return [{ id: 'businessKey', label: 'businessKey' }]
|
||||
const eligible: SelectOption[] = [{ id: 'businessKey', label: 'businessKey' }]
|
||||
for (const [key, prop] of Object.entries(props)) {
|
||||
if (Boolean(prop['x-localized'])) continue
|
||||
const type = prop.type
|
||||
if (type === 'array' || type === 'object') continue
|
||||
eligible.push({ id: key, label: key })
|
||||
}
|
||||
return eligible
|
||||
}, [targetDetail.data?.schemaJson])
|
||||
|
||||
const handleDictChange = (next: string) => {
|
||||
// При смене target dict сбрасываем field на 'code' — это default для
|
||||
// 95% справочников (x-id-source: code). Admin может переписать.
|
||||
onChange(next ? `${next}.${refField || 'code'}` : '')
|
||||
// При смене target dict сбрасываем field на businessKey — universal
|
||||
// identifier что всегда существует на любом dict. Юзер потом выберет
|
||||
// более специфичное поле (designator / code / etc) из загруженного
|
||||
// списка fieldOptions.
|
||||
onChange(next ? `${next}.businessKey` : '')
|
||||
}
|
||||
|
||||
const handleFieldChange = (next: string) => {
|
||||
@@ -258,6 +285,11 @@ const ReferenceTargetPicker = ({ value, onChange }: ReferenceTargetPickerProps)
|
||||
}
|
||||
|
||||
const isValid = Boolean(refDict && refField)
|
||||
// Warn если выбранный field не существует в target schema (legacy
|
||||
// schemas с broken references, до этого fix'а можно было сохранить
|
||||
// такое значение). target dict ещё loading → не warn.
|
||||
const fieldExistsInTarget =
|
||||
!targetDetail.data || fieldOptions.some((opt) => opt.id === refField)
|
||||
|
||||
return (
|
||||
<div className="border border-accent/20 bg-accent/4 rounded-md p-3 space-y-3">
|
||||
@@ -275,18 +307,29 @@ const ReferenceTargetPicker = ({ value, onChange }: ReferenceTargetPickerProps)
|
||||
placeholder={dictsQuery.isLoading ? '…' : '—'}
|
||||
disabled={dictsQuery.isLoading}
|
||||
/>
|
||||
<TextInput
|
||||
<SingleSelect
|
||||
label={t('schema.reference.targetField')}
|
||||
required
|
||||
placeholder="code"
|
||||
options={fieldOptions}
|
||||
value={refField}
|
||||
onChange={(e) => handleFieldChange(e.target.value)}
|
||||
hint={t('schema.reference.targetFieldHint')}
|
||||
onChange={handleFieldChange}
|
||||
placeholder={targetDetail.isLoading ? '…' : '—'}
|
||||
disabled={!refDict || targetDetail.isLoading}
|
||||
/>
|
||||
</div>
|
||||
{!isValid && (
|
||||
<div className="text-cell text-mars">{t('schema.reference.required')}</div>
|
||||
)}
|
||||
{isValid && !fieldExistsInTarget && (
|
||||
<div className="text-cell text-mars">
|
||||
{t('schema.reference.targetFieldMissing', {
|
||||
defaultValue:
|
||||
'Поле «{{field}}» отсутствует в схеме «{{dict}}». Выберите существующее поле — иначе записи ссылающиеся на этот FK не пройдут валидацию.',
|
||||
field: refField,
|
||||
dict: refDict,
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user