1- import React , { useEffect , useState } from 'react' ;
1+ import React , { useEffect , useMemo , useState } from 'react' ;
22import { useFieldArray , useForm } from 'react-hook-form' ;
33import { useTranslation } from 'react-i18next' ;
4+ import { useNavigate } from 'react-router-dom' ;
45
5- import { Button , FormSelect , Header , Link , ListEmptyMessage , Pagination , Table } from 'components' ;
6+ import { Button , FormSelect , Header , Link , ListEmptyMessage , Pagination , SpaceBetween , Table } from 'components' ;
67
7- import { useCollection } from 'hooks' ;
8+ import { useAppSelector , useCollection , useNotifications } from 'hooks' ;
9+ import { selectUserData } from 'App/slice' ;
810import { ROUTES } from 'routes' ;
911import { useGetUserListQuery } from 'services/user' ;
12+ import { useAddProjectMemberMutation , useRemoveProjectMemberMutation } from 'services/project' ;
1013
1114import { UserAutosuggest } from './UsersAutosuggest' ;
1215
@@ -16,10 +19,15 @@ import { TRoleSelectOption } from 'pages/User/Form/types';
1619
1720import styles from './styles.module.scss' ;
1821
19- export const ProjectMembers : React . FC < IProps > = ( { members, loading, onChange, readonly, isAdmin } ) => {
22+ export const ProjectMembers : React . FC < IProps > = ( { members, loading, onChange, readonly, isAdmin, project } ) => {
2023 const { t } = useTranslation ( ) ;
24+ const navigate = useNavigate ( ) ;
25+ const [ pushNotification ] = useNotifications ( ) ;
2126 const [ selectedItems , setSelectedItems ] = useState < TProjectMemberWithIndex [ ] > ( [ ] ) ;
2227 const { data : usersData } = useGetUserListQuery ( ) ;
28+ const userData = useAppSelector ( selectUserData ) ;
29+ const [ addMember , { isLoading : isAdding } ] = useAddProjectMemberMutation ( ) ;
30+ const [ removeMember , { isLoading : isRemoving } ] = useRemoveProjectMemberMutation ( ) ;
2331
2432 const { handleSubmit, control, getValues, setValue } = useForm < TFormValues > ( {
2533 defaultValues : { members : members ?? [ ] } ,
@@ -30,6 +38,67 @@ export const ProjectMembers: React.FC<IProps> = ({ members, loading, onChange, r
3038 name : 'members' ,
3139 } ) ;
3240
41+ const currentUserRole = useMemo ( ( ) => {
42+ if ( ! userData ?. username ) return null ;
43+ const member = members ?. find ( m => m . user . username === userData . username ) ;
44+ return member ?. project_role || null ;
45+ } , [ members , userData ?. username ] ) ;
46+
47+ const isProjectOwner = useMemo ( ( ) => {
48+ return userData ?. username === project ?. owner . username ;
49+ } , [ userData ?. username , project ?. owner . username ] ) ;
50+
51+ const isMember = currentUserRole !== null ;
52+ const isMemberActionLoading = isAdding || isRemoving ;
53+
54+ const handleJoinProject = async ( ) => {
55+ if ( ! userData ?. username || ! project ) return ;
56+
57+ try {
58+ await addMember ( {
59+ project_name : project . project_name ,
60+ username : userData . username ,
61+ project_role : 'user' ,
62+ } ) . unwrap ( ) ;
63+
64+ pushNotification ( {
65+ type : 'success' ,
66+ content : t ( 'projects.join_success' ) ,
67+ } ) ;
68+ } catch ( error ) {
69+ console . error ( 'Failed to join project:' , error ) ;
70+ pushNotification ( {
71+ type : 'error' ,
72+ content : t ( 'projects.join_error' ) ,
73+ } ) ;
74+ }
75+ } ;
76+
77+ const handleLeaveProject = async ( ) => {
78+ if ( ! userData ?. username || ! project ) return ;
79+
80+ try {
81+ await removeMember ( {
82+ project_name : project . project_name ,
83+ username : userData . username ,
84+ } ) . unwrap ( ) ;
85+
86+ pushNotification ( {
87+ type : 'success' ,
88+ content : t ( 'projects.leave_success' ) ,
89+ } ) ;
90+
91+ // Redirect to project list after successfully leaving
92+ navigate ( ROUTES . PROJECT . LIST ) ;
93+ } catch ( error ) {
94+ console . error ( 'Failed to leave project:' , error ) ;
95+ pushNotification ( {
96+ type : 'error' ,
97+ content : t ( 'projects.leave_error' ) ,
98+ } ) ;
99+ }
100+ } ;
101+
33102 useEffect ( ( ) => {
34103 if ( members ) {
35104 setValue ( 'members' , members ) ;
@@ -60,7 +129,7 @@ export const ProjectMembers: React.FC<IProps> = ({ members, loading, onChange, r
60129 { label : t ( 'roles.user' ) , value : 'user' } ,
61130 ] ;
62131
63- const addMember = ( username : string ) => {
132+ const addMemberHandler = ( username : string ) => {
64133 const selectedUser = usersData ?. find ( ( u ) => u . username === username ) ;
65134
66135 if ( selectedUser ) {
@@ -90,6 +159,61 @@ export const ProjectMembers: React.FC<IProps> = ({ members, loading, onChange, r
90159 onChangeHandler ( ) ;
91160 } ;
92161
162+ const renderMemberActions = ( ) => {
163+ const actions = [ ] ;
164+
165+ // Add management actions only if not readonly
166+ if ( ! readonly ) {
167+ actions . push (
168+ < Button
169+ key = "delete"
170+ formAction = "none"
171+ onClick = { deleteSelectedMembers }
172+ disabled = { ! selectedItems . length }
173+ >
174+ { t ( 'common.delete' ) }
175+ </ Button >
176+ ) ;
177+ }
178+
179+ // Add join/leave button if user is authenticated (available even in readonly mode)
180+ if ( userData ?. username && project ) {
181+ if ( ! isMember ) {
182+ actions . unshift (
183+ < Button
184+ key = "join"
185+ onClick = { handleJoinProject }
186+ disabled = { isMemberActionLoading }
187+ variant = "primary"
188+ >
189+ { isMemberActionLoading ? t ( 'common.loading' ) : t ( 'projects.join' ) }
190+ </ Button >
191+ ) ;
192+ } else {
193+ // Prevent owners and admins from leaving their projects
194+ const canLeave = ! isProjectOwner && currentUserRole !== 'admin' ;
195+
196+ actions . unshift (
197+ < Button
198+ key = "leave"
199+ onClick = { handleLeaveProject }
200+ disabled = { isMemberActionLoading || ! canLeave }
201+ variant = "normal"
202+ >
203+ { ! canLeave
204+ ? t ( 'projects.owner_cannot_leave' )
205+ : isMemberActionLoading
206+ ? t ( 'common.loading' )
207+ : t ( 'projects.leave' )
208+ }
209+ </ Button >
210+ ) ;
211+ }
212+ }
213+
214+ return actions . length > 0 ? < SpaceBetween size = "xs" direction = "horizontal" > { actions } </ SpaceBetween > : undefined ;
215+ } ;
216+
93217 const COLUMN_DEFINITIONS = [
94218 {
95219 id : 'name' ,
@@ -153,13 +277,7 @@ export const ProjectMembers: React.FC<IProps> = ({ members, loading, onChange, r
153277 < Header
154278 variant = "h2"
155279 counter = { `(${ items ?. length } )` }
156- actions = {
157- readonly ? undefined : (
158- < Button formAction = "none" onClick = { deleteSelectedMembers } disabled = { ! selectedItems . length } >
159- { t ( 'common.delete' ) }
160- </ Button >
161- )
162- }
280+ actions = { renderMemberActions ( ) }
163281 >
164282 { t ( 'projects.edit.members.section_title' ) }
165283 </ Header >
@@ -168,7 +286,7 @@ export const ProjectMembers: React.FC<IProps> = ({ members, loading, onChange, r
168286 readonly ? undefined : (
169287 < UserAutosuggest
170288 disabled = { loading }
171- onSelect = { ( { detail } ) => addMember ( detail . value ) }
289+ onSelect = { ( { detail } ) => addMemberHandler ( detail . value ) }
172290 optionsFilter = { ( options ) => options . filter ( ( o ) => ! fields . find ( ( f ) => f . user . username === o . value ) ) }
173291 />
174292 )
0 commit comments