mirror of
https://github.com/johannesjo/super-productivity.git
synced 2026-07-17 16:37:43 +00:00
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:
parent
7733c4a7e5
commit
830b7acefa
5 changed files with 156 additions and 226 deletions
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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', () => {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue