@svgrid/enterprise 2.2.1 → 2.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +95 -81
- package/dist/cdn/svgrid-enterprise.svelte-external.js +26642 -11252
- package/dist/designer/assets/GridMenus-BtWVk9Ab.js +7 -0
- package/dist/designer/assets/SvGridChartPanel-DWlBO2SD.js +10 -0
- package/dist/designer/assets/SvGridChartView-8c8Mb4j8.js +1 -0
- package/dist/designer/assets/SvGridChartView-DX9HfkBR.css +1 -0
- package/dist/designer/assets/index-DsDgp9Xq.js +78758 -0
- package/dist/designer/assets/index-tTY_Dx4P.css +1 -0
- package/dist/designer/assets/jszip.min-fkJdmAmj.js +2 -0
- package/dist/designer/assets/pdfmake-DeCsnyl9.js +242 -0
- package/dist/designer/assets/smart.export-BZlSCE8T.js +35 -0
- package/dist/designer/assets/vfs_fonts-eX2NpmfX.js +1 -0
- package/dist/designer/index.html +13 -0
- package/dist/node/studio.js +15304 -2708
- package/package.json +35 -11
- package/src/SvAlertRuleEditor.svelte +294 -0
- package/src/SvAlertsManager.svelte +210 -0
- package/src/SvAlertsPanel.svelte +129 -0
- package/src/SvBoard.svelte +3 -1
- package/src/SvExpressionEditor.svelte +341 -0
- package/src/SvGridAlerts.dom.test.ts +113 -0
- package/src/SvGridAlerts.svelte +265 -0
- package/src/SvGridBoard.svelte +2358 -0
- package/src/SvGridEditPanel.svelte +849 -794
- package/src/SvGridScheduler.svelte +5358 -4410
- package/src/SvPivotDesigner.svelte +3 -3
- package/src/SvRecordDetail.svelte +6 -2
- package/src/SvSchedule.svelte +3 -1
- package/src/{ai-export-pdf.test.ts → ai-export-pdf.dom.test.ts} +77 -74
- package/src/{ai-export-xlsx.test.ts → ai-export-xlsx.dom.test.ts} +90 -87
- package/src/{ai-export.test.ts → ai-export.dom.test.ts} +114 -110
- package/src/alerts/alert-engine-attach.ts +165 -0
- package/src/alerts/alert-engine.test.ts +135 -0
- package/src/alerts/alert-engine.ts +260 -0
- package/src/alerts/alert-formats.test.ts +87 -0
- package/src/alerts/alert-formats.ts +77 -0
- package/src/alerts/alert-observer.test.ts +189 -0
- package/src/alerts/alert-observer.ts +208 -0
- package/src/alerts/alert-scheduler.ts +96 -0
- package/src/alerts/alert-storage.test.ts +54 -0
- package/src/alerts/alert-storage.ts +116 -0
- package/src/alerts/alert-store.svelte.ts +80 -0
- package/src/alerts/alert-types.ts +94 -0
- package/src/alerts.ts +28 -0
- package/src/board.dom.test.ts +941 -0
- package/src/board.ts +36 -0
- package/src/export-ooxml.ts +4 -0
- package/src/export-xls.ts +3 -0
- package/src/export.ts +6 -0
- package/src/expressions/evaluate.test.ts +111 -0
- package/src/expressions/evaluate.ts +237 -0
- package/src/expressions/expression-columns.ts +133 -0
- package/src/expressions/expression-types.ts +84 -0
- package/src/expressions/parse.test.ts +106 -0
- package/src/expressions/parse.ts +610 -0
- package/src/import.test.ts +1 -1
- package/src/import.ts +3 -1
- package/src/index.ts +192 -46
- package/src/install.ts +12 -2
- package/src/pivot-enable.ts +44 -0
- package/src/pivot.test.ts +0 -1
- package/src/scheduler-assignments.test.ts +97 -0
- package/src/scheduler-assignments.ts +134 -0
- package/src/scheduler-axis.test.ts +108 -0
- package/src/scheduler-axis.ts +238 -0
- package/src/scheduler-booking.test.ts +57 -0
- package/src/scheduler-booking.ts +63 -0
- package/src/scheduler-config.ts +179 -0
- package/src/scheduler-dependencies.test.ts +155 -0
- package/src/scheduler-dependencies.ts +223 -0
- package/src/scheduler-freebusy.test.ts +41 -0
- package/src/scheduler-freebusy.ts +36 -0
- package/src/scheduler-heatmap.test.ts +39 -0
- package/src/scheduler-heatmap.ts +55 -0
- package/src/scheduler-resource-tree.test.ts +84 -0
- package/src/scheduler-resource-tree.ts +107 -0
- package/src/scheduler-slots.test.ts +53 -0
- package/src/scheduler-slots.ts +94 -0
- package/src/scheduler-summary.test.ts +47 -0
- package/src/scheduler-summary.ts +81 -0
- package/src/schema-designer.ts +1 -1
- package/src/sources/index.ts +1 -1
- package/src/sources/introspect-supabase.test.ts +13 -1
- package/src/sources/introspect-supabase.ts +20 -0
- package/src/studio/copilot-core.test.ts +45 -0
- package/src/studio/copilot-core.ts +65 -0
- package/src/studio/deploy-cli.test.ts +56 -0
- package/src/studio/deploy-cli.ts +100 -0
- package/src/studio/emit-project.test.ts +800 -24
- package/src/studio/emit-project.ts +1307 -135
- package/src/studio/emit-schema.test.ts +17 -0
- package/src/studio/emit-schema.ts +182 -41
- package/src/studio/index.ts +59 -1
- package/src/studio/init-flow.test.ts +239 -0
- package/src/studio/init-flow.ts +358 -0
- package/src/studio/introspect-openapi.test.ts +84 -0
- package/src/studio/introspect-openapi.ts +252 -0
- package/src/studio/project.test.ts +57 -1
- package/src/studio/project.ts +302 -8
- package/src/studio/samples/crm.ts +282 -258
- package/src/studio/samples/datasets.test.ts +84 -0
- package/src/studio/samples/datasets.ts +340 -0
- package/src/studio/samples/fleet.ts +213 -186
- package/src/studio/samples/insurance.ts +223 -195
- package/src/studio/samples/inventory.ts +213 -182
- package/src/studio/samples/live-data.test.ts +99 -98
- package/src/studio/samples/live-data.ts +8 -10
- package/src/studio/samples/projects.ts +212 -190
- package/src/studio/samples/samples.test.ts +268 -216
- package/src/studio/samples/shared.ts +22 -145
- package/src/studio/samples/starter.ts +251 -0
- package/src/studio/samples/support.ts +209 -184
- package/src/studio/screen-suites.test.ts +226 -0
- package/src/studio/screen-suites.ts +445 -0
- package/src/studio/ui-components-surface.test.ts +185 -0
- package/src/studio/ui-components.generated.ts +7154 -0
- package/src/studio/ui-components.ts +273 -3
- package/src/sveltekit/index.ts +1 -0
- package/src/sveltekit/sql-source.test.ts +13 -0
- package/src/sveltekit/sql-source.ts +12 -7
- package/src/sveltekit/transport-scope.test.ts +125 -0
- package/src/sveltekit/transport.ts +76 -2
- package/src/upgrade-prompt.ts +2 -2
- package/src/watermark.ts +2 -2
- package/src/ai.test.ts +0 -522
- package/src/ai.ts +0 -1388
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
popIn,
|
|
32
32
|
createDismissableLayer,
|
|
33
33
|
createFocusTrap,
|
|
34
|
+
onScrollOutside,
|
|
34
35
|
type ColumnDef,
|
|
35
36
|
type ChartType,
|
|
36
37
|
type MenuItem,
|
|
@@ -692,9 +693,8 @@
|
|
|
692
693
|
trap.activate()
|
|
693
694
|
const layer = createDismissableLayer({ element: () => ctxPanelEl, onDismiss: closeCtx })
|
|
694
695
|
layer.activate()
|
|
695
|
-
const
|
|
696
|
-
|
|
697
|
-
return () => { layer.release(); trap.release(); window.removeEventListener('scroll', onScroll, true) }
|
|
696
|
+
const offScroll = onScrollOutside(() => ctxPanelEl, closeCtx)
|
|
697
|
+
return () => { layer.release(); trap.release(); offScroll() }
|
|
698
698
|
})
|
|
699
699
|
|
|
700
700
|
function clearWell(well: Well) {
|
|
@@ -19,7 +19,9 @@
|
|
|
19
19
|
type Section = { label: string; fields: string[] }
|
|
20
20
|
type Related = {
|
|
21
21
|
label: string
|
|
22
|
-
|
|
22
|
+
// Accept any specialized EntitySchema<T> (the type param is invariant); this is
|
|
23
|
+
// pure presentation over the schema's field metadata + plain record rows.
|
|
24
|
+
schema: EntitySchema<any>
|
|
23
25
|
rows?: ReadonlyArray<Row>
|
|
24
26
|
/** The child field pointing back at this record. */
|
|
25
27
|
foreignKey: string
|
|
@@ -46,7 +48,9 @@
|
|
|
46
48
|
loading = false,
|
|
47
49
|
height,
|
|
48
50
|
}: {
|
|
49
|
-
|
|
51
|
+
// Accept any specialized EntitySchema<T> (the type param is invariant); this is
|
|
52
|
+
// pure presentation over the schema's field metadata + plain record rows.
|
|
53
|
+
schema: EntitySchema<any>
|
|
50
54
|
rows?: ReadonlyArray<Row>
|
|
51
55
|
/** The specific record to show. Omit to self-drive off `rows`. */
|
|
52
56
|
row?: Row
|
package/src/SvSchedule.svelte
CHANGED
|
@@ -23,7 +23,9 @@
|
|
|
23
23
|
loading = false,
|
|
24
24
|
height,
|
|
25
25
|
}: {
|
|
26
|
-
|
|
26
|
+
// Accept any specialized EntitySchema<T> (the type param is invariant); this is
|
|
27
|
+
// pure presentation over the schema's field metadata + plain record rows.
|
|
28
|
+
schema: EntitySchema<any>
|
|
27
29
|
rows?: ReadonlyArray<Row>
|
|
28
30
|
/** The date / datetime field that places a row on the calendar. */
|
|
29
31
|
dateField: string
|
|
@@ -1,74 +1,77 @@
|
|
|
1
|
-
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
2
|
-
|
|
3
|
-
// Capture exactly what exportGrid receives (so we don't need pdfmake / the DOM).
|
|
4
|
-
const calls: Array<{ format: string; rows?: unknown[]; groupBy?: string[] }> = []
|
|
5
|
-
vi.mock('./export', () => ({
|
|
6
|
-
exportGrid: async (_api: unknown, opts: { format: string; rows?: unknown[]; groupBy?: string[] }) => {
|
|
7
|
-
calls.push({ format: opts.format, rows: opts.rows as unknown[], groupBy: opts.groupBy })
|
|
8
|
-
},
|
|
9
|
-
}))
|
|
10
|
-
|
|
11
|
-
import { aiExport, setAIProvider, mockAIProvider } from '
|
|
12
|
-
import {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
{ id: '
|
|
32
|
-
{ id: '
|
|
33
|
-
{ id: '
|
|
34
|
-
{ id: '
|
|
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
|
-
expect(
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
expect(
|
|
65
|
-
expect(
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
})
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
2
|
+
|
|
3
|
+
// Capture exactly what exportGrid receives (so we don't need pdfmake / the DOM).
|
|
4
|
+
const calls: Array<{ format: string; rows?: unknown[]; groupBy?: string[] }> = []
|
|
5
|
+
vi.mock('./export', () => ({
|
|
6
|
+
exportGrid: async (_api: unknown, opts: { format: string; rows?: unknown[]; groupBy?: string[] }) => {
|
|
7
|
+
calls.push({ format: opts.format, rows: opts.rows as unknown[], groupBy: opts.groupBy })
|
|
8
|
+
},
|
|
9
|
+
}))
|
|
10
|
+
|
|
11
|
+
import { aiExport, setAIProvider, mockAIProvider, registerExportProvider } from '@svgrid/grid'
|
|
12
|
+
import { exportGrid } from './export'
|
|
13
|
+
import { setLicenseKey } from './license'
|
|
14
|
+
|
|
15
|
+
type Order = { country: string; company: string; product: string; quantity: number; price: number }
|
|
16
|
+
const rows: Order[] = Array.from({ length: 120 }, (_, i) => ({
|
|
17
|
+
country: ['USA', 'UK', 'Germany'][i % 3]!,
|
|
18
|
+
company: `Co${i}`,
|
|
19
|
+
product: 'Widget',
|
|
20
|
+
quantity: 5 + (i % 50),
|
|
21
|
+
price: Math.round((10 + ((i * 41) % 480)) * 100) / 100, // spread 10..490
|
|
22
|
+
}))
|
|
23
|
+
|
|
24
|
+
function makeApi() {
|
|
25
|
+
return {
|
|
26
|
+
getData: () => rows,
|
|
27
|
+
// Simulate the grid returning EVERYTHING (the reactive-filter bug we route around).
|
|
28
|
+
getDisplayedRows: () => rows,
|
|
29
|
+
getSelectedRows: () => [],
|
|
30
|
+
getColumns: () => [
|
|
31
|
+
{ id: 'country', field: 'country', header: 'Country', visible: true },
|
|
32
|
+
{ id: 'company', field: 'company', header: 'Company', visible: true },
|
|
33
|
+
{ id: 'product', field: 'product', header: 'Product', visible: true },
|
|
34
|
+
{ id: 'quantity', field: 'quantity', header: 'Qty', visible: true },
|
|
35
|
+
{ id: 'price', field: 'price', header: 'Price', visible: true },
|
|
36
|
+
],
|
|
37
|
+
getState: () => ({ grouping: [] }),
|
|
38
|
+
clearAllFilters: vi.fn(),
|
|
39
|
+
clearSort: vi.fn(),
|
|
40
|
+
setFilter: vi.fn(),
|
|
41
|
+
setSort: vi.fn(),
|
|
42
|
+
setGroupBy: vi.fn(),
|
|
43
|
+
} as any
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
beforeEach(() => {
|
|
47
|
+
calls.length = 0
|
|
48
|
+
setLicenseKey('SVENTERPRISE-DEV-TEST')
|
|
49
|
+
setAIProvider(mockAIProvider)
|
|
50
|
+
// aiExport delegates to the registered engine - here the mocked exportGrid.
|
|
51
|
+
registerExportProvider(exportGrid as never)
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
describe('aiExport - the demo 203 "grouped PDF" query', () => {
|
|
55
|
+
it('parses the plan and exports non-empty, filtered, grouped rows', async () => {
|
|
56
|
+
const plan = await aiExport(makeApi(), 'export orders over $300 as a grouped PDF by country')
|
|
57
|
+
|
|
58
|
+
expect(plan.format).toBe('pdf')
|
|
59
|
+
expect(plan.groupBy).toEqual(['country'])
|
|
60
|
+
expect(plan.filters).toContainEqual({ field: 'price', operator: 'greaterThan', value: '300' })
|
|
61
|
+
|
|
62
|
+
// exportGrid was called with real, filtered rows (not the empty grid view).
|
|
63
|
+
expect(calls).toHaveLength(1)
|
|
64
|
+
expect(calls[0]!.format).toBe('pdf')
|
|
65
|
+
expect(calls[0]!.groupBy).toEqual(['country'])
|
|
66
|
+
const exported = calls[0]!.rows as Order[]
|
|
67
|
+
expect(exported.length).toBeGreaterThan(0)
|
|
68
|
+
expect(exported.every((r) => r.price > 300)).toBe(true)
|
|
69
|
+
// Rows are contiguous by country (group order preserved).
|
|
70
|
+
const countries = exported.map((r) => r.country)
|
|
71
|
+
const firstSeen = [...new Set(countries)]
|
|
72
|
+
const contiguous = countries.join('|')
|
|
73
|
+
for (const c of firstSeen) {
|
|
74
|
+
expect(contiguous).toMatch(new RegExp(`(?:^|\\|)${c}(?:\\|${c})*(?:\\||$)`))
|
|
75
|
+
}
|
|
76
|
+
})
|
|
77
|
+
})
|
|
@@ -1,87 +1,90 @@
|
|
|
1
|
-
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
2
|
-
|
|
3
|
-
// Real exportGrid runs (NOT mocked). Capture the xlsx parts / avoid the DOM.
|
|
4
|
-
const captured: { parts: Record<string, string> } = { parts: {} }
|
|
5
|
-
vi.mock('jszip', () => ({
|
|
6
|
-
default: class {
|
|
7
|
-
file(p: string, d: string) {
|
|
8
|
-
captured.parts[p] = d
|
|
9
|
-
}
|
|
10
|
-
async generateAsync() {
|
|
11
|
-
return new Blob([])
|
|
12
|
-
}
|
|
13
|
-
},
|
|
14
|
-
}))
|
|
15
|
-
vi.mock('./export-serialize', async (importOriginal) => {
|
|
16
|
-
const actual = (await importOriginal()) as Record<string, unknown>
|
|
17
|
-
return { ...actual, downloadBlobFile: () => {}, downloadTextFile: () => {} }
|
|
18
|
-
})
|
|
19
|
-
|
|
20
|
-
import { aiExport, setAIProvider, mockAIProvider } from '
|
|
21
|
-
import {
|
|
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
|
-
{ id: '
|
|
55
|
-
{ id: '
|
|
56
|
-
{ id: '
|
|
57
|
-
{ id: '
|
|
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
|
-
expect(
|
|
83
|
-
|
|
84
|
-
const
|
|
85
|
-
expect(
|
|
86
|
-
|
|
87
|
-
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
2
|
+
|
|
3
|
+
// Real exportGrid runs (NOT mocked). Capture the xlsx parts / avoid the DOM.
|
|
4
|
+
const captured: { parts: Record<string, string> } = { parts: {} }
|
|
5
|
+
vi.mock('jszip', () => ({
|
|
6
|
+
default: class {
|
|
7
|
+
file(p: string, d: string) {
|
|
8
|
+
captured.parts[p] = d
|
|
9
|
+
}
|
|
10
|
+
async generateAsync() {
|
|
11
|
+
return new Blob([])
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
}))
|
|
15
|
+
vi.mock('./export-serialize', async (importOriginal) => {
|
|
16
|
+
const actual = (await importOriginal()) as Record<string, unknown>
|
|
17
|
+
return { ...actual, downloadBlobFile: () => {}, downloadTextFile: () => {} }
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
import { aiExport, setAIProvider, mockAIProvider, registerExportProvider } from '@svgrid/grid'
|
|
21
|
+
import { exportGrid } from './export'
|
|
22
|
+
import { setLicenseKey } from './license'
|
|
23
|
+
|
|
24
|
+
// Mirrors the real makeOrders shape - crucially includes `inStock` (boolean)
|
|
25
|
+
// and `quantity`/`index` (numbers that never exceed 300), so the mock must
|
|
26
|
+
// target `price` for "$300", not a boolean or a count.
|
|
27
|
+
type Order = {
|
|
28
|
+
id: string
|
|
29
|
+
index: number
|
|
30
|
+
country: string
|
|
31
|
+
company: string
|
|
32
|
+
product: string
|
|
33
|
+
inStock: boolean
|
|
34
|
+
quantity: number
|
|
35
|
+
price: number
|
|
36
|
+
}
|
|
37
|
+
const rows: Order[] = Array.from({ length: 120 }, (_, i) => ({
|
|
38
|
+
id: `o${i}`,
|
|
39
|
+
index: i + 1,
|
|
40
|
+
country: ['USA', 'UK', 'Germany'][i % 3]!,
|
|
41
|
+
company: `Co${i}`,
|
|
42
|
+
product: 'Widget',
|
|
43
|
+
inStock: i % 3 !== 0,
|
|
44
|
+
quantity: 5 + (i % 50), // max 54 - never > 300
|
|
45
|
+
price: Math.round((10 + ((i * 41) % 480)) * 100) / 100, // spread 10..490
|
|
46
|
+
}))
|
|
47
|
+
|
|
48
|
+
function makeApi() {
|
|
49
|
+
return {
|
|
50
|
+
getData: () => rows,
|
|
51
|
+
getDisplayedRows: () => rows,
|
|
52
|
+
getSelectedRows: () => [],
|
|
53
|
+
getColumns: () => [
|
|
54
|
+
{ id: 'country', field: 'country', header: 'Country', visible: true },
|
|
55
|
+
{ id: 'company', field: 'company', header: 'Company', visible: true },
|
|
56
|
+
{ id: 'product', field: 'product', header: 'Product', visible: true },
|
|
57
|
+
{ id: 'quantity', field: 'quantity', header: 'Qty', visible: true },
|
|
58
|
+
{ id: 'price', field: 'price', header: 'Price', visible: true, format: { type: 'currency', currency: 'USD' } },
|
|
59
|
+
],
|
|
60
|
+
getState: () => ({ grouping: [] }),
|
|
61
|
+
getColumnPinning: () => ({ left: [], right: [] }),
|
|
62
|
+
clearAllFilters: vi.fn(),
|
|
63
|
+
clearSort: vi.fn(),
|
|
64
|
+
setFilter: vi.fn(),
|
|
65
|
+
setSort: vi.fn(),
|
|
66
|
+
setGroupBy: vi.fn(),
|
|
67
|
+
} as any
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
beforeEach(() => {
|
|
71
|
+
captured.parts = {}
|
|
72
|
+
setLicenseKey('SVENTERPRISE-DEV-TEST')
|
|
73
|
+
setAIProvider(mockAIProvider)
|
|
74
|
+
// aiExport is free (grid) and delegates the real write to enterprise's engine.
|
|
75
|
+
registerExportProvider(exportGrid as never)
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
describe('aiExport - "export orders over $300" (xlsx default)', () => {
|
|
79
|
+
it('produces a non-empty xlsx via the real export path', async () => {
|
|
80
|
+
const plan = await aiExport(makeApi(), 'export orders over $300')
|
|
81
|
+
expect(plan.format).toBe('xlsx')
|
|
82
|
+
expect(plan.filters).toContainEqual({ field: 'price', operator: 'greaterThan', value: '300' })
|
|
83
|
+
|
|
84
|
+
const sheet = captured.parts['xl/worksheets/sheet1.xml']
|
|
85
|
+
expect(sheet).toBeTruthy()
|
|
86
|
+
// header row + at least one data row
|
|
87
|
+
const rowCount = (sheet!.match(/<row /g) ?? []).length
|
|
88
|
+
expect(rowCount).toBeGreaterThan(1)
|
|
89
|
+
})
|
|
90
|
+
})
|
|
@@ -1,110 +1,114 @@
|
|
|
1
|
-
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
2
|
-
|
|
3
|
-
// Capture downloads instead of touching the DOM (aiExport -> exportGrid).
|
|
4
|
-
const downloads: Array<{ text: string; filename: string }> = []
|
|
5
|
-
vi.mock('./export-serialize', async (importOriginal) => {
|
|
6
|
-
const actual = (await importOriginal()) as Record<string, unknown>
|
|
7
|
-
return {
|
|
8
|
-
...actual,
|
|
9
|
-
downloadTextFile: (text: string, filename: string) => {
|
|
10
|
-
downloads.push({ text, filename })
|
|
11
|
-
},
|
|
12
|
-
}
|
|
13
|
-
})
|
|
14
|
-
|
|
15
|
-
import { aiExport, aiFindAnomalies, setAIProvider, mockAIProvider } from '
|
|
16
|
-
import {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
{ region: '
|
|
22
|
-
{ region: '
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
{ id: '
|
|
33
|
-
{ id: '
|
|
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
|
-
expect(
|
|
62
|
-
expect(
|
|
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
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
expect(result.
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
2
|
+
|
|
3
|
+
// Capture downloads instead of touching the DOM (aiExport -> exportGrid).
|
|
4
|
+
const downloads: Array<{ text: string; filename: string }> = []
|
|
5
|
+
vi.mock('./export-serialize', async (importOriginal) => {
|
|
6
|
+
const actual = (await importOriginal()) as Record<string, unknown>
|
|
7
|
+
return {
|
|
8
|
+
...actual,
|
|
9
|
+
downloadTextFile: (text: string, filename: string) => {
|
|
10
|
+
downloads.push({ text, filename })
|
|
11
|
+
},
|
|
12
|
+
}
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
import { aiExport, aiFindAnomalies, setAIProvider, mockAIProvider, registerExportProvider } from '@svgrid/grid'
|
|
16
|
+
import { exportGrid } from './export'
|
|
17
|
+
import { setLicenseKey } from './license'
|
|
18
|
+
|
|
19
|
+
type Row = { region: string; amount: number; name: string }
|
|
20
|
+
const rows: Row[] = [
|
|
21
|
+
{ region: 'EMEA', amount: 100, name: 'A' },
|
|
22
|
+
{ region: 'NA', amount: 2000, name: 'B' },
|
|
23
|
+
{ region: 'EMEA', amount: 50, name: 'C' },
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
function makeApi() {
|
|
27
|
+
return {
|
|
28
|
+
getData: () => rows,
|
|
29
|
+
getDisplayedRows: () => rows,
|
|
30
|
+
getSelectedRows: () => [],
|
|
31
|
+
getColumns: () => [
|
|
32
|
+
{ id: 'region', field: 'region', header: 'Region', visible: true },
|
|
33
|
+
{ id: 'amount', field: 'amount', header: 'Amount', visible: true },
|
|
34
|
+
{ id: 'name', field: 'name', header: 'Name', visible: true },
|
|
35
|
+
],
|
|
36
|
+
getState: () => ({ grouping: [] }),
|
|
37
|
+
clearAllFilters: vi.fn(),
|
|
38
|
+
clearSort: vi.fn(),
|
|
39
|
+
setFilter: vi.fn(),
|
|
40
|
+
setSort: vi.fn(),
|
|
41
|
+
} as any
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
beforeEach(() => {
|
|
45
|
+
downloads.length = 0
|
|
46
|
+
setLicenseKey('SVENTERPRISE-DEV-TEST')
|
|
47
|
+
setAIProvider(mockAIProvider)
|
|
48
|
+
// aiExport is free (grid) and delegates the WRITE to the registered engine;
|
|
49
|
+
// register enterprise's exportGrid so the download path actually runs.
|
|
50
|
+
registerExportProvider(exportGrid as never)
|
|
51
|
+
})
|
|
52
|
+
afterEach(() => setAIProvider(null))
|
|
53
|
+
|
|
54
|
+
describe('aiExport', () => {
|
|
55
|
+
it('parses format + groupBy + filters; applies to the grid only when asked', async () => {
|
|
56
|
+
const api = makeApi()
|
|
57
|
+
const plan = await aiExport(api, 'export deals over 500 grouped by region as a pdf', {
|
|
58
|
+
run: false,
|
|
59
|
+
apply: true,
|
|
60
|
+
})
|
|
61
|
+
expect(plan.format).toBe('pdf')
|
|
62
|
+
expect(plan.groupBy).toEqual(['region'])
|
|
63
|
+
expect(plan.filters).toContainEqual({ field: 'amount', operator: 'greaterThan', value: '500' })
|
|
64
|
+
// apply: true -> the grid is mutated
|
|
65
|
+
expect(api.clearAllFilters).toHaveBeenCalled()
|
|
66
|
+
expect(api.setFilter).toHaveBeenCalledWith('amount', { operator: 'greaterThan', value: '500' })
|
|
67
|
+
// run:false -> nothing downloaded
|
|
68
|
+
expect(downloads).toHaveLength(0)
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it('defaults to xlsx and does NOT touch the grid (apply defaults off)', async () => {
|
|
72
|
+
const api = makeApi()
|
|
73
|
+
const plan = await aiExport(api, 'just export everything', { run: false })
|
|
74
|
+
expect(plan.format).toBe('xlsx')
|
|
75
|
+
expect(api.clearAllFilters).not.toHaveBeenCalled()
|
|
76
|
+
expect(api.setFilter).not.toHaveBeenCalled()
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
it('runs the export (downloads) when run is not false', async () => {
|
|
80
|
+
const api = makeApi()
|
|
81
|
+
await aiExport(api, 'export as csv', { filename: 'report' })
|
|
82
|
+
expect(downloads).toHaveLength(1)
|
|
83
|
+
expect(downloads[0]!.filename).toBe('report.csv')
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
it('exports the plan-filtered rows directly (not the grid displayed rows)', async () => {
|
|
87
|
+
// getDisplayedRows returns EVERYTHING (as if the grid hadn't re-filtered);
|
|
88
|
+
// the export must still contain only rows matching the plan's filter.
|
|
89
|
+
const api = makeApi()
|
|
90
|
+
await aiExport(api, 'export deals over 500 as csv', { filename: 'big' })
|
|
91
|
+
const { text } = downloads[0]!
|
|
92
|
+
expect(text).toContain('2000') // amount 2000 > 500 kept
|
|
93
|
+
expect(text).not.toContain('100') // amount 100 filtered out
|
|
94
|
+
expect(text).not.toContain('50') // amount 50 filtered out
|
|
95
|
+
})
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
describe('aiFindAnomalies', () => {
|
|
99
|
+
it('flags a numeric outlier over the scanned rows', async () => {
|
|
100
|
+
const api = makeApi()
|
|
101
|
+
const result = await aiFindAnomalies(api, {})
|
|
102
|
+
expect(result.anomalies.length).toBeGreaterThan(0)
|
|
103
|
+
const amountAnomaly = result.anomalies.find((a) => a.field === 'amount')
|
|
104
|
+
expect(amountAnomaly?.value).toBe(2000) // the max in the sample
|
|
105
|
+
expect(amountAnomaly?.severity).toBe('medium')
|
|
106
|
+
expect(result.summary).toBeTruthy()
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
it('returns empty for an empty selection', async () => {
|
|
110
|
+
const api = makeApi()
|
|
111
|
+
const result = await aiFindAnomalies(api, { target: { kind: 'selection', rowIndices: [] } })
|
|
112
|
+
expect(result.anomalies).toEqual([])
|
|
113
|
+
})
|
|
114
|
+
})
|