feat(data-grid): color cells by database-aware types
This commit is contained in:
25 files changed
+619
-46
No files matched your search
@@ -2,7 +2,7 @@
|
||||
import { computed, ref, nextTick, watch, onMounted, onBeforeUnmount } from "vue";
|
||||
import { uuid } from "@/lib/common/utils";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { RefreshCw, Trash2, Plus, Save, ChevronDown, ChevronLeft, ChevronRight, Table2, Braces, X, Search, Wrench, Filter, Columns3Cog, SquareDashed, Minus, Rows3, AlignLeft, AlignRight, EyeOff } from "@lucide/vue";
|
||||
import { RefreshCw, Trash2, Plus, Save, ChevronDown, ChevronLeft, ChevronRight, Table2, Braces, X, Search, Wrench, Filter, Columns3Cog, SquareDashed, Minus, Rows3, AlignLeft, AlignRight, EyeOff, Palette } from "@lucide/vue";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Input } from "@/components/ui/input";
|
||||
@@ -135,6 +135,7 @@ const columnWidthDensity = computed(() => settingsStore.editorSettings.columnWid
|
||||
const dataGridRenderMode = computed(() => settingsStore.editorSettings.dataGridRenderMode);
|
||||
const tableFontSize = computed(() => settingsStore.editorSettings.tableFontSize);
|
||||
const numericColumnRightAlign = computed(() => settingsStore.editorSettings.numericColumnRightAlign ?? true);
|
||||
const colorizeDataGridCellTypes = computed(() => settingsStore.editorSettings.colorizeDataGridCellTypes);
|
||||
const viewMode = computed<ViewMode>({
|
||||
get: () => settingsStore.editorSettings.mongoViewMode,
|
||||
set: (value) => settingsStore.updateEditorSettings({ mongoViewMode: value }),
|
||||
@@ -177,6 +178,10 @@ function increaseTableFontSize() {
|
||||
function setNumericColumnRightAlign(value: boolean) {
|
||||
settingsStore.updateEditorSettings({ numericColumnRightAlign: value });
|
||||
}
|
||||
|
||||
function setColorizeDataGridCellTypes(value: boolean) {
|
||||
settingsStore.updateEditorSettings({ colorizeDataGridCellTypes: value });
|
||||
}
|
||||
const tableSearchSplitContainerRef = ref<HTMLDivElement>();
|
||||
const tableFindPaneWidth = ref<number | null>(null);
|
||||
const isResizingTableSearchSplit = ref(false);
|
||||
@@ -1867,6 +1872,13 @@ defineExpose({ focusSearch });
|
||||
</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">
|
||||
<Palette class="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
|
||||
<span>{{ t("grid.colorizeDataTypes") }}</span>
|
||||
</div>
|
||||
<Switch size="sm" :model-value="colorizeDataGridCellTypes" :aria-label="t('grid.colorizeDataTypes')" @update:model-value="setColorizeDataGridCellTypes" />
|
||||
</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" />
|
||||
|
||||
@@ -309,6 +309,7 @@ const debugLogCopied = ref(false);
|
||||
const debugLogDownloaded = ref(false);
|
||||
const editShowColumnCommentsInHeader = ref(settingsStore.editorSettings.showColumnCommentsInHeader);
|
||||
const editShowColumnTypesInHeader = ref(settingsStore.editorSettings.showColumnTypesInHeader);
|
||||
const editColorizeDataGridCellTypes = ref(settingsStore.editorSettings.colorizeDataGridCellTypes);
|
||||
const editShowIndexIndicatorsInHeader = ref(settingsStore.editorSettings.showIndexIndicatorsInHeader);
|
||||
const editCompactColumnHeaderActions = ref(settingsStore.editorSettings.compactColumnHeaderActions);
|
||||
const editDataGridQuickEntry = ref(settingsStore.editorSettings.dataGridQuickEntry);
|
||||
@@ -482,6 +483,7 @@ function currentEditorSettingsDraft(): EditorSettingsDraft {
|
||||
tabLayout: editTabLayout.value,
|
||||
showColumnCommentsInHeader: editShowColumnCommentsInHeader.value,
|
||||
showColumnTypesInHeader: editShowColumnTypesInHeader.value,
|
||||
colorizeDataGridCellTypes: editColorizeDataGridCellTypes.value,
|
||||
showIndexIndicatorsInHeader: editShowIndexIndicatorsInHeader.value,
|
||||
compactColumnHeaderActions: editCompactColumnHeaderActions.value,
|
||||
dataGridQuickEntry: editDataGridQuickEntry.value,
|
||||
@@ -754,6 +756,7 @@ function syncEditorSettingsDraftFromStore() {
|
||||
editTabLayout.value = settingsStore.editorSettings.tabLayout;
|
||||
editShowColumnCommentsInHeader.value = settingsStore.editorSettings.showColumnCommentsInHeader;
|
||||
editShowColumnTypesInHeader.value = settingsStore.editorSettings.showColumnTypesInHeader;
|
||||
editColorizeDataGridCellTypes.value = settingsStore.editorSettings.colorizeDataGridCellTypes;
|
||||
editShowIndexIndicatorsInHeader.value = settingsStore.editorSettings.showIndexIndicatorsInHeader;
|
||||
editCompactColumnHeaderActions.value = settingsStore.editorSettings.compactColumnHeaderActions;
|
||||
editDataGridQuickEntry.value = settingsStore.editorSettings.dataGridQuickEntry;
|
||||
@@ -998,6 +1001,7 @@ function resetDefaultsForTab(tab: SettingsCategory) {
|
||||
} else if (tab === "data") {
|
||||
editShowColumnCommentsInHeader.value = DEFAULT_EDITOR_SETTINGS.showColumnCommentsInHeader;
|
||||
editShowColumnTypesInHeader.value = DEFAULT_EDITOR_SETTINGS.showColumnTypesInHeader;
|
||||
editColorizeDataGridCellTypes.value = DEFAULT_EDITOR_SETTINGS.colorizeDataGridCellTypes;
|
||||
editShowIndexIndicatorsInHeader.value = DEFAULT_EDITOR_SETTINGS.showIndexIndicatorsInHeader;
|
||||
editCompactColumnHeaderActions.value = DEFAULT_EDITOR_SETTINGS.compactColumnHeaderActions;
|
||||
editDataGridQuickEntry.value = DEFAULT_EDITOR_SETTINGS.dataGridQuickEntry;
|
||||
@@ -1065,6 +1069,7 @@ function resetAllDefaults() {
|
||||
editSidebarTablePageSize.value = DEFAULT_SIDEBAR_TABLE_PAGE_SIZE;
|
||||
editShowColumnCommentsInHeader.value = DEFAULT_EDITOR_SETTINGS.showColumnCommentsInHeader;
|
||||
editShowColumnTypesInHeader.value = DEFAULT_EDITOR_SETTINGS.showColumnTypesInHeader;
|
||||
editColorizeDataGridCellTypes.value = DEFAULT_EDITOR_SETTINGS.colorizeDataGridCellTypes;
|
||||
editShowIndexIndicatorsInHeader.value = DEFAULT_EDITOR_SETTINGS.showIndexIndicatorsInHeader;
|
||||
editCompactColumnHeaderActions.value = DEFAULT_EDITOR_SETTINGS.compactColumnHeaderActions;
|
||||
editDataGridQuickEntry.value = DEFAULT_EDITOR_SETTINGS.dataGridQuickEntry;
|
||||
@@ -4817,6 +4822,17 @@ onUnmounted(() => {
|
||||
</div>
|
||||
<Switch id="show-column-types-in-header" v-model="editShowColumnTypesInHeader" />
|
||||
</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="colorize-data-grid-cell-types">
|
||||
{{ t("settings.colorizeDataGridCellTypes") }}
|
||||
</Label>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
{{ t("settings.colorizeDataGridCellTypesDescription") }}
|
||||
</p>
|
||||
</div>
|
||||
<Switch id="colorize-data-grid-cell-types" v-model="editColorizeDataGridCellTypes" />
|
||||
</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="show-index-indicators-in-header">
|
||||
|
||||
@@ -83,7 +83,8 @@ import { buildTableSelectSql, quoteTableDataIdentifier } from "@/lib/table/table
|
||||
import { tableOpenPageLimit } from "@/lib/table/tableOpenPageLimit";
|
||||
import { uuid } from "@/lib/common/utils";
|
||||
import { generateCellValues, type CellValueGenerationKind } from "@/lib/dataGrid/cellValueGeneration";
|
||||
import { compactHeaderColumnType, isNumericColumnType, resolveHeaderColumnType, resolveResultColumnType } from "@/lib/dataGrid/dataGridColumnType";
|
||||
import { compactHeaderColumnType, isNumericColumnType, resolveDataGridTypeVisualKind, resolveHeaderColumnType, resolveResultColumnType } from "@/lib/dataGrid/dataGridColumnType";
|
||||
import { dataGridCellTextClass, dataGridTypeVisualClass } from "@/lib/dataGrid/dataGridCellTextVisual";
|
||||
import {
|
||||
canDeleteExistingTdengineRows,
|
||||
canEditExistingTableRows,
|
||||
@@ -292,6 +293,7 @@ const multiRowTranspose = computed(() => settingsStore.editorSettings.dataGridMu
|
||||
const hideNullColumns = computed(() => settingsStore.editorSettings.dataGridHideNullColumns);
|
||||
const booleanCellsUseCheckbox = computed(() => settingsStore.editorSettings.dataGridBooleanDisplayMode === "checkbox");
|
||||
const booleanDisplayMode = computed(() => settingsStore.editorSettings.dataGridBooleanDisplayMode);
|
||||
const colorizeDataGridCellTypes = computed(() => settingsStore.editorSettings.colorizeDataGridCellTypes);
|
||||
const { isDark, themePalette } = useTheme();
|
||||
const { toast } = useToast();
|
||||
// 外键单元格跳转复用导航入口;对话框引用传 stub(同 AiAssistant 模式)
|
||||
@@ -657,23 +659,7 @@ function selectedSortMenuValue(column: string, columnIndex: number): SortMenuVal
|
||||
}
|
||||
|
||||
function typeColorClass(t: string): string {
|
||||
// Strip precision/scale suffix like (20,6)
|
||||
const base = t.replace(/\(.*\)$/, "").toLowerCase();
|
||||
if (["int", "int2", "int4", "int8", "smallint", "bigint", "integer", "serial", "bigserial", "tinyint", "mediumint"].includes(base)) return "text-blue-500";
|
||||
// number: Oracle/Dameng NUMBER;binary_float/binary_double: Oracle IEEE 浮点
|
||||
if (["float4", "float8", "double", "decimal", "numeric", "real", "float", "money", "number", "binary_float", "binary_double", "dec"].includes(base)) return "text-cyan-500";
|
||||
// varchar2/nvarchar2/long: Oracle/Dameng 字符类型(LONG 在 Oracle 中是变长字符)
|
||||
if (["varchar", "varchar2", "nvarchar2", "text", "char", "character varying", "character", "string", "nvarchar", "nchar", "ntext", "longtext", "mediumtext", "tinytext", "clob", "long"].includes(base)) return "text-green-500";
|
||||
if (["bool", "boolean", "bit"].includes(base)) return "text-orange-500";
|
||||
if (["timestamp", "timestamptz", "datetime", "date", "time", "timetz", "datetime2", "smalldatetime"].includes(base)) return "text-purple-500";
|
||||
// xmltype: Oracle XMLType
|
||||
if (["json", "jsonb", "xml", "xmltype", "array"].includes(base)) return "text-pink-500";
|
||||
if (["uuid", "uniqueidentifier"].includes(base)) return "text-amber-500";
|
||||
// raw/long raw/bfile: Oracle 二进制
|
||||
if (["bytea", "blob", "binary", "varbinary", "image", "raw", "long raw", "bfile"].includes(base)) return "text-red-400";
|
||||
// sdo_geometry: Oracle Spatial
|
||||
if (["geometry", "geography", "sdo_geometry"].includes(base)) return "text-emerald-500";
|
||||
return "text-muted-foreground";
|
||||
return dataGridTypeVisualClass(resolveDataGridTypeVisualKind(t, resolvedDatabaseType.value));
|
||||
}
|
||||
const contextCell = ref<{
|
||||
rowId: number;
|
||||
@@ -1976,6 +1962,8 @@ const allColumnTypes = computed(() =>
|
||||
),
|
||||
);
|
||||
const visibleColumnTypes = computed(() => visibleColumnIndexes.value.map((index) => allColumnTypes.value[index]));
|
||||
const allColumnTypeVisualKinds = computed(() => allColumnTypes.value.map((type) => resolveDataGridTypeVisualKind(type, resolvedDatabaseType.value)));
|
||||
const visibleColumnTypeVisualKinds = computed(() => visibleColumnIndexes.value.map((index) => allColumnTypeVisualKinds.value[index] ?? "unknown"));
|
||||
const visibleColumnComments = computed(() => visibleColumnIndexes.value.map((index) => dataGridColumnCommentFor(columnCommentMap.value, props.result.columns[index] ?? "", props.sourceColumns?.[index])));
|
||||
const visibleColumnCount = computed(() => visibleColumnIndexes.value.length);
|
||||
|
||||
@@ -1985,6 +1973,45 @@ const columnAligns = computed<("left" | "right")[]>(() => {
|
||||
return visibleColumnTypes.value.map((type) => (isNumericColumnType(type) ? "right" : "left"));
|
||||
});
|
||||
|
||||
function gridCellTextColorClass(item: RowItem, actualColIdx: number, visibleColIdx: number): string {
|
||||
if (!colorizeDataGridCellTypes.value) return "text-foreground";
|
||||
const value = item.data[actualColIdx];
|
||||
const checkbox = booleanCellsUseCheckbox.value && isBooleanGridCell(item, actualColIdx) && value !== null;
|
||||
return dataGridCellTextClass({
|
||||
colorizeTypes: colorizeDataGridCellTypes.value,
|
||||
typeKind: allColumnTypeVisualKinds.value[actualColIdx] ?? "unknown",
|
||||
isNull: value === null,
|
||||
isDraft: item.isDraft && value === null,
|
||||
isEditing: editingCell.value?.rowId === item.id && editingCell.value.col === actualColIdx,
|
||||
isControl: checkbox,
|
||||
isSelected: rowCellsUseSelectionVisual(item.id) || cellIsSelected(item.displayIndex, visibleColIdx),
|
||||
isCurrentSearchMatch: cellIsCurrentMatch(item.displayIndex, actualColIdx),
|
||||
isSearchMatch: cellIsSearchMatch(item.displayIndex, actualColIdx),
|
||||
isDirty: item.isDirtyCol[actualColIdx],
|
||||
isDeleted: item.isDeleted,
|
||||
});
|
||||
}
|
||||
|
||||
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];
|
||||
return dataGridCellTextClass({
|
||||
colorizeTypes: colorizeDataGridCellTypes.value,
|
||||
typeKind: allColumnTypeVisualKinds.value[actualColIdx] ?? "unknown",
|
||||
isNull: value === null,
|
||||
isDraft: item.isDraft && value === null,
|
||||
isEditing: editingCell.value?.rowId === item.id && editingCell.value.col === actualColIdx,
|
||||
isControl: booleanCellsUseCheckbox.value && isBooleanGridCell(item, actualColIdx) && value !== null,
|
||||
isSelected: transposeRecordUsesSelectionVisual(recordIndex) || transposeCellIsSelected(recordIndex, actualColIdx),
|
||||
isCurrentSearchMatch: cellIsCurrentMatch(recordIndex, actualColIdx),
|
||||
isSearchMatch: cellIsSearchMatch(recordIndex, actualColIdx),
|
||||
isDirty: item.isDirtyCol[actualColIdx],
|
||||
isDeleted: item.isDeleted,
|
||||
});
|
||||
}
|
||||
|
||||
/** Preview actions from the result preview registry for the current result. */
|
||||
const previewActions = computed(() => {
|
||||
if (!props.result) return [];
|
||||
@@ -5349,10 +5376,6 @@ function queryColumnRef(name: string): string {
|
||||
return props.databaseType === "neo4j" ? `n.${quoted}` : quoted;
|
||||
}
|
||||
|
||||
function isNull(value: unknown): boolean {
|
||||
return value === null;
|
||||
}
|
||||
|
||||
function rowNumberStatusClass(item: RowItem): string {
|
||||
if (item.status === "draft") {
|
||||
return "border-muted-foreground/20 bg-muted/20 font-semibold text-muted-foreground";
|
||||
@@ -6077,6 +6100,8 @@ function drawCanvasGrid() {
|
||||
pageOffset: rowNumberPageOffset(),
|
||||
frozenColumnCount: frozenColumnCount.value,
|
||||
columnAligns: columnAligns.value,
|
||||
columnTypeVisualKinds: visibleColumnTypeVisualKinds.value,
|
||||
colorizeDataTypes: colorizeDataGridCellTypes.value,
|
||||
rightAlignedActionCell: canvasRightAlignedActionCell.value,
|
||||
columnIsBoolean: isBooleanGridColumn,
|
||||
booleanDisplayMode: booleanDisplayMode.value,
|
||||
@@ -6097,6 +6122,7 @@ watch(showDataGridTopbar, () => nextTick(observeDataGridTopbarWidth), {
|
||||
});
|
||||
watch(columnAligns, () => scheduleCanvasDraw());
|
||||
watch(booleanDisplayMode, () => scheduleCanvasDraw());
|
||||
watch(colorizeDataGridCellTypes, () => scheduleCanvasDraw());
|
||||
watch(
|
||||
[
|
||||
displayRowRefs,
|
||||
@@ -10163,22 +10189,24 @@ const gridContextMenuItems = computed<ContextMenuItem[]>(() => {
|
||||
v-for="cell in item.values"
|
||||
:key="`${item.id}:${cell.recordIndex}`"
|
||||
class="relative flex shrink-0 items-center border-r border-border/70 px-2 py-0 truncate"
|
||||
:class="{
|
||||
'text-muted-foreground italic': cell.isNull,
|
||||
'cell-selected': transposeCellIsSelected(cell.recordIndex, cell.valueIndex) && !displayItems[cell.recordIndex]?.isDirtyCol[cell.valueIndex],
|
||||
'cell-selected-dirty': transposeCellIsSelected(cell.recordIndex, cell.valueIndex) && displayItems[cell.recordIndex]?.isDirtyCol[cell.valueIndex],
|
||||
'cell-selected--sparse': transposeCellIsSelected(cell.recordIndex, cell.valueIndex) && !displayItems[cell.recordIndex]?.isDirtyCol[cell.valueIndex],
|
||||
'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],
|
||||
'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),
|
||||
'ring-2 ring-inset ring-yellow-500 bg-yellow-300/60 dark:bg-yellow-500/40': cellIsCurrentMatch(cell.recordIndex, cell.valueIndex),
|
||||
'cursor-text': !isScrolling && canEditCellItem(displayItems[cell.recordIndex], cell.valueIndex),
|
||||
'hover:bg-gray-200 dark:hover:bg-gray-800':
|
||||
!isScrolling && canEditCellItem(displayItems[cell.recordIndex], cell.valueIndex) && !transposeRecordUsesSelectionVisual(cell.recordIndex) && !transposeRecordUsesActiveHighlight(cell.recordIndex) && !transposeCellIsSelected(cell.recordIndex, cell.valueIndex),
|
||||
}"
|
||||
:class="[
|
||||
transposeCellTextColorClass(cell.recordIndex, cell.valueIndex),
|
||||
{
|
||||
'cell-selected': transposeCellIsSelected(cell.recordIndex, cell.valueIndex) && !displayItems[cell.recordIndex]?.isDirtyCol[cell.valueIndex],
|
||||
'cell-selected-dirty': transposeCellIsSelected(cell.recordIndex, cell.valueIndex) && displayItems[cell.recordIndex]?.isDirtyCol[cell.valueIndex],
|
||||
'cell-selected--sparse': transposeCellIsSelected(cell.recordIndex, cell.valueIndex) && !displayItems[cell.recordIndex]?.isDirtyCol[cell.valueIndex],
|
||||
'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],
|
||||
'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),
|
||||
'ring-2 ring-inset ring-yellow-500 bg-yellow-300/60 dark:bg-yellow-500/40': cellIsCurrentMatch(cell.recordIndex, cell.valueIndex),
|
||||
'cursor-text': !isScrolling && canEditCellItem(displayItems[cell.recordIndex], cell.valueIndex),
|
||||
'hover:bg-gray-200 hover:text-foreground dark:hover:bg-gray-800':
|
||||
!isScrolling && canEditCellItem(displayItems[cell.recordIndex], cell.valueIndex) && !transposeRecordUsesSelectionVisual(cell.recordIndex) && !transposeRecordUsesActiveHighlight(cell.recordIndex) && !transposeCellIsSelected(cell.recordIndex, cell.valueIndex),
|
||||
},
|
||||
]"
|
||||
:style="{
|
||||
width: `${getTransposeRecordWidth(cell.recordIndex)}px`,
|
||||
}"
|
||||
@@ -10915,12 +10943,12 @@ const gridContextMenuItems = computed<ContextMenuItem[]>(() => {
|
||||
class="data-grid-cell group/cell shrink-0 px-3 py-1 border-r border-border whitespace-nowrap overflow-hidden text-ellipsis relative select-none inline-block items-center tabular-nums"
|
||||
:style="renderedColumnStyle(col.visibleColIdx)"
|
||||
:class="[
|
||||
gridCellTextColorClass(item, col.actualColIdx, col.visibleColIdx),
|
||||
selectionFrameEdgeClass(item.displayIndex, col.visibleColIdx),
|
||||
{
|
||||
'data-grid-cell--frozen': col.visibleColIdx < frozenColumnCount,
|
||||
'data-grid-cell--frozen-separator': frozenColumnCount > 0 && col.visibleColIdx === frozenColumnCount - 1,
|
||||
'text-right': columnAligns[col.visibleColIdx] === 'right',
|
||||
'text-muted-foreground italic': isNull(item.data[col.actualColIdx]),
|
||||
'bg-yellow-500/10 cell-dirty': item.isDirtyCol[col.actualColIdx],
|
||||
'cell-selected': cellIsSelected(item.displayIndex, col.visibleColIdx) && !item.isDirtyCol[col.actualColIdx],
|
||||
'cell-selected-dirty': cellIsSelected(item.displayIndex, col.visibleColIdx) && item.isDirtyCol[col.actualColIdx],
|
||||
@@ -10934,7 +10962,7 @@ 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) && !(booleanCellsUseCheckbox && isBooleanGridCell(item, col.actualColIdx) && item.data[col.actualColIdx] !== null),
|
||||
'cursor-text hover:bg-gray-200 hover:text-foreground 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,
|
||||
|
||||
@@ -7,7 +7,36 @@ import { defaultViewForResult } from "@/lib/query/queryResultDefaultView";
|
||||
import { isQueryExecutionErrorResult } from "@/lib/query/queryResultError";
|
||||
import type { CSSProperties } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { Check, CheckSquare2, Columns3Cog, Copy, 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,
|
||||
Copy,
|
||||
EyeOff,
|
||||
Loader2,
|
||||
Search,
|
||||
TableProperties,
|
||||
ChevronDown,
|
||||
ChevronUp,
|
||||
Inbox,
|
||||
RefreshCcw,
|
||||
Wrench,
|
||||
Toolbox,
|
||||
Database,
|
||||
Download,
|
||||
Upload,
|
||||
X,
|
||||
Pin,
|
||||
Rows3,
|
||||
SquareDashed,
|
||||
Minus,
|
||||
Plus,
|
||||
ShieldAlert,
|
||||
AlignLeft,
|
||||
AlignRight,
|
||||
PanelsTopLeft,
|
||||
Palette,
|
||||
} from "@lucide/vue";
|
||||
import { Splitpanes, Pane } from "splitpanes";
|
||||
import { DynamicScroller, DynamicScrollerItem } from "vue-virtual-scroller";
|
||||
import "splitpanes/dist/splitpanes.css";
|
||||
@@ -194,6 +223,8 @@ const connectionStore = useConnectionStore();
|
||||
const settingsStore = useSettingsStore();
|
||||
const booleanDisplayMode = computed(() => settingsStore.editorSettings.dataGridBooleanDisplayMode);
|
||||
const setBooleanDisplayMode = (mode: "checkbox" | "dropdown") => settingsStore.updateEditorSettings({ dataGridBooleanDisplayMode: mode });
|
||||
const colorizeDataGridCellTypes = computed(() => settingsStore.editorSettings.colorizeDataGridCellTypes);
|
||||
const setColorizeDataGridCellTypes = (value: boolean) => settingsStore.updateEditorSettings({ colorizeDataGridCellTypes: value });
|
||||
const { toast } = useToast();
|
||||
const DEFAULT_QUERY_RESULTS_PANE_SIZE = 68;
|
||||
|
||||
@@ -1365,6 +1396,13 @@ 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">
|
||||
<Palette class="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
|
||||
<span>{{ t("grid.colorizeDataTypes") }}</span>
|
||||
</div>
|
||||
<Switch size="sm" :model-value="colorizeDataGridCellTypes" :aria-label="t('grid.colorizeDataTypes')" @update:model-value="setColorizeDataGridCellTypes" />
|
||||
</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" />
|
||||
@@ -1858,6 +1896,13 @@ 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">
|
||||
<Palette class="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
|
||||
<span>{{ t("grid.colorizeDataTypes") }}</span>
|
||||
</div>
|
||||
<Switch size="sm" :model-value="colorizeDataGridCellTypes" :aria-label="t('grid.colorizeDataTypes')" @update:model-value="setColorizeDataGridCellTypes" />
|
||||
</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" />
|
||||
|
||||
@@ -1675,6 +1675,7 @@ export default {
|
||||
searchModeHint: "Choose whether Ctrl+F filters matching rows or keeps all rows visible and highlights matches.",
|
||||
searchModeFilter: "Filter",
|
||||
searchModeHighlight: "Highlight",
|
||||
colorizeDataTypes: "Data type colors",
|
||||
numericColumnAlign: "Numeric Column Align",
|
||||
numericColumnAlignLeft: "Left",
|
||||
numericColumnAlignRight: "Right",
|
||||
@@ -5604,6 +5605,8 @@ export default {
|
||||
showColumnCommentsInHeaderDescription: "Display table column comments directly below grid column names.",
|
||||
showColumnTypesInHeader: "Show column types under names",
|
||||
showColumnTypesInHeaderDescription: "Display each column's data type directly below grid column names.",
|
||||
colorizeDataGridCellTypes: "Color-code data grid values",
|
||||
colorizeDataGridCellTypesDescription: "Use subtle, theme-aware text colors to distinguish values by their column data type.",
|
||||
showIndexIndicatorsInHeader: "Show index indicators beside column names",
|
||||
showIndexIndicatorsInHeaderDescription: "Use icons to indicate each column's index type in the grid header: primary key (orange), unique index (red), regular index (green).",
|
||||
compactColumnHeaderActions: "Compact column header tools",
|
||||
|
||||
@@ -1839,6 +1839,7 @@ export default withEnglishFallback({
|
||||
},
|
||||
cachedResultUnavailable: "Resultado en caché faltante o incompatible.",
|
||||
reexecuteQuery: "Volver a ejecutar consulta",
|
||||
colorizeDataTypes: "Colores de tipos de datos",
|
||||
numericColumnAlign: "Alineación de columna numérica",
|
||||
numericColumnAlignLeft: "Alineación izquierda",
|
||||
numericColumnAlignRight: "Alineación derecha",
|
||||
@@ -5359,6 +5360,8 @@ export default withEnglishFallback({
|
||||
showColumnCommentsInHeaderDescription: "Muestra los comentarios de columnas directamente debajo del nombre de la columna.",
|
||||
showColumnTypesInHeader: "Mostrar tipos de columna bajo los nombres",
|
||||
showColumnTypesInHeaderDescription: "Muestra el tipo de dato de cada columna directamente debajo del nombre de la columna.",
|
||||
colorizeDataGridCellTypes: "Colorear valores por tipo de dato",
|
||||
colorizeDataGridCellTypesDescription: "Usa colores de texto sutiles adaptados al tema para distinguir los valores según el tipo de dato de su columna.",
|
||||
showIndexIndicatorsInHeader: "Mostrar indicadores de índice junto a los nombres",
|
||||
showIndexIndicatorsInHeaderDescription: "Usa iconos para indicar el tipo de índice de cada columna en el encabezado: clave primaria (naranja), índice único (rojo), índice regular (verde).",
|
||||
compactColumnHeaderActions: "Compactar herramientas del encabezado",
|
||||
|
||||
@@ -1837,6 +1837,7 @@ export default withEnglishFallback({
|
||||
},
|
||||
cachedResultUnavailable: "Risultato in cache mancante o non compatibile.",
|
||||
reexecuteQuery: "Esegui di nuovo la query",
|
||||
colorizeDataTypes: "Colori dei tipi di dato",
|
||||
numericColumnAlign: "Allineamento colonna numerica",
|
||||
numericColumnAlignLeft: "Allineamento a sinistra",
|
||||
numericColumnAlignRight: "Allineamento a destra",
|
||||
@@ -5359,6 +5360,8 @@ export default withEnglishFallback({
|
||||
showColumnCommentsInHeaderDescription: "Visualizza i commenti delle colonne della tabella direttamente sotto i nomi delle colonne nella griglia.",
|
||||
showColumnTypesInHeader: "Mostra i tipi di colonna sotto i nomi",
|
||||
showColumnTypesInHeaderDescription: "Visualizza il tipo di dato di ciascuna colonna direttamente sotto i nomi delle colonne nella griglia.",
|
||||
colorizeDataGridCellTypes: "Colora i valori in base al tipo di dato",
|
||||
colorizeDataGridCellTypesDescription: "Usa colori del testo discreti e adatti al tema per distinguere i valori in base al tipo di dato della colonna.",
|
||||
showIndexIndicatorsInHeader: "Mostra gli indicatori di indice accanto ai nomi",
|
||||
showIndexIndicatorsInHeaderDescription: "Utilizza icone per indicare il tipo di indice di ciascuna colonna nell'intestazione: chiave primaria (arancione), indice univoco (rosso), indice normale (verde).",
|
||||
compactColumnHeaderActions: "Strumenti intestazione colonna compatti",
|
||||
|
||||
@@ -1859,6 +1859,7 @@ export default withEnglishFallback({
|
||||
},
|
||||
cachedResultUnavailable: "キャッシュ結果が不足しているか互換性がありません。",
|
||||
reexecuteQuery: "クエリを再実行",
|
||||
colorizeDataTypes: "データ型の色分け",
|
||||
numericColumnAlign: "数値列の配置",
|
||||
numericColumnAlignLeft: "左揃え",
|
||||
numericColumnAlignRight: "右揃え",
|
||||
@@ -5394,6 +5395,8 @@ export default withEnglishFallback({
|
||||
showColumnCommentsInHeaderDescription: "グリッド列名の直下にテーブル列コメントを表示します。",
|
||||
showColumnTypesInHeader: "列名の下にデータ型を表示",
|
||||
showColumnTypesInHeaderDescription: "グリッド列名の直下に各列のデータ型を表示します。",
|
||||
colorizeDataGridCellTypes: "データ型に応じて値を色分け",
|
||||
colorizeDataGridCellTypesDescription: "テーマに合わせた控えめな文字色で、列のデータ型ごとに値を区別します。",
|
||||
showIndexIndicatorsInHeader: "列名の横にインデックス指標を表示",
|
||||
showIndexIndicatorsInHeaderDescription: "グリッド列ヘッダーにインデックスの種類をアイコンで表示します:主キー(オレンジ)、一意インデックス(赤)、通常インデックス(緑)。",
|
||||
compactColumnHeaderActions: "列ヘッダーツールをコンパクトに",
|
||||
|
||||
@@ -1559,6 +1559,7 @@ export default withEnglishFallback({
|
||||
searchModeHint: "Ctrl+F가 일치하는 행을 필터링할지, 모든 행을 표시하고 일치 항목을 강조할지 선택하세요.",
|
||||
searchModeFilter: "필터",
|
||||
searchModeHighlight: "강조",
|
||||
colorizeDataTypes: "데이터 타입 색상",
|
||||
numericColumnAlign: "숫자 컬럼 정렬",
|
||||
numericColumnAlignLeft: "왼쪽",
|
||||
numericColumnAlignRight: "오른쪽",
|
||||
@@ -5102,6 +5103,8 @@ export default withEnglishFallback({
|
||||
showColumnCommentsInHeaderDescription: "그리드 컬럼 이름 바로 아래에 테이블 컬럼 주석을 표시합니다.",
|
||||
showColumnTypesInHeader: "이름 아래에 컬럼 타입 표시",
|
||||
showColumnTypesInHeaderDescription: "그리드 컬럼 이름 바로 아래에 각 컬럼의 데이터 타입을 표시합니다.",
|
||||
colorizeDataGridCellTypes: "데이터 타입별 값 색상 표시",
|
||||
colorizeDataGridCellTypesDescription: "테마에 맞는 은은한 텍스트 색상으로 컬럼 데이터 타입에 따라 값을 구분합니다.",
|
||||
showIndexIndicatorsInHeader: "컬럼 이름 옆에 인덱스 표시",
|
||||
showIndexIndicatorsInHeaderDescription: "그리드 컬럼 헤더에 인덱스 유형을 아이콘으로 표시합니다: 기본키(주황색), 고유 인덱스(빨간색), 일반 인덱스(녹색).",
|
||||
compactColumnHeaderActions: "컴팩트한 컬럼 머리글 도구",
|
||||
|
||||
@@ -1839,6 +1839,7 @@ export default withEnglishFallback({
|
||||
},
|
||||
cachedResultUnavailable: "Resultado em cache ausente ou incompatível.",
|
||||
reexecuteQuery: "Reexecutar consulta",
|
||||
colorizeDataTypes: "Cores por tipo de dado",
|
||||
numericColumnAlign: "Alinhamento de colunas numéricas",
|
||||
numericColumnAlignLeft: "Alinhamento à esquerda",
|
||||
numericColumnAlignRight: "Alinhamento à direita",
|
||||
@@ -5361,6 +5362,8 @@ export default withEnglishFallback({
|
||||
showColumnCommentsInHeaderDescription: "Exibir comentários de colunas da tabela diretamente abaixo dos nomes das colunas da grade.",
|
||||
showColumnTypesInHeader: "Mostrar tipos de coluna sob os nomes",
|
||||
showColumnTypesInHeaderDescription: "Exibir o tipo de dado de cada coluna diretamente abaixo dos nomes das colunas da grade.",
|
||||
colorizeDataGridCellTypes: "Colorir valores por tipo de dado",
|
||||
colorizeDataGridCellTypesDescription: "Usa cores de texto sutis e adaptadas ao tema para diferenciar valores pelo tipo de dado da coluna.",
|
||||
showIndexIndicatorsInHeader: "Mostrar indicadores de índice ao lado dos nomes",
|
||||
showIndexIndicatorsInHeaderDescription: "Use ícones para indicar o tipo de índice de cada coluna no cabeçalho: chave primária (laranja), índice exclusivo (vermelho), índice regular (verde).",
|
||||
compactColumnHeaderActions: "Ferramentas compactas no cabeçalho da coluna",
|
||||
|
||||
@@ -1601,6 +1601,7 @@ export default withEnglishFallback({
|
||||
searchModeHint: "选择 Ctrl+F 搜索时过滤匹配行,或保留全部行并高亮定位命中项。",
|
||||
searchModeFilter: "过滤",
|
||||
searchModeHighlight: "高亮",
|
||||
colorizeDataTypes: "数据类型着色",
|
||||
numericColumnAlign: "数值列对齐",
|
||||
numericColumnAlignLeft: "左对齐",
|
||||
numericColumnAlignRight: "右对齐",
|
||||
@@ -5526,6 +5527,8 @@ export default withEnglishFallback({
|
||||
showColumnCommentsInHeaderDescription: "把表字段注释直接显示在结果表头字段名下方。",
|
||||
showColumnTypesInHeader: "在字段名下方显示数据类型",
|
||||
showColumnTypesInHeaderDescription: "把每个字段的数据类型直接显示在结果表头字段名下方。",
|
||||
colorizeDataGridCellTypes: "按数据类型为表格值着色",
|
||||
colorizeDataGridCellTypesDescription: "使用适配主题的克制文字颜色,按字段数据类型区分表格中的值。",
|
||||
showIndexIndicatorsInHeader: "在字段名旁显示索引标识",
|
||||
showIndexIndicatorsInHeaderDescription: "在结果表头字段名旁用图标标识字段的索引类型:主键(橙色)、唯一索引(红色)、普通索引(绿色)。",
|
||||
compactColumnHeaderActions: "收起字段表头工具",
|
||||
|
||||
@@ -1843,6 +1843,7 @@ export default withEnglishFallback({
|
||||
},
|
||||
cachedResultUnavailable: "快取結果缺失或不相容。",
|
||||
reexecuteQuery: "重新執行查詢",
|
||||
colorizeDataTypes: "資料類型著色",
|
||||
numericColumnAlign: "數值列對齊",
|
||||
numericColumnAlignLeft: "左對齊",
|
||||
numericColumnAlignRight: "右對齊",
|
||||
@@ -4699,6 +4700,8 @@ export default withEnglishFallback({
|
||||
showColumnCommentsInHeaderDescription: "直接在資料表格欄位名稱下方顯示資料表欄位註解。",
|
||||
showColumnTypesInHeader: "在欄位名稱下方顯示資料類型",
|
||||
showColumnTypesInHeaderDescription: "直接在資料表格欄位名稱下方顯示每個欄位的資料類型。",
|
||||
colorizeDataGridCellTypes: "依資料類型為表格值著色",
|
||||
colorizeDataGridCellTypesDescription: "使用配合主題的柔和文字顏色,依欄位資料類型區分表格中的值。",
|
||||
showIndexIndicatorsInHeader: "在欄位名稱旁顯示索引標識",
|
||||
showIndexIndicatorsInHeaderDescription: "在資料表格欄位名稱旁用圖示標識欄位的索引類型:主鍵(橙色)、唯一索引(紅色)、普通索引(綠色)。",
|
||||
compactColumnHeaderActions: "收起欄位表頭工具",
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { resolveDataGridCellTextRole } from "@/lib/dataGrid/dataGridCellTextVisual";
|
||||
import { resolveDataGridTypeVisualKind, resolveHeaderColumnType, type DataGridTypeVisualKind } from "@/lib/dataGrid/dataGridColumnType";
|
||||
|
||||
const dataGridSource = readFileSync(new URL("../../../components/grid/DataGrid.vue", import.meta.url), "utf8");
|
||||
const globalStylesSource = readFileSync(new URL("../../../styles/globals.css", import.meta.url), "utf8");
|
||||
|
||||
describe("data grid type visual kind", () => {
|
||||
it.each<[string, DataGridTypeVisualKind]>([
|
||||
["BIGINT", "integer"],
|
||||
["Nullable(UInt64)", "integer"],
|
||||
["DECIMAL(18, 4)", "numeric"],
|
||||
["DOUBLE PRECISION", "numeric"],
|
||||
["character varying(255)", "string"],
|
||||
["BOOLEAN", "boolean"],
|
||||
["timestamp with time zone", "temporal"],
|
||||
["JSONB", "structured"],
|
||||
["Array(Nullable(UInt64))", "structured"],
|
||||
["text[]", "structured"],
|
||||
["_int4", "structured"],
|
||||
["_text", "structured"],
|
||||
["_jsonb", "structured"],
|
||||
["_uuid", "structured"],
|
||||
["UUID", "identifier"],
|
||||
["BYTEA", "binary"],
|
||||
["SDO_GEOMETRY", "spatial"],
|
||||
["inet", "unknown"],
|
||||
])("maps %s to %s", (dataType, expected) => {
|
||||
expect(resolveDataGridTypeVisualKind(dataType)).toBe(expected);
|
||||
});
|
||||
|
||||
it("keeps an absent type neutral", () => {
|
||||
expect(resolveDataGridTypeVisualKind(undefined)).toBe("unknown");
|
||||
expect(resolveDataGridTypeVisualKind(" ")).toBe("unknown");
|
||||
});
|
||||
|
||||
it.each([
|
||||
["timestamp", "sqlserver", "binary"],
|
||||
["rowversion", "sqlserver", "binary"],
|
||||
["bit(8)", "postgres", "binary"],
|
||||
["bit varying(8)", "postgres", "binary"],
|
||||
] as const)("maps %s for %s to %s", (dataType, databaseType, expected) => {
|
||||
expect(resolveDataGridTypeVisualKind(dataType, databaseType)).toBe(expected);
|
||||
});
|
||||
|
||||
it("keeps ambiguous types on their generic defaults without a matching dialect", () => {
|
||||
expect(resolveDataGridTypeVisualKind("timestamp")).toBe("temporal");
|
||||
expect(resolveDataGridTypeVisualKind("timestamp", "postgres")).toBe("temporal");
|
||||
expect(resolveDataGridTypeVisualKind("bit")).toBe("boolean");
|
||||
expect(resolveDataGridTypeVisualKind("bit", "sqlserver")).toBe("boolean");
|
||||
});
|
||||
});
|
||||
|
||||
describe("data grid header type color", () => {
|
||||
it("uses the displayed metadata type for the standard header color", () => {
|
||||
const displayedType = resolveHeaderColumnType({
|
||||
tableColumnType: "decimal(10,2)",
|
||||
resultColumnTypes: ["text"],
|
||||
actualColIdx: 0,
|
||||
});
|
||||
|
||||
expect(displayedType).toBe("decimal(10,2)");
|
||||
expect(resolveDataGridTypeVisualKind(displayedType)).toBe("numeric");
|
||||
expect(dataGridSource).toContain(':type-class="typeColorClass(headerColumnType(col.name, col.actualColIdx))"');
|
||||
expect(dataGridSource).not.toContain("typeColorClass(allColumnTypes[col.actualColIdx]");
|
||||
});
|
||||
});
|
||||
|
||||
describe("data grid cell text visual priority", () => {
|
||||
const ordinaryInteger = {
|
||||
colorizeTypes: true,
|
||||
typeKind: "integer" as const,
|
||||
};
|
||||
|
||||
it("uses a type color only for an ordinary typed value", () => {
|
||||
expect(resolveDataGridCellTextRole(ordinaryInteger)).toBe("type");
|
||||
});
|
||||
|
||||
it.each([{ colorizeTypes: false }, { typeKind: "unknown" as const }, { isEditing: true }, { isControl: true }, { isSelected: true }, { isCurrentSearchMatch: true }, { isSearchMatch: true }, { isDirty: true }, { isDeleted: true }])("keeps interaction state visually dominant for %o", (override) => {
|
||||
expect(resolveDataGridCellTextRole({ ...ordinaryInteger, ...override })).toBe("neutral");
|
||||
});
|
||||
|
||||
it.each([{ isNull: true }, { isDraft: true }, { isNull: true, isSelected: true }])("keeps null and draft placeholders muted for %o", (override) => {
|
||||
expect(resolveDataGridCellTextRole({ ...ordinaryInteger, ...override })).toBe("muted");
|
||||
});
|
||||
|
||||
it("uses a neutral foreground on editable DOM hover surfaces", () => {
|
||||
expect(dataGridSource).toContain("'hover:bg-gray-200 hover:text-foreground dark:hover:bg-gray-800':");
|
||||
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("places data-grid type selectors in the components layer", () => {
|
||||
const componentsLayerStart = globalStylesSource.indexOf("@layer components {");
|
||||
const integerTypeSelector = globalStylesSource.indexOf(".data-grid-type-integer");
|
||||
|
||||
expect(componentsLayerStart).toBeGreaterThan(-1);
|
||||
expect(integerTypeSelector).toBeGreaterThan(componentsLayerStart);
|
||||
});
|
||||
});
|
||||
@@ -3,6 +3,8 @@ import { readFileSync } from "node:fs";
|
||||
import { DATA_GRID_DARK_ACTIVE_ROW_BG, DATA_GRID_LIGHT_ACTIVE_ROW_BG, dataGridActiveRowBackground, resolveDataGridPaintTheme } from "@/lib/dataGrid/dataGridPaintTheme";
|
||||
|
||||
function parseRgb(value: string): { r: number; g: number; b: number } | null {
|
||||
const hex = value.match(/^#([\da-f]{2})([\da-f]{2})([\da-f]{2})$/i);
|
||||
if (hex) return { r: Number.parseInt(hex[1], 16), g: Number.parseInt(hex[2], 16), b: Number.parseInt(hex[3], 16) };
|
||||
const match = value.match(/rgba?\(\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)/i);
|
||||
if (!match) return null;
|
||||
return { r: Number(match[1]), g: Number(match[2]), b: Number(match[3]) };
|
||||
@@ -112,6 +114,39 @@ describe("data grid paint theme", () => {
|
||||
|
||||
expect(theme.cellDirty).toBe("rgb(94, 56, 57)");
|
||||
});
|
||||
|
||||
it("resolves accessible semantic type colors for light and dark grids", () => {
|
||||
const emptyCssVariable = () => "";
|
||||
const light = resolveDataGridPaintTheme({ getVar: emptyCssVariable, isDark: false });
|
||||
const dark = resolveDataGridPaintTheme({ getVar: emptyCssVariable, isDark: true });
|
||||
|
||||
expect(light.typeForegrounds.integer).toBe("#1d4ed8");
|
||||
expect(light.typeForegrounds.boolean).toBe("#c2410c");
|
||||
expect(dark.typeForegrounds.integer).toBe("#93c5fd");
|
||||
expect(dark.typeForegrounds.boolean).toBe("#fdba74");
|
||||
expect(light.typeForegrounds.unknown).toBe(light.foreground);
|
||||
expect(dark.typeForegrounds.unknown).toBe(dark.foreground);
|
||||
|
||||
for (const [kind, color] of Object.entries(light.typeForegrounds)) {
|
||||
if (kind === "unknown") continue;
|
||||
expect(contrastRatio(color, "#ffffff")).toBeGreaterThanOrEqual(4.5);
|
||||
expect(contrastRatio(color, "#f0f0f0")).toBeGreaterThanOrEqual(4.5);
|
||||
}
|
||||
for (const [kind, color] of Object.entries(dark.typeForegrounds)) {
|
||||
if (kind === "unknown") continue;
|
||||
expect(contrastRatio(color, "#131416")).toBeGreaterThanOrEqual(4.5);
|
||||
expect(contrastRatio(color, "#28282b")).toBeGreaterThanOrEqual(4.5);
|
||||
}
|
||||
});
|
||||
|
||||
it("honors a custom semantic type color token", () => {
|
||||
const theme = resolveDataGridPaintTheme({
|
||||
getVar: (name) => (name === "--data-grid-type-spatial-fg" ? "rgb(12, 98, 74)" : ""),
|
||||
isDark: false,
|
||||
});
|
||||
|
||||
expect(theme.typeForegrounds.spatial).toBe("rgb(12, 98, 74)");
|
||||
});
|
||||
});
|
||||
|
||||
describe("dbx-control-chrome cascade contract", () => {
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { firstLineCellDisplayValue, type CellValue } from "@/lib/dataGrid/cellValue";
|
||||
import { BOOLEAN_CHECKBOX_SIZE, isBooleanCellValue, normalizeBooleanCellValue } from "@/lib/dataGrid/dataGridBooleanColumn";
|
||||
import { resolveDataGridCellTextRole } from "@/lib/dataGrid/dataGridCellTextVisual";
|
||||
import type { DataGridTypeVisualKind } from "@/lib/dataGrid/dataGridColumnType";
|
||||
import { dataGridFrameCoversRow, dataGridFrameIsMultiCell, dataGridSelectionFrameKindAtCell, dataGridSelectionUsesOuterFrame } from "@/lib/dataGrid/dataGridSelectionFrames";
|
||||
import type { CellSelectionRange } from "@/lib/dataGrid/gridSelection";
|
||||
import type { RowStatus } from "@/lib/dataGrid/gridRowStatus";
|
||||
import { DATA_GRID_DARK_SEARCH_COLORS, resolveDataGridPaintTheme, type DataGridPaintTheme } from "@/lib/dataGrid/dataGridPaintTheme";
|
||||
import { DATA_GRID_DARK_SEARCH_COLORS, dataGridTypeForeground, resolveDataGridPaintTheme, type DataGridPaintTheme } from "@/lib/dataGrid/dataGridPaintTheme";
|
||||
|
||||
export const CANVAS_DATA_GRID_ROW_HEIGHT = 26;
|
||||
export const MAX_CANVAS_DATA_GRID_PIXEL_RATIO = 4;
|
||||
@@ -89,6 +91,8 @@ export interface DrawCanvasDataGridOptions {
|
||||
pageOffset: number;
|
||||
frozenColumnCount?: number;
|
||||
columnAligns?: readonly ("left" | "right")[];
|
||||
columnTypeVisualKinds?: readonly DataGridTypeVisualKind[];
|
||||
colorizeDataTypes?: boolean;
|
||||
rightAlignedActionCell?: CanvasRightAlignedActionCell | null;
|
||||
booleanDisplayMode?: "checkbox" | "dropdown";
|
||||
}
|
||||
@@ -344,6 +348,8 @@ export function drawCanvasDataGrid(options: DrawCanvasDataGridOptions) {
|
||||
pageOffset,
|
||||
frozenColumnCount = 0,
|
||||
columnAligns,
|
||||
columnTypeVisualKinds,
|
||||
colorizeDataTypes = false,
|
||||
rightAlignedActionCell,
|
||||
columnIsBoolean,
|
||||
booleanDisplayMode = "dropdown",
|
||||
@@ -534,8 +540,23 @@ export function drawCanvasDataGrid(options: DrawCanvasDataGridOptions) {
|
||||
const isEditingThisCell = editingCell?.rowId === item.id && editingCell.col === actualColIdx;
|
||||
const isBooleanNullCell = booleanDisplayMode === "checkbox" && isBooleanCell && value === null && !isEditingThisCell;
|
||||
const shouldRenderBooleanCheckbox = booleanDisplayMode === "checkbox" && isBooleanCell && value !== null && !isEditingThisCell;
|
||||
const typeKind = columnTypeVisualKinds?.[visibleColIdx] ?? "unknown";
|
||||
const textRole = resolveDataGridCellTextRole({
|
||||
colorizeTypes: colorizeDataTypes,
|
||||
typeKind,
|
||||
isNull: value === null,
|
||||
isDraft: item.isDraft && value === null,
|
||||
isEditing: isEditingThisCell,
|
||||
isControl: shouldRenderBooleanCheckbox,
|
||||
isSelected: selectedFillVisual,
|
||||
isCurrentSearchMatch,
|
||||
isSearchMatch,
|
||||
isDirty: isDirtyCell,
|
||||
isDeleted: item.isDeleted,
|
||||
});
|
||||
const cellTextColor = textRole === "muted" ? theme.mutedForeground : textRole === "type" ? dataGridTypeForeground(theme, typeKind) : theme.foreground;
|
||||
ctx.textAlign = isBooleanNullCell ? "center" : isRightAlign ? "right" : "left";
|
||||
ctx.fillStyle = value === null ? theme.mutedForeground : theme.foreground;
|
||||
ctx.fillStyle = cellTextColor;
|
||||
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;
|
||||
@@ -545,7 +566,7 @@ export function drawCanvasDataGrid(options: DrawCanvasDataGridOptions) {
|
||||
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.strokeStyle = cellTextColor;
|
||||
ctx.lineWidth = 1;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(boxX - 1, strikeY);
|
||||
@@ -561,7 +582,7 @@ export function drawCanvasDataGrid(options: DrawCanvasDataGridOptions) {
|
||||
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.strokeStyle = cellTextColor;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(lineStartX, textY);
|
||||
ctx.lineTo(alignCanvasPixel(lineStartX + textWidth, scaleX), textY);
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import type { DataGridTypeVisualKind } from "@/lib/dataGrid/dataGridColumnType";
|
||||
|
||||
export type DataGridCellTextRole = "muted" | "neutral" | "type";
|
||||
|
||||
export interface DataGridCellTextVisualState {
|
||||
colorizeTypes: boolean;
|
||||
typeKind: DataGridTypeVisualKind;
|
||||
isNull?: boolean;
|
||||
isDraft?: boolean;
|
||||
isEditing?: boolean;
|
||||
isControl?: boolean;
|
||||
isSelected?: boolean;
|
||||
isCurrentSearchMatch?: boolean;
|
||||
isSearchMatch?: boolean;
|
||||
isDirty?: boolean;
|
||||
isDeleted?: boolean;
|
||||
}
|
||||
|
||||
const DATA_GRID_TYPE_VISUAL_CLASSES: Record<DataGridTypeVisualKind, string> = {
|
||||
integer: "data-grid-type-integer",
|
||||
numeric: "data-grid-type-numeric",
|
||||
string: "data-grid-type-string",
|
||||
boolean: "data-grid-type-boolean",
|
||||
temporal: "data-grid-type-temporal",
|
||||
structured: "data-grid-type-structured",
|
||||
identifier: "data-grid-type-identifier",
|
||||
binary: "data-grid-type-binary",
|
||||
spatial: "data-grid-type-spatial",
|
||||
unknown: "text-muted-foreground",
|
||||
};
|
||||
|
||||
export function dataGridTypeVisualClass(kind: DataGridTypeVisualKind): string {
|
||||
return DATA_GRID_TYPE_VISUAL_CLASSES[kind];
|
||||
}
|
||||
|
||||
/** Keep interaction and edit state more prominent than the optional type cue. */
|
||||
export function resolveDataGridCellTextRole(state: DataGridCellTextVisualState): DataGridCellTextRole {
|
||||
if (state.isNull || state.isDraft) return "muted";
|
||||
if (!state.colorizeTypes || state.typeKind === "unknown" || state.isEditing || state.isControl || state.isSelected || state.isCurrentSearchMatch || state.isSearchMatch || state.isDirty || state.isDeleted) {
|
||||
return "neutral";
|
||||
}
|
||||
return "type";
|
||||
}
|
||||
|
||||
export function dataGridCellTextClass(state: DataGridCellTextVisualState): string {
|
||||
const role = resolveDataGridCellTextRole(state);
|
||||
if (role === "muted") return "text-muted-foreground italic";
|
||||
if (role === "neutral") return "text-foreground";
|
||||
return dataGridTypeVisualClass(state.typeKind);
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
import type { DatabaseType } from "@/types/database";
|
||||
|
||||
/**
|
||||
* Resolve the data type to display in a data-grid column header.
|
||||
*
|
||||
@@ -138,3 +140,133 @@ export function isNumericColumnType(dataType: string | undefined): boolean {
|
||||
const base = normalized.split(/[\s([]/, 1)[0];
|
||||
return NUMERIC_COLUMN_TYPE_BASES.has(base);
|
||||
}
|
||||
|
||||
export const DATA_GRID_TYPE_VISUAL_KINDS = ["integer", "numeric", "string", "boolean", "temporal", "structured", "identifier", "binary", "spatial", "unknown"] as const;
|
||||
|
||||
export type DataGridTypeVisualKind = (typeof DATA_GRID_TYPE_VISUAL_KINDS)[number];
|
||||
|
||||
const INTEGER_COLUMN_TYPE_BASES = new Set([
|
||||
"tinyint",
|
||||
"smallint",
|
||||
"mediumint",
|
||||
"int",
|
||||
"integer",
|
||||
"bigint",
|
||||
"serial",
|
||||
"smallserial",
|
||||
"bigserial",
|
||||
"int1",
|
||||
"int2",
|
||||
"int4",
|
||||
"int8",
|
||||
"int16",
|
||||
"int32",
|
||||
"int64",
|
||||
"int128",
|
||||
"int256",
|
||||
"intn",
|
||||
"uint",
|
||||
"uint8",
|
||||
"uint16",
|
||||
"uint32",
|
||||
"uint64",
|
||||
"uint128",
|
||||
"uint256",
|
||||
"year",
|
||||
]);
|
||||
|
||||
const STRING_COLUMN_TYPE_BASES = new Set([
|
||||
"varchar",
|
||||
"varchar2",
|
||||
"nvarchar",
|
||||
"nvarchar2",
|
||||
"text",
|
||||
"char",
|
||||
"nchar",
|
||||
"ntext",
|
||||
"string",
|
||||
"fixedstring",
|
||||
"tinytext",
|
||||
"mediumtext",
|
||||
"longtext",
|
||||
"clob",
|
||||
"nclob",
|
||||
"long",
|
||||
"enum",
|
||||
"enum8",
|
||||
"enum16",
|
||||
"set",
|
||||
"character",
|
||||
"character varying",
|
||||
"national character",
|
||||
"national character varying",
|
||||
]);
|
||||
|
||||
const BOOLEAN_COLUMN_TYPE_BASES = new Set(["bool", "boolean", "bit"]);
|
||||
const TEMPORAL_COLUMN_TYPE_BASES = new Set(["date", "date32", "daten", "time", "time64", "timen", "timetz", "datetime", "datetime2", "datetime4", "datetime64", "datetimen", "datetimeoffset", "datetimeoffsetn", "smalldatetime", "timestamp", "timestampdty", "timestamptz", "interval"]);
|
||||
const STRUCTURED_COLUMN_TYPE_BASES = new Set(["json", "jsonb", "jsonpath", "xml", "xmltype", "array", "map", "tuple", "struct", "row", "object", "document", "variant"]);
|
||||
const IDENTIFIER_COLUMN_TYPE_BASES = new Set(["uuid", "uniqueidentifier", "rowid", "urowid"]);
|
||||
const BINARY_COLUMN_TYPE_BASES = new Set(["bytea", "blob", "tinyblob", "mediumblob", "longblob", "binary", "varbinary", "image", "raw", "long raw", "bfile"]);
|
||||
const SPATIAL_COLUMN_TYPE_BASES = new Set(["geometry", "geography", "sdo_geometry", "point", "linestring", "polygon", "multipoint", "multilinestring", "multipolygon", "geometrycollection"]);
|
||||
const TYPE_VISUAL_TRANSPARENT_WRAPPERS = new Set(["nullable", "lowcardinality"]);
|
||||
|
||||
function unwrapDataGridColumnType(dataType: string): { normalized: string; array: boolean } {
|
||||
let normalized = dataType.trim().toLowerCase().replace(/\s+/g, " ");
|
||||
let array = false;
|
||||
while (normalized) {
|
||||
if (/\[\s*\]$/.test(normalized)) {
|
||||
array = true;
|
||||
normalized = normalized.replace(/(?:\[\s*\])+$/, "").trim();
|
||||
continue;
|
||||
}
|
||||
const wrapper = normalized.match(/^([a-z][a-z0-9_]*)\s*\((.*)\)$/s);
|
||||
if (!wrapper) break;
|
||||
const wrapperName = wrapper[1] ?? "";
|
||||
if (wrapperName === "array") {
|
||||
array = true;
|
||||
normalized = wrapper[2]?.trim() ?? "";
|
||||
continue;
|
||||
}
|
||||
if (!TYPE_VISUAL_TRANSPARENT_WRAPPERS.has(wrapperName)) break;
|
||||
normalized = wrapper[2]?.trim() ?? "";
|
||||
}
|
||||
// PostgreSQL exposes array type names through Type.name() using its catalog
|
||||
// convention (`_int4`, `_text`, `_jsonb`, including custom arrays).
|
||||
if (normalized.length > 1 && normalized.startsWith("_")) array = true;
|
||||
return { normalized, array };
|
||||
}
|
||||
|
||||
function dataGridColumnTypeBase(dataType: string): { base: string; array: boolean } {
|
||||
const unwrapped = unwrapDataGridColumnType(dataType);
|
||||
let base = unwrapped.normalized.replace(/\s+unsigned\b/g, "").trim();
|
||||
const parameterStart = base.indexOf("(");
|
||||
if (parameterStart >= 0) base = base.slice(0, parameterStart).trim();
|
||||
if (base.startsWith("timestamp with ")) base = "timestamptz";
|
||||
else if (base.startsWith("timestamp without ")) base = "timestamp";
|
||||
else if (base.startsWith("time with ")) base = "timetz";
|
||||
else if (base.startsWith("time without ")) base = "time";
|
||||
else if (base === "double precision") base = "double";
|
||||
return { base, array: unwrapped.array };
|
||||
}
|
||||
|
||||
/**
|
||||
* Collapse driver-specific SQL type names into the small semantic palette used
|
||||
* by grid headers and values. Unknown types deliberately stay neutral.
|
||||
*/
|
||||
export function resolveDataGridTypeVisualKind(dataType: string | undefined, databaseType?: DatabaseType): DataGridTypeVisualKind {
|
||||
if (!dataType?.trim()) return "unknown";
|
||||
const { base, array } = dataGridColumnTypeBase(dataType);
|
||||
if (array) return "structured";
|
||||
if (databaseType === "sqlserver" && (base === "timestamp" || base === "rowversion")) return "binary";
|
||||
if (databaseType === "postgres" && (base === "bit" || base === "bit varying")) return "binary";
|
||||
if (INTEGER_COLUMN_TYPE_BASES.has(base)) return "integer";
|
||||
if (isNumericColumnType(dataType)) return "numeric";
|
||||
if (BOOLEAN_COLUMN_TYPE_BASES.has(base)) return "boolean";
|
||||
if (TEMPORAL_COLUMN_TYPE_BASES.has(base) || base.startsWith("timestamp_")) return "temporal";
|
||||
if (STRUCTURED_COLUMN_TYPE_BASES.has(base)) return "structured";
|
||||
if (IDENTIFIER_COLUMN_TYPE_BASES.has(base)) return "identifier";
|
||||
if (BINARY_COLUMN_TYPE_BASES.has(base)) return "binary";
|
||||
if (SPATIAL_COLUMN_TYPE_BASES.has(base)) return "spatial";
|
||||
if (STRING_COLUMN_TYPE_BASES.has(base)) return "string";
|
||||
return "unknown";
|
||||
}
|
||||
@@ -1,3 +1,7 @@
|
||||
import type { DataGridTypeVisualKind } from "@/lib/dataGrid/dataGridColumnType";
|
||||
|
||||
export type DataGridTypeForegrounds = Record<DataGridTypeVisualKind, string>;
|
||||
|
||||
export interface DataGridPaintTheme {
|
||||
background: string;
|
||||
border: string;
|
||||
@@ -28,6 +32,7 @@ export interface DataGridPaintTheme {
|
||||
rowNumberTextNew: string;
|
||||
rowNumberTextEdited: string;
|
||||
rowNumberTextDeleted: string;
|
||||
typeForegrounds: DataGridTypeForegrounds;
|
||||
}
|
||||
|
||||
export const DATA_GRID_DARK_SEARCH_COLORS = {
|
||||
@@ -266,6 +271,18 @@ export function resolveDataGridPaintTheme(options: { getVar: (name: string) => s
|
||||
const rowNumberDeleted = isDark ? DATA_GRID_DARK_ROW_NUMBER_DELETED_BG : DATA_GRID_LIGHT_ROW_NUMBER_DELETED_BG;
|
||||
const rowNumberActive = activeSurface;
|
||||
const rowNumberSelected = isDark ? "rgb(30, 64, 96)" : "rgb(191, 219, 254)";
|
||||
const typeForegrounds: DataGridTypeForegrounds = {
|
||||
integer: paintToken(getVar, "--data-grid-type-integer-fg", isDark ? "#93c5fd" : "#1d4ed8"),
|
||||
numeric: paintToken(getVar, "--data-grid-type-numeric-fg", isDark ? "#67e8f9" : "#0e7490"),
|
||||
string: paintToken(getVar, "--data-grid-type-string-fg", isDark ? "#86efac" : "#166534"),
|
||||
boolean: paintToken(getVar, "--data-grid-type-boolean-fg", isDark ? "#fdba74" : "#c2410c"),
|
||||
temporal: paintToken(getVar, "--data-grid-type-temporal-fg", isDark ? "#d8b4fe" : "#7e22ce"),
|
||||
structured: paintToken(getVar, "--data-grid-type-structured-fg", isDark ? "#f9a8d4" : "#be185d"),
|
||||
identifier: paintToken(getVar, "--data-grid-type-identifier-fg", isDark ? "#fcd34d" : "#92400e"),
|
||||
binary: paintToken(getVar, "--data-grid-type-binary-fg", isDark ? "#fca5a5" : "#b91c1c"),
|
||||
spatial: paintToken(getVar, "--data-grid-type-spatial-fg", isDark ? "#6ee7b7" : "#047857"),
|
||||
unknown: foreground,
|
||||
};
|
||||
|
||||
return {
|
||||
background,
|
||||
@@ -297,5 +314,10 @@ export function resolveDataGridPaintTheme(options: { getVar: (name: string) => s
|
||||
rowNumberTextNew: isDark ? "rgb(94, 233, 181)" : "rgb(0, 122, 85)",
|
||||
rowNumberTextEdited: isDark ? "rgb(255, 210, 48)" : "rgb(187, 77, 0)",
|
||||
rowNumberTextDeleted: destructive,
|
||||
typeForegrounds,
|
||||
};
|
||||
}
|
||||
|
||||
export function dataGridTypeForeground(theme: Pick<DataGridPaintTheme, "foreground" | "typeForegrounds">, kind: DataGridTypeVisualKind): string {
|
||||
return theme.typeForegrounds[kind] ?? theme.foreground;
|
||||
}
|
||||
@@ -60,6 +60,10 @@ describe("EDITOR_SETTINGS_DRAFT_KEYS", () => {
|
||||
expect(EDITOR_SETTINGS_DRAFT_KEYS).toContain("dataTabReuseMode");
|
||||
});
|
||||
|
||||
it("includes data grid type colors", () => {
|
||||
expect(EDITOR_SETTINGS_DRAFT_KEYS).toContain("colorizeDataGridCellTypes");
|
||||
});
|
||||
|
||||
it("includes completionTriggerMode", () => {
|
||||
expect(EDITOR_SETTINGS_DRAFT_KEYS).toContain("completionTriggerMode");
|
||||
});
|
||||
@@ -86,6 +90,10 @@ describe("editorSettingsDraftFromSettings", () => {
|
||||
expect(draft.continueOnErrorOnBatch).toBe(false);
|
||||
});
|
||||
|
||||
it("maps the data grid type color preference from settings", () => {
|
||||
expect(editorSettingsDraftFromSettings(makeSettings({ colorizeDataGridCellTypes: false })).colorizeDataGridCellTypes).toBe(false);
|
||||
});
|
||||
|
||||
it("preserves the table-open default for legacy settings", () => {
|
||||
const settings = makeSettings();
|
||||
delete (settings as Partial<EditorSettings>).tableOpenPageSize;
|
||||
|
||||
@@ -137,6 +137,7 @@ describe("settings search", () => {
|
||||
{ titleKey: "settings.confirmDangerousSqlExecution", category: "editor", targetId: "editor" },
|
||||
{ titleKey: "settings.continueOnErrorOnBatch", category: "editor", targetId: "editor" },
|
||||
{ titleKey: "settings.dataGridQuickEntry", category: "data", targetId: "data" },
|
||||
{ titleKey: "settings.colorizeDataGridCellTypes", category: "data", targetId: "data" },
|
||||
{ titleKey: "transfer.dataTransfer", category: "appearance", targetId: "appearance" },
|
||||
{ titleKey: "toolbar.driverManager", category: "appearance", targetId: "appearance" },
|
||||
{ titleKey: "toolbar.theme", category: "appearance", targetId: "appearance" },
|
||||
|
||||
@@ -31,6 +31,7 @@ export const EDITOR_SETTINGS_DRAFT_KEYS = [
|
||||
"tabLayout",
|
||||
"showColumnCommentsInHeader",
|
||||
"showColumnTypesInHeader",
|
||||
"colorizeDataGridCellTypes",
|
||||
"showIndexIndicatorsInHeader",
|
||||
"compactColumnHeaderActions",
|
||||
"dataGridQuickEntry",
|
||||
|
||||
@@ -173,6 +173,7 @@ export const SETTINGS_SEARCH_DEFINITIONS: readonly SettingsSearchDefinition[] =
|
||||
{ id: "query-result-max-rows", category: "data", titleKey: "settings.queryResultMaxRows", descriptionKey: "settings.queryResultMaxRowsDescription", targetId: "data" },
|
||||
{ id: "data-grid-header-comments", category: "data", titleKey: "settings.showColumnCommentsInHeader", descriptionKey: "settings.showColumnCommentsInHeaderDescription", targetId: "data" },
|
||||
{ id: "data-grid-header-types", category: "data", titleKey: "settings.showColumnTypesInHeader", descriptionKey: "settings.showColumnTypesInHeaderDescription", targetId: "data" },
|
||||
{ id: "data-grid-cell-type-colors", category: "data", titleKey: "settings.colorizeDataGridCellTypes", descriptionKey: "settings.colorizeDataGridCellTypesDescription", targetId: "data" },
|
||||
{ id: "data-grid-index-indicators", category: "data", titleKey: "settings.showIndexIndicatorsInHeader", descriptionKey: "settings.showIndexIndicatorsInHeaderDescription", targetId: "data" },
|
||||
{ id: "data-grid-compact-header-actions", category: "data", titleKey: "settings.compactColumnHeaderActions", descriptionKey: "settings.compactColumnHeaderActionsDescription", targetId: "data" },
|
||||
{ id: "data-grid-auto-total", category: "data", titleKey: "settings.autoCalculateTotalRows", descriptionKey: "settings.autoCalculateTotalRowsDescription", targetId: "data" },
|
||||
|
||||
@@ -5,6 +5,12 @@ import { DEFAULT_EDITOR_SETTINGS, EXECUTE_MODE_CURRENT_DEFAULT_VERSION, enforceR
|
||||
import type { AiConfigItem } from "@/types/ai";
|
||||
|
||||
describe("normalizeEditorSettings", () => {
|
||||
it("keeps data type colors disabled by default and preserves an explicit opt-in", () => {
|
||||
expect(normalizeEditorSettings({}).colorizeDataGridCellTypes).toBe(false);
|
||||
expect(normalizeEditorSettings({ colorizeDataGridCellTypes: true }).colorizeDataGridCellTypes).toBe(true);
|
||||
expect(normalizeEditorSettings({ colorizeDataGridCellTypes: false }).colorizeDataGridCellTypes).toBe(false);
|
||||
});
|
||||
|
||||
it("defaults and migrates the data-tab reuse mode", () => {
|
||||
expect(normalizeEditorSettings({}).dataTabReuseMode).toBe("same-table");
|
||||
expect(normalizeEditorSettings({ dataTabReuseMode: "always-new" }).dataTabReuseMode).toBe("always-new");
|
||||
|
||||
@@ -538,6 +538,7 @@ export interface EditorSettings {
|
||||
mongoViewMode: "document" | "table";
|
||||
showColumnCommentsInHeader: boolean;
|
||||
showColumnTypesInHeader: boolean;
|
||||
colorizeDataGridCellTypes: boolean;
|
||||
showIndexIndicatorsInHeader: boolean;
|
||||
compactColumnHeaderActions: boolean;
|
||||
columnWidthDensity: ColumnWidthDensity;
|
||||
@@ -729,6 +730,7 @@ export const DEFAULT_EDITOR_SETTINGS: EditorSettings = {
|
||||
mongoViewMode: "document",
|
||||
showColumnCommentsInHeader: true,
|
||||
showColumnTypesInHeader: true,
|
||||
colorizeDataGridCellTypes: false,
|
||||
showIndexIndicatorsInHeader: true,
|
||||
compactColumnHeaderActions: true,
|
||||
columnWidthDensity: "standard",
|
||||
@@ -1065,6 +1067,7 @@ export function normalizeEditorSettings(settings: Partial<EditorSettings>, exist
|
||||
mongoViewMode: settings.mongoViewMode === "table" ? "table" : DEFAULT_EDITOR_SETTINGS.mongoViewMode,
|
||||
showColumnCommentsInHeader: settings.showColumnCommentsInHeader ?? DEFAULT_EDITOR_SETTINGS.showColumnCommentsInHeader,
|
||||
showColumnTypesInHeader: settings.showColumnTypesInHeader ?? DEFAULT_EDITOR_SETTINGS.showColumnTypesInHeader,
|
||||
colorizeDataGridCellTypes: settings.colorizeDataGridCellTypes ?? DEFAULT_EDITOR_SETTINGS.colorizeDataGridCellTypes,
|
||||
showIndexIndicatorsInHeader: settings.showIndexIndicatorsInHeader ?? DEFAULT_EDITOR_SETTINGS.showIndexIndicatorsInHeader,
|
||||
compactColumnHeaderActions: settings.compactColumnHeaderActions ?? DEFAULT_EDITOR_SETTINGS.compactColumnHeaderActions,
|
||||
columnWidthDensity: normalizeColumnWidthDensity(settings.columnWidthDensity),
|
||||
@@ -1640,6 +1643,7 @@ export const useSettingsStore = defineStore("settings", () => {
|
||||
if (partial.mongoViewMode !== undefined) editorSettings.value.mongoViewMode = partial.mongoViewMode;
|
||||
if (partial.showColumnCommentsInHeader !== undefined) editorSettings.value.showColumnCommentsInHeader = partial.showColumnCommentsInHeader;
|
||||
if (partial.showColumnTypesInHeader !== undefined) editorSettings.value.showColumnTypesInHeader = partial.showColumnTypesInHeader;
|
||||
if (partial.colorizeDataGridCellTypes !== undefined) editorSettings.value.colorizeDataGridCellTypes = partial.colorizeDataGridCellTypes === true;
|
||||
if (partial.showIndexIndicatorsInHeader !== undefined) editorSettings.value.showIndexIndicatorsInHeader = partial.showIndexIndicatorsInHeader;
|
||||
if (partial.compactColumnHeaderActions !== undefined) editorSettings.value.compactColumnHeaderActions = partial.compactColumnHeaderActions;
|
||||
if (partial.columnWidthDensity !== undefined) editorSettings.value.columnWidthDensity = normalizeColumnWidthDensity(partial.columnWidthDensity);
|
||||
|
||||
@@ -76,6 +76,68 @@
|
||||
*/
|
||||
@import "./tokens.css";
|
||||
|
||||
:root {
|
||||
--data-grid-type-integer-fg: #1d4ed8;
|
||||
--data-grid-type-numeric-fg: #0e7490;
|
||||
--data-grid-type-string-fg: #166534;
|
||||
--data-grid-type-boolean-fg: #c2410c;
|
||||
--data-grid-type-temporal-fg: #7e22ce;
|
||||
--data-grid-type-structured-fg: #be185d;
|
||||
--data-grid-type-identifier-fg: #92400e;
|
||||
--data-grid-type-binary-fg: #b91c1c;
|
||||
--data-grid-type-spatial-fg: #047857;
|
||||
}
|
||||
|
||||
:root.dark {
|
||||
--data-grid-type-integer-fg: #93c5fd;
|
||||
--data-grid-type-numeric-fg: #67e8f9;
|
||||
--data-grid-type-string-fg: #86efac;
|
||||
--data-grid-type-boolean-fg: #fdba74;
|
||||
--data-grid-type-temporal-fg: #d8b4fe;
|
||||
--data-grid-type-structured-fg: #f9a8d4;
|
||||
--data-grid-type-identifier-fg: #fcd34d;
|
||||
--data-grid-type-binary-fg: #fca5a5;
|
||||
--data-grid-type-spatial-fg: #6ee7b7;
|
||||
}
|
||||
|
||||
@layer components {
|
||||
.data-grid-type-integer {
|
||||
color: var(--data-grid-type-integer-fg);
|
||||
}
|
||||
|
||||
.data-grid-type-numeric {
|
||||
color: var(--data-grid-type-numeric-fg);
|
||||
}
|
||||
|
||||
.data-grid-type-string {
|
||||
color: var(--data-grid-type-string-fg);
|
||||
}
|
||||
|
||||
.data-grid-type-boolean {
|
||||
color: var(--data-grid-type-boolean-fg);
|
||||
}
|
||||
|
||||
.data-grid-type-temporal {
|
||||
color: var(--data-grid-type-temporal-fg);
|
||||
}
|
||||
|
||||
.data-grid-type-structured {
|
||||
color: var(--data-grid-type-structured-fg);
|
||||
}
|
||||
|
||||
.data-grid-type-identifier {
|
||||
color: var(--data-grid-type-identifier-fg);
|
||||
}
|
||||
|
||||
.data-grid-type-binary {
|
||||
color: var(--data-grid-type-binary-fg);
|
||||
}
|
||||
|
||||
.data-grid-type-spatial {
|
||||
color: var(--data-grid-type-spatial-fg);
|
||||
}
|
||||
}
|
||||
|
||||
:root[data-corner-style="large"] {
|
||||
--dbx-radius-default: 6px;
|
||||
--dbx-radius-sm: 6px;
|
||||
|
||||
Reference in new issue
Block a user