diff --git a/src/components/grid/DataGrid.vue b/src/components/grid/DataGrid.vue index 0bd87cd10..99f92a989 100644 --- a/src/components/grid/DataGrid.vue +++ b/src/components/grid/DataGrid.vue @@ -63,6 +63,7 @@ import { buildTableSelectSql, quoteTableIdentifier } from "@/lib/tableSelectSql" import { isHiddenGridColumn, usesSyntheticRowIdKey } from "@/lib/tableEditing"; import { formatGridSqlLiteral } from "@/lib/dataGridSql"; import { matchesRowStatusFilter, type RowStatus, type RowStatusFilter } from "@/lib/gridRowStatus"; +import { displayCellValue, type CellValue } from "@/lib/cellValue"; import { useToast } from "@/composables/useToast"; import { useDataGridExport } from "@/composables/useDataGridExport"; @@ -817,7 +818,6 @@ function changePageSize(size: number) { } // --- Editing (composable) --- -type CellValue = string | number | boolean | null; interface RowItem { id: number; @@ -1041,7 +1041,7 @@ const activeCellDetail = computed(() => { const column = props.result.columns[cell.col]; if (!item || !column) return null; const value = item.data[cell.col] ?? null; - const rawValue = formatCell(value); + const rawValue = displayCellValue(value); const valueText = value === null ? "" : typeof value === "object" ? JSON.stringify(value) : String(value); const trimmed = valueText.trim(); const maybeJson = typeof value === "string" && (trimmed.startsWith("{") || trimmed.startsWith("[")); @@ -1339,7 +1339,6 @@ const { selectedCells, contextCell: exportContextCell, getRowItem: (rowId: number) => visibleDisplayItems.value.find((item) => item.id === rowId), - formatCell, quoteIdent, escapeVal, }); @@ -1419,8 +1418,10 @@ async function onGridKeydown(event: KeyboardEvent) { } function copyDetailValue() { - if (!activeCellDetail.value) return; - copyText(activeCellDetail.value.rawValue); + const detail = activeCellDetail.value; + if (!detail) return; + const text = detail.value === null ? "" : displayCellValue(detail.value); + copyText(text); } function copyDetailColumnName() { @@ -2326,7 +2327,8 @@ defineExpose({
{{ activeCellDetail.formattedJson }}
+ {{ activeCellDetail.formattedJson }}
@@ -2618,9 +2620,11 @@ defineExpose({
color: oklch(0.6 0.15 250);
font-weight: 600;
}
+
.ddl-code :deep(.ddl-ident) {
color: oklch(0.65 0.15 150);
}
+
.ddl-code :deep(.ddl-str) {
color: oklch(0.65 0.15 50);
}
diff --git a/src/composables/useDataGridExport.ts b/src/composables/useDataGridExport.ts
index be253548c..0c9a9f002 100644
--- a/src/composables/useDataGridExport.ts
+++ b/src/composables/useDataGridExport.ts
@@ -10,8 +10,7 @@ import {
type SelectionData,
} from "@/lib/gridSelection";
import { useToast } from "@/composables/useToast";
-
-type CellValue = string | number | boolean | null;
+import { displayCellValue, type CellValue } from "@/lib/cellValue";
interface RowItem {
id: number;
@@ -36,7 +35,6 @@ export interface UseDataGridExportOptions {
| Ref<{ rowId: number; rowIndex: number; col: number } | null>
| ComputedRef<{ rowId: number; rowIndex: number; col: number } | null>;
getRowItem: (rowId: number) => RowItem | undefined;
- formatCell: (value: CellValue) => string;
quoteIdent: (name: string) => string;
escapeVal: (value: CellValue) => string;
}
@@ -54,7 +52,6 @@ export function useDataGridExport(options: UseDataGridExportOptions) {
selectedCells,
contextCell,
getRowItem,
- formatCell,
quoteIdent,
escapeVal,
} = options;
@@ -90,7 +87,7 @@ export function useDataGridExport(options: UseDataGridExportOptions) {
if (!contextCell.value || contextCell.value.col < 0) return;
const item = getRowItem(contextCell.value.rowId);
const val = item?.data[contextCell.value.col] ?? null;
- copyText(formatCell(val));
+ copyText(displayCellValue(val));
}
function copyRow() {
@@ -118,7 +115,7 @@ export function useDataGridExport(options: UseDataGridExportOptions) {
function copyAll() {
const header = columns.value.join("\t");
- const body = displayItems.value.map((item) => item.data.map((c) => formatCell(c)).join("\t")).join("\n");
+ const body = displayItems.value.map((item) => item.data.map((c) => displayCellValue(c)).join("\t")).join("\n");
copyText(`${header}\n${body}`);
}
@@ -184,7 +181,7 @@ export function useDataGridExport(options: UseDataGridExportOptions) {
// --- Export functions ---
async function exportCsv() {
try {
- const rows = displayItems.value.map((item) => item.data.map((c) => formatCell(c)));
+ const rows = displayItems.value.map((item) => item.data.map((c) => displayCellValue(c)));
if (await saveFileContent(formatCsv(columns.value, rows), "export.csv", "CSV", "csv")) {
toast(t("grid.exported"));
}
diff --git a/src/lib/cellValue.ts b/src/lib/cellValue.ts
new file mode 100644
index 000000000..b3403622b
--- /dev/null
+++ b/src/lib/cellValue.ts
@@ -0,0 +1,8 @@
+export type CellValue = string | number | boolean | null;
+
+export function displayCellValue(value: CellValue): string {
+ if (value === null) return "NULL";
+ if (typeof value === "boolean") return value ? "true" : "false";
+ if (typeof value === "object") return JSON.stringify(value);
+ return String(value);
+}
diff --git a/tests/cellValue.test.ts b/tests/cellValue.test.ts
new file mode 100644
index 000000000..c35fc063c
--- /dev/null
+++ b/tests/cellValue.test.ts
@@ -0,0 +1,52 @@
+import { strict as assert } from "node:assert";
+import test from "node:test";
+import { displayCellValue } from "../src/lib/cellValue.ts";
+
+test("displayCellValue returns NULL for null", () => {
+ assert.equal(displayCellValue(null), "NULL");
+});
+
+test("displayCellValue returns string representation for booleans", () => {
+ assert.equal(displayCellValue(true), "true");
+ assert.equal(displayCellValue(false), "false");
+});
+
+test("displayCellValue returns String for numbers", () => {
+ assert.equal(displayCellValue(42), "42");
+ assert.equal(displayCellValue(-3.14), "-3.14");
+ assert.equal(displayCellValue(0), "0");
+});
+
+test("displayCellValue returns String for strings", () => {
+ assert.equal(displayCellValue("hello"), "hello");
+ assert.equal(displayCellValue(""), "");
+});
+
+test("displayCellValue does not truncate long strings", () => {
+ const long = "a".repeat(1000);
+ assert.equal(displayCellValue(long), long);
+});
+
+test("displayCellValue serializes JSON strings containing complex nested objects", () => {
+ const payload = '{"metadata":{"version":"2.1.0","tags":["alpha","beta"],"flags":{"enabled":true,"nested":{"depth":3,"items":[{"id":1,"label":"a"},{"id":2,"label":"b"}]}}},"data":{"records":[{"key":"k-001","value":42},{"key":"k-002","value":-999}],"summary":{"total":3,"valid":2}}}';
+ assert.equal(displayCellValue(payload), payload);
+});
+
+test("displayCellValue handles strings with unicode and special characters", () => {
+ const unicodeStr = "δ½ ε₯½δΈη ππ