feat(editor): flat block data (#9854)

Flat block data.

A new block type to flatten the block data

```typescript
// For developers
type Model = {
  blocks: Record<string, {
    flavour: string;
    cells: Record<string, {
      rowId: string;
      colId: string;
      text: Text;
    }>;
    cols: Record<string, {
      align: string;
    }>
    rows: Record<string, {
      backgroundColor: string;
    }>
  }>
}

// How it's saved in yjs
const yData = {
  blocks: {
    'blockId1': {
      flavour: 'affine:table',
      'prop:rows:row1:backgroundColor': 'white',
      'prop:cols:col1:align': 'left',
      'prop:cells:cell1:rowId': 'row1',
      'prop:cells:cell1:colId': 'col1',
      'prop:cells:cell1:text': YText,
      prop:children: []
    },
  }
}
```
This commit is contained in:
Saul-Mirone
2025-01-25 12:57:21 +00:00
parent 9c5375ca06
commit 1858947e0c
17 changed files with 1189 additions and 389 deletions
@@ -87,6 +87,15 @@ export class BlockModel<
yBlock!: YBlock;
_props!: SignaledProps<Props>;
get props() {
if (!this._props) {
throw new Error('props is only supported in flat data model');
}
return this._props;
}
get flavour(): string {
return this.schema.model.flavour;
}
@@ -1,12 +1,13 @@
import type { Schema } from '../../schema/index.js';
import type { Store } from '../store/store.js';
import { FlatSyncController } from './flat-sync-controller.js';
import { SyncController } from './sync-controller.js';
import type { BlockOptions, YBlock } from './types.js';
export type BlockViewType = 'bypass' | 'display' | 'hidden';
export class Block {
private readonly _syncController: SyncController;
private readonly _syncController: SyncController | FlatSyncController;
blockViewType: BlockViewType = 'display';
@@ -45,6 +46,17 @@ export class Block {
: (key: string, value: unknown) => {
options.onChange?.(this, key, value);
};
this._syncController = new SyncController(schema, yBlock, doc, onChange);
const flavour = yBlock.get('sys:flavour') as string;
const blockSchema = this.schema.get(flavour);
if (blockSchema?.model.isFlatData) {
this._syncController = new FlatSyncController(
schema,
yBlock,
doc,
onChange
);
} else {
this._syncController = new SyncController(schema, yBlock, doc, onChange);
}
}
}
@@ -0,0 +1,174 @@
import { BlockSuiteError, ErrorCode } from '@blocksuite/global/exceptions';
import * as Y from 'yjs';
import { ReactiveFlatYMap } from '../../reactive/flat-native-y.js';
import type { Schema } from '../../schema/schema.js';
import type { Store } from '../store/store.js';
import { BlockModel } from './block-model.js';
import type { YBlock } from './types.js';
import { internalPrimitives } from './zod.js';
export class FlatSyncController {
private _reactive!: ReactiveFlatYMap;
readonly flavour: string;
readonly id: string;
readonly model: BlockModel;
readonly version: number;
readonly yChildren: Y.Array<string[]>;
constructor(
readonly schema: Schema,
readonly yBlock: YBlock,
readonly doc?: Store,
readonly onChange?: (key: string, value: unknown) => void
) {
const { id, flavour, version, yChildren, props } = this._parseYBlock();
this.id = id;
this.flavour = flavour;
this.yChildren = yChildren;
this.version = version;
this.model = this._createModel(props);
}
private _createModel(props: Set<string>) {
const schema = this.schema.flavourSchemaMap.get(this.flavour);
if (!schema) {
throw new BlockSuiteError(
ErrorCode.ModelCRUDError,
`schema for flavour: ${this.flavour} not found`
);
}
const model = schema.model.toModel?.() ?? new BlockModel<object>();
model.schema = schema;
model.id = this.id;
model.keys = Array.from(props).map(key => key.replace('prop:', ''));
model.yBlock = this.yBlock;
const reactive = new ReactiveFlatYMap(
this.yBlock,
model.deleted,
this.onChange
);
this._reactive = reactive;
const proxy = reactive.proxy;
model._props = proxy;
model.stash = this.stash;
model.pop = this.pop;
if (this.doc) {
model.doc = this.doc;
}
const defaultProps = schema.model.props?.(internalPrimitives);
if (defaultProps) {
Object.entries(defaultProps).forEach(([key, value]) => {
const keyWithProp = `prop:${key}`;
if (keyWithProp in proxy) {
return;
}
proxy[key] = value;
});
}
return model;
}
private _parseYBlock() {
let id: string | undefined;
let flavour: string | undefined;
let version: number | undefined;
let yChildren: Y.Array<string[]> | undefined;
const props: Set<string> = new Set();
this.yBlock.forEach((value, key) => {
if (key === 'sys:id' && typeof value === 'string') {
id = value;
return;
}
if (key === 'sys:flavour' && typeof value === 'string') {
flavour = value;
return;
}
if (key === 'sys:children' && value instanceof Y.Array) {
yChildren = value;
return;
}
if (key === 'sys:version' && typeof value === 'number') {
version = value;
return;
}
if (key.startsWith('prop:')) {
const keyName = key.replace('prop:', '');
const keys = keyName.split('.');
const propKey = keys[0];
props.add(propKey);
return;
}
});
if (!id) {
throw new BlockSuiteError(
ErrorCode.ModelCRUDError,
'block id is not found when creating model'
);
}
if (!flavour) {
throw new BlockSuiteError(
ErrorCode.ModelCRUDError,
'block flavour is not found when creating model'
);
}
if (!yChildren) {
throw new BlockSuiteError(
ErrorCode.ModelCRUDError,
'block children is not found when creating model'
);
}
const schema = this.schema.flavourSchemaMap.get(flavour);
if (!schema) {
throw new BlockSuiteError(
ErrorCode.ModelCRUDError,
`schema for flavour: ${flavour} not found`
);
}
if (typeof version !== 'number') {
// no version found in data, set to schema version
version = schema.version;
}
const defaultProps = schema.model.props?.(internalPrimitives);
// 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);
});
}
return {
id,
flavour,
version,
props,
yChildren,
};
}
get stash() {
return this._reactive.stash;
}
get pop() {
return this._reactive.pop;
}
}
@@ -30,6 +30,7 @@ export const BlockSchema = z.object({
flavour: FlavourSchema,
parent: ParentSchema,
children: ContentSchema,
isFlatData: z.boolean().optional(),
props: z
.function()
.args(z.custom<InternalPrimitives>())
@@ -71,6 +72,7 @@ export function defineBlockSchema<
role: Role;
parent?: string[];
children?: string[];
isFlatData?: boolean;
}>,
Model extends BlockModel<Props>,
Transformer extends BaseBlockTransformer<Props>,
@@ -102,6 +104,7 @@ export function defineBlockSchema({
role: RoleType;
parent?: string[];
children?: string[];
isFlatData?: boolean;
};
props?: (internalPrimitives: InternalPrimitives) => Record<string, unknown>;
toModel?: () => BlockModel;
@@ -116,6 +119,7 @@ export function defineBlockSchema({
flavour,
props,
toModel,
isFlatData: metadata.isFlatData,
},
transformer,
} satisfies z.infer<typeof BlockSchema>;