mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-21 12:06:35 +08:00
refactor(core): move block collection to affine and implement as doc (#9514)
This commit is contained in:
@@ -167,7 +167,7 @@ export class BlockQueryDataSource extends DataSourceBase {
|
||||
type ?? propertyPresets.multiSelectPropertyConfig.type
|
||||
].create(this.newColumnName());
|
||||
|
||||
const id = doc.generateBlockId();
|
||||
const id = doc.collection.idGenerator();
|
||||
if (this.block.columns.some(v => v.id === id)) {
|
||||
return id;
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ export class DataViewBlockModel extends BlockModel<Props> {
|
||||
}
|
||||
|
||||
duplicateView(id: string): string {
|
||||
const newId = this.doc.generateBlockId();
|
||||
const newId = this.doc.collection.idGenerator();
|
||||
this.doc.transact(() => {
|
||||
const index = this.views.findIndex(v => v.id === id);
|
||||
const view = this.views[index];
|
||||
|
||||
@@ -448,7 +448,7 @@ export const databaseViewInitTemplate = (
|
||||
const rowId = model.doc.addBlock(
|
||||
'affine:paragraph',
|
||||
{
|
||||
text: new model.doc.Text(`Task ${i + 1}`),
|
||||
text: new Text(`Task ${i + 1}`),
|
||||
},
|
||||
model.id
|
||||
);
|
||||
|
||||
@@ -19,7 +19,7 @@ export function addProperty(
|
||||
id?: string;
|
||||
}
|
||||
): string {
|
||||
const id = column.id ?? model.doc.generateBlockId();
|
||||
const id = column.id ?? model.doc.collection.idGenerator();
|
||||
if (model.columns.some(v => v.id === id)) {
|
||||
return id;
|
||||
}
|
||||
@@ -101,7 +101,7 @@ export function deleteView(model: DatabaseBlockModel, id: string) {
|
||||
}
|
||||
|
||||
export function duplicateView(model: DatabaseBlockModel, id: string): string {
|
||||
const newId = model.doc.generateBlockId();
|
||||
const newId = model.doc.collection.idGenerator();
|
||||
model.doc.transact(() => {
|
||||
const index = model.views.findIndex(v => v.id === id);
|
||||
const view = model.views[index];
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
type DraftModel,
|
||||
type Query,
|
||||
Slice,
|
||||
Text,
|
||||
} from '@blocksuite/store';
|
||||
import { render, type TemplateResult } from 'lit';
|
||||
|
||||
@@ -407,7 +408,7 @@ export function createLinkedDocFromSlice(
|
||||
const linkedDoc = doc.collection.createDoc({});
|
||||
linkedDoc.load(() => {
|
||||
const rootId = linkedDoc.addBlock('affine:page', {
|
||||
title: new doc.Text(docTitle),
|
||||
title: new Text(docTitle),
|
||||
});
|
||||
linkedDoc.addBlock('affine:surface', {}, rootId);
|
||||
const noteId = linkedDoc.addBlock('affine:note', {}, rootId);
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
referenceToNode,
|
||||
} from '@blocksuite/affine-shared/utils';
|
||||
import { Bound, throttle } from '@blocksuite/global/utils';
|
||||
import { Text } from '@blocksuite/store';
|
||||
import { computed } from '@preact/signals-core';
|
||||
import { html, nothing } from 'lit';
|
||||
import { property, queryAsync, state } from 'lit/decorators.js';
|
||||
@@ -171,7 +172,7 @@ export class EmbedLinkedDocBlockComponent extends EmbedBlockComponent<EmbedLinke
|
||||
...this.referenceInfo$.peek(),
|
||||
},
|
||||
});
|
||||
const text = new doc.Text(yText);
|
||||
const text = new Text(yText);
|
||||
|
||||
doc.addBlock(
|
||||
'affine:paragraph',
|
||||
|
||||
@@ -29,6 +29,7 @@ import {
|
||||
BlockViewType,
|
||||
type GetDocOptions,
|
||||
type Query,
|
||||
Text,
|
||||
} from '@blocksuite/store';
|
||||
import { computed } from '@preact/signals-core';
|
||||
import { html, type PropertyValues } from 'lit';
|
||||
@@ -280,7 +281,7 @@ export class EmbedSyncedDocBlockComponent extends EmbedBlockComponent<EmbedSynce
|
||||
...this.referenceInfo,
|
||||
},
|
||||
});
|
||||
const text = new doc.Text(yText);
|
||||
const text = new Text(yText);
|
||||
|
||||
doc.addBlock(
|
||||
'affine:paragraph',
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { focusTextModel } from '@blocksuite/affine-components/rich-text';
|
||||
import { getLastNoteBlock } from '@blocksuite/affine-shared/utils';
|
||||
import type { Command } from '@blocksuite/block-std';
|
||||
import { Text } from '@blocksuite/store';
|
||||
|
||||
/**
|
||||
* Append a paragraph block at the end of the whole page.
|
||||
@@ -19,11 +20,7 @@ export const appendParagraphCommand: Command<
|
||||
if (!noteId) {
|
||||
noteId = doc.addBlock('affine:note', {}, doc.root.id);
|
||||
}
|
||||
const id = doc.addBlock(
|
||||
'affine:paragraph',
|
||||
{ text: new doc.Text(text) },
|
||||
noteId
|
||||
);
|
||||
const id = doc.addBlock('affine:paragraph', { text: new Text(text) }, noteId);
|
||||
|
||||
focusTextModel(std, id, text.length);
|
||||
next();
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { Workspace } from '@blocksuite/store';
|
||||
import { Text } from '@blocksuite/store';
|
||||
|
||||
export function createDefaultDoc(
|
||||
collection: Workspace,
|
||||
@@ -9,7 +10,7 @@ export function createDefaultDoc(
|
||||
doc.load();
|
||||
const title = options.title ?? '';
|
||||
const rootId = doc.addBlock('affine:page', {
|
||||
title: new doc.Text(title),
|
||||
title: new Text(title),
|
||||
});
|
||||
collection.meta.setDocMeta(doc.id, {
|
||||
title,
|
||||
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
} from '@blocksuite/affine-model';
|
||||
import { propertyModelPresets } from '@blocksuite/data-view/property-pure-presets';
|
||||
import type { BlockModel, Blocks } from '@blocksuite/store';
|
||||
import { Schema } from '@blocksuite/store';
|
||||
import { Schema, Text } from '@blocksuite/store';
|
||||
import {
|
||||
createAutoIncrementIdGenerator,
|
||||
TestWorkspace,
|
||||
@@ -71,7 +71,7 @@ describe('DatabaseManager', () => {
|
||||
doc = createTestDoc();
|
||||
|
||||
rootId = doc.addBlock('affine:page', {
|
||||
title: new doc.Text('database test'),
|
||||
title: new Text('database test'),
|
||||
});
|
||||
noteBlockId = doc.addBlock('affine:note', {}, rootId);
|
||||
|
||||
@@ -114,14 +114,14 @@ describe('DatabaseManager', () => {
|
||||
p1 = doc.addBlock(
|
||||
'affine:paragraph',
|
||||
{
|
||||
text: new doc.Text('text1'),
|
||||
text: new Text('text1'),
|
||||
},
|
||||
databaseBlockId
|
||||
);
|
||||
p2 = doc.addBlock(
|
||||
'affine:paragraph',
|
||||
{
|
||||
text: new doc.Text('text2'),
|
||||
text: new Text('text2'),
|
||||
},
|
||||
databaseBlockId
|
||||
);
|
||||
@@ -173,7 +173,7 @@ describe('DatabaseManager', () => {
|
||||
const modelId = doc.addBlock(
|
||||
'affine:paragraph',
|
||||
{
|
||||
text: new doc.Text('paragraph'),
|
||||
text: new Text('paragraph'),
|
||||
},
|
||||
noteBlockId
|
||||
);
|
||||
@@ -201,7 +201,7 @@ describe('DatabaseManager', () => {
|
||||
const newRowId = doc.addBlock(
|
||||
'affine:paragraph',
|
||||
{
|
||||
text: new doc.Text('text3'),
|
||||
text: new Text('text3'),
|
||||
},
|
||||
databaseBlockId
|
||||
);
|
||||
|
||||
+3
-3
@@ -5,7 +5,7 @@ import { DocModeProvider } from '@blocksuite/affine-shared/services';
|
||||
import { getBlockProps } from '@blocksuite/affine-shared/utils';
|
||||
import type { EditorHost } from '@blocksuite/block-std';
|
||||
import { GfxBlockElementModel } from '@blocksuite/block-std/gfx';
|
||||
import { type BlockModel, type Blocks } from '@blocksuite/store';
|
||||
import { type BlockModel, type Blocks, Text } from '@blocksuite/store';
|
||||
|
||||
import {
|
||||
getElementProps,
|
||||
@@ -43,7 +43,7 @@ export function createLinkedDocFromNote(
|
||||
const linkedDoc = doc.collection.createDoc({});
|
||||
linkedDoc.load(() => {
|
||||
const rootId = linkedDoc.addBlock('affine:page', {
|
||||
title: new doc.Text(docTitle),
|
||||
title: new Text(docTitle),
|
||||
});
|
||||
linkedDoc.addBlock('affine:surface', {}, rootId);
|
||||
const blockProps = getBlockProps(note);
|
||||
@@ -74,7 +74,7 @@ export function createLinkedDocFromEdgelessElements(
|
||||
const linkedDoc = host.doc.collection.createDoc({});
|
||||
linkedDoc.load(() => {
|
||||
const rootId = linkedDoc.addBlock('affine:page', {
|
||||
title: new host.doc.Text(docTitle),
|
||||
title: new Text(docTitle),
|
||||
});
|
||||
const surfaceId = linkedDoc.addBlock('affine:surface', {}, rootId);
|
||||
const surface = getSurfaceBlock(linkedDoc);
|
||||
|
||||
@@ -53,7 +53,7 @@ import {
|
||||
} from '@blocksuite/affine-shared/services';
|
||||
import { getHostName, referenceToNode } from '@blocksuite/affine-shared/utils';
|
||||
import { type BlockStdScope, WidgetComponent } from '@blocksuite/block-std';
|
||||
import { type BlockModel } from '@blocksuite/store';
|
||||
import { type BlockModel, Text } from '@blocksuite/store';
|
||||
import { autoUpdate, computePosition, flip, offset } from '@floating-ui/dom';
|
||||
import { html, nothing, type TemplateResult } from 'lit';
|
||||
import { query, state } from 'lit/decorators.js';
|
||||
@@ -619,7 +619,7 @@ export class EmbedCardToolbar extends WidgetComponent<
|
||||
const insert = title || caption || url;
|
||||
yText.insert(0, insert);
|
||||
yText.format(0, insert.length, { link: url });
|
||||
const text = new doc.Text(yText);
|
||||
const text = new Text(yText);
|
||||
doc.addBlock(
|
||||
'affine:paragraph',
|
||||
{
|
||||
|
||||
@@ -709,9 +709,7 @@ export const defaultSlashMenuConfig: SlashMenuConfig = {
|
||||
model.flavour as never,
|
||||
{
|
||||
type: (model as ParagraphBlockModel).type,
|
||||
text: new rootComponent.doc.Text(
|
||||
model.text.toDelta() as DeltaInsert[]
|
||||
),
|
||||
text: new Text(model.text.toDelta() as DeltaInsert[]),
|
||||
// @ts-expect-error FIXME: ts error
|
||||
checked: model.checked,
|
||||
},
|
||||
|
||||
@@ -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';
|
||||
|
||||
+18
-63
@@ -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,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { effects as blocksEffects } from '@blocksuite/blocks/effects';
|
||||
import type { BlockCollection, Blocks, Job } from '@blocksuite/store';
|
||||
import type { Blocks, Job } from '@blocksuite/store';
|
||||
|
||||
import { effects } from '../../effects.js';
|
||||
|
||||
@@ -57,9 +57,7 @@ function initCollection(collection: TestWorkspace) {
|
||||
|
||||
async function createEditor(collection: TestWorkspace, mode: DocMode = 'page') {
|
||||
const app = document.createElement('div');
|
||||
const blockCollection = collection.docs.values().next().value as
|
||||
| BlockCollection
|
||||
| undefined;
|
||||
const blockCollection = collection.docs.values().next().value;
|
||||
assertExists(blockCollection, 'Need to create a doc first');
|
||||
const doc = blockCollection.getDoc();
|
||||
const editor = new AffineEditorContainer();
|
||||
|
||||
@@ -64,11 +64,11 @@ test(scoped`basic init with external text`, async ({ page }) => {
|
||||
await page.evaluate(() => {
|
||||
const { doc } = window;
|
||||
const rootId = doc.addBlock('affine:page', {
|
||||
title: new doc.Text('hello'),
|
||||
title: new window.$blocksuite.store.Text('hello'),
|
||||
});
|
||||
const note = doc.addBlock('affine:note', {}, rootId);
|
||||
|
||||
const text = new doc.Text('world');
|
||||
const text = new window.$blocksuite.store.Text('world');
|
||||
doc.addBlock('affine:paragraph', { text }, note);
|
||||
|
||||
const delta = [
|
||||
@@ -78,7 +78,7 @@ test(scoped`basic init with external text`, async ({ page }) => {
|
||||
doc.addBlock(
|
||||
'affine:paragraph',
|
||||
{
|
||||
text: new doc.Text(delta as DeltaInsert[]),
|
||||
text: new window.$blocksuite.store.Text(delta as DeltaInsert[]),
|
||||
},
|
||||
note
|
||||
);
|
||||
|
||||
@@ -628,7 +628,7 @@ test.fixme(
|
||||
const { doc } = window;
|
||||
|
||||
const rootId = doc.addBlock('affine:page', {
|
||||
title: new doc.Text(),
|
||||
title: new window.$blocksuite.store.Text(),
|
||||
});
|
||||
doc.addBlock('affine:surface', {}, rootId);
|
||||
|
||||
@@ -637,7 +637,7 @@ test.fixme(
|
||||
doc.addBlock(
|
||||
'affine:paragraph',
|
||||
{
|
||||
text: new doc.Text(text),
|
||||
text: new window.$blocksuite.store.Text(text),
|
||||
},
|
||||
noteId
|
||||
);
|
||||
|
||||
@@ -554,7 +554,7 @@ test('press backspace at the start of first line when edgeless text exist', asyn
|
||||
await page.evaluate(() => {
|
||||
const { doc } = window;
|
||||
const rootId = doc.addBlock('affine:page', {
|
||||
title: new doc.Text(),
|
||||
title: new window.$blocksuite.store.Text(),
|
||||
});
|
||||
doc.addBlock('affine:surface', {}, rootId);
|
||||
doc.addBlock('affine:note', {}, rootId);
|
||||
|
||||
@@ -140,7 +140,7 @@ test.describe('Embed synced doc', () => {
|
||||
await page.evaluate(() => {
|
||||
const { doc, collection } = window;
|
||||
const rootId = doc.addBlock('affine:page', {
|
||||
title: new doc.Text(),
|
||||
title: new window.$blocksuite.store.Text(),
|
||||
});
|
||||
|
||||
const noteId = doc.addBlock('affine:note', {}, rootId);
|
||||
@@ -149,14 +149,14 @@ test.describe('Embed synced doc', () => {
|
||||
const doc2 = collection.createDoc({ id: 'doc2' });
|
||||
doc2.load();
|
||||
const rootId2 = doc2.addBlock('affine:page', {
|
||||
title: new doc.Text('Doc 2'),
|
||||
title: new window.$blocksuite.store.Text('Doc 2'),
|
||||
});
|
||||
|
||||
const noteId2 = doc2.addBlock('affine:note', {}, rootId2);
|
||||
doc2.addBlock(
|
||||
'affine:paragraph',
|
||||
{
|
||||
text: new doc.Text('Hello from Doc 2'),
|
||||
text: new window.$blocksuite.store.Text('Hello from Doc 2'),
|
||||
},
|
||||
noteId2
|
||||
);
|
||||
@@ -164,14 +164,14 @@ test.describe('Embed synced doc', () => {
|
||||
const doc3 = collection.createDoc({ id: 'doc3' });
|
||||
doc3.load();
|
||||
const rootId3 = doc3.addBlock('affine:page', {
|
||||
title: new doc.Text('Doc 3'),
|
||||
title: new window.$blocksuite.store.Text('Doc 3'),
|
||||
});
|
||||
|
||||
const noteId3 = doc3.addBlock('affine:note', {}, rootId3);
|
||||
doc3.addBlock(
|
||||
'affine:paragraph',
|
||||
{
|
||||
text: new doc.Text('Hello from Doc 3'),
|
||||
text: new window.$blocksuite.store.Text('Hello from Doc 3'),
|
||||
},
|
||||
noteId3
|
||||
);
|
||||
@@ -232,7 +232,7 @@ test.describe('Embed synced doc', () => {
|
||||
const databaseId = doc2.addBlock(
|
||||
'affine:database',
|
||||
{
|
||||
title: new doc2.Text('Database 1'),
|
||||
title: new window.$blocksuite.store.Text('Database 1'),
|
||||
},
|
||||
noteId
|
||||
);
|
||||
|
||||
@@ -453,10 +453,10 @@ test('should format quick bar position correct at the start of second line', asy
|
||||
await page.evaluate(() => {
|
||||
const { doc } = window;
|
||||
const rootId = doc.addBlock('affine:page', {
|
||||
title: new doc.Text(),
|
||||
title: new window.$blocksuite.store.Text(),
|
||||
});
|
||||
const note = doc.addBlock('affine:note', {}, rootId);
|
||||
const text = new doc.Text('a'.repeat(100));
|
||||
const text = new window.$blocksuite.store.Text('a'.repeat(100));
|
||||
const paragraphId = doc.addBlock('affine:paragraph', { text }, note);
|
||||
return paragraphId;
|
||||
});
|
||||
@@ -715,7 +715,7 @@ test('should format bar style active correctly', async ({ page }) => {
|
||||
await page.evaluate(() => {
|
||||
const { doc } = window;
|
||||
const rootId = doc.addBlock('affine:page', {
|
||||
title: new doc.Text(),
|
||||
title: new window.$blocksuite.store.Text(),
|
||||
});
|
||||
const note = doc.addBlock('affine:note', {}, rootId);
|
||||
const delta = [
|
||||
@@ -723,7 +723,7 @@ test('should format bar style active correctly', async ({ page }) => {
|
||||
{ insert: '2', attributes: { bold: true, underline: true } },
|
||||
{ insert: '3', attributes: { bold: true, code: true } },
|
||||
];
|
||||
const text = new doc.Text(delta as DeltaInsert[]);
|
||||
const text = new window.$blocksuite.store.Text(delta as DeltaInsert[]);
|
||||
doc.addBlock('affine:paragraph', { text }, note);
|
||||
});
|
||||
|
||||
@@ -869,7 +869,7 @@ test('should update the format quick bar state when there is a change in keyboar
|
||||
await page.evaluate(() => {
|
||||
const { doc } = window;
|
||||
const rootId = doc.addBlock('affine:page', {
|
||||
title: new doc.Text(),
|
||||
title: new window.$blocksuite.store.Text(),
|
||||
});
|
||||
const note = doc.addBlock('affine:note', {}, rootId);
|
||||
const delta = [
|
||||
@@ -877,7 +877,7 @@ test('should update the format quick bar state when there is a change in keyboar
|
||||
{ insert: '2', attributes: { bold: true } },
|
||||
{ insert: '3', attributes: { bold: false } },
|
||||
];
|
||||
const text = new doc.Text(delta as DeltaInsert[]);
|
||||
const text = new window.$blocksuite.store.Text(delta as DeltaInsert[]);
|
||||
doc.addBlock('affine:paragraph', { text }, note);
|
||||
});
|
||||
await focusTitle(page);
|
||||
|
||||
@@ -121,11 +121,11 @@ async function createLinkBlock(page: Page, str: string, link: string) {
|
||||
([str, link]) => {
|
||||
const { doc } = window;
|
||||
const rootId = doc.addBlock('affine:page', {
|
||||
title: new doc.Text('title'),
|
||||
title: new window.$blocksuite.store.Text('title'),
|
||||
});
|
||||
const noteId = doc.addBlock('affine:note', {}, rootId);
|
||||
|
||||
const text = new doc.Text([
|
||||
const text = new window.$blocksuite.store.Text([
|
||||
{ insert: 'Hello' },
|
||||
{ insert: str, attributes: { link } },
|
||||
]);
|
||||
|
||||
@@ -825,20 +825,20 @@ test('press arrow down should move caret to the start of line', async ({
|
||||
await page.evaluate(() => {
|
||||
const { doc } = window;
|
||||
const rootId = doc.addBlock('affine:page', {
|
||||
title: new doc.Text(),
|
||||
title: new window.$blocksuite.store.Text(),
|
||||
});
|
||||
const note = doc.addBlock('affine:note', {}, rootId);
|
||||
doc.addBlock(
|
||||
'affine:paragraph',
|
||||
{
|
||||
text: new doc.Text('0'.repeat(100)),
|
||||
text: new window.$blocksuite.store.Text('0'.repeat(100)),
|
||||
},
|
||||
note
|
||||
);
|
||||
doc.addBlock(
|
||||
'affine:paragraph',
|
||||
{
|
||||
text: new doc.Text('1'),
|
||||
text: new window.$blocksuite.store.Text('1'),
|
||||
},
|
||||
note
|
||||
);
|
||||
@@ -860,7 +860,7 @@ test('press arrow up in the second line should move caret to the first line', as
|
||||
await page.evaluate(() => {
|
||||
const { doc } = window;
|
||||
const rootId = doc.addBlock('affine:page', {
|
||||
title: new doc.Text(),
|
||||
title: new window.$blocksuite.store.Text(),
|
||||
});
|
||||
const note = doc.addBlock('affine:note', {}, rootId);
|
||||
const delta = Array.from({ length: 150 }, (_, i) => {
|
||||
@@ -868,7 +868,7 @@ test('press arrow up in the second line should move caret to the first line', as
|
||||
? { insert: 'i', attributes: { italic: true } }
|
||||
: { insert: 'b', attributes: { bold: true } };
|
||||
}) as DeltaInsert[];
|
||||
const text = new doc.Text(delta);
|
||||
const text = new window.$blocksuite.store.Text(delta);
|
||||
doc.addBlock('affine:paragraph', { text }, note);
|
||||
doc.addBlock('affine:paragraph', {}, note);
|
||||
});
|
||||
@@ -912,7 +912,7 @@ test('press arrow down in indent line should not move caret to the start of line
|
||||
await page.evaluate(() => {
|
||||
const { doc } = window;
|
||||
const rootId = doc.addBlock('affine:page', {
|
||||
title: new doc.Text(),
|
||||
title: new window.$blocksuite.store.Text(),
|
||||
});
|
||||
const note = doc.addBlock('affine:note', {}, rootId);
|
||||
const p1 = doc.addBlock('affine:paragraph', {}, note);
|
||||
@@ -921,7 +921,7 @@ test('press arrow down in indent line should not move caret to the start of line
|
||||
doc.addBlock(
|
||||
'affine:paragraph',
|
||||
{
|
||||
text: new doc.Text('0'),
|
||||
text: new window.$blocksuite.store.Text('0'),
|
||||
},
|
||||
note
|
||||
);
|
||||
@@ -1003,20 +1003,22 @@ test.describe('press ArrowDown when cursor is at the last line of a block', () =
|
||||
await page.evaluate(() => {
|
||||
const { doc } = window;
|
||||
const rootId = doc.addBlock('affine:page', {
|
||||
title: new doc.Text(),
|
||||
title: new window.$blocksuite.store.Text(),
|
||||
});
|
||||
const note = doc.addBlock('affine:note', {}, rootId);
|
||||
doc.addBlock(
|
||||
'affine:paragraph',
|
||||
{
|
||||
text: new doc.Text('This is the 2nd last block.'),
|
||||
text: new window.$blocksuite.store.Text(
|
||||
'This is the 2nd last block.'
|
||||
),
|
||||
},
|
||||
note
|
||||
);
|
||||
doc.addBlock(
|
||||
'affine:paragraph',
|
||||
{
|
||||
text: new doc.Text('This is the last block.'),
|
||||
text: new window.$blocksuite.store.Text('This is the last block.'),
|
||||
},
|
||||
note
|
||||
);
|
||||
@@ -1172,7 +1174,7 @@ test('delete at the start of paragraph (multiple notes)', async ({ page }) => {
|
||||
const { doc } = window;
|
||||
|
||||
const rootId = doc.addBlock('affine:page', {
|
||||
title: new doc.Text(),
|
||||
title: new window.$blocksuite.store.Text(),
|
||||
});
|
||||
doc.addBlock('affine:surface', {}, rootId);
|
||||
|
||||
@@ -1181,7 +1183,7 @@ test('delete at the start of paragraph (multiple notes)', async ({ page }) => {
|
||||
doc.addBlock(
|
||||
'affine:paragraph',
|
||||
{
|
||||
text: new doc.Text(text),
|
||||
text: new window.$blocksuite.store.Text(text),
|
||||
},
|
||||
noteId
|
||||
);
|
||||
|
||||
@@ -413,7 +413,7 @@ export async function enterPlaygroundWithList(
|
||||
({ contents, type }: { contents: string[]; type: ListType }) => {
|
||||
const { doc } = window;
|
||||
const rootId = doc.addBlock('affine:page', {
|
||||
title: new doc.Text(),
|
||||
title: new window.$blocksuite.store.Text(),
|
||||
});
|
||||
const noteId = doc.addBlock('affine:note', {}, rootId);
|
||||
// eslint-disable-next-line @typescript-eslint/prefer-for-of
|
||||
@@ -421,7 +421,7 @@ export async function enterPlaygroundWithList(
|
||||
doc.addBlock(
|
||||
'affine:list',
|
||||
contents.length > 0
|
||||
? { text: new doc.Text(contents[i]), type }
|
||||
? { text: new window.$blocksuite.store.Text(contents[i]), type }
|
||||
: { type },
|
||||
noteId
|
||||
);
|
||||
@@ -439,7 +439,7 @@ export async function initEmptyParagraphState(page: Page, rootId?: string) {
|
||||
doc.captureSync();
|
||||
if (!rootId) {
|
||||
rootId = doc.addBlock('affine:page', {
|
||||
title: new doc.Text(),
|
||||
title: new window.$blocksuite.store.Text(),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -464,7 +464,7 @@ export async function initMultipleNoteWithParagraphState(
|
||||
doc.captureSync();
|
||||
if (!rootId) {
|
||||
rootId = doc.addBlock('affine:page', {
|
||||
title: new doc.Text(),
|
||||
title: new window.$blocksuite.store.Text(),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -490,7 +490,7 @@ export async function initEmptyEdgelessState(page: Page) {
|
||||
const ids = await page.evaluate(() => {
|
||||
const { doc } = window;
|
||||
const rootId = doc.addBlock('affine:page', {
|
||||
title: new doc.Text(),
|
||||
title: new window.$blocksuite.store.Text(),
|
||||
});
|
||||
doc.addBlock('affine:surface', {}, rootId);
|
||||
const noteId = doc.addBlock('affine:note', {}, rootId);
|
||||
@@ -509,14 +509,14 @@ export async function initEmptyDatabaseState(page: Page, rootId?: string) {
|
||||
doc.captureSync();
|
||||
if (!rootId) {
|
||||
rootId = doc.addBlock('affine:page', {
|
||||
title: new doc.Text(),
|
||||
title: new window.$blocksuite.store.Text(),
|
||||
});
|
||||
}
|
||||
const noteId = doc.addBlock('affine:note', {}, rootId);
|
||||
const databaseId = doc.addBlock(
|
||||
'affine:database',
|
||||
{
|
||||
title: new doc.Text('Database 1'),
|
||||
title: new window.$blocksuite.store.Text('Database 1'),
|
||||
},
|
||||
noteId
|
||||
);
|
||||
@@ -553,14 +553,14 @@ export async function initKanbanViewState(
|
||||
doc.captureSync();
|
||||
if (!rootId) {
|
||||
rootId = doc.addBlock('affine:page', {
|
||||
title: new doc.Text(),
|
||||
title: new window.$blocksuite.store.Text(),
|
||||
});
|
||||
}
|
||||
const noteId = doc.addBlock('affine:note', {}, rootId);
|
||||
const databaseId = doc.addBlock(
|
||||
'affine:database',
|
||||
{
|
||||
title: new doc.Text('Database 1'),
|
||||
title: new window.$blocksuite.store.Text('Database 1'),
|
||||
},
|
||||
noteId
|
||||
);
|
||||
@@ -572,7 +572,7 @@ export async function initKanbanViewState(
|
||||
const rowIds = config.rows.map(rowText => {
|
||||
const rowId = doc.addBlock(
|
||||
'affine:paragraph',
|
||||
{ type: 'text', text: new doc.Text(rowText) },
|
||||
{ type: 'text', text: new window.$blocksuite.store.Text(rowText) },
|
||||
databaseId
|
||||
);
|
||||
return rowId;
|
||||
@@ -590,7 +590,7 @@ export async function initKanbanViewState(
|
||||
columnId,
|
||||
value:
|
||||
column.type === 'rich-text'
|
||||
? new doc.Text(value as string)
|
||||
? new window.$blocksuite.store.Text(value as string)
|
||||
: value,
|
||||
});
|
||||
}
|
||||
@@ -619,14 +619,14 @@ export async function initEmptyDatabaseWithParagraphState(
|
||||
doc.captureSync();
|
||||
if (!rootId) {
|
||||
rootId = doc.addBlock('affine:page', {
|
||||
title: new doc.Text(),
|
||||
title: new window.$blocksuite.store.Text(),
|
||||
});
|
||||
}
|
||||
const noteId = doc.addBlock('affine:note', {}, rootId);
|
||||
const databaseId = doc.addBlock(
|
||||
'affine:database',
|
||||
{
|
||||
title: new doc.Text('Database 1'),
|
||||
title: new window.$blocksuite.store.Text('Database 1'),
|
||||
},
|
||||
noteId
|
||||
);
|
||||
@@ -1326,7 +1326,7 @@ export async function initImageState(page: Page, prependParagraph = false) {
|
||||
await page.evaluate(async prepend => {
|
||||
const { doc } = window;
|
||||
const rootId = doc.addBlock('affine:page', {
|
||||
title: new doc.Text(),
|
||||
title: new window.$blocksuite.store.Text(),
|
||||
});
|
||||
const noteId = doc.addBlock('affine:note', {}, rootId);
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import type {
|
||||
} from '@blocksuite/block-std';
|
||||
import type { AffineEditorContainer } from '@blocksuite/presets';
|
||||
import type { StarterDebugMenu } from '@playground/apps/_common/components/starter-debug-menu.js';
|
||||
import type { BlockModel, Doc, Job, Workspace } from '@store/index.js';
|
||||
import type { BlockModel, Blocks, Job, Workspace } from '@store/index.js';
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
@@ -38,7 +38,7 @@ declare global {
|
||||
};
|
||||
collection: Workspace;
|
||||
blockSchema: Record<string, typeof BlockModel>;
|
||||
doc: Doc;
|
||||
doc: Blocks;
|
||||
debugMenu: StarterDebugMenu;
|
||||
editor: AffineEditorContainer;
|
||||
host: EditorHost;
|
||||
|
||||
@@ -44,7 +44,7 @@ test(
|
||||
await page.evaluate(() => {
|
||||
const { doc } = window;
|
||||
const rootId = doc.addBlock('affine:page', {
|
||||
title: new doc.Text(),
|
||||
title: new window.$blocksuite.store.Text(),
|
||||
});
|
||||
const note = doc.addBlock('affine:note', {}, rootId);
|
||||
doc.addBlock('affine:code', {}, note);
|
||||
@@ -71,7 +71,7 @@ test(
|
||||
async ({ bookMarkUrl, embedUrl }) => {
|
||||
const { doc } = window;
|
||||
const rootId = doc.addBlock('affine:page', {
|
||||
title: new doc.Text(),
|
||||
title: new window.$blocksuite.store.Text(),
|
||||
});
|
||||
const note = doc.addBlock('affine:note', {}, rootId);
|
||||
doc.addBlock('affine:code', {}, note);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { DeltaInsert } from '@blocksuite/affine/inline';
|
||||
import type { Workspace } from '@blocksuite/affine/store';
|
||||
import { Text, type Workspace } from '@blocksuite/affine/store';
|
||||
import type { AffineTextAttributes } from '@blocksuite/affine-shared/types';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
@@ -10,7 +10,7 @@ export function useReferenceLinkHelper(docCollection: Workspace) {
|
||||
if (!page) {
|
||||
return;
|
||||
}
|
||||
const text = new page.Text([
|
||||
const text = new Text([
|
||||
{
|
||||
insert: ' ',
|
||||
attributes: {
|
||||
|
||||
+4
-4
@@ -5,7 +5,7 @@ import 'fake-indexeddb/auto';
|
||||
|
||||
import { AffineSchemas } from '@blocksuite/affine/blocks/schemas';
|
||||
import { assertExists } from '@blocksuite/affine/global/utils';
|
||||
import type { Blocks } from '@blocksuite/affine/store';
|
||||
import { type Blocks, Text } from '@blocksuite/affine/store';
|
||||
import { Schema } from '@blocksuite/store';
|
||||
import { TestWorkspace } from '@blocksuite/store/test';
|
||||
import { renderHook } from '@testing-library/react';
|
||||
@@ -27,7 +27,7 @@ beforeEach(async () => {
|
||||
expect(page).not.toBeNull();
|
||||
assertExists(page);
|
||||
const pageBlockId = page.addBlock('affine:page', {
|
||||
title: new page.Text(''),
|
||||
title: new Text(''),
|
||||
});
|
||||
const frameId = page.addBlock('affine:note', {}, pageBlockId);
|
||||
page.addBlock('affine:paragraph', {}, frameId);
|
||||
@@ -41,7 +41,7 @@ describe('useBlockSuitePagePreview', () => {
|
||||
const id = page.addBlock(
|
||||
'affine:paragraph',
|
||||
{
|
||||
text: new page.Text('Hello, world!'),
|
||||
text: new Text('Hello, world!'),
|
||||
},
|
||||
page.getBlockByFlavour('affine:note')[0].id
|
||||
);
|
||||
@@ -58,7 +58,7 @@ describe('useBlockSuitePagePreview', () => {
|
||||
page.addBlock(
|
||||
'affine:paragraph',
|
||||
{
|
||||
text: new page.Text('First block!'),
|
||||
text: new Text('First block!'),
|
||||
},
|
||||
page.getBlockByFlavour('affine:note')[0].id,
|
||||
0
|
||||
|
||||
@@ -2,6 +2,7 @@ import { DebugLogger } from '@affine/debug';
|
||||
import { Unreachable } from '@affine/env/constant';
|
||||
import type { DocMode } from '@blocksuite/affine/blocks';
|
||||
import type { DeltaInsert } from '@blocksuite/affine/inline';
|
||||
import { Text } from '@blocksuite/affine/store';
|
||||
import type { AffineTextAttributes } from '@blocksuite/affine-shared/types';
|
||||
import { LiveData, ObjectPool, Service } from '@toeverything/infra';
|
||||
import { omitBy } from 'lodash-es';
|
||||
@@ -121,7 +122,7 @@ export class DocsService extends Service {
|
||||
const { doc, release } = this.open(targetDocId);
|
||||
doc.setPriorityLoad(10);
|
||||
await doc.waitForSyncReady();
|
||||
const text = new doc.blockSuiteDoc.Text([
|
||||
const text = new Text([
|
||||
{
|
||||
insert: ' ',
|
||||
attributes: {
|
||||
|
||||
@@ -0,0 +1,383 @@
|
||||
import { type Disposable, Slot } from '@blocksuite/affine/global/utils';
|
||||
import {
|
||||
type AwarenessStore,
|
||||
Blocks,
|
||||
type BlockSuiteDoc,
|
||||
type Doc,
|
||||
type GetDocOptions,
|
||||
type Query,
|
||||
type Workspace,
|
||||
type YBlock,
|
||||
} from '@blocksuite/affine/store';
|
||||
import { signal } from '@preact/signals-core';
|
||||
import * as Y from 'yjs';
|
||||
|
||||
type DocOptions = {
|
||||
id: string;
|
||||
collection: Workspace;
|
||||
doc: BlockSuiteDoc;
|
||||
awarenessStore: AwarenessStore;
|
||||
};
|
||||
|
||||
export class DocImpl implements Doc {
|
||||
private _awarenessUpdateDisposable: Disposable | null = null;
|
||||
|
||||
private readonly _canRedo = signal(false);
|
||||
|
||||
private readonly _canUndo = signal(false);
|
||||
|
||||
private readonly _collection: Workspace;
|
||||
|
||||
private readonly _docMap = {
|
||||
undefined: new Map<string, Blocks>(),
|
||||
true: new Map<string, Blocks>(),
|
||||
false: new Map<string, Blocks>(),
|
||||
};
|
||||
|
||||
// doc/space container.
|
||||
private readonly _handleYEvents = (events: Y.YEvent<YBlock | Y.Text>[]) => {
|
||||
events.forEach(event => this._handleYEvent(event));
|
||||
};
|
||||
|
||||
private _history!: Y.UndoManager;
|
||||
|
||||
private readonly _historyObserver = () => {
|
||||
this._updateCanUndoRedoSignals();
|
||||
this.slots.historyUpdated.emit();
|
||||
};
|
||||
|
||||
private readonly _initSubDoc = () => {
|
||||
let subDoc = this.rootDoc.spaces.get(this.id);
|
||||
if (!subDoc) {
|
||||
subDoc = new Y.Doc({
|
||||
guid: this.id,
|
||||
});
|
||||
this.rootDoc.spaces.set(this.id, subDoc);
|
||||
this._loaded = true;
|
||||
this._onLoadSlot.emit();
|
||||
} else {
|
||||
this._loaded = false;
|
||||
this.rootDoc.on('subdocs', this._onSubdocEvent);
|
||||
}
|
||||
|
||||
return subDoc;
|
||||
};
|
||||
|
||||
private _loaded!: boolean;
|
||||
|
||||
private readonly _onLoadSlot = new Slot();
|
||||
|
||||
private readonly _onSubdocEvent = ({
|
||||
loaded,
|
||||
}: {
|
||||
loaded: Set<Y.Doc>;
|
||||
}): void => {
|
||||
const result = Array.from(loaded).find(
|
||||
doc => doc.guid === this._ySpaceDoc.guid
|
||||
);
|
||||
if (!result) {
|
||||
return;
|
||||
}
|
||||
this.rootDoc.off('subdocs', this._onSubdocEvent);
|
||||
this._loaded = true;
|
||||
this._onLoadSlot.emit();
|
||||
};
|
||||
|
||||
/** Indicate whether the block tree is ready */
|
||||
private _ready = false;
|
||||
|
||||
private _shouldTransact = true;
|
||||
|
||||
private readonly _updateCanUndoRedoSignals = () => {
|
||||
const canRedo = this.readonly ? false : this._history.canRedo();
|
||||
const canUndo = this.readonly ? false : this._history.canUndo();
|
||||
if (this._canRedo.peek() !== canRedo) {
|
||||
this._canRedo.value = canRedo;
|
||||
}
|
||||
if (this._canUndo.peek() !== canUndo) {
|
||||
this._canUndo.value = canUndo;
|
||||
}
|
||||
};
|
||||
|
||||
protected readonly _yBlocks: Y.Map<YBlock>;
|
||||
|
||||
/**
|
||||
* @internal Used for convenient access to the underlying Yjs map,
|
||||
* can be used interchangeably with ySpace
|
||||
*/
|
||||
protected readonly _ySpaceDoc: Y.Doc;
|
||||
|
||||
readonly awarenessStore: AwarenessStore;
|
||||
|
||||
readonly id: string;
|
||||
|
||||
readonly rootDoc: BlockSuiteDoc;
|
||||
|
||||
readonly slots = {
|
||||
historyUpdated: new Slot(),
|
||||
yBlockUpdated: new Slot<
|
||||
| {
|
||||
type: 'add';
|
||||
id: string;
|
||||
}
|
||||
| {
|
||||
type: 'delete';
|
||||
id: string;
|
||||
}
|
||||
>(),
|
||||
};
|
||||
|
||||
get blobSync() {
|
||||
return this.collection.blobSync;
|
||||
}
|
||||
|
||||
get canRedo() {
|
||||
return this._canRedo.peek();
|
||||
}
|
||||
|
||||
get canUndo() {
|
||||
return this._canUndo.peek();
|
||||
}
|
||||
|
||||
get collection() {
|
||||
return this._collection;
|
||||
}
|
||||
|
||||
get docSync() {
|
||||
return this.collection.docSync;
|
||||
}
|
||||
|
||||
get history() {
|
||||
return this._history;
|
||||
}
|
||||
|
||||
get isEmpty() {
|
||||
return this._yBlocks.size === 0;
|
||||
}
|
||||
|
||||
get loaded() {
|
||||
return this._loaded;
|
||||
}
|
||||
|
||||
get meta() {
|
||||
return this.collection.meta.getDocMeta(this.id);
|
||||
}
|
||||
|
||||
get readonly(): boolean {
|
||||
return this.awarenessStore.isReadonly(this);
|
||||
}
|
||||
|
||||
get ready() {
|
||||
return this._ready;
|
||||
}
|
||||
|
||||
get schema() {
|
||||
return this.collection.schema;
|
||||
}
|
||||
|
||||
get spaceDoc() {
|
||||
return this._ySpaceDoc;
|
||||
}
|
||||
|
||||
get yBlocks() {
|
||||
return this._yBlocks;
|
||||
}
|
||||
|
||||
constructor({ id, collection, doc, awarenessStore }: DocOptions) {
|
||||
this.id = id;
|
||||
this.rootDoc = doc;
|
||||
this.awarenessStore = awarenessStore;
|
||||
|
||||
this._ySpaceDoc = this._initSubDoc();
|
||||
|
||||
this._yBlocks = this._ySpaceDoc.getMap('blocks');
|
||||
this._collection = collection;
|
||||
}
|
||||
|
||||
private _getReadonlyKey(readonly?: boolean): 'true' | 'false' | 'undefined' {
|
||||
return (readonly?.toString() as 'true' | 'false') ?? 'undefined';
|
||||
}
|
||||
|
||||
private _handleVersion() {
|
||||
// Initialization from empty yDoc, indicating that the document is new.
|
||||
if (!this.collection.meta.hasVersion) {
|
||||
this.collection.meta.writeVersion(this.collection);
|
||||
}
|
||||
}
|
||||
|
||||
private _handleYBlockAdd(id: string) {
|
||||
this.slots.yBlockUpdated.emit({ type: 'add', id });
|
||||
}
|
||||
|
||||
private _handleYBlockDelete(id: string) {
|
||||
this.slots.yBlockUpdated.emit({ type: 'delete', id });
|
||||
}
|
||||
|
||||
private _handleYEvent(event: Y.YEvent<YBlock | Y.Text | Y.Array<unknown>>) {
|
||||
// event on top-level block store
|
||||
if (event.target !== this._yBlocks) {
|
||||
return;
|
||||
}
|
||||
event.keys.forEach((value, id) => {
|
||||
try {
|
||||
if (value.action === 'add') {
|
||||
this._handleYBlockAdd(id);
|
||||
return;
|
||||
}
|
||||
if (value.action === 'delete') {
|
||||
this._handleYBlockDelete(id);
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('An error occurred while handling Yjs event:');
|
||||
console.error(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private _initYBlocks() {
|
||||
const { _yBlocks } = this;
|
||||
_yBlocks.observeDeep(this._handleYEvents);
|
||||
this._history = new Y.UndoManager([_yBlocks], {
|
||||
trackedOrigins: new Set([this._ySpaceDoc.clientID]),
|
||||
});
|
||||
|
||||
this._history.on('stack-cleared', this._historyObserver);
|
||||
this._history.on('stack-item-added', this._historyObserver);
|
||||
this._history.on('stack-item-popped', this._historyObserver);
|
||||
this._history.on('stack-item-updated', this._historyObserver);
|
||||
}
|
||||
|
||||
/** Capture current operations to undo stack synchronously. */
|
||||
captureSync() {
|
||||
this._history.stopCapturing();
|
||||
}
|
||||
|
||||
clear() {
|
||||
this._yBlocks.clear();
|
||||
}
|
||||
|
||||
clearQuery(query: Query, readonly?: boolean) {
|
||||
const readonlyKey = this._getReadonlyKey(readonly);
|
||||
|
||||
this._docMap[readonlyKey].delete(JSON.stringify(query));
|
||||
}
|
||||
|
||||
private _destroy() {
|
||||
this._ySpaceDoc.destroy();
|
||||
this._onLoadSlot.dispose();
|
||||
this._loaded = false;
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this.slots.historyUpdated.dispose();
|
||||
this._awarenessUpdateDisposable?.dispose();
|
||||
|
||||
if (this.ready) {
|
||||
this._yBlocks.unobserveDeep(this._handleYEvents);
|
||||
this._yBlocks.clear();
|
||||
}
|
||||
}
|
||||
|
||||
getDoc({ readonly, query }: GetDocOptions = {}) {
|
||||
const readonlyKey = this._getReadonlyKey(readonly);
|
||||
|
||||
const key = JSON.stringify(query);
|
||||
|
||||
if (this._docMap[readonlyKey].has(key)) {
|
||||
return this._docMap[readonlyKey].get(key) as Blocks;
|
||||
}
|
||||
|
||||
const doc = new Blocks({
|
||||
blockCollection: this,
|
||||
schema: this.collection.schema,
|
||||
readonly,
|
||||
query,
|
||||
});
|
||||
|
||||
this._docMap[readonlyKey].set(key, doc);
|
||||
|
||||
return doc;
|
||||
}
|
||||
|
||||
load(initFn?: () => void): this {
|
||||
if (this.ready) {
|
||||
return this;
|
||||
}
|
||||
|
||||
this._ySpaceDoc.load();
|
||||
|
||||
if ((this.collection.meta.docs?.length ?? 0) <= 1) {
|
||||
this._handleVersion();
|
||||
}
|
||||
|
||||
this._initYBlocks();
|
||||
|
||||
this._yBlocks.forEach((_, id) => {
|
||||
this._handleYBlockAdd(id);
|
||||
});
|
||||
|
||||
this._awarenessUpdateDisposable = this.awarenessStore.slots.update.on(
|
||||
() => {
|
||||
// change readonly state will affect the undo/redo state
|
||||
this._updateCanUndoRedoSignals();
|
||||
}
|
||||
);
|
||||
|
||||
initFn?.();
|
||||
|
||||
this._ready = true;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
redo() {
|
||||
if (this.readonly) {
|
||||
console.error('cannot modify data in readonly mode');
|
||||
return;
|
||||
}
|
||||
this._history.redo();
|
||||
}
|
||||
|
||||
undo() {
|
||||
if (this.readonly) {
|
||||
console.error('cannot modify data in readonly mode');
|
||||
return;
|
||||
}
|
||||
this._history.undo();
|
||||
}
|
||||
|
||||
remove() {
|
||||
this._destroy();
|
||||
this.rootDoc.spaces.delete(this.id);
|
||||
}
|
||||
|
||||
resetHistory() {
|
||||
this._history.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* If `shouldTransact` is `false`, the transaction will not be push to the history stack.
|
||||
*/
|
||||
transact(fn: () => void, shouldTransact: boolean = this._shouldTransact) {
|
||||
this._ySpaceDoc.transact(
|
||||
() => {
|
||||
try {
|
||||
fn();
|
||||
} catch (e) {
|
||||
console.error(
|
||||
`An error occurred while Y.doc ${this._ySpaceDoc.guid} transacting:`
|
||||
);
|
||||
console.error(e);
|
||||
}
|
||||
},
|
||||
shouldTransact ? this.rootDoc.clientID : null
|
||||
);
|
||||
}
|
||||
|
||||
withoutTransact(callback: () => void) {
|
||||
this._shouldTransact = false;
|
||||
callback();
|
||||
this._shouldTransact = true;
|
||||
}
|
||||
}
|
||||
@@ -6,10 +6,10 @@ import type { BlockSuiteFlags } from '@blocksuite/affine/global/types';
|
||||
import { NoopLogger, Slot } from '@blocksuite/affine/global/utils';
|
||||
import {
|
||||
AwarenessStore,
|
||||
BlockCollection,
|
||||
type Blocks,
|
||||
BlockSuiteDoc,
|
||||
type CreateDocOptions,
|
||||
type Doc,
|
||||
DocCollectionMeta,
|
||||
type GetDocOptions,
|
||||
type IdGenerator,
|
||||
@@ -27,6 +27,8 @@ import {
|
||||
} from '@blocksuite/affine/sync';
|
||||
import { Awareness } from 'y-protocols/awareness.js';
|
||||
|
||||
import { DocImpl } from './doc';
|
||||
|
||||
type WorkspaceOptions = {
|
||||
id?: string;
|
||||
schema: Schema;
|
||||
@@ -62,7 +64,7 @@ export class WorkspaceImpl implements Workspace {
|
||||
|
||||
readonly blobSync: BlobEngine;
|
||||
|
||||
readonly blockCollections = new Map<string, BlockCollection>();
|
||||
readonly blockCollections = new Map<string, Doc>();
|
||||
|
||||
readonly doc: BlockSuiteDoc;
|
||||
|
||||
@@ -114,12 +116,11 @@ export class WorkspaceImpl implements Workspace {
|
||||
|
||||
private _bindDocMetaEvents() {
|
||||
this.meta.docMetaAdded.on(docId => {
|
||||
const doc = new BlockCollection({
|
||||
const doc = new DocImpl({
|
||||
id: docId,
|
||||
collection: this,
|
||||
doc: this.doc,
|
||||
awarenessStore: this.awarenessStore,
|
||||
idGenerator: this.idGenerator,
|
||||
});
|
||||
this.blockCollections.set(doc.id, doc);
|
||||
});
|
||||
@@ -185,8 +186,8 @@ export class WorkspaceImpl implements Workspace {
|
||||
this.awarenessSync.disconnect();
|
||||
}
|
||||
|
||||
getBlockCollection(docId: string): BlockCollection | null {
|
||||
const space = this.docs.get(docId) as BlockCollection | undefined;
|
||||
getBlockCollection(docId: string): Doc | null {
|
||||
const space = this.docs.get(docId) as Doc | undefined;
|
||||
return space ?? null;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user