Skip to content

Commit

Permalink
feat(core/settings): #3999 Allow groups to specify settings that host…
Browse files Browse the repository at this point in the history
…s inherit
  • Loading branch information
Clem-Fern committed Aug 11, 2023
1 parent 0ef24dd commit 695c5ba
Show file tree
Hide file tree
Showing 8 changed files with 189 additions and 46 deletions.
29 changes: 24 additions & 5 deletions tabby-core/src/services/profiles.service.ts
Expand Up @@ -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
}

Expand Down Expand Up @@ -373,19 +373,29 @@ 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) : {},
]
}

/*
* 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
Expand All @@ -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

Expand Down Expand Up @@ -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] ?? {}
}

}
29 changes: 29 additions & 0 deletions 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
34 changes: 34 additions & 0 deletions 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<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>
}
11 changes: 6 additions & 5 deletions 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}'
Expand All @@ -10,15 +10,15 @@
.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',
autofocus,
[(ngModel)]='profile.name',
)

.mb-3(*ngIf='!defaultsMode')
.mb-3(*ngIf='defaultsMode === "disabled"')
label(translate) Group
input.form-control(
type='text',
Expand All @@ -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(
Expand Down
17 changes: 4 additions & 13 deletions tabby-settings/src/components/editProfileModal.component.ts
Expand Up @@ -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(
Expand All @@ -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

Expand All @@ -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)
Expand All @@ -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 () {
Expand Down Expand Up @@ -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
}

Expand Down
22 changes: 15 additions & 7 deletions tabby-settings/src/components/profilesSettingsTab.component.pug
Expand Up @@ -27,27 +27,35 @@ 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')
ng-container(*ngIf='isGroupVisible(group)')
.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')
Expand Down
91 changes: 75 additions & 16 deletions tabby-settings/src/components/profilesSettingsTab.component.ts
Expand Up @@ -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')
Expand Down Expand Up @@ -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?.value.trim()) {
await this.profilesService.newProfileGroup({ id: '', name: result.value })
}
}

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) {
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<ProfileGroup>): Promise<void> {
async deleteProfileGroup (group: PartialProfileGroup<ProfileGroup>): Promise<void> {
if ((await this.platform.showMessageBox(
{
type: 'warning',
Expand Down Expand Up @@ -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))
}
Expand Down Expand Up @@ -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)
}
Expand All @@ -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
Expand Down

0 comments on commit 695c5ba

Please sign in to comment.