diff --git a/packages/web/src/lib/workspace.ts b/packages/web/src/lib/workspace.ts index 070c6fd8..83f21ebf 100644 --- a/packages/web/src/lib/workspace.ts +++ b/packages/web/src/lib/workspace.ts @@ -13,7 +13,7 @@ import { useQuery, useQueryClient, } from "@tanstack/react-query"; -import { useParams } from "@tanstack/react-router"; +import { useNavigate, useParams } from "@tanstack/react-router"; import { Effect, Schema } from "effect"; import { useSyncExternalStore } from "react"; import { client } from "@/api.ts"; @@ -189,17 +189,29 @@ export function useCancelWorkspaceInvitation(workspaceId: string) { } /** - * Leaving takes the whole client with it: every pod, agent and conversation on - * screen belonged to a workspace this person is no longer in, so the cache is - * cleared rather than refetched. + * Leaving takes the workspace out of the client rather than refetching it: the + * API answers anything asked about it with a 403 once this person is gone. + * + * Like deleting, the workspace leaves the list at once and `/` opens another, + * or onboarding when there is none. Once its screens have closed, what is + * cached under its id goes too. Pods and conversations are cached by their own + * ids, so theirs expires unread with the cache's `gcTime` instead. */ export function useLeaveWorkspace(workspaceId: string) { const queries = useQueryClient(); + const navigate = useNavigate(); return useMutation({ mutationFn: () => Effect.runPromise(client.api.workspaces.leave({ params: { workspace: workspaceId } })), onSuccess: async () => { - await queries.invalidateQueries(); + queries.setQueryData(workspacesQuery.queryKey, (workspaces) => + workspaces?.filter((workspace) => workspace.id !== workspaceId), + ); + await navigate({ to: "/", replace: true }); + queries.removeQueries({ + type: "inactive", + predicate: (query) => query.queryKey.includes(workspaceId), + }); }, }); } diff --git a/packages/web/src/shell/shell.test.tsx b/packages/web/src/shell/shell.test.tsx index 7669fd39..ab13282a 100644 --- a/packages/web/src/shell/shell.test.tsx +++ b/packages/web/src/shell/shell.test.tsx @@ -2066,6 +2066,29 @@ describe("workspace settings", () => { }); }); + it("opens another workspace once you leave, asking nothing more of the one you left", async () => { + const other = { ...workspace, id: "0199a3a0-0000-7000-8000-0000000000f9", slug: "nitric" }; + client.api.workspaces.list.mockReturnValue(Effect.succeed([workspace, other])); + client.api.workspaces.leave.mockImplementation(() => { + client.api.workspaces.list.mockReturnValue(Effect.succeed([other])); + client.api.pods.list.mockClear(); + client.api.workspaceAccess.mockClear(); + return Effect.void; + }); + const router = mount(samPage); + + fireEvent.click(await screen.findByRole("button", { name: "Leave workspace" })); + await screen.findByRole("heading", { name: "Leave this workspace?" }); + fireEvent.click(screen.getByRole("button", { name: "Leave" })); + + await waitFor(() => expect(router.state.location.pathname).toMatch(/^\/nitric\//)); + const askedOfTheLeftWorkspace = [ + ...client.api.pods.list.mock.calls, + ...client.api.workspaceAccess.mock.calls, + ].filter(([request]) => request.params.workspace === workspace.id); + expect(askedOfTheLeftWorkspace).toEqual([]); + }); + it("lists an invitation that has not been accepted, and can withdraw it", async () => { mount("/suga/settings/members");