Skip to content

Commit 50676b5

Browse files
committed
Fix global worktrees root partial-save state
1 parent d56053b commit 50676b5

2 files changed

Lines changed: 95 additions & 14 deletions

File tree

src/features/settings/components/SettingsView.test.tsx

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -814,6 +814,40 @@ describe("SettingsView Environments", () => {
814814
expect(onUpdateAppSettings).not.toHaveBeenCalled();
815815
});
816816

817+
it("keeps the global worktrees root marked as saved after workspace save fails", async () => {
818+
const onUpdateAppSettings = vi.fn().mockResolvedValue(undefined);
819+
const onUpdateWorkspaceSettings = vi
820+
.fn()
821+
.mockRejectedValueOnce(new Error("Failed to save workspace settings"))
822+
.mockResolvedValueOnce(undefined);
823+
renderEnvironmentsSection({
824+
appSettings: { globalWorktreesFolder: "I:/existing-worktrees" },
825+
onUpdateAppSettings,
826+
onUpdateWorkspaceSettings,
827+
});
828+
829+
fireEvent.change(screen.getByLabelText("Global worktrees root"), {
830+
target: { value: "I:/cm-worktrees" },
831+
});
832+
fireEvent.change(screen.getByPlaceholderText("pnpm install"), {
833+
target: { value: "echo updated" },
834+
});
835+
fireEvent.click(screen.getByRole("button", { name: "Save" }));
836+
837+
expect(
838+
await screen.findByText("Failed to save workspace settings"),
839+
).toBeTruthy();
840+
expect(onUpdateAppSettings).toHaveBeenCalledTimes(1);
841+
expect(onUpdateWorkspaceSettings).toHaveBeenCalledTimes(1);
842+
843+
fireEvent.click(screen.getByRole("button", { name: "Save" }));
844+
845+
await waitFor(() => {
846+
expect(onUpdateWorkspaceSettings).toHaveBeenCalledTimes(2);
847+
});
848+
expect(onUpdateAppSettings).toHaveBeenCalledTimes(1);
849+
});
850+
817851
it("keeps the global worktrees root editable when there are no projects", async () => {
818852
const onUpdateAppSettings = vi.fn().mockResolvedValue(undefined);
819853
renderEnvironmentsSection({
@@ -853,6 +887,50 @@ describe("SettingsView Environments", () => {
853887
).toBeTruthy();
854888
});
855889

890+
it("keeps the new global worktrees root as saved when workspace settings fail afterward", async () => {
891+
const onUpdateAppSettings = vi.fn().mockResolvedValue(undefined);
892+
const onUpdateWorkspaceSettings = vi
893+
.fn()
894+
.mockRejectedValue(new Error("Failed to save workspace settings"));
895+
renderEnvironmentsSection({
896+
appSettings: { globalWorktreesFolder: "I:/existing-worktrees" },
897+
onUpdateAppSettings,
898+
onUpdateWorkspaceSettings,
899+
});
900+
901+
const input = screen.getByLabelText("Global worktrees root");
902+
const textarea = screen.getByPlaceholderText("pnpm install");
903+
fireEvent.change(input, { target: { value: "I:/cm-worktrees" } });
904+
fireEvent.change(textarea, { target: { value: "echo updated" } });
905+
fireEvent.click(screen.getByRole("button", { name: "Save" }));
906+
907+
expect(
908+
await screen.findByText("Failed to save workspace settings"),
909+
).toBeTruthy();
910+
911+
await waitFor(() => {
912+
expect(onUpdateAppSettings).toHaveBeenCalledWith(
913+
expect.objectContaining({
914+
globalWorktreesFolder: "I:/cm-worktrees",
915+
}),
916+
);
917+
expect(onUpdateWorkspaceSettings).toHaveBeenCalledWith("w1", {
918+
worktreeSetupScript: "echo updated",
919+
worktreesFolder: null,
920+
});
921+
});
922+
923+
expect((input as HTMLInputElement).value).toBe("I:/cm-worktrees");
924+
925+
onUpdateWorkspaceSettings.mockResolvedValueOnce(undefined);
926+
fireEvent.click(screen.getByRole("button", { name: "Save" }));
927+
928+
await waitFor(() => {
929+
expect(onUpdateWorkspaceSettings).toHaveBeenCalledTimes(2);
930+
});
931+
expect(onUpdateAppSettings).toHaveBeenCalledTimes(1);
932+
});
933+
856934
it("saves the setup script for the selected project", async () => {
857935
const onUpdateWorkspaceSettings = vi.fn().mockResolvedValue(undefined);
858936
renderEnvironmentsSection({ onUpdateWorkspaceSettings });

src/features/settings/hooks/useSettingsEnvironmentsSection.ts

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useEffect, useMemo, useState } from "react";
1+
import { useEffect, useMemo, useRef, useState } from "react";
22
import type { Dispatch, SetStateAction } from "react";
33
import type { AppSettings, WorkspaceInfo } from "@/types";
44
import { normalizeWorktreeSetupScript } from "@settings/components/settingsViewHelpers";
@@ -54,6 +54,7 @@ export const useSettingsEnvironmentsSection = ({
5454
);
5555
const [worktreesFolderDraft, setWorktreesFolderDraft] = useState("");
5656
const [worktreesFolderSaved, setWorktreesFolderSaved] = useState<string | null>(null);
57+
const lastGlobalWorktreesFolderProp = useRef(appSettings.globalWorktreesFolder);
5758

5859
const environmentWorkspace = useMemo(() => {
5960
if (mainWorkspaces.length === 0) return null;
@@ -82,15 +83,16 @@ export const useSettingsEnvironmentsSection = ({
8283
const worktreesFolderDirty = (worktreesFolderDraft.trim() || null) !== worktreesFolderSaved;
8384

8485
useEffect(() => {
85-
if (!globalWorktreesFolderDirty && globalWorktreesFolderSaved !== appSettings.globalWorktreesFolder) {
86+
const previousGlobalWorktreesFolder = lastGlobalWorktreesFolderProp.current;
87+
lastGlobalWorktreesFolderProp.current = appSettings.globalWorktreesFolder;
88+
if (previousGlobalWorktreesFolder === appSettings.globalWorktreesFolder) {
89+
return;
90+
}
91+
if (!globalWorktreesFolderDirty) {
8692
setGlobalWorktreesFolderSaved(appSettings.globalWorktreesFolder);
8793
setGlobalWorktreesFolderDraft(appSettings.globalWorktreesFolder ?? "");
8894
}
89-
}, [
90-
appSettings.globalWorktreesFolder,
91-
globalWorktreesFolderDirty,
92-
globalWorktreesFolderSaved,
93-
]);
95+
}, [appSettings.globalWorktreesFolder, globalWorktreesFolderDirty]);
9496

9597
useEffect(() => {
9698
if (!environmentWorkspace) {
@@ -147,28 +149,29 @@ export const useSettingsEnvironmentsSection = ({
147149
const nextScript = environmentDraftNormalized;
148150
const nextGlobalFolder = globalWorktreesFolderDraft.trim() || null;
149151
const nextFolder = worktreesFolderDraft.trim() || null;
152+
const globalWorktreesFolderChanged = nextGlobalFolder !== globalWorktreesFolderSaved;
150153
const workspaceSettingsDirty = environmentDirty || worktreesFolderDirty;
151154
setEnvironmentSaving(true);
152155
setEnvironmentError(null);
153156
try {
154-
if (nextGlobalFolder !== globalWorktreesFolderSaved) {
157+
if (globalWorktreesFolderChanged) {
155158
await onUpdateAppSettings({
156159
...appSettings,
157160
globalWorktreesFolder: nextGlobalFolder,
158161
});
162+
setGlobalWorktreesFolderSaved(nextGlobalFolder);
163+
setGlobalWorktreesFolderDraft(nextGlobalFolder ?? "");
159164
}
160165
if (environmentWorkspace && workspaceSettingsDirty) {
161166
await onUpdateWorkspaceSettings(environmentWorkspace.id, {
162167
worktreeSetupScript: nextScript,
163168
worktreesFolder: nextFolder,
164169
});
170+
setEnvironmentSavedScript(nextScript);
171+
setEnvironmentDraftScript(nextScript ?? "");
172+
setWorktreesFolderSaved(nextFolder);
173+
setWorktreesFolderDraft(nextFolder ?? "");
165174
}
166-
setGlobalWorktreesFolderSaved(nextGlobalFolder);
167-
setGlobalWorktreesFolderDraft(nextGlobalFolder ?? "");
168-
setEnvironmentSavedScript(nextScript);
169-
setEnvironmentDraftScript(nextScript ?? "");
170-
setWorktreesFolderSaved(nextFolder);
171-
setWorktreesFolderDraft(nextFolder ?? "");
172175
} catch (error) {
173176
setEnvironmentError(error instanceof Error ? error.message : String(error));
174177
} finally {

0 commit comments

Comments
 (0)