(front) delete invitations mails domains access

add delete button for delete invitations to mails domains access
This commit is contained in:
Eléonore Voisin
2026-01-28 11:33:25 +01:00
parent 894a90d25d
commit 7e10207808
5 changed files with 231 additions and 10 deletions
+1
View File
@@ -10,6 +10,7 @@ and this project adheres to
### Added
- ✨(front) delete invitations mails domains access
- ✨(front) add show invitations mails domains access #1040
- ✨(invitations) can delete domain invitations
@@ -3,4 +3,5 @@ export * from './useInvitationMailDomainAccesses';
export * from './useUpdateMailDomainAccess';
export * from './useCreateMailDomainAccess';
export * from './useDeleteMailDomainAccess';
export * from './useDeleteMailDomainInvitation';
export * from './useCreateInvitation';
@@ -0,0 +1,89 @@
import {
UseMutationOptions,
useMutation,
useQueryClient,
} from '@tanstack/react-query';
import { APIError, errorCauses, fetchAPI } from '@/api';
import {
KEY_LIST_MAIL_DOMAIN,
KEY_MAIL_DOMAIN,
} from '@/features/mail-domains/domains';
import { KEY_LIST_INVITATION_DOMAIN_ACCESSES } from './useInvitationMailDomainAccesses';
interface DeleteMailDomainInvitationProps {
slug: string;
invitationId: string;
}
export const deleteMailDomainInvitation = async ({
slug,
invitationId,
}: DeleteMailDomainInvitationProps): Promise<void> => {
const response = await fetchAPI(
`mail-domains/${slug}/invitations/${invitationId}/`,
{
method: 'DELETE',
},
);
if (!response.ok) {
throw new APIError(
'Failed to delete the invitation',
await errorCauses(response),
);
}
};
type UseDeleteMailDomainInvitationOptions = UseMutationOptions<
void,
APIError,
DeleteMailDomainInvitationProps
>;
export const useDeleteMailDomainInvitation = (
options?: UseDeleteMailDomainInvitationOptions,
) => {
const queryClient = useQueryClient();
const {
onSuccess: optionsOnSuccess,
onError: optionsOnError,
...restOptions
} = options || {};
return useMutation<void, APIError, DeleteMailDomainInvitationProps>({
mutationFn: deleteMailDomainInvitation,
...restOptions,
onSuccess: (data, variables, context) => {
void queryClient.invalidateQueries({
queryKey: [KEY_LIST_INVITATION_DOMAIN_ACCESSES],
});
void queryClient.invalidateQueries({
queryKey: [KEY_MAIL_DOMAIN],
});
void queryClient.invalidateQueries({
queryKey: [KEY_LIST_MAIL_DOMAIN],
});
if (optionsOnSuccess) {
(
optionsOnSuccess as unknown as (
data: void,
variables: DeleteMailDomainInvitationProps,
context: unknown,
) => void
)(data, variables, context);
}
},
onError: (error, variables, context) => {
if (optionsOnError) {
(
optionsOnError as unknown as (
error: APIError,
variables: DeleteMailDomainInvitationProps,
context: unknown,
) => void
)(error, variables, context);
}
},
});
};
@@ -12,6 +12,7 @@ import { PAGE_SIZE } from '../conf';
import { Access } from '../types';
import { AccessAction } from './AccessAction';
import { InvitationAction } from './InvitationAction';
interface AccessesListProps {
mailDomain: MailDomain;
@@ -165,20 +166,23 @@ export const AccessesList = ({
email={access.user.email}
showEmail
/>
<Text
$size="small"
$margin={{ left: '4px' }}
$color={colorsTokens()['greyscale-500']}
>
{t('on pending')}
</Text>
</Box>
}
/>
<Box $direction="row" $gap="20px" $align="center">
<Box
$background={colorsTokens()['info-600']}
$radius="4px"
$padding={{ horizontal: 'base', vertical: '3px' }}
>
<Text $size="small" $color={colorsTokens()['info-100']}>
{t('Pending')}
</Text>
</Box>
<Box $direction="row" $align="center">
<Text>{localizedRoles[access.role]}</Text>
<InvitationAction
mailDomain={mailDomain}
access={access}
currentRole={currentRole}
/>
</Box>
</Box>
))}
@@ -0,0 +1,126 @@
import {
Button,
VariantType,
useToastProvider,
} from '@openfun/cunningham-react';
import React, { useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { IconOptions, Text } from '@/components';
import { MailDomain, Role } from '../../domains/types';
import { useDeleteMailDomainInvitation } from '../api';
import { Access } from '../types';
interface InvitationActionProps {
access: Access;
currentRole: Role;
mailDomain: MailDomain;
}
export const InvitationAction = ({
access,
currentRole,
mailDomain,
}: InvitationActionProps) => {
const { t } = useTranslation();
const { toast } = useToastProvider();
const [isDropOpen, setIsDropOpen] = useState(false);
const dropdownRef = useRef<HTMLDivElement>(null);
const { mutate: deleteMailDomainInvitation } = useDeleteMailDomainInvitation({
onSuccess: () => {
toast(t('The invitation has been deleted'), VariantType.SUCCESS, {
duration: 4000,
});
},
});
useEffect(() => {
const handleClickOutside = (e: MouseEvent) => {
if (
dropdownRef.current &&
!dropdownRef.current.contains(e.target as Node)
) {
setIsDropOpen(false);
}
};
if (isDropOpen) {
document.addEventListener('mousedown', handleClickOutside);
}
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, [isDropOpen]);
if (currentRole === Role.VIEWER || !mailDomain.abilities.delete) {
return null;
}
return (
<div
style={{ position: 'relative', display: 'inline-block' }}
ref={dropdownRef}
>
<button
onClick={() => setIsDropOpen((prev) => !prev)}
aria-label={t('Open the invitation options modal')}
style={{
background: 'none',
border: 'none',
cursor: 'pointer',
padding: 4,
}}
type="button"
>
<IconOptions />
</button>
{isDropOpen && (
<div
role="menu"
tabIndex={0}
style={{
position: 'absolute',
top: '100%',
right: 0,
zIndex: 1000,
background: 'white',
border: '1px solid #ccc',
borderRadius: 4,
padding: '0.5rem',
minWidth: '210px',
boxShadow: '0 4px 6px rgba(0, 0, 0, 0.1)',
}}
onClick={(e) => e.stopPropagation()}
onKeyDown={(e) => {
if (e.key === 'Escape' || e.key === 'Enter') {
setIsDropOpen(false);
}
}}
>
<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>
);
};