Skip to content

Commit 7fe32ad

Browse files
AntoLClunika
authored andcommitted
✨(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 4109ccc commit 7fe32ad

File tree

10 files changed

+936
-6
lines changed

10 files changed

+936
-6
lines changed
Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
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, 0, '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 thread.getByText('This is a comment').first().hover();
50+
51+
// We add a reaction with the first user
52+
await thread.locator('[data-test="addreaction"]').first().click();
53+
await thread.getByRole('button', { name: '👍' }).click();
54+
55+
await expect(thread.getByText('This is a comment').first()).toBeVisible();
56+
await expect(thread.getByText(`E2E ${browserName}`).first()).toBeVisible();
57+
await expect(thread.locator('.bn-comment-reaction')).toHaveText('👍1');
58+
59+
const urlCommentDoc = page.url();
60+
61+
// We open another browser with another user
62+
const otherBrowser = await chromium.launch({ headless: true });
63+
const otherContext = await otherBrowser.newContext({
64+
locale: 'en-US',
65+
timezoneId: 'Europe/Paris',
66+
permissions: [],
67+
storageState: {
68+
cookies: [],
69+
origins: [],
70+
},
71+
});
72+
const otherPage = await otherContext.newPage();
73+
await otherPage.goto(urlCommentDoc);
74+
75+
await otherPage.getByRole('button', { name: 'Login' }).click({
76+
timeout: 15000,
77+
});
78+
79+
await keyCloakSignIn(otherPage, otherBrowserName, false);
80+
81+
await verifyDocName(otherPage, docTitle);
82+
83+
const otherEditor = otherPage.locator('.ProseMirror');
84+
await otherEditor.getByText('Hello').click();
85+
const otherThread = otherPage.locator('.bn-thread');
86+
87+
await otherThread.getByText('This is a comment').first().hover();
88+
await otherThread.locator('[data-test="addreaction"]').first().click();
89+
await otherThread.getByRole('button', { name: '👍' }).click();
90+
91+
// We check that the comment made by the first user is visible for the second user
92+
await expect(
93+
otherThread.getByText('This is a comment').first(),
94+
).toBeVisible();
95+
await expect(
96+
otherThread.getByText(`E2E ${browserName}`).first(),
97+
).toBeVisible();
98+
await expect(otherThread.locator('.bn-comment-reaction')).toHaveText('👍2');
99+
100+
// We add a comment with the second user
101+
await otherThread
102+
.getByRole('paragraph')
103+
.last()
104+
.fill('This is a comment from the other user');
105+
await otherThread.locator('[data-test="save"]').click();
106+
107+
// We check that the second user can see the comment he just made
108+
await expect(
109+
otherThread.getByText('This is a comment from the other user').first(),
110+
).toBeVisible();
111+
await expect(
112+
otherThread.getByText(`E2E ${otherBrowserName}`).first(),
113+
).toBeVisible();
114+
115+
// We check that the first user can see the comment made by the second user in real time
116+
await expect(
117+
thread.getByText('This is a comment from the other user').first(),
118+
).toBeVisible();
119+
await expect(
120+
thread.getByText(`E2E ${otherBrowserName}`).first(),
121+
).toBeVisible();
122+
});
123+
124+
test('it checks the comments interactions', async ({ page, browserName }) => {
125+
await createDoc(page, 'comment-interaction', browserName, 1);
126+
127+
// Checks add react reaction
128+
const editor = page.locator('.ProseMirror');
129+
await editor.locator('.bn-block-outer').last().fill('Hello World');
130+
await editor.getByText('Hello').selectText();
131+
await page.getByRole('button', { name: 'Add comment' }).click();
132+
133+
const thread = page.locator('.bn-thread');
134+
await thread.getByRole('paragraph').first().fill('This is a comment');
135+
await thread.locator('[data-test="save"]').click();
136+
// Check background color changed
137+
await expect(editor.getByText('Hello')).toHaveCSS(
138+
'background-color',
139+
'rgb(244, 210, 97)',
140+
);
141+
await editor.getByText('Hello').click();
142+
143+
await thread.getByText('This is a comment').first().hover();
144+
145+
// We add a reaction with the first user
146+
await thread.locator('[data-test="addreaction"]').first().click();
147+
await thread.getByRole('button', { name: '👍' }).click();
148+
149+
await expect(thread.locator('.bn-comment-reaction')).toHaveText('👍1');
150+
151+
// Edit Comment
152+
await thread.getByText('This is a comment').first().hover();
153+
await thread.locator('[data-test="moreactions"]').first().click();
154+
await thread.getByRole('menuitem', { name: 'Edit comment' }).click();
155+
const commentEditor = thread.getByText('This is a comment').first();
156+
await commentEditor.fill('This is an edited comment');
157+
const saveBtn = thread.getByRole('button', { name: 'Save' });
158+
await saveBtn.click();
159+
await expect(saveBtn).toBeHidden();
160+
await expect(
161+
thread.getByText('This is an edited comment').first(),
162+
).toBeVisible();
163+
await expect(thread.getByText('This is a comment').first()).toBeHidden();
164+
165+
// Add second comment
166+
await thread.getByRole('paragraph').last().fill('This is a second comment');
167+
await thread.getByRole('button', { name: 'Save' }).click();
168+
await expect(
169+
thread.getByText('This is an edited comment').first(),
170+
).toBeVisible();
171+
await expect(
172+
thread.getByText('This is a second comment').first(),
173+
).toBeVisible();
174+
175+
// Delete second comment
176+
await thread.getByText('This is a second comment').first().hover();
177+
await thread.locator('[data-test="moreactions"]').first().click();
178+
await thread.getByRole('menuitem', { name: 'Delete comment' }).click();
179+
await expect(
180+
thread.getByText('This is a second comment').first(),
181+
).toBeHidden();
182+
183+
// Resolve thread
184+
await thread.getByText('This is an edited comment').first().hover();
185+
await thread.locator('[data-test="resolve"]').click();
186+
await expect(thread).toBeHidden();
187+
await expect(editor.getByText('Hello')).toHaveCSS(
188+
'background-color',
189+
'rgba(0, 0, 0, 0)',
190+
);
191+
});
192+
});

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,
@@ -79,6 +81,7 @@ export const BlockNoteEditor = ({ doc, provider }: BlockNoteEditorProps) => {
7981
const { isEditable, isLoading } = useIsCollaborativeEditable(doc);
8082
const isConnectedToCollabServer = provider.isSynced;
8183
const readOnly = !doc.abilities.partial_update || !isEditable || isLoading;
84+
const canSeeComment = doc.abilities.comment;
8285

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

97+
const threadStore = useComments(provider.document, doc, user);
98+
9499
const editor: DocsBlockNoteEditor = useCreateBlockNote(
95100
{
96101
codeBlock,
@@ -143,11 +148,25 @@ export const BlockNoteEditor = ({ doc, provider }: BlockNoteEditorProps) => {
143148
},
144149
showCursorLabels: showCursorLabels as 'always' | 'activity',
145150
},
151+
comments: { threadStore },
146152
dictionary: {
147153
...locales[lang as keyof typeof locales],
148154
multi_column:
149155
multiColumnLocales?.[lang as keyof typeof multiColumnLocales],
150156
},
157+
resolveUsers: async (userIds) => {
158+
return Promise.resolve(
159+
userIds.map((encodedURIUserId) => {
160+
const fullName = decodeURIComponent(encodedURIUserId);
161+
162+
return {
163+
id: encodedURIUserId,
164+
username: fullName,
165+
avatarUrl: 'https://i.pravatar.cc/300',
166+
};
167+
}),
168+
);
169+
},
151170
tables: {
152171
splitCells: true,
153172
cellBackgroundColor: true,
@@ -157,7 +176,7 @@ export const BlockNoteEditor = ({ doc, provider }: BlockNoteEditorProps) => {
157176
uploadFile,
158177
schema: blockNoteSchema,
159178
},
160-
[collabName, lang, provider, uploadFile],
179+
[collabName, lang, provider, uploadFile, threadStore],
161180
);
162181

163182
useHeadings(editor);
@@ -176,7 +195,10 @@ export const BlockNoteEditor = ({ doc, provider }: BlockNoteEditorProps) => {
176195
<Box
177196
$padding={{ top: 'md' }}
178197
$background="white"
179-
$css={cssEditor(readOnly)}
198+
$css={css`
199+
${cssEditor(readOnly)};
200+
${cssComments(canSeeComment)}
201+
`}
180202
className="--docs--editor-container"
181203
>
182204
{errorAttachment && (
@@ -190,11 +212,13 @@ export const BlockNoteEditor = ({ doc, provider }: BlockNoteEditorProps) => {
190212
)}
191213

192214
<BlockNoteView
215+
className="--docs--main-editor"
193216
editor={editor}
194217
formattingToolbar={false}
195218
slashMenu={false}
196219
editable={!readOnly}
197220
theme="light"
221+
comments={canSeeComment}
198222
>
199223
<BlockNoteSuggestionMenu />
200224
<BlockNoteToolbar />
@@ -228,7 +252,12 @@ export const BlockNoteEditorVersion = ({
228252

229253
return (
230254
<Box $css={cssEditor(readOnly)} className="--docs--editor-container">
231-
<BlockNoteView editor={editor} editable={!readOnly} theme="light" />
255+
<BlockNoteView
256+
className="--docs--main-editor"
257+
editor={editor}
258+
editable={!readOnly}
259+
theme="light"
260+
/>
232261
</Box>
233262
);
234263
};

0 commit comments

Comments
 (0)