fix(work-context): make isTodayListActive reactive to context changes (#8863)

task.component's `isTodayListActive` computed read the plain mutable
`WorkContextService.isTodayList` boolean, which is not a signal producer,
so the computed never invalidated and returned its first value for the
component's lifetime.

Expose `isTodayListSignal` (a toSignal mirror of isTodayList$) on the
service and read that from the computed. The plain boolean stays for the
synchronous reader in history.component.

#8843
This commit is contained in:
John Costa 2026-07-09 06:35:59 -07:00 committed by GitHub
parent 8a1105bbb2
commit 24c799ee13
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 86 additions and 2 deletions

View file

@ -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)),

View file

@ -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);
});
});

View file

@ -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<boolean> = combineLatest([
this.mainListTasks$,