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?",