@svgrid/enterprise 2.2.0 → 2.3.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 (101) hide show
  1. package/README.md +95 -81
  2. package/dist/cdn/svgrid-enterprise.svelte-external.js +22479 -10466
  3. package/dist/node/studio.js +11590 -2907
  4. package/package.json +31 -10
  5. package/src/SvAlertRuleEditor.svelte +294 -0
  6. package/src/SvAlertsManager.svelte +210 -0
  7. package/src/SvAlertsPanel.svelte +129 -0
  8. package/src/SvBoard.svelte +3 -1
  9. package/src/SvExpressionEditor.svelte +341 -0
  10. package/src/SvGridAlerts.dom.test.ts +113 -0
  11. package/src/SvGridAlerts.svelte +265 -0
  12. package/src/SvGridBoard.svelte +2355 -0
  13. package/src/SvGridEditPanel.svelte +849 -794
  14. package/src/SvGridScheduler.svelte +5334 -4410
  15. package/src/SvPivotDesigner.svelte +3 -3
  16. package/src/SvRecordDetail.svelte +6 -2
  17. package/src/SvSchedule.svelte +3 -1
  18. package/src/{ai-export-pdf.test.ts → ai-export-pdf.dom.test.ts} +4 -1
  19. package/src/{ai-export-xlsx.test.ts → ai-export-xlsx.dom.test.ts} +4 -1
  20. package/src/{ai-export.test.ts → ai-export.dom.test.ts} +5 -1
  21. package/src/alerts/alert-engine-attach.ts +165 -0
  22. package/src/alerts/alert-engine.test.ts +135 -0
  23. package/src/alerts/alert-engine.ts +260 -0
  24. package/src/alerts/alert-formats.test.ts +87 -0
  25. package/src/alerts/alert-formats.ts +77 -0
  26. package/src/alerts/alert-observer.test.ts +189 -0
  27. package/src/alerts/alert-observer.ts +208 -0
  28. package/src/alerts/alert-scheduler.ts +96 -0
  29. package/src/alerts/alert-storage.test.ts +54 -0
  30. package/src/alerts/alert-storage.ts +116 -0
  31. package/src/alerts/alert-store.svelte.ts +80 -0
  32. package/src/alerts/alert-types.ts +94 -0
  33. package/src/alerts.ts +28 -0
  34. package/src/board.dom.test.ts +941 -0
  35. package/src/board.ts +36 -0
  36. package/src/export.ts +13 -1
  37. package/src/expressions/evaluate.test.ts +111 -0
  38. package/src/expressions/evaluate.ts +228 -0
  39. package/src/expressions/expression-columns.ts +133 -0
  40. package/src/expressions/expression-types.ts +84 -0
  41. package/src/expressions/parse.test.ts +106 -0
  42. package/src/expressions/parse.ts +610 -0
  43. package/src/import.ts +2 -0
  44. package/src/index.ts +180 -46
  45. package/src/install.ts +12 -2
  46. package/src/pivot-enable.ts +44 -0
  47. package/src/scheduler-assignments.test.ts +97 -0
  48. package/src/scheduler-assignments.ts +134 -0
  49. package/src/scheduler-axis.test.ts +108 -0
  50. package/src/scheduler-axis.ts +238 -0
  51. package/src/scheduler-booking.test.ts +57 -0
  52. package/src/scheduler-booking.ts +63 -0
  53. package/src/scheduler-config.ts +179 -0
  54. package/src/scheduler-dependencies.test.ts +155 -0
  55. package/src/scheduler-dependencies.ts +223 -0
  56. package/src/scheduler-freebusy.test.ts +41 -0
  57. package/src/scheduler-freebusy.ts +36 -0
  58. package/src/scheduler-heatmap.test.ts +39 -0
  59. package/src/scheduler-heatmap.ts +55 -0
  60. package/src/scheduler-resource-tree.test.ts +84 -0
  61. package/src/scheduler-resource-tree.ts +107 -0
  62. package/src/scheduler-slots.test.ts +53 -0
  63. package/src/scheduler-slots.ts +94 -0
  64. package/src/scheduler-summary.test.ts +47 -0
  65. package/src/scheduler-summary.ts +81 -0
  66. package/src/sources/index.ts +1 -1
  67. package/src/sources/introspect-supabase.test.ts +13 -1
  68. package/src/sources/introspect-supabase.ts +20 -0
  69. package/src/studio/copilot-core.test.ts +45 -0
  70. package/src/studio/copilot-core.ts +65 -0
  71. package/src/studio/deploy-cli.test.ts +56 -0
  72. package/src/studio/deploy-cli.ts +100 -0
  73. package/src/studio/emit-project.test.ts +503 -18
  74. package/src/studio/emit-project.ts +829 -111
  75. package/src/studio/emit-schema.test.ts +17 -0
  76. package/src/studio/emit-schema.ts +166 -35
  77. package/src/studio/index.ts +25 -1
  78. package/src/studio/introspect-openapi.test.ts +84 -0
  79. package/src/studio/introspect-openapi.ts +252 -0
  80. package/src/studio/project.test.ts +57 -0
  81. package/src/studio/project.ts +193 -6
  82. package/src/studio/samples/crm.ts +282 -258
  83. package/src/studio/samples/fleet.ts +213 -186
  84. package/src/studio/samples/insurance.ts +223 -195
  85. package/src/studio/samples/inventory.ts +213 -182
  86. package/src/studio/samples/live-data.test.ts +99 -98
  87. package/src/studio/samples/live-data.ts +8 -10
  88. package/src/studio/samples/projects.ts +212 -190
  89. package/src/studio/samples/samples.test.ts +52 -0
  90. package/src/studio/samples/shared.ts +26 -6
  91. package/src/studio/samples/starter.ts +251 -0
  92. package/src/studio/samples/support.ts +209 -184
  93. package/src/studio/ui-components-surface.test.ts +185 -0
  94. package/src/studio/ui-components.generated.ts +5617 -0
  95. package/src/studio/ui-components.ts +641 -472
  96. package/src/sveltekit/sql-source.test.ts +13 -0
  97. package/src/sveltekit/sql-source.ts +11 -6
  98. package/src/upgrade-prompt.ts +2 -2
  99. package/src/watermark.ts +2 -2
  100. package/src/ai.test.ts +0 -522
  101. package/src/ai.ts +0 -1388
