feat: 工作目录支持页面内目录浏览器

This commit is contained in:
2026-10-24 23:20:20 +08:00
parent dd7612bc74
commit e555610037
15 changed files with 1154 additions and 16 deletions
+48 -11
View File
@@ -14,6 +14,7 @@ import {
type Task,
type VenvProbeResponse,
} from '../api/client'
import { DirectoryPickerDialog } from './DirectoryPickerDialog'
import {
createEmptyTaskFormValues,
mapServerFieldToFormField,
@@ -42,8 +43,10 @@ export function TaskEditorModal({
}: TaskEditorModalProps) {
const [submitError, setSubmitError] = useState<string | null>(null)
const [cronPreviewCount, setCronPreviewCount] = useState(5)
const [pickerOpen, setPickerOpen] = useState(false)
const dialogRef = useRef<HTMLElement>(null)
useFocusTrap(open, dialogRef, '[data-dialog-autofocus]')
// 目录选择弹窗打开时让出焦点管理,避免两个焦点陷阱互相争夺 Tab。
useFocusTrap(open && !pickerOpen, dialogRef, '[data-dialog-autofocus]')
const {
register,
control,
@@ -51,6 +54,7 @@ export function TaskEditorModal({
reset,
watch,
setError,
setValue,
formState: { errors },
} = useForm<TaskFormValues>({
resolver: zodResolver(taskFormSchema),
@@ -119,6 +123,7 @@ export function TaskEditorModal({
const previewVirtualEnv =
scriptPreview.venv.trim() || linkedVenv?.venv_path || ''
const previewInterpreter = linkedVenv?.display_path ?? null
const scriptArgs = useFieldArray({ control, name: 'script.args' })
const scriptEnv = useFieldArray({ control, name: 'script.env' })
const httpHeaders = useFieldArray({ control, name: 'http.headers' })
@@ -132,6 +137,7 @@ export function TaskEditorModal({
reset(task ? taskToFormValues(task) : createEmptyTaskFormValues())
setCronPreviewCount(5)
setSubmitError(null)
setPickerOpen(false)
}, [open, reset, task])
useEffect(() => {
@@ -346,16 +352,35 @@ export function TaskEditorModal({
{actionType === 'script' ? (
<div className="dynamic-fields">
<Field
label="项目工作目录 (Working Directory)"
error={errors.script?.working_dir?.message}
>
<input
{...register('script.working_dir')}
className="field-input mono"
placeholder="D:\Projects\sync"
/>
</Field>
<div className="field">
<label className="field-label" htmlFor="task-working-dir">
项目工作目录 (Working Directory)
</label>
<div className="field-with-action">
<input
id="task-working-dir"
{...register('script.working_dir')}
className="field-input mono"
placeholder="D:\Projects\sync"
aria-invalid={Boolean(errors.script?.working_dir)}
/>
<button
type="button"
className="button button-secondary pick-directory-button"
// 不夺走输入框焦点:否则 blur 校验插入错误行会让表单瞬间下移,
// 点击可能落到移位后的其它元素上。
onMouseDown={(event) => event.preventDefault()}
onClick={() => setPickerOpen(true)}
>
选择文件夹…
</button>
</div>
{errors.script?.working_dir?.message ? (
<span className="field-error">
{errors.script.working_dir.message}
</span>
) : null}
</div>
<VenvDetectorBar
workingDir={debouncedWorkingDir}
probe={venvProbe.data}
@@ -643,6 +668,18 @@ export function TaskEditorModal({
</button>
</footer>
</section>
<DirectoryPickerDialog
open={pickerOpen}
initialPath={scriptPreview.working_dir}
onSelect={(path) => {
setValue('script.working_dir', path, {
shouldDirty: true,
shouldValidate: true,
})
}}
onClose={() => setPickerOpen(false)}
/>
</div>
)
}