mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-15 05:37:32 +00:00
refactor(core): move block collection to affine and implement as doc (#9514)
This commit is contained in:
@@ -7,6 +7,7 @@ import { applyUpdate, encodeStateAsUpdate } from 'yjs';
|
||||
import { COLLECTION_VERSION, PAGE_VERSION } from '../consts.js';
|
||||
import type { BlockModel, Blocks, BlockSchemaType } from '../index.js';
|
||||
import { Schema } from '../index.js';
|
||||
import { Text } from '../reactive/text.js';
|
||||
import type { DocMeta } from '../store/workspace.js';
|
||||
import { TestWorkspace } from '../test/test-workspace.js';
|
||||
import { createAutoIncrementIdGenerator } from '../utils/id-generator.js';
|
||||
@@ -166,7 +167,7 @@ describe('basic', () => {
|
||||
|
||||
doc.load(() => {
|
||||
const rootId = doc.addBlock('affine:page', {
|
||||
title: new doc.Text(),
|
||||
title: new Text(),
|
||||
});
|
||||
expect(rootAddedCallback).toBeCalledTimes(1);
|
||||
|
||||
@@ -186,7 +187,7 @@ describe('basic', () => {
|
||||
});
|
||||
doc.load(() => {
|
||||
doc.addBlock('affine:page', {
|
||||
title: new doc.Text(),
|
||||
title: new Text(),
|
||||
});
|
||||
});
|
||||
{
|
||||
@@ -245,7 +246,7 @@ describe('addBlock', () => {
|
||||
it('can add single model', () => {
|
||||
const doc = createTestDoc();
|
||||
doc.addBlock('affine:page', {
|
||||
title: new doc.Text(),
|
||||
title: new Text(),
|
||||
});
|
||||
|
||||
assert.deepEqual(serializCollection(doc.rootDoc).spaces[spaceId].blocks, {
|
||||
@@ -264,7 +265,7 @@ describe('addBlock', () => {
|
||||
|
||||
it('can add model with props', () => {
|
||||
const doc = createTestDoc();
|
||||
doc.addBlock('affine:page', { title: new doc.Text('hello') });
|
||||
doc.addBlock('affine:page', { title: new Text('hello') });
|
||||
|
||||
assert.deepEqual(serializCollection(doc.rootDoc).spaces[spaceId].blocks, {
|
||||
'0': {
|
||||
@@ -283,7 +284,7 @@ describe('addBlock', () => {
|
||||
it('can add multi models', () => {
|
||||
const doc = createTestDoc();
|
||||
const rootId = doc.addBlock('affine:page', {
|
||||
title: new doc.Text(),
|
||||
title: new Text(),
|
||||
});
|
||||
const noteId = doc.addBlock('affine:note', {}, rootId);
|
||||
doc.addBlock('affine:paragraph', {}, noteId);
|
||||
@@ -344,7 +345,7 @@ describe('addBlock', () => {
|
||||
|
||||
queueMicrotask(() =>
|
||||
doc.addBlock('affine:page', {
|
||||
title: new doc.Text(),
|
||||
title: new Text(),
|
||||
})
|
||||
);
|
||||
const blockId = await waitOnce(doc.slots.rootAdded);
|
||||
@@ -389,7 +390,7 @@ describe('addBlock', () => {
|
||||
assert.equal(collection.docs.size, 2);
|
||||
|
||||
doc0.addBlock('affine:page', {
|
||||
title: new doc0.Text(),
|
||||
title: new Text(),
|
||||
});
|
||||
collection.removeDoc(doc0.id);
|
||||
|
||||
|
||||
@@ -5,15 +5,15 @@ import { signal } from '@preact/signals-core';
|
||||
import type { BlockModel, Schema } from '../../schema/index.js';
|
||||
import type { DraftModel } from '../../transformer/index.js';
|
||||
import { syncBlockProps } from '../../utils/utils.js';
|
||||
import type { BlockProps, Doc } from '../workspace.js';
|
||||
import type { BlockOptions } from './block/index.js';
|
||||
import { Block } from './block/index.js';
|
||||
import type { BlockCollection, BlockProps } from './block-collection.js';
|
||||
import { DocCRUD } from './crud.js';
|
||||
import { type Query, runQuery } from './query.js';
|
||||
|
||||
type DocOptions = {
|
||||
schema: Schema;
|
||||
blockCollection: BlockCollection;
|
||||
blockCollection: Doc;
|
||||
readonly?: boolean;
|
||||
query?: Query;
|
||||
};
|
||||
@@ -23,7 +23,7 @@ export class Blocks {
|
||||
runQuery(this._query, block);
|
||||
};
|
||||
|
||||
protected readonly _blockCollection: BlockCollection;
|
||||
protected readonly _blockCollection: Doc;
|
||||
|
||||
protected readonly _blocks = signal<Record<string, Block>>({});
|
||||
|
||||
@@ -40,7 +40,7 @@ export class Blocks {
|
||||
|
||||
protected readonly _schema: Schema;
|
||||
|
||||
readonly slots: BlockCollection['slots'] & {
|
||||
readonly slots: Doc['slots'] & {
|
||||
/** This is always triggered after `doc.load` is called. */
|
||||
ready: Slot;
|
||||
/**
|
||||
@@ -179,10 +179,6 @@ export class Blocks {
|
||||
return this._blockCollection.collection;
|
||||
}
|
||||
|
||||
get generateBlockId() {
|
||||
return this._blockCollection.generateBlockId.bind(this._blockCollection);
|
||||
}
|
||||
|
||||
get history() {
|
||||
return this._blockCollection.history;
|
||||
}
|
||||
@@ -250,10 +246,6 @@ export class Blocks {
|
||||
return this._blockCollection.spaceDoc;
|
||||
}
|
||||
|
||||
get Text() {
|
||||
return this._blockCollection.Text;
|
||||
}
|
||||
|
||||
get transact() {
|
||||
return this._blockCollection.transact.bind(this._blockCollection);
|
||||
}
|
||||
@@ -432,7 +424,7 @@ export class Blocks {
|
||||
);
|
||||
}
|
||||
|
||||
const id = blockProps.id ?? this._blockCollection.generateBlockId();
|
||||
const id = blockProps.id ?? this._blockCollection.collection.idGenerator();
|
||||
|
||||
this.transact(() => {
|
||||
this._crud.addBlock(
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
export * from './block/index.js';
|
||||
export * from './block-collection.js';
|
||||
export * from './consts.js';
|
||||
export * from './doc.js';
|
||||
export * from './query.js';
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
export type * from './doc/block-collection.js';
|
||||
export * from './doc/index.js';
|
||||
export * from './meta.js';
|
||||
export * from './workspace.js';
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import type { Slot } from '@blocksuite/global/utils';
|
||||
import type { BlobEngine, DocEngine } from '@blocksuite/sync';
|
||||
import type * as Y from 'yjs';
|
||||
|
||||
import type { BlockModel } from '../schema/base.js';
|
||||
import type { Schema } from '../schema/schema.js';
|
||||
import type { IdGenerator } from '../utils/id-generator.js';
|
||||
import type { AwarenessStore } from '../yjs/awareness.js';
|
||||
import type { BlockSuiteDoc } from '../yjs/doc.js';
|
||||
import type { YBlock } from './doc/block/types.js';
|
||||
import type { Blocks } from './doc/doc.js';
|
||||
import type { BlockCollection } from './doc/index.js';
|
||||
import type { Query } from './doc/query.js';
|
||||
|
||||
export type Tag = {
|
||||
@@ -70,7 +72,7 @@ export interface Workspace {
|
||||
|
||||
get schema(): Schema;
|
||||
get doc(): BlockSuiteDoc;
|
||||
get docs(): Map<string, BlockCollection>;
|
||||
get docs(): Map<string, Doc>;
|
||||
|
||||
slots: {
|
||||
docListUpdated: Slot;
|
||||
@@ -85,6 +87,77 @@ export interface Workspace {
|
||||
dispose(): void;
|
||||
}
|
||||
|
||||
export interface Doc {
|
||||
readonly id: string;
|
||||
get meta(): DocMeta | undefined;
|
||||
get schema(): Schema;
|
||||
|
||||
remove(): void;
|
||||
load(initFn?: () => void): void;
|
||||
get ready(): boolean;
|
||||
dispose(): void;
|
||||
|
||||
slots: {
|
||||
historyUpdated: Slot;
|
||||
yBlockUpdated: Slot<
|
||||
| {
|
||||
type: 'add';
|
||||
id: string;
|
||||
}
|
||||
| {
|
||||
type: 'delete';
|
||||
id: string;
|
||||
}
|
||||
>;
|
||||
};
|
||||
|
||||
get canRedo(): boolean;
|
||||
get canUndo(): boolean;
|
||||
undo(): void;
|
||||
redo(): void;
|
||||
resetHistory(): void;
|
||||
transact(fn: () => void, shouldTransact?: boolean): void;
|
||||
withoutTransact(fn: () => void): void;
|
||||
|
||||
captureSync(): void;
|
||||
clear(): void;
|
||||
getDoc(options?: GetDocOptions): Blocks;
|
||||
clearQuery(query: Query, readonly?: boolean): void;
|
||||
|
||||
get history(): Y.UndoManager;
|
||||
get loaded(): boolean;
|
||||
get readonly(): boolean;
|
||||
get awarenessStore(): AwarenessStore;
|
||||
|
||||
get collection(): Workspace;
|
||||
|
||||
get rootDoc(): BlockSuiteDoc;
|
||||
get spaceDoc(): Y.Doc;
|
||||
get yBlocks(): Y.Map<YBlock>;
|
||||
}
|
||||
|
||||
export interface StackItem {
|
||||
meta: Map<'selection-state', unknown>;
|
||||
}
|
||||
|
||||
export type YBlocks = Y.Map<YBlock>;
|
||||
|
||||
/** JSON-serializable properties of a block */
|
||||
export type BlockSysProps = {
|
||||
id: string;
|
||||
flavour: string;
|
||||
children?: BlockModel[];
|
||||
};
|
||||
export type BlockProps = BlockSysProps & Record<string, unknown>;
|
||||
|
||||
declare global {
|
||||
namespace BlockSuite {
|
||||
interface BlockModels {}
|
||||
|
||||
type Flavour = string & keyof BlockModels;
|
||||
|
||||
type ModelProps<Model> = Partial<
|
||||
Model extends BlockModel<infer U> ? U : never
|
||||
>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
export { createAutoIncrementIdGenerator } from '../utils/id-generator.js';
|
||||
export * from './test-doc.js';
|
||||
export * from './test-workspace.js';
|
||||
|
||||
@@ -1,36 +1,21 @@
|
||||
import { type Disposable, Slot } from '@blocksuite/global/utils';
|
||||
import { signal } from '@preact/signals-core';
|
||||
import { uuidv4 } from 'lib0/random.js';
|
||||
import * as Y from 'yjs';
|
||||
|
||||
import { Text } from '../../reactive/text.js';
|
||||
import type { BlockModel } from '../../schema/base.js';
|
||||
import type { IdGenerator } from '../../utils/id-generator.js';
|
||||
import type { AwarenessStore, BlockSuiteDoc } from '../../yjs/index.js';
|
||||
import type { GetDocOptions, Workspace } from '../workspace.js';
|
||||
import { Blocks } from './doc.js';
|
||||
import type { YBlock } from './index.js';
|
||||
import type { Query } from './query.js';
|
||||
|
||||
export type YBlocks = Y.Map<YBlock>;
|
||||
|
||||
/** JSON-serializable properties of a block */
|
||||
export type BlockSysProps = {
|
||||
id: string;
|
||||
flavour: string;
|
||||
children?: BlockModel[];
|
||||
};
|
||||
export type BlockProps = BlockSysProps & Record<string, unknown>;
|
||||
import { Blocks } from '../store/doc/doc.js';
|
||||
import type { YBlock } from '../store/doc/index.js';
|
||||
import type { Query } from '../store/doc/query.js';
|
||||
import type { Doc, GetDocOptions, Workspace } from '../store/workspace.js';
|
||||
import type { AwarenessStore, BlockSuiteDoc } from '../yjs/index.js';
|
||||
|
||||
type DocOptions = {
|
||||
id: string;
|
||||
collection: Workspace;
|
||||
doc: BlockSuiteDoc;
|
||||
awarenessStore: AwarenessStore;
|
||||
idGenerator?: IdGenerator;
|
||||
};
|
||||
|
||||
export class BlockCollection {
|
||||
export class TestDoc implements Doc {
|
||||
private _awarenessUpdateDisposable: Disposable | null = null;
|
||||
|
||||
private readonly _canRedo$ = signal(false);
|
||||
@@ -57,8 +42,6 @@ export class BlockCollection {
|
||||
this.slots.historyUpdated.emit();
|
||||
};
|
||||
|
||||
private readonly _idGenerator: IdGenerator;
|
||||
|
||||
private readonly _initSubDoc = () => {
|
||||
let subDoc = this.rootDoc.spaces.get(this.id);
|
||||
if (!subDoc) {
|
||||
@@ -184,7 +167,7 @@ export class BlockCollection {
|
||||
return this.collection.meta.getDocMeta(this.id);
|
||||
}
|
||||
|
||||
get readonly() {
|
||||
get readonly(): boolean {
|
||||
return this.awarenessStore.isReadonly(this);
|
||||
}
|
||||
|
||||
@@ -200,21 +183,11 @@ export class BlockCollection {
|
||||
return this._ySpaceDoc;
|
||||
}
|
||||
|
||||
get Text() {
|
||||
return Text;
|
||||
}
|
||||
|
||||
get yBlocks() {
|
||||
return this._yBlocks;
|
||||
}
|
||||
|
||||
constructor({
|
||||
id,
|
||||
collection,
|
||||
doc,
|
||||
awarenessStore,
|
||||
idGenerator = uuidv4,
|
||||
}: DocOptions) {
|
||||
constructor({ id, collection, doc, awarenessStore }: DocOptions) {
|
||||
this.id = id;
|
||||
this.rootDoc = doc;
|
||||
this.awarenessStore = awarenessStore;
|
||||
@@ -223,7 +196,6 @@ export class BlockCollection {
|
||||
|
||||
this._yBlocks = this._ySpaceDoc.getMap('blocks');
|
||||
this._collection = collection;
|
||||
this._idGenerator = idGenerator;
|
||||
}
|
||||
|
||||
private _getReadonlyKey(readonly?: boolean): 'true' | 'false' | 'undefined' {
|
||||
@@ -295,7 +267,7 @@ export class BlockCollection {
|
||||
this._docMap[readonlyKey].delete(JSON.stringify(query));
|
||||
}
|
||||
|
||||
destroy() {
|
||||
private _destroy() {
|
||||
this._ySpaceDoc.destroy();
|
||||
this._onLoadSlot.dispose();
|
||||
this._loaded = false;
|
||||
@@ -311,10 +283,6 @@ export class BlockCollection {
|
||||
}
|
||||
}
|
||||
|
||||
generateBlockId() {
|
||||
return this._idGenerator();
|
||||
}
|
||||
|
||||
getDoc({ readonly, query }: GetDocOptions = {}) {
|
||||
const readonlyKey = this._getReadonlyKey(readonly);
|
||||
|
||||
@@ -375,8 +343,16 @@ export class BlockCollection {
|
||||
this._history.redo();
|
||||
}
|
||||
|
||||
undo() {
|
||||
if (this.readonly) {
|
||||
console.error('cannot modify data in readonly mode');
|
||||
return;
|
||||
}
|
||||
this._history.undo();
|
||||
}
|
||||
|
||||
remove() {
|
||||
this.destroy();
|
||||
this._destroy();
|
||||
this.rootDoc.spaces.delete(this.id);
|
||||
}
|
||||
|
||||
@@ -403,30 +379,9 @@ export class BlockCollection {
|
||||
);
|
||||
}
|
||||
|
||||
// Handle all the events that happen at _any_ level (potentially deep inside the structure).
|
||||
undo() {
|
||||
if (this.readonly) {
|
||||
console.error('cannot modify data in readonly mode');
|
||||
return;
|
||||
}
|
||||
this._history.undo();
|
||||
}
|
||||
|
||||
withoutTransact(callback: () => void) {
|
||||
this._shouldTransact = false;
|
||||
callback();
|
||||
this._shouldTransact = true;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
namespace BlockSuite {
|
||||
interface BlockModels {}
|
||||
|
||||
type Flavour = string & keyof BlockModels;
|
||||
|
||||
type ModelProps<Model> = Partial<
|
||||
Model extends BlockModel<infer U> ? U : never
|
||||
>;
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,6 @@ import { Awareness } from 'y-protocols/awareness.js';
|
||||
|
||||
import type { Schema } from '../schema/index.js';
|
||||
import {
|
||||
BlockCollection,
|
||||
type Blocks,
|
||||
type CreateDocOptions,
|
||||
DocCollectionMeta,
|
||||
@@ -30,6 +29,7 @@ import {
|
||||
BlockSuiteDoc,
|
||||
type RawAwarenessState,
|
||||
} from '../yjs/index.js';
|
||||
import { TestDoc } from './test-doc.js';
|
||||
|
||||
export type DocCollectionOptions = {
|
||||
schema: Schema;
|
||||
@@ -80,7 +80,7 @@ export class TestWorkspace implements Workspace {
|
||||
|
||||
readonly blobSync: BlobEngine;
|
||||
|
||||
readonly blockCollections = new Map<string, BlockCollection>();
|
||||
readonly blockCollections = new Map<string, TestDoc>();
|
||||
|
||||
readonly doc: BlockSuiteDoc;
|
||||
|
||||
@@ -154,12 +154,11 @@ export class TestWorkspace implements Workspace {
|
||||
|
||||
private _bindDocMetaEvents() {
|
||||
this.meta.docMetaAdded.on(docId => {
|
||||
const doc = new BlockCollection({
|
||||
const doc = new TestDoc({
|
||||
id: docId,
|
||||
collection: this,
|
||||
doc: this.doc,
|
||||
awarenessStore: this.awarenessStore,
|
||||
idGenerator: this.idGenerator,
|
||||
});
|
||||
this.blockCollections.set(doc.id, doc);
|
||||
});
|
||||
@@ -225,8 +224,8 @@ export class TestWorkspace implements Workspace {
|
||||
this.awarenessSync.disconnect();
|
||||
}
|
||||
|
||||
getBlockCollection(docId: string): BlockCollection | null {
|
||||
const space = this.docs.get(docId) as BlockCollection | undefined;
|
||||
getBlockCollection(docId: string): TestDoc | null {
|
||||
const space = this.docs.get(docId) as TestDoc | undefined;
|
||||
return space ?? null;
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import { native2Y } from '../reactive/index.js';
|
||||
import type { BlockModel, BlockSchema } from '../schema/base.js';
|
||||
import { internalPrimitives } from '../schema/base.js';
|
||||
import type { YBlock } from '../store/doc/block/index.js';
|
||||
import type { BlockProps } from '../store/doc/block-collection.js';
|
||||
import type { BlockProps } from '../store/workspace.js';
|
||||
|
||||
export function syncBlockProps(
|
||||
schema: z.infer<typeof BlockSchema>,
|
||||
|
||||
@@ -5,7 +5,7 @@ import clonedeep from 'lodash.clonedeep';
|
||||
import merge from 'lodash.merge';
|
||||
import type { Awareness as YAwareness } from 'y-protocols/awareness.js';
|
||||
|
||||
import type { BlockCollection } from '../store/index.js';
|
||||
import type { Doc } from '../store/index.js';
|
||||
|
||||
export interface UserInfo {
|
||||
name: string;
|
||||
@@ -115,7 +115,7 @@ export class AwarenessStore<Flags extends BlockSuiteFlags = BlockSuiteFlags> {
|
||||
return this.awareness.getStates();
|
||||
}
|
||||
|
||||
isReadonly(blockCollection: BlockCollection): boolean {
|
||||
isReadonly(blockCollection: Doc): boolean {
|
||||
const rd = this.getFlag('readonly');
|
||||
if (rd && typeof rd === 'object') {
|
||||
return Boolean((rd as Record<string, boolean>)[blockCollection.id]);
|
||||
@@ -137,7 +137,7 @@ export class AwarenessStore<Flags extends BlockSuiteFlags = BlockSuiteFlags> {
|
||||
});
|
||||
}
|
||||
|
||||
setReadonly(blockCollection: BlockCollection, value: boolean): void {
|
||||
setReadonly(blockCollection: Doc, value: boolean): void {
|
||||
const flags = this.getFlag('readonly') ?? {};
|
||||
this.setFlag('readonly', {
|
||||
...flags,
|
||||
|
||||
Reference in New Issue
Block a user