From 0ddba871f7cbc62ac49a72144cfc9c98dfc861d4 Mon Sep 17 00:00:00 2001 From: "Zimin A.N." Date: Mon, 11 May 2026 13:54:26 +0300 Subject: [PATCH] feat(admin-ui): editor 6-tab layout (Stage 3.4) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Dictionary editor route теперь имеет TabBar (handoff design Stage 3.4): Records | Relations | Fields | JSON | Events | History URL-synced через ?tab= (default: records). Backward-compat: старый ?view=hub auto-remap'нется в ?tab=relations при mount'е. Tab contents: - records (default): существующий records table + filters + edit modal flow — без изменений, обёрнут в conditional render - relations: DictionaryHubView (был раньше при view=hub) - fields: schema properties summary — name/type/required/unique/i18n/FK/desc - json: pretty-printed schemaJson read-only - events: link к /audit с pre-filled dict filter - history: placeholder (schema version timeline — backend endpoint pending) EditorTabBar — inline component с handoff styling (border-b border-line + active accent border + font-semibold), -mb-px overlap для clean look. Hub view's neighbor cards теперь search={tab:'relations'} вместо view='hub' (consistent с new param). DictionaryDependentsPanel рендерится только в Records tab — auto Hub view в Relations tab уже включает dependents через свой layout. Tests: 116/116 PASS, build green, TS clean. --- .../components/lineage/DictionaryHubView.tsx | 2 +- .../src/routes/dictionaries.$name.tsx | 239 +++++++++++++++--- 2 files changed, 199 insertions(+), 42 deletions(-) diff --git a/ordinis-admin-ui/src/components/lineage/DictionaryHubView.tsx b/ordinis-admin-ui/src/components/lineage/DictionaryHubView.tsx index 90220d9..4bd20f0 100644 --- a/ordinis-admin-ui/src/components/lineage/DictionaryHubView.tsx +++ b/ordinis-admin-ui/src/components/lineage/DictionaryHubView.tsx @@ -53,7 +53,7 @@ const NeighborCard = ({ = new Set(['PUBLIC', 'INTERNAL', 'RESTRICTED']) @@ -84,7 +90,15 @@ const validateSearch = (raw: Record): DictSearch => { if (typeof raw.at === 'string' && !isNaN(Date.parse(raw.at))) { out.at = raw.at } - if (raw.view === 'hub' || raw.view === 'records') out.view = raw.view + // Tab param + backward-compat для старого ?view=hub. + const TABS: ReadonlySet = new Set([ + 'records', 'relations', 'fields', 'json', 'events', 'history', + ]) + if (typeof raw.tab === 'string' && TABS.has(raw.tab as DictSearch['tab'])) { + out.tab = raw.tab as DictSearch['tab'] + } else if (raw.view === 'hub') { + out.tab = 'relations' + } return out } @@ -566,47 +580,52 @@ function DictionaryDetail() { } /> - {/* View toggle: Записи (default table-based view) | Связи (Hub view с - neighbors на боках). Per design handoff B. */} + {/* Editor tabs per handoff Stage 3.4: + Records | Relations | Fields | JSON | Events | History. + URL-synced через ?tab=. Default — records (records table + filters). */} {detailQuery.data && ( -
- {(['records', 'hub'] as const).map((mode) => { - const active = (urlSearch.view ?? 'records') === mode - return ( - - ) - })} -
+ + void navigate({ + search: (prev) => ({ + ...prev, + tab: tab === 'records' ? undefined : tab, + }), + replace: true, + }) + } + /> )} - {urlSearch.view === 'hub' && detailQuery.data ? ( + {/* Relations tab — incoming FK + outgoing schema FK. */} + {urlSearch.tab === 'relations' && detailQuery.data && ( - ) : ( - /* Phase 1 dict-relationships-v2: schema-level reverse FK card. - Hide-on-empty — большинство справочников без incoming refs не - увидят этот блок. */ + )} + + {/* Fields tab — schema properties grid. */} + {urlSearch.tab === 'fields' && detailQuery.data && ( + + )} + + {/* JSON tab — raw schema viewer (read-only). */} + {urlSearch.tab === 'json' && detailQuery.data && ( + + )} + + {/* Events tab — link to /audit с pre-filled dict filter. */} + {urlSearch.tab === 'events' && ( + + )} + + {/* History tab — записи version timeline (стуб, Stage 3.x). */} + {urlSearch.tab === 'history' && ( + + )} + + {/* DictionaryDependentsPanel — отображается ТОЛЬКО на Records tab. + Phase 1 dict-relationships-v2 reverse FK card. Hide-on-empty. */} + {(!urlSearch.tab || urlSearch.tab === 'records') && ( )} @@ -629,6 +648,10 @@ function DictionaryDetail() { )} + {/* === Records tab content (default, also fallback if no ?tab) === */} + {(!urlSearch.tab || urlSearch.tab === 'records') && ( + <> + {/* Time-travel picker — раскрывается по клику на toolbar button. ISO datetime в URL ?at=… — share-friendly. Active state показывает ambient banner справа от input'а. Phase v1 stretch. */} @@ -870,6 +893,10 @@ function DictionaryDetail() { )} + + )} + {/* === end records tab content === */} + setEdit({ kind: 'closed' })} @@ -1302,3 +1329,133 @@ const serverErrorMessage = (err: unknown): string | null => { } return err instanceof Error ? err.message : 'Unknown error' } + +// ===== Editor Tabs (Stage 3.4) ===== + +type EditorTab = 'records' | 'relations' | 'fields' | 'json' | 'events' | 'history' + +function EditorTabBar({ + active, + onChange, +}: { + active: EditorTab + onChange: (tab: EditorTab) => void +}) { + const { t } = useTranslation() + const tabs: { id: EditorTab; label: string }[] = [ + { id: 'records', label: t('editor.tab.records', { defaultValue: 'Записи' }) }, + { id: 'relations', label: t('editor.tab.relations', { defaultValue: 'Связи' }) }, + { id: 'fields', label: t('editor.tab.fields', { defaultValue: 'Поля' }) }, + { id: 'json', label: t('editor.tab.json', { defaultValue: 'JSON' }) }, + { id: 'events', label: t('editor.tab.events', { defaultValue: 'События' }) }, + { id: 'history', label: t('editor.tab.history', { defaultValue: 'История' }) }, + ] + return ( +
+ {tabs.map((tab) => { + const isActive = active === tab.id + return ( + + ) + })} +
+ ) +} + +/** Fields tab — schema properties summary. */ +function FieldsTabContent({ detail }: { detail: { schemaJson: import('@/api/client').JsonSchema } }) { + const props = detail.schemaJson.properties ?? {} + const required = new Set(detail.schemaJson.required ?? []) + const entries = Object.entries(props) + if (entries.length === 0) { + return ( +
+ Schema без properties. +
+ ) + } + return ( +
+ + + + + + + + + + + + + + {entries.map(([key, prop]) => ( + + + + + + + + + + ))} + +
nametyperequireduniquei18n→ FKdescription
{key}{prop.type ?? '—'}{required.has(key) ? '✓' : ''}{prop['x-unique'] ? '✓' : ''}{prop['x-localized'] ? '✓' : ''} + {prop['x-references'] ?? ''} + + {prop.description ?? ''} +
+
+ ) +} + +/** JSON tab — raw schema JSON, pretty-printed read-only. */ +function JsonTabContent({ detail }: { detail: { schemaJson: import('@/api/client').JsonSchema } }) { + const json = JSON.stringify(detail.schemaJson, null, 2) + return ( +
+      {json}
+    
+ ) +} + +/** Events tab — pointer to audit log scoped to this dict. */ +function EventsTabContent({ dictName }: { dictName: string }) { + return ( +
+

+ События для справочника {dictName} — в общем аудит-логе. +

+ + Открыть /audit с фильтром → + +
+ ) +} + +/** History tab — placeholder (Stage 3.x followup, требует backend changelog endpoint). */ +function HistoryTabContent() { + return ( +
+ Timeline изменений справочника — скоро. Требует backend endpoint для schema versions. +
+ ) +}