diff --git a/tabby-core/src/services/profiles.service.ts b/tabby-core/src/services/profiles.service.ts index 229ff9415f..329fc4fe29 100644 --- a/tabby-core/src/services/profiles.service.ts +++ b/tabby-core/src/services/profiles.service.ts @@ -65,8 +65,8 @@ export class ProfilesService { * Return ConfigProxy for a given Profile * arg: skipUserDefaults -> do not merge global provider defaults in ConfigProxy */ - getConfigProxyForProfile (profile: PartialProfile, skipUserDefaults = false): T { - const defaults = this.getProfileDefaults(profile, skipUserDefaults).reduce(configMerge, {}) + getConfigProxyForProfile (profile: PartialProfile, skipGlobalDefaults = false, skipGroupDefaults = false): T { + const defaults = this.getProfileDefaults(profile, skipGlobalDefaults, skipGroupDefaults).reduce(configMerge, {}) return new ConfigProxy(profile, defaults) as unknown as T } @@ -373,12 +373,14 @@ export class ProfilesService { * Always return something, empty object if no defaults found * arg: skipUserDefaults -> do not merge global provider defaults in ConfigProxy */ - getProfileDefaults (profile: PartialProfile, skipUserDefaults = false): any { + getProfileDefaults (profile: PartialProfile, skipGlobalDefaults = false, skipGroupDefaults = false): any[] { const provider = this.providerForProfile(profile) + return [ this.profileDefaults, provider?.configDefaults ?? {}, - !provider || skipUserDefaults ? {} : this.getProviderDefaults(provider), + provider && !skipGlobalDefaults ? this.getProviderDefaults(provider) : {}, + provider && !skipGlobalDefaults && !skipGroupDefaults ? this.getProviderProfileGroupDefaults(profile.group ?? '', provider) : {}, ] } @@ -386,6 +388,14 @@ export class ProfilesService { * Methods used to interract with ProfileGroup */ + /** + * Synchronously return an Array of the existing ProfileGroups + * Does not return builtin groups + */ + getSyncProfileGroups (): PartialProfileGroup[] { + return deepClone(this.config.store.groups ?? []) + } + /** * Return an Array of the existing ProfileGroups * arg: includeProfiles (default: false) -> if false, does not fill up the profiles field of ProfileGroup @@ -397,7 +407,7 @@ export class ProfilesService { profiles = await this.getProfiles(includeNonUserGroup, true) } - let groups: PartialProfileGroup[] = deepClone(this.config.store.groups ?? []) + let groups: PartialProfileGroup[] = this.getSyncProfileGroups() groups = groups.map(x => { x.editable = true @@ -516,4 +526,13 @@ export class ProfilesService { return this.config.store.groups.find(g => g.id === groupId)?.name ?? groupId } + /** + * Return defaults for a given group ID and provider + * Always return something, empty object if no defaults found + * arg: skipUserDefaults -> do not merge global provider defaults in ConfigProxy + */ + getProviderProfileGroupDefaults (groupId: string, provider: ProfileProvider): any { + return this.getSyncProfileGroups().find(g => g.id === groupId)?.defaults?.[provider.id] ?? {} + } + } diff --git a/tabby-settings/src/components/editProfileGroupModal.component.pug b/tabby-settings/src/components/editProfileGroupModal.component.pug new file mode 100644 index 0000000000..54468a3b36 --- /dev/null +++ b/tabby-settings/src/components/editProfileGroupModal.component.pug @@ -0,0 +1,29 @@ +.modal-header + h3.m-0 {{group.name}} + +.modal-body + .row + .col-12.col-lg-4 + .mb-3 + label(translate) Name + input.form-control( + type='text', + autofocus, + [(ngModel)]='group.name', + ) + + .col-12.col-lg-8 + .form-line.content-box + .header + .title(translate) Default profile group settings + .description(translate) These apply to all profiles of a given type in this group + + .list-group.mt-3.mb-3.content-box + a.list-group-item.list-group-item-action( + (click)='editDefaults(provider)', + *ngFor='let provider of providers' + ) {{provider.name|translate}} + +.modal-footer + button.btn.btn-primary((click)='save()', translate) Save + button.btn.btn-danger((click)='cancel()', translate) Cancel diff --git a/tabby-settings/src/components/editProfileGroupModal.component.ts b/tabby-settings/src/components/editProfileGroupModal.component.ts new file mode 100644 index 0000000000..fd00b026f2 --- /dev/null +++ b/tabby-settings/src/components/editProfileGroupModal.component.ts @@ -0,0 +1,34 @@ +/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ +import { Component, Input } from '@angular/core' +import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap' +import { ConfigProxy, ProfileGroup, Profile, ProfileProvider } from 'tabby-core' + +/** @hidden */ +@Component({ + templateUrl: './editProfileGroupModal.component.pug', +}) +export class EditProfileGroupModalComponent { + @Input() group: G & ConfigProxy + @Input() providers: ProfileProvider[] + + constructor ( + private modalInstance: NgbActiveModal, + ) {} + + save () { + this.modalInstance.close({ group: this.group }) + } + + cancel () { + this.modalInstance.dismiss() + } + + editDefaults (provider: ProfileProvider) { + this.modalInstance.close({ group: this.group, provider }) + } +} + +export interface EditProfileGroupModalComponentResult { + group: G + provider?: ProfileProvider +} diff --git a/tabby-settings/src/components/editProfileModal.component.pug b/tabby-settings/src/components/editProfileModal.component.pug index 0619345d4f..10d7fea2e4 100644 --- a/tabby-settings/src/components/editProfileModal.component.pug +++ b/tabby-settings/src/components/editProfileModal.component.pug @@ -1,7 +1,7 @@ -.modal-header(*ngIf='!defaultsMode') +.modal-header(*ngIf='defaultsMode === "disabled"') h3.m-0 {{profile.name}} -.modal-header(*ngIf='defaultsMode') +.modal-header(*ngIf='defaultsMode !== "disabled"') h3.m-0( translate='Defaults for {type}', [translateParams]='{type: profileProvider.name}' @@ -10,7 +10,7 @@ .modal-body .row .col-12.col-lg-4 - .mb-3(*ngIf='!defaultsMode') + .mb-3(*ngIf='defaultsMode === "disabled"') label(translate) Name input.form-control( type='text', @@ -18,7 +18,7 @@ [(ngModel)]='profile.name', ) - .mb-3(*ngIf='!defaultsMode') + .mb-3(*ngIf='defaultsMode === "disabled"') label(translate) Group input.form-control( type='text', @@ -28,9 +28,10 @@ [ngbTypeahead]='groupTypeahead', [inputFormatter]="groupFormatter", [resultFormatter]="groupFormatter", + [editable]="false" ) - .mb-3(*ngIf='!defaultsMode') + .mb-3(*ngIf='defaultsMode === "disabled"') label(translate) Icon .input-group input.form-control( diff --git a/tabby-settings/src/components/editProfileModal.component.ts b/tabby-settings/src/components/editProfileModal.component.ts index f1b64f6a23..e193aa9437 100644 --- a/tabby-settings/src/components/editProfileModal.component.ts +++ b/tabby-settings/src/components/editProfileModal.component.ts @@ -3,7 +3,6 @@ import { Observable, OperatorFunction, debounceTime, map, distinctUntilChanged } import { Component, Input, ViewChild, ViewContainerRef, ComponentFactoryResolver, Injector } from '@angular/core' import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap' import { ConfigProxy, ConfigService, PartialProfileGroup, Profile, ProfileProvider, ProfileSettingsComponent, ProfilesService, TAB_COLORS, ProfileGroup } from 'tabby-core' -import { v4 as uuidv4 } from 'uuid' const iconsData = require('../../../tabby-core/src/icons.json') const iconsClassList = Object.keys(iconsData).map( @@ -20,8 +19,8 @@ export class EditProfileModalComponent

{ @Input() profile: P & ConfigProxy @Input() profileProvider: ProfileProvider

@Input() settingsComponent: new () => ProfileSettingsComponent

- @Input() defaultsMode = false - @Input() profileGroup: PartialProfileGroup | string | undefined + @Input() defaultsMode: 'enabled'|'group'|'disabled' = 'disabled' + @Input() profileGroup: PartialProfileGroup | undefined groups: PartialProfileGroup[] @ViewChild('placeholder', { read: ViewContainerRef }) placeholder: ViewContainerRef @@ -35,7 +34,7 @@ export class EditProfileModalComponent

{ config: ConfigService, private modalInstance: NgbActiveModal, ) { - if (!this.defaultsMode) { + if (this.defaultsMode === 'disabled') { this.profilesService.getProfileGroups().then(groups => { this.groups = groups this.profileGroup = groups.find(g => g.id === this.profile.group) @@ -59,7 +58,7 @@ export class EditProfileModalComponent

{ ngOnInit () { this._profile = this.profile - this.profile = this.profilesService.getConfigProxyForProfile(this.profile, this.defaultsMode) + this.profile = this.profilesService.getConfigProxyForProfile(this.profile, this.defaultsMode === 'enabled', this.defaultsMode === 'group') } ngAfterViewInit () { @@ -94,14 +93,6 @@ export class EditProfileModalComponent

{ if (!this.profileGroup) { this.profile.group = undefined } else { - if (typeof this.profileGroup === 'string') { - const newGroup: PartialProfileGroup = { - id: uuidv4(), - name: this.profileGroup, - } - this.profilesService.newProfileGroup(newGroup, false, false) - this.profileGroup = newGroup - } this.profile.group = this.profileGroup.id } diff --git a/tabby-settings/src/components/profilesSettingsTab.component.pug b/tabby-settings/src/components/profilesSettingsTab.component.pug index 9c29887171..ed61b23d0e 100644 --- a/tabby-settings/src/components/profilesSettingsTab.component.pug +++ b/tabby-settings/src/components/profilesSettingsTab.component.pug @@ -27,9 +27,17 @@ ul.nav-tabs(ngbNav, #nav='ngbNav') i.fas.fa-fw.fa-search input.form-control(type='search', [placeholder]='"Filter"|translate', [(ngModel)]='filter') - button.btn.btn-primary.flex-shrink-0.ms-3((click)='newProfile()') - i.fas.fa-fw.fa-plus - span(translate) New profile + div(ngbDropdown).d-inline-block.flex-shrink-0.ms-3 + button.btn.btn-primary(ngbDropdownToggle) + i.fas.fa-fw.fa-plus + span(translate) New + div(ngbDropdownMenu) + button(ngbDropdownItem, (click)='newProfile()') + i.fas.fa-fw.fa-plus + span(translate) New profile + button(ngbDropdownItem, (click)='newProfileGroup()') + i.fas.fa-fw.fa-plus + span(translate) New profile Group .list-group.mt-3.mb-3 ng-container(*ngFor='let group of profileGroups') @@ -37,17 +45,17 @@ ul.nav-tabs(ngbNav, #nav='ngbNav') .list-group-item.list-group-item-action.d-flex.align-items-center( (click)='toggleGroupCollapse(group)' ) - .fa.fa-fw.fa-chevron-right(*ngIf='group.collapsed') - .fa.fa-fw.fa-chevron-down(*ngIf='!group.collapsed') + .fa.fa-fw.fa-chevron-right(*ngIf='group.collapsed && group.profiles?.length > 0') + .fa.fa-fw.fa-chevron-down(*ngIf='!group.collapsed && group.profiles?.length > 0') span.ms-3.me-auto {{group.name || ("Ungrouped"|translate)}} button.btn.btn-sm.btn-link.hover-reveal.ms-2( *ngIf='group.editable && group.name', - (click)='$event.stopPropagation(); editGroup(group)' + (click)='$event.stopPropagation(); editProfileGroup(group)' ) i.fas.fa-pencil-alt button.btn.btn-sm.btn-link.hover-reveal.ms-2( *ngIf='group.editable && group.name', - (click)='$event.stopPropagation(); deleteGroup(group)' + (click)='$event.stopPropagation(); deleteProfileGroup(group)' ) i.fas.fa-trash-alt ng-container(*ngIf='!group.collapsed') diff --git a/tabby-settings/src/components/profilesSettingsTab.component.ts b/tabby-settings/src/components/profilesSettingsTab.component.ts index 08e78561cf..41b3e44393 100644 --- a/tabby-settings/src/components/profilesSettingsTab.component.ts +++ b/tabby-settings/src/components/profilesSettingsTab.component.ts @@ -4,6 +4,7 @@ import { Component, Inject } from '@angular/core' import { NgbModal } from '@ng-bootstrap/ng-bootstrap' import { ConfigService, HostAppService, Profile, SelectorService, ProfilesService, PromptModalComponent, PlatformService, BaseComponent, PartialProfile, ProfileProvider, TranslateService, Platform, ProfileGroup, PartialProfileGroup } from 'tabby-core' import { EditProfileModalComponent } from './editProfileModal.component' +import { EditProfileGroupModalComponent, EditProfileGroupModalComponentResult } from './editProfileGroupModal.component' _('Filter') _('Ungrouped') @@ -140,27 +141,73 @@ export class ProfilesSettingsTabComponent extends BaseComponent { } } - async refresh (): Promise { - const profileGroupCollapsed = JSON.parse(window.localStorage.profileGroupCollapsed ?? '{}') - const groups = await this.profilesService.getProfileGroups(true, true) - 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 => ProfilesSettingsTabComponent.intoPartialCollapsableProfileGroup(g, profileGroupCollapsed[g.id] ?? false)) - } - - async editGroup (group: PartialProfileGroup): Promise { + async newProfileGroup (): Promise { const modal = this.ngbModal.open(PromptModalComponent) - modal.componentInstance.prompt = this.translate.instant('New name') - modal.componentInstance.value = group.name + modal.componentInstance.prompt = this.translate.instant('New group name') const result = await modal.result + if (result?.value.trim()) { + await this.profilesService.newProfileGroup({ id: '', name: result.value }) + } + } + + async editProfileGroup (group: PartialProfileGroup): Promise { + const result = await this.showProfileGroupEditModal(group) + if (!result) { + return + } + Object.assign(group, result) + await this.profilesService.writeProfileGroup(ProfilesSettingsTabComponent.collapsableIntoPartialProfileGroup(group)) + } + + async showProfileGroupEditModal (group: PartialProfileGroup): Promise|null> { + const modal = this.ngbModal.open( + EditProfileGroupModalComponent, + { size: 'lg' }, + ) + + modal.componentInstance.group = deepClone(group) + modal.componentInstance.providers = this.profileProviders + + const result: EditProfileGroupModalComponentResult | null = await modal.result.catch(() => null) + if (!result) { + return null + } + + if (result.provider) { + return this.editProfileGroupDefaults(result.group, result.provider) + } + + return result.group + } + + private async editProfileGroupDefaults (group: PartialProfileGroup, provider: ProfileProvider): Promise|null> { + 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) { - group.name = result.value - await this.profilesService.writeProfileGroup(ProfilesSettingsTabComponent.collapsableIntoPartialProfileGroup(group)) + // 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.showProfileGroupEditModal(group) } - async deleteGroup (group: PartialProfileGroup): Promise { + async deleteProfileGroup (group: PartialProfileGroup): Promise { if ((await this.platform.showMessageBox( { type: 'warning', @@ -193,6 +240,15 @@ export class ProfilesSettingsTabComponent extends BaseComponent { } } + async refresh (): Promise { + const profileGroupCollapsed = JSON.parse(window.localStorage.profileGroupCollapsed ?? '{}') + const groups = await this.profilesService.getProfileGroups(true, true) + 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 => ProfilesSettingsTabComponent.intoPartialCollapsableProfileGroup(g, profileGroupCollapsed[g.id] ?? false)) + } + isGroupVisible (group: PartialProfileGroup): boolean { return !this.filter || (group.profiles ?? []).some(x => this.isProfileVisible(x)) } @@ -223,6 +279,9 @@ export class ProfilesSettingsTabComponent extends BaseComponent { } toggleGroupCollapse (group: PartialProfileGroup): void { + if (group.profiles?.length === 0) { + return + } group.collapsed = !group.collapsed this.saveProfileGroupCollapse(group) } @@ -236,7 +295,7 @@ export class ProfilesSettingsTabComponent extends BaseComponent { model.type = provider.id modal.componentInstance.profile = Object.assign({}, model) modal.componentInstance.profileProvider = provider - modal.componentInstance.defaultsMode = true + modal.componentInstance.defaultsMode = 'enabled' const result = await modal.result // Fully replace the config diff --git a/tabby-settings/src/index.ts b/tabby-settings/src/index.ts index 850b8d61f7..e6ae3a1e16 100644 --- a/tabby-settings/src/index.ts +++ b/tabby-settings/src/index.ts @@ -7,6 +7,7 @@ import { InfiniteScrollModule } from 'ngx-infinite-scroll' import TabbyCorePlugin, { ToolbarButtonProvider, HotkeyProvider, ConfigProvider, HotkeysService, AppService } from 'tabby-core' import { EditProfileModalComponent } from './components/editProfileModal.component' +import { EditProfileGroupModalComponent } from './components/editProfileGroupModal.component' import { HotkeyInputModalComponent } from './components/hotkeyInputModal.component' import { HotkeySettingsTabComponent } from './components/hotkeySettingsTab.component' import { MultiHotkeyInputComponent } from './components/multiHotkeyInput.component' @@ -48,6 +49,7 @@ import { HotkeySettingsTabProvider, WindowSettingsTabProvider, VaultSettingsTabP ], declarations: [ EditProfileModalComponent, + EditProfileGroupModalComponent, HotkeyInputModalComponent, HotkeySettingsTabComponent, MultiHotkeyInputComponent,