From c6b8f2b58468ee3c1e9dd806be1bd71aafa2e218 Mon Sep 17 00:00:00 2001 From: Saul-Mirone Date: Mon, 10 Feb 2025 19:09:33 +0000 Subject: [PATCH] refactor(editor): flat data for table block (#10010) close: BS-2548 --- blocksuite/affine/block-table/src/commands.ts | 49 +++++------ .../block-table/src/table-data-manager.ts | 81 ++++++++++--------- .../model/src/blocks/table/table-model.ts | 1 + blocksuite/framework/store/src/consts.ts | 2 +- .../framework/store/src/model/block/block.ts | 3 + .../src/model/block/flat-sync-controller.ts | 10 +-- .../framework/store/src/model/store/crud.ts | 25 ++++-- .../framework/store/src/model/store/utils.ts | 3 +- .../store/src/reactive/flat-native-y.ts | 49 +++++++---- 9 files changed, 125 insertions(+), 98 deletions(-) diff --git a/blocksuite/affine/block-table/src/commands.ts b/blocksuite/affine/block-table/src/commands.ts index 659c1b3f83..8548bb8be2 100644 --- a/blocksuite/affine/block-table/src/commands.ts +++ b/blocksuite/affine/block-table/src/commands.ts @@ -1,7 +1,12 @@ -import { TableModelFlavour } from '@blocksuite/affine-model'; -import { generateFractionalIndexingKeyBetween } from '@blocksuite/affine-shared/utils'; +import { + type TableBlockModel, + TableModelFlavour, +} from '@blocksuite/affine-model'; import type { Command } from '@blocksuite/block-std'; -import { type BlockModel, nanoid, Text } from '@blocksuite/store'; +import { type BlockModel } from '@blocksuite/store'; + +import { TableDataManager } from './table-data-manager'; + export const insertTableBlockCommand: Command< { place?: 'after' | 'before'; @@ -22,41 +27,25 @@ export const insertTableBlockCommand: Command< if (!targetModel) return; - const row1Id = nanoid(); - const row2Id = nanoid(); - const col1Id = nanoid(); - const col2Id = nanoid(); - const order1 = generateFractionalIndexingKeyBetween(null, null); - const order2 = generateFractionalIndexingKeyBetween(order1, null); - - const initialTableData = { - rows: { - [row1Id]: { rowId: row1Id, order: order1 }, - [row2Id]: { rowId: row2Id, order: order2 }, - }, - columns: { - [col1Id]: { columnId: col1Id, order: order1 }, - [col2Id]: { columnId: col2Id, order: order2 }, - }, - cells: { - [`${row1Id}:${col1Id}`]: { text: new Text() }, - [`${row1Id}:${col2Id}`]: { text: new Text() }, - [`${row2Id}:${col1Id}`]: { text: new Text() }, - [`${row2Id}:${col2Id}`]: { text: new Text() }, - }, - }; - const result = std.store.addSiblingBlocks( targetModel, - [{ flavour: TableModelFlavour, ...initialTableData }], + [{ flavour: TableModelFlavour }], place ); const blockId = result[0]; if (blockId == null) return; - if (removeEmptyLine && targetModel.text?.length === 0) { - std.store.deleteBlock(targetModel); + const model = std.store.getBlock(blockId)?.model as TableBlockModel; + if (model == null) return; + + const dataManager = new TableDataManager(model); + + dataManager.addNRow(2); + dataManager.addNColumn(2); + + if (removeEmptyLine && model.text?.length === 0) { + std.store.deleteBlock(model); } next({ insertedTableBlockId: blockId }); diff --git a/blocksuite/affine/block-table/src/table-data-manager.ts b/blocksuite/affine/block-table/src/table-data-manager.ts index 256c4c00b5..aed172ac83 100644 --- a/blocksuite/affine/block-table/src/table-data-manager.ts +++ b/blocksuite/affine/block-table/src/table-data-manager.ts @@ -23,13 +23,13 @@ export class TableDataManager { `${this.virtualRowCount$.value + this.rows$.value.length} x ${this.virtualColumnCount$.value + this.columns$.value.length}` ); rows$ = computed(() => { - return Object.values(this.model.rows$.value).sort((a, b) => + return Object.values(this.model.props.rows$.value).sort((a, b) => a.order > b.order ? 1 : -1 ); }); columns$ = computed(() => { - return Object.values(this.model.columns$.value).sort((a, b) => + return Object.values(this.model.props.columns$.value).sort((a, b) => a.order > b.order ? 1 : -1 ); }); @@ -72,19 +72,20 @@ export class TableDataManager { }); getCell(rowId: string, columnId: string): TableCell | undefined { - return this.model.cells$.value[`${rowId}:${columnId}`]; + return this.model.props.cells$.value[`${rowId}:${columnId}`]; } addRow(after?: number) { const order = this.getOrder(this.rows$.value, after); const rowId = nanoid(); this.model.doc.transact(() => { - this.model.rows[rowId] = { + this.model.props.rows[rowId] = { rowId, order, }; + this.columns$.value.forEach(column => { - this.model.cells[`${rowId}:${column.columnId}`] = { + this.model.props.cells[`${rowId}:${column.columnId}`] = { text: new Text(), }; }); @@ -148,12 +149,12 @@ export class TableDataManager { const order = this.getOrder(this.columns$.value, after); const columnId = nanoid(); this.model.doc.transact(() => { - this.model.columns[columnId] = { + this.model.props.columns[columnId] = { columnId, order, }; this.rows$.value.forEach(row => { - this.model.cells[`${row.rowId}:${columnId}`] = { + this.model.props.cells[`${row.rowId}:${columnId}`] = { text: new Text(), }; }); @@ -163,14 +164,14 @@ export class TableDataManager { deleteRow(rowId: string) { this.model.doc.transact(() => { - Object.keys(this.model.rows).forEach(id => { + Object.keys(this.model.props.rows).forEach(id => { if (id === rowId) { - delete this.model.rows[id]; + delete this.model.props.rows[id]; } }); - Object.keys(this.model.cells).forEach(id => { + Object.keys(this.model.props.cells).forEach(id => { if (id.startsWith(rowId)) { - delete this.model.cells[id]; + delete this.model.props.cells[id]; } }); }); @@ -178,14 +179,14 @@ export class TableDataManager { deleteColumn(columnId: string) { this.model.doc.transact(() => { - Object.keys(this.model.columns).forEach(id => { + Object.keys(this.model.props.columns).forEach(id => { if (id === columnId) { - delete this.model.columns[id]; + delete this.model.props.columns[id]; } }); - Object.keys(this.model.cells).forEach(id => { + Object.keys(this.model.props.cells).forEach(id => { if (id.endsWith(`:${columnId}`)) { - delete this.model.cells[id]; + delete this.model.props.cells[id]; } }); }); @@ -193,51 +194,51 @@ export class TableDataManager { updateRowOrder(rowId: string, newOrder: string) { this.model.doc.transact(() => { - if (this.model.rows[rowId]) { - this.model.rows[rowId].order = newOrder; + if (this.model.props.rows[rowId]) { + this.model.props.rows[rowId].order = newOrder; } }); } updateColumnOrder(columnId: string, newOrder: string) { this.model.doc.transact(() => { - if (this.model.columns[columnId]) { - this.model.columns[columnId].order = newOrder; + if (this.model.props.columns[columnId]) { + this.model.props.columns[columnId].order = newOrder; } }); } setRowBackgroundColor(rowId: string, color?: string) { this.model.doc.transact(() => { - if (this.model.rows[rowId]) { - this.model.rows[rowId].backgroundColor = color; + if (this.model.props.rows[rowId]) { + this.model.props.rows[rowId].backgroundColor = color; } }); } setColumnBackgroundColor(columnId: string, color?: string) { this.model.doc.transact(() => { - if (this.model.columns[columnId]) { - this.model.columns[columnId].backgroundColor = color; + if (this.model.props.columns[columnId]) { + this.model.props.columns[columnId].backgroundColor = color; } }); } setColumnWidth(columnId: string, width: number) { this.model.doc.transact(() => { - if (this.model.columns[columnId]) { - this.model.columns[columnId].width = width; + if (this.model.props.columns[columnId]) { + this.model.props.columns[columnId].width = width; } }); } clearRow(rowId: string) { this.model.doc.transact(() => { - Object.keys(this.model.cells).forEach(id => { + Object.keys(this.model.props.cells).forEach(id => { if (id.startsWith(rowId)) { - this.model.cells[id]?.text.replace( + this.model.props.cells[id]?.text.replace( 0, - this.model.cells[id]?.text.length, + this.model.props.cells[id]?.text.length, '' ); } @@ -247,11 +248,11 @@ export class TableDataManager { clearColumn(columnId: string) { this.model.doc.transact(() => { - Object.keys(this.model.cells).forEach(id => { + Object.keys(this.model.props.cells).forEach(id => { if (id.endsWith(`:${columnId}`)) { - this.model.cells[id]?.text.replace( + this.model.props.cells[id]?.text.replace( 0, - this.model.cells[id]?.text.length, + this.model.props.cells[id]?.text.length, '' ); } @@ -286,7 +287,7 @@ export class TableDataManager { clearCells(cells: { rowId: string; columnId: string }[]) { this.model.doc.transact(() => { cells.forEach(({ rowId, columnId }) => { - const text = this.model.cells[`${rowId}:${columnId}`]?.text; + const text = this.model.props.cells[`${rowId}:${columnId}`]?.text; if (text) { text.replace(0, text.length, ''); } @@ -308,7 +309,7 @@ export class TableDataManager { if (!column) return; const order = this.getOrder(columns, after); this.model.doc.transact(() => { - const realColumn = this.model.columns[column.columnId]; + const realColumn = this.model.props.columns[column.columnId]; if (realColumn) { realColumn.order = order; } @@ -321,7 +322,7 @@ export class TableDataManager { if (!row) return; const order = this.getOrder(rows, after); this.model.doc.transact(() => { - const realRow = this.model.rows[row.rowId]; + const realRow = this.model.props.rows[row.rowId]; if (realRow) { realRow.order = order; } @@ -334,15 +335,15 @@ export class TableDataManager { const order = this.getOrder(this.columns$.value, index); const newColumnId = nanoid(); this.model.doc.transact(() => { - this.model.columns[newColumnId] = { + this.model.props.columns[newColumnId] = { ...oldColumn, columnId: newColumnId, order, }; this.rows$.value.forEach(row => { - this.model.cells[`${row.rowId}:${newColumnId}`] = { + this.model.props.cells[`${row.rowId}:${newColumnId}`] = { text: - this.model.cells[ + this.model.props.cells[ `${row.rowId}:${oldColumn.columnId}` ]?.text.clone() ?? new Text(), }; @@ -357,15 +358,15 @@ export class TableDataManager { const order = this.getOrder(this.rows$.value, index); const newRowId = nanoid(); this.model.doc.transact(() => { - this.model.rows[newRowId] = { + this.model.props.rows[newRowId] = { ...oldRow, rowId: newRowId, order, }; this.columns$.value.forEach(column => { - this.model.cells[`${newRowId}:${column.columnId}`] = { + this.model.props.cells[`${newRowId}:${column.columnId}`] = { text: - this.model.cells[ + this.model.props.cells[ `${oldRow.rowId}:${column.columnId}` ]?.text.clone() ?? new Text(), }; diff --git a/blocksuite/affine/model/src/blocks/table/table-model.ts b/blocksuite/affine/model/src/blocks/table/table-model.ts index 6946ca271b..0de6645fdf 100644 --- a/blocksuite/affine/model/src/blocks/table/table-model.ts +++ b/blocksuite/affine/model/src/blocks/table/table-model.ts @@ -48,6 +48,7 @@ export const TableBlockSchema = defineBlockSchema({ cells: {}, }), metadata: { + isFlatData: true, role: 'content', version: 1, parent: ['affine:note'], diff --git a/blocksuite/framework/store/src/consts.ts b/blocksuite/framework/store/src/consts.ts index db824f3157..518147a189 100644 --- a/blocksuite/framework/store/src/consts.ts +++ b/blocksuite/framework/store/src/consts.ts @@ -4,4 +4,4 @@ export const SCHEMA_NOT_FOUND_MESSAGE = export const TEXT_UNIQ_IDENTIFIER = '$blocksuite:internal:text$'; export const NATIVE_UNIQ_IDENTIFIER = '$blocksuite:internal:native$'; -export const SYS_KEYS = new Set(['id', 'flavour', 'children']); +export const SYS_KEYS = new Set(['id', 'flavour', 'children', 'version']); diff --git a/blocksuite/framework/store/src/model/block/block.ts b/blocksuite/framework/store/src/model/block/block.ts index d540289d75..0b091404c0 100644 --- a/blocksuite/framework/store/src/model/block/block.ts +++ b/blocksuite/framework/store/src/model/block/block.ts @@ -44,6 +44,9 @@ export class Block { const onChange = !options.onChange ? undefined : (key: string, value: unknown) => { + if (!this._syncController || !this.model) { + return; + } options.onChange?.(this, key, value); }; const flavour = yBlock.get('sys:flavour') as string; diff --git a/blocksuite/framework/store/src/model/block/flat-sync-controller.ts b/blocksuite/framework/store/src/model/block/flat-sync-controller.ts index 30ed745937..72887d40ab 100644 --- a/blocksuite/framework/store/src/model/block/flat-sync-controller.ts +++ b/blocksuite/framework/store/src/model/block/flat-sync-controller.ts @@ -50,7 +50,7 @@ export class FlatSyncController { model.schema = schema; model.id = this.id; - model.keys = Array.from(props).map(key => key.replace('prop:', '')); + model.keys = Array.from(props); model.yBlock = this.yBlock; const reactive = new ReactiveFlatYMap( this.yBlock, @@ -69,8 +69,7 @@ export class FlatSyncController { const defaultProps = schema.model.props?.(internalPrimitives); if (defaultProps) { Object.entries(defaultProps).forEach(([key, value]) => { - const keyWithProp = `prop:${key}`; - if (keyWithProp in proxy) { + if (key in proxy) { return; } proxy[key] = value; @@ -149,9 +148,8 @@ export class FlatSyncController { // Set default props if not exists if (defaultProps) { Object.keys(defaultProps).forEach(key => { - const keyWithProp = `prop:${key}`; - if (props.has(keyWithProp)) return; - props.add(keyWithProp); + if (props.has(key)) return; + props.add(key); }); } diff --git a/blocksuite/framework/store/src/model/store/crud.ts b/blocksuite/framework/store/src/model/store/crud.ts index b4dc660937..e964759fde 100644 --- a/blocksuite/framework/store/src/model/store/crud.ts +++ b/blocksuite/framework/store/src/model/store/crud.ts @@ -1,7 +1,7 @@ import { BlockSuiteError, ErrorCode } from '@blocksuite/global/exceptions'; import * as Y from 'yjs'; -import { native2Y } from '../../reactive/index.js'; +import { isPureObject, native2Y } from '../../reactive/index.js'; import type { Schema } from '../../schema/index.js'; import type { BlockModel } from '../block/block-model.js'; import type { YBlock } from '../block/types.js'; @@ -95,11 +95,26 @@ export class DocCRUD { delete props.flavour; delete props.children; - Object.entries(props).forEach(([key, value]) => { - if (value === undefined) return; + const isFlatData = schema.model.isFlatData; + if (isFlatData) { + const run = (obj: unknown, basePath: string) => { + if (isPureObject(obj)) { + Object.entries(obj).forEach(([key, value]) => { + const fullPath = basePath ? `${basePath}.${key}` : key; + run(value, fullPath); + }); + } else { + yBlock.set(`prop:${basePath}`, native2Y(obj)); + } + }; + run(props, ''); + } else { + Object.entries(props).forEach(([key, value]) => { + if (value === undefined) return; - yBlock.set(`prop:${key}`, native2Y(value)); - }); + yBlock.set(`prop:${key}`, native2Y(value)); + }); + } const parentId = parent ?? (schema.model.role === 'root' ? null : this.root); diff --git a/blocksuite/framework/store/src/model/store/utils.ts b/blocksuite/framework/store/src/model/store/utils.ts index a2209da890..3b91094122 100644 --- a/blocksuite/framework/store/src/model/store/utils.ts +++ b/blocksuite/framework/store/src/model/store/utils.ts @@ -1,7 +1,6 @@ import type { z } from 'zod'; import { SYS_KEYS } from '../../consts.js'; -import { native2Y } from '../../reactive/index.js'; import type { BlockModel } from '../block/block-model.js'; import type { BlockProps, YBlock } from '../block/types.js'; import type { BlockSchema } from '../block/zod.js'; @@ -32,6 +31,6 @@ export function syncBlockProps( } // @ts-expect-error allow props - model[key] = native2Y(value); + model[key] = value; }); } diff --git a/blocksuite/framework/store/src/reactive/flat-native-y.ts b/blocksuite/framework/store/src/reactive/flat-native-y.ts index c3372b62b2..5e77cde8eb 100644 --- a/blocksuite/framework/store/src/reactive/flat-native-y.ts +++ b/blocksuite/framework/store/src/reactive/flat-native-y.ts @@ -33,6 +33,7 @@ type CreateProxyOptions = { shouldByPassSignal: () => boolean; byPassSignalUpdate: (fn: () => void) => void; stashed: Set; + initialized: () => boolean; }; const proxySymbol = Symbol('proxy'); @@ -60,6 +61,7 @@ function createProxy( byPassSignalUpdate, basePath, onChange, + initialized, transform = (_key, value) => value, stashed, } = options; @@ -73,7 +75,7 @@ function createProxy( if (isPureObject(value) && !isProxy(value)) { const proxy = createProxy(yMap, value as UnRecord, root, { ...options, - basePath: `${basePath}.${key}`, + basePath: basePath ? `${basePath}.${key}` : key, }); base[key] = proxy; } @@ -111,6 +113,9 @@ function createProxy( root[signalKey] = signalData; onDispose.once( signalData.subscribe(next => { + if (!initialized()) { + return; + } byPassSignalUpdate(() => { proxy[p] = next; onChange?.(firstKey, next); @@ -137,7 +142,7 @@ function createProxy( if (isPureObject(value)) { const syncYMap = () => { yMap.forEach((_, key) => { - if (keyWithoutPrefix(key).startsWith(fullPath)) { + if (initialized() && keyWithoutPrefix(key).startsWith(fullPath)) { yMap.delete(key); } }); @@ -148,13 +153,13 @@ function createProxy( run(value, fullPath); } else { list.push(() => { - yMap.set(keyWithPrefix(fullPath), value); + yMap.set(keyWithPrefix(fullPath), native2Y(value)); }); } }); }; run(value, fullPath); - if (list.length) { + if (list.length && initialized()) { yMap.doc?.transact( () => { list.forEach(fn => fn()); @@ -180,7 +185,7 @@ function createProxy( const yValue = native2Y(value); const next = transform(firstKey, value, yValue); - if (!isStashed) { + if (!isStashed && initialized()) { yMap.doc?.transact( () => { yMap.set(keyWithPrefix(fullPath), yValue); @@ -233,7 +238,7 @@ function createProxy( }); }; - if (!isStashed) { + if (!isStashed && initialized()) { yMap.doc?.transact( () => { const fullKey = keyWithPrefix(fullPath); @@ -268,6 +273,8 @@ export class ReactiveFlatYMap extends BaseReactiveYData< protected readonly _source: UnRecord; protected readonly _options?: ProxyOptions; + private readonly _initialized; + private readonly _observer = (event: YMapEvent) => { const yMap = this._ySource; const proxy = this._proxy; @@ -344,9 +351,12 @@ export class ReactiveFlatYMap extends BaseReactiveYData< }; private readonly _createDefaultData = (): UnRecord => { - const data: UnRecord = {}; + const root: UnRecord = {}; const transform = this._transform; Array.from(this._ySource.entries()).forEach(([key, value]) => { + if (key.startsWith('sys')) { + return; + } const keys = keyWithoutPrefix(key).split('.'); const firstKey = keys[0]; @@ -356,7 +366,8 @@ export class ReactiveFlatYMap extends BaseReactiveYData< } else if (value instanceof YArray) { finalData = transform(firstKey, value.toArray(), value); } else if (value instanceof YText) { - finalData = transform(firstKey, new Text(value), value); + const next = new Text(value); + finalData = transform(firstKey, next, value); } else if (value instanceof YMap) { throw new BlockSuiteError( ErrorCode.ReactiveProxyError, @@ -369,21 +380,25 @@ export class ReactiveFlatYMap extends BaseReactiveYData< void keys.reduce((acc: UnRecord, key, index) => { if (!acc[key] && index !== allLength - 1) { const path = keys.slice(0, index + 1).join('.'); - const data = this._getProxy({} as UnRecord, path); + const data = this._getProxy({} as UnRecord, root, path); acc[key] = data; } if (index === allLength - 1) { acc[key] = finalData; } return acc[key] as UnRecord; - }, data); + }, root); }); - return data; + return root; }; - private readonly _getProxy = (source: UnRecord, path?: string): UnRecord => { - return createProxy(this._ySource, source, source, { + private readonly _getProxy = ( + source: UnRecord, + root: UnRecord, + path?: string + ): UnRecord => { + return createProxy(this._ySource, source, root, { onDispose: this._onDispose, shouldByPassSignal: () => this._skipNext, byPassSignalUpdate: this._updateWithSkip, @@ -391,6 +406,7 @@ export class ReactiveFlatYMap extends BaseReactiveYData< onChange: this._onChange, transform: this._transform, stashed: this._stashed, + initialized: () => this._initialized, }); }; @@ -400,16 +416,20 @@ export class ReactiveFlatYMap extends BaseReactiveYData< private readonly _onChange?: OnChange ) { super(); + this._initialized = false; const source = this._createDefaultData(); this._source = source; - const proxy = this._getProxy(source); + const proxy = this._getProxy(source, source); Object.entries(source).forEach(([key, value]) => { const signalData = signal(value); source[`${key}$`] = signalData; _onDispose.once( signalData.subscribe(next => { + if (!this._initialized) { + return; + } this._updateWithSkip(() => { proxy[key] = next; this._onChange?.(key, next); @@ -420,6 +440,7 @@ export class ReactiveFlatYMap extends BaseReactiveYData< this._proxy = proxy; this._ySource.observe(this._observer); + this._initialized = true; } pop = (prop: string): void => {