@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.
- package/README.md +95 -81
- package/dist/cdn/svgrid-enterprise.svelte-external.js +22468 -10459
- package/dist/node/studio.js +11590 -2907
- package/package.json +31 -10
- 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 +2355 -0
- package/src/SvGridEditPanel.svelte +849 -794
- package/src/SvGridScheduler.svelte +5334 -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} +4 -1
- package/src/{ai-export-xlsx.test.ts → ai-export-xlsx.dom.test.ts} +4 -1
- package/src/{ai-export.test.ts → ai-export.dom.test.ts} +5 -1
- 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.ts +6 -0
- package/src/expressions/evaluate.test.ts +111 -0
- package/src/expressions/evaluate.ts +228 -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.ts +2 -0
- package/src/index.ts +180 -46
- package/src/install.ts +12 -2
- package/src/pivot-enable.ts +44 -0
- 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/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 +503 -18
- package/src/studio/emit-project.ts +829 -111
- package/src/studio/emit-schema.test.ts +17 -0
- package/src/studio/emit-schema.ts +166 -35
- package/src/studio/index.ts +25 -1
- package/src/studio/introspect-openapi.test.ts +84 -0
- package/src/studio/introspect-openapi.ts +252 -0
- package/src/studio/project.test.ts +57 -0
- package/src/studio/project.ts +193 -6
- package/src/studio/samples/crm.ts +282 -258
- 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 +52 -0
- package/src/studio/samples/shared.ts +26 -6
- package/src/studio/samples/starter.ts +251 -0
- package/src/studio/samples/support.ts +209 -184
- package/src/studio/ui-components-surface.test.ts +185 -0
- package/src/studio/ui-components.generated.ts +5617 -0
- package/src/studio/ui-components.ts +641 -472
- package/src/sveltekit/sql-source.test.ts +13 -0
- package/src/sveltekit/sql-source.ts +11 -6
- 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
|
@@ -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
|
+
})
|