From 2706045cc2e5e0e093d12fd5a82fe193eb412679 Mon Sep 17 00:00:00 2001 From: Eugene Pankov Date: Sun, 11 Jul 2021 12:38:35 +0200 Subject: [PATCH] tab context menu option to save split layouts as profiles - fixes #3468 --- tabby-core/src/api/profileProvider.ts | 2 +- .../src/components/promptModal.component.pug | 12 ++-- .../src/components/splitTab.component.ts | 2 +- tabby-core/src/index.ts | 6 +- tabby-core/src/profiles.ts | 57 +++++++++++++++++++ tabby-core/src/tabContextMenu.ts | 20 +++++++ .../components/editProfileModal.component.pug | 2 +- .../components/editProfileModal.component.ts | 21 ++++--- .../profilesSettingsTab.component.ts | 1 + 9 files changed, 103 insertions(+), 20 deletions(-) create mode 100644 tabby-core/src/profiles.ts diff --git a/tabby-core/src/api/profileProvider.ts b/tabby-core/src/api/profileProvider.ts index 397cd374..f8327ae8 100644 --- a/tabby-core/src/api/profileProvider.ts +++ b/tabby-core/src/api/profileProvider.ts @@ -28,7 +28,7 @@ export abstract class ProfileProvider { id: string name: string supportsQuickConnect = false - settingsComponent: new (...args: any[]) => ProfileSettingsComponent + settingsComponent?: new (...args: any[]) => ProfileSettingsComponent configDefaults = {} abstract getBuiltinProfiles (): Promise diff --git a/tabby-core/src/components/promptModal.component.pug b/tabby-core/src/components/promptModal.component.pug index 1dfb759c..a1176dca 100644 --- a/tabby-core/src/components/promptModal.component.pug +++ b/tabby-core/src/components/promptModal.component.pug @@ -1,19 +1,19 @@ .modal-body input.form-control( - [type]='password ? "password" : "text"', + [type]='password ? "password" : "text"', autofocus, - [(ngModel)]='value', - #input, - [placeholder]='prompt', + [(ngModel)]='value', + #input, + [placeholder]='prompt', (keyup.enter)='ok()', (keyup.esc)='cancel()', ) .d-flex.align-items-start.mt-2 checkbox( *ngIf='showRememberCheckbox', - [(ngModel)]='remember', + [(ngModel)]='remember', text='Remember' ) button.btn.btn-primary.ml-auto( (click)='ok()', - ) Enter + ) OK diff --git a/tabby-core/src/components/splitTab.component.ts b/tabby-core/src/components/splitTab.component.ts index 64d2b834..63e43ae7 100644 --- a/tabby-core/src/components/splitTab.component.ts +++ b/tabby-core/src/components/splitTab.component.ts @@ -636,7 +636,7 @@ export class SplitTabComponent extends BaseTabComponent implements AfterViewInit } /** @hidden */ -@Injectable() +@Injectable({ providedIn: 'root' }) export class SplitTabRecoveryProvider extends TabRecoveryProvider { async applicableTo (recoveryToken: RecoveryToken): Promise { return recoveryToken.type === 'app:split-tab' diff --git a/tabby-core/src/index.ts b/tabby-core/src/index.ts index 419da2ae..b07823f5 100644 --- a/tabby-core/src/index.ts +++ b/tabby-core/src/index.ts @@ -30,7 +30,7 @@ import { AlwaysVisibleTypeaheadDirective } from './directives/alwaysVisibleTypea import { FastHtmlBindDirective } from './directives/fastHtmlBind.directive' import { DropZoneDirective } from './directives/dropZone.directive' -import { Theme, CLIHandler, TabContextMenuItemProvider, TabRecoveryProvider, HotkeyProvider, ConfigProvider, PlatformService, FileProvider, ToolbarButtonProvider, ProfilesService } from './api' +import { Theme, CLIHandler, TabContextMenuItemProvider, TabRecoveryProvider, HotkeyProvider, ConfigProvider, PlatformService, FileProvider, ToolbarButtonProvider, ProfilesService, ProfileProvider } from './api' import { AppService } from './services/app.service' import { ConfigService } from './services/config.service' @@ -43,6 +43,7 @@ import { AppHotkeyProvider } from './hotkeys' import { TaskCompletionContextMenu, CommonOptionsContextMenu, TabManagementContextMenu, ProfilesContextMenu } from './tabContextMenu' import { LastCLIHandler, ProfileCLIHandler } from './cli' import { ButtonProvider } from './buttonProvider' +import { SplitLayoutProfilesService } from './profiles' import 'perfect-scrollbar/css/perfect-scrollbar.css' import 'ng2-dnd/bundles/style.css' @@ -57,12 +58,13 @@ const PROVIDERS = [ { provide: TabContextMenuItemProvider, useClass: TabManagementContextMenu, multi: true }, { provide: TabContextMenuItemProvider, useClass: TaskCompletionContextMenu, multi: true }, { provide: TabContextMenuItemProvider, useClass: ProfilesContextMenu, multi: true }, - { provide: TabRecoveryProvider, useClass: SplitTabRecoveryProvider, multi: true }, + { provide: TabRecoveryProvider, useExisting: SplitTabRecoveryProvider, multi: true }, { provide: CLIHandler, useClass: ProfileCLIHandler, multi: true }, { provide: CLIHandler, useClass: LastCLIHandler, multi: true }, { provide: PERFECT_SCROLLBAR_CONFIG, useValue: { suppressScrollX: true } }, { provide: FileProvider, useClass: VaultFileProvider, multi: true }, { provide: ToolbarButtonProvider, useClass: ButtonProvider, multi: true }, + { provide: ProfileProvider, useExisting: SplitLayoutProfilesService, multi: true }, ] /** @hidden */ diff --git a/tabby-core/src/profiles.ts b/tabby-core/src/profiles.ts new file mode 100644 index 00000000..b819c9d6 --- /dev/null +++ b/tabby-core/src/profiles.ts @@ -0,0 +1,57 @@ +import slugify from 'slugify' +import { v4 as uuidv4 } from 'uuid' +import { Injectable } from '@angular/core' +import { ConfigService, NewTabParameters, Profile, ProfileProvider } from './api' +import { SplitTabComponent, SplitTabRecoveryProvider } from './components/splitTab.component' + +export interface SplitLayoutProfileOptions { + recoveryToken: any +} + +export interface SplitLayoutProfile extends Profile { + options: SplitLayoutProfileOptions +} + +@Injectable({ providedIn: 'root' }) +export class SplitLayoutProfilesService extends ProfileProvider { + id = 'split-layout' + name = 'Saved layout' + configDefaults = { + options: { + recoveryToken: null, + }, + } + + constructor ( + private splitTabRecoveryProvider: SplitTabRecoveryProvider, + private config: ConfigService, + ) { + super() + } + + async getBuiltinProfiles (): Promise { + return [] + } + + async getNewTabParameters (profile: SplitLayoutProfile): Promise> { + return this.splitTabRecoveryProvider.recover(profile.options.recoveryToken) + } + + getDescription (_: SplitLayoutProfile): string { + return '' + } + + async createProfile (tab: SplitTabComponent, name: string): Promise { + const token = await tab.getRecoveryToken() + const profile: SplitLayoutProfile = { + id: `${this.id}:custom:${slugify(name)}:${uuidv4()}`, + type: this.id, + name, + options: { + recoveryToken: token, + }, + } + this.config.store.profiles.push(profile) + await this.config.save() + } +} diff --git a/tabby-core/src/tabContextMenu.ts b/tabby-core/src/tabContextMenu.ts index 809256df..a444fc36 100644 --- a/tabby-core/src/tabContextMenu.ts +++ b/tabby-core/src/tabContextMenu.ts @@ -1,5 +1,6 @@ /* eslint-disable @typescript-eslint/explicit-module-boundary-types */ import { Injectable } from '@angular/core' +import { NgbModal } from '@ng-bootstrap/ng-bootstrap' import { Subscription } from 'rxjs' import { AppService } from './services/app.service' import { BaseTabComponent } from './components/baseTab.component' @@ -10,6 +11,8 @@ import { MenuItemOptions } from './api/menu' import { ProfilesService } from './services/profiles.service' import { TabsService } from './services/tabs.service' import { HotkeysService } from './services/hotkeys.service' +import { PromptModalComponent } from './components/promptModal.component' +import { SplitLayoutProfilesService } from './profiles' /** @hidden */ @Injectable() @@ -103,6 +106,8 @@ export class CommonOptionsContextMenu extends TabContextMenuItemProvider { constructor ( private app: AppService, + private ngbModal: NgbModal, + private splitLayoutProfilesService: SplitLayoutProfilesService, ) { super() } @@ -133,6 +138,21 @@ export class CommonOptionsContextMenu extends TabContextMenuItemProvider { })) as MenuItemOptions[], }, ] + + if (tab instanceof SplitTabComponent && tab.getAllTabs().length > 1) { + items.push({ + label: 'Save layout as profile', + click: async () => { + const modal = this.ngbModal.open(PromptModalComponent) + modal.componentInstance.prompt = 'Profile name' + const name = (await modal.result)?.value + if (!name) { + return + } + this.splitLayoutProfilesService.createProfile(tab, name) + }, + }) + } } return items } diff --git a/tabby-settings/src/components/editProfileModal.component.pug b/tabby-settings/src/components/editProfileModal.component.pug index ef506882..17de0c2a 100644 --- a/tabby-settings/src/components/editProfileModal.component.pug +++ b/tabby-settings/src/components/editProfileModal.component.pug @@ -57,7 +57,7 @@ .mb-4 - .col-12.col-lg-8 + .col-12.col-lg-8(*ngIf='this.profileProvider.settingsComponent') ng-template(#placeholder) .modal-footer diff --git a/tabby-settings/src/components/editProfileModal.component.ts b/tabby-settings/src/components/editProfileModal.component.ts index d0a289f6..a1375275 100644 --- a/tabby-settings/src/components/editProfileModal.component.ts +++ b/tabby-settings/src/components/editProfileModal.component.ts @@ -16,7 +16,7 @@ const iconsClassList = Object.keys(iconsData).map( template: require('./editProfileModal.component.pug'), }) export class EditProfileModalComponent { - @Input() profile: Profile|ConfigProxy + @Input() profile: Profile & ConfigProxy @Input() profileProvider: ProfileProvider @Input() settingsComponent: new () => ProfileSettingsComponent groupNames: string[] @@ -41,17 +41,20 @@ export class EditProfileModalComponent { ngOnInit () { this._profile = this.profile - this.profile = this.profilesService.getConfigProxyForProfile(this.profile) + this.profile = this.profilesService.getConfigProxyForProfile(this.profile) as any } ngAfterViewInit () { - setTimeout(() => { - const componentFactory = this.componentFactoryResolver.resolveComponentFactory(this.profileProvider.settingsComponent) - const componentRef = componentFactory.create(this.injector) - this.settingsComponentInstance = componentRef.instance - this.settingsComponentInstance.profile = this.profile - this.placeholder.insert(componentRef.hostView) - }) + const componentType = this.profileProvider.settingsComponent + if (componentType) { + setTimeout(() => { + const componentFactory = this.componentFactoryResolver.resolveComponentFactory(componentType) + const componentRef = componentFactory.create(this.injector) + this.settingsComponentInstance = componentRef.instance + this.settingsComponentInstance.profile = this.profile + this.placeholder.insert(componentRef.hostView) + }) + } } groupTypeahead = (text$: Observable) => diff --git a/tabby-settings/src/components/profilesSettingsTab.component.ts b/tabby-settings/src/components/profilesSettingsTab.component.ts index fbddb892..b4f643db 100644 --- a/tabby-settings/src/components/profilesSettingsTab.component.ts +++ b/tabby-settings/src/components/profilesSettingsTab.component.ts @@ -206,6 +206,7 @@ export class ProfilesSettingsTabComponent extends BaseComponent { ssh: 'secondary', serial: 'success', telnet: 'info', + 'split-layout': 'primary', }[this.profilesService.providerForProfile(profile)?.id ?? ''] ?? 'warning' } }