From 4776bb7ac9463602000b5c7e3b7409964f6247a0 Mon Sep 17 00:00:00 2001 From: invince Date: Fri, 10 Jan 2025 00:11:16 +0100 Subject: [PATCH 01/10] profile/secret, create new and switch tab or close tab, the new stays there #55 Be able to quick create secret in dropdown select list #125 - miss custom profile secret selection secret add icon for different type #124 - miss custom profile secret selection --- .../components/menu/cloud/cloud.component.ts | 2 +- .../custom-profile-form.component.html | 9 +++++++- .../custom-profile-form.component.ts | 23 ++++++++++++++++--- .../ssh-profile-form.component.html | 2 +- .../vnc-profile-form.component.ts | 2 +- .../profiles-menu/profiles-menu.component.ts | 2 +- .../quickconnect-menu.component.ts | 2 +- .../secret-form/secretFormMixin.ts | 2 +- .../secrets-menu/secrets-menu.component.ts | 2 +- .../groups-form/groups-form.component.ts | 2 +- .../setting-menu/setting-menu.component.ts | 2 +- src/app/services/profile.service.ts | 4 ++-- src/app/services/secret-storage.service.ts | 18 +++++++++++---- 13 files changed, 52 insertions(+), 20 deletions(-) diff --git a/src/app/components/menu/cloud/cloud.component.ts b/src/app/components/menu/cloud/cloud.component.ts index fb8eeec..b31ac0f 100644 --- a/src/app/components/menu/cloud/cloud.component.ts +++ b/src/app/components/menu/cloud/cloud.component.ts @@ -214,7 +214,7 @@ export class CloudComponent extends MenuComponent implements OnInit, OnDestroy { } filterSecret() { - return this.secretStorageService.data.secrets?.filter(one => one.secretType == SecretType.LOGIN_PASSWORD); + return this.secretStorageService.filter(one => one.secretType == SecretType.LOGIN_PASSWORD); } quickCreateSecret() { diff --git a/src/app/components/menu/profile-form/custom-profile-form/custom-profile-form.component.html b/src/app/components/menu/profile-form/custom-profile-form/custom-profile-form.component.html index 0aa90d9..80534c1 100644 --- a/src/app/components/menu/profile-form/custom-profile-form/custom-profile-form.component.html +++ b/src/app/components/menu/profile-form/custom-profile-form/custom-profile-form.component.html @@ -50,8 +50,15 @@ Secret - @for (oneSecret of secretStorageService.data.secrets; track oneSecret.id) { + + add + Add New... + + @for (oneSecret of filterSecret(); track oneSecret.id) { + @if (oneSecret.icon) { + {{oneSecret.icon}} + } {{ secretService.displaySecretOptionName(oneSecret) }} } diff --git a/src/app/components/menu/profile-form/custom-profile-form/custom-profile-form.component.ts b/src/app/components/menu/profile-form/custom-profile-form/custom-profile-form.component.ts index 8eece62..50d2316 100644 --- a/src/app/components/menu/profile-form/custom-profile-form/custom-profile-form.component.ts +++ b/src/app/components/menu/profile-form/custom-profile-form/custom-profile-form.component.ts @@ -1,9 +1,10 @@ -import {Component, forwardRef, Injector, Self} from '@angular/core'; +import {Component, forwardRef, Injector} from '@angular/core'; import {CommonModule} from '@angular/common'; import { FormBuilder, FormGroup, - FormsModule, NG_VALIDATORS, + FormsModule, + NG_VALIDATORS, NG_VALUE_ACCESSOR, ReactiveFormsModule, Validators @@ -16,7 +17,7 @@ import {MatIcon} from '@angular/material/icon'; import {MatIconButton} from '@angular/material/button'; import {ChildFormAsFormControl} from '../../../enhanced-form-mixin'; import {MenuComponent} from '../../menu.component'; -import {AuthType} from '../../../../domain/Secret'; +import {AuthType, SecretType} from '../../../../domain/Secret'; import {SecretStorageService} from '../../../../services/secret-storage.service'; import {SecretService} from '../../../../services/secret.service'; import {SettingStorageService} from '../../../../services/setting-storage.service'; @@ -27,6 +28,8 @@ import { ModelFieldWithPrecondition, ModelFormController } from '../../../../utils/ModelFormController'; +import {SecretQuickFormComponent} from '../../../dialog/secret-quick-form/secret-quick-form.component'; +import {MatDialog} from '@angular/material/dialog'; @Component({ selector: 'app-custom-profile-form', @@ -72,6 +75,7 @@ export class CustomProfileFormComponent extends ChildFormAsFormControl(MenuCompo public secretStorageService: SecretStorageService, // in html public secretService: SecretService, // in html public settingStorage: SettingStorageService, + public dialog: MatDialog, ) { super(); @@ -150,4 +154,17 @@ export class CustomProfileFormComponent extends ChildFormAsFormControl(MenuCompo this.form.get('secretId')?.setValue(null); } } + + quickCreateSecret() { + this.dialog.open(SecretQuickFormComponent, { + width: '650px', + data: { + secretTypes: [SecretType.LOGIN_PASSWORD, SecretType.PASSWORD_ONLY] + } + }); + } + + filterSecret() { + return this.secretStorageService.filter(one => one.secretType == SecretType.LOGIN_PASSWORD || one.secretType == SecretType.PASSWORD_ONLY); + } } diff --git a/src/app/components/menu/profile-form/ssh-profile-form/ssh-profile-form.component.html b/src/app/components/menu/profile-form/ssh-profile-form/ssh-profile-form.component.html index e0f2bd2..0adf237 100644 --- a/src/app/components/menu/profile-form/ssh-profile-form/ssh-profile-form.component.html +++ b/src/app/components/menu/profile-form/ssh-profile-form/ssh-profile-form.component.html @@ -88,7 +88,7 @@ add Add New... - @for (oneSecret of secretStorageService.data.secrets; track oneSecret.id) { + @for (oneSecret of secretStorageService.dataCopy.secrets; track oneSecret.id) { @if (oneSecret.icon) { {{oneSecret.icon}} diff --git a/src/app/components/menu/profile-form/vnc-profile-form/vnc-profile-form.component.ts b/src/app/components/menu/profile-form/vnc-profile-form/vnc-profile-form.component.ts index a56db50..1c66c8d 100644 --- a/src/app/components/menu/profile-form/vnc-profile-form/vnc-profile-form.component.ts +++ b/src/app/components/menu/profile-form/vnc-profile-form/vnc-profile-form.component.ts @@ -156,7 +156,7 @@ export class VncProfileFormComponent extends ChildFormAsFormControl(MenuComponen } filterSecret() { - return this.secretStorageService.data.secrets?.filter(one => one.secretType == SecretType.PASSWORD_ONLY); + return this.secretStorageService.filter(one => one.secretType == SecretType.PASSWORD_ONLY); } } diff --git a/src/app/components/menu/profiles-menu/profiles-menu.component.ts b/src/app/components/menu/profiles-menu/profiles-menu.component.ts index 2f53bb8..b38d1b1 100644 --- a/src/app/components/menu/profiles-menu/profiles-menu.component.ts +++ b/src/app/components/menu/profiles-menu/profiles-menu.component.ts @@ -105,7 +105,7 @@ export class ProfilesMenuComponent extends HasChildForm(MenuComponent) implement this.notification.info(message); } - this.profilesCopy = this.profileService.profiles; + this.profilesCopy = this.profileService.profilesCopy; this.profilesCopy.profiles = this.profilesCopy.profiles.sort((a: Profile, b: Profile) => a.name.localeCompare(b.name)); this.sideNavType = this.settingStorage.settings.ui.profileSideNavType; this.refreshForm(); diff --git a/src/app/components/menu/quickconnect-menu/quickconnect-menu.component.ts b/src/app/components/menu/quickconnect-menu/quickconnect-menu.component.ts index 2d6ddad..fac152f 100644 --- a/src/app/components/menu/quickconnect-menu/quickconnect-menu.component.ts +++ b/src/app/components/menu/quickconnect-menu/quickconnect-menu.component.ts @@ -42,7 +42,7 @@ export class QuickconnectMenuComponent extends MenuComponent implements OnInit } ngOnInit(): void { - this.profilesCopy = this.profileService.profiles; + this.profilesCopy = this.profileService.profilesCopy; } async onSaveOne($event: Profile) { diff --git a/src/app/components/menu/secrets-menu/secret-form/secretFormMixin.ts b/src/app/components/menu/secrets-menu/secret-form/secretFormMixin.ts index 50cb45c..6c5b2b3 100644 --- a/src/app/components/menu/secrets-menu/secret-form/secretFormMixin.ts +++ b/src/app/components/menu/secrets-menu/secret-form/secretFormMixin.ts @@ -39,7 +39,7 @@ export class SecretFormMixin { static secretNameShouldBeUnique(secretStorageService: SecretStorageService, secret: Secret | undefined = undefined) { // NOTE: inside validatorFn, we cannot use inject thing return (group: FormGroup) => { let name = group.get("name")?.value; - if (name && secretStorageService.data.secrets?.find(one => one.name === name && one.id !== secret?.id)) { + if (name && secretStorageService.dataCopy.secrets?.find(one => one.name === name && one.id !== secret?.id)) { return {duplicateSecret: true}; } return null; diff --git a/src/app/components/menu/secrets-menu/secrets-menu.component.ts b/src/app/components/menu/secrets-menu/secrets-menu.component.ts index 4a4c40b..a6595a0 100644 --- a/src/app/components/menu/secrets-menu/secrets-menu.component.ts +++ b/src/app/components/menu/secrets-menu/secrets-menu.component.ts @@ -93,7 +93,7 @@ export class SecretsMenuComponent extends HasChildForm(MenuComponent) implements this.secretService.reload(); } - this.secretsCopy = this.secretStorageService.data; + this.secretsCopy = this.secretStorageService.dataCopy; this.secretsCopy.secrets = this.secretsCopy.secrets.sort((a: Secret, b: Secret) => a.name.localeCompare(b.name)); } diff --git a/src/app/components/menu/setting-menu/groups-form/groups-form.component.ts b/src/app/components/menu/setting-menu/groups-form/groups-form.component.ts index ea239e1..37864ac 100644 --- a/src/app/components/menu/setting-menu/groups-form/groups-form.component.ts +++ b/src/app/components/menu/setting-menu/groups-form/groups-form.component.ts @@ -66,7 +66,7 @@ export class GroupsFormComponent implements OnInit{ createGroupDataSource(): GroupNode[] { return GroupNode.map2DataSource( this.settingStorage.settings.groups, - this.profileService.profiles.profiles, + this.profileService.profilesCopy.profiles, true, false, (group, node) => { diff --git a/src/app/components/menu/setting-menu/setting-menu.component.ts b/src/app/components/menu/setting-menu/setting-menu.component.ts index 8f509a2..898d448 100644 --- a/src/app/components/menu/setting-menu/setting-menu.component.ts +++ b/src/app/components/menu/setting-menu/setting-menu.component.ts @@ -367,7 +367,7 @@ export class SettingMenuComponent extends MenuComponent implements OnInit, OnDes } filterSecret() { - return this.secretStorageService.data.secrets?.filter(one => one.secretType == SecretType.LOGIN_PASSWORD); + return this.secretStorageService.filter(one => one.secretType == SecretType.LOGIN_PASSWORD); } } diff --git a/src/app/services/profile.service.ts b/src/app/services/profile.service.ts index f192cc6..7b66b29 100644 --- a/src/app/services/profile.service.ts +++ b/src/app/services/profile.service.ts @@ -82,7 +82,7 @@ export class ProfileService implements OnDestroy{ } // NOTE: if you add any field on Profiles, you need copy it here - get profiles(): Profiles { + get profilesCopy(): Profiles { let result = new Profiles(); // to avoid if this._profiles is deserialized we don't have fn on it if (this._profiles) { result.profiles = [...this._profiles.profiles]; // copy the elements @@ -93,7 +93,7 @@ export class ProfileService implements OnDestroy{ return result; } - async save(profiles: Profiles = this.profiles) { + async save(profiles: Profiles = this._profiles) { if (!profiles) { profiles = new Profiles(); } diff --git a/src/app/services/secret-storage.service.ts b/src/app/services/secret-storage.service.ts index 6fd0828..9eb91c8 100644 --- a/src/app/services/secret-storage.service.ts +++ b/src/app/services/secret-storage.service.ts @@ -1,5 +1,5 @@ import { Injectable } from '@angular/core'; -import {Secret, Secrets} from '../domain/Secret'; +import {Secret, Secrets, SecretType} from '../domain/Secret'; @Injectable({ providedIn: 'root' @@ -14,15 +14,23 @@ export class SecretStorageService { this._data = data; } - get data(): Secrets { - if (!this._data) { - this._data = new Secrets(); + get dataCopy(): Secrets { + let result = new Secrets(); // to avoid if this._data is deserialized we don't have fn on it + if (this._data) { + result.secrets = [...this._data.secrets]; // copy elements + result.version = this._data.version; + result.compatibleVersion = this._data.compatibleVersion; + result.revision = this._data.revision; } - return this._data; + return result; } findById(id: string): Secret | undefined { return this._data.secrets.find(one => one.id == id); } + filter(predicate : (one:Secret) => boolean): Secret[] { + return this._data.secrets?.filter(predicate); + } + } From 0008809fe58d954d792388fc7f5f18f64e769a58 Mon Sep 17 00:00:00 2001 From: invince Date: Sat, 11 Jan 2025 13:39:23 +0100 Subject: [PATCH 02/10] when open new tab, sometime, we don't switch to the correct one #130 use id instead length -1 --- src/app/app.component.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/app/app.component.ts b/src/app/app.component.ts index c557839..216f675 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -107,8 +107,9 @@ export class AppComponent implements OnInit, OnDestroy{ } this.modalControl.closeModal([this.MENU_PROFILE, this.MENU_ADD ]); if (Profile.requireOpenNewTab(profile)) { - this.tabService.addTab(new TabInstance(profile.category, this.sessionService.create(profile, profile.profileType))); // Adds a new terminal identifier - this.tabService.currentTabIndex = this.tabService.tabs.length - 1; + const tab = new TabInstance(profile.category, this.sessionService.create(profile, profile.profileType)); + this.tabService.addTab(tab); // Adds a new terminal identifier + this.tabService.currentTabIndex = this.tabService.tabs.findIndex(one => one.id === tab.id); } else { this.sessionService.openSessionWithoutTab(profile); } @@ -159,9 +160,10 @@ export class AppComponent implements OnInit, OnDestroy{ addLocalTerminal() { this.modalControl.closeModal(); - this.tabService.addTab(new TabInstance( ProfileCategory.TERMINAL, - this.sessionService.create(this.settingService.createLocalTerminalProfile(),ProfileType.LOCAL_TERMINAL))); // Adds a new terminal identifier - this.tabService.currentTabIndex = this.tabService.tabs.length - 1; + const tab = new TabInstance( ProfileCategory.TERMINAL, + this.sessionService.create(this.settingService.createLocalTerminalProfile(),ProfileType.LOCAL_TERMINAL)); + this.tabService.addTab(tab); // Adds a new terminal identifier + this.tabService.currentTabIndex = this.tabService.tabs.findIndex(one => one.id === tab.id); } From 8f9a64b7a58a4f85eff36a5ef9da6eb9c4ffaf03 Mon Sep 17 00:00:00 2001 From: invince Date: Sat, 11 Jan 2025 16:54:38 +0100 Subject: [PATCH 03/10] reconnect button/ closed status is not ok #129 --- TestPlan.md | 2 ++ src-electron/ipc/terminal.js | 3 +++ src/app/app.component.ts | 2 +- .../components/terminal/terminal.component.ts | 27 +++++++++++-------- src/app/domain/TabInstance.ts | 6 ----- src/app/domain/session/Session.ts | 6 +++++ src/app/services/electron.service.ts | 4 ++- src/app/services/session.service.ts | 12 +++++++++ src/app/services/tab.service.ts | 6 ----- 9 files changed, 43 insertions(+), 25 deletions(-) diff --git a/TestPlan.md b/TestPlan.md index fe88ff3..9c286ce 100644 --- a/TestPlan.md +++ b/TestPlan.md @@ -120,6 +120,8 @@ - download ok # Terminal +- reconnect: open a ssh terminal, reboot, then the reconnect button should appear + - after you click it, your session should be reconnected ## Local Terminal - (Settings) if default open option checked, then a local terminal should start at startup diff --git a/src-electron/ipc/terminal.js b/src-electron/ipc/terminal.js index f81e9c3..ad71cdb 100644 --- a/src-electron/ipc/terminal.js +++ b/src-electron/ipc/terminal.js @@ -136,12 +136,15 @@ function initTerminalIpcHandler(log, terminalMap) { // Handle end event conn.on('end', () => { log.info('SSH connection ended for id:', id); + }); // Handle close event conn.on('close', (hadError) => { if (hadError) { log.info(`SSH connection closed for id: ${id}, hadError: ${hadError}`); + event.sender.send('session.disconnect.terminal.ssh', { id: id , error: hadError }); + } else { event.sender.send('session.disconnect.terminal.ssh', { id: id }); } }); diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 216f675..22f1846 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -155,7 +155,7 @@ export class AppComponent implements OnInit, OnDestroy{ } reconnect(i: number) { - this.tabService.reconnect(i); + this.sessionService.reconnect(i); } addLocalTerminal() { diff --git a/src/app/components/terminal/terminal.component.ts b/src/app/components/terminal/terminal.component.ts index d09f2ed..c33e9cf 100644 --- a/src/app/components/terminal/terminal.component.ts +++ b/src/app/components/terminal/terminal.component.ts @@ -32,9 +32,10 @@ export class TerminalComponent implements AfterViewInit, OnChanges, OnDestroy { private isViewInitialized = false; private xtermUnderlying : Terminal | undefined; - subscriptions: Subscription[] = []; private webglAddon = new WebglAddon(); + private terminalOnDataSubscription?: Subscription; + constructor( private electron: ElectronService, ) { @@ -88,15 +89,7 @@ export class TerminalComponent implements AfterViewInit, OnChanges, OnDestroy { } this.initTab(); - // Listen to output from Electron and display in xterm - this.electron.onTerminalOutput(this.session.id, (data) => { - this.terminal.write(data.data); - }); - // Send user input back to Electron main process - this.subscriptions.push(this.terminal.onData().subscribe(data => { - this.electron.sendTerminalInput(this.session.id, data); - })); this.isViewInitialized = true; } @@ -116,12 +109,24 @@ export class TerminalComponent implements AfterViewInit, OnChanges, OnDestroy { // Set up data listeners and communication with the Electron main process this.session.open(); + // Listen to output from Electron and display in xterm + this.electron.onTerminalOutput(this.session.id, (data) => { + this.terminal.write(data.data); + }); + + // Send user input back to Electron main process + if (this.terminalOnDataSubscription) { + this.terminalOnDataSubscription.unsubscribe(); + } + this.terminalOnDataSubscription = this.terminal.onData().subscribe(data => { + this.electron.sendTerminalInput(this.session.id, data); + }); } ngOnDestroy() { this.session.close(); - if (this.subscriptions) { - this.subscriptions.forEach(one => one.unsubscribe()); + if (this.terminalOnDataSubscription) { + this.terminalOnDataSubscription.unsubscribe(); } } } diff --git a/src/app/domain/TabInstance.ts b/src/app/domain/TabInstance.ts index 5ba1161..441acfc 100644 --- a/src/app/domain/TabInstance.ts +++ b/src/app/domain/TabInstance.ts @@ -24,10 +24,4 @@ export class TabInstance { } } - - clone() { - let tab = new TabInstance(this.category, this.session); - tab.name = this.name; - return tab; - } } diff --git a/src/app/domain/session/Session.ts b/src/app/domain/session/Session.ts index c225532..eb8e4a2 100644 --- a/src/app/domain/session/Session.ts +++ b/src/app/domain/session/Session.ts @@ -6,10 +6,16 @@ export class Session { readonly id: string = uuidv4(); // uuid constructor(public profile: Profile, public profileType: ProfileType, protected tabService: TabService) { } + open(...args: any): void { this.tabService.connected(this.id); }; + close(...args: any): void { this.tabService.disconnected(this.id); }; + + clone(): Session { + return new Session(this.profile, this.profileType, this.tabService); + } } diff --git a/src/app/services/electron.service.ts b/src/app/services/electron.service.ts index 1502962..7333ff8 100644 --- a/src/app/services/electron.service.ts +++ b/src/app/services/electron.service.ts @@ -129,7 +129,9 @@ export class ElectronService { this.ipc.on(SESSION_DISCONNECT_SSH, (event, data) => { this.log({level: 'info', message: 'SSH Disconnected:' + data.id}); - this.notification.error('SSH Disconnected, you can try reconnect later'); + if (data.error) { + this.notification.error('SSH Disconnected, you can try reconnect later'); + } this.tabService.disconnected(data.id); // Handle disconnection logic }); diff --git a/src/app/services/session.service.ts b/src/app/services/session.service.ts index 113e0fc..ed8c39f 100644 --- a/src/app/services/session.service.ts +++ b/src/app/services/session.service.ts @@ -11,6 +11,7 @@ import {VncService} from './vnc.service'; import {ScpSession} from '../domain/session/ScpSession'; import {ScpService} from './scp.service'; import {NotificationService} from './notification.service'; +import {TabInstance} from '../domain/TabInstance'; @Injectable({ providedIn: 'root' @@ -65,4 +66,15 @@ export class SessionService { } } } + + reconnect(i: number) { + if(this.tabService.tabs) { + let oldTab = this.tabService.tabs[i]; + let oldSession = oldTab.session; + let newSession = this.create(oldSession.profile, oldSession.profileType); + let newTab = new TabInstance(oldTab.category, newSession); + newTab.name = oldTab.name; + this.tabService.tabs[i] = newTab; + } + } } diff --git a/src/app/services/tab.service.ts b/src/app/services/tab.service.ts index 09a640f..0c678b4 100644 --- a/src/app/services/tab.service.ts +++ b/src/app/services/tab.service.ts @@ -41,12 +41,6 @@ export class TabService { this._tabs.push(tabInstance); } - reconnect(i: number) { - if(this._tabs) { - this._tabs[i] = this._tabs[i].clone(); - } - } - getSelectedTab() { if(this._tabs) { return this._tabs[this.currentTabIndex]; From 464e40c1f9427843ba72f71c7d5e5e515d1e7db0 Mon Sep 17 00:00:00 2001 From: invince Date: Sat, 11 Jan 2025 21:10:00 +0100 Subject: [PATCH 04/10] when open new tab, sometime, we don't switch to the correct one #130 --- src/app/app.component.html | 2 +- src/app/app.component.ts | 3 ++- src/app/services/tab.service.ts | 7 +++++++ 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/app/app.component.html b/src/app/app.component.html index 77cebc4..21954bc 100644 --- a/src/app/app.component.html +++ b/src/app/app.component.html @@ -26,7 +26,7 @@ - + @for (tab of tabService.tabs ; let i = $index; track tab.id) { diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 22f1846..f051bc9 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -146,7 +146,8 @@ export class AppComponent implements OnInit, OnDestroy{ } removeTab(index: number) { - this.tabService.tabs.splice(index, 1); + this.tabService.removeTab(index); + } diff --git a/src/app/services/tab.service.ts b/src/app/services/tab.service.ts index 0c678b4..6326c23 100644 --- a/src/app/services/tab.service.ts +++ b/src/app/services/tab.service.ts @@ -47,4 +47,11 @@ export class TabService { } return; } + + removeTab(index: any) { + if (index <= this.currentTabIndex && this.currentTabIndex != 0) { + this.currentTabIndex --; + } + this._tabs.splice(index, 1); + } } From 2e0d57068e48b4867f117dbb2ebc992b58daef96 Mon Sep 17 00:00:00 2001 From: invince Date: Sat, 11 Jan 2025 22:19:09 +0100 Subject: [PATCH 05/10] in profile view, double click can connect directly #131 --- .../menu/profiles-menu/profiles-menu.component.html | 6 +++--- .../menu/profiles-menu/profiles-menu.component.ts | 6 ++++++ 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/app/components/menu/profiles-menu/profiles-menu.component.html b/src/app/components/menu/profiles-menu/profiles-menu.component.html index df66685..1c657a8 100644 --- a/src/app/components/menu/profiles-menu/profiles-menu.component.html +++ b/src/app/components/menu/profiles-menu/profiles-menu.component.html @@ -42,7 +42,7 @@

Profiles

+ (click)="onTabChange(node.profile)" (dblclick)="onTabConnect(node.profile)"> @if (node.profile.icon) { {{node.profile.icon}} } @@ -78,12 +78,12 @@

Profiles

} - @else { + @else { // in list mode @for (profile of profilesCopy.profiles | filterKeyword:keywordsProviders:filter; track profile.id) {