mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-27 02:42:25 +08:00
@@ -2,7 +2,13 @@ import { readFileSync } from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
import { expect, test } from 'vitest';
|
||||
import { applyUpdate, Array as YArray, Doc as YDoc, Map as YMap } from 'yjs';
|
||||
import {
|
||||
applyUpdate,
|
||||
Array as YArray,
|
||||
Doc as YDoc,
|
||||
Map as YMap,
|
||||
Text as YText,
|
||||
} from 'yjs';
|
||||
|
||||
import {
|
||||
parsePageDoc,
|
||||
@@ -160,3 +166,79 @@ test('should parse page full doc work with ai editable', () => {
|
||||
|
||||
expect(result.md).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should index references from database rich-text cells', async () => {
|
||||
const doc = new YDoc({
|
||||
guid: 'db-doc',
|
||||
});
|
||||
const blocks = doc.getMap('blocks');
|
||||
|
||||
const pageTitle = new YText();
|
||||
pageTitle.insert(0, 'Page');
|
||||
const page = new YMap();
|
||||
page.set('sys:id', 'page');
|
||||
page.set('sys:flavour', 'affine:page');
|
||||
page.set('sys:children', YArray.from(['note']));
|
||||
page.set('prop:title', pageTitle);
|
||||
blocks.set('page', page);
|
||||
|
||||
const note = new YMap();
|
||||
note.set('sys:id', 'note');
|
||||
note.set('sys:flavour', 'affine:note');
|
||||
note.set('sys:children', YArray.from(['db']));
|
||||
note.set('prop:displayMode', 'page');
|
||||
blocks.set('note', note);
|
||||
|
||||
const dbTitle = new YText();
|
||||
dbTitle.insert(0, 'Database');
|
||||
const db = new YMap();
|
||||
db.set('sys:id', 'db');
|
||||
db.set('sys:flavour', 'affine:database');
|
||||
db.set('sys:children', new YArray());
|
||||
db.set('prop:title', dbTitle);
|
||||
|
||||
const columns = new YArray();
|
||||
const column = new YMap();
|
||||
column.set('id', 'col1');
|
||||
column.set('name', 'Text');
|
||||
column.set('type', 'rich-text');
|
||||
column.set('data', new YMap());
|
||||
columns.push([column]);
|
||||
db.set('prop:columns', columns);
|
||||
|
||||
const cellText = new YText();
|
||||
cellText.applyDelta([
|
||||
{ insert: 'See ' },
|
||||
{
|
||||
insert: 'Target',
|
||||
attributes: {
|
||||
reference: {
|
||||
pageId: 'target-doc',
|
||||
params: { mode: 'page' },
|
||||
},
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const cell = new YMap();
|
||||
cell.set('columnId', 'col1');
|
||||
cell.set('value', cellText);
|
||||
const row = new YMap();
|
||||
row.set('col1', cell);
|
||||
const cells = new YMap();
|
||||
cells.set('row1', row);
|
||||
db.set('prop:cells', cells);
|
||||
|
||||
blocks.set('db', db);
|
||||
|
||||
const result = await readAllBlocksFromDoc({
|
||||
ydoc: doc,
|
||||
spaceId: 'test-space',
|
||||
});
|
||||
|
||||
const dbBlock = result?.blocks.find(block => block.blockId === 'db');
|
||||
expect(dbBlock?.refDocId).toEqual(['target-doc']);
|
||||
expect(dbBlock?.ref).toEqual([
|
||||
JSON.stringify({ docId: 'target-doc', mode: 'page' }),
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
type TransformerMiddleware,
|
||||
type YBlock,
|
||||
} from '@blocksuite/affine/store';
|
||||
import { uniq } from 'lodash-es';
|
||||
import { uniqBy } from 'lodash-es';
|
||||
import {
|
||||
Array as YArray,
|
||||
type Doc as YDoc,
|
||||
@@ -58,6 +58,81 @@ const bookmarkFlavours = new Set([
|
||||
'affine:embed-loom',
|
||||
]);
|
||||
|
||||
const collectInlineReferences = (
|
||||
deltas: DeltaInsert<AffineTextAttributes>[]
|
||||
): { refDocId: string; ref: string }[] =>
|
||||
uniqBy(
|
||||
deltas
|
||||
.flatMap(delta => {
|
||||
if (
|
||||
delta.attributes &&
|
||||
delta.attributes.reference &&
|
||||
delta.attributes.reference.pageId
|
||||
) {
|
||||
const { pageId: refDocId, params = {} } = delta.attributes.reference;
|
||||
return {
|
||||
refDocId,
|
||||
ref: JSON.stringify({ docId: refDocId, ...params }),
|
||||
};
|
||||
}
|
||||
return null;
|
||||
})
|
||||
.filter((ref): ref is { refDocId: string; ref: string } => ref !== null),
|
||||
item => item.ref
|
||||
);
|
||||
|
||||
const getTextDeltasFromCellValue = (
|
||||
value: unknown
|
||||
): DeltaInsert<AffineTextAttributes>[] | null => {
|
||||
if (!value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (value instanceof YText) {
|
||||
return value.toDelta() as DeltaInsert<AffineTextAttributes>[];
|
||||
}
|
||||
|
||||
if (typeof value === 'object' && value !== null) {
|
||||
const maybeText = value as { yText?: unknown };
|
||||
if (maybeText.yText instanceof YText) {
|
||||
return maybeText.yText.toDelta() as DeltaInsert<AffineTextAttributes>[];
|
||||
}
|
||||
}
|
||||
|
||||
if (value instanceof YMap) {
|
||||
const marker = value.get('$blocksuite:internal:text$');
|
||||
const delta = value.get('delta');
|
||||
if (marker) {
|
||||
if (delta instanceof YArray) {
|
||||
return delta
|
||||
.toArray()
|
||||
.map(entry => (entry instanceof YMap ? entry.toJSON() : entry)) as
|
||||
| DeltaInsert<AffineTextAttributes>[]
|
||||
| null;
|
||||
}
|
||||
if (Array.isArray(delta)) {
|
||||
return delta as DeltaInsert<AffineTextAttributes>[];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
typeof value === 'object' &&
|
||||
value !== null &&
|
||||
'$blocksuite:internal:text$' in value
|
||||
) {
|
||||
const delta = (value as { delta?: unknown }).delta;
|
||||
if (delta instanceof YArray) {
|
||||
return delta.toArray() as DeltaInsert<AffineTextAttributes>[];
|
||||
}
|
||||
if (Array.isArray(delta)) {
|
||||
return delta as DeltaInsert<AffineTextAttributes>[];
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
function generateMarkdownPreviewBuilder(
|
||||
workspaceId: string,
|
||||
blocks: BlockDocumentInfo[],
|
||||
@@ -587,25 +662,7 @@ export async function readAllBlocksFromDoc({
|
||||
}
|
||||
|
||||
const deltas: DeltaInsert<AffineTextAttributes>[] = text.toDelta();
|
||||
const refs = uniq(
|
||||
deltas
|
||||
.flatMap(delta => {
|
||||
if (
|
||||
delta.attributes &&
|
||||
delta.attributes.reference &&
|
||||
delta.attributes.reference.pageId
|
||||
) {
|
||||
const { pageId: refDocId, params = {} } =
|
||||
delta.attributes.reference;
|
||||
return {
|
||||
refDocId,
|
||||
ref: JSON.stringify({ docId: refDocId, ...params }),
|
||||
};
|
||||
}
|
||||
return null;
|
||||
})
|
||||
.filter(ref => !!ref)
|
||||
);
|
||||
const refs = collectInlineReferences(deltas);
|
||||
|
||||
const databaseName =
|
||||
flavour === 'affine:paragraph' && parentFlavour === 'affine:database' // if block is a database row
|
||||
@@ -741,6 +798,29 @@ export async function readAllBlocksFromDoc({
|
||||
}
|
||||
}
|
||||
|
||||
const databaseRefs: { refDocId: string; ref: string }[] = [];
|
||||
const cellsObj = block.get('prop:cells');
|
||||
if (cellsObj instanceof YMap) {
|
||||
for (const row of cellsObj.values()) {
|
||||
if (!(row instanceof YMap)) {
|
||||
continue;
|
||||
}
|
||||
for (const cell of row.values()) {
|
||||
if (!(cell instanceof YMap)) {
|
||||
continue;
|
||||
}
|
||||
const deltas = getTextDeltasFromCellValue(cell.get('value'));
|
||||
if (!deltas?.length) {
|
||||
continue;
|
||||
}
|
||||
const refs = collectInlineReferences(deltas);
|
||||
if (refs.length) {
|
||||
databaseRefs.push(...refs);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
blockDocuments.push({
|
||||
...commonBlockProps,
|
||||
content: texts,
|
||||
@@ -748,6 +828,16 @@ export async function readAllBlocksFromDoc({
|
||||
...commonBlockProps.additional,
|
||||
databaseName: databaseTitle?.toString(),
|
||||
},
|
||||
...(databaseRefs.length
|
||||
? databaseRefs.reduce<{ refDocId: string[]; ref: string[] }>(
|
||||
(prev, curr) => {
|
||||
prev.refDocId.push(curr.refDocId);
|
||||
prev.ref.push(curr.ref);
|
||||
return prev;
|
||||
},
|
||||
{ refDocId: [], ref: [] }
|
||||
)
|
||||
: {}),
|
||||
});
|
||||
} else if (flavour === 'affine:latex') {
|
||||
blockDocuments.push({
|
||||
|
||||
Reference in New Issue
Block a user