feat(shortcuts): add optional shortcuts for scheduling #8093 (#8189)

* feat(shortcuts): add optional shortcuts for scheduling tasks to tomorrow, next week, and next month #8093

This change adds configurable, unassigned-by-default keyboard shortcuts for scheduling tasks to Tomorrow, Next Week, and Next Month. It also renames the existing 'moveToTodaysTasks' shortcut to 'taskScheduleToday' to align with the new schedule shortcuts.

Closes #8093

* feat: add migration for renamed keyboard shortcut moveToTodaysTasks to taskScheduleToday

* docs: fix corrupted fragment in keyboard shortcuts wiki

* test: add unit tests for task scheduling shortcuts and fix migration typing

* refactor: use getNextWeekDayOffset helper and preserve time/reminders for timed tasks

* docs: clarify Next Month shortcut behavior

* fix(config): ensure keyboard shortcut migration is one-shot by stripping legacy keys

* fix(tasks): preserve reminder offset and show snack when rescheduling timed tasks

* fix(tasks): fix regression in scheduleTask call and add missing snackbar confirmation

* fix(tasks): align scheduling snack with planner effect formatting

Use LocaleDatePipe.shortDate and include getSnackExtraStr() so the
timed-task reschedule snack matches planTaskForDay's snack output.

---------

Co-authored-by: johannesjo <johannes.millan@gmail.com>
This commit is contained in:
Maikel Hajiabadi 2026-06-09 22:01:00 +02:00 committed by GitHub
parent 595e7cef2a
commit d2c989663e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 380 additions and 30 deletions

View file

@ -119,6 +119,8 @@ Shortcuts are stored in global config (`GlobalConfigState.keyboard`) and persist
**Task-level shortcuts:** Actions with a task focus (e.g. mark done, add subtask, schedule, delete, move) apply to the currently selected task. Selection is by keyboard (tab/arrows) or mouse. The [[3.02-Settings-and-Preferences]] keyboard help text describes these as applying to "the currently selected task."
Specifically for the **Next Month** shortcut, the task is scheduled for the **1st day of the next month**. This avoids issues with different month lengths (e.g., scheduling for the 31st when the next month only has 30 days).
**Schedule week view:** `Ctrl`+MouseWheel changes the vertical scale of the week schedule grid. The value is stored locally for future schedule visits.
**Focus Mode overlay:** `Escape` closes the overlay. If a focus session or break is running, it continues in the header focus button.
@ -128,7 +130,7 @@ Shortcuts are stored in global config (`GlobalConfigState.keyboard`) and persist
## Conflict and Precedence
- **Input focus:** When focus is in an input element or a blocking overlay is open, shortcut handling is skipped so typing is not interrupted.
- **Contextual Precedence:** Task-level shortcuts take precedence over global shortcuts when a task is currently focused. For example, pressing `Shift+S` opens the deadline dialog if a task is focused, but navigates to the Schedule view if no task is focused. Similarly, `Shift+T` moves the selected task to Today, but navigates to the Today view otherwise.
- **Contextual Precedence:** Task-level shortcuts take precedence over global shortcuts when a task is currently focused. For example, pressing `Shift+S` opens the deadline dialog if a task is focused, but navigates to the Schedule view if no task is focused. Similarly, `Shift+T` schedules the selected task for today, but navigates to the Today view otherwise.
- **Conflicts:** The configuration UI warns if two actions are assigned the same key combination. The user can press **Escape**, **Backspace**, or **Delete** in the shortcut field to clear an existing assignment.
## Storage and Definition

View file