@@ -1,98 +1,99 @@
1
- import { describe, it, expect } from 'vitest'
2
- import { compile } from 'svelte/compiler'
3
- import { liveDataSamples, getLiveDataSample } from './live-data.js'
4
- import { validateProject, parseProject, serializeProject, type EntityDataSource } from '../project.js'
5
- import { emitStudioProject } from '../emit-project.js'
6
-
7
- describe('live-data starters', () => {
8
- it('ships a gallery with unique ids and card metadata', () => {
9
- expect(liveDataSamples.length).toBeGreaterThanOrEqual(3)
10
- const ids = liveDataSamples.map((s) => s.id)
11
- expect(new Set(ids).size).toBe(ids.length)
12
- for (const s of liveDataSamples) {
13
- expect(s.name).toBeTruthy()
14
- expect(s.description).toBeTruthy()
15
- expect(s.emoji).toBeTruthy()
16
- expect(s.accent).toMatch(/^#[0-9a-f]{6}$/i)
17
- }
18
- })
19
-
20
- it('getLiveDataSample resolves by id', () => {
21
- expect(getLiveDataSample('live-northwind-pglite')?.name).toBe('Northwind · Local Postgres')
22
- expect(getLiveDataSample('nope')).toBeUndefined()
23
- })
24
-
25
- for (const sample of liveDataSamples) {
26
- describe(sample.id, () => {
27
- const project = sample.build()
28
-
29
- it('is a valid project (no errors)', () => {
30
- const errors = validateProject(project).filter((i) => i.level === 'error')
31
- expect(errors, JSON.stringify(errors)).toHaveLength(0)
32
- })
33
-
34
- it('binds every entity to a REAL source (never memory)', () => {
35
- expect(project.entities.length).toBeGreaterThanOrEqual(1)
36
- for (const e of project.entities) {
37
- const src = project.dataSources?.[e.name] as EntityDataSource | undefined
38
- expect(src, `${e.name}: no source`).toBeTruthy()
39
- expect(src!.kind).not.toBe('memory')
40
- }
41
- })
42
-
43
- it('round-trips through parse/serialize', () => {
44
- const again = parseProject(serializeProject(project))
45
- expect(again.entities.map((e) => e.name)).toEqual(project.entities.map((e) => e.name))
46
- expect(again.dataSources).toEqual(project.dataSources)
47
- })
48
-
49
- it('generates compiling Svelte pages', () => {
50
- const files = emitStudioProject(project)
51
- for (const f of files.filter((f) => f.path.endsWith('.svelte'))) {
52
- expect(() => compile(f.contents, { filename: f.path, generate: 'client' }), f.path).not.toThrow()
53
- }
54
- })
55
- })
56
- }
57
-
58
- it('every starter is a real app: a dashboard, a detail page, and drill-through', () => {
59
- for (const s of liveDataSamples) {
60
- const blocks = s.build().screens.flatMap((sc) => sc.blocks)
61
- expect(blocks.some((b) => b.config.kind === 'kpi'), `${s.id}: no dashboard KPIs`).toBe(true)
62
- const all = emitStudioProject(s.build()).map((f) => f.contents).join('\n')
63
- expect(all.includes('SvRecordDetail'), `${s.id}: no detail page`).toBe(true)
64
- expect(all.includes('import { goto }'), `${s.id}: no drill-through`).toBe(true)
65
- expect(all.includes("selectedId={$page.url.searchParams.get('id')"), `${s.id}: detail not URL-addressable`).toBe(true)
66
- }
67
- })
68
-
69
- it('the Northwind (Supabase) detail timelines match the denormalized view by name/company', () => {
70
- const all = emitStudioProject(getLiveDataSample('live-northwind-supabase')!.build()).map((f) => f.contents).join('\n')
71
- expect(all).toContain('parentField: "name"') // product -> order_lines by product name
72
- expect(all).toContain('parentField: "company"') // customer -> order_lines by company
73
- })
74
-
75
- it('PGlite starter generates embedded Postgres bootstrap + seeded rows', () => {
76
- const data = emitStudioProject(getLiveDataSample('live-northwind-pglite')!.build()).find((f) => f.path.endsWith('src/lib/data.ts'))!.contents
77
- expect(data).toContain("import { PGlite } from '@electric-sql/pglite'")
78
- expect(data).toContain('createSqlDataSource')
79
- expect(data).toContain('Chai') // a curated seed row is baked in
80
- })
81
-
82
- it('Supabase starter generates a client bound to the hosted sample', () => {
83
- const files = emitStudioProject(getLiveDataSample('live-northwind-supabase')!.build())
84
- const conn = files.find((f) => f.path.endsWith('src/lib/connections.ts'))!.contents
85
- expect(conn).toContain('rbnnlzgtfzsylllniozo.supabase.co')
86
- expect(conn).toContain('createClient')
87
- const data = files.find((f) => f.path.endsWith('src/lib/data.ts'))!.contents
88
- expect(data).toContain('createSupabaseDataSource')
89
- expect(data).toContain("table: 'order_lines'")
90
- })
91
-
92
- it('REST starter generates createRestDataSource against DummyJSON', () => {
93
- const data = emitStudioProject(getLiveDataSample('live-dummyjson-rest')!.build()).find((f) => f.path.endsWith('src/lib/data.ts'))!.contents
94
- expect(data).toContain('createRestDataSource')
95
- expect(data).toContain('dummyjson.com/products')
96
- expect(data).toContain('body?.products') // rowsPath parse
97
- })
98
- })
1
+ import { describe, it, expect } from 'vitest'
2
+ import { compile } from 'svelte/compiler'
3
+ import { liveDataSamples, getLiveDataSample } from './live-data.js'
4
+ import { validateProject, parseProject, serializeProject, type EntityDataSource } from '../project.js'
5
+ import { emitStudioProject } from '../emit-project.js'
6
+
7
+ describe('live-data starters', () => {
8
+ it('ships a gallery with unique ids and card metadata', () => {
9
+ expect(liveDataSamples.length).toBeGreaterThanOrEqual(3)
10
+ const ids = liveDataSamples.map((s) => s.id)
11
+ expect(new Set(ids).size).toBe(ids.length)
12
+ for (const s of liveDataSamples) {
13
+ expect(s.name).toBeTruthy()
14
+ expect(s.description).toBeTruthy()
15
+ expect(s.emoji).toBeTruthy()
16
+ expect(s.accent).toMatch(/^#[0-9a-f]{6}$/i)
17
+ }
18
+ })
19
+
20
+ it('getLiveDataSample resolves by id', () => {
21
+ expect(getLiveDataSample('live-northwind-pglite')?.name).toBe('Northwind · Local Postgres')
22
+ expect(getLiveDataSample('nope')).toBeUndefined()
23
+ })
24
+
25
+ for (const sample of liveDataSamples) {
26
+ describe(sample.id, () => {
27
+ const project = sample.build()
28
+
29
+ it('is a valid project (no errors)', () => {
30
+ const errors = validateProject(project).filter((i) => i.level === 'error')
31
+ expect(errors, JSON.stringify(errors)).toHaveLength(0)
32
+ })
33
+
34
+ it('binds every entity to a REAL source (never memory)', () => {
35
+ expect(project.entities.length).toBeGreaterThanOrEqual(1)
36
+ for (const e of project.entities) {
37
+ const src = project.dataSources?.[e.name] as EntityDataSource | undefined
38
+ expect(src, `${e.name}: no source`).toBeTruthy()
39
+ expect(src!.kind).not.toBe('memory')
40
+ }
41
+ })
42
+
43
+ it('round-trips through parse/serialize', () => {
44
+ const again = parseProject(serializeProject(project))
45
+ expect(again.entities.map((e) => e.name)).toEqual(project.entities.map((e) => e.name))
46
+ expect(again.dataSources).toEqual(project.dataSources)
47
+ })
48
+
49
+ it('generates compiling Svelte pages', () => {
50
+ const files = emitStudioProject(project)
51
+ for (const f of files.filter((f) => f.path.endsWith('.svelte'))) {
52
+ expect(() => compile(f.contents, { filename: f.path, generate: 'client' }), f.path).not.toThrow()
53
+ }
54
+ })
55
+ })
56
+ }
57
+
58
+ it('every starter is a real app: a dashboard, a detail page, and drill-through', () => {
59
+ for (const s of liveDataSamples) {
60
+ const blocks = s.build().screens.flatMap((sc) => sc.blocks)
61
+ expect(blocks.some((b) => b.config.kind === 'kpi'), `${s.id}: no dashboard KPIs`).toBe(true)
62
+ const all = emitStudioProject(s.build()).map((f) => f.contents).join('\n')
63
+ expect(all.includes('SvRecordDetail'), `${s.id}: no detail page`).toBe(true)
64
+ expect(all.includes('import { goto }'), `${s.id}: no drill-through`).toBe(true)
65
+ expect(all.includes("selectedId={$page.url.searchParams.get('id')"), `${s.id}: detail not URL-addressable`).toBe(true)
66
+ }
67
+ })
68
+
69
+ it('the Northwind (Supabase) detail timelines match the denormalized view by name/company', () => {
70
+ const all = emitStudioProject(getLiveDataSample('live-northwind-supabase')!.build()).map((f) => f.contents).join('\n')
71
+ expect(all).toContain('parentField: "name"') // product -> order_lines by product name
72
+ expect(all).toContain('parentField: "company"') // customer -> order_lines by company
73
+ })
74
+
75
+ it('PGlite starter generates embedded Postgres bootstrap + seeded rows', () => {
76
+ const data = emitStudioProject(getLiveDataSample('live-northwind-pglite')!.build()).find((f) => f.path.endsWith('src/lib/data.ts'))!.contents
77
+ expect(data).toContain("import { PGlite } from '@electric-sql/pglite'")
78
+ expect(data).toContain('createSqlDataSource')
79
+ expect(data).toContain('Chai') // a curated seed row is baked in
80
+ })
81
+
82
+ it('Supabase starter generates a client bound to the hosted sample', () => {
83
+ const files = emitStudioProject(getLiveDataSample('live-northwind-supabase')!.build())
84
+ const conn = files.find((f) => f.path.endsWith('src/lib/connections.ts'))!.contents
85
+ expect(conn).toContain('rbnnlzgtfzsylllniozo.supabase.co')
86
+ expect(conn).toContain('createClient')
87
+ const data = files.find((f) => f.path.endsWith('src/lib/data.ts'))!.contents
88
+ expect(data).toContain('createSupabaseDataSource')
89
+ expect(data).toContain("table: 'order_lines'")
90
+ })
91
+
92
+ it('REST starter generates createRestDataSource against DummyJSON', () => {
93
+ const data = emitStudioProject(getLiveDataSample('live-dummyjson-rest')!.build()).find((f) => f.path.endsWith('src/lib/data.ts'))!.contents
94
+ expect(data).toContain('createRestDataSource')
95
+ expect(data).toContain('dummyjson.com/products')
96
+ expect(data).toContain('dummyJsonAdapter') // real server paging via the adapter (skip/limit/sortBy)
97
+ expect(data).not.toContain('body?.products') // adapter unwraps { products, total }, no manual parse
98
+ })
99
+ })
@@ -260,10 +260,9 @@ const dummyjsonRest: SampleApp = {
260
260
  preset: 'shadcn',
261
261
  dataSource: 'rest',
262
262
  entities: [restProduct],
263
- // A page size that covers the whole catalogue: the grid asks for one page,
264
- // so it reads the entire live feed in a single request. (DummyJSON pages by
265
- // `skip`, not `offset`, so deep server-paging would need a REST adapter -
266
- // see docs/enterprise/studio/rest-api.md.)
263
+ // The DummyJSON REST adapter gives the grid REAL server-side paging + sort
264
+ // (skip/limit/sortBy), and the dashboard aggregates fetch the catalogue in one
265
+ // large page. No pageSize hack, no manual row/total mapping.
267
266
  screens: [
268
267
  // Catalog dashboard: price / rating / stock KPIs + category & brand breakdowns
269
268
  // over the live feed, with the grid drilling into a product page.
@@ -275,7 +274,7 @@ const dummyjsonRest: SampleApp = {
275
274
  { chart: 'category', reduce: 'count', type: 'bar', span: 2 },
276
275
  { chart: 'brand', measure: 'price', reduce: 'avg', type: 'bar', span: 1 },
277
276
  { filter: ['category', 'brand'], span: 3 },
278
- { grid: true, pageSize: 200, rowLink: { screen: 'product-detail', sourceField: 'id', targetField: 'id' }, span: 3 },
277
+ { grid: true, rowLink: { screen: 'product-detail', sourceField: 'id', targetField: 'id' }, span: 3 },
279
278
  ]),
280
279
  detailScreen(restProduct, { id: 'product-detail', title: 'Product', order: 1 }, {
281
280
  titleField: 'title', subtitleField: 'brand', metricFields: ['price', 'rating', 'stock'],
@@ -283,16 +282,15 @@ const dummyjsonRest: SampleApp = {
283
282
  }),
284
283
  ],
285
284
  sources: {
286
- // Rows + total read from DummyJSON's { products, total } envelope. `limit=0`
287
- // asks DummyJSON for the whole catalogue so the dashboard aggregates it all.
285
+ // The dummyjson adapter maps skip/limit/sortBy + unwraps the { products, total }
286
+ // envelope, so the grid pages + sorts against the live feed for real.
288
287
  products: {
289
288
  kind: 'rest',
290
289
  baseUrl: 'https://dummyjson.com',
291
290
  path: 'products',
292
291
  method: 'GET',
293
- params: [{ name: 'limit', location: 'query', type: 'number', value: '0' }],
294
- rowsPath: 'products',
295
- totalPath: 'total',
292
+ params: [],
293
+ adapter: { kind: 'dummyjson', rowsKey: 'products' },
296
294
  },
297
295
  },
298
296
  })