✨(frontend) invitation list
- Display the list of invitations for a document in the share modal. - We can now cancel an invitation. - We can now update the role of a invited user.
This commit is contained in:
@@ -16,6 +16,7 @@ and this project adheres to
|
|||||||
- 🌐Internationalize invitation email #167
|
- 🌐Internationalize invitation email #167
|
||||||
- ✨(frontend) White branding #164
|
- ✨(frontend) White branding #164
|
||||||
- ✨Email invitation when add user to doc #171
|
- ✨Email invitation when add user to doc #171
|
||||||
|
- ✨Invitation management #174
|
||||||
|
|
||||||
## Fixed
|
## Fixed
|
||||||
|
|
||||||
|
|||||||
@@ -124,6 +124,15 @@ test.describe('Document create member', () => {
|
|||||||
expect(responseAddUser.request().headers()['content-language']).toBe(
|
expect(responseAddUser.request().headers()['content-language']).toBe(
|
||||||
'en-us',
|
'en-us',
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const listInvitation = page.getByLabel('List invitation card');
|
||||||
|
await expect(listInvitation.locator('li').getByText(email)).toBeVisible();
|
||||||
|
await expect(
|
||||||
|
listInvitation.locator('li').getByText('Invited'),
|
||||||
|
).toBeVisible();
|
||||||
|
|
||||||
|
const listMember = page.getByLabel('List members card');
|
||||||
|
await expect(listMember.locator('li').getByText(user.email)).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('it try to add twice the same user', async ({ page, browserName }) => {
|
test('it try to add twice the same user', async ({ page, browserName }) => {
|
||||||
@@ -255,4 +264,47 @@ test.describe('Document create member', () => {
|
|||||||
responseCreateInvitation.request().headers()['content-language'],
|
responseCreateInvitation.request().headers()['content-language'],
|
||||||
).toBe('fr-fr');
|
).toBe('fr-fr');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('it manages invitation', async ({ page, browserName }) => {
|
||||||
|
await createDoc(page, 'user-invitation', browserName, 1);
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: 'Share' }).click();
|
||||||
|
|
||||||
|
const inputSearch = page.getByLabel(/Find a member to add to the document/);
|
||||||
|
|
||||||
|
const email = randomName('test@test.fr', browserName, 1)[0];
|
||||||
|
await inputSearch.fill(email);
|
||||||
|
await page.getByRole('option', { name: email }).click();
|
||||||
|
|
||||||
|
// Choose a role
|
||||||
|
await page.getByRole('combobox', { name: /Choose a role/ }).click();
|
||||||
|
await page.getByRole('option', { name: 'Administrator' }).click();
|
||||||
|
|
||||||
|
const responsePromiseCreateInvitation = page.waitForResponse(
|
||||||
|
(response) =>
|
||||||
|
response.url().includes('/invitations/') && response.status() === 201,
|
||||||
|
);
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: 'Validate' }).click();
|
||||||
|
|
||||||
|
// Check invitation sent
|
||||||
|
await expect(page.getByText(`Invitation sent to ${email}`)).toBeVisible();
|
||||||
|
const responseCreateInvitation = await responsePromiseCreateInvitation;
|
||||||
|
expect(responseCreateInvitation.ok()).toBeTruthy();
|
||||||
|
|
||||||
|
const listInvitation = page.getByLabel('List invitation card');
|
||||||
|
const li = listInvitation.locator('li').filter({
|
||||||
|
hasText: email,
|
||||||
|
});
|
||||||
|
await expect(li.getByText(email)).toBeVisible();
|
||||||
|
|
||||||
|
await li.getByRole('combobox', { name: /Role/ }).click();
|
||||||
|
await li.getByRole('option', { name: 'Reader' }).click();
|
||||||
|
await expect(page.getByText(`The role has been updated.`)).toBeVisible();
|
||||||
|
await li.getByText('delete').click();
|
||||||
|
await expect(
|
||||||
|
page.getByText(`The invitation has been removed.`),
|
||||||
|
).toBeVisible();
|
||||||
|
await expect(listInvitation.locator('li').getByText(email)).toBeHidden();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,11 +2,11 @@ import { t } from 'i18next';
|
|||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { createGlobalStyle } from 'styled-components';
|
import { createGlobalStyle } from 'styled-components';
|
||||||
|
|
||||||
import { Box, Card, Text } from '@/components';
|
import { Box, Card, SideModal, Text } from '@/components';
|
||||||
import { SideModal } from '@/components/SideModal';
|
import { InvitationList } from '@/features/docs/members/invitation-list';
|
||||||
|
import { AddMembers } from '@/features/docs/members/members-add';
|
||||||
|
import { MemberList } from '@/features/docs/members/members-list';
|
||||||
|
|
||||||
import { AddMembers } from '../../members/members-add';
|
|
||||||
import { MemberList } from '../../members/members-list/components/MemberList';
|
|
||||||
import { Doc } from '../types';
|
import { Doc } from '../types';
|
||||||
import { currentDocRole } from '../utils';
|
import { currentDocRole } from '../utils';
|
||||||
|
|
||||||
@@ -67,6 +67,7 @@ export const ModalShare = ({ onClose, doc }: ModalShareProps) => {
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<AddMembers doc={doc} currentRole={currentDocRole(doc.abilities)} />
|
<AddMembers doc={doc} currentRole={currentDocRole(doc.abilities)} />
|
||||||
|
<InvitationList doc={doc} />
|
||||||
<MemberList doc={doc} />
|
<MemberList doc={doc} />
|
||||||
</SideModal>
|
</SideModal>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export * from './useDeleteDocInvitation';
|
||||||
|
export * from './useDocInvitations';
|
||||||
|
export * from './useUpdateDocInvitation';
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
import {
|
||||||
|
UseMutationOptions,
|
||||||
|
useMutation,
|
||||||
|
useQueryClient,
|
||||||
|
} from '@tanstack/react-query';
|
||||||
|
|
||||||
|
import { APIError, errorCauses, fetchAPI } from '@/api';
|
||||||
|
|
||||||
|
import { KEY_LIST_DOC_INVITATIONS } from './useDocInvitations';
|
||||||
|
|
||||||
|
interface DeleteDocInvitationProps {
|
||||||
|
docId: string;
|
||||||
|
invitationId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const deleteDocInvitation = async ({
|
||||||
|
docId,
|
||||||
|
invitationId,
|
||||||
|
}: DeleteDocInvitationProps): Promise<void> => {
|
||||||
|
const response = await fetchAPI(
|
||||||
|
`documents/${docId}/invitations/${invitationId}/`,
|
||||||
|
{
|
||||||
|
method: 'DELETE',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new APIError(
|
||||||
|
'Failed to delete the invitation',
|
||||||
|
await errorCauses(response),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
type UseDeleteDocInvitationOptions = UseMutationOptions<
|
||||||
|
void,
|
||||||
|
APIError,
|
||||||
|
DeleteDocInvitationProps
|
||||||
|
>;
|
||||||
|
|
||||||
|
export const useDeleteDocInvitation = (
|
||||||
|
options?: UseDeleteDocInvitationOptions,
|
||||||
|
) => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation<void, APIError, DeleteDocInvitationProps>({
|
||||||
|
mutationFn: deleteDocInvitation,
|
||||||
|
...options,
|
||||||
|
onSuccess: (data, variables, context) => {
|
||||||
|
void queryClient.invalidateQueries({
|
||||||
|
queryKey: [KEY_LIST_DOC_INVITATIONS],
|
||||||
|
});
|
||||||
|
if (options?.onSuccess) {
|
||||||
|
options.onSuccess(data, variables, context);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onError: (error, variables, context) => {
|
||||||
|
if (options?.onError) {
|
||||||
|
options.onError(error, variables, context);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
import {
|
||||||
|
DefinedInitialDataInfiniteOptions,
|
||||||
|
InfiniteData,
|
||||||
|
QueryKey,
|
||||||
|
UseQueryOptions,
|
||||||
|
useInfiniteQuery,
|
||||||
|
useQuery,
|
||||||
|
} from '@tanstack/react-query';
|
||||||
|
|
||||||
|
import { APIError, APIList, errorCauses, fetchAPI } from '@/api';
|
||||||
|
|
||||||
|
import { Invitation } from '../types';
|
||||||
|
|
||||||
|
export type DocInvitationsParams = {
|
||||||
|
docId: string;
|
||||||
|
ordering?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DocInvitationsAPIParams = DocInvitationsParams & {
|
||||||
|
page: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
type DocInvitationsResponse = APIList<Invitation>;
|
||||||
|
|
||||||
|
export const getDocInvitations = async ({
|
||||||
|
page,
|
||||||
|
docId,
|
||||||
|
ordering,
|
||||||
|
}: DocInvitationsAPIParams): Promise<DocInvitationsResponse> => {
|
||||||
|
let url = `documents/${docId}/invitations/?page=${page}`;
|
||||||
|
|
||||||
|
if (ordering) {
|
||||||
|
url += '&ordering=' + ordering;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetchAPI(url);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new APIError(
|
||||||
|
'Failed to get the doc accesses',
|
||||||
|
await errorCauses(response),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.json() as Promise<DocInvitationsResponse>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const KEY_LIST_DOC_INVITATIONS = 'docs-invitations';
|
||||||
|
|
||||||
|
export function useDocInvitations(
|
||||||
|
params: DocInvitationsAPIParams,
|
||||||
|
queryConfig?: UseQueryOptions<
|
||||||
|
DocInvitationsResponse,
|
||||||
|
APIError,
|
||||||
|
DocInvitationsResponse
|
||||||
|
>,
|
||||||
|
) {
|
||||||
|
return useQuery<DocInvitationsResponse, APIError, DocInvitationsResponse>({
|
||||||
|
queryKey: [KEY_LIST_DOC_INVITATIONS, params],
|
||||||
|
queryFn: () => getDocInvitations(params),
|
||||||
|
...queryConfig,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param param Used for infinite scroll pagination
|
||||||
|
* @param queryConfig
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export function useDocInvitationsInfinite(
|
||||||
|
param: DocInvitationsParams,
|
||||||
|
queryConfig?: DefinedInitialDataInfiniteOptions<
|
||||||
|
DocInvitationsResponse,
|
||||||
|
APIError,
|
||||||
|
InfiniteData<DocInvitationsResponse>,
|
||||||
|
QueryKey,
|
||||||
|
number
|
||||||
|
>,
|
||||||
|
) {
|
||||||
|
return useInfiniteQuery<
|
||||||
|
DocInvitationsResponse,
|
||||||
|
APIError,
|
||||||
|
InfiniteData<DocInvitationsResponse>,
|
||||||
|
QueryKey,
|
||||||
|
number
|
||||||
|
>({
|
||||||
|
initialPageParam: 1,
|
||||||
|
queryKey: [KEY_LIST_DOC_INVITATIONS, param],
|
||||||
|
queryFn: ({ pageParam }) =>
|
||||||
|
getDocInvitations({
|
||||||
|
...param,
|
||||||
|
page: pageParam,
|
||||||
|
}),
|
||||||
|
getNextPageParam(lastPage, allPages) {
|
||||||
|
return lastPage.next ? allPages.length + 1 : undefined;
|
||||||
|
},
|
||||||
|
...queryConfig,
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
import {
|
||||||
|
UseMutationOptions,
|
||||||
|
useMutation,
|
||||||
|
useQueryClient,
|
||||||
|
} from '@tanstack/react-query';
|
||||||
|
|
||||||
|
import { APIError, errorCauses, fetchAPI } from '@/api';
|
||||||
|
import { Role } from '@/features/docs/doc-management';
|
||||||
|
|
||||||
|
import { Invitation } from '../types';
|
||||||
|
|
||||||
|
import { KEY_LIST_DOC_INVITATIONS } from './useDocInvitations';
|
||||||
|
|
||||||
|
interface UpdateDocInvitationProps {
|
||||||
|
docId: string;
|
||||||
|
invitationId: string;
|
||||||
|
role: Role;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const updateDocInvitation = async ({
|
||||||
|
docId,
|
||||||
|
invitationId,
|
||||||
|
role,
|
||||||
|
}: UpdateDocInvitationProps): Promise<Invitation> => {
|
||||||
|
const response = await fetchAPI(
|
||||||
|
`documents/${docId}/invitations/${invitationId}/`,
|
||||||
|
{
|
||||||
|
method: 'PATCH',
|
||||||
|
body: JSON.stringify({
|
||||||
|
role,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new APIError('Failed to update role', await errorCauses(response));
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.json() as Promise<Invitation>;
|
||||||
|
};
|
||||||
|
|
||||||
|
type UseUpdateDocInvitation = Partial<Invitation>;
|
||||||
|
|
||||||
|
type UseUpdateDocInvitationOptions = UseMutationOptions<
|
||||||
|
Invitation,
|
||||||
|
APIError,
|
||||||
|
UseUpdateDocInvitation
|
||||||
|
>;
|
||||||
|
|
||||||
|
export const useUpdateDocInvitation = (
|
||||||
|
options?: UseUpdateDocInvitationOptions,
|
||||||
|
) => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation<Invitation, APIError, UpdateDocInvitationProps>({
|
||||||
|
mutationFn: updateDocInvitation,
|
||||||
|
...options,
|
||||||
|
onSuccess: (data, variables, context) => {
|
||||||
|
void queryClient.invalidateQueries({
|
||||||
|
queryKey: [KEY_LIST_DOC_INVITATIONS],
|
||||||
|
});
|
||||||
|
if (options?.onSuccess) {
|
||||||
|
options.onSuccess(data, variables, context);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onError: (error, variables, context) => {
|
||||||
|
if (options?.onError) {
|
||||||
|
options.onError(error, variables, context);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Loader,
|
||||||
|
VariantType,
|
||||||
|
useToastProvider,
|
||||||
|
} from '@openfun/cunningham-react';
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import { Box, IconBG, Text, TextErrors } from '@/components';
|
||||||
|
import { useCunninghamTheme } from '@/cunningham';
|
||||||
|
import { Role } from '@/features/docs/doc-management';
|
||||||
|
import { ChooseRole } from '@/features/docs/members/members-add/';
|
||||||
|
|
||||||
|
import { useDeleteDocInvitation, useUpdateDocInvitation } from '../api';
|
||||||
|
import { Invitation } from '../types';
|
||||||
|
|
||||||
|
interface InvitationItemProps {
|
||||||
|
role: Role;
|
||||||
|
currentRole: Role;
|
||||||
|
invitation: Invitation;
|
||||||
|
docId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const InvitationItem = ({
|
||||||
|
docId,
|
||||||
|
role,
|
||||||
|
invitation,
|
||||||
|
currentRole,
|
||||||
|
}: InvitationItemProps) => {
|
||||||
|
const canDelete = invitation.abilities.destroy;
|
||||||
|
const canUpdate = invitation.abilities.partial_update;
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [localRole, setLocalRole] = useState(role);
|
||||||
|
const { colorsTokens } = useCunninghamTheme();
|
||||||
|
const { toast } = useToastProvider();
|
||||||
|
const { mutate: updateDocInvitation, error: errorUpdate } =
|
||||||
|
useUpdateDocInvitation({
|
||||||
|
onSuccess: () => {
|
||||||
|
toast(t('The role has been updated.'), VariantType.SUCCESS, {
|
||||||
|
duration: 4000,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { mutate: removeDocInvitation, error: errorDelete } =
|
||||||
|
useDeleteDocInvitation({
|
||||||
|
onSuccess: () => {
|
||||||
|
toast(t('The invitation has been removed.'), VariantType.SUCCESS, {
|
||||||
|
duration: 4000,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!invitation.email) {
|
||||||
|
return (
|
||||||
|
<Box className="m-auto">
|
||||||
|
<Loader />
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box $width="100%" $gap="0.7rem">
|
||||||
|
<Box $direction="row" $gap="1rem">
|
||||||
|
<IconBG iconName="account_circle" $size="2rem" />
|
||||||
|
<Box
|
||||||
|
$align="center"
|
||||||
|
$direction="row"
|
||||||
|
$gap="1rem"
|
||||||
|
$justify="space-between"
|
||||||
|
$width="100%"
|
||||||
|
$wrap="wrap"
|
||||||
|
>
|
||||||
|
<Box>
|
||||||
|
<Text
|
||||||
|
$size="t"
|
||||||
|
$background={colorsTokens()['info-600']}
|
||||||
|
$color="white"
|
||||||
|
$radius="2px"
|
||||||
|
$padding="xtiny"
|
||||||
|
$css="align-self: flex-start;"
|
||||||
|
>
|
||||||
|
{t('Invited')}
|
||||||
|
</Text>
|
||||||
|
<Text $justify="center">{invitation.email}</Text>
|
||||||
|
</Box>
|
||||||
|
<Box $direction="row" $gap="1rem" $align="center">
|
||||||
|
<Box $minWidth="13rem">
|
||||||
|
<ChooseRole
|
||||||
|
label={t('Role')}
|
||||||
|
defaultRole={localRole}
|
||||||
|
currentRole={currentRole}
|
||||||
|
disabled={!canUpdate}
|
||||||
|
setRole={(role) => {
|
||||||
|
setLocalRole(role);
|
||||||
|
updateDocInvitation({
|
||||||
|
docId,
|
||||||
|
invitationId: invitation.id,
|
||||||
|
role,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
<Button
|
||||||
|
color="tertiary-text"
|
||||||
|
icon={
|
||||||
|
<Text
|
||||||
|
$isMaterialIcon
|
||||||
|
$theme={!canDelete ? 'greyscale' : 'primary'}
|
||||||
|
$variation={!canDelete ? '500' : 'text'}
|
||||||
|
>
|
||||||
|
delete
|
||||||
|
</Text>
|
||||||
|
}
|
||||||
|
disabled={!canDelete}
|
||||||
|
onClick={() =>
|
||||||
|
removeDocInvitation({ docId, invitationId: invitation.id })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
{(errorUpdate || errorDelete) && (
|
||||||
|
<TextErrors causes={errorUpdate?.cause || errorDelete?.cause} />
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
import { Loader } from '@openfun/cunningham-react';
|
||||||
|
import React, { useMemo, useRef } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import { APIError } from '@/api';
|
||||||
|
import { Box, Card, InfiniteScroll, TextErrors } from '@/components';
|
||||||
|
import { useCunninghamTheme } from '@/cunningham';
|
||||||
|
import { Doc, currentDocRole } from '@/features/docs/doc-management';
|
||||||
|
|
||||||
|
import { useDocInvitationsInfinite } from '../api';
|
||||||
|
import { Invitation } from '../types';
|
||||||
|
|
||||||
|
import { InvitationItem } from './InvitationItem';
|
||||||
|
|
||||||
|
interface InvitationListStateProps {
|
||||||
|
isLoading: boolean;
|
||||||
|
error: APIError | null;
|
||||||
|
invitations?: Invitation[];
|
||||||
|
doc: Doc;
|
||||||
|
}
|
||||||
|
|
||||||
|
const InvitationListState = ({
|
||||||
|
invitations,
|
||||||
|
error,
|
||||||
|
isLoading,
|
||||||
|
doc,
|
||||||
|
}: InvitationListStateProps) => {
|
||||||
|
const { colorsTokens } = useCunninghamTheme();
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return <TextErrors causes={error.cause} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isLoading || !invitations) {
|
||||||
|
return (
|
||||||
|
<Box $align="center" className="m-l">
|
||||||
|
<Loader />
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return invitations?.map((invitation, index) => {
|
||||||
|
if (!invitation.email) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
key={`${invitation.id}-${index}`}
|
||||||
|
$background={!(index % 2) ? 'white' : colorsTokens()['greyscale-000']}
|
||||||
|
$direction="row"
|
||||||
|
$padding="small"
|
||||||
|
$align="center"
|
||||||
|
$gap="1rem"
|
||||||
|
$radius="4px"
|
||||||
|
as="li"
|
||||||
|
>
|
||||||
|
<InvitationItem
|
||||||
|
invitation={invitation}
|
||||||
|
role={invitation.role}
|
||||||
|
docId={doc.id}
|
||||||
|
currentRole={currentDocRole(doc.abilities)}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
interface InvitationListProps {
|
||||||
|
doc: Doc;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const InvitationList = ({ doc }: InvitationListProps) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const {
|
||||||
|
data,
|
||||||
|
isLoading,
|
||||||
|
error,
|
||||||
|
fetchNextPage,
|
||||||
|
hasNextPage,
|
||||||
|
isFetchingNextPage,
|
||||||
|
} = useDocInvitationsInfinite({
|
||||||
|
docId: doc.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
const invitations = useMemo(() => {
|
||||||
|
return data?.pages.reduce((acc, page) => {
|
||||||
|
return acc.concat(page.results);
|
||||||
|
}, [] as Invitation[]);
|
||||||
|
}, [data?.pages]);
|
||||||
|
|
||||||
|
if (!invitations?.length) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
$margin="tiny"
|
||||||
|
$padding="tiny"
|
||||||
|
$maxHeight="60%"
|
||||||
|
$overflow="auto"
|
||||||
|
aria-label={t('List invitation card')}
|
||||||
|
>
|
||||||
|
<Box ref={containerRef} $overflow="auto">
|
||||||
|
<InfiniteScroll
|
||||||
|
hasMore={hasNextPage}
|
||||||
|
isLoading={isFetchingNextPage}
|
||||||
|
next={() => {
|
||||||
|
void fetchNextPage();
|
||||||
|
}}
|
||||||
|
scrollContainer={containerRef.current}
|
||||||
|
as="ul"
|
||||||
|
className="p-0 mt-0"
|
||||||
|
role="listbox"
|
||||||
|
>
|
||||||
|
<InvitationListState
|
||||||
|
isLoading={isLoading}
|
||||||
|
error={error}
|
||||||
|
invitations={invitations}
|
||||||
|
doc={doc}
|
||||||
|
/>
|
||||||
|
</InfiniteScroll>
|
||||||
|
</Box>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from './InvitationList';
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export const PAGE_SIZE = 20;
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export * from './api';
|
||||||
|
export * from './components';
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import { Role } from '@/features/docs/doc-management';
|
||||||
|
|
||||||
|
export interface Invitation {
|
||||||
|
id: string;
|
||||||
|
role: Role;
|
||||||
|
document: string;
|
||||||
|
created_at: string;
|
||||||
|
is_expired: boolean;
|
||||||
|
issuer: string;
|
||||||
|
email: string;
|
||||||
|
abilities: {
|
||||||
|
destroy: boolean;
|
||||||
|
retrieve: boolean;
|
||||||
|
partial_update: boolean;
|
||||||
|
update: boolean;
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
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 { Doc, Role } from '@/features/docs/doc-management';
|
import { Doc, Role } from '@/features/docs/doc-management';
|
||||||
import { ContentLanguage } from '@/i18n/types';
|
import { ContentLanguage } from '@/i18n/types';
|
||||||
|
|
||||||
|
import { KEY_LIST_DOC_INVITATIONS } from '../../invitation-list';
|
||||||
import { DocInvitation, OptionType } from '../types';
|
import { DocInvitation, OptionType } from '../types';
|
||||||
|
|
||||||
interface CreateDocInvitationParams {
|
interface CreateDocInvitationParams {
|
||||||
@@ -45,7 +46,13 @@ export const createDocInvitation = async ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
export function useCreateInvitation() {
|
export function useCreateInvitation() {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
return useMutation<DocInvitation, APIError, CreateDocInvitationParams>({
|
return useMutation<DocInvitation, APIError, CreateDocInvitationParams>({
|
||||||
mutationFn: createDocInvitation,
|
mutationFn: createDocInvitation,
|
||||||
|
onSuccess: () => {
|
||||||
|
void queryClient.invalidateQueries({
|
||||||
|
queryKey: [KEY_LIST_DOC_INVITATIONS],
|
||||||
|
});
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user