@svgrid/enterprise 2.3.0 → 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/dist/cdn/svgrid-enterprise.svelte-external.js +6885 -3504
- 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 +5660 -1747
- package/package.json +8 -5
- package/src/SvGridBoard.svelte +4 -1
- package/src/SvGridScheduler.svelte +114 -90
- package/src/ai-export-pdf.dom.test.ts +77 -77
- package/src/ai-export-xlsx.dom.test.ts +90 -90
- package/src/ai-export.dom.test.ts +114 -114
- package/src/export-ooxml.ts +4 -0
- package/src/export-xls.ts +3 -0
- package/src/expressions/evaluate.ts +9 -0
- package/src/import.test.ts +1 -1
- package/src/import.ts +1 -1
- package/src/index.ts +12 -0
- package/src/pivot.test.ts +0 -1
- package/src/schema-designer.ts +1 -1
- package/src/studio/emit-project.test.ts +298 -7
- package/src/studio/emit-project.ts +483 -29
- package/src/studio/emit-schema.ts +20 -10
- package/src/studio/index.ts +34 -0
- package/src/studio/init-flow.test.ts +239 -0
- package/src/studio/init-flow.ts +358 -0
- package/src/studio/project.test.ts +0 -1
- package/src/studio/project.ts +109 -2
- package/src/studio/samples/datasets.test.ts +84 -0
- package/src/studio/samples/datasets.ts +340 -0
- package/src/studio/samples/live-data.test.ts +1 -1
- package/src/studio/samples/samples.test.ts +268 -268
- package/src/studio/samples/shared.ts +7 -150
- 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 +2 -2
- package/src/studio/ui-components.generated.ts +1643 -106
- package/src/studio/ui-components.ts +742 -641
- package/src/sveltekit/index.ts +1 -0
- package/src/sveltekit/sql-source.ts +1 -1
- package/src/sveltekit/transport-scope.test.ts +125 -0
- package/src/sveltekit/transport.ts +76 -2
package/package.json
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"type": "commercial",
|
|
5
5
|
"url": "https://svgrid.com/pricing"
|
|
6
6
|
},
|
|
7
|
-
"version": "2.3.
|
|
7
|
+
"version": "2.3.1",
|
|
8
8
|
"description": "Enterprise feature pack for SvGrid: Excel (xlsx), PDF, CSV, TSV and HTML export, Excel/CSV/JSON import, pivot tables with a drag-and-drop designer, paginated print, Kanban board and scheduler views. Requires a paid license key.",
|
|
9
9
|
"license": "SEE LICENSE IN LICENSE",
|
|
10
10
|
"author": "jQWidgets <sales@jqwidgets.com>",
|
|
@@ -54,6 +54,7 @@
|
|
|
54
54
|
"src",
|
|
55
55
|
"dist/node",
|
|
56
56
|
"dist/cdn",
|
|
57
|
+
"dist/designer",
|
|
57
58
|
"README.md",
|
|
58
59
|
"LICENSE"
|
|
59
60
|
],
|
|
@@ -64,7 +65,7 @@
|
|
|
64
65
|
"svelte": "^5.0.0",
|
|
65
66
|
"jszip": "^3.10.0",
|
|
66
67
|
"pdfmake": "^0.2.0",
|
|
67
|
-
"@svgrid/grid": "^2.2.
|
|
68
|
+
"@svgrid/grid": "^2.2.31"
|
|
68
69
|
},
|
|
69
70
|
"peerDependenciesMeta": {
|
|
70
71
|
"jszip": {
|
|
@@ -81,7 +82,7 @@
|
|
|
81
82
|
"svelte": "^5.55.5",
|
|
82
83
|
"typescript": "6.0.3",
|
|
83
84
|
"vite": "^8.0.10",
|
|
84
|
-
"@svgrid/grid": "^2.2.
|
|
85
|
+
"@svgrid/grid": "^2.2.31"
|
|
85
86
|
},
|
|
86
87
|
"repository": {
|
|
87
88
|
"type": "git",
|
|
@@ -111,9 +112,11 @@
|
|
|
111
112
|
"scripts": {
|
|
112
113
|
"test": "vitest run",
|
|
113
114
|
"test:types": "tsc -p tsconfig.json --noEmit",
|
|
114
|
-
"build": "node ./scripts/build-cdn.mjs && node ./scripts/build-node.mjs",
|
|
115
|
+
"build": "node ./scripts/build-cdn.mjs && node ./scripts/build-node.mjs && node ./scripts/build-designer.mjs",
|
|
115
116
|
"build:cdn": "node ./scripts/build-cdn.mjs",
|
|
116
117
|
"build:node": "node ./scripts/build-node.mjs",
|
|
117
|
-
"
|
|
118
|
+
"build:designer": "node ./scripts/build-designer.mjs",
|
|
119
|
+
"extract:ui-props": "node scripts/extract-ui-props.mjs",
|
|
120
|
+
"lint": "eslint ./src"
|
|
118
121
|
}
|
|
119
122
|
}
|
package/src/SvGridBoard.svelte
CHANGED
|
@@ -381,7 +381,10 @@
|
|
|
381
381
|
function drawerOptions(col: DrawerCol, row: TData) {
|
|
382
382
|
const raw =
|
|
383
383
|
typeof col.editorOptions === "function" ? col.editorOptions(row) : col.editorOptions;
|
|
384
|
-
|
|
384
|
+
// editorOptions may resolve asynchronously, but the drawer form is built
|
|
385
|
+
// synchronously - those columns render without a preset option list rather
|
|
386
|
+
// than throwing on Promise.map.
|
|
387
|
+
if (!raw || !Array.isArray(raw)) return undefined;
|
|
385
388
|
return raw.map((o) =>
|
|
386
389
|
typeof o === "object"
|
|
387
390
|
? { value: o.value, label: o.label ?? String(o.value) }
|
|
@@ -74,6 +74,7 @@
|
|
|
74
74
|
popIn,
|
|
75
75
|
createDismissableLayer,
|
|
76
76
|
onScrollOutside,
|
|
77
|
+
type TimelineAxis,
|
|
77
78
|
} from "@svgrid/grid";
|
|
78
79
|
// Scheduler Pro feature models (this package).
|
|
79
80
|
import { cascade, violations, type SchedulerDependency } from "./scheduler-dependencies";
|
|
@@ -296,6 +297,13 @@
|
|
|
296
297
|
return out;
|
|
297
298
|
});
|
|
298
299
|
let hiddenResources = $state<Set<string>>(new Set());
|
|
300
|
+
// --- Scheduler Pro config ---------------------------------------------------
|
|
301
|
+
// The renderer receives config untyped (registered as Component<any>); read the
|
|
302
|
+
// Pro fields through a cast so the free grid's SchedulerConfig stays unchanged.
|
|
303
|
+
// Declared here rather than beside the dependency code further down: half the
|
|
304
|
+
// derivations below read it, and a `const` used above its declaration is a type
|
|
305
|
+
// error even though `$derived` is lazy enough to work at runtime.
|
|
306
|
+
const pcfg = $derived(scheduler as unknown as SchedulerProConfig<TFeatures, TData>);
|
|
299
307
|
// --- multi-calendar overlay ---
|
|
300
308
|
const calendars = $derived(pcfg.calendars ?? []);
|
|
301
309
|
const calField = $derived(pcfg.calendarField);
|
|
@@ -341,6 +349,40 @@
|
|
|
341
349
|
const tlLaneH = $derived(scheduler.timelineLaneHeight ?? 26);
|
|
342
350
|
const tlResW = $derived(scheduler.resourceAreaWidth ?? 160);
|
|
343
351
|
const tlSlot = $derived(Math.max(1, scheduler.timelineSlotMinutes ?? slotMinutes));
|
|
352
|
+
// --- Scheduler Pro: non-working-time collapse + continuous zoom ------------
|
|
353
|
+
// Declared ahead of `tlAxis` / `tlAxisWidth` (which read `axisPro` and
|
|
354
|
+
// `proAxisData` directly) rather than beside the other Pro features: a `const`
|
|
355
|
+
// used above its declaration is a type error even though `$derived` is lazy
|
|
356
|
+
// enough to work at runtime.
|
|
357
|
+
const collapseOn = $derived(isTimeline && (pcfg.collapseNonWorking === true || pcfg.collapseWeekends === true));
|
|
358
|
+
// Zoom: consumer-controlled `zoom`, overridable by the built-in stepper.
|
|
359
|
+
let zoomOverride = $state<number | null>(null);
|
|
360
|
+
const zoomValue = $derived(zoomOverride ?? pcfg.zoom ?? null);
|
|
361
|
+
const zoomOn = $derived(isTimeline && zoomValue != null);
|
|
362
|
+
const zoomLadder = $derived(pcfg.zoomLevels ?? zoomPresets);
|
|
363
|
+
const axisPro = $derived(collapseOn || zoomOn);
|
|
364
|
+
// The piecewise axis (working spans full width, non-working collapsed) + tick size.
|
|
365
|
+
const proAxisData = $derived.by<{ axis: Axis; tickMinutes: number; level: ZoomLevel | null } | null>(() => {
|
|
366
|
+
if (!axisPro) return null;
|
|
367
|
+
const z = zoomOn ? resolveZoom(zoomValue as number | ZoomLevel, zoomLadder) : null;
|
|
368
|
+
const pxPerMinute = z ? z.pxPerMinute : 0.5;
|
|
369
|
+
const axis = buildAxis(range.start, range.end, {
|
|
370
|
+
pxPerMinute,
|
|
371
|
+
businessHours: scheduler.businessHours,
|
|
372
|
+
nonWorkingDays: scheduler.nonWorkingDays,
|
|
373
|
+
collapseNonWorking: pcfg.collapseNonWorking,
|
|
374
|
+
collapseWeekends: pcfg.collapseWeekends,
|
|
375
|
+
collapsedGapPx: pcfg.collapsedGapPx,
|
|
376
|
+
});
|
|
377
|
+
const tickMinutes = z ? z.level.tickMinutes : view === "timelineDay" ? 60 : 1440;
|
|
378
|
+
return { axis, tickMinutes, level: z?.level ?? null };
|
|
379
|
+
});
|
|
380
|
+
function stepZoom(dir: number) {
|
|
381
|
+
const cur = zoomValue == null ? 3 : typeof zoomValue === "number" ? zoomValue : zoomLadder.indexOf(zoomValue as ZoomLevel);
|
|
382
|
+
const next = Math.max(0, Math.min(zoomLadder.length - 1, (cur < 0 ? 3 : cur) + dir));
|
|
383
|
+
zoomOverride = next;
|
|
384
|
+
pcfg.onZoomChange?.(zoomLadder[next]!);
|
|
385
|
+
}
|
|
344
386
|
const tlAxis = $derived(
|
|
345
387
|
axisPro
|
|
346
388
|
? proTimelineAxis()
|
|
@@ -365,14 +407,10 @@
|
|
|
365
407
|
? Math.max(proAxisData.axis.totalPx, tlOuterW ? tlOuterW - tlResW : 0, 320)
|
|
366
408
|
: Math.max(tlAxis.ticks.length * TL_TICK_MIN, tlOuterW ? tlOuterW - tlResW : 0, 320),
|
|
367
409
|
);
|
|
368
|
-
const tlRows = $derived(
|
|
369
|
-
timelineRows(
|
|
370
|
-
resourceField ? (visibleResIds ? resources.filter((r) => visibleResIds.has(r.id)) : resources) : null,
|
|
371
|
-
tlEventsForRows,
|
|
372
|
-
),
|
|
373
|
-
);
|
|
374
|
-
|
|
375
410
|
// --- Grouped / tree resources (timeline) ----------------------------------
|
|
411
|
+
// This block and the multi-assignment one below it feed `tlRows`, so both are
|
|
412
|
+
// declared ahead of it (a `const` used above its declaration is a type error
|
|
413
|
+
// even though `$derived` is lazy enough to work at runtime).
|
|
376
414
|
const GROUP_ROW_H = 30;
|
|
377
415
|
const hasGroups = $derived(isTimeline && !!resourceField && !!(pcfg.resourceGroups && pcfg.resourceGroups.length));
|
|
378
416
|
let collapsedGroups = $state<Set<string>>(new Set());
|
|
@@ -396,6 +434,54 @@
|
|
|
396
434
|
hasGroups ? buildResourceRows(resources, pcfg.resourceGroups!, pcfg.resourceGroupOf ?? (() => undefined), collapsedGroups) : [],
|
|
397
435
|
);
|
|
398
436
|
const visibleResIds = $derived(hasGroups ? new Set(visibleResourceIds(treeRows)) : null);
|
|
437
|
+
|
|
438
|
+
// --- Scheduler Pro: multi-assignment (one event under several resources) ---
|
|
439
|
+
// Resolve assignments from the flat list + any per-row `assignmentField`.
|
|
440
|
+
const assignmentsList = $derived.by<SchedulerAssignment[]>(() => {
|
|
441
|
+
const out: SchedulerAssignment[] = [];
|
|
442
|
+
if (pcfg.assignments) out.push(...pcfg.assignments);
|
|
443
|
+
const f = pcfg.assignmentField;
|
|
444
|
+
if (f) {
|
|
445
|
+
for (const row of data) {
|
|
446
|
+
const raw = fieldValue(row, f);
|
|
447
|
+
if (!Array.isArray(raw)) continue;
|
|
448
|
+
const evId = key(row);
|
|
449
|
+
for (const r of raw) {
|
|
450
|
+
if (typeof r === "string") out.push({ id: `${evId}@${r}`, eventId: evId, resourceId: r });
|
|
451
|
+
else if (r && typeof r === "object" && "resourceId" in r) {
|
|
452
|
+
const o = r as { id?: string; resourceId: string; units?: number };
|
|
453
|
+
out.push({ id: o.id ?? `${evId}@${o.resourceId}`, eventId: evId, resourceId: o.resourceId, units: o.units });
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
return out;
|
|
459
|
+
});
|
|
460
|
+
const hasAssignments = $derived(assignmentsList.length > 0 || !!pcfg.assignmentField);
|
|
461
|
+
// Flattened (event x resource) allocations for the visible timeline events.
|
|
462
|
+
const tlAllocations = $derived.by(() => {
|
|
463
|
+
if (!isTimeline || !resourceField) return [];
|
|
464
|
+
return expandAssignments(viewEvents, assignmentsList, {
|
|
465
|
+
keyOf: (e) => e.rowKey,
|
|
466
|
+
startOf: (e) => e.start,
|
|
467
|
+
endOf: (e) => e.end,
|
|
468
|
+
resourceOf: (e) => e.resourceId,
|
|
469
|
+
});
|
|
470
|
+
});
|
|
471
|
+
// Events fed to timelineRows: expanded per assigned resource when assignments
|
|
472
|
+
// are in play (one event can then appear under several resources), else plain.
|
|
473
|
+
const tlEventsForRows = $derived(
|
|
474
|
+
hasAssignments
|
|
475
|
+
? tlAllocations.map((a) => ({ ...a.event, key: `${a.event.key}@${a.resourceId}`, resourceId: a.resourceId }))
|
|
476
|
+
: viewEvents,
|
|
477
|
+
);
|
|
478
|
+
|
|
479
|
+
const tlRows = $derived(
|
|
480
|
+
timelineRows(
|
|
481
|
+
resourceField ? (visibleResIds ? resources.filter((r) => visibleResIds.has(r.id)) : resources) : null,
|
|
482
|
+
tlEventsForRows,
|
|
483
|
+
),
|
|
484
|
+
);
|
|
399
485
|
// The unified list the timeline body iterates: group-header rows interleaved
|
|
400
486
|
// with resource rows (each carrying its lane-packed tlRow + its tlRows index).
|
|
401
487
|
type TlPacked = (typeof tlRows)[number];
|
|
@@ -416,10 +502,6 @@
|
|
|
416
502
|
return out;
|
|
417
503
|
});
|
|
418
504
|
|
|
419
|
-
// --- Scheduler Pro: event dependencies + auto-reschedule -------------------
|
|
420
|
-
// The renderer receives config untyped (registered as Component<any>); read the
|
|
421
|
-
// Pro fields through a cast so the free grid's SchedulerConfig stays unchanged.
|
|
422
|
-
const pcfg = $derived(scheduler as unknown as SchedulerProConfig<TFeatures, TData>);
|
|
423
505
|
// Resolve dependency links from the flat list + any per-row `dependencyField`.
|
|
424
506
|
const depList = $derived.by<SchedulerDependency[]>(() => {
|
|
425
507
|
const out: SchedulerDependency[] = [];
|
|
@@ -531,46 +613,8 @@
|
|
|
531
613
|
pcfg.onDependenciesChange?.(moves);
|
|
532
614
|
}
|
|
533
615
|
|
|
534
|
-
// --- Scheduler Pro:
|
|
535
|
-
//
|
|
536
|
-
const assignmentsList = $derived.by<SchedulerAssignment[]>(() => {
|
|
537
|
-
const out: SchedulerAssignment[] = [];
|
|
538
|
-
if (pcfg.assignments) out.push(...pcfg.assignments);
|
|
539
|
-
const f = pcfg.assignmentField;
|
|
540
|
-
if (f) {
|
|
541
|
-
for (const row of data) {
|
|
542
|
-
const raw = fieldValue(row, f);
|
|
543
|
-
if (!Array.isArray(raw)) continue;
|
|
544
|
-
const evId = key(row);
|
|
545
|
-
for (const r of raw) {
|
|
546
|
-
if (typeof r === "string") out.push({ id: `${evId}@${r}`, eventId: evId, resourceId: r });
|
|
547
|
-
else if (r && typeof r === "object" && "resourceId" in r) {
|
|
548
|
-
const o = r as { id?: string; resourceId: string; units?: number };
|
|
549
|
-
out.push({ id: o.id ?? `${evId}@${o.resourceId}`, eventId: evId, resourceId: o.resourceId, units: o.units });
|
|
550
|
-
}
|
|
551
|
-
}
|
|
552
|
-
}
|
|
553
|
-
}
|
|
554
|
-
return out;
|
|
555
|
-
});
|
|
556
|
-
const hasAssignments = $derived(assignmentsList.length > 0 || !!pcfg.assignmentField);
|
|
557
|
-
// Flattened (event x resource) allocations for the visible timeline events.
|
|
558
|
-
const tlAllocations = $derived.by(() => {
|
|
559
|
-
if (!isTimeline || !resourceField) return [];
|
|
560
|
-
return expandAssignments(viewEvents, assignmentsList, {
|
|
561
|
-
keyOf: (e) => e.rowKey,
|
|
562
|
-
startOf: (e) => e.start,
|
|
563
|
-
endOf: (e) => e.end,
|
|
564
|
-
resourceOf: (e) => e.resourceId,
|
|
565
|
-
});
|
|
566
|
-
});
|
|
567
|
-
// Events fed to timelineRows: expanded per assigned resource when assignments
|
|
568
|
-
// are in play (one event can then appear under several resources), else plain.
|
|
569
|
-
const tlEventsForRows = $derived(
|
|
570
|
-
hasAssignments
|
|
571
|
-
? tlAllocations.map((a) => ({ ...a.event, key: `${a.event.key}@${a.resourceId}`, resourceId: a.resourceId }))
|
|
572
|
-
: viewEvents,
|
|
573
|
-
);
|
|
616
|
+
// --- Scheduler Pro: resource histogram + column summary --------------------
|
|
617
|
+
// (multi-assignment lives further up, beside the timeline row derivations)
|
|
574
618
|
// Resource utilization histogram (timeline): a bar per axis tick under each row.
|
|
575
619
|
const histCfg = $derived(typeof pcfg.resourceHistogram === "object" ? pcfg.resourceHistogram : {});
|
|
576
620
|
const histOn = $derived(!!pcfg.resourceHistogram && isTimeline && !!resourceField);
|
|
@@ -671,7 +715,7 @@
|
|
|
671
715
|
return out;
|
|
672
716
|
}
|
|
673
717
|
// Per-time-column summary strip (counts / sums / custom reducer) over axis ticks.
|
|
674
|
-
const summaryCfg = $derived(pcfg.columnSummary
|
|
718
|
+
const summaryCfg = $derived(pcfg.columnSummary ? pcfg.columnSummary : null);
|
|
675
719
|
const tlSummary = $derived.by(() => {
|
|
676
720
|
if (!summaryCfg || !isTimeline) return null;
|
|
677
721
|
const cols = tlAxis.ticks.map((t) => ({ start: t.start, end: t.end }));
|
|
@@ -683,36 +727,9 @@
|
|
|
683
727
|
return vals;
|
|
684
728
|
});
|
|
685
729
|
|
|
686
|
-
// --- Scheduler Pro: non-working
|
|
687
|
-
|
|
688
|
-
//
|
|
689
|
-
let zoomOverride = $state<number | null>(null);
|
|
690
|
-
const zoomValue = $derived(zoomOverride ?? pcfg.zoom ?? null);
|
|
691
|
-
const zoomOn = $derived(isTimeline && zoomValue != null);
|
|
692
|
-
const zoomLadder = $derived(pcfg.zoomLevels ?? zoomPresets);
|
|
693
|
-
const axisPro = $derived(collapseOn || zoomOn);
|
|
694
|
-
// The piecewise axis (working spans full width, non-working collapsed) + tick size.
|
|
695
|
-
const proAxisData = $derived.by<{ axis: Axis; tickMinutes: number; level: ZoomLevel | null } | null>(() => {
|
|
696
|
-
if (!axisPro) return null;
|
|
697
|
-
const z = zoomOn ? resolveZoom(zoomValue as number | ZoomLevel, zoomLadder) : null;
|
|
698
|
-
const pxPerMinute = z ? z.pxPerMinute : 0.5;
|
|
699
|
-
const axis = buildAxis(range.start, range.end, {
|
|
700
|
-
pxPerMinute,
|
|
701
|
-
businessHours: scheduler.businessHours,
|
|
702
|
-
nonWorkingDays: scheduler.nonWorkingDays,
|
|
703
|
-
collapseNonWorking: pcfg.collapseNonWorking,
|
|
704
|
-
collapseWeekends: pcfg.collapseWeekends,
|
|
705
|
-
collapsedGapPx: pcfg.collapsedGapPx,
|
|
706
|
-
});
|
|
707
|
-
const tickMinutes = z ? z.level.tickMinutes : view === "timelineDay" ? 60 : 1440;
|
|
708
|
-
return { axis, tickMinutes, level: z?.level ?? null };
|
|
709
|
-
});
|
|
710
|
-
function stepZoom(dir: number) {
|
|
711
|
-
const cur = zoomValue == null ? 3 : typeof zoomValue === "number" ? zoomValue : zoomLadder.indexOf(zoomValue as ZoomLevel);
|
|
712
|
-
const next = Math.max(0, Math.min(zoomLadder.length - 1, (cur < 0 ? 3 : cur) + dir));
|
|
713
|
-
zoomOverride = next;
|
|
714
|
-
pcfg.onZoomChange?.(zoomLadder[next]!);
|
|
715
|
-
}
|
|
730
|
+
// --- Scheduler Pro: axis tick labels (non-working collapse + zoom) ---------
|
|
731
|
+
// The axis itself (`axisPro` / `proAxisData`) is built further up, beside the
|
|
732
|
+
// rest of the timeline derivations that consume it.
|
|
716
733
|
const TL_WD_S = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
|
|
717
734
|
const TL_MO_S = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
|
|
718
735
|
function proTickLabel(d: Date, tickMinutes: number): string {
|
|
@@ -724,7 +741,10 @@
|
|
|
724
741
|
// Build a TimelineAxis-shaped object from the piecewise Pro axis so the existing
|
|
725
742
|
// timeline template (ticks / majors / gridlines) renders unchanged. Collapsed
|
|
726
743
|
// segments emit no ticks - the visual gap conveys the compression.
|
|
727
|
-
|
|
744
|
+
// Annotated with the real `TimelineAxis` rather than `typeof tlAxis`: tlAxis
|
|
745
|
+
// CALLS this function, so referring back to it made the type circular and
|
|
746
|
+
// silently degraded tlAxis (and everything derived from it) to `any`.
|
|
747
|
+
function proTimelineAxis(): TimelineAxis {
|
|
728
748
|
const data = proAxisData!;
|
|
729
749
|
const ax = data.axis;
|
|
730
750
|
const total = ax.totalPx || 1;
|
|
@@ -2111,16 +2131,20 @@
|
|
|
2111
2131
|
}
|
|
2112
2132
|
function drawerOptions(col: DrawerCol, row: TData) {
|
|
2113
2133
|
const raw = typeof col.editorOptions === "function" ? col.editorOptions(row) : col.editorOptions;
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2134
|
+
// editorOptions may resolve asynchronously, but the drawer form is built
|
|
2135
|
+
// synchronously - those columns render without a preset option list rather
|
|
2136
|
+
// than throwing on Promise.map.
|
|
2137
|
+
if (!raw || !Array.isArray(raw)) return undefined;
|
|
2138
|
+
const opts: ReadonlyArray<string | number | { value: string | number; label?: string; color?: string }> = raw;
|
|
2139
|
+
return opts.map((o) =>
|
|
2140
|
+
typeof o === "object"
|
|
2117
2141
|
? {
|
|
2118
|
-
value:
|
|
2119
|
-
label: String(
|
|
2142
|
+
value: o.value,
|
|
2143
|
+
label: String(o.label ?? o.value),
|
|
2120
2144
|
// Carry a color swatch through so the drawer's select shows it.
|
|
2121
|
-
color:
|
|
2145
|
+
color: o.color,
|
|
2122
2146
|
}
|
|
2123
|
-
: { value: o
|
|
2147
|
+
: { value: o, label: String(o) },
|
|
2124
2148
|
);
|
|
2125
2149
|
}
|
|
2126
2150
|
let drawerInitial = $state<Record<string, unknown>>({});
|
|
@@ -1,77 +1,77 @@
|
|
|
1
|
-
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
2
|
-
|
|
3
|
-
// Capture exactly what exportGrid receives (so we don't need pdfmake / the DOM).
|
|
4
|
-
const calls: Array<{ format: string; rows?: unknown[]; groupBy?: string[] }> = []
|
|
5
|
-
vi.mock('./export', () => ({
|
|
6
|
-
exportGrid: async (_api: unknown, opts: { format: string; rows?: unknown[]; groupBy?: string[] }) => {
|
|
7
|
-
calls.push({ format: opts.format, rows: opts.rows as unknown[], groupBy: opts.groupBy })
|
|
8
|
-
},
|
|
9
|
-
}))
|
|
10
|
-
|
|
11
|
-
import { aiExport, setAIProvider, mockAIProvider, registerExportProvider } from '@svgrid/grid'
|
|
12
|
-
import { exportGrid } from './export'
|
|
13
|
-
import { setLicenseKey } from './license'
|
|
14
|
-
|
|
15
|
-
type Order = { country: string; company: string; product: string; quantity: number; price: number }
|
|
16
|
-
const rows: Order[] = Array.from({ length: 120 }, (_, i) => ({
|
|
17
|
-
country: ['USA', 'UK', 'Germany'][i % 3]!,
|
|
18
|
-
company: `Co${i}`,
|
|
19
|
-
product: 'Widget',
|
|
20
|
-
quantity: 5 + (i % 50),
|
|
21
|
-
price: Math.round((10 + ((i * 41) % 480)) * 100) / 100, // spread 10..490
|
|
22
|
-
}))
|
|
23
|
-
|
|
24
|
-
function makeApi() {
|
|
25
|
-
return {
|
|
26
|
-
getData: () => rows,
|
|
27
|
-
// Simulate the grid returning EVERYTHING (the reactive-filter bug we route around).
|
|
28
|
-
getDisplayedRows: () => rows,
|
|
29
|
-
getSelectedRows: () => [],
|
|
30
|
-
getColumns: () => [
|
|
31
|
-
{ id: 'country', field: 'country', header: 'Country', visible: true },
|
|
32
|
-
{ id: 'company', field: 'company', header: 'Company', visible: true },
|
|
33
|
-
{ id: 'product', field: 'product', header: 'Product', visible: true },
|
|
34
|
-
{ id: 'quantity', field: 'quantity', header: 'Qty', visible: true },
|
|
35
|
-
{ id: 'price', field: 'price', header: 'Price', visible: true },
|
|
36
|
-
],
|
|
37
|
-
getState: () => ({ grouping: [] }),
|
|
38
|
-
clearAllFilters: vi.fn(),
|
|
39
|
-
clearSort: vi.fn(),
|
|
40
|
-
setFilter: vi.fn(),
|
|
41
|
-
setSort: vi.fn(),
|
|
42
|
-
setGroupBy: vi.fn(),
|
|
43
|
-
} as any
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
beforeEach(() => {
|
|
47
|
-
calls.length = 0
|
|
48
|
-
setLicenseKey('SVENTERPRISE-DEV-TEST')
|
|
49
|
-
setAIProvider(mockAIProvider)
|
|
50
|
-
// aiExport delegates to the registered engine - here the mocked exportGrid.
|
|
51
|
-
registerExportProvider(exportGrid)
|
|
52
|
-
})
|
|
53
|
-
|
|
54
|
-
describe('aiExport - the demo 203 "grouped PDF" query', () => {
|
|
55
|
-
it('parses the plan and exports non-empty, filtered, grouped rows', async () => {
|
|
56
|
-
const plan = await aiExport(makeApi(), 'export orders over $300 as a grouped PDF by country')
|
|
57
|
-
|
|
58
|
-
expect(plan.format).toBe('pdf')
|
|
59
|
-
expect(plan.groupBy).toEqual(['country'])
|
|
60
|
-
expect(plan.filters).toContainEqual({ field: 'price', operator: 'greaterThan', value: '300' })
|
|
61
|
-
|
|
62
|
-
// exportGrid was called with real, filtered rows (not the empty grid view).
|
|
63
|
-
expect(calls).toHaveLength(1)
|
|
64
|
-
expect(calls[0]!.format).toBe('pdf')
|
|
65
|
-
expect(calls[0]!.groupBy).toEqual(['country'])
|
|
66
|
-
const exported = calls[0]!.rows as Order[]
|
|
67
|
-
expect(exported.length).toBeGreaterThan(0)
|
|
68
|
-
expect(exported.every((r) => r.price > 300)).toBe(true)
|
|
69
|
-
// Rows are contiguous by country (group order preserved).
|
|
70
|
-
const countries = exported.map((r) => r.country)
|
|
71
|
-
const firstSeen = [...new Set(countries)]
|
|
72
|
-
const contiguous = countries.join('|')
|
|
73
|
-
for (const c of firstSeen) {
|
|
74
|
-
expect(contiguous).toMatch(new RegExp(`(?:^|\\|)${c}(?:\\|${c})*(?:\\||$)`))
|
|
75
|
-
}
|
|
76
|
-
})
|
|
77
|
-
})
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
2
|
+
|
|
3
|
+
// Capture exactly what exportGrid receives (so we don't need pdfmake / the DOM).
|
|
4
|
+
const calls: Array<{ format: string; rows?: unknown[]; groupBy?: string[] }> = []
|
|
5
|
+
vi.mock('./export', () => ({
|
|
6
|
+
exportGrid: async (_api: unknown, opts: { format: string; rows?: unknown[]; groupBy?: string[] }) => {
|
|
7
|
+
calls.push({ format: opts.format, rows: opts.rows as unknown[], groupBy: opts.groupBy })
|
|
8
|
+
},
|
|
9
|
+
}))
|
|
10
|
+
|
|
11
|
+
import { aiExport, setAIProvider, mockAIProvider, registerExportProvider } from '@svgrid/grid'
|
|
12
|
+
import { exportGrid } from './export'
|
|
13
|
+
import { setLicenseKey } from './license'
|
|
14
|
+
|
|
15
|
+
type Order = { country: string; company: string; product: string; quantity: number; price: number }
|
|
16
|
+
const rows: Order[] = Array.from({ length: 120 }, (_, i) => ({
|
|
17
|
+
country: ['USA', 'UK', 'Germany'][i % 3]!,
|
|
18
|
+
company: `Co${i}`,
|
|
19
|
+
product: 'Widget',
|
|
20
|
+
quantity: 5 + (i % 50),
|
|
21
|
+
price: Math.round((10 + ((i * 41) % 480)) * 100) / 100, // spread 10..490
|
|
22
|
+
}))
|
|
23
|
+
|
|
24
|
+
function makeApi() {
|
|
25
|
+
return {
|
|
26
|
+
getData: () => rows,
|
|
27
|
+
// Simulate the grid returning EVERYTHING (the reactive-filter bug we route around).
|
|
28
|
+
getDisplayedRows: () => rows,
|
|
29
|
+
getSelectedRows: () => [],
|
|
30
|
+
getColumns: () => [
|
|
31
|
+
{ id: 'country', field: 'country', header: 'Country', visible: true },
|
|
32
|
+
{ id: 'company', field: 'company', header: 'Company', visible: true },
|
|
33
|
+
{ id: 'product', field: 'product', header: 'Product', visible: true },
|
|
34
|
+
{ id: 'quantity', field: 'quantity', header: 'Qty', visible: true },
|
|
35
|
+
{ id: 'price', field: 'price', header: 'Price', visible: true },
|
|
36
|
+
],
|
|
37
|
+
getState: () => ({ grouping: [] }),
|
|
38
|
+
clearAllFilters: vi.fn(),
|
|
39
|
+
clearSort: vi.fn(),
|
|
40
|
+
setFilter: vi.fn(),
|
|
41
|
+
setSort: vi.fn(),
|
|
42
|
+
setGroupBy: vi.fn(),
|
|
43
|
+
} as any
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
beforeEach(() => {
|
|
47
|
+
calls.length = 0
|
|
48
|
+
setLicenseKey('SVENTERPRISE-DEV-TEST')
|
|
49
|
+
setAIProvider(mockAIProvider)
|
|
50
|
+
// aiExport delegates to the registered engine - here the mocked exportGrid.
|
|
51
|
+
registerExportProvider(exportGrid as never)
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
describe('aiExport - the demo 203 "grouped PDF" query', () => {
|
|
55
|
+
it('parses the plan and exports non-empty, filtered, grouped rows', async () => {
|
|
56
|
+
const plan = await aiExport(makeApi(), 'export orders over $300 as a grouped PDF by country')
|
|
57
|
+
|
|
58
|
+
expect(plan.format).toBe('pdf')
|
|
59
|
+
expect(plan.groupBy).toEqual(['country'])
|
|
60
|
+
expect(plan.filters).toContainEqual({ field: 'price', operator: 'greaterThan', value: '300' })
|
|
61
|
+
|
|
62
|
+
// exportGrid was called with real, filtered rows (not the empty grid view).
|
|
63
|
+
expect(calls).toHaveLength(1)
|
|
64
|
+
expect(calls[0]!.format).toBe('pdf')
|
|
65
|
+
expect(calls[0]!.groupBy).toEqual(['country'])
|
|
66
|
+
const exported = calls[0]!.rows as Order[]
|
|
67
|
+
expect(exported.length).toBeGreaterThan(0)
|
|
68
|
+
expect(exported.every((r) => r.price > 300)).toBe(true)
|
|
69
|
+
// Rows are contiguous by country (group order preserved).
|
|
70
|
+
const countries = exported.map((r) => r.country)
|
|
71
|
+
const firstSeen = [...new Set(countries)]
|
|
72
|
+
const contiguous = countries.join('|')
|
|
73
|
+
for (const c of firstSeen) {
|
|
74
|
+
expect(contiguous).toMatch(new RegExp(`(?:^|\\|)${c}(?:\\|${c})*(?:\\||$)`))
|
|
75
|
+
}
|
|
76
|
+
})
|
|
77
|
+
})
|