🐛(frontend) paste content with comments from another document

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.
This commit is contained in:
Anthony LC
2025-12-12 14:45:04 +01:00
parent 08fb191e6b
commit 31bd475418
4 changed files with 85 additions and 8 deletions

View File

@@ -15,6 +15,10 @@ and this project adheres to
- 🚸(backend) use unaccented full name for user search #1637
- 🌐(backend) internationalize demo #1644
### Fixed
- 🐛(frontend) paste content with comments from another document #1732
## [4.1.0] - 2025-12-09
### Added

View File

@@ -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', () => {

View File

@@ -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) => {

View File

@@ -117,7 +117,7 @@ export const cssComments = (
}
& svg {
color: var(--c--globals--colors--info-600);
color: var(--c--contextuals--background--semantic--brand--primary);
}
}
@@ -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
);
}
}
}
@@ -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);
@@ -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);
}
}
}