mirror of
https://github.com/johannesjo/super-productivity.git
synced 2026-07-18 08:56:41 +00:00
feat: improve performance during route changes
This commit is contained in:
parent
c34f258c50
commit
fddedf3fa6
3 changed files with 144 additions and 38 deletions
|
|
@ -1,6 +1,6 @@
|
|||
import { Injectable, signal, inject } from '@angular/core';
|
||||
import { Observable, combineLatest } from 'rxjs';
|
||||
import { map } from 'rxjs/operators';
|
||||
import { Observable, animationFrameScheduler, combineLatest } from 'rxjs';
|
||||
import { map, observeOn } from 'rxjs/operators';
|
||||
import { TaskWithSubTasks } from '../tasks/task.model';
|
||||
import { selectAllProjects } from '../project/store/project.selectors';
|
||||
import { selectAllTags } from './../tag/store/tag.reducer';
|
||||
|
|
@ -66,11 +66,26 @@ export class TaskViewCustomizerService {
|
|||
toObservable(this.selectedFilter),
|
||||
toObservable(this.filterInputValue),
|
||||
]).pipe(
|
||||
observeOn(animationFrameScheduler),
|
||||
map(([undone, sort, group, filter, filterVal]) => {
|
||||
const filtered = this.applyFilter(undone, filter, filterVal);
|
||||
const sorted = this.applySort(filtered, sort);
|
||||
const grouped =
|
||||
group !== 'default' ? this.applyGrouping(sorted, group) : undefined;
|
||||
const normalizedFilterVal =
|
||||
typeof filterVal === 'string' ? filterVal.trim() : filterVal;
|
||||
const filterValueToUse =
|
||||
typeof normalizedFilterVal === 'string' ? normalizedFilterVal : '';
|
||||
|
||||
const isDefaultFilter = filter === 'default' || !filterValueToUse;
|
||||
const isDefaultSort = sort === 'default';
|
||||
const isDefaultGroup = group === 'default';
|
||||
|
||||
if (isDefaultFilter && isDefaultSort && isDefaultGroup) {
|
||||
return { list: undone };
|
||||
}
|
||||
|
||||
const filtered = isDefaultFilter
|
||||
? undone
|
||||
: this.applyFilter(undone, filter, filterValueToUse);
|
||||
const sorted = isDefaultSort ? filtered : this.applySort(filtered, sort);
|
||||
const grouped = !isDefaultGroup ? this.applyGrouping(sorted, group) : undefined;
|
||||
return { list: sorted, grouped };
|
||||
}),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import {
|
|||
WorkContextType,
|
||||
} from './work-context.model';
|
||||
import { setActiveWorkContext } from './store/work-context.actions';
|
||||
import { NavigationEnd, Router } from '@angular/router';
|
||||
import { NavigationEnd, NavigationStart, Router } from '@angular/router';
|
||||
import {
|
||||
concatMap,
|
||||
delayWhen,
|
||||
|
|
@ -81,6 +81,10 @@ export class WorkContextService {
|
|||
private _translateService = inject(TranslateService);
|
||||
private _timeTrackingService = inject(TimeTrackingService);
|
||||
private _taskArchiveService = inject(TaskArchiveService);
|
||||
private _pendingNavigationUrl: string | null = null;
|
||||
private readonly _navigationMeasureName = 'work-view-route';
|
||||
private readonly _navigationStartMark = 'work-view-route:start';
|
||||
private readonly _navigationEndMark = 'work-view-route:end';
|
||||
|
||||
// here because to avoid circular dependencies
|
||||
// should be treated as private
|
||||
|
|
@ -409,6 +413,10 @@ export class WorkContextService {
|
|||
this.activeWorkContextType = v.activeType;
|
||||
});
|
||||
|
||||
this._router.events
|
||||
.pipe(filter((event): event is NavigationStart => event instanceof NavigationStart))
|
||||
.subscribe((event) => this._markNavigationStart(event.url));
|
||||
|
||||
// we need all data to be loaded before we dispatch a setActiveContext action
|
||||
this._router.events
|
||||
.pipe(
|
||||
|
|
@ -427,6 +435,8 @@ export class WorkContextService {
|
|||
),
|
||||
)
|
||||
.subscribe(({ urlAfterRedirects }: NavigationEnd) => {
|
||||
this._markNavigationEnd(urlAfterRedirects);
|
||||
|
||||
const split = urlAfterRedirects.split('/');
|
||||
const id = split[2];
|
||||
|
||||
|
|
@ -668,4 +678,64 @@ export class WorkContextService {
|
|||
private _setActiveContext(activeId: string, activeType: WorkContextType): void {
|
||||
this._store$.dispatch(setActiveWorkContext({ activeId, activeType }));
|
||||
}
|
||||
|
||||
private _markNavigationStart(url: string): void {
|
||||
if (!this._isPerformanceApiAvailable() || !this._shouldTrackNavigation(url)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._pendingNavigationUrl = url;
|
||||
performance.mark(this._navigationStartMark);
|
||||
}
|
||||
|
||||
private _markNavigationEnd(url: string): void {
|
||||
if (
|
||||
!this._isPerformanceApiAvailable() ||
|
||||
!this._pendingNavigationUrl ||
|
||||
!this._shouldTrackNavigation(url)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
performance.mark(this._navigationEndMark);
|
||||
try {
|
||||
performance.measure(
|
||||
this._navigationMeasureName,
|
||||
this._navigationStartMark,
|
||||
this._navigationEndMark,
|
||||
);
|
||||
} catch {
|
||||
// ignore measure errors (e.g. start mark missing)
|
||||
} finally {
|
||||
this._clearNavigationMarks();
|
||||
this._pendingNavigationUrl = null;
|
||||
}
|
||||
}
|
||||
|
||||
private _clearNavigationMarks(): void {
|
||||
if (!this._isPerformanceApiAvailable()) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
performance.clearMarks(this._navigationStartMark);
|
||||
performance.clearMarks(this._navigationEndMark);
|
||||
performance.clearMeasures(this._navigationMeasureName);
|
||||
} catch {
|
||||
// ignore cleanup errors
|
||||
}
|
||||
}
|
||||
|
||||
private _shouldTrackNavigation(url: string): boolean {
|
||||
return /(tag|project)\/.+\/tasks/.test(url);
|
||||
}
|
||||
|
||||
private _isPerformanceApiAvailable(): boolean {
|
||||
return (
|
||||
typeof performance !== 'undefined' &&
|
||||
typeof performance.mark === 'function' &&
|
||||
typeof performance.measure === 'function' &&
|
||||
typeof performance.clearMarks === 'function' &&
|
||||
typeof performance.clearMeasures === 'function'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
import {
|
||||
AfterContentInit,
|
||||
ChangeDetectionStrategy,
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
computed,
|
||||
effect,
|
||||
ElementRef,
|
||||
afterNextRender,
|
||||
inject,
|
||||
input,
|
||||
OnDestroy,
|
||||
|
|
@ -51,7 +51,6 @@ import { AsyncPipe, CommonModule } from '@angular/common';
|
|||
import { MsToStringPipe } from '../../ui/duration/ms-to-string.pipe';
|
||||
import { TranslatePipe } from '@ngx-translate/core';
|
||||
import {
|
||||
flattenTasks,
|
||||
selectLaterTodayTasksWithSubTasks,
|
||||
selectOverdueTasksWithSubTasks,
|
||||
} from '../tasks/store/task.selectors';
|
||||
|
|
@ -94,7 +93,7 @@ import { FinishDayBtnComponent } from './finish-day-btn/finish-day-btn.component
|
|||
FinishDayBtnComponent,
|
||||
],
|
||||
})
|
||||
export class WorkViewComponent implements OnInit, OnDestroy, AfterContentInit {
|
||||
export class WorkViewComponent implements OnInit, OnDestroy {
|
||||
taskService = inject(TaskService);
|
||||
takeABreakService = inject(TakeABreakService);
|
||||
planningModeService = inject(PlanningModeService);
|
||||
|
|
@ -177,29 +176,18 @@ export class WorkViewComponent implements OnInit, OnDestroy, AfterContentInit {
|
|||
const currentSelectedId = this.selectedTaskId();
|
||||
if (!currentSelectedId) return;
|
||||
|
||||
const undoneArr = flattenTasks(this.undoneTasks());
|
||||
if (undoneArr.some((t) => t.id === currentSelectedId)) return;
|
||||
|
||||
const doneArr = flattenTasks(this.doneTasks());
|
||||
if (doneArr.some((t) => t.id === currentSelectedId)) return;
|
||||
|
||||
if (
|
||||
this.laterTodayTasks().some(
|
||||
(t) => t.id === currentSelectedId || t.subTaskIds.includes(currentSelectedId),
|
||||
)
|
||||
)
|
||||
return;
|
||||
if (this._hasTaskInList(this.undoneTasks(), currentSelectedId)) return;
|
||||
if (this._hasTaskInList(this.doneTasks(), currentSelectedId)) return;
|
||||
if (this._hasTaskInList(this.laterTodayTasks(), currentSelectedId)) return;
|
||||
|
||||
if (
|
||||
this.workContextService.activeWorkContextId === TODAY_TAG.id &&
|
||||
this.overdueTasks().length > 0 &&
|
||||
flattenTasks(this.overdueTasks()).some((t) => t.id === currentSelectedId)
|
||||
this._hasTaskInList(this.overdueTasks(), currentSelectedId)
|
||||
)
|
||||
return;
|
||||
|
||||
// Check if task is in backlog
|
||||
const backlogArr = flattenTasks(this.backlogTasks());
|
||||
if (backlogArr.some((t) => t.id === currentSelectedId)) return;
|
||||
if (this._hasTaskInList(this.backlogTasks(), currentSelectedId)) return;
|
||||
|
||||
// if task really is gone
|
||||
this.taskService.setSelectedId(null);
|
||||
|
|
@ -231,6 +219,8 @@ export class WorkViewComponent implements OnInit, OnDestroy, AfterContentInit {
|
|||
localStorage.removeItem(LS.OVERDUE_TASKS_HIDDEN);
|
||||
}
|
||||
});
|
||||
|
||||
afterNextRender(() => this._initScrollTracking());
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
|
|
@ -251,22 +241,11 @@ export class WorkViewComponent implements OnInit, OnDestroy, AfterContentInit {
|
|||
);
|
||||
}
|
||||
|
||||
ngAfterContentInit(): void {
|
||||
this._subs.add(
|
||||
this.upperContainerScroll$.subscribe(({ target }) => {
|
||||
if ((target as HTMLElement).scrollTop !== 0) {
|
||||
this.layoutService.isScrolled.set(true);
|
||||
} else {
|
||||
this.layoutService.isScrolled.set(false);
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
if (this._switchListAnimationTimeout) {
|
||||
window.clearTimeout(this._switchListAnimationTimeout);
|
||||
}
|
||||
this._subs.unsubscribe();
|
||||
this.layoutService.isScrolled.set(false);
|
||||
}
|
||||
|
||||
|
|
@ -323,4 +302,46 @@ export class WorkViewComponent implements OnInit, OnDestroy, AfterContentInit {
|
|||
}),
|
||||
);
|
||||
}
|
||||
|
||||
private _initScrollTracking(): void {
|
||||
this._subs.add(
|
||||
this.upperContainerScroll$.subscribe(({ target }) => {
|
||||
if ((target as HTMLElement).scrollTop !== 0) {
|
||||
this.layoutService.isScrolled.set(true);
|
||||
} else {
|
||||
this.layoutService.isScrolled.set(false);
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
private _hasTaskInList(
|
||||
taskList: TaskWithSubTasks[] | null | undefined,
|
||||
taskId: string,
|
||||
): boolean {
|
||||
if (!taskList || !taskList.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const task of taskList) {
|
||||
if (!task) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (task.id === taskId) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const subTasks = task.subTasks;
|
||||
if (Array.isArray(subTasks) && subTasks.length) {
|
||||
for (const subTask of subTasks) {
|
||||
if (subTask && subTask.id === taskId) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue