feat(activity): remit_received events drill to RemitDrawer
This commit is contained in:
@@ -21,6 +21,7 @@ vi.mock("@/lib/api", () => ({
|
|||||||
api: {
|
api: {
|
||||||
isConfigured: true,
|
isConfigured: true,
|
||||||
listActivity: vi.fn(),
|
listActivity: vi.fn(),
|
||||||
|
getRemittance: vi.fn(),
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -158,6 +159,14 @@ describe("ActivityLog page filters", () => {
|
|||||||
(api.listActivity as unknown as ReturnType<typeof vi.fn>).mockResolvedValue(
|
(api.listActivity as unknown as ReturnType<typeof vi.fn>).mockResolvedValue(
|
||||||
EMPTY,
|
EMPTY,
|
||||||
);
|
);
|
||||||
|
// Default for the per-remit detail fetch — the drawer fetches
|
||||||
|
// this whenever `?remit=` is in the URL or `remit_received`
|
||||||
|
// events drill in. Return a never-resolving promise so the
|
||||||
|
// drawer stays in the loading state; the smoke tests only
|
||||||
|
// assert the drawer mounts.
|
||||||
|
(
|
||||||
|
api.getRemittance as unknown as ReturnType<typeof vi.fn>
|
||||||
|
).mockReturnValue(new Promise(() => {}));
|
||||||
});
|
});
|
||||||
|
|
||||||
it("test_renders_filter_controls_when_mounted", async () => {
|
it("test_renders_filter_controls_when_mounted", async () => {
|
||||||
@@ -446,4 +455,105 @@ describe("ActivityLog page filters", () => {
|
|||||||
|
|
||||||
unmount();
|
unmount();
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("SP21 Task 4.7: ActivityLog → RemitDrawer wiring", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
(api.listActivity as unknown as ReturnType<typeof vi.fn>).mockResolvedValue(
|
||||||
|
EMPTY,
|
||||||
|
);
|
||||||
|
(
|
||||||
|
api.getRemittance as unknown as ReturnType<typeof vi.fn>
|
||||||
|
).mockReturnValue(new Promise(() => {}));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("clicking a remit_received event opens the RemitDrawer", async () => {
|
||||||
|
(api.listActivity as unknown as ReturnType<typeof vi.fn>).mockResolvedValue({
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
id: "A-1",
|
||||||
|
kind: "remit_received",
|
||||||
|
message: "Remit PCN-1 received",
|
||||||
|
timestamp: "2026-06-20T10:00:00Z",
|
||||||
|
remittanceId: "REM-1",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
total: 1,
|
||||||
|
returned: 1,
|
||||||
|
has_more: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { unmount } = renderActivity();
|
||||||
|
|
||||||
|
// Wait for the row to render so we can click it.
|
||||||
|
await settle(() =>
|
||||||
|
document.body.textContent?.includes("Remit PCN-1 received") ?? false,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Drawer should not be mounted before the click.
|
||||||
|
expect(document.body.querySelector('[data-testid="remit-drawer"]')).toBeNull();
|
||||||
|
|
||||||
|
// Find the <li role="button"> row and click it. ActivityFeed renders
|
||||||
|
// each row as a button-role <li> when `onItemClick` is provided, with
|
||||||
|
// an aria-label like "View remit received: <message>".
|
||||||
|
const row = document.body.querySelector('[aria-label^="View remit received"]') as
|
||||||
|
| HTMLLIElement
|
||||||
|
| null;
|
||||||
|
expect(row).not.toBeNull();
|
||||||
|
await act(async () => {
|
||||||
|
row!.click();
|
||||||
|
});
|
||||||
|
|
||||||
|
// After click, the RemitDrawer should be mounted (with the skeleton,
|
||||||
|
// since the never-resolving getRemittance keeps it in loading state).
|
||||||
|
await settle(
|
||||||
|
() => document.body.querySelector('[data-testid="remit-drawer"]') !== null,
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
document.body.querySelector('[data-testid="remit-drawer"]'),
|
||||||
|
).not.toBeNull();
|
||||||
|
|
||||||
|
unmount();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("deep-link ?remit=ID opens the RemitDrawer on mount", async () => {
|
||||||
|
(api.listActivity as unknown as ReturnType<typeof vi.fn>).mockResolvedValue({
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
id: "A-1",
|
||||||
|
kind: "remit_received",
|
||||||
|
message: "Remit PCN-1 received",
|
||||||
|
timestamp: "2026-06-20T10:00:00Z",
|
||||||
|
remittanceId: "REM-7",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
total: 1,
|
||||||
|
returned: 1,
|
||||||
|
has_more: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
// The hook reads `?remit=` from `window.location.search`, so set
|
||||||
|
// BOTH the MemoryRouter initial entry (for the page's URL display)
|
||||||
|
// AND `window.happyDOM.setURL` (for the hook).
|
||||||
|
(window as unknown as { happyDOM: { setURL: (u: string) => void } })
|
||||||
|
.happyDOM.setURL("http://localhost/activity?remit=REM-7");
|
||||||
|
|
||||||
|
const { unmount } = renderActivity({
|
||||||
|
initialEntries: ["/activity?remit=REM-7"],
|
||||||
|
});
|
||||||
|
|
||||||
|
await settle(
|
||||||
|
() => document.body.querySelector('[data-testid="remit-drawer"]') !== null,
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
document.body.querySelector('[data-testid="remit-drawer"]'),
|
||||||
|
).not.toBeNull();
|
||||||
|
|
||||||
|
unmount();
|
||||||
|
|
||||||
|
// Reset URL so any subsequent tests see a clean /activity URL.
|
||||||
|
(window as unknown as { happyDOM: { setURL: (u: string) => void } })
|
||||||
|
.happyDOM.setURL("http://localhost/activity");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
@@ -1,12 +1,16 @@
|
|||||||
import { useCallback, useMemo } from "react";
|
import { useCallback, useMemo } from "react";
|
||||||
import { useSearchParams } from "react-router-dom";
|
import { useSearchParams, useNavigate } from "react-router-dom";
|
||||||
|
import { toast } from "sonner";
|
||||||
import { useActivity } from "@/hooks/useActivity";
|
import { useActivity } from "@/hooks/useActivity";
|
||||||
import { useTailStream } from "@/hooks/useTailStream";
|
import { useTailStream } from "@/hooks/useTailStream";
|
||||||
import { useMergedTail } from "@/hooks/useMergedTail";
|
import { useMergedTail } from "@/hooks/useMergedTail";
|
||||||
|
import { useRemitDrawerUrlState } from "@/hooks/useRemitDrawerUrlState";
|
||||||
|
import { eventKindToUrl } from "@/lib/event-routing";
|
||||||
import { TailStatusPill } from "@/components/TailStatusPill";
|
import { TailStatusPill } from "@/components/TailStatusPill";
|
||||||
import { PageHeader } from "@/components/PageHeader";
|
import { PageHeader } from "@/components/PageHeader";
|
||||||
import { ActivityFeed } from "@/components/ActivityFeed";
|
import { ActivityFeed } from "@/components/ActivityFeed";
|
||||||
import { ActivityFilters, type SinceValue } from "@/components/ActivityFilters";
|
import { ActivityFilters, type SinceValue } from "@/components/ActivityFilters";
|
||||||
|
import { RemitDrawer } from "@/components/RemitDrawer";
|
||||||
import { Skeleton } from "@/components/ui/skeleton";
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
import { EmptyState } from "@/components/ui/empty-state";
|
import { EmptyState } from "@/components/ui/empty-state";
|
||||||
import { ErrorState } from "@/components/ui/error-state";
|
import { ErrorState } from "@/components/ui/error-state";
|
||||||
@@ -35,6 +39,13 @@ function useSinceIso(since: SinceValue): string | undefined {
|
|||||||
|
|
||||||
export function ActivityLog() {
|
export function ActivityLog() {
|
||||||
const [searchParams, setSearchParams] = useSearchParams();
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
// SP21 Phase 4 Task 4.7: `remit_received` events with a
|
||||||
|
// remittanceId drill into the RemitDrawer. Calling `open(id)`
|
||||||
|
// pushes `?remit=ID` onto the current URL (no navigation away
|
||||||
|
// from `/activity`), so the activity feed stays visible behind
|
||||||
|
// the drawer and the drawer portals in over it.
|
||||||
|
const { remitId, open, close } = useRemitDrawerUrlState();
|
||||||
|
|
||||||
const selectedKinds = useMemo<ActivityKind[]>(
|
const selectedKinds = useMemo<ActivityKind[]>(
|
||||||
() =>
|
() =>
|
||||||
@@ -163,9 +174,48 @@ export function ActivityLog() {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<ActivityFeed items={items} emptyMessage="No activity recorded yet." />
|
<ActivityFeed
|
||||||
|
items={items}
|
||||||
|
emptyMessage="No activity recorded yet."
|
||||||
|
onItemClick={(evt) => {
|
||||||
|
// SP21 Phase 4 Task 4.7: drill into the right surface
|
||||||
|
// based on event kind. `claim_*` and `provider_added`
|
||||||
|
// navigate away via `eventKindToUrl` (the Dashboard uses
|
||||||
|
// the same helper). `remit_received` events stay on
|
||||||
|
// `/activity` and open the RemitDrawer via `open(id)`
|
||||||
|
// — `eventKindToUrl` still returns `null` for that kind
|
||||||
|
// because cross-page navigation isn't the right UX here
|
||||||
|
// (we want to keep the activity feed as context behind
|
||||||
|
// the drawer). Anything else falls back to the
|
||||||
|
// "coming soon" toast so the click still gives feedback.
|
||||||
|
const url = eventKindToUrl(evt);
|
||||||
|
if (url) navigate(url);
|
||||||
|
else if (evt.kind === "remit_received" && evt.remittanceId) {
|
||||||
|
open(evt.remittanceId);
|
||||||
|
} else {
|
||||||
|
toast.info(
|
||||||
|
`Drill for ${evt.kind.replace(/_/g, " ")} coming in a later phase.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* SP21 Phase 4 Task 4.7: RemitDrawer mount. The activity
|
||||||
|
feed's `remit_received` rows drill into the drawer via
|
||||||
|
`open()`. `remits` is empty (the activity feed doesn't
|
||||||
|
keep a flat list of remits around), so j/k is a no-op
|
||||||
|
here — closing reverts the URL via `close()`. */}
|
||||||
|
<RemitDrawer
|
||||||
|
remitId={remitId}
|
||||||
|
remits={[]}
|
||||||
|
onClose={close}
|
||||||
|
onNavigate={open}
|
||||||
|
onToggleHelp={() => {
|
||||||
|
// ActivityLog has no cheatsheet; `?` is a no-op here.
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user