mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-19 16:16:45 +00:00
feat(grid): let users pick how boolean cells read
This commit is contained in:
@@ -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());
|
||||
|
||||
+166
@@ -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);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -3026,6 +3026,9 @@ export default withEnglishFallback({
|
||||
booleanDisplayMode: "ブール値の表示",
|
||||
booleanDisplayCheckbox: "チェック",
|
||||
booleanDisplayDropdown: "ドロップダウン",
|
||||
booleanValueStyle: "ブール値のテキスト",
|
||||
booleanValueTrueFalse: "TRUE / FALSE",
|
||||
booleanValueZeroOne: "0 / 1",
|
||||
batchAppendPasteNotEditable: "現在の結果は編集できません。",
|
||||
batchAppendPasteInvalidTarget: "空の新規行の行番号セルを選択してから貼り付けてください。",
|
||||
batchAppendPasteTargetNotEmpty: "選択した新規行には既にデータが含まれています。",
|
||||
|
||||
@@ -2582,6 +2582,9 @@ export default withEnglishFallback({
|
||||
booleanDisplayMode: "불리언 표시",
|
||||
booleanDisplayCheckbox: "체크",
|
||||
booleanDisplayDropdown: "드롭다운",
|
||||
booleanValueStyle: "불리언 값 텍스트",
|
||||
booleanValueTrueFalse: "TRUE / FALSE",
|
||||
booleanValueZeroOne: "0 / 1",
|
||||
moreValues: "{count}개 값 더, 결과를 좁히려면 계속 입력하세요",
|
||||
filterByValue: "이 값으로 필터",
|
||||
filterExcludeValue: "이 값 제외",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -2699,6 +2699,9 @@ export default withEnglishFallback({
|
||||
booleanDisplayMode: "Отображение логических значений",
|
||||
booleanDisplayCheckbox: "Флажок",
|
||||
booleanDisplayDropdown: "Раскрывающийся список",
|
||||
booleanValueStyle: "Текст логического значения",
|
||||
booleanValueTrueFalse: "TRUE / FALSE",
|
||||
booleanValueZeroOne: "0 / 1",
|
||||
moreValues: "ещё {count} значений, продолжайте вводить для уточнения",
|
||||
filterByValue: "Фильтровать по этому значению",
|
||||
filterExcludeValue: "Исключить это значение",
|
||||
|
||||
@@ -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: "ไม่รวมค่านี้",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -2885,6 +2885,9 @@ export default withEnglishFallback({
|
||||
booleanDisplayMode: "布尔值显示",
|
||||
booleanDisplayCheckbox: "复选框",
|
||||
booleanDisplayDropdown: "下拉列表",
|
||||
booleanValueStyle: "布尔值文本",
|
||||
booleanValueTrueFalse: "TRUE / FALSE",
|
||||
booleanValueZeroOne: "0 / 1",
|
||||
moreValues: "还有 {count} 个值,输入关键词继续缩小范围",
|
||||
filterByValue: "筛选此值",
|
||||
filterExcludeValue: "排除此值",
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user