@agent-native/dispatch 0.17.3 → 0.17.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/actions/provider-api-register.d.ts +10 -10
- package/dist/client/transactional-emails.d.ts +57 -0
- package/dist/client/transactional-emails.d.ts.map +1 -0
- package/dist/client/transactional-emails.js +116 -0
- package/dist/client/transactional-emails.js.map +1 -0
- package/dist/components/layout/Layout.d.ts.map +1 -1
- package/dist/components/layout/Layout.js +8 -1
- package/dist/components/layout/Layout.js.map +1 -1
- package/dist/components/transactional-email-activity.d.ts +24 -0
- package/dist/components/transactional-email-activity.d.ts.map +1 -0
- package/dist/components/transactional-email-activity.js +31 -0
- package/dist/components/transactional-email-activity.js.map +1 -0
- package/dist/components/transactional-email-metrics.d.ts +40 -0
- package/dist/components/transactional-email-metrics.d.ts.map +1 -0
- package/dist/components/transactional-email-metrics.js +44 -0
- package/dist/components/transactional-email-metrics.js.map +1 -0
- package/dist/components/transactional-email-preview.d.ts +7 -0
- package/dist/components/transactional-email-preview.d.ts.map +1 -0
- package/dist/components/transactional-email-preview.js +41 -0
- package/dist/components/transactional-email-preview.js.map +1 -0
- package/dist/routes/index.d.ts.map +1 -1
- package/dist/routes/index.js +2 -0
- package/dist/routes/index.js.map +1 -1
- package/dist/routes/pages/transactional-email.$appId.$id.d.ts +5 -0
- package/dist/routes/pages/transactional-email.$appId.$id.d.ts.map +1 -0
- package/dist/routes/pages/transactional-email.$appId.$id.js +144 -0
- package/dist/routes/pages/transactional-email.$appId.$id.js.map +1 -0
- package/dist/routes/pages/transactional-email.d.ts +5 -0
- package/dist/routes/pages/transactional-email.d.ts.map +1 -0
- package/dist/routes/pages/transactional-email.js +139 -0
- package/dist/routes/pages/transactional-email.js.map +1 -0
- package/package.json +2 -2
- package/src/client/transactional-emails.spec.ts +91 -0
- package/src/client/transactional-emails.ts +201 -0
- package/src/components/layout/Layout.tsx +8 -0
- package/src/components/transactional-email-activity.tsx +102 -0
- package/src/components/transactional-email-metrics.tsx +127 -0
- package/src/components/transactional-email-preview.tsx +88 -0
- package/src/routes/index.ts +5 -0
- package/src/routes/pages/transactional-email.$appId.$id.tsx +362 -0
- package/src/routes/pages/transactional-email.tsx +571 -0
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useActionQuery } from "@agent-native/core/client/hooks";
|
|
3
|
+
import { useT } from "@agent-native/core/client/i18n";
|
|
4
|
+
import { IconAlertTriangle, IconChevronRight, IconEye, IconInfoCircle, IconList, IconMail, } from "@tabler/icons-react";
|
|
5
|
+
import { useQueries, useQuery } from "@tanstack/react-query";
|
|
6
|
+
import { useMemo, useState } from "react";
|
|
7
|
+
import { Link } from "react-router";
|
|
8
|
+
import { aggregateSharedEmails, callAppAction, fetchAppEmailCatalog, } from "../../client/transactional-emails.js";
|
|
9
|
+
import { ActionQueryError } from "../../components/action-query-error.js";
|
|
10
|
+
import { DispatchShell } from "../../components/dispatch-shell.js";
|
|
11
|
+
import { ActivityTable, } from "../../components/transactional-email-activity.js";
|
|
12
|
+
import { OpenRateCell, SendsCell, LastSentCell, } from "../../components/transactional-email-metrics.js";
|
|
13
|
+
import { EmailPreviewPane } from "../../components/transactional-email-preview.js";
|
|
14
|
+
import { Alert, AlertDescription, AlertTitle } from "../../components/ui/alert.js";
|
|
15
|
+
import { Button } from "../../components/ui/button.js";
|
|
16
|
+
import { Collapsible, CollapsibleContent, CollapsibleTrigger, } from "../../components/ui/collapsible.js";
|
|
17
|
+
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, } from "../../components/ui/dialog.js";
|
|
18
|
+
import { Skeleton } from "../../components/ui/skeleton.js";
|
|
19
|
+
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "../../components/ui/table.js";
|
|
20
|
+
export function meta() {
|
|
21
|
+
return [{ title: "Transactional email — Dispatch" }];
|
|
22
|
+
}
|
|
23
|
+
const WINDOW_DAYS = 30;
|
|
24
|
+
const ACTIVITY_LIMIT = 50;
|
|
25
|
+
/** Shape of the local `list-transactional-emails` action response. */
|
|
26
|
+
function PreviewDialog({ email, appId, appPath, open, onOpenChange, }) {
|
|
27
|
+
const t = useT();
|
|
28
|
+
return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { className: "max-w-3xl", children: [_jsxs(DialogHeader, { children: [_jsx(DialogTitle, { children: email.name }), _jsx(DialogDescription, { children: t("dispatch.transactionalEmail.previewDescription") })] }), _jsx(EmailPreviewPane, { appId: appId, appPath: appPath, id: email.id, name: email.name })] }) }));
|
|
29
|
+
}
|
|
30
|
+
function ActivityDialog({ email, appPath, unavailableReason, open, onOpenChange, }) {
|
|
31
|
+
const t = useT();
|
|
32
|
+
const activityQuery = useQuery({
|
|
33
|
+
queryKey: ["list-email-activity", appPath, email.id, ACTIVITY_LIMIT],
|
|
34
|
+
queryFn: () => callAppAction(appPath, "list-email-activity", { templateId: email.id, limit: ACTIVITY_LIMIT }, "GET"),
|
|
35
|
+
enabled: open && !unavailableReason,
|
|
36
|
+
});
|
|
37
|
+
return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { className: "max-w-4xl", children: [_jsxs(DialogHeader, { children: [_jsx(DialogTitle, { children: t("dispatch.transactionalEmail.activityTitle", {
|
|
38
|
+
name: email.name,
|
|
39
|
+
}) }), _jsx(DialogDescription, { children: t("dispatch.transactionalEmail.retentionNote") })] }), _jsx(ActivityTable, { result: unavailableReason
|
|
40
|
+
? { available: false, reason: unavailableReason }
|
|
41
|
+
: activityQuery.data, isLoading: activityQuery.isLoading, isError: activityQuery.isError, error: activityQuery.error, onRetry: () => void activityQuery.refetch() })] }) }));
|
|
42
|
+
}
|
|
43
|
+
function EmailRow({ email, appId, appPath, engagement, engagementUnavailable, engagementLoading, }) {
|
|
44
|
+
const t = useT();
|
|
45
|
+
const [previewOpen, setPreviewOpen] = useState(false);
|
|
46
|
+
const [activityOpen, setActivityOpen] = useState(false);
|
|
47
|
+
return (_jsxs(TableRow, { children: [_jsxs(TableCell, { className: "align-top", children: [_jsx(Link, { to: `/transactional-email/${appId}/${email.id}`, className: "text-sm font-medium text-foreground hover:underline", children: email.name }), _jsx("div", { className: "font-mono text-xs text-muted-foreground", children: email.id })] }), _jsx(TableCell, { className: "max-w-64 align-top text-xs text-muted-foreground", children: email.trigger }), _jsx(TableCell, { className: "max-w-56 align-top text-xs text-muted-foreground", children: email.recipientLabel }), _jsx(TableCell, { className: "max-w-56 align-top text-xs text-muted-foreground", children: email.senderLabel }), _jsx(TableCell, { className: "align-top text-sm", children: _jsx(SendsCell, { sent: email.sent, failed: email.failed }) }), _jsx(TableCell, { className: "align-top text-sm", children: _jsx(OpenRateCell, { engagement: engagement, unavailableReason: engagementUnavailable, loading: engagementLoading }) }), _jsx(TableCell, { className: "align-top text-xs text-muted-foreground", children: _jsx(LastSentCell, { lastSentAt: email.lastSentAt, sent: email.sent }) }), _jsxs(TableCell, { className: "align-top", children: [_jsxs("div", { className: "flex justify-end gap-2", children: [_jsxs(Button, { variant: "outline", size: "sm", onClick: () => setPreviewOpen(true), children: [_jsx(IconEye, { className: "size-4" }), t("dispatch.transactionalEmail.preview")] }), _jsxs(Button, { variant: "ghost", size: "sm", onClick: () => setActivityOpen(true), children: [_jsx(IconList, { className: "size-4" }), t("dispatch.transactionalEmail.activityLink")] })] }), _jsx(PreviewDialog, { email: email, appId: appId, appPath: appPath, open: previewOpen, onOpenChange: setPreviewOpen }), _jsx(ActivityDialog, { email: email, appPath: appPath, unavailableReason: engagementUnavailable, open: activityOpen, onOpenChange: setActivityOpen })] })] }));
|
|
48
|
+
}
|
|
49
|
+
function AppEmailTable({ catalog, engagementByTemplate, engagementUnavailable, engagementLoading, }) {
|
|
50
|
+
const t = useT();
|
|
51
|
+
if (catalog.error) {
|
|
52
|
+
return (_jsxs(Alert, { variant: "destructive", children: [_jsx(IconAlertTriangle, { className: "size-4" }), _jsx(AlertTitle, { children: t("dispatch.transactionalEmail.catalogUnreadable") }), _jsx(AlertDescription, { children: catalog.error })] }));
|
|
53
|
+
}
|
|
54
|
+
return (_jsxs(_Fragment, { children: [catalog.statsError ? (_jsxs(Alert, { className: "mb-4", children: [_jsx(IconInfoCircle, { className: "size-4" }), _jsx(AlertTitle, { children: t("dispatch.transactionalEmail.countsUnreadable") }), _jsx(AlertDescription, { children: catalog.statsError })] })) : null, catalog.emails.length === 0 ? (_jsx("div", { className: "rounded-xl border border-dashed px-4 py-6 text-sm text-muted-foreground", children: t("dispatch.transactionalEmail.appSendsNoEmail") })) : (_jsxs(Table, { children: [_jsx(TableHeader, { children: _jsxs(TableRow, { children: [_jsx(TableHead, { children: t("dispatch.transactionalEmail.email") }), _jsx(TableHead, { children: t("dispatch.transactionalEmail.trigger") }), _jsx(TableHead, { children: t("dispatch.transactionalEmail.recipient") }), _jsx(TableHead, { children: t("dispatch.transactionalEmail.sender") }), _jsx(TableHead, { children: t("dispatch.transactionalEmail.sends") }), _jsx(TableHead, { children: t("dispatch.transactionalEmail.openRate") }), _jsx(TableHead, { children: t("dispatch.transactionalEmail.lastSent") }), _jsx(TableHead, {})] }) }), _jsx(TableBody, { children: catalog.emails.map((email) => (_jsx(EmailRow, { email: email, appId: catalog.appId, appPath: catalog.appPath, engagement: engagementByTemplate.get(email.id), engagementUnavailable: engagementUnavailable, engagementLoading: engagementLoading }, email.id))) })] }))] }));
|
|
55
|
+
}
|
|
56
|
+
function AppEmailCard({ name, path, open, onOpenChange, catalog, isLoading, isError, error, onRetry, engagementByTemplate, engagementUnavailable, engagementLoading, }) {
|
|
57
|
+
return (_jsxs(Collapsible, { open: open, onOpenChange: onOpenChange, className: "rounded-2xl bg-card", children: [_jsxs(CollapsibleTrigger, { className: "group flex w-full cursor-pointer items-center justify-between gap-3 p-5 text-left hover:bg-muted/20", children: [_jsxs("span", { className: "flex items-center gap-2", children: [_jsx(IconChevronRight, { className: "h-4 w-4 shrink-0 text-muted-foreground transition-transform group-data-[state=open]:rotate-90" }), _jsx("span", { className: "text-sm font-medium text-foreground", children: name })] }), _jsx("span", { className: "font-mono text-xs text-muted-foreground", children: path })] }), _jsx(CollapsibleContent, { className: "border-t px-5 pb-5 pt-4", children: isError ? (_jsx(ActionQueryError, { error: error, onRetry: onRetry })) : isLoading || !catalog ? (_jsx(Skeleton, { className: "h-24 w-full" })) : (_jsx(AppEmailTable, { catalog: catalog, engagementByTemplate: engagementByTemplate, engagementUnavailable: engagementUnavailable, engagementLoading: engagementLoading })) })] }));
|
|
58
|
+
}
|
|
59
|
+
export default function TransactionalEmailRoute() {
|
|
60
|
+
const t = useT();
|
|
61
|
+
const appsQuery = useActionQuery("list-workspace-apps", {
|
|
62
|
+
includeAgentCards: false,
|
|
63
|
+
});
|
|
64
|
+
const apps = useMemo(() => (appsQuery.data ?? [])
|
|
65
|
+
.filter((app) => app.status !== "pending")
|
|
66
|
+
.map((app) => ({ id: app.id, name: app.name, path: app.path })), [appsQuery.data]);
|
|
67
|
+
const [expanded, setExpanded] = useState({});
|
|
68
|
+
const [sharedOpen, setSharedOpen] = useState(true);
|
|
69
|
+
const sharedQuery = useActionQuery("list-transactional-emails", { windowDays: WINDOW_DAYS });
|
|
70
|
+
const catalogQueries = useQueries({
|
|
71
|
+
queries: apps.map((app) => ({
|
|
72
|
+
queryKey: ["transactional-email-catalog", app.id, WINDOW_DAYS],
|
|
73
|
+
queryFn: () => fetchAppEmailCatalog(app, WINDOW_DAYS),
|
|
74
|
+
enabled: sharedOpen || expanded[app.id] === true,
|
|
75
|
+
staleTime: Infinity,
|
|
76
|
+
})),
|
|
77
|
+
});
|
|
78
|
+
const sharedCatalog = useMemo(() => {
|
|
79
|
+
if (!sharedQuery.data)
|
|
80
|
+
return undefined;
|
|
81
|
+
const aggregate = aggregateSharedEmails(sharedQuery.data, catalogQueries
|
|
82
|
+
.map((query) => query.data)
|
|
83
|
+
.filter((catalog) => Boolean(catalog)));
|
|
84
|
+
return {
|
|
85
|
+
appId: "core",
|
|
86
|
+
appName: t("dispatch.transactionalEmail.sharedTitle"),
|
|
87
|
+
appPath: t("dispatch.transactionalEmail.sharedSubtitle"),
|
|
88
|
+
emails: aggregate.emails,
|
|
89
|
+
error: null,
|
|
90
|
+
statsError: aggregate.statsError,
|
|
91
|
+
};
|
|
92
|
+
}, [sharedQuery.data, catalogQueries, t]);
|
|
93
|
+
const engagementQueries = useQueries({
|
|
94
|
+
queries: apps.map((app, index) => {
|
|
95
|
+
const catalog = catalogQueries[index]?.data;
|
|
96
|
+
const templateIds = catalog && !catalog.error
|
|
97
|
+
? catalog.emails.map((email) => email.id)
|
|
98
|
+
: [];
|
|
99
|
+
return {
|
|
100
|
+
queryKey: [
|
|
101
|
+
"list-email-engagement",
|
|
102
|
+
app.id,
|
|
103
|
+
templateIds.join(","),
|
|
104
|
+
WINDOW_DAYS,
|
|
105
|
+
],
|
|
106
|
+
queryFn: () => callAppAction(app.path, "list-email-engagement", { templateIds, windowDays: WINDOW_DAYS }, "POST"),
|
|
107
|
+
enabled: expanded[app.id] === true && templateIds.length > 0,
|
|
108
|
+
};
|
|
109
|
+
}),
|
|
110
|
+
});
|
|
111
|
+
const sharedLoading = sharedQuery.isLoading ||
|
|
112
|
+
(sharedOpen && catalogQueries.some((query) => query.isLoading));
|
|
113
|
+
const sharedError = catalogQueries.find((query) => query.isError)?.error;
|
|
114
|
+
const sharedProviderReason = t("dispatch.transactionalEmail.sharedProviderMetricsUnavailable");
|
|
115
|
+
return (_jsx(DispatchShell, { title: t("dispatch.transactionalEmail.title"), description: t("dispatch.transactionalEmail.description"), children: _jsxs("div", { className: "flex flex-col gap-4", children: [_jsxs(Alert, { children: [_jsx(IconInfoCircle, { className: "size-4" }), _jsx(AlertTitle, { children: t("dispatch.transactionalEmail.retentionTitle") }), _jsx(AlertDescription, { children: t("dispatch.transactionalEmail.retentionNote") })] }), appsQuery.isError ? (_jsx(ActionQueryError, { error: appsQuery.error, onRetry: () => void appsQuery.refetch() })) : null, appsQuery.isLoading ? (_jsxs("div", { className: "space-y-3", children: [_jsx(Skeleton, { className: "h-16 w-full rounded-2xl" }), _jsx(Skeleton, { className: "h-16 w-full rounded-2xl" })] })) : null, !appsQuery.isLoading && apps.length === 0 ? (_jsxs("div", { className: "rounded-2xl border border-dashed px-6 py-12 text-center text-sm text-muted-foreground", children: [_jsx(IconMail, { className: "mx-auto mb-2 size-5" }), t("dispatch.transactionalEmail.noApps")] })) : null, _jsx(AppEmailCard, { name: t("dispatch.transactionalEmail.sharedTitle"), path: t("dispatch.transactionalEmail.sharedSubtitle"), open: sharedOpen, onOpenChange: setSharedOpen, catalog: sharedCatalog, isLoading: sharedLoading, isError: sharedQuery.isError || Boolean(sharedError), error: sharedQuery.error ?? sharedError, onRetry: () => {
|
|
116
|
+
void sharedQuery.refetch();
|
|
117
|
+
for (const query of catalogQueries)
|
|
118
|
+
void query.refetch();
|
|
119
|
+
}, engagementByTemplate: new Map(), engagementUnavailable: sharedProviderReason, engagementLoading: false }), apps.map((app, index) => {
|
|
120
|
+
const query = catalogQueries[index];
|
|
121
|
+
const engagementQuery = engagementQueries[index];
|
|
122
|
+
const engagementResult = engagementQuery?.data;
|
|
123
|
+
const engagementUnavailable = engagementResult && !engagementResult.available
|
|
124
|
+
? engagementResult.reason
|
|
125
|
+
: engagementQuery?.isError
|
|
126
|
+
? engagementQuery.error instanceof Error
|
|
127
|
+
? engagementQuery.error.message
|
|
128
|
+
: String(engagementQuery.error)
|
|
129
|
+
: null;
|
|
130
|
+
const engagementByTemplate = new Map();
|
|
131
|
+
if (engagementResult?.available) {
|
|
132
|
+
for (const entry of engagementResult.data) {
|
|
133
|
+
engagementByTemplate.set(entry.templateId, entry);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return (_jsx(AppEmailCard, { name: app.name, path: app.path, open: expanded[app.id] === true, onOpenChange: (next) => setExpanded((current) => ({ ...current, [app.id]: next })), catalog: query?.data, isLoading: query?.isLoading ?? false, isError: query?.isError ?? false, error: query?.error, onRetry: () => void query?.refetch(), engagementByTemplate: engagementByTemplate, engagementUnavailable: engagementUnavailable, engagementLoading: engagementQuery?.isLoading ?? false }, app.id));
|
|
137
|
+
})] }) }));
|
|
138
|
+
}
|
|
139
|
+
//# sourceMappingURL=transactional-email.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"transactional-email.js","sourceRoot":"","sources":["../../../src/routes/pages/transactional-email.tsx"],"names":[],"mappings":"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","sourcesContent":["import { callAction, useActionQuery } from \"@agent-native/core/client/hooks\";\nimport { useT } from \"@agent-native/core/client/i18n\";\nimport {\n IconAlertTriangle,\n IconChevronRight,\n IconEye,\n IconInfoCircle,\n IconList,\n IconMail,\n} from \"@tabler/icons-react\";\nimport { useQueries, useQuery } from \"@tanstack/react-query\";\nimport { useMemo, useState } from \"react\";\nimport { Link } from \"react-router\";\n\nimport {\n aggregateSharedEmails,\n callAppAction,\n fetchAppEmailCatalog,\n type AppEmailCatalog,\n type AppTransactionalEmail,\n type LocalTransactionalEmailCatalog,\n} from \"../../client/transactional-emails\";\nimport { ActionQueryError } from \"../../components/action-query-error\";\nimport { DispatchShell } from \"../../components/dispatch-shell\";\nimport {\n ActivityTable,\n type EmailActivityEntry,\n} from \"../../components/transactional-email-activity\";\nimport {\n OpenRateCell,\n SendsCell,\n LastSentCell,\n type EmailEngagement,\n type ProviderMetricsResult,\n} from \"../../components/transactional-email-metrics\";\nimport { EmailPreviewPane } from \"../../components/transactional-email-preview\";\nimport { Alert, AlertDescription, AlertTitle } from \"../../components/ui/alert\";\nimport { Button } from \"../../components/ui/button\";\nimport {\n Collapsible,\n CollapsibleContent,\n CollapsibleTrigger,\n} from \"../../components/ui/collapsible\";\nimport {\n Dialog,\n DialogContent,\n DialogDescription,\n DialogHeader,\n DialogTitle,\n} from \"../../components/ui/dialog\";\nimport { Skeleton } from \"../../components/ui/skeleton\";\nimport {\n Table,\n TableBody,\n TableCell,\n TableHead,\n TableHeader,\n TableRow,\n} from \"../../components/ui/table\";\n\nexport function meta() {\n return [{ title: \"Transactional email — Dispatch\" }];\n}\n\nconst WINDOW_DAYS = 30;\nconst ACTIVITY_LIMIT = 50;\n\ninterface WorkspaceAppRef {\n id: string;\n name: string;\n path: string;\n status?: \"ready\" | \"pending\";\n}\n\n/** Shape of the local `list-transactional-emails` action response. */\nfunction PreviewDialog({\n email,\n appId,\n appPath,\n open,\n onOpenChange,\n}: {\n email: AppTransactionalEmail;\n appId: string;\n appPath: string;\n open: boolean;\n onOpenChange: (next: boolean) => void;\n}) {\n const t = useT();\n return (\n <Dialog open={open} onOpenChange={onOpenChange}>\n <DialogContent className=\"max-w-3xl\">\n <DialogHeader>\n <DialogTitle>{email.name}</DialogTitle>\n <DialogDescription>\n {t(\"dispatch.transactionalEmail.previewDescription\")}\n </DialogDescription>\n </DialogHeader>\n {/* Dialog content only mounts while open, so this fetches on open and\n drops the request when closed rather than needing an enabled flag. */}\n <EmailPreviewPane\n appId={appId}\n appPath={appPath}\n id={email.id}\n name={email.name}\n />\n </DialogContent>\n </Dialog>\n );\n}\n\nfunction ActivityDialog({\n email,\n appPath,\n unavailableReason,\n open,\n onOpenChange,\n}: {\n email: AppTransactionalEmail;\n appPath: string;\n unavailableReason: string | null;\n open: boolean;\n onOpenChange: (next: boolean) => void;\n}) {\n const t = useT();\n const activityQuery = useQuery({\n queryKey: [\"list-email-activity\", appPath, email.id, ACTIVITY_LIMIT],\n queryFn: () =>\n callAppAction<ProviderMetricsResult<EmailActivityEntry[]>>(\n appPath,\n \"list-email-activity\",\n { templateId: email.id, limit: ACTIVITY_LIMIT },\n \"GET\",\n ),\n enabled: open && !unavailableReason,\n });\n\n return (\n <Dialog open={open} onOpenChange={onOpenChange}>\n <DialogContent className=\"max-w-4xl\">\n <DialogHeader>\n <DialogTitle>\n {t(\"dispatch.transactionalEmail.activityTitle\", {\n name: email.name,\n })}\n </DialogTitle>\n <DialogDescription>\n {t(\"dispatch.transactionalEmail.retentionNote\")}\n </DialogDescription>\n </DialogHeader>\n <ActivityTable\n result={\n unavailableReason\n ? { available: false, reason: unavailableReason }\n : activityQuery.data\n }\n isLoading={activityQuery.isLoading}\n isError={activityQuery.isError}\n error={activityQuery.error}\n onRetry={() => void activityQuery.refetch()}\n />\n </DialogContent>\n </Dialog>\n );\n}\n\nfunction EmailRow({\n email,\n appId,\n appPath,\n engagement,\n engagementUnavailable,\n engagementLoading,\n}: {\n email: AppTransactionalEmail;\n appId: string;\n appPath: string;\n engagement: EmailEngagement | undefined;\n engagementUnavailable: string | null;\n engagementLoading: boolean;\n}) {\n const t = useT();\n const [previewOpen, setPreviewOpen] = useState(false);\n const [activityOpen, setActivityOpen] = useState(false);\n\n return (\n <TableRow>\n <TableCell className=\"align-top\">\n <Link\n to={`/transactional-email/${appId}/${email.id}`}\n className=\"text-sm font-medium text-foreground hover:underline\"\n >\n {email.name}\n </Link>\n <div className=\"font-mono text-xs text-muted-foreground\">\n {email.id}\n </div>\n </TableCell>\n <TableCell className=\"max-w-64 align-top text-xs text-muted-foreground\">\n {email.trigger}\n </TableCell>\n <TableCell className=\"max-w-56 align-top text-xs text-muted-foreground\">\n {email.recipientLabel}\n </TableCell>\n <TableCell className=\"max-w-56 align-top text-xs text-muted-foreground\">\n {email.senderLabel}\n </TableCell>\n <TableCell className=\"align-top text-sm\">\n <SendsCell sent={email.sent} failed={email.failed} />\n </TableCell>\n <TableCell className=\"align-top text-sm\">\n <OpenRateCell\n engagement={engagement}\n unavailableReason={engagementUnavailable}\n loading={engagementLoading}\n />\n </TableCell>\n <TableCell className=\"align-top text-xs text-muted-foreground\">\n <LastSentCell lastSentAt={email.lastSentAt} sent={email.sent} />\n </TableCell>\n <TableCell className=\"align-top\">\n <div className=\"flex justify-end gap-2\">\n <Button\n variant=\"outline\"\n size=\"sm\"\n onClick={() => setPreviewOpen(true)}\n >\n <IconEye className=\"size-4\" />\n {t(\"dispatch.transactionalEmail.preview\")}\n </Button>\n <Button\n variant=\"ghost\"\n size=\"sm\"\n onClick={() => setActivityOpen(true)}\n >\n <IconList className=\"size-4\" />\n {t(\"dispatch.transactionalEmail.activityLink\")}\n </Button>\n </div>\n <PreviewDialog\n email={email}\n appId={appId}\n appPath={appPath}\n open={previewOpen}\n onOpenChange={setPreviewOpen}\n />\n <ActivityDialog\n email={email}\n appPath={appPath}\n unavailableReason={engagementUnavailable}\n open={activityOpen}\n onOpenChange={setActivityOpen}\n />\n </TableCell>\n </TableRow>\n );\n}\n\nfunction AppEmailTable({\n catalog,\n engagementByTemplate,\n engagementUnavailable,\n engagementLoading,\n}: {\n catalog: AppEmailCatalog;\n engagementByTemplate: Map<string, EmailEngagement>;\n engagementUnavailable: string | null;\n engagementLoading: boolean;\n}) {\n const t = useT();\n\n if (catalog.error) {\n return (\n <Alert variant=\"destructive\">\n <IconAlertTriangle className=\"size-4\" />\n <AlertTitle>\n {t(\"dispatch.transactionalEmail.catalogUnreadable\")}\n </AlertTitle>\n <AlertDescription>{catalog.error}</AlertDescription>\n </Alert>\n );\n }\n\n return (\n <>\n {catalog.statsError ? (\n <Alert className=\"mb-4\">\n <IconInfoCircle className=\"size-4\" />\n <AlertTitle>\n {t(\"dispatch.transactionalEmail.countsUnreadable\")}\n </AlertTitle>\n <AlertDescription>{catalog.statsError}</AlertDescription>\n </Alert>\n ) : null}\n {catalog.emails.length === 0 ? (\n <div className=\"rounded-xl border border-dashed px-4 py-6 text-sm text-muted-foreground\">\n {t(\"dispatch.transactionalEmail.appSendsNoEmail\")}\n </div>\n ) : (\n <Table>\n <TableHeader>\n <TableRow>\n <TableHead>{t(\"dispatch.transactionalEmail.email\")}</TableHead>\n <TableHead>{t(\"dispatch.transactionalEmail.trigger\")}</TableHead>\n <TableHead>\n {t(\"dispatch.transactionalEmail.recipient\")}\n </TableHead>\n <TableHead>{t(\"dispatch.transactionalEmail.sender\")}</TableHead>\n <TableHead>{t(\"dispatch.transactionalEmail.sends\")}</TableHead>\n <TableHead>{t(\"dispatch.transactionalEmail.openRate\")}</TableHead>\n <TableHead>{t(\"dispatch.transactionalEmail.lastSent\")}</TableHead>\n <TableHead />\n </TableRow>\n </TableHeader>\n <TableBody>\n {catalog.emails.map((email) => (\n <EmailRow\n key={email.id}\n email={email}\n appId={catalog.appId}\n appPath={catalog.appPath}\n engagement={engagementByTemplate.get(email.id)}\n engagementUnavailable={engagementUnavailable}\n engagementLoading={engagementLoading}\n />\n ))}\n </TableBody>\n </Table>\n )}\n </>\n );\n}\n\nfunction AppEmailCard({\n name,\n path,\n open,\n onOpenChange,\n catalog,\n isLoading,\n isError,\n error,\n onRetry,\n engagementByTemplate,\n engagementUnavailable,\n engagementLoading,\n}: {\n name: string;\n path: string;\n open: boolean;\n onOpenChange: (next: boolean) => void;\n catalog: AppEmailCatalog | undefined;\n isLoading: boolean;\n isError: boolean;\n error: unknown;\n onRetry: () => void;\n engagementByTemplate: Map<string, EmailEngagement>;\n engagementUnavailable: string | null;\n engagementLoading: boolean;\n}) {\n return (\n <Collapsible\n open={open}\n onOpenChange={onOpenChange}\n className=\"rounded-2xl bg-card\"\n >\n <CollapsibleTrigger className=\"group flex w-full cursor-pointer items-center justify-between gap-3 p-5 text-left hover:bg-muted/20\">\n <span className=\"flex items-center gap-2\">\n <IconChevronRight className=\"h-4 w-4 shrink-0 text-muted-foreground transition-transform group-data-[state=open]:rotate-90\" />\n <span className=\"text-sm font-medium text-foreground\">{name}</span>\n </span>\n <span className=\"font-mono text-xs text-muted-foreground\">{path}</span>\n </CollapsibleTrigger>\n <CollapsibleContent className=\"border-t px-5 pb-5 pt-4\">\n {isError ? (\n <ActionQueryError error={error} onRetry={onRetry} />\n ) : isLoading || !catalog ? (\n <Skeleton className=\"h-24 w-full\" />\n ) : (\n <AppEmailTable\n catalog={catalog}\n engagementByTemplate={engagementByTemplate}\n engagementUnavailable={engagementUnavailable}\n engagementLoading={engagementLoading}\n />\n )}\n </CollapsibleContent>\n </Collapsible>\n );\n}\n\nexport default function TransactionalEmailRoute() {\n const t = useT();\n const appsQuery = useActionQuery<WorkspaceAppRef[]>(\"list-workspace-apps\", {\n includeAgentCards: false,\n });\n\n const apps = useMemo(\n () =>\n (appsQuery.data ?? [])\n .filter((app) => app.status !== \"pending\")\n .map((app) => ({ id: app.id, name: app.name, path: app.path })),\n [appsQuery.data],\n );\n\n const [expanded, setExpanded] = useState<Record<string, boolean>>({});\n const [sharedOpen, setSharedOpen] = useState(true);\n\n const sharedQuery = useActionQuery<LocalTransactionalEmailCatalog>(\n \"list-transactional-emails\",\n { windowDays: WINDOW_DAYS },\n );\n\n const catalogQueries = useQueries({\n queries: apps.map((app) => ({\n queryKey: [\"transactional-email-catalog\", app.id, WINDOW_DAYS],\n queryFn: () => fetchAppEmailCatalog(app, WINDOW_DAYS),\n enabled: sharedOpen || expanded[app.id] === true,\n staleTime: Infinity,\n })),\n });\n\n const sharedCatalog = useMemo<AppEmailCatalog | undefined>(() => {\n if (!sharedQuery.data) return undefined;\n const aggregate = aggregateSharedEmails(\n sharedQuery.data,\n catalogQueries\n .map((query) => query.data)\n .filter((catalog): catalog is AppEmailCatalog => Boolean(catalog)),\n );\n return {\n appId: \"core\",\n appName: t(\"dispatch.transactionalEmail.sharedTitle\"),\n appPath: t(\"dispatch.transactionalEmail.sharedSubtitle\"),\n emails: aggregate.emails,\n error: null,\n statsError: aggregate.statsError,\n };\n }, [sharedQuery.data, catalogQueries, t]);\n\n const engagementQueries = useQueries({\n queries: apps.map((app, index) => {\n const catalog = catalogQueries[index]?.data;\n const templateIds =\n catalog && !catalog.error\n ? catalog.emails.map((email) => email.id)\n : [];\n return {\n queryKey: [\n \"list-email-engagement\",\n app.id,\n templateIds.join(\",\"),\n WINDOW_DAYS,\n ],\n queryFn: () =>\n callAppAction<ProviderMetricsResult<EmailEngagement[]>>(\n app.path,\n \"list-email-engagement\",\n { templateIds, windowDays: WINDOW_DAYS },\n \"POST\",\n ),\n enabled: expanded[app.id] === true && templateIds.length > 0,\n };\n }),\n });\n\n const sharedLoading =\n sharedQuery.isLoading ||\n (sharedOpen && catalogQueries.some((query) => query.isLoading));\n const sharedError = catalogQueries.find((query) => query.isError)?.error;\n const sharedProviderReason = t(\n \"dispatch.transactionalEmail.sharedProviderMetricsUnavailable\",\n );\n\n return (\n <DispatchShell\n title={t(\"dispatch.transactionalEmail.title\")}\n description={t(\"dispatch.transactionalEmail.description\")}\n >\n <div className=\"flex flex-col gap-4\">\n <Alert>\n <IconInfoCircle className=\"size-4\" />\n <AlertTitle>\n {t(\"dispatch.transactionalEmail.retentionTitle\")}\n </AlertTitle>\n <AlertDescription>\n {t(\"dispatch.transactionalEmail.retentionNote\")}\n </AlertDescription>\n </Alert>\n\n {appsQuery.isError ? (\n <ActionQueryError\n error={appsQuery.error}\n onRetry={() => void appsQuery.refetch()}\n />\n ) : null}\n\n {appsQuery.isLoading ? (\n <div className=\"space-y-3\">\n <Skeleton className=\"h-16 w-full rounded-2xl\" />\n <Skeleton className=\"h-16 w-full rounded-2xl\" />\n </div>\n ) : null}\n\n {!appsQuery.isLoading && apps.length === 0 ? (\n <div className=\"rounded-2xl border border-dashed px-6 py-12 text-center text-sm text-muted-foreground\">\n <IconMail className=\"mx-auto mb-2 size-5\" />\n {t(\"dispatch.transactionalEmail.noApps\")}\n </div>\n ) : null}\n\n <AppEmailCard\n name={t(\"dispatch.transactionalEmail.sharedTitle\")}\n path={t(\"dispatch.transactionalEmail.sharedSubtitle\")}\n open={sharedOpen}\n onOpenChange={setSharedOpen}\n catalog={sharedCatalog}\n isLoading={sharedLoading}\n isError={sharedQuery.isError || Boolean(sharedError)}\n error={sharedQuery.error ?? sharedError}\n onRetry={() => {\n void sharedQuery.refetch();\n for (const query of catalogQueries) void query.refetch();\n }}\n engagementByTemplate={new Map()}\n engagementUnavailable={sharedProviderReason}\n engagementLoading={false}\n />\n\n {apps.map((app, index) => {\n const query = catalogQueries[index];\n const engagementQuery = engagementQueries[index];\n const engagementResult = engagementQuery?.data;\n const engagementUnavailable =\n engagementResult && !engagementResult.available\n ? engagementResult.reason\n : engagementQuery?.isError\n ? engagementQuery.error instanceof Error\n ? engagementQuery.error.message\n : String(engagementQuery.error)\n : null;\n const engagementByTemplate = new Map<string, EmailEngagement>();\n if (engagementResult?.available) {\n for (const entry of engagementResult.data) {\n engagementByTemplate.set(entry.templateId, entry);\n }\n }\n return (\n <AppEmailCard\n key={app.id}\n name={app.name}\n path={app.path}\n open={expanded[app.id] === true}\n onOpenChange={(next) =>\n setExpanded((current) => ({ ...current, [app.id]: next }))\n }\n catalog={query?.data}\n isLoading={query?.isLoading ?? false}\n isError={query?.isError ?? false}\n error={query?.error}\n onRetry={() => void query?.refetch()}\n engagementByTemplate={engagementByTemplate}\n engagementUnavailable={engagementUnavailable}\n engagementLoading={engagementQuery?.isLoading ?? false}\n />\n );\n })}\n </div>\n </DispatchShell>\n );\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agent-native/dispatch",
|
|
3
|
-
"version": "0.17.
|
|
3
|
+
"version": "0.17.4",
|
|
4
4
|
"description": "Dispatch — workspace control plane for agent-native apps. Vault, integrations, destinations, scheduled jobs, and cross-app delegation, shipped as a single drop-in package.",
|
|
5
5
|
"homepage": "https://github.com/BuilderIO/agent-native#readme",
|
|
6
6
|
"bugs": {
|
|
@@ -98,7 +98,7 @@
|
|
|
98
98
|
"typescript-7": "npm:typescript@^7.0.2",
|
|
99
99
|
"vite": "8.1.0",
|
|
100
100
|
"vitest": "^4.1.5",
|
|
101
|
-
"@agent-native/core": "0.
|
|
101
|
+
"@agent-native/core": "0.141.4"
|
|
102
102
|
},
|
|
103
103
|
"peerDependencies": {
|
|
104
104
|
"@agent-native/core": ">=0.8.0",
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
aggregateSharedEmails,
|
|
5
|
+
type AppEmailCatalog,
|
|
6
|
+
type AppTransactionalEmail,
|
|
7
|
+
type LocalTransactionalEmailCatalog,
|
|
8
|
+
} from "./transactional-emails";
|
|
9
|
+
|
|
10
|
+
const coreEmail: AppTransactionalEmail = {
|
|
11
|
+
id: "core.reset-password",
|
|
12
|
+
app: "core",
|
|
13
|
+
name: "Reset password",
|
|
14
|
+
trigger: "A password reset is requested.",
|
|
15
|
+
recipient: "The account address.",
|
|
16
|
+
recipientLabel: "Account address",
|
|
17
|
+
sender: "The configured sender.",
|
|
18
|
+
senderLabel: "Configured sender",
|
|
19
|
+
sent: 2,
|
|
20
|
+
failed: 1,
|
|
21
|
+
lastSentAt: 100,
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const local: LocalTransactionalEmailCatalog = {
|
|
25
|
+
app: "dispatch",
|
|
26
|
+
statsAvailable: true,
|
|
27
|
+
statsError: null,
|
|
28
|
+
emails: [coreEmail],
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
function appCatalog(
|
|
32
|
+
appId: string,
|
|
33
|
+
email: AppTransactionalEmail,
|
|
34
|
+
): AppEmailCatalog {
|
|
35
|
+
return {
|
|
36
|
+
appId,
|
|
37
|
+
appName: appId,
|
|
38
|
+
appPath: `/${appId}`,
|
|
39
|
+
emails: [email],
|
|
40
|
+
error: null,
|
|
41
|
+
statsError: null,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
describe("aggregateSharedEmails", () => {
|
|
46
|
+
it("sums shared email counts across sending apps", () => {
|
|
47
|
+
const result = aggregateSharedEmails(local, [
|
|
48
|
+
appCatalog("calendar", {
|
|
49
|
+
...coreEmail,
|
|
50
|
+
sent: 3,
|
|
51
|
+
failed: 0,
|
|
52
|
+
lastSentAt: 200,
|
|
53
|
+
}),
|
|
54
|
+
appCatalog("forms", {
|
|
55
|
+
...coreEmail,
|
|
56
|
+
sent: 1,
|
|
57
|
+
failed: 2,
|
|
58
|
+
lastSentAt: 150,
|
|
59
|
+
}),
|
|
60
|
+
]);
|
|
61
|
+
|
|
62
|
+
expect(result).toEqual({
|
|
63
|
+
statsError: null,
|
|
64
|
+
emails: [
|
|
65
|
+
{
|
|
66
|
+
...coreEmail,
|
|
67
|
+
sent: 6,
|
|
68
|
+
failed: 3,
|
|
69
|
+
lastSentAt: 200,
|
|
70
|
+
},
|
|
71
|
+
],
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("does not present partial totals when an app log is unreadable", () => {
|
|
76
|
+
const unreadable = appCatalog("calendar", coreEmail);
|
|
77
|
+
unreadable.statsError = "database unavailable";
|
|
78
|
+
|
|
79
|
+
expect(aggregateSharedEmails(local, [unreadable])).toEqual({
|
|
80
|
+
statsError: "database unavailable",
|
|
81
|
+
emails: [
|
|
82
|
+
{
|
|
83
|
+
...coreEmail,
|
|
84
|
+
sent: null,
|
|
85
|
+
failed: null,
|
|
86
|
+
lastSentAt: null,
|
|
87
|
+
},
|
|
88
|
+
],
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
});
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reads each mounted app's transactional email catalog from the browser.
|
|
3
|
+
*
|
|
4
|
+
* Workspace apps are path-mounted on a single origin, so the browser can call
|
|
5
|
+
* another app's action endpoint directly and its own session cookie carries the
|
|
6
|
+
* caller's identity. Fanning out here rather than server-side keeps every app's
|
|
7
|
+
* existing access checks in force — Dispatch never sees a catalog the signed-in
|
|
8
|
+
* user could not have loaded themselves.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export interface AppTransactionalEmail {
|
|
12
|
+
id: string;
|
|
13
|
+
app: string;
|
|
14
|
+
name: string;
|
|
15
|
+
trigger: string;
|
|
16
|
+
recipient: string;
|
|
17
|
+
recipientLabel: string;
|
|
18
|
+
sender: string;
|
|
19
|
+
senderLabel: string;
|
|
20
|
+
/** null when the send log could not be read — not the same as zero sends. */
|
|
21
|
+
sent: number | null;
|
|
22
|
+
failed: number | null;
|
|
23
|
+
lastSentAt: number | null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface AppEmailCatalog {
|
|
27
|
+
appId: string;
|
|
28
|
+
appName: string;
|
|
29
|
+
appPath: string;
|
|
30
|
+
emails: AppTransactionalEmail[];
|
|
31
|
+
/** Set when this app's catalog could not be read at all. */
|
|
32
|
+
error: string | null;
|
|
33
|
+
/** Set when the catalog loaded but its send counts did not. */
|
|
34
|
+
statsError: string | null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface LocalTransactionalEmailCatalog {
|
|
38
|
+
app: string;
|
|
39
|
+
statsAvailable: boolean;
|
|
40
|
+
statsError: string | null;
|
|
41
|
+
emails: AppTransactionalEmail[];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function aggregateSharedEmails(
|
|
45
|
+
local: LocalTransactionalEmailCatalog,
|
|
46
|
+
appCatalogs: AppEmailCatalog[],
|
|
47
|
+
): { emails: AppTransactionalEmail[]; statsError: string | null } {
|
|
48
|
+
const catalogs = [
|
|
49
|
+
{
|
|
50
|
+
appId: local.app,
|
|
51
|
+
appName: local.app,
|
|
52
|
+
appPath: "",
|
|
53
|
+
emails: local.emails,
|
|
54
|
+
error: null,
|
|
55
|
+
statsError: local.statsAvailable ? null : local.statsError,
|
|
56
|
+
},
|
|
57
|
+
...appCatalogs.filter((catalog) => catalog.appId !== local.app),
|
|
58
|
+
];
|
|
59
|
+
const unreadable = catalogs.find(
|
|
60
|
+
(catalog) => catalog.error || catalog.statsError,
|
|
61
|
+
);
|
|
62
|
+
const statsAvailable = !unreadable;
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
statsError: unreadable?.error ?? unreadable?.statsError ?? null,
|
|
66
|
+
emails: local.emails
|
|
67
|
+
.filter((email) => email.app === "core")
|
|
68
|
+
.map((definition) => {
|
|
69
|
+
if (!statsAvailable) {
|
|
70
|
+
return { ...definition, sent: null, failed: null, lastSentAt: null };
|
|
71
|
+
}
|
|
72
|
+
let sent = 0;
|
|
73
|
+
let failed = 0;
|
|
74
|
+
let lastSentAt: number | null = null;
|
|
75
|
+
for (const catalog of catalogs) {
|
|
76
|
+
const email = catalog.emails.find(
|
|
77
|
+
(candidate) => candidate.id === definition.id,
|
|
78
|
+
);
|
|
79
|
+
sent += email?.sent ?? 0;
|
|
80
|
+
failed += email?.failed ?? 0;
|
|
81
|
+
if (
|
|
82
|
+
email?.lastSentAt != null &&
|
|
83
|
+
(lastSentAt === null || email.lastSentAt > lastSentAt)
|
|
84
|
+
) {
|
|
85
|
+
lastSentAt = email.lastSentAt;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return { ...definition, sent, failed, lastSentAt };
|
|
89
|
+
}),
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function actionUrl(appPath: string, action: string, query = ""): string {
|
|
94
|
+
const base = appPath.replace(/\/$/, "");
|
|
95
|
+
return `${base}/_agent-native/actions/${action}${query}`;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export async function callAppAction<T>(
|
|
99
|
+
appPath: string,
|
|
100
|
+
action: string,
|
|
101
|
+
params: Record<string, unknown>,
|
|
102
|
+
method: "GET" | "POST",
|
|
103
|
+
): Promise<T> {
|
|
104
|
+
const query =
|
|
105
|
+
method === "GET"
|
|
106
|
+
? `?${new URLSearchParams(
|
|
107
|
+
Object.entries(params).map(([key, value]) => [key, String(value)]),
|
|
108
|
+
)}`
|
|
109
|
+
: "";
|
|
110
|
+
const res = await fetch(actionUrl(appPath, action, query), {
|
|
111
|
+
method,
|
|
112
|
+
credentials: "include",
|
|
113
|
+
headers: {
|
|
114
|
+
Accept: "application/json",
|
|
115
|
+
"Content-Type": "application/json",
|
|
116
|
+
"X-Agent-Native-Frontend": "1",
|
|
117
|
+
},
|
|
118
|
+
...(method === "POST" ? { body: JSON.stringify(params) } : {}),
|
|
119
|
+
});
|
|
120
|
+
if (!res.ok) throw new Error(`${action} failed: HTTP ${res.status}`);
|
|
121
|
+
return (await res.json()) as T;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Load one app's catalog. Never throws — a single unreachable app must not
|
|
126
|
+
* blank the whole screen, but its failure is returned rather than swallowed so
|
|
127
|
+
* the UI can say "couldn't read" instead of showing it as having no emails.
|
|
128
|
+
*/
|
|
129
|
+
const CATALOG_TIMEOUT_MS = 10_000;
|
|
130
|
+
|
|
131
|
+
export async function fetchAppEmailCatalog(
|
|
132
|
+
app: { id: string; name: string; path: string },
|
|
133
|
+
windowDays: number,
|
|
134
|
+
): Promise<AppEmailCatalog> {
|
|
135
|
+
const base: AppEmailCatalog = {
|
|
136
|
+
appId: app.id,
|
|
137
|
+
appName: app.name,
|
|
138
|
+
appPath: app.path,
|
|
139
|
+
emails: [],
|
|
140
|
+
error: null,
|
|
141
|
+
statsError: null,
|
|
142
|
+
};
|
|
143
|
+
try {
|
|
144
|
+
const res = await fetch(
|
|
145
|
+
actionUrl(
|
|
146
|
+
app.path,
|
|
147
|
+
"list-transactional-emails",
|
|
148
|
+
`?windowDays=${windowDays}`,
|
|
149
|
+
),
|
|
150
|
+
{
|
|
151
|
+
credentials: "include",
|
|
152
|
+
headers: { Accept: "application/json" },
|
|
153
|
+
signal: AbortSignal.timeout(CATALOG_TIMEOUT_MS),
|
|
154
|
+
},
|
|
155
|
+
);
|
|
156
|
+
if (!res.ok) {
|
|
157
|
+
return { ...base, error: `HTTP ${res.status}` };
|
|
158
|
+
}
|
|
159
|
+
const body = (await res.json()) as {
|
|
160
|
+
emails?: AppTransactionalEmail[];
|
|
161
|
+
statsAvailable?: boolean;
|
|
162
|
+
statsError?: string | null;
|
|
163
|
+
};
|
|
164
|
+
return {
|
|
165
|
+
...base,
|
|
166
|
+
emails: body.emails ?? [],
|
|
167
|
+
statsError:
|
|
168
|
+
body.statsAvailable === false ? (body.statsError ?? "unknown") : null,
|
|
169
|
+
};
|
|
170
|
+
} catch (error) {
|
|
171
|
+
return {
|
|
172
|
+
...base,
|
|
173
|
+
error: error instanceof Error ? error.message : String(error),
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export interface EmailPreview {
|
|
179
|
+
subject: string;
|
|
180
|
+
html: string;
|
|
181
|
+
text: string;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** Render one email's preview with dummy data, from the owning app. */
|
|
185
|
+
export async function fetchEmailPreview(
|
|
186
|
+
appPath: string,
|
|
187
|
+
id: string,
|
|
188
|
+
): Promise<EmailPreview> {
|
|
189
|
+
const res = await fetch(
|
|
190
|
+
actionUrl(
|
|
191
|
+
appPath,
|
|
192
|
+
"render-transactional-email-preview",
|
|
193
|
+
`?id=${encodeURIComponent(id)}`,
|
|
194
|
+
),
|
|
195
|
+
{ credentials: "include", headers: { Accept: "application/json" } },
|
|
196
|
+
);
|
|
197
|
+
if (!res.ok) {
|
|
198
|
+
throw new Error(`Preview failed: HTTP ${res.status}`);
|
|
199
|
+
}
|
|
200
|
+
return (await res.json()) as EmailPreview;
|
|
201
|
+
}
|
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
IconKey,
|
|
29
29
|
IconChevronDown,
|
|
30
30
|
IconLayersSubtract,
|
|
31
|
+
IconMail,
|
|
31
32
|
IconMessageQuestion,
|
|
32
33
|
IconMessages,
|
|
33
34
|
IconPlugConnected,
|
|
@@ -161,6 +162,13 @@ const OPERATIONS_NAV_ITEMS = [
|
|
|
161
162
|
icon: IconPuzzle,
|
|
162
163
|
section: "operations",
|
|
163
164
|
},
|
|
165
|
+
{
|
|
166
|
+
id: "transactional-email",
|
|
167
|
+
to: "/transactional-email",
|
|
168
|
+
label: "Transactional email",
|
|
169
|
+
icon: IconMail,
|
|
170
|
+
section: "operations",
|
|
171
|
+
},
|
|
164
172
|
{
|
|
165
173
|
id: "vault",
|
|
166
174
|
to: "/vault",
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { useT } from "@agent-native/core/client/i18n";
|
|
2
|
+
import { IconInfoCircle } from "@tabler/icons-react";
|
|
3
|
+
|
|
4
|
+
import { ActionQueryError } from "./action-query-error";
|
|
5
|
+
import { type ProviderMetricsResult } from "./transactional-email-metrics";
|
|
6
|
+
import { Alert, AlertDescription, AlertTitle } from "./ui/alert";
|
|
7
|
+
import { Skeleton } from "./ui/skeleton";
|
|
8
|
+
import {
|
|
9
|
+
Table,
|
|
10
|
+
TableBody,
|
|
11
|
+
TableCell,
|
|
12
|
+
TableHead,
|
|
13
|
+
TableHeader,
|
|
14
|
+
TableRow,
|
|
15
|
+
} from "./ui/table";
|
|
16
|
+
|
|
17
|
+
export interface EmailActivityEntry {
|
|
18
|
+
msgId: string;
|
|
19
|
+
toEmail: string;
|
|
20
|
+
fromEmail: string;
|
|
21
|
+
subject: string;
|
|
22
|
+
status: string;
|
|
23
|
+
opensCount: number;
|
|
24
|
+
clicksCount: number;
|
|
25
|
+
lastEventTime: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Renders one email's activity feed. Used both inline on the detail page and
|
|
30
|
+
* inside the list page's activity dialog, so the "unavailable" and "empty"
|
|
31
|
+
* states only need to be written once.
|
|
32
|
+
*/
|
|
33
|
+
export function ActivityTable({
|
|
34
|
+
result,
|
|
35
|
+
isLoading,
|
|
36
|
+
isError,
|
|
37
|
+
error,
|
|
38
|
+
onRetry,
|
|
39
|
+
}: {
|
|
40
|
+
result: ProviderMetricsResult<EmailActivityEntry[]> | undefined;
|
|
41
|
+
isLoading: boolean;
|
|
42
|
+
isError: boolean;
|
|
43
|
+
error: unknown;
|
|
44
|
+
onRetry: () => void;
|
|
45
|
+
}) {
|
|
46
|
+
const t = useT();
|
|
47
|
+
|
|
48
|
+
if (isError) {
|
|
49
|
+
return <ActionQueryError error={error} onRetry={onRetry} />;
|
|
50
|
+
}
|
|
51
|
+
if (isLoading) {
|
|
52
|
+
return <Skeleton className="h-40 w-full" />;
|
|
53
|
+
}
|
|
54
|
+
if (result && !result.available) {
|
|
55
|
+
return (
|
|
56
|
+
<Alert>
|
|
57
|
+
<IconInfoCircle className="size-4" />
|
|
58
|
+
<AlertTitle>
|
|
59
|
+
{t("dispatch.transactionalEmail.activityUnavailable")}
|
|
60
|
+
</AlertTitle>
|
|
61
|
+
<AlertDescription>{result.reason}</AlertDescription>
|
|
62
|
+
</Alert>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
if (result && result.data.length === 0) {
|
|
66
|
+
return (
|
|
67
|
+
<div className="rounded-xl border border-dashed px-4 py-8 text-sm text-muted-foreground">
|
|
68
|
+
{t("dispatch.transactionalEmail.activityEmpty")}
|
|
69
|
+
</div>
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
if (!result) return null;
|
|
73
|
+
|
|
74
|
+
return (
|
|
75
|
+
<div className="max-h-96 overflow-auto">
|
|
76
|
+
<Table>
|
|
77
|
+
<TableHeader>
|
|
78
|
+
<TableRow>
|
|
79
|
+
<TableHead>{t("dispatch.transactionalEmail.recipient")}</TableHead>
|
|
80
|
+
<TableHead>{t("dispatch.transactionalEmail.subject")}</TableHead>
|
|
81
|
+
<TableHead>{t("dispatch.transactionalEmail.status")}</TableHead>
|
|
82
|
+
<TableHead>{t("dispatch.transactionalEmail.opens")}</TableHead>
|
|
83
|
+
<TableHead>{t("dispatch.transactionalEmail.lastEvent")}</TableHead>
|
|
84
|
+
</TableRow>
|
|
85
|
+
</TableHeader>
|
|
86
|
+
<TableBody>
|
|
87
|
+
{result.data.map((entry) => (
|
|
88
|
+
<TableRow key={entry.msgId}>
|
|
89
|
+
<TableCell className="text-xs">{entry.toEmail}</TableCell>
|
|
90
|
+
<TableCell className="text-xs">{entry.subject}</TableCell>
|
|
91
|
+
<TableCell className="text-xs">{entry.status}</TableCell>
|
|
92
|
+
<TableCell className="text-xs tabular-nums">
|
|
93
|
+
{entry.opensCount}
|
|
94
|
+
</TableCell>
|
|
95
|
+
<TableCell className="text-xs">{entry.lastEventTime}</TableCell>
|
|
96
|
+
</TableRow>
|
|
97
|
+
))}
|
|
98
|
+
</TableBody>
|
|
99
|
+
</Table>
|
|
100
|
+
</div>
|
|
101
|
+
);
|
|
102
|
+
}
|