feat(sidebar): close connected connections in group

This commit is contained in:
jearhe authored and GitHub committed 2026-08-24 13:33:37 +08:00
1 parent 917bbc1238
commit a8369390df
11 files changed
+104 -8

No files matched your search

@@ -67,6 +67,7 @@ function connectionStore(selectedTreeNodeIds: string[]) {
connectedIds: new Set<string>(),
connectingIds: new Set<string>(),
disconnect: vi.fn().mockResolvedValue(undefined),
disconnectAndForgetConnectionPassword: vi.fn().mockResolvedValue(undefined),
isTreeNodeChildrenLoaded: vi.fn(() => false),
getConfig: vi.fn(() => undefined),
isDefaultDatabase: vi.fn(() => false),
@@ -244,6 +245,60 @@ describe("sidebar connection group deletion selection", () => {
});
});
describe("sidebar connection group disconnect", () => {
beforeEach(() => {
vi.clearAllMocks();
sidebarFormTarget.value = null;
});
it("recursively disconnects only connected connections under a collapsed group", async () => {
const group = connectionGroupNode("group-parent");
group.isExpanded = false;
const store = connectionStore([group.id]);
store.connectionIdsInGroups.mockReturnValue(["conn-1", "conn-2", "conn-3"]);
store.connectedIds = new Set(["conn-1", "conn-3"]);
const { canDisconnectConnectionGroup, connectionGroupDisconnectMenuLabel, disconnectConnectionGroup } = runtime(group, store);
expect(canDisconnectConnectionGroup()).toBe(true);
expect(connectionGroupDisconnectMenuLabel()).toBe('connectionGroup.closeConnections:{"count":2}');
await disconnectConnectionGroup();
expect(store.connectionIdsInGroups).toHaveBeenCalledWith(["group-parent"]);
expect(store.disconnect.mock.calls).toEqual([["conn-1"], ["conn-3"]]);
expect(store.removeConnections).not.toHaveBeenCalled();
expect(store.deleteConnectionGroups).not.toHaveBeenCalled();
expect(store.disconnectAndForgetConnectionPassword).not.toHaveBeenCalled();
});
it("disables and safely no-ops when the group has no connected connections", async () => {
const group = connectionGroupNode("group-parent");
const store = connectionStore([group.id]);
store.connectionIdsInGroups.mockReturnValue(["conn-1", "conn-2"]);
const { canDisconnectConnectionGroup, connectionGroupDisconnectMenuLabel, disconnectConnectionGroup } = runtime(group, store);
expect(canDisconnectConnectionGroup()).toBe(false);
expect(connectionGroupDisconnectMenuLabel()).toBe('connectionGroup.closeConnections:{"count":0}');
await disconnectConnectionGroup();
expect(store.disconnect).not.toHaveBeenCalled();
expect(mocks.toast).not.toHaveBeenCalled();
});
it("continues group disconnects after one connection fails", async () => {
const group = connectionGroupNode("group-parent");
const store = connectionStore([group.id]);
store.connectionIdsInGroups.mockReturnValue(["conn-1", "conn-2"]);
store.connectedIds = new Set(["conn-1", "conn-2"]);
store.disconnect.mockRejectedValueOnce(new Error("failed")).mockResolvedValueOnce(undefined);
const { disconnectConnectionGroup } = runtime(group, store);
await disconnectConnectionGroup();
expect(store.disconnect.mock.calls).toEqual([["conn-1"], ["conn-2"]]);
expect(mocks.toast).toHaveBeenCalledWith('connection.disconnectSelectedPartial:{"succeeded":1,"failed":1}', 5000);
});
});
describe("sidebar connection disconnect selection", () => {
beforeEach(() => {
vi.clearAllMocks();
@@ -212,16 +212,12 @@ export function useSidebarConnectionMutationRuntime(options: SidebarConnectionMu
}
}
async function disconnectConnection() {
const targets = selectedConnectionDisconnectTargets(activeNode.value, selectedTreeNodesInVisibleOrder()).filter((target) => connectionStore.connectedIds.has(target.connectionId));
if (!targets.length) return;
async function disconnectConnectionIds(connectionIds: readonly string[]) {
if (!connectionIds.length) return;
const result = await disconnectSidebarConnections(
targets.map((target) => target.connectionId),
(connectionId) => connectionStore.disconnect(connectionId),
);
const result = await disconnectSidebarConnections(connectionIds, (connectionId) => connectionStore.disconnect(connectionId));
if (!result.failed) {
toast(targets.length > 1 ? t("connection.disconnectedSelected", { count: targets.length }) : t("connection.disconnected"), 2000);
toast(connectionIds.length > 1 ? t("connection.disconnectedSelected", { count: connectionIds.length }) : t("connection.disconnected"), 2000);
return;
}
if (result.succeeded > 0) {
@@ -232,6 +228,13 @@ export function useSidebarConnectionMutationRuntime(options: SidebarConnectionMu
toast(t("connection.saveFailed", { message }), 5000);
}
async function disconnectConnection() {
const connectionIds = selectedConnectionDisconnectTargets(activeNode.value, selectedTreeNodesInVisibleOrder())
.filter((target) => connectionStore.connectedIds.has(target.connectionId))
.map((target) => target.connectionId);
await disconnectConnectionIds(connectionIds);
}
function connectionDisconnectTargets() {
return selectedConnectionDisconnectTargets(activeNode.value, selectedTreeNodesInVisibleOrder());
}
@@ -246,6 +249,24 @@ export function useSidebarConnectionMutationRuntime(options: SidebarConnectionMu
return connectionDisconnectTargets().some((target) => connectionStore.connectedIds.has(target.connectionId));
}
function connectionGroupDisconnectTargets(): string[] {
const node = activeNode.value;
if (node.type !== "connection-group") return [];
return connectionStore.connectionIdsInGroups([node.id]).filter((connectionId) => connectionStore.connectedIds.has(connectionId));
}
function connectionGroupDisconnectMenuLabel(): string {
return t("connectionGroup.closeConnections", { count: connectionGroupDisconnectTargets().length });
}
function canDisconnectConnectionGroup(): boolean {
return connectionGroupDisconnectTargets().length > 0;
}
async function disconnectConnectionGroup() {
await disconnectConnectionIds(connectionGroupDisconnectTargets());
}
/** "断开并忘记本次密码"是否可用:save_password=false 且当前已连接(本次运行期必已输入密码)。 */
function canForgetSessionCredential(): boolean {
const node = activeNode.value;
@@ -426,6 +447,9 @@ export function useSidebarConnectionMutationRuntime(options: SidebarConnectionMu
disconnectConnection,
connectionDisconnectMenuLabel,
canDisconnectConnection,
connectionGroupDisconnectMenuLabel,
canDisconnectConnectionGroup,
disconnectConnectionGroup,
canForgetSessionCredential,
disconnectAndForgetConnectionPassword,
cancelConnectionAttempt,