@svgrid/enterprise 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +97 -0
- package/README.md +37 -0
- package/package.json +72 -0
- package/src/ai.test.ts +522 -0
- package/src/ai.ts +782 -0
- package/src/export.ts +512 -0
- package/src/import.test.ts +415 -0
- package/src/import.ts +648 -0
- package/src/index.ts +88 -0
- package/src/install.ts +114 -0
- package/src/license.ts +90 -0
- package/src/pdfmake-shims.d.ts +22 -0
- package/src/pivot.test.ts +308 -0
- package/src/pivot.ts +549 -0
- package/src/print.ts +127 -0
- package/src/revoked.ts +49 -0
- package/src/smart-shim.ts +105 -0
- package/src/smart.export.d.ts +5 -0
- package/src/smart.export.js +7 -0
- package/src/staged-editing.ts +0 -0
- package/src/upgrade-prompt.test.ts +71 -0
- package/src/upgrade-prompt.ts +148 -0
- package/src/watermark.test.ts +97 -0
- package/src/watermark.ts +156 -0
package/src/export.ts
ADDED
|
@@ -0,0 +1,512 @@
|
|
|
1
|
+
/// <reference path="./pdfmake-shims.d.ts" />
|
|
2
|
+
import type { RowData, TableFeatures } from '@svgrid/grid'
|
|
3
|
+
import type { SvGridApi } from '@svgrid/grid'
|
|
4
|
+
import { assertProLicensed } from './license'
|
|
5
|
+
import { installSmartShim, type SmartDataExporterInstance } from './smart-shim'
|
|
6
|
+
|
|
7
|
+
export type ExportFormat = 'xlsx' | 'pdf' | 'csv' | 'tsv' | 'html'
|
|
8
|
+
|
|
9
|
+
export type ExportColumn = {
|
|
10
|
+
/** Data field name on the row object. */
|
|
11
|
+
field: string
|
|
12
|
+
/** Header label to render in the exported file. Defaults to `field`. */
|
|
13
|
+
header?: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Per-cell style descriptor. Mirrors a subset of CSS; the keys the
|
|
18
|
+
* underlying exporter honours are font / colour / background / border /
|
|
19
|
+
* alignment. Anything else is ignored gracefully.
|
|
20
|
+
*/
|
|
21
|
+
export type ExportCellStyle = {
|
|
22
|
+
color?: string
|
|
23
|
+
backgroundColor?: string
|
|
24
|
+
fontWeight?: 'normal' | 'bold' | number
|
|
25
|
+
fontStyle?: 'normal' | 'italic'
|
|
26
|
+
fontSize?: number | string
|
|
27
|
+
fontFamily?: string
|
|
28
|
+
border?: string
|
|
29
|
+
textAlign?: 'left' | 'right' | 'center'
|
|
30
|
+
verticalAlign?: 'top' | 'middle' | 'bottom'
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Document-level style. Apply blanket styles to the header row, the
|
|
35
|
+
* value rows, or selectively per cell-reference (e.g. `'B2'`).
|
|
36
|
+
*/
|
|
37
|
+
export type ExportStyles = {
|
|
38
|
+
/** Style applied to every header cell. */
|
|
39
|
+
headerRow?: ExportCellStyle
|
|
40
|
+
/** Style applied to every data row. Even / odd zebra are derived from this if `rowAlternate` is set. */
|
|
41
|
+
rows?: ExportCellStyle
|
|
42
|
+
/** Optional zebra background for odd-indexed rows. */
|
|
43
|
+
rowAlternate?: ExportCellStyle
|
|
44
|
+
/** Per-cell overrides keyed by Excel-style reference (`'A1'`, `'C3'`). */
|
|
45
|
+
cells?: Record<string, ExportCellStyle>
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Header / footer entries for xlsx + pdf. Each line is rendered top to
|
|
50
|
+
* bottom on the page. Embed an image with `{ image: dataUrl }`; embed
|
|
51
|
+
* text with `{ text: '...', style?: ExportCellStyle }`.
|
|
52
|
+
*/
|
|
53
|
+
export type ExportHeaderFooterLine =
|
|
54
|
+
| { text: string; style?: ExportCellStyle }
|
|
55
|
+
| { image: string; width?: number; height?: number }
|
|
56
|
+
| { left?: string; center?: string; right?: string }
|
|
57
|
+
|
|
58
|
+
export type ExportSheet<TData> = {
|
|
59
|
+
/** Sheet/tab label. Required. */
|
|
60
|
+
label: string
|
|
61
|
+
/** Rows for this sheet. */
|
|
62
|
+
rows: ReadonlyArray<TData>
|
|
63
|
+
/** Per-sheet columns. Falls back to the top-level `columns` if omitted. */
|
|
64
|
+
columns?: ReadonlyArray<ExportColumn>
|
|
65
|
+
/** Per-sheet styles. */
|
|
66
|
+
styles?: ExportStyles
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export type ExportOptions<TData> = {
|
|
70
|
+
format: ExportFormat
|
|
71
|
+
/** Base filename (extension is appended if missing). Defaults to "grid". */
|
|
72
|
+
filename?: string
|
|
73
|
+
/**
|
|
74
|
+
* Columns to include. If omitted, every key of the first row is exported
|
|
75
|
+
* (in original object order) using the field name as the header label.
|
|
76
|
+
*/
|
|
77
|
+
columns?: ReadonlyArray<ExportColumn>
|
|
78
|
+
/**
|
|
79
|
+
* Source rows. If omitted, the current displayed rows from the api are used.
|
|
80
|
+
* Provide explicitly when you want to override (e.g. export the whole
|
|
81
|
+
* dataset rather than the filtered view).
|
|
82
|
+
*/
|
|
83
|
+
rows?: ReadonlyArray<TData>
|
|
84
|
+
/** PDF only. Page orientation. Defaults to "portrait". */
|
|
85
|
+
pageOrientation?: 'portrait' | 'landscape'
|
|
86
|
+
/**
|
|
87
|
+
* Cell + row styles. Apply once to match a light/dark theme, or per-cell
|
|
88
|
+
* for conditional formatting. xlsx and pdf honour these; csv/tsv ignore
|
|
89
|
+
* them; html bakes them into inline `style=` attributes.
|
|
90
|
+
*/
|
|
91
|
+
styles?: ExportStyles
|
|
92
|
+
/** Page header lines (xlsx, pdf, html). Logos go here as `{ image: ... }`. */
|
|
93
|
+
header?: ReadonlyArray<ExportHeaderFooterLine>
|
|
94
|
+
/** Page footer lines. Common pattern: page number on the right. */
|
|
95
|
+
footer?: ReadonlyArray<ExportHeaderFooterLine>
|
|
96
|
+
/**
|
|
97
|
+
* Multi-sheet export (xlsx only). When set, each entry becomes one
|
|
98
|
+
* sheet/tab. The top-level `rows` / `columns` are ignored.
|
|
99
|
+
*/
|
|
100
|
+
sheets?: ReadonlyArray<ExportSheet<TData>>
|
|
101
|
+
/**
|
|
102
|
+
* If a row has a column whose value matches one of these field names
|
|
103
|
+
* AND the value looks like a URL or data URL, the cell is exported as
|
|
104
|
+
* an embedded image (xlsx). Defaults to `[]` (no auto-detection).
|
|
105
|
+
*/
|
|
106
|
+
imageFields?: ReadonlyArray<string>
|
|
107
|
+
/**
|
|
108
|
+
* Pixel dimensions used when embedding images. Smart's xlsx writer
|
|
109
|
+
* draws the image at this size relative to the cell origin. Defaults
|
|
110
|
+
* to `{ width: 32, height: 32 }`. Set larger for thumbnails, smaller
|
|
111
|
+
* for inline icons.
|
|
112
|
+
*/
|
|
113
|
+
imageSize?: { width: number; height: number }
|
|
114
|
+
/**
|
|
115
|
+
* Group flat rows by one or more field names. The exporter wraps each
|
|
116
|
+
* group in an Excel outline row (with the +/- expand button), and
|
|
117
|
+
* emits a `<value> <field>` group header above every cluster. Maps
|
|
118
|
+
* straight to Smart DataExporter's constructor `groupBy` arg. xlsx
|
|
119
|
+
* only; csv/tsv/html flatten the groups back out.
|
|
120
|
+
*/
|
|
121
|
+
groupBy?: ReadonlyArray<string>
|
|
122
|
+
/**
|
|
123
|
+
* Mark rows as a hierarchical (tree) data source. Each row should
|
|
124
|
+
* declare its own children either via the `subRows` convention or by
|
|
125
|
+
* matching the parent/child shape Smart expects on the input rows.
|
|
126
|
+
* Mutually exclusive with `groupBy`. xlsx only.
|
|
127
|
+
*/
|
|
128
|
+
hierarchical?: boolean
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
let exporterCtorPromise: Promise<
|
|
132
|
+
new (
|
|
133
|
+
options: Record<string, unknown>,
|
|
134
|
+
groupBy?: ReadonlyArray<string>,
|
|
135
|
+
filterBy?: Record<string, unknown>,
|
|
136
|
+
conditionalFormatting?: unknown,
|
|
137
|
+
) => SmartDataExporterInstance
|
|
138
|
+
> | null = null
|
|
139
|
+
|
|
140
|
+
async function getDataExporter() {
|
|
141
|
+
if (typeof window === 'undefined') {
|
|
142
|
+
throw new Error('@svgrid/enterprise: export requires a browser environment')
|
|
143
|
+
}
|
|
144
|
+
if (!exporterCtorPromise) {
|
|
145
|
+
exporterCtorPromise = (async () => {
|
|
146
|
+
installSmartShim()
|
|
147
|
+
// Side-effect import: the IIFE registers Smart.Utilities.DataExporter
|
|
148
|
+
// on the global Smart namespace that installSmartShim() set up.
|
|
149
|
+
await import('./smart.export.js')
|
|
150
|
+
const Ctor = window.Smart?.Utilities?.DataExporter
|
|
151
|
+
if (!Ctor) {
|
|
152
|
+
throw new Error('@svgrid/enterprise: failed to load Smart.Utilities.DataExporter')
|
|
153
|
+
}
|
|
154
|
+
return Ctor
|
|
155
|
+
})()
|
|
156
|
+
}
|
|
157
|
+
return exporterCtorPromise
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async function ensureGlobals(format: ExportFormat): Promise<void> {
|
|
161
|
+
const g = globalThis as unknown as { JSZip?: unknown; pdfMake?: unknown }
|
|
162
|
+
if (format === 'xlsx' && g.JSZip == null) {
|
|
163
|
+
let mod: unknown
|
|
164
|
+
try {
|
|
165
|
+
mod = await import('jszip')
|
|
166
|
+
} catch {
|
|
167
|
+
throw new Error(
|
|
168
|
+
'@svgrid/enterprise: xlsx export requires the "jszip" peer dependency. ' +
|
|
169
|
+
'Install it with: pnpm add jszip',
|
|
170
|
+
)
|
|
171
|
+
}
|
|
172
|
+
g.JSZip = (mod as { default?: unknown }).default ?? mod
|
|
173
|
+
}
|
|
174
|
+
if (format === 'pdf' && g.pdfMake == null) {
|
|
175
|
+
let pdfMakeMod: unknown
|
|
176
|
+
let vfsMod: unknown
|
|
177
|
+
try {
|
|
178
|
+
pdfMakeMod = await import('pdfmake/build/pdfmake')
|
|
179
|
+
vfsMod = await import('pdfmake/build/vfs_fonts')
|
|
180
|
+
} catch {
|
|
181
|
+
throw new Error(
|
|
182
|
+
'@svgrid/enterprise: pdf export requires the "pdfmake" peer dependency. ' +
|
|
183
|
+
'Install it with: pnpm add pdfmake',
|
|
184
|
+
)
|
|
185
|
+
}
|
|
186
|
+
const pdfMake = ((pdfMakeMod as { default?: unknown }).default ?? pdfMakeMod) as {
|
|
187
|
+
vfs?: Record<string, string>
|
|
188
|
+
createPdf: (def: unknown) => { download(name: string): void; getBlob(cb: (b: Blob) => void): void }
|
|
189
|
+
}
|
|
190
|
+
const vfsRoot = (vfsMod as { default?: unknown }).default ?? vfsMod
|
|
191
|
+
// pdfmake's vfs_fonts file historically exports either { pdfMake: { vfs } }
|
|
192
|
+
// or { default: { vfs } } or { vfs } depending on bundler. Try each shape.
|
|
193
|
+
const candidate =
|
|
194
|
+
(vfsRoot as { pdfMake?: { vfs?: Record<string, string> } }).pdfMake?.vfs ??
|
|
195
|
+
(vfsRoot as { vfs?: Record<string, string> }).vfs ??
|
|
196
|
+
(vfsRoot as { default?: { vfs?: Record<string, string> } }).default?.vfs
|
|
197
|
+
if (candidate) pdfMake.vfs = candidate
|
|
198
|
+
g.pdfMake = pdfMake
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function buildExportPayload<
|
|
203
|
+
TFeatures extends TableFeatures,
|
|
204
|
+
TData extends RowData,
|
|
205
|
+
>(
|
|
206
|
+
api: SvGridApi<TFeatures, TData>,
|
|
207
|
+
opts: ExportOptions<TData>,
|
|
208
|
+
): {
|
|
209
|
+
rows: ReadonlyArray<Record<string, unknown>>
|
|
210
|
+
filename: string
|
|
211
|
+
} {
|
|
212
|
+
const sourceRows: ReadonlyArray<TData> = opts.rows ?? api.getDisplayedRows()
|
|
213
|
+
let cols: ReadonlyArray<ExportColumn>
|
|
214
|
+
if (opts.columns && opts.columns.length > 0) {
|
|
215
|
+
cols = opts.columns
|
|
216
|
+
} else {
|
|
217
|
+
// Auto-derive columns from the grid's own columnDefs. This picks up the
|
|
218
|
+
// human-readable `header` labels so the exported file shows what the
|
|
219
|
+
// user sees on-screen (e.g. "Order ID" rather than "orderId"). Only
|
|
220
|
+
// visible, field-backed columns are exported.
|
|
221
|
+
const gridCols = api.getColumns().filter((c) => c.visible && c.field)
|
|
222
|
+
if (gridCols.length > 0) {
|
|
223
|
+
cols = gridCols.map((c) => ({ field: c.field!, header: c.header }))
|
|
224
|
+
} else if (sourceRows.length > 0) {
|
|
225
|
+
cols = Object.keys(sourceRows[0] as Record<string, unknown>)
|
|
226
|
+
.filter((k) => !k.startsWith('_'))
|
|
227
|
+
.map((field) => ({ field }))
|
|
228
|
+
} else {
|
|
229
|
+
cols = []
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
const headerRow: Record<string, unknown> = {}
|
|
233
|
+
for (const c of cols) headerRow[c.field] = c.header ?? c.field
|
|
234
|
+
|
|
235
|
+
const projected: Array<Record<string, unknown>> = [headerRow]
|
|
236
|
+
for (const r of sourceRows) {
|
|
237
|
+
const row: Record<string, unknown> = {}
|
|
238
|
+
const src = r as unknown as Record<string, unknown>
|
|
239
|
+
for (const c of cols) row[c.field] = src[c.field]
|
|
240
|
+
projected.push(row)
|
|
241
|
+
}
|
|
242
|
+
const filename = (opts.filename ?? 'grid').trim() || 'grid'
|
|
243
|
+
return { rows: projected, filename }
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Translate the public {@link ExportStyles} shape into the keys the Smart
|
|
248
|
+
* exporter actually consumes:
|
|
249
|
+
* `style.header` → all header cells
|
|
250
|
+
* `style.rows` → all data rows. Cell-prop keys (color, backgroundColor,
|
|
251
|
+
* fontWeight, fontStyle, fontSize, fontFamily,
|
|
252
|
+
* textAlign, verticalAlign, textDecoration, numFmt)
|
|
253
|
+
* are recognised by Smart's `storeCellStyle`.
|
|
254
|
+
* `style.rows.alternationCount` + `alternationStart` +
|
|
255
|
+
* `alternationIndex1BackgroundColor` (etc.) → zebra striping
|
|
256
|
+
* `style.cells.<ref>` → HTML-only per-cell ref overrides
|
|
257
|
+
*/
|
|
258
|
+
function translateStyles(styles: ExportStyles | undefined): Record<string, unknown> | undefined {
|
|
259
|
+
if (!styles) return undefined
|
|
260
|
+
const out: Record<string, unknown> = {}
|
|
261
|
+
if (styles.headerRow) out.header = { ...styles.headerRow }
|
|
262
|
+
const rows: Record<string, unknown> = styles.rows ? { ...styles.rows } : {}
|
|
263
|
+
if (styles.rowAlternate) {
|
|
264
|
+
rows.alternationCount = 2
|
|
265
|
+
rows.alternationStart = 0
|
|
266
|
+
if (styles.rowAlternate.backgroundColor)
|
|
267
|
+
rows.alternationIndex1BackgroundColor = styles.rowAlternate.backgroundColor
|
|
268
|
+
if (styles.rowAlternate.color)
|
|
269
|
+
rows.alternationIndex1Color = styles.rowAlternate.color
|
|
270
|
+
}
|
|
271
|
+
if (Object.keys(rows).length > 0) out.rows = rows
|
|
272
|
+
if (styles.cells) out.cells = styles.cells
|
|
273
|
+
return out
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
let _imgIdCounter = 0
|
|
277
|
+
const _imgIdCache = new Map<string, string>()
|
|
278
|
+
function imageIdFor(dataUrl: string): string {
|
|
279
|
+
const cached = _imgIdCache.get(dataUrl)
|
|
280
|
+
if (cached) return cached
|
|
281
|
+
const id = `img${++_imgIdCounter}`
|
|
282
|
+
_imgIdCache.set(dataUrl, id)
|
|
283
|
+
return id
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Build the image object Smart's xlsx writer expects. The exporter pulls
|
|
288
|
+
* `{ id, base64, imageType, width, height }` off the return of
|
|
289
|
+
* `addImageToCell(...).image` and stores it under `this.images` →
|
|
290
|
+
* eventually written to the workbook as `xl/media/imageN.<imageType>`.
|
|
291
|
+
*
|
|
292
|
+
* `base64` may be a full data URL - Smart strips the prefix when writing.
|
|
293
|
+
*/
|
|
294
|
+
function makeSmartImage(dataUrl: string, width = 64, height = 64): {
|
|
295
|
+
id: string; base64: string; imageType: string; width: number; height: number
|
|
296
|
+
} {
|
|
297
|
+
// Extract MIME → imageType. Defaults to png for safety.
|
|
298
|
+
const mime = /^data:image\/([a-zA-Z0-9+]+);/i.exec(dataUrl)?.[1]?.toLowerCase() ?? 'png'
|
|
299
|
+
const imageType = mime === 'jpg' ? 'jpeg'
|
|
300
|
+
: mime === 'svg+xml' ? 'svg'
|
|
301
|
+
: mime
|
|
302
|
+
return {
|
|
303
|
+
id: imageIdFor(dataUrl),
|
|
304
|
+
base64: dataUrl,
|
|
305
|
+
imageType,
|
|
306
|
+
width,
|
|
307
|
+
height,
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Build the header / footer arrays the Smart exporter understands.
|
|
313
|
+
* Each input line becomes one full-width row in the spreadsheet with
|
|
314
|
+
* `style.mergeAcross` so the line spans every column.
|
|
315
|
+
*
|
|
316
|
+
* Image lines are written as a data-URL string into the first column and
|
|
317
|
+
* the wrapper's `addImageToCell` hook embeds them as real picture cells.
|
|
318
|
+
*/
|
|
319
|
+
function buildContentRows(
|
|
320
|
+
lines: ReadonlyArray<ExportHeaderFooterLine> | undefined,
|
|
321
|
+
datafields: ReadonlyArray<string>,
|
|
322
|
+
): Array<{ cells: Record<string, unknown>; style?: Record<string, unknown> }> | undefined {
|
|
323
|
+
if (!lines || lines.length === 0 || datafields.length === 0) return undefined
|
|
324
|
+
const first = datafields[0]!
|
|
325
|
+
const out: Array<{ cells: Record<string, unknown>; style?: Record<string, unknown> }> = []
|
|
326
|
+
for (const line of lines) {
|
|
327
|
+
const cells: Record<string, unknown> = {}
|
|
328
|
+
let style: Record<string, unknown> | undefined
|
|
329
|
+
if ('image' in line) {
|
|
330
|
+
cells[first] = line.image
|
|
331
|
+
style = { mergeAcross: true, textAlign: 'left' }
|
|
332
|
+
} else if ('text' in line) {
|
|
333
|
+
cells[first] = line.text
|
|
334
|
+
style = { mergeAcross: true, ...(line.style ?? {}) }
|
|
335
|
+
} else {
|
|
336
|
+
// 3-column { left, center, right }. Distribute across the first,
|
|
337
|
+
// middle, and last datafield.
|
|
338
|
+
const mid = datafields[Math.floor(datafields.length / 2)]!
|
|
339
|
+
const last = datafields[datafields.length - 1]!
|
|
340
|
+
if (line.left !== undefined) cells[first] = line.left
|
|
341
|
+
if (line.center !== undefined) cells[mid] = line.center
|
|
342
|
+
if (line.right !== undefined) cells[last] = line.right
|
|
343
|
+
style = { textAlign: 'left' }
|
|
344
|
+
}
|
|
345
|
+
out.push({ cells, style })
|
|
346
|
+
}
|
|
347
|
+
return out
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Build an `addImageToCell` handler that embeds a value as a real image
|
|
352
|
+
* cell whenever the value is a data URL. Smart calls this for every body
|
|
353
|
+
* cell AND every header/footer cell, so the same handler covers both
|
|
354
|
+
* code paths.
|
|
355
|
+
*
|
|
356
|
+
* `bodyImageSize` controls the embedded pixel size for body cells (the
|
|
357
|
+
* thumbnails). Header/footer image lines use their own size hint
|
|
358
|
+
* already encoded in the `cells[first]` value's metadata - here we just
|
|
359
|
+
* embed with a slightly larger default so logos read well in the page
|
|
360
|
+
* banner.
|
|
361
|
+
*/
|
|
362
|
+
function buildImageHandler(
|
|
363
|
+
imageFields: ReadonlyArray<string> | undefined,
|
|
364
|
+
bodyImageSize: { width: number; height: number },
|
|
365
|
+
): NonNullable<SmartDataExporterInstance['addImageToCell']> {
|
|
366
|
+
const allowed = imageFields && imageFields.length > 0 ? new Set(imageFields) : null
|
|
367
|
+
return (_rowIndex, dataField, value) => {
|
|
368
|
+
if (typeof value !== 'string') return null
|
|
369
|
+
if (!/^data:image\//i.test(value)) return null
|
|
370
|
+
if (allowed && !allowed.has(dataField)) {
|
|
371
|
+
// Header/footer image lines come through the first column too -
|
|
372
|
+
// those cells aren't in `imageFields`, but the wrapper wrote the
|
|
373
|
+
// data URL there itself. Embed bigger (logo banner).
|
|
374
|
+
return { image: makeSmartImage(value, 96, 96), value: '' }
|
|
375
|
+
}
|
|
376
|
+
return { image: makeSmartImage(value, bodyImageSize.width, bodyImageSize.height), value: '' }
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Build the constructor-options object passed to the Smart DataExporter.
|
|
382
|
+
* Constructor only consumes a small set of keys; instance-level options
|
|
383
|
+
* (`headerContent`, `footerContent`, `addImageToCell`) are applied
|
|
384
|
+
* separately in {@link exportGrid} after `new DataExporter(...)`.
|
|
385
|
+
*/
|
|
386
|
+
function buildExporterOptions<TData>(
|
|
387
|
+
opts: ExportOptions<TData>,
|
|
388
|
+
): Record<string, unknown> {
|
|
389
|
+
const out: Record<string, unknown> = {
|
|
390
|
+
exportHeader: true,
|
|
391
|
+
pageOrientation: opts.pageOrientation ?? 'portrait',
|
|
392
|
+
}
|
|
393
|
+
const style = translateStyles(opts.styles)
|
|
394
|
+
if (style) out.style = style
|
|
395
|
+
// Smart's xlsx writer emits Excel-native row outlining when
|
|
396
|
+
// `hierarchical: true` is set on the constructor options OR when the
|
|
397
|
+
// second constructor arg is a non-empty groupBy array. We thread
|
|
398
|
+
// hierarchical through here and groupBy through the constructor call
|
|
399
|
+
// site in exportGrid.
|
|
400
|
+
if (opts.hierarchical) out.hierarchical = true
|
|
401
|
+
return out
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Apply instance-level options the constructor does not consume.
|
|
406
|
+
* Mutates and returns the exporter for fluent use.
|
|
407
|
+
*/
|
|
408
|
+
function applyInstanceOptions<TData>(
|
|
409
|
+
exporter: SmartDataExporterInstance,
|
|
410
|
+
opts: ExportOptions<TData>,
|
|
411
|
+
datafields: ReadonlyArray<string>,
|
|
412
|
+
): SmartDataExporterInstance {
|
|
413
|
+
const headerRows = buildContentRows(opts.header, datafields)
|
|
414
|
+
const footerRows = buildContentRows(opts.footer, datafields)
|
|
415
|
+
if (headerRows) exporter.headerContent = headerRows
|
|
416
|
+
if (footerRows) exporter.footerContent = footerRows
|
|
417
|
+
|
|
418
|
+
// Wire image embedding when the caller opts in via imageFields OR the
|
|
419
|
+
// header/footer carries an image line. Both code paths funnel through
|
|
420
|
+
// the same handler.
|
|
421
|
+
const hasHeaderOrFooterImage =
|
|
422
|
+
(opts.header?.some((l) => 'image' in l) ?? false) ||
|
|
423
|
+
(opts.footer?.some((l) => 'image' in l) ?? false)
|
|
424
|
+
if (opts.imageFields?.length || hasHeaderOrFooterImage) {
|
|
425
|
+
const bodySize = opts.imageSize ?? { width: 32, height: 32 }
|
|
426
|
+
exporter.addImageToCell = buildImageHandler(opts.imageFields, bodySize)
|
|
427
|
+
}
|
|
428
|
+
return exporter
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
export async function exportGrid<
|
|
432
|
+
TFeatures extends TableFeatures,
|
|
433
|
+
TData extends RowData,
|
|
434
|
+
>(api: SvGridApi<TFeatures, TData>, opts: ExportOptions<TData>): Promise<void> {
|
|
435
|
+
assertProLicensed('Export')
|
|
436
|
+
await ensureGlobals(opts.format)
|
|
437
|
+
const Ctor = await getDataExporter()
|
|
438
|
+
|
|
439
|
+
// Multi-sheet path. Smart's `spreadsheets` option takes an array of
|
|
440
|
+
// `{ label, dataSource, columns, dataFields? }`. Each entry becomes one
|
|
441
|
+
// tab; the exporter prepends its own header row built from `columns`.
|
|
442
|
+
if (opts.sheets && opts.sheets.length > 0) {
|
|
443
|
+
if (opts.format !== 'xlsx') {
|
|
444
|
+
throw new Error(`@svgrid/enterprise: multi-sheet export requires format 'xlsx', got '${opts.format}'`)
|
|
445
|
+
}
|
|
446
|
+
// Same column-derivation fallback used by the single-sheet path: pull
|
|
447
|
+
// the human-readable headers from the grid when the caller didn't pass
|
|
448
|
+
// explicit columns.
|
|
449
|
+
const gridCols = api.getColumns().filter((c) => c.visible && c.field)
|
|
450
|
+
const fallbackCols: ReadonlyArray<ExportColumn> =
|
|
451
|
+
gridCols.length > 0
|
|
452
|
+
? gridCols.map((c) => ({ field: c.field!, header: c.header }))
|
|
453
|
+
: []
|
|
454
|
+
const sheets = opts.sheets.map((sheet) => {
|
|
455
|
+
const cols = (sheet.columns ?? opts.columns ?? (
|
|
456
|
+
fallbackCols.length > 0
|
|
457
|
+
? fallbackCols
|
|
458
|
+
: sheet.rows.length > 0
|
|
459
|
+
? Object.keys(sheet.rows[0] as Record<string, unknown>)
|
|
460
|
+
.filter((k) => !k.startsWith('_'))
|
|
461
|
+
.map((field) => ({ field } as ExportColumn))
|
|
462
|
+
: []
|
|
463
|
+
))
|
|
464
|
+
// Project each row to only the included fields (in column order).
|
|
465
|
+
const dataSource = sheet.rows.map((r) => {
|
|
466
|
+
const src = r as unknown as Record<string, unknown>
|
|
467
|
+
const out: Record<string, unknown> = {}
|
|
468
|
+
for (const c of cols) out[c.field] = src[c.field]
|
|
469
|
+
return out
|
|
470
|
+
})
|
|
471
|
+
return {
|
|
472
|
+
label: sheet.label,
|
|
473
|
+
dataSource,
|
|
474
|
+
columns: cols.map((c) => ({ dataField: c.field, label: c.header ?? c.field })),
|
|
475
|
+
dataFields: cols.map((c) => c.field),
|
|
476
|
+
style: translateStyles(sheet.styles ?? opts.styles),
|
|
477
|
+
}
|
|
478
|
+
})
|
|
479
|
+
const filename = (opts.filename ?? 'grid').trim() || 'grid'
|
|
480
|
+
const ctorOpts = buildExporterOptions(opts)
|
|
481
|
+
// Smart needs at least one main sheet to operate. Use the first sheet
|
|
482
|
+
// as the "main" exportData payload AND keep the full set on the
|
|
483
|
+
// `spreadsheets` property so the workbook ends up with one tab per
|
|
484
|
+
// entry.
|
|
485
|
+
const firstSheet = sheets[0]!
|
|
486
|
+
const exporter = new Ctor(ctorOpts)
|
|
487
|
+
exporter.spreadsheets = sheets
|
|
488
|
+
applyInstanceOptions(exporter, opts, firstSheet.dataFields)
|
|
489
|
+
// First sheet's data goes through the standard exportData path with
|
|
490
|
+
// its own header row.
|
|
491
|
+
const headerRow: Record<string, unknown> = {}
|
|
492
|
+
for (const c of firstSheet.columns) headerRow[c.dataField] = c.label
|
|
493
|
+
const firstSheetRows = [headerRow, ...firstSheet.dataSource]
|
|
494
|
+
exporter.exportData(firstSheetRows, opts.format, filename)
|
|
495
|
+
return
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
const { rows, filename } = buildExportPayload(api, opts)
|
|
499
|
+
if (rows.length <= 1) {
|
|
500
|
+
throw new Error('@svgrid/enterprise: nothing to export - the grid has no rows')
|
|
501
|
+
}
|
|
502
|
+
const datafields = Object.keys(rows[0]!)
|
|
503
|
+
// The constructor's SECOND arg is `groupBy`: when non-empty AND
|
|
504
|
+
// `hierarchical` is NOT set, Smart wraps each group of rows in an
|
|
505
|
+
// Excel outline row with an expand/collapse button at the group key.
|
|
506
|
+
const groupBy = !opts.hierarchical && opts.groupBy && opts.groupBy.length > 0
|
|
507
|
+
? opts.groupBy
|
|
508
|
+
: undefined
|
|
509
|
+
const exporter = new Ctor(buildExporterOptions(opts), groupBy)
|
|
510
|
+
applyInstanceOptions(exporter, opts, datafields)
|
|
511
|
+
exporter.exportData(rows, opts.format, filename)
|
|
512
|
+
}
|