Skip to content

Commit eb71028

Browse files
committed
🚸(frontend) separate viewers from editors
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.
1 parent 39c22b0 commit eb71028

File tree

8 files changed

+152
-123
lines changed

8 files changed

+152
-123
lines changed

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,17 @@ and this project adheres to
1010

1111
- ✨(frontend) create skeleton component for DocEditor #1491
1212
- ✨(frontend) add an EmojiPicker in the document tree and title #1381
13+
- ✨(frontend) ajustable left panel #1456
1314

1415
### Changed
1516

1617
- ♻️(frontend) adapt custom blocks to new implementation #1375
1718
- ♻️(backend) increase user short_name field length
19+
- 🚸(frontend) separate viewers from editors #1509
1820

1921
### Fixed
2022

2123
- 🐛(frontend) fix duplicate document entries in grid #1479
22-
- 🐛(frontend) show full nested doc names with ajustable bar #1456
2324
- 🐛(backend) fix trashbin list
2425
- ♿(frontend) improve accessibility:
2526
- ♿(frontend) remove empty alt on logo due to Axe a11y error #1516

src/frontend/apps/e2e/__tests__/app-impress/doc-visibility.spec.ts

Lines changed: 78 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import {
77
keyCloakSignIn,
88
verifyDocName,
99
} from './utils-common';
10-
import { writeInEditor } from './utils-editor';
10+
import { getEditor, writeInEditor } from './utils-editor';
1111
import { addNewMember, connectOtherUserToDoc } from './utils-share';
1212
import { createRootSubPage } from './utils-sub-pages';
1313

@@ -182,15 +182,14 @@ test.describe('Doc Visibility: Restricted', () => {
182182
});
183183

