feat(structure): add DDL edit entry and drag reorder

This commit is contained in:
zipg authored and GitHub committed 2026-06-29 12:26:43 +08:00
1 parent a65a5881d5
commit 3b457c13ec
4 files changed
+166 -29

No files matched your search

@@ -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"