Skip to content

Commit 3376146

Browse files
committed
✨(frontend) link to create new doc
We create a special URL to create a new doc, we can set the doc with the URL param to set the visibility, the permission and the title.
1 parent dc2fe49 commit 3376146

7 files changed

Lines changed: 214 additions & 41 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ and this project adheres to
1010

1111
- ✨ Add comments feature to the editor #1330
1212
- ✨(backend) Comments on text editor #1330
13+
- ✨(frontend) link to create new doc #1574
1314

1415
### Changed
1516

‎src/frontend/apps/e2e/__tests__/app-impress/doc-create.spec.ts‎

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,53 @@ test.describe('Doc Create', () => {
7373
page.locator('.c__tree-view--row-content').getByText('Untitled document'),
7474
).toBeVisible();
7575
});
76+
77+
test('it creates a doc with link "/doc/new/', async ({ page }) => {
78+
// Private doc creation
79+
await page.goto('/docs/new/?title=My+private+doc+from+url');
80+
81+
await verifyDocName(page, 'My private doc from url');
82+
83+
await page.getByRole('button', { name: 'Share' }).click();
84+
85+
await expect(
86+
page.getByTestId('doc-visibility').getByText('Private').first(),
87+
).toBeVisible();
88+
89+
// Public editing doc creation
90+
await page.goto(
91+
'/docs/new/?title=My+public+doc+from+url&link-reach=public&link-role=editor',
92+
);
93+
94+
await verifyDocName(page, 'My public doc from url');
95+
96+
await page.getByRole('button', { name: 'Share' }).click();
97+
98+
await expect(
99+
page.getByTestId('doc-visibility').getByText('Public').first(),
100+
).toBeVisible();
101+
102+
await expect(
103+
page.getByTestId('doc-access-mode').getByText('Editing').first(),
104+
).toBeVisible();
105+
106+
// Authenticated reading doc creation
107+
await page.goto(
108+
'/docs/new/?title=My+authenticated+doc+from+url&link-reach=authenticated&link-role=reader',
109+
);
110+
111+
await verifyDocName(page, 'My authenticated doc from url');
112+
113+
await page.getByRole('button', { name: 'Share' }).click();
114+
115+
await expect(
116+
page.getByTestId('doc-visibility').getByText('Connected').first(),
117+
).toBeVisible();
118+
119+
await expect(
120+
page.getByTestId('doc-access-mode').getByText('Reading').first(),
121+
).toBeVisible();
122+
});
76123
});
77124

