mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-17 22:37:04 +08: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 { toast } from '@affine/component';
|
||||||
import { ShadowlessElement } from '@blocksuite/affine/block-std';
|
import { ShadowlessElement } from '@blocksuite/affine/block-std';
|
||||||
import { type LinkedMenuGroup } from '@blocksuite/affine/blocks/root';
|
|
||||||
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 } from '@blocksuite/affine/shared/utils';
|
import { openFileOrFiles, type Signal } from '@blocksuite/affine/shared/utils';
|
||||||
import { SearchIcon, UploadIcon } from '@blocksuite/icons/lit';
|
import {
|
||||||
|
CollectionsIcon,
|
||||||
|
SearchIcon,
|
||||||
|
TagsIcon,
|
||||||
|
UploadIcon,
|
||||||
|
} from '@blocksuite/icons/lit';
|
||||||
import type { DocMeta } from '@blocksuite/store';
|
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 { property, query, state } from 'lit/decorators.js';
|
||||||
|
|
||||||
import type { DocSearchMenuConfig } from '../chat-config';
|
import type { DocSearchMenuConfig } from '../chat-config';
|
||||||
import type { ChatChip } from '../chat-context';
|
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(
|
export class ChatPanelAddPopover extends SignalWatcher(
|
||||||
WithDisposable(ShadowlessElement)
|
WithDisposable(ShadowlessElement)
|
||||||
) {
|
) {
|
||||||
static override styles = css`
|
static override styles = css`
|
||||||
.add-popover {
|
.add-popover {
|
||||||
width: 280px;
|
width: 280px;
|
||||||
max-height: 240px;
|
max-height: 450px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
border: 0.5px solid var(--affine-border-color);
|
border: 0.5px solid var(--affine-border-color);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
@@ -76,14 +102,65 @@ export class ChatPanelAddPopover extends SignalWatcher(
|
|||||||
private accessor _query = '';
|
private accessor _query = '';
|
||||||
|
|
||||||
@state()
|
@state()
|
||||||
private accessor _docGroup: LinkedMenuGroup = {
|
private accessor _docGroup: MenuGroup = {
|
||||||
name: 'No Result',
|
name: 'No Result',
|
||||||
items: [],
|
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()
|
@state()
|
||||||
private accessor _activatedItemIndex = 0;
|
private accessor _activatedItemIndex = 0;
|
||||||
|
|
||||||
|
@state()
|
||||||
|
private accessor _mode: AddPopoverMode = AddPopoverMode.Default;
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
accessor docSearchMenuConfig!: DocSearchMenuConfig;
|
accessor docSearchMenuConfig!: DocSearchMenuConfig;
|
||||||
|
|
||||||
@@ -108,68 +185,90 @@ export class ChatPanelAddPopover extends SignalWatcher(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override render() {
|
override render() {
|
||||||
const items = Array.isArray(this._docGroup.items)
|
const groups = this._getMenuGroup();
|
||||||
? this._docGroup.items
|
|
||||||
: this._docGroup.items.value;
|
|
||||||
return html`<div class="add-popover">
|
return html`<div class="add-popover">
|
||||||
<div class="search-input-wrapper">
|
${this._renderSearchInput()} ${this._renderDivider()}
|
||||||
${SearchIcon()}
|
${this._renderMenuGroup(groups)}
|
||||||
<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>
|
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private readonly _addFileChip = async () => {
|
private _renderSearchInput() {
|
||||||
const file = await openFileOrFiles();
|
return html`<div class="search-input-wrapper">
|
||||||
if (!file) return;
|
${SearchIcon()}
|
||||||
if (file.size > 50 * 1024 * 1024) {
|
<input
|
||||||
toast('You can only upload files less than 50MB');
|
class="search-input"
|
||||||
return;
|
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) {
|
private _onInput(event: Event) {
|
||||||
this._query = (event.target as HTMLInputElement).value;
|
this._query = (event.target as HTMLInputElement).value;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
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 { AddCollectionIcon } from '@blocksuite/icons/lit';
|
import { CollectionsIcon } from '@blocksuite/icons/lit';
|
||||||
import { html } from 'lit';
|
import { html } from 'lit';
|
||||||
import { property } from 'lit/decorators.js';
|
import { property } from 'lit/decorators.js';
|
||||||
|
|
||||||
@@ -17,7 +17,7 @@ export class ChatPanelCollectionChip extends SignalWatcher(
|
|||||||
const { state, collectionName } = this.chip;
|
const { state, collectionName } = this.chip;
|
||||||
const isLoading = state === 'processing';
|
const isLoading = state === 'processing';
|
||||||
const tooltip = getChipTooltip(state, collectionName, this.chip.tooltip);
|
const tooltip = getChipTooltip(state, collectionName, this.chip.tooltip);
|
||||||
const collectionIcon = AddCollectionIcon();
|
const collectionIcon = CollectionsIcon();
|
||||||
const icon = getChipIcon(state, collectionIcon);
|
const icon = getChipIcon(state, collectionIcon);
|
||||||
|
|
||||||
return html`<chat-panel-chip
|
return html`<chat-panel-chip
|
||||||
|
|||||||
Reference in New Issue
Block a user