mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-09-07 01:09:54 +08:00
feat(editor): add block meta service (#10561)
This commit is contained in:
@@ -30,3 +30,10 @@ export type Connectable = Exclude<
|
|||||||
GfxModel,
|
GfxModel,
|
||||||
ConnectorElementModel | BrushElementModel | GroupElementModel
|
ConnectorElementModel | BrushElementModel | GroupElementModel
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
export type BlockMeta = {
|
||||||
|
'meta:createdAt'?: number;
|
||||||
|
'meta:createdBy'?: string;
|
||||||
|
'meta:updatedAt'?: number;
|
||||||
|
'meta:updatedBy'?: string;
|
||||||
|
};
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
export * from './block-meta-service';
|
||||||
export * from './doc-display-meta-service';
|
export * from './doc-display-meta-service';
|
||||||
export * from './doc-mode-service';
|
export * from './doc-mode-service';
|
||||||
export * from './drag-handle-config';
|
export * from './drag-handle-config';
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ import {
|
|||||||
ImageSelectionExtension,
|
ImageSelectionExtension,
|
||||||
} from '@blocksuite/affine-shared/selection';
|
} from '@blocksuite/affine-shared/selection';
|
||||||
import {
|
import {
|
||||||
|
BlockMetaService,
|
||||||
FeatureFlagService,
|
FeatureFlagService,
|
||||||
FileSizeLimitService,
|
FileSizeLimitService,
|
||||||
LinkPreviewerService,
|
LinkPreviewerService,
|
||||||
@@ -87,15 +88,15 @@ export const StoreExtensions: ExtensionType[] = [
|
|||||||
DatabaseSelectionExtension,
|
DatabaseSelectionExtension,
|
||||||
TableSelectionExtension,
|
TableSelectionExtension,
|
||||||
|
|
||||||
FeatureFlagService,
|
|
||||||
LinkPreviewerService,
|
|
||||||
FileSizeLimitService,
|
|
||||||
ImageStoreSpec,
|
|
||||||
|
|
||||||
HtmlAdapterExtension,
|
HtmlAdapterExtension,
|
||||||
MarkdownAdapterExtension,
|
MarkdownAdapterExtension,
|
||||||
NotionHtmlAdapterExtension,
|
NotionHtmlAdapterExtension,
|
||||||
PlainTextAdapterExtension,
|
PlainTextAdapterExtension,
|
||||||
|
|
||||||
AdapterFactoryExtensions,
|
AdapterFactoryExtensions,
|
||||||
|
|
||||||
|
FeatureFlagService,
|
||||||
|
LinkPreviewerService,
|
||||||
|
FileSizeLimitService,
|
||||||
|
ImageStoreSpec,
|
||||||
|
BlockMetaService,
|
||||||
].flat();
|
].flat();
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ export const StoreExtensionIdentifier =
|
|||||||
export const storeExtensionSymbol = Symbol('StoreExtension');
|
export const storeExtensionSymbol = Symbol('StoreExtension');
|
||||||
|
|
||||||
export class StoreExtension extends Extension {
|
export class StoreExtension extends Extension {
|
||||||
|
static readonly key: string;
|
||||||
|
|
||||||
constructor(readonly store: Store) {
|
constructor(readonly store: Store) {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
@@ -30,8 +32,6 @@ export class StoreExtension extends Extension {
|
|||||||
provider.get(this)
|
provider.get(this)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
static readonly key: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isStoreExtensionConstructor(
|
export function isStoreExtensionConstructor(
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { computed, signal } from '@preact/signals-core';
|
|||||||
import type { ExtensionType } from '../../extension/extension.js';
|
import type { ExtensionType } from '../../extension/extension.js';
|
||||||
import {
|
import {
|
||||||
BlockSchemaIdentifier,
|
BlockSchemaIdentifier,
|
||||||
|
StoreExtensionIdentifier,
|
||||||
StoreSelectionExtension,
|
StoreSelectionExtension,
|
||||||
} from '../../extension/index.js';
|
} from '../../extension/index.js';
|
||||||
import { Schema } from '../../schema/index.js';
|
import { Schema } from '../../schema/index.js';
|
||||||
@@ -313,6 +314,17 @@ export class Store {
|
|||||||
}
|
}
|
||||||
|
|
||||||
constructor({ doc, readonly, query, provider, extensions }: StoreOptions) {
|
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();
|
const container = new Container();
|
||||||
container.addImpl(StoreIdentifier, () => this);
|
container.addImpl(StoreIdentifier, () => this);
|
||||||
|
|
||||||
@@ -327,18 +339,6 @@ export class Store {
|
|||||||
});
|
});
|
||||||
|
|
||||||
this._provider = container.provider(undefined, provider);
|
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._provider.getAll(BlockSchemaIdentifier).forEach(schema => {
|
||||||
this._schema.register([schema]);
|
this._schema.register([schema]);
|
||||||
});
|
});
|
||||||
@@ -698,6 +698,7 @@ export class Store {
|
|||||||
|
|
||||||
load(initFn?: () => void) {
|
load(initFn?: () => void) {
|
||||||
this._doc.load(initFn);
|
this._doc.load(initFn);
|
||||||
|
this._provider.getAll(StoreExtensionIdentifier);
|
||||||
this.slots.ready.emit();
|
this.slots.ready.emit();
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user