feat(editor): add block meta service (#10561)

This commit is contained in:
Saul-Mirone
2025-03-03 06:13:06 +00:00
parent 3711e13e0e
commit a587abca85
6 changed files with 142 additions and 20 deletions

View File

@@ -30,3 +30,10 @@ export type Connectable = Exclude<
GfxModel,
ConnectorElementModel | BrushElementModel | GroupElementModel
>;
export type BlockMeta = {
'meta:createdAt'?: number;
'meta:createdBy'?: string;
'meta:updatedAt'?: number;
'meta:updatedBy'?: string;
};

View File

@@ -0,0 +1,112 @@
import type { BlockMeta } from '@blocksuite/affine-model';
import { type BlockModel, type Store, StoreExtension } from '@blocksuite/store';
import { FeatureFlagService } from './feature-flag-service';
import { UserProvider } from './user-service';
/**
* The service is used to add following info to the block.
* - createdAt: The time when the block is created.
* - createdBy: The user who created the block.
* - updatedAt: The time when the block is updated.
* - updatedBy: The user who updated the block.
*/
export class BlockMetaService extends StoreExtension {
static override key = 'affine-block-meta-service';
get isBlockMetaEnabled() {
return (
this.store.get(FeatureFlagService).getFlag('enable_block_meta') === true
);
}
constructor(store: Store) {
super(store);
if (!this.isBlockMetaEnabled) return;
this.store.slots.blockUpdated.on(({ type, id }) => {
if (!this.isBlockMetaEnabled) return;
const model = this.store.getBlock(id)?.model;
if (!model) return;
if (type === 'add') {
return this._onBlockCreated(model);
}
if (type === 'update') {
return this._onBlockUpdated(model);
}
});
}
private readonly _onBlockCreated = (model: BlockModel<BlockMeta>): void => {
if (!isBlockMetaSupported(model)) {
return;
}
const currentUser = this._getCurrentUser();
if (!currentUser) return;
const now = getNow();
this.store.withoutTransact(() => {
const isFlatModel = model.schema.model.isFlatData;
if (!isFlatModel) {
model['meta:createdAt'] = now;
model['meta:createdBy'] = currentUser.id;
return;
}
model.props['meta:createdAt'] = now;
model.props['meta:createdBy'] = currentUser.id;
});
};
private readonly _onBlockUpdated = (model: BlockModel<BlockMeta>): void => {
if (!isBlockMetaSupported(model)) {
return;
}
const currentUser = this._getCurrentUser();
if (!currentUser) return;
const now = getNow();
this.store.withoutTransact(() => {
const isFlatModel = model.schema.model.isFlatData;
if (!isFlatModel) {
model['meta:updatedAt'] = now;
model['meta:updatedBy'] = currentUser.id;
if (!model['meta:createdAt']) {
model['meta:createdAt'] = now;
model['meta:createdBy'] = currentUser.id;
}
return;
}
model.props['meta:updatedAt'] = now;
model.props['meta:updatedBy'] = currentUser.id;
if (!model.props['meta:createdAt']) {
model.props['meta:createdAt'] = now;
model.props['meta:createdBy'] = currentUser.id;
}
});
};
private readonly _getCurrentUser = () => {
return this.store.getOptional(UserProvider)?.getCurrentUser();
};
}
function isBlockMetaSupported(model: BlockModel) {
return [
'meta:createdAt',
'meta:createdBy',
'meta:updatedAt',
'meta:updatedBy',
].every(key => model.keys.includes(key));
}
function getNow() {
return Date.now();
}

View File

@@ -1,3 +1,4 @@
export * from './block-meta-service';
export * from './doc-display-meta-service';
export * from './doc-mode-service';
export * from './drag-handle-config';

View File

@@ -32,6 +32,7 @@ import {
ImageSelectionExtension,
} from '@blocksuite/affine-shared/selection';
import {
BlockMetaService,
FeatureFlagService,
FileSizeLimitService,
LinkPreviewerService,
@@ -87,15 +88,15 @@ export const StoreExtensions: ExtensionType[] = [
DatabaseSelectionExtension,
TableSelectionExtension,
FeatureFlagService,
LinkPreviewerService,
FileSizeLimitService,
ImageStoreSpec,
HtmlAdapterExtension,
MarkdownAdapterExtension,
NotionHtmlAdapterExtension,
PlainTextAdapterExtension,
AdapterFactoryExtensions,
FeatureFlagService,
LinkPreviewerService,
FileSizeLimitService,
ImageStoreSpec,
BlockMetaService,
].flat();

View File

@@ -11,6 +11,8 @@ export const StoreExtensionIdentifier =
export const storeExtensionSymbol = Symbol('StoreExtension');
export class StoreExtension extends Extension {
static readonly key: string;
constructor(readonly store: Store) {
super();
}
@@ -30,8 +32,6 @@ export class StoreExtension extends Extension {
provider.get(this)
);
}
static readonly key: string;
}
export function isStoreExtensionConstructor(

View File

@@ -6,6 +6,7 @@ import { computed, signal } from '@preact/signals-core';
import type { ExtensionType } from '../../extension/extension.js';
import {
BlockSchemaIdentifier,
StoreExtensionIdentifier,
StoreSelectionExtension,
} from '../../extension/index.js';
import { Schema } from '../../schema/index.js';
@@ -313,6 +314,17 @@ export class Store {
}
constructor({ doc, readonly, query, provider, extensions }: StoreOptions) {
this._doc = doc;
this.slots = {
ready: new Slot(),
rootAdded: new Slot(),
rootDeleted: new Slot(),
blockUpdated: new Slot(),
historyUpdated: this._doc.slots.historyUpdated,
yBlockUpdated: this._doc.slots.yBlockUpdated,
};
this._schema = new Schema();
const container = new Container();
container.addImpl(StoreIdentifier, () => this);
@@ -327,18 +339,6 @@ export class Store {
});
this._provider = container.provider(undefined, provider);
this._doc = doc;
this.slots = {
ready: new Slot(),
rootAdded: new Slot(),
rootDeleted: new Slot(),
blockUpdated: new Slot(),
historyUpdated: this._doc.slots.historyUpdated,
yBlockUpdated: this._doc.slots.yBlockUpdated,
};
this._schema = new Schema();
this._provider.getAll(BlockSchemaIdentifier).forEach(schema => {
this._schema.register([schema]);
});
@@ -698,6 +698,7 @@ export class Store {
load(initFn?: () => void) {
this._doc.load(initFn);
this._provider.getAll(StoreExtensionIdentifier);
this.slots.ready.emit();
return this;
}