mirror of
https://github.com/multica-ai/multica.git
synced 2026-08-12 10:59:06 +02:00
Adds a cross-workspace unread summary so the workspace switcher shows the existing brand dot when a workspace OTHER than the active one has unread inbox items. The active workspace's own unread stays on the Inbox nav count to avoid a duplicate signal, and the dot is shared with the pending- invitation indicator. Backend: new GET /api/inbox/unread-summary returns per-workspace unread counts for the user, scoped via a member join so a left workspace can't light the dot. One account-level query instead of N per-workspace inbox fetches. Frontend: schema-guarded api.getInboxUnreadSummary, a single account-level TanStack Query, and a derived "other workspace has unread" boolean in AppSidebar (shared by web + desktop). Inbox WS events (new/read/archived/ batch) and reconnect invalidate the summary, so the dot appears and clears in realtime even for events from a non-active workspace. Closes multica-ai/multica#3773 Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai>
97 lines
3.0 KiB
TypeScript
97 lines
3.0 KiB
TypeScript
import { describe, it, expect, vi } from "vitest";
|
|
import { QueryClient } from "@tanstack/react-query";
|
|
import { onInboxIssueDeleted, onInboxIssueStatusChanged, onInboxSummaryInvalidate } from "./ws-updaters";
|
|
import { inboxKeys } from "./queries";
|
|
import type { InboxItem } from "../types";
|
|
|
|
const wsId = "ws-1";
|
|
|
|
function makeItem(
|
|
id: string,
|
|
issueId: string | null,
|
|
overrides: Partial<InboxItem> = {},
|
|
): InboxItem {
|
|
return {
|
|
id,
|
|
workspace_id: wsId,
|
|
recipient_type: "member",
|
|
recipient_id: "user-1",
|
|
actor_type: null,
|
|
actor_id: null,
|
|
type: "mentioned",
|
|
severity: "info",
|
|
issue_id: issueId,
|
|
title: `item ${id}`,
|
|
body: null,
|
|
issue_status: null,
|
|
read: false,
|
|
archived: false,
|
|
created_at: "2025-01-01T00:00:00Z",
|
|
details: null,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
describe("onInboxIssueDeleted", () => {
|
|
it("removes all inbox items referencing the deleted issue", () => {
|
|
const qc = new QueryClient();
|
|
const items = [
|
|
makeItem("i1", "issue-a"),
|
|
makeItem("i2", "issue-a"),
|
|
makeItem("i3", "issue-b"),
|
|
makeItem("i4", null),
|
|
];
|
|
qc.setQueryData<InboxItem[]>(inboxKeys.list(wsId), items);
|
|
|
|
onInboxIssueDeleted(qc, wsId, "issue-a");
|
|
|
|
const after = qc.getQueryData<InboxItem[]>(inboxKeys.list(wsId));
|
|
expect(after?.map((i) => i.id)).toEqual(["i3", "i4"]);
|
|
});
|
|
|
|
it("is a no-op when the inbox cache is empty", () => {
|
|
const qc = new QueryClient();
|
|
expect(() => onInboxIssueDeleted(qc, wsId, "issue-a")).not.toThrow();
|
|
expect(qc.getQueryData<InboxItem[]>(inboxKeys.list(wsId))).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe("onInboxSummaryInvalidate", () => {
|
|
it("invalidates the account-level summary key regardless of active workspace", () => {
|
|
const qc = new QueryClient();
|
|
const spy = vi.spyOn(qc, "invalidateQueries");
|
|
|
|
onInboxSummaryInvalidate(qc);
|
|
|
|
expect(spy).toHaveBeenCalledWith({ queryKey: inboxKeys.unreadSummary() });
|
|
});
|
|
|
|
it("does not disturb a workspace-scoped inbox list cache", () => {
|
|
const qc = new QueryClient();
|
|
qc.setQueryData<InboxItem[]>(inboxKeys.list(wsId), [makeItem("i1", "issue-a")]);
|
|
|
|
onInboxSummaryInvalidate(qc);
|
|
|
|
// The list cache entry is untouched (different key); only the summary
|
|
// query is marked stale.
|
|
expect(qc.getQueryData<InboxItem[]>(inboxKeys.list(wsId))?.[0]?.id).toBe("i1");
|
|
});
|
|
});
|
|
|
|
describe("onInboxIssueStatusChanged", () => {
|
|
it("updates issue_status only for items referencing the issue", () => {
|
|
const qc = new QueryClient();
|
|
const items = [
|
|
makeItem("i1", "issue-a", { issue_status: "todo" }),
|
|
makeItem("i2", "issue-b", { issue_status: "todo" }),
|
|
];
|
|
qc.setQueryData<InboxItem[]>(inboxKeys.list(wsId), items);
|
|
|
|
onInboxIssueStatusChanged(qc, wsId, "issue-a", "done");
|
|
|
|
const after = qc.getQueryData<InboxItem[]>(inboxKeys.list(wsId));
|
|
expect(after?.find((i) => i.id === "i1")?.issue_status).toBe("done");
|
|
expect(after?.find((i) => i.id === "i2")?.issue_status).toBe("todo");
|
|
});
|
|
});
|