Skip to content

Commit 503af2a

Browse files
committed
Add toast
1 parent 3aa41c9 commit 503af2a

File tree

4 files changed

+151
-9
lines changed

4 files changed

+151
-9
lines changed

public/locales/en.json

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,12 @@
174174
"saveButton": "Save changes",
175175
"defaultNamespaceInfo": "Leave empty to use <span>default</span> namespace",
176176
"serviceAccoutsGuide": "You can also use our <link1>Service Account Guide</link1> for more information.",
177-
"reuseMembersButton": "Reuse"
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."
178183
},
179184

180185
"ProjectsPage": {
@@ -432,7 +437,7 @@
432437
"reuseFromLabel": "Reuse from",
433438
"filterForLabel": "Filter for",
434439
"addMembersButton0": "Add members",
435-
"addMembersButton1": "Add 1 member",
440+
"addMembersButton1": "Add member",
436441
"addMembersButtonN": "Add {{count}} members"
437442
}
438443
}

src/components/Members/EditMembers.tsx

Lines changed: 49 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
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';
99
import { ImportMembersDialog } from './ImportMembersDialog.tsx';
10+
import { useToast } from '../../context/ToastContext.tsx';
11+
import { TFunction } from 'i18next';
1012

1113
export interface EditMembersProps {
1214
members: Member[];
@@ -73,15 +75,29 @@ export const EditMembers: FC<EditMembersProps> = ({
7375
setIsImportDialogOpen(false);
7476
}, []);
7577

78+
const toast = useToast();
79+
7680
const handleImportMembers = useCallback(
7781
(imported: Member[]) => {
78-
const byName = new Map<string, Member>();
79-
members.forEach((m) => byName.set(m.name, m));
80-
imported.forEach((m) => byName.set(m.name, m));
81-
const merged = Array.from(byName.values());
82-
onMemberChanged(merged);
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);
8399
},
84-
[members, onMemberChanged],
100+
[members, onMemberChanged, t],
85101
);
86102

87103
const handleSaveMember = useCallback(
@@ -161,3 +177,29 @@ export const EditMembers: FC<EditMembersProps> = ({
161177
</FlexBox>
162178
);
163179
};
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: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import { describe, it, expect } from 'vitest';
2+
import { areMembersEqual, Member } from './members.ts';
3+
4+
const makeMember = (overrides: Partial<Member> = {}): Member => ({
5+
kind: 'User',
6+
name: 'alice',
7+
namespace: 'default-namespace',
8+
roles: ['Viewer', 'Admin'],
9+
...overrides,
10+
});
11+
12+
describe('members', () => {
13+
describe('areMembersEqual', () => {
14+
it('returns true when a and b are the same reference', () => {
15+
const a = makeMember();
16+
expect(areMembersEqual(a, a)).toBe(true);
17+
});
18+
19+
it('returns true when the members have the very same attributes', () => {
20+
const a = makeMember();
21+
const b = makeMember();
22+
expect(areMembersEqual(a, b)).toBe(true);
23+
});
24+
25+
it('returns true even if the roles are sorted differently', () => {
26+
const a = makeMember({ roles: ['Viewer', 'Admin'] });
27+
const b = makeMember({ roles: ['Admin', 'Viewer'] });
28+
expect(areMembersEqual(a, b)).toBe(true);
29+
});
30+
31+
it('handles empty roles correctly', () => {
32+
const a = makeMember({ roles: [] });
33+
const b = makeMember({ roles: [] });
34+
expect(areMembersEqual(a, b)).toBe(true);
35+
});
36+
37+
it('returns false when b is undefined', () => {
38+
const a = makeMember();
39+
expect(areMembersEqual(a, undefined)).toBe(false);
40+
});
41+
42+
it('returns false when kinds differ', () => {
43+
const a = makeMember({ kind: 'User' });
44+
const b = makeMember({ kind: 'ServiceAccount' });
45+
expect(areMembersEqual(a, b)).toBe(false);
46+
});
47+
48+
it('returns false when names differ', () => {
49+
const a = makeMember({ name: 'alice' });
50+
const b = makeMember({ name: 'bob' });
51+
expect(areMembersEqual(a, b)).toBe(false);
52+
});
53+
54+
it('returns false when namespaces differ', () => {
55+
const a = makeMember({ namespace: 'namespace-a' });
56+
const b = makeMember({ namespace: 'namespace-b' });
57+
expect(areMembersEqual(a, b)).toBe(false);
58+
});
59+
60+
it('returns false when role counts differ', () => {
61+
const a = makeMember({ roles: ['Viewer', 'Admin'] });
62+
const b = makeMember({ roles: ['Viewer'] });
63+
expect(areMembersEqual(a, b)).toBe(false);
64+
});
65+
66+
it('returns false when roles differ (same length)', () => {
67+
const a = makeMember({ roles: ['Viewer', 'Admin'] });
68+
const b = makeMember({ roles: ['Viewer', 'OtherRole'] });
69+
expect(areMembersEqual(a, b)).toBe(false);
70+
});
71+
72+
it('does not treat duplicate roles in a special way (debatable)', () => {
73+
const a = makeMember({ roles: ['Viewer', 'Admin'] });
74+
const b = makeMember({ roles: ['Viewer', 'Viewer', 'Admin'] });
75+
expect(areMembersEqual(a, b)).toBe(false);
76+
});
77+
78+
it('fails when b has a role a does not have', () => {
79+
const a = makeMember({ roles: ['Viewer'] });
80+
const b = makeMember({ roles: ['Viewer', 'Admin'] });
81+
expect(areMembersEqual(a, b)).toBe(false);
82+
});
83+
});
84+
});

src/lib/api/types/shared/members.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,17 @@ export interface Member {
2929
namespace?: string;
3030
}
3131

32+
export function areMembersEqual(a: Member, b?: Member): boolean {
33+
return (
34+
!!b &&
35+
a.kind === b.kind &&
36+
a.name === b.name &&
37+
a.namespace === b.namespace &&
38+
a.roles.length === b.roles.length &&
39+
a.roles.every((r) => b.roles.includes(r))
40+
);
41+
}
42+
3243
export interface MemberPayload {
3344
kind: string;
3445
name: string;

0 commit comments

Comments
 (0)