mirror of
https://github.com/Eugeny/tabby.git
synced 2025-07-20 02:18:01 +00:00
moved profile settings view to settings plugin, web entry cleanup
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "tabby-settings",
|
||||
"version": "1.0.144",
|
||||
"version": "1.0.145-nightly.0",
|
||||
"description": "Tabby terminal settings page",
|
||||
"keywords": [
|
||||
"tabby-builtin-plugin"
|
||||
|
65
tabby-settings/src/components/editProfileModal.component.pug
Normal file
65
tabby-settings/src/components/editProfileModal.component.pug
Normal file
@@ -0,0 +1,65 @@
|
||||
.modal-header
|
||||
h3.m-0 {{profile.name}}
|
||||
|
||||
.modal-body
|
||||
.row
|
||||
.col-12.col-lg-4
|
||||
.form-group
|
||||
label Name
|
||||
input.form-control(
|
||||
type='text',
|
||||
autofocus,
|
||||
[(ngModel)]='profile.name',
|
||||
)
|
||||
|
||||
.form-group
|
||||
label Group
|
||||
input.form-control(
|
||||
type='text',
|
||||
alwaysVisibleTypeahead,
|
||||
placeholder='Ungrouped',
|
||||
[(ngModel)]='profile.group',
|
||||
[ngbTypeahead]='groupTypeahead',
|
||||
)
|
||||
|
||||
.form-group
|
||||
label Icon
|
||||
.input-group
|
||||
input.form-control(
|
||||
type='text',
|
||||
alwaysVisibleTypeahead,
|
||||
[(ngModel)]='profile.icon',
|
||||
[ngbTypeahead]='iconSearch',
|
||||
[resultTemplate]='rt'
|
||||
)
|
||||
.input-group-append
|
||||
.input-group-text
|
||||
i([class]='"fa-fw " + profile.icon')
|
||||
|
||||
ng-template(#rt,let-r='result',let-t='term')
|
||||
i([class]='"fa-fw " + r')
|
||||
ngb-highlight.ml-2([result]='r', [term]='t')
|
||||
|
||||
.form-line
|
||||
.header
|
||||
.title Color
|
||||
input.form-control.w-50(
|
||||
type='text',
|
||||
[(ngModel)]='profile.color',
|
||||
placeholder='#000000'
|
||||
)
|
||||
|
||||
.form-line
|
||||
.header
|
||||
.title Disable dynamic tab title
|
||||
.description Connection name will be used instead
|
||||
toggle([(ngModel)]='profile.disableDynamicTitle')
|
||||
|
||||
.mb-4
|
||||
|
||||
.col-12.col-lg-8
|
||||
ng-template(#placeholder)
|
||||
|
||||
.modal-footer
|
||||
button.btn.btn-outline-primary((click)='save()') Save
|
||||
button.btn.btn-outline-danger((click)='cancel()') Cancel
|
72
tabby-settings/src/components/editProfileModal.component.ts
Normal file
72
tabby-settings/src/components/editProfileModal.component.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||
import { Observable, OperatorFunction, debounceTime, map, distinctUntilChanged } from 'rxjs'
|
||||
import { Component, Input, ViewChild, ViewContainerRef, ComponentFactoryResolver, Injector } from '@angular/core'
|
||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { ConfigService, Profile, ProfileProvider, ProfileSettingsComponent } from 'tabby-core'
|
||||
|
||||
const iconsData = require('../../../tabby-core/src/icons.json')
|
||||
const iconsClassList = Object.keys(iconsData).map(
|
||||
icon => iconsData[icon].map(
|
||||
style => `fa${style[0]} fa-${icon}`
|
||||
)
|
||||
).flat()
|
||||
|
||||
/** @hidden */
|
||||
@Component({
|
||||
template: require('./editProfileModal.component.pug'),
|
||||
})
|
||||
export class EditProfileModalComponent {
|
||||
@Input() profile: Profile
|
||||
@Input() profileProvider: ProfileProvider
|
||||
@Input() settingsComponent: new () => ProfileSettingsComponent
|
||||
groupNames: string[]
|
||||
@ViewChild('placeholder', { read: ViewContainerRef }) placeholder: ViewContainerRef
|
||||
|
||||
private settingsComponentInstance: ProfileSettingsComponent
|
||||
|
||||
constructor (
|
||||
private injector: Injector,
|
||||
private componentFactoryResolver: ComponentFactoryResolver,
|
||||
config: ConfigService,
|
||||
private modalInstance: NgbActiveModal,
|
||||
) {
|
||||
this.groupNames = [...new Set(
|
||||
(config.store.profiles as Profile[])
|
||||
.map(x => x.group)
|
||||
.filter(x => !!x)
|
||||
)].sort() as string[]
|
||||
}
|
||||
|
||||
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)
|
||||
})
|
||||
}
|
||||
|
||||
groupTypeahead = (text$: Observable<string>) =>
|
||||
text$.pipe(
|
||||
debounceTime(200),
|
||||
distinctUntilChanged(),
|
||||
map(q => this.groupNames.filter(x => !q || x.toLowerCase().includes(q.toLowerCase())))
|
||||
)
|
||||
|
||||
iconSearch: OperatorFunction<string, string[]> = (text$: Observable<string>) =>
|
||||
text$.pipe(
|
||||
debounceTime(200),
|
||||
map(term => iconsClassList.filter(v => v.toLowerCase().includes(term.toLowerCase())).slice(0, 10))
|
||||
)
|
||||
|
||||
save () {
|
||||
this.profile.group ||= undefined
|
||||
this.settingsComponentInstance.save?.()
|
||||
this.modalInstance.close(this.profile)
|
||||
}
|
||||
|
||||
cancel () {
|
||||
this.modalInstance.dismiss()
|
||||
}
|
||||
}
|
@@ -0,0 +1,94 @@
|
||||
h3.mb-3 Profiles
|
||||
|
||||
.form-line
|
||||
.header
|
||||
.title Default profile for new tabs
|
||||
|
||||
select.form-control(
|
||||
[(ngModel)]='config.store.terminal.profile',
|
||||
(ngModelChange)='config.save()',
|
||||
)
|
||||
option(
|
||||
*ngFor='let profile of profiles',
|
||||
[ngValue]='profile.id'
|
||||
) {{profile.name}}
|
||||
option(
|
||||
*ngFor='let profile of builtinProfiles',
|
||||
[ngValue]='profile.id'
|
||||
) {{profile.name}}
|
||||
|
||||
.form-line(*ngIf='config.store.profiles.length > 0')
|
||||
.header
|
||||
.title Show built-in profiles in selector
|
||||
.description If disabled, only custom profiles will show up in the profile selector
|
||||
|
||||
toggle(
|
||||
[(ngModel)]='config.store.terminal.showBuiltinProfiles',
|
||||
(ngModelChange)='config.save()'
|
||||
)
|
||||
|
||||
.d-flex.mb-3.mt-4
|
||||
.input-group
|
||||
.input-group-prepend
|
||||
.input-group-text
|
||||
i.fas.fa-fw.fa-search
|
||||
input.form-control(type='search', placeholder='Filter', [(ngModel)]='filter')
|
||||
|
||||
button.btn.btn-primary.flex-shrink-0.ml-3((click)='newProfile()')
|
||||
i.fas.fa-fw.fa-plus
|
||||
| New profile
|
||||
|
||||
.list-group.list-group-light.mt-3.mb-3
|
||||
ng-container(*ngFor='let group of profileGroups')
|
||||
ng-container(*ngIf='isGroupVisible(group)')
|
||||
.list-group-item.list-group-item-action.d-flex.align-items-center(
|
||||
(click)='group.collapsed = !group.collapsed'
|
||||
)
|
||||
.fa.fa-fw.fa-chevron-right(*ngIf='group.collapsed')
|
||||
.fa.fa-fw.fa-chevron-down(*ngIf='!group.collapsed')
|
||||
span.ml-3.mr-auto {{group.name || "Ungrouped"}}
|
||||
button.btn.btn-sm.btn-link.hover-reveal.ml-2(
|
||||
*ngIf='group.editable && group.name',
|
||||
(click)='$event.stopPropagation(); editGroup(group)'
|
||||
)
|
||||
i.fas.fa-pencil-alt
|
||||
button.btn.btn-sm.btn-link.hover-reveal.ml-2(
|
||||
*ngIf='group.editable && group.name',
|
||||
(click)='$event.stopPropagation(); deleteGroup(group)'
|
||||
)
|
||||
i.fas.fa-trash
|
||||
ng-container(*ngIf='!group.collapsed')
|
||||
ng-container(*ngFor='let profile of group.profiles')
|
||||
.list-group-item.pl-5.d-flex.align-items-center(
|
||||
*ngIf='isProfileVisible(profile)',
|
||||
[class.list-group-item-action]='!profile.isBuiltin',
|
||||
(click)='profile.isBuiltin ? null : editProfile(profile)'
|
||||
)
|
||||
i.icon(
|
||||
class='fa-fw {{profile.icon}}',
|
||||
[style.color]='profile.color',
|
||||
*ngIf='!iconIsSVG(profile.icon)'
|
||||
)
|
||||
.icon(
|
||||
[fastHtmlBind]='profile.icon',
|
||||
*ngIf='iconIsSVG(profile.icon)'
|
||||
)
|
||||
|
||||
div {{profile.name}}
|
||||
.text-muted.ml-2 {{getDescription(profile)}}
|
||||
|
||||
.mr-auto
|
||||
|
||||
button.btn.btn-link.hover-reveal.ml-1((click)='$event.stopPropagation(); launchProfile(profile)')
|
||||
i.fas.fa-play
|
||||
|
||||
button.btn.btn-link.hover-reveal.ml-1((click)='$event.stopPropagation(); newProfile(profile)')
|
||||
i.fas.fa-copy
|
||||
|
||||
button.btn.btn-link.text-danger.hover-reveal.ml-1(
|
||||
*ngIf='!profile.isBuiltin',
|
||||
(click)='$event.stopPropagation(); deleteProfile(profile)'
|
||||
)
|
||||
i.fas.fa-trash
|
||||
|
||||
.ml-1(class='badge badge-{{getTypeColorClass(profile)}}') {{getTypeLabel(profile)}}
|
@@ -0,0 +1,8 @@
|
||||
.icon {
|
||||
width: 1.25rem;
|
||||
margin-right: 0.25rem;
|
||||
}
|
||||
|
||||
.icon + * {
|
||||
margin-left: 10px;
|
||||
}
|
204
tabby-settings/src/components/profilesSettingsTab.component.ts
Normal file
204
tabby-settings/src/components/profilesSettingsTab.component.ts
Normal file
@@ -0,0 +1,204 @@
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import slugify from 'slugify'
|
||||
import deepClone from 'clone-deep'
|
||||
import { Component } from '@angular/core'
|
||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { ConfigService, HostAppService, Profile, SelectorService, ProfilesService, PromptModalComponent, PlatformService, BaseComponent } from 'tabby-core'
|
||||
import { EditProfileModalComponent } from './editProfileModal.component'
|
||||
|
||||
interface ProfileGroup {
|
||||
name?: string
|
||||
profiles: Profile[]
|
||||
editable: boolean
|
||||
collapsed: boolean
|
||||
}
|
||||
|
||||
/** @hidden */
|
||||
@Component({
|
||||
template: require('./profilesSettingsTab.component.pug'),
|
||||
styles: [require('./profilesSettingsTab.component.scss')],
|
||||
})
|
||||
export class ProfilesSettingsTabComponent extends BaseComponent {
|
||||
profiles: Profile[] = []
|
||||
builtinProfiles: Profile[] = []
|
||||
templateProfiles: Profile[] = []
|
||||
profileGroups: ProfileGroup[]
|
||||
filter = ''
|
||||
|
||||
constructor (
|
||||
public config: ConfigService,
|
||||
public hostApp: HostAppService,
|
||||
private profilesService: ProfilesService,
|
||||
private selector: SelectorService,
|
||||
private ngbModal: NgbModal,
|
||||
private platform: PlatformService,
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
async ngOnInit (): Promise<void> {
|
||||
this.refresh()
|
||||
this.builtinProfiles = (await this.profilesService.getProfiles()).filter(x => x.isBuiltin)
|
||||
this.templateProfiles = this.builtinProfiles.filter(x => x.isTemplate)
|
||||
this.builtinProfiles = this.builtinProfiles.filter(x => !x.isTemplate)
|
||||
this.refresh()
|
||||
this.subscribeUntilDestroyed(this.config.changed$, () => this.refresh())
|
||||
}
|
||||
|
||||
launchProfile (profile: Profile): void {
|
||||
this.profilesService.openNewTabForProfile(profile)
|
||||
}
|
||||
|
||||
async newProfile (base?: Profile): Promise<void> {
|
||||
if (!base) {
|
||||
const profiles = [...this.templateProfiles, ...this.builtinProfiles, ...this.profiles]
|
||||
profiles.sort((a, b) => (a.weight ?? 0) - (b.weight ?? 0))
|
||||
base = await this.selector.show(
|
||||
'Select a base profile to use as a template',
|
||||
profiles.map(p => ({
|
||||
icon: p.icon,
|
||||
description: this.profilesService.providerForProfile(p)?.getDescription(p),
|
||||
name: p.group ? `${p.group} / ${p.name}` : p.name,
|
||||
result: p,
|
||||
})),
|
||||
)
|
||||
}
|
||||
const profile = deepClone(base)
|
||||
profile.id = null
|
||||
profile.name = ''
|
||||
profile.isBuiltin = false
|
||||
profile.isTemplate = false
|
||||
await this.editProfile(profile)
|
||||
profile.id = `${profile.type}:custom:${slugify(profile.name)}:${uuidv4()}`
|
||||
this.config.store.profiles = [profile, ...this.config.store.profiles]
|
||||
await this.config.save()
|
||||
}
|
||||
|
||||
async editProfile (profile: Profile): Promise<void> {
|
||||
const modal = this.ngbModal.open(
|
||||
EditProfileModalComponent,
|
||||
{ size: 'lg' },
|
||||
)
|
||||
modal.componentInstance.profile = Object.assign({}, profile)
|
||||
modal.componentInstance.profileProvider = this.profilesService.providerForProfile(profile)
|
||||
const result = await modal.result
|
||||
Object.assign(profile, result)
|
||||
await this.config.save()
|
||||
}
|
||||
|
||||
async deleteProfile (profile: Profile): Promise<void> {
|
||||
if ((await this.platform.showMessageBox(
|
||||
{
|
||||
type: 'warning',
|
||||
message: `Delete "${profile.name}"?`,
|
||||
buttons: ['Keep', 'Delete'],
|
||||
defaultId: 0,
|
||||
}
|
||||
)).response === 1) {
|
||||
this.profilesService.providerForProfile(profile)?.deleteProfile(profile)
|
||||
this.config.store.profiles = this.config.store.profiles.filter(x => x !== profile)
|
||||
await this.config.save()
|
||||
}
|
||||
}
|
||||
|
||||
refresh (): void {
|
||||
this.profiles = this.config.store.profiles
|
||||
this.profileGroups = []
|
||||
|
||||
for (const profile of this.profiles) {
|
||||
let group = this.profileGroups.find(x => x.name === profile.group)
|
||||
if (!group) {
|
||||
group = {
|
||||
name: profile.group,
|
||||
profiles: [],
|
||||
editable: true,
|
||||
collapsed: false,
|
||||
}
|
||||
this.profileGroups.push(group)
|
||||
}
|
||||
group.profiles.push(profile)
|
||||
}
|
||||
|
||||
this.profileGroups.sort((a, b) => a.name?.localeCompare(b.name ?? '') ?? -1)
|
||||
|
||||
this.profileGroups.push({
|
||||
name: 'Built-in',
|
||||
profiles: this.builtinProfiles,
|
||||
editable: false,
|
||||
collapsed: false,
|
||||
})
|
||||
}
|
||||
|
||||
async editGroup (group: ProfileGroup): Promise<void> {
|
||||
const modal = this.ngbModal.open(PromptModalComponent)
|
||||
modal.componentInstance.prompt = 'New name'
|
||||
modal.componentInstance.value = group.name
|
||||
const result = await modal.result
|
||||
if (result) {
|
||||
for (const profile of this.profiles.filter(x => x.group === group.name)) {
|
||||
profile.group = result.value
|
||||
}
|
||||
this.config.store.profiles = this.profiles
|
||||
await this.config.save()
|
||||
}
|
||||
}
|
||||
|
||||
async deleteGroup (group: ProfileGroup): Promise<void> {
|
||||
if ((await this.platform.showMessageBox(
|
||||
{
|
||||
type: 'warning',
|
||||
message: `Delete "${group.name}"?`,
|
||||
buttons: ['Keep', 'Delete'],
|
||||
defaultId: 0,
|
||||
}
|
||||
)).response === 1) {
|
||||
if ((await this.platform.showMessageBox(
|
||||
{
|
||||
type: 'warning',
|
||||
message: `Delete the group's profiles?`,
|
||||
buttons: ['Move to "Ungrouped"', 'Delete'],
|
||||
defaultId: 0,
|
||||
}
|
||||
)).response === 0) {
|
||||
for (const profile of this.profiles.filter(x => x.group === group.name)) {
|
||||
delete profile.group
|
||||
}
|
||||
} else {
|
||||
this.config.store.profiles = this.config.store.profiles.filter(x => x.group !== group.name)
|
||||
}
|
||||
await this.config.save()
|
||||
}
|
||||
}
|
||||
|
||||
isGroupVisible (group: ProfileGroup): boolean {
|
||||
return !this.filter || group.profiles.some(x => this.isProfileVisible(x))
|
||||
}
|
||||
|
||||
isProfileVisible (profile: Profile): boolean {
|
||||
return !this.filter || profile.name.toLowerCase().includes(this.filter.toLowerCase())
|
||||
}
|
||||
|
||||
iconIsSVG (icon?: string): boolean {
|
||||
return icon?.startsWith('<') ?? false
|
||||
}
|
||||
|
||||
getDescription (profile: Profile): string|null {
|
||||
return this.profilesService.providerForProfile(profile)?.getDescription(profile) ?? null
|
||||
}
|
||||
|
||||
getTypeLabel (profile: Profile): string {
|
||||
const name = this.profilesService.providerForProfile(profile)?.name
|
||||
if (name === 'Local') {
|
||||
return ''
|
||||
}
|
||||
return name ?? 'Unknown'
|
||||
}
|
||||
|
||||
getTypeColorClass (profile: Profile): string {
|
||||
return {
|
||||
ssh: 'secondary',
|
||||
serial: 'success',
|
||||
telnet: 'info',
|
||||
}[this.profilesService.providerForProfile(profile)?.id ?? ''] ?? 'warning'
|
||||
}
|
||||
}
|
@@ -5,6 +5,7 @@ import { NgbModule } from '@ng-bootstrap/ng-bootstrap'
|
||||
|
||||
import TabbyCorePlugin, { ToolbarButtonProvider, HotkeyProvider, ConfigProvider } from 'tabby-core'
|
||||
|
||||
import { EditProfileModalComponent } from './components/editProfileModal.component'
|
||||
import { HotkeyInputModalComponent } from './components/hotkeyInputModal.component'
|
||||
import { HotkeySettingsTabComponent } from './components/hotkeySettingsTab.component'
|
||||
import { MultiHotkeyInputComponent } from './components/multiHotkeyInput.component'
|
||||
@@ -13,12 +14,13 @@ import { SettingsTabBodyComponent } from './components/settingsTabBody.component
|
||||
import { WindowSettingsTabComponent } from './components/windowSettingsTab.component'
|
||||
import { VaultSettingsTabComponent } from './components/vaultSettingsTab.component'
|
||||
import { SetVaultPassphraseModalComponent } from './components/setVaultPassphraseModal.component'
|
||||
import { ProfilesSettingsTabComponent } from './components/profilesSettingsTab.component'
|
||||
|
||||
import { SettingsTabProvider } from './api'
|
||||
import { ButtonProvider } from './buttonProvider'
|
||||
import { SettingsHotkeyProvider } from './hotkeys'
|
||||
import { SettingsConfigProvider } from './config'
|
||||
import { HotkeySettingsTabProvider, WindowSettingsTabProvider, VaultSettingsTabProvider } from './settings'
|
||||
import { HotkeySettingsTabProvider, WindowSettingsTabProvider, VaultSettingsTabProvider, ProfilesSettingsTabProvider } from './settings'
|
||||
|
||||
/** @hidden */
|
||||
@NgModule({
|
||||
@@ -35,19 +37,24 @@ import { HotkeySettingsTabProvider, WindowSettingsTabProvider, VaultSettingsTabP
|
||||
{ provide: SettingsTabProvider, useClass: HotkeySettingsTabProvider, multi: true },
|
||||
{ provide: SettingsTabProvider, useClass: WindowSettingsTabProvider, multi: true },
|
||||
{ provide: SettingsTabProvider, useClass: VaultSettingsTabProvider, multi: true },
|
||||
{ provide: SettingsTabProvider, useClass: ProfilesSettingsTabProvider, multi: true },
|
||||
],
|
||||
entryComponents: [
|
||||
EditProfileModalComponent,
|
||||
HotkeyInputModalComponent,
|
||||
HotkeySettingsTabComponent,
|
||||
ProfilesSettingsTabComponent,
|
||||
SettingsTabComponent,
|
||||
SetVaultPassphraseModalComponent,
|
||||
VaultSettingsTabComponent,
|
||||
WindowSettingsTabComponent,
|
||||
],
|
||||
declarations: [
|
||||
EditProfileModalComponent,
|
||||
HotkeyInputModalComponent,
|
||||
HotkeySettingsTabComponent,
|
||||
MultiHotkeyInputComponent,
|
||||
ProfilesSettingsTabComponent,
|
||||
SettingsTabComponent,
|
||||
SettingsTabBodyComponent,
|
||||
SetVaultPassphraseModalComponent,
|
||||
|
@@ -3,6 +3,7 @@ import { SettingsTabProvider } from './api'
|
||||
import { HotkeySettingsTabComponent } from './components/hotkeySettingsTab.component'
|
||||
import { WindowSettingsTabComponent } from './components/windowSettingsTab.component'
|
||||
import { VaultSettingsTabComponent } from './components/vaultSettingsTab.component'
|
||||
import { ProfilesSettingsTabComponent } from './components/profilesSettingsTab.component'
|
||||
|
||||
/** @hidden */
|
||||
@Injectable()
|
||||
@@ -41,3 +42,16 @@ export class VaultSettingsTabProvider extends SettingsTabProvider {
|
||||
return VaultSettingsTabComponent
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/** @hidden */
|
||||
@Injectable()
|
||||
export class ProfilesSettingsTabProvider extends SettingsTabProvider {
|
||||
id = 'profiles'
|
||||
icon = 'window-restore'
|
||||
title = 'Profiles'
|
||||
|
||||
getComponentType (): any {
|
||||
return ProfilesSettingsTabComponent
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user