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:
parent
a0f5022de2
commit
d7d859f49d
3 changed files with 84 additions and 27 deletions
|
|
@ -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">
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue