feat(structure): add DDL edit entry and drag reorder
This commit is contained in:
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"
|
||||
|
||||
Reference in new issue
Block a user