Skip to content

Commit f30d894

Browse files
committed
chore: create separate sharing component with workspace sharing hook
1 parent 029ec0a commit f30d894

File tree

5 files changed

+506
-352
lines changed

5 files changed

+506
-352
lines changed
Lines changed: 309 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,309 @@
1+
import type {
2+
Group,
3+
WorkspaceACL,
4+
WorkspaceGroup,
5+
WorkspaceRole,
6+
WorkspaceUser,
7+
} from "api/typesGenerated";
8+
import { ErrorAlert } from "components/Alert/ErrorAlert";
9+
import { Avatar } from "components/Avatar/Avatar";
10+
import { AvatarData } from "components/Avatar/AvatarData";
11+
import { Button } from "components/Button/Button";
12+
import {
13+
DropdownMenu,
14+
DropdownMenuContent,
15+
DropdownMenuItem,
16+
DropdownMenuTrigger,
17+
} from "components/DropdownMenu/DropdownMenu";
18+
import { EmptyState } from "components/EmptyState/EmptyState";
19+
import {
20+
Select,
21+
SelectContent,
22+
SelectItem,
23+
SelectTrigger,
24+
SelectValue,
25+
} from "components/Select/Select";
26+
import { Spinner } from "components/Spinner/Spinner";
27+
import {
28+
Table,
29+
TableBody,
30+
TableCell,
31+
TableHead,
32+
TableHeader,
33+
TableRow,
34+
} from "components/Table/Table";
35+
import { TableLoader } from "components/TableLoader/TableLoader";
36+
import { EllipsisVertical, UserPlusIcon } from "lucide-react";
37+
import type { FC, ReactNode } from "react";
38+
import { getGroupSubtitle } from "utils/groups";
39+
40+
interface RoleSelectProps {
41+
value: WorkspaceRole;
42+
disabled?: boolean;
43+
onValueChange: (value: WorkspaceRole) => void;
44+
}
45+
46+
export const RoleSelect: FC<RoleSelectProps> = ({
47+
value,
48+
disabled,
49+
onValueChange,
50+
}) => {
51+
const roleLabels: Record<WorkspaceRole, string> = {
52+
use: "Use",
53+
admin: "Admin",
54+
"": "",
55+
};
56+
57+
return (
58+
<Select value={value} onValueChange={onValueChange} disabled={disabled}>
59+
<SelectTrigger className="w-40 h-auto">
60+
<SelectValue>
61+
<span className="bg-surface-secondary rounded-md px-3 py-0.5 inline-block">
62+
{roleLabels[value]}
63+
</span>
64+
</SelectValue>
65+
</SelectTrigger>
66+
<SelectContent>
67+
<SelectItem value="use" className="flex-col items-start py-2 w-64">
68+
<div className="font-medium text-content-primary">Use</div>
69+
<div className="text-xs text-content-secondary leading-snug mt-0.5">
70+
Can read and access this workspace.
71+
</div>
72+
</SelectItem>
73+
<SelectItem value="admin" className="flex-col items-start py-2 w-64">
74+
<div className="font-medium text-content-primary">Admin</div>
75+
<div className="text-xs text-content-secondary leading-snug mt-0.5">
76+
Can manage workspace metadata, permissions, and settings.
77+
</div>
78+
</SelectItem>
79+
</SelectContent>
80+
</Select>
81+
);
82+
};
83+
84+
type AddWorkspaceMemberFormProps = {
85+
isLoading: boolean;
86+
onSubmit: () => void;
87+
disabled: boolean;
88+
children: ReactNode;
89+
};
90+
91+
export const AddWorkspaceMemberForm: FC<AddWorkspaceMemberFormProps> = ({
92+
isLoading,
93+
onSubmit,
94+
disabled,
95+
children,
96+
}) => {
97+
return (
98+
<form
99+
onSubmit={(event) => {
100+
event.preventDefault();
101+
onSubmit();
102+
}}
103+
>
104+
<div className="flex flex-row items-center gap-2">
105+
{children}
106+
<Button disabled={disabled || isLoading} type="submit">
107+
<Spinner loading={isLoading}>
108+
<UserPlusIcon className="size-icon-sm" />
109+
</Spinner>
110+
Add member
111+
</Button>
112+
</div>
113+
</form>
114+
);
115+
};
116+
117+
type RoleSelectFieldProps = {
118+
value: WorkspaceRole;
119+
onChange: (value: WorkspaceRole) => void;
120+
disabled?: boolean;
121+
};
122+
123+
export const RoleSelectField: FC<RoleSelectFieldProps> = ({
124+
value,
125+
onChange,
126+
disabled,
127+
}) => {
128+
return (
129+
<Select
130+
value={value}
131+
onValueChange={(val: WorkspaceRole) => onChange(val)}
132+
disabled={disabled}
133+
>
134+
<SelectTrigger className="w-40">
135+
<SelectValue />
136+
</SelectTrigger>
137+
<SelectContent>
138+
<SelectItem value="use">Use</SelectItem>
139+
<SelectItem value="admin">Admin</SelectItem>
140+
</SelectContent>
141+
</Select>
142+
);
143+
};
144+
145+
export interface WorkspaceSharingFormProps {
146+
workspaceACL: WorkspaceACL | undefined;
147+
canUpdatePermissions: boolean;
148+
error: unknown;
149+
onUpdateUser: (user: WorkspaceUser, role: WorkspaceRole) => void;
150+
updatingUserId: WorkspaceUser["id"] | undefined;
151+
onRemoveUser: (user: WorkspaceUser) => void;
152+
onUpdateGroup: (group: WorkspaceGroup, role: WorkspaceRole) => void;
153+
updatingGroupId?: WorkspaceGroup["id"] | undefined;
154+
onRemoveGroup: (group: Group) => void;
155+
addMemberForm?: ReactNode;
156+
}
157+
158+
export const WorkspaceSharingForm: FC<WorkspaceSharingFormProps> = ({
159+
workspaceACL,
160+
canUpdatePermissions,
161+
error,
162+
updatingUserId,
163+
onUpdateUser,
164+
onRemoveUser,
165+
updatingGroupId,
166+
onUpdateGroup,
167+
onRemoveGroup,
168+
addMemberForm,
169+
}) => {
170+
const isEmpty = Boolean(
171+
workspaceACL &&
172+
workspaceACL.users.length === 0 &&
173+
workspaceACL.group.length === 0,
174+
);
175+
176+
return (
177+
<div className="flex flex-col gap-4">
178+
{Boolean(error) && <ErrorAlert error={error} />}
179+
{canUpdatePermissions && addMemberForm}
180+
<Table>
181+
<TableHeader>
182+
<TableRow>
183+
<TableHead className="w-[60%] py-2">Member</TableHead>
184+
<TableHead className="w-[40%] py-2">Role</TableHead>
185+
<TableHead className="w-[1%] py-2" />
186+
</TableRow>
187+
</TableHeader>
188+
<TableBody>
189+
{!workspaceACL ? (
190+
<TableLoader />
191+
) : isEmpty ? (
192+
<TableRow>
193+
<TableCell colSpan={999}>
194+
<EmptyState
195+
message="No shared members or groups yet"
196+
description="Add a member or group using the controls above"
197+
/>
198+
</TableCell>
199+
</TableRow>
200+
) : (
201+
<>
202+
{workspaceACL.group.map((group) => (
203+
<TableRow key={group.id}>
204+
<TableCell className="py-2">
205+
<AvatarData
206+
avatar={
207+
<Avatar
208+
size="lg"
209+
fallback={group.display_name || group.name}
210+
src={group.avatar_url}
211+
/>
212+
}
213+
title={group.display_name || group.name}
214+
subtitle={getGroupSubtitle(group)}
215+
/>
216+
</TableCell>
217+
<TableCell className="py-2">
218+
{canUpdatePermissions ? (
219+
<RoleSelect
220+
value={group.role}
221+
disabled={updatingGroupId === group.id}
222+
onValueChange={(value) => onUpdateGroup(group, value)}
223+
/>
224+
) : (
225+
<div className="capitalize">{group.role}</div>
226+
)}
227+
</TableCell>
228+
229+
<TableCell className="py-2">
230+
{canUpdatePermissions && (
231+
<DropdownMenu>
232+
<DropdownMenuTrigger asChild>
233+
<Button
234+
size="icon-lg"
235+
variant="subtle"
236+
aria-label="Open menu"
237+
>
238+
<EllipsisVertical aria-hidden="true" />
239+
<span className="sr-only">Open menu</span>
240+
</Button>
241+
</DropdownMenuTrigger>
242+
<DropdownMenuContent align="end">
243+
<DropdownMenuItem
244+
className="text-content-destructive focus:text-content-destructive"
245+
onClick={() => onRemoveGroup(group)}
246+
>
247+
Remove
248+
</DropdownMenuItem>
249+
</DropdownMenuContent>
250+
</DropdownMenu>
251+
)}
252+
</TableCell>
253+
</TableRow>
254+
))}
255+
256+
{workspaceACL.users.map((user) => (
257+
<TableRow key={user.id}>
258+
<TableCell className="py-2">
259+
<AvatarData
260+
title={user.username}
261+
subtitle={user.name}
262+
src={user.avatar_url}
263+
/>
264+
</TableCell>
265+
<TableCell className="py-2">
266+
{canUpdatePermissions ? (
267+
<RoleSelect
268+
value={user.role}
269+
disabled={updatingUserId === user.id}
270+
onValueChange={(value) => onUpdateUser(user, value)}
271+
/>
272+
) : (
273+
<div className="capitalize">{user.role}</div>
274+
)}
275+
</TableCell>
276+
277+
<TableCell className="py-2">
278+
{canUpdatePermissions && (
279+
<DropdownMenu>
280+
<DropdownMenuTrigger asChild>
281+
<Button
282+
size="icon-lg"
283+
variant="subtle"
284+
aria-label="Open menu"
285+
>
286+
<EllipsisVertical aria-hidden="true" />
287+
<span className="sr-only">Open menu</span>
288+
</Button>
289+
</DropdownMenuTrigger>
290+
<DropdownMenuContent align="end">
291+
<DropdownMenuItem
292+
className="text-content-destructive focus:text-content-destructive"
293+
onClick={() => onRemoveUser(user)}
294+
>
295+
Remove
296+
</DropdownMenuItem>
297+
</DropdownMenuContent>
298+
</DropdownMenu>
299+
)}
300+
</TableCell>
301+
</TableRow>
302+
))}
303+
</>
304+
)}
305+
</TableBody>
306+
</Table>
307+
</div>
308+
);
309+
};

0 commit comments

Comments
 (0)