diff --git a/src/app/features/tasks/task/task.component.ts b/src/app/features/tasks/task/task.component.ts index dab59ae3ec..3e7de460ef 100644 --- a/src/app/features/tasks/task/task.component.ts +++ b/src/app/features/tasks/task/task.component.ts @@ -234,7 +234,7 @@ export class TaskComponent implements OnDestroy, AfterViewInit { return 'chat'; }); - isTodayListActive = computed(() => this.workContextService.isTodayList); + isTodayListActive = computed(() => this.workContextService.isTodayListSignal()); taskIdWithPrefix = computed(() => 't-' + this.task().id); isRepeatTaskCreatedToday = computed( () => !!(this.task().repeatCfgId && this._dateService.isToday(this.task().created)), diff --git a/src/app/features/work-context/work-context.service.spec.ts b/src/app/features/work-context/work-context.service.spec.ts index 9d1743c32d..2e74ba8117 100644 --- a/src/app/features/work-context/work-context.service.spec.ts +++ b/src/app/features/work-context/work-context.service.spec.ts @@ -13,7 +13,10 @@ import { TimeTrackingService } from '../time-tracking/time-tracking.service'; import { TaskArchiveService } from '../archive/task-archive.service'; import { TODAY_TAG } from '../tag/tag.const'; import { WorkContext, WorkContextType } from './work-context.model'; -import { selectActiveWorkContext } from './store/work-context.selectors'; +import { + selectActiveContextId, + selectActiveWorkContext, +} from './store/work-context.selectors'; import { allDataWasLoaded } from '../../root-store/meta/all-data-was-loaded.actions'; describe('WorkContextService - undoneTasks$ filtering', () => { @@ -642,3 +645,79 @@ describe('WorkContextService - activeWorkContext$ distinctUntilChanged', () => { sub.unsubscribe(); }); }); + +// #8843: `task.component.ts` read `workContextService.isTodayList` (a plain +// mutable boolean) inside a `computed()`, so the computed had no signal producer +// and never invalidated. `isTodayListSignal` is a `toSignal(isTodayList$)` mirror +// that must reactively track the active work context. +describe('WorkContextService - isTodayListSignal reactivity', () => { + let store: MockStore; + let service: WorkContextService; + + beforeEach(() => { + const tagServiceMock = jasmine.createSpyObj('TagService', ['getTagById$']); + tagServiceMock.getTagById$.and.returnValue(of(TODAY_TAG)); + + const globalTrackingIntervalServiceMock = jasmine.createSpyObj( + 'GlobalTrackingIntervalService', + [], + { todayDateStr$: of('2026-04-06') }, + ); + + const dateServiceMock = jasmine.createSpyObj('DateService', ['todayStr']); + dateServiceMock.todayStr.and.returnValue('2026-04-06'); + + const timeTrackingServiceMock = jasmine.createSpyObj('TimeTrackingService', [ + 'getWorkStartEndForWorkContext$', + ]); + timeTrackingServiceMock.getWorkStartEndForWorkContext$.and.returnValue(of({})); + + const taskArchiveServiceMock = jasmine.createSpyObj('TaskArchiveService', [ + 'loadYoung', + ]); + taskArchiveServiceMock.loadYoung.and.returnValue( + Promise.resolve({ ids: [], entities: {} }), + ); + + TestBed.configureTestingModule({ + imports: [TranslateModule.forRoot()], + providers: [ + provideMockStore({ + initialState: { + workContext: { activeId: TODAY_TAG.id, activeType: 'TAG' }, + tag: { entities: {}, ids: [] }, + project: { entities: {}, ids: [] }, + task: { entities: {}, ids: [] }, + }, + }), + // activeWorkContextId$ is gated behind the allDataWasLoaded action. + provideMockActions(() => of(allDataWasLoaded())), + { provide: Router, useValue: { events: of(), url: '/' } }, + { provide: TagService, useValue: tagServiceMock }, + { + provide: GlobalTrackingIntervalService, + useValue: globalTrackingIntervalServiceMock, + }, + { provide: DateService, useValue: dateServiceMock }, + { provide: TimeTrackingService, useValue: timeTrackingServiceMock }, + { provide: TaskArchiveService, useValue: taskArchiveServiceMock }, + WorkContextService, + ], + }); + + store = TestBed.inject(MockStore); + store.overrideSelector(selectActiveContextId, TODAY_TAG.id); + store.refreshState(); + + service = TestBed.inject(WorkContextService); + }); + + it('is true on the Today list and flips to false when the context changes', () => { + expect(service.isTodayListSignal()).toBe(true); + + store.overrideSelector(selectActiveContextId, 'some-other-context'); + store.refreshState(); + + expect(service.isTodayListSignal()).toBe(false); + }); +}); diff --git a/src/app/features/work-context/work-context.service.ts b/src/app/features/work-context/work-context.service.ts index fdd3e10ea2..7f674fedeb 100644 --- a/src/app/features/work-context/work-context.service.ts +++ b/src/app/features/work-context/work-context.service.ts @@ -373,6 +373,11 @@ export class WorkContextService { map((id) => id === TODAY_TAG.id), shareReplay(1), ); + // Signal mirror of `isTodayList$`. The `isTodayList` boolean above is a plain + // mutable field kept in sync via subscribe, so reading it inside a `computed()` + // never invalidates the computed (a non-signal is not a producer, #8843). Reactive + // consumers should read this signal instead; the boolean stays for synchronous reads. + readonly isTodayListSignal = toSignal(this.isTodayList$, { initialValue: false }); isHasTasksToWorkOn$: Observable = combineLatest([ this.mainListTasks$,