@svgrid/enterprise 3.1.25 → 3.1.27
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/dist/cdn/svgrid-enterprise.svelte-external.js +18048 -17567
- package/dist/designer/assets/{GridFooter-DKMfZpL4.js → GridFooter-EyLg77jG.js} +1 -1
- package/dist/designer/assets/{GridMenus-DWDghydL.js → GridMenus-BAgfpDd-.js} +3 -3
- package/dist/designer/assets/{SvDateRangeInput-DbfmXqJj.js → SvDateRangeInput-y8PkioQk.js} +1 -1
- package/dist/designer/assets/SvGrid.helpers-DHkH3mHW.js +1 -0
- package/dist/designer/assets/SvGridCellEditor-BHxH4b5h.js +4 -0
- package/dist/designer/assets/{SvGridChart-DYtGHM3F.js → SvGridChart-DzCeEdlb.js} +3 -3
- package/dist/designer/assets/{SvGridChartBuilder-BjdQrnz5.js → SvGridChartBuilder-CsXVVKvA.js} +1 -1
- package/dist/designer/assets/SvGridChartPanel-Dj3IxTS2.js +4 -0
- package/dist/designer/assets/{SvGridChartView-Bfs0bvDb.js → SvGridChartView-Dwm4hxbW.js} +1 -1
- package/dist/designer/assets/{chart-export-B6bkQ3X8.js → chart-export-Dqq_eHRq.js} +4 -4
- package/dist/designer/assets/{chart-export-pdf-BbA28-gt.js → chart-export-pdf-Gw22jkxa.js} +1 -1
- package/dist/designer/assets/column-types-Bl5R-97G.js +1 -0
- package/dist/designer/assets/csv-injection-C4a0uulO.js +1 -0
- package/dist/designer/assets/export-format-NYxEY-0Y.js +6 -0
- package/dist/designer/assets/index-BU3ieI05.css +1 -0
- package/dist/designer/assets/{index-UhFdGGXv.js → index-Cim1_diz.js} +340 -330
- package/dist/designer/assets/src-D23PPsK1.js +1 -0
- package/dist/designer/assets/src-sFRH_up7.js +60 -0
- package/dist/designer/index.html +13 -11
- package/dist/export-conditional.d.ts +2 -0
- package/dist/export-conditional.js +2 -0
- package/dist/export-pdf.d.ts +142 -7
- package/dist/export-pdf.js +368 -44
- package/dist/export.d.ts +22 -4
- package/dist/export.js +150 -53
- package/dist/import.js +14 -3
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/node/studio.js +20 -12
- package/dist/pdf-worker.d.ts +92 -0
- package/dist/pdf-worker.js +147 -0
- package/dist/sheet/ods-document.js +3 -1
- package/dist/sheet/open-file.js +4 -2
- package/dist/sheet/xlsx-document.d.ts +2 -2
- package/dist/sheet/xlsx-document.js +18 -2
- package/dist/studio/emit-schema.js +18 -10
- package/dist/version.d.ts +2 -2
- package/dist/version.js +2 -2
- package/dist/wc/{GridMenus-h7RsvcmI.js → GridMenus-BIQXB6a7.js} +1 -1
- package/dist/wc/{SvDateRangeInput-CJZohgBe.js → SvDateRangeInput-DHYuAJeY.js} +1 -0
- package/dist/wc/SvGridCellEditor-CZwhQx0a.js +584 -0
- package/dist/wc/{SvGridChart-DWRqlhCV.js → SvGridChart-DcfmVvzB.js} +3 -3
- package/dist/wc/{SvGridChartBuilder-BXW3yUR0.js → SvGridChartBuilder-1Z69mxbM.js} +2 -2
- package/dist/wc/{SvGridChartPanel-28n4tomx.js → SvGridChartPanel-BaVsa-T-.js} +5 -5
- package/dist/wc/{SvGridChartView-CbG_WrU-.js → SvGridChartView-LEC8ZNeU.js} +1 -1
- package/dist/wc/{chart-export-CeSADGfi.js → chart-export-C1KSVdMU.js} +29 -28
- package/dist/wc/{chart-export-pdf-BGko7MER.js → chart-export-pdf-CmEd365D.js} +1 -1
- package/dist/wc/csv-injection-Ce26_TOb.js +8 -0
- package/dist/wc/sv-sheet-element.js +6275 -6215
- package/dist/xlsx-limits.d.ts +51 -0
- package/dist/xlsx-limits.js +89 -0
- package/package.json +7 -3
- package/dist/designer/assets/SvGrid.helpers-BF2QRcNt.js +0 -1
- package/dist/designer/assets/SvGridCellEditor-od4VbndV.js +0 -4
- package/dist/designer/assets/SvGridChartPanel-Cm9_K7BV.js +0 -4
- package/dist/designer/assets/export-format-BCuqcexA.js +0 -6
- package/dist/designer/assets/index-CHx3F4Ij.css +0 -1
- package/dist/designer/assets/src-CITpcm6b.js +0 -1
- package/dist/designer/assets/src-fddEt37f.js +0 -58
- package/dist/wc/SvGridCellEditor-C7ios4i_.js +0 -582
package/dist/export.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { applyGroupAggregate } from '@svgrid/grid/core';
|
|
1
2
|
// Import the pure formatters from the grid's Svelte-free `/format` subpath so
|
|
2
3
|
// this module (and the enterprise unit tests) don't drag in SvGrid.svelte.
|
|
3
4
|
import { formatValueForExport, toExcelNumFmt, valueForExcel } from '@svgrid/grid/format';
|
|
@@ -6,7 +7,10 @@ import { installSmartShim } from './smart-shim.js';
|
|
|
6
7
|
import { downloadBlobFile, downloadTextFile, serializeDelimited, serializeHtml, serializeJson, serializeMarkdown, serializeXml, } from './export-serialize.js';
|
|
7
8
|
import { serializeSpreadsheetML } from './export-xls.js';
|
|
8
9
|
import { buildXlsxParts, packageXlsx } from './export-ooxml.js';
|
|
9
|
-
import { buildPdfDocDefinition, pdfBlob, readGridPdfTheme, registerPdfFonts, resolvePdfCharts } from './export-pdf.js';
|
|
10
|
+
import { buildPdfDocData, buildPdfDocDefinition, pdfBlob, PDF_ROBOTO_MISSING, readGridPdfTheme, registerPdfFont, registerPdfFonts, resolvePdfCharts } from './export-pdf.js';
|
|
11
|
+
import { renderPdfInWorker } from './pdf-worker.js';
|
|
12
|
+
/** Rows past which a PDF rendered on the page earns a warning: about a second of blocked page per thousand. */
|
|
13
|
+
const LARGE_PDF_ROWS = 2000;
|
|
10
14
|
import { buildConditionalResolver } from './export-conditional.js';
|
|
11
15
|
/** Formats served by the native, dependency-free, streaming serializers. */
|
|
12
16
|
const NATIVE_TEXT_FORMATS = [
|
|
@@ -102,6 +106,7 @@ export function resolveColumns(api, opts, sourceRows) {
|
|
|
102
106
|
header: c.header,
|
|
103
107
|
format: c.format,
|
|
104
108
|
align: c.align,
|
|
109
|
+
aggregate: c.aggregate,
|
|
105
110
|
}));
|
|
106
111
|
}
|
|
107
112
|
if (sourceRows.length > 0) {
|
|
@@ -550,27 +555,58 @@ function isSummableColumn(col) {
|
|
|
550
555
|
}
|
|
551
556
|
/**
|
|
552
557
|
* Build a grouped PDF body: a bold group header per cluster (nested for
|
|
553
|
-
* multi-level grouping), the cluster's data rows,
|
|
554
|
-
* the
|
|
558
|
+
* multi-level grouping), the cluster's data rows, a subtotal row, and a grand
|
|
559
|
+
* total at the end. A column totals with its own `aggregate` (avg, min, max,
|
|
560
|
+
* count, ...), as the grid's group rows do; without one, number and currency
|
|
561
|
+
* columns sum. Values and group names use the same formatting as the grid.
|
|
562
|
+
* Each data row carries its index in `rows`, so conditional formatting and
|
|
563
|
+
* links can find their row.
|
|
555
564
|
*/
|
|
556
|
-
export function buildGroupedPdfBody(cols, rows, groupBy) {
|
|
565
|
+
export function buildGroupedPdfBody(cols, rows, groupBy, opts = {}) {
|
|
557
566
|
const out = [];
|
|
558
|
-
const
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
567
|
+
const colOf = (field) => cols.find((c) => c.field === field);
|
|
568
|
+
const aggOf = (c) => c.aggregate ?? (isSummableColumn(c) ? 'sum' : undefined);
|
|
569
|
+
const hasTotals = cols.some((c) => aggOf(c) !== undefined);
|
|
570
|
+
// The label goes in the first column that has no total of its own; if
|
|
571
|
+
// every column has one, -1 means "don't overwrite a total with the label".
|
|
572
|
+
const labelColIdx = cols.findIndex((c) => aggOf(c) === undefined);
|
|
573
|
+
const index = new Map();
|
|
574
|
+
rows.forEach((r, i) => index.set(r, i));
|
|
575
|
+
const field = (r, f) => r[f];
|
|
576
|
+
const total = (c, subset) => {
|
|
577
|
+
const agg = aggOf(c);
|
|
578
|
+
if (agg === undefined)
|
|
579
|
+
return '';
|
|
580
|
+
// The grid's own aggregator, over the same values the column shows.
|
|
581
|
+
const v = applyGroupAggregate(agg, c.field, subset.map((r) => ({ original: r, getCellValueByColumnId: () => field(r, c.field) })));
|
|
582
|
+
if (v == null || (typeof v === 'number' && !Number.isFinite(v)))
|
|
583
|
+
return '';
|
|
584
|
+
// A count is a number of rows, not an amount: no currency or percent.
|
|
585
|
+
if (agg === 'count' || agg === 'countDistinct')
|
|
586
|
+
return String(v);
|
|
587
|
+
if (Array.isArray(v))
|
|
588
|
+
return v.map((x) => formatValueForExport(x, c.format)).join(' - ');
|
|
589
|
+
return formatValueForExport(v, c.format);
|
|
590
|
+
};
|
|
591
|
+
const totalRow = (subset, label) => {
|
|
592
|
+
const cells = cols.map((c) => total(c, subset));
|
|
593
|
+
if (labelColIdx >= 0)
|
|
594
|
+
cells[labelColIdx] = label;
|
|
595
|
+
return { kind: 'subtotal', cells };
|
|
596
|
+
};
|
|
562
597
|
const recurse = (subset, level) => {
|
|
563
598
|
if (level >= groupBy.length) {
|
|
564
599
|
for (const r of subset) {
|
|
565
|
-
out.push({ kind: 'data', cells: cols.map((c) => String(cellValueFor(c, r, false) ?? '')) });
|
|
600
|
+
out.push({ kind: 'data', cells: cols.map((c) => String(cellValueFor(c, r, false) ?? '')), source: index.get(r) });
|
|
566
601
|
}
|
|
567
602
|
return;
|
|
568
603
|
}
|
|
569
|
-
const
|
|
604
|
+
const groupField = groupBy[level];
|
|
605
|
+
const groupCol = colOf(groupField);
|
|
570
606
|
// Preserve first-seen order of group keys.
|
|
571
607
|
const buckets = new Map();
|
|
572
608
|
for (const r of subset) {
|
|
573
|
-
const key = String(r
|
|
609
|
+
const key = String(field(r, groupField) ?? '');
|
|
574
610
|
const bucket = buckets.get(key);
|
|
575
611
|
if (bucket)
|
|
576
612
|
bucket.push(r);
|
|
@@ -578,26 +614,31 @@ export function buildGroupedPdfBody(cols, rows, groupBy) {
|
|
|
578
614
|
buckets.set(key, [r]);
|
|
579
615
|
}
|
|
580
616
|
for (const [key, groupRows] of buckets) {
|
|
581
|
-
|
|
617
|
+
// The name as the grid shows it: a date group reads "Oct 3, 2026", not
|
|
618
|
+
// the timestamp it is keyed by.
|
|
619
|
+
const shown = groupCol ? String(cellValueFor(groupCol, groupRows[0], false) ?? key) : key;
|
|
620
|
+
out.push({ kind: 'group', label: `${groupCol?.header ?? groupField}: ${shown} (${groupRows.length})`, level });
|
|
582
621
|
recurse(groupRows, level + 1);
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
if (hasSummable) {
|
|
586
|
-
const cells = cols.map((c) => {
|
|
587
|
-
if (!isSummableColumn(c))
|
|
588
|
-
return '';
|
|
589
|
-
const sum = groupRows.reduce((a, r) => a + (Number(r[c.field]) || 0), 0);
|
|
590
|
-
return formatValueForExport(sum, c.format);
|
|
591
|
-
});
|
|
592
|
-
if (labelColIdx >= 0)
|
|
593
|
-
cells[labelColIdx] = `Subtotal (${key})`;
|
|
594
|
-
out.push({ kind: 'subtotal', cells });
|
|
595
|
-
}
|
|
622
|
+
if (hasTotals)
|
|
623
|
+
out.push(totalRow(groupRows, opts.labels?.subtotal?.(shown) ?? `Subtotal (${shown})`));
|
|
596
624
|
}
|
|
597
625
|
};
|
|
598
626
|
recurse(rows, 0);
|
|
627
|
+
if (hasTotals && opts.grandTotal !== false && rows.length)
|
|
628
|
+
out.push(totalRow(rows, opts.labels?.total ?? 'Total'));
|
|
599
629
|
return out;
|
|
600
630
|
}
|
|
631
|
+
/**
|
|
632
|
+
* Say so when the PDF holds text Roboto cannot draw. Without a `pdf.font` it
|
|
633
|
+
* prints as empty boxes, and nothing else would tell the developer why.
|
|
634
|
+
*/
|
|
635
|
+
function warnMissingGlyphs(texts) {
|
|
636
|
+
const hit = texts.find((t) => PDF_ROBOTO_MISSING.test(t));
|
|
637
|
+
if (hit === undefined)
|
|
638
|
+
return;
|
|
639
|
+
console.warn(`@svgrid/enterprise: the PDF has text its default font (Roboto) cannot draw, such as "${hit.slice(0, 40)}"; ` +
|
|
640
|
+
'it prints as empty boxes. Pass pdf.font with a font that covers it (Noto Sans CJK, Noto Sans Arabic, ...).');
|
|
641
|
+
}
|
|
601
642
|
/**
|
|
602
643
|
* PDF export via our own pdfmake docDefinition (page setup, repeated header,
|
|
603
644
|
* page numbers, per-column alignment/widths, title/logo, theme colors, and
|
|
@@ -605,18 +646,17 @@ export function buildGroupedPdfBody(cols, rows, groupBy) {
|
|
|
605
646
|
* formatted display strings, so the PDF matches the grid.
|
|
606
647
|
*/
|
|
607
648
|
async function exportPdf(api, opts) {
|
|
608
|
-
await ensureGlobals('pdf');
|
|
609
|
-
const pdfMake = globalThis.pdfMake;
|
|
610
|
-
if (!pdfMake) {
|
|
611
|
-
throw new Error('@svgrid/enterprise: pdf export requires the "pdfmake" peer dependency.');
|
|
612
|
-
}
|
|
613
649
|
const sourceRows = resolveRowSource(api, opts.rows);
|
|
614
650
|
if (sourceRows.length === 0) {
|
|
615
651
|
throw new Error('@svgrid/enterprise: nothing to export - the grid has no rows');
|
|
616
652
|
}
|
|
617
653
|
const cols = resolveColumns(api, opts, sourceRows);
|
|
618
654
|
const fields = cols.map((c) => c.field);
|
|
619
|
-
|
|
655
|
+
// The grid's columns arrive with the alignment the screen uses; columns an
|
|
656
|
+
// app passes itself may not say, so a number or date format right-aligns,
|
|
657
|
+
// as it does on screen.
|
|
658
|
+
const alignOf = (c) => c.align ?? (c.format && ['number', 'currency', 'percent', 'date', 'datetime', 'time'].includes(String(c.format.type)) ? 'right' : undefined);
|
|
659
|
+
const columns = cols.map((c) => ({ header: c.header ?? c.field, align: alignOf(c) }));
|
|
620
660
|
// Carry the grid's active grouping into group + subtotal rows (unless the
|
|
621
661
|
// caller opted out or passed hierarchical), matching the xlsx outline path.
|
|
622
662
|
const autoGrouping = opts.autoGroup !== false && !opts.groupBy?.length && !opts.hierarchical
|
|
@@ -648,33 +688,67 @@ async function exportPdf(api, opts) {
|
|
|
648
688
|
return chartToPngBlob(el, { scale: 2 });
|
|
649
689
|
});
|
|
650
690
|
const now = new Date();
|
|
651
|
-
let
|
|
691
|
+
let params;
|
|
692
|
+
// A boolean prints as the grid's checkbox does: a tick for true and nothing
|
|
693
|
+
// for false, rather than the words. Only plain fields: a column with a
|
|
694
|
+
// format or an `exportValue` prints what it asks for.
|
|
695
|
+
const boolAt = (rowIdx, colIdx) => {
|
|
696
|
+
const col = cols[colIdx];
|
|
697
|
+
if (!col || col.exportValue || col.format)
|
|
698
|
+
return undefined;
|
|
699
|
+
const v = sourceRows[rowIdx]?.[col.field];
|
|
700
|
+
return typeof v === 'boolean' ? v : undefined;
|
|
701
|
+
};
|
|
702
|
+
const withTick = (v, b) => {
|
|
703
|
+
const icon = v?.icon ?? (b ? '✓' : undefined);
|
|
704
|
+
return v || icon ? { fill: v?.fill, color: v?.color, bold: v?.bold, icon, iconOnly: v?.iconOnly } : undefined;
|
|
705
|
+
};
|
|
652
706
|
if (effGroupBy?.length) {
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
707
|
+
const grouped = buildGroupedPdfBody(cols, sourceRows, effGroupBy, { grandTotal: pdfOpts.grandTotal, labels: pdfOpts.labels });
|
|
708
|
+
const body = grouped.map((row) => row.kind === 'data' && row.source !== undefined
|
|
709
|
+
? { ...row, cells: row.cells.map((text, c) => (boolAt(row.source, c) === undefined ? text : '')) }
|
|
710
|
+
: row);
|
|
711
|
+
if (!pdfOpts.font)
|
|
712
|
+
warnMissingGlyphs(body.flatMap((r) => (r.kind === 'group' ? [r.label] : r.cells)));
|
|
713
|
+
// Data rows come out in group order; conditional formatting and links are
|
|
714
|
+
// looked up by each row's place in the exported rows.
|
|
715
|
+
const sources = body.flatMap((r) => (r.kind === 'data' ? [r.source ?? -1] : []));
|
|
716
|
+
const visual = buildConditionalResolver(cols, sourceRows, opts.conditionalFormats);
|
|
717
|
+
const link = buildLinkResolver(cols, sourceRows);
|
|
718
|
+
params = {
|
|
719
|
+
columns,
|
|
720
|
+
body,
|
|
721
|
+
dataCellStyle: (r, c) => {
|
|
722
|
+
const at = sources[r] ?? -1;
|
|
723
|
+
return at >= 0 ? withTick(visual(at, c), boolAt(at, c)) : undefined;
|
|
724
|
+
},
|
|
725
|
+
dataCellLink: link
|
|
726
|
+
? (r, c) => {
|
|
727
|
+
const at = sources[r] ?? -1;
|
|
728
|
+
return at >= 0 ? link(at, c) : undefined;
|
|
729
|
+
}
|
|
730
|
+
: undefined,
|
|
731
|
+
opts: pdfOpts,
|
|
732
|
+
styles: opts.styles,
|
|
733
|
+
theme,
|
|
734
|
+
...lines,
|
|
735
|
+
charts,
|
|
736
|
+
now,
|
|
737
|
+
};
|
|
657
738
|
}
|
|
658
739
|
else {
|
|
659
740
|
const projected = await projectRows(sourceRows, cols, opts);
|
|
660
|
-
const dataRows = projected.slice(1).map((r) => fields.map((f) => String(r[f] ?? '')));
|
|
661
|
-
// Conditional formatting -> per-cell fill / color / bold
|
|
741
|
+
const dataRows = projected.slice(1).map((r, ri) => fields.map((f, c) => (boolAt(ri, c) === undefined ? String(r[f] ?? '') : '')));
|
|
742
|
+
// Conditional formatting -> per-cell fill / color / bold, and the icon,
|
|
743
|
+
// which the builder draws (the icon sets' glyphs are not in the font).
|
|
662
744
|
const visual = buildConditionalResolver(cols, sourceRows, opts.conditionalFormats);
|
|
663
|
-
for (let r = 0; r < dataRows.length; r++) {
|
|
664
|
-
for (let c = 0; c < cols.length; c++) {
|
|
665
|
-
const icon = visual(r, c)?.icon;
|
|
666
|
-
if (icon)
|
|
667
|
-
dataRows[r][c] = `${icon} ${dataRows[r][c] ?? ''}`;
|
|
668
|
-
}
|
|
669
|
-
}
|
|
670
745
|
const link = buildLinkResolver(cols, sourceRows);
|
|
671
|
-
|
|
746
|
+
if (!pdfOpts.font)
|
|
747
|
+
warnMissingGlyphs([...columns.map((c) => c.header), ...dataRows.flat()]);
|
|
748
|
+
params = {
|
|
672
749
|
columns,
|
|
673
750
|
rows: dataRows,
|
|
674
|
-
dataCellStyle: (r, c) =>
|
|
675
|
-
const v = visual(r, c);
|
|
676
|
-
return v ? { fill: v.fill, color: v.color, bold: v.bold } : undefined;
|
|
677
|
-
},
|
|
751
|
+
dataCellStyle: (r, c) => withTick(visual(r, c), boolAt(r, c)),
|
|
678
752
|
dataCellLink: link,
|
|
679
753
|
opts: pdfOpts,
|
|
680
754
|
styles: opts.styles,
|
|
@@ -683,11 +757,34 @@ async function exportPdf(api, opts) {
|
|
|
683
757
|
merges: opts.merges,
|
|
684
758
|
charts,
|
|
685
759
|
now,
|
|
686
|
-
}
|
|
760
|
+
};
|
|
687
761
|
}
|
|
688
762
|
const filename = (opts.filename ?? 'grid').trim() || 'grid';
|
|
689
|
-
const
|
|
690
|
-
|
|
763
|
+
const total = sourceRows.length;
|
|
764
|
+
let blob;
|
|
765
|
+
if (pdfOpts.worker) {
|
|
766
|
+
// The document crosses to the worker as data; pdfmake is loaded there.
|
|
767
|
+
blob = await renderPdfInWorker(pdfOpts.worker, buildPdfDocData(params), {
|
|
768
|
+
font: pdfOpts.font,
|
|
769
|
+
signal: opts.signal,
|
|
770
|
+
onProgress: (ratio) => opts.onProgress?.({ phase: 'write', ratio, total }),
|
|
771
|
+
});
|
|
772
|
+
}
|
|
773
|
+
else {
|
|
774
|
+
if (total > LARGE_PDF_ROWS) {
|
|
775
|
+
console.warn(`@svgrid/enterprise: rendering a ${total.toLocaleString('en-US')}-row PDF on the page; pdfmake blocks it until done ` +
|
|
776
|
+
'(about a second per thousand rows). Pass pdf.worker to render in a worker instead.');
|
|
777
|
+
}
|
|
778
|
+
await ensureGlobals('pdf');
|
|
779
|
+
const pdfMake = globalThis.pdfMake;
|
|
780
|
+
if (!pdfMake) {
|
|
781
|
+
throw new Error('@svgrid/enterprise: pdf export requires the "pdfmake" peer dependency.');
|
|
782
|
+
}
|
|
783
|
+
if (pdfOpts.font)
|
|
784
|
+
registerPdfFont(pdfMake, pdfOpts.font);
|
|
785
|
+
blob = await pdfBlob(pdfMake.createPdf(buildPdfDocDefinition(params)));
|
|
786
|
+
}
|
|
787
|
+
opts.onProgress?.({ phase: 'write', ratio: 1, total });
|
|
691
788
|
return deliver(blob, ensureExt(filename, 'pdf'), 'application/pdf', sourceRows.length, opts.download);
|
|
692
789
|
}
|
|
693
790
|
/**
|
package/dist/import.js
CHANGED
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
* `print`, so unlicensed evaluation works but emits a one-time nudge.
|
|
25
25
|
*/
|
|
26
26
|
import { assertEnterpriseLicensed } from './license.js';
|
|
27
|
+
import { createUnzipBudget, readZipEntry, XLSX_MAX_COLS, XLSX_MAX_FILL, xlsxFillError, xlsxRangeError } from './xlsx-limits.js';
|
|
27
28
|
// ---------------------------------------------------------------------------
|
|
28
29
|
// Lazy peer dependency: jszip (xlsx only)
|
|
29
30
|
// ---------------------------------------------------------------------------
|
|
@@ -182,11 +183,12 @@ async function parseXlsx(file) {
|
|
|
182
183
|
const J = await getJSZip();
|
|
183
184
|
const buf = await file.arrayBuffer();
|
|
184
185
|
const zip = await J.loadAsync(buf);
|
|
186
|
+
const budget = createUnzipBudget();
|
|
185
187
|
// Read sharedStrings.xml if it exists.
|
|
186
188
|
let sharedStrings = [];
|
|
187
189
|
const ssEntry = zip.file('xl/sharedStrings.xml');
|
|
188
190
|
if (ssEntry) {
|
|
189
|
-
const ssText = await ssEntry
|
|
191
|
+
const ssText = await readZipEntry(ssEntry, 'string', budget);
|
|
190
192
|
sharedStrings = parseSharedStrings(ssText);
|
|
191
193
|
}
|
|
192
194
|
// Read the FIRST sheet. Workbook.xml -> rels mapping is involved; for the
|
|
@@ -196,7 +198,7 @@ async function parseXlsx(file) {
|
|
|
196
198
|
if (!sheetEntry) {
|
|
197
199
|
throw new Error('@svgrid/enterprise: could not locate sheet1.xml in the .xlsx archive');
|
|
198
200
|
}
|
|
199
|
-
const sheetText = await sheetEntry
|
|
201
|
+
const sheetText = await readZipEntry(sheetEntry, 'string', budget);
|
|
200
202
|
return parseSheetXml(sheetText, sharedStrings);
|
|
201
203
|
}
|
|
202
204
|
/** Pull every `<t>...</t>` out of sharedStrings.xml in document order. */
|
|
@@ -242,6 +244,7 @@ function parseSheetXml(xml, sharedStrings) {
|
|
|
242
244
|
const tRe = /<t\b[^>]*>([\s\S]*?)<\/t>/g;
|
|
243
245
|
const vRe = /<v>([\s\S]*?)<\/v>/;
|
|
244
246
|
let rowMatch;
|
|
247
|
+
let filled = 0;
|
|
245
248
|
while ((rowMatch = rowRe.exec(xml)) !== null) {
|
|
246
249
|
const rowBody = rowMatch[1] ?? '';
|
|
247
250
|
const cells = [];
|
|
@@ -253,7 +256,15 @@ function parseSheetXml(xml, sharedStrings) {
|
|
|
253
256
|
const ref = /r="([A-Z]+)\d+"/.exec(attrs)?.[1] ?? '';
|
|
254
257
|
const colIndex = colRefToIndex(ref);
|
|
255
258
|
const type = /t="([^"]+)"/.exec(attrs)?.[1] ?? 'n';
|
|
256
|
-
// Pad holes with empty strings so column alignment survives
|
|
259
|
+
// Pad holes with empty strings so column alignment survives, within
|
|
260
|
+
// the sheet's real width and the blank budget (see xlsx-limits.ts).
|
|
261
|
+
if (colIndex >= XLSX_MAX_COLS)
|
|
262
|
+
throw xlsxRangeError(/r="([^"]*)"/.exec(attrs)?.[1] ?? ref);
|
|
263
|
+
if (colIndex > cells.length) {
|
|
264
|
+
filled += colIndex - cells.length;
|
|
265
|
+
if (filled > XLSX_MAX_FILL)
|
|
266
|
+
throw xlsxFillError();
|
|
267
|
+
}
|
|
257
268
|
while (cells.length < colIndex)
|
|
258
269
|
cells.push('');
|
|
259
270
|
let value = '';
|
package/dist/index.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ export { setLicenseKey, clearLicenseKey, getLicenseKey, isLicenseKeySet, hasVali
|
|
|
2
2
|
export { parseLicenseExpiry } from './license-core.js';
|
|
3
3
|
export { exportGrid, copyExportToClipboard, type ExportFormat, type ExportOptions, type ExportColumn, type ExportCellStyle, type ExportStyles, type ExportHeaderFooterLine, type ExportSheet, type ExportRowSource, type ExportProgress, type ExportResult, type ClipboardFormat, type ClipboardExportOptions, } from './export.js';
|
|
4
4
|
export { default as SvExportMenu } from './SvExportMenu.svelte';
|
|
5
|
-
export { buildPdfDocDefinition, resolvePdfCharts, resolvePdfVfs, registerPdfFonts, type PdfExportOptions, type PdfPageSize, type PdfChart, type PdfKpi, type PdfChartImage, type PdfVirtualFileSystem, type PdfMakeLike } from './export-pdf.js';
|
|
5
|
+
export { buildPdfDocDefinition, buildPdfDocData, hydratePdfDoc, resolvePdfCharts, resolvePdfVfs, registerPdfFonts, registerPdfFont, type PdfExportOptions, type PdfDocData, type PdfDocParams, type PdfWorkerLike, type PdfPageSize, type PdfChart, type PdfKpi, type PdfChartImage, type PdfVirtualFileSystem, type PdfMakeLike, type PdfFont } from './export-pdf.js';
|
|
6
6
|
export { printGrid, type PrintOptions } from './print.js';
|
|
7
7
|
export { importData, readImportMatrix, mapImportMatrix, mapImportMatrixAsync, autoMapColumns, inferImportColumnTypes, type ImportFormat, type ImportFieldType, type ImportOptions, type ImportResult, type ImportProgress, type ImportColumnMap, type ImportColumnTypes, type ImportGridColumn, type ImportRowError, type ImportValidator, } from './import.js';
|
|
8
8
|
export { default as SvImportDialog } from './SvImportDialog.svelte';
|
package/dist/index.js
CHANGED
|
@@ -8,7 +8,7 @@ licenseCovers, getLicenseEdition, nudgeEnterpriseFor, editionCovers, } from './l
|
|
|
8
8
|
export { parseLicenseExpiry } from './license-core.js';
|
|
9
9
|
export { exportGrid, copyExportToClipboard, } from './export.js';
|
|
10
10
|
export { default as SvExportMenu } from './SvExportMenu.svelte';
|
|
11
|
-
export { buildPdfDocDefinition, resolvePdfCharts, resolvePdfVfs, registerPdfFonts } from './export-pdf.js';
|
|
11
|
+
export { buildPdfDocDefinition, buildPdfDocData, hydratePdfDoc, resolvePdfCharts, resolvePdfVfs, registerPdfFonts, registerPdfFont } from './export-pdf.js';
|
|
12
12
|
export { printGrid } from './print.js';
|
|
13
13
|
export { importData, readImportMatrix, mapImportMatrix, mapImportMatrixAsync, autoMapColumns, inferImportColumnTypes, } from './import.js';
|
|
14
14
|
export { default as SvImportDialog } from './SvImportDialog.svelte';
|
package/dist/node/studio.js
CHANGED
|
@@ -16124,8 +16124,8 @@ function layoutFile(nav, opts = {}) {
|
|
|
16124
16124
|
${toolbarOn ? `<div class="sv-app__toolbar">
|
|
16125
16125
|
<div class="sv-app__search">
|
|
16126
16126
|
<svg class="sv-app__search-ic" viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><circle cx="11" cy="11" r="7"/><path d="M21 21l-4.35-4.35"/></svg>
|
|
16127
|
-
<input class="sv-app__search-in" type="search" placeholder="Search {brand}…" bind:value={q}
|
|
16128
|
-
<
|
|
16127
|
+
<input class="sv-app__search-in" type="search" placeholder="Search {brand}…" bind:value={q} aria-label="Search screens" />
|
|
16128
|
+
<button type="button" class="sv-app__kbd" title="Command palette (⌘K / Ctrl+K)" aria-label="Open the command palette" onclick={() => (paletteOpen = true)}>⌘K</button>
|
|
16129
16129
|
{#if results.length}
|
|
16130
16130
|
<div class="sv-app__results">
|
|
16131
16131
|
{#each results as r (r.href)}
|
|
@@ -16165,6 +16165,7 @@ function layoutFile(nav, opts = {}) {
|
|
|
16165
16165
|
</div>
|
|
16166
16166
|
</div>
|
|
16167
16167
|
{#if bellOpen || menuOpen}<button type="button" class="sv-app__scrim" tabindex="-1" aria-label="Close menus" onclick={() => { bellOpen = false; menuOpen = false }}></button>{/if}
|
|
16168
|
+
<SvCommand bind:open={paletteOpen} {commands} placeholder="Go to a screen or run an action…" emptyText="No matching screen" />
|
|
16168
16169
|
</div>\n ` : ""}<div class="sv-app__content">
|
|
16169
16170
|
${opts.supabaseAuth ? `<SvAuthGate client={supabaseClient as unknown as SupabaseAuthClientLike} title={\`Sign in to \${brand}\`}>
|
|
16170
16171
|
{@render children()}
|
|
@@ -16254,7 +16255,7 @@ function layoutFile(nav, opts = {}) {
|
|
|
16254
16255
|
import '../app.css'
|
|
16255
16256
|
import '../custom.css'
|
|
16256
16257
|
import { page } from '$app/state'
|
|
16257
|
-
import { setLicenseKey } from '@svgrid/enterprise'${opts.access ? `\n import { currentRole, canScreen } from '$lib/access'` : ""}${opts.i18n ? `\n import { t, currentLocale, locales } from '$lib/i18n'` : ""}${opts.auth ? `\n import type { LayoutData } from './$types'` : ""}${opts.supabaseAuth ? `\n import { SvAuthGate, type SupabaseAuthClientLike } from '@svgrid/enterprise'\n import { supabaseClient } from '$lib/connections'` : ""}
|
|
16258
|
+
import { setLicenseKey } from '@svgrid/enterprise'${toolbarOn ? `\n import { goto } from '$app/navigation'\n import { SvCommand, type CommandItem } from '@svgrid/grid'` : ""}${opts.access ? `\n import { currentRole, canScreen } from '$lib/access'` : ""}${opts.i18n ? `\n import { t, currentLocale, locales } from '$lib/i18n'` : ""}${opts.auth ? `\n import type { LayoutData } from './$types'` : ""}${opts.supabaseAuth ? `\n import { SvAuthGate, type SupabaseAuthClientLike } from '@svgrid/enterprise'\n import { supabaseClient } from '$lib/connections'` : ""}
|
|
16258
16259
|
|
|
16259
16260
|
// VITE_SVPRO_KEY in .env is your SvGrid license key; it removes the unlicensed watermark.
|
|
16260
16261
|
if (import.meta.env.VITE_SVPRO_KEY) setLicenseKey(import.meta.env.VITE_SVPRO_KEY)
|
|
@@ -16280,24 +16281,30 @@ function layoutFile(nav, opts = {}) {
|
|
|
16280
16281
|
try { const s = localStorage.getItem('svapp:theme'); if (s === 'light' || s === 'dark') t = s } catch (_) { /* storage blocked */ }
|
|
16281
16282
|
applyTheme(t)
|
|
16282
16283
|
})` : ""}${sideState}${toolbarOn ? `
|
|
16283
|
-
// App-chrome quick-search: filter the screens by label as you type.
|
|
16284
|
+
// App-chrome quick-search: filter the screens by label as you type. Only the
|
|
16285
|
+
// screens the sidebar would show${opts.access ? " to this role" : ""}.
|
|
16284
16286
|
const initials = ${JSON.stringify(initials)}
|
|
16285
16287
|
const acctEmail = ${JSON.stringify(`admin@${brandSlug}.com`)}
|
|
16288
|
+
const visibleNav = ${opts.access ? "$derived(nav.filter((n) => !n.id || canScreen($currentRole, n.id)))" : "nav"}
|
|
16286
16289
|
let q = $state('')
|
|
16287
|
-
const results = $derived(q.trim() ?
|
|
16290
|
+
const results = $derived(q.trim() ? visibleNav.filter((n) => n.label.toLowerCase().includes(q.trim().toLowerCase())).slice(0, 6) : [])
|
|
16291
|
+
// Cmd/Ctrl+K: a palette over the same screens. SvCommand binds the key itself.
|
|
16292
|
+
let paletteOpen = $state(false)
|
|
16293
|
+
const commands: CommandItem[] = $derived([
|
|
16294
|
+
...visibleNav.map((n) => ({ id: 'go:' + n.href, label: ${opts.i18n ? "n.id ? $t('nav.' + n.id, n.label) : n.label" : "n.label"}, group: 'Go to', onRun: () => goto(n.href) })),${hasSwitch ? `
|
|
16295
|
+
{ id: 'theme', label: theme === 'dark' ? 'Switch to light mode' : 'Switch to dark mode', group: 'Actions', onRun: toggleTheme },` : ""}
|
|
16296
|
+
])
|
|
16288
16297
|
let bellOpen = $state(false)
|
|
16289
16298
|
let menuOpen = $state(false)
|
|
16290
|
-
let searchEl: HTMLInputElement | null = $state(null)
|
|
16291
16299
|
const notifications = [
|
|
16292
16300
|
{ title: 'Welcome to ' + brand, time: 'just now', color: 'var(--sg-accent, #6366f1)' },
|
|
16293
16301
|
{ title: 'Your workspace is ready', time: '2m ago', color: '#10b981' },
|
|
16294
16302
|
{ title: 'Sample data loaded', time: '5m ago', color: '#f59e0b' },
|
|
16295
16303
|
]
|
|
16296
|
-
//
|
|
16304
|
+
// Escape closes the menus.
|
|
16297
16305
|
$effect(() => {
|
|
16298
16306
|
function onKey(e: KeyboardEvent) {
|
|
16299
|
-
if (
|
|
16300
|
-
else if (e.key === 'Escape') { bellOpen = false; menuOpen = false }
|
|
16307
|
+
if (e.key === 'Escape') { bellOpen = false; menuOpen = false }
|
|
16301
16308
|
}
|
|
16302
16309
|
window.addEventListener('keydown', onKey)
|
|
16303
16310
|
return () => window.removeEventListener('keydown', onKey)
|
|
@@ -16336,7 +16343,8 @@ ${styles}
|
|
|
16336
16343
|
.sv-app__tool:hover { color: var(--sg-fg, #0f172a); border-color: color-mix(in srgb, var(--sg-fg, #0f172a) 20%, var(--sg-border, #e6e8ec)); }
|
|
16337
16344
|
.sv-app__dot { position: absolute; top: 5px; right: 5px; width: 6px; height: 6px; border-radius: 50%; background: var(--sg-accent, #6366f1); box-shadow: 0 0 0 2px var(--sg-bg, #fff); }
|
|
16338
16345
|
.sv-app__avatar { display: inline-flex; align-items: center; justify-content: center; width: 32px; height: 32px; padding: 0; border: none; border-radius: 50%; font: inherit; font-size: 12px; font-weight: 700; color: #fff; background: var(--sg-accent, #6366f1); letter-spacing: 0.02em; cursor: pointer; }
|
|
16339
|
-
.sv-app__kbd { position: absolute; right: 8px; font-size: 10.5px; font-weight: 600; color: var(--sg-muted, #94a3b8); background: color-mix(in srgb, var(--sg-fg, #0f172a) 6%, transparent); border: 1px solid var(--sg-border, #e6e8ec); border-radius: 5px; padding: 1px 5px;
|
|
16346
|
+
.sv-app__kbd { position: absolute; right: 8px; font-size: 10.5px; font-weight: 600; color: var(--sg-muted, #94a3b8); background: color-mix(in srgb, var(--sg-fg, #0f172a) 6%, transparent); border: 1px solid var(--sg-border, #e6e8ec); border-radius: 5px; padding: 1px 5px; font-family: inherit; line-height: inherit; cursor: pointer; }
|
|
16347
|
+
.sv-app__kbd:hover { color: var(--sg-fg, #0f172a); }
|
|
16340
16348
|
.sv-app__pop { position: relative; }
|
|
16341
16349
|
.sv-app__scrim { position: fixed; inset: 0; z-index: 44; border: 0; padding: 0; background: transparent; cursor: default; }
|
|
16342
16350
|
.sv-app__menu { position: absolute; top: calc(100% + 8px); right: 0; z-index: 46; min-width: 232px; padding: 6px; background: var(--sg-bg, #fff); border: 1px solid var(--sg-border, #e6e8ec); border-radius: 11px; box-shadow: 0 16px 40px -10px rgba(15, 23, 42, 0.3); }
|
|
@@ -16506,7 +16514,7 @@ function hasFieldConditions(schema) {
|
|
|
16506
16514
|
*
|
|
16507
16515
|
* `version.test.ts` asserts this matches `package.json`, so the two cannot drift.
|
|
16508
16516
|
*/
|
|
16509
|
-
var SVGRID_VERSION = "3.1.
|
|
16517
|
+
var SVGRID_VERSION = "3.1.27";
|
|
16510
16518
|
/**
|
|
16511
16519
|
* The version of `@svgrid/grid`, which releases on its own line: enterprise is
|
|
16512
16520
|
* on 3.1.x while the grid is on 3.0.x.
|
|
@@ -16519,7 +16527,7 @@ var SVGRID_VERSION = "3.1.25";
|
|
|
16519
16527
|
*
|
|
16520
16528
|
* `version.test.ts` asserts this matches packages/grid/package.json.
|
|
16521
16529
|
*/
|
|
16522
|
-
var GRID_VERSION = "3.0.
|
|
16530
|
+
var GRID_VERSION = "3.0.23";
|
|
16523
16531
|
//#endregion
|
|
16524
16532
|
//#region src/studio/emit-project.ts
|
|
16525
16533
|
var has = (blocks, kind) => blocks.some((b) => b.config.kind === kind);
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Render a PDF export in a worker, so a large report does not freeze the
|
|
3
|
+
* page. pdfmake lays out and writes the whole document in one synchronous
|
|
4
|
+
* run, about a millisecond per row of eight columns on a desktop machine:
|
|
5
|
+
* a 20,000-row export held the main thread for over twenty seconds.
|
|
6
|
+
*
|
|
7
|
+
* The app owns the worker file, so any bundler builds it:
|
|
8
|
+
*
|
|
9
|
+
* ```ts
|
|
10
|
+
* // pdf.worker.ts
|
|
11
|
+
* import { servePdfWorker } from '@svgrid/enterprise/pdf-worker'
|
|
12
|
+
* servePdfWorker()
|
|
13
|
+
* ```
|
|
14
|
+
*
|
|
15
|
+
* ```ts
|
|
16
|
+
* await api.exportData({
|
|
17
|
+
* format: 'pdf',
|
|
18
|
+
* pdf: { worker: () => new Worker(new URL('./pdf.worker.ts', import.meta.url), { type: 'module' }) },
|
|
19
|
+
* })
|
|
20
|
+
* ```
|
|
21
|
+
*
|
|
22
|
+
* The main thread builds the document as plain data (`buildPdfDocData`) and
|
|
23
|
+
* posts it here with the font, if any; the worker loads pdfmake, attaches
|
|
24
|
+
* the layout and footer functions (`hydratePdfDoc`), renders, and posts the
|
|
25
|
+
* bytes back with progress along the way.
|
|
26
|
+
*/
|
|
27
|
+
import { type PdfDocData, type PdfFont, type PdfMakeLike, type PdfWorkerLike } from './export-pdf.js';
|
|
28
|
+
export type { PdfWorkerLike };
|
|
29
|
+
/** Tags this channel's messages, so a worker shared with other traffic ignores the rest. */
|
|
30
|
+
export declare const PDF_WORKER_CHANNEL = "svgrid-pdf";
|
|
31
|
+
export type PdfWorkerRequest = {
|
|
32
|
+
channel: typeof PDF_WORKER_CHANNEL;
|
|
33
|
+
id: number;
|
|
34
|
+
doc: PdfDocData;
|
|
35
|
+
font?: PdfFont;
|
|
36
|
+
};
|
|
37
|
+
export type PdfWorkerReply = {
|
|
38
|
+
channel: typeof PDF_WORKER_CHANNEL;
|
|
39
|
+
id: number;
|
|
40
|
+
progress: number;
|
|
41
|
+
} | {
|
|
42
|
+
channel: typeof PDF_WORKER_CHANNEL;
|
|
43
|
+
id: number;
|
|
44
|
+
pdf: ArrayBuffer;
|
|
45
|
+
} | {
|
|
46
|
+
channel: typeof PDF_WORKER_CHANNEL;
|
|
47
|
+
id: number;
|
|
48
|
+
error: string;
|
|
49
|
+
};
|
|
50
|
+
/** The pdfmake calls rendering needs, in either the 0.2 or the 0.3 shape. */
|
|
51
|
+
export type PdfMakeRenderer = PdfMakeLike & {
|
|
52
|
+
fonts?: Record<string, unknown>;
|
|
53
|
+
addFonts?: (fonts: Record<string, unknown>) => void;
|
|
54
|
+
createPdf(def: unknown): {
|
|
55
|
+
getBuffer(cb?: (bytes: Uint8Array) => void, options?: {
|
|
56
|
+
progressCallback?: (progress: number) => void;
|
|
57
|
+
}): unknown;
|
|
58
|
+
};
|
|
59
|
+
};
|
|
60
|
+
/** The part of a worker's global scope (or a `MessagePort`) this needs. */
|
|
61
|
+
export type PdfWorkerScope = {
|
|
62
|
+
postMessage(message: unknown, transfer?: Transferable[]): void;
|
|
63
|
+
addEventListener(type: 'message', listener: (event: MessageEvent) => void): void;
|
|
64
|
+
removeEventListener(type: 'message', listener: (event: MessageEvent) => void): void;
|
|
65
|
+
};
|
|
66
|
+
/**
|
|
67
|
+
* Render a document from `buildPdfDocData` with the pdfmake given. The
|
|
68
|
+
* worker's half of the export, exported so it can run anywhere pdfmake does.
|
|
69
|
+
*/
|
|
70
|
+
export declare function renderPdfDoc(doc: PdfDocData, pdfMake: PdfMakeRenderer, opts?: {
|
|
71
|
+
font?: PdfFont;
|
|
72
|
+
onProgress?: (progress: number) => void;
|
|
73
|
+
}): Promise<Uint8Array>;
|
|
74
|
+
export type ServePdfWorkerOptions = {
|
|
75
|
+
/** Where to listen. Default: the worker's global scope. */
|
|
76
|
+
scope?: PdfWorkerScope;
|
|
77
|
+
/** How to get pdfmake. Default: import the installed `pdfmake` peer. */
|
|
78
|
+
loadPdfMake?: () => Promise<PdfMakeRenderer>;
|
|
79
|
+
};
|
|
80
|
+
/** Answer PDF export requests from the main thread. Call it once, in the worker. */
|
|
81
|
+
export declare function servePdfWorker(options?: ServePdfWorkerOptions): {
|
|
82
|
+
dispose(): void;
|
|
83
|
+
};
|
|
84
|
+
/**
|
|
85
|
+
* The main thread's half: post the document to a fresh worker, report its
|
|
86
|
+
* progress, and resolve with the PDF. An abort terminates the worker.
|
|
87
|
+
*/
|
|
88
|
+
export declare function renderPdfInWorker(makeWorker: () => PdfWorkerLike, doc: PdfDocData, opts?: {
|
|
89
|
+
font?: PdfFont;
|
|
90
|
+
signal?: AbortSignal;
|
|
91
|
+
onProgress?: (progress: number) => void;
|
|
92
|
+
}): Promise<Blob>;
|