@svgrid/enterprise 2.3.0 → 2.3.2

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 (73) hide show
  1. package/dist/cdn/{pdfmake-C6pyKjX0.js → pdfmake-DVbZqKeD.js} +2297 -2327
  2. package/dist/cdn/rolldown-runtime-DtPi1Y-2.js +13 -0
  3. package/dist/cdn/{smart.export-CfW2zpyq.js → smart.export-D9FF4wAB.js} +54 -51
  4. package/dist/cdn/svgrid-enterprise.svelte-external.js +11308 -7898
  5. package/dist/cdn/{vfs_fonts-BktEULEs.js → vfs_fonts-iHMTEkpG.js} +1 -1
  6. package/dist/designer/assets/GridMenus-CW-rnHS9.js +7 -0
  7. package/dist/designer/assets/SvDateTimePicker-Cygij-dr.css +1 -0
  8. package/dist/designer/assets/SvDateTimePicker-DOBYyLeN.js +1 -0
  9. package/dist/designer/assets/SvGridChart-BEDNTdc5.js +2 -0
  10. package/dist/designer/assets/SvGridChart-Vu4Nvjiv.css +1 -0
  11. package/dist/designer/assets/SvGridChartPanel-x3lzSxuf.js +10 -0
  12. package/dist/designer/assets/SvGridChartView-CAKhPELi.js +1 -0
  13. package/dist/designer/assets/SvGridChartView-DX9HfkBR.css +1 -0
  14. package/dist/designer/assets/SvGridDropdown-CAYG7Y-O.js +1 -0
  15. package/dist/designer/assets/SvGridDropdown-Crwb2P3Q.css +1 -0
  16. package/dist/designer/assets/SvListBox-BMSfFJJT.css +1 -0
  17. package/dist/designer/assets/SvListBox-BVzVfCG3.js +1 -0
  18. package/dist/designer/assets/chart-DHEgt4lN.js +1 -0
  19. package/dist/designer/assets/disclose-version-CIm4S9xS.js +2 -0
  20. package/dist/designer/assets/dismissable-2EZKoens.js +1 -0
  21. package/dist/designer/assets/dismissable-BF-9TJSd.css +1 -0
  22. package/dist/designer/assets/easing-zaypKrr0.js +3 -0
  23. package/dist/designer/assets/export-format-DwjuTfrQ.js +6 -0
  24. package/dist/designer/assets/index-DVrv9dH6.js +75797 -0
  25. package/dist/designer/assets/index-DjPp1mn7.css +1 -0
  26. package/dist/designer/assets/js-scroller.svelte-tHanKJx0.js +110 -0
  27. package/dist/designer/assets/jszip.min-CX2fPs3O.js +2 -0
  28. package/dist/designer/assets/pdfmake-BpIYFHoG.js +242 -0
  29. package/dist/designer/assets/popover-CUkLsvRX.js +1 -0
  30. package/dist/designer/assets/rolldown-runtime-Dd_uD5pT.js +1 -0
  31. package/dist/designer/assets/smart.export-CDCwbFH4.js +37 -0
  32. package/dist/designer/assets/src-BRKu53tV.js +2842 -0
  33. package/dist/designer/assets/src-DseB1VRN.css +1 -0
  34. package/dist/designer/assets/vfs_fonts-BV-Ul9v2.js +1 -0
  35. package/dist/designer/index.html +32 -0
  36. package/dist/node/studio.js +16778 -12672
  37. package/package.json +10 -7
  38. package/src/SvGridBoard.svelte +4 -1
  39. package/src/SvGridScheduler.svelte +114 -90
  40. package/src/ai-export-pdf.dom.test.ts +77 -77
  41. package/src/ai-export-xlsx.dom.test.ts +90 -90
  42. package/src/ai-export.dom.test.ts +114 -114
  43. package/src/export-ooxml.ts +4 -0
  44. package/src/export-xls.ts +3 -0
  45. package/src/expressions/evaluate.ts +9 -0
  46. package/src/import.test.ts +1 -1
  47. package/src/import.ts +1 -1
  48. package/src/index.ts +12 -0
  49. package/src/pivot.test.ts +0 -1
  50. package/src/schema-designer.ts +1 -1
  51. package/src/sources/introspect-supabase.ts +2 -2
  52. package/src/studio/emit-project.test.ts +298 -7
  53. package/src/studio/emit-project.ts +484 -30
  54. package/src/studio/emit-schema.ts +20 -10
  55. package/src/studio/index.ts +34 -0
  56. package/src/studio/init-flow.test.ts +364 -0
  57. package/src/studio/init-flow.ts +409 -0
  58. package/src/studio/project.test.ts +0 -1
  59. package/src/studio/project.ts +109 -2
  60. package/src/studio/samples/datasets.test.ts +84 -0
  61. package/src/studio/samples/datasets.ts +340 -0
  62. package/src/studio/samples/live-data.test.ts +1 -1
  63. package/src/studio/samples/samples.test.ts +268 -268
  64. package/src/studio/samples/shared.ts +7 -150
  65. package/src/studio/screen-suites.test.ts +226 -0
  66. package/src/studio/screen-suites.ts +445 -0
  67. package/src/studio/ui-components-surface.test.ts +2 -2
  68. package/src/studio/ui-components.generated.ts +1643 -106
  69. package/src/studio/ui-components.ts +742 -641
  70. package/src/sveltekit/index.ts +1 -0
  71. package/src/sveltekit/sql-source.ts +1 -1
  72. package/src/sveltekit/transport-scope.test.ts +125 -0
  73. 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.0",
7
+ "version": "2.3.2",
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
  ],
@@ -61,10 +62,10 @@
61
62
  "access": "public"
62
63
  },
63
64
  "peerDependencies": {
64
- "svelte": "^5.0.0",
65
65
  "jszip": "^3.10.0",
66
66
  "pdfmake": "^0.2.0",
67
- "@svgrid/grid": "^2.2.27"
67
+ "svelte": "^5.0.0",
68
+ "@svgrid/grid": "^2.2.33"
68
69
  },
69
70
  "peerDependenciesMeta": {
70
71
  "jszip": {
@@ -80,8 +81,8 @@
80
81
  "pdfmake": "^0.2.10",
81
82
  "svelte": "^5.55.5",
82
83
  "typescript": "6.0.3",
83
- "vite": "^8.0.10",
84
- "@svgrid/grid": "^2.2.27"
84
+ "vite": "^8.2.1",
85
+ "@svgrid/grid": "^2.2.33"
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
- "extract:ui-props": "node scripts/extract-ui-props.mjs"
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
  }
@@ -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
- if (!raw) return undefined;
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: multi-assignment + resource histogram + column summary --
535
- // Resolve assignments from the flat list + any per-row `assignmentField`.
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 && pcfg.columnSummary !== false ? pcfg.columnSummary : null);
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-time collapse + continuous zoom ------------
687
- const collapseOn = $derived(isTimeline && (pcfg.collapseNonWorking === true || pcfg.collapseWeekends === true));
688
- // Zoom: consumer-controlled `zoom`, overridable by the built-in stepper.
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
- function proTimelineAxis(): typeof tlAxis {
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
- if (!raw) return undefined;
2115
- return raw.map((o) =>
2116
- typeof o === "object" && o != null && "value" in o
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: (o as { value: unknown }).value as string | number,
2119
- label: String((o as { label?: unknown }).label ?? (o as { value: unknown }).value),
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: (o as { color?: unknown }).color as string | undefined,
2145
+ color: o.color,
2122
2146
  }
2123
- : { value: o as string | number, label: String(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
+ })