fix(grid): optimize data grid interactions
This commit is contained in:
parent
84215a7e60
commit
150daadb53
|
|
@ -1,7 +1,7 @@
|
|||
<script setup lang="ts">
|
||||
import { ref, computed, watch, onMounted, onBeforeUnmount, nextTick } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { AlignLeft, Copy, ChevronDown } from "@lucide/vue";
|
||||
import { AlignLeft, Copy, ChevronDown, Undo2, Redo2 } from "@lucide/vue";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Tooltip, TooltipTrigger, TooltipContent } from "@/components/ui/tooltip";
|
||||
import { useTheme } from "@/composables/useTheme";
|
||||
|
|
@ -14,10 +14,14 @@ const props = defineProps<{
|
|||
sql: string;
|
||||
sqlFormatDialect?: SqlFormatDialect;
|
||||
loading?: boolean;
|
||||
canUndo?: boolean;
|
||||
canRedo?: boolean;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
close: [];
|
||||
undo: [];
|
||||
redo: [];
|
||||
}>();
|
||||
|
||||
const { t } = useI18n();
|
||||
|
|
@ -145,7 +149,7 @@ onBeforeUnmount(() => {
|
|||
<span class="flex-1 min-w-0" />
|
||||
<Tooltip>
|
||||
<TooltipTrigger as-child>
|
||||
<Button variant="ghost" size="icon" class="h-6 w-6" :class="isFormatted ? 'text-amber-600 bg-amber-500/10' : 'text-amber-600/60 hover:text-amber-700 hover:bg-amber-500/10'" :disabled="formatting || !hasSql" @click="toggleFormat">
|
||||
<Button variant="ghost" size="icon" class="h-6 w-6" :class="isFormatted ? 'text-amber-600 bg-amber-500/10' : 'text-amber-600/60 hover:text-amber-700 hover:bg-amber-500/10'" :disabled="formatting || !hasSql" :aria-label="t('toolbar.formatSql')" @click="toggleFormat">
|
||||
<AlignLeft class="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
|
|
@ -153,7 +157,23 @@ onBeforeUnmount(() => {
|
|||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger as-child>
|
||||
<Button variant="ghost" size="icon" class="h-6 w-6 text-muted-foreground/60 hover:text-foreground hover:bg-accent" :disabled="!hasSql" @click="handleCopy">
|
||||
<Button variant="ghost" size="icon" class="h-6 w-6 text-muted-foreground/60 hover:text-foreground hover:bg-accent" :disabled="!canUndo" :aria-label="t('grid.undoChange')" @click="emit('undo')">
|
||||
<Undo2 class="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{{ t("grid.undoChange") }}</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger as-child>
|
||||
<Button variant="ghost" size="icon" class="h-6 w-6 text-muted-foreground/60 hover:text-foreground hover:bg-accent" :disabled="!canRedo" :aria-label="t('grid.redoChange')" @click="emit('redo')">
|
||||
<Redo2 class="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{{ t("grid.redoChange") }}</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger as-child>
|
||||
<Button variant="ghost" size="icon" class="h-6 w-6 text-muted-foreground/60 hover:text-foreground hover:bg-accent" :disabled="!hasSql" :aria-label="t('grid.copy')" @click="handleCopy">
|
||||
<Copy class="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
|
|
@ -161,7 +181,7 @@ onBeforeUnmount(() => {
|
|||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger as-child>
|
||||
<Button variant="ghost" size="icon" class="h-6 w-6 text-muted-foreground/60 hover:text-foreground hover:bg-accent" @click="emit('close')">
|
||||
<Button variant="ghost" size="icon" class="h-6 w-6 text-muted-foreground/60 hover:text-foreground hover:bg-accent" :aria-label="t('toolbar.hidePreviewSql')" @click="emit('close')">
|
||||
<ChevronDown class="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
|
|
@ -178,7 +198,7 @@ onBeforeUnmount(() => {
|
|||
|
||||
<!-- Empty -->
|
||||
<div v-else-if="!hasSql" class="flex items-center justify-center h-full text-xs text-muted-foreground">
|
||||
{{ t("editor.pressToExecute", { mod: "Cmd/Ctrl" }) }}
|
||||
{{ t("grid.previewSqlEmpty") }}
|
||||
</div>
|
||||
|
||||
<!-- Shiki highlighted SQL -->
|
||||
|
|
|
|||
|
|
@ -84,7 +84,7 @@ import TemporalCellEditor from "@/components/grid/TemporalCellEditor.vue";
|
|||
import EnumCellEditor from "@/components/grid/EnumCellEditor.vue";
|
||||
import type { QueryResult, ColumnInfo, DatabaseType, ForeignKeyInfo, IndexInfo, TriggerInfo, TableInfoTab } from "@/types/database";
|
||||
import * as api from "@/lib/api";
|
||||
import { coerceDataGridCellValue, dataGridCellDisplayText, dataGridCellEditorText } from "@/lib/dataGridCellCoercion";
|
||||
import { dataGridCellDisplayText, dataGridCellEditorText } from "@/lib/dataGridCellCoercion";
|
||||
import { createColumnDrafts } from "@/lib/tableStructureEditorState";
|
||||
import type { BuildSingleColumnAlterSqlOptions } from "@/lib/tableStructureEditorSql";
|
||||
import { buildTableSelectSql, quoteTableIdentifier } from "@/lib/tableSelectSql";
|
||||
|
|
@ -147,6 +147,7 @@ import { getTableMetadataCapabilities } from "@/lib/tableMetadataCapabilities";
|
|||
import { forgetDataGridConditionHistory, loadDataGridConditionHistory, rememberDataGridConditionHistory } from "@/lib/dataGridConditionHistory";
|
||||
import { caretPositionInsideInsertedSqlSingleQuotes, insertedSqlSingleQuoteAtCaret } from "@/lib/sqlQuoteCaret";
|
||||
import { effectiveDatabaseTypeForConnection } from "@/lib/jdbcDialect";
|
||||
import { isMacOS } from "@/lib/platform";
|
||||
|
||||
const SqlPreviewPanel = defineAsyncComponent(() => import("@/components/editor/SqlPreviewPanel.vue"));
|
||||
|
||||
|
|
@ -218,6 +219,9 @@ const props = defineProps<{
|
|||
const dataGridTraceId = uuid().slice(0, 8);
|
||||
const dataGridCreatedAt = performance.now();
|
||||
const dataGridElapsed = () => `${Math.round(performance.now() - dataGridCreatedAt)}ms`;
|
||||
const isMac = isMacOS();
|
||||
const shortcutMod = isMac ? "Cmd" : "Ctrl";
|
||||
const DATA_GRID_COMPACT_TOPBAR_WIDTH = 900;
|
||||
|
||||
const emit = defineEmits<{
|
||||
reload: [sql?: string, searchText?: string, whereInput?: string, orderBy?: string, limit?: number, offset?: number];
|
||||
|
|
@ -309,10 +313,12 @@ const columnCommentMap = computed(() => {
|
|||
}
|
||||
return map;
|
||||
});
|
||||
const dataGridTopbarWidth = ref(0);
|
||||
const showColumnCommentsInHeader = computed(() => settingsStore.editorSettings.showColumnCommentsInHeader);
|
||||
const showColumnTypesInHeader = computed(() => settingsStore.editorSettings.showColumnTypesInHeader);
|
||||
const compactColumnHeaderActions = computed(() => settingsStore.editorSettings.compactColumnHeaderActions);
|
||||
const dataGridRenderMode = computed(() => settingsStore.editorSettings.dataGridRenderMode);
|
||||
const compactDataGridToolbar = computed(() => dataGridTopbarWidth.value > 0 && dataGridTopbarWidth.value < DATA_GRID_COMPACT_TOPBAR_WIDTH);
|
||||
const infiniteScrollEnabled = computed(() => settingsStore.editorSettings.infiniteScroll);
|
||||
const infiniteScrollMaxRows = computed(() => settingsStore.editorSettings.infiniteScrollMaxRows);
|
||||
|
||||
|
|
@ -1346,13 +1352,17 @@ function dismissWhereSuggestions() {
|
|||
|
||||
function navigateWhereSuggestion(delta: number) {
|
||||
if (whereSuggestions.value.length === 0) return;
|
||||
if (whereSuggestionIndex.value < 0) {
|
||||
whereSuggestionIndex.value = delta > 0 ? 0 : whereSuggestions.value.length - 1;
|
||||
return;
|
||||
}
|
||||
whereSuggestionIndex.value = Math.min(Math.max(whereSuggestionIndex.value + delta, 0), whereSuggestions.value.length - 1);
|
||||
}
|
||||
|
||||
function showWhereHistorySuggestions() {
|
||||
const history = loadDataGridConditionHistory("where", conditionHistoryScope.value, whereFilterInput.value);
|
||||
whereSuggestions.value = history.map((value) => ({ value, kind: "history" }));
|
||||
whereSuggestionIndex.value = whereSuggestions.value.length ? 0 : -1;
|
||||
whereSuggestionIndex.value = -1;
|
||||
if (whereSuggestions.value.length) updateWhereSuggestionPosition();
|
||||
}
|
||||
|
||||
|
|
@ -1469,7 +1479,7 @@ function onWhereFilterKeydown(e: KeyboardEvent) {
|
|||
}
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
if (whereSuggestions.value.length > 0) {
|
||||
if (whereSuggestions.value.length > 0 && whereSuggestionIndex.value >= 0) {
|
||||
acceptWhereSuggestion();
|
||||
return;
|
||||
}
|
||||
|
|
@ -1523,13 +1533,17 @@ function dismissOrderBySuggestions() {
|
|||
|
||||
function navigateOrderBySuggestion(delta: number) {
|
||||
if (orderBySuggestions.value.length === 0) return;
|
||||
if (orderBySuggestionIndex.value < 0) {
|
||||
orderBySuggestionIndex.value = delta > 0 ? 0 : orderBySuggestions.value.length - 1;
|
||||
return;
|
||||
}
|
||||
orderBySuggestionIndex.value = Math.min(Math.max(orderBySuggestionIndex.value + delta, 0), orderBySuggestions.value.length - 1);
|
||||
}
|
||||
|
||||
function showOrderByHistorySuggestions() {
|
||||
const history = loadDataGridConditionHistory("orderBy", conditionHistoryScope.value, orderByInput.value);
|
||||
orderBySuggestions.value = history.map((value) => ({ value, kind: "history" }));
|
||||
orderBySuggestionIndex.value = orderBySuggestions.value.length ? 0 : -1;
|
||||
orderBySuggestionIndex.value = -1;
|
||||
if (orderBySuggestions.value.length) updateOrderBySuggestionPosition();
|
||||
}
|
||||
|
||||
|
|
@ -1588,7 +1602,7 @@ function onOrderByKeydown(e: KeyboardEvent) {
|
|||
}
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
if (orderBySuggestions.value.length > 0) {
|
||||
if (orderBySuggestions.value.length > 0 && orderBySuggestionIndex.value >= 0) {
|
||||
acceptOrderBySuggestion();
|
||||
return;
|
||||
}
|
||||
|
|
@ -1599,6 +1613,7 @@ function onOrderByKeydown(e: KeyboardEvent) {
|
|||
const isApplyingWhere = ref(false);
|
||||
const rowStatusFilter = ref<RowStatusFilter>("all");
|
||||
const gridRef = ref<HTMLDivElement>();
|
||||
const dataGridTopbarRef = ref<HTMLDivElement>();
|
||||
const headerRef = ref<HTMLDivElement>();
|
||||
const gridScrollbarGutter = ref(0);
|
||||
const gridHorizontalScrollbarTrackRef = ref<HTMLDivElement>();
|
||||
|
|
@ -1613,6 +1628,7 @@ const gridHorizontalScrollbarDragging = ref(false);
|
|||
const gridVerticalScrollbarDragging = ref(false);
|
||||
let gridHorizontalScrollbarFrame = 0;
|
||||
let gridHorizontalScrollbarResizeObserver: ResizeObserver | null = null;
|
||||
let dataGridTopbarResizeObserver: ResizeObserver | null = null;
|
||||
let gridHorizontalScrollbarDragState: {
|
||||
trackRect: DOMRect;
|
||||
thumbOffsetPx: number;
|
||||
|
|
@ -1892,6 +1908,21 @@ function observeGridHorizontalScrollbarScroller() {
|
|||
scheduleGridHorizontalScrollbarUpdate();
|
||||
}
|
||||
|
||||
function updateDataGridTopbarWidth() {
|
||||
dataGridTopbarWidth.value = dataGridTopbarRef.value?.clientWidth ?? 0;
|
||||
}
|
||||
|
||||
function observeDataGridTopbarWidth() {
|
||||
dataGridTopbarResizeObserver?.disconnect();
|
||||
dataGridTopbarResizeObserver = null;
|
||||
const topbar = dataGridTopbarRef.value;
|
||||
updateDataGridTopbarWidth();
|
||||
if (topbar && typeof ResizeObserver !== "undefined") {
|
||||
dataGridTopbarResizeObserver = new ResizeObserver(updateDataGridTopbarWidth);
|
||||
dataGridTopbarResizeObserver.observe(topbar);
|
||||
}
|
||||
}
|
||||
|
||||
function applyGridHorizontalScrollbarDrag(clientX: number) {
|
||||
const scroller = gridScrollerElement();
|
||||
const dragState = gridHorizontalScrollbarDragState;
|
||||
|
|
@ -2705,12 +2736,12 @@ const {
|
|||
isSaving,
|
||||
saveError,
|
||||
useTransaction,
|
||||
enterTransaction,
|
||||
exitTransaction,
|
||||
startEdit,
|
||||
commitEdit,
|
||||
commitEditFromBlur,
|
||||
applyCellValue,
|
||||
restoreCellValue,
|
||||
cancelEdit,
|
||||
onEditKeydown,
|
||||
addRow: addEditorRow,
|
||||
|
|
@ -2725,6 +2756,10 @@ const {
|
|||
cloneRows,
|
||||
saveChanges,
|
||||
discardChanges,
|
||||
canUndoPendingChange,
|
||||
canRedoPendingChange,
|
||||
undoPendingChange,
|
||||
redoPendingChange,
|
||||
rowDataWithChanges,
|
||||
canEditColumn,
|
||||
resetGridVerticalScroll,
|
||||
|
|
@ -2752,7 +2787,7 @@ async function refreshPreviewSql() {
|
|||
function schedulePreviewRefresh() {
|
||||
if (!showSqlPreview.value) return;
|
||||
if (pendingChangeCount.value === 0) {
|
||||
// All changes were discarded — close the preview
|
||||
// Keep the panel visible so undo/redo results are explicit in the SQL preview area.
|
||||
previewSqlText.value = "";
|
||||
return;
|
||||
}
|
||||
|
|
@ -2833,15 +2868,6 @@ function tableColumnForGridColumn(columnIndex: number): ColumnInfo | undefined {
|
|||
return props.tableMeta?.columns.find((column) => column.name.toLowerCase() === columnName.toLowerCase());
|
||||
}
|
||||
|
||||
function coerceDetailCellValue(value: string, oldValue: CellValue | undefined, columnIndex: number): CellValue {
|
||||
return coerceDataGridCellValue({
|
||||
value,
|
||||
oldValue,
|
||||
databaseType: props.databaseType,
|
||||
columnInfo: tableColumnForGridColumn(columnIndex),
|
||||
}) as CellValue;
|
||||
}
|
||||
|
||||
function temporalEditorKindForColumn(columnIndex: number): TemporalCellEditorKind | undefined {
|
||||
return temporalCellEditorKind(tableColumnForGridColumn(columnIndex)?.data_type, props.databaseType);
|
||||
}
|
||||
|
|
@ -3645,30 +3671,8 @@ function commitDetailEdit() {
|
|||
|
||||
const item = getRowItem(detail.rowId);
|
||||
if (!item || item.isDeleted) return;
|
||||
|
||||
if (item.isNew && item.newIndex !== undefined) {
|
||||
const oldVal = newRows.value[item.newIndex]?.[detail.colIndex];
|
||||
newRows.value[item.newIndex][detail.colIndex] = coerceDetailCellValue(detailEditValue.value, oldVal, detail.colIndex);
|
||||
return;
|
||||
}
|
||||
|
||||
if (item.sourceIndex === undefined) return;
|
||||
if (!canEditExistingRows.value) return;
|
||||
|
||||
const oldVal = props.result.rows[item.sourceIndex]?.[detail.colIndex];
|
||||
const newVal = coerceDetailCellValue(detailEditValue.value, oldVal, detail.colIndex);
|
||||
if (newVal !== oldVal) {
|
||||
if (!dirtyRows.value.has(item.sourceIndex)) dirtyRows.value.set(item.sourceIndex, new Map());
|
||||
dirtyRows.value.get(item.sourceIndex)!.set(detail.colIndex, newVal);
|
||||
if (useTransaction.value && !transactionActive.value) {
|
||||
enterTransaction();
|
||||
}
|
||||
} else {
|
||||
const rowChanges = dirtyRows.value.get(item.sourceIndex);
|
||||
rowChanges?.delete(detail.colIndex);
|
||||
if (rowChanges?.size === 0) dirtyRows.value.delete(item.sourceIndex);
|
||||
}
|
||||
dirtyRows.value = new Map(dirtyRows.value);
|
||||
applyCellValue(detail.rowId, detail.colIndex, detailEditValue.value);
|
||||
detailCell.value = detailCell.value ? { ...detailCell.value } : null;
|
||||
}
|
||||
|
||||
function cancelDetailEdit() {
|
||||
|
|
@ -3710,16 +3714,10 @@ function restoreDetailOriginalValue() {
|
|||
|
||||
let restoredValue: CellValue = null;
|
||||
|
||||
if (item.isNew && item.newIndex !== undefined) {
|
||||
newRows.value[item.newIndex][detail.colIndex] = null;
|
||||
newRows.value = [...newRows.value];
|
||||
} else if (item.sourceIndex !== undefined) {
|
||||
if (!item.isNew && item.sourceIndex !== undefined) {
|
||||
restoredValue = props.result.rows[item.sourceIndex]?.[detail.colIndex] ?? null;
|
||||
const rowChanges = dirtyRows.value.get(item.sourceIndex);
|
||||
rowChanges?.delete(detail.colIndex);
|
||||
if (rowChanges?.size === 0) dirtyRows.value.delete(item.sourceIndex);
|
||||
dirtyRows.value = new Map(dirtyRows.value);
|
||||
}
|
||||
restoreCellValue(detail.rowId, detail.colIndex);
|
||||
|
||||
detailEditValue.value = dataGridCellEditorText({
|
||||
value: restoredValue,
|
||||
|
|
@ -3752,22 +3750,7 @@ function setDetailNull() {
|
|||
const item = getRowItem(detail.rowId);
|
||||
if (!item || item.isDeleted) return;
|
||||
|
||||
if (item.isNew && item.newIndex !== undefined) {
|
||||
newRows.value[item.newIndex][detail.colIndex] = null;
|
||||
newRows.value = [...newRows.value];
|
||||
resetDetailEdit();
|
||||
detailCell.value = { ...detailCell.value! };
|
||||
return;
|
||||
}
|
||||
|
||||
if (item.sourceIndex === undefined) return;
|
||||
if (!canEditExistingRows.value) return;
|
||||
if (!dirtyRows.value.has(item.sourceIndex)) dirtyRows.value.set(item.sourceIndex, new Map());
|
||||
dirtyRows.value.get(item.sourceIndex)!.set(detail.colIndex, null);
|
||||
dirtyRows.value = new Map(dirtyRows.value);
|
||||
if (useTransaction.value && !transactionActive.value) {
|
||||
enterTransaction();
|
||||
}
|
||||
applyCellValue(detail.rowId, detail.colIndex, null);
|
||||
resetDetailEdit();
|
||||
detailCell.value = { ...detailCell.value! };
|
||||
}
|
||||
|
|
@ -4478,6 +4461,7 @@ function drawCanvasGrid() {
|
|||
}
|
||||
|
||||
watch(useCanvasGridRows, () => nextTick(attachCanvasResizeObserver), { immediate: true });
|
||||
watch(showDataGridTopbar, () => nextTick(observeDataGridTopbarWidth), { immediate: true });
|
||||
watch(
|
||||
[
|
||||
displayRowRefs,
|
||||
|
|
@ -4508,6 +4492,8 @@ function pauseCanvasGridWork() {
|
|||
dataGridIsActive = false;
|
||||
canvasResizeObserver?.disconnect();
|
||||
canvasResizeObserver = null;
|
||||
dataGridTopbarResizeObserver?.disconnect();
|
||||
dataGridTopbarResizeObserver = null;
|
||||
canvasPixelRatioMediaQueryCleanup?.();
|
||||
canvasPixelRatioMediaQueryCleanup = null;
|
||||
canvasPixelRatioMediaQuery = null;
|
||||
|
|
@ -4525,6 +4511,7 @@ function resumeCanvasGridWork() {
|
|||
dataGridIsActive = true;
|
||||
nextTick(() => {
|
||||
attachCanvasResizeObserver();
|
||||
observeDataGridTopbarWidth();
|
||||
refreshGridScrollerMetrics();
|
||||
observeGridHorizontalScrollbarScroller();
|
||||
});
|
||||
|
|
@ -4542,6 +4529,7 @@ onDeactivated(pauseCanvasGridWork);
|
|||
onUnmounted(() => {
|
||||
pauseCanvasGridWork();
|
||||
gridHorizontalScrollbarResizeObserver?.disconnect();
|
||||
dataGridTopbarResizeObserver?.disconnect();
|
||||
stopColumnHeaderDrag(false);
|
||||
stopGridHorizontalScrollbarDrag();
|
||||
stopGridVerticalScrollbarDrag();
|
||||
|
|
@ -5160,6 +5148,18 @@ function commitGridEdit() {
|
|||
nextTick(() => gridRef.value?.focus({ preventScroll: true }));
|
||||
}
|
||||
|
||||
function undoGridChange(): boolean {
|
||||
if (editingCell.value || !canUndoPendingChange.value) return false;
|
||||
undoPendingChange();
|
||||
return true;
|
||||
}
|
||||
|
||||
function redoGridChange(): boolean {
|
||||
if (editingCell.value || !canRedoPendingChange.value) return false;
|
||||
redoPendingChange();
|
||||
return true;
|
||||
}
|
||||
|
||||
function openCellDetailSearch(): boolean {
|
||||
return getDetailEditor()?.openSearch() ?? false;
|
||||
}
|
||||
|
|
@ -5179,6 +5179,37 @@ async function onGridKeydown(event: KeyboardEvent) {
|
|||
return;
|
||||
}
|
||||
if (eventTargetAllowsNativeClipboard(event)) return;
|
||||
if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === "z") {
|
||||
const handled = event.shiftKey ? redoGridChange() : undoGridChange();
|
||||
if (handled) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (event.ctrlKey && !event.metaKey && event.key.toLowerCase() === "y") {
|
||||
if (redoGridChange()) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if ((event.metaKey || event.ctrlKey) && !event.shiftKey && event.key.toLowerCase() === "n") {
|
||||
if (props.editable && (props.tableMeta || props.customSaveHandler)) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
addRow();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if ((event.metaKey || event.ctrlKey) && !event.shiftKey && event.key.toLowerCase() === "s") {
|
||||
if (saveToolbarState.value.showActions && !saveToolbarState.value.actionsDisabled) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
await onToolbarCommit();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (isCopyCurrentRowShortcut(event, settingsStore.editorSettings.shortcuts) && copyCurrentRow()) {
|
||||
event.preventDefault();
|
||||
return;
|
||||
|
|
@ -6530,6 +6561,7 @@ const gridContextMenuItems = computed<ContextMenuItem[]>(() => {
|
|||
<div v-if="hasData || canShowWhereSearch" class="flex-1 flex flex-col overflow-hidden" @contextmenu="onContextMenu">
|
||||
<!-- Search bar -->
|
||||
<div
|
||||
ref="dataGridTopbarRef"
|
||||
v-if="showDataGridTopbar"
|
||||
class="data-grid-topbar-scroll shrink-0 overflow-x-auto border-b bg-muted/20"
|
||||
@scroll="
|
||||
|
|
@ -6537,7 +6569,7 @@ const gridContextMenuItems = computed<ContextMenuItem[]>(() => {
|
|||
updateOrderBySuggestionPosition();
|
||||
"
|
||||
>
|
||||
<div class="data-grid-topbar flex items-stretch relative">
|
||||
<div class="data-grid-topbar flex items-stretch relative" :class="{ 'data-grid-topbar--compact': compactDataGridToolbar }">
|
||||
<div v-if="useTransaction && editable && (tableMeta || customSaveHandler)" class="flex items-center px-2 py-0.5 border-r shrink-0">
|
||||
<Select :model-value="rowStatusFilter" @update:model-value="(value: any) => setRowStatusFilter(String(value))">
|
||||
<SelectTrigger class="h-5 max-w-28 border-0 bg-transparent px-0 py-0 text-xs font-medium text-foreground/70 shadow-none focus-visible:ring-0 data-[state=open]:text-foreground [&_svg]:size-3">
|
||||
|
|
@ -6705,15 +6737,16 @@ const gridContextMenuItems = computed<ContextMenuItem[]>(() => {
|
|||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<span class="text-blue-600 dark:text-blue-400 text-xs font-medium select-none shrink-0">WHERE</span>
|
||||
<span class="data-grid-topbar-condition-label data-grid-topbar-condition-label--where" :class="{ 'data-grid-topbar-condition-label--compact': compactDataGridToolbar }">WHERE</span>
|
||||
<input
|
||||
ref="whereFilterInputRef"
|
||||
v-model="whereFilterInput"
|
||||
autocapitalize="off"
|
||||
autocorrect="off"
|
||||
spellcheck="false"
|
||||
class="flex-1 h-5 min-w-0 text-xs bg-transparent outline-none placeholder:text-muted-foreground/60"
|
||||
placeholder=""
|
||||
class="data-grid-topbar-condition-input data-grid-topbar-condition-input--where flex-1 h-5 min-w-0 text-xs bg-transparent outline-none"
|
||||
:class="{ 'data-grid-topbar-condition-input--compact': compactDataGridToolbar }"
|
||||
placeholder="WHERE"
|
||||
@input="onWhereFilterInput"
|
||||
@keydown="onWhereFilterKeydown"
|
||||
@focus="showWhereHistorySuggestions"
|
||||
|
|
@ -6767,15 +6800,16 @@ const gridContextMenuItems = computed<ContextMenuItem[]>(() => {
|
|||
<span class="h-5 w-px bg-border group-hover:bg-primary/60" />
|
||||
</button>
|
||||
<div class="flex flex-1 items-center gap-1 px-2 py-0.5 border-r min-w-0 relative">
|
||||
<span class="text-orange-600 dark:text-orange-400 text-xs font-medium select-none shrink-0">ORDER BY</span>
|
||||
<span class="data-grid-topbar-condition-label data-grid-topbar-condition-label--order" :class="{ 'data-grid-topbar-condition-label--compact': compactDataGridToolbar }">ORDER BY</span>
|
||||
<input
|
||||
ref="orderByInputRef"
|
||||
v-model="orderByInput"
|
||||
autocapitalize="off"
|
||||
autocorrect="off"
|
||||
spellcheck="false"
|
||||
class="flex-1 h-5 min-w-0 text-xs bg-transparent outline-none placeholder:text-muted-foreground/60"
|
||||
placeholder=""
|
||||
class="data-grid-topbar-condition-input data-grid-topbar-condition-input--order flex-1 h-5 min-w-0 text-xs bg-transparent outline-none"
|
||||
:class="{ 'data-grid-topbar-condition-input--compact': compactDataGridToolbar }"
|
||||
placeholder="ORDER BY"
|
||||
@keydown="onOrderByKeydown"
|
||||
@focus="showOrderByHistorySuggestions"
|
||||
@click="updateOrderBySuggestionPosition"
|
||||
|
|
@ -6845,30 +6879,47 @@ const gridContextMenuItems = computed<ContextMenuItem[]>(() => {
|
|||
{{ t("grid.keylessEditWarningHint") }}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<Button v-if="props.context !== 'results'" variant="ghost" size="sm" class="h-5 text-xs px-1.5 shrink-0" :disabled="isSaving" @click="onToolbarRefresh">
|
||||
<Loader2 v-if="loading" class="w-3 h-3 mr-1 animate-spin" />
|
||||
<RefreshCcw v-else class="w-3 h-3 mr-1" />
|
||||
{{ t("grid.refresh") }}
|
||||
</Button>
|
||||
<Tooltip>
|
||||
<Tooltip v-if="props.context !== 'results'">
|
||||
<TooltipTrigger as-child>
|
||||
<Button variant="ghost" size="sm" class="h-5 text-xs px-1.5 shrink-0" :class="dataGridRenderMode === 'canvas' ? 'text-primary bg-primary/10' : ''" @click="toggleDataGridRenderMode">
|
||||
<SquareDashed class="w-3 h-3 mr-1" />
|
||||
{{ dataGridRenderMode === "canvas" ? t("grid.canvasRenderMode") : t("grid.domRenderMode") }}
|
||||
<Button variant="ghost" size="sm" :class="['data-grid-topbar-action-button h-5 shrink-0 text-xs px-1.5', compactDataGridToolbar ? 'data-grid-topbar-action-button--compact' : '', isSaving ? '' : '']" :disabled="isSaving" @click="onToolbarRefresh">
|
||||
<Loader2 v-if="loading" class="data-grid-topbar-action-icon w-3 h-3 animate-spin" />
|
||||
<RefreshCcw v-else class="data-grid-topbar-action-icon w-3 h-3" />
|
||||
<span class="data-grid-topbar-action-label" :class="{ 'data-grid-topbar-action-label--compact': compactDataGridToolbar }">{{ t("grid.refresh") }}</span>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom" class="max-w-sm">
|
||||
{{ t("grid.renderModeHint") }}
|
||||
</TooltipContent>
|
||||
<TooltipContent side="bottom">{{ t("grid.refresh") }} ({{ shortcutMod }}+R)</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger as-child>
|
||||
<Button variant="ghost" size="sm" :class="['data-grid-topbar-action-button h-5 shrink-0 text-xs px-1.5', compactDataGridToolbar ? 'data-grid-topbar-action-button--compact' : '', dataGridRenderMode === 'canvas' ? 'text-primary bg-primary/10' : '']" @click="toggleDataGridRenderMode">
|
||||
<SquareDashed class="data-grid-topbar-action-icon w-3 h-3" />
|
||||
<span class="data-grid-topbar-action-label" :class="{ 'data-grid-topbar-action-label--compact': compactDataGridToolbar }">{{ dataGridRenderMode === "canvas" ? t("grid.canvasRenderMode") : t("grid.domRenderMode") }}</span>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom" class="max-w-sm"> {{ dataGridRenderMode === "canvas" ? t("grid.canvasRenderMode") : t("grid.domRenderMode") }} · {{ t("grid.renderModeHint") }} </TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip v-if="editable && (tableMeta || customSaveHandler)">
|
||||
<TooltipTrigger as-child>
|
||||
<Button variant="ghost" size="sm" :class="['data-grid-topbar-action-button h-5 shrink-0 text-xs px-1.5', compactDataGridToolbar ? 'data-grid-topbar-action-button--compact' : '']" @click="addRow">
|
||||
<Plus class="data-grid-topbar-action-icon w-3 h-3" />
|
||||
<span class="data-grid-topbar-action-label" :class="{ 'data-grid-topbar-action-label--compact': compactDataGridToolbar }">{{ t("grid.addRow") }}</span>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom">{{ t("grid.addRow") }} ({{ shortcutMod }}+N)</TooltipContent>
|
||||
</Tooltip>
|
||||
<Button v-if="editable && (tableMeta || customSaveHandler)" variant="ghost" size="sm" class="h-5 text-xs px-1.5 shrink-0" @click="addRow"> <Plus class="w-3 h-3 mr-1" /> {{ t("grid.addRow") }} </Button>
|
||||
<template v-if="saveToolbarState.showActions">
|
||||
<Tooltip v-if="pendingChangeCount > 0">
|
||||
<TooltipTrigger as-child>
|
||||
<Button variant="ghost" size="sm" class="h-5 text-xs px-1.5 shrink-0 text-sky-600 hover:bg-sky-500/10 hover:text-sky-700" :disabled="isPreviewLoading" @click="openSqlPreview">
|
||||
<Loader2 v-if="isPreviewLoading" class="w-3 h-3 mr-1 animate-spin" />
|
||||
<Eye v-else class="w-3 h-3 mr-1" />
|
||||
{{ t("toolbar.previewSql") }}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
:class="['data-grid-topbar-action-button h-5 shrink-0 text-xs px-1.5 text-sky-600 hover:bg-sky-500/10 hover:text-sky-700', compactDataGridToolbar ? 'data-grid-topbar-action-button--compact' : '']"
|
||||
:disabled="isPreviewLoading"
|
||||
@click="openSqlPreview"
|
||||
>
|
||||
<Loader2 v-if="isPreviewLoading" class="data-grid-topbar-action-icon w-3 h-3 animate-spin" />
|
||||
<Eye v-else class="data-grid-topbar-action-icon w-3 h-3" />
|
||||
<span class="data-grid-topbar-action-label" :class="{ 'data-grid-topbar-action-label--compact': compactDataGridToolbar }">{{ t("toolbar.previewSql") }}</span>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom" class="max-w-sm">
|
||||
|
|
@ -6877,23 +6928,45 @@ const gridContextMenuItems = computed<ContextMenuItem[]>(() => {
|
|||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger as-child>
|
||||
<Button variant="default" size="sm" class="h-5 text-xs px-1.5 shrink-0" :disabled="saveToolbarState.actionsDisabled" @click="onToolbarCommit">
|
||||
<Loader2 v-if="isSaving" class="w-3 h-3 mr-1 animate-spin" />
|
||||
<span v-else-if="pendingChangeCount > 0" class="mr-1 flex h-3.5 min-w-3.5 items-center justify-center rounded-full bg-amber-300 px-1 text-[9px] font-semibold leading-none text-amber-950 shadow-[0_0_0_1px_rgba(120,53,15,0.16)] dark:bg-amber-400 dark:text-amber-950">
|
||||
<Button
|
||||
variant="default"
|
||||
size="sm"
|
||||
:class="['data-grid-topbar-action-button data-grid-topbar-action-button--commit relative h-5 shrink-0 text-xs px-1.5', compactDataGridToolbar ? 'data-grid-topbar-action-button--compact' : '']"
|
||||
:disabled="saveToolbarState.actionsDisabled"
|
||||
@click="onToolbarCommit"
|
||||
>
|
||||
<Loader2 v-if="isSaving" class="data-grid-topbar-action-icon w-3 h-3 animate-spin" />
|
||||
<Save v-else-if="compactDataGridToolbar || pendingChangeCount === 0" class="data-grid-topbar-action-icon w-3 h-3" />
|
||||
<span
|
||||
v-if="pendingChangeCount > 0"
|
||||
:class="
|
||||
compactDataGridToolbar
|
||||
? 'absolute -right-1 -top-1 flex h-3.5 min-w-3.5 items-center justify-center rounded-full bg-amber-300 px-1 text-[9px] font-semibold leading-none text-amber-950 shadow-[0_0_0_1px_rgba(120,53,15,0.16)] dark:bg-amber-400 dark:text-amber-950'
|
||||
: 'mr-1 flex h-3.5 min-w-3.5 items-center justify-center rounded-full bg-amber-300 px-1 text-[9px] font-semibold leading-none text-amber-950 shadow-[0_0_0_1px_rgba(120,53,15,0.16)] dark:bg-amber-400 dark:text-amber-950'
|
||||
"
|
||||
>
|
||||
{{ pendingChangeCount }}
|
||||
</span>
|
||||
<Save v-else class="w-3 h-3 mr-1" />
|
||||
{{ t(saveActionMode.labelKey, { count: pendingChangeCount }) }}
|
||||
<span class="data-grid-topbar-action-label" :class="{ 'data-grid-topbar-action-label--compact': compactDataGridToolbar }">{{ t(saveActionMode.labelKey, { count: pendingChangeCount }) }}</span>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom" class="max-w-sm">
|
||||
{{ t(saveActionMode.tooltipKey, { count: pendingChangeCount }) }}
|
||||
</TooltipContent>
|
||||
<TooltipContent side="bottom" class="max-w-sm"> {{ t(saveActionMode.tooltipKey, { count: pendingChangeCount }) }} ({{ shortcutMod }}+S) </TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger as-child>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
:class="['data-grid-topbar-action-button h-5 shrink-0 text-xs px-1.5', compactDataGridToolbar ? 'data-grid-topbar-action-button--compact' : '']"
|
||||
:disabled="saveToolbarState.actionsDisabled"
|
||||
@click="useTransaction ? onToolbarRollback() : discardChanges()"
|
||||
>
|
||||
<RotateCcw class="data-grid-topbar-action-icon w-3 h-3" />
|
||||
<span class="data-grid-topbar-action-label" :class="{ 'data-grid-topbar-action-label--compact': compactDataGridToolbar }">{{ t(saveActionMode.secondaryActionKey) }}</span>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom">{{ t(saveActionMode.secondaryActionKey) }}</TooltipContent>
|
||||
</Tooltip>
|
||||
<Button variant="outline" size="sm" class="h-5 text-xs px-1.5 shrink-0" :disabled="saveToolbarState.actionsDisabled" @click="useTransaction ? onToolbarRollback() : discardChanges()">
|
||||
<RotateCcw class="w-3 h-3 mr-1" />
|
||||
{{ t(saveActionMode.secondaryActionKey) }}
|
||||
</Button>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -8436,7 +8509,7 @@ const gridContextMenuItems = computed<ContextMenuItem[]>(() => {
|
|||
|
||||
<!-- SQL Preview panel for pending data changes -->
|
||||
<div v-if="showSqlPreview" class="h-52 shrink-0 border-t">
|
||||
<SqlPreviewPanel :sql="previewSqlText" :loading="isPreviewLoading" @close="closeSqlPreview" />
|
||||
<SqlPreviewPanel :sql="previewSqlText" :loading="isPreviewLoading" :can-undo="canUndoPendingChange" :can-redo="canRedoPendingChange" @undo="undoGridChange" @redo="redoGridChange" @close="closeSqlPreview" />
|
||||
</div>
|
||||
|
||||
<DangerConfirmDialog
|
||||
|
|
@ -8520,7 +8593,134 @@ const gridContextMenuItems = computed<ContextMenuItem[]>(() => {
|
|||
}
|
||||
|
||||
.data-grid-topbar {
|
||||
--data-grid-topbar-transition-duration: 340ms;
|
||||
--data-grid-topbar-transition-easing: cubic-bezier(0.22, 1, 0.36, 1);
|
||||
min-width: 760px;
|
||||
transition: min-width var(--data-grid-topbar-transition-duration) var(--data-grid-topbar-transition-easing);
|
||||
}
|
||||
|
||||
.data-grid-topbar--compact {
|
||||
min-width: 620px;
|
||||
}
|
||||
|
||||
.data-grid-topbar-condition-label {
|
||||
display: inline-flex;
|
||||
flex-shrink: 0;
|
||||
max-width: 5rem;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
user-select: none;
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
transition:
|
||||
max-width var(--data-grid-topbar-transition-duration) var(--data-grid-topbar-transition-easing),
|
||||
opacity 240ms ease 60ms,
|
||||
transform var(--data-grid-topbar-transition-duration) var(--data-grid-topbar-transition-easing),
|
||||
color 240ms ease;
|
||||
}
|
||||
|
||||
.data-grid-topbar-condition-label--where {
|
||||
color: rgb(37 99 235);
|
||||
}
|
||||
|
||||
.data-grid-topbar-condition-label--order {
|
||||
color: rgb(234 88 12);
|
||||
}
|
||||
|
||||
:global(.dark) [data-grid-root] .data-grid-topbar-condition-label--where {
|
||||
color: rgb(96 165 250);
|
||||
}
|
||||
|
||||
:global(.dark) [data-grid-root] .data-grid-topbar-condition-label--order {
|
||||
color: rgb(251 146 60);
|
||||
}
|
||||
|
||||
.data-grid-topbar-condition-label--compact {
|
||||
max-width: 0;
|
||||
opacity: 0;
|
||||
transform: translateX(-4px);
|
||||
}
|
||||
|
||||
.data-grid-topbar-condition-input::placeholder {
|
||||
color: transparent;
|
||||
transition: color 240ms ease;
|
||||
}
|
||||
|
||||
.data-grid-topbar-condition-input--where.data-grid-topbar-condition-input--compact::placeholder {
|
||||
color: rgb(59 130 246 / 70%);
|
||||
}
|
||||
|
||||
.data-grid-topbar-condition-input--order.data-grid-topbar-condition-input--compact::placeholder {
|
||||
color: rgb(249 115 22 / 70%);
|
||||
}
|
||||
|
||||
:global(.dark) [data-grid-root] .data-grid-topbar-condition-input--where.data-grid-topbar-condition-input--compact::placeholder {
|
||||
color: rgb(147 197 253 / 70%);
|
||||
}
|
||||
|
||||
:global(.dark) [data-grid-root] .data-grid-topbar-condition-input--order.data-grid-topbar-condition-input--compact::placeholder {
|
||||
color: rgb(253 186 116 / 70%);
|
||||
}
|
||||
|
||||
.data-grid-topbar-action-button {
|
||||
max-width: 9rem;
|
||||
min-width: 1.25rem;
|
||||
gap: 0;
|
||||
overflow: hidden;
|
||||
transition:
|
||||
max-width var(--data-grid-topbar-transition-duration) var(--data-grid-topbar-transition-easing),
|
||||
min-width var(--data-grid-topbar-transition-duration) var(--data-grid-topbar-transition-easing),
|
||||
padding-inline var(--data-grid-topbar-transition-duration) var(--data-grid-topbar-transition-easing),
|
||||
color 220ms ease,
|
||||
background-color 220ms ease,
|
||||
border-color 220ms ease;
|
||||
}
|
||||
|
||||
.data-grid-topbar-action-button--compact {
|
||||
max-width: 1.25rem;
|
||||
min-width: 1.25rem;
|
||||
padding-inline: 0;
|
||||
}
|
||||
|
||||
.data-grid-topbar-action-button--commit.data-grid-topbar-action-button--compact {
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.data-grid-topbar-action-icon {
|
||||
flex-shrink: 0;
|
||||
transition:
|
||||
margin-inline-end var(--data-grid-topbar-transition-duration) var(--data-grid-topbar-transition-easing),
|
||||
transform var(--data-grid-topbar-transition-duration) var(--data-grid-topbar-transition-easing);
|
||||
}
|
||||
|
||||
.data-grid-topbar-action-button:not(.data-grid-topbar-action-button--compact) .data-grid-topbar-action-icon {
|
||||
margin-inline-end: 0.25rem;
|
||||
}
|
||||
|
||||
.data-grid-topbar-action-button--compact .data-grid-topbar-action-icon {
|
||||
margin-inline-end: 0;
|
||||
transform: scale(0.96);
|
||||
}
|
||||
|
||||
.data-grid-topbar-action-label {
|
||||
display: inline-block;
|
||||
max-width: 8rem;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
transition:
|
||||
max-width var(--data-grid-topbar-transition-duration) var(--data-grid-topbar-transition-easing),
|
||||
opacity 240ms ease 60ms,
|
||||
transform var(--data-grid-topbar-transition-duration) var(--data-grid-topbar-transition-easing);
|
||||
}
|
||||
|
||||
.data-grid-topbar-action-label--compact {
|
||||
max-width: 0;
|
||||
opacity: 0;
|
||||
transform: translateX(-4px);
|
||||
}
|
||||
|
||||
.data-grid-topbar-scroll {
|
||||
|
|
|
|||
|
|
@ -907,24 +907,46 @@ async function openData() {
|
|||
});
|
||||
const tableSchema = connectionObjectTreeNodeSchema(config, node.database, node.schema);
|
||||
const tableType = node.type === "view" ? "VIEW" : node.type === "materialized_view" ? "MATERIALIZED_VIEW" : "TABLE";
|
||||
const isSameDataTableTab = (tab: (typeof queryStore.tabs)[number]) => tab.mode === "data" && tab.connectionId === node.connectionId && tab.database === node.database && (tab.schema || "") === (tableSchema || "") && (tab.tableMeta?.tableName || tab.title) === node.label;
|
||||
const activateExistingSameTableTab = () => {
|
||||
const existing = queryStore.tabs.find(isSameDataTableTab);
|
||||
if (!existing) return false;
|
||||
queryStore.activeTabId = existing.id;
|
||||
return true;
|
||||
};
|
||||
const resetReusedDataTabState = (tab: (typeof queryStore.tabs)[number]) => {
|
||||
tab.title = node.label;
|
||||
tab.schema = tableSchema;
|
||||
tab.whereInput = undefined;
|
||||
tab.orderByInput = undefined;
|
||||
tab.previewSql = undefined;
|
||||
tab.resultSortColumn = undefined;
|
||||
tab.resultSortColumnIndex = undefined;
|
||||
tab.resultSortDirection = undefined;
|
||||
tab.resultSortMode = undefined;
|
||||
tab.resultLocalSortOriginalRows = undefined;
|
||||
tab.resultSortedSql = undefined;
|
||||
tab.resultPageSql = undefined;
|
||||
tab.resultPageLimit = undefined;
|
||||
tab.resultPageOffset = undefined;
|
||||
tab.resultTotalRowCount = undefined;
|
||||
tab.resultTotalRowCountLoading = undefined;
|
||||
tab.queryAnalysis = undefined;
|
||||
tab.querySourceColumns = undefined;
|
||||
tab.queryEditabilityReason = undefined;
|
||||
};
|
||||
|
||||
if (activateExistingSameTableTab()) {
|
||||
logPhase("existing-tab-activated", { table: node.label });
|
||||
return;
|
||||
}
|
||||
|
||||
const tabId = (() => {
|
||||
if (settingsStore.editorSettings.reuseDataTab) {
|
||||
const existing = queryStore.tabs.find((tab) => tab.mode === "data" && tab.connectionId === node.connectionId && tab.database === node.database);
|
||||
if (existing) {
|
||||
existing.title = node.label;
|
||||
existing.schema = tableSchema;
|
||||
// Reset per-table filter/sort state so the reused tab doesn't keep
|
||||
// the previous table's WHERE/ORDER BY. DataGrid remounts (result is
|
||||
// cleared below) and reinitializes its inputs from these props.
|
||||
existing.whereInput = undefined;
|
||||
existing.orderByInput = undefined;
|
||||
existing.resultSortColumn = undefined;
|
||||
existing.resultSortColumnIndex = undefined;
|
||||
existing.resultSortDirection = undefined;
|
||||
existing.resultSortMode = undefined;
|
||||
existing.resultLocalSortOriginalRows = undefined;
|
||||
existing.resultSortedSql = undefined;
|
||||
queryStore.activeTabId = existing.id;
|
||||
resetReusedDataTabState(existing);
|
||||
return existing.id;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -83,9 +83,12 @@ interface PendingChangesSnapshot {
|
|||
rowCount: number;
|
||||
}
|
||||
|
||||
type PendingChangesHistorySnapshot = Pick<PendingChangesSnapshot, "newRows" | "dirtyRows" | "deletedRows" | "transactionActive">;
|
||||
|
||||
const pendingChangesCache = new Map<string, PendingChangesSnapshot>();
|
||||
const closingPendingSnapshotTabs = new Set<string>();
|
||||
const BEFORE_TAB_SWITCH_EVENT = "dbx:before-tab-switch";
|
||||
const MAX_PENDING_CHANGES_HISTORY = 100;
|
||||
|
||||
function cacheKeyBelongsToTab(cacheKey: string, tabId: string) {
|
||||
return cacheKey === tabId || cacheKey.startsWith(`${tabId}-`);
|
||||
|
|
@ -143,6 +146,8 @@ export function useDataGridEditor(options: UseDataGridEditorOptions) {
|
|||
const dirtyRows = ref<Map<number, Map<number, CellValue>>>(new Map());
|
||||
const newRows = ref<CellValue[][]>([]);
|
||||
const deletedRows = ref<Set<number>>(new Set());
|
||||
const undoStack = ref<PendingChangesHistorySnapshot[]>([]);
|
||||
const redoStack = ref<PendingChangesHistorySnapshot[]>([]);
|
||||
const pendingChangesVersion = ref(0);
|
||||
let restoredEditingCell = false;
|
||||
let restoredTransactionActive = false;
|
||||
|
|
@ -175,6 +180,8 @@ export function useDataGridEditor(options: UseDataGridEditorOptions) {
|
|||
const deletedRowCount = computed(() => deletedRows.value.size);
|
||||
const pendingChangeCount = computed(() => dirtyRowCount.value + newRowCount.value + deletedRowCount.value);
|
||||
const hasPendingChanges = computed(() => pendingChangeCount.value > 0);
|
||||
const canUndoPendingChange = computed(() => undoStack.value.length > 0);
|
||||
const canRedoPendingChange = computed(() => redoStack.value.length > 0);
|
||||
const resolvedDatabaseType = computed(() => databaseType.value ?? effectiveDatabaseTypeForConnection(connectionStore.getConfig(connectionId.value ?? "")));
|
||||
|
||||
// --- Transaction state ---
|
||||
|
|
@ -225,6 +232,50 @@ export function useDataGridEditor(options: UseDataGridEditorOptions) {
|
|||
pendingChangesVersion.value++;
|
||||
}
|
||||
|
||||
function pendingChangesSnapshot(): PendingChangesHistorySnapshot {
|
||||
return {
|
||||
newRows: newRows.value.map((row) => [...row]),
|
||||
dirtyRows: new Map([...dirtyRows.value].map(([rowIndex, changes]) => [rowIndex, new Map(changes)])),
|
||||
deletedRows: new Set(deletedRows.value),
|
||||
transactionActive: transactionActive.value,
|
||||
};
|
||||
}
|
||||
|
||||
function restorePendingChangesSnapshot(snapshot: PendingChangesHistorySnapshot) {
|
||||
newRows.value = snapshot.newRows.map((row) => [...row]);
|
||||
dirtyRows.value = new Map([...snapshot.dirtyRows].map(([rowIndex, changes]) => [rowIndex, new Map(changes)]));
|
||||
deletedRows.value = new Set(snapshot.deletedRows);
|
||||
transactionActive.value = snapshot.transactionActive === true && useTransaction.value === true;
|
||||
editingCell.value = null;
|
||||
touchPendingChanges();
|
||||
}
|
||||
|
||||
function pushUndoSnapshot() {
|
||||
undoStack.value = [...undoStack.value.slice(-MAX_PENDING_CHANGES_HISTORY + 1), pendingChangesSnapshot()];
|
||||
redoStack.value = [];
|
||||
}
|
||||
|
||||
function clearPendingChangeHistory() {
|
||||
undoStack.value = [];
|
||||
redoStack.value = [];
|
||||
}
|
||||
|
||||
function undoPendingChange() {
|
||||
const snapshot = undoStack.value[undoStack.value.length - 1];
|
||||
if (!snapshot) return;
|
||||
undoStack.value = undoStack.value.slice(0, -1);
|
||||
redoStack.value = [...redoStack.value, pendingChangesSnapshot()];
|
||||
restorePendingChangesSnapshot(snapshot);
|
||||
}
|
||||
|
||||
function redoPendingChange() {
|
||||
const snapshot = redoStack.value[redoStack.value.length - 1];
|
||||
if (!snapshot) return;
|
||||
redoStack.value = redoStack.value.slice(0, -1);
|
||||
undoStack.value = [...undoStack.value, pendingChangesSnapshot()];
|
||||
restorePendingChangesSnapshot(snapshot);
|
||||
}
|
||||
|
||||
function exitTransaction() {
|
||||
transactionActive.value = false;
|
||||
}
|
||||
|
|
@ -420,6 +471,7 @@ export function useDataGridEditor(options: UseDataGridEditorOptions) {
|
|||
const oldVal = newRows.value[item.newIndex]?.[col];
|
||||
const newVal = coerceCellValue(editValue.value, oldVal, col);
|
||||
if (newRows.value[item.newIndex]) {
|
||||
if (newVal !== oldVal) pushUndoSnapshot();
|
||||
newRows.value[item.newIndex][col] = newVal;
|
||||
}
|
||||
newRows.value = [...newRows.value];
|
||||
|
|
@ -443,6 +495,7 @@ export function useDataGridEditor(options: UseDataGridEditorOptions) {
|
|||
const oldVal = result.value.rows[item.sourceIndex]?.[col];
|
||||
const newVal = coerceCellValue(editValue.value, oldVal, col);
|
||||
if (newVal !== oldVal) {
|
||||
pushUndoSnapshot();
|
||||
if (!dirtyRows.value.has(item.sourceIndex)) dirtyRows.value.set(item.sourceIndex, new Map());
|
||||
dirtyRows.value.get(item.sourceIndex)!.set(col, newVal);
|
||||
if (useTransaction.value && !transactionActive.value) {
|
||||
|
|
@ -450,6 +503,7 @@ export function useDataGridEditor(options: UseDataGridEditorOptions) {
|
|||
}
|
||||
} else {
|
||||
const rowChanges = dirtyRows.value.get(item.sourceIndex);
|
||||
if (rowChanges?.has(col)) pushUndoSnapshot();
|
||||
rowChanges?.delete(col);
|
||||
if (rowChanges?.size === 0) dirtyRows.value.delete(item.sourceIndex);
|
||||
}
|
||||
|
|
@ -473,8 +527,13 @@ export function useDataGridEditor(options: UseDataGridEditorOptions) {
|
|||
if (!item || item.isDeleted) return;
|
||||
|
||||
if (item.isNew && item.newIndex !== undefined) {
|
||||
const oldVal = newRows.value[item.newIndex]?.[col];
|
||||
newRows.value[item.newIndex][col] = value === null ? null : coerceCellValue(value, oldVal, col);
|
||||
const row = newRows.value[item.newIndex];
|
||||
if (!row) return;
|
||||
const oldVal = row[col];
|
||||
const newVal = value === null ? null : coerceCellValue(value, oldVal, col);
|
||||
if (newVal === oldVal) return;
|
||||
pushUndoSnapshot();
|
||||
row[col] = newVal;
|
||||
newRows.value = [...newRows.value];
|
||||
touchPendingChanges();
|
||||
return;
|
||||
|
|
@ -484,15 +543,20 @@ export function useDataGridEditor(options: UseDataGridEditorOptions) {
|
|||
if (!canEditExistingRows.value) return;
|
||||
|
||||
const oldVal = result.value.rows[item.sourceIndex]?.[col];
|
||||
const rowChanges = dirtyRows.value.get(item.sourceIndex);
|
||||
const hasPendingCellChange = rowChanges?.has(col) ?? false;
|
||||
const currentVal = hasPendingCellChange ? rowChanges!.get(col) : oldVal;
|
||||
const newVal = value === null ? null : coerceCellValue(value, oldVal, col);
|
||||
if (newVal === currentVal) return;
|
||||
if (newVal !== oldVal) {
|
||||
pushUndoSnapshot();
|
||||
if (!dirtyRows.value.has(item.sourceIndex)) dirtyRows.value.set(item.sourceIndex, new Map());
|
||||
dirtyRows.value.get(item.sourceIndex)!.set(col, newVal);
|
||||
if (useTransaction.value && !transactionActive.value) {
|
||||
enterTransaction();
|
||||
}
|
||||
} else {
|
||||
const rowChanges = dirtyRows.value.get(item.sourceIndex);
|
||||
if (hasPendingCellChange) pushUndoSnapshot();
|
||||
rowChanges?.delete(col);
|
||||
if (rowChanges?.size === 0) dirtyRows.value.delete(item.sourceIndex);
|
||||
}
|
||||
|
|
@ -500,6 +564,32 @@ export function useDataGridEditor(options: UseDataGridEditorOptions) {
|
|||
touchPendingChanges();
|
||||
}
|
||||
|
||||
function restoreCellValue(rowId: number, col: number) {
|
||||
if (!canEditColumn(col)) return;
|
||||
const item = getRowItem(rowId);
|
||||
if (!item || item.isDeleted) return;
|
||||
|
||||
if (item.isNew && item.newIndex !== undefined) {
|
||||
const row = newRows.value[item.newIndex];
|
||||
if (!row || row[col] === null) return;
|
||||
pushUndoSnapshot();
|
||||
row[col] = null;
|
||||
newRows.value = [...newRows.value];
|
||||
touchPendingChanges();
|
||||
return;
|
||||
}
|
||||
|
||||
if (item.sourceIndex === undefined) return;
|
||||
if (!canEditExistingRows.value) return;
|
||||
const rowChanges = dirtyRows.value.get(item.sourceIndex);
|
||||
if (!rowChanges?.has(col)) return;
|
||||
pushUndoSnapshot();
|
||||
rowChanges.delete(col);
|
||||
if (rowChanges.size === 0) dirtyRows.value.delete(item.sourceIndex);
|
||||
dirtyRows.value = new Map(dirtyRows.value);
|
||||
touchPendingChanges();
|
||||
}
|
||||
|
||||
function cancelEdit() {
|
||||
const restoreScroll = preserveScrollPosition();
|
||||
isCancelling = true;
|
||||
|
|
@ -521,6 +611,7 @@ export function useDataGridEditor(options: UseDataGridEditorOptions) {
|
|||
}
|
||||
|
||||
function addRow() {
|
||||
pushUndoSnapshot();
|
||||
rowStatusFilter.value = rowStatusFilterAfterAddingRow(rowStatusFilter.value);
|
||||
newRows.value.push(result.value.columns.map(() => null));
|
||||
newRows.value = [...newRows.value];
|
||||
|
|
@ -557,6 +648,7 @@ export function useDataGridEditor(options: UseDataGridEditorOptions) {
|
|||
const item = getRowItem(rowId);
|
||||
if (!item) return;
|
||||
const clonedData = clonedRowData(item);
|
||||
pushUndoSnapshot();
|
||||
rowStatusFilter.value = rowStatusFilterAfterAddingRow(rowStatusFilter.value);
|
||||
newRows.value.push(clonedData);
|
||||
newRows.value = [...newRows.value];
|
||||
|
|
@ -573,10 +665,11 @@ export function useDataGridEditor(options: UseDataGridEditorOptions) {
|
|||
}
|
||||
|
||||
function cloneRows(rowIds: number[]) {
|
||||
const rowsToClone = rowIds.map((rowId) => getRowItem(rowId)).filter(Boolean) as RowItem[];
|
||||
if (rowsToClone.length === 0) return;
|
||||
pushUndoSnapshot();
|
||||
rowStatusFilter.value = rowStatusFilterAfterAddingRow(rowStatusFilter.value);
|
||||
for (const rowId of rowIds) {
|
||||
const item = getRowItem(rowId);
|
||||
if (!item) continue;
|
||||
for (const item of rowsToClone) {
|
||||
const clonedData = clonedRowData(item);
|
||||
newRows.value.push(clonedData);
|
||||
}
|
||||
|
|
@ -591,10 +684,12 @@ export function useDataGridEditor(options: UseDataGridEditorOptions) {
|
|||
const item = getRowItem(rowId);
|
||||
if (!item) return;
|
||||
if (item.isNew && item.newIndex !== undefined) {
|
||||
pushUndoSnapshot();
|
||||
newRows.value.splice(item.newIndex, 1);
|
||||
newRows.value = [...newRows.value];
|
||||
} else if (item.sourceIndex !== undefined) {
|
||||
if (!canEditExistingRows.value) return;
|
||||
pushUndoSnapshot();
|
||||
dirtyRows.value.delete(item.sourceIndex);
|
||||
deletedRows.value.add(item.sourceIndex);
|
||||
dirtyRows.value = new Map(dirtyRows.value);
|
||||
|
|
@ -636,7 +731,8 @@ export function useDataGridEditor(options: UseDataGridEditorOptions) {
|
|||
|
||||
function restoreRow(rowId: number) {
|
||||
const item = getRowItem(rowId);
|
||||
if (item?.sourceIndex !== undefined) {
|
||||
if (item?.sourceIndex !== undefined && deletedRows.value.has(item.sourceIndex)) {
|
||||
pushUndoSnapshot();
|
||||
deletedRows.value.delete(item.sourceIndex);
|
||||
deletedRows.value = new Set(deletedRows.value);
|
||||
touchPendingChanges();
|
||||
|
|
@ -644,9 +740,14 @@ export function useDataGridEditor(options: UseDataGridEditorOptions) {
|
|||
}
|
||||
|
||||
function restoreRows(rowIds: number[]) {
|
||||
for (const rowId of rowIds) {
|
||||
restoreRow(rowId);
|
||||
const sourceIndexes = rowIds.map((rowId) => getRowItem(rowId)?.sourceIndex).filter((sourceIndex): sourceIndex is number => sourceIndex !== undefined && deletedRows.value.has(sourceIndex));
|
||||
if (sourceIndexes.length === 0) return;
|
||||
pushUndoSnapshot();
|
||||
for (const sourceIndex of sourceIndexes) {
|
||||
deletedRows.value.delete(sourceIndex);
|
||||
}
|
||||
deletedRows.value = new Set(deletedRows.value);
|
||||
touchPendingChanges();
|
||||
}
|
||||
|
||||
function deleteSelectedRow(contextCell: Ref<{ rowId: number; rowIndex: number; col: number } | null>) {
|
||||
|
|
@ -749,6 +850,7 @@ export function useDataGridEditor(options: UseDataGridEditorOptions) {
|
|||
dirtyRows.value.clear();
|
||||
newRows.value = [];
|
||||
deletedRows.value.clear();
|
||||
clearPendingChangeHistory();
|
||||
touchPendingChanges();
|
||||
exitTransaction();
|
||||
isSaving.value = false;
|
||||
|
|
@ -834,6 +936,7 @@ export function useDataGridEditor(options: UseDataGridEditorOptions) {
|
|||
dirtyRows.value.clear();
|
||||
newRows.value = [];
|
||||
deletedRows.value.clear();
|
||||
clearPendingChangeHistory();
|
||||
touchPendingChanges();
|
||||
exitTransaction();
|
||||
isSaving.value = false;
|
||||
|
|
@ -846,6 +949,7 @@ export function useDataGridEditor(options: UseDataGridEditorOptions) {
|
|||
dirtyRows.value.clear();
|
||||
newRows.value = [];
|
||||
deletedRows.value.clear();
|
||||
clearPendingChangeHistory();
|
||||
touchPendingChanges();
|
||||
editingCell.value = null;
|
||||
exitTransaction();
|
||||
|
|
@ -973,6 +1077,8 @@ export function useDataGridEditor(options: UseDataGridEditorOptions) {
|
|||
deletedRowCount,
|
||||
pendingChangeCount,
|
||||
hasPendingChanges,
|
||||
canUndoPendingChange,
|
||||
canRedoPendingChange,
|
||||
transactionActive,
|
||||
isSaving,
|
||||
saveError,
|
||||
|
|
@ -983,6 +1089,9 @@ export function useDataGridEditor(options: UseDataGridEditorOptions) {
|
|||
commitEdit,
|
||||
commitEditFromBlur,
|
||||
applyCellValue,
|
||||
restoreCellValue,
|
||||
undoPendingChange,
|
||||
redoPendingChange,
|
||||
cancelEdit,
|
||||
onEditKeydown,
|
||||
addRow,
|
||||
|
|
|
|||
|
|
@ -592,6 +592,7 @@ export default {
|
|||
exportFailed: "Export failed: {message}",
|
||||
copied: "Copied",
|
||||
copyFailed: "Copy failed: {message}",
|
||||
previewSqlEmpty: "No pending SQL changes to preview",
|
||||
domRenderMode: "DOM",
|
||||
canvasRenderMode: "Canvas",
|
||||
renderModeHint: "Switch between Canvas rendering and the DOM fallback grid.",
|
||||
|
|
@ -802,6 +803,8 @@ export default {
|
|||
dataUnavailableHintPrefix: "Press ",
|
||||
dataUnavailableHintSuffix: " or click Refresh below to reload.",
|
||||
refresh: "Refresh",
|
||||
undoChange: "Undo change",
|
||||
redoChange: "Redo change",
|
||||
commit: "Commit",
|
||||
rollback: "Rollback",
|
||||
transactionSaveHint: "Commit {count} pending change(s) in a transaction.",
|
||||
|
|
|
|||
|
|
@ -593,6 +593,7 @@ export default {
|
|||
exportFailed: "导出失败:{message}",
|
||||
copied: "已复制",
|
||||
copyFailed: "复制失败:{message}",
|
||||
previewSqlEmpty: "暂无待预览的 SQL 更改",
|
||||
domRenderMode: "DOM",
|
||||
canvasRenderMode: "Canvas",
|
||||
renderModeHint: "在 Canvas 渲染和 DOM 兜底表格之间切换。",
|
||||
|
|
@ -803,6 +804,8 @@ export default {
|
|||
dataUnavailableHintPrefix: "按 ",
|
||||
dataUnavailableHintSuffix: " 或点击下方刷新按钮重新加载。",
|
||||
refresh: "刷新",
|
||||
undoChange: "撤销更改",
|
||||
redoChange: "重做更改",
|
||||
commit: "提交",
|
||||
rollback: "回滚",
|
||||
transactionSaveHint: "在事务中提交 {count} 项待保存更改。",
|
||||
|
|
|
|||
|
|
@ -102,6 +102,65 @@ function column(name: string, isPrimaryKey = false, extra: string | null = null)
|
|||
};
|
||||
}
|
||||
|
||||
function createPeopleGridEditor(result = computed(() => ({ columns: ["id", "name"], rows: [[1, "Ada"] as CellValue[]] }))) {
|
||||
const rowStatusFilter = ref<"all" | "changed" | "edited" | "new" | "deleted">("all");
|
||||
let editor: ReturnType<typeof useDataGridEditor>;
|
||||
|
||||
editor = useDataGridEditor({
|
||||
result,
|
||||
editable: computed(() => true),
|
||||
databaseType: computed(() => "postgres"),
|
||||
connectionId: computed(() => undefined),
|
||||
database: computed(() => undefined),
|
||||
tableMeta: computed(() => ({
|
||||
tableName: "people",
|
||||
columns: [column("id", true), column("name")],
|
||||
primaryKeys: ["id"],
|
||||
})),
|
||||
onExecuteSql: computed(() => undefined),
|
||||
customSaveHandler: computed(() => undefined),
|
||||
sql: computed(() => "SELECT id, name FROM people"),
|
||||
searchText: ref(""),
|
||||
whereFilterInput: ref(""),
|
||||
orderByInput: ref(""),
|
||||
currentWhereInput: computed(() => undefined),
|
||||
rowStatusFilter,
|
||||
pageSize: ref(50),
|
||||
currentPage: ref(1),
|
||||
getRowItem: (rowId) => {
|
||||
if (rowId === 0) {
|
||||
return {
|
||||
id: 0,
|
||||
sourceIndex: 0,
|
||||
data: editor.rowDataWithChanges(result.value.rows[0], 0),
|
||||
isNew: false,
|
||||
isDeleted: editor.deletedRows.value.has(0),
|
||||
isDirtyCol: [false, editor.dirtyRows.value.get(0)?.has(1) ?? false],
|
||||
status: editor.deletedRows.value.has(0) ? "deleted" : editor.dirtyRows.value.has(0) ? "edited" : "clean",
|
||||
};
|
||||
}
|
||||
if (rowId < 0) {
|
||||
const newIndex = -rowId - 1;
|
||||
const row = editor.newRows.value[newIndex];
|
||||
if (!row) return undefined;
|
||||
return {
|
||||
id: rowId,
|
||||
newIndex,
|
||||
data: row,
|
||||
isNew: true,
|
||||
isDeleted: false,
|
||||
isDirtyCol: [false, false],
|
||||
status: "new",
|
||||
};
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
emit: () => {},
|
||||
});
|
||||
|
||||
return editor;
|
||||
}
|
||||
|
||||
test("row data helper reuses unchanged rows and clones dirty rows only", () => {
|
||||
setActivePinia(createPinia());
|
||||
installBrowserTestGlobals();
|
||||
|
|
@ -441,6 +500,84 @@ test("saving edited rows without deletes does not reload table data", async () =
|
|||
assert.deepEqual(emitted, []);
|
||||
});
|
||||
|
||||
test("undo and redo restore pending cell edits before save", () => {
|
||||
setActivePinia(createPinia());
|
||||
installBrowserTestGlobals();
|
||||
|
||||
const result = computed(() => ({
|
||||
columns: ["id", "name"],
|
||||
rows: [[1, "Ada"] as CellValue[]],
|
||||
}));
|
||||
const editor = createPeopleGridEditor(result);
|
||||
|
||||
editor.applyCellValue(0, 1, "Ada Lovelace");
|
||||
assert.equal(editor.canUndoPendingChange.value, true);
|
||||
assert.equal(editor.canRedoPendingChange.value, false);
|
||||
assert.deepEqual(editor.rowDataWithChanges(result.value.rows[0], 0), [1, "Ada Lovelace"]);
|
||||
|
||||
editor.undoPendingChange();
|
||||
assert.equal(editor.canUndoPendingChange.value, false);
|
||||
assert.equal(editor.canRedoPendingChange.value, true);
|
||||
assert.equal(editor.dirtyRows.value.size, 0);
|
||||
assert.deepEqual(editor.rowDataWithChanges(result.value.rows[0], 0), [1, "Ada"]);
|
||||
|
||||
editor.redoPendingChange();
|
||||
assert.equal(editor.canUndoPendingChange.value, true);
|
||||
assert.equal(editor.canRedoPendingChange.value, false);
|
||||
assert.deepEqual(editor.rowDataWithChanges(result.value.rows[0], 0), [1, "Ada Lovelace"]);
|
||||
});
|
||||
|
||||
test("restoring a pending cell edit records undo and redo history", () => {
|
||||
setActivePinia(createPinia());
|
||||
installBrowserTestGlobals();
|
||||
|
||||
const result = computed(() => ({
|
||||
columns: ["id", "name"],
|
||||
rows: [[1, "Ada"] as CellValue[]],
|
||||
}));
|
||||
const editor = createPeopleGridEditor(result);
|
||||
|
||||
editor.applyCellValue(0, 1, "Ada Lovelace");
|
||||
editor.restoreCellValue(0, 1);
|
||||
assert.equal(editor.canUndoPendingChange.value, true);
|
||||
assert.equal(editor.canRedoPendingChange.value, false);
|
||||
assert.equal(editor.dirtyRows.value.size, 0);
|
||||
assert.deepEqual(editor.rowDataWithChanges(result.value.rows[0], 0), [1, "Ada"]);
|
||||
|
||||
editor.undoPendingChange();
|
||||
assert.equal(editor.canUndoPendingChange.value, true);
|
||||
assert.equal(editor.canRedoPendingChange.value, true);
|
||||
assert.deepEqual(editor.rowDataWithChanges(result.value.rows[0], 0), [1, "Ada Lovelace"]);
|
||||
|
||||
editor.redoPendingChange();
|
||||
assert.equal(editor.canUndoPendingChange.value, true);
|
||||
assert.equal(editor.canRedoPendingChange.value, false);
|
||||
assert.equal(editor.dirtyRows.value.size, 0);
|
||||
assert.deepEqual(editor.rowDataWithChanges(result.value.rows[0], 0), [1, "Ada"]);
|
||||
});
|
||||
|
||||
test("undo and redo cover row add and delete operations", () => {
|
||||
setActivePinia(createPinia());
|
||||
installBrowserTestGlobals();
|
||||
|
||||
const editor = createPeopleGridEditor();
|
||||
|
||||
editor.addRow();
|
||||
assert.equal(editor.newRows.value.length, 1);
|
||||
editor.undoPendingChange();
|
||||
assert.equal(editor.newRows.value.length, 0);
|
||||
editor.redoPendingChange();
|
||||
assert.equal(editor.newRows.value.length, 1);
|
||||
|
||||
editor.applyDeleteRow(0);
|
||||
assert.deepEqual([...editor.deletedRows.value], [0]);
|
||||
editor.undoPendingChange();
|
||||
assert.deepEqual([...editor.deletedRows.value], []);
|
||||
assert.equal(editor.newRows.value.length, 1);
|
||||
editor.redoPendingChange();
|
||||
assert.deepEqual([...editor.deletedRows.value], [0]);
|
||||
});
|
||||
|
||||
test("saving manually typed JSON from a MySQL grid normalizes smart quotes", async () => {
|
||||
setActivePinia(createPinia());
|
||||
installBrowserTestGlobals();
|
||||
|
|
|
|||
Loading…
Reference in New Issue