snapshot of https://github.com/toeverything/AFFiNE.git (canary, 2026-09-16; blobs > 131072B omitted)
Some checks failed
Build & Test / Analyze (javascript, affine) (push) Failing after 4s
Build & Test / Analyze (typescript, affine) (push) Failing after 3s
Build & Test / Rust test filter (push) Failing after 4s
Build & Test / Check yarn binary (push) Failing after 3s
Build & Test / E2E BlockSuite Test (3) (push) Failing after 3s
Build & Test / Analyze (javascript, blocksuite) (push) Failing after 3s
Build & Test / Analyze (typescript, blocksuite) (push) Failing after 3s
Build & Test / Mobile native build filter (push) Failing after 3s
Build & Test / E2E BlockSuite Test (1) (push) Failing after 4s
Build & Test / E2E BlockSuite Test (2) (push) Failing after 4s
Build & Test / E2E Mobile Test (1) (push) Failing after 3s
Build & Test / Unit Test (1) (push) Has been skipped
Build & Test / Unit Test (2) (push) Has been skipped
Build & Test / Unit Test (3) (push) Has been skipped
Build & Test / Unit Test (4) (push) Has been skipped
Build & Test / Unit Test (5) (push) Has been skipped
Build & Test / Native Unit Test (push) Has been skipped
Build & Test / E2E BlockSuite Test (4) (push) Failing after 3s
Build & Test / E2E BlockSuite Test (5) (push) Failing after 3s
Build & Test / E2E BlockSuite Cross Browser Test (push) Failing after 3s
Build & Test / E2E Mobile Test (2) (push) Failing after 3s
Build & Test / Build AFFiNE native (x86_64-unknown-linux-gnu) (push) Failing after 3s
Build & Test / Build Server native (push) Failing after 3s
Build & Test / Check Git Status (push) Has been skipped
Build & Test / Server Test (0, 4) (push) Has been skipped
Build & Test / Server Test (1, 4) (push) Has been skipped
Build & Test / Server Test (2, 4) (push) Has been skipped
Build & Test / Server Test (3, 4) (push) Has been skipped
Build & Test / Server Test with Elasticsearch (push) Has been skipped
Build & Test / Build @affine/electron renderer (push) Failing after 3s
Build & Test / Copilot test filter (push) Failing after 4s
Build & Test / Lint Rust (push) Has been skipped
Build & Test / Server Test with Embedded Search (push) Has been skipped
Build & Test / Server Test with Manticore Search (push) Has been skipped
Build & Test / Server E2E Test (push) Has been skipped
Build & Test / Cloud Desktop E2E Test (push) Has been skipped
Build & Test / Cloud E2E Test 1/2 (push) Has been skipped
Build & Test / Cloud E2E Test 2/2 (push) Has been skipped
Build & Test / Build Android app (push) Has been skipped
Build & Test / Test Android app (push) Has been skipped
Build & Test / Run native tests (push) Has been skipped
Build & Test / Server Copilot Api Test (push) Has been skipped
Build & Test / Frontend Copilot E2E Test (1, 5) (push) Has been skipped
Build & Test / Frontend Copilot E2E Test (2, 5) (push) Has been skipped
Build & Test / Frontend Copilot E2E Test (3, 5) (push) Has been skipped
Build & Test / Frontend Copilot E2E Test (4, 5) (push) Has been skipped
Build & Test / Frontend Copilot E2E Test (5, 5) (push) Has been skipped
Release / Canary Gate (push) Has been skipped
Build & Test / Build AFFiNE native (aarch64-apple-darwin) (push) Has been cancelled
Build & Test / Lint (push) Has been cancelled
Build & Test / Typecheck (push) Has been cancelled
Build & Test / Build AFFiNE native (x86_64-apple-darwin) (push) Has been cancelled
Build & Test / Desktop Test (ubuntu-latest, linux, x64, x86_64-unknown-linux-gnu, true) (push) Has been cancelled
Build & Test / Desktop Test (windows-latest, windows, x64, x86_64-pc-windows-msvc, true) (push) Has been cancelled
Build & Test / E2E Test (1) (push) Has been cancelled
Build & Test / E2E Test (2) (push) Has been cancelled
Build & Test / E2E Test (3) (push) Has been cancelled
Build & Test / E2E Test (4) (push) Has been cancelled
Build & Test / E2E Test (5) (push) Has been cancelled
Build & Test / Build AFFiNE native (aarch64-pc-windows-msvc) (push) Has been cancelled
Build & Test / Build AFFiNE native (x86_64-pc-windows-msvc) (push) Has been cancelled
Build & Test / Desktop Test (macos-latest, macos, arm64, aarch64-apple-darwin, true) (push) Has been cancelled
Build & Test / Desktop Test (macos-latest, macos, x64, x86_64-apple-darwin, false) (push) Has been cancelled
Build & Test / 3, 2, 1 Launch (push) Has been cancelled
Build & Test / Build iOS app (push) Has been cancelled
Copilot Test Automatically / Setup Test (push) Failing after 1s
Release / Prepare (push) Failing after 2s
Release / Release Cloud (push) Has been skipped
Release / Release Desktop (push) Has been skipped
Release / Release Docker Image (push) Has been skipped
Release / Release Mobile (push) Has been skipped
Some checks failed
Build & Test / Analyze (javascript, affine) (push) Failing after 4s
Build & Test / Analyze (typescript, affine) (push) Failing after 3s
Build & Test / Rust test filter (push) Failing after 4s
Build & Test / Check yarn binary (push) Failing after 3s
Build & Test / E2E BlockSuite Test (3) (push) Failing after 3s
Build & Test / Analyze (javascript, blocksuite) (push) Failing after 3s
Build & Test / Analyze (typescript, blocksuite) (push) Failing after 3s
Build & Test / Mobile native build filter (push) Failing after 3s
Build & Test / E2E BlockSuite Test (1) (push) Failing after 4s
Build & Test / E2E BlockSuite Test (2) (push) Failing after 4s
Build & Test / E2E Mobile Test (1) (push) Failing after 3s
Build & Test / Unit Test (1) (push) Has been skipped
Build & Test / Unit Test (2) (push) Has been skipped
Build & Test / Unit Test (3) (push) Has been skipped
Build & Test / Unit Test (4) (push) Has been skipped
Build & Test / Unit Test (5) (push) Has been skipped
Build & Test / Native Unit Test (push) Has been skipped
Build & Test / E2E BlockSuite Test (4) (push) Failing after 3s
Build & Test / E2E BlockSuite Test (5) (push) Failing after 3s
Build & Test / E2E BlockSuite Cross Browser Test (push) Failing after 3s
Build & Test / E2E Mobile Test (2) (push) Failing after 3s
Build & Test / Build AFFiNE native (x86_64-unknown-linux-gnu) (push) Failing after 3s
Build & Test / Build Server native (push) Failing after 3s
Build & Test / Check Git Status (push) Has been skipped
Build & Test / Server Test (0, 4) (push) Has been skipped
Build & Test / Server Test (1, 4) (push) Has been skipped
Build & Test / Server Test (2, 4) (push) Has been skipped
Build & Test / Server Test (3, 4) (push) Has been skipped
Build & Test / Server Test with Elasticsearch (push) Has been skipped
Build & Test / Build @affine/electron renderer (push) Failing after 3s
Build & Test / Copilot test filter (push) Failing after 4s
Build & Test / Lint Rust (push) Has been skipped
Build & Test / Server Test with Embedded Search (push) Has been skipped
Build & Test / Server Test with Manticore Search (push) Has been skipped
Build & Test / Server E2E Test (push) Has been skipped
Build & Test / Cloud Desktop E2E Test (push) Has been skipped
Build & Test / Cloud E2E Test 1/2 (push) Has been skipped
Build & Test / Cloud E2E Test 2/2 (push) Has been skipped
Build & Test / Build Android app (push) Has been skipped
Build & Test / Test Android app (push) Has been skipped
Build & Test / Run native tests (push) Has been skipped
Build & Test / Server Copilot Api Test (push) Has been skipped
Build & Test / Frontend Copilot E2E Test (1, 5) (push) Has been skipped
Build & Test / Frontend Copilot E2E Test (2, 5) (push) Has been skipped
Build & Test / Frontend Copilot E2E Test (3, 5) (push) Has been skipped
Build & Test / Frontend Copilot E2E Test (4, 5) (push) Has been skipped
Build & Test / Frontend Copilot E2E Test (5, 5) (push) Has been skipped
Release / Canary Gate (push) Has been skipped
Build & Test / Build AFFiNE native (aarch64-apple-darwin) (push) Has been cancelled
Build & Test / Lint (push) Has been cancelled
Build & Test / Typecheck (push) Has been cancelled
Build & Test / Build AFFiNE native (x86_64-apple-darwin) (push) Has been cancelled
Build & Test / Desktop Test (ubuntu-latest, linux, x64, x86_64-unknown-linux-gnu, true) (push) Has been cancelled
Build & Test / Desktop Test (windows-latest, windows, x64, x86_64-pc-windows-msvc, true) (push) Has been cancelled
Build & Test / E2E Test (1) (push) Has been cancelled
Build & Test / E2E Test (2) (push) Has been cancelled
Build & Test / E2E Test (3) (push) Has been cancelled
Build & Test / E2E Test (4) (push) Has been cancelled
Build & Test / E2E Test (5) (push) Has been cancelled
Build & Test / Build AFFiNE native (aarch64-pc-windows-msvc) (push) Has been cancelled
Build & Test / Build AFFiNE native (x86_64-pc-windows-msvc) (push) Has been cancelled
Build & Test / Desktop Test (macos-latest, macos, arm64, aarch64-apple-darwin, true) (push) Has been cancelled
Build & Test / Desktop Test (macos-latest, macos, x64, x86_64-apple-darwin, false) (push) Has been cancelled
Build & Test / 3, 2, 1 Launch (push) Has been cancelled
Build & Test / Build iOS app (push) Has been cancelled
Copilot Test Automatically / Setup Test (push) Failing after 1s
Release / Prepare (push) Failing after 2s
Release / Release Cloud (push) Has been skipped
Release / Release Desktop (push) Has been skipped
Release / Release Docker Image (push) Has been skipped
Release / Release Mobile (push) Has been skipped
This commit is contained in:
134
blocksuite/framework/store/src/extension/extension.ts
Normal file
134
blocksuite/framework/store/src/extension/extension.ts
Normal file
@@ -0,0 +1,134 @@
|
||||
import type { Container } from '@blocksuite/global/di';
|
||||
|
||||
/**
|
||||
* # Understanding Extensions
|
||||
*
|
||||
* Extensions provide a way to extend the functionality of a system using dependency injection.
|
||||
* They allow you to register services, implementations, and factories in the DI container,
|
||||
* which can then be retrieved and used by different parts of the application.
|
||||
*
|
||||
* Extensions are particularly useful for:
|
||||
* - Registering different implementations for different types
|
||||
* - Creating pluggable architecture where components can be added or removed
|
||||
* - Managing dependencies between different parts of the application
|
||||
*
|
||||
* # Usage Example: Fruit Processing System
|
||||
*
|
||||
* Let's consider a fruit processing system where different types of fruits need
|
||||
* different processing methods. We'll show how to implement this using extensions.
|
||||
*
|
||||
* ## Step 1: Define the interfaces
|
||||
*
|
||||
* ```ts
|
||||
* interface FruitProcessor {
|
||||
* process(fruit: Fruit): void;
|
||||
* }
|
||||
*
|
||||
* interface Fruit {
|
||||
* type: string;
|
||||
* // other properties
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* ## Step 2: Create a service identifier
|
||||
*
|
||||
* ```ts
|
||||
* import { createIdentifier } from '@blocksuite/global/di';
|
||||
*
|
||||
* const FruitProcessorProvider = createIdentifier<FruitProcessor>('fruit-processor-provider');
|
||||
* ```
|
||||
*
|
||||
* ## Step 3: Create implementations
|
||||
*
|
||||
* ```ts
|
||||
* class AppleProcessor implements FruitProcessor {
|
||||
* process(fruit: Fruit): void {
|
||||
* console.log('Slicing apple');
|
||||
* // Apple-specific processing
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* class BananaProcessor implements FruitProcessor {
|
||||
* process(fruit: Fruit): void {
|
||||
* console.log('Peeling banana');
|
||||
* // Banana-specific processing
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* ## Step 4: Create an extension factory
|
||||
*
|
||||
* ```ts
|
||||
* const FruitProcessorExtension = (
|
||||
* fruitType: string,
|
||||
* implementation: new () => FruitProcessor
|
||||
* ): ExtensionType => {
|
||||
* return {
|
||||
* setup: di => {
|
||||
* di.addImpl(FruitProcessorProvider(fruitType), implementation);
|
||||
* }
|
||||
* };
|
||||
* };
|
||||
* ```
|
||||
*
|
||||
* ## Step 5: Create concrete extensions
|
||||
*
|
||||
* ```ts
|
||||
* export const AppleProcessorExtension = FruitProcessorExtension('apple', AppleProcessor);
|
||||
* export const BananaProcessorExtension = FruitProcessorExtension('banana', BananaProcessor);
|
||||
* ```
|
||||
*
|
||||
* ## Step 6: Use the extensions
|
||||
*
|
||||
* ```ts
|
||||
* import { Container } from '@blocksuite/global/di';
|
||||
*
|
||||
* class FruitProcessingSystem {
|
||||
* provider: ServiceProvider;
|
||||
*
|
||||
* constructor(extensions: ExtensionType[]) {
|
||||
* const container = new Container();
|
||||
*
|
||||
* // Set up all extensions
|
||||
* extensions.forEach(ext => ext.setup(container));
|
||||
*
|
||||
* // Create a provider from the container
|
||||
* this.provider = container.provider();
|
||||
* }
|
||||
*
|
||||
* processFruit(fruit: Fruit) {
|
||||
* // Get the appropriate processor based on fruit type
|
||||
* const processor = this.provider.get(FruitProcessorProvider(fruit.type));
|
||||
*
|
||||
* // Process the fruit
|
||||
* processor.process(fruit);
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* // Initialize the system with extensions
|
||||
* const system = new FruitProcessingSystem([
|
||||
* AppleProcessorExtension,
|
||||
* BananaProcessorExtension
|
||||
* ]);
|
||||
*
|
||||
* // Use the system
|
||||
* system.processFruit({ type: 'apple' }); // Output: Slicing apple
|
||||
* system.processFruit({ type: 'banana' }); // Output: Peeling banana
|
||||
* ```
|
||||
*
|
||||
* Note: We deliberately used a non-block specific example here. In BlockSuite, the extension
|
||||
* pattern can be applied to any entity that can be configured by third parties, not just blocks.
|
||||
* This includes different tools in the whiteboard, different column types in database blocks,
|
||||
* and many other extensible components. The pattern remains the same regardless of what you're extending.
|
||||
*
|
||||
* @category Extension
|
||||
*/
|
||||
export abstract class Extension {
|
||||
static setup(_di: Container): void {
|
||||
// do nothing
|
||||
}
|
||||
}
|
||||
|
||||
export interface ExtensionType {
|
||||
setup(di: Container): void;
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
import { signal } from '@preact/signals-core';
|
||||
import { Subject } from 'rxjs';
|
||||
import * as Y from 'yjs';
|
||||
|
||||
import type { Store } from '../../model';
|
||||
import { StoreExtension } from '../store-extension';
|
||||
|
||||
export class HistoryExtension extends StoreExtension {
|
||||
static override readonly key = 'history';
|
||||
|
||||
private readonly _history!: Y.UndoManager;
|
||||
|
||||
private readonly _canRedo = signal(false);
|
||||
|
||||
private readonly _canUndo = signal(false);
|
||||
|
||||
readonly onUpdated = new Subject<void>();
|
||||
|
||||
constructor(store: Store) {
|
||||
super(store);
|
||||
|
||||
this._history = new Y.UndoManager([this.store.doc.yBlocks], {
|
||||
trackedOrigins: new Set([this.store.doc.spaceDoc.clientID]),
|
||||
});
|
||||
}
|
||||
|
||||
private readonly _updateCanUndoRedoSignals = () => {
|
||||
const canRedo = this._history.canRedo();
|
||||
const canUndo = this._history.canUndo();
|
||||
if (this._canRedo.peek() !== canRedo) {
|
||||
this._canRedo.value = canRedo;
|
||||
}
|
||||
if (this._canUndo.peek() !== canUndo) {
|
||||
this._canUndo.value = canUndo;
|
||||
}
|
||||
};
|
||||
|
||||
get canRedo() {
|
||||
return this._canRedo.peek();
|
||||
}
|
||||
|
||||
get canUndo() {
|
||||
return this._canUndo.peek();
|
||||
}
|
||||
|
||||
get canRedo$() {
|
||||
return this._canRedo;
|
||||
}
|
||||
|
||||
get canUndo$() {
|
||||
return this._canUndo;
|
||||
}
|
||||
|
||||
get undoManager() {
|
||||
return this._history;
|
||||
}
|
||||
|
||||
override loaded() {
|
||||
this._updateCanUndoRedoSignals();
|
||||
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);
|
||||
}
|
||||
|
||||
private readonly _historyObserver = () => {
|
||||
this._updateCanUndoRedoSignals();
|
||||
this.onUpdated.next();
|
||||
};
|
||||
|
||||
override disposed() {
|
||||
super.disposed();
|
||||
this._history.off('stack-cleared', this._historyObserver);
|
||||
this._history.off('stack-item-added', this._historyObserver);
|
||||
this._history.off('stack-item-popped', this._historyObserver);
|
||||
this._history.off('stack-item-updated', this._historyObserver);
|
||||
this.onUpdated.complete();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './history-extension';
|
||||
6
blocksuite/framework/store/src/extension/index.ts
Normal file
6
blocksuite/framework/store/src/extension/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export * from './extension';
|
||||
export * from './history';
|
||||
export * from './schema';
|
||||
export * from './selection';
|
||||
export * from './store-extension';
|
||||
export * from './workspace';
|
||||
20
blocksuite/framework/store/src/extension/schema.ts
Normal file
20
blocksuite/framework/store/src/extension/schema.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { createIdentifier } from '@blocksuite/global/di';
|
||||
|
||||
import type { BlockSchemaType } from '../model/block/zod';
|
||||
import type { ExtensionType } from './extension';
|
||||
|
||||
export const BlockSchemaIdentifier =
|
||||
createIdentifier<BlockSchemaType>('BlockSchema');
|
||||
|
||||
export function BlockSchemaExtension(
|
||||
blockSchema: BlockSchemaType
|
||||
): ExtensionType {
|
||||
return {
|
||||
setup: di => {
|
||||
di.addImpl(
|
||||
BlockSchemaIdentifier(blockSchema.model.flavour),
|
||||
() => blockSchema
|
||||
);
|
||||
},
|
||||
};
|
||||
}
|
||||
46
blocksuite/framework/store/src/extension/selection/base.ts
Normal file
46
blocksuite/framework/store/src/extension/selection/base.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { BlockSuiteError, ErrorCode } from '@blocksuite/global/exceptions';
|
||||
|
||||
import type { SelectionConstructor } from './types';
|
||||
|
||||
export type BaseSelectionOptions = {
|
||||
blockId: string;
|
||||
};
|
||||
|
||||
export abstract class BaseSelection {
|
||||
static readonly group: string;
|
||||
|
||||
static readonly type: string;
|
||||
|
||||
static readonly recoverable: boolean = false;
|
||||
|
||||
readonly blockId: string;
|
||||
|
||||
get group(): string {
|
||||
return (this.constructor as SelectionConstructor).group;
|
||||
}
|
||||
|
||||
get type(): string {
|
||||
return (this.constructor as SelectionConstructor).type as string;
|
||||
}
|
||||
|
||||
constructor({ blockId }: BaseSelectionOptions) {
|
||||
this.blockId = blockId;
|
||||
}
|
||||
|
||||
static fromJSON(_: Record<string, unknown>): BaseSelection {
|
||||
throw new BlockSuiteError(
|
||||
ErrorCode.SelectionError,
|
||||
'You must override this method'
|
||||
);
|
||||
}
|
||||
|
||||
abstract equals(other: BaseSelection): boolean;
|
||||
|
||||
is<T extends SelectionConstructor>(
|
||||
type: T
|
||||
): this is T extends SelectionConstructor<infer U> ? U : never {
|
||||
return this.type === type.type;
|
||||
}
|
||||
|
||||
abstract toJSON(): Record<string, unknown>;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { createIdentifier } from '@blocksuite/global/di';
|
||||
|
||||
import type { ExtensionType } from '../extension';
|
||||
import type { SelectionConstructor } from './types';
|
||||
|
||||
export const SelectionIdentifier =
|
||||
createIdentifier<SelectionConstructor>('Selection');
|
||||
|
||||
export function SelectionExtension(
|
||||
selectionCtor: SelectionConstructor
|
||||
): ExtensionType {
|
||||
return {
|
||||
setup: di => {
|
||||
di.addImpl(SelectionIdentifier(selectionCtor.type), () => selectionCtor);
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export * from './base';
|
||||
export * from './identifier';
|
||||
export * from './selection-extension';
|
||||
export * from './types';
|
||||
@@ -0,0 +1,189 @@
|
||||
import { BlockSuiteError, ErrorCode } from '@blocksuite/global/exceptions';
|
||||
import { computed, signal } from '@preact/signals-core';
|
||||
import { Subject } from 'rxjs';
|
||||
|
||||
import { nanoid } from '../../utils/id-generator';
|
||||
import type { StackItem } from '../../yjs';
|
||||
import { StoreExtension } from '../store-extension';
|
||||
import type { BaseSelection } from './base';
|
||||
import { SelectionIdentifier } from './identifier';
|
||||
import type { SelectionConstructor } from './types';
|
||||
|
||||
export class StoreSelectionExtension extends StoreExtension {
|
||||
static override readonly key = 'selection';
|
||||
|
||||
private readonly _id = `${this.store.id}:${nanoid()}`;
|
||||
private _selectionConstructors: Record<string, SelectionConstructor> = {};
|
||||
private readonly _selections = signal<BaseSelection[]>([]);
|
||||
private readonly _remoteSelections = signal<Map<number, BaseSelection[]>>(
|
||||
new Map()
|
||||
);
|
||||
|
||||
private readonly _itemAdded = (event: { stackItem: StackItem }) => {
|
||||
event.stackItem.meta.set('selection-state', this._selections.value);
|
||||
};
|
||||
|
||||
private readonly _itemPopped = (event: { stackItem: StackItem }) => {
|
||||
const selection = event.stackItem.meta.get('selection-state');
|
||||
if (selection) {
|
||||
this.set(selection as BaseSelection[]);
|
||||
}
|
||||
};
|
||||
|
||||
private readonly _jsonToSelection = (json: Record<string, unknown>) => {
|
||||
const ctor = this._selectionConstructors[json.type as string];
|
||||
if (!ctor) {
|
||||
throw new BlockSuiteError(
|
||||
ErrorCode.SelectionError,
|
||||
`Unknown selection type: ${json.type}`
|
||||
);
|
||||
}
|
||||
return ctor.fromJSON(json);
|
||||
};
|
||||
|
||||
slots = {
|
||||
changed: new Subject<BaseSelection[]>(),
|
||||
remoteChanged: new Subject<Map<number, BaseSelection[]>>(),
|
||||
};
|
||||
|
||||
override loaded() {
|
||||
this.store.provider.getAll(SelectionIdentifier).forEach(ctor => {
|
||||
[ctor].flat().forEach(ctor => {
|
||||
this._selectionConstructors[ctor.type] = ctor;
|
||||
});
|
||||
});
|
||||
|
||||
this.store.awarenessStore.awareness.on(
|
||||
'change',
|
||||
(change: { updated: number[]; added: number[]; removed: number[] }) => {
|
||||
const all = change.updated.concat(change.added).concat(change.removed);
|
||||
const localClientID = this.store.awarenessStore.awareness.clientID;
|
||||
const exceptLocal = all.filter(id => id !== localClientID);
|
||||
|
||||
// Only consider remote selections from other clients
|
||||
if (exceptLocal.length > 0) {
|
||||
const map = new Map<number, BaseSelection[]>();
|
||||
this.store.awarenessStore.getStates().forEach((state, id) => {
|
||||
if (id === this.store.awarenessStore.awareness.clientID) return;
|
||||
// selection id starts with the same block collection id from others clients would be considered as remote selections
|
||||
const selection = Object.entries(state.selectionV2)
|
||||
.filter(([key]) => key.startsWith(this.store.id))
|
||||
.flatMap(([_, selection]) => selection);
|
||||
|
||||
const selections = selection
|
||||
.map(json => {
|
||||
try {
|
||||
return this._jsonToSelection(json);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
'Parse remote selection failed:',
|
||||
id,
|
||||
json,
|
||||
error
|
||||
);
|
||||
return null;
|
||||
}
|
||||
})
|
||||
.filter((sel): sel is BaseSelection => !!sel);
|
||||
|
||||
map.set(id, selections);
|
||||
});
|
||||
this._remoteSelections.value = map;
|
||||
this.slots.remoteChanged.next(map);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
this.store.history.undoManager.on('stack-item-added', this._itemAdded);
|
||||
this.store.history.undoManager.on('stack-item-popped', this._itemPopped);
|
||||
}
|
||||
|
||||
override disposed() {
|
||||
super.disposed();
|
||||
this.store.history.undoManager.off('stack-item-added', this._itemAdded);
|
||||
this.store.history.undoManager.off('stack-item-popped', this._itemPopped);
|
||||
}
|
||||
|
||||
get value() {
|
||||
return this._selections.value;
|
||||
}
|
||||
|
||||
get remoteSelections() {
|
||||
return this._remoteSelections.value;
|
||||
}
|
||||
|
||||
clear(types?: string[]) {
|
||||
if (types) {
|
||||
const values = this.value.filter(
|
||||
selection => !types.includes(selection.type)
|
||||
);
|
||||
this.set(values);
|
||||
} else {
|
||||
this.set([]);
|
||||
}
|
||||
}
|
||||
|
||||
create<T extends SelectionConstructor>(
|
||||
Type: T,
|
||||
...args: ConstructorParameters<T>
|
||||
): InstanceType<T> {
|
||||
return new Type(...args) as InstanceType<T>;
|
||||
}
|
||||
|
||||
getGroup(group: string) {
|
||||
return this.value.filter(s => s.group === group);
|
||||
}
|
||||
|
||||
filter<T extends SelectionConstructor>(type: T) {
|
||||
return this.filter$(type).value;
|
||||
}
|
||||
|
||||
filter$<T extends SelectionConstructor>(type: T) {
|
||||
return computed(() =>
|
||||
this.value.filter((sel): sel is InstanceType<T> => sel.is(type))
|
||||
);
|
||||
}
|
||||
|
||||
find<T extends SelectionConstructor>(type: T) {
|
||||
return this.find$(type).value;
|
||||
}
|
||||
|
||||
find$<T extends SelectionConstructor>(type: T) {
|
||||
return computed(() =>
|
||||
this.value.find((sel): sel is InstanceType<T> => sel.is(type))
|
||||
);
|
||||
}
|
||||
|
||||
set(selections: BaseSelection[]) {
|
||||
this.store.awarenessStore.setLocalSelection(
|
||||
this._id,
|
||||
selections.map(s => s.toJSON())
|
||||
);
|
||||
this._selections.value = selections;
|
||||
this.slots.changed.next(selections);
|
||||
}
|
||||
|
||||
setGroup(group: string, selections: BaseSelection[]) {
|
||||
const current = this.value.filter(s => s.group !== group);
|
||||
this.set([...current, ...selections]);
|
||||
}
|
||||
|
||||
// This method is used to clear **current editor's remote selections**
|
||||
// When the editor is not active, the remote selections should be cleared
|
||||
// So other editors won't see the remote selections from this editor
|
||||
clearRemote() {
|
||||
this.store.awarenessStore.setLocalSelection(this._id, []);
|
||||
}
|
||||
|
||||
update(fn: (currentSelections: BaseSelection[]) => BaseSelection[]) {
|
||||
const selections = fn(this.value);
|
||||
this.set(selections);
|
||||
}
|
||||
|
||||
fromJSON(json: Record<string, unknown>[]) {
|
||||
const selections = json.map(json => {
|
||||
return this._jsonToSelection(json);
|
||||
});
|
||||
return this.set(selections);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import type { BaseSelection } from './base';
|
||||
|
||||
export interface SelectionConstructor<T extends BaseSelection = BaseSelection> {
|
||||
type: string;
|
||||
group: string;
|
||||
|
||||
new (...args: any[]): T;
|
||||
fromJSON(json: Record<string, unknown>): T;
|
||||
}
|
||||
61
blocksuite/framework/store/src/extension/store-extension.ts
Normal file
61
blocksuite/framework/store/src/extension/store-extension.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import { type Container, createIdentifier } from '@blocksuite/global/di';
|
||||
import { BlockSuiteError, ErrorCode } from '@blocksuite/global/exceptions';
|
||||
|
||||
import type { Store } from '../model/store';
|
||||
import { StoreIdentifier } from '../model/store/identifier';
|
||||
import { Extension } from './extension';
|
||||
|
||||
export const StoreExtensionIdentifier =
|
||||
createIdentifier<StoreExtension>('StoreExtension');
|
||||
|
||||
export const storeExtensionSymbol = Symbol('StoreExtension');
|
||||
|
||||
/**
|
||||
* Store extensions are used to extend the store.
|
||||
* They should be registered to the store. And they should be able to run in a none-dom environment.
|
||||
*
|
||||
* @category Extension
|
||||
*/
|
||||
export class StoreExtension extends Extension {
|
||||
/**
|
||||
* The key of the store extension.
|
||||
* **You must override this property with a unique string.**
|
||||
*/
|
||||
static readonly key: string;
|
||||
|
||||
constructor(readonly store: Store) {
|
||||
super();
|
||||
}
|
||||
|
||||
/**
|
||||
* Lifecycle hook when the yjs document is loaded.
|
||||
*/
|
||||
loaded() {}
|
||||
|
||||
/**
|
||||
* Lifecycle hook when the yjs document is disposed.
|
||||
*/
|
||||
disposed() {}
|
||||
|
||||
static readonly [storeExtensionSymbol] = true;
|
||||
|
||||
static override setup(di: Container) {
|
||||
if (!this.key) {
|
||||
throw new BlockSuiteError(
|
||||
ErrorCode.ValueNotExists,
|
||||
'Key is not defined in the StoreExtension'
|
||||
);
|
||||
}
|
||||
|
||||
di.add(this, [StoreIdentifier]);
|
||||
di.addImpl(StoreExtensionIdentifier(this.key), provider =>
|
||||
provider.get(this)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function isStoreExtensionConstructor(
|
||||
extension: object
|
||||
): extension is typeof StoreExtension {
|
||||
return storeExtensionSymbol in extension;
|
||||
}
|
||||
38
blocksuite/framework/store/src/extension/workspace/doc.ts
Normal file
38
blocksuite/framework/store/src/extension/workspace/doc.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { createIdentifier } from '@blocksuite/global/di';
|
||||
import type * as Y from 'yjs';
|
||||
|
||||
import type { Store, StoreOptions, YBlock } from '../../model';
|
||||
import type { AwarenessStore } from '../../yjs';
|
||||
import type { Workspace } from './workspace.js';
|
||||
import type { DocMeta } from './workspace-meta.js';
|
||||
|
||||
export type GetStoreOptions = Omit<StoreOptions, 'schema' | 'doc'>;
|
||||
export type RemoveStoreOptions = Pick<
|
||||
StoreOptions,
|
||||
'query' | 'id' | 'readonly'
|
||||
>;
|
||||
|
||||
export interface Doc {
|
||||
readonly id: string;
|
||||
get meta(): DocMeta | undefined;
|
||||
|
||||
remove(): void;
|
||||
load(initFn?: () => void): void;
|
||||
get ready(): boolean;
|
||||
dispose(): void;
|
||||
|
||||
clear(): void;
|
||||
getStore(options?: GetStoreOptions): Store;
|
||||
removeStore(options: RemoveStoreOptions): void;
|
||||
|
||||
get loaded(): boolean;
|
||||
get awarenessStore(): AwarenessStore;
|
||||
|
||||
get workspace(): Workspace;
|
||||
|
||||
get rootDoc(): Y.Doc;
|
||||
get spaceDoc(): Y.Doc;
|
||||
get yBlocks(): Y.Map<YBlock>;
|
||||
}
|
||||
|
||||
export const DocIdentifier = createIdentifier<Doc>('store-doc');
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from './doc';
|
||||
export * from './workspace';
|
||||
export * from './workspace-meta';
|
||||
@@ -0,0 +1,40 @@
|
||||
import type { Subject } from 'rxjs';
|
||||
|
||||
export type Tag = {
|
||||
id: string;
|
||||
value: string;
|
||||
color: string;
|
||||
};
|
||||
export type DocsPropertiesMeta = {
|
||||
tags?: {
|
||||
options: Tag[];
|
||||
};
|
||||
};
|
||||
export interface DocMeta {
|
||||
id: string;
|
||||
title: string;
|
||||
tags: string[];
|
||||
createDate: number;
|
||||
updatedDate?: number;
|
||||
favorite?: boolean;
|
||||
trash?: boolean;
|
||||
}
|
||||
|
||||
export interface WorkspaceMeta {
|
||||
get docMetas(): DocMeta[];
|
||||
|
||||
addDocMeta(props: DocMeta, index?: number): void;
|
||||
getDocMeta(id: string): DocMeta | undefined;
|
||||
setDocMeta(id: string, props: Partial<DocMeta>): void;
|
||||
removeDocMeta(id: string): void;
|
||||
|
||||
get properties(): DocsPropertiesMeta;
|
||||
setProperties(meta: DocsPropertiesMeta): void;
|
||||
|
||||
get docs(): unknown[] | undefined;
|
||||
initialize(): void;
|
||||
|
||||
docMetaAdded: Subject<string>;
|
||||
docMetaRemoved: Subject<string>;
|
||||
docMetaUpdated: Subject<void>;
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import type { BlobEngine } from '@blocksuite/sync';
|
||||
import type { Subject } from 'rxjs';
|
||||
import type { Awareness } from 'y-protocols/awareness.js';
|
||||
import type * as Y from 'yjs';
|
||||
|
||||
import type { IdGenerator } from '../../utils/id-generator';
|
||||
import type { Doc } from './doc.js';
|
||||
import type { WorkspaceMeta } from './workspace-meta.js';
|
||||
|
||||
export interface Workspace {
|
||||
readonly id: string;
|
||||
readonly meta: WorkspaceMeta;
|
||||
readonly idGenerator: IdGenerator;
|
||||
readonly blobSync: BlobEngine;
|
||||
readonly onLoadDoc?: (doc: Y.Doc) => void;
|
||||
readonly onLoadAwareness?: (awareness: Awareness) => void;
|
||||
|
||||
get doc(): Y.Doc;
|
||||
get docs(): Map<string, Doc>;
|
||||
|
||||
slots: {
|
||||
docListUpdated: Subject<void>;
|
||||
};
|
||||
|
||||
createDoc(docId?: string): Doc;
|
||||
getDoc(docId: string): Doc | null;
|
||||
removeDoc(docId: string): void;
|
||||
|
||||
dispose(): void;
|
||||
}
|
||||
Reference in New Issue
Block a user