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:
@@ -43,15 +43,24 @@
|
|||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
@for (group of store.grouped().folders; track group.folder.id) {
|
@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" />
|
<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">
|
<span class="flex-1 truncate text-[11px] font-medium text-muted-foreground uppercase">
|
||||||
{{ group.folder.name }}
|
{{ group.folder.name }}
|
||||||
</span>
|
</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
|
<button
|
||||||
(click)="deleteFolder(group.folder.id, group.folder.name)"
|
(click)="deleteFolder(group.folder.id, group.folder.name)"
|
||||||
title="Delete folder"
|
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" />
|
<ng-icon name="mynaTrash" size="0.75rem" />
|
||||||
</button>
|
</button>
|
||||||
@@ -115,13 +124,16 @@
|
|||||||
<label class="flex flex-col gap-1 text-xs">
|
<label class="flex flex-col gap-1 text-xs">
|
||||||
<span class="text-muted-foreground">Folder</span>
|
<span class="text-muted-foreground">Folder</span>
|
||||||
<select
|
<select
|
||||||
[value]="d.folderId ?? ''"
|
|
||||||
(change)="patch('folderId', $any($event.target).value || null)"
|
(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"
|
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) {
|
@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>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
@@ -155,7 +167,7 @@
|
|||||||
name="authKind"
|
name="authKind"
|
||||||
[checked]="d.authKind === 'password'"
|
[checked]="d.authKind === 'password'"
|
||||||
(change)="patch('authKind', 'password')"
|
(change)="patch('authKind', 'password')"
|
||||||
class="accent-[var(--primary)]"
|
class="accent-primary"
|
||||||
/>
|
/>
|
||||||
Password
|
Password
|
||||||
</label>
|
</label>
|
||||||
@@ -165,7 +177,7 @@
|
|||||||
name="authKind"
|
name="authKind"
|
||||||
[checked]="d.authKind === 'key'"
|
[checked]="d.authKind === 'key'"
|
||||||
(change)="patch('authKind', 'key')"
|
(change)="patch('authKind', 'key')"
|
||||||
class="accent-[var(--primary)]"
|
class="accent-primary"
|
||||||
/>
|
/>
|
||||||
Private key
|
Private key
|
||||||
</label>
|
</label>
|
||||||
|
|||||||
@@ -149,6 +149,11 @@ export class ConnectionManager {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
openFolder(connections: SavedConnection[]): void {
|
||||||
|
this.panels.openInNewTabs(connections);
|
||||||
|
this.close();
|
||||||
|
}
|
||||||
|
|
||||||
newFolder(): void {
|
newFolder(): void {
|
||||||
this.dialogs.prompt.set({
|
this.dialogs.prompt.set({
|
||||||
title: 'New folder',
|
title: 'New folder',
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ const PERMS = ['r', 'w', 'x'] as const;
|
|||||||
type="checkbox"
|
type="checkbox"
|
||||||
[checked]="hasBit(r, p)"
|
[checked]="hasBit(r, p)"
|
||||||
(change)="toggleBit(r, p)"
|
(change)="toggleBit(r, p)"
|
||||||
class="size-3.5 accent-[var(--primary)]"
|
class="size-3.5 accent-primary"
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Injectable, computed, signal, type WritableSignal } from '@angular/core';
|
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 { RpcService } from './rpc.service';
|
||||||
import { ToastService } from './toast.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. */
|
/** Reorder a tab (drag & drop). Order is purely visual; ids stay stable. */
|
||||||
moveWorkspace(id: number, toIndex: number): void {
|
moveWorkspace(id: number, toIndex: number): void {
|
||||||
this.workspaces.update((list) => {
|
this.workspaces.update((list) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user