дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-09 11:15:40 +02:00
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
Цей коміт міститься в:
1 джерело
cbc77941d4
коміт
16df2ae472
3 files changed
+71
-5
No files matched your search
@@ -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);
|
||||
|
||||
@@ -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]);
|
||||
});
|
||||
@@ -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(
|
||||
|
||||
Reference in new issue
Заблокувати користувача