mirror of
https://github.com/callumalpass/tasknotes.git
synced 2026-07-22 12:50:26 +00:00
fix: anchor task timeblocks to scheduled date
This commit is contained in:
parent
dd38ef969f
commit
f5a21bebdd
3 changed files with 124 additions and 45 deletions
|
|
@ -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,
|
||||
|
|
|
|||
54
src/utils/timeblockPrefillUtils.ts
Normal file
54
src/utils/timeblockPrefillUtils.ts
Normal 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),
|
||||
};
|
||||
}
|
||||
67
tests/unit/utils/timeblockPrefillUtils.test.ts
Normal file
67
tests/unit/utils/timeblockPrefillUtils.test.ts
Normal 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",
|
||||
});
|
||||
});
|
||||
});
|
||||
Loading…
Reference in a new issue