diff --git a/blocksuite/framework/store/src/__tests__/block.unit.spec.ts b/blocksuite/framework/store/src/__tests__/block.unit.spec.ts index db8c862061..761c27c507 100644 --- a/blocksuite/framework/store/src/__tests__/block.unit.spec.ts +++ b/blocksuite/framework/store/src/__tests__/block.unit.spec.ts @@ -452,6 +452,33 @@ describe('flat', () => { expect.anything() ); + onChange.mockClear(); + onColUpdated.mockClear(); + delete (model.props.cols.a as Record).color; + expect(yBlock.get('prop:cols.a.color')).toBe(undefined); + expect(model.props.cols.a).toEqual({}); + expect(model.props.cols$.value).toEqual({ a: {} }); + expect(onColUpdated).toHaveBeenCalledTimes(1); + expect(onChange).toHaveBeenCalledTimes(1); + expect(onChange).toHaveBeenCalledWith( + expect.anything(), + 'cols', + expect.anything() + ); + + model.props.cols = { + a: { color: 'red' }, + b: { color: 'blue' }, + }; + onChange.mockClear(); + onColUpdated.mockClear(); + delete (model.props as Record).cols; + expect(model.props.cols$.value).toBeUndefined(); + expect(yBlock.get('prop:cols.a.color')).toBe(undefined); + expect(yBlock.get('prop:cols.b.color')).toBe(undefined); + expect(onColUpdated).toHaveBeenCalledTimes(1); + expect(onChange).toHaveBeenCalledTimes(1); + onChange.mockClear(); model.props.title.insert('test', 0); expect((yBlock.get('prop:title') as Y.Text).toJSON()).toBe('test'); @@ -483,6 +510,16 @@ describe('flat', () => { 'labels', expect.anything() ); + + onChange.mockClear(); + model.props.labels.splice(0, 1); + expect(getLabels().toJSON()).toEqual([]); + expect(model.props.labels$.value).toEqual([]); + expect(onChange).toHaveBeenCalledWith( + expect.anything(), + 'labels', + expect.anything() + ); }); test('stash and pop', () => { diff --git a/blocksuite/framework/store/src/reactive/flat-native-y.ts b/blocksuite/framework/store/src/reactive/flat-native-y.ts index 2d29c03766..c3372b62b2 100644 --- a/blocksuite/framework/store/src/reactive/flat-native-y.ts +++ b/blocksuite/framework/store/src/reactive/flat-native-y.ts @@ -8,6 +8,7 @@ import { type YMapEvent, } from 'yjs'; +import { SYS_KEYS } from '../consts'; import { BaseReactiveYData } from './base-reactive-data'; import { Boxed, type OnBoxedChange } from './boxed'; import { isPureObject } from './is-pure-object'; @@ -18,6 +19,9 @@ import type { ProxyOptions, UnRecord } from './types'; const keyWithoutPrefix = (key: string) => key.replace(/(prop|sys):/, ''); +const keyWithPrefix = (key: string) => + SYS_KEYS.has(key) ? `sys:${key}` : `prop:${key}`; + type OnChange = (key: string, value: unknown) => void; type Transform = (key: string, value: unknown, origin: unknown) => unknown; @@ -144,7 +148,7 @@ function createProxy( run(value, fullPath); } else { list.push(() => { - yMap.set(`prop:${fullPath}`, value); + yMap.set(keyWithPrefix(fullPath), value); }); } }); @@ -179,7 +183,7 @@ function createProxy( if (!isStashed) { yMap.doc?.transact( () => { - yMap.set(`prop:${fullPath}`, yValue); + yMap.set(keyWithPrefix(fullPath), yValue); }, { proxy: true } ); @@ -191,6 +195,64 @@ function createProxy( } return Reflect.set(target, p, value, receiver); }, + deleteProperty: (target, p) => { + if (typeof p === 'string') { + const fullPath = basePath ? `${basePath}.${p}` : p; + const firstKey = fullPath.split('.')[0]; + if (!firstKey) { + throw new Error(`Invalid key for: ${fullPath}`); + } + + const isStashed = stashed.has(firstKey); + + const updateSignal = () => { + if (shouldByPassSignal()) { + return; + } + + const signalKey = `${firstKey}$`; + if (!(signalKey in root)) { + if (!isRoot) { + return; + } + delete root[signalKey]; + return; + } + byPassSignalUpdate(() => { + const prev = root[firstKey]; + const next = isRoot + ? prev + : isPureObject(prev) + ? { ...prev } + : Array.isArray(prev) + ? [...prev] + : prev; + // @ts-expect-error allow magic props + root[signalKey].value = next; + onChange?.(firstKey, next); + }); + }; + + if (!isStashed) { + yMap.doc?.transact( + () => { + const fullKey = keyWithPrefix(fullPath); + yMap.forEach((_, key) => { + if (key.startsWith(fullKey)) { + yMap.delete(key); + } + }); + }, + { proxy: true } + ); + } + + const result = Reflect.deleteProperty(target, p); + updateSignal(); + return result; + } + return Reflect.deleteProperty(target, p); + }, }); markProxy(proxy); @@ -217,7 +279,7 @@ export class ReactiveFlatYMap extends BaseReactiveYData< } if (type.action === 'update' || type.action === 'add') { const value = yMap.get(key); - const keyName: string = key.replace('prop:', ''); + const keyName: string = keyWithoutPrefix(key); const keys = keyName.split('.'); const firstKey = keys[0]; if (this._stashed.has(firstKey)) { @@ -232,7 +294,7 @@ export class ReactiveFlatYMap extends BaseReactiveYData< return; } if (type.action === 'delete') { - const keyName: string = key.replace('prop:', ''); + const keyName: string = keyWithoutPrefix(key); const keys = keyName.split('.'); const firstKey = keys[0]; if (this._stashed.has(firstKey)) {