mirror of
https://github.com/wu736139669/hapi.git
synced 2026-08-05 06:24:37 +00:00
feat(web): customize composer toolbar visibility (#1298)
This commit is contained in:
@@ -37,10 +37,10 @@ export function ComposerToolbarLayoutControl() {
|
||||
|
||||
const moveDraggedItem = (group: ComposerToolbarGroup, index: number) => {
|
||||
if (draggedItem) {
|
||||
const next = layout.mode === 'split'
|
||||
const next = group === 'hidden' || layout.hidden.includes(draggedItem) || layout.mode === 'split'
|
||||
? moveComposerToolbarItem(layout, draggedItem, group, index)
|
||||
: moveComposerToolbarItemInSingleLayout(layout, draggedItem, index)
|
||||
const unchanged = next.left.join() === layout.left.join() && next.right.join() === layout.right.join()
|
||||
const unchanged = next.left.join() === layout.left.join() && next.right.join() === layout.right.join() && next.hidden.join() === layout.hidden.join()
|
||||
if (!unchanged) {
|
||||
setLayout(next)
|
||||
}
|
||||
@@ -51,9 +51,10 @@ export function ComposerToolbarLayoutControl() {
|
||||
event.preventDefault()
|
||||
const item = draggedItem ?? event.dataTransfer.getData('text/plain')
|
||||
if ((COMPOSER_TOOLBAR_ITEM_IDS as readonly string[]).includes(item)) {
|
||||
const next = layout.mode === 'split'
|
||||
? moveComposerToolbarItem(layout, item as ComposerToolbarItemId, group, index)
|
||||
: moveComposerToolbarItemInSingleLayout(layout, item as ComposerToolbarItemId, index)
|
||||
const typedItem = item as ComposerToolbarItemId
|
||||
const next = group === 'hidden' || layout.hidden.includes(typedItem) || layout.mode === 'split'
|
||||
? moveComposerToolbarItem(layout, typedItem, group, index)
|
||||
: moveComposerToolbarItemInSingleLayout(layout, typedItem, index)
|
||||
setLayout(next)
|
||||
}
|
||||
setDraggedItem(null)
|
||||
@@ -61,7 +62,7 @@ export function ComposerToolbarLayoutControl() {
|
||||
|
||||
const moveItemByOffset = (item: ComposerToolbarItemId, group: ComposerToolbarGroup, index: number, offset: -1 | 1) => {
|
||||
const targetIndex = Math.max(0, index + offset)
|
||||
const next = layout.mode === 'split'
|
||||
const next = group === 'hidden' || layout.mode === 'split'
|
||||
? moveComposerToolbarItem(layout, item, group, targetIndex)
|
||||
: moveComposerToolbarItemInSingleLayout(layout, item, targetIndex)
|
||||
setLayout(next)
|
||||
@@ -127,8 +128,10 @@ export function ComposerToolbarLayoutControl() {
|
||||
|
||||
const singleAlignment = layout.mode === 'center' ? 'justify-center' : layout.mode === 'right' ? 'justify-end' : 'justify-start'
|
||||
const singleItems = [...layout.left, ...layout.right]
|
||||
const selectedGroup: ComposerToolbarGroup = selectedItem && layout.right.includes(selectedItem) ? 'right' : 'left'
|
||||
const selectedItems = layout.mode === 'split' ? layout[selectedGroup] : singleItems
|
||||
const selectedGroup: ComposerToolbarGroup = selectedItem && layout.hidden.includes(selectedItem)
|
||||
? 'hidden'
|
||||
: selectedItem && layout.right.includes(selectedItem) ? 'right' : 'left'
|
||||
const selectedItems = selectedGroup === 'hidden' ? layout.hidden : layout.mode === 'split' ? layout[selectedGroup] : singleItems
|
||||
const selectedIndex = selectedItem ? selectedItems.indexOf(selectedItem) : -1
|
||||
|
||||
return (
|
||||
@@ -173,6 +176,22 @@ export function ComposerToolbarLayoutControl() {
|
||||
<span className="ml-1" title={t('composer.send')}><ComposerSendButtonPreview /></span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mx-3 mt-3">
|
||||
<h4 className="text-sm font-medium text-[var(--app-fg)]">{t('settings.chat.composerToolbar.hidden')}</h4>
|
||||
<div className="mt-2 rounded-xl border border-dashed border-[var(--app-border)] bg-[var(--app-subtle-bg)] px-2 py-1.5">
|
||||
{layout.hidden.length > 0
|
||||
? renderGroup('hidden', layout.hidden, 'justify-start', true)
|
||||
: (
|
||||
<div
|
||||
className="flex min-h-10 items-center justify-center text-xs text-[var(--app-hint)]"
|
||||
onDragOver={(event) => event.preventDefault()}
|
||||
onDrop={(event) => onDrop(event, 'hidden', 0)}
|
||||
>
|
||||
{t('settings.chat.composerToolbar.emptyHidden')}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{selectedItem && selectedIndex >= 0 ? (
|
||||
<div className="mx-3 mt-2 flex items-center justify-between gap-2 rounded-lg bg-[var(--app-subtle-bg)] px-3 py-2 text-sm">
|
||||
<span className="min-w-0 truncate text-[var(--app-hint)]">{t(ITEM_LABEL_KEYS[selectedItem])}</span>
|
||||
@@ -197,7 +216,28 @@ export function ComposerToolbarLayoutControl() {
|
||||
>
|
||||
→
|
||||
</button>
|
||||
{layout.mode === 'split' ? (
|
||||
{selectedGroup !== 'hidden' ? (
|
||||
<button
|
||||
type="button"
|
||||
aria-label={t('settings.chat.composerToolbar.hide')}
|
||||
title={t('settings.chat.composerToolbar.hide')}
|
||||
onClick={() => setLayout(moveComposerToolbarItem(layout, selectedItem, 'hidden', layout.hidden.length))}
|
||||
className="ml-1 rounded-lg px-2.5 py-1.5 text-xs text-[var(--app-link)] hover:bg-[var(--app-bg)]"
|
||||
>
|
||||
{t('settings.chat.composerToolbar.hide')}
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
aria-label={t('settings.chat.composerToolbar.show')}
|
||||
title={t('settings.chat.composerToolbar.show')}
|
||||
onClick={() => setLayout(moveComposerToolbarItem(layout, selectedItem, 'left', layout.left.length))}
|
||||
className="ml-1 rounded-lg px-2.5 py-1.5 text-xs text-[var(--app-link)] hover:bg-[var(--app-bg)]"
|
||||
>
|
||||
{t('settings.chat.composerToolbar.show')}
|
||||
</button>
|
||||
)}
|
||||
{layout.mode === 'split' && selectedGroup !== 'hidden' ? (
|
||||
<button
|
||||
type="button"
|
||||
aria-label={selectedGroup === 'left' ? t('settings.chat.composerToolbar.moveRight') : t('settings.chat.composerToolbar.moveLeft')}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { beforeEach, describe, expect, it } from 'vitest'
|
||||
import {
|
||||
DEFAULT_COMPOSER_TOOLBAR_LAYOUT,
|
||||
moveComposerToolbarItem,
|
||||
moveComposerToolbarItemInSingleLayout,
|
||||
normalizeComposerToolbarLayout,
|
||||
} from './useComposerToolbarLayout'
|
||||
@@ -19,6 +20,7 @@ describe('DEFAULT_COMPOSER_TOOLBAR_LAYOUT', () => {
|
||||
'schedule',
|
||||
'abort',
|
||||
])
|
||||
expect(DEFAULT_COMPOSER_TOOLBAR_LAYOUT.hidden).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -39,7 +41,30 @@ describe('normalizeComposerToolbarLayout', () => {
|
||||
expect(result.mode).toBe('split')
|
||||
expect(result.left.slice(0, 2)).toEqual(['settings', 'attachment'])
|
||||
expect(result.right).toEqual(['abort', 'schedule'])
|
||||
expect([...result.left, ...result.right]).toHaveLength(DEFAULT_COMPOSER_TOOLBAR_LAYOUT.left.length)
|
||||
expect([...result.left, ...result.right, ...result.hidden]).toHaveLength(DEFAULT_COMPOSER_TOOLBAR_LAYOUT.left.length)
|
||||
})
|
||||
|
||||
it('preserves hidden tools and keeps newly introduced tools visible', () => {
|
||||
const result = normalizeComposerToolbarLayout({
|
||||
mode: 'left',
|
||||
left: ['attachment'],
|
||||
right: [],
|
||||
hidden: ['settings', 'abort', 'settings'],
|
||||
})
|
||||
|
||||
expect(result.hidden).toEqual(['settings', 'abort'])
|
||||
expect(result.left).toContain('schedule')
|
||||
expect(result.left).not.toContain('settings')
|
||||
})
|
||||
|
||||
it('moves tools between visible and hidden groups without duplication', () => {
|
||||
const hidden = moveComposerToolbarItem(DEFAULT_COMPOSER_TOOLBAR_LAYOUT, 'terminal', 'hidden', 0)
|
||||
expect(hidden.hidden).toEqual(['terminal'])
|
||||
expect(hidden.left).not.toContain('terminal')
|
||||
|
||||
const visible = moveComposerToolbarItem(hidden, 'terminal', 'right', 0)
|
||||
expect(visible.hidden).toEqual([])
|
||||
expect(visible.right).toEqual(['terminal'])
|
||||
})
|
||||
|
||||
it('reorders across a hidden split boundary in single-column modes', () => {
|
||||
|
||||
@@ -15,12 +15,13 @@ export const COMPOSER_TOOLBAR_ITEM_IDS = [
|
||||
|
||||
export type ComposerToolbarItemId = typeof COMPOSER_TOOLBAR_ITEM_IDS[number]
|
||||
export type ComposerToolbarLayoutMode = 'left' | 'center' | 'right' | 'split'
|
||||
export type ComposerToolbarGroup = 'left' | 'right'
|
||||
export type ComposerToolbarGroup = 'left' | 'right' | 'hidden'
|
||||
|
||||
export type ComposerToolbarLayout = {
|
||||
mode: ComposerToolbarLayoutMode
|
||||
left: ComposerToolbarItemId[]
|
||||
right: ComposerToolbarItemId[]
|
||||
hidden: ComposerToolbarItemId[]
|
||||
}
|
||||
|
||||
export const DEFAULT_COMPOSER_TOOLBAR_LAYOUT: ComposerToolbarLayout = {
|
||||
@@ -30,6 +31,7 @@ export const DEFAULT_COMPOSER_TOOLBAR_LAYOUT: ComposerToolbarLayout = {
|
||||
'abort',
|
||||
],
|
||||
right: [],
|
||||
hidden: [],
|
||||
}
|
||||
|
||||
export function moveComposerToolbarItem(
|
||||
@@ -40,9 +42,10 @@ export function moveComposerToolbarItem(
|
||||
): ComposerToolbarLayout {
|
||||
const left = layout.left.filter((entry) => entry !== item)
|
||||
const right = layout.right.filter((entry) => entry !== item)
|
||||
const target = targetGroup === 'left' ? left : right
|
||||
const hidden = layout.hidden.filter((entry) => entry !== item)
|
||||
const target = targetGroup === 'left' ? left : targetGroup === 'right' ? right : hidden
|
||||
target.splice(Math.max(0, Math.min(targetIndex, target.length)), 0, item)
|
||||
return { ...layout, left, right }
|
||||
return { ...layout, left, right, hidden }
|
||||
}
|
||||
|
||||
export function moveComposerToolbarItemInSingleLayout(
|
||||
@@ -52,11 +55,13 @@ export function moveComposerToolbarItemInSingleLayout(
|
||||
): ComposerToolbarLayout {
|
||||
const leftCount = layout.left.length
|
||||
const items = [...layout.left, ...layout.right].filter((entry) => entry !== item)
|
||||
const hidden = layout.hidden.filter((entry) => entry !== item)
|
||||
items.splice(Math.max(0, Math.min(targetIndex, items.length)), 0, item)
|
||||
return {
|
||||
...layout,
|
||||
left: items.slice(0, leftCount),
|
||||
right: items.slice(leftCount),
|
||||
hidden,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,6 +98,7 @@ export function normalizeComposerToolbarLayout(value: unknown): ComposerToolbarL
|
||||
|
||||
const left = normalizeGroup(candidate.left)
|
||||
const right = normalizeGroup(candidate.right)
|
||||
const hidden = normalizeGroup(candidate.hidden)
|
||||
for (const item of COMPOSER_TOOLBAR_ITEM_IDS) {
|
||||
if (!seen.has(item)) {
|
||||
left.push(item)
|
||||
@@ -103,6 +109,7 @@ export function normalizeComposerToolbarLayout(value: unknown): ComposerToolbarL
|
||||
mode: isLayoutMode(candidate.mode) ? candidate.mode : DEFAULT_COMPOSER_TOOLBAR_LAYOUT.mode,
|
||||
left,
|
||||
right,
|
||||
hidden,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -725,12 +725,16 @@ export default {
|
||||
'settings.chat.composerToolbar.description': 'Choose how toolbar actions are arranged below the message input.',
|
||||
'settings.chat.composerToolbar.layout': 'Alignment',
|
||||
'settings.chat.composerToolbar.order': 'Tool order',
|
||||
'settings.chat.composerToolbar.hidden': 'Hidden tools',
|
||||
'settings.chat.composerToolbar.emptyHidden': 'Drag tools here to hide them',
|
||||
'settings.chat.composerToolbar.hide': 'Hide',
|
||||
'settings.chat.composerToolbar.show': 'Show',
|
||||
'settings.chat.composerToolbar.layout.left': 'Left',
|
||||
'settings.chat.composerToolbar.layout.center': 'Center',
|
||||
'settings.chat.composerToolbar.layout.right': 'Right',
|
||||
'settings.chat.composerToolbar.layout.split': 'Split',
|
||||
'settings.chat.composerToolbar.dragHint': 'Drag actions to reorder them. In split layout, drag between the two groups.',
|
||||
'settings.chat.composerToolbar.previewHint': 'Drag the actual toolbar icons below. Changes are reflected in this preview immediately; hover an icon to see its action.',
|
||||
'settings.chat.composerToolbar.previewHint': 'Drag the actual toolbar icons below to reorder them, or drag them to Hidden tools. Changes appear immediately; hover an icon to see its action.',
|
||||
'settings.chat.composerToolbar.reset': 'Reset',
|
||||
'settings.chat.composerToolbar.leftGroup': 'Left group',
|
||||
'settings.chat.composerToolbar.rightGroup': 'Right group',
|
||||
|
||||
@@ -729,12 +729,16 @@ export default {
|
||||
'settings.chat.composerToolbar.description': '设置消息输入框底部工具的排列方式。',
|
||||
'settings.chat.composerToolbar.layout': '对齐方式',
|
||||
'settings.chat.composerToolbar.order': '工具顺序',
|
||||
'settings.chat.composerToolbar.hidden': '隐藏工具',
|
||||
'settings.chat.composerToolbar.emptyHidden': '将工具拖到这里隐藏',
|
||||
'settings.chat.composerToolbar.hide': '隐藏',
|
||||
'settings.chat.composerToolbar.show': '显示',
|
||||
'settings.chat.composerToolbar.layout.left': '左对齐',
|
||||
'settings.chat.composerToolbar.layout.center': '居中',
|
||||
'settings.chat.composerToolbar.layout.right': '右对齐',
|
||||
'settings.chat.composerToolbar.layout.split': '左右分组',
|
||||
'settings.chat.composerToolbar.dragHint': '拖动操作项可调整顺序;左右分组布局下也可在两组之间拖动。',
|
||||
'settings.chat.composerToolbar.previewHint': '直接拖动下方的实际工具图标,预览会立即更新;鼠标悬停可查看功能名称。',
|
||||
'settings.chat.composerToolbar.previewHint': '直接拖动下方的实际工具图标调整顺序,或拖到隐藏工具;预览会立即更新,鼠标悬停可查看功能名称。',
|
||||
'settings.chat.composerToolbar.reset': '恢复默认',
|
||||
'settings.chat.composerToolbar.leftGroup': '左侧分组',
|
||||
'settings.chat.composerToolbar.rightGroup': '右侧分组',
|
||||
|
||||
Reference in New Issue
Block a user