fix(admin-ui): UserPicker — TextInput fallback когда cache пустой

Если /api/v1/admin/users возвращает [] (cache пустой / writer только-что
рестартован / у юзера нет INTERNAL scope → 403 → catch → []), picker
показывал «Пользователи не найдены» и фильтр становился недоступен. До
MR !263 здесь был обычный TextInput, не лишаем юзера возможности вписать
UUID руками.

При isLoading=false, isError=false, data.length===0 — рендерим TextInput
с подсказкой «Список юзеров пуст — введите вручную». Backend cache
прогреется по мере того как юзеры будут делать authenticated requests
(JwtUserCaptureFilter перепишет user_display_cache); тогда picker
включится автоматически.
This commit is contained in:
Zimin A.N.
2026-05-25 17:50:23 +03:00
parent 07a748be53
commit 838138af5e
@@ -2,7 +2,7 @@ import * as React from 'react'
import { useMemo, useState } from 'react'
import { CaretDownIcon, MagnifyingGlassIcon, UserIcon, XIcon } from '@phosphor-icons/react'
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from 'cmdk'
import { Popover, PopoverTrigger, PopoverContent, FieldLabel } from '@/ui'
import { Popover, PopoverTrigger, PopoverContent, FieldLabel, TextInput } from '@/ui'
import { useAllUsers, type UserListItem } from '@/api/queries'
import { cn } from '@/lib/utils'
@@ -38,11 +38,32 @@ export function UserPicker({ value, onChange, label, placeholder, disabled }: Us
const [open, setOpen] = useState(false)
const usersQ = useAllUsers()
// Fallback на TextInput если cache пустой или endpoint вернул 403/404 (юзер
// без INTERNAL scope, либо writer был только-что рестартован и cache не
// прогрелся). Без этого user-фильтр становится недоступен — раньше тут был
// TextInput, не лишаем юзера возможности вписать UUID руками.
const usersList = usersQ.data ?? []
const fallbackToTextInput =
!usersQ.isLoading && !usersQ.isError && usersList.length === 0
const usersBySub = useMemo(() => {
const m = new Map<string, UserListItem>()
for (const u of usersQ.data ?? []) m.set(u.sub, u)
for (const u of usersList) m.set(u.sub, u)
return m
}, [usersQ.data])
}, [usersList])
if (fallbackToTextInput) {
return (
<TextInput
label={label}
value={value ?? ''}
onChange={(e) => onChange(e.target.value || undefined)}
placeholder="UUID или username"
hint="Список юзеров пуст — введите вручную"
disabled={disabled}
/>
)
}
const selected = value ? usersBySub.get(value) : undefined
const triggerLabel = useMemo(() => {