feat: add visual drag-and-drop reordering indicators and improve event handling for connection items #4

Merged
jamie merged 2 commits from v1.1.2 into main 2026-06-09 15:47:26 +01:00
3 changed files with 44 additions and 7 deletions
+11 -1
View File
@@ -91,12 +91,22 @@ async function loadConnections() {
draggedConnId = null; draggedConnId = null;
el.style.opacity = '1'; el.style.opacity = '1';
}); });
el.addEventListener('dragenter', (e) => {
e.preventDefault();
if (draggedConnId && draggedConnId !== c.id) {
el.style.borderTop = '2px solid var(--accent)';
}
});
el.addEventListener('dragleave', (e) => {
el.style.borderTop = '';
});
el.addEventListener('dragover', (e) => { el.addEventListener('dragover', (e) => {
e.preventDefault(); e.preventDefault();
e.dataTransfer.dropEffect = 'move'; e.dataTransfer.dropEffect = 'move';
}); });
el.addEventListener('drop', async (e) => { el.addEventListener('drop', async (e) => {
e.preventDefault(); e.preventDefault();
el.style.borderTop = '';
if (!draggedConnId || draggedConnId === c.id) return; if (!draggedConnId || draggedConnId === c.id) return;
const ids = connectionsCache.map(conn => conn.id); const ids = connectionsCache.map(conn => conn.id);
@@ -109,10 +119,10 @@ async function loadConnections() {
try { try {
await ReorderConnections(ids); await ReorderConnections(ids);
await loadConnections();
} catch (err) { } catch (err) {
console.error("Failed to reorder connections:", err); console.error("Failed to reorder connections:", err);
} }
await loadConnections();
} }
}); });
+8
View File
@@ -57,6 +57,14 @@ body {
transition: background 0.2s; transition: background 0.2s;
} }
.conn-item * {
pointer-events: none;
}
.conn-item .edit-btn {
pointer-events: auto;
}
.conn-item:hover { .conn-item:hover {
background: var(--surface-hover); background: var(--surface-hover);
} }
+25 -6
View File
@@ -32,7 +32,7 @@ func (e *SFTPExplorer) Connect() error {
port = 22 port = 22
} }
var authMethod ssh.AuthMethod var authMethods []ssh.AuthMethod
secretStr := strings.TrimSpace(e.secret) secretStr := strings.TrimSpace(e.secret)
// Handle case where newlines might be escaped by mistake // Handle case where newlines might be escaped by mistake
@@ -43,16 +43,35 @@ func (e *SFTPExplorer) Connect() error {
if err != nil { if err != nil {
return fmt.Errorf("failed to parse private key: %w", err) return fmt.Errorf("failed to parse private key: %w", err)
} }
authMethod = ssh.PublicKeys(signer) authMethods = append(authMethods, ssh.PublicKeys(signer))
} else { } else {
authMethod = ssh.Password(e.secret) authMethods = append(authMethods, ssh.Password(e.secret))
} }
// Always add KeyboardInteractive to support Duo MFA and servers that require interactive auth.
authMethods = append(authMethods, ssh.KeyboardInteractive(func(user, instruction string, questions []string, echos []bool) (answers []string, err error) {
answers = make([]string, len(questions))
for i, q := range questions {
qLower := strings.ToLower(q)
if strings.Contains(secretStr, "-----BEGIN") {
// If using a private key, we don't have a password.
// If it's a Duo prompt asking for an option, "1" is typically Duo Push.
if strings.Contains(qLower, "duo push") || strings.Contains(qLower, "passcode or option") {
answers[i] = "1"
} else {
answers[i] = ""
}
} else {
// Otherwise, respond with the password to keyboard interactive prompts
answers[i] = e.secret
}
}
return answers, nil
}))
config := &ssh.ClientConfig{ config := &ssh.ClientConfig{
User: e.cfg.Username, User: e.cfg.Username,
Auth: []ssh.AuthMethod{ Auth: authMethods,
authMethod,
},
HostKeyCallback: ssh.InsecureIgnoreHostKey(), HostKeyCallback: ssh.InsecureIgnoreHostKey(),
} }