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
npm i -g @angular/cli
npm i
# prepare the env file once
npm run env
```
**Run the dev server**

View file

@ -97,7 +97,12 @@ export const initDebug = (
}
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) => {

View file

@ -121,21 +121,21 @@ export class MainHeaderComponent implements OnDestroy {
currentTaskContext = toSignal(this._currentTaskContext$);
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
startWith(true), // Always true since right-panel is now global
);
isRouteWithSidePanel = toSignal(this._isRouteWithSidePanel$, { initialValue: true });
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)$/)),
startWith(!!this._router.url.match(/(schedule)$/)),
);
isScheduleSection = toSignal(this._isScheduleSection$, { initialValue: false });
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$/)),
startWith(!!this._router.url.match(/tasks$/)),
);

View file

@ -125,7 +125,7 @@ export class MobileSidePanelMenuComponent {
// Convert observables to signals
readonly isRouteWithSidePanel = toSignal(
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
startWith(true), // Always true since right-panel is now global
),
@ -134,7 +134,7 @@ export class MobileSidePanelMenuComponent {
readonly isWorkViewPage = toSignal(
this._router.events.pipe(
filter((event: any) => event instanceof NavigationEnd),
filter((event): event is NavigationEnd => event instanceof NavigationEnd),
map((event) => !!event.urlAfterRedirects.match(/tasks$/)),
startWith(!!this._router.url.match(/tasks$/)),
),
@ -185,7 +185,12 @@ export class MobileSidePanelMenuComponent {
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));
if (button.onClick) {

View file

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

View file

@ -9,7 +9,10 @@ const FIELDS_TO_COMPARE: (keyof SimpleCounterCfgFields)[] = [
'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 (a.length !== b.length) {
return false;
@ -18,7 +21,7 @@ export const isEqualSimpleCounterCfg = (a: any, b: any): boolean => {
if (a[i] !== b[i]) {
// eslint-disable-next-line @typescript-eslint/prefer-for-of
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]) {
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 {
selectAllSimpleCounters,
@ -12,8 +12,8 @@ import {
deleteSimpleCounter,
deleteSimpleCounters,
increaseSimpleCounterCounterToday,
setSimpleCounterCounterToday,
setSimpleCounterCounterForDate,
setSimpleCounterCounterToday,
toggleSimpleCounterCounter,
turnOffAllSimpleCounterCounters,
updateAllSimpleCounters,
@ -38,7 +38,9 @@ export class SimpleCounterService {
select(selectAllSimpleCounters),
);
simpleCountersUpdatedOnCfgChange$: Observable<SimpleCounter[]> =
this.simpleCounters$.pipe(distinctUntilChanged(isEqualSimpleCounterCfg));
this.simpleCounters$.pipe(
distinctUntilChanged((a, b) => isEqualSimpleCounterCfg(a, b)),
);
enabledSimpleCounters$: Observable<SimpleCounter[]> = this._store$.select(
selectEnabledSimpleCounters,
@ -47,9 +49,6 @@ export class SimpleCounterService {
selectEnabledSimpleStopWatchCounters,
);
enabledSimpleCountersUpdatedOnCfgChange$: Observable<SimpleCounter[]> =
this.enabledSimpleCounters$.pipe(distinctUntilChanged(isEqualSimpleCounterCfg));
enabledAndToggledSimpleCounters$: Observable<SimpleCounter[]> = this._store$.select(
selectEnabledAndToggledSimpleCounters,
);

View file

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

View file

@ -57,7 +57,7 @@ export class TaskRelatedModelEffects {
),
);
autoAddTodayTagOnMarkAsDone: any = createEffect(() =>
autoAddTodayTagOnMarkAsDone = createEffect(() =>
this.ifAutoAddTodayEnabled$(
this._actions$.pipe(
ofType(TaskSharedActions.updateTask),
@ -76,7 +76,7 @@ export class TaskRelatedModelEffects {
// EXTERNAL ===> TASKS
// -------------------
moveTaskToUnDone$: any = createEffect(() =>
moveTaskToUnDone$ = createEffect(() =>
this._actions$.pipe(
ofType(moveTaskInTodayList, moveProjectTaskToRegularList),
filter(
@ -95,7 +95,7 @@ export class TaskRelatedModelEffects {
),
);
moveTaskToDone$: any = createEffect(() =>
moveTaskToDone$ = createEffect(() =>
this._actions$.pipe(
ofType(moveTaskInTodayList, moveProjectTaskToRegularList),
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;
}
sortWorklogItemsReverse(a: any, b: any): number {
sortWorklogItemsReverse(a: { key: number }, b: { key: number }): number {
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;
}

View file

@ -89,9 +89,9 @@ export class FileImexComponent implements OnInit {
}
// NOTE: after promise done the file is NOT yet read
async handleFileInput(ev: any): Promise<void> {
const files = ev.target.files;
const file = files.item(0);
async handleFileInput(ev: Event): Promise<void> {
const files = (ev.target as HTMLInputElement).files;
const file = files?.item(0);
if (!file) {
// 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 { 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
* This reducer processes all operations in a single state update for efficiency
*/
export const taskBatchUpdateMetaReducer = (
reducer: any,
): ((state: any, action: any) => any) => {
return (state: any, action: Action) => {
export const taskBatchUpdateMetaReducer = <T extends Partial<RootState> = RootState>(
reducer: ActionReducer<T>,
): ActionReducer<T> => {
return (state: T | undefined, action: Action) => {
if (action.type === TaskSharedActions.batchUpdateForProject.type) {
const { projectId, operations, createdTaskIds } = action as ReturnType<
typeof TaskSharedActions.batchUpdateForProject
>;
// 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');
return reducer(state, action);
}
// Validate project exists
if (!state[PROJECT_FEATURE_NAME].entities[projectId]) {
if (!rootState[PROJECT_FEATURE_NAME].entities[projectId]) {
Log.error(`taskBatchUpdateMetaReducer: Project ${projectId} not found`);
return reducer(state, action);
}
let newState = { ...state } as RootState;
let newState = { ...rootState } as RootState;
const tasksToAdd: Task[] = [];
const tasksToUpdate: { id: string; changes: Partial<Task> }[] = [];
const taskIdsToDelete: string[] = [];
@ -346,7 +351,7 @@ export const taskBatchUpdateMetaReducer = (
newTaskOrder,
);
return reducer(newState, action);
return reducer(newState as T, action);
}
return reducer(state, action);

View file

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