refactor: improve typing

This commit is contained in:
Johannes Millan 2025-08-13 19:20:31 +02:00
parent fbc45de335
commit f077a6f719
15 changed files with 73 additions and 65 deletions

View file

@ -350,6 +350,8 @@ git clone https://github.com/johannesjo/super-productivity.git
cd super-productivity cd super-productivity
npm i -g @angular/cli npm i -g @angular/cli
npm i npm i
# prepare the env file once
npm run env
``` ```
**Run the dev server** **Run the dev server**

View file

@ -97,7 +97,12 @@ export const initDebug = (
} }
if (opts.devToolsMode !== 'previous' && opts.devToolsMode) { if (opts.devToolsMode !== 'previous' && opts.devToolsMode) {
devToolsOptions.mode = opts.devToolsMode as 'bottom' | 'left' | 'right' | 'undocked' | 'detach'; devToolsOptions.mode = opts.devToolsMode as
| 'bottom'
| 'left'
| 'right'
| 'undocked'
| 'detach';
} }
app.on('browser-window-created', (event, win) => { app.on('browser-window-created', (event, win) => {

View file

@ -121,21 +121,21 @@ export class MainHeaderComponent implements OnDestroy {
currentTaskContext = toSignal(this._currentTaskContext$); currentTaskContext = toSignal(this._currentTaskContext$);
private _isRouteWithSidePanel$ = this._router.events.pipe( private _isRouteWithSidePanel$ = this._router.events.pipe(
filter((event: any) => event instanceof NavigationEnd), filter((event): event is NavigationEnd => event instanceof NavigationEnd),
map((event) => true), // Always true since right-panel is now global map((event) => true), // Always true since right-panel is now global
startWith(true), // Always true since right-panel is now global startWith(true), // Always true since right-panel is now global
); );
isRouteWithSidePanel = toSignal(this._isRouteWithSidePanel$, { initialValue: true }); isRouteWithSidePanel = toSignal(this._isRouteWithSidePanel$, { initialValue: true });
private _isScheduleSection$ = this._router.events.pipe( private _isScheduleSection$ = this._router.events.pipe(
filter((event: any) => event instanceof NavigationEnd), filter((event): event is NavigationEnd => event instanceof NavigationEnd),
map((event) => !!event.urlAfterRedirects.match(/(schedule)$/)), map((event) => !!event.urlAfterRedirects.match(/(schedule)$/)),
startWith(!!this._router.url.match(/(schedule)$/)), startWith(!!this._router.url.match(/(schedule)$/)),
); );
isScheduleSection = toSignal(this._isScheduleSection$, { initialValue: false }); isScheduleSection = toSignal(this._isScheduleSection$, { initialValue: false });
private _isWorkViewPage$ = this._router.events.pipe( private _isWorkViewPage$ = this._router.events.pipe(
filter((event: any) => event instanceof NavigationEnd), filter((event): event is NavigationEnd => event instanceof NavigationEnd),
map((event) => !!event.urlAfterRedirects.match(/tasks$/)), map((event) => !!event.urlAfterRedirects.match(/tasks$/)),
startWith(!!this._router.url.match(/tasks$/)), startWith(!!this._router.url.match(/tasks$/)),
); );

View file

@ -125,7 +125,7 @@ export class MobileSidePanelMenuComponent {
// Convert observables to signals // Convert observables to signals
readonly isRouteWithSidePanel = toSignal( readonly isRouteWithSidePanel = toSignal(
this._router.events.pipe( this._router.events.pipe(
filter((event: any) => event instanceof NavigationEnd), filter((event): event is NavigationEnd => event instanceof NavigationEnd),
map((event) => true), // Always true since right-panel is now global map((event) => true), // Always true since right-panel is now global
startWith(true), // Always true since right-panel is now global startWith(true), // Always true since right-panel is now global
), ),
@ -134,7 +134,7 @@ export class MobileSidePanelMenuComponent {
readonly isWorkViewPage = toSignal( readonly isWorkViewPage = toSignal(
this._router.events.pipe( this._router.events.pipe(
filter((event: any) => event instanceof NavigationEnd), filter((event): event is NavigationEnd => event instanceof NavigationEnd),
map((event) => !!event.urlAfterRedirects.match(/tasks$/)), map((event) => !!event.urlAfterRedirects.match(/tasks$/)),
startWith(!!this._router.url.match(/tasks$/)), startWith(!!this._router.url.match(/tasks$/)),
), ),
@ -185,7 +185,12 @@ export class MobileSidePanelMenuComponent {
this.isShowMobileMenu.update((v) => !v); this.isShowMobileMenu.update((v) => !v);
} }
onPluginButtonClick(button: any): void { onPluginButtonClick(button: {
pluginId: string;
onClick?: () => void;
label?: string;
icon?: string;
}): void {
this._store.dispatch(togglePluginPanel(button.pluginId)); this._store.dispatch(togglePluginPanel(button.pluginId));
if (button.onClick) { if (button.onClick) {

View file

@ -86,7 +86,7 @@ export class DatabaseService {
e: Error | unknown, e: Error | unknown,
// eslint-disable-next-line @typescript-eslint/ban-types // eslint-disable-next-line @typescript-eslint/ban-types
fn: Function, fn: Function,
args: any[], args: unknown[],
): Promise<void> { ): Promise<void> {
devError(e); devError(e);
if (confirm(this._translateService.instant(T.CONFIRM.RELOAD_AFTER_IDB_ERROR))) { if (confirm(this._translateService.instant(T.CONFIRM.RELOAD_AFTER_IDB_ERROR))) {

View file

@ -9,7 +9,10 @@ const FIELDS_TO_COMPARE: (keyof SimpleCounterCfgFields)[] = [
'countdownDuration', 'countdownDuration',
]; ];
export const isEqualSimpleCounterCfg = (a: any, b: any): boolean => { export const isEqualSimpleCounterCfg = (
a: SimpleCounterCfgFields[] | unknown,
b: SimpleCounterCfgFields[] | unknown,
): boolean => {
if (Array.isArray(a) && Array.isArray(b)) { if (Array.isArray(a) && Array.isArray(b)) {
if (a.length !== b.length) { if (a.length !== b.length) {
return false; return false;
@ -18,7 +21,7 @@ export const isEqualSimpleCounterCfg = (a: any, b: any): boolean => {
if (a[i] !== b[i]) { if (a[i] !== b[i]) {
// eslint-disable-next-line @typescript-eslint/prefer-for-of // eslint-disable-next-line @typescript-eslint/prefer-for-of
for (let j = 0; j < FIELDS_TO_COMPARE.length; j++) { for (let j = 0; j < FIELDS_TO_COMPARE.length; j++) {
const field: any = FIELDS_TO_COMPARE[j]; const field = FIELDS_TO_COMPARE[j];
if (a[field] !== b[field]) { if (a[field] !== b[field]) {
return false; return false;
} }

View file

@ -1,4 +1,4 @@
import { Injectable, inject } from '@angular/core'; import { inject, Injectable } from '@angular/core';
import { select, Store } from '@ngrx/store'; import { select, Store } from '@ngrx/store';
import { import {
selectAllSimpleCounters, selectAllSimpleCounters,
@ -12,8 +12,8 @@ import {
deleteSimpleCounter, deleteSimpleCounter,
deleteSimpleCounters, deleteSimpleCounters,
increaseSimpleCounterCounterToday, increaseSimpleCounterCounterToday,
setSimpleCounterCounterToday,
setSimpleCounterCounterForDate, setSimpleCounterCounterForDate,
setSimpleCounterCounterToday,
toggleSimpleCounterCounter, toggleSimpleCounterCounter,
turnOffAllSimpleCounterCounters, turnOffAllSimpleCounterCounters,
updateAllSimpleCounters, updateAllSimpleCounters,
@ -38,7 +38,9 @@ export class SimpleCounterService {
select(selectAllSimpleCounters), select(selectAllSimpleCounters),
); );
simpleCountersUpdatedOnCfgChange$: Observable<SimpleCounter[]> = simpleCountersUpdatedOnCfgChange$: Observable<SimpleCounter[]> =
this.simpleCounters$.pipe(distinctUntilChanged(isEqualSimpleCounterCfg)); this.simpleCounters$.pipe(
distinctUntilChanged((a, b) => isEqualSimpleCounterCfg(a, b)),
);
enabledSimpleCounters$: Observable<SimpleCounter[]> = this._store$.select( enabledSimpleCounters$: Observable<SimpleCounter[]> = this._store$.select(
selectEnabledSimpleCounters, selectEnabledSimpleCounters,
@ -47,9 +49,6 @@ export class SimpleCounterService {
selectEnabledSimpleStopWatchCounters, selectEnabledSimpleStopWatchCounters,
); );
enabledSimpleCountersUpdatedOnCfgChange$: Observable<SimpleCounter[]> =
this.enabledSimpleCounters$.pipe(distinctUntilChanged(isEqualSimpleCounterCfg));
enabledAndToggledSimpleCounters$: Observable<SimpleCounter[]> = this._store$.select( enabledAndToggledSimpleCounters$: Observable<SimpleCounter[]> = this._store$.select(
selectEnabledAndToggledSimpleCounters, selectEnabledAndToggledSimpleCounters,
); );

View file

@ -3,7 +3,7 @@ import { Task } from '../task.model';
@Pipe({ name: 'subTaskTotalTimeSpent' }) @Pipe({ name: 'subTaskTotalTimeSpent' })
export class SubTaskTotalTimeSpentPipe implements PipeTransform { export class SubTaskTotalTimeSpentPipe implements PipeTransform {
transform: (value: any, ...args: any[]) => any = getSubTasksTotalTimeSpent; transform: (value: Task[]) => number = getSubTasksTotalTimeSpent;
} }
export const getSubTasksTotalTimeSpent = (subTasks: Task[]): number => { export const getSubTasksTotalTimeSpent = (subTasks: Task[]): number => {

View file

@ -57,7 +57,7 @@ export class TaskRelatedModelEffects {
), ),
); );
autoAddTodayTagOnMarkAsDone: any = createEffect(() => autoAddTodayTagOnMarkAsDone = createEffect(() =>
this.ifAutoAddTodayEnabled$( this.ifAutoAddTodayEnabled$(
this._actions$.pipe( this._actions$.pipe(
ofType(TaskSharedActions.updateTask), ofType(TaskSharedActions.updateTask),
@ -76,7 +76,7 @@ export class TaskRelatedModelEffects {
// EXTERNAL ===> TASKS // EXTERNAL ===> TASKS
// ------------------- // -------------------
moveTaskToUnDone$: any = createEffect(() => moveTaskToUnDone$ = createEffect(() =>
this._actions$.pipe( this._actions$.pipe(
ofType(moveTaskInTodayList, moveProjectTaskToRegularList), ofType(moveTaskInTodayList, moveProjectTaskToRegularList),
filter( filter(
@ -95,7 +95,7 @@ export class TaskRelatedModelEffects {
), ),
); );
moveTaskToDone$: any = createEffect(() => moveTaskToDone$ = createEffect(() =>
this._actions$.pipe( this._actions$.pipe(
ofType(moveTaskInTodayList, moveProjectTaskToRegularList), ofType(moveTaskInTodayList, moveProjectTaskToRegularList),
filter( filter(

View file

@ -149,15 +149,15 @@ export class WorklogComponent implements AfterViewInit, OnDestroy {
}); });
} }
sortWorklogItems(a: any, b: any): number { sortWorklogItems(a: { key: number }, b: { key: number }): number {
return b.key - a.key; return b.key - a.key;
} }
sortWorklogItemsReverse(a: any, b: any): number { sortWorklogItemsReverse(a: { key: number }, b: { key: number }): number {
return a.key - b.key; return a.key - b.key;
} }
trackByKey(i: number, val: { key: any; val: any }): number { trackByKey(i: number, val: { key: number; val: unknown }): number {
return val.key; return val.key;
} }

View file

@ -89,9 +89,9 @@ export class FileImexComponent implements OnInit {
} }
// NOTE: after promise done the file is NOT yet read // NOTE: after promise done the file is NOT yet read
async handleFileInput(ev: any): Promise<void> { async handleFileInput(ev: Event): Promise<void> {
const files = ev.target.files; const files = (ev.target as HTMLInputElement).files;
const file = files.item(0); const file = files?.item(0);
if (!file) { if (!file) {
// No file selected or selection cancelled // No file selected or selection cancelled

View file

@ -1,4 +1,4 @@
import { Action } from '@ngrx/store'; import { Action, ActionReducer } from '@ngrx/store';
import { DEFAULT_TASK, Task, TaskCopy } from '../../../features/tasks/task.model'; import { DEFAULT_TASK, Task, TaskCopy } from '../../../features/tasks/task.model';
import { TaskSharedActions } from '../task-shared.actions'; import { TaskSharedActions } from '../task-shared.actions';
@ -21,28 +21,33 @@ import { validateAndFixDataConsistencyAfterBatchUpdate } from './validate-and-fi
* Meta reducer for handling batch updates to tasks within a project * Meta reducer for handling batch updates to tasks within a project
* This reducer processes all operations in a single state update for efficiency * This reducer processes all operations in a single state update for efficiency
*/ */
export const taskBatchUpdateMetaReducer = ( export const taskBatchUpdateMetaReducer = <T extends Partial<RootState> = RootState>(
reducer: any, reducer: ActionReducer<T>,
): ((state: any, action: any) => any) => { ): ActionReducer<T> => {
return (state: any, action: Action) => { return (state: T | undefined, action: Action) => {
if (action.type === TaskSharedActions.batchUpdateForProject.type) { if (action.type === TaskSharedActions.batchUpdateForProject.type) {
const { projectId, operations, createdTaskIds } = action as ReturnType< const { projectId, operations, createdTaskIds } = action as ReturnType<
typeof TaskSharedActions.batchUpdateForProject typeof TaskSharedActions.batchUpdateForProject
>; >;
// Ensure state has required properties // Ensure state has required properties
if (!state[TASK_FEATURE_NAME] || !state[PROJECT_FEATURE_NAME]) { const rootState = state as unknown as RootState;
if (
!rootState ||
!rootState[TASK_FEATURE_NAME] ||
!rootState[PROJECT_FEATURE_NAME]
) {
Log.error('taskBatchUpdateMetaReducer: Missing required state properties'); Log.error('taskBatchUpdateMetaReducer: Missing required state properties');
return reducer(state, action); return reducer(state, action);
} }
// Validate project exists // Validate project exists
if (!state[PROJECT_FEATURE_NAME].entities[projectId]) { if (!rootState[PROJECT_FEATURE_NAME].entities[projectId]) {
Log.error(`taskBatchUpdateMetaReducer: Project ${projectId} not found`); Log.error(`taskBatchUpdateMetaReducer: Project ${projectId} not found`);
return reducer(state, action); return reducer(state, action);
} }
let newState = { ...state } as RootState; let newState = { ...rootState } as RootState;
const tasksToAdd: Task[] = []; const tasksToAdd: Task[] = [];
const tasksToUpdate: { id: string; changes: Partial<Task> }[] = []; const tasksToUpdate: { id: string; changes: Partial<Task> }[] = [];
const taskIdsToDelete: string[] = []; const taskIdsToDelete: string[] = [];
@ -346,7 +351,7 @@ export const taskBatchUpdateMetaReducer = (
newTaskOrder, newTaskOrder,
); );
return reducer(newState, action); return reducer(newState as T, action);
} }
return reducer(state, action); return reducer(state, action);

View file

@ -33,7 +33,7 @@ export class InputDurationSliderComponent implements OnInit, OnDestroy {
// Convert to signals // Convert to signals
readonly minutesBefore = signal(0); readonly minutesBefore = signal(0);
readonly dots = signal<any[]>([]); readonly dots = signal<number[]>([]);
readonly uid = 'duration-input-slider' + nanoid(); readonly uid = 'duration-input-slider' + nanoid();
readonly el: HTMLElement; readonly el: HTMLElement;
@ -47,9 +47,9 @@ export class InputDurationSliderComponent implements OnInit, OnDestroy {
// Internal model signal // Internal model signal
readonly _model = signal(0); readonly _model = signal(0);
startHandler?: (ev: any) => void; startHandler?: (ev: MouseEvent | TouchEvent) => void;
endHandler?: () => void; endHandler?: () => void;
moveHandler?: (ev: any) => void; moveHandler?: (ev: MouseEvent | TouchEvent) => void;
readonly circleEl = viewChild<ElementRef>('circleEl'); readonly circleEl = viewChild<ElementRef>('circleEl');
@ -74,7 +74,10 @@ export class InputDurationSliderComponent implements OnInit, OnDestroy {
} }
// don't execute when clicked on label or input // don't execute when clicked on label or input
if (ev.target.tagName === 'LABEL' || ev.target.tagName === 'INPUT') { if (
(ev.target as HTMLElement)?.tagName === 'LABEL' ||
(ev.target as HTMLElement)?.tagName === 'INPUT'
) {
this.endHandler(); this.endHandler();
return; return;
} }
@ -91,7 +94,8 @@ export class InputDurationSliderComponent implements OnInit, OnDestroy {
this.moveHandler = (ev) => { this.moveHandler = (ev) => {
if ( if (
ev.type === 'click' && ev.type === 'click' &&
(ev.target.tagName === 'LABEL' || ev.target.tagName === 'INPUT') ((ev.target as HTMLElement)?.tagName === 'LABEL' ||
(ev.target as HTMLElement)?.tagName === 'INPUT')
) { ) {
return; return;
} }
@ -108,16 +112,18 @@ export class InputDurationSliderComponent implements OnInit, OnDestroy {
const centerX = circleEl.nativeElement.offsetWidth / 2; const centerX = circleEl.nativeElement.offsetWidth / 2;
const centerY = circleEl.nativeElement.offsetHeight / 2; const centerY = circleEl.nativeElement.offsetHeight / 2;
let offsetX; let offsetX: number;
let offsetY: number;
let offsetY;
if (ev.type === 'touchmove') { if (ev.type === 'touchmove') {
const rect = ev.target.getBoundingClientRect(); const touchEv = ev as TouchEvent;
offsetX = ev.targetTouches[0].pageX - rect.left; const rect = (ev.target as Element).getBoundingClientRect();
offsetY = ev.targetTouches[0].pageY - rect.top; offsetX = touchEv.targetTouches[0].pageX - rect.left;
offsetY = touchEv.targetTouches[0].pageY - rect.top;
} else { } else {
offsetX = ev.offsetX; const mouseEv = ev as MouseEvent;
offsetY = ev.offsetY; offsetX = mouseEv.offsetX;
offsetY = mouseEv.offsetY;
} }
const x = offsetX - centerX; const x = offsetX - centerX;

View file

@ -1,17 +0,0 @@
import { Pipe, PipeTransform } from '@angular/core';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { msToString } from './ms-to-string.pipe';
@Pipe({ name: 'msToString$' })
export class MsToStringPipe$ implements PipeTransform {
transform(value$: Observable<any> | undefined, showSeconds?: boolean): any {
if (value$) {
value$.pipe(
map((value) => {
return msToString(value, showSeconds);
}),
);
}
}
}

View file

@ -41,7 +41,7 @@ export class PanDirective implements OnDestroy {
private _isPanning = false; private _isPanning = false;
private _touchIdentifier: number | null = null; private _touchIdentifier: number | null = null;
private _isScrolling = false; private _isScrolling = false;
private _scrollTimeout: any = null; private _scrollTimeout: ReturnType<typeof setTimeout> | null = null;
private _scrollListener: (() => void) | null = null; private _scrollListener: (() => void) | null = null;
private _scrollableParent: HTMLElement | null = null; private _scrollableParent: HTMLElement | null = null;