fix: simplify knowledge action menus
Remove repeated document names from action labels, shorten the reparse label, and narrow knowledge menus to 220px while preserving other menu widths and viewport bounds. Keep object context on menu triggers and removal confirmations. Validation: 72 focused and Electron layout tests, typecheck, lint, and diff checks passed. Release note: 知识库操作菜单去掉重复文件名,将操作简写为重新解析,并收窄菜单宽度,减少换行。
This commit is contained in:
@@ -3,10 +3,11 @@ import { flushSync } from 'react-dom'
|
||||
import { FloatingPortal } from './FloatingPortal'
|
||||
import './anchored-menu.css'
|
||||
|
||||
export function AnchoredMenu({ anchorRef, id, label, onClose, children }: {
|
||||
export function AnchoredMenu({ anchorRef, id, label, width: preferredWidth = 280, onClose, children }: {
|
||||
anchorRef: RefObject<HTMLButtonElement | null>
|
||||
id: string
|
||||
label: string
|
||||
width?: number
|
||||
onClose: () => void
|
||||
children: ReactNode
|
||||
}): React.JSX.Element {
|
||||
@@ -22,7 +23,7 @@ export function AnchoredMenu({ anchorRef, id, label, onClose, children }: {
|
||||
const width = viewport?.width ?? window.innerWidth
|
||||
const height = viewport?.height ?? window.innerHeight
|
||||
const rect = anchor.getBoundingClientRect()
|
||||
const menuWidth = Math.max(0, Math.min(280, width - 32))
|
||||
const menuWidth = Math.max(0, Math.min(preferredWidth, width - 32))
|
||||
menu.style.width = `${menuWidth}px`
|
||||
const above = Math.max(0, Math.min(height - 32, rect.top - top - 24))
|
||||
const below = Math.max(0, Math.min(height - 32, top + height - rect.bottom - 24))
|
||||
|
||||
@@ -281,14 +281,15 @@ describe('KnowledgeWorkspace', () => {
|
||||
}
|
||||
const trigger = within(rows[1]!).getByRole('button', { name: 'Guide 的更多操作' })
|
||||
fireEvent.click(trigger)
|
||||
expect(screen.getByRole('menuitem', { name: '打开 Guide 的原始来源' })).toHaveFocus()
|
||||
expect(screen.getByRole('menu')).toHaveStyle({ width: '220px' })
|
||||
expect(screen.getByRole('menuitem', { name: '打开来源' })).toHaveFocus()
|
||||
fireEvent.keyDown(document.activeElement!, { key: 'End' })
|
||||
expect(screen.getByRole('menuitem', { name: '移除来源 Guide' })).toHaveFocus()
|
||||
expect(screen.getByRole('menuitem', { name: '移除来源' })).toHaveFocus()
|
||||
fireEvent.keyDown(document.activeElement!, { key: 'Escape' })
|
||||
expect(trigger).toHaveFocus()
|
||||
expect(screen.queryByRole('menu')).not.toBeInTheDocument()
|
||||
fireEvent.click(trigger)
|
||||
fireEvent.click(screen.getByRole('menuitem', { name: '打开 Guide 的原始来源' }))
|
||||
fireEvent.click(screen.getByRole('menuitem', { name: '打开来源' }))
|
||||
await waitFor(() => expect(props.onOpenDocumentSource).toHaveBeenCalledWith(library.id, 'doc-1'))
|
||||
fireEvent.change(screen.getByRole('searchbox', { name: '搜索文档与来源' }), { target: { value: 'edition=two' } })
|
||||
expect(within(table).getAllByRole('row')).toHaveLength(2)
|
||||
@@ -354,10 +355,10 @@ describe('KnowledgeWorkspace', () => {
|
||||
expect(screen.getAllByText('manual.pdf')).toHaveLength(1)
|
||||
const openMenu = (): void => { fireEvent.click(screen.getByRole('button', { name: 'manual.pdf 的更多操作' })) }
|
||||
openMenu()
|
||||
fireEvent.click(screen.getByRole('menuitem', { name: '同步 manual.pdf' }))
|
||||
fireEvent.click(screen.getByRole('menuitem', { name: '同步' }))
|
||||
await waitFor(() => expect(props.onSyncSource).toHaveBeenCalledWith(source.id))
|
||||
openMenu()
|
||||
fireEvent.click(screen.getByRole('menuitem', { name: '使用当前设置重新解析' }))
|
||||
fireEvent.click(screen.getByRole('menuitem', { name: '重新解析' }))
|
||||
await waitFor(() => expect(props.onRebuildDocument).toHaveBeenCalledWith(library.id, document.id))
|
||||
for (const [status, name, callback] of [
|
||||
['syncing', '暂停', props.onPauseSource],
|
||||
@@ -366,7 +367,7 @@ describe('KnowledgeWorkspace', () => {
|
||||
] as const) {
|
||||
view.rerender(<KnowledgeWorkspace {...props} sources={[{ ...source, name: document.name, status }]} />)
|
||||
openMenu()
|
||||
fireEvent.click(screen.getByRole('menuitem', { name: `${name} manual.pdf` }))
|
||||
fireEvent.click(screen.getByRole('menuitem', { name }))
|
||||
await waitFor(() => {
|
||||
expect(callback).toHaveBeenCalledWith(source.id)
|
||||
expect(screen.queryByRole('menu')).not.toBeInTheDocument()
|
||||
@@ -408,9 +409,9 @@ describe('KnowledgeWorkspace', () => {
|
||||
expect(within(table).getAllByRole('row')).toHaveLength(8)
|
||||
expect(within(table).getByText('orphan.md')).toBeVisible()
|
||||
expect(within(table).getByText('unassigned.md')).toBeVisible()
|
||||
fireEvent.click(screen.getByRole('button', { name: '重试 failed.pdf' }))
|
||||
fireEvent.click(screen.getByRole('button', { name: '重试' }))
|
||||
await waitFor(() => expect(props.onRetrySource).toHaveBeenCalledWith('empty'))
|
||||
fireEvent.click(screen.getByRole('button', { name: '暂停 loading.pdf' }))
|
||||
fireEvent.click(screen.getByRole('button', { name: '暂停' }))
|
||||
await waitFor(() => expect(props.onPauseSource).toHaveBeenCalledWith('syncing'))
|
||||
const search = screen.getByRole('searchbox', { name: '搜索文档与来源' })
|
||||
fireEvent.change(search, { target: { value: '产品手册' } })
|
||||
@@ -419,7 +420,7 @@ describe('KnowledgeWorkspace', () => {
|
||||
expect(within(table).getAllByRole('row')).toHaveLength(3)
|
||||
expect(within(table).getByText('产品手册')).toBeVisible()
|
||||
fireEvent.click(screen.getByRole('button', { name: '产品手册 的更多操作' }))
|
||||
fireEvent.click(screen.getByRole('menuitem', { name: '移除来源 产品手册' }))
|
||||
fireEvent.click(screen.getByRole('menuitem', { name: '移除来源' }))
|
||||
expect(screen.getByRole('alertdialog')).toHaveAccessibleDescription(
|
||||
'将删除此来源的 2 篇文档、检索索引、图谱证据和应用托管副本。磁盘上的原始文件不会改变。'
|
||||
)
|
||||
@@ -515,8 +516,8 @@ describe('KnowledgeWorkspace', () => {
|
||||
}
|
||||
fireEvent.click(screen.getByRole('button', { name: 'A.md 的更多操作' }))
|
||||
if (entry === 'preview') {
|
||||
expect(screen.getByRole('menuitem', { name: '同步 产品手册' })).toBeEnabled()
|
||||
expect(screen.getByRole('menuitem', { name: '移除来源 产品手册' })).toBeEnabled()
|
||||
expect(screen.getByRole('menuitem', { name: '同步' })).toBeEnabled()
|
||||
expect(screen.getByRole('menuitem', { name: '移除来源' })).toBeEnabled()
|
||||
}
|
||||
fireEvent.click(screen.getByRole('menuitem', { name: '查看任务' }))
|
||||
expect(screen.getByText('共 1 个任务批次')).toBeVisible()
|
||||
@@ -545,14 +546,14 @@ describe('KnowledgeWorkspace', () => {
|
||||
})
|
||||
render(<KnowledgeWorkspace {...props} />)
|
||||
const aMenuName = operation === 'sync' ? 'Source A 的更多操作' : 'A.md 的更多操作'
|
||||
const aActionName = operation === 'sync' ? '同步 Source A' : '使用当前设置重新解析'
|
||||
const aActionName = operation === 'sync' ? '同步' : '重新解析'
|
||||
const checkPending = (aPending: boolean, bPending: boolean): void => {
|
||||
fireEvent.click(screen.getByRole('button', { name: aMenuName }))
|
||||
expect(screen.getByRole('menuitem', { name: aActionName })).toHaveProperty('disabled', aPending)
|
||||
fireEvent.keyDown(document.activeElement!, { key: 'Escape' })
|
||||
expect(screen.getByRole('button', { name: '暂停 Source B' })).toHaveProperty('disabled', bPending)
|
||||
expect(screen.getByRole('button', { name: '暂停' })).toHaveProperty('disabled', bPending)
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Source B 的更多操作' }))
|
||||
expect(screen.getByRole('menuitem', { name: '暂停 Source B' })).toHaveProperty('disabled', bPending)
|
||||
expect(screen.getByRole('menuitem', { name: '暂停' })).toHaveProperty('disabled', bPending)
|
||||
fireEvent.keyDown(document.activeElement!, { key: 'Escape' })
|
||||
}
|
||||
fireEvent.click(screen.getByRole('button', { name: aMenuName }))
|
||||
@@ -561,10 +562,10 @@ describe('KnowledgeWorkspace', () => {
|
||||
else expect(props.onRebuildDocument).toHaveBeenCalledWith(library.id, 'document-1')
|
||||
checkPending(true, false)
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Source B 的更多操作' }))
|
||||
fireEvent.click(screen.getByRole('menuitem', { name: '暂停 Source B' }))
|
||||
fireEvent.click(screen.getByRole('menuitem', { name: '暂停' }))
|
||||
expect(props.onPauseSource).toHaveBeenCalledExactlyOnceWith('source-b')
|
||||
checkPending(true, true)
|
||||
fireEvent.click(screen.getByRole('button', { name: '暂停 Source B' }))
|
||||
fireEvent.click(screen.getByRole('button', { name: '暂停' }))
|
||||
expect(props.onPauseSource).toHaveBeenCalledTimes(1)
|
||||
if (completion === 'a-first') {
|
||||
await act(async () => { resolveA(); await a })
|
||||
@@ -597,15 +598,15 @@ describe('KnowledgeWorkspace', () => {
|
||||
expect(screen.getByText('Preview saved-result')).toBeVisible()
|
||||
})
|
||||
fireEvent.click(screen.getByRole('button', { name: '架构说明.md 的更多操作' }))
|
||||
fireEvent.click(screen.getByRole('menuitem', { name: '使用当前设置重新解析' }))
|
||||
fireEvent.click(screen.getByRole('menuitem', { name: '重新解析' }))
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole('alert')).toHaveTextContent('Parser unavailable')
|
||||
expect(screen.getByText('Preview saved-result')).toBeVisible()
|
||||
expect(screen.queryByRole('status')).not.toBeInTheDocument()
|
||||
})
|
||||
fireEvent.click(screen.getByRole('button', { name: '架构说明.md 的更多操作' }))
|
||||
expect(screen.getByRole('menuitem', { name: '使用当前设置重新解析' })).toBeEnabled()
|
||||
fireEvent.click(screen.getByRole('menuitem', { name: '移除来源 产品手册' }))
|
||||
expect(screen.getByRole('menuitem', { name: '重新解析' })).toBeEnabled()
|
||||
fireEvent.click(screen.getByRole('menuitem', { name: '移除来源' }))
|
||||
expect(screen.getByRole('alertdialog')).toBeVisible()
|
||||
fireEvent.click(screen.getByRole('button', { name: '取消' }))
|
||||
expect(screen.getByText('Preview saved-result')).toBeVisible()
|
||||
@@ -735,7 +736,7 @@ describe('KnowledgeWorkspace', () => {
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '产品手册 的更多操作' }))
|
||||
fireEvent.click(screen.getByRole('menuitem', { name: '移除来源 产品手册' }))
|
||||
fireEvent.click(screen.getByRole('menuitem', { name: '移除来源' }))
|
||||
expect(
|
||||
screen.getByRole('alertdialog', {
|
||||
name: '移除来源“产品手册”?'
|
||||
@@ -1842,11 +1843,11 @@ describe('KnowledgeWorkspace', () => {
|
||||
.toBeInTheDocument()
|
||||
expect(screen.queryByTitle('D:\\Private\\产品手册')).not.toBeInTheDocument()
|
||||
const syncSource = screen.getByRole('button', {
|
||||
name: '同步 产品手册'
|
||||
name: '同步'
|
||||
})
|
||||
fireEvent.click(screen.getByRole('button', { name: '产品手册 的更多操作' }))
|
||||
const removeSource = screen.getByRole('menuitem', {
|
||||
name: '移除来源 产品手册'
|
||||
name: '移除来源'
|
||||
})
|
||||
expect(syncSource.parentElement).toHaveClass('knowledge-document-actions')
|
||||
expect(removeSource.closest('[role="menu"]')).toBeInTheDocument()
|
||||
|
||||
@@ -72,6 +72,7 @@ export const knowledge = {
|
||||
sync: 'Sync',
|
||||
remove: 'Remove',
|
||||
openSource: 'Open source',
|
||||
removeSource: 'Remove source',
|
||||
retryDocument: 'Process again',
|
||||
useInChat: 'Use in this conversation',
|
||||
saveEntity: 'Save entity',
|
||||
@@ -601,7 +602,7 @@ export const knowledge = {
|
||||
lastSynced: 'Last synced: {{time}}',
|
||||
moreActionsFor: 'More actions for {{name}}',
|
||||
openParsedOriginal: 'Open original used for parsing',
|
||||
reparse: 'Reparse with current settings',
|
||||
reparse: 'Reparse',
|
||||
viewResult: 'View parsed result',
|
||||
previewLabel: 'Parsed result: {{name}}',
|
||||
backToList: 'Back to document list',
|
||||
|
||||
@@ -69,6 +69,7 @@ export const knowledge = {
|
||||
sync: '同步',
|
||||
remove: '移除',
|
||||
openSource: '打开来源',
|
||||
removeSource: '移除来源',
|
||||
retryDocument: '重新处理',
|
||||
useInChat: '用于当前对话',
|
||||
saveEntity: '保存实体',
|
||||
@@ -565,7 +566,7 @@ export const knowledge = {
|
||||
lastSynced: '最近同步:{{time}}',
|
||||
moreActionsFor: '{{name}} 的更多操作',
|
||||
openParsedOriginal: '打开解析时原件',
|
||||
reparse: '使用当前设置重新解析',
|
||||
reparse: '重新解析',
|
||||
viewResult: '查看解析结果',
|
||||
previewLabel: '解析结果:{{name}}',
|
||||
backToList: '返回文档列表',
|
||||
|
||||
@@ -234,18 +234,18 @@ export function DocumentsView({
|
||||
|
||||
const sourceActions = (source: KnowledgeSource): KnowledgeAction[] => [
|
||||
{
|
||||
label: t(source.status === 'syncing' ? 'documents.actions.pauseSource' : source.status === 'failed' ? 'documents.actions.retrySource' : 'documents.actions.syncSource', { name: source.name }),
|
||||
label: t(source.status === 'syncing' ? 'actions.pause' : source.status === 'failed' ? 'actions.retry' : 'actions.sync'),
|
||||
disabled: pending.has(source.id),
|
||||
onClick: () => void run(source.id, () => source.status === 'syncing' ? onPauseSource(source.id) : source.status === 'failed' ? onRetrySource(source.id) : onSyncSource(source.id))
|
||||
},
|
||||
{
|
||||
label: t('documents.actions.removeSource', { name: source.name }),
|
||||
label: t('actions.removeSource'),
|
||||
disabled: pending.has(source.id), danger: true, separator: true,
|
||||
onClick: () => setRemovingSource(source)
|
||||
}
|
||||
]
|
||||
const documentActions = (document: KnowledgeDocumentItem, source: KnowledgeSource | undefined, taskSourceId?: string): KnowledgeAction[] => [
|
||||
{ label: t('documents.actions.openDocumentSource', { name: document.name }), disabled: pending.has(`open:${document.id}`),
|
||||
{ label: t('actions.openSource'), disabled: pending.has(`open:${document.id}`),
|
||||
onClick: () => void run(`open:${document.id}`, () => onOpenDocumentSource(library.id, document.id)) },
|
||||
...(document.resultId ? [{
|
||||
label: t('documents.openParsedOriginal'), disabled: pending.has(`original:${document.id}`),
|
||||
|
||||
@@ -21,7 +21,7 @@ export function KnowledgeActionsMenu({ label, actions }: {
|
||||
<button ref={anchorRef} type="button" className="icon-button" aria-label={label} title={label}
|
||||
aria-haspopup="menu" aria-expanded={open} aria-controls={open ? id : undefined}
|
||||
onClick={() => setOpen(!open)}><Ellipsis aria-hidden="true" size={16} /></button>
|
||||
{open && <AnchoredMenu anchorRef={anchorRef} id={id} label={label} onClose={() => setOpen(false)}>
|
||||
{open && <AnchoredMenu anchorRef={anchorRef} id={id} label={label} width={220} onClose={() => setOpen(false)}>
|
||||
{actions.map((action) => <div key={action.label}>
|
||||
{action.separator && <div role="separator" />}
|
||||
<button type="button" role="menuitem" className={`knowledge-action${action.danger ? ' knowledge-action--danger' : ''}`}
|
||||
|
||||
Reference in New Issue
Block a user