@svgrid/enterprise 2.2.1 → 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 +22468 -10459
  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 +6 -0
  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
@@ -0,0 +1,185 @@
1
+ import { execFileSync } from 'node:child_process'
2
+ import { dirname, join } from 'node:path'
3
+ import { fileURLToPath } from 'node:url'
4
+ import { describe, expect, it } from 'vitest'
5
+ import { compile } from 'svelte/compiler'
6
+ import { GENERATED_UI_SURFACE } from './ui-components.generated'
7
+ import { UI_COMPONENT_REGISTRY, uiComponentSpec, gridPropSurface } from './ui-components'
8
+ import { addComponentBlock, addFreestandingScreen, createProject, eventSlot, setHandlerBody, setHandlerSteps, updateBlock, type GridConfig } from './project'
9
+ import { emitStudioProject } from './emit-project'
10
+ import type { EntitySchema } from '../schema'
11
+
12
+ const customers: EntitySchema = {
13
+ name: 'customers',
14
+ label: 'Customers',
15
+ idField: 'id',
16
+ fields: [
17
+ { field: 'id', label: 'Id', type: 'text', primaryKey: true },
18
+ { field: 'name', label: 'Name', type: 'text' },
19
+ ],
20
+ }
21
+
22
+ describe('full component surface (extractor-driven)', () => {
23
+ it('the generated surface is current (regen --check)', () => {
24
+ const here = dirname(fileURLToPath(import.meta.url))
25
+ // Throws (non-zero exit) when a kit prop change was not re-extracted.
26
+ expect(() => execFileSync(process.execPath, [join(here, '../../scripts/extract-ui-props.mjs'), '--check'], { encoding: 'utf8' })).not.toThrow()
27
+ })
28
+
29
+ it('every registered component has an extracted surface, tooltips, and events metadata', () => {
30
+ for (const spec of UI_COMPONENT_REGISTRY) {
31
+ const generated = GENERATED_UI_SURFACE[spec.importName]
32
+ expect(generated, `${spec.key} (${spec.importName}) missing from the extractor's COMPONENTS list`).toBeTruthy()
33
+ // The merged registry exposes the FULL surface, not just curated chrome.
34
+ expect(spec.props.length, `${spec.key} lost extracted props in the merge`).toBeGreaterThanOrEqual(generated!.props.length - (spec.hiddenProps?.length ?? 0) - (spec.fixed?.length ?? 0))
35
+ // Guidance exists: at least one prop carries a JSDoc-sourced tooltip.
36
+ // (Components whose surface is fully hand-curated chrome may have none.)
37
+ if (generated!.props.some((p) => p.description)) {
38
+ expect(spec.props.some((p) => p.description), `${spec.key} dropped descriptions in the merge`).toBe(true)
39
+ }
40
+ }
41
+ })
42
+
43
+ it('every registered component emits and its screen compiles', () => {
44
+ let p = addFreestandingScreen(createProject([customers]), { title: 'Kitchen sink', route: 'sink' })
45
+ const sid = p.screens.find((s) => s.title === 'Kitchen sink')!.id
46
+ for (const spec of UI_COMPONENT_REGISTRY) p = addComponentBlock(p, sid, spec.key)
47
+ const page = emitStudioProject(p).find((f) => f.path === 'src/routes/sink/+page.svelte')!
48
+ for (const spec of UI_COMPONENT_REGISTRY) expect(page.contents, `missing <${spec.importName}`).toContain(`<${spec.importName}`)
49
+ expect(() => compile(page.contents, { filename: page.path, generate: 'client' })).not.toThrow()
50
+ })
51
+
52
+ it('json props emit as literal expressions (radio group options)', () => {
53
+ let p = addFreestandingScreen(createProject([customers]), { title: 'R', route: 'r' })
54
+ const sid = p.screens.find((s) => s.title === 'R')!.id
55
+ p = addComponentBlock(p, sid, 'radio-group')
56
+ const page = emitStudioProject(p).find((f) => f.path === 'src/routes/r/+page.svelte')!.contents
57
+ expect(page).toMatch(/options=\{\[\{"value":"a","label":"Option A"\}/)
58
+ })
59
+
60
+ it('every component exposes the full standard DOM event surface (wired + wireable)', () => {
61
+ let p = addFreestandingScreen(createProject([customers]), { title: 'E', route: 'e' })
62
+ const sid = p.screens.find((s) => s.title === 'E')!.id
63
+ p = addComponentBlock(p, sid, 'button')
64
+ const block = p.screens.find((s) => s.id === sid)!.blocks.at(-1)!
65
+ // The wrapper forwards focus/blur (focusin/focusout), keydown, contextmenu, ...
66
+ p = setHandlerSteps(p, sid, eventSlot('keydown', block.id), [{ type: 'alert', message: 'k' }])
67
+ p = setHandlerSteps(p, sid, eventSlot('focus', block.id), [{ type: 'alert', message: 'f' }])
68
+ const files = emitStudioProject(p)
69
+ const page = files.find((f) => f.path === 'src/routes/e/+page.svelte')!.contents
70
+ expect(page).toMatch(/onkeydown=\{\(e\) => \w+\.fire\('keydown', e\)\}/)
71
+ expect(page).toMatch(/onfocusin=\{\(e\) => \w+\.fire\('focus', e\)\}/) // focus via focusin
72
+ expect(page).toMatch(/onfocusout=\{\(e\) => \w+\.fire\('blur', e\)\}/)
73
+ expect(page).toMatch(/oncontextmenu=\{\(e\) => \w+\.fire\('contextmenu', e\)\}/)
74
+ const handlers = files.find((f) => f.path === 'src/routes/e/handlers.ts')!.contents
75
+ expect(handlers).toMatch(/\.onkeydown = async \(\) => \{/)
76
+ expect(handlers).toMatch(/\.onfocus = async \(\) => \{/)
77
+ expect(() => compile(page, { filename: 'e.svelte', generate: 'client' })).not.toThrow()
78
+ })
79
+
80
+ it('the handle proxy honors a camelCase on<Event> assignment (ctx.x.onKeyDown = fn)', () => {
81
+ // Double-click inserts `ctx.list_box1.onKeyDown = (e) => {}` into onLoad; the wrapper
82
+ // fires the lowercase DOM key ('keydown'), so the proxy must normalize casing.
83
+ let p = addFreestandingScreen(createProject([customers]), { title: 'K', route: 'k' })
84
+ const sid = p.screens.find((s) => s.title === 'K')!.id
85
+ p = addComponentBlock(p, sid, 'list-box')
86
+ // Double-clicking an event writes `ctx.list_box1.onKeyDown = (e) => {}` into onLoad (code mode).
87
+ p = setHandlerBody(p, sid, 'onLoad', 'ctx.list_box1.onKeyDown = (e) => {}')
88
+ const handles = emitStudioProject(p).find((f) => f.path === 'src/lib/handles.svelte.ts')!.contents
89
+ // onEvent + fire both lowercase, so onKeyDown, onkeydown and fire('keydown') all agree.
90
+ expect(handles).toContain('[name.toLowerCase()]: fn')
91
+ expect(handles).toContain('this.on[name.toLowerCase()]?.(e)')
92
+ // The set trap accepts any on<Event> casing (onKeyDown = fn), not just lowercase.
93
+ expect(handles).toContain('/^on[A-Za-z]/.test(k)) { t.onEvent(k.slice(2), v')
94
+ })
95
+
96
+ it('the Grid exposes its full event surface as ctx.grid subscriptions', () => {
97
+ // An entity screen's grid, made code-enabled by an onLoad body that subscribes.
98
+ let p = createProject([customers])
99
+ const sid = p.screens[0].id
100
+ p = setHandlerBody(p, sid, 'onLoad', 'ctx.grid.onCellClick = (e) => { console.log(e.row) }')
101
+ const files = emitStudioProject(p)
102
+ const page = files.find((f) => f.path.endsWith('+page.svelte'))!.contents
103
+ const ctxFile = files.find((f) => f.path.endsWith('page-context.ts'))!.contents
104
+ // ctx.grid is the real SvGridApi PLUS typed event setters (so ctx.grid.onCellClick type-checks).
105
+ expect(ctxFile).toContain('grid: SvGridApi<any, Customers> &')
106
+ expect(ctxFile).toContain('onCellClick?:')
107
+ expect(ctxFile).toContain('onRowSelectionChange?:')
108
+ // A grid handle wraps the api; free events fire into it, built-in props compose the fire.
109
+ expect(page).toContain('const gridCtx = gridHandle(() => gridApi)')
110
+ expect(page).toContain("onCellClick={(...a) => gridCtx.fire('cellClick', ...a)}")
111
+ expect(page).toContain("onSortingChange={(s) => { controller.setSort(s); gridCtx.fire('sortingChange', s) }}")
112
+ // Data events (row added/updated/deleted) fire from the wrapped controller, not a grid prop.
113
+ expect(ctxFile).toContain('onRowAdded?:')
114
+ expect(ctxFile).toContain('onRowDeleted?:')
115
+ expect(page).toContain("const r = await __ctl.createRow(i); gridCtx.fire('rowAdded', r)")
116
+ expect(page).toContain("await __ctl.deleteRow(id); gridCtx.fire('rowDeleted', id)")
117
+ expect(page).not.toContain('onRowAdded={') // never a grid markup prop
118
+ expect(() => compile(page, { filename: 'g.svelte', generate: 'client' })).not.toThrow()
119
+ })
120
+
121
+ it('ctx.grid exposes runtime-settable props (ctx.grid.sortable = true) with types + wiring', () => {
122
+ // An entity screen's grid, made code-enabled by an onLoad body that sets a prop.
123
+ let p = createProject([customers])
124
+ const sid = p.screens[0].id
125
+ p = setHandlerBody(p, sid, 'onLoad', 'ctx.grid.sortable = true\n ctx.grid.zebraRows = true\n if (ctx.grid.sortable) ctx.grid.rowHeight = 40')
126
+ const files = emitStudioProject(p)
127
+ const page = files.find((f) => f.path.endsWith('+page.svelte'))!.contents
128
+ const ctxFile = files.find((f) => f.path.endsWith('page-context.ts'))!.contents
129
+ const handles = files.find((f) => f.path === 'src/lib/handles.svelte.ts')!.contents
130
+ // Type: ctx.grid gains the settable prop surface, so `ctx.grid.sortable = true`
131
+ // type-checks (intellisense in the code view) - alongside the api + event setters.
132
+ expect(ctxFile).toContain('grid: SvGridApi<any, Customers> &')
133
+ expect(ctxFile).toContain('sortable?: boolean')
134
+ expect(ctxFile).toContain('zebraRows?: boolean')
135
+ expect(ctxFile).toContain('rowHeight?: number')
136
+ expect(ctxFile).not.toContain('data?:') // structural prop excluded
137
+ // Runtime: the gridHandle set trap routes a prop assignment to the reactive
138
+ // setOption channel; reads route to getOption.
139
+ expect(handles).toContain("typeof api.setOption === 'function'")
140
+ expect(handles).toContain('(api.setOption as (key: unknown, value: unknown) => void)(k, val)')
141
+ expect(handles).toContain('(api.getOption as (key: unknown) => unknown)(k)')
142
+ // The generated page compiles with the assignments in onLoad.
143
+ expect(() => compile(page, { filename: 'gp.svelte', generate: 'client' })).not.toThrow()
144
+ })
145
+
146
+ it('the grid exposes its full SvGrid prop surface as "All properties" overrides', () => {
147
+ // Uncurated surface = every extracted SvGrid prop the curated controls don't manage.
148
+ const surf = gridPropSurface()
149
+ expect(surf.length).toBeGreaterThan(20)
150
+ const keys = new Set(surf.map((p) => p.key))
151
+ expect(keys.has('virtualization')).toBe(true)
152
+ expect(keys.has('rowNumberWidth')).toBe(true)
153
+ expect(keys.has('sortable')).toBe(false) // curated -> not offered again here
154
+ expect(keys.has('data')).toBe(false)
155
+ // Overrides pass straight through to <SvGrid>, deduped against curated props.
156
+ let p = createProject([customers])
157
+ const sid = p.screens[0]!.id
158
+ const blk = p.screens.find((s) => s.id === sid)!.blocks.find((b) => b.config.kind === 'grid')!
159
+ p = updateBlock(p, sid, blk.id, { config: { props: { virtualization: false, rowNumberWidth: 44, sortable: false } } as Partial<GridConfig> })
160
+ const page = emitStudioProject(p).find((f) => f.path.endsWith('+page.svelte'))!.contents
161
+ expect(page).toContain('virtualization={false}')
162
+ expect(page).toContain('rowNumberWidth={44}')
163
+ // `sortable` is curated (emitted once) - the raw override must NOT duplicate it.
164
+ expect((page.match(/\bsortable\b/g) ?? []).length).toBe(1)
165
+ expect(() => compile(page, { filename: 'g2.svelte', generate: 'client' })).not.toThrow()
166
+ })
167
+
168
+ it('declared events wire through the component callback prop + a method slot each', () => {
169
+ const spec = uiComponentSpec('tree')!
170
+ expect(spec.events!.map((e) => e.key)).toEqual(expect.arrayContaining(['select', 'toggle', 'check']))
171
+ // Steps on a non-click/change event compile to ctx.<name>.on<event> wiring.
172
+ let p = addFreestandingScreen(createProject([customers]), { title: 'T', route: 't' })
173
+ const sid = p.screens.find((s) => s.title === 'T')!.id
174
+ p = addComponentBlock(p, sid, 'tree')
175
+ const block = p.screens.find((s) => s.id === sid)!.blocks.at(-1)!
176
+ p = setHandlerSteps(p, sid, eventSlot('select', block.id), [{ type: 'alert', message: 'picked' }])
177
+ const files = emitStudioProject(p)
178
+ const page = files.find((f) => f.path === 'src/routes/t/+page.svelte')!.contents
179
+ expect(page).toMatch(/onSelect=\{\(e\) => \w+\.fire\('select', e\)\}/)
180
+ // The visual steps compile into the handlers companion, wired via the handle.
181
+ const handlers = files.find((f) => f.path === 'src/routes/t/handlers.ts')!.contents
182
+ expect(handlers).toMatch(/\.onselect = async \(\) => \{/)
183
+ expect(handlers).toContain("alert('picked')")
184
+ })
185
+ })