feat(context-menu): copy INSERT from selected columns

This commit is contained in:
二丫讲梵 authored and GitHub committed 2026-07-20 13:59:49 +08:00
1 parent 7078c3fe19
commit 5803df3f73
18 files changed
+551 -49

No files matched your search

@@ -3345,6 +3345,7 @@ const {
isSelectingAll,
selectedRange,
selectedCells,
selectedCellMatrix,
selectedCellCount,
hasCellSelection,
clearCellSelection,
@@ -4979,6 +4980,11 @@ const {
copySelectionCsv,
copySelectionJson,
copySelectionSqlInList,
copySelectionAsInsert,
prefetchSelectionAsInsertStatement,
canCopyPreparedSelectionInsert,
canCopySelectionAsInsert,
selectionInsertRowCount,
copySelectedRowsTsv,
copySelectedRowsTsvWithHeaders,
copyColumnNames,
@@ -5018,6 +5024,7 @@ const {
exportBatchSize: computed(() => settingsStore.editorSettings.exportBatchSize),
hasCellSelection,
selectedCells,
selectedCellMatrix,
selectedRange,
contextCell: exportContextCell,
getRowItem: (rowId: number) => visibleDisplayItems.value.find((item) => item.id === rowId),
@@ -6356,6 +6363,7 @@ function onHeaderContext(col: string, columnIndex: number) {
}
contextHeaderColumn.value = col;
contextHeaderColumnIndex.value = columnIndex;
void prefetchCopyStatements();
}
async function copyHeaderColumn() {
if (!contextHeaderColumn.value) return;
@@ -6568,6 +6576,12 @@ function onRowContext(rowId: number, rowIndex: number) {
}
async function prefetchCopyStatements() {
if (canCopySelectionAsInsert.value) {
await prefetchSelectionAsInsertStatement();
if (selectionInsertRowCount.value > 1 && canCopyPreparedSelectionInsert()) {
await prefetchSelectionAsInsertStatement("row-by-row");
}
}
await prefetchRowAsInsertStatement(false);
if (isMultiRow.value) {
await prefetchRowAsInsertStatement(false, "row-by-row");
@@ -7262,6 +7276,13 @@ function copySubmenu(): ContextMenuItem {
}
function selectionSubmenu(): ContextMenuItem {
const insertItems: ContextMenuItem[] =
selectedCells.value.rows.length > 1
? [
{ label: t("grid.copySelectionInsertMerged"), action: () => copySelectionAsInsert("merged"), disabled: () => !canCopySelectionAsInsert.value || !canCopyPreparedSelectionInsert("merged") },
{ label: t("grid.copySelectionInsertRowByRow"), action: () => copySelectionAsInsert("row-by-row"), disabled: () => !canCopySelectionAsInsert.value || !canCopyPreparedSelectionInsert("row-by-row") },
]
: [{ label: t("grid.copySelectionInsert"), action: () => copySelectionAsInsert(), disabled: () => !canCopySelectionAsInsert.value || !canCopyPreparedSelectionInsert() }];
return {
label: t("grid.selection"),
icon: SquareDashed,
@@ -7271,6 +7292,7 @@ function selectionSubmenu(): ContextMenuItem {
{ label: t("grid.copySelectionCsv"), action: copySelectionCsv },
{ label: t("grid.copySelectionJson"), action: copySelectionJson },
{ label: t("grid.copySelectionSql"), action: copySelectionSqlInList },
...insertItems,
{ label: "", separator: true },
{ label: t("grid.clearSelection"), action: clearCellSelection },
],
@@ -7,7 +7,7 @@ import { registerGlobalContextMenu, type ContextMenuRegistration } from "@/compo
export interface ContextMenuItem {
label: string;
action?: () => void;
disabled?: boolean;
disabled?: boolean | (() => boolean);
separator?: boolean;
icon?: Component;
iconClass?: string;
@@ -102,14 +102,14 @@ onMounted(() => {
});
function handleItemClick(item: ContextMenuItem) {
if (item.disabled) return;
if (itemIsDisabled(item)) return;
if (item.children?.length) return; // submenu trigger — do nothing on click
close();
item.action?.();
}
function handleSubItemClick(item: ContextMenuItem) {
if (item.disabled) return;
if (itemIsDisabled(item)) return;
close();
item.action?.();
}
@@ -143,7 +143,7 @@ function onItemMouseEnter(index: number, event: MouseEvent) {
lastMouseX = event.clientX;
lastMouseY = event.clientY;
const item = activeItems.value[index];
if (!item?.children?.length || item.disabled) {
if (!item?.children?.length || itemIsDisabled(item)) {
// Moving to an item without children — close submenu immediately, no delay needed
activeSubIndex.value = null;
return;
@@ -240,6 +240,10 @@ function itemButtonClass(variant?: "default" | "destructive") {
];
}
function itemIsDisabled(item: ContextMenuItem): boolean {
return typeof item.disabled === "function" ? item.disabled() : !!item.disabled;
}
function shortcutKeys(shortcut?: string): string[] {
return shortcutDisplayKeys(shortcut);
}
@@ -264,7 +268,7 @@ onBeforeUnmount(() => {
<div v-if="item.separator" class="-mx-1 my-1 flex items-center px-1">
<div class="h-px flex-1 bg-border/70" />
</div>
<button v-else :disabled="item.disabled" :class="[...itemButtonClass(item.variant), activeSubIndex === index ? 'bg-accent text-accent-foreground' : '']" @click="handleItemClick(item)" @mouseenter="(e) => onItemMouseEnter(index, e)" @mouseleave="onItemMouseLeave">
<button v-else :disabled="itemIsDisabled(item)" :class="[...itemButtonClass(item.variant), activeSubIndex === index ? 'bg-accent text-accent-foreground' : '']" @click="handleItemClick(item)" @mouseenter="(e) => onItemMouseEnter(index, e)" @mouseleave="onItemMouseLeave">
<span class="flex size-4 shrink-0 items-center justify-center">
<component :is="item.icon" v-if="item.icon" :class="['size-4', item.iconClass]" />
</span>
@@ -293,7 +297,7 @@ onBeforeUnmount(() => {
<div v-if="child.separator" class="-mx-1 my-1 flex items-center px-1">
<div class="h-px flex-1 bg-border/70" />
</div>
<button v-else :disabled="child.disabled" :class="itemButtonClass(child.variant)" @click="handleSubItemClick(child)">
<button v-else :disabled="itemIsDisabled(child)" :class="itemButtonClass(child.variant)" @click="handleSubItemClick(child)">
<span class="flex size-4 shrink-0 items-center justify-center">
<component :is="child.icon" v-if="child.icon" :class="['size-4', child.iconClass]" />
</span>