78125
test.describe('Doc Create: Not logged', () => {
Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,23 @@
1-
import { useMutation, useQueryClient } from '@tanstack/react-query';
1+
import {
2+
UseMutationOptions,
3+
useMutation,
4+
useQueryClient,
5+
} from '@tanstack/react-query';
26

37
import { APIError, errorCauses, fetchAPI } from '@/api';
48

59
import { Doc } from '../types';
610

711
import { KEY_LIST_DOC } from './useDocs';
812

9-
export const createDoc = async (): Promise<Doc> => {
13+
type CreateDocParams = {
14+
title?: string;
15+
} | void;
16+
17+
export const createDoc = async (params: CreateDocParams): Promise<Doc> => {
1018
const response = await fetchAPI(`documents/`, {
1119
method: 'POST',
20+
body: JSON.stringify({ title: params?.title }),
1221
});
1322

1423
if (!response.ok) {
@@ -18,23 +27,17 @@ export const createDoc = async (): Promise<Doc> => {
1827
return response.json() as Promise<Doc>;
1928
};
2029

21-
interface CreateDocProps {
22-
onSuccess: (data: Doc) => void;
23-
onError?: (error: APIError) => void;
24-
}
30+
type UseCreateDocOptions = UseMutationOptions<Doc, APIError, CreateDocParams>;
2531

26-
export function useCreateDoc({ onSuccess, onError }: CreateDocProps) {
32+
export function useCreateDoc(options?: UseCreateDocOptions) {
2733
const queryClient = useQueryClient();
28-
return useMutation<Doc, APIError>({
34+
return useMutation<Doc, APIError, CreateDocParams>({
2935
mutationFn: createDoc,
30-
onSuccess: (data) => {
36+
onSuccess: (data, variables, onMutateResult, context) => {
3137
void queryClient.resetQueries({
3238
queryKey: [KEY_LIST_DOC],
3339
});
34-
onSuccess(data);
35-
},
36-
onError: (error) => {
37-
onError?.(error);
40+
options?.onSuccess?.(data, variables, onMutateResult, context);
3841
},
3942
});
4043
}
Lines changed: 22 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,21 @@
1-
import { VariantType, useToastProvider } from '@openfun/cunningham-react';
2-
import { useMutation, useQueryClient } from '@tanstack/react-query';
3-
import { useTranslation } from 'react-i18next';
1+
import {
2+
UseMutationOptions,
3+
useMutation,
4+
useQueryClient,
5+
} from '@tanstack/react-query';
46

57
import { APIError, errorCauses, fetchAPI } from '@/api';
6-
import { Doc } from '@/docs/doc-management';
8+
import { Doc, LinkReach, LinkRole } from '@/docs/doc-management';
79

810
export type UpdateDocLinkParams = Pick<Doc, 'id' | 'link_reach'> &
911
Partial<Pick<Doc, 'link_role'>>;
1012

13+
type UpdateDocLinkResponse = { link_role: LinkRole; link_reach: LinkReach };
14+
1115
export const updateDocLink = async ({
1216
id,
1317
...params
14-
}: UpdateDocLinkParams): Promise<Doc> => {
18+
}: UpdateDocLinkParams): Promise<UpdateDocLinkResponse> => {
1519
const response = await fetchAPI(`documents/${id}/link-configuration/`, {
1620
method: 'PUT',
1721
body: JSON.stringify({
@@ -26,40 +30,31 @@ export const updateDocLink = async ({
2630
);
2731
}
2832

29-
return response.json() as Promise<Doc>;
33+
return response.json() as Promise<UpdateDocLinkResponse>;
3034
};
3135

32-
interface UpdateDocLinkProps {
33-
onSuccess?: (data: Doc) => void;
36+
type UseUpdateDocLinkOptions = UseMutationOptions<
37+
UpdateDocLinkResponse,
38+
APIError,
39+
UpdateDocLinkParams
40+
> & {
3441
listInvalidQueries?: string[];
35-
}
42+
};
3643

37-
export function useUpdateDocLink({
38-
onSuccess,
39-
listInvalidQueries,
40-
}: UpdateDocLinkProps = {}) {
44+
export function useUpdateDocLink(options?: UseUpdateDocLinkOptions) {
4145
const queryClient = useQueryClient();
42-
const { toast } = useToastProvider();
43-
const { t } = useTranslation();
4446

45-
return useMutation<Doc, APIError, UpdateDocLinkParams>({
47+
return useMutation<UpdateDocLinkResponse, APIError, UpdateDocLinkParams>({
4648
mutationFn: updateDocLink,
47-
onSuccess: (data) => {
48-
listInvalidQueries?.forEach((queryKey) => {
49+
...options,
50+
onSuccess: (data, variables, onMutateResult, context) => {
51+
options?.listInvalidQueries?.forEach((queryKey) => {
4952
void queryClient.invalidateQueries({
5053
queryKey: [queryKey],
5154
});
5255
});
5356

54-
toast(
55-
t('The document visibility has been updated.'),
56-
VariantType.SUCCESS,
57-
{
58-
duration: 2000,
59-
},
60-
);
61-
62-
onSuccess?.(data);
57+
options?.onSuccess?.(data, variables, onMutateResult, context);
6358
},
6459
});
6560
}

‎src/frontend/apps/impress/src/features/docs/doc-share/components/DocDesynchronized.tsx‎

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1-
import { Button } from '@openfun/cunningham-react';
1+
import {
2+
Button,
3+
VariantType,
4+
useToastProvider,
5+
} from '@openfun/cunningham-react';
26
import { useTranslation } from 'react-i18next';
37

48
import { Box, Card, Text } from '@/components';
@@ -17,9 +21,15 @@ interface DocDesynchronizedProps {
1721
export const DocDesynchronized = ({ doc }: DocDesynchronizedProps) => {
1822
const { t } = useTranslation();
1923
const { spacingsTokens } = useCunninghamTheme();
24+
const { toast } = useToastProvider();
2025

2126
const { mutate: updateDocLink } = useUpdateDocLink({
2227
listInvalidQueries: [KEY_LIST_DOC, KEY_DOC],
28+
onSuccess: () => {
29+
toast(t('The document visibility restored.'), VariantType.SUCCESS, {
30+
duration: 2000,
31+
});
32+
},
2333
});
2434

2535
return (

‎src/frontend/apps/impress/src/features/docs/doc-share/components/DocVisibility.tsx‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { VariantType, useToastProvider } from '@openfun/cunningham-react';
12
import { useMemo } from 'react';
23
import { useTranslation } from 'react-i18next';
34
import { css } from 'styled-components';
@@ -41,6 +42,7 @@ export const DocVisibility = ({ doc }: DocVisibilityProps) => {
4142
const { isDesynchronized } = useDocUtils(doc);
4243
const { linkModeTranslations, linkReachChoices, linkReachTranslations } =
4344
useTranslatedShareSettings();
45+
const { toast } = useToastProvider();
4446

4547
const description =
4648
docLinkRole === LinkRole.READER
@@ -49,6 +51,15 @@ export const DocVisibility = ({ doc }: DocVisibilityProps) => {
4951

5052
const { mutate: updateDocLink } = useUpdateDocLink({
5153
listInvalidQueries: [KEY_LIST_DOC, KEY_DOC],
54+
onSuccess: () => {
55+
toast(
56+
t('The document visibility has been updated.'),
57+
VariantType.SUCCESS,
58+
{
59+
duration: 2000,
60+
},
61+
);
62+
},
5263
});
5364

5465
const linkReachOptions: DropdownMenuOption[] = useMemo(() => {
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import { captureException } from '@sentry/nextjs';
2+
import Head from 'next/head';
3+
import { useSearchParams } from 'next/navigation';
4+
import { useRouter } from 'next/router';
5+
import { ReactElement, useCallback, useEffect } from 'react';
6+
7+
import { Loading } from '@/components';
8+
import {
9+
LinkReach,
10+
LinkRole,
11+
useCreateDoc,
12+
} from '@/features/docs/doc-management';
13+
import { useUpdateDocLink } from '@/features/docs/doc-share/api/useUpdateDocLink';
14+
import { useSkeletonStore } from '@/features/skeletons';
15+
import { MainLayout } from '@/layouts';
16+
import { NextPageWithLayout } from '@/types/next';
17+
18+
const Page: NextPageWithLayout = () => {
19+
const { setIsSkeletonVisible } = useSkeletonStore();
20+
const router = useRouter();
21+
const searchParams = useSearchParams();
22+
const linkReach = searchParams.get('link-reach');
23+
const linkRole = searchParams.get('link-role');
24+
const title = searchParams.get('title');
25+
26+
const { mutateAsync: createDocAsync, data: doc } = useCreateDoc();
27+
28+
const { mutateAsync: updateDocLinkAsync } = useUpdateDocLink();
29+
30+
const redirectToDoc = useCallback(
31+
(docId: string) => {
32+
void router.push(`/docs/${docId}`);
33+
},
34+
[router],
35+
);
36+
37+
useEffect(() => {
38+
setIsSkeletonVisible(true);
39+
}, [setIsSkeletonVisible]);
40+
41+
useEffect(() => {
42+
if (doc) {
43+
return;
44+
}
45+
46+
createDocAsync({
47+
title: title || undefined,
48+
})
49+
.then((createdDoc) => {
50+
if ((linkReach && linkRole) || linkReach) {
51+
updateDocLinkAsync({
52+
id: createdDoc.id,
53+
link_reach: linkReach as LinkReach,
54+
link_role: (linkRole as LinkRole | undefined) || undefined,
55+
})
56+
.catch((error) => {
57+
captureException(error, {
58+
extra: {
59+
docId: createdDoc.id,
60+
linkReach,
61+
linkRole,
62+
},
63+
});
64+
})
65+
.finally(() => {
66+
redirectToDoc(createdDoc.id);
67+
});
68+
69+
return;
70+
}
71+
72+
redirectToDoc(createdDoc.id);
73+
})
74+
.catch((error) => {
75+
captureException(error, {
76+
extra: {
77+
title,
78+
},
79+
});
80+
});
81+
}, [
82+
createDocAsync,
83+
doc,
84+
linkReach,
85+
linkRole,
86+
redirectToDoc,
87+
title,
88+
updateDocLinkAsync,
89+
]);
90+
91+
return <Loading />;
92+
};
93+
94+
Page.getLayout = function getLayout(page: ReactElement) {
95+
return (
96+
<>
97+
<Head>
98+
<meta name="robots" content="noindex" />
99+
</Head>
100+
101+
<MainLayout enableResizablePanel={false}>{page}</MainLayout>
102+
</>
103+
);
104+
};
105+
106+
export default Page;

0 commit comments

Comments
 (0)