fix(grid): responsive cell detail layout in narrow panels

This commit is contained in:
jearhe
2026-09-05 00:41:51 +08:00
committed by GitHub
parent 719aa3e849
commit 23a4ca8903
3 changed files with 54 additions and 32 deletions
+18 -23
View File
@@ -6469,13 +6469,6 @@ const activeBinaryHexBytes = computed(() => {
const activeBinaryHexRows = computed(() => (activeBinaryHexBytes.value ? buildBinaryHexViewRows(activeBinaryHexBytes.value) : []));
const activeBinaryHexByteCount = computed(() => activeBinaryHexBytes.value?.length ?? 0);
const activeCellDetailTabsGridClass = computed(() => {
const count = activeCellDetailTabs.value.length;
if (count >= 3) return "grid-cols-3";
if (count === 2) return "grid-cols-2";
return "grid-cols-1";
});
watch(activeCellDetailTabs, (tabs) => {
if (!tabs.includes(activeCellDetailTab.value)) {
activeCellDetailTab.value = defaultCellDetailTab();
@@ -14476,8 +14469,8 @@ function openGridSnapshot() {
>
<div v-if="!cellDetailPanelIsBottom" class="absolute left-0 top-0 bottom-0 z-20 w-1.5 -translate-x-1/2 cursor-col-resize hover:bg-primary/30" @mousedown.prevent="onDetailResizeStart" />
<div v-else class="data-grid-detail-resize-handle data-grid-detail-resize-handle--bottom absolute left-0 right-0 top-0 z-20 h-2 -translate-y-1/2 cursor-row-resize" @mousedown.prevent="onDetailResizeStart" />
<Tabs v-model="activeCellDetailTab" class="flex-1 min-h-0 gap-0">
<div class="h-9 flex items-center gap-2 px-3 border-b shrink-0 bg-muted/20">
<Tabs v-model="activeCellDetailTab" class="min-w-0 flex-1 min-h-0 gap-0">
<div class="h-9 flex min-w-0 items-center gap-2 overflow-hidden border-b bg-muted/20 px-3 shrink-0">
<Button
variant="ghost"
size="icon"
@@ -14490,15 +14483,17 @@ function openGridSnapshot() {
<ChevronRight v-if="cellDetailMetadataCollapsed" class="w-3 h-3" />
<ChevronDown v-else class="w-3 h-3" />
</Button>
<TabsList class="grid h-7 min-w-0 flex-1 p-0.5" :class="activeCellDetailTabsGridClass">
<TabsTrigger value="details" class="h-6 text-xs">{{ t("grid.cellDetails") }}</TabsTrigger>
<TabsTrigger v-if="activeCellDetailTabs.includes('hexViewer')" value="hexViewer" class="h-6 text-xs">
{{ t("grid.hexViewer") }}
</TabsTrigger>
<TabsTrigger v-if="activeCellDetailTabs.includes('valueEditor')" value="valueEditor" class="h-6 text-xs">
{{ t("grid.valueEditor") }}
</TabsTrigger>
</TabsList>
<div class="min-w-0 flex-1 overflow-x-auto overflow-y-hidden overscroll-x-contain">
<TabsList class="flex h-7 w-max min-w-full justify-start p-0.5">
<TabsTrigger value="details" class="h-6 min-w-max flex-1 shrink-0 text-xs">{{ t("grid.cellDetails") }}</TabsTrigger>
<TabsTrigger v-if="activeCellDetailTabs.includes('hexViewer')" value="hexViewer" class="h-6 min-w-max flex-1 shrink-0 text-xs">
{{ t("grid.hexViewer") }}
</TabsTrigger>
<TabsTrigger v-if="activeCellDetailTabs.includes('valueEditor')" value="valueEditor" class="h-6 min-w-max flex-1 shrink-0 text-xs">
{{ t("grid.valueEditor") }}
</TabsTrigger>
</TabsList>
</div>
<div class="ml-auto flex shrink-0 items-center gap-1">
<Button variant="ghost" size="icon" class="h-5 w-5" :title="cellDetailPanelIsBottom ? t('grid.cellDetailLayoutRight') : t('grid.cellDetailLayoutBottom')" @click="toggleCellDetailPanelLayout">
<PanelRight v-if="cellDetailPanelIsBottom" class="w-3 h-3" />
@@ -14554,7 +14549,7 @@ function openGridSnapshot() {
@copy-column-name="copyDetailColumnName"
@copy-sql-condition="copyDetailSqlCondition"
/>
<TabsContent v-if="activeCellDetailTabs.includes('hexViewer')" value="hexViewer" class="m-0 min-h-0 flex-1 flex flex-col p-3 text-xs">
<TabsContent v-if="activeCellDetailTabs.includes('hexViewer')" value="hexViewer" class="m-0 min-h-0 min-w-0 flex-1 flex flex-col p-3 text-xs">
<div class="mb-2 min-w-0 shrink-0">
<div class="font-medium">{{ t("grid.hexViewer") }}</div>
<div class="text-[11px] text-muted-foreground">
@@ -14584,8 +14579,8 @@ function openGridSnapshot() {
</div>
</TabsContent>
<TabsContent v-if="activeCellDetailTabs.includes('valueEditor')" value="valueEditor" class="m-0 min-h-0 flex-1 flex flex-col p-3 text-xs">
<div class="flex min-h-0 flex-1 flex-col">
<TabsContent v-if="activeCellDetailTabs.includes('valueEditor')" value="valueEditor" class="m-0 min-h-0 min-w-0 flex-1 flex flex-col p-3 text-xs">
<div class="min-w-0 flex min-h-0 flex-1 flex-col">
<TemporalCellEditor
v-if="detailTemporalEditorConfig"
v-model="detailEditValue"
@@ -14596,9 +14591,9 @@ function openGridSnapshot() {
@cancel="cancelValueEditorEdit"
@commit="commitValueEditorEdit"
/>
<div v-else ref="valueEditorContainer" data-cell-detail-editor-root class="min-h-0 flex-1 w-full rounded border overflow-auto" />
<div v-else ref="valueEditorContainer" data-cell-detail-editor-root class="min-h-0 min-w-0 flex-1 w-full rounded border overflow-auto" />
</div>
<div class="flex gap-1 mt-2 shrink-0">
<div class="min-w-0 flex flex-wrap gap-1 mt-2 shrink-0">
<DropdownMenu v-if="activeCellDetail?.isEditable">
<DropdownMenuTrigger as-child>
<Button variant="outline" size="sm" class="h-6 gap-1 text-xs" @mousedown.prevent>
@@ -80,8 +80,8 @@ defineExpose({ openSearch });
</script>
<template>
<TabsContent value="details" class="m-0 min-h-0 flex-1 flex flex-col">
<div data-native-clipboard class="flex-1 min-h-0 overflow-auto px-3 pt-3 text-xs" :class="[valueFillsHeight ? 'flex flex-col gap-3' : 'space-y-3', editing && !panelIsBottom ? 'pb-1' : 'pb-3']">
<TabsContent value="details" class="m-0 min-h-0 min-w-0 flex-1 flex flex-col">
<div data-native-clipboard class="min-w-0 flex-1 min-h-0 overflow-auto px-3 pt-3 text-xs" :class="[valueFillsHeight ? 'flex flex-col gap-3' : 'space-y-3', editing && !panelIsBottom ? 'pb-1' : 'pb-3']">
<div v-if="panelIsBottom && !metadataCollapsed" class="grid grid-cols-[minmax(180px,1.6fr)_repeat(4,minmax(74px,0.55fr))_minmax(160px,1fr)] gap-3 rounded border bg-muted/20 p-2">
<div class="min-w-0 space-y-1">
<div class="text-muted-foreground">{{ t("grid.columnName") }}</div>
@@ -138,9 +138,9 @@ defineExpose({ openSearch });
</template>
<div class="space-y-1" :class="[{ 'min-h-0 flex flex-col': valueFillsHeight }, valueFillsHeight && !(detail.imagePreviewUrl && !editing) ? 'flex-1' : '', detail.imagePreviewUrl && !editing ? 'shrink-0' : '']">
<div class="flex min-h-5 items-center justify-between gap-2">
<div class="text-muted-foreground">{{ t("grid.cellValue") }}</div>
<div class="flex items-center gap-1">
<div class="flex min-h-5 min-w-0 flex-wrap items-center justify-between gap-2">
<div class="shrink-0 text-muted-foreground">{{ t("grid.cellValue") }}</div>
<div class="min-w-0 flex flex-1 flex-wrap items-center justify-end gap-1">
<Button v-if="editing && showCompareJson" variant="ghost" size="sm" class="h-5 gap-1 px-1.5 text-xs" :disabled="!canCompareJson" :title="t('grid.compareJson')" @mousedown.prevent @click="emit('compareJson')"><FileDiff class="h-3 w-3" />{{ t("grid.compareJson") }}</Button>
<Button v-if="showCompactJson" variant="ghost" size="sm" class="h-5 gap-1 px-1.5 text-xs" :disabled="!canCompactJson" :title="t('grid.compactJson')" @click="emit('compactJson')"><Code2 class="h-3 w-3" />{{ t("grid.compactJson") }}</Button>
<Button v-if="!editing && detail.formattedJson" :variant="sideJsonView ? 'secondary' : 'ghost'" size="sm" class="h-5 gap-1 px-1.5 text-xs" :title="t('grid.formattedJson')" @click="emit('toggleFormatted')"><Code2 class="h-3 w-3" />{{ t("grid.formattedJson") }}</Button>
@@ -168,7 +168,7 @@ defineExpose({ openSearch });
/></a>
</div>
<template v-if="editing"
><div class="dbx-data-grid-value-font min-h-0 flex-1" :style="editorStyle">
><div class="dbx-data-grid-value-font min-h-0 min-w-0 flex-1" :style="editorStyle">
<TemporalCellEditor v-if="temporalEditorConfig" v-model="detailEditValue" :kind="temporalEditorConfig.kind" :fraction-precision="temporalEditorConfig.fractionPrecision" variant="inline" :commit-on-close="false" @cancel="emit('cancel')" @commit="emit('commit')" />
<div v-else ref="detailsEditorContainer" data-cell-detail-editor-root class="min-h-0 h-full w-full rounded border overflow-hidden" />
</div>
@@ -196,12 +196,12 @@ defineExpose({ openSearch });
<div v-if="detail.isValuePreviewTruncated && !sideJsonView" class="text-[11px] text-muted-foreground">{{ t("grid.largeValuePreviewHint", { count: detail.rawValuePreview.length }) }}</div>
</div>
</div>
<div class="border-t flex gap-1 overflow-hidden bg-background p-1.5" :class="panelIsBottom ? 'shrink-0 items-center' : 'shrink-0 flex-col'">
<div v-if="editing && panelIsBottom" class="flex shrink-0 gap-1 mr-auto">
<div class="flex min-w-0 flex-wrap gap-1 overflow-hidden border-t bg-background p-1.5" :class="panelIsBottom ? 'shrink-0 items-center' : 'shrink-0 flex-col'">
<div v-if="editing && panelIsBottom" class="mr-auto flex shrink-0 flex-wrap gap-1">
<Button size="sm" class="h-6 text-xs" @click="emit('commit')">{{ t("dangerDialog.confirm") }}</Button
><Button variant="outline" size="sm" class="h-6 text-xs" @click="emit('cancel')">{{ t("dangerDialog.cancel") }}</Button>
</div>
<div class="flex gap-1" :class="panelIsBottom ? 'ml-auto shrink-0 justify-end' : 'flex-col'">
<div class="min-w-0 flex flex-wrap gap-1" :class="panelIsBottom ? 'ml-auto justify-end' : 'flex-col'">
<Button v-if="detail.isEditable && detail.value !== null" variant="ghost" size="sm" class="h-6 justify-start text-xs" @click="emit('setNull')"><X class="w-3 h-3 mr-2" />{{ t("grid.setNull") }}</Button
><Button variant="ghost" size="sm" class="h-6 justify-start text-xs" @click="emit('copyColumnName')"><Copy class="w-3 h-3 mr-2" />{{ t("grid.copyColumnName") }}</Button
><Button variant="ghost" size="sm" class="h-6 justify-start text-xs" :disabled="!canCopySqlCondition()" @click="emit('copySqlCondition')"><Code2 class="w-3 h-3 mr-2" />{{ t("grid.copySqlCondition") }}</Button>
@@ -101,6 +101,7 @@ import DataGridQueryControls from "@/components/grid/DataGridQueryControls.vue";
import DataGridSearchBar from "@/components/grid/DataGridSearchBar.vue";
const dataGridSource = readFileSync("apps/desktop/src/components/grid/DataGrid.vue", "utf8");
const cellDetailPanelSource = readFileSync("apps/desktop/src/components/grid/DataGridCellDetailPanel.vue", "utf8");
const globalsCss = readFileSync("apps/desktop/src/styles/globals.css", "utf8");
function detail(patch: Partial<DataGridCellDetail> = {}): DataGridCellDetail {
@@ -1365,6 +1366,32 @@ describe("DataGridTextFilterWorkbench", () => {
});
describe("cell detail surfaces", () => {
it("keeps detail tabs and editor actions usable when the panel narrows", () => {
const tabsStart = dataGridSource.indexOf('<Tabs v-model="activeCellDetailTab"');
const panelStart = dataGridSource.indexOf("<DataGridCellDetailPanel", tabsStart);
const tabsHeader = dataGridSource.slice(tabsStart, panelStart);
const tabViewport = tabsHeader.match(/<div class="([^"]*overflow-x-auto[^"]*)">\s*<TabsList/);
const tabList = tabsHeader.match(/<TabsList class="([^"]+)">/);
const triggerClasses = Array.from(tabsHeader.matchAll(/<TabsTrigger\b[^>]*class="([^"]+)"/g), ([, classes]) => classes.split(/\s+/));
expect(tabViewport?.[1]?.split(/\s+/)).toEqual(expect.arrayContaining(["min-w-0", "flex-1", "overflow-x-auto"]));
expect(tabList?.[1]?.split(/\s+/)).toEqual(expect.arrayContaining(["flex", "w-max", "min-w-full"]));
expect(triggerClasses).toHaveLength(3);
for (const classes of triggerClasses) {
expect(classes).toEqual(expect.arrayContaining(["min-w-max", "flex-1", "shrink-0"]));
}
expect(dataGridSource).not.toContain("activeCellDetailTabsGridClass");
const valueEditorStart = dataGridSource.indexOf("<TabsContent v-if=\"activeCellDetailTabs.includes('valueEditor')\"");
const valueEditorEnd = dataGridSource.indexOf("</TabsContent>", valueEditorStart);
const valueEditor = dataGridSource.slice(valueEditorStart, valueEditorEnd);
expect(valueEditor).toMatch(/<TabsContent[^>]*class="[^"]*\bmin-w-0\b[^"]*">/);
expect(valueEditor).toMatch(/<div class="[^"]*\bmin-w-0\b[^"]*\bflex-wrap\b[^"]*">/);
expect(cellDetailPanelSource).toMatch(/<TabsContent value="details" class="[^"]*\bmin-w-0\b[^"]*">/);
expect(cellDetailPanelSource).toMatch(/<div class="[^"]*\bmin-w-0\b[^"]*\bflex-wrap\b[^"]*">/);
});
it("presents printable LONG BLOB bytes as text and copies the presented value", async () => {
const copyText = vi.fn();
const blobDetail = detail({