From c76ba2f58e4282ebc1e00e4269fe627f079319d0 Mon Sep 17 00:00:00 2001 From: Naiyuan Qing <145280634+NevilleQingNY@users.noreply.github.com> Date: Mon, 13 Apr 2026 14:00:28 +0800 Subject: [PATCH] fix(workspace): seed React Query cache at all list-acquisition points - staleTime: 0 on fetchQuery after leave/delete so fresh data is fetched - setQueryData before switchWorkspace in createWorkspace so sidebar is consistent on first render - seed workspaceKeys.list() cache in login, Google callback, and settings save so the first useQuery(workspaceListOptions()) hit is free - remove dead onError from WorkspaceStoreOptions (used only by the deleted refreshWorkspaces action) Co-Authored-By: Claude Sonnet 4.6 --- apps/web/app/auth/callback/page.tsx | 6 +++++- packages/core/workspace/mutations.ts | 12 +++++++----- packages/core/workspace/store.ts | 1 - packages/views/auth/login-page.tsx | 6 +++++- packages/views/settings/components/workspace-tab.tsx | 9 +++++++-- 5 files changed, 24 insertions(+), 10 deletions(-) diff --git a/apps/web/app/auth/callback/page.tsx b/apps/web/app/auth/callback/page.tsx index d49610375..f277b7e29 100644 --- a/apps/web/app/auth/callback/page.tsx +++ b/apps/web/app/auth/callback/page.tsx @@ -2,8 +2,10 @@ import { Suspense, useEffect, useState } from "react"; import { useSearchParams, useRouter } from "next/navigation"; +import { useQueryClient } from "@tanstack/react-query"; import { useAuthStore } from "@multica/core/auth"; import { useWorkspaceStore } from "@multica/core/workspace"; +import { workspaceKeys } from "@multica/core/workspace/queries"; import { api } from "@multica/core/api"; import { Card, @@ -17,6 +19,7 @@ import { Loader2 } from "lucide-react"; function CallbackContent() { const router = useRouter(); const searchParams = useSearchParams(); + const qc = useQueryClient(); const loginWithGoogle = useAuthStore((s) => s.loginWithGoogle); const hydrateWorkspace = useWorkspaceStore((s) => s.hydrateWorkspace); const [error, setError] = useState(""); @@ -39,6 +42,7 @@ function CallbackContent() { loginWithGoogle(code, redirectUri) .then(async () => { const wsList = await api.listWorkspaces(); + qc.setQueryData(workspaceKeys.list(), wsList); const lastWsId = localStorage.getItem("multica_workspace_id"); await hydrateWorkspace(wsList, lastWsId); router.push("/issues"); @@ -46,7 +50,7 @@ function CallbackContent() { .catch((err) => { setError(err instanceof Error ? err.message : "Login failed"); }); - }, [searchParams, loginWithGoogle, hydrateWorkspace, router]); + }, [searchParams, loginWithGoogle, hydrateWorkspace, router, qc]); if (error) { return ( diff --git a/packages/core/workspace/mutations.ts b/packages/core/workspace/mutations.ts index 512a01bf3..b4b80fdbd 100644 --- a/packages/core/workspace/mutations.ts +++ b/packages/core/workspace/mutations.ts @@ -1,4 +1,5 @@ import { useMutation, useQueryClient } from "@tanstack/react-query"; +import type { Workspace } from "../types"; import { api } from "../api"; import { workspaceKeys, workspaceListOptions } from "./queries"; import { useWorkspaceStore } from "./index"; @@ -9,7 +10,8 @@ export function useCreateWorkspace() { mutationFn: (data: { name: string; slug: string; description?: string }) => api.createWorkspace(data), onSuccess: (newWs) => { - // Switch to the newly created workspace immediately + // Add to cache before switching so sidebar list is consistent on first render + qc.setQueryData(workspaceKeys.list(), (old: Workspace[] = []) => [...old, newWs]); useWorkspaceStore.getState().switchWorkspace(newWs); }, onSettled: () => { @@ -25,8 +27,8 @@ export function useLeaveWorkspace() { onSuccess: async (_, workspaceId) => { const currentWsId = useWorkspaceStore.getState().workspace?.id; if (currentWsId === workspaceId) { - // Left our current workspace — refetch and pick another - const wsList = await qc.fetchQuery(workspaceListOptions()); + // staleTime: 0 forces a real network fetch — cache still has the left workspace + const wsList = await qc.fetchQuery({ ...workspaceListOptions(), staleTime: 0 }); useWorkspaceStore.getState().hydrateWorkspace(wsList); } }, @@ -43,8 +45,8 @@ export function useDeleteWorkspace() { onSuccess: async (_, workspaceId) => { const currentWsId = useWorkspaceStore.getState().workspace?.id; if (currentWsId === workspaceId) { - // Deleted our current workspace — refetch and pick another - const wsList = await qc.fetchQuery(workspaceListOptions()); + // staleTime: 0 forces a real network fetch — cache still has the deleted workspace + const wsList = await qc.fetchQuery({ ...workspaceListOptions(), staleTime: 0 }); useWorkspaceStore.getState().hydrateWorkspace(wsList); } }, diff --git a/packages/core/workspace/store.ts b/packages/core/workspace/store.ts index d2ba67122..b001d0176 100644 --- a/packages/core/workspace/store.ts +++ b/packages/core/workspace/store.ts @@ -8,7 +8,6 @@ const logger = createLogger("workspace-store"); interface WorkspaceStoreOptions { storage?: StorageAdapter; - onError?: (message: string) => void; } interface WorkspaceState { diff --git a/packages/views/auth/login-page.tsx b/packages/views/auth/login-page.tsx index 64ccee6eb..daf8ab5c1 100644 --- a/packages/views/auth/login-page.tsx +++ b/packages/views/auth/login-page.tsx @@ -1,6 +1,7 @@ "use client"; import { useState, useEffect, useCallback, type ReactNode } from "react"; +import { useQueryClient } from "@tanstack/react-query"; import { Card, CardHeader, @@ -19,6 +20,7 @@ import { } from "@multica/ui/components/ui/input-otp"; import { useAuthStore } from "@multica/core/auth"; import { useWorkspaceStore } from "@multica/core/workspace"; +import { workspaceKeys } from "@multica/core/workspace/queries"; import { api } from "@multica/core/api"; import type { User } from "@multica/core/types"; @@ -87,6 +89,7 @@ export function LoginPage({ lastWorkspaceId, onTokenObtained, }: LoginPageProps) { + const qc = useQueryClient(); const [step, setStep] = useState<"email" | "code" | "cli_confirm">("email"); const [email, setEmail] = useState(""); const [code, setCode] = useState(""); @@ -167,6 +170,7 @@ export function LoginPage({ // Normal path await useAuthStore.getState().verifyCode(email, value); const wsList = await api.listWorkspaces(); + qc.setQueryData(workspaceKeys.list(), wsList); useWorkspaceStore.getState().hydrateWorkspace(wsList, lastWorkspaceId); onTokenObtained?.(); onSuccess(); @@ -178,7 +182,7 @@ export function LoginPage({ setLoading(false); } }, - [email, onSuccess, cliCallback, lastWorkspaceId, onTokenObtained], + [email, onSuccess, cliCallback, lastWorkspaceId, onTokenObtained, qc], ); const handleResend = async () => { diff --git a/packages/views/settings/components/workspace-tab.tsx b/packages/views/settings/components/workspace-tab.tsx index 147b4ab48..9ee1f51a0 100644 --- a/packages/views/settings/components/workspace-tab.tsx +++ b/packages/views/settings/components/workspace-tab.tsx @@ -18,19 +18,21 @@ import { AlertDialogAction, } from "@multica/ui/components/ui/alert-dialog"; import { toast } from "sonner"; -import { useQuery } from "@tanstack/react-query"; +import { useQuery, useQueryClient } from "@tanstack/react-query"; import { useAuthStore } from "@multica/core/auth"; import { useWorkspaceStore } from "@multica/core/workspace"; import { useLeaveWorkspace, useDeleteWorkspace } from "@multica/core/workspace/mutations"; import { useWorkspaceId } from "@multica/core/hooks"; -import { memberListOptions } from "@multica/core/workspace/queries"; +import { memberListOptions, workspaceKeys } from "@multica/core/workspace/queries"; import { api } from "@multica/core/api"; +import type { Workspace } from "@multica/core/types"; export function WorkspaceTab() { const user = useAuthStore((s) => s.user); const workspace = useWorkspaceStore((s) => s.workspace); const wsId = useWorkspaceId(); const { data: members = [] } = useQuery(memberListOptions(wsId)); + const qc = useQueryClient(); const updateWorkspace = useWorkspaceStore((s) => s.updateWorkspace); const leaveWorkspace = useLeaveWorkspace(); const deleteWorkspace = useDeleteWorkspace(); @@ -67,6 +69,9 @@ export function WorkspaceTab() { context, }); updateWorkspace(updated); + qc.setQueryData(workspaceKeys.list(), (old: Workspace[] | undefined) => + old?.map((ws) => (ws.id === updated.id ? updated : ws)), + ); toast.success("Workspace settings saved"); } catch (e) { toast.error(e instanceof Error ? e.message : "Failed to save workspace settings");