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