feat(grid): add row/column crosshair highlight setting

This commit is contained in:
Abeautifulsnow authored and GitHub committed 2026-08-27 22:23:13 +08:00
1 parent b09c6f4f21
commit ec0b6768be
18 files changed
+496 -2

No files matched your search

@@ -415,6 +415,7 @@ const dataGridFilterViewPreviewExpanded = ref(true);
const editDataGridTextFilterPanelHeight = ref(settingsStore.editorSettings.dataGridTextFilterPanelHeight);
const editDataGridAutoTransposeSingleRow = ref(settingsStore.editorSettings.dataGridAutoTransposeSingleRow);
const editDataGridCellDetailButtonVisible = ref(settingsStore.editorSettings.dataGridCellDetailButtonVisible);
const editDataGridCrosshairHighlight = ref(settingsStore.editorSettings.dataGridCrosshairHighlight);
const editPageSize = ref(settingsStore.editorSettings.pageSize);
const editTableOpenPageSize = ref(settingsStore.editorSettings.tableOpenPageSize);
const editQueryResultMaxRowsEnabled = ref(settingsStore.editorSettings.queryResultMaxRowsEnabled);
@@ -607,6 +608,7 @@ function currentEditorSettingsDraft(): EditorSettingsDraft {
dataGridTextFilterPanelHeight: editDataGridTextFilterPanelHeight.value,
dataGridAutoTransposeSingleRow: editDataGridAutoTransposeSingleRow.value,
dataGridCellDetailButtonVisible: editDataGridCellDetailButtonVisible.value,
dataGridCrosshairHighlight: editDataGridCrosshairHighlight.value,
flatteningMultiLineText: editFlatteningMultiLineText.value,
pageSize: editPageSize.value,
tableOpenPageSize: editTableOpenPageSize.value,
@@ -871,6 +873,7 @@ function syncEditorSettingsDraftFromStore() {
editDataGridTextFilterPanelHeight.value = settingsStore.editorSettings.dataGridTextFilterPanelHeight;
editDataGridAutoTransposeSingleRow.value = settingsStore.editorSettings.dataGridAutoTransposeSingleRow;
editDataGridCellDetailButtonVisible.value = settingsStore.editorSettings.dataGridCellDetailButtonVisible;
editDataGridCrosshairHighlight.value = settingsStore.editorSettings.dataGridCrosshairHighlight;
editFlatteningMultiLineText.value = settingsStore.editorSettings.flatteningMultiLineText;
editPageSize.value = settingsStore.editorSettings.pageSize;
editTableOpenPageSize.value = settingsStore.editorSettings.tableOpenPageSize;
@@ -1170,6 +1173,7 @@ function resetDefaultsForTab(tab: SettingsCategory) {
editDataGridTextFilterPanelHeight.value = DEFAULT_EDITOR_SETTINGS.dataGridTextFilterPanelHeight;
editDataGridAutoTransposeSingleRow.value = DEFAULT_EDITOR_SETTINGS.dataGridAutoTransposeSingleRow;
editDataGridCellDetailButtonVisible.value = DEFAULT_EDITOR_SETTINGS.dataGridCellDetailButtonVisible;
editDataGridCrosshairHighlight.value = DEFAULT_EDITOR_SETTINGS.dataGridCrosshairHighlight;
editFlatteningMultiLineText.value = DEFAULT_EDITOR_SETTINGS.flatteningMultiLineText;
editPageSize.value = DEFAULT_EDITOR_SETTINGS.pageSize;
editTableOpenPageSize.value = DEFAULT_EDITOR_SETTINGS.tableOpenPageSize;
@@ -1252,6 +1256,7 @@ function resetAllDefaults() {
editDataGridTextFilterPanelHeight.value = DEFAULT_EDITOR_SETTINGS.dataGridTextFilterPanelHeight;
editDataGridAutoTransposeSingleRow.value = DEFAULT_EDITOR_SETTINGS.dataGridAutoTransposeSingleRow;
editDataGridCellDetailButtonVisible.value = DEFAULT_EDITOR_SETTINGS.dataGridCellDetailButtonVisible;
editDataGridCrosshairHighlight.value = DEFAULT_EDITOR_SETTINGS.dataGridCrosshairHighlight;
editFlatteningMultiLineText.value = DEFAULT_EDITOR_SETTINGS.flatteningMultiLineText;
editPageSize.value = DEFAULT_EDITOR_SETTINGS.pageSize;
editTableOpenPageSize.value = DEFAULT_EDITOR_SETTINGS.tableOpenPageSize;
@@ -5509,6 +5514,17 @@ onUnmounted(() => {
</div>
<Switch id="data-grid-cell-detail-button-visible" v-model="editDataGridCellDetailButtonVisible" />
</div>
<div class="flex items-center justify-between gap-4 rounded-md border bg-muted/20 px-3 py-2">
<div class="space-y-1">
<Label for="data-grid-crosshair-highlight">
{{ t("settings.dataGridCrosshairHighlight") }}
</Label>
<p class="text-xs text-muted-foreground">
{{ t("settings.dataGridCrosshairHighlightDescription") }}
</p>
</div>
<Switch id="data-grid-crosshair-highlight" v-model="editDataGridCrosshairHighlight" />
</div>
<div class="flex items-center justify-between gap-4 rounded-md border bg-muted/20 px-3 py-2">
<div class="space-y-1">
<Label for="flattening-multi-line">
@@ -231,6 +231,7 @@ import {
} from "@/lib/dataGrid/dataGridInfiniteScroll";
import { resolveDataGridWheelScroll } from "@/lib/dataGrid/dataGridWheel";
import { CANVAS_DATA_GRID_ROW_HEIGHT, MAX_CANVAS_DATA_GRID_PIXEL_RATIO, canvasDataGridActionOverlayWidth, canvasDataGridActionReservedWidth, dataGridSearchMatchKey, drawCanvasDataGrid, type CanvasDevicePixelSize } from "@/lib/dataGrid/canvasDataGridRenderer";
import { resolveCrosshairTarget, type CrosshairTarget } from "@/lib/dataGrid/crosshairHighlight";
import { DATA_GRID_DARK_STRIPED_ROW_BG, DATA_GRID_LIGHT_STRIPED_ROW_BG, dataGridActiveRowBackground } from "@/lib/dataGrid/dataGridPaintTheme";
import { createRowLowerTextCache } from "@/lib/dataGrid/dataGridRowLowerText";
import { dataGridPreviewLabelKey, dataGridSaveActionMode, dataGridSaveToolbarState } from "@/lib/dataGrid/dataGridSaveUi";
@@ -387,6 +388,7 @@ const connectionStore = useConnectionStore();
const queryStore = useQueryStore();
const settingsStore = useSettingsStore();
const cellDetailButtonEnabled = computed(() => settingsStore.editorSettings.dataGridCellDetailButtonVisible);
const dataGridCrosshairHighlight = computed(() => settingsStore.editorSettings.dataGridCrosshairHighlight);
const tableFontSize = computed(() => settingsStore.editorSettings.tableFontSize);
const rowNumberWidth = ref(DATA_GRID_ROW_NUM_WIDTH);
const largeValueResolutionVersion = ref(0);
@@ -5550,6 +5552,29 @@ const {
isRowSelected,
} = selection;
// 行列十字高亮目标:以 selectionFocus(原始坐标系)为中心解析,整行/整列选中时从
// lastClickedRowIndex / selectedRowIds / selectedColumnIndexes 派生。开关关闭时恒为 null。
const crosshairTarget = computed<CrosshairTarget | null>(() => {
if (!dataGridCrosshairHighlight.value) return null;
let fallbackRowIndex = selection.lastClickedRowIndex.value;
if (fallbackRowIndex === null && selectedRowIds.value.size > 0) {
const firstSelectedId = [...selectedRowIds.value][0];
const firstSelectedIndex = displayItems.value.findIndex((item) => item.id === firstSelectedId);
fallbackRowIndex = firstSelectedIndex >= 0 ? firstSelectedIndex : null;
}
if (fallbackRowIndex === null && transposeRowIndex.value !== null) fallbackRowIndex = transposeRowIndex.value;
const fallbackColumnIndex = selectedColumnIndexes.value.size > 0 ? [...selectedColumnIndexes.value][0] : null;
return resolveCrosshairTarget({
selectionFocus: selectionFocus.value,
selectionAnchor: selectionAnchor.value,
hasRowSelection: hasRowSelection.value,
hasColumnSelection: hasColumnSelection.value,
visibleColumnIndexes: visibleColumnIndexes.value,
fallbackRowIndex,
fallbackColumnIndex,
});
});
function selectionCaptureBase(): Omit<CaptureDataGridSelectionOptions, "selectedRowIds" | "selectedColumnIndexes" | "selectedCellKeys" | "selectionAnchor" | "selectionFocus" | "selectingAll" | "lastClickedRowIndex"> {
return {
columns: props.result.columns,
@@ -7674,6 +7699,7 @@ function drawCanvasGrid() {
columnAligns: columnAligns.value,
columnTypeVisualKinds: visibleColumnTypeVisualKinds.value,
colorizeDataTypes: colorizeDataGridCellTypes.value,
crosshair: crosshairTarget.value,
rightAlignedActionCell: canvasRightAlignedActionCell.value,
columnIsBoolean: isBooleanGridColumn,
booleanDisplayMode: booleanDisplayMode.value,
@@ -12295,6 +12321,7 @@ function openGridSnapshot() {
'transpose-record-header-selected text-primary font-semibold': transposeRecordUsesFramedHeader(recordIndex),
'transpose-record-header-active text-primary': transposeRecordUsesActiveHighlight(recordIndex) && !transposeRecordUsesFramedHeader(recordIndex),
'data-grid-header-cell': !transposeRecordUsesActiveHighlight(recordIndex) && !transposeRecordUsesFramedHeader(recordIndex),
'crosshair-column': !!crosshairTarget?.rowCrosshair && crosshairTarget.rowIndex === recordIndex && !transposeRecordUsesFramedHeader(recordIndex) && !transposeRecordUsesActiveHighlight(recordIndex),
}"
:style="{
width: `${getTransposeRecordWidth(recordIndex)}px`,
@@ -12374,6 +12401,8 @@ function openGridSnapshot() {
'cell-selected-dirty--sparse': transposeCellIsSelected(cell.recordIndex, cell.valueIndex) && displayItems[cell.recordIndex]?.isDirtyCol[cell.valueIndex],
'row-cell-selected': transposeRecordUsesSelectionVisual(cell.recordIndex) && !transposeCellIsSelected(cell.recordIndex, cell.valueIndex) && !displayItems[cell.recordIndex]?.isDirtyCol[cell.valueIndex],
'row-cell-selected-dirty': transposeRecordUsesSelectionVisual(cell.recordIndex) && !transposeCellIsSelected(cell.recordIndex, cell.valueIndex) && displayItems[cell.recordIndex]?.isDirtyCol[cell.valueIndex],
'crosshair-row':
!!crosshairTarget?.columnCrosshair && cell.valueIndex === crosshairTarget.actualColIdx && !transposeRecordUsesSelectionVisual(cell.recordIndex) && !displayItems[cell.recordIndex]?.isDirtyCol[cell.valueIndex] && !transposeCellIsSelected(cell.recordIndex, cell.valueIndex),
'bg-primary/15': transposeRecordUsesActiveHighlight(cell.recordIndex) && !transposeRecordUsesSelectionVisual(cell.recordIndex) && !displayItems[cell.recordIndex]?.isDirtyCol[cell.valueIndex] && !transposeCellIsSelected(cell.recordIndex, cell.valueIndex),
'bg-yellow-500/10 cell-dirty': displayItems[cell.recordIndex]?.isDirtyCol[cell.valueIndex],
'bg-yellow-200/60 dark:bg-yellow-500/20': cellIsSearchMatch(cell.recordIndex, cell.valueIndex),
@@ -12520,6 +12549,7 @@ function openGridSnapshot() {
:name="col.name"
:actual-column-index="col.actualColIdx"
:visible-column-index="col.visibleColIdx"
:class="{ 'crosshair-column': !!crosshairTarget?.columnCrosshair && crosshairTarget.visibleColIdx === col.visibleColIdx }"
:selected="highlightedColumnIndex === col.actualColIdx || columnIsSelected(col.visibleColIdx)"
:search-match="currentSearchMatch?.kind === 'column' && currentSearchMatch.col === col.actualColIdx"
:dark="isDark"
@@ -13275,6 +13305,7 @@ function openGridSnapshot() {
'data-grid-row--draft': item.isDraft && !isRowActive(item.displayIndex),
'data-grid-row--striped': !item.isNew && !item.isDraft && !item.isDeleted && !isRowActive(item.displayIndex) && item.displayIndex % 2 === 1,
'active-row': isRowActive(item.displayIndex) && !item.isDeleted,
'crosshair-row': !!crosshairTarget?.rowCrosshair && crosshairTarget.rowIndex === item.displayIndex && !item.isDeleted,
'relative z-20 overflow-visible': editingCell?.rowId === item.id || readonlyTextCell?.rowId === item.id,
}"
:style="dataGridRowStyle(item)"
@@ -13321,6 +13352,7 @@ function openGridSnapshot() {
'cell-selected-dirty--sparse': selectionFramesData.sparse && cellIsSelected(item.displayIndex, col.visibleColIdx) && item.isDirtyCol[col.actualColIdx],
'row-cell-selected': rowCellsUseSelectionVisual(item.id) && !cellIsSelected(item.displayIndex, col.visibleColIdx) && !item.isDirtyCol[col.actualColIdx],
'row-cell-selected-dirty': rowCellsUseSelectionVisual(item.id) && !cellIsSelected(item.displayIndex, col.visibleColIdx) && item.isDirtyCol[col.actualColIdx],
'crosshair-column': !!crosshairTarget?.columnCrosshair && crosshairTarget.visibleColIdx === col.visibleColIdx && !item.isDeleted,
'cell-search-match': cellIsSearchMatch(item.displayIndex, col.actualColIdx),
'cell-current-search-match': cellIsCurrentMatch(item.displayIndex, col.actualColIdx),
'bg-yellow-200/60 dark:bg-yellow-500/20': cellIsSearchMatch(item.displayIndex, col.actualColIdx),
@@ -14164,6 +14196,8 @@ function openGridSnapshot() {
--data-grid-row-new-bg: rgb(243, 243, 243);
--data-grid-row-deleted-bg: rgb(255, 244, 244);
--data-grid-cell-active-bg: rgb(244, 248, 255);
--data-grid-cell-crosshair-row-bg: rgb(174, 195, 224);
--data-grid-cell-crosshair-col-bg: rgb(142, 170, 210);
--data-grid-cell-dirty-bg: rgb(255, 248, 230);
--data-grid-cell-selected-bg: rgb(239, 246, 255);
--data-grid-cell-selected-single-bg: rgb(191, 219, 254);
@@ -14196,6 +14230,8 @@ function openGridSnapshot() {
--data-grid-row-new-bg: rgb(51, 51, 55);
--data-grid-row-deleted-bg: rgb(55, 31, 32);
--data-grid-cell-active-bg: rgb(25, 34, 46);
--data-grid-cell-crosshair-row-bg: rgb(75, 84, 98);
--data-grid-cell-crosshair-col-bg: rgb(98, 111, 130);
--data-grid-cell-dirty-bg: rgb(94, 75, 26);
--data-grid-cell-selected-bg: rgb(20, 40, 60);
--data-grid-cell-selected-single-bg: rgb(30, 64, 96);
@@ -14229,6 +14265,8 @@ function openGridSnapshot() {
--data-grid-row-new-bg: color-mix(in oklab, var(--primary) 5%, transparent);
--data-grid-row-deleted-bg: color-mix(in oklab, var(--destructive) 5%, transparent);
--data-grid-cell-dirty-bg: color-mix(in oklab, rgb(240 177 0) 10%, transparent);
--data-grid-cell-crosshair-row-bg: color-mix(in srgb, var(--primary) 34%, var(--background));
--data-grid-cell-crosshair-col-bg: color-mix(in srgb, var(--primary) 50%, var(--background));
--data-grid-cell-selected-bg: color-mix(in oklab, rgb(59 130 246) 12%, var(--background));
--data-grid-cell-selected-single-bg: color-mix(in oklab, rgb(59 130 246) 30%, var(--background));
--data-grid-cell-selected-dirty-bg: color-mix(in oklab, rgb(234 181 50) 30%, color-mix(in oklab, rgb(59 130 246) 18%, var(--background)));
@@ -14239,6 +14277,11 @@ function openGridSnapshot() {
--data-grid-row-number-deleted-bg: color-mix(in oklab, var(--destructive) 15%, var(--background));
--data-grid-row-number-selected-bg: color-mix(in oklab, rgb(59 130 246) 30%, var(--background));
}
[data-grid-root].data-grid--dark,
:global(.dark) [data-grid-root] {
--data-grid-cell-crosshair-row-bg: color-mix(in srgb, var(--primary) 34%, var(--background));
--data-grid-cell-crosshair-col-bg: color-mix(in srgb, var(--primary) 50%, var(--background));
}
[data-grid-root].data-grid--has-save-error {
--data-grid-cell-dirty-bg: rgb(250, 212, 216) !important;
--data-grid-cell-selected-dirty-bg: rgb(240, 192, 198) !important;
@@ -14301,6 +14344,18 @@ function openGridSnapshot() {
background-color: var(--data-grid-cell-bg);
}
/* 行列十字高亮:覆盖基础单元格变量而非只涂父行,避免不透明 data-grid-cell /
* frozen cell 遮住行高亮。选中、脏数据和搜索状态仍用各自的显式背景覆盖它。 */
.crosshair-row {
--data-grid-cell-bg: var(--data-grid-cell-crosshair-row-bg) !important;
background-color: var(--data-grid-cell-crosshair-row-bg);
}
.crosshair-column {
--data-grid-cell-bg: var(--data-grid-cell-crosshair-col-bg) !important;
background-color: var(--data-grid-cell-crosshair-col-bg);
}
.data-grid-cell {
background-color: var(--data-grid-cell-bg);
}
+2
View File
@@ -6341,6 +6341,8 @@ export default {
dataGridAutoTransposeSingleRowDescription: "When enabled, SQL query results with exactly one row and multiple columns automatically switch to transpose view.",
dataGridCellDetailButtonVisible: "Show cell detail button",
dataGridCellDetailButtonVisibleDescription: "Show the cell detail button when hovering over a cell.",
dataGridCrosshairHighlight: "Crosshair row & column highlight",
dataGridCrosshairHighlightDescription: "When enabled, lightly highlight the entire row and column of the active cell (like Excel). The focused cell keeps its selected style.",
infiniteScroll: "Infinite scroll loading",
autoCalculateTotalRows: "Auto-calculate total row count",
autoCalculateTotalRowsDescription: "Run COUNT(*) automatically after each query to show the total matching rows. Off by default to keep large queries fast — calculate it on demand from the result footer.",
+2
View File
@@ -6043,6 +6043,8 @@ export default withEnglishFallback({
dataGridAutoTransposeSingleRowDescription: "Al activarlo, los resultados de consultas SQL con exactamente una fila y varias columnas cambian automáticamente a la vista transpuesta.",
dataGridCellDetailButtonVisible: "Mostrar botón de detalles de celda",
dataGridCellDetailButtonVisibleDescription: "Mostrar el botón de detalles de celda al pasar el cursor sobre una celda.",
dataGridCrosshairHighlight: "Resaltado de fila y columna en cruz",
dataGridCrosshairHighlightDescription: "Cuando esté activado, resalta ligeramente toda la fila y la columna de la celda activa (como Excel). La celda enfocada conserva su estilo seleccionado.",
infiniteScroll: "Carga de desplazamiento infinito",
autoCalculateTotalRows: "Calcular automáticamente el total de filas",
autoCalculateTotalRowsDescription: "Ejecuta COUNT(*) automáticamente tras cada consulta para mostrar el total de filas coincidentes. Desactivado por defecto para mantener rápidas las consultas grandes; puedes calcularlo cuando quieras desde el pie de resultados.",
+2
View File
@@ -6043,6 +6043,8 @@ export default withEnglishFallback({
dataGridAutoTransposeSingleRowDescription: "Quando è attivata, i risultati delle query SQL con esattamente una riga e più colonne passano automaticamente alla vista trasposta.",
dataGridCellDetailButtonVisible: "Mostra il pulsante dei dettagli della cella",
dataGridCellDetailButtonVisibleDescription: "Mostra il pulsante dei dettagli della cella al passaggio del mouse su una cella.",
dataGridCrosshairHighlight: "Evidenzia riga e colonna a croce",
dataGridCrosshairHighlightDescription: "Se attivato, evidenzia leggermente l'intera riga e la colonna della cella attiva (come Excel). La cella focalizzata mantiene il suo stile selezionato.",
infiniteScroll: "Caricamento a scorrimento infinito",
autoCalculateTotalRows: "Calcola automaticamente il totale delle righe",
autoCalculateTotalRowsDescription: "Esegue COUNT(*) automaticamente dopo ogni query per mostrare il totale delle righe corrispondenti. Disattivato per impostazione predefinita per mantenere veloci le query grandi; puoi calcolarlo all'occorrenza dal piè di pagina dei risultati.",
+2
View File
@@ -6077,6 +6077,8 @@ export default withEnglishFallback({
dataGridAutoTransposeSingleRowDescription: "有効にすると、1 行かつ複数列の SQL クエリ結果が自動的に転置表示に切り替わります。",
dataGridCellDetailButtonVisible: "セル詳細ボタンを表示",
dataGridCellDetailButtonVisibleDescription: "セルにカーソルを合わせたときにセル詳細ボタンを表示します。",
dataGridCrosshairHighlight: "行と列の十字ハイライト",
dataGridCrosshairHighlightDescription: "有効にすると、アクティブなセルの行と列全体を淡くハイライトします(Excel と同様)。フォーカスされたセルは選択スタイルを維持します。",
tableColumnTemplateFields: "新規テーブルのプリセット列",
tableColumnTemplateFieldsDescription: "データベース種別を選択し、新規テーブル作成時に使うプリセット列の型を設定します。",
tableColumnTemplateAdd: "列を追加",
+2
View File
@@ -5738,6 +5738,8 @@ export default withEnglishFallback({
dataGridAutoTransposeSingleRowDescription: "활성화하면 정확히 한 행과 여러 열이 있는 SQL 쿼리 결과가 자동으로 전치 보기로 전환됩니다.",
dataGridCellDetailButtonVisible: "셀 세부 정보 버튼 표시",
dataGridCellDetailButtonVisibleDescription: "셀 위에 마우스를 올리면 셀 세부 정보 버튼을 표시합니다.",
dataGridCrosshairHighlight: "행·열 십자 강조",
dataGridCrosshairHighlightDescription: "활성화하면 활성 셀의 전체 행과 열을 연하게 강조합니다(Excel과 유사). 포커스 셀은 기존 선택 스타일을 유지합니다.",
infiniteScroll: "무한 스크롤 로딩",
autoCalculateTotalRows: "전체 행 수 자동 집계",
autoCalculateTotalRowsDescription: "각 쿼리 후 자동으로 COUNT(*)를 실행하여 일치하는 전체 행 수를 표시합니다. 대규모 쿼리를 빠르게 유지하기 위해 기본적으로 꺼져 있으며, 결과 푸터에서 요청 시 집계할 수 있습니다.",
+2
View File
@@ -6045,6 +6045,8 @@ export default withEnglishFallback({
dataGridAutoTransposeSingleRowDescription: "Quando ativado, os resultados de consultas SQL com exatamente uma linha e várias colunas alternam automaticamente para a visualização transposta.",
dataGridCellDetailButtonVisible: "Mostrar botão de detalhes da célula",
dataGridCellDetailButtonVisibleDescription: "Mostrar o botão de detalhes da célula ao passar o mouse sobre uma célula.",
dataGridCrosshairHighlight: "Destacar linha e coluna em cruz",
dataGridCrosshairHighlightDescription: "Quando ativado, destaca levemente toda a linha e a coluna da célula ativa (como no Excel). A célula focada mantém o estilo selecionado.",
infiniteScroll: "Carregamento por rolagem infinita",
autoCalculateTotalRows: "Calcular automaticamente o total de linhas",
autoCalculateTotalRowsDescription: "Executa COUNT(*) automaticamente após cada consulta para mostrar o total de linhas correspondentes. Desativado por padrão para manter consultas grandes rápidas; você pode calculá-lo quando quiser no rodapé dos resultados.",
+2
View File
@@ -6325,6 +6325,8 @@ export default withEnglishFallback({
dataGridAutoTransposeSingleRowDescription: "开启后,SQL 查询结果只有一行且包含多列时,自动切换为转置视图。",
dataGridCellDetailButtonVisible: "显示单元格详情按钮",
dataGridCellDetailButtonVisibleDescription: "鼠标悬停在单元格上时显示单元格详情按钮。",
dataGridCrosshairHighlight: "行列十字高亮",
dataGridCrosshairHighlightDescription: "开启后,以当前活动单元格为中心淡色高亮整行和整列(类似 Excel)。焦点单元格仍保留原有的选中样式。",
infiniteScroll: "无限滚动加载",
autoCalculateTotalRows: "自动统计总行数",
autoCalculateTotalRowsDescription: "每次查询后自动执行 COUNT(*) 显示匹配的总行数。默认关闭以保证大查询速度 —— 可在结果栏按需手动统计。",
+2
View File
@@ -5362,6 +5362,8 @@ export default withEnglishFallback({
dataGridAutoTransposeSingleRowDescription: "開啟後,SQL 查詢結果只有一行且包含多個欄位時,會自動切換為轉置檢視。",
dataGridCellDetailButtonVisible: "顯示儲存格詳細資料按鈕",
dataGridCellDetailButtonVisibleDescription: "游標懸停在儲存格上時顯示儲存格詳細資料按鈕。",
dataGridCrosshairHighlight: "行列十字高亮",
dataGridCrosshairHighlightDescription: "開啟後,以目前活動儲存格為中心淡色高亮整列和整行(類似 Excel)。焦點儲存格仍保留原有選取樣式。",
infiniteScroll: "無限滾動載入",
autoCalculateTotalRows: "自動統計總筆數",
autoCalculateTotalRowsDescription: "每次查詢後自動執行 COUNT(*) 顯示符合的總筆數。預設關閉以確保大型查詢速度 —— 可在結果列按需手動統計。",
@@ -0,0 +1,174 @@
import { describe, expect, it } from "vitest";
import { resolveCrosshairTarget } from "@/lib/dataGrid/crosshairHighlight";
import type { CellPosition } from "@/lib/dataGrid/gridSelection";
function focus(rowIndex: number, colIndex: number): CellPosition {
return { rowIndex, colIndex };
}
describe("resolveCrosshairTarget", () => {
const visibleColumnIndexes = [3, 7, 11];
it("resolves both axes from selectionFocus for a plain cell", () => {
const target = resolveCrosshairTarget({
selectionFocus: focus(2, 1),
selectionAnchor: focus(2, 1),
hasRowSelection: false,
hasColumnSelection: false,
visibleColumnIndexes,
});
expect(target).toEqual({
rowIndex: 2,
visibleColIdx: 1,
actualColIdx: 7,
rowCrosshair: true,
columnCrosshair: true,
});
});
it("keeps both axes for a multi-cell range (anchor + focus)", () => {
const target = resolveCrosshairTarget({
selectionFocus: focus(5, 2),
selectionAnchor: focus(1, 0),
hasRowSelection: false,
hasColumnSelection: false,
visibleColumnIndexes,
});
expect(target).toEqual({
rowIndex: 5,
visibleColIdx: 2,
actualColIdx: 11,
rowCrosshair: true,
columnCrosshair: true,
});
});
it("only highlights the row when hasRowSelection is set alongside focus", () => {
const target = resolveCrosshairTarget({
selectionFocus: focus(4, 0),
selectionAnchor: focus(4, 0),
hasRowSelection: true,
hasColumnSelection: false,
visibleColumnIndexes,
});
expect(target?.rowCrosshair).toBe(true);
expect(target?.columnCrosshair).toBe(false);
expect(target?.rowIndex).toBe(4);
});
it("only highlights the column when hasColumnSelection is set alongside focus", () => {
const target = resolveCrosshairTarget({
selectionFocus: focus(4, 0),
selectionAnchor: focus(4, 0),
hasRowSelection: false,
hasColumnSelection: true,
visibleColumnIndexes,
});
expect(target?.rowCrosshair).toBe(false);
expect(target?.columnCrosshair).toBe(true);
expect(target?.visibleColIdx).toBe(0);
});
it("derives the row from fallbackRowIndex for a whole-row selection with no focus", () => {
const target = resolveCrosshairTarget({
selectionFocus: null,
selectionAnchor: null,
hasRowSelection: true,
hasColumnSelection: false,
visibleColumnIndexes,
fallbackRowIndex: 9,
});
expect(target).toEqual({
rowIndex: 9,
visibleColIdx: 0,
actualColIdx: 3,
rowCrosshair: true,
columnCrosshair: false,
});
});
it("returns null for a whole-row selection when fallbackRowIndex is missing", () => {
const target = resolveCrosshairTarget({
selectionFocus: null,
selectionAnchor: null,
hasRowSelection: true,
hasColumnSelection: false,
visibleColumnIndexes,
});
expect(target).toBeNull();
});
it("derives the column from fallbackColumnIndex for a whole-column selection with no focus", () => {
const target = resolveCrosshairTarget({
selectionFocus: null,
selectionAnchor: null,
hasRowSelection: false,
hasColumnSelection: true,
visibleColumnIndexes,
fallbackColumnIndex: 2,
});
expect(target).toEqual({
rowIndex: 0,
visibleColIdx: 2,
actualColIdx: 11,
rowCrosshair: false,
columnCrosshair: true,
});
});
it("returns null for a whole-column selection when fallbackColumnIndex is missing", () => {
const target = resolveCrosshairTarget({
selectionFocus: null,
selectionAnchor: null,
hasRowSelection: false,
hasColumnSelection: true,
visibleColumnIndexes,
});
expect(target).toBeNull();
});
it("returns null when there is no focus and no row/column selection", () => {
const target = resolveCrosshairTarget({
selectionFocus: null,
selectionAnchor: null,
hasRowSelection: false,
hasColumnSelection: false,
visibleColumnIndexes,
});
expect(target).toBeNull();
});
it("returns null when selectionFocus.colIndex is out of range for visibleColumnIndexes", () => {
const target = resolveCrosshairTarget({
selectionFocus: focus(0, 5),
selectionAnchor: focus(0, 5),
hasRowSelection: false,
hasColumnSelection: false,
visibleColumnIndexes,
});
expect(target).toBeNull();
});
it("returns null when fallbackColumnIndex is out of range for visibleColumnIndexes", () => {
const target = resolveCrosshairTarget({
selectionFocus: null,
selectionAnchor: null,
hasRowSelection: false,
hasColumnSelection: true,
visibleColumnIndexes,
fallbackColumnIndex: 9,
});
expect(target).toBeNull();
});
it("returns null when visibleColumnIndexes is empty even with a focus", () => {
const target = resolveCrosshairTarget({
selectionFocus: focus(0, 0),
selectionAnchor: focus(0, 0),
hasRowSelection: false,
hasColumnSelection: false,
visibleColumnIndexes: [],
});
expect(target).toBeNull();
});
});
@@ -1,6 +1,15 @@
import { describe, expect, it } from "vitest";
import { readFileSync } from "node:fs";
import { DATA_GRID_DARK_ACTIVE_ROW_BG, DATA_GRID_LIGHT_ACTIVE_ROW_BG, dataGridActiveRowBackground, resolveDataGridPaintTheme } from "@/lib/dataGrid/dataGridPaintTheme";
import {
DATA_GRID_DARK_ACTIVE_ROW_BG,
DATA_GRID_DARK_CROSSHAIR_COL_BG,
DATA_GRID_DARK_CROSSHAIR_ROW_BG,
DATA_GRID_LIGHT_ACTIVE_ROW_BG,
DATA_GRID_LIGHT_CROSSHAIR_COL_BG,
DATA_GRID_LIGHT_CROSSHAIR_ROW_BG,
dataGridActiveRowBackground,
resolveDataGridPaintTheme,
} from "@/lib/dataGrid/dataGridPaintTheme";
import { DEFAULT_DATA_GRID_TYPE_COLORS_DARK, DEFAULT_DATA_GRID_TYPE_COLORS_LIGHT, dataGridTypeColorCssVar } from "@/lib/dataGrid/dataGridTypeColorScheme";
function parseRgb(value: string): { r: number; g: number; b: number } | null {
@@ -42,6 +51,69 @@ describe("data grid paint theme", () => {
expect(resolveDataGridPaintTheme({ getVar: emptyCssVariable, isDark: true }).rowNumberActive).toBe(DATA_GRID_DARK_ACTIVE_ROW_BG);
});
it("resolves crosshair row/col fills in both color schemes", () => {
const emptyCssVariable = () => "";
const light = resolveDataGridPaintTheme({ getVar: emptyCssVariable, isDark: false });
const dark = resolveDataGridPaintTheme({ getVar: emptyCssVariable, isDark: true });
// 十字底色必须比 cellActive 更明显(与 cellActive 不同、列比行更深的蓝色系)
expect(light.cellCrosshairRow).toBe(DATA_GRID_LIGHT_CROSSHAIR_ROW_BG);
expect(light.cellCrosshairCol).toBe(DATA_GRID_LIGHT_CROSSHAIR_COL_BG);
expect(light.cellCrosshairRow).not.toBe(light.cellActive);
expect(light.cellCrosshairCol).not.toBe(light.cellActive);
expect(light.cellCrosshairCol).not.toBe(light.cellCrosshairRow);
expect(dark.cellCrosshairRow).toBe(DATA_GRID_DARK_CROSSHAIR_ROW_BG);
expect(dark.cellCrosshairCol).toBe(DATA_GRID_DARK_CROSSHAIR_COL_BG);
expect(dark.cellCrosshairRow).not.toBe(dark.cellActive);
expect(dark.cellCrosshairCol).not.toBe(dark.cellActive);
expect(dark.cellCrosshairCol).not.toBe(dark.cellCrosshairRow);
// These are spatial location cues rather than text, but they must remain
// visibly distinct from the grid surface in both modes.
expect(contrastRatio(light.background, light.cellCrosshairRow)).toBeGreaterThanOrEqual(1.5);
expect(contrastRatio(light.background, light.cellCrosshairCol)).toBeGreaterThanOrEqual(2);
expect(contrastRatio(dark.background, dark.cellCrosshairRow)).toBeGreaterThanOrEqual(1.5);
expect(contrastRatio(dark.background, dark.cellCrosshairCol)).toBeGreaterThanOrEqual(2);
});
it("lets explicit crosshair CSS variables drive the canvas fills in light mode", () => {
const vars: Record<string, string> = {
"--data-grid-cell-crosshair-row-bg": "rgb(190, 220, 250)",
"--data-grid-cell-crosshair-col-bg": "rgb(160, 205, 250)",
};
const theme = resolveDataGridPaintTheme({ getVar: (name) => vars[name] ?? "", isDark: false });
expect(theme.cellCrosshairRow).toBe("rgb(190, 220, 250)");
expect(theme.cellCrosshairCol).toBe("rgb(160, 205, 250)");
});
it("uses the theme primary color at the same stronger sRGB ratios as the DOM grid", () => {
const vars: Record<string, string> = {
"--primary": "rgb(58, 123, 106)",
"--background": "rgb(248, 250, 248)",
"--data-grid-cell-crosshair-row-bg": "color-mix(in srgb, var(--primary) 34%, var(--background))",
"--data-grid-cell-crosshair-col-bg": "color-mix(in srgb, var(--primary) 50%, var(--background))",
};
const theme = resolveDataGridPaintTheme({ getVar: (name) => vars[name] ?? "", isDark: false });
expect(theme.cellCrosshairRow).toBe("rgb(183, 207, 200)");
expect(theme.cellCrosshairCol).toBe("rgb(153, 187, 177)");
});
it("propagates crosshair fills through the base cell variable so DOM and frozen cells stay visible", () => {
const gridSource = readFileSync(new URL("../../../components/grid/DataGrid.vue", import.meta.url), "utf8");
expect(gridSource).toMatch(/\.crosshair-row\s*\{\s*--data-grid-cell-bg:\s*var\(--data-grid-cell-crosshair-row-bg\)\s*!important;/);
expect(gridSource).toMatch(/\.crosshair-column\s*\{\s*--data-grid-cell-bg:\s*var\(--data-grid-cell-crosshair-col-bg\)\s*!important;/);
expect(gridSource).toContain("color-mix(in srgb, var(--primary) 34%, var(--background))");
expect(gridSource).toContain("color-mix(in srgb, var(--primary) 50%, var(--background))");
expect(gridSource).toMatch(/\.data-grid-cell--frozen\s*\{\s*background-color:\s*var\(--data-grid-cell-bg,/);
});
it("falls back to the built-in type palette for the active appearance", () => {
const emptyCssVariable = () => "";
@@ -6,6 +6,7 @@ import { dataGridFrameCoversRow, dataGridFrameIsMultiCell, dataGridSelectionFram
import type { CellSelectionRange } from "@/lib/dataGrid/gridSelection";
import type { RowStatus } from "@/lib/dataGrid/gridRowStatus";
import { DATA_GRID_DARK_SEARCH_COLORS, dataGridTypeForeground, resolveDataGridPaintTheme, type DataGridPaintTheme } from "@/lib/dataGrid/dataGridPaintTheme";
import type { CrosshairTarget } from "@/lib/dataGrid/crosshairHighlight";
export const CANVAS_DATA_GRID_ROW_HEIGHT = 26;
export const MAX_CANVAS_DATA_GRID_PIXEL_RATIO = 4;
@@ -94,6 +95,8 @@ export interface DrawCanvasDataGridOptions {
columnAligns?: readonly ("left" | "right")[];
columnTypeVisualKinds?: readonly DataGridTypeVisualKind[];
colorizeDataTypes?: boolean;
/** 行列十字高亮目标(原样传入,null 表示开关关闭或无焦点)。只画当前 viewport 内的行/列底色 */
crosshair?: CrosshairTarget | null;
rightAlignedActionCell?: CanvasRightAlignedActionCell | null;
booleanDisplayMode?: "checkbox" | "dropdown";
flatteningMultiLineEnabled: boolean;
@@ -353,6 +356,7 @@ export function drawCanvasDataGrid(options: DrawCanvasDataGridOptions) {
columnAligns,
columnTypeVisualKinds,
colorizeDataTypes = false,
crosshair,
rightAlignedActionCell,
columnIsBoolean,
booleanDisplayMode = "dropdown",
@@ -429,6 +433,13 @@ export function drawCanvasDataGrid(options: DrawCanvasDataGridOptions) {
ctx.fillStyle = rowFill;
ctx.fillRect(0, y, width, CANVAS_DATA_GRID_ROW_HEIGHT);
// 十字行高亮:叠在基础行色之上,但低于整行选中(rowSelectionVisual),
// 也低于后续 drawCell 的脏格/搜索/选中格填充
if (crosshair?.rowCrosshair && item.displayIndex === crosshair.rowIndex && !rowSelectionVisual && !item.isDeleted) {
ctx.fillStyle = theme.cellCrosshairRow;
ctx.fillRect(rowNumberWidth, y, width - rowNumberWidth, CANVAS_DATA_GRID_ROW_HEIGHT);
}
// 选区覆盖指示(Navicat 风格):行落在选区范围内时行号淡色高亮;
// 优先级低于行选中/状态色/活动行,与 DOM 的级联顺序一致
const rowInSelection = !rowSelectionVisual && selectionRowCoverage && dataGridFrameCoversRow(selectionFrames, item.displayIndex);
@@ -503,6 +514,12 @@ export function drawCanvasDataGrid(options: DrawCanvasDataGridOptions) {
const cellPaintWidth = Math.min(width, drawX + colWidth) - clippedX;
if (cellPaintWidth <= 0) return;
// 十字列高亮:整列覆盖,叠在行底色之上;脏格/搜索/选中格填充在其后绘制,优先级更高
if (crosshair?.columnCrosshair && visibleColIdx === crosshair.visibleColIdx && !selectedFillVisual && !item.isDeleted) {
ctx.fillStyle = theme.cellCrosshairCol;
ctx.fillRect(clippedX, y, cellPaintWidth, CANVAS_DATA_GRID_ROW_HEIGHT);
}
if (isDirtyCell && !selectedFillVisual) {
ctx.fillStyle = theme.cellDirty;
ctx.fillRect(clippedX, y, cellPaintWidth, CANVAS_DATA_GRID_ROW_HEIGHT);
@@ -658,6 +675,11 @@ export function drawCanvasDataGrid(options: DrawCanvasDataGridOptions) {
ctx.fillStyle = rowFill;
ctx.fillRect(rowNumberWidth, y, frozenWidth, CANVAS_DATA_GRID_ROW_HEIGHT);
}
// 冻结区会重绘底色遮挡第一轮溢入内容,需在此重绘十字行底色保持一致
if (crosshair?.rowCrosshair && item.displayIndex === crosshair.rowIndex && !rowSelectionVisual && !item.isDeleted) {
ctx.fillStyle = theme.cellCrosshairRow;
ctx.fillRect(rowNumberWidth, y, frozenWidth, CANVAS_DATA_GRID_ROW_HEIGHT);
}
// 绘制冻结列的每个单元格(x 坐标不受 scrollLeft 影响)
for (let fcIdx = 0; fcIdx < frozenColumnCount; fcIdx++) {
const cellX = rowNumberWidth + (offsets[fcIdx] ?? 0);
@@ -0,0 +1,81 @@
import type { CellPosition } from "@/lib/dataGrid/gridSelection";
/**
* 行列十字高亮目标。只解析「原始选择状态」,不做转置投影(转置是纯展示层映射,
* 由 DataGrid 在渲染时把 x/y 轴互换)。供 Canvas / 常规 DOM / 转置 DOM 三个渲染分支共享。
*
* 坐标约定:
* - rowIndex 为原始行(displayIndex);
* - visibleColIdx 为可见列下标(selectionFocus.colIndex 语义,与 cellIsSelected 一致);
* - actualColIdx 为 visibleColumnIndexes[visibleColIdx] 对应的真实列下标。
*/
export interface CrosshairTarget {
rowIndex: number; // 原始行(displayIndex)
visibleColIdx: number; // selectionFocus.colIndex(可见列)
actualColIdx: number; // visibleColumnIndexes[visibleColIdx]
rowCrosshair: boolean; // 这一轴是否绘制(非"是否焦点格")
columnCrosshair: boolean;
}
export interface ResolveCrosshairTargetOptions {
selectionFocus: CellPosition | null;
selectionAnchor: CellPosition | null;
hasRowSelection: boolean;
hasColumnSelection: boolean;
visibleColumnIndexes: readonly number[];
/** 整行选中且无单元格焦点时派生行(DataGrid 从 lastClickedRowIndex / 首个 selectedRowId 推导) */
fallbackRowIndex?: number | null;
/** 整列选中且无单元格焦点时派生列(DataGrid 从首个 selectedColumnIndexes 推导) */
fallbackColumnIndex?: number | null;
}
export function resolveCrosshairTarget(options: ResolveCrosshairTargetOptions): CrosshairTarget | null {
const { selectionFocus, hasRowSelection, hasColumnSelection, visibleColumnIndexes } = options;
if (selectionFocus) {
const actualColIdx = visibleColumnIndexes[selectionFocus.colIndex];
if (actualColIdx === undefined) return null;
// 整行/整列选中会清空 focus(selectRow / selectColumns),因此普通格或多格范围
// 的 focus 必不与其他整行/整列选中并存;但防御性处理:若共存,只亮对应轴。
const rowCrosshair = !hasColumnSelection;
const columnCrosshair = !hasRowSelection;
return {
rowIndex: selectionFocus.rowIndex,
visibleColIdx: selectionFocus.colIndex,
actualColIdx,
rowCrosshair,
columnCrosshair,
};
}
// selectRow / handleRowClick 会调用 clearCellSelection() 清空 focus,整行选中时只能派生行。
if (hasRowSelection) {
if (options.fallbackRowIndex === null || options.fallbackRowIndex === undefined) return null;
const actualColIdx = visibleColumnIndexes[0];
if (actualColIdx === undefined) return null;
return {
rowIndex: options.fallbackRowIndex,
visibleColIdx: 0,
actualColIdx,
rowCrosshair: true,
columnCrosshair: false,
};
}
// selectColumns 会清空 anchor/focus,整列选中时只能派生列。
if (hasColumnSelection) {
if (options.fallbackColumnIndex === null || options.fallbackColumnIndex === undefined) return null;
const actualColIdx = visibleColumnIndexes[options.fallbackColumnIndex];
if (actualColIdx === undefined) return null;
return {
rowIndex: 0,
visibleColIdx: options.fallbackColumnIndex,
actualColIdx,
rowCrosshair: false,
columnCrosshair: true,
};
}
// 无焦点、加载中、空结果不绘制。
return null;
}
@@ -13,6 +13,8 @@ export interface DataGridPaintTheme {
rowNew: string;
rowDeleted: string;
cellActive: string;
cellCrosshairRow: string;
cellCrosshairCol: string;
cellDirty: string;
cellSelected: string;
cellSelectedDirty: string;
@@ -43,6 +45,17 @@ export const DATA_GRID_DARK_SEARCH_COLORS = {
} as const;
export const DATA_GRID_LIGHT_ACTIVE_ROW_BG = "rgb(244, 248, 255)";
export const DATA_GRID_DARK_ACTIVE_ROW_BG = "rgb(25, 34, 46)";
// 十字行/列底色:混入背景的实色(color-mix(in srgb, var(--primary) P%, var(--background))),
// 而非透明叠加——铺在浅底上的半透明色会被冲淡到几乎不可见,对比度不足正 jira #7276 的核心痛点。
// 行/列占 primary 34%/50%(CSS 方向:P% primary + 其余 background);现有 28%/40% 在浅色
// 主题的大面积网格中仍不够醒目。列更重,交点通过 Canvas 的两次绘制进一步加深;选中格仍以显式
// 填充与描边优先,故不会被十字高亮覆盖。
// 常量按默认主题 soft-light/dark(primary/background)混合得出,作为 var 未设置 / 旧浏览器
// 降级实色;color-mix 支持时网格组件的 CSS 变量会用它重新混成 —— 两处取值一致。
export const DATA_GRID_LIGHT_CROSSHAIR_ROW_BG = "rgb(174, 195, 224)";
export const DATA_GRID_LIGHT_CROSSHAIR_COL_BG = "rgb(142, 170, 210)";
export const DATA_GRID_DARK_CROSSHAIR_ROW_BG = "rgb(75, 84, 98)";
export const DATA_GRID_DARK_CROSSHAIR_COL_BG = "rgb(98, 111, 130)";
export const DATA_GRID_LIGHT_STRIPED_ROW_BG = "rgb(240, 240, 240)";
export const DATA_GRID_DARK_STRIPED_ROW_BG = "rgb(40, 40, 43)";
export const DATA_GRID_DARK_ROW_NUMBER_BG = "rgb(35, 37, 42)";
@@ -151,7 +164,8 @@ function mixRgb(first: RgbaColor, firstPercent: number, second: RgbaColor, secon
}
function parseColorMix(value: string): string | null {
const body = value.match(/^color-mix\(\s*in\s+oklab\s*,\s*(.*)\)$/i)?.[1];
// Crosshair tokens use sRGB so Canvas can reproduce the DOM blend exactly.
const body = value.match(/^color-mix\(\s*in\s+(?:srgb|oklab)\s*,\s*(.*)\)$/i)?.[1];
if (!body) return null;
const [firstRaw, secondRaw] = splitTopLevelCommas(body);
if (!firstRaw || !secondRaw) return null;
@@ -257,6 +271,10 @@ export function resolveDataGridPaintTheme(options: { getVar: (name: string) => s
const rowNew = isDark ? "rgb(51, 51, 55)" : "rgb(243, 243, 243)";
const rowDeleted = isDark ? "rgb(55, 31, 32)" : "rgb(255, 244, 244)";
const cellActive = activeSurface;
// 浅/深都读 CSS 变量(--data-grid-cell-crosshair-*-bg 从 --primary 派生),保证 DOM 与
// Canvas 在不同主题/深浅模式下取到同一色源;固定常量仅作 var 未设置时的降级默认。
const cellCrosshairRow = paintToken(getVar, "--data-grid-cell-crosshair-row-bg", isDark ? DATA_GRID_DARK_CROSSHAIR_ROW_BG : DATA_GRID_LIGHT_CROSSHAIR_ROW_BG);
const cellCrosshairCol = paintToken(getVar, "--data-grid-cell-crosshair-col-bg", isDark ? DATA_GRID_DARK_CROSSHAIR_COL_BG : DATA_GRID_LIGHT_CROSSHAIR_COL_BG);
const cellDirty = isDark ? "rgb(94, 75, 26)" : "rgb(255, 248, 230)";
const cellSelected = isDark ? "rgb(20, 40, 60)" : "rgb(239, 246, 255)";
const cellSelectedDirty = isDark ? "rgb(76, 66, 38)" : "rgb(235, 224, 184)";
@@ -288,6 +306,8 @@ export function resolveDataGridPaintTheme(options: { getVar: (name: string) => s
rowNew: isDark ? rowNew : paintToken(getVar, "--data-grid-row-new-bg", rowNew),
rowDeleted: isDark ? rowDeleted : paintToken(getVar, "--data-grid-row-deleted-bg", rowDeleted),
cellActive: isDark ? cellActive : paintToken(getVar, "--data-grid-cell-active-bg", cellActive),
cellCrosshairRow,
cellCrosshairCol,
cellDirty: paintToken(getVar, "--data-grid-cell-dirty-bg", cellDirty),
cellSelected: paintToken(getVar, "--data-grid-cell-selected-bg", cellSelected),
cellSelectedDirty: paintToken(getVar, "--data-grid-cell-selected-dirty-bg", cellSelectedDirty),
@@ -47,6 +47,7 @@ export const EDITOR_SETTINGS_DRAFT_KEYS = [
"dataGridTextFilterPanelHeight",
"dataGridAutoTransposeSingleRow",
"dataGridCellDetailButtonVisible",
"dataGridCrosshairHighlight",
"pageSize",
"tableOpenPageSize",
"queryResultMaxRowsEnabled",
+33
View File
@@ -305,6 +305,16 @@ describe("normalizeEditorSettings", () => {
}
});
it("defaults the crosshair highlight off and preserves only boolean values", () => {
expect(normalizeEditorSettings({}).dataGridCrosshairHighlight).toBe(false);
expect(normalizeEditorSettings({ dataGridCrosshairHighlight: true }).dataGridCrosshairHighlight).toBe(true);
expect(normalizeEditorSettings({ dataGridCrosshairHighlight: false }).dataGridCrosshairHighlight).toBe(false);
for (const invalidValue of [0, 1, "true", null]) {
expect(normalizeEditorSettings({ dataGridCrosshairHighlight: invalidValue as never }).dataGridCrosshairHighlight).toBe(false);
}
});
it("defaults the data grid font and preserves a custom font family", () => {
const defaultFontFamily = `"Geist Variable Tabular", "Geist Variable", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif`;
expect(normalizeEditorSettings({}).tableFontFamily).toBe(defaultFontFamily);
@@ -780,6 +790,29 @@ describe("settingsStore persisted settings initialization", () => {
expect(restartedStore.editorSettings.dataGridCellDetailButtonVisible).toBe(true);
});
it("defaults the crosshair highlight to off, persists an opt-in, and reloads it", async () => {
let persistedSettings: Record<string, unknown> = {};
const loadEditorSettings = vi.fn(async () => JSON.parse(JSON.stringify(persistedSettings)));
const saveEditorSettings = vi.fn(async (settings: Record<string, unknown>) => {
persistedSettings = JSON.parse(JSON.stringify(settings));
});
vi.doMock("@/lib/backend/api", () => ({ loadEditorSettings, saveEditorSettings }));
const { useSettingsStore } = await import("@/stores/settingsStore");
const store = useSettingsStore();
await store.initEditorSettings();
expect(store.editorSettings.dataGridCrosshairHighlight).toBe(false);
await store.updateEditorSettingsAndPersist({ dataGridCrosshairHighlight: true });
expect(saveEditorSettings).toHaveBeenLastCalledWith(expect.objectContaining({ dataGridCrosshairHighlight: true }));
setActivePinia(createPinia());
const restartedStore = useSettingsStore();
await restartedStore.initEditorSettings();
expect(restartedStore.editorSettings.dataGridCrosshairHighlight).toBe(true);
});
it("loads, persists, and reloads hidden query editor line numbers", async () => {
let persistedSettings: Record<string, unknown> = { showLineNumbers: true };
const loadEditorSettings = vi.fn(async () => JSON.parse(JSON.stringify(persistedSettings)));
+4
View File
@@ -582,6 +582,7 @@ export interface EditorSettings {
resultRunDisplayMode: ResultRunDisplayMode;
dataGridAutoTransposeSingleRow: boolean;
dataGridCellDetailButtonVisible: boolean;
dataGridCrosshairHighlight: boolean;
dataGridMultiRowTranspose: boolean;
dataGridHideNullColumns: boolean;
dataGridBooleanDisplayMode: "dropdown" | "checkbox";
@@ -795,6 +796,7 @@ export const DEFAULT_EDITOR_SETTINGS: EditorSettings = {
resultRunDisplayMode: "tabs",
dataGridAutoTransposeSingleRow: false,
dataGridCellDetailButtonVisible: true,
dataGridCrosshairHighlight: false,
dataGridMultiRowTranspose: false,
dataGridHideNullColumns: false,
dataGridBooleanDisplayMode: "dropdown",
@@ -1172,6 +1174,7 @@ export function normalizeEditorSettings(settings: Partial<EditorSettings>, exist
resultRunDisplayMode: normalizeResultRunDisplayMode(settings.resultRunDisplayMode),
dataGridAutoTransposeSingleRow: settings.dataGridAutoTransposeSingleRow === true,
dataGridCellDetailButtonVisible: typeof settings.dataGridCellDetailButtonVisible === "boolean" ? settings.dataGridCellDetailButtonVisible : DEFAULT_EDITOR_SETTINGS.dataGridCellDetailButtonVisible,
dataGridCrosshairHighlight: typeof settings.dataGridCrosshairHighlight === "boolean" ? settings.dataGridCrosshairHighlight : DEFAULT_EDITOR_SETTINGS.dataGridCrosshairHighlight,
dataGridMultiRowTranspose: settings.dataGridMultiRowTranspose === true,
dataGridHideNullColumns: settings.dataGridHideNullColumns === true,
dataGridBooleanDisplayMode: settings.dataGridBooleanDisplayMode === "checkbox" ? "checkbox" : "dropdown",
@@ -1788,6 +1791,7 @@ export const useSettingsStore = defineStore("settings", () => {
if (partial.resultRunDisplayMode !== undefined) editorSettings.value.resultRunDisplayMode = normalizeResultRunDisplayMode(partial.resultRunDisplayMode);
if (partial.dataGridAutoTransposeSingleRow !== undefined) editorSettings.value.dataGridAutoTransposeSingleRow = partial.dataGridAutoTransposeSingleRow === true;
if (partial.dataGridCellDetailButtonVisible !== undefined) editorSettings.value.dataGridCellDetailButtonVisible = typeof partial.dataGridCellDetailButtonVisible === "boolean" ? partial.dataGridCellDetailButtonVisible : DEFAULT_EDITOR_SETTINGS.dataGridCellDetailButtonVisible;
if (partial.dataGridCrosshairHighlight !== undefined) editorSettings.value.dataGridCrosshairHighlight = typeof partial.dataGridCrosshairHighlight === "boolean" ? partial.dataGridCrosshairHighlight : DEFAULT_EDITOR_SETTINGS.dataGridCrosshairHighlight;
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";