feat(trust): let users add trusted folders from settings (#1812)

* feat(trust): let users add trusted folders from settings

The trusted-locations check already matches projects by path prefix, but
the only way to populate it was the "Trust projects in…" checkbox on the
first-open dialog — a folder could never be trusted ahead of time.

Add a project_trust_add_location command that canonicalizes the picked
directory, rejects non-directories, and deduplicates, plus an "Add
folder…" button on the Project Trust settings page backed by the native
directory picker.

Fixes #1743

* fix(trust): translate the trusted-location additions into Chinese

The zh source-text catalog must cover every literal UI text; the three
new strings in the trusted-locations settings page were missing.
Цей коміт міститься в:
arookieofc authored and GitHub committed 2026-11-06 22:08:10 +08:00
1 джерело 954ad354a1
коміт b54a745d6d
9 files changed
+115 -4

No files matched your search

+1
Переглянути файл
@@ -28,6 +28,7 @@ export const commands = {
projectTrustStatus: (path: string) => typedError<TrustStatus, IdeError>(__TAURI_INVOKE("project_trust_status", { path })),
projectTrustList: () => typedError<TrustDecisions, IdeError>(__TAURI_INVOKE("project_trust_list")),
projectTrustSet: (path: string, trusted: boolean, folder: boolean) => typedError<null, IdeError>(__TAURI_INVOKE("project_trust_set", { path, trusted, folder })),
projectTrustAddLocation: (path: string) => typedError<null, IdeError>(__TAURI_INVOKE("project_trust_add_location", { path })),
projectTrustRemoveLocation: (path: string) => typedError<null, IdeError>(__TAURI_INVOKE("project_trust_remove_location", { path })),
workspaceCreate: (path: string, name: string, projects: string[]) => typedError<IpcPath, IdeError>(__TAURI_INVOKE("workspace_create", { path, name, projects })),
workspaceAttach: (path: string) => typedError<IpcPath, IdeError>(__TAURI_INVOKE("workspace_attach", { path })),
+23 -3
Переглянути файл
@@ -115,6 +115,18 @@ function TrustSettings() {
setError(String(e));
}
};
const addLocation = async () => {
try {
const { open } = await import("@tauri-apps/plugin-dialog");
const picked = await open({ directory: true, multiple: false, title: "Add Trusted Location" });
const path = Array.isArray(picked) ? picked[0] : picked;
if (!path) return;
await call(commands.projectTrustAddLocation(path));
await refresh();
} catch (e) {
setError(String(e));
}
};
return (
<div className={styles.content}>
<p>
@@ -132,9 +144,17 @@ function TrustSettings() {
</Button>
</div>
))}
<h3>
<T>Trusted locations</T>
</h3>
<div className={styles.sectionHead}>
<h3>
<T>Trusted locations</T>
</h3>
<Button onClick={() => void addLocation()}>
<T>Add folder…</T>
</Button>
</div>
<p className={styles.hint}>
<T>Every project inside a trusted location is trusted without asking.</T>
</p>
{decisions?.locations.map((path) => (
<div className={styles.row} key={path}>
<span className={styles.path}>{path}</span>
+12
Переглянути файл
@@ -52,6 +52,18 @@
justify-content: space-between;
gap: var(--space-3);
}
.sectionHead {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--space-3);
}
.sectionHead h3 {
margin: 0;
}
.hint {
color: var(--fg-secondary);
}
.path {
overflow-wrap: anywhere;
}
+30 -1
Переглянути файл
@@ -6,7 +6,7 @@ import { emitLocalIdeEvent } from "@/platform/ipc";
import { useProject } from "@/platform/project";
import { invokeLocal } from "@/platform/tauriCore";
import { OverlayHost } from "@/workbench/OverlayHost";
import { useContributions } from "@/workbench/contributions";
import { useContributions, useSettingsPages } from "@/workbench/contributions";
import { activate } from "./index";
const status = vi.fn();
@@ -15,6 +15,10 @@ vi.mock("@/platform/tauriCore", async (original) => ({
...(await original<typeof import("@/platform/tauriCore")>()),
invokeLocal: vi.fn(),
}));
const addLocation = vi.fn();
const listDecisions = vi.fn();
const pickFolder = vi.fn();
vi.mock("@tauri-apps/plugin-dialog", () => ({ open: pickFolder }));
vi.mock("@/platform/ipc", async (original) => {
const real = await original<typeof import("@/platform/ipc")>();
return {
@@ -23,6 +27,8 @@ vi.mock("@/platform/ipc", async (original) => {
...real.commands,
projectTrustStatus: (path: string) => status(path),
projectTrustSet: (...args: unknown[]) => decide(...args),
projectTrustList: () => listDecisions(),
projectTrustAddLocation: (...args: unknown[]) => addLocation(...args),
projectCurrent: async () => null,
},
};
@@ -34,6 +40,9 @@ beforeEach(() => {
.mockReset()
.mockImplementation(async (root: string) => ({ status: "ok", data: { root, trusted: false, decided: false } }));
decide.mockReset().mockResolvedValue({ status: "ok", data: null });
addLocation.mockReset().mockResolvedValue({ status: "ok", data: null });
listDecisions.mockReset().mockResolvedValue({ status: "ok", data: { projects: [], locations: [], restricted: [] } });
pickFolder.mockReset().mockResolvedValue(null);
useProject.setState({ project: null });
dispose = activate();
});
@@ -88,6 +97,26 @@ it("shows and trusts the canonical folder when opening through a symlink", async
expect(decide).toHaveBeenCalledWith("/real/projects/new", true, true);
});
it("adds a picked folder as a trusted location from settings", async () => {
const Page = useSettingsPages.getState().pages.get("projectTrust")!.component!;
pickFolder.mockResolvedValue("/projects");
addLocation.mockImplementation(async () => {
listDecisions.mockResolvedValue({
status: "ok",
data: { projects: [], locations: ["/projects"], restricted: [] },
});
return { status: "ok", data: null };
});
render(<Page />);
fireEvent.click(await screen.findByRole("button", { name: "Add folder…" }));
await screen.findByText("/projects");
expect(addLocation).toHaveBeenCalledWith("/projects");
pickFolder.mockResolvedValue(null);
fireEvent.click(screen.getByRole("button", { name: "Add folder…" }));
await act(async () => {});
expect(addLocation).toHaveBeenCalledTimes(1);
});
it.each(["Trust Project", "Preview in Restricted Mode"])(
"keeps a local opening's trust decision on the client: %s",
async (button) => {
+3
Переглянути файл
@@ -232,6 +232,8 @@
"Activate virtualenv": "激活 virtualenv",
"Add": "添加",
"Add Column": "添加列",
"Add Trusted Location": "添加受信任的位置",
"Add folder…": "添加文件夹…",
"Add Field Watchpoint": "添加字段监视点",
"Add File to Git": "将文件添加到 Git",
"Add GitHub Pattern": "添加 GitHub 模式",
@@ -3155,6 +3157,7 @@
"(empty line)": "(空行)",
"Name cannot be empty": "名称不能为空",
"Every scope needs a name": "每个范围都需要名称",
"Every project inside a trusted location is trusted without asking.": "受信任的位置内的每个项目都会被信任,不再询问。",
"Scopes": "范围",
"Add Scope": "添加范围",
"Remove Scope": "移除范围",