1- import { FC , useCallback , useState } from 'react' ;
1+ import { FC , useCallback , useMemo , useState } from 'react' ;
22import { Button , FlexBox } from '@ui5/webcomponents-react' ;
33import { MemberTable } from './MemberTable.tsx' ;
4- import { Member } from '../../lib/api/types/shared/members' ;
4+ import { areMembersEqual , Member } from '../../lib/api/types/shared/members' ;
55import { useTranslation } from 'react-i18next' ;
66import styles from './Members.module.css' ;
77import { RadioButtonsSelectOption } from '../Ui/RadioButtonsSelect/RadioButtonsSelect.tsx' ;
88import { AddEditMemberDialog } from './AddEditMemberDialog.tsx' ;
9+ import { ImportMembersDialog } from './ImportMembersDialog.tsx' ;
10+ import { useToast } from '../../context/ToastContext.tsx' ;
11+ import { TFunction } from 'i18next' ;
912
1013export interface EditMembersProps {
1114 members : Member [ ] ;
1215 onMemberChanged : ( members : Member [ ] ) => void ;
1316 isValidationError ?: boolean ;
1417 requireAtLeastOneMember ?: boolean ;
18+ projectName ?: string ;
19+ workspaceName ?: string ;
20+ type : 'workspace' | 'project' | 'mcp' ;
1521}
1622
1723export const ACCOUNT_TYPES : RadioButtonsSelectOption [ ] = [
18- { value : 'User' , label : 'User Account ' , icon : 'employee' } ,
24+ { value : 'User' , label : 'User' , icon : 'employee' } ,
1925 { value : 'ServiceAccount' , label : 'Service Account' , icon : 'machine' } ,
2026] ;
2127
2228export type AccountType = 'User' | 'ServiceAccount' ;
2329
30+ const PROJECT_PREFIX = 'project-' ;
31+ const removeProjectPrefix = ( name ?: string ) =>
32+ name ?. startsWith ( PROJECT_PREFIX ) ? name . slice ( PROJECT_PREFIX . length ) : name ;
33+
2434export const EditMembers : FC < EditMembersProps > = ( {
2535 members,
2636 onMemberChanged,
2737 isValidationError = false ,
2838 requireAtLeastOneMember = true ,
39+ workspaceName,
40+ projectName,
41+ type,
2942} ) => {
3043 const { t } = useTranslation ( ) ;
3144
3245 const [ isMemberDialogOpen , setIsMemberDialogOpen ] = useState ( false ) ;
3346 const [ memberToEdit , setMemberToEdit ] = useState < Member | undefined > ( undefined ) ;
47+ const [ isImportDialogOpen , setIsImportDialogOpen ] = useState ( false ) ;
3448
3549 const handleRemoveMember = useCallback (
3650 ( email : string ) => {
@@ -53,6 +67,39 @@ export const EditMembers: FC<EditMembersProps> = ({
5367 setIsMemberDialogOpen ( false ) ;
5468 } , [ ] ) ;
5569
70+ const handleOpenImportDialog = useCallback ( ( ) => {
71+ setIsImportDialogOpen ( true ) ;
72+ } , [ ] ) ;
73+
74+ const handleCloseImportDialog = useCallback ( ( ) => {
75+ setIsImportDialogOpen ( false ) ;
76+ } , [ ] ) ;
77+
78+ const toast = useToast ( ) ;
79+
80+ const handleImportMembers = useCallback (
81+ ( imported : Member [ ] ) => {
82+ let numberOfAddedMembers = 0 ;
83+ let numberOfChangedMembers = 0 ;
84+
85+ const membersByName = new Map < string , Member > ( members . map ( ( member ) => [ member . name , member ] ) ) ;
86+ imported . forEach ( ( importedMember ) => {
87+ const existingMember = membersByName . get ( importedMember . name ) ;
88+ if ( ! existingMember ) {
89+ numberOfAddedMembers ++ ;
90+ } else if ( ! areMembersEqual ( importedMember , existingMember ) ) {
91+ numberOfChangedMembers ++ ;
92+ }
93+ membersByName . set ( importedMember . name , importedMember ) ;
94+ } ) ;
95+ const updatedMembers = Array . from ( membersByName . values ( ) ) ;
96+
97+ toast . show ( buildToastMessage ( numberOfAddedMembers , numberOfChangedMembers , t ) ) ;
98+ onMemberChanged ( updatedMembers ) ;
99+ } ,
100+ [ members , onMemberChanged , t , toast ] ,
101+ ) ;
102+
56103 const handleSaveMember = useCallback (
57104 ( member : Member , isEdit : boolean ) => {
58105 let updatedMembers : Member [ ] ;
@@ -74,17 +121,34 @@ export const EditMembers: FC<EditMembersProps> = ({
74121 [ members , onMemberChanged , memberToEdit ] ,
75122 ) ;
76123
124+ const computedProjectName = useMemo (
125+ ( ) => ( type === 'mcp' ? removeProjectPrefix ( projectName ) : projectName ) ,
126+ [ type , projectName ] ,
127+ ) ;
128+
77129 return (
78130 < FlexBox direction = "Column" gap = { 8 } >
79- < Button
80- className = { styles . addButton }
81- data-testid = "add-member-button"
82- design = "Emphasized"
83- icon = { 'sap-icon://add-employee' }
84- onClick = { handleOpenMemberFormDialog }
85- >
86- { t ( 'EditMembers.addButton' ) }
87- </ Button >
131+ < FlexBox gap = { 8 } justifyContent = "SpaceBetween" >
132+ < Button
133+ className = { styles . addButton }
134+ data-testid = "add-member-button"
135+ design = "Emphasized"
136+ icon = { 'sap-icon://add-employee' }
137+ onClick = { handleOpenMemberFormDialog }
138+ >
139+ { t ( 'EditMembers.addButton' ) }
140+ </ Button >
141+ { type !== 'project' && (
142+ < Button
143+ className = { styles . narrowButton }
144+ data-testid = "import-members-button"
145+ icon = { 'cause' }
146+ onClick = { handleOpenImportDialog }
147+ >
148+ { t ( 'EditMembers.reuseMembersButton' ) }
149+ </ Button >
150+ ) }
151+ </ FlexBox >
88152 < AddEditMemberDialog
89153 open = { isMemberDialogOpen }
90154 existingMembers = { members }
@@ -93,6 +157,16 @@ export const EditMembers: FC<EditMembersProps> = ({
93157 onSave = { handleSaveMember }
94158 />
95159
160+ { computedProjectName && (
161+ < ImportMembersDialog
162+ isOpen = { isImportDialogOpen }
163+ workspaceName = { workspaceName }
164+ projectName = { computedProjectName }
165+ onClose = { handleCloseImportDialog }
166+ onImport = { handleImportMembers }
167+ />
168+ ) }
169+
96170 < MemberTable
97171 requireAtLeastOneMember = { requireAtLeastOneMember }
98172 members = { members }
@@ -103,3 +177,29 @@ export const EditMembers: FC<EditMembersProps> = ({
103177 </ FlexBox >
104178 ) ;
105179} ;
180+
181+ function buildToastMessage ( addedCount : number , changedCount : number , t : TFunction ) {
182+ const messages : string [ ] = [ ] ;
183+
184+ if ( addedCount === 0 && changedCount === 0 ) {
185+ return t ( 'EditMembers.membersToastNoChanges' ) ;
186+ }
187+
188+ if ( addedCount > 0 ) {
189+ messages . push (
190+ addedCount === 1
191+ ? t ( 'EditMembers.membersToastAdded1' )
192+ : t ( 'EditMembers.membersToastAddedN' , { count : addedCount } ) ,
193+ ) ;
194+ }
195+
196+ if ( changedCount > 0 ) {
197+ messages . push (
198+ changedCount === 1
199+ ? t ( 'EditMembers.membersToastChanged1' )
200+ : t ( 'EditMembers.membersToastChangedN' , { count : changedCount } ) ,
201+ ) ;
202+ }
203+
204+ return messages . join ( ' ' ) ;
205+ }
0 commit comments