mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-14 21:27:20 +00:00
fix(core): dedicated link config for comments (#13003)
#### PR Dependency Tree * **PR #13003** 👈 This tree was auto-generated by [Charcoal](https://github.com/danerwilliams/charcoal) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Introduced enhanced member mention functionality in the comment editor, including improved search, highlighting, and avatar display for mentioning users. * **Refactor** * Simplified configuration options for linked widgets and menu groups, removing the ability to selectively include menu groups. * Updated internal logic to streamline menu group handling and improve performance during member searches. * **Bug Fixes** * Prevented unnecessary member search calls on empty queries for better efficiency. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
@@ -1,17 +1,11 @@
|
||||
import {
|
||||
AtMenuConfigService,
|
||||
type LinkedMenuGroupType,
|
||||
} from '@affine/core/modules/at-menu-config/services';
|
||||
import { AtMenuConfigService } from '@affine/core/modules/at-menu-config/services';
|
||||
import type { LinkedWidgetConfig } from '@blocksuite/affine/widgets/linked-doc';
|
||||
import { type FrameworkProvider } from '@toeverything/infra';
|
||||
|
||||
export function createLinkedWidgetConfig(
|
||||
framework: FrameworkProvider,
|
||||
options?: {
|
||||
includedGroups?: LinkedMenuGroupType[];
|
||||
}
|
||||
framework: FrameworkProvider
|
||||
): Partial<LinkedWidgetConfig> | undefined {
|
||||
const service = framework.getOptional(AtMenuConfigService);
|
||||
if (!service) return;
|
||||
return service.getConfig(options?.includedGroups);
|
||||
return service.getConfig();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,165 @@
|
||||
import { MemberSearchService } from '@affine/core/modules/permissions';
|
||||
import { highlighter } from '@affine/core/modules/quicksearch/utils/highlighter';
|
||||
import { I18n } from '@affine/i18n';
|
||||
import track from '@affine/track';
|
||||
import type { AffineInlineEditor } from '@blocksuite/affine/shared/types';
|
||||
import type {
|
||||
LinkedMenuItem,
|
||||
LinkedWidgetConfig,
|
||||
} from '@blocksuite/affine/widgets/linked-doc';
|
||||
import { UserIcon } from '@blocksuite/icons/lit';
|
||||
import { BLOCK_ID_ATTR, type BlockComponent } from '@blocksuite/std';
|
||||
import { computed } from '@preact/signals-core';
|
||||
import type { FrameworkProvider } from '@toeverything/infra';
|
||||
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||
import Fuse, { type FuseResultMatch } from 'fuse.js';
|
||||
import { html } from 'lit';
|
||||
import { styleMap } from 'lit/directives/style-map.js';
|
||||
import { unsafeHTML } from 'lit/directives/unsafe-html.js';
|
||||
|
||||
export const createCommentLinkedWidgetConfig = (
|
||||
framework: FrameworkProvider
|
||||
): Partial<LinkedWidgetConfig> | undefined => {
|
||||
const memberGroup = (
|
||||
query: string,
|
||||
close: () => void,
|
||||
inlineEditor: AffineInlineEditor
|
||||
) => {
|
||||
const memberSearchService = framework.get(MemberSearchService);
|
||||
|
||||
const getMenuItem = (
|
||||
id: string,
|
||||
name?: string | null,
|
||||
avatar?: string | null
|
||||
): LinkedMenuItem => {
|
||||
const avatarStyle = styleMap({
|
||||
borderRadius: '50%',
|
||||
border: `1px solid ${cssVarV2('layer/background/overlayPanel')}`,
|
||||
width: '20px',
|
||||
height: '20px',
|
||||
boxSizing: 'border-box',
|
||||
});
|
||||
const icon = avatar
|
||||
? html`<img style=${avatarStyle} src="${avatar}" />`
|
||||
: UserIcon();
|
||||
|
||||
let displayName = name ?? 'Unknown';
|
||||
return {
|
||||
key: id,
|
||||
name: html`${unsafeHTML(displayName)}`,
|
||||
icon,
|
||||
action: () => {
|
||||
const root = inlineEditor.rootElement;
|
||||
const block = root?.closest<BlockComponent>(`[${BLOCK_ID_ATTR}]`);
|
||||
if (!block) return;
|
||||
|
||||
close();
|
||||
|
||||
track.doc.editor.atMenu.mentionMember({
|
||||
type: 'member',
|
||||
});
|
||||
|
||||
const inlineRange = inlineEditor.getInlineRange();
|
||||
if (!inlineRange || inlineRange.length !== 0) return;
|
||||
|
||||
inlineEditor.insertText(inlineRange, ' ', {
|
||||
mention: {
|
||||
member: id,
|
||||
},
|
||||
});
|
||||
inlineEditor.setInlineRange({
|
||||
index: inlineRange.index + 1,
|
||||
length: 0,
|
||||
});
|
||||
},
|
||||
};
|
||||
};
|
||||
const highlightFuseTitle = (
|
||||
matches: readonly FuseResultMatch[] | undefined,
|
||||
title: string,
|
||||
key: string
|
||||
): string => {
|
||||
if (!matches) {
|
||||
return title;
|
||||
}
|
||||
const normalizedRange = ([start, end]: [number, number]) =>
|
||||
[
|
||||
start,
|
||||
end + 1 /* in fuse, the `end` is different from the `substring` */,
|
||||
] as [number, number];
|
||||
const titleMatches = matches
|
||||
?.filter(match => match.key === key)
|
||||
.flatMap(match => match.indices.map(normalizedRange));
|
||||
return (
|
||||
highlighter(
|
||||
title,
|
||||
`<span style="color: ${cssVarV2('text/emphasis')}">`,
|
||||
'</span>',
|
||||
titleMatches ?? []
|
||||
) ?? title
|
||||
);
|
||||
};
|
||||
|
||||
memberSearchService.search(query);
|
||||
|
||||
const items = computed<LinkedMenuItem[]>(() => {
|
||||
const members = memberSearchService.result$.signal.value;
|
||||
console.log('members', members);
|
||||
|
||||
if (query.length === 0) {
|
||||
return members
|
||||
.slice(0, 3)
|
||||
.map(member => getMenuItem(member.id, member.name, member.avatarUrl));
|
||||
}
|
||||
|
||||
// Create a single Fuse instance for all members
|
||||
const fuse = new Fuse(members, {
|
||||
keys: ['name'],
|
||||
includeMatches: true,
|
||||
includeScore: true,
|
||||
ignoreLocation: true,
|
||||
threshold: 0.0,
|
||||
});
|
||||
const searchResults = fuse.search(query);
|
||||
|
||||
return searchResults.map(result => {
|
||||
const member = result.item;
|
||||
const displayName = highlightFuseTitle(
|
||||
result.matches,
|
||||
member.name ?? 'Unknown',
|
||||
'name'
|
||||
);
|
||||
return {
|
||||
...getMenuItem(member.id, member.name, member.avatarUrl),
|
||||
name: html`${unsafeHTML(displayName)}`,
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
name: I18n.t('com.affine.editor.at-menu.mention-members'),
|
||||
items,
|
||||
loading: memberSearchService.isLoading$.signal,
|
||||
hidden: computed(() => {
|
||||
return (
|
||||
memberSearchService.result$.signal.value.length === 0 &&
|
||||
!memberSearchService.isLoading$.signal.value
|
||||
);
|
||||
}),
|
||||
maxDisplay: 3,
|
||||
overflowText: computed(() => {
|
||||
const totalCount = memberSearchService.result$.signal.value.length;
|
||||
const remainingCount = totalCount - 3;
|
||||
return I18n.t('com.affine.editor.at-menu.more-members-hint', {
|
||||
count: remainingCount,
|
||||
});
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
||||
return {
|
||||
getMenus: (query, close, _editorHost, inlineEditor) => {
|
||||
return [memberGroup(query, close, inlineEditor)];
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -1,8 +1,6 @@
|
||||
import { CloudViewExtension } from '@affine/core/blocksuite/view-extensions/cloud';
|
||||
import { createLinkedWidgetConfig } from '@affine/core/blocksuite/view-extensions/editor-config/linked';
|
||||
import { AffineEditorViewExtension } from '@affine/core/blocksuite/view-extensions/editor-view/editor-view';
|
||||
import { AffineThemeViewExtension } from '@affine/core/blocksuite/view-extensions/theme';
|
||||
import { LinkedMenuGroupType } from '@affine/core/modules/at-menu-config/services';
|
||||
import { CodeBlockViewExtension } from '@blocksuite/affine/blocks/code/view';
|
||||
import { DividerViewExtension } from '@blocksuite/affine/blocks/divider/view';
|
||||
import { LatexViewExtension as LatexBlockViewExtension } from '@blocksuite/affine/blocks/latex/view';
|
||||
@@ -58,6 +56,8 @@ import { ViewportOverlayViewExtension } from '@blocksuite/affine/widgets/viewpor
|
||||
import type { FrameworkProvider } from '@toeverything/infra';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { createCommentLinkedWidgetConfig } from './linked-widget-config';
|
||||
|
||||
const commentEditorViewExtensionOptionsSchema = z.object({
|
||||
peekView: z.optional(z.custom<PeekViewService>()),
|
||||
fontConfig: z.optional(z.array(fontConfigSchema)),
|
||||
@@ -160,9 +160,7 @@ export function getCommentEditorViewManager(framework: FrameworkProvider) {
|
||||
|
||||
manager.configure(
|
||||
LinkedDocViewExtension,
|
||||
createLinkedWidgetConfig(framework, {
|
||||
includedGroups: [LinkedMenuGroupType.Mention],
|
||||
})
|
||||
createCommentLinkedWidgetConfig(framework)
|
||||
);
|
||||
|
||||
manager.configure(CloudViewExtension, {
|
||||
|
||||
@@ -62,13 +62,6 @@ const RESERVED_ITEM_KEYS = {
|
||||
datePicker: 'date-picker',
|
||||
};
|
||||
|
||||
export enum LinkedMenuGroupType {
|
||||
LinkToDoc = 'link-to-doc',
|
||||
Mention = 'mention',
|
||||
Journal = 'journal',
|
||||
NewDoc = 'new-doc',
|
||||
}
|
||||
|
||||
export class AtMenuConfigService extends Service {
|
||||
constructor(
|
||||
private readonly journalService: JournalService,
|
||||
@@ -86,11 +79,9 @@ export class AtMenuConfigService extends Service {
|
||||
|
||||
// todo(@peng17): maybe refactor the config using entity, so that each config
|
||||
// can be reactive to the query, instead of recreating the whole config?
|
||||
getConfig(
|
||||
includedGroups?: LinkedMenuGroupType[]
|
||||
): Partial<LinkedWidgetConfig> {
|
||||
getConfig(): Partial<LinkedWidgetConfig> {
|
||||
return {
|
||||
getMenus: this.getMenusFn(includedGroups),
|
||||
getMenus: this.getMenusFn(),
|
||||
mobile: this.getMobileConfig(),
|
||||
autoFocusedItemKey: this.autoFocusedItemKey,
|
||||
};
|
||||
@@ -111,14 +102,14 @@ export class AtMenuConfigService extends Service {
|
||||
return null;
|
||||
}
|
||||
|
||||
const linkToDocGroup = menus.at(0);
|
||||
const memberGroup = menus.at(1);
|
||||
const linkToDocGroup = menus[0];
|
||||
const memberGroup = menus[1];
|
||||
|
||||
if (memberGroup && resolveSignal(memberGroup.items).length > 1) {
|
||||
if (resolveSignal(memberGroup.items).length > 1) {
|
||||
return resolveSignal(memberGroup.items)[0]?.key;
|
||||
}
|
||||
|
||||
if (linkToDocGroup && resolveSignal(linkToDocGroup.items).length > 0) {
|
||||
if (resolveSignal(linkToDocGroup.items).length > 0) {
|
||||
return resolveSignal(linkToDocGroup.items)[0]?.key;
|
||||
}
|
||||
|
||||
@@ -644,7 +635,9 @@ export class AtMenuConfigService extends Service {
|
||||
return query.length > 0 && !loading && members.length === 0;
|
||||
});
|
||||
|
||||
this.memberSearchService.search(query);
|
||||
if (query.length > 0) {
|
||||
this.memberSearchService.search(query);
|
||||
}
|
||||
|
||||
return {
|
||||
name: I18n.t('com.affine.editor.at-menu.mention-members'),
|
||||
@@ -662,28 +655,13 @@ export class AtMenuConfigService extends Service {
|
||||
};
|
||||
}
|
||||
|
||||
private getMenusFn(
|
||||
includedGroups: LinkedMenuGroupType[] = [
|
||||
LinkedMenuGroupType.LinkToDoc,
|
||||
LinkedMenuGroupType.Mention,
|
||||
LinkedMenuGroupType.Journal,
|
||||
LinkedMenuGroupType.NewDoc,
|
||||
]
|
||||
): LinkedWidgetConfig['getMenus'] {
|
||||
private getMenusFn(): LinkedWidgetConfig['getMenus'] {
|
||||
return (query, close, editorHost, inlineEditor, abortSignal) => {
|
||||
return [
|
||||
...(includedGroups?.includes(LinkedMenuGroupType.LinkToDoc)
|
||||
? [this.linkToDocGroup(query, close, inlineEditor, abortSignal)]
|
||||
: []),
|
||||
...(includedGroups?.includes(LinkedMenuGroupType.Mention)
|
||||
? [this.memberGroup(query, close, inlineEditor, abortSignal)]
|
||||
: []),
|
||||
...(includedGroups?.includes(LinkedMenuGroupType.Journal)
|
||||
? [this.journalGroup(query, close, inlineEditor)]
|
||||
: []),
|
||||
...(includedGroups?.includes(LinkedMenuGroupType.NewDoc)
|
||||
? [this.newDocMenuGroup(query, close, editorHost, inlineEditor)]
|
||||
: []),
|
||||
this.linkToDocGroup(query, close, inlineEditor, abortSignal),
|
||||
this.memberGroup(query, close, inlineEditor, abortSignal),
|
||||
this.journalGroup(query, close, inlineEditor),
|
||||
this.newDocMenuGroup(query, close, editorHost, inlineEditor),
|
||||
];
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user