From cc63812025541efaad831c1d226ca5c926f84312 Mon Sep 17 00:00:00 2001 From: Anthony LC Date: Tue, 21 Oct 2025 17:22:16 +0200 Subject: [PATCH 1/3] =?UTF-8?q?=F0=9F=9A=9A(frontend)=20better=20separatio?= =?UTF-8?q?n=20concern=20doc-versioning=20features?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We move some components related to doc versioning into the doc-versioning feature folder to have a better separation of concerns. We don't need a provider for the doc versioning components since they will receive the doc data directly via a request. --- .../docs/doc-editor/components/DocEditor.tsx | 141 +++++------------- .../docs/doc-editor/components/index.ts | 1 + .../docs/doc-header/components/index.ts | 2 +- .../components/DocVersionEditor.tsx | 83 +++++++++++ .../components/DocVersionHeader.tsx | 3 +- .../components/ModalSelectVersion.tsx | 7 +- 6 files changed, 131 insertions(+), 106 deletions(-) create mode 100644 src/frontend/apps/impress/src/features/docs/doc-versioning/components/DocVersionEditor.tsx rename src/frontend/apps/impress/src/features/docs/{doc-header => doc-versioning}/components/DocVersionHeader.tsx (92%) 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..5e1a99c014 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,26 @@ -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 { - Doc, - base64ToBlocknoteXmlFragment, - useProviderStore, -} from '@/docs/doc-management'; +import { Box, Loading } from '@/components'; +import { DocHeader } from '@/docs/doc-header/'; +import { Doc, 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 { BlockNoteEditor } from './BlockNoteEditor'; -interface DocEditorProps { - doc: Doc; - versionId?: Versions['version_id']; +interface DocEditorContainerProps { + docHeader: React.ReactNode; + docEditor: React.ReactNode; } -export const DocEditor = ({ doc, versionId }: DocEditorProps) => { +export const DocEditorContainer = ({ + docHeader, + docEditor, +}: 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 +50,36 @@ 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(); - if (isLoading || !version || !initialContent) { - return ( - - - - ); + // TODO: Use skeleton instead of loading + if (!provider || !isReady) { + return ; } - return ; + return ( + <> + {isDesktop && ( + + + + )} + } + docEditor={} + /> + + ); }; 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-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-versioning/components/DocVersionEditor.tsx b/src/frontend/apps/impress/src/features/docs/doc-versioning/components/DocVersionEditor.tsx new file mode 100644 index 0000000000..652a61ee7b --- /dev/null +++ b/src/frontend/apps/impress/src/features/docs/doc-versioning/components/DocVersionEditor.tsx @@ -0,0 +1,83 @@ +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 { BlockNoteEditorVersion, 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={} + /> + ); +}; 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/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 && ( From f82b0f5a2a8a4c4c89ab811b46947fd97df6100b Mon Sep 17 00:00:00 2001 From: Anthony LC Date: Tue, 21 Oct 2025 17:28:25 +0200 Subject: [PATCH 2/3] =?UTF-8?q?=F0=9F=9A=B8(frontend)=20separate=20viewers?= =?UTF-8?q?=20from=20editors?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We are now totally separating the viewers with the editors. We will not load the provider when we are in viewer mode, meaning the viewers will not be aware of other users and will not show their cursors anymore. We still get the document updates in real-time. --- CHANGELOG.md | 6 +- .../app-impress/doc-visibility.spec.ts | 110 ++++++++++++------ .../e2e/__tests__/app-impress/utils-common.ts | 1 + .../doc-editor/components/BlockNoteEditor.tsx | 61 +++++----- .../docs/doc-editor/components/DocEditor.tsx | 39 ++++++- .../hook/__tests__/useSaveDoc.test.tsx | 39 +------ .../docs/doc-editor/hook/useSaveDoc.tsx | 12 +- .../components/DocVersionEditor.tsx | 6 +- 8 files changed, 151 insertions(+), 123 deletions(-) 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 5e1a99c014..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 @@ -2,20 +2,30 @@ import { css } from 'styled-components'; import { Box, Loading } from '@/components'; import { DocHeader } from '@/docs/doc-header/'; -import { Doc, useProviderStore } from '@/docs/doc-management'; +import { + Doc, + useIsCollaborativeEditable, + useProviderStore, +} from '@/docs/doc-management'; import { TableContent } from '@/docs/doc-table-content/'; import { useResponsiveStore } from '@/stores'; -import { BlockNoteEditor } from './BlockNoteEditor'; +import { cssEditor } from '../styles'; + +import { BlockNoteEditor, BlockNoteReader } from './BlockNoteEditor'; interface DocEditorContainerProps { docHeader: React.ReactNode; docEditor: React.ReactNode; + isDeletedDoc: boolean; + readOnly: boolean; } export const DocEditorContainer = ({ docHeader, docEditor, + isDeletedDoc, + readOnly, }: DocEditorContainerProps) => { const { isDesktop } = useResponsiveStore(); @@ -42,7 +52,14 @@ export const DocEditorContainer = ({ className="--docs--doc-editor-content" > - {docEditor} + + {docEditor} + @@ -57,6 +74,8 @@ interface DocEditorProps { 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; // TODO: Use skeleton instead of loading if (!provider || !isReady) { @@ -78,7 +97,19 @@ export const DocEditor = ({ doc }: DocEditorProps) => { )} } - docEditor={} + docEditor={ + readOnly ? ( + + ) : ( + + ) + } + isDeletedDoc={!!doc.deleted_at} + readOnly={readOnly} /> ); 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..d32648ecc7 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,7 +13,6 @@ const SAVE_INTERVAL = 60000; export const useSaveDoc = ( docId: string, yDoc: Y.Doc, - canSave: boolean, isConnectedToCollabServer: boolean, ) => { const { mutate: updateDoc } = useUpdateDoc({ @@ -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-versioning/components/DocVersionEditor.tsx b/src/frontend/apps/impress/src/features/docs/doc-versioning/components/DocVersionEditor.tsx index 652a61ee7b..a7574a44d4 100644 --- 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 @@ -4,7 +4,7 @@ import { useEffect, useState } from 'react'; import * as Y from 'yjs'; import { Box, Text, TextErrors } from '@/components'; -import { BlockNoteEditorVersion, DocEditorContainer } from '@/docs/doc-editor/'; +import { BlockNoteReader, DocEditorContainer } from '@/docs/doc-editor/'; import { Doc, base64ToBlocknoteXmlFragment } from '@/docs/doc-management'; import { Versions, useDocVersion } from '@/docs/doc-versioning/'; @@ -77,7 +77,9 @@ export const DocVersionEditor = ({ return ( } - docEditor={} + docEditor={} + isDeletedDoc={false} + readOnly={true} /> ); }; From 781e1e3de5255f36202d8f797c2819ebdf9d6702 Mon Sep 17 00:00:00 2001 From: Anthony LC Date: Tue, 21 Oct 2025 17:35:15 +0200 Subject: [PATCH 3/3] =?UTF-8?q?=E2=9C=8F=EF=B8=8F(frontend)=20fix=20typo?= =?UTF-8?q?=20listInvalidQueries?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In many places the property name "listInvalidQueries" was misspelled. --- .../src/features/docs/doc-editor/hook/useSaveDoc.tsx | 2 +- .../src/features/docs/doc-header/components/DocTitle.tsx | 2 +- .../src/features/docs/doc-header/components/DocToolBox.tsx | 4 ++-- .../docs/doc-management/api/useCreateFavoriteDoc.tsx | 6 +++--- .../docs/doc-management/api/useDeleteFavoriteDoc.tsx | 6 +++--- .../features/docs/doc-management/api/useDuplicateDoc.tsx | 2 +- .../src/features/docs/doc-management/api/useUpdateDoc.tsx | 4 ++-- .../src/features/docs/doc-share/api/useUpdateDocLink.tsx | 6 +++--- .../docs/doc-share/components/DocDesynchronized.tsx | 2 +- .../features/docs/doc-share/components/DocVisibility.tsx | 2 +- .../doc-versioning/components/ModalConfirmationVersion.tsx | 2 +- .../features/docs/docs-grid/components/DocsGridActions.tsx | 4 ++-- 12 files changed, 21 insertions(+), 21 deletions(-) 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 d32648ecc7..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 @@ -16,7 +16,7 @@ export const useSaveDoc = ( isConnectedToCollabServer: boolean, ) => { const { mutate: updateDoc } = useUpdateDoc({ - listInvalideQueries: [KEY_LIST_DOC_VERSIONS], + listInvalidQueries: [KEY_LIST_DOC_VERSIONS], onSuccess: () => { setIsLocalChange(false); }, 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-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/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/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[] = [