Fix Monday.com push: format column values by column type

The root cause was sending raw text to checkbox, status, dropdown, and
date columns. Monday.com requires specific JSON structures per type.

- Store column types in _columnTypes when Fetch Columns is clicked
- Format boolean/checkbox: {"checked": "true"/"false"}
- Format status/color: {"label": "value"}
- Format dropdown: {"labels": ["val1", "val2"]}
- Format date: parse MM/DD/YYYY to {"date": "YYYY-MM-DD"}
- Format email/phone/link with proper nested structures
- Text columns pass through as plain strings

Made-with: Cursor
This commit is contained in:
Randall Stillwell 2026-04-07 16:01:44 -05:00
parent a0f5022de2
commit d7d859f49d
3 changed files with 84 additions and 27 deletions

View file

@ -114,7 +114,7 @@ type SettingsData = {
mondayApiToken: string; mondayApiToken: string;
mondayBoardId: string; mondayBoardId: string;
mondayEnabled: boolean; mondayEnabled: boolean;
mondayColumnMap: Record<string, string> | null; mondayColumnMap: Record<string, unknown> | null;
mondayWebhookId: string; mondayWebhookId: string;
mondayWebhookUrl: string; mondayWebhookUrl: string;
webhookUrl: string; webhookUrl: string;
@ -358,6 +358,14 @@ export default function SettingsPage() {
const data = await res.json(); const data = await res.json();
if (res.ok && data.columns) { if (res.ok && data.columns) {
setMondayColumns(data.columns); setMondayColumns(data.columns);
const typeMap: Record<string, string> = {};
for (const col of data.columns as MondayColumn[]) {
typeMap[col.id] = col.type;
}
setSettings((s) => ({
...s,
mondayColumnMap: { ...(s.mondayColumnMap || {}), _columnTypes: typeMap },
}));
toast.success(`Found ${data.columns.length} columns`); toast.success(`Found ${data.columns.length} columns`);
} else { } else {
toast.error(data.error || "Failed to fetch columns"); toast.error(data.error || "Failed to fetch columns");
@ -945,7 +953,7 @@ export default function SettingsPage() {
<div key={field} className="flex items-center gap-2"> <div key={field} className="flex items-center gap-2">
<span className="text-xs text-muted-foreground w-36 shrink-0">{label}</span> <span className="text-xs text-muted-foreground w-36 shrink-0">{label}</span>
<Select <Select
value={(settings.mondayColumnMap || {})[field] || "__none__"} value={String((settings.mondayColumnMap || {})[field] || "__none__")}
onValueChange={(v) => setColumnMapping(field, !v || v === "__none__" ? "" : v)} onValueChange={(v) => setColumnMapping(field, !v || v === "__none__" ? "" : v)}
> >
<SelectTrigger className="h-8 text-xs"> <SelectTrigger className="h-8 text-xs">
@ -963,7 +971,7 @@ export default function SettingsPage() {
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="text-xs text-muted-foreground w-36 shrink-0">Files (images)</span> <span className="text-xs text-muted-foreground w-36 shrink-0">Files (images)</span>
<Select <Select
value={(settings.mondayColumnMap || {})._files || "__none__"} value={String((settings.mondayColumnMap || {})._files || "__none__")}
onValueChange={(v) => setColumnMapping("_files", !v || v === "__none__" ? "" : v)} onValueChange={(v) => setColumnMapping("_files", !v || v === "__none__" ? "" : v)}
> >
<SelectTrigger className="h-8 text-xs"> <SelectTrigger className="h-8 text-xs">

View file

@ -174,7 +174,7 @@ async function handleMonday(
} else if (event === "card_reviewed" || event === "card_exported") { } else if (event === "card_reviewed" || event === "card_exported") {
const token = settings.mondayApiToken; const token = settings.mondayApiToken;
const boardId = settings.mondayBoardId; const boardId = settings.mondayBoardId;
const columnMap = (settings.mondayColumnMap as Record<string, string>) ?? {}; const columnMap = (settings.mondayColumnMap as Record<string, unknown>) ?? {};
const mondayItemId = card.mondayItemId as string | null; const mondayItemId = card.mondayItemId as string | null;
if (mondayItemId) { if (mondayItemId) {
const columnValues = mapCardToColumnValues(card, columnMap); const columnValues = mapCardToColumnValues(card, columnMap);
@ -198,7 +198,7 @@ export async function pushCardToMonday(
const cardData = card as unknown as Record<string, unknown>; const cardData = card as unknown as Record<string, unknown>;
const token = settings.mondayApiToken; const token = settings.mondayApiToken;
const boardId = settings.mondayBoardId; const boardId = settings.mondayBoardId;
const columnMap = (settings.mondayColumnMap as Record<string, string>) ?? {}; const columnMap = (settings.mondayColumnMap as Record<string, unknown>) ?? {};
const columnValues = mapCardToColumnValues(cardData, columnMap); const columnValues = mapCardToColumnValues(cardData, columnMap);
const itemName = (card.name as string) || "Unnamed Card"; const itemName = (card.name as string) || "Unnamed Card";
@ -216,7 +216,7 @@ export async function pushCardToMonday(
data: { mondayItemId: itemId }, data: { mondayItemId: itemId },
}); });
const filesColId = columnMap._files; const filesColId = columnMap._files as string | undefined;
if (filesColId) { if (filesColId) {
for (const imgPath of [cardData.backImagePath, cardData.frontImagePath]) { for (const imgPath of [cardData.backImagePath, cardData.frontImagePath]) {
if (typeof imgPath === "string" && imgPath) { if (typeof imgPath === "string" && imgPath) {

View file

@ -140,45 +140,93 @@ export async function readItem(
}; };
} }
const BOOLEAN_CARD_FIELDS = new Set([ function toCleanString(val: unknown): string | null {
"iSaidYesBookSent", if (val === null || val === undefined) return null;
"ftGuestLetterSent",
"prayerForTeam",
"prayerConfidential",
]);
function formatValue(cardField: string, val: unknown): unknown | null {
if (BOOLEAN_CARD_FIELDS.has(cardField) || typeof val === "boolean") {
return { checked: val === true || val === "true" ? "true" : "false" };
}
if (Array.isArray(val)) { if (Array.isArray(val)) {
const joined = val.filter(Boolean).join(", "); const joined = val.filter(Boolean).join(", ");
return joined || null; return joined || null;
} }
if (typeof val === "object") { if (typeof val === "object") {
const s = JSON.stringify(val); const s = JSON.stringify(val);
return s === "{}" || s === "[]" ? null : s; return s === "{}" || s === "[]" ? null : s;
} }
const str = String(val).trim();
if (str === "" || str === "null" || str === "undefined" || str === "[object Object]") return null;
return str;
}
const strVal = String(val).trim(); function formatForColumnType(colType: string, val: unknown): unknown | null {
if (strVal === "" || strVal === "null" || strVal === "undefined" || strVal === "[object Object]") return null; switch (colType) {
return strVal; case "boolean": {
const isChecked = val === true || val === "true";
return { checked: isChecked ? "true" : "false" };
}
case "color": {
const str = toCleanString(val);
return str ? { label: str } : null;
}
case "dropdown": {
const str = toCleanString(val);
if (!str) return null;
const labels = str.split(",").map((s) => s.trim()).filter(Boolean);
return labels.length > 0 ? { labels } : null;
}
case "date": {
const str = toCleanString(val);
if (!str) return null;
const iso = parseToISO(str);
return iso ? { date: iso } : str;
}
case "email": {
const str = toCleanString(val);
return str ? { email: str, text: str } : null;
}
case "phone": {
const str = toCleanString(val);
return str ? { phone: str, countryShortName: "US" } : null;
}
case "link": {
const str = toCleanString(val);
return str ? { url: str, text: str } : null;
}
default: {
return toCleanString(val);
}
}
}
function parseToISO(dateStr: string): string | null {
const parts = dateStr.match(/^(\d{1,2})[/\-.](\d{1,2})[/\-.](\d{2,4})$/);
if (!parts) return null;
let [, m, d, y] = parts;
if (y.length === 2) y = parseInt(y) > 50 ? `19${y}` : `20${y}`;
const month = m.padStart(2, "0");
const day = d.padStart(2, "0");
return `${y}-${month}-${day}`;
} }
export function mapCardToColumnValues( export function mapCardToColumnValues(
card: Record<string, unknown>, card: Record<string, unknown>,
columnMap: Record<string, string> columnMap: Record<string, unknown>
): Record<string, unknown> { ): Record<string, unknown> {
const values: Record<string, unknown> = {}; const values: Record<string, unknown> = {};
const columnTypes = ((columnMap._columnTypes as Record<string, string>) ?? {});
for (const [cardField, colId] of Object.entries(columnMap)) { for (const [cardField, rawColId] of Object.entries(columnMap)) {
if (!colId || !cardField || cardField.startsWith("_")) continue; if (!rawColId || !cardField || cardField.startsWith("_") || typeof rawColId !== "string") continue;
const colId = rawColId;
const val = card[cardField]; const val = card[cardField];
if (val === null || val === undefined) continue; if (val === null || val === undefined) continue;
const formatted = formatValue(cardField, val); const colType = columnTypes[colId] || "text";
const formatted = formatForColumnType(colType, val);
if (formatted !== null) { if (formatted !== null) {
values[colId] = formatted; values[colId] = formatted;
} }
@ -189,10 +237,11 @@ export function mapCardToColumnValues(
export function mapItemToCardFields( export function mapItemToCardFields(
columnValues: ItemColumnValue[], columnValues: ItemColumnValue[],
columnMap: Record<string, string> columnMap: Record<string, unknown>
): Record<string, string> { ): Record<string, string> {
const reverseMap: Record<string, string> = {}; const reverseMap: Record<string, string> = {};
for (const [cardField, colId] of Object.entries(columnMap)) { for (const [cardField, colId] of Object.entries(columnMap)) {
if (cardField.startsWith("_") || typeof colId !== "string") continue;
reverseMap[colId] = cardField; reverseMap[colId] = cardField;
} }