Files
task_pilot/frontend/src/components/DirectoryPickerDialog.tsx
T

209 lines
6.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useQuery } from '@tanstack/react-query'
import { useEffect, useRef, useState } from 'react'
import { listDirectories } from '../api/client'
import { useFocusTrap } from '../lib/useFocusTrap'
interface DirectoryPickerDialogProps {
open: boolean
initialPath: string
onSelect: (path: string) => void
onClose: () => void
}
/**
* 页面内的目录浏览器:从盘符开始逐级进入文件夹,最终把绝对路径回填到表单。
* 浏览器拿不到系统文件对话框的真实路径,因此目录浏览由后端提供列表接口。
*/
export function DirectoryPickerDialog({
open,
initialPath,
onSelect,
onClose,
}: DirectoryPickerDialogProps) {
const [currentPath, setCurrentPath] = useState<string | null>(null)
const [draftPath, setDraftPath] = useState('')
const dialogRef = useRef<HTMLElement>(null)
useFocusTrap(open, dialogRef, '[data-picker-path]')
useEffect(() => {
if (!open) {
return
}
const start = initialPath.trim()
setCurrentPath(start || null)
setDraftPath(start)
}, [initialPath, open])
const listing = useQuery({
queryKey: ['directories', currentPath],
queryFn: ({ signal }) => listDirectories(currentPath ?? undefined, signal),
enabled: open,
retry: false,
})
useEffect(() => {
if (listing.data?.path) {
setDraftPath(listing.data.path)
}
}, [listing.data?.path])
useEffect(() => {
if (!open) {
return
}
const onKeyDown = (event: KeyboardEvent) => {
if (event.key === 'Escape') {
onClose()
}
}
window.addEventListener('keydown', onKeyDown)
return () => window.removeEventListener('keydown', onKeyDown)
}, [onClose, open])
if (!open) {
return null
}
const entries = listing.data?.entries ?? []
const currentDirectory = listing.data?.path ?? null
const parent = listing.data?.parent ?? null
const navigate = (path: string | null) => {
setCurrentPath(path)
setDraftPath(path ?? '')
}
return (
<div
className="modal-overlay"
onMouseDown={(event) => {
if (event.currentTarget === event.target) {
onClose()
}
}}
>
<section
ref={dialogRef}
className="modal-card directory-picker"
role="dialog"
aria-modal="true"
aria-labelledby="directory-picker-title"
tabIndex={-1}
>
<header className="modal-header">
<div>
<p className="eyebrow">WORKING DIRECTORY</p>
<h2 id="directory-picker-title">选择项目工作目录</h2>
</div>
<button
type="button"
className="icon-button"
onClick={onClose}
aria-label="关闭目录选择"
>
×
</button>
</header>
<div className="directory-picker-toolbar">
<button
type="button"
className="button button-secondary button-small"
onClick={() => navigate(parent)}
disabled={!parent}
>
上一级
</button>
<button
type="button"
className="button button-secondary button-small"
onClick={() => navigate(null)}
>
盘符
</button>
<form
className="directory-path-form"
onSubmit={(event) => {
event.preventDefault()
navigate(draftPath.trim() || null)
}}
>
<input
className="field-input mono"
value={draftPath}
onChange={(event) => setDraftPath(event.target.value)}
placeholder="输入或粘贴绝对路径,例如 D:\Projects\sync"
aria-label="目录路径"
data-picker-path
spellCheck={false}
/>
</form>
</div>
<div className="directory-picker-body">
{listing.isPending ? (
<p className="muted-copy">正在读取目录…</p>
) : null}
{listing.isError ? (
<div className="inline-alert inline-alert-error" role="alert">
{listing.error.message}
</div>
) : null}
{!listing.isPending && !listing.isError && entries.length === 0 ? (
<p className="empty-inline">
{currentDirectory ? '该目录下没有子文件夹' : '没有可用的盘符'}
</p>
) : null}
{entries.length > 0 ? (
<ul className="directory-list">
{entries.map((entry) => (
<li key={entry.path}>
<button
type="button"
className="directory-row"
onClick={() => navigate(entry.path)}
title={entry.path}
>
<span className="directory-icon" aria-hidden="true">
📁
</span>
<span className="directory-name">{entry.name}</span>
</button>
</li>
))}
</ul>
) : null}
{listing.data?.truncated ? (
<p className="field-hint">文件夹过多,仅列出前 500 个。</p>
) : null}
</div>
<footer className="directory-picker-footer">
<div className="directory-picker-summary">
<span className="section-label">将使用</span>
<code>{currentDirectory ?? '尚未选择目录'}</code>
</div>
<div className="modal-actions directory-picker-actions">
<button type="button" className="button button-secondary" onClick={onClose}>
取消
</button>
<button
type="button"
className="button button-primary"
disabled={!currentDirectory}
onClick={() => {
if (currentDirectory) {
onSelect(currentDirectory)
onClose()
}
}}
>
使用此目录
</button>
</div>
</footer>
</section>
</div>
)
}