diff --git a/ordinis-admin-ui/src/components/editor/EditorInfoPanel.tsx b/ordinis-admin-ui/src/components/editor/EditorInfoPanel.tsx new file mode 100644 index 0000000..092b192 --- /dev/null +++ b/ordinis-admin-ui/src/components/editor/EditorInfoPanel.tsx @@ -0,0 +1,164 @@ +import { useMemo } from 'react' +import { Link } from '@tanstack/react-router' +import { useTranslation } from 'react-i18next' +import { Badge } from '@/ui' +import { useDictionaryDependents } from '@/api/queries' +import type { DictionaryDetail } from '@/api/client' + +/** + * EditorInfoPanel — left rail с dict metadata per handoff prototype Screen 2. + * + *

Содержимое (top → bottom): + *

+ * + *

Action buttons (Экспорт / Time-travel) идут в PageHeader actions slot, + * не в info panel — handoff prototype показывает им в info panel но семантика + * редизайна: actions около title для discoverability. + */ +export type EditorInfoPanelProps = { + detail: DictionaryDetail + recordCount: number +} + +export function EditorInfoPanel({ detail, recordCount }: EditorInfoPanelProps) { + const { t } = useTranslation() + const { data: refByRaw } = useDictionaryDependents(detail.name) + + const outgoingNames = useMemo(() => extractOutgoingFk(detail.schemaJson), [detail.schemaJson]) + const incomingNames = useMemo(() => { + const seen = new Set() + for (const dep of refByRaw ?? []) seen.add(dep.sourceDict) + return Array.from(seen).sort() + }, [refByRaw]) + + return ( +

+ ) +} + +function Section({ + label, + children, +}: { + label: React.ReactNode + children: React.ReactNode +}) { + return ( +
+
{label}
+ {children} +
+ ) +} + +function MetaCell({ + label, + children, +}: { + label: React.ReactNode + children: React.ReactNode +}) { + return ( +
+
{label}
+
{children}
+
+ ) +} + +const extractOutgoingFk = ( + schema: import('@/api/client').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() +} diff --git a/ordinis-admin-ui/src/routes/dictionaries.$name.tsx b/ordinis-admin-ui/src/routes/dictionaries.$name.tsx index aaa3d7d..7cdfda3 100644 --- a/ordinis-admin-ui/src/routes/dictionaries.$name.tsx +++ b/ordinis-admin-ui/src/routes/dictionaries.$name.tsx @@ -42,6 +42,7 @@ import { CascadeConfirmDialog } from '@/components/lineage/CascadeConfirmDialog' import { RecordHistoryDrawer } from '@/components/record/RecordHistoryDrawer' import { RecordDrawer } from '@/components/record/RecordDrawer' import { WorkflowBanner } from '@/components/editor/WorkflowBanner' +import { EditorInfoPanel } from '@/components/editor/EditorInfoPanel' import { AoiPickerDialog, type AoiResult } from '@/components/geo/AoiPickerDialog' import { TimeTravelPicker } from '@/components/timetravel/TimeTravelPicker' import { nowIsoLocal } from '@/lib/dates' @@ -621,15 +622,23 @@ function DictionaryDetail() { } /> - {/* WorkflowBanner — status (live/review/draft) above tab bar per handoff Screen 2. */} - {detailQuery.data && ( - - )} + {/* 3-col layout per handoff prototype design/compact.html: + [Sidebar (220px global)] [InfoPanel 220px] [main content] + Info panel — sticky слева с dict metadata + relations. + На + {detailQuery.data && ( + + )} - {/* Editor tabs per handoff Stage 3.4: - Records | Relations | Fields | JSON | Events | History. - URL-synced через ?tab=. Default — records (records table + filters). */} - {detailQuery.data && ( +
+ {/* WorkflowBanner — status (live/review/draft) above tab bar per handoff Screen 2. */} + {detailQuery.data && ( + + )} + + {/* Editor tabs per handoff Stage 3.4 */} + {detailQuery.data && ( @@ -945,6 +954,9 @@ function DictionaryDetail() { )} {/* === end records tab content === */} +
+ + {/* === end 3-col grid === */}