feat(grid): inherit physical column formatting in query results

This commit is contained in:
二丫讲梵 authored and GitHub committed 2026-09-01 11:19:46 +08:00
1 parent 8be503bf40
commit c7cea5a047
19 files changed
+329 -53

No files matched your search

+38 -25
View File
@@ -175,7 +175,7 @@ import {
import { adjacentDataGridDetailIndex, type DataGridDetailNavigationDelta } from "@/lib/dataGrid/dataGridDetailNavigation";
import {
applyColumnFormatter,
buildColumnFormatterKey,
columnFormatterKeys,
defaultIoTDBTimestampFormatter,
DataGridDateTimePatterns,
displayTimeZoneOption,
@@ -1521,7 +1521,7 @@ function openCompactLocalFilter(colIdx: number, mode: LocalFilterMode = "local")
});
}
function compactColumnActionMenuItems(columnName: string, columnIndex: number) {
function compactColumnActionMenuItems(columnIndex: number) {
return createDataGridCompactColumnActionItems({
labels: {
formatter: t("grid.columnFormatter"),
@@ -1530,7 +1530,7 @@ function compactColumnActionMenuItems(columnName: string, columnIndex: number) {
serverFilter: t("grid.databaseValueFilter"),
},
icons: { formatter: Code2, clearFormatter: Eraser, filter: Filter, database: Database },
formatterAvailable: !!formatterKeyForColumn(columnName),
formatterAvailable: !!formatterKeyForColumn(columnIndex),
formatterActive: columnHasFormatter(columnIndex),
serverFilterAvailable: canUseServerColumnFilter.value,
});
@@ -1567,23 +1567,36 @@ function closeLocalFilter() {
resetServerFilterState();
}
function formatterKeyForColumn(column: string): string | null {
if (!props.connectionId || !props.tableMeta) return null;
return buildColumnFormatterKey({
function formatterKeysForColumn(columnIndex: number): string[] {
const resultColumn = props.result.columns[columnIndex];
if (!props.connectionId || !resultColumn) return [];
return columnFormatterKeys({
connectionId: props.connectionId,
database: props.database,
schema: props.tableMeta.schema,
tableName: props.tableMeta.tableName,
column,
schema: props.schema,
databaseType: resolvedDatabaseType.value,
resultColumn,
sourceColumn: props.sourceColumns?.[columnIndex],
displaySource: props.queryDisplaySourceColumns?.[columnIndex],
tableMeta: props.tableMeta,
});
}
function formatterKeyForColumn(columnIndex: number): string | null {
return formatterKeysForColumn(columnIndex)[0] ?? null;
}
function savedColumnFormatterEntry(columnIndex: number): { key: string; formatter: ColumnFormatterConfig } | undefined {
for (const key of formatterKeysForColumn(columnIndex)) {
const formatter = settingsStore.editorSettings.columnFormatters[key];
if (formatter) return { key, formatter };
}
return undefined;
}
function columnFormatter(columnIndex: number): ColumnFormatterConfig | undefined {
const column = props.result.columns[columnIndex];
if (!column) return undefined;
const key = formatterKeyForColumn(column);
const columnType = props.result.column_types?.[columnIndex] ?? tableColumnForGridColumn(columnIndex)?.data_type;
const savedFormatter = key ? settingsStore.editorSettings.columnFormatters[key] : undefined;
const savedFormatter = savedColumnFormatterEntry(columnIndex)?.formatter;
const configured = resolveColumnFormatter(savedFormatter, settingsStore.editorSettings.customColumnFormatters, {
pattern: settingsStore.editorSettings.globalDateTimeDisplayFormat,
columnType,
@@ -1593,10 +1606,7 @@ function columnFormatter(columnIndex: number): ColumnFormatterConfig | undefined
}
function savedColumnFormatter(columnIndex: number): ColumnFormatterConfig | undefined {
const column = props.result.columns[columnIndex];
if (!column) return undefined;
const key = formatterKeyForColumn(column);
return key ? settingsStore.editorSettings.columnFormatters[key] : undefined;
return savedColumnFormatterEntry(columnIndex)?.formatter;
}
function columnHasFormatter(columnIndex: number): boolean {
@@ -1880,8 +1890,7 @@ function handleColumnFormatterOpenChange(value: boolean, columnIndex: number) {
}
async function saveColumnFormatter(columnIndex: number) {
const column = props.result.columns[columnIndex];
const key = column ? formatterKeyForColumn(column) : null;
const key = formatterKeyForColumn(columnIndex);
if (!key) return;
try {
let formatter = currentFormatterDraft();
@@ -1909,10 +1918,12 @@ async function saveColumnFormatter(columnIndex: number) {
}
function clearColumnFormatter(columnIndex: number) {
const column = props.result.columns[columnIndex];
const key = column ? formatterKeyForColumn(column) : null;
if (!key) return;
settingsStore.updateColumnFormatter(key, undefined);
// Clear every candidate key: MySQL-family columns can carry both the shared
// empty-schema spelling and the legacy query-side mirrored-schema spelling,
// and either surface must leave the column unformatted everywhere.
const keys = formatterKeysForColumn(columnIndex);
if (!keys.length) return;
for (const key of keys) settingsStore.updateColumnFormatter(key, undefined);
closeColumnFormatter();
}
@@ -13083,6 +13094,8 @@ function openGridSnapshot() {
:column-unique-index-label="t('grid.columnUniqueIndex')"
:column-regular-index-label="t('grid.columnRegularIndex')"
:column-index-kind="showIndexIndicatorsInHeader ? columnIndexMap.get(columnIndexNameKey(col.name)) : undefined"
:formatter-active="columnHasFormatter(col.actualColIdx)"
:formatter-label="t('grid.columnFormatterActive')"
@pointerdown="startColumnHeaderDrag(col.visibleColIdx, $event)"
@click-capture="onHeaderClickCapture"
@click="onHeaderClick(col.visibleColIdx, $event)"
@@ -13130,7 +13143,7 @@ function openGridSnapshot() {
</LightDropdownMenu>
<LightDropdownMenu
v-if="compactColumnHeaderActions"
:items="compactColumnActionMenuItems(col.name, col.actualColIdx)"
:items="compactColumnActionMenuItems(col.actualColIdx)"
:open="headerActionMenuOpenColumn === col.actualColIdx"
check-position="none"
align="end"
@@ -13163,7 +13176,7 @@ function openGridSnapshot() {
type="button"
class="flex h-4 w-4 shrink-0 items-center justify-center rounded text-muted-foreground hover:bg-gray-200 dark:hover:bg-gray-800 hover:text-foreground"
:class="columnHasFormatter(col.actualColIdx) ? 'text-primary opacity-100' : 'opacity-80'"
:disabled="!formatterKeyForColumn(col.name)"
:disabled="!formatterKeyForColumn(col.actualColIdx)"
:title="t('grid.columnFormatter')"
@click.stop
>
@@ -37,6 +37,9 @@ const props = defineProps<{
columnRegularIndexLabel: string;
/** 该列的索引类型,undefined 时不显示标识 */
columnIndexKind?: ColumnIndexKind;
/** 该列当前展示值应用了格式化规则。 */
formatterActive?: boolean;
formatterLabel?: string;
}>();
function columnIndexText(kind: ColumnIndexKind): string {
@@ -75,7 +78,9 @@ const emit = defineEmits<{
<Hash v-else-if="columnIndexKind && columnIndexKind !== 'none'" class="h-3 w-3 shrink-0" :class="columnIndexColorClass(columnIndexKind)" :title="columnIndexText(columnIndexKind)" />
<LightTooltip :text="name" side="bottom" :side-offset="4" :disabled="tooltipDisabled">
<span data-column-tooltip-trigger class="flex min-w-0 flex-1 flex-col overflow-hidden">
<span class="min-w-0 truncate leading-4">{{ name }}</span>
<span class="flex min-w-0 items-center gap-1 leading-4">
<span class="min-w-0 truncate">{{ name }}</span>
</span>
<span v-if="showTypeLine" data-grid-header-type-line class="h-3 min-w-0 truncate text-[10px] font-normal leading-3" :class="[typeClass, { invisible: !columnType }]" :title="columnType || undefined" :aria-hidden="columnType ? undefined : true">{{ columnType }}</span>
<span v-if="showCommentLine" data-grid-header-comment-line class="h-3 min-w-0 truncate text-[10px] font-normal leading-3 text-muted-foreground" :class="{ invisible: !columnComment }" :title="columnComment || undefined" :aria-hidden="columnComment ? undefined : true">{{
columnComment
@@ -113,6 +118,7 @@ const emit = defineEmits<{
</div>
</template>
</LightTooltip>
<span v-if="formatterActive" data-column-formatter-indicator class="inline-flex h-4 w-4 shrink-0 items-center justify-center rounded-sm bg-violet-600 text-[10px] font-bold leading-none text-white shadow-sm dark:bg-violet-500" :title="formatterLabel" :aria-label="formatterLabel">F</span>
<span data-column-header-actions class="contents"><slot name="actions" /></span>
</span>
<div data-column-resize-handle class="absolute right-0 top-0 bottom-0 w-1.5 cursor-col-resize hover:bg-primary/30" @mousedown.stop="emit('resizeStart', $event)" @click.stop.prevent @dblclick.stop="emit('autoFit')" />
@@ -0,0 +1,31 @@
import { readFileSync } from "node:fs";
import { describe, expect, it } from "vitest";
const dataGridSource = readFileSync(new URL("../DataGrid.vue", import.meta.url), "utf8");
function functionSource(name: string): string {
const start = dataGridSource.indexOf(`function ${name}`);
expect(start).toBeGreaterThanOrEqual(0);
const end = dataGridSource.indexOf("\nfunction ", start + 1);
return dataGridSource.slice(start, end < 0 ? undefined : end);
}
describe("DataGrid column formatter keys", () => {
it("derives every key list from the shared canonical key builder", () => {
const keysSource = functionSource("formatterKeysForColumn");
expect(keysSource).toContain("return columnFormatterKeys({");
expect(keysSource).toContain("databaseType: resolvedDatabaseType.value");
expect(keysSource).toContain("displaySource: props.queryDisplaySourceColumns?.[columnIndex]");
// Saves keep writing the single canonical keys[0] spelling.
const saveSource = functionSource("saveColumnFormatter");
expect(saveSource).toContain("const key = formatterKeyForColumn(columnIndex);");
expect(saveSource).toContain("settingsStore.updateColumnFormatter(key, formatter);");
});
it("clears every candidate key so either surface leaves the column unformatted", () => {
const clearSource = functionSource("clearColumnFormatter");
expect(clearSource).toContain("const keys = formatterKeysForColumn(columnIndex);");
expect(clearSource).toContain("if (!keys.length) return;");
expect(clearSource).toContain("for (const key of keys) settingsStore.updateColumnFormatter(key, undefined);");
});
});
@@ -413,6 +413,28 @@ describe("DataGridPagination", () => {
});
describe("DataGridColumnHeader", () => {
it("uses a compact formatter marker with an accessible explanation", () => {
const mounted = mountComponent(DataGridColumnHeader, {
name: "created_at",
actualColumnIndex: 0,
visibleColumnIndex: 0,
formatterActive: true,
formatterLabel: "Formatted display is active",
copyColumnNameLabel: "copy",
columnNameLabel: "name",
columnTypeLabel: "type",
columnCommentLabel: "comment",
});
const indicator = findOne(mounted.root, (node) => node.props["data-column-formatter-indicator"] === "");
const trigger = findOne(mounted.root, (node) => node.props["data-column-tooltip-trigger"] === "");
expect(hostText(indicator)).toBe("F");
expect(String(indicator.props.class)).toContain("h-4 w-4");
expect(indicator.props.title).toBe("Formatted display is active");
expect(indicator.props["aria-label"]).toBe("Formatted display is active");
expect(indicator.parent).not.toBe(trigger);
});
it("limits the metadata tooltip trigger to the column text block", () => {
const mounted = mountComponent(DataGridColumnHeader, {
name: "status",