feat(grid): show column index indicators in data headers

This commit is contained in:
高桥凉介 authored and GitHub committed 2026-08-06 16:29:26 +08:00
1 parent b55d944514
commit f36c280d6d
16 files changed
+272 -5

No files matched your search

@@ -333,6 +333,7 @@ const debugLogCopied = ref(false);
const debugLogDownloaded = ref(false);
const editShowColumnCommentsInHeader = ref(settingsStore.editorSettings.showColumnCommentsInHeader);
const editShowColumnTypesInHeader = ref(settingsStore.editorSettings.showColumnTypesInHeader);
const editShowIndexIndicatorsInHeader = ref(settingsStore.editorSettings.showIndexIndicatorsInHeader);
const editCompactColumnHeaderActions = ref(settingsStore.editorSettings.compactColumnHeaderActions);
const editDataGridQuickEntry = ref(settingsStore.editorSettings.dataGridQuickEntry);
const editDataGridAutoTransposeSingleRow = ref(settingsStore.editorSettings.dataGridAutoTransposeSingleRow);
@@ -485,6 +486,7 @@ function currentEditorSettingsDraft(): EditorSettingsDraft {
tabLayout: editTabLayout.value,
showColumnCommentsInHeader: editShowColumnCommentsInHeader.value,
showColumnTypesInHeader: editShowColumnTypesInHeader.value,
showIndexIndicatorsInHeader: editShowIndexIndicatorsInHeader.value,
compactColumnHeaderActions: editCompactColumnHeaderActions.value,
dataGridQuickEntry: editDataGridQuickEntry.value,
dataGridAutoTransposeSingleRow: editDataGridAutoTransposeSingleRow.value,
@@ -752,6 +754,7 @@ function syncEditorSettingsDraftFromStore() {
editTabLayout.value = settingsStore.editorSettings.tabLayout;
editShowColumnCommentsInHeader.value = settingsStore.editorSettings.showColumnCommentsInHeader;
editShowColumnTypesInHeader.value = settingsStore.editorSettings.showColumnTypesInHeader;
editShowIndexIndicatorsInHeader.value = settingsStore.editorSettings.showIndexIndicatorsInHeader;
editCompactColumnHeaderActions.value = settingsStore.editorSettings.compactColumnHeaderActions;
editDataGridQuickEntry.value = settingsStore.editorSettings.dataGridQuickEntry;
editDataGridAutoTransposeSingleRow.value = settingsStore.editorSettings.dataGridAutoTransposeSingleRow;
@@ -989,6 +992,7 @@ function resetDefaultsForTab(tab: SettingsCategory) {
} else if (tab === "data") {
editShowColumnCommentsInHeader.value = DEFAULT_EDITOR_SETTINGS.showColumnCommentsInHeader;
editShowColumnTypesInHeader.value = DEFAULT_EDITOR_SETTINGS.showColumnTypesInHeader;
editShowIndexIndicatorsInHeader.value = DEFAULT_EDITOR_SETTINGS.showIndexIndicatorsInHeader;
editCompactColumnHeaderActions.value = DEFAULT_EDITOR_SETTINGS.compactColumnHeaderActions;
editDataGridQuickEntry.value = DEFAULT_EDITOR_SETTINGS.dataGridQuickEntry;
editDataGridAutoTransposeSingleRow.value = DEFAULT_EDITOR_SETTINGS.dataGridAutoTransposeSingleRow;
@@ -1052,6 +1056,7 @@ function resetAllDefaults() {
editSidebarTablePageSize.value = DEFAULT_SIDEBAR_TABLE_PAGE_SIZE;
editShowColumnCommentsInHeader.value = DEFAULT_EDITOR_SETTINGS.showColumnCommentsInHeader;
editShowColumnTypesInHeader.value = DEFAULT_EDITOR_SETTINGS.showColumnTypesInHeader;
editShowIndexIndicatorsInHeader.value = DEFAULT_EDITOR_SETTINGS.showIndexIndicatorsInHeader;
editCompactColumnHeaderActions.value = DEFAULT_EDITOR_SETTINGS.compactColumnHeaderActions;
editDataGridQuickEntry.value = DEFAULT_EDITOR_SETTINGS.dataGridQuickEntry;
editDataGridAutoTransposeSingleRow.value = DEFAULT_EDITOR_SETTINGS.dataGridAutoTransposeSingleRow;
@@ -4325,6 +4330,17 @@ onUnmounted(() => {
</div>
<Switch id="show-column-types-in-header" v-model="editShowColumnTypesInHeader" />
</div>
<div class="flex items-center justify-between gap-4 rounded-md border bg-muted/20 px-3 py-2">
<div class="space-y-1">
<Label for="show-index-indicators-in-header">
{{ t("settings.showIndexIndicatorsInHeader") }}
</Label>
<p class="text-xs text-muted-foreground">
{{ t("settings.showIndexIndicatorsInHeaderDescription") }}
</p>
</div>
<Switch id="show-index-indicators-in-header" v-model="editShowIndexIndicatorsInHeader" />
</div>
<div class="flex items-center justify-between gap-4 rounded-md border bg-muted/20 px-3 py-2">
<div class="space-y-1">
<Label for="compact-column-header-actions">
+48 -3
View File
@@ -230,6 +230,7 @@ import { getTableMetadataCapabilities } from "@/lib/table/tableMetadataCapabilit
import { getTableStructureCapabilities } from "@/lib/table/tableStructureCapabilities";
import { filterObjectBrowserTableColumns } from "@/lib/table/objectBrowserTableInfo";
import { reserveDataGridHeaderLine } from "@/lib/dataGrid/dataGridHeaderLayout";
import { buildColumnIndexMap, columnIndexMetadataRequestCurrent, columnIndexNameKey, columnIndexTableIdentity } from "@/lib/dataGrid/dataGridColumnIndexIcon";
import { supportsTableStructureEditing } from "@/lib/database/databaseCapabilities";
import { rememberDataGridConditionHistory } from "@/lib/dataGrid/dataGridConditionHistory";
import { restoreDataGridLocalColumnFilters, serializeDataGridLocalColumnFilters } from "@/lib/dataGrid/dataGridLocalColumnFilterState";
@@ -495,6 +496,15 @@ const dataGridTopbarWidth = ref(0);
const dataGridViewportWidth = ref(0);
const showColumnCommentsInHeader = computed(() => settingsStore.editorSettings.showColumnCommentsInHeader);
const showColumnTypesInHeader = computed(() => settingsStore.editorSettings.showColumnTypesInHeader);
const showIndexIndicatorsInHeader = computed(() => settingsStore.editorSettings.showIndexIndicatorsInHeader !== false);
const primaryKeyColumnNames = computed(() => {
const names = new Set(props.tableMeta?.primaryKeys ?? []);
for (const column of props.tableMeta?.columns ?? []) {
if (column.is_primary_key) names.add(column.name);
}
return [...names];
});
const columnIndexMap = computed(() => buildColumnIndexMap(indexes.value, primaryKeyColumnNames.value));
const compactColumnHeaderActions = computed(() => settingsStore.editorSettings.compactColumnHeaderActions);
const dataGridRenderMode = computed(() => settingsStore.editorSettings.dataGridRenderMode);
const dataGridSearchMode = computed(() => settingsStore.editorSettings.dataGridSearchMode);
@@ -7480,6 +7490,16 @@ const indexes = ref<IndexInfo[]>([]);
const indexesLoaded = ref(false);
const indexesLoading = ref(false);
const indexesError = ref("");
const currentIndexTableIdentity = computed(() =>
columnIndexTableIdentity({
connectionId: props.connectionId,
database: props.database,
catalog: props.tableMeta?.catalog,
schema: props.tableMeta?.schema,
tableName: props.tableMeta?.tableName,
}),
);
let indexesRequestGeneration = 0;
const showDropMongoIndexConfirm = ref(false);
const dropMongoIndexLoading = ref(false);
const pendingDropMongoIndex = ref<IndexInfo | null>(null);
@@ -7673,16 +7693,26 @@ async function fetchDdl() {
}
async function fetchIndexes() {
if (!props.connectionId || !props.tableMeta || indexesLoaded.value || indexesLoading.value) return;
const requestIdentity = currentIndexTableIdentity.value;
if (!props.connectionId || !props.tableMeta || !canShowTableIndexes.value || !requestIdentity || indexesLoaded.value || indexesLoading.value) return;
const connectionId = props.connectionId;
const database = props.database || "";
const schema = props.tableMeta.schema || props.database || "";
const tableName = props.tableMeta.tableName;
const catalog = props.tableMeta.catalog;
const requestGeneration = ++indexesRequestGeneration;
indexesLoading.value = true;
indexesError.value = "";
try {
indexes.value = await api.listIndexes(props.connectionId, props.database || "", props.tableMeta.schema || props.database || "", props.tableMeta.tableName, props.tableMeta.catalog);
const nextIndexes = await api.listIndexes(connectionId, database, schema, tableName, catalog);
if (!columnIndexMetadataRequestCurrent({ requestGeneration, currentGeneration: indexesRequestGeneration, requestIdentity, currentIdentity: currentIndexTableIdentity.value })) return;
indexes.value = nextIndexes;
indexesLoaded.value = true;
} catch (e: any) {
if (!columnIndexMetadataRequestCurrent({ requestGeneration, currentGeneration: indexesRequestGeneration, requestIdentity, currentIdentity: currentIndexTableIdentity.value })) return;
indexesError.value = String(e?.message || e);
} finally {
indexesLoading.value = false;
if (columnIndexMetadataRequestCurrent({ requestGeneration, currentGeneration: indexesRequestGeneration, requestIdentity, currentIdentity: currentIndexTableIdentity.value })) indexesLoading.value = false;
}
}
@@ -7748,7 +7778,9 @@ watch(
ddlContent.value = "";
indexes.value = [];
indexesLoaded.value = false;
indexesLoading.value = false;
indexesError.value = "";
indexesRequestGeneration += 1;
foreignKeys.value = [];
foreignKeysLoaded.value = false;
foreignKeysLoading.value = false;
@@ -7761,6 +7793,14 @@ watch(
},
);
watch(
() => [showIndexIndicatorsInHeader.value, canShowTableIndexes.value, currentIndexTableIdentity.value] as const,
([showIndicators, canShowIndexes, tableIdentity]) => {
if (showIndicators && canShowIndexes && tableIdentity) void fetchIndexes();
},
{ immediate: true },
);
// ---- 外键单元格跳转 ----
const columnForeignKeyMap = computed(() => buildColumnForeignKeyMap(foreignKeys.value));
const foreignKeyNavigationEnabled = computed(() => !!props.connectionId && !!props.tableMeta?.tableName && tableMetadataCapabilities.value.foreignKeys);
@@ -8941,6 +8981,11 @@ const gridContextMenuItems = computed<ContextMenuItem[]>(() => {
:column-name-label="t('grid.columnName')"
:column-type-label="t('grid.columnType')"
:column-comment-label="t('grid.columnComment')"
:column-index-label="t('grid.tableInfoIndexes')"
:column-primary-index-label="t('grid.columnPrimaryIndex')"
:column-unique-index-label="t('grid.columnUniqueIndex')"
:column-regular-index-label="t('grid.columnRegularIndex')"
:column-index-kind="showIndexIndicatorsInHeader ? columnIndexMap.get(columnIndexNameKey(col.name)) : undefined"
@pointerdown="startColumnHeaderDrag(col.visibleColIdx, $event)"
@click-capture="onHeaderClickCapture"
@click="onHeaderClick(col.visibleColIdx, $event)"
@@ -1,9 +1,10 @@
<script setup lang="ts">
import type { CSSProperties, HTMLAttributes } from "vue";
import { Copy } from "@lucide/vue";
import { Copy, KeyRound, Hash } from "@lucide/vue";
import LightTooltip from "@/components/ui/LightTooltip.vue";
import { columnIndexColorClass, type ColumnIndexKind } from "@/lib/dataGrid/dataGridColumnIndexIcon";
defineProps<{
const props = defineProps<{
name: string;
actualColumnIndex: number;
visibleColumnIndex: number;
@@ -26,8 +27,21 @@ defineProps<{
columnNameLabel: string;
columnTypeLabel: string;
columnCommentLabel: string;
columnIndexLabel: string;
columnPrimaryIndexLabel: string;
columnUniqueIndexLabel: string;
columnRegularIndexLabel: string;
/** 该列的索引类型,undefined 时不显示标识 */
columnIndexKind?: ColumnIndexKind;
}>();
function columnIndexText(kind: ColumnIndexKind): string {
if (kind === "primary") return props.columnPrimaryIndexLabel;
if (kind === "unique") return props.columnUniqueIndexLabel;
if (kind === "index") return props.columnRegularIndexLabel;
return "";
}
const emit = defineEmits<{
pointerdown: [event: PointerEvent];
clickCapture: [event: MouseEvent];
@@ -60,6 +74,9 @@ const emit = defineEmits<{
@contextmenu="emit('contextmenu', $event)"
>
<span class="flex min-w-0 items-center gap-1 overflow-hidden">
<!-- 索引标识:显示在列名左侧 -->
<KeyRound v-if="columnIndexKind === 'primary'" class="h-3 w-3 shrink-0" :class="columnIndexColorClass(columnIndexKind)" :title="columnIndexText(columnIndexKind)" />
<Hash v-else-if="columnIndexKind && columnIndexKind !== 'none'" class="h-3 w-3 shrink-0" :class="columnIndexColorClass(columnIndexKind)" :title="columnIndexText(columnIndexKind)" />
<span class="flex min-w-0 flex-1 flex-col overflow-hidden">
<span class="min-w-0 truncate leading-4">{{ name }}</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>
@@ -88,6 +105,14 @@ const emit = defineEmits<{
<span class="text-background/70">{{ columnCommentLabel }}</span>
<span>{{ tooltipColumnComment ?? columnComment }}</span>
</template>
<template v-if="columnIndexKind && columnIndexKind !== 'none'">
<span class="text-background/70">{{ columnIndexLabel }}</span>
<span class="flex items-center gap-1">
<KeyRound v-if="columnIndexKind === 'primary'" class="h-3 w-3" :class="columnIndexColorClass(columnIndexKind)" />
<Hash v-else class="h-3 w-3" :class="columnIndexColorClass(columnIndexKind)" />
<span :class="columnIndexColorClass(columnIndexKind)">{{ columnIndexText(columnIndexKind) }}</span>
</span>
</template>
</div>
</template>
</LightTooltip>
+5
View File
@@ -1526,6 +1526,9 @@ export default {
tableInfo: "Table Info",
tableInfoColumns: "Columns",
tableInfoIndexes: "Indexes",
columnPrimaryIndex: "Primary key index",
columnUniqueIndex: "Unique index",
columnRegularIndex: "Regular index",
tableInfoForeignKeys: "Foreign Keys",
foreignKeyNavigate: "Go to {table}",
tableInfoTriggers: "Triggers",
@@ -5013,6 +5016,8 @@ export default {
showColumnCommentsInHeaderDescription: "Display table column comments directly below grid column names.",
showColumnTypesInHeader: "Show column types under names",
showColumnTypesInHeaderDescription: "Display each column's data type directly below grid column names.",
showIndexIndicatorsInHeader: "Show index indicators beside column names",
showIndexIndicatorsInHeaderDescription: "Use icons to indicate each column's index type in the grid header: primary key (orange), unique index (red), regular index (green).",
compactColumnHeaderActions: "Compact column header tools",
compactColumnHeaderActionsDescription: "Move formatter and local filter tools into a more menu so column names get priority.",
dataGridQuickEntry: "Quick grid entry",
+5
View File
@@ -1414,6 +1414,9 @@ export default withEnglishFallback({
tableInfo: "Información de tabla",
tableInfoColumns: "Columnas",
tableInfoIndexes: "Índices",
columnPrimaryIndex: "Índice de clave primaria",
columnUniqueIndex: "Índice único",
columnRegularIndex: "Índice normal",
tableInfoForeignKeys: "Claves foráneas",
foreignKeyNavigate: "Ir a {table}",
tableInfoTriggers: "Disparadores",
@@ -4790,6 +4793,8 @@ export default withEnglishFallback({
showColumnCommentsInHeaderDescription: "Muestra los comentarios de columnas directamente debajo del nombre de la columna.",
showColumnTypesInHeader: "Mostrar tipos de columna bajo los nombres",
showColumnTypesInHeaderDescription: "Muestra el tipo de dato de cada columna directamente debajo del nombre de la columna.",
showIndexIndicatorsInHeader: "Mostrar indicadores de índice junto a los nombres",
showIndexIndicatorsInHeaderDescription: "Usa iconos para indicar el tipo de índice de cada columna en el encabezado: clave primaria (naranja), índice único (rojo), índice regular (verde).",
compactColumnHeaderActions: "Compactar herramientas del encabezado",
compactColumnHeaderActionsDescription: "Mueve formatear y filtrar a un menú de más acciones para priorizar el nombre de columna.",
dataGridQuickEntry: "Entrada rápida en grilla",
+5
View File
@@ -1403,6 +1403,9 @@ export default withEnglishFallback({
tableInfo: "Informazioni Tabella",
tableInfoColumns: "Colonne",
tableInfoIndexes: "Indici",
columnPrimaryIndex: "Indice chiave primaria",
columnUniqueIndex: "Indice univoco",
columnRegularIndex: "Indice normale",
tableInfoForeignKeys: "Chiavi Esterne",
foreignKeyNavigate: "Vai a {table}",
tableInfoTriggers: "Trigger",
@@ -4790,6 +4793,8 @@ export default withEnglishFallback({
showColumnCommentsInHeaderDescription: "Visualizza i commenti delle colonne della tabella direttamente sotto i nomi delle colonne nella griglia.",
showColumnTypesInHeader: "Mostra i tipi di colonna sotto i nomi",
showColumnTypesInHeaderDescription: "Visualizza il tipo di dato di ciascuna colonna direttamente sotto i nomi delle colonne nella griglia.",
showIndexIndicatorsInHeader: "Mostra gli indicatori di indice accanto ai nomi",
showIndexIndicatorsInHeaderDescription: "Utilizza icone per indicare il tipo di indice di ciascuna colonna nell'intestazione: chiave primaria (arancione), indice univoco (rosso), indice normale (verde).",
compactColumnHeaderActions: "Strumenti intestazione colonna compatti",
compactColumnHeaderActionsDescription: "Sposta gli strumenti del formattatore e dei filtri locali in un menu 'altro' in modo che i nomi delle colonne abbiano la priorità.",
dataGridQuickEntry: "Inserimento rapido griglia",
+5
View File
@@ -1421,6 +1421,9 @@ export default withEnglishFallback({
tableInfo: "テーブル情報",
tableInfoColumns: "列",
tableInfoIndexes: "インデックス",
columnPrimaryIndex: "主キーインデックス",
columnUniqueIndex: "ユニークインデックス",
columnRegularIndex: "通常インデックス",
tableInfoForeignKeys: "外部キー",
foreignKeyNavigate: "{table} へ移動",
tableInfoTriggers: "トリガー",
@@ -4827,6 +4830,8 @@ export default withEnglishFallback({
showColumnCommentsInHeaderDescription: "グリッド列名の直下にテーブル列コメントを表示します。",
showColumnTypesInHeader: "列名の下にデータ型を表示",
showColumnTypesInHeaderDescription: "グリッド列名の直下に各列のデータ型を表示します。",
showIndexIndicatorsInHeader: "列名の横にインデックス指標を表示",
showIndexIndicatorsInHeaderDescription: "グリッド列ヘッダーにインデックスの種類をアイコンで表示します:主キー(オレンジ)、一意インデックス(赤)、通常インデックス(緑)。",
compactColumnHeaderActions: "列ヘッダーツールをコンパクトに",
compactColumnHeaderActionsDescription: "フォーマッターとローカルフィルターツールをもっと見るメニューに移動し、列名を優先表示します。",
dataGridQuickEntry: "グリッド高速入力",
+5
View File
@@ -1421,6 +1421,9 @@ export default withEnglishFallback({
tableInfo: "테이블 정보",
tableInfoColumns: "컬럼",
tableInfoIndexes: "인덱스",
columnPrimaryIndex: "기본 키 인덱스",
columnUniqueIndex: "고유 인덱스",
columnRegularIndex: "일반 인덱스",
tableInfoForeignKeys: "외래 키",
foreignKeyNavigate: "{table}(으)로 이동",
tableInfoTriggers: "트리거",
@@ -4555,6 +4558,8 @@ export default withEnglishFallback({
showColumnCommentsInHeaderDescription: "그리드 컬럼 이름 바로 아래에 테이블 컬럼 주석을 표시합니다.",
showColumnTypesInHeader: "이름 아래에 컬럼 타입 표시",
showColumnTypesInHeaderDescription: "그리드 컬럼 이름 바로 아래에 각 컬럼의 데이터 타입을 표시합니다.",
showIndexIndicatorsInHeader: "컬럼 이름 옆에 인덱스 표시",
showIndexIndicatorsInHeaderDescription: "그리드 컬럼 헤더에 인덱스 유형을 아이콘으로 표시합니다: 기본키(주황색), 고유 인덱스(빨간색), 일반 인덱스(녹색).",
compactColumnHeaderActions: "컴팩트한 컬럼 머리글 도구",
compactColumnHeaderActionsDescription: "포매터와 로컬 필터 도구를 더보기 메뉴로 이동하여 컬럼 이름에 우선순위를 둡니다.",
dataGridQuickEntry: "빠른 그리드 입력",
+5
View File
@@ -1405,6 +1405,9 @@ export default withEnglishFallback({
tableInfo: "Informações da Tabela",
tableInfoColumns: "Colunas",
tableInfoIndexes: "Índices",
columnPrimaryIndex: "Índice de chave primária",
columnUniqueIndex: "Índice exclusivo",
columnRegularIndex: "Índice comum",
tableInfoForeignKeys: "Chaves Estrangeiras",
foreignKeyNavigate: "Ir para {table}",
tableInfoTriggers: "Gatilhos",
@@ -4792,6 +4795,8 @@ export default withEnglishFallback({
showColumnCommentsInHeaderDescription: "Exibir comentários de colunas da tabela diretamente abaixo dos nomes das colunas da grade.",
showColumnTypesInHeader: "Mostrar tipos de coluna sob os nomes",
showColumnTypesInHeaderDescription: "Exibir o tipo de dado de cada coluna diretamente abaixo dos nomes das colunas da grade.",
showIndexIndicatorsInHeader: "Mostrar indicadores de índice ao lado dos nomes",
showIndexIndicatorsInHeaderDescription: "Use ícones para indicar o tipo de índice de cada coluna no cabeçalho: chave primária (laranja), índice exclusivo (vermelho), índice regular (verde).",
compactColumnHeaderActions: "Ferramentas compactas no cabeçalho da coluna",
compactColumnHeaderActionsDescription: "Mover as ferramentas de formatação e filtro local para um menu de mais opções para que os nomes das colunas tenham prioridade.",
dataGridQuickEntry: "Entrada rápida na grade",
+5
View File
@@ -1527,6 +1527,9 @@ export default withEnglishFallback({
tableInfo: "表属性",
tableInfoColumns: "字段",
tableInfoIndexes: "索引",
columnPrimaryIndex: "主键索引",
columnUniqueIndex: "唯一索引",
columnRegularIndex: "普通索引",
tableInfoForeignKeys: "外键",
foreignKeyNavigate: "跳转到 {table}",
tableInfoTriggers: "触发器",
@@ -5011,6 +5014,8 @@ export default withEnglishFallback({
showColumnCommentsInHeaderDescription: "把表字段注释直接显示在结果表头字段名下方。",
showColumnTypesInHeader: "在字段名下方显示数据类型",
showColumnTypesInHeaderDescription: "把每个字段的数据类型直接显示在结果表头字段名下方。",
showIndexIndicatorsInHeader: "在字段名旁显示索引标识",
showIndexIndicatorsInHeaderDescription: "在结果表头字段名旁用图标标识字段的索引类型:主键(橙色)、唯一索引(红色)、普通索引(绿色)。",
compactColumnHeaderActions: "收起字段表头工具",
compactColumnHeaderActionsDescription: "将格式化、本地筛选收进更多菜单,优先显示字段名称。",
dataGridQuickEntry: "表格快捷录入",
+5
View File
@@ -1405,6 +1405,9 @@ export default withEnglishFallback({
tableInfo: "資料表屬性",
tableInfoColumns: "欄位",
tableInfoIndexes: "索引",
columnPrimaryIndex: "主鍵索引",
columnUniqueIndex: "唯一索引",
columnRegularIndex: "一般索引",
tableInfoForeignKeys: "外鍵",
foreignKeyNavigate: "跳轉到 {table}",
tableInfoTriggers: "觸發器",
@@ -4253,6 +4256,8 @@ export default withEnglishFallback({
showColumnCommentsInHeaderDescription: "直接在資料表格欄位名稱下方顯示資料表欄位註解。",
showColumnTypesInHeader: "在欄位名稱下方顯示資料類型",
showColumnTypesInHeaderDescription: "直接在資料表格欄位名稱下方顯示每個欄位的資料類型。",
showIndexIndicatorsInHeader: "在欄位名稱旁顯示索引標識",
showIndexIndicatorsInHeaderDescription: "在資料表格欄位名稱旁用圖示標識欄位的索引類型:主鍵(橙色)、唯一索引(紅色)、普通索引(綠色)。",
compactColumnHeaderActions: "收起欄位表頭工具",
compactColumnHeaderActionsDescription: "將格式設定和本機篩選工具移到更多選單,讓欄位名稱優先顯示。",
dataGridQuickEntry: "表格快速輸入",
@@ -0,0 +1,70 @@
import { describe, expect, it } from "vitest";
import { buildColumnIndexMap, columnIndexMetadataRequestCurrent, columnIndexNameKey, columnIndexTableIdentity } from "@/lib/dataGrid/dataGridColumnIndexIcon";
import type { IndexInfo } from "@/types/database";
function indexInfo(overrides: Partial<IndexInfo>): IndexInfo {
return {
name: "idx_users_email",
columns: ["email"],
is_unique: false,
is_primary: false,
...overrides,
};
}
describe("buildColumnIndexMap", () => {
it("uses column metadata for custom-named and composite primary keys", () => {
const map = buildColumnIndexMap([indexInfo({ name: "PK_USERS", columns: ["User_ID", "Tenant_ID"], is_unique: true }), indexInfo({ columns: ["user_id"] })], ["user_id", "tenant_id"]);
expect(map.get(columnIndexNameKey("USER_ID"))).toBe("primary");
expect(map.get(columnIndexNameKey("tenant_id"))).toBe("primary");
});
it("keeps the highest-priority kind with case-insensitive names", () => {
const map = buildColumnIndexMap([indexInfo({ columns: ["Email"] }), indexInfo({ name: "uq_email", columns: ["email"], is_unique: true })]);
expect(map.get(columnIndexNameKey("EMAIL"))).toBe("unique");
});
});
describe("column index metadata request guard", () => {
it("includes the full table identity and rejects stale responses", () => {
const users = columnIndexTableIdentity({
connectionId: "c1",
database: "db",
schema: "public",
tableName: "users",
})!;
const orders = columnIndexTableIdentity({
connectionId: "c1",
database: "db",
schema: "public",
tableName: "orders",
})!;
expect(users).not.toBe(orders);
expect(
columnIndexMetadataRequestCurrent({
requestGeneration: 2,
currentGeneration: 2,
requestIdentity: users,
currentIdentity: users,
}),
).toBe(true);
expect(
columnIndexMetadataRequestCurrent({
requestGeneration: 2,
currentGeneration: 3,
requestIdentity: users,
currentIdentity: users,
}),
).toBe(false);
expect(
columnIndexMetadataRequestCurrent({
requestGeneration: 2,
currentGeneration: 2,
requestIdentity: users,
currentIdentity: orders,
}),
).toBe(false);
});
});
@@ -0,0 +1,65 @@
import type { IndexInfo } from "@/types/database";
/** 列的索引类型分类 */
export type ColumnIndexKind = "primary" | "unique" | "index" | "none";
export function columnIndexNameKey(name: string): string {
return name.trim().toLowerCase();
}
/** 从主键和索引列表构建规范化列名 → 最高优先级索引类型的映射 */
export function buildColumnIndexMap(indexes: IndexInfo[], primaryKeyColumns: readonly string[] = []): Map<string, ColumnIndexKind> {
const map = new Map<string, ColumnIndexKind>();
for (const column of primaryKeyColumns) {
const key = columnIndexNameKey(column);
if (key) map.set(key, "primary");
}
for (const idx of indexes) {
const kind: ColumnIndexKind = idx.is_primary ? "primary" : idx.is_unique ? "unique" : "index";
for (const col of idx.columns) {
const key = columnIndexNameKey(col);
if (!key) continue;
const existing = map.get(key);
if (!existing || columnIndexPriority(kind) > columnIndexPriority(existing)) {
map.set(key, kind);
}
}
}
return map;
}
export function columnIndexTableIdentity(options: { connectionId?: string; database?: string; catalog?: string; schema?: string; tableName?: string }): string | undefined {
if (!options.connectionId || !options.tableName) return undefined;
return JSON.stringify([options.connectionId, options.database ?? "", options.catalog ?? "", options.schema ?? "", options.tableName]);
}
export function columnIndexMetadataRequestCurrent(options: { requestGeneration: number; currentGeneration: number; requestIdentity: string; currentIdentity?: string }): boolean {
return options.requestGeneration === options.currentGeneration && options.requestIdentity === options.currentIdentity;
}
function columnIndexPriority(kind: ColumnIndexKind): number {
switch (kind) {
case "primary":
return 3;
case "unique":
return 2;
case "index":
return 1;
default:
return 0;
}
}
/** 索引类型对应的颜色类名(橙色=主键,红色=唯一,绿色=普通) */
export function columnIndexColorClass(kind: ColumnIndexKind): string {
switch (kind) {
case "primary":
return "text-orange-500";
case "unique":
return "text-red-500";
case "index":
return "text-green-500";
default:
return "";
}
}
@@ -28,6 +28,7 @@ export const EDITOR_SETTINGS_DRAFT_KEYS = [
"tabLayout",
"showColumnCommentsInHeader",
"showColumnTypesInHeader",
"showIndexIndicatorsInHeader",
"compactColumnHeaderActions",
"dataGridQuickEntry",
"dataGridAutoTransposeSingleRow",
@@ -168,6 +168,7 @@ export const SETTINGS_SEARCH_DEFINITIONS: readonly SettingsSearchDefinition[] =
{ id: "data-page-size", category: "data", titleKey: "settings.tableOpenPageSize", descriptionKey: "settings.tableOpenPageSizeDescription", targetId: "data" },
{ id: "appearance-header-comments", category: "appearance", titleKey: "settings.showColumnCommentsInHeader", descriptionKey: "settings.showColumnCommentsInHeaderDescription", targetId: "appearance" },
{ id: "appearance-header-types", category: "appearance", titleKey: "settings.showColumnTypesInHeader", descriptionKey: "settings.showColumnTypesInHeaderDescription", targetId: "appearance" },
{ id: "appearance-index-indicators", category: "appearance", titleKey: "settings.showIndexIndicatorsInHeader", descriptionKey: "settings.showIndexIndicatorsInHeaderDescription", targetId: "appearance" },
{ id: "appearance-compact-header-actions", category: "appearance", titleKey: "settings.compactColumnHeaderActions", descriptionKey: "settings.compactColumnHeaderActionsDescription", targetId: "appearance" },
{ id: "appearance-auto-total", category: "appearance", titleKey: "settings.autoCalculateTotalRows", descriptionKey: "settings.autoCalculateTotalRowsDescription", targetId: "appearance" },
{ id: "appearance-infinite-scroll", category: "appearance", titleKey: "settings.infiniteScroll", descriptionKey: "settings.infiniteScrollDescription", targetId: "appearance" },
+4
View File
@@ -469,6 +469,7 @@ export interface EditorSettings {
mongoViewMode: "document" | "table";
showColumnCommentsInHeader: boolean;
showColumnTypesInHeader: boolean;
showIndexIndicatorsInHeader: boolean;
compactColumnHeaderActions: boolean;
columnWidthDensity: ColumnWidthDensity;
dataGridQuickEntry: boolean;
@@ -647,6 +648,7 @@ export const DEFAULT_EDITOR_SETTINGS: EditorSettings = {
mongoViewMode: "document",
showColumnCommentsInHeader: true,
showColumnTypesInHeader: true,
showIndexIndicatorsInHeader: true,
compactColumnHeaderActions: true,
columnWidthDensity: "standard",
dataGridQuickEntry: false,
@@ -956,6 +958,7 @@ export function normalizeEditorSettings(settings: Partial<EditorSettings>, exist
mongoViewMode: settings.mongoViewMode === "table" ? "table" : DEFAULT_EDITOR_SETTINGS.mongoViewMode,
showColumnCommentsInHeader: settings.showColumnCommentsInHeader ?? DEFAULT_EDITOR_SETTINGS.showColumnCommentsInHeader,
showColumnTypesInHeader: settings.showColumnTypesInHeader ?? DEFAULT_EDITOR_SETTINGS.showColumnTypesInHeader,
showIndexIndicatorsInHeader: settings.showIndexIndicatorsInHeader ?? DEFAULT_EDITOR_SETTINGS.showIndexIndicatorsInHeader,
compactColumnHeaderActions: settings.compactColumnHeaderActions ?? DEFAULT_EDITOR_SETTINGS.compactColumnHeaderActions,
columnWidthDensity: normalizeColumnWidthDensity(settings.columnWidthDensity),
dataGridQuickEntry: settings.dataGridQuickEntry ?? DEFAULT_EDITOR_SETTINGS.dataGridQuickEntry,
@@ -1449,6 +1452,7 @@ export const useSettingsStore = defineStore("settings", () => {
if (partial.mongoViewMode !== undefined) editorSettings.value.mongoViewMode = partial.mongoViewMode;
if (partial.showColumnCommentsInHeader !== undefined) editorSettings.value.showColumnCommentsInHeader = partial.showColumnCommentsInHeader;
if (partial.showColumnTypesInHeader !== undefined) editorSettings.value.showColumnTypesInHeader = partial.showColumnTypesInHeader;
if (partial.showIndexIndicatorsInHeader !== undefined) editorSettings.value.showIndexIndicatorsInHeader = partial.showIndexIndicatorsInHeader;
if (partial.compactColumnHeaderActions !== undefined) editorSettings.value.compactColumnHeaderActions = partial.compactColumnHeaderActions;
if (partial.columnWidthDensity !== undefined) editorSettings.value.columnWidthDensity = normalizeColumnWidthDensity(partial.columnWidthDensity);
if (partial.dataGridQuickEntry !== undefined) editorSettings.value.dataGridQuickEntry = partial.dataGridQuickEntry;