Skip to content

Commit 5a74e9d

Browse files
committed
✨(frontend) add comments feature
Implemented the comments feature for the document editor. We are now able to add, view, and manage comments within the document editor interface.
1 parent efc90fa commit 5a74e9d

File tree

10 files changed

+863
-6
lines changed

10 files changed

+863
-6
lines changed
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
import { chromium, expect, test } from '@playwright/test';
2+
3+
import {
4+
BROWSERS,
5+
createDoc,
6+
keyCloakSignIn,
7+
verifyDocName,
8+
} from './utils-common';
9+
import { addNewMember } from './utils-share';
10+
11+
test.beforeEach(async ({ page }) => {
12+
await page.goto('/');
13+
});
14+
15+
test.describe('Doc Comments', () => {
16+
test('it checks comments with 2 users in real time', async ({
17+
page,
18+
browserName,
19+
}) => {
20+
const [docTitle] = await createDoc(page, 'comment-doc', browserName, 1);
21+
22+
// We share the doc with another user
23+
const otherBrowserName = BROWSERS.find((b) => b !== browserName);
24+
if (!otherBrowserName) {
25+
throw new Error('No alternative browser found');
26+
}
27+
await page.getByRole('button', { name: 'Share' }).click();
28+
await addNewMember(page, 1, 'Administrator', otherBrowserName);
29+
30+
await expect(
31+
page
32+
.getByRole('listbox', { name: 'Suggestions' })
33+
.getByText(new RegExp(otherBrowserName)),
34+
).toBeVisible();
35+
36+
await page.getByRole('button', { name: 'close' }).click();
37+
38+
// We add a comment with the first user
39+
const editor = page.locator('.ProseMirror');
40+
await editor.locator('.bn-block-outer').last().fill('Hello World');
41+
await editor.getByText('Hello').selectText();
42+
await page.getByRole('button', { name: 'Add comment' }).click();
43+
44+
const thread = page.locator('.bn-thread');
45+
await thread.getByRole('paragraph').first().fill('This is a comment');
46+
await thread.locator('[data-test="save"]').click();
47+
await editor.getByText('Hello').click();
48+
49+
await expect(thread.getByText('This is a comment').first()).toBeVisible();
50+
await expect(thread.getByText(`E2E ${browserName}`).first()).toBeVisible();
51+
52+
const urlCommentDoc = page.url();
53+
54+
/**
55+
* We open another browser that will connect to the collaborative server
56+
* and will block the current browser to edit the doc.
57+
*/
58+
const otherBrowser = await chromium.launch({ headless: true });
59+
const otherContext = await otherBrowser.newContext({
60+
locale: 'en-US',
61+
timezoneId: 'Europe/Paris',
62+
permissions: [],
63+
storageState: {
64+
cookies: [],
65+
origins: [],
66+
},
67+
});
68+
const otherPage = await otherContext.newPage();
69+
await otherPage.goto(urlCommentDoc);
70+
71+
await otherPage.getByRole('button', { name: 'Login' }).click({
72+
timeout: 15000,
73+
});
74+
75+
await keyCloakSignIn(otherPage, otherBrowserName, false);
76+
77+
await verifyDocName(otherPage, docTitle);
78+
79+
const otherEditor = otherPage.locator('.ProseMirror');
80+
await otherEditor.getByText('Hello').click();
81+
const otherThread = otherPage.locator('.bn-thread');
82+
83+
// We check that the comment made by the first user is visible for the second user
84+
await expect(
85+
otherThread.getByText('This is a comment').first(),
86+
).toBeVisible();
87+
await expect(
88+
otherThread.getByText(`E2E ${browserName}`).first(),
89+
).toBeVisible();
90+
91+
// We add a comment with the second user
92+
await otherThread
93+
.getByRole('paragraph')
94+
.last()
95+
.fill('This is a comment from the other user');
96+
await otherThread.locator('[data-test="save"]').click();
97+
98+
// We check that the second user can see his comment he just made
99+
await expect(
100+
otherThread.getByText('This is a comment from the other user').first(),
101+
).toBeVisible();
102+
await expect(
103+
otherThread.getByText(`E2E ${otherBrowserName}`).first(),
104+
).toBeVisible();
105+
106+
// We check that the first user can see the comment made by the second user in real time
107+
await expect(
108+
thread.getByText('This is a comment from the other user').first(),
109+
).toBeVisible();
110+
await expect(
111+
thread.getByText(`E2E ${otherBrowserName}`).first(),
112+
).toBeVisible();
113+
});
114+
115+
test('it checks the comments interactions', async ({
116+
page,
117+
browserName,
118+
}) => {});
119+
});

