import { useT } from "@agent-native/core/client/i18n"; import { DateRangePicker, dateRangeToInterval, type DateRange, } from "@agent-native/toolkit/dashboard"; import { IconCheck, IconChevronDown, IconX } from "@tabler/icons-react"; import { useQuery } from "@tanstack/react-query"; import { useEffect, useId, useState } from "react"; import { callAppAction, type LocalTransactionalEmailCatalog, } from "../client/transactional-emails"; import { resolveEmailPreviewAssets } from "../lib/transactional-email-preview"; import { ActionQueryError } from "./action-query-error"; import { Badge } from "./ui/badge"; import { Button } from "./ui/button"; import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, } from "./ui/command"; import { Dialog, DialogContent, DialogHeader, DialogTitle } from "./ui/dialog"; import { Input } from "./ui/input"; import { Popover, PopoverContent, PopoverTrigger } from "./ui/popover"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "./ui/select"; import { Skeleton } from "./ui/skeleton"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "./ui/table"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "./ui/tabs"; interface SendLogEntry { id: string; templateId: string | null; app: string | null; recipient: string; sender: string; subject: string; status: string; error: string | null; provider: string; responseStatus: number | null; createdAt: number; } interface SendLogEntryBody { htmlBody: string | null; textBody: string | null; } /** One page of `list-email-log`, fetched at `PAGE_SIZE + 1` to detect "has more". */ const PAGE_SIZE = 50; function useDebounced(value: string, delayMs = 300): string { const [debounced, setDebounced] = useState(value); useEffect(() => { const timer = setTimeout(() => setDebounced(value), delayMs); return () => clearTimeout(timer); }, [value, delayMs]); return debounced; } /** * Bodies are large (up to the 8,000-char logged cap) and sensitive, so * `list-email-log` never returns them — this fetches one row's body only * once its dialog is open, keeping list responses and the query cache small. */ function SendLogBody({ appPath, id }: { appPath: string; id: string }) { const t = useT(); const query = useQuery({ queryKey: ["get-email-log-body", appPath, id], queryFn: () => callAppAction( appPath, "get-email-log-body", { id }, "GET", ), }); if (query.isError) { return ( void query.refetch()} /> ); } if (query.isLoading) { return ; } const { htmlBody, textBody } = query.data ?? { htmlBody: null, textBody: null, }; if (!htmlBody && !textBody) return null; return ( {htmlBody ? ( {t("dispatch.transactionalEmail.sendLogBodyHtml")} ) : null} {textBody ? ( {t("dispatch.transactionalEmail.sendLogBodyText")} ) : null} {htmlBody ? ( {/* sandbox="" (no allow-scripts) keeps the logged HTML from running script in the Dispatch origin. */}