Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit9f34a1d

Browse files
authored
chore: create Workspace sharing form component using workspace sharing hook (#21276)
This PR separate the data retrieval for workspace sharing ACL into acustom hook and creates a separate form component. This is inpreparation for reusing the workspace sharing form from a new sharebutton on the workspace page.
1 parentbd753d9 commit9f34a1d

File tree

5 files changed

+501
-352
lines changed

5 files changed

+501
-352
lines changed

‎site/src/modules/groups.ts‎

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,21 @@
1-
importtype{Group,ReducedUser,User}from"api/typesGenerated";
1+
importtype{
2+
Group,
3+
ReducedUser,
4+
User,
5+
WorkspaceUser,
6+
}from"api/typesGenerated";
27

3-
typeUserOrGroupAutocompleteValue=User|ReducedUser|Group|null;
8+
/**
9+
* Union of all user-like types that can be distinguished from Group.
10+
*/
11+
typeUserLike=User|ReducedUser|WorkspaceUser;
412

513
/**
614
* Type guard to check if the value is a Group.
715
* Groups have a "members" property that users don't have.
816
*/
9-
exportconstisGroup=(
10-
value:UserOrGroupAutocompleteValue,
11-
):value isGroup=>{
12-
returnvalue!==null&&typeofvalue==="object"&&"members"invalue;
17+
exportconstisGroup=(value:UserLike|Group):value isGroup=>{
18+
return"members"invalue;
1319
};
1420

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

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp