From 830b7acefad5586b228ea190c13f2e186df7c38b Mon Sep 17 00:00:00 2001 From: Rushi <84287593+Rishi943@users.noreply.github.com> Date: Sat, 11 Jul 2026 10:11:09 -0400 Subject: [PATCH] refactor(config): reduce duplicated form and selector boilerplate (#8928) - sync-form: shared rootSyncProvider/isSyncProvider/isNotSyncProvider helpers replace ~25 repeated parent-depth syncProvider predicates - keyboard-form: drop two dead commented-out field blocks - global-config.reducer.spec: itBehavesLikeConfigSectionSelector helper replaces 10 duplicated describe blocks (same assertions) - config-page: shared tab-label ng-template + isSectionExpanded() replace 6 duplicated template blocks No behavior change. Closes #7922 Co-authored-by: Claude Fable 5 --- .../config/form-cfgs/keyboard-form.const.ts | 14 -- .../config/form-cfgs/sync-form.const.ts | 97 +++++++----- .../store/global-config.reducer.spec.ts | 144 ++++-------------- .../config-page/config-page.component.html | 116 +++++++------- .../config-page/config-page.component.ts | 11 ++ 5 files changed, 156 insertions(+), 226 deletions(-) diff --git a/src/app/features/config/form-cfgs/keyboard-form.const.ts b/src/app/features/config/form-cfgs/keyboard-form.const.ts index f864326d6c..1ff0bcd7cf 100644 --- a/src/app/features/config/form-cfgs/keyboard-form.const.ts +++ b/src/app/features/config/form-cfgs/keyboard-form.const.ts @@ -56,26 +56,12 @@ export const KEYBOARD_SETTINGS_FORM_CFG: ConfigFormSection = { T.GCF.KEYBOARD.TOGGLE_TASK_VIEW_CUSTOMIZER_PANEL, ), kbField('toggleIssuePanel', T.GCF.KEYBOARD.TOGGLE_ISSUE_PANEL), - // { - // key: 'showHelp', - // type: 'keyboard', - // templateOptions: { - // label: T.GCF.KEYBOARD.SHOW_HELP - // }, - // }, kbField('showSearchBar', T.GCF.KEYBOARD.SHOW_SEARCH_BAR), kbField('toggleBacklog', T.GCF.KEYBOARD.TOGGLE_BACKLOG), kbField('goToWorkView', T.GCF.KEYBOARD.GO_TO_WORK_VIEW), kbField('goToFocusMode', T.GCF.KEYBOARD.GO_TO_FOCUS_MODE), kbField('goToTimeline', T.GCF.KEYBOARD.GO_TO_SCHEDULE), kbField('goToScheduledView', T.GCF.KEYBOARD.GO_TO_SCHEDULED_VIEW), - // { - // key: 'goToDailyAgenda', - // type: 'keyboard', - // templateOptions: { - // label: T.GCF.KEYBOARD.GO_TO_DAILY_AGENDA - // }, - // }, kbField('goToSettings', T.GCF.KEYBOARD.GO_TO_SETTINGS), kbField('zoomIn', T.GCF.KEYBOARD.ZOOM_IN), kbField('zoomOut', T.GCF.KEYBOARD.ZOOM_OUT), diff --git a/src/app/features/config/form-cfgs/sync-form.const.ts b/src/app/features/config/form-cfgs/sync-form.const.ts index 787b495661..6b97bce35d 100644 --- a/src/app/features/config/form-cfgs/sync-form.const.ts +++ b/src/app/features/config/form-cfgs/sync-form.const.ts @@ -80,7 +80,7 @@ const createWebdavFormFields = (options: { }, expressions: { 'props.required': (field: FormlyFieldConfig) => - field?.parent?.parent?.model?.syncProvider === SyncProviderId.WebDAV, + isSyncProvider(field, 2, SyncProviderId.WebDAV), }, }, { @@ -92,7 +92,7 @@ const createWebdavFormFields = (options: { }, expressions: { 'props.required': (field: FormlyFieldConfig) => - field?.parent?.parent?.model?.syncProvider === SyncProviderId.WebDAV, + isSyncProvider(field, 2, SyncProviderId.WebDAV), }, }, { @@ -105,7 +105,7 @@ const createWebdavFormFields = (options: { }, expressions: { 'props.required': (field: FormlyFieldConfig) => - field?.parent?.parent?.model?.syncProvider === SyncProviderId.WebDAV, + isSyncProvider(field, 2, SyncProviderId.WebDAV), }, }, { @@ -117,14 +117,42 @@ const createWebdavFormFields = (options: { }, expressions: { 'props.required': (field: FormlyFieldConfig) => - field?.parent?.parent?.model?.syncProvider === SyncProviderId.WebDAV, + isSyncProvider(field, 2, SyncProviderId.WebDAV), }, }, ]; }; +/** + * Reads the sync form's root `syncProvider`, given a field some number of + * `.parent` hops below the root. Centralized because Formly's parent-depth + * is otherwise easy to get wrong when copy-pasting a predicate to a field at + * a different nesting level: `depth: 1` for a field placed directly in + * `SYNC_FORM.items` (typically a provider's `fieldGroup` wrapper), `depth: 2` + * for a field nested one level inside such a fieldGroup. + */ +const rootSyncProvider = ( + field: FormlyFieldConfig | undefined, + depth: 1 | 2, +): SyncProviderId | null | undefined => { + const root = depth === 1 ? field?.parent : field?.parent?.parent; + return root?.model?.syncProvider; +}; + +const isSyncProvider = ( + field: FormlyFieldConfig | undefined, + depth: 1 | 2, + providerId: SyncProviderId | null, +): boolean => rootSyncProvider(field, depth) === providerId; + +const isNotSyncProvider = ( + field: FormlyFieldConfig | undefined, + depth: 1 | 2, + providerId: SyncProviderId | null, +): boolean => rootSyncProvider(field, depth) !== providerId; + const isOneDriveClientIdRequired = (field: FormlyFieldConfig): boolean => { - if (field?.parent?.parent?.model?.syncProvider !== SyncProviderId.OneDrive) { + if (!isSyncProvider(field, 2, SyncProviderId.OneDrive)) { return false; } @@ -182,8 +210,7 @@ export const SYNC_FORM: ConfigFormSection = { }, { hideExpression: (m, v, field) => - field?.parent?.model.syncProvider !== SyncProviderId.LocalFile || - IS_ANDROID_WEB_VIEW, + isNotSyncProvider(field, 1, SyncProviderId.LocalFile) || IS_ANDROID_WEB_VIEW, resetOnHide: false, key: 'localFileSync', fieldGroup: [ @@ -216,8 +243,7 @@ export const SYNC_FORM: ConfigFormSection = { }, { hideExpression: (m, v, field) => - field?.parent?.model.syncProvider !== SyncProviderId.LocalFile || - !IS_ANDROID_WEB_VIEW, + isNotSyncProvider(field, 1, SyncProviderId.LocalFile) || !IS_ANDROID_WEB_VIEW, resetOnHide: false, key: 'localFileSync', fieldGroup: [ @@ -246,7 +272,7 @@ export const SYNC_FORM: ConfigFormSection = { }, expressions: { 'props.required': (field: FormlyFieldConfig) => - field?.parent?.parent?.model?.syncProvider === SyncProviderId.LocalFile, + isSyncProvider(field, 2, SyncProviderId.LocalFile), }, }, ], @@ -255,7 +281,7 @@ export const SYNC_FORM: ConfigFormSection = { // Nextcloud provider form fields { hideExpression: (m, v, field) => - field?.parent?.model.syncProvider !== SyncProviderId.Nextcloud, + isNotSyncProvider(field, 1, SyncProviderId.Nextcloud), resetOnHide: false, key: 'nextcloud', fieldGroup: [ @@ -280,7 +306,7 @@ export const SYNC_FORM: ConfigFormSection = { }, expressions: { 'props.required': (field: FormlyFieldConfig) => - field?.parent?.parent?.model?.syncProvider === SyncProviderId.Nextcloud, + isSyncProvider(field, 2, SyncProviderId.Nextcloud), }, }, { @@ -292,7 +318,7 @@ export const SYNC_FORM: ConfigFormSection = { }, expressions: { 'props.required': (field: FormlyFieldConfig) => - field?.parent?.parent?.model?.syncProvider === SyncProviderId.Nextcloud, + isSyncProvider(field, 2, SyncProviderId.Nextcloud), }, }, { @@ -313,7 +339,7 @@ export const SYNC_FORM: ConfigFormSection = { }, expressions: { 'props.required': (field: FormlyFieldConfig) => - field?.parent?.parent?.model?.syncProvider === SyncProviderId.Nextcloud, + isSyncProvider(field, 2, SyncProviderId.Nextcloud), }, }, { @@ -324,7 +350,7 @@ export const SYNC_FORM: ConfigFormSection = { }, expressions: { 'props.required': (field: FormlyFieldConfig) => - field?.parent?.parent?.model?.syncProvider === SyncProviderId.Nextcloud, + isSyncProvider(field, 2, SyncProviderId.Nextcloud), }, }, ], @@ -332,8 +358,7 @@ export const SYNC_FORM: ConfigFormSection = { // WebDAV provider form fields { - hideExpression: (m, v, field) => - field?.parent?.model.syncProvider !== SyncProviderId.WebDAV, + hideExpression: (m, v, field) => isNotSyncProvider(field, 1, SyncProviderId.WebDAV), resetOnHide: false, key: 'webDav', fieldGroup: createWebdavFormFields({ @@ -348,7 +373,7 @@ export const SYNC_FORM: ConfigFormSection = { // Note: No key needed - Dropbox credentials stored privately via SyncCredentialStore { hideExpression: (m, v, field) => - field?.parent?.model.syncProvider !== SyncProviderId.Dropbox, + isNotSyncProvider(field, 1, SyncProviderId.Dropbox), resetOnHide: false, fieldGroup: [ { @@ -363,7 +388,7 @@ export const SYNC_FORM: ConfigFormSection = { // OneDrive provider form fields { hideExpression: (m, v, field) => - field?.parent?.model.syncProvider !== SyncProviderId.OneDrive, + isNotSyncProvider(field, 1, SyncProviderId.OneDrive), resetOnHide: false, key: 'oneDrive', fieldGroup: [ @@ -431,7 +456,7 @@ export const SYNC_FORM: ConfigFormSection = { // OneDrive provider authentication panel { hideExpression: (m, v, field) => - field?.parent?.model.syncProvider !== SyncProviderId.OneDrive, + isNotSyncProvider(field, 1, SyncProviderId.OneDrive), resetOnHide: false, props: {}, fieldGroup: [ @@ -468,8 +493,7 @@ export const SYNC_FORM: ConfigFormSection = { btnType: 'primary', btnStyle: 'stroked', onClick: async (field: FormlyFieldConfig) => { - const isSuperSync = - field?.parent?.parent?.model?.syncProvider === SyncProviderId.SuperSync; + const isSuperSync = isSyncProvider(field, 2, SyncProviderId.SuperSync); await (isSuperSync ? openEncryptionPasswordChangeDialog() : openEncryptionPasswordChangeDialogForFileBased()); @@ -479,7 +503,7 @@ export const SYNC_FORM: ConfigFormSection = { // Hide disable encryption for SuperSync — encryption is mandatory { hideExpression: (m: any, v: any, field?: FormlyFieldConfig) => - field?.parent?.parent?.model?.syncProvider === SyncProviderId.SuperSync, + isSyncProvider(field, 2, SyncProviderId.SuperSync), type: 'btn', className: 'e2e-disable-encryption-btn', templateOptions: { @@ -510,8 +534,7 @@ export const SYNC_FORM: ConfigFormSection = { // The dialog component drops this hide in edit mode and appends action buttons. { type: 'collapsible', - hideExpression: (m, v, field) => - field?.parent?.model.syncProvider === SyncProviderId.SuperSync, + hideExpression: (m, v, field) => isSyncProvider(field, 1, SyncProviderId.SuperSync), // syncRole is a stable structural marker the dialog routes on, so a // future global rename of T.G.ADVANCED_CFG cannot silently break it. props: { label: T.G.ADVANCED_CFG, syncRole: 'advanced' } as SyncCollapsibleProps, @@ -534,8 +557,7 @@ export const SYNC_FORM: ConfigFormSection = { key: 'isManualSyncOnly', type: 'checkbox', // Only show for file-based providers (Dropbox, WebDAV, LocalFile, Nextcloud) - hideExpression: (m, v, field) => - field?.parent?.parent?.model?.syncProvider === null, + hideExpression: (m, v, field) => isSyncProvider(field, 2, null), templateOptions: { label: T.F.SYNC.FORM.L_MANUAL_SYNC_ONLY, }, @@ -553,8 +575,8 @@ export const SYNC_FORM: ConfigFormSection = { key: 'isUseSplitSyncFiles', type: 'checkbox', hideExpression: (m, v, field) => - field?.parent?.parent?.model?.syncProvider === null || - field?.parent?.parent?.model?.syncProvider === SyncProviderId.SuperSync, + isSyncProvider(field, 2, null) || + isSyncProvider(field, 2, SyncProviderId.SuperSync), templateOptions: { label: T.F.SYNC.FORM.L_USE_SPLIT_SYNC_FILES, }, @@ -562,8 +584,7 @@ export const SYNC_FORM: ConfigFormSection = { // Enable encryption button for file-based providers (shown when encryption is disabled) { hideExpression: (m: any, v: any, field?: FormlyFieldConfig) => - field?.parent?.parent?.model.syncProvider === SyncProviderId.SuperSync || - m.isEncryptionEnabled, + isSyncProvider(field, 2, SyncProviderId.SuperSync) || m.isEncryptionEnabled, type: 'btn', className: 'e2e-file-based-enable-encryption-btn', templateOptions: { @@ -594,7 +615,7 @@ export const SYNC_FORM: ConfigFormSection = { // above already shows "Encryption password is set" in that case). { hideExpression: (m: any, v: any, field?: FormlyFieldConfig) => - field?.parent?.parent?.model?.syncProvider !== SyncProviderId.SuperSync || + isNotSyncProvider(field, 2, SyncProviderId.SuperSync) || (field?.parent?.parent?.model?.isEncryptionEnabled ?? false), type: 'tpl', templateOptions: { @@ -605,7 +626,7 @@ export const SYNC_FORM: ConfigFormSection = { }, { hideExpression: (m, v, field) => - field?.parent?.parent?.model.syncProvider !== SyncProviderId.SuperSync, + isNotSyncProvider(field, 2, SyncProviderId.SuperSync), type: 'btn', templateOptions: { text: T.F.SYNC.FORM.SUPER_SYNC.BTN_GET_TOKEN, @@ -621,7 +642,7 @@ export const SYNC_FORM: ConfigFormSection = { }, { hideExpression: (m, v, field) => - field?.parent?.parent?.model.syncProvider !== SyncProviderId.SuperSync, + isNotSyncProvider(field, 2, SyncProviderId.SuperSync), key: 'accessToken', type: 'textarea', className: 'e2e-accessToken', @@ -632,7 +653,7 @@ export const SYNC_FORM: ConfigFormSection = { }, expressions: { 'props.required': (field: FormlyFieldConfig) => - field?.parent?.parent?.model?.syncProvider === SyncProviderId.SuperSync, + isSyncProvider(field, 2, SyncProviderId.SuperSync), }, }, // Encryption encouragement warning (shown when encryption is NOT enabled) @@ -642,7 +663,7 @@ export const SYNC_FORM: ConfigFormSection = { // the root flag from SuperSync's privateCfg.isEncryptionEnabled). { hideExpression: (m: any, v: any, field?: FormlyFieldConfig) => - field?.parent?.parent?.model?.syncProvider !== SyncProviderId.SuperSync || + isNotSyncProvider(field, 2, SyncProviderId.SuperSync) || (field?.parent?.parent?.model?.isEncryptionEnabled ?? false) || field?.parent?.parent?.model?._isInitialSetup === true, type: 'tpl', @@ -656,7 +677,7 @@ export const SYNC_FORM: ConfigFormSection = { // Hidden during initial setup (encryption dialog opens automatically after save) { hideExpression: (m: any, v: any, field?: FormlyFieldConfig) => - field?.parent?.parent?.model?.syncProvider !== SyncProviderId.SuperSync || + isNotSyncProvider(field, 2, SyncProviderId.SuperSync) || (field?.parent?.parent?.model?.isEncryptionEnabled ?? false) || field?.parent?.parent?.model?._isInitialSetup === true, type: 'btn', @@ -694,7 +715,7 @@ export const SYNC_FORM: ConfigFormSection = { { type: 'collapsible', hideExpression: (m, v, field) => - field?.parent?.parent?.model.syncProvider !== SyncProviderId.SuperSync, + isNotSyncProvider(field, 2, SyncProviderId.SuperSync), props: { label: T.G.ADVANCED_CFG, syncRole: 'advanced', diff --git a/src/app/features/config/store/global-config.reducer.spec.ts b/src/app/features/config/store/global-config.reducer.spec.ts index c7b736fb4d..acc3464f69 100644 --- a/src/app/features/config/store/global-config.reducer.spec.ts +++ b/src/app/features/config/store/global-config.reducer.spec.ts @@ -20,6 +20,7 @@ import { import { updateGlobalConfigSection } from './global-config.actions'; import { loadAllData } from '../../../root-store/meta/load-all-data.action'; import { GlobalConfigState } from '../global-config.model'; +import { MemoizedSelector } from '@ngrx/store'; import { SyncProviderId } from '../../../op-log/sync-providers/provider.const'; import { AppDataComplete } from '../../../op-log/model/model-config'; import { DEFAULT_GLOBAL_CONFIG } from '../default-global-config.const'; @@ -1053,124 +1054,63 @@ describe('GlobalConfigReducer', () => { }); describe('Selectors', () => { - describe('selectLocalizationConfig', () => { + // Shared contract of every `createConfigSectionSelector` output: falls back to + // the baked-in default when state is undefined, otherwise passes the slice + // through unchanged. Keeps the per-selector blocks below to only what's + // beyond that shared contract (e.g. selectFocusModeConfig's extra regression). + const itBehavesLikeConfigSectionSelector = ( + selector: MemoizedSelector, + key: K, + ): void => { it('should return default config when state is undefined', () => { - const result = selectLocalizationConfig.projector(undefined as any); - expect(result).toEqual(DEFAULT_GLOBAL_CONFIG.localization); + const result = selector.projector(undefined as any); + expect(result).toEqual(DEFAULT_GLOBAL_CONFIG[key]); }); - it('should return localization config when state is defined', () => { - const result = selectLocalizationConfig.projector(initialGlobalConfigState); - expect(result).toEqual(initialGlobalConfigState.localization); + it(`should return ${key} config when state is defined`, () => { + const result = selector.projector(initialGlobalConfigState); + expect(result).toEqual(initialGlobalConfigState[key]); }); + }; + + describe('selectLocalizationConfig', () => { + itBehavesLikeConfigSectionSelector(selectLocalizationConfig, 'localization'); }); describe('selectMiscConfig', () => { - it('should return default config when state is undefined', () => { - const result = selectMiscConfig.projector(undefined as any); - expect(result).toEqual(DEFAULT_GLOBAL_CONFIG.misc); - }); - - it('should return misc config when state is defined', () => { - const result = selectMiscConfig.projector(initialGlobalConfigState); - expect(result).toEqual(initialGlobalConfigState.misc); - }); + itBehavesLikeConfigSectionSelector(selectMiscConfig, 'misc'); }); describe('selectShortSyntaxConfig', () => { - it('should return default config when state is undefined', () => { - const result = selectShortSyntaxConfig.projector(undefined as any); - expect(result).toEqual(DEFAULT_GLOBAL_CONFIG.shortSyntax); - }); - - it('should return shortSyntax config when state is defined', () => { - const result = selectShortSyntaxConfig.projector(initialGlobalConfigState); - expect(result).toEqual(initialGlobalConfigState.shortSyntax); - }); + itBehavesLikeConfigSectionSelector(selectShortSyntaxConfig, 'shortSyntax'); }); describe('selectSoundConfig', () => { - it('should return default config when state is undefined', () => { - const result = selectSoundConfig.projector(undefined as any); - expect(result).toEqual(DEFAULT_GLOBAL_CONFIG.sound); - }); - - it('should return sound config when state is defined', () => { - const result = selectSoundConfig.projector(initialGlobalConfigState); - expect(result).toEqual(initialGlobalConfigState.sound); - }); + itBehavesLikeConfigSectionSelector(selectSoundConfig, 'sound'); }); describe('selectEvaluationConfig', () => { - it('should return default config when state is undefined', () => { - const result = selectEvaluationConfig.projector(undefined as any); - expect(result).toEqual(DEFAULT_GLOBAL_CONFIG.evaluation); - }); - - it('should return evaluation config when state is defined', () => { - const result = selectEvaluationConfig.projector(initialGlobalConfigState); - expect(result).toEqual(initialGlobalConfigState.evaluation); - }); + itBehavesLikeConfigSectionSelector(selectEvaluationConfig, 'evaluation'); }); describe('selectIdleConfig', () => { - it('should return default config when state is undefined', () => { - const result = selectIdleConfig.projector(undefined as any); - expect(result).toEqual(DEFAULT_GLOBAL_CONFIG.idle); - }); - - it('should return idle config when state is defined', () => { - const result = selectIdleConfig.projector(initialGlobalConfigState); - expect(result).toEqual(initialGlobalConfigState.idle); - }); + itBehavesLikeConfigSectionSelector(selectIdleConfig, 'idle'); }); describe('selectSyncConfig', () => { - it('should return default config when state is undefined', () => { - const result = selectSyncConfig.projector(undefined as any); - expect(result).toEqual(DEFAULT_GLOBAL_CONFIG.sync); - }); - - it('should return sync config when state is defined', () => { - const result = selectSyncConfig.projector(initialGlobalConfigState); - expect(result).toEqual(initialGlobalConfigState.sync); - }); + itBehavesLikeConfigSectionSelector(selectSyncConfig, 'sync'); }); describe('selectTakeABreakConfig', () => { - it('should return default config when state is undefined', () => { - const result = selectTakeABreakConfig.projector(undefined as any); - expect(result).toEqual(DEFAULT_GLOBAL_CONFIG.takeABreak); - }); - - it('should return takeABreak config when state is defined', () => { - const result = selectTakeABreakConfig.projector(initialGlobalConfigState); - expect(result).toEqual(initialGlobalConfigState.takeABreak); - }); + itBehavesLikeConfigSectionSelector(selectTakeABreakConfig, 'takeABreak'); }); describe('selectTimelineConfig', () => { - it('should return default config when state is undefined', () => { - const result = selectTimelineConfig.projector(undefined as any); - expect(result).toEqual(DEFAULT_GLOBAL_CONFIG.schedule); - }); - - it('should return schedule config when state is defined', () => { - const result = selectTimelineConfig.projector(initialGlobalConfigState); - expect(result).toEqual(initialGlobalConfigState.schedule); - }); + itBehavesLikeConfigSectionSelector(selectTimelineConfig, 'schedule'); }); describe('selectIsDominaModeConfig', () => { - it('should return default config when state is undefined', () => { - const result = selectIsDominaModeConfig.projector(undefined as any); - expect(result).toEqual(DEFAULT_GLOBAL_CONFIG.dominaMode); - }); - - it('should return dominaMode config when state is defined', () => { - const result = selectIsDominaModeConfig.projector(initialGlobalConfigState); - expect(result).toEqual(initialGlobalConfigState.dominaMode); - }); + itBehavesLikeConfigSectionSelector(selectIsDominaModeConfig, 'dominaMode'); }); describe('selectFocusModeConfig', () => { @@ -1180,39 +1120,15 @@ describe('GlobalConfigReducer', () => { expect(DEFAULT_GLOBAL_CONFIG.focusMode.isPauseTrackingDuringBreak).toBe(true); }); - it('should return default config when state is undefined', () => { - const result = selectFocusModeConfig.projector(undefined as any); - expect(result).toEqual(DEFAULT_GLOBAL_CONFIG.focusMode); - }); - - it('should return focusMode config when state is defined', () => { - const result = selectFocusModeConfig.projector(initialGlobalConfigState); - expect(result).toEqual(initialGlobalConfigState.focusMode); - }); + itBehavesLikeConfigSectionSelector(selectFocusModeConfig, 'focusMode'); }); describe('selectPomodoroConfig', () => { - it('should return default config when state is undefined', () => { - const result = selectPomodoroConfig.projector(undefined as any); - expect(result).toEqual(DEFAULT_GLOBAL_CONFIG.pomodoro); - }); - - it('should return pomodoro config when state is defined', () => { - const result = selectPomodoroConfig.projector(initialGlobalConfigState); - expect(result).toEqual(initialGlobalConfigState.pomodoro); - }); + itBehavesLikeConfigSectionSelector(selectPomodoroConfig, 'pomodoro'); }); describe('selectReminderConfig', () => { - it('should return default config when state is undefined', () => { - const result = selectReminderConfig.projector(undefined as any); - expect(result).toEqual(DEFAULT_GLOBAL_CONFIG.reminder); - }); - - it('should return reminder config when state is defined', () => { - const result = selectReminderConfig.projector(initialGlobalConfigState); - expect(result).toEqual(initialGlobalConfigState.reminder); - }); + itBehavesLikeConfigSectionSelector(selectReminderConfig, 'reminder'); }); describe('selectIsFocusModeEnabled', () => { diff --git a/src/app/pages/config-page/config-page.component.html b/src/app/pages/config-page/config-page.component.html index 70eb7117ff..b03a443340 100644 --- a/src/app/pages/config-page/config-page.component.html +++ b/src/app/pages/config-page/config-page.component.html @@ -1,6 +1,20 @@
@if (globalCfg) {
+ + + {{ icon }} + {{ labelKey | translate }} + + - settings - {{ 'PS.TABS.GENERAL' | translate }} +

@@ -26,10 +40,7 @@ @@ -40,12 +51,12 @@ - task - {{ 'PS.TABS.TASKS' | translate }} +

@@ -56,10 +67,7 @@ @@ -80,12 +88,12 @@ - timer - {{ 'PS.TABS.TIME_TRACKING' | translate }} +

@@ -96,10 +104,7 @@ @@ -110,12 +115,12 @@ - trending_up - {{ 'PS.TABS.PRODUCTIVITY' | translate }} +

@@ -126,10 +131,7 @@ @@ -140,12 +142,12 @@ - extension - {{ 'PS.TABS.PLUGINS' | translate }} +

@@ -159,10 +161,7 @@ @@ -173,12 +172,12 @@ - cloud_sync - {{ 'PS.TABS.SYNC_BACKUP' | translate }} +

@@ -246,10 +245,7 @@ diff --git a/src/app/pages/config-page/config-page.component.ts b/src/app/pages/config-page/config-page.component.ts index 55a946780a..707cd0a00f 100644 --- a/src/app/pages/config-page/config-page.component.ts +++ b/src/app/pages/config-page/config-page.component.ts @@ -25,6 +25,7 @@ import { } from '../../features/config/global-config-form-config.const'; import { ConfigFormConfig, + GenericConfigFormSection, GlobalConfigFormSectionKey, GlobalConfigSectionKey, GlobalConfigState, @@ -67,6 +68,7 @@ import { MatTab, MatTabGroup, MatTabLabel } from '@angular/material/tabs'; import { MatIcon } from '@angular/material/icon'; import { MatTooltip } from '@angular/material/tooltip'; import { MatButton } from '@angular/material/button'; +import { NgTemplateOutlet } from '@angular/common'; import { LocalBackupService } from '../../imex/local-backup/local-backup.service'; @Component({ @@ -86,6 +88,7 @@ import { LocalBackupService } from '../../imex/local-backup/local-backup.service MatIcon, MatTooltip, MatButton, + NgTemplateOutlet, ], }) export class ConfigPageComponent implements OnInit { @@ -419,6 +422,14 @@ export class ConfigPageComponent implements OnInit { return !!(await firstValueFrom(dialogRef.afterClosed())); } + /** Shared `[isExpanded]` check for the `config-section` repeated across every tab. */ + isSectionExpanded(section: GenericConfigFormSection): boolean { + return ( + section.key === this.expandedSection || + section.customSection === this.expandedSection + ); + } + getGlobalCfgSection( sectionKey: GlobalConfigFormSectionKey | ProjectCfgFormKey, ): GlobalSectionConfig {