/
githubmirror
/
tabby
Обзор
Документация
Войти
/
githubmirror
/
tabby
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
master
tabby-core/src/components/profileTree.component.ts
288 строк
11 KB
D3VL Jack
[Feature] Group nesting "folders" & Profile tree view (#10788)
09 июн 2026, 00:37
Не верифицирован
09 июн 2026, 00:37
692ebdd
Код
Авторство
О чём код?
import { Component, HostBinding, HostListener, Input } from '@angular/core' import { TranslateService } from '@ngx-translate/core' import { NgbModal } from '@ng-bootstrap/ng-bootstrap' import deepClone from 'clone-deep' import FuzzySearch from 'fuzzy-search' import { ConfigService } from '../services/config.service' import { ProfilesService } from '../services/profiles.service' import { AppService } from '../services/app.service' import { PlatformService } from '../api/platform' import { ProfileProvider } from '../api/index' import { PartialProfileGroup, ProfileGroup, PartialProfile, Profile } from '../index' import { BaseComponent } from './base.component' interface CollapsableProfileGroup extends ProfileGroup { collapsed: boolean children: PartialProfileGroup<CollapsableProfileGroup>[] } /** @hidden */ @Component({ selector: 'profile-tree', styleUrls: ['./profileTree.component.scss'], templateUrl: './profileTree.component.pug', }) export class ProfileTreeComponent extends BaseComponent { profileGroups: PartialProfileGroup<ProfileGroup>[] = [] rootGroups: PartialProfileGroup<ProfileGroup>[] = [] filteredProfiles: PartialProfile<Profile>[] = [] @Input() filter = '' panelMinWidth = 200 panelMaxWidth = 600 panelInternalWidth: number = parseInt(window.localStorage.profileTreeWidth ?? '300') panelStartWidth = this.panelInternalWidth panelIsResizing = false panelStartX = 0 constructor ( private app: AppService, private platform: PlatformService, private config: ConfigService, private profilesService: ProfilesService, private translate: TranslateService, private ngbModal: NgbModal, ) { super() } async ngOnInit (): Promise<void> { await this.loadTreeItems() this.subscribeUntilDestroyed(this.config.changed$, () => this.loadTreeItems()) this.app.tabsChanged$.subscribe(() => this.tabStateChanged()) this.app.activeTabChange$.subscribe(() => this.tabStateChanged()) } private async loadTreeItems (): Promise<void> { const profileGroupCollapsed = JSON.parse(window.localStorage.profileGroupCollapsed ?? '{}') let groups = await this.profilesService.getProfileGroups({ includeNonUserGroup: true, includeProfiles: true }) for (const group of groups) { if (group.profiles?.length) { // remove template profiles group.profiles = group.profiles.filter(x => !x.isTemplate) // remove blocklisted profiles group.profiles = group.profiles.filter(x => x.id && !this.config.store.profileBlacklist.includes(x.id)) } } if (!this.config.store.terminal.showBuiltinProfiles) { groups = groups.filter(g => g.id !== 'built-in') } groups.sort((a, b) => a.name.localeCompare(b.name)) groups.sort((a, b) => (a.id === 'built-in' || !a.editable ? 1 : 0) - (b.id === 'built-in' || !b.editable ? 1 : 0)) groups.sort((a, b) => (a.id === 'ungrouped' ? 0 : 1) - (b.id === 'ungrouped' ? 0 : 1)) this.profileGroups = groups.map(g => ProfileTreeComponent.intoPartialCollapsableProfileGroup(g, profileGroupCollapsed[g.id] ?? false)) this.rootGroups = this.profilesService.buildGroupTree(this.profileGroups) } private async editProfile (profile: PartialProfile<Profile>): Promise<void> { const { EditProfileModalComponent } = window['nodeRequire']('tabby-settings') const modal = this.ngbModal.open( EditProfileModalComponent, { size: 'lg' }, ) const provider = this.profilesService.providerForProfile(profile) if (!provider) { throw new Error('Cannot edit a profile without a provider') } modal.componentInstance.partialProfile = deepClone(profile) modal.componentInstance.profileProvider = provider const result = await modal.result.catch(() => null) if (!result) { return } result.type = provider.id await this.profilesService.writeProfile(result) await this.config.save() } private async editProfileGroup (group: PartialProfileGroup<CollapsableProfileGroup>): Promise<void> { const { EditProfileGroupModalComponent } = window['nodeRequire']('tabby-settings') const modal = this.ngbModal.open( EditProfileGroupModalComponent, { size: 'lg' }, ) modal.componentInstance.group = deepClone(group) modal.componentInstance.providers = this.profilesService.getProviders() const result: PartialProfileGroup<ProfileGroup & { group: PartialProfileGroup<CollapsableProfileGroup>, provider?: ProfileProvider<Profile> }> | null = await modal.result.catch(() => null) if (!result) { return } if (!result.group) { return } if (result.provider) { return this.editProfileGroupDefaults(result.group, result.provider) } delete result.group.collapsed delete result.group.children await this.profilesService.writeProfileGroup(result.group) await this.config.save() } private async editProfileGroupDefaults (group: PartialProfileGroup<CollapsableProfileGroup>, provider: ProfileProvider<Profile>): Promise<void> { const { EditProfileModalComponent } = window['nodeRequire']('tabby-settings') const modal = this.ngbModal.open( EditProfileModalComponent, { size: 'lg' }, ) const model = group.defaults?.[provider.id] ?? {} model.type = provider.id modal.componentInstance.profile = Object.assign({}, model) modal.componentInstance.profileProvider = provider modal.componentInstance.defaultsMode = 'group' const result = await modal.result.catch(() => null) if (result) { // Fully replace the config for (const k in model) { // eslint-disable-next-line @typescript-eslint/no-dynamic-delete delete model[k] } Object.assign(model, result) if (!group.defaults) { group.defaults = {} } group.defaults[provider.id] = model } return this.editProfileGroup(group) } async profileContextMenu (profile: PartialProfile<Profile>, event: MouseEvent): Promise<void> { event.preventDefault() this.platform.popupContextMenu([ { type: 'normal', label: this.translate.instant('Run'), click: () => this.launchProfile(profile), }, { type: 'normal', label: this.translate.instant('Edit profile'), click: () => this.editProfile(profile), enabled: !(profile.isBuiltin ?? profile.isTemplate), }, ]) } async groupContextMenu (group: PartialProfileGroup<CollapsableProfileGroup>, event: MouseEvent): Promise<void> { event.preventDefault() this.platform.popupContextMenu([ { type: 'normal', label: group.collapsed ? this.translate.instant('Expand group') : this.translate.instant('Collapse group'), click: () => this.toggleGroupCollapse(group), }, { type: 'normal', label: this.translate.instant('Edit group'), click: () => this.editProfileGroup(group), enabled: group.editable, }, ]) } private async tabStateChanged (): Promise<void> { // TODO: show active tab in the side panel with eye icon } async launchProfile<P extends Profile> (profile: PartialProfile<P>): Promise<any> { return this.profilesService.launchProfile(profile) } async onFilterChange (): Promise<void> { try { const q = this.filter.trim().toLowerCase() if (q.length === 0) { this.rootGroups = this.profilesService.buildGroupTree(this.profileGroups) return } const profiles = await this.profilesService.getProfiles({ includeBuiltin: this.config.store.terminal.showBuiltinProfiles, clone: true, }) const matches = new FuzzySearch( profiles.filter(p => !p.isTemplate), ['name', 'description'], { sort: false }, ).search(q) this.rootGroups = [ { id: 'search', editable: false, name: this.translate.instant('Filter results'), icon: 'fas fa-magnifying-glass', profiles: matches, }, ] } catch (error) { console.error('Error occurred during search:', error) } } ////// RESIZING ////// startResize (event: MouseEvent): void { this.panelIsResizing = true this.panelStartX = event.clientX this.panelStartWidth = this.panelWidth event.preventDefault() } @HostListener('document:mousemove', ['$event']) onMouseMove (event: MouseEvent): void { if (!this.panelIsResizing) { return } const delta = event.clientX - this.panelStartX const width = Math.min(Math.max(this.panelMinWidth, this.panelStartWidth + delta), this.panelMaxWidth) this.panelWidth = width window.localStorage.profileTreeWidth = width } @HostListener('document:mouseup') stopResize (): boolean { this.panelIsResizing = false return true } @HostBinding('style.width.px') get panelWidth (): number { return this.panelInternalWidth } set panelWidth (value: number) { this.panelInternalWidth = value } ////// GROUP COLLAPSING ////// toggleGroupCollapse (group: PartialProfileGroup<CollapsableProfileGroup>): void { group.collapsed = !group.collapsed this.saveProfileGroupCollapse(group) } private saveProfileGroupCollapse (group: PartialProfileGroup<CollapsableProfileGroup>): void { const profileGroupCollapsed = JSON.parse(window.localStorage.profileGroupCollapsed ?? '{}') profileGroupCollapsed[group.id] = group.collapsed window.localStorage.profileGroupCollapsed = JSON.stringify(profileGroupCollapsed) } private static intoPartialCollapsableProfileGroup (group: PartialProfileGroup<ProfileGroup>, collapsed: boolean): PartialProfileGroup<CollapsableProfileGroup> { const collapsableGroup = { ...group, collapsed, } return collapsableGroup } }