refactor(editor): use extension level config (#12110)

Closes: BS-3396

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **New Features**
  - Introduced structured and validated configuration options for database and linked document views, allowing for more flexible and reliable customization.
  - Enhanced view manager to conditionally enable AI-related paragraph placeholders and database/linked document extensions based on configuration.
- **Chores**
  - Updated dependencies to include the latest version of the Zod validation library.
  - Simplified and consolidated internal configuration and registration logic for AI and widget-related extensions.
- **Refactor**
  - Streamlined configuration types and removed unused or redundant configuration utilities to improve maintainability.
  - Improved robustness of linked widget configuration retrieval to handle optional service availability gracefully.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Saul-Mirone
2025-05-04 13:53:25 +00:00
parent a23112c12a
commit f3b5c36cf7
11 changed files with 156 additions and 118 deletions
@@ -1,10 +1,6 @@
import type { MenuOptions } from '@blocksuite/affine-components/context-menu';
import { type DatabaseBlockModel } from '@blocksuite/affine-model';
import { ConfigExtensionFactory } from '@blocksuite/std'; import { ConfigExtensionFactory } from '@blocksuite/std';
export interface DatabaseOptionsConfig { import type { DatabaseViewExtensionOptions } from './view';
configure: (model: DatabaseBlockModel, options: MenuOptions) => MenuOptions;
}
export const DatabaseConfigExtension = export const DatabaseConfigExtension =
ConfigExtensionFactory<DatabaseOptionsConfig>('affine:database'); ConfigExtensionFactory<DatabaseViewExtensionOptions>('affine:database');
@@ -46,10 +46,7 @@ import { computed, signal } from '@preact/signals-core';
import { css, html, nothing, unsafeCSS } from 'lit'; import { css, html, nothing, unsafeCSS } from 'lit';
import { popSideDetail } from './components/layout.js'; import { popSideDetail } from './components/layout.js';
import { import { DatabaseConfigExtension } from './config.js';
DatabaseConfigExtension,
type DatabaseOptionsConfig,
} from './config.js';
import { HostContextKey } from './context/host-context.js'; import { HostContextKey } from './context/host-context.js';
import { DatabaseBlockDataSource } from './data-source.js'; import { DatabaseBlockDataSource } from './data-source.js';
import { BlockRenderer } from './detail-panel/block-renderer.js'; import { BlockRenderer } from './detail-panel/block-renderer.js';
@@ -57,6 +54,7 @@ import { NoteRenderer } from './detail-panel/note-renderer.js';
import { DatabaseSelection } from './selection.js'; import { DatabaseSelection } from './selection.js';
import { currentViewStorage } from './utils/current-view.js'; import { currentViewStorage } from './utils/current-view.js';
import { getSingleDocIdFromText } from './utils/title-doc.js'; import { getSingleDocIdFromText } from './utils/title-doc.js';
import type { DatabaseViewExtensionOptions } from './view';
export class DatabaseBlockComponent extends CaptionedBlockComponent<DatabaseBlockModel> { export class DatabaseBlockComponent extends CaptionedBlockComponent<DatabaseBlockModel> {
static override styles = css` static override styles = css`
@@ -345,7 +343,7 @@ export class DatabaseBlockComponent extends CaptionedBlockComponent<DatabaseBloc
return this._dataSource; return this._dataSource;
} }
get optionsConfig(): DatabaseOptionsConfig { get optionsConfig(): DatabaseViewExtensionOptions {
return { return {
configure: (_model, options) => options, configure: (_model, options) => options,
...this.std.getOptional(DatabaseConfigExtension.identifier), ...this.std.getOptional(DatabaseConfigExtension.identifier),
+25 -2
View File
@@ -1,28 +1,51 @@
import type { MenuOptions } from '@blocksuite/affine-components/context-menu';
import { import {
type ViewExtensionContext, type ViewExtensionContext,
ViewExtensionProvider, ViewExtensionProvider,
} from '@blocksuite/affine-ext-loader'; } from '@blocksuite/affine-ext-loader';
import { DatabaseBlockModel } from '@blocksuite/affine-model';
import { SlashMenuConfigExtension } from '@blocksuite/affine-widget-slash-menu'; import { SlashMenuConfigExtension } from '@blocksuite/affine-widget-slash-menu';
import { BlockViewExtension, FlavourExtension } from '@blocksuite/std'; import { BlockViewExtension, FlavourExtension } from '@blocksuite/std';
import { literal } from 'lit/static-html.js'; import { literal } from 'lit/static-html.js';
import { z } from 'zod';
import { DatabaseConfigExtension } from './config';
import { databaseSlashMenuConfig } from './configs/slash-menu.js'; import { databaseSlashMenuConfig } from './configs/slash-menu.js';
import { effects } from './effects'; import { effects } from './effects';
export class DatabaseViewExtension extends ViewExtensionProvider { const optionsSchema = z.object({
configure: z
.function()
.args(z.instanceof(DatabaseBlockModel), z.custom<MenuOptions>())
.returns(z.custom<MenuOptions>()),
});
export type DatabaseViewExtensionOptions = z.infer<typeof optionsSchema>;
export class DatabaseViewExtension extends ViewExtensionProvider<DatabaseViewExtensionOptions> {
override name = 'affine-database-block'; override name = 'affine-database-block';
override schema = optionsSchema;
override effect() { override effect() {
super.effect(); super.effect();
effects(); effects();
} }
override setup(context: ViewExtensionContext) { override setup(
context: ViewExtensionContext,
options?: DatabaseViewExtensionOptions
) {
super.setup(context); super.setup(context);
context.register([ context.register([
FlavourExtension('affine:database'), FlavourExtension('affine:database'),
BlockViewExtension('affine:database', literal`affine-database`), BlockViewExtension('affine:database', literal`affine-database`),
SlashMenuConfigExtension('affine:database', databaseSlashMenuConfig), SlashMenuConfigExtension('affine:database', databaseSlashMenuConfig),
]); ]);
if (options) {
context.register(
DatabaseConfigExtension({ configure: options.configure })
);
}
} }
} }
@@ -28,7 +28,8 @@
"fflate": "^0.8.2", "fflate": "^0.8.2",
"lit": "^3.2.0", "lit": "^3.2.0",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"rxjs": "^7.8.1" "rxjs": "^7.8.1",
"zod": "^3.23.8"
}, },
"exports": { "exports": {
".": "./src/index.ts", ".": "./src/index.ts",
@@ -27,62 +27,12 @@ import type { InlineRange } from '@blocksuite/std/inline';
import type { TemplateResult } from 'lit'; import type { TemplateResult } from 'lit';
import { showImportModal } from './import-doc/index.js'; import { showImportModal } from './import-doc/index.js';
import type { LinkedDocViewExtensionOptions } from './view';
export interface LinkedWidgetConfig { export type LinkedWidgetConfig = Required<
/** Omit<LinkedDocViewExtensionOptions, 'autoFocusedItemKey'>
* The first item of the trigger keys will be the primary key > &
* e.g. @, [[ Pick<LinkedDocViewExtensionOptions, 'autoFocusedItemKey'>;
*/
triggerKeys: [string, ...string[]];
/**
* Convert trigger key to primary key (the first item of the trigger keys)
* [[ -> @
*/
convertTriggerKey: boolean;
ignoreBlockTypes: string[];
ignoreSelector: string;
getMenus: (
query: string,
abort: () => void,
editorHost: EditorHost,
inlineEditor: AffineInlineEditor,
abortSignal: AbortSignal
) => Promise<LinkedMenuGroup[]> | LinkedMenuGroup[];
/**
* Auto focused item
*
* Will be called when the menu is
* - opened
* - query changed
* - menu group or its items changed
*
* If the return value is not null, no action will be taken.
*/
autoFocusedItemKey?: (
menus: LinkedMenuGroup[],
query: string,
currentActiveKey: string | null,
editorHost: EditorHost,
inlineEditor: AffineInlineEditor
) => string | null;
mobile: {
/**
* The linked doc menu widget will scroll the container to make sure the input cursor is visible in viewport.
* It accepts a selector string, HTMLElement or Window
*
* @default getViewportElement(editorHost) this is the scrollable container in playground
*/
scrollContainer?: string | HTMLElement | Window;
/**
* The offset between the top of viewport and the input cursor
*
* @default 46 The height of header in playground
*/
scrollTopOffset?: number | (() => number);
};
}
export type LinkedMenuItem = { export type LinkedMenuItem = {
key: string; key: string;
@@ -269,7 +219,6 @@ export function getMenus(
} }
export const LinkedWidgetUtils = { export const LinkedWidgetUtils = {
createLinkedDocMenuGroup,
createNewDocMenuGroup, createNewDocMenuGroup,
insertLinkedNode, insertLinkedNode,
}; };
@@ -2,20 +2,82 @@ import {
type ViewExtensionContext, type ViewExtensionContext,
ViewExtensionProvider, ViewExtensionProvider,
} from '@blocksuite/affine-ext-loader'; } from '@blocksuite/affine-ext-loader';
import type { AffineInlineEditor } from '@blocksuite/affine-shared/types';
import type { EditorHost } from '@blocksuite/std';
import { z } from 'zod';
import { type LinkedMenuGroup, LinkedWidgetConfigExtension } from './config';
import { effects } from './effects'; import { effects } from './effects';
import { linkedDocWidget } from './widget'; import { linkedDocWidget } from './widget';
export class LinkedDocViewExtension extends ViewExtensionProvider { const optionsSchema = z.object({
triggerKeys: z.optional(z.tuple([z.string()]).rest(z.string())),
convertTriggerKey: z.boolean().optional(),
ignoreBlockTypes: z.array(z.string()).optional(),
ignoreSelector: z.string().optional(),
getMenus: z.optional(
z
.function()
.args(
z.string(),
z.function().returns(z.void()),
z.custom<EditorHost>(),
z.custom<AffineInlineEditor>(),
z.instanceof(AbortSignal)
)
.returns(
z.union([
z.promise(z.array(z.custom<LinkedMenuGroup>())),
z.array(z.custom<LinkedMenuGroup>()),
])
)
),
autoFocusedItemKey: z.optional(
z
.function()
.args(
z.array(z.custom<LinkedMenuGroup>()),
z.string(),
z.string().nullable(),
z.custom<EditorHost>(),
z.custom<AffineInlineEditor>()
)
.returns(z.string().nullable())
),
mobile: z
.object({
scrollContainer: z.optional(
z.union([z.string(), z.instanceof(HTMLElement), z.custom<Window>()])
),
scrollTopOffset: z.optional(
z.union([z.number(), z.function().returns(z.number())])
),
})
.optional(),
});
export type LinkedDocViewExtensionOptions = z.infer<typeof optionsSchema>;
export class LinkedDocViewExtension extends ViewExtensionProvider<LinkedDocViewExtensionOptions> {
override name = 'affine-linked-doc-widget'; override name = 'affine-linked-doc-widget';
override schema = optionsSchema;
override effect() { override effect() {
super.effect(); super.effect();
effects(); effects();
} }
override setup(context: ViewExtensionContext) { override setup(
context: ViewExtensionContext,
options?: LinkedDocViewExtensionOptions
) {
super.setup(context); super.setup(context);
context.register(linkedDocWidget); context.register(linkedDocWidget);
if (options) {
context.register(LinkedWidgetConfigExtension(options));
}
} }
} }
@@ -1,14 +1,10 @@
import { WorkspaceServerService } from '@affine/core/modules/cloud'; import { WorkspaceServerService } from '@affine/core/modules/cloud';
import { EditorSettingService } from '@affine/core/modules/editor-setting'; import { EditorSettingService } from '@affine/core/modules/editor-setting';
import { DatabaseConfigExtension } from '@blocksuite/affine/blocks/database';
import { ToolbarMoreMenuConfigExtension } from '@blocksuite/affine/components/toolbar'; import { ToolbarMoreMenuConfigExtension } from '@blocksuite/affine/components/toolbar';
import { EditorSettingExtension } from '@blocksuite/affine/shared/services'; import { EditorSettingExtension } from '@blocksuite/affine/shared/services';
import type { ExtensionType } from '@blocksuite/affine/store'; import type { ExtensionType } from '@blocksuite/affine/store';
import { LinkedWidgetConfigExtension } from '@blocksuite/affine/widgets/linked-doc';
import type { FrameworkProvider } from '@toeverything/infra'; import type { FrameworkProvider } from '@toeverything/infra';
import { createDatabaseOptionsConfig } from './database';
import { createLinkedWidgetConfig } from './linked';
import { import {
createCustomToolbarExtension, createCustomToolbarExtension,
createToolbarMoreMenuConfig, createToolbarMoreMenuConfig,
@@ -27,8 +23,6 @@ export function getEditorConfigExtension(
setting$: editorSettingService.editorSetting.settingSignal, setting$: editorSettingService.editorSetting.settingSignal,
set: (k, v) => editorSettingService.editorSetting.set(k, v), set: (k, v) => editorSettingService.editorSetting.set(k, v),
}), }),
DatabaseConfigExtension(createDatabaseOptionsConfig(framework)),
LinkedWidgetConfigExtension(createLinkedWidgetConfig(framework)),
ToolbarMoreMenuConfigExtension(createToolbarMoreMenuConfig(framework)), ToolbarMoreMenuConfigExtension(createToolbarMoreMenuConfig(framework)),
createCustomToolbarExtension(editorSettingService.editorSetting, baseUrl), createCustomToolbarExtension(editorSettingService.editorSetting, baseUrl),
@@ -4,6 +4,8 @@ import { type FrameworkProvider } from '@toeverything/infra';
export function createLinkedWidgetConfig( export function createLinkedWidgetConfig(
framework: FrameworkProvider framework: FrameworkProvider
): Partial<LinkedWidgetConfig> { ): Partial<LinkedWidgetConfig> | undefined {
return framework.get(AtMenuConfigService).getConfig(); const service = framework.getOptional(AtMenuConfigService);
if (!service) return;
return service.getConfig();
} }
@@ -21,7 +21,6 @@ import {
getThemeExtension, getThemeExtension,
} from '@affine/core/blocksuite/extensions/theme'; } from '@affine/core/blocksuite/extensions/theme';
import { PeekViewService } from '@affine/core/modules/peek-view'; import { PeekViewService } from '@affine/core/modules/peek-view';
import { ParagraphBlockConfigExtension } from '@blocksuite/affine/blocks/paragraph';
import { import {
type ViewExtensionContext, type ViewExtensionContext,
ViewExtensionProvider, ViewExtensionProvider,
@@ -60,58 +59,37 @@ export class AffineCommonViewExtension extends ViewExtensionProvider<
) { ) {
context.register(AIChatBlockSpec); context.register(AIChatBlockSpec);
context.register(AITranscriptionBlockSpec); context.register(AITranscriptionBlockSpec);
context.register( context.register([
[ AICodeBlockWatcher,
AICodeBlockWatcher, ToolbarModuleExtension({
id: BlockFlavourIdentifier('custom:affine:image'),
config: imageToolbarAIEntryConfig(),
}),
]);
if (context.scope === 'edgeless' || context.scope === 'page') {
context.register([
aiPanelWidget,
AiSlashMenuConfigExtension(),
ToolbarModuleExtension({ ToolbarModuleExtension({
id: BlockFlavourIdentifier('custom:affine:image'), id: BlockFlavourIdentifier('custom:affine:note'),
config: imageToolbarAIEntryConfig(), config: toolbarAIEntryConfig(),
}), }),
ParagraphBlockConfigExtension({ ]);
getPlaceholder: model => { }
const placeholders = {
text: "Type '/' for commands, 'space' for AI",
h1: 'Heading 1',
h2: 'Heading 2',
h3: 'Heading 3',
h4: 'Heading 4',
h5: 'Heading 5',
h6: 'Heading 6',
quote: '',
};
return placeholders[model.props.type];
},
}),
].flat()
);
if (context.scope === 'edgeless') { if (context.scope === 'edgeless') {
context.register([ context.register([
CopilotTool, CopilotTool,
aiPanelWidget,
edgelessCopilotWidget, edgelessCopilotWidget,
getAIEdgelessRootWatcher(framework), getAIEdgelessRootWatcher(framework),
// In note // In note
ToolbarModuleExtension({
id: BlockFlavourIdentifier('custom:affine:note'),
config: toolbarAIEntryConfig(),
}),
ToolbarModuleExtension({ ToolbarModuleExtension({
id: BlockFlavourIdentifier('custom:affine:surface:*'), id: BlockFlavourIdentifier('custom:affine:surface:*'),
config: edgelessToolbarAIEntryConfig(), config: edgelessToolbarAIEntryConfig(),
}), }),
AiSlashMenuConfigExtension(),
]); ]);
} }
if (context.scope === 'page') { if (context.scope === 'page') {
context.register([ context.register(getAIPageRootWatcher(framework));
aiPanelWidget,
getAIPageRootWatcher(framework),
ToolbarModuleExtension({
id: BlockFlavourIdentifier('custom:affine:note'),
config: toolbarAIEntryConfig(),
}),
AiSlashMenuConfigExtension(),
]);
} }
} }
@@ -1,10 +1,15 @@
import { createDatabaseOptionsConfig } from '@affine/core/blocksuite/extensions/editor-config/database';
import { createLinkedWidgetConfig } from '@affine/core/blocksuite/extensions/editor-config/linked';
import { AffineCommonViewExtension } from '@affine/core/blocksuite/manager/common-view'; import { AffineCommonViewExtension } from '@affine/core/blocksuite/manager/common-view';
import { import {
AffineEditorViewExtension, AffineEditorViewExtension,
type AffineEditorViewOptions, type AffineEditorViewOptions,
} from '@affine/core/blocksuite/manager/editor-view'; } from '@affine/core/blocksuite/manager/editor-view';
import { DatabaseViewExtension } from '@blocksuite/affine/blocks/database/view';
import { ParagraphViewExtension } from '@blocksuite/affine/blocks/paragraph/view';
import { ViewExtensionManager } from '@blocksuite/affine/ext-loader'; import { ViewExtensionManager } from '@blocksuite/affine/ext-loader';
import { getInternalViewExtensions } from '@blocksuite/affine/extensions/view'; import { getInternalViewExtensions } from '@blocksuite/affine/extensions/view';
import { LinkedDocViewExtension } from '@blocksuite/affine/widgets/linked-doc/view';
import type { FrameworkProvider } from '@toeverything/infra'; import type { FrameworkProvider } from '@toeverything/infra';
const manager = new ViewExtensionManager([ const manager = new ViewExtensionManager([
@@ -24,5 +29,34 @@ export function getViewManager(
enableAI, enableAI,
}); });
manager.configure(AffineEditorViewExtension, options); manager.configure(AffineEditorViewExtension, options);
if (framework) {
manager.configure(
DatabaseViewExtension,
createDatabaseOptionsConfig(framework)
);
manager.configure(
LinkedDocViewExtension,
createLinkedWidgetConfig(framework)
);
}
if (enableAI) {
manager.configure(ParagraphViewExtension, {
getPlaceholder: model => {
const placeholders = {
text: "Type '/' for commands, 'space' for AI",
h1: 'Heading 1',
h2: 'Heading 2',
h3: 'Heading 3',
h4: 'Heading 4',
h5: 'Heading 5',
h6: 'Heading 6',
quote: '',
};
return placeholders[model.props.type] ?? '';
},
});
}
return manager; return manager;
} }
+1
View File
@@ -3921,6 +3921,7 @@ __metadata:
lit: "npm:^3.2.0" lit: "npm:^3.2.0"
lodash-es: "npm:^4.17.21" lodash-es: "npm:^4.17.21"
rxjs: "npm:^7.8.1" rxjs: "npm:^7.8.1"
zod: "npm:^3.23.8"
languageName: unknown languageName: unknown
linkType: soft linkType: soft