Skip to content

Commit 1a663f4

Browse files
committed
🐛(y-provider) keep the sub-docs block when converting documents
1 parent abcb11e commit 1a663f4

3 files changed

Lines changed: 84 additions & 0 deletions

File tree

‎src/frontend/servers/y-provider/__tests__/convert.test.ts‎

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
import {
2+
BlockNoteSchema,
3+
createBlockSpec,
4+
defaultBlockSpecs,
5+
} from '@blocknote/core';
16
import {
27
CommentsExtension,
38
DefaultThreadStoreAuth,
@@ -703,6 +708,57 @@ describe('Conversion Testing', () => {
703708
});
704709
});
705710

711+
test('POST /api/convert keeps the docChildren block in JSON and leaves no trace of it in HTML or Markdown', async () => {
712+
const app = initApp();
713+
// Written the way the frontend writes it, with its own docChildren spec,
714+
// so the test fails if the server schema drops the block when reading
715+
const frontendEditor = ServerBlockNoteEditor.create({
716+
schema: BlockNoteSchema.create({
717+
blockSpecs: {
718+
...defaultBlockSpecs,
719+
docChildren: createBlockSpec(
720+
{ type: 'docChildren', propSchema: {}, content: 'none' },
721+
{ render: () => ({ dom: document.createElement('div') }) },
722+
)(),
723+
},
724+
}),
725+
});
726+
const yDocument = frontendEditor.blocksToYDoc(
727+
[
728+
{ type: 'paragraph' as const, content: 'Before' },
729+
{ type: 'docChildren' as const },
730+
{ type: 'paragraph' as const, content: 'After' },
731+
],
732+
'document-store',
733+
);
734+
const yjsUpdate = Buffer.from(Y.encodeStateAsUpdate(yDocument));
735+
const convert = (accept: string) =>
736+
request(app)
737+
.post('/api/convert')
738+
.set('origin', origin)
739+
.set('authorization', `Bearer ${apiKey}`)
740+
.set('content-type', 'application/vnd.yjs.doc')
741+
.set('accept', accept)
742+
.send(yjsUpdate);
743+
744+
const jsonResponse = await convert('application/json');
745+
expect(jsonResponse.status).toBe(200);
746+
expect(
747+
(jsonResponse.body as { type: string }[]).map((block) => block.type),
748+
).toContain('docChildren');
749+
750+
const htmlResponse = await convert('text/html');
751+
expect(htmlResponse.status).toBe(200);
752+
expect(htmlResponse.text).toContain('After');
753+
expect(htmlResponse.text).not.toContain('\uFFFC');
754+
755+
const markdownResponse = await convert('text/markdown');
756+
expect(markdownResponse.status).toBe(200);
757+
expect(markdownResponse.text).toContain('Before');
758+
expect(markdownResponse.text).toContain('After');
759+
expect(markdownResponse.text).not.toContain('\uFFFC');
760+
});
761+
706762
test('POST /api/convert with invalid Yjs content returns 400', async () => {
707763
const destroySpy = vi.spyOn(Y.Doc.prototype, 'destroy');
708764
const app = initApp();
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { createBlockSpec } from '@blocknote/core';
2+
3+
// Must stay in sync with the frontend DocChildrenBlock
4+
// (custom-blocks/DocChildrenBlock.tsx).
5+
const docChildrenConfig = {
6+
type: 'docChildren' as const,
7+
propSchema: {},
8+
content: 'none' as const,
9+
};
10+
11+
// Lists the sub-docs of the doc, which the editor fetches when it renders the
12+
// block. Conversion only has the document, not its sub-docs, so the block
13+
// renders as an empty marker: it round-trips in JSON and leaves nothing in
14+
// HTML or Markdown.
15+
export const DocChildrenBlock = createBlockSpec(docChildrenConfig, {
16+
render: () => {
17+
const dom = document.createElement('div');
18+
dom.setAttribute('data-content-type', 'docChildren');
19+
return { dom };
20+
},
21+
toExternalHTML: () => {
22+
const dom = document.createElement('div');
23+
dom.setAttribute('data-content-type', 'docChildren');
24+
return { dom };
25+
},
26+
});

‎src/frontend/servers/y-provider/src/blockSpecs/index.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
} from '@blocknote/core';
77

88
import { CalloutBlock } from './Callout';
9+
import { DocChildrenBlock } from './DocChildren';
910
import { InterlinkingLinkInline } from './InterlinkingLinkInline';
1011
import { PdfBlock } from './Pdf';
1112
import { UploadLoaderBlock } from './UploadLoader';
@@ -17,6 +18,7 @@ export const docsBlockNoteSchema = withPageBreak(
1718
blockSpecs: {
1819
...defaultBlockSpecs,
1920
callout: CalloutBlock(),
21+
docChildren: DocChildrenBlock(),
2022
pdf: PdfBlock(),
2123
uploadLoader: UploadLoaderBlock(),
2224
},

0 commit comments

Comments
 (0)