From 3b457c13ec7b34758b2ac8ec33ed815c11e94bfe Mon Sep 17 00:00:00 2001 From: zipg Date: Mon, 29 Jun 2026 12:26:43 +0800 Subject: [PATCH] feat(structure): add DDL edit entry and drag reorder --- apps/desktop/src/components/grid/DataGrid.vue | 63 ++++++++- .../structure/TableStructureEditor.vue | 130 ++++++++++++++---- apps/desktop/src/i18n/locales/en.ts | 1 + apps/desktop/src/i18n/locales/zh-CN.ts | 1 + 4 files changed, 166 insertions(+), 29 deletions(-) diff --git a/apps/desktop/src/components/grid/DataGrid.vue b/apps/desktop/src/components/grid/DataGrid.vue index 1060a42fa..e0d4f901c 100644 --- a/apps/desktop/src/components/grid/DataGrid.vue +++ b/apps/desktop/src/components/grid/DataGrid.vue @@ -64,6 +64,7 @@ import { TableProperties, Database, Columns3, + PencilRuler, } from "@lucide/vue"; import { Button } from "@/components/ui/button"; import QueryLoadingState from "@/components/common/QueryLoadingState.vue"; @@ -144,9 +145,11 @@ import { useSqlHighlighter } from "@/composables/useSqlHighlighter"; import { useCellDetailEditor, type UseCellDetailEditorReturn } from "@/composables/useCellDetailEditor"; import { useTheme } from "@/composables/useTheme"; import { useConnectionStore } from "@/stores/connectionStore"; +import { useQueryStore } from "@/stores/queryStore"; import { useSettingsStore } from "@/stores/settingsStore"; import type { DataGridSortDirection, DataGridSortMode } from "@/lib/dataGridSort"; import { getTableMetadataCapabilities } from "@/lib/tableMetadataCapabilities"; +import { supportsTableStructureEditing } from "@/lib/databaseCapabilities"; import { forgetDataGridConditionHistory, loadDataGridConditionHistory, rememberDataGridConditionHistory } from "@/lib/dataGridConditionHistory"; import { caretPositionInsideInsertedSqlSingleQuotes, insertedSqlSingleQuoteAtCaret } from "@/lib/sqlQuoteCaret"; import { effectiveDatabaseTypeForConnection } from "@/lib/jdbcDialect"; @@ -159,6 +162,7 @@ const FORMATTED_JSON_EDIT_WARNING_STORAGE_KEY = "dbx-cell-detail-formatted-json- const { t } = useI18n(); const slots = useSlots(); const connectionStore = useConnectionStore(); +const queryStore = useQueryStore(); const settingsStore = useSettingsStore(); const { isDark } = useTheme(); const { toast } = useToast(); @@ -6566,6 +6570,7 @@ function toggleCellDetailPanelLayout() { } const tableMetadataCapabilities = computed(() => getTableMetadataCapabilities(props.databaseType)); +const canOpenTableStructureEditor = computed(() => !!props.connectionId && !!props.database && !!props.tableMeta?.tableName && supportsTableStructureEditing(resolvedDatabaseType.value)); const tableInfoTabs = computed(() => { const tabs: TableInfoTabItem[] = []; if (tableMetadataCapabilities.value.columns) { @@ -6706,6 +6711,11 @@ function copyDdl() { copyText(ddlContent.value); } +function openTableStructureEditor() { + if (!props.connectionId || !props.database || !props.tableMeta?.tableName || !canOpenTableStructureEditor.value) return; + queryStore.openTableStructure(props.connectionId, props.database, props.tableMeta.schema, props.tableMeta.tableName); +} + function toggleDdlWrap() { ddlWrap.value = !ddlWrap.value; } @@ -8422,15 +8432,21 @@ const gridContextMenuItems = computed(() => { -
+
{{ tableMeta?.tableName }} - +
+ + +
@@ -9662,6 +9678,43 @@ const gridContextMenuItems = computed(() => { transition: none; } +.table-info-drawer { + container-type: inline-size; +} + +.table-info-ddl-action-button { + gap: 0.25rem; + max-width: 8rem; + overflow: hidden; + transition: + max-width 180ms ease, + padding-inline 180ms ease; +} + +.table-info-ddl-action-label { + min-width: 0; + max-width: 6rem; + overflow: hidden; + white-space: nowrap; + opacity: 1; + transition: + max-width 180ms ease, + opacity 120ms ease; +} + +@container (max-width: 360px) { + .table-info-ddl-action-button { + width: 1.5rem; + max-width: 1.5rem; + padding-inline: 0; + } + + .table-info-ddl-action-label { + max-width: 0; + opacity: 0; + } +} + .detail-drawer-resizing { transition: none; } diff --git a/apps/desktop/src/components/structure/TableStructureEditor.vue b/apps/desktop/src/components/structure/TableStructureEditor.vue index 92478cc42..8d6ffc392 100644 --- a/apps/desktop/src/components/structure/TableStructureEditor.vue +++ b/apps/desktop/src/components/structure/TableStructureEditor.vue @@ -6,7 +6,7 @@ import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Badge } from "@/components/ui/badge"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; -import { AlertTriangle, Check, ChevronDown, ChevronUp, Copy, Database, Info, KeyRound, Loader2, Maximize2, Plus, RefreshCw, Save, Settings, SlidersHorizontal, Trash2, X } from "@lucide/vue"; +import { AlertTriangle, Check, ChevronDown, Copy, Database, Info, KeyRound, ListChevronsUpDown, Loader2, Maximize2, Plus, RefreshCw, Save, Settings, SlidersHorizontal, Trash2, X } from "@lucide/vue"; import { DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"; import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; @@ -966,30 +966,105 @@ function removeNewColumn(column: EditableStructureColumn) { columns.value = columns.value.filter((item) => item.id !== column.id); } -function canMoveColumn(index: number, direction: -1 | 1): boolean { +type ColumnDragState = { + columnId: string; + sourceIndex: number; + insertionIndex: number; +}; + +const columnDragState = ref(null); + +function canDragColumn(index: number): boolean { if (loading.value || saving.value) return false; - if (!Number.isInteger(index) || (direction !== -1 && direction !== 1)) return false; - const targetIndex = index + direction; - if (targetIndex < 0 || targetIndex >= columns.value.length) return false; - if (columns.value[index]?.markedForDrop || columns.value[targetIndex]?.markedForDrop) return false; - // Draft columns can always swap order among themselves — - // ADD COLUMN statement order determines their final physical placement. - if (!columns.value[index]?.original && !columns.value[targetIndex]?.original) return true; - return canShowColumnMoveControls.value; + if (!Number.isInteger(index) || index < 0 || index >= columns.value.length) return false; + const column = columns.value[index]; + if (!column || column.markedForDrop) return false; + if (!column.original) return true; + return canShowColumnDragControls.value; } -const canShowColumnMoveControls = computed(() => isCreateMode.value || structureCapabilities.value.reorderColumn); +function canDropColumnAt(sourceIndex: number, insertionIndex: number): boolean { + if (!canDragColumn(sourceIndex)) return false; + if (!Number.isInteger(insertionIndex) || insertionIndex < 0 || insertionIndex > columns.value.length) return false; + if (insertionIndex === sourceIndex || insertionIndex === sourceIndex + 1) return false; + const sourceColumn = columns.value[sourceIndex]; + if (!sourceColumn) return false; + const crossedColumns = insertionIndex < sourceIndex ? columns.value.slice(insertionIndex, sourceIndex) : columns.value.slice(sourceIndex + 1, insertionIndex); + if (crossedColumns.some((column) => column.markedForDrop)) return false; + if (canShowColumnDragControls.value) return true; + if (sourceColumn.original) return false; + return crossedColumns.every((column) => !column.original); +} -function moveColumn(index: number, direction: -1 | 1) { - if (!canMoveColumn(index, direction)) return; - const targetIndex = index + direction; +const canShowColumnDragControls = computed(() => isCreateMode.value || structureCapabilities.value.reorderColumn); + +function moveColumnTo(index: number, insertionIndex: number) { + if (!canDropColumnAt(index, insertionIndex)) return; const nextColumns = [...columns.value]; const [column] = nextColumns.splice(index, 1); if (!column) return; - nextColumns.splice(targetIndex, 0, column); + const adjustedInsertionIndex = insertionIndex > index ? insertionIndex - 1 : insertionIndex; + nextColumns.splice(adjustedInsertionIndex, 0, column); columns.value = nextColumns; } +function onColumnDragStart(index: number, event: DragEvent) { + if (!canDragColumn(index)) { + event.preventDefault(); + return; + } + const column = columns.value[index]; + if (!column) return; + columnDragState.value = { + columnId: column.id, + sourceIndex: index, + insertionIndex: index, + }; + if (event.dataTransfer) { + event.dataTransfer.effectAllowed = "move"; + event.dataTransfer.setData("text/plain", column.name || column.id); + } +} + +function onColumnDragOver(index: number, event: DragEvent) { + const state = columnDragState.value; + if (!state || columns.value[index]?.markedForDrop) return; + const insertionIndex = columnDragInsertionIndex(index, event); + if (!canDropColumnAt(state.sourceIndex, insertionIndex)) return; + event.preventDefault(); + state.insertionIndex = insertionIndex; + if (event.dataTransfer) event.dataTransfer.dropEffect = "move"; +} + +function onColumnDrop(index: number, event: DragEvent) { + const state = columnDragState.value; + if (!state) return; + event.preventDefault(); + moveColumnTo(state.sourceIndex, columnDragInsertionIndex(index, event)); + columnDragState.value = null; +} + +function onColumnDragEnd() { + columnDragState.value = null; +} + +function columnRowClass(column: EditableStructureColumn, index: number) { + const dragState = columnDragState.value; + return { + "bg-destructive/5 opacity-60": column.markedForDrop, + "opacity-55": dragState?.columnId === column.id, + "bg-primary/5 shadow-[inset_0_2px_0_var(--primary)]": dragState && canDropColumnAt(dragState.sourceIndex, index) && dragState.insertionIndex === index, + "bg-primary/5 shadow-[inset_0_-2px_0_var(--primary)]": dragState && canDropColumnAt(dragState.sourceIndex, index + 1) && dragState.insertionIndex === index + 1, + }; +} + +function columnDragInsertionIndex(index: number, event: DragEvent): number { + const target = event.currentTarget; + if (!(target instanceof HTMLElement)) return index; + const rect = target.getBoundingClientRect(); + return event.clientY > rect.top + rect.height / 2 ? index + 1 : index; +} + function toggleDropColumn(column: EditableStructureColumn) { if (!canDropColumn(column)) return; column.markedForDrop = !column.markedForDrop; @@ -1562,7 +1637,7 @@ watch(activeTab, (tab) => { - +
{{ index + 1 }} @@ -1788,14 +1863,21 @@ watch(activeTab, (tab) => {
- +