mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-12 12:28:42 +00:00
feat(core): add tags and collections menu item (#10951)
Close [BS-2827](https://linear.app/affine-design/issue/BS-2827). 
This commit is contained in:
@@ -1,24 +1,50 @@
|
||||
import { toast } from '@affine/component';
|
||||
import { ShadowlessElement } from '@blocksuite/affine/block-std';
|
||||
import { type LinkedMenuGroup } from '@blocksuite/affine/blocks/root';
|
||||
import { SignalWatcher, WithDisposable } from '@blocksuite/affine/global/lit';
|
||||
import { scrollbarStyle } from '@blocksuite/affine/shared/styles';
|
||||
import { openFileOrFiles } from '@blocksuite/affine/shared/utils';
|
||||
import { SearchIcon, UploadIcon } from '@blocksuite/icons/lit';
|
||||
import { openFileOrFiles, type Signal } from '@blocksuite/affine/shared/utils';
|
||||
import {
|
||||
CollectionsIcon,
|
||||
SearchIcon,
|
||||
TagsIcon,
|
||||
UploadIcon,
|
||||
} from '@blocksuite/icons/lit';
|
||||
import type { DocMeta } from '@blocksuite/store';
|
||||
import { css, html } from 'lit';
|
||||
import { css, html, type TemplateResult } from 'lit';
|
||||
import { property, query, state } from 'lit/decorators.js';
|
||||
|
||||
import type { DocSearchMenuConfig } from '../chat-config';
|
||||
import type { ChatChip } from '../chat-context';
|
||||
|
||||
enum AddPopoverMode {
|
||||
Default = 'default',
|
||||
Tags = 'tags',
|
||||
Collections = 'collections',
|
||||
}
|
||||
|
||||
export type MenuGroup = {
|
||||
name: string;
|
||||
items: MenuItem[] | Signal<MenuItem[]>;
|
||||
maxDisplay?: number;
|
||||
};
|
||||
|
||||
export type MenuItem = {
|
||||
key: string;
|
||||
name: string | TemplateResult<1>;
|
||||
icon: TemplateResult<1>;
|
||||
action: MenuAction;
|
||||
suffix?: string | TemplateResult<1>;
|
||||
};
|
||||
|
||||
export type MenuAction = () => Promise<void> | void;
|
||||
|
||||
export class ChatPanelAddPopover extends SignalWatcher(
|
||||
WithDisposable(ShadowlessElement)
|
||||
) {
|
||||
static override styles = css`
|
||||
.add-popover {
|
||||
width: 280px;
|
||||
max-height: 240px;
|
||||
max-height: 450px;
|
||||
overflow-y: auto;
|
||||
border: 0.5px solid var(--affine-border-color);
|
||||
border-radius: 4px;
|
||||
@@ -76,14 +102,65 @@ export class ChatPanelAddPopover extends SignalWatcher(
|
||||
private accessor _query = '';
|
||||
|
||||
@state()
|
||||
private accessor _docGroup: LinkedMenuGroup = {
|
||||
private accessor _docGroup: MenuGroup = {
|
||||
name: 'No Result',
|
||||
items: [],
|
||||
};
|
||||
|
||||
private readonly tcGroup: MenuGroup = {
|
||||
name: 'Tag & Collection',
|
||||
items: [
|
||||
{
|
||||
key: 'tags',
|
||||
name: 'Tags',
|
||||
icon: TagsIcon(),
|
||||
action: () => {
|
||||
this._mode = AddPopoverMode.Tags;
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'collections',
|
||||
name: 'Collections',
|
||||
icon: CollectionsIcon(),
|
||||
action: () => {
|
||||
this._mode = AddPopoverMode.Collections;
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
private readonly _addFileChip = async () => {
|
||||
const file = await openFileOrFiles();
|
||||
if (!file) return;
|
||||
if (file.size > 50 * 1024 * 1024) {
|
||||
toast('You can only upload files less than 50MB');
|
||||
return;
|
||||
}
|
||||
this.addChip({
|
||||
file,
|
||||
state: 'processing',
|
||||
});
|
||||
this.abortController.abort();
|
||||
};
|
||||
|
||||
private readonly uploadGroup: MenuGroup = {
|
||||
name: 'Upload',
|
||||
items: [
|
||||
{
|
||||
key: 'files',
|
||||
name: 'Upload files (pdf, txt, csv)',
|
||||
icon: UploadIcon(),
|
||||
action: this._addFileChip,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@state()
|
||||
private accessor _activatedItemIndex = 0;
|
||||
|
||||
@state()
|
||||
private accessor _mode: AddPopoverMode = AddPopoverMode.Default;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor docSearchMenuConfig!: DocSearchMenuConfig;
|
||||
|
||||
@@ -108,68 +185,90 @@ export class ChatPanelAddPopover extends SignalWatcher(
|
||||
}
|
||||
|
||||
override render() {
|
||||
const items = Array.isArray(this._docGroup.items)
|
||||
? this._docGroup.items
|
||||
: this._docGroup.items.value;
|
||||
const groups = this._getMenuGroup();
|
||||
return html`<div class="add-popover">
|
||||
<div class="search-input-wrapper">
|
||||
${SearchIcon()}
|
||||
<input
|
||||
class="search-input"
|
||||
type="text"
|
||||
placeholder="Search Doc"
|
||||
.value=${this._query}
|
||||
@input=${this._onInput}
|
||||
/>
|
||||
</div>
|
||||
<div class="divider"></div>
|
||||
<div class="search-group" style=${this._docGroup.styles ?? ''}>
|
||||
${items.length > 0
|
||||
? items.map(({ key, name, icon, action }, curIdx) => {
|
||||
return html`<icon-button
|
||||
width="280px"
|
||||
height="30px"
|
||||
data-id=${key}
|
||||
.text=${name}
|
||||
hover=${this._activatedItemIndex === curIdx}
|
||||
@click=${() => action()?.catch(console.error)}
|
||||
@mousemove=${() => (this._activatedItemIndex = curIdx)}
|
||||
>
|
||||
${icon}
|
||||
</icon-button>`;
|
||||
})
|
||||
: html`<div class="no-result">No Result</div>`}
|
||||
</div>
|
||||
<div class="divider"></div>
|
||||
<div class="upload-wrapper">
|
||||
<icon-button
|
||||
width="280px"
|
||||
height="30px"
|
||||
data-id="upload"
|
||||
.text=${'Upload files (pdf, txt, csv)'}
|
||||
hover=${this._activatedItemIndex === items.length + 1}
|
||||
@click=${this._addFileChip}
|
||||
@mousemove=${() => (this._activatedItemIndex = items.length + 1)}
|
||||
>
|
||||
${UploadIcon()}
|
||||
</icon-button>
|
||||
</div>
|
||||
${this._renderSearchInput()} ${this._renderDivider()}
|
||||
${this._renderMenuGroup(groups)}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
private readonly _addFileChip = async () => {
|
||||
const file = await openFileOrFiles();
|
||||
if (!file) return;
|
||||
if (file.size > 50 * 1024 * 1024) {
|
||||
toast('You can only upload files less than 50MB');
|
||||
return;
|
||||
private _renderSearchInput() {
|
||||
return html`<div class="search-input-wrapper">
|
||||
${SearchIcon()}
|
||||
<input
|
||||
class="search-input"
|
||||
type="text"
|
||||
placeholder=${this._getPlaceholder()}
|
||||
.value=${this._query}
|
||||
@input=${this._onInput}
|
||||
/>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
private _getPlaceholder() {
|
||||
switch (this._mode) {
|
||||
case AddPopoverMode.Tags:
|
||||
return 'Search Tag';
|
||||
case AddPopoverMode.Collections:
|
||||
return 'Search Collection';
|
||||
default:
|
||||
return 'Search docs, tags, collections';
|
||||
}
|
||||
this.addChip({
|
||||
file,
|
||||
state: 'processing',
|
||||
}
|
||||
|
||||
private _renderDivider() {
|
||||
return html`<div class="divider"></div>`;
|
||||
}
|
||||
|
||||
private _renderMenuGroup(groups: MenuGroup[]) {
|
||||
let startIndex = 0;
|
||||
return groups.map((group, idx) => {
|
||||
const items = Array.isArray(group.items)
|
||||
? group.items
|
||||
: group.items.value;
|
||||
const menuGroup = html`<div class="menu-group">
|
||||
${this._renderMenuItems(items, startIndex)}
|
||||
${idx < groups.length - 1 ? this._renderDivider() : ''}
|
||||
</div>`;
|
||||
startIndex += items.length;
|
||||
return menuGroup;
|
||||
});
|
||||
this.abortController.abort();
|
||||
};
|
||||
}
|
||||
|
||||
private _renderMenuItems(items: MenuItem[], startIndex: number) {
|
||||
return html`<div>
|
||||
${items.length > 0
|
||||
? items.map(({ key, name, icon, action }, idx) => {
|
||||
const curIdx = startIndex + idx;
|
||||
return html`<icon-button
|
||||
width="280px"
|
||||
height="30px"
|
||||
data-id=${key}
|
||||
.text=${name}
|
||||
hover=${this._activatedItemIndex === curIdx}
|
||||
@click=${() => action()?.catch(console.error)}
|
||||
@mousemove=${() => (this._activatedItemIndex = curIdx)}
|
||||
>
|
||||
${icon}
|
||||
</icon-button>`;
|
||||
})
|
||||
: html`<div class="no-result">No Result</div>`}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
private _getMenuGroup() {
|
||||
switch (this._mode) {
|
||||
case AddPopoverMode.Tags:
|
||||
return [];
|
||||
case AddPopoverMode.Collections:
|
||||
return [];
|
||||
default:
|
||||
if (this._query) {
|
||||
return [this._docGroup, this.uploadGroup];
|
||||
}
|
||||
return [this._docGroup, this.tcGroup, this.uploadGroup];
|
||||
}
|
||||
}
|
||||
|
||||
private _onInput(event: Event) {
|
||||
this._query = (event.target as HTMLInputElement).value;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ShadowlessElement } from '@blocksuite/affine/block-std';
|
||||
import { SignalWatcher, WithDisposable } from '@blocksuite/affine/global/lit';
|
||||
import { AddCollectionIcon } from '@blocksuite/icons/lit';
|
||||
import { CollectionsIcon } from '@blocksuite/icons/lit';
|
||||
import { html } from 'lit';
|
||||
import { property } from 'lit/decorators.js';
|
||||
|
||||
@@ -17,7 +17,7 @@ export class ChatPanelCollectionChip extends SignalWatcher(
|
||||
const { state, collectionName } = this.chip;
|
||||
const isLoading = state === 'processing';
|
||||
const tooltip = getChipTooltip(state, collectionName, this.chip.tooltip);
|
||||
const collectionIcon = AddCollectionIcon();
|
||||
const collectionIcon = CollectionsIcon();
|
||||
const icon = getChipIcon(state, collectionIcon);
|
||||
|
||||
return html`<chat-panel-chip
|
||||
|
||||
Reference in New Issue
Block a user