/** * Hub view (View B из design handoff) — focused dictionary в центре + соседи * по обе стороны. * * Layout: * ┌──────────────┬─────────────────┬──────────────┐ * │ → ссылается │ │ ← N использ. │ * │ (outgoing) │ (focused) │ (refBy) │ * └──────────────┴─────────────────┴──────────────┘ * * Outgoing FK derived from schemaJson properties[*].x-references * (format: "dict.field"). Incoming refBy via useDictionaryDependents. * * Click на neighbor card → navigate к его Hub view (recursion fine). */ import { useMemo } from 'react' import { Link } from '@tanstack/react-router' import { useTranslation } from 'react-i18next' import { Badge } from '@/ui' import { useDictionaryDependents, useDictionaries } from '@/api/queries' import type { DictionaryDetail, DictionaryDefinition, JsonSchema } from '@/api/client' const SCOPE_STRIP: Record = { PUBLIC: 'bg-emerald-500', INTERNAL: 'bg-amber-500', RESTRICTED: 'bg-rose-500', } /** * Walk schemaJson properties — собираем targets из x-references. * Format: "targetDict.targetField" → возвращаем уникальные targetDict names. */ const extractOutgoingFk = (schema: JsonSchema | undefined): string[] => { if (!schema?.properties) return [] const targets = new Set() for (const prop of Object.values(schema.properties)) { const ref = prop['x-references'] if (typeof ref === 'string' && ref.length > 0) { const [target] = ref.split('.') if (target) targets.add(target) } } return Array.from(targets).sort() } const NeighborCard = ({ dict, side, }: { dict: DictionaryDefinition side: 'left' | 'right' }) => (
{dict.name}
) const SectionLabel = ({ side, count, }: { side: 'left' | 'right' count: number }) => { const { t } = useTranslation() return (
{side === 'left' ? t('hub.outgoing', { count, defaultValue: '→ ссылается ({{count}})' }) : t('hub.incoming', { count, defaultValue: '← {{count}} использ.' })}
) } export function DictionaryHubView({ detail }: { detail: DictionaryDetail }) { const { t } = useTranslation() const { data: allDicts } = useDictionaries() const { data: refByRaw } = useDictionaryDependents(detail.name) // Outgoing FK targets — names derived из schemaJson. const outgoingNames = useMemo( () => extractOutgoingFk(detail.schemaJson), [detail.schemaJson], ) // Resolve names → DictionaryDefinition (для card rendering). // Filter недоступные / unknown names (защита от устаревших schema refs). const outgoingDicts = useMemo(() => { if (!allDicts) return [] const byName = new Map(allDicts.map((d) => [d.name, d])) return outgoingNames .map((name) => byName.get(name)) .filter((d): d is DictionaryDefinition => Boolean(d)) }, [allDicts, outgoingNames]) // refBy: уникальные source dicts (те что ссылаются на нас) → resolve в definitions. const incomingDicts = useMemo(() => { if (!allDicts || !refByRaw) return [] const byName = new Map(allDicts.map((d) => [d.name, d])) const seen = new Set() const out: DictionaryDefinition[] = [] for (const dep of refByRaw) { if (seen.has(dep.sourceDict)) continue seen.add(dep.sourceDict) const def = byName.get(dep.sourceDict) if (def) out.push(def) } return out }, [allDicts, refByRaw]) return (
{/* Left rail: outgoing FK */}
{outgoingDicts.length === 0 ? (
{t('hub.noOutgoing', { defaultValue: 'нет исходящих ссылок' })}
) : (
{outgoingDicts.map((d) => ( ))}
)}
{/* Center: focused dict (large card) */}
{detail.scope} {detail.approvalRequired && ( {t('dict.list.approval', { defaultValue: 'approval' })} )} v{detail.schemaVersion}

{detail.displayName ?? detail.name}

{detail.name}
{detail.description && (

{detail.description}

)}
{detail.bundle} {detail.supportedLocales.join(', ')} {typeof detail.recordCount === 'number' && ( {t('dict.list.recordCount', { count: detail.recordCount })} )}
{/* Right rail: refBy */}
{incomingDicts.length === 0 ? (
{t('hub.noIncoming', { defaultValue: 'никто не ссылается' })}
) : (
{incomingDicts.map((d) => ( ))}
)}
) }