diff --git a/src/frontend/apps/impress/cunningham.ts b/src/frontend/apps/impress/cunningham.ts
index 135370f9..43c337f3 100644
--- a/src/frontend/apps/impress/cunningham.ts
+++ b/src/frontend/apps/impress/cunningham.ts
@@ -64,6 +64,16 @@ tokens.themes.default.components = {
'png-light': '/assets/favicon-light.png',
'png-dark': '/assets/favicon-dark.png',
},
+ button: {
+ ...tokens.themes.default.components.button,
+ primary: {
+ ...tokens.themes.default.components.button.primary,
+ ...{
+ 'background--disabled': 'var(--c--theme--colors--greyscale-100)',
+ },
+ disabled: 'var(--c--theme--colors--greyscale-400)',
+ },
+ },
},
};
diff --git a/src/frontend/apps/impress/src/components/LoadMoreText.tsx b/src/frontend/apps/impress/src/components/Loading.tsx
similarity index 81%
rename from src/frontend/apps/impress/src/components/LoadMoreText.tsx
rename to src/frontend/apps/impress/src/components/Loading.tsx
index 4caa811a..ec688b48 100644
--- a/src/frontend/apps/impress/src/components/LoadMoreText.tsx
+++ b/src/frontend/apps/impress/src/components/Loading.tsx
@@ -1,3 +1,4 @@
+import { Loader } from '@openfun/cunningham-react';
import { useTranslation } from 'react-i18next';
import { Box } from './Box';
@@ -34,3 +35,9 @@ export const LoadMoreText = ({
);
};
+
+export const Loading = () => (
+
+
+
+);
diff --git a/src/frontend/apps/impress/src/components/index.ts b/src/frontend/apps/impress/src/components/index.ts
index 205b7224..c1a1314f 100644
--- a/src/frontend/apps/impress/src/components/index.ts
+++ b/src/frontend/apps/impress/src/components/index.ts
@@ -6,7 +6,7 @@ export * from './DropdownMenu';
export * from './Icon';
export * from './InfiniteScroll';
export * from './Link';
-export * from './LoadMoreText';
+export * from './Loading';
export * from './SideModal';
export * from './separators';
export * from './Text';
diff --git a/src/frontend/apps/impress/src/cunningham/cunningham-tokens.css b/src/frontend/apps/impress/src/cunningham/cunningham-tokens.css
index b1b9df6a..4ab56fa3 100644
--- a/src/frontend/apps/impress/src/cunningham/cunningham-tokens.css
+++ b/src/frontend/apps/impress/src/cunningham/cunningham-tokens.css
@@ -218,7 +218,10 @@
--c--components--button--primary--color-active: #fff;
--c--components--button--primary--color-focus-visible: #fff;
--c--components--button--primary--disabled: var(
- --c--theme--colors--greyscale-500
+ --c--theme--colors--greyscale-400
+ );
+ --c--components--button--primary--background--disabled: var(
+ --c--theme--colors--greyscale-100
);
--c--components--button--primary-text--background--color: var(
--c--theme--colors--primary-text
diff --git a/src/frontend/apps/impress/src/cunningham/cunningham-tokens.ts b/src/frontend/apps/impress/src/cunningham/cunningham-tokens.ts
index 8696aa2f..5b5df690 100644
--- a/src/frontend/apps/impress/src/cunningham/cunningham-tokens.ts
+++ b/src/frontend/apps/impress/src/cunningham/cunningham-tokens.ts
@@ -229,7 +229,8 @@ export const tokens = {
'color-hover': '#fff',
'color-active': '#fff',
'color-focus-visible': '#fff',
- disabled: '#7C7C7C',
+ disabled: 'var(--c--theme--colors--greyscale-400)',
+ 'background--disabled': 'var(--c--theme--colors--greyscale-100)',
},
'primary-text': {
'background--color': '#000091',
diff --git a/src/frontend/apps/impress/src/features/docs/doc-management/types.tsx b/src/frontend/apps/impress/src/features/docs/doc-management/types.tsx
index e57dc6e1..2cc2b6c1 100644
--- a/src/frontend/apps/impress/src/features/docs/doc-management/types.tsx
+++ b/src/frontend/apps/impress/src/features/docs/doc-management/types.tsx
@@ -76,3 +76,18 @@ export enum DocDefaultFilter {
MY_DOCS = 'my_docs',
SHARED_WITH_ME = 'shared_with_me',
}
+
+export interface AccessRequest {
+ id: string;
+ document: string;
+ user: User;
+ role: Role;
+ created_at: string;
+ abilities: {
+ destroy: boolean;
+ update: boolean;
+ partial_update: boolean;
+ retrieve: boolean;
+ accept: boolean;
+ };
+}
diff --git a/src/frontend/apps/impress/src/features/docs/doc-share/api/useDocAccessRequest.tsx b/src/frontend/apps/impress/src/features/docs/doc-share/api/useDocAccessRequest.tsx
new file mode 100644
index 00000000..062edba6
--- /dev/null
+++ b/src/frontend/apps/impress/src/features/docs/doc-share/api/useDocAccessRequest.tsx
@@ -0,0 +1,102 @@
+import {
+ UseMutationOptions,
+ UseQueryOptions,
+ useMutation,
+ useQuery,
+ useQueryClient,
+} from '@tanstack/react-query';
+
+import { APIError, APIList, errorCauses, fetchAPI } from '@/api';
+import { AccessRequest, Doc, Role } from '@/docs/doc-management';
+
+import { OptionType } from '../types';
+
+interface CreateDocAccessRequestParams {
+ docId: Doc['id'];
+ role?: Role;
+}
+
+export const createDocAccessRequest = async ({
+ docId,
+ role,
+}: CreateDocAccessRequestParams): Promise => {
+ const response = await fetchAPI(`documents/${docId}/ask-for-access/`, {
+ method: 'POST',
+ body: JSON.stringify({
+ role,
+ }),
+ });
+
+ if (!response.ok) {
+ throw new APIError(
+ `Failed to create a request to access to the doc.`,
+ await errorCauses(response, {
+ type: OptionType.NEW_MEMBER,
+ }),
+ );
+ }
+
+ return null;
+};
+
+type UseCreateDocAccessRequestOptions = UseMutationOptions<
+ null,
+ APIError,
+ CreateDocAccessRequestParams
+>;
+
+export function useCreateDocAccessRequest(
+ options?: UseCreateDocAccessRequestOptions,
+) {
+ const queryClient = useQueryClient();
+
+ return useMutation({
+ mutationFn: createDocAccessRequest,
+ ...options,
+ onSuccess: (data, variables, context) => {
+ void queryClient.resetQueries({
+ queryKey: [KEY_LIST_DOC_ACCESS_REQUESTS],
+ });
+
+ void options?.onSuccess?.(data, variables, context);
+ },
+ });
+}
+
+type AccessRequestResponse = APIList;
+
+interface GetDocAccessRequestsParams {
+ docId: Doc['id'];
+}
+
+export const getDocAccessRequests = async ({
+ docId,
+}: GetDocAccessRequestsParams): Promise => {
+ const response = await fetchAPI(`documents/${docId}/ask-for-access/`);
+
+ if (!response.ok) {
+ throw new APIError(
+ 'Failed to get the doc access requests',
+ await errorCauses(response),
+ );
+ }
+
+ return response.json() as Promise;
+};
+
+export const KEY_LIST_DOC_ACCESS_REQUESTS = 'docs-access-requests';
+
+export function useDocAccessRequests(
+ params: GetDocAccessRequestsParams,
+ queryConfig?: UseQueryOptions<
+ AccessRequestResponse,
+ APIError,
+ AccessRequestResponse
+ >,
+) {
+ return useQuery({
+ queryKey: [KEY_LIST_DOC_ACCESS_REQUESTS, params],
+ queryFn: () => getDocAccessRequests(params),
+ ...queryConfig,
+ });
+}
diff --git a/src/frontend/apps/impress/src/features/service-worker/service-worker.ts b/src/frontend/apps/impress/src/features/service-worker/service-worker.ts
index 957de160..38701044 100644
--- a/src/frontend/apps/impress/src/features/service-worker/service-worker.ts
+++ b/src/frontend/apps/impress/src/features/service-worker/service-worker.ts
@@ -110,7 +110,6 @@ const precacheResources = [
'/index.html',
'/401/',
'/404/',
- '/403/',
FALLBACK.offline,
FALLBACK.images,
FALLBACK.docs,
diff --git a/src/frontend/apps/impress/src/pages/403.tsx b/src/frontend/apps/impress/src/pages/403.tsx
deleted file mode 100644
index dc5a0fbf..00000000
--- a/src/frontend/apps/impress/src/pages/403.tsx
+++ /dev/null
@@ -1,68 +0,0 @@
-import { Button } from '@openfun/cunningham-react';
-import Head from 'next/head';
-import Image from 'next/image';
-import { ReactElement } from 'react';
-import { useTranslation } from 'react-i18next';
-import styled from 'styled-components';
-
-import img403 from '@/assets/icons/icon-403.png';
-import { Box, Icon, StyledLink, Text } from '@/components';
-import { PageLayout } from '@/layouts';
-import { NextPageWithLayout } from '@/types/next';
-
-const StyledButton = styled(Button)`
- width: fit-content;
-`;
-
-const Page: NextPageWithLayout = () => {
- const { t } = useTranslation();
-
- return (
- <>
-
-
- {t('Access Denied - Error 403')} - {t('Docs')}
-
-
-
-
-
-
-
-
- {t('You do not have permission to view this document.')}
-
-
-
- }>
- {t('Home')}
-
-
-
-
- >
- );
-};
-
-Page.getLayout = function getLayout(page: ReactElement) {
- return {page};
-};
-
-export default Page;
diff --git a/src/frontend/apps/impress/src/pages/docs/[id]/403.tsx b/src/frontend/apps/impress/src/pages/docs/[id]/403.tsx
new file mode 100644
index 00000000..3eb96173
--- /dev/null
+++ b/src/frontend/apps/impress/src/pages/docs/[id]/403.tsx
@@ -0,0 +1,161 @@
+import {
+ Button,
+ VariantType,
+ useToastProvider,
+} from '@openfun/cunningham-react';
+import Head from 'next/head';
+import Image from 'next/image';
+import { useRouter } from 'next/router';
+import { useTranslation } from 'react-i18next';
+import styled from 'styled-components';
+
+import img403 from '@/assets/icons/icon-403.png';
+import { Box, Icon, Loading, StyledLink, Text } from '@/components';
+import { DEFAULT_QUERY_RETRY } from '@/core';
+import { KEY_DOC, useDoc } from '@/features/docs';
+import {
+ useCreateDocAccessRequest,
+ useDocAccessRequests,
+} from '@/features/docs/doc-share/api/useDocAccessRequest';
+import { MainLayout } from '@/layouts';
+import { NextPageWithLayout } from '@/types/next';
+
+const StyledButton = styled(Button)`
+ width: fit-content;
+`;
+
+export function DocLayout() {
+ const {
+ query: { id },
+ } = useRouter();
+
+ if (typeof id !== 'string') {
+ return null;
+ }
+
+ return (
+ <>
+
+
+
+
+
+
+
+ >
+ );
+}
+
+interface DocProps {
+ id: string;
+}
+
+const DocPage403 = ({ id }: DocProps) => {
+ const { t } = useTranslation();
+ const { data: requests, isLoading: isLoadingRequest } = useDocAccessRequests({
+ docId: id,
+ });
+ const { replace } = useRouter();
+ const { toast } = useToastProvider();
+ const { mutate: createRequest } = useCreateDocAccessRequest({
+ onSuccess: () => {
+ toast(t('Access request sent successfully.'), VariantType.SUCCESS, {
+ duration: 3000,
+ });
+ },
+ });
+
+ const hasRequested = !!requests?.results.find(
+ (request) => request.document === id,
+ );
+
+ const { error, isLoading: isLoadingDoc } = useDoc(
+ { id },
+ {
+ staleTime: 0,
+ queryKey: [KEY_DOC, { id }],
+ retry: (failureCount, error) => {
+ if (error.status == 403) {
+ return false;
+ } else {
+ return failureCount < DEFAULT_QUERY_RETRY;
+ }
+ },
+ },
+ );
+
+ if (error?.status !== 403) {
+ void replace(`/docs/${id}`);
+ return ;
+ }
+
+ if (isLoadingDoc || isLoadingRequest) {
+ return ;
+ }
+
+ return (
+ <>
+
+
+ {t('Access Denied - Error 403')} - {t('Docs')}
+
+
+
+
+
+
+
+
+ {hasRequested
+ ? t('Your access request for this document is pending.')
+ : t('Insufficient access rights to view the document.')}
+
+
+
+
+ }
+ color="tertiary"
+ >
+ {t('Home')}
+
+
+
+
+
+
+ >
+ );
+};
+
+const Page: NextPageWithLayout = () => {
+ return null;
+};
+
+Page.getLayout = function getLayout() {
+ return ;
+};
+
+export default Page;
diff --git a/src/frontend/apps/impress/src/pages/docs/[id]/index.tsx b/src/frontend/apps/impress/src/pages/docs/[id]/index.tsx
index da4c10ba..41931518 100644
--- a/src/frontend/apps/impress/src/pages/docs/[id]/index.tsx
+++ b/src/frontend/apps/impress/src/pages/docs/[id]/index.tsx
@@ -1,11 +1,10 @@
-import { Loader } from '@openfun/cunningham-react';
import { useQueryClient } from '@tanstack/react-query';
import Head from 'next/head';
import { useRouter } from 'next/router';
import { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
-import { Box, Icon, TextErrors } from '@/components';
+import { Box, Icon, Loading, TextErrors } from '@/components';
import { DEFAULT_QUERY_RETRY } from '@/core';
import { DocEditor } from '@/docs/doc-editor';
import {
@@ -104,6 +103,8 @@ const DocPage = ({ id }: DocProps) => {
if (isError && error) {
if ([403, 404, 401].includes(error.status)) {
+ let replacePath = `/${error.status}`;
+
if (error.status === 401) {
if (authenticated) {
queryClient.setQueryData([KEY_AUTH], {
@@ -112,15 +113,13 @@ const DocPage = ({ id }: DocProps) => {
});
}
setAuthUrl();
+ } else if (error.status === 403) {
+ replacePath = `/docs/${id}/403`;
}
- void replace(`/${error.status}`);
+ void replace(replacePath);
- return (
-
-
-
- );
+ return ;
}
return (
@@ -138,11 +137,7 @@ const DocPage = ({ id }: DocProps) => {
}
if (!doc) {
- return (
-
-
-
- );
+ return ;
}
return (