feat(data-grid): show column comments in lookup

This commit is contained in:
二丫讲梵
2026-07-08 23:44:33 +08:00
committed by GitHub
parent ddf6b3ed6b
commit 7748c13a1e
12 changed files with 137 additions and 23 deletions
+12 -11
View File
@@ -128,6 +128,7 @@ import { appendColumnValueFilterCondition, buildColumnValueFilterCondition, buil
import { clampSearchSplitWidth } from "@/lib/dataGrid/dataGridSearchSplit";
import { MAX_RESULT_PAGE_SIZE, MIN_RESULT_PAGE_SIZE, normalizeResultPageSize, resultPageSizeMenuOptions } from "@/lib/dataGrid/paginationPageSize";
import { allNullColumnIndexes, filterColumnVisibilityOptions, hiddenColumnIndexesWithAllNullColumns, invertedHiddenColumnIndexes, nextHiddenColumnIndexes, removeAutoHiddenColumnIndexes, visibleColumnIndexesForFilter } from "@/lib/dataGrid/dataGridColumnVisibility";
import { buildDataGridColumnLookupItems, filterDataGridColumnLookupItems } from "@/lib/dataGrid/dataGridColumnLookup";
import { columnOrderKeysForIndexes, isDefaultColumnOrder, moveVisibleColumnIndex, orderedColumnIndexes, uniqueDataGridColumnOrderKeys } from "@/lib/dataGrid/dataGridColumnOrder";
import { dataGridColumnLayoutScopeKey, loadDataGridColumnOrder, removeDataGridColumnOrder, saveDataGridColumnOrder } from "@/lib/dataGrid/dataGridColumnLayoutStorage";
import { parseClipboardTable, summarizeSelection } from "@/lib/dataGrid/gridSelection";
@@ -2497,16 +2498,15 @@ const displayableColumnIndexes = computed(() =>
.map(({ index }) => index),
);
const goToColumnItems = computed(() =>
displayableColumnIndexes.value.map((index) => ({
index,
name: props.result.columns[index] ?? `#${index + 1}`,
sourceName: props.sourceColumns?.[index],
})),
buildDataGridColumnLookupItems({
columns: props.result.columns,
sourceColumns: props.sourceColumns,
displayableIndexes: displayableColumnIndexes.value,
commentByColumn: columnCommentMap.value,
}),
);
const filteredGoToColumns = computed(() => {
const query = goToColumnSearch.value.trim().toLocaleLowerCase();
if (!query) return goToColumnItems.value;
return goToColumnItems.value.filter((column) => column.name.toLocaleLowerCase().includes(query) || column.sourceName?.toLocaleLowerCase().includes(query));
return filterDataGridColumnLookupItems(goToColumnItems.value, goToColumnSearch.value);
});
const orderedDisplayableColumnIndexes = computed(() =>
orderedColumnIndexes({
@@ -2550,7 +2550,7 @@ const allNullColumnCount = computed(() => allNullColumnIndexesForResult.value.le
const canToggleAllNullColumns = computed(() => nullColumnsHidden.value || (allNullColumnCount.value > 0 && displayableColumnCount.value > 1));
function filteredColumnVisibilityOptions(query: string) {
const displayable = new Set(displayableColumnIndexes.value);
return filterColumnVisibilityOptions(props.result.columns, query).filter((option) => displayable.has(option.index));
return filterColumnVisibilityOptions(props.result.columns, query, { sourceColumns: props.sourceColumns, commentByColumn: columnCommentMap.value }).filter((option) => displayable.has(option.index));
}
function isColumnVisible(columnIndex: number): boolean {
return !hiddenColumnIndexes.value.has(columnIndex);
@@ -8862,9 +8862,10 @@ const gridContextMenuItems = computed<ContextMenuItem[]>(() => {
</button>
</div>
<div class="max-h-56 overflow-auto rounded border">
<button v-for="column in filteredGoToColumns" :key="column.index" type="button" class="flex w-full items-center gap-1.5 px-2 py-1.5 text-left text-xs hover:bg-accent hover:text-accent-foreground" @click="scrollToColumn(column.index)">
<button v-for="column in filteredGoToColumns" :key="column.index" type="button" class="grid w-full grid-cols-[minmax(0,1fr)_auto] items-center gap-x-2 gap-y-0.5 px-2 py-1.5 text-left text-xs hover:bg-accent hover:text-accent-foreground" @click="scrollToColumn(column.index)">
<span class="min-w-0 truncate">{{ column.name }}</span>
<span class="ml-auto shrink-0 font-mono text-[10px] text-muted-foreground">#{{ column.index + 1 }}</span>
<span class="shrink-0 font-mono text-[10px] text-muted-foreground">#{{ column.index + 1 }}</span>
<span v-if="column.comment" class="col-span-2 min-w-0 truncate text-[11px] leading-4 text-muted-foreground" :title="column.comment">{{ column.comment }}</span>
</button>
<div v-if="!filteredGoToColumns.length" class="px-2 py-3 text-center text-xs text-muted-foreground">{{ t("grid.noColumnsFound") }}</div>
</div>
@@ -80,7 +80,7 @@ type DataGridHandle = {
visibleColumnCount: number;
displayableColumnCount: number;
hiddenColumnCount: number;
filteredColumnVisibilityOptions: (search: string) => Array<{ index: number; column: string }>;
filteredColumnVisibilityOptions: (search: string) => Array<{ index: number; column: string; comment?: string }>;
isColumnVisible: (columnIndex: number) => boolean;
toggleColumnVisibility: (columnIndex: number) => void;
showAllColumns: () => void;
@@ -1179,7 +1179,10 @@ defineExpose({ focusSearch, refreshData, handleModRTarget, requestQueryEditorExe
<span class="flex h-4 w-4 items-center justify-center rounded border" :class="dataGridRef?.isColumnVisible(option.index) ? 'border-primary bg-primary text-primary-foreground' : 'border-border bg-background text-transparent'">
<Check class="h-3 w-3 stroke-[3]" />
</span>
<span class="truncate font-mono text-xs" :title="option.column">{{ option.column }}</span>
<span class="min-w-0">
<span class="block truncate font-mono text-xs" :title="option.column">{{ option.column }}</span>
<span v-if="option.comment" class="block truncate text-[11px] leading-4 text-muted-foreground" :title="option.comment">{{ option.comment }}</span>
</span>
</button>
<div v-if="columnVisibilityOptions.length === 0" class="px-2 py-6 text-center text-xs text-muted-foreground">
{{ t("grid.noSearchResults") }}
+1 -1
View File
@@ -947,7 +947,7 @@ export default {
tableInfoSearch: "Search...",
tableInfoNoResults: "No results",
goToColumn: "Go to column",
searchColumn: "Search column...",
searchColumn: "Search column/comment...",
noColumnsFound: "No columns found",
queryError: "Query Error",
dataUnavailable: "Table data needs to be reloaded.",
+1 -1
View File
@@ -884,7 +884,7 @@ export default withEnglishFallback({
tableInfoSearch: "Buscar...",
tableInfoNoResults: "Sin resultados",
goToColumn: "Ir a la columna",
searchColumn: "Buscar columna...",
searchColumn: "Buscar columna/comentario...",
noColumnsFound: "No se encontraron columnas",
tableInfo: "Información de tabla",
tableInfoColumns: "Columnas",
+1 -1
View File
@@ -889,7 +889,7 @@ export default withEnglishFallback({
tableInfoSearch: "Cerca...",
tableInfoNoResults: "Nessun risultato",
goToColumn: "Vai alla colonna",
searchColumn: "Cerca colonna...",
searchColumn: "Cerca colonna/commento...",
noColumnsFound: "Nessuna colonna trovata",
queryError: "Errore Query",
dataUnavailable: "I dati della tabella devono essere ricaricati.",
+1 -1
View File
@@ -885,7 +885,7 @@ export default withEnglishFallback({
tableInfoSearch: "検索...",
tableInfoNoResults: "結果なし",
goToColumn: "列へ移動",
searchColumn: "列を検索...",
searchColumn: "列/コメントを検索...",
noColumnsFound: "列が見つかりません",
queryError: "クエリエラー",
dataUnavailable: "テーブルデータを再読み込みする必要があります。",
+1 -1
View File
@@ -890,7 +890,7 @@ export default withEnglishFallback({
tableInfoSearch: "Pesquisar...",
tableInfoNoResults: "Nenhum resultado",
goToColumn: "Ir para a coluna",
searchColumn: "Buscar coluna...",
searchColumn: "Buscar coluna/comentário...",
noColumnsFound: "Nenhuma coluna encontrada",
queryError: "Erro na Consulta",
dataUnavailable: "Os dados da tabela precisam ser recarregados.",
+1 -1
View File
@@ -949,7 +949,7 @@ export default withEnglishFallback({
tableInfoSearch: "搜索...",
tableInfoNoResults: "无匹配结果",
goToColumn: "跳转列",
searchColumn: "搜索列...",
searchColumn: "搜索列/注释...",
noColumnsFound: "未找到列",
queryError: "查询出错",
dataUnavailable: "表数据需要重新加载。",
+1 -1
View File
@@ -890,7 +890,7 @@ export default withEnglishFallback({
tableInfoSearch: "搜尋……",
tableInfoNoResults: "沒有相符結果",
goToColumn: "跳轉欄",
searchColumn: "搜尋欄...",
searchColumn: "搜尋欄/註解...",
noColumnsFound: "未找到欄",
dataUnavailable: "資料表資料需要重新載入。",
dataUnavailableHintPrefix: "按 ",
@@ -0,0 +1,53 @@
export interface DataGridColumnLookupItem {
index: number;
name: string;
sourceName?: string;
comment?: string;
}
export interface DataGridColumnLookupOptions {
columns: readonly string[];
sourceColumns?: readonly (string | undefined)[];
displayableIndexes?: readonly number[];
commentByColumn?: ReadonlyMap<string, string>;
}
function normalizedSearchText(value: string): string {
return value.trim().toLocaleLowerCase();
}
function nonEmptyComment(value: string | undefined): string | undefined {
const trimmed = value?.trim();
return trimmed ? trimmed : undefined;
}
export function dataGridColumnCommentFor(commentByColumn: ReadonlyMap<string, string> | undefined, columnName: string, sourceName?: string): string | undefined {
if (!commentByColumn) return undefined;
const keys = [sourceName, sourceName?.toLocaleLowerCase(), columnName, columnName.toLocaleLowerCase()].filter((value): value is string => !!value);
for (const key of keys) {
const comment = nonEmptyComment(commentByColumn.get(key));
if (comment) return comment;
}
return undefined;
}
export function buildDataGridColumnLookupItems(options: DataGridColumnLookupOptions): DataGridColumnLookupItem[] {
const indexes = options.displayableIndexes ?? options.columns.map((_, index) => index);
return indexes.map((index) => {
const name = options.columns[index] ?? `#${index + 1}`;
const sourceName = options.sourceColumns?.[index];
const comment = dataGridColumnCommentFor(options.commentByColumn, name, sourceName);
return {
index,
name,
...(sourceName ? { sourceName } : {}),
...(comment ? { comment } : {}),
};
});
}
export function filterDataGridColumnLookupItems(items: readonly DataGridColumnLookupItem[], query: string): DataGridColumnLookupItem[] {
const normalizedQuery = normalizedSearchText(query);
if (!normalizedQuery) return [...items];
return items.filter((item) => [item.name, item.sourceName, item.comment].filter((value): value is string => !!value).some((value) => value.toLocaleLowerCase().includes(normalizedQuery)));
}
@@ -1,11 +1,29 @@
import { buildDataGridColumnLookupItems, filterDataGridColumnLookupItems } from "./dataGridColumnLookup";
export interface ColumnVisibilityOption {
column: string;
comment?: string;
index: number;
}
export function filterColumnVisibilityOptions(columns: string[], query: string): ColumnVisibilityOption[] {
const normalizedQuery = query.trim().toLowerCase();
return columns.map((column, index) => ({ column, index })).filter(({ column }) => !normalizedQuery || column.toLowerCase().includes(normalizedQuery));
export interface ColumnVisibilityFilterOptions {
sourceColumns?: readonly (string | undefined)[];
commentByColumn?: ReadonlyMap<string, string>;
}
export function filterColumnVisibilityOptions(columns: readonly string[], query: string, options: ColumnVisibilityFilterOptions = {}): ColumnVisibilityOption[] {
return filterDataGridColumnLookupItems(
buildDataGridColumnLookupItems({
columns,
sourceColumns: options.sourceColumns,
commentByColumn: options.commentByColumn,
}),
query,
).map((item) => ({
column: item.name,
...(item.comment ? { comment: item.comment } : {}),
index: item.index,
}));
}
export function visibleColumnIndexesForFilter(availableIndexes: number[], hiddenIndexes: ReadonlySet<number>): number[] {
@@ -1,5 +1,6 @@
import { strict as assert } from "node:assert";
import { test } from "vitest";
import { buildDataGridColumnLookupItems, filterDataGridColumnLookupItems } from "../../apps/desktop/src/lib/dataGrid/dataGridColumnLookup.ts";
import { allNullColumnIndexes, filterColumnVisibilityOptions, hiddenColumnIndexesWithAllNullColumns, invertedHiddenColumnIndexes, nextHiddenColumnIndexes, removeAutoHiddenColumnIndexes, visibleColumnIndexesForFilter } from "../../apps/desktop/src/lib/dataGrid/dataGridColumnVisibility.ts";
test("filters column visibility options by trimmed case-insensitive text", () => {
@@ -8,6 +9,44 @@ test("filters column visibility options by trimmed case-insensitive text", () =>
assert.deepEqual(options, [{ column: "CustomerName", index: 2 }]);
});
test("filters column visibility options by column comments", () => {
const options = filterColumnVisibilityOptions(["id", "created_at", "customer_name"], "created time", {
commentByColumn: new Map([["created_at", "Created time"]]),
});
assert.deepEqual(options, [{ column: "created_at", comment: "Created time", index: 1 }]);
});
test("builds go-to column lookup items with source column comments first", () => {
const items = buildDataGridColumnLookupItems({
columns: ["display_name"],
sourceColumns: ["name"],
commentByColumn: new Map([
["display_name", "Alias label"],
["name", "Customer name"],
]),
});
assert.deepEqual(items, [{ index: 0, name: "display_name", sourceName: "name", comment: "Customer name" }]);
});
test("filters go-to column lookup items by source column and comment", () => {
const items = buildDataGridColumnLookupItems({
columns: ["label", "created_at"],
sourceColumns: ["user_name", undefined],
commentByColumn: new Map([["user_name", "Customer name"]]),
});
assert.deepEqual(filterDataGridColumnLookupItems(items, "user_name").map((item) => item.index), [0]);
assert.deepEqual(filterDataGridColumnLookupItems(items, "customer").map((item) => item.index), [0]);
});
test("keeps go-to column lookup usable without metadata", () => {
const items = buildDataGridColumnLookupItems({ columns: ["id", "display_name"] });
assert.deepEqual(filterDataGridColumnLookupItems(items, "display"), [{ index: 1, name: "display_name" }]);
});
test("removes hidden indexes from visible columns", () => {
const indexes = visibleColumnIndexesForFilter([0, 1, 2, 3], new Set([1, 3]));