mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-19 16:16:45 +00:00
refactor(grid): split cell detail panels
This commit is contained in:
@@ -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+/));
|
||||
|
||||
Reference in New Issue
Block a user