fix(grid): copy NULL cells as empty fields
This commit is contained in:
11 files changed
+113
-16
No files matched your search
@@ -730,6 +730,66 @@ describe("useDataGridExport prepared row statements", () => {
|
||||
expect(copyToClipboard).toHaveBeenCalledWith(text);
|
||||
});
|
||||
|
||||
it("copies a NULL cell as an empty clipboard value", async () => {
|
||||
const state = createExportState(editableTable, ["id", "name"], undefined, [1, null], undefined, undefined, [], DEFAULT_DATA_GRID_EXTRACTOR_OPTIONS, false, undefined, false, 1, 1);
|
||||
|
||||
await state.copyCell();
|
||||
|
||||
expect(copyToClipboard).toHaveBeenCalledWith("");
|
||||
});
|
||||
|
||||
it("copies all rows with empty fields for NULL cells", async () => {
|
||||
const text = "id\tname\n1\t\n2\tAda";
|
||||
const state = createExportState(editableTable, ["id", "name"], undefined, undefined, undefined, [
|
||||
[1, null],
|
||||
[2, "Ada"],
|
||||
]);
|
||||
|
||||
await state.copyAll();
|
||||
|
||||
expect(copyToClipboard).toHaveBeenCalledWith(text);
|
||||
expect(parseDataGridClipboard(text)).toEqual([
|
||||
["id", "name"],
|
||||
["1", null],
|
||||
["2", "Ada"],
|
||||
]);
|
||||
});
|
||||
|
||||
it("copies a NULL smart single-cell selection as an empty value", async () => {
|
||||
const matrix: CellSelectionMatrix = {
|
||||
rowIndexes: [0],
|
||||
columnIndexes: [1],
|
||||
columns: ["name"],
|
||||
rows: [[null]],
|
||||
};
|
||||
vi.mocked(extractDataGridSelection).mockResolvedValueOnce({ text: "", mimeType: "text/plain", fileExtension: "txt", rowCount: 1, columnCount: 1 });
|
||||
const state = createExportState(editableTable, ["id", "name"], matrix, [1, null]);
|
||||
|
||||
await expect(state.copyWithPreference("smart")).resolves.toBe(true);
|
||||
|
||||
expect(copyToClipboard).toHaveBeenCalledWith("");
|
||||
});
|
||||
|
||||
it("uses an empty default NULL text for TSV clipboard output", async () => {
|
||||
const matrix: CellSelectionMatrix = {
|
||||
rowIndexes: [0],
|
||||
columnIndexes: [0, 1],
|
||||
columns: ["id", "name"],
|
||||
rows: [[1, null]],
|
||||
};
|
||||
vi.mocked(extractDataGridSelection).mockResolvedValueOnce({ text: "1\t", mimeType: "text/tab-separated-values", fileExtension: "tsv", rowCount: 1, columnCount: 2 });
|
||||
const state = createExportState(editableTable, ["id", "name"], matrix, [1, null]);
|
||||
|
||||
await expect(state.copyWithExtractor("tsv")).resolves.toBe(true);
|
||||
|
||||
expect(extractDataGridSelection).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
options: expect.objectContaining({ dsv: expect.objectContaining({ nullText: "" }) }),
|
||||
}),
|
||||
);
|
||||
expect(copyToClipboard).toHaveBeenCalledWith("1\t");
|
||||
});
|
||||
|
||||
it("preserves JSON-column text in a single-cell smart copy", async () => {
|
||||
const tableMeta: DataGridTableMeta = {
|
||||
tableName: "events",
|
||||
|
||||
@@ -8,7 +8,7 @@ import { type CellSelectionMatrix, type CellSelectionRange, type SelectionData }
|
||||
import type { DataGridExtractRequest, DataGridExtractorOptions } from "@/lib/dataGrid/dataGridCopyExtractor";
|
||||
import { useToast } from "@/composables/useToast";
|
||||
import { useExportTracker } from "@/composables/useExportTracker";
|
||||
import { displayCellValue, type CellValue } from "@/lib/dataGrid/cellValue";
|
||||
import { clipboardCellValue, type CellValue } from "@/lib/dataGrid/cellValue";
|
||||
import { tryStartExclusiveActivation, type ActionActivationGuard } from "@/lib/connection/actionActivation";
|
||||
import { copyToClipboard } from "@/lib/common/clipboard";
|
||||
import { clearDataGridClipboardCopy, rememberDataGridClipboardCopy } from "@/lib/dataGrid/dataGridClipboard";
|
||||
@@ -529,7 +529,7 @@ export function useDataGridExport(options: UseDataGridExportOptions) {
|
||||
const sourceIndex = visibleColumnIndexes.value[contextCell.value.col] ?? contextCell.value.col;
|
||||
const [resolvedItem] = await resolveVisibleRowValues([item], [sourceIndex]);
|
||||
const val = resolvedItem?.data[contextCell.value.col] ?? null;
|
||||
await copyText(displayCellValue(val), { rows: [[val]] });
|
||||
await copyText(clipboardCellValue(val), { rows: [[val]] });
|
||||
}
|
||||
|
||||
async function copyRow() {
|
||||
@@ -671,10 +671,8 @@ export function useDataGridExport(options: UseDataGridExportOptions) {
|
||||
});
|
||||
|
||||
async function copyAll() {
|
||||
const header = columns.value.join("\t");
|
||||
const rows = (await resolveVisibleRowValues(displayItems.value.filter((item) => !item.isDraft))).map((item) => item.data);
|
||||
const body = rows.map((row) => row.map((cell) => displayCellValue(cell)).join("\t")).join("\n");
|
||||
await copyText(`${header}\n${body}`, { rows, header: columns.value });
|
||||
await copyText(formatTsv(columns.value, rows), { rows, header: columns.value });
|
||||
}
|
||||
|
||||
// --- Export functions ---
|
||||
|
||||
@@ -329,7 +329,7 @@ export function useDataGridExtractor(options: UseDataGridExtractorOptions) {
|
||||
const request = await resolveRequestSourceValues(initialRequest);
|
||||
const mongoResult = await resolveMongoExtractorResult(extractor, request);
|
||||
const result = mongoResult ?? (await api.extractDataGridSelection(request));
|
||||
if (!result.text) return false;
|
||||
if (!result.text && !extractorAllowsEmptyOutput(extractor)) return false;
|
||||
// Derive the grid paste-back payload from the effective request schema so
|
||||
// hidden support columns, row headers, NULLs, tabs, and newlines keep the
|
||||
// same shape and values as the rendered raw text or TSV.
|
||||
@@ -365,7 +365,7 @@ export function useDataGridExtractor(options: UseDataGridExtractorOptions) {
|
||||
const request = await resolveRequestSourceValues(initialRequest, previewRowCount);
|
||||
const mongoResult = await resolveMongoExtractorResult(extractor, request, previewRowCount);
|
||||
const result = mongoResult ?? (await api.extractDataGridSelection(request));
|
||||
if (!result.text) throw new Error(t("grid.copyExtractorEmptySelection"));
|
||||
if (!result.text && !extractorAllowsEmptyOutput(extractor)) throw new Error(t("grid.copyExtractorEmptySelection"));
|
||||
return { ...result, sourceRowCount, truncated: sourceRowCount > result.rowCount };
|
||||
}
|
||||
|
||||
@@ -396,6 +396,10 @@ export function useDataGridExtractor(options: UseDataGridExtractorOptions) {
|
||||
return { copyWithExtractor, copyWithPreference, previewWithExtractor, previewWithPreference, canCopyWithExtractor };
|
||||
}
|
||||
|
||||
function extractorAllowsEmptyOutput(extractor: DataGridCopyExtractorId): boolean {
|
||||
return DATA_GRID_COPY_EXTRACTOR_DESCRIPTORS[extractor].category === "raw" || DATA_GRID_COPY_EXTRACTOR_DESCRIPTORS[extractor].category === "delimited";
|
||||
}
|
||||
|
||||
function compactTableMeta(tableMeta: DataGridTableMeta | undefined, requiredColumns: string[]): DataGridTableMeta | undefined {
|
||||
if (!tableMeta?.columns) return tableMeta;
|
||||
const requiredNames = new Set(requiredColumns.map(normalizeName));
|
||||
|
||||
Reference in new issue
Block a user