fix(frontend): 切换目录时只保留一次虚拟环境状态
切换目录时先同步清掉旧目录自动填入的 venv;实时 venv 为空时立即忽略防抖旧值;旧查询结果不再回填新目录;补充 Playwright 回归用例。
This commit is contained in:
@@ -204,6 +204,33 @@ test('switches script language and adapts the form fields', async ({ page }) =>
|
||||
await dialog.getByRole('button', { name: '取消' }).click()
|
||||
})
|
||||
|
||||
test('avoids a transient invalid venv when switching directories', async ({
|
||||
page,
|
||||
}) => {
|
||||
const venvDir = createVenvFixture()
|
||||
const emptyProject = join(venvDir, '..', 'empty-project')
|
||||
mkdirSync(emptyProject, { recursive: true })
|
||||
|
||||
await page.goto('/')
|
||||
await page.getByRole('button', { name: '+ 新建任务' }).click()
|
||||
const dialog = page.getByRole('dialog', { name: '新建定时任务' })
|
||||
const workingDir = dialog.getByLabel(/项目工作目录/)
|
||||
const bar = dialog.locator('.env-detector-bar')
|
||||
|
||||
await workingDir.fill(venvDir)
|
||||
await expect(bar).toContainText('已关联虚拟环境')
|
||||
|
||||
await workingDir.fill(emptyProject)
|
||||
const states = new Set<string>()
|
||||
for (let index = 0; index < 30; index += 1) {
|
||||
states.add(await bar.innerText())
|
||||
await page.waitForTimeout(80)
|
||||
}
|
||||
|
||||
expect([...states].some((text) => text.includes('缺少'))).toBe(false)
|
||||
await expect(bar).toContainText('未检测到虚拟环境')
|
||||
})
|
||||
|
||||
test('keeps the dashboard usable on a narrow viewport', async ({
|
||||
page,
|
||||
request,
|
||||
|
||||
@@ -136,6 +136,10 @@ export function TaskEditorModal({
|
||||
350,
|
||||
)
|
||||
const debouncedVenv = useDebouncedValue(scriptPreview.venv ?? '', 350)
|
||||
// 切换工作目录时会同步清掉自动填入的 venv;如果实时值已经为空,
|
||||
// 探测必须立刻当作没有 venv,不能继续用防抖中的旧值,
|
||||
// 否则会先报“缺少 pyvenv.cfg”,再跳到“未检测到虚拟环境”。
|
||||
const probeVenv = scriptPreview.venv.trim() ? debouncedVenv : ''
|
||||
const cronPreview = useQuery({
|
||||
queryKey: [
|
||||
'cron-preview',
|
||||
@@ -187,12 +191,24 @@ export function TaskEditorModal({
|
||||
}
|
||||
}
|
||||
|
||||
const clearAutoVenv = () => {
|
||||
if (
|
||||
autoVenvRef.current &&
|
||||
getValues('script.venv').trim() === autoVenvRef.current
|
||||
) {
|
||||
setValue('script.venv', '', { shouldDirty: false })
|
||||
autoVenvRef.current = ''
|
||||
}
|
||||
}
|
||||
|
||||
const workingDirField = register('script.working_dir')
|
||||
|
||||
const venvProbe = useQuery({
|
||||
queryKey: [
|
||||
'python-venv',
|
||||
debouncedWorkingDir,
|
||||
debouncedInterpreter,
|
||||
debouncedVenv,
|
||||
probeVenv,
|
||||
scriptLanguage,
|
||||
],
|
||||
queryFn: ({ signal }) =>
|
||||
@@ -200,7 +216,7 @@ export function TaskEditorModal({
|
||||
{
|
||||
working_dir: debouncedWorkingDir,
|
||||
interpreter: debouncedInterpreter.trim() || 'python.exe',
|
||||
venv: debouncedVenv.trim() || null,
|
||||
venv: probeVenv.trim() || null,
|
||||
},
|
||||
signal,
|
||||
),
|
||||
@@ -262,10 +278,13 @@ export function TaskEditorModal({
|
||||
if (!open) {
|
||||
return
|
||||
}
|
||||
if (autoVenvRef.current && getValues('script.venv').trim() === autoVenvRef.current) {
|
||||
if (
|
||||
autoVenvRef.current &&
|
||||
getValues('script.venv').trim() === autoVenvRef.current
|
||||
) {
|
||||
setValue('script.venv', '', { shouldDirty: false })
|
||||
autoVenvRef.current = ''
|
||||
}
|
||||
autoVenvRef.current = ''
|
||||
}, [debouncedWorkingDir, getValues, open, setValue])
|
||||
|
||||
// 探测到虚拟环境就自动填进「虚拟环境目录」,但用户自己改过就保留用户的值。
|
||||
@@ -277,6 +296,10 @@ export function TaskEditorModal({
|
||||
if (!probe || probe.status !== 'linked' || !probe.venv_path) {
|
||||
return
|
||||
}
|
||||
// 工作目录还在防抖中时,旧查询结果不能回填到新目录。
|
||||
if (debouncedWorkingDir !== scriptPreview.working_dir.trim()) {
|
||||
return
|
||||
}
|
||||
const current = getValues('script.venv').trim()
|
||||
// 防抖中的输入可能还没落到查询键上:只有「当前输入」与「本次探测用的输入」
|
||||
// 一致时才回填,否则工作目录刚变化时可能把上一轮探测结果又填回去。
|
||||
@@ -295,7 +318,7 @@ export function TaskEditorModal({
|
||||
shouldDirty: true,
|
||||
shouldValidate: true,
|
||||
})
|
||||
}, [debouncedVenv, debouncedWorkingDir, getValues, isPythonScript, open, setValue, venvProbe.data])
|
||||
}, [debouncedVenv, debouncedWorkingDir, getValues, isPythonScript, open, scriptPreview.working_dir, setValue, venvProbe.data])
|
||||
|
||||
// 打开后把焦点给第一个可编辑字段:新建时是任务标识,编辑时 ID 已锁定,跳名称。
|
||||
// 原生 dialog 默认聚焦关闭按钮,不符合编辑表单的节奏。
|
||||
@@ -561,7 +584,13 @@ export function TaskEditorModal({
|
||||
<div className="field-with-action">
|
||||
<Input
|
||||
id="task-working-dir"
|
||||
{...register('script.working_dir')}
|
||||
{...workingDirField}
|
||||
onChange={(event) => {
|
||||
// 先清掉旧目录自动填入的 venv,再更新工作目录;
|
||||
// 这样新目录的第一次探测就不会带着旧 .venv 去报“缺少 pyvenv.cfg”。
|
||||
clearAutoVenv()
|
||||
void workingDirField.onChange(event)
|
||||
}}
|
||||
className="mono-input"
|
||||
placeholder="D:\Projects\sync"
|
||||
aria-invalid={Boolean(errors.script?.working_dir)}
|
||||
@@ -595,7 +624,7 @@ export function TaskEditorModal({
|
||||
</FormField>
|
||||
|
||||
<VenvDetectorBar
|
||||
workingDir={debouncedWorkingDir}
|
||||
workingDir={scriptPreview.working_dir}
|
||||
probe={venvProbe.data}
|
||||
pending={venvProbe.isPending}
|
||||
failed={venvProbe.isError}
|
||||
@@ -895,6 +924,7 @@ export function TaskEditorModal({
|
||||
open={pickerOpen}
|
||||
initialPath={scriptPreview.working_dir}
|
||||
onSelect={(path) => {
|
||||
clearAutoVenv()
|
||||
setValue('script.working_dir', path, {
|
||||
shouldDirty: true,
|
||||
shouldValidate: true,
|
||||
@@ -1013,9 +1043,7 @@ function VenvDetectorBar({
|
||||
if (!workingDir.trim()) {
|
||||
return (
|
||||
<div className="env-detector-bar" role="status">
|
||||
<span className="env-status-badge warn">
|
||||
● 请输入有效的工作目录以探测环境
|
||||
</span>
|
||||
<span className="env-detector-hint">填写工作目录后自动检测环境</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user