mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-08-01 17:39:55 +08:00
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:
@@ -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>;
|
||||
|
||||
Reference in New Issue
Block a user