From d989c064f39d42e6232a34e86da36832fd7ea851 Mon Sep 17 00:00:00 2001 From: Johannes Millan Date: Tue, 10 Mar 2026 12:13:42 +0100 Subject: [PATCH] perf: reduce initial bundle size via lazy-loading - Lazy-load locale data: only en-GB statically imported, all 26 others use dynamic import() loaded during idle time - Remove core-js/es/object polyfill (Object.fromEntries supported since Chrome 73+, Android 9 is EOL) - Defer right-panel content with @defer (prefetch on idle) blocks, moving markdown, schedule, and task-detail to lazy chunks - Lazy-load bottom panel via dynamic import() - Lazy-load Formly: remove FormlyConfigModule from bootstrap, each dialog component imports it directly; dialog openers use dynamic import() so Formly is only loaded when a dialog opens - Extract _openSyncInitialCfgDialog helper to DRY up duplicate import - Guard bottom sheet and shortcut dialog opens against race conditions after async import --- .../magic-nav-config.service.ts | 5 +- src/app/core-ui/shortcut/shortcut.service.ts | 21 +-- .../work-context-menu.component.ts | 7 +- src/app/core/locale.constants.ts | 122 ++++++++---------- .../boards/board-edit/board-edit.component.ts | 4 +- .../config-form/config-form.component.ts | 5 +- .../dialog-pomodoro-settings.component.ts | 5 +- .../dialog-edit-issue-provider.component.ts | 4 +- .../dialog-create-project.component.ts | 5 +- .../right-panel-content.component.html | 28 ++-- .../right-panel/right-panel.component.ts | 39 +++--- ...-simple-counter-edit-settings.component.ts | 5 +- .../dialog-edit-task-repeat-cfg.component.ts | 5 +- .../add-task-bar/add-task-bar.component.ts | 5 +- src/app/features/tasks/task/task.component.ts | 5 +- .../dialog-work-context-settings.component.ts | 5 +- .../dialog-sync-initial-cfg.component.ts | 2 - src/app/imex/sync/sync-wrapper.service.ts | 11 +- .../plugin-config-dialog.component.ts | 5 +- src/main.ts | 41 +++--- src/polyfills.ts | 3 +- 21 files changed, 175 insertions(+), 157 deletions(-) diff --git a/src/app/core-ui/magic-side-nav/magic-nav-config.service.ts b/src/app/core-ui/magic-side-nav/magic-nav-config.service.ts index b3ebeb1160..b2ff7d1073 100644 --- a/src/app/core-ui/magic-side-nav/magic-nav-config.service.ts +++ b/src/app/core-ui/magic-side-nav/magic-nav-config.service.ts @@ -10,7 +10,6 @@ import { ShepherdService } from '../../features/shepherd/shepherd.service'; import { TourId } from '../../features/shepherd/shepherd-steps.const'; import { T } from '../../t.const'; import { LS } from '../../core/persistence/storage-keys.const'; -import { DialogCreateProjectComponent } from '../../features/project/dialogs/create-project/dialog-create-project.component'; import { getGithubErrorUrl } from '../../core/error-handler/global-error-handler.util'; import { DialogPromptComponent } from '../../ui/dialog-prompt/dialog-prompt.component'; import { @@ -498,7 +497,9 @@ export class MagicNavConfigService { } // Simple action handlers - private _openCreateProject(): void { + private async _openCreateProject(): Promise { + const { DialogCreateProjectComponent } = + await import('../../features/project/dialogs/create-project/dialog-create-project.component'); this._matDialog .open(DialogCreateProjectComponent, { restoreFocus: true }) .afterClosed() diff --git a/src/app/core-ui/shortcut/shortcut.service.ts b/src/app/core-ui/shortcut/shortcut.service.ts index 88d8898559..7cd8f2e220 100644 --- a/src/app/core-ui/shortcut/shortcut.service.ts +++ b/src/app/core-ui/shortcut/shortcut.service.ts @@ -9,7 +9,6 @@ import { LayoutService } from '../layout/layout.service'; import { TaskService } from '../../features/tasks/task.service'; import { MatDialog } from '@angular/material/dialog'; import { DialogAddNoteComponent } from '../../features/note/dialog-add-note/dialog-add-note.component'; -import { DialogCreateProjectComponent } from '../../features/project/dialogs/create-project/dialog-create-project.component'; import { IPC } from '../../../../electron/shared-with-frontend/ipc-events.const'; import { UiHelperService } from '../../features/ui-helper/ui-helper.service'; import { WorkContextService } from '../../features/work-context/work-context.service'; @@ -156,15 +155,19 @@ export class ShortcutService { ev.preventDefault(); } else if (checkKeyCombo(ev, keys.addNewProject)) { if (this._matDialog.openDialogs.length === 0) { - this._matDialog - .open(DialogCreateProjectComponent, { restoreFocus: true }) - .afterClosed() - .subscribe((newProjectId: string | undefined) => { - if (newProjectId) { - this._router.navigate([`project/${newProjectId}/tasks`]); - } - }); ev.preventDefault(); + const { DialogCreateProjectComponent } = + await import('../../features/project/dialogs/create-project/dialog-create-project.component'); + if (this._matDialog.openDialogs.length === 0) { + this._matDialog + .open(DialogCreateProjectComponent, { restoreFocus: true }) + .afterClosed() + .subscribe((newProjectId: string | undefined) => { + if (newProjectId) { + this._router.navigate([`project/${newProjectId}/tasks`]); + } + }); + } } } else if (checkKeyCombo(ev, keys.addNewNote)) { if (this._matDialog.openDialogs.length === 0) { diff --git a/src/app/core-ui/work-context-menu/work-context-menu.component.ts b/src/app/core-ui/work-context-menu/work-context-menu.component.ts index b51c8ec9bf..d6bab155ad 100644 --- a/src/app/core-ui/work-context-menu/work-context-menu.component.ts +++ b/src/app/core-ui/work-context-menu/work-context-menu.component.ts @@ -28,10 +28,7 @@ import { Store } from '@ngrx/store'; import { TaskSharedActions } from '../../root-store/meta/task-shared.actions'; import { TaskWithSubTasks } from '../../features/tasks/task.model'; import { firstValueFrom } from 'rxjs'; -import { - DialogWorkContextSettingsComponent, - WorkContextSettingsDialogData, -} from '../../features/work-context/dialog-work-context-settings/dialog-work-context-settings.component'; +import type { WorkContextSettingsDialogData } from '../../features/work-context/dialog-work-context-settings/dialog-work-context-settings.component'; @Component({ selector: 'work-context-menu', @@ -238,6 +235,8 @@ export class WorkContextMenuComponent implements OnInit { this._tagService.getTagById$(this.contextId).pipe(first()), ); + const { DialogWorkContextSettingsComponent } = + await import('../../features/work-context/dialog-work-context-settings/dialog-work-context-settings.component'); this._matDialog.open(DialogWorkContextSettingsComponent, { restoreFocus: true, backdropClass: 'cdk-overlay-transparent-backdrop', diff --git a/src/app/core/locale.constants.ts b/src/app/core/locale.constants.ts index 35b2709211..ef2bdca3f6 100644 --- a/src/app/core/locale.constants.ts +++ b/src/app/core/locale.constants.ts @@ -1,29 +1,4 @@ -import localeEnUS from '@angular/common/locales/en'; import localeEnGB from '@angular/common/locales/en-GB'; -import localeEs from '@angular/common/locales/es'; -import localeDe from '@angular/common/locales/de'; -import localeAr from '@angular/common/locales/ar'; -import localeCs from '@angular/common/locales/cs'; -import localeFa from '@angular/common/locales/fa'; -import localeFr from '@angular/common/locales/fr'; -import localeJa from '@angular/common/locales/ja'; -import localeKo from '@angular/common/locales/ko'; -import localeRu from '@angular/common/locales/ru'; -import localeSk from '@angular/common/locales/sk'; -import localeTr from '@angular/common/locales/tr'; -import localeZh from '@angular/common/locales/zh'; -import localeIt from '@angular/common/locales/it'; -import localePl from '@angular/common/locales/pl'; -import localePt from '@angular/common/locales/pt'; -import localeNl from '@angular/common/locales/nl'; -import localeNb from '@angular/common/locales/nb'; -import localeHr from '@angular/common/locales/hr'; -import localeUk from '@angular/common/locales/uk'; -import localeId from '@angular/common/locales/id'; -import localeFi from '@angular/common/locales/fi'; -import localeSv from '@angular/common/locales/sv'; -import localeRo from '@angular/common/locales/ro'; -import localeRoMD from '@angular/common/locales/ro-MD'; /** * All of available app languages @@ -102,51 +77,62 @@ export const DateTimeLocales = { export type DateTimeLocale = (typeof DateTimeLocales)[keyof typeof DateTimeLocales]; -export const LocalesImports: Record = { - en: localeEnGB, - en_gb: localeEnGB, - en_us: localeEnUS, - tr_tr: localeTr, - de_de: localeDe, - de: localeDe, - fr_fr: localeFr, - es_es: localeEs, - es: localeEs, - it_it: localeIt, - pt_br: localePt, - ru_ru: localeRu, - ru: localeRu, - zh_cn: localeZh, - ja_jp: localeJa, - ja: localeJa, - ko_kr: localeKo, - ko: localeKo, - zh_tw: localeZh, - ar: localeAr, - cs_cz: localeCs, - cs: localeCs, - fa: localeFa, - fi: localeFi, - fr: localeFr, - id: localeId, - it: localeIt, - pl: localePl, - pt: localePt, - nl: localeNl, - nb: localeNb, - hr: localeHr, - uk_ua: localeUk, - uk: localeUk, - sk_sk: localeSk, - sk: localeSk, - sv: localeSv, - tr: localeTr, - zh: localeZh, - ro: localeRo, - ro_ro: localeRo, - ro_md: localeRoMD, +/** + * Maps locale keys to dynamic import functions for lazy loading. + * Only the default locale (en-GB) is statically imported; all others + * are loaded on demand to reduce the initial bundle size. + */ +export const LocaleImportFns: Record< + keyof typeof DateTimeLocales, + () => Promise<{ default: unknown }> +> = { + en: () => Promise.resolve({ default: localeEnGB }), + en_gb: () => Promise.resolve({ default: localeEnGB }), + en_us: () => import('@angular/common/locales/en'), + tr_tr: () => import('@angular/common/locales/tr'), + de_de: () => import('@angular/common/locales/de'), + de: () => import('@angular/common/locales/de'), + fr_fr: () => import('@angular/common/locales/fr'), + es_es: () => import('@angular/common/locales/es'), + es: () => import('@angular/common/locales/es'), + it_it: () => import('@angular/common/locales/it'), + pt_br: () => import('@angular/common/locales/pt'), + ru_ru: () => import('@angular/common/locales/ru'), + ru: () => import('@angular/common/locales/ru'), + zh_cn: () => import('@angular/common/locales/zh'), + ja_jp: () => import('@angular/common/locales/ja'), + ja: () => import('@angular/common/locales/ja'), + ko_kr: () => import('@angular/common/locales/ko'), + ko: () => import('@angular/common/locales/ko'), + zh_tw: () => import('@angular/common/locales/zh'), + ar: () => import('@angular/common/locales/ar'), + cs_cz: () => import('@angular/common/locales/cs'), + cs: () => import('@angular/common/locales/cs'), + fa: () => import('@angular/common/locales/fa'), + fi: () => import('@angular/common/locales/fi'), + fr: () => import('@angular/common/locales/fr'), + id: () => import('@angular/common/locales/id'), + it: () => import('@angular/common/locales/it'), + pl: () => import('@angular/common/locales/pl'), + pt: () => import('@angular/common/locales/pt'), + nl: () => import('@angular/common/locales/nl'), + nb: () => import('@angular/common/locales/nb'), + hr: () => import('@angular/common/locales/hr'), + uk_ua: () => import('@angular/common/locales/uk'), + uk: () => import('@angular/common/locales/uk'), + sk_sk: () => import('@angular/common/locales/sk'), + sk: () => import('@angular/common/locales/sk'), + sv: () => import('@angular/common/locales/sv'), + tr: () => import('@angular/common/locales/tr'), + zh: () => import('@angular/common/locales/zh'), + ro: () => import('@angular/common/locales/ro'), + ro_ro: () => import('@angular/common/locales/ro'), + ro_md: () => import('@angular/common/locales/ro-MD'), }; +/** Default locale data, statically imported for instant availability */ +export const DEFAULT_LOCALE_DATA = localeEnGB; + export const DEFAULT_LANGUAGE = LanguageCode.en; export const DEFAULT_LOCALE = DateTimeLocales.en_gb; export const DEFAULT_FIRST_DAY_OF_WEEK = 1; // monday diff --git a/src/app/features/boards/board-edit/board-edit.component.ts b/src/app/features/boards/board-edit/board-edit.component.ts index 298d70dca0..c0813659bc 100644 --- a/src/app/features/boards/board-edit/board-edit.component.ts +++ b/src/app/features/boards/board-edit/board-edit.component.ts @@ -8,7 +8,7 @@ import { output, signal, } from '@angular/core'; -import { FormlyModule } from '@ngx-formly/core'; +import { FormlyConfigModule } from '../../../ui/formly-config.module'; import { BoardCfg } from '../boards.model'; import { BOARDS_FORM } from '../boards-form.const'; import { UntypedFormGroup } from '@angular/forms'; @@ -24,7 +24,7 @@ import { DEFAULT_BOARD_CFG, DEFAULT_PANEL_CFG } from '../boards.const'; @Component({ selector: 'board-edit', standalone: true, - imports: [FormlyModule, MatButton, MatIcon, TranslatePipe], + imports: [FormlyConfigModule, MatButton, MatIcon, TranslatePipe], templateUrl: './board-edit.component.html', styleUrl: './board-edit.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, diff --git a/src/app/features/config/config-form/config-form.component.ts b/src/app/features/config/config-form/config-form.component.ts index 8ed939a87f..cf4fe430aa 100644 --- a/src/app/features/config/config-form/config-form.component.ts +++ b/src/app/features/config/config-form/config-form.component.ts @@ -5,7 +5,8 @@ import { input, output, } from '@angular/core'; -import { FormlyFieldConfig, FormlyFormOptions, FormlyModule } from '@ngx-formly/core'; +import { FormlyFieldConfig, FormlyFormOptions } from '@ngx-formly/core'; +import { FormlyConfigModule } from '../../../ui/formly-config.module'; import { FormsModule, ReactiveFormsModule, UntypedFormGroup } from '@angular/forms'; import { GlobalConfigSectionKey } from '../global-config.model'; import { ProjectCfgFormKey } from '../../project/project.model'; @@ -19,7 +20,7 @@ import { Log } from '../../../core/log'; templateUrl: './config-form.component.html', styleUrls: ['./config-form.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush, - imports: [FormsModule, ReactiveFormsModule, FormlyModule], + imports: [FormsModule, ReactiveFormsModule, FormlyConfigModule], }) export class ConfigFormComponent { T: typeof T = T; diff --git a/src/app/features/focus-mode/dialog-pomodoro-settings/dialog-pomodoro-settings.component.ts b/src/app/features/focus-mode/dialog-pomodoro-settings/dialog-pomodoro-settings.component.ts index 8104fd0b24..24d2fd8ae3 100644 --- a/src/app/features/focus-mode/dialog-pomodoro-settings/dialog-pomodoro-settings.component.ts +++ b/src/app/features/focus-mode/dialog-pomodoro-settings/dialog-pomodoro-settings.component.ts @@ -1,7 +1,8 @@ import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; import { FormGroup, ReactiveFormsModule } from '@angular/forms'; import { MatDialogRef, MatDialogTitle, MatDialogContent } from '@angular/material/dialog'; -import { FormlyFieldConfig, FormlyModule } from '@ngx-formly/core'; +import { FormlyFieldConfig } from '@ngx-formly/core'; +import { FormlyConfigModule } from '../../../ui/formly-config.module'; import { GlobalConfigService } from '../../config/global-config.service'; import { T } from '../../../t.const'; import { PomodoroConfig } from '../../config/global-config.model'; @@ -52,7 +53,7 @@ const POMODORO_DURATION_FIELDS: FormlyFieldConfig[] = [ standalone: true, imports: [ ReactiveFormsModule, - FormlyModule, + FormlyConfigModule, MatDialogTitle, MatDialogContent, TranslatePipe, diff --git a/src/app/features/issue/dialog-edit-issue-provider/dialog-edit-issue-provider.component.ts b/src/app/features/issue/dialog-edit-issue-provider/dialog-edit-issue-provider.component.ts index 2c46abfdde..336487a34c 100644 --- a/src/app/features/issue/dialog-edit-issue-provider/dialog-edit-issue-provider.component.ts +++ b/src/app/features/issue/dialog-edit-issue-provider/dialog-edit-issue-provider.component.ts @@ -43,7 +43,7 @@ import { JiraAdditionalCfgComponent } from '../providers/jira/jira-view-componen import { HelpSectionComponent } from '../../../ui/help-section/help-section.component'; import { TranslatePipe } from '@ngx-translate/core'; import { MatSlideToggle } from '@angular/material/slide-toggle'; -import { FormlyModule } from '@ngx-formly/core'; +import { FormlyConfigModule } from '../../../ui/formly-config.module'; import { MatButton } from '@angular/material/button'; import { MatIcon } from '@angular/material/icon'; import { IS_ANDROID_WEB_VIEW } from '../../../util/is-android-web-view'; @@ -72,7 +72,7 @@ import { ISSUE_PROVIDER_COMMON_FORM_FIELDS } from '../common-issue-form-stuff.co HelpSectionComponent, TranslatePipe, MatSlideToggle, - FormlyModule, + FormlyConfigModule, MatDialogActions, MatButton, MatIcon, diff --git a/src/app/features/project/dialogs/create-project/dialog-create-project.component.ts b/src/app/features/project/dialogs/create-project/dialog-create-project.component.ts index 0d74b3b5c1..3a7c2aee89 100644 --- a/src/app/features/project/dialogs/create-project/dialog-create-project.component.ts +++ b/src/app/features/project/dialogs/create-project/dialog-create-project.component.ts @@ -14,7 +14,8 @@ import { } from '@angular/material/dialog'; import { Project, ProjectCopy } from '../../project.model'; import { FormsModule, ReactiveFormsModule, UntypedFormGroup } from '@angular/forms'; -import { FormlyFieldConfig, FormlyFormOptions, FormlyModule } from '@ngx-formly/core'; +import { FormlyFieldConfig, FormlyFormOptions } from '@ngx-formly/core'; +import { FormlyConfigModule } from '../../../../ui/formly-config.module'; import { ProjectService } from '../../project.service'; import { DEFAULT_PROJECT } from '../../project.const'; import { JiraCfg } from '../../../issue/providers/jira/jira.model'; @@ -48,7 +49,7 @@ import { Log } from '../../../../core/log'; FormsModule, ReactiveFormsModule, MatDialogContent, - FormlyModule, + FormlyConfigModule, MatDialogActions, MatButton, TranslatePipe, diff --git a/src/app/features/right-panel/right-panel-content.component.html b/src/app/features/right-panel/right-panel-content.component.html index 330d2e4afb..5951db5a87 100644 --- a/src/app/features/right-panel/right-panel-content.component.html +++ b/src/app/features/right-panel/right-panel-content.component.html @@ -1,20 +1,30 @@ @if (panelContent() === 'ISSUE_PANEL') { - + @defer (prefetch on idle) { + + } } @else if (panelContent() === 'NOTES') { - + @defer (prefetch on idle) { + + } } @else if (panelContent() === 'PLUGIN') { @for (key of pluginPanelKeys(); track key) { - + @defer (prefetch on idle) { + + } } } @else if (panelContent() === 'SCHEDULE_DAY_PANEL') { - + @defer (prefetch on idle) { + + } } @else { @if (selectedTaskWithDelayForNone(); as task) { - + @defer (prefetch on idle) { + + } } } diff --git a/src/app/features/right-panel/right-panel.component.ts b/src/app/features/right-panel/right-panel.component.ts index 06233b49fb..0fb892c153 100644 --- a/src/app/features/right-panel/right-panel.component.ts +++ b/src/app/features/right-panel/right-panel.component.ts @@ -37,7 +37,6 @@ import { import { isInputElement } from '../../util/dom-element'; import { BottomPanelStateService } from '../../core-ui/bottom-panel-state.service'; import { slideRightPanelAni } from './slide-right-panel-out.ani'; -import { BottomPanelContainerComponent } from '../bottom-panel/bottom-panel-container.component'; import { MatBottomSheet, MatBottomSheetRef } from '@angular/material/bottom-sheet'; import { PanelContentService } from '../panels/panel-content.service'; // Right panel resize constants @@ -155,7 +154,7 @@ export class RightPanelComponent implements AfterViewInit, OnDestroy { private readonly _boundOnPointerUp = this._handlePointerUp.bind(this); private readonly _boundOnWindowResize = this._throttledWindowResize.bind(this); private _bottomSheet = inject(MatBottomSheet); - private _bottomSheetRef: MatBottomSheetRef | null = null; + private _bottomSheetRef: MatBottomSheetRef | null = null; private _bottomSheetSubscription: Subscription | null = null; // Track listener state to prevent double attachment/removal @@ -218,21 +217,29 @@ export class RightPanelComponent implements AfterViewInit, OnDestroy { if (hasContent && !this._bottomSheetRef) { // Open bottom sheet - this._bottomSheetRef = this._bottomSheet.open(BottomPanelContainerComponent, { - hasBackdrop: true, - closeOnNavigation: false, - panelClass: 'bottom-panel-sheet', - // Let CSS handle positioning and height - }); + import('../bottom-panel/bottom-panel-container.component').then((m) => { + // Re-check: conditions may have changed while chunk was loading + if (this._bottomSheetRef) { + return; + } + this._bottomSheetRef = this._bottomSheet.open( + m.BottomPanelContainerComponent, + { + hasBackdrop: true, + closeOnNavigation: false, + panelClass: 'bottom-panel-sheet', + }, + ); - // Handle bottom sheet dismissal - this._bottomSheetSubscription = this._bottomSheetRef - .afterDismissed() - .subscribe(() => { - this._bottomSheetRef = null; - this._bottomSheetSubscription = null; - this.close(); - }); + // Handle bottom sheet dismissal + this._bottomSheetSubscription = this._bottomSheetRef + .afterDismissed() + .subscribe(() => { + this._bottomSheetRef = null; + this._bottomSheetSubscription = null; + this.close(); + }); + }); } else if (!hasContent && this._bottomSheetRef) { // Close bottom sheet this._bottomSheetRef.dismiss(); diff --git a/src/app/features/simple-counter/dialog-simple-counter-edit-settings/dialog-simple-counter-edit-settings.component.ts b/src/app/features/simple-counter/dialog-simple-counter-edit-settings/dialog-simple-counter-edit-settings.component.ts index 1c68c9347d..ce6a833377 100644 --- a/src/app/features/simple-counter/dialog-simple-counter-edit-settings/dialog-simple-counter-edit-settings.component.ts +++ b/src/app/features/simple-counter/dialog-simple-counter-edit-settings/dialog-simple-counter-edit-settings.component.ts @@ -14,7 +14,8 @@ import { SimpleCounterType, } from '../simple-counter.model'; import { T } from '../../../t.const'; -import { FormlyFieldConfig, FormlyFormOptions, FormlyModule } from '@ngx-formly/core'; +import { FormlyFieldConfig, FormlyFormOptions } from '@ngx-formly/core'; +import { FormlyConfigModule } from '../../../ui/formly-config.module'; import { FormsModule, ReactiveFormsModule, UntypedFormGroup } from '@angular/forms'; import { adjustToDialogFormlyForm } from '../../../util/adjust-to-dialog-formly-form'; import { SIMPLE_COUNTER_FORM } from '../../config/form-cfgs/simple-counter-form.const'; @@ -38,7 +39,7 @@ import { DialogConfirmComponent } from '../../../ui/dialog-confirm/dialog-confir TranslatePipe, ReactiveFormsModule, FormsModule, - FormlyModule, + FormlyConfigModule, ], }) export class DialogSimpleCounterEditSettingsComponent { diff --git a/src/app/features/task-repeat-cfg/dialog-edit-task-repeat-cfg/dialog-edit-task-repeat-cfg.component.ts b/src/app/features/task-repeat-cfg/dialog-edit-task-repeat-cfg/dialog-edit-task-repeat-cfg.component.ts index a53f82acd8..a39e8bc5ba 100644 --- a/src/app/features/task-repeat-cfg/dialog-edit-task-repeat-cfg/dialog-edit-task-repeat-cfg.component.ts +++ b/src/app/features/task-repeat-cfg/dialog-edit-task-repeat-cfg/dialog-edit-task-repeat-cfg.component.ts @@ -21,7 +21,8 @@ import { TaskRepeatCfg, TaskRepeatCfgCopy, } from '../task-repeat-cfg.model'; -import { FormlyFieldConfig, FormlyModule } from '@ngx-formly/core'; +import { FormlyFieldConfig } from '@ngx-formly/core'; +import { FormlyConfigModule } from '../../../ui/formly-config.module'; import { UntypedFormGroup } from '@angular/forms'; import { TASK_REPEAT_CFG_ADVANCED_FORM_CFG, @@ -63,7 +64,7 @@ import { CollapsibleComponent } from '../../../ui/collapsible/collapsible.compon TranslatePipe, MatDialogContent, HelpSectionComponent, - FormlyModule, + FormlyConfigModule, ChipListInputComponent, MatDialogActions, MatButton, diff --git a/src/app/features/tasks/add-task-bar/add-task-bar.component.ts b/src/app/features/tasks/add-task-bar/add-task-bar.component.ts index 2c465f9a4d..ff041d846f 100644 --- a/src/app/features/tasks/add-task-bar/add-task-bar.component.ts +++ b/src/app/features/tasks/add-task-bar/add-task-bar.component.ts @@ -70,7 +70,6 @@ import { CHRONO_SUGGESTIONS } from './add-task-bar.const'; import { TaskRepeatCfgService } from '../../task-repeat-cfg/task-repeat-cfg.service'; import { DEFAULT_TASK_REPEAT_CFG } from '../../task-repeat-cfg/task-repeat-cfg.model'; import { getQuickSettingUpdates } from '../../task-repeat-cfg/dialog-edit-task-repeat-cfg/get-quick-setting-updates'; -import { DialogEditTaskRepeatCfgComponent } from '../../task-repeat-cfg/dialog-edit-task-repeat-cfg/dialog-edit-task-repeat-cfg.component'; import { TranslateModule, TranslateService } from '@ngx-translate/core'; import { ShortSyntaxTag, shortSyntaxToTags } from './short-syntax-to-tags'; import { DEFAULT_PROJECT_COLOR } from '../../work-context/work-context.const'; @@ -791,7 +790,9 @@ export class AddTaskBarComponent implements AfterViewInit, OnInit, OnDestroy { .getByIdOnce$(taskId) .pipe(timeout(1000), takeUntilDestroyed(this._destroyRef)) .subscribe({ - next: (task) => { + next: async (task) => { + const { DialogEditTaskRepeatCfgComponent } = + await import('../../task-repeat-cfg/dialog-edit-task-repeat-cfg/dialog-edit-task-repeat-cfg.component'); this._matDialog.open(DialogEditTaskRepeatCfgComponent, { data: { task, defaultRemindOption: remindOption }, }); diff --git a/src/app/features/tasks/task/task.component.ts b/src/app/features/tasks/task/task.component.ts index 098c0cab7a..da7b300b70 100644 --- a/src/app/features/tasks/task/task.component.ts +++ b/src/app/features/tasks/task/task.component.ts @@ -35,7 +35,6 @@ import { PanDirective, PanEvent } from '../../../ui/swipe-gesture/pan.directive' import { TaskAttachmentService } from '../task-attachment/task-attachment.service'; import { DialogEditTaskAttachmentComponent } from '../task-attachment/dialog-edit-attachment/dialog-edit-task-attachment.component'; import { swirlAnimation } from '../../../ui/animations/swirl-in-out.ani'; -import { DialogEditTaskRepeatCfgComponent } from '../../task-repeat-cfg/dialog-edit-task-repeat-cfg/dialog-edit-task-repeat-cfg.component'; import { ProjectService } from '../../project/project.service'; import { Project } from '../../project/project.model'; import { _MISSING_PROJECT_ } from '../../project/project.const'; @@ -389,7 +388,9 @@ export class TaskComponent implements OnDestroy, AfterViewInit { }); } - editTaskRepeatCfg(): void { + async editTaskRepeatCfg(): Promise { + const { DialogEditTaskRepeatCfgComponent } = + await import('../../task-repeat-cfg/dialog-edit-task-repeat-cfg/dialog-edit-task-repeat-cfg.component'); this._matDialog .open(DialogEditTaskRepeatCfgComponent, { data: { diff --git a/src/app/features/work-context/dialog-work-context-settings/dialog-work-context-settings.component.ts b/src/app/features/work-context/dialog-work-context-settings/dialog-work-context-settings.component.ts index 362474a4ac..ed5cb32544 100644 --- a/src/app/features/work-context/dialog-work-context-settings/dialog-work-context-settings.component.ts +++ b/src/app/features/work-context/dialog-work-context-settings/dialog-work-context-settings.component.ts @@ -7,7 +7,8 @@ import { MatDialogTitle, } from '@angular/material/dialog'; import { ReactiveFormsModule, UntypedFormGroup } from '@angular/forms'; -import { FormlyFieldConfig, FormlyModule } from '@ngx-formly/core'; +import { FormlyFieldConfig } from '@ngx-formly/core'; +import { FormlyConfigModule } from '../../../ui/formly-config.module'; import { T } from '../../../t.const'; import { Project } from '../../project/project.model'; import { Tag } from '../../tag/tag.model'; @@ -34,7 +35,7 @@ export interface WorkContextSettingsDialogData { MatDialogTitle, ReactiveFormsModule, MatDialogContent, - FormlyModule, + FormlyConfigModule, MatDialogActions, MatButton, TranslatePipe, diff --git a/src/app/imex/sync/dialog-sync-initial-cfg/dialog-sync-initial-cfg.component.ts b/src/app/imex/sync/dialog-sync-initial-cfg/dialog-sync-initial-cfg.component.ts index e08f6d1ba0..e7b127e4f4 100644 --- a/src/app/imex/sync/dialog-sync-initial-cfg/dialog-sync-initial-cfg.component.ts +++ b/src/app/imex/sync/dialog-sync-initial-cfg/dialog-sync-initial-cfg.component.ts @@ -18,7 +18,6 @@ import { T } from '../../../t.const'; import { SYNC_FORM } from '../../../features/config/form-cfgs/sync-form.const'; import { FormGroup } from '@angular/forms'; import { FormlyConfigModule } from '../../../ui/formly-config.module'; -import { FormlyModule } from '@ngx-formly/core'; import { FormlyFieldConfig } from '@ngx-formly/core'; import { SyncConfig } from '../../../features/config/global-config.model'; import { SyncProviderId } from '../../../op-log/sync-providers/provider.const'; @@ -46,7 +45,6 @@ import { isOnline } from '../../../util/is-online'; MatIcon, TranslatePipe, FormlyConfigModule, - FormlyModule, ], }) export class DialogSyncInitialCfgComponent implements AfterViewInit { diff --git a/src/app/imex/sync/sync-wrapper.service.ts b/src/app/imex/sync/sync-wrapper.service.ts index d2baf38c68..5f8746410a 100644 --- a/src/app/imex/sync/sync-wrapper.service.ts +++ b/src/app/imex/sync/sync-wrapper.service.ts @@ -51,7 +51,6 @@ import { DialogConflictResolutionResult } from './sync.model'; import { DialogSyncConflictComponent } from './dialog-sync-conflict/dialog-sync-conflict.component'; import { ReminderService } from '../../features/reminder/reminder.service'; -import { DialogSyncInitialCfgComponent } from './dialog-sync-initial-cfg/dialog-sync-initial-cfg.component'; import { DialogHandleDecryptErrorComponent } from './dialog-handle-decrypt-error/dialog-handle-decrypt-error.component'; import { DialogEnterEncryptionPasswordComponent } from './dialog-enter-encryption-password/dialog-enter-encryption-password.component'; import { @@ -449,14 +448,14 @@ export class SyncWrapperService { msg: T.F.SYNC.S.AUTH_TOKEN_REJECTED, translateParams: { reason: error.message }, type: 'ERROR', - actionFn: async () => this._matDialog.open(DialogSyncInitialCfgComponent), + actionFn: () => this._openSyncInitialCfgDialog(), actionStr: T.F.SYNC.S.BTN_CONFIGURE, }); } else { this._snackService.open({ msg: T.F.SYNC.S.INCOMPLETE_CFG, type: 'ERROR', - actionFn: async () => this._matDialog.open(DialogSyncInitialCfgComponent), + actionFn: () => this._openSyncInitialCfgDialog(), actionStr: T.F.SYNC.S.BTN_CONFIGURE, }); } @@ -693,6 +692,12 @@ export class SyncWrapperService { * Handle incoherent timestamps dialog with proper async error handling. * Uses fire-and-forget pattern but logs errors instead of swallowing them. */ + private async _openSyncInitialCfgDialog(): Promise { + const { DialogSyncInitialCfgComponent } = + await import('./dialog-sync-initial-cfg/dialog-sync-initial-cfg.component'); + this._matDialog.open(DialogSyncInitialCfgComponent); + } + private _handleIncoherentTimestampsDialog(): void { this._openSyncErrorDialog({ type: 'incoherent-timestamps' }); } diff --git a/src/app/plugins/ui/plugin-config-dialog/plugin-config-dialog.component.ts b/src/app/plugins/ui/plugin-config-dialog/plugin-config-dialog.component.ts index 5549d48965..d129c4c81c 100644 --- a/src/app/plugins/ui/plugin-config-dialog/plugin-config-dialog.component.ts +++ b/src/app/plugins/ui/plugin-config-dialog/plugin-config-dialog.component.ts @@ -9,7 +9,8 @@ import { import { MatButton } from '@angular/material/button'; import { MatIcon } from '@angular/material/icon'; import { FormGroup, ReactiveFormsModule } from '@angular/forms'; -import { FormlyFieldConfig, FormlyModule } from '@ngx-formly/core'; +import { FormlyFieldConfig } from '@ngx-formly/core'; +import { FormlyConfigModule } from '../../../ui/formly-config.module'; import { FormlyJsonschema } from '@ngx-formly/core/json-schema'; import { JSONSchema7 } from 'json-schema'; import { PluginManifest } from '../../plugin-api.model'; @@ -123,7 +124,7 @@ interface PluginConfigData { MatProgressSpinner, MatError, ReactiveFormsModule, - FormlyModule, + FormlyConfigModule, ], }) export class PluginConfigDialogComponent { diff --git a/src/main.ts b/src/main.ts index 2cba12fe1c..110bff0c9c 100644 --- a/src/main.ts +++ b/src/main.ts @@ -11,7 +11,11 @@ import { registerLocaleData } from '@angular/common'; import { environment } from './environments/environment'; import { IS_ELECTRON } from './app/app.constants'; -import { DEFAULT_LANGUAGE, LocalesImports } from './app/core/locale.constants'; +import { + DEFAULT_LANGUAGE, + DEFAULT_LOCALE_DATA, + LocaleImportFns, +} from './app/core/locale.constants'; import { IS_ANDROID_WEB_VIEW } from './app/util/is-android-web-view'; import { androidInterface } from './app/features/android/android-interface'; import { IS_IOS_NATIVE, IS_NATIVE_PLATFORM } from './app/util/is-native-platform'; @@ -30,7 +34,6 @@ import { MatDateFormats, DateAdapter, } from '@angular/material/core'; -import { FormlyConfigModule } from './app/ui/formly-config.module'; import { markedOptionsFactory } from './app/ui/marked-options-factory'; import { MaterialCssVarsModule } from 'angular-material-css-vars'; import { MatSidenavModule } from '@angular/material/sidenav'; @@ -96,7 +99,6 @@ bootstrapApplication(AppComponent, { importProvidersFrom( FeatureStoresModule, MatNativeDateModule, - FormlyConfigModule, MarkdownModule.forRoot({ markedOptions: { provide: MARKED_OPTIONS, @@ -251,27 +253,24 @@ bootstrapApplication(AppComponent, { // Initialize touch fix for Material menus initializeMatMenuTouchFix(); - // Register default locale immediately for fast startup - registerLocaleData(LocalesImports[DEFAULT_LANGUAGE], DEFAULT_LANGUAGE); + // Register default locale immediately (statically imported, no network fetch) + registerLocaleData(DEFAULT_LOCALE_DATA, DEFAULT_LANGUAGE); - // Defer other locales to idle time for better initial load performance - if (typeof requestIdleCallback === 'function') { - requestIdleCallback(() => { - Object.keys(LocalesImports).forEach((locale) => { - if (locale !== DEFAULT_LANGUAGE) { - registerLocaleData(LocalesImports[locale], locale); - } - }); + // Lazily load and register remaining locales during idle time + const registerRemainingLocales = (): void => { + Object.keys(LocaleImportFns).forEach((locale) => { + if (locale !== DEFAULT_LANGUAGE) { + LocaleImportFns[locale as keyof typeof LocaleImportFns]().then((m) => { + registerLocaleData(m.default, locale); + }); + } }); + }; + + if (typeof requestIdleCallback === 'function') { + requestIdleCallback(() => registerRemainingLocales()); } else { - // Fallback for browsers without requestIdleCallback - setTimeout(() => { - Object.keys(LocalesImports).forEach((locale) => { - if (locale !== DEFAULT_LANGUAGE) { - registerLocaleData(LocalesImports[locale], locale); - } - }); - }, 0); + setTimeout(() => registerRemainingLocales(), 0); } // TODO make asset caching work for electron diff --git a/src/polyfills.ts b/src/polyfills.ts index 3960606ea7..ceb65ac7ad 100644 --- a/src/polyfills.ts +++ b/src/polyfills.ts @@ -20,7 +20,8 @@ /* IE9, IE10 and IE11 requires all of the following polyfills. **/ //import 'core-js/es/symbol'; -import 'core-js/es/object'; // Support for Chrome mobile <= 69, which is the default browser for Android <= 9, especially the following error: TypeError: Object.fromEntries is not a function +// core-js/es/object removed: Object.fromEntries is supported since Chrome 73+ (2019). +// Android 9 (Chrome 69) is EOL and no longer targeted. /*import 'core-js/es/function'; import 'core-js/es/parse-int'; import 'core-js/es/parse-float';