From d475d88da3dc9de48466c8f10260139fb0a206c5 Mon Sep 17 00:00:00 2001 From: Johannes Millan Date: Fri, 9 Jan 2026 13:40:00 +0100 Subject: [PATCH] feat(work-view): show day of week in scheduled date group headers Display weekday alongside date when tasks are grouped by scheduled date (e.g., "Wed 1/15" instead of "2025-01-15"), making it easier to identify weekends at a glance. Closes #5941 --- .../work-view/work-view.component.html | 2 +- .../features/work-view/work-view.component.ts | 2 + .../pipes/scheduled-date-group.pipe.spec.ts | 98 +++++++++++++++++++ src/app/ui/pipes/scheduled-date-group.pipe.ts | 57 +++++++++++ 4 files changed, 158 insertions(+), 1 deletion(-) create mode 100644 src/app/ui/pipes/scheduled-date-group.pipe.spec.ts create mode 100644 src/app/ui/pipes/scheduled-date-group.pipe.ts diff --git a/src/app/features/work-view/work-view.component.html b/src/app/features/work-view/work-view.component.html index 868d2be79..690f4542c 100644 --- a/src/app/features/work-view/work-view.component.html +++ b/src/app/features/work-view/work-view.component.html @@ -174,7 +174,7 @@ @if (customized.grouped) { @for (group of customized.grouped | keyvalue; track group.key) { diff --git a/src/app/features/work-view/work-view.component.ts b/src/app/features/work-view/work-view.component.ts index 1f7771d22..d8fd9c7f4 100644 --- a/src/app/features/work-view/work-view.component.ts +++ b/src/app/features/work-view/work-view.component.ts @@ -62,6 +62,7 @@ import { TaskSharedActions } from '../../root-store/meta/task-shared.actions'; import { TODAY_TAG } from '../tag/tag.const'; import { LS } from '../../core/persistence/storage-keys.const'; import { FinishDayBtnComponent } from './finish-day-btn/finish-day-btn.component'; +import { ScheduledDateGroupPipe } from '../../ui/pipes/scheduled-date-group.pipe'; @Component({ selector: 'work-view', @@ -92,6 +93,7 @@ import { FinishDayBtnComponent } from './finish-day-btn/finish-day-btn.component CollapsibleComponent, CommonModule, FinishDayBtnComponent, + ScheduledDateGroupPipe, ], }) export class WorkViewComponent implements OnInit, OnDestroy { diff --git a/src/app/ui/pipes/scheduled-date-group.pipe.spec.ts b/src/app/ui/pipes/scheduled-date-group.pipe.spec.ts new file mode 100644 index 000000000..0b82445a7 --- /dev/null +++ b/src/app/ui/pipes/scheduled-date-group.pipe.spec.ts @@ -0,0 +1,98 @@ +import { TestBed } from '@angular/core/testing'; +import { ScheduledDateGroupPipe } from './scheduled-date-group.pipe'; +import { DateTimeFormatService } from '../../core/date-time-format/date-time-format.service'; +import { TranslateService } from '@ngx-translate/core'; +import { getDbDateStr } from '../../util/get-db-date-str'; + +describe('ScheduledDateGroupPipe', () => { + let pipe: ScheduledDateGroupPipe; + let mockDateTimeFormatService: jasmine.SpyObj; + let mockTranslateService: jasmine.SpyObj; + + beforeEach(() => { + mockDateTimeFormatService = jasmine.createSpyObj('DateTimeFormatService', [], { + currentLocale: 'en-US', + }); + mockTranslateService = jasmine.createSpyObj('TranslateService', ['instant']); + mockTranslateService.instant.and.callFake((key: string) => { + if (key === 'G.TODAY_TAG_TITLE') return 'Today'; + return key; + }); + + TestBed.configureTestingModule({ + providers: [ + ScheduledDateGroupPipe, + { provide: DateTimeFormatService, useValue: mockDateTimeFormatService }, + { provide: TranslateService, useValue: mockTranslateService }, + ], + }); + + pipe = TestBed.inject(ScheduledDateGroupPipe); + }); + + it('should create the pipe', () => { + expect(pipe).toBeTruthy(); + }); + + it('should format date string with weekday', () => { + // Wednesday, January 15, 2025 + const result = pipe.transform('2025-01-15'); + expect(result).toMatch(/Wed/i); + expect(result).toContain('1'); + expect(result).toContain('15'); + }); + + it('should return "No date" unchanged', () => { + const result = pipe.transform('No date'); + expect(result).toBe('No date'); + }); + + it('should return null for null input', () => { + const result = pipe.transform(null); + expect(result).toBeNull(); + }); + + it('should return null for undefined input', () => { + const result = pipe.transform(undefined); + expect(result).toBeNull(); + }); + + it('should handle "Today" translation for today\'s date', () => { + const todayStr = getDbDateStr(); + const result = pipe.transform(todayStr); + expect(result).toBe('Today'); + expect(mockTranslateService.instant).toHaveBeenCalled(); + }); + + it('should format weekend dates correctly', () => { + // Saturday, January 18, 2025 + const saturdayResult = pipe.transform('2025-01-18'); + expect(saturdayResult).toMatch(/Sat/i); + + // Sunday, January 19, 2025 + const sundayResult = pipe.transform('2025-01-19'); + expect(sundayResult).toMatch(/Sun/i); + }); + + it('should respect configured locale for weekday names', () => { + // Change locale to German + Object.defineProperty(mockDateTimeFormatService, 'currentLocale', { + get: () => 'de-DE', + }); + + // Wednesday in German is "Mi" (Mittwoch) + const result = pipe.transform('2025-01-15'); + expect(result).toMatch(/Mi/i); + }); + + it('should pass through non-date strings that are not "No date"', () => { + // "No tag", "No project" etc. should pass through unchanged + const result = pipe.transform('No tag'); + expect(result).toBe('No tag'); + }); + + it('should handle invalid date strings gracefully', () => { + const result = pipe.transform('invalid-date'); + expect(result).toBe('invalid-date'); + }); +}); diff --git a/src/app/ui/pipes/scheduled-date-group.pipe.ts b/src/app/ui/pipes/scheduled-date-group.pipe.ts new file mode 100644 index 000000000..14007310c --- /dev/null +++ b/src/app/ui/pipes/scheduled-date-group.pipe.ts @@ -0,0 +1,57 @@ +import { inject, Pipe, PipeTransform } from '@angular/core'; +import { TranslateService } from '@ngx-translate/core'; +import { T } from 'src/app/t.const'; +import { getDbDateStr } from '../../util/get-db-date-str'; +import { dateStrToUtcDate } from '../../util/date-str-to-utc-date'; +import { DateTimeFormatService } from '../../core/date-time-format/date-time-format.service'; + +const DATE_REGEX = /^\d{4}-\d{2}-\d{2}$/; + +/** + * Pipe that formats scheduled date group keys with day of week. + * Input: YYYY-MM-DD date string or special strings like "No date" + * Output: "Wed 1/15" or "Today" or passthrough for non-date strings + */ +@Pipe({ + name: 'scheduledDateGroup', + standalone: true, + pure: false, +}) +export class ScheduledDateGroupPipe implements PipeTransform { + private _dateTimeFormatService = inject(DateTimeFormatService); + private _translateService = inject(TranslateService); + + transform(value: unknown): string | null { + if (value === null || value === undefined) { + return null; + } + + // Ensure value is a string + if (typeof value !== 'string') { + return String(value); + } + + // Check if it's a date string (YYYY-MM-DD format) + if (!DATE_REGEX.test(value)) { + // Pass through non-date strings like "No date", "No tag", etc. + return value; + } + + const todayStr = getDbDateStr(); + if (value === todayStr) { + return this._translateService.instant(T.G.TODAY_TAG_TITLE); + } + + const date = dateStrToUtcDate(value); + const locale = this._dateTimeFormatService.currentLocale; + + // Format with weekday and date: "Wed 1/15" + const formatter = new Intl.DateTimeFormat(locale, { + weekday: 'short', + month: 'numeric', + day: 'numeric', + }); + + return formatter.format(date); + } +}