Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ and this project adheres to

- ✨(backend) allow to create a new user in a marketing system

### Fixed

- 🐛(frontend) paste content with comments from another document #1732

## [4.1.0] - 2025-12-09

### Added
Expand Down
43 changes: 43 additions & 0 deletions src/frontend/apps/e2e/__tests__/app-impress/doc-comments.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,49 @@ test.describe('Doc Comments', () => {

await cleanup();
});

test('it checks comments pasting from another document', async ({
page,
browserName,
}) => {
await createDoc(page, 'comment-doc-1', browserName, 1);

// We add a comment in the first document
const editor1 = await writeInEditor({ page, text: 'Document One' });
await editor1.getByText('Document One').selectText();
await page.getByRole('button', { name: 'Comment' }).click();

const thread1 = page.locator('.bn-thread');
await thread1.getByRole('paragraph').first().fill('Comment in Doc One');
await thread1.locator('[data-test="save"]').click();
await expect(thread1.getByText('Comment in Doc One').first()).toBeHidden();

await expect(editor1.getByText('Document One')).toHaveCSS(
'background-color',
'rgba(237, 180, 0, 0.4)',
);

await editor1.getByText('Document One').click();
// We copy the content including the comment from the first document
await editor1.getByText('Document One').selectText();
await page.keyboard.press('Control+C');

// We create a second document
await createDoc(page, 'comment-doc-2', browserName, 1);

// We paste the content into the second document
const editor2 = await writeInEditor({ page, text: '' });
await editor2.click();
await page.keyboard.press('Control+V');

await expect(editor2.getByText('Document One')).toHaveCSS(
'background-color',
'rgba(0, 0, 0, 0)',
);

await editor2.getByText('Document One').click();
await expect(page.locator('.bn-thread')).toBeHidden();
});
});

test.describe('Doc Comments mobile', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,26 @@ export const BlockNoteEditor = ({ doc, provider }: BlockNoteEditorProps) => {
multi_column:
multiColumnLocales?.[lang as keyof typeof multiColumnLocales],
},
pasteHandler: ({ event, defaultPasteHandler }) => {
// Get clipboard data
const blocknoteData = event.clipboardData?.getData('blocknote/html');

/**
* When pasting comments, the data-bn-thread-id
* attribute is present in the clipboard data.
* This indicates that the pasted content contains comments.
* But if the content with comments comes from another document,
* it will create orphaned comments that are not linked to this document
* and create errors.
* To avoid this, we refresh the threads to ensure that only comments
* relevant to the current document are displayed.
*/
if (blocknoteData && blocknoteData.includes('data-bn-thread-id')) {
void threadStore.refreshThreads();
}

return defaultPasteHandler();
},
resolveUsers: async (userIds) => {
return Promise.resolve(
userIds.map((encodedURIUserId) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ export const cssComments = (
}

& svg {
color: var(--c--globals--colors--info-600);
color: var(--c--contextuals--background--semantic--brand--primary);
}
}

Expand All @@ -134,15 +134,23 @@ export const cssComments = (
padding-inline: var(--c--globals--spacings--st);

&[data-test='save'] {
border: 1px solid var(--c--globals--colors--info-600);
background: var(--c--globals--colors--info-600);
color: white;
border: 1px solid
var(--c--contextuals--background--semantic--brand--primary);
background: var(
--c--contextuals--background--semantic--brand--primary
);
color: var(
--c--contextuals--content--semantic--brand--on-brand
);
}

&[data-test='cancel'] {
background: white;
border: 1px solid var(--c--globals--colors--gray-300);
color: var(--c--globals--colors--info-600);
border: 1px solid
var(--c--contextuals--border--surface--primary);
color: var(
--c--contextuals--background--semantic--brand--primary
);
}
}
}
Expand Down Expand Up @@ -184,7 +192,9 @@ export const cssComments = (

button {
font-size: 0;
background: var(--c--globals--colors--info-600);
background: var(
--c--contextuals--background--semantic--brand--primary
);
width: var(--c--globals--spacings--md);
height: var(--c--globals--spacings--md);
padding: var(--c--globals--spacings--0);
Expand All @@ -197,7 +207,7 @@ export const cssComments = (
content: 'arrow_upward_alt';
font-family: 'Material Symbols Outlined Variable', sans-serif;
font-size: 18px;
color: var(--c--globals--colors--gray-100);
color: var(--c--contextuals--content--semantic--brand--on-brand);
}
}
}
Expand Down
Loading