diff --git a/src/frontend/apps/e2e/__tests__/app-impress/doc-create.spec.ts b/src/frontend/apps/e2e/__tests__/app-impress/doc-create.spec.ts index 6954ae25d4..b102ccd908 100644 --- a/src/frontend/apps/e2e/__tests__/app-impress/doc-create.spec.ts +++ b/src/frontend/apps/e2e/__tests__/app-impress/doc-create.spec.ts @@ -7,6 +7,7 @@ import { randomName, verifyDocName, } from './utils-common'; +import { writeInEditor } from './utils-editor'; test.beforeEach(async ({ page }) => { await page.goto('/'); @@ -73,6 +74,53 @@ test.describe('Doc Create', () => { page.locator('.c__tree-view--row-content').getByText('Untitled document'), ).toBeVisible(); }); + + test('it creates a doc with link "/doc/new/', async ({ page }) => { + // Private doc creation + await page.goto('/docs/new/?title=My+private+doc+from+url'); + + await verifyDocName(page, 'My private doc from url'); + + await page.getByRole('button', { name: 'Share' }).click(); + + await expect( + page.getByTestId('doc-visibility').getByText('Private').first(), + ).toBeVisible(); + + // Public editing doc creation + await page.goto( + '/docs/new/?title=My+public+doc+from+url&link-reach=public&link-role=editor', + ); + + await verifyDocName(page, 'My public doc from url'); + + await page.getByRole('button', { name: 'Share' }).click(); + + await expect( + page.getByTestId('doc-visibility').getByText('Public').first(), + ).toBeVisible(); + + await expect( + page.getByTestId('doc-access-mode').getByText('Editing').first(), + ).toBeVisible(); + + // Authenticated reading doc creation + await page.goto( + '/docs/new/?title=My+authenticated+doc+from+url&link-reach=authenticated&link-role=reader', + ); + + await verifyDocName(page, 'My authenticated doc from url'); + + await page.getByRole('button', { name: 'Share' }).click(); + + await expect( + page.getByTestId('doc-visibility').getByText('Connected').first(), + ).toBeVisible(); + + await expect( + page.getByTestId('doc-access-mode').getByText('Reading').first(), + ).toBeVisible(); + }); }); test.describe('Doc Create: Not logged', () => { diff --git a/src/frontend/apps/impress/src/features/docs/doc-management/api/useCreateDoc.tsx b/src/frontend/apps/impress/src/features/docs/doc-management/api/useCreateDoc.tsx index ed03cf087c..4587e21ee4 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-management/api/useCreateDoc.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-management/api/useCreateDoc.tsx @@ -1,4 +1,8 @@ -import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { + UseMutationOptions, + useMutation, + useQueryClient, +} from '@tanstack/react-query'; import { APIError, errorCauses, fetchAPI } from '@/api'; @@ -6,9 +10,14 @@ import { Doc } from '../types'; import { KEY_LIST_DOC } from './useDocs'; -export const createDoc = async (): Promise => { +type CreateDocParams = { + title?: string; +} | void; + +export const createDoc = async (params: CreateDocParams): Promise => { const response = await fetchAPI(`documents/`, { method: 'POST', + body: JSON.stringify({ title: params?.title }), }); if (!response.ok) { @@ -18,23 +27,17 @@ export const createDoc = async (): Promise => { return response.json() as Promise; }; -interface CreateDocProps { - onSuccess: (data: Doc) => void; - onError?: (error: APIError) => void; -} +type UseCreateDocOptions = UseMutationOptions; -export function useCreateDoc({ onSuccess, onError }: CreateDocProps) { +export function useCreateDoc(options?: UseCreateDocOptions) { const queryClient = useQueryClient(); - return useMutation({ + return useMutation({ mutationFn: createDoc, - onSuccess: (data) => { + onSuccess: (data, variables, onMutateResult, context) => { void queryClient.resetQueries({ queryKey: [KEY_LIST_DOC], }); - onSuccess(data); - }, - onError: (error) => { - onError?.(error); + options?.onSuccess?.(data, variables, onMutateResult, context); }, }); } diff --git a/src/frontend/apps/impress/src/features/docs/doc-share/api/useUpdateDocLink.tsx b/src/frontend/apps/impress/src/features/docs/doc-share/api/useUpdateDocLink.tsx index 19fb33eb2c..5a950d0e8e 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-share/api/useUpdateDocLink.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-share/api/useUpdateDocLink.tsx @@ -1,17 +1,21 @@ -import { VariantType, useToastProvider } from '@openfun/cunningham-react'; -import { useMutation, useQueryClient } from '@tanstack/react-query'; -import { useTranslation } from 'react-i18next'; +import { + UseMutationOptions, + useMutation, + useQueryClient, +} from '@tanstack/react-query'; import { APIError, errorCauses, fetchAPI } from '@/api'; -import { Doc } from '@/docs/doc-management'; +import { Doc, LinkReach, LinkRole } from '@/docs/doc-management'; export type UpdateDocLinkParams = Pick & Partial>; +type UpdateDocLinkResponse = { link_role: LinkRole; link_reach: LinkReach }; + export const updateDocLink = async ({ id, ...params -}: UpdateDocLinkParams): Promise => { +}: UpdateDocLinkParams): Promise => { const response = await fetchAPI(`documents/${id}/link-configuration/`, { method: 'PUT', body: JSON.stringify({ @@ -26,40 +30,31 @@ export const updateDocLink = async ({ ); } - return response.json() as Promise; + return response.json() as Promise; }; -interface UpdateDocLinkProps { - onSuccess?: (data: Doc) => void; +type UseUpdateDocLinkOptions = UseMutationOptions< + UpdateDocLinkResponse, + APIError, + UpdateDocLinkParams +> & { listInvalidQueries?: string[]; -} +}; -export function useUpdateDocLink({ - onSuccess, - listInvalidQueries, -}: UpdateDocLinkProps = {}) { +export function useUpdateDocLink(options?: UseUpdateDocLinkOptions) { const queryClient = useQueryClient(); - const { toast } = useToastProvider(); - const { t } = useTranslation(); - return useMutation({ + return useMutation({ mutationFn: updateDocLink, - onSuccess: (data) => { - listInvalidQueries?.forEach((queryKey) => { + ...options, + onSuccess: (data, variables, onMutateResult, context) => { + options?.listInvalidQueries?.forEach((queryKey) => { void queryClient.invalidateQueries({ queryKey: [queryKey], }); }); - toast( - t('The document visibility has been updated.'), - VariantType.SUCCESS, - { - duration: 2000, - }, - ); - - onSuccess?.(data); + options?.onSuccess?.(data, variables, onMutateResult, context); }, }); } diff --git a/src/frontend/apps/impress/src/features/docs/doc-share/components/DocDesynchronized.tsx b/src/frontend/apps/impress/src/features/docs/doc-share/components/DocDesynchronized.tsx index 87038abf27..4e8aaff1a5 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-share/components/DocDesynchronized.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-share/components/DocDesynchronized.tsx @@ -1,4 +1,8 @@ -import { Button } from '@openfun/cunningham-react'; +import { + Button, + VariantType, + useToastProvider, +} from '@openfun/cunningham-react'; import { useTranslation } from 'react-i18next'; import { Box, Card, Text } from '@/components'; @@ -17,9 +21,15 @@ interface DocDesynchronizedProps { export const DocDesynchronized = ({ doc }: DocDesynchronizedProps) => { const { t } = useTranslation(); const { spacingsTokens } = useCunninghamTheme(); + const { toast } = useToastProvider(); const { mutate: updateDocLink } = useUpdateDocLink({ listInvalidQueries: [KEY_LIST_DOC, KEY_DOC], + onSuccess: () => { + toast(t('The document visibility restored.'), VariantType.SUCCESS, { + duration: 2000, + }); + }, }); return ( diff --git a/src/frontend/apps/impress/src/features/docs/doc-share/components/DocVisibility.tsx b/src/frontend/apps/impress/src/features/docs/doc-share/components/DocVisibility.tsx index 9971434d91..9db9aaad5f 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-share/components/DocVisibility.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-share/components/DocVisibility.tsx @@ -1,3 +1,4 @@ +import { VariantType, useToastProvider } from '@openfun/cunningham-react'; import { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { css } from 'styled-components'; @@ -41,6 +42,7 @@ export const DocVisibility = ({ doc }: DocVisibilityProps) => { const { isDesynchronized } = useDocUtils(doc); const { linkModeTranslations, linkReachChoices, linkReachTranslations } = useTranslatedShareSettings(); + const { toast } = useToastProvider(); const description = docLinkRole === LinkRole.READER @@ -49,6 +51,15 @@ export const DocVisibility = ({ doc }: DocVisibilityProps) => { const { mutate: updateDocLink } = useUpdateDocLink({ listInvalidQueries: [KEY_LIST_DOC, KEY_DOC], + onSuccess: () => { + toast( + t('The document visibility has been updated.'), + VariantType.SUCCESS, + { + duration: 2000, + }, + ); + }, }); const linkReachOptions: DropdownMenuOption[] = useMemo(() => { diff --git a/src/frontend/apps/impress/src/pages/docs/new/index.tsx b/src/frontend/apps/impress/src/pages/docs/new/index.tsx new file mode 100644 index 0000000000..d40619e554 --- /dev/null +++ b/src/frontend/apps/impress/src/pages/docs/new/index.tsx @@ -0,0 +1,106 @@ +import { captureException } from '@sentry/nextjs'; +import Head from 'next/head'; +import { useSearchParams } from 'next/navigation'; +import { useRouter } from 'next/router'; +import { ReactElement, useCallback, useEffect } from 'react'; + +import { Loading } from '@/components'; +import { + LinkReach, + LinkRole, + useCreateDoc, +} from '@/features/docs/doc-management'; +import { useUpdateDocLink } from '@/features/docs/doc-share/api/useUpdateDocLink'; +import { useSkeletonStore } from '@/features/skeletons'; +import { MainLayout } from '@/layouts'; +import { NextPageWithLayout } from '@/types/next'; + +const Page: NextPageWithLayout = () => { + const { setIsSkeletonVisible } = useSkeletonStore(); + const router = useRouter(); + const searchParams = useSearchParams(); + const linkReach = searchParams.get('link-reach'); + const linkRole = searchParams.get('link-role'); + const title = searchParams.get('title'); + + const { mutateAsync: createDocAsync, data: doc } = useCreateDoc(); + + const { mutateAsync: updateDocLinkAsync } = useUpdateDocLink(); + + const redirectToDoc = useCallback( + (docId: string) => { + void router.push(`/docs/${docId}`); + }, + [router], + ); + + useEffect(() => { + setIsSkeletonVisible(true); + }, [setIsSkeletonVisible]); + + useEffect(() => { + if (doc) { + return; + } + + createDocAsync({ + title: title || undefined, + }) + .then((createdDoc) => { + if ((linkReach && linkRole) || linkReach) { + updateDocLinkAsync({ + id: createdDoc.id, + link_reach: linkReach as LinkReach, + link_role: (linkRole as LinkRole | undefined) || undefined, + }) + .catch((error) => { + captureException(error, { + extra: { + docId: createdDoc.id, + linkReach, + linkRole, + }, + }); + }) + .finally(() => { + redirectToDoc(createdDoc.id); + }); + + return; + } + + redirectToDoc(createdDoc.id); + }) + .catch((error) => { + captureException(error, { + extra: { + title, + }, + }); + }); + }, [ + createDocAsync, + doc, + linkReach, + linkRole, + redirectToDoc, + title, + updateDocLinkAsync, + ]); + + return ; +}; + +Page.getLayout = function getLayout(page: ReactElement) { + return ( + <> + + + + + {page} + + ); +}; + +export default Page;