feat(grid): preview text stored in MySQL BLOB cells

This commit is contained in:
zipg authored and GitHub committed 2026-08-24 20:34:04 +08:00
1 parent 566bc87014
commit f45fcdb69d
8 files changed
+217 -15

No files matched your search

+10 -6
View File
@@ -141,6 +141,8 @@ import {
canDownloadBinaryCellValue,
downloadBinaryCellPayload,
formatBinaryCellByteSize,
binaryCellUtf8Text,
isBlobCellColumnType,
isBinaryCellColumnType,
openBinaryCellFile,
parseBinaryCellBytes,
@@ -4120,8 +4122,8 @@ function cellEditContentNeedsExpandedEditor(options: { displayText: string; edit
function cellEditorTextForValue(value: CellValue | undefined, columnIndex: number): string {
return dataGridCellEditorText({
value: value ?? null,
databaseType: props.databaseType,
columnInfo: tableColumnForGridColumn(columnIndex),
databaseType: resolvedDatabaseType.value,
columnInfo: tableColumnForGridColumn(columnIndex) ?? resultColumnInfoForGridColumn(columnIndex),
});
}
@@ -4185,7 +4187,7 @@ function isIoTDBTimestampColumn(columnIndex: number): boolean {
}
function inlineCellEditorText(value: CellValue, columnIndex: number): string {
const columnInfo = tableColumnForGridColumn(columnIndex);
const columnInfo = tableColumnForGridColumn(columnIndex) ?? resultColumnInfoForGridColumn(columnIndex);
const columnType = props.result.column_types?.[columnIndex] ?? columnInfo?.data_type;
return (
formatIoTDBTimestampEditorValue(value, resolvedDatabaseType.value, columnType, resolvedConnectionConfig.value?.url_params) ??
@@ -6054,8 +6056,8 @@ watch(activeCellDetail, (detail) => {
}
const value = dataGridCellEditorText({
value: detail.value,
databaseType: props.databaseType,
columnInfo: tableColumnForGridColumn(detail.colIndex),
databaseType: resolvedDatabaseType.value,
columnInfo: tableColumnForGridColumn(detail.colIndex) ?? resultColumnInfoForGridColumn(detail.colIndex),
});
detailEditValue.value = value;
detailEditOriginalValue.value = value;
@@ -9100,7 +9102,7 @@ async function copyDetailValue() {
if (!initialDetail || !(await hydrateLargeValueCell(initialDetail.rowId, initialDetail.colIndex))) return;
const detail = activeCellDetail.value;
if (!detail) return;
const text = detail.value === null ? "" : displayCellValue(detail.value);
const text = detail.value === null ? "" : isBlobCellColumnType(detail.type) ? (binaryCellUtf8Text(detail.value, detail.type, resolvedDatabaseType.value) ?? displayCellValue(detail.value)) : displayCellValue(detail.value);
copyText(text);
}
@@ -13356,6 +13358,7 @@ function currentGridContextMenuItems(): ContextMenuItem[] {
:import-binary-value="importDetailBinaryValue"
:open-image-preview="openImagePreview"
:can-copy-sql-condition="canCopyPreparedDetailSqlCondition"
:database-type="resolvedDatabaseType"
@start-edit="startDetailEdit"
@compact-json="compactDetailJson"
@toggle-formatted="toggleCellDetailJsonFormatted"
@@ -13545,6 +13548,7 @@ function currentGridContextMenuItems(): ContextMenuItem[] {
:download-binary-value="downloadDetailBinaryValue"
:can-import-binary-value="canImportDetailBinaryValue"
:import-binary-value="importDetailBinaryValue"
:database-type="resolvedDatabaseType"
@edit="openDialogCellInSidePanel"
/>
@@ -9,10 +9,11 @@ import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover
import { useCellDetailEditor, type UseCellDetailEditorReturn } from "@/composables/useCellDetailEditor";
import { useTheme } from "@/composables/useTheme";
import { useSettingsStore } from "@/stores/settingsStore";
import { BINARY_CELL_DOWNLOAD_MODES, type BinaryCellDownloadMode } from "@/lib/dataGrid/binaryCellDownload";
import { BINARY_CELL_DOWNLOAD_MODES, binaryCellUtf8Text, isBlobCellColumnType, type BinaryCellDownloadMode } from "@/lib/dataGrid/binaryCellDownload";
import { isGeometryColumnType } from "@/lib/dataGrid/cellDetailPresentation";
import { isHexGeometry, renderWktOnCanvas } from "@/lib/dataGrid/geometryPreview";
import type { DataGridCellDetail } from "@/lib/dataGrid/dataGridDetail";
import type { DatabaseType } from "@/types/database";
const { t } = useI18n();
const settingsStore = useSettingsStore();
@@ -27,6 +28,8 @@ const props = defineProps<{
downloadBinaryValue: (detail: DataGridCellDetail | null, mode: BinaryCellDownloadMode) => void | Promise<void>;
canImportBinaryValue: (detail: DataGridCellDetail | null) => boolean;
importBinaryValue: (detail: DataGridCellDetail | null) => void | Promise<void>;
/** BLOB 文本预览与编辑写回一致,仅在 MySQL 连接开启。 */
databaseType?: DatabaseType;
}>();
const emit = defineEmits<{
@@ -41,6 +44,8 @@ let jsonPreviewEditor: UseCellDetailEditorReturn | null = null;
const jsonFormatted = computed(() => settingsStore.editorSettings.cellDetailJsonFormatted);
const jsonView = computed(() => jsonFormatted.value && !!props.detail?.formattedJson);
const binaryTextPreview = computed(() => (props.detail && isBlobCellColumnType(props.detail.type) ? binaryCellUtf8Text(props.detail.value, props.detail.type, props.databaseType) : null));
const presentedValuePreview = computed(() => (binaryTextPreview.value === null ? props.detail?.rawValuePreview : props.detail?.displayValuePreview) ?? "");
function toggleJsonFormatted() {
settingsStore.updateEditorSettings({ cellDetailJsonFormatted: !jsonFormatted.value });
@@ -53,7 +58,7 @@ function copyCurrentValue() {
props.copyText(detail.formattedJson);
return;
}
props.copyText(detail.value === null ? "" : detail.rawValue);
props.copyText(detail.value === null ? "" : (binaryTextPreview.value ?? detail.rawValue));
}
function copyColumnName() {
@@ -186,7 +191,7 @@ watch(
<img :src="detail.imagePreviewUrl" :alt="detail.column" loading="lazy" decoding="async" referrerpolicy="no-referrer" class="max-h-72 w-full object-contain" />
</a>
<div v-if="jsonView && detail.formattedJson" ref="jsonPreviewContainer" data-cell-detail-editor-root class="h-[44vh] min-h-60 overflow-hidden rounded border bg-muted/20 p-3" />
<pre v-else class="dbx-data-grid-value-font max-h-[44vh] overflow-auto rounded border bg-muted/20 p-3 text-xs whitespace-pre-wrap break-words" :class="{ 'italic text-muted-foreground': detail.value === null }">{{ detail.rawValuePreview }}</pre>
<pre v-else class="dbx-data-grid-value-font max-h-[44vh] overflow-auto rounded border bg-muted/20 p-3 text-xs whitespace-pre-wrap break-words" :class="{ 'italic text-muted-foreground': detail.value === null }">{{ presentedValuePreview }}</pre>
<div v-if="detail.isValuePreviewTruncated && !jsonView" class="text-[11px] text-muted-foreground">
{{ t("grid.largeValuePreviewHint", { count: detail.rawValuePreview.length }) }}
</div>
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { toRef } from "vue";
import { computed, toRef } from "vue";
import { Code2, Copy, Download, Eye, FileUp, Pencil, X } from "@lucide/vue";
import { useI18n } from "vue-i18n";
import { Button } from "@/components/ui/button";
@@ -8,11 +8,12 @@ import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover
import { TabsContent } from "@/components/ui/tabs";
import TemporalCellEditor from "@/components/grid/TemporalCellEditor.vue";
import { useDataGridCellDetail } from "@/composables/useDataGridCellDetail";
import { BINARY_CELL_DOWNLOAD_MODES, type BinaryCellDownloadMode } from "@/lib/dataGrid/binaryCellDownload";
import { BINARY_CELL_DOWNLOAD_MODES, binaryCellUtf8Text, isBlobCellColumnType, type BinaryCellDownloadMode } from "@/lib/dataGrid/binaryCellDownload";
import { isGeometryColumnType } from "@/lib/dataGrid/cellDetailPresentation";
import { isHexGeometry } from "@/lib/dataGrid/geometryPreview";
import type { DataGridCellDetail } from "@/lib/dataGrid/dataGridDetail";
import type { TemporalCellEditorConfig } from "@/lib/dataGrid/dataGridTemporalEditor";
import type { DatabaseType } from "@/types/database";
const { t } = useI18n();
@@ -34,6 +35,8 @@ const props = defineProps<{
importBinaryValue: (detail: DataGridCellDetail | null) => void | Promise<void>;
openImagePreview: (src: string, title: string) => void;
canCopySqlCondition: () => boolean;
/** BLOB 文本预览与编辑写回一致,仅在 MySQL 连接开启。 */
databaseType?: DatabaseType;
}>();
const detailEditValue = defineModel<string>("value", { default: "" });
@@ -55,6 +58,9 @@ void geometryCanvas;
void detailsEditorContainer;
void sideJsonPreviewContainer;
const binaryTextPreview = computed(() => (isBlobCellColumnType(props.detail.type) ? binaryCellUtf8Text(props.detail.value, props.detail.type, props.databaseType) : null));
const presentedValuePreview = computed(() => (binaryTextPreview.value === null ? props.detail.rawValuePreview : props.detail.displayValuePreview));
function startJsonEditFromBlankArea(event: MouseEvent) {
const target = event.target as { closest?: (selector: string) => Element | null } | null;
const line = target?.closest?.(".cm-line");
@@ -181,7 +187,7 @@ defineExpose({ openSearch });
class="dbx-data-grid-value-font overflow-auto rounded border bg-muted/20 p-2 text-xs whitespace-pre-wrap break-words cursor-pointer hover:border-primary/50"
:class="[{ 'cursor-text': detail.isEditable }, panelIsBottom && detail.imagePreviewUrl ? 'min-h-24 max-h-32 shrink-0' : '', valueFillsHeight && !detail.imagePreviewUrl ? 'min-h-0 flex-1' : '']"
@dblclick="emit('startEdit')"
>{{ detail.rawValuePreview }}</pre
>{{ presentedValuePreview }}</pre
>
<div v-if="detail.isValuePreviewTruncated && !sideJsonView" class="text-[11px] text-muted-foreground">{{ t("grid.largeValuePreviewHint", { count: detail.rawValuePreview.length }) }}</div>
</div>
@@ -1184,6 +1184,105 @@ describe("DataGridTextFilterWorkbench", () => {
});
describe("cell detail surfaces", () => {
it("presents printable LONG BLOB bytes as text and copies the presented value", async () => {
const copyText = vi.fn();
const blobDetail = detail({
type: "LONGBLOB",
value: "0x2332303035383035",
rawValue: "0x2332303035383035",
rawValuePreview: "0x2332303035383035",
displayValue: "#2005805",
displayValuePreview: "#2005",
formattedJson: "",
});
const dialog = mountComponent(DataGridCellDetailDialog, {
open: true,
detail: blobDetail,
typeColorClass: () => "",
openImagePreview: vi.fn(),
copyText,
canDownloadBinaryValue: () => true,
downloadBinaryValue: vi.fn(),
canImportBinaryValue: () => false,
importBinaryValue: vi.fn(),
databaseType: "mysql",
});
const panel = mountComponent(DataGridCellDetailPanel, {
detail: blobDetail,
panelIsBottom: true,
metadataCollapsed: false,
valueFillsHeight: false,
editing: false,
sideJsonView: false,
showCompactJson: false,
canCompactJson: false,
typeColorClass: () => "",
canDownloadBinaryValue: () => true,
downloadBinaryValue: vi.fn(),
canImportBinaryValue: () => false,
importBinaryValue: vi.fn(),
openImagePreview: vi.fn(),
canCopySqlCondition: () => true,
databaseType: "mysql",
});
expect(hostText(dialog.root)).toContain("#2005");
expect(hostText(dialog.root)).not.toContain("#2005805");
expect(hostText(panel.root)).toContain("#2005");
expect(hostText(panel.root)).not.toContain("#2005805");
dispatch(
findOne(dialog.root, (node) => node.props.title === "grid.copyValue"),
"click",
);
expect(copyText).toHaveBeenCalledWith("#2005805");
});
it("keeps non-MySQL BLOB detail previews in hex", () => {
const panel = mountComponent(DataGridCellDetailPanel, {
detail: detail({ type: "BLOB", value: "0x2332303035383035", rawValue: "0x2332303035383035", rawValuePreview: "0x2332303035383035", displayValue: "BLOB [8 bytes]", displayValuePreview: "BLOB [8 bytes]", formattedJson: "" }),
panelIsBottom: true,
metadataCollapsed: false,
valueFillsHeight: false,
editing: false,
sideJsonView: false,
showCompactJson: false,
canCompactJson: false,
typeColorClass: () => "",
canDownloadBinaryValue: () => true,
downloadBinaryValue: vi.fn(),
canImportBinaryValue: () => false,
importBinaryValue: vi.fn(),
openImagePreview: vi.fn(),
canCopySqlCondition: () => true,
databaseType: "sqlite",
});
expect(hostText(panel.root)).toContain("0x2332303035383035");
});
it("keeps non-text LONG BLOB values in hex", () => {
const panel = mountComponent(DataGridCellDetailPanel, {
detail: detail({ type: "LONGBLOB", value: "0x89504e470d0a1a0a", rawValue: "0x89504e470d0a1a0a", rawValuePreview: "0x89504e470d0a1a0a", formattedJson: "" }),
panelIsBottom: true,
metadataCollapsed: false,
valueFillsHeight: false,
editing: false,
sideJsonView: false,
showCompactJson: false,
canCompactJson: false,
typeColorClass: () => "",
canDownloadBinaryValue: () => true,
downloadBinaryValue: vi.fn(),
canImportBinaryValue: () => false,
importBinaryValue: vi.fn(),
openImagePreview: vi.fn(),
canCopySqlCondition: () => true,
databaseType: "mysql",
});
expect(hostText(panel.root)).toContain("0x89504e470d0a1a0a");
});
it("copies the presented value, emits edit, closes, and replaces the JSON result", async () => {
const copyText = vi.fn();
const edit = vi.fn();