fix(run): remove temporary configurations from Edit Configurations for good (#1180)

Removing a temporary configuration in Edit Configurations only changed the dialog's draft; the reload after OK read it back from the saved temporary list. OK / Apply now drops removed temporaries from that list, keeps unedited ones temporary instead of saving every non-detected temporary configuration as a user configuration, and saves edited ones as before. Removing the last temporary configuration also cancels a pending rewrite of the list that could bring the removed entry back (the chooser's Delete had the same race).

Fixes #1162
Цей коміт міститься в:
l0ng-ai authored and GitHub committed 2026-11-04 13:20:41 +08:00
1 джерело cbc77941d4
коміт 16df2ae472
3 files changed
+71 -5

No files matched your search

+2 -3
Переглянути файл
@@ -22,13 +22,12 @@ import {
kindLabel,
removeConfig,
upsertConfig,
userConfigs,
validateConfig,
type ConfigErrors,
} from "./configs";
import { importConfigs, importLabel } from "./importConfigs";
import { targetName } from "./remote";
import { saveUserConfigs, selectConfig, useRun } from "./store";
import { saveEditedConfigs, selectConfig, useRun } from "./store";
import { blankTemplate, COMPOUND, missingParams, newConfigOfType, templateId, typeFor } from "./types";
import styles from "./Run.module.css";
import { tx } from "@/platform/i18n";
@@ -305,7 +304,7 @@ function EditConfigsDialog() {
}
setSaving(true);
try {
if (dirty) await saveUserConfigs(userConfigs(draft), removedDetectedIds);
if (dirty) await saveEditedConfigs(draft, removedDetectedIds);
if (templatesDirty && templates) await call(commands.runSaveTemplates(templates));
setRemovedDetectedIds([]);
setDirty(false);
+43 -1
Переглянути файл
@@ -6,6 +6,8 @@ const ipc = vi.hoisted(() => ({
runConfigs: vi.fn(),
runTemplates: vi.fn(async () => ({ status: "ok", data: [] })),
runImportSources: vi.fn(async () => ({ status: "ok", data: [] })),
runWithoutDerived: vi.fn(async (configs: unknown) => ({ status: "ok", data: configs })),
runApplySettings: vi.fn(async (configs: unknown) => ({ status: "ok", data: configs })),
}));
vi.mock("@/platform/ipc", async (importOriginal) => ({
...(await importOriginal<typeof import("@/platform/ipc")>()),
@@ -17,7 +19,7 @@ import type { RunConfig } from "@/platform/ipc";
import { useProject } from "@/platform/project";
import { EditConfigsOverlay } from "./EditConfigsDialog";
import { ConfigChooserOverlay } from "./ConfigChooser";
import { canDeleteConfig, deleteConfig, loadConfigs, resetRuns, useRun } from "./store";
import { adoptTemporary, canDeleteConfig, deleteConfig, loadConfigs, resetRuns, useRun } from "./store";
import { registerBuiltinTypes } from "./types";
const config = (kind = "npm"): RunConfig => ({
@@ -125,3 +127,43 @@ it("Cancel discards a pending detected removal", () => {
expect(ipc.runSaveConfigs).not.toHaveBeenCalled();
expect(useRun.getState().configs).toEqual([config()]);
});
const temporaryOf = () =>
JSON.parse(localStorage.getItem("openidea.run.temporary.v2:/test-removal") ?? "[]") as RunConfig[];
const mainGo: RunConfig = { ...config("go"), id: "go:file:/test-removal/app/main.go", name: "main.go" };
it("removes a temporary configuration from Edit Configurations for good", async () => {
adoptTemporary(mainGo);
useRun.setState({ editTarget: mainGo.id });
render(<EditConfigsOverlay />);
fireEvent.click(screen.getByRole("treeitem", { name: "main.go" }));
fireEvent.click(screen.getByRole("button", { name: "Remove Configuration" }));
fireEvent.click(screen.getByRole("button", { name: "OK" }));
await waitFor(() => expect(useRun.getState().editorOpen).toBe(false));
// Not a suppressed detection: it was never detected by the backend.
expect(ipc.runSaveConfigs).toHaveBeenCalledWith([], []);
expect(temporaryOf()).toEqual([]);
await act(() => loadConfigs());
expect(useRun.getState().configs.map((c) => c.id)).toEqual(["npm:app"]);
expect(useRun.getState().temporary).toEqual([]);
});
it("keeps unedited temporary configurations temporary and saves edited ones", async () => {
const goal: RunConfig = { ...config("maven"), id: "maven:goal:pom.xml:test", name: "app [test]", detected: false };
const other: RunConfig = {
...config("maven"),
id: "maven:goal:pom.xml:verify",
name: "app [verify]",
detected: false,
};
adoptTemporary(goal);
adoptTemporary(other);
useRun.setState({ editTarget: other.id });
render(<EditConfigsOverlay />);
fireEvent.change(screen.getByRole("textbox", { name: "Name" }), { target: { value: "verify app" } });
fireEvent.click(screen.getByRole("button", { name: "OK" }));
await waitFor(() => expect(useRun.getState().editorOpen).toBe(false));
expect(ipc.runSaveConfigs).toHaveBeenCalledWith([expect.objectContaining({ id: other.id, name: "verify app" })], []);
expect(useRun.getState().temporary).toEqual([goal.id]);
expect(temporaryOf().map((c) => c.id)).toEqual([goal.id]);
});
+26 -1
Переглянути файл
@@ -347,10 +347,11 @@ function persistTemporary() {
const { configs, temporary } = useRun.getState();
const list = temporary.map((id) => configs.find((c) => c.id === id)).filter((c): c is RunConfig => !!c);
writeTemporary(root, list);
// A rewrite still pending from an earlier call must not bring back what was removed since.
const generation = ++persisting;
if (!list.length || !isTauri()) return;
// Then rewritten without what settings derived (the project's Maven, JDK, interpreter…), so a
// later settings change reaches them as it does detected and saved configurations.
const generation = ++persisting;
void call(commands.runWithoutDerived(list))
.then((kept) => {
if (generation === persisting && useProject.getState().project?.root === root) writeTemporary(root, kept);
@@ -432,6 +433,30 @@ export async function deleteConfig(id: string): Promise<void> {
);
}
/**
* Edit Configurations' OK / Apply. A temporary configuration stays temporary unless it was edited
* there (then it is saved, as IDEA makes it permanent), and one removed there is forgotten.
*/
export async function saveEditedConfigs(draft: readonly RunConfig[], removedDetectedIds: string[] = []): Promise<void> {
const { configs, temporary } = useRun.getState();
const unchanged = (c: RunConfig) => {
const before = configs.find((o) => o.id === c.id);
return !!before && JSON.stringify(before) === JSON.stringify(c);
};
const kept = temporary.filter((id) => draft.some((c) => c.id === id && unchanged(c)));
if (kept.length !== temporary.length) {
useRun.setState({
configs: configs.filter((c) => !temporary.includes(c.id) || kept.includes(c.id)),
temporary: kept,
});
persistTemporary();
}
await saveUserConfigs(
draft.filter((c) => !c.detected && !kept.includes(c.id)),
removedDetectedIds,
);
}
/** Persists user configurations and reloads the merged list. */
export async function saveUserConfigs(user: RunConfig[], removedDetectedIds: string[] = []): Promise<void> {
user = await Promise.all(