(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
- ✨(invitations) allow delete invitations mails domains access by an admin
- ✨(front) delete invitations mails domains access
- ✨(front) add show invitations mails domains access #1040
- ✨(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 { 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 { MailDomain, Role } from '../../domains';
import { KEY_LIST_INVITATION_DOMAIN_ACCESSES } from './useInvitationMailDomainAccesses';
interface CreateInvitationParams {
email: User['email'];
@@ -42,7 +48,20 @@ export const createInvitation = async ({
};
export function useCreateInvitation() {
const queryClient = useQueryClient();
return useMutation<Invitation, APIError, CreateInvitationParams>({
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]);
if (
currentRole === Role.VIEWER ||
(access.role === Role.OWNER && currentRole === Role.ADMIN)
) {
if (currentRole === Role.VIEWER) {
return null;
}
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;
}
@@ -94,7 +101,7 @@ export const AccessAction = ({
}
}}
>
{(mailDomain.abilities.put || mailDomain.abilities.patch) && (
{canUpdateRole && (
<Button
aria-label={t(
'Open the modal to update the role of this access',
@@ -115,7 +122,7 @@ export const AccessAction = ({
<Text $theme="primary">{t('Update role')}</Text>
</Button>
)}
{mailDomain.abilities.delete && (
{canDelete && (
<Button
aria-label={t('Open the modal to delete this access')}
onClick={() => {
@@ -138,16 +145,15 @@ export const AccessAction = ({
)}
</div>
{isModalRoleOpen &&
(mailDomain.abilities.put || mailDomain.abilities.patch) && (
<ModalRole
access={access}
currentRole={currentRole}
onClose={() => setIsModalRoleOpen(false)}
slug={mailDomain.slug}
/>
)}
{isModalDeleteOpen && mailDomain.abilities.delete && (
{isModalRoleOpen && canUpdateRole && (
<ModalRole
access={access}
currentRole={currentRole}
onClose={() => setIsModalRoleOpen(false)}
slug={mailDomain.slug}
/>
)}
{isModalDeleteOpen && canDelete && (
<ModalDelete
access={access}
currentRole={currentRole}

View File

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

View File

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

View File

@@ -278,6 +278,137 @@ test.describe('Mail domain', () => {
.getByText('Enabled'),
).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', () => {
@@ -609,6 +740,104 @@ test.describe('Mail domain', () => {
[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', () => {