feat(run): browse for .env files from the Environment variables field (#1106)

A folder button next to the Environment variables input opens a file dialog at the project root and appends the chosen files to the configuration's .env files (relative to the root inside the project, duplicates skipped). Hidden in remote windows, where the dialog would pick local paths.
Цей коміт міститься в:
l0ng-ai authored and GitHub committed 2026-11-04 05:55:20 +08:00
1 джерело 9a6b84e287
коміт c347e331ab
3 files changed
+73 -1

No files matched your search

+27 -1
Переглянути файл
@@ -6,8 +6,10 @@
import { parseJavaArgs } from "@/platform/javaLaunch";
import { useMemo, useRef, useState, type KeyboardEvent, type ReactNode, type RefObject } from "react";
import clsx from "clsx";
import type { BeforeLaunchTask, RunConfig } from "@/platform/ipc";
import { isTauri, type BeforeLaunchTask, type RunConfig } from "@/platform/ipc";
import { useWorkflowSettings } from "@/platform/workflowSettings";
import { relativePath, useProject } from "@/platform/project";
import { isRemoteWindow } from "@/platform/remote";
import { useTx, useTxTemplate } from "@/platform/i18n";
import {
Button,
@@ -169,6 +171,14 @@ export interface ConfigFormProps {
modifyRequest: number;
}
/** Picks `.env` files; paths inside the project are returned relative to its root. */
async function chooseEnvFiles(root: string | null, title: string): Promise<string[]> {
const { open } = await import("@tauri-apps/plugin-dialog");
const picked = await open({ multiple: true, title, defaultPath: root ?? undefined });
const paths = picked === null ? [] : Array.isArray(picked) ? picked : [picked];
return paths.map((p) => (root ? relativePath(root, p) : p));
}
export function ConfigForm({ config, all, type, errors, template, onChange, modifyRequest }: ConfigFormProps) {
const openByDefault = useWorkflowSettings((s) => s.run.showOnStart);
const externalTools = useWorkflowSettings((s) => s.externalTools);
@@ -176,6 +186,7 @@ export function ConfigForm({ config, all, type, errors, template, onChange, modi
const toolWindows = useContributions((s) => s.toolWindows);
const tx = useTx();
const txTemplate = useTxTemplate();
const root = useProject((s) => s.project?.root ?? null);
const [shown, setShown] = useState<ReadonlySet<OptionId>>(() => new Set());
const [argsText, setArgsText] = useState<string | null>(null);
const [envText, setEnvText] = useState<string | null>(null);
@@ -480,6 +491,21 @@ export function ConfigForm({ config, all, type, errors, template, onChange, modi
title="Edit Environment Variables"
onClick={() => setEnvOpen(true)}
/>
{isTauri() && !isRemoteWindow() && (
<IconButton
icon="folder-open"
size="small"
iconSize={14}
title="Browse for .env Files"
onClick={() =>
void chooseEnvFiles(root, tx("Browse for .env Files")).then((files) => {
const current = config.envFiles ?? [];
const added = files.filter((f) => !current.includes(f));
if (added.length) update({ envFiles: [...current, ...added] });
})
}
/>
)}
</div>
{config.includeSystemEnv === false && (
<span className={styles.chooserMuted}>
+45
Переглянути файл
@@ -0,0 +1,45 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import { cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react";
const open = vi.hoisted(() => vi.fn());
vi.mock("@tauri-apps/plugin-dialog", () => ({ open }));
vi.mock("@/platform/ipc", async (importOriginal) => ({
...(await importOriginal<typeof import("@/platform/ipc")>()),
isTauri: () => true,
}));
import type { RunConfig } from "@/platform/ipc";
import { useProject } from "@/platform/project";
import { ConfigForm } from "./ConfigForm";
const config: RunConfig = {
id: "app",
name: "app",
kind: "shell",
command: "python3",
args: [],
cwd: null,
env: {},
detected: false,
isTest: false,
debugAdapter: null,
envFiles: [".env"],
};
afterEach(cleanup);
describe("Environment variables", () => {
it("adds the .env files chosen in the file dialog, relative to the project root", async () => {
useProject.setState({ project: { root: "/work/app" } as never });
open.mockResolvedValue(["/work/app/.env", "/work/app/config/dev.env", "/elsewhere/shared.env"]);
const onChange = vi.fn();
render(<ConfigForm config={config} all={[]} type={undefined} errors={{}} onChange={onChange} modifyRequest={0} />);
fireEvent.click(screen.getByRole("button", { name: "Browse for .env Files" }));
await waitFor(() =>
expect(onChange).toHaveBeenCalledWith(
expect.objectContaining({ envFiles: [".env", "config/dev.env", "/elsewhere/shared.env"] }),
),
);
expect(open).toHaveBeenCalledWith(expect.objectContaining({ multiple: true, defaultPath: "/work/app" }));
});
});
+1
Переглянути файл
@@ -2935,6 +2935,7 @@
"Modify options": "修改选项",
"e.g. ./gradlew or cargo": "例如 ./gradlew 或 cargo",
"Edit Environment Variables": "编辑环境变量",
"Browse for .env Files": "浏览 .env 文件",
"System environment variables are not passed": "不传递系统环境变量",
"Redirect input from": "重定向输入自",
"File relative to the project root": "相对于项目根目录的文件",