mirror of
https://github.com/warmbly/warmbly.git
synced 2026-10-29 16:14:37 +00:00
feat: show the new failed lead state in the campaign Leads view (pill with the worker's reason on hover, strip chip and counts) and toast user-addressed mailbox and send errors from the realtime ERROR event, which previously only refreshed the dashboard so a reply that could not be sent was never surfaced
This commit is contained in:
@@ -990,6 +990,7 @@ const LEAD_META: Record<
|
||||
completed: { label: "Done", dot: "bg-indigo-500", text: "text-indigo-700", Icon: CheckIcon },
|
||||
replied: { label: "Replied", dot: "bg-emerald-500", text: "text-emerald-700", Icon: CornerUpLeftIcon },
|
||||
bounced: { label: "Bounced", dot: "bg-rose-500", text: "text-rose-600", Icon: AlertTriangleIcon },
|
||||
failed: { label: "Failed", dot: "bg-rose-500", text: "text-rose-600", Icon: AlertTriangleIcon },
|
||||
unsubscribed: { label: "Unsubscribed", dot: "bg-slate-300", text: "text-slate-400", Icon: BanIcon },
|
||||
};
|
||||
|
||||
@@ -997,9 +998,16 @@ function LeadStatusPill({ lead }: { lead?: ContactCampaignProgress | null }) {
|
||||
const status: LeadStatus = lead?.status ?? "pending";
|
||||
const meta = LEAD_META[status];
|
||||
const Icon = meta.Icon;
|
||||
// A failed lead carries the worker's reason; surface it on hover since the
|
||||
// pill itself only has room for the word.
|
||||
const title =
|
||||
status === "failed" && lead?.failure_reason
|
||||
? `Could not send: ${lead.failure_reason}`
|
||||
: undefined;
|
||||
return (
|
||||
<span
|
||||
className={`inline-flex items-center gap-1.5 text-[10.5px] font-medium uppercase tracking-[0.08em] ${meta.text}`}
|
||||
title={title}
|
||||
>
|
||||
{status === "active" ? (
|
||||
<span className="campaign-grid text-sky-600 shrink-0" aria-hidden />
|
||||
@@ -1030,6 +1038,7 @@ function LeadProgressStrip({
|
||||
completed: 0,
|
||||
replied: 0,
|
||||
bounced: 0,
|
||||
failed: 0,
|
||||
unsubscribed: 0,
|
||||
};
|
||||
for (const ct of contacts) c[ct.campaign_lead?.status ?? "pending"]++;
|
||||
@@ -1045,6 +1054,7 @@ function LeadProgressStrip({
|
||||
{ key: "replied", color: "bg-emerald-500" },
|
||||
{ key: "pending", color: "bg-slate-300" },
|
||||
{ key: "bounced", color: "bg-rose-400" },
|
||||
{ key: "failed", color: "bg-rose-500" },
|
||||
{ key: "unsubscribed", color: "bg-slate-200" },
|
||||
];
|
||||
|
||||
@@ -1069,6 +1079,7 @@ function LeadProgressStrip({
|
||||
<StripChip dot="bg-emerald-500" label="Replied" n={counts.replied} />
|
||||
<StripChip dot="bg-slate-300" label="Queued" n={counts.pending} />
|
||||
<StripChip dot="bg-rose-400" label="Bounced" n={counts.bounced} />
|
||||
{counts.failed > 0 && <StripChip dot="bg-rose-500" label="Failed" n={counts.failed} />}
|
||||
<StripChip dot="bg-slate-300" label="Unsub" n={counts.unsubscribed} />
|
||||
</div>
|
||||
<div className="ml-auto flex items-center gap-2 text-[10.5px] text-slate-400 tabular-nums">
|
||||
|
||||
@@ -171,6 +171,24 @@ export function useRealtimeEvents() {
|
||||
return
|
||||
}
|
||||
|
||||
// A user-addressed mailbox/send error (a compose or reply the worker
|
||||
// could not send, a mailbox that needs re-authorizing). Nothing else
|
||||
// would tell the user, so toast it, then refresh the mailbox views.
|
||||
if (event === 'ERROR') {
|
||||
const data = payload.data as Record<string, unknown> | undefined
|
||||
const title =
|
||||
typeof data?.title === 'string' && data.title
|
||||
? data.title
|
||||
: (getString('message') ?? 'Something went wrong')
|
||||
const detail = typeof data?.message === 'string' ? data.message : ''
|
||||
toast.error(detail ? `${title}: ${detail}` : title, {
|
||||
id: `email-error-${getString('task_id') ?? getString('email_id') ?? 'general'}`,
|
||||
duration: 8000,
|
||||
})
|
||||
invalidate([['emails', 'list'], ['unibox']])
|
||||
return
|
||||
}
|
||||
|
||||
if (includes('ACCOUNT', 'EMAIL_STATUS', 'EMAIL_ERROR', 'WARMUP')) {
|
||||
// ACCOUNT_SYNC_STATE: the mailbox's import finished or fair use
|
||||
// started/stopped holding it; the drawer's sync card refetches.
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
import type MiniCampaign from "../campaigns/MiniCampaign";
|
||||
import type MiniCategory from "./MiniCategory";
|
||||
|
||||
// LeadStatus mirrors models.ContactCampaignProgress.Status — a contact's
|
||||
// LeadStatus mirrors models.ContactCampaignProgress.Status, a contact's
|
||||
// processing state inside a single campaign. "completed" = every step sent, no
|
||||
// reply (done); "active" = some but not all steps sent (still processing).
|
||||
// reply (done); "active" = some but not all steps sent (still processing);
|
||||
// "failed" = the mailbox could not send a step after every retry.
|
||||
export type LeadStatus =
|
||||
| "pending"
|
||||
| "active"
|
||||
| "completed"
|
||||
| "replied"
|
||||
| "bounced"
|
||||
| "failed"
|
||||
| "unsubscribed";
|
||||
|
||||
// ContactCampaignProgress is set only on contacts returned by a single-campaign
|
||||
@@ -25,6 +27,9 @@ export interface ContactCampaignProgress {
|
||||
// Label of the step the lead is on now (latest step sent). Empty when the
|
||||
// lead hasn't been contacted yet.
|
||||
current_step?: string;
|
||||
// The worker's reason for the last failed send; set only when status is
|
||||
// "failed".
|
||||
failure_reason?: string;
|
||||
}
|
||||
|
||||
export default interface Contact {
|
||||
|
||||
Reference in New Issue
Block a user