feat(grid): add checkbox/dropdown display mode for boolean columns

This commit is contained in:
Diego Fabricio authored and GitHub committed 2026-08-07 15:51:09 +08:00
1 parent 9fab90b617
commit 19401112d2
15 files changed
+363 -54

No files matched your search

+90 -3
View File
@@ -135,7 +135,7 @@ import { canFormatCellDetailJson, cellDetailEditorText, compactJsonText, default
import { buildDataGridCellDetail, buildDataGridColumnDetail, buildDataGridRowDetail, CELL_DETAIL_VALUE_PREVIEW_MAX_LENGTH, dataGridColumnDetailJson, dataGridColumnDetailTsv, dataGridRowDetailJson, dataGridRowDetailTsv, type DataGridCellDetail } from "@/lib/dataGrid/dataGridDetail";
import { applyColumnFormatter, buildColumnFormatterKey, getSupportedTimeZoneOptions, normalizeColumnFormatter, resolveColumnFormatter, type ColumnFormatterConfig, type DateTimeFormatterUnit, DateTimePatterns } from "@/lib/dataGrid/columnFormatter";
import { temporalCellEditorConfig, type TemporalCellEditorConfig } from "@/lib/dataGrid/dataGridTemporalEditor";
import { BOOLEAN_CELL_EDITOR_VALUES, booleanCellEditorValue, isBooleanCellValue, isBooleanColumnType, parseBooleanCellEditorValue } from "@/lib/dataGrid/dataGridBooleanColumn";
import { BOOLEAN_CELL_EDITOR_VALUES, booleanCellEditorValue, isBooleanCellValue, isBooleanColumnType, isPointInBooleanCheckbox, nextBooleanCellValue, normalizeBooleanCellValue, parseBooleanCellEditorValue } from "@/lib/dataGrid/dataGridBooleanColumn";
import { resolveDataGridColumnsByResultIndex } from "@/lib/dataGrid/dataGridColumnMetadata";
import { isCancelSearchShortcut, isCopyCurrentRowShortcut, isDeleteCurrentRowShortcut, isFocusSearchShortcut, isModRShortcut, isSaveShortcut, isToggleTransposeShortcut } from "@/lib/editor/keyboardShortcuts";
import { dataGridHeaderContentWidth, scrollbarGutterWidth } from "@/lib/dataGrid/dataGridScrollGutter";
@@ -273,6 +273,8 @@ const tableFontSize = computed(() => settingsStore.editorSettings.tableFontSize)
const rowNumberWidth = ref(DATA_GRID_ROW_NUM_WIDTH);
const multiRowTranspose = computed(() => settingsStore.editorSettings.dataGridMultiRowTranspose);
const hideNullColumns = computed(() => settingsStore.editorSettings.dataGridHideNullColumns);
const booleanCellsUseCheckbox = computed(() => settingsStore.editorSettings.dataGridBooleanDisplayMode === "checkbox");
const booleanDisplayMode = computed(() => settingsStore.editorSettings.dataGridBooleanDisplayMode);
const { isDark, themePalette } = useTheme();
const { toast } = useToast();
// 外键单元格跳转复用导航入口;对话框引用传 stub(同 AiAssistant 模式)
@@ -3179,6 +3181,7 @@ function showReadonlyCellDetailsOnDblClick(item: RowItem, rowIndex: number, visi
}
function onDomCellDblClick(item: RowItem, rowIndex: number, visibleColIdx: number, actualColIdx: number, event: MouseEvent) {
if (booleanCellsUseCheckbox.value && isBooleanGridCell(item, actualColIdx) && canEditCellItem(item, actualColIdx)) return;
if (showReadonlyCellDetailsOnDblClick(item, rowIndex, visibleColIdx, actualColIdx)) return;
startDomCellEdit(item.id, actualColIdx, formatCellCached(item.data[actualColIdx], actualColIdx), event);
}
@@ -3218,6 +3221,19 @@ function measureCellTextWidth(text: string, font: string): number {
return width;
}
const cellTextWidthCache = new Map<string, number>();
function measureCellTextWidthCached(text: string, font: string): number {
const key = `${font}|${text}`;
let width = cellTextWidthCache.get(key);
if (width === undefined) {
width = measureCellTextWidth(text, font);
if (cellTextWidthCache.size >= 200) cellTextWidthCache.clear();
cellTextWidthCache.set(key, width);
}
return width;
}
const tableColumnsByResultIndex = computed(() =>
resolveDataGridColumnsByResultIndex({
resultColumns: props.result.columns,
@@ -3282,6 +3298,17 @@ const booleanEditorModelValue = computed({
},
});
function booleanCellChecked(value: unknown): boolean {
return normalizeBooleanCellValue(value) === true;
}
function cycleBooleanGridCell(item: RowItem | undefined, actualColIdx: number, event?: MouseEvent) {
if (!item || !isBooleanGridCell(item, actualColIdx) || !canEditCellItem(item, actualColIdx)) return;
event?.stopPropagation();
const next = nextBooleanCellValue(item.data[actualColIdx], isBooleanGridColumnNullable(actualColIdx));
applyCellValue(item.id, actualColIdx, next === null ? null : next ? "true" : "false");
}
function cellEditInputModeForColumn(columnIndex: number): "decimal" | "numeric" | undefined {
const dataType = normalizedColumnDataType(tableColumnForGridColumn(columnIndex));
if (isIntegerColumnType(dataType)) return "numeric";
@@ -5133,7 +5160,9 @@ function onCanvasMouseMove(event: MouseEvent) {
const next = hit && hitItem ? { rowIndex: hitItem.displayIndex, visibleColIdx: hit.rowNumber ? -1 : hit.visibleColIdx } : null;
const actualColIdx = next ? visibleColumnIndexes.value[next.visibleColIdx] : undefined;
if (canvasRef.value) {
canvasRef.value.style.cursor = hit?.rowNumber ? "default" : hitItem && actualColIdx !== undefined && canEditCellItem(hitItem, actualColIdx) ? "text" : "cell";
const overBooleanInteractive =
booleanCellsUseCheckbox.value && hit != null && !hit.rowNumber && hitItem != null && actualColIdx !== undefined && isBooleanGridCell(hitItem, actualColIdx) && canEditCellItem(hitItem, actualColIdx) && booleanInteractiveHitFromCanvasEvent(hitItem, hit, actualColIdx, event);
canvasRef.value.style.cursor = hit?.rowNumber ? "default" : overBooleanInteractive ? "pointer" : hitItem && actualColIdx !== undefined && canEditCellItem(hitItem, actualColIdx) ? "text" : "cell";
}
if (next?.rowIndex === canvasHoverCell.value?.rowIndex && next?.visibleColIdx === canvasHoverCell.value?.visibleColIdx) {
return;
@@ -5176,6 +5205,43 @@ function clearCanvasDetailHover(event?: MouseEvent) {
onCanvasMouseLeave();
}
function booleanCheckboxHitFromCanvasEvent(item: RowItem, hit: { rowIndex: number; visibleColIdx: number }, actualColIdx: number, event: MouseEvent): boolean {
if (item.data[actualColIdx] === null) return false;
const canvas = canvasRef.value;
const canvasRect = canvas?.getBoundingClientRect();
const cellRect = canvasCellViewportRect(hit.rowIndex, hit.visibleColIdx);
if (!canvasRect || !cellRect) return false;
return isPointInBooleanCheckbox({ x: event.clientX - canvasRect.left, y: event.clientY - canvasRect.top }, { left: cellRect.left, top: cellRect.top, width: cellRect.width, height: cellRect.height });
}
function booleanNullTextHitFromCanvasEvent(item: RowItem, hit: { rowIndex: number; visibleColIdx: number }, actualColIdx: number, event: MouseEvent): boolean {
if (item.data[actualColIdx] !== null) return false;
const canvas = canvasRef.value;
const canvasRect = canvas?.getBoundingClientRect();
const cellRect = canvasCellViewportRect(hit.rowIndex, hit.visibleColIdx);
if (!canvasRect || !cellRect) return false;
const text = firstLineCellDisplayValue(formatCellCached(item.data[actualColIdx], actualColIdx));
if (!text) return false;
const textWidth = measureCellTextWidthCached(text, `italic 400 ${tableFontSize.value}px ${tableFontFamily.value}`);
if (textWidth <= 0) return false;
const left = cellRect.left + (cellRect.width - textWidth) / 2 - 2;
const right = left + textWidth + 4;
const x = event.clientX - canvasRect.left;
const y = event.clientY - canvasRect.top;
return x >= left && x <= right && y >= cellRect.top && y <= cellRect.top + cellRect.height;
}
function booleanInteractiveHitFromCanvasEvent(item: RowItem, hit: { rowIndex: number; visibleColIdx: number }, actualColIdx: number, event: MouseEvent): boolean {
return item.data[actualColIdx] === null ? booleanNullTextHitFromCanvasEvent(item, hit, actualColIdx, event) : booleanCheckboxHitFromCanvasEvent(item, hit, actualColIdx, event);
}
function tryCycleBooleanCheckboxOnCanvasMouseDown(item: RowItem, hit: { rowIndex: number; visibleColIdx: number }, actualColIdx: number, event: MouseEvent): boolean {
if (!booleanCellsUseCheckbox.value || !isBooleanGridCell(item, actualColIdx) || !canEditCellItem(item, actualColIdx)) return false;
if (!booleanInteractiveHitFromCanvasEvent(item, hit, actualColIdx, event)) return false;
cycleBooleanGridCell(item, actualColIdx);
return true;
}
function onCanvasMouseDown(event: MouseEvent) {
if (event.button !== 0) return;
const hit = canvasHitTest(event);
@@ -5192,6 +5258,7 @@ function onCanvasMouseDown(event: MouseEvent) {
onRowNumberMouseDown(item, event);
} else {
handleDataCellMousedown(item.displayIndex, hit.visibleColIdx, item.id, event);
if (actualColIdx !== undefined) tryCycleBooleanCheckboxOnCanvasMouseDown(item, hit, actualColIdx, event);
}
gridRef.value?.focus({ preventScroll: true });
scheduleCanvasDraw();
@@ -5224,6 +5291,7 @@ function onCanvasDblClick(event: MouseEvent) {
const actualColIdx = visibleColumnIndexes.value[hit.visibleColIdx];
if (!item || actualColIdx === undefined) return;
if (showReadonlyCellDetailsOnDblClick(item, item.displayIndex, hit.visibleColIdx, actualColIdx)) return;
if (booleanCellsUseCheckbox.value && isBooleanGridCell(item, actualColIdx) && canEditCellItem(item, actualColIdx)) return;
startCellEdit(item.id, actualColIdx, canvasCellContentOverflows(item, actualColIdx, hit.visibleColIdx));
}
@@ -5399,6 +5467,8 @@ function drawCanvasGrid() {
frozenColumnCount: frozenColumnCount.value,
columnAligns: columnAligns.value,
rightAlignedActionCell: canvasRightAlignedActionCell.value,
columnIsBoolean: isBooleanGridColumn,
booleanDisplayMode: booleanDisplayMode.value,
});
}
@@ -5413,6 +5483,7 @@ watch(
);
watch(showDataGridTopbar, () => nextTick(observeDataGridTopbarWidth), { immediate: true });
watch(columnAligns, () => scheduleCanvasDraw());
watch(booleanDisplayMode, () => scheduleCanvasDraw());
watch(
[
displayRowRefs,
@@ -9660,7 +9731,8 @@ const gridContextMenuItems = computed<ContextMenuItem[]>(() => {
'bg-yellow-200/60 dark:bg-yellow-500/20': cellIsSearchMatch(item.displayIndex, col.actualColIdx),
'ring-2 ring-inset ring-yellow-500 bg-yellow-300/60 dark:bg-yellow-500/40': cellIsCurrentMatch(item.displayIndex, col.actualColIdx),
'tabular-nums': typeof item.data[col.actualColIdx] === 'number',
'cursor-text hover:bg-gray-200 dark:hover:bg-gray-800': !isScrolling && canEditCellItem(item, col.actualColIdx),
'cursor-text hover:bg-gray-200 dark:hover:bg-gray-800': !isScrolling && canEditCellItem(item, col.actualColIdx) && !(booleanCellsUseCheckbox && isBooleanGridCell(item, col.actualColIdx) && item.data[col.actualColIdx] !== null),
'cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-800': !isScrolling && booleanCellsUseCheckbox && isBooleanGridCell(item, col.actualColIdx) && item.data[col.actualColIdx] !== null && canEditCellItem(item, col.actualColIdx),
'line-through': item.isDeleted,
'overflow-visible z-20 border-r-transparent': editingCell?.rowId === item.id && editingCell?.col === col.actualColIdx,
'overflow-hidden': !(editingCell?.rowId === item.id && editingCell?.col === col.actualColIdx),
@@ -9738,6 +9810,21 @@ const gridContextMenuItems = computed<ContextMenuItem[]>(() => {
<template v-if="draftCellPlaceholder(item, col.actualColIdx)">
<span class="text-muted-foreground/70 italic">{{ draftCellPlaceholder(item, col.actualColIdx) }}</span>
</template>
<template v-else-if="booleanCellsUseCheckbox && isBooleanGridCell(item, col.actualColIdx) && item.data[col.actualColIdx] !== null && canEditCellItem(item, col.actualColIdx)">
<div class="flex h-full items-center justify-center">
<input
type="checkbox"
class="h-3.5 w-3.5 rounded border-border/70 bg-background text-primary focus:ring-0"
:checked="booleanCellChecked(item.data[col.actualColIdx])"
:disabled="!canEditCellItem(item, col.actualColIdx)"
@mousedown.stop
@click.stop="cycleBooleanGridCell(item, col.actualColIdx, $event)"
/>
</div>
</template>
<template v-else-if="booleanCellsUseCheckbox && isBooleanGridCell(item, col.actualColIdx) && item.data[col.actualColIdx] === null && canEditCellItem(item, col.actualColIdx)">
<span class="italic text-muted-foreground cursor-pointer select-none" @click.stop="cycleBooleanGridCell(item, col.actualColIdx, $event)">{{ firstLineCellDisplayValue(formatCellCached(item.data[col.actualColIdx], col.actualColIdx)) }}</span>
</template>
<template v-else>{{ firstLineCellDisplayValue(formatCellCached(item.data[col.actualColIdx], col.actualColIdx)) }}</template>
<div v-if="cellDetailButtonVisible(item.displayIndex, col.actualColIdx)" class="absolute right-2 top-1/2 flex -translate-y-1/2 items-center gap-1">
<LightDropdownMenu
@@ -7,7 +7,7 @@ import { defaultViewForResult } from "@/lib/query/queryResultDefaultView";
import { isQueryExecutionErrorResult } from "@/lib/query/queryResultError";
import type { CSSProperties } from "vue";
import { useI18n } from "vue-i18n";
import { Check, Columns3Cog, EyeOff, Loader2, Search, TableProperties, ChevronDown, ChevronUp, Inbox, RefreshCcw, Wrench, Toolbox, Database, Download, Upload, X, Pin, Rows3, SquareDashed, Minus, Plus, ShieldAlert, AlignLeft, AlignRight, PanelsTopLeft } from "@lucide/vue";
import { Check, CheckSquare2, Columns3Cog, EyeOff, Loader2, Search, TableProperties, ChevronDown, ChevronUp, Inbox, RefreshCcw, Wrench, Toolbox, Database, Download, Upload, X, Pin, Rows3, SquareDashed, Minus, Plus, ShieldAlert, AlignLeft, AlignRight, PanelsTopLeft } from "@lucide/vue";
import { Splitpanes, Pane } from "splitpanes";
import "splitpanes/dist/splitpanes.css";
import { Button } from "@/components/ui/button";
@@ -185,6 +185,8 @@ const { t, locale } = useI18n();
const queryStore = useQueryStore();
const connectionStore = useConnectionStore();
const settingsStore = useSettingsStore();
const booleanDisplayMode = computed(() => settingsStore.editorSettings.dataGridBooleanDisplayMode);
const setBooleanDisplayMode = (mode: "checkbox" | "dropdown") => settingsStore.updateEditorSettings({ dataGridBooleanDisplayMode: mode });
const { toast } = useToast();
const DEFAULT_QUERY_RESULTS_PANE_SIZE = 68;
@@ -1292,6 +1294,30 @@ defineExpose({ focusSearch, refreshData, refreshQueryEditorCompletionCache, hand
</button>
</div>
</div>
<div class="flex items-center justify-between gap-3 px-3 py-1.5 text-xs">
<div class="min-w-0 flex items-center gap-2 font-medium">
<CheckSquare2 class="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
<span>{{ t("grid.booleanDisplayMode") }}</span>
</div>
<div class="grid w-32 grid-cols-2 rounded-md border bg-muted/40 p-0.5">
<button
type="button"
class="h-5 min-w-0 truncate whitespace-nowrap rounded-[5px] px-2 text-xs transition-colors"
:class="booleanDisplayMode === 'dropdown' ? 'bg-background font-semibold text-foreground shadow-sm' : 'text-muted-foreground hover:text-foreground'"
@click="setBooleanDisplayMode('dropdown')"
>
{{ t("grid.booleanDisplayDropdown") }}
</button>
<button
type="button"
class="h-5 min-w-0 truncate whitespace-nowrap rounded-[5px] px-2 text-xs transition-colors"
:class="booleanDisplayMode === 'checkbox' ? 'bg-background font-semibold text-foreground shadow-sm' : 'text-muted-foreground hover:text-foreground'"
@click="setBooleanDisplayMode('checkbox')"
>
{{ t("grid.booleanDisplayCheckbox") }}
</button>
</div>
</div>
<div class="flex items-center justify-between gap-3 px-3 py-1.5 text-xs" :class="{ 'opacity-60': !dataGridRef?.canToggleAllNullColumns }">
<span class="min-w-0 flex items-center gap-2 font-medium">
<EyeOff class="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
@@ -1751,6 +1777,30 @@ defineExpose({ focusSearch, refreshData, refreshQueryEditorCompletionCache, hand
</button>
</div>
</div>
<div class="flex items-center justify-between gap-3 px-3 py-1.5 text-xs">
<div class="min-w-0 flex items-center gap-2 font-medium">
<CheckSquare2 class="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
<span>{{ t("grid.booleanDisplayMode") }}</span>
</div>
<div class="grid w-32 grid-cols-2 rounded-md border bg-muted/40 p-0.5">
<button
type="button"
class="h-5 min-w-0 truncate whitespace-nowrap rounded-[5px] px-2 text-xs transition-colors"
:class="booleanDisplayMode === 'dropdown' ? 'bg-background font-semibold text-foreground shadow-sm' : 'text-muted-foreground hover:text-foreground'"
@click="setBooleanDisplayMode('dropdown')"
>
{{ t("grid.booleanDisplayDropdown") }}
</button>
<button
type="button"
class="h-5 min-w-0 truncate whitespace-nowrap rounded-[5px] px-2 text-xs transition-colors"
:class="booleanDisplayMode === 'checkbox' ? 'bg-background font-semibold text-foreground shadow-sm' : 'text-muted-foreground hover:text-foreground'"
@click="setBooleanDisplayMode('checkbox')"
>
{{ t("grid.booleanDisplayCheckbox") }}
</button>
</div>
</div>
<div class="flex items-center justify-between gap-3 px-3 py-1.5 text-xs" :class="{ 'opacity-60': !dataGridRef?.canToggleAllNullColumns }">
<span class="min-w-0 flex items-center gap-2 font-medium">
<EyeOff class="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
+3
View File
@@ -1392,6 +1392,9 @@ export default {
numericColumnAlign: "Numeric Column Align",
numericColumnAlignLeft: "Left",
numericColumnAlignRight: "Right",
booleanDisplayMode: "Boolean display",
booleanDisplayCheckbox: "Check",
booleanDisplayDropdown: "Dropdown",
moreValues: "{count} more values, keep typing to narrow results",
filterByValue: "Filter by This Value",
filterExcludeValue: "Exclude This Value",
+3
View File
@@ -1557,6 +1557,9 @@ export default withEnglishFallback({
numericColumnAlign: "Alineación de columna numérica",
numericColumnAlignLeft: "Alineación izquierda",
numericColumnAlignRight: "Alineación derecha",
booleanDisplayMode: "Visualización de booleanos",
booleanDisplayCheckbox: "Check",
booleanDisplayDropdown: "Lista",
batchAppendPasteNotEditable: "El resultado actual no se puede editar.",
batchAppendPasteInvalidTarget: "Primero seleccione una celda de número de fila de una nueva fila vacía y luego pegue.",
batchAppendPasteTargetNotEmpty: "La nueva fila seleccionada ya contiene datos.",
+3
View File
@@ -1555,6 +1555,9 @@ export default withEnglishFallback({
numericColumnAlign: "Allineamento colonna numerica",
numericColumnAlignLeft: "Allineamento a sinistra",
numericColumnAlignRight: "Allineamento a destra",
booleanDisplayMode: "Visualizzazione booleani",
booleanDisplayCheckbox: "Check",
booleanDisplayDropdown: "Dropdown",
batchAppendPasteNotEditable: "Il risultato corrente non è modificabile.",
batchAppendPasteInvalidTarget: "Seleziona prima una cella vuota della riga nuova, poi incolla.",
batchAppendPasteTargetNotEmpty: "La riga nuova selezionata contiene già dati.",
+3
View File
@@ -1577,6 +1577,9 @@ export default withEnglishFallback({
numericColumnAlign: "数値列の配置",
numericColumnAlignLeft: "左揃え",
numericColumnAlignRight: "右揃え",
booleanDisplayMode: "ブール値の表示",
booleanDisplayCheckbox: "チェック",
booleanDisplayDropdown: "ドロップダウン",
batchAppendPasteNotEditable: "現在の結果は編集できません。",
batchAppendPasteInvalidTarget: "空の新規行の行番号セルを選択してから貼り付けてください。",
batchAppendPasteTargetNotEmpty: "選択した新規行には既にデータが含まれています。",
+3
View File
@@ -1296,6 +1296,9 @@ export default withEnglishFallback({
numericColumnAlign: "숫자 컬럼 정렬",
numericColumnAlignLeft: "왼쪽",
numericColumnAlignRight: "오른쪽",
booleanDisplayMode: "불리언 표시",
booleanDisplayCheckbox: "체크",
booleanDisplayDropdown: "드롭다운",
moreValues: "{count}개 값 더, 결과를 좁히려면 계속 입력하세요",
filterByValue: "이 값으로 필터",
filterExcludeValue: "이 값 제외",
+3
View File
@@ -1557,6 +1557,9 @@ export default withEnglishFallback({
numericColumnAlign: "Alinhamento de colunas numéricas",
numericColumnAlignLeft: "Alinhamento à esquerda",
numericColumnAlignRight: "Alinhamento à direita",
booleanDisplayMode: "Exibição booleana",
booleanDisplayCheckbox: "Check",
booleanDisplayDropdown: "Dropdown",
batchAppendPasteNotEditable: "O resultado atual não é editável.",
batchAppendPasteInvalidTarget: "Selecione primeiro o número da linha de uma nova linha vazia e depois cole.",
batchAppendPasteTargetNotEmpty: "A nova linha selecionada já contém dados.",
+3
View File
@@ -1392,6 +1392,9 @@ export default withEnglishFallback({
numericColumnAlign: "数值列对齐",
numericColumnAlignLeft: "左对齐",
numericColumnAlignRight: "右对齐",
booleanDisplayMode: "布尔值显示",
booleanDisplayCheckbox: "复选框",
booleanDisplayDropdown: "下拉列表",
moreValues: "还有 {count} 个值,输入关键词继续缩小范围",
filterByValue: "筛选此值",
filterExcludeValue: "排除此值",
+3
View File
@@ -1557,6 +1557,9 @@ export default withEnglishFallback({
numericColumnAlign: "數值列對齊",
numericColumnAlignLeft: "左對齊",
numericColumnAlignRight: "右對齊",
booleanDisplayMode: "布林值顯示",
booleanDisplayCheckbox: "核取方塊",
booleanDisplayDropdown: "下拉清單",
batchAppendPasteNotEditable: "目前結果不可編輯。",
batchAppendPasteInvalidTarget: "請先選取一個空白新增行的行號儲存格,再進行貼上。",
batchAppendPasteTargetNotEmpty: "選取的新增行已包含資料。",
@@ -1,4 +1,5 @@
import { firstLineCellDisplayValue, type CellValue } from "@/lib/dataGrid/cellValue";
import { BOOLEAN_CHECKBOX_SIZE, isBooleanCellValue, normalizeBooleanCellValue } from "@/lib/dataGrid/dataGridBooleanColumn";
import type { RowStatus } from "@/lib/dataGrid/gridRowStatus";
import { DATA_GRID_DARK_SEARCH_COLORS, resolveDataGridPaintTheme, type DataGridPaintTheme } from "@/lib/dataGrid/dataGridPaintTheme";
@@ -73,6 +74,7 @@ export interface DrawCanvasDataGridOptions {
searchMatchKeys: ReadonlySet<number>;
currentSearchMatch: CanvasSearchMatch | null;
formatCell: (value: CellValue, columnIndex: number) => string;
columnIsBoolean?: (columnIndex: number) => boolean;
draftCellPlaceholder?: string;
isRowActive: (rowIndex: number) => boolean;
rowCellsUseSelectionVisual: (rowId: number) => boolean;
@@ -83,6 +85,7 @@ export interface DrawCanvasDataGridOptions {
frozenColumnCount?: number;
columnAligns?: readonly ("left" | "right")[];
rightAlignedActionCell?: CanvasRightAlignedActionCell | null;
booleanDisplayMode?: "checkbox" | "dropdown";
}
type NumericCanvasContext = CanvasRenderingContext2D & {
@@ -229,6 +232,29 @@ function alignCanvasPixel(value: number, dpr: number): number {
return Math.round(value * dpr) / dpr;
}
function drawBooleanCheckbox(ctx: CanvasRenderingContext2D, options: { drawX: number; y: number; colWidth: number; scaleX: number; scaleY: number; theme: DataGridPaintTheme; checked: boolean }): void {
const { drawX, y, colWidth, scaleX, scaleY, theme, checked } = options;
const size = BOOLEAN_CHECKBOX_SIZE;
const boxX = alignCanvasPixel(drawX + (colWidth - size) / 2, scaleX);
const boxY = alignCanvasPixel(y + (CANVAS_DATA_GRID_ROW_HEIGHT - size) / 2, scaleY);
ctx.lineWidth = 1;
if (checked) {
ctx.fillStyle = theme.primary;
ctx.fillRect(boxX, boxY, size, size);
ctx.strokeStyle = theme.background;
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(boxX + 3, boxY + size / 2);
ctx.lineTo(boxX + size / 2 - 0.5, boxY + size - 3.5);
ctx.lineTo(boxX + size - 2.5, boxY + 3);
ctx.stroke();
ctx.lineWidth = 1;
} else {
ctx.strokeStyle = theme.mutedForeground;
ctx.strokeRect(boxX + 0.5, boxY + 0.5, size - 1, size - 1);
}
}
function crispCanvasLine(value: number, dpr: number): number {
return alignCanvasPixel(value, dpr) + 0.5 / dpr;
}
@@ -313,6 +339,8 @@ export function drawCanvasDataGrid(options: DrawCanvasDataGridOptions) {
frozenColumnCount = 0,
columnAligns,
rightAlignedActionCell,
columnIsBoolean,
booleanDisplayMode = "dropdown",
} = options;
const fallbackRatio = Math.max(1, options.pixelRatio ?? window.devicePixelRatio ?? 1);
const { pixelWidth, pixelHeight, scaleX, scaleY } = resolveCanvasBackingStoreMetrics({
@@ -479,26 +507,44 @@ 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 isBooleanCell = columnIsBoolean?.(actualColIdx) === true && isBooleanCellValue(value);
const isRightAlign = columnAligns?.[visibleColIdx] === "right";
const isEditingThisCell = editingCell?.rowId === item.id && editingCell.col === actualColIdx;
ctx.textAlign = isRightAlign ? "right" : "left";
const isBooleanNullCell = booleanDisplayMode === "checkbox" && isBooleanCell && value === null && !isEditingThisCell;
const shouldRenderBooleanCheckbox = booleanDisplayMode === "checkbox" && isBooleanCell && value !== null && !isEditingThisCell;
ctx.textAlign = isBooleanNullCell ? "center" : isRightAlign ? "right" : "left";
ctx.fillStyle = value === null ? theme.mutedForeground : theme.foreground;
ctx.font = value === null ? italicFont : tabularFont;
setCanvasNumericVariant(ctx, value === null ? "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 });
const rawDisplayText = item.isDraft && value === null ? (draftCellPlaceholder ?? "") : formatCell(value, actualColIdx);
const displayText = isEditingThisCell ? "" : firstLineCellDisplayValue(rawDisplayText);
const text = isEditingThisCell ? displayText : fitCanvasText(ctx, displayText, cellMaxWidth, isRightAlign ? "right" : "left");
ctx.fillText(text, textAnchorX, textY);
if (item.isDeleted && text) {
const textWidth = ctx.measureText(text).width;
const lineStartX = isRightAlign ? textAnchorX - textWidth : textAnchorX;
ctx.strokeStyle = theme.foreground;
ctx.beginPath();
ctx.moveTo(lineStartX, textY);
ctx.lineTo(alignCanvasPixel(lineStartX + textWidth, scaleX), textY);
ctx.stroke();
if (shouldRenderBooleanCheckbox) {
drawBooleanCheckbox(ctx, { drawX, y, colWidth, scaleX, scaleY, theme, checked: normalizeBooleanCellValue(value) === true });
if (item.isDeleted) {
const boxX = alignCanvasPixel(drawX + (colWidth - BOOLEAN_CHECKBOX_SIZE) / 2, scaleX);
const strikeY = alignCanvasPixel(y + CANVAS_DATA_GRID_ROW_HEIGHT / 2, scaleY);
ctx.strokeStyle = theme.foreground;
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(boxX - 1, strikeY);
ctx.lineTo(alignCanvasPixel(boxX + BOOLEAN_CHECKBOX_SIZE + 1, scaleX), strikeY);
ctx.stroke();
}
} else {
const rawDisplayText = item.isDraft && value === null ? (draftCellPlaceholder ?? "") : formatCell(value, actualColIdx);
const displayText = isEditingThisCell ? "" : firstLineCellDisplayValue(rawDisplayText);
const text = isEditingThisCell ? displayText : fitCanvasText(ctx, displayText, cellMaxWidth, isBooleanNullCell ? "left" : isRightAlign ? "right" : "left");
const anchorX = isBooleanNullCell ? alignCanvasPixel(drawX + colWidth / 2, scaleX) : textAnchorX;
ctx.fillText(text, anchorX, textY);
if (item.isDeleted && text) {
const textWidth = ctx.measureText(text).width;
const lineStartX = isBooleanNullCell ? anchorX - textWidth / 2 : isRightAlign ? textAnchorX - textWidth : textAnchorX;
ctx.strokeStyle = theme.foreground;
ctx.beginPath();
ctx.moveTo(lineStartX, textY);
ctx.lineTo(alignCanvasPixel(lineStartX + textWidth, scaleX), textY);
ctx.stroke();
}
}
ctx.restore();
setCanvasNumericVariant(ctx, "normal");
@@ -1,5 +1,6 @@
import type { DatabaseType } from "@/types/database";
export const BOOLEAN_CHECKBOX_SIZE = 13;
export const BOOLEAN_CELL_EDITOR_VALUES = ["true", "false"];
export function isBooleanColumnType(dataType: string | undefined, databaseType?: DatabaseType): boolean {
@@ -27,6 +28,13 @@ export function isBooleanCellValue(value: unknown): boolean {
return value === null || normalizeBooleanCellValue(value) !== null;
}
export function nextBooleanCellValue(current: unknown, nullable: boolean): boolean | null {
const normalized = normalizeBooleanCellValue(current);
if (normalized === true) return false;
if (normalized === false) return nullable ? null : true;
return true;
}
export function booleanCellEditorValue(value: unknown): string {
const normalized = normalizeBooleanCellValue(value);
if (normalized === true) return "true";
@@ -40,3 +48,16 @@ export function parseBooleanCellEditorValue(value: string | null): boolean | nul
if (value === "false") return false;
return undefined;
}
export function booleanCheckboxRect(cell: { left: number; top: number; width: number; height: number }): { left: number; top: number; size: number } {
return {
left: cell.left + (cell.width - BOOLEAN_CHECKBOX_SIZE) / 2,
top: cell.top + (cell.height - BOOLEAN_CHECKBOX_SIZE) / 2,
size: BOOLEAN_CHECKBOX_SIZE,
};
}
export function isPointInBooleanCheckbox(point: { x: number; y: number }, cell: { left: number; top: number; width: number; height: number }): boolean {
const rect = booleanCheckboxRect(cell);
return point.x >= rect.left && point.x <= rect.left + rect.size && point.y >= rect.top && point.y <= rect.top + rect.size;
}
+80 -16
View File
@@ -1,7 +1,7 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { enforceRightSidebarPanelExclusivity, EXECUTE_MODE_CURRENT_DEFAULT_VERSION, normalizeAiConfig, normalizeDesktopSettings, normalizeEditorSettings, normalizeMcpGlobalPolicy, transitionRightSidebarPanels, type RightSidebarPanelState } from "@/stores/settingsStore";
import { createPinia, setActivePinia } from "pinia";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { isProxy } from "vue";
import { EXECUTE_MODE_CURRENT_DEFAULT_VERSION, enforceRightSidebarPanelExclusivity, normalizeAiConfig, normalizeDesktopSettings, normalizeEditorSettings, normalizeMcpGlobalPolicy, type RightSidebarPanelState, transitionRightSidebarPanels } from "@/stores/settingsStore";
import type { AiConfigItem } from "@/types/ai";
describe("normalizeEditorSettings", () => {
@@ -31,7 +31,12 @@ describe("normalizeEditorSettings", () => {
expect(normalizeEditorSettings({ sidebarTableCommentLayout: "hidden" } as any).sidebarObjectInfoMode).toBe("hidden");
expect(normalizeEditorSettings({ sidebarHideTableComments: false } as any).sidebarObjectInfoMode).toBe("comment-inline");
expect(normalizeEditorSettings({ sidebarHideTableComments: true } as any).sidebarObjectInfoMode).toBe("hidden");
expect(normalizeEditorSettings({ sidebarHideTableComments: true, sidebarShowDatabaseSizes: true } as any).sidebarObjectInfoMode).toBe("hidden");
expect(
normalizeEditorSettings({
sidebarHideTableComments: true,
sidebarShowDatabaseSizes: true,
} as any).sidebarObjectInfoMode,
).toBe("hidden");
expect(normalizeEditorSettings({ sidebarShowDatabaseSizes: true } as any).sidebarObjectInfoMode).toBe("size");
expect(normalizeEditorSettings({ sidebarObjectInfoMode: "invalid" } as any).sidebarObjectInfoMode).toBe("comment-inline");
});
@@ -39,7 +44,12 @@ describe("normalizeEditorSettings", () => {
it("defaults SQL execution to the current statement and migrates legacy execute-all settings", () => {
expect(normalizeEditorSettings({}).executeMode).toBe("current");
expect(normalizeEditorSettings({ executeMode: "all" }).executeMode).toBe("current");
expect(normalizeEditorSettings({ executeMode: "all", executeModeDefaultVersion: EXECUTE_MODE_CURRENT_DEFAULT_VERSION }).executeMode).toBe("all");
expect(
normalizeEditorSettings({
executeMode: "all",
executeModeDefaultVersion: EXECUTE_MODE_CURRENT_DEFAULT_VERSION,
}).executeMode,
).toBe("all");
});
it("enables automatic table aliases by default", () => {
@@ -142,8 +152,17 @@ describe("normalizeEditorSettings", () => {
it("normalizes persistent extractor configuration fail-fast defaults", () => {
const defaults = normalizeEditorSettings({}).dataGridExtractorOptions;
expect(defaults.dsv).toMatchObject({ columnSeparator: ",", rowSeparator: "\n", quote: '"', quotePolicy: "minimal" });
expect(defaults.sql).toMatchObject({ skipComputedColumns: true, skipGeneratedColumns: true, insertMode: "merged" });
expect(defaults.dsv).toMatchObject({
columnSeparator: ",",
rowSeparator: "\n",
quote: '"',
quotePolicy: "minimal",
});
expect(defaults.sql).toMatchObject({
skipComputedColumns: true,
skipGeneratedColumns: true,
insertMode: "merged",
});
const configured = normalizeEditorSettings({
dataGridExtractorOptions: {
@@ -168,14 +187,25 @@ describe("normalizeEditorSettings", () => {
const defaults = normalizeEditorSettings({});
expect(defaults.dataGridMultiRowTranspose).toBe(false);
expect(defaults.dataGridHideNullColumns).toBe(false);
expect(defaults.dataGridBooleanDisplayMode).toBe("dropdown");
const enabled = normalizeEditorSettings({ dataGridMultiRowTranspose: true, dataGridHideNullColumns: true });
const enabled = normalizeEditorSettings({
dataGridMultiRowTranspose: true,
dataGridHideNullColumns: true,
dataGridBooleanDisplayMode: "dropdown",
});
expect(enabled.dataGridMultiRowTranspose).toBe(true);
expect(enabled.dataGridHideNullColumns).toBe(true);
expect(enabled.dataGridBooleanDisplayMode).toBe("dropdown");
const invalid = normalizeEditorSettings({ dataGridMultiRowTranspose: "true" as any, dataGridHideNullColumns: 1 as any });
const invalid = normalizeEditorSettings({
dataGridMultiRowTranspose: "true" as any,
dataGridHideNullColumns: 1 as any,
dataGridBooleanDisplayMode: "invalid" as any,
});
expect(invalid.dataGridMultiRowTranspose).toBe(false);
expect(invalid.dataGridHideNullColumns).toBe(false);
expect(invalid.dataGridBooleanDisplayMode).toBe("dropdown");
});
it("defaults the data grid font and preserves a custom font family", () => {
@@ -372,7 +402,10 @@ describe("settingsStore AI API key normalization", () => {
const { useSettingsStore } = await import("@/stores/settingsStore");
const store = useSettingsStore();
const config = makeTestConfig({ id: "trimmed-key", apiKey: " \tsecret\r\n" });
const config = makeTestConfig({
id: "trimmed-key",
apiKey: " \tsecret\r\n",
});
await store.createAiConfig(config);
@@ -411,7 +444,10 @@ describe("settingsStore MCP policy persistence", () => {
};
store.mcpGlobalPolicy = previous;
const update = store.updateMcpGlobalPolicy({ readOnly: false, allowedConnectionIds: [] });
const update = store.updateMcpGlobalPolicy({
readOnly: false,
allowedConnectionIds: [],
});
expect(store.mcpGlobalPolicy).toEqual({
readOnly: false,
allowDangerousSql: false,
@@ -611,7 +647,13 @@ describe("settingsStore activeModel lifecycle", () => {
loadAiChatSelection: vi.fn().mockResolvedValue({
version: 1,
active: { configId: "c1", modelId: "runtime-model" },
effortPreferences: [{ configId: "c1", modelId: "runtime-model", selection: { kind: "enum", value: "high" } }],
effortPreferences: [
{
configId: "c1",
modelId: "runtime-model",
selection: { kind: "enum", value: "high" },
},
],
}),
saveAiChatSelection: vi.fn().mockResolvedValue(undefined),
}));
@@ -620,7 +662,10 @@ describe("settingsStore activeModel lifecycle", () => {
const store = useSettingsStore();
await store.initAiConfigs();
expect(store.activeModel).toEqual({ configId: "c1", modelId: "runtime-model" });
expect(store.activeModel).toEqual({
configId: "c1",
modelId: "runtime-model",
});
expect(store.activeEffort).toEqual({ kind: "enum", value: "high" });
});
@@ -654,7 +699,13 @@ describe("settingsStore activeModel lifecycle", () => {
store.updateActiveEffort({ kind: "enum", value: "high" });
await store.updateAiConfigItem("c1", { provider: "gemini" });
await vi.waitFor(() => expect(saveAiChatSelection).toHaveBeenLastCalledWith({ version: 1, active: undefined, effortPreferences: [] }));
await vi.waitFor(() =>
expect(saveAiChatSelection).toHaveBeenLastCalledWith({
version: 1,
active: undefined,
effortPreferences: [],
}),
);
expect(saveAiConfigItem).toHaveBeenCalledWith(expect.objectContaining({ id: "c1", provider: "gemini" }));
expect(store.activeModel).toBeNull();
@@ -675,9 +726,16 @@ describe("settingsStore activeModel lifecycle", () => {
store.updateActiveModel({ configId: "c1", modelId: "gpt-5" });
store.updateActiveEffort({ kind: "enum", value: "high" });
await store.updateAiConfigItem("c1", { endpoint: "https://gateway.example/v1" });
await store.updateAiConfigItem("c1", {
endpoint: "https://gateway.example/v1",
});
expect(saveAiConfigItem).toHaveBeenCalledWith(expect.objectContaining({ id: "c1", endpoint: "https://gateway.example/v1" }));
expect(saveAiConfigItem).toHaveBeenCalledWith(
expect.objectContaining({
id: "c1",
endpoint: "https://gateway.example/v1",
}),
);
expect(store.activeModel).toEqual({ configId: "c1", modelId: "gpt-5" });
expect(store.activeEffort).toEqual({ kind: "enum", value: "high" });
});
@@ -707,7 +765,13 @@ describe("settingsStore activeModel lifecycle", () => {
expect(saveAiChatSelection.mock.calls[1][0]).toEqual({
version: 1,
active: { configId: "c1", modelId: "model-a" },
effortPreferences: [{ configId: "c1", modelId: "model-a", selection: { kind: "enum", value: "high" } }],
effortPreferences: [
{
configId: "c1",
modelId: "model-a",
selection: { kind: "enum", value: "high" },
},
],
});
});
+20 -18
View File
@@ -1,29 +1,27 @@
import { defineStore } from "pinia";
import { ref, computed } from "vue";
import { computed, ref } from "vue";
import { aiConfigToItem, generateId, getConfigKey } from "@/lib/ai/aiConfigList";
import { DEFAULT_DATA_GRID_FONT_FAMILY, DEFAULT_UI_FONT_FAMILY } from "@/lib/app/appFonts";
import * as api from "@/lib/backend/api";
import { generateId, getConfigKey, aiConfigToItem } from "@/lib/ai/aiConfigList";
import { normalizeColumnFormatter, normalizeCustomColumnFormatter, normalizeGlobalDateTimePattern, type ColumnFormatterConfig, type CustomColumnFormatterConfig } from "@/lib/dataGrid/columnFormatter";
import { normalizeShortcutSettings, type ShortcutSettings } from "@/lib/editor/shortcutRegistry";
import { setDebugLoggingEnabled } from "@/lib/backend/debugLog";
import { safeLocalStorageGet, safeLocalStorageRemove } from "@/lib/backend/safeStorage";
import { type ColumnFormatterConfig, type CustomColumnFormatterConfig, normalizeColumnFormatter, normalizeCustomColumnFormatter, normalizeGlobalDateTimePattern } from "@/lib/dataGrid/columnFormatter";
import { type DataGridCopyPreference, type DataGridExtractorOptions, DEFAULT_DATA_GRID_EXTRACTOR_OPTIONS, normalizeDataGridCopyPreference, normalizeDataGridExtractorOptions } from "@/lib/dataGrid/dataGridCopyExtractor";
import { normalizeResultPageSize } from "@/lib/dataGrid/paginationPageSize";
import { DEFAULT_DATA_GRID_EXTRACTOR_OPTIONS, normalizeDataGridCopyPreference, normalizeDataGridExtractorOptions, type DataGridCopyPreference, type DataGridExtractorOptions } from "@/lib/dataGrid/dataGridCopyExtractor";
import { normalizeSidebarHiddenTablePrefixes } from "@/lib/sidebar/sidebarTableNameDisplay";
import { normalizeShortcutSettings, type ShortcutSettings } from "@/lib/editor/shortcutRegistry";
import type { SavedSqlOpenTargetMode } from "@/lib/savedSql/savedSqlExecutionTarget";
import type { ConnectionListSortMode } from "@/lib/sidebar/connectionListSort";
import { normalizeSidebarHiddenTablePrefixes } from "@/lib/sidebar/sidebarTableNameDisplay";
import type { SidebarActivation } from "@/lib/sidebar/treeNodeClick";
import { DEFAULT_SQL_SNIPPETS } from "@/lib/sql/sqlCompletion";
import { DEFAULT_SQL_FORMATTER_SETTINGS, normalizeSqlFormatterSettings, type SqlFormatterSettings } from "@/lib/sql/sqlFormatterConfig";
import { normalizeSqlVariableSyntaxOverrides, type SqlVariableSyntaxOverrides } from "@/lib/sql/sqlVariableSyntax";
import type { SavedSqlOpenTargetMode } from "@/lib/savedSql/savedSqlExecutionTarget";
import type { SidebarActivation } from "@/lib/sidebar/treeNodeClick";
import { DEFAULT_DATA_TAB_REUSE_MODE, normalizeDataTabReuseMode, type DataTabReuseMode } from "@/lib/tabs/dataTabReuseMode";
import type { SqlSnippet, TableInfoTab } from "@/types/database";
import { DEFAULT_SQL_SNIPPETS } from "@/lib/sql/sqlCompletion";
import { setDebugLoggingEnabled } from "@/lib/backend/debugLog";
import { DEFAULT_TABLE_COLUMN_TEMPLATE_FIELDS, normalizeTableColumnTemplateFields } from "@/lib/table/tableColumnTemplates";
import { DEFAULT_DATA_GRID_FONT_FAMILY, DEFAULT_UI_FONT_FAMILY } from "@/lib/app/appFonts";
import { safeLocalStorageGet, safeLocalStorageRemove } from "@/lib/backend/safeStorage";
import type { AiProvider, AiApiStyle, AiAuthMethod, AiEffortLevel, AiReasoningLevel, AiConfiguredModel, AiConfig, AiTestConnectionResult, AiConfigItem, AiChatSelectionState, AiEffortSelection, AiModelEffortPreference } from "@/types/ai";
import { type DataTabReuseMode, DEFAULT_DATA_TAB_REUSE_MODE, normalizeDataTabReuseMode } from "@/lib/tabs/dataTabReuseMode";
import type { AiApiStyle, AiAuthMethod, AiChatSelectionState, AiConfig, AiConfigItem, AiConfiguredModel, AiEffortLevel, AiEffortSelection, AiModelEffortPreference, AiProvider, AiReasoningLevel, AiTestConnectionResult } from "@/types/ai";
import type { SqlSnippet, TableInfoTab } from "@/types/database";
export type { AiProvider, AiApiStyle, AiAuthMethod, AiEffortLevel, AiReasoningLevel, AiConfiguredModel, AiConfig, AiTestConnectionResult, AiConfigItem, AiChatSelectionState, AiEffortSelection };
export type { SavedSqlOpenTargetMode };
export type { DataTabReuseMode };
export type { AiApiStyle, AiAuthMethod, AiChatSelectionState, AiConfig, AiConfigItem, AiConfiguredModel, AiEffortLevel, AiEffortSelection, AiProvider, AiReasoningLevel, AiTestConnectionResult, DataTabReuseMode, SavedSqlOpenTargetMode };
export interface DesktopSettings {
show_tray_icon: boolean;
@@ -481,6 +479,7 @@ export interface EditorSettings {
dataGridAutoTransposeSingleRow: boolean;
dataGridMultiRowTranspose: boolean;
dataGridHideNullColumns: boolean;
dataGridBooleanDisplayMode: "dropdown" | "checkbox";
numericColumnRightAlign: boolean;
tableFontFamily: string;
tableFontSize: number;
@@ -660,6 +659,7 @@ export const DEFAULT_EDITOR_SETTINGS: EditorSettings = {
dataGridAutoTransposeSingleRow: false,
dataGridMultiRowTranspose: false,
dataGridHideNullColumns: false,
dataGridBooleanDisplayMode: "dropdown",
numericColumnRightAlign: true,
tableFontFamily: DEFAULT_DATA_GRID_FONT_FAMILY,
tableFontSize: TABLE_FONT_SIZE_DEFAULT,
@@ -970,6 +970,7 @@ export function normalizeEditorSettings(settings: Partial<EditorSettings>, exist
dataGridAutoTransposeSingleRow: settings.dataGridAutoTransposeSingleRow === true,
dataGridMultiRowTranspose: settings.dataGridMultiRowTranspose === true,
dataGridHideNullColumns: settings.dataGridHideNullColumns === true,
dataGridBooleanDisplayMode: settings.dataGridBooleanDisplayMode === "checkbox" ? "checkbox" : "dropdown",
numericColumnRightAlign: typeof settings.numericColumnRightAlign === "boolean" ? settings.numericColumnRightAlign : DEFAULT_EDITOR_SETTINGS.numericColumnRightAlign,
tableFontFamily: normalizeFontFamily(settings.tableFontFamily, DEFAULT_EDITOR_SETTINGS.tableFontFamily),
tableFontSize: normalizeTableFontSize(settings.tableFontSize),
@@ -1464,6 +1465,7 @@ export const useSettingsStore = defineStore("settings", () => {
if (partial.dataGridAutoTransposeSingleRow !== undefined) editorSettings.value.dataGridAutoTransposeSingleRow = partial.dataGridAutoTransposeSingleRow === true;
if (partial.dataGridMultiRowTranspose !== undefined) editorSettings.value.dataGridMultiRowTranspose = partial.dataGridMultiRowTranspose === true;
if (partial.dataGridHideNullColumns !== undefined) editorSettings.value.dataGridHideNullColumns = partial.dataGridHideNullColumns === true;
if (partial.dataGridBooleanDisplayMode !== undefined) editorSettings.value.dataGridBooleanDisplayMode = partial.dataGridBooleanDisplayMode === "dropdown" ? "dropdown" : "checkbox";
if (partial.numericColumnRightAlign !== undefined) editorSettings.value.numericColumnRightAlign = partial.numericColumnRightAlign === true;
if (partial.tableFontFamily !== undefined) editorSettings.value.tableFontFamily = normalizeFontFamily(partial.tableFontFamily, DEFAULT_EDITOR_SETTINGS.tableFontFamily);
if (partial.tableFontSize !== undefined) editorSettings.value.tableFontSize = normalizeTableFontSize(partial.tableFontSize);