diff --git a/apps/desktop/src/components/grid/DataGrid.vue b/apps/desktop/src/components/grid/DataGrid.vue index b5449e6871..b10acaaa07 100644 --- a/apps/desktop/src/components/grid/DataGrid.vue +++ b/apps/desktop/src/components/grid/DataGrid.vue @@ -599,6 +599,7 @@ let preservedSelectionOnNextResult: { } | null = null; let preservedDetailsOnNextResult: { sideCell?: PersistedDataGridSelection; + sideCellHasPendingDraft?: boolean; cellDialog?: PersistedDataGridSelection; rowDialog?: PersistedDataGridSelection; columnDialog?: PersistedDataGridSelection; @@ -5942,6 +5943,10 @@ function captureColumnTargetForRefresh(columnIndex: number | null): PersistedDat function captureDetailsForRefresh() { const details = { sideCell: showCellDetail.value ? captureCellTargetForRefresh(detailCell.value) : undefined, + // A refresh must not silently discard an unsaved value-editor draft: closing + // and reopening the panel (even at the "same" cell) would re-derive its text + // from the freshly fetched row, wiping whatever the user was typing. + sideCellHasPendingDraft: showCellDetail.value && hasPendingDetailEditorDraft.value, cellDialog: cellDetailDialogOpen.value ? captureCellTargetForRefresh(cellDetailDialogTarget.value) : undefined, rowDialog: rowDetailDialogOpen.value ? captureRowTargetForRefresh(rowDetailDialogRowId.value) : undefined, columnDialog: columnDetailDialogOpen.value ? captureColumnTargetForRefresh(columnDetailDialogColumnIndex.value) : undefined, @@ -5965,11 +5970,17 @@ function restoreDetailCellAfterRefresh(snapshot: PersistedDataGridSelection | un } function restoreDetailsAfterRefresh(details: NonNullable) { - const sideCell = restoreDetailCellAfterRefresh(details.sideCell); - if (sideCell) { - detailCell.value = sideCell; - showCellDetail.value = true; - hydrateCellDetailTarget(sideCell); + // When the side panel was left open with an unsaved value-editor draft, it was + // deliberately kept open (not closed) across this refresh, so there is nothing + // to restore here - reassigning detailCell would re-derive the editor text from + // the freshly fetched row and clobber the draft the user is still editing. + if (!details.sideCellHasPendingDraft) { + const sideCell = restoreDetailCellAfterRefresh(details.sideCell); + if (sideCell) { + detailCell.value = sideCell; + showCellDetail.value = true; + hydrateCellDetailTarget(sideCell); + } } const cellDialog = restoreDetailCellAfterRefresh(details.cellDialog); @@ -6461,6 +6472,12 @@ watch(activeCellDetailTab, (tab) => { const detailEditValue = ref(""); const detailEditOriginalValue = ref(""); const isEditingDetail = ref(false); +// commitDetailEdit() intentionally re-triggers the activeCellDetail watch below to +// re-sync the editor with the canonical committed value. Any other change to the +// same cell's underlying data while still editing (e.g. a result refresh, or a +// large-value hydration resolving) must not clobber the user's in-progress draft. +let allowActiveCellDetailResync = false; +let lastSyncedDetailCellKey: string | null = null; const detailValueDiffOpen = ref(false); const detailValueDiffSnapshot = ref | null>(null); const hasPendingDetailEditorDraft = computed(() => isEditingDetail.value && detailEditValue.value !== detailEditOriginalValue.value); @@ -6591,6 +6608,15 @@ watch(activeCellDetail, (detail) => { resetDetailEdit(); return; } + const cellKey = `${detail.rowId}:${detail.colIndex}`; + const isSameCellStillEditing = isEditingDetail.value && cellKey === lastSyncedDetailCellKey; + lastSyncedDetailCellKey = cellKey; + if (isSameCellStillEditing && !allowActiveCellDetailResync) { + // The row's underlying data changed (e.g. a result refresh or a large-value + // hydration) while the user is still editing this same cell. Keep their draft. + return; + } + allowActiveCellDetailResync = false; const value = dataGridCellEditorText({ value: detail.value, databaseType: resolvedDatabaseType.value, @@ -6668,6 +6694,7 @@ function resetDetailEdit() { isEditingDetail.value = false; detailEditValue.value = ""; detailEditOriginalValue.value = ""; + lastSyncedDetailCellKey = null; } function closeCellDetails() { @@ -6741,6 +6768,9 @@ function commitDetailEdit() { if (!item || item.isDeleted) return; applyCellValue(detail.rowId, detail.colIndex, detailEditValue.value); detailEditOriginalValue.value = detailEditValue.value; + // Force the activeCellDetail watch to re-run so the editor picks up the + // canonical (possibly coerced) committed value. + allowActiveCellDetailResync = true; detailCell.value = detailCell.value ? { ...detailCell.value } : null; } @@ -10529,7 +10559,10 @@ watch( clearCellSelection(); clearRowSelection(); invalidateContextMenuTarget(); - closeCellDetails(); + // Don't discard an unsaved value-editor draft just because the result set + // refreshed underneath it; leave the panel/editor state as-is and let + // restoreDetailsAfterRefresh() below skip re-opening it from fresh data. + if (!detailsSnapshot?.sideCellHasPendingDraft) closeCellDetails(); closeDetailDialogs(); if (shouldPreserveTranspose) { applyTransposeState(nextTransposeStateForRecordCount(showTranspose.value, transposeRowIndex.value, displayRowCount.value));