mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-05 13:29:44 +02:00
fix: mention closure, onSettled invalidation, cleanup singleton
- Fix Tiptap mention: pass QueryClient via closure from ContentEditor instead of getQueryClient() singleton (resolves @mention empty list) - Add onSettled invalidation to useUpdateIssue (prevents cache drift with staleTime: Infinity + self-event WS filter) - Add cache shape comment to issueListOptions (select transforms ListIssuesResponse → Issue[], but cache stores raw response) - Memoize sidebar inbox dedup computation - Remove dead getQueryClient/setQueryClient singleton + window property - Remove ActorSync component and _members/_agents Zustand mirror (superseded by closure approach) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { usePathname, useRouter } from "next/navigation";
|
import { usePathname, useRouter } from "next/navigation";
|
||||||
import {
|
import {
|
||||||
@@ -77,7 +78,10 @@ export function AppSidebar() {
|
|||||||
|
|
||||||
const wsId = useWorkspaceId();
|
const wsId = useWorkspaceId();
|
||||||
const { data: inboxItems = [] } = useQuery(inboxListOptions(wsId));
|
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 = () => {
|
const logout = () => {
|
||||||
router.push("/");
|
router.push("/");
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
export { createQueryClient, getQueryClient, setQueryClient } from "./query-client";
|
export { createQueryClient } from "./query-client";
|
||||||
export { QueryProvider } from "./provider";
|
export { QueryProvider } from "./provider";
|
||||||
export { useWorkspaceId } from "./hooks";
|
export { useWorkspaceId } from "./hooks";
|
||||||
|
|||||||
@@ -60,6 +60,9 @@ export function useUpdateIssue() {
|
|||||||
if (ctx?.prevDetail)
|
if (ctx?.prevDetail)
|
||||||
qc.setQueryData(issueKeys.detail(wsId, ctx.id), ctx.prevDetail);
|
qc.setQueryData(issueKeys.detail(wsId, ctx.id), ctx.prevDetail);
|
||||||
},
|
},
|
||||||
|
onSettled: (_data, _err, vars) => {
|
||||||
|
qc.invalidateQueries({ queryKey: issueKeys.detail(wsId, vars.id) });
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,11 @@ export const issueKeys = {
|
|||||||
["issues", "subscribers", issueId] as const,
|
["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<ListIssuesResponse>(...) — NOT setQueryData<Issue[]>.
|
||||||
|
*/
|
||||||
export function issueListOptions(wsId: string) {
|
export function issueListOptions(wsId: string) {
|
||||||
return queryOptions({
|
return queryOptions({
|
||||||
queryKey: issueKeys.list(wsId),
|
queryKey: issueKeys.list(wsId),
|
||||||
|
|||||||
@@ -3,15 +3,11 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { QueryClientProvider } from "@tanstack/react-query";
|
import { QueryClientProvider } from "@tanstack/react-query";
|
||||||
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
|
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
|
||||||
import { createQueryClient, setQueryClient } from "./query-client";
|
import { createQueryClient } from "./query-client";
|
||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
|
|
||||||
export function QueryProvider({ children }: { children: ReactNode }) {
|
export function QueryProvider({ children }: { children: ReactNode }) {
|
||||||
const [queryClient] = useState(() => {
|
const [queryClient] = useState(createQueryClient);
|
||||||
const client = createQueryClient();
|
|
||||||
setQueryClient(client);
|
|
||||||
return client;
|
|
||||||
});
|
|
||||||
return (
|
return (
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
import { QueryClient } from "@tanstack/react-query";
|
import { QueryClient } from "@tanstack/react-query";
|
||||||
|
|
||||||
let _queryClient: QueryClient | null = null;
|
|
||||||
|
|
||||||
export function createQueryClient(): QueryClient {
|
export function createQueryClient(): QueryClient {
|
||||||
return new QueryClient({
|
return new QueryClient({
|
||||||
defaultOptions: {
|
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;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ import {
|
|||||||
import { useEditor, EditorContent } from "@tiptap/react";
|
import { useEditor, EditorContent } from "@tiptap/react";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import type { UploadResult } from "@/shared/hooks/use-file-upload";
|
import type { UploadResult } from "@/shared/hooks/use-file-upload";
|
||||||
|
import { useQueryClient } from "@tanstack/react-query";
|
||||||
import { createEditorExtensions } from "./extensions";
|
import { createEditorExtensions } from "./extensions";
|
||||||
import { uploadAndInsertFile } from "./extensions/file-upload";
|
import { uploadAndInsertFile } from "./extensions/file-upload";
|
||||||
import { preprocessMarkdown } from "./utils/preprocess";
|
import { preprocessMarkdown } from "./utils/preprocess";
|
||||||
@@ -94,6 +95,8 @@ const ContentEditor = forwardRef<ContentEditorRef, ContentEditorProps>(
|
|||||||
onBlurRef.current = onBlur;
|
onBlurRef.current = onBlur;
|
||||||
onUploadFileRef.current = onUploadFile;
|
onUploadFileRef.current = onUploadFile;
|
||||||
|
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
const editor = useEditor({
|
const editor = useEditor({
|
||||||
immediatelyRender: false,
|
immediatelyRender: false,
|
||||||
editable,
|
editable,
|
||||||
@@ -102,6 +105,7 @@ const ContentEditor = forwardRef<ContentEditorRef, ContentEditorProps>(
|
|||||||
extensions: createEditorExtensions({
|
extensions: createEditorExtensions({
|
||||||
editable,
|
editable,
|
||||||
placeholder: placeholderText,
|
placeholder: placeholderText,
|
||||||
|
queryClient,
|
||||||
onSubmitRef,
|
onSubmitRef,
|
||||||
onUploadFileRef,
|
onUploadFileRef,
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -76,6 +76,7 @@ const ImageExtension = Image.extend({
|
|||||||
export interface EditorExtensionsOptions {
|
export interface EditorExtensionsOptions {
|
||||||
editable: boolean;
|
editable: boolean;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
|
queryClient?: import("@tanstack/react-query").QueryClient;
|
||||||
onSubmitRef?: RefObject<(() => void) | undefined>;
|
onSubmitRef?: RefObject<(() => void) | undefined>;
|
||||||
onUploadFileRef?: RefObject<
|
onUploadFileRef?: RefObject<
|
||||||
((file: File) => Promise<UploadResult | null>) | undefined
|
((file: File) => Promise<UploadResult | null>) | undefined
|
||||||
@@ -107,7 +108,7 @@ export function createEditorExtensions(
|
|||||||
Markdown,
|
Markdown,
|
||||||
BaseMentionExtension.configure({
|
BaseMentionExtension.configure({
|
||||||
HTMLAttributes: { class: "mention" },
|
HTMLAttributes: { class: "mention" },
|
||||||
...(editable ? { suggestion: createMentionSuggestion() } : {}),
|
...(editable && options.queryClient ? { suggestion: createMentionSuggestion(options.queryClient) } : {}),
|
||||||
}),
|
}),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -10,8 +10,8 @@ import {
|
|||||||
} from "react";
|
} from "react";
|
||||||
import { ReactRenderer } from "@tiptap/react";
|
import { ReactRenderer } from "@tiptap/react";
|
||||||
import { computePosition, offset, flip, shift } from "@floating-ui/dom";
|
import { computePosition, offset, flip, shift } from "@floating-ui/dom";
|
||||||
|
import type { QueryClient } from "@tanstack/react-query";
|
||||||
import { useWorkspaceStore } from "@/features/workspace";
|
import { useWorkspaceStore } from "@/features/workspace";
|
||||||
import { getQueryClient } from "@core/query-client";
|
|
||||||
import { issueKeys } from "@core/issues/queries";
|
import { issueKeys } from "@core/issues/queries";
|
||||||
import { workspaceKeys } from "@core/workspace/queries";
|
import { workspaceKeys } from "@core/workspace/queries";
|
||||||
import type { Issue, ListIssuesResponse, MemberWithUser, Agent } from "@/shared/types";
|
import type { Issue, ListIssuesResponse, MemberWithUser, Agent } from "@/shared/types";
|
||||||
@@ -213,18 +213,19 @@ function MentionRow({
|
|||||||
// Suggestion config factory
|
// Suggestion config factory
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
export function createMentionSuggestion(): Omit<
|
export function createMentionSuggestion(qc: QueryClient): Omit<
|
||||||
SuggestionOptions<MentionItem>,
|
SuggestionOptions<MentionItem>,
|
||||||
"editor"
|
"editor"
|
||||||
> {
|
> {
|
||||||
return {
|
return {
|
||||||
items: ({ query }) => {
|
items: ({ query }) => {
|
||||||
const wsId = useWorkspaceStore.getState().workspace?.id;
|
const wsId = useWorkspaceStore.getState().workspace?.id;
|
||||||
const members: MemberWithUser[] = wsId ? getQueryClient().getQueryData(workspaceKeys.members(wsId)) ?? [] : [];
|
const members: MemberWithUser[] = wsId ? qc.getQueryData(workspaceKeys.members(wsId)) ?? [] : [];
|
||||||
const agents: Agent[] = wsId ? getQueryClient().getQueryData(workspaceKeys.agents(wsId)) ?? [] : [];
|
const agents: Agent[] = wsId ? qc.getQueryData(workspaceKeys.agents(wsId)) ?? [] : [];
|
||||||
const issues: Issue[] = wsId
|
const issues: Issue[] = wsId
|
||||||
? getQueryClient().getQueryData<ListIssuesResponse>(issueKeys.list(wsId))?.issues ?? []
|
? qc.getQueryData<ListIssuesResponse>(issueKeys.list(wsId))?.issues ?? []
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
const q = query.toLowerCase();
|
const q = query.toLowerCase();
|
||||||
|
|
||||||
// Show "All members" option when query is empty or matches "all"
|
// Show "All members" option when query is empty or matches "all"
|
||||||
|
|||||||
Reference in New Issue
Block a user