7bcd1a2a2d
The appshell SDK useQuery only re-runs when the action changes, not the variables — so switching mail items (same crm.mail.history action, new threadId) never refetched and the reader kept the first thread's content (only the subject, a direct prop, updated). Key MailReader by threadId to remount it on switch (same pattern Messenger already uses for ThreadView). Also force a refetch of crm.inbox.list when the filter changes, which hit the same stale-variables bug. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
96 lines
4.4 KiB
TypeScript
96 lines
4.4 KiB
TypeScript
"use client";
|
|
|
|
// Inbox data layer. The inbox is a personalized work/awareness feed IIOS projects from events
|
|
// (NEEDS_REPLY, MENTION, …). The CRM lists it and transitions item state; items are never created
|
|
// here. Mock when the Shell isn't configured; live via the be-crm data door (crm.inbox.*) otherwise.
|
|
//
|
|
// Live contract (be-crm):
|
|
// query crm.inbox.list { state? } -> InboxItem[]
|
|
// cmd crm.inbox.transition { id, state, reason? } -> InboxItem
|
|
|
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
import { useAppShell, useQuery } from "@abe-kap/appshell-sdk/react";
|
|
import { isShellConfigured } from "./appshell";
|
|
import type { MailThread } from "./mail-api";
|
|
|
|
export type InboxState = "OPEN" | "SNOOZED" | "DONE" | "ARCHIVED" | "CANCELLED" | "STALE";
|
|
export interface UiInboxItem {
|
|
id: string; kind: string; state: InboxState; title: string; summary?: string;
|
|
priority: string; threadId?: string; createdAt: string;
|
|
}
|
|
|
|
const SHELL = isShellConfigured();
|
|
|
|
export interface InboxData {
|
|
live: boolean; loading: boolean; error: string | null;
|
|
items: UiInboxItem[];
|
|
transition: (id: string, state: InboxState) => Promise<void>;
|
|
refetch: () => void;
|
|
}
|
|
|
|
export function useInboxData(state?: InboxState): InboxData {
|
|
return SHELL ? useLiveInbox(state) : useMockInbox(state);
|
|
}
|
|
|
|
function useLiveInbox(state?: InboxState): InboxData {
|
|
const { sdk } = useAppShell();
|
|
const q = useQuery<UiInboxItem[]>("crm.inbox.list", state ? { state } : {});
|
|
// Mail lives in crm-mail threads, NOT the inbox projection — fold it into the one unified
|
|
// surface. Mail has no inbox work-item state, so it only shows in the Open (or unfiltered) view.
|
|
const showMail = !state || state === "OPEN";
|
|
const mq = useQuery<MailThread[]>("crm.mail.list", {});
|
|
|
|
// The SDK's useQuery only refetches when the ACTION changes, not the variables — so a filter
|
|
// change (same action, new { state }) wouldn't reload. Force a refetch when the filter changes.
|
|
const refetchInbox = q.refetch;
|
|
useEffect(() => { refetchInbox(); }, [state, refetchInbox]);
|
|
|
|
const items = useMemo<UiInboxItem[]>(() => {
|
|
const inboxItems = q.data ?? [];
|
|
const mailItems: UiInboxItem[] = showMail
|
|
? (mq.data ?? []).map((t) => ({
|
|
id: `mail:${t.threadId}`,
|
|
kind: "MAIL",
|
|
state: "OPEN" as InboxState,
|
|
title: t.subject || "(no subject)",
|
|
...(t.lastMessage ? { summary: t.lastMessage } : {}),
|
|
priority: t.unread > 0 ? "HIGH" : "LOW",
|
|
threadId: t.threadId,
|
|
createdAt: t.lastAt ?? "",
|
|
}))
|
|
: [];
|
|
// Newest first; mail and inbox items interleave by time.
|
|
return [...mailItems, ...inboxItems].sort((a, b) => (b.createdAt ?? "").localeCompare(a.createdAt ?? ""));
|
|
}, [q.data, mq.data, showMail]);
|
|
|
|
const transition = useCallback(async (id: string, next: InboxState) => {
|
|
await sdk.command("crm.inbox.transition", { id, state: next });
|
|
q.refetch();
|
|
}, [sdk, q]);
|
|
|
|
return {
|
|
live: true,
|
|
loading: q.loading || (showMail && mq.loading),
|
|
// Don't let a mail-list hiccup blank the whole inbox — surface only the inbox error.
|
|
error: q.error?.message ?? null,
|
|
items,
|
|
transition,
|
|
refetch: () => { q.refetch(); mq.refetch(); },
|
|
};
|
|
}
|
|
|
|
const MOCK_ITEMS: UiInboxItem[] = [
|
|
{ id: "in_1", kind: "MENTION", state: "OPEN", title: "Sofia mentioned you", summary: "@you — can you confirm the Henderson scope?", priority: "HIGH", threadId: "th_mock_1", createdAt: new Date().toISOString() },
|
|
{ id: "in_2", kind: "NEEDS_REPLY", state: "OPEN", title: "Reply needed — Storm response", summary: "Dan: Crew is rolling out at 7.", priority: "MEDIUM", threadId: "th_mock_2", createdAt: new Date().toISOString() },
|
|
{ id: "in_3", kind: "SUPPORT_UPDATE", state: "OPEN", title: "Ticket TK-204 updated", summary: "Customer replied on the roof-leak case.", priority: "LOW", createdAt: new Date().toISOString() },
|
|
];
|
|
|
|
function useMockInbox(state?: InboxState): InboxData {
|
|
const [items, setItems] = useState<UiInboxItem[]>(MOCK_ITEMS);
|
|
const filtered = useMemo(() => (state ? items.filter((i) => i.state === state) : items), [items, state]);
|
|
const transition = useCallback(async (id: string, next: InboxState) => {
|
|
setItems((l) => l.map((i) => (i.id === id ? { ...i, state: next } : i)));
|
|
}, []);
|
|
return { live: false, loading: false, error: null, items: filtered, transition, refetch: () => {} };
|
|
}
|