mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-20 00:17:44 +00:00
feat(sidebar): close connected connections in group
This commit is contained in:
@@ -425,6 +425,9 @@ const {
|
||||
disconnectConnection,
|
||||
connectionDisconnectMenuLabel,
|
||||
canDisconnectConnection,
|
||||
connectionGroupDisconnectMenuLabel,
|
||||
canDisconnectConnectionGroup,
|
||||
disconnectConnectionGroup,
|
||||
canForgetSessionCredential,
|
||||
disconnectAndForgetConnectionPassword,
|
||||
cancelConnectionAttempt,
|
||||
@@ -4963,6 +4966,12 @@ function buildConnectionSidebarMenu(context: SidebarMenuFactoryContext): boolean
|
||||
items.push({ label: "", separator: true });
|
||||
items.push({ label: t("toolbar.newConnection"), action: newConnectionInGroup, icon: Plus });
|
||||
items.push({ label: t("connectionGroup.newGroup"), action: newSubgroup, icon: FolderPlus });
|
||||
items.push({
|
||||
label: connectionGroupDisconnectMenuLabel(),
|
||||
action: disconnectConnectionGroup,
|
||||
icon: Unplug,
|
||||
disabled: !canDisconnectConnectionGroup(),
|
||||
});
|
||||
items.push({ label: "", separator: true });
|
||||
items.push({
|
||||
label: t("connectionGroup.renameGroup"),
|
||||
|
||||
Vendored
+55
@@ -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,
|
||||
|
||||
@@ -2324,6 +2324,7 @@ export default {
|
||||
selectedConnections: "Selected {count}",
|
||||
exitMultiSelect: "Exit selection",
|
||||
renameGroup: "Rename Group",
|
||||
closeConnections: "Close Connections in Group ({count})",
|
||||
deleteGroup: "Delete Group",
|
||||
deleteSelectedGroups: "Delete {count} Selected Groups",
|
||||
moveToGroup: "Move to Group",
|
||||
|
||||
@@ -2253,6 +2253,7 @@ export default withEnglishFallback({
|
||||
createGroup: "Nuevo grupo",
|
||||
groupNamePlaceholder: "Nombre del grupo",
|
||||
renameGroup: "Renombrar grupo",
|
||||
closeConnections: "Cerrar conexiones del grupo ({count})",
|
||||
deleteGroup: "Eliminar grupo",
|
||||
deleteSelectedGroups: "Eliminar {count} grupos seleccionados",
|
||||
moveToGroup: "Mover al grupo",
|
||||
|
||||
@@ -2251,6 +2251,7 @@ export default withEnglishFallback({
|
||||
createGroup: "Nuovo Gruppo",
|
||||
groupNamePlaceholder: "Nome gruppo",
|
||||
renameGroup: "Rinomina Gruppo",
|
||||
closeConnections: "Chiudi le connessioni del gruppo ({count})",
|
||||
deleteGroup: "Elimina Gruppo",
|
||||
deleteSelectedGroups: "Elimina {count} gruppi selezionati",
|
||||
moveToGroup: "Sposta nel Gruppo",
|
||||
|
||||
@@ -2278,6 +2278,7 @@ export default withEnglishFallback({
|
||||
createGroup: "新しいグループ",
|
||||
groupNamePlaceholder: "グループ名",
|
||||
renameGroup: "グループ名を変更",
|
||||
closeConnections: "グループ内の接続を閉じる({count})",
|
||||
deleteGroup: "グループを削除",
|
||||
deleteSelectedGroups: "選択した {count} 個のグループを削除",
|
||||
moveToGroup: "グループに移動",
|
||||
|
||||
@@ -2158,6 +2158,7 @@ export default withEnglishFallback({
|
||||
selectedConnections: "{count}개 선택",
|
||||
exitMultiSelect: "선택 종료",
|
||||
renameGroup: "그룹 이름 변경",
|
||||
closeConnections: "그룹 내 연결 {count}개 닫기",
|
||||
deleteGroup: "그룹 삭제",
|
||||
deleteSelectedGroups: "선택한 그룹 {count}개 삭제",
|
||||
moveToGroup: "그룹으로 이동",
|
||||
|
||||
@@ -2253,6 +2253,7 @@ export default withEnglishFallback({
|
||||
createGroup: "Novo Grupo",
|
||||
groupNamePlaceholder: "Nome do grupo",
|
||||
renameGroup: "Renomear Grupo",
|
||||
closeConnections: "Fechar conexões do grupo ({count})",
|
||||
deleteGroup: "Excluir Grupo",
|
||||
deleteSelectedGroups: "Excluir {count} grupos selecionados",
|
||||
moveToGroup: "Mover para Grupo",
|
||||
|
||||
@@ -2248,6 +2248,7 @@ export default withEnglishFallback({
|
||||
selectedConnections: "已选 {count}",
|
||||
exitMultiSelect: "退出多选",
|
||||
renameGroup: "重命名分组",
|
||||
closeConnections: "关闭分组内连接({count})",
|
||||
deleteGroup: "删除分组",
|
||||
deleteSelectedGroups: "删除选中的 {count} 个分组",
|
||||
moveToGroup: "移至分组",
|
||||
|
||||
@@ -2252,6 +2252,7 @@ export default withEnglishFallback({
|
||||
createGroup: "建立群組",
|
||||
groupNamePlaceholder: "群組名稱",
|
||||
renameGroup: "重新命名群組",
|
||||
closeConnections: "關閉群組內連線({count})",
|
||||
deleteGroup: "刪除群組",
|
||||
deleteSelectedGroups: "刪除選取的 {count} 個群組",
|
||||
moveToGroup: "移至群組",
|
||||
|
||||
Reference in New Issue
Block a user