feat(structure): add DDL edit entry and drag reorder
This commit is contained in:
parent
a65a5881d5
commit
3b457c13ec
|
|
@ -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<ContextMenuItem[]>(() => {
|
|||
</template>
|
||||
</div>
|
||||
<!-- Table Info Drawer -->
|
||||
<div v-if="showTableInfo" class="relative col-start-2 row-start-1 border-l flex flex-col bg-background min-w-0" :class="[{ 'row-span-2': cellDetailPanelIsBottom }, { 'ddl-drawer-resizing': isResizingDdl }]" :style="ddlDrawerStyle" @contextmenu="onDrawerContextMenu">
|
||||
<div v-if="showTableInfo" class="table-info-drawer relative col-start-2 row-start-1 border-l flex flex-col bg-background min-w-0" :class="[{ 'row-span-2': cellDetailPanelIsBottom }, { 'ddl-drawer-resizing': isResizingDdl }]" :style="ddlDrawerStyle" @contextmenu="onDrawerContextMenu">
|
||||
<div class="absolute left-0 top-0 bottom-0 z-20 w-1.5 -translate-x-1/2 cursor-col-resize hover:bg-primary/30" @mousedown.prevent="onDdlResizeStart" />
|
||||
<div class="flex items-center gap-2 px-3 py-1.5 border-b shrink-0 bg-muted/20 h-9">
|
||||
<TableProperties class="w-3.5 h-3.5 text-muted-foreground" />
|
||||
<span class="text-xs font-medium flex-1 min-w-0 truncate">{{ tableMeta?.tableName }}</span>
|
||||
<Button v-if="activeTableInfoTab === 'ddl'" variant="ghost" size="sm" class="h-6 px-2 text-xs" :title="t('grid.copyDdl')" :aria-label="t('grid.copyDdl')" @click="copyDdl">
|
||||
<Copy class="w-3 h-3" />
|
||||
<span>{{ t("grid.copyDdl") }}</span>
|
||||
</Button>
|
||||
<div v-if="activeTableInfoTab === 'ddl'" class="table-info-ddl-actions flex min-w-0 shrink-0 items-center gap-1">
|
||||
<Button v-if="canOpenTableStructureEditor" variant="ghost" size="sm" class="table-info-ddl-action-button h-6 px-2 text-xs" :title="t('contextMenu.editStructure')" :aria-label="t('contextMenu.editStructure')" @click="openTableStructureEditor">
|
||||
<PencilRuler class="w-3 h-3" />
|
||||
<span class="table-info-ddl-action-label">{{ t("contextMenu.editStructure") }}</span>
|
||||
</Button>
|
||||
<Button variant="ghost" size="sm" class="table-info-ddl-action-button h-6 px-2 text-xs" :title="t('grid.copyDdl')" :aria-label="t('grid.copyDdl')" @click="copyDdl">
|
||||
<Copy class="w-3 h-3" />
|
||||
<span class="table-info-ddl-action-label">{{ t("grid.copyDdl") }}</span>
|
||||
</Button>
|
||||
</div>
|
||||
<Button v-if="activeTableInfoTab === 'ddl'" variant="ghost" size="icon" class="h-6 w-6" :class="{ 'bg-accent': ddlWrap }" @click="toggleDdlWrap">
|
||||
<WrapText class="w-3 h-3" />
|
||||
</Button>
|
||||
|
|
@ -9662,6 +9678,43 @@ const gridContextMenuItems = computed<ContextMenuItem[]>(() => {
|
|||
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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<ColumnDragState | null>(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) => {
|
|||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(column, index) in columns" :key="column.id" :class="column.markedForDrop ? 'bg-destructive/5 opacity-60' : ''" :data-new-column-row="!column.original ? 'true' : undefined">
|
||||
<tr v-for="(column, index) in columns" :key="column.id" :class="columnRowClass(column, index)" :data-new-column-row="!column.original ? 'true' : undefined" @dragover="onColumnDragOver(index, $event)" @drop="onColumnDrop(index, $event)">
|
||||
<td :class="[structureCellClass, 'text-muted-foreground']">
|
||||
<div class="flex items-center gap-1">
|
||||
<span>{{ index + 1 }}</span>
|
||||
|
|
@ -1788,14 +1863,21 @@ watch(activeTab, (tab) => {
|
|||
</td>
|
||||
<td :class="structureLastCellClass">
|
||||
<div class="flex min-w-0 items-center justify-start gap-0.5 overflow-hidden">
|
||||
<template v-if="canShowColumnMoveControls || !column.original">
|
||||
<Button type="button" variant="ghost" size="icon" :class="structureActionButtonClass" :disabled="!canMoveColumn(index, -1)" :title="t('structureEditor.moveColumnUp')" :aria-label="t('structureEditor.moveColumnUp')" @click.stop.prevent="moveColumn(index, -1)">
|
||||
<ChevronUp :class="structureIconClass" />
|
||||
</Button>
|
||||
<Button type="button" variant="ghost" size="icon" :class="structureActionButtonClass" :disabled="!canMoveColumn(index, 1)" :title="t('structureEditor.moveColumnDown')" :aria-label="t('structureEditor.moveColumnDown')" @click.stop.prevent="moveColumn(index, 1)">
|
||||
<ChevronDown :class="structureIconClass" />
|
||||
</Button>
|
||||
</template>
|
||||
<Button
|
||||
v-if="canShowColumnDragControls || !column.original"
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
:class="[structureActionButtonClass, canDragColumn(index) ? 'cursor-grab active:cursor-grabbing' : 'cursor-not-allowed']"
|
||||
:disabled="!canDragColumn(index)"
|
||||
:title="t('structureEditor.dragColumn')"
|
||||
:aria-label="t('structureEditor.dragColumn')"
|
||||
draggable="true"
|
||||
@dragstart="onColumnDragStart(index, $event)"
|
||||
@dragend="onColumnDragEnd"
|
||||
>
|
||||
<ListChevronsUpDown :class="structureIconClass" />
|
||||
</Button>
|
||||
<Button
|
||||
v-if="column.original"
|
||||
variant="ghost"
|
||||
|
|
|
|||
|
|
@ -1554,6 +1554,7 @@ export default {
|
|||
remove: "Remove",
|
||||
moveColumnUp: "Move column up",
|
||||
moveColumnDown: "Move column down",
|
||||
dragColumn: "Drag to reorder column",
|
||||
yes: "Yes",
|
||||
no: "No",
|
||||
emptyReadonly: "No records",
|
||||
|
|
|
|||
|
|
@ -1557,6 +1557,7 @@ export default withEnglishFallback({
|
|||
remove: "移除",
|
||||
moveColumnUp: "上移字段",
|
||||
moveColumnDown: "下移字段",
|
||||
dragColumn: "拖拽调整字段顺序",
|
||||
yes: "是",
|
||||
no: "否",
|
||||
emptyReadonly: "暂无记录",
|
||||
|
|
|
|||
Loading…
Reference in New Issue