(invitations) allow delete invitations by an admin (#1052)

allow delete invitations mails domains access by an admin
This commit is contained in:
elvoisin
2026-02-11 10:29:03 +01:00
committed by GitHub
parent 59f9f54b34
commit 40c39bd9ba
6 changed files with 318 additions and 41 deletions

View File

@@ -10,6 +10,7 @@ and this project adheres to
### Added ### Added
- ✨(invitations) allow delete invitations mails domains access by an admin
- ✨(front) delete invitations mails domains access - ✨(front) delete invitations mails domains access
- ✨(front) add show invitations mails domains access #1040 - ✨(front) add show invitations mails domains access #1040
- ✨(invitations) can delete domain invitations - ✨(invitations) can delete domain invitations

View File

@@ -1,10 +1,16 @@
import { useMutation } from '@tanstack/react-query'; import { useMutation, useQueryClient } from '@tanstack/react-query';
import { APIError, errorCauses, fetchAPI } from '@/api'; import { APIError, errorCauses, fetchAPI } from '@/api';
import { User } from '@/core/auth'; import { User } from '@/core/auth';
import {
KEY_LIST_MAIL_DOMAIN,
KEY_MAIL_DOMAIN,
MailDomain,
Role,
} from '@/features/mail-domains/domains';
import { Invitation, OptionType } from '@/features/teams/member-add/types'; import { Invitation, OptionType } from '@/features/teams/member-add/types';
import { MailDomain, Role } from '../../domains'; import { KEY_LIST_INVITATION_DOMAIN_ACCESSES } from './useInvitationMailDomainAccesses';
interface CreateInvitationParams { interface CreateInvitationParams {
email: User['email']; email: User['email'];
@@ -42,7 +48,20 @@ export const createInvitation = async ({
}; };
export function useCreateInvitation() { export function useCreateInvitation() {
const queryClient = useQueryClient();
return useMutation<Invitation, APIError, CreateInvitationParams>({ return useMutation<Invitation, APIError, CreateInvitationParams>({
mutationFn: createInvitation, mutationFn: createInvitation,
onSuccess: () => {
void queryClient.invalidateQueries({
queryKey: [KEY_LIST_INVITATION_DOMAIN_ACCESSES],
});
void queryClient.invalidateQueries({
queryKey: [KEY_MAIL_DOMAIN],
});
void queryClient.invalidateQueries({
queryKey: [KEY_LIST_MAIL_DOMAIN],
});
},
}); });
} }

View File

@@ -45,10 +45,17 @@ export const AccessAction = ({
}; };
}, [isDropOpen]); }, [isDropOpen]);
if ( if (currentRole === Role.VIEWER) {
currentRole === Role.VIEWER || return null;
(access.role === Role.OWNER && currentRole === Role.ADMIN) }
) {
const canUpdateRole =
(mailDomain.abilities.put || mailDomain.abilities.patch) &&
access.can_set_role_to &&
access.can_set_role_to.length > 0;
const canDelete = mailDomain.abilities.delete;
if (!canUpdateRole && !canDelete) {
return null; return null;
} }
@@ -94,7 +101,7 @@ export const AccessAction = ({
} }
}} }}
> >
{(mailDomain.abilities.put || mailDomain.abilities.patch) && ( {canUpdateRole && (
<Button <Button
aria-label={t( aria-label={t(
'Open the modal to update the role of this access', 'Open the modal to update the role of this access',
@@ -115,7 +122,7 @@ export const AccessAction = ({
<Text $theme="primary">{t('Update role')}</Text> <Text $theme="primary">{t('Update role')}</Text>
</Button> </Button>
)} )}
{mailDomain.abilities.delete && ( {canDelete && (
<Button <Button
aria-label={t('Open the modal to delete this access')} aria-label={t('Open the modal to delete this access')}
onClick={() => { onClick={() => {
@@ -138,16 +145,15 @@ export const AccessAction = ({
)} )}
</div> </div>
{isModalRoleOpen && {isModalRoleOpen && canUpdateRole && (
(mailDomain.abilities.put || mailDomain.abilities.patch) && ( <ModalRole
<ModalRole access={access}
access={access} currentRole={currentRole}
currentRole={currentRole} onClose={() => setIsModalRoleOpen(false)}
onClose={() => setIsModalRoleOpen(false)} slug={mailDomain.slug}
slug={mailDomain.slug} />
/> )}
)} {isModalDeleteOpen && canDelete && (
{isModalDeleteOpen && mailDomain.abilities.delete && (
<ModalDelete <ModalDelete
access={access} access={access}
currentRole={currentRole} currentRole={currentRole}

View File

@@ -53,7 +53,13 @@ export const InvitationAction = ({
}; };
}, [isDropOpen]); }, [isDropOpen]);
if (currentRole === Role.VIEWER || !mailDomain.abilities.delete) { if (currentRole === Role.VIEWER) {
return null;
}
const canDelete = currentRole === Role.OWNER || currentRole === Role.ADMIN;
if (!canDelete) {
return null; return null;
} }
@@ -99,26 +105,28 @@ export const InvitationAction = ({
} }
}} }}
> >
<Button {canDelete && (
aria-label={t('Delete this invitation')} <Button
onClick={() => { aria-label={t('Delete this invitation')}
deleteMailDomainInvitation({ onClick={() => {
slug: mailDomain.slug, deleteMailDomainInvitation({
invitationId: access.id, slug: mailDomain.slug,
}); invitationId: access.id,
setIsDropOpen(false); });
}} setIsDropOpen(false);
color="primary-text" }}
size="small" color="primary-text"
fullWidth size="small"
icon={ fullWidth
<span className="material-icons" aria-hidden="true"> icon={
delete <span className="material-icons" aria-hidden="true">
</span> delete
} </span>
> }
<Text $theme="primary">{t('Delete invitation')}</Text> >
</Button> <Text $theme="primary">{t('Delete invitation')}</Text>
</Button>
)}
</div> </div>
)} )}
</div> </div>

View File

@@ -96,18 +96,32 @@ export const ModalDomainAccessesManagement = ({
switchActions(selectedMembers), switchActions(selectedMembers),
); );
let hasInvitation = false;
let hasError = false;
settledPromises.forEach((settledPromise) => { settledPromises.forEach((settledPromise) => {
switch (settledPromise.status) { switch (settledPromise.status) {
case 'rejected': case 'rejected':
onError((settledPromise.reason as APIErrorMember).data); onError((settledPromise.reason as APIErrorMember).data);
hasError = true;
break; break;
case 'fulfilled': case 'fulfilled':
onSuccess(settledPromise.value); const option = settledPromise.value;
onSuccess(option);
if (!isOptionNewMember(option)) {
hasInvitation = true;
}
break; break;
} }
onClose();
}); });
if (hasInvitation && !hasError) {
setSelectedMembers([]);
setRole(Role.VIEWER);
} else if (!hasInvitation) {
onClose();
}
}; };
return ( return (

View File

@@ -278,6 +278,137 @@ test.describe('Mail domain', () => {
.getByText('Enabled'), .getByText('Enabled'),
).toBeVisible(); ).toBeVisible();
}); });
test('admin can delete invitation', async ({ page, browserName }) => {
// Intercept API calls to include enabled-domain.com in the list
await page.route('**/api/v1.0/mail-domains/\?*', async (route) => {
await route.fulfill({
json: {
count: 1,
next: null,
previous: null,
results: [
{
name: 'enabled-domain.com',
id: '456ac6ca-0402-4615-8005-69bc1efde43i',
created_at: currentDateIso,
updated_at: currentDateIso,
slug: 'enabled-domaincom',
status: 'enabled',
abilities: {
get: true,
patch: true,
put: true,
post: true,
delete: true,
manage_accesses: true,
},
},
],
},
});
});
await page.route(
'**/api/v1.0/mail-domains/enabled-domaincom/',
async (route) => {
await route.fulfill({
json: {
name: 'enabled-domain.com',
id: '456ac6ca-0402-4615-8005-69bc1efde43i',
created_at: currentDateIso,
updated_at: currentDateIso,
slug: 'enabled-domaincom',
status: 'enabled',
abilities: {
get: true,
patch: true,
put: true,
post: true,
delete: true,
manage_accesses: true,
},
},
});
},
);
await page.goto('/');
await keyCloakSignIn(page, browserName, 'mail-administrator');
await clickOnMailDomainsNavButton(page);
await expect(page).toHaveURL(/mail-domains\//);
await expect(
page.getByText('enabled-domain.com', { exact: true }),
).toBeVisible();
await page
.getByLabel(`enabled-domain.com listboxDomains button`)
.click();
await expect(page).toHaveURL(/mail-domains\/enabled-domaincom\//);
await expect(
page.getByRole('heading', { name: 'enabled-domain.com' }),
).toBeVisible();
await page.route(
'**/api/v1.0/mail-domains/enabled-domaincom/invitations/',
async (route) => {
await route.fulfill({
json: {
count: 1,
next: null,
previous: null,
results: [
{
id: '123e4567-e89b-12d3-a456-426614174000',
email: 'people@people.world',
role: 'administrator',
created_at: currentDateIso,
can_set_role_to: [],
},
],
},
});
},
);
let deleteInvitationCalled = false;
await page.route(
'**/api/v1.0/mail-domains/enabled-domaincom/invitations/123e4567-e89b-12d3-a456-426614174000/',
async (route) => {
if (route.request().method() === 'DELETE') {
deleteInvitationCalled = true;
await route.fulfill({
status: 204,
json: {},
});
} else {
await route.continue();
}
},
);
await page.getByText('Access management').click();
await expect(page.getByText('Invitations')).toBeVisible();
await expect(page.getByText('people@people.world')).toBeVisible();
await expect(
page.getByLabel('Open the invitation options modal'),
).toBeVisible();
await page.getByLabel('Open the invitation options modal').click();
await page.getByText('Delete invitation').click();
// Verify invitation is deleted (API call was made)
await expect(() => {
expect(deleteInvitationCalled).toBe(true);
}).toPass();
// Verify success toast appears
await expect(
page.getByText('The invitation has been deleted'),
).toBeVisible();
});
}); });
test.describe('mail domain creation is pending', () => { test.describe('mail domain creation is pending', () => {
@@ -609,6 +740,104 @@ test.describe('Mail domain', () => {
[mailboxesFixtures.domainFr.page1, mailboxesFixtures.domainFr.page2], [mailboxesFixtures.domainFr.page1, mailboxesFixtures.domainFr.page2],
); );
}); });
test('viewer cannot delete invitation', async ({ page, browserName }) => {
// Intercept API calls to include enabled-domain.com in the list
await page.route('**/api/v1.0/mail-domains/\?*', async (route) => {
await route.fulfill({
json: {
count: 1,
next: null,
previous: null,
results: [
{
name: 'enabled-domain.com',
id: '456ac6ca-0402-4615-8005-69bc1efde43i',
created_at: currentDateIso,
updated_at: currentDateIso,
slug: 'enabled-domaincom',
status: 'enabled',
abilities: {
get: true,
patch: false,
put: false,
post: false,
delete: false,
manage_accesses: false,
},
},
],
},
});
});
await page.route(
'**/api/v1.0/mail-domains/enabled-domaincom/',
async (route) => {
await route.fulfill({
json: {
name: 'enabled-domain.com',
id: '456ac6ca-0402-4615-8005-69bc1efde43i',
created_at: currentDateIso,
updated_at: currentDateIso,
slug: 'enabled-domaincom',
status: 'enabled',
abilities: {
get: true,
patch: false,
put: false,
post: false,
delete: false,
manage_accesses: false,
},
},
});
},
);
// Intercept invitations API call BEFORE navigation
await page.route(
'**/api/v1.0/mail-domains/enabled-domaincom/invitations/',
async (route) => {
await route.fulfill({
json: {
count: 1,
next: null,
previous: null,
results: [
{
id: '123e4567-e89b-12d3-a456-426614174000',
email: 'people@people.world',
role: 'administrator',
created_at: currentDateIso,
can_set_role_to: [],
},
],
},
});
},
);
await page.goto('/');
await keyCloakSignIn(page, browserName, 'mail-member');
await clickOnMailDomainsNavButton(page);
await expect(page).toHaveURL(/mail-domains\//);
await expect(
page.getByText('enabled-domain.com', { exact: true }),
).toBeVisible();
await page
.getByLabel(`enabled-domain.com listboxDomains button`)
.click();
await expect(page).toHaveURL(/mail-domains\/enabled-domaincom\//);
await expect(
page.getByRole('heading', { name: 'enabled-domain.com' }),
).toBeVisible();
// Verify that "Access management" button is not visible for a viewer
await expect(page.getByText('Access management')).toBeHidden();
});
}); });
test.describe('mail domain creation is pending', () => { test.describe('mail domain creation is pending', () => {