♻️ refactor multi select components

This commit is contained in:
Fred KISSIE
2026-04-29 05:19:36 +02:00
parent 1bc7175dd4
commit 85f2165a1e
5 changed files with 40 additions and 25 deletions

View File

@@ -9,12 +9,13 @@ import {
} from "../ui/command";
import { cn } from "@app/lib/cn";
import { CheckIcon } from "lucide-react";
import { useTranslations } from "next-intl";
export type TagValue = { text: string; id: string };
export type MultiSelectTagsProps<T extends TagValue> = {
emptyPlaceholder: string;
searchPlaceholder: string;
emptyPlaceholder?: string;
searchPlaceholder?: string;
searchQuery?: string;
options: Array<T>;
value: Array<T>;
@@ -33,16 +34,19 @@ export function MultiSelectContent<T extends TagValue>({
onSearch,
onChange
}: MultiSelectTagsProps<T>) {
const t = useTranslations();
const selectedValues = new Set(value.map((v) => v.id));
return (
<Command shouldFilter={false}>
<CommandInput
placeholder={searchPlaceholder}
placeholder={searchPlaceholder ?? t("search")}
value={searchQuery}
onValueChange={onSearch}
/>
<CommandList>
<CommandEmpty>{emptyPlaceholder}</CommandEmpty>
<CommandEmpty className="text-muted-foreground">
{emptyPlaceholder ?? t("noResults")}
</CommandEmpty>
<CommandGroup>
{options.map((option) => (
<CommandItem

View File

@@ -25,7 +25,14 @@ export function MultiSelectTagInput<T extends TagValue>({
const selectedValues = new Set(props.value.map((v) => v.id));
return (
<Popover>
<Popover
onOpenChange={(open) => {
if (!open) {
// clear input when popover is closed
props.onSearch("");
}
}}
>
<PopoverTrigger asChild>
<div
role="combobox"

View File

@@ -12,32 +12,36 @@ export type RolesSelectorProps = {
orgId: string;
selectedRoles?: SelectedRole[];
onSelectRoles: (roles: SelectedRole[]) => void;
disabled?: boolean
disabled?: boolean;
restrictAdminRole?: boolean;
};
export function RolesSelector({
orgId,
selectedRoles = [],
onSelectRoles,
disabled
disabled,
restrictAdminRole
}: RolesSelectorProps) {
const t = useTranslations();
const [roleSearchQuery, setRoleSearchQuery] = useState("");
const [debouncedValue] = useDebounce(roleSearchQuery, 150);
const perPage = 7;
const { data: roles = [] } = useQuery(
orgQueries.roles({ orgId, perPage, query: debouncedValue })
orgQueries.roles({ orgId, perPage: 7, query: debouncedValue })
);
// always include the selected roles in the list (if the user isn't searching)
const rolesShown = useMemo(() => {
const allRoles: Array<SelectedRole> = roles.map((r) => ({
id: r.roleId.toString(),
text: r.name
}));
let allRoles: Array<SelectedRole & { isAdmin?: boolean }> = roles.map(
(r) => ({
id: r.roleId.toString(),
text: r.name,
isAdmin: Boolean(r.isAdmin)
})
);
if (debouncedValue.trim().length === 0) {
for (const role of selectedRoles) {
if (!allRoles.find((r) => r.id === role.id)) {
@@ -45,14 +49,17 @@ export function RolesSelector({
}
}
}
if (restrictAdminRole) {
allRoles = allRoles.filter((role) => !role.isAdmin);
}
return allRoles;
}, [roles, selectedRoles, debouncedValue]);
}, [roles, selectedRoles, debouncedValue, restrictAdminRole]);
return (
<MultiSelectTagInput
buttonText={t("selectRole")}
searchPlaceholder={t("search")}
emptyPlaceholder={t("roles")}
buttonText={t("alertingSelectRoles")}
searchQuery={roleSearchQuery}
onSearch={setRoleSearchQuery}
options={rolesShown}

View File

@@ -96,12 +96,13 @@ function CommandList({
}
function CommandEmpty({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive.Empty>) {
return (
<CommandPrimitive.Empty
data-slot="command-empty"
className="py-6 text-center text-sm"
className={cn("py-6 text-center text-sm", className)}
{...props}
/>
);

View File

@@ -26,10 +26,8 @@ export function UsersSelector({
const [debouncedValue] = useDebounce(userSearchQuery, 150);
const perPage = 7;
const { data: users = [] } = useQuery(
orgQueries.users({ orgId, perPage, query: debouncedValue })
orgQueries.users({ orgId, perPage: 7, query: debouncedValue })
);
// always include the selected users in the list (if the user isn't searching)
@@ -50,9 +48,7 @@ export function UsersSelector({
return (
<MultiSelectTagInput
buttonText={t("accessUserSelect")}
searchPlaceholder={t("search")}
emptyPlaceholder={t("userNotFoundWithUsername")}
buttonText={t("alertingSelectUsers")}
searchQuery={userSearchQuery}
onSearch={setUserSearchQuery}
options={usersShown}