mirror of
https://github.com/multica-ai/multica.git
synced 2026-08-03 19:20:07 +02:00
* fix(my-issues): use server-side filtering instead of client-side My Issues was fetching ALL workspace issues and filtering client-side, causing the Done column to show wrong counts (269 vs user's actual count) and only 2-3 done issues to appear from the first 50-item page. Backend: - Add creator_id and assignee_ids (uuid[]) filters to ListIssues, ListOpenIssues, and CountIssues SQL queries - Parse creator_id and assignee_ids (comma-separated) query params Frontend: - Add myIssueListOptions with per-scope server-filtered queries - Each tab now calls the API with the right filter: Assigned → assignee_id, Created → creator_id, My Agents → assignee_ids - Add useLoadMoreMyDoneIssues for server-filtered done pagination - WS events invalidate My Issues cache via issueKeys.myAll Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refactor(my-issues): merge duplicate load-more hooks into one Both board-view and list-view were unconditionally calling two hooks (useLoadMoreDoneIssues + useLoadMoreMyDoneIssues) and picking one at runtime. Merged into a single useLoadMoreDoneIssues with an optional myIssues param so only one hook runs per render. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
129 lines
4.1 KiB
TypeScript
129 lines
4.1 KiB
TypeScript
import { queryOptions } from "@tanstack/react-query";
|
|
import { api } from "../api";
|
|
import type { ListIssuesParams } from "../types";
|
|
|
|
export const issueKeys = {
|
|
all: (wsId: string) => ["issues", wsId] as const,
|
|
list: (wsId: string) => [...issueKeys.all(wsId), "list"] as const,
|
|
/** All "my issues" queries — use for bulk invalidation. */
|
|
myAll: (wsId: string) => [...issueKeys.all(wsId), "my"] as const,
|
|
/** Per-scope "my issues" list with filter identity baked into the key. */
|
|
myList: (wsId: string, scope: string, filter: MyIssuesFilter) =>
|
|
[...issueKeys.myAll(wsId), scope, filter] as const,
|
|
detail: (wsId: string, id: string) =>
|
|
[...issueKeys.all(wsId), "detail", id] as const,
|
|
children: (wsId: string, id: string) =>
|
|
[...issueKeys.all(wsId), "children", id] as const,
|
|
timeline: (issueId: string) => ["issues", "timeline", issueId] as const,
|
|
reactions: (issueId: string) => ["issues", "reactions", issueId] as const,
|
|
subscribers: (issueId: string) =>
|
|
["issues", "subscribers", issueId] as const,
|
|
usage: (issueId: string) => ["issues", "usage", issueId] as const,
|
|
};
|
|
|
|
export type MyIssuesFilter = Pick<ListIssuesParams, "assignee_id" | "assignee_ids" | "creator_id">;
|
|
|
|
export const CLOSED_PAGE_SIZE = 50;
|
|
|
|
/**
|
|
* CACHE SHAPE NOTE: The raw cache stores ListIssuesResponse ({ issues, total, doneTotal }),
|
|
* but `select` transforms it to Issue[] for consumers. Mutations and ws-updaters
|
|
* must use setQueryData<ListIssuesResponse>(...) — NOT setQueryData<Issue[]>.
|
|
*
|
|
* Fetches all open issues + first page of done issues. Use useLoadMoreDoneIssues()
|
|
* to paginate additional done items into the cache.
|
|
*/
|
|
export function issueListOptions(wsId: string) {
|
|
return queryOptions({
|
|
queryKey: issueKeys.list(wsId),
|
|
queryFn: async () => {
|
|
const [openRes, closedRes] = await Promise.all([
|
|
api.listIssues({ open_only: true }),
|
|
api.listIssues({ status: "done", limit: CLOSED_PAGE_SIZE, offset: 0 }),
|
|
]);
|
|
return {
|
|
issues: [...openRes.issues, ...closedRes.issues],
|
|
total: openRes.total + closedRes.total,
|
|
doneTotal: closedRes.total,
|
|
};
|
|
},
|
|
select: (data) => data.issues,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Server-filtered issue list for the My Issues page.
|
|
* Each scope gets its own cache entry so switching tabs is instant after first load.
|
|
*/
|
|
export function myIssueListOptions(
|
|
wsId: string,
|
|
scope: string,
|
|
filter: MyIssuesFilter,
|
|
) {
|
|
return queryOptions({
|
|
queryKey: issueKeys.myList(wsId, scope, filter),
|
|
queryFn: async () => {
|
|
const [openRes, closedRes] = await Promise.all([
|
|
api.listIssues({ open_only: true, ...filter }),
|
|
api.listIssues({
|
|
status: "done",
|
|
limit: CLOSED_PAGE_SIZE,
|
|
offset: 0,
|
|
...filter,
|
|
}),
|
|
]);
|
|
return {
|
|
issues: [...openRes.issues, ...closedRes.issues],
|
|
total: openRes.total + closedRes.total,
|
|
doneTotal: closedRes.total,
|
|
};
|
|
},
|
|
select: (data) => data.issues,
|
|
});
|
|
}
|
|
|
|
export function issueDetailOptions(wsId: string, id: string) {
|
|
return queryOptions({
|
|
queryKey: issueKeys.detail(wsId, id),
|
|
queryFn: () => api.getIssue(id),
|
|
});
|
|
}
|
|
|
|
export function childIssuesOptions(wsId: string, id: string) {
|
|
return queryOptions({
|
|
queryKey: issueKeys.children(wsId, id),
|
|
queryFn: () => api.listChildIssues(id).then((r) => r.issues),
|
|
});
|
|
}
|
|
|
|
export function issueTimelineOptions(issueId: string) {
|
|
return queryOptions({
|
|
queryKey: issueKeys.timeline(issueId),
|
|
queryFn: () => api.listTimeline(issueId),
|
|
});
|
|
}
|
|
|
|
export function issueReactionsOptions(issueId: string) {
|
|
return queryOptions({
|
|
queryKey: issueKeys.reactions(issueId),
|
|
queryFn: async () => {
|
|
const issue = await api.getIssue(issueId);
|
|
return issue.reactions ?? [];
|
|
},
|
|
});
|
|
}
|
|
|
|
export function issueSubscribersOptions(issueId: string) {
|
|
return queryOptions({
|
|
queryKey: issueKeys.subscribers(issueId),
|
|
queryFn: () => api.listIssueSubscribers(issueId),
|
|
});
|
|
}
|
|
|
|
export function issueUsageOptions(issueId: string) {
|
|
return queryOptions({
|
|
queryKey: issueKeys.usage(issueId),
|
|
queryFn: () => api.getIssueUsage(issueId),
|
|
});
|
|
}
|