feat(core): hybird search for docs, tags and collections (#11008)

Close [BS-2466](https://linear.app/affine-design/issue/BS-2466).

![截屏2025-03-19 18.25.38.png](https://graphite-user-uploaded-assets-prod.s3.amazonaws.com/sJGviKxfE3Ap685cl5bj/15ba2b7d-2d91-408a-8e7d-93f845017bcb.png)
This commit is contained in:
akumatus
2025-03-20 08:21:25 +00:00
parent 03364f9d03
commit 6ff19ca307

View File

@@ -6,7 +6,7 @@ import type {
import { ShadowlessElement } from '@blocksuite/affine/block-std'; import { ShadowlessElement } from '@blocksuite/affine/block-std';
import { SignalWatcher, WithDisposable } from '@blocksuite/affine/global/lit'; import { SignalWatcher, WithDisposable } from '@blocksuite/affine/global/lit';
import { scrollbarStyle } from '@blocksuite/affine/shared/styles'; import { scrollbarStyle } from '@blocksuite/affine/shared/styles';
import { openFileOrFiles, type Signal } from '@blocksuite/affine/shared/utils'; import { openFileOrFiles } from '@blocksuite/affine/shared/utils';
import { import {
CollectionsIcon, CollectionsIcon,
MoreHorizontalIcon, MoreHorizontalIcon,
@@ -15,6 +15,7 @@ import {
UploadIcon, UploadIcon,
} from '@blocksuite/icons/lit'; } from '@blocksuite/icons/lit';
import type { DocMeta } from '@blocksuite/store'; import type { DocMeta } from '@blocksuite/store';
import { Signal } from '@preact/signals-core';
import { css, html, type TemplateResult } from 'lit'; import { css, html, type TemplateResult } from 'lit';
import { property, query, state } from 'lit/decorators.js'; import { property, query, state } from 'lit/decorators.js';
import { repeat } from 'lit/directives/repeat.js'; import { repeat } from 'lit/directives/repeat.js';
@@ -33,6 +34,8 @@ export type MenuGroup = {
items: MenuItem[] | Signal<MenuItem[]>; items: MenuItem[] | Signal<MenuItem[]>;
maxDisplay?: number; maxDisplay?: number;
overflowText?: string | Signal<string>; overflowText?: string | Signal<string>;
divider?: TemplateResult<1>;
noResult?: TemplateResult<1>;
}; };
export type MenuItem = { export type MenuItem = {
@@ -45,6 +48,10 @@ export type MenuItem = {
export type MenuAction = () => Promise<void> | void; export type MenuAction = () => Promise<void> | void;
export function resolveSignal<T>(data: T | Signal<T>): T {
return data instanceof Signal ? data.value : data;
}
export class ChatPanelAddPopover extends SignalWatcher( export class ChatPanelAddPopover extends SignalWatcher(
WithDisposable(ShadowlessElement) WithDisposable(ShadowlessElement)
) { ) {
@@ -117,10 +124,7 @@ export class ChatPanelAddPopover extends SignalWatcher(
private accessor _query = ''; private accessor _query = '';
@state() @state()
private accessor _searchGroup: MenuGroup = { private accessor _searchGroups: MenuGroup[] = [];
name: 'No Result',
items: [],
};
private readonly _toggleMode = (mode: AddPopoverMode) => { private readonly _toggleMode = (mode: AddPopoverMode) => {
this._mode = mode; this._mode = mode;
@@ -189,22 +193,22 @@ export class ChatPanelAddPopover extends SignalWatcher(
switch (this._mode) { switch (this._mode) {
case AddPopoverMode.Tags: case AddPopoverMode.Tags:
groups = [this._searchGroup]; groups = this._searchGroups;
break; break;
case AddPopoverMode.Collections: case AddPopoverMode.Collections:
groups = [this._searchGroup]; groups = this._searchGroups;
break; break;
default: default:
if (this._query) { if (this._query) {
groups = [this._searchGroup, this.uploadGroup]; groups = [...this._searchGroups, this.uploadGroup];
} else { } else {
groups = [this._searchGroup, this.tcGroup, this.uploadGroup]; groups = [...this._searchGroups, this.tcGroup, this.uploadGroup];
} }
} }
// Process maxDisplay for each group // Process maxDisplay for each group
return groups.map(group => { return groups.map(group => {
let items = Array.isArray(group.items) ? group.items : group.items.value; let items = resolveSignal(group.items);
const maxDisplay = group.maxDisplay ?? items.length; const maxDisplay = group.maxDisplay ?? items.length;
const hasMore = items.length > maxDisplay; const hasMore = items.length > maxDisplay;
if (!hasMore) { if (!hasMore) {
@@ -216,10 +220,7 @@ export class ChatPanelAddPopover extends SignalWatcher(
...items.slice(0, maxDisplay), ...items.slice(0, maxDisplay),
{ {
key: `${group.name} More`, key: `${group.name} More`,
name: name: resolveSignal(group.overflowText) ?? 'more',
typeof group.overflowText === 'string'
? group.overflowText
: (group.overflowText?.value ?? 'more'),
icon: MoreHorizontalIcon(), icon: MoreHorizontalIcon(),
action: () => { action: () => {
this._resetMaxDisplay(group); this._resetMaxDisplay(group);
@@ -233,7 +234,7 @@ export class ChatPanelAddPopover extends SignalWatcher(
private get _flattenMenuGroup() { private get _flattenMenuGroup() {
return this._menuGroup.flatMap(group => { return this._menuGroup.flatMap(group => {
return Array.isArray(group.items) ? group.items : group.items.value; return resolveSignal(group.items);
}); });
} }
@@ -293,9 +294,9 @@ export class ChatPanelAddPopover extends SignalWatcher(
private _getPlaceholder() { private _getPlaceholder() {
switch (this._mode) { switch (this._mode) {
case AddPopoverMode.Tags: case AddPopoverMode.Tags:
return 'Search Tag'; return 'Search tags';
case AddPopoverMode.Collections: case AddPopoverMode.Collections:
return 'Search Collection'; return 'Search collections';
default: default:
return 'Search docs, tags, collections'; return 'Search docs, tags, collections';
} }
@@ -305,19 +306,25 @@ export class ChatPanelAddPopover extends SignalWatcher(
return html`<div class="divider"></div>`; return html`<div class="divider"></div>`;
} }
private _renderNoResult() {
return html`<div class="no-result">No Result</div>`;
}
private _renderMenuGroup(groups: MenuGroup[]) { private _renderMenuGroup(groups: MenuGroup[]) {
let startIndex = 0; let startIndex = 0;
return repeat( return repeat(
groups, groups,
group => group.name, group => group.name,
(group, idx) => { (group, idx) => {
const items = Array.isArray(group.items) const items = resolveSignal(group.items);
? group.items
: group.items.value;
const menuGroup = html`<div class="menu-group"> const menuGroup = html`<div class="menu-group">
${this._renderMenuItems(items, startIndex)} ${items.length > 0
${idx < groups.length - 1 ? this._renderDivider() : ''} ? this._renderMenuItems(items, startIndex)
: (group.noResult ?? this._renderNoResult())}
${idx < groups.length - 1
? (group.divider ?? this._renderDivider())
: ''}
</div>`; </div>`;
startIndex += items.length; startIndex += items.length;
return menuGroup; return menuGroup;
@@ -327,28 +334,26 @@ export class ChatPanelAddPopover extends SignalWatcher(
private _renderMenuItems(items: MenuItem[], startIndex: number) { private _renderMenuItems(items: MenuItem[], startIndex: number) {
return html`<div class="menu-items"> return html`<div class="menu-items">
${items.length > 0 ${repeat(
? repeat( items,
items, item => item.key,
item => item.key, ({ key, name, icon, action, suffix }, idx) => {
({ key, name, icon, action, suffix }, idx) => { const curIdx = startIndex + idx;
const curIdx = startIndex + idx; return html`<icon-button
return html`<icon-button width="280px"
width="280px" height="30px"
height="30px" data-id=${key}
data-id=${key} data-index=${curIdx}
data-index=${curIdx} .text=${name}
.text=${name} hover=${this._activatedIndex === curIdx}
hover=${this._activatedIndex === curIdx} @click=${() => action()?.catch(console.error)}
@click=${() => action()?.catch(console.error)} @mousemove=${() => (this._activatedIndex = curIdx)}
@mousemove=${() => (this._activatedIndex = curIdx)} >
> ${icon}
${icon} ${suffix ? html`<div class="item-suffix">${suffix}</div>` : ''}
${suffix ? html`<div class="item-suffix">${suffix}</div>` : ''} </icon-button>`;
</icon-button>`; }
} )}
)
: html`<div class="no-result">No Result</div>`}
</div>`; </div>`;
} }
@@ -365,26 +370,78 @@ export class ChatPanelAddPopover extends SignalWatcher(
private _updateSearchGroup() { private _updateSearchGroup() {
switch (this._mode) { switch (this._mode) {
case AddPopoverMode.Tags: case AddPopoverMode.Tags: {
this._searchGroup = this.searchMenuConfig.getTagMenuGroup( this._searchGroups = [
this._query, this.searchMenuConfig.getTagMenuGroup(
this._addTagChip, this._query,
this.abortController.signal this._addTagChip,
); this.abortController.signal
),
];
break; break;
case AddPopoverMode.Collections: }
this._searchGroup = this.searchMenuConfig.getCollectionMenuGroup( case AddPopoverMode.Collections: {
this._query, this._searchGroups = [
this._addCollectionChip, this.searchMenuConfig.getCollectionMenuGroup(
this.abortController.signal this._query,
); this._addCollectionChip,
this.abortController.signal
),
];
break; break;
default: }
this._searchGroup = this.searchMenuConfig.getDocMenuGroup( default: {
const docGroup = this.searchMenuConfig.getDocMenuGroup(
this._query, this._query,
this._addDocChip, this._addDocChip,
this.abortController.signal this.abortController.signal
); );
if (!this._query) {
this._searchGroups = [docGroup];
} else {
const tagGroup = this.searchMenuConfig.getTagMenuGroup(
this._query,
this._addTagChip,
this.abortController.signal
);
const collectionGroup = this.searchMenuConfig.getCollectionMenuGroup(
this._query,
this._addCollectionChip,
this.abortController.signal
);
const hasNoResult =
resolveSignal(docGroup.items).length === 0 &&
resolveSignal(tagGroup.items).length === 0 &&
resolveSignal(collectionGroup.items).length === 0;
if (hasNoResult) {
this._searchGroups = [
{
name: 'No Result',
items: [],
},
];
} else {
const nothing = html``;
this._searchGroups = [
{
...docGroup,
divider: nothing,
noResult: nothing,
},
{
...tagGroup,
divider: nothing,
noResult: nothing,
},
{
...collectionGroup,
noResult: nothing,
},
];
}
}
break;
}
} }
} }