Fix scheduled job list/edit dropping message content
toClient() never returned the message body (html/text for email, message for WhatsApp), so "Manage scheduled" showed nothing to view or edit even though the content existed in storage. Editing a scheduled WhatsApp message also saved to the wrong payload field (text instead of message), silently discarding the edit. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -12,6 +12,7 @@ and this project follows [Semantic Versioning](https://semver.org/).
|
|||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
- Scheduling a WhatsApp message was silently sent as an email instead: the scheduled-job store never persisted the `channel` field, so the send worker always fell through to its email branch regardless of what was requested. Scheduled WhatsApp jobs now correctly send via WhatsApp.
|
- Scheduling a WhatsApp message was silently sent as an email instead: the scheduled-job store never persisted the `channel` field, so the send worker always fell through to its email branch regardless of what was requested. Scheduled WhatsApp jobs now correctly send via WhatsApp.
|
||||||
|
- Scheduled emails/WhatsApp messages showed no content when viewing or editing them in "Manage scheduled", even though the message existed in storage: the list endpoint never returned the message body (`html`/`text` for email, `message` for WhatsApp), and editing a scheduled WhatsApp message saved to the wrong payload field (`text` instead of `message`), so edits were silently lost. Both are now fixed, and the email list also shows a body preview like the WhatsApp one already did.
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ function toClient(job) {
|
|||||||
const subject = job?.payload?.subject || '';
|
const subject = job?.payload?.subject || '';
|
||||||
const html = job?.payload?.html || '';
|
const html = job?.payload?.html || '';
|
||||||
const text = job?.payload?.text || '';
|
const text = job?.payload?.text || '';
|
||||||
|
const message = job?.payload?.message || '';
|
||||||
return {
|
return {
|
||||||
id: job.id,
|
id: job.id,
|
||||||
kind,
|
kind,
|
||||||
@@ -20,6 +21,9 @@ function toClient(job) {
|
|||||||
sentAt: job.sentAt || null,
|
sentAt: job.sentAt || null,
|
||||||
lastError: job.lastError || null,
|
lastError: job.lastError || null,
|
||||||
subject,
|
subject,
|
||||||
|
html,
|
||||||
|
text,
|
||||||
|
message,
|
||||||
hasHtml: !!html,
|
hasHtml: !!html,
|
||||||
hasText: !!text,
|
hasText: !!text,
|
||||||
};
|
};
|
||||||
@@ -50,7 +54,7 @@ const listScheduledEmails = async (req, res) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// PATCH /api/scheduled-emails/:id
|
// PATCH /api/scheduled-emails/:id
|
||||||
// Allows editing scheduledAt, subject, html/text on queued jobs only
|
// Allows editing scheduledAt and message content (subject/html/text for email jobs, message for WhatsApp jobs) on queued jobs only
|
||||||
const updateScheduledEmail = async (req, res) => {
|
const updateScheduledEmail = async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
@@ -58,7 +62,7 @@ const updateScheduledEmail = async (req, res) => {
|
|||||||
if (!job) return res.status(404).json({ message: 'Job not found' });
|
if (!job) return res.status(404).json({ message: 'Job not found' });
|
||||||
if (job.status !== 'queued') return res.status(400).json({ message: 'Only queued jobs can be edited' });
|
if (job.status !== 'queued') return res.status(400).json({ message: 'Only queued jobs can be edited' });
|
||||||
|
|
||||||
const { scheduledAt, subject, html, text } = req.body || {};
|
const { scheduledAt, subject, html, text, message } = req.body || {};
|
||||||
|
|
||||||
const patch = {};
|
const patch = {};
|
||||||
if (scheduledAt) {
|
if (scheduledAt) {
|
||||||
@@ -66,14 +70,12 @@ const updateScheduledEmail = async (req, res) => {
|
|||||||
if (isNaN(when.getTime())) return res.status(400).json({ message: 'scheduledAt must be a valid ISO date-time' });
|
if (isNaN(when.getTime())) return res.status(400).json({ message: 'scheduledAt must be a valid ISO date-time' });
|
||||||
patch.scheduledAt = when.toISOString();
|
patch.scheduledAt = when.toISOString();
|
||||||
}
|
}
|
||||||
if (subject != null || html != null || text != null) {
|
if (subject != null || html != null || text != null || message != null) {
|
||||||
const payload = { ...(job.payload || {}) };
|
const payload = { ...(job.payload || {}) };
|
||||||
if (subject != null) payload.subject = subject;
|
if (subject != null) payload.subject = subject;
|
||||||
if (html != null || text != null) {
|
|
||||||
// If html provided explicitly, set html; if text provided, set text
|
|
||||||
if (html != null) payload.html = html;
|
if (html != null) payload.html = html;
|
||||||
if (text != null) payload.text = text;
|
if (text != null) payload.text = text;
|
||||||
}
|
if (message != null) payload.message = message;
|
||||||
patch.payload = payload;
|
patch.payload = payload;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -261,7 +261,7 @@ function EmailAttendeesPageInner() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Scheduled jobs state
|
// Scheduled jobs state
|
||||||
type ScheduledJob = { id: string; kind: 'attendees'|'broadcast'|'unknown'; eventId?: string|null; broadcast?: boolean; channel?: string; recipient?: string|null; scheduledAt: string; createdAt: string; status: 'queued'|'sending'|'sent'|'error'; attempts: number; sentAt?: string|null; lastError?: string|null; subject?: string; hasHtml?: boolean; hasText?: boolean };
|
type ScheduledJob = { id: string; kind: 'attendees'|'broadcast'|'unknown'; eventId?: string|null; broadcast?: boolean; channel?: string; recipient?: string|null; scheduledAt: string; createdAt: string; status: 'queued'|'sending'|'sent'|'error'; attempts: number; sentAt?: string|null; lastError?: string|null; subject?: string; html?: string; text?: string; hasHtml?: boolean; hasText?: boolean };
|
||||||
const [scheduled, setScheduled] = useState<ScheduledJob[]>([]);
|
const [scheduled, setScheduled] = useState<ScheduledJob[]>([]);
|
||||||
const [loadingScheduled, setLoadingScheduled] = useState(false);
|
const [loadingScheduled, setLoadingScheduled] = useState(false);
|
||||||
const [editing, setEditing] = useState<ScheduledJob | null>(null);
|
const [editing, setEditing] = useState<ScheduledJob | null>(null);
|
||||||
@@ -288,7 +288,7 @@ function EmailAttendeesPageInner() {
|
|||||||
const openEdit = (job: ScheduledJob) => {
|
const openEdit = (job: ScheduledJob) => {
|
||||||
setEditing(job);
|
setEditing(job);
|
||||||
setEditSubject(job.subject || '');
|
setEditSubject(job.subject || '');
|
||||||
setEditBody(''); // body not included in list; will let user set a new one if needed
|
setEditBody(job.html || job.text || '');
|
||||||
try { setEditWhen(toLocalInputValue(new Date(job.scheduledAt))); } catch { setEditWhen(''); }
|
try { setEditWhen(toLocalInputValue(new Date(job.scheduledAt))); } catch { setEditWhen(''); }
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -882,6 +882,13 @@ Jane Doe <jane@example.com>
|
|||||||
<span className="inline-block px-2 py-0.5 text-xs rounded border bg-gray-50">{job.kind}</span>
|
<span className="inline-block px-2 py-0.5 text-xs rounded border bg-gray-50">{job.kind}</span>
|
||||||
<span className="truncate">{job.subject || '(no subject)'}</span>
|
<span className="truncate">{job.subject || '(no subject)'}</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="text-xs text-gray-600 mt-1 truncate">
|
||||||
|
{(() => {
|
||||||
|
const body = (job.html || job.text || '').replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim();
|
||||||
|
if (!body) return '(no content)';
|
||||||
|
return body.length > 80 ? body.slice(0, 80) + '…' : body;
|
||||||
|
})()}
|
||||||
|
</div>
|
||||||
<div className="text-xs text-gray-600 mt-1">
|
<div className="text-xs text-gray-600 mt-1">
|
||||||
<span className="mr-2">To: {job.recipient || 'Unknown recipients'}</span>
|
<span className="mr-2">To: {job.recipient || 'Unknown recipients'}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -535,7 +535,7 @@ function WhatsAppAttendeesPageInner() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// ── Scheduled tab ──────────────────────────────────────────────────────────
|
// ── Scheduled tab ──────────────────────────────────────────────────────────
|
||||||
type ScheduledJob = { id: string; kind: string; eventId?: string | null; broadcast?: boolean; channel?: string; recipient?: string | null; scheduledAt: string; createdAt: string; status: string; attempts: number; sentAt?: string | null; lastError?: string | null; subject?: string; payload?: any };
|
type ScheduledJob = { id: string; kind: string; eventId?: string | null; broadcast?: boolean; channel?: string; recipient?: string | null; scheduledAt: string; createdAt: string; status: string; attempts: number; sentAt?: string | null; lastError?: string | null; subject?: string; message?: string };
|
||||||
const [scheduled, setScheduled] = useState<ScheduledJob[]>([]);
|
const [scheduled, setScheduled] = useState<ScheduledJob[]>([]);
|
||||||
const [loadingScheduled, setLoadingScheduled] = useState(false);
|
const [loadingScheduled, setLoadingScheduled] = useState(false);
|
||||||
const [editing, setEditing] = useState<ScheduledJob | null>(null);
|
const [editing, setEditing] = useState<ScheduledJob | null>(null);
|
||||||
@@ -561,7 +561,7 @@ function WhatsAppAttendeesPageInner() {
|
|||||||
if (!token) { setError("Not authenticated"); return; }
|
if (!token) { setError("Not authenticated"); return; }
|
||||||
const body: any = {};
|
const body: any = {};
|
||||||
if (editWhen) body.scheduledAt = new Date(editWhen).toISOString();
|
if (editWhen) body.scheduledAt = new Date(editWhen).toISOString();
|
||||||
if (editMessage.trim()) body.text = editMessage;
|
if (editMessage.trim()) body.message = editMessage;
|
||||||
await apiFetch(`/api/scheduled-emails/${encodeURIComponent(editing.id)}`, { method: "PATCH", authToken: token, body });
|
await apiFetch(`/api/scheduled-emails/${encodeURIComponent(editing.id)}`, { method: "PATCH", authToken: token, body });
|
||||||
setInfo("Scheduled message updated.");
|
setInfo("Scheduled message updated.");
|
||||||
setEditing(null);
|
setEditing(null);
|
||||||
@@ -888,7 +888,7 @@ function WhatsAppAttendeesPageInner() {
|
|||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<div className="text-sm font-medium text-gray-900 flex items-center gap-2">
|
<div className="text-sm font-medium text-gray-900 flex items-center gap-2">
|
||||||
<span className="inline-block px-2 py-0.5 text-xs rounded border bg-green-50 text-green-700">{job.broadcast ? "broadcast" : "attendees"}</span>
|
<span className="inline-block px-2 py-0.5 text-xs rounded border bg-green-50 text-green-700">{job.broadcast ? "broadcast" : "attendees"}</span>
|
||||||
<span className="truncate text-gray-700">{job.payload?.message ? String(job.payload.message).slice(0, 60) + (String(job.payload.message).length > 60 ? "…" : "") : "(no message)"}</span>
|
<span className="truncate text-gray-700">{job.message ? String(job.message).slice(0, 60) + (String(job.message).length > 60 ? "…" : "") : "(no message)"}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs text-gray-600 mt-1">
|
<div className="text-xs text-gray-600 mt-1">
|
||||||
<span className="mr-2">To: {job.recipient || "Unknown recipients"}</span>
|
<span className="mr-2">To: {job.recipient || "Unknown recipients"}</span>
|
||||||
@@ -902,7 +902,7 @@ function WhatsAppAttendeesPageInner() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2 shrink-0">
|
<div className="flex items-center gap-2 shrink-0">
|
||||||
<button type="button" disabled={job.status !== "queued"} className="px-2 py-1 text-xs rounded border bg-white hover:bg-gray-50 disabled:opacity-50"
|
<button type="button" disabled={job.status !== "queued"} className="px-2 py-1 text-xs rounded border bg-white hover:bg-gray-50 disabled:opacity-50"
|
||||||
onClick={() => { setEditing(job); setEditMessage(job.payload?.message || ""); try { setEditWhen(toLocalInputValue(new Date(job.scheduledAt))); } catch { setEditWhen(""); } }}>
|
onClick={() => { setEditing(job); setEditMessage(job.message || ""); try { setEditWhen(toLocalInputValue(new Date(job.scheduledAt))); } catch { setEditWhen(""); } }}>
|
||||||
Edit
|
Edit
|
||||||
</button>
|
</button>
|
||||||
<button type="button" disabled={job.status !== "queued"} className="px-2 py-1 text-xs rounded border bg-white hover:bg-gray-50 disabled:opacity-50" onClick={() => removeJob(job)}>
|
<button type="button" disabled={job.status !== "queued"} className="px-2 py-1 text-xs rounded border bg-white hover:bg-gray-50 disabled:opacity-50" onClick={() => removeJob(job)}>
|
||||||
|
|||||||
Reference in New Issue
Block a user