From 171d049fc54d86257eac482e346b6c80cc9deb21 Mon Sep 17 00:00:00 2001 From: "Zimin A.N." Date: Wed, 6 May 2026 08:46:45 +0300 Subject: [PATCH] =?UTF-8?q?feat(admin-ui):=20=D0=BF=D0=BE=D0=B8=D1=81?= =?UTF-8?q?=D0=BA=20=D1=81=D0=BF=D1=80=D0=B0=D0=B2=D0=BE=D1=87=D0=BD=D0=B8?= =?UTF-8?q?=D0=BA=D0=BE=D0=B2=20+=20=D1=81=D1=87=D1=91=D1=82=D1=87=D0=B8?= =?UTF-8?q?=D0=BA=20=D0=B0=D0=BA=D1=82=D0=B8=D0=B2=D0=BD=D1=8B=D1=85=20?= =?UTF-8?q?=D0=B7=D0=B0=D0=BF=D0=B8=D1=81=D0=B5=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Backend: - DictionaryRecordRepository.countActiveGroupedByDictionary — один GROUP BY по всем справочникам с фильтром по scope, без N+1. - DictionaryResponse.recordCount (nullable) + factory from(d, count). - DictionaryDefinitionController передаёт счётчики в list/get, фильтрованные по currentScopes() пользователя. Frontend: - SearchInput над сеткой карточек, client-side filter по name/displayName/description (case-insensitive, useDeferredValue). - Badge "N записей" (i18n plural ru/en) в углу карточки. - "Показано N из M" рядом с поиском. - EmptyState когда поиск пустой. --- ordinis-admin-ui/src/api/client.ts | 1 + ordinis-admin-ui/src/i18n.ts | 12 ++ .../src/routes/dictionaries.index.tsx | 106 +++++++++++++----- .../record/DictionaryRecordRepository.java | 19 ++++ .../restapi/dto/DictionaryResponse.java | 6 + .../web/DictionaryDefinitionController.java | 28 ++++- 6 files changed, 141 insertions(+), 31 deletions(-) diff --git a/ordinis-admin-ui/src/api/client.ts b/ordinis-admin-ui/src/api/client.ts index 0808a38..95fa61c 100644 --- a/ordinis-admin-ui/src/api/client.ts +++ b/ordinis-admin-ui/src/api/client.ts @@ -35,6 +35,7 @@ export type DictionaryDefinition = { bundle: string supportedLocales: string[] defaultLocale: string + recordCount?: number createdAt: string updatedAt: string } diff --git a/ordinis-admin-ui/src/i18n.ts b/ordinis-admin-ui/src/i18n.ts index ed596c9..40c1e44 100644 --- a/ordinis-admin-ui/src/i18n.ts +++ b/ordinis-admin-ui/src/i18n.ts @@ -61,6 +61,13 @@ i18n 'outbox.dlq.empty': 'DLQ пуста — все события публикуются успешно', 'header.scope': 'Доступный scope', 'dict.list.subtitle': 'Каталоги НСИ ЦУОД ОДХ', + 'dict.list.search.placeholder': 'Поиск по названию, коду или описанию', + 'dict.list.search.empty': 'Ничего не найдено', + 'dict.list.found': 'Показано {{shown}} из {{total}}', + 'dict.list.recordCount_one': '{{count}} запись', + 'dict.list.recordCount_few': '{{count}} записи', + 'dict.list.recordCount_many': '{{count}} записей', + 'dict.list.recordCount_other': '{{count}} записей', 'dict.empty': 'В этом справочнике пока нет записей', 'dict.col.businessKey': 'Бизнес-ключ', 'dict.col.scope': 'Scope', @@ -227,6 +234,11 @@ i18n 'outbox.dlq.empty': 'DLQ empty — all events publish successfully', 'header.scope': 'Allowed scope', 'dict.list.subtitle': 'Reference data catalogues', + 'dict.list.search.placeholder': 'Search by name, code or description', + 'dict.list.search.empty': 'Nothing found', + 'dict.list.found': 'Showing {{shown}} of {{total}}', + 'dict.list.recordCount_one': '{{count}} record', + 'dict.list.recordCount_other': '{{count}} records', 'dict.empty': 'No records in this dictionary yet', 'dict.col.businessKey': 'Business key', 'dict.col.scope': 'Scope', diff --git a/ordinis-admin-ui/src/routes/dictionaries.index.tsx b/ordinis-admin-ui/src/routes/dictionaries.index.tsx index 339da4d..65e76bd 100644 --- a/ordinis-admin-ui/src/routes/dictionaries.index.tsx +++ b/ordinis-admin-ui/src/routes/dictionaries.index.tsx @@ -1,20 +1,45 @@ -import { useState } from 'react' +import { useDeferredValue, useMemo, useState } from 'react' import { createFileRoute, Link, useNavigate } from '@tanstack/react-router' import { useTranslation } from 'react-i18next' -import { Alert, Badge, Button, EmptyState, LoadingBlock, PageHeader } from '@nstart/ui' +import { + Alert, + Badge, + Button, + EmptyState, + LoadingBlock, + PageHeader, + SearchInput, +} from '@nstart/ui' import { PlusIcon } from '@phosphor-icons/react' import { useDictionaries } from '@/api/queries' +import type { DictionaryDefinition } from '@/api/client' import { DictionaryEditorDialog } from '@/components/schema/DictionaryEditorDialog' export const Route = createFileRoute('/dictionaries/')({ component: DictionariesPage, }) +const matchesQuery = (d: DictionaryDefinition, q: string): boolean => { + if (!q) return true + const haystack = [d.name, d.displayName ?? '', d.description ?? ''] + .join(' ') + .toLowerCase() + return haystack.includes(q) +} + function DictionariesPage() { const { t } = useTranslation() const navigate = useNavigate() const { data, isLoading, error } = useDictionaries() const [createOpen, setCreateOpen] = useState(false) + const [query, setQuery] = useState('') + const deferredQuery = useDeferredValue(query) + + const filtered = useMemo(() => { + if (!data) return [] + const q = deferredQuery.trim().toLowerCase() + return data.filter((d) => matchesQuery(d, q)) + }, [data, deferredQuery]) const createButton = (