Skip to content

Commit c930091

Browse files
lucasgoralGenosseOttLasserichandreaskienle
authored
feat: Import members feature for Workspace and MCP creation (#265)
Co-authored-by: Johannes Ott <[email protected]> Co-authored-by: Lasse <[email protected]> Co-authored-by: Andreas Kienle <[email protected]>
1 parent 2eb3dda commit c930091

File tree

12 files changed

+522
-56
lines changed

12 files changed

+522
-56
lines changed

public/locales/en.json

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,10 @@
77
},
88
"Entities": {
99
"ManagedControlPlane": "Managed Control Plane",
10-
"Project": "Project"
10+
"Project": "Project",
11+
"Workspace": "Workspace",
12+
"Users": "Users",
13+
"ServiceAccounts": "ServiceAccounts"
1114
},
1215
"ComponentList": {
1316
"tableComponentHeader": "Name",
@@ -142,7 +145,7 @@
142145
"subtitle": "Fetching data..."
143146
},
144147
"MemberTable": {
145-
"columnEmailHeader": "Email",
148+
"columnNameHeader": "Name",
146149
"columnRoleHeader": "Role",
147150
"columnTypeHeader": "Type",
148151
"columnNamespaceHeader": "Namespace"
@@ -165,12 +168,18 @@
165168
"membersHeader": "Members"
166169
},
167170
"EditMembers": {
168-
"addButton": "Add new member or service account",
169-
"editHeader": "Edit member or service account",
170-
"addHeader": "Add new member or service account",
171+
"addButton": "Add User or ServiceAccount",
172+
"editHeader": "Edit User or ServiceAccount",
173+
"addHeader": "Add User or ServiceAccount",
171174
"saveButton": "Save changes",
172175
"defaultNamespaceInfo": "Leave empty to use <span>default</span> namespace",
173-
"serviceAccoutsGuide": "You can also use our <link1>Service Account Guide</link1> for more information."
176+
"serviceAccoutsGuide": "You can also use our <link1>Service Account Guide</link1> for more information.",
177+
"reuseMembersButton": "Reuse",
178+
"membersToastNoChanges": "No changes.",
179+
"membersToastAdded1": "1 member added.",
180+
"membersToastAddedN": "{{count}} members added.",
181+
"membersToastChanged1": "1 member changed.",
182+
"membersToastChangedN": "{{count}} members changed."
174183
},
175184

176185
"ProjectsPage": {
@@ -315,6 +324,7 @@
315324
"notValidChargingTargetFormat": "Use lowercase letters a-f, numbers 0-9, and hyphens (-) in the format: aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"
316325
},
317326
"common": {
327+
"all": "All",
318328
"documentation": "Documentation",
319329
"close": "Close",
320330
"cannotLoadData": "Cannot load data",
@@ -421,4 +431,13 @@
421431
"activate": "Activate"
422432
}
423433
}
434+
,
435+
"ImportMembersDialog": {
436+
"dialogTitle": "Reuse Members",
437+
"reuseFromLabel": "Reuse from",
438+
"filterForLabel": "Filter for",
439+
"addMembersButton0": "Add members",
440+
"addMembersButton1": "Add member",
441+
"addMembersButtonN": "Add {{count}} members"
442+
}
424443
}

src/components/Dialogs/CreateProjectWorkspaceDialog.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ export interface CreateProjectWorkspaceDialogProps {
3535
errors: FieldErrors<CreateDialogProps>;
3636
setValue: UseFormSetValue<CreateDialogProps>;
3737
projectName?: string;
38-
type: 'workspace' | 'project';
38+
type: 'workspace' | 'project' | 'mcp';
3939
watch: UseFormWatch<CreateDialogProps>;
4040
}
4141

@@ -93,7 +93,13 @@ export function CreateProjectWorkspaceDialog({
9393
requireChargingTarget={type === 'project'}
9494
sideFormContent={
9595
<FormGroup headerText={t('CreateProjectWorkspaceDialog.membersHeader')}>
96-
<EditMembers members={members} isValidationError={!!errors.members} onMemberChanged={setMembers} />
96+
<EditMembers
97+
type={type}
98+
members={members}
99+
isValidationError={!!errors.members}
100+
projectName={projectName}
101+
onMemberChanged={setMembers}
102+
/>
97103
</FormGroup>
98104
}
99105
/>

src/components/Members/EditMembers.tsx

Lines changed: 112 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,50 @@
1-
import { FC, useCallback, useState } from 'react';
1+
import { FC, useCallback, useMemo, useState } from 'react';
22
import { Button, FlexBox } from '@ui5/webcomponents-react';
33
import { MemberTable } from './MemberTable.tsx';
4-
import { Member } from '../../lib/api/types/shared/members';
4+
import { areMembersEqual, Member } from '../../lib/api/types/shared/members';
55
import { useTranslation } from 'react-i18next';
66
import styles from './Members.module.css';
77
import { RadioButtonsSelectOption } from '../Ui/RadioButtonsSelect/RadioButtonsSelect.tsx';
88
import { AddEditMemberDialog } from './AddEditMemberDialog.tsx';
9+
import { ImportMembersDialog } from './ImportMembersDialog.tsx';
10+
import { useToast } from '../../context/ToastContext.tsx';
11+
import { TFunction } from 'i18next';
912

1013
export 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

1723
export 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

2228
export 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+
2434
export 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+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
.dialog {
2+
min-width: 650px;
3+
}
4+
5+
.grid {
6+
display: grid;
7+
grid-template-columns: auto 1fr;
8+
gap: 1rem;
9+
padding: 1rem 1rem 2rem;
10+
}
11+
12+
.gridColumnLabel {
13+
align-self: center;
14+
}
15+
16+
.tableContainer {
17+
padding: 1rem;
18+
}

0 commit comments

Comments
 (0)