From d37868d97a73725cdfa0db11be39c312a656e35a Mon Sep 17 00:00:00 2001 From: L-Sun Date: Fri, 7 Mar 2025 09:05:03 +0000 Subject: [PATCH] refactor(editor): ai slash menu config extension (#10680) --- .../affine/widget-slash-menu/src/config.ts | 10 +- .../affine/widget-slash-menu/src/index.ts | 1 + .../affine/widget-slash-menu/src/types.ts | 2 +- .../affine/widget-slash-menu/src/utils.ts | 7 +- .../affine/widget-slash-menu/src/widget.ts | 9 +- .../tests-legacy/e2e/slash-menu.spec.ts | 124 ------------------ .../ai/extensions/ai-edgeless-root.ts | 8 +- .../blocksuite/ai/extensions/ai-page-root.ts | 8 +- .../ai-slash-menu.ts} | 22 ++-- 9 files changed, 26 insertions(+), 165 deletions(-) rename packages/frontend/core/src/blocksuite/ai/{entries/slash-menu/setup-slash-menu.ts => extensions/ai-slash-menu.ts} (85%) diff --git a/blocksuite/affine/widget-slash-menu/src/config.ts b/blocksuite/affine/widget-slash-menu/src/config.ts index 8318d322a4..428c61f479 100644 --- a/blocksuite/affine/widget-slash-menu/src/config.ts +++ b/blocksuite/affine/widget-slash-menu/src/config.ts @@ -41,7 +41,7 @@ export const defaultSlashMenuConfig: SlashMenuConfig = { disableWhen: ({ model }) => { return model.flavour === 'affine:code'; }, - items: [ + items: ({ std, model }) => [ { name: 'New Doc', description: 'Start a new document.', @@ -103,7 +103,7 @@ export const defaultSlashMenuConfig: SlashMenuConfig = { }, // --------------------------------------------------------- - ({ std, model }) => { + ...(() => { const { host } = std; const surfaceModel = getSurfaceBlock(host.doc); @@ -152,10 +152,10 @@ export const defaultSlashMenuConfig: SlashMenuConfig = { })); return [...frameItems, ...groupItems]; - }, + })(), // --------------------------------------------------------- - () => { + ...((): SlashMenuActionItem[] => { const now = new Date(); const tomorrow = new Date(); const yesterday = new Date(); @@ -209,7 +209,7 @@ export const defaultSlashMenuConfig: SlashMenuConfig = { }, }, ]; - }, + })(), // --------------------------------------------------------- // { groupName: 'Actions' }, diff --git a/blocksuite/affine/widget-slash-menu/src/index.ts b/blocksuite/affine/widget-slash-menu/src/index.ts index 8417487dc8..7b62da696b 100644 --- a/blocksuite/affine/widget-slash-menu/src/index.ts +++ b/blocksuite/affine/widget-slash-menu/src/index.ts @@ -2,4 +2,5 @@ export { AFFINE_SLASH_MENU_WIDGET } from './consts'; // TODO(@L-Sun): narrow the scope of the exported symbols export * from './extensions'; export * from './types'; +// TODO(@L-Sun): remove this when refactoring quick search export * from './widget'; diff --git a/blocksuite/affine/widget-slash-menu/src/types.ts b/blocksuite/affine/widget-slash-menu/src/types.ts index 49cf2cd1aa..068f6c9385 100644 --- a/blocksuite/affine/widget-slash-menu/src/types.ts +++ b/blocksuite/affine/widget-slash-menu/src/types.ts @@ -51,7 +51,7 @@ export type SlashMenuConfig = { /** * The items in the slash menu. It can be generated dynamically with the context. */ - items: (SlashMenuItem | ((ctx: SlashMenuContext) => SlashMenuItem[]))[]; + items: SlashMenuItem[] | ((ctx: SlashMenuContext) => SlashMenuItem[]); /** * Slash menu will not be triggered when the condition is true. diff --git a/blocksuite/affine/widget-slash-menu/src/utils.ts b/blocksuite/affine/widget-slash-menu/src/utils.ts index 428c162c67..19185b29cc 100644 --- a/blocksuite/affine/widget-slash-menu/src/utils.ts +++ b/blocksuite/affine/widget-slash-menu/src/utils.ts @@ -73,11 +73,14 @@ export function mergeSlashMenuConfigs( configs: Map ): SlashMenuConfig { return { - items: Array.from(configs.values().flatMap(config => config.items)), + items: ctx => + Array.from(configs.values()).flatMap(({ items }) => + typeof items === 'function' ? items(ctx) : items + ), disableWhen: ctx => configs .values() - .map(config => config.disableWhen?.(ctx) ?? false) + .map(({ disableWhen }) => disableWhen?.(ctx) ?? false) .some(Boolean), }; } diff --git a/blocksuite/affine/widget-slash-menu/src/widget.ts b/blocksuite/affine/widget-slash-menu/src/widget.ts index 1ee28b9d6b..8d82400f36 100644 --- a/blocksuite/affine/widget-slash-menu/src/widget.ts +++ b/blocksuite/affine/widget-slash-menu/src/widget.ts @@ -48,14 +48,7 @@ const showSlashMenu = debounce( disposables.add(() => slashMenu.remove()); slashMenu.context = context; slashMenu.items = buildSlashMenuItems( - config.items - .map(item => { - if (typeof item === 'function') { - return item(context); - } - return item; - }) - .flat(), + typeof config.items === 'function' ? config.items(context) : config.items, context, configItemTransform ); diff --git a/blocksuite/tests-legacy/e2e/slash-menu.spec.ts b/blocksuite/tests-legacy/e2e/slash-menu.spec.ts index cfb715b2d7..a29708c4a8 100644 --- a/blocksuite/tests-legacy/e2e/slash-menu.spec.ts +++ b/blocksuite/tests-legacy/e2e/slash-menu.spec.ts @@ -1,4 +1,3 @@ -import type { SlashMenuActionItem } from '@blocksuite/blocks'; import { expect } from '@playwright/test'; import { addNote, switchEditorMode } from './utils/actions/edgeless.js'; @@ -762,129 +761,6 @@ test('should insert database', async ({ page }) => { expect(await defaultRows.count()).toBe(3); }); -// TODO(@L-Sun): Refactor this test after refactoring the slash menu -test.describe('slash menu with customize menu', () => { - test('can remove specified menus', async ({ page }) => { - await enterPlaygroundRoom(page); - await initEmptyParagraphState(page); - await page.evaluate(async () => { - // https://github.com/lit/lit/blob/84df6ef8c73fffec92384891b4b031d7efc01a64/packages/lit-html/src/static.ts#L93 - const fakeLiteral = (strings: TemplateStringsArray) => - ({ - ['_$litStatic$']: strings[0], - r: Symbol.for(''), - }) as const; - - const editor = document.querySelector('affine-editor-container'); - if (!editor) throw new Error("Can't find affine-editor-container"); - - const SlashMenuWidget = window.$blocksuite.blocks.AffineSlashMenuWidget; - class CustomSlashMenu extends SlashMenuWidget { - override get config() { - return { - items: super.config.items - .filter(item => 'action' in item) - .slice(0, 5) - .map((item, index) => ({ - ...item, - group: `0_custom-group@${index++}`, - })), - }; - } - } - // Fix `Illegal constructor` error - // see https://stackoverflow.com/questions/41521812/illegal-constructor-with-ecmascript-6 - customElements.define('affine-custom-slash-menu', CustomSlashMenu); - - const pageSpecs = window.$blocksuite.blocks.PageEditorBlockSpecs; - editor.pageSpecs = [ - ...pageSpecs, - { - setup: di => { - di.override( - window.$blocksuite.blockStd.WidgetViewIdentifier( - 'affine:page|affine-slash-menu-widget' - ), - // @ts-ignore - fakeLiteral`affine-custom-slash-menu` - ); - }, - }, - ]; - await editor.updateComplete; - }); - - await focusRichText(page); - - const slashMenu = page.locator(`.slash-menu`); - const slashItems = slashMenu.locator('icon-button'); - - await type(page, '/'); - await expect(slashMenu).toBeVisible(); - await expect(slashItems).toHaveCount(5); - }); - - test('can add some menus', async ({ page }) => { - await enterPlaygroundRoom(page); - await initEmptyParagraphState(page); - await page.evaluate(async () => { - // https://github.com/lit/lit/blob/84df6ef8c73fffec92384891b4b031d7efc01a64/packages/lit-html/src/static.ts#L93 - // eslint-disable-next-line sonarjs/no-identical-functions - const fakeLiteral = (strings: TemplateStringsArray) => - ({ - ['_$litStatic$']: strings[0], - r: Symbol.for(''), - }) as const; - - const editor = document.querySelector('affine-editor-container'); - if (!editor) throw new Error("Can't find affine-editor-container"); - const SlashMenuWidget = window.$blocksuite.blocks.AffineSlashMenuWidget; - - class CustomSlashMenu extends SlashMenuWidget { - override get config() { - return { - items: [ - { - name: 'Custom Menu Item', - group: '0_custom-group@0', - action: () => {}, - } satisfies SlashMenuActionItem, - ], - }; - } - } - // Fix `Illegal constructor` error - // see https://stackoverflow.com/questions/41521812/illegal-constructor-with-ecmascript-6 - customElements.define('affine-custom-slash-menu', CustomSlashMenu); - - const pageSpecs = window.$blocksuite.blocks.PageEditorBlockSpecs; - editor.pageSpecs = [ - ...pageSpecs, - { - setup: di => - di.override( - window.$blocksuite.blockStd.WidgetViewIdentifier( - 'affine:page|affine-slash-menu-widget' - ), - // @ts-ignore - fakeLiteral`affine-custom-slash-menu` - ), - }, - ]; - await editor.updateComplete; - }); - - await focusRichText(page); - - const slashMenu = page.locator(`.slash-menu`); - const slashItems = slashMenu.locator('icon-button'); - - await type(page, '/'); - await expect(slashMenu).toBeVisible(); - await expect(slashItems).toHaveCount(1); - }); -}); - test('move block up and down by slash menu', async ({ page }) => { await enterPlaygroundRoom(page); await initEmptyParagraphState(page); diff --git a/packages/frontend/core/src/blocksuite/ai/extensions/ai-edgeless-root.ts b/packages/frontend/core/src/blocksuite/ai/extensions/ai-edgeless-root.ts index 5a75e2a726..7d0a8c84a9 100644 --- a/packages/frontend/core/src/blocksuite/ai/extensions/ai-edgeless-root.ts +++ b/packages/frontend/core/src/blocksuite/ai/extensions/ai-edgeless-root.ts @@ -3,7 +3,6 @@ import { LifeCycleWatcher, } from '@blocksuite/affine/block-std'; import { - AffineSlashMenuWidget, EdgelessElementToolbarWidget, EdgelessRootBlockSpec, ToolbarModuleExtension, @@ -17,7 +16,6 @@ import { setupEdgelessCopilot, setupEdgelessElementToolbarAIEntry, } from '../entries/edgeless/index'; -import { setupSlashMenuAIEntry } from '../entries/slash-menu/setup-slash-menu'; import { setupSpaceAIEntry } from '../entries/space/setup-space'; import { CopilotTool } from '../tool/copilot-tool'; import { @@ -28,6 +26,7 @@ import { EdgelessCopilotWidget, edgelessCopilotWidget, } from '../widgets/edgeless-copilot'; +import { AiSlashMenuConfigExtension } from './ai-slash-menu'; export function createAIEdgelessRootBlockSpec( framework: FrameworkProvider @@ -42,6 +41,7 @@ export function createAIEdgelessRootBlockSpec( id: BlockFlavourIdentifier('custom:affine:note'), config: toolbarAIEntryConfig(), }), + AiSlashMenuConfigExtension(), ]; } @@ -70,10 +70,6 @@ function getAIEdgelessRootWatcher(framework: FrameworkProvider) { if (component instanceof EdgelessElementToolbarWidget) { setupEdgelessElementToolbarAIEntry(component); } - - if (component instanceof AffineSlashMenuWidget) { - setupSlashMenuAIEntry(this.std); - } }); } } diff --git a/packages/frontend/core/src/blocksuite/ai/extensions/ai-page-root.ts b/packages/frontend/core/src/blocksuite/ai/extensions/ai-page-root.ts index e8ed263b92..1cfa6cfcf5 100644 --- a/packages/frontend/core/src/blocksuite/ai/extensions/ai-page-root.ts +++ b/packages/frontend/core/src/blocksuite/ai/extensions/ai-page-root.ts @@ -3,7 +3,6 @@ import { LifeCycleWatcher, } from '@blocksuite/affine/block-std'; import { - AffineSlashMenuWidget, PageRootBlockSpec, ToolbarModuleExtension, } from '@blocksuite/affine/blocks'; @@ -12,12 +11,12 @@ import type { FrameworkProvider } from '@toeverything/infra'; import { buildAIPanelConfig } from '../ai-panel'; import { toolbarAIEntryConfig } from '../entries'; -import { setupSlashMenuAIEntry } from '../entries/slash-menu/setup-slash-menu'; import { setupSpaceAIEntry } from '../entries/space/setup-space'; import { AffineAIPanelWidget, aiPanelWidget, } from '../widgets/ai-panel/ai-panel'; +import { AiSlashMenuConfigExtension } from './ai-slash-menu'; function getAIPageRootWatcher(framework: FrameworkProvider) { class AIPageRootWatcher extends LifeCycleWatcher { @@ -36,10 +35,6 @@ function getAIPageRootWatcher(framework: FrameworkProvider) { component.config = buildAIPanelConfig(component, framework); setupSpaceAIEntry(component); } - - if (component instanceof AffineSlashMenuWidget) { - setupSlashMenuAIEntry(this.std); - } }); } } @@ -57,5 +52,6 @@ export function createAIPageRootBlockSpec( id: BlockFlavourIdentifier('custom:affine:note'), config: toolbarAIEntryConfig(), }), + AiSlashMenuConfigExtension(), ]; } diff --git a/packages/frontend/core/src/blocksuite/ai/entries/slash-menu/setup-slash-menu.ts b/packages/frontend/core/src/blocksuite/ai/extensions/ai-slash-menu.ts similarity index 85% rename from packages/frontend/core/src/blocksuite/ai/entries/slash-menu/setup-slash-menu.ts rename to packages/frontend/core/src/blocksuite/ai/extensions/ai-slash-menu.ts index 8a4bee8745..13b904fbc0 100644 --- a/packages/frontend/core/src/blocksuite/ai/entries/slash-menu/setup-slash-menu.ts +++ b/packages/frontend/core/src/blocksuite/ai/extensions/ai-slash-menu.ts @@ -2,26 +2,23 @@ import { AIStarIcon, DocModeProvider, type SlashMenuActionItem, + SlashMenuConfigExtension, type SlashMenuContext, - SlashMenuExtension, type SlashMenuItem, type SlashMenuSubMenu, } from '@blocksuite/affine/blocks'; -import type { BlockStdScope } from '@blocksuite/block-std'; import { MoreHorizontalIcon } from '@blocksuite/icons/lit'; import { html } from 'lit'; -import { pageAIGroups } from '../../_common/config'; -import { handleInlineAskAIAction } from '../../actions/doc-handler'; -import type { AIItemConfig } from '../../components/ai-item/types'; +import { pageAIGroups } from '../_common/config'; +import { handleInlineAskAIAction } from '../actions/doc-handler'; +import type { AIItemConfig } from '../components/ai-item/types'; import { AFFINE_AI_PANEL_WIDGET, type AffineAIPanelWidget, -} from '../../widgets/ai-panel/ai-panel'; - -export function setupSlashMenuAIEntry(std: BlockStdScope) { - const slashMenuExtension = std.get(SlashMenuExtension); +} from '../widgets/ai-panel/ai-panel'; +export function AiSlashMenuConfigExtension() { const AIItems = pageAIGroups.map(group => group.items).flat(); const iconWrapper = (icon: AIItemConfig['icon']) => { @@ -129,8 +126,7 @@ export function setupSlashMenuAIEntry(std: BlockStdScope) { }, ]; - slashMenuExtension.config = { - ...slashMenuExtension.config, - items: [...AIMenuItems, ...slashMenuExtension.config.items], - }; + return SlashMenuConfigExtension('ai', { + items: AIMenuItems, + }); }