feat(editor): unify block props api (#10888)

Closes: [BS-2707](https://linear.app/affine-design/issue/BS-2707/统一使用props获取和更新block-prop)
This commit is contained in:
Saul-Mirone
2025-03-16 05:48:34 +00:00
parent 8f9e5bf0aa
commit 26285f7dcb
193 changed files with 1019 additions and 891 deletions

View File

@@ -11,28 +11,10 @@ import type { BlockSchemaType } from './zod.js';
type SignaledProps<Props> = Props & {
[P in keyof Props & string as `${P}$`]: Signal<Props[P]>;
};
/**
* The MagicProps function is used to append the props to the class.
* For example:
*
* ```ts
* class MyBlock extends MagicProps()<{ foo: string }> {}
* const myBlock = new MyBlock();
* // You'll get type checking for the foo prop
* myBlock.foo = 'bar';
* ```
*/
function MagicProps(): { new <Props>(): Props } {
return class {} as never;
}
const modelLabel = Symbol('model_label');
// @ts-expect-error allow magic props
export class BlockModel<
Props extends object = object,
PropsSignal extends object = SignaledProps<Props>,
> extends MagicProps()<PropsSignal> {
export class BlockModel<Props extends object = object> {
private readonly _children = signal<string[]>([]);
private _store!: Store;
@@ -82,8 +64,15 @@ export class BlockModel<
stash!: (prop: keyof Props & string) => void;
// text is optional
text?: Text;
get text(): Text | undefined {
return (this.props as { text?: Text }).text;
}
set text(text: Text) {
if (this.keys.includes('text')) {
(this.props as { text?: Text }).text = text;
}
}
yBlock!: YBlock;
@@ -125,7 +114,6 @@ export class BlockModel<
}
constructor() {
super();
this._onCreated = {
dispose: this.created.pipe(take(1)).subscribe(() => {
this._children.value = this.yBlock.get('sys:children').toArray();

View File

@@ -11,18 +11,17 @@ export type DraftModel<Model extends BlockModel = BlockModel> = Pick<
PropsInDraft
> & {
children: DraftModel[];
} & ModelProps<Model> & {
[draftModelSymbol]: true;
};
props: ModelProps<Model>;
[draftModelSymbol]: true;
};
export function toDraftModel<Model extends BlockModel = BlockModel>(
origin: Model
): DraftModel<Model> {
const { id, version, flavour, role, keys, text, children } = origin;
const isFlatData = origin.schema.model.isFlatData;
const props = origin.keys.reduce((acc, key) => {
const target = isFlatData ? origin.props : origin;
const target = origin.props;
const value = target[key as keyof typeof target];
return {
...acc,
@@ -38,6 +37,6 @@ export function toDraftModel<Model extends BlockModel = BlockModel>(
keys,
text,
children: children.map(toDraftModel),
...props,
props,
} as DraftModel<Model>;
}

View File

@@ -55,12 +55,12 @@ export class SyncController {
const proxy = this._getPropsProxy(keyName, value);
this._byPassUpdate(() => {
// @ts-expect-error allow magic props
this.model[keyName] = proxy;
this.model.props[keyName] = proxy;
const signalKey = `${keyName}$`;
this._mutex(() => {
if (signalKey in this.model) {
if (signalKey in this.model.props) {
// @ts-expect-error allow magic props
this.model[signalKey].value = y2Native(value);
this.model.props[signalKey].value = y2Native(value);
}
});
});
@@ -71,10 +71,10 @@ export class SyncController {
const keyName = key.replace('prop:', '');
this._byPassUpdate(() => {
// @ts-expect-error allow magic props
delete this.model[keyName];
if (`${keyName}$` in this.model) {
delete this.model.props[keyName];
if (`${keyName}$` in this.model.props) {
// @ts-expect-error allow magic props
this.model[`${keyName}$`].value = undefined;
this.model.props[`${keyName}$`].value = undefined;
}
});
this.onChange?.(keyName, isLocal);
@@ -146,7 +146,7 @@ export class SyncController {
if (!this.model) return;
_mutex(() => {
// @ts-expect-error allow magic props
this.model[key] = value;
this.model.props[key] = value;
});
});
const subscription = model.deleted.subscribe(() => {
@@ -161,7 +161,6 @@ export class SyncController {
},
{} as Record<string, unknown>
);
Object.assign(model, signalWithProps);
model.id = this.id;
model.keys = Object.keys(props);
@@ -172,7 +171,7 @@ export class SyncController {
model.doc = this.doc;
}
const proxy = new Proxy(model, {
const proxy = new Proxy(signalWithProps, {
has: (target, p) => {
return Reflect.has(target, p);
},
@@ -217,14 +216,15 @@ export class SyncController {
return Reflect.deleteProperty(target, p);
},
});
model._props = proxy;
function setValue(target: BlockModel, p: string, value: unknown) {
function setValue(target: UnRecord, p: string, value: unknown) {
_mutex(() => {
// @ts-expect-error allow magic props
target[`${p}$`].value = value;
});
}
return proxy;
return model;
}
private _getPropsProxy(name: string, value: unknown) {
@@ -232,10 +232,10 @@ export class SyncController {
onChange: (_, isLocal) => {
this.onChange?.(name, isLocal);
const signalKey = `${name}$`;
if (signalKey in this.model) {
if (signalKey in this.model.props) {
this._mutex(() => {
// @ts-expect-error allow magic props
this.model[signalKey].value = y2Native(value);
this.model.props[signalKey].value = y2Native(value);
});
}
},
@@ -331,13 +331,13 @@ export class SyncController {
private _popProp(prop: string) {
const model = this.model as BlockModel<Record<string, unknown>>;
const value = model[prop];
const value = model.props[prop];
this._stashed.delete(prop);
model[prop] = value;
model.props[prop] = value;
}
private _stashProp(prop: string) {
(this.model as BlockModel<Record<string, unknown>>)[prop] = y2Native(
(this.model as BlockModel<Record<string, unknown>>).props[prop] = y2Native(
this.yBlock.get(`prop:${prop}`),
{
transform: (value, origin) => {

View File

@@ -39,7 +39,7 @@ function getBlockViewType(query: Query, block: Block): BlockViewType {
(acc, key) => {
return {
...acc,
[key]: block.model[key as keyof BlockModel],
[key]: block.model.props[key as keyof BlockModel['props']],
};
},
{} as Record<string, unknown>

View File

@@ -19,7 +19,7 @@ export function syncBlockProps(
if (value === undefined) return;
// @ts-expect-error allow props
model[key] = value;
model.props[key] = value;
});
// set default value