feat: driver store UX improvements and multi-row range copy

- ESC key closes driver store before closing tabs
- Auto-scroll to driver store tab when opened
- Deactivate query tab highlight when driver store is open
- Support copying range-selected rows as JSON
- Add multiRowCount/isMultiRow computed properties for DataGrid
This commit is contained in:
t8y2
2026-05-13 16:34:56 +08:00
parent adb320192a
commit e513c260b5
4 changed files with 66 additions and 36 deletions
+5 -1
View File
@@ -411,7 +411,11 @@ function onAiRequestAutoExecuteSql(sql: string) {
function handleKeydown(e: KeyboardEvent) {
if (isCloseTabShortcut(e)) {
e.preventDefault();
if (queryStore.activeTabId) queryStore.closeTab(queryStore.activeTabId);
if (showDriverStore.value) {
showDriverStore.value = false;
} else if (queryStore.activeTabId) {
queryStore.closeTab(queryStore.activeTabId);
}
return;
}
if (
+26 -30
View File
@@ -1091,6 +1091,26 @@ const selectionSummary = computed(() => {
return t("grid.selectedCells", { count: selectedCellCount.value });
});
const multiRowCount = computed(() => {
if (hasRowSelection.value) return selectedRowCount.value;
const range = selectedRange.value;
if (range && range.startRow !== range.endRow) return range.endRow - range.startRow + 1;
return 1;
});
const isMultiRow = computed(() => multiRowCount.value > 1);
function affectedRowIds(): number[] {
if (hasRowSelection.value && selectedRowCount.value > 0) {
return [...selectedRowIds.value];
}
const range = selectedRange.value;
if (range && range.startRow !== range.endRow) {
return displayItems.value.slice(range.startRow, range.endRow + 1).map((item) => item.id);
}
return [];
}
function isRowActive(index: number): boolean {
const item = displayItems.value[index];
if (item && isRowSelected(item.id)) return true;
@@ -2570,18 +2590,10 @@ defineExpose({
<ContextMenuSubContent class="w-max max-w-[min(80vw,18rem)]">
<ContextMenuItem v-if="contextColumn" @click="copyCell">{{ t("grid.copyCell") }}</ContextMenuItem>
<ContextMenuItem @click="copyRow">
{{
hasRowSelection && selectedRowCount > 1
? t("grid.copyRows", { count: selectedRowCount })
: t("grid.copyRow")
}}
{{ isMultiRow ? t("grid.copyRows", { count: multiRowCount }) : t("grid.copyRow") }}
</ContextMenuItem>
<ContextMenuItem @click="copyRowAsInsert">
{{
hasRowSelection && selectedRowCount > 1
? t("grid.copyRowsInsert", { count: selectedRowCount })
: t("grid.copyRowInsert")
}}
{{ isMultiRow ? t("grid.copyRowsInsert", { count: multiRowCount }) : t("grid.copyRowInsert") }}
</ContextMenuItem>
<ContextMenuItem @click="copyAll">{{ t("grid.copyAll") }}</ContextMenuItem>
</ContextMenuSubContent>
@@ -2604,17 +2616,9 @@ defineExpose({
</ContextMenuSub>
<ContextMenuSeparator />
<template v-if="editable && contextRowItem">
<ContextMenuItem
@click="
hasRowSelection && selectedRowCount > 1 ? cloneRows([...selectedRowIds]) : cloneRow(contextRowItem.id)
"
>
<ContextMenuItem @click="isMultiRow ? cloneRows(affectedRowIds()) : cloneRow(contextRowItem.id)">
<CopyPlus class="w-3.5 h-3.5 mr-2" />
{{
hasRowSelection && selectedRowCount > 1
? t("grid.cloneRows", { count: selectedRowCount })
: t("grid.cloneRow")
}}
{{ isMultiRow ? t("grid.cloneRows", { count: multiRowCount }) : t("grid.cloneRow") }}
</ContextMenuItem>
<ContextMenuItem v-if="contextRowItem.isDeleted" @click="restoreRow(contextRowItem.id)">
<Undo2 class="w-3.5 h-3.5 mr-2" /> {{ t("grid.restoreRow") }}
@@ -2622,18 +2626,10 @@ defineExpose({
<ContextMenuItem
v-else
class="text-destructive"
@click="
hasRowSelection && selectedRowCount > 1
? requestDeleteRows([...selectedRowIds])
: requestDeleteRow(contextRowItem.id)
"
@click="isMultiRow ? requestDeleteRows(affectedRowIds()) : requestDeleteRow(contextRowItem.id)"
>
<Trash2 class="w-3.5 h-3.5 mr-2" />
{{
hasRowSelection && selectedRowCount > 1
? t("grid.deleteRows", { count: selectedRowCount })
: t("grid.deleteRow")
}}
{{ isMultiRow ? t("grid.deleteRows", { count: multiRowCount }) : t("grid.deleteRow") }}
</ContextMenuItem>
<ContextMenuSeparator />
</template>
+22 -5
View File
@@ -17,7 +17,7 @@ import { connectionColor, tabDisplayTitle, tabTooltipLines } from "@/lib/tabPres
import { hexToRgba } from "@/lib/color";
import type { QueryTab } from "@/types/database";
defineProps<{
const props = defineProps<{
showDriverStore?: boolean;
}>();
@@ -55,9 +55,25 @@ watch(
},
);
watch(
() => props.showDriverStore,
(show) => {
if (!show) return;
nextTick(() => {
const container = tabsContainerRef.value;
if (!container) return;
const el = container.querySelector("[data-driver-store-tab]");
if (el) {
el.scrollIntoView({ behavior: "smooth", block: "nearest", inline: "center" });
}
updateScrollButtons();
});
},
);
function tabColorStyle(tab: QueryTab) {
const color = connectionColor(tab.connectionId);
const isActive = tab.id === queryStore.activeTabId;
const isActive = tab.id === queryStore.activeTabId && !props.showDriverStore;
const isClassic = settingsStore.editorSettings.appLayout === "classic";
if (!color) {
if (isClassic) {
@@ -124,19 +140,19 @@ function tabIconClass(tab: QueryTab) {
settingsStore.editorSettings.appLayout === 'classic'
? [
'h-full border-r border-border/50',
tab.id === queryStore.activeTabId
tab.id === queryStore.activeTabId && !showDriverStore
? 'bg-background text-foreground font-medium'
: 'text-foreground/70 hover:text-foreground/90',
]
: [
'h-7 rounded-md border',
tab.id === queryStore.activeTabId
tab.id === queryStore.activeTabId && !showDriverStore
? 'text-foreground font-medium'
: 'border-border/60 text-foreground/70 hover:border-border hover:text-foreground/90',
]
"
:style="tabColorStyle(tab)"
:data-active-tab="tab.id === queryStore.activeTabId"
:data-active-tab="tab.id === queryStore.activeTabId && !showDriverStore"
@click="
queryStore.activeTabId = tab.id;
emit('close-driver-store');
@@ -202,6 +218,7 @@ function tabIconClass(tab: QueryTab) {
<!-- Driver Store Tab -->
<div
v-if="showDriverStore"
data-driver-store-tab
class="group flex min-w-38 items-center gap-1 px-2 text-xs cursor-pointer transition-colors whitespace-nowrap"
:class="
settingsStore.editorSettings.appLayout === 'classic'
+13
View File
@@ -110,6 +110,19 @@ export function useDataGridExport(options: UseDataGridExportOptions) {
copyText(JSON.stringify(objects, null, 2));
return;
}
const range = selectedRange.value;
if (range && range.startRow !== range.endRow) {
const items = displayItems.value.slice(range.startRow, range.endRow + 1);
const objects = items.map((item) => {
const obj: Record<string, unknown> = {};
columns.value.forEach((col, i) => {
obj[col] = item.data[i];
});
return obj;
});
copyText(JSON.stringify(objects, null, 2));
return;
}
if (!contextCell.value) return;
const item = getRowItem(contextCell.value.rowId);
if (!item) return;