refactor(grid): split cell detail panels

This commit is contained in:
t8y2
2026-09-08 23:22:14 +08:00
parent 4a3d1b1db3
commit d4f4538843
4 changed files with 129 additions and 78 deletions
+15 -75
View File
@@ -37,8 +37,6 @@ import {
Link2,
ListTree,
Maximize2,
PanelBottom,
PanelRight,
RefreshCw,
RefreshCcw,
TableProperties,
@@ -64,7 +62,7 @@ import LightTooltip from "@/components/ui/LightTooltip.vue";
import { Popover, PopoverAnchor, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Switch } from "@/components/ui/switch";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Tabs, TabsContent } from "@/components/ui/tabs";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
import ErrorBanner from "@/components/ui/ErrorBanner.vue";
import DangerConfirmDialog from "@/components/editor/DangerConfirmDialog.vue";
@@ -80,6 +78,8 @@ import DataGridFilterWorkbench from "@/components/grid/DataGridFilterWorkbench.v
import DataGridTextFilterWorkbench from "@/components/grid/DataGridTextFilterWorkbench.vue";
import DataGridTableInfoPanels from "@/components/grid/DataGridTableInfoPanels.vue";
import DataGridColumnFilterPopover from "@/components/grid/DataGridColumnFilterPopover.vue";
import DataGridCellDetailHeader from "@/components/grid/DataGridCellDetailHeader.vue";
import DataGridCellDetailHexViewer from "@/components/grid/DataGridCellDetailHexViewer.vue";
import TemporalCellEditor from "@/components/grid/TemporalCellEditor.vue";
import EnumCellEditor from "@/components/grid/EnumCellEditor.vue";
import DataGridReadonlyTextSelection from "@/components/grid/DataGridReadonlyTextSelection.vue";
@@ -13668,49 +13668,17 @@ 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="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"
class="h-5 w-5 shrink-0"
:title="cellDetailMetadataCollapsed ? t('grid.expandCellDetailMetadata') : t('grid.collapseCellDetailMetadata')"
:aria-label="cellDetailMetadataCollapsed ? t('grid.expandCellDetailMetadata') : t('grid.collapseCellDetailMetadata')"
:aria-expanded="!cellDetailMetadataCollapsed"
@click="toggleCellDetailMetadataCollapsed"
>
<ChevronRight v-if="cellDetailMetadataCollapsed" class="w-3 h-3" />
<ChevronDown v-else class="w-3 h-3" />
</Button>
<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" />
<PanelBottom v-else class="w-3 h-3" />
</Button>
<Button variant="ghost" size="icon" class="h-5 w-5" :title="t('grid.openCellDetailsDialog')" @click="openActiveCellDetailDialog">
<Maximize2 class="w-3 h-3" />
</Button>
<Button variant="ghost" size="icon" class="h-5 w-5" :title="t('grid.openRowDetailsDialog')" @click="openActiveRowDetailDialog">
<ListTree class="w-3 h-3" />
</Button>
<Button variant="ghost" size="icon" class="h-5 w-5" :title="t('grid.openColumnDetailsDialog')" @click="openActiveColumnDetailDialog">
<TableProperties class="w-3 h-3" />
</Button>
<Button variant="ghost" size="icon" class="h-5 w-5" @click="closeCellDetails">
<X class="w-3 h-3" />
</Button>
</div>
</div>
<DataGridCellDetailHeader
:metadata-collapsed="cellDetailMetadataCollapsed"
:panel-is-bottom="cellDetailPanelIsBottom"
:active-tabs="activeCellDetailTabs"
@toggle-metadata="toggleCellDetailMetadataCollapsed"
@toggle-layout="toggleCellDetailPanelLayout"
@open-cell-details="openActiveCellDetailDialog"
@open-row-details="openActiveRowDetailDialog"
@open-column-details="openActiveColumnDetailDialog"
@close="closeCellDetails"
/>
<DataGridCellDetailPanel
v-if="activeCellDetail"
@@ -13747,35 +13715,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 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">
{{
t("grid.hexViewerByteCount", {
count: activeBinaryHexByteCount,
})
}}
</div>
</div>
<div class="min-h-0 flex-1 overflow-auto rounded border bg-muted/20 font-mono text-[11px]">
<div class="sticky top-0 grid grid-cols-[5.5rem_minmax(24rem,1fr)_8rem] gap-3 border-b bg-muted px-2 py-1 font-semibold text-muted-foreground">
<div>{{ t("grid.hexViewerOffset") }}</div>
<div>{{ t("grid.hexViewerHex") }}</div>
<div>{{ t("grid.hexViewerAscii") }}</div>
</div>
<div v-for="row in activeBinaryHexRows" :key="row.offset" class="grid grid-cols-[5.5rem_minmax(24rem,1fr)_8rem] gap-3 border-b border-border/50 px-2 py-1 last:border-b-0">
<div class="select-all text-muted-foreground">
{{ row.offset }}
</div>
<div class="select-all whitespace-pre">{{ row.hex }}</div>
<div class="select-all whitespace-pre">{{ row.ascii }}</div>
</div>
<div v-if="activeBinaryHexRows.length === 0" class="px-2 py-6 text-center font-sans text-muted-foreground">
{{ t("grid.hexViewerEmpty") }}
</div>
</div>
</TabsContent>
<DataGridCellDetailHexViewer v-if="activeCellDetailTabs.includes('hexViewer')" :rows="activeBinaryHexRows" :byte-count="activeBinaryHexByteCount" />
<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">
@@ -0,0 +1,72 @@
<script setup lang="ts">
import { ChevronDown, ChevronRight, ListTree, Maximize2, PanelBottom, PanelRight, TableProperties, X } from "@lucide/vue";
import { useI18n } from "vue-i18n";
import { Button } from "@/components/ui/button";
import { TabsList, TabsTrigger } from "@/components/ui/tabs";
import type { CellDetailTab } from "@/lib/dataGrid/cellDetailPresentation";
interface DataGridCellDetailHeaderProps {
metadataCollapsed: boolean;
panelIsBottom: boolean;
activeTabs: CellDetailTab[];
}
const props = defineProps<DataGridCellDetailHeaderProps>();
const emit = defineEmits<{
toggleMetadata: [];
toggleLayout: [];
openCellDetails: [];
openRowDetails: [];
openColumnDetails: [];
close: [];
}>();
const { t } = useI18n();
</script>
<template>
<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"
class="h-5 w-5 shrink-0"
:title="props.metadataCollapsed ? t('grid.expandCellDetailMetadata') : t('grid.collapseCellDetailMetadata')"
:aria-label="props.metadataCollapsed ? t('grid.expandCellDetailMetadata') : t('grid.collapseCellDetailMetadata')"
:aria-expanded="!props.metadataCollapsed"
@click="emit('toggleMetadata')"
>
<ChevronRight v-if="props.metadataCollapsed" class="w-3 h-3" />
<ChevronDown v-else class="w-3 h-3" />
</Button>
<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="props.activeTabs.includes('hexViewer')" value="hexViewer" class="h-6 min-w-max flex-1 shrink-0 text-xs">
{{ t("grid.hexViewer") }}
</TabsTrigger>
<TabsTrigger v-if="props.activeTabs.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="props.panelIsBottom ? t('grid.cellDetailLayoutRight') : t('grid.cellDetailLayoutBottom')" @click="emit('toggleLayout')">
<PanelRight v-if="props.panelIsBottom" class="w-3 h-3" />
<PanelBottom v-else class="w-3 h-3" />
</Button>
<Button variant="ghost" size="icon" class="h-5 w-5" :title="t('grid.openCellDetailsDialog')" @click="emit('openCellDetails')">
<Maximize2 class="w-3 h-3" />
</Button>
<Button variant="ghost" size="icon" class="h-5 w-5" :title="t('grid.openRowDetailsDialog')" @click="emit('openRowDetails')">
<ListTree class="w-3 h-3" />
</Button>
<Button variant="ghost" size="icon" class="h-5 w-5" :title="t('grid.openColumnDetailsDialog')" @click="emit('openColumnDetails')">
<TableProperties class="w-3 h-3" />
</Button>
<Button variant="ghost" size="icon" class="h-5 w-5" @click="emit('close')">
<X class="w-3 h-3" />
</Button>
</div>
</div>
</template>
@@ -0,0 +1,40 @@
<script setup lang="ts">
import { useI18n } from "vue-i18n";
import { TabsContent } from "@/components/ui/tabs";
import type { BinaryHexViewRow } from "@/lib/dataGrid/binaryHexViewer";
interface DataGridCellDetailHexViewerProps {
rows: BinaryHexViewRow[];
byteCount: number;
}
const props = defineProps<DataGridCellDetailHexViewerProps>();
const { t } = useI18n();
</script>
<template>
<TabsContent 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">
{{ t("grid.hexViewerByteCount", { count: props.byteCount }) }}
</div>
</div>
<div class="min-h-0 flex-1 overflow-auto rounded border bg-muted/20 font-mono text-[11px]">
<div class="sticky top-0 grid grid-cols-[5.5rem_minmax(24rem,1fr)_8rem] gap-3 border-b bg-muted px-2 py-1 font-semibold text-muted-foreground">
<div>{{ t("grid.hexViewerOffset") }}</div>
<div>{{ t("grid.hexViewerHex") }}</div>
<div>{{ t("grid.hexViewerAscii") }}</div>
</div>
<div v-for="row in props.rows" :key="row.offset" class="grid grid-cols-[5.5rem_minmax(24rem,1fr)_8rem] gap-3 border-b border-border/50 px-2 py-1 last:border-b-0">
<div class="select-all text-muted-foreground">{{ row.offset }}</div>
<div class="select-all whitespace-pre">{{ row.hex }}</div>
<div class="select-all whitespace-pre">{{ row.ascii }}</div>
</div>
<div v-if="props.rows.length === 0" class="px-2 py-6 text-center font-sans text-muted-foreground">
{{ t("grid.hexViewerEmpty") }}
</div>
</div>
</TabsContent>
</template>
@@ -102,6 +102,7 @@ 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 cellDetailHeaderSource = readFileSync("apps/desktop/src/components/grid/DataGridCellDetailHeader.vue", "utf8");
const globalsCss = readFileSync("apps/desktop/src/styles/globals.css", "utf8");
function detail(patch: Partial<DataGridCellDetail> = {}): DataGridCellDetail {
@@ -1367,9 +1368,7 @@ 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 tabsHeader = cellDetailHeaderSource;
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+/));