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:
parent
0ef24ddf1d
commit
695c5ba670
@ -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] ?? {}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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
|
@ -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>
|
||||
}
|
@ -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(
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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')
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
Loading…
Reference in New Issue
Block a user