mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-12 04:18:54 +00:00
feat(editor): support member node (#11075)
Close [BS-2793](https://linear.app/affine-design/issue/BS-2793/inline-member)
This commit is contained in:
@@ -19,7 +19,7 @@
|
||||
"@affine/templates": "workspace:*",
|
||||
"@affine/track": "workspace:*",
|
||||
"@blocksuite/affine": "workspace:*",
|
||||
"@blocksuite/icons": "^2.2.6",
|
||||
"@blocksuite/icons": "^2.2.8",
|
||||
"@dotlottie/player-component": "^2.7.12",
|
||||
"@emotion/cache": "^11.14.0",
|
||||
"@emotion/react": "^11.14.0",
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { type Framework } from '@toeverything/infra';
|
||||
|
||||
import { WorkspaceServerService } from '../cloud';
|
||||
import { WorkspaceDialogService } from '../dialogs';
|
||||
import { DocsService } from '../doc';
|
||||
import { DocDisplayMetaService } from '../doc-display-meta';
|
||||
import { EditorSettingService } from '../editor-setting';
|
||||
import { JournalService } from '../journal';
|
||||
import { MemberSearchService } from '../permissions';
|
||||
import { SearchMenuService } from '../search-menu/services';
|
||||
import { WorkspaceScope } from '../workspace';
|
||||
import { AtMenuConfigService } from './services';
|
||||
@@ -19,5 +21,7 @@ export function configAtMenuConfigModule(framework: Framework) {
|
||||
EditorSettingService,
|
||||
DocsService,
|
||||
SearchMenuService,
|
||||
WorkspaceServerService,
|
||||
MemberSearchService,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
import { notify } from '@affine/component';
|
||||
import { UserFriendlyError } from '@affine/error';
|
||||
import { type DocMode as GraphqlDocMode } from '@affine/graphql';
|
||||
import { I18n, i18nTime } from '@affine/i18n';
|
||||
import track from '@affine/track';
|
||||
import type { EditorHost } from '@blocksuite/affine/block-std';
|
||||
import {
|
||||
BLOCK_ID_ATTR,
|
||||
type BlockComponent,
|
||||
type EditorHost,
|
||||
} from '@blocksuite/affine/block-std';
|
||||
import {
|
||||
type LinkedMenuGroup,
|
||||
type LinkedMenuItem,
|
||||
@@ -8,24 +15,29 @@ import {
|
||||
LinkedWidgetUtils,
|
||||
} from '@blocksuite/affine/blocks/root';
|
||||
import type { DocMode } from '@blocksuite/affine/model';
|
||||
import { DocModeProvider } from '@blocksuite/affine/shared/services';
|
||||
import type { AffineInlineEditor } from '@blocksuite/affine/shared/types';
|
||||
import type { DocMeta } from '@blocksuite/affine/store';
|
||||
import { Text } from '@blocksuite/affine/store';
|
||||
import {
|
||||
DateTimeIcon,
|
||||
MainAvatarIcon,
|
||||
NewXxxEdgelessIcon,
|
||||
NewXxxPageIcon,
|
||||
} from '@blocksuite/icons/lit';
|
||||
import { computed, Signal } from '@preact/signals-core';
|
||||
import { computed, Signal, signal } from '@preact/signals-core';
|
||||
import { Service } from '@toeverything/infra';
|
||||
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||
import { html } from 'lit';
|
||||
|
||||
import type { WorkspaceServerService } from '../../cloud';
|
||||
import type { WorkspaceDialogService } from '../../dialogs';
|
||||
import type { DocsService } from '../../doc';
|
||||
import type { DocDisplayMetaService } from '../../doc-display-meta';
|
||||
import type { EditorSettingService } from '../../editor-setting';
|
||||
import { type JournalService, suggestJournalDate } from '../../journal';
|
||||
import { NotificationService } from '../../notification';
|
||||
import type { Member, MemberSearchService } from '../../permissions';
|
||||
import type { SearchMenuService } from '../../search-menu/services';
|
||||
|
||||
function resolveSignal<T>(data: T | Signal<T>): T {
|
||||
@@ -45,7 +57,9 @@ export class AtMenuConfigService extends Service {
|
||||
private readonly dialogService: WorkspaceDialogService,
|
||||
private readonly editorSettingService: EditorSettingService,
|
||||
private readonly docsService: DocsService,
|
||||
private readonly searchMenuService: SearchMenuService
|
||||
private readonly searchMenuService: SearchMenuService,
|
||||
private readonly workspaceServerService: WorkspaceServerService,
|
||||
private readonly memberSearchService: MemberSearchService
|
||||
) {
|
||||
super();
|
||||
}
|
||||
@@ -310,12 +324,123 @@ export class AtMenuConfigService extends Service {
|
||||
return result;
|
||||
}
|
||||
|
||||
private memberGroup(
|
||||
query: string,
|
||||
close: () => void,
|
||||
inlineEditor: AffineInlineEditor,
|
||||
_: AbortSignal
|
||||
): LinkedMenuGroup {
|
||||
const inviteItem: LinkedMenuItem = {
|
||||
key: 'invite',
|
||||
name: 'Invite...',
|
||||
icon: MainAvatarIcon(),
|
||||
action: () => {
|
||||
close();
|
||||
|
||||
this.dialogService.open('setting', {
|
||||
activeTab: 'workspace:members',
|
||||
});
|
||||
},
|
||||
};
|
||||
const convertMemberToMenuItem = (member: Member) => {
|
||||
const { id, name, avatarUrl } = member;
|
||||
const icon = avatarUrl
|
||||
? html`<img style="width: 20px; height: 20px;" src="${avatarUrl}" />`
|
||||
: MainAvatarIcon();
|
||||
return {
|
||||
key: id,
|
||||
name: name ?? 'Unknown',
|
||||
icon,
|
||||
action: () => {
|
||||
close();
|
||||
|
||||
const root = inlineEditor.rootElement;
|
||||
const block = root?.closest<BlockComponent>(`[${BLOCK_ID_ATTR}]`);
|
||||
if (!block) return;
|
||||
|
||||
const notificationService =
|
||||
this.workspaceServerService.server?.scope.get(NotificationService);
|
||||
if (!notificationService) return;
|
||||
|
||||
const doc = block.doc;
|
||||
const workspaceId = doc.workspace.id;
|
||||
const docId = doc.id;
|
||||
const docTitle = doc.meta?.title ?? '';
|
||||
const mode = block.std.get(DocModeProvider).getEditorMode() ?? 'page';
|
||||
|
||||
notificationService
|
||||
.mentionUser(id, workspaceId, {
|
||||
id: docId,
|
||||
title: docTitle,
|
||||
blockId: block.blockId,
|
||||
mode: mode as GraphqlDocMode,
|
||||
})
|
||||
.then(notificationId => {
|
||||
const inlineRange = inlineEditor.getInlineRange();
|
||||
if (inlineRange && inlineRange.length === 0) {
|
||||
inlineEditor.insertText(inlineRange, ' ', {
|
||||
mention: {
|
||||
member: id,
|
||||
notification: notificationId,
|
||||
},
|
||||
});
|
||||
inlineEditor.setInlineRange({
|
||||
index: inlineRange.index + 1,
|
||||
length: 0,
|
||||
});
|
||||
}
|
||||
|
||||
notify.success({
|
||||
title: I18n.t('com.affine.editor.at-menu.mention-success'),
|
||||
});
|
||||
})
|
||||
.catch(error => {
|
||||
const err = UserFriendlyError.fromAny(error);
|
||||
notify.error({
|
||||
title: I18n[`error.${err.name}`](err.data),
|
||||
});
|
||||
});
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
if (query.length === 0) {
|
||||
return {
|
||||
name: I18n.t('com.affine.editor.at-menu.mention-member'),
|
||||
items: [
|
||||
...this.memberSearchService.result$.value
|
||||
.slice(0, 3)
|
||||
.map(member => convertMemberToMenuItem(member)),
|
||||
inviteItem,
|
||||
],
|
||||
};
|
||||
}
|
||||
this.memberSearchService.reset();
|
||||
|
||||
const items = signal<LinkedMenuItem[]>([inviteItem]);
|
||||
const loading = this.memberSearchService.isLoading$.signal;
|
||||
this.memberSearchService.result$.subscribe(members => {
|
||||
items.value = [
|
||||
...members.map(member => convertMemberToMenuItem(member)),
|
||||
inviteItem,
|
||||
];
|
||||
});
|
||||
this.memberSearchService.search(query);
|
||||
|
||||
return {
|
||||
name: I18n.t('com.affine.editor.at-menu.mention-member'),
|
||||
items,
|
||||
loading,
|
||||
};
|
||||
}
|
||||
|
||||
private getMenusFn(): LinkedWidgetConfig['getMenus'] {
|
||||
return (query, close, editorHost, inlineEditor, abortSignal) => {
|
||||
return [
|
||||
this.journalGroup(query, close, inlineEditor),
|
||||
this.linkToDocGroup(query, close, inlineEditor, abortSignal),
|
||||
this.newDocMenuGroup(query, close, editorHost, inlineEditor),
|
||||
this.memberGroup(query, close, inlineEditor, abortSignal),
|
||||
];
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user