feat(data-grid): add configurable default sorting

This commit is contained in:
二丫讲梵 2026-07-29 04:46:46 +08:00 committed by GitHub
parent fdaef62518
commit 2e5b439c8a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
24 changed files with 720 additions and 17 deletions

View File

@ -38,6 +38,9 @@ import {
type InterfaceLayout,
type DisconnectTabHandlingMode,
type OpenTabsRestoreMode,
type OpenTableDefaultSortMode,
type DataGridQuickSortMode,
type DataGridQuickSortDirection,
type SidebarObjectInfoMode,
type SqlSemanticDiagnosticsMode,
type UpdateDownloadSource,
@ -307,6 +310,10 @@ const debugLogDownloaded = ref(false);
const editShowColumnCommentsInHeader = ref(settingsStore.editorSettings.showColumnCommentsInHeader);
const editShowColumnTypesInHeader = ref(settingsStore.editorSettings.showColumnTypesInHeader);
const editCompactColumnHeaderActions = ref(settingsStore.editorSettings.compactColumnHeaderActions);
const editOpenTableDefaultSortMode = ref<OpenTableDefaultSortMode>(settingsStore.editorSettings.openTableDefaultSortMode);
const editDataGridQuickSortEnabled = ref(settingsStore.editorSettings.dataGridQuickSortEnabled);
const editDataGridQuickSortMode = ref<DataGridQuickSortMode>(settingsStore.editorSettings.dataGridQuickSortMode);
const editDataGridQuickSortDirection = ref<DataGridQuickSortDirection>(settingsStore.editorSettings.dataGridQuickSortDirection);
const editDataGridQuickEntry = ref(settingsStore.editorSettings.dataGridQuickEntry);
const editDataGridAutoTransposeSingleRow = ref(settingsStore.editorSettings.dataGridAutoTransposeSingleRow);
const editTableOpenPageSize = ref(settingsStore.editorSettings.tableOpenPageSize);
@ -448,6 +455,10 @@ function currentEditorSettingsDraft(): EditorSettingsDraft {
showColumnCommentsInHeader: editShowColumnCommentsInHeader.value,
showColumnTypesInHeader: editShowColumnTypesInHeader.value,
compactColumnHeaderActions: editCompactColumnHeaderActions.value,
openTableDefaultSortMode: editOpenTableDefaultSortMode.value,
dataGridQuickSortEnabled: editDataGridQuickSortEnabled.value,
dataGridQuickSortMode: editDataGridQuickSortMode.value,
dataGridQuickSortDirection: editDataGridQuickSortDirection.value,
dataGridQuickEntry: editDataGridQuickEntry.value,
dataGridAutoTransposeSingleRow: editDataGridAutoTransposeSingleRow.value,
tableOpenPageSize: editTableOpenPageSize.value,
@ -701,6 +712,10 @@ function syncEditorSettingsDraftFromStore() {
editShowColumnCommentsInHeader.value = settingsStore.editorSettings.showColumnCommentsInHeader;
editShowColumnTypesInHeader.value = settingsStore.editorSettings.showColumnTypesInHeader;
editCompactColumnHeaderActions.value = settingsStore.editorSettings.compactColumnHeaderActions;
editOpenTableDefaultSortMode.value = settingsStore.editorSettings.openTableDefaultSortMode;
editDataGridQuickSortEnabled.value = settingsStore.editorSettings.dataGridQuickSortEnabled;
editDataGridQuickSortMode.value = settingsStore.editorSettings.dataGridQuickSortMode;
editDataGridQuickSortDirection.value = settingsStore.editorSettings.dataGridQuickSortDirection;
editDataGridQuickEntry.value = settingsStore.editorSettings.dataGridQuickEntry;
editDataGridAutoTransposeSingleRow.value = settingsStore.editorSettings.dataGridAutoTransposeSingleRow;
editTableOpenPageSize.value = settingsStore.editorSettings.tableOpenPageSize;
@ -929,6 +944,10 @@ function resetDefaultsForTab(tab: SettingsCategory) {
editShowColumnCommentsInHeader.value = DEFAULT_EDITOR_SETTINGS.showColumnCommentsInHeader;
editShowColumnTypesInHeader.value = DEFAULT_EDITOR_SETTINGS.showColumnTypesInHeader;
editCompactColumnHeaderActions.value = DEFAULT_EDITOR_SETTINGS.compactColumnHeaderActions;
editOpenTableDefaultSortMode.value = DEFAULT_EDITOR_SETTINGS.openTableDefaultSortMode;
editDataGridQuickSortEnabled.value = DEFAULT_EDITOR_SETTINGS.dataGridQuickSortEnabled;
editDataGridQuickSortMode.value = DEFAULT_EDITOR_SETTINGS.dataGridQuickSortMode;
editDataGridQuickSortDirection.value = DEFAULT_EDITOR_SETTINGS.dataGridQuickSortDirection;
editDataGridQuickEntry.value = DEFAULT_EDITOR_SETTINGS.dataGridQuickEntry;
editDataGridAutoTransposeSingleRow.value = DEFAULT_EDITOR_SETTINGS.dataGridAutoTransposeSingleRow;
editTableOpenPageSize.value = DEFAULT_EDITOR_SETTINGS.tableOpenPageSize;
@ -990,6 +1009,10 @@ function resetAllDefaults() {
editShowColumnCommentsInHeader.value = DEFAULT_EDITOR_SETTINGS.showColumnCommentsInHeader;
editShowColumnTypesInHeader.value = DEFAULT_EDITOR_SETTINGS.showColumnTypesInHeader;
editCompactColumnHeaderActions.value = DEFAULT_EDITOR_SETTINGS.compactColumnHeaderActions;
editOpenTableDefaultSortMode.value = DEFAULT_EDITOR_SETTINGS.openTableDefaultSortMode;
editDataGridQuickSortEnabled.value = DEFAULT_EDITOR_SETTINGS.dataGridQuickSortEnabled;
editDataGridQuickSortMode.value = DEFAULT_EDITOR_SETTINGS.dataGridQuickSortMode;
editDataGridQuickSortDirection.value = DEFAULT_EDITOR_SETTINGS.dataGridQuickSortDirection;
editDataGridQuickEntry.value = DEFAULT_EDITOR_SETTINGS.dataGridQuickEntry;
editDataGridAutoTransposeSingleRow.value = DEFAULT_EDITOR_SETTINGS.dataGridAutoTransposeSingleRow;
editTableOpenPageSize.value = DEFAULT_EDITOR_SETTINGS.tableOpenPageSize;
@ -1222,6 +1245,18 @@ function onUpdateDownloadSourceChange(v: any) {
if (v === "official" || v === "cnb") editUpdateDownloadSource.value = v;
}
function onOpenTableDefaultSortModeChange(v: any) {
if (v === "none" || v === "primary-key-asc" || v === "primary-key-desc") editOpenTableDefaultSortMode.value = v;
}
function onDataGridQuickSortModeChange(v: any) {
if (v === "database" || v === "local") editDataGridQuickSortMode.value = v;
}
function onDataGridQuickSortDirectionChange(v: any) {
if (v === "asc" || v === "desc") editDataGridQuickSortDirection.value = v;
}
function setSidebarObjectDisplay(value: "grouped" | "simple") {
editSidebarObjectDisplay.value = value;
}
@ -3689,6 +3724,65 @@ onUnmounted(cleanupPreviewEditor);
</div>
<Switch id="compact-column-header-actions" v-model="editCompactColumnHeaderActions" />
</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="open-table-default-sort-mode">
{{ t("settings.openTableDefaultSortMode") }}
</Label>
<p class="text-xs text-muted-foreground">
{{ t("settings.openTableDefaultSortModeDescription") }}
</p>
</div>
<Select :model-value="editOpenTableDefaultSortMode" @update:model-value="onOpenTableDefaultSortModeChange">
<SelectTrigger id="open-table-default-sort-mode" class="h-8 w-44">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="none">{{ t("settings.openTableDefaultSortModeNone") }}</SelectItem>
<SelectItem value="primary-key-asc">{{ t("settings.openTableDefaultSortModePrimaryKeyAsc") }}</SelectItem>
<SelectItem value="primary-key-desc">{{ t("settings.openTableDefaultSortModePrimaryKeyDesc") }}</SelectItem>
</SelectContent>
</Select>
</div>
<div class="space-y-3 rounded-md border bg-muted/20 px-3 py-2">
<div class="flex items-center justify-between gap-4">
<div class="space-y-1">
<Label for="data-grid-quick-sort-enabled">
{{ t("settings.dataGridQuickSortEnabled") }}
</Label>
<p class="text-xs text-muted-foreground">
{{ t("settings.dataGridQuickSortDescription") }}
</p>
</div>
<Switch id="data-grid-quick-sort-enabled" v-model="editDataGridQuickSortEnabled" />
</div>
<div v-if="editDataGridQuickSortEnabled" class="flex justify-end gap-2">
<div class="space-y-1">
<Label for="data-grid-quick-sort-mode" class="text-xs text-muted-foreground">{{ t("settings.dataGridQuickSortMode") }}</Label>
<Select :model-value="editDataGridQuickSortMode" @update:model-value="onDataGridQuickSortModeChange">
<SelectTrigger id="data-grid-quick-sort-mode" class="h-8 w-40">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="local">{{ t("settings.dataGridQuickSortModeCurrentPage") }}</SelectItem>
<SelectItem value="database">{{ t("settings.dataGridQuickSortModeDatabase") }}</SelectItem>
</SelectContent>
</Select>
</div>
<div class="space-y-1">
<Label for="data-grid-quick-sort-direction" class="text-xs text-muted-foreground">{{ t("settings.dataGridQuickSortDirection") }}</Label>
<Select :model-value="editDataGridQuickSortDirection" @update:model-value="onDataGridQuickSortDirectionChange">
<SelectTrigger id="data-grid-quick-sort-direction" class="h-8 w-28">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="asc">{{ t("settings.dataGridQuickSortDirectionAsc") }}</SelectItem>
<SelectItem value="desc">{{ t("settings.dataGridQuickSortDirectionDesc") }}</SelectItem>
</SelectContent>
</Select>
</div>
</div>
</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="data-grid-quick-entry">

View File

@ -205,7 +205,7 @@ import { useTheme } from "@/composables/useTheme";
import { useConnectionStore } from "@/stores/connectionStore";
import { useQueryStore } from "@/stores/queryStore";
import { useSettingsStore } from "@/stores/settingsStore";
import type { DataGridSortDirection, DataGridSortMode } from "@/lib/dataGrid/dataGridSort";
import { resolveDataGridHeaderSortAction, type DataGridSortDirection, type DataGridSortMode } from "@/lib/dataGrid/dataGridSort";
import { DATA_GRID_COMPACT_TOPBAR_WIDTH, type DataGridReloadIntent, type DataGridToolbarActionCapability, type DataGridToolbarAutoRefreshCapability, type DataGridToolbarCopyCapability, type DataGridToolbarSaveCapability } from "@/lib/dataGrid/dataGridToolbar";
import { getTableMetadataCapabilities } from "@/lib/table/tableMetadataCapabilities";
import { getTableStructureCapabilities } from "@/lib/table/tableStructureCapabilities";
@ -4150,6 +4150,22 @@ function applyColumnSort(column: string, columnIndex: number, direction: "asc" |
emit("sort", column, columnIndex, direction, currentWhereInput(), mode);
}
function onHeaderSortClick(toggle: () => void, column: string, columnIndex: number) {
const action = resolveDataGridHeaderSortAction({
enabled: settingsStore.editorSettings.dataGridQuickSortEnabled,
configuredDirection: settingsStore.editorSettings.dataGridQuickSortDirection,
configuredMode: settingsStore.editorSettings.dataGridQuickSortMode,
currentColumnSorted: columnIsSorted(column, columnIndex),
currentDirection: sortDir.value,
currentMode: sortMode.value,
});
if (action.kind === "menu") {
toggle();
return;
}
applyColumnSort(column, columnIndex, action.direction, action.mode);
}
function selectHeaderSort(value: string, column: string, columnIndex: number) {
if (value === "clear") {
applyColumnSort(column, columnIndex, null, sortMode.value);
@ -8174,7 +8190,8 @@ const gridContextMenuItems = computed<ContextMenuItem[]>(() => {
:title="t('grid.sort')"
:aria-expanded="open"
@mousedown.stop
@click.stop="toggle"
@click.stop="onHeaderSortClick(toggle, col.name, col.actualColIdx)"
@contextmenu.prevent.stop="toggle"
>
<ArrowUp v-if="columnIsSorted(col.name, col.actualColIdx) && sortDir === 'asc'" class="h-3 w-3 shrink-0" />
<ArrowDown v-else-if="columnIsSorted(col.name, col.actualColIdx) && sortDir === 'desc'" class="h-3 w-3 shrink-0" />

View File

@ -14,7 +14,9 @@ const mocks = vi.hoisted(() => ({
getColumns: vi.fn(),
listIndexes: vi.fn(),
ensureConnected: vi.fn(),
sortTabResultLocally: vi.fn(),
tableOpenPageSize: 100,
openTableDefaultSortMode: "none" as "none" | "primary-key-asc" | "primary-key-desc",
tabs: [] as QueryTab[],
setTableMeta: vi.fn(),
}));
@ -38,6 +40,7 @@ vi.mock("@/stores/connectionStore", () => ({
useConnectionStore: () => ({
getConfig: mocks.getConfig,
ensureConnected: mocks.ensureConnected,
connectionIdentifierQuote: () => undefined,
}),
}));
@ -46,6 +49,7 @@ vi.mock("@/stores/queryStore", () => ({
executeTabSql: mocks.executeTabSql,
setExecuting: mocks.setExecuting,
updateSql: mocks.updateSql,
sortTabResultLocally: mocks.sortTabResultLocally,
tabs: mocks.tabs,
setTableMeta: mocks.setTableMeta.mockImplementation((id: string, meta: NonNullable<QueryTab["tableMeta"]>) => {
const tab = mocks.tabs.find((item) => item.id === id);
@ -60,7 +64,14 @@ vi.mock("@/stores/queryStore", () => ({
}));
vi.mock("@/stores/settingsStore", () => ({
useSettingsStore: () => ({ editorSettings: { tableOpenPageSize: mocks.tableOpenPageSize } }),
useSettingsStore: () => ({
editorSettings: {
tableOpenPageSize: mocks.tableOpenPageSize,
openTableDefaultSortMode: mocks.openTableDefaultSortMode,
infiniteScroll: false,
infiniteScrollMaxRows: 5000,
},
}),
}));
vi.mock("@/composables/useToast", () => ({
@ -98,6 +109,7 @@ describe("useDataGridActions", () => {
vi.clearAllMocks();
mocks.tabs.length = 0;
mocks.tableOpenPageSize = 100;
mocks.openTableDefaultSortMode = "none";
mocks.getConfig.mockReturnValue({ id: "postgres-1", db_type: "postgres" });
mocks.buildTableSelectSql.mockResolvedValue("SELECT * FROM public.users LIMIT 100 OFFSET 0");
mocks.buildSortedQuerySql.mockResolvedValue({ ok: true, sql: "SELECT sorted" });
@ -305,4 +317,64 @@ describe("useDataGridActions", () => {
}),
);
});
it("rebuilds composite primary-key ordering for toolbar refresh and pagination", async () => {
mocks.openTableDefaultSortMode = "primary-key-desc";
const tab = tableDataTab({
tableMeta: {
schema: "public",
tableName: "users",
tableType: "TABLE",
columns: [
{ name: "tenant_id", data_type: "text", is_nullable: false, column_default: null, is_primary_key: true, extra: null },
{ name: "record_id", data_type: "bigint", is_nullable: false, column_default: null, is_primary_key: true, extra: null },
],
primaryKeys: ["tenant_id", "record_id"],
},
openTableDefaultSortApplied: true,
openTableDefaultSortOrderBy: '"tenant_id" ASC, "record_id" ASC',
orderByInput: '"tenant_id" ASC, "record_id" ASC',
});
const actions = useDataGridActions(computed(() => tab));
await actions.onReloadData(tab.sql, "", "", tab.orderByInput, 25, 50, "refresh");
expect(mocks.buildTableSelectSql).toHaveBeenLastCalledWith(
expect.objectContaining({
orderBy: '"tenant_id" DESC, "record_id" DESC',
limit: 25,
offset: 50,
}),
);
await actions.onPaginate(75, 25, "", tab.orderByInput);
expect(mocks.buildTableSelectSql).toHaveBeenLastCalledWith(
expect.objectContaining({
orderBy: '"tenant_id" DESC, "record_id" DESC',
limit: 25,
offset: 75,
}),
);
});
it("sorts locally without issuing a database query", async () => {
const tab = tableDataTab();
const actions = useDataGridActions(computed(() => tab));
await actions.onSort("id", 0, "asc", "", "local");
expect(mocks.sortTabResultLocally).toHaveBeenCalledWith("tab-1", "id", 0, "asc");
expect(mocks.buildTableSelectSql).not.toHaveBeenCalled();
expect(mocks.executeTabSql).not.toHaveBeenCalled();
});
it("sorts in the database by rebuilding and executing table SQL", async () => {
const tab = tableDataTab();
const actions = useDataGridActions(computed(() => tab));
await actions.onSort("id", 0, "desc", "", "database");
expect(mocks.sortTabResultLocally).not.toHaveBeenCalled();
expect(mocks.buildTableSelectSql).toHaveBeenCalledWith(expect.objectContaining({ orderBy: '"id" DESC' }));
expect(mocks.executeTabSql).toHaveBeenCalledWith("tab-1", expect.any(String), expect.objectContaining({ preserveResultDuringExecution: true }));
});
});

View File

@ -9,9 +9,11 @@ const mocks = vi.hoisted(() => ({
cachedMetadata: undefined as unknown,
ensureConnected: vi.fn(),
executeTabSql: vi.fn(),
buildTableSelectSql: vi.fn(),
loadTableMetadata: vi.fn(),
setErrorResult: vi.fn(),
cancelTabExecution: vi.fn(),
openTableDefaultSortMode: "none" as "none" | "primary-key-asc" | "primary-key-desc",
}));
vi.mock("@/stores/connectionStore", () => ({
@ -69,7 +71,7 @@ vi.mock("@/stores/queryStore", () => ({
}));
vi.mock("@/stores/settingsStore", () => ({
useSettingsStore: () => ({ editorSettings: { reuseDataTab: false, pageSize: 100 } }),
useSettingsStore: () => ({ editorSettings: { reuseDataTab: false, tableOpenPageSize: 100, openTableDefaultSortMode: mocks.openTableDefaultSortMode } }),
}));
vi.mock("@/lib/database/jdbcDialect", () => ({
@ -92,7 +94,10 @@ vi.mock("@/lib/backend/debugLog", () => ({ appendDebugLog: vi.fn(), isDebugLoggi
// dataTabOpenPolicy 使用真实实现beforeEach 清空 tabs 时无候选可复用,
// 取消窗口测试则依赖真实 findExistingDataTabCandidate 选中同表 tab
vi.mock("@/lib/sidebar/treeNodeContext", () => ({ hasTreeNodeDatabaseContext: () => true }));
vi.mock("@/lib/table/tableSelectSql", () => ({ buildTableSelectSql: async () => "SELECT * FROM users" }));
vi.mock("@/lib/table/tableSelectSql", async (importOriginal) => ({
...(await importOriginal<typeof import("@/lib/table/tableSelectSql")>()),
buildTableSelectSql: mocks.buildTableSelectSql,
}));
vi.mock("@/lib/table/tableEditing", () => ({ usesSyntheticRowIdKey: () => false }));
vi.mock("@/lib/table/tableOpenPageLimit", () => ({ tableOpenPageLimit: () => 100 }));
vi.mock("@/lib/tabs/dataTabActivation", () => ({ canActivateExistingDataTableTab: () => false }));
@ -114,7 +119,9 @@ describe("useSidebarDataOpenRuntime", () => {
mocks.callOrder.length = 0;
mocks.tabs.length = 0;
mocks.cachedMetadata = undefined;
mocks.openTableDefaultSortMode = "none";
mocks.ensureConnected.mockResolvedValue(undefined);
mocks.buildTableSelectSql.mockResolvedValue("SELECT * FROM users");
mocks.executeTabSql.mockImplementation(async () => {
mocks.callOrder.push("query");
});
@ -307,4 +314,43 @@ describe("useSidebarDataOpenRuntime", () => {
expect(mocks.tabs[0]?.tableMetaPending).toBeFalsy();
expect(mocks.loadTableMetadata).not.toHaveBeenCalled();
});
it("loads composite primary-key metadata before the first query when default ordering is enabled", async () => {
mocks.databaseType = "postgres";
mocks.openTableDefaultSortMode = "primary-key-asc";
mocks.loadTableMetadata.mockResolvedValueOnce({
metadata: {
schema: "public",
tableName: "users",
tableType: "TABLE",
database: "app",
columns: [
{ name: "Tenant", data_type: "text", is_nullable: false, column_default: null, is_primary_key: true, extra: null },
{ name: "Order", data_type: "bigint", is_nullable: false, column_default: null, is_primary_key: true, extra: null },
],
indexes: [],
primaryKeys: ["Tenant", "Order"],
cachedAt: Date.now(),
},
cacheStatus: "miss",
ageMs: 0,
});
await useSidebarDataOpenRuntime().openData(tableNode);
expect(mocks.loadTableMetadata).toHaveBeenCalledTimes(1);
expect(mocks.buildTableSelectSql).toHaveBeenCalledWith(
expect.objectContaining({
primaryKeys: ["Tenant", "Order"],
orderBy: '"Tenant" ASC, "Order" ASC',
}),
);
expect(mocks.tabs[0]).toMatchObject({
resultSortColumn: "Tenant",
resultSortDirection: "asc",
resultSortMode: "database",
orderByInput: '"Tenant" ASC, "Order" ASC',
openTableDefaultSortApplied: true,
});
});
});

View File

@ -4,6 +4,7 @@ import { useConnectionStore } from "@/stores/connectionStore";
import { useQueryStore } from "@/stores/queryStore";
import { useSettingsStore } from "@/stores/settingsStore";
import { buildTableSelectSql, quoteTableDataIdentifier } from "@/lib/table/tableSelectSql";
import { applyOpenTableDefaultSortState, buildOpenTableDefaultSort, clearOpenTableDefaultSortMarker, shouldApplyOpenTableDefaultSort } from "@/lib/table/tableDefaultSort";
import { tableOpenPageLimit } from "@/lib/table/tableOpenPageLimit";
import { usesSyntheticRowIdKey } from "@/lib/table/tableEditing";
import { tableMetaForDataTab } from "@/lib/table/tableDataTabMeta";
@ -71,6 +72,31 @@ export function useDataGridActions(activeTab: ComputedRef<QueryTab | undefined>)
});
}
function openTableDefaultSortForTab(tab: QueryTab) {
const config = connectionStore.getConfig(tab.connectionId);
const tableMeta = tableMetaForDataTab(tab);
const primaryKeys = tab.tableMeta ? tab.tableMeta.primaryKeys : (tableMeta?.primaryKeys ?? []);
return buildOpenTableDefaultSort({
mode: settingsStore.editorSettings.openTableDefaultSortMode,
databaseType: effectiveDatabaseTypeForConnection(config),
identifierQuote: connectionStore.connectionIdentifierQuote?.(tab.connectionId),
primaryKeys,
columns: tableMeta?.columns.map((column) => column.name) ?? [],
});
}
function resolveTableOrderBy(tab: QueryTab, orderBy?: string): string | undefined {
if (shouldApplyOpenTableDefaultSort(tab, orderBy)) {
const defaultSort = openTableDefaultSortForTab(tab);
applyOpenTableDefaultSortState(tab, defaultSort);
return defaultSort?.orderBy;
}
if (orderBy?.trim() !== tab.openTableDefaultSortOrderBy?.trim()) {
clearOpenTableDefaultSortMarker(tab);
}
return orderBy;
}
async function refreshDataTabTableMeta(tab: QueryTab, trace?: { traceId: string; elapsed: () => string }): Promise<void> {
if (tab.mode !== "data" || !tab.connectionId || !tab.database) return;
const tableMeta = tableMetaForDataTab(tab);
@ -179,7 +205,8 @@ export function useDataGridActions(activeTab: ComputedRef<QueryTab | undefined>)
}
try {
console.info("[DBX][reloadData:build-sql:start]", { traceId, elapsed: elapsed() });
const nextSql = await buildTableSql(tab, { whereInput, orderBy, limit: pageLimit, offset: pageOffset });
const nextOrderBy = resolveTableOrderBy(tab, orderBy);
const nextSql = await buildTableSql(tab, { whereInput, orderBy: nextOrderBy, limit: pageLimit, offset: pageOffset });
console.info("[DBX][reloadData:build-sql:done]", { traceId, elapsed: elapsed() });
queryStore.updateSql(tab.id, nextSql);
console.info("[DBX][reloadData:execute:start]", { traceId, elapsed: elapsed() });
@ -282,7 +309,8 @@ export function useDataGridActions(activeTab: ComputedRef<QueryTab | undefined>)
if (!tableMetaForDataTab(tab)) return;
tab.whereInput = whereInput ?? "";
const sql = await buildTableSql(tab, { limit, offset, whereInput, orderBy });
const nextOrderBy = resolveTableOrderBy(tab, orderBy);
const sql = await buildTableSql(tab, { limit, offset, whereInput, orderBy: nextOrderBy });
queryStore.updateSql(tab.id, sql);
await queryStore.executeTabSql(tab.id, sql, {
pagination: { offset, limit },
@ -299,6 +327,7 @@ export function useDataGridActions(activeTab: ComputedRef<QueryTab | undefined>)
tab.resultSortColumnIndex = direction ? columnIndex : undefined;
tab.resultSortDirection = direction ?? undefined;
tab.resultSortMode = direction ? mode : undefined;
clearOpenTableDefaultSortMarker(tab);
if (mode === "local") {
if (tab.mode === "data") {

View File

@ -11,6 +11,7 @@ import { canApplyDataTabMetadata, dataTabMetadataNeedsRefresh, findExistingDataT
import type { SidebarDataOpenRequest } from "@/lib/sidebar/sidebarDataOpenCoordinator";
import { hasTreeNodeDatabaseContext } from "@/lib/sidebar/treeNodeContext";
import { buildTableSelectSql } from "@/lib/table/tableSelectSql";
import { applyOpenTableDefaultSortState, buildOpenTableDefaultSort } from "@/lib/table/tableDefaultSort";
import { usesSyntheticRowIdKey } from "@/lib/table/tableEditing";
import { tableOpenPageLimit } from "@/lib/table/tableOpenPageLimit";
import { canActivateExistingDataTableTab } from "@/lib/tabs/dataTabActivation";
@ -141,6 +142,8 @@ export function useSidebarDataOpenRuntime() {
tab.resultLocalSortOriginalMongoDocuments = undefined;
tab.resultLocalSortOriginalMongoCopyDocuments = undefined;
tab.resultSortedSql = undefined;
tab.openTableDefaultSortApplied = undefined;
tab.openTableDefaultSortOrderBy = undefined;
tab.resultPageSql = undefined;
tab.resultPageLimit = undefined;
tab.resultPageOffset = undefined;
@ -274,22 +277,48 @@ export function useSidebarDataOpenRuntime() {
if (!config) throw new Error("Connection config not found");
const limit = tableOpenPageLimit(settingsStore.editorSettings.tableOpenPageSize);
const shouldRefreshTableMeta = !cachedTableMeta;
const openTableDefaultSortMode = settingsStore.editorSettings.openTableDefaultSortMode;
let tableMetaForQuery = cachedTableMeta;
let shouldRefreshTableMeta = !tableMetaForQuery;
// Dameng metadata calls must remain serialized behind the table query.
const deferTableMetaRefresh = effectiveDbType === "dameng";
if (cachedTableMeta) {
let attemptedDefaultSortMetadataLoad = false;
if (openTableDefaultSortMode !== "none" && !tableMetaForQuery && !deferTableMetaRefresh) {
attemptedDefaultSortMetadataLoad = true;
try {
const loadedMetadata = await loadTableMetadata({
connectionId: node.connectionId,
database: node.database,
schema: querySchema,
tableName: node.label,
tableType,
databaseType: metadataDatabaseType,
driverProfile: config.driver_profile || config.db_type,
catalog: node.catalog,
traceLogger: isDebugLoggingEnabled() ? (event) => openDataLog("debug", "metadata:trace", { sourceTraceId: traceId, ...event }) : undefined,
});
if (!isActive() || !canApplyTableMetadata(tabId)) return;
tableMetaForQuery = tableMetadataToDataTabMeta(loadedMetadata.metadata, tableSchema);
queryStore.setTableMeta(tabId, tableMetaForQuery);
shouldRefreshTableMeta = false;
logPhase("metadata-loaded-for-default-sort", { tabId, columnCount: tableMetaForQuery.columns.length, primaryKeyCount: tableMetaForQuery.primaryKeys.length });
} catch (error) {
openDataLog("warn", "metadata:default-sort:error", { traceId, tabId, elapsed: elapsed(), error });
}
}
if (tableMetaForQuery) {
openDataLog("info", "metadata:cache-hit", {
traceId,
tabId,
columnCount: cachedTableMeta.columns.length,
primaryKeyCount: cachedTableMeta.primaryKeys.length,
columnCount: tableMetaForQuery.columns.length,
primaryKeyCount: tableMetaForQuery.primaryKeys.length,
source: cachedTableMetaSource,
ageMs: Math.round(cachedTableMetaAgeMs),
elapsed: elapsed(),
});
} else if (deferTableMetaRefresh) {
logPhase("metadata-deferred", { tabId });
} else {
} else if (!attemptedDefaultSortMetadataLoad) {
void refreshTableMetaInBackground(tabId);
logPhase("metadata-started", { tabId });
}
@ -300,8 +329,15 @@ export function useSidebarDataOpenRuntime() {
return;
}
const columns = cachedTableMeta?.columns ?? [];
const primaryKeys = cachedTableMeta?.primaryKeys ?? [];
const columns = tableMetaForQuery?.columns ?? [];
const primaryKeys = tableMetaForQuery?.primaryKeys ?? [];
const defaultSort = buildOpenTableDefaultSort({
mode: openTableDefaultSortMode,
databaseType: effectiveDbType,
identifierQuote: connectionStore.connectionIdentifierQuote?.(node.connectionId),
primaryKeys,
columns: columns.map((column) => column.name),
});
const includeRowId = usesSyntheticRowIdKey(effectiveDbType, primaryKeys, tableType);
const sql = await buildTableSelectSql({
databaseType: effectiveDbType,
@ -313,6 +349,7 @@ export function useSidebarDataOpenRuntime() {
catalog: node.catalog,
columns: columns.map((column) => column.name),
primaryKeys,
orderBy: defaultSort?.orderBy,
limit,
includeRowId,
});
@ -339,6 +376,8 @@ export function useSidebarDataOpenRuntime() {
skipEnsureConnected: true,
pagination: { limit, offset: 0 },
});
const currentTab = queryStore.tabs.find((item) => item.id === tabId);
if (currentTab) applyOpenTableDefaultSortState(currentTab, defaultSort);
openDataLog("info", "execute:done", { traceId, tabId, elapsed: elapsed() });
logPhase("execute-tab-sql", { tabId });
if (shouldRefreshTableMeta && deferTableMetaRefresh && canApplyTableMetadata(tabId)) {

View File

@ -4381,6 +4381,19 @@ export default {
showColumnTypesInHeaderDescription: "Display each column's data type directly below grid column names.",
compactColumnHeaderActions: "Compact column header tools",
compactColumnHeaderActionsDescription: "Move formatter and local filter tools into a more menu so column names get priority.",
openTableDefaultSortMode: "Open table default sort",
openTableDefaultSortModeDescription: "Controls whether opening a table or refreshing the data editor adds primary-key database sorting. Disabled keeps the database return order.",
openTableDefaultSortModeNone: "Disabled",
openTableDefaultSortModePrimaryKeyAsc: "Primary key ascending",
openTableDefaultSortModePrimaryKeyDesc: "Primary key descending",
dataGridQuickSortEnabled: "Enable header quick sort",
dataGridQuickSortDescription: "When enabled, clicking a column sort icon immediately uses the configured scope and direction. Disabled keeps the existing sort menu behavior.",
dataGridQuickSortMode: "Sort scope",
dataGridQuickSortModeDatabase: "Database sort",
dataGridQuickSortModeCurrentPage: "Current page sort",
dataGridQuickSortDirection: "Sort direction",
dataGridQuickSortDirectionAsc: "Ascending",
dataGridQuickSortDirectionDesc: "Descending",
dataGridQuickEntry: "Quick grid entry",
dataGridQuickEntryDescription: "When enabled, editing a cell or filling the bottom blank row saves to the database as soon as focus leaves. Useful for frequent entry, but it can cause accidental writes.",
dataGridAutoTransposeSingleRow: "Auto-transpose single-row query results",

View File

@ -4162,6 +4162,19 @@ export default withEnglishFallback({
showColumnTypesInHeaderDescription: "Muestra el tipo de dato de cada columna directamente debajo del nombre de la columna.",
compactColumnHeaderActions: "Compactar herramientas del encabezado",
compactColumnHeaderActionsDescription: "Mueve formatear y filtrar a un menú de más acciones para priorizar el nombre de columna.",
openTableDefaultSortMode: "Orden predeterminado al abrir tabla",
openTableDefaultSortModeDescription: "Controla si al abrir una tabla o actualizar el editor de datos se agrega ordenación por clave primaria en la base de datos. Desactivado conserva el orden devuelto por la base de datos.",
openTableDefaultSortModeNone: "Desactivado",
openTableDefaultSortModePrimaryKeyAsc: "Clave primaria ascendente",
openTableDefaultSortModePrimaryKeyDesc: "Clave primaria descendente",
dataGridQuickSortEnabled: "Activar orden rápido del encabezado",
dataGridQuickSortDescription: "Al activarlo, el icono de ordenación usa inmediatamente el alcance y la dirección configurados. Desactivado conserva el menú de ordenación existente.",
dataGridQuickSortMode: "Alcance",
dataGridQuickSortModeDatabase: "Ordenar en base de datos",
dataGridQuickSortModeCurrentPage: "Ordenar página actual",
dataGridQuickSortDirection: "Dirección",
dataGridQuickSortDirectionAsc: "Ascendente",
dataGridQuickSortDirectionDesc: "Descendente",
dataGridQuickEntry: "Entrada rápida en grilla",
dataGridQuickEntryDescription: "Al activarlo, editar una celda o llenar la fila vacía inferior se guarda en la base de datos al perder el foco. Agiliza la captura frecuente, pero puede causar escrituras accidentales.",
dataGridAutoTransposeSingleRow: "Transponer automáticamente los resultados de consulta de una sola fila",

View File

@ -4160,6 +4160,19 @@ export default withEnglishFallback({
showColumnTypesInHeaderDescription: "Visualizza il tipo di dato di ciascuna colonna direttamente sotto i nomi delle colonne nella griglia.",
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à.",
openTableDefaultSortMode: "Ordinamento predefinito all'apertura tabella",
openTableDefaultSortModeDescription: "Controlla se aprendo una tabella o aggiornando l'editor dati viene aggiunto un ordinamento per chiave primaria nel database. Disattivato mantiene l'ordine restituito dal database.",
openTableDefaultSortModeNone: "Disattivato",
openTableDefaultSortModePrimaryKeyAsc: "Chiave primaria crescente",
openTableDefaultSortModePrimaryKeyDesc: "Chiave primaria decrescente",
dataGridQuickSortEnabled: "Abilita ordinamento rapido intestazione",
dataGridQuickSortDescription: "Se attivo, l'icona di ordinamento usa subito ambito e direzione configurati. Se disattivo, mantiene il menu di ordinamento esistente.",
dataGridQuickSortMode: "Ambito",
dataGridQuickSortModeDatabase: "Ordina nel database",
dataGridQuickSortModeCurrentPage: "Ordina pagina corrente",
dataGridQuickSortDirection: "Direzione",
dataGridQuickSortDirectionAsc: "Crescente",
dataGridQuickSortDirectionDesc: "Decrescente",
dataGridQuickEntry: "Inserimento rapido griglia",
dataGridQuickEntryDescription: "Se attivo, la modifica di una cella o la compilazione della riga vuota in fondo viene salvata nel database appena il focus esce. Utile per inserimenti frequenti, ma può causare scritture accidentali.",
dataGridAutoTransposeSingleRow: "Trasponi automaticamente i risultati delle query a riga singola",

View File

@ -4157,6 +4157,19 @@ export default withEnglishFallback({
showColumnTypesInHeaderDescription: "グリッド列名の直下に各列のデータ型を表示します。",
compactColumnHeaderActions: "列ヘッダーツールをコンパクトに",
compactColumnHeaderActionsDescription: "フォーマッターとローカルフィルターツールをもっと見るメニューに移動し、列名を優先表示します。",
openTableDefaultSortMode: "テーブルを開くときの既定の並び替え",
openTableDefaultSortModeDescription: "テーブルを開く、またはデータエディターを更新するときに主キーでデータベース並び替えを追加するかを制御します。無効の場合はデータベースの戻り順を維持します。",
openTableDefaultSortModeNone: "無効",
openTableDefaultSortModePrimaryKeyAsc: "主キー昇順",
openTableDefaultSortModePrimaryKeyDesc: "主キー降順",
dataGridQuickSortEnabled: "ヘッダーのクイック並び替えを有効化",
dataGridQuickSortDescription: "有効にすると、並び替えアイコンは設定した範囲と方向をすぐに適用します。無効の場合は従来の並び替えメニューを維持します。",
dataGridQuickSortMode: "並び替え範囲",
dataGridQuickSortModeDatabase: "データベースで並び替え",
dataGridQuickSortModeCurrentPage: "現在のページで並び替え",
dataGridQuickSortDirection: "並び替え方向",
dataGridQuickSortDirectionAsc: "昇順",
dataGridQuickSortDirectionDesc: "降順",
dataGridQuickEntry: "グリッド高速入力",
dataGridQuickEntryDescription: "有効にすると、セル編集や下部の空行入力はフォーカスが外れた時点でデータベースに保存されます。頻繁な入力に便利ですが、誤操作による書き込みのリスクがあります。",
dataGridAutoTransposeSingleRow: "1 行のクエリ結果を自動で転置表示",

View File

@ -4162,6 +4162,19 @@ export default withEnglishFallback({
showColumnTypesInHeaderDescription: "Exibir o tipo de dado de cada coluna diretamente abaixo dos nomes das colunas da grade.",
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.",
openTableDefaultSortMode: "Ordenação padrão ao abrir tabela",
openTableDefaultSortModeDescription: "Controla se abrir uma tabela ou atualizar o editor de dados adiciona ordenação por chave primária no banco. Desativado mantém a ordem retornada pelo banco.",
openTableDefaultSortModeNone: "Desativado",
openTableDefaultSortModePrimaryKeyAsc: "Chave primária crescente",
openTableDefaultSortModePrimaryKeyDesc: "Chave primária decrescente",
dataGridQuickSortEnabled: "Ativar ordenação rápida do cabeçalho",
dataGridQuickSortDescription: "Quando ativado, o ícone de ordenação usa imediatamente o escopo e a direção configurados. Desativado mantém o menu de ordenação existente.",
dataGridQuickSortMode: "Escopo",
dataGridQuickSortModeDatabase: "Ordenar no banco",
dataGridQuickSortModeCurrentPage: "Ordenar página atual",
dataGridQuickSortDirection: "Direção",
dataGridQuickSortDirectionAsc: "Crescente",
dataGridQuickSortDirectionDesc: "Decrescente",
dataGridQuickEntry: "Entrada rápida na grade",
dataGridQuickEntryDescription: "Quando ativado, editar uma célula ou preencher a linha vazia inferior salva no banco de dados assim que o foco sai. Ajuda na entrada frequente, mas pode causar gravações acidentais.",
dataGridAutoTransposeSingleRow: "Transpor automaticamente resultados de consulta com uma única linha",

View File

@ -4381,6 +4381,19 @@ export default withEnglishFallback({
showColumnTypesInHeaderDescription: "把每个字段的数据类型直接显示在结果表头字段名下方。",
compactColumnHeaderActions: "收起字段表头工具",
compactColumnHeaderActionsDescription: "将格式化、本地筛选收进更多菜单,优先显示字段名称。",
openTableDefaultSortMode: "打开表默认排序",
openTableDefaultSortModeDescription: "控制打开表或刷新数据编辑器时是否自动按全部主键列生成数据库排序;关闭时保持数据库返回顺序。",
openTableDefaultSortModeNone: "关闭",
openTableDefaultSortModePrimaryKeyAsc: "主键升序",
openTableDefaultSortModePrimaryKeyDesc: "主键降序",
dataGridQuickSortEnabled: "启用表头快速排序",
dataGridQuickSortDescription: "启用后,点击列头排序图标会立即使用配置的范围和方向;关闭时保持原有排序菜单行为。",
dataGridQuickSortMode: "排序范围",
dataGridQuickSortModeDatabase: "数据库排序",
dataGridQuickSortModeCurrentPage: "当前页排序",
dataGridQuickSortDirection: "排序方向",
dataGridQuickSortDirectionAsc: "升序",
dataGridQuickSortDirectionDesc: "降序",
dataGridQuickEntry: "表格快捷录入",
dataGridQuickEntryDescription: "开启后,编辑单元格或填写底部空行并离焦会立即保存到数据库。便于高频录入,但存在误操作风险。",
dataGridAutoTransposeSingleRow: "自动转置单行查询结果",

View File

@ -3828,6 +3828,19 @@ export default withEnglishFallback({
showColumnTypesInHeaderDescription: "直接在資料表格欄位名稱下方顯示每個欄位的資料類型。",
compactColumnHeaderActions: "收起欄位表頭工具",
compactColumnHeaderActionsDescription: "將格式設定和本機篩選工具移到更多選單,讓欄位名稱優先顯示。",
openTableDefaultSortMode: "開啟資料表預設排序",
openTableDefaultSortModeDescription: "控制開啟資料表或重新整理資料編輯器時,是否自動依全部主鍵欄位產生資料庫排序;關閉時保留資料庫返回順序。",
openTableDefaultSortModeNone: "關閉",
openTableDefaultSortModePrimaryKeyAsc: "主鍵遞增",
openTableDefaultSortModePrimaryKeyDesc: "主鍵遞減",
dataGridQuickSortEnabled: "啟用表頭快速排序",
dataGridQuickSortDescription: "啟用後,點擊欄位排序圖示會立即使用設定的範圍與方向;關閉時維持原有排序選單行為。",
dataGridQuickSortMode: "排序範圍",
dataGridQuickSortModeDatabase: "資料庫排序",
dataGridQuickSortModeCurrentPage: "目前頁排序",
dataGridQuickSortDirection: "排序方向",
dataGridQuickSortDirectionAsc: "遞增",
dataGridQuickSortDirectionDesc: "遞減",
dataGridQuickEntry: "表格快速輸入",
dataGridQuickEntryDescription: "開啟後,編輯儲存格或填寫底部空白列並離焦會立即儲存到資料庫。適合高頻輸入,但有誤操作風險。",
dataGridAutoTransposeSingleRow: "自動轉置單行查詢結果",

View File

@ -0,0 +1,46 @@
import { describe, expect, it } from "vitest";
import { resolveDataGridHeaderSortAction } from "@/lib/dataGrid/dataGridSort";
describe("resolveDataGridHeaderSortAction", () => {
it("keeps the existing menu action when quick sorting is disabled", () => {
expect(
resolveDataGridHeaderSortAction({
enabled: false,
configuredDirection: "desc",
configuredMode: "database",
currentColumnSorted: false,
currentDirection: "asc",
currentMode: "local",
}),
).toEqual({ kind: "menu" });
});
it.each([
["local", "asc"],
["database", "desc"],
] as const)("uses the explicit %s/%s preference for an unsorted column", (configuredMode, configuredDirection) => {
expect(
resolveDataGridHeaderSortAction({
enabled: true,
configuredDirection,
configuredMode,
currentColumnSorted: false,
currentDirection: "asc",
currentMode: "database",
}),
).toEqual({ kind: "sort", direction: configuredDirection, mode: configuredMode });
});
it("toggles an existing sort while preserving its scope", () => {
expect(
resolveDataGridHeaderSortAction({
enabled: true,
configuredDirection: "asc",
configuredMode: "database",
currentColumnSorted: true,
currentDirection: "asc",
currentMode: "local",
}),
).toEqual({ kind: "sort", direction: "desc", mode: "local" });
});
});

View File

@ -0,0 +1,48 @@
import { describe, expect, it } from "vitest";
import { buildOpenTableDefaultSort, buildPrimaryKeyOrderBy } from "@/lib/table/tableDefaultSort";
describe("buildPrimaryKeyOrderBy", () => {
it.each([
["postgres", '"Tenant" ASC, "Order" ASC'],
["mysql", "`Tenant` ASC, `Order` ASC"],
["sqlserver", "[Tenant] ASC, [Order] ASC"],
["oracle", '"Tenant" ASC, "Order" ASC'],
["neo4j", "n.`Tenant` ASC, n.`Order` ASC"],
] as const)("quotes composite keys for %s", (databaseType, expected) => {
expect(
buildPrimaryKeyOrderBy({
databaseType,
primaryKeys: ["Tenant", "Order"],
direction: "asc",
}),
).toBe(expected);
});
it("quotes every mixed-case or reserved identifier and applies descending to every key", () => {
expect(
buildPrimaryKeyOrderBy({
databaseType: "postgres",
primaryKeys: ['Tenant"Id', "select"],
direction: "desc",
}),
).toBe('"Tenant""Id" DESC, "select" DESC');
});
});
describe("buildOpenTableDefaultSort", () => {
it("keeps metadata order for all primary-key columns while using the first key as the grid indicator", () => {
expect(
buildOpenTableDefaultSort({
mode: "primary-key-desc",
databaseType: "postgres",
primaryKeys: ["tenant_id", "record_id"],
columns: ["record_id", "tenant_id", "value"],
}),
).toEqual({
column: "tenant_id",
columnIndex: 1,
direction: "desc",
orderBy: '"tenant_id" DESC, "record_id" DESC',
});
});
});

View File

@ -34,6 +34,8 @@ export interface SavedOpenTab {
resultSortDirection?: QueryTab["resultSortDirection"];
resultSortMode?: QueryTab["resultSortMode"];
orderByInput?: string;
openTableDefaultSortApplied?: boolean;
openTableDefaultSortOrderBy?: string;
resultPageLimit?: number;
resultPageOffset?: number;
whereInput?: string;
@ -102,6 +104,8 @@ export function serializeOpenTabs(tabs: QueryTab[]): SavedOpenTab[] {
...(tab.resultSortDirection !== undefined ? { resultSortDirection: tab.resultSortDirection } : {}),
...(tab.resultSortMode !== undefined ? { resultSortMode: tab.resultSortMode } : {}),
...(tab.orderByInput !== undefined ? { orderByInput: tab.orderByInput } : {}),
...(tab.openTableDefaultSortApplied !== undefined ? { openTableDefaultSortApplied: tab.openTableDefaultSortApplied } : {}),
...(tab.openTableDefaultSortOrderBy !== undefined ? { openTableDefaultSortOrderBy: tab.openTableDefaultSortOrderBy } : {}),
...(tab.resultPageLimit !== undefined ? { resultPageLimit: tab.resultPageLimit } : {}),
...(tab.resultPageOffset !== undefined ? { resultPageOffset: tab.resultPageOffset } : {}),
...(tab.whereInput !== undefined ? { whereInput: tab.whereInput } : {}),

View File

@ -7,6 +7,24 @@ export interface DataGridSortState {
direction: DataGridSortDirection;
}
export type DataGridHeaderSortAction = { kind: "menu" } | { kind: "sort"; direction: DataGridSortDirection; mode: DataGridSortMode };
export function resolveDataGridHeaderSortAction(options: { enabled: boolean; configuredDirection: DataGridSortDirection; configuredMode: DataGridSortMode; currentDirection: DataGridSortDirection; currentMode: DataGridSortMode; currentColumnSorted: boolean }): DataGridHeaderSortAction {
if (!options.enabled) return { kind: "menu" };
if (options.currentColumnSorted) {
return {
kind: "sort",
direction: options.currentDirection === "asc" ? "desc" : "asc",
mode: options.currentMode,
};
}
return {
kind: "sort",
direction: options.configuredDirection,
mode: options.configuredMode,
};
}
export function nextDataGridSortState(current: DataGridSortState, column: string, columnIndex: number): DataGridSortState {
if (current.column === column && current.columnIndex === columnIndex) {
if (current.direction === "asc") {

View File

@ -30,6 +30,10 @@ export const EDITOR_SETTINGS_DRAFT_KEYS = [
"compactColumnHeaderActions",
"dataGridQuickEntry",
"dataGridAutoTransposeSingleRow",
"openTableDefaultSortMode",
"dataGridQuickSortEnabled",
"dataGridQuickSortMode",
"dataGridQuickSortDirection",
"tableOpenPageSize",
"infiniteScroll",
"infiniteScrollMaxRows",

View File

@ -0,0 +1,73 @@
import type { OpenTableDefaultSortMode } from "@/stores/settingsStore";
import type { DatabaseType, QueryTab } from "@/types/database";
import { quoteTableDataIdentifier } from "@/lib/table/tableSelectSql";
export type TableDefaultSortDirection = "asc" | "desc";
export interface OpenTableDefaultSort {
column: string;
columnIndex: number;
direction: TableDefaultSortDirection;
orderBy: string;
}
export function buildPrimaryKeyOrderBy(options: { databaseType?: DatabaseType; identifierQuote?: string; primaryKeys: string[]; direction: TableDefaultSortDirection }): string | undefined {
if (options.primaryKeys.length === 0) return undefined;
const direction = options.direction.toUpperCase();
const prefix = options.databaseType === "neo4j" ? "n." : "";
return options.primaryKeys.map((primaryKey) => `${prefix}${quoteTableDataIdentifier(options.databaseType, primaryKey, options.identifierQuote)} ${direction}`).join(", ");
}
export function buildOpenTableDefaultSort(options: { mode: OpenTableDefaultSortMode; databaseType?: DatabaseType; identifierQuote?: string; primaryKeys: string[]; columns: string[] }): OpenTableDefaultSort | undefined {
if (options.mode === "none") return undefined;
const column = options.primaryKeys[0];
if (!column) return undefined;
const direction = options.mode === "primary-key-desc" ? "desc" : "asc";
const orderBy = buildPrimaryKeyOrderBy({
databaseType: options.databaseType,
identifierQuote: options.identifierQuote,
primaryKeys: options.primaryKeys,
direction,
});
if (!orderBy) return undefined;
return {
column,
columnIndex: options.columns.indexOf(column),
direction,
orderBy,
};
}
export function shouldApplyOpenTableDefaultSort(tab: QueryTab, orderBy?: string): boolean {
const trimmedOrderBy = orderBy?.trim();
if (tab.openTableDefaultSortApplied) {
const defaultOrderBy = tab.openTableDefaultSortOrderBy?.trim();
return !trimmedOrderBy || (!!defaultOrderBy && trimmedOrderBy === defaultOrderBy);
}
return !trimmedOrderBy && !tab.resultSortColumn && !tab.resultSortDirection && !tab.orderByInput?.trim();
}
export function applyOpenTableDefaultSortState(tab: QueryTab, sort: OpenTableDefaultSort | undefined): void {
if (!sort) {
tab.resultSortColumn = undefined;
tab.resultSortColumnIndex = undefined;
tab.resultSortDirection = undefined;
tab.resultSortMode = undefined;
tab.orderByInput = undefined;
tab.openTableDefaultSortApplied = undefined;
tab.openTableDefaultSortOrderBy = undefined;
return;
}
tab.resultSortColumn = sort.column;
tab.resultSortColumnIndex = sort.columnIndex >= 0 ? sort.columnIndex : undefined;
tab.resultSortDirection = sort.direction;
tab.resultSortMode = "database";
tab.orderByInput = sort.orderBy;
tab.openTableDefaultSortApplied = true;
tab.openTableDefaultSortOrderBy = sort.orderBy;
}
export function clearOpenTableDefaultSortMarker(tab: QueryTab): void {
tab.openTableDefaultSortApplied = undefined;
tab.openTableDefaultSortOrderBy = undefined;
}

View File

@ -8,6 +8,7 @@ const mocks = vi.hoisted(() => ({
executeMulti: vi.fn(),
getConnectionConfig: vi.fn(),
saveOpenTabsState: vi.fn(),
openTableDefaultSortMode: "none" as "none" | "primary-key-asc" | "primary-key-desc",
}));
vi.mock("@/lib/backend/api", () => ({
@ -22,13 +23,14 @@ vi.mock("@/stores/connectionStore", () => ({
useConnectionStore: () => ({
ensureConnected: vi.fn().mockResolvedValue(undefined),
getConfig: mocks.getConnectionConfig,
connectionIdentifierQuote: () => undefined,
recordConnectionLostError: vi.fn(),
}),
}));
vi.mock("@/stores/settingsStore", () => ({
useSettingsStore: () => ({
editorSettings: { pageSize: 1000 },
editorSettings: { pageSize: 1000, tableOpenPageSize: 100, openTableDefaultSortMode: mocks.openTableDefaultSortMode },
}),
}));
@ -47,6 +49,7 @@ describe("queryStore table data refresh", () => {
vi.unstubAllGlobals();
installLocalStorage();
setActivePinia(createPinia());
mocks.openTableDefaultSortMode = "none";
mocks.getConnectionConfig.mockReturnValue({
id: "pg-1",
name: "Postgres",
@ -291,4 +294,35 @@ describe("queryStore table data refresh", () => {
expect(tab.result?.execution_error).toBe(true);
expect(mocks.executeMulti).not.toHaveBeenCalled();
});
it("rebuilds persisted default sorting with every composite primary-key column", async () => {
mocks.openTableDefaultSortMode = "primary-key-desc";
const { useQueryStore } = await import("@/stores/queryStore");
const store = useQueryStore();
const tabId = store.createTab("pg-1", "app", "orders", "data", "public");
store.setTableMeta(tabId, {
schema: "public",
tableName: "orders",
tableType: "TABLE",
columns: [
{ name: "Tenant", data_type: "text", is_nullable: false, column_default: null, is_primary_key: true, extra: null },
{ name: "Order", data_type: "bigint", is_nullable: false, column_default: null, is_primary_key: true, extra: null },
],
primaryKeys: ["Tenant", "Order"],
});
const tab = store.tabs.find((candidate) => candidate.id === tabId)!;
tab.openTableDefaultSortApplied = true;
tab.openTableDefaultSortOrderBy = '"Tenant" ASC, "Order" ASC';
tab.orderByInput = tab.openTableDefaultSortOrderBy;
await expect(store.refreshDataTab(tabId)).resolves.toBe(true);
expect(mocks.buildTableSelectSql).toHaveBeenCalledWith(
expect.objectContaining({
primaryKeys: ["Tenant", "Order"],
orderBy: '"Tenant" DESC, "Order" DESC',
}),
);
expect(tab.orderByInput).toBe('"Tenant" DESC, "Order" DESC');
});
});

View File

@ -5,6 +5,31 @@ import { isProxy } from "vue";
import type { AiConfigItem } from "@/types/ai";
describe("normalizeEditorSettings", () => {
it("keeps quick sorting disabled unless current or legacy settings explicitly enable it", () => {
const missing = normalizeEditorSettings({});
expect(missing.dataGridQuickSortEnabled).toBe(false);
expect(missing.dataGridQuickSortMode).toBe("local");
expect(missing.dataGridQuickSortDirection).toBe("asc");
const legacyEnabled = normalizeEditorSettings({
defaultDataGridSortEnabled: true,
defaultDataGridSortMode: "database",
defaultDataGridSortDirection: "desc",
} as any);
expect(legacyEnabled.dataGridQuickSortEnabled).toBe(true);
expect(legacyEnabled.dataGridQuickSortMode).toBe("database");
expect(legacyEnabled.dataGridQuickSortDirection).toBe("desc");
const legacyDisabled = normalizeEditorSettings({
defaultDataGridSortEnabled: false,
defaultDataGridSortMode: "database",
defaultDataGridSortDirection: "desc",
} as any);
expect(legacyDisabled.dataGridQuickSortEnabled).toBe(false);
expect(legacyDisabled.dataGridQuickSortMode).toBe("database");
expect(legacyDisabled.dataGridQuickSortDirection).toBe("desc");
});
it("uses aligned comments by default and preserves legacy comment visibility", () => {
expect(normalizeEditorSettings({}).sidebarObjectInfoMode).toBe("comment-aligned");
expect(normalizeEditorSettings({ sidebarObjectInfoMode: "comment-aligned" }).sidebarObjectInfoMode).toBe("comment-aligned");

View File

@ -41,6 +41,7 @@ import { dataTabExecutionDatabase } from "@/lib/table/dataTabExecutionDatabase";
import { tableOpenPageLimit } from "@/lib/table/tableOpenPageLimit";
import { getCachedTableMetadata, loadTableIndexes, loadTableMetadata, type TableMetadataRequest } from "@/lib/metadata/tableMetadataCache";
import { buildTableSelectSql, quoteTableDataIdentifier } from "@/lib/table/tableSelectSql";
import { applyOpenTableDefaultSortState, buildOpenTableDefaultSort } from "@/lib/table/tableDefaultSort";
import { connectionQueryExecutionSchema, connectionUsesDatabaseObjectTreeMode, effectiveDatabaseTypeForConnection, metadataSchemaForConnection } from "@/lib/database/jdbcDialect";
import { frontendQueryTimeoutSecsForSql, queryTimeoutSecsForConnection } from "@/lib/sql/queryTimeout";
import { queryResultNameFromPreamble, queryResultSourceLabel } from "@/lib/sql/queryResultSource";
@ -1103,6 +1104,8 @@ export const useQueryStore = defineStore("query", () => {
resultSortDirection: t.resultSortDirection,
resultSortMode: t.resultSortMode,
orderByInput: t.orderByInput,
openTableDefaultSortApplied: t.openTableDefaultSortApplied,
openTableDefaultSortOrderBy: t.openTableDefaultSortOrderBy,
resultPageLimit: t.resultPageLimit,
resultPageOffset: t.resultPageOffset,
whereInput: t.whereInput,
@ -1950,6 +1953,8 @@ export const useQueryStore = defineStore("query", () => {
resultLocalSortOriginalMongoDocuments: undefined,
resultLocalSortOriginalMongoCopyDocuments: undefined,
orderByInput: undefined,
openTableDefaultSortApplied: undefined,
openTableDefaultSortOrderBy: undefined,
resultPageSql: undefined,
resultPageLimit: undefined,
resultPageOffset: undefined,
@ -2078,8 +2083,18 @@ export const useQueryStore = defineStore("query", () => {
const effectiveDbType = effectiveDatabaseTypeForConnection(conn);
const identifierQuote = connStore.connectionIdentifierQuote?.(tab.connectionId);
const primaryKeys = tab.tableMeta ? tab.tableMeta.primaryKeys : tableMeta.primaryKeys;
const openTableDefaultSort = tab.openTableDefaultSortApplied
? buildOpenTableDefaultSort({
mode: settingsStore.editorSettings.openTableDefaultSortMode,
databaseType: effectiveDbType,
identifierQuote,
primaryKeys,
columns: tableMeta.columns.map((column) => column.name),
})
: undefined;
if (tab.openTableDefaultSortApplied) applyOpenTableDefaultSortState(tab, openTableDefaultSort);
const sortOrder = tab.resultSortColumn && tab.resultSortDirection ? `${quoteTableDataIdentifier(effectiveDbType, tab.resultSortColumn, identifierQuote)} ${tab.resultSortDirection.toUpperCase()}` : undefined;
const orderBy = tab.orderByInput?.trim() || sortOrder;
const orderBy = openTableDefaultSort?.orderBy ?? (tab.orderByInput?.trim() || sortOrder);
const limit = tab.resultPageLimit ?? tableOpenPageLimit(settingsStore.editorSettings.tableOpenPageSize);
const offset = tab.resultPageOffset ?? 0;
const refreshPreparationId = uuid();
@ -4616,8 +4631,18 @@ export const useQueryStore = defineStore("query", () => {
const effectiveDbType = effectiveDatabaseTypeForConnection(conn);
const identifierQuote = connStore.connectionIdentifierQuote?.(tab.connectionId);
const primaryKeys = tab.tableMeta ? tab.tableMeta.primaryKeys : tableMeta.primaryKeys;
const openTableDefaultSort = tab.openTableDefaultSortApplied
? buildOpenTableDefaultSort({
mode: settingsStore.editorSettings.openTableDefaultSortMode,
databaseType: effectiveDbType,
identifierQuote,
primaryKeys,
columns: tableMeta.columns.map((column) => column.name),
})
: undefined;
if (tab.openTableDefaultSortApplied) applyOpenTableDefaultSortState(tab, openTableDefaultSort);
const sortOrder = tab.resultSortColumn && tab.resultSortDirection ? `${quoteTableDataIdentifier(effectiveDbType, tab.resultSortColumn, identifierQuote)} ${tab.resultSortDirection.toUpperCase()}` : undefined;
const orderBy = tab.orderByInput?.trim() || sortOrder;
const orderBy = openTableDefaultSort?.orderBy ?? (tab.orderByInput?.trim() || sortOrder);
const queryTimeoutSecs = queryTimeoutSecsForConnection(conn);
const executionDatabase = dataTabExecutionDatabase(conn, tab.database, tableMeta.catalog);
const rows: QueryResult["rows"] = [];

View File

@ -49,6 +49,9 @@ export type InterfaceLayout = "separated" | "classic";
export type UpdateDownloadSource = "official" | "cnb";
export type SqlSemanticDiagnosticsMode = "auto" | "enabled" | "disabled";
export type OpenTableDefaultSortMode = "none" | "primary-key-asc" | "primary-key-desc";
export type DataGridQuickSortMode = "database" | "local";
export type DataGridQuickSortDirection = "asc" | "desc";
export type OpenTabsRestoreMode = "all" | "pinned" | "none";
export const DEFAULT_SIDEBAR_TABLE_PAGE_SIZE = 1000;
@ -434,6 +437,10 @@ export interface EditorSettings {
showColumnTypesInHeader: boolean;
compactColumnHeaderActions: boolean;
columnWidthDensity: ColumnWidthDensity;
openTableDefaultSortMode: OpenTableDefaultSortMode;
dataGridQuickSortEnabled: boolean;
dataGridQuickSortMode: DataGridQuickSortMode;
dataGridQuickSortDirection: DataGridQuickSortDirection;
dataGridQuickEntry: boolean;
dataGridRenderMode: DataGridRenderMode;
dataGridSearchMode: DataGridSearchMode;
@ -602,6 +609,10 @@ export const DEFAULT_EDITOR_SETTINGS: EditorSettings = {
showColumnTypesInHeader: true,
compactColumnHeaderActions: true,
columnWidthDensity: "standard",
openTableDefaultSortMode: "none",
dataGridQuickSortEnabled: false,
dataGridQuickSortMode: "local",
dataGridQuickSortDirection: "asc",
dataGridQuickEntry: false,
dataGridRenderMode: "canvas",
dataGridSearchMode: "filter",
@ -697,6 +708,18 @@ function normalizeDataGridRenderMode(value: unknown): DataGridRenderMode {
return DATA_GRID_RENDER_MODES.includes(value as DataGridRenderMode) ? (value as DataGridRenderMode) : DEFAULT_EDITOR_SETTINGS.dataGridRenderMode;
}
function normalizeOpenTableDefaultSortMode(value: unknown): OpenTableDefaultSortMode {
return value === "primary-key-asc" || value === "primary-key-desc" || value === "none" ? value : DEFAULT_EDITOR_SETTINGS.openTableDefaultSortMode;
}
function normalizeDataGridQuickSortMode(value: unknown): DataGridQuickSortMode {
return value === "database" || value === "local" ? value : DEFAULT_EDITOR_SETTINGS.dataGridQuickSortMode;
}
function normalizeDataGridQuickSortDirection(value: unknown): DataGridQuickSortDirection {
return value === "desc" ? "desc" : DEFAULT_EDITOR_SETTINGS.dataGridQuickSortDirection;
}
function normalizeDataGridSearchMode(value: unknown): DataGridSearchMode {
return DATA_GRID_SEARCH_MODES.includes(value as DataGridSearchMode) ? (value as DataGridSearchMode) : DEFAULT_EDITOR_SETTINGS.dataGridSearchMode;
}
@ -824,6 +847,11 @@ function normalizeTableInfoTab(value: unknown): TableInfoTab {
}
export function normalizeEditorSettings(settings: Partial<EditorSettings>, existing?: EditorSettings): EditorSettings {
const legacyDefaultSort = settings as Partial<EditorSettings> & {
defaultDataGridSortEnabled?: boolean;
defaultDataGridSortMode?: DataGridQuickSortMode;
defaultDataGridSortDirection?: DataGridQuickSortDirection;
};
const sqlSemanticDiagnosticsMode = normalizeSqlSemanticDiagnosticsMode(settings.sqlSemanticDiagnosticsMode, settings.sqlSemanticDiagnosticsEnabled);
const savedExecuteModeDefaultVersion = settings.executeModeDefaultVersion;
const executeModeDefaultVersion = typeof savedExecuteModeDefaultVersion === "number" && savedExecuteModeDefaultVersion >= EXECUTE_MODE_CURRENT_DEFAULT_VERSION ? savedExecuteModeDefaultVersion : EXECUTE_MODE_CURRENT_DEFAULT_VERSION;
@ -889,6 +917,10 @@ export function normalizeEditorSettings(settings: Partial<EditorSettings>, exist
showColumnTypesInHeader: settings.showColumnTypesInHeader ?? DEFAULT_EDITOR_SETTINGS.showColumnTypesInHeader,
compactColumnHeaderActions: settings.compactColumnHeaderActions ?? DEFAULT_EDITOR_SETTINGS.compactColumnHeaderActions,
columnWidthDensity: normalizeColumnWidthDensity(settings.columnWidthDensity),
openTableDefaultSortMode: normalizeOpenTableDefaultSortMode(settings.openTableDefaultSortMode),
dataGridQuickSortEnabled: typeof settings.dataGridQuickSortEnabled === "boolean" ? settings.dataGridQuickSortEnabled : legacyDefaultSort.defaultDataGridSortEnabled === true,
dataGridQuickSortMode: normalizeDataGridQuickSortMode(settings.dataGridQuickSortMode ?? legacyDefaultSort.defaultDataGridSortMode),
dataGridQuickSortDirection: normalizeDataGridQuickSortDirection(settings.dataGridQuickSortDirection ?? legacyDefaultSort.defaultDataGridSortDirection),
dataGridQuickEntry: settings.dataGridQuickEntry ?? DEFAULT_EDITOR_SETTINGS.dataGridQuickEntry,
dataGridRenderMode: normalizeDataGridRenderMode(settings.dataGridRenderMode),
dataGridSearchMode: normalizeDataGridSearchMode(settings.dataGridSearchMode),
@ -1330,6 +1362,10 @@ export const useSettingsStore = defineStore("settings", () => {
if (partial.showColumnTypesInHeader !== undefined) editorSettings.value.showColumnTypesInHeader = partial.showColumnTypesInHeader;
if (partial.compactColumnHeaderActions !== undefined) editorSettings.value.compactColumnHeaderActions = partial.compactColumnHeaderActions;
if (partial.columnWidthDensity !== undefined) editorSettings.value.columnWidthDensity = normalizeColumnWidthDensity(partial.columnWidthDensity);
if (partial.openTableDefaultSortMode !== undefined) editorSettings.value.openTableDefaultSortMode = normalizeOpenTableDefaultSortMode(partial.openTableDefaultSortMode);
if (partial.dataGridQuickSortEnabled !== undefined) editorSettings.value.dataGridQuickSortEnabled = partial.dataGridQuickSortEnabled === true;
if (partial.dataGridQuickSortMode !== undefined) editorSettings.value.dataGridQuickSortMode = normalizeDataGridQuickSortMode(partial.dataGridQuickSortMode);
if (partial.dataGridQuickSortDirection !== undefined) editorSettings.value.dataGridQuickSortDirection = normalizeDataGridQuickSortDirection(partial.dataGridQuickSortDirection);
if (partial.dataGridQuickEntry !== undefined) editorSettings.value.dataGridQuickEntry = partial.dataGridQuickEntry;
if (partial.dataGridRenderMode !== undefined) editorSettings.value.dataGridRenderMode = normalizeDataGridRenderMode(partial.dataGridRenderMode);
if (partial.dataGridSearchMode !== undefined) editorSettings.value.dataGridSearchMode = normalizeDataGridSearchMode(partial.dataGridSearchMode);

View File

@ -885,6 +885,8 @@ export interface QueryTab {
resultLocalSortOriginalMongoDocuments?: QueryResult["mongo_documents"];
resultLocalSortOriginalMongoCopyDocuments?: QueryResult["mongo_copy_documents"];
orderByInput?: string;
openTableDefaultSortApplied?: boolean;
openTableDefaultSortOrderBy?: string;
resultPageSql?: string;
resultPageLimit?: number;
resultPageOffset?: number;