1
- import type { Interpolation , Theme } from "@emotion/react" ;
2
- import PersonAdd from "@mui/icons-material/PersonAdd" ;
3
- import LoadingButton from "@mui/lab/LoadingButton" ;
4
- import Table from "@mui/material/Table" ;
5
- import TableBody from "@mui/material/TableBody" ;
6
- import TableCell from "@mui/material/TableCell" ;
7
- import TableContainer from "@mui/material/TableContainer" ;
8
- import TableHead from "@mui/material/TableHead" ;
9
- import TableRow from "@mui/material/TableRow" ;
10
- import { type FC , useState } from "react" ;
1
+ import type { FC } from "react" ;
11
2
import { useMutation , useQuery , useQueryClient } from "react-query" ;
12
3
import { useParams } from "react-router-dom" ;
13
- import { getErrorMessage } from "api/errors" ;
14
4
import {
15
5
addOrganizationMember ,
16
6
organizationMembers ,
@@ -19,26 +9,11 @@ import {
19
9
updateOrganizationMemberRoles ,
20
10
} from "api/queries/organizations" ;
21
11
import { organizationRoles } from "api/queries/roles" ;
22
- import type { User } from "api/typesGenerated" ;
23
- import { ErrorAlert } from "components/Alert/ErrorAlert" ;
24
- import { AvatarData } from "components/AvatarData/AvatarData" ;
25
- import { displayError , displaySuccess } from "components/GlobalSnackbar/utils" ;
12
+ import type { OrganizationMemberWithUserData , User } from "api/typesGenerated" ;
26
13
import { Loader } from "components/Loader/Loader" ;
27
- import {
28
- MoreMenu ,
29
- MoreMenuTrigger ,
30
- MoreMenuContent ,
31
- MoreMenuItem ,
32
- ThreeDotsButton ,
33
- } from "components/MoreMenu/MoreMenu" ;
34
- import { PageHeader , PageHeaderTitle } from "components/PageHeader/PageHeader" ;
35
- import { Stack } from "components/Stack/Stack" ;
36
- import { UserAutocomplete } from "components/UserAutocomplete/UserAutocomplete" ;
37
- import { UserAvatar } from "components/UserAvatar/UserAvatar" ;
38
14
import { useAuthenticated } from "contexts/auth/RequireAuth" ;
39
15
import { useOrganizationSettings } from "./ManagementSettingsLayout" ;
40
- import { TableColumnHelpTooltip } from "./UserTable/TableColumnHelpTooltip" ;
41
- import { UserRoleCell } from "./UserTable/UserRoleCell" ;
16
+ import { OrganizationMembersPageView } from "./OrganizationMembersPageView" ;
42
17
43
18
const OrganizationMembersPage :FC = ( ) => {
44
19
const queryClient = useQueryClient ( ) ;
@@ -71,182 +46,39 @@ const OrganizationMembersPage: FC = () => {
71
46
return < Loader /> ;
72
47
}
73
48
74
- const error =
75
- membersQuery . error ?? addMemberMutation . error ?? removeMemberMutation . error ;
76
- const members = membersQuery . data ;
77
-
78
- return (
79
- < div >
80
- < PageHeader >
81
- < PageHeaderTitle > Organization members</ PageHeaderTitle >
82
- </ PageHeader >
83
-
84
- < Stack >
85
- { Boolean ( error ) && < ErrorAlert error = { error } /> }
86
-
87
- { permissions . editMembers && (
88
- < AddOrganizationMember
89
- isLoading = { addMemberMutation . isLoading }
90
- onSubmit = { async ( user ) => {
91
- await addMemberMutation . mutateAsync ( user . id ) ;
92
- void membersQuery . refetch ( ) ;
93
- } }
94
- />
95
- ) }
96
-
97
- < TableContainer >
98
- < Table >
99
- < TableHead >
100
- < TableRow >
101
- < TableCell width = "50%" > User</ TableCell >
102
- < TableCell width = "49%" >
103
- < Stack direction = "row" spacing = { 1 } alignItems = "center" >
104
- < span > Roles</ span >
105
- < TableColumnHelpTooltip variant = "roles" />
106
- </ Stack >
107
- </ TableCell >
108
- < TableCell width = "1%" > </ TableCell >
109
- </ TableRow >
110
- </ TableHead >
111
- < TableBody >
112
- { members ?. map ( ( member ) => (
113
- < TableRow key = { member . user_id } >
114
- < TableCell >
115
- < AvatarData
116
- avatar = {
117
- < UserAvatar
118
- username = { member . username }
119
- avatarURL = { member . avatar_url }
120
- />
121
- }
122
- title = { member . name || member . username }
123
- subtitle = { member . email }
124
- />
125
- </ TableCell >
126
- < UserRoleCell
127
- inheritedRoles = { member . global_roles }
128
- roles = { member . roles }
129
- allAvailableRoles = { organizationRolesQuery . data }
130
- oidcRoleSyncEnabled = { false }
131
- isLoading = { updateMemberRolesMutation . isLoading }
132
- canEditUsers = { permissions . editMembers }
133
- onEditRoles = { async ( newRoleNames ) => {
134
- try {
135
- await updateMemberRolesMutation . mutateAsync ( {
136
- userId :member . user_id ,
137
- roles :newRoleNames ,
138
- } ) ;
139
- displaySuccess ( "Roles updated successfully." ) ;
140
- } catch ( e ) {
141
- displayError (
142
- getErrorMessage ( e , "Failed to update roles." ) ,
143
- ) ;
144
- }
145
- } }
146
- />
147
- < TableCell >
148
- { member . user_id !== me . id && permissions . editMembers && (
149
- < MoreMenu >
150
- < MoreMenuTrigger >
151
- < ThreeDotsButton />
152
- </ MoreMenuTrigger >
153
- < MoreMenuContent >
154
- < MoreMenuItem
155
- danger
156
- onClick = { async ( ) => {
157
- try {
158
- await removeMemberMutation . mutateAsync (
159
- member . user_id ,
160
- ) ;
161
- void membersQuery . refetch ( ) ;
162
- displaySuccess ( "Member removed." ) ;
163
- } catch ( e ) {
164
- displayError (
165
- getErrorMessage (
166
- e ,
167
- "Failed to remove member." ,
168
- ) ,
169
- ) ;
170
- }
171
- } }
172
- >
173
- Remove
174
- </ MoreMenuItem >
175
- </ MoreMenuContent >
176
- </ MoreMenu >
177
- ) }
178
- </ TableCell >
179
- </ TableRow >
180
- ) ) }
181
- </ TableBody >
182
- </ Table >
183
- </ TableContainer >
184
- </ Stack >
185
- </ div >
186
- ) ;
187
- } ;
188
-
189
- export default OrganizationMembersPage ;
190
-
191
- interface AddOrganizationMemberProps {
192
- isLoading :boolean ;
193
- onSubmit :( user :User ) => Promise < void > ;
194
- }
195
-
196
- const AddOrganizationMember :FC < AddOrganizationMemberProps > = ( {
197
- isLoading,
198
- onSubmit,
199
- } ) => {
200
- const [ selectedUser , setSelectedUser ] = useState < User | null > ( null ) ;
201
-
202
49
return (
203
- < form
204
- onSubmit = { async ( e ) => {
205
- e . preventDefault ( ) ;
206
-
207
- if ( selectedUser ) {
208
- try {
209
- await onSubmit ( selectedUser ) ;
210
- setSelectedUser ( null ) ;
211
- } catch ( error ) {
212
- displayError ( getErrorMessage ( error , "Failed to add member." ) ) ;
213
- }
214
- }
50
+ < OrganizationMembersPageView
51
+ allAvailableRoles = { organizationRolesQuery . data }
52
+ canEditMembers = { permissions . editMembers }
53
+ error = {
54
+ membersQuery . error ??
55
+ addMemberMutation . error ??
56
+ removeMemberMutation . error ??
57
+ updateMemberRolesMutation . error
58
+ }
59
+ isAddingMember = { addMemberMutation . isLoading }
60
+ isUpdatingMemberRoles = { updateMemberRolesMutation . isLoading }
61
+ me = { me }
62
+ members = { membersQuery . data }
63
+ addMember = { async ( user :User ) => {
64
+ await addMemberMutation . mutateAsync ( user . id ) ;
65
+ void membersQuery . refetch ( ) ;
215
66
} }
216
- >
217
- < Stack direction = "row" alignItems = "center" spacing = { 1 } >
218
- < UserAutocomplete
219
- css = { styles . autoComplete }
220
- value = { selectedUser }
221
- onChange = { ( newValue ) => {
222
- setSelectedUser ( newValue ) ;
223
- } }
224
- />
225
-
226
- < LoadingButton
227
- loadingPosition = "start"
228
- disabled = { ! selectedUser }
229
- type = "submit"
230
- startIcon = { < PersonAdd /> }
231
- loading = { isLoading }
232
- >
233
- Add user
234
- </ LoadingButton >
235
- </ Stack >
236
- </ form >
67
+ removeMember = { async ( member :OrganizationMemberWithUserData ) => {
68
+ await removeMemberMutation . mutateAsync ( member . user_id ) ;
69
+ void membersQuery . refetch ( ) ;
70
+ } }
71
+ updateMemberRoles = { async (
72
+ member :OrganizationMemberWithUserData ,
73
+ newRoles :string [ ] ,
74
+ ) => {
75
+ await updateMemberRolesMutation . mutateAsync ( {
76
+ userId :member . user_id ,
77
+ roles :newRoles ,
78
+ } ) ;
79
+ } }
80
+ />
237
81
) ;
238
82
} ;
239
83
240
- const styles = {
241
- role :( theme ) => ( {
242
- backgroundColor :theme . roles . info . background ,
243
- borderColor :theme . roles . info . outline ,
244
- } ) ,
245
- globalRole :( theme ) => ( {
246
- backgroundColor :theme . roles . inactive . background ,
247
- borderColor :theme . roles . inactive . outline ,
248
- } ) ,
249
- autoComplete :{
250
- width :300 ,
251
- } ,
252
- } satisfies Record < string , Interpolation < Theme > > ;
84
+ export default OrganizationMembersPage ;