diff --git a/packages/app/e2e/browser/sidebar-workspace-rename.spec.ts b/packages/app/e2e/browser/sidebar-workspace-rename.spec.ts index 8a857281e7..e733f8004d 100644 --- a/packages/app/e2e/browser/sidebar-workspace-rename.spec.ts +++ b/packages/app/e2e/browser/sidebar-workspace-rename.spec.ts @@ -32,6 +32,15 @@ async function chooseRenameFromKebab(page: Page, workspaceId: string): Promise { + await page.evaluate(async () => { + const nextFrame = () => new Promise((resolve) => requestAnimationFrame(resolve)); + await nextFrame(); + await nextFrame(); + }); +} + interface Box { x: number; y: number; @@ -165,6 +174,38 @@ test.describe("Sidebar workspace rename", () => { } }); + test("an untouched in-place rename keeps a title set elsewhere while it was open", async ({ + page, + }) => { + const workspace = await seedWorkspace({ repoPrefix: "sidebar-rename-external-" }); + + try { + const gate = await installDaemonWebSocketGate(page); + await page.setViewportSize({ width: 1280, height: 800 }); + await gotoAppShell(page); + const row = page.getByTestId(workspaceRowTestId(workspace.workspaceId)); + const input = page.getByTestId(workspaceRenameInputTestId(workspace.workspaceId)); + + await chooseRenameFromKebab(page, workspace.workspaceId); + await expect(input).toBeFocused(); + await expect(input).toHaveValue("main"); + + // Another client renames the workspace while this field is open and untouched. + gate.holdNextServerMessage("workspace_update"); + await workspace.client.setWorkspaceTitle(workspace.workspaceId, "External title"); + await gate.waitForHeldServerMessage("workspace_update"); + gate.releaseHeldServerMessage("workspace_update"); + await waitForTwoFrames(page); + + await page.mouse.click(900, 700); + await expect(input).toHaveCount(0); + await expect(row).toContainText("External title"); + expect(gate.getClientRequestCount("workspace.title.set.request")).toBe(0); + } finally { + await workspace.cleanup(); + } + }); + test("below the lg breakpoint renaming opens the modal", async ({ page }) => { const workspace = await seedWorkspace({ repoPrefix: "sidebar-rename-modal-" }); diff --git a/packages/app/src/workspace-rename/internal/use-sidebar-workspace-rename.tsx b/packages/app/src/workspace-rename/internal/use-sidebar-workspace-rename.tsx index 88e05d5ce3..496f79c4af 100644 --- a/packages/app/src/workspace-rename/internal/use-sidebar-workspace-rename.tsx +++ b/packages/app/src/workspace-rename/internal/use-sidebar-workspace-rename.tsx @@ -14,14 +14,21 @@ export interface SidebarRenamableWorkspace extends RenamableWorkspace { } /** + * An inline session owns the text editing started from. The field keeps that text while it is + * open, so a rename arriving from elsewhere must not make an untouched field read as an edit. + * * `saving` and `saved` keep the field on screen holding the submitted text. The daemon answers * the request before it pushes the updated workspace, so closing on the answer would show the * old title for a beat. `saved` ends when the row's title moves off the one editing started from. */ -type RenameSession = - | { kind: "closed" } - | { kind: "modal" } - | { kind: "inline"; phase: "editing" | "saving" | "saved"; titleAtStart: string | null }; +interface InlineRenameSession { + kind: "inline"; + phase: "editing" | "saving" | "saved"; + initialValue: string; + titleAtStart: string | null; +} + +type RenameSession = { kind: "closed" } | { kind: "modal" } | InlineRenameSession; const CLOSED: RenameSession = { kind: "closed" }; @@ -50,25 +57,26 @@ export function useSidebarWorkspaceRename( setSession(CLOSED); } + const initialValue = workspaceRenameInitialValue(workspace); const begin = useCallback(() => { setSession( resolveWorkspaceRenameSurface() === "inline" - ? { kind: "inline", phase: "editing", titleAtStart: currentTitle } + ? { kind: "inline", phase: "editing", initialValue, titleAtStart: currentTitle } : { kind: "modal" }, ); - }, [currentTitle]); + }, [currentTitle, initialValue]); const close = useCallback(() => setSession(CLOSED), []); - const initialValue = workspaceRenameInitialValue(workspace); const commit = useCallback( (text: string) => { + if (session.kind !== "inline") return; const title = text.trim(); - if (title.length === 0 || title === initialValue) { + if (title.length === 0 || title === session.initialValue) { setSession(CLOSED); return; } - setSession({ kind: "inline", phase: "saving", titleAtStart: currentTitle }); + setSession({ ...session, phase: "saving" }); setWorkspaceTitle(title).then( () => setSession((previous) => @@ -82,13 +90,13 @@ export function useSidebarWorkspaceRename( }, ); }, - [currentTitle, initialValue, setWorkspaceTitle, toast], + [session, setWorkspaceTitle, toast], ); const titleEditor = session.kind === "inline" ? (