diff --git a/CHANGELOG.md b/CHANGELOG.md
index a2cd9de6..b3c1913e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,16 +10,17 @@ and this project adheres to
- ✨(frontend) create skeleton component for DocEditor #1491
- ✨(frontend) add an EmojiPicker in the document tree and title #1381
+- ✨(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
- 🐛(backend) fix trashbin list
- ♿(frontend) improve accessibility:
- ♿(frontend) remove empty alt on logo due to Axe a11y error #1516
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 24cf4bfb..88881e43 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,63 @@ 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();
+ const docUrl = page.url();
- await page
- .getByRole('button', {
- name: 'Logout',
- })
- .click();
+ const { otherPage, cleanup } = await connectOtherUserToDoc({
+ browserName,
+ docUrl,
+ withoutSignIn: true,
+ });
- await expectLoginPage(page);
-
- await page.goto(urlDoc);
-
- 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();
+
+ // Can still see changes made by others
+ await writeInEditor({ page, text: 'Can you see it ?' });
+ await expect(otherEditor.getByText('Can you see it ?')).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(
- 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 +332,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 fdfea088..6377c07e 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 3b6a67ad..9423cbb1 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 860d424e..1ec11e91 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
@@ -3,21 +3,31 @@ 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 { useSkeletonStore } from '@/features/skeletons';
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();
@@ -44,7 +54,14 @@ export const DocEditorContainer = ({
className="--docs--doc-editor-content"
>
- {docEditor}
+
+ {docEditor}
+
@@ -59,6 +76,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;
const { setIsSkeletonVisible } = useSkeletonStore();
const isProviderReady = isReady && provider;
@@ -87,7 +106,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 30f62d29..e532c804 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 57656a63..d32648ec 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 652a61ee..a7574a44 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}
/>
);
};