mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-17 22:37:04 +08:00
feat(editor): add at member highlight (#12535)
Closes: BS-2896 <img width="468" alt="image" src="https://github.com/user-attachments/assets/2b84c484-29b8-4650-b74c-da7afd3a1e41" /> <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Enhanced member search results with highlighted text, making it easier to visually identify matched parts of member names during searches. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
@@ -31,8 +31,11 @@ import {
|
|||||||
import { computed, Signal } from '@preact/signals-core';
|
import { computed, Signal } from '@preact/signals-core';
|
||||||
import { Service } from '@toeverything/infra';
|
import { Service } from '@toeverything/infra';
|
||||||
import { cssVarV2 } from '@toeverything/theme/v2';
|
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||||
|
import type { FuseResultMatch } from 'fuse.js';
|
||||||
|
import Fuse from 'fuse.js';
|
||||||
import { html } from 'lit';
|
import { html } from 'lit';
|
||||||
import { styleMap } from 'lit/directives/style-map.js';
|
import { styleMap } from 'lit/directives/style-map.js';
|
||||||
|
import { unsafeHTML } from 'lit/directives/unsafe-html.js';
|
||||||
import {
|
import {
|
||||||
createAbsolutePositionFromRelativePosition,
|
createAbsolutePositionFromRelativePosition,
|
||||||
createRelativePositionFromTypeIndex,
|
createRelativePositionFromTypeIndex,
|
||||||
@@ -46,6 +49,7 @@ import { type JournalService, suggestJournalDate } from '../../journal';
|
|||||||
import { NotificationService } from '../../notification';
|
import { NotificationService } from '../../notification';
|
||||||
import type { GuardService, MemberSearchService } from '../../permissions';
|
import type { GuardService, MemberSearchService } from '../../permissions';
|
||||||
import type { DocGrantedUsersService } from '../../permissions/services/doc-granted-users';
|
import type { DocGrantedUsersService } from '../../permissions/services/doc-granted-users';
|
||||||
|
import { highlighter } from '../../quicksearch/utils/highlighter';
|
||||||
import type { SearchMenuService } from '../../search-menu/services';
|
import type { SearchMenuService } from '../../search-menu/services';
|
||||||
|
|
||||||
function resolveSignal<T>(data: T | Signal<T>): T {
|
function resolveSignal<T>(data: T | Signal<T>): T {
|
||||||
@@ -327,6 +331,32 @@ export class AtMenuConfigService extends Service {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private 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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
private memberGroup(
|
private memberGroup(
|
||||||
query: string,
|
query: string,
|
||||||
close: () => void,
|
close: () => void,
|
||||||
@@ -350,9 +380,10 @@ export class AtMenuConfigService extends Service {
|
|||||||
? html`<img style=${avatarStyle} src="${avatar}" />`
|
? html`<img style=${avatarStyle} src="${avatar}" />`
|
||||||
: UserIcon();
|
: UserIcon();
|
||||||
|
|
||||||
|
let displayName = name ?? 'Unknown';
|
||||||
return {
|
return {
|
||||||
key: id,
|
key: id,
|
||||||
name: name ?? 'Unknown',
|
name: html`${unsafeHTML(displayName)}`,
|
||||||
icon,
|
icon,
|
||||||
action: () => {
|
action: () => {
|
||||||
const root = inlineEditor.rootElement;
|
const root = inlineEditor.rootElement;
|
||||||
@@ -567,15 +598,34 @@ export class AtMenuConfigService extends Service {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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 [
|
return [
|
||||||
...members.map(member =>
|
...searchResults.map(result => {
|
||||||
getMenuItem(
|
const member = result.item;
|
||||||
member.id,
|
const displayName = this.highlightFuseTitle(
|
||||||
member.name,
|
result.matches,
|
||||||
member.avatarUrl,
|
member.name ?? 'Unknown',
|
||||||
member.id !== currentUser?.id
|
'name'
|
||||||
)
|
);
|
||||||
),
|
return {
|
||||||
|
...getMenuItem(
|
||||||
|
member.id,
|
||||||
|
member.name,
|
||||||
|
member.avatarUrl,
|
||||||
|
member.id !== currentUser?.id
|
||||||
|
),
|
||||||
|
name: html`${unsafeHTML(displayName)}`,
|
||||||
|
};
|
||||||
|
}),
|
||||||
...(canUserManage ? [inviteItem] : []),
|
...(canUserManage ? [inviteItem] : []),
|
||||||
];
|
];
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user