diff --git a/src/app/features/schedule/schedule.constants.ts b/src/app/features/schedule/schedule.constants.ts index d40a6d4ba2..e756cad62a 100644 --- a/src/app/features/schedule/schedule.constants.ts +++ b/src/app/features/schedule/schedule.constants.ts @@ -18,8 +18,12 @@ export const SCHEDULE_CONSTANTS = { BREAKPOINTS: { /** Width threshold for tablet devices (768px) */ TABLET: 768, + /** Width threshold below which the schedule header switches to its compact form. Mirrors `$layout-xs` in `_media-queries.scss`. */ + XS: 600, /** Width threshold for mobile devices (480px) */ MOBILE: 480, + /** Width threshold below which the schedule header drops the date range and shows only the week number. */ + XXS: 420, }, /** diff --git a/src/app/features/schedule/schedule/schedule.component.html b/src/app/features/schedule/schedule/schedule.component.html index ea6beb4164..ae5d483d1f 100644 --- a/src/app/features/schedule/schedule/schedule.component.html +++ b/src/app/features/schedule/schedule/schedule.component.html @@ -22,6 +22,44 @@ + @if (showCalFilterBtn()) { + + + +
+ @for (provider of enabledCalendarProviders(); track provider.id) { + + } +
+
+
+ } +
-@if (showCalFilterBar()) { - - @for (provider of enabledCalendarProviders(); track provider.id) { - - @if (provider.color) { - - } - {{ calProviderInitials(provider) }} - - } - -} -
{ }); }); - describe('showCalFilterBar computed', () => { + describe('showCalFilterBtn computed', () => { const makeProvider = (id: string): any => ({ id, isEnabled: true, @@ -725,7 +725,7 @@ describe('ScheduleComponent', () => { store.overrideSelector(selectCalendarProviders, []); store.refreshState(); fixture.detectChanges(); - expect(component.showCalFilterBar()).toBe(false); + expect(component.showCalFilterBtn()).toBe(false); }); it('should be false with a single visible provider', () => { @@ -733,7 +733,7 @@ describe('ScheduleComponent', () => { store.overrideSelector(selectCalendarProviders, [makeProvider('only')]); store.refreshState(); fixture.detectChanges(); - expect(component.showCalFilterBar()).toBe(false); + expect(component.showCalFilterBtn()).toBe(false); }); it('should be true when the only enabled provider is hidden (C2 regression)', () => { @@ -743,7 +743,7 @@ describe('ScheduleComponent', () => { const hidden = TestBed.inject(HiddenCalendarProvidersService); hidden.setHidden(['only']); fixture.detectChanges(); - expect(component.showCalFilterBar()).toBe(true); + expect(component.showCalFilterBtn()).toBe(true); }); it('should be true with multiple enabled providers', () => { @@ -754,7 +754,7 @@ describe('ScheduleComponent', () => { ]); store.refreshState(); fixture.detectChanges(); - expect(component.showCalFilterBar()).toBe(true); + expect(component.showCalFilterBtn()).toBe(true); }); }); diff --git a/src/app/features/schedule/schedule/schedule.component.ts b/src/app/features/schedule/schedule/schedule.component.ts index 995dc85403..86caae5739 100644 --- a/src/app/features/schedule/schedule/schedule.component.ts +++ b/src/app/features/schedule/schedule/schedule.component.ts @@ -11,16 +11,14 @@ import { fromEvent } from 'rxjs'; import { select, Store } from '@ngrx/store'; import { selectCalendarProviders } from '../../issue/store/issue-provider.selectors'; import { HiddenCalendarProvidersService } from '../../calendar-integration/hidden-calendar-providers.service'; -import { - getIssueProviderInitials, - getIssueProviderTooltip, -} from '../../issue/mapping-helper/get-issue-provider-tooltip'; +import { getIssueProviderTooltip } from '../../issue/mapping-helper/get-issue-provider-tooltip'; import { IssueProvider } from '../../issue/issue.model'; import { - MatChipListbox, - MatChipListboxChange, - MatChipOption, -} from '@angular/material/chips'; + MatMenu, + MatMenuContent, + MatMenuItem, + MatMenuTrigger, +} from '@angular/material/menu'; import { debounceTime, map, startWith } from 'rxjs/operators'; import { safeFormatDate } from '../../../util/safe-format-date'; import { TaskService } from '../../tasks/task.service'; @@ -55,8 +53,10 @@ import { parseDbDateStr } from '../../../util/parse-db-date-str'; MatIcon, MatTooltip, TranslatePipe, - MatChipListbox, - MatChipOption, + MatMenu, + MatMenuContent, + MatMenuItem, + MatMenuTrigger, ], templateUrl: './schedule.component.html', styleUrls: ['./schedule.component.scss'], @@ -86,26 +86,19 @@ export class ScheduleComponent { .pipe(map((ps) => ps.filter((p) => p.isEnabled))), { initialValue: [] }, ); - // Show the bar with multiple providers, OR with a single provider that is - // currently hidden — otherwise the user has no UI to re-enable the only + // Show the button with multiple providers, OR with a single provider that + // is currently hidden — otherwise the user has no UI to re-enable the only // calendar after, e.g., deleting all but one provider in settings. - readonly showCalFilterBar = computed(() => { + readonly showCalFilterBtn = computed(() => { const providers = this.enabledCalendarProviders(); if (providers.length > 1) return true; const hidden = this.hiddenCalendarProviderIds(); return providers.some((p) => hidden.includes(p.id)); }); readonly calProviderLabel = (p: IssueProvider): string => getIssueProviderTooltip(p); - readonly calProviderInitials = (p: IssueProvider): string => - getIssueProviderInitials(p) ?? - getIssueProviderTooltip(p).substring(0, 2).toUpperCase(); - onCalProvidersChange(ev: MatChipListboxChange): void { - const visible = new Set((ev.value as string[]) ?? []); - const hidden = this.enabledCalendarProviders() - .map((p) => p.id) - .filter((id) => !visible.has(id)); - this._hiddenCalendarProviders.setHidden(hidden); + toggleCalProvider(providerId: string): void { + this._hiddenCalendarProviders.toggle(providerId); } private _currentTimeViewMode = computed(() => this.layoutService.selectedTimeView()); @@ -188,6 +181,15 @@ export class ScheduleComponent { weeksToShow = computed(() => Math.ceil(this.daysToShow().length / 7)); + // Memoized so headerTitle only re-runs at the breakpoint boundary, + // not on every debounced resize tick. + private _isCompact = computed( + () => this._windowSize().width < SCHEDULE_CONSTANTS.BREAKPOINTS.XS, + ); + private _isVeryCompact = computed( + () => this._windowSize().width < SCHEDULE_CONSTANTS.BREAKPOINTS.XXS, + ); + headerTitle = computed(() => { const days = this.daysToShow(); if (!days.length) return ''; @@ -201,9 +203,17 @@ export class ScheduleComponent { const start = parseDbDateStr(days[0]); const end = parseDbDateStr(days[days.length - 1]); const weekNr = getWeekNumber(start); // ISO — default firstDayOfWeek=1 + const sameMonth = + start.getMonth() === end.getMonth() && start.getFullYear() === end.getFullYear(); const startStr = safeFormatDate(start, 'MMM d', locale); - const endStr = safeFormatDate(end, 'MMM d', locale); - const label = this._translate.instant(T.F.WORKLOG.CMP.WEEK_NR, { nr: weekNr }); + const endStr = sameMonth + ? safeFormatDate(end, 'd', locale) + : safeFormatDate(end, 'MMM d', locale); + const labelKey = this._isCompact() + ? T.F.WORKLOG.CMP.WEEK_NR_SHORT + : T.F.WORKLOG.CMP.WEEK_NR; + const label = this._translate.instant(labelKey, { nr: weekNr }); + if (this._isVeryCompact()) return label; return `${label} · ${startStr} – ${endStr}`; }); diff --git a/src/app/t.const.ts b/src/app/t.const.ts index c8269ec91c..d9163a6116 100644 --- a/src/app/t.const.ts +++ b/src/app/t.const.ts @@ -1984,6 +1984,7 @@ const T = { TOTAL_TIME: 'F.WORKLOG.CMP.TOTAL_TIME', VIEW_TASK_DETAILS: 'F.WORKLOG.CMP.VIEW_TASK_DETAILS', WEEK_NR: 'F.WORKLOG.CMP.WEEK_NR', + WEEK_NR_SHORT: 'F.WORKLOG.CMP.WEEK_NR_SHORT', WORKED: 'F.WORKLOG.CMP.WORKED', }, D_CONFIRM_RESTORE: 'F.WORKLOG.D_CONFIRM_RESTORE', diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index 7089e32323..7edc94b557 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -1940,6 +1940,7 @@ "TOTAL_TIME": "Time spent total:", "VIEW_TASK_DETAILS": "View task details", "WEEK_NR": "Week {{nr}}", + "WEEK_NR_SHORT": "W{{nr}}", "WORKED": "Worked" }, "D_CONFIRM_RESTORE": "Are you sure you want to move the task \"{{title}}\" into your Today task list?",