@affino/datagrid-vue 0.3.30 → 0.3.32

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.
@@ -2,6 +2,52 @@ import { computed, nextTick, ref } from "vue";
2
2
  import { invokeDataGridCellInteraction, resolveDataGridCellKeyboardAction, toggleDataGridCellValue, } from "@affino/datagrid-core";
3
3
  import { buildDataGridFillMatrix, canToggleDataGridFillBehavior, resolveDataGridDefaultFillBehavior, } from "../composables/dataGridFillBehavior";
4
4
  import { useDataGridAxisAutoScrollDelta, useDataGridCellNavigation, useDataGridCellPointerDownRouter, useDataGridDragPointerSelection, useDataGridFillHandleStart, useDataGridFillSelectionLifecycle, useDataGridHistoryActionRunner, useDataGridKeyboardCommandRouter, useDataGridPointerCellCoordResolver, useDataGridPointerAutoScroll, useDataGridPointerPreviewRouter, useDataGridRangeMoveLifecycle, useDataGridRangeMoveStart, useDataGridRangeMutationEngine, } from "../advanced";
5
+ import { resolveMissingRowIndexInRange } from "./useDataGridAppClipboard";
6
+ function getFillRangeStart(range) {
7
+ const candidate = range;
8
+ return Number.isFinite(candidate.startRow)
9
+ ? Math.max(0, Math.trunc(Number(candidate.startRow)))
10
+ : Math.max(0, Math.trunc(Number(candidate.start ?? 0)));
11
+ }
12
+ function getFillRangeEnd(range) {
13
+ const candidate = range;
14
+ return Number.isFinite(candidate.endRow)
15
+ ? Math.max(0, Math.trunc(Number(candidate.endRow)))
16
+ : Math.max(0, Math.trunc(Number(candidate.end ?? getFillRangeStart(range))));
17
+ }
18
+ function formatServerFillAffectedRange(range, fallbackColumns) {
19
+ const start = getFillRangeStart(range);
20
+ const end = getFillRangeEnd(range);
21
+ const startColumn = Number.isFinite(range.startColumn)
22
+ ? range.startColumn
23
+ : fallbackColumns.startColumn;
24
+ const endColumn = Number.isFinite(range.endColumn)
25
+ ? range.endColumn
26
+ : fallbackColumns.endColumn;
27
+ return `${start}..${end} x ${startColumn}..${endColumn}`;
28
+ }
29
+ function normalizeServerFillInvalidationRange(range) {
30
+ if (!range) {
31
+ return null;
32
+ }
33
+ const candidate = range;
34
+ const rawStart = Number.isFinite(candidate.startRow)
35
+ ? candidate.startRow
36
+ : candidate.start;
37
+ const rawEnd = Number.isFinite(candidate.endRow)
38
+ ? candidate.endRow
39
+ : candidate.end;
40
+ const start = Number(rawStart);
41
+ const end = Number(rawEnd ?? rawStart);
42
+ if (!Number.isFinite(start) || !Number.isFinite(end)) {
43
+ return null;
44
+ }
45
+ return {
46
+ start: Math.max(0, Math.trunc(start)),
47
+ end: Math.max(Math.max(0, Math.trunc(start)), Math.trunc(end)),
48
+ };
49
+ }
50
+ const ROW_SELECTION_COLUMN_KEY = "__datagrid_row_selection__";
5
51
  import { useDataGridAppFill, } from "./useDataGridAppFill";
6
52
  import { restoreDataGridFocus } from "./dataGridFocusRestore";
7
53
  const DRAG_SELECTION_POINTER_THRESHOLD_PX = 4;
@@ -30,10 +76,53 @@ export function useDataGridAppInteractionController(options) {
30
76
  }
31
77
  return options.readCell(row, columnKey).trim().length > 0;
32
78
  };
