diff --git a/ordinis-admin-ui/src/routes/dictionaries.index.tsx b/ordinis-admin-ui/src/routes/dictionaries.index.tsx index 1c0d9ad..25916dd 100644 --- a/ordinis-admin-ui/src/routes/dictionaries.index.tsx +++ b/ordinis-admin-ui/src/routes/dictionaries.index.tsx @@ -14,7 +14,7 @@ * (followup), сейчас рендерится только если backend начнёт возвращать в DictionaryDefinition. */ import { useDeferredValue, useMemo, useState } from 'react' -import { createFileRoute, Link, useNavigate } from '@tanstack/react-router' +import { createFileRoute, useNavigate } from '@tanstack/react-router' import { useTranslation } from 'react-i18next' import { Alert, Badge, Button, EmptyState, LoadingBlock, PageHeader, SearchInput } from '@/ui' import { PlusIcon } from '@phosphor-icons/react' @@ -83,14 +83,6 @@ export const groupByBundle = ( return out } -// Scope strip color — vertical 3px на левом крае карточки. -// Использует design handoff tokens — auto-switch в dark mode. -const SCOPE_STRIP: Record = { - PUBLIC: 'bg-accent', - INTERNAL: 'bg-warn', - RESTRICTED: 'bg-pink', -} - // ===== Route ===== export const Route = createFileRoute('/dictionaries/')({ @@ -181,7 +173,6 @@ function DictionariesPage() { return out }, [data]) - const grouped = useMemo(() => groupByBundle(filtered), [filtered]) const filtersActive = Boolean(q) || scopeFilter.size > 0 || bundleFilter || withDepsOnly const resetFilters = () => navigate({ search: {} }) @@ -365,24 +356,10 @@ function DictionariesPage() { )} ) : ( - // Bundle-grouped 2-col card grid - Array.from(grouped.entries()).map(([bundle, items]) => ( -
-

- - {bundle} - - - · {items.length} {t('dict.list.records.short')} - -

-
- {items.map((d) => ( - - ))} -
-
- )) + // Row-based table per handoff prototype design/compact.html. + // Single flat table (bundle is a column, не group header). + // Hover/keyboard клик на строку → navigate к editor. + )} ['t'] -const MAX_REFBY_CHIPS = 3 - /** * Дедуплицирует SchemaDependent[] по `sourceDict` (одно название может * указывать через несколько fields — на катаоге показываем как один chip). @@ -421,99 +396,142 @@ export const uniqueRefBy = (deps: SchemaDependent[]): SchemaDependent[] => { return out } -const FkChip = ({ - to, - label, - dim, -}: { - to: string - label: string - dim?: boolean -}) => ( - e.stopPropagation()} - title={to} - className={`inline-flex items-center px-[7px] py-[2px] rounded-[4px] font-mono text-[11px] transition focus:outline-none focus:ring-2 focus:ring-accent/40 ${ - dim - ? 'border border-dashed border-line text-mute hover:border-accent/60 hover:text-accent' - : 'bg-accent-bg text-accent hover:bg-accent hover:text-on-accent' - }`} - > - {label} - -) +// ===== Row table per handoff prototype design/compact.html ===== -const DictCard = ({ d, t }: { d: DictionaryDefinition; t: TFunc }) => { - // refBy — backend возвращает SchemaDependent[] (cached 5min, parallel - // queries dedup'ятся TanStack Query'ем). На каталоге фетчим per dict, что на - // 40 dicts = 40 параллельных GET — bursty первый раз, кешировано далее. - // outgoing fk → ссылается потребует schemaJson per dict (отдельный followup, - // backend batch endpoint желателен). +/** + * Catalog list — compact row table per handoff Screen 1 (line 173+). + * Columns: name+subtitle / id / bundle / scope / records / → / ← / updated. + * Click row → navigate к editor. + */ +function DictionaryListTable({ rows }: { rows: DictionaryDefinition[] }) { + const { t } = useTranslation() + return ( +
+ + + + + + + + + + + + + + + {rows.map((d) => ( + + ))} + +
+ {t('dict.col.title', { defaultValue: 'Название' })} + + {t('dict.col.id', { defaultValue: 'id' })} + + {t('dict.col.bundle', { defaultValue: 'bundle' })} + + {t('dict.col.scope', { defaultValue: 'scope' })} + + {t('dict.col.records', { defaultValue: 'записей' })} + + → + + ← + + {t('dict.col.updated', { defaultValue: 'изменён' })} +
+
+ ) +} + +function DictRow({ d, t }: { d: DictionaryDefinition; t: TFunc }) { + const navigate = useNavigate({ from: '/dictionaries/' }) const { data: refByRaw } = useDictionaryDependents(d.name) const refBy = useMemo(() => uniqueRefBy(refByRaw ?? []), [refByRaw]) - const hasFkRow = refBy.length > 0 + const incomingCount = refBy.length + + const updatedLabel = useMemo(() => { + const date = new Date(d.updatedAt) + return isNaN(date.getTime()) + ? '—' + : date.toLocaleDateString(undefined, { + year: 'numeric', + month: '2-digit', + day: '2-digit', + }) + }, [d.updatedAt]) + + const handleClick = () => { + void navigate({ to: '/dictionaries/$name', params: { name: d.name } }) + } + const handleKey = (e: React.KeyboardEvent) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault() + handleClick() + } + } return ( - - {/* Scope strip 3px вертикальная полоса (handoff spec) */} -