184184
test.describe('Doc Visibility: Public', () => {
185-
test.use({ storageState: { cookies: [], origins: [] } });
185+
test.beforeEach(async ({ page }) => {
186+
await page.goto('/');
187+
});
186188

187189
test('It checks a public doc in read only mode', async ({
188190
page,
189191
browserName,
190192
}) => {
191-
await page.goto('/');
192-
await keyCloakSignIn(page, browserName);
193-
194193
const [docTitle] = await createDoc(
195194
page,
196195
'Public read only',
@@ -200,6 +199,8 @@ test.describe('Doc Visibility: Public', () => {
200199

201200
await verifyDocName(page, docTitle);
202201

202+
await writeInEditor({ page, text: 'Hello Public Viewonly' });
203+
203204
await page.getByRole('button', { name: 'Share' }).click();
204205
const selectVisibility = page.getByTestId('doc-visibility');
205206
await selectVisibility.click();
@@ -241,49 +242,63 @@ test.describe('Doc Visibility: Public', () => {
241242
await expect(page.getByTestId('search-docs-button')).toBeVisible();
242243
await expect(page.getByTestId('new-doc-button')).toBeVisible();
243244

244-
const urlDoc = page.url();
245-
246-
await page
247-
.getByRole('button', {
248-
name: 'Logout',
249-
})
250-
.click();
251-
252-
await expectLoginPage(page);
245+
const docUrl = page.url();
253246

254-
await page.goto(urlDoc);
247+
const { otherPage, cleanup } = await connectOtherUserToDoc({
248+
browserName,
249+
docUrl,
250+
withoutSignIn: true,
251+
});
255252

256-
await expect(page.locator('h2').getByText(docTitle)).toBeVisible();
257-
await expect(page.getByTestId('search-docs-button')).toBeHidden();
258-
await expect(page.getByTestId('new-doc-button')).toBeHidden();
259-
await expect(page.getByRole('button', { name: 'Share' })).toBeVisible();
260-
const card = page.getByLabel('It is the card information');
253+
await expect(otherPage.locator('h2').getByText(docTitle)).toBeVisible();
254+
await expect(otherPage.getByTestId('search-docs-button')).toBeHidden();
255+
await expect(otherPage.getByTestId('new-doc-button')).toBeHidden();
256+
await expect(
257+
otherPage.getByRole('button', { name: 'Share' }),
258+
).toBeVisible();
259+
const card = otherPage.getByLabel('It is the card information');
261260
await expect(card).toBeVisible();
262261
await expect(card.getByText('Reader')).toBeVisible();
263262

264-
await page.getByRole('button', { name: 'Share' }).click();
263+
const otherEditor = await getEditor({ page: otherPage });
264+
await expect(otherEditor).toHaveAttribute('contenteditable', 'false');
265+
await expect(otherEditor.getByText('Hello Public Viewonly')).toBeVisible();
266+
267+
// Cursor and selection of the anonymous user are not visible
268+
await otherEditor.getByText('Hello Public').selectText();
265269
await expect(
266-
page.getByText(
270+
page.locator('.collaboration-cursor-custom__base'),
271+
).toBeHidden();
272+
await expect(page.locator('.ProseMirror-yjs-selection')).toBeHidden();
273+
274+
// Can still see changes made by others
275+
await writeInEditor({ page, text: 'Can you see it ?' });
276+
await expect(otherEditor.getByText('Can you see it ?')).toBeVisible();
277+
278+
await otherPage.getByRole('button', { name: 'Share' }).click();
279+
await expect(
280+
otherPage.getByText(
267281
'You can view this document but need additional access to see its members or modify settings.',
268282
),
269283
).toBeVisible();
270284

271285
await expect(
272-
page.getByRole('button', { name: 'Request access' }),
286+
otherPage.getByRole('button', { name: 'Request access' }),
273287
).toBeHidden();
288+
289+
await cleanup();
274290
});
275291

276292
test('It checks a public doc in editable mode', async ({
277293
page,
278294
browserName,
279295
}) => {
280-
await page.goto('/');
281-
await keyCloakSignIn(page, browserName);
282-
283296
const [docTitle] = await createDoc(page, 'Public editable', browserName, 1);
284297

285298
await verifyDocName(page, docTitle);
286299

300+
await writeInEditor({ page, text: 'Hello Public Editable' });
301+
287302
await page.getByRole('button', { name: 'Share' }).click();
288303
const selectVisibility = page.getByTestId('doc-visibility');
289304
await selectVisibility.click();
@@ -317,20 +332,47 @@ test.describe('Doc Visibility: Public', () => {
317332
cardContainer.getByText('Public document', { exact: true }),
318333
).toBeVisible();
319334

320-
const urlDoc = page.url();
335+
const docUrl = page.url();
321336

322-
await page
323-
.getByRole('button', {
324-
name: 'Logout',
325-
})
326-
.click();
337+
const { otherPage, cleanup } = await connectOtherUserToDoc({
338+
browserName,
339+
docUrl,
340+
withoutSignIn: true,
341+
docTitle,
342+
});
327343

328-
await expectLoginPage(page);
344+
await expect(otherPage.getByTestId('search-docs-button')).toBeHidden();
345+
await expect(otherPage.getByTestId('new-doc-button')).toBeHidden();
329346

330-
await page.goto(urlDoc);
347+
const otherEditor = await getEditor({ page: otherPage });
348+
await expect(otherEditor).toHaveAttribute('contenteditable', 'true');
349+
await expect(otherEditor.getByText('Hello Public Editable')).toBeVisible();
331350

332-
await verifyDocName(page, docTitle);
333-
await expect(page.getByRole('button', { name: 'Share' })).toBeVisible();
351+
// We can see the collaboration cursor of the anonymous user
352+
await otherEditor.getByText('Hello Public').selectText();
353+
await expect(
354+
page.locator('.collaboration-cursor-custom__base').getByText('Anonymous'),
355+
).toBeVisible();
356+
357+
await expect(
358+
otherPage.getByRole('button', { name: 'Share' }),
359+
).toBeVisible();
360+
const card = otherPage.getByLabel('It is the card information');
361+
await expect(card).toBeVisible();
362+
await expect(card.getByText('Editor')).toBeVisible();
363+
364+
await otherPage.getByRole('button', { name: 'Share' }).click();
365+
await expect(
366+
otherPage.getByText(
367+
'You can view this document but need additional access to see its members or modify settings.',
368+
),
369+
).toBeVisible();
370+
371+
await expect(
372+
otherPage.getByRole('button', { name: 'Request access' }),
373+
).toBeHidden();
374+
375+
await cleanup();
334376
});
335377
});
336378

src/frontend/apps/e2e/__tests__/app-impress/utils-common.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,7 @@ export const expectLoginPage = async (page: Page) =>
277277
).toBeVisible({
278278
timeout: 10000,
279279
});
280+
280281
// language helper
281282
export const TestLanguage = {
282283
English: {

src/frontend/apps/impress/src/features/docs/doc-editor/components/BlockNoteEditor.tsx

Lines changed: 26 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,7 @@ import { useTranslation } from 'react-i18next';
1717
import * as Y from 'yjs';
1818

1919
import { Box, TextErrors } from '@/components';
20-
import {
21-
Doc,
22-
useIsCollaborativeEditable,
23-
useProviderStore,
24-
} from '@/docs/doc-management';
20+
import { Doc, useProviderStore } from '@/docs/doc-management';
2521
import { useAuth } from '@/features/auth';
2622

2723
import {
@@ -32,7 +28,6 @@ import {
3228
useUploadStatus,
3329
} from '../hook';
3430
import { useEditorStore } from '../stores';
35-
import { cssEditor } from '../styles';
3631
import { DocsBlockNoteEditor } from '../types';
3732
import { randomColor } from '../utils';
3833

@@ -85,25 +80,19 @@ export const BlockNoteEditor = ({ doc, provider }: BlockNoteEditorProps) => {
8580
const { t } = useTranslation();
8681
const { isSynced: isConnectedToCollabServer } = useProviderStore();
8782

88-
const { isEditable, isLoading } = useIsCollaborativeEditable(doc);
89-
const readOnly = !doc.abilities.partial_update || !isEditable || isLoading;
90-
const isDeletedDoc = !!doc.deleted_at;
91-
92-
useSaveDoc(doc.id, provider.document, !readOnly, isConnectedToCollabServer);
83+
useSaveDoc(doc.id, provider.document, isConnectedToCollabServer);
9384
const { i18n } = useTranslation();
9485
const lang = i18n.resolvedLanguage;
9586

9687
const { uploadFile, errorAttachment } = useUploadFile(doc.id);
9788

98-
const collabName = readOnly
99-
? 'Reader'
100-
: user?.full_name || user?.email || t('Anonymous');
89+
const collabName = user?.full_name || user?.email || t('Anonymous');
10190
const showCursorLabels: 'always' | 'activity' | (string & {}) = 'activity';
10291

10392
const editor: DocsBlockNoteEditor = useCreateBlockNote(
10493
{
10594
collaboration: {
106-
provider,
95+
provider: provider,
10796
fragment: provider.document.getXmlFragment('document-store'),
10897
user: {
10998
name: collabName,
@@ -117,10 +106,6 @@ export const BlockNoteEditor = ({ doc, provider }: BlockNoteEditorProps) => {
117106
renderCursor: (user: { color: string; name: string }) => {
118107
const cursorElement = document.createElement('span');
119108

120-
if (user.name === 'Reader') {
121-
return cursorElement;
122-
}
123-
124109
cursorElement.classList.add('collaboration-cursor-custom__base');
125110
const caretElement = document.createElement('span');
126111
caretElement.classList.add('collaboration-cursor-custom__caret');
@@ -181,12 +166,7 @@ export const BlockNoteEditor = ({ doc, provider }: BlockNoteEditorProps) => {
181166
}, [setEditor, editor]);
182167

183168
return (
184-
<Box
185-
$padding={{ top: 'md' }}
186-
$background="white"
187-
$css={cssEditor(readOnly, isDeletedDoc)}
188-
className="--docs--editor-container"
189-
>
169+
<>
190170
{errorAttachment && (
191171
<Box $margin={{ bottom: 'big', top: 'none', horizontal: 'large' }}>
192172
<TextErrors
@@ -201,24 +181,21 @@ export const BlockNoteEditor = ({ doc, provider }: BlockNoteEditorProps) => {
201181
editor={editor}
202182
formattingToolbar={false}
203183
slashMenu={false}
204-
editable={!readOnly}
205184
theme="light"
206185
>
207186
<BlockNoteSuggestionMenu />
208187
<BlockNoteToolbar />
209188
</BlockNoteView>
210-
</Box>
189+
</>
211190
);
212191
};
213192

214-
interface BlockNoteEditorVersionProps {
193+
interface BlockNoteReaderProps {
215194
initialContent: Y.XmlFragment;
216195
}
217196

218-
export const BlockNoteEditorVersion = ({
219-
initialContent,
220-
}: BlockNoteEditorVersionProps) => {
221-
const readOnly = true;
197+
export const BlockNoteReader = ({ initialContent }: BlockNoteReaderProps) => {
198+
const { setEditor } = useEditorStore();
222199
const editor = useCreateBlockNote(
223200
{
224201
collaboration: {
@@ -234,9 +211,23 @@ export const BlockNoteEditorVersion = ({
234211
[initialContent],
235212
);
236213

214+
useEffect(() => {
215+
setEditor(editor);
216+
217+
return () => {
218+
setEditor(undefined);
219+
};
220+
}, [setEditor, editor]);
221+
222+
useHeadings(editor);
223+
237224
return (
238-
<Box $css={cssEditor(readOnly, true)} className="--docs--editor-container">
239-
<BlockNoteView editor={editor} editable={!readOnly} theme="light" />
240-
</Box>
225+
<BlockNoteView
226+
editor={editor}
227+
editable={false}
228+
theme="light"
229+
formattingToolbar={false}
230+
slashMenu={false}
231+
/>
241232
);
242233
};

0 commit comments

Comments
 (0)