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 <noreply@anthropic.com>
This commit is contained in:
Rushi 2026-07-11 10:11:09 -04:00 committed by GitHub
parent 7733c4a7e5
commit 830b7acefa
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 156 additions and 226 deletions

View file

@ -56,26 +56,12 @@ export const KEYBOARD_SETTINGS_FORM_CFG: ConfigFormSection<KeyboardConfig> = {
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),

View file

@ -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<SyncConfig> = {
},
{
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<SyncConfig> = {
},
{
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<SyncConfig> = {
},
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<SyncConfig> = {
// 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<SyncConfig> = {
},
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<SyncConfig> = {
},
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<SyncConfig> = {
},
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<SyncConfig> = {
},
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<SyncConfig> = {
// 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<SyncConfig> = {
// 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<SyncConfig> = {
// 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<SyncConfig> = {
// 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<SyncConfig> = {
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<SyncConfig> = {
// 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<SyncConfig> = {
// 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<SyncConfig> = {
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<SyncConfig> = {
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<SyncConfig> = {
// 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<SyncConfig> = {
// 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<SyncConfig> = {
},
{
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<SyncConfig> = {
},
{
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<SyncConfig> = {
},
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<SyncConfig> = {
// 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<SyncConfig> = {
// 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<SyncConfig> = {
{
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',

View file

@ -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 = <K extends keyof GlobalConfigState>(
selector: MemoizedSelector<object, GlobalConfigState[K]>,
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', () => {

View file

@ -1,6 +1,20 @@
<div class="page-settings page-wrapper">
@if (globalCfg) {
<div class="settings-container">
<!-- Shared by every tab's `mat-tab-label`: an icon + translated title. -->
<ng-template
#tabLabel
let-icon
let-labelKey="labelKey"
>
<mat-icon
class="tab-icon"
[matTooltip]="labelKey | translate"
>{{ icon }}</mat-icon
>
<span class="tab-label">{{ labelKey | translate }}</span>
</ng-template>
<mat-tab-group
class="settings-tabs"
[(selectedIndex)]="selectedTabIndex"
@ -9,12 +23,12 @@
<!-- Tab: General -->
<mat-tab>
<ng-template mat-tab-label>
<mat-icon
class="tab-icon"
[matTooltip]="'PS.TABS.GENERAL' | translate"
>settings</mat-icon
>
<span class="tab-label">{{ 'PS.TABS.GENERAL' | translate }}</span>
<ng-container
*ngTemplateOutlet="
tabLabel;
context: { $implicit: 'settings', labelKey: 'PS.TABS.GENERAL' }
"
></ng-container>
</ng-template>
<div class="tab-content">
<h2 class="mat-h2 section-title">
@ -26,10 +40,7 @@
<config-section
(save)="saveGlobalCfg($event)"
[cfg]="getGlobalCfgSection(section.key)"
[isExpanded]="
section.key === expandedSection ||
section.customSection === expandedSection
"
[isExpanded]="isSectionExpanded(section)"
[section]="section"
></config-section>
</section>
@ -40,12 +51,12 @@
<!-- Tab: Tasks -->
<mat-tab>
<ng-template mat-tab-label>
<mat-icon
class="tab-icon"
[matTooltip]="'PS.TABS.TASKS' | translate"
>task</mat-icon
>
<span class="tab-label">{{ 'PS.TABS.TASKS' | translate }}</span>
<ng-container
*ngTemplateOutlet="
tabLabel;
context: { $implicit: 'task', labelKey: 'PS.TABS.TASKS' }
"
></ng-container>
</ng-template>
<div class="tab-content">
<h2 class="mat-h2 section-title">
@ -56,10 +67,7 @@
<config-section
(save)="saveGlobalCfg($event)"
[cfg]="getGlobalCfgSection(section.key)"
[isExpanded]="
section.key === expandedSection ||
section.customSection === expandedSection
"
[isExpanded]="isSectionExpanded(section)"
[section]="section"
></config-section>
</section>
@ -80,12 +88,12 @@
<!-- Tab: Time & Tracking -->
<mat-tab>
<ng-template mat-tab-label>
<mat-icon
class="tab-icon"
[matTooltip]="'PS.TABS.TIME_TRACKING' | translate"
>timer</mat-icon
>
<span class="tab-label">{{ 'PS.TABS.TIME_TRACKING' | translate }}</span>
<ng-container
*ngTemplateOutlet="
tabLabel;
context: { $implicit: 'timer', labelKey: 'PS.TABS.TIME_TRACKING' }
"
></ng-container>
</ng-template>
<div class="tab-content">
<h2 class="mat-h2 section-title">
@ -96,10 +104,7 @@
<config-section
(save)="saveGlobalCfg($event)"
[cfg]="getGlobalCfgSection(section.key)"
[isExpanded]="
section.key === expandedSection ||
section.customSection === expandedSection
"
[isExpanded]="isSectionExpanded(section)"
[section]="section"
></config-section>
</section>
@ -110,12 +115,12 @@
<!-- Tab: Productivity -->
<mat-tab>
<ng-template mat-tab-label>
<mat-icon
class="tab-icon"
[matTooltip]="'PS.TABS.PRODUCTIVITY' | translate"
>trending_up</mat-icon
>
<span class="tab-label">{{ 'PS.TABS.PRODUCTIVITY' | translate }}</span>
<ng-container
*ngTemplateOutlet="
tabLabel;
context: { $implicit: 'trending_up', labelKey: 'PS.TABS.PRODUCTIVITY' }
"
></ng-container>
</ng-template>
<div class="tab-content tour-productivityHelper">
<h2 class="mat-h2 section-title">
@ -126,10 +131,7 @@
<config-section
(save)="saveGlobalCfg($event)"
[cfg]="getGlobalCfgSection(section.key)"
[isExpanded]="
section.key === expandedSection ||
section.customSection === expandedSection
"
[isExpanded]="isSectionExpanded(section)"
[section]="section"
></config-section>
</section>
@ -140,12 +142,12 @@
<!-- Tab: Plugins -->
<mat-tab>
<ng-template mat-tab-label>
<mat-icon
class="tab-icon"
[matTooltip]="'PS.TABS.PLUGINS' | translate"
>extension</mat-icon
>
<span class="tab-label">{{ 'PS.TABS.PLUGINS' | translate }}</span>
<ng-container
*ngTemplateOutlet="
tabLabel;
context: { $implicit: 'extension', labelKey: 'PS.TABS.PLUGINS' }
"
></ng-container>
</ng-template>
<div class="tab-content">
<h2 class="mat-h2 section-title">
@ -159,10 +161,7 @@
<config-section
(save)="saveGlobalCfg($event)"
[cfg]="getGlobalCfgSection(section.key)"
[isExpanded]="
section.key === expandedSection ||
section.customSection === expandedSection
"
[isExpanded]="isSectionExpanded(section)"
[section]="section"
></config-section>
</section>
@ -173,12 +172,12 @@
<!-- Tab: Sync & Backup -->
<mat-tab>
<ng-template mat-tab-label>
<mat-icon
class="tab-icon"
[matTooltip]="'PS.TABS.SYNC_BACKUP' | translate"
>cloud_sync</mat-icon
>
<span class="tab-label">{{ 'PS.TABS.SYNC_BACKUP' | translate }}</span>
<ng-container
*ngTemplateOutlet="
tabLabel;
context: { $implicit: 'cloud_sync', labelKey: 'PS.TABS.SYNC_BACKUP' }
"
></ng-container>
</ng-template>
<div class="tab-content">
<h2 class="mat-h2 section-title">
@ -246,10 +245,7 @@
<config-section
(save)="saveGlobalCfg($event)"
[cfg]="getGlobalCfgSection(section.key)"
[isExpanded]="
section.key === expandedSection ||
section.customSection === expandedSection
"
[isExpanded]="isSectionExpanded(section)"
[section]="section"
></config-section>
</section>

View file

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