mirror of
https://github.com/johannesjo/super-productivity.git
synced 2026-07-17 16:37:43 +00:00
feat(schedule): add a single-day view to the Schedule tab (#9058)
* feat(schedule): allow 'day' as a persisted time-view mode * feat(schedule): compute single-day range and header for day view * feat(schedule): add day-view toggle and single-day labels Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JzGvAaeT2TrZcEUA6fYPSZ * docs(schedule): note the day view mode in the Schedule wiki * fix(schedule): make the day-view header compact and locale-aware Address review feedback on the single-day view: - Build the day title with a locale-aware Intl skeleton instead of a hardcoded 'EEE, MMM d, yyyy' pattern, which pinned en-US field ordering. - Shrink the title responsively so it never gets ellipsis-clipped: show the full weekday + date + year on roomy widths, and fall back to a compact month + day below TABLET, where the toggle group and nav controls leave too little room. The weekday still shows in the day-column header. - Rename the .day-view-btn test hook to .e2e-day-view-btn per convention. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * refactor(schedule): rename week-month-* classes to time-view-* The toggle group now has three buttons (Day, Week, Month), so the week-month-selector / week-month-btn names no longer describe it. Rename to time-view-selector / time-view-btn. Pure CSS class rename, no behavior change. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * refactor(schedule): complete time-view-* rename in zen theme The class rename left zen.css targeting .week-month-*, so its overrides matched nothing. The :not(.active) rule is the load-bearing one: its !important suppresses the toggle's hover highlight, which zen exists to strip. Co-authored-by: Jon Kilroy <jkusa@users.noreply.github.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: Johannes Millan <johannes.millan@gmail.com> Co-authored-by: Jon Kilroy <jkusa@users.noreply.github.com>
This commit is contained in:
parent
5097155e25
commit
d5afe62016
10 changed files with 309 additions and 24 deletions
|
|
@ -35,7 +35,7 @@ Tasks that run past midnight are continued on the next day, so you see the full
|
|||
|
||||
## What You See
|
||||
|
||||
The Schedule shows a range of days (the upcoming 30 days). How many days are visible at once depends on your screen size and the view mode (week or month): fewer on narrow screens, more on wide ones.
|
||||
The Schedule shows a range of days (the upcoming 30 days). How many days are visible at once depends on the view mode and your screen size: the **day** view shows a single day, while the **week** and **month** views show fewer days on narrow screens and more on wide ones.
|
||||
|
||||
## Planning Vs Scheduling
|
||||
|
||||
|
|
|
|||
97
e2e/tests/schedule/schedule-day-view.spec.ts
Normal file
97
e2e/tests/schedule/schedule-day-view.spec.ts
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
import { expect, test } from '../../fixtures/test.fixture';
|
||||
|
||||
/** Format a Date as the app's day key (YYYY-MM-DD in local time). */
|
||||
const dbDate = (d: Date): string => {
|
||||
const y = d.getFullYear();
|
||||
const m = String(d.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(d.getDate()).padStart(2, '0');
|
||||
return `${y}-${m}-${day}`;
|
||||
};
|
||||
|
||||
// schedule-week draws two [data-day] cells per day (main + end-of-day) and
|
||||
// schedule-month also uses [data-day], so scope day counting to the main
|
||||
// week columns only.
|
||||
const DAY_COL = 'schedule-week .col:not(.end-of-day)[data-day]';
|
||||
|
||||
const DESKTOP_VIEWPORT = { width: 1280, height: 720 };
|
||||
|
||||
test.describe('Schedule day view', () => {
|
||||
test.use({ viewport: DESKTOP_VIEWPORT });
|
||||
|
||||
test('renders one day, navigates a day at a time, and switches views', async ({
|
||||
page,
|
||||
workViewPage,
|
||||
}) => {
|
||||
await workViewPage.waitForTaskList();
|
||||
await page.getByRole('menuitem', { name: 'Schedule' }).click();
|
||||
|
||||
const dayCols = page.locator(DAY_COL);
|
||||
const dayBtn = page.getByRole('button', { name: 'View Day' });
|
||||
const weekBtn = page.getByRole('button', { name: 'View Week' });
|
||||
const monthBtn = page.getByRole('button', { name: 'View Month' });
|
||||
const nextBtn = page.getByRole('button', { name: 'Next Day' });
|
||||
const prevBtn = page.getByRole('button', { name: 'Previous Day' });
|
||||
const todayBtn = page.locator('schedule .today-btn');
|
||||
// Scope to the header container: task events also render a `.title`, so a
|
||||
// bare `schedule .title` would be ambiguous once the schedule has events.
|
||||
const title = page.locator('schedule .schedule-nav-controls .title');
|
||||
const scheduleWeek = page.locator('schedule-week');
|
||||
const scheduleMonth = page.locator('schedule-month');
|
||||
|
||||
const today = dbDate(new Date());
|
||||
// Calendar-based next day (DST-safe) so it matches the app's setDate(+1) logic.
|
||||
const tomorrowDate = new Date();
|
||||
tomorrowDate.setDate(tomorrowDate.getDate() + 1);
|
||||
const tomorrow = dbDate(tomorrowDate);
|
||||
|
||||
// --- Day view renders exactly one day (today), single-date header ---
|
||||
await dayBtn.click();
|
||||
await expect(dayCols).toHaveCount(1);
|
||||
await expect(dayCols.first()).toHaveAttribute('data-day', today);
|
||||
await expect(dayBtn).toHaveAttribute('aria-pressed', 'true');
|
||||
// Header is a single en-GB date ("Fri, 17 Jul 2026"; locale pinned in
|
||||
// config), not a week range.
|
||||
await expect(title).toHaveText(/^\w+, \d{1,2} \w+ \d{4}$/);
|
||||
// Viewing today: cannot go earlier, "today" reset is disabled.
|
||||
await expect(prevBtn).toBeDisabled();
|
||||
await expect(todayBtn).toBeDisabled();
|
||||
|
||||
// --- Navigation moves exactly one day at a time ---
|
||||
await nextBtn.click();
|
||||
await expect(dayCols).toHaveCount(1);
|
||||
await expect(dayCols.first()).toHaveAttribute('data-day', tomorrow);
|
||||
// Off today, both "back to today" affordances become enabled.
|
||||
await expect(prevBtn).toBeEnabled();
|
||||
await expect(todayBtn).toBeEnabled();
|
||||
|
||||
// Prev steps back to today (snap-to-today) and re-locks.
|
||||
await prevBtn.click();
|
||||
await expect(dayCols.first()).toHaveAttribute('data-day', today);
|
||||
await expect(prevBtn).toBeDisabled();
|
||||
|
||||
// The Today button also resets to today after navigating away (distinct
|
||||
// code path from Prev: goToToday() vs goToPreviousPeriod()).
|
||||
await nextBtn.click();
|
||||
await expect(dayCols.first()).toHaveAttribute('data-day', tomorrow);
|
||||
await todayBtn.click();
|
||||
await expect(dayCols.first()).toHaveAttribute('data-day', today);
|
||||
await expect(todayBtn).toBeDisabled();
|
||||
|
||||
// --- Switching views renders the expected grid each time ---
|
||||
await weekBtn.click(); // day -> week: week grid, more than one day
|
||||
await expect(weekBtn).toHaveAttribute('aria-pressed', 'true');
|
||||
await expect(scheduleWeek).toBeVisible();
|
||||
// week shows multiple day columns (retry-safe; not coupled to the exact count)
|
||||
await expect.poll(() => dayCols.count()).toBeGreaterThan(1);
|
||||
|
||||
await monthBtn.click(); // week -> month: month grid replaces the week grid
|
||||
await expect(monthBtn).toHaveAttribute('aria-pressed', 'true');
|
||||
await expect(scheduleMonth).toBeVisible();
|
||||
await expect(scheduleWeek).toHaveCount(0);
|
||||
|
||||
await dayBtn.click(); // month -> day: back to a single day column
|
||||
await expect(dayBtn).toHaveAttribute('aria-pressed', 'true');
|
||||
await expect(scheduleMonth).toHaveCount(0);
|
||||
await expect(dayCols).toHaveCount(1);
|
||||
});
|
||||
});
|
||||
|
|
@ -61,7 +61,7 @@ export class LayoutService {
|
|||
select(selectIsShowIssuePanel),
|
||||
);
|
||||
|
||||
readonly selectedTimeView = signal<'week' | 'month'>('week');
|
||||
readonly selectedTimeView = signal<'week' | 'month' | 'day'>('week');
|
||||
readonly isWorkViewScrolled = signal<boolean>(false);
|
||||
readonly isShowAddTaskBar = toSignal(this.isShowAddTaskBar$, { initialValue: false });
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,21 @@
|
|||
<div class="schedule-nav-controls">
|
||||
<div class="week-month-selector">
|
||||
<div class="time-view-selector">
|
||||
<button
|
||||
mat-icon-button
|
||||
class="week-month-btn"
|
||||
[class.active]="!isMonthView()"
|
||||
[attr.aria-pressed]="!isMonthView()"
|
||||
class="time-view-btn e2e-day-view-btn"
|
||||
[class.active]="isDayView()"
|
||||
[attr.aria-pressed]="isDayView()"
|
||||
(click)="selectTimeView('day')"
|
||||
[matTooltip]="T.F.SCHEDULE.VIEW_DAY | translate"
|
||||
[attr.aria-label]="T.F.SCHEDULE.VIEW_DAY | translate"
|
||||
>
|
||||
<mat-icon>calendar_view_day</mat-icon>
|
||||
</button>
|
||||
<button
|
||||
mat-icon-button
|
||||
class="time-view-btn"
|
||||
[class.active]="isWeekView()"
|
||||
[attr.aria-pressed]="isWeekView()"
|
||||
(click)="selectTimeView('week')"
|
||||
[matTooltip]="T.F.SCHEDULE.VIEW_WEEK | translate"
|
||||
[attr.aria-label]="T.F.SCHEDULE.VIEW_WEEK | translate"
|
||||
|
|
@ -13,7 +24,7 @@
|
|||
</button>
|
||||
<button
|
||||
mat-icon-button
|
||||
class="week-month-btn"
|
||||
class="time-view-btn"
|
||||
[class.active]="isMonthView()"
|
||||
[attr.aria-pressed]="isMonthView()"
|
||||
(click)="selectTimeView('month')"
|
||||
|
|
@ -68,12 +79,20 @@
|
|||
(click)="goToPreviousPeriod()"
|
||||
[disabled]="isViewingToday()"
|
||||
[attr.aria-label]="
|
||||
(isMonthView() ? T.F.SCHEDULE.PREVIOUS_MONTH : T.F.SCHEDULE.PREVIOUS_WEEK)
|
||||
| translate
|
||||
(isMonthView()
|
||||
? T.F.SCHEDULE.PREVIOUS_MONTH
|
||||
: isDayView()
|
||||
? T.F.SCHEDULE.PREVIOUS_DAY
|
||||
: T.F.SCHEDULE.PREVIOUS_WEEK
|
||||
) | translate
|
||||
"
|
||||
[matTooltip]="
|
||||
(isMonthView() ? T.F.SCHEDULE.PREVIOUS_MONTH : T.F.SCHEDULE.PREVIOUS_WEEK)
|
||||
| translate
|
||||
(isMonthView()
|
||||
? T.F.SCHEDULE.PREVIOUS_MONTH
|
||||
: isDayView()
|
||||
? T.F.SCHEDULE.PREVIOUS_DAY
|
||||
: T.F.SCHEDULE.PREVIOUS_WEEK
|
||||
) | translate
|
||||
"
|
||||
>
|
||||
<mat-icon>chevron_left</mat-icon>
|
||||
|
|
@ -94,10 +113,20 @@
|
|||
mat-icon-button
|
||||
(click)="goToNextPeriod()"
|
||||
[attr.aria-label]="
|
||||
(isMonthView() ? T.F.SCHEDULE.NEXT_MONTH : T.F.SCHEDULE.NEXT_WEEK) | translate
|
||||
(isMonthView()
|
||||
? T.F.SCHEDULE.NEXT_MONTH
|
||||
: isDayView()
|
||||
? T.F.SCHEDULE.NEXT_DAY
|
||||
: T.F.SCHEDULE.NEXT_WEEK
|
||||
) | translate
|
||||
"
|
||||
[matTooltip]="
|
||||
(isMonthView() ? T.F.SCHEDULE.NEXT_MONTH : T.F.SCHEDULE.NEXT_WEEK) | translate
|
||||
(isMonthView()
|
||||
? T.F.SCHEDULE.NEXT_MONTH
|
||||
: isDayView()
|
||||
? T.F.SCHEDULE.NEXT_DAY
|
||||
: T.F.SCHEDULE.NEXT_WEEK
|
||||
) | translate
|
||||
"
|
||||
>
|
||||
<mat-icon>chevron_right</mat-icon>
|
||||
|
|
|
|||
|
|
@ -85,7 +85,7 @@
|
|||
align-items: center;
|
||||
}
|
||||
|
||||
.week-month-selector {
|
||||
.time-view-selector {
|
||||
display: flex;
|
||||
margin-left: var(--s);
|
||||
border-radius: var(--card-border-radius);
|
||||
|
|
@ -94,7 +94,7 @@
|
|||
background: var(--bg-lighter);
|
||||
}
|
||||
|
||||
.week-month-btn {
|
||||
.time-view-btn {
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
transition: var(--transition-standard);
|
||||
|
|
@ -111,7 +111,7 @@
|
|||
color: white;
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
&:not(:last-child) {
|
||||
border-right: 1px solid var(--extra-border-color);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -92,7 +92,9 @@ describe('ScheduleComponent', () => {
|
|||
);
|
||||
|
||||
mockGlobalConfigService = jasmine.createSpyObj('GlobalConfigService', [], {
|
||||
localization: signal({ firstDayOfWeek: 1 }),
|
||||
// Pin the date locale so Intl-formatted headers are deterministic across
|
||||
// runners (an en-GB runner would render "20 Jan", not "Jan 20").
|
||||
localization: signal({ firstDayOfWeek: 1, dateTimeLocale: 'en-US' }),
|
||||
cfg: signal(undefined),
|
||||
});
|
||||
|
||||
|
|
@ -274,6 +276,22 @@ describe('ScheduleComponent', () => {
|
|||
});
|
||||
|
||||
describe('goToPreviousPeriod', () => {
|
||||
it('should go back exactly one day in day view', () => {
|
||||
mockLayoutService.selectedTimeView.set('day');
|
||||
mockScheduleService.getDaysToShow.and.returnValue(['2027-06-15']);
|
||||
component['_selectedDate'].set(new Date(2027, 5, 15)); // Jun 15, 2027 (future → no snap-to-today)
|
||||
fixture.detectChanges();
|
||||
expect(component.daysToShow().length).toBe(1);
|
||||
|
||||
component.goToPreviousPeriod();
|
||||
|
||||
const d = component['_selectedDate']();
|
||||
expect(d?.getFullYear()).toBe(2027);
|
||||
expect(d?.getMonth()).toBe(5);
|
||||
expect(d?.getDate()).toBe(14); // back by exactly one day
|
||||
expect(d?.getHours()).toBe(0); // normalized to midnight
|
||||
});
|
||||
|
||||
it('should navigate backward by the number of days currently shown', () => {
|
||||
// Arrange - view a future range that doesn't contain today
|
||||
mockScheduleService.getDaysToShow.and.returnValue([
|
||||
|
|
@ -343,6 +361,22 @@ describe('ScheduleComponent', () => {
|
|||
});
|
||||
|
||||
describe('goToNextPeriod', () => {
|
||||
it('should advance exactly one day in day view', () => {
|
||||
mockLayoutService.selectedTimeView.set('day');
|
||||
mockScheduleService.getDaysToShow.and.returnValue(['2027-06-15']);
|
||||
component['_selectedDate'].set(new Date(2027, 5, 15)); // Jun 15, 2027
|
||||
fixture.detectChanges();
|
||||
expect(component.daysToShow().length).toBe(1);
|
||||
|
||||
component.goToNextPeriod();
|
||||
|
||||
const d = component['_selectedDate']();
|
||||
expect(d?.getFullYear()).toBe(2027);
|
||||
expect(d?.getMonth()).toBe(5);
|
||||
expect(d?.getDate()).toBe(16); // advanced by exactly one day
|
||||
expect(d?.getHours()).toBe(0); // normalized to midnight
|
||||
});
|
||||
|
||||
it('should navigate forward by the number of days currently shown', () => {
|
||||
// Arrange
|
||||
const startDate = new Date(2026, 0, 20); // Jan 20, 2026
|
||||
|
|
@ -786,6 +820,12 @@ describe('ScheduleComponent', () => {
|
|||
});
|
||||
|
||||
describe('shouldEnableHorizontalScroll computed', () => {
|
||||
it('should return false in day view', () => {
|
||||
mockLayoutService.selectedTimeView.set('day');
|
||||
fixture.detectChanges();
|
||||
expect(component.shouldEnableHorizontalScroll()).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false in month view regardless of window size', () => {
|
||||
// Arrange
|
||||
mockLayoutService.selectedTimeView.set('month');
|
||||
|
|
@ -915,4 +955,99 @@ describe('ScheduleComponent', () => {
|
|||
expect(weeks).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('day view persistence', () => {
|
||||
afterEach(() => localStorage.removeItem('SELECTED_TIME_VIEW'));
|
||||
|
||||
it('persists the day view and reads it back', () => {
|
||||
component.selectTimeView('day');
|
||||
expect(mockLayoutService.selectedTimeView()).toBe('day');
|
||||
expect(localStorage.getItem('SELECTED_TIME_VIEW')).toBe('day');
|
||||
// getTimeView is private; cast to reach it
|
||||
expect((component as any).getTimeView()).toBe('day');
|
||||
});
|
||||
|
||||
it('reads back month, and defaults to week for absent or unknown values', () => {
|
||||
localStorage.setItem('SELECTED_TIME_VIEW', 'month');
|
||||
expect((component as any).getTimeView()).toBe('month');
|
||||
|
||||
localStorage.removeItem('SELECTED_TIME_VIEW');
|
||||
expect((component as any).getTimeView()).toBe('week');
|
||||
|
||||
localStorage.setItem('SELECTED_TIME_VIEW', 'not-a-view');
|
||||
expect((component as any).getTimeView()).toBe('week');
|
||||
});
|
||||
});
|
||||
|
||||
describe('day view mode logic', () => {
|
||||
it('shows exactly one day in day mode', () => {
|
||||
mockScheduleService.getDaysToShow.and.returnValue(['2026-01-20']);
|
||||
mockLayoutService.selectedTimeView.set('day');
|
||||
fixture.detectChanges();
|
||||
expect((component as any)._daysToShowCount()).toBe(1);
|
||||
// Verify the count is actually wired into the day range (the mock returns
|
||||
// a fixed 1-element array regardless of args, so length alone is not proof).
|
||||
expect(mockScheduleService.getDaysToShow).toHaveBeenCalledWith(1, null);
|
||||
expect(component.daysToShow().length).toBe(1);
|
||||
expect(component.isDayView()).toBe(true);
|
||||
expect(component.isMonthView()).toBe(false);
|
||||
});
|
||||
|
||||
it('renders the full single-date header when roomy', () => {
|
||||
// Force the roomy (non-tablet) state so the full form is deterministic
|
||||
// regardless of the test runner's window width.
|
||||
component['_isTablet'] = signal(false);
|
||||
mockScheduleService.getDaysToShow.and.returnValue(['2026-01-20']);
|
||||
mockLayoutService.selectedTimeView.set('day');
|
||||
fixture.detectChanges();
|
||||
// 2026-01-20 is a Tuesday (en-US locale pinned in beforeEach).
|
||||
expect(component.headerTitle()).toBe('Tue, Jan 20, 2026');
|
||||
});
|
||||
|
||||
it('compacts the day header to month and day when tight', () => {
|
||||
component['_isTablet'] = signal(true);
|
||||
mockScheduleService.getDaysToShow.and.returnValue(['2026-01-20']);
|
||||
mockLayoutService.selectedTimeView.set('day');
|
||||
fixture.detectChanges();
|
||||
// Compact form is month + day only (no weekday, no year).
|
||||
expect(component.headerTitle()).toBe('Jan 20');
|
||||
});
|
||||
|
||||
it('exposes mutually exclusive view-mode flags', () => {
|
||||
mockLayoutService.selectedTimeView.set('week');
|
||||
fixture.detectChanges();
|
||||
expect(component.isWeekView()).toBe(true);
|
||||
expect(component.isDayView()).toBe(false);
|
||||
expect(component.isMonthView()).toBe(false);
|
||||
|
||||
mockLayoutService.selectedTimeView.set('day');
|
||||
fixture.detectChanges();
|
||||
expect(component.isDayView()).toBe(true);
|
||||
expect(component.isWeekView()).toBe(false);
|
||||
expect(component.isMonthView()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('day view toggle rendering', () => {
|
||||
afterEach(() => localStorage.removeItem('SELECTED_TIME_VIEW'));
|
||||
|
||||
it('renders a schedule-week (not schedule-month) with one day when in day view', () => {
|
||||
mockScheduleService.getDaysToShow.and.returnValue(['2026-01-20']);
|
||||
mockLayoutService.selectedTimeView.set('day');
|
||||
fixture.detectChanges();
|
||||
const el: HTMLElement = fixture.nativeElement;
|
||||
expect(el.querySelector('schedule-week')).toBeTruthy();
|
||||
expect(el.querySelector('schedule-month')).toBeFalsy();
|
||||
});
|
||||
|
||||
it('has a day-view toggle button that selects day mode', () => {
|
||||
const el: HTMLElement = fixture.nativeElement;
|
||||
const dayBtn = el.querySelector<HTMLButtonElement>(
|
||||
'.time-view-btn.e2e-day-view-btn',
|
||||
);
|
||||
expect(dayBtn).toBeTruthy();
|
||||
dayBtn!.click();
|
||||
expect(mockLayoutService.selectedTimeView()).toBe('day');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -103,6 +103,8 @@ export class ScheduleComponent {
|
|||
|
||||
private _currentTimeViewMode = computed(() => this.layoutService.selectedTimeView());
|
||||
isMonthView = computed(() => this._currentTimeViewMode() === 'month');
|
||||
isDayView = computed(() => this._currentTimeViewMode() === 'day');
|
||||
isWeekView = computed(() => this._currentTimeViewMode() === 'week');
|
||||
|
||||
// Navigation state - null = viewing today, Date = viewing selected date
|
||||
private _selectedDate = signal<Date | null>(null);
|
||||
|
|
@ -141,6 +143,8 @@ export class ScheduleComponent {
|
|||
const width = size.width;
|
||||
const height = size.height;
|
||||
|
||||
if (selectedView === 'day') return 1;
|
||||
|
||||
if (selectedView === 'month') {
|
||||
const availableHeight = height - SCHEDULE_CONSTANTS.MONTH_VIEW.HEADER_OFFSET;
|
||||
const minHeightPerWeek =
|
||||
|
|
@ -189,12 +193,24 @@ export class ScheduleComponent {
|
|||
private _isVeryCompact = computed(
|
||||
() => this._windowSize().width < SCHEDULE_CONSTANTS.BREAKPOINTS.XXS,
|
||||
);
|
||||
private _isTablet = computed(
|
||||
() => this._windowSize().width < SCHEDULE_CONSTANTS.BREAKPOINTS.TABLET,
|
||||
);
|
||||
|
||||
headerTitle = computed(() => {
|
||||
const days = this.daysToShow();
|
||||
if (!days.length) return '';
|
||||
const locale = this._dateTimeFormatService.currentLocale();
|
||||
|
||||
if (this.isDayView()) {
|
||||
// On tablet width and below the full date clips, so drop to month + day
|
||||
// (the weekday still shows in the day-column header).
|
||||
const dayOpts: Intl.DateTimeFormatOptions = this._isTablet()
|
||||
? { month: 'short', day: 'numeric' }
|
||||
: { weekday: 'short', month: 'short', day: 'numeric', year: 'numeric' };
|
||||
return new Intl.DateTimeFormat(locale, dayOpts).format(parseDbDateStr(days[0]));
|
||||
}
|
||||
|
||||
if (this.isMonthView()) {
|
||||
const mid = parseDbDateStr(days[Math.floor(days.length / 2)]);
|
||||
return safeFormatDate(mid, 'LLLL yyyy', locale);
|
||||
|
|
@ -404,14 +420,16 @@ export class ScheduleComponent {
|
|||
});
|
||||
}
|
||||
|
||||
selectTimeView(view: 'week' | 'month'): void {
|
||||
selectTimeView(view: 'week' | 'month' | 'day'): void {
|
||||
this.layoutService.selectedTimeView.set(view);
|
||||
localStorage.setItem(LS.SELECTED_TIME_VIEW, view);
|
||||
}
|
||||
|
||||
private getTimeView(): 'week' | 'month' {
|
||||
private getTimeView(): 'week' | 'month' | 'day' {
|
||||
const preservedView = localStorage.getItem(LS.SELECTED_TIME_VIEW);
|
||||
return preservedView === 'month' ? 'month' : 'week';
|
||||
if (preservedView === 'month') return 'month';
|
||||
if (preservedView === 'day') return 'day';
|
||||
return 'week';
|
||||
}
|
||||
|
||||
constructor() {
|
||||
|
|
|
|||
|
|
@ -1161,11 +1161,14 @@ const T = {
|
|||
INSERT_BEFORE: 'F.SCHEDULE.INSERT_BEFORE',
|
||||
LUNCH_BREAK: 'F.SCHEDULE.LUNCH_BREAK',
|
||||
MONTH: 'F.SCHEDULE.MONTH',
|
||||
NEXT_DAY: 'F.SCHEDULE.NEXT_DAY',
|
||||
NEXT_MONTH: 'F.SCHEDULE.NEXT_MONTH',
|
||||
NEXT_WEEK: 'F.SCHEDULE.NEXT_WEEK',
|
||||
PREVIOUS_DAY: 'F.SCHEDULE.PREVIOUS_DAY',
|
||||
PREVIOUS_MONTH: 'F.SCHEDULE.PREVIOUS_MONTH',
|
||||
PREVIOUS_WEEK: 'F.SCHEDULE.PREVIOUS_WEEK',
|
||||
PLAN_TASK_FOR_DAY_PLACEHOLDER: 'F.SCHEDULE.PLAN_TASK_FOR_DAY_PLACEHOLDER',
|
||||
VIEW_DAY: 'F.SCHEDULE.VIEW_DAY',
|
||||
VIEW_MONTH: 'F.SCHEDULE.VIEW_MONTH',
|
||||
VIEW_WEEK: 'F.SCHEDULE.VIEW_WEEK',
|
||||
NO_TASKS: 'F.SCHEDULE.NO_TASKS',
|
||||
|
|
|
|||
|
|
@ -1139,11 +1139,14 @@
|
|||
"INSERT_BEFORE": "Before",
|
||||
"LUNCH_BREAK": "Lunch Break",
|
||||
"MONTH": "Month",
|
||||
"NEXT_DAY": "Next Day",
|
||||
"NEXT_MONTH": "Next Month",
|
||||
"NEXT_WEEK": "Next Week",
|
||||
"PREVIOUS_DAY": "Previous Day",
|
||||
"PREVIOUS_MONTH": "Previous Month",
|
||||
"PREVIOUS_WEEK": "Previous Week",
|
||||
"PLAN_TASK_FOR_DAY_PLACEHOLDER": "Plan task for day...",
|
||||
"VIEW_DAY": "View Day",
|
||||
"VIEW_MONTH": "View Month",
|
||||
"VIEW_WEEK": "View Week",
|
||||
"NO_TASKS": "Currently there are no tasks. Please add some tasks via the plus (+) button in the top bar.",
|
||||
|
|
|
|||
|
|
@ -257,17 +257,17 @@ body.isDarkTheme.hasBgImage schedule {
|
|||
--bg-lighter: rgba(19, 19, 20, 0.85);
|
||||
}
|
||||
|
||||
/* Flatten week/month selector in main header */
|
||||
body .week-month-selector {
|
||||
/* Flatten day/week/month selector in main header */
|
||||
body .time-view-selector {
|
||||
box-shadow: none !important;
|
||||
border: 1px solid rgba(128, 128, 128, 0.2);
|
||||
}
|
||||
|
||||
body.isDarkTheme .week-month-selector {
|
||||
body.isDarkTheme .time-view-selector {
|
||||
border-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
body .week-month-btn:not(.active) {
|
||||
body .time-view-btn:not(.active) {
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue