@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,54 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { createAlertRules, memoryAlertRules } from './alert-storage'
3
+ import type { AlertRule } from './alert-types'
4
+
5
+ const rule = (id: string, over: Partial<AlertRule> = {}): AlertRule => ({
6
+ id,
7
+ name: `Rule ${id}`,
8
+ enabled: true,
9
+ severity: 'info',
10
+ scope: 'row',
11
+ predicate: { kind: 'const', value: true },
12
+ trigger: { type: 'dataChange' },
13
+ actions: [{ kind: 'toast' }],
14
+ createdAt: 0,
15
+ ...over,
16
+ })
17
+
18
+ describe('createAlertRules CRUD', () => {
19
+ it('saves, replaces, gets, removes and toggles', () => {
20
+ const mgr = createAlertRules(memoryAlertRules())
21
+ mgr.save(rule('a'))
22
+ mgr.save(rule('b'))
23
+ expect(mgr.list().map((r) => r.id)).toEqual(['a', 'b'])
24
+
25
+ // Save with an existing id replaces in place.
26
+ mgr.save(rule('a', { name: 'Renamed' }))
27
+ expect(mgr.get('a')?.name).toBe('Renamed')
28
+ expect(mgr.list()).toHaveLength(2)
29
+
30
+ expect(mgr.toggle('a')).toBe(false)
31
+ expect(mgr.get('a')?.enabled).toBe(false)
32
+
33
+ expect(mgr.rename('b', 'Bee')).toBe(true)
34
+ expect(mgr.get('b')?.name).toBe('Bee')
35
+
36
+ expect(mgr.remove('a')).toBe(true)
37
+ expect(mgr.remove('missing')).toBe(false)
38
+ expect(mgr.list().map((r) => r.id)).toEqual(['b'])
39
+ })
40
+ })
41
+
42
+ describe('export / import share', () => {
43
+ it('round-trips a rule set through JSON', () => {
44
+ const src = createAlertRules(memoryAlertRules())
45
+ src.save(rule('a'))
46
+ src.save(rule('b', { severity: 'error' }))
47
+ const json = src.export()
48
+
49
+ const dest = createAlertRules(memoryAlertRules())
50
+ const imported = dest.import(json)
51
+ expect(imported).toHaveLength(2)
52
+ expect(dest.get('b')?.severity).toBe('error')
53
+ })
54
+ })
@@ -0,0 +1,116 @@
1
+ /**
2
+ * Alert-rule persistence + sharing. Mirrors the grid's `named-views` storage
3
+ * shape: a pluggable `AlertRulesStorage` (in-memory or localStorage), wrapped by
4
+ * a small CRUD manager. `export()` / `import()` make a rule set shareable as
5
+ * JSON, so a team lead can hand round a set of alerts.
6
+ */
7
+ import type { AlertRule } from './alert-types'
8
+
9
+ export type AlertRulesStorage = {
10
+ read(): AlertRule[]
11
+ write(rules: AlertRule[]): void
12
+ }
13
+
14
+ /** In-memory storage (default). State lives only as long as the manager. */
15
+ export function memoryAlertRules(initial: AlertRule[] = []): AlertRulesStorage {
16
+ let rules = [...initial]
17
+ return {
18
+ read: () => rules,
19
+ write: (next) => {
20
+ rules = next
21
+ },
22
+ }
23
+ }
24
+
25
+ /** localStorage-backed storage. Safe to construct under SSR (no-ops there). */
26
+ export function localStorageAlertRules(key: string): AlertRulesStorage {
27
+ const available = typeof localStorage !== 'undefined'
28
+ return {
29
+ read() {
30
+ if (!available) return []
31
+ try {
32
+ const raw = localStorage.getItem(key)
33
+ const parsed = raw ? JSON.parse(raw) : []
34
+ return Array.isArray(parsed) ? (parsed as AlertRule[]) : []
35
+ } catch {
36
+ return []
37
+ }
38
+ },
39
+ write(rules) {
40
+ if (!available) return
41
+ try {
42
+ localStorage.setItem(key, JSON.stringify(rules))
43
+ } catch {
44
+ // quota / private-mode - persistence is best-effort
45
+ }
46
+ },
47
+ }
48
+ }
49
+
50
+ export type AlertRulesManager = {
51
+ list(): AlertRule[]
52
+ get(id: string): AlertRule | undefined
53
+ /** Insert or replace a rule (matched by id). Returns the saved rule. */
54
+ save(rule: AlertRule): AlertRule
55
+ remove(id: string): boolean
56
+ rename(id: string, name: string): boolean
57
+ /** Flip a rule's `enabled` flag. Returns the new value (or undefined). */
58
+ toggle(id: string): boolean | undefined
59
+ /** Serialise the whole rule set to shareable JSON. */
60
+ export(): string
61
+ /** Replace the rule set from JSON produced by `export()`. */
62
+ import(json: string): AlertRule[]
63
+ }
64
+
65
+ /** CRUD manager over a storage adapter (defaults to in-memory). */
66
+ export function createAlertRules(
67
+ storage: AlertRulesStorage = memoryAlertRules(),
68
+ ): AlertRulesManager {
69
+ const load = () => storage.read()
70
+ const commit = (rules: AlertRule[]) => storage.write(rules)
71
+
72
+ return {
73
+ list: () => load(),
74
+ get: (id) => load().find((r) => r.id === id),
75
+ save(rule) {
76
+ const rules = load()
77
+ const i = rules.findIndex((r) => r.id === rule.id)
78
+ if (i >= 0) rules[i] = rule
79
+ else rules.push(rule)
80
+ commit(rules)
81
+ return rule
82
+ },
83
+ remove(id) {
84
+ const rules = load()
85
+ const next = rules.filter((r) => r.id !== id)
86
+ if (next.length === rules.length) return false
87
+ commit(next)
88
+ return true
89
+ },
90
+ rename(id, name) {
91
+ const rules = load()
92
+ const rule = rules.find((r) => r.id === id)
93
+ if (!rule) return false
94
+ rule.name = name
95
+ commit(rules)
96
+ return true
97
+ },
98
+ toggle(id) {
99
+ const rules = load()
100
+ const rule = rules.find((r) => r.id === id)
101
+ if (!rule) return undefined
102
+ rule.enabled = !rule.enabled
103
+ commit(rules)
104
+ return rule.enabled
105
+ },
106
+ export() {
107
+ return JSON.stringify(load(), null, 2)
108
+ },
109
+ import(json) {
110
+ const parsed = JSON.parse(json)
111
+ const rules = Array.isArray(parsed) ? (parsed as AlertRule[]) : []
112
+ commit(rules)
113
+ return rules
114
+ },
115
+ }
116
+ }
@@ -0,0 +1,80 @@
1
+ /**
2
+ * alert-store - the reactive log behind the alerts badge + panel. A singleton
3
+ * queue of fired `AlertEvent`s (newest first) with an unacknowledged count,
4
+ * mirroring the grid's `toast-store` shape. The observer pushes events here; the
5
+ * badge reads `activeCount`; the panel lists, acknowledges, and clears them.
6
+ *
7
+ * Perf: a burst of events from one evaluation is written in ONE array update via
8
+ * `pushMany` (not one reassignment per event), and `activeCount` is a maintained
9
+ * counter - not an O(n) reduce recomputed on every render.
10
+ */
11
+ import type { AlertEvent } from './alert-types'
12
+
13
+ const MAX = 500
14
+
15
+ function createAlertStore() {
16
+ let events = $state<AlertEvent[]>([])
17
+ let active = $state(0)
18
+ let seq = 0
19
+
20
+ /** Recompute the unacknowledged counter after a trim/clear/ack. */
21
+ function recount(list: ReadonlyArray<AlertEvent>): number {
22
+ let n = 0
23
+ for (const e of list) if (!e.acknowledged) n++
24
+ return n
25
+ }
26
+
27
+ return {
28
+ get events() {
29
+ return events
30
+ },
31
+ /** Number of unacknowledged events (what the badge shows). */
32
+ get activeCount() {
33
+ return active
34
+ },
35
+ /** Append one event (kept for API compatibility; prefer `pushMany`). */
36
+ push(event: AlertEvent) {
37
+ this.pushMany([event])
38
+ },
39
+ /** Append a batch of events in a single reactive update (newest first). */
40
+ pushMany(batch: ReadonlyArray<AlertEvent>) {
41
+ if (batch.length === 0) return
42
+ const tagged: AlertEvent[] = []
43
+ let added = 0
44
+ // Newest first: reverse so the last event in the batch ends up on top.
45
+ for (let i = batch.length - 1; i >= 0; i--) {
46
+ tagged.push({ ...batch[i]!, _id: ++seq } as AlertEvent & { _id: number })
47
+ if (!batch[i]!.acknowledged) added++
48
+ }
49
+ let next = [...tagged, ...events]
50
+ if (next.length > MAX) {
51
+ next = next.slice(0, MAX)
52
+ // A trim can drop unacknowledged events - recount to stay exact.
53
+ active = recount(next)
54
+ } else {
55
+ active += added
56
+ }
57
+ events = next
58
+ },
59
+ acknowledge(index: number) {
60
+ const next = events.slice()
61
+ const cur = next[index]
62
+ if (cur && !cur.acknowledged) {
63
+ next[index] = { ...cur, acknowledged: true }
64
+ active = Math.max(0, active - 1)
65
+ events = next
66
+ }
67
+ },
68
+ acknowledgeAll() {
69
+ events = events.map((e) => (e.acknowledged ? e : { ...e, acknowledged: true }))
70
+ active = 0
71
+ },
72
+ clear() {
73
+ events = []
74
+ active = 0
75
+ },
76
+ }
77
+ }
78
+
79
+ /** The shared alerts log. Read `.events` / `.activeCount` inside a component. */
80
+ export const alertStore = createAlertStore()
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Alert Rules engine - the model layer.
3
+ *
4
+ * An alert rule is a declarative "when this predicate holds (or this value
5
+ * moves), do these things": raise a toast, paint a badge/highlight, flash the
6
+ * cell, veto the edit, or log it. Rules are authored by end users at runtime,
7
+ * persisted, and shareable - the AdapTable-style alerting surface, built on the
8
+ * grid's own filter/conditional-format engines.
9
+ *
10
+ * Predicates and change tests come from the expression layer (`../expressions`).
11
+ */
12
+ import type { ChangeExpr, PredicateExpr } from '../expressions/expression-types'
13
+ import type { Schedule } from '../scheduling'
14
+
15
+ /** Maps onto the toast variants + conditional-format intent. */
16
+ export type AlertSeverity = 'info' | 'success' | 'warning' | 'error'
17
+
18
+ /** What the rule watches over. */
19
+ export type AlertScope = 'row' | 'cell' | 'aggregate'
20
+
21
+ /**
22
+ * When a rule fires:
23
+ * - `dataChange` - a row newly satisfies `predicate` after an edit/update.
24
+ * - `relativeChange` - a value moved (delta / percent / crossed a threshold).
25
+ * - `validation` - evaluated on edit; may veto the change (`preventEdit`).
26
+ * - `scheduled` - re-checked on a cron/one-off schedule (reuses the enterprise
27
+ * scheduler); surfaces rows currently matching `predicate`.
28
+ */
29
+ export type AlertTrigger =
30
+ | { type: 'dataChange' }
31
+ | { type: 'relativeChange'; expr: ChangeExpr }
32
+ | { type: 'validation' }
33
+ | { type: 'scheduled'; schedule: Schedule }
34
+
35
+ /** A thing the rule does when it fires. */
36
+ export type AlertActionKind =
37
+ | 'toast'
38
+ | 'badge'
39
+ | 'cellFlash'
40
+ | 'highlight'
41
+ | 'preventEdit'
42
+ | 'log'
43
+
44
+ /** Visual styling for `badge` / `highlight` actions. */
45
+ export type AlertActionStyle = {
46
+ background?: string
47
+ color?: string
48
+ /** A short glyph/emoji shown as a badge. */
49
+ icon?: string
50
+ }
51
+
52
+ export type AlertAction = {
53
+ kind: AlertActionKind
54
+ /**
55
+ * Message template. Tokens: `{rule}`, `{severity}`, `{column}`, `{value}`,
56
+ * and `{field}` / `{row.field}` for any row field.
57
+ */
58
+ message?: string
59
+ style?: AlertActionStyle
60
+ /** Columns the badge/highlight/flash targets (defaults to the rule's). */
61
+ columns?: string[]
62
+ }
63
+
64
+ export type AlertRule = {
65
+ id: string
66
+ name: string
67
+ enabled: boolean
68
+ severity: AlertSeverity
69
+ scope: AlertScope
70
+ predicate: PredicateExpr
71
+ trigger: AlertTrigger
72
+ actions: AlertAction[]
73
+ /** Columns in scope for cell-level styling (badge/highlight/flash). */
74
+ columns?: string[]
75
+ createdAt: number
76
+ }
77
+
78
+ /** A single firing of a rule. */
79
+ export type AlertEvent = {
80
+ ruleId: string
81
+ ruleName: string
82
+ severity: AlertSeverity
83
+ scope: AlertScope
84
+ triggerType: AlertTrigger['type']
85
+ /** The row that matched (absent for aggregate-scope events). */
86
+ rowId?: string
87
+ /** The column that triggered/targets the event, when applicable. */
88
+ columnId?: string
89
+ value?: unknown
90
+ message: string
91
+ actions: AlertAction[]
92
+ firedAt: number
93
+ acknowledged?: boolean
94
+ }
package/src/alerts.ts ADDED
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Alerts (Pro) - the public entry for the Alert Rules engine + expression layer.
3
+ *
4
+ * Unlike the scheduler/board *views*, alerts are an overlay the consumer mounts
5
+ * next to the grid (`<SvGridAlerts>`), so there is no renderer to register with
6
+ * the grid. `enableAlerts()` exists only to keep the Pro soft-gate consistent
7
+ * with the rest of Enterprise: it works without a license key but nudges once in
8
+ * the console when unlicensed.
9
+ *
10
+ * ```ts
11
+ * import { setLicenseKey, enableAlerts } from '@svgrid/enterprise'
12
+ * setLicenseKey('YOUR-KEY')
13
+ * enableAlerts()
14
+ * // then mount <SvGridAlerts data={rows} columns={cols} bind:formats /> and
15
+ * // spread `formats` into <SvGrid conditionalFormats={[...base, ...formats]} />
16
+ * ```
17
+ */
18
+ import { isLicenseKeySet } from './license'
19
+ import { emitUnlicensedNudge } from './watermark'
20
+
21
+ let enabled = false
22
+
23
+ /** Turn on the alerts feature (soft-gated). Idempotent. */
24
+ export function enableAlerts(): void {
25
+ if (enabled) return
26
+ enabled = true
27
+ if (!isLicenseKeySet()) emitUnlicensedNudge()
28
+ }