mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-31 00:22:30 +00:00
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:
+5
-1
@@ -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 (
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user