diff --git a/CHANGELOG.md b/CHANGELOG.md index 96791f3cd7..5f114c2e5d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,15 +6,19 @@ and this project adheres to ## [Unreleased] +### Added + +- ✨(frontend) ajustable left panel #1456 + ### Changed - ♻️(frontend) adapt custom blocks to new implementation #1375 - ♻️(backend) increase user short_name field length +- 🚸(frontend) separate viewers from editors #1509 ### Fixed - 🐛(frontend) fix duplicate document entries in grid #1479 -- 🐛(frontend) show full nested doc names with ajustable bar #1456 ## [3.8.2] - 2025-10-17 diff --git a/src/frontend/apps/e2e/__tests__/app-impress/doc-visibility.spec.ts b/src/frontend/apps/e2e/__tests__/app-impress/doc-visibility.spec.ts index 24cf4bfb90..536b27aa30 100644 --- a/src/frontend/apps/e2e/__tests__/app-impress/doc-visibility.spec.ts +++ b/src/frontend/apps/e2e/__tests__/app-impress/doc-visibility.spec.ts @@ -7,7 +7,7 @@ import { keyCloakSignIn, verifyDocName, } from './utils-common'; -import { writeInEditor } from './utils-editor'; +import { getEditor, writeInEditor } from './utils-editor'; import { addNewMember, connectOtherUserToDoc } from './utils-share'; import { createRootSubPage } from './utils-sub-pages'; @@ -182,15 +182,14 @@ test.describe('Doc Visibility: Restricted', () => { }); test.describe('Doc Visibility: Public', () => { - test.use({ storageState: { cookies: [], origins: [] } }); + test.beforeEach(async ({ page }) => { + await page.goto('/'); + }); test('It checks a public doc in read only mode', async ({ page, browserName, }) => { - await page.goto('/'); - await keyCloakSignIn(page, browserName); - const [docTitle] = await createDoc( page, 'Public read only', @@ -200,6 +199,8 @@ test.describe('Doc Visibility: Public', () => { await verifyDocName(page, docTitle); + await writeInEditor({ page, text: 'Hello Public Viewonly' }); + await page.getByRole('button', { name: 'Share' }).click(); const selectVisibility = page.getByTestId('doc-visibility'); await selectVisibility.click(); @@ -241,49 +242,59 @@ test.describe('Doc Visibility: Public', () => { await expect(page.getByTestId('search-docs-button')).toBeVisible(); await expect(page.getByTestId('new-doc-button')).toBeVisible(); - const urlDoc = page.url(); - - await page - .getByRole('button', { - name: 'Logout', - }) - .click(); - - await expectLoginPage(page); + const docUrl = page.url(); - await page.goto(urlDoc); + const { otherPage, cleanup } = await connectOtherUserToDoc({ + browserName, + docUrl, + withoutSignIn: true, + }); - await expect(page.locator('h2').getByText(docTitle)).toBeVisible(); - await expect(page.getByTestId('search-docs-button')).toBeHidden(); - await expect(page.getByTestId('new-doc-button')).toBeHidden(); - await expect(page.getByRole('button', { name: 'Share' })).toBeVisible(); - const card = page.getByLabel('It is the card information'); + await expect(otherPage.locator('h2').getByText(docTitle)).toBeVisible(); + await expect(otherPage.getByTestId('search-docs-button')).toBeHidden(); + await expect(otherPage.getByTestId('new-doc-button')).toBeHidden(); + await expect( + otherPage.getByRole('button', { name: 'Share' }), + ).toBeVisible(); + const card = otherPage.getByLabel('It is the card information'); await expect(card).toBeVisible(); await expect(card.getByText('Reader')).toBeVisible(); - await page.getByRole('button', { name: 'Share' }).click(); + const otherEditor = await getEditor({ page: otherPage }); + await expect(otherEditor).toHaveAttribute('contenteditable', 'false'); + await expect(otherEditor.getByText('Hello Public Viewonly')).toBeVisible(); + + // Cursor and selection of the anonymous user are not visible + await otherEditor.getByText('Hello Public').selectText(); await expect( - page.getByText( + page.locator('.collaboration-cursor-custom__base'), + ).toBeHidden(); + await expect(page.locator('.ProseMirror-yjs-selection')).toBeHidden(); + + await otherPage.getByRole('button', { name: 'Share' }).click(); + await expect( + otherPage.getByText( 'You can view this document but need additional access to see its members or modify settings.', ), ).toBeVisible(); await expect( - page.getByRole('button', { name: 'Request access' }), + otherPage.getByRole('button', { name: 'Request access' }), ).toBeHidden(); + + await cleanup(); }); test('It checks a public doc in editable mode', async ({ page, browserName, }) => { - await page.goto('/'); - await keyCloakSignIn(page, browserName); - const [docTitle] = await createDoc(page, 'Public editable', browserName, 1); await verifyDocName(page, docTitle); + await writeInEditor({ page, text: 'Hello Public Editable' }); + await page.getByRole('button', { name: 'Share' }).click(); const selectVisibility = page.getByTestId('doc-visibility'); await selectVisibility.click(); @@ -317,20 +328,47 @@ test.describe('Doc Visibility: Public', () => { cardContainer.getByText('Public document', { exact: true }), ).toBeVisible(); - const urlDoc = page.url(); + const docUrl = page.url(); - await page - .getByRole('button', { - name: 'Logout', - }) - .click(); + const { otherPage, cleanup } = await connectOtherUserToDoc({ + browserName, + docUrl, + withoutSignIn: true, + docTitle, + }); - await expectLoginPage(page); + await expect(otherPage.getByTestId('search-docs-button')).toBeHidden(); + await expect(otherPage.getByTestId('new-doc-button')).toBeHidden(); - await page.goto(urlDoc); + const otherEditor = await getEditor({ page: otherPage }); + await expect(otherEditor).toHaveAttribute('contenteditable', 'true'); + await expect(otherEditor.getByText('Hello Public Editable')).toBeVisible(); - await verifyDocName(page, docTitle); - await expect(page.getByRole('button', { name: 'Share' })).toBeVisible(); + // We can see the collaboration cursor of the anonymous user + await otherEditor.getByText('Hello Public').selectText(); + await expect( + page.locator('.collaboration-cursor-custom__base').getByText('Anonymous'), + ).toBeVisible(); + + await expect( + otherPage.getByRole('button', { name: 'Share' }), + ).toBeVisible(); + const card = otherPage.getByLabel('It is the card information'); + await expect(card).toBeVisible(); + await expect(card.getByText('Editor')).toBeVisible(); + + await otherPage.getByRole('button', { name: 'Share' }).click(); + await expect( + otherPage.getByText( + 'You can view this document but need additional access to see its members or modify settings.', + ), + ).toBeVisible(); + + await expect( + otherPage.getByRole('button', { name: 'Request access' }), + ).toBeHidden(); + + await cleanup(); }); }); diff --git a/src/frontend/apps/e2e/__tests__/app-impress/utils-common.ts b/src/frontend/apps/e2e/__tests__/app-impress/utils-common.ts index fdfea08824..6377c07e92 100644 --- a/src/frontend/apps/e2e/__tests__/app-impress/utils-common.ts +++ b/src/frontend/apps/e2e/__tests__/app-impress/utils-common.ts @@ -277,6 +277,7 @@ export const expectLoginPage = async (page: Page) => ).toBeVisible({ timeout: 10000, }); + // language helper export const TestLanguage = { English: { diff --git a/src/frontend/apps/impress/src/features/docs/doc-editor/components/BlockNoteEditor.tsx b/src/frontend/apps/impress/src/features/docs/doc-editor/components/BlockNoteEditor.tsx index 3b6a67adeb..9423cbb198 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-editor/components/BlockNoteEditor.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-editor/components/BlockNoteEditor.tsx @@ -17,11 +17,7 @@ import { useTranslation } from 'react-i18next'; import * as Y from 'yjs'; import { Box, TextErrors } from '@/components'; -import { - Doc, - useIsCollaborativeEditable, - useProviderStore, -} from '@/docs/doc-management'; +import { Doc, useProviderStore } from '@/docs/doc-management'; import { useAuth } from '@/features/auth'; import { @@ -32,7 +28,6 @@ import { useUploadStatus, } from '../hook'; import { useEditorStore } from '../stores'; -import { cssEditor } from '../styles'; import { DocsBlockNoteEditor } from '../types'; import { randomColor } from '../utils'; @@ -85,25 +80,19 @@ export const BlockNoteEditor = ({ doc, provider }: BlockNoteEditorProps) => { const { t } = useTranslation(); const { isSynced: isConnectedToCollabServer } = useProviderStore(); - const { isEditable, isLoading } = useIsCollaborativeEditable(doc); - const readOnly = !doc.abilities.partial_update || !isEditable || isLoading; - const isDeletedDoc = !!doc.deleted_at; - - useSaveDoc(doc.id, provider.document, !readOnly, isConnectedToCollabServer); + useSaveDoc(doc.id, provider.document, isConnectedToCollabServer); const { i18n } = useTranslation(); const lang = i18n.resolvedLanguage; const { uploadFile, errorAttachment } = useUploadFile(doc.id); - const collabName = readOnly - ? 'Reader' - : user?.full_name || user?.email || t('Anonymous'); + const collabName = user?.full_name || user?.email || t('Anonymous'); const showCursorLabels: 'always' | 'activity' | (string & {}) = 'activity'; const editor: DocsBlockNoteEditor = useCreateBlockNote( { collaboration: { - provider, + provider: provider, fragment: provider.document.getXmlFragment('document-store'), user: { name: collabName, @@ -117,10 +106,6 @@ export const BlockNoteEditor = ({ doc, provider }: BlockNoteEditorProps) => { renderCursor: (user: { color: string; name: string }) => { const cursorElement = document.createElement('span'); - if (user.name === 'Reader') { - return cursorElement; - } - cursorElement.classList.add('collaboration-cursor-custom__base'); const caretElement = document.createElement('span'); caretElement.classList.add('collaboration-cursor-custom__caret'); @@ -181,12 +166,7 @@ export const BlockNoteEditor = ({ doc, provider }: BlockNoteEditorProps) => { }, [setEditor, editor]); return ( - + <> {errorAttachment && ( { editor={editor} formattingToolbar={false} slashMenu={false} - editable={!readOnly} theme="light" > - + ); }; -interface BlockNoteEditorVersionProps { +interface BlockNoteReaderProps { initialContent: Y.XmlFragment; } -export const BlockNoteEditorVersion = ({ - initialContent, -}: BlockNoteEditorVersionProps) => { - const readOnly = true; +export const BlockNoteReader = ({ initialContent }: BlockNoteReaderProps) => { + const { setEditor } = useEditorStore(); const editor = useCreateBlockNote( { collaboration: { @@ -234,9 +211,23 @@ export const BlockNoteEditorVersion = ({ [initialContent], ); + useEffect(() => { + setEditor(editor); + + return () => { + setEditor(undefined); + }; + }, [setEditor, editor]); + + useHeadings(editor); + return ( - - - + ); }; diff --git a/src/frontend/apps/impress/src/features/docs/doc-editor/components/DocEditor.tsx b/src/frontend/apps/impress/src/features/docs/doc-editor/components/DocEditor.tsx index 7ad0a9415a..8e89057bf4 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-editor/components/DocEditor.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-editor/components/DocEditor.tsx @@ -1,50 +1,36 @@ -import { Loader } from '@openfun/cunningham-react'; -import { useRouter } from 'next/router'; -import { useEffect, useState } from 'react'; import { css } from 'styled-components'; -import * as Y from 'yjs'; -import { Box, Loading, Text, TextErrors } from '@/components'; -import { DocHeader, DocVersionHeader } from '@/docs/doc-header/'; +import { Box, Loading } from '@/components'; +import { DocHeader } from '@/docs/doc-header/'; import { Doc, - base64ToBlocknoteXmlFragment, + useIsCollaborativeEditable, useProviderStore, } from '@/docs/doc-management'; import { TableContent } from '@/docs/doc-table-content/'; -import { Versions, useDocVersion } from '@/docs/doc-versioning/'; import { useResponsiveStore } from '@/stores'; -import { BlockNoteEditor, BlockNoteEditorVersion } from './BlockNoteEditor'; +import { cssEditor } from '../styles'; -interface DocEditorProps { - doc: Doc; - versionId?: Versions['version_id']; +import { BlockNoteEditor, BlockNoteReader } from './BlockNoteEditor'; + +interface DocEditorContainerProps { + docHeader: React.ReactNode; + docEditor: React.ReactNode; + isDeletedDoc: boolean; + readOnly: boolean; } -export const DocEditor = ({ doc, versionId }: DocEditorProps) => { +export const DocEditorContainer = ({ + docHeader, + docEditor, + isDeletedDoc, + readOnly, +}: DocEditorContainerProps) => { const { isDesktop } = useResponsiveStore(); - const isVersion = !!versionId && typeof versionId === 'string'; - const { provider, isReady } = useProviderStore(); - - // TODO: Use skeleton instead of loading - if (!provider || !isReady) { - return ; - } return ( <> - {isDesktop && !isVersion && ( - - - - )} { $padding={{ horizontal: isDesktop ? '54px' : 'base' }} className="--docs--doc-editor-header" > - {isVersion ? : } + {docHeader} { className="--docs--doc-editor-content" > - {isVersion ? ( - - ) : ( - - )} + + {docEditor} + @@ -78,69 +67,50 @@ export const DocEditor = ({ doc, versionId }: DocEditorProps) => { ); }; -interface DocVersionEditorProps { - docId: Doc['id']; - versionId: Versions['version_id']; +interface DocEditorProps { + doc: Doc; } -export const DocVersionEditor = ({ - docId, - versionId, -}: DocVersionEditorProps) => { - const { - data: version, - isLoading, - isError, - error, - } = useDocVersion({ - docId, - versionId, - }); - - const { replace } = useRouter(); - const [initialContent, setInitialContent] = useState(); - - useEffect(() => { - if (!version?.content) { - return; - } - - setInitialContent(base64ToBlocknoteXmlFragment(version.content)); - }, [version?.content]); - - if (isError && error) { - if (error.status === 404) { - void replace(`/404`); - return null; - } - - return ( - - - wifi_off - - ) : undefined - } - /> - - ); - } +export const DocEditor = ({ doc }: DocEditorProps) => { + const { isDesktop } = useResponsiveStore(); + const { provider, isReady } = useProviderStore(); + const { isEditable, isLoading } = useIsCollaborativeEditable(doc); + const readOnly = !doc.abilities.partial_update || !isEditable || isLoading; - if (isLoading || !version || !initialContent) { - return ( - - - - ); + // TODO: Use skeleton instead of loading + if (!provider || !isReady) { + return ; } - return ; + return ( + <> + {isDesktop && ( + + + + )} + } + docEditor={ + readOnly ? ( + + ) : ( + + ) + } + isDeletedDoc={!!doc.deleted_at} + readOnly={readOnly} + /> + + ); }; diff --git a/src/frontend/apps/impress/src/features/docs/doc-editor/components/index.ts b/src/frontend/apps/impress/src/features/docs/doc-editor/components/index.ts index 643b57fa45..8e3b3f5f20 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-editor/components/index.ts +++ b/src/frontend/apps/impress/src/features/docs/doc-editor/components/index.ts @@ -1,2 +1,3 @@ +export * from './BlockNoteEditor'; export * from './DocEditor'; export * from './custom-blocks/'; diff --git a/src/frontend/apps/impress/src/features/docs/doc-editor/hook/__tests__/useSaveDoc.test.tsx b/src/frontend/apps/impress/src/features/docs/doc-editor/hook/__tests__/useSaveDoc.test.tsx index 30f62d2913..e532c8049e 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-editor/hook/__tests__/useSaveDoc.test.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-editor/hook/__tests__/useSaveDoc.test.tsx @@ -43,7 +43,7 @@ describe('useSaveDoc', () => { const addEventListenerSpy = vi.spyOn(window, 'addEventListener'); - renderHook(() => useSaveDoc(docId, yDoc, true, true), { + renderHook(() => useSaveDoc(docId, yDoc, true), { wrapper: AppWrapper, }); @@ -62,37 +62,6 @@ describe('useSaveDoc', () => { addEventListenerSpy.mockRestore(); }); - it('should not save when canSave is false', () => { - vi.useFakeTimers(); - const yDoc = new Y.Doc(); - const docId = 'test-doc-id'; - - fetchMock.patch('http://test.jest/api/v1.0/documents/test-doc-id/', { - body: JSON.stringify({ - id: 'test-doc-id', - content: 'test-content', - title: 'test-title', - }), - }); - - renderHook(() => useSaveDoc(docId, yDoc, false, true), { - wrapper: AppWrapper, - }); - - act(() => { - // Trigger a local update - yDoc.getMap('test').set('key', 'value'); - - // Advance timers to trigger the save interval - vi.advanceTimersByTime(61000); - }); - - // Since canSave is false, no API call should be made - expect(fetchMock.calls().length).toBe(0); - - vi.useRealTimers(); - }); - it('should save when there are local changes', async () => { vi.useFakeTimers(); const yDoc = new Y.Doc(); @@ -106,7 +75,7 @@ describe('useSaveDoc', () => { }), }); - renderHook(() => useSaveDoc(docId, yDoc, true, true), { + renderHook(() => useSaveDoc(docId, yDoc, true), { wrapper: AppWrapper, }); @@ -143,7 +112,7 @@ describe('useSaveDoc', () => { }), }); - renderHook(() => useSaveDoc(docId, yDoc, true, true), { + renderHook(() => useSaveDoc(docId, yDoc, true), { wrapper: AppWrapper, }); @@ -163,7 +132,7 @@ describe('useSaveDoc', () => { const docId = 'test-doc-id'; const removeEventListenerSpy = vi.spyOn(window, 'removeEventListener'); - const { unmount } = renderHook(() => useSaveDoc(docId, yDoc, true, true), { + const { unmount } = renderHook(() => useSaveDoc(docId, yDoc, true), { wrapper: AppWrapper, }); diff --git a/src/frontend/apps/impress/src/features/docs/doc-editor/hook/useSaveDoc.tsx b/src/frontend/apps/impress/src/features/docs/doc-editor/hook/useSaveDoc.tsx index 57656a6343..a5d1d585ab 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-editor/hook/useSaveDoc.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-editor/hook/useSaveDoc.tsx @@ -13,11 +13,10 @@ const SAVE_INTERVAL = 60000; export const useSaveDoc = ( docId: string, yDoc: Y.Doc, - canSave: boolean, isConnectedToCollabServer: boolean, ) => { const { mutate: updateDoc } = useUpdateDoc({ - listInvalideQueries: [KEY_LIST_DOC_VERSIONS], + listInvalidQueries: [KEY_LIST_DOC_VERSIONS], onSuccess: () => { setIsLocalChange(false); }, @@ -47,7 +46,7 @@ export const useSaveDoc = ( }, [yDoc]); const saveDoc = useCallback(() => { - if (!canSave || !isLocalChange) { + if (!isLocalChange) { return false; } @@ -58,14 +57,7 @@ export const useSaveDoc = ( }); return true; - }, [ - canSave, - isLocalChange, - updateDoc, - docId, - yDoc, - isConnectedToCollabServer, - ]); + }, [isLocalChange, updateDoc, docId, yDoc, isConnectedToCollabServer]); const router = useRouter(); diff --git a/src/frontend/apps/impress/src/features/docs/doc-header/components/DocTitle.tsx b/src/frontend/apps/impress/src/features/docs/doc-header/components/DocTitle.tsx index 42fea42895..2d2f4497d1 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-header/components/DocTitle.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-header/components/DocTitle.tsx @@ -61,7 +61,7 @@ const DocTitleInput = ({ doc }: DocTitleProps) => { const { broadcast } = useBroadcastStore(); const { mutate: updateDoc } = useUpdateDoc({ - listInvalideQueries: [KEY_DOC, KEY_LIST_DOC], + listInvalidQueries: [KEY_DOC, KEY_LIST_DOC], onSuccess(updatedDoc) { // Broadcast to every user connected to the document broadcast(`${KEY_DOC}-${updatedDoc.id}`); diff --git a/src/frontend/apps/impress/src/features/docs/doc-header/components/DocToolBox.tsx b/src/frontend/apps/impress/src/features/docs/doc-header/components/DocToolBox.tsx index 95a4244169..390d76ce90 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-header/components/DocToolBox.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-header/components/DocToolBox.tsx @@ -67,10 +67,10 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => { }); const { isFeatureFlagActivated } = useAnalytics(); const removeFavoriteDoc = useDeleteFavoriteDoc({ - listInvalideQueries: [KEY_LIST_DOC, KEY_DOC], + listInvalidQueries: [KEY_LIST_DOC, KEY_DOC], }); const makeFavoriteDoc = useCreateFavoriteDoc({ - listInvalideQueries: [KEY_LIST_DOC, KEY_DOC], + listInvalidQueries: [KEY_LIST_DOC, KEY_DOC], }); useEffect(() => { diff --git a/src/frontend/apps/impress/src/features/docs/doc-header/components/index.ts b/src/frontend/apps/impress/src/features/docs/doc-header/components/index.ts index ce2b58d4e5..b0e56b2dee 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-header/components/index.ts +++ b/src/frontend/apps/impress/src/features/docs/doc-header/components/index.ts @@ -1,2 +1,2 @@ export * from './DocHeader'; -export * from './DocVersionHeader'; +export * from './DocTitle'; diff --git a/src/frontend/apps/impress/src/features/docs/doc-management/api/useCreateFavoriteDoc.tsx b/src/frontend/apps/impress/src/features/docs/doc-management/api/useCreateFavoriteDoc.tsx index c951e86c73..cd7353db2b 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-management/api/useCreateFavoriteDoc.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-management/api/useCreateFavoriteDoc.tsx @@ -21,18 +21,18 @@ export const createFavoriteDoc = async ({ id }: CreateFavoriteDocParams) => { interface CreateFavoriteDocProps { onSuccess?: () => void; - listInvalideQueries?: string[]; + listInvalidQueries?: string[]; } export function useCreateFavoriteDoc({ onSuccess, - listInvalideQueries, + listInvalidQueries, }: CreateFavoriteDocProps) { const queryClient = useQueryClient(); return useMutation({ mutationFn: createFavoriteDoc, onSuccess: () => { - listInvalideQueries?.forEach((queryKey) => { + listInvalidQueries?.forEach((queryKey) => { void queryClient.invalidateQueries({ queryKey: [queryKey], }); diff --git a/src/frontend/apps/impress/src/features/docs/doc-management/api/useDeleteFavoriteDoc.tsx b/src/frontend/apps/impress/src/features/docs/doc-management/api/useDeleteFavoriteDoc.tsx index baceb1b577..fa4dd021f5 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-management/api/useDeleteFavoriteDoc.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-management/api/useDeleteFavoriteDoc.tsx @@ -21,18 +21,18 @@ export const deleteFavoriteDoc = async ({ id }: DeleteFavoriteDocParams) => { interface DeleteFavoriteDocProps { onSuccess?: () => void; - listInvalideQueries?: string[]; + listInvalidQueries?: string[]; } export function useDeleteFavoriteDoc({ onSuccess, - listInvalideQueries, + listInvalidQueries, }: DeleteFavoriteDocProps) { const queryClient = useQueryClient(); return useMutation({ mutationFn: deleteFavoriteDoc, onSuccess: () => { - listInvalideQueries?.forEach((queryKey) => { + listInvalidQueries?.forEach((queryKey) => { void queryClient.invalidateQueries({ queryKey: [queryKey], }); diff --git a/src/frontend/apps/impress/src/features/docs/doc-management/api/useDuplicateDoc.tsx b/src/frontend/apps/impress/src/features/docs/doc-management/api/useDuplicateDoc.tsx index de128ef1cb..c7ee040a2b 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-management/api/useDuplicateDoc.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-management/api/useDuplicateDoc.tsx @@ -60,7 +60,7 @@ export function useDuplicateDoc(options?: DuplicateDocOptions) { const { provider } = useProviderStore(); const { mutateAsync: updateDoc } = useUpdateDoc({ - listInvalideQueries: [KEY_LIST_DOC_VERSIONS], + listInvalidQueries: [KEY_LIST_DOC_VERSIONS], }); return useMutation({ diff --git a/src/frontend/apps/impress/src/features/docs/doc-management/api/useUpdateDoc.tsx b/src/frontend/apps/impress/src/features/docs/doc-management/api/useUpdateDoc.tsx index cdd8ad234c..aded223dbe 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-management/api/useUpdateDoc.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-management/api/useUpdateDoc.tsx @@ -34,7 +34,7 @@ export const updateDoc = async ({ }; type UseUpdateDoc = UseMutationOptions> & { - listInvalideQueries?: string[]; + listInvalidQueries?: string[]; }; export function useUpdateDoc(queryConfig?: UseUpdateDoc) { @@ -43,7 +43,7 @@ export function useUpdateDoc(queryConfig?: UseUpdateDoc) { mutationFn: updateDoc, ...queryConfig, onSuccess: (data, variables, onMutateResult, context) => { - queryConfig?.listInvalideQueries?.forEach((queryKey) => { + queryConfig?.listInvalidQueries?.forEach((queryKey) => { void queryClient.invalidateQueries({ queryKey: [queryKey], }); 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 c679e2e517..19fb33eb2c 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 @@ -31,12 +31,12 @@ export const updateDocLink = async ({ interface UpdateDocLinkProps { onSuccess?: (data: Doc) => void; - listInvalideQueries?: string[]; + listInvalidQueries?: string[]; } export function useUpdateDocLink({ onSuccess, - listInvalideQueries, + listInvalidQueries, }: UpdateDocLinkProps = {}) { const queryClient = useQueryClient(); const { toast } = useToastProvider(); @@ -45,7 +45,7 @@ export function useUpdateDocLink({ return useMutation({ mutationFn: updateDocLink, onSuccess: (data) => { - listInvalideQueries?.forEach((queryKey) => { + listInvalidQueries?.forEach((queryKey) => { void queryClient.invalidateQueries({ queryKey: [queryKey], }); 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 cded052871..2dead48e88 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 @@ -20,7 +20,7 @@ export const DocDesynchronized = ({ doc }: DocDesynchronizedProps) => { const { spacingsTokens, colorsTokens } = useCunninghamTheme(); const { mutate: updateDocLink } = useUpdateDocLink({ - listInvalideQueries: [KEY_LIST_DOC, KEY_DOC], + listInvalidQueries: [KEY_LIST_DOC, KEY_DOC], }); 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 c506bab9a3..3f621f8db8 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 @@ -48,7 +48,7 @@ export const DocVisibility = ({ doc }: DocVisibilityProps) => { : linkReachChoices[docLinkReach].descriptionEdit; const { mutate: updateDocLink } = useUpdateDocLink({ - listInvalideQueries: [KEY_LIST_DOC, KEY_DOC], + listInvalidQueries: [KEY_LIST_DOC, KEY_DOC], }); const linkReachOptions: DropdownMenuOption[] = useMemo(() => { diff --git a/src/frontend/apps/impress/src/features/docs/doc-versioning/components/DocVersionEditor.tsx b/src/frontend/apps/impress/src/features/docs/doc-versioning/components/DocVersionEditor.tsx new file mode 100644 index 0000000000..a7574a44d4 --- /dev/null +++ b/src/frontend/apps/impress/src/features/docs/doc-versioning/components/DocVersionEditor.tsx @@ -0,0 +1,85 @@ +import { Loader } from '@openfun/cunningham-react'; +import { useRouter } from 'next/router'; +import { useEffect, useState } from 'react'; +import * as Y from 'yjs'; + +import { Box, Text, TextErrors } from '@/components'; +import { BlockNoteReader, DocEditorContainer } from '@/docs/doc-editor/'; +import { Doc, base64ToBlocknoteXmlFragment } from '@/docs/doc-management'; +import { Versions, useDocVersion } from '@/docs/doc-versioning/'; + +import { DocVersionHeader } from './DocVersionHeader'; + +interface DocVersionEditorProps { + docId: Doc['id']; + versionId: Versions['version_id']; +} + +export const DocVersionEditor = ({ + docId, + versionId, +}: DocVersionEditorProps) => { + const { + data: version, + isLoading, + isError, + error, + } = useDocVersion({ + docId, + versionId, + }); + + const { replace } = useRouter(); + const [initialContent, setInitialContent] = useState(); + + useEffect(() => { + if (!version?.content) { + return; + } + + setInitialContent(base64ToBlocknoteXmlFragment(version.content)); + }, [version?.content]); + + if (isError && error) { + if (error.status === 404) { + void replace(`/404`); + return null; + } + + return ( + + + wifi_off + + ) : undefined + } + /> + + ); + } + + if (isLoading || !version || !initialContent) { + return ( + + + + ); + } + + return ( + } + docEditor={} + isDeletedDoc={false} + readOnly={true} + /> + ); +}; diff --git a/src/frontend/apps/impress/src/features/docs/doc-header/components/DocVersionHeader.tsx b/src/frontend/apps/impress/src/features/docs/doc-versioning/components/DocVersionHeader.tsx similarity index 92% rename from src/frontend/apps/impress/src/features/docs/doc-header/components/DocVersionHeader.tsx rename to src/frontend/apps/impress/src/features/docs/doc-versioning/components/DocVersionHeader.tsx index 511b803c9f..b3cc0a8800 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-header/components/DocVersionHeader.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-versioning/components/DocVersionHeader.tsx @@ -2,8 +2,7 @@ import { useTranslation } from 'react-i18next'; import { Box, HorizontalSeparator } from '@/components'; import { useCunninghamTheme } from '@/cunningham'; - -import { DocTitleText } from './DocTitle'; +import { DocTitleText } from '@/docs/doc-header'; export const DocVersionHeader = () => { const { spacingsTokens } = useCunninghamTheme(); diff --git a/src/frontend/apps/impress/src/features/docs/doc-versioning/components/ModalConfirmationVersion.tsx b/src/frontend/apps/impress/src/features/docs/doc-versioning/components/ModalConfirmationVersion.tsx index d87020da00..7cbb73d0d6 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-versioning/components/ModalConfirmationVersion.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-versioning/components/ModalConfirmationVersion.tsx @@ -42,7 +42,7 @@ export const ModalConfirmationVersion = ({ const { push } = useRouter(); const { provider } = useProviderStore(); const { mutate: updateDoc } = useUpdateDoc({ - listInvalideQueries: [KEY_LIST_DOC_VERSIONS], + listInvalidQueries: [KEY_LIST_DOC_VERSIONS], onSuccess: () => { const onDisplaySuccess = () => { toast(t('Version restored successfully'), VariantType.SUCCESS); diff --git a/src/frontend/apps/impress/src/features/docs/doc-versioning/components/ModalSelectVersion.tsx b/src/frontend/apps/impress/src/features/docs/doc-versioning/components/ModalSelectVersion.tsx index b8efa43ca3..8c8b9a63b9 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-versioning/components/ModalSelectVersion.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-versioning/components/ModalSelectVersion.tsx @@ -4,11 +4,11 @@ import { useTranslation } from 'react-i18next'; import { createGlobalStyle, css } from 'styled-components'; import { Box, ButtonCloseModal, Text } from '@/components'; -import { DocEditor } from '@/docs/doc-editor'; import { Doc } from '@/docs/doc-management'; import { Versions } from '../types'; +import { DocVersionEditor } from './DocVersionEditor'; import { ModalConfirmationVersion } from './ModalConfirmationVersion'; import { VersionList } from './VersionList'; @@ -81,7 +81,10 @@ export const ModalSelectVersion = ({ $align="center" > {selectedVersionId && ( - + )} {!selectedVersionId && ( diff --git a/src/frontend/apps/impress/src/features/docs/docs-grid/components/DocsGridActions.tsx b/src/frontend/apps/impress/src/features/docs/docs-grid/components/DocsGridActions.tsx index 60521f0962..f37a982ebf 100644 --- a/src/frontend/apps/impress/src/features/docs/docs-grid/components/DocsGridActions.tsx +++ b/src/frontend/apps/impress/src/features/docs/docs-grid/components/DocsGridActions.tsx @@ -27,10 +27,10 @@ export const DocsGridActions = ({ const { mutate: duplicateDoc } = useDuplicateDoc(); const removeFavoriteDoc = useDeleteFavoriteDoc({ - listInvalideQueries: [KEY_LIST_DOC], + listInvalidQueries: [KEY_LIST_DOC], }); const makeFavoriteDoc = useCreateFavoriteDoc({ - listInvalideQueries: [KEY_LIST_DOC], + listInvalidQueries: [KEY_LIST_DOC], }); const options: DropdownMenuOption[] = [