fix: anchor task timeblocks to scheduled date

This commit is contained in:
callumalpass 2026-03-29 09:48:39 +11:00
parent dd38ef969f
commit f5a21bebdd
3 changed files with 124 additions and 45 deletions

View file

@ -1,7 +1,7 @@
import { Menu, Notice, TFile } from "obsidian";
import TaskNotesPlugin from "../main";
import { TaskDependency, TaskInfo } from "../types";
import { formatDateForStorage, parseDateToLocal } from "../utils/dateUtils";
import { formatDateForStorage } from "../utils/dateUtils";
import { ReminderModal } from "../modals/ReminderModal";
import { CalendarExportService } from "../services/CalendarExportService";
import { showConfirmationModal } from "../modals/ConfirmationModal";
@ -18,6 +18,7 @@ import {
} from "../utils/dependencyUtils";
import { generateLink } from "../utils/linkUtils";
import { ContextMenu } from "./ContextMenu";
import { buildTimeblockPrefillForTask } from "../utils/timeblockPrefillUtils";
import { TimeblockCreationModal } from "../modals/TimeblockCreationModal";
export interface TaskContextMenuOptions {
@ -42,49 +43,6 @@ export class TaskContextMenu {
return this.options.plugin.i18n.translate(key, params);
}
private formatTimeForInput(date: Date): string {
const hours = String(date.getHours()).padStart(2, "0");
const minutes = String(date.getMinutes()).padStart(2, "0");
return `${hours}:${minutes}`;
}
private getTimeblockPrefillForTask(task: TaskInfo): {
date: string;
startTime: string;
endTime: string;
} {
let startDate = new Date(this.options.targetDate);
// If the task has a scheduled datetime, use it as the timeblock anchor.
if (task.scheduled && task.scheduled.includes("T")) {
try {
startDate = parseDateToLocal(task.scheduled);
} catch {
// Fallback to target date defaults if parsing fails.
}
}
if (isNaN(startDate.getTime())) {
startDate = new Date();
}
// For date-only tasks, default to a 09:00 planning slot.
if (!task.scheduled || !task.scheduled.includes("T")) {
startDate.setHours(9, 0, 0, 0);
}
const durationMinutes = task.timeEstimate && task.timeEstimate > 0
? task.timeEstimate
: 60;
const endDate = new Date(startDate.getTime() + durationMinutes * 60 * 1000);
return {
date: formatDateForStorage(startDate),
startTime: this.formatTimeForInput(startDate),
endTime: this.formatTimeForInput(endDate),
};
}
private buildMenu(): void {
const { task, plugin } = this.options;
@ -362,7 +320,7 @@ export class TaskContextMenu {
item.setTitle("Create timeblock");
item.setIcon("calendar-plus");
item.onClick(() => {
const prefill = this.getTimeblockPrefillForTask(task);
const prefill = buildTimeblockPrefillForTask(task, this.options.targetDate);
const modal = new TimeblockCreationModal(plugin.app, plugin, {
date: prefill.date,
startTime: prefill.startTime,

View file

@ -0,0 +1,54 @@
import { TaskInfo } from "../types";
import {
formatDateForStorage,
hasTimeComponent,
parseDateToLocal,
} from "./dateUtils";
export interface TimeblockPrefill {
date: string;
startTime: string;
endTime: string;
}
function formatTimeForInput(date: Date): string {
const hours = String(date.getHours()).padStart(2, "0");
const minutes = String(date.getMinutes()).padStart(2, "0");
return `${hours}:${minutes}`;
}
export function buildTimeblockPrefillForTask(
task: TaskInfo,
targetDate: Date
): TimeblockPrefill {
let startDate: Date | null = null;
if (task.scheduled) {
try {
startDate = parseDateToLocal(task.scheduled);
} catch {
startDate = null;
}
}
if (!startDate || isNaN(startDate.getTime())) {
startDate = new Date(targetDate);
}
if (isNaN(startDate.getTime())) {
startDate = new Date();
}
if (!task.scheduled || !hasTimeComponent(task.scheduled)) {
startDate.setHours(9, 0, 0, 0);
}
const durationMinutes = task.timeEstimate && task.timeEstimate > 0 ? task.timeEstimate : 60;
const endDate = new Date(startDate.getTime() + durationMinutes * 60 * 1000);
return {
date: formatDateForStorage(startDate),
startTime: formatTimeForInput(startDate),
endTime: formatTimeForInput(endDate),
};
}

View file

@ -0,0 +1,67 @@
import { describe, expect, it } from "@jest/globals";
import { TaskInfo } from "../../../src/types";
import {
formatDateForStorage,
parseDateToLocal,
} from "../../../src/utils/dateUtils";
import { buildTimeblockPrefillForTask } from "../../../src/utils/timeblockPrefillUtils";
function makeTask(overrides: Partial<TaskInfo> = {}): TaskInfo {
return {
path: "TaskNotes/Tasks/example.md",
title: "Example task",
status: "open",
priority: "normal",
...overrides,
};
}
describe("buildTimeblockPrefillForTask", () => {
it("uses the scheduled date for date-only scheduled tasks", () => {
const prefill = buildTimeblockPrefillForTask(
makeTask({
scheduled: "2026-03-10",
timeEstimate: 30,
}),
new Date(2026, 2, 29, 12, 0, 0, 0)
);
expect(prefill).toEqual({
date: formatDateForStorage(parseDateToLocal("2026-03-10")),
startTime: "09:00",
endTime: "09:30",
});
});
it("uses the scheduled datetime for time-aware scheduled tasks", () => {
const prefill = buildTimeblockPrefillForTask(
makeTask({
scheduled: "2026-03-10T14:15",
timeEstimate: 45,
}),
new Date(2026, 2, 29, 12, 0, 0, 0)
);
expect(prefill).toEqual({
date: formatDateForStorage(parseDateToLocal("2026-03-10T14:15")),
startTime: "14:15",
endTime: "15:00",
});
});
it("falls back to the target date for unscheduled tasks", () => {
const prefill = buildTimeblockPrefillForTask(
makeTask(),
new Date(2026, 2, 29, 12, 0, 0, 0)
);
const expectedFallbackDate = new Date(2026, 2, 29, 12, 0, 0, 0);
expectedFallbackDate.setHours(9, 0, 0, 0);
expect(prefill).toEqual({
date: formatDateForStorage(expectedFallbackDate),
startTime: "09:00",
endTime: "10:00",
});
});
});