mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-24 07:45:09 +00:00
feat(objects): support object browser column resizing
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from "vue";
|
||||
import { computed, onBeforeUnmount, ref, watch } from "vue";
|
||||
import { RecycleScroller } from "vue-virtual-scroller";
|
||||
import { useSqlHighlighter } from "@/composables/useSqlHighlighter";
|
||||
import {
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
Eraser,
|
||||
Eye,
|
||||
FileCode,
|
||||
GripVertical,
|
||||
ListTree,
|
||||
Upload,
|
||||
Loader2,
|
||||
@@ -83,6 +84,7 @@ import {
|
||||
} from "@/lib/objectBrowserRows";
|
||||
|
||||
type ObjectFilter = "all" | "tables" | "views" | "procedures" | "functions" | "sequences" | "packages";
|
||||
type ObjectBrowserColumnKey = "select" | "name" | "type" | "estimatedRows" | "totalBytes" | "created_at" | "updated_at" | "comment";
|
||||
|
||||
const props = defineProps<{
|
||||
connection: ConnectionConfig;
|
||||
@@ -148,7 +150,18 @@ const selectedTableIds = ref<Set<string>>(new Set());
|
||||
const expandedPartitionParentIds = ref<Set<string>>(new Set());
|
||||
const showBatchDropConfirm = ref(false);
|
||||
const batchDropPreviewSql = ref("");
|
||||
const objectColumnWidths = ref<Record<ObjectBrowserColumnKey, number>>({
|
||||
select: 34,
|
||||
name: 360,
|
||||
type: 110,
|
||||
estimatedRows: 110,
|
||||
totalBytes: 100,
|
||||
created_at: 150,
|
||||
updated_at: 150,
|
||||
comment: 260,
|
||||
});
|
||||
let loadId = 0;
|
||||
let stopColumnResize: (() => void) | null = null;
|
||||
|
||||
// Export via background tracker
|
||||
const { addTask: addExportTask } = useExportTracker();
|
||||
@@ -202,12 +215,22 @@ const objectFilters = computed<ObjectFilter[]>(() =>
|
||||
const showObjectFilter = computed(() => objectFilters.value.length > 2);
|
||||
const hasCreatedAt = computed(() => rows.value.some((row) => row.created_at?.trim()));
|
||||
const hasUpdatedAt = computed(() => rows.value.some((row) => row.updated_at?.trim()));
|
||||
const objectBrowserColumns = computed<ObjectBrowserColumnKey[]>(() => {
|
||||
const columns: ObjectBrowserColumnKey[] = ["select", "name", "type", "estimatedRows", "totalBytes"];
|
||||
if (hasCreatedAt.value) columns.push("created_at");
|
||||
if (hasUpdatedAt.value) columns.push("updated_at");
|
||||
columns.push("comment");
|
||||
return columns;
|
||||
});
|
||||
const gridTemplateColumns = computed(() => {
|
||||
const columns = ["34px", "minmax(160px,1fr)", "110px", "110px", "100px"];
|
||||
if (hasCreatedAt.value) columns.push("150px");
|
||||
if (hasUpdatedAt.value) columns.push("150px");
|
||||
columns.push("minmax(160px,0.7fr)");
|
||||
return columns.join(" ");
|
||||
return objectBrowserColumns.value
|
||||
.map((key, index, columns) => {
|
||||
const width = objectColumnWidths.value[key];
|
||||
if (key === "select") return `${width}px`;
|
||||
if (index === columns.length - 1) return `minmax(${width}px,1fr)`;
|
||||
return `${width}px`;
|
||||
})
|
||||
.join(" ");
|
||||
});
|
||||
const partitionRowsByParentId = computed(() => {
|
||||
const groups = new Map<string, ObjectBrowserRow[]>();
|
||||
@@ -262,6 +285,49 @@ function toggleSort(key: ObjectBrowserSortKey) {
|
||||
sortDirection.value = initialObjectBrowserSortDirection(key);
|
||||
}
|
||||
|
||||
function minimumColumnWidth(key: ObjectBrowserColumnKey) {
|
||||
if (key === "select") return 34;
|
||||
if (key === "name" || key === "comment") return 120;
|
||||
return 72;
|
||||
}
|
||||
|
||||
function onObjectColumnResizeStart(key: ObjectBrowserColumnKey, event: MouseEvent) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
stopColumnResize?.();
|
||||
|
||||
const startX = event.clientX;
|
||||
const startWidth = objectColumnWidths.value[key];
|
||||
const minWidth = minimumColumnWidth(key);
|
||||
document.body.classList.add("select-none", "cursor-col-resize");
|
||||
|
||||
const onMove = (moveEvent: MouseEvent) => {
|
||||
objectColumnWidths.value = {
|
||||
...objectColumnWidths.value,
|
||||
[key]: Math.max(minWidth, startWidth + moveEvent.clientX - startX),
|
||||
};
|
||||
};
|
||||
const onUp = () => {
|
||||
document.removeEventListener("mousemove", onMove);
|
||||
document.removeEventListener("mouseup", onUp);
|
||||
document.body.classList.remove("select-none", "cursor-col-resize");
|
||||
stopColumnResize = null;
|
||||
};
|
||||
|
||||
stopColumnResize = onUp;
|
||||
document.addEventListener("mousemove", onMove);
|
||||
document.addEventListener("mouseup", onUp);
|
||||
}
|
||||
|
||||
function resetObjectColumnWidth(key: ObjectBrowserColumnKey, width: number, event: MouseEvent) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
objectColumnWidths.value = {
|
||||
...objectColumnWidths.value,
|
||||
[key]: width,
|
||||
};
|
||||
}
|
||||
|
||||
function rowMatchesObjectFilter(row: ObjectBrowserRow) {
|
||||
if (objectFilter.value === "tables") return row.type === "TABLE";
|
||||
if (objectFilter.value === "views") return row.type === "VIEW" || row.type === "MATERIALIZED_VIEW";
|
||||
@@ -1149,6 +1215,10 @@ function onSearchKeydown(event: KeyboardEvent) {
|
||||
|
||||
defineExpose({ focusSearch });
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
stopColumnResize?.();
|
||||
});
|
||||
|
||||
watch(
|
||||
() => [props.connection.id, props.database, props.schema] as const,
|
||||
() => {
|
||||
@@ -1357,38 +1427,94 @@ function getObjectBrowserMenuItems(item: ObjectBrowserRow): ContextMenuItem[] {
|
||||
</div>
|
||||
<div v-else class="flex min-h-0 flex-1 flex-col">
|
||||
<div class="grid h-7 shrink-0 items-center gap-3 border-b bg-muted/40 px-3 text-xs font-medium text-muted-foreground" :style="{ gridTemplateColumns }">
|
||||
<button class="flex h-6 w-6 items-center justify-center rounded-sm hover:bg-accent" type="button" :disabled="visibleSelectableRows.length === 0" @click="toggleVisibleTableSelection">
|
||||
<CheckSquare v-if="allVisibleTablesSelected" class="h-3.5 w-3.5 text-primary" />
|
||||
<Square v-else class="h-3.5 w-3.5" />
|
||||
</button>
|
||||
<button class="flex min-w-0 items-center gap-1 truncate text-left" type="button" @click="toggleSort('name')">
|
||||
<span class="truncate">{{ t("objects.name") }}</span>
|
||||
<component :is="sortIconFor('name')" v-if="sortIconFor('name')" class="h-3 w-3 shrink-0" />
|
||||
</button>
|
||||
<button class="flex min-w-0 items-center gap-1 truncate text-left" type="button" @click="toggleSort('type')">
|
||||
<span class="truncate">{{ t("objects.type") }}</span>
|
||||
<component :is="sortIconFor('type')" v-if="sortIconFor('type')" class="h-3 w-3 shrink-0" />
|
||||
</button>
|
||||
<button class="flex min-w-0 items-center justify-end gap-1 truncate text-right" type="button" :title="t('objects.statisticsHint')" @click="toggleSort('estimatedRows')">
|
||||
<span class="truncate">{{ t("objects.rows") }}</span>
|
||||
<component :is="sortIconFor('estimatedRows')" v-if="sortIconFor('estimatedRows')" class="h-3 w-3 shrink-0" />
|
||||
</button>
|
||||
<button class="flex min-w-0 items-center justify-end gap-1 truncate text-right" type="button" :title="t('objects.statisticsHint')" @click="toggleSort('totalBytes')">
|
||||
<span class="truncate">{{ t("objects.size") }}</span>
|
||||
<component :is="sortIconFor('totalBytes')" v-if="sortIconFor('totalBytes')" class="h-3 w-3 shrink-0" />
|
||||
</button>
|
||||
<button v-if="hasCreatedAt" class="flex min-w-0 items-center gap-1 truncate text-left" type="button" @click="toggleSort('created_at')">
|
||||
<span class="truncate">{{ t("objects.createdAt") }}</span>
|
||||
<component :is="sortIconFor('created_at')" v-if="sortIconFor('created_at')" class="h-3 w-3 shrink-0" />
|
||||
</button>
|
||||
<button v-if="hasUpdatedAt" class="flex min-w-0 items-center gap-1 truncate text-left" type="button" @click="toggleSort('updated_at')">
|
||||
<span class="truncate">{{ t("objects.updatedAt") }}</span>
|
||||
<component :is="sortIconFor('updated_at')" v-if="sortIconFor('updated_at')" class="h-3 w-3 shrink-0" />
|
||||
</button>
|
||||
<button class="flex min-w-0 items-center gap-1 truncate text-left" type="button" @click="toggleSort('comment')">
|
||||
<span class="truncate">{{ t("objects.comment") }}</span>
|
||||
<component :is="sortIconFor('comment')" v-if="sortIconFor('comment')" class="h-3 w-3 shrink-0" />
|
||||
</button>
|
||||
<div class="relative flex min-w-0 items-center">
|
||||
<button class="flex h-6 w-6 items-center justify-center rounded-sm hover:bg-accent" type="button" :disabled="visibleSelectableRows.length === 0" @click="toggleVisibleTableSelection">
|
||||
<CheckSquare v-if="allVisibleTablesSelected" class="h-3.5 w-3.5 text-primary" />
|
||||
<Square v-else class="h-3.5 w-3.5" />
|
||||
</button>
|
||||
<div class="absolute -right-2 top-0 bottom-0 z-10 flex w-3 cursor-col-resize items-center justify-center text-muted-foreground/70 hover:bg-primary/30 hover:text-primary" @mousedown="onObjectColumnResizeStart('select', $event)" @dblclick="resetObjectColumnWidth('select', 34, $event)">
|
||||
<GripVertical class="h-3 w-3" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="relative flex min-w-0 items-center">
|
||||
<button class="flex min-w-0 items-center gap-1 truncate pr-4 text-left" type="button" @click="toggleSort('name')">
|
||||
<span class="truncate">{{ t("objects.name") }}</span>
|
||||
<component :is="sortIconFor('name')" v-if="sortIconFor('name')" class="h-3 w-3 shrink-0" />
|
||||
</button>
|
||||
<div class="absolute -right-2 top-0 bottom-0 z-10 flex w-3 cursor-col-resize items-center justify-center text-muted-foreground/70 hover:bg-primary/30 hover:text-primary" @mousedown="onObjectColumnResizeStart('name', $event)" @dblclick="resetObjectColumnWidth('name', 360, $event)">
|
||||
<GripVertical class="h-3 w-3" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="relative flex min-w-0 items-center">
|
||||
<button class="flex min-w-0 items-center gap-1 truncate pr-4 text-left" type="button" @click="toggleSort('type')">
|
||||
<span class="truncate">{{ t("objects.type") }}</span>
|
||||
<component :is="sortIconFor('type')" v-if="sortIconFor('type')" class="h-3 w-3 shrink-0" />
|
||||
</button>
|
||||
<div class="absolute -right-2 top-0 bottom-0 z-10 flex w-3 cursor-col-resize items-center justify-center text-muted-foreground/70 hover:bg-primary/30 hover:text-primary" @mousedown="onObjectColumnResizeStart('type', $event)" @dblclick="resetObjectColumnWidth('type', 110, $event)">
|
||||
<GripVertical class="h-3 w-3" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="relative flex min-w-0 items-center justify-end">
|
||||
<button class="flex min-w-0 items-center justify-end gap-1 truncate pr-4 text-right" type="button" :title="t('objects.statisticsHint')" @click="toggleSort('estimatedRows')">
|
||||
<span class="truncate">{{ t("objects.rows") }}</span>
|
||||
<component :is="sortIconFor('estimatedRows')" v-if="sortIconFor('estimatedRows')" class="h-3 w-3 shrink-0" />
|
||||
</button>
|
||||
<div
|
||||
class="absolute -right-2 top-0 bottom-0 z-10 flex w-3 cursor-col-resize items-center justify-center text-muted-foreground/70 hover:bg-primary/30 hover:text-primary"
|
||||
@mousedown="onObjectColumnResizeStart('estimatedRows', $event)"
|
||||
@dblclick="resetObjectColumnWidth('estimatedRows', 110, $event)"
|
||||
>
|
||||
<GripVertical class="h-3 w-3" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="relative flex min-w-0 items-center justify-end">
|
||||
<button class="flex min-w-0 items-center justify-end gap-1 truncate pr-4 text-right" type="button" :title="t('objects.statisticsHint')" @click="toggleSort('totalBytes')">
|
||||
<span class="truncate">{{ t("objects.size") }}</span>
|
||||
<component :is="sortIconFor('totalBytes')" v-if="sortIconFor('totalBytes')" class="h-3 w-3 shrink-0" />
|
||||
</button>
|
||||
<div
|
||||
class="absolute -right-2 top-0 bottom-0 z-10 flex w-3 cursor-col-resize items-center justify-center text-muted-foreground/70 hover:bg-primary/30 hover:text-primary"
|
||||
@mousedown="onObjectColumnResizeStart('totalBytes', $event)"
|
||||
@dblclick="resetObjectColumnWidth('totalBytes', 100, $event)"
|
||||
>
|
||||
<GripVertical class="h-3 w-3" />
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="hasCreatedAt" class="relative flex min-w-0 items-center">
|
||||
<button class="flex min-w-0 items-center gap-1 truncate pr-4 text-left" type="button" @click="toggleSort('created_at')">
|
||||
<span class="truncate">{{ t("objects.createdAt") }}</span>
|
||||
<component :is="sortIconFor('created_at')" v-if="sortIconFor('created_at')" class="h-3 w-3 shrink-0" />
|
||||
</button>
|
||||
<div
|
||||
class="absolute -right-2 top-0 bottom-0 z-10 flex w-3 cursor-col-resize items-center justify-center text-muted-foreground/70 hover:bg-primary/30 hover:text-primary"
|
||||
@mousedown="onObjectColumnResizeStart('created_at', $event)"
|
||||
@dblclick="resetObjectColumnWidth('created_at', 150, $event)"
|
||||
>
|
||||
<GripVertical class="h-3 w-3" />
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="hasUpdatedAt" class="relative flex min-w-0 items-center">
|
||||
<button class="flex min-w-0 items-center gap-1 truncate pr-4 text-left" type="button" @click="toggleSort('updated_at')">
|
||||
<span class="truncate">{{ t("objects.updatedAt") }}</span>
|
||||
<component :is="sortIconFor('updated_at')" v-if="sortIconFor('updated_at')" class="h-3 w-3 shrink-0" />
|
||||
</button>
|
||||
<div
|
||||
class="absolute -right-2 top-0 bottom-0 z-10 flex w-3 cursor-col-resize items-center justify-center text-muted-foreground/70 hover:bg-primary/30 hover:text-primary"
|
||||
@mousedown="onObjectColumnResizeStart('updated_at', $event)"
|
||||
@dblclick="resetObjectColumnWidth('updated_at', 150, $event)"
|
||||
>
|
||||
<GripVertical class="h-3 w-3" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="relative flex min-w-0 items-center">
|
||||
<button class="flex min-w-0 items-center gap-1 truncate pr-4 text-left" type="button" @click="toggleSort('comment')">
|
||||
<span class="truncate">{{ t("objects.comment") }}</span>
|
||||
<component :is="sortIconFor('comment')" v-if="sortIconFor('comment')" class="h-3 w-3 shrink-0" />
|
||||
</button>
|
||||
<div class="absolute -right-2 top-0 bottom-0 z-10 flex w-3 cursor-col-resize items-center justify-center text-muted-foreground/70 hover:bg-primary/30 hover:text-primary" @mousedown="onObjectColumnResizeStart('comment', $event)" @dblclick="resetObjectColumnWidth('comment', 260, $event)">
|
||||
<GripVertical class="h-3 w-3" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<RecycleScroller class="object-browser-scroller min-h-0 flex-1" :items="filteredRows" :item-size="34" :buffer="600" :skip-hover="true" key-field="id">
|
||||
<template #default="{ item }">
|
||||
|
||||
Reference in New Issue
Block a user