mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-19 16:16:45 +00:00
feat(data-grid): show column comments in lookup
This commit is contained in:
@@ -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") }}
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -885,7 +885,7 @@ export default withEnglishFallback({
|
||||
tableInfoSearch: "検索...",
|
||||
tableInfoNoResults: "結果なし",
|
||||
goToColumn: "列へ移動",
|
||||
searchColumn: "列を検索...",
|
||||
searchColumn: "列/コメントを検索...",
|
||||
noColumnsFound: "列が見つかりません",
|
||||
queryError: "クエリエラー",
|
||||
dataUnavailable: "テーブルデータを再読み込みする必要があります。",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -949,7 +949,7 @@ export default withEnglishFallback({
|
||||
tableInfoSearch: "搜索...",
|
||||
tableInfoNoResults: "无匹配结果",
|
||||
goToColumn: "跳转列",
|
||||
searchColumn: "搜索列...",
|
||||
searchColumn: "搜索列/注释...",
|
||||
noColumnsFound: "未找到列",
|
||||
queryError: "查询出错",
|
||||
dataUnavailable: "表数据需要重新加载。",
|
||||
|
||||
@@ -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]));
|
||||
|
||||
|
||||
Reference in New Issue
Block a user