Finder-launched terminals now inherit a login PATH so tools like ollama resolve, destructive SFTP/editor actions require confirmation, and terminal search plus window dragging work again. SSH forwards and RDP sessions keep listeners and survive reconnect without FastPath crashes. Co-authored-by: Cursor <cursoragent@cursor.com>
104 lines
3.6 KiB
TypeScript
104 lines
3.6 KiB
TypeScript
/**
|
|
* TransferPanel - 传输队列面板
|
|
*
|
|
* 显示文件上传/下载的传输队列;进行中任务置顶。
|
|
*/
|
|
|
|
import { useMemo } from 'react'
|
|
import { useTranslation } from 'react-i18next'
|
|
import { ChevronDown, ChevronUp } from 'lucide-react'
|
|
import { TransferItem } from './TransferItem'
|
|
import type { TransferTask } from '../../../shared/types/sftp.types'
|
|
|
|
interface TransferPanelProps {
|
|
tasks: TransferTask[]
|
|
isOpen: boolean
|
|
onToggle: () => void
|
|
onCancel?: (id: string) => void
|
|
onRemove: (id: string) => void
|
|
onRetry?: (id: string) => void
|
|
}
|
|
|
|
/** transferring → pending → failed/canceled → completed(组内保持原相对顺序) */
|
|
// eslint-disable-next-line react-refresh/only-export-components -- pure helper used by tests and panel
|
|
export function sortTransferTasks(tasks: TransferTask[]): TransferTask[] {
|
|
const rank = (status: TransferTask['status']): number => {
|
|
switch (status) {
|
|
case 'transferring':
|
|
return 0
|
|
case 'pending':
|
|
return 1
|
|
case 'failed':
|
|
case 'canceled':
|
|
return 2
|
|
case 'completed':
|
|
return 3
|
|
default:
|
|
return 4
|
|
}
|
|
}
|
|
|
|
return tasks
|
|
.map((task, index) => ({ task, index }))
|
|
.sort((a, b) => {
|
|
const byStatus = rank(a.task.status) - rank(b.task.status)
|
|
return byStatus !== 0 ? byStatus : a.index - b.index
|
|
})
|
|
.map(({ task }) => task)
|
|
}
|
|
|
|
export function TransferPanel({ tasks, isOpen, onToggle, onCancel, onRemove, onRetry }: TransferPanelProps) {
|
|
const { t } = useTranslation('sftp')
|
|
const orderedTasks = useMemo(() => sortTransferTasks(tasks), [tasks])
|
|
const activeCount = tasks.filter((task) => task.status === 'pending' || task.status === 'transferring').length
|
|
const completedCount = tasks.filter((task) => task.status === 'completed').length
|
|
|
|
return (
|
|
<div className="bg-dark-800 border-t border-dark-700/30">
|
|
<button
|
|
type="button"
|
|
onClick={onToggle}
|
|
className="w-full flex items-center justify-between px-4 py-2 hover:bg-dark-700 transition-colors"
|
|
>
|
|
<div className="flex items-center gap-2">
|
|
<span className="text-sm font-medium">{t('transfer.queue')}</span>
|
|
{activeCount > 0 && (
|
|
<span className="px-2 py-0.5 bg-accent-blue/20 text-accent-blue text-xs rounded-full">
|
|
{t('transfer.active', { count: activeCount })}
|
|
</span>
|
|
)}
|
|
{completedCount > 0 && (
|
|
<span className="px-2 py-0.5 bg-feedback-success/15 text-feedback-success text-xs rounded-full">
|
|
{t('transfer.completed', { count: completedCount })}
|
|
</span>
|
|
)}
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<span className="text-xs text-txt-muted">{t('transfer.taskCount', { count: tasks.length })}</span>
|
|
{isOpen ? <ChevronDown className="w-4 h-4" /> : <ChevronUp className="w-4 h-4" />}
|
|
</div>
|
|
</button>
|
|
|
|
{isOpen && orderedTasks.length > 0 && (
|
|
<div className="max-h-48 overflow-y-auto border-t border-dark-700/30">
|
|
{orderedTasks.map((task) => (
|
|
<TransferItem
|
|
key={task.id}
|
|
task={task}
|
|
onCancel={onCancel ? () => onCancel(task.id) : undefined}
|
|
onRemove={() => onRemove(task.id)}
|
|
onRetry={onRetry ? () => onRetry(task.id) : undefined}
|
|
/>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
{isOpen && tasks.length === 0 && (
|
|
<div className="py-4 text-center text-txt-muted text-sm">
|
|
{t('transfer.empty')}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|