Merge branch 'fix/topbar-remove-search-catalog-clear-x' into 'main'
fix(ui): remove TopBar search + add clear X to catalog input See merge request 2-6/2-6-4/terravault/ordinis!92
This commit is contained in:
@@ -1,10 +1,8 @@
|
|||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { Link, useLocation, useNavigate } from '@tanstack/react-router'
|
import { Link, useLocation } from '@tanstack/react-router'
|
||||||
import { useDictionaries, useDictionaryDetail } from '@/api/queries'
|
import { useDictionaries, useDictionaryDetail } from '@/api/queries'
|
||||||
import { VersionBadge } from '@/components/version/VersionBadge'
|
import { VersionBadge } from '@/components/version/VersionBadge'
|
||||||
import { SearchInput } from '@/ui/components/search-input'
|
|
||||||
import { Bell, Menu } from 'lucide-react'
|
import { Bell, Menu } from 'lucide-react'
|
||||||
import { useEffect, useRef, useState } from 'react'
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* TopBar — sticky header (h=56) per handoff design.
|
* TopBar — sticky header (h=56) per handoff design.
|
||||||
@@ -17,78 +15,8 @@ import { useEffect, useRef, useState } from 'react'
|
|||||||
*/
|
*/
|
||||||
export function TopBar({ onMenuClick }: { onMenuClick?: () => void }) {
|
export function TopBar({ onMenuClick }: { onMenuClick?: () => void }) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const navigate = useNavigate()
|
|
||||||
const location = useLocation()
|
|
||||||
const isCatalog = location.pathname === '/dictionaries'
|
|
||||||
const catalogQ = isCatalog
|
|
||||||
? ((location.search as Record<string, unknown>)?.q as string | undefined) ?? ''
|
|
||||||
: ''
|
|
||||||
const [searchValue, setSearchValue] = useState(catalogQ)
|
|
||||||
const inputRef = useRef<HTMLInputElement>(null)
|
|
||||||
const breadcrumb = useBreadcrumb()
|
const breadcrumb = useBreadcrumb()
|
||||||
|
|
||||||
// Sync local input value c URL `?q=` when navigating between routes.
|
|
||||||
useEffect(() => {
|
|
||||||
setSearchValue(catalogQ)
|
|
||||||
}, [catalogQ])
|
|
||||||
|
|
||||||
// ⌘K / Ctrl+K focus shortcut.
|
|
||||||
useEffect(() => {
|
|
||||||
const handler = (e: KeyboardEvent) => {
|
|
||||||
if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === 'k') {
|
|
||||||
const target = e.target as HTMLElement
|
|
||||||
if (target && (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA')) {
|
|
||||||
if (target === inputRef.current) return
|
|
||||||
}
|
|
||||||
e.preventDefault()
|
|
||||||
inputRef.current?.focus()
|
|
||||||
inputRef.current?.select()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
window.addEventListener('keydown', handler)
|
|
||||||
return () => window.removeEventListener('keydown', handler)
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
// Context-aware search:
|
|
||||||
// - На /dictionaries — live filter каталога через URL `?q=`
|
|
||||||
// - На остальных routes — local state, submit redirect'ит на /search
|
|
||||||
const handleSearchInput = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
const v = e.target.value
|
|
||||||
setSearchValue(v)
|
|
||||||
if (isCatalog) {
|
|
||||||
void navigate({
|
|
||||||
to: '/dictionaries',
|
|
||||||
search: (prev: Record<string, unknown>) => ({
|
|
||||||
...prev,
|
|
||||||
q: v.length > 0 ? v : undefined,
|
|
||||||
}),
|
|
||||||
replace: true,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleSearchSubmit = (e: React.FormEvent) => {
|
|
||||||
e.preventDefault()
|
|
||||||
const q = searchValue.trim()
|
|
||||||
if (q.length === 0) return
|
|
||||||
if (isCatalog) return
|
|
||||||
void navigate({ to: '/search', search: { q } })
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleSearchClear = () => {
|
|
||||||
setSearchValue('')
|
|
||||||
if (isCatalog) {
|
|
||||||
void navigate({
|
|
||||||
to: '/dictionaries',
|
|
||||||
search: (prev: Record<string, unknown>) => {
|
|
||||||
const { q: _q, ...rest } = prev
|
|
||||||
return rest
|
|
||||||
},
|
|
||||||
replace: true,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header className="h-11 shrink-0 border-b border-line bg-surface flex items-center px-4 sm:px-6 gap-3">
|
<header className="h-11 shrink-0 border-b border-line bg-surface flex items-center px-4 sm:px-6 gap-3">
|
||||||
{/* Hamburger (lg:hidden) — toggle MobileSidebar */}
|
{/* Hamburger (lg:hidden) — toggle MobileSidebar */}
|
||||||
@@ -144,24 +72,9 @@ export function TopBar({ onMenuClick }: { onMenuClick?: () => void }) {
|
|||||||
})}
|
})}
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
{/* Search center — context-aware (catalog mode = live filter, else =
|
{/* TopBar search убран per user — catalog имеет свой input в toolbar,
|
||||||
* records search submit). Двойной X fixed в SearchInput component
|
* для records search используется /search route через sidebar nav.
|
||||||
* (type="text" вместо "search" — убрал native browser X). */}
|
* "Search из TopBar убран — все верно". */}
|
||||||
<form onSubmit={handleSearchSubmit} className="hidden md:block ml-auto w-full max-w-[280px]">
|
|
||||||
<SearchInput
|
|
||||||
ref={inputRef}
|
|
||||||
placeholder={
|
|
||||||
isCatalog
|
|
||||||
? t('topbar.search.catalog', { defaultValue: 'Поиск по справочникам, ⌘K' })
|
|
||||||
: t('topbar.search.placeholder')
|
|
||||||
}
|
|
||||||
value={searchValue}
|
|
||||||
onChange={handleSearchInput}
|
|
||||||
onClear={handleSearchClear}
|
|
||||||
aria-label={t('topbar.search.label')}
|
|
||||||
className="!h-7 text-cell"
|
|
||||||
/>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
{/* Right cluster — broadcast slot per user: "руководство и версию
|
{/* Right cluster — broadcast slot per user: "руководство и версию
|
||||||
* оставить в топбар чтобы подсвечивать новинки. туда же и сообщения".
|
* оставить в топбар чтобы подсвечивать новинки. туда же и сообщения".
|
||||||
|
|||||||
@@ -232,9 +232,9 @@ function DictionariesPage() {
|
|||||||
* Узкая: px-3 py-1.5 (~6px vertical) — per user feedback ("бар поужать"). */}
|
* Узкая: px-3 py-1.5 (~6px vertical) — per user feedback ("бар поужать"). */}
|
||||||
<div className="flex flex-wrap items-center gap-2 bg-surface-2 border-b border-line px-3 py-1.5">
|
<div className="flex flex-wrap items-center gap-2 bg-surface-2 border-b border-line px-3 py-1.5">
|
||||||
{/* Catalog search — filter dicts by name/displayName/description.
|
{/* Catalog search — filter dicts by name/displayName/description.
|
||||||
* type="text" (не "search") чтобы избежать native X — мы рендерим
|
* type="text" чтобы избежать native browser X — рендерим свой
|
||||||
* свой clear button если нужно. */}
|
* custom clear button справа когда есть value. Single X. */}
|
||||||
<div className="flex-1 min-w-[200px] max-w-xs">
|
<div className="flex-1 min-w-[200px] max-w-xs relative">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={search.q ?? ''}
|
value={search.q ?? ''}
|
||||||
@@ -247,8 +247,18 @@ function DictionariesPage() {
|
|||||||
aria-label={t('dict.list.search.placeholder', {
|
aria-label={t('dict.list.search.placeholder', {
|
||||||
defaultValue: 'Поиск по справочникам…',
|
defaultValue: 'Поиск по справочникам…',
|
||||||
})}
|
})}
|
||||||
className="h-8 w-full px-2.5 rounded-md border border-line bg-surface text-cell text-ink placeholder:text-mute focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent"
|
className={`h-8 w-full px-2.5 ${search.q ? 'pr-7' : ''} rounded-md border border-line bg-surface text-cell text-ink placeholder:text-mute focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent`}
|
||||||
/>
|
/>
|
||||||
|
{search.q && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setSearch({ q: undefined })}
|
||||||
|
aria-label={t('common.clear', { defaultValue: 'Очистить' })}
|
||||||
|
className="absolute right-1.5 top-1/2 -translate-y-1/2 text-mute hover:text-ink rounded-sm p-0.5 hover:bg-surface-2"
|
||||||
|
>
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Scope filter: PUBLIC / INTERNAL / RESTRICTED — pill chips с border */}
|
{/* Scope filter: PUBLIC / INTERNAL / RESTRICTED — pill chips с border */}
|
||||||
|
|||||||
Reference in New Issue
Block a user