Connection manager fixes: folder select, open-all-in-tabs, stable hover

- Folder select showed '(none)' regardless of the actual folder (Angular sets
  [value] on the select before its options render), so 'move out of folder'
  silently saved the old folderId. Per-option [selected] bindings fix both the
  display and the save.
- Folder rows gain 'Open all in tabs': each connection opens in its own new
  tab with the left panel connecting (right stays local); manager closes.
- Folder-row action buttons no longer shift the row on hover (opacity toggle
  instead of display toggle); also canonicalized accent-primary classes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Jokin
2026-07-11 19:37:51 +02:00
co-authored by Claude Fable 5
parent 2d7e7b44cd
commit 7712655c3d
4 changed files with 36 additions and 9 deletions
@@ -43,15 +43,24 @@
</button>
}
@for (group of store.grouped().folders; track group.folder.id) {
<div class="group flex items-center gap-1.5 px-3 pt-2 pb-0.5">
<div class="group flex items-center gap-1 px-3 pt-2 pb-0.5">
<ng-icon name="mynaFolder" size="0.85rem" class="shrink-0 text-muted-foreground" />
<span class="flex-1 truncate text-[11px] font-medium text-muted-foreground uppercase">
{{ group.folder.name }}
</span>
@if (group.connections.length) {
<button
(click)="openFolder(group.connections)"
title="Open all in tabs"
class="grid size-4 shrink-0 place-items-center rounded text-muted-foreground opacity-0 group-hover:opacity-100 hover:text-primary"
>
<ng-icon name="mynaArrowRight" size="0.75rem" />
</button>
}
<button
(click)="deleteFolder(group.folder.id, group.folder.name)"
title="Delete folder"
class="hidden rounded p-0.5 text-muted-foreground group-hover:block hover:text-destructive"
class="grid size-4 shrink-0 place-items-center rounded text-muted-foreground opacity-0 group-hover:opacity-100 hover:text-destructive"
>
<ng-icon name="mynaTrash" size="0.75rem" />
</button>
@@ -115,13 +124,16 @@
<label class="flex flex-col gap-1 text-xs">
<span class="text-muted-foreground">Folder</span>
<select
[value]="d.folderId ?? ''"
(change)="patch('folderId', $any($event.target).value || null)"
class="h-7 rounded-md border border-input bg-background px-1 outline-none focus:ring-2 focus:ring-ring"
>
<option value="">(none)</option>
<!-- [selected] per option: binding [value] on the <select> runs
before the options render and silently shows "(none)" -->
<option value="" [selected]="!d.folderId">(none)</option>
@for (folder of store.folders(); track folder.id) {
<option [value]="folder.id">{{ folder.name }}</option>
<option [value]="folder.id" [selected]="d.folderId === folder.id">
{{ folder.name }}
</option>
}
</select>
</label>
@@ -155,7 +167,7 @@
name="authKind"
[checked]="d.authKind === 'password'"
(change)="patch('authKind', 'password')"
class="accent-[var(--primary)]"
class="accent-primary"
/>
Password
</label>
@@ -165,7 +177,7 @@
name="authKind"
[checked]="d.authKind === 'key'"
(change)="patch('authKind', 'key')"
class="accent-[var(--primary)]"
class="accent-primary"
/>
Private key
</label>
@@ -149,6 +149,11 @@ export class ConnectionManager {
});
}
openFolder(connections: SavedConnection[]): void {
this.panels.openInNewTabs(connections);
this.close();
}
newFolder(): void {
this.dialogs.prompt.set({
title: 'New folder',
+1 -1
View File
@@ -36,7 +36,7 @@ const PERMS = ['r', 'w', 'x'] as const;
type="checkbox"
[checked]="hasBit(r, p)"
(change)="toggleBit(r, p)"
class="size-3.5 accent-[var(--primary)]"
class="size-3.5 accent-primary"
/>
</td>
}
+11 -1
View File
@@ -1,5 +1,5 @@
import { Injectable, computed, signal, type WritableSignal } from '@angular/core';
import type { ConnectionConfig, FileEntry } from '@shared/types';
import type { ConnectionConfig, FileEntry, SavedConnection } from '@shared/types';
import { RpcService } from './rpc.service';
import { ToastService } from './toast.service';
@@ -280,6 +280,16 @@ export class PanelsService {
}
}
/** One new tab per connection, its left panel connecting (right stays local). */
openInNewTabs(connections: SavedConnection[]): void {
for (const connection of connections) {
this.newWorkspace();
// connectPanel resolves the panel synchronously (before awaiting), so
// firing these back-to-back binds each connection to its own tab
void this.connectPanel('left', connection, connection.name);
}
}
/** Reorder a tab (drag & drop). Order is purely visual; ids stay stable. */
moveWorkspace(id: number, toIndex: number): void {
this.workspaces.update((list) => {