{"Name":"Sub List to HTML Table","Description":"Generates a professionally styled HTML table from a Yeeflow Sub List for use in emails, documents, or web pages.","ImplType":0,"DraftCode":"export async function main({connections, params, modules}: ServiceContext) {\n  /**\n   * Escape HTML special characters to keep markup safe and valid.\n   */\n  function escapeHtml(value: any): string {\n    const str = value == null ? '' : String(value);\n    return str\n      .replace(/&/g, '&amp;')\n      .replace(/</g, '&lt;')\n      .replace(/>/g, '&gt;')\n      .replace(/\"/g, '&quot;')\n      .replace(/'/g, '&#39;');\n  }\n\n  /**\n   * Normalize incoming subListJson into an array-safe structure.\n   * - Accepts array directly.\n   * - Accepts JSON string and parses safely.\n   * - Invalid/empty input becomes empty array.\n   */\n  function normalizeRows(input: any): any[] {\n    if (Array.isArray(input)) return input;\n    if (input == null || input === '') return [];\n\n    if (typeof input === 'string') {\n      const trimmed = input.trim();\n      if (!trimmed) return [];\n      try {\n        const parsed = JSON.parse(trimmed);\n        return Array.isArray(parsed) ? parsed : [];\n      } catch {\n        return [];\n      }\n    }\n\n    return [];\n  }\n\n  /**\n   * Detect business columns dynamically from row objects,\n   * excluding internal/system fields.\n   */\n  function detectBusinessColumns(rows: any[]): string[] {\n    const excluded = new Set(['_id', '__ctxidx']);\n    const columns: string[] = [];\n    const seen = new Set<string>();\n\n    for (const row of rows) {\n      if (!row || typeof row !== 'object' || Array.isArray(row)) continue;\n      for (const key of Object.keys(row)) {\n        if (excluded.has(key)) continue;\n        if (!seen.has(key)) {\n          seen.add(key);\n          columns.push(key);\n        }\n      }\n    }\n\n    return columns;\n  }\n\n  /**\n   * Check whether a row is empty across all detected business fields.\n   */\n  function isBusinessRowEmpty(row: any, columns: string[]): boolean {\n    return columns.every((field) => {\n      const value = row?.[field];\n      if (value == null) return true;\n      if (typeof value === 'string') return value.trim() === '';\n      return false;\n    });\n  }\n\n  /**\n   * Detect whether a string looks like a date.\n   */\n  function isDateLikeString(value: string): boolean {\n    const v = value.trim();\n    if (!v) return false;\n\n    const isoLike = /^\\d{4}-\\d{1,2}-\\d{1,2}(?:[T\\s].*)?$/;\n    const slashLike = /^\\d{1,2}[/-]\\d{1,2}[/-]\\d{2,4}$/;\n\n    if (!isoLike.test(v) && !slashLike.test(v)) return false;\n\n    const d = new Date(v);\n    return !Number.isNaN(d.getTime());\n  }\n\n  /**\n   * Format date as DD/MM/YYYY.\n   */\n  function formatDate(value: any): string {\n    const date = new Date(value);\n    if (Number.isNaN(date.getTime())) return '';\n\n    const dd = String(date.getDate()).padStart(2, '0');\n    const mm = String(date.getMonth() + 1).padStart(2, '0');\n    const yyyy = String(date.getFullYear());\n    return `${dd}/${mm}/${yyyy}`;\n  }\n\n  /**\n   * Format numeric values with thousand separators.\n   */\n  function formatNumber(value: number): string {\n    return value.toLocaleString('en-US');\n  }\n\n  /**\n   * Convert any cell value into safe display text.\n   */\n  function formatCellValue(value: any): string {\n    if (value == null) return '';\n\n    if (typeof value === 'number') {\n      return Number.isFinite(value) ? formatNumber(value) : '';\n    }\n\n    if (typeof value === 'string') {\n      const trimmed = value.trim();\n      if (!trimmed) return '';\n\n      if (isDateLikeString(trimmed)) {\n        const formattedDate = formatDate(trimmed);\n        if (formattedDate) return formattedDate;\n      }\n\n      const normalizedNumeric = trimmed.replace(/,/g, '');\n      if (/^-?\\d+(\\.\\d+)?$/.test(normalizedNumeric)) {\n        const num = Number(normalizedNumeric);\n        if (Number.isFinite(num)) return formatNumber(num);\n      }\n\n      return trimmed;\n    }\n\n    if (typeof value === 'boolean') return value ? 'true' : 'false';\n\n    try {\n      return JSON.stringify(value);\n    } catch {\n      return String(value);\n    }\n  }\n\n  /**\n   * Render email-compatible HTML table using inline CSS only.\n   */\n  function renderTable(columns: string[], rows: any[]): string {\n    const tableStyle = 'width:100%;border-collapse:collapse;font-family:Arial, Helvetica, sans-serif;font-size:14px;color:#374151;border:1px solid #e5e7eb;';\n    const thStyle = 'padding:12px 16px;background:#f8fafc;font-weight:600;border-bottom:1px solid #d1d5db;text-align:left;';\n    const tdStyle = 'padding:10px 16px;border-bottom:1px solid #e5e7eb;vertical-align:middle;';\n\n    if (!columns.length) {\n      return `<table role=\"presentation\" cellpadding=\"0\" cellspacing=\"0\" border=\"0\" style=\"${tableStyle}\"><tbody><tr><td style=\"${tdStyle}\"></td></tr></tbody></table>`;\n    }\n\n    const validRows = rows.filter((row) => row && typeof row === 'object' && !Array.isArray(row) && !isBusinessRowEmpty(row, columns));\n\n    const headerHtml = columns\n      .map((col) => `<th style=\"${thStyle}\"><strong>${escapeHtml(col)}</strong></th>`)\n      .join('');\n\n    const bodyHtml = validRows\n      .map((row, index) => {\n        const rowStyle = (index + 1) % 2 === 0 ? ' style=\"background:#fafafa;\"' : '';\n        const cells = columns\n          .map((col) => {\n            const display = formatCellValue(row?.[col]);\n            const safeDisplay = escapeHtml(display).replace(/\\r\\n|\\r|\\n/g, '<br>');\n            return `<td style=\"${tdStyle}\">${safeDisplay}</td>`;\n          })\n          .join('');\n        return `<tr${rowStyle}>${cells}</tr>`;\n      })\n      .join('');\n\n    const emptyRowHtml = `<tr>${columns.map(() => `<td style=\"${tdStyle}\"></td>`).join('')}</tr>`;\n\n    return [\n      `<table role=\"presentation\" cellpadding=\"0\" cellspacing=\"0\" border=\"0\" style=\"${tableStyle}\">`,\n      `<thead><tr>${headerHtml}</tr></thead>`,\n      `<tbody>${bodyHtml || emptyRowHtml}</tbody>`,\n      `</table>`,\n    ].join('');\n  }\n\n  // Read input variable: subListJson (array or JSON string)\n  const normalizedRows = normalizeRows(params?.subListJson);\n\n  // Detect dynamic business columns from input rows\n  const businessColumns = detectBusinessColumns(normalizedRows);\n\n  // Generate final HTML table and assign to output variable: htmlTable\n  const htmlTable = renderTable(businessColumns, normalizedRows);\n\n  return { htmlTable };\n}","DraftConfig":"{\"params\":[{\"id\":\"subListJson\",\"type\":\"text\",\"desc\":\"The JSON array data from the Yeeflow Sub List control. Each object represents one row in the Sub List.\"}],\"connections\":[],\"outputs\":[{\"id\":\"htmlTable\",\"type\":\"text\",\"desc\":\"The generated email-compatible HTML table with inline CSS styles, converted from the Yeeflow Sub List JSON data.\"}]}","ExtData":null}