feat: add email export option (#212)
This commit is contained in:
@@ -16,6 +16,7 @@ import {
|
|||||||
MailSearch,
|
MailSearch,
|
||||||
MailWarning,
|
MailWarning,
|
||||||
MailX,
|
MailX,
|
||||||
|
Download,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { formatDate, formatDistanceToNow } from "date-fns";
|
import { formatDate, formatDistanceToNow } from "date-fns";
|
||||||
import { EmailStatus } from "@prisma/client";
|
import { EmailStatus } from "@prisma/client";
|
||||||
@@ -74,6 +75,16 @@ export default function EmailsList() {
|
|||||||
apiId: apiId,
|
apiId: apiId,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const exportQuery = api.email.exportEmails.useQuery(
|
||||||
|
{
|
||||||
|
status: status?.toUpperCase() as EmailStatus,
|
||||||
|
domain: domainId,
|
||||||
|
search,
|
||||||
|
apiId: apiId,
|
||||||
|
},
|
||||||
|
{ enabled: false },
|
||||||
|
);
|
||||||
|
|
||||||
const { data: domainsQuery } = api.domain.domains.useQuery();
|
const { data: domainsQuery } = api.domain.domains.useQuery();
|
||||||
const { data: apiKeysQuery } = api.apiKey.getApiKeys.useQuery();
|
const { data: apiKeysQuery } = api.apiKey.getApiKeys.useQuery();
|
||||||
|
|
||||||
@@ -99,9 +110,43 @@ export default function EmailsList() {
|
|||||||
setSearch(value);
|
setSearch(value);
|
||||||
}, 1000);
|
}, 1000);
|
||||||
|
|
||||||
|
const handleExport = async () => {
|
||||||
|
try {
|
||||||
|
const resp = await exportQuery.refetch();
|
||||||
|
if (!resp.data) return;
|
||||||
|
|
||||||
|
const escape = (val: unknown) => {
|
||||||
|
const s = String(val ?? "");
|
||||||
|
const startsRisky = /^\s*[=+\-@]/.test(s);
|
||||||
|
const safe = (startsRisky ? "'" : "") + s.replace(/"/g, '""');
|
||||||
|
return /[",\r\n]/.test(safe) ? `"${safe}"` : safe;
|
||||||
|
};
|
||||||
|
|
||||||
|
const header = ["To", "Status", "Subject", "Sent At"].join(",");
|
||||||
|
const rows = resp.data.map((e) =>
|
||||||
|
[e.to, e.status, e.subject, e.sentAt].map(escape).join(","),
|
||||||
|
);
|
||||||
|
const csv = [header, ...rows].join("\n");
|
||||||
|
|
||||||
|
const blob = new Blob(["\uFEFF" + csv], {
|
||||||
|
type: "text/csv;charset=utf-8",
|
||||||
|
});
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement("a");
|
||||||
|
a.href = url;
|
||||||
|
a.download = `emails-${new Date().toISOString().split("T")[0]}.csv`;
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
a.remove();
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Export failed", err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mt-10 flex flex-col gap-4">
|
<div className="mt-10 flex flex-col gap-4">
|
||||||
<div className="flex justify-between">
|
<div className="flex justify-between items-center">
|
||||||
<Input
|
<Input
|
||||||
placeholder="Search by subject or email"
|
placeholder="Search by subject or email"
|
||||||
className="w-[350px] mr-4"
|
className="w-[350px] mr-4"
|
||||||
@@ -181,6 +226,14 @@ export default function EmailsList() {
|
|||||||
))}
|
))}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={handleExport}
|
||||||
|
disabled={exportQuery.isFetching}
|
||||||
|
>
|
||||||
|
<Download className="h-4 w-4 mr-2" />
|
||||||
|
Export
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col rounded-xl border shadow">
|
<div className="flex flex-col rounded-xl border shadow">
|
||||||
|
@@ -22,7 +22,7 @@ export const emailRouter = createTRPCRouter({
|
|||||||
domain: z.number().optional(),
|
domain: z.number().optional(),
|
||||||
search: z.string().optional().nullable(),
|
search: z.string().optional().nullable(),
|
||||||
apiId: z.number().optional(),
|
apiId: z.number().optional(),
|
||||||
})
|
}),
|
||||||
)
|
)
|
||||||
.query(async ({ ctx, input }) => {
|
.query(async ({ ctx, input }) => {
|
||||||
const page = input.page || 1;
|
const page = input.page || 1;
|
||||||
@@ -61,6 +61,59 @@ export const emailRouter = createTRPCRouter({
|
|||||||
return { emails };
|
return { emails };
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
exportEmails: teamProcedure
|
||||||
|
.input(
|
||||||
|
z.object({
|
||||||
|
status: z.enum(statuses).optional().nullable(),
|
||||||
|
domain: z.number().optional(),
|
||||||
|
search: z.string().optional().nullable(),
|
||||||
|
apiId: z.number().optional(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.query(async ({ ctx, input }) => {
|
||||||
|
const emails = await db.$queryRaw<
|
||||||
|
Array<{
|
||||||
|
to: string[];
|
||||||
|
latestStatus: EmailStatus;
|
||||||
|
subject: string;
|
||||||
|
scheduledAt: Date | null;
|
||||||
|
createdAt: Date;
|
||||||
|
}>
|
||||||
|
>`
|
||||||
|
SELECT
|
||||||
|
"to",
|
||||||
|
"latestStatus",
|
||||||
|
subject,
|
||||||
|
"scheduledAt",
|
||||||
|
"createdAt"
|
||||||
|
FROM "Email"
|
||||||
|
WHERE "teamId" = ${ctx.team.id}
|
||||||
|
${input.status ? Prisma.sql`AND "latestStatus"::text = ${input.status}` : Prisma.sql``}
|
||||||
|
${input.domain ? Prisma.sql`AND "domainId" = ${input.domain}` : Prisma.sql``}
|
||||||
|
${input.apiId ? Prisma.sql`AND "apiId" = ${input.apiId}` : Prisma.sql``}
|
||||||
|
${
|
||||||
|
input.search
|
||||||
|
? Prisma.sql`AND (
|
||||||
|
"subject" ILIKE ${`%${input.search}%`}
|
||||||
|
OR EXISTS (
|
||||||
|
SELECT 1 FROM unnest("to") AS email
|
||||||
|
WHERE email ILIKE ${`%${input.search}%`}
|
||||||
|
)
|
||||||
|
)`
|
||||||
|
: Prisma.sql``
|
||||||
|
}
|
||||||
|
ORDER BY "createdAt" DESC
|
||||||
|
LIMIT 10000
|
||||||
|
`;
|
||||||
|
|
||||||
|
return emails.map((email) => ({
|
||||||
|
to: email.to.join("; "),
|
||||||
|
status: email.latestStatus,
|
||||||
|
subject: email.subject,
|
||||||
|
sentAt: (email.scheduledAt ?? email.createdAt).toISOString(),
|
||||||
|
}));
|
||||||
|
}),
|
||||||
|
|
||||||
getEmail: emailProcedure.query(async ({ input }) => {
|
getEmail: emailProcedure.query(async ({ input }) => {
|
||||||
const email = await db.email.findUnique({
|
const email = await db.email.findUnique({
|
||||||
where: {
|
where: {
|
||||||
|
Reference in New Issue
Block a user