fix(mongodb): expand nested JSON strings when copying rows as JSON
This commit is contained in:
parent
56fb49058f
commit
585a6e2d40
|
|
@ -11,6 +11,7 @@ import { buildDataGridCopyInsertStatement, buildDataGridCopyUpdateStatements, ty
|
|||
import { formatSqlInsert } from "@/lib/exportFormats";
|
||||
import { uuid } from "@/lib/utils";
|
||||
import { useSettingsStore } from "@/stores/settingsStore";
|
||||
import { expandNestedJsonStringsForCopy } from "@/lib/jsonCopyValue";
|
||||
import type { DatabaseType, QueryResult } from "@/types/database";
|
||||
import type { QueryResultExportRequest } from "@/lib/api";
|
||||
|
||||
|
|
@ -395,7 +396,8 @@ export function useDataGridExport(options: UseDataGridExportOptions) {
|
|||
async function copyRowsAsJson(items: RowItem[]) {
|
||||
if (items.length === 0) return;
|
||||
const value = items.length === 1 ? rowToJsonObject(items[0]) : items.map(rowToJsonObject);
|
||||
await copyText(JSON.stringify(value, null, 2));
|
||||
const copyValue = options.databaseType.value === "mongodb" ? expandNestedJsonStringsForCopy(value) : value;
|
||||
await copyText(JSON.stringify(copyValue, null, 2));
|
||||
}
|
||||
|
||||
// --- Cell/row copy ---
|
||||
|
|
|
|||
|
|
@ -0,0 +1,30 @@
|
|||
export function expandNestedJsonStringsForCopy(value: unknown): unknown {
|
||||
if (typeof value === "string") {
|
||||
return parseNestedJsonString(value) ?? value;
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
return value.map(expandNestedJsonStringsForCopy);
|
||||
}
|
||||
|
||||
if (isPlainObject(value)) {
|
||||
return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, expandNestedJsonStringsForCopy(item)]));
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
function parseNestedJsonString(value: string): unknown | undefined {
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed || (trimmed[0] !== "{" && trimmed[0] !== "[")) return undefined;
|
||||
|
||||
try {
|
||||
return expandNestedJsonStringsForCopy(JSON.parse(trimmed));
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
}
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
import { strict as assert } from "node:assert";
|
||||
import { test } from "vitest";
|
||||
import { expandNestedJsonStringsForCopy } from "../../apps/desktop/src/lib/jsonCopyValue.ts";
|
||||
|
||||
test("expands nested JSON strings for copied rows", () => {
|
||||
const value = {
|
||||
_id: "67218700e884ae1f527640b6",
|
||||
accountId: 581,
|
||||
data: '{"endingBalance":{"beginningBalance":"0","endingBalance":"20000","endingDate":"2024-10-30"},"financeChargeInfo":null,"interestChargeInfo":null,"Line":[]}',
|
||||
status: "draft",
|
||||
};
|
||||
|
||||
assert.deepEqual(expandNestedJsonStringsForCopy(value), {
|
||||
_id: "67218700e884ae1f527640b6",
|
||||
accountId: 581,
|
||||
data: {
|
||||
endingBalance: {
|
||||
beginningBalance: "0",
|
||||
endingBalance: "20000",
|
||||
endingDate: "2024-10-30",
|
||||
},
|
||||
financeChargeInfo: null,
|
||||
interestChargeInfo: null,
|
||||
Line: [],
|
||||
},
|
||||
status: "draft",
|
||||
});
|
||||
});
|
||||
|
||||
test("recursively expands JSON strings in arrays and objects", () => {
|
||||
const value = {
|
||||
items: ['{"id":1,"meta":"{\\"ok\\":true}"}', "plain text"],
|
||||
};
|
||||
|
||||
assert.deepEqual(expandNestedJsonStringsForCopy(value), {
|
||||
items: [{ id: 1, meta: { ok: true } }, "plain text"],
|
||||
});
|
||||
});
|
||||
|
||||
test("keeps non-object JSON-like cell strings unchanged", () => {
|
||||
assert.equal(expandNestedJsonStringsForCopy("123"), "123");
|
||||
assert.equal(expandNestedJsonStringsForCopy('"text"'), '"text"');
|
||||
assert.equal(expandNestedJsonStringsForCopy("2024-10-30T01:08:14.454Z"), "2024-10-30T01:08:14.454Z");
|
||||
});
|
||||
|
|
@ -15,8 +15,12 @@ const apiMock = vi.hoisted(() => ({
|
|||
exportQueryResultMarkdown: vi.fn(),
|
||||
exportQueryResultsXlsx: vi.fn(),
|
||||
}));
|
||||
const clipboardMock = vi.hoisted(() => ({
|
||||
copyToClipboard: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/api", () => apiMock);
|
||||
vi.mock("@/lib/clipboard", () => clipboardMock);
|
||||
vi.mock("@/lib/tauriRuntime", () => ({ isTauriRuntime: () => false }));
|
||||
vi.mock("@/composables/useToast", () => ({ useToast: () => ({ toast: vi.fn() }) }));
|
||||
vi.mock("vue-i18n", () => ({ useI18n: () => ({ t: (key: string) => key }) }));
|
||||
|
|
@ -184,6 +188,7 @@ function buildTableDataExportHarness() {
|
|||
beforeEach(() => {
|
||||
setActivePinia(createPinia());
|
||||
vi.clearAllMocks();
|
||||
clipboardMock.copyToClipboard.mockResolvedValue(undefined);
|
||||
apiMock.startQueryResultExport.mockImplementation(async (_request, onProgress) => {
|
||||
onProgress({ exportId: _request.exportId, tableName: "", rowsExported: 2, totalRows: 2, status: "Done" });
|
||||
return { exportId: _request.exportId, tableName: "", rowsExported: 2, totalRows: 2, status: "Done" };
|
||||
|
|
@ -194,6 +199,101 @@ beforeEach(() => {
|
|||
});
|
||||
});
|
||||
|
||||
test("copy row JSON expands nested JSON strings", async () => {
|
||||
const contextCell = ref({ rowId: 1, rowIndex: 0, col: 0 });
|
||||
const jsonString = '{"endingBalance":{"beginningBalance":"0","endingBalance":"20000","endingDate":"2024-10-30"},"financeChargeInfo":null,"interestChargeInfo":null,"Line":[]}';
|
||||
const row = {
|
||||
id: 1,
|
||||
data: ["67218700e884ae1f527640b6", jsonString, "draft"],
|
||||
isNew: false,
|
||||
isDeleted: false,
|
||||
isDirtyCol: [false, false, false],
|
||||
status: "",
|
||||
};
|
||||
const composable = useDataGridExport({
|
||||
columns: computed(() => ["_id", "data", "status"]),
|
||||
displayItems: computed(() => [row]),
|
||||
sql: computed(() => undefined),
|
||||
tableMeta: computed(() => undefined),
|
||||
databaseType: computed(() => "mongodb"),
|
||||
connectionId: computed(() => "conn-1"),
|
||||
database: computed(() => "db"),
|
||||
context: computed(() => "results"),
|
||||
sourceColumns: computed(() => undefined),
|
||||
columnTypes: computed(() => undefined),
|
||||
whereInput: computed(() => undefined),
|
||||
orderBy: computed(() => undefined),
|
||||
exportBatchSize: computed(() => 1000),
|
||||
hasCellSelection: computed(() => false),
|
||||
selectedCells: computed(() => ({ columns: [], rows: [] })),
|
||||
selectedRange: computed(() => null),
|
||||
contextCell,
|
||||
getRowItem: () => row,
|
||||
selectedRowIds: ref(new Set<number>()),
|
||||
hasRowSelection: computed(() => false),
|
||||
});
|
||||
|
||||
await composable.copyRow();
|
||||
|
||||
assert.equal(clipboardMock.copyToClipboard.mock.calls.length, 1);
|
||||
assert.deepEqual(JSON.parse(clipboardMock.copyToClipboard.mock.calls[0][0]), {
|
||||
_id: "67218700e884ae1f527640b6",
|
||||
data: {
|
||||
endingBalance: {
|
||||
beginningBalance: "0",
|
||||
endingBalance: "20000",
|
||||
endingDate: "2024-10-30",
|
||||
},
|
||||
financeChargeInfo: null,
|
||||
interestChargeInfo: null,
|
||||
Line: [],
|
||||
},
|
||||
status: "draft",
|
||||
});
|
||||
});
|
||||
|
||||
test("copy row JSON keeps nested JSON strings for non-MongoDB rows", async () => {
|
||||
const contextCell = ref({ rowId: 1, rowIndex: 0, col: 0 });
|
||||
const jsonString = '{"enabled":true}';
|
||||
const row = {
|
||||
id: 1,
|
||||
data: [1, jsonString],
|
||||
isNew: false,
|
||||
isDeleted: false,
|
||||
isDirtyCol: [false, false],
|
||||
status: "",
|
||||
};
|
||||
const composable = useDataGridExport({
|
||||
columns: computed(() => ["id", "payload"]),
|
||||
displayItems: computed(() => [row]),
|
||||
sql: computed(() => undefined),
|
||||
tableMeta: computed(() => undefined),
|
||||
databaseType: computed(() => "mysql"),
|
||||
connectionId: computed(() => "conn-1"),
|
||||
database: computed(() => "db"),
|
||||
context: computed(() => "table"),
|
||||
sourceColumns: computed(() => undefined),
|
||||
columnTypes: computed(() => undefined),
|
||||
whereInput: computed(() => undefined),
|
||||
orderBy: computed(() => undefined),
|
||||
exportBatchSize: computed(() => 1000),
|
||||
hasCellSelection: computed(() => false),
|
||||
selectedCells: computed(() => ({ columns: [], rows: [] })),
|
||||
selectedRange: computed(() => null),
|
||||
contextCell,
|
||||
getRowItem: () => row,
|
||||
selectedRowIds: ref(new Set<number>()),
|
||||
hasRowSelection: computed(() => false),
|
||||
});
|
||||
|
||||
await composable.copyRow();
|
||||
|
||||
assert.deepEqual(JSON.parse(clipboardMock.copyToClipboard.mock.calls[0][0]), {
|
||||
id: 1,
|
||||
payload: jsonString,
|
||||
});
|
||||
});
|
||||
|
||||
test("default data grid export file names use sanitized base names and compact local timestamps", () => {
|
||||
vi.useFakeTimers();
|
||||
try {
|
||||
|
|
|
|||
Loading…
Reference in New Issue