diff --git a/apps/web/app/(dashboard)/_components/app-sidebar.tsx b/apps/web/app/(dashboard)/_components/app-sidebar.tsx index 683cd9ad..bbcea35f 100644 --- a/apps/web/app/(dashboard)/_components/app-sidebar.tsx +++ b/apps/web/app/(dashboard)/_components/app-sidebar.tsx @@ -1,5 +1,6 @@ "use client"; +import React from "react"; import Link from "next/link"; import { usePathname, useRouter } from "next/navigation"; import { @@ -77,7 +78,10 @@ export function AppSidebar() { const wsId = useWorkspaceId(); const { data: inboxItems = [] } = useQuery(inboxListOptions(wsId)); - const unreadCount = deduplicateInboxItems(inboxItems).filter((i) => !i.read).length; + const unreadCount = React.useMemo( + () => deduplicateInboxItems(inboxItems).filter((i) => !i.read).length, + [inboxItems], + ); const logout = () => { router.push("/"); diff --git a/apps/web/core/index.ts b/apps/web/core/index.ts index 26716d72..97d2430c 100644 --- a/apps/web/core/index.ts +++ b/apps/web/core/index.ts @@ -1,3 +1,3 @@ -export { createQueryClient, getQueryClient, setQueryClient } from "./query-client"; +export { createQueryClient } from "./query-client"; export { QueryProvider } from "./provider"; export { useWorkspaceId } from "./hooks"; diff --git a/apps/web/core/issues/mutations.ts b/apps/web/core/issues/mutations.ts index 89d0ef5a..e05d17de 100644 --- a/apps/web/core/issues/mutations.ts +++ b/apps/web/core/issues/mutations.ts @@ -60,6 +60,9 @@ export function useUpdateIssue() { if (ctx?.prevDetail) qc.setQueryData(issueKeys.detail(wsId, ctx.id), ctx.prevDetail); }, + onSettled: (_data, _err, vars) => { + qc.invalidateQueries({ queryKey: issueKeys.detail(wsId, vars.id) }); + }, }); } diff --git a/apps/web/core/issues/queries.ts b/apps/web/core/issues/queries.ts index b848cb47..bdf50d03 100644 --- a/apps/web/core/issues/queries.ts +++ b/apps/web/core/issues/queries.ts @@ -12,6 +12,11 @@ export const issueKeys = { ["issues", "subscribers", issueId] as const, }; +/** + * CACHE SHAPE NOTE: The raw cache stores ListIssuesResponse ({ issues, total }), + * but `select` transforms it to Issue[] for consumers. Mutations and ws-updaters + * must use setQueryData(...) — NOT setQueryData. + */ export function issueListOptions(wsId: string) { return queryOptions({ queryKey: issueKeys.list(wsId), diff --git a/apps/web/core/provider.tsx b/apps/web/core/provider.tsx index 7723c771..41331d2e 100644 --- a/apps/web/core/provider.tsx +++ b/apps/web/core/provider.tsx @@ -3,15 +3,11 @@ import { useState } from "react"; import { QueryClientProvider } from "@tanstack/react-query"; import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; -import { createQueryClient, setQueryClient } from "./query-client"; +import { createQueryClient } from "./query-client"; import type { ReactNode } from "react"; export function QueryProvider({ children }: { children: ReactNode }) { - const [queryClient] = useState(() => { - const client = createQueryClient(); - setQueryClient(client); - return client; - }); + const [queryClient] = useState(createQueryClient); return ( {children} diff --git a/apps/web/core/query-client.ts b/apps/web/core/query-client.ts index b882845b..be5ebea0 100644 --- a/apps/web/core/query-client.ts +++ b/apps/web/core/query-client.ts @@ -1,7 +1,5 @@ import { QueryClient } from "@tanstack/react-query"; -let _queryClient: QueryClient | null = null; - export function createQueryClient(): QueryClient { return new QueryClient({ defaultOptions: { @@ -18,14 +16,3 @@ export function createQueryClient(): QueryClient { }, }); } - -/** Called by QueryProvider on mount to register the singleton. */ -export function setQueryClient(client: QueryClient) { - _queryClient = client; -} - -/** Access QueryClient outside React tree (WS handlers, Zustand actions). */ -export function getQueryClient(): QueryClient { - if (!_queryClient) throw new Error("QueryClient not initialized"); - return _queryClient; -} diff --git a/apps/web/features/editor/content-editor.tsx b/apps/web/features/editor/content-editor.tsx index 85e040d2..3e125115 100644 --- a/apps/web/features/editor/content-editor.tsx +++ b/apps/web/features/editor/content-editor.tsx @@ -34,6 +34,7 @@ import { import { useEditor, EditorContent } from "@tiptap/react"; import { cn } from "@/lib/utils"; import type { UploadResult } from "@/shared/hooks/use-file-upload"; +import { useQueryClient } from "@tanstack/react-query"; import { createEditorExtensions } from "./extensions"; import { uploadAndInsertFile } from "./extensions/file-upload"; import { preprocessMarkdown } from "./utils/preprocess"; @@ -94,6 +95,8 @@ const ContentEditor = forwardRef( onBlurRef.current = onBlur; onUploadFileRef.current = onUploadFile; + const queryClient = useQueryClient(); + const editor = useEditor({ immediatelyRender: false, editable, @@ -102,6 +105,7 @@ const ContentEditor = forwardRef( extensions: createEditorExtensions({ editable, placeholder: placeholderText, + queryClient, onSubmitRef, onUploadFileRef, }), diff --git a/apps/web/features/editor/extensions/index.ts b/apps/web/features/editor/extensions/index.ts index c182dd8a..4052c6d5 100644 --- a/apps/web/features/editor/extensions/index.ts +++ b/apps/web/features/editor/extensions/index.ts @@ -76,6 +76,7 @@ const ImageExtension = Image.extend({ export interface EditorExtensionsOptions { editable: boolean; placeholder?: string; + queryClient?: import("@tanstack/react-query").QueryClient; onSubmitRef?: RefObject<(() => void) | undefined>; onUploadFileRef?: RefObject< ((file: File) => Promise) | undefined @@ -107,7 +108,7 @@ export function createEditorExtensions( Markdown, BaseMentionExtension.configure({ HTMLAttributes: { class: "mention" }, - ...(editable ? { suggestion: createMentionSuggestion() } : {}), + ...(editable && options.queryClient ? { suggestion: createMentionSuggestion(options.queryClient) } : {}), }), ]; diff --git a/apps/web/features/editor/extensions/mention-suggestion.tsx b/apps/web/features/editor/extensions/mention-suggestion.tsx index 4c15204a..1b9ed73e 100644 --- a/apps/web/features/editor/extensions/mention-suggestion.tsx +++ b/apps/web/features/editor/extensions/mention-suggestion.tsx @@ -10,8 +10,8 @@ import { } from "react"; import { ReactRenderer } from "@tiptap/react"; import { computePosition, offset, flip, shift } from "@floating-ui/dom"; +import type { QueryClient } from "@tanstack/react-query"; import { useWorkspaceStore } from "@/features/workspace"; -import { getQueryClient } from "@core/query-client"; import { issueKeys } from "@core/issues/queries"; import { workspaceKeys } from "@core/workspace/queries"; import type { Issue, ListIssuesResponse, MemberWithUser, Agent } from "@/shared/types"; @@ -213,18 +213,19 @@ function MentionRow({ // Suggestion config factory // --------------------------------------------------------------------------- -export function createMentionSuggestion(): Omit< +export function createMentionSuggestion(qc: QueryClient): Omit< SuggestionOptions, "editor" > { return { items: ({ query }) => { const wsId = useWorkspaceStore.getState().workspace?.id; - const members: MemberWithUser[] = wsId ? getQueryClient().getQueryData(workspaceKeys.members(wsId)) ?? [] : []; - const agents: Agent[] = wsId ? getQueryClient().getQueryData(workspaceKeys.agents(wsId)) ?? [] : []; + const members: MemberWithUser[] = wsId ? qc.getQueryData(workspaceKeys.members(wsId)) ?? [] : []; + const agents: Agent[] = wsId ? qc.getQueryData(workspaceKeys.agents(wsId)) ?? [] : []; const issues: Issue[] = wsId - ? getQueryClient().getQueryData(issueKeys.list(wsId))?.issues ?? [] + ? qc.getQueryData(issueKeys.list(wsId))?.issues ?? [] : []; + const q = query.toLowerCase(); // Show "All members" option when query is empty or matches "all"