@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
package/src/index.ts
CHANGED
|
@@ -54,6 +54,82 @@ export { installEnterprise, type EnterpriseGridApi, type EnterpriseAIApi, type E
|
|
|
54
54
|
// renderer so `<SvGrid scheduler={...}>` shows a Month/Week/Day/Agenda calendar.
|
|
55
55
|
// The pure model helpers + config types live in @svgrid/grid; import them from there.
|
|
56
56
|
export { enableSchedulerView, SvGridScheduler } from './scheduler'
|
|
57
|
+
// Kanban board view (Pro). `enableBoardView()` registers the renderer so
|
|
58
|
+
// `<SvGrid board={...}>` shows lanes of cards. Config types live in @svgrid/grid.
|
|
59
|
+
export { enableBoardView, SvGridBoard } from './board'
|
|
60
|
+
// In-grid pivot mode (Pro). `enablePivot()` registers the pivot engine so
|
|
61
|
+
// `<SvGrid pivot={...}>` renders a pivot table in place of the flat table.
|
|
62
|
+
export { enablePivot } from './pivot-enable'
|
|
63
|
+
// Scheduler Pro feature surface (dependencies, multi-assignment + summaries,
|
|
64
|
+
// axis collapse + zoom). Config type extends the free grid's SchedulerConfig;
|
|
65
|
+
// the pure models are unit-tested framework-free helpers the renderer consumes.
|
|
66
|
+
export type { SchedulerProConfig } from './scheduler-config'
|
|
67
|
+
export {
|
|
68
|
+
buildDependencyGraph,
|
|
69
|
+
topoOrder,
|
|
70
|
+
hasCycle,
|
|
71
|
+
requiredStart,
|
|
72
|
+
cascade,
|
|
73
|
+
violations,
|
|
74
|
+
type DependencyType,
|
|
75
|
+
type SchedulerDependency,
|
|
76
|
+
type EventTimes,
|
|
77
|
+
type DependencyGraph,
|
|
78
|
+
type CascadeOptions,
|
|
79
|
+
} from './scheduler-dependencies'
|
|
80
|
+
export {
|
|
81
|
+
expandAssignments,
|
|
82
|
+
resourceLoad,
|
|
83
|
+
overallocations,
|
|
84
|
+
assignedMinutes,
|
|
85
|
+
type SchedulerAssignment,
|
|
86
|
+
type Allocation,
|
|
87
|
+
type Overallocation,
|
|
88
|
+
} from './scheduler-assignments'
|
|
89
|
+
export {
|
|
90
|
+
columnSummaries,
|
|
91
|
+
sumReducer,
|
|
92
|
+
countReducer,
|
|
93
|
+
busyMinutesReducer,
|
|
94
|
+
type ColumnReducer,
|
|
95
|
+
type SummaryItem,
|
|
96
|
+
} from './scheduler-summary'
|
|
97
|
+
export {
|
|
98
|
+
buildAxis,
|
|
99
|
+
timeToX,
|
|
100
|
+
xToTime,
|
|
101
|
+
resolveZoom,
|
|
102
|
+
zoomPresets,
|
|
103
|
+
type Axis,
|
|
104
|
+
type AxisSegment,
|
|
105
|
+
type AxisSegmentKind,
|
|
106
|
+
type BuildAxisOptions,
|
|
107
|
+
type ZoomLevel,
|
|
108
|
+
type ZoomMajorUnit,
|
|
109
|
+
} from './scheduler-axis'
|
|
110
|
+
export {
|
|
111
|
+
buildResourceRows,
|
|
112
|
+
visibleResourceIds,
|
|
113
|
+
type SchedulerResourceGroup,
|
|
114
|
+
type ResourceRow,
|
|
115
|
+
} from './scheduler-resource-tree'
|
|
116
|
+
export { heatCells, heatPeak, type HeatCell } from './scheduler-heatmap'
|
|
117
|
+
export {
|
|
118
|
+
respectsBuffer,
|
|
119
|
+
withinDurationBounds,
|
|
120
|
+
afterLead,
|
|
121
|
+
type BusyEvent,
|
|
122
|
+
type GapMin,
|
|
123
|
+
} from './scheduler-booking'
|
|
124
|
+
export {
|
|
125
|
+
availableSlots,
|
|
126
|
+
mergeIntervals,
|
|
127
|
+
subtractIntervals,
|
|
128
|
+
type Interval,
|
|
129
|
+
type Slot,
|
|
130
|
+
type SlotOptions,
|
|
131
|
+
} from './scheduler-slots'
|
|
132
|
+
export { mergeBusy, commonFree } from './scheduler-freebusy'
|
|
57
133
|
export {
|
|
58
134
|
createStagedEditing,
|
|
59
135
|
type StagedChange,
|
|
@@ -125,6 +201,7 @@ export {
|
|
|
125
201
|
type PlanOp,
|
|
126
202
|
type KitDataSourceOptions,
|
|
127
203
|
type KitHandlerOptions,
|
|
204
|
+
type KitScope,
|
|
128
205
|
type KitHandlers,
|
|
129
206
|
type KitMessage,
|
|
130
207
|
type SqlDialect,
|
|
@@ -141,6 +218,7 @@ export {
|
|
|
141
218
|
dummyJsonAdapter,
|
|
142
219
|
jsonServerAdapter,
|
|
143
220
|
introspectSupabaseTable,
|
|
221
|
+
listSupabaseTables,
|
|
144
222
|
createSupabaseRealtime,
|
|
145
223
|
createRelationLookup,
|
|
146
224
|
createSupabaseAuth,
|
|
@@ -240,9 +318,21 @@ export {
|
|
|
240
318
|
emitStudioApp,
|
|
241
319
|
sampleApps,
|
|
242
320
|
getSampleApp,
|
|
321
|
+
starterProject,
|
|
243
322
|
liveDataSamples,
|
|
244
323
|
getLiveDataSample,
|
|
245
324
|
type SampleApp,
|
|
325
|
+
starterDatasets,
|
|
326
|
+
getStarterDataset,
|
|
327
|
+
type StarterDataset,
|
|
328
|
+
crudSuiteScreens,
|
|
329
|
+
addCrudSuite,
|
|
330
|
+
crudAppFromSchemas,
|
|
331
|
+
type CrudScreenKind,
|
|
332
|
+
type CrudEditingMode,
|
|
333
|
+
type CrudSuiteOptions,
|
|
334
|
+
type CrudAppOptions,
|
|
335
|
+
introspectJson,
|
|
246
336
|
sanitizeStudioProject,
|
|
247
337
|
buildStudioBugReport,
|
|
248
338
|
type BugReport,
|
|
@@ -360,50 +450,106 @@ export {
|
|
|
360
450
|
type Well,
|
|
361
451
|
} from './pivot-designer'
|
|
362
452
|
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
hasAIProvider,
|
|
368
|
-
mockAIProvider,
|
|
369
|
-
type AIProvider,
|
|
370
|
-
type AIRequest,
|
|
371
|
-
type AITask,
|
|
372
|
-
// Filter
|
|
373
|
-
aiFilter,
|
|
374
|
-
type AIFilterOptions,
|
|
375
|
-
type AIFilterResult,
|
|
376
|
-
type AIFilterClause,
|
|
377
|
-
type AISortClause,
|
|
378
|
-
// Smart fill
|
|
379
|
-
aiSmartFill,
|
|
380
|
-
type AISmartFillOptions,
|
|
381
|
-
type AISmartFillResult,
|
|
382
|
-
type AISmartFillExample,
|
|
383
|
-
// Summarize
|
|
384
|
-
aiSummarize,
|
|
385
|
-
type AISummarizeOptions,
|
|
386
|
-
type AISummarizeTarget,
|
|
387
|
-
type AISummary,
|
|
388
|
-
// Classify
|
|
389
|
-
aiClassify,
|
|
390
|
-
type AIClassifyOptions,
|
|
391
|
-
type AIClassifyResult,
|
|
392
|
-
// Natural-language export
|
|
393
|
-
aiExport,
|
|
394
|
-
type AIExportOptions,
|
|
395
|
-
type AIExportPlan,
|
|
396
|
-
// Anomaly detection
|
|
397
|
-
aiFindAnomalies,
|
|
398
|
-
type AIAnomaly,
|
|
399
|
-
type AIAnomalyOptions,
|
|
400
|
-
type AIAnomalyResult,
|
|
401
|
-
// Natural-language chart ("chart this")
|
|
402
|
-
aiChart,
|
|
403
|
-
enableAiCharting,
|
|
404
|
-
disableAiCharting,
|
|
405
|
-
type AIChartOptions,
|
|
406
|
-
type AIChartPlan,
|
|
407
|
-
type AIChartType,
|
|
408
|
-
} from './ai'
|
|
453
|
+
// The AI helpers (setAIProvider, aiFilter, aiSmartFill, aiSummarize, aiClassify,
|
|
454
|
+
// aiExport, aiFindAnomalies, aiChart, enableAiCharting, ...) are now BUILT-IN and
|
|
455
|
+
// FREE in @svgrid/grid - import them from there. Enterprise only registers its
|
|
456
|
+
// export engine so AI-planned Excel/PDF exports can run (see install.ts).
|
|
409
457
|
export { pivotToChartSpec, type PivotChartOptions } from './pivot-chart'
|
|
458
|
+
|
|
459
|
+
// Expression / query language (Pro). One predicate/scalar/change model that
|
|
460
|
+
// powers alerts (and, in time, styled + calculated columns). Leaf comparisons
|
|
461
|
+
// reuse the grid's own `applyExcelFilter`, so operators match the filter row.
|
|
462
|
+
export {
|
|
463
|
+
evaluatePredicate,
|
|
464
|
+
evaluateScalar,
|
|
465
|
+
evaluateChange,
|
|
466
|
+
BUILTIN_FUNCTION_NAMES,
|
|
467
|
+
isBuiltinFunction,
|
|
468
|
+
} from './expressions/evaluate'
|
|
469
|
+
export {
|
|
470
|
+
parsePredicate,
|
|
471
|
+
stringifyPredicate,
|
|
472
|
+
stringifyScalar,
|
|
473
|
+
validateExpression,
|
|
474
|
+
collectColumnRefs,
|
|
475
|
+
ExpressionParseError,
|
|
476
|
+
} from './expressions/parse'
|
|
477
|
+
export {
|
|
478
|
+
OPERATORS,
|
|
479
|
+
operatorMeta,
|
|
480
|
+
operatorsForType,
|
|
481
|
+
isValueless,
|
|
482
|
+
isSetOperator,
|
|
483
|
+
isRangeOperator,
|
|
484
|
+
columnById,
|
|
485
|
+
resolveColumnRef,
|
|
486
|
+
columnLabel,
|
|
487
|
+
columnRefText,
|
|
488
|
+
type ExprColumn,
|
|
489
|
+
type ExprColumnType,
|
|
490
|
+
type OperatorMeta,
|
|
491
|
+
} from './expressions/expression-columns'
|
|
492
|
+
export type {
|
|
493
|
+
ScalarExpr,
|
|
494
|
+
PredicateExpr,
|
|
495
|
+
ChangeExpr,
|
|
496
|
+
ComparisonOp,
|
|
497
|
+
ArithmeticOp,
|
|
498
|
+
AggFn,
|
|
499
|
+
EvalContext,
|
|
500
|
+
ExprRow,
|
|
501
|
+
} from './expressions/expression-types'
|
|
502
|
+
export { default as SvExpressionEditor } from './SvExpressionEditor.svelte'
|
|
503
|
+
|
|
504
|
+
// Alert Rules engine (Pro). Declarative "when this holds / this moves -> toast /
|
|
505
|
+
// badge / highlight / flash / prevent-edit / log". Authored at runtime,
|
|
506
|
+
// persisted, shareable. Surfacing reuses the grid's toast + conditional-format
|
|
507
|
+
// engines; `<SvGridAlerts>` is the runtime overlay the consumer mounts.
|
|
508
|
+
export { enableAlerts } from './alerts'
|
|
509
|
+
export {
|
|
510
|
+
createAlertEngine,
|
|
511
|
+
renderTemplate,
|
|
512
|
+
type AlertEngine,
|
|
513
|
+
type AlertEngineOptions,
|
|
514
|
+
type ValidateEditResult,
|
|
515
|
+
} from './alerts/alert-engine'
|
|
516
|
+
export {
|
|
517
|
+
attachAlertEngine,
|
|
518
|
+
applyAlertEvents,
|
|
519
|
+
type AlertAttachment,
|
|
520
|
+
type AttachAlertOptions,
|
|
521
|
+
type FlashTarget,
|
|
522
|
+
} from './alerts/alert-engine-attach'
|
|
523
|
+
export {
|
|
524
|
+
memoryAlertRules,
|
|
525
|
+
localStorageAlertRules,
|
|
526
|
+
createAlertRules,
|
|
527
|
+
type AlertRulesStorage,
|
|
528
|
+
type AlertRulesManager,
|
|
529
|
+
} from './alerts/alert-storage'
|
|
530
|
+
export {
|
|
531
|
+
createAlertObserver,
|
|
532
|
+
type AlertObserver,
|
|
533
|
+
type AlertObserverOptions,
|
|
534
|
+
} from './alerts/alert-observer'
|
|
535
|
+
export {
|
|
536
|
+
createAlertScheduler,
|
|
537
|
+
type AlertScheduler,
|
|
538
|
+
type AlertSchedulerMode,
|
|
539
|
+
} from './alerts/alert-scheduler'
|
|
540
|
+
export { toConditionalFormats, rulesToConditionalFormats, type StylingContext } from './alerts/alert-formats'
|
|
541
|
+
export { alertStore } from './alerts/alert-store.svelte'
|
|
542
|
+
export type {
|
|
543
|
+
AlertRule,
|
|
544
|
+
AlertEvent,
|
|
545
|
+
AlertTrigger,
|
|
546
|
+
AlertAction,
|
|
547
|
+
AlertActionKind,
|
|
548
|
+
AlertActionStyle,
|
|
549
|
+
AlertScope,
|
|
550
|
+
AlertSeverity,
|
|
551
|
+
} from './alerts/alert-types'
|
|
552
|
+
export { default as SvGridAlerts } from './SvGridAlerts.svelte'
|
|
553
|
+
export { default as SvAlertRuleEditor } from './SvAlertRuleEditor.svelte'
|
|
554
|
+
export { default as SvAlertsManager } from './SvAlertsManager.svelte'
|
|
555
|
+
export { default as SvAlertsPanel } from './SvAlertsPanel.svelte'
|
package/src/install.ts
CHANGED
|
@@ -10,9 +10,11 @@ import { printGrid, type PrintOptions } from './print'
|
|
|
10
10
|
import { importData, type ImportOptions, type ImportResult } from './import'
|
|
11
11
|
import { isLicenseKeySet } from './license'
|
|
12
12
|
import { emitUnlicensedNudge } from './watermark'
|
|
13
|
+
// AI helpers are built-in + free in @svgrid/grid now; enterprise just wires them
|
|
14
|
+
// into the Pro API and registers its export engine so AI exports can write xlsx/pdf.
|
|
13
15
|
import {
|
|
14
16
|
aiFilter, aiSmartFill, aiSummarize, aiClassify, aiExport, aiFindAnomalies,
|
|
15
|
-
aiChart, enableAiCharting,
|
|
17
|
+
aiChart, enableAiCharting, registerExportProvider,
|
|
16
18
|
type AIChartOptions, type AIChartPlan,
|
|
17
19
|
type AIFilterOptions, type AIFilterResult,
|
|
18
20
|
type AISmartFillOptions, type AISmartFillResult,
|
|
@@ -20,13 +22,15 @@ import {
|
|
|
20
22
|
type AIClassifyOptions, type AIClassifyResult,
|
|
21
23
|
type AIExportOptions, type AIExportPlan,
|
|
22
24
|
type AIAnomalyOptions, type AIAnomalyResult,
|
|
23
|
-
} from '
|
|
25
|
+
} from '@svgrid/grid'
|
|
24
26
|
import {
|
|
25
27
|
createPivotModel,
|
|
26
28
|
type PivotConfig,
|
|
27
29
|
type PivotResult,
|
|
28
30
|
} from './pivot'
|
|
29
31
|
import { enableSchedulerView } from './scheduler'
|
|
32
|
+
import { enableBoardView } from './board'
|
|
33
|
+
import { enablePivot } from './pivot-enable'
|
|
30
34
|
|
|
31
35
|
export type EnterpriseAIApi<TData extends RowData> = {
|
|
32
36
|
/** Natural-language -> filter + sort plan (and optionally apply it). */
|
|
@@ -128,6 +132,12 @@ export function installEnterprise<
|
|
|
128
132
|
enableAiCharting(pro)
|
|
129
133
|
// Register the scheduler / calendar view (no-op without the `scheduler` prop).
|
|
130
134
|
enableSchedulerView()
|
|
135
|
+
// Register the Kanban board view (no-op without the `board` prop).
|
|
136
|
+
enableBoardView()
|
|
137
|
+
// Register the export engine so built-in AI exports (aiExport) can write the
|
|
138
|
+
// enterprise formats (xlsx / pdf / ...); free grids without it get the plan only.
|
|
139
|
+
registerExportProvider(exportGrid as never)
|
|
140
|
+
enablePivot()
|
|
131
141
|
pro.pivot = {
|
|
132
142
|
build: (config) =>
|
|
133
143
|
createPivotModel<TFeatures, TData>(pro.getData(), config),
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-grid pivot (Pro). `enablePivot()` registers the pivot ENGINE with the grid
|
|
3
|
+
* so `<SvGrid pivot={...}>` renders a pivot table in place of the flat table -
|
|
4
|
+
* the same grid, aggregated with nested column headers. The engine (which turns
|
|
5
|
+
* rows into a pivot result) is `createPivotModel`; the grid ships the `pivot`
|
|
6
|
+
* prop + config types for free and looks the engine up through the
|
|
7
|
+
* `registerPivotEngine` seam.
|
|
8
|
+
*
|
|
9
|
+
* Soft-gated like the rest of Enterprise: it works without a license key but the
|
|
10
|
+
* grid shows the unlicensed watermark + a one-time console nudge.
|
|
11
|
+
*
|
|
12
|
+
* ```ts
|
|
13
|
+
* import { setLicenseKey, enablePivot } from '@svgrid/enterprise'
|
|
14
|
+
* setLicenseKey('YOUR-KEY')
|
|
15
|
+
* enablePivot()
|
|
16
|
+
* // then: <SvGrid {data} {columns} pivot={{ rows: ['region'], cols: ['quarter'], values: [{ field: 'sales', agg: 'sum' }] }} />
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
import { registerPivotEngine, type PivotEngine } from '@svgrid/grid'
|
|
20
|
+
import { isLicenseKeySet } from './license'
|
|
21
|
+
import { emitUnlicensedNudge } from './watermark'
|
|
22
|
+
import { createPivotModel, type PivotConfig } from './pivot'
|
|
23
|
+
|
|
24
|
+
let enabled = false
|
|
25
|
+
|
|
26
|
+
// The grid's `PivotEngine` seam is generic over row type; `createPivotModel` is
|
|
27
|
+
// the concrete implementation. Bridge them with a single boundary cast.
|
|
28
|
+
const pivotEngine: PivotEngine = (data, config) =>
|
|
29
|
+
createPivotModel(
|
|
30
|
+
data as ReadonlyArray<Record<string, unknown>>,
|
|
31
|
+
config as unknown as PivotConfig<Record<string, unknown>>,
|
|
32
|
+
) as never
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Register the Enterprise pivot engine. Idempotent - safe to call from every
|
|
36
|
+
* component that uses `pivot`, or once at app start. Also invoked by
|
|
37
|
+
* {@link installEnterprise} so wiring the Pro API turns pivot on too.
|
|
38
|
+
*/
|
|
39
|
+
export function enablePivot(): void {
|
|
40
|
+
if (enabled) return
|
|
41
|
+
enabled = true
|
|
42
|
+
registerPivotEngine(pivotEngine)
|
|
43
|
+
if (!isLicenseKeySet()) emitUnlicensedNudge()
|
|
44
|
+
}
|
package/src/pivot.test.ts
CHANGED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import {
|
|
3
|
+
assignedMinutes,
|
|
4
|
+
expandAssignments,
|
|
5
|
+
overallocations,
|
|
6
|
+
resourceLoad,
|
|
7
|
+
type SchedulerAssignment,
|
|
8
|
+
} from './scheduler-assignments'
|
|
9
|
+
|
|
10
|
+
type Ev = { key: string; start: Date; end: Date; resource?: string }
|
|
11
|
+
const D = (h: number) => new Date(2026, 0, 5, h, 0, 0, 0)
|
|
12
|
+
const ev = (key: string, s: number, e: number, resource?: string): Ev => ({ key, start: D(s), end: D(e), resource })
|
|
13
|
+
|
|
14
|
+
const opts = {
|
|
15
|
+
keyOf: (e: Ev) => e.key,
|
|
16
|
+
startOf: (e: Ev) => e.start,
|
|
17
|
+
endOf: (e: Ev) => e.end,
|
|
18
|
+
resourceOf: (e: Ev) => e.resource,
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
describe('expandAssignments', () => {
|
|
22
|
+
it('renders one event once per assigned resource', () => {
|
|
23
|
+
const events = [ev('e1', 9, 11)]
|
|
24
|
+
const assigns: SchedulerAssignment[] = [
|
|
25
|
+
{ id: 'a1', eventId: 'e1', resourceId: 'r1' },
|
|
26
|
+
{ id: 'a2', eventId: 'e1', resourceId: 'r2' },
|
|
27
|
+
]
|
|
28
|
+
const out = expandAssignments(events, assigns, opts)
|
|
29
|
+
expect(out.map((a) => a.resourceId).sort()).toEqual(['r1', 'r2'])
|
|
30
|
+
expect(out.every((a) => a.units === 1)).toBe(true)
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('dedupes duplicate resource assignments to the max units', () => {
|
|
34
|
+
const out = expandAssignments([ev('e1', 9, 11)], [
|
|
35
|
+
{ id: 'a1', eventId: 'e1', resourceId: 'r1', units: 0.5 },
|
|
36
|
+
{ id: 'a2', eventId: 'e1', resourceId: 'r1', units: 0.8 },
|
|
37
|
+
], opts)
|
|
38
|
+
expect(out).toHaveLength(1)
|
|
39
|
+
expect(out[0]!.units).toBe(0.8)
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('falls back to resourceOf when the event has no assignment', () => {
|
|
43
|
+
const out = expandAssignments([ev('e1', 9, 11, 'rX')], [], opts)
|
|
44
|
+
expect(out).toEqual([expect.objectContaining({ resourceId: 'rX', units: 1 })])
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('drops an event with neither assignment nor resource', () => {
|
|
48
|
+
const out = expandAssignments([ev('e1', 9, 11)], [], opts)
|
|
49
|
+
expect(out).toHaveLength(0)
|
|
50
|
+
})
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
describe('resourceLoad', () => {
|
|
54
|
+
it('sums units of overlapping allocations on the resource', () => {
|
|
55
|
+
const events = [ev('e1', 9, 11), ev('e2', 10, 12)]
|
|
56
|
+
const allocs = expandAssignments(events, [
|
|
57
|
+
{ id: 'a1', eventId: 'e1', resourceId: 'r1', units: 1 },
|
|
58
|
+
{ id: 'a2', eventId: 'e2', resourceId: 'r1', units: 0.5 },
|
|
59
|
+
], opts)
|
|
60
|
+
expect(resourceLoad(allocs, 'r1', D(10), D(11))).toBe(1.5) // both overlap 10-11
|
|
61
|
+
expect(resourceLoad(allocs, 'r1', D(9), D(10))).toBe(1) // only e1
|
|
62
|
+
expect(resourceLoad(allocs, 'r2', D(9), D(12))).toBe(0)
|
|
63
|
+
})
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
describe('overallocations', () => {
|
|
67
|
+
it('flags buckets where load exceeds capacity', () => {
|
|
68
|
+
const events = [ev('e1', 9, 12), ev('e2', 9, 12)]
|
|
69
|
+
const allocs = expandAssignments(events, [
|
|
70
|
+
{ id: 'a1', eventId: 'e1', resourceId: 'r1' },
|
|
71
|
+
{ id: 'a2', eventId: 'e2', resourceId: 'r1' },
|
|
72
|
+
], opts)
|
|
73
|
+
const buckets = [{ start: D(9), end: D(10) }, { start: D(10), end: D(11) }]
|
|
74
|
+
const over = overallocations(allocs, ['r1'], buckets, 1)
|
|
75
|
+
expect(over).toHaveLength(2) // 2 concurrent > capacity 1, in both buckets
|
|
76
|
+
expect(over[0]).toMatchObject({ resourceId: 'r1', load: 2, capacity: 1 })
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
it('respects a per-resource capacity function', () => {
|
|
80
|
+
const events = [ev('e1', 9, 12), ev('e2', 9, 12)]
|
|
81
|
+
const allocs = expandAssignments(events, [
|
|
82
|
+
{ id: 'a1', eventId: 'e1', resourceId: 'r1' },
|
|
83
|
+
{ id: 'a2', eventId: 'e2', resourceId: 'r1' },
|
|
84
|
+
], opts)
|
|
85
|
+
const over = overallocations(allocs, ['r1'], [{ start: D(9), end: D(10) }], () => 2)
|
|
86
|
+
expect(over).toHaveLength(0) // capacity 2 absorbs 2 concurrent
|
|
87
|
+
})
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
describe('assignedMinutes', () => {
|
|
91
|
+
it('totals weighted minutes across allocations', () => {
|
|
92
|
+
const allocs = expandAssignments([ev('e1', 9, 11)], [
|
|
93
|
+
{ id: 'a1', eventId: 'e1', resourceId: 'r1', units: 0.5 },
|
|
94
|
+
], opts)
|
|
95
|
+
expect(assignedMinutes(allocs, 'r1')).toBe(60) // 120 min * 0.5
|
|
96
|
+
})
|
|
97
|
+
})
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* scheduler-assignments - the pure core behind Scheduler Pro *multi-assignment*
|
|
3
|
+
* (one event assigned to several resources) and the *resource utilization
|
|
4
|
+
* histogram*. No Svelte, no DOM. Generic over the event shape via accessor
|
|
5
|
+
* callbacks so it needs no dependency on the grid's ResolvedEvent.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/** A many-to-many event <-> resource link, with an optional allocation (0..1). */
|
|
9
|
+
export type SchedulerAssignment = {
|
|
10
|
+
id: string
|
|
11
|
+
eventId: string
|
|
12
|
+
resourceId: string
|
|
13
|
+
/** Allocation fraction, default 1 (full). Summed for histogram / overallocation. */
|
|
14
|
+
units?: number
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** One event placed on one resource (the flattened render unit). */
|
|
18
|
+
export type Allocation<E = unknown> = {
|
|
19
|
+
event: E
|
|
20
|
+
key: string
|
|
21
|
+
resourceId: string
|
|
22
|
+
units: number
|
|
23
|
+
start: Date
|
|
24
|
+
end: Date
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export type ExpandOptions<E> = {
|
|
28
|
+
keyOf: (e: E) => string
|
|
29
|
+
startOf: (e: E) => Date
|
|
30
|
+
endOf: (e: E) => Date
|
|
31
|
+
/** Single-resource fallback when an event has no assignment. */
|
|
32
|
+
resourceOf?: (e: E) => string | undefined
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const MS_MIN = 60_000
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Flatten events into per-resource allocations. An event with assignments renders
|
|
39
|
+
* once per assigned resource (deduped by eventId+resourceId, keeping the max
|
|
40
|
+
* units); an event with none falls back to its single `resourceOf` (units 1), or
|
|
41
|
+
* is dropped when it has neither. The result drives the resource-column bucketing.
|
|
42
|
+
*/
|
|
43
|
+
export function expandAssignments<E>(
|
|
44
|
+
events: ReadonlyArray<E>,
|
|
45
|
+
assignments: ReadonlyArray<SchedulerAssignment>,
|
|
46
|
+
opts: ExpandOptions<E>,
|
|
47
|
+
): Allocation<E>[] {
|
|
48
|
+
// Group assignments by eventId, collapsing duplicate resources to the max units.
|
|
49
|
+
const byEvent = new Map<string, Map<string, number>>()
|
|
50
|
+
for (const a of assignments) {
|
|
51
|
+
if (!a || !a.eventId || !a.resourceId) continue
|
|
52
|
+
let m = byEvent.get(a.eventId)
|
|
53
|
+
if (!m) byEvent.set(a.eventId, (m = new Map()))
|
|
54
|
+
const units = a.units == null ? 1 : a.units
|
|
55
|
+
m.set(a.resourceId, Math.max(m.get(a.resourceId) ?? 0, units))
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const out: Allocation<E>[] = []
|
|
59
|
+
for (const e of events) {
|
|
60
|
+
const key = opts.keyOf(e)
|
|
61
|
+
const start = opts.startOf(e)
|
|
62
|
+
const end = opts.endOf(e)
|
|
63
|
+
const m = byEvent.get(key)
|
|
64
|
+
if (m && m.size) {
|
|
65
|
+
for (const [resourceId, units] of m) out.push({ event: e, key, resourceId, units, start, end })
|
|
66
|
+
} else {
|
|
67
|
+
const resourceId = opts.resourceOf?.(e)
|
|
68
|
+
if (resourceId != null) out.push({ event: e, key, resourceId, units: 1, start, end })
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return out
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** True when `[aStart,aEnd)` overlaps `[bStart,bEnd)`. */
|
|
75
|
+
function overlaps(aStart: Date, aEnd: Date, bStart: Date, bEnd: Date): boolean {
|
|
76
|
+
return aStart.getTime() < bEnd.getTime() && aEnd.getTime() > bStart.getTime()
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Summed allocation units on `resourceId` overlapping the bucket
|
|
81
|
+
* `[bucketStart, bucketEnd)` - the height of one histogram bar.
|
|
82
|
+
*/
|
|
83
|
+
export function resourceLoad<E>(
|
|
84
|
+
allocs: ReadonlyArray<Allocation<E>>,
|
|
85
|
+
resourceId: string,
|
|
86
|
+
bucketStart: Date,
|
|
87
|
+
bucketEnd: Date,
|
|
88
|
+
): number {
|
|
89
|
+
let load = 0
|
|
90
|
+
for (const a of allocs) {
|
|
91
|
+
if (a.resourceId !== resourceId) continue
|
|
92
|
+
if (overlaps(a.start, a.end, bucketStart, bucketEnd)) load += a.units
|
|
93
|
+
}
|
|
94
|
+
return load
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** One over-capacity bucket (summed load exceeds the resource capacity). */
|
|
98
|
+
export type Overallocation = { resourceId: string; start: Date; end: Date; load: number; capacity: number }
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Buckets where a resource's summed load exceeds its capacity. `buckets` are the
|
|
102
|
+
* axis intervals to test; `capacity` is a flat number or a per-resource function
|
|
103
|
+
* (default 1). Used to tint histogram bars / flag conflicts.
|
|
104
|
+
*/
|
|
105
|
+
export function overallocations<E>(
|
|
106
|
+
allocs: ReadonlyArray<Allocation<E>>,
|
|
107
|
+
resourceIds: ReadonlyArray<string>,
|
|
108
|
+
buckets: ReadonlyArray<{ start: Date; end: Date }>,
|
|
109
|
+
capacity: number | ((resourceId: string) => number) = 1,
|
|
110
|
+
): Overallocation[] {
|
|
111
|
+
const capOf = typeof capacity === 'function' ? capacity : () => capacity
|
|
112
|
+
const out: Overallocation[] = []
|
|
113
|
+
for (const resourceId of resourceIds) {
|
|
114
|
+
const cap = capOf(resourceId)
|
|
115
|
+
for (const b of buckets) {
|
|
116
|
+
const load = resourceLoad(allocs, resourceId, b.start, b.end)
|
|
117
|
+
if (load > cap + 1e-9) out.push({ resourceId, start: b.start, end: b.end, load, capacity: cap })
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return out
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Total assigned minutes per resource across `allocs` (a simple utilization
|
|
125
|
+
* roll-up for a legend / header, independent of buckets).
|
|
126
|
+
*/
|
|
127
|
+
export function assignedMinutes<E>(allocs: ReadonlyArray<Allocation<E>>, resourceId: string): number {
|
|
128
|
+
let mins = 0
|
|
129
|
+
for (const a of allocs) {
|
|
130
|
+
if (a.resourceId !== resourceId) continue
|
|
131
|
+
mins += ((a.end.getTime() - a.start.getTime()) / MS_MIN) * a.units
|
|
132
|
+
}
|
|
133
|
+
return mins
|
|
134
|
+
}
|