From 56c937f844aee94e416cabc667b62ad765230b92 Mon Sep 17 00:00:00 2001 From: t8y2 <1156263951@qq.com> Date: Thu, 28 May 2026 13:24:03 +0800 Subject: [PATCH] refactor: migrate all context menus to CustomContextMenu Replace reka-ui ContextMenu usage in QueryHistory, DataGrid, AppTabBar, ObjectBrowser, and TreeItem with the lightweight CustomContextMenu component. This removes the last reka-ui context menu dependencies and eliminates the performance overhead of multiple cached ContextMenu instances. --- .../src/components/editor/QueryHistory.vue | 85 +- apps/desktop/src/components/grid/DataGrid.vue | 3629 +++++++++-------- .../src/components/layout/AppTabBar.vue | 78 +- .../src/components/objects/ObjectBrowser.vue | 341 +- .../src/components/sidebar/TreeItem.vue | 823 ++-- 5 files changed, 2476 insertions(+), 2480 deletions(-) diff --git a/apps/desktop/src/components/editor/QueryHistory.vue b/apps/desktop/src/components/editor/QueryHistory.vue index 1dc925f851..fee07a017e 100644 --- a/apps/desktop/src/components/editor/QueryHistory.vue +++ b/apps/desktop/src/components/editor/QueryHistory.vue @@ -6,7 +6,7 @@ import { Clock, Copy, Database, RotateCcw, Search, Sparkles, Trash2, X } from "l import { RecycleScroller } from "vue-virtual-scroller"; import { Button } from "@/components/ui/button"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog"; -import { ContextMenu, ContextMenuContent, ContextMenuItem, ContextMenuTrigger } from "@/components/ui/context-menu"; +import CustomContextMenu, { type ContextMenuItem } from "@/components/ui/CustomContextMenu.vue"; import { useHistoryStore } from "@/stores/historyStore"; import { useToast } from "@/composables/useToast"; import { shouldClearHistory, shouldDeleteHistoryEntry } from "@/lib/historyActions"; @@ -170,6 +170,17 @@ async function rollback(entry: HistoryEntry) { } } +function getHistoryMenuItems(entry: HistoryEntry): ContextMenuItem[] { + return [ + { label: t("history.viewDetails"), action: () => (selectedEntry = entry) }, + { label: t("history.restore"), action: () => restore(entry) }, + { label: t("history.analyzeWithAi"), action: () => emit("analyzeAi", entry), icon: Sparkles }, + { label: t("history.copy"), action: () => copyText(entry.sql) }, + ...(canRollbackHistoryEntry(entry) ? [{ label: t("history.rollback"), action: () => rollback(entry) }] : []), + { label: t("history.delete"), action: () => confirmDeleteEntry(entry.id), variant: "destructive" as const }, + ]; +} + onMounted(() => store.load()); @@ -224,51 +235,35 @@ onMounted(() => store.load()); key-field="id" > diff --git a/apps/desktop/src/components/grid/DataGrid.vue b/apps/desktop/src/components/grid/DataGrid.vue index 94fc95668e..8bd8311e57 100644 --- a/apps/desktop/src/components/grid/DataGrid.vue +++ b/apps/desktop/src/components/grid/DataGrid.vue @@ -46,16 +46,7 @@ import { LockKeyhole, } from "lucide-vue-next"; import { Button } from "@/components/ui/button"; -import { - ContextMenu, - ContextMenuContent, - ContextMenuItem, - ContextMenuSeparator, - ContextMenuSub, - ContextMenuSubContent, - ContextMenuSubTrigger, - ContextMenuTrigger, -} from "@/components/ui/context-menu"; +import CustomContextMenu, { type ContextMenuItem } from "@/components/ui/CustomContextMenu.vue"; import { DropdownMenu, DropdownMenuContent, @@ -3611,6 +3602,176 @@ defineExpose({ invertColumnVisibility, openCellDetailSearch, }); + +// ---- CustomContextMenu ---- + +function rowActionLabels() { + return { + clone: isMultiRow.value ? t("grid.cloneRows", { count: multiRowCount.value }) : t("grid.cloneRow"), + restore: isMultiRow.value ? t("grid.restoreRows", { count: multiRowCount.value }) : t("grid.restoreRow"), + delete: isMultiRow.value ? t("grid.deleteRows", { count: multiRowCount.value }) : t("grid.deleteRow"), + }; +} + +function copyRowLabels() { + const count = multiRowCount.value; + return { + row: isMultiRow.value ? t("grid.copyRows", { count }) : t("grid.copyRow"), + insert: isMultiRow.value ? t("grid.copyRowsInsert", { count }) : t("grid.copyRowInsert"), + insertNoPk: isMultiRow.value + ? t("grid.copyRowsInsertWithoutPrimaryKeys", { count }) + : t("grid.copyRowInsertWithoutPrimaryKeys"), + update: isMultiRow.value ? t("grid.copyRowsUpdate", { count }) : t("grid.copyRowUpdate"), + }; +} + +function filterSubmenu(): ContextMenuItem { + return { + label: t("grid.filter"), + icon: Filter, + children: [ + { label: t("grid.filterByValue"), action: () => applyContextFilter("equals") }, + { label: t("grid.filterExcludeValue"), action: () => applyContextFilter("not-equals") }, + { label: t("grid.filterLike"), action: () => applyContextFilter("like") }, + { label: t("grid.filterNotLike"), action: () => applyContextFilter("not-like") }, + { label: t("grid.filterLessThan"), action: () => applyContextFilter("less-than") }, + { label: t("grid.filterGreaterThan"), action: () => applyContextFilter("greater-than") }, + { label: "", separator: true }, + { label: t("grid.filterIsNull"), action: () => applyContextFilter("is-null") }, + { label: t("grid.filterIsNotNull"), action: () => applyContextFilter("is-not-null") }, + { label: "", separator: true }, + { label: t("grid.clearFilter"), action: clearContextFilter }, + ], + }; +} + +function copySubmenu(): ContextMenuItem { + const labels = copyRowLabels(); + const items: ContextMenuItem[] = []; + if (contextColumn.value) { + items.push({ label: t("grid.copyCell"), action: copyCell }); + } + items.push({ label: labels.row, action: copyRow }); + items.push({ label: labels.insert, action: copyRowAsInsert, disabled: !canCopyPreparedInsert(false) }); + if (canCopyRowAsInsertWithoutPrimaryKeys.value) { + items.push({ + label: labels.insertNoPk, + action: copyRowAsInsertWithoutPrimaryKeys, + disabled: !canCopyPreparedInsert(true), + }); + } + if (canCopyRowAsUpdate.value) { + items.push({ label: labels.update, action: copyRowAsUpdate, disabled: !canCopyPreparedUpdate() }); + } + items.push({ label: t("grid.copyAll"), action: copyAll }); + return { label: t("grid.copy"), icon: Copy, children: items }; +} + +function selectionSubmenu(): ContextMenuItem { + return { + label: t("grid.selection"), + icon: SquareDashed, + children: [ + { label: t("grid.copySelectionTsv"), action: copySelectionTsv }, + { label: t("grid.copySelectionCsv"), action: copySelectionCsv }, + { label: t("grid.copySelectionJson"), action: copySelectionJson }, + { label: t("grid.copySelectionSql"), action: copySelectionSqlInList }, + { label: "", separator: true }, + { label: t("grid.clearSelection"), action: clearCellSelection }, + ], + }; +} + +function exportSubmenu(): ContextMenuItem { + const items: ContextMenuItem[] = [ + { label: t("grid.exportCsv"), action: exportCsv }, + { label: t("grid.exportXlsx"), action: exportXlsx }, + { label: t("grid.exportJson"), action: exportJson }, + { label: t("grid.exportMarkdown"), action: exportMarkdown }, + ]; + if (isMultiRow.value) { + items.push( + { label: "", separator: true }, + { label: t("grid.exportSelectedRowsCsv"), action: exportSelectedRowsCsv }, + { label: t("grid.exportSelectedRowsXlsx"), action: exportSelectedRowsXlsx }, + { label: t("grid.exportSelectedRowsJson"), action: exportSelectedRowsJson }, + { label: t("grid.exportSelectedRowsMarkdown"), action: exportSelectedRowsMarkdown }, + ); + } + return { label: t("grid.export"), icon: FileDown, children: items }; +} + +const gridContextMenuItems = computed(() => { + const items: ContextMenuItem[] = []; + + // 1. Copy column name + if (contextHeaderColumn.value) { + items.push({ label: t("grid.copyColumnName"), action: copyHeaderColumn, icon: Copy }); + } + + // 2. Column sort & filter + if (contextColumn.value) { + items.push( + { label: t("grid.sortAscending"), action: () => applyContextSort("asc"), icon: ArrowUp }, + { label: t("grid.sortDescending"), action: () => applyContextSort("desc"), icon: ArrowDown }, + ); + if (sortCol.value) { + items.push({ label: t("grid.clearSort"), action: () => applyContextSort(null), icon: ArrowUpDown }); + } + if (canUseWhereSearch.value) { + items.push({ label: "", separator: true }); + items.push(filterSubmenu()); + } + items.push({ label: "", separator: true }); + } + + // 3. Copy submenu + if (!contextHeaderColumn.value) { + items.push(copySubmenu()); + } + + // 4. Transpose + if (contextCell.value) { + items.push({ label: t("grid.transpose"), action: openContextTranspose, icon: Rows3 }); + } + + // 5. Selection submenu + if (hasCellSelection.value) { + items.push(selectionSubmenu()); + } + + // 6. Row actions + if (props.editable && contextRowItem.value) { + const labels = rowActionLabels(); + items.push({ label: "", separator: true }); + items.push({ + label: labels.clone, + action: () => (isMultiRow.value ? cloneRows(affectedRowIds()) : cloneRow(contextRowItem.value!.id)), + icon: CopyPlus, + }); + if (contextRowItem.value.isDeleted) { + items.push({ + label: labels.restore, + action: () => (isMultiRow.value ? restoreRows(affectedRowIds()) : restoreRow(contextRowItem.value!.id)), + icon: Undo2, + }); + } else if (canDeleteRowItem(contextRowItem.value)) { + items.push({ + label: labels.delete, + action: () => + isMultiRow.value ? requestDeleteRows(affectedRowIds()) : requestDeleteRow(contextRowItem.value!.id), + icon: Trash2, + variant: "destructive" as const, + }); + } + items.push({ label: "", separator: true }); + } + + // 7. Export submenu + items.push(exportSubmenu()); + + return items; +});