mirror of
https://github.com/taskgenius/taskgenius-plugin.git
synced 2026-07-22 06:40:25 +00:00
refactor(calendar): update calendar component integration and styles
- Add TaskCalendarAdapter for task-to-calendar event conversion - Update calendar component with improved view container styling - Refresh calendar styles from @taskgenius/calendar package
This commit is contained in:
parent
5b6e488be9
commit
6987fdd2ba
4 changed files with 787 additions and 318 deletions
|
|
@ -100,7 +100,7 @@ class NormalizedDateFnsAdapter implements DateAdapter<Date> {
|
|||
format(date: Date, formatStr: string) {
|
||||
return this.fns.format(
|
||||
date,
|
||||
this.normalizeFormat(formatStr) ?? formatStr,
|
||||
this.normalizeFormat(formatStr) ?? formatStr
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -216,7 +216,7 @@ class NormalizedDateFnsAdapter implements DateAdapter<Date> {
|
|||
if (!unit) return this.fns.isBefore(date1, date2);
|
||||
return this.fns.isBefore(
|
||||
this.startOf(date1, unit),
|
||||
this.startOf(date2, unit),
|
||||
this.startOf(date2, unit)
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -224,7 +224,7 @@ class NormalizedDateFnsAdapter implements DateAdapter<Date> {
|
|||
if (!unit) return this.fns.isAfter(date1, date2);
|
||||
return this.fns.isAfter(
|
||||
this.startOf(date1, unit),
|
||||
this.startOf(date2, unit),
|
||||
this.startOf(date2, unit)
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -232,7 +232,7 @@ class NormalizedDateFnsAdapter implements DateAdapter<Date> {
|
|||
if (!unit) return this.fns.isEqual(date1, date2);
|
||||
return this.fns.isEqual(
|
||||
this.startOf(date1, unit),
|
||||
this.startOf(date2, unit),
|
||||
this.startOf(date2, unit)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -276,7 +276,7 @@ export class CalendarComponent extends Component {
|
|||
onTaskCompleted?: (task: Task) => void;
|
||||
onEventContextMenu?: (ev: MouseEvent, event: CalendarEvent) => void;
|
||||
} = {},
|
||||
private viewId: string = "calendar",
|
||||
private viewId: string = "calendar"
|
||||
) {
|
||||
super();
|
||||
this.app = app;
|
||||
|
|
@ -286,12 +286,12 @@ export class CalendarComponent extends Component {
|
|||
|
||||
this.headerEl = this.containerEl.createDiv("calendar-header");
|
||||
this.viewContainerEl = this.containerEl.createDiv(
|
||||
"calendar-view-container",
|
||||
"calendar-view-container"
|
||||
);
|
||||
|
||||
// Load saved view mode
|
||||
const savedView = this.app.loadLocalStorage(
|
||||
"task-genius:calendar-view",
|
||||
"task-genius:calendar-view"
|
||||
);
|
||||
if (savedView) {
|
||||
this.currentViewMode = savedView as CalendarViewMode;
|
||||
|
|
@ -495,10 +495,10 @@ export class CalendarComponent extends Component {
|
|||
.addOption("day", t("Day"))
|
||||
.addOption("agenda", t("Agenda"))
|
||||
.onChange((value) =>
|
||||
this.setView(value as CalendarViewMode),
|
||||
this.setView(value as CalendarViewMode)
|
||||
)
|
||||
.setValue(this.currentViewMode);
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -525,7 +525,7 @@ export class CalendarComponent extends Component {
|
|||
"view-month",
|
||||
"view-week",
|
||||
"view-day",
|
||||
"view-agenda",
|
||||
"view-agenda"
|
||||
);
|
||||
this.viewContainerEl.addClass(`view-${this.currentViewMode}`);
|
||||
|
||||
|
|
@ -562,8 +562,8 @@ export class CalendarComponent extends Component {
|
|||
this.currentViewMode === "day")
|
||||
? workingHours(
|
||||
config.workingHoursStart ?? 9,
|
||||
config.workingHoursEnd ?? 18,
|
||||
)
|
||||
config.workingHoursEnd ?? 18
|
||||
)
|
||||
: undefined,
|
||||
},
|
||||
dateAdapter: new NormalizedDateFnsAdapter(dateFns),
|
||||
|
|
@ -589,18 +589,24 @@ export class CalendarComponent extends Component {
|
|||
event: "var(--font-ui-smaller)",
|
||||
},
|
||||
},
|
||||
// Event interactions
|
||||
onEventClick: this.handleTGEventClick.bind(this),
|
||||
onEventDrop: this.handleTGEventDrop.bind(this),
|
||||
// Date interactions (v0.6.0+)
|
||||
onDateClick: this.handleDateClick.bind(this),
|
||||
onDateDoubleClick: this.handleDateDoubleClick.bind(this),
|
||||
onDateContextMenu: this.handleDateContextMenu.bind(this),
|
||||
// Event interactions - use arrow functions for safe binding
|
||||
onEventClick: (event: any) => this.handleTGEventClick(event),
|
||||
onEventDrop: (event: any, newStart: any, newEnd: any) =>
|
||||
this.handleTGEventDrop(event, newStart, newEnd),
|
||||
onEventResize: (event: any, newStart: any, newEnd: any) =>
|
||||
this.handleTGEventResize(event, newStart, newEnd),
|
||||
// Date interactions (v0.6.0+) - match library's callback signatures
|
||||
onDateClick: (date: Date) => this.handleDateClick(date),
|
||||
onDateDoubleClick: (date: Date) => this.handleDateDoubleClick(date),
|
||||
onDateContextMenu: (date: Date, x: number, y: number) =>
|
||||
this.handleDateContextMenu(date, x, y),
|
||||
// Time slot interactions (week/day views, v0.6.0+)
|
||||
onTimeSlotClick: this.handleTimeSlotClick.bind(this),
|
||||
onTimeSlotDoubleClick: this.handleTimeSlotDoubleClick.bind(this),
|
||||
onTimeSlotClick: (dateTime: Date) =>
|
||||
this.handleTimeSlotClick(dateTime),
|
||||
onTimeSlotDoubleClick: (dateTime: Date) =>
|
||||
this.handleTimeSlotDoubleClick(dateTime),
|
||||
// Custom cell rendering
|
||||
onRenderDateCell: this.handleTGRenderDateCell.bind(this),
|
||||
onRenderDateCell: (ctx: any) => this.handleTGRenderDateCell(ctx),
|
||||
};
|
||||
|
||||
this.tgCalendar = new Calendar(this.viewContainerEl, calendarConfig);
|
||||
|
|
@ -621,7 +627,7 @@ export class CalendarComponent extends Component {
|
|||
onEventHover: this.onEventHover,
|
||||
onEventContextMenu: this.onEventContextMenu,
|
||||
onEventComplete: this.onEventComplete,
|
||||
},
|
||||
}
|
||||
);
|
||||
this.addChild(this.agendaView);
|
||||
this.agendaView.updateEvents(this.events);
|
||||
|
|
@ -643,7 +649,7 @@ export class CalendarComponent extends Component {
|
|||
onMonthClick: this.onMonthClick,
|
||||
onMonthHover: this.onMonthHover,
|
||||
},
|
||||
config,
|
||||
config
|
||||
);
|
||||
this.addChild(this.yearView);
|
||||
this.yearView.updateEvents(this.events);
|
||||
|
|
@ -653,7 +659,7 @@ export class CalendarComponent extends Component {
|
|||
// Event Handlers
|
||||
// ============================================
|
||||
|
||||
private handleTGEventClick(event: TGCalendarEvent) {
|
||||
private handleTGEventClick(event: AdapterCalendarEvent) {
|
||||
const task = getTaskFromEvent(event as any);
|
||||
if (task) {
|
||||
this.params?.onTaskSelected?.(task);
|
||||
|
|
@ -661,9 +667,9 @@ export class CalendarComponent extends Component {
|
|||
}
|
||||
|
||||
private async handleTGEventDrop(
|
||||
event: TGCalendarEvent,
|
||||
event: AdapterCalendarEvent,
|
||||
newStart: Date | string,
|
||||
newEnd: Date | string,
|
||||
newEnd: Date | string
|
||||
) {
|
||||
const task = getTaskFromEvent(event as any);
|
||||
if (!task) {
|
||||
|
|
@ -676,8 +682,8 @@ export class CalendarComponent extends Component {
|
|||
if (isIcsTask) {
|
||||
new Notice(
|
||||
t(
|
||||
"Cannot move external calendar events. Please update them in the original calendar.",
|
||||
),
|
||||
"Cannot move external calendar events. Please update them in the original calendar."
|
||||
)
|
||||
);
|
||||
// Refresh to reset the visual position
|
||||
setTimeout(() => {
|
||||
|
|
@ -687,97 +693,166 @@ export class CalendarComponent extends Component {
|
|||
return;
|
||||
}
|
||||
|
||||
// Normalize to Date objects (temporary compatibility for v0.8.0 transition)
|
||||
const newStartDate =
|
||||
newStart instanceof Date ? newStart : new Date(newStart);
|
||||
const newEndDate = newEnd instanceof Date ? newEnd : new Date(newEnd);
|
||||
const newEndInput = newEnd ?? newStart;
|
||||
const newEndDate =
|
||||
newEndInput instanceof Date ? newEndInput : new Date(newEndInput);
|
||||
const oldStartDate = new Date(event.start);
|
||||
const oldEndDate = event.end ? new Date(event.end) : oldStartDate;
|
||||
|
||||
// Get original dates (normalized to day start)
|
||||
const oldStartDate = startOfDay(new Date(event.start));
|
||||
const oldEndDate = event.end
|
||||
? startOfDay(new Date(event.end))
|
||||
: oldStartDate;
|
||||
if (
|
||||
Number.isNaN(newStartDate.getTime()) ||
|
||||
Number.isNaN(newEndDate.getTime()) ||
|
||||
Number.isNaN(oldStartDate.getTime()) ||
|
||||
Number.isNaN(oldEndDate.getTime())
|
||||
) {
|
||||
console.warn("Calendar: Invalid date detected during drag/drop", {
|
||||
event,
|
||||
newStart,
|
||||
newEnd,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Calculate changes using date-fns
|
||||
const startDiff = differenceInDays(
|
||||
startOfDay(newStartDate),
|
||||
oldStartDate,
|
||||
);
|
||||
const endDiff = differenceInDays(startOfDay(newEndDate), oldEndDate);
|
||||
|
||||
// No change at all
|
||||
if (startDiff === 0 && endDiff === 0) return;
|
||||
const isAllDayEvent = event.allDay === true;
|
||||
|
||||
try {
|
||||
const updates: any = { metadata: {} };
|
||||
let updatedFields: string[] = [];
|
||||
|
||||
// Determine the operation type:
|
||||
// - If both start and end moved by the same amount: it's a MOVE (drag)
|
||||
// - If only start changed: RESIZE from start
|
||||
// - If only end changed: RESIZE from end
|
||||
const isMove = startDiff === endDiff;
|
||||
const isResizeStart = startDiff !== 0 && startDiff !== endDiff;
|
||||
const isResizeEnd = endDiff !== 0 && startDiff !== endDiff;
|
||||
if (isAllDayEvent) {
|
||||
const normalizedNewStart = startOfDay(newStartDate);
|
||||
const normalizedNewEnd = startOfDay(newEndDate);
|
||||
const normalizedOldStart = startOfDay(oldStartDate);
|
||||
const normalizedOldEnd = startOfDay(oldEndDate);
|
||||
|
||||
if (isMove) {
|
||||
// MOVE: Shift all date fields by the same amount
|
||||
if (task.metadata.dueDate) {
|
||||
updates.metadata.dueDate = addDays(
|
||||
task.metadata.dueDate,
|
||||
startDiff,
|
||||
).getTime();
|
||||
updatedFields.push("due date");
|
||||
}
|
||||
const startDiff = differenceInDays(
|
||||
normalizedNewStart,
|
||||
normalizedOldStart
|
||||
);
|
||||
const endDiff = differenceInDays(
|
||||
normalizedNewEnd,
|
||||
normalizedOldEnd
|
||||
);
|
||||
|
||||
if (task.metadata.scheduledDate) {
|
||||
updates.metadata.scheduledDate = addDays(
|
||||
task.metadata.scheduledDate,
|
||||
startDiff,
|
||||
).getTime();
|
||||
updatedFields.push("scheduled date");
|
||||
}
|
||||
if (startDiff === 0 && endDiff === 0) return;
|
||||
|
||||
if (task.metadata.startDate) {
|
||||
updates.metadata.startDate = addDays(
|
||||
task.metadata.startDate,
|
||||
startDiff,
|
||||
).getTime();
|
||||
const isMove = startDiff === endDiff;
|
||||
const isResizeStart = startDiff !== 0 && startDiff !== endDiff;
|
||||
const isResizeEnd = endDiff !== 0 && startDiff !== endDiff;
|
||||
|
||||
if (isMove) {
|
||||
if (task.metadata.dueDate) {
|
||||
updates.metadata.dueDate = addDays(
|
||||
startOfDay(new Date(task.metadata.dueDate)),
|
||||
startDiff
|
||||
).getTime();
|
||||
updatedFields.push("due date");
|
||||
}
|
||||
|
||||
if (task.metadata.scheduledDate) {
|
||||
updates.metadata.scheduledDate = addDays(
|
||||
startOfDay(new Date(task.metadata.scheduledDate)),
|
||||
startDiff
|
||||
).getTime();
|
||||
updatedFields.push("scheduled date");
|
||||
}
|
||||
|
||||
if (task.metadata.startDate) {
|
||||
updates.metadata.startDate = addDays(
|
||||
startOfDay(new Date(task.metadata.startDate)),
|
||||
startDiff
|
||||
).getTime();
|
||||
updatedFields.push("start date");
|
||||
}
|
||||
|
||||
if (updatedFields.length === 0) {
|
||||
updates.metadata.dueDate = normalizedNewStart.getTime();
|
||||
updatedFields.push("due date");
|
||||
}
|
||||
} else if (isResizeStart) {
|
||||
updates.metadata.startDate = normalizedNewStart.getTime();
|
||||
updatedFields.push("start date");
|
||||
}
|
||||
|
||||
// If no date fields found, set dueDate
|
||||
if (updatedFields.length === 0) {
|
||||
updates.metadata.dueDate =
|
||||
startOfDay(newStartDate).getTime();
|
||||
if (
|
||||
!task.metadata.dueDate &&
|
||||
differenceInDays(newEndDate, newStartDate) > 0
|
||||
) {
|
||||
updates.metadata.dueDate = normalizedNewEnd.getTime();
|
||||
updatedFields.push("due date");
|
||||
}
|
||||
} else if (isResizeEnd) {
|
||||
updates.metadata.dueDate = normalizedNewEnd.getTime();
|
||||
updatedFields.push("due date");
|
||||
}
|
||||
} else if (isResizeStart) {
|
||||
// RESIZE from START: Update startDate
|
||||
updates.metadata.startDate = startOfDay(newStartDate).getTime();
|
||||
updatedFields.push("start date");
|
||||
|
||||
// If we're creating a multi-day event and don't have an end date, set dueDate
|
||||
if (
|
||||
!task.metadata.dueDate &&
|
||||
differenceInDays(newEndDate, newStartDate) > 0
|
||||
) {
|
||||
updates.metadata.dueDate = startOfDay(newEndDate).getTime();
|
||||
updatedFields.push("due date");
|
||||
if (
|
||||
!task.metadata.startDate &&
|
||||
differenceInDays(newEndDate, newStartDate) > 0
|
||||
) {
|
||||
updates.metadata.startDate =
|
||||
normalizedNewStart.getTime();
|
||||
updatedFields.push("start date");
|
||||
}
|
||||
}
|
||||
} else if (isResizeEnd) {
|
||||
// RESIZE from END: Update dueDate
|
||||
updates.metadata.dueDate = startOfDay(newEndDate).getTime();
|
||||
updatedFields.push("due date");
|
||||
} else {
|
||||
const startDiffMs =
|
||||
newStartDate.getTime() - oldStartDate.getTime();
|
||||
const endDiffMs = newEndDate.getTime() - oldEndDate.getTime();
|
||||
|
||||
// If we're creating a multi-day event and don't have a start date, set startDate
|
||||
if (
|
||||
!task.metadata.startDate &&
|
||||
differenceInDays(newEndDate, newStartDate) > 0
|
||||
) {
|
||||
updates.metadata.startDate =
|
||||
startOfDay(newStartDate).getTime();
|
||||
if (startDiffMs === 0 && endDiffMs === 0) return;
|
||||
|
||||
const isMove = startDiffMs === endDiffMs;
|
||||
const isResizeStart =
|
||||
startDiffMs !== 0 && startDiffMs !== endDiffMs;
|
||||
const isResizeEnd =
|
||||
endDiffMs !== 0 && startDiffMs !== endDiffMs;
|
||||
|
||||
if (isMove) {
|
||||
if (task.metadata.dueDate) {
|
||||
updates.metadata.dueDate =
|
||||
task.metadata.dueDate + startDiffMs;
|
||||
updatedFields.push("due date");
|
||||
}
|
||||
|
||||
if (task.metadata.scheduledDate) {
|
||||
updates.metadata.scheduledDate =
|
||||
task.metadata.scheduledDate + startDiffMs;
|
||||
updatedFields.push("scheduled date");
|
||||
}
|
||||
|
||||
if (task.metadata.startDate) {
|
||||
updates.metadata.startDate =
|
||||
task.metadata.startDate + startDiffMs;
|
||||
updatedFields.push("start date");
|
||||
}
|
||||
|
||||
if (updatedFields.length === 0) {
|
||||
updates.metadata.dueDate = newStartDate.getTime();
|
||||
updatedFields.push("due date");
|
||||
}
|
||||
} else if (isResizeStart) {
|
||||
updates.metadata.startDate = newStartDate.getTime();
|
||||
updatedFields.push("start date");
|
||||
|
||||
if (
|
||||
!task.metadata.dueDate &&
|
||||
newEndDate.getTime() !== newStartDate.getTime()
|
||||
) {
|
||||
updates.metadata.dueDate = newEndDate.getTime();
|
||||
updatedFields.push("due date");
|
||||
}
|
||||
} else if (isResizeEnd) {
|
||||
updates.metadata.dueDate = newEndDate.getTime();
|
||||
updatedFields.push("due date");
|
||||
|
||||
if (
|
||||
!task.metadata.startDate &&
|
||||
newEndDate.getTime() !== newStartDate.getTime()
|
||||
) {
|
||||
updates.metadata.startDate = newStartDate.getTime();
|
||||
updatedFields.push("start date");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -805,7 +880,7 @@ export class CalendarComponent extends Component {
|
|||
} else {
|
||||
console.error(
|
||||
"Calendar: Failed to update task after drag:",
|
||||
result.error,
|
||||
result.error
|
||||
);
|
||||
new Notice(t("Failed to update task"));
|
||||
}
|
||||
|
|
@ -815,6 +890,111 @@ export class CalendarComponent extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
private async handleTGEventResize(
|
||||
event: AdapterCalendarEvent,
|
||||
newStart: Date | string,
|
||||
newEnd: Date | string
|
||||
) {
|
||||
const task = getTaskFromEvent(event as any);
|
||||
if (!task) {
|
||||
new Notice(t("Failed to update task: Task not found"));
|
||||
return;
|
||||
}
|
||||
|
||||
const isIcsTask = (task as any).source?.type === "ics";
|
||||
if (isIcsTask) {
|
||||
new Notice(
|
||||
"In current version, cannot resize external calendar events"
|
||||
);
|
||||
setTimeout(() => {
|
||||
this.processTasks();
|
||||
this.renderCurrentView();
|
||||
}, 100);
|
||||
return;
|
||||
}
|
||||
|
||||
const newStartDate =
|
||||
newStart instanceof Date ? newStart : new Date(newStart);
|
||||
const newEndDate = newEnd instanceof Date ? newEnd : new Date(newEnd);
|
||||
const oldStartDate = new Date(event.start);
|
||||
const oldEndDate = event.end ? new Date(event.end) : oldStartDate;
|
||||
const isAllDayEvent = event.allDay === true;
|
||||
|
||||
const normalizedNewStart = isAllDayEvent
|
||||
? startOfDay(newStartDate)
|
||||
: newStartDate;
|
||||
const normalizedNewEnd = isAllDayEvent
|
||||
? startOfDay(newEndDate)
|
||||
: newEndDate;
|
||||
const normalizedOldStart = isAllDayEvent
|
||||
? startOfDay(oldStartDate)
|
||||
: oldStartDate;
|
||||
const normalizedOldEnd = isAllDayEvent
|
||||
? startOfDay(oldEndDate)
|
||||
: oldEndDate;
|
||||
|
||||
const startChanged =
|
||||
normalizedNewStart.getTime() !== normalizedOldStart.getTime();
|
||||
const endChanged =
|
||||
normalizedNewEnd.getTime() !== normalizedOldEnd.getTime();
|
||||
|
||||
if (!startChanged && !endChanged) return;
|
||||
|
||||
const updates: any = { metadata: {} };
|
||||
const updatedFields: string[] = [];
|
||||
const newStartMs = normalizedNewStart.getTime();
|
||||
const newEndMs = normalizedNewEnd.getTime();
|
||||
const setField = (
|
||||
key: "startDate" | "dueDate",
|
||||
value: number,
|
||||
label: string
|
||||
) => {
|
||||
if (updates.metadata[key] === undefined) {
|
||||
updates.metadata[key] = value;
|
||||
updatedFields.push(label);
|
||||
}
|
||||
};
|
||||
|
||||
if (startChanged) {
|
||||
setField("startDate", newStartMs, "start date");
|
||||
}
|
||||
|
||||
if (endChanged) {
|
||||
setField("dueDate", newEndMs, "due date");
|
||||
}
|
||||
|
||||
// If a task only had one date, ensure both ends are updated for the new range
|
||||
if (!task.metadata.startDate) {
|
||||
setField("startDate", newStartMs, "start date");
|
||||
}
|
||||
if (!task.metadata.dueDate) {
|
||||
setField("dueDate", newEndMs, "due date");
|
||||
}
|
||||
|
||||
if (updatedFields.length === 0) return;
|
||||
|
||||
if (!this.plugin.writeAPI) {
|
||||
new Notice(t("Task update system not available"));
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await this.plugin.writeAPI.updateTask({
|
||||
taskId: task.id,
|
||||
updates,
|
||||
});
|
||||
|
||||
if (result.success) {
|
||||
new Notice(t("Task time updated: ") + updatedFields.join(", "));
|
||||
setTimeout(() => {
|
||||
this.processTasks();
|
||||
this.renderCurrentView();
|
||||
}, 100);
|
||||
} else {
|
||||
console.error("Calendar: Failed to resize task:", result.error);
|
||||
new Notice(t("Failed to update task"));
|
||||
}
|
||||
}
|
||||
|
||||
private handleTGRenderDateCell(ctx: any) {
|
||||
const { date, cellEl } = ctx;
|
||||
const tasksOnDate = this.getTasksForDate(date);
|
||||
|
|
@ -827,7 +1007,7 @@ export class CalendarComponent extends Component {
|
|||
const dateHeader = cellEl.querySelector(".tg-date-header");
|
||||
if (dateHeader) {
|
||||
const badgesContainer = dateHeader.createDiv(
|
||||
"calendar-badges-container",
|
||||
"calendar-badges-container"
|
||||
);
|
||||
badgeEvents.forEach((badgeEvent) => {
|
||||
const badgeEl = badgesContainer.createEl("div", {
|
||||
|
|
@ -903,7 +1083,7 @@ export class CalendarComponent extends Component {
|
|||
if (!task.completed) {
|
||||
this.app.workspace.trigger(
|
||||
"task-genius:task-completed",
|
||||
task,
|
||||
task
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -936,7 +1116,7 @@ export class CalendarComponent extends Component {
|
|||
/**
|
||||
* Handle single click on a date cell (month view)
|
||||
*/
|
||||
private handleDateClick(date: Date, ev: MouseEvent) {
|
||||
private handleDateClick(date: Date) {
|
||||
// Allow event bubbling for other interactions
|
||||
// This maintains compatibility with existing click behavior
|
||||
}
|
||||
|
|
@ -944,19 +1124,19 @@ export class CalendarComponent extends Component {
|
|||
/**
|
||||
* Handle double click on a date cell - opens quick capture modal
|
||||
*/
|
||||
private handleDateDoubleClick(date: Date, ev: MouseEvent) {
|
||||
private handleDateDoubleClick(date: Date) {
|
||||
new QuickCaptureModal(
|
||||
this.app,
|
||||
this.plugin,
|
||||
{ dueDate: date },
|
||||
true,
|
||||
true
|
||||
).open();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle context menu (right-click) on a date cell
|
||||
*/
|
||||
private handleDateContextMenu(date: Date, ev: MouseEvent) {
|
||||
private handleDateContextMenu(date: Date, x?: number, y?: number) {
|
||||
// Can be extended to show custom context menu
|
||||
// For now, allow default browser context menu
|
||||
}
|
||||
|
|
@ -964,20 +1144,20 @@ export class CalendarComponent extends Component {
|
|||
/**
|
||||
* Handle click on time slot (week/day view)
|
||||
*/
|
||||
private handleTimeSlotClick(date: Date, ev: MouseEvent) {
|
||||
private handleTimeSlotClick(dateTime: Date) {
|
||||
// Single click on time slot - could show tooltip or select
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle double click on time slot - opens quick capture with time
|
||||
*/
|
||||
private handleTimeSlotDoubleClick(date: Date, ev: MouseEvent) {
|
||||
private handleTimeSlotDoubleClick(dateTime: Date) {
|
||||
// Create task with specific time
|
||||
new QuickCaptureModal(
|
||||
this.app,
|
||||
this.plugin,
|
||||
{ dueDate: date },
|
||||
true,
|
||||
{ dueDate: dateTime },
|
||||
true
|
||||
).open();
|
||||
}
|
||||
|
||||
|
|
@ -996,7 +1176,7 @@ export class CalendarComponent extends Component {
|
|||
private onDayClick = (
|
||||
ev: MouseEvent,
|
||||
day: number,
|
||||
options: { behavior: "open-quick-capture" | "open-task-view" },
|
||||
options: { behavior: "open-quick-capture" | "open-task-view" }
|
||||
) => {
|
||||
const dayDate = new Date(day);
|
||||
if (this.currentViewMode === "year") {
|
||||
|
|
@ -1008,7 +1188,7 @@ export class CalendarComponent extends Component {
|
|||
this.app,
|
||||
this.plugin,
|
||||
{ dueDate: dayDate },
|
||||
true,
|
||||
true
|
||||
).open();
|
||||
}
|
||||
};
|
||||
|
|
@ -1079,7 +1259,7 @@ export class CalendarComponent extends Component {
|
|||
task.metadata.startDate !== task.metadata.dueDate
|
||||
) {
|
||||
const taskStart = startOfDay(
|
||||
new Date(task.metadata.startDate),
|
||||
new Date(task.metadata.startDate)
|
||||
);
|
||||
const taskDue = startOfDay(new Date(task.metadata.dueDate));
|
||||
if (isBefore(taskStart, taskDue)) {
|
||||
|
|
@ -1111,7 +1291,7 @@ export class CalendarComponent extends Component {
|
|||
|
||||
private convertTasksToTGEvents(): AdapterCalendarEvent[] {
|
||||
const tasksWithDates = this.tasks.filter((task) =>
|
||||
hasDateInformation(task),
|
||||
hasDateInformation(task)
|
||||
);
|
||||
return tasksToCalendarEvents(tasksWithDates);
|
||||
}
|
||||
|
|
@ -1122,19 +1302,19 @@ export class CalendarComponent extends Component {
|
|||
return this.tasks.filter((task) => {
|
||||
if (task.metadata.dueDate) {
|
||||
const dueDate = this.normalizeDateToDay(
|
||||
new Date(task.metadata.dueDate),
|
||||
new Date(task.metadata.dueDate)
|
||||
);
|
||||
if (dueDate.getTime() === targetTime) return true;
|
||||
}
|
||||
if (task.metadata.scheduledDate) {
|
||||
const scheduledDate = this.normalizeDateToDay(
|
||||
new Date(task.metadata.scheduledDate),
|
||||
new Date(task.metadata.scheduledDate)
|
||||
);
|
||||
if (scheduledDate.getTime() === targetTime) return true;
|
||||
}
|
||||
if (task.metadata.startDate) {
|
||||
const startDate = this.normalizeDateToDay(
|
||||
new Date(task.metadata.startDate),
|
||||
new Date(task.metadata.startDate)
|
||||
);
|
||||
if (startDate.getTime() === targetTime) return true;
|
||||
}
|
||||
|
|
@ -1174,17 +1354,25 @@ export class CalendarComponent extends Component {
|
|||
const endOfWeek = this.currentDate.clone().endOf("week");
|
||||
if (startOfWeek.month() !== endOfWeek.month()) {
|
||||
if (startOfWeek.year() !== endOfWeek.year()) {
|
||||
return `${startOfWeek.format("MMM D, YYYY")} - ${endOfWeek.format("MMM D, YYYY")}`;
|
||||
return `${startOfWeek.format(
|
||||
"MMM D, YYYY"
|
||||
)} - ${endOfWeek.format("MMM D, YYYY")}`;
|
||||
}
|
||||
return `${startOfWeek.format("MMM D")} - ${endOfWeek.format("MMM D, YYYY")}`;
|
||||
return `${startOfWeek.format("MMM D")} - ${endOfWeek.format(
|
||||
"MMM D, YYYY"
|
||||
)}`;
|
||||
}
|
||||
return `${startOfWeek.format("MMM D")} - ${endOfWeek.format("D, YYYY")}`;
|
||||
return `${startOfWeek.format("MMM D")} - ${endOfWeek.format(
|
||||
"D, YYYY"
|
||||
)}`;
|
||||
}
|
||||
case "day":
|
||||
return this.currentDate.format("dddd, MMMM D, YYYY");
|
||||
case "agenda": {
|
||||
const endOfAgenda = this.currentDate.clone().add(6, "days");
|
||||
return `${this.currentDate.format("MMM D")} - ${endOfAgenda.format("MMM D, YYYY")}`;
|
||||
return `${this.currentDate.format(
|
||||
"MMM D"
|
||||
)} - ${endOfAgenda.format("MMM D, YYYY")}`;
|
||||
}
|
||||
default:
|
||||
return this.currentDate.format("MMMM YYYY");
|
||||
|
|
@ -1193,7 +1381,7 @@ export class CalendarComponent extends Component {
|
|||
|
||||
private getEffectiveCalendarConfig(): Partial<CalendarSpecificConfig> {
|
||||
const baseCfg = this.plugin.settings.viewConfiguration.find(
|
||||
(v) => v.id === this.viewId,
|
||||
(v) => v.id === this.viewId
|
||||
)?.specificConfig as Partial<CalendarSpecificConfig> | undefined;
|
||||
|
||||
return { ...(baseCfg ?? {}), ...(this.configOverride ?? {}) };
|
||||
|
|
|
|||
|
|
@ -381,13 +381,22 @@
|
|||
|
||||
/* Vertical resize handle (time view) */
|
||||
.tg-resize-v {
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 8px;
|
||||
cursor: row-resize;
|
||||
}
|
||||
|
||||
.tg-resize-v.tg-top {
|
||||
top: 0;
|
||||
bottom: auto;
|
||||
}
|
||||
|
||||
.tg-resize-v.tg-bottom {
|
||||
bottom: 0;
|
||||
top: auto;
|
||||
}
|
||||
|
||||
/* Ghost element for drag preview */
|
||||
.tg-ghost-event {
|
||||
position: absolute;
|
||||
|
|
|
|||
240
src/utils/adapters/TaskCalendarAdapter.ts
Normal file
240
src/utils/adapters/TaskCalendarAdapter.ts
Normal file
|
|
@ -0,0 +1,240 @@
|
|||
/**
|
||||
* TaskCalendarAdapter - Adapter for converting Task objects to CalendarEvent format
|
||||
*
|
||||
* This adapter handles the conversion between Task objects (from TaskGenius)
|
||||
* and CalendarEvent objects (expected by @taskgenius/calendar).
|
||||
*
|
||||
* Multi-day span priority (as per user requirements):
|
||||
* 1. startDate → dueDate (highest priority)
|
||||
* 2. startDate → scheduledDate
|
||||
* 3. scheduledDate → dueDate (lowest priority)
|
||||
*/
|
||||
|
||||
import { Task, StandardTaskMetadata } from "@/types/task";
|
||||
import { format } from "date-fns";
|
||||
import { isDateOnly } from "@/utils/date/date-utils";
|
||||
|
||||
/**
|
||||
* CalendarEvent interface as expected by @taskgenius/calendar
|
||||
* Note: 'end' is required by the library, for single-day events it should equal 'start'
|
||||
*/
|
||||
export interface CalendarEvent {
|
||||
id: string;
|
||||
title: string;
|
||||
start: string; // ISO 8601 format: 'YYYY-MM-DD HH:mm'
|
||||
end: string; // ISO 8601 format: 'YYYY-MM-DD HH:mm' (required, use start for single-day)
|
||||
allDay?: boolean;
|
||||
color?: string;
|
||||
metadata?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Date range for a task
|
||||
*/
|
||||
interface TaskDateRange {
|
||||
start: number;
|
||||
end?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a Task to a CalendarEvent
|
||||
*
|
||||
* @param task - The task to convert
|
||||
* @returns CalendarEvent object or null if task has no date
|
||||
*/
|
||||
export function taskToCalendarEvent(task: Task): CalendarEvent | null {
|
||||
const dateRange = calculateTaskDateRange(task);
|
||||
|
||||
if (!dateRange.start) {
|
||||
return null; // No date information, skip this task
|
||||
}
|
||||
|
||||
const isStartDateOnly = isDateOnly(dateRange.start);
|
||||
const isEndDateOnly = !dateRange.end || isDateOnly(dateRange.end);
|
||||
const isAllDay = isStartDateOnly && isEndDateOnly;
|
||||
|
||||
const startFormatted = formatDateForCalendar(dateRange.start, isAllDay);
|
||||
const endFormatted = dateRange.end
|
||||
? formatDateForCalendar(dateRange.end, isAllDay)
|
||||
: startFormatted; // For single-day tasks, end = start
|
||||
|
||||
return {
|
||||
id: task.id,
|
||||
title: task.content,
|
||||
start: startFormatted,
|
||||
end: endFormatted,
|
||||
allDay: isAllDay,
|
||||
color: getTaskColor(task),
|
||||
metadata: {
|
||||
originalTask: task,
|
||||
priority: task.metadata.priority,
|
||||
tags: task.metadata.tags,
|
||||
project: task.metadata.project,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert multiple tasks to calendar events
|
||||
* Filters out tasks without dates
|
||||
*
|
||||
* @param tasks - Array of tasks
|
||||
* @returns Array of CalendarEvent objects
|
||||
*/
|
||||
export function tasksToCalendarEvents(tasks: Task[]): CalendarEvent[] {
|
||||
return tasks
|
||||
.map((task) => taskToCalendarEvent(task))
|
||||
.filter((event): event is CalendarEvent => event !== null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the date range for a task
|
||||
*
|
||||
* Priority for multi-day span:
|
||||
* 1. startDate → dueDate (highest priority)
|
||||
* 2. startDate → scheduledDate
|
||||
* 3. scheduledDate → dueDate (lowest priority)
|
||||
*
|
||||
* For single-day tasks:
|
||||
* - Priority: dueDate > scheduledDate > startDate
|
||||
*
|
||||
* @param task - The task to analyze
|
||||
* @returns Object with start and optional end timestamp
|
||||
*/
|
||||
function calculateTaskDateRange(task: Task): TaskDateRange {
|
||||
const { startDate, dueDate, scheduledDate } = task.metadata;
|
||||
|
||||
// Priority 1: startDate → dueDate (multi-day span)
|
||||
if (startDate && dueDate && startDate !== dueDate && startDate < dueDate) {
|
||||
return { start: startDate, end: dueDate };
|
||||
}
|
||||
|
||||
// Priority 2: startDate → scheduledDate (multi-day span)
|
||||
if (
|
||||
startDate &&
|
||||
scheduledDate &&
|
||||
startDate !== scheduledDate &&
|
||||
startDate < scheduledDate
|
||||
) {
|
||||
return { start: startDate, end: scheduledDate };
|
||||
}
|
||||
|
||||
// Priority 3: scheduledDate → dueDate (multi-day span)
|
||||
if (
|
||||
scheduledDate &&
|
||||
dueDate &&
|
||||
scheduledDate !== dueDate &&
|
||||
scheduledDate < dueDate
|
||||
) {
|
||||
return { start: scheduledDate, end: dueDate };
|
||||
}
|
||||
|
||||
// Single-day task: priority dueDate > scheduledDate > startDate
|
||||
const singleDate = dueDate || scheduledDate || startDate;
|
||||
|
||||
if (singleDate) {
|
||||
return { start: singleDate };
|
||||
}
|
||||
|
||||
// No date information
|
||||
return { start: 0 };
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the display color for a task based on its status and priority
|
||||
*
|
||||
* @param task - The task
|
||||
* @returns CSS color value
|
||||
*/
|
||||
function getTaskColor(task: Task): string {
|
||||
// Completed tasks use muted color
|
||||
if (task.completed) {
|
||||
return "var(--text-muted)";
|
||||
}
|
||||
|
||||
// Cancelled tasks
|
||||
if (task.metadata.cancelledDate) {
|
||||
return "var(--text-faint)";
|
||||
}
|
||||
|
||||
// Color by priority
|
||||
const priority = task.metadata.priority ?? 3; // Default to medium priority
|
||||
|
||||
switch (priority) {
|
||||
case 1: // Highest
|
||||
return "var(--color-red)";
|
||||
case 2: // High
|
||||
return "var(--color-orange)";
|
||||
case 3: // Medium
|
||||
return "var(--color-yellow)";
|
||||
case 4: // Low
|
||||
return "var(--color-green)";
|
||||
case 5: // Lowest
|
||||
return "var(--color-blue)";
|
||||
default:
|
||||
return "var(--interactive-accent)";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a timestamp to ISO 8601 date-time string for calendar
|
||||
* Format: 'YYYY-MM-DD HH:mm'
|
||||
*
|
||||
* @param timestamp - Unix timestamp in milliseconds
|
||||
* @returns Formatted date string
|
||||
*/
|
||||
function formatDateForCalendar(timestamp: number, isAllDay: boolean): string {
|
||||
const date = new Date(timestamp);
|
||||
if (isAllDay) {
|
||||
return format(date, "yyyy-MM-dd");
|
||||
}
|
||||
return format(date, "yyyy-MM-dd HH:mm");
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the Task object from a CalendarEvent's metadata
|
||||
*
|
||||
* @param event - The calendar event
|
||||
* @returns Original Task object or null
|
||||
*/
|
||||
export function getTaskFromEvent(event: CalendarEvent): Task | null {
|
||||
return (event.metadata?.originalTask as Task) ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a task has any date information
|
||||
*
|
||||
* @param task - The task to check
|
||||
* @returns true if task has at least one date field
|
||||
*/
|
||||
export function hasDateInformation(task: Task): boolean {
|
||||
const { startDate, dueDate, scheduledDate } = task.metadata;
|
||||
return !!(startDate || dueDate || scheduledDate);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the primary date for a task (for sorting purposes)
|
||||
* Priority: dueDate > scheduledDate > startDate
|
||||
*
|
||||
* @param task - The task
|
||||
* @returns Primary date timestamp or null
|
||||
*/
|
||||
export function getPrimaryDate(task: Task): number | null {
|
||||
const { startDate, dueDate, scheduledDate } = task.metadata;
|
||||
return dueDate ?? scheduledDate ?? startDate ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a task spans multiple days
|
||||
*
|
||||
* @param task - The task to check
|
||||
* @returns true if task has valid start and end dates that differ
|
||||
*/
|
||||
export function isMultiDayTask(task: Task): boolean {
|
||||
const dateRange = calculateTaskDateRange(task);
|
||||
return !!(
|
||||
dateRange.start &&
|
||||
dateRange.end &&
|
||||
dateRange.start !== dateRange.end
|
||||
);
|
||||
}
|
||||
418
styles.css
418
styles.css
File diff suppressed because one or more lines are too long
Loading…
Reference in a new issue