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 { TrashIcon, ArrowUpIcon, ArrowDownIcon } from '@phosphor-icons/react'
|
||||||
import type { PropertyDef, PropertyKind } from './types'
|
import type { PropertyDef, PropertyKind } from './types'
|
||||||
import { PROPERTY_KINDS } from './types'
|
import { PROPERTY_KINDS } from './types'
|
||||||
import { useDictionaries } from '@/api/queries'
|
import { useDictionaries, useDictionaryDetail } from '@/api/queries'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
prop: PropertyDef
|
prop: PropertyDef
|
||||||
@@ -236,6 +236,13 @@ const ReferenceTargetPicker = ({ value, onChange }: ReferenceTargetPickerProps)
|
|||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const dictsQuery = useDictionaries()
|
const dictsQuery = useDictionaries()
|
||||||
const [refDict = '', refField = ''] = (value ?? '').split('.', 2)
|
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 dictOptions: SelectOption[] = useMemo(() => {
|
||||||
const list = dictsQuery.data ?? []
|
const list = dictsQuery.data ?? []
|
||||||
@@ -247,10 +254,30 @@ const ReferenceTargetPicker = ({ value, onChange }: ReferenceTargetPickerProps)
|
|||||||
.sort((a, b) => a.label.localeCompare(b.label, 'ru'))
|
.sort((a, b) => a.label.localeCompare(b.label, 'ru'))
|
||||||
}, [dictsQuery.data])
|
}, [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) => {
|
const handleDictChange = (next: string) => {
|
||||||
// При смене target dict сбрасываем field на 'code' — это default для
|
// При смене target dict сбрасываем field на businessKey — universal
|
||||||
// 95% справочников (x-id-source: code). Admin может переписать.
|
// identifier что всегда существует на любом dict. Юзер потом выберет
|
||||||
onChange(next ? `${next}.${refField || 'code'}` : '')
|
// более специфичное поле (designator / code / etc) из загруженного
|
||||||
|
// списка fieldOptions.
|
||||||
|
onChange(next ? `${next}.businessKey` : '')
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleFieldChange = (next: string) => {
|
const handleFieldChange = (next: string) => {
|
||||||
@@ -258,6 +285,11 @@ const ReferenceTargetPicker = ({ value, onChange }: ReferenceTargetPickerProps)
|
|||||||
}
|
}
|
||||||
|
|
||||||
const isValid = Boolean(refDict && refField)
|
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 (
|
return (
|
||||||
<div className="border border-accent/20 bg-accent/4 rounded-md p-3 space-y-3">
|
<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 ? '…' : '—'}
|
placeholder={dictsQuery.isLoading ? '…' : '—'}
|
||||||
disabled={dictsQuery.isLoading}
|
disabled={dictsQuery.isLoading}
|
||||||
/>
|
/>
|
||||||
<TextInput
|
<SingleSelect
|
||||||
label={t('schema.reference.targetField')}
|
label={t('schema.reference.targetField')}
|
||||||
required
|
required
|
||||||
placeholder="code"
|
options={fieldOptions}
|
||||||
value={refField}
|
value={refField}
|
||||||
onChange={(e) => handleFieldChange(e.target.value)}
|
onChange={handleFieldChange}
|
||||||
hint={t('schema.reference.targetFieldHint')}
|
placeholder={targetDetail.isLoading ? '…' : '—'}
|
||||||
|
disabled={!refDict || targetDetail.isLoading}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{!isValid && (
|
{!isValid && (
|
||||||
<div className="text-cell text-mars">{t('schema.reference.required')}</div>
|
<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>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user