feat: /audit поле «Пользователь» — typeahead picker из user-cache

Backend: GET /api/v1/admin/users (INTERNAL+ scope) — list cached юзеров
из user_display_cache. UserDisplayService.listAll(limit) сортирует по
preferred_username, hard-cap 1000. Контроллер: UserDisplayController.list().

Frontend:
- queries.ts → useAllUsers() / allUsersQuery (5min stale, 403/404 → []).
- components/audit/UserPicker.tsx (новый, ~140 строк) — cmdk-powered
  combobox: trigger показывает username (или UUID prefix fallback),
  popover c поиском по preferredUsername/email/UUID. X сбрасывает selection.
- routes/audit.tsx — TextInput → UserPicker.

UX: раньше юзер вручную копипастил UUID из audit-row'а; теперь поиск
по имени/email типа `solov` → находит `solovyev.da`.
This commit is contained in:
Zimin A.N.
2026-05-25 16:48:41 +03:00
parent e72aa37e84
commit 3f05741ed1
5 changed files with 228 additions and 4 deletions
+33
View File
@@ -782,6 +782,39 @@ export const useLiveRecord = (dict: string | undefined, businessKey: string | un
export const useDictionaries = () => useQuery(dictionariesQuery)
export const useDictionaryDetail = (name: string | undefined) =>
useQuery({ ...dictionaryDetailQuery(name ?? ''), enabled: Boolean(name) })
/**
* Список всех закэшированных юзеров — для пикеров «Пользователь» в
* фильтрах audit/reviews. INTERNAL+ scope; non-INTERNAL получит 403,
* fallback на пустой массив (retry: false).
*/
export type UserListItem = {
sub: string
preferredUsername?: string | null
name?: string | null
email?: string | null
updatedAt: string
}
export const allUsersQuery = queryOptions({
queryKey: ['users', 'all'] as const,
staleTime: 5 * 60 * 1000,
retry: false,
queryFn: async (): Promise<UserListItem[]> => {
try {
const { data } = await apiClient.get<UserListItem[]>('/admin/users', {
params: { limit: 500 },
})
return data
} catch (e: unknown) {
const status = (e as { response?: { status?: number } })?.response?.status
if (status === 403 || status === 404) return []
throw e
}
},
})
export const useAllUsers = () => useQuery(allUsersQuery)
export const useRecords = (
dictionaryName: string,
scopeCsv: string,