mirror of
https://github.com/l0ng-ai/openidea.git
synced 2026-11-08 08:52:12 +00:00
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.
This commit is contained in:
1 parent
954ad354a1
commit
b54a745d6d
9 files changed
+115
-4
No files matched your search
@@ -74,6 +74,21 @@ impl TrustStore {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn add_location(&self, path: &Path) -> IdeResult<()> {
|
||||
let root = dunce::canonicalize(path)?;
|
||||
if !root.is_dir() {
|
||||
return Err(IdeError::invalid("trusted location must be an existing directory"));
|
||||
}
|
||||
let mut state = self.state.lock();
|
||||
let mut next = state.clone()?;
|
||||
if !next.locations.contains(&root) {
|
||||
next.locations.push(root);
|
||||
persist(&self.file, &next)?;
|
||||
*state = Ok(next);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn remove_location(&self, path: &Path) -> IdeResult<()> {
|
||||
let mut state = self.state.lock();
|
||||
let mut next = state.clone()?;
|
||||
|
||||
@@ -349,6 +349,29 @@ fn trusted_locations_use_canonical_directory_boundaries() {
|
||||
assert_eq!(trust.status(&alias).unwrap().root.as_ref(), dunce::canonicalize(&other).unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_location_trusts_every_project_under_it() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let location = tmp.path().join("projects");
|
||||
let inside = location.join("nested/deep/app");
|
||||
let outside = tmp.path().join("projects-other/app");
|
||||
std::fs::create_dir_all(&inside).unwrap();
|
||||
std::fs::create_dir_all(&outside).unwrap();
|
||||
let file = tmp.path().join("not-a-dir");
|
||||
std::fs::write(&file, "x").unwrap();
|
||||
let trust = TrustStore::load(&tmp.path().join("config"), &tmp.path().join("data"));
|
||||
trust.add_location(&location).unwrap();
|
||||
assert!(trust.trusted(&inside).unwrap());
|
||||
assert!(!trust.trusted(&outside).unwrap());
|
||||
assert_eq!(trust.decisions().unwrap().locations.len(), 1);
|
||||
trust.add_location(&location).unwrap();
|
||||
assert_eq!(trust.decisions().unwrap().locations.len(), 1, "re-adding is a no-op");
|
||||
assert!(trust.add_location(&file).is_err(), "a file is not a location");
|
||||
assert!(trust.add_location(&tmp.path().join("missing")).is_err(), "a missing path is not a location");
|
||||
let reloaded = TrustStore::load(&tmp.path().join("config"), &tmp.path().join("data"));
|
||||
assert!(reloaded.trusted(&inside).unwrap());
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn restricted_git_views_ignore_project_executables_and_mutations_cannot_run_hooks() {
|
||||
|
||||
@@ -314,6 +314,13 @@ pub async fn project_trust_set(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn project_trust_add_location(state: State<'_, AppState>, path: PathBuf) -> IdeResult<()> {
|
||||
state.trust.add_location(&path)?;
|
||||
state.host.refresh_project_trust().await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn project_trust_remove_location(state: State<'_, AppState>, path: PathBuf) -> IdeResult<()> {
|
||||
|
||||
@@ -75,6 +75,7 @@ pub fn builder() -> tauri_specta::Builder<tauri::Wry> {
|
||||
app::project_trust_status,
|
||||
app::project_trust_list,
|
||||
app::project_trust_set,
|
||||
app::project_trust_add_location,
|
||||
app::project_trust_remove_location,
|
||||
workspace::workspace_create,
|
||||
workspace::workspace_attach,
|
||||
|
||||
@@ -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 })),
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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": "移除范围",
|
||||
|
||||
Reference in new issue
Block a user