feat(grid): let users pick how boolean cells read

This commit is contained in:
弈秋
2026-10-16 15:07:06 +08:00
committed by GitHub
parent 13e1823617
commit ce113aabf1
20 changed files with 379 additions and 1 deletions
+10 -1
View File
@@ -204,7 +204,7 @@ import {
import { adjacentDataGridDetailIndex, type DataGridDetailNavigationDelta } from "@/lib/dataGrid/dataGridDetailNavigation";
import { applyColumnFormatter, DataGridDateTimePatterns, displayTimeZoneOption, formatIoTDBTimestampEditorValue, iotdbTimestampFractionDigits, iotdbTimestampPrecision, parseIoTDBTimestampEditorValue, type ColumnFormatterConfig } from "@/lib/dataGrid/columnFormatter";
import { temporalCellEditorConfig, type TemporalCellEditorConfig } from "@/lib/dataGrid/dataGridTemporalEditor";
import { BOOLEAN_CELL_EDITOR_VALUES, booleanCellEditorValue, isBooleanCellValue, isBooleanColumnType, isPointInBooleanCheckbox, nextBooleanCellValue, normalizeBooleanCellValue, parseBooleanCellEditorValue } from "@/lib/dataGrid/dataGridBooleanColumn";
import { BOOLEAN_CELL_EDITOR_VALUES, booleanCellDisplayText, booleanCellEditorValue, isBooleanCellValue, isBooleanColumnType, isPointInBooleanCheckbox, nextBooleanCellValue, normalizeBooleanCellValue, parseBooleanCellEditorValue } from "@/lib/dataGrid/dataGridBooleanColumn";
import { resolveDataGridColumnNullability, resolveDataGridColumnsByResultIndex } from "@/lib/dataGrid/dataGridColumnMetadata";
import { resolveDataGridNewRowCellPlaceholder } from "@/lib/dataGrid/dataGridDefaultPlaceholder";
import {
@@ -461,6 +461,7 @@ const multiRowTranspose = computed(() => settingsStore.editorSettings.dataGridMu
const hideNullColumns = computed(() => settingsStore.editorSettings.dataGridHideNullColumns);
const hideEmptyColumns = computed(() => settingsStore.editorSettings.dataGridHideEmptyColumns);
const booleanCellsUseCheckbox = computed(() => settingsStore.editorSettings.dataGridBooleanDisplayMode === "checkbox");
const booleanCellValueStyle = computed(() => settingsStore.editorSettings.dataGridBooleanValueStyle);
const booleanDisplayMode = computed(() => settingsStore.editorSettings.dataGridBooleanDisplayMode);
const colorizeDataGridCellTypes = computed(() => settingsStore.editorSettings.colorizeDataGridCellTypes);
const { isDark, themePalette } = useTheme();
@@ -7667,6 +7668,8 @@ function formatCell(value: CellValue, columnIndex?: number, originalBytes?: numb
if (arrayDisplay !== undefined) return arrayDisplay;
const binaryDisplay = formatter ? null : binaryCellDisplayText(value, columnIndex === undefined ? undefined : allColumnTypes.value[columnIndex], originalBytes, resolvedDatabaseType.value);
if (binaryDisplay !== null) return binaryDisplay;
const booleanDisplay = formatter ? undefined : booleanCellDisplayText(value, booleanCellValueStyle.value);
if (booleanDisplay !== undefined) return booleanDisplay;
const s = applyColumnFormatter(value, formatter);
return limitDisplay ? limitDataGridCellDisplay(s, resolvedDatabaseType.value === "sqlserver" ? SQLSERVER_DATA_GRID_CELL_DISPLAY_MAX_LENGTH : undefined) : s;
}
@@ -8679,6 +8682,12 @@ watch(showDataGridTopbar, () => nextTick(observeDataGridTopbarWidth), {
});
watch(columnAligns, () => scheduleCanvasDraw());
watch(booleanDisplayMode, () => scheduleCanvasDraw());
// The value style is baked into the cached cell text, so flipping it must drop
// the cache as well as repaint; canvas otherwise keeps the stale TRUE/FALSE.
watch(booleanCellValueStyle, () => {
clearCellFormatCache();
scheduleCanvasDraw();
});
watch(flatteningMultiLineEnabled, () => scheduleCanvasDraw());
watch(showWhitespaceEnabled, () => scheduleCanvasDraw());
watch(dataGridStripedRows, () => scheduleCanvasDraw());
@@ -0,0 +1,166 @@
// @vitest-environment happy-dom
import { createApp, defineComponent, h, markRaw, nextTick, shallowRef, type App, type PropType } from "vue";
import { createPinia, setActivePinia } from "pinia";
import { afterEach, describe, expect, it, vi } from "vitest";
import i18n from "@/i18n";
import type { QueryResult } from "@/types/database";
import { TooltipProvider } from "@/components/ui/tooltip";
vi.mock("@/lib/backend/api", async (importOriginal) => {
const actual = await importOriginal<typeof import("@/lib/backend/api")>();
return {
...actual,
buildTableSelectSql: vi.fn(async ({ tableName }: { tableName: string }) => `SELECT * FROM ${tableName}`),
loadEditorSettings: vi.fn().mockResolvedValue({}),
saveEditorSettings: vi.fn().mockResolvedValue(undefined),
};
});
vi.mock("vue-virtual-scroller", async () => {
const { defineComponent, h } = await import("vue");
return {
RecycleScroller: defineComponent({
props: {
items: {
type: Array as PropType<unknown[]>,
default: () => [],
},
},
setup(props, { attrs, slots }) {
return () =>
h(
"div",
attrs,
props.items.map((item) => slots.default?.({ item })),
);
},
}),
};
});
vi.mock("@/composables/useDataGridColumnResize", async (importOriginal) => {
const actual = await importOriginal<typeof import("@/composables/useDataGridColumnResize")>();
const { ref } = await import("vue");
return {
...actual,
useDataGridColumnResize: () => ({
initColumnWidths: vi.fn(),
onResizeStart: vi.fn(),
autoFitColumn: vi.fn(),
renderedColumnWidths: ref([120]),
totalWidth: ref(120),
columnVars: ref({ "--total-w": "120px" }),
getIsResizing: () => false,
}),
};
});
import DataGrid from "../DataGrid.vue";
import { useSettingsStore } from "@/stores/settingsStore";
const mountedApps: Array<{ app: App; host: HTMLElement }> = [];
function mountGrid() {
const pinia = createPinia();
setActivePinia(pinia);
const settingsStore = useSettingsStore();
settingsStore.updateEditorSettings({ dataGridRenderMode: "canvas" });
const resultRef = shallowRef(
markRaw<QueryResult>({
columns: ["id", "flag"],
rows: [
[1, true],
[2, false],
],
affected_rows: 0,
execution_time_ms: 0,
}),
);
const host = document.createElement("div");
document.body.append(host);
const Root = defineComponent({
setup() {
return () =>
h(
TooltipProvider,
{ delayDuration: 0 },
{
default: () =>
h(DataGrid, {
result: resultRef.value,
databaseType: "postgres",
context: "table-data",
connectionId: "conn-1",
database: "db1",
tableMeta: {
tableName: "items",
columns: [
{ name: "id", data_type: "int" },
{ name: "flag", data_type: "boolean" },
],
primaryKeys: ["id"],
},
onExecuteSql: vi.fn().mockResolvedValue(undefined),
}),
},
);
},
});
const app = createApp(Root);
app.use(pinia);
app.use(i18n);
app.mount(host);
// Boolean cell text lives in the DOM surface; canvas mode paints it instead.
settingsStore.updateEditorSettings({ dataGridRenderMode: "dom" });
mountedApps.push({ app, host });
return { host, settingsStore };
}
async function settle() {
await nextTick();
await Promise.resolve();
await nextTick();
await new Promise((resolve) => setTimeout(resolve, 20));
}
/** Text of the boolean ("flag") column for each rendered row. */
function flagColumnText(host: HTMLElement): string[] {
return [...host.querySelectorAll<HTMLElement>('[data-row-index] [data-visible-col-index="1"]')].map((node) => node.textContent?.trim() ?? "");
}
afterEach(() => {
for (const { app, host } of mountedApps.splice(0)) {
app.unmount();
host.remove();
}
});
describe("DataGrid boolean cell text style", () => {
it("shows TRUE/FALSE by default and 1/0 once the zero-one style is selected", async () => {
const { host, settingsStore } = mountGrid();
await settle();
expect(flagColumnText(host)).toEqual(["TRUE", "FALSE"]);
settingsStore.updateEditorSettings({ dataGridBooleanValueStyle: "zero-one" });
await settle();
expect(flagColumnText(host)).toEqual(["1", "0"]);
});
it("repaints the cached cell text when the style is toggled back", async () => {
const { host, settingsStore } = mountGrid();
await settle();
settingsStore.updateEditorSettings({ dataGridBooleanValueStyle: "zero-one" });
await settle();
expect(flagColumnText(host)).toEqual(["1", "0"]);
settingsStore.updateEditorSettings({ dataGridBooleanValueStyle: "true-false" });
await settle();
expect(flagColumnText(host)).toEqual(["TRUE", "FALSE"]);
});
});
@@ -48,6 +48,7 @@ import {
ShieldAlert,
AlignLeft,
AlignRight,
Binary,
PanelsTopLeft,
Palette,
CircleAlert,
@@ -310,6 +311,8 @@ function groupedQueryReadonlyColumnIndexes(tab: QueryTab): number[] | undefined
const settingsStore = useSettingsStore();
const booleanDisplayMode = computed(() => settingsStore.editorSettings.dataGridBooleanDisplayMode);
const setBooleanDisplayMode = (mode: "checkbox" | "dropdown") => settingsStore.updateEditorSettings({ dataGridBooleanDisplayMode: mode });
const booleanValueStyle = computed(() => settingsStore.editorSettings.dataGridBooleanValueStyle);
const setBooleanValueStyle = (style: "true-false" | "zero-one") => settingsStore.updateEditorSettings({ dataGridBooleanValueStyle: style });
const colorizeDataGridCellTypes = computed(() => settingsStore.editorSettings.colorizeDataGridCellTypes);
const setColorizeDataGridCellTypes = (value: boolean) => settingsStore.updateEditorSettings({ colorizeDataGridCellTypes: value });
const { toast } = useToast();
@@ -2461,6 +2464,32 @@ defineExpose({
</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">
<Binary class="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
<span>{{ t("grid.booleanValueStyle") }}</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 disabled:pointer-events-none disabled:opacity-50"
:disabled="booleanDisplayMode === 'checkbox'"
:class="booleanValueStyle === 'true-false' ? 'bg-background font-semibold text-foreground shadow-sm' : 'text-muted-foreground hover:text-foreground'"
@click="setBooleanValueStyle('true-false')"
>
{{ t("grid.booleanValueTrueFalse") }}
</button>
<button
type="button"
class="h-5 min-w-0 truncate whitespace-nowrap rounded-[5px] px-2 text-xs transition-colors disabled:pointer-events-none disabled:opacity-50"
:disabled="booleanDisplayMode === 'checkbox'"
:class="booleanValueStyle === 'zero-one' ? 'bg-background font-semibold text-foreground shadow-sm' : 'text-muted-foreground hover:text-foreground'"
@click="setBooleanValueStyle('zero-one')"
>
{{ t("grid.booleanValueZeroOne") }}
</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" />
@@ -3164,6 +3193,32 @@ defineExpose({
</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">
<Binary class="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
<span>{{ t("grid.booleanValueStyle") }}</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 disabled:pointer-events-none disabled:opacity-50"
:disabled="booleanDisplayMode === 'checkbox'"
:class="booleanValueStyle === 'true-false' ? 'bg-background font-semibold text-foreground shadow-sm' : 'text-muted-foreground hover:text-foreground'"
@click="setBooleanValueStyle('true-false')"
>
{{ t("grid.booleanValueTrueFalse") }}
</button>
<button
type="button"
class="h-5 min-w-0 truncate whitespace-nowrap rounded-[5px] px-2 text-xs transition-colors disabled:pointer-events-none disabled:opacity-50"
:disabled="booleanDisplayMode === 'checkbox'"
:class="booleanValueStyle === 'zero-one' ? 'bg-background font-semibold text-foreground shadow-sm' : 'text-muted-foreground hover:text-foreground'"
@click="setBooleanValueStyle('zero-one')"
>
{{ t("grid.booleanValueZeroOne") }}
</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" />
@@ -0,0 +1,44 @@
import { describe, expect, it } from "vitest";
import az from "../locales/az";
import en from "../locales/en";
import es from "../locales/es";
import id from "../locales/id";
import it_ from "../locales/it";
import ja from "../locales/ja";
import ko from "../locales/ko";
import ptBR from "../locales/pt-BR";
import ru from "../locales/ru";
import th from "../locales/th";
import tr from "../locales/tr";
import zhCN from "../locales/zh-CN";
import zhTW from "../locales/zh-TW";
const locales: Array<[string, Record<string, unknown>]> = [
["az", az],
["en", en],
["es", es],
["id", id],
["it", it_],
["ja", ja],
["ko", ko],
["pt-BR", ptBR],
["ru", ru],
["th", th],
["tr", tr],
["zh-CN", zhCN],
["zh-TW", zhTW],
];
// The grid display-settings panel renders in whatever locale the user runs, so a
// missing key would leak a raw key next to the boolean value-style control.
const GRID_BOOLEAN_VALUE_STYLE_KEYS = ["booleanValueStyle", "booleanValueTrueFalse", "booleanValueZeroOne"] as const;
describe("grid boolean value style locale parity", () => {
it.each(locales)("%s exposes the boolean value-style copy", (_name, locale) => {
const grid = (locale as { grid: Record<string, unknown> }).grid;
for (const key of GRID_BOOLEAN_VALUE_STYLE_KEYS) {
expect(grid[key], `${_name}: grid.${key}`).toBeTypeOf("string");
expect(grid[key] as string, `${_name}: grid.${key}`).not.toHaveLength(0);
}
});
});
+3
View File
@@ -2214,6 +2214,9 @@ export default withEnglishFallback({
booleanDisplayMode: "Məntiqi dəyərlərin göstərilməsi",
booleanDisplayCheckbox: "İşarə qutusu",
booleanDisplayDropdown: "Açılan siyahı",
booleanValueStyle: "Məntiqi dəyər mətni",
booleanValueTrueFalse: "TRUE / FALSE",
booleanValueZeroOne: "0 / 1",
moreValues: "Daha {count} dəyər var, nəticələri daraltmaq üçün yazmağa davam edin",
filterByValue: "Bu dəyərə görə süz",
filterExcludeValue: "Bu dəyəri istisna et",
+3
View File
@@ -2951,6 +2951,9 @@ export default {
booleanDisplayMode: "Boolean display",
booleanDisplayCheckbox: "Check",
booleanDisplayDropdown: "Dropdown",
booleanValueStyle: "Boolean text",
booleanValueTrueFalse: "TRUE / FALSE",
booleanValueZeroOne: "0 / 1",
moreValues: "{count} more values, keep typing to narrow results",
filterByValue: "Filter by This Value",
filterExcludeValue: "Exclude This Value",
+3
View File
@@ -3138,6 +3138,9 @@ export default withEnglishFallback({
booleanDisplayMode: "Visualización de booleanos",
booleanDisplayCheckbox: "Check",
booleanDisplayDropdown: "Lista",
booleanValueStyle: "Texto booleano",
booleanValueTrueFalse: "TRUE / FALSE",
booleanValueZeroOne: "0 / 1",
batchAppendPasteNotEditable: "El resultado actual no se puede editar.",
batchAppendPasteInvalidTarget: "Primero seleccione una celda de número de fila de una nueva fila vacía y luego pegue.",
batchAppendPasteTargetNotEmpty: "La nueva fila seleccionada ya contiene datos.",
+3
View File
@@ -2512,6 +2512,9 @@ export default withEnglishFallback({
booleanDisplayMode: "Tampilan boolean",
booleanDisplayCheckbox: "Centang",
booleanDisplayDropdown: "Dropdown",
booleanValueStyle: "Teks boolean",
booleanValueTrueFalse: "TRUE / FALSE",
booleanValueZeroOne: "0 / 1",
moreValues: "{count} nilai lagi, terus ketik untuk mempersempit hasil",
filterByValue: "Filter berdasarkan Nilai Ini",
filterExcludeValue: "Kecualikan Nilai Ini",
+3
View File
@@ -3011,6 +3011,9 @@ export default withEnglishFallback({
booleanDisplayMode: "Visualizzazione booleani",
booleanDisplayCheckbox: "Check",
booleanDisplayDropdown: "Dropdown",
booleanValueStyle: "Testo booleano",
booleanValueTrueFalse: "TRUE / FALSE",
booleanValueZeroOne: "0 / 1",
batchAppendPasteNotEditable: "Il risultato corrente non è modificabile.",
batchAppendPasteInvalidTarget: "Seleziona prima una cella vuota della riga nuova, poi incolla.",
batchAppendPasteTargetNotEmpty: "La riga nuova selezionata contiene già dati.",
+3
View File
@@ -3026,6 +3026,9 @@ export default withEnglishFallback({
booleanDisplayMode: "ブール値の表示",
booleanDisplayCheckbox: "チェック",
booleanDisplayDropdown: "ドロップダウン",
booleanValueStyle: "ブール値のテキスト",
booleanValueTrueFalse: "TRUE / FALSE",
booleanValueZeroOne: "0 / 1",
batchAppendPasteNotEditable: "現在の結果は編集できません。",
batchAppendPasteInvalidTarget: "空の新規行の行番号セルを選択してから貼り付けてください。",
batchAppendPasteTargetNotEmpty: "選択した新規行には既にデータが含まれています。",
+3
View File
@@ -2582,6 +2582,9 @@ export default withEnglishFallback({
booleanDisplayMode: "불리언 표시",
booleanDisplayCheckbox: "체크",
booleanDisplayDropdown: "드롭다운",
booleanValueStyle: "불리언 값 텍스트",
booleanValueTrueFalse: "TRUE / FALSE",
booleanValueZeroOne: "0 / 1",
moreValues: "{count}개 값 더, 결과를 좁히려면 계속 입력하세요",
filterByValue: "이 값으로 필터",
filterExcludeValue: "이 값 제외",
+3
View File
@@ -3011,6 +3011,9 @@ export default withEnglishFallback({
booleanDisplayMode: "Exibição booleana",
booleanDisplayCheckbox: "Check",
booleanDisplayDropdown: "Dropdown",
booleanValueStyle: "Texto booleano",
booleanValueTrueFalse: "TRUE / FALSE",
booleanValueZeroOne: "0 / 1",
batchAppendPasteNotEditable: "O resultado atual não é editável.",
batchAppendPasteInvalidTarget: "Selecione primeiro o número da linha de uma nova linha vazia e depois cole.",
batchAppendPasteTargetNotEmpty: "A nova linha selecionada já contém dados.",
+3
View File
@@ -2699,6 +2699,9 @@ export default withEnglishFallback({
booleanDisplayMode: "Отображение логических значений",
booleanDisplayCheckbox: "Флажок",
booleanDisplayDropdown: "Раскрывающийся список",
booleanValueStyle: "Текст логического значения",
booleanValueTrueFalse: "TRUE / FALSE",
booleanValueZeroOne: "0 / 1",
moreValues: "ещё {count} значений, продолжайте вводить для уточнения",
filterByValue: "Фильтровать по этому значению",
filterExcludeValue: "Исключить это значение",
+3
View File
@@ -2902,6 +2902,9 @@ export default withEnglishFallback({
booleanDisplayMode: "การแสดง Boolean",
booleanDisplayCheckbox: "Checkbox",
booleanDisplayDropdown: "Dropdown",
booleanValueStyle: "ข้อความค่า Boolean",
booleanValueTrueFalse: "TRUE / FALSE",
booleanValueZeroOne: "0 / 1",
moreValues: "มีค่าอีก {count} รายการ พิมพ์ต่อเพื่อจำกัดผลลัพธ์",
filterByValue: "กรองตามค่านี้",
filterExcludeValue: "ไม่รวมค่านี้",
+3
View File
@@ -2207,6 +2207,9 @@ export default withEnglishFallback({
booleanDisplayMode: "Boole görünümü",
booleanDisplayCheckbox: "Onay kutusu",
booleanDisplayDropdown: "Açılır liste",
booleanValueStyle: "Boole değeri metni",
booleanValueTrueFalse: "TRUE / FALSE",
booleanValueZeroOne: "0 / 1",
moreValues: "{count} değer daha var, sonuçları daraltmak için yazmaya devam edin",
filterByValue: "Bu Değere Göre Filtrele",
filterExcludeValue: "Bu Değeri Hariç Tut",
+3
View File
@@ -2885,6 +2885,9 @@ export default withEnglishFallback({
booleanDisplayMode: "布尔值显示",
booleanDisplayCheckbox: "复选框",
booleanDisplayDropdown: "下拉列表",
booleanValueStyle: "布尔值文本",
booleanValueTrueFalse: "TRUE / FALSE",
booleanValueZeroOne: "0 / 1",
moreValues: "还有 {count} 个值,输入关键词继续缩小范围",
filterByValue: "筛选此值",
filterExcludeValue: "排除此值",
+3
View File
@@ -3012,6 +3012,9 @@ export default withEnglishFallback({
booleanDisplayMode: "布林值顯示",
booleanDisplayCheckbox: "核取方塊",
booleanDisplayDropdown: "下拉清單",
booleanValueStyle: "布林值文字",
booleanValueTrueFalse: "TRUE / FALSE",
booleanValueZeroOne: "0 / 1",
batchAppendPasteNotEditable: "目前結果不可編輯。",
batchAppendPasteInvalidTarget: "請先選取一個空白新增行的行號儲存格,再進行貼上。",
batchAppendPasteTargetNotEmpty: "選取的新增行已包含資料。",
@@ -0,0 +1,34 @@
import { describe, expect, it } from "vitest";
import { booleanCellDisplayText, normalizeBooleanCellValueStyle } from "@/lib/dataGrid/dataGridBooleanColumn";
describe("booleanCellDisplayText", () => {
it("renders the uppercase TRUE/FALSE text by default", () => {
expect(booleanCellDisplayText(true, "true-false")).toBe("TRUE");
expect(booleanCellDisplayText(false, "true-false")).toBe("FALSE");
});
it("renders 1/0 when the zero-one style is selected", () => {
expect(booleanCellDisplayText(true, "zero-one")).toBe("1");
expect(booleanCellDisplayText(false, "zero-one")).toBe("0");
});
it("declines non-boolean values so the caller can keep its own formatting", () => {
expect(booleanCellDisplayText(null, "zero-one")).toBeUndefined();
expect(booleanCellDisplayText("true", "zero-one")).toBeUndefined();
expect(booleanCellDisplayText(1, "zero-one")).toBeUndefined();
expect(booleanCellDisplayText({}, "zero-one")).toBeUndefined();
});
});
describe("normalizeBooleanCellValueStyle", () => {
it("keeps known styles", () => {
expect(normalizeBooleanCellValueStyle("zero-one")).toBe("zero-one");
expect(normalizeBooleanCellValueStyle("true-false")).toBe("true-false");
});
it("falls back to true/false for unknown values", () => {
expect(normalizeBooleanCellValueStyle(undefined)).toBe("true-false");
expect(normalizeBooleanCellValueStyle("TRUE")).toBe("true-false");
expect(normalizeBooleanCellValueStyle(0)).toBe("true-false");
});
});
@@ -3,6 +3,15 @@ import type { DatabaseType } from "@/types/database";
export const BOOLEAN_CHECKBOX_SIZE = 13;
export const BOOLEAN_CELL_EDITOR_VALUES = ["true", "false"];
export const BOOLEAN_CELL_VALUE_STYLES = ["true-false", "zero-one"] as const;
/** How the grid renders a boolean cell's text. */
export type BooleanCellValueStyle = (typeof BOOLEAN_CELL_VALUE_STYLES)[number];
export const DEFAULT_BOOLEAN_CELL_VALUE_STYLE: BooleanCellValueStyle = "true-false";
export function normalizeBooleanCellValueStyle(value: unknown): BooleanCellValueStyle {
return BOOLEAN_CELL_VALUE_STYLES.includes(value as BooleanCellValueStyle) ? (value as BooleanCellValueStyle) : DEFAULT_BOOLEAN_CELL_VALUE_STYLE;
}
const MYSQL_BIT_BOOLEAN_DATABASE_TYPES = new Set<DatabaseType>(["mysql"]);
export function isBooleanColumnType(dataType: string | undefined, databaseType?: DatabaseType): boolean {
@@ -45,6 +54,19 @@ export function booleanCellEditorValue(value: unknown): string {
return "";
}
/**
* Text the grid shows for a boolean cell. `true-false` renders the uppercase
* TRUE/FALSE form the display-options label advertises and that DBeaver/Navicat
* use; `zero-one` matches the 1/0 form other database clients use for the same
* column (#12187). Non-boolean values return `undefined` so callers can fall
* through to the normal formatter.
*/
export function booleanCellDisplayText(value: unknown, style: BooleanCellValueStyle): string | undefined {
if (typeof value !== "boolean") return undefined;
if (style === "zero-one") return value ? "1" : "0";
return value ? "TRUE" : "FALSE";
}
export function parseBooleanCellEditorValue(value: string | null): boolean | null | undefined {
if (value === null) return null;
if (value === "true") return true;
+9
View File
@@ -12,6 +12,7 @@ import * as api from "@/lib/backend/api";
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 BooleanCellValueStyle, normalizeBooleanCellValueStyle } from "@/lib/dataGrid/dataGridBooleanColumn";
import { type DataGridCopyPreference, type DataGridExtractorOptions, DATA_GRID_EXTRACTOR_OPTIONS_MIGRATION_VERSION, DEFAULT_DATA_GRID_EXTRACTOR_OPTIONS, normalizeDataGridCopyPreference, normalizeDataGridExtractorOptions } from "@/lib/dataGrid/dataGridCopyExtractor";
import { DATA_GRID_TEXT_FILTER_PANEL_HEIGHT_DEFAULT, normalizeDataGridTextFilterPanelHeight } from "@/lib/dataGrid/dataGridTextFilterPanel";
import { DATA_GRID_TYPE_COLOR_SCHEME_AUTO_ID, type DataGridTypeColorScheme, normalizeActiveDataGridTypeColorSchemeId, normalizeDataGridTypeColorSchemes } from "@/lib/dataGrid/dataGridTypeColorScheme";
@@ -1104,6 +1105,7 @@ export interface EditorSettings {
dataGridHideEmptyColumns: boolean;
dataGridFreezeFirstColumn: boolean;
dataGridBooleanDisplayMode: "dropdown" | "checkbox";
dataGridBooleanValueStyle: BooleanCellValueStyle;
numericColumnRightAlign: boolean;
tableFontFamily: string;
tableFontSize: number;
@@ -1445,6 +1447,7 @@ export const DEFAULT_EDITOR_SETTINGS: EditorSettings = {
dataGridHideEmptyColumns: false,
dataGridFreezeFirstColumn: false,
dataGridBooleanDisplayMode: "dropdown",
dataGridBooleanValueStyle: "true-false",
numericColumnRightAlign: true,
tableFontFamily: DEFAULT_DATA_GRID_FONT_FAMILY,
tableFontSize: TABLE_FONT_SIZE_DEFAULT,
@@ -1647,6 +1650,10 @@ function normalizeDataGridRowNumberMode(value: unknown): DataGridRowNumberMode {
return DATA_GRID_ROW_NUMBER_MODES.includes(value as DataGridRowNumberMode) ? (value as DataGridRowNumberMode) : DEFAULT_EDITOR_SETTINGS.dataGridRowNumberMode;
}
function normalizeDataGridBooleanValueStyle(value: unknown): BooleanCellValueStyle {
return normalizeBooleanCellValueStyle(value);
}
function normalizeDoubleClickStringSelectionMode(value: unknown): DoubleClickStringSelectionMode {
return DOUBLE_CLICK_STRING_SELECTION_MODES.includes(value as DoubleClickStringSelectionMode) ? (value as DoubleClickStringSelectionMode) : DEFAULT_EDITOR_SETTINGS.doubleClickStringSelectionMode;
}
@@ -2101,6 +2108,7 @@ export function normalizeEditorSettings(settings: Partial<EditorSettings>, exist
dataGridHideEmptyColumns: settings.dataGridHideEmptyColumns === true,
dataGridFreezeFirstColumn: settings.dataGridFreezeFirstColumn === true,
dataGridBooleanDisplayMode: settings.dataGridBooleanDisplayMode === "checkbox" ? "checkbox" : "dropdown",
dataGridBooleanValueStyle: normalizeDataGridBooleanValueStyle(settings.dataGridBooleanValueStyle),
numericColumnRightAlign: typeof settings.numericColumnRightAlign === "boolean" ? settings.numericColumnRightAlign : DEFAULT_EDITOR_SETTINGS.numericColumnRightAlign,
tableFontFamily: normalizeFontFamily(settings.tableFontFamily, DEFAULT_EDITOR_SETTINGS.tableFontFamily),
tableFontSize: normalizeTableFontSize(settings.tableFontSize),
@@ -2998,6 +3006,7 @@ export const useSettingsStore = defineStore("settings", () => {
if (partial.dataGridHideNullColumns !== undefined) editorSettings.value.dataGridHideNullColumns = partial.dataGridHideNullColumns === true;
if (partial.dataGridHideEmptyColumns !== undefined) editorSettings.value.dataGridHideEmptyColumns = partial.dataGridHideEmptyColumns === true;
if (partial.dataGridFreezeFirstColumn !== undefined) editorSettings.value.dataGridFreezeFirstColumn = partial.dataGridFreezeFirstColumn === true;
if (partial.dataGridBooleanValueStyle !== undefined) editorSettings.value.dataGridBooleanValueStyle = normalizeDataGridBooleanValueStyle(partial.dataGridBooleanValueStyle);
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);