Skip to content

Commit 1306d3b

Browse files
committed
✨(frontend) integrate doc access request
When a user is redirected on the 403 page, they can now request access to the document.
1 parent 8376011 commit 1306d3b

File tree

7 files changed

+266
-3
lines changed

7 files changed

+266
-3
lines changed

src/frontend/apps/impress/cunningham.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,16 @@ tokens.themes.default.components = {
6565
'png-light': '/assets/favicon-light.png',
6666
'png-dark': '/assets/favicon-dark.png',
6767
},
68+
button: {
69+
...tokens.themes.default.components.button,
70+
primary: {
71+
...tokens.themes.default.components.button.primary,
72+
...{
73+
'background--disabled': 'var(--c--theme--colors--greyscale-100)',
74+
},
75+
disabled: 'var(--c--theme--colors--greyscale-400)',
76+
},
77+
},
6878
},
6979
};
7080

src/frontend/apps/impress/src/cunningham/cunningham-tokens.css

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -218,7 +218,10 @@
218218
--c--components--button--primary--color-active: #fff;
219219
--c--components--button--primary--color-focus-visible: #fff;
220220
--c--components--button--primary--disabled: var(
221-
--c--theme--colors--greyscale-500
221+
--c--theme--colors--greyscale-400
222+
);
223+
--c--components--button--primary--background--disabled: var(
224+
--c--theme--colors--greyscale-100
222225
);
223226
--c--components--button--primary-text--background--color: var(
224227
--c--theme--colors--primary-text

src/frontend/apps/impress/src/cunningham/cunningham-tokens.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -229,7 +229,8 @@ export const tokens = {
229229
'color-hover': '#fff',
230230
'color-active': '#fff',
231231
'color-focus-visible': '#fff',
232-
disabled: '#7C7C7C',
232+
disabled: 'var(--c--theme--colors--greyscale-400)',
233+
'background--disabled': 'var(--c--theme--colors--greyscale-100)',
233234
},
234235
'primary-text': {
235236
'background--color': '#000091',

src/frontend/apps/impress/src/features/docs/doc-management/types.tsx

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,3 +76,18 @@ export enum DocDefaultFilter {
7676
MY_DOCS = 'my_docs',
7777
SHARED_WITH_ME = 'shared_with_me',
7878
}
79+
80+
export interface AccessRequest {
81+
id: string;
82+
document: string;
83+
user: string;
84+
role: Role;
85+
created_at: string;
86+
abilities: {
87+
destroy: boolean;
88+
update: boolean;
89+
partial_update: boolean;
90+
retrieve: boolean;
91+
accept: boolean;
92+
};
93+
}
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import {
2+
UseMutationOptions,
3+
UseQueryOptions,
4+
useMutation,
5+
useQuery,
6+
useQueryClient,
7+
} from '@tanstack/react-query';
8+
9+
import { APIError, APIList, errorCauses, fetchAPI } from '@/api';
10+
import { AccessRequest, Doc, Role } from '@/docs/doc-management';
11+
12+
import { OptionType } from '../types';
13+
14+
interface CreateDocAccessRequestParams {
15+
docId: Doc['id'];
16+
role?: Role;
17+
}
18+
19+
export const createDocAccessRequest = async ({
20+
docId,
21+
role,
22+
}: CreateDocAccessRequestParams): Promise<null> => {
23+
const response = await fetchAPI(`documents/${docId}/ask-for-access/`, {
24+
method: 'POST',
25+
body: JSON.stringify({
26+
role,
27+
}),
28+
});
29+
30+
if (!response.ok) {
31+
throw new APIError(
32+
`Failed to create a request to access to the doc.`,
33+
await errorCauses(response, {
34+
type: OptionType.NEW_MEMBER,
35+
}),
36+
);
37+
}
38+
39+
return null;
40+
};
41+
42+
type UseCreateDocAccessRequestOptions = UseMutationOptions<
43+
null,
44+
APIError,
45+
CreateDocAccessRequestParams
46+
>;
47+
48+
export function useCreateDocAccessRequest(
49+
options?: UseCreateDocAccessRequestOptions,
50+
) {
51+
const queryClient = useQueryClient();
52+
53+
return useMutation<null, APIError, CreateDocAccessRequestParams>({
54+
mutationFn: createDocAccessRequest,
55+
...options,
56+
onSuccess: (data, variables, context) => {
57+
void queryClient.resetQueries({
58+
queryKey: [KEY_LIST_DOC_ACCESS_REQUESTS],
59+
});
60+
61+
void options?.onSuccess?.(data, variables, context);
62+
},
63+
});
64+
}
65+
66+
type AccessRequestResponse = APIList<AccessRequest>;
67+
68+
export const getDocAccessRequests = async (
69+
docId: Doc['id'],
70+
): Promise<AccessRequestResponse> => {
71+
const response = await fetchAPI(`documents/${docId}/ask-for-access/`);
72+
73+
if (!response.ok) {
74+
throw new APIError(
75+
'Failed to get the doc access requests',
76+
await errorCauses(response),
77+
);
78+
}
79+
80+
return response.json() as Promise<AccessRequestResponse>;
81+
};
82+
83+
export const KEY_LIST_DOC_ACCESS_REQUESTS = 'docs-access-requests';
84+
85+
export function useDocAccessRequests(
86+
params: Doc['id'],
87+
queryConfig?: UseQueryOptions<
88+
AccessRequestResponse,
89+
APIError,
90+
AccessRequestResponse
91+
>,
92+
) {
93+
return useQuery<AccessRequestResponse, APIError, AccessRequestResponse>({
94+
queryKey: [KEY_LIST_DOC_ACCESS_REQUESTS, params],
95+
queryFn: () => getDocAccessRequests(params),
96+
...queryConfig,
97+
});
98+
}
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
import {
2+
Button,
3+
VariantType,
4+
useToastProvider,
5+
} from '@openfun/cunningham-react';
6+
import Head from 'next/head';
7+
import Image from 'next/image';
8+
import { useRouter } from 'next/router';
9+
import { useTranslation } from 'react-i18next';
10+
import styled from 'styled-components';
11+
12+
import img403 from '@/assets/icons/icon-403.png';
13+
import { Box, Icon, StyledLink, Text } from '@/components';
14+
import {
15+
useCreateDocAccessRequest,
16+
useDocAccessRequests,
17+
} from '@/features/docs/doc-share/api/useDocAccessRequest';
18+
import { MainLayout } from '@/layouts';
19+
import { NextPageWithLayout } from '@/types/next';
20+
21+
const StyledButton = styled(Button)`
22+
width: fit-content;
23+
`;
24+
25+
export function DocLayout() {
26+
const {
27+
query: { id },
28+
} = useRouter();
29+
30+
if (typeof id !== 'string') {
31+
return null;
32+
}
33+
34+
return (
35+
<>
36+
<Head>
37+
<meta name="robots" content="noindex" />
38+
</Head>
39+
40+
<MainLayout>
41+
<DocPage403 id={id} />
42+
</MainLayout>
43+
</>
44+
);
45+
}
46+
47+
interface DocProps {
48+
id: string;
49+
}
50+
51+
const DocPage403 = ({ id }: DocProps) => {
52+
const { t } = useTranslation();
53+
const { data: requests } = useDocAccessRequests(id);
54+
const { toast } = useToastProvider();
55+
const { mutate: createRequest } = useCreateDocAccessRequest({
56+
onSuccess: () => {
57+
toast(t('Access request sent successfully.'), VariantType.SUCCESS, {
58+
duration: 3000,
59+
});
60+
},
61+
});
62+
63+
const hasRequested = !!requests?.results.find(
64+
(request) => request.document === id,
65+
);
66+
67+
return (
68+
<>
69+
<Head>
70+
<title>
71+
{t('Access Denied - Error 403')} - {t('Docs')}
72+
</title>
73+
<meta
74+
property="og:title"
75+
content={`${t('Access Denied - Error 403')} - ${t('Docs')}`}
76+
key="title"
77+
/>
78+
</Head>
79+
<Box
80+
$align="center"
81+
$margin="auto"
82+
$gap="1rem"
83+
$padding={{ bottom: '2rem' }}
84+
>
85+
<Image
86+
className="c__image-system-filter"
87+
src={img403}
88+
alt={t('Image 403')}
89+
style={{
90+
maxWidth: '100%',
91+
height: 'auto',
92+
}}
93+
/>
94+
95+
<Box $align="center" $gap="0.8rem">
96+
<Text as="p" $textAlign="center" $maxWidth="350px" $theme="primary">
97+
{hasRequested
98+
? t('Your access request for this document is pending.')
99+
: t('Insufficient access rights to view the document.')}
100+
</Text>
101+
102+
<Box $direction="row" $gap="0.7rem">
103+
<StyledLink href="/">
104+
<StyledButton
105+
icon={<Icon iconName="house" $theme="primary" />}
106+
color="tertiary"
107+
>
108+
{t('Home')}
109+
</StyledButton>
110+
</StyledLink>
111+
<Button
112+
onClick={() => createRequest({ docId: id })}
113+
disabled={hasRequested}
114+
>
115+
{t('Request access')}
116+
</Button>
117+
</Box>
118+
</Box>
119+
</Box>
120+
</>
121+
);
122+
};
123+
124+
const Page: NextPageWithLayout = () => {
125+
return null;
126+
};
127+
128+
Page.getLayout = function getLayout() {
129+
return <DocLayout />;
130+
};
131+
132+
export default Page;

src/frontend/apps/impress/src/pages/docs/[id]/index.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,8 @@ const DocPage = ({ id }: DocProps) => {
104104

105105
if (isError && error) {
106106
if ([403, 404, 401].includes(error.status)) {
107+
let replacePath = `/${error.status}`;
108+
107109
if (error.status === 401) {
108110
if (authenticated) {
109111
queryClient.setQueryData([KEY_AUTH], {
@@ -112,9 +114,11 @@ const DocPage = ({ id }: DocProps) => {
112114
});
113115
}
114116
setAuthUrl();
117+
} else if (error.status === 403) {
118+
replacePath = `/docs/${id}/403`;
115119
}
116120

117-
void replace(`/${error.status}`);
121+
void replace(replacePath);
118122

119123
return (
120124
<Box $align="center" $justify="center" $height="100%">

0 commit comments

Comments
 (0)