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[] = [