diff --git a/apps/desktop/src/components/grid/DataGrid.vue b/apps/desktop/src/components/grid/DataGrid.vue index d8f974ba20..43c4cfc975 100644 --- a/apps/desktop/src/components/grid/DataGrid.vue +++ b/apps/desktop/src/components/grid/DataGrid.vue @@ -3091,7 +3091,7 @@ watch( const tab = queryStore.tabs.find((item) => item.id === props.cacheKey); if (tab?.mode === "data") tab.pendingDataChangeCount = count || undefined; }, - { immediate: true }, + { immediate: true, flush: "sync" }, ); const saveActionMode = computed(() => @@ -4125,6 +4125,32 @@ watch(activeCellDetailTab, (tab) => { } }); +const detailEditValue = ref(""); +const detailEditOriginalValue = ref(""); +const isEditingDetail = ref(false); +const hasPendingDetailEditorDraft = computed(() => isEditingDetail.value && detailEditValue.value !== detailEditOriginalValue.value); +const hasPendingInlineEditorDraft = computed(() => { + const cell = editingCell.value; + if (!cell) return false; + const item = getRowItem(cell.rowId); + if (!item || item.isDeleted) return false; + const originalValue = dataGridCellEditorText({ + value: item.data[cell.col] ?? null, + databaseType: resolvedDatabaseType.value, + columnInfo: tableColumnForGridColumn(cell.col), + }); + return editValue.value !== originalValue; +}); +const hasPendingDataEditorDraft = computed(() => hasPendingDetailEditorDraft.value || hasPendingInlineEditorDraft.value); + +function syncPendingDataEditorDraft(pending: boolean) { + if (props.context !== "table-data" || !props.cacheKey) return; + const tab = queryStore.tabs.find((item) => item.id === props.cacheKey); + if (tab?.mode === "data") tab.hasPendingDataEditorDraft = pending || undefined; +} + +watch(hasPendingDataEditorDraft, syncPendingDataEditorDraft, { immediate: true, flush: "sync" }); + const activeValueEditorActions = computed(() => { const detail = activeCellDetail.value; return valueEditorActions({ @@ -4213,17 +4239,17 @@ watch(activeCellDetail, (detail) => { resetDetailEdit(); return; } - detailEditValue.value = dataGridCellEditorText({ + const value = dataGridCellEditorText({ value: detail.value, databaseType: props.databaseType, columnInfo: tableColumnForGridColumn(detail.colIndex), }); + detailEditValue.value = value; + detailEditOriginalValue.value = value; syncEditorFromDetailEdit(); isEditingDetail.value = true; }); -const detailEditValue = ref(""); -const isEditingDetail = ref(false); const detailTemporalEditorConfig = computed(() => { const detail = activeCellDetail.value; return detail ? temporalEditorConfigForColumn(detail.colIndex) : undefined; @@ -4287,6 +4313,7 @@ watch(valueEditorContainer, async (el) => { function resetDetailEdit() { isEditingDetail.value = false; detailEditValue.value = ""; + detailEditOriginalValue.value = ""; } function closeCellDetails() { @@ -4338,7 +4365,9 @@ function startDetailEdit() { const detail = activeCellDetail.value; if (!detail || !detail.isEditable) return; warnFormattedJsonEditIfNeeded(detail); - detailEditValue.value = cellDetailEditText(detail); + const value = cellDetailEditText(detail); + detailEditValue.value = value; + detailEditOriginalValue.value = value; isEditingDetail.value = true; } @@ -4350,6 +4379,7 @@ function commitDetailEdit() { const item = getRowItem(detail.rowId); if (!item || item.isDeleted) return; applyCellValue(detail.rowId, detail.colIndex, detailEditValue.value); + detailEditOriginalValue.value = detailEditValue.value; detailCell.value = detailCell.value ? { ...detailCell.value } : null; } @@ -4367,11 +4397,13 @@ function syncEditorFromDetailEdit() { function cancelValueEditorEdit() { const detail = activeCellDetail.value; if (!detail || !detail.isEditable) return; - detailEditValue.value = dataGridCellEditorText({ + const value = dataGridCellEditorText({ value: detail.value, databaseType: props.databaseType, columnInfo: tableColumnForGridColumn(detail.colIndex), }); + detailEditValue.value = value; + detailEditOriginalValue.value = value; syncEditorFromDetailEdit(); isEditingDetail.value = true; } @@ -4397,11 +4429,13 @@ function restoreDetailOriginalValue() { } restoreCellValue(detail.rowId, detail.colIndex); - detailEditValue.value = dataGridCellEditorText({ + const value = dataGridCellEditorText({ value: restoredValue, databaseType: props.databaseType, columnInfo: tableColumnForGridColumn(detail.colIndex), }); + detailEditValue.value = value; + detailEditOriginalValue.value = value; syncEditorFromDetailEdit(); isEditingDetail.value = activeCellDetailTab.value === "valueEditor"; detailCell.value = { ...detailCell.value! }; @@ -4410,6 +4444,7 @@ function restoreDetailOriginalValue() { function setValueEditorNull() { setDetailNull(); detailEditValue.value = cellDetailEditorText(null); + detailEditOriginalValue.value = detailEditValue.value; syncEditorFromDetailEdit(); isEditingDetail.value = activeCellDetailTab.value === "valueEditor"; } @@ -8071,6 +8106,7 @@ onDeactivated(() => { }); onUnmounted(() => { + syncPendingDataEditorDraft(false); cleanupFrames(); autoRefresh.stop(); onDdlResizeEnd(); diff --git a/apps/desktop/src/composables/__tests__/useSidebarDataOpenRuntime.spec.ts b/apps/desktop/src/composables/__tests__/useSidebarDataOpenRuntime.spec.ts index cfabec7f85..b4f18dfeb9 100644 --- a/apps/desktop/src/composables/__tests__/useSidebarDataOpenRuntime.spec.ts +++ b/apps/desktop/src/composables/__tests__/useSidebarDataOpenRuntime.spec.ts @@ -223,6 +223,7 @@ describe("useSidebarDataOpenRuntime", () => { ["pinned", { pinned: true }], ["executing", { isExecuting: true, executionId: "running" }], ["pending edits", { pendingDataChangeCount: 1 }], + ["pending editor draft", { hasPendingDataEditorDraft: true }], ])("opens a new tab instead of replacing an active %s data tab", async (_label, patch) => { mocks.dataTabReuseMode = "active-tab"; const activeTab = { diff --git a/apps/desktop/src/lib/__tests__/sidebar/dataTabOpenPolicy.spec.ts b/apps/desktop/src/lib/__tests__/sidebar/dataTabOpenPolicy.spec.ts index 5f915365d0..ad43e26435 100644 --- a/apps/desktop/src/lib/__tests__/sidebar/dataTabOpenPolicy.spec.ts +++ b/apps/desktop/src/lib/__tests__/sidebar/dataTabOpenPolicy.spec.ts @@ -79,6 +79,7 @@ describe("dataTabOpenPolicy", () => { ["explaining", { isExplaining: true }], ["manual transaction", { txnSessionId: "txn-1" }], ["pending edits", { pendingDataChangeCount: 1 }], + ["pending editor draft", { hasPendingDataEditorDraft: true }], ])("does not reuse the active tab when it is %s", (_label, patch) => { const active = Object.assign(dataTab("orders", "orders"), patch); diff --git a/apps/desktop/src/lib/sidebar/dataTabOpenPolicy.ts b/apps/desktop/src/lib/sidebar/dataTabOpenPolicy.ts index 0876997fc5..1a962ad066 100644 --- a/apps/desktop/src/lib/sidebar/dataTabOpenPolicy.ts +++ b/apps/desktop/src/lib/sidebar/dataTabOpenPolicy.ts @@ -5,7 +5,7 @@ import type { QueryTab, TreeNodeType } from "@/types/database"; export type DataTabOpenMode = "default" | "new-tab"; export type { DataTabReuseMode }; -type DataTabLike = Pick; +type DataTabLike = Pick; export interface DataTabTarget { connectionId: string; @@ -45,7 +45,7 @@ function isSameTable(tab: DataTabLike, target: DataTabTarget): boolean { } export function canReuseActiveDataTab(tab: DataTabLike | undefined, target: DataTabTarget): boolean { - return tab !== undefined && isSameDatabase(tab, target) && dataTabCatalog(tab) === (target.catalog || "") && !tab.pinned && !tab.isExecuting && !tab.isCancelling && !tab.isExplaining && !tab.txnSessionId && !tab.pendingDataChangeCount; + return tab !== undefined && isSameDatabase(tab, target) && dataTabCatalog(tab) === (target.catalog || "") && !tab.pinned && !tab.isExecuting && !tab.isCancelling && !tab.isExplaining && !tab.txnSessionId && !tab.pendingDataChangeCount && !tab.hasPendingDataEditorDraft; } export function canApplyDataTabMetadata(tab: DataTabLike | undefined, target: DataTabTarget, signal?: AbortSignal): boolean { diff --git a/apps/desktop/src/types/database.ts b/apps/desktop/src/types/database.ts index d45b7d34f3..7e017dc236 100644 --- a/apps/desktop/src/types/database.ts +++ b/apps/desktop/src/types/database.ts @@ -1062,6 +1062,8 @@ export interface QueryTab { }; tableMetaUpdatedAt?: number; pendingDataChangeCount?: number; + /** Ephemeral editor draft that has not yet been applied to the data grid. */ + hasPendingDataEditorDraft?: boolean; /** 冷缓存打开表数据时元数据仍在途:行标识未知,编辑/保存必须等待其落地 */ tableMetaPending?: boolean; /** 取消请求单调计数:isCancelling 是瞬态的(取消失败/查询先完成会被清),