fix(structure): compact editor layout

This commit is contained in:
t8y2 2026-05-22 18:53:50 +08:00
parent ebf8f674c0
commit a26a762b8c
2 changed files with 110 additions and 74 deletions

View File

@ -74,7 +74,7 @@ const indexes = ref<EditableStructureIndex[]>([]);
const foreignKeys = ref<ForeignKeyInfo[]>([]);
const triggers = ref<TriggerInfo[]>([]);
const indexColWidths = ref([160, 240, 80, 112, 160, 192, 160, 96]);
const indexColWidths = ref([132, 200, 64, 96, 132, 160, 132, 76]);
const resizing = ref<{ col: number; startX: number; startW: number } | null>(null);
function onIndexColResize(e: MouseEvent, col: number) {
@ -379,7 +379,11 @@ watch(
<template>
<Dialog v-model:open="open">
<DialogScrollContent class="sm:max-w-[1180px]" :trap-focus="false" @interact-outside.prevent>
<DialogScrollContent
class="max-h-[calc(100vh-32px)] gap-3 p-3 sm:max-w-[1180px]"
:trap-focus="false"
@interact-outside.prevent
>
<DialogHeader>
<DialogTitle class="flex items-center gap-2">
<TableProperties class="h-4 w-4" />
@ -387,8 +391,8 @@ watch(
</DialogTitle>
</DialogHeader>
<div class="space-y-3 py-2">
<div class="flex items-center gap-2 rounded-md border bg-muted/20 px-3 py-2 text-xs">
<div class="space-y-2 py-1 text-[11px]" data-structure-density="compact">
<div class="flex items-center gap-2 rounded-md border bg-muted/20 px-2.5 py-1.5 text-[11px]">
<Database class="h-3.5 w-3.5 text-muted-foreground" />
<span class="min-w-0 flex-1 truncate font-medium">{{ targetLabel || t("editor.noDatabase") }}</span>
<Badge variant="outline">{{ connection?.driver_label || databaseType }}</Badge>
@ -396,7 +400,7 @@ watch(
v-if="!isCreateMode"
variant="ghost"
size="sm"
class="h-7 gap-1"
class="h-6 gap-1 px-2 text-[11px]"
:disabled="loading || saving"
@click="loadStructure"
>
@ -405,12 +409,14 @@ watch(
</Button>
</div>
<div v-if="isCreateMode" class="flex items-center gap-3">
<label class="text-xs font-medium text-muted-foreground shrink-0">{{ t("structureEditor.tableName") }}</label>
<div v-if="isCreateMode" class="flex items-center gap-2">
<label class="shrink-0 text-[11px] font-medium text-muted-foreground">{{
t("structureEditor.tableName")
}}</label>
<Input
v-model="newTableName"
:placeholder="t('contextMenu.duplicateNamePlaceholder')"
class="h-7 text-xs max-w-[240px]"
class="h-6 max-w-[220px] text-[11px]"
/>
</div>
@ -419,10 +425,10 @@ watch(
{{ t("common.loading") }}
</div>
<div v-else class="grid min-h-[520px] grid-cols-[minmax(0,1fr)_360px] gap-3">
<div v-else class="grid min-h-[520px] grid-cols-[minmax(0,1fr)_300px] gap-2">
<div class="min-w-0 rounded-md border">
<Tabs v-model="activeTab" class="flex h-full flex-col">
<div class="flex items-center justify-between border-b px-3 py-2">
<div class="flex items-center justify-between border-b px-2 py-1.5">
<TabsList>
<TabsTrigger value="columns">{{ t("structureEditor.columns") }}</TabsTrigger>
<TabsTrigger value="indexes">{{ t("structureEditor.indexes") }}</TabsTrigger>
@ -432,7 +438,7 @@ watch(
<Button
v-if="activeTab === 'columns'"
size="sm"
class="h-7 gap-1"
class="h-6 gap-1 px-2 text-[11px]"
:disabled="!structureCapabilities.addColumn"
@click="addColumn"
>
@ -442,7 +448,7 @@ watch(
<Button
v-if="activeTab === 'indexes'"
size="sm"
class="h-7 gap-1"
class="h-6 gap-1 px-2 text-[11px]"
:disabled="!structureCapabilities.createIndex"
@click="addIndex"
>
@ -452,26 +458,26 @@ watch(
</div>
<TabsContent value="columns" class="m-0 min-h-0 flex-1 overflow-auto p-0">
<table class="min-w-full border-separate border-spacing-0 text-xs">
<table class="min-w-full border-separate border-spacing-0 text-[11px]">
<thead class="sticky top-0 z-10 bg-background">
<tr>
<th class="w-8 border-b border-r px-2 py-2 text-left">#</th>
<th class="min-w-36 border-b border-r px-2 py-2 text-left">
<th class="w-7 border-b border-r px-1.5 py-1.5 text-left">#</th>
<th class="min-w-32 border-b border-r px-1.5 py-1.5 text-left">
{{ t("structureEditor.columnName") }}
</th>
<th class="min-w-40 border-b border-r px-2 py-2 text-left">
<th class="min-w-36 border-b border-r px-1.5 py-1.5 text-left">
{{ t("structureEditor.dataType") }}
</th>
<th class="w-20 whitespace-nowrap border-b border-r px-2 py-2 text-left">
<th class="w-16 whitespace-nowrap border-b border-r px-1.5 py-1.5 text-left">
{{ t("structureEditor.nullable") }}
</th>
<th class="min-w-32 border-b border-r px-2 py-2 text-left">
<th class="min-w-28 border-b border-r px-1.5 py-1.5 text-left">
{{ t("structureEditor.defaultValue") }}
</th>
<th class="min-w-36 border-b border-r px-2 py-2 text-left">
<th class="min-w-32 border-b border-r px-1.5 py-1.5 text-left">
{{ t("structureEditor.comment") }}
</th>
<th class="w-40 border-b px-2 py-2 text-left">
<th class="w-32 border-b px-1.5 py-1.5 text-left">
{{ t("structureEditor.actions") }}
</th>
</tr>
@ -482,27 +488,27 @@ watch(
:key="column.id"
:class="column.markedForDrop ? 'bg-destructive/5 opacity-60' : ''"
>
<td class="border-b border-r px-2 py-1.5 text-muted-foreground">
<td class="border-b border-r px-1.5 py-1 text-muted-foreground">
<div class="flex items-center gap-1">
<span>{{ index + 1 }}</span>
<KeyRound v-if="column.isPrimaryKey" class="h-3 w-3 text-amber-500" />
</div>
</td>
<td class="border-b border-r px-2 py-1.5">
<td class="border-b border-r px-1.5 py-1">
<Input
v-model="column.name"
class="h-7 min-w-32 text-xs"
class="h-6 min-w-28 text-[11px]"
:disabled="isColumnNameDisabled(column)"
/>
</td>
<td class="border-b border-r px-2 py-1.5">
<td class="border-b border-r px-1.5 py-1">
<Input
v-model="column.dataType"
class="h-7 min-w-36 font-mono text-xs"
class="h-6 min-w-32 font-mono text-[11px]"
:disabled="isColumnTypeDisabled(column)"
/>
</td>
<td class="border-b border-r px-2 py-1.5">
<td class="border-b border-r px-1.5 py-1">
<label class="flex items-center gap-1.5">
<input
v-model="column.isNullable"
@ -513,18 +519,18 @@ watch(
<span>{{ column.isNullable ? t("structureEditor.yes") : t("structureEditor.no") }}</span>
</label>
</td>
<td class="border-b border-r px-2 py-1.5">
<td class="border-b border-r px-1.5 py-1">
<Input
v-model="column.defaultValue"
class="h-7 min-w-28 font-mono text-xs"
class="h-6 min-w-24 font-mono text-[11px]"
:disabled="isColumnDefaultDisabled(column)"
/>
</td>
<td class="border-b border-r px-2 py-1.5">
<div class="flex min-w-44 items-center gap-1">
<td class="border-b border-r px-1.5 py-1">
<div class="flex min-w-36 items-center gap-1">
<Input
v-model="column.comment"
class="h-7 min-w-0 flex-1 text-xs"
class="h-6 min-w-0 flex-1 text-[11px]"
:disabled="isColumnCommentDisabled(column)"
/>
<Popover>
@ -532,7 +538,7 @@ watch(
<Button
variant="ghost"
size="icon"
class="h-7 w-7 shrink-0"
class="h-6 w-6 shrink-0"
:disabled="isColumnCommentDisabled(column)"
:aria-label="t('structureEditor.editComment')"
:title="t('structureEditor.editComment')"
@ -559,12 +565,12 @@ watch(
</Popover>
</div>
</td>
<td class="border-b px-2 py-1.5">
<td class="border-b px-1.5 py-1">
<div class="flex items-center gap-1">
<Button
variant="ghost"
size="icon"
class="h-7 w-7"
class="h-6 w-6"
:disabled="!canMoveColumn(index, -1)"
:title="t('structureEditor.moveColumnUp')"
:aria-label="t('structureEditor.moveColumnUp')"
@ -575,7 +581,7 @@ watch(
<Button
variant="ghost"
size="icon"
class="h-7 w-7"
class="h-6 w-6"
:disabled="!canMoveColumn(index, 1)"
:title="t('structureEditor.moveColumnDown')"
:aria-label="t('structureEditor.moveColumnDown')"
@ -587,14 +593,20 @@ watch(
v-if="column.original"
variant="ghost"
size="sm"
class="h-7 gap-1"
class="h-6 gap-1 px-1.5 text-[11px]"
:disabled="!canDropColumn(column)"
@click="toggleDropColumn(column)"
>
<Trash2 class="h-3.5 w-3.5" />
{{ column.markedForDrop ? t("structureEditor.restore") : t("structureEditor.drop") }}
</Button>
<Button v-else variant="ghost" size="sm" class="h-7 gap-1" @click="removeNewColumn(column)">
<Button
v-else
variant="ghost"
size="sm"
class="h-6 gap-1 px-1.5 text-[11px]"
@click="removeNewColumn(column)"
>
<X class="h-3.5 w-3.5" />
{{ t("structureEditor.remove") }}
</Button>
@ -606,13 +618,13 @@ watch(
</TabsContent>
<TabsContent value="indexes" class="m-0 min-h-0 flex-1 overflow-auto p-0">
<table class="min-w-full border-separate border-spacing-0 text-xs">
<table class="min-w-full border-separate border-spacing-0 text-[11px]">
<thead class="sticky top-0 z-10 bg-background">
<tr>
<th
v-for="(label, i) in indexColLabels"
:key="i"
class="relative border-b border-r px-2 py-2 text-left"
class="relative border-b border-r px-1.5 py-1.5 text-left"
:style="{
width: indexColWidths[i] + 'px',
minWidth: indexColWidths[i] + 'px',
@ -634,13 +646,13 @@ watch(
:key="index.id"
:class="index.markedForDrop ? 'bg-destructive/5 opacity-60' : ''"
>
<td class="border-b border-r px-2 py-1.5">
<Input v-model="index.name" class="h-7 text-xs" :disabled="!canEditIndexDraft(index)" />
<td class="border-b border-r px-1.5 py-1">
<Input v-model="index.name" class="h-6 text-[11px]" :disabled="!canEditIndexDraft(index)" />
</td>
<td class="overflow-hidden border-b border-r px-2 py-1.5">
<td class="overflow-hidden border-b border-r px-1.5 py-1">
<DropdownMenu v-if="canEditIndexDraft(index)">
<DropdownMenuTrigger as-child>
<Button variant="outline" class="h-7 w-full justify-between font-mono text-xs">
<Button variant="outline" class="h-6 w-full justify-between px-2 font-mono text-[11px]">
<span class="truncate">{{
toColumnNames(index.columns) || t("structureEditor.indexColumnsPlaceholder")
}}</span>
@ -648,7 +660,7 @@ watch(
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent
class="max-h-60 min-w-48 overflow-y-auto"
class="max-h-56 min-w-44 overflow-y-auto"
side="bottom"
:side-offset="2"
:avoid-collisions="false"
@ -657,7 +669,7 @@ watch(
<div class="px-1.5 pb-1 pt-0.5">
<Input
v-model="colSearch"
class="h-6 text-xs"
class="h-6 text-[11px]"
:placeholder="t('grid.search')"
@click.stop
/>
@ -674,11 +686,11 @@ watch(
</DropdownMenuCheckboxItem>
</DropdownMenuContent>
</DropdownMenu>
<span v-else class="font-mono text-xs text-muted-foreground">{{
<span v-else class="font-mono text-[11px] text-muted-foreground">{{
toColumnNames(index.columns)
}}</span>
</td>
<td class="border-b border-r px-2 py-1.5">
<td class="border-b border-r px-1.5 py-1">
<label class="flex items-center gap-1.5">
<input
v-model="index.isUnique"
@ -689,14 +701,14 @@ watch(
<span>{{ index.isUnique ? t("structureEditor.yes") : t("structureEditor.no") }}</span>
</label>
</td>
<td class="border-b border-r px-2 py-1.5">
<td class="border-b border-r px-1.5 py-1">
<Select
v-if="indexTypeOptions.length > 0"
:model-value="index.indexType || 'BTREE'"
:disabled="!canEditIndexDraft(index)"
@update:model-value="(v: any) => (index.indexType = String(v ?? ''))"
>
<SelectTrigger class="h-7 font-mono text-xs">
<SelectTrigger class="h-6 font-mono text-[11px]">
<SelectValue />
</SelectTrigger>
<SelectContent>
@ -706,15 +718,15 @@ watch(
<Input
v-else
v-model="index.indexType"
class="h-7 font-mono text-xs"
class="h-6 font-mono text-[11px]"
placeholder="BTREE"
:disabled="!canEditIndexDraft(index) || !structureCapabilities.indexType"
/>
</td>
<td class="overflow-hidden border-b border-r px-2 py-1.5">
<td class="overflow-hidden border-b border-r px-1.5 py-1">
<DropdownMenu v-if="canEditIndexDraft(index) && structureCapabilities.indexInclude">
<DropdownMenuTrigger as-child>
<Button variant="outline" class="h-7 w-full justify-between font-mono text-xs">
<Button variant="outline" class="h-6 w-full justify-between px-2 font-mono text-[11px]">
<span class="truncate">{{
index.includedColumns.join(", ") || t("structureEditor.includedColumnsPlaceholder")
}}</span>
@ -722,7 +734,7 @@ watch(
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent
class="max-h-60 min-w-48 overflow-y-auto"
class="max-h-56 min-w-44 overflow-y-auto"
side="bottom"
:side-offset="2"
:avoid-collisions="false"
@ -731,7 +743,7 @@ watch(
<div class="px-1.5 pb-1 pt-0.5">
<Input
v-model="colSearch"
class="h-6 text-xs"
class="h-6 text-[11px]"
:placeholder="t('grid.search')"
@click.stop
/>
@ -748,33 +760,45 @@ watch(
</DropdownMenuCheckboxItem>
</DropdownMenuContent>
</DropdownMenu>
<span v-else class="text-muted-foreground text-xs">{{ index.includedColumns.join(", ") }}</span>
<span v-else class="text-[11px] text-muted-foreground">{{
index.includedColumns.join(", ")
}}</span>
</td>
<td class="border-b border-r px-2 py-1.5">
<td class="border-b border-r px-1.5 py-1">
<Input
v-model="index.filter"
class="h-7 font-mono text-xs"
class="h-6 font-mono text-[11px]"
:placeholder="index.original?.filter || ''"
:disabled="!canEditIndexFilter(index)"
/>
</td>
<td class="border-b border-r px-2 py-1.5">
<Input v-model="index.comment" class="h-7 text-xs" :disabled="!canEditIndexComment(index)" />
<td class="border-b border-r px-1.5 py-1">
<Input
v-model="index.comment"
class="h-6 text-[11px]"
:disabled="!canEditIndexComment(index)"
/>
</td>
<td class="border-b px-2 py-1.5">
<td class="border-b px-1.5 py-1">
<Badge v-if="index.isPrimary" variant="outline">{{ t("structureEditor.primary") }}</Badge>
<Button
v-else-if="index.original"
variant="ghost"
size="sm"
class="h-7 gap-1"
class="h-6 gap-1 px-1.5 text-[11px]"
:disabled="!canDropIndex(index)"
@click="toggleDropIndex(index)"
>
<Trash2 class="h-3.5 w-3.5" />
{{ index.markedForDrop ? t("structureEditor.restore") : t("structureEditor.drop") }}
</Button>
<Button v-else variant="ghost" size="sm" class="h-7 gap-1" @click="removeNewIndex(index)">
<Button
v-else
variant="ghost"
size="sm"
class="h-6 gap-1 px-1.5 text-[11px]"
@click="removeNewIndex(index)"
>
<X class="h-3.5 w-3.5" />
{{ t("structureEditor.remove") }}
</Button>
@ -784,12 +808,12 @@ watch(
</table>
</TabsContent>
<TabsContent value="foreignKeys" class="m-0 min-h-0 flex-1 overflow-auto p-3">
<TabsContent value="foreignKeys" class="m-0 min-h-0 flex-1 overflow-auto p-2">
<div v-if="foreignKeys.length === 0" class="py-10 text-center text-sm text-muted-foreground">
{{ t("structureEditor.emptyReadonly") }}
</div>
<div v-else class="space-y-2">
<div v-for="fk in foreignKeys" :key="fk.name" class="rounded-md border px-3 py-2 text-xs">
<div v-else class="space-y-1.5">
<div v-for="fk in foreignKeys" :key="fk.name" class="rounded-md border px-2 py-1.5 text-[11px]">
<div class="font-medium">{{ fk.name }}</div>
<div class="mt-1 font-mono text-muted-foreground">
{{ fk.column }} -> {{ fk.ref_table }}.{{ fk.ref_column }}
@ -798,12 +822,16 @@ watch(
</div>
</TabsContent>
<TabsContent value="triggers" class="m-0 min-h-0 flex-1 overflow-auto p-3">
<TabsContent value="triggers" class="m-0 min-h-0 flex-1 overflow-auto p-2">
<div v-if="triggers.length === 0" class="py-10 text-center text-sm text-muted-foreground">
{{ t("structureEditor.emptyReadonly") }}
</div>
<div v-else class="space-y-2">
<div v-for="trigger in triggers" :key="trigger.name" class="rounded-md border px-3 py-2 text-xs">
<div v-else class="space-y-1.5">
<div
v-for="trigger in triggers"
:key="trigger.name"
class="rounded-md border px-2 py-1.5 text-[11px]"
>
<div class="font-medium">{{ trigger.name }}</div>
<div class="mt-1 font-mono text-muted-foreground">{{ trigger.timing }} {{ trigger.event }}</div>
</div>
@ -813,16 +841,16 @@ watch(
</div>
<div class="flex min-w-0 flex-col rounded-md border">
<div class="flex items-center justify-between border-b px-3 py-2 text-xs font-medium">
<div class="flex items-center justify-between border-b px-2 py-1.5 text-[11px] font-medium">
<span>{{ t("structureEditor.sqlPreview") }}</span>
<Badge variant="secondary">{{ pendingStatements.length }}</Badge>
</div>
<div class="min-h-0 flex-1 overflow-auto p-3">
<div v-if="warnings.length" class="mb-3 space-y-1">
<div class="min-h-0 flex-1 overflow-auto p-2">
<div v-if="warnings.length" class="mb-2 space-y-1">
<div
v-for="warning in warnings"
:key="warning"
class="flex gap-2 rounded-md border border-yellow-300/40 bg-yellow-500/10 px-2 py-1.5 text-xs text-yellow-700 dark:text-yellow-300"
class="flex gap-1.5 rounded-md border border-yellow-300/40 bg-yellow-500/10 px-2 py-1 text-[11px] text-yellow-700 dark:text-yellow-300"
>
<AlertTriangle class="mt-0.5 h-3.5 w-3.5 shrink-0" />
<span>{{ warning }}</span>
@ -830,7 +858,7 @@ watch(
</div>
<pre
v-if="pendingStatements.length"
class="whitespace-pre-wrap break-words rounded-md bg-muted/40 p-3 font-mono text-xs leading-5"
class="whitespace-pre-wrap break-words rounded-md bg-muted/40 p-2 font-mono text-[11px] leading-4"
>{{ pendingStatements.join("\n") }}</pre
>
<div v-else class="flex h-full items-center justify-center text-sm text-muted-foreground">
@ -842,7 +870,7 @@ watch(
<div
v-if="errorMessage"
class="rounded-md border border-destructive/30 bg-destructive/10 px-3 py-2 text-xs text-destructive"
class="rounded-md border border-destructive/30 bg-destructive/10 px-2.5 py-1.5 text-[11px] text-destructive"
>
{{ errorMessage }}
</div>

View File

@ -50,3 +50,11 @@ test("structure editor exposes column order controls", () => {
assert.match(source, /t\(['"]structureEditor\.moveColumnUp['"]\)/);
assert.match(source, /t\(['"]structureEditor\.moveColumnDown['"]\)/);
});
test("structure editor uses a dense wide layout for large tables", () => {
assert.match(source, /sm:max-w-\[1180px\]/);
assert.doesNotMatch(source, /1500px/);
assert.match(source, /grid-cols-\[minmax\(0,1fr\)_300px\]/);
assert.match(source, /data-structure-density="compact"/);
assert.match(source, /class="h-6 min-w-28 text-\[11px\]"/);
});