mirror of
https://github.com/fosrl/pangolin.git
synced 2026-05-12 07:14:14 +00:00
♻️ refactor multi select components
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
Reference in New Issue
Block a user