feat(desktop): improve table structure editor

This commit is contained in:
t8y2 committed 2026-06-01 23:52:53 +08:00
1 parent ad33db0886
commit 130a33808e
5 files changed
+177 -56

No files matched your search

@@ -1,5 +1,5 @@
<script setup lang="ts">
import { computed, nextTick, onMounted, ref, watch } from "vue";
import { computed, nextTick, onActivated, onBeforeUnmount, onDeactivated, onMounted, ref, watch } from "vue";
import { uuid } from "@/lib/utils";
import { useI18n } from "vue-i18n";
import { Button } from "@/components/ui/button";
@@ -97,6 +97,12 @@ const warnings = ref<string[]>([]);
const foreignKeys = ref<ForeignKeyInfo[]>([]);
const triggers = ref<TriggerInfo[]>([]);
function isPlainModShortcut(event: KeyboardEvent, key: string): boolean {
if (event.isComposing || event.altKey || event.shiftKey) return false;
if (!event.metaKey && !event.ctrlKey) return false;
return event.key.toLowerCase() === key;
}
const structureDensityValues: StructureEditorDensity[] = ["compact", "standard", "comfortable"];
const structureDensityMetrics: Record<
StructureEditorDensity,
@@ -118,52 +124,52 @@ const structureDensityMetrics: Record<
}
> = {
compact: {
columns: [24, 104, 120, 72, 54, 46, 92, 104, 104, 96],
indexes: [104, 156, 54, 78, 108, 128, 104, 62],
columns: [28, 120, 136, 82, 60, 52, 108, 124, 128, 108],
indexes: [120, 180, 60, 88, 124, 144, 120, 70],
minColumnWidth: 24,
minIndexColumnWidth: 48,
fontSize: 10,
shellPadding: 8,
cellPaddingX: 4,
cellPaddingY: 3,
headerPaddingY: 4,
controlHeight: 20,
controlPaddingX: 6,
iconSize: 12,
checkboxSize: 12,
lineHeight: 1.25,
},
standard: {
columns: [28, 128, 144, 96, 64, 56, 112, 128, 128, 128],
indexes: [132, 200, 64, 96, 132, 160, 132, 76],
minColumnWidth: 28,
minIndexColumnWidth: 60,
fontSize: 11,
shellPadding: 12,
shellPadding: 10,
cellPaddingX: 6,
cellPaddingY: 4,
headerPaddingY: 6,
headerPaddingY: 5,
controlHeight: 24,
controlPaddingX: 8,
iconSize: 14,
checkboxSize: 14,
checkboxSize: 13,
lineHeight: 1.35,
},
comfortable: {
columns: [32, 152, 168, 104, 76, 68, 136, 168, 152, 132],
indexes: [156, 232, 76, 112, 156, 192, 156, 92],
minColumnWidth: 32,
minIndexColumnWidth: 64,
standard: {
columns: [32, 144, 160, 104, 72, 64, 128, 152, 152, 136],
indexes: [148, 224, 72, 108, 148, 180, 148, 84],
minColumnWidth: 28,
minIndexColumnWidth: 60,
fontSize: 12,
shellPadding: 14,
shellPadding: 12,
cellPaddingX: 8,
cellPaddingY: 6,
headerPaddingY: 8,
cellPaddingY: 5,
headerPaddingY: 7,
controlHeight: 28,
controlPaddingX: 10,
iconSize: 15,
checkboxSize: 15,
lineHeight: 1.45,
checkboxSize: 14,
lineHeight: 1.4,
},
comfortable: {
columns: [36, 168, 188, 116, 84, 76, 152, 188, 176, 148],
indexes: [176, 260, 84, 124, 176, 216, 176, 104],
minColumnWidth: 32,
minIndexColumnWidth: 64,
fontSize: 13,
shellPadding: 16,
cellPaddingX: 10,
cellPaddingY: 7,
headerPaddingY: 9,
controlHeight: 32,
controlPaddingX: 12,
iconSize: 16,
checkboxSize: 16,
lineHeight: 1.5,
},
};
@@ -346,6 +352,7 @@ const targetLabel = computed(() =>
);
let sqlPreviewRequestId = 0;
let keydownListenerRegistered = false;
async function refreshSqlPreview() {
const requestId = ++sqlPreviewRequestId;
@@ -519,6 +526,7 @@ function canDropColumn(column: EditableStructureColumn): boolean {
function addIndex() {
if (!structureCapabilities.value.createIndex) return;
activeTab.value = "indexes";
indexes.value.push({
id: `new:${uuid()}`,
name: "",
@@ -531,6 +539,14 @@ function addIndex() {
comment: "",
markedForDrop: false,
});
void nextTick(() => {
const indexRows = rootRef.value?.querySelectorAll<HTMLElement>('[data-new-index-row="true"]');
const row = indexRows?.[indexRows.length - 1];
const input = row?.querySelector<HTMLInputElement>("[data-index-name-input]");
row?.scrollIntoView({ block: "nearest" });
input?.focus();
input?.select();
});
}
const availableColumnNames = computed(() =>
@@ -611,11 +627,56 @@ async function applyChanges() {
}
}
function addItemForActiveTab(): boolean {
if (activeTab.value === "columns" && structureCapabilities.value.addColumn) {
void addColumn();
return true;
}
if (activeTab.value === "indexes" && structureCapabilities.value.createIndex) {
addIndex();
return true;
}
return false;
}
function onStructureEditorKeydown(event: KeyboardEvent) {
if (isPlainModShortcut(event, "s")) {
event.preventDefault();
event.stopPropagation();
void applyChanges();
return;
}
if (isPlainModShortcut(event, "n")) {
event.preventDefault();
event.stopPropagation();
addItemForActiveTab();
}
}
function registerStructureEditorShortcuts() {
if (keydownListenerRegistered) return;
keydownListenerRegistered = true;
window.addEventListener("keydown", onStructureEditorKeydown);
}
function unregisterStructureEditorShortcuts() {
if (!keydownListenerRegistered) return;
keydownListenerRegistered = false;
window.removeEventListener("keydown", onStructureEditorKeydown);
}
onMounted(() => {
resetState();
registerStructureEditorShortcuts();
void loadStructure();
});
onActivated(registerStructureEditorShortcuts);
onDeactivated(unregisterStructureEditorShortcuts);
onBeforeUnmount(() => {
unregisterStructureEditorShortcuts();
});
watch(
[isCreateMode, databaseType, () => props.schema, () => props.tableName, newTableName, tableComment, columns, indexes],
() => {
@@ -684,8 +745,8 @@ watch(
{{ t("common.loading") }}
</div>
<div v-else class="grid min-h-0 flex-1 grid-cols-[minmax(0,1fr)_300px] gap-2 overflow-hidden">
<div class="min-h-0 min-w-0 overflow-hidden rounded-md border">
<div v-else class="flex min-h-0 flex-1 flex-col gap-2 overflow-hidden">
<div class="min-h-0 min-w-0 flex-1 overflow-hidden rounded-md border">
<Tabs v-model="activeTab" class="flex h-full min-h-0 flex-col">
<div class="flex shrink-0 items-center justify-between gap-2 border-b px-2 py-[var(--structure-header-py)]">
<TabsList>
@@ -911,16 +972,15 @@ watch(
</template>
<!-- PostgreSQL: IDENTITY -->
<template v-else-if="databaseType === 'postgres'">
<select
:value="column.extra.identity?.generation ?? ''"
class="h-[var(--structure-control-height)] rounded border bg-background px-[var(--structure-control-px)] text-[length:var(--structure-font-size)]"
@change="
(e) => {
const v = (e.target as HTMLSelectElement).value;
if (v) {
<Select
:model-value="column.extra.identity?.generation ?? 'none'"
@update:model-value="
(value: any) => {
const generation = String(value ?? '');
if (generation && generation !== 'none') {
column.extra.identity = {
...column.extra.identity,
generation: v as 'BY DEFAULT' | 'ALWAYS',
generation: generation as 'BY DEFAULT' | 'ALWAYS',
};
} else {
column.extra.identity = undefined;
@@ -928,10 +988,17 @@ watch(
}
"
>
<option value="">{{ t("structureEditor.no") }}</option>
<option value="BY DEFAULT">BY DEFAULT</option>
<option value="ALWAYS">ALWAYS</option>
</select>
<SelectTrigger
class="h-[var(--structure-control-height)] w-28 rounded-md px-[var(--structure-control-px)] text-[length:var(--structure-font-size)]"
>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="none">{{ t("structureEditor.no") }}</SelectItem>
<SelectItem value="BY DEFAULT">BY DEFAULT</SelectItem>
<SelectItem value="ALWAYS">ALWAYS</SelectItem>
</SelectContent>
</Select>
<template v-if="column.extra.identity?.generation">
<Input
:model-value="column.extra.identity.seed?.toString() ?? ''"
@@ -1081,9 +1148,15 @@ watch(
v-for="index in indexes"
:key="index.id"
:class="index.markedForDrop ? 'bg-destructive/5 opacity-60' : ''"
:data-new-index-row="!index.original ? 'true' : undefined"
>
<td :class="structureCellClass">
<Input v-model="index.name" :class="structureControlClass" :disabled="!canEditIndexDraft(index)" />
<Input
v-model="index.name"
:class="structureControlClass"
:disabled="!canEditIndexDraft(index)"
data-index-name-input
/>
</td>
<td :class="[structureCellClass, 'overflow-hidden']">
<DropdownMenu v-if="canEditIndexDraft(index)">
@@ -1282,7 +1355,7 @@ watch(
</Tabs>
</div>
<div class="flex min-h-0 min-w-0 flex-col overflow-hidden rounded-md border">
<div class="flex h-[28%] min-h-40 min-w-0 max-h-64 shrink-0 flex-col overflow-hidden rounded-md border">
<div
class="flex shrink-0 items-center justify-between border-b px-[var(--structure-cell-px)] py-[var(--structure-header-py)] text-[length:var(--structure-font-size)] font-medium"
>
@@ -1302,7 +1375,7 @@ watch(
<span v-else>{{ pendingStatements.length }}</span>
</Badge>
</div>
<div class="min-h-0 flex-1 overflow-auto p-2">
<div class="min-h-0 flex-1 overflow-auto p-2.5">
<div v-if="warnings.length" class="mb-2 space-y-1">
<div
v-for="warning in warnings"
@@ -1315,7 +1388,7 @@ watch(
</div>
<pre
v-if="pendingStatements.length"
class="whitespace-pre-wrap break-words rounded-md bg-muted/40 p-[var(--structure-cell-px)] font-mono text-[length:var(--structure-font-size)] leading-4"
class="whitespace-pre-wrap break-words rounded-md bg-muted/40 p-2.5 font-mono text-[calc(var(--structure-font-size)+1px)] leading-5"
v-html="highlightedSql"
/>
<div