mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-21 06:00:39 +00:00
fix(grid): responsive cell detail layout in narrow panels
This commit is contained in:
@@ -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({
|
||||
|
||||
Reference in New Issue
Block a user