src/frontend/apps/impress/src/features/auth/api/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,5 @@ export interface User {
1313
short_name: string;
1414
language?: string;
1515
}
16+
17+
export type UserLight = Pick<User, 'full_name' | 'short_name'>;

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

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { useCreateBlockNote } from '@blocknote/react';
1313
import { HocuspocusProvider } from '@hocuspocus/provider';
1414
import { useEffect } from 'react';
1515
import { useTranslation } from 'react-i18next';
16+
import { css } from 'styled-components';
1617
import * as Y from 'yjs';
1718

1819
import { Box, TextErrors } from '@/components';
@@ -33,6 +34,7 @@ import { randomColor } from '../utils';
3334

3435
import { BlockNoteSuggestionMenu } from './BlockNoteSuggestionMenu';
3536
import { BlockNoteToolbar } from './BlockNoteToolBar/BlockNoteToolbar';
37+
import { cssComments, useComments } from './comments/';
3638
import {
3739
AccessibleImageBlock,
3840
CalloutBlock,
@@ -80,6 +82,7 @@ export const BlockNoteEditor = ({ doc, provider }: BlockNoteEditorProps) => {
8082
const { isEditable, isLoading } = useIsCollaborativeEditable(doc);
8183
const isConnectedToCollabServer = provider.isSynced;
8284
const readOnly = !doc.abilities.partial_update || !isEditable || isLoading;
85+
const canSeeComment = doc.abilities.comment;
8386

8487
useSaveDoc(doc.id, provider.document, !readOnly, isConnectedToCollabServer);
8588
const { i18n } = useTranslation();
@@ -92,6 +95,8 @@ export const BlockNoteEditor = ({ doc, provider }: BlockNoteEditorProps) => {
9295
: user?.full_name || user?.email || t('Anonymous');
9396
const showCursorLabels: 'always' | 'activity' | (string & {}) = 'activity';
9497

98+
const threadStore = useComments(provider.document, doc, user);
99+
95100
const editor: DocsBlockNoteEditor = useCreateBlockNote(
96101
{
97102
codeBlock,
@@ -144,11 +149,25 @@ export const BlockNoteEditor = ({ doc, provider }: BlockNoteEditorProps) => {
144149
},
145150
showCursorLabels: showCursorLabels as 'always' | 'activity',
146151
},
152+
comments: { threadStore },
147153
dictionary: {
148154
...locales[lang as keyof typeof locales],
149155
multi_column:
150156
multiColumnLocales?.[lang as keyof typeof multiColumnLocales],
151157
},
158+
resolveUsers: async (userIds) => {
159+
return Promise.resolve(
160+
userIds.map((encodedURIUserId) => {
161+
const fullName = decodeURIComponent(encodedURIUserId);
162+
163+
return {
164+
id: encodedURIUserId,
165+
username: fullName,
166+
avatarUrl: 'https://i.pravatar.cc/300',
167+
};
168+
}),
169+
);
170+
},
152171
tables: {
153172
splitCells: true,
154173
cellBackgroundColor: true,
@@ -159,7 +178,7 @@ export const BlockNoteEditor = ({ doc, provider }: BlockNoteEditorProps) => {
159178
schema: blockNoteSchema,
160179
dropCursor: multiColumnDropCursor,
161180
},
162-
[collabName, lang, provider, uploadFile],
181+
[collabName, lang, provider, uploadFile, threadStore],
163182
);
164183

165184
useHeadings(editor);
@@ -178,7 +197,10 @@ export const BlockNoteEditor = ({ doc, provider }: BlockNoteEditorProps) => {
178197
<Box
179198
$padding={{ top: 'md' }}
180199
$background="white"
181-
$css={cssEditor(readOnly)}
200+
$css={css`
201+
${cssEditor(readOnly)};
202+
${cssComments(canSeeComment)}
203+
`}
182204
className="--docs--editor-container"
183205
>
184206
{errorAttachment && (
@@ -192,11 +214,13 @@ export const BlockNoteEditor = ({ doc, provider }: BlockNoteEditorProps) => {
192214
)}
193215

194216
<BlockNoteView
217+
className="--docs--main-editor"
195218
editor={editor}
196219
formattingToolbar={false}
197220
slashMenu={false}
198221
editable={!readOnly}
199222
theme="light"
223+
comments={canSeeComment}
200224
>
201225
<BlockNoteSuggestionMenu />
202226
<BlockNoteToolbar />
@@ -230,7 +254,12 @@ export const BlockNoteEditorVersion = ({
230254

231255
return (
232256
<Box $css={cssEditor(readOnly)} className="--docs--editor-container">
233-
<BlockNoteView editor={editor} editable={!readOnly} theme="light" />
257+
<BlockNoteView
258+
className="--docs--main-editor"
259+
editor={editor}
260+
editable={!readOnly}
261+
theme="light"
262+
/>
234263
</Box>
235264
);
236265
};

0 commit comments

Comments
 (0)