From b9fa36763ead87e023f336e3270d675e41f7d595 Mon Sep 17 00:00:00 2001 From: t8y2 <1156263951@qq.com> Date: Mon, 1 Jun 2026 19:49:53 +0800 Subject: [PATCH] fix(desktop): guard cell detail previews for large values --- apps/desktop/src/components/grid/DataGrid.vue | 35 ++++++++++++++----- .../src/composables/useCellDetailEditor.ts | 3 +- apps/desktop/src/i18n/locales/en.ts | 1 + apps/desktop/src/i18n/locales/es.ts | 1 + apps/desktop/src/i18n/locales/zh-CN.ts | 1 + apps/desktop/src/i18n/locales/zh-TW.ts | 1 + .../desktop/src/lib/cellDetailPresentation.ts | 4 +++ apps/desktop/src/lib/dataGridDetail.ts | 21 +++++++++-- packages/app-tests/dataGridDetail.test.ts | 24 +++++++++++++ 9 files changed, 80 insertions(+), 11 deletions(-) diff --git a/apps/desktop/src/components/grid/DataGrid.vue b/apps/desktop/src/components/grid/DataGrid.vue index 50161e9ca..d732b2aab 100644 --- a/apps/desktop/src/components/grid/DataGrid.vue +++ b/apps/desktop/src/components/grid/DataGrid.vue @@ -6296,14 +6296,24 @@ const gridContextMenuItems = computed(() => { class="max-h-56 overflow-auto rounded border bg-muted/20 p-2 font-mono text-xs whitespace-pre cursor-pointer hover:border-primary/50" :class="{ 'cursor-text': activeCellDetail.isEditable }" @dblclick="startDetailEdit" - >{{ activeCellDetail.displayValue }}{{ activeCellDetail.displayValuePreview }} +
+ {{ + t("grid.largeValuePreviewHint", { + count: activeCellDetail.displayValuePreview.length, + }) + }} +
-
+
{{ t("grid.rawValue") }}
{{ activeCellDetail.rawValue }}
{{ activeCellDetail.rawValuePreview }}
@@ -6634,14 +6644,17 @@ const gridContextMenuItems = computed(() => {
{{ dialogCellDetail.rawValue }}
{{ dialogCellDetail.rawValuePreview }} +
+ {{ t("grid.largeValuePreviewHint", { count: dialogCellDetail.rawValuePreview.length }) }} +
-
+
{{ t("grid.formattedValue") }}
{{
-              dialogCellDetail.displayValue
+              dialogCellDetail.displayValuePreview
             }}
@@ -6741,8 +6754,11 @@ const gridContextMenuItems = computed(() => {
{{ field.rawValue }}
{{ field.rawValuePreview }} +
+ {{ t("grid.largeValuePreviewHint", { count: field.rawValuePreview.length }) }} +
{{ t("grid.formattedJson") }}
(() => {
                   
{{ field.rawValue }}
{{ field.rawValuePreview }}
+
+ {{ t("grid.largeValuePreviewHint", { count: field.rawValuePreview.length }) }} +
{{ t("grid.formattedJson") }}
 CELL_DETAIL_JSON_FORMAT_MAX_LENGTH) return false;
   if (isJsonColumnType(columnType)) return true;
   if (value && looksLikeJsonString(value)) return true;
   return false;
diff --git a/apps/desktop/src/i18n/locales/en.ts b/apps/desktop/src/i18n/locales/en.ts
index c57b0260b..0b878ebd5 100644
--- a/apps/desktop/src/i18n/locales/en.ts
+++ b/apps/desktop/src/i18n/locales/en.ts
@@ -568,6 +568,7 @@ export default {
     formattedJson: "Formatted JSON",
     formattedValue: "Formatted Value",
     rawValue: "Raw Value",
+    largeValuePreviewHint: "Previewing first {count} characters. Copy still uses the full value.",
     copyValue: "Copy Value",
     downloadBinaryValue: "Download Value",
     downloadSaved: "Saved to {path}",
diff --git a/apps/desktop/src/i18n/locales/es.ts b/apps/desktop/src/i18n/locales/es.ts
index 1f7c9c72a..cac33dce9 100644
--- a/apps/desktop/src/i18n/locales/es.ts
+++ b/apps/desktop/src/i18n/locales/es.ts
@@ -539,6 +539,7 @@ export default {
     formattedJson: "JSON formateado",
     formattedValue: "Valor formateado",
     rawValue: "Valor sin procesar",
+    largeValuePreviewHint: "Mostrando los primeros {count} caracteres. Copiar sigue usando el valor completo.",
     copyValue: "Copiar valor",
     downloadBinaryValue: "Descargar valor",
     downloadSaved: "Guardado en {path}",
diff --git a/apps/desktop/src/i18n/locales/zh-CN.ts b/apps/desktop/src/i18n/locales/zh-CN.ts
index c7a5481f8..5a3884102 100644
--- a/apps/desktop/src/i18n/locales/zh-CN.ts
+++ b/apps/desktop/src/i18n/locales/zh-CN.ts
@@ -563,6 +563,7 @@ export default {
     formattedJson: "格式化 JSON",
     formattedValue: "格式化值",
     rawValue: "原始值",
+    largeValuePreviewHint: "仅预览前 {count} 个字符,复制仍会使用完整值。",
     copyValue: "复制值",
     downloadBinaryValue: "下载值",
     downloadSaved: "已保存到 {path}",
diff --git a/apps/desktop/src/i18n/locales/zh-TW.ts b/apps/desktop/src/i18n/locales/zh-TW.ts
index cdd845b5c..1007c76c6 100644
--- a/apps/desktop/src/i18n/locales/zh-TW.ts
+++ b/apps/desktop/src/i18n/locales/zh-TW.ts
@@ -563,6 +563,7 @@ export default {
     formattedJson: "格式化 JSON",
     formattedValue: "格式化值",
     rawValue: "原始值",
+    largeValuePreviewHint: "僅預覽前 {count} 個字元,複製仍會使用完整值。",
     copyValue: "複製值",
     downloadBinaryValue: "下載值",
     downloadSaved: "已儲存到 {path}",
diff --git a/apps/desktop/src/lib/cellDetailPresentation.ts b/apps/desktop/src/lib/cellDetailPresentation.ts
index 8e23e7341..01fb862fb 100644
--- a/apps/desktop/src/lib/cellDetailPresentation.ts
+++ b/apps/desktop/src/lib/cellDetailPresentation.ts
@@ -1,6 +1,8 @@
 export type CellDetailTab = "details" | "valueEditor";
 export type ValueEditorAction = "formatJson" | "setNull" | "restoreOriginal";
 
+export const CELL_DETAIL_JSON_FORMAT_MAX_LENGTH = 100_000;
+
 export interface CellDetailPresentationOptions {
   isEditable: boolean;
 }
@@ -66,6 +68,7 @@ export function isJsonColumnType(columnType: string | undefined): boolean {
 export function canFormatCellDetailJson(value: unknown, columnType?: string): boolean {
   if (value === null || value === undefined) return false;
   const text = cellDetailRawEditorText(value);
+  if (text.length > CELL_DETAIL_JSON_FORMAT_MAX_LENGTH) return false;
   if (isJsonColumnType(columnType)) return !!formatJsonText(text);
   return typeof value === "string" && looksLikeJsonContainer(text) && !!formatJsonText(text);
 }
@@ -73,6 +76,7 @@ export function canFormatCellDetailJson(value: unknown, columnType?: string): bo
 export function formatJsonText(text: string): string | undefined {
   const trimmed = text.trim();
   if (!trimmed) return undefined;
+  if (trimmed.length > CELL_DETAIL_JSON_FORMAT_MAX_LENGTH) return undefined;
   try {
     return JSON.stringify(JSON.parse(trimmed), null, 2);
   } catch {
diff --git a/apps/desktop/src/lib/dataGridDetail.ts b/apps/desktop/src/lib/dataGridDetail.ts
index 614f5971c..7f4994da2 100644
--- a/apps/desktop/src/lib/dataGridDetail.ts
+++ b/apps/desktop/src/lib/dataGridDetail.ts
@@ -2,6 +2,8 @@ import { cellImagePreviewUrl } from "@/lib/cellImageUrl";
 import { displayCellValue, type CellValue } from "@/lib/cellValue";
 import { formatJsonText } from "@/lib/cellDetailPresentation";
 
+export const CELL_DETAIL_VALUE_PREVIEW_MAX_LENGTH = 12_000;
+
 export interface DataGridCellDetail {
   rowNumber: number;
   rowId: number;
@@ -11,7 +13,10 @@ export interface DataGridCellDetail {
   comment: string;
   value: CellValue;
   rawValue: string;
+  rawValuePreview: string;
   displayValue: string;
+  displayValuePreview: string;
+  isValuePreviewTruncated: boolean;
   imagePreviewUrl: string | null;
   length: number;
   formattedJson: string;
@@ -78,7 +83,10 @@ export function buildDataGridCellDetail(options: BuildDataGridCellDetailOptions)
 
   const value = options.row[options.columnIndex] ?? null;
   const rawValue = displayCellValue(value);
+  const displayValue = options.displayValue(value, options.columnIndex);
   const formattedJson = typeof value === "string" && looksLikeJsonContainer(value) ? (formatJsonText(value) ?? "") : "";
+  const rawValuePreview = previewText(rawValue);
+  const displayValuePreview = previewText(displayValue);
 
   return {
     rowNumber: options.rowIndex + 1,
@@ -89,8 +97,12 @@ export function buildDataGridCellDetail(options: BuildDataGridCellDetailOptions)
     comment: options.commentByColumn?.get(column) ?? "",
     value,
     rawValue,
-    displayValue: options.displayValue(value, options.columnIndex),
-    imagePreviewUrl: cellImagePreviewUrl(value),
+    rawValuePreview,
+    displayValue,
+    displayValuePreview,
+    isValuePreviewTruncated:
+      rawValuePreview.length < rawValue.length || displayValuePreview.length < displayValue.length,
+    imagePreviewUrl: rawValue.length <= CELL_DETAIL_VALUE_PREVIEW_MAX_LENGTH ? cellImagePreviewUrl(value) : null,
     length: value === null ? 0 : String(value).length,
     formattedJson,
     isEditable: options.isEditable,
@@ -181,3 +193,8 @@ function looksLikeJsonContainer(text: string): boolean {
   const trimmed = text.trim();
   return trimmed.startsWith("{") || trimmed.startsWith("[");
 }
+
+function previewText(text: string): string {
+  if (text.length <= CELL_DETAIL_VALUE_PREVIEW_MAX_LENGTH) return text;
+  return text.slice(0, CELL_DETAIL_VALUE_PREVIEW_MAX_LENGTH);
+}
diff --git a/packages/app-tests/dataGridDetail.test.ts b/packages/app-tests/dataGridDetail.test.ts
index 63e3680a9..322a594c2 100644
--- a/packages/app-tests/dataGridDetail.test.ts
+++ b/packages/app-tests/dataGridDetail.test.ts
@@ -50,7 +50,10 @@ test("buildDataGridCellDetail preserves full value metadata", () => {
     comment: "raw event payload",
     value: '{"ok":true,"items":[1,2]}',
     rawValue: '{"ok":true,"items":[1,2]}',
+    rawValuePreview: '{"ok":true,"items":[1,2]}',
     displayValue: 'formatted:{"ok":tr',
+    displayValuePreview: 'formatted:{"ok":tr',
+    isValuePreviewTruncated: false,
     imagePreviewUrl: null,
     length: 25,
     formattedJson: '{\n  "ok": true,\n  "items": [\n    1,\n    2\n  ]\n}',
@@ -72,6 +75,27 @@ test("buildDataGridCellDetail reports image preview URLs", () => {
   assert.equal(detail?.imagePreviewUrl, "https://example.com/avatar.png");
 });
 
+test("buildDataGridCellDetail limits rendered previews for huge text values", () => {
+  const hugeJson = `{"body":"${"x".repeat(120_000)}"}`;
+  const detail = buildDataGridCellDetail({
+    rowIndex: 0,
+    rowId: 1,
+    row: [hugeJson],
+    columns: ["payload"],
+    columnIndex: 0,
+    typeByColumn: new Map([["payload", "nvarchar(max)"]]),
+    displayValue: (value) => String(value),
+    isEditable: false,
+  });
+
+  assert.ok(detail);
+  assert.equal(detail.rawValue, hugeJson);
+  assert.equal(detail.rawValuePreview.length, 12_000);
+  assert.equal(detail.displayValuePreview.length, 12_000);
+  assert.equal(detail.isValuePreviewTruncated, true);
+  assert.equal(detail.formattedJson, "");
+});
+
 test("buildDataGridRowDetail maps requested column indexes in order", () => {
   const row: CellValue[] = ["Ada", null, true];
   const detail = buildDataGridRowDetail({