dbx/apps/desktop/src/composables/useSidebarConnectionMutatio...

330 lines
14 KiB
TypeScript

import { computed, type ShallowRef } from "vue";
import { useI18n } from "vue-i18n";
import { useToast } from "@/composables/useToast";
import { useConnectionStore } from "@/stores/connectionStore";
import { useQueryStore } from "@/stores/queryStore";
import type { TreeNode } from "@/types/database";
import * as api from "@/lib/backend/api";
import { translateBackendError } from "@/i18n/backend-errors";
import { copyToClipboard } from "@/lib/common/clipboard";
import { uuid } from "@/lib/common/utils";
import { connectionFilePath, defaultSqliteBackupFileName, isMemorySqlitePath, sqliteBackupSourcePath } from "@/lib/connection/connectionFile";
import { hasEnabledTransportLayers } from "@/lib/backend/connectionTransport";
import { isTauriRuntime } from "@/lib/backend/tauriRuntime";
import { revealPathInFileManager } from "@/lib/backend/tauri";
import { canConfigureVisibleSchemasForTreeNode } from "@/lib/database/databaseFeatureSupport";
import { canCloseSidebarDatabaseConnection } from "@/lib/sidebar/sidebarDatabaseOpenState";
import { selectedConnectionDeleteTargets, selectedConnectionDuplicateTargets } from "@/lib/sidebar/sidebarConnectionSelection";
import { connectionDeleteTargetSnapshot, showDeleteConfirm, showDeleteGroupConfirm, sidebarFormTarget } from "@/components/sidebar/sidebarTreeDialogState";
import { connectionCanConfigureSidebarVisibleDatabases } from "@/lib/sidebar/sidebarVisibleFilterMenu";
interface SidebarConnectionMutationRuntimeOptions {
activeNode: ShallowRef<TreeNode>;
releaseActiveNodeReference: (nodeIds: readonly string[]) => void;
selectedTreeNodesInVisibleOrder: () => TreeNode[];
connectionStore: ReturnType<typeof useConnectionStore>;
queryStore: ReturnType<typeof useQueryStore>;
requestGroupRename: (groupId: string) => void;
groupCreated: (groupId: string) => void;
openVisibleDatabases: (node: TreeNode) => void;
openVisibleSchemas: (node: TreeNode) => void;
}
export function useSidebarConnectionMutationRuntime(options: SidebarConnectionMutationRuntimeOptions) {
const { t } = useI18n();
const { toast } = useToast();
const { activeNode, selectedTreeNodesInVisibleOrder, connectionStore, queryStore } = options;
async function setNodeAsDefaultDatabase() {
const node = activeNode.value;
if (!node.connectionId || !node.database) return;
try {
await connectionStore.setDefaultDatabase(node.connectionId, node.database);
} catch (error: any) {
toast(t("connection.saveFailed", { message: error?.message || String(error) }), 5000);
}
}
async function clearNodeDefaultDatabase() {
const node = activeNode.value;
if (!node.connectionId) return;
try {
await connectionStore.clearDefaultDatabase(node.connectionId);
} catch (error: any) {
toast(t("connection.saveFailed", { message: error?.message || String(error) }), 5000);
}
}
function connectionDeleteTargets() {
if (showDeleteConfirm.value && connectionDeleteTargetSnapshot.value.length) return connectionDeleteTargetSnapshot.value;
return selectedConnectionDeleteTargets(activeNode.value, selectedTreeNodesInVisibleOrder());
}
function connectionDeleteMenuLabel(): string {
const count = connectionDeleteTargets().length;
return count > 1 ? t("contextMenu.deleteSelectedConnections", { count }) : t("contextMenu.deleteConnection");
}
function connectionDuplicateTargets() {
return selectedConnectionDuplicateTargets(activeNode.value, selectedTreeNodesInVisibleOrder());
}
function connectionDuplicateMenuLabel(): string {
const count = connectionDuplicateTargets().length;
return count > 1 ? t("contextMenu.duplicateSelectedConnections", { count }) : t("contextMenu.duplicateConnection");
}
function connectionDeleteConfirmMessage(): string {
const targets = connectionDeleteTargets();
return targets.length > 1 ? t("contextMenu.confirmDeleteSelectedMessage", { count: targets.length }) : t("contextMenu.confirmDeleteMessage", { name: targets[0]?.label || sidebarFormTarget.value?.label || activeNode.value.label });
}
function deleteConnection() {
const targets = selectedConnectionDeleteTargets(activeNode.value, selectedTreeNodesInVisibleOrder());
if (!targets.length) return;
connectionDeleteTargetSnapshot.value = targets.slice();
showDeleteConfirm.value = true;
}
async function confirmDelete() {
const targets = connectionDeleteTargets();
if (!targets.length) return;
const connectionIds = targets.map((target) => target.connectionId);
try {
await connectionStore.removeConnections(connectionIds);
options.releaseActiveNodeReference(targets.map((target) => target.id));
for (const connectionId of connectionIds) {
connectionStore.disconnect(connectionId).catch((error) => {
// Removal has already succeeded; disconnect cleanup must not turn it into a failed delete.
console.warn("[DBX][connection:delete:disconnect-failed]", { connectionId, error });
});
}
toast(targets.length > 1 ? t("connection.deletedSelected", { count: targets.length }) : t("connection.deleted"), 2000);
} catch (error: any) {
toast(t("connection.saveFailed", { message: error?.message || String(error) }), 5000);
}
}
async function copyFinalProxyPort() {
const connectionId = activeNode.value.connectionId;
const config = connectionId ? connectionStore.getConfig(connectionId) : undefined;
if (!config || !hasEnabledTransportLayers(config)) return;
try {
const port = await api.connectionFinalProxyPort(config);
await copyToClipboard(String(port));
toast(t("contextMenu.finalProxyPortCopied", { port }), 2000);
} catch (error: any) {
toast(t("grid.copyFailed", { message: translateBackendError(t, error) }), 5000);
}
}
async function duplicateConnection() {
const targets = connectionDuplicateTargets();
if (!targets.length) return;
let duplicatedCount = 0;
for (const target of targets) {
const config = connectionStore.getConfig(target.connectionId);
if (!config) continue;
const newConfig = { ...config, id: uuid(), name: `${config.name} (Copy)` };
await connectionStore.addConnection(newConfig, connectionStore.groupIdForConnection(target.connectionId));
duplicatedCount += 1;
}
if (!duplicatedCount) return;
toast(duplicatedCount > 1 ? t("connection.duplicatedSelected", { count: duplicatedCount }) : t("connection.duplicated"), 2000);
}
function editConnection() {
const connectionId = activeNode.value.connectionId;
if (connectionId) connectionStore.startEditing(connectionId);
}
const revealConnectionFilePath = computed<string | null>(() => {
if (activeNode.value.type !== "connection" || !activeNode.value.connectionId) return null;
const config = connectionStore.getConfig(activeNode.value.connectionId);
return config ? connectionFilePath(config) : null;
});
async function revealDatabaseFile() {
const path = revealConnectionFilePath.value;
if (!path) return;
try {
await revealPathInFileManager(path);
} catch (error: any) {
toast(translateBackendError(t, error), 5000);
}
}
const sqliteBackupSource = computed<string | null>(() => {
if (activeNode.value.type !== "connection" || !activeNode.value.connectionId) return null;
const config = connectionStore.getConfig(activeNode.value.connectionId);
return config ? sqliteBackupSourcePath(config) : null;
});
const canBackupSqliteDatabase = computed(() => {
const source = sqliteBackupSource.value;
if (!source || !activeNode.value.connectionId) return false;
return isTauriRuntime() && (!isMemorySqlitePath(source) || connectionStore.connectedIds.has(activeNode.value.connectionId));
});
async function backupSqliteDatabase() {
const connectionId = activeNode.value.connectionId;
const config = connectionId ? connectionStore.getConfig(connectionId) : undefined;
const sourcePath = sqliteBackupSource.value;
if (!connectionId || !config || !sourcePath) return;
try {
const { save } = await import("@tauri-apps/plugin-dialog");
const destinationPath = await save({
defaultPath: defaultSqliteBackupFileName(config),
filters: [{ name: "SQLite", extensions: ["db", "sqlite", "sqlite3"] }],
});
if (!destinationPath) return;
toast(t("contextMenu.backupSqliteDatabaseInProgress"), 2000);
if (!isMemorySqlitePath(sourcePath)) await connectionStore.ensureConnected(connectionId);
await api.backupSqliteDatabase(connectionId, destinationPath);
toast(t("contextMenu.backupSqliteDatabaseSuccess"), 3000);
} catch (error: any) {
toast(t("contextMenu.backupSqliteDatabaseFailed", { message: error?.message || String(error) }), 5000);
}
}
async function disconnectConnection() {
const node = activeNode.value;
if (!node.connectionId) return;
try {
await connectionStore.disconnect(node.connectionId);
node.isExpanded = false;
node.children = [];
toast(t("connection.disconnected"), 2000);
} catch (error: any) {
toast(t("connection.saveFailed", { message: error?.message || String(error) }), 5000);
}
}
async function cancelConnectionAttempt() {
const connectionId = activeNode.value.connectionId;
if (!connectionId) return;
try {
const cancelled = await connectionStore.cancelConnecting(connectionId);
if (cancelled) toast(t("connection.connectCancelled"), 2000);
} catch (error: any) {
toast(t("connection.saveFailed", { message: error?.message || String(error) }), 5000);
}
}
async function closeDatabaseConnection() {
const node = activeNode.value;
if (node.type !== "database" || !node.connectionId || node.database == null) return;
try {
await connectionStore.closeDatabaseConnection(node.connectionId, node.database);
toast(t("connection.databaseConnectionClosed", { name: node.label }), 2000);
} catch (error: any) {
toast(t("connection.saveFailed", { message: error?.message || String(error) }), 5000);
}
}
const isPinned = computed(() => activeNode.value.pinned || connectionStore.isTreeNodePinned(activeNode.value));
const isNodeDefaultDatabase = computed(
() => (activeNode.value.type === "database" || activeNode.value.type === "redis-db" || activeNode.value.type === "mongo-db") && !!activeNode.value.connectionId && !!activeNode.value.database && connectionStore.isDefaultDatabase(activeNode.value.connectionId, activeNode.value.database),
);
const isConnected = computed(() => activeNode.value.type === "connection" && !!activeNode.value.connectionId && connectionStore.connectedIds.has(activeNode.value.connectionId));
const isConnecting = computed(() => activeNode.value.type === "connection" && !!activeNode.value.connectionId && connectionStore.connectingIds.has(activeNode.value.connectionId));
const canCloseDatabaseConnection = computed(() => canCloseSidebarDatabaseConnection(activeNode.value, connectionStore.isTreeNodeChildrenLoaded, (connectionId, database) => queryStore.openDatabaseKeys.has(`${connectionId}\x00${database}`)));
const canConfigureVisibleDatabases = computed(() => {
if (activeNode.value.type !== "connection" || !activeNode.value.connectionId) return false;
const databaseType = connectionStore.getConfig(activeNode.value.connectionId)?.db_type;
return connectionCanConfigureSidebarVisibleDatabases(databaseType);
});
const canConfigureVisibleSchemas = computed(() => {
if (!activeNode.value.connectionId) return false;
const databaseType = connectionStore.getConfig(activeNode.value.connectionId)?.db_type;
return canConfigureVisibleSchemasForTreeNode(databaseType, activeNode.value.type, activeNode.value.database);
});
const canCopyFinalProxyPort = computed(() => activeNode.value.type === "connection" && !!activeNode.value.connectionId && hasEnabledTransportLayers(connectionStore.getConfig(activeNode.value.connectionId)));
function togglePin() {
connectionStore.toggleTreeNodePin(activeNode.value);
}
function openVisibleDatabasesDialog() {
options.openVisibleDatabases(activeNode.value);
}
function openVisibleSchemasDialog() {
options.openVisibleSchemas(activeNode.value);
}
function startRenameGroup() {
if (activeNode.value.type === "connection-group") options.requestGroupRename(activeNode.value.id);
}
function deleteConnectionGroup() {
showDeleteGroupConfirm.value = true;
}
function newConnectionInGroup() {
connectionStore.startCreatingConnectionInGroup(activeNode.value.id);
}
function newSubgroup() {
const groupId = connectionStore.createConnectionGroup(t("connectionGroup.newGroupDefault"), activeNode.value.id);
connectionStore.selectedTreeNodeId = groupId;
options.groupCreated(groupId);
}
function confirmDeleteGroup() {
const node = sidebarFormTarget.value ?? activeNode.value;
connectionStore.deleteConnectionGroup(node.id);
options.releaseActiveNodeReference([node.id]);
showDeleteGroupConfirm.value = false;
toast(t("connection.groupDeleted"), 2000);
}
function moveToGroup(groupId: string | null) {
const connectionId = activeNode.value.connectionId;
if (connectionId) connectionStore.moveConnectionToGroup(connectionId, groupId);
}
return {
setNodeAsDefaultDatabase,
clearNodeDefaultDatabase,
connectionDeleteTargets,
connectionDeleteMenuLabel,
connectionDuplicateTargets,
connectionDuplicateMenuLabel,
connectionDeleteConfirmMessage,
deleteConnection,
confirmDelete,
copyFinalProxyPort,
duplicateConnection,
editConnection,
revealConnectionFilePath,
revealDatabaseFile,
sqliteBackupSource,
canBackupSqliteDatabase,
backupSqliteDatabase,
disconnectConnection,
cancelConnectionAttempt,
closeDatabaseConnection,
isPinned,
isNodeDefaultDatabase,
isConnected,
isConnecting,
canCloseDatabaseConnection,
canConfigureVisibleDatabases,
canConfigureVisibleSchemas,
canCopyFinalProxyPort,
togglePin,
openVisibleDatabasesDialog,
openVisibleSchemasDialog,
startRenameGroup,
deleteConnectionGroup,
newConnectionInGroup,
newSubgroup,
confirmDeleteGroup,
moveToGroup,
};
}