feat(tabs): add tab group pills with names, colors and collapsible side bar
This commit is contained in:
15 files changed
+1253
-51
No files matched your search
@@ -281,7 +281,8 @@ const rightSidebarPanelStorageKeys: Partial<Record<RightSidebarPanelId, string>>
|
||||
let lastOpenedRightSidebarPanel = RIGHT_SIDEBAR_PANEL_IDS.find((panelId) => rightSidebarPanelRefs[panelId].value);
|
||||
const sidebarOpen = ref(safeLocalStorageGet("dbx-sidebar-open") !== "false");
|
||||
const aiPanelReady = ref(false);
|
||||
const { sidebarWidth, aiPanelWidth, historyWidth, sqlLibraryWidth, sqlFilePanelWidth, startSidebarResize, startAiPanelResize, startHistoryResize, startSqlLibraryResize, startSqlFilePanelResize } = usePanelResize();
|
||||
const { sidebarWidth, aiPanelWidth, historyWidth, sqlLibraryWidth, sqlFilePanelWidth, tabBarWidth, tabBarCollapsed, startSidebarResize, startAiPanelResize, startHistoryResize, startSqlLibraryResize, startSqlFilePanelResize, startLeftTabBarResize, startRightTabBarResize, setTabBarCollapsed } =
|
||||
usePanelResize();
|
||||
const aiAssistantRef = ref<AiAssistantHandle | null>(null);
|
||||
const appSidebarRef = ref<InstanceType<typeof AppSidebar> | null>(null);
|
||||
const appTabBarRef = ref<InstanceType<typeof AppTabBar> | null>(null);
|
||||
@@ -864,6 +865,18 @@ const tabWorkspaceLayoutClass = computed(() => {
|
||||
if (settingsStore.editorSettings.tabPlacement === "right") return "flex-row-reverse";
|
||||
return isVerticalTabPlacement.value ? "flex-row" : "flex-col";
|
||||
});
|
||||
|
||||
function startTabBarResize(event: MouseEvent) {
|
||||
if (settingsStore.editorSettings.tabPlacement === "right") {
|
||||
startRightTabBarResize(event);
|
||||
return;
|
||||
}
|
||||
startLeftTabBarResize(event);
|
||||
}
|
||||
|
||||
function toggleTabBarCollapsed() {
|
||||
setTabBarCollapsed(!tabBarCollapsed.value);
|
||||
}
|
||||
const updateNotificationsEnabled = computed(() => settingsStore.editorSettings.updateNotificationsEnabled);
|
||||
|
||||
function openSettings(initialTab = "appearance", initialSection?: string) {
|
||||
@@ -3377,6 +3390,8 @@ onUnmounted(() => {
|
||||
:agent-driver-update-count="toolbarAgentDriverUpdateCount"
|
||||
:detached-drop-target="detachedDropTargetTabId !== null"
|
||||
:can-detach-tabs="isDesktop"
|
||||
:tab-bar-width="tabBarWidth"
|
||||
:tab-bar-collapsed="tabBarCollapsed"
|
||||
@toggle-zen-mode="toggleZenMode"
|
||||
@activate-driver-store="openDriverStorePage"
|
||||
@activate-settings-page="activateSettingsPage"
|
||||
@@ -3390,6 +3405,8 @@ onUnmounted(() => {
|
||||
@discard-all-tab-close="handleDiscardAllPendingTabClose"
|
||||
@cancel-tab-close="cancelPendingAppClose"
|
||||
@detach-tab="detachTab"
|
||||
@start-resize="startTabBarResize"
|
||||
@toggle-collapse="toggleTabBarCollapsed"
|
||||
/>
|
||||
<DetachedTabHeader
|
||||
v-else-if="activeTab"
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -136,6 +136,38 @@ describe("AppTabBar special page selection", () => {
|
||||
expect(tabBarSource).toContain(':style="specialTabActiveStyle(settingsPageActive)"');
|
||||
expect(tabBarSource).toContain(':style="specialTabActiveStyle(driverStoreActive)"');
|
||||
});
|
||||
|
||||
it("does not leave a query group highlighted behind an active special page", () => {
|
||||
const groupActive = sourceBetween("function isTabGroupActive", "function toggleTabGroup");
|
||||
|
||||
expect(groupActive).toContain("if (props.driverStoreActive || props.settingsPageActive) return false;");
|
||||
});
|
||||
|
||||
it("restores the active special page after closing the group containing the retained query tab", () => {
|
||||
const restoreSurface = sourceBetween("function restoreSpecialRegularSurface", "function closeSpecialRegularSurfaces");
|
||||
const closeGroup = sourceBetween("function closeTabGroup", "function tabsToRightInGroup");
|
||||
|
||||
expect(restoreSurface).toContain('if (surface === "settings" && props.settingsPageOpen) emit("activate-settings-page")');
|
||||
expect(restoreSurface).toContain('if (surface === "driverStore" && props.driverStoreOpen) emit("activate-driver-store")');
|
||||
expect(restoreSurface).toContain("const surface = pendingGroupCloseSpecialSurface.value;");
|
||||
expect(restoreSurface).toContain("pendingGroupCloseSpecialSurface.value = null;");
|
||||
expect(closeGroup).toContain('props.settingsPageActive ? "settings" : props.driverStoreActive ? "driverStore" : null');
|
||||
expect(closeGroup).toContain("pendingGroupCloseSpecialSurface.value = activeSpecialSurface;");
|
||||
expect(closeGroup).toContain("queryStore.closeTabsByIds(tabsToClose, finalActiveTabId, finishSpecialSurfacePreservingGroupClose)");
|
||||
expect(closeGroup).toContain("restoreSpecialRegularSurface(activeSpecialSurface);");
|
||||
});
|
||||
|
||||
it("keeps and restores the special page while dirty grouped tabs are confirmed or cancelled", () => {
|
||||
const activeTabWatch = sourceBetween("watch(\n () => queryStore.activeTabId", "watch(\n () => props.driverStoreActive");
|
||||
const dismissClose = sourceBetween("function dismissCloseConfirm", "function handleCancelClose");
|
||||
const cancelClose = sourceBetween("function handleCancelClose", "const tabsContainerRef");
|
||||
|
||||
expect(activeTabWatch).toContain("restoreSpecialRegularSurface(pendingGroupCloseSpecialSurface.value);");
|
||||
expect(dismissClose).toContain("pendingGroupCloseSpecialSurface.value = null;");
|
||||
expect(dismissClose).toContain("restoreSpecialRegularSurface(specialSurface);");
|
||||
expect(cancelClose).toContain("dismissCloseConfirm();");
|
||||
expect(tabBarSource).toContain("if (!open) dismissCloseConfirm();");
|
||||
});
|
||||
});
|
||||
|
||||
describe("AppTabBar overflow search", () => {
|
||||
@@ -162,3 +194,115 @@ describe("AppTabBar query execution status", () => {
|
||||
expect(tabBarSource.match(/<\/TabExecutionStatus>/g)).toHaveLength(4);
|
||||
});
|
||||
});
|
||||
|
||||
describe("AppTabBar collapsible groups", () => {
|
||||
it("supports a persistent icon-only vertical tab bar without losing tab actions", () => {
|
||||
expect(tabBarSource).toContain("tabBarCollapsed?: boolean;");
|
||||
expect(tabBarSource).toContain('"toggle-collapse": [];');
|
||||
expect(tabBarSource).toContain('return { width: "3.5rem", flex: "0 0 3.5rem" };');
|
||||
expect(tabBarSource).toContain('<component :is="tabBarCollapseIcon"');
|
||||
expect(tabBarSource).toContain("@click=\"emit('toggle-collapse')\"");
|
||||
expect(tabBarSource).toContain('v-if="!isTabBarCollapsed" class="panel-resize-handle"');
|
||||
expect(tabBarSource).toContain(".vertical-tab-layout--collapsed .app-tab-pill");
|
||||
expect(tabBarSource).toContain(".vertical-tab-layout.vertical-tab-layout--collapsed .tab-group-tab::before");
|
||||
expect(tabBarSource.match(/v-if="isTabBarCollapsed && isDirtyTab\(tab\)"/g)).toHaveLength(2);
|
||||
expect(tabBarSource).toMatch(/\(collapsed\) => \{\s*if \(collapsed\) tabSearchQuery\.value = "";/);
|
||||
});
|
||||
|
||||
it("keeps horizontal placements immune to the persisted vertical collapse state", () => {
|
||||
expect(tabBarSource).toContain("const isTabBarCollapsed = computed(() => isVerticalLayout.value && !!props.tabBarCollapsed);");
|
||||
expect(tabBarSource).not.toContain('v-else-if="!tabBarCollapsed"');
|
||||
expect(tabBarSource).not.toContain('v-if="!tabBarCollapsed" class="min-w-0 truncate');
|
||||
expect(tabBarSource).not.toContain('v-if="tabBarCollapsed && isDirtyTab');
|
||||
// The only raw-prop guards left render inside the vertical-only toolbar.
|
||||
expect(tabBarSource.match(/!tabBarCollapsed/g)).toHaveLength(3);
|
||||
});
|
||||
|
||||
it("keeps only grouping and collapse in the compact vertical toolbar", () => {
|
||||
expect(tabBarSource).toContain('const verticalTabToolbarButtonClass = "inline-flex h-7 w-7');
|
||||
expect(tabBarSource.match(/:trigger-class="verticalTabToolbarButtonClass"/g)).toHaveLength(1);
|
||||
const toolbar = sourceBetween('<div v-if="!tabBarCollapsed" class="flex shrink-0 items-center gap-0">', "</div>\n <button");
|
||||
expect(toolbar).toContain(':trigger-icon="ListFilter"');
|
||||
expect(toolbar).not.toContain(':trigger-icon="ArrowDownUp"');
|
||||
expect(toolbar).not.toContain(':trigger-icon="PanelTop"');
|
||||
expect(tabBarSource).toContain('class="flex shrink-0 items-center gap-0.5 border-b p-1.5"');
|
||||
expect(tabBarSource).toContain('class="flex shrink-0 items-center gap-0"');
|
||||
});
|
||||
|
||||
it("keeps separate collapsed state for regular and fixed groups", () => {
|
||||
expect(tabBarSource).toContain("const collapsedTabGroups = ref<Set<string>>(new Set());");
|
||||
expect(tabBarSource).toContain('return `${tab.pinned ? "fixed" : "regular"}:${tabGroupKey(tab)}`;');
|
||||
expect(tabBarSource).toContain("function toggleTabGroup(tab: QueryTab)");
|
||||
expect(tabBarSource.match(/:aria-expanded="!isTabGroupCollapsed\(tab\)"/g)).toHaveLength(2);
|
||||
expect(tabBarSource.match(/<CustomContextMenu v-if="!isTabGroupCollapsed\(tab\)"/g)).toHaveLength(2);
|
||||
});
|
||||
|
||||
it("temporarily reveals matches and expands a collapsed group when one of its tabs becomes active", () => {
|
||||
expect(tabBarSource).toContain('if (settingsStore.editorSettings.tabGroupMode === "none" || tabSearchQuery.value.trim()) return false;');
|
||||
expect(tabBarSource).toContain("expandTabGroupForTab(tabId);");
|
||||
});
|
||||
|
||||
it("uses compact group pills and places the accent next to content for top and bottom bars", () => {
|
||||
expect(tabBarSource).toContain(':data-placement="settingsStore.editorSettings.tabPlacement"');
|
||||
expect(tabBarSource).toContain(".app-tab-bar:not(.vertical-tab-layout) .tab-group-header");
|
||||
expect(tabBarSource).toContain(".app-tab-bar:not(.vertical-tab-layout) .tab-group-header::after");
|
||||
expect(tabBarSource).toContain(".app-tab-bar:not(.vertical-tab-layout) .tab-group-header--collapsed::after");
|
||||
expect(tabBarSource).toContain('.app-tab-bar:not(.vertical-tab-layout)[data-placement="bottom"] .tab-group-tab::after');
|
||||
expect(tabBarSource).toContain(".app-tab-bar:not(.vertical-tab-layout) .tab-group-tab--last::after");
|
||||
expect(tabBarSource).toContain(".vertical-tab-layout .tab-group-tab::before");
|
||||
});
|
||||
|
||||
it("supports persistent names and colors from the group header context menu", () => {
|
||||
expect(tabBarSource.match(/:items="\(\) => getTabGroupMenuItems\(tab\)"/g)).toHaveLength(2);
|
||||
expect(tabBarSource).toContain('["#2563eb", "#d97706", "#7c3aed", "#059669", "#dc2626", "#0891b2", "#db2777", "#475569"]');
|
||||
expect(tabBarSource).toContain("tabGroupCustomizations: customizations");
|
||||
expect(tabBarSource).toContain("data-tab-group-name-input");
|
||||
expect(tabBarSource).toContain('type="color"');
|
||||
});
|
||||
|
||||
it("exposes placement, grouping, and sorting preferences from the group context menu", () => {
|
||||
const preferences = sourceBetween("function getTabPreferenceMenuItems", "function getTabGroupMenuItems");
|
||||
const groupMenu = sourceBetween("function getTabGroupMenuItems", "function openTabGroupContextMenu");
|
||||
expect(preferences).toContain('label: t("settings.tabPlacement")');
|
||||
expect(preferences).toContain("action: () => updateTabPlacement(item.value)");
|
||||
expect(preferences).toContain('label: t("settings.tabGroup")');
|
||||
expect(preferences).toContain("action: () => updateTabGroupMode(item.value)");
|
||||
expect(preferences).toContain('label: t("settings.tabSort")');
|
||||
expect(preferences).toContain("action: () => updateTabSortMode(item.value)");
|
||||
expect(preferences.match(/checked: item\.value === settingsStore\.editorSettings\./g)).toHaveLength(3);
|
||||
expect(groupMenu).toContain("...getTabPreferenceMenuItems()");
|
||||
});
|
||||
|
||||
it("exposes placement, grouping, and sorting preferences from each tab context menu", () => {
|
||||
const menu = sourceBetween("function getTabMenuItems", "function handleSaveAndClose");
|
||||
expect(menu).toContain("...getTabPreferenceMenuItems()");
|
||||
});
|
||||
|
||||
it("waits for tab preference persistence and treats the whole group title as the context target", () => {
|
||||
expect(tabBarSource).toContain("await settingsStore.updateEditorSettingsAndPersist(partial)");
|
||||
expect(tabBarSource.match(/openTabGroupContextMenu\(\$event, onContextMenu\)/g)).toHaveLength(2);
|
||||
expect(tabBarSource).toContain("document.getSelection()?.removeAllRanges()");
|
||||
expect(tabBarSource).toContain("user-select: none;");
|
||||
});
|
||||
|
||||
it("removes the unexplained fixed-tab divider from vertical placement", () => {
|
||||
expect(tabBarSource).toContain("isVerticalLayout ? 'max-h-[40%] flex-col bg-background pt-1'");
|
||||
expect(tabBarSource).toContain('<Pin v-if="!isTabBarCollapsed" class="tab-group-pin" aria-hidden="true" />');
|
||||
});
|
||||
|
||||
it("uses a soft active shadow and a single indented rail for sidebar groups", () => {
|
||||
expect(tabBarSource).toContain("if (isVerticalLayout.value) {");
|
||||
expect(tabBarSource).toContain('"--app-tab-background": "var(--accent)"');
|
||||
expect(tabBarSource).toContain('.vertical-tab-layout .app-tab-pill[data-active-tab="true"]');
|
||||
expect(tabBarSource).toContain("inset 0 0 0 1px color-mix");
|
||||
expect(tabBarSource).toContain(".vertical-tab-layout .tab-group-header:not(.tab-group-header--collapsed)::after");
|
||||
expect(tabBarSource).toContain(".vertical-tab-layout .tab-group-tab::after");
|
||||
expect(tabBarSource).toContain("margin-inline: 2.5rem 0.5rem;");
|
||||
expect(tabBarSource).toContain("padding-inline-start: 0.65rem !important;");
|
||||
expect(tabBarSource).toContain("left: -0.65rem;");
|
||||
expect(tabBarSource).toContain(".vertical-tab-layout .tab-group-tab--first::before");
|
||||
expect(tabBarSource).toContain("bottom: -1.08rem;");
|
||||
expect(tabBarSource).toContain("top: 50%;");
|
||||
expect(tabBarSource).toContain("border-bottom-left-radius: 0.1875rem;");
|
||||
});
|
||||
});
|
||||
@@ -79,4 +79,19 @@ describe("usePanelResize", () => {
|
||||
expect(aiPanelWidth.value).toBe(600);
|
||||
expect(localStorage.getItem("dbx-ai-panel-width")).toBe("600");
|
||||
});
|
||||
|
||||
it("persists the collapsed vertical tab bar state across composable instances", () => {
|
||||
const first = usePanelResize();
|
||||
expect(first.tabBarCollapsed.value).toBe(false);
|
||||
|
||||
first.setTabBarCollapsed(true);
|
||||
expect(first.tabBarCollapsed.value).toBe(true);
|
||||
expect(localStorage.getItem("dbx-tab-bar-collapsed")).toBe("true");
|
||||
|
||||
const restored = usePanelResize();
|
||||
expect(restored.tabBarCollapsed.value).toBe(true);
|
||||
|
||||
restored.setTabBarCollapsed(false);
|
||||
expect(localStorage.getItem("dbx-tab-bar-collapsed")).toBe("false");
|
||||
});
|
||||
});
|
||||
@@ -21,6 +21,8 @@ export function usePanelResize() {
|
||||
const historyWidth = ref(Number(safeLocalStorageGet("dbx-history-width")) || 288);
|
||||
const sqlLibraryWidth = ref(Number(safeLocalStorageGet("dbx-sql-library-width")) || 288);
|
||||
const sqlFilePanelWidth = ref(Number(safeLocalStorageGet("dbx-sql-file-panel-width")) || 288);
|
||||
const tabBarWidth = ref(Number(safeLocalStorageGet("dbx-tab-bar-width")) || 240);
|
||||
const tabBarCollapsed = ref(safeLocalStorageGet("dbx-tab-bar-collapsed") === "true");
|
||||
|
||||
function startPanelResize(widthRef: Ref<number>, storageKey: string, direction: "left" | "right", maxWidth: PanelMaxWidth = DEFAULT_PANEL_MAX_WIDTH) {
|
||||
return (e: MouseEvent) => {
|
||||
@@ -55,6 +57,13 @@ export function usePanelResize() {
|
||||
const startHistoryResize = startPanelResize(historyWidth, "dbx-history-width", "left");
|
||||
const startSqlLibraryResize = startPanelResize(sqlLibraryWidth, "dbx-sql-library-width", "left");
|
||||
const startSqlFilePanelResize = startPanelResize(sqlFilePanelWidth, "dbx-sql-file-panel-width", "left");
|
||||
const startLeftTabBarResize = startPanelResize(tabBarWidth, "dbx-tab-bar-width", "right");
|
||||
const startRightTabBarResize = startPanelResize(tabBarWidth, "dbx-tab-bar-width", "left");
|
||||
|
||||
function setTabBarCollapsed(collapsed: boolean) {
|
||||
tabBarCollapsed.value = collapsed;
|
||||
safeLocalStorageSet("dbx-tab-bar-collapsed", String(collapsed));
|
||||
}
|
||||
|
||||
return {
|
||||
sidebarWidth,
|
||||
@@ -62,10 +71,15 @@ export function usePanelResize() {
|
||||
historyWidth,
|
||||
sqlLibraryWidth,
|
||||
sqlFilePanelWidth,
|
||||
tabBarWidth,
|
||||
tabBarCollapsed,
|
||||
startSidebarResize,
|
||||
startAiPanelResize,
|
||||
startHistoryResize,
|
||||
startSqlLibraryResize,
|
||||
startSqlFilePanelResize,
|
||||
startLeftTabBarResize,
|
||||
startRightTabBarResize,
|
||||
setTabBarCollapsed,
|
||||
};
|
||||
}
|
||||
@@ -1496,11 +1496,20 @@ export default {
|
||||
maximizeWindow: "Maximize",
|
||||
closeWindow: "Close",
|
||||
searchOpenTabs: "Search open tabs...",
|
||||
collapseTabBar: "Collapse tab bar",
|
||||
expandTabBar: "Expand tab bar",
|
||||
switcherTitle: "Switch Tab",
|
||||
switcherHint: "keep pressing to move, release to switch",
|
||||
noMatchingTabs: "No matching tabs",
|
||||
fixedTabs: "Fixed tabs",
|
||||
openDataTabs: "Open tables",
|
||||
editGroupTitle: "Edit group: {name}",
|
||||
groupName: "Group name",
|
||||
groupColor: "Group color",
|
||||
groupColorAuto: "Use default color",
|
||||
groupColorCustom: "Choose a custom color",
|
||||
resetGroup: "Reset",
|
||||
settingsSaveFailed: "Could not save tab settings: {message}",
|
||||
},
|
||||
databaseBrowser: {
|
||||
search: "Search databases...",
|
||||
@@ -3035,6 +3044,9 @@ export default {
|
||||
compactTabTitle: "Shorten Tab Titles",
|
||||
fullTabTitle: "Show Full Tab Titles",
|
||||
closeTab: "Close Tab",
|
||||
editTabGroup: "Edit Tab Group",
|
||||
resetTabGroup: "Reset Tab Group",
|
||||
closeTabGroup: "Close Tab Group",
|
||||
closeFixedTab: "Close Fixed Tab",
|
||||
closeLeftTabs: "Close Tabs to the Left",
|
||||
closeRightTabs: "Close Tabs to the Right",
|
||||
|
||||
@@ -1463,11 +1463,20 @@ export default withEnglishFallback({
|
||||
scrollRight: "Desplazar pestañas a la derecha",
|
||||
openTabs: "Pestañas abiertas",
|
||||
searchOpenTabs: "Buscar pestañas abiertas...",
|
||||
collapseTabBar: "Contraer barra de pestañas",
|
||||
expandTabBar: "Expandir barra de pestañas",
|
||||
switcherTitle: "Cambiar de pestaña",
|
||||
switcherHint: "sigue pulsando para moverte, suelta para cambiar",
|
||||
noMatchingTabs: "No se encontraron pestañas",
|
||||
fixedTabs: "Pestañas fijadas",
|
||||
openDataTabs: "Tablas abiertas",
|
||||
editGroupTitle: "Editar grupo: {name}",
|
||||
groupName: "Nombre del grupo",
|
||||
groupColor: "Color del grupo",
|
||||
groupColorAuto: "Usar color predeterminado",
|
||||
groupColorCustom: "Elegir color personalizado",
|
||||
resetGroup: "Restablecer",
|
||||
settingsSaveFailed: "No se pudo guardar la configuración de pestañas: {message}",
|
||||
gridfs: "GridFS",
|
||||
etcdDashboard: "Panel de etcd",
|
||||
etcdAccessControl: "Control de acceso de etcd",
|
||||
@@ -2952,6 +2961,9 @@ export default withEnglishFallback({
|
||||
compactTabTitle: "Acortar títulos de pestañas",
|
||||
fullTabTitle: "Mostrar títulos completos de pestañas",
|
||||
closeTab: "Cerrar pestaña",
|
||||
editTabGroup: "Editar grupo de pestañas",
|
||||
resetTabGroup: "Restablecer grupo de pestañas",
|
||||
closeTabGroup: "Cerrar grupo de pestañas",
|
||||
closeFixedTab: "Cerrar pestaña fijada",
|
||||
closeOtherTabs: "Cerrar otras pestañas",
|
||||
closeOtherRegularTabs: "Cerrar otras pestañas normales",
|
||||
|
||||
@@ -1461,11 +1461,20 @@ export default withEnglishFallback({
|
||||
scrollRight: "Scorri schede a destra",
|
||||
openTabs: "Schede aperte",
|
||||
searchOpenTabs: "Cerca schede aperte...",
|
||||
collapseTabBar: "Comprimi barra delle schede",
|
||||
expandTabBar: "Espandi barra delle schede",
|
||||
switcherTitle: "Cambia scheda",
|
||||
switcherHint: "continua a premere per spostarti, rilascia per cambiare",
|
||||
noMatchingTabs: "Nessuna scheda corrispondente",
|
||||
fixedTabs: "Schede bloccate",
|
||||
openDataTabs: "Tabelle aperte",
|
||||
editGroupTitle: "Modifica gruppo: {name}",
|
||||
groupName: "Nome gruppo",
|
||||
groupColor: "Colore gruppo",
|
||||
groupColorAuto: "Usa colore predefinito",
|
||||
groupColorCustom: "Scegli colore personalizzato",
|
||||
resetGroup: "Ripristina",
|
||||
settingsSaveFailed: "Impossibile salvare le impostazioni delle schede: {message}",
|
||||
gridfs: "GridFS",
|
||||
etcdDashboard: "etcd dashboard",
|
||||
etcdAccessControl: "controllo accessi etcd",
|
||||
@@ -2950,6 +2959,9 @@ export default withEnglishFallback({
|
||||
compactTabTitle: "Accorcia Titoli Schede",
|
||||
fullTabTitle: "Mostra titoli completi delle schede",
|
||||
closeTab: "Chiudi Scheda",
|
||||
editTabGroup: "Modifica gruppo di schede",
|
||||
resetTabGroup: "Ripristina gruppo di schede",
|
||||
closeTabGroup: "Chiudi Gruppo di Schede",
|
||||
closeFixedTab: "Chiudi Scheda Fissa",
|
||||
closeOtherTabs: "Chiudi Altre Schede",
|
||||
closeOtherRegularTabs: "Chiudi Altre Schede Normali",
|
||||
|
||||
@@ -1480,11 +1480,20 @@ export default withEnglishFallback({
|
||||
scrollRight: "タブを右にスクロール",
|
||||
openTabs: "開いているタブ",
|
||||
searchOpenTabs: "開いているタブを検索...",
|
||||
collapseTabBar: "タブバーを折りたたむ",
|
||||
expandTabBar: "タブバーを展開",
|
||||
switcherTitle: "タブを切り替え",
|
||||
switcherHint: "押し続けて移動、離して切り替え",
|
||||
noMatchingTabs: "一致するタブがありません",
|
||||
fixedTabs: "固定タブ",
|
||||
openDataTabs: "開いているテーブル",
|
||||
editGroupTitle: "グループを編集: {name}",
|
||||
groupName: "グループ名",
|
||||
groupColor: "グループの色",
|
||||
groupColorAuto: "既定の色を使用",
|
||||
groupColorCustom: "カスタムカラーを選択",
|
||||
resetGroup: "リセット",
|
||||
settingsSaveFailed: "タブ設定を保存できません: {message}",
|
||||
zookeeper: "ZooKeeper",
|
||||
consul: "Consul",
|
||||
vector: "ベクター",
|
||||
@@ -2974,6 +2983,9 @@ export default withEnglishFallback({
|
||||
compactTabTitle: "タブタイトルを短縮",
|
||||
fullTabTitle: "タブの完全なタイトルを表示",
|
||||
closeTab: "タブを閉じる",
|
||||
editTabGroup: "タブグループを編集",
|
||||
resetTabGroup: "タブグループをリセット",
|
||||
closeTabGroup: "このグループのタブを閉じる",
|
||||
closeFixedTab: "固定タブを閉じる",
|
||||
closeOtherTabs: "他のタブを閉じる",
|
||||
closeOtherRegularTabs: "他の通常タブを閉じる",
|
||||
|
||||
@@ -1367,11 +1367,20 @@ export default withEnglishFallback({
|
||||
scrollRight: "탭을 오른쪽으로 스크롤",
|
||||
openTabs: "열린 탭",
|
||||
searchOpenTabs: "열린 탭 검색...",
|
||||
collapseTabBar: "탭 표시줄 접기",
|
||||
expandTabBar: "탭 표시줄 펼치기",
|
||||
switcherTitle: "탭 전환",
|
||||
switcherHint: "계속 눌러 이동하고 놓으면 전환",
|
||||
noMatchingTabs: "일치하는 탭이 없습니다",
|
||||
fixedTabs: "고정 탭",
|
||||
openDataTabs: "열린 테이블",
|
||||
editGroupTitle: "그룹 편집: {name}",
|
||||
groupName: "그룹 이름",
|
||||
groupColor: "그룹 색상",
|
||||
groupColorAuto: "기본 색상 사용",
|
||||
groupColorCustom: "사용자 지정 색상 선택",
|
||||
resetGroup: "초기화",
|
||||
settingsSaveFailed: "탭 설정을 저장하지 못했습니다: {message}",
|
||||
},
|
||||
databaseBrowser: {
|
||||
search: "데이터베이스 검색...",
|
||||
@@ -2844,6 +2853,9 @@ export default withEnglishFallback({
|
||||
compactTabTitle: "탭 제목 줄이기",
|
||||
fullTabTitle: "전체 탭 제목 표시",
|
||||
closeTab: "탭 닫기",
|
||||
editTabGroup: "탭 그룹 편집",
|
||||
resetTabGroup: "탭 그룹 초기화",
|
||||
closeTabGroup: "이 그룹의 탭 닫기",
|
||||
closeFixedTab: "고정 탭 닫기",
|
||||
closeLeftTabs: "왼쪽 탭 닫기",
|
||||
closeRightTabs: "오른쪽 탭 닫기",
|
||||
|
||||
@@ -1463,11 +1463,20 @@ export default withEnglishFallback({
|
||||
scrollRight: "Rolar abas para a direita",
|
||||
openTabs: "Abas abertas",
|
||||
searchOpenTabs: "Pesquisar abas abertas...",
|
||||
collapseTabBar: "Recolher barra de abas",
|
||||
expandTabBar: "Expandir barra de abas",
|
||||
switcherTitle: "Alternar aba",
|
||||
switcherHint: "continue pressionando para mover, solte para alternar",
|
||||
noMatchingTabs: "Nenhuma aba correspondente",
|
||||
fixedTabs: "Abas fixadas",
|
||||
openDataTabs: "Tabelas abertas",
|
||||
editGroupTitle: "Editar grupo: {name}",
|
||||
groupName: "Nome do grupo",
|
||||
groupColor: "Cor do grupo",
|
||||
groupColorAuto: "Usar cor padrão",
|
||||
groupColorCustom: "Escolher cor personalizada",
|
||||
resetGroup: "Redefinir",
|
||||
settingsSaveFailed: "Falha ao salvar configurações de abas: {message}",
|
||||
gridfs: "GridFS",
|
||||
etcdDashboard: "painel etcd",
|
||||
etcdAccessControl: "controle de acesso do etcd",
|
||||
@@ -2952,6 +2961,9 @@ export default withEnglishFallback({
|
||||
compactTabTitle: "Encurtar Títulos das Abas",
|
||||
fullTabTitle: "Mostrar títulos completos das abas",
|
||||
closeTab: "Fechar Aba",
|
||||
editTabGroup: "Editar grupo de abas",
|
||||
resetTabGroup: "Redefinir grupo de abas",
|
||||
closeTabGroup: "Fechar Grupo de Abas",
|
||||
closeFixedTab: "Fechar Aba Fixa",
|
||||
closeOtherTabs: "Fechar Outras Abas",
|
||||
closeOtherRegularTabs: "Fechar Outras Abas Normais",
|
||||
|
||||
@@ -1419,12 +1419,21 @@ export default withEnglishFallback({
|
||||
minimizeWindow: "最小化",
|
||||
maximizeWindow: "最大化",
|
||||
closeWindow: "关闭",
|
||||
searchOpenTabs: "搜索已打开的标签页...",
|
||||
searchOpenTabs: "搜索...",
|
||||
collapseTabBar: "收起标签栏",
|
||||
expandTabBar: "展开标签栏",
|
||||
switcherTitle: "切换标签页",
|
||||
switcherHint: "继续按选择,松开修饰键切换",
|
||||
noMatchingTabs: "未找到匹配的标签页",
|
||||
fixedTabs: "固定标签页",
|
||||
openDataTabs: "已打开的数据表",
|
||||
editGroupTitle: "编辑分组:{name}",
|
||||
groupName: "分组名称",
|
||||
groupColor: "分组颜色",
|
||||
groupColorAuto: "使用默认颜色",
|
||||
groupColorCustom: "选择自定义颜色",
|
||||
resetGroup: "恢复默认",
|
||||
settingsSaveFailed: "无法保存标签设置:{message}",
|
||||
},
|
||||
databaseBrowser: {
|
||||
search: "搜索数据库...",
|
||||
@@ -2958,6 +2967,9 @@ export default withEnglishFallback({
|
||||
compactTabTitle: "缩短标签标题",
|
||||
fullTabTitle: "完整标签标题",
|
||||
closeTab: "关闭标签页",
|
||||
editTabGroup: "编辑标签分组",
|
||||
resetTabGroup: "恢复默认分组",
|
||||
closeTabGroup: "关闭本组标签页",
|
||||
closeFixedTab: "关闭固定标签页",
|
||||
closeLeftTabs: "关闭左侧标签页",
|
||||
closeRightTabs: "关闭右侧标签页",
|
||||
|
||||
@@ -1462,11 +1462,20 @@ export default withEnglishFallback({
|
||||
scrollRight: "向右捲動分頁",
|
||||
openTabs: "已開啟的分頁",
|
||||
searchOpenTabs: "搜尋已開啟的分頁...",
|
||||
collapseTabBar: "收合分頁列",
|
||||
expandTabBar: "展開分頁列",
|
||||
switcherTitle: "切換分頁",
|
||||
switcherHint: "繼續按選擇,放開修飾鍵切換",
|
||||
noMatchingTabs: "找不到符合的分頁",
|
||||
fixedTabs: "固定分頁",
|
||||
openDataTabs: "已開啟的資料表",
|
||||
editGroupTitle: "編輯群組:{name}",
|
||||
groupName: "群組名稱",
|
||||
groupColor: "群組顏色",
|
||||
groupColorAuto: "使用預設顏色",
|
||||
groupColorCustom: "選擇自訂顏色",
|
||||
resetGroup: "重設",
|
||||
settingsSaveFailed: "無法儲存分頁設定:{message}",
|
||||
gridfs: "GridFS",
|
||||
etcdDashboard: "etcd 儀表板",
|
||||
etcdAccessControl: "etcd 存取控制",
|
||||
@@ -2949,6 +2958,9 @@ export default withEnglishFallback({
|
||||
compactTabTitle: "縮短標籤標題",
|
||||
fullTabTitle: "完整分頁標題",
|
||||
closeTab: "關閉分頁",
|
||||
editTabGroup: "編輯分頁群組",
|
||||
resetTabGroup: "重設分頁群組",
|
||||
closeTabGroup: "關閉本群組分頁",
|
||||
closeFixedTab: "關閉固定分頁",
|
||||
closeOtherTabs: "關閉其他分頁",
|
||||
closeOtherRegularTabs: "關閉其他普通分頁",
|
||||
|
||||
@@ -38,6 +38,21 @@ describe("normalizeEditorSettings", () => {
|
||||
expect(normalizeEditorSettings({ dataGridFilterEditorView: "invalid" } as any).dataGridFilterEditorView).toBe("quick");
|
||||
});
|
||||
|
||||
it("normalizes persisted tab group names and colors", () => {
|
||||
expect(normalizeEditorSettings({}).tabGroupCustomizations).toEqual({});
|
||||
expect(
|
||||
normalizeEditorSettings({
|
||||
tabGroupCustomizations: {
|
||||
"connection:local": { name: " Local services ", color: "#E11D48" },
|
||||
"database-type:mysql": { name: "", color: "invalid" },
|
||||
broken: "value",
|
||||
},
|
||||
} as any).tabGroupCustomizations,
|
||||
).toEqual({
|
||||
"connection:local": { name: "Local services", color: "#e11d48" },
|
||||
});
|
||||
});
|
||||
|
||||
it("defaults and bounds the persisted text filter panel height", () => {
|
||||
expect(normalizeEditorSettings({}).dataGridTextFilterPanelHeight).toBe(168);
|
||||
expect(normalizeEditorSettings({ dataGridTextFilterPanelHeight: 236.4 }).dataGridTextFilterPanelHeight).toBe(236);
|
||||
@@ -894,6 +909,40 @@ describe("settingsStore persisted settings initialization", () => {
|
||||
expect(saveEditorSettings).toHaveBeenLastCalledWith(expect.objectContaining({ openDataTabsNextToActive: false }));
|
||||
});
|
||||
|
||||
it("persists tab placement, grouping, sorting, and group customizations across restart", async () => {
|
||||
let persistedSettings: Record<string, unknown> = {};
|
||||
const loadEditorSettings = vi.fn(async () => JSON.parse(JSON.stringify(persistedSettings)));
|
||||
const saveEditorSettings = vi.fn(async (settings: Record<string, unknown>) => {
|
||||
persistedSettings = JSON.parse(JSON.stringify(settings));
|
||||
});
|
||||
vi.doMock("@/lib/backend/api", () => ({ loadEditorSettings, saveEditorSettings }));
|
||||
|
||||
const { useSettingsStore } = await import("@/stores/settingsStore");
|
||||
const firstStore = useSettingsStore();
|
||||
await firstStore.initEditorSettings();
|
||||
await firstStore.updateEditorSettingsAndPersist({
|
||||
tabPlacement: "left",
|
||||
tabGroupMode: "connection",
|
||||
tabSortMode: "title-asc",
|
||||
tabGroupCustomizations: {
|
||||
"connection:local": { name: "Local services", color: "#2563eb" },
|
||||
},
|
||||
});
|
||||
|
||||
setActivePinia(createPinia());
|
||||
const restartedStore = useSettingsStore();
|
||||
await restartedStore.initEditorSettings();
|
||||
|
||||
expect(restartedStore.editorSettings).toMatchObject({
|
||||
tabPlacement: "left",
|
||||
tabGroupMode: "connection",
|
||||
tabSortMode: "title-asc",
|
||||
tabGroupCustomizations: {
|
||||
"connection:local": { name: "Local services", color: "#2563eb" },
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("loads, persists, and reloads the cell detail button visibility", async () => {
|
||||
let persistedSettings: Record<string, unknown> = { dataGridCellDetailButtonVisible: false };
|
||||
const loadEditorSettings = vi.fn(async () => JSON.parse(JSON.stringify(persistedSettings)));
|
||||
|
||||
@@ -576,6 +576,10 @@ 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];
|
||||
export interface TabGroupCustomization {
|
||||
name?: string;
|
||||
color?: string;
|
||||
}
|
||||
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;
|
||||
@@ -711,6 +715,7 @@ export interface EditorSettings {
|
||||
tabLayout: TabLayoutMode;
|
||||
tabPlacement: TabPlacement;
|
||||
tabGroupMode: TabGroupMode;
|
||||
tabGroupCustomizations: Record<string, TabGroupCustomization>;
|
||||
tabSortMode: TabSortMode;
|
||||
appLayout: "separated" | "classic";
|
||||
pageSize: number;
|
||||
@@ -938,6 +943,7 @@ export const DEFAULT_EDITOR_SETTINGS: EditorSettings = {
|
||||
tabLayout: "scroll",
|
||||
tabPlacement: "top",
|
||||
tabGroupMode: "none",
|
||||
tabGroupCustomizations: {},
|
||||
tabSortMode: "manual",
|
||||
appLayout: "classic",
|
||||
pageSize: 100,
|
||||
@@ -1090,6 +1096,20 @@ function normalizeTabGroupMode(value: unknown): TabGroupMode {
|
||||
return TAB_GROUP_MODES.includes(value as TabGroupMode) ? (value as TabGroupMode) : DEFAULT_EDITOR_SETTINGS.tabGroupMode;
|
||||
}
|
||||
|
||||
export function normalizeTabGroupCustomizations(value: unknown): Record<string, TabGroupCustomization> {
|
||||
if (!value || typeof value !== "object" || Array.isArray(value)) return {};
|
||||
const customizations: Record<string, TabGroupCustomization> = {};
|
||||
for (const [rawKey, rawCustomization] of Object.entries(value as Record<string, unknown>).slice(0, 200)) {
|
||||
const key = rawKey.trim().slice(0, 512);
|
||||
if (!key || !rawCustomization || typeof rawCustomization !== "object" || Array.isArray(rawCustomization)) continue;
|
||||
const customization = rawCustomization as Record<string, unknown>;
|
||||
const name = typeof customization.name === "string" ? customization.name.trim().slice(0, 80) : "";
|
||||
const color = typeof customization.color === "string" && /^#[0-9a-f]{6}$/i.test(customization.color.trim()) ? customization.color.trim().toLowerCase() : "";
|
||||
if (name || color) customizations[key] = { ...(name ? { name } : {}), ...(color ? { color } : {}) };
|
||||
}
|
||||
return customizations;
|
||||
}
|
||||
|
||||
function normalizeTabSortMode(value: unknown): TabSortMode {
|
||||
return TAB_SORT_MODES.includes(value as TabSortMode) ? (value as TabSortMode) : DEFAULT_EDITOR_SETTINGS.tabSortMode;
|
||||
}
|
||||
@@ -1366,6 +1386,7 @@ export function normalizeEditorSettings(settings: Partial<EditorSettings>, exist
|
||||
tabLayout: normalizeTabLayout(settings.tabLayout),
|
||||
tabPlacement: normalizeTabPlacement(settings.tabPlacement),
|
||||
tabGroupMode: normalizeTabGroupMode(settings.tabGroupMode),
|
||||
tabGroupCustomizations: normalizeTabGroupCustomizations(settings.tabGroupCustomizations),
|
||||
tabSortMode: normalizeTabSortMode(settings.tabSortMode),
|
||||
appLayout: settings.appLayout ?? DEFAULT_EDITOR_SETTINGS.appLayout,
|
||||
pageSize: normalizeResultPageSize(settings.pageSize),
|
||||
@@ -2069,6 +2090,7 @@ export const useSettingsStore = defineStore("settings", () => {
|
||||
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.tabGroupCustomizations !== undefined) editorSettings.value.tabGroupCustomizations = normalizeTabGroupCustomizations(partial.tabGroupCustomizations);
|
||||
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);
|
||||
|
||||
Reference in new issue
Block a user