diff --git a/ordinis-admin-ui/src/routes/dictionaries.$name.tsx b/ordinis-admin-ui/src/routes/dictionaries.$name.tsx
index b14e055..cb5031d 100644
--- a/ordinis-admin-ui/src/routes/dictionaries.$name.tsx
+++ b/ordinis-admin-ui/src/routes/dictionaries.$name.tsx
@@ -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() {
{t('dict.col.businessKey')}
name / code
+ {extraColumns.map((c) => (
+ {c.label}
+ ))}
{t('dict.col.scope')}
- {t('dict.col.locale')}
{t('dict.col.validFrom')}
{t('dict.col.actions')}
@@ -386,12 +409,34 @@ function DictionaryDetail() {
detailQuery.data?.defaultLocale ?? 'ru-RU',
)}
+ {extraColumns.map((c) => {
+ const v = r.data?.[c.key]
+ if (v == null || v === '') {
+ return (
+
+ —
+
+ )
+ }
+ const text = String(v)
+ return (
+
+
+ {text}
+
+
+ )
+ })}
{r.dataScope}
-
- {r._meta?.locale ?? '—'}
-
{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)