fix(admin-ui): v1.1.1 patch — version routing + guest mode + timeline UX

This commit is contained in:
Александр Зимин
2026-05-10 19:34:47 +00:00
parent 54817e1a9b
commit 69f1a05a1f
8 changed files with 419 additions and 144 deletions
@@ -18,9 +18,11 @@ import { createFileRoute, Link, useNavigate } from '@tanstack/react-router'
import { useTranslation } from 'react-i18next'
import { Alert, Badge, Button, EmptyState, LoadingBlock, PageHeader, SearchInput } from '@nstart/ui'
import { PlusIcon } from '@phosphor-icons/react'
import { useDictionaries, useDictionaryDependents } from '@/api/queries'
import { useQueries } from '@tanstack/react-query'
import { dictionaryDependentsQuery, useDictionaries, useDictionaryDependents } from '@/api/queries'
import type { DataScope, DictionaryDefinition, SchemaDependent } from '@/api/client'
import { DictionaryEditorDialog } from '@/components/schema/DictionaryEditorDialog'
import { useCanMutate } from '@/auth/useCanMutate'
import { SCOPE_DOT, SCOPE_ORDER } from '@/lib/scope-style'
// ===== Search params =====
@@ -101,6 +103,7 @@ function DictionariesPage() {
const search = Route.useSearch()
const { data, isLoading, error } = useDictionaries()
const [createOpen, setCreateOpen] = useState(false)
const canMutate = useCanMutate()
const q = (search.q ?? '').trim().toLowerCase()
const deferredQuery = useDeferredValue(q)
@@ -130,19 +133,40 @@ function DictionariesPage() {
const setBundle = (bundle: string | undefined) => setSearch({ bundle })
// Батчевая загрузка refBy для всех словарей — для активации withDeps filter.
// useQueries dedup'ит c per-card useDictionaryDependents (тот же queryKey),
// так что N=37 запросов выполнятся один раз и cached. Запускается ТОЛЬКО
// когда deps filter активен — для anonymous browsing N+1 burst не нужен.
const dependentsResults = useQueries({
queries: withDepsOnly && data
? data.map((d) => ({ ...dictionaryDependentsQuery(d.name) }))
: [],
})
const dependentsMap = useMemo(() => {
const m = new Map<string, number>()
if (!withDepsOnly || !data) return m
data.forEach((d, i) => {
const r = dependentsResults[i]
// Пока loading или error — считаем 0 (не показываем). После загрузки —
// refBy.length. Уникализация не нужна для бинарного hasDeps теста.
m.set(d.name, r?.data?.length ?? 0)
})
return m
}, [data, dependentsResults, withDepsOnly])
const filtered = useMemo(() => {
if (!data) return []
return data.filter((d) => {
if (scopeFilter.size > 0 && !scopeFilter.has(d.scope)) return false
if (bundleFilter && d.bundle !== bundleFilter) return false
if (!matchesQuery(d, deferredQuery)) return false
// withDepsOnly: пока нет FK metadata в DictionaryDefinition, фильтр
// эффективно no-op. После backend batch FK endpoint — заработает.
// TODO: backend пишет fk + refBy в DictionaryDefinition; раскомментить:
// if (withDepsOnly && d.fk.length === 0 && d.refBy.length === 0) return false
// «Со связями» — справочник используется в других справочниках (refBy > 0).
// Outgoing FK потребовал бы загрузки полной schema per dict — not worth
// the bandwidth для catalog filter. refBy достаточно для semantic.
if (withDepsOnly && (dependentsMap.get(d.name) ?? 0) === 0) return false
return true
})
}, [data, deferredQuery, scopeFilter, bundleFilter])
}, [data, deferredQuery, scopeFilter, bundleFilter, withDepsOnly, dependentsMap])
const bundleCounts = useMemo(() => {
const out = new Map<string, number>()
@@ -160,7 +184,9 @@ function DictionariesPage() {
const filtersActive = Boolean(q) || scopeFilter.size > 0 || bundleFilter || withDepsOnly
const resetFilters = () => navigate({ search: {} })
const createButton = (
// Guest mode (anonymous) — кнопка «Создать справочник» скрыта. Backend всё
// равно вернёт 401 на POST, но UI hide избегает confusing UX.
const createButton = canMutate ? (
<Button
type="button"
variant="primary"
@@ -170,7 +196,7 @@ function DictionariesPage() {
>
{t('schema.action.create')}
</Button>
)
) : null
if (isLoading) return <LoadingBlock size="md" label={t('loading')} />
if (error) {