From 27b2ec309d18e598d6297fc46a377bea9029ed88 Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Sat, 25 Apr 2026 06:18:13 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=9A=A7=20users=20selector?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/InternalResourceForm.tsx | 21 +++++- src/components/machines-selector.tsx | 6 +- ...lect-tags.tsx => multi-select-content.tsx} | 5 +- ...t-input.tsx => multi-select-tag-input.tsx} | 8 +-- src/components/roles-selector.tsx | 1 + src/components/ui/command.tsx | 2 +- src/components/users-selector.tsx | 64 +++++++++++++++++++ src/lib/queries.ts | 22 ++++++- 8 files changed, 114 insertions(+), 15 deletions(-) rename src/components/multi-select/{multi-select-tags.tsx => multi-select-content.tsx} (93%) rename src/components/multi-select/{multi-select-input.tsx => multi-select-tag-input.tsx} (95%) create mode 100644 src/components/roles-selector.tsx create mode 100644 src/components/users-selector.tsx diff --git a/src/components/InternalResourceForm.tsx b/src/components/InternalResourceForm.tsx index f63192129..af3841818 100644 --- a/src/components/InternalResourceForm.tsx +++ b/src/components/InternalResourceForm.tsx @@ -60,6 +60,7 @@ import { MachinesSelector } from "./machines-selector"; import DomainPicker from "@app/components/DomainPicker"; import { SwitchInput } from "@app/components/SwitchInput"; import CertificateStatus from "@app/components/CertificateStatus"; +import { UsersSelector } from "./users-selector"; // --- Helpers (shared) --- @@ -1522,7 +1523,7 @@ export function InternalResourceForm({ render={({ field }) => ( {t("users")} - + {/* - + */} + { + form.setValue( + "users", + newUsers as [ + Tag, + ...Tag[] + ] + ); + }} + /> )} diff --git a/src/components/machines-selector.tsx b/src/components/machines-selector.tsx index d4b37bb74..cfae4c2d8 100644 --- a/src/components/machines-selector.tsx +++ b/src/components/machines-selector.tsx @@ -5,7 +5,7 @@ import { useMemo, useState } from "react"; import { useDebounce } from "use-debounce"; import { useTranslations } from "next-intl"; -import { MultiSelectInput } from "./multi-select/multi-select-input"; +import { MultiSelectTagInput } from "./multi-select/multi-select-tag-input"; export type SelectedMachine = Pick< ListClientsResponse["clients"][number], @@ -46,11 +46,11 @@ export function MachinesSelector({ } } } - return allMachines.slice(0, perPage); + return allMachines; }, [machines, selectedMachines, debouncedValue]); return ( - = { ref?: Ref; }; -export function MultiSelectTags({ +export function MultiSelectContent({ emptyPlaceholder, searchPlaceholder, searchQuery, @@ -40,7 +40,8 @@ export function MultiSelectTags({ value={searchQuery} onValueChange={onSearch} /> - + {/* FIXME: why isn't this list scrolling ????? */} + {emptyPlaceholder} {options.map((option) => ( diff --git a/src/components/multi-select/multi-select-input.tsx b/src/components/multi-select/multi-select-tag-input.tsx similarity index 95% rename from src/components/multi-select/multi-select-input.tsx rename to src/components/multi-select/multi-select-tag-input.tsx index 4f4fec528..dfca035a7 100644 --- a/src/components/multi-select/multi-select-input.tsx +++ b/src/components/multi-select/multi-select-tag-input.tsx @@ -9,8 +9,8 @@ import { ChevronDownIcon, XIcon } from "lucide-react"; import { type TagValue, type MultiSelectTagsProps, - MultiSelectTags -} from "./multi-select-tags"; + MultiSelectContent +} from "./multi-select-content"; import { useState } from "react"; export interface MultiSelectInputProps< @@ -19,7 +19,7 @@ export interface MultiSelectInputProps< buttonText?: string; } -export function MultiSelectInput({ +export function MultiSelectTagInput({ buttonText, ...props }: MultiSelectInputProps) { @@ -83,7 +83,7 @@ export function MultiSelectInput({ - + ); diff --git a/src/components/roles-selector.tsx b/src/components/roles-selector.tsx new file mode 100644 index 000000000..70b786d12 --- /dev/null +++ b/src/components/roles-selector.tsx @@ -0,0 +1 @@ +// TODO diff --git a/src/components/ui/command.tsx b/src/components/ui/command.tsx index 8b2b6748a..67cffeec3 100644 --- a/src/components/ui/command.tsx +++ b/src/components/ui/command.tsx @@ -87,7 +87,7 @@ function CommandList({ void; +}; + +export function UsersSelector({ + orgId, + selectedUsers = [], + onSelectUsers +}: UsersSelectorProps) { + const t = useTranslations(); + const [userSearchQuery, setUserSearchQuery] = useState(""); + + const [debouncedValue] = useDebounce(userSearchQuery, 150); + + // TODO: switch back to 7 items + const perPage = 1; + + const { data: users = [] } = useQuery( + orgQueries.users({ orgId, perPage, query: debouncedValue }) + ); + + // always include the selected users in the list (if the user isn't searching) + const usersShown = useMemo(() => { + const allUsers: Array = users.map((u) => ({ + id: u.id, + text: getUserDisplayName(u) + })); + if (debouncedValue.trim().length === 0) { + for (const user of selectedUsers) { + if (!allUsers.find((u) => u.id === user.id)) { + allUsers.unshift(user); + } + } + } + return allUsers; + }, [users, selectedUsers, debouncedValue]); + + return ( + + ); +} diff --git a/src/lib/queries.ts b/src/lib/queries.ts index 3e38a7ba0..ab22b5b57 100644 --- a/src/lib/queries.ts +++ b/src/lib/queries.ts @@ -125,13 +125,29 @@ export const orgQueries = { return res.data.data.clients; } }), - users: ({ orgId }: { orgId: string }) => + users: ({ + orgId, + query, + perPage = 10_000 + }: { + orgId: string; + query?: string; + perPage?: number; + }) => queryOptions({ - queryKey: ["ORG", orgId, "USERS"] as const, + queryKey: ["ORG", orgId, "USERS", { query, perPage }] as const, queryFn: async ({ signal, meta }) => { + const sp = new URLSearchParams({ + pageSize: perPage.toString() + }); + + if (query?.trim()) { + sp.set("query", query); + } + const res = await meta!.api.get< AxiosResponse - >(`/org/${orgId}/users`, { signal }); + >(`/org/${orgId}/users?${sp.toString()}`, { signal }); return res.data.data.users; }