@ -160,6 +160,10 @@ export const DEFAULT_GLOBAL_CONFIG: GlobalConfigState = {
taskOpenNotesFullscreen: null,
taskOpenEstimationDialog: 'T',
taskSchedule: 'S',
taskScheduleToday: 'Shift+T',
taskScheduleTomorrow: null,
taskScheduleNextWeek: null,
taskScheduleNextMonth: null,
taskScheduleDeadline: 'Shift+S',
taskUnschedule: 'U',
taskToggleDone: 'D',
@ -175,7 +179,6 @@ export const DEFAULT_GLOBAL_CONFIG: GlobalConfigState = {
moveTaskToTop: 'Ctrl+Alt+ArrowUp',
moveTaskToBottom: 'Ctrl+Alt+ArrowDown',
moveToBacklog: 'Shift+B',
moveToTodaysTasks: 'Shift+T',
expandSubTasks: null,
collapseSubTasks: null,
togglePlay: 'Y',

View file

@ -97,6 +97,10 @@ export const KEYBOARD_SETTINGS_FORM_CFG: ConfigFormSection<KeyboardConfig> = {
kbField('taskOpenNotesPanel', T.GCF.KEYBOARD.TASK_OPEN_NOTES_PANEL),
kbField('taskOpenEstimationDialog', T.GCF.KEYBOARD.TASK_OPEN_ESTIMATION_DIALOG),
kbField('taskSchedule', T.GCF.KEYBOARD.TASK_SCHEDULE),
kbField('taskScheduleToday', T.GCF.KEYBOARD.TASK_SCHEDULE_TODAY),
kbField('taskScheduleTomorrow', T.GCF.KEYBOARD.TASK_SCHEDULE_TOMORROW),
kbField('taskScheduleNextWeek', T.GCF.KEYBOARD.TASK_SCHEDULE_NEXT_WEEK),
kbField('taskScheduleNextMonth', T.GCF.KEYBOARD.TASK_SCHEDULE_NEXT_MONTH),
kbField('taskScheduleDeadline', T.GCF.KEYBOARD.TASK_SCHEDULE_DEADLINE),
kbField('taskUnschedule', T.GCF.KEYBOARD.TASK_UNSCHEDULE),
kbField('taskToggleDone', T.GCF.KEYBOARD.TASK_TOGGLE_DONE),
@ -113,7 +117,6 @@ export const KEYBOARD_SETTINGS_FORM_CFG: ConfigFormSection<KeyboardConfig> = {
kbField('moveTaskToTop', T.GCF.KEYBOARD.MOVE_TASK_TO_TOP),
kbField('moveTaskToBottom', T.GCF.KEYBOARD.MOVE_TASK_TO_BOTTOM),
kbField('moveToBacklog', T.GCF.KEYBOARD.MOVE_TO_BACKLOG),
kbField('moveToTodaysTasks', T.GCF.KEYBOARD.MOVE_TO_REGULARS_TASKS),
kbField('expandSubTasks', T.GCF.KEYBOARD.EXPAND_SUB_TASKS),
kbField('collapseSubTasks', T.GCF.KEYBOARD.COLLAPSE_SUB_TASKS),
kbField('togglePlay', T.GCF.KEYBOARD.TOGGLE_PLAY),

View file

@ -37,6 +37,10 @@ export type KeyboardConfig = Readonly<{
taskOpenContextMenu?: string | null;
taskDelete?: string | null;
taskSchedule?: string | null;
taskScheduleToday?: string | null;
taskScheduleTomorrow?: string | null;
taskScheduleNextWeek?: string | null;
taskScheduleNextMonth?: string | null;
taskScheduleDeadline?: string | null;
taskUnschedule?: string | null;
selectPreviousTask?: string | null;
@ -46,7 +50,6 @@ export type KeyboardConfig = Readonly<{
moveTaskToTop?: string | null;
moveTaskToBottom?: string | null;
moveToBacklog?: string | null;
moveToTodaysTasks?: string | null;
expandSubTasks?: string | null;
collapseSubTasks?: string | null;
togglePlay?: string | null;

View file

@ -234,6 +234,98 @@ describe('GlobalConfigReducer', () => {
expect(result.keyboard.addNewNote).toBe('N');
expect(result.keyboard.taskOpenNotesPanel).toBe('Alt+Shift+N');
});
describe('moveToTodaysTasks migration', () => {
it('should migrate moveToTodaysTasks to taskScheduleToday', () => {
const legacyConfig = {
...initialGlobalConfigState,
keyboard: {
...initialGlobalConfigState.keyboard,
moveToTodaysTasks: 'Shift+T',
taskScheduleToday: null,
},
};
const result = globalConfigReducer(
initialGlobalConfigState,
loadAllData({
appDataComplete: {
globalConfig: legacyConfig,
} as unknown as AppDataComplete,
}),
);
expect(result.keyboard.taskScheduleToday).toBe('Shift+T');
expect((result.keyboard as any).moveToTodaysTasks).toBeUndefined();
});
it('should NOT re-migrate if taskScheduleToday is already null (manually disabled)', () => {
const legacyConfigWithBoth = {
...initialGlobalConfigState,
keyboard: {
...initialGlobalConfigState.keyboard,
moveToTodaysTasks: 'Shift+T',
taskScheduleToday: null,
},
};
// First migration
const result1 = globalConfigReducer(
initialGlobalConfigState,
loadAllData({
appDataComplete: {
globalConfig: legacyConfigWithBoth,
} as unknown as AppDataComplete,
}),
);
expect(result1.keyboard.taskScheduleToday).toBe('Shift+T');
expect((result1.keyboard as any).moveToTodaysTasks).toBeUndefined();
// User disables it
const configWithDisabled = {
...result1,
keyboard: {
...result1.keyboard,
taskScheduleToday: null,
},
};
// Second load (e.g. restart)
const result2 = globalConfigReducer(
initialGlobalConfigState,
loadAllData({
appDataComplete: {
globalConfig: configWithDisabled,
} as unknown as AppDataComplete,
}),
);
expect(result2.keyboard.taskScheduleToday).toBeNull();
});
it('should strip moveToTodaysTasks even if no migration is needed', () => {
const legacyConfig = {
...initialGlobalConfigState,
keyboard: {
...initialGlobalConfigState.keyboard,
moveToTodaysTasks: 'Shift+T',
taskScheduleToday: 'Ctrl+T',
},
};
const result = globalConfigReducer(
initialGlobalConfigState,
loadAllData({
appDataComplete: {
globalConfig: legacyConfig,
} as unknown as AppDataComplete,
}),
);
expect(result.keyboard.taskScheduleToday).toBe('Ctrl+T');
expect((result.keyboard as any).moveToTodaysTasks).toBeUndefined();
});
});
});
it('should use syncProvider from snapshot when oldState has null (initial load)', () => {

View file

@ -106,14 +106,24 @@ export const initialGlobalConfigState: GlobalConfigState = {
};
const migrateKeyboardConfig = (cfg: KeyboardConfig | undefined): KeyboardConfig => {
const keyboard: KeyboardConfig = {
const { moveToTodaysTasks, ...rest } =
(cfg as (KeyboardConfig & { moveToTodaysTasks?: string | null }) | undefined) ?? {};
let keyboard: KeyboardConfig = {
...DEFAULT_GLOBAL_CONFIG.keyboard,
...cfg,
...rest,
};
if (moveToTodaysTasks != null && rest.taskScheduleToday == null) {
keyboard = {
...keyboard,
taskScheduleToday: moveToTodaysTasks,
};
}
if (
cfg?.addNewNote === 'N' &&
(cfg.taskOpenNotesPanel === undefined || cfg.taskOpenNotesPanel === null)
rest.addNewNote === 'N' &&
(rest.taskOpenNotesPanel === undefined || rest.taskOpenNotesPanel === null)
) {
return {
...keyboard,

View file

@ -26,6 +26,7 @@ import { isValidSplitTime } from '../../../util/is-valid-split-time';
import { normalizeClockStr } from '../../../util/normalize-clock-str';
import { getDbDateStr } from '../../../util/get-db-date-str';
import { dateStrToUtcDate } from '../../../util/date-str-to-utc-date';
import { getNextWeekDayOffset } from '../../../util/get-next-week-day-offset';
import { DateService } from '../../../core/date/date.service';
import { DateAdapter } from '@angular/material/core';
import { MatButton } from '@angular/material/button';
@ -235,11 +236,7 @@ export class DialogDeadlineComponent implements AfterViewInit {
d.setDate(d.getDate() + 1);
return d;
case 'nextWeek': {
const dayOffset =
(this._dateAdapter.getFirstDayOfWeek() -
this._dateAdapter.getDayOfWeek(d) +
7) %
7 || 7;
const dayOffset = getNextWeekDayOffset(this._dateAdapter, d);
d.setDate(d.getDate() + dayOffset);
return d;
}

View file

@ -247,7 +247,7 @@
<mat-icon>arrow_upward</mat-icon>
{{ T.F.TASK.CMP.MOVE_TO_REGULAR | translate }}
</span>
<span class="key-i">{{ kb.moveToTodaysTasks }}</span>
<span class="key-i">{{ kb.taskScheduleToday }}</span>
</button>
}

View file

@ -64,6 +64,7 @@ import { getDbDateStr } from '../../../../util/get-db-date-str';
import { PlannerActions } from '../../../planner/store/planner.actions';
import { addSubTask } from '../../../tasks/store/task.actions';
import { combineDateAndTime } from '../../../../util/combine-date-and-time';
import { getNextWeekDayOffset } from '../../../../util/get-next-week-day-offset';
import { DateAdapter } from '@angular/material/core';
import { ICAL_TYPE } from '../../../issue/issue.const';
import { IssueIconPipe } from '../../../issue/issue-icon/issue-icon.pipe';
@ -723,11 +724,7 @@ export class TaskContextMenuInnerComponent implements AfterViewInit, OnDestroy {
break;
case 3:
const nextFirstDayOfWeek = tDate;
const dayOffset =
(this._dateAdapter.getFirstDayOfWeek() -
this._dateAdapter.getDayOfWeek(nextFirstDayOfWeek) +
7) %
7 || 7;
const dayOffset = getNextWeekDayOffset(this._dateAdapter, nextFirstDayOfWeek);
nextFirstDayOfWeek.setDate(nextFirstDayOfWeek.getDate() + dayOffset);
this._schedule(nextFirstDayOfWeek);
break;

View file

@ -37,7 +37,7 @@ describe('TaskShortcutService', () => {
taskEditTags: 'G',
taskOpenContextMenu: null,
moveToBacklog: 'B',
moveToTodaysTasks: 'F',
taskScheduleToday: 'F',
selectPreviousTask: 'K',
selectNextTask: 'J',
collapseSubTasks: 'H',

View file

@ -169,6 +169,21 @@ export class TaskShortcutService {
ev.preventDefault();
return true;
}
if (checkKeyCombo(ev, keys.taskScheduleTomorrow)) {
this._handleTaskShortcut(focusedTaskId, 'scheduleTaskTomorrow');
ev.preventDefault();
return true;
}
if (checkKeyCombo(ev, keys.taskScheduleNextWeek)) {
this._handleTaskShortcut(focusedTaskId, 'scheduleTaskNextWeek');
ev.preventDefault();
return true;
}
if (checkKeyCombo(ev, keys.taskScheduleNextMonth)) {
this._handleTaskShortcut(focusedTaskId, 'scheduleTaskNextMonth');
ev.preventDefault();
return true;
}
if (checkKeyCombo(ev, keys.taskScheduleDeadline)) {
this._handleTaskShortcut(focusedTaskId, 'openDeadlineDialog');
ev.preventDefault();
@ -229,7 +244,7 @@ export class TaskShortcutService {
return true;
}
if (checkKeyCombo(ev, keys.moveToTodaysTasks)) {
if (checkKeyCombo(ev, keys.taskScheduleToday)) {
this._handleTaskShortcut(focusedTaskId, 'moveToTodayWithFocus');
ev.preventDefault();
ev.stopPropagation();

View file

@ -29,7 +29,7 @@
<button
(click)="parent.addToMyDay()"
title="{{ T.F.TASK.CMP.ADD_TO_MY_DAY | translate }} {{
kb.moveToTodaysTasks ? '[' + kb.moveToTodaysTasks + ']' : ''
kb.taskScheduleToday ? '[' + kb.taskScheduleToday + ']' : ''
}}"
class="ico-btn"
color=""

View file

@ -3,6 +3,8 @@ import { TestBed } from '@angular/core/testing';
import { MatDialog } from '@angular/material/dialog';
import { Store } from '@ngrx/store';
import { of } from 'rxjs';
import { DateAdapter } from '@angular/material/core';
import { PlannerActions } from '../../planner/store/planner.actions';
import { DateService } from '../../../core/date/date.service';
import { GlobalTrackingIntervalService } from '../../../core/global-tracking-interval/global-tracking-interval.service';
import { LayoutService } from '../../../core-ui/layout/layout.service';
@ -15,6 +17,10 @@ import { DEFAULT_TASK, HideSubTasksMode, TaskWithSubTasks } from '../task.model'
import { TaskService } from '../task.service';
import { WorkContextService } from '../../work-context/work-context.service';
import { TaskComponent } from './task.component';
import { SnackService } from '../../../core/snack/snack.service';
import { TranslateService } from '@ngx-translate/core';
import { LocaleDatePipe } from '../../../ui/pipes/locale-date.pipe';
import { PlannerService } from '../../planner/planner.service';
describe('TaskComponent shortcut handling', () => {
let fixture: import('@angular/core/testing').ComponentFixture<TaskComponent>;
@ -65,6 +71,7 @@ describe('TaskComponent shortcut handling', () => {
'pauseCurrent',
'getByIdWithSubTaskData$',
'focusTaskById',
'scheduleTask',
],
{
currentTaskId: signal<string | null>(null),
@ -101,7 +108,7 @@ describe('TaskComponent shortcut handling', () => {
{
provide: GlobalConfigService,
useValue: jasmine.createSpyObj('GlobalConfigService', ['cfg'], {
cfg: () => ({ keyboard: {}, tasks: {} }),
cfg: () => ({ keyboard: {}, tasks: {}, reminder: {} }),
}),
},
{
@ -112,6 +119,22 @@ describe('TaskComponent shortcut handling', () => {
]),
},
{ provide: Store, useValue: storeSpy },
{
provide: SnackService,
useValue: jasmine.createSpyObj('SnackService', ['open']),
},
{
provide: TranslateService,
useValue: jasmine.createSpyObj('TranslateService', ['instant']),
},
{
provide: LocaleDatePipe,
useValue: jasmine.createSpyObj('LocaleDatePipe', ['transform']),
},
{
provide: PlannerService,
useValue: jasmine.createSpyObj('PlannerService', ['getSnackExtraStr']),
},
{
provide: ProjectService,
useValue: jasmine.createSpyObj('ProjectService', [
@ -130,9 +153,13 @@ describe('TaskComponent shortcut handling', () => {
},
{
provide: DateService,
useValue: jasmine.createSpyObj('DateService', ['isToday'], {
isToday: () => false,
}),
useValue: jasmine.createSpyObj(
'DateService',
['isToday', 'getLogicalTodayDate'],
{
isToday: () => false,
},
),
},
{
provide: GlobalTrackingIntervalService,
@ -152,6 +179,13 @@ describe('TaskComponent shortcut handling', () => {
isTodayList: signal(false),
},
},
{
provide: DateAdapter,
useValue: jasmine.createSpyObj('DateAdapter', [
'getFirstDayOfWeek',
'getDayOfWeek',
]),
},
],
})
.overrideComponent(TaskComponent, {
@ -337,4 +371,116 @@ describe('TaskComponent shortcut handling', () => {
expect(taskServiceSpy.addSubTaskTo).not.toHaveBeenCalled();
});
describe('Scheduling shortcuts', () => {
let dateService: jasmine.SpyObj<DateService>;
let dateAdapter: jasmine.SpyObj<DateAdapter<unknown>>;
let plannerService: jasmine.SpyObj<PlannerService>;
beforeEach(() => {
dateService = TestBed.inject(DateService) as jasmine.SpyObj<DateService>;
dateAdapter = TestBed.inject(DateAdapter) as jasmine.SpyObj<DateAdapter<unknown>>;
plannerService = TestBed.inject(PlannerService) as jasmine.SpyObj<PlannerService>;
// Mock "logical today" to 2026-06-01 (a Monday)
dateService.getLogicalTodayDate.and.returnValue(new Date('2026-06-01T12:00:00'));
dateAdapter.getDayOfWeek.and.callFake((d: any) => (d as Date).getDay());
dateAdapter.getFirstDayOfWeek.and.returnValue(1); // Monday
plannerService.getSnackExtraStr.and.returnValue(Promise.resolve(''));
});
it('schedules for tomorrow', () => {
component.scheduleTaskTomorrow();
expect(storeSpy.dispatch).toHaveBeenCalledWith(
PlannerActions.planTaskForDay({
task: component.task() as any,
day: '2026-06-02',
isShowSnack: true,
}),
);
});
it('schedules for next week (next Monday)', () => {
component.scheduleTaskNextWeek();
// Next week from Monday June 1st should be June 8th
expect(storeSpy.dispatch).toHaveBeenCalledWith(
PlannerActions.planTaskForDay({
task: component.task() as any,
day: '2026-06-08',
isShowSnack: true,
}),
);
});
it('schedules for next week (from Sunday, next Monday)', () => {
dateService.getLogicalTodayDate.and.returnValue(new Date('2026-06-07T12:00:00')); // Sunday
component.scheduleTaskNextWeek();
// Next week from Sunday June 7th (first day Monday) should be June 8th
expect(storeSpy.dispatch).toHaveBeenCalledWith(
PlannerActions.planTaskForDay({
task: component.task() as any,
day: '2026-06-08',
isShowSnack: true,
}),
);
});
it('schedules for next week (from Sunday, next Monday) - US locale (Sunday first)', () => {
dateAdapter.getFirstDayOfWeek.and.returnValue(0); // Sunday
dateService.getLogicalTodayDate.and.returnValue(new Date('2026-06-07T12:00:00')); // Sunday
component.scheduleTaskNextWeek();
// Next week from Sunday June 7th (first day Sunday) should be June 14th
expect(storeSpy.dispatch).toHaveBeenCalledWith(
PlannerActions.planTaskForDay({
task: component.task() as any,
day: '2026-06-14',
isShowSnack: true,
}),
);
});
it('schedules for next month (first of next month)', () => {
component.scheduleTaskNextMonth();
expect(storeSpy.dispatch).toHaveBeenCalledWith(
PlannerActions.planTaskForDay({
task: component.task() as any,
day: '2026-07-01',
isShowSnack: true,
}),
);
});
it('preserves time and reminder when scheduling a timed task for tomorrow', async () => {
const timedTask = {
...component.task(),
dueWithTime: new Date('2026-06-01T10:00:00').getTime(),
};
fixture.componentRef.setInput('task', timedTask);
await component.scheduleTaskTomorrow();
// Should call taskService.scheduleTask instead of dispatching planTaskForDay
// June 2nd at 10:00:00
expect(taskServiceSpy.scheduleTask).toHaveBeenCalledWith(
timedTask as any,
new Date('2026-06-02T10:00:00').getTime(),
jasmine.any(String),
false,
);
expect(TestBed.inject(SnackService).open).toHaveBeenCalled();
expect(storeSpy.dispatch).not.toHaveBeenCalledWith(
PlannerActions.planTaskForDay({
task: timedTask as any,
day: '2026-06-02',
isShowSnack: true,
}),
);
});
});
});

View file

@ -57,13 +57,19 @@ import { TaskRepeatCfgService } from '../../task-repeat-cfg/task-repeat-cfg.serv
import { DialogConfirmComponent } from '../../../ui/dialog-confirm/dialog-confirm.component';
import { DialogFullscreenMarkdownComponent } from '../../../ui/dialog-fullscreen-markdown/dialog-fullscreen-markdown.component';
import { Update } from '@ngrx/entity';
import { DateAdapter } from '@angular/material/core';
import { getDbDateStr, isDBDateStr } from '../../../util/get-db-date-str';
import { combineDateAndTime } from '../../../util/combine-date-and-time';
import { getNextWeekDayOffset } from '../../../util/get-next-week-day-offset';
import { DEFAULT_GLOBAL_CONFIG } from '../../config/default-global-config.const';
import { DateService } from '../../../core/date/date.service';
import { isTouchActive } from '../../../util/input-intent';
import { IS_HYBRID_DEVICE } from '../../../util/is-mouse-primary';
import { DRAG_DELAY_FOR_TOUCH } from '../../../app.constants';
import { KeyboardConfig } from '../../config/keyboard-config.model';
import { DialogScheduleTaskComponent } from '../../planner/dialog-schedule-task/dialog-schedule-task.component';
import { PlannerActions } from '../../planner/store/planner.actions';
import { PlannerService } from '../../planner/planner.service';
import { DialogDeadlineComponent } from '../dialog-deadline/dialog-deadline.component';
import { isDeadlineOverdue as isDeadlineOverdueFn } from '../util/is-deadline-overdue';
import { isDeadlineApproaching as isDeadlineApproachingFn } from '../util/is-deadline-approaching';
@ -79,7 +85,8 @@ import { TaskListComponent } from '../task-list/task-list.component';
import { MsToStringPipe } from '../../../ui/duration/ms-to-string.pipe';
import { ShortPlannedAtPipe } from '../../../ui/pipes/short-planned-at.pipe';
import { LocalDateStrPipe } from '../../../ui/pipes/local-date-str.pipe';
import { TranslatePipe } from '@ngx-translate/core';
import { LocaleDatePipe } from '../../../ui/pipes/locale-date.pipe';
import { TranslatePipe, TranslateService } from '@ngx-translate/core';
import { SubTaskTotalTimeSpentPipe } from '../pipes/sub-task-total-time-spent.pipe';
import { TagListComponent } from '../../tag/tag-list/tag-list.component';
import { TagToggleMenuListComponent } from '../../tag/tag-toggle-menu-list/tag-toggle-menu-list.component';
@ -93,8 +100,10 @@ import { LayoutService } from '../../../core-ui/layout/layout.service';
import { TaskFocusService } from '../task-focus.service';
import { selectTimeConflictTaskIds } from '../store/task.selectors';
import { MatTooltip } from '@angular/material/tooltip';
import { millisecondsDiffToRemindOption } from '../util/remind-option-to-milliseconds';
import { MenuTreeService } from '../../menu-tree/menu-tree.service';
import { SelectOptionRowComponent } from '../../../ui/select-option-row/select-option-row.component';
import { SnackService } from '../../../core/snack/snack.service';
@Component({
selector: 'task',
@ -148,12 +157,17 @@ export class TaskComponent implements OnDestroy, AfterViewInit {
private readonly _configService = inject(GlobalConfigService);
private readonly _attachmentService = inject(TaskAttachmentService);
private readonly _elementRef = inject(ElementRef);
private readonly _dateAdapter = inject(DateAdapter);
private readonly _store = inject(Store);
private readonly _projectService = inject(ProjectService);
private readonly _taskFocusService = inject(TaskFocusService);
private readonly _dateService = inject(DateService);
private readonly _destroyRef = inject(DestroyRef);
private readonly _menuTreeService = inject(MenuTreeService);
private readonly _snackService = inject(SnackService);
private readonly _translateService = inject(TranslateService);
private readonly _datePipe = inject(LocaleDatePipe);
private readonly _plannerService = inject(PlannerService);
readonly workContextService = inject(WorkContextService);
readonly layoutService = inject(LayoutService);
@ -506,6 +520,60 @@ export class TaskComponent implements OnDestroy, AfterViewInit {
});
}
async scheduleTaskTomorrow(): Promise<void> {
const tDate = this._dateService.getLogicalTodayDate();
tDate.setDate(tDate.getDate() + 1);
await this._scheduleForDay(tDate);
}
async scheduleTaskNextWeek(): Promise<void> {
const tDate = this._dateService.getLogicalTodayDate();
const dayOffset = getNextWeekDayOffset(this._dateAdapter, tDate);
tDate.setDate(tDate.getDate() + dayOffset);
await this._scheduleForDay(tDate);
}
async scheduleTaskNextMonth(): Promise<void> {
const tDate = this._dateService.getLogicalTodayDate();
tDate.setDate(1);
tDate.setMonth(tDate.getMonth() + 1);
await this._scheduleForDay(tDate);
}
private async _scheduleForDay(dayDate: Date): Promise<void> {
const day = getDbDateStr(dayDate);
const task = this.task();
if (task.dueWithTime) {
const newDate = combineDateAndTime(dayDate, new Date(task.dueWithTime));
const remindCfg = task.reminderId
? millisecondsDiffToRemindOption(task.dueWithTime, task.remindAt)
: (this._configService.cfg()?.reminder.defaultTaskRemindOption ??
DEFAULT_GLOBAL_CONFIG.reminder.defaultTaskRemindOption!);
this._taskService.scheduleTask(task, newDate.getTime(), remindCfg, false);
this._snackService.open({
type: 'SUCCESS',
msg: T.F.PLANNER.S.TASK_PLANNED_FOR,
ico: 'today',
translateParams: {
date: this._dateService.isToday(newDate)
? this._translateService.instant(T.G.TODAY_TAG_TITLE)
: (this._datePipe.transform(day, 'shortDate') as string),
extra: await this._plannerService.getSnackExtraStr(day),
},
});
} else {
this._store.dispatch(
PlannerActions.planTaskForDay({
task: task as TaskCopy,
day,
isShowSnack: true,
}),
);
}
this.focusSelfOrNextIfNotPossible();
}
async editTaskRepeatCfg(): Promise<void> {
const { DialogEditTaskRepeatCfgComponent } =
await import('../../task-repeat-cfg/dialog-edit-task-repeat-cfg/dialog-edit-task-repeat-cfg.component');

View file

@ -2428,7 +2428,6 @@ const T = {
MOVE_TASK_TO_TOP: 'GCF.KEYBOARD.MOVE_TASK_TO_TOP',
MOVE_TASK_UP: 'GCF.KEYBOARD.MOVE_TASK_UP',
MOVE_TO_BACKLOG: 'GCF.KEYBOARD.MOVE_TO_BACKLOG',
MOVE_TO_REGULARS_TASKS: 'GCF.KEYBOARD.MOVE_TO_REGULARS_TASKS',
OPEN_PROJECT_NOTES: 'GCF.KEYBOARD.OPEN_PROJECT_NOTES',
PLUGIN_SHORTCUTS: 'GCF.KEYBOARD.PLUGIN_SHORTCUTS',
SELECT_NEXT_TASK: 'GCF.KEYBOARD.SELECT_NEXT_TASK',
@ -2444,6 +2443,10 @@ const T = {
TASK_OPEN_CONTEXT_MENU: 'GCF.KEYBOARD.TASK_OPEN_CONTEXT_MENU',
TASK_OPEN_ESTIMATION_DIALOG: 'GCF.KEYBOARD.TASK_OPEN_ESTIMATION_DIALOG',
TASK_SCHEDULE: 'GCF.KEYBOARD.TASK_SCHEDULE',
TASK_SCHEDULE_TODAY: 'GCF.KEYBOARD.TASK_SCHEDULE_TODAY',
TASK_SCHEDULE_TOMORROW: 'GCF.KEYBOARD.TASK_SCHEDULE_TOMORROW',
TASK_SCHEDULE_NEXT_WEEK: 'GCF.KEYBOARD.TASK_SCHEDULE_NEXT_WEEK',
TASK_SCHEDULE_NEXT_MONTH: 'GCF.KEYBOARD.TASK_SCHEDULE_NEXT_MONTH',
TASK_SCHEDULE_DEADLINE: 'GCF.KEYBOARD.TASK_SCHEDULE_DEADLINE',
TASK_SHORTCUTS: 'GCF.KEYBOARD.TASK_SHORTCUTS',
TASK_SHORTCUTS_INFO: 'GCF.KEYBOARD.TASK_SHORTCUTS_INFO',

View file

@ -0,0 +1,8 @@
import { DateAdapter } from '@angular/material/core';
export const getNextWeekDayOffset = (
dateAdapter: DateAdapter<unknown>,
date: Date,
): number => {
return (dateAdapter.getFirstDayOfWeek() - dateAdapter.getDayOfWeek(date) + 7) % 7 || 7;
};

View file

@ -2371,7 +2371,6 @@
"MOVE_TASK_TO_TOP": "Move task to the top of the list",
"MOVE_TASK_UP": "Move task up in the list",
"MOVE_TO_BACKLOG": "Move task to project backlog",
"MOVE_TO_REGULARS_TASKS": "Move task to Today",
"OPEN_PROJECT_NOTES": "Show/hide notes",
"PLUGIN_SHORTCUTS": "Plugin Shortcuts",
"SELECT_NEXT_TASK": "Select next task",
@ -2387,6 +2386,10 @@
"TASK_OPEN_CONTEXT_MENU": "Open task context menu",
"TASK_OPEN_ESTIMATION_DIALOG": "Edit estimate/time spent",
"TASK_SCHEDULE": "Schedule task",
"TASK_SCHEDULE_TODAY": "Schedule task for today",
"TASK_SCHEDULE_TOMORROW": "Schedule task for tomorrow",
"TASK_SCHEDULE_NEXT_WEEK": "Schedule task for next week",
"TASK_SCHEDULE_NEXT_MONTH": "Schedule task for next month",
"TASK_SCHEDULE_DEADLINE": "Set task deadline",
"TASK_SHORTCUTS": "Tasks",
"TASK_SHORTCUTS_INFO": "The following shortcuts apply to the currently selected task (selected via tab or mouse).",

View file

@ -480,7 +480,7 @@
"moveTaskToTop": "Ctrl+Alt+ArrowUp",
"moveTaskToBottom": "Ctrl+Alt+ArrowDown",
"moveToBacklog": "Shift+B",
"moveToTodaysTasks": "Shift+T",
"taskScheduleToday": "Shift+T",
"expandSubTasks": null,
"collapseSubTasks": null,
"togglePlay": "y",