feat(sp28): Inbox ack orphans lane with Dismiss + Link to dropdown
This commit is contained in:
@@ -0,0 +1,81 @@
|
||||
// ---------------------------------------------------------------------------
|
||||
// SP28: Inbox "Ack orphans" lane hook (D7).
|
||||
//
|
||||
// Fetches the three ack-kind orphan lists (999 / 277ca / ta1) and
|
||||
// flattens them into a single lane of rows. The backend endpoint
|
||||
// `/api/inbox/ack-orphans?kind=…` takes one kind at a time so the
|
||||
// hook fires three parallel requests and merges by source_batch +
|
||||
// ack_id (a single ack row never appears in two kinds' orphan lists,
|
||||
// so the merge is just a concat).
|
||||
//
|
||||
// Refetch is wired through `useAckOrphans`'s `refetch` callback. The
|
||||
// Inbox page calls it after a manual match lands (the orphan lane
|
||||
// can drop a row that just got linked) — the same pattern the
|
||||
// existing `useInboxLanes` uses for its tail-driven refetch.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { api, type AckOrphanRow } from "@/lib/api";
|
||||
|
||||
export type AckOrphansState = {
|
||||
loading: boolean;
|
||||
error: Error | null;
|
||||
items: AckOrphanRow[];
|
||||
refetch: () => Promise<void>;
|
||||
};
|
||||
|
||||
export function useAckOrphans(): AckOrphansState {
|
||||
const [items, setItems] = useState<AckOrphanRow[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<Error | null>(null);
|
||||
|
||||
const refetch = useCallback(async () => {
|
||||
try {
|
||||
// Fire all three kind queries in parallel — the backend serves
|
||||
// each independently. Fail-soft per kind: if one of the three
|
||||
// returns a 5xx we surface the error and skip that kind so the
|
||||
// operator still sees the other two.
|
||||
const [a999, a277, aTa1] = await Promise.allSettled([
|
||||
api.listAckOrphans("999"),
|
||||
api.listAckOrphans("277ca"),
|
||||
api.listAckOrphans("ta1"),
|
||||
]);
|
||||
const merged: AckOrphanRow[] = [];
|
||||
const errs: Error[] = [];
|
||||
for (const r of [a999, a277, aTa1] as const) {
|
||||
if (r.status === "fulfilled") {
|
||||
merged.push(...r.value);
|
||||
} else {
|
||||
errs.push(
|
||||
r.reason instanceof Error
|
||||
? r.reason
|
||||
: new Error(String(r.reason)),
|
||||
);
|
||||
}
|
||||
}
|
||||
// Sort newest-first by parsedAt so the lane reads in the same
|
||||
// direction as the rest of the inbox.
|
||||
merged.sort((a, b) => {
|
||||
const at = Date.parse(a.parsedAt);
|
||||
const bt = Date.parse(b.parsedAt);
|
||||
return bt - at;
|
||||
});
|
||||
setItems(merged);
|
||||
// Only surface an error if EVERY kind failed; partial failures
|
||||
// show their rows and silently swallow the broken kind (the
|
||||
// operator can retry — a future improvement could surface a
|
||||
// per-kind "fetch failed" pill).
|
||||
setError(errs.length === 3 ? errs[0] : null);
|
||||
} catch (e) {
|
||||
setError(e as Error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
void refetch();
|
||||
}, [refetch]);
|
||||
|
||||
return { items, loading, error, refetch };
|
||||
}
|
||||
Reference in New Issue
Block a user