Excel Export: Builds .xlsx workbooks from supplied tables or grid panel data and writes/opens them in the plugin's exports folder.
| 1 | import type { BackendContext } from '@jaspers-ai/sdk' |
| 2 | import type { TableInput } from './save' |
| 3 | |
| 4 | type Rec = Record<string, unknown> |
| 5 | |
| 6 | async function tryGet(ctx: BackendContext, path: string): Promise<unknown> { |
| 7 | try { |
| 8 | return await ctx.state.get(path) |
| 9 | } catch { |
| 10 | return null |
| 11 | } |
| 12 | } |
| 13 | |
| 14 | const isRec = (v: unknown): v is Rec => !!v && typeof v === 'object' && !Array.isArray(v) |
| 15 | |
| 16 | function columnName(c: unknown): string { |
| 17 | if (typeof c === 'string') return c |
| 18 | if (isRec(c)) return String(c.label ?? c.title ?? c.header ?? c.name ?? c.key ?? c.field ?? '') |
| 19 | return String(c) |
| 20 | } |
| 21 | |
| 22 | function columnKey(c: unknown): string { |
| 23 | if (isRec(c)) return String(c.key ?? c.field ?? c.name ?? c.id ?? columnName(c)) |
| 24 | return String(c) |
| 25 | } |
| 26 | |
| 27 | /** A table from an object with rows under a usual key, and columns if it names them. */ |
| 28 | function fromObject(o: unknown): { columns: string[]; rows: unknown[] } | null { |
| 29 | if (!isRec(o)) return null |
| 30 | for (const key of ['rows', 'data', 'items', 'results', 'records', 'table']) { |
| 31 | const v = o[key] |
| 32 | if (isRec(v)) { |
| 33 | const inner = fromObject(v) |
| 34 | if (inner) return { columns: inner.columns, rows: inner.rows } |
| 35 | } |
| 36 | if (!Array.isArray(v) || v.length === 0) continue |
| 37 | const cols = (o.columns ?? o.headers ?? o.fields) as unknown[] | undefined |
| 38 | if (Array.isArray(v[0])) { |
| 39 | const columns = Array.isArray(cols) ? cols.map(columnName) : v[0].map((_, i) => `Column ${i + 1}`) |
| 40 | return { columns, rows: v as unknown[] } |
| 41 | } |
| 42 | if (isRec(v[0])) { |
| 43 | const keys = Array.isArray(cols) ? cols.map(columnKey) : Object.keys(v[0]) |
| 44 | const names = Array.isArray(cols) ? cols.map(columnName) : keys |
| 45 | return { columns: names, rows: (v as Rec[]).map((r) => keys.map((k) => r[k])) } |
| 46 | } |
| 47 | } |
| 48 | return null |
| 49 | } |
| 50 | |
| 51 | /** A table from text: a Markdown pipe table, or tab-separated lines. */ |
| 52 | function fromText(text: unknown): { columns: string[]; rows: unknown[] } | null { |
| 53 | if (typeof text !== 'string') return null |
| 54 | const lines = text.split(/\r?\n/) |
| 55 | const pipe = lines.filter((l) => /^\s*\|.*\|\s*$/.test(l)) |
| 56 | if (pipe.length >= 2) { |
| 57 | const cells = (l: string) => l.trim().replace(/^\||\|$/g, '').split('|').map((c) => c.trim()) |
| 58 | const body = pipe.filter((l) => !/^\s*\|[\s:|-]+\|\s*$/.test(l)) |
| 59 | if (body.length >= 1) return { columns: cells(body[0]!), rows: body.slice(1).map(cells) } |
| 60 | } |
| 61 | const tsv = lines.filter((l) => l.includes('\t')) |
| 62 | if (tsv.length >= 2) return { columns: tsv[0]!.split('\t'), rows: tsv.slice(1).map((l) => l.split('\t')) } |
| 63 | return null |
| 64 | } |
| 65 | |
| 66 | /** A panel's table: its rows from the panel's output, text, or state, with a title. */ |
| 67 | export async function readPanelTable(ctx: BackendContext, id: string, title?: string): Promise<TableInput> { |
| 68 | const panel = await tryGet(ctx, `panels/${id}`) |
| 69 | const state = (await tryGet(ctx, `panels/${id}/state`)) ?? (isRec(panel) ? panel.state : null) |
| 70 | const output = (await tryGet(ctx, `panels/${id}/output`)) ?? (isRec(panel) ? panel.output : null) |
| 71 | const text = (await tryGet(ctx, `panels/${id}/text`)) ?? (isRec(panel) ? panel.text : null) |
| 72 | if (panel === null && state === null && output === null) throw new Error(`No panel ${id} on the grid.`) |
| 73 | const table = fromObject(output) ?? fromObject(state) ?? fromText(text) ?? fromObject(panel) |
| 74 | const named = |
| 75 | title ?? |
| 76 | [state, output, panel].map((o) => (isRec(o) ? (o.title ?? o.name) : undefined)).find((t) => typeof t === 'string') ?? |
| 77 | `Panel ${id}` |
| 78 | if (!table) { |
| 79 | const s = isRec(state) ? state : {} |
| 80 | const how = s.sql |
| 81 | ? `its SQL (${String(s.sql).slice(0, 200)})` |
| 82 | : s.source |
| 83 | ? `source ${String(s.source)} with args ${JSON.stringify(s.args ?? {}).slice(0, 200)}` |
| 84 | : 'its source' |
| 85 | throw new Error( |
| 86 | `Panel ${id} publishes no rows this plugin can read. Run ${how}, then pass the rows to excel-export/export-tables. Panel state: ${JSON.stringify(state).slice(0, 300)}; output keys: ${isRec(output) ? Object.keys(output).join(', ') : String(output)}`, |
| 87 | ) |
| 88 | } |
| 89 | return { title: String(named), columns: table.columns, rows: table.rows } |
| 90 | } |