diff --git a/blocksuite/affine/blocks/attachment/package.json b/blocksuite/affine/blocks/attachment/package.json index ebbd525c9a..ebefe8098d 100644 --- a/blocksuite/affine/blocks/attachment/package.json +++ b/blocksuite/affine/blocks/attachment/package.json @@ -13,6 +13,7 @@ "@blocksuite/affine-block-embed": "workspace:*", "@blocksuite/affine-block-surface": "workspace:*", "@blocksuite/affine-components": "workspace:*", + "@blocksuite/affine-ext-loader": "workspace:*", "@blocksuite/affine-model": "workspace:*", "@blocksuite/affine-shared": "workspace:*", "@blocksuite/affine-widget-slash-menu": "workspace:*", @@ -32,7 +33,9 @@ }, "exports": { ".": "./src/index.ts", - "./effects": "./src/effects.ts" + "./effects": "./src/effects.ts", + "./store": "./src/store.ts", + "./view": "./src/view.ts" }, "files": [ "src", diff --git a/blocksuite/affine/blocks/attachment/src/store.ts b/blocksuite/affine/blocks/attachment/src/store.ts new file mode 100644 index 0000000000..a10d3bc816 --- /dev/null +++ b/blocksuite/affine/blocks/attachment/src/store.ts @@ -0,0 +1,17 @@ +import { + type StoreExtensionContext, + StoreExtensionProvider, +} from '@blocksuite/affine-ext-loader'; +import { AttachmentBlockSchemaExtension } from '@blocksuite/affine-model'; + +import { AttachmentBlockNotionHtmlAdapterExtension } from './adapters/notion-html'; + +export default class AttachmentStoreExtension extends StoreExtensionProvider { + override name = 'affine-attachment-block'; + + override setup(context: StoreExtensionContext) { + super.setup(context); + context.register(AttachmentBlockSchemaExtension); + context.register(AttachmentBlockNotionHtmlAdapterExtension); + } +} diff --git a/blocksuite/affine/blocks/attachment/src/view.ts b/blocksuite/affine/blocks/attachment/src/view.ts new file mode 100644 index 0000000000..c6076fb7a8 --- /dev/null +++ b/blocksuite/affine/blocks/attachment/src/view.ts @@ -0,0 +1,45 @@ +import { + type ViewExtensionContext, + ViewExtensionProvider, +} from '@blocksuite/affine-ext-loader'; +import { AttachmentBlockSchema } from '@blocksuite/affine-model'; +import { SlashMenuConfigExtension } from '@blocksuite/affine-widget-slash-menu'; +import { BlockViewExtension, FlavourExtension } from '@blocksuite/std'; +import { literal } from 'lit/static-html.js'; + +import { AttachmentDropOption } from './attachment-service.js'; +import { attachmentSlashMenuConfig } from './configs/slash-menu.js'; +import { createBuiltinToolbarConfigExtension } from './configs/toolbar'; +import { effects } from './effects.js'; +import { + AttachmentEmbedConfigExtension, + AttachmentEmbedService, +} from './embed'; + +const flavour = AttachmentBlockSchema.model.flavour; + +export default class AttachmentViewExtension extends ViewExtensionProvider { + override name = 'affine-attachment-block'; + + override effect() { + super.effect(); + effects(); + } + + override setup(context: ViewExtensionContext) { + super.setup(context); + context.register([ + FlavourExtension(flavour), + BlockViewExtension(flavour, model => { + return model.parent?.flavour === 'affine:surface' + ? literal`affine-edgeless-attachment` + : literal`affine-attachment`; + }), + AttachmentDropOption, + AttachmentEmbedConfigExtension(), + AttachmentEmbedService, + SlashMenuConfigExtension(flavour, attachmentSlashMenuConfig), + ...createBuiltinToolbarConfigExtension(flavour), + ]); + } +} diff --git a/blocksuite/affine/blocks/attachment/tsconfig.json b/blocksuite/affine/blocks/attachment/tsconfig.json index ba7d24e138..c555016157 100644 --- a/blocksuite/affine/blocks/attachment/tsconfig.json +++ b/blocksuite/affine/blocks/attachment/tsconfig.json @@ -10,6 +10,7 @@ { "path": "../embed" }, { "path": "../surface" }, { "path": "../../components" }, + { "path": "../../ext-loader" }, { "path": "../../model" }, { "path": "../../shared" }, { "path": "../../widgets/slash-menu" }, diff --git a/blocksuite/affine/blocks/bookmark/package.json b/blocksuite/affine/blocks/bookmark/package.json index 63896814fe..565f4f905b 100644 --- a/blocksuite/affine/blocks/bookmark/package.json +++ b/blocksuite/affine/blocks/bookmark/package.json @@ -13,6 +13,7 @@ "@blocksuite/affine-block-embed": "workspace:*", "@blocksuite/affine-block-surface": "workspace:*", "@blocksuite/affine-components": "workspace:*", + "@blocksuite/affine-ext-loader": "workspace:*", "@blocksuite/affine-model": "workspace:*", "@blocksuite/affine-shared": "workspace:*", "@blocksuite/affine-widget-slash-menu": "workspace:*", @@ -31,7 +32,9 @@ }, "exports": { ".": "./src/index.ts", - "./effects": "./src/effects.ts" + "./effects": "./src/effects.ts", + "./store": "./src/store.ts", + "./view": "./src/view.ts" }, "files": [ "src", diff --git a/blocksuite/affine/blocks/bookmark/src/store.ts b/blocksuite/affine/blocks/bookmark/src/store.ts new file mode 100644 index 0000000000..89a8ae1b70 --- /dev/null +++ b/blocksuite/affine/blocks/bookmark/src/store.ts @@ -0,0 +1,17 @@ +import { + type StoreExtensionContext, + StoreExtensionProvider, +} from '@blocksuite/affine-ext-loader'; +import { BookmarkBlockSchemaExtension } from '@blocksuite/affine-model'; + +import { BookmarkBlockAdapterExtensions } from './adapters/extension'; + +export default class BookmarkStoreExtension extends StoreExtensionProvider { + override name = 'affine-bookmark-block'; + + override setup(context: StoreExtensionContext) { + super.setup(context); + context.register(BookmarkBlockSchemaExtension); + context.register(BookmarkBlockAdapterExtensions); + } +} diff --git a/blocksuite/affine/blocks/bookmark/src/view.ts b/blocksuite/affine/blocks/bookmark/src/view.ts new file mode 100644 index 0000000000..cfb21a216d --- /dev/null +++ b/blocksuite/affine/blocks/bookmark/src/view.ts @@ -0,0 +1,36 @@ +import { + type ViewExtensionContext, + ViewExtensionProvider, +} from '@blocksuite/affine-ext-loader'; +import { BookmarkBlockSchema } from '@blocksuite/affine-model'; +import { BlockViewExtension, FlavourExtension } from '@blocksuite/std'; +import { literal } from 'lit/static-html.js'; + +import { BookmarkSlashMenuConfigExtension } from './configs/slash-menu'; +import { createBuiltinToolbarConfigExtension } from './configs/toolbar'; +import { effects } from './effects'; + +const flavour = BookmarkBlockSchema.model.flavour; + +export default class BookmarkViewExtension extends ViewExtensionProvider { + override name = 'affine-bookmark-block'; + + override effect() { + super.effect(); + effects(); + } + + override setup(context: ViewExtensionContext) { + super.setup(context); + context.register([ + FlavourExtension(flavour), + BlockViewExtension(flavour, model => { + return model.parent?.flavour === 'affine:surface' + ? literal`affine-edgeless-bookmark` + : literal`affine-bookmark`; + }), + BookmarkSlashMenuConfigExtension, + ]); + context.register(createBuiltinToolbarConfigExtension(flavour)); + } +} diff --git a/blocksuite/affine/blocks/bookmark/tsconfig.json b/blocksuite/affine/blocks/bookmark/tsconfig.json index ba7d24e138..c555016157 100644 --- a/blocksuite/affine/blocks/bookmark/tsconfig.json +++ b/blocksuite/affine/blocks/bookmark/tsconfig.json @@ -10,6 +10,7 @@ { "path": "../embed" }, { "path": "../surface" }, { "path": "../../components" }, + { "path": "../../ext-loader" }, { "path": "../../model" }, { "path": "../../shared" }, { "path": "../../widgets/slash-menu" }, diff --git a/blocksuite/affine/ext-loader/README.md b/blocksuite/affine/ext-loader/README.md index 3712edff58..940d21eb78 100644 --- a/blocksuite/affine/ext-loader/README.md +++ b/blocksuite/affine/ext-loader/README.md @@ -78,6 +78,14 @@ class MyViewProvider extends ViewExtensionProvider<{ theme: string }> { context.register([DarkModeExt]); } } + + // Override effect to run one-time initialization logic + override effect() { + // This will only run once per provider class + console.log('Initializing MyViewProvider'); + // Register lit elements + this.registerLitElements(); + } } // Create and use the view extension manager @@ -89,6 +97,25 @@ const pageExtensions = manager.get('page'); const edgelessExtensions = manager.get('edgeless'); ``` +### One-time Initialization with Effect + +View extensions support one-time initialization through the `effect` method. This method is called automatically during setup, but only once per provider class. It's useful for: + +- Initializing global state +- Registering lit elements +- Setting up shared resources + +```typescript +class MyViewProvider extends ViewExtensionProvider { + override effect() { + // This will only run once, even if multiple instances are created + initializeGlobalState(); + registerLitElements(); + setupGlobalEventListeners(); + } +} +``` + ### Available View Scopes The view extension system supports the following scopes: diff --git a/blocksuite/affine/ext-loader/src/__tests__/ext-loader.unit.spec.ts b/blocksuite/affine/ext-loader/src/__tests__/ext-loader.unit.spec.ts index 207a01feaf..ae945ed9e9 100644 --- a/blocksuite/affine/ext-loader/src/__tests__/ext-loader.unit.spec.ts +++ b/blocksuite/affine/ext-loader/src/__tests__/ext-loader.unit.spec.ts @@ -12,6 +12,7 @@ import { type StoreExtensionContext, StoreExtensionProvider, } from '../store-provider'; +import { ViewExtensionManager } from '../view-manager'; import { type ViewExtensionContext, ViewExtensionProvider, @@ -165,3 +166,56 @@ it('should extension manager be able to be injected', () => { const provider = container.provider(); expect(provider.get(StoreExtensionManagerIdentifier)).toBe(manager); }); + +it('should effect only run once', () => { + const effect1 = vi.fn(); + const effect2 = vi.fn(); + class ViewExt1 extends ViewExtensionProvider { + override name = 'ViewExt1'; + + override effect() { + super.effect(); + effect1(); + } + + override setup(context: ViewExtensionContext) { + super.setup(context); + context.register(Ext1); + } + } + + class ViewExt2 extends ViewExtensionProvider { + override name = 'ViewExt2'; + + override effect() { + super.effect(); + effect2(); + } + + override setup(context: ViewExtensionContext) { + super.setup(context); + context.register(Ext2); + } + } + + const manager = new ViewExtensionManager([ViewExt1]); + + expect(ViewExt1.effectRunned).toBe(false); + expect(ViewExt2.effectRunned).toBe(false); + + manager.get('page'); + + expect(ViewExt1.effectRunned).toBe(true); + expect(ViewExt2.effectRunned).toBe(false); + + expect(effect1).toHaveBeenCalledTimes(1); + expect(effect2).toHaveBeenCalledTimes(0); + + manager.get('edgeless'); + + expect(ViewExt1.effectRunned).toBe(true); + expect(ViewExt2.effectRunned).toBe(false); + + expect(effect1).toHaveBeenCalledTimes(1); + expect(effect2).toHaveBeenCalledTimes(0); +}); diff --git a/blocksuite/affine/ext-loader/src/view-provider.ts b/blocksuite/affine/ext-loader/src/view-provider.ts index 8b7c0d7644..fca68b9162 100644 --- a/blocksuite/affine/ext-loader/src/view-provider.ts +++ b/blocksuite/affine/ext-loader/src/view-provider.ts @@ -45,6 +45,12 @@ export type ViewScope = * context.register([DarkModeExt]); * } * } + * + * // Override effect to run one-time initialization logic + * override effect() { + * // This will only run once per provider class + * console.log('Initializing MyViewProvider'); + * } * } * ``` */ @@ -53,6 +59,36 @@ export class ViewExtensionProvider< > extends BaseExtensionProvider { /** The name of the view extension provider */ override name = 'ViewExtension'; + + /** + * Static flag to ensure effect is only run once per provider class + * @internal + */ + static effectRunned = false; + + /** + * Override this method to implement one-time initialization logic for the provider. + * This method will be called automatically during setup, but only once per provider class. + * + * @example + * ```ts + * override effect() { + * super.effect(); + * // Register lit elements + * registerLitElements(); + * } + * ``` + */ + effect(): void {} + + override setup(context: ViewExtensionContext, options?: Options) { + super.setup(context, options); + const constructer = this.constructor as typeof ViewExtensionProvider; + if (!constructer.effectRunned) { + this.effect(); + constructer.effectRunned = true; + } + } } /** diff --git a/tools/utils/src/workspace.gen.ts b/tools/utils/src/workspace.gen.ts index 94da4edf77..9fbd55fa10 100644 --- a/tools/utils/src/workspace.gen.ts +++ b/tools/utils/src/workspace.gen.ts @@ -70,6 +70,7 @@ export const PackageList = [ 'blocksuite/affine/blocks/embed', 'blocksuite/affine/blocks/surface', 'blocksuite/affine/components', + 'blocksuite/affine/ext-loader', 'blocksuite/affine/model', 'blocksuite/affine/shared', 'blocksuite/affine/widgets/slash-menu', @@ -85,6 +86,7 @@ export const PackageList = [ 'blocksuite/affine/blocks/embed', 'blocksuite/affine/blocks/surface', 'blocksuite/affine/components', + 'blocksuite/affine/ext-loader', 'blocksuite/affine/model', 'blocksuite/affine/shared', 'blocksuite/affine/widgets/slash-menu', diff --git a/yarn.lock b/yarn.lock index 860a556e21..54ad5c7493 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2375,6 +2375,7 @@ __metadata: "@blocksuite/affine-block-embed": "workspace:*" "@blocksuite/affine-block-surface": "workspace:*" "@blocksuite/affine-components": "workspace:*" + "@blocksuite/affine-ext-loader": "workspace:*" "@blocksuite/affine-model": "workspace:*" "@blocksuite/affine-shared": "workspace:*" "@blocksuite/affine-widget-slash-menu": "workspace:*" @@ -2401,6 +2402,7 @@ __metadata: "@blocksuite/affine-block-embed": "workspace:*" "@blocksuite/affine-block-surface": "workspace:*" "@blocksuite/affine-components": "workspace:*" + "@blocksuite/affine-ext-loader": "workspace:*" "@blocksuite/affine-model": "workspace:*" "@blocksuite/affine-shared": "workspace:*" "@blocksuite/affine-widget-slash-menu": "workspace:*" @@ -2969,7 +2971,7 @@ __metadata: languageName: unknown linkType: soft -"@blocksuite/affine-ext-loader@workspace:blocksuite/affine/ext-loader": +"@blocksuite/affine-ext-loader@workspace:*, @blocksuite/affine-ext-loader@workspace:blocksuite/affine/ext-loader": version: 0.0.0-use.local resolution: "@blocksuite/affine-ext-loader@workspace:blocksuite/affine/ext-loader" dependencies: