@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.
- package/dist/cdn/svgrid-enterprise.svelte-external.js +26039 -805
- package/dist/designer/assets/index-Dp44bTid.js +939 -0
- package/dist/designer/assets/index-RJp6x8tw.css +1 -0
- package/dist/designer/assets/jszip.min-CjMo-QGg.js +2 -0
- package/dist/designer/index.html +13 -0
- package/dist/node/studio.js +22554 -0
- package/package.json +10 -3
- package/src/SvAuthGate.svelte +115 -0
- package/src/SvBoard.dom.test.ts +67 -0
- package/src/SvBoard.svelte +192 -0
- package/src/SvExportMenu.svelte +553 -0
- package/src/SvFileInput.svelte +113 -0
- package/src/SvGridEditPanel.dom.test.ts +146 -0
- package/src/SvGridEditPanel.svelte +793 -0
- package/src/SvGridMasterDetail.svelte +95 -0
- package/src/SvImportDialog.svelte +1020 -0
- package/src/SvLookupInput.svelte +180 -0
- package/src/SvRecordDetail.dom.test.ts +137 -0
- package/src/SvRecordDetail.svelte +288 -0
- package/src/SvSchedule.dom.test.ts +57 -0
- package/src/SvSchedule.svelte +156 -0
- package/src/SvSchemaChart.svelte +233 -0
- package/src/SvSchemaDashboard.svelte +130 -0
- package/src/ai-export-pdf.test.ts +74 -0
- package/src/ai-export-xlsx.test.ts +87 -0
- package/src/ai-export.test.ts +110 -0
- package/src/ai.ts +1188 -782
- package/src/edit-panel.test.ts +213 -0
- package/src/edit-panel.ts +228 -0
- package/src/export-conditional.test.ts +60 -0
- package/src/export-conditional.ts +94 -0
- package/src/export-ooxml.test.ts +152 -0
- package/src/export-ooxml.ts +485 -0
- package/src/export-pdf.test.ts +138 -0
- package/src/export-pdf.ts +245 -0
- package/src/export-print.test.ts +66 -0
- package/src/export-print.ts +132 -0
- package/src/export-serialize.test.ts +56 -0
- package/src/export-serialize.ts +24 -0
- package/src/export-xls.ts +196 -0
- package/src/export-xlsx-roundtrip.test.ts +120 -0
- package/src/export-xlsx.test.ts +150 -0
- package/src/export.test.ts +349 -0
- package/src/export.ts +1452 -549
- package/src/import-automap.test.ts +131 -0
- package/src/import-hardening.test.ts +158 -0
- package/src/import.ts +1042 -648
- package/src/index.ts +282 -0
- package/src/install.ts +134 -114
- package/src/license-core.test.ts +23 -0
- package/src/license-core.ts +35 -0
- package/src/license.ts +103 -90
- package/src/master-detail.test.ts +61 -0
- package/src/master-detail.ts +42 -0
- package/src/print.ts +107 -127
- package/src/schema-designer.test.ts +121 -0
- package/src/schema-designer.ts +142 -0
- package/src/schema.test.ts +268 -0
- package/src/schema.ts +499 -0
- package/src/smart-shim.ts +107 -105
- package/src/sources/aggregate.test.ts +79 -0
- package/src/sources/aggregate.ts +102 -0
- package/src/sources/auth-supabase.test.ts +80 -0
- package/src/sources/auth-supabase.ts +87 -0
- package/src/sources/dashboard.kpi.test.ts +50 -0
- package/src/sources/dashboard.test.ts +61 -0
- package/src/sources/dashboard.ts +136 -0
- package/src/sources/field-inference.test.ts +60 -0
- package/src/sources/field-inference.ts +63 -0
- package/src/sources/filters.test.ts +58 -0
- package/src/sources/filters.ts +64 -0
- package/src/sources/index.ts +67 -0
- package/src/sources/introspect-supabase.test.ts +100 -0
- package/src/sources/introspect-supabase.ts +119 -0
- package/src/sources/realtime-supabase.test.ts +93 -0
- package/src/sources/realtime-supabase.ts +99 -0
- package/src/sources/relation-lookup.test.ts +99 -0
- package/src/sources/relation-lookup.ts +128 -0
- package/src/sources/rest-adapters.test.ts +95 -0
- package/src/sources/rest-adapters.ts +120 -0
- package/src/sources/rest.test.ts +104 -0
- package/src/sources/rest.ts +129 -0
- package/src/sources/schema-from-columns.test.ts +106 -0
- package/src/sources/schema-from-columns.ts +88 -0
- package/src/sources/supabase.test.ts +110 -0
- package/src/sources/supabase.ts +99 -0
- package/src/sources/with-entity-rules.test.ts +104 -0
- package/src/sources/with-entity-rules.ts +78 -0
- package/src/sources/with-relation-labels.test.ts +75 -0
- package/src/sources/with-relation-labels.ts +73 -0
- package/src/studio/bug-report.test.ts +101 -0
- package/src/studio/bug-report.ts +165 -0
- package/src/studio/cli.test.ts +187 -0
- package/src/studio/cli.ts +112 -0
- package/src/studio/csv.test.ts +90 -0
- package/src/studio/csv.ts +165 -0
- package/src/studio/db-connect-string.test.ts +94 -0
- package/src/studio/db-connect-string.ts +128 -0
- package/src/studio/emit-project.test.ts +923 -0
- package/src/studio/emit-project.ts +1273 -0
- package/src/studio/emit-schema.test.ts +94 -0
- package/src/studio/emit-schema.ts +920 -0
- package/src/studio/index.ts +195 -0
- package/src/studio/introspect-db.test.ts +186 -0
- package/src/studio/introspect-db.ts +312 -0
- package/src/studio/introspect-prisma.test.ts +99 -0
- package/src/studio/introspect-prisma.ts +175 -0
- package/src/studio/introspect.test.ts +172 -0
- package/src/studio/introspect.ts +310 -0
- package/src/studio/pipeline.test.ts +42 -0
- package/src/studio/project-robust.test.ts +157 -0
- package/src/studio/project.test.ts +473 -0
- package/src/studio/project.ts +916 -0
- package/src/studio/sample-data.test.ts +58 -0
- package/src/studio/sample-data.ts +200 -0
- package/src/studio/samples/ats.ts +202 -0
- package/src/studio/samples/clinic.ts +177 -0
- package/src/studio/samples/crm.ts +250 -0
- package/src/studio/samples/ecommerce.ts +187 -0
- package/src/studio/samples/events.ts +199 -0
- package/src/studio/samples/fleet.ts +184 -0
- package/src/studio/samples/gym.ts +213 -0
- package/src/studio/samples/hr.ts +193 -0
- package/src/studio/samples/index.ts +55 -0
- package/src/studio/samples/insurance.ts +195 -0
- package/src/studio/samples/inventory.ts +182 -0
- package/src/studio/samples/invoicing.ts +162 -0
- package/src/studio/samples/library.ts +180 -0
- package/src/studio/samples/live-data.test.ts +98 -0
- package/src/studio/samples/live-data.ts +308 -0
- package/src/studio/samples/projects.ts +190 -0
- package/src/studio/samples/realestate.ts +196 -0
- package/src/studio/samples/restaurant.ts +187 -0
- package/src/studio/samples/samples.test.ts +208 -0
- package/src/studio/samples/school.ts +197 -0
- package/src/studio/samples/seed-floor.test.ts +25 -0
- package/src/studio/samples/shared.ts +305 -0
- package/src/studio/samples/subscriptions.ts +183 -0
- package/src/studio/samples/support.ts +182 -0
- package/src/studio/scaffold-app.test.ts +91 -0
- package/src/studio/scaffold-app.ts +161 -0
- package/src/studio/scaffold.test.ts +178 -0
- package/src/studio/scaffold.ts +374 -0
- package/src/studio/themes.ts +172 -0
- package/src/studio/verify.test.ts +47 -0
- package/src/studio/verify.ts +79 -0
- package/src/sveltekit/in-memory.test.ts +104 -0
- package/src/sveltekit/in-memory.ts +129 -0
- package/src/sveltekit/index.ts +23 -0
- package/src/sveltekit/query-plan.test.ts +109 -0
- package/src/sveltekit/query-plan.ts +125 -0
- package/src/sveltekit/sql-source.test.ts +185 -0
- package/src/sveltekit/sql-source.ts +166 -0
- package/src/sveltekit/sql.test.ts +80 -0
- package/src/sveltekit/sql.ts +131 -0
- package/src/sveltekit/transport.test.ts +175 -0
- package/src/sveltekit/transport.ts +254 -0
- package/src/sveltekit/types.ts +11 -0
- 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 {
|
|
18
|
-
import { emitUnlicensedNudge } from './watermark'
|
|
19
|
-
import { showUpgradePrompt, type EnterpriseFeatureLabel } from './upgrade-prompt'
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
let
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
|
|
2
|
-
import { assertEnterpriseLicensed } from './license'
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
+
}
|