✨(frontend) add access request on doc share modal
Add the access request to the document share modal, allowing admin to see and manage access requests directly from the modal interface.
This commit is contained in:
committed by
Manuel Raynaud
parent
411d52c73b
commit
2360a832af
@@ -6,11 +6,19 @@ import {
|
||||
useQueryClient,
|
||||
} from '@tanstack/react-query';
|
||||
|
||||
import { APIError, APIList, errorCauses, fetchAPI } from '@/api';
|
||||
import {
|
||||
APIError,
|
||||
APIList,
|
||||
errorCauses,
|
||||
fetchAPI,
|
||||
useAPIInfiniteQuery,
|
||||
} from '@/api';
|
||||
import { AccessRequest, Doc, Role } from '@/docs/doc-management';
|
||||
|
||||
import { OptionType } from '../types';
|
||||
|
||||
import { KEY_LIST_DOC_ACCESSES } from './useDocAccesses';
|
||||
|
||||
interface CreateDocAccessRequestParams {
|
||||
docId: Doc['id'];
|
||||
role?: Role;
|
||||
@@ -19,7 +27,7 @@ interface CreateDocAccessRequestParams {
|
||||
export const createDocAccessRequest = async ({
|
||||
docId,
|
||||
role,
|
||||
}: CreateDocAccessRequestParams): Promise<null> => {
|
||||
}: CreateDocAccessRequestParams): Promise<void> => {
|
||||
const response = await fetchAPI(`documents/${docId}/ask-for-access/`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
@@ -35,12 +43,10 @@ export const createDocAccessRequest = async ({
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
type UseCreateDocAccessRequestOptions = UseMutationOptions<
|
||||
null,
|
||||
void,
|
||||
APIError,
|
||||
CreateDocAccessRequestParams
|
||||
>;
|
||||
@@ -50,7 +56,7 @@ export function useCreateDocAccessRequest(
|
||||
) {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<null, APIError, CreateDocAccessRequestParams>({
|
||||
return useMutation<void, APIError, CreateDocAccessRequestParams>({
|
||||
mutationFn: createDocAccessRequest,
|
||||
...options,
|
||||
onSuccess: (data, variables, context) => {
|
||||
@@ -65,14 +71,21 @@ export function useCreateDocAccessRequest(
|
||||
|
||||
type AccessRequestResponse = APIList<AccessRequest>;
|
||||
|
||||
interface GetDocAccessRequestsParams {
|
||||
interface DocAccessRequestsParams {
|
||||
docId: Doc['id'];
|
||||
}
|
||||
|
||||
export type DocAccessRequestsAPIParams = DocAccessRequestsParams & {
|
||||
page: number;
|
||||
};
|
||||
|
||||
export const getDocAccessRequests = async ({
|
||||
docId,
|
||||
}: GetDocAccessRequestsParams): Promise<AccessRequestResponse> => {
|
||||
const response = await fetchAPI(`documents/${docId}/ask-for-access/`);
|
||||
page,
|
||||
}: DocAccessRequestsAPIParams): Promise<AccessRequestResponse> => {
|
||||
const response = await fetchAPI(
|
||||
`documents/${docId}/ask-for-access/?page=${page}`,
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new APIError(
|
||||
@@ -87,7 +100,7 @@ export const getDocAccessRequests = async ({
|
||||
export const KEY_LIST_DOC_ACCESS_REQUESTS = 'docs-access-requests';
|
||||
|
||||
export function useDocAccessRequests(
|
||||
params: GetDocAccessRequestsParams,
|
||||
params: DocAccessRequestsAPIParams,
|
||||
queryConfig?: UseQueryOptions<
|
||||
AccessRequestResponse,
|
||||
APIError,
|
||||
@@ -100,3 +113,124 @@ export function useDocAccessRequests(
|
||||
...queryConfig,
|
||||
});
|
||||
}
|
||||
|
||||
export const useDocAccessRequestsInfinite = (
|
||||
params: DocAccessRequestsParams,
|
||||
) => {
|
||||
return useAPIInfiniteQuery(
|
||||
KEY_LIST_DOC_ACCESS_REQUESTS,
|
||||
getDocAccessRequests,
|
||||
params,
|
||||
);
|
||||
};
|
||||
|
||||
interface acceptDocAccessRequestsParams {
|
||||
docId: string;
|
||||
accessRequestId: string;
|
||||
role: Role;
|
||||
}
|
||||
|
||||
export const acceptDocAccessRequests = async ({
|
||||
docId,
|
||||
accessRequestId,
|
||||
role,
|
||||
}: acceptDocAccessRequestsParams): Promise<void> => {
|
||||
const response = await fetchAPI(
|
||||
`documents/${docId}/ask-for-access/${accessRequestId}/accept/`,
|
||||
{
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
role,
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new APIError(
|
||||
'Failed to accept the access request',
|
||||
await errorCauses(response),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
type UseAcceptDocAccessRequests = Partial<AccessRequest>;
|
||||
|
||||
type UseAcceptDocAccessRequestsOptions = UseMutationOptions<
|
||||
void,
|
||||
APIError,
|
||||
UseAcceptDocAccessRequests
|
||||
>;
|
||||
|
||||
export const useAcceptDocAccessRequest = (
|
||||
options?: UseAcceptDocAccessRequestsOptions,
|
||||
) => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<void, APIError, acceptDocAccessRequestsParams>({
|
||||
mutationFn: acceptDocAccessRequests,
|
||||
...options,
|
||||
onSuccess: (data, variables, context) => {
|
||||
void queryClient.invalidateQueries({
|
||||
queryKey: [KEY_LIST_DOC_ACCESSES],
|
||||
});
|
||||
|
||||
void queryClient.invalidateQueries({
|
||||
queryKey: [KEY_LIST_DOC_ACCESS_REQUESTS],
|
||||
});
|
||||
|
||||
if (options?.onSuccess) {
|
||||
void options.onSuccess(data, variables, context);
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
interface DeleteDocAccessRequestParams {
|
||||
docId: string;
|
||||
accessRequestId: string;
|
||||
}
|
||||
|
||||
export const deleteDocAccessRequest = async ({
|
||||
docId,
|
||||
accessRequestId,
|
||||
}: DeleteDocAccessRequestParams): Promise<void> => {
|
||||
const response = await fetchAPI(
|
||||
`documents/${docId}/ask-for-access/${accessRequestId}/`,
|
||||
{
|
||||
method: 'DELETE',
|
||||
},
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new APIError(
|
||||
'Failed to delete the access request',
|
||||
await errorCauses(response),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
type UseDeleteDocAccessRequestOptions = UseMutationOptions<
|
||||
void,
|
||||
APIError,
|
||||
DeleteDocAccessRequestParams
|
||||
>;
|
||||
|
||||
export const useDeleteDocAccessRequest = (
|
||||
options?: UseDeleteDocAccessRequestOptions,
|
||||
) => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<void, APIError, DeleteDocAccessRequestParams>({
|
||||
mutationFn: deleteDocAccessRequest,
|
||||
...options,
|
||||
onSuccess: (data, variables, context) => {
|
||||
void queryClient.invalidateQueries({
|
||||
queryKey: [KEY_LIST_DOC_ACCESS_REQUESTS],
|
||||
});
|
||||
|
||||
if (options?.onSuccess) {
|
||||
void options.onSuccess(data, variables, context);
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
import {
|
||||
Button,
|
||||
VariantType,
|
||||
useToastProvider,
|
||||
} from '@openfun/cunningham-react';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { createGlobalStyle } from 'styled-components';
|
||||
|
||||
import { Box, BoxButton, Icon, LoadMoreText } from '@/components';
|
||||
import { QuickSearchData, QuickSearchGroup } from '@/components/quick-search';
|
||||
import { useCunninghamTheme } from '@/cunningham';
|
||||
import { AccessRequest, Doc } from '@/docs/doc-management/';
|
||||
|
||||
import {
|
||||
useAcceptDocAccessRequest,
|
||||
useDeleteDocAccessRequest,
|
||||
useDocAccessRequestsInfinite,
|
||||
} from '../api/useDocAccessRequest';
|
||||
|
||||
import { DocRoleDropdown } from './DocRoleDropdown';
|
||||
import { SearchUserRow } from './SearchUserRow';
|
||||
|
||||
const QuickSearchGroupAccessRequestStyle = createGlobalStyle`
|
||||
.--docs--share-access-request [cmdk-item][data-selected='true'] {
|
||||
background: inherit
|
||||
}
|
||||
`;
|
||||
|
||||
type Props = {
|
||||
doc: Doc;
|
||||
accessRequest: AccessRequest;
|
||||
};
|
||||
|
||||
const DocShareAccessRequestItem = ({ doc, accessRequest }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const { toast } = useToastProvider();
|
||||
const { spacingsTokens } = useCunninghamTheme();
|
||||
const { mutate: acceptDocAccessRequests } = useAcceptDocAccessRequest();
|
||||
const [role, setRole] = useState(accessRequest.role);
|
||||
|
||||
const { mutate: removeDocAccess } = useDeleteDocAccessRequest({
|
||||
onError: () => {
|
||||
toast(t('Error while removing the request.'), VariantType.ERROR, {
|
||||
duration: 4000,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
if (!doc.abilities.accesses_view) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Box
|
||||
$width="100%"
|
||||
data-testid={`doc-share-access-request-row-${accessRequest.user.email}`}
|
||||
className="--docs--doc-share-access-request-item"
|
||||
>
|
||||
<SearchUserRow
|
||||
alwaysShowRight={true}
|
||||
user={accessRequest.user}
|
||||
right={
|
||||
<Box $direction="row" $align="center" $gap={spacingsTokens['sm']}>
|
||||
<DocRoleDropdown
|
||||
currentRole={role}
|
||||
onSelectRole={setRole}
|
||||
canUpdate={doc.abilities.accesses_manage}
|
||||
/>
|
||||
<Button
|
||||
color="tertiary"
|
||||
onClick={() =>
|
||||
acceptDocAccessRequests({
|
||||
docId: doc.id,
|
||||
accessRequestId: accessRequest.id,
|
||||
role,
|
||||
})
|
||||
}
|
||||
size="small"
|
||||
>
|
||||
{t('Approve')}
|
||||
</Button>
|
||||
|
||||
{doc.abilities.accesses_manage && (
|
||||
<BoxButton
|
||||
onClick={() =>
|
||||
removeDocAccess({
|
||||
accessRequestId: accessRequest.id,
|
||||
docId: doc.id,
|
||||
})
|
||||
}
|
||||
>
|
||||
<Icon iconName="close" $variation="600" $size="16px" />
|
||||
</BoxButton>
|
||||
)}
|
||||
</Box>
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
interface QuickSearchGroupAccessRequestProps {
|
||||
doc: Doc;
|
||||
}
|
||||
|
||||
export const QuickSearchGroupAccessRequest = ({
|
||||
doc,
|
||||
}: QuickSearchGroupAccessRequestProps) => {
|
||||
const { t } = useTranslation();
|
||||
const accessRequestQuery = useDocAccessRequestsInfinite({ docId: doc.id });
|
||||
|
||||
const accessRequestsData: QuickSearchData<AccessRequest> = useMemo(() => {
|
||||
const accessRequests =
|
||||
accessRequestQuery.data?.pages.flatMap((page) => page.results) || [];
|
||||
|
||||
return {
|
||||
groupName: t('Access Requests'),
|
||||
elements: accessRequests,
|
||||
endActions: accessRequestQuery.hasNextPage
|
||||
? [
|
||||
{
|
||||
content: <LoadMoreText data-testid="load-more-requests" />,
|
||||
onSelect: () => void accessRequestQuery.fetchNextPage(),
|
||||
},
|
||||
]
|
||||
: undefined,
|
||||
};
|
||||
}, [accessRequestQuery, t]);
|
||||
|
||||
if (!accessRequestsData.elements.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Box
|
||||
aria-label={t('List request access card')}
|
||||
className="--docs--share-access-request"
|
||||
>
|
||||
<QuickSearchGroupAccessRequestStyle />
|
||||
<QuickSearchGroup
|
||||
group={accessRequestsData}
|
||||
renderElement={(accessRequest) => (
|
||||
<DocShareAccessRequestItem doc={doc} accessRequest={accessRequest} />
|
||||
)}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -17,6 +17,7 @@ import { isValidEmail } from '@/utils';
|
||||
|
||||
import { KEY_LIST_USER, useUsers } from '../api';
|
||||
|
||||
import { QuickSearchGroupAccessRequest } from './DocShareAccessRequest';
|
||||
import { DocShareAddMemberList } from './DocShareAddMemberList';
|
||||
import {
|
||||
DocShareModalInviteUserRow,
|
||||
@@ -26,6 +27,9 @@ import { QuickSearchGroupMember } from './DocShareMember';
|
||||
import { DocShareModalFooter } from './DocShareModalFooter';
|
||||
|
||||
const ShareModalStyle = createGlobalStyle`
|
||||
.--docs--doc-share-modal [cmdk-item] {
|
||||
cursor: auto;
|
||||
}
|
||||
.c__modal__title {
|
||||
padding-bottom: 0 !important;
|
||||
}
|
||||
@@ -173,6 +177,7 @@ export const DocShareModal = ({ doc, onClose }: Props) => {
|
||||
>
|
||||
{showMemberSection ? (
|
||||
<>
|
||||
<QuickSearchGroupAccessRequest doc={doc} />
|
||||
<QuickSearchGroupInvitation doc={doc} />
|
||||
<QuickSearchGroupMember doc={doc} />
|
||||
</>
|
||||
|
||||
Reference in New Issue
Block a user