diff --git a/apps/desktop/src/components/grid/DataGrid.vue b/apps/desktop/src/components/grid/DataGrid.vue index de36ece1c..8ce90bdf9 100644 --- a/apps/desktop/src/components/grid/DataGrid.vue +++ b/apps/desktop/src/components/grid/DataGrid.vue @@ -138,6 +138,7 @@ import { dataGridColumnDetailTsv, dataGridRowDetailJson, dataGridRowDetailTsv, + filterDataGridDetailFields, type DataGridCellDetail, } from "@/lib/dataGridDetail"; import { @@ -428,6 +429,9 @@ const rowDetailDialogOpen = ref(false); const rowDetailDialogRowId = ref(null); const columnDetailDialogOpen = ref(false); const columnDetailDialogColumnIndex = ref(null); +const cellDetailJsonView = ref(false); +const rowDetailSearch = ref(""); +const columnDetailSearch = ref(""); const isResizingDetail = ref(false); const imagePreviewOpen = ref(false); const imagePreviewSrc = ref(""); @@ -2699,16 +2703,31 @@ const columnDetail = computed(() => { }); }); +const filteredRowDetailFields = computed(() => + rowDetail.value ? filterDataGridDetailFields(rowDetail.value.fields, rowDetailSearch.value) : [], +); + +const filteredColumnDetailFields = computed(() => + columnDetail.value ? filterDataGridDetailFields(columnDetail.value.fields, columnDetailSearch.value) : [], +); + watch(cellDetailDialogOpen, (open) => { - if (!open) cellDetailDialogTarget.value = null; + if (open) cellDetailJsonView.value = false; + else cellDetailDialogTarget.value = null; }); watch(rowDetailDialogOpen, (open) => { - if (!open) rowDetailDialogRowId.value = null; + if (!open) { + rowDetailDialogRowId.value = null; + rowDetailSearch.value = ""; + } }); watch(columnDetailDialogOpen, (open) => { - if (!open) columnDetailDialogColumnIndex.value = null; + if (!open) { + columnDetailDialogColumnIndex.value = null; + columnDetailSearch.value = ""; + } }); watch(sideGeometryPreviewOpen, async (open) => { @@ -4469,6 +4488,14 @@ function copyDialogCellFormattedJson() { copyText(detail.formattedJson); } +function copyDialogCellCurrentValue() { + if (cellDetailJsonView.value && dialogCellDetail.value?.formattedJson) { + copyDialogCellFormattedJson(); + } else { + copyDialogCellValue(); + } +} + function copyDialogCellColumnName() { const detail = dialogCellDetail.value; if (!detail) return; @@ -7999,6 +8026,17 @@ const gridContextMenuItems = computed(() => {
{{ t("grid.cellValue") }}
+ @@ -8079,46 +8117,37 @@ const gridContextMenuItems = computed(() => {
{{ dialogCellDetail.rawValuePreview }}
{{ + cellDetailJsonView && dialogCellDetail.formattedJson + ? dialogCellDetail.formattedJson + : dialogCellDetail.rawValuePreview + }} +
-
{{ t("grid.largeValuePreviewHint", { count: dialogCellDetail.rawValuePreview.length }) }}
-
+
{{ t("grid.formattedValue") }}
-
{{
-              dialogCellDetail.displayValuePreview
-            }}
-
- -
-
-
{{ t("grid.formattedJson") }}
- -
{{ dialogCellDetail.formattedJson }}
{{ dialogCellDetail.displayValuePreview }}
-
- -
+
+
@@ -8132,8 +8161,18 @@ const gridContextMenuItems = computed(() => { -
- {{ t("grid.columnsCount", { count: rowDetail.fields.length }) }} +
+ {{ t("grid.columnsCount", { count: rowDetail.fields.length }) }} +
+ + +
@@ -8148,13 +8187,13 @@ const gridContextMenuItems = computed(() => { {{ fieldIndex + 1 }} -
{{ field.column }}
+
{{ field.column }}
(() => { {{ field.comment }}
- +
{{ field.value === null ? t("grid.nullValue") : t("grid.valueLength") }}: {{ field.value === null ? "true" : field.length }} @@ -8216,15 +8255,23 @@ const gridContextMenuItems = computed(() => { +
+ {{ t("grid.detailSearchNoMatch") }} +
- - - + +
+ + +
@@ -8261,6 +8308,20 @@ const gridContextMenuItems = computed(() => {
+
+ {{ t("grid.rowCount") }}: {{ columnDetail.fields.length }} +
+ + +
+
+
@@ -8272,12 +8333,12 @@ const gridContextMenuItems = computed(() => { -
{{ field.rowNumber }} +
{{ field.value === null ? t("grid.nullValue") : t("grid.valueLength") }}: {{ field.value === null ? "true" : field.length }} @@ -8328,6 +8389,12 @@ const gridContextMenuItems = computed(() => {
+
+ {{ t("grid.detailSearchNoMatch") }} +
diff --git a/apps/desktop/src/i18n/locales/en.ts b/apps/desktop/src/i18n/locales/en.ts index 3c835c996..79da2993b 100644 --- a/apps/desktop/src/i18n/locales/en.ts +++ b/apps/desktop/src/i18n/locales/en.ts @@ -595,6 +595,8 @@ export default { noComment: "No comment", formattedJson: "Formatted JSON", formattedValue: "Formatted Value", + detailSearchPlaceholder: "Search field or value…", + detailSearchNoMatch: "No matches", rawValue: "Raw Value", largeValuePreviewHint: "Previewing first {count} characters. Copy still uses the full value.", copyValue: "Copy Value", diff --git a/apps/desktop/src/i18n/locales/es.ts b/apps/desktop/src/i18n/locales/es.ts index 3a731347d..8f07b5d9c 100644 --- a/apps/desktop/src/i18n/locales/es.ts +++ b/apps/desktop/src/i18n/locales/es.ts @@ -561,6 +561,8 @@ export default { noComment: "Sin comentario", formattedJson: "JSON formateado", formattedValue: "Valor formateado", + detailSearchPlaceholder: "Buscar campo o valor…", + detailSearchNoMatch: "Sin coincidencias", rawValue: "Valor sin procesar", largeValuePreviewHint: "Mostrando los primeros {count} caracteres. Copiar sigue usando el valor completo.", copyValue: "Copiar valor", diff --git a/apps/desktop/src/i18n/locales/it.ts b/apps/desktop/src/i18n/locales/it.ts index 914f28991..befa75e25 100644 --- a/apps/desktop/src/i18n/locales/it.ts +++ b/apps/desktop/src/i18n/locales/it.ts @@ -600,6 +600,8 @@ export default { noComment: "Nessun commento", formattedJson: "JSON Formattato", formattedValue: "Valore Formattato", + detailSearchPlaceholder: "Cerca campo o valore…", + detailSearchNoMatch: "Nessun risultato", rawValue: "Valore Grezzo", largeValuePreviewHint: "Anteprima dei primi {count} caratteri. La copia utilizzerà comunque il valore completo.", copyValue: "Copia Valore", diff --git a/apps/desktop/src/i18n/locales/pt-BR.ts b/apps/desktop/src/i18n/locales/pt-BR.ts index 6bbdf0e39..611fa0ee9 100644 --- a/apps/desktop/src/i18n/locales/pt-BR.ts +++ b/apps/desktop/src/i18n/locales/pt-BR.ts @@ -599,6 +599,8 @@ export default { noComment: "Sem comentário", formattedJson: "JSON Formatado", formattedValue: "Valor Formatado", + detailSearchPlaceholder: "Buscar campo ou valor…", + detailSearchNoMatch: "Nenhuma correspondência", rawValue: "Valor Bruto", largeValuePreviewHint: "Visualizando os primeiros {count} caracteres. A cópia ainda usa o valor completo.", copyValue: "Copiar Valor", diff --git a/apps/desktop/src/i18n/locales/zh-CN.ts b/apps/desktop/src/i18n/locales/zh-CN.ts index 6b2b4fa78..dedc15473 100644 --- a/apps/desktop/src/i18n/locales/zh-CN.ts +++ b/apps/desktop/src/i18n/locales/zh-CN.ts @@ -590,6 +590,8 @@ export default { noComment: "暂无注释", formattedJson: "格式化 JSON", formattedValue: "格式化值", + detailSearchPlaceholder: "搜索字段名或值…", + detailSearchNoMatch: "无匹配项", rawValue: "原始值", largeValuePreviewHint: "仅预览前 {count} 个字符,复制仍会使用完整值。", copyValue: "复制值", diff --git a/apps/desktop/src/i18n/locales/zh-TW.ts b/apps/desktop/src/i18n/locales/zh-TW.ts index ffa391e4d..3e55f0ee1 100644 --- a/apps/desktop/src/i18n/locales/zh-TW.ts +++ b/apps/desktop/src/i18n/locales/zh-TW.ts @@ -590,6 +590,8 @@ export default { noComment: "暫無註解", formattedJson: "格式化 JSON", formattedValue: "格式化值", + detailSearchPlaceholder: "搜尋欄位名或值…", + detailSearchNoMatch: "無相符項目", rawValue: "原始值", largeValuePreviewHint: "僅預覽前 {count} 個字元,複製仍會使用完整值。", copyValue: "複製值", diff --git a/apps/desktop/src/lib/dataGridDetail.ts b/apps/desktop/src/lib/dataGridDetail.ts index 7f4994da2..5cf81adfe 100644 --- a/apps/desktop/src/lib/dataGridDetail.ts +++ b/apps/desktop/src/lib/dataGridDetail.ts @@ -189,6 +189,18 @@ export function dataGridColumnDetailTsv(detail: DataGridColumnDetail): string { return detail.fields.map((field) => displayCellValue(field.value)).join("\n"); } +export function filterDataGridDetailFields(fields: readonly T[], keyword: string): T[] { + if (keyword.trim() === "") return [...fields]; + const kw = keyword.trim().toLowerCase(); + return fields.filter( + (field) => + field.column.toLowerCase().includes(kw) || + field.rawValuePreview.toLowerCase().includes(kw) || + field.displayValuePreview.toLowerCase().includes(kw) || + String(field.rowNumber).includes(kw), + ); +} + function looksLikeJsonContainer(text: string): boolean { const trimmed = text.trim(); return trimmed.startsWith("{") || trimmed.startsWith("["); diff --git a/packages/app-tests/dataGridDetail.test.ts b/packages/app-tests/dataGridDetail.test.ts index 322a594c2..be908b933 100644 --- a/packages/app-tests/dataGridDetail.test.ts +++ b/packages/app-tests/dataGridDetail.test.ts @@ -9,6 +9,8 @@ import { buildDataGridRowDetail, dataGridRowDetailJson, dataGridRowDetailTsv, + filterDataGridDetailFields, + type DataGridCellDetail, } from "../../apps/desktop/src/lib/dataGridDetail.ts"; import type { CellValue } from "../../apps/desktop/src/lib/cellValue.ts"; @@ -219,3 +221,57 @@ test("dataGridColumnDetailJson and dataGridColumnDetailTsv format copy payloads" ); assert.equal(dataGridColumnDetailTsv(detail), "Ada\nNULL"); }); + +const detailFields: DataGridCellDetail[] = [ + { rowNumber: 1, column: "id", rawValuePreview: "7", displayValuePreview: "7" } as DataGridCellDetail, + { rowNumber: 2, column: "Email", rawValuePreview: "ada@example.com", displayValuePreview: "ada@example.com" } as DataGridCellDetail, + { rowNumber: 3, column: "status", rawValuePreview: "ACTIVE", displayValuePreview: "Active" } as DataGridCellDetail, +]; + +test("filterDataGridDetailFields returns all fields for an empty keyword", () => { + const result = filterDataGridDetailFields(detailFields, " "); + + assert.deepEqual(result, detailFields); + assert.notEqual(result, detailFields); +}); + +test("filterDataGridDetailFields matches column names case-insensitively", () => { + const result = filterDataGridDetailFields(detailFields, "EMAIL"); + + assert.deepEqual( + result.map((field) => field.column), + ["Email"], + ); +}); + +test("filterDataGridDetailFields matches raw value previews", () => { + const result = filterDataGridDetailFields(detailFields, "example.com"); + + assert.deepEqual( + result.map((field) => field.column), + ["Email"], + ); +}); + +test("filterDataGridDetailFields matches row numbers", () => { + const result = filterDataGridDetailFields(detailFields, "3"); + + assert.deepEqual( + result.map((field) => field.rowNumber), + [3], + ); +}); + +test("filterDataGridDetailFields returns an empty array when nothing matches", () => { + const result = filterDataGridDetailFields(detailFields, "nope"); + + assert.deepEqual(result, []); +}); + +test("filterDataGridDetailFields does not mutate the input array", () => { + const original = [...detailFields]; + const result = filterDataGridDetailFields(detailFields, "id"); + + assert.deepEqual(detailFields, original); + assert.notEqual(result, detailFields); +});