1
1
mirror of https://github.com/Eugeny/tabby.git synced 2024-11-23 05:03:36 +03:00

feat(core/settings): Eugeny/tabby#3999 Allow groups to specify settings that hosts inherit

This commit is contained in:
Clem Fern 2023-08-11 23:38:16 +02:00
parent 0ef24ddf1d
commit 695c5ba670
8 changed files with 190 additions and 47 deletions

View File

@ -65,8 +65,8 @@ export class ProfilesService {
* Return ConfigProxy for a given Profile
* arg: skipUserDefaults -> do not merge global provider defaults in ConfigProxy
*/
getConfigProxyForProfile <T extends Profile> (profile: PartialProfile<T>, skipUserDefaults = false): T {
const defaults = this.getProfileDefaults(profile, skipUserDefaults).reduce(configMerge, {})
getConfigProxyForProfile <T extends Profile> (profile: PartialProfile<T>, 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<Profile>, skipUserDefaults = false): any {
getProfileDefaults (profile: PartialProfile<Profile>, 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<ProfileGroup>[] {
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<ProfileGroup>[] = deepClone(this.config.store.groups ?? [])
let groups: PartialProfileGroup<ProfileGroup>[] = 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<Profile>): any {
return this.getSyncProfileGroups().find(g => g.id === groupId)?.defaults?.[provider.id] ?? {}
}
}

View File

@ -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

View File

@ -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<G extends ProfileGroup> {
@Input() group: G & ConfigProxy
@Input() providers: ProfileProvider<Profile>[]
constructor (
private modalInstance: NgbActiveModal,
) {}
save () {
this.modalInstance.close({ group: this.group })
}
cancel () {
this.modalInstance.dismiss()
}
editDefaults (provider: ProfileProvider<Profile>) {
this.modalInstance.close({ group: this.group, provider })
}
}
export interface EditProfileGroupModalComponentResult<G extends ProfileGroup> {
group: G
provider?: ProfileProvider<Profile>
}

View File

@ -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(

View File

@ -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<P extends Profile> {
@Input() profile: P & ConfigProxy
@Input() profileProvider: ProfileProvider<P>
@Input() settingsComponent: new () => ProfileSettingsComponent<P>
@Input() defaultsMode = false
@Input() profileGroup: PartialProfileGroup<ProfileGroup> | string | undefined
@Input() defaultsMode: 'enabled'|'group'|'disabled' = 'disabled'
@Input() profileGroup: PartialProfileGroup<ProfileGroup> | undefined
groups: PartialProfileGroup<ProfileGroup>[]
@ViewChild('placeholder', { read: ViewContainerRef }) placeholder: ViewContainerRef
@ -35,7 +34,7 @@ export class EditProfileModalComponent<P extends Profile> {
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<P extends Profile> {
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<P extends Profile> {
if (!this.profileGroup) {
this.profile.group = undefined
} else {
if (typeof this.profileGroup === 'string') {
const newGroup: PartialProfileGroup<ProfileGroup> = {
id: uuidv4(),
name: this.profileGroup,
}
this.profilesService.newProfileGroup(newGroup, false, false)
this.profileGroup = newGroup
}
this.profile.group = this.profileGroup.id
}

View File

@ -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')

View File

@ -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<void> {
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<CollapsableProfileGroup>): Promise<void> {
async newProfileGroup (): Promise<void> {
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) {
group.name = result.value
await this.profilesService.writeProfileGroup(ProfilesSettingsTabComponent.collapsableIntoPartialProfileGroup(group))
if (result?.value.trim()) {
await this.profilesService.newProfileGroup({ id: '', name: result.value })
}
}
async deleteGroup (group: PartialProfileGroup<ProfileGroup>): Promise<void> {
async editProfileGroup (group: PartialProfileGroup<CollapsableProfileGroup>): Promise<void> {
const result = await this.showProfileGroupEditModal(group)
if (!result) {
return
}
Object.assign(group, result)
await this.profilesService.writeProfileGroup(ProfilesSettingsTabComponent.collapsableIntoPartialProfileGroup(group))
}
async showProfileGroupEditModal (group: PartialProfileGroup<CollapsableProfileGroup>): Promise<PartialProfileGroup<CollapsableProfileGroup>|null> {
const modal = this.ngbModal.open(
EditProfileGroupModalComponent,
{ size: 'lg' },
)
modal.componentInstance.group = deepClone(group)
modal.componentInstance.providers = this.profileProviders
const result: EditProfileGroupModalComponentResult<CollapsableProfileGroup> | 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<CollapsableProfileGroup>, provider: ProfileProvider<Profile>): Promise<PartialProfileGroup<CollapsableProfileGroup>|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) {
// 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 deleteProfileGroup (group: PartialProfileGroup<ProfileGroup>): Promise<void> {
if ((await this.platform.showMessageBox(
{
type: 'warning',
@ -193,6 +240,15 @@ export class ProfilesSettingsTabComponent extends BaseComponent {
}
}
async refresh (): Promise<void> {
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<ProfileGroup>): boolean {
return !this.filter || (group.profiles ?? []).some(x => this.isProfileVisible(x))
}
@ -223,6 +279,9 @@ export class ProfilesSettingsTabComponent extends BaseComponent {
}
toggleGroupCollapse (group: PartialProfileGroup<CollapsableProfileGroup>): 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

View File

@ -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,