From ce113aabf14d586b0d95034a2487d13281fa2c55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=88=E7=A7=8B?= <49514152+yiqiui@users.noreply.github.com> Date: Fri, 16 Oct 2026 15:07:06 +0800 Subject: [PATCH] feat(grid): let users pick how boolean cells read --- apps/desktop/src/components/grid/DataGrid.vue | 11 +- .../DataGridBooleanValueStyle.spec.ts | 166 ++++++++++++++++++ .../src/components/layout/ContentArea.vue | 55 ++++++ .../gridBooleanValueStyleParity.spec.ts | 44 +++++ apps/desktop/src/i18n/locales/az.ts | 3 + apps/desktop/src/i18n/locales/en.ts | 3 + apps/desktop/src/i18n/locales/es.ts | 3 + apps/desktop/src/i18n/locales/id.ts | 3 + apps/desktop/src/i18n/locales/it.ts | 3 + apps/desktop/src/i18n/locales/ja.ts | 3 + apps/desktop/src/i18n/locales/ko.ts | 3 + apps/desktop/src/i18n/locales/pt-BR.ts | 3 + apps/desktop/src/i18n/locales/ru.ts | 3 + apps/desktop/src/i18n/locales/th.ts | 3 + apps/desktop/src/i18n/locales/tr.ts | 3 + apps/desktop/src/i18n/locales/zh-CN.ts | 3 + apps/desktop/src/i18n/locales/zh-TW.ts | 3 + .../__tests__/dataGridBooleanColumn.spec.ts | 34 ++++ .../src/lib/dataGrid/dataGridBooleanColumn.ts | 22 +++ apps/desktop/src/stores/settingsStore.ts | 9 + 20 files changed, 379 insertions(+), 1 deletion(-) create mode 100644 apps/desktop/src/components/grid/__tests__/DataGridBooleanValueStyle.spec.ts create mode 100644 apps/desktop/src/i18n/__tests__/gridBooleanValueStyleParity.spec.ts create mode 100644 apps/desktop/src/lib/dataGrid/__tests__/dataGridBooleanColumn.spec.ts diff --git a/apps/desktop/src/components/grid/DataGrid.vue b/apps/desktop/src/components/grid/DataGrid.vue index 2f7d67d0eb..78e24b0e99 100644 --- a/apps/desktop/src/components/grid/DataGrid.vue +++ b/apps/desktop/src/components/grid/DataGrid.vue @@ -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()); diff --git a/apps/desktop/src/components/grid/__tests__/DataGridBooleanValueStyle.spec.ts b/apps/desktop/src/components/grid/__tests__/DataGridBooleanValueStyle.spec.ts new file mode 100644 index 0000000000..4c41fcd8f1 --- /dev/null +++ b/apps/desktop/src/components/grid/__tests__/DataGridBooleanValueStyle.spec.ts @@ -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(); + 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, + 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(); + 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({ + 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('[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"]); + }); +}); diff --git a/apps/desktop/src/components/layout/ContentArea.vue b/apps/desktop/src/components/layout/ContentArea.vue index 4ac52267a2..d2c61da232 100644 --- a/apps/desktop/src/components/layout/ContentArea.vue +++ b/apps/desktop/src/components/layout/ContentArea.vue @@ -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({ +
+
+ + {{ t("grid.booleanValueStyle") }} +
+
+ + +
+
@@ -3164,6 +3193,32 @@ defineExpose({
+
+
+ + {{ t("grid.booleanValueStyle") }} +
+
+ + +
+
diff --git a/apps/desktop/src/i18n/__tests__/gridBooleanValueStyleParity.spec.ts b/apps/desktop/src/i18n/__tests__/gridBooleanValueStyleParity.spec.ts new file mode 100644 index 0000000000..dfd872c694 --- /dev/null +++ b/apps/desktop/src/i18n/__tests__/gridBooleanValueStyleParity.spec.ts @@ -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]> = [ + ["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 }).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); + } + }); +}); diff --git a/apps/desktop/src/i18n/locales/az.ts b/apps/desktop/src/i18n/locales/az.ts index 5ce381a6e8..7a000c47db 100644 --- a/apps/desktop/src/i18n/locales/az.ts +++ b/apps/desktop/src/i18n/locales/az.ts @@ -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", diff --git a/apps/desktop/src/i18n/locales/en.ts b/apps/desktop/src/i18n/locales/en.ts index 2afcc0a822..54a2220af6 100644 --- a/apps/desktop/src/i18n/locales/en.ts +++ b/apps/desktop/src/i18n/locales/en.ts @@ -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", diff --git a/apps/desktop/src/i18n/locales/es.ts b/apps/desktop/src/i18n/locales/es.ts index 6c75ecc0b9..138ab135d4 100644 --- a/apps/desktop/src/i18n/locales/es.ts +++ b/apps/desktop/src/i18n/locales/es.ts @@ -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.", diff --git a/apps/desktop/src/i18n/locales/id.ts b/apps/desktop/src/i18n/locales/id.ts index 3dfd37e035..784426f441 100644 --- a/apps/desktop/src/i18n/locales/id.ts +++ b/apps/desktop/src/i18n/locales/id.ts @@ -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", diff --git a/apps/desktop/src/i18n/locales/it.ts b/apps/desktop/src/i18n/locales/it.ts index c65ec99ed1..e106eef8f1 100644 --- a/apps/desktop/src/i18n/locales/it.ts +++ b/apps/desktop/src/i18n/locales/it.ts @@ -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.", diff --git a/apps/desktop/src/i18n/locales/ja.ts b/apps/desktop/src/i18n/locales/ja.ts index a145b2a6d8..220baac0bc 100644 --- a/apps/desktop/src/i18n/locales/ja.ts +++ b/apps/desktop/src/i18n/locales/ja.ts @@ -3026,6 +3026,9 @@ export default withEnglishFallback({ booleanDisplayMode: "ブール値の表示", booleanDisplayCheckbox: "チェック", booleanDisplayDropdown: "ドロップダウン", + booleanValueStyle: "ブール値のテキスト", + booleanValueTrueFalse: "TRUE / FALSE", + booleanValueZeroOne: "0 / 1", batchAppendPasteNotEditable: "現在の結果は編集できません。", batchAppendPasteInvalidTarget: "空の新規行の行番号セルを選択してから貼り付けてください。", batchAppendPasteTargetNotEmpty: "選択した新規行には既にデータが含まれています。", diff --git a/apps/desktop/src/i18n/locales/ko.ts b/apps/desktop/src/i18n/locales/ko.ts index 0ec983e376..df564983fb 100644 --- a/apps/desktop/src/i18n/locales/ko.ts +++ b/apps/desktop/src/i18n/locales/ko.ts @@ -2582,6 +2582,9 @@ export default withEnglishFallback({ booleanDisplayMode: "불리언 표시", booleanDisplayCheckbox: "체크", booleanDisplayDropdown: "드롭다운", + booleanValueStyle: "불리언 값 텍스트", + booleanValueTrueFalse: "TRUE / FALSE", + booleanValueZeroOne: "0 / 1", moreValues: "{count}개 값 더, 결과를 좁히려면 계속 입력하세요", filterByValue: "이 값으로 필터", filterExcludeValue: "이 값 제외", diff --git a/apps/desktop/src/i18n/locales/pt-BR.ts b/apps/desktop/src/i18n/locales/pt-BR.ts index 25bbac20f9..97a186bec9 100644 --- a/apps/desktop/src/i18n/locales/pt-BR.ts +++ b/apps/desktop/src/i18n/locales/pt-BR.ts @@ -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.", diff --git a/apps/desktop/src/i18n/locales/ru.ts b/apps/desktop/src/i18n/locales/ru.ts index e933f0db35..f210a0f368 100644 --- a/apps/desktop/src/i18n/locales/ru.ts +++ b/apps/desktop/src/i18n/locales/ru.ts @@ -2699,6 +2699,9 @@ export default withEnglishFallback({ booleanDisplayMode: "Отображение логических значений", booleanDisplayCheckbox: "Флажок", booleanDisplayDropdown: "Раскрывающийся список", + booleanValueStyle: "Текст логического значения", + booleanValueTrueFalse: "TRUE / FALSE", + booleanValueZeroOne: "0 / 1", moreValues: "ещё {count} значений, продолжайте вводить для уточнения", filterByValue: "Фильтровать по этому значению", filterExcludeValue: "Исключить это значение", diff --git a/apps/desktop/src/i18n/locales/th.ts b/apps/desktop/src/i18n/locales/th.ts index cf3182c0ff..b6aca0fe34 100644 --- a/apps/desktop/src/i18n/locales/th.ts +++ b/apps/desktop/src/i18n/locales/th.ts @@ -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: "ไม่รวมค่านี้", diff --git a/apps/desktop/src/i18n/locales/tr.ts b/apps/desktop/src/i18n/locales/tr.ts index 5e681fc24a..7605f10089 100644 --- a/apps/desktop/src/i18n/locales/tr.ts +++ b/apps/desktop/src/i18n/locales/tr.ts @@ -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", diff --git a/apps/desktop/src/i18n/locales/zh-CN.ts b/apps/desktop/src/i18n/locales/zh-CN.ts index 64c8fc7610..53c575cf1b 100644 --- a/apps/desktop/src/i18n/locales/zh-CN.ts +++ b/apps/desktop/src/i18n/locales/zh-CN.ts @@ -2885,6 +2885,9 @@ export default withEnglishFallback({ booleanDisplayMode: "布尔值显示", booleanDisplayCheckbox: "复选框", booleanDisplayDropdown: "下拉列表", + booleanValueStyle: "布尔值文本", + booleanValueTrueFalse: "TRUE / FALSE", + booleanValueZeroOne: "0 / 1", moreValues: "还有 {count} 个值,输入关键词继续缩小范围", filterByValue: "筛选此值", filterExcludeValue: "排除此值", diff --git a/apps/desktop/src/i18n/locales/zh-TW.ts b/apps/desktop/src/i18n/locales/zh-TW.ts index a98f0a16e5..ebc870eab5 100644 --- a/apps/desktop/src/i18n/locales/zh-TW.ts +++ b/apps/desktop/src/i18n/locales/zh-TW.ts @@ -3012,6 +3012,9 @@ export default withEnglishFallback({ booleanDisplayMode: "布林值顯示", booleanDisplayCheckbox: "核取方塊", booleanDisplayDropdown: "下拉清單", + booleanValueStyle: "布林值文字", + booleanValueTrueFalse: "TRUE / FALSE", + booleanValueZeroOne: "0 / 1", batchAppendPasteNotEditable: "目前結果不可編輯。", batchAppendPasteInvalidTarget: "請先選取一個空白新增行的行號儲存格,再進行貼上。", batchAppendPasteTargetNotEmpty: "選取的新增行已包含資料。", diff --git a/apps/desktop/src/lib/dataGrid/__tests__/dataGridBooleanColumn.spec.ts b/apps/desktop/src/lib/dataGrid/__tests__/dataGridBooleanColumn.spec.ts new file mode 100644 index 0000000000..9bab63236e --- /dev/null +++ b/apps/desktop/src/lib/dataGrid/__tests__/dataGridBooleanColumn.spec.ts @@ -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"); + }); +}); diff --git a/apps/desktop/src/lib/dataGrid/dataGridBooleanColumn.ts b/apps/desktop/src/lib/dataGrid/dataGridBooleanColumn.ts index 7ba42f3104..39c36e3e57 100644 --- a/apps/desktop/src/lib/dataGrid/dataGridBooleanColumn.ts +++ b/apps/desktop/src/lib/dataGrid/dataGridBooleanColumn.ts @@ -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(["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; diff --git a/apps/desktop/src/stores/settingsStore.ts b/apps/desktop/src/stores/settingsStore.ts index 5dc99d6181..fe9170320f 100644 --- a/apps/desktop/src/stores/settingsStore.ts +++ b/apps/desktop/src/stores/settingsStore.ts @@ -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, 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);