✨(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
|
- ✨ Add comments feature to the editor #1330
|
||||||
- ✨(backend) Comments on text editor #1330
|
- ✨(backend) Comments on text editor #1330
|
||||||
|
- ✨(frontend) link to create new doc #1574
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
randomName,
|
randomName,
|
||||||
verifyDocName,
|
verifyDocName,
|
||||||
} from './utils-common';
|
} from './utils-common';
|
||||||
|
import { connectOtherUserToDoc } from './utils-share';
|
||||||
|
|
||||||
test.beforeEach(async ({ page }) => {
|
test.beforeEach(async ({ page }) => {
|
||||||
await page.goto('/');
|
await page.goto('/');
|
||||||
@@ -73,6 +74,82 @@ test.describe('Doc Create', () => {
|
|||||||
page.locator('.c__tree-view--row-content').getByText('Untitled document'),
|
page.locator('.c__tree-view--row-content').getByText('Untitled document'),
|
||||||
).toBeVisible();
|
).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', () => {
|
test.describe('Doc Create: Not logged', () => {
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ export const setAuthUrl = () => {
|
|||||||
window.location.pathname !== '/' &&
|
window.location.pathname !== '/' &&
|
||||||
window.location.pathname !== `${HOME_URL}/`
|
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';
|
import { APIError, errorCauses, fetchAPI } from '@/api';
|
||||||
|
|
||||||
@@ -6,9 +10,14 @@ import { Doc } from '../types';
|
|||||||
|
|
||||||
import { KEY_LIST_DOC } from './useDocs';
|
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/`, {
|
const response = await fetchAPI(`documents/`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
body: JSON.stringify({ title: params?.title }),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
@@ -18,23 +27,17 @@ export const createDoc = async (): Promise<Doc> => {
|
|||||||
return response.json() as Promise<Doc>;
|
return response.json() as Promise<Doc>;
|
||||||
};
|
};
|
||||||
|
|
||||||
interface CreateDocProps {
|
type UseCreateDocOptions = UseMutationOptions<Doc, APIError, CreateDocParams>;
|
||||||
onSuccess: (data: Doc) => void;
|
|
||||||
onError?: (error: APIError) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useCreateDoc({ onSuccess, onError }: CreateDocProps) {
|
export function useCreateDoc(options?: UseCreateDocOptions) {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
return useMutation<Doc, APIError>({
|
return useMutation<Doc, APIError, CreateDocParams>({
|
||||||
mutationFn: createDoc,
|
mutationFn: createDoc,
|
||||||
onSuccess: (data) => {
|
onSuccess: (data, variables, onMutateResult, context) => {
|
||||||
void queryClient.resetQueries({
|
void queryClient.resetQueries({
|
||||||
queryKey: [KEY_LIST_DOC],
|
queryKey: [KEY_LIST_DOC],
|
||||||
});
|
});
|
||||||
onSuccess(data);
|
options?.onSuccess?.(data, variables, onMutateResult, context);
|
||||||
},
|
|
||||||
onError: (error) => {
|
|
||||||
onError?.(error);
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,21 @@
|
|||||||
import { VariantType, useToastProvider } from '@openfun/cunningham-react';
|
import {
|
||||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
UseMutationOptions,
|
||||||
import { useTranslation } from 'react-i18next';
|
useMutation,
|
||||||
|
useQueryClient,
|
||||||
|
} from '@tanstack/react-query';
|
||||||
|
|
||||||
import { APIError, errorCauses, fetchAPI } from '@/api';
|
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'> &
|
export type UpdateDocLinkParams = Pick<Doc, 'id' | 'link_reach'> &
|
||||||
Partial<Pick<Doc, 'link_role'>>;
|
Partial<Pick<Doc, 'link_role'>>;
|
||||||
|
|
||||||
|
type UpdateDocLinkResponse = { link_role: LinkRole; link_reach: LinkReach };
|
||||||
|
|
||||||
export const updateDocLink = async ({
|
export const updateDocLink = async ({
|
||||||
id,
|
id,
|
||||||
...params
|
...params
|
||||||
}: UpdateDocLinkParams): Promise<Doc> => {
|
}: UpdateDocLinkParams): Promise<UpdateDocLinkResponse> => {
|
||||||
const response = await fetchAPI(`documents/${id}/link-configuration/`, {
|
const response = await fetchAPI(`documents/${id}/link-configuration/`, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
body: JSON.stringify({
|
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 {
|
type UseUpdateDocLinkOptions = UseMutationOptions<
|
||||||
onSuccess?: (data: Doc) => void;
|
UpdateDocLinkResponse,
|
||||||
|
APIError,
|
||||||
|
UpdateDocLinkParams
|
||||||
|
> & {
|
||||||
listInvalidQueries?: string[];
|
listInvalidQueries?: string[];
|
||||||
}
|
};
|
||||||
|
|
||||||
export function useUpdateDocLink({
|
export function useUpdateDocLink(options?: UseUpdateDocLinkOptions) {
|
||||||
onSuccess,
|
|
||||||
listInvalidQueries,
|
|
||||||
}: UpdateDocLinkProps = {}) {
|
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const { toast } = useToastProvider();
|
|
||||||
const { t } = useTranslation();
|
|
||||||
|
|
||||||
return useMutation<Doc, APIError, UpdateDocLinkParams>({
|
return useMutation<UpdateDocLinkResponse, APIError, UpdateDocLinkParams>({
|
||||||
mutationFn: updateDocLink,
|
mutationFn: updateDocLink,
|
||||||
onSuccess: (data) => {
|
...options,
|
||||||
listInvalidQueries?.forEach((queryKey) => {
|
onSuccess: (data, variables, onMutateResult, context) => {
|
||||||
|
options?.listInvalidQueries?.forEach((queryKey) => {
|
||||||
void queryClient.invalidateQueries({
|
void queryClient.invalidateQueries({
|
||||||
queryKey: [queryKey],
|
queryKey: [queryKey],
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
toast(
|
options?.onSuccess?.(data, variables, onMutateResult, context);
|
||||||
t('The document visibility has been updated.'),
|
|
||||||
VariantType.SUCCESS,
|
|
||||||
{
|
|
||||||
duration: 2000,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
onSuccess?.(data);
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
import { Button } from '@openfun/cunningham-react';
|
import {
|
||||||
|
Button,
|
||||||
|
VariantType,
|
||||||
|
useToastProvider,
|
||||||
|
} from '@openfun/cunningham-react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import { Box, Card, Text } from '@/components';
|
import { Box, Card, Text } from '@/components';
|
||||||
@@ -17,9 +21,15 @@ interface DocDesynchronizedProps {
|
|||||||
export const DocDesynchronized = ({ doc }: DocDesynchronizedProps) => {
|
export const DocDesynchronized = ({ doc }: DocDesynchronizedProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { spacingsTokens } = useCunninghamTheme();
|
const { spacingsTokens } = useCunninghamTheme();
|
||||||
|
const { toast } = useToastProvider();
|
||||||
|
|
||||||
const { mutate: updateDocLink } = useUpdateDocLink({
|
const { mutate: updateDocLink } = useUpdateDocLink({
|
||||||
listInvalidQueries: [KEY_LIST_DOC, KEY_DOC],
|
listInvalidQueries: [KEY_LIST_DOC, KEY_DOC],
|
||||||
|
onSuccess: () => {
|
||||||
|
toast(t('The document visibility restored.'), VariantType.SUCCESS, {
|
||||||
|
duration: 2000,
|
||||||
|
});
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { VariantType, useToastProvider } from '@openfun/cunningham-react';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { css } from 'styled-components';
|
import { css } from 'styled-components';
|
||||||
@@ -41,6 +42,7 @@ export const DocVisibility = ({ doc }: DocVisibilityProps) => {
|
|||||||
const { isDesynchronized } = useDocUtils(doc);
|
const { isDesynchronized } = useDocUtils(doc);
|
||||||
const { linkModeTranslations, linkReachChoices, linkReachTranslations } =
|
const { linkModeTranslations, linkReachChoices, linkReachTranslations } =
|
||||||
useTranslatedShareSettings();
|
useTranslatedShareSettings();
|
||||||
|
const { toast } = useToastProvider();
|
||||||
|
|
||||||
const description =
|
const description =
|
||||||
docLinkRole === LinkRole.READER
|
docLinkRole === LinkRole.READER
|
||||||
@@ -49,6 +51,15 @@ export const DocVisibility = ({ doc }: DocVisibilityProps) => {
|
|||||||
|
|
||||||
const { mutate: updateDocLink } = useUpdateDocLink({
|
const { mutate: updateDocLink } = useUpdateDocLink({
|
||||||
listInvalidQueries: [KEY_LIST_DOC, KEY_DOC],
|
listInvalidQueries: [KEY_LIST_DOC, KEY_DOC],
|
||||||
|
onSuccess: () => {
|
||||||
|
toast(
|
||||||
|
t('The document visibility has been updated.'),
|
||||||
|
VariantType.SUCCESS,
|
||||||
|
{
|
||||||
|
duration: 2000,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const linkReachOptions: DropdownMenuOption[] = useMemo(() => {
|
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