@affino/datagrid-vue 0.3.32 → 0.3.34

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.
@@ -3,6 +3,17 @@ import { invokeDataGridCellInteraction, resolveDataGridCellKeyboardAction, toggl
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
5
  import { resolveMissingRowIndexInRange } from "./useDataGridAppClipboard";
6
+ const SERVER_FILL_SERIES_UNSUPPORTED_WARNING = "Series fill is not supported by the server datasource yet; using copy fill.";
7
+ function resolveServerFillCommitBehavior(behavior) {
8
+ return behavior === "series"
9
+ ? { behavior: "copy", downgraded: true }
10
+ : { behavior, downgraded: false };
11
+ }
12
+ function normalizeServerFillOperationId(value) {
13
+ return typeof value === "string" && value.trim().length > 0
14
+ ? value
15
+ : null;
16
+ }
6
17
  function getFillRangeStart(range) {
7
18
  const candidate = range;
8
19
  return Number.isFinite(candidate.startRow)
@@ -213,6 +224,37 @@ export function useDataGridAppInteractionController(options) {
213
224
  const rangeMoveBaseRange = ref(null);
214
225
  const rangeMoveOrigin = ref(null);
215
226
  const rangeMovePreviewRange = ref(null);
227
+ const serverFillBoundaryConsistencyMetadata = ref(null);
228
+ let fillPreviewCancelVersion = 0;
229
+ let lastServerFillMaterializationWarning = null;
230
+ const hasActiveFillPreviewState = () => {
231
+ return isFillDragging.value || fillBaseRange.value != null || fillPreviewRange.value != null;
232
+ };
233
+ const clearFillPreviewState = (expectedBaseRange, expectedPreviewRange) => {
234
+ if (expectedBaseRange !== undefined
235
+ && !options.rangesEqual(fillBaseRange.value, expectedBaseRange)) {
236
+ return false;
237
+ }
238
+ if (expectedPreviewRange !== undefined
239
+ && !options.rangesEqual(fillPreviewRange.value, expectedPreviewRange)) {
240
+ return false;
241
+ }
242
+ isFillDragging.value = false;
243
+ fillDragStartPointer.value = null;
244
+ fillPointer.value = null;
245
+ fillBaseRange.value = null;
246
+ fillPreviewRange.value = null;
247
+ fillOriginFocusCoord.value = null;
248
+ serverFillBoundaryConsistencyMetadata.value = null;
249
+ return true;
250
+ };
251
+ const cancelFillPreviewState = () => {
252
+ if (!hasActiveFillPreviewState()) {
253
+ return false;
254
+ }
255
+ fillPreviewCancelVersion += 1;
256
+ return clearFillPreviewState();
257
+ };
216
258
  const focusViewport = () => {
217
259
  options.bodyViewportRef.value?.focus({ preventScroll: true });
218
260
  };
@@ -434,6 +476,97 @@ export function useDataGridAppInteractionController(options) {
434
476
  }
435
477
  return options.captureRowsSnapshotForRowIds?.(rowIds) ?? options.captureRowsSnapshot();
436
478
  };
479
+ const isRowMaterializedForOperation = (rowIndex) => {
480
+ const row = getBodyRowAtIndex(rowIndex);
481
+ return options.isRowMaterializedAtIndex
482
+ ? options.isRowMaterializedAtIndex(rowIndex)
483
+ : !!row && row.__placeholder !== true;
484
+ };
485
+ const isRangeFullyMaterialized = (range) => {
486
+ const normalizedRange = range ? options.normalizeClipboardRange(range) : null;
487
+ if (!normalizedRange) {
488
+ return false;
489
+ }
490
+ for (let rowIndex = normalizedRange.startRow; rowIndex <= normalizedRange.endRow; rowIndex += 1) {
491
+ if (!isRowMaterializedForOperation(rowIndex)) {
492
+ return false;
493
+ }
494
+ }
495
+ return true;
496
+ };
497
+ const applyServerBackedRangeMove = async (baseRange, targetRange) => {
498
+ if (!isRangeFullyMaterialized(baseRange) || !isRangeFullyMaterialized(targetRange)) {
499
+ options.reportFillWarning?.("Range move includes unloaded rows. Load the full source and target ranges before moving.");
500
+ return false;
501
+ }
502
+ const editsByRowId = new Map();
503
+ let appliedCells = 0;
504
+ let blockedCells = 0;
505
+ for (let rowOffset = 0; rowOffset <= baseRange.endRow - baseRange.startRow; rowOffset += 1) {
506
+ const sourceRowIndex = baseRange.startRow + rowOffset;
507
+ const targetRowIndex = targetRange.startRow + rowOffset;
508
+ const sourceRow = getBodyRowAtIndex(sourceRowIndex);
509
+ const targetRow = getBodyRowAtIndex(targetRowIndex);
510
+ if (!sourceRow
511
+ || !targetRow
512
+ || sourceRow.kind === "group"
513
+ || targetRow.kind === "group"
514
+ || sourceRow.rowId == null
515
+ || targetRow.rowId == null) {
516
+ blockedCells += Math.max(0, baseRange.endColumn - baseRange.startColumn + 1);
517
+ continue;
518
+ }
519
+ for (let columnOffset = 0; columnOffset <= baseRange.endColumn - baseRange.startColumn; columnOffset += 1) {
520
+ const sourceColumnIndex = baseRange.startColumn + columnOffset;
521
+ const targetColumnIndex = targetRange.startColumn + columnOffset;
522
+ const sourceColumnKey = options.visibleColumns.value[sourceColumnIndex]?.key;
523
+ const targetColumnKey = options.visibleColumns.value[targetColumnIndex]?.key;
524
+ if (!sourceColumnKey
525
+ || !targetColumnKey
526
+ || sourceColumnKey === ROW_SELECTION_COLUMN_KEY
527
+ || targetColumnKey === ROW_SELECTION_COLUMN_KEY
528
+ || !options.isCellEditable(sourceRow, sourceRowIndex, sourceColumnKey, sourceColumnIndex)
529
+ || !options.isCellEditable(targetRow, targetRowIndex, targetColumnKey, targetColumnIndex)) {
530
+ blockedCells += 1;
531
+ continue;
532
+ }
533
+ const sourceValue = options.readCell(sourceRow, sourceColumnKey);
534
+ const sourcePatch = editsByRowId.get(sourceRow.rowId) ?? {};
535
+ sourcePatch[sourceColumnKey] = "";
536
+ editsByRowId.set(sourceRow.rowId, sourcePatch);
537
+ const targetPatch = editsByRowId.get(targetRow.rowId) ?? {};
538
+ targetPatch[targetColumnKey] = sourceValue;
539
+ editsByRowId.set(targetRow.rowId, targetPatch);
540
+ appliedCells += 1;
541
+ }
542
+ }
543
+ if (appliedCells === 0) {
544
+ return false;
545
+ }
546
+ const updates = Array.from(editsByRowId.entries(), ([rowId, data]) => ({
547
+ rowId,
548
+ data: data,
549
+ }));
550
+ const beforeSnapshot = captureRowsSnapshotForRanges([baseRange, targetRange]);
551
+ try {
552
+ await Promise.resolve(options.runtime.api.rows.applyEdits(updates));
553
+ }
554
+ catch (error) {
555
+ options.reportFillWarning?.(error instanceof Error ? error.message : "range move commit failed");
556
+ return false;
557
+ }
558
+ const afterSnapshot = captureRowsSnapshotForRanges([baseRange, targetRange]);
559
+ void options.recordIntentTransaction({
560
+ intent: "move",
561
+ label: blockedCells > 0
562
+ ? `Move ${appliedCells} cells (blocked ${blockedCells})`
563
+ : `Move ${appliedCells} cells`,
564
+ affectedRange: targetRange,
565
+ }, beforeSnapshot, afterSnapshot);
566
+ options.applySelectionRange(targetRange);
567
+ options.syncViewport();
568
+ return true;
569
+ };
437
570
  const rangeMutationEngine = useDataGridRangeMutationEngine({
438
571
  resolveRangeMoveBaseRange: () => rangeMoveBaseRange.value,
439
572
  resolveRangeMovePreviewRange: () => rangeMovePreviewRange.value,
@@ -507,41 +640,45 @@ export function useDataGridAppInteractionController(options) {
507
640
  buildFillMatrixFromRange: options.buildFillMatrixFromRange,
508
641
  shouldUseServerFill: (_baseRange, previewRange) => {
509
642
  const controllerRowModel = options.runtimeRowModel;
510
- const runtimeRowModelDataSource = controllerRowModel?.dataSource;
511
643
  const runtimeRowModelFromRuntime = options.runtime.rowModel?.dataSource;
512
644
  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");
645
+ options.reportFillPlumbingState?.("controller_runtimeRowModel_dataSource_exists", controllerRowModel?.dataSource != null);
646
+ options.reportFillPlumbingState?.("controller_runtimeRowModel_dataSource_keys", Object.keys(controllerRowModel?.dataSource ?? {}).length > 0);
647
+ options.reportFillPlumbingDetail?.("controller_runtimeRowModel_dataSource_keys", Object.keys(controllerRowModel?.dataSource ?? {}).join(","));
648
+ options.reportFillPlumbingState?.("controller_runtimeRowModel_commit_type", typeof controllerRowModel?.dataSource?.commitFillOperation === "function");
517
649
  options.reportFillPlumbingState?.("controller_runtime_rowModel_keys", Object.keys(runtimeRowModelFromRuntime ?? {}).length > 0);
518
650
  options.reportFillPlumbingDetail?.("controller_runtime_rowModel_keys", Object.keys(runtimeRowModelFromRuntime ?? {}).join(","));
519
651
  options.reportFillPlumbingState?.("controller_runtime_rowModel_commit_type", typeof runtimeRowModelFromRuntime?.commitFillOperation === "function");
520
- const dataSource = runtimeRowModelDataSource ?? runtimeRowModelFromRuntime;
652
+ const dataSource = resolveServerFillDataSource();
521
653
  options.reportFillPlumbingState?.("commitFillOperation_available", typeof dataSource?.commitFillOperation === "function");
522
654
  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;
655
+ return serverFillBoundaryConsistencyMetadata.value != null || !isRangeFullyMaterialized(previewRange);
528
656
  },
529
657
  commitServerFill: async ({ baseRange, previewRange, behavior }) => {
530
- const runtimeRowModelDataSource = options.runtimeRowModel?.dataSource;
531
- const runtimeRowModelFallback = options.runtime.rowModel?.dataSource;
532
- const dataSource = runtimeRowModelDataSource ?? runtimeRowModelFallback;
658
+ lastServerFillMaterializationWarning = null;
659
+ const serverFillBehavior = resolveServerFillCommitBehavior(behavior);
660
+ if (serverFillBehavior.downgraded) {
661
+ options.reportFillWarning?.(SERVER_FILL_SERIES_UNSUPPORTED_WARNING);
662
+ options.reportFillPlumbingState?.("server_fill_series_downgraded_to_copy", true);
663
+ }
664
+ const clearCurrentFillPreview = () => {
665
+ clearFillPreviewState(baseRange, previewRange);
666
+ };
667
+ const dataSource = resolveServerFillDataSource();
533
668
  if (!dataSource?.commitFillOperation) {
534
669
  options.reportFillWarning?.("server fill execution not implemented yet");
670
+ clearCurrentFillPreview();
535
671
  return null;
536
672
  }
537
673
  const snapshot = options.runtime.api.rows.getSnapshot();
538
674
  const operationId = `fill-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
675
+ const boundaryConsistencyMetadata = serverFillBoundaryConsistencyMetadata.value;
539
676
  const sourceMatrix = options.buildFillMatrixFromRange(baseRange);
540
677
  const fillMatrix = buildDataGridFillMatrix({
541
678
  baseRange,
542
679
  previewRange,
543
680
  sourceMatrix,
544
- behavior,
681
+ behavior: serverFillBehavior.behavior,
545
682
  });
546
683
  const materializedTargetRowIds = [];
547
684
  const optimisticUpdatesByRowId = new Map();
@@ -571,6 +708,31 @@ export function useDataGridAppInteractionController(options) {
571
708
  }
572
709
  }
573
710
  }
711
+ const sourceResolution = resolveProjectedFillRowIds(baseRange);
712
+ reportProjectedFillRowIdResolution("source", sourceResolution);
713
+ if (!sourceResolution.fullyMaterialized) {
714
+ options.reportFillWarning?.(`server fill source row ids are not fully materialized (${sourceResolution.missingCount} source rows missing)`);
715
+ clearCurrentFillPreview();
716
+ return null;
717
+ }
718
+ const sourceRowIds = sourceResolution.rowIds;
719
+ let targetResolution = resolveProjectedFillRowIds(previewRange);
720
+ reportProjectedFillRowIdResolution("target", targetResolution);
721
+ if (!targetResolution.fullyMaterialized) {
722
+ const materialized = await materializeProjectedFillRows(previewRange);
723
+ if (materialized) {
724
+ targetResolution = resolveProjectedFillRowIds(previewRange);
725
+ reportProjectedFillRowIdResolution("target", targetResolution);
726
+ }
727
+ }
728
+ const missingTargetRows = targetResolution.missingCount;
729
+ if (!targetResolution.fullyMaterialized || targetResolution.rowIds.length <= sourceRowIds.length) {
730
+ options.reportFillWarning?.(`server fill target row ids are not fully materialized (${missingTargetRows} target rows missing)`);
731
+ clearCurrentFillPreview();
732
+ return null;
733
+ }
734
+ const commitTargetRange = previewRange;
735
+ const targetRowIds = targetResolution.rowIds;
574
736
  let optimisticRollbackUpdates = null;
575
737
  if (optimisticFillCandidate && materializedTargetRowIds.length > 0 && optimisticUpdatesByRowId.size > 0) {
576
738
  const baselineSnapshot = options.captureRowsSnapshotForRowIds?.(materializedTargetRowIds);
@@ -621,14 +783,25 @@ export function useDataGridAppInteractionController(options) {
621
783
  },
622
784
  },
623
785
  sourceRange: baseRange,
624
- targetRange: previewRange,
786
+ targetRange: commitTargetRange,
787
+ sourceRowIds,
788
+ targetRowIds,
625
789
  fillColumns: options.visibleColumns.value
626
790
  .slice(previewRange.startColumn, previewRange.endColumn + 1)
627
791
  .map(column => String(column.key)),
628
792
  referenceColumns: options.visibleColumns.value
629
793
  .slice(baseRange.startColumn, baseRange.endColumn + 1)
630
794
  .map(column => String(column.key)),
631
- mode: behavior,
795
+ mode: serverFillBehavior.behavior,
796
+ ...(boundaryConsistencyMetadata?.revision !== undefined
797
+ ? { baseRevision: boundaryConsistencyMetadata.revision }
798
+ : {}),
799
+ ...(boundaryConsistencyMetadata?.projectionHash !== undefined
800
+ ? { projectionHash: boundaryConsistencyMetadata.projectionHash }
801
+ : {}),
802
+ ...(boundaryConsistencyMetadata?.boundaryToken !== undefined
803
+ ? { boundaryToken: boundaryConsistencyMetadata.boundaryToken }
804
+ : {}),
632
805
  metadata: { origin: "double-click-fill", behaviorSource: "default" },
633
806
  });
634
807
  options.reportFillPlumbingState?.("commitFillOperation_called", true);
@@ -641,6 +814,9 @@ export function useDataGridAppInteractionController(options) {
641
814
  options.reportFillWarning?.("server fill commit failed");
642
815
  return null;
643
816
  }
817
+ finally {
818
+ clearCurrentFillPreview();
819
+ }
644
820
  if (!result) {
645
821
  if (optimisticRollbackUpdates && optimisticRollbackUpdates.length > 0) {
646
822
  await Promise.resolve(options.runtime.api.rows.applyEdits(optimisticRollbackUpdates));
@@ -648,33 +824,92 @@ export function useDataGridAppInteractionController(options) {
648
824
  options.reportFillWarning?.("server fill commit failed");
649
825
  return null;
650
826
  }
651
- const invalidationRange = result.invalidation?.kind === "range" ? result.invalidation.range : previewRange;
827
+ const affectedRowCount = result.affectedRowCount ?? 0;
828
+ const affectedCellCount = result.affectedCellCount ?? affectedRowCount;
829
+ const warnings = result.warnings ?? [];
830
+ if (affectedCellCount <= 0) {
831
+ if (optimisticRollbackUpdates && optimisticRollbackUpdates.length > 0) {
832
+ await Promise.resolve(options.runtime.api.rows.applyEdits(optimisticRollbackUpdates));
833
+ }
834
+ if (warnings.length > 0) {
835
+ for (const warning of warnings) {
836
+ options.reportFillWarning?.(warning);
837
+ }
838
+ }
839
+ else {
840
+ options.reportFillWarning?.("server fill no-op");
841
+ }
842
+ options.reportFillPlumbingState?.("server_fill_affectedRowCount", false);
843
+ return null;
844
+ }
845
+ const committedOperationId = normalizeServerFillOperationId(result.operationId);
846
+ const invalidationRange = result.invalidation?.kind === "range" ? result.invalidation.range : commitTargetRange;
847
+ const serverFillRefreshPayload = result.invalidation ?? invalidationRange;
848
+ const hasRowInvalidation = result.invalidation?.kind === "rows";
652
849
  const normalizedInvalidationRange = normalizeServerFillInvalidationRange(invalidationRange);
653
850
  options.reportFillPlumbingDetail?.("server_fill_raw_invalidation", JSON.stringify(invalidationRange ?? null));
654
851
  options.reportFillPlumbingDetail?.("server_fill_normalized_invalidation", normalizedInvalidationRange ? `${normalizedInvalidationRange.start}..${normalizedInvalidationRange.end}` : "none");
655
852
  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);
853
+ const runtimeRowModel = options.runtimeRowModel;
854
+ const runtimeRowModelFallback = options.runtime.rowModel;
855
+ const rowsApi = options.runtime.api.rows;
856
+ const invalidateRangeTarget = runtimeRowModel?.invalidateRange ?? runtimeRowModelFallback?.invalidateRange;
857
+ const invalidateRowsTarget = runtimeRowModel?.invalidateRows ?? runtimeRowModelFallback?.invalidateRows;
858
+ const applyServerFillInvalidation = async () => {
859
+ if (result.invalidation?.kind === "rows") {
860
+ if (typeof invalidateRowsTarget === "function") {
861
+ invalidateRowsTarget(result.invalidation.rowIds);
862
+ options.reportFillPlumbingState?.("server_fill_invalidation_applied", true);
863
+ return;
864
+ }
865
+ await Promise.resolve(rowsApi.refresh?.());
866
+ return;
867
+ }
868
+ if (normalizedInvalidationRange && typeof invalidateRangeTarget === "function") {
869
+ invalidateRangeTarget(normalizedInvalidationRange);
667
870
  options.reportFillPlumbingState?.("server_fill_invalidation_applied", true);
871
+ return;
668
872
  }
669
873
  await Promise.resolve(rowsApi.refresh?.());
874
+ };
875
+ promoteCommittedFillSelection(previewRange);
876
+ if (!committedOperationId) {
877
+ options.reportFillWarning?.("server fill committed without operation id; undo/redo disabled");
878
+ options.reportFillPlumbingState?.("server_fill_operationId", false);
879
+ if (hasRowInvalidation) {
880
+ await applyServerFillInvalidation();
881
+ }
882
+ else if (options.refreshServerFillViewport) {
883
+ await Promise.resolve(options.refreshServerFillViewport(serverFillRefreshPayload));
884
+ }
885
+ else {
886
+ await applyServerFillInvalidation();
887
+ }
888
+ return null;
889
+ }
890
+ if (warnings.length > 0) {
891
+ for (const warning of warnings) {
892
+ options.reportFillWarning?.(warning);
893
+ }
894
+ }
895
+ else {
896
+ options.reportFillWarning?.("server fill committed");
897
+ }
898
+ options.reportFillPlumbingState?.("server_fill_affectedRowCount", true);
899
+ options.reportFillPlumbingState?.("server-fill-committed", true);
900
+ if (hasRowInvalidation || !options.refreshServerFillViewport) {
901
+ await applyServerFillInvalidation();
902
+ }
903
+ else {
904
+ await Promise.resolve(options.refreshServerFillViewport(serverFillRefreshPayload));
670
905
  }
671
906
  return {
672
- operationId: result.operationId,
907
+ operationId: committedOperationId,
673
908
  revision: result.revision,
674
909
  affectedRange: invalidationRange,
675
910
  invalidation: result.invalidation ?? null,
676
911
  affectedRowCount,
677
- affectedCellCount: result.affectedCellCount ?? affectedRowCount,
912
+ affectedCellCount,
678
913
  warnings,
679
914
  };
680
915
  },
@@ -704,6 +939,9 @@ export function useDataGridAppInteractionController(options) {
704
939
  mode: session.behavior,
705
940
  });
706
941
  },
942
+ applyCommittedFillSelection: (range) => {
943
+ applySelectionRangeWithActivePosition(range, "start");
944
+ },
707
945
  syncServerFillViewport: options.refreshServerFillViewport,
708
946
  syncViewport: options.syncViewport,
709
947
  });
@@ -785,6 +1023,7 @@ export function useDataGridAppInteractionController(options) {
785
1023
  sourceMatrix,
786
1024
  behavior: resolvedBehavior,
787
1025
  }), { recordHistory: false });
1026
+ promoteCommittedFillSelection(previewRange);
788
1027
  lastAppliedFill.value = {
789
1028
  baseRange: { ...baseRange },
790
1029
  previewRange: { ...previewRange },
@@ -1193,6 +1432,9 @@ export function useDataGridAppInteractionController(options) {
1193
1432
  options.selectionSnapshot.value = nextSnapshot;
1194
1433
  options.runtime.api.selection.setSnapshot(nextSnapshot);
1195
1434
  };
1435
+ const promoteCommittedFillSelection = (range) => {
1436
+ applySelectionRangeWithActivePosition(range, "start");
1437
+ };
1196
1438
  const resolveFillOriginFocusCoord = () => {
1197
1439
  const anchorCoord = resolveSelectionAnchorCoord();
1198
1440
  if (anchorCoord) {
@@ -1232,6 +1474,9 @@ export function useDataGridAppInteractionController(options) {
1232
1474
  if (options.applyRangeMove && baseRange && targetRange) {
1233
1475
  return options.applyRangeMove(baseRange, targetRange);
1234
1476
  }
1477
+ if (baseRange && targetRange && isServerBackedRowModel()) {
1478
+ return applyServerBackedRangeMove(baseRange, targetRange);
1479
+ }
1235
1480
  return rangeMutationEngine.applyRangeMove();
1236
1481
  },
1237
1482
  setRangeMoving: value => {
@@ -1486,6 +1731,7 @@ export function useDataGridAppInteractionController(options) {
1486
1731
  const originCoord = resolveFillOriginFocusCoord();
1487
1732
  const restartSession = resolveRestartableFillSession(baseRange);
1488
1733
  if (fillHandleStart.onSelectionHandleMouseDown(event)) {
1734
+ fillPreviewCancelVersion += 1;
1489
1735
  activeRestartFillSession.value = restartSession;
1490
1736
  fillOriginFocusCoord.value = originCoord;
1491
1737
  }
@@ -1520,6 +1766,100 @@ export function useDataGridAppInteractionController(options) {
1520
1766
  },
1521
1767
  };
1522
1768
  };
1769
+ const resolveServerFillDataSource = () => {
1770
+ const controllerDataSource = options.runtimeRowModel?.dataSource;
1771
+ const runtimeDataSource = options.runtime.rowModel?.dataSource;
1772
+ if (typeof controllerDataSource?.commitFillOperation === "function") {
1773
+ return controllerDataSource;
1774
+ }
1775
+ if (typeof runtimeDataSource?.commitFillOperation === "function") {
1776
+ return runtimeDataSource;
1777
+ }
1778
+ if (typeof controllerDataSource?.resolveFillBoundary === "function") {
1779
+ return controllerDataSource;
1780
+ }
1781
+ if (typeof runtimeDataSource?.resolveFillBoundary === "function") {
1782
+ return runtimeDataSource;
1783
+ }
1784
+ return controllerDataSource ?? runtimeDataSource;
1785
+ };
1786
+ const resolveProjectedFillRowIds = (range) => {
1787
+ const rowIds = [];
1788
+ const requestedRowCount = Math.max(0, range.endRow - range.startRow + 1);
1789
+ let firstMissingRowIndex = null;
1790
+ let lastResolvedRowIndex = null;
1791
+ for (let rowIndex = range.startRow; rowIndex <= range.endRow; rowIndex += 1) {
1792
+ const row = getBodyRowAtIndex(rowIndex);
1793
+ const isMaterialized = options.isRowMaterializedAtIndex
1794
+ ? options.isRowMaterializedAtIndex(rowIndex)
1795
+ : !!row && row.__placeholder !== true;
1796
+ if (!isMaterialized || !row || row.kind === "group" || row.rowId == null || row.__placeholder === true) {
1797
+ firstMissingRowIndex = rowIndex;
1798
+ break;
1799
+ }
1800
+ rowIds.push(row.rowId);
1801
+ lastResolvedRowIndex = rowIndex;
1802
+ }
1803
+ const missingCount = firstMissingRowIndex == null
1804
+ ? 0
1805
+ : Math.max(0, range.endRow - firstMissingRowIndex + 1);
1806
+ return {
1807
+ rowIds,
1808
+ requestedRowCount,
1809
+ missingCount,
1810
+ firstMissingRowIndex,
1811
+ lastResolvedRowIndex,
1812
+ fullyMaterialized: missingCount === 0,
1813
+ };
1814
+ };
1815
+ const reportProjectedFillRowIdResolution = (label, resolution) => {
1816
+ options.reportFillPlumbingDetail?.(`server_fill_${label}_rows_requested`, String(resolution.requestedRowCount));
1817
+ options.reportFillPlumbingDetail?.(`server_fill_${label}_row_ids_resolved`, String(resolution.rowIds.length));
1818
+ options.reportFillPlumbingDetail?.(`server_fill_${label}_row_ids_missing`, String(resolution.missingCount));
1819
+ options.reportFillPlumbingDetail?.(`server_fill_${label}_first_missing_row_index`, resolution.firstMissingRowIndex == null ? "none" : String(resolution.firstMissingRowIndex));
1820
+ };
1821
+ const materializeProjectedFillRows = async (range) => {
1822
+ const normalizedRange = {
1823
+ start: Math.max(0, Math.trunc(range.startRow)),
1824
+ end: Math.max(Math.max(0, Math.trunc(range.startRow)), Math.trunc(range.endRow)),
1825
+ };
1826
+ const runtimeWithViewport = options.runtime;
1827
+ const rowModel = (options.runtimeRowModel ?? options.runtime.rowModel);
1828
+ const rowsApi = options.runtime.api.rows;
1829
+ let attempted = false;
1830
+ try {
1831
+ if (typeof runtimeWithViewport.setViewportRange === "function") {
1832
+ runtimeWithViewport.setViewportRange(normalizedRange);
1833
+ attempted = true;
1834
+ }
1835
+ else if (typeof rowModel?.setViewportRange === "function") {
1836
+ rowModel.setViewportRange(normalizedRange);
1837
+ attempted = true;
1838
+ }
1839
+ if (typeof rowModel?.refresh === "function") {
1840
+ attempted = true;
1841
+ await Promise.resolve(rowModel.refresh("fill-materialize"));
1842
+ }
1843
+ else if (typeof rowsApi.refresh === "function") {
1844
+ attempted = true;
1845
+ await Promise.resolve(rowsApi.refresh());
1846
+ }
1847
+ if (typeof runtimeWithViewport.syncBodyRowsInRange === "function") {
1848
+ runtimeWithViewport.syncBodyRowsInRange(normalizedRange);
1849
+ attempted = true;
1850
+ }
1851
+ }
1852
+ catch (caught) {
1853
+ options.reportFillPlumbingDetail?.("server_fill_materialize_error", caught instanceof Error ? caught.message : String(caught));
1854
+ return false;
1855
+ }
1856
+ options.reportFillPlumbingState?.("server_fill_materialize_attempted", attempted);
1857
+ options.reportFillPlumbingDetail?.("server_fill_materialize_range", `${normalizedRange.start}..${normalizedRange.end}`);
1858
+ return attempted;
1859
+ };
1860
+ const isAcceptedServerFillBoundaryKind = (boundaryKind) => {
1861
+ return boundaryKind === "data-end" || boundaryKind === "gap" || boundaryKind === "cache-boundary";
1862
+ };
1523
1863
  const resolveReferenceColumnExtentEndRow = (baseRange, columnIndex) => {
1524
1864
  const columnKey = options.visibleColumns.value[columnIndex]?.key;
1525
1865
  if (!columnKey) {
@@ -1571,6 +1911,7 @@ export function useDataGridAppInteractionController(options) {
1571
1911
  return evaluateDirection(baseRange.endColumn + 1, 1);
1572
1912
  };
1573
1913
  const resolveServerAwareFillBoundary = async (baseRange) => {
1914
+ serverFillBoundaryConsistencyMetadata.value = null;
1574
1915
  if (!options.resolveFillBoundary) {
1575
1916
  options.reportFillPlumbingState?.("interaction_controller_option", false);
1576
1917
  return null;
@@ -1624,15 +1965,25 @@ export function useDataGridAppInteractionController(options) {
1624
1965
  };
1625
1966
  const left = await resolve(leftReferenceColumns, "left");
1626
1967
  options.reportFillPlumbingState?.("double_click_left_result", left != null);
1627
- if (left && left.boundaryKind !== "unresolved" && left.endRowIndex != null) {
1968
+ if (left && isAcceptedServerFillBoundaryKind(left.boundaryKind) && left.endRowIndex != null) {
1628
1969
  options.reportFillPlumbingState?.("double_click_left_selected", true);
1629
- return { endRow: left.endRowIndex, boundaryKind: left.boundaryKind, resolvedByServer: true };
1970
+ serverFillBoundaryConsistencyMetadata.value = {
1971
+ revision: left.revision,
1972
+ projectionHash: left.projectionHash,
1973
+ boundaryToken: left.boundaryToken,
1974
+ };
1975
+ return { endRow: left.endRowIndex, boundaryKind: left.boundaryKind, resolvedByServer: true, truncated: left.truncated === true };
1630
1976
  }
1631
1977
  const right = await resolve(rightReferenceColumns, "right");
1632
1978
  options.reportFillPlumbingState?.("double_click_right_result", right != null);
1633
- if (right && right.boundaryKind !== "unresolved" && right.endRowIndex != null) {
1979
+ if (right && isAcceptedServerFillBoundaryKind(right.boundaryKind) && right.endRowIndex != null) {
1634
1980
  options.reportFillPlumbingState?.("double_click_right_selected", true);
1635
- return { endRow: right.endRowIndex, boundaryKind: right.boundaryKind, resolvedByServer: true };
1981
+ serverFillBoundaryConsistencyMetadata.value = {
1982
+ revision: right.revision,
1983
+ projectionHash: right.projectionHash,
1984
+ boundaryToken: right.boundaryToken,
1985
+ };
1986
+ return { endRow: right.endRowIndex, boundaryKind: right.boundaryKind, resolvedByServer: true, truncated: right.truncated === true };
1636
1987
  }
1637
1988
  if (left?.boundaryKind === "unresolved" || right?.boundaryKind === "unresolved") {
1638
1989
  options.reportFillWarning?.("server fill boundary unresolved; using loaded-cache fallback");
@@ -1676,6 +2027,10 @@ export function useDataGridAppInteractionController(options) {
1676
2027
  })
1677
2028
  : null;
1678
2029
  if (!previewRange || options.rangesEqual(baseRange, previewRange)) {
2030
+ serverFillBoundaryConsistencyMetadata.value = null;
2031
+ if (resolved?.resolvedByServer && resolved.truncated) {
2032
+ options.reportFillWarning?.("Fill truncated at cache boundary");
2033
+ }
1679
2034
  return;
1680
2035
  }
1681
2036
  const sourceMatrix = options.buildFillMatrixFromRange(baseRange);
@@ -1686,23 +2041,26 @@ export function useDataGridAppInteractionController(options) {
1686
2041
  });
1687
2042
  if (resolved?.resolvedByServer) {
1688
2043
  options.reportFillPlumbingState?.("double_click_server_branch_entered", true);
1689
- const resolvedRowCount = previewRange.endRow - previewRange.startRow + 1;
1690
- const threshold = 500;
2044
+ const normalizedFillBaseRange = options.normalizeClipboardRange(baseRange);
2045
+ if (!options.rangesEqual(options.resolveSelectionRange(), baseRange)) {
2046
+ serverFillBoundaryConsistencyMetadata.value = null;
2047
+ return;
2048
+ }
2049
+ if (normalizedFillBaseRange) {
2050
+ fillBaseRange.value = normalizedFillBaseRange;
2051
+ fillPreviewRange.value = previewRange;
2052
+ }
2053
+ const fillPreviewCancelVersionAtCommitStart = fillPreviewCancelVersion;
1691
2054
  const rowModel = options.runtimeRowModel;
1692
- const dataSource = rowModel?.dataSource ?? options.runtime.rowModel?.dataSource;
2055
+ const runtimeRowModel = options.runtime.rowModel;
2056
+ const dataSource = resolveServerFillDataSource();
1693
2057
  options.reportFillPlumbingState?.("controller_runtimeRowModel_exists", rowModel != null);
1694
- options.reportFillPlumbingState?.("controller_runtimeRowModel_dataSource_exists", dataSource != null);
2058
+ options.reportFillPlumbingState?.("controller_runtimeRowModel_dataSource_exists", rowModel?.dataSource != null);
1695
2059
  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");
2060
+ options.reportFillPlumbingState?.("controller_runtime_rowModel_commit_type", typeof runtimeRowModel?.dataSource?.commitFillOperation === "function");
1697
2061
  const canCommitServerFill = typeof dataSource?.commitFillOperation === "function";
1698
2062
  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
- }
2063
+ const serverFillBehavior = resolveServerFillCommitBehavior(behavior);
1706
2064
  let allLoaded = true;
1707
2065
  for (let rowIndex = previewRange.startRow; rowIndex <= previewRange.endRow; rowIndex += 1) {
1708
2066
  const isMaterialized = options.isRowMaterializedAtIndex
@@ -1716,25 +2074,54 @@ export function useDataGridAppInteractionController(options) {
1716
2074
  if (!allLoaded && !canCommitServerFill) {
1717
2075
  options.reportFillPlumbingState?.("double_click_blocked_unloaded", true);
1718
2076
  options.reportFillWarning?.("server fill execution not implemented yet");
2077
+ clearFillPreviewState(normalizedFillBaseRange, previewRange);
1719
2078
  return;
1720
2079
  }
1721
2080
  if (canCommitServerFill) {
1722
- options.reportFillPlumbingState?.("server-fill-committed", true);
1723
- void Promise.resolve(applyFillRange(baseRange, previewRange, behavior)).then((applied) => {
2081
+ if (serverFillBehavior.downgraded) {
2082
+ options.reportFillWarning?.(SERVER_FILL_SERIES_UNSUPPORTED_WARNING);
2083
+ options.reportFillPlumbingState?.("server_fill_series_downgraded_to_copy", true);
2084
+ }
2085
+ void Promise.resolve(applyFillRange(baseRange, previewRange, serverFillBehavior.behavior))
2086
+ .then((applied) => {
1724
2087
  if (!applied) {
1725
2088
  return;
1726
2089
  }
1727
- activeFillBehavior.value = behavior;
2090
+ if (fillPreviewCancelVersion !== fillPreviewCancelVersionAtCommitStart) {
2091
+ return;
2092
+ }
2093
+ applySelectionRangeWithActivePosition(previewRange, "start");
2094
+ if (normalizedFillBaseRange
2095
+ && options.rangesEqual(fillBaseRange.value, normalizedFillBaseRange)
2096
+ && options.rangesEqual(fillPreviewRange.value, previewRange)) {
2097
+ fillBaseRange.value = null;
2098
+ fillPreviewRange.value = null;
2099
+ }
2100
+ activeFillBehavior.value = serverFillBehavior.behavior;
2101
+ if (resolved.truncated && !lastServerFillMaterializationWarning) {
2102
+ options.reportFillWarning?.("Fill truncated at cache boundary");
2103
+ }
1728
2104
  const restoredCoord = preferredFocusCoord
1729
2105
  ? restoreSelectionActiveCellToCoord(preferredFocusCoord)
1730
2106
  : null;
1731
2107
  restoreActiveCellFocus(restoredCoord ?? preferredFocusCoord);
2108
+ })
2109
+ .catch((caught) => {
2110
+ options.reportFillWarning?.(`server fill commit failed: ${caught instanceof Error ? caught.message : String(caught)}`);
2111
+ })
2112
+ .finally(() => {
2113
+ clearFillPreviewState(normalizedFillBaseRange, previewRange);
1732
2114
  });
1733
2115
  return;
1734
2116
  }
2117
+ options.reportFillPlumbingState?.("double_click_blocked_unloaded", true);
2118
+ options.reportFillWarning?.("server fill execution not implemented yet");
2119
+ clearFillPreviewState(normalizedFillBaseRange, previewRange);
2120
+ return;
1735
2121
  }
1736
2122
  options.reportFillPlumbingState?.("double_click_batch_commit_path", true);
1737
- void applyCommittedFillRange(baseRange, previewRange, behavior).then(applied => {
2123
+ void applyCommittedFillRange(baseRange, previewRange, behavior)
2124
+ .then(applied => {
1738
2125
  if (!applied) {
1739
2126
  return;
1740
2127
  }
@@ -1743,7 +2130,12 @@ export function useDataGridAppInteractionController(options) {
1743
2130
  ? restoreSelectionActiveCellToCoord(preferredFocusCoord)
1744
2131
  : null;
1745
2132
  restoreActiveCellFocus(restoredCoord ?? preferredFocusCoord);
2133
+ })
2134
+ .catch((caught) => {
2135
+ options.reportFillWarning?.(`fill commit failed: ${caught instanceof Error ? caught.message : String(caught)}`);
1746
2136
  });
2137
+ }).catch((caught) => {
2138
+ options.reportFillWarning?.(`server fill boundary failed: ${caught instanceof Error ? caught.message : String(caught)}`);
1747
2139
  });
1748
2140
  };
1749
2141
  const applyLastFillBehavior = async (behavior) => {
@@ -1777,6 +2169,15 @@ export function useDataGridAppInteractionController(options) {
1777
2169
  columnIndex,
1778
2170
  rowId: row.rowId ?? null,
1779
2171
  });
2172
+ if (hasActiveFillPreviewState()) {
2173
+ cancelFillPreviewState();
2174
+ if (event.button === 0 && coord) {
2175
+ event.preventDefault();
2176
+ options.setCellSelection(row, rowOffset, columnIndex, false, false);
2177
+ target?.focus({ preventScroll: true });
2178
+ }
2179
+ return;
2180
+ }
1780
2181
  const currentRange = options.resolveSelectionRange();
1781
2182
  if (options.mode.value === "base"
1782
2183
  && isRangeMoveEnabled.value
@@ -1819,9 +2220,9 @@ export function useDataGridAppInteractionController(options) {
1819
2220
  };
1820
2221
  const handleCellKeydown = (event, row, rowOffset, columnIndex) => {
1821
2222
  const rowIndex = resolveRowIndex(row, rowOffset);
1822
- if (isFillDragging.value && event.key === "Escape") {
2223
+ if (hasActiveFillPreviewState() && event.key === "Escape") {
1823
2224
  event.preventDefault();
1824
- stopFillSelection(false);
2225
+ cancelFillPreviewState();
1825
2226
  return;
1826
2227
  }
1827
2228
  if (!supportsCellSelectionMode()) {