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

View File

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

View File

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

View File

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

View File

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