Skip to content

Commit bf47f63

Browse files
authored
Align default document initialization ids (#370)
* Align default document initialization ids * Use UUID view id in publish snapshot fixture * Fix embedded row document initialization
1 parent 0349102 commit bf47f63

6 files changed

Lines changed: 203 additions & 122 deletions

File tree

src/application/publish-snapshot/__fixtures__/published-page-snapshots.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ export const publishedDocumentPayload: PublishedDocumentSnapshotPayload = {
1818
namespace: 'published-namespace',
1919
publishName: 'published-document',
2020
view: {
21-
viewId: 'published-document-view-id',
21+
viewId: '6e91148b-e42a-56b1-b9a0-58fbaa31552d',
2222
name: 'Published document',
2323
icon: {
2424
ty: ViewIconType.Icon,

src/application/slate-yjs/__tests__/yjs-utils.test.ts

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import * as Y from 'yjs';
44
import {
55
createBlock,
66
createEmptyDocument,
7+
deleteBlock,
78
getBlock,
89
getChildrenArray,
910
getText,
@@ -78,11 +79,12 @@ describe('pageIdFromDocumentId', () => {
7879

7980
// Should be a valid UUID format
8081
expect(pageId1).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/);
82+
expect(pageId1).toBe('69d1e3fb-c2f3-5156-b8e3-636273bad252');
8183
});
8284

8385
it('should generate different page_ids for different document_ids', () => {
8486
const documentId1 = '6e91148b-e42a-56b1-b9a0-58fbaa31552d';
85-
const documentId2 = '7f02259c-f53b-67c2-c1b1-69gcbb42663e';
87+
const documentId2 = '7f02259c-f53b-67c2-a1b1-69fcbb42663e';
8688

8789
const pageId1 = pageIdFromDocumentId(documentId1);
8890
const pageId2 = pageIdFromDocumentId(documentId2);
@@ -171,6 +173,63 @@ describe('initializeDocumentStructure', () => {
171173
expect(pageId).toBe(expectedPageId);
172174
});
173175

176+
it('should use document-scoped initial paragraph ids when documentId is provided', () => {
177+
const documentId = '6e91148b-e42a-56b1-b9a0-58fbaa31552d';
178+
initializeDocumentStructure(doc, true, documentId);
179+
180+
const sharedRoot = doc.getMap(YjsEditorKey.data_section) as YSharedRoot;
181+
const document = sharedRoot.get(YjsEditorKey.document);
182+
const pageId = document.get(YjsEditorKey.page_id);
183+
const blocks = document.get(YjsEditorKey.blocks);
184+
const meta = document.get(YjsEditorKey.meta);
185+
const childrenMap = meta.get(YjsEditorKey.children_map);
186+
const textMap = meta.get(YjsEditorKey.text_map);
187+
const pageChildren = childrenMap.get(pageId);
188+
const paragraphId = pageChildren.get(0);
189+
const paragraphBlock = blocks.get(paragraphId);
190+
191+
expect(paragraphId).toBe('TdtM1tmgqJ');
192+
expect(paragraphBlock.get(YjsEditorKey.block_children)).toBe('NyYr0DfnAu');
193+
expect(paragraphBlock.get(YjsEditorKey.block_external_id)).toBe('VbaxI-lEf5');
194+
expect(childrenMap.has('NyYr0DfnAu')).toBe(true);
195+
expect(textMap.has('VbaxI-lEf5')).toBe(true);
196+
});
197+
198+
it('should not override the local Yjs client id when initializing document structure', () => {
199+
const documentId = '6e91148b-e42a-56b1-b9a0-58fbaa31552d';
200+
const originalClientId = doc.clientID;
201+
202+
initializeDocumentStructure(doc, true, documentId);
203+
204+
expect(doc.clientID).toBe(originalClientId);
205+
});
206+
207+
it('should clean up document-scoped paragraph children and text ids when deleted', () => {
208+
const documentId = '6e91148b-e42a-56b1-b9a0-58fbaa31552d';
209+
210+
initializeDocumentStructure(doc, true, documentId);
211+
212+
const sharedRoot = doc.getMap(YjsEditorKey.data_section) as YSharedRoot;
213+
const document = sharedRoot.get(YjsEditorKey.document);
214+
const pageId = document.get(YjsEditorKey.page_id);
215+
const blocks = document.get(YjsEditorKey.blocks);
216+
const meta = document.get(YjsEditorKey.meta);
217+
const childrenMap = meta.get(YjsEditorKey.children_map);
218+
const textMap = meta.get(YjsEditorKey.text_map);
219+
const pageChildren = childrenMap.get(pageId);
220+
const paragraphId = pageChildren.get(0);
221+
const paragraphBlock = blocks.get(paragraphId);
222+
const paragraphChildrenId = paragraphBlock.get(YjsEditorKey.block_children);
223+
const paragraphTextId = paragraphBlock.get(YjsEditorKey.block_external_id);
224+
225+
deleteBlock(sharedRoot, paragraphId);
226+
227+
expect(blocks.has(paragraphId)).toBe(false);
228+
expect(childrenMap.has(paragraphChildrenId)).toBe(false);
229+
expect(textMap.has(paragraphTextId)).toBe(false);
230+
expect(pageChildren.toArray()).toEqual([]);
231+
});
232+
174233
it('should skip initialization if document already exists', () => {
175234
// First initialization
176235
initializeDocumentStructure(doc, false);

src/application/slate-yjs/utils/yjs.ts

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ import {
2424
} from '@/application/types';
2525
import { Log } from '@/utils/log';
2626

27+
const RUST_NANOID_SAFE_ALPHABET = '_-0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
28+
const DEFAULT_ID_LEN = 10;
2729
// UUID namespace OID (same as Rust's Uuid::NAMESPACE_OID)
2830
// Note: 6ba7b812 (not 6ba7b810 which is NAMESPACE_DNS)
2931
const UUID_NAMESPACE_OID = '6ba7b812-9dad-11d1-80b4-00c04fd430c8';
@@ -60,6 +62,25 @@ export function pageIdFromDocumentId(documentId: string): string {
6062
return pageId;
6163
}
6264

65+
function idFromDocumentId(documentId: string, role: string): string {
66+
const docUuid = uuidValidate(documentId) ? documentId : uuidv5(documentId, UUID_NAMESPACE_OID);
67+
68+
return uuidv5(role, docUuid);
69+
}
70+
71+
function nanoidFromDocumentId(documentId: string, role: string): string {
72+
const uuid = idFromDocumentId(documentId, role).replace(/-/g, '');
73+
let id = '';
74+
75+
for (let i = 0; i < DEFAULT_ID_LEN; i += 1) {
76+
const byte = parseInt(uuid.slice(i * 2, i * 2 + 2), 16);
77+
78+
id += RUST_NANOID_SAFE_ALPHABET[byte & 0x3f];
79+
}
80+
81+
return id;
82+
}
83+
6384
export function getTextMap(sharedRoot: YSharedRoot) {
6485
const document = sharedRoot.get(YjsEditorKey.document);
6586
const meta = document.get(YjsEditorKey.meta) as YMeta;
@@ -355,21 +376,23 @@ export function initializeDocumentStructure(doc: YDoc, includeInitialParagraph =
355376
if (includeInitialParagraph) {
356377
// Create an empty paragraph block as child of page
357378
// The Slate editor requires at least one text block to render properly
358-
const paragraphId = nanoid(8);
379+
const paragraphId = documentId ? nanoidFromDocumentId(documentId, 'block') : nanoid(8);
380+
const paragraphChildrenId = documentId ? nanoidFromDocumentId(documentId, 'children') : paragraphId;
381+
const paragraphTextId = documentId ? nanoidFromDocumentId(documentId, 'text') : paragraphId;
359382
const paragraphBlock = new Y.Map();
360383

361384
paragraphBlock.set(YjsEditorKey.block_id, paragraphId);
362385
paragraphBlock.set(YjsEditorKey.block_type, BlockType.Paragraph);
363-
paragraphBlock.set(YjsEditorKey.block_children, paragraphId);
364-
paragraphBlock.set(YjsEditorKey.block_external_id, paragraphId);
386+
paragraphBlock.set(YjsEditorKey.block_children, paragraphChildrenId);
387+
paragraphBlock.set(YjsEditorKey.block_external_id, paragraphTextId);
365388
paragraphBlock.set(YjsEditorKey.block_external_type, YjsEditorKey.text);
366389
paragraphBlock.set(YjsEditorKey.block_data, '{}');
367390
paragraphBlock.set(YjsEditorKey.block_parent, pageId);
368391
blocks.set(paragraphId, paragraphBlock);
369392

370393
pageChildren.push([paragraphId]);
371-
childrenMap.set(paragraphId, new Y.Array());
372-
textMap.set(paragraphId, new Y.Text());
394+
childrenMap.set(paragraphChildrenId, new Y.Array());
395+
textMap.set(paragraphTextId, new Y.Text());
373396
}
374397

375398
childrenMap.set(pageId, pageChildren);
@@ -448,6 +471,8 @@ export function deleteBlock(sharedRoot: YSharedRoot, blockId: string) {
448471
const meta = document.get(YjsEditorKey.meta) as YMeta;
449472
const childrenMap = meta.get(YjsEditorKey.children_map) as YChildrenMap;
450473
const textMap = meta.get(YjsEditorKey.text_map) as YTextMap;
474+
const blockChildrenId = block.get(YjsEditorKey.block_children);
475+
const blockExternalId = block.get(YjsEditorKey.block_external_id);
451476

452477
const parent = getBlock(parentId, sharedRoot);
453478

@@ -465,8 +490,8 @@ export function deleteBlock(sharedRoot: YSharedRoot, blockId: string) {
465490
}
466491

467492
blocks.delete(blockId);
468-
childrenMap.delete(blockId);
469-
textMap.delete(blockId);
493+
childrenMap.delete(blockChildrenId);
494+
textMap.delete(blockExternalId);
470495

471496
// delete parent if it's empty column block
472497
if (parentType === BlockType.ColumnBlock && afterDeletedLength === 0) {

0 commit comments

Comments
 (0)