chore: create Workspace sharing form component using workspace sharing hook (#21276)

This PR separate the data retrieval for workspace sharing ACL into a
custom hook and creates a separate form component. This is in
preparation for reusing the workspace sharing form from a new share
button on the workspace page.
This commit is contained in:
Jaayden Halko
2025-12-17 16:39:27 +00:00
committed by GitHub
parent bd753d9cb9
commit 9f34a1dbad
5 changed files with 500 additions and 351 deletions
+12 -6
View File
@@ -1,15 +1,21 @@
import type { Group, ReducedUser, User } from "api/typesGenerated";
import type {
Group,
ReducedUser,
User,
WorkspaceUser,
} from "api/typesGenerated";
type UserOrGroupAutocompleteValue = User | ReducedUser | Group | null;
/**
* Union of all user-like types that can be distinguished from Group.
*/
type UserLike = User | ReducedUser | WorkspaceUser;
/**
* Type guard to check if the value is a Group.
* Groups have a "members" property that users don't have.
*/
export const isGroup = (
value: UserOrGroupAutocompleteValue,
): value is Group => {
return value !== null && typeof value === "object" && "members" in value;
export const isGroup = (value: UserLike | Group): value is Group => {
return "members" in value;
};
/**
@@ -0,0 +1,304 @@
import type {
Group,
WorkspaceACL,
WorkspaceGroup,
WorkspaceRole,
WorkspaceUser,
} from "api/typesGenerated";
import { ErrorAlert } from "components/Alert/ErrorAlert";
import { Avatar } from "components/Avatar/Avatar";
import { AvatarData } from "components/Avatar/AvatarData";
import { Button } from "components/Button/Button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "components/DropdownMenu/DropdownMenu";
import { EmptyState } from "components/EmptyState/EmptyState";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "components/Select/Select";
import { Spinner } from "components/Spinner/Spinner";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "components/Table/Table";
import { TableLoader } from "components/TableLoader/TableLoader";
import { EllipsisVertical, UserPlusIcon } from "lucide-react";
import { getGroupSubtitle } from "modules/groups";
import type { FC, ReactNode } from "react";
interface RoleSelectProps {
value: WorkspaceRole;
disabled?: boolean;
onValueChange: (value: WorkspaceRole) => void;
}
const RoleSelect: FC<RoleSelectProps> = ({
value,
disabled,
onValueChange,
}) => {
const roleLabels: Record<WorkspaceRole, string> = {
use: "Use",
admin: "Admin",
"": "",
};
return (
<Select value={value} onValueChange={onValueChange} disabled={disabled}>
<SelectTrigger className="w-40 h-auto">
<SelectValue>
<span className="bg-surface-secondary rounded-md px-3 py-0.5 inline-block">
{roleLabels[value]}
</span>
</SelectValue>
</SelectTrigger>
<SelectContent>
<SelectItem value="use" className="flex-col items-start py-2 w-64">
<div className="font-medium text-content-primary">Use</div>
<div className="text-xs text-content-secondary leading-snug mt-0.5">
Can read and access this workspace.
</div>
</SelectItem>
<SelectItem value="admin" className="flex-col items-start py-2 w-64">
<div className="font-medium text-content-primary">Admin</div>
<div className="text-xs text-content-secondary leading-snug mt-0.5">
Can manage workspace metadata, permissions, and settings.
</div>
</SelectItem>
</SelectContent>
</Select>
);
};
type AddWorkspaceMemberFormProps = {
isLoading: boolean;
onSubmit: () => void;
disabled: boolean;
children: ReactNode;
};
export const AddWorkspaceMemberForm: FC<AddWorkspaceMemberFormProps> = ({
isLoading,
onSubmit,
disabled,
children,
}) => {
return (
<form action={onSubmit}>
<div className="flex flex-row items-center gap-2">
{children}
<Button disabled={disabled || isLoading} type="submit">
<Spinner loading={isLoading}>
<UserPlusIcon className="size-icon-sm" />
</Spinner>
Add member
</Button>
</div>
</form>
);
};
type RoleSelectFieldProps = {
value: WorkspaceRole;
onChange: (value: WorkspaceRole) => void;
disabled?: boolean;
};
export const RoleSelectField: FC<RoleSelectFieldProps> = ({
value,
onChange,
disabled,
}) => {
return (
<Select
value={value}
onValueChange={(val: WorkspaceRole) => onChange(val)}
disabled={disabled}
>
<SelectTrigger className="w-40">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="use">Use</SelectItem>
<SelectItem value="admin">Admin</SelectItem>
</SelectContent>
</Select>
);
};
interface WorkspaceSharingFormProps {
workspaceACL: WorkspaceACL | undefined;
canUpdatePermissions: boolean;
error: unknown;
onUpdateUser: (user: WorkspaceUser, role: WorkspaceRole) => void;
updatingUserId: WorkspaceUser["id"] | undefined;
onRemoveUser: (user: WorkspaceUser) => void;
onUpdateGroup: (group: WorkspaceGroup, role: WorkspaceRole) => void;
updatingGroupId?: WorkspaceGroup["id"] | undefined;
onRemoveGroup: (group: Group) => void;
addMemberForm?: ReactNode;
}
export const WorkspaceSharingForm: FC<WorkspaceSharingFormProps> = ({
workspaceACL,
canUpdatePermissions,
error,
updatingUserId,
onUpdateUser,
onRemoveUser,
updatingGroupId,
onUpdateGroup,
onRemoveGroup,
addMemberForm,
}) => {
const isEmpty = Boolean(
workspaceACL &&
workspaceACL.users.length === 0 &&
workspaceACL.group.length === 0,
);
return (
<div className="flex flex-col gap-4">
{Boolean(error) && <ErrorAlert error={error} />}
{canUpdatePermissions && addMemberForm}
<Table>
<TableHeader>
<TableRow>
<TableHead className="w-[60%] py-2">Member</TableHead>
<TableHead className="w-[40%] py-2">Role</TableHead>
<TableHead className="w-[1%] py-2" />
</TableRow>
</TableHeader>
<TableBody>
{!workspaceACL ? (
<TableLoader />
) : isEmpty ? (
<TableRow>
<TableCell colSpan={999}>
<EmptyState
message="No shared members or groups yet"
description="Add a member or group using the controls above"
/>
</TableCell>
</TableRow>
) : (
<>
{workspaceACL.group.map((group) => (
<TableRow key={group.id}>
<TableCell className="py-2">
<AvatarData
avatar={
<Avatar
size="lg"
fallback={group.display_name || group.name}
src={group.avatar_url}
/>
}
title={group.display_name || group.name}
subtitle={getGroupSubtitle(group)}
/>
</TableCell>
<TableCell className="py-2">
{canUpdatePermissions ? (
<RoleSelect
value={group.role}
disabled={updatingGroupId === group.id}
onValueChange={(value) => onUpdateGroup(group, value)}
/>
) : (
<div className="capitalize">{group.role}</div>
)}
</TableCell>
<TableCell className="py-2">
{canUpdatePermissions && (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
size="icon-lg"
variant="subtle"
aria-label="Open menu"
>
<EllipsisVertical aria-hidden="true" />
<span className="sr-only">Open menu</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem
className="text-content-destructive focus:text-content-destructive"
onClick={() => onRemoveGroup(group)}
>
Remove
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)}
</TableCell>
</TableRow>
))}
{workspaceACL.users.map((user) => (
<TableRow key={user.id}>
<TableCell className="py-2">
<AvatarData
title={user.username}
subtitle={user.name}
src={user.avatar_url}
/>
</TableCell>
<TableCell className="py-2">
{canUpdatePermissions ? (
<RoleSelect
value={user.role}
disabled={updatingUserId === user.id}
onValueChange={(value) => onUpdateUser(user, value)}
/>
) : (
<div className="capitalize">{user.role}</div>
)}
</TableCell>
<TableCell className="py-2">
{canUpdatePermissions && (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
size="icon-lg"
variant="subtle"
aria-label="Open menu"
>
<EllipsisVertical aria-hidden="true" />
<span className="sr-only">Open menu</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem
className="text-content-destructive focus:text-content-destructive"
onClick={() => onRemoveUser(user)}
>
Remove
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)}
</TableCell>
</TableRow>
))}
</>
)}
</TableBody>
</Table>
</div>
);
};
@@ -0,0 +1,128 @@
import {
setWorkspaceGroupRole,
setWorkspaceUserRole,
workspaceACL,
} from "api/queries/workspaces";
import type {
Group,
Workspace,
WorkspaceGroup,
WorkspaceRole,
WorkspaceUser,
} from "api/typesGenerated";
import { displaySuccess } from "components/GlobalSnackbar/utils";
import { useMutation, useQuery, useQueryClient } from "react-query";
/**
* Encapsulates all data fetching and mutations for workspace sharing.
* This hook manages the workspace ACL query and provides methods to
* add, update, and remove users and groups from the workspace.
*/
export function useWorkspaceSharing(workspace: Workspace) {
const queryClient = useQueryClient();
const workspaceACLQuery = useQuery(workspaceACL(workspace.id));
const addUserMutation = useMutation(setWorkspaceUserRole(queryClient));
const updateUserMutation = useMutation(setWorkspaceUserRole(queryClient));
const removeUserMutation = useMutation(setWorkspaceUserRole(queryClient));
const addGroupMutation = useMutation(setWorkspaceGroupRole(queryClient));
const updateGroupMutation = useMutation(setWorkspaceGroupRole(queryClient));
const removeGroupMutation = useMutation(setWorkspaceGroupRole(queryClient));
const addUser = async (
user: WorkspaceUser,
role: WorkspaceRole,
reset: () => void,
) => {
await addUserMutation.mutateAsync({
workspaceId: workspace.id,
userId: user.id,
role,
});
displaySuccess("User added to workspace successfully!");
reset();
};
const updateUser = async (user: WorkspaceUser, role: WorkspaceRole) => {
await updateUserMutation.mutateAsync({
workspaceId: workspace.id,
userId: user.id,
role,
});
displaySuccess("User role updated successfully!");
};
const removeUser = async (user: WorkspaceUser) => {
await removeUserMutation.mutateAsync({
workspaceId: workspace.id,
userId: user.id,
role: "",
});
displaySuccess("User removed successfully!");
};
const addGroup = async (
group: Group,
role: WorkspaceRole,
reset: () => void,
) => {
await addGroupMutation.mutateAsync({
workspaceId: workspace.id,
groupId: group.id,
role,
});
displaySuccess("Group added to workspace successfully!");
reset();
};
const updateGroup = async (group: WorkspaceGroup, role: WorkspaceRole) => {
await updateGroupMutation.mutateAsync({
workspaceId: workspace.id,
groupId: group.id,
role,
});
displaySuccess("Group role updated successfully!");
};
const removeGroup = async (group: Group) => {
await removeGroupMutation.mutateAsync({
workspaceId: workspace.id,
groupId: group.id,
role: "",
});
displaySuccess("Group removed successfully!");
};
const mutationError =
addUserMutation.error ??
updateUserMutation.error ??
removeUserMutation.error ??
addGroupMutation.error ??
updateGroupMutation.error ??
removeGroupMutation.error;
return {
workspaceACL: workspaceACLQuery.data,
isLoading: workspaceACLQuery.isLoading,
error: workspaceACLQuery.error,
mutationError,
// User actions
addUser,
updateUser,
removeUser,
isAddingUser: addUserMutation.isPending,
updatingUserId: updateUserMutation.isPending
? updateUserMutation.variables?.userId
: undefined,
// Group actions
addGroup,
updateGroup,
removeGroup,
isAddingGroup: addGroupMutation.isPending,
updatingGroupId: updateGroupMutation.isPending
? updateGroupMutation.variables?.groupId
: undefined,
} as const;
}
@@ -1,16 +1,10 @@
import { checkAuthorization } from "api/queries/authCheck";
import {
setWorkspaceGroupRole,
setWorkspaceUserRole,
workspaceACL,
} from "api/queries/workspaces";
import { ErrorAlert } from "components/Alert/ErrorAlert";
import { displaySuccess } from "components/GlobalSnackbar/utils";
import { Link } from "components/Link/Link";
import type { WorkspacePermissions } from "modules/workspaces/permissions";
import { workspaceChecks } from "modules/workspaces/permissions";
import { useWorkspaceSharing } from "modules/workspaces/WorkspaceSharingForm/useWorkspaceSharing";
import type { FC } from "react";
import { useMutation, useQuery, useQueryClient } from "react-query";
import { useQuery } from "react-query";
import { docs } from "utils/docs";
import { pageTitle } from "utils/page";
import { useWorkspaceSettings } from "../WorkspaceSettingsLayout";
@@ -18,32 +12,17 @@ import { WorkspaceSharingPageView } from "./WorkspaceSharingPageView";
const WorkspaceSharingPage: FC = () => {
const workspace = useWorkspaceSettings();
const queryClient = useQueryClient();
const sharing = useWorkspaceSharing(workspace);
const workspaceACLQuery = useQuery(workspaceACL(workspace.id));
const checks = workspaceChecks(workspace);
const permissionsQuery = useQuery<WorkspacePermissions>({
...checkAuthorization({ checks }),
});
const permissions = permissionsQuery.data;
const addUserMutation = useMutation(setWorkspaceUserRole(queryClient));
const updateUserMutation = useMutation(setWorkspaceUserRole(queryClient));
const removeUserMutation = useMutation(setWorkspaceUserRole(queryClient));
const addGroupMutation = useMutation(setWorkspaceGroupRole(queryClient));
const updateGroupMutation = useMutation(setWorkspaceGroupRole(queryClient));
const removeGroupMutation = useMutation(setWorkspaceGroupRole(queryClient));
const canUpdatePermissions = Boolean(permissions?.updateWorkspace);
const mutationError =
addUserMutation.error ??
updateUserMutation.error ??
removeUserMutation.error ??
addGroupMutation.error ??
updateGroupMutation.error ??
removeGroupMutation.error;
const error =
sharing.error ?? permissionsQuery.error ?? sharing.mutationError;
return (
<div className="flex flex-col gap-12 max-w-screen-md">
@@ -60,80 +39,21 @@ const WorkspaceSharingPage: FC = () => {
</div>
</header>
{workspaceACLQuery.isError && (
<ErrorAlert error={workspaceACLQuery.error} />
)}
{permissionsQuery.isError && (
<ErrorAlert error={permissionsQuery.error} />
)}
{Boolean(mutationError) && <ErrorAlert error={mutationError} />}
<WorkspaceSharingPageView
workspace={workspace}
workspaceACL={workspaceACLQuery.data}
workspaceACL={sharing.workspaceACL}
canUpdatePermissions={canUpdatePermissions}
onAddUser={async (user, role, reset) => {
await addUserMutation.mutateAsync({
workspaceId: workspace.id,
userId: user.id,
role,
});
displaySuccess("User added to workspace successfully!");
reset();
}}
isAddingUser={addUserMutation.isPending}
onUpdateUser={async (user, role) => {
await updateUserMutation.mutateAsync({
workspaceId: workspace.id,
userId: user.id,
role,
});
displaySuccess("User role updated successfully!");
}}
updatingUserId={
updateUserMutation.isPending
? updateUserMutation.variables?.userId
: undefined
}
onRemoveUser={async (user) => {
await removeUserMutation.mutateAsync({
workspaceId: workspace.id,
userId: user.id,
role: "",
});
displaySuccess("User removed successfully!");
}}
onAddGroup={async (group, role, reset) => {
await addGroupMutation.mutateAsync({
workspaceId: workspace.id,
groupId: group.id,
role,
});
displaySuccess("Group added to workspace successfully!");
reset();
}}
isAddingGroup={addGroupMutation.isPending}
onUpdateGroup={async (group, role) => {
await updateGroupMutation.mutateAsync({
workspaceId: workspace.id,
groupId: group.id,
role,
});
displaySuccess("Group role updated successfully!");
}}
updatingGroupId={
updateGroupMutation.isPending
? updateGroupMutation.variables?.groupId
: undefined
}
onRemoveGroup={async (group) => {
await removeGroupMutation.mutateAsync({
workspaceId: workspace.id,
groupId: group.id,
role: "",
});
displaySuccess("Group removed successfully!");
}}
error={error}
onAddUser={sharing.addUser}
isAddingUser={sharing.isAddingUser}
onUpdateUser={sharing.updateUser}
updatingUserId={sharing.updatingUserId}
onRemoveUser={sharing.removeUser}
onAddGroup={sharing.addGroup}
isAddingGroup={sharing.isAddingGroup}
onUpdateGroup={sharing.updateGroup}
updatingGroupId={sharing.updatingGroupId}
onRemoveGroup={sharing.removeGroup}
/>
</div>
);
@@ -7,35 +7,12 @@ import type {
WorkspaceRole,
WorkspaceUser,
} from "api/typesGenerated";
import { Avatar } from "components/Avatar/Avatar";
import { AvatarData } from "components/Avatar/AvatarData";
import { Button } from "components/Button/Button";
import { isGroup } from "modules/groups";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "components/DropdownMenu/DropdownMenu";
import { EmptyState } from "components/EmptyState/EmptyState";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "components/Select/Select";
import { Spinner } from "components/Spinner/Spinner";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "components/Table/Table";
import { TableLoader } from "components/TableLoader/TableLoader";
import { EllipsisVertical, UserPlusIcon } from "lucide-react";
import { getGroupSubtitle } from "modules/groups";
AddWorkspaceMemberForm,
RoleSelectField,
WorkspaceSharingForm,
} from "modules/workspaces/WorkspaceSharingForm/WorkspaceSharingForm";
import { type FC, useState } from "react";
import {
UserOrGroupAutocomplete,
@@ -72,10 +49,10 @@ const AddWorkspaceUserOrGroup: FC<AddWorkspaceUserOrGroupProps> = ({
};
return (
<form
onSubmit={(event) => {
event.preventDefault();
<AddWorkspaceMemberForm
isLoading={isLoading}
disabled={!selectedRole || !selectedOption}
onSubmit={() => {
if (selectedOption && selectedRole) {
onSubmit(
{
@@ -88,85 +65,21 @@ const AddWorkspaceUserOrGroup: FC<AddWorkspaceUserOrGroupProps> = ({
}
}}
>
<div className="flex flex-row items-center gap-2">
<UserOrGroupAutocomplete
organizationId={organizationID}
value={selectedOption}
exclude={excludeFromAutocomplete}
onChange={(newValue) => {
setSelectedOption(newValue);
}}
/>
<UserOrGroupAutocomplete
organizationId={organizationID}
value={selectedOption}
exclude={excludeFromAutocomplete}
onChange={(newValue) => {
setSelectedOption(newValue);
}}
/>
<Select
value={selectedRole}
onValueChange={(value: WorkspaceRole) => setSelectedRole(value)}
disabled={isLoading}
>
<SelectTrigger className="w-40">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="use">Use</SelectItem>
<SelectItem value="admin">Admin</SelectItem>
</SelectContent>
</Select>
<Button
disabled={!selectedRole || !selectedOption || isLoading}
type="submit"
>
<Spinner loading={isLoading}>
<UserPlusIcon className="size-icon-sm" />
</Spinner>
Add member
</Button>
</div>
</form>
);
};
interface RoleSelectProps {
value: WorkspaceRole;
disabled?: boolean;
onValueChange: (value: WorkspaceRole) => void;
}
const RoleSelect: FC<RoleSelectProps> = ({
value,
disabled,
onValueChange,
}) => {
const roleLabels: Record<WorkspaceRole, string> = {
use: "Use",
admin: "Admin",
"": "",
};
return (
<Select value={value} onValueChange={onValueChange} disabled={disabled}>
<SelectTrigger className="w-40 h-auto">
<SelectValue>
<span className="bg-surface-secondary rounded-md px-3 py-0.5 inline-block">
{roleLabels[value]}
</span>
</SelectValue>
</SelectTrigger>
<SelectContent>
<SelectItem value="use" className="flex-col items-start py-2 w-64">
<div className="font-medium text-content-primary">Use</div>
<div className="text-xs text-content-secondary leading-snug mt-0.5">
Can read and access this workspace.
</div>
</SelectItem>
<SelectItem value="admin" className="flex-col items-start py-2 w-64">
<div className="font-medium text-content-primary">Admin</div>
<div className="text-xs text-content-secondary leading-snug mt-0.5">
Can manage workspace metadata, permissions, and settings.
</div>
</SelectItem>
</SelectContent>
</Select>
<RoleSelectField
value={selectedRole}
onChange={setSelectedRole}
disabled={isLoading}
/>
</AddWorkspaceMemberForm>
);
};
@@ -174,6 +87,7 @@ interface WorkspaceSharingPageViewProps {
workspace: Workspace;
workspaceACL: WorkspaceACL | undefined;
canUpdatePermissions: boolean;
error: unknown;
onAddUser: (
user: WorkspaceUser,
role: WorkspaceRole,
@@ -194,6 +108,7 @@ export const WorkspaceSharingPageView: FC<WorkspaceSharingPageViewProps> = ({
workspace,
workspaceACL,
canUpdatePermissions,
error,
onAddUser,
isAddingUser,
updatingUserId,
@@ -205,153 +120,29 @@ export const WorkspaceSharingPageView: FC<WorkspaceSharingPageViewProps> = ({
onUpdateGroup,
onRemoveGroup,
}) => {
const isEmpty = Boolean(
workspaceACL &&
workspaceACL.users.length === 0 &&
workspaceACL.group.length === 0,
);
return (
<div className="flex flex-col gap-4">
{canUpdatePermissions && (
<WorkspaceSharingForm
workspaceACL={workspaceACL}
canUpdatePermissions={canUpdatePermissions}
error={error}
updatingUserId={updatingUserId}
onUpdateUser={onUpdateUser}
onRemoveUser={onRemoveUser}
updatingGroupId={updatingGroupId}
onUpdateGroup={onUpdateGroup}
onRemoveGroup={onRemoveGroup}
addMemberForm={
<AddWorkspaceUserOrGroup
organizationID={workspace.organization_id}
workspaceACL={workspaceACL}
isLoading={isAddingUser || isAddingGroup}
onSubmit={(value, role, resetAutocomplete) =>
"members" in value
isGroup(value)
? onAddGroup(value, role, resetAutocomplete)
: onAddUser(value, role, resetAutocomplete)
}
/>
)}
<Table>
<TableHeader>
<TableRow>
<TableHead className="w-[60%] py-2">Member</TableHead>
<TableHead className="w-[40%] py-2">Role</TableHead>
<TableHead className="w-[1%] py-2" />
</TableRow>
</TableHeader>
<TableBody>
{!workspaceACL ? (
<TableLoader />
) : isEmpty ? (
<TableRow>
<TableCell colSpan={999}>
<EmptyState
message="No shared members or groups yet"
description="Add a member or group using the controls above"
/>
</TableCell>
</TableRow>
) : (
<>
{workspaceACL.group.map((group) => (
<TableRow key={group.id}>
<TableCell className="py-2">
<AvatarData
avatar={
<Avatar
size="lg"
fallback={group.display_name || group.name}
src={group.avatar_url}
/>
}
title={group.display_name || group.name}
subtitle={getGroupSubtitle(group)}
/>
</TableCell>
<TableCell className="py-2">
{canUpdatePermissions ? (
<RoleSelect
value={group.role}
disabled={updatingGroupId === group.id}
onValueChange={(value) => onUpdateGroup(group, value)}
/>
) : (
<div className="capitalize">{group.role}</div>
)}
</TableCell>
<TableCell className="py-2">
{canUpdatePermissions && (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
size="icon-lg"
variant="subtle"
aria-label="Open menu"
>
<EllipsisVertical aria-hidden="true" />
<span className="sr-only">Open menu</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem
className="text-content-destructive focus:text-content-destructive"
onClick={() => onRemoveGroup(group)}
>
Remove
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)}
</TableCell>
</TableRow>
))}
{workspaceACL.users.map((user) => (
<TableRow key={user.id}>
<TableCell className="py-2">
<AvatarData
title={user.username}
subtitle={user.name}
src={user.avatar_url}
/>
</TableCell>
<TableCell className="py-2">
{canUpdatePermissions ? (
<RoleSelect
value={user.role}
disabled={updatingUserId === user.id}
onValueChange={(value) => onUpdateUser(user, value)}
/>
) : (
<div className="capitalize">{user.role}</div>
)}
</TableCell>
<TableCell className="py-2">
{canUpdatePermissions && (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
size="icon-lg"
variant="subtle"
aria-label="Open menu"
>
<EllipsisVertical aria-hidden="true" />
<span className="sr-only">Open menu</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem
className="text-content-destructive focus:text-content-destructive"
onClick={() => onRemoveUser(user)}
>
Remove
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)}
</TableCell>
</TableRow>
))}
</>
)}
</TableBody>
</Table>
</div>
}
/>
);
};