✨(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.
This commit is contained in:
@@ -10,6 +10,7 @@ and this project adheres to
|
||||
|
||||
- ✨ Add comments feature to the editor #1330
|
||||
- ✨(backend) Comments on text editor #1330
|
||||
- ✨(frontend) link to create new doc #1574
|
||||
|
||||
### Changed
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
randomName,
|
||||
verifyDocName,
|
||||
} from './utils-common';
|
||||
import { connectOtherUserToDoc } from './utils-share';
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/');
|
||||
@@ -73,6 +74,82 @@ 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,
|
||||
browserName,
|
||||
}) => {
|
||||
test.slow();
|
||||
|
||||
// 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();
|
||||
|
||||
const { cleanup, otherPage, otherBrowserName } =
|
||||
await connectOtherUserToDoc({
|
||||
docUrl:
|
||||
'/docs/new/?title=From+unlogged+doc+from+url&link-reach=authenticated&link-role=reader',
|
||||
browserName,
|
||||
withoutSignIn: true,
|
||||
});
|
||||
|
||||
await keyCloakSignIn(otherPage, otherBrowserName, false);
|
||||
|
||||
await verifyDocName(otherPage, 'From unlogged doc from url');
|
||||
|
||||
await otherPage.getByRole('button', { name: 'Share' }).click();
|
||||
|
||||
await expect(
|
||||
otherPage.getByTestId('doc-visibility').getByText('Connected').first(),
|
||||
).toBeVisible();
|
||||
|
||||
await expect(
|
||||
otherPage.getByTestId('doc-access-mode').getByText('Reading').first(),
|
||||
).toBeVisible();
|
||||
|
||||
await cleanup();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Doc Create: Not logged', () => {
|
||||
|
||||
@@ -27,7 +27,7 @@ export const setAuthUrl = () => {
|
||||
window.location.pathname !== '/' &&
|
||||
window.location.pathname !== `${HOME_URL}/`
|
||||
) {
|
||||
localStorage.setItem(PATH_AUTH_LOCAL_STORAGE, window.location.pathname);
|
||||
localStorage.setItem(PATH_AUTH_LOCAL_STORAGE, window.location.href);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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<Doc> => {
|
||||
type CreateDocParams = {
|
||||
title?: string;
|
||||
} | void;
|
||||
|
||||
export const createDoc = async (params: CreateDocParams): Promise<Doc> => {
|
||||
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<Doc> => {
|
||||
return response.json() as Promise<Doc>;
|
||||
};
|
||||
|
||||
interface CreateDocProps {
|
||||
onSuccess: (data: Doc) => void;
|
||||
onError?: (error: APIError) => void;
|
||||
}
|
||||
type UseCreateDocOptions = UseMutationOptions<Doc, APIError, CreateDocParams>;
|
||||
|
||||
export function useCreateDoc({ onSuccess, onError }: CreateDocProps) {
|
||||
export function useCreateDoc(options?: UseCreateDocOptions) {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation<Doc, APIError>({
|
||||
return useMutation<Doc, APIError, CreateDocParams>({
|
||||
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);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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<Doc, 'id' | 'link_reach'> &
|
||||
Partial<Pick<Doc, 'link_role'>>;
|
||||
|
||||
type UpdateDocLinkResponse = { link_role: LinkRole; link_reach: LinkReach };
|
||||
|
||||
export const updateDocLink = async ({
|
||||
id,
|
||||
...params
|
||||
}: UpdateDocLinkParams): Promise<Doc> => {
|
||||
}: UpdateDocLinkParams): Promise<UpdateDocLinkResponse> => {
|
||||
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<Doc>;
|
||||
return response.json() as Promise<UpdateDocLinkResponse>;
|
||||
};
|
||||
|
||||
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<Doc, APIError, UpdateDocLinkParams>({
|
||||
return useMutation<UpdateDocLinkResponse, APIError, UpdateDocLinkParams>({
|
||||
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);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
114
src/frontend/apps/impress/src/pages/docs/new/index.tsx
Normal file
114
src/frontend/apps/impress/src/pages/docs/new/index.tsx
Normal file
@@ -0,0 +1,114 @@
|
||||
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 { LOGIN_URL, setAuthUrl, useAuth } from '@/features/auth';
|
||||
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 { authenticated } = useAuth();
|
||||
|
||||
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;
|
||||
}
|
||||
if (!authenticated) {
|
||||
setAuthUrl();
|
||||
window.location.replace(LOGIN_URL);
|
||||
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,
|
||||
},
|
||||
});
|
||||
});
|
||||
}, [
|
||||
authenticated,
|
||||
createDocAsync,
|
||||
doc,
|
||||
linkReach,
|
||||
linkRole,
|
||||
redirectToDoc,
|
||||
title,
|
||||
updateDocLinkAsync,
|
||||
]);
|
||||
|
||||
return <Loading />;
|
||||
};
|
||||
|
||||
Page.getLayout = function getLayout(page: ReactElement) {
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<meta name="robots" content="noindex" />
|
||||
</Head>
|
||||
|
||||
<MainLayout enableResizablePanel={false}>{page}</MainLayout>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Page;
|
||||
Reference in New Issue
Block a user