From 28206f29631eb98b2f9c5f1cbac66686e2118a5e Mon Sep 17 00:00:00 2001 From: zipg Date: Sun, 13 Sep 2026 13:26:26 +0800 Subject: [PATCH] fix(mongodb): null cell display and inline editing --- apps/desktop/src/components/grid/DataGrid.vue | 19 ++++++++++++++----- .../dataGrid/dataGridCellTypeVisual.spec.ts | 7 ++++--- .../lib/dataGrid/canvasDataGridRenderer.ts | 9 ++++++--- .../src/lib/mongo/mongoDocumentValues.ts | 10 +++++++++- .../app-tests/mongoDocumentValues.test.ts | 8 ++++++++ 5 files changed, 41 insertions(+), 12 deletions(-) diff --git a/apps/desktop/src/components/grid/DataGrid.vue b/apps/desktop/src/components/grid/DataGrid.vue index 6e5238c0d..26b0624f6 100644 --- a/apps/desktop/src/components/grid/DataGrid.vue +++ b/apps/desktop/src/components/grid/DataGrid.vue @@ -99,7 +99,7 @@ import type { BuildSingleColumnAlterSqlOptions } from "@/lib/table/tableStructur import { buildTableSelectSql, qualifyTableReferencesInSql, quoteTableDataIdentifier } from "@/lib/table/tableSelectSql"; import { uuid } from "@/lib/common/utils"; import { generateCellValues, type CellValueGenerationKind } from "@/lib/dataGrid/cellValueGeneration"; -import { MONGO_DOCUMENT_GRID_NULL, mongoDocumentGridDisplayText, mongoDocumentGridEditorText, mongoDocumentGridExternalValue, mongoDocumentGridInputValue } from "@/lib/mongo/mongoDocumentValues"; +import { MONGO_DOCUMENT_GRID_NULL, mongoDocumentGridClipboardText, mongoDocumentGridDisplayText, mongoDocumentGridEditorText, mongoDocumentGridExternalValue, mongoDocumentGridInputValue } from "@/lib/mongo/mongoDocumentValues"; import { compactHeaderColumnType, formatMetadataColumnTypeLabel, isNumericColumnType, resolveDataGridTypeVisualKind, resolveHeaderColumnType, resolveResultColumnType } from "@/lib/dataGrid/dataGridColumnType"; import { dataGridCellTextClass, dataGridTypeVisualClass } from "@/lib/dataGrid/dataGridCellTextVisual"; import { DATA_GRID_TYPE_COLOR_KEYS, resolveActiveDataGridTypeColors } from "@/lib/dataGrid/dataGridTypeColorScheme"; @@ -2017,8 +2017,10 @@ const columnAligns = computed<("left" | "right")[]>(() => { }); function gridCellTextColorClass(item: RowItem, actualColIdx: number, visibleColIdx: number): string { - if (!colorizeDataGridCellTypes.value) return "text-foreground"; const value = item.data[actualColIdx]; + const isMongoDocumentNull = props.mongoCollectionGrid === true && value === MONGO_DOCUMENT_GRID_NULL; + if (isMongoDocumentNull) return "text-muted-foreground italic"; + if (!colorizeDataGridCellTypes.value) return "text-foreground"; const checkbox = booleanCellsUseCheckbox.value && isBooleanGridCell(item, actualColIdx) && value !== null; return dataGridCellTextClass({ colorizeTypes: colorizeDataGridCellTypes.value, @@ -2036,10 +2038,12 @@ function gridCellTextColorClass(item: RowItem, actualColIdx: number, visibleColI } function transposeCellTextColorClass(recordIndex: number, actualColIdx: number): string { - if (!colorizeDataGridCellTypes.value) return "text-foreground"; const item = displayItems.value[recordIndex]; if (!item) return "text-foreground"; const value = item.data[actualColIdx]; + const isMongoDocumentNull = props.mongoCollectionGrid === true && value === MONGO_DOCUMENT_GRID_NULL; + if (isMongoDocumentNull) return "text-muted-foreground italic"; + if (!colorizeDataGridCellTypes.value) return "text-foreground"; return dataGridCellTextClass({ colorizeTypes: colorizeDataGridCellTypes.value, typeKind: allColumnTypeVisualKinds.value[actualColIdx] ?? "unknown", @@ -3947,6 +3951,10 @@ function isIoTDBTimestampColumn(columnIndex: number): boolean { } function inlineCellEditorText(value: CellValue, columnIndex: number): string { + if (props.mongoCollectionGrid) { + const documentGridText = mongoDocumentGridEditorText(value); + if (documentGridText !== undefined) return documentGridText; + } const columnInfo = tableColumnForGridColumn(columnIndex) ?? resultColumnInfoForGridColumn(columnIndex); const columnType = props.result.column_types?.[columnIndex] ?? columnInfo?.data_type; return ( @@ -7073,6 +7081,7 @@ function drawCanvasGrid() { searchMatchKeys: searchMatchSet.value, currentSearchMatch: currentSearchMatch.value, formatCell: (value, columnIndex, row) => formatCellCached(visibleLargeValuePreviewValue(row, columnIndex, value), columnIndex, largeValueOriginalBytes(row, columnIndex)), + isNullValue: (value) => value === null || (props.mongoCollectionGrid === true && value === MONGO_DOCUMENT_GRID_NULL), newRowCellPlaceholder, isRowActive, rowCellsUseSelectionVisual, @@ -7395,7 +7404,7 @@ const { columnComments: visibleColumnComments, allColumnComments, displayValue: formatCellCached, - cellClipboardText: (value) => (props.mongoCollectionGrid ? mongoDocumentGridEditorText(value) : undefined), + cellClipboardText: (value) => (props.mongoCollectionGrid ? mongoDocumentGridClipboardText(value) : undefined), externalCellValue: (value) => (props.mongoCollectionGrid ? mongoDocumentGridExternalValue(value) : value), mongoDocuments: computed(() => props.result.mongo_copy_documents ?? props.result.mongo_documents), spatialColumns: computed(() => props.result.spatial_columns), @@ -8933,7 +8942,7 @@ async function onGridKeydown(event: KeyboardEvent) { function detailClipboardText(detail: DataGridCellDetail): string { if (props.mongoCollectionGrid) { - const documentGridText = mongoDocumentGridEditorText(detail.value); + const documentGridText = mongoDocumentGridClipboardText(detail.value); if (documentGridText !== undefined) return documentGridText; } if (detail.value === null) return ""; diff --git a/apps/desktop/src/lib/__tests__/dataGrid/dataGridCellTypeVisual.spec.ts b/apps/desktop/src/lib/__tests__/dataGrid/dataGridCellTypeVisual.spec.ts index 6718c1df0..feaf35fd1 100644 --- a/apps/desktop/src/lib/__tests__/dataGrid/dataGridCellTypeVisual.spec.ts +++ b/apps/desktop/src/lib/__tests__/dataGrid/dataGridCellTypeVisual.spec.ts @@ -103,9 +103,10 @@ describe("data grid cell text visual priority", () => { expect(dataGridSource).toContain("'cursor-text hover:bg-gray-200 hover:text-foreground dark:hover:bg-gray-800':"); }); - it("returns before reading cell state when type colors are disabled", () => { - expect(dataGridSource).toContain('function gridCellTextColorClass(item: RowItem, actualColIdx: number, visibleColIdx: number): string {\n if (!colorizeDataGridCellTypes.value) return "text-foreground";\n const value = item.data[actualColIdx];'); - expect(dataGridSource).toContain('function transposeCellTextColorClass(recordIndex: number, actualColIdx: number): string {\n if (!colorizeDataGridCellTypes.value) return "text-foreground";\n const item = displayItems.value[recordIndex];'); + it("keeps Mongo BSON null muted when type colors are disabled", () => { + const mongoNullPriority = 'const isMongoDocumentNull = props.mongoCollectionGrid === true && value === MONGO_DOCUMENT_GRID_NULL;\n if (isMongoDocumentNull) return "text-muted-foreground italic";\n if (!colorizeDataGridCellTypes.value) return "text-foreground";'; + + expect(dataGridSource.split(mongoNullPriority)).toHaveLength(3); }); it("places data-grid type selectors in the components layer", () => { diff --git a/apps/desktop/src/lib/dataGrid/canvasDataGridRenderer.ts b/apps/desktop/src/lib/dataGrid/canvasDataGridRenderer.ts index 118bffab5..4bf60ef86 100644 --- a/apps/desktop/src/lib/dataGrid/canvasDataGridRenderer.ts +++ b/apps/desktop/src/lib/dataGrid/canvasDataGridRenderer.ts @@ -80,6 +80,7 @@ export interface DrawCanvasDataGridOptions { searchMatchKeys: ReadonlySet; currentSearchMatch: CanvasSearchMatch | null; formatCell: (value: CellValue, columnIndex: number, row: CanvasDataGridRow) => string; + isNullValue?: (value: CellValue) => boolean; columnIsBoolean?: (columnIndex: number) => boolean; newRowCellPlaceholder?: (row: CanvasDataGridRow, columnIndex: number) => string | null; isRowActive: (rowIndex: number) => boolean; @@ -345,6 +346,7 @@ export function drawCanvasDataGrid(options: DrawCanvasDataGridOptions) { searchMatchKeys, currentSearchMatch, formatCell, + isNullValue, newRowCellPlaceholder, isRowActive, rowCellsUseSelectionVisual, @@ -558,6 +560,7 @@ export function drawCanvasDataGrid(options: DrawCanvasDataGridOptions) { ctx.rect(clippedX, y, Math.min(cellPaintWidth, width - clippedX), CANVAS_DATA_GRID_ROW_HEIGHT); ctx.clip(); const value = item.data[actualColIdx]; + const isNullCell = isNullValue?.(value) ?? value === null; const isBooleanCell = columnIsBoolean?.(actualColIdx) === true && isBooleanCellValue(value); const isRightAlign = columnAligns?.[visibleColIdx] === "right"; const isEditingThisCell = editingCell?.rowId === item.id && editingCell.col === actualColIdx; @@ -567,7 +570,7 @@ export function drawCanvasDataGrid(options: DrawCanvasDataGridOptions) { const textRole = resolveDataGridCellTextRole({ colorizeTypes: colorizeDataTypes, typeKind, - isNull: value === null, + isNull: isNullCell, isDraft: item.isDraft && value === null, isEditing: isEditingThisCell, isControl: shouldRenderBooleanCheckbox, @@ -580,8 +583,8 @@ export function drawCanvasDataGrid(options: DrawCanvasDataGridOptions) { const cellTextColor = textRole === "muted" ? theme.mutedForeground : textRole === "type" ? dataGridTypeForeground(theme, typeKind) : theme.foreground; ctx.textAlign = isBooleanNullCell ? "center" : isRightAlign ? "right" : "left"; ctx.fillStyle = cellTextColor; - ctx.font = value === null ? italicFont : tabularFont; - setCanvasNumericVariant(ctx, value === null ? "normal" : "tabular-nums"); + ctx.font = isNullCell ? italicFont : tabularFont; + setCanvasNumericVariant(ctx, isNullCell ? "normal" : "tabular-nums"); const reservedWidth = rightAlignedActionCell?.rowIndex === item.displayIndex && rightAlignedActionCell.visibleColIdx === visibleColIdx ? rightAlignedActionCell.reservedWidth : 0; const { textAnchorX, maxWidth: cellMaxWidth } = resolveCanvasCellTextLayout({ drawX, colWidth, dpr: scaleX, isRightAlign, reservedWidth }); if (shouldRenderBooleanCheckbox) { diff --git a/apps/desktop/src/lib/mongo/mongoDocumentValues.ts b/apps/desktop/src/lib/mongo/mongoDocumentValues.ts index 03dc6bef5..0e57aeb66 100644 --- a/apps/desktop/src/lib/mongo/mongoDocumentValues.ts +++ b/apps/desktop/src/lib/mongo/mongoDocumentValues.ts @@ -145,10 +145,18 @@ function mongoDocumentGridEscapedString(value: unknown): string | undefined { /** Returns the text presented in a collection-grid editor, when customized. */ export function mongoDocumentGridEditorText(value: unknown): string | undefined { - if (value === MONGO_DOCUMENT_GRID_NULL) return "NULL"; + // An existing BSON null is represented as NULL in the grid, but editing it + // starts with an empty input. The private marker must never be user-facing. + if (value === MONGO_DOCUMENT_GRID_NULL) return ""; return mongoDocumentGridEscapedString(value); } +/** Returns the text used when copying a collection-grid cell. */ +export function mongoDocumentGridClipboardText(value: unknown): string | undefined { + if (value === MONGO_DOCUMENT_GRID_NULL) return "NULL"; + return mongoDocumentGridEditorText(value); +} + /** Returns the custom display text required by collection-grid BSON values. */ export function mongoDocumentGridDisplayText(value: unknown): string | undefined { if (value === MONGO_DOCUMENT_GRID_NULL) return "NULL"; diff --git a/packages/app-tests/mongoDocumentValues.test.ts b/packages/app-tests/mongoDocumentValues.test.ts index c29728fa8..585efbca3 100644 --- a/packages/app-tests/mongoDocumentValues.test.ts +++ b/packages/app-tests/mongoDocumentValues.test.ts @@ -11,6 +11,7 @@ import { MONGO_DOCUMENT_GRID_NULL, mongoDocumentDisplayValue, mongoDocumentGridDisplayText, + mongoDocumentGridClipboardText, mongoDocumentGridEditorText, mongoDocumentGridExternalValue, mongoDocumentGridInputValue, @@ -403,6 +404,13 @@ test("escapes Mongo collection-grid values reserved for BSON null state", () => }); }); +test("keeps BSON null empty in editors while preserving a copy marker", () => { + assert.equal(mongoDocumentGridEditorText(MONGO_DOCUMENT_GRID_NULL), ""); + assert.equal(mongoDocumentGridClipboardText(MONGO_DOCUMENT_GRID_NULL), "NULL"); + assert.equal(mongoDocumentGridEditorText("NULL"), undefined); + assert.equal(mongoDocumentGridClipboardText("NULL"), undefined); +}); + test("restores internal Mongo collection-grid values for external output", () => { assert.equal(mongoDocumentGridExternalValue(MONGO_DOCUMENT_GRID_NULL), null); assert.equal(mongoDocumentGridExternalValue("NULL"), "NULL");