feat(deps): upgrade Angular to v21

- Update @angular/* packages to v21.0.8
- Update @angular/material and @angular/cdk to v21.0.6
- Update @angular-eslint/* to v21.1.0
- Update @ngrx/* to v21.0.1
- Update TypeScript to 5.9.3
- Update ngx-markdown to v21 and marked to v17
- Update typia to v11 for TypeScript 5.9 support
- Update @types/node to v22, chart.js to v4.5.1

Breaking changes addressed:
- Replace deep imports with public API imports (idb, formly, ngrx)
- Update marked-options-factory for marked v17 API changes
- Add custom FormlySliderComponent (formly slider incompatible with Mat v21)
- Update ngx-markdown SANITIZE configuration
- Fix HostListener decorators with unused $event args
- Fix crypto.subtle type compatibility
- Add skipLibCheck for dependency type conflicts
- Update tsconfig module settings for Angular 21

Removed:
- @angular-builders/custom-webpack (unused)
This commit is contained in:
Johannes Millan 2026-01-10 15:27:20 +01:00
parent 3f86044147
commit 132947a69b
20 changed files with 257 additions and 1527 deletions

1636
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -140,7 +140,6 @@
"node-fetch": "^2.7.0"
},
"devDependencies": {
"@angular-builders/custom-webpack": "^20.0.0",
"@angular-devkit/build-angular": "^21.0.5",
"@angular-eslint/builder": "^21.1.0",
"@angular-eslint/eslint-plugin": "^21.1.0",
@ -181,18 +180,18 @@
"@ngrx/schematics": "^21.0.1",
"@ngrx/store": "21.0.1",
"@ngrx/store-devtools": "^21.0.1",
"@ngx-formly/core": "7.0.0",
"@ngx-formly/material": "7.0.0",
"@ngx-formly/core": "^7.0.1",
"@ngx-formly/material": "^7.0.1",
"@ngx-translate/core": "^17.0.0",
"@ngx-translate/http-loader": "^17.0.0",
"@playwright/test": "^1.56.1",
"@schematics/angular": "^20.1.4",
"@schematics/angular": "^21.0.0",
"@types/electron": "^1.4.38",
"@types/electron-localshortcut": "^3.1.3",
"@types/file-saver": "^2.0.5",
"@types/jasmine": "^3.10.2",
"@types/jasminewd2": "~2.0.13",
"@types/node": "20.12.4",
"@types/node": "^22.19.5",
"@types/node-fetch": "^2.6.6",
"@types/object-path": "^0.11.4",
"@typescript-eslint/eslint-plugin": "^7.18.0",
@ -203,7 +202,7 @@
"baseline-browser-mapping": "^2.9.11",
"canvas-confetti": "^1.9.4",
"chai": "^5.1.2",
"chart.js": "^4.4.7",
"chart.js": "^4.5.1",
"chrono-node": "^2.8.3",
"clipboard": "^2.0.11",
"conventional-changelog-cli": "^5.0.0",
@ -235,11 +234,11 @@
"karma-jasmine": "~5.1.0",
"karma-jasmine-html-reporter": "^1.6.0",
"karma-spec-reporter": "^0.0.36",
"marked": "^12.0.2",
"marked": "^17.0.0",
"nanoid": "^5.1.6",
"new-github-issue-url": "^1.1.0",
"ng2-charts": "^8.0.0",
"ngx-markdown": "^20.0.0",
"ngx-markdown": "^21.0.0",
"playwright": "^1.56.1",
"prettier": "^3.5.1",
"pretty-quick": "^4.1.1",
@ -258,9 +257,6 @@
"typia": "^11.0.0"
},
"overrides": {
"ngx-markdown": {
"marked": "12.0.2"
},
"@conventional-changelog/git-client": "^2.5.1"
},
"optionalDependencies": {

View file

@ -1,6 +1,5 @@
import { Injectable, signal } from '@angular/core';
import { IDBPDatabase } from 'idb/build';
import { DBSchema, openDB } from 'idb';
import { DBSchema, IDBPDatabase, openDB } from 'idb';
import { DBAdapter } from './db-adapter.model';
import { Log } from '../log';

View file

@ -1,6 +1,6 @@
import { AppBaseData } from '../../imex/sync/sync.model';
import { Action } from '@ngrx/store';
import { ActionReducer } from '@ngrx/store/src/models';
import { ActionReducer } from '@ngrx/store';
export interface PersistenceLegacyBaseModel<T> {
appDataKey: keyof AppBaseData;

View file

@ -9,7 +9,7 @@ import {
import { nanoid } from 'nanoid';
import { T } from '../../t.const';
import { DEFAULT_PANEL_CFG } from './boards.const';
import { FormlyFieldConfig } from '@ngx-formly/core/lib/models/fieldconfig';
import { FormlyFieldConfig } from '@ngx-formly/core';
const getNewPanel = (): BoardPanelCfg => ({
...DEFAULT_PANEL_CFG,

View file

@ -7,7 +7,7 @@ import {
import { T } from '../../../t.const';
import { EMPTY_SIMPLE_COUNTER } from '../../simple-counter/simple-counter.const';
import { nanoid } from 'nanoid';
import { FormlyFieldConfig } from '@ngx-formly/core/lib/models/fieldconfig';
import { FormlyFieldConfig } from '@ngx-formly/core';
export const SIMPLE_COUNTER_FORM: ConfigFormSection<SimpleCounterConfig> = {
title: T.F.SIMPLE_COUNTER.FORM.TITLE,

View file

@ -5,7 +5,7 @@ import { TaskCopy } from '../../tasks/task.model';
import { IssueProviderActions } from './issue-provider.actions';
import { first, tap } from 'rxjs/operators';
import { Observable } from 'rxjs';
import { Update } from '@ngrx/entity/src/models';
import { Update } from '@ngrx/entity';
import { Store } from '@ngrx/store';
import { __updateMultipleTaskSimple } from '../../tasks/store/task.actions';
import { TaskArchiveService } from '../../time-tracking/task-archive.service';

View file

@ -31,7 +31,7 @@ export class PlannerCalendarEventComponent {
return this.isBeingSubmitted;
}
@HostListener('click', ['$event'])
@HostListener('click')
async onClick(): Promise<void> {
if (this.isBeingSubmitted) {
return;

View file

@ -83,7 +83,7 @@ export class PlannerTaskComponent extends BaseComponent implements OnInit, OnDes
return this.task.id === this._taskService.currentTaskId();
}
@HostListener('click', ['$event'])
@HostListener('click')
async clickHandler(): Promise<void> {
if (this.task) {
// Use bottom panel on mobile, dialog on desktop

View file

@ -15,7 +15,7 @@ import { DEFAULT_SIMPLE_COUNTERS } from '../simple-counter.const';
import { arrayToDictionary } from '../../../util/array-to-dictionary';
import { loadAllData } from '../../../root-store/meta/load-all-data.action';
import { updateAllInDictionary } from '../../../util/update-all-in-dictionary';
import { Update } from '@ngrx/entity/src/models';
import { Update } from '@ngrx/entity';
import {
addSimpleCounter,
decreaseSimpleCounterCounterToday,

View file

@ -2,7 +2,7 @@ import { WorkContextCommon, WorkContextThemeCfg } from './work-context.model';
import { WorklogExportSettings, WorklogGrouping } from '../worklog/worklog.model';
import { ConfigFormSection } from '../config/global-config.model';
import { T } from '../../t.const';
import { FormlyFieldConfig } from '@ngx-formly/core/lib/models/fieldconfig';
import { FormlyFieldConfig } from '@ngx-formly/core';
export const WORKLOG_EXPORT_DEFAULTS: WorklogExportSettings = {
cols: ['DATE', 'START', 'END', 'TIME_CLOCK', 'TITLES_INCLUDING_SUB'],

View file

@ -1,5 +1,4 @@
import { IDBPDatabase } from 'idb/build';
import { DBSchema, openDB } from 'idb';
import { DBSchema, IDBPDatabase, openDB } from 'idb';
import { DatabaseAdapter } from './database-adapter.model';
import { MiniObservable } from '../util/mini-observable';
import { PFLog } from '../../../core/log';

View file

@ -87,10 +87,13 @@ const _deriveKeyArgon = async (
outputType: 'binary',
});
return window.crypto.subtle.importKey('raw', derivedBytes, { name: ALGORITHM }, false, [
'encrypt',
'decrypt',
]);
return window.crypto.subtle.importKey(
'raw',
derivedBytes.buffer as ArrayBuffer,
{ name: ALGORITHM },
false,
['encrypt', 'decrypt'],
);
};
const decryptArgon = async (data: string, password: string): Promise<string> => {

View file

@ -11,7 +11,7 @@ import { TASK_FEATURE_NAME } from '../../features/tasks/store/task.reducer';
import { TAG_FEATURE_NAME, tagAdapter } from '../../features/tag/store/tag.reducer';
import { taskAdapter } from '../../features/tasks/store/task.adapter';
import { Project } from '../../features/project/project.model';
import { Action, ActionReducer } from '@ngrx/store/src/models';
import { Action, ActionReducer } from '@ngrx/store';
import { TODAY_TAG } from '../../features/tag/tag.const';
import { Log } from '../../core/log';

View file

@ -29,7 +29,7 @@ export class EnlargeImgDirective {
this.imageEl = this._el.nativeElement;
}
@HostListener('click', ['$event']) onClick(): void {
@HostListener('click') onClick(): void {
this.isImg = this.imageEl.tagName.toLowerCase() === 'img';
if (this.isImg || this.enlargeImg()) {

View file

@ -18,7 +18,7 @@ import { KeyboardInputComponent } from '../features/config/keyboard-input/keyboa
import { IconInputComponent } from '../features/config/icon-input/icon-input.component';
import { SelectProjectComponent } from '../features/config/select-project/select-project.component';
import { RepeatSectionTypeComponent } from '../features/config/repeat-section-type/repeat-section-type.component';
import { FormlyMatSliderModule } from '@ngx-formly/material/slider';
import { FormlySliderComponent } from './formly-slider/formly-slider.component';
import { FormlyTagSelectionComponent } from './formly-tag-selection/formly-tag-selection.component';
import { FormlyBtnComponent } from './formly-button/formly-btn.component';
import { FormlyImageInputComponent } from './formly-image-input/formly-image-input.component';
@ -28,7 +28,7 @@ import { ColorInputComponent } from '../features/config/color-input/color-input.
imports: [
CommonModule,
FormsModule,
FormlyMatSliderModule,
FormlySliderComponent,
ReactiveFormsModule,
FormlyModule.forRoot({
validationMessages: [
@ -40,6 +40,7 @@ import { ColorInputComponent } from '../features/config/color-input/color-input.
{ name: 'maxLength', message: 'Value is too long' },
],
types: [
{ name: 'slider', component: FormlySliderComponent, wrappers: ['form-field'] },
{ name: 'link', component: FormlyLinkWidgetComponent },
{
name: 'duration',

View file

@ -0,0 +1,55 @@
import { Component, ChangeDetectionStrategy } from '@angular/core';
import { FieldType, FieldTypeConfig } from '@ngx-formly/core';
import { MatSliderModule } from '@angular/material/slider';
import { ReactiveFormsModule } from '@angular/forms';
import { FormlyFieldProps } from '@ngx-formly/material/form-field';
interface SliderProps extends FormlyFieldProps {
displayWith?: (value: number) => string;
discrete?: boolean;
showTickMarks?: boolean;
thumbLabel?: boolean;
}
@Component({
selector: 'formly-field-mat-slider',
standalone: true,
imports: [MatSliderModule, ReactiveFormsModule],
template: `
<mat-slider
[min]="props.min ?? 0"
[max]="props.max ?? 100"
[step]="props.step ?? 1"
[discrete]="props.discrete ?? props.thumbLabel ?? true"
[showTickMarks]="props.showTickMarks ?? false"
[displayWith]="props.displayWith ?? defaultDisplayWith"
>
<input
matSliderThumb
[formControl]="formControl"
/>
</mat-slider>
`,
styles: [
`
:host {
display: block;
width: 100%;
}
mat-slider {
width: 100%;
}
`,
],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class FormlySliderComponent extends FieldType<FieldTypeConfig<SliderProps>> {
defaultDisplayWith = (value: number): string => `${value}`;
override defaultOptions = {
props: {
hideFieldUnderline: true,
floatLabel: 'always' as const,
},
};
}

View file

@ -3,18 +3,18 @@ import { MarkedOptions, MarkedRenderer } from 'ngx-markdown';
export const markedOptionsFactory = (): MarkedOptions => {
const renderer = new MarkedRenderer();
renderer.checkbox = (isChecked: boolean) =>
`<span class="checkbox material-icons">${isChecked ? 'check_box' : 'check_box_outline_blank'}</span>`;
renderer.checkbox = ({ checked }) =>
`<span class="checkbox material-icons">${checked ? 'check_box' : 'check_box_outline_blank'}</span>`;
renderer.listitem = (text: string) =>
renderer.listitem = ({ text }) =>
text.includes('checkbox')
? `<li class="checkbox-wrapper ${text.includes('check_box_outline_blank') ? 'undone' : 'done'}">${text}</li>`
: `<li>${text}</li>`;
renderer.link = (href, title, text) =>
`<a target="_blank" href="${href}" title="${title}">${text}</a>`;
renderer.link = ({ href, title, text }) =>
`<a target="_blank" href="${href}" title="${title || ''}">${text}</a>`;
renderer.paragraph = (text) => {
renderer.paragraph = ({ text }) => {
const split = text.split('\n');
return split.reduce((acc, p, i) => {
const result = /h(\d)\./.exec(p);
@ -35,13 +35,15 @@ export const markedOptionsFactory = (): MarkedOptions => {
const urlPattern =
/\b((([A-Za-z][A-Za-z0-9+.-]*):\/\/([^\/?#]*))([^?#]*)(\?([^#]*))?(#(.*))?)\b/gi;
const rendererTxtOld = renderer.text;
renderer.text = (text) => {
return rendererTxtOld(
text.replace(urlPattern, (url) => {
const rendererTxtOld = renderer.text.bind(renderer);
renderer.text = (token) => {
const modifiedToken = {
...token,
text: token.text.replace(urlPattern, (url) => {
return `<a href="${url}" target="_blank">${url}</a>`;
}),
);
};
return rendererTxtOld(modifiedToken);
};
return {

View file

@ -17,7 +17,7 @@ import { App as CapacitorApp } from '@capacitor/app';
import { GlobalErrorHandler } from './app/core/error-handler/global-error-handler.class';
import { bootstrapApplication, BrowserModule } from '@angular/platform-browser';
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http';
import { MarkdownModule, MARKED_OPTIONS, provideMarkdown } from 'ngx-markdown';
import { MarkdownModule, MARKED_OPTIONS, provideMarkdown, SANITIZE } from 'ngx-markdown';
import { MAT_FORM_FIELD_DEFAULT_OPTIONS } from '@angular/material/form-field';
import { FeatureStoresModule } from './app/root-store/feature-stores.module';
import { MATERIAL_ANIMATIONS, MatNativeDateModule } from '@angular/material/core';
@ -93,7 +93,7 @@ bootstrapApplication(AppComponent, {
provide: MARKED_OPTIONS,
useFactory: markedOptionsFactory,
},
sanitize: SecurityContext.HTML,
sanitize: { provide: SANITIZE, useValue: SecurityContext.HTML },
}),
MaterialCssVarsModule.forRoot(),
MatSidenavModule,

View file

@ -5,8 +5,9 @@
"outDir": "./dist/out-tsc",
"sourceMap": true,
"declaration": false,
"module": "es2022",
"moduleResolution": "node",
"skipLibCheck": true,
"module": "preserve",
"moduleResolution": "bundler",
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"strict": true,