mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-12 15:46:29 +08:00
refactor(editor): ai slash menu config extension (#10680)
This commit is contained in:
@@ -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' },
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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
|
||||
);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
];
|
||||
}
|
||||
|
||||
+9
-13
@@ -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,
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user