@svgrid/enterprise 1.2.0 → 2.0.0

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.
Files changed (159) hide show
  1. package/dist/cdn/svgrid-enterprise.svelte-external.js +26039 -805
  2. package/dist/designer/assets/index-Dp44bTid.js +939 -0
  3. package/dist/designer/assets/index-RJp6x8tw.css +1 -0
  4. package/dist/designer/assets/jszip.min-CjMo-QGg.js +2 -0
  5. package/dist/designer/index.html +13 -0
  6. package/dist/node/studio.js +22554 -0
  7. package/package.json +10 -3
  8. package/src/SvAuthGate.svelte +115 -0
  9. package/src/SvBoard.dom.test.ts +67 -0
  10. package/src/SvBoard.svelte +192 -0
  11. package/src/SvExportMenu.svelte +553 -0
  12. package/src/SvFileInput.svelte +113 -0
  13. package/src/SvGridEditPanel.dom.test.ts +146 -0
  14. package/src/SvGridEditPanel.svelte +793 -0
  15. package/src/SvGridMasterDetail.svelte +95 -0
  16. package/src/SvImportDialog.svelte +1020 -0
  17. package/src/SvLookupInput.svelte +180 -0
  18. package/src/SvRecordDetail.dom.test.ts +137 -0
  19. package/src/SvRecordDetail.svelte +288 -0
  20. package/src/SvSchedule.dom.test.ts +57 -0
  21. package/src/SvSchedule.svelte +156 -0
  22. package/src/SvSchemaChart.svelte +233 -0
  23. package/src/SvSchemaDashboard.svelte +130 -0
  24. package/src/ai-export-pdf.test.ts +74 -0
  25. package/src/ai-export-xlsx.test.ts +87 -0
  26. package/src/ai-export.test.ts +110 -0
  27. package/src/ai.ts +1188 -782
  28. package/src/edit-panel.test.ts +213 -0
  29. package/src/edit-panel.ts +228 -0
  30. package/src/export-conditional.test.ts +60 -0
  31. package/src/export-conditional.ts +94 -0
  32. package/src/export-ooxml.test.ts +152 -0
  33. package/src/export-ooxml.ts +485 -0
  34. package/src/export-pdf.test.ts +138 -0
  35. package/src/export-pdf.ts +245 -0
  36. package/src/export-print.test.ts +66 -0
  37. package/src/export-print.ts +132 -0
  38. package/src/export-serialize.test.ts +56 -0
  39. package/src/export-serialize.ts +24 -0
  40. package/src/export-xls.ts +196 -0
  41. package/src/export-xlsx-roundtrip.test.ts +120 -0
  42. package/src/export-xlsx.test.ts +150 -0
  43. package/src/export.test.ts +349 -0
  44. package/src/export.ts +1452 -549
  45. package/src/import-automap.test.ts +131 -0
  46. package/src/import-hardening.test.ts +158 -0
  47. package/src/import.ts +1042 -648
  48. package/src/index.ts +282 -0
  49. package/src/install.ts +134 -114
  50. package/src/license-core.test.ts +23 -0
  51. package/src/license-core.ts +35 -0
  52. package/src/license.ts +103 -90
  53. package/src/master-detail.test.ts +61 -0
  54. package/src/master-detail.ts +42 -0
  55. package/src/print.ts +107 -127
  56. package/src/schema-designer.test.ts +121 -0
  57. package/src/schema-designer.ts +142 -0
  58. package/src/schema.test.ts +268 -0
  59. package/src/schema.ts +499 -0
  60. package/src/smart-shim.ts +107 -105
  61. package/src/sources/aggregate.test.ts +79 -0
  62. package/src/sources/aggregate.ts +102 -0
  63. package/src/sources/auth-supabase.test.ts +80 -0
  64. package/src/sources/auth-supabase.ts +87 -0
  65. package/src/sources/dashboard.kpi.test.ts +50 -0
  66. package/src/sources/dashboard.test.ts +61 -0
  67. package/src/sources/dashboard.ts +136 -0
  68. package/src/sources/field-inference.test.ts +60 -0
  69. package/src/sources/field-inference.ts +63 -0
  70. package/src/sources/filters.test.ts +58 -0
  71. package/src/sources/filters.ts +64 -0
  72. package/src/sources/index.ts +67 -0
  73. package/src/sources/introspect-supabase.test.ts +100 -0
  74. package/src/sources/introspect-supabase.ts +119 -0
  75. package/src/sources/realtime-supabase.test.ts +93 -0
  76. package/src/sources/realtime-supabase.ts +99 -0
  77. package/src/sources/relation-lookup.test.ts +99 -0
  78. package/src/sources/relation-lookup.ts +128 -0
  79. package/src/sources/rest-adapters.test.ts +95 -0
  80. package/src/sources/rest-adapters.ts +120 -0
  81. package/src/sources/rest.test.ts +104 -0
  82. package/src/sources/rest.ts +129 -0
  83. package/src/sources/schema-from-columns.test.ts +106 -0
  84. package/src/sources/schema-from-columns.ts +88 -0
  85. package/src/sources/supabase.test.ts +110 -0
  86. package/src/sources/supabase.ts +99 -0
  87. package/src/sources/with-entity-rules.test.ts +104 -0
  88. package/src/sources/with-entity-rules.ts +78 -0
  89. package/src/sources/with-relation-labels.test.ts +75 -0
  90. package/src/sources/with-relation-labels.ts +73 -0
  91. package/src/studio/bug-report.test.ts +101 -0
  92. package/src/studio/bug-report.ts +165 -0
  93. package/src/studio/cli.test.ts +187 -0
  94. package/src/studio/cli.ts +112 -0
  95. package/src/studio/csv.test.ts +90 -0
  96. package/src/studio/csv.ts +165 -0
  97. package/src/studio/db-connect-string.test.ts +94 -0
  98. package/src/studio/db-connect-string.ts +128 -0
  99. package/src/studio/emit-project.test.ts +923 -0
  100. package/src/studio/emit-project.ts +1273 -0
  101. package/src/studio/emit-schema.test.ts +94 -0
  102. package/src/studio/emit-schema.ts +920 -0
  103. package/src/studio/index.ts +195 -0
  104. package/src/studio/introspect-db.test.ts +186 -0
  105. package/src/studio/introspect-db.ts +312 -0
  106. package/src/studio/introspect-prisma.test.ts +99 -0
  107. package/src/studio/introspect-prisma.ts +175 -0
  108. package/src/studio/introspect.test.ts +172 -0
  109. package/src/studio/introspect.ts +310 -0
  110. package/src/studio/pipeline.test.ts +42 -0
  111. package/src/studio/project-robust.test.ts +157 -0
  112. package/src/studio/project.test.ts +473 -0
  113. package/src/studio/project.ts +916 -0
  114. package/src/studio/sample-data.test.ts +58 -0
  115. package/src/studio/sample-data.ts +200 -0
  116. package/src/studio/samples/ats.ts +202 -0
  117. package/src/studio/samples/clinic.ts +177 -0
  118. package/src/studio/samples/crm.ts +250 -0
  119. package/src/studio/samples/ecommerce.ts +187 -0
  120. package/src/studio/samples/events.ts +199 -0
  121. package/src/studio/samples/fleet.ts +184 -0
  122. package/src/studio/samples/gym.ts +213 -0
  123. package/src/studio/samples/hr.ts +193 -0
  124. package/src/studio/samples/index.ts +55 -0
  125. package/src/studio/samples/insurance.ts +195 -0
  126. package/src/studio/samples/inventory.ts +182 -0
  127. package/src/studio/samples/invoicing.ts +162 -0
  128. package/src/studio/samples/library.ts +180 -0
  129. package/src/studio/samples/live-data.test.ts +98 -0
  130. package/src/studio/samples/live-data.ts +308 -0
  131. package/src/studio/samples/projects.ts +190 -0
  132. package/src/studio/samples/realestate.ts +196 -0
  133. package/src/studio/samples/restaurant.ts +187 -0
  134. package/src/studio/samples/samples.test.ts +208 -0
  135. package/src/studio/samples/school.ts +197 -0
  136. package/src/studio/samples/seed-floor.test.ts +25 -0
  137. package/src/studio/samples/shared.ts +305 -0
  138. package/src/studio/samples/subscriptions.ts +183 -0
  139. package/src/studio/samples/support.ts +182 -0
  140. package/src/studio/scaffold-app.test.ts +91 -0
  141. package/src/studio/scaffold-app.ts +161 -0
  142. package/src/studio/scaffold.test.ts +178 -0
  143. package/src/studio/scaffold.ts +374 -0
  144. package/src/studio/themes.ts +172 -0
  145. package/src/studio/verify.test.ts +47 -0
  146. package/src/studio/verify.ts +79 -0
  147. package/src/sveltekit/in-memory.test.ts +104 -0
  148. package/src/sveltekit/in-memory.ts +129 -0
  149. package/src/sveltekit/index.ts +23 -0
  150. package/src/sveltekit/query-plan.test.ts +109 -0
  151. package/src/sveltekit/query-plan.ts +125 -0
  152. package/src/sveltekit/sql-source.test.ts +185 -0
  153. package/src/sveltekit/sql-source.ts +166 -0
  154. package/src/sveltekit/sql.test.ts +80 -0
  155. package/src/sveltekit/sql.ts +131 -0
  156. package/src/sveltekit/transport.test.ts +175 -0
  157. package/src/sveltekit/transport.ts +254 -0
  158. package/src/sveltekit/types.ts +11 -0
  159. package/src/upgrade-prompt.ts +149 -148
package/src/license.ts CHANGED
@@ -1,90 +1,103 @@
1
- // Polite license gate. Not crypto - anyone with devtools can extract the key
2
- // from a deployed bundle. The point is to make commercial use require a
3
- // transaction, not to defeat reverse engineering.
4
- //
5
- // Behavior matrix:
6
- //
7
- // currentKey state -> result
8
- // ────────────────────────────────────────────────────────────────────
9
- // null (no key set) -> soft-gate: watermark + console.log,
10
- // feature still runs
11
- // does not start with "SVENTERPRISE-" -> throws (programmer error)
12
- // in REVOKED_KEYS -> throws (revoked / leaked / expired)
13
- // starts with "SVENTERPRISE-DEV" or
14
- // "SVENTERPRISE-EVAL" -> works; one-time console.info notice
15
- // any other "SVENTERPRISE-..." -> works silently (paid production)
16
-
17
- import { REVOKED_KEYS } from './revoked'
18
- import { emitUnlicensedNudge } from './watermark'
19
- import { showUpgradePrompt, type EnterpriseFeatureLabel } from './upgrade-prompt'
20
-
21
- const VALID_PREFIX = 'SVENTERPRISE-'
22
- let currentKey: string | null = null
23
- let noticedDev = false
24
-
25
- export function setLicenseKey(key: string): void {
26
- if (typeof key !== 'string' || key.length === 0) {
27
- throw new Error('@svgrid/enterprise: setLicenseKey() requires a non-empty string')
28
- }
29
- currentKey = key
30
- noticedDev = false
31
- }
32
-
33
- export function clearLicenseKey(): void {
34
- currentKey = null
35
- noticedDev = false
36
- }
37
-
38
- export function getLicenseKey(): string | null {
39
- return currentKey
40
- }
41
-
42
- /** True if a key is set at all (regardless of validity). */
43
- export function isLicenseKeySet(): boolean {
44
- return currentKey != null
45
- }
46
-
47
- /**
48
- * True if the current key passes every check: present, valid prefix, not
49
- * revoked. Use this from callers that want to branch on license status
50
- * (e.g. UI that hides Pro-only options when unlicensed).
51
- */
52
- export function hasValidLicense(): boolean {
53
- if (currentKey == null) return false
54
- if (!currentKey.startsWith(VALID_PREFIX)) return false
55
- if (REVOKED_KEYS.has(currentKey)) return false
56
- return true
57
- }
58
-
59
- export function assertEnterpriseLicensed(feature?: EnterpriseFeatureLabel): void {
60
- if (currentKey == null) {
61
- // Soft-gate: the feature still runs, but the user gets a watermark +
62
- // a one-time console.log nudge directing them to pricing, plus a
63
- // contextual moment-of-intent upgrade card naming the feature they
64
- // just reached for.
65
- emitUnlicensedNudge()
66
- showUpgradePrompt(feature)
67
- return
68
- }
69
- if (!currentKey.startsWith(VALID_PREFIX)) {
70
- throw new Error(
71
- '@svgrid/enterprise: invalid license key format (expected "SVENTERPRISE-..." prefix).',
72
- )
73
- }
74
- if (REVOKED_KEYS.has(currentKey)) {
75
- throw new Error(
76
- '@svgrid/enterprise: this license key has been revoked. ' +
77
- 'Contact sales@jqwidgets.com for a replacement.',
78
- )
79
- }
80
- if (
81
- !noticedDev &&
82
- (currentKey.startsWith('SVENTERPRISE-DEV') || currentKey.startsWith('SVENTERPRISE-EVAL'))
83
- ) {
84
- // eslint-disable-next-line no-console
85
- console.info(
86
- '@svgrid/enterprise: using a development / evaluation license. Not for production use.',
87
- )
88
- noticedDev = true
89
- }
90
- }
1
+ // Polite license gate. Not crypto - anyone with devtools can extract the key
2
+ // from a deployed bundle. The point is to make commercial use require a
3
+ // transaction, not to defeat reverse engineering.
4
+ //
5
+ // Behavior matrix:
6
+ //
7
+ // currentKey state -> result
8
+ // ────────────────────────────────────────────────────────────────────
9
+ // null (no key set) -> soft-gate: watermark + console.log,
10
+ // feature still runs
11
+ // does not start with "SVENTERPRISE-" -> throws (programmer error)
12
+ // in REVOKED_KEYS -> throws (revoked / leaked / expired)
13
+ // starts with "SVENTERPRISE-DEV" or
14
+ // "SVENTERPRISE-EVAL" -> works; one-time console.info notice
15
+ // any other "SVENTERPRISE-..." -> works silently (paid production)
16
+
17
+ import { checkLicenseKey, VALID_PREFIX, type LicenseInfo } from './license-core'
18
+ import { emitUnlicensedNudge } from './watermark'
19
+ import { showUpgradePrompt, type EnterpriseFeatureLabel } from './upgrade-prompt'
20
+
21
+ export { checkLicenseKey, type LicenseInfo, type LicenseStatus } from './license-core'
22
+
23
+ let currentKey: string | null = null
24
+ let noticedDev = false
25
+
26
+ export function setLicenseKey(key: string): void {
27
+ if (typeof key !== 'string' || key.length === 0) {
28
+ throw new Error('@svgrid/enterprise: setLicenseKey() requires a non-empty string')
29
+ }
30
+ currentKey = key
31
+ noticedDev = false
32
+ }
33
+
34
+ export function clearLicenseKey(): void {
35
+ currentKey = null
36
+ noticedDev = false
37
+ }
38
+
39
+ export function getLicenseKey(): string | null {
40
+ return currentKey
41
+ }
42
+
43
+ /** True if a key is set at all (regardless of validity). */
44
+ export function isLicenseKeySet(): boolean {
45
+ return currentKey != null
46
+ }
47
+
48
+ /**
49
+ * True if the current key passes every check: present, valid prefix, not
50
+ * revoked. Use this from callers that want to branch on license status
51
+ * (e.g. UI that hides Pro-only options when unlicensed).
52
+ */
53
+ export function hasValidLicense(): boolean {
54
+ return checkLicenseKey(currentKey).valid
55
+ }
56
+
57
+ /**
58
+ * Soft-gate a Pro surface: if the current key isn't valid, nudge (watermark +
59
+ * one-time console log + a moment-of-intent upgrade card naming `feature`) and
60
+ * return. NEVER throws and NEVER blocks - the feature always runs. This is the
61
+ * gate the Studio uses, and it's safe to call on the server (the nudges no-op
62
+ * without a DOM). Idempotent/self-throttling, so call it freely at chokepoints.
63
+ */
64
+ export function nudgeEnterprise(feature?: EnterpriseFeatureLabel): void {
65
+ if (checkLicenseKey(currentKey).valid) return
66
+ emitUnlicensedNudge()
67
+ showUpgradePrompt(feature)
68
+ }
69
+
70
+ export function assertEnterpriseLicensed(feature?: EnterpriseFeatureLabel): void {
71
+ const info: LicenseInfo = checkLicenseKey(currentKey)
72
+ switch (info.status) {
73
+ case 'unset':
74
+ // Soft-gate: the feature still runs, but the user gets a watermark +
75
+ // a one-time console.log nudge directing them to pricing, plus a
76
+ // contextual moment-of-intent upgrade card naming the feature they
77
+ // just reached for.
78
+ emitUnlicensedNudge()
79
+ showUpgradePrompt(feature)
80
+ return
81
+ case 'invalid':
82
+ throw new Error(
83
+ `@svgrid/enterprise: invalid license key format (expected "${VALID_PREFIX}..." prefix).`,
84
+ )
85
+ case 'revoked':
86
+ throw new Error(
87
+ '@svgrid/enterprise: this license key has been revoked. ' +
88
+ 'Contact sales@jqwidgets.com for a replacement.',
89
+ )
90
+ case 'dev':
91
+ case 'eval':
92
+ if (!noticedDev) {
93
+ // eslint-disable-next-line no-console
94
+ console.info(
95
+ '@svgrid/enterprise: using a development / evaluation license. Not for production use.',
96
+ )
97
+ noticedDev = true
98
+ }
99
+ return
100
+ case 'licensed':
101
+ return
102
+ }
103
+ }
@@ -0,0 +1,61 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { buildDisplayRows, isDetailRow, toggleExpanded } from './master-detail'
3
+
4
+ type Order = { id: string; customer: string }
5
+ const orders: Order[] = [
6
+ { id: 'o1', customer: 'Ann' },
7
+ { id: 'o2', customer: 'Bob' },
8
+ { id: 'o3', customer: 'Cara' },
9
+ ]
10
+ const idOf = (o: Order) => o.id
11
+
12
+ describe('toggleExpanded', () => {
13
+ it('adds then removes an id, immutably', () => {
14
+ const a = toggleExpanded(new Set(), 'o1')
15
+ expect([...a]).toEqual(['o1'])
16
+ const b = toggleExpanded(a, 'o1')
17
+ expect([...b]).toEqual([])
18
+ expect([...a]).toEqual(['o1']) // original untouched
19
+ })
20
+ })
21
+
22
+ describe('isDetailRow', () => {
23
+ it('distinguishes detail markers from plain rows', () => {
24
+ expect(isDetailRow({ __svgridDetail: true, parent: {}, parentId: 'x' })).toBe(true)
25
+ expect(isDetailRow({ id: 'o1' })).toBe(false)
26
+ expect(isDetailRow(null)).toBe(false)
27
+ })
28
+ })
29
+
30
+ describe('buildDisplayRows', () => {
31
+ it('returns just the parents when nothing is expanded', () => {
32
+ const rows = buildDisplayRows(orders, new Set(), idOf)
33
+ expect(rows).toHaveLength(3)
34
+ expect(rows.every((r) => !isDetailRow(r))).toBe(true)
35
+ })
36
+
37
+ it('inserts a detail row right after each expanded parent, preserving order', () => {
38
+ const rows = buildDisplayRows(orders, new Set(['o2']), idOf)
39
+ expect(rows).toHaveLength(4)
40
+ expect(isDetailRow(rows[2])).toBe(true) // after o2
41
+ const detail = rows[2]
42
+ if (isDetailRow(detail)) {
43
+ expect(detail.parentId).toBe('o2')
44
+ expect((detail.parent as Order).customer).toBe('Bob')
45
+ }
46
+ // surrounding parents intact
47
+ expect((rows[1] as Order).id).toBe('o2')
48
+ expect((rows[3] as Order).id).toBe('o3')
49
+ })
50
+
51
+ it('handles multiple expanded parents', () => {
52
+ const rows = buildDisplayRows(orders, new Set(['o1', 'o3']), idOf)
53
+ expect(rows.map((r) => (isDetailRow(r) ? 'detail' : (r as Order).id))).toEqual([
54
+ 'o1',
55
+ 'detail',
56
+ 'o2',
57
+ 'o3',
58
+ 'detail',
59
+ ])
60
+ })
61
+ })
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Master-detail logic. The testable core behind `SvGridMasterDetail.svelte`:
3
+ * track which parent rows are expanded and splice a synthetic "detail" row in
4
+ * after each expanded parent, so a single `<SvGrid>` renders both the master
5
+ * rows and a full-width detail region (via the grid's `isDetailRow` /
6
+ * `renderDetailRow`). Pure - no Svelte, no DOM.
7
+ */
8
+
9
+ /** A synthetic row inserted after an expanded parent; carries the parent it belongs to. */
10
+ export type DetailRow<TParent> = { __svgridDetail: true; parent: TParent; parentId: string }
11
+
12
+ /** True for the synthetic detail rows produced by `buildDisplayRows`. */
13
+ export function isDetailRow(row: unknown): row is DetailRow<unknown> {
14
+ return !!row && typeof row === 'object' && (row as { __svgridDetail?: unknown }).__svgridDetail === true
15
+ }
16
+
17
+ /** Toggle a parent id in the expanded set, returning a new set (immutable). */
18
+ export function toggleExpanded(expanded: ReadonlySet<string>, id: string): Set<string> {
19
+ const next = new Set(expanded)
20
+ if (next.has(id)) next.delete(id)
21
+ else next.add(id)
22
+ return next
23
+ }
24
+
25
+ /**
26
+ * Weave detail rows into the master rows: each expanded parent is followed by a
27
+ * `DetailRow` marker. Order is otherwise preserved, so the grid renders parents
28
+ * in place with their detail region directly beneath.
29
+ */
30
+ export function buildDisplayRows<TParent>(
31
+ parents: ReadonlyArray<TParent>,
32
+ expanded: ReadonlySet<string>,
33
+ getRowId: (parent: TParent) => string,
34
+ ): Array<TParent | DetailRow<TParent>> {
35
+ const out: Array<TParent | DetailRow<TParent>> = []
36
+ for (const parent of parents) {
37
+ out.push(parent)
38
+ const id = getRowId(parent)
39
+ if (expanded.has(id)) out.push({ __svgridDetail: true, parent, parentId: id })
40
+ }
41
+ return out
42
+ }
package/src/print.ts CHANGED
@@ -1,127 +1,107 @@
1
- import type { RowData, TableFeatures, SvGridApi } from '@svgrid/grid'
2
- import { assertEnterpriseLicensed } from './license'
3
- import { installSmartShim } from './smart-shim'
4
- import type { ExportColumn } from './export'
5
-
6
- export type PrintOptions<TData> = {
7
- /** Title placed above the printed grid. Defaults to "Grid". */
8
- title?: string
9
- /** Columns to print. If omitted, every key of the first row is printed. */
10
- columns?: ReadonlyArray<ExportColumn>
11
- /** Rows to print. If omitted, the api's displayed rows are used. */
12
- rows?: ReadonlyArray<TData>
13
- /**
14
- * Print orientation hint. Browsers honor it via `@page { size: ... }`.
15
- * Defaults to "portrait".
16
- */
17
- orientation?: 'portrait' | 'landscape'
18
- }
19
-
20
- type DataExporterCtor = new (
21
- options: Record<string, unknown>,
22
- ) => {
23
- exportData(
24
- data: ReadonlyArray<Record<string, unknown>>,
25
- format: string,
26
- filename?: string,
27
- ): unknown
28
- }
29
-
30
- let exporterCtorPromise: Promise<DataExporterCtor> | null = null
31
-
32
- async function getDataExporter(): Promise<DataExporterCtor> {
33
- if (typeof window === 'undefined') {
34
- throw new Error('@svgrid/enterprise: print requires a browser environment')
35
- }
36
- if (!exporterCtorPromise) {
37
- exporterCtorPromise = (async () => {
38
- installSmartShim()
39
- await import('./smart.export.js')
40
- const Ctor = window.Smart?.Utilities?.DataExporter as DataExporterCtor | undefined
41
- if (!Ctor) {
42
- throw new Error('@svgrid/enterprise: failed to load Smart.Utilities.DataExporter')
43
- }
44
- return Ctor
45
- })()
46
- }
47
- return exporterCtorPromise
48
- }
49
-
50
- export async function printGrid<
51
- TFeatures extends TableFeatures,
52
- TData extends RowData,
53
- >(api: SvGridApi<TFeatures, TData>, opts?: PrintOptions<TData>): Promise<void> {
54
- assertEnterpriseLicensed('Print')
55
- const sourceRows: ReadonlyArray<TData> = opts?.rows ?? api.getDisplayedRows()
56
- let cols: ReadonlyArray<ExportColumn>
57
- if (opts?.columns && opts.columns.length > 0) {
58
- cols = opts.columns
59
- } else if (sourceRows.length > 0) {
60
- cols = Object.keys(sourceRows[0] as Record<string, unknown>)
61
- .filter((k) => !k.startsWith('_'))
62
- .map((field) => ({ field }))
63
- } else {
64
- cols = []
65
- }
66
- if (sourceRows.length === 0) {
67
- throw new Error('@svgrid/enterprise: nothing to print - the grid has no rows')
68
- }
69
- const header: Record<string, unknown> = {}
70
- for (const c of cols) header[c.field] = c.header ?? c.field
71
- const projected: Array<Record<string, unknown>> = [header]
72
- for (const r of sourceRows) {
73
- const out: Record<string, unknown> = {}
74
- const src = r as unknown as Record<string, unknown>
75
- for (const c of cols) out[c.field] = src[c.field]
76
- projected.push(out)
77
- }
78
-
79
- const Ctor = await getDataExporter()
80
- const exporter = new Ctor({ exportHeader: true })
81
- // No filename means exportData returns the HTML string instead of triggering
82
- // a download (see downloadFile in smart.export.js: when filename is falsy it
83
- // returns the raw content unchanged).
84
- const html = exporter.exportData(projected, 'html') as string
85
-
86
- const title = (opts?.title ?? 'Grid').replace(/[<>]/g, '')
87
- const orientation = opts?.orientation ?? 'portrait'
88
- const wrapped = `<!DOCTYPE html>
89
- <html><head><meta charset="utf-8"><title>${title}</title>
90
- <style>
91
- @page { size: ${orientation}; margin: 14mm; }
92
- @media print { body { -webkit-print-color-adjust: exact; print-color-adjust: exact; } }
93
- body { font-family: -apple-system, "Segoe UI", Roboto, sans-serif; color: #111; }
94
- h1 { font-size: 16pt; margin: 0 0 8mm 0; }
95
- table { border-collapse: collapse; width: 100%; font-size: 9pt; }
96
- th, td { border: 1px solid #444; padding: 4px 6px; text-align: left; }
97
- thead { background: #eee; }
98
- </style></head>
99
- <body>
100
- <h1>${title}</h1>
101
- ${html.replace(/^[\s\S]*?<body>/i, '').replace(/<\/body>[\s\S]*$/i, '')}
102
- </body></html>`
103
-
104
- const w = window.open('', '_blank', 'width=900,height=700')
105
- if (!w) {
106
- throw new Error(
107
- '@svgrid/enterprise: print() could not open a window - the browser blocked the popup',
108
- )
109
- }
110
- w.document.open()
111
- w.document.write(wrapped)
112
- w.document.close()
113
- // Give the new window one tick to paint before invoking print.
114
- w.focus()
115
- w.addEventListener('load', () => {
116
- setTimeout(() => w.print(), 50)
117
- })
118
- // Some browsers fire 'load' before the listener attaches if document was
119
- // already complete. Fall back: try a print after a short delay regardless.
120
- setTimeout(() => {
121
- try {
122
- w.print()
123
- } catch {
124
- // ignore - listener path will have fired
125
- }
126
- }, 300)
127
- }
1
+ import type { RowData, TableFeatures, SvGridApi } from '@svgrid/grid'
2
+ import { assertEnterpriseLicensed } from './license'
3
+ import {
4
+ buildLinkResolver,
5
+ projectRows,
6
+ resolveColumns,
7
+ resolveRowSource,
8
+ type ExportColumn,
9
+ type ExportRowSource,
10
+ } from './export'
11
+ import { buildPrintDocument, type PrintDocOptions } from './export-print'
12
+ import { buildConditionalResolver } from './export-conditional'
13
+ import type { ConditionalFormat } from '@svgrid/grid/format'
14
+
15
+ export type PrintOptions<TData> = {
16
+ /** Title placed above the printed grid. Defaults to "Grid". */
17
+ title?: string
18
+ /** Smaller line under the title. */
19
+ subtitle?: string
20
+ /** Logo data URL, shown above the title. */
21
+ logo?: string
22
+ /** Columns to print. Defaults to the grid's own visible columns (with their
23
+ * header labels + `format`, so values print formatted). */
24
+ columns?: ReadonlyArray<ExportColumn<TData>>
25
+ /** Rows to print. Defaults to the current view; accepts the same
26
+ * `'displayed' | 'selected' | 'all'` shorthand as `exportData`. */
27
+ rows?: ExportRowSource<TData>
28
+ /** Write raw underlying values instead of the formatted display values. */
29
+ rawValues?: boolean
30
+ /** Print orientation. Browsers honor it via `@page { size }`. Default "portrait". */
31
+ orientation?: 'portrait' | 'landscape'
32
+ /** `@page` size keyword, e.g. 'A4' | 'Letter'. Combined with orientation. */
33
+ pageSize?: string
34
+ /** Page margin, e.g. '14mm'. Default '14mm'. */
35
+ margin?: string
36
+ /** Zebra-stripe rows. Default true. */
37
+ zebra?: boolean
38
+ /** Header row background color. */
39
+ headerColor?: string
40
+ /** Conditional formatting rules (same array as `<SvGrid conditionalFormats>`);
41
+ * carried into the print view as cell background / color / bold + icons. */
42
+ conditionalFormats?: ReadonlyArray<ConditionalFormat<TData>>
43
+ }
44
+
45
+ export async function printGrid<
46
+ TFeatures extends TableFeatures,
47
+ TData extends RowData,
48
+ >(api: SvGridApi<TFeatures, TData>, opts?: PrintOptions<TData>): Promise<void> {
49
+ assertEnterpriseLicensed('Print')
50
+ if (typeof window === 'undefined') {
51
+ throw new Error('@svgrid/enterprise: print requires a browser environment')
52
+ }
53
+ const sourceRows = resolveRowSource(api, opts?.rows)
54
+ if (sourceRows.length === 0) {
55
+ throw new Error('@svgrid/enterprise: nothing to print - the grid has no rows')
56
+ }
57
+ const cols = resolveColumns(api, { columns: opts?.columns }, sourceRows)
58
+ const fields = cols.map((c) => c.field)
59
+ // Formatted display values (or raw when asked) so the print matches the grid.
60
+ const projected = await projectRows(sourceRows, cols, { rawValues: opts?.rawValues })
61
+ const dataRows = projected.slice(1).map((r) => fields.map((f) => String(r[f] ?? '')))
62
+ const columns = cols.map((c) => ({ header: c.header ?? c.field, align: c.align }))
63
+
64
+ const docOpts: PrintDocOptions = {
65
+ title: opts?.title ?? 'Grid',
66
+ subtitle: opts?.subtitle,
67
+ logo: opts?.logo,
68
+ orientation: opts?.orientation ?? 'portrait',
69
+ pageSize: opts?.pageSize,
70
+ margin: opts?.margin,
71
+ zebra: opts?.zebra,
72
+ headerColor: opts?.headerColor,
73
+ }
74
+ const visual = buildConditionalResolver(cols, sourceRows, opts?.conditionalFormats)
75
+ const link = buildLinkResolver(cols, sourceRows)
76
+ const html = buildPrintDocument({
77
+ columns,
78
+ rows: dataRows,
79
+ cellStyle: (r, c) => visual(r, c),
80
+ cellLink: link,
81
+ opts: docOpts,
82
+ })
83
+
84
+ const w = window.open('', '_blank', 'width=900,height=700')
85
+ if (!w) {
86
+ throw new Error(
87
+ '@svgrid/enterprise: print() could not open a window - the browser blocked the popup',
88
+ )
89
+ }
90
+ w.document.open()
91
+ w.document.write(html)
92
+ w.document.close()
93
+ // Give the new window one tick to paint before invoking print.
94
+ w.focus()
95
+ w.addEventListener('load', () => {
96
+ setTimeout(() => w.print(), 50)
97
+ })
98
+ // Some browsers fire 'load' before the listener attaches if the document was
99
+ // already complete. Fall back: try a print after a short delay regardless.
100
+ setTimeout(() => {
101
+ try {
102
+ w.print()
103
+ } catch {
104
+ // ignore - the listener path will have fired
105
+ }
106
+ }, 300)
107
+ }