79
+ const isServerBackedRowModel = () => {
80
+ const controllerRowModel = options.runtimeRowModel;
81
+ const runtimeRowModel = options.runtime.rowModel;
82
+ return controllerRowModel?.dataSource != null || runtimeRowModel?.dataSource != null;
83
+ };
84
+ const resolveSelectableColumnIndexes = () => {
85
+ return options.visibleColumns.value
86
+ .map((column, columnIndex) => ({ column, columnIndex }))
87
+ .filter(({ column }) => column.key !== ROW_SELECTION_COLUMN_KEY)
88
+ .map(({ columnIndex }) => columnIndex);
89
+ };
33
90
  const resolveDirectionalSemanticJumpTarget = (current, direction, event) => {
34
91
  if (!(event.ctrlKey || event.metaKey) || event.altKey) {
35
92
  return undefined;
36
93
  }
94
+ if (event.shiftKey && isServerBackedRowModel()) {
95
+ const lastRowIndex = Math.max(0, options.totalRows.value - 1);
96
+ const selectableColumnIndexes = resolveSelectableColumnIndexes();
97
+ const firstSelectableColumnIndex = selectableColumnIndexes[0] ?? 0;
98
+ const lastSelectableColumnIndex = selectableColumnIndexes[selectableColumnIndexes.length - 1] ?? Math.max(0, options.visibleColumns.value.length - 1);
99
+ switch (direction) {
100
+ case "down":
101
+ return options.normalizeCellCoord({
102
+ ...current,
103
+ rowIndex: lastRowIndex,
104
+ rowId: getBodyRowAtIndex(lastRowIndex)?.rowId ?? null,
105
+ }) ?? current;
106
+ case "up":
107
+ return options.normalizeCellCoord({
108
+ ...current,
109
+ rowIndex: 0,
110
+ rowId: getBodyRowAtIndex(0)?.rowId ?? null,
111
+ }) ?? current;
112
+ case "right":
113
+ return options.normalizeCellCoord({
114
+ ...current,
115
+ columnIndex: lastSelectableColumnIndex,
116
+ rowId: getBodyRowAtIndex(current.rowIndex)?.rowId ?? null,
117
+ }) ?? current;
118
+ case "left":
119
+ return options.normalizeCellCoord({
120
+ ...current,
121
+ columnIndex: firstSelectableColumnIndex,
122
+ rowId: getBodyRowAtIndex(current.rowIndex)?.rowId ?? null,
123
+ }) ?? current;
124
+ }
125
+ }
37
126
  const currentCellIsNonEmpty = isSemanticNavigationCellNonEmpty(current.rowIndex, current.columnIndex);
38
127
  if (currentCellIsNonEmpty == null) {
39
128
  return undefined;
@@ -174,7 +263,7 @@ export function useDataGridAppInteractionController(options) {
174
263
  }
175
264
  return event.key.length === 1;
176
265
  };
177
- const applyDirectCellEdit = (row, rowIndex, columnIndex, columnKey, nextValue, label) => {
266
+ const applyDirectCellEdit = async (row, rowIndex, columnIndex, columnKey, nextValue, label) => {
178
267
  if (row.kind === "group" || row.rowId == null) {
179
268
  return false;
180
269
  }
@@ -183,7 +272,7 @@ export function useDataGridAppInteractionController(options) {
183
272
  if (resolvedRow.kind === "group" || resolvedRow.rowId == null) {
184
273
  return false;
185
274
  }
186
- options.runtime.api.rows.applyEdits([
275
+ await options.runtime.api.rows.applyEdits([
187
276
  {
188
277
  rowId: resolvedRow.rowId,
189
278
  data: {
@@ -397,12 +486,12 @@ export function useDataGridAppInteractionController(options) {
397
486
  setSelectionFromRange: (range, activePosition) => {
398
487
  applySelectionRangeWithActivePosition(range, activePosition);
399
488
  },
400
- recordIntent: (descriptor, beforeSnapshot) => {
489
+ recordIntent: (descriptor, beforeSnapshot, afterSnapshotOverride) => {
401
490
  void options.recordIntentTransaction({
402
491
  intent: descriptor.intent,
403
492
  label: descriptor.label,
404
493
  affectedRange: descriptor.affectedRange ?? null,
405
- }, beforeSnapshot);
494
+ }, beforeSnapshot, afterSnapshotOverride);
406
495
  },
407
496
  setLastAction: () => undefined,
408
497
  });
@@ -416,6 +505,179 @@ export function useDataGridAppInteractionController(options) {
416
505
  resolveSelectionRange: options.resolveSelectionRange,
417
506
  rangesEqual: options.rangesEqual,
418
507
  buildFillMatrixFromRange: options.buildFillMatrixFromRange,
508
+ shouldUseServerFill: (_baseRange, previewRange) => {
509
+ const controllerRowModel = options.runtimeRowModel;
510
+ const runtimeRowModelDataSource = controllerRowModel?.dataSource;
511
+ const runtimeRowModelFromRuntime = options.runtime.rowModel?.dataSource;
512
+ options.reportFillPlumbingState?.("controller_runtimeRowModel_exists", controllerRowModel != null);
513
+ options.reportFillPlumbingState?.("controller_runtimeRowModel_dataSource_exists", runtimeRowModelDataSource != null);
514
+ options.reportFillPlumbingState?.("controller_runtimeRowModel_dataSource_keys", Object.keys(runtimeRowModelDataSource ?? {}).length > 0);
515
+ options.reportFillPlumbingDetail?.("controller_runtimeRowModel_dataSource_keys", Object.keys(runtimeRowModelDataSource ?? {}).join(","));
516
+ options.reportFillPlumbingState?.("controller_runtimeRowModel_commit_type", typeof runtimeRowModelDataSource?.commitFillOperation === "function");
517
+ options.reportFillPlumbingState?.("controller_runtime_rowModel_keys", Object.keys(runtimeRowModelFromRuntime ?? {}).length > 0);
518
+ options.reportFillPlumbingDetail?.("controller_runtime_rowModel_keys", Object.keys(runtimeRowModelFromRuntime ?? {}).join(","));
519
+ options.reportFillPlumbingState?.("controller_runtime_rowModel_commit_type", typeof runtimeRowModelFromRuntime?.commitFillOperation === "function");
520
+ const dataSource = runtimeRowModelDataSource ?? runtimeRowModelFromRuntime;
521
+ options.reportFillPlumbingState?.("commitFillOperation_available", typeof dataSource?.commitFillOperation === "function");
522
+ options.reportFillPlumbingState?.("server_fill_dispatch_attempted", true);
523
+ if (typeof dataSource?.commitFillOperation !== "function") {
524
+ return false;
525
+ }
526
+ const rowCount = Math.max(0, previewRange.endRow - previewRange.startRow + 1);
527
+ return rowCount >= 256;
528
+ },
529
+ commitServerFill: async ({ baseRange, previewRange, behavior }) => {
530
+ const runtimeRowModelDataSource = options.runtimeRowModel?.dataSource;
531
+ const runtimeRowModelFallback = options.runtime.rowModel?.dataSource;
532
+ const dataSource = runtimeRowModelDataSource ?? runtimeRowModelFallback;
533
+ if (!dataSource?.commitFillOperation) {
534
+ options.reportFillWarning?.("server fill execution not implemented yet");
535
+ return null;
536
+ }
537
+ const snapshot = options.runtime.api.rows.getSnapshot();
538
+ const operationId = `fill-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
539
+ const sourceMatrix = options.buildFillMatrixFromRange(baseRange);
540
+ const fillMatrix = buildDataGridFillMatrix({
541
+ baseRange,
542
+ previewRange,
543
+ sourceMatrix,
544
+ behavior,
545
+ });
546
+ const materializedTargetRowIds = [];
547
+ const optimisticUpdatesByRowId = new Map();
548
+ let optimisticFillCandidate = options.captureRowsSnapshotForRowIds != null;
549
+ if (optimisticFillCandidate) {
550
+ for (let rowIndex = previewRange.startRow; rowIndex <= previewRange.endRow; rowIndex += 1) {
551
+ const targetRow = getBodyRowAtIndex(rowIndex);
552
+ const isMaterialized = options.isRowMaterializedAtIndex
553
+ ? options.isRowMaterializedAtIndex(rowIndex)
554
+ : !!targetRow && targetRow.__placeholder !== true;
555
+ if (!isMaterialized || !targetRow || targetRow.kind === "group" || targetRow.rowId == null || targetRow.__placeholder === true) {
556
+ optimisticFillCandidate = false;
557
+ break;
558
+ }
559
+ materializedTargetRowIds.push(targetRow.rowId);
560
+ for (let columnIndex = previewRange.startColumn; columnIndex <= previewRange.endColumn; columnIndex += 1) {
561
+ const columnKey = options.visibleColumns.value[columnIndex]?.key;
562
+ if (!columnKey || columnKey === ROW_SELECTION_COLUMN_KEY) {
563
+ continue;
564
+ }
565
+ const rowOffset = rowIndex - previewRange.startRow;
566
+ const columnOffset = columnIndex - previewRange.startColumn;
567
+ const nextValue = fillMatrix[rowOffset]?.[columnOffset] ?? "";
568
+ const current = optimisticUpdatesByRowId.get(targetRow.rowId) ?? {};
569
+ current[columnKey] = nextValue;
570
+ optimisticUpdatesByRowId.set(targetRow.rowId, current);
571
+ }
572
+ }
573
+ }
574
+ let optimisticRollbackUpdates = null;
575
+ if (optimisticFillCandidate && materializedTargetRowIds.length > 0 && optimisticUpdatesByRowId.size > 0) {
576
+ const baselineSnapshot = options.captureRowsSnapshotForRowIds?.(materializedTargetRowIds);
577
+ const baselineRows = baselineSnapshot?.rows ?? [];
578
+ if (baselineRows.length === materializedTargetRowIds.length) {
579
+ const baselineByRowId = new Map();
580
+ for (const entry of baselineRows) {
581
+ baselineByRowId.set(entry.rowId, entry.row);
582
+ }
583
+ optimisticRollbackUpdates = materializedTargetRowIds.flatMap(rowId => {
584
+ const row = baselineByRowId.get(rowId);
585
+ return row ? [{ rowId, data: row }] : [];
586
+ });
587
+ if (optimisticRollbackUpdates.length > 0) {
588
+ options.reportFillPlumbingState?.("server_fill_optimistic_applied", true);
589
+ await Promise.resolve(options.runtime.api.rows.applyEdits(Array.from(optimisticUpdatesByRowId.entries()).map(([rowId, data]) => ({
590
+ rowId,
591
+ data: data,
592
+ }))));
593
+ }
594
+ else {
595
+ optimisticRollbackUpdates = null;
596
+ }
597
+ }
598
+ else {
599
+ optimisticFillCandidate = false;
600
+ }
601
+ }
602
+ let result = null;
603
+ try {
604
+ result = await dataSource.commitFillOperation({
605
+ operationId,
606
+ projection: {
607
+ sortModel: snapshot.sortModel ?? [],
608
+ filterModel: snapshot.filterModel ?? null,
609
+ groupBy: snapshot.groupBy ?? null,
610
+ groupExpansion: snapshot.groupExpansion ?? { expandedByDefault: false, toggledGroupKeys: [] },
611
+ treeData: null,
612
+ pivot: null,
613
+ pagination: snapshot.pagination ?? {
614
+ enabled: false,
615
+ pageSize: 0,
616
+ currentPage: 0,
617
+ pageCount: 0,
618
+ totalRowCount: 0,
619
+ startIndex: 0,
620
+ endIndex: 0,
621
+ },
622
+ },
623
+ sourceRange: baseRange,
624
+ targetRange: previewRange,
625
+ fillColumns: options.visibleColumns.value
626
+ .slice(previewRange.startColumn, previewRange.endColumn + 1)
627
+ .map(column => String(column.key)),
628
+ referenceColumns: options.visibleColumns.value
629
+ .slice(baseRange.startColumn, baseRange.endColumn + 1)
630
+ .map(column => String(column.key)),
631
+ mode: behavior,
632
+ metadata: { origin: "double-click-fill", behaviorSource: "default" },
633
+ });
634
+ options.reportFillPlumbingState?.("commitFillOperation_called", true);
635
+ options.reportFillPlumbingState?.("server_fill_operationId", true);
636
+ }
637
+ catch {
638
+ if (optimisticRollbackUpdates && optimisticRollbackUpdates.length > 0) {
639
+ await Promise.resolve(options.runtime.api.rows.applyEdits(optimisticRollbackUpdates));
640
+ }
641
+ options.reportFillWarning?.("server fill commit failed");
642
+ return null;
643
+ }
644
+ if (!result) {
645
+ if (optimisticRollbackUpdates && optimisticRollbackUpdates.length > 0) {
646
+ await Promise.resolve(options.runtime.api.rows.applyEdits(optimisticRollbackUpdates));
647
+ }
648
+ options.reportFillWarning?.("server fill commit failed");
649
+ return null;
650
+ }
651
+ const invalidationRange = result.invalidation?.kind === "range" ? result.invalidation.range : previewRange;
652
+ const normalizedInvalidationRange = normalizeServerFillInvalidationRange(invalidationRange);
653
+ options.reportFillPlumbingDetail?.("server_fill_raw_invalidation", JSON.stringify(invalidationRange ?? null));
654
+ options.reportFillPlumbingDetail?.("server_fill_normalized_invalidation", normalizedInvalidationRange ? `${normalizedInvalidationRange.start}..${normalizedInvalidationRange.end}` : "none");
655
+ options.reportFillPlumbingDetail?.("server_fill_affected_range", formatServerFillAffectedRange(normalizedInvalidationRange ?? invalidationRange, previewRange));
656
+ const affectedRowCount = result.affectedRowCount ?? 0;
657
+ const warnings = result.warnings ?? [];
658
+ options.reportFillWarning?.(warnings[0] ?? (affectedRowCount > 0 ? "server fill committed" : "server fill no-op"));
659
+ options.reportFillPlumbingState?.("server_fill_affectedRowCount", true);
660
+ if (!options.refreshServerFillViewport) {
661
+ const runtimeRowModel = options.runtimeRowModel;
662
+ const runtimeRowModelFallback = options.runtime.rowModel;
663
+ const rowsApi = options.runtime.api.rows;
664
+ const invalidateTarget = runtimeRowModel?.invalidateRange ?? runtimeRowModelFallback?.invalidateRange;
665
+ if (normalizedInvalidationRange && typeof invalidateTarget === "function") {
666
+ invalidateTarget(normalizedInvalidationRange);
667
+ options.reportFillPlumbingState?.("server_fill_invalidation_applied", true);
668
+ }
669
+ await Promise.resolve(rowsApi.refresh?.());
670
+ }
671
+ return {
672
+ operationId: result.operationId,
673
+ revision: result.revision,
674
+ affectedRange: invalidationRange,
675
+ invalidation: result.invalidation ?? null,
676
+ affectedRowCount,
677
+ affectedCellCount: result.affectedCellCount ?? affectedRowCount,
678
+ warnings,
679
+ };
680
+ },
419
681
  applyClipboardEdits: options.applyClipboardEdits,
420
682
  isCellEditableAt: (rowIndex, columnIndex) => {
421
683
  const row = getBodyRowAtIndex(rowIndex);
@@ -429,9 +691,23 @@ export function useDataGridAppInteractionController(options) {
429
691
  lastAppliedFill.value = session;
430
692
  activeFillBehavior.value = session?.behavior ?? activeFillBehavior.value;
431
693
  },
694
+ setLastServerFillSession: session => {
695
+ if (!session) {
696
+ return;
697
+ }
698
+ options.recordServerFillTransaction?.({
699
+ intent: "fill",
700
+ label: "Server fill",
701
+ affectedRange: session.affectedRange ?? null,
702
+ operationId: session.operationId,
703
+ revision: session.revision,
704
+ mode: session.behavior,
705
+ });
706
+ },
707
+ syncServerFillViewport: options.refreshServerFillViewport,
432
708
  syncViewport: options.syncViewport,
433
709
  });
434
- const applyCommittedFillRange = (baseRange, previewRange, behavior) => {
710
+ const applyCommittedFillRange = async (baseRange, previewRange, behavior) => {
435
711
  const resolveRemovedFillRange = (previousRange, nextRange) => {
436
712
  if (options.rangesEqual(previousRange, nextRange)) {
437
713
  return null;
@@ -497,13 +773,13 @@ export function useDataGridAppInteractionController(options) {
497
773
  previewRange,
498
774
  sourceMatrix,
499
775
  });
500
- options.applyClipboardEdits(removedRange, [[""]], { recordHistory: false });
776
+ await options.applyClipboardEdits(removedRange, [[""]], { recordHistory: false });
501
777
  if (options.rangesEqual(baseRange, previewRange)) {
502
778
  options.applySelectionRange(baseRange);
503
779
  lastAppliedFill.value = null;
504
780
  }
505
781
  else {
506
- options.applyClipboardEdits(previewRange, buildDataGridFillMatrix({
782
+ await options.applyClipboardEdits(previewRange, buildDataGridFillMatrix({
507
783
  baseRange,
508
784
  previewRange,
509
785
  sourceMatrix,
@@ -520,19 +796,20 @@ export function useDataGridAppInteractionController(options) {
520
796
  }),
521
797
  };
522
798
  }
799
+ const afterSnapshot = captureRowsSnapshotForRanges([removedRange, previewRange]);
523
800
  options.syncViewport();
524
801
  void options.recordIntentTransaction({
525
802
  intent: "fill",
526
- label: "Fill cells",
803
+ label: "Fill edit",
527
804
  affectedRange: previewRange,
528
- }, beforeSnapshot);
805
+ }, beforeSnapshot, afterSnapshot);
529
806
  if (behavior) {
530
807
  activeFillBehavior.value = behavior;
531
808
  }
532
809
  return true;
533
810
  }
534
811
  }
535
- const applied = applyFillRange(baseRange, previewRange, behavior);
812
+ const applied = await applyFillRange(baseRange, previewRange, behavior);
536
813
  if (applied && behavior) {
537
814
  activeFillBehavior.value = behavior;
538
815
  }
@@ -806,7 +1083,7 @@ export function useDataGridAppInteractionController(options) {
806
1083
  if (!baseRange || !previewRange) {
807
1084
  return;
808
1085
  }
809
- applyCommittedFillRange(baseRange, previewRange);
1086
+ void applyCommittedFillRange(baseRange, previewRange);
810
1087
  },
811
1088
  setFillDragging: value => {
812
1089
  isFillDragging.value = value;
@@ -1065,9 +1342,14 @@ export function useDataGridAppInteractionController(options) {
1065
1342
  if (!range) {
1066
1343
  return false;
1067
1344
  }
1345
+ const missingRowIndex = resolveMissingRowIndexInRange(getBodyRowAtIndex, range);
1346
+ if (missingRowIndex != null) {
1347
+ options.reportFillWarning?.("Selected range includes unloaded rows. Load rows or use server operation.");
1348
+ return false;
1349
+ }
1068
1350
  const beforeSnapshot = captureRowsSnapshotForRanges([range]);
1069
1351
  options.clearPendingClipboardOperation(false);
1070
- const applied = options.applyClipboardEdits(range, [[""]], { recordHistory: false });
1352
+ const applied = await options.applyClipboardEdits(range, [[""]], { recordHistory: false });
1071
1353
  if (applied <= 0) {
1072
1354
  return false;
1073
1355
  }
@@ -1211,6 +1493,33 @@ export function useDataGridAppInteractionController(options) {
1211
1493
  const isNonEmptyFillReferenceValue = (row, columnKey) => {
1212
1494
  return options.readCell(row, columnKey).trim().length > 0;
1213
1495
  };
1496
+ const buildFillProjectionContext = () => {
1497
+ const getSnapshot = options.runtime.api.rows.getSnapshot;
1498
+ if (typeof getSnapshot !== "function") {
1499
+ return null;
1500
+ }
1501
+ const snapshot = getSnapshot();
1502
+ if (!snapshot) {
1503
+ return null;
1504
+ }
1505
+ return {
1506
+ sortModel: snapshot.sortModel ?? [],
1507
+ filterModel: snapshot.filterModel ?? null,
1508
+ groupBy: snapshot.groupBy ?? null,
1509
+ groupExpansion: snapshot.groupExpansion ?? { expandedByDefault: false, toggledGroupKeys: [] },
1510
+ treeData: null,
1511
+ pivot: null,
1512
+ pagination: snapshot.pagination ?? {
1513
+ enabled: false,
1514
+ pageSize: 0,
1515
+ currentPage: 0,
1516
+ pageCount: 0,
1517
+ totalRowCount: 0,
1518
+ startIndex: 0,
1519
+ endIndex: 0,
1520
+ },
1521
+ };
1522
+ };
1214
1523
  const resolveReferenceColumnExtentEndRow = (baseRange, columnIndex) => {
1215
1524
  const columnKey = options.visibleColumns.value[columnIndex]?.key;
1216
1525
  if (!columnKey) {
@@ -1261,52 +1570,183 @@ export function useDataGridAppInteractionController(options) {
1261
1570
  }
1262
1571
  return evaluateDirection(baseRange.endColumn + 1, 1);
1263
1572
  };
1573
+ const resolveServerAwareFillBoundary = async (baseRange) => {
1574
+ if (!options.resolveFillBoundary) {
1575
+ options.reportFillPlumbingState?.("interaction_controller_option", false);
1576
+ return null;
1577
+ }
1578
+ options.reportFillPlumbingState?.("interaction_controller_option", true);
1579
+ const fillColumns = [];
1580
+ for (let columnIndex = baseRange.startColumn; columnIndex <= baseRange.endColumn; columnIndex += 1) {
1581
+ const columnKey = options.visibleColumns.value[columnIndex]?.key;
1582
+ if (columnKey) {
1583
+ fillColumns.push(columnKey);
1584
+ }
1585
+ }
1586
+ const leftReferenceColumns = [];
1587
+ for (let columnIndex = baseRange.startColumn - 1; columnIndex >= 0; columnIndex -= 1) {
1588
+ const columnKey = options.visibleColumns.value[columnIndex]?.key;
1589
+ if (columnKey) {
1590
+ leftReferenceColumns.push(columnKey);
1591
+ }
1592
+ }
1593
+ const rightReferenceColumns = [];
1594
+ for (let columnIndex = baseRange.endColumn + 1; columnIndex < options.visibleColumns.value.length; columnIndex += 1) {
1595
+ const columnKey = options.visibleColumns.value[columnIndex]?.key;
1596
+ if (columnKey) {
1597
+ rightReferenceColumns.push(columnKey);
1598
+ }
1599
+ }
1600
+ const projection = buildFillProjectionContext();
1601
+ options.reportFillPlumbingState?.("double_click_fill_columns", fillColumns.length > 0);
1602
+ options.reportFillPlumbingState?.("double_click_left_refs", leftReferenceColumns.length > 0);
1603
+ options.reportFillPlumbingState?.("double_click_right_refs", rightReferenceColumns.length > 0);
1604
+ options.reportFillPlumbingState?.("double_click_projection", projection != null);
1605
+ if (!projection) {
1606
+ return null;
1607
+ }
1608
+ const resolve = async (referenceColumns, direction) => {
1609
+ if (referenceColumns.length === 0) {
1610
+ options.reportFillPlumbingState?.(`double_click_resolve_${direction}_skipped_no_refs`, true);
1611
+ return null;
1612
+ }
1613
+ options.reportFillPlumbingState?.(`double_click_resolve_${direction}_called`, true);
1614
+ return options.resolveFillBoundary({
1615
+ direction,
1616
+ baseRange,
1617
+ fillColumns,
1618
+ referenceColumns,
1619
+ projection,
1620
+ startRowIndex: baseRange.startRow,
1621
+ startColumnIndex: direction === "left" ? baseRange.startColumn - 1 : baseRange.endColumn + 1,
1622
+ limit: 500,
1623
+ });
1624
+ };
1625
+ const left = await resolve(leftReferenceColumns, "left");
1626
+ options.reportFillPlumbingState?.("double_click_left_result", left != null);
1627
+ if (left && left.boundaryKind !== "unresolved" && left.endRowIndex != null) {
1628
+ options.reportFillPlumbingState?.("double_click_left_selected", true);
1629
+ return { endRow: left.endRowIndex, boundaryKind: left.boundaryKind, resolvedByServer: true };
1630
+ }
1631
+ const right = await resolve(rightReferenceColumns, "right");
1632
+ options.reportFillPlumbingState?.("double_click_right_result", right != null);
1633
+ if (right && right.boundaryKind !== "unresolved" && right.endRowIndex != null) {
1634
+ options.reportFillPlumbingState?.("double_click_right_selected", true);
1635
+ return { endRow: right.endRowIndex, boundaryKind: right.boundaryKind, resolvedByServer: true };
1636
+ }
1637
+ if (left?.boundaryKind === "unresolved" || right?.boundaryKind === "unresolved") {
1638
+ options.reportFillWarning?.("server fill boundary unresolved; using loaded-cache fallback");
1639
+ }
1640
+ return null;
1641
+ };
1264
1642
  const startFillHandleDoubleClick = (event) => {
1643
+ options.reportFillPlumbingState?.("double_click_handler", true);
1265
1644
  if (options.mode.value !== "base" || !isFillHandleEnabled.value) {
1645
+ options.reportFillPlumbingState?.("double_click_handler_skipped", true);
1266
1646
  return;
1267
1647
  }
1268
1648
  const baseRange = options.resolveSelectionRange();
1269
1649
  if (!baseRange) {
1650
+ options.reportFillPlumbingState?.("double_click_handler_skipped_no_range", true);
1270
1651
  return;
1271
1652
  }
1272
1653
  const anchorRow = getBodyRowAtIndex(baseRange.endRow);
1273
1654
  const anchorColumnKey = options.visibleColumns.value[baseRange.endColumn]?.key;
1274
1655
  if (!anchorRow || !anchorColumnKey) {
1656
+ options.reportFillPlumbingState?.("double_click_handler_skipped_no_anchor", true);
1275
1657
  return;
1276
1658
  }
1277
1659
  if (!options.isCellEditable(anchorRow, baseRange.endRow, anchorColumnKey, baseRange.endColumn)) {
1660
+ options.reportFillPlumbingState?.("double_click_handler_skipped_not_editable", true);
1278
1661
  return;
1279
1662
  }
1280
- const targetEndRow = resolveBestNeighborFillEndRow(baseRange);
1281
- const previewRange = targetEndRow > baseRange.endRow
1282
- ? options.normalizeClipboardRange({
1283
- startRow: baseRange.startRow,
1284
- endRow: targetEndRow,
1285
- startColumn: baseRange.startColumn,
1286
- endColumn: baseRange.endColumn,
1287
- })
1288
- : null;
1289
- if (!previewRange || options.rangesEqual(baseRange, previewRange)) {
1290
- return;
1291
- }
1292
- const sourceMatrix = options.buildFillMatrixFromRange(baseRange);
1293
- const behavior = activeFillBehavior.value ?? resolveDataGridDefaultFillBehavior({
1294
- baseRange,
1295
- previewRange,
1296
- sourceMatrix,
1297
- });
1298
1663
  const preferredFocusCoord = resolveFillOriginFocusCoord();
1299
1664
  event.preventDefault();
1300
1665
  event.stopPropagation();
1301
- if (applyCommittedFillRange(baseRange, previewRange, behavior)) {
1302
- activeFillBehavior.value = behavior;
1303
- const restoredCoord = preferredFocusCoord
1304
- ? restoreSelectionActiveCellToCoord(preferredFocusCoord)
1666
+ void resolveServerAwareFillBoundary(baseRange).then(resolved => {
1667
+ options.reportFillPlumbingState?.("double_click_resolved_server_branch", !!resolved?.resolvedByServer);
1668
+ options.reportFillPlumbingState?.("double_click_resolved_endrow", typeof resolved?.endRow === "number");
1669
+ const targetEndRow = resolved?.endRow ?? resolveBestNeighborFillEndRow(baseRange);
1670
+ const previewRange = targetEndRow > baseRange.endRow
1671
+ ? options.normalizeClipboardRange({
1672
+ startRow: baseRange.startRow,
1673
+ endRow: targetEndRow,
1674
+ startColumn: baseRange.startColumn,
1675
+ endColumn: baseRange.endColumn,
1676
+ })
1305
1677
  : null;
1306
- restoreActiveCellFocus(restoredCoord ?? preferredFocusCoord);
1307
- }
1678
+ if (!previewRange || options.rangesEqual(baseRange, previewRange)) {
1679
+ return;
1680
+ }
1681
+ const sourceMatrix = options.buildFillMatrixFromRange(baseRange);
1682
+ const behavior = activeFillBehavior.value ?? resolveDataGridDefaultFillBehavior({
1683
+ baseRange,
1684
+ previewRange,
1685
+ sourceMatrix,
1686
+ });
1687
+ if (resolved?.resolvedByServer) {
1688
+ options.reportFillPlumbingState?.("double_click_server_branch_entered", true);
1689
+ const resolvedRowCount = previewRange.endRow - previewRange.startRow + 1;
1690
+ const threshold = 500;
1691
+ const rowModel = options.runtimeRowModel;
1692
+ const dataSource = rowModel?.dataSource ?? options.runtime.rowModel?.dataSource;
1693
+ options.reportFillPlumbingState?.("controller_runtimeRowModel_exists", rowModel != null);
1694
+ options.reportFillPlumbingState?.("controller_runtimeRowModel_dataSource_exists", dataSource != null);
1695
+ options.reportFillPlumbingState?.("controller_runtimeRowModel_commit_type", typeof rowModel?.dataSource?.commitFillOperation === "function");
1696
+ options.reportFillPlumbingState?.("controller_runtime_rowModel_commit_type", typeof options.runtime.rowModel?.dataSource?.commitFillOperation === "function");
1697
+ const canCommitServerFill = typeof dataSource?.commitFillOperation === "function";
1698
+ options.reportFillPlumbingState?.("commitFillOperation_available", canCommitServerFill);
1699
+ if (resolvedRowCount > threshold) {
1700
+ if (!canCommitServerFill) {
1701
+ options.reportFillPlumbingState?.("double_click_blocked_large", true);
1702
+ options.reportFillWarning?.("server fill execution not implemented yet");
1703
+ return;
1704
+ }
1705
+ }
1706
+ let allLoaded = true;
1707
+ for (let rowIndex = previewRange.startRow; rowIndex <= previewRange.endRow; rowIndex += 1) {
1708
+ const isMaterialized = options.isRowMaterializedAtIndex
1709
+ ? options.isRowMaterializedAtIndex(rowIndex)
1710
+ : !!getBodyRowAtIndex(rowIndex) && getBodyRowAtIndex(rowIndex).__placeholder !== true;
1711
+ if (!isMaterialized) {
1712
+ allLoaded = false;
1713
+ break;
1714
+ }
1715
+ }
1716
+ if (!allLoaded && !canCommitServerFill) {
1717
+ options.reportFillPlumbingState?.("double_click_blocked_unloaded", true);
1718
+ options.reportFillWarning?.("server fill execution not implemented yet");
1719
+ return;
1720
+ }
1721
+ if (canCommitServerFill) {
1722
+ options.reportFillPlumbingState?.("server-fill-committed", true);
1723
+ void Promise.resolve(applyFillRange(baseRange, previewRange, behavior)).then((applied) => {
1724
+ if (!applied) {
1725
+ return;
1726
+ }
1727
+ activeFillBehavior.value = behavior;
1728
+ const restoredCoord = preferredFocusCoord
1729
+ ? restoreSelectionActiveCellToCoord(preferredFocusCoord)
1730
+ : null;
1731
+ restoreActiveCellFocus(restoredCoord ?? preferredFocusCoord);
1732
+ });
1733
+ return;
1734
+ }
1735
+ }
1736
+ options.reportFillPlumbingState?.("double_click_batch_commit_path", true);
1737
+ void applyCommittedFillRange(baseRange, previewRange, behavior).then(applied => {
1738
+ if (!applied) {
1739
+ return;
1740
+ }
1741
+ activeFillBehavior.value = behavior;
1742
+ const restoredCoord = preferredFocusCoord
1743
+ ? restoreSelectionActiveCellToCoord(preferredFocusCoord)
1744
+ : null;
1745
+ restoreActiveCellFocus(restoredCoord ?? preferredFocusCoord);
1746
+ });
1747
+ });
1308
1748
  };
1309
- const applyLastFillBehavior = (behavior) => {
1749
+ const applyLastFillBehavior = async (behavior) => {
1310
1750
  if (!isFillHandleEnabled.value) {
1311
1751
  return false;
1312
1752
  }
@@ -1315,7 +1755,7 @@ export function useDataGridAppInteractionController(options) {
1315
1755
  return false;
1316
1756
  }
1317
1757
  const preferredFocusCoord = resolveFillOriginFocusCoord();
1318
- const applied = applyCommittedFillRange(session.baseRange, session.previewRange, behavior);
1758
+ const applied = await applyCommittedFillRange(session.baseRange, session.previewRange, behavior);
1319
1759
  if (applied) {
1320
1760
  activeFillBehavior.value = behavior;
1321
1761
  const restoredCoord = preferredFocusCoord
@@ -1459,7 +1899,7 @@ export function useDataGridAppInteractionController(options) {
1459
1899
  if (keyboardAction === "toggle" && columnSnapshot && columnKey) {
1460
1900
  event.preventDefault();
1461
1901
  options.setCellSelection(row, rowOffset, columnIndex, event.shiftKey);
1462
- applyDirectCellEdit(row, rowIndex, columnIndex, columnKey, toggleDataGridCellValue({
1902
+ void applyDirectCellEdit(row, rowIndex, columnIndex, columnKey, toggleDataGridCellValue({
1463
1903
  column: columnSnapshot.column,
1464
1904
  row: row.kind !== "group" ? row.data : undefined,
1465
1905
  }), `Toggle ${columnKey}`);