From d7d859f49d4876a0c039e5bb93cae8e029f8fef5 Mon Sep 17 00:00:00 2001 From: Randall Stillwell Date: Tue, 7 Apr 2026 16:01:44 -0500 Subject: [PATCH] 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 --- src/app/settings/page.tsx | 14 ++++-- src/lib/integrations.ts | 6 +-- src/lib/monday.ts | 91 ++++++++++++++++++++++++++++++--------- 3 files changed, 84 insertions(+), 27 deletions(-) diff --git a/src/app/settings/page.tsx b/src/app/settings/page.tsx index a1bf3bb..7417783 100644 --- a/src/app/settings/page.tsx +++ b/src/app/settings/page.tsx @@ -114,7 +114,7 @@ type SettingsData = { mondayApiToken: string; mondayBoardId: string; mondayEnabled: boolean; - mondayColumnMap: Record | null; + mondayColumnMap: Record | null; mondayWebhookId: string; mondayWebhookUrl: string; webhookUrl: string; @@ -358,6 +358,14 @@ export default function SettingsPage() { const data = await res.json(); if (res.ok && data.columns) { setMondayColumns(data.columns); + const typeMap: Record = {}; + 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`); } else { toast.error(data.error || "Failed to fetch columns"); @@ -945,7 +953,7 @@ export default function SettingsPage() {
{label} setColumnMapping("_files", !v || v === "__none__" ? "" : v)} > diff --git a/src/lib/integrations.ts b/src/lib/integrations.ts index 65731e2..a0b632f 100644 --- a/src/lib/integrations.ts +++ b/src/lib/integrations.ts @@ -174,7 +174,7 @@ async function handleMonday( } else if (event === "card_reviewed" || event === "card_exported") { const token = settings.mondayApiToken; const boardId = settings.mondayBoardId; - const columnMap = (settings.mondayColumnMap as Record) ?? {}; + const columnMap = (settings.mondayColumnMap as Record) ?? {}; const mondayItemId = card.mondayItemId as string | null; if (mondayItemId) { const columnValues = mapCardToColumnValues(card, columnMap); @@ -198,7 +198,7 @@ export async function pushCardToMonday( const cardData = card as unknown as Record; const token = settings.mondayApiToken; const boardId = settings.mondayBoardId; - const columnMap = (settings.mondayColumnMap as Record) ?? {}; + const columnMap = (settings.mondayColumnMap as Record) ?? {}; const columnValues = mapCardToColumnValues(cardData, columnMap); const itemName = (card.name as string) || "Unnamed Card"; @@ -216,7 +216,7 @@ export async function pushCardToMonday( data: { mondayItemId: itemId }, }); - const filesColId = columnMap._files; + const filesColId = columnMap._files as string | undefined; if (filesColId) { for (const imgPath of [cardData.backImagePath, cardData.frontImagePath]) { if (typeof imgPath === "string" && imgPath) { diff --git a/src/lib/monday.ts b/src/lib/monday.ts index 1499ebf..b604041 100644 --- a/src/lib/monday.ts +++ b/src/lib/monday.ts @@ -140,45 +140,93 @@ export async function readItem( }; } -const BOOLEAN_CARD_FIELDS = new Set([ - "iSaidYesBookSent", - "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" }; - } - +function toCleanString(val: unknown): string | null { + if (val === null || val === undefined) return null; if (Array.isArray(val)) { const joined = val.filter(Boolean).join(", "); return joined || null; } - if (typeof val === "object") { const s = JSON.stringify(val); 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(); - if (strVal === "" || strVal === "null" || strVal === "undefined" || strVal === "[object Object]") return null; - return strVal; +function formatForColumnType(colType: string, val: unknown): unknown | null { + switch (colType) { + 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( card: Record, - columnMap: Record + columnMap: Record ): Record { const values: Record = {}; + const columnTypes = ((columnMap._columnTypes as Record) ?? {}); - for (const [cardField, colId] of Object.entries(columnMap)) { - if (!colId || !cardField || cardField.startsWith("_")) continue; + for (const [cardField, rawColId] of Object.entries(columnMap)) { + if (!rawColId || !cardField || cardField.startsWith("_") || typeof rawColId !== "string") continue; + const colId = rawColId; const val = card[cardField]; if (val === null || val === undefined) continue; - const formatted = formatValue(cardField, val); + const colType = columnTypes[colId] || "text"; + const formatted = formatForColumnType(colType, val); if (formatted !== null) { values[colId] = formatted; } @@ -189,10 +237,11 @@ export function mapCardToColumnValues( export function mapItemToCardFields( columnValues: ItemColumnValue[], - columnMap: Record + columnMap: Record ): Record { const reverseMap: Record = {}; for (const [cardField, colId] of Object.entries(columnMap)) { + if (cardField.startsWith("_") || typeof colId !== "string") continue; reverseMap[colId] = cardField; }