mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-08-23 21:06:22 +08:00
feat(core): add plain text adapter for database block (#9225)
[BS-2059](https://linear.app/affine-design/issue/BS-2059/补齐-database-plain-text-adapter)
This commit is contained in:
@@ -1186,4 +1186,229 @@ describe('snapshot to plain text', () => {
|
|||||||
});
|
});
|
||||||
expect(target.file).toBe(plainText);
|
expect(target.file).toBe(plainText);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('table', async () => {
|
||||||
|
const blockSnapshot: BlockSnapshot = {
|
||||||
|
type: 'block',
|
||||||
|
id: 'block:8Wb7CSJ9Qe',
|
||||||
|
flavour: 'affine:database',
|
||||||
|
props: {
|
||||||
|
cells: {
|
||||||
|
'block:P_-Wg7Rg9O': {
|
||||||
|
'block:qyo8q9VPWU': {
|
||||||
|
columnId: 'block:qyo8q9VPWU',
|
||||||
|
value: 'TKip9uc7Yx',
|
||||||
|
},
|
||||||
|
'block:5cglrBmAr3': {
|
||||||
|
columnId: 'block:5cglrBmAr3',
|
||||||
|
value: 1702598400000,
|
||||||
|
},
|
||||||
|
'block:8Fa0JQe7WY': {
|
||||||
|
columnId: 'block:8Fa0JQe7WY',
|
||||||
|
value: 1,
|
||||||
|
},
|
||||||
|
'block:5ej6StPuF_': {
|
||||||
|
columnId: 'block:5ej6StPuF_',
|
||||||
|
value: 65,
|
||||||
|
},
|
||||||
|
'block:DPhZ6JBziD': {
|
||||||
|
columnId: 'block:DPhZ6JBziD',
|
||||||
|
value: ['-2_QD3GZT1', '73UrEZWaKk'],
|
||||||
|
},
|
||||||
|
'block:O8dpIDiP7-': {
|
||||||
|
columnId: 'block:O8dpIDiP7-',
|
||||||
|
value: {
|
||||||
|
'$blocksuite:internal:text$': true,
|
||||||
|
delta: [
|
||||||
|
{
|
||||||
|
insert: 'test2',
|
||||||
|
attributes: {
|
||||||
|
link: 'https://google.com',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'block:U8lPD59MkF': {
|
||||||
|
columnId: 'block:U8lPD59MkF',
|
||||||
|
value: 'https://google.com',
|
||||||
|
},
|
||||||
|
'block:-DT7B0TafG': {
|
||||||
|
columnId: 'block:-DT7B0TafG',
|
||||||
|
value: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'block:0vhfgcHtPF': {
|
||||||
|
'block:qyo8q9VPWU': {
|
||||||
|
columnId: 'block:qyo8q9VPWU',
|
||||||
|
value: 'F2bgsaE3X2',
|
||||||
|
},
|
||||||
|
'block:O8dpIDiP7-': {
|
||||||
|
columnId: 'block:O8dpIDiP7-',
|
||||||
|
value: {
|
||||||
|
'$blocksuite:internal:text$': true,
|
||||||
|
delta: [
|
||||||
|
{
|
||||||
|
insert: 'test1',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'block:5cglrBmAr3': {
|
||||||
|
columnId: 'block:5cglrBmAr3',
|
||||||
|
value: 1703030400000,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'block:b4_02QXMAM': {
|
||||||
|
'block:qyo8q9VPWU': {
|
||||||
|
columnId: 'block:qyo8q9VPWU',
|
||||||
|
value: 'y3O1A2IHHu',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'block:W_eirvg7EJ': {
|
||||||
|
'block:qyo8q9VPWU': {
|
||||||
|
columnId: 'block:qyo8q9VPWU',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
type: 'title',
|
||||||
|
name: 'Title',
|
||||||
|
data: {},
|
||||||
|
id: 'block:2VfUaitjf9',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'select',
|
||||||
|
name: 'Status',
|
||||||
|
data: {
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
id: 'TKip9uc7Yx',
|
||||||
|
color: 'var(--affine-tag-white)',
|
||||||
|
value: 'TODO',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'F2bgsaE3X2',
|
||||||
|
color: 'var(--affine-tag-green)',
|
||||||
|
value: 'In Progress',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'y3O1A2IHHu',
|
||||||
|
color: 'var(--affine-tag-gray)',
|
||||||
|
value: 'Done',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
id: 'block:qyo8q9VPWU',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'date',
|
||||||
|
name: 'Date',
|
||||||
|
data: {},
|
||||||
|
id: 'block:5cglrBmAr3',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'number',
|
||||||
|
name: 'Number',
|
||||||
|
data: {
|
||||||
|
decimal: 0,
|
||||||
|
},
|
||||||
|
id: 'block:8Fa0JQe7WY',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'progress',
|
||||||
|
name: 'Progress',
|
||||||
|
data: {},
|
||||||
|
id: 'block:5ej6StPuF_',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'multi-select',
|
||||||
|
name: 'MultiSelect',
|
||||||
|
data: {
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
id: '73UrEZWaKk',
|
||||||
|
value: 'test2',
|
||||||
|
color: 'var(--affine-tag-purple)',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '-2_QD3GZT1',
|
||||||
|
value: 'test1',
|
||||||
|
color: 'var(--affine-tag-teal)',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
id: 'block:DPhZ6JBziD',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'rich-text',
|
||||||
|
name: 'RichText',
|
||||||
|
data: {},
|
||||||
|
id: 'block:O8dpIDiP7-',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'link',
|
||||||
|
name: 'Link',
|
||||||
|
data: {},
|
||||||
|
id: 'block:U8lPD59MkF',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'checkbox',
|
||||||
|
name: 'Checkbox',
|
||||||
|
data: {},
|
||||||
|
id: 'block:-DT7B0TafG',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
type: 'block',
|
||||||
|
id: 'block:P_-Wg7Rg9O',
|
||||||
|
flavour: 'affine:paragraph',
|
||||||
|
props: {
|
||||||
|
type: 'text',
|
||||||
|
text: {
|
||||||
|
'$blocksuite:internal:text$': true,
|
||||||
|
delta: [
|
||||||
|
{
|
||||||
|
insert: 'Task 1',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'block',
|
||||||
|
id: 'block:0vhfgcHtPF',
|
||||||
|
flavour: 'affine:paragraph',
|
||||||
|
props: {
|
||||||
|
type: 'text',
|
||||||
|
text: {
|
||||||
|
'$blocksuite:internal:text$': true,
|
||||||
|
delta: [
|
||||||
|
{
|
||||||
|
insert: 'Task 2',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const plainText = `\
|
||||||
|
| Title | Status | Date | Number | Progress | MultiSelect | RichText | Link | Checkbox |
|
||||||
|
| ------ | ----------- | ---------- | ------ | -------- | ----------- | ------------------------- | ------------------ | -------- |
|
||||||
|
| Task 1 | TODO | 2023-12-15 | 1 | 65 | test1,test2 | test2: https://google.com | https://google.com | true |
|
||||||
|
| Task 2 | In Progress | 2023-12-20 | | | | test1 | | |
|
||||||
|
`;
|
||||||
|
const plainTextAdapter = new PlainTextAdapter(createJob());
|
||||||
|
const target = await plainTextAdapter.fromBlockSnapshot({
|
||||||
|
snapshot: blockSnapshot,
|
||||||
|
});
|
||||||
|
expect(target.file).toBe(plainText);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import type { BlockPlainTextAdapterMatcher } from '@blocksuite/affine-shared/ada
|
|||||||
|
|
||||||
import { bookmarkBlockPlainTextAdapterMatcher } from '../../../bookmark-block/adapters/plain-text.js';
|
import { bookmarkBlockPlainTextAdapterMatcher } from '../../../bookmark-block/adapters/plain-text.js';
|
||||||
import { codeBlockPlainTextAdapterMatcher } from '../../../code-block/adapters/plain-text.js';
|
import { codeBlockPlainTextAdapterMatcher } from '../../../code-block/adapters/plain-text.js';
|
||||||
|
import { databaseBlockPlainTextAdapterMatcher } from '../../../database-block/adapters/plain-text.js';
|
||||||
import { dividerBlockPlainTextAdapterMatcher } from '../../../divider-block/adapters/plain-text.js';
|
import { dividerBlockPlainTextAdapterMatcher } from '../../../divider-block/adapters/plain-text.js';
|
||||||
import { latexBlockPlainTextAdapterMatcher } from '../../../latex-block/adapters/plain-text.js';
|
import { latexBlockPlainTextAdapterMatcher } from '../../../latex-block/adapters/plain-text.js';
|
||||||
|
|
||||||
@@ -29,4 +30,5 @@ export const defaultBlockPlainTextAdapterMatchers: BlockPlainTextAdapterMatcher[
|
|||||||
embedLinkedDocBlockPlainTextAdapterMatcher,
|
embedLinkedDocBlockPlainTextAdapterMatcher,
|
||||||
embedSyncedDocBlockPlainTextAdapterMatcher,
|
embedSyncedDocBlockPlainTextAdapterMatcher,
|
||||||
latexBlockPlainTextAdapterMatcher,
|
latexBlockPlainTextAdapterMatcher,
|
||||||
|
databaseBlockPlainTextAdapterMatcher,
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -3,9 +3,11 @@ import type { ExtensionType } from '@blocksuite/block-std';
|
|||||||
import { DatabaseBlockHtmlAdapterExtension } from './html.js';
|
import { DatabaseBlockHtmlAdapterExtension } from './html.js';
|
||||||
import { DatabaseBlockMarkdownAdapterExtension } from './markdown.js';
|
import { DatabaseBlockMarkdownAdapterExtension } from './markdown.js';
|
||||||
import { DatabaseBlockNotionHtmlAdapterExtension } from './notion-html.js';
|
import { DatabaseBlockNotionHtmlAdapterExtension } from './notion-html.js';
|
||||||
|
import { DatabaseBlockPlainTextAdapterExtension } from './plain-text.js';
|
||||||
|
|
||||||
export const DatabaseBlockAdapterExtensions: ExtensionType[] = [
|
export const DatabaseBlockAdapterExtensions: ExtensionType[] = [
|
||||||
DatabaseBlockHtmlAdapterExtension,
|
DatabaseBlockHtmlAdapterExtension,
|
||||||
DatabaseBlockMarkdownAdapterExtension,
|
DatabaseBlockMarkdownAdapterExtension,
|
||||||
DatabaseBlockNotionHtmlAdapterExtension,
|
DatabaseBlockNotionHtmlAdapterExtension,
|
||||||
|
DatabaseBlockPlainTextAdapterExtension,
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -0,0 +1,92 @@
|
|||||||
|
import {
|
||||||
|
type Column,
|
||||||
|
DatabaseBlockSchema,
|
||||||
|
type SerializedCells,
|
||||||
|
} from '@blocksuite/affine-model';
|
||||||
|
import {
|
||||||
|
BlockPlainTextAdapterExtension,
|
||||||
|
type BlockPlainTextAdapterMatcher,
|
||||||
|
} from '@blocksuite/affine-shared/adapters';
|
||||||
|
import type { DeltaInsert } from '@blocksuite/inline';
|
||||||
|
import type { BlockSnapshot } from '@blocksuite/store';
|
||||||
|
import { format } from 'date-fns/format';
|
||||||
|
|
||||||
|
import { formatTable } from './utils.js';
|
||||||
|
|
||||||
|
export const databaseBlockPlainTextAdapterMatcher: BlockPlainTextAdapterMatcher =
|
||||||
|
{
|
||||||
|
flavour: DatabaseBlockSchema.model.flavour,
|
||||||
|
toMatch: () => false,
|
||||||
|
fromMatch: o => o.node.flavour === DatabaseBlockSchema.model.flavour,
|
||||||
|
toBlockSnapshot: {},
|
||||||
|
fromBlockSnapshot: {
|
||||||
|
enter: (o, context) => {
|
||||||
|
const { walkerContext, deltaConverter } = context;
|
||||||
|
const rows: string[][] = [];
|
||||||
|
const columns = o.node.props.columns as Array<Column>;
|
||||||
|
const children = o.node.children;
|
||||||
|
const cells = o.node.props.cells as SerializedCells;
|
||||||
|
const tableCells = children.map((v: BlockSnapshot) =>
|
||||||
|
columns.map(col => {
|
||||||
|
const cell = cells[v.id]?.[col.id];
|
||||||
|
if (!cell && col.type !== 'title') {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
switch (col.type) {
|
||||||
|
case 'rich-text':
|
||||||
|
return deltaConverter
|
||||||
|
.deltaToAST((cell.value as { delta: DeltaInsert[] }).delta)
|
||||||
|
.join('');
|
||||||
|
case 'title':
|
||||||
|
return deltaConverter
|
||||||
|
.deltaToAST((v.props.text as { delta: DeltaInsert[] }).delta)
|
||||||
|
.join('');
|
||||||
|
case 'date':
|
||||||
|
return format(new Date(cell.value as number), 'yyyy-MM-dd');
|
||||||
|
case 'select': {
|
||||||
|
const value = (
|
||||||
|
col.data as { options: Array<Record<string, string>> }
|
||||||
|
).options.find(opt => opt.id === cell.value)?.value;
|
||||||
|
return value || '';
|
||||||
|
}
|
||||||
|
case 'multi-select': {
|
||||||
|
const value = (cell.value as string[])
|
||||||
|
.map(
|
||||||
|
val =>
|
||||||
|
(
|
||||||
|
col.data as { options: Array<Record<string, string>> }
|
||||||
|
).options.find(opt => val === opt.id)?.value
|
||||||
|
)
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(',');
|
||||||
|
return value || '';
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return String(cell.value);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// Handle first row.
|
||||||
|
if (Array.isArray(columns)) {
|
||||||
|
rows.push(columns.map(col => col.name));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle 2-... rows
|
||||||
|
tableCells.forEach(children => {
|
||||||
|
rows.push(children);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Convert rows to table string
|
||||||
|
const tableString = formatTable(rows);
|
||||||
|
|
||||||
|
context.textBuffer.content += tableString;
|
||||||
|
context.textBuffer.content += '\n';
|
||||||
|
|
||||||
|
walkerContext.skipAllChildren();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DatabaseBlockPlainTextAdapterExtension =
|
||||||
|
BlockPlainTextAdapterExtension(databaseBlockPlainTextAdapterMatcher);
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
function calculateColumnWidths(rows: string[][]): number[] {
|
||||||
|
return rows[0].map((_, colIndex) =>
|
||||||
|
Math.max(...rows.map(row => (row[colIndex] || '').length))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatRow(
|
||||||
|
row: string[],
|
||||||
|
columnWidths: number[],
|
||||||
|
isHeader: boolean
|
||||||
|
): string {
|
||||||
|
const cells = row.map((cell, colIndex) =>
|
||||||
|
cell.padEnd(columnWidths[colIndex], ' ')
|
||||||
|
);
|
||||||
|
const rowString = `| ${cells.join(' | ')} |`;
|
||||||
|
return isHeader
|
||||||
|
? `${rowString}\n${formatSeparator(columnWidths)}`
|
||||||
|
: rowString;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatSeparator(columnWidths: number[]): string {
|
||||||
|
const separator = columnWidths.map(width => '-'.repeat(width)).join(' | ');
|
||||||
|
return `| ${separator} |`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatTable(rows: string[][]): string {
|
||||||
|
const columnWidths = calculateColumnWidths(rows);
|
||||||
|
const formattedRows = rows.map((row, index) =>
|
||||||
|
formatRow(row, columnWidths, index === 0)
|
||||||
|
);
|
||||||
|
return formattedRows.join('\n');
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user