mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-09-07 01:09:54 +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 }) => {
|
disableWhen: ({ model }) => {
|
||||||
return model.flavour === 'affine:code';
|
return model.flavour === 'affine:code';
|
||||||
},
|
},
|
||||||
items: [
|
items: ({ std, model }) => [
|
||||||
{
|
{
|
||||||
name: 'New Doc',
|
name: 'New Doc',
|
||||||
description: 'Start a new document.',
|
description: 'Start a new document.',
|
||||||
@@ -103,7 +103,7 @@ export const defaultSlashMenuConfig: SlashMenuConfig = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
// ---------------------------------------------------------
|
// ---------------------------------------------------------
|
||||||
({ std, model }) => {
|
...(() => {
|
||||||
const { host } = std;
|
const { host } = std;
|
||||||
|
|
||||||
const surfaceModel = getSurfaceBlock(host.doc);
|
const surfaceModel = getSurfaceBlock(host.doc);
|
||||||
@@ -152,10 +152,10 @@ export const defaultSlashMenuConfig: SlashMenuConfig = {
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
return [...frameItems, ...groupItems];
|
return [...frameItems, ...groupItems];
|
||||||
},
|
})(),
|
||||||
|
|
||||||
// ---------------------------------------------------------
|
// ---------------------------------------------------------
|
||||||
() => {
|
...((): SlashMenuActionItem[] => {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const tomorrow = new Date();
|
const tomorrow = new Date();
|
||||||
const yesterday = new Date();
|
const yesterday = new Date();
|
||||||
@@ -209,7 +209,7 @@ export const defaultSlashMenuConfig: SlashMenuConfig = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
},
|
})(),
|
||||||
|
|
||||||
// ---------------------------------------------------------
|
// ---------------------------------------------------------
|
||||||
// { groupName: 'Actions' },
|
// { groupName: 'Actions' },
|
||||||
|
|||||||
@@ -2,4 +2,5 @@ export { AFFINE_SLASH_MENU_WIDGET } from './consts';
|
|||||||
// TODO(@L-Sun): narrow the scope of the exported symbols
|
// TODO(@L-Sun): narrow the scope of the exported symbols
|
||||||
export * from './extensions';
|
export * from './extensions';
|
||||||
export * from './types';
|
export * from './types';
|
||||||
|
// TODO(@L-Sun): remove this when refactoring quick search
|
||||||
export * from './widget';
|
export * from './widget';
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ export type SlashMenuConfig = {
|
|||||||
/**
|
/**
|
||||||
* The items in the slash menu. It can be generated dynamically with the context.
|
* 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.
|
* Slash menu will not be triggered when the condition is true.
|
||||||
|
|||||||
@@ -73,11 +73,14 @@ export function mergeSlashMenuConfigs(
|
|||||||
configs: Map<string, SlashMenuConfig>
|
configs: Map<string, SlashMenuConfig>
|
||||||
): SlashMenuConfig {
|
): SlashMenuConfig {
|
||||||
return {
|
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 =>
|
disableWhen: ctx =>
|
||||||
configs
|
configs
|
||||||
.values()
|
.values()
|
||||||
.map(config => config.disableWhen?.(ctx) ?? false)
|
.map(({ disableWhen }) => disableWhen?.(ctx) ?? false)
|
||||||
.some(Boolean),
|
.some(Boolean),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,14 +48,7 @@ const showSlashMenu = debounce(
|
|||||||
disposables.add(() => slashMenu.remove());
|
disposables.add(() => slashMenu.remove());
|
||||||
slashMenu.context = context;
|
slashMenu.context = context;
|
||||||
slashMenu.items = buildSlashMenuItems(
|
slashMenu.items = buildSlashMenuItems(
|
||||||
config.items
|
typeof config.items === 'function' ? config.items(context) : config.items,
|
||||||
.map(item => {
|
|
||||||
if (typeof item === 'function') {
|
|
||||||
return item(context);
|
|
||||||
}
|
|
||||||
return item;
|
|
||||||
})
|
|
||||||
.flat(),
|
|
||||||
context,
|
context,
|
||||||
configItemTransform
|
configItemTransform
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import type { SlashMenuActionItem } from '@blocksuite/blocks';
|
|
||||||
import { expect } from '@playwright/test';
|
import { expect } from '@playwright/test';
|
||||||
|
|
||||||
import { addNote, switchEditorMode } from './utils/actions/edgeless.js';
|
import { addNote, switchEditorMode } from './utils/actions/edgeless.js';
|
||||||
@@ -762,129 +761,6 @@ test('should insert database', async ({ page }) => {
|
|||||||
expect(await defaultRows.count()).toBe(3);
|
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 }) => {
|
test('move block up and down by slash menu', async ({ page }) => {
|
||||||
await enterPlaygroundRoom(page);
|
await enterPlaygroundRoom(page);
|
||||||
await initEmptyParagraphState(page);
|
await initEmptyParagraphState(page);
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import {
|
|||||||
LifeCycleWatcher,
|
LifeCycleWatcher,
|
||||||
} from '@blocksuite/affine/block-std';
|
} from '@blocksuite/affine/block-std';
|
||||||
import {
|
import {
|
||||||
AffineSlashMenuWidget,
|
|
||||||
EdgelessElementToolbarWidget,
|
EdgelessElementToolbarWidget,
|
||||||
EdgelessRootBlockSpec,
|
EdgelessRootBlockSpec,
|
||||||
ToolbarModuleExtension,
|
ToolbarModuleExtension,
|
||||||
@@ -17,7 +16,6 @@ import {
|
|||||||
setupEdgelessCopilot,
|
setupEdgelessCopilot,
|
||||||
setupEdgelessElementToolbarAIEntry,
|
setupEdgelessElementToolbarAIEntry,
|
||||||
} from '../entries/edgeless/index';
|
} from '../entries/edgeless/index';
|
||||||
import { setupSlashMenuAIEntry } from '../entries/slash-menu/setup-slash-menu';
|
|
||||||
import { setupSpaceAIEntry } from '../entries/space/setup-space';
|
import { setupSpaceAIEntry } from '../entries/space/setup-space';
|
||||||
import { CopilotTool } from '../tool/copilot-tool';
|
import { CopilotTool } from '../tool/copilot-tool';
|
||||||
import {
|
import {
|
||||||
@@ -28,6 +26,7 @@ import {
|
|||||||
EdgelessCopilotWidget,
|
EdgelessCopilotWidget,
|
||||||
edgelessCopilotWidget,
|
edgelessCopilotWidget,
|
||||||
} from '../widgets/edgeless-copilot';
|
} from '../widgets/edgeless-copilot';
|
||||||
|
import { AiSlashMenuConfigExtension } from './ai-slash-menu';
|
||||||
|
|
||||||
export function createAIEdgelessRootBlockSpec(
|
export function createAIEdgelessRootBlockSpec(
|
||||||
framework: FrameworkProvider
|
framework: FrameworkProvider
|
||||||
@@ -42,6 +41,7 @@ export function createAIEdgelessRootBlockSpec(
|
|||||||
id: BlockFlavourIdentifier('custom:affine:note'),
|
id: BlockFlavourIdentifier('custom:affine:note'),
|
||||||
config: toolbarAIEntryConfig(),
|
config: toolbarAIEntryConfig(),
|
||||||
}),
|
}),
|
||||||
|
AiSlashMenuConfigExtension(),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,10 +70,6 @@ function getAIEdgelessRootWatcher(framework: FrameworkProvider) {
|
|||||||
if (component instanceof EdgelessElementToolbarWidget) {
|
if (component instanceof EdgelessElementToolbarWidget) {
|
||||||
setupEdgelessElementToolbarAIEntry(component);
|
setupEdgelessElementToolbarAIEntry(component);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (component instanceof AffineSlashMenuWidget) {
|
|
||||||
setupSlashMenuAIEntry(this.std);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import {
|
|||||||
LifeCycleWatcher,
|
LifeCycleWatcher,
|
||||||
} from '@blocksuite/affine/block-std';
|
} from '@blocksuite/affine/block-std';
|
||||||
import {
|
import {
|
||||||
AffineSlashMenuWidget,
|
|
||||||
PageRootBlockSpec,
|
PageRootBlockSpec,
|
||||||
ToolbarModuleExtension,
|
ToolbarModuleExtension,
|
||||||
} from '@blocksuite/affine/blocks';
|
} from '@blocksuite/affine/blocks';
|
||||||
@@ -12,12 +11,12 @@ import type { FrameworkProvider } from '@toeverything/infra';
|
|||||||
|
|
||||||
import { buildAIPanelConfig } from '../ai-panel';
|
import { buildAIPanelConfig } from '../ai-panel';
|
||||||
import { toolbarAIEntryConfig } from '../entries';
|
import { toolbarAIEntryConfig } from '../entries';
|
||||||
import { setupSlashMenuAIEntry } from '../entries/slash-menu/setup-slash-menu';
|
|
||||||
import { setupSpaceAIEntry } from '../entries/space/setup-space';
|
import { setupSpaceAIEntry } from '../entries/space/setup-space';
|
||||||
import {
|
import {
|
||||||
AffineAIPanelWidget,
|
AffineAIPanelWidget,
|
||||||
aiPanelWidget,
|
aiPanelWidget,
|
||||||
} from '../widgets/ai-panel/ai-panel';
|
} from '../widgets/ai-panel/ai-panel';
|
||||||
|
import { AiSlashMenuConfigExtension } from './ai-slash-menu';
|
||||||
|
|
||||||
function getAIPageRootWatcher(framework: FrameworkProvider) {
|
function getAIPageRootWatcher(framework: FrameworkProvider) {
|
||||||
class AIPageRootWatcher extends LifeCycleWatcher {
|
class AIPageRootWatcher extends LifeCycleWatcher {
|
||||||
@@ -36,10 +35,6 @@ function getAIPageRootWatcher(framework: FrameworkProvider) {
|
|||||||
component.config = buildAIPanelConfig(component, framework);
|
component.config = buildAIPanelConfig(component, framework);
|
||||||
setupSpaceAIEntry(component);
|
setupSpaceAIEntry(component);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (component instanceof AffineSlashMenuWidget) {
|
|
||||||
setupSlashMenuAIEntry(this.std);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -57,5 +52,6 @@ export function createAIPageRootBlockSpec(
|
|||||||
id: BlockFlavourIdentifier('custom:affine:note'),
|
id: BlockFlavourIdentifier('custom:affine:note'),
|
||||||
config: toolbarAIEntryConfig(),
|
config: toolbarAIEntryConfig(),
|
||||||
}),
|
}),
|
||||||
|
AiSlashMenuConfigExtension(),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
+9
-13
@@ -2,26 +2,23 @@ import {
|
|||||||
AIStarIcon,
|
AIStarIcon,
|
||||||
DocModeProvider,
|
DocModeProvider,
|
||||||
type SlashMenuActionItem,
|
type SlashMenuActionItem,
|
||||||
|
SlashMenuConfigExtension,
|
||||||
type SlashMenuContext,
|
type SlashMenuContext,
|
||||||
SlashMenuExtension,
|
|
||||||
type SlashMenuItem,
|
type SlashMenuItem,
|
||||||
type SlashMenuSubMenu,
|
type SlashMenuSubMenu,
|
||||||
} from '@blocksuite/affine/blocks';
|
} from '@blocksuite/affine/blocks';
|
||||||
import type { BlockStdScope } from '@blocksuite/block-std';
|
|
||||||
import { MoreHorizontalIcon } from '@blocksuite/icons/lit';
|
import { MoreHorizontalIcon } from '@blocksuite/icons/lit';
|
||||||
import { html } from 'lit';
|
import { html } from 'lit';
|
||||||
|
|
||||||
import { pageAIGroups } from '../../_common/config';
|
import { pageAIGroups } from '../_common/config';
|
||||||
import { handleInlineAskAIAction } from '../../actions/doc-handler';
|
import { handleInlineAskAIAction } from '../actions/doc-handler';
|
||||||
import type { AIItemConfig } from '../../components/ai-item/types';
|
import type { AIItemConfig } from '../components/ai-item/types';
|
||||||
import {
|
import {
|
||||||
AFFINE_AI_PANEL_WIDGET,
|
AFFINE_AI_PANEL_WIDGET,
|
||||||
type AffineAIPanelWidget,
|
type AffineAIPanelWidget,
|
||||||
} from '../../widgets/ai-panel/ai-panel';
|
} from '../widgets/ai-panel/ai-panel';
|
||||||
|
|
||||||
export function setupSlashMenuAIEntry(std: BlockStdScope) {
|
|
||||||
const slashMenuExtension = std.get(SlashMenuExtension);
|
|
||||||
|
|
||||||
|
export function AiSlashMenuConfigExtension() {
|
||||||
const AIItems = pageAIGroups.map(group => group.items).flat();
|
const AIItems = pageAIGroups.map(group => group.items).flat();
|
||||||
|
|
||||||
const iconWrapper = (icon: AIItemConfig['icon']) => {
|
const iconWrapper = (icon: AIItemConfig['icon']) => {
|
||||||
@@ -129,8 +126,7 @@ export function setupSlashMenuAIEntry(std: BlockStdScope) {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
slashMenuExtension.config = {
|
return SlashMenuConfigExtension('ai', {
|
||||||
...slashMenuExtension.config,
|
items: AIMenuItems,
|
||||||
items: [...AIMenuItems, ...slashMenuExtension.config.items],
|
});
|
||||||
};
|
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user