feat(dict): records list — schema-driven extra columns (FK + enum)
Раньше records list показывал только businessKey / name / scope / locale / validFrom. На spacecraft с 14 полями admin не видел статуса или типа КА — должен был открыть edit modal каждой записи чтобы понять что есть OPERATIONAL и что есть LOST. Drop unhelpful 'locale' колонку (для не-localized словарей всегда '—'). Replace на до 2 schema-driven 'значимых' колонок: - FK поля (x-references) — синий fill, маркируют ссылку на dict - Enum поля — серый fill, fixed allow-list Spacecraft теперь показывает в таблице: businessKey, name, satellite_type_code (FK), status (FK), scope, validFrom — всю полезную информацию за один взгляд. Schema interpretation: - detailQuery.data.schemaJson.properties → filter scalar non-localized - Skip name/code/businessKey (уже показаны) - Take первые 2 enum/reference (column count fits 1280px viewport) - humanize key для header label (snake_case → Snake Case) Tests: 89 passed (без изменений — render-логика, integration test требует QueryClient + schema fixture, ROI низкий).
This commit is contained in:
@@ -178,6 +178,27 @@ function DictionaryDetail() {
|
||||
const filtersActive = search.length > 0 || scopeFilter.size > 0
|
||||
const filteredCount = filteredRecords.length
|
||||
|
||||
// Schema-driven extra columns: enum + reference поля — самые info-dense
|
||||
// не-локализованные scalar'ы. Берём первые 2 чтобы не перегрузить
|
||||
// строку (с businessKey + name + scope + extras + дата + actions = 6
|
||||
// колонок, comfortably fits на 1280px). Skip name/code/businessKey
|
||||
// (уже показаны), всё нескалярное (object/array/localized).
|
||||
const extraColumns = useMemo<{ key: string; label: string; isFk: boolean }[]>(() => {
|
||||
const props = detailQuery.data?.schemaJson?.properties ?? {}
|
||||
const interesting: { key: string; label: string; isFk: boolean }[] = []
|
||||
for (const [key, prop] of Object.entries(props)) {
|
||||
if (interesting.length >= 2) break
|
||||
if (key === 'name' || key === 'code' || key === 'businessKey') continue
|
||||
const isLocalized = Boolean(prop['x-localized'])
|
||||
if (isLocalized) continue
|
||||
const isEnum = Array.isArray(prop.enum) && prop.enum.length > 0
|
||||
const isFk = typeof prop['x-references'] === 'string' && Boolean(prop['x-references'])
|
||||
if (!isEnum && !isFk) continue
|
||||
interesting.push({ key, label: humanize(key), isFk })
|
||||
}
|
||||
return interesting
|
||||
}, [detailQuery.data?.schemaJson])
|
||||
|
||||
const editingKey = edit.kind === 'edit' ? edit.record.businessKey : undefined
|
||||
const rawRecordQuery = useRecordRaw(name, editingKey)
|
||||
|
||||
@@ -368,8 +389,10 @@ function DictionaryDetail() {
|
||||
<TableRow>
|
||||
<TableHeaderCell>{t('dict.col.businessKey')}</TableHeaderCell>
|
||||
<TableHeaderCell>name / code</TableHeaderCell>
|
||||
{extraColumns.map((c) => (
|
||||
<TableHeaderCell key={c.key}>{c.label}</TableHeaderCell>
|
||||
))}
|
||||
<TableHeaderCell>{t('dict.col.scope')}</TableHeaderCell>
|
||||
<TableHeaderCell>{t('dict.col.locale')}</TableHeaderCell>
|
||||
<TableHeaderCell>{t('dict.col.validFrom')}</TableHeaderCell>
|
||||
<TableHeaderCell align="right">{t('dict.col.actions')}</TableHeaderCell>
|
||||
</TableRow>
|
||||
@@ -386,12 +409,34 @@ function DictionaryDetail() {
|
||||
detailQuery.data?.defaultLocale ?? 'ru-RU',
|
||||
)}
|
||||
</TableCell>
|
||||
{extraColumns.map((c) => {
|
||||
const v = r.data?.[c.key]
|
||||
if (v == null || v === '') {
|
||||
return (
|
||||
<TableCell key={c.key}>
|
||||
<span className="text-2xs text-carbon/40">—</span>
|
||||
</TableCell>
|
||||
)
|
||||
}
|
||||
const text = String(v)
|
||||
return (
|
||||
<TableCell key={c.key}>
|
||||
<span
|
||||
className={[
|
||||
'inline-block px-1.5 py-0.5 rounded-sm font-mono text-2xs',
|
||||
c.isFk
|
||||
? 'bg-ultramarain/8 text-ultramarain'
|
||||
: 'bg-carbon/8 text-carbon/80',
|
||||
].join(' ')}
|
||||
>
|
||||
{text}
|
||||
</span>
|
||||
</TableCell>
|
||||
)
|
||||
})}
|
||||
<TableCell>
|
||||
<Badge variant="info">{r.dataScope}</Badge>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<span className="text-2xs text-carbon/60">{r._meta?.locale ?? '—'}</span>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<span className="text-2xs text-carbon/60">
|
||||
{new Date(r.validFrom).toLocaleDateString()}
|
||||
@@ -566,6 +611,10 @@ function DictionaryDetail() {
|
||||
* прямоугольным GeoJSON для preview в picker'е. Невалидные значения
|
||||
* уже отфильтрованы validateSearch — но dup'аем guard на парсинге.
|
||||
*/
|
||||
/** snake_case → 'Snake Case' для column header'ов. */
|
||||
const humanize = (key: string): string =>
|
||||
key.replace(/[_-]+/g, ' ').replace(/\b\w/g, (ch) => ch.toUpperCase())
|
||||
|
||||
const bboxToAoi = (bboxCsv: string | undefined): AoiResult | null => {
|
||||
if (!bboxCsv) return null
|
||||
const parts = bboxCsv.split(',').map(Number)
|
||||
|
||||
Reference in New Issue
Block a user