feat(tabs): configurable tab bar position, grouping and sorting
This commit is contained in:
18 files changed
+728
-353
No files matched your search
+147
-139
@@ -573,6 +573,12 @@ useScheduledDatabaseBackups({ scheduler: true });
|
||||
|
||||
const appVersion = ref("");
|
||||
const isClassicLayout = computed(() => settingsStore.editorSettings.appLayout === "classic");
|
||||
const isVerticalTabPlacement = computed(() => settingsStore.editorSettings.tabPlacement === "left" || settingsStore.editorSettings.tabPlacement === "right");
|
||||
const tabWorkspaceLayoutClass = computed(() => {
|
||||
if (settingsStore.editorSettings.tabPlacement === "bottom") return "flex-col-reverse";
|
||||
if (settingsStore.editorSettings.tabPlacement === "right") return "flex-row-reverse";
|
||||
return isVerticalTabPlacement.value ? "flex-row" : "flex-col";
|
||||
});
|
||||
const updateNotificationsEnabled = computed(() => settingsStore.editorSettings.updateNotificationsEnabled);
|
||||
|
||||
function openSettings(initialTab = "appearance", initialSection?: string) {
|
||||
@@ -3046,7 +3052,7 @@ onUnmounted(() => {
|
||||
</div>
|
||||
|
||||
<div v-show="!isAiPanelMaximized || isZenMode" :class="isClassicLayout ? 'flex-1 min-w-0 overflow-hidden' : 'flex-1 min-w-0 overflow-hidden rounded-md border border-border/80 bg-background'">
|
||||
<div class="h-full flex flex-col min-w-0">
|
||||
<div class="h-full flex min-w-0" :class="tabWorkspaceLayoutClass">
|
||||
<AppTabBar
|
||||
ref="appTabBarRef"
|
||||
:driver-store-open="driverStoreTabOpen"
|
||||
@@ -3067,155 +3073,157 @@ onUnmounted(() => {
|
||||
@discard-all-tab-close="handleDiscardAllPendingTabClose"
|
||||
@cancel-tab-close="cancelPendingAppClose"
|
||||
/>
|
||||
<DriverStorePage v-if="driverStoreTabOpen" v-show="driverStoreActive" v-model:active-tab="driverStoreActiveTab" class="flex-1 min-h-0" :update-notifications-enabled="updateNotificationsEnabled" :focus-target="driverStoreFocus" @update-count-change="updateAgentDriverUpdateCount" />
|
||||
<EditorSettingsPage
|
||||
v-if="settingsPageTabOpen"
|
||||
v-show="settingsStore.settingsPageActive"
|
||||
variant="page"
|
||||
:open="settingsPageTabOpen"
|
||||
:initial-tab="settingsInitialTab"
|
||||
:initial-section="settingsInitialSection"
|
||||
:navigation-request-id="settingsNavigationRequestId"
|
||||
:ai-config-draft="settingsAiConfigDraft"
|
||||
:ai-config-request-id="settingsAiConfigRequestId"
|
||||
:app-version="appVersion"
|
||||
:checking-updates="checkingUpdates"
|
||||
class="flex-1 min-h-0"
|
||||
@update:open="(open: boolean) => (open ? activateSettingsPage() : closeSettingsPage())"
|
||||
@check-updates="checkUpdates()"
|
||||
/>
|
||||
<div v-if="activeTab" v-show="!driverStoreActive && !settingsStore.settingsPageActive" class="flex flex-col flex-1 min-h-0">
|
||||
<EditorToolbar
|
||||
v-if="activeTab.mode === 'query' && !isPreviewTab(activeTab)"
|
||||
:active-tab="activeTab"
|
||||
:active-connection="activeConnection"
|
||||
:executable-sql="executableSql"
|
||||
:explain-mode="explainMode"
|
||||
:block-dangerous-redis-commands="blockDangerousRedisCommands"
|
||||
:sql-keyword-case="settingsStore.editorSettings.sqlFormatter.keywordCase"
|
||||
:database-required-signal="databaseRequiredTabId === activeTab.id ? databaseRequiredSignal : 0"
|
||||
:auto-commit="activeTab.autoCommit ?? true"
|
||||
:txn-session-id="activeTab?.txnSessionId"
|
||||
:txn-auto-rolled-back="activeTab?.txnAutoRolledBack"
|
||||
:oracle-txn-possibly-dirty="activeTab?.oracleTxnPossiblyDirty"
|
||||
:is-oracle-manual-transaction="isOracleManualTransaction"
|
||||
@update:explain-mode="(m: 'explain' | 'autotrace') => (explainMode = m)"
|
||||
@update:block-dangerous-redis-commands="(v: boolean) => (blockDangerousRedisCommands = v)"
|
||||
@update:auto-commit="
|
||||
(v: boolean) => {
|
||||
if (activeTab) queryStore.setAutoCommit(activeTab.id, v);
|
||||
}
|
||||
"
|
||||
@commit="activeTab && queryStore.commitTransaction(activeTab.id)"
|
||||
@rollback="activeTab && queryStore.rollbackTransaction(activeTab.id)"
|
||||
@dismiss-txn-rolled-back="activeTab && (activeTab.txnAutoRolledBack = false)"
|
||||
@execute-pointer-down="captureActiveEditorExecutionSnapshot()"
|
||||
@execute="requestActiveEditorExecute($event)"
|
||||
@multi-execute="requestMultiDbExecute()"
|
||||
@cancel="cancelActiveExecution()"
|
||||
@explain="tryExplain()"
|
||||
@format-sql="formatActiveSql"
|
||||
@compress-sql="compressActiveSql"
|
||||
@toggle-sql-keyword-case="toggleSqlKeywordCase"
|
||||
@save-sql="void openSaveSqlDialog()"
|
||||
@open-sql="openSqlFile"
|
||||
@import-result-archive="importResultArchive"
|
||||
@paste-sql-in-condition="pasteClipboardAsSqlInCondition"
|
||||
@change-connection="changeActiveConnection"
|
||||
@change-database="changeActiveDatabase"
|
||||
@change-catalog="changeActiveCatalog"
|
||||
@change-schema="changeActiveSchema"
|
||||
@set-default-database="setActiveDatabaseAsDefault"
|
||||
@clear-default-database="clearActiveDefaultDatabase"
|
||||
<div class="flex min-h-0 min-w-0 flex-1 flex-col">
|
||||
<DriverStorePage v-if="driverStoreTabOpen" v-show="driverStoreActive" v-model:active-tab="driverStoreActiveTab" class="flex-1 min-h-0" :update-notifications-enabled="updateNotificationsEnabled" :focus-target="driverStoreFocus" @update-count-change="updateAgentDriverUpdateCount" />
|
||||
<EditorSettingsPage
|
||||
v-if="settingsPageTabOpen"
|
||||
v-show="settingsStore.settingsPageActive"
|
||||
variant="page"
|
||||
:open="settingsPageTabOpen"
|
||||
:initial-tab="settingsInitialTab"
|
||||
:initial-section="settingsInitialSection"
|
||||
:navigation-request-id="settingsNavigationRequestId"
|
||||
:ai-config-draft="settingsAiConfigDraft"
|
||||
:ai-config-request-id="settingsAiConfigRequestId"
|
||||
:app-version="appVersion"
|
||||
:checking-updates="checkingUpdates"
|
||||
class="flex-1 min-h-0"
|
||||
@update:open="(open: boolean) => (open ? activateSettingsPage() : closeSettingsPage())"
|
||||
@check-updates="checkUpdates()"
|
||||
/>
|
||||
<KeepAlive :max="4">
|
||||
<ContentArea
|
||||
ref="contentAreaRef"
|
||||
:key="activeTab.id"
|
||||
<div v-if="activeTab" v-show="!driverStoreActive && !settingsStore.settingsPageActive" class="flex flex-col flex-1 min-h-0">
|
||||
<EditorToolbar
|
||||
v-if="activeTab.mode === 'query' && !isPreviewTab(activeTab)"
|
||||
:active-tab="activeTab"
|
||||
:active-connection="activeConnection"
|
||||
:executable-sql="executableSql"
|
||||
:active-output-view="activeOutputView"
|
||||
:format-sql-request="formatSqlRequest"
|
||||
:compress-sql-request="compressSqlRequest"
|
||||
:selected-sql="selectedSql"
|
||||
:cursor-pos="cursorPos"
|
||||
:explain-mode="explainMode"
|
||||
:block-dangerous-redis-commands="blockDangerousRedisCommands"
|
||||
@update:active-output-view="activeOutputView = $event"
|
||||
@fix-with-ai="fixWithAi"
|
||||
@send-selection-to-ai="sendSelectionToAi"
|
||||
@execute="tryExecute($event)"
|
||||
@execute-in-new-result-tab="tryExecuteInNewResultTab($event)"
|
||||
:sql-keyword-case="settingsStore.editorSettings.sqlFormatter.keywordCase"
|
||||
:database-required-signal="databaseRequiredTabId === activeTab.id ? databaseRequiredSignal : 0"
|
||||
:auto-commit="activeTab.autoCommit ?? true"
|
||||
:txn-session-id="activeTab?.txnSessionId"
|
||||
:txn-auto-rolled-back="activeTab?.txnAutoRolledBack"
|
||||
:oracle-txn-possibly-dirty="activeTab?.oracleTxnPossiblyDirty"
|
||||
:is-oracle-manual-transaction="isOracleManualTransaction"
|
||||
@update:explain-mode="(m: 'explain' | 'autotrace') => (explainMode = m)"
|
||||
@update:block-dangerous-redis-commands="(v: boolean) => (blockDangerousRedisCommands = v)"
|
||||
@update:auto-commit="
|
||||
(v: boolean) => {
|
||||
if (activeTab) queryStore.setAutoCommit(activeTab.id, v);
|
||||
}
|
||||
"
|
||||
@commit="activeTab && queryStore.commitTransaction(activeTab.id)"
|
||||
@rollback="activeTab && queryStore.rollbackTransaction(activeTab.id)"
|
||||
@dismiss-txn-rolled-back="activeTab && (activeTab.txnAutoRolledBack = false)"
|
||||
@execute-pointer-down="captureActiveEditorExecutionSnapshot()"
|
||||
@execute="requestActiveEditorExecute($event)"
|
||||
@multi-execute="requestMultiDbExecute()"
|
||||
@cancel="cancelActiveExecution()"
|
||||
@explain="tryExplain()"
|
||||
@editor-update="(tabId: string, v: string) => queryStore.updateSql(tabId, v)"
|
||||
@editor-selection-change="(v: string) => (selectedSql = v)"
|
||||
@editor-cursor-change="(p: number) => (cursorPos = p)"
|
||||
@editor-viewport-change="(tabId: string, viewport: { scrollTop: number; scrollLeft: number }) => queryStore.updateEditorViewport(tabId, viewport)"
|
||||
@editor-selection-state-change="(tabId: string, selection: { anchor: number; head: number }) => queryStore.updateEditorSelection(tabId, selection)"
|
||||
@format-error="toast(t('toolbar.formatSqlFailed'))"
|
||||
@format-sql="formatActiveSql"
|
||||
@compress-sql="compressActiveSql"
|
||||
@toggle-sql-keyword-case="toggleSqlKeywordCase"
|
||||
@save-sql="void openSaveSqlDialog()"
|
||||
@reload="(sql, searchText, whereInput, orderBy, limit, offset, intent) => onReloadData(sql, searchText, whereInput, orderBy, limit, offset, intent)"
|
||||
@paginate="onPaginate"
|
||||
@sort="onSort"
|
||||
@execute-sql="onExecuteSql"
|
||||
@click-table="onClickTable"
|
||||
@view-table-data="onViewTableData"
|
||||
@edit-table-structure="onEditTableStructure"
|
||||
@view-table-ddl="onViewTableDdl"
|
||||
@open-object-source="onOpenObjectSource"
|
||||
@open-object-table="
|
||||
(target) =>
|
||||
activeTab &&
|
||||
openObjectBrowserTableTarget({
|
||||
connectionId: activeTab.connectionId,
|
||||
database: activeTab.database,
|
||||
schema: target.schema,
|
||||
catalog: target.catalog,
|
||||
tableName: target.tableName,
|
||||
tableType: target.tableType,
|
||||
})
|
||||
"
|
||||
@object-schema-change="(schema) => activeTab && queryStore.updateSchema(activeTab.id, schema)"
|
||||
@object-browser-viewport-change="(tabId, viewport) => queryStore.updateObjectBrowserViewport(tabId, viewport)"
|
||||
@structure-editor-saved="
|
||||
(commentChanged) =>
|
||||
activeTab &&
|
||||
onStructureEditorSaved(
|
||||
onReloadData,
|
||||
toast,
|
||||
{
|
||||
@open-sql="openSqlFile"
|
||||
@import-result-archive="importResultArchive"
|
||||
@paste-sql-in-condition="pasteClipboardAsSqlInCondition"
|
||||
@change-connection="changeActiveConnection"
|
||||
@change-database="changeActiveDatabase"
|
||||
@change-catalog="changeActiveCatalog"
|
||||
@change-schema="changeActiveSchema"
|
||||
@set-default-database="setActiveDatabaseAsDefault"
|
||||
@clear-default-database="clearActiveDefaultDatabase"
|
||||
/>
|
||||
<KeepAlive :max="4">
|
||||
<ContentArea
|
||||
ref="contentAreaRef"
|
||||
:key="activeTab.id"
|
||||
:active-tab="activeTab"
|
||||
:active-connection="activeConnection"
|
||||
:executable-sql="executableSql"
|
||||
:active-output-view="activeOutputView"
|
||||
:format-sql-request="formatSqlRequest"
|
||||
:compress-sql-request="compressSqlRequest"
|
||||
:selected-sql="selectedSql"
|
||||
:cursor-pos="cursorPos"
|
||||
:block-dangerous-redis-commands="blockDangerousRedisCommands"
|
||||
@update:active-output-view="activeOutputView = $event"
|
||||
@fix-with-ai="fixWithAi"
|
||||
@send-selection-to-ai="sendSelectionToAi"
|
||||
@execute="tryExecute($event)"
|
||||
@execute-in-new-result-tab="tryExecuteInNewResultTab($event)"
|
||||
@cancel="cancelActiveExecution()"
|
||||
@explain="tryExplain()"
|
||||
@editor-update="(tabId: string, v: string) => queryStore.updateSql(tabId, v)"
|
||||
@editor-selection-change="(v: string) => (selectedSql = v)"
|
||||
@editor-cursor-change="(p: number) => (cursorPos = p)"
|
||||
@editor-viewport-change="(tabId: string, viewport: { scrollTop: number; scrollLeft: number }) => queryStore.updateEditorViewport(tabId, viewport)"
|
||||
@editor-selection-state-change="(tabId: string, selection: { anchor: number; head: number }) => queryStore.updateEditorSelection(tabId, selection)"
|
||||
@format-error="toast(t('toolbar.formatSqlFailed'))"
|
||||
@save-sql="void openSaveSqlDialog()"
|
||||
@reload="(sql, searchText, whereInput, orderBy, limit, offset, intent) => onReloadData(sql, searchText, whereInput, orderBy, limit, offset, intent)"
|
||||
@paginate="onPaginate"
|
||||
@sort="onSort"
|
||||
@execute-sql="onExecuteSql"
|
||||
@click-table="onClickTable"
|
||||
@view-table-data="onViewTableData"
|
||||
@edit-table-structure="onEditTableStructure"
|
||||
@view-table-ddl="onViewTableDdl"
|
||||
@open-object-source="onOpenObjectSource"
|
||||
@open-object-table="
|
||||
(target) =>
|
||||
activeTab &&
|
||||
openObjectBrowserTableTarget({
|
||||
connectionId: activeTab.connectionId,
|
||||
database: activeTab.database,
|
||||
schema: activeTab.schema,
|
||||
catalog: activeTab.catalog,
|
||||
tableName: activeTab.structureTableName || '',
|
||||
},
|
||||
commentChanged,
|
||||
)
|
||||
"
|
||||
@structure-editor-close="activeTab && queryStore.closeTab(activeTab.id)"
|
||||
@open-settings="openSettings"
|
||||
@open-connection-settings="openConnectionSettings"
|
||||
/>
|
||||
</KeepAlive>
|
||||
schema: target.schema,
|
||||
catalog: target.catalog,
|
||||
tableName: target.tableName,
|
||||
tableType: target.tableType,
|
||||
})
|
||||
"
|
||||
@object-schema-change="(schema) => activeTab && queryStore.updateSchema(activeTab.id, schema)"
|
||||
@object-browser-viewport-change="(tabId, viewport) => queryStore.updateObjectBrowserViewport(tabId, viewport)"
|
||||
@structure-editor-saved="
|
||||
(commentChanged) =>
|
||||
activeTab &&
|
||||
onStructureEditorSaved(
|
||||
onReloadData,
|
||||
toast,
|
||||
{
|
||||
connectionId: activeTab.connectionId,
|
||||
database: activeTab.database,
|
||||
schema: activeTab.schema,
|
||||
catalog: activeTab.catalog,
|
||||
tableName: activeTab.structureTableName || '',
|
||||
},
|
||||
commentChanged,
|
||||
)
|
||||
"
|
||||
@structure-editor-close="activeTab && queryStore.closeTab(activeTab.id)"
|
||||
@open-settings="openSettings"
|
||||
@open-connection-settings="openConnectionSettings"
|
||||
/>
|
||||
</KeepAlive>
|
||||
</div>
|
||||
<WelcomeScreen
|
||||
v-else-if="!driverStoreActive && !settingsStore.settingsPageActive"
|
||||
:connection-stats="connectionStats"
|
||||
:recent-connections="recentConnections"
|
||||
:saved-sql-history-items="savedSqlHistoryItems"
|
||||
:app-version="appVersion"
|
||||
:has-connections="connectionStore.connections.length > 0"
|
||||
@open-connection-query="openConnectionQuery"
|
||||
@open-saved-sql="openSavedSqlFromWelcome"
|
||||
@new-connection="showConnectionDialog = true"
|
||||
@new-query="newQuery"
|
||||
@show-history="openRightSidebarPanel('history')"
|
||||
@import-config="dialogs.onImportClick"
|
||||
@open-github="openGitHub"
|
||||
@open-mcp-guide="openMcpGuide"
|
||||
/>
|
||||
</div>
|
||||
<WelcomeScreen
|
||||
v-else-if="!driverStoreActive && !settingsStore.settingsPageActive"
|
||||
:connection-stats="connectionStats"
|
||||
:recent-connections="recentConnections"
|
||||
:saved-sql-history-items="savedSqlHistoryItems"
|
||||
:app-version="appVersion"
|
||||
:has-connections="connectionStore.connections.length > 0"
|
||||
@open-connection-query="openConnectionQuery"
|
||||
@open-saved-sql="openSavedSqlFromWelcome"
|
||||
@new-connection="showConnectionDialog = true"
|
||||
@new-query="newQuery"
|
||||
@show-history="openRightSidebarPanel('history')"
|
||||
@import-config="dialogs.onImportClick"
|
||||
@open-github="openGitHub"
|
||||
@open-mcp-guide="openMcpGuide"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -80,6 +80,9 @@ import {
|
||||
type SidebarObjectInfoMode,
|
||||
type SqlSemanticDiagnosticsMode,
|
||||
type SavedSqlOpenTargetMode,
|
||||
type TabGroupMode,
|
||||
type TabPlacement,
|
||||
type TabSortMode,
|
||||
type UpdateDownloadSource,
|
||||
type CustomThemeColors,
|
||||
type CustomTheme,
|
||||
@@ -411,6 +414,9 @@ const editAppCloseUnsavedTabsMode = ref<AppCloseUnsavedTabsMode>(settingsStore.e
|
||||
const editSavedSqlOpenTargetMode = ref<SavedSqlOpenTargetMode>(settingsStore.editorSettings.savedSqlOpenTargetMode);
|
||||
const editAppLayout = ref(settingsStore.editorSettings.appLayout);
|
||||
const editTabLayout = ref(settingsStore.editorSettings.tabLayout);
|
||||
const editTabPlacement = ref<TabPlacement>(settingsStore.editorSettings.tabPlacement);
|
||||
const editTabGroupMode = ref<TabGroupMode>(settingsStore.editorSettings.tabGroupMode);
|
||||
const editTabSortMode = ref<TabSortMode>(settingsStore.editorSettings.tabSortMode);
|
||||
const editShowTrayIcon = ref(settingsStore.desktopSettings.show_tray_icon);
|
||||
const editQuitOnClose = ref(settingsStore.desktopSettings.quit_on_close);
|
||||
const desktopCloseBehaviorResetPending = ref(false);
|
||||
@@ -629,6 +635,9 @@ function currentEditorSettingsDraft(): EditorSettingsDraft {
|
||||
savedSqlOpenTargetMode: editSavedSqlOpenTargetMode.value,
|
||||
appLayout: editAppLayout.value,
|
||||
tabLayout: editTabLayout.value,
|
||||
tabPlacement: editTabPlacement.value,
|
||||
tabGroupMode: editTabGroupMode.value,
|
||||
tabSortMode: editTabSortMode.value,
|
||||
showColumnCommentsInHeader: editShowColumnCommentsInHeader.value,
|
||||
showColumnTypesInHeader: editShowColumnTypesInHeader.value,
|
||||
dataGridShowTransposeFieldMetadata: editDataGridShowTransposeFieldMetadata.value,
|
||||
@@ -1027,6 +1036,9 @@ function syncEditorSettingsDraftFromStore() {
|
||||
editSavedSqlOpenTargetMode.value = settingsStore.editorSettings.savedSqlOpenTargetMode;
|
||||
editAppLayout.value = settingsStore.editorSettings.appLayout;
|
||||
editTabLayout.value = settingsStore.editorSettings.tabLayout;
|
||||
editTabPlacement.value = settingsStore.editorSettings.tabPlacement;
|
||||
editTabGroupMode.value = settingsStore.editorSettings.tabGroupMode;
|
||||
editTabSortMode.value = settingsStore.editorSettings.tabSortMode;
|
||||
editShowColumnCommentsInHeader.value = settingsStore.editorSettings.showColumnCommentsInHeader;
|
||||
editShowColumnTypesInHeader.value = settingsStore.editorSettings.showColumnTypesInHeader;
|
||||
editDataGridShowTransposeFieldMetadata.value = settingsStore.editorSettings.dataGridShowTransposeFieldMetadata;
|
||||
@@ -1311,6 +1323,9 @@ function resetDefaultsForTab(tab: SettingsCategory) {
|
||||
editActiveCustomThemeId.value = DEFAULT_EDITOR_SETTINGS.activeCustomThemeId;
|
||||
editAppLayout.value = DEFAULT_EDITOR_SETTINGS.appLayout;
|
||||
editTabLayout.value = DEFAULT_EDITOR_SETTINGS.tabLayout;
|
||||
editTabPlacement.value = DEFAULT_EDITOR_SETTINGS.tabPlacement;
|
||||
editTabGroupMode.value = DEFAULT_EDITOR_SETTINGS.tabGroupMode;
|
||||
editTabSortMode.value = DEFAULT_EDITOR_SETTINGS.tabSortMode;
|
||||
editShowTrayIcon.value = DEFAULT_DESKTOP_SETTINGS.show_tray_icon;
|
||||
editQuitOnClose.value = DEFAULT_DESKTOP_SETTINGS.quit_on_close;
|
||||
desktopCloseBehaviorResetPending.value = true;
|
||||
@@ -1784,6 +1799,18 @@ function setTabLayout(value: "scroll" | "wrap") {
|
||||
editTabLayout.value = value;
|
||||
}
|
||||
|
||||
function setTabPlacement(value: TabPlacement) {
|
||||
editTabPlacement.value = value;
|
||||
}
|
||||
|
||||
function setTabGroupMode(value: TabGroupMode) {
|
||||
editTabGroupMode.value = value;
|
||||
}
|
||||
|
||||
function setTabSortMode(value: TabSortMode) {
|
||||
editTabSortMode.value = value;
|
||||
}
|
||||
|
||||
function setSidebarActivation(value: "single" | "double") {
|
||||
editSidebarActivation.value = value;
|
||||
}
|
||||
@@ -5284,6 +5311,49 @@ onUnmounted(() => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settings-appearance-group">
|
||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-3">
|
||||
<div class="space-y-2">
|
||||
<Label>{{ t("settings.tabPlacement") }}</Label>
|
||||
<Select :model-value="editTabPlacement" @update:model-value="setTabPlacement($event as TabPlacement)">
|
||||
<SelectTrigger class="w-full"><SelectValue /></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="top">{{ t("settings.tabPlacementTop") }}</SelectItem>
|
||||
<SelectItem value="bottom">{{ t("settings.tabPlacementBottom") }}</SelectItem>
|
||||
<SelectItem value="left">{{ t("settings.tabPlacementLeft") }}</SelectItem>
|
||||
<SelectItem value="right">{{ t("settings.tabPlacementRight") }}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label>{{ t("settings.tabGroup") }}</Label>
|
||||
<Select :model-value="editTabGroupMode" @update:model-value="setTabGroupMode($event as TabGroupMode)">
|
||||
<SelectTrigger class="w-full"><SelectValue /></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="none">{{ t("settings.tabGroupNone") }}</SelectItem>
|
||||
<SelectItem value="database-type">{{ t("settings.tabGroupDatabaseType") }}</SelectItem>
|
||||
<SelectItem value="connection">{{ t("settings.tabGroupConnection") }}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label>{{ t("settings.tabSort") }}</Label>
|
||||
<Select :model-value="editTabSortMode" @update:model-value="setTabSortMode($event as TabSortMode)">
|
||||
<SelectTrigger class="w-full"><SelectValue /></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="manual">{{ t("settings.tabSortManual") }}</SelectItem>
|
||||
<SelectItem value="created-asc">{{ t("settings.tabSortCreated") }}</SelectItem>
|
||||
<SelectItem value="title-asc">{{ t("settings.tabSortTitle") }}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-1 text-xs text-muted-foreground">
|
||||
<p>{{ t("settings.tabPlacementDescription") }}</p>
|
||||
<p>{{ t("settings.tabOrganizationDescription") }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="!isWeb" 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-tray-icon">{{ t("settings.showTrayIcon") }}</Label>
|
||||
|
||||
@@ -52,15 +52,60 @@ const connectionStore = useConnectionStore();
|
||||
const queryStore = useQueryStore();
|
||||
const settingsStore = useSettingsStore();
|
||||
const { toast } = useToast();
|
||||
const tabDrag = useTabDrag((draggedId, targetId, position) => {
|
||||
return queryStore.reorderTab(draggedId, targetId, position);
|
||||
});
|
||||
const editingTabId = ref<string | null>(null);
|
||||
const editingTitle = ref("");
|
||||
const isClassicLayout = computed(() => settingsStore.editorSettings.appLayout === "classic");
|
||||
const isWrapLayout = computed(() => settingsStore.editorSettings.tabLayout === "wrap");
|
||||
const fixedTabs = computed(() => queryStore.tabs.filter((tab) => tab.pinned));
|
||||
const regularTabs = computed(() => queryStore.tabs.filter((tab) => !tab.pinned));
|
||||
const isVerticalLayout = computed(() => settingsStore.editorSettings.tabPlacement === "left" || settingsStore.editorSettings.tabPlacement === "right");
|
||||
const isWrapLayout = computed(() => !isVerticalLayout.value && settingsStore.editorSettings.tabLayout === "wrap");
|
||||
const isManualTabOrder = computed(() => settingsStore.editorSettings.tabSortMode === "manual");
|
||||
const tabDrag = useTabDrag(
|
||||
(draggedId, targetId, position) => queryStore.reorderTab(draggedId, targetId, position),
|
||||
() => (isVerticalLayout.value ? "vertical" : "horizontal"),
|
||||
);
|
||||
|
||||
function tabGroupKey(tab: QueryTab) {
|
||||
const connection = connectionStore.getConfig(tab.connectionId);
|
||||
if (settingsStore.editorSettings.tabGroupMode === "connection") return tab.connectionId;
|
||||
return connection?.driver_profile || connection?.db_type || tab.connectionId || "unknown";
|
||||
}
|
||||
|
||||
function sortDisplayedTabs(tabs: QueryTab[]) {
|
||||
const sortMode = settingsStore.editorSettings.tabSortMode;
|
||||
const groupMode = settingsStore.editorSettings.tabGroupMode;
|
||||
return tabs
|
||||
.map((tab, index) => ({ tab, index }))
|
||||
.sort((left, right) => {
|
||||
if (groupMode !== "none") {
|
||||
const group = tabGroupKey(left.tab).localeCompare(tabGroupKey(right.tab), undefined, { sensitivity: "base", numeric: true });
|
||||
if (group) return group;
|
||||
}
|
||||
if (sortMode === "manual") return left.index - right.index;
|
||||
if (sortMode === "created-asc") {
|
||||
const created = (left.tab.createdAt ?? left.index) - (right.tab.createdAt ?? right.index);
|
||||
if (created) return created;
|
||||
} else {
|
||||
const title = tabTitleText(left.tab).localeCompare(tabTitleText(right.tab), undefined, { sensitivity: "base", numeric: true });
|
||||
if (title) return title;
|
||||
}
|
||||
return left.index - right.index;
|
||||
})
|
||||
.map(({ tab }) => tab);
|
||||
}
|
||||
|
||||
const fixedTabs = computed(() => sortDisplayedTabs(queryStore.tabs.filter((tab) => tab.pinned)));
|
||||
const regularTabs = computed(() => sortDisplayedTabs(queryStore.tabs.filter((tab) => !tab.pinned)));
|
||||
const displayedTabs = computed(() => [...fixedTabs.value, ...regularTabs.value]);
|
||||
|
||||
function isGroupStart(tabs: QueryTab[], index: number) {
|
||||
if (settingsStore.editorSettings.tabGroupMode === "none") return false;
|
||||
return index === 0 || tabGroupKey(tabs[index - 1]!) !== tabGroupKey(tabs[index]!);
|
||||
}
|
||||
|
||||
function tabGroupLabel(tab: QueryTab) {
|
||||
const connection = connectionStore.getConfig(tab.connectionId);
|
||||
if (settingsStore.editorSettings.tabGroupMode === "connection") return connection?.name || tab.connectionId;
|
||||
return connection?.driver_label || connection?.driver_profile || connection?.db_type || tab.connectionId;
|
||||
}
|
||||
const hasFixedTabs = computed(() => fixedTabs.value.length > 0);
|
||||
const regularSurfaceCount = computed(() => regularTabs.value.length + (props.driverStoreOpen ? 1 : 0) + (props.settingsPageOpen ? 1 : 0));
|
||||
const closeConfirmDirtyCount = computed(() => queryStore.closeConfirmDirtyTabIds.length);
|
||||
@@ -202,7 +247,9 @@ function hasTabsToRight(tab: QueryTab) {
|
||||
|
||||
function closeTabsToRightFromTab(tab: QueryTab) {
|
||||
const shouldActivateTarget = !tab.pinned && (!!props.settingsPageActive || !!props.driverStoreActive);
|
||||
queryStore.closeRightTabs(tab.id, () => {
|
||||
const tabsToClose = tabsToRightInGroup(tab).map((item) => item.id);
|
||||
const finalActiveTabId = queryStore.activeTabId && !tabsToClose.includes(queryStore.activeTabId) ? queryStore.activeTabId : tab.id;
|
||||
queryStore.closeTabsByIds(tabsToClose, finalActiveTabId, () => {
|
||||
if (tab.pinned) return;
|
||||
closeSpecialRegularSurfaces();
|
||||
if (shouldActivateTarget) activateTab(tab.id);
|
||||
@@ -220,7 +267,12 @@ function hasTabsToLeft(tab: QueryTab) {
|
||||
}
|
||||
|
||||
function closeTabsToLeftFromTab(tab: QueryTab) {
|
||||
queryStore.closeLeftTabs(tab.id);
|
||||
// Mirror the close-right path: compute the ids from the sorted display
|
||||
// order (fixedTabs/regularTabs), not the underlying array order, so the
|
||||
// menu closes exactly the tabs shown to the left of the target.
|
||||
const tabsToClose = tabsToLeftInGroup(tab).map((item) => item.id);
|
||||
const finalActiveTabId = queryStore.activeTabId && !tabsToClose.includes(queryStore.activeTabId) ? queryStore.activeTabId : tab.id;
|
||||
queryStore.closeTabsByIds(tabsToClose, finalActiveTabId);
|
||||
}
|
||||
|
||||
function hasSpecialRegularSurfaceToRight(surface: SpecialRegularSurface) {
|
||||
@@ -448,7 +500,9 @@ watch(
|
||||
if (!container) continue;
|
||||
const activeEl = container.querySelector('[data-active-tab="true"]');
|
||||
if (activeEl) {
|
||||
activeEl.scrollIntoView({ behavior: tabScrollBehavior.value, block: "nearest", inline: activeTabScrollInline(container, queryStore.activeTabId) });
|
||||
activeEl.scrollIntoView(
|
||||
isVerticalLayout.value ? { behavior: tabScrollBehavior.value, block: activeTabScrollInline(container, queryStore.activeTabId), inline: "nearest" } : { behavior: tabScrollBehavior.value, block: "nearest", inline: activeTabScrollInline(container, queryStore.activeTabId) },
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -469,7 +523,7 @@ watch(
|
||||
if (!container) return;
|
||||
const el = container.querySelector("[data-driver-store-tab]");
|
||||
if (el) {
|
||||
el.scrollIntoView({ behavior: "smooth", block: "nearest", inline: "center" });
|
||||
el.scrollIntoView(isVerticalLayout.value ? { behavior: "smooth", block: "center", inline: "nearest" } : { behavior: "smooth", block: "nearest", inline: "center" });
|
||||
}
|
||||
updateAllScrollButtons();
|
||||
});
|
||||
@@ -486,7 +540,7 @@ watch(
|
||||
if (!container) return;
|
||||
const el = container.querySelector("[data-settings-page-tab]");
|
||||
if (el) {
|
||||
el.scrollIntoView({ behavior: "smooth", block: "nearest", inline: "center" });
|
||||
el.scrollIntoView(isVerticalLayout.value ? { behavior: "smooth", block: "center", inline: "nearest" } : { behavior: "smooth", block: "nearest", inline: "center" });
|
||||
}
|
||||
updateAllScrollButtons();
|
||||
});
|
||||
@@ -550,19 +604,42 @@ function tabDatabaseIconType(tab: QueryTab) {
|
||||
return connection.driver_profile || connection.db_type;
|
||||
}
|
||||
|
||||
const showRegularTabScrollbar = computed(() => hasTabOverflow.value && !isWrapLayout.value);
|
||||
const showFixedTabScrollbar = computed(() => hasFixedTabOverflow.value && !isWrapLayout.value);
|
||||
const showRegularTabOverflowControls = computed(() => regularTabs.value.length > 0 && hasTabOverflow.value && !isWrapLayout.value);
|
||||
const showRegularTabScrollbar = computed(() => hasTabOverflow.value && !isWrapLayout.value && !isVerticalLayout.value);
|
||||
const showFixedTabScrollbar = computed(() => hasFixedTabOverflow.value && !isWrapLayout.value && !isVerticalLayout.value);
|
||||
const showRegularTabOverflowControls = computed(() => regularTabs.value.length > 0 && hasTabOverflow.value && !isWrapLayout.value && !isVerticalLayout.value);
|
||||
const regularTabOverflowOpen = ref(false);
|
||||
const fixedTabOverflowOpen = ref(false);
|
||||
const tabSearchQuery = ref("");
|
||||
const filteredOpenTabs = computed(() => {
|
||||
const query = tabSearchQuery.value.trim().toLocaleLowerCase();
|
||||
if (!query) return queryStore.tabs;
|
||||
return queryStore.tabs.filter((tab) => tabTitleText(tab).toLocaleLowerCase().includes(query) || tab.title.toLocaleLowerCase().includes(query));
|
||||
if (!query) return displayedTabs.value;
|
||||
return displayedTabs.value.filter((tab) => tabMatchesSearch(tab, query));
|
||||
});
|
||||
const tabBarClass = computed(() => [isClassicLayout.value ? "bg-muted" : "border-b bg-background", hasFixedTabs.value ? "flex-col" : "", isClassicLayout.value && hasFixedTabs.value ? "border-b" : ""]);
|
||||
const regularTabRowClass = computed(() => [isClassicLayout.value ? "h-9 items-stretch" : "h-10 items-center px-2", isClassicLayout.value && !hasFixedTabs.value ? "border-b" : ""]);
|
||||
|
||||
function tabMatchesSearch(tab: QueryTab, query: string) {
|
||||
const connectionName = connectionStore.getConfig(tab.connectionId)?.name || "";
|
||||
return tabTitleText(tab).toLocaleLowerCase().includes(query) || tab.title.toLocaleLowerCase().includes(query) || connectionName.toLocaleLowerCase().includes(query);
|
||||
}
|
||||
|
||||
const filteredRegularTabs = computed(() => {
|
||||
const query = tabSearchQuery.value.trim().toLocaleLowerCase();
|
||||
return query ? regularTabs.value.filter((tab) => tabMatchesSearch(tab, query)) : regularTabs.value;
|
||||
});
|
||||
const filteredFixedTabs = computed(() => {
|
||||
const query = tabSearchQuery.value.trim().toLocaleLowerCase();
|
||||
return query ? fixedTabs.value.filter((tab) => tabMatchesSearch(tab, query)) : fixedTabs.value;
|
||||
});
|
||||
const hasVisibleFixedTabs = computed(() => filteredFixedTabs.value.length > 0);
|
||||
const tabBarClass = computed(() => [
|
||||
isVerticalLayout.value
|
||||
? `vertical-tab-layout h-full w-60 flex-col bg-background ${settingsStore.editorSettings.tabPlacement === "right" ? "border-l" : "border-r"}`
|
||||
: isClassicLayout.value
|
||||
? "w-full bg-muted"
|
||||
: `w-full bg-background ${settingsStore.editorSettings.tabPlacement === "bottom" ? "border-t" : "border-b"}`,
|
||||
hasFixedTabs.value ? "flex-col" : "",
|
||||
isClassicLayout.value && hasFixedTabs.value && !isVerticalLayout.value ? "border-b" : "",
|
||||
]);
|
||||
const regularTabRowClass = computed(() => (isVerticalLayout.value ? ["min-h-0 flex-1 flex-col items-stretch"] : [isClassicLayout.value ? "h-9 items-stretch" : "h-10 items-center px-2", isClassicLayout.value && !hasFixedTabs.value ? "border-b" : ""]));
|
||||
|
||||
watch(regularTabOverflowOpen, (open) => {
|
||||
tabSearchQuery.value = "";
|
||||
@@ -617,10 +694,11 @@ function handleTabMouseDown(event: PointerEvent, tabId: string) {
|
||||
// overflowing tab bar.
|
||||
if (event.pointerType !== "touch") event.preventDefault();
|
||||
}
|
||||
tabDrag.startDrag(event, tabId);
|
||||
if (isManualTabOrder.value) tabDrag.startDrag(event, tabId);
|
||||
}
|
||||
|
||||
function handleTabDragTarget(event: MouseEvent, tab: QueryTab) {
|
||||
if (!isManualTabOrder.value) return;
|
||||
const draggedTab = queryStore.tabs.find((item) => item.id === tabDrag.state.draggedId);
|
||||
if (draggedTab && draggedTab.pinned !== tab.pinned) {
|
||||
tabDrag.clearTarget(tab.id);
|
||||
@@ -634,10 +712,8 @@ function tabDropStyle(tabId: string) {
|
||||
if (tabDrag.state.draggedId === tabId) return { opacity: 0.4 };
|
||||
if (tabDrag.state.targetId !== tabId) return {};
|
||||
const dropColor = `var(--ring)`;
|
||||
if (tabDrag.state.dropPosition === "before") {
|
||||
return { boxShadow: `inset 3px 0 0 0 ${dropColor}` };
|
||||
}
|
||||
return { boxShadow: `inset -3px 0 0 0 ${dropColor}` };
|
||||
if (isVerticalLayout.value) return tabDrag.state.dropPosition === "before" ? { boxShadow: `inset 0 3px 0 0 ${dropColor}` } : { boxShadow: `inset 0 -3px 0 0 ${dropColor}` };
|
||||
return tabDrag.state.dropPosition === "before" ? { boxShadow: `inset 3px 0 0 0 ${dropColor}` } : { boxShadow: `inset -3px 0 0 0 ${dropColor}` };
|
||||
}
|
||||
|
||||
const tabsContainerStyle = computed<CSSProperties>(() => ({
|
||||
@@ -656,8 +732,8 @@ const fixedTabScrollbarThumbStyle = computed<CSSProperties>(() => ({
|
||||
width: `${fixedScrollThumbWidthPercent.value}%`,
|
||||
}));
|
||||
|
||||
const tabTailDragRegionClass = computed(() => (showRegularTabOverflowControls.value || isWrapLayout.value ? "w-0 flex-none self-stretch" : "min-w-8 flex-1 self-stretch"));
|
||||
const fixedTabTailDragRegionClass = computed(() => (showFixedTabScrollbar.value || isWrapLayout.value ? "w-0 flex-none self-stretch" : "min-w-8 flex-1 self-stretch"));
|
||||
const tabTailDragRegionClass = computed(() => (isVerticalLayout.value || showRegularTabOverflowControls.value || isWrapLayout.value ? "w-0 flex-none self-stretch" : "min-w-8 flex-1 self-stretch"));
|
||||
const fixedTabTailDragRegionClass = computed(() => (isVerticalLayout.value || showFixedTabScrollbar.value || isWrapLayout.value ? "w-0 flex-none self-stretch" : "min-w-8 flex-1 self-stretch"));
|
||||
|
||||
const tabOverflowControlClass = computed(() =>
|
||||
isClassicLayout.value
|
||||
@@ -697,7 +773,11 @@ function onOverflowItemKeydown(event: KeyboardEvent, tabId: string, kind: "regul
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="queryStore.tabs.length > 0 || driverStoreOpen || settingsPageOpen" class="app-tab-bar relative flex w-full min-w-0 shrink-0 overflow-hidden" :class="tabBarClass">
|
||||
<div v-if="queryStore.tabs.length > 0 || driverStoreOpen || settingsPageOpen" class="app-tab-bar relative flex min-w-0 shrink-0 overflow-hidden" :class="tabBarClass">
|
||||
<div v-if="isVerticalLayout" class="relative shrink-0 border-b p-2">
|
||||
<Search class="pointer-events-none absolute left-4 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-muted-foreground" />
|
||||
<Input v-model="tabSearchQuery" type="search" :placeholder="t('tabs.searchOpenTabs')" class="h-8 w-full pl-7 text-sm" />
|
||||
</div>
|
||||
<div class="flex w-full min-w-0 shrink-0 overflow-hidden" :class="regularTabRowClass">
|
||||
<div class="app-tab-strip relative h-full min-w-0 flex-1 overflow-hidden">
|
||||
<div v-if="showRegularTabScrollbar" class="app-tab-scrollbar" :class="{ 'app-tab-scrollbar--dragging': isScrollbarDragging }" @pointerdown="startScrollbarDrag">
|
||||
@@ -705,89 +785,96 @@ function onOverflowItemKeydown(event: KeyboardEvent, tabId: string, kind: "regul
|
||||
</div>
|
||||
<div
|
||||
ref="tabsContainerRef"
|
||||
class="app-tab-scroll flex w-full min-w-0 flex-1 items-center overflow-x-auto"
|
||||
:class="[isClassicLayout ? 'h-full' : 'h-full gap-1.5 py-1.5', isWrapLayout ? 'wrap-mode' : '', isWrapLayout && isClassicLayout ? 'classic-wrap' : '']"
|
||||
class="app-tab-scroll flex w-full min-w-0 flex-1"
|
||||
:class="[
|
||||
isVerticalLayout ? 'flex-col items-stretch overflow-y-auto overflow-x-hidden py-1' : isClassicLayout ? 'h-full items-center overflow-x-auto' : 'h-full items-center gap-1.5 overflow-x-auto py-1.5',
|
||||
isWrapLayout ? 'wrap-mode' : '',
|
||||
isWrapLayout && isClassicLayout ? 'classic-wrap' : '',
|
||||
]"
|
||||
:style="tabsContainerStyle"
|
||||
@scroll="updateScrollButtons"
|
||||
@wheel="onTabsWheel"
|
||||
@scroll="!isVerticalLayout && updateScrollButtons()"
|
||||
@wheel="!isVerticalLayout && onTabsWheel($event)"
|
||||
>
|
||||
<CustomContextMenu v-for="tab in regularTabs" :key="tab.id" :items="getTabMenuItems(tab)" v-slot="{ onContextMenu }">
|
||||
<div :class="isClassicLayout ? 'h-full' : ''" @contextmenu="onContextMenu">
|
||||
<Tooltip>
|
||||
<TooltipTrigger as-child>
|
||||
<div
|
||||
class="app-tab-pill group flex cursor-default items-center gap-1 px-2 text-xs transition-colors whitespace-nowrap select-none"
|
||||
:class="
|
||||
isClassicLayout
|
||||
? [
|
||||
compactTabTitle ? 'min-w-24' : 'min-w-38',
|
||||
'h-full border-r border-border/80 font-medium dark:border-border/45',
|
||||
tab.id === queryStore.activeTabId && !driverStoreActive && !settingsPageActive ? 'bg-background text-foreground' : 'text-foreground/70 hover:text-foreground/90',
|
||||
]
|
||||
: [compactTabTitle ? 'min-w-24' : 'min-w-38', 'h-7 rounded-md border', tab.id === queryStore.activeTabId && !driverStoreActive && !settingsPageActive ? 'text-foreground font-medium' : 'border-border/60 text-foreground/70 hover:border-border hover:text-foreground/90']
|
||||
"
|
||||
:style="[tabColorStyle(tab), tabDropStyle(tab.id)]"
|
||||
:data-active-tab="tab.id === queryStore.activeTabId && !driverStoreActive && !settingsPageActive"
|
||||
@click="handleTabClick(tab)"
|
||||
@dblclick="handleTabDoubleClick(tab, $event)"
|
||||
@mousedown.middle.prevent="queryStore.closeTab(tab.id)"
|
||||
@pointerdown="handleTabMouseDown($event, tab.id)"
|
||||
@mouseenter="handleTabDragTarget($event, tab)"
|
||||
@mousemove="handleTabDragTarget($event, tab)"
|
||||
@mouseleave="tabDrag.clearTarget(tab.id)"
|
||||
>
|
||||
<TabExecutionStatus :tab="tab">
|
||||
<span class="shrink-0" :class="tabIconClass(tab)">
|
||||
<AlertTriangle v-if="tab.externalSqlFileMissing" class="h-3.5 w-3.5" />
|
||||
<Table2 v-else-if="tab.mode === 'data' || tab.mode === 'mongo' || tab.mode === 'redis' || tab.mode === 'hbase'" class="h-3.5 w-3.5" />
|
||||
<DatabaseIcon v-else-if="tab.mode === 'mq'" :db-type="tabDatabaseIconType(tab)" class="h-3.5 w-3.5" />
|
||||
<TableProperties v-else-if="tab.mode === 'vector'" class="h-3.5 w-3.5" />
|
||||
<KeyRound v-else-if="tab.mode === 'etcd' || tab.mode === 'zookeeper' || tab.mode === 'consul'" class="h-3.5 w-3.5" />
|
||||
<Gauge v-else-if="tab.mode === 'consul-overview'" class="h-3.5 w-3.5" />
|
||||
<Gauge v-else-if="tab.mode === 'etcd-dashboard'" class="h-3.5 w-3.5" />
|
||||
<ShieldCheck v-else-if="tab.mode === 'etcd-access-control'" class="h-3.5 w-3.5" />
|
||||
<Network v-else-if="tab.mode === 'nacos'" class="h-3.5 w-3.5" />
|
||||
<Database v-else-if="tab.mode === 'databases'" class="h-3.5 w-3.5" />
|
||||
<TableProperties v-else-if="tab.mode === 'objects'" class="h-3.5 w-3.5" />
|
||||
<PencilRuler v-else-if="tab.mode === 'structure'" class="h-3.5 w-3.5" />
|
||||
<CalendarClock v-else-if="tab.mode === 'dameng-jobs'" class="h-3.5 w-3.5" />
|
||||
<Activity v-else-if="tab.mode === 'processlist' || tab.mode === 'sqlserver-trace'" class="h-3.5 w-3.5" />
|
||||
<Gauge v-else-if="tab.mode === 'mysql-dashboard' || tab.mode === 'postgres-dashboard' || tab.mode === 'nacos-dashboard'" class="h-3.5 w-3.5" />
|
||||
<GitBranch v-else-if="tab.mode === 'dolt-version-control'" class="h-3.5 w-3.5" />
|
||||
<Code2 v-else class="h-3.5 w-3.5" />
|
||||
<template v-for="(tab, index) in filteredRegularTabs" :key="tab.id">
|
||||
<div v-if="isGroupStart(filteredRegularTabs, index)" class="tab-group-header">{{ tabGroupLabel(tab) }}</div>
|
||||
<CustomContextMenu :items="getTabMenuItems(tab)" v-slot="{ onContextMenu }">
|
||||
<div :class="isClassicLayout ? 'h-full' : ''" @contextmenu="onContextMenu">
|
||||
<Tooltip>
|
||||
<TooltipTrigger as-child>
|
||||
<div
|
||||
class="app-tab-pill group flex cursor-default items-center gap-1 px-2 text-xs transition-colors whitespace-nowrap select-none"
|
||||
:class="
|
||||
isClassicLayout
|
||||
? [
|
||||
compactTabTitle ? 'min-w-24' : 'min-w-38',
|
||||
'h-full border-r border-border/80 font-medium dark:border-border/45',
|
||||
tab.id === queryStore.activeTabId && !driverStoreActive && !settingsPageActive ? 'bg-background text-foreground' : 'text-foreground/70 hover:text-foreground/90',
|
||||
]
|
||||
: [compactTabTitle ? 'min-w-24' : 'min-w-38', 'h-7 rounded-md border', tab.id === queryStore.activeTabId && !driverStoreActive && !settingsPageActive ? 'text-foreground font-medium' : 'border-border/60 text-foreground/70 hover:border-border hover:text-foreground/90']
|
||||
"
|
||||
:style="[tabColorStyle(tab), tabDropStyle(tab.id)]"
|
||||
:data-active-tab="tab.id === queryStore.activeTabId && !driverStoreActive && !settingsPageActive"
|
||||
@click="handleTabClick(tab)"
|
||||
@dblclick="handleTabDoubleClick(tab, $event)"
|
||||
@mousedown.middle.prevent="queryStore.closeTab(tab.id)"
|
||||
@pointerdown="handleTabMouseDown($event, tab.id)"
|
||||
@mouseenter="handleTabDragTarget($event, tab)"
|
||||
@mousemove="handleTabDragTarget($event, tab)"
|
||||
@mouseleave="tabDrag.clearTarget(tab.id)"
|
||||
>
|
||||
<TabExecutionStatus :tab="tab">
|
||||
<span class="shrink-0" :class="tabIconClass(tab)">
|
||||
<AlertTriangle v-if="tab.externalSqlFileMissing" class="h-3.5 w-3.5" />
|
||||
<Table2 v-else-if="tab.mode === 'data' || tab.mode === 'mongo' || tab.mode === 'redis' || tab.mode === 'hbase'" class="h-3.5 w-3.5" />
|
||||
<DatabaseIcon v-else-if="tab.mode === 'mq'" :db-type="tabDatabaseIconType(tab)" class="h-3.5 w-3.5" />
|
||||
<TableProperties v-else-if="tab.mode === 'vector'" class="h-3.5 w-3.5" />
|
||||
<KeyRound v-else-if="tab.mode === 'etcd' || tab.mode === 'zookeeper' || tab.mode === 'consul'" class="h-3.5 w-3.5" />
|
||||
<Gauge v-else-if="tab.mode === 'consul-overview'" class="h-3.5 w-3.5" />
|
||||
<Gauge v-else-if="tab.mode === 'etcd-dashboard'" class="h-3.5 w-3.5" />
|
||||
<ShieldCheck v-else-if="tab.mode === 'etcd-access-control'" class="h-3.5 w-3.5" />
|
||||
<Network v-else-if="tab.mode === 'nacos'" class="h-3.5 w-3.5" />
|
||||
<Database v-else-if="tab.mode === 'databases'" class="h-3.5 w-3.5" />
|
||||
<TableProperties v-else-if="tab.mode === 'objects'" class="h-3.5 w-3.5" />
|
||||
<PencilRuler v-else-if="tab.mode === 'structure'" class="h-3.5 w-3.5" />
|
||||
<CalendarClock v-else-if="tab.mode === 'dameng-jobs'" class="h-3.5 w-3.5" />
|
||||
<Activity v-else-if="tab.mode === 'processlist' || tab.mode === 'sqlserver-trace'" class="h-3.5 w-3.5" />
|
||||
<Gauge v-else-if="tab.mode === 'mysql-dashboard' || tab.mode === 'postgres-dashboard' || tab.mode === 'nacos-dashboard'" class="h-3.5 w-3.5" />
|
||||
<GitBranch v-else-if="tab.mode === 'dolt-version-control'" class="h-3.5 w-3.5" />
|
||||
<Code2 v-else class="h-3.5 w-3.5" />
|
||||
</span>
|
||||
</TabExecutionStatus>
|
||||
<input
|
||||
v-if="editingTabId === tab.id"
|
||||
v-model="editingTitle"
|
||||
:data-tab-title-input="tab.id"
|
||||
:aria-label="t('contextMenu.renameTab')"
|
||||
class="h-5 min-w-0 flex-1 rounded border border-ring bg-background px-1.5 text-xs font-normal text-foreground outline-none"
|
||||
@click.stop
|
||||
@mousedown.stop
|
||||
@keydown.enter.prevent="commitRenameTab(tab)"
|
||||
@keydown.escape.prevent="cancelRenameTab"
|
||||
@blur="commitRenameTab(tab)"
|
||||
/>
|
||||
<span v-else class="inline-flex min-w-0 flex-1 items-center gap-0.5 overflow-hidden">
|
||||
<span v-if="isDirtyTab(tab)" aria-hidden="true" class="dirty-tab-marker">*</span>
|
||||
<span class="min-w-0 flex-1 truncate" :style="tabTitleStyle(tab)">{{ tabTitleText(tab) }}</span>
|
||||
</span>
|
||||
</TabExecutionStatus>
|
||||
<input
|
||||
v-if="editingTabId === tab.id"
|
||||
v-model="editingTitle"
|
||||
:data-tab-title-input="tab.id"
|
||||
:aria-label="t('contextMenu.renameTab')"
|
||||
class="h-5 min-w-0 flex-1 rounded border border-ring bg-background px-1.5 text-xs font-normal text-foreground outline-none"
|
||||
@click.stop
|
||||
@mousedown.stop
|
||||
@keydown.enter.prevent="commitRenameTab(tab)"
|
||||
@keydown.escape.prevent="cancelRenameTab"
|
||||
@blur="commitRenameTab(tab)"
|
||||
/>
|
||||
<span v-else class="inline-flex min-w-0 flex-1 items-center gap-0.5 overflow-hidden">
|
||||
<span v-if="isDirtyTab(tab)" aria-hidden="true" class="dirty-tab-marker">*</span>
|
||||
<span class="min-w-0 flex-1 truncate" :style="tabTitleStyle(tab)">{{ tabTitleText(tab) }}</span>
|
||||
</span>
|
||||
<ReadOnlySessionControl :connection-id="tab.connectionId" compact />
|
||||
<button class="rounded hover:bg-muted-foreground/20 p-0.5 shrink-0" @click.stop="queryStore.closeTab(tab.id)">
|
||||
<X class="h-3 w-3" />
|
||||
</button>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom" class="text-xs grid grid-cols-[auto_1fr] gap-x-2">
|
||||
<template v-for="line in tabTooltipLines(tab, t)" :key="line.label">
|
||||
<span class="text-muted-foreground">{{ line.label }}</span>
|
||||
<span>{{ line.value }}</span>
|
||||
</template>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</CustomContextMenu>
|
||||
<ReadOnlySessionControl :connection-id="tab.connectionId" compact />
|
||||
<button class="rounded hover:bg-muted-foreground/20 p-0.5 shrink-0" @click.stop="queryStore.closeTab(tab.id)">
|
||||
<X class="h-3 w-3" />
|
||||
</button>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom" class="text-xs grid grid-cols-[auto_1fr] gap-x-2">
|
||||
<template v-for="line in tabTooltipLines(tab, t)" :key="line.label">
|
||||
<span class="text-muted-foreground">{{ line.label }}</span>
|
||||
<span>{{ line.value }}</span>
|
||||
</template>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</CustomContextMenu>
|
||||
</template>
|
||||
|
||||
<!-- Settings Page Tab -->
|
||||
<CustomContextMenu v-if="settingsPageOpen" :items="getSpecialRegularTabMenuItems('settings')" v-slot="{ onContextMenu }">
|
||||
@@ -903,99 +990,110 @@ function onOverflowItemKeydown(event: KeyboardEvent, tabId: string, kind: "regul
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="hasFixedTabs" class="flex w-full min-w-0 shrink-0 overflow-hidden border-t" :class="isClassicLayout ? 'h-8 items-stretch border-border/80 bg-background/45 dark:border-border/45 dark:bg-background/20' : 'h-9 items-center border-border/70 bg-muted/45 px-2 dark:bg-muted/25'">
|
||||
<div
|
||||
v-if="hasVisibleFixedTabs"
|
||||
class="flex w-full min-w-0 shrink-0 overflow-hidden border-t"
|
||||
:class="isVerticalLayout ? 'max-h-[40%] flex-col border-border/70 bg-muted/45 py-1 dark:bg-muted/25' : isClassicLayout ? 'h-8 items-stretch border-border/80 bg-background/45 dark:border-border/45 dark:bg-background/20' : 'h-9 items-center border-border/70 bg-muted/45 px-2 dark:bg-muted/25'"
|
||||
>
|
||||
<div class="app-tab-strip relative h-full min-w-0 flex-1 overflow-hidden">
|
||||
<div v-if="showFixedTabScrollbar" class="app-tab-scrollbar app-tab-scrollbar--bottom" :class="{ 'app-tab-scrollbar--dragging': isFixedScrollbarDragging }" @pointerdown="startFixedScrollbarDrag">
|
||||
<div class="app-tab-scrollbar__thumb" :style="fixedTabScrollbarThumbStyle" />
|
||||
</div>
|
||||
<div
|
||||
ref="fixedTabsContainerRef"
|
||||
class="app-tab-scroll flex w-full min-w-0 flex-1 items-center overflow-x-auto"
|
||||
:class="[isClassicLayout ? 'h-full' : 'h-full gap-1.5 py-1', isWrapLayout ? 'wrap-mode' : '', isWrapLayout && isClassicLayout ? 'classic-wrap' : '']"
|
||||
class="app-tab-scroll flex w-full min-w-0 flex-1"
|
||||
:class="[
|
||||
isVerticalLayout ? 'flex-col items-stretch overflow-y-auto overflow-x-hidden py-1' : isClassicLayout ? 'h-full items-center overflow-x-auto' : 'h-full items-center gap-1.5 overflow-x-auto py-1',
|
||||
isWrapLayout ? 'wrap-mode' : '',
|
||||
isWrapLayout && isClassicLayout ? 'classic-wrap' : '',
|
||||
]"
|
||||
:style="tabsContainerStyle"
|
||||
@scroll="updateFixedScrollButtons"
|
||||
@wheel="onFixedTabsWheel"
|
||||
@scroll="!isVerticalLayout && updateFixedScrollButtons()"
|
||||
@wheel="!isVerticalLayout && onFixedTabsWheel($event)"
|
||||
>
|
||||
<CustomContextMenu v-for="tab in fixedTabs" :key="tab.id" :items="getTabMenuItems(tab)" v-slot="{ onContextMenu }">
|
||||
<div :class="isClassicLayout ? 'h-full' : ''" @contextmenu="onContextMenu">
|
||||
<Tooltip>
|
||||
<TooltipTrigger as-child>
|
||||
<div
|
||||
class="app-tab-pill group flex cursor-default items-center gap-1 px-2 text-xs transition-colors whitespace-nowrap select-none"
|
||||
:class="
|
||||
isClassicLayout
|
||||
? [
|
||||
compactTabTitle ? 'min-w-24' : 'min-w-38',
|
||||
'h-full border-r border-border/80 font-medium dark:border-border/45',
|
||||
tab.id === queryStore.activeTabId && !driverStoreActive && !settingsPageActive ? 'bg-background text-foreground' : 'text-foreground/70 hover:text-foreground/90',
|
||||
]
|
||||
: [compactTabTitle ? 'min-w-24' : 'min-w-38', 'h-7 rounded-md border', tab.id === queryStore.activeTabId && !driverStoreActive && !settingsPageActive ? 'text-foreground font-medium' : 'border-border/60 text-foreground/70 hover:border-border hover:text-foreground/90']
|
||||
"
|
||||
:style="[tabColorStyle(tab), tabDropStyle(tab.id)]"
|
||||
:data-active-tab="tab.id === queryStore.activeTabId && !driverStoreActive && !settingsPageActive"
|
||||
@click="handleTabClick(tab)"
|
||||
@dblclick="handleTabDoubleClick(tab, $event)"
|
||||
@mousedown.middle.prevent="queryStore.closeTab(tab.id)"
|
||||
@pointerdown="handleTabMouseDown($event, tab.id)"
|
||||
@mouseenter="handleTabDragTarget($event, tab)"
|
||||
@mousemove="handleTabDragTarget($event, tab)"
|
||||
@mouseleave="tabDrag.clearTarget(tab.id)"
|
||||
>
|
||||
<TabExecutionStatus :tab="tab">
|
||||
<span class="shrink-0" :class="tabIconClass(tab)">
|
||||
<AlertTriangle v-if="tab.externalSqlFileMissing" class="h-3.5 w-3.5" />
|
||||
<Table2 v-else-if="tab.mode === 'data' || tab.mode === 'mongo' || tab.mode === 'redis' || tab.mode === 'hbase'" class="h-3.5 w-3.5" />
|
||||
<DatabaseIcon v-else-if="tab.mode === 'mq'" :db-type="tabDatabaseIconType(tab)" class="h-3.5 w-3.5" />
|
||||
<TableProperties v-else-if="tab.mode === 'vector'" class="h-3.5 w-3.5" />
|
||||
<KeyRound v-else-if="tab.mode === 'etcd' || tab.mode === 'zookeeper' || tab.mode === 'consul'" class="h-3.5 w-3.5" />
|
||||
<Gauge v-else-if="tab.mode === 'consul-overview'" class="h-3.5 w-3.5" />
|
||||
<Gauge v-else-if="tab.mode === 'etcd-dashboard'" class="h-3.5 w-3.5" />
|
||||
<ShieldCheck v-else-if="tab.mode === 'etcd-access-control'" class="h-3.5 w-3.5" />
|
||||
<Network v-else-if="tab.mode === 'nacos'" class="h-3.5 w-3.5" />
|
||||
<Database v-else-if="tab.mode === 'databases'" class="h-3.5 w-3.5" />
|
||||
<TableProperties v-else-if="tab.mode === 'objects'" class="h-3.5 w-3.5" />
|
||||
<PencilRuler v-else-if="tab.mode === 'structure'" class="h-3.5 w-3.5" />
|
||||
<CalendarClock v-else-if="tab.mode === 'dameng-jobs'" class="h-3.5 w-3.5" />
|
||||
<Activity v-else-if="tab.mode === 'processlist' || tab.mode === 'sqlserver-trace'" class="h-3.5 w-3.5" />
|
||||
<Gauge v-else-if="tab.mode === 'mysql-dashboard' || tab.mode === 'postgres-dashboard' || tab.mode === 'nacos-dashboard'" class="h-3.5 w-3.5" />
|
||||
<GitBranch v-else-if="tab.mode === 'dolt-version-control'" class="h-3.5 w-3.5" />
|
||||
<Code2 v-else class="h-3.5 w-3.5" />
|
||||
<template v-for="(tab, index) in filteredFixedTabs" :key="tab.id">
|
||||
<div v-if="isGroupStart(filteredFixedTabs, index)" class="tab-group-header">{{ tabGroupLabel(tab) }}</div>
|
||||
<CustomContextMenu :items="getTabMenuItems(tab)" v-slot="{ onContextMenu }">
|
||||
<div :class="isClassicLayout ? 'h-full' : ''" @contextmenu="onContextMenu">
|
||||
<Tooltip>
|
||||
<TooltipTrigger as-child>
|
||||
<div
|
||||
class="app-tab-pill group flex cursor-default items-center gap-1 px-2 text-xs transition-colors whitespace-nowrap select-none"
|
||||
:class="
|
||||
isClassicLayout
|
||||
? [
|
||||
compactTabTitle ? 'min-w-24' : 'min-w-38',
|
||||
'h-full border-r border-border/80 font-medium dark:border-border/45',
|
||||
tab.id === queryStore.activeTabId && !driverStoreActive && !settingsPageActive ? 'bg-background text-foreground' : 'text-foreground/70 hover:text-foreground/90',
|
||||
]
|
||||
: [compactTabTitle ? 'min-w-24' : 'min-w-38', 'h-7 rounded-md border', tab.id === queryStore.activeTabId && !driverStoreActive && !settingsPageActive ? 'text-foreground font-medium' : 'border-border/60 text-foreground/70 hover:border-border hover:text-foreground/90']
|
||||
"
|
||||
:style="[tabColorStyle(tab), tabDropStyle(tab.id)]"
|
||||
:data-active-tab="tab.id === queryStore.activeTabId && !driverStoreActive && !settingsPageActive"
|
||||
@click="handleTabClick(tab)"
|
||||
@dblclick="handleTabDoubleClick(tab, $event)"
|
||||
@mousedown.middle.prevent="queryStore.closeTab(tab.id)"
|
||||
@pointerdown="handleTabMouseDown($event, tab.id)"
|
||||
@mouseenter="handleTabDragTarget($event, tab)"
|
||||
@mousemove="handleTabDragTarget($event, tab)"
|
||||
@mouseleave="tabDrag.clearTarget(tab.id)"
|
||||
>
|
||||
<TabExecutionStatus :tab="tab">
|
||||
<span class="shrink-0" :class="tabIconClass(tab)">
|
||||
<AlertTriangle v-if="tab.externalSqlFileMissing" class="h-3.5 w-3.5" />
|
||||
<Table2 v-else-if="tab.mode === 'data' || tab.mode === 'mongo' || tab.mode === 'redis' || tab.mode === 'hbase'" class="h-3.5 w-3.5" />
|
||||
<DatabaseIcon v-else-if="tab.mode === 'mq'" :db-type="tabDatabaseIconType(tab)" class="h-3.5 w-3.5" />
|
||||
<TableProperties v-else-if="tab.mode === 'vector'" class="h-3.5 w-3.5" />
|
||||
<KeyRound v-else-if="tab.mode === 'etcd' || tab.mode === 'zookeeper' || tab.mode === 'consul'" class="h-3.5 w-3.5" />
|
||||
<Gauge v-else-if="tab.mode === 'consul-overview'" class="h-3.5 w-3.5" />
|
||||
<Gauge v-else-if="tab.mode === 'etcd-dashboard'" class="h-3.5 w-3.5" />
|
||||
<ShieldCheck v-else-if="tab.mode === 'etcd-access-control'" class="h-3.5 w-3.5" />
|
||||
<Network v-else-if="tab.mode === 'nacos'" class="h-3.5 w-3.5" />
|
||||
<Database v-else-if="tab.mode === 'databases'" class="h-3.5 w-3.5" />
|
||||
<TableProperties v-else-if="tab.mode === 'objects'" class="h-3.5 w-3.5" />
|
||||
<PencilRuler v-else-if="tab.mode === 'structure'" class="h-3.5 w-3.5" />
|
||||
<CalendarClock v-else-if="tab.mode === 'dameng-jobs'" class="h-3.5 w-3.5" />
|
||||
<Activity v-else-if="tab.mode === 'processlist' || tab.mode === 'sqlserver-trace'" class="h-3.5 w-3.5" />
|
||||
<Gauge v-else-if="tab.mode === 'mysql-dashboard' || tab.mode === 'postgres-dashboard' || tab.mode === 'nacos-dashboard'" class="h-3.5 w-3.5" />
|
||||
<GitBranch v-else-if="tab.mode === 'dolt-version-control'" class="h-3.5 w-3.5" />
|
||||
<Code2 v-else class="h-3.5 w-3.5" />
|
||||
</span>
|
||||
</TabExecutionStatus>
|
||||
<input
|
||||
v-if="editingTabId === tab.id"
|
||||
v-model="editingTitle"
|
||||
:data-tab-title-input="tab.id"
|
||||
:aria-label="t('contextMenu.renameTab')"
|
||||
class="h-5 min-w-0 flex-1 rounded border border-ring bg-background px-1.5 text-xs font-normal text-foreground outline-none"
|
||||
@click.stop
|
||||
@mousedown.stop
|
||||
@keydown.enter.prevent="commitRenameTab(tab)"
|
||||
@keydown.escape.prevent="cancelRenameTab"
|
||||
@blur="commitRenameTab(tab)"
|
||||
/>
|
||||
<span v-else class="inline-flex min-w-0 flex-1 items-center gap-0.5 overflow-hidden text-foreground">
|
||||
<span v-if="isDirtyTab(tab)" aria-hidden="true" class="dirty-tab-marker">*</span>
|
||||
<span class="min-w-0 flex-1 truncate" :style="tabTitleStyle(tab)">{{ tabTitleText(tab) }}</span>
|
||||
</span>
|
||||
</TabExecutionStatus>
|
||||
<input
|
||||
v-if="editingTabId === tab.id"
|
||||
v-model="editingTitle"
|
||||
:data-tab-title-input="tab.id"
|
||||
:aria-label="t('contextMenu.renameTab')"
|
||||
class="h-5 min-w-0 flex-1 rounded border border-ring bg-background px-1.5 text-xs font-normal text-foreground outline-none"
|
||||
@click.stop
|
||||
@mousedown.stop
|
||||
@keydown.enter.prevent="commitRenameTab(tab)"
|
||||
@keydown.escape.prevent="cancelRenameTab"
|
||||
@blur="commitRenameTab(tab)"
|
||||
/>
|
||||
<span v-else class="inline-flex min-w-0 flex-1 items-center gap-0.5 overflow-hidden text-foreground">
|
||||
<span v-if="isDirtyTab(tab)" aria-hidden="true" class="dirty-tab-marker">*</span>
|
||||
<span class="min-w-0 flex-1 truncate" :style="tabTitleStyle(tab)">{{ tabTitleText(tab) }}</span>
|
||||
</span>
|
||||
<ReadOnlySessionControl :connection-id="tab.connectionId" compact />
|
||||
<button class="rounded p-0.5 text-primary hover:bg-muted-foreground/20 shrink-0" :aria-label="t('contextMenu.unfixTab')" :title="t('contextMenu.unfixTab')" @click.stop="queryStore.togglePinnedTab(tab.id)">
|
||||
<Pin class="h-3 w-3 fill-current" aria-hidden="true" />
|
||||
</button>
|
||||
<button class="rounded hover:bg-muted-foreground/20 p-0.5 shrink-0" @click.stop="queryStore.closeTab(tab.id)">
|
||||
<X class="h-3 w-3" />
|
||||
</button>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom" class="text-xs grid grid-cols-[auto_1fr] gap-x-2">
|
||||
<template v-for="line in tabTooltipLines(tab, t)" :key="line.label">
|
||||
<span class="text-muted-foreground">{{ line.label }}</span>
|
||||
<span>{{ line.value }}</span>
|
||||
</template>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</CustomContextMenu>
|
||||
<ReadOnlySessionControl :connection-id="tab.connectionId" compact />
|
||||
<button class="rounded p-0.5 text-primary hover:bg-muted-foreground/20 shrink-0" :aria-label="t('contextMenu.unfixTab')" :title="t('contextMenu.unfixTab')" @click.stop="queryStore.togglePinnedTab(tab.id)">
|
||||
<Pin class="h-3 w-3 fill-current" aria-hidden="true" />
|
||||
</button>
|
||||
<button class="rounded hover:bg-muted-foreground/20 p-0.5 shrink-0" @click.stop="queryStore.closeTab(tab.id)">
|
||||
<X class="h-3 w-3" />
|
||||
</button>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom" class="text-xs grid grid-cols-[auto_1fr] gap-x-2">
|
||||
<template v-for="line in tabTooltipLines(tab, t)" :key="line.label">
|
||||
<span class="text-muted-foreground">{{ line.label }}</span>
|
||||
<span>{{ line.value }}</span>
|
||||
</template>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</CustomContextMenu>
|
||||
</template>
|
||||
<div :class="fixedTabTailDragRegionClass" data-tauri-drag-region />
|
||||
</div>
|
||||
</div>
|
||||
@@ -1119,6 +1217,46 @@ function onOverflowItemKeydown(event: KeyboardEvent, tabId: string, kind: "regul
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.vertical-tab-layout {
|
||||
flex: 0 0 15rem;
|
||||
}
|
||||
|
||||
.vertical-tab-layout .app-tab-strip {
|
||||
height: auto;
|
||||
min-height: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.vertical-tab-layout .app-tab-pill {
|
||||
width: 100%;
|
||||
min-width: 0 !important;
|
||||
min-height: 2rem;
|
||||
height: 2rem !important;
|
||||
border-right: 0;
|
||||
}
|
||||
|
||||
.vertical-tab-layout .app-tab-scroll > * {
|
||||
width: 100%;
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.vertical-tab-layout .tab-group-header {
|
||||
width: 100%;
|
||||
padding: 0.45rem 0.6rem 0.2rem;
|
||||
color: var(--muted-foreground);
|
||||
font-size: 0.7rem;
|
||||
font-weight: 600;
|
||||
line-height: 1rem;
|
||||
text-transform: uppercase;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.app-tab-scroll:not(.wrap-mode) .tab-group-header {
|
||||
align-self: stretch;
|
||||
}
|
||||
|
||||
/* 父级 strip 容器在包含 wrap-mode 时也需要解除高度约束和裁剪 */
|
||||
.app-tab-strip:has(.wrap-mode) {
|
||||
height: auto !important;
|
||||
|
||||
@@ -98,7 +98,7 @@ describe("AppTabBar right-side close action", () => {
|
||||
});
|
||||
|
||||
it("waits for query tab confirmation before closing special surfaces", () => {
|
||||
expect(tabBarSource).toMatch(/queryStore\.closeRightTabs\(tab\.id, \(\) => \{[\s\S]*closeSpecialRegularSurfaces\(\);/);
|
||||
expect(tabBarSource).toMatch(/queryStore\.closeTabsByIds\(tabsToClose, finalActiveTabId, \(\) => \{[\s\S]*closeSpecialRegularSurfaces\(\);/);
|
||||
expect(tabBarSource).toContain("if (shouldActivateTarget) activateTab(tab.id)");
|
||||
});
|
||||
|
||||
@@ -142,7 +142,8 @@ describe("AppTabBar overflow search", () => {
|
||||
it("filters every open tab by its display and source titles", () => {
|
||||
expect(tabBarSource).toContain('const tabSearchQuery = ref("");');
|
||||
expect(tabBarSource).toContain("const filteredOpenTabs = computed(() => {");
|
||||
expect(tabBarSource).toContain("return queryStore.tabs.filter((tab) => tabTitleText(tab).toLocaleLowerCase().includes(query) || tab.title.toLocaleLowerCase().includes(query));");
|
||||
expect(tabBarSource).toContain("return displayedTabs.value.filter((tab) => tabMatchesSearch(tab, query));");
|
||||
expect(tabBarSource).toContain("return tabTitleText(tab).toLocaleLowerCase().includes(query) || tab.title.toLocaleLowerCase().includes(query) || connectionName.toLocaleLowerCase().includes(query);");
|
||||
});
|
||||
|
||||
it("provides the same focused search control and empty state in both overflow menus", () => {
|
||||
|
||||
@@ -35,6 +35,7 @@ let pending: {
|
||||
sourceEl: HTMLElement | null;
|
||||
} | null = null;
|
||||
let onDropCallback: ((draggedId: string, targetId: string, position: TabDropPosition) => boolean) | null = null;
|
||||
let dragAxis: () => "horizontal" | "vertical" = () => "horizontal";
|
||||
let ghostEl: HTMLElement | null = null;
|
||||
|
||||
function createGhost(sourceEl: HTMLElement, x: number, y: number) {
|
||||
@@ -82,8 +83,8 @@ function onPointerMove(event: PointerEvent) {
|
||||
if (!pending && !state.active) return;
|
||||
|
||||
if (pending && !state.active) {
|
||||
const dx = event.clientX - pending.x;
|
||||
if (Math.abs(dx) < TAB_DRAG_HORIZONTAL_THRESHOLD) return;
|
||||
const delta = dragAxis() === "vertical" ? event.clientY - pending.y : event.clientX - pending.x;
|
||||
if (Math.abs(delta) < TAB_DRAG_HORIZONTAL_THRESHOLD) return;
|
||||
state.active = true;
|
||||
state.draggedId = pending.id;
|
||||
state.startX = pending.x;
|
||||
@@ -131,9 +132,10 @@ function ensureListeners() {
|
||||
listenersAttached = true;
|
||||
}
|
||||
|
||||
export function useTabDrag(onDrop: (draggedId: string, targetId: string, position: TabDropPosition) => boolean) {
|
||||
export function useTabDrag(onDrop: (draggedId: string, targetId: string, position: TabDropPosition) => boolean, axis: () => "horizontal" | "vertical" = () => "horizontal") {
|
||||
ensureListeners();
|
||||
onDropCallback = onDrop;
|
||||
dragAxis = axis;
|
||||
|
||||
function startDrag(event: PointerEvent, tabId: string) {
|
||||
if (event.button !== 0) return;
|
||||
@@ -163,9 +165,10 @@ export function useTabDrag(onDrop: (draggedId: string, targetId: string, positio
|
||||
|
||||
const el = event.currentTarget as HTMLElement;
|
||||
const rect = el.getBoundingClientRect();
|
||||
const x = event.clientX - rect.left;
|
||||
|
||||
state.dropPosition = x < rect.width / 2 ? "before" : "after";
|
||||
const vertical = axis() === "vertical";
|
||||
const offset = vertical ? event.clientY - rect.top : event.clientX - rect.left;
|
||||
const size = vertical ? rect.height : rect.width;
|
||||
state.dropPosition = offset < size / 2 ? "before" : "after";
|
||||
}
|
||||
|
||||
function clearTarget(tabId: string) {
|
||||
|
||||
@@ -6355,6 +6355,21 @@ export default {
|
||||
tabLayoutScrollDescription: "Tabs scroll horizontally in one row when overflowing.",
|
||||
tabLayoutWrap: "Multi-row wrap",
|
||||
tabLayoutWrapDescription: "Tabs wrap to multiple rows to show all at once.",
|
||||
tabPlacement: "Tab bar position",
|
||||
tabPlacementTop: "Top",
|
||||
tabPlacementBottom: "Bottom",
|
||||
tabPlacementLeft: "Left side",
|
||||
tabPlacementRight: "Right side",
|
||||
tabPlacementDescription: "Side tab bars scroll vertically; multi-row wrapping applies only above or below content.",
|
||||
tabGroup: "Group by",
|
||||
tabGroupNone: "No grouping",
|
||||
tabGroupDatabaseType: "Group by database type",
|
||||
tabGroupConnection: "Group by connection",
|
||||
tabSort: "Sort by",
|
||||
tabSortManual: "Manual order",
|
||||
tabSortCreated: "Creation time",
|
||||
tabSortTitle: "Tab name",
|
||||
tabOrganizationDescription: "Grouping and sorting affect only the displayed order. Dragging is available in manual order.",
|
||||
iconTheme: "Icon and logo style",
|
||||
iconThemeDefault: "Default",
|
||||
iconThemeDefaultDescription: "Use the color DBX app icon.",
|
||||
|
||||
@@ -6031,6 +6031,21 @@ export default withEnglishFallback({
|
||||
tabLayoutScrollDescription: "Las pestañas se desplazan horizontalmente al desbordarse.",
|
||||
tabLayoutWrap: "Ajuste multilínea",
|
||||
tabLayoutWrapDescription: "Las pestañas se ajustan en varias filas para mostrarlas todas.",
|
||||
tabPlacement: "Posición de la barra de pestañas",
|
||||
tabPlacementTop: "Arriba",
|
||||
tabPlacementBottom: "Abajo",
|
||||
tabPlacementLeft: "Lado izquierdo",
|
||||
tabPlacementRight: "Lado derecho",
|
||||
tabPlacementDescription: "Las barras laterales se desplazan verticalmente; el ajuste multilínea solo se aplica arriba o abajo.",
|
||||
tabGroup: "Agrupar por",
|
||||
tabGroupNone: "Sin agrupación",
|
||||
tabGroupDatabaseType: "Agrupar por tipo de base de datos",
|
||||
tabGroupConnection: "Agrupar por conexión",
|
||||
tabSort: "Ordenar por",
|
||||
tabSortManual: "Orden manual",
|
||||
tabSortCreated: "Hora de creación",
|
||||
tabSortTitle: "Nombre de pestaña",
|
||||
tabOrganizationDescription: "La agrupación y la ordenación solo afectan al orden visible. Arrastre disponible en orden manual.",
|
||||
iconTheme: "Estilo de icono y logo",
|
||||
iconThemeDefault: "Predeterminado",
|
||||
iconThemeDefaultDescription: "Usar el icono de color de DBX.",
|
||||
|
||||
@@ -6031,6 +6031,21 @@ export default withEnglishFallback({
|
||||
tabLayoutScrollDescription: "Le schede scorrono orizzontalmente quando traboccano.",
|
||||
tabLayoutWrap: "Righe multiple",
|
||||
tabLayoutWrapDescription: "Le schede vanno a capo su più righe per mostrarle tutte.",
|
||||
tabPlacement: "Posizione barra schede",
|
||||
tabPlacementTop: "In alto",
|
||||
tabPlacementBottom: "In basso",
|
||||
tabPlacementLeft: "Lato sinistro",
|
||||
tabPlacementRight: "Lato destro",
|
||||
tabPlacementDescription: "Le barre laterali scorrono verticalmente; le righe multiple valgono solo sopra o sotto il contenuto.",
|
||||
tabGroup: "Raggruppa per",
|
||||
tabGroupNone: "Nessun raggruppamento",
|
||||
tabGroupDatabaseType: "Raggruppa per tipo di database",
|
||||
tabGroupConnection: "Raggruppa per connessione",
|
||||
tabSort: "Ordina per",
|
||||
tabSortManual: "Ordine manuale",
|
||||
tabSortCreated: "Data di creazione",
|
||||
tabSortTitle: "Nome scheda",
|
||||
tabOrganizationDescription: "Raggruppamento e ordinamento influenzano solo l'ordine visibile. Il trascinamento è disponibile nell'ordine manuale.",
|
||||
iconTheme: "Stile icona e logo",
|
||||
iconThemeDefault: "Predefinito",
|
||||
iconThemeDefaultDescription: "Utilizza l'icona colorata dell'app DBX.",
|
||||
|
||||
@@ -6069,6 +6069,21 @@ export default withEnglishFallback({
|
||||
tabLayoutScrollDescription: "タブが1行に収まらないとき水平スクロールします。",
|
||||
tabLayoutWrap: "複数行折り返し",
|
||||
tabLayoutWrapDescription: "タブが1行に収まらないとき折り返してすべて表示します。",
|
||||
tabPlacement: "タブバーの位置",
|
||||
tabPlacementTop: "上",
|
||||
tabPlacementBottom: "下",
|
||||
tabPlacementLeft: "左側",
|
||||
tabPlacementRight: "右側",
|
||||
tabPlacementDescription: "サイドのタブバーは縦にスクロールします。複数行表示はコンテンツの上下でのみ利用できます。",
|
||||
tabGroup: "グループ化",
|
||||
tabGroupNone: "グループ化しない",
|
||||
tabGroupDatabaseType: "データベース種別でグループ化",
|
||||
tabGroupConnection: "接続ごとにグループ化",
|
||||
tabSort: "並べ替え",
|
||||
tabSortManual: "手動順",
|
||||
tabSortCreated: "作成日時",
|
||||
tabSortTitle: "タブ名",
|
||||
tabOrganizationDescription: "グループ化と並べ替えは表示順にのみ影響します。ドラッグは手動順で利用できます。",
|
||||
iconTheme: "アイコンとロゴのスタイル",
|
||||
iconThemeDefault: "デフォルト",
|
||||
iconThemeDefaultDescription: "カラーDBXアプリアイコンを使用。",
|
||||
|
||||
@@ -5757,6 +5757,21 @@ export default withEnglishFallback({
|
||||
tabLayoutScrollDescription: "탭이 넘칠 때 한 행에서 가로로 스크롤됩니다.",
|
||||
tabLayoutWrap: "다중 행 줄바꿈",
|
||||
tabLayoutWrapDescription: "탭이 여러 행으로 줄바꿈되어 모두 한 번에 표시됩니다.",
|
||||
tabPlacement: "탭 표시줄 위치",
|
||||
tabPlacementTop: "위쪽",
|
||||
tabPlacementBottom: "아래쪽",
|
||||
tabPlacementLeft: "왼쪽",
|
||||
tabPlacementRight: "오른쪽",
|
||||
tabPlacementDescription: "사이드 탭 표시줄은 세로로 스크롤됩니다. 여러 줄 표시는 콘텐츠 위 또는 아래에서만 적용됩니다.",
|
||||
tabGroup: "그룹 기준",
|
||||
tabGroupNone: "그룹화 안 함",
|
||||
tabGroupDatabaseType: "데이터베이스 유형별 그룹화",
|
||||
tabGroupConnection: "연결별 그룹화",
|
||||
tabSort: "정렬 기준",
|
||||
tabSortManual: "수동 순서",
|
||||
tabSortCreated: "생성 시간",
|
||||
tabSortTitle: "탭 이름",
|
||||
tabOrganizationDescription: "그룹화와 정렬은 표시 순서에만 영향을 줍니다. 끌기는 수동 순서에서 사용할 수 있습니다.",
|
||||
iconTheme: "아이콘 및 로고 스타일",
|
||||
iconThemeDefault: "기본",
|
||||
iconThemeDefaultDescription: "컬러 DBX 앱 아이콘을 사용합니다.",
|
||||
|
||||
@@ -6033,6 +6033,21 @@ export default withEnglishFallback({
|
||||
tabLayoutScrollDescription: "As abas rolam horizontalmente quando excedem o espaço.",
|
||||
tabLayoutWrap: "Quebra em múltiplas linhas",
|
||||
tabLayoutWrapDescription: "As abas quebram em múltiplas linhas para exibir todas.",
|
||||
tabPlacement: "Posição da barra de abas",
|
||||
tabPlacementTop: "Superior",
|
||||
tabPlacementBottom: "Inferior",
|
||||
tabPlacementLeft: "Lado esquerdo",
|
||||
tabPlacementRight: "Lado direito",
|
||||
tabPlacementDescription: "As barras laterais rolam verticalmente; a quebra em múltiplas linhas só se aplica acima ou abaixo do conteúdo.",
|
||||
tabGroup: "Agrupar por",
|
||||
tabGroupNone: "Sem agrupamento",
|
||||
tabGroupDatabaseType: "Agrupar por tipo de banco de dados",
|
||||
tabGroupConnection: "Agrupar por conexão",
|
||||
tabSort: "Ordenar por",
|
||||
tabSortManual: "Ordem manual",
|
||||
tabSortCreated: "Hora de criação",
|
||||
tabSortTitle: "Nome da aba",
|
||||
tabOrganizationDescription: "Agrupamento e ordenação afetam apenas a ordem exibida. Arrastar fica disponível na ordem manual.",
|
||||
iconTheme: "Estilo do ícone e do logo",
|
||||
iconThemeDefault: "Padrão",
|
||||
iconThemeDefaultDescription: "Usar o ícone colorido do app DBX.",
|
||||
|
||||
@@ -6338,6 +6338,21 @@ export default withEnglishFallback({
|
||||
tabLayoutScrollDescription: "标签页超出一行时水平滚动显示。",
|
||||
tabLayoutWrap: "多行平铺",
|
||||
tabLayoutWrapDescription: "标签页超出一行时自动换行,平铺显示所有标签。",
|
||||
tabPlacement: "标签栏位置",
|
||||
tabPlacementTop: "上方",
|
||||
tabPlacementBottom: "下方",
|
||||
tabPlacementLeft: "左侧",
|
||||
tabPlacementRight: "右侧",
|
||||
tabPlacementDescription: "侧边标签栏使用纵向滚动;多行平铺仅适用于上方或下方。",
|
||||
tabGroup: "分组方式",
|
||||
tabGroupNone: "不分组",
|
||||
tabGroupDatabaseType: "按数据库类型分组",
|
||||
tabGroupConnection: "按连接分组",
|
||||
tabSort: "排序方式",
|
||||
tabSortManual: "手动排序",
|
||||
tabSortCreated: "按创建时间",
|
||||
tabSortTitle: "按标签页名称",
|
||||
tabOrganizationDescription: "分组和排序只影响显示顺序;仅手动排序时支持拖拽。",
|
||||
iconTheme: "图标与 Logo 样式",
|
||||
iconThemeDefault: "默认",
|
||||
iconThemeDefaultDescription: "使用彩色 DBX 应用图标。",
|
||||
|
||||
@@ -5353,6 +5353,21 @@ export default withEnglishFallback({
|
||||
tabLayoutScrollDescription: "標籤頁超出一行時水平滾動顯示。",
|
||||
tabLayoutWrap: "多行平鋪",
|
||||
tabLayoutWrapDescription: "標籤頁超出一行時自動換行,平鋪顯示所有標籤。",
|
||||
tabPlacement: "標籤欄位置",
|
||||
tabPlacementTop: "上方",
|
||||
tabPlacementBottom: "下方",
|
||||
tabPlacementLeft: "左側",
|
||||
tabPlacementRight: "右側",
|
||||
tabPlacementDescription: "側邊標籤欄使用垂直捲動;多行平鋪僅適用於上方或下方。",
|
||||
tabGroup: "分組方式",
|
||||
tabGroupNone: "不分組",
|
||||
tabGroupDatabaseType: "依資料庫類型分組",
|
||||
tabGroupConnection: "依連線分組",
|
||||
tabSort: "排序方式",
|
||||
tabSortManual: "手動排序",
|
||||
tabSortCreated: "依建立時間",
|
||||
tabSortTitle: "依標籤頁名稱",
|
||||
tabOrganizationDescription: "分組和排序只影響顯示順序;只有手動排序時可拖曳。",
|
||||
iconTheme: "圖示與 Logo 樣式",
|
||||
iconThemeDefault: "預設",
|
||||
iconThemeDefaultDescription: "使用彩色 DBX 應用程式圖示。",
|
||||
|
||||
@@ -17,6 +17,7 @@ export interface SavedQueryResultRun {
|
||||
|
||||
export interface SavedOpenTab {
|
||||
id: string;
|
||||
createdAt?: number;
|
||||
title: string;
|
||||
customTitle?: boolean;
|
||||
connectionId: string;
|
||||
@@ -91,6 +92,7 @@ function restoredOriginalSql(tab: SavedOpenTab, mode: QueryTab["mode"], sql: str
|
||||
export function serializeOpenTabs(tabs: QueryTab[]): SavedOpenTab[] {
|
||||
return tabs.map((tab) => ({
|
||||
id: tab.id,
|
||||
...(typeof tab.createdAt === "number" ? { createdAt: tab.createdAt } : {}),
|
||||
title: tab.title,
|
||||
...(tab.customTitle ? { customTitle: true } : {}),
|
||||
connectionId: tab.connectionId,
|
||||
|
||||
@@ -34,6 +34,9 @@ export const EDITOR_SETTINGS_DRAFT_KEYS = [
|
||||
"savedSqlOpenTargetMode",
|
||||
"appLayout",
|
||||
"tabLayout",
|
||||
"tabPlacement",
|
||||
"tabGroupMode",
|
||||
"tabSortMode",
|
||||
"showColumnCommentsInHeader",
|
||||
"showColumnTypesInHeader",
|
||||
"dataGridShowTransposeFieldMetadata",
|
||||
|
||||
@@ -733,6 +733,16 @@ export const useQueryStore = defineStore("query", () => {
|
||||
const t = getI18nT();
|
||||
const settingsStore = useSettingsStore();
|
||||
const tabs = ref<QueryTab[]>([]);
|
||||
// New tabs are created by many feature-specific entry points. Tracking id
|
||||
// changes here gives every one a stable timestamp without duplicating the
|
||||
// field at each call site; restored legacy tabs retain their saved order.
|
||||
function ensureTabCreationTimes() {
|
||||
const migrationBase = Date.now() - tabs.value.length;
|
||||
tabs.value.forEach((tab, index) => {
|
||||
if (typeof tab.createdAt !== "number" || !Number.isFinite(tab.createdAt)) tab.createdAt = migrationBase + index;
|
||||
});
|
||||
}
|
||||
watch(() => tabs.value.map((tab) => tab.id), ensureTabCreationTimes, { immediate: true, flush: "sync" });
|
||||
// Default auto-commit for a fresh query tab follows the user's global
|
||||
// "默认事务提交方式" setting (Settings > Editor), not a hard-coded constant.
|
||||
const defaultAutoCommitForDbTypeWithSetting = (dbType?: string) => defaultAutoCommitForDbType(dbType, settingsStore.editorSettings.defaultTransactionMode);
|
||||
@@ -2858,20 +2868,8 @@ export const useQueryStore = defineStore("query", () => {
|
||||
);
|
||||
}
|
||||
|
||||
function closeLeftTabs(id: string, onComplete?: () => void) {
|
||||
const target = tabs.value.find((tab) => tab.id === id);
|
||||
if (!target) return;
|
||||
|
||||
const groupedTabs = tabs.value.filter((tab) => Boolean(tab.pinned) === Boolean(target.pinned));
|
||||
const targetIndex = groupedTabs.findIndex((tab) => tab.id === id);
|
||||
const ids = groupedTabs.slice(0, targetIndex).map((tab) => tab.id);
|
||||
if (ids.length === 0) {
|
||||
onComplete?.();
|
||||
return;
|
||||
}
|
||||
|
||||
const finalActiveTabId = activeTabId.value && !ids.includes(activeTabId.value) ? activeTabId.value : id;
|
||||
beginBatchClose(ids, finalActiveTabId, onComplete);
|
||||
function closeTabsByIds(ids: Iterable<string>, finalActiveTabId?: string | null, onComplete?: () => void) {
|
||||
beginBatchClose([...ids], finalActiveTabId, onComplete);
|
||||
}
|
||||
|
||||
function closeRightTabs(id: string, onComplete?: () => void) {
|
||||
@@ -2887,7 +2885,7 @@ export const useQueryStore = defineStore("query", () => {
|
||||
}
|
||||
|
||||
const finalActiveTabId = activeTabId.value && !ids.includes(activeTabId.value) ? activeTabId.value : id;
|
||||
beginBatchClose(ids, finalActiveTabId, onComplete);
|
||||
closeTabsByIds(ids, finalActiveTabId, onComplete);
|
||||
}
|
||||
|
||||
function finalActiveTabAfterClosing(ids: string[]) {
|
||||
@@ -6852,7 +6850,7 @@ export const useQueryStore = defineStore("query", () => {
|
||||
discardTabChanges,
|
||||
requestAppCloseConfirmation,
|
||||
closeOtherTabs,
|
||||
closeLeftTabs,
|
||||
closeTabsByIds,
|
||||
closeRightTabs,
|
||||
closeOtherRegularTabs,
|
||||
closeRegularTabs,
|
||||
|
||||
@@ -476,6 +476,12 @@ const CELL_DETAIL_PANEL_LAYOUTS = ["bottom", "right"] as const;
|
||||
export type CellDetailPanelLayout = (typeof CELL_DETAIL_PANEL_LAYOUTS)[number];
|
||||
const TAB_LAYOUT_MODES = ["scroll", "wrap"] as const;
|
||||
export type TabLayoutMode = (typeof TAB_LAYOUT_MODES)[number];
|
||||
const TAB_PLACEMENTS = ["top", "bottom", "left", "right"] as const;
|
||||
export type TabPlacement = (typeof TAB_PLACEMENTS)[number];
|
||||
const TAB_GROUP_MODES = ["none", "database-type", "connection"] as const;
|
||||
export type TabGroupMode = (typeof TAB_GROUP_MODES)[number];
|
||||
const TAB_SORT_MODES = ["manual", "created-asc", "title-asc"] as const;
|
||||
export type TabSortMode = (typeof TAB_SORT_MODES)[number];
|
||||
const DATA_GRID_RENDER_MODES = ["dom", "canvas"] as const;
|
||||
export type DataGridRenderMode = (typeof DATA_GRID_RENDER_MODES)[number];
|
||||
const DATA_GRID_SEARCH_MODES = ["filter", "highlight"] as const;
|
||||
@@ -607,6 +613,9 @@ export interface EditorSettings {
|
||||
savedSqlOpenTargetMode: SavedSqlOpenTargetMode;
|
||||
compactTabTitle: boolean;
|
||||
tabLayout: TabLayoutMode;
|
||||
tabPlacement: TabPlacement;
|
||||
tabGroupMode: TabGroupMode;
|
||||
tabSortMode: TabSortMode;
|
||||
appLayout: "separated" | "classic";
|
||||
pageSize: number;
|
||||
tableOpenPageSize: number;
|
||||
@@ -829,6 +838,9 @@ export const DEFAULT_EDITOR_SETTINGS: EditorSettings = {
|
||||
savedSqlOpenTargetMode: "saved",
|
||||
compactTabTitle: false,
|
||||
tabLayout: "scroll",
|
||||
tabPlacement: "top",
|
||||
tabGroupMode: "none",
|
||||
tabSortMode: "manual",
|
||||
appLayout: "classic",
|
||||
pageSize: 100,
|
||||
tableOpenPageSize: 100,
|
||||
@@ -970,6 +982,18 @@ function normalizeTabLayout(value: unknown): TabLayoutMode {
|
||||
return TAB_LAYOUT_MODES.includes(value as TabLayoutMode) ? (value as TabLayoutMode) : DEFAULT_EDITOR_SETTINGS.tabLayout;
|
||||
}
|
||||
|
||||
function normalizeTabPlacement(value: unknown): TabPlacement {
|
||||
return TAB_PLACEMENTS.includes(value as TabPlacement) ? (value as TabPlacement) : DEFAULT_EDITOR_SETTINGS.tabPlacement;
|
||||
}
|
||||
|
||||
function normalizeTabGroupMode(value: unknown): TabGroupMode {
|
||||
return TAB_GROUP_MODES.includes(value as TabGroupMode) ? (value as TabGroupMode) : DEFAULT_EDITOR_SETTINGS.tabGroupMode;
|
||||
}
|
||||
|
||||
function normalizeTabSortMode(value: unknown): TabSortMode {
|
||||
return TAB_SORT_MODES.includes(value as TabSortMode) ? (value as TabSortMode) : DEFAULT_EDITOR_SETTINGS.tabSortMode;
|
||||
}
|
||||
|
||||
function normalizeCellDetailPanelLayout(value: unknown): CellDetailPanelLayout {
|
||||
return CELL_DETAIL_PANEL_LAYOUTS.includes(value as CellDetailPanelLayout) ? (value as CellDetailPanelLayout) : DEFAULT_EDITOR_SETTINGS.cellDetailPanelLayout;
|
||||
}
|
||||
@@ -1240,6 +1264,9 @@ export function normalizeEditorSettings(settings: Partial<EditorSettings>, exist
|
||||
savedSqlOpenTargetMode: settings.savedSqlOpenTargetMode === "current" ? "current" : DEFAULT_EDITOR_SETTINGS.savedSqlOpenTargetMode,
|
||||
compactTabTitle: settings.compactTabTitle ?? DEFAULT_EDITOR_SETTINGS.compactTabTitle,
|
||||
tabLayout: normalizeTabLayout(settings.tabLayout),
|
||||
tabPlacement: normalizeTabPlacement(settings.tabPlacement),
|
||||
tabGroupMode: normalizeTabGroupMode(settings.tabGroupMode),
|
||||
tabSortMode: normalizeTabSortMode(settings.tabSortMode),
|
||||
appLayout: settings.appLayout ?? DEFAULT_EDITOR_SETTINGS.appLayout,
|
||||
pageSize: normalizeResultPageSize(settings.pageSize),
|
||||
tableOpenPageSize: normalizeResultPageSize(settings.tableOpenPageSize, DEFAULT_EDITOR_SETTINGS.tableOpenPageSize),
|
||||
@@ -1864,6 +1891,9 @@ export const useSettingsStore = defineStore("settings", () => {
|
||||
if (partial.savedSqlOpenTargetMode !== undefined) editorSettings.value.savedSqlOpenTargetMode = partial.savedSqlOpenTargetMode === "current" ? "current" : "saved";
|
||||
if (partial.compactTabTitle !== undefined) editorSettings.value.compactTabTitle = partial.compactTabTitle;
|
||||
if (partial.tabLayout !== undefined) editorSettings.value.tabLayout = normalizeTabLayout(partial.tabLayout);
|
||||
if (partial.tabPlacement !== undefined) editorSettings.value.tabPlacement = normalizeTabPlacement(partial.tabPlacement);
|
||||
if (partial.tabGroupMode !== undefined) editorSettings.value.tabGroupMode = normalizeTabGroupMode(partial.tabGroupMode);
|
||||
if (partial.tabSortMode !== undefined) editorSettings.value.tabSortMode = normalizeTabSortMode(partial.tabSortMode);
|
||||
if (partial.appLayout !== undefined) editorSettings.value.appLayout = partial.appLayout;
|
||||
if (partial.pageSize !== undefined) editorSettings.value.pageSize = normalizeResultPageSize(partial.pageSize);
|
||||
if (partial.tableOpenPageSize !== undefined) editorSettings.value.tableOpenPageSize = normalizeResultPageSize(partial.tableOpenPageSize, DEFAULT_EDITOR_SETTINGS.tableOpenPageSize);
|
||||
|
||||
@@ -1115,6 +1115,8 @@ export interface QueryPageJumpProgress {
|
||||
|
||||
export interface QueryTab {
|
||||
id: string;
|
||||
/** Stable creation time used when tabs are displayed in creation order. */
|
||||
createdAt?: number;
|
||||
title: string;
|
||||
customTitle?: boolean;
|
||||
/** Force the editor to word-wrap regardless of the global setting, e.g. for auto-generated single-line templates. */
|
||||
|
||||
Reference in new issue
Block a user