mirror of
https://github.com/fosrl/pangolin.git
synced 2026-05-08 05:17:23 +00:00
🚧 users selector
This commit is contained in:
@@ -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 }) => (
|
||||
<FormItem className="flex flex-col items-start">
|
||||
<FormLabel>{t("users")}</FormLabel>
|
||||
<FormControl>
|
||||
{/* <FormControl>
|
||||
<TagInput
|
||||
{...field}
|
||||
activeTagIndex={
|
||||
@@ -1558,7 +1559,23 @@ export function InternalResourceForm({
|
||||
}
|
||||
sortTags={true}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormControl> */}
|
||||
<UsersSelector
|
||||
{...field}
|
||||
selectedUsers={
|
||||
field.value ?? []
|
||||
}
|
||||
orgId={orgId}
|
||||
onSelectUsers={(newUsers) => {
|
||||
form.setValue(
|
||||
"users",
|
||||
newUsers as [
|
||||
Tag,
|
||||
...Tag[]
|
||||
]
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
|
||||
@@ -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 (
|
||||
<MultiSelectInput
|
||||
<MultiSelectTagInput
|
||||
buttonText={t("accessClientSelect")}
|
||||
searchPlaceholder={t("search")}
|
||||
emptyPlaceholder={t("machineNotFound")}
|
||||
|
||||
@@ -23,7 +23,7 @@ export type MultiSelectTagsProps<T extends TagValue> = {
|
||||
ref?: Ref<HTMLButtonElement>;
|
||||
};
|
||||
|
||||
export function MultiSelectTags<T extends TagValue>({
|
||||
export function MultiSelectContent<T extends TagValue>({
|
||||
emptyPlaceholder,
|
||||
searchPlaceholder,
|
||||
searchQuery,
|
||||
@@ -40,7 +40,8 @@ export function MultiSelectTags<T extends TagValue>({
|
||||
value={searchQuery}
|
||||
onValueChange={onSearch}
|
||||
/>
|
||||
<CommandList>
|
||||
{/* FIXME: why isn't this list scrolling ????? */}
|
||||
<CommandList className="scroll-py-0 max-h-20">
|
||||
<CommandEmpty>{emptyPlaceholder}</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{options.map((option) => (
|
||||
@@ -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<T extends TagValue>({
|
||||
export function MultiSelectTagInput<T extends TagValue>({
|
||||
buttonText,
|
||||
...props
|
||||
}: MultiSelectInputProps<T>) {
|
||||
@@ -83,7 +83,7 @@ export function MultiSelectInput<T extends TagValue>({
|
||||
</div>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="p-0">
|
||||
<MultiSelectTags {...props} />
|
||||
<MultiSelectContent {...props} />
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
1
src/components/roles-selector.tsx
Normal file
1
src/components/roles-selector.tsx
Normal file
@@ -0,0 +1 @@
|
||||
// TODO
|
||||
@@ -87,7 +87,7 @@ function CommandList({
|
||||
<CommandPrimitive.List
|
||||
data-slot="command-list"
|
||||
className={cn(
|
||||
"max-h-[300px] scroll-py-1 overflow-x-hidden overflow-y-auto",
|
||||
"max-h-[300px] scroll-py-1 overflow-x-clip overflow-y-auto",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
||||
64
src/components/users-selector.tsx
Normal file
64
src/components/users-selector.tsx
Normal file
@@ -0,0 +1,64 @@
|
||||
import { orgQueries } from "@app/lib/queries";
|
||||
import type { ListUsersResponse } from "@server/routers/user";
|
||||
import { getUserDisplayName } from "@app/lib/getUserDisplayName";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useMemo, useState } from "react";
|
||||
import { useDebounce } from "use-debounce";
|
||||
|
||||
import { useTranslations } from "next-intl";
|
||||
import { MultiSelectTagInput } from "./multi-select/multi-select-tag-input";
|
||||
|
||||
export type SelectedUser = { id: string; text: string };
|
||||
|
||||
export type UsersSelectorProps = {
|
||||
orgId: string;
|
||||
selectedUsers?: SelectedUser[];
|
||||
onSelectUsers: (users: SelectedUser[]) => 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<SelectedUser> = 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 (
|
||||
<MultiSelectTagInput
|
||||
buttonText={t("accessUserSelect")}
|
||||
searchPlaceholder={t("search")}
|
||||
emptyPlaceholder={t("userNotFoundWithUsername")}
|
||||
searchQuery={userSearchQuery}
|
||||
onSearch={setUserSearchQuery}
|
||||
options={usersShown}
|
||||
value={selectedUsers}
|
||||
onChange={onSelectUsers}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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<ListUsersResponse>
|
||||
>(`/org/${orgId}/users`, { signal });
|
||||
>(`/org/${orgId}/users?${sp.toString()}`, { signal });
|
||||
|
||||
return res.data.data.users;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user