Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 17 additions & 5 deletions packages/web/src/lib/workspace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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),
});
},
});
}
Expand Down
23 changes: 23 additions & 0 deletions packages/web/src/shell/shell.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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");

Expand Down
Loading