fix(issues): drop redundant WS position->list invalidate

onIssueUpdated already surgically patches the non-filtered workspace board
via patchIssueInBuckets (cross-status move + same-column reorder). The extra
`if (position) invalidateQueries(list)` re-pulled the whole board on top of
that, re-introducing drag flicker through the echoed-back WS event. Removed.
Filtered myAll lists still invalidate (membership can change there) — the
client-side membership reconciliation for those is a separate follow-up.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
Naiyuan Qing
2026-06-22 19:26:43 +08:00
parent f9cda25a06
commit ac2f2c7789
2 changed files with 46 additions and 3 deletions

View File

@@ -262,6 +262,41 @@ describe("project progress invalidation", () => {
});
});
describe("onIssueUpdated — position move is surgical, not a list refetch", () => {
let qc: QueryClient;
beforeEach(() => {
qc = new QueryClient();
});
const issueA: Issue = { ...baseIssue, id: "issue-1", position: 0 };
const issueB: Issue = { ...baseIssue, id: "issue-2", position: 10 };
it("reorders the moved card in place and does NOT invalidate the workspace list", () => {
qc.setQueryData<ListIssuesCache>(issueKeys.list(WS_ID), makeListCache(issueA, issueB));
// issue-1 moves below issue-2 (position 0 -> 20) — a remote/echoed drag.
onIssueUpdated(qc, WS_ID, { ...issueA, position: 20 });
const list = qc.getQueryData<ListIssuesCache>(issueKeys.list(WS_ID));
// Surgically reordered into its new slot: proof the patch alone suffices.
expect(list?.byStatus.todo?.issues.map((i) => i.id)).toEqual(["issue-2", "issue-1"]);
// The old redundant `position -> invalidate(list)` is gone — no full-board
// refetch on top of the surgical patch (that was the flicker source).
expect(qc.getQueryState(issueKeys.list(WS_ID))?.isInvalidated).toBe(false);
});
it("still invalidates the filtered myAll lists (membership can change there)", () => {
qc.setQueryData<ListIssuesCache>(issueKeys.list(WS_ID), makeListCache(issueA, issueB));
qc.setQueryData<ListIssuesCache>(issueKeys.myAll(WS_ID), makeListCache(issueA));
onIssueUpdated(qc, WS_ID, { ...issueA, position: 20 });
expectInvalidated(qc, issueKeys.myAll(WS_ID));
expect(qc.getQueryState(issueKeys.list(WS_ID))?.isInvalidated).toBe(false);
});
});
describe("onIssueDeleted", () => {
let qc: QueryClient;

View File

@@ -59,9 +59,17 @@ export function onIssueUpdated(
for (const [key, data] of listQueries) {
if (data) qc.setQueryData<ListIssuesCache>(key, patchIssueInBuckets(data, issue.id, issue));
}
if (issue.position !== undefined) {
qc.invalidateQueries({ queryKey: issueKeys.list(wsId) });
}
// The workspace board (issueKeys.list) is NOT filtered: an issue is always a
// member, so patchIssueInBuckets above is a complete surgical reconcile —
// cross-status move, same-column reorder, and field updates all land in the
// right bucket/slot. The old `if (position) invalidateQueries(list)` re-pulled
// the entire board on top of that, which is the full-list refetch that made a
// drag (local or echoed back over WS) flicker. It is pure redundancy here.
//
// myAll (My Issues / Project / actor lists) IS filtered: a change can move an
// issue in/out of the filter and the client cannot recompute that membership
// here, so that one is still invalidated. (Replacing this with client-side
// membership reconciliation is the separate follow-up — see issue thread.)
qc.invalidateQueries({ queryKey: issueKeys.myAll(wsId) });
qc.invalidateQueries({ queryKey: issueKeys.assigneeGroupsAll(wsId) });
qc.invalidateQueries({ queryKey: issueKeys.myAssigneeGroupsAll(wsId) });