feat(editor): support delete in flat block proxy (#9994)

This commit is contained in:
Saul-Mirone
2025-02-06 16:30:06 +00:00
parent 86de184593
commit 5b750bbba4
2 changed files with 103 additions and 4 deletions

View File

@@ -452,6 +452,33 @@ describe('flat', () => {
expect.anything() expect.anything()
); );
onChange.mockClear();
onColUpdated.mockClear();
delete (model.props.cols.a as Record<string, unknown>).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<string, unknown>).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(); onChange.mockClear();
model.props.title.insert('test', 0); model.props.title.insert('test', 0);
expect((yBlock.get('prop:title') as Y.Text).toJSON()).toBe('test'); expect((yBlock.get('prop:title') as Y.Text).toJSON()).toBe('test');
@@ -483,6 +510,16 @@ describe('flat', () => {
'labels', 'labels',
expect.anything() 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', () => { test('stash and pop', () => {

View File

@@ -8,6 +8,7 @@ import {
type YMapEvent, type YMapEvent,
} from 'yjs'; } from 'yjs';
import { SYS_KEYS } from '../consts';
import { BaseReactiveYData } from './base-reactive-data'; import { BaseReactiveYData } from './base-reactive-data';
import { Boxed, type OnBoxedChange } from './boxed'; import { Boxed, type OnBoxedChange } from './boxed';
import { isPureObject } from './is-pure-object'; 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 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 OnChange = (key: string, value: unknown) => void;
type Transform = (key: string, value: unknown, origin: unknown) => unknown; type Transform = (key: string, value: unknown, origin: unknown) => unknown;
@@ -144,7 +148,7 @@ function createProxy(
run(value, fullPath); run(value, fullPath);
} else { } else {
list.push(() => { list.push(() => {
yMap.set(`prop:${fullPath}`, value); yMap.set(keyWithPrefix(fullPath), value);
}); });
} }
}); });
@@ -179,7 +183,7 @@ function createProxy(
if (!isStashed) { if (!isStashed) {
yMap.doc?.transact( yMap.doc?.transact(
() => { () => {
yMap.set(`prop:${fullPath}`, yValue); yMap.set(keyWithPrefix(fullPath), yValue);
}, },
{ proxy: true } { proxy: true }
); );
@@ -191,6 +195,64 @@ function createProxy(
} }
return Reflect.set(target, p, value, receiver); 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); markProxy(proxy);
@@ -217,7 +279,7 @@ export class ReactiveFlatYMap extends BaseReactiveYData<
} }
if (type.action === 'update' || type.action === 'add') { if (type.action === 'update' || type.action === 'add') {
const value = yMap.get(key); const value = yMap.get(key);
const keyName: string = key.replace('prop:', ''); const keyName: string = keyWithoutPrefix(key);
const keys = keyName.split('.'); const keys = keyName.split('.');
const firstKey = keys[0]; const firstKey = keys[0];
if (this._stashed.has(firstKey)) { if (this._stashed.has(firstKey)) {
@@ -232,7 +294,7 @@ export class ReactiveFlatYMap extends BaseReactiveYData<
return; return;
} }
if (type.action === 'delete') { if (type.action === 'delete') {
const keyName: string = key.replace('prop:', ''); const keyName: string = keyWithoutPrefix(key);
const keys = keyName.split('.'); const keys = keyName.split('.');
const firstKey = keys[0]; const firstKey = keys[0];
if (this._stashed.has(firstKey)) { if (this._stashed.has(firstKey)) {