refactor(editor): ai slash menu config extension (#10680)

This commit is contained in:
L-Sun
2025-03-07 09:05:03 +00:00
parent 264f0dd2be
commit d37868d97a
9 changed files with 26 additions and 165 deletions

View File

@@ -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' },

View File

@@ -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';

View File

@@ -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.

View File

@@ -73,11 +73,14 @@ export function mergeSlashMenuConfigs(
configs: Map<string, SlashMenuConfig>
): 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),
};
}

View File

@@ -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
);

View File

@@ -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<SlashMenuActionItem>((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);