@svgrid/enterprise 1.0.1 → 1.1.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/LICENSE +97 -35
- package/README.md +50 -18
- package/package.json +1 -1
- package/src/SvPivotDesigner.svelte +1045 -0
- package/src/ai.test.ts +1 -1
- package/src/ai.ts +6 -6
- package/src/export.ts +2 -2
- package/src/import.test.ts +1 -1
- package/src/import.ts +2 -2
- package/src/index.ts +17 -3
- package/src/install.ts +8 -8
- package/src/license.ts +9 -9
- package/src/pivot-designer.ts +101 -0
- package/src/pivot.test.ts +31 -3
- package/src/pivot.ts +39 -10
- package/src/print.ts +2 -2
- package/src/revoked.ts +3 -3
- package/src/upgrade-prompt.test.ts +1 -1
- package/src/upgrade-prompt.ts +5 -5
- package/src/watermark.test.ts +1 -1
- package/src/watermark.ts +3 -3
package/src/ai.test.ts
CHANGED
|
@@ -70,7 +70,7 @@ beforeEach(() => {
|
|
|
70
70
|
// Default to a licensed state so the soft-gate doesn't dump noise on the
|
|
71
71
|
// console during expected-failure tests. Individual cases clear it when
|
|
72
72
|
// they want to assert soft-gate behaviour.
|
|
73
|
-
setLicenseKey('
|
|
73
|
+
setLicenseKey('SVENTERPRISE-DEV-TEST')
|
|
74
74
|
})
|
|
75
75
|
afterEach(() => {
|
|
76
76
|
setAIProvider(null)
|
package/src/ai.ts
CHANGED
|
@@ -20,13 +20,13 @@
|
|
|
20
20
|
* `AIProvider` that knows how to call OpenAI / Anthropic / a local
|
|
21
21
|
* model / a server-side proxy. We never bundle a model client.
|
|
22
22
|
*
|
|
23
|
-
* License gate: every AI call routes through `
|
|
23
|
+
* License gate: every AI call routes through `assertEnterpriseLicensed()`,
|
|
24
24
|
* which soft-gates (still works, adds a watermark, console nudge) when
|
|
25
25
|
* no key is set so demos and evaluation stay frictionless.
|
|
26
26
|
*/
|
|
27
27
|
|
|
28
28
|
import type { RowData, SvGridApi, TableFeatures } from '@svgrid/grid'
|
|
29
|
-
import {
|
|
29
|
+
import { assertEnterpriseLicensed } from './license'
|
|
30
30
|
|
|
31
31
|
// ---------------------------------------------------------------------------
|
|
32
32
|
// Provider contract
|
|
@@ -230,7 +230,7 @@ export async function aiFilter<
|
|
|
230
230
|
query: string,
|
|
231
231
|
opts: AIFilterOptions = {},
|
|
232
232
|
): Promise<AIFilterResult> {
|
|
233
|
-
|
|
233
|
+
assertEnterpriseLicensed('AI assistant')
|
|
234
234
|
const schema = buildColumnSchema(api)
|
|
235
235
|
const prompt =
|
|
236
236
|
`You are a data-grid filter planner. Translate the user's natural-language ` +
|
|
@@ -317,7 +317,7 @@ export async function aiSmartFill<
|
|
|
317
317
|
api: SvGridApi<TFeatures, TData>,
|
|
318
318
|
opts: AISmartFillOptions,
|
|
319
319
|
): Promise<AISmartFillResult<TValue>> {
|
|
320
|
-
|
|
320
|
+
assertEnterpriseLicensed('AI assistant')
|
|
321
321
|
if (opts.examples.length === 0) {
|
|
322
322
|
throw new Error('@svgrid/enterprise/ai: aiSmartFill requires at least one example.')
|
|
323
323
|
}
|
|
@@ -408,7 +408,7 @@ export async function aiSummarize<
|
|
|
408
408
|
api: SvGridApi<TFeatures, TData>,
|
|
409
409
|
opts: AISummarizeOptions,
|
|
410
410
|
): Promise<AISummary> {
|
|
411
|
-
|
|
411
|
+
assertEnterpriseLicensed('AI assistant')
|
|
412
412
|
const all = api.getData()
|
|
413
413
|
const rows: TData[] = (() => {
|
|
414
414
|
const t = opts.target
|
|
@@ -497,7 +497,7 @@ export async function aiClassify<
|
|
|
497
497
|
api: SvGridApi<TFeatures, TData>,
|
|
498
498
|
opts: AIClassifyOptions,
|
|
499
499
|
): Promise<AIClassifyResult> {
|
|
500
|
-
|
|
500
|
+
assertEnterpriseLicensed('AI assistant')
|
|
501
501
|
const data = api.getData()
|
|
502
502
|
const targets = opts.targetRowIndices ?? data.map((_, i) => i)
|
|
503
503
|
const rubric = opts.classDescriptions
|
package/src/export.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/// <reference path="./pdfmake-shims.d.ts" />
|
|
2
2
|
import type { RowData, TableFeatures } from '@svgrid/grid'
|
|
3
3
|
import type { SvGridApi } from '@svgrid/grid'
|
|
4
|
-
import {
|
|
4
|
+
import { assertEnterpriseLicensed } from './license'
|
|
5
5
|
import { installSmartShim, type SmartDataExporterInstance } from './smart-shim'
|
|
6
6
|
|
|
7
7
|
export type ExportFormat = 'xlsx' | 'pdf' | 'csv' | 'tsv' | 'html'
|
|
@@ -432,7 +432,7 @@ export async function exportGrid<
|
|
|
432
432
|
TFeatures extends TableFeatures,
|
|
433
433
|
TData extends RowData,
|
|
434
434
|
>(api: SvGridApi<TFeatures, TData>, opts: ExportOptions<TData>): Promise<void> {
|
|
435
|
-
|
|
435
|
+
assertEnterpriseLicensed('Export')
|
|
436
436
|
await ensureGlobals(opts.format)
|
|
437
437
|
const Ctor = await getDataExporter()
|
|
438
438
|
|
package/src/import.test.ts
CHANGED
package/src/import.ts
CHANGED
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
*/
|
|
26
26
|
|
|
27
27
|
import type { RowData, SvGridApi, TableFeatures } from '@svgrid/grid'
|
|
28
|
-
import {
|
|
28
|
+
import { assertEnterpriseLicensed } from './license'
|
|
29
29
|
|
|
30
30
|
// ---------------------------------------------------------------------------
|
|
31
31
|
// Public types
|
|
@@ -557,7 +557,7 @@ export async function importData<
|
|
|
557
557
|
api: SvGridApi<TFeatures, TData>,
|
|
558
558
|
opts: ImportOptions<TData>,
|
|
559
559
|
): Promise<ImportResult<TData>> {
|
|
560
|
-
|
|
560
|
+
assertEnterpriseLicensed('Import')
|
|
561
561
|
const format = sniffFormat(opts.file, opts.format ?? 'auto')
|
|
562
562
|
|
|
563
563
|
let matrix: string[][]
|
package/src/index.ts
CHANGED
|
@@ -4,7 +4,7 @@ export {
|
|
|
4
4
|
getLicenseKey,
|
|
5
5
|
isLicenseKeySet,
|
|
6
6
|
hasValidLicense,
|
|
7
|
-
|
|
7
|
+
assertEnterpriseLicensed,
|
|
8
8
|
} from './license'
|
|
9
9
|
|
|
10
10
|
export {
|
|
@@ -29,7 +29,7 @@ export {
|
|
|
29
29
|
type ImportRowError,
|
|
30
30
|
type ImportValidator,
|
|
31
31
|
} from './import'
|
|
32
|
-
export {
|
|
32
|
+
export { installEnterprise, type EnterpriseGridApi, type EnterpriseAIApi, type EnterprisePivotApi } from './install'
|
|
33
33
|
export {
|
|
34
34
|
createStagedEditing,
|
|
35
35
|
type StagedChange,
|
|
@@ -40,7 +40,7 @@ export { dismissUnlicensedNudge } from './watermark'
|
|
|
40
40
|
export {
|
|
41
41
|
showUpgradePrompt,
|
|
42
42
|
dismissUpgradePrompt,
|
|
43
|
-
type
|
|
43
|
+
type EnterpriseFeatureLabel,
|
|
44
44
|
} from './upgrade-prompt'
|
|
45
45
|
|
|
46
46
|
export {
|
|
@@ -56,6 +56,20 @@ export {
|
|
|
56
56
|
type PivotValueConfig,
|
|
57
57
|
} from './pivot'
|
|
58
58
|
|
|
59
|
+
export { default as SvPivotDesigner } from './SvPivotDesigner.svelte'
|
|
60
|
+
export {
|
|
61
|
+
ALL_AGGREGATORS,
|
|
62
|
+
AGG_LABEL,
|
|
63
|
+
EMPTY_LAYOUT,
|
|
64
|
+
defaultLayoutFor,
|
|
65
|
+
type PivotField,
|
|
66
|
+
type PivotValueChip,
|
|
67
|
+
type PivotFilterChip,
|
|
68
|
+
type PivotLayout,
|
|
69
|
+
type PivotPreset,
|
|
70
|
+
type Well,
|
|
71
|
+
} from './pivot-designer'
|
|
72
|
+
|
|
59
73
|
export {
|
|
60
74
|
// Provider plumbing
|
|
61
75
|
setAIProvider,
|
package/src/install.ts
CHANGED
|
@@ -17,7 +17,7 @@ import {
|
|
|
17
17
|
type PivotResult,
|
|
18
18
|
} from './pivot'
|
|
19
19
|
|
|
20
|
-
export type
|
|
20
|
+
export type EnterpriseAIApi<TData extends RowData> = {
|
|
21
21
|
/** Natural-language -> filter + sort plan (and optionally apply it). */
|
|
22
22
|
filter(query: string, opts?: AIFilterOptions): Promise<AIFilterResult>
|
|
23
23
|
/** Propose values for empty cells based on a few worked examples. */
|
|
@@ -32,7 +32,7 @@ export type ProAIApi<TData extends RowData> = {
|
|
|
32
32
|
readonly _rowType?: TData
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
export type
|
|
35
|
+
export type EnterprisePivotApi<
|
|
36
36
|
TFeatures extends TableFeatures,
|
|
37
37
|
TData extends RowData,
|
|
38
38
|
> = {
|
|
@@ -54,7 +54,7 @@ export type ProPivotApi<
|
|
|
54
54
|
): PivotResult<TFeatures>
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
-
export type
|
|
57
|
+
export type EnterpriseGridApi<
|
|
58
58
|
TFeatures extends TableFeatures,
|
|
59
59
|
TData extends RowData,
|
|
60
60
|
> = SvGridApi<TFeatures, TData> & {
|
|
@@ -68,10 +68,10 @@ export type ProGridApi<
|
|
|
68
68
|
importData(opts: ImportOptions<TData>): Promise<ImportResult<TData>>
|
|
69
69
|
/** AI helpers (Pro). All calls route through a consumer-registered
|
|
70
70
|
* AIProvider via setAIProvider(); no model client is bundled. */
|
|
71
|
-
ai:
|
|
71
|
+
ai: EnterpriseAIApi<TData>
|
|
72
72
|
/** Pivot table builder (Pro). Pure - returns `{ rows, columns }` you
|
|
73
73
|
* hand to a separate `<SvGrid>` instance. */
|
|
74
|
-
pivot:
|
|
74
|
+
pivot: EnterprisePivotApi<TFeatures, TData>
|
|
75
75
|
}
|
|
76
76
|
|
|
77
77
|
/**
|
|
@@ -84,11 +84,11 @@ export type ProGridApi<
|
|
|
84
84
|
* the console emits a one-time nudge. Revoked or malformed keys throw on
|
|
85
85
|
* first call.
|
|
86
86
|
*/
|
|
87
|
-
export function
|
|
87
|
+
export function installEnterprise<
|
|
88
88
|
TFeatures extends TableFeatures,
|
|
89
89
|
TData extends RowData,
|
|
90
|
-
>(api: SvGridApi<TFeatures, TData>):
|
|
91
|
-
const pro = api as
|
|
90
|
+
>(api: SvGridApi<TFeatures, TData>): EnterpriseGridApi<TFeatures, TData> {
|
|
91
|
+
const pro = api as EnterpriseGridApi<TFeatures, TData>
|
|
92
92
|
pro.exportData = (opts) => exportGrid(pro, opts)
|
|
93
93
|
pro.print = (opts) => printGrid(pro, opts)
|
|
94
94
|
pro.importData = (opts) => importData(pro, opts)
|
package/src/license.ts
CHANGED
|
@@ -8,17 +8,17 @@
|
|
|
8
8
|
// ────────────────────────────────────────────────────────────────────
|
|
9
9
|
// null (no key set) -> soft-gate: watermark + console.log,
|
|
10
10
|
// feature still runs
|
|
11
|
-
// does not start with "
|
|
11
|
+
// does not start with "SVENTERPRISE-" -> throws (programmer error)
|
|
12
12
|
// in REVOKED_KEYS -> throws (revoked / leaked / expired)
|
|
13
|
-
// starts with "
|
|
14
|
-
// "
|
|
15
|
-
// any other "
|
|
13
|
+
// starts with "SVENTERPRISE-DEV" or
|
|
14
|
+
// "SVENTERPRISE-EVAL" -> works; one-time console.info notice
|
|
15
|
+
// any other "SVENTERPRISE-..." -> works silently (paid production)
|
|
16
16
|
|
|
17
17
|
import { REVOKED_KEYS } from './revoked'
|
|
18
18
|
import { emitUnlicensedNudge } from './watermark'
|
|
19
|
-
import { showUpgradePrompt, type
|
|
19
|
+
import { showUpgradePrompt, type EnterpriseFeatureLabel } from './upgrade-prompt'
|
|
20
20
|
|
|
21
|
-
const VALID_PREFIX = '
|
|
21
|
+
const VALID_PREFIX = 'SVENTERPRISE-'
|
|
22
22
|
let currentKey: string | null = null
|
|
23
23
|
let noticedDev = false
|
|
24
24
|
|
|
@@ -56,7 +56,7 @@ export function hasValidLicense(): boolean {
|
|
|
56
56
|
return true
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
-
export function
|
|
59
|
+
export function assertEnterpriseLicensed(feature?: EnterpriseFeatureLabel): void {
|
|
60
60
|
if (currentKey == null) {
|
|
61
61
|
// Soft-gate: the feature still runs, but the user gets a watermark +
|
|
62
62
|
// a one-time console.log nudge directing them to pricing, plus a
|
|
@@ -68,7 +68,7 @@ export function assertProLicensed(feature?: ProFeatureLabel): void {
|
|
|
68
68
|
}
|
|
69
69
|
if (!currentKey.startsWith(VALID_PREFIX)) {
|
|
70
70
|
throw new Error(
|
|
71
|
-
'@svgrid/enterprise: invalid license key format (expected "
|
|
71
|
+
'@svgrid/enterprise: invalid license key format (expected "SVENTERPRISE-..." prefix).',
|
|
72
72
|
)
|
|
73
73
|
}
|
|
74
74
|
if (REVOKED_KEYS.has(currentKey)) {
|
|
@@ -79,7 +79,7 @@ export function assertProLicensed(feature?: ProFeatureLabel): void {
|
|
|
79
79
|
}
|
|
80
80
|
if (
|
|
81
81
|
!noticedDev &&
|
|
82
|
-
(currentKey.startsWith('
|
|
82
|
+
(currentKey.startsWith('SVENTERPRISE-DEV') || currentKey.startsWith('SVENTERPRISE-EVAL'))
|
|
83
83
|
) {
|
|
84
84
|
// eslint-disable-next-line no-console
|
|
85
85
|
console.info(
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Types + helpers for the SvPivotDesigner component.
|
|
3
|
+
*
|
|
4
|
+
* Why a separate module: the component file (.svelte) imports these,
|
|
5
|
+
* and so do consumers wiring up custom presets, formatters, or wells.
|
|
6
|
+
* Keeping the pure-TS surface here also lets us unit-test the layout
|
|
7
|
+
* normalisation + serialisation without instantiating the component.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { CellFormatConfig } from '@svgrid/grid'
|
|
11
|
+
import type { PivotAggregatorId } from './pivot'
|
|
12
|
+
|
|
13
|
+
/** One field the designer offers in its left-rail picker.
|
|
14
|
+
* Dimensions land in Rows by default; measures land in Values. */
|
|
15
|
+
export type PivotField<T extends Record<string, unknown> = Record<string, unknown>> = {
|
|
16
|
+
field: keyof T & string
|
|
17
|
+
/** Display name shown in the picker + on chips. */
|
|
18
|
+
label: string
|
|
19
|
+
/** Default well: 'dimension' -> Rows, 'measure' -> Values. */
|
|
20
|
+
kind: 'dimension' | 'measure'
|
|
21
|
+
/** Optional group name for the field picker (e.g. 'Demographics'). */
|
|
22
|
+
group?: string
|
|
23
|
+
/** Optional formatter applied to value-well chips by default. */
|
|
24
|
+
format?: CellFormatConfig
|
|
25
|
+
/** Optional default aggregator when added to Values (default 'sum'). */
|
|
26
|
+
defaultAgg?: PivotAggregatorId
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** A single chip in the Values well. */
|
|
30
|
+
export type PivotValueChip = {
|
|
31
|
+
field: string
|
|
32
|
+
agg: PivotAggregatorId
|
|
33
|
+
label?: string
|
|
34
|
+
format?: CellFormatConfig
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** A single chip in the Filters well. `allowed === null` => all values pass. */
|
|
38
|
+
export type PivotFilterChip = {
|
|
39
|
+
field: string
|
|
40
|
+
/** Distinct values that pass the filter. `null` means no restriction. */
|
|
41
|
+
allowed: string[] | null
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** The full pivot layout. The designer is a controlled component that
|
|
45
|
+
* reads + writes this single shape via `bind:layout`. */
|
|
46
|
+
export type PivotLayout = {
|
|
47
|
+
rows: string[]
|
|
48
|
+
cols: string[]
|
|
49
|
+
values: PivotValueChip[]
|
|
50
|
+
filters: PivotFilterChip[]
|
|
51
|
+
/** Hide subtotal rows between groups. Default false. */
|
|
52
|
+
hideSubtotals?: boolean
|
|
53
|
+
/** Hide grand-total row + column. Default false. */
|
|
54
|
+
hideGrandTotals?: boolean
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** A saved layout the user can pick from the toolbar's preset menu. */
|
|
58
|
+
export type PivotPreset = {
|
|
59
|
+
name: string
|
|
60
|
+
layout: PivotLayout
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** A built-in aggregator option. Consumers can pass a subset to
|
|
64
|
+
* `aggregators` to restrict the chip menu (e.g. 'count' only). */
|
|
65
|
+
export const ALL_AGGREGATORS: PivotAggregatorId[] = [
|
|
66
|
+
'sum', 'avg', 'min', 'max', 'count', 'countDistinct', 'first', 'last',
|
|
67
|
+
]
|
|
68
|
+
|
|
69
|
+
/** Empty layout used by Reset + the initial state. */
|
|
70
|
+
export const EMPTY_LAYOUT: PivotLayout = {
|
|
71
|
+
rows: [], cols: [], values: [], filters: [],
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Build an initial layout from a field list - useful when the
|
|
75
|
+
* consumer just wants "throw the first dimension in Rows + first
|
|
76
|
+
* measure in Values" defaults. */
|
|
77
|
+
export function defaultLayoutFor<T extends Record<string, unknown>>(
|
|
78
|
+
fields: ReadonlyArray<PivotField<T>>,
|
|
79
|
+
): PivotLayout {
|
|
80
|
+
const dim = fields.find((f) => f.kind === 'dimension')
|
|
81
|
+
const meas = fields.find((f) => f.kind === 'measure')
|
|
82
|
+
return {
|
|
83
|
+
rows: dim ? [dim.field] : [],
|
|
84
|
+
cols: [],
|
|
85
|
+
values: meas
|
|
86
|
+
? [{ field: meas.field, agg: meas.defaultAgg ?? 'sum', label: meas.label, format: meas.format }]
|
|
87
|
+
: [],
|
|
88
|
+
filters: [],
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Move / add / remove a chip across the four wells. The well names
|
|
93
|
+
* match the layout shape. */
|
|
94
|
+
export type Well = 'rows' | 'cols' | 'values' | 'filters'
|
|
95
|
+
|
|
96
|
+
/** Pretty label for an aggregator id. */
|
|
97
|
+
export const AGG_LABEL: Record<PivotAggregatorId, string> = {
|
|
98
|
+
sum: 'Sum', avg: 'Average', min: 'Min', max: 'Max',
|
|
99
|
+
count: 'Count', countDistinct: 'Distinct count',
|
|
100
|
+
first: 'First', last: 'Last',
|
|
101
|
+
}
|
package/src/pivot.test.ts
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
createPivotModel,
|
|
8
8
|
filterCollapsedPivotRows,
|
|
9
9
|
pivotAggregators,
|
|
10
|
+
type PivotConfig,
|
|
10
11
|
type PivotRow,
|
|
11
12
|
} from './pivot'
|
|
12
13
|
|
|
@@ -162,15 +163,42 @@ describe('createPivotModel - configuration toggles', () => {
|
|
|
162
163
|
expect(r.rows.find((row) => row.__pivotKind === 'grandTotal')).toBeUndefined()
|
|
163
164
|
})
|
|
164
165
|
|
|
165
|
-
it('
|
|
166
|
+
it('keeps group headers but blanks their values when rowSubtotals=false', () => {
|
|
166
167
|
const r = createPivotModel(facts, {
|
|
167
168
|
rows: ['region', 'salesPerson'],
|
|
168
169
|
cols: ['quarter'],
|
|
169
170
|
values: [{ field: 'amount', agg: 'sum' }],
|
|
170
171
|
rowSubtotals: false,
|
|
171
172
|
})
|
|
172
|
-
//
|
|
173
|
-
|
|
173
|
+
// Group header rows are STILL emitted - the grouping hierarchy is
|
|
174
|
+
// preserved; turning off subtotals never removes a grouping level.
|
|
175
|
+
const groups = r.rows.filter((row) => row.__pivotKind === 'group')
|
|
176
|
+
expect(groups.length).toBeGreaterThan(0)
|
|
177
|
+
expect(groups.map((row) => row.__pivotLabel)).toContain('AMER')
|
|
178
|
+
// ...but each header carries no aggregate values: the value cells are
|
|
179
|
+
// present (so columns line up) and explicitly null.
|
|
180
|
+
for (const g of groups) {
|
|
181
|
+
const valueKeys = Object.keys(g).filter((k) => !k.startsWith('__pivot'))
|
|
182
|
+
expect(valueKeys.length).toBeGreaterThan(0)
|
|
183
|
+
expect(valueKeys.every((k) => (g as Record<string, unknown>)[k] === null)).toBe(true)
|
|
184
|
+
}
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
it('keeps the grand-total value identical whether or not subtotals show', () => {
|
|
188
|
+
const base: PivotConfig<Sale> = {
|
|
189
|
+
rows: ['region', 'salesPerson'],
|
|
190
|
+
cols: ['quarter'],
|
|
191
|
+
values: [{ field: 'amount', agg: 'sum' }],
|
|
192
|
+
}
|
|
193
|
+
const withSub = createPivotModel(facts, { ...base, rowSubtotals: true })
|
|
194
|
+
const noSub = createPivotModel(facts, { ...base, rowSubtotals: false })
|
|
195
|
+
const totalValues = (m: typeof withSub) => {
|
|
196
|
+
const g = m.rows.find((row) => row.__pivotKind === 'grandTotal')!
|
|
197
|
+
return Object.fromEntries(
|
|
198
|
+
Object.entries(g).filter(([k]) => !k.startsWith('__pivot')),
|
|
199
|
+
)
|
|
200
|
+
}
|
|
201
|
+
expect(totalValues(noSub)).toEqual(totalValues(withSub))
|
|
174
202
|
})
|
|
175
203
|
|
|
176
204
|
it('honours a custom rowSort comparator', () => {
|
package/src/pivot.ts
CHANGED
|
@@ -349,6 +349,28 @@ function computeRowValues(
|
|
|
349
349
|
return result
|
|
350
350
|
}
|
|
351
351
|
|
|
352
|
+
/**
|
|
353
|
+
* Build the same value-column keys as `computeRowValues`, but with every cell
|
|
354
|
+
* `null`. Used for group HEADER rows when `rowSubtotals` is off: the row keeps
|
|
355
|
+
* its place in the hierarchy (label + expand/collapse) yet shows no aggregate
|
|
356
|
+
* numbers. Cells are `null` rather than absent so consumers can distinguish a
|
|
357
|
+
* deliberately-blank subtotal from a missing column.
|
|
358
|
+
*/
|
|
359
|
+
function blankRowValues(
|
|
360
|
+
colLeafPaths: ReadonlyArray<ReadonlyArray<unknown>>,
|
|
361
|
+
values: ReadonlyArray<PivotValueConfig<unknown>>,
|
|
362
|
+
includeGrandTotalCol: boolean,
|
|
363
|
+
): Record<string, null> {
|
|
364
|
+
const result: Record<string, null> = {}
|
|
365
|
+
for (const colPath of colLeafPaths) {
|
|
366
|
+
for (let i = 0; i < values.length; i += 1) result[leafColumnId(colPath, i)] = null
|
|
367
|
+
}
|
|
368
|
+
if (includeGrandTotalCol) {
|
|
369
|
+
for (let i = 0; i < values.length; i += 1) result[leafColumnId(['__total'], i)] = null
|
|
370
|
+
}
|
|
371
|
+
return result
|
|
372
|
+
}
|
|
373
|
+
|
|
352
374
|
function walkRowTree(
|
|
353
375
|
node: AxisNode,
|
|
354
376
|
out: PivotRow[],
|
|
@@ -387,11 +409,16 @@ function walkRowTree(
|
|
|
387
409
|
return
|
|
388
410
|
}
|
|
389
411
|
|
|
390
|
-
// Group node. Emit
|
|
391
|
-
//
|
|
412
|
+
// Group node. Emit the group HEADER row whenever this node is a real group
|
|
413
|
+
// (not the synthetic root, not the deepest level). The header is emitted
|
|
414
|
+
// regardless of `rowSubtotals` so the grouping hierarchy and the
|
|
415
|
+
// expand/collapse parent chain stay intact; the flag only controls whether
|
|
416
|
+
// the header carries its aggregate (subtotal) values or renders as a
|
|
417
|
+
// label-only row. This matches Excel / AG Grid, where hiding subtotals never
|
|
418
|
+
// removes a grouping level from the row axis.
|
|
392
419
|
const isRealGroup = node.level > 0 && node.level < rowFields.length
|
|
393
420
|
let nextParentId: string | null = parentId
|
|
394
|
-
if (isRealGroup
|
|
421
|
+
if (isRealGroup) {
|
|
395
422
|
const id = `group__${node.pathKey}`
|
|
396
423
|
out.push({
|
|
397
424
|
__pivotId: id,
|
|
@@ -400,13 +427,15 @@ function walkRowTree(
|
|
|
400
427
|
__pivotLabel: String(node.value ?? '(All)'),
|
|
401
428
|
__pivotParentId: parentId,
|
|
402
429
|
__pivotExpandable: true,
|
|
403
|
-
...
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
430
|
+
...(wantSubtotals
|
|
431
|
+
? computeRowValues(
|
|
432
|
+
node.rows as ReadonlyArray<Record<string, unknown>>,
|
|
433
|
+
colLeafPaths,
|
|
434
|
+
colFields,
|
|
435
|
+
values,
|
|
436
|
+
includeGrandTotalCol,
|
|
437
|
+
)
|
|
438
|
+
: blankRowValues(colLeafPaths, values, includeGrandTotalCol)),
|
|
410
439
|
})
|
|
411
440
|
// Children of this group point at this row as their parent so a
|
|
412
441
|
// collapse hides the whole subtree, not just the immediate leaves.
|
package/src/print.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { RowData, TableFeatures, SvGridApi } from '@svgrid/grid'
|
|
2
|
-
import {
|
|
2
|
+
import { assertEnterpriseLicensed } from './license'
|
|
3
3
|
import { installSmartShim } from './smart-shim'
|
|
4
4
|
import type { ExportColumn } from './export'
|
|
5
5
|
|
|
@@ -51,7 +51,7 @@ export async function printGrid<
|
|
|
51
51
|
TFeatures extends TableFeatures,
|
|
52
52
|
TData extends RowData,
|
|
53
53
|
>(api: SvGridApi<TFeatures, TData>, opts?: PrintOptions<TData>): Promise<void> {
|
|
54
|
-
|
|
54
|
+
assertEnterpriseLicensed('Print')
|
|
55
55
|
const sourceRows: ReadonlyArray<TData> = opts?.rows ?? api.getDisplayedRows()
|
|
56
56
|
let cols: ReadonlyArray<ExportColumn>
|
|
57
57
|
if (opts?.columns && opts.columns.length > 0) {
|
package/src/revoked.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Revoked license keys. A key in this set throws on use even if it has the
|
|
2
|
-
// valid
|
|
2
|
+
// valid SVENTERPRISE- prefix. Use this to invalidate keys that have been leaked,
|
|
3
3
|
// shared outside their seat count, or issued for trials that have expired.
|
|
4
4
|
//
|
|
5
5
|
// =============================================================================
|
|
@@ -44,6 +44,6 @@
|
|
|
44
44
|
// =============================================================================
|
|
45
45
|
|
|
46
46
|
export const REVOKED_KEYS: ReadonlySet<string> = new Set<string>([
|
|
47
|
-
// '
|
|
48
|
-
// '
|
|
47
|
+
// 'SVENTERPRISE-EVAL-acme-202506-3F7K9P', // example - expired ACME trial
|
|
48
|
+
// 'SVENTERPRISE-LEAKED-globex-202612-X8N4P', // example - key leaked on GitHub
|
|
49
49
|
])
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
9
9
|
import { showUpgradePrompt, dismissUpgradePrompt } from './upgrade-prompt'
|
|
10
10
|
|
|
11
|
-
const CARD_ATTR = 'data
|
|
11
|
+
const CARD_ATTR = 'data-svgrid-enterprise-upgrade'
|
|
12
12
|
|
|
13
13
|
function card(): HTMLElement | null {
|
|
14
14
|
return document.querySelector(`[${CARD_ATTR}]`)
|
package/src/upgrade-prompt.ts
CHANGED
|
@@ -17,16 +17,16 @@
|
|
|
17
17
|
// - Zero dependencies, inline styles - no CSS file to import, nothing to
|
|
18
18
|
// bundle, works in any host app regardless of its styling.
|
|
19
19
|
|
|
20
|
-
const CARD_ATTR = 'data
|
|
20
|
+
const CARD_ATTR = 'data-svgrid-enterprise-upgrade'
|
|
21
21
|
const PRICING_URL = 'https://www.svgrid.com/pricing'
|
|
22
22
|
// ?ref=in-app lets us measure how many trials start from this exact prompt
|
|
23
23
|
// vs. the pricing page itself - the whole point of a moment-of-intent CTA.
|
|
24
|
-
const TRIAL_URL = 'https://www.svgrid.com/pricing?ref=in-app&utm_source
|
|
24
|
+
const TRIAL_URL = 'https://www.svgrid.com/pricing?ref=in-app&utm_source=svgrid-enterprise&utm_medium=upgrade-prompt'
|
|
25
25
|
|
|
26
26
|
let shownThisSession = false
|
|
27
27
|
|
|
28
28
|
/** Human-readable labels for the Pro surfaces that gate behind a license. */
|
|
29
|
-
export type
|
|
29
|
+
export type EnterpriseFeatureLabel =
|
|
30
30
|
| 'Export'
|
|
31
31
|
| 'Import'
|
|
32
32
|
| 'Print'
|
|
@@ -38,7 +38,7 @@ export type ProFeatureLabel =
|
|
|
38
38
|
* been shown this session, a card is already up, or we're on the server. Safe
|
|
39
39
|
* to call on every Pro feature invocation - it self-throttles.
|
|
40
40
|
*/
|
|
41
|
-
export function showUpgradePrompt(feature?:
|
|
41
|
+
export function showUpgradePrompt(feature?: EnterpriseFeatureLabel | string): void {
|
|
42
42
|
if (shownThisSession) return
|
|
43
43
|
if (typeof document === 'undefined' || typeof window === 'undefined') return
|
|
44
44
|
if (document.querySelector(`[${CARD_ATTR}]`)) return
|
|
@@ -72,7 +72,7 @@ function close(card: HTMLElement): void {
|
|
|
72
72
|
window.setTimeout(() => card.remove(), 220)
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
-
function buildCard(feature?:
|
|
75
|
+
function buildCard(feature?: EnterpriseFeatureLabel | string): HTMLElement {
|
|
76
76
|
const card = document.createElement('div')
|
|
77
77
|
card.setAttribute(CARD_ATTR, '1')
|
|
78
78
|
card.setAttribute('role', 'dialog')
|
package/src/watermark.test.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
10
10
|
import { emitUnlicensedNudge, dismissUnlicensedNudge } from './watermark'
|
|
11
11
|
|
|
12
|
-
const WATERMARK_ATTR = 'data
|
|
12
|
+
const WATERMARK_ATTR = 'data-svgrid-enterprise-watermark'
|
|
13
13
|
|
|
14
14
|
function makeGridRoot(): HTMLElement {
|
|
15
15
|
const el = document.createElement('div')
|
package/src/watermark.ts
CHANGED
|
@@ -5,10 +5,10 @@
|
|
|
5
5
|
// this file is only for the "no key set" case where the feature continues to
|
|
6
6
|
// work but the user gets nudged.
|
|
7
7
|
|
|
8
|
-
const WATERMARK_ATTR = 'data
|
|
8
|
+
const WATERMARK_ATTR = 'data-svgrid-enterprise-watermark'
|
|
9
9
|
// Marks a grid that has already shown (and faded) its watermark, so the
|
|
10
10
|
// MutationObserver doesn't re-add it once it fades out and is removed.
|
|
11
|
-
const NUDGED_ATTR = 'data
|
|
11
|
+
const NUDGED_ATTR = 'data-svgrid-enterprise-nudged'
|
|
12
12
|
const HOMEPAGE_URL = 'https://www.svgrid.com'
|
|
13
13
|
const LICENSE_URL = 'https://www.svgrid.com/pricing'
|
|
14
14
|
const WATERMARK_LABEL = 'www.svgrid.com'
|
|
@@ -24,7 +24,7 @@ let observer: MutationObserver | null = null
|
|
|
24
24
|
export function emitUnlicensedNudge(): void {
|
|
25
25
|
showConsoleOnce()
|
|
26
26
|
if (typeof document === 'undefined' || typeof window === 'undefined') return
|
|
27
|
-
// Defer one tick:
|
|
27
|
+
// Defer one tick: installEnterprise may run inside onApiReady before the grid's
|
|
28
28
|
// root element is in the DOM. The microtask gives Svelte time to flush.
|
|
29
29
|
queueMicrotask(() => attachToAllGrids())
|
|
30
30
|
// Watch for grids that mount later (route changes, dynamic loads).
|