diff --git a/src/components/TaskContextMenu.ts b/src/components/TaskContextMenu.ts index 3d2274a3..2dcd3502 100644 --- a/src/components/TaskContextMenu.ts +++ b/src/components/TaskContextMenu.ts @@ -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, diff --git a/src/utils/timeblockPrefillUtils.ts b/src/utils/timeblockPrefillUtils.ts new file mode 100644 index 00000000..39ede525 --- /dev/null +++ b/src/utils/timeblockPrefillUtils.ts @@ -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), + }; +} diff --git a/tests/unit/utils/timeblockPrefillUtils.test.ts b/tests/unit/utils/timeblockPrefillUtils.test.ts new file mode 100644 index 00000000..f8f3b6e5 --- /dev/null +++ b/tests/unit/utils/timeblockPrefillUtils.test.ts @@ -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 { + 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", + }); + }); +});