feat(grid): inherit physical column formatting in query results
This commit is contained in:
19 files changed
+329
-53
No files matched your search
@@ -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')" />
|
||||
|
||||
+31
@@ -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",
|
||||
|
||||
Reference in new issue
Block a user