chore: update design of add users dialog (#24287)

update design to match Figma design

Figma:
https://www.figma.com/design/klGTlHSPQwI4KBvAMdebrx/Customer-Usage-Controls-for-AI-Governance-Add-On?node-id=448-9898&m=dev

Before:

<img width="482" height="638" alt="Screenshot 2026-04-13 at 16 48 32"
src="https://github.com/user-attachments/assets/940dbbd0-4817-4365-aad2-08148550109c"
/>

After:

<img width="474" height="653" alt="Screenshot 2026-04-13 at 16 45 55"
src="https://github.com/user-attachments/assets/c63591fa-1513-4c66-b48f-82df794ca080"
/>
This commit is contained in:
Jaayden Halko
2026-04-16 13:48:08 +01:00
committed by GitHub
parent 509784a2a3
commit 4064b602de
2 changed files with 153 additions and 102 deletions
@@ -13,18 +13,7 @@ import { AvatarDataSkeleton } from "#/components/Avatar/AvatarDataSkeleton";
import { Checkbox } from "#/components/Checkbox/Checkbox";
import { EmptyState } from "#/components/EmptyState/EmptyState";
import { SearchField } from "#/components/SearchField/SearchField";
import {
Table,
TableBody,
TableCell,
TableRow,
} from "#/components/Table/Table";
import {
TableLoaderSkeleton,
TableRowSkeleton,
} from "#/components/TableLoader/TableLoader";
import { useDebouncedFunction } from "#/hooks/debounce";
import { useClickableTableRow } from "#/hooks/useClickableTableRow";
import { cn } from "#/utils/cn";
import { prepareQuery } from "#/utils/filters";
@@ -120,9 +109,9 @@ const InnerMultiSelect = <T extends SelectedUser>({
);
return (
<div className={cn("flex flex-col gap-2", className)}>
<div className={cn("flex flex-col gap-4", className)}>
<SearchField
className="w-full"
className="h-12 w-full rounded-lg"
value={inputValue}
aria-label="Search users"
onChange={(query) => {
@@ -136,17 +125,22 @@ const InnerMultiSelect = <T extends SelectedUser>({
}}
placeholder="Search users..."
/>
<div className="max-h-[360px] overflow-auto">
<Table>
<TableBody className="[&_td]:h-[72px]">
<div className="h-96 w-full rounded-lg border border-border border-solid">
<div className="h-full overflow-hidden p-px">
<div
className="h-full overflow-y-auto overflow-x-hidden overscroll-contain"
onWheel={(event) => {
event.stopPropagation();
}}
>
<UsersTable
error={error}
onChange={onChange}
selected={selected}
users={users}
/>
</TableBody>
</Table>
</div>
</div>
</div>
</div>
);
@@ -167,11 +161,9 @@ const UsersTable = <T extends SelectedUser>({
}: UsersTable<T>) => {
if (error) {
return (
<TableRow>
<TableCell colSpan={999}>
<ErrorAlert error={error} />
</TableCell>
</TableRow>
<div className="p-3">
<ErrorAlert error={error} />
</div>
);
}
@@ -181,66 +173,72 @@ const UsersTable = <T extends SelectedUser>({
if (users.length === 0) {
return (
<TableRow>
<TableCell colSpan={999}>
<EmptyState message="No users found" isCompact />
</TableCell>
</TableRow>
<div className="p-3">
<EmptyState message="No users found" isCompact />
</div>
);
}
return users.map((user) => {
const checked = selected.some((u) => userMatches(u, user));
return (
<UserRow
key={user.username}
user={user}
checked={checked}
onChange={onChange}
>
<TableCell className="border-0">
<div className="flex items-center gap-5">
<Checkbox
data-testid={`checkbox-${user.username}`}
checked={checked}
onClick={(e) => {
e.stopPropagation();
}}
onCheckedChange={(checked) => {
onChange(user, Boolean(checked));
}}
aria-label={`Select user ${user.username}`}
/>
<AvatarData
title={user.username}
subtitle={user.email}
src={user.avatar_url}
/>
</div>
</TableCell>
</UserRow>
);
});
return (
<div className="flex flex-col">
{users.map((user, index) => {
const checked = selected.some((u) => userMatches(u, user));
return (
<UserRow
key={user.username}
user={user}
checked={checked}
isFirst={index === 0}
isLast={index === users.length - 1}
onChange={onChange}
>
<div className="flex items-center gap-3">
<Checkbox
data-testid={`checkbox-${user.username}`}
checked={checked}
onClick={(e) => {
e.stopPropagation();
}}
onCheckedChange={(checked) => {
onChange(user, Boolean(checked));
}}
aria-label={`Select user ${user.username}`}
/>
<AvatarData
title={user.username}
subtitle={user.email}
src={user.avatar_url}
/>
</div>
</UserRow>
);
})}
</div>
);
};
const TableLoader: FC = () => {
const skeletonRows = Array.from({ length: 6 }, (_, index) => index);
return (
<TableLoaderSkeleton>
<TableRowSkeleton>
<TableCell className="w-2/6">
<div className="flex items-center gap-5">
<div>
{skeletonRows.map((row) => (
<div className="flex min-h-[64px] items-center px-4 py-3" key={row}>
<div className="flex items-center gap-3">
<Checkbox disabled />
<AvatarDataSkeleton />
</div>
</TableCell>
</TableRowSkeleton>
</TableLoaderSkeleton>
</div>
))}
</div>
);
};
interface UserRowProps<T extends SelectedUser> {
checked: boolean;
children?: ReactNode;
isFirst: boolean;
isLast: boolean;
onChange: (user: T, checked: boolean) => void;
user: T;
}
@@ -248,23 +246,40 @@ interface UserRowProps<T extends SelectedUser> {
const UserRow = <T extends SelectedUser>({
checked,
children,
isFirst,
isLast,
onChange,
user,
}: UserRowProps<T>) => {
const clickableProps = useClickableTableRow({
onClick: () => onChange(user, !checked),
});
return (
<TableRow
{...clickableProps}
<div
data-testid={`user-${user.username}`}
className={cn([
checked ? "bg-muted hover:bg-muted" : undefined,
clickableProps.className,
])}
tabIndex={-1}
className={cn(
"cursor-pointer",
"hover:[&>div]:ring-1 hover:[&>div]:ring-inset hover:[&>div]:ring-border-hover",
checked
? "[&>div]:bg-surface-secondary hover:[&>div]:bg-surface-secondary"
: undefined,
)}
onClick={() => onChange(user, !checked)}
onKeyDown={(event) => {
if (event.key === "Enter" || event.key === " ") {
event.preventDefault();
onChange(user, !checked);
}
}}
>
{children}
</TableRow>
<div
className={cn(
"flex min-h-[64px] items-center px-4 py-3",
isFirst && "rounded-tl-md rounded-tr-md",
isLast && "rounded-bl-md rounded-br-md",
)}
>
{children}
</div>
</div>
);
};
+60 -24
View File
@@ -14,7 +14,12 @@ import type {
import { Avatar } from "#/components/Avatar/Avatar";
import { AvatarData } from "#/components/Avatar/AvatarData";
import { Button } from "#/components/Button/Button";
import { ConfirmDialog } from "#/components/Dialogs/ConfirmDialog/ConfirmDialog";
import {
Dialog,
DialogContent,
DialogFooter,
DialogTitle,
} from "#/components/Dialog/Dialog";
import {
DropdownMenu,
DropdownMenuContent,
@@ -26,6 +31,7 @@ import { UsersFilter } from "#/components/Filter/UsersFilter";
import { LastSeen } from "#/components/LastSeen/LastSeen";
import { MultiMemberSelect } from "#/components/MultiUserSelect/MultiUserSelect";
import { PaginationContainer } from "#/components/PaginationWidget/PaginationContainer";
import { Spinner } from "#/components/Spinner/Spinner";
import {
Table,
TableBody,
@@ -134,17 +140,33 @@ const AddUsersDialog: FC<AddUsersDialogProps> = ({
const [selected, setSelected] = useState<OrganizationMemberWithUserData[]>(
[],
);
const closeDialog = () => {
setAddUserDialogOpen(false);
setFilter("");
setSelected([]);
};
return (
<>
<Button size="lg" onClick={() => setAddUserDialogOpen(true)}>
<UserPlusIcon />
Add users
</Button>
<ConfirmDialog
<Dialog
open={addUserDialogOpen}
title="Add users"
disabled={submitting}
description={
onOpenChange={(open) => {
if (!open) {
closeDialog();
}
}}
>
<DialogContent
data-testid="dialog"
className="max-w-md gap-4 border-border-default bg-surface-primary p-8 text-content-primary"
>
<DialogTitle className="font-semibold text-content-primary">
Add user(s)
</DialogTitle>
<MultiMemberSelect
organizationId={organizationId}
filter={filter}
@@ -158,25 +180,39 @@ const AddUsersDialog: FC<AddUsersDialogProps> = ({
}}
selected={selected}
/>
}
hideCancel={false}
cancelText="Cancel"
confirmText="Add users"
onClose={() => setAddUserDialogOpen(false)}
onConfirm={async () => {
try {
setSubmitting(true);
await onSubmit(selected);
setAddUserDialogOpen(false);
} catch (error) {
toast.error(getErrorMessage(error, "Failed to add members."), {
description: getErrorDetail(error),
});
} finally {
setSubmitting(false);
}
}}
/>
<DialogFooter className="mt-4 flex-row justify-end gap-3">
<Button
variant="outline"
onClick={closeDialog}
disabled={submitting}
>
Cancel
</Button>
<Button
disabled={submitting || selected.length === 0}
onClick={async () => {
try {
setSubmitting(true);
await onSubmit(selected);
closeDialog();
} catch (error) {
toast.error(
getErrorMessage(error, "Failed to add members."),
{
description: getErrorDetail(error),
},
);
} finally {
setSubmitting(false);
}
}}
>
<Spinner loading={submitting} />
Add users
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</>
);
};