diff --git a/packages/app/e2e/browser/sidebar-workspace-rename.spec.ts b/packages/app/e2e/browser/sidebar-workspace-rename.spec.ts index e733f8004d..2a10e52c03 100644 --- a/packages/app/e2e/browser/sidebar-workspace-rename.spec.ts +++ b/packages/app/e2e/browser/sidebar-workspace-rename.spec.ts @@ -41,6 +41,22 @@ async function waitForTwoFrames(page: Page): Promise { }); } +type DaemonWebSocketGate = Awaited>; + +/** Sets the title from a second daemon client and waits until this page has rendered it. */ +async function renameFromAnotherClient( + page: Page, + gate: DaemonWebSocketGate, + workspace: Awaited>, + title: string, +): Promise { + gate.holdNextServerMessage("workspace_update"); + await workspace.client.setWorkspaceTitle(workspace.workspaceId, title); + await gate.waitForHeldServerMessage("workspace_update"); + gate.releaseHeldServerMessage("workspace_update"); + await waitForTwoFrames(page); +} + interface Box { x: number; y: number; @@ -190,12 +206,7 @@ test.describe("Sidebar workspace rename", () => { 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 renameFromAnotherClient(page, gate, workspace, "External title"); await page.mouse.click(900, 700); await expect(input).toHaveCount(0); @@ -206,6 +217,38 @@ test.describe("Sidebar workspace rename", () => { } }); + test("saving after a rename elsewhere holds the submitted title until it arrives", async ({ + page, + }) => { + const workspace = await seedWorkspace({ repoPrefix: "sidebar-rename-external-save-" }); + + 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 renameFromAnotherClient(page, gate, workspace, "External title"); + + await input.fill("Submitted title"); + gate.holdNextServerMessage("workspace_update"); + await page.keyboard.press("Enter"); + await gate.waitForHeldServerMessage("workspace_update"); + + await expect(input).toHaveValue("Submitted title"); + await expect(row).not.toContainText("External title"); + + gate.releaseHeldServerMessage("workspace_update"); + await expect(input).toHaveCount(0); + await expect(row).toContainText("Submitted title"); + } 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 496f79c4af..dd27e9a508 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 @@ -19,14 +19,13 @@ export interface SidebarRenamableWorkspace extends RenamableWorkspace { * * `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. + * old title for a beat. `saved` ends when the row's title moves off the one it had at submit, + * which is not the one editing started from when another client renamed it in between. */ -interface InlineRenameSession { - kind: "inline"; - phase: "editing" | "saving" | "saved"; - initialValue: string; - titleAtStart: string | null; -} +type InlineRenameSession = { kind: "inline"; initialValue: string } & ( + | { phase: "editing" } + | { phase: "saving" | "saved"; titleBeforeSave: string | null } +); type RenameSession = { kind: "closed" } | { kind: "modal" } | InlineRenameSession; @@ -52,7 +51,7 @@ export function useSidebarWorkspaceRename( if ( session.kind === "inline" && session.phase === "saved" && - currentTitle !== session.titleAtStart + currentTitle !== session.titleBeforeSave ) { setSession(CLOSED); } @@ -61,10 +60,10 @@ export function useSidebarWorkspaceRename( const begin = useCallback(() => { setSession( resolveWorkspaceRenameSurface() === "inline" - ? { kind: "inline", phase: "editing", initialValue, titleAtStart: currentTitle } + ? { kind: "inline", phase: "editing", initialValue } : { kind: "modal" }, ); - }, [currentTitle, initialValue]); + }, [initialValue]); const close = useCallback(() => setSession(CLOSED), []); @@ -76,7 +75,12 @@ export function useSidebarWorkspaceRename( setSession(CLOSED); return; } - setSession({ ...session, phase: "saving" }); + setSession({ + kind: "inline", + initialValue: session.initialValue, + phase: "saving", + titleBeforeSave: currentTitle, + }); setWorkspaceTitle(title).then( () => setSession((previous) => @@ -90,7 +94,7 @@ export function useSidebarWorkspaceRename( }, ); }, - [session, setWorkspaceTitle, toast], + [currentTitle, session, setWorkspaceTitle, toast], ); const titleEditor =