diff --git a/ordinis-admin-ui/src/components/audit/UserPicker.tsx b/ordinis-admin-ui/src/components/audit/UserPicker.tsx index d6a0f17..4b93a34 100644 --- a/ordinis-admin-ui/src/components/audit/UserPicker.tsx +++ b/ordinis-admin-ui/src/components/audit/UserPicker.tsx @@ -46,11 +46,23 @@ export function UserPicker({ value, onChange, label, placeholder, disabled }: Us const fallbackToTextInput = !usersQ.isLoading && !usersQ.isError && usersList.length === 0 + // Все хуки ДО любого early-return — иначе rules-of-hooks нарушится при + // переходе loading→empty (loading rendered ВСЕ хуки → empty возвращает + // TextInput минуя последующие useMemo). React в production падает на + // "rendered fewer hooks than expected" → error boundary → "Что-то пошло + // не так". Reproduce: loading=true первый рендер, loading=false data=[] + // второй. const usersBySub = useMemo(() => { const m = new Map() for (const u of usersList) m.set(u.sub, u) return m }, [usersList]) + const selected = value ? usersBySub.get(value) : undefined + const triggerLabel = useMemo(() => { + if (!value) return placeholder ?? 'Любой' + if (selected) return labelFor(selected) + return `${value.slice(0, 8)}…` + }, [value, selected, placeholder]) if (fallbackToTextInput) { return ( @@ -65,14 +77,6 @@ export function UserPicker({ value, onChange, label, placeholder, disabled }: Us ) } - const selected = value ? usersBySub.get(value) : undefined - const triggerLabel = useMemo(() => { - if (!value) return placeholder ?? 'Любой' - if (selected) return labelFor(selected) - // Value есть, но юзера нет в кеше — показываем UUID prefix. - return `${value.slice(0, 8)}…` - }, [value, selected, placeholder]) - // Поиск: substring по preferredUsername / name / email / sub. cmdk // делает client-side фильтрацию по value атрибуту CommandItem'а; // концатенируем все поля в один searchable string.