feat(admin-ui): Hub view (B) per design handoff — dict relations 3-col layout
This commit is contained in:
@@ -35,6 +35,7 @@ import type { BulkCloseResponse, CreateRecordRequest, DataScope, FlattenedRecord
|
||||
import { SchemaDrivenForm } from '@/components/form/SchemaDrivenForm'
|
||||
import { DictionaryEditorDialog } from '@/components/schema/DictionaryEditorDialog'
|
||||
import { DictionaryDependentsPanel } from '@/components/lineage/DictionaryDependentsPanel'
|
||||
import { DictionaryHubView } from '@/components/lineage/DictionaryHubView'
|
||||
import { CascadeConfirmDialog } from '@/components/lineage/CascadeConfirmDialog'
|
||||
import { RecordHistoryDrawer } from '@/components/record/RecordHistoryDrawer'
|
||||
import { AoiPickerDialog, type AoiResult } from '@/components/geo/AoiPickerDialog'
|
||||
@@ -57,6 +58,9 @@ type DictSearch = {
|
||||
/** Time-travel: ISO datetime "просмотр на момент X". Если задан — backend
|
||||
* findActiveAt(at) возвращает active version на этот момент вместо now(). */
|
||||
at?: string
|
||||
/** View mode: 'records' (default — table + filters) | 'hub' (relations graph
|
||||
* с current dict в центре + neighbors на боках). См. DictionaryHubView. */
|
||||
view?: 'records' | 'hub'
|
||||
}
|
||||
|
||||
const SCOPE_VALUES: ReadonlySet<DataScope> = new Set(['PUBLIC', 'INTERNAL', 'RESTRICTED'])
|
||||
@@ -78,6 +82,7 @@ const validateSearch = (raw: Record<string, unknown>): 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
|
||||
return out
|
||||
}
|
||||
|
||||
@@ -514,10 +519,49 @@ function DictionaryDetail() {
|
||||
}
|
||||
/>
|
||||
|
||||
{/* Phase 1 dict-relationships-v2: schema-level reverse FK card.
|
||||
Hide-on-empty — большинство справочников без incoming refs не
|
||||
увидят этот блок. */}
|
||||
<DictionaryDependentsPanel dictionaryName={name} />
|
||||
{/* View toggle: Записи (default table-based view) | Связи (Hub view с
|
||||
neighbors на боках). Per design handoff B. */}
|
||||
{detailQuery.data && (
|
||||
<div role="group" aria-label="View mode" className="flex border border-regolith rounded-sm overflow-hidden w-fit">
|
||||
{(['records', 'hub'] as const).map((mode) => {
|
||||
const active = (urlSearch.view ?? 'records') === mode
|
||||
return (
|
||||
<button
|
||||
key={mode}
|
||||
type="button"
|
||||
onClick={() =>
|
||||
void navigate({
|
||||
search: (prev) => ({
|
||||
...prev,
|
||||
view: mode === 'records' ? undefined : mode,
|
||||
}),
|
||||
replace: true,
|
||||
})
|
||||
}
|
||||
aria-pressed={active}
|
||||
className={`px-3 py-1 text-2xs uppercase tracking-label transition focus:outline-none focus:ring-2 focus:ring-ultramarain/40 ${
|
||||
active
|
||||
? 'bg-orbit/40 text-ultramarain'
|
||||
: 'hover:bg-orbit/20 text-carbon'
|
||||
}`}
|
||||
>
|
||||
{t(`dict.view.${mode}`, {
|
||||
defaultValue: mode === 'records' ? 'Записи' : 'Связи',
|
||||
})}
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{urlSearch.view === 'hub' && detailQuery.data ? (
|
||||
<DictionaryHubView detail={detailQuery.data} />
|
||||
) : (
|
||||
/* Phase 1 dict-relationships-v2: schema-level reverse FK card.
|
||||
Hide-on-empty — большинство справочников без incoming refs не
|
||||
увидят этот блок. */
|
||||
<DictionaryDependentsPanel dictionaryName={name} />
|
||||
)}
|
||||
|
||||
{aoi && (
|
||||
<div className="flex items-center gap-3 px-3 py-2 rounded-sm border border-ultramarain/30 bg-ultramarain/4 text-sm">
|
||||
|
||||
Reference in New Issue
Block a user