mirror of
https://github.com/taskgenius/taskgenius-plugin.git
synced 2026-07-22 06:40:25 +00:00
feat(time-parsing): improve emoji-based date type detection and time preservation
This commit is contained in:
parent
c35880eb6f
commit
82a20200a3
15 changed files with 2531 additions and 1318 deletions
|
|
@ -1 +1 @@
|
|||
Subproject commit 3a269f99499732440f56573a382f6bc6a1dcd0aa
|
||||
Subproject commit 65aaa0ee9840428dc14f96fc120c004e5cbaf413
|
||||
813
pnpm-lock.yaml
813
pnpm-lock.yaml
File diff suppressed because it is too large
Load diff
|
|
@ -57,10 +57,6 @@ import "@/styles/calendar/event.css";
|
|||
import "@/styles/calendar/badge.css";
|
||||
import { t } from "@/translations/helper";
|
||||
|
||||
// Import original view implementations for agenda/year (legacy, kept for reference)
|
||||
import { AgendaView } from "./views/agenda-view";
|
||||
import { YearView } from "./views/year-view";
|
||||
|
||||
// Import new TG-based view implementations
|
||||
import { TGAgendaView } from "./views/tg-agenda-view";
|
||||
import { TGYearView } from "./views/tg-year-view";
|
||||
|
|
@ -273,10 +269,6 @@ export class CalendarComponent extends Component {
|
|||
// View registry for custom views (agenda, year, and user-defined)
|
||||
private viewRegistry: ViewRegistry = new ViewRegistry();
|
||||
|
||||
// Original view components (legacy, kept for backward compatibility)
|
||||
private agendaView: AgendaView | null = null;
|
||||
private yearView: YearView | null = null;
|
||||
|
||||
// Badge events cache
|
||||
private badgeEventsCache: Map<string, CalendarEvent[]> = new Map();
|
||||
|
||||
|
|
@ -381,16 +373,6 @@ export class CalendarComponent extends Component {
|
|||
this.tgCalendar = null;
|
||||
}
|
||||
|
||||
// Clean up original views
|
||||
if (this.agendaView) {
|
||||
this.removeChild(this.agendaView);
|
||||
this.agendaView = null;
|
||||
}
|
||||
if (this.yearView) {
|
||||
this.removeChild(this.yearView);
|
||||
this.yearView = null;
|
||||
}
|
||||
|
||||
this.containerEl.empty();
|
||||
}
|
||||
|
||||
|
|
@ -462,10 +444,6 @@ export class CalendarComponent extends Component {
|
|||
this.render();
|
||||
}
|
||||
|
||||
public get currentViewComponent() {
|
||||
return this.agendaView || this.yearView || null;
|
||||
}
|
||||
|
||||
public getBadgeEventsForDate(date: Date): CalendarEvent[] {
|
||||
const dateKey = this.formatDateKey(date);
|
||||
|
||||
|
|
@ -723,14 +701,6 @@ export class CalendarComponent extends Component {
|
|||
this.tgCalendar.destroy();
|
||||
this.tgCalendar = null;
|
||||
}
|
||||
if (this.agendaView) {
|
||||
this.removeChild(this.agendaView);
|
||||
this.agendaView = null;
|
||||
}
|
||||
if (this.yearView) {
|
||||
this.removeChild(this.yearView);
|
||||
this.yearView = null;
|
||||
}
|
||||
|
||||
// Update view class
|
||||
this.viewContainerEl.removeClass(
|
||||
|
|
@ -928,7 +898,7 @@ export class CalendarComponent extends Component {
|
|||
},
|
||||
draggable: {
|
||||
enabled: true,
|
||||
snapMinutes: 1440,
|
||||
snapMinutes: 15, // 15-minute snap for range selection support
|
||||
ghostOpacity: 0.5,
|
||||
},
|
||||
theme: {
|
||||
|
|
@ -955,6 +925,11 @@ export class CalendarComponent extends Component {
|
|||
this.handleTimeSlotClick(dateTime),
|
||||
onTimeSlotDoubleClick: (dateTime: Date) =>
|
||||
this.handleTimeSlotDoubleClick(dateTime),
|
||||
// Range selection (drag to select multiple cells)
|
||||
onDateRangeSelect: (startDate: Date, endDate: Date) =>
|
||||
this.handleDateRangeSelect(startDate, endDate),
|
||||
onTimeRangeSelect: (startDateTime: Date, endDateTime: Date) =>
|
||||
this.handleTimeRangeSelect(startDateTime, endDateTime),
|
||||
onRenderDateCell: (ctx: any) => this.handleTGRenderDateCell(ctx),
|
||||
// Custom event rendering - adds checkbox for task completion
|
||||
onRenderEvent: (ctx: EventRenderContext) =>
|
||||
|
|
@ -1011,7 +986,7 @@ export class CalendarComponent extends Component {
|
|||
},
|
||||
draggable: {
|
||||
enabled: true,
|
||||
snapMinutes: 1440, // Date-only drag
|
||||
snapMinutes: 15, // 15-minute snap for range selection support
|
||||
ghostOpacity: 0.5,
|
||||
},
|
||||
theme: {
|
||||
|
|
@ -1042,6 +1017,11 @@ export class CalendarComponent extends Component {
|
|||
this.handleTimeSlotClick(dateTime),
|
||||
onTimeSlotDoubleClick: (dateTime: Date) =>
|
||||
this.handleTimeSlotDoubleClick(dateTime),
|
||||
// Range selection (drag to select multiple cells)
|
||||
onDateRangeSelect: (startDate: Date, endDate: Date) =>
|
||||
this.handleDateRangeSelect(startDate, endDate),
|
||||
onTimeRangeSelect: (startDateTime: Date, endDateTime: Date) =>
|
||||
this.handleTimeRangeSelect(startDateTime, endDateTime),
|
||||
// Custom cell rendering
|
||||
onRenderDateCell: (ctx: any) => this.handleTGRenderDateCell(ctx),
|
||||
// Custom event rendering - adds checkbox for task completion
|
||||
|
|
@ -1102,46 +1082,6 @@ export class CalendarComponent extends Component {
|
|||
this.tgCalendar.goToDate(this.currentDate.toDate());
|
||||
}
|
||||
|
||||
private renderAgendaView() {
|
||||
this.agendaView = new AgendaView(
|
||||
this.app,
|
||||
this.plugin,
|
||||
this.viewContainerEl,
|
||||
this.currentDate,
|
||||
this.events,
|
||||
{
|
||||
onEventClick: this.onEventClick,
|
||||
onEventHover: this.onEventHover,
|
||||
onEventContextMenu: this.onEventContextMenu,
|
||||
onEventComplete: this.onEventComplete,
|
||||
},
|
||||
);
|
||||
this.addChild(this.agendaView);
|
||||
this.agendaView.updateEvents(this.events);
|
||||
}
|
||||
|
||||
private renderYearView() {
|
||||
const config = this.getEffectiveCalendarConfig();
|
||||
this.yearView = new YearView(
|
||||
this.app,
|
||||
this.plugin,
|
||||
this.viewContainerEl,
|
||||
this.currentDate,
|
||||
this.events,
|
||||
{
|
||||
onEventClick: this.onEventClick,
|
||||
onEventHover: this.onEventHover,
|
||||
onDayClick: this.onDayClick,
|
||||
onDayHover: this.onDayHover,
|
||||
onMonthClick: this.onMonthClick,
|
||||
onMonthHover: this.onMonthHover,
|
||||
},
|
||||
config,
|
||||
);
|
||||
this.addChild(this.yearView);
|
||||
this.yearView.updateEvents(this.events);
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Event Handlers
|
||||
// ============================================
|
||||
|
|
@ -1790,6 +1730,38 @@ export class CalendarComponent extends Component {
|
|||
).open();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle date range selection in month view
|
||||
* Opens quick capture modal with start and due dates pre-filled
|
||||
*/
|
||||
private handleDateRangeSelect(startDate: Date, endDate: Date) {
|
||||
new QuickCaptureModal(
|
||||
this.app,
|
||||
this.plugin,
|
||||
{
|
||||
startDate: startDate,
|
||||
dueDate: endDate,
|
||||
},
|
||||
true,
|
||||
).open();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle time range selection in week/day view
|
||||
* Opens quick capture modal with start and due dates/times pre-filled
|
||||
*/
|
||||
private handleTimeRangeSelect(startDateTime: Date, endDateTime: Date) {
|
||||
new QuickCaptureModal(
|
||||
this.app,
|
||||
this.plugin,
|
||||
{
|
||||
startDate: startDateTime,
|
||||
dueDate: endDateTime,
|
||||
},
|
||||
true,
|
||||
).open();
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Original Event Handlers (for Agenda/Year views)
|
||||
// ============================================
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import type { EnhancedTimeParsingConfig } from "@/types/time-parsing";
|
|||
|
||||
export function renderTimeParsingSettingsTab(
|
||||
pluginSettingTab: TaskProgressBarSettingTab,
|
||||
containerEl: HTMLElement
|
||||
containerEl: HTMLElement,
|
||||
) {
|
||||
containerEl.createEl("h2", { text: t("Time Parsing Settings") });
|
||||
|
||||
|
|
@ -46,16 +46,17 @@ export function renderTimeParsingSettingsTab(
|
|||
.setName(t("Enable Time Parsing"))
|
||||
.setDesc(
|
||||
t(
|
||||
"Automatically parse natural language time expressions and specific times (12:00, 1:30 PM, 12:00-13:00)"
|
||||
)
|
||||
"Automatically parse natural language time expressions and specific times (12:00, 1:30 PM, 12:00-13:00)",
|
||||
),
|
||||
)
|
||||
.addToggle((toggle) =>
|
||||
toggle
|
||||
.setValue(pluginSettingTab.plugin.settings.timeParsing.enabled)
|
||||
.onChange(async (value) => {
|
||||
pluginSettingTab.plugin.settings.timeParsing.enabled = value;
|
||||
pluginSettingTab.plugin.settings.timeParsing.enabled =
|
||||
value;
|
||||
pluginSettingTab.applySettingsUpdate();
|
||||
})
|
||||
}),
|
||||
);
|
||||
|
||||
// Remove Original Text
|
||||
|
|
@ -66,27 +67,30 @@ export function renderTimeParsingSettingsTab(
|
|||
toggle
|
||||
.setValue(
|
||||
pluginSettingTab.plugin.settings.timeParsing
|
||||
?.removeOriginalText ?? true
|
||||
?.removeOriginalText ?? true,
|
||||
)
|
||||
.onChange(async (value) => {
|
||||
if (!pluginSettingTab.plugin.settings.timeParsing) return;
|
||||
pluginSettingTab.plugin.settings.timeParsing.removeOriginalText =
|
||||
value;
|
||||
pluginSettingTab.applySettingsUpdate();
|
||||
})
|
||||
}),
|
||||
);
|
||||
|
||||
// Supported Languages
|
||||
containerEl.createEl("h3", { text: t("Supported Languages") });
|
||||
containerEl.createEl("p", {
|
||||
text: t(
|
||||
"Currently supports English and Chinese time expressions. More languages may be added in future updates."
|
||||
),
|
||||
cls: "setting-item-description",
|
||||
});
|
||||
new Setting(containerEl)
|
||||
.setName(t("Supported Languages"))
|
||||
.setDesc(
|
||||
t(
|
||||
"Currently supports English and Chinese time expressions. More languages may be added in future updates.",
|
||||
),
|
||||
)
|
||||
.setHeading();
|
||||
|
||||
// Date Keywords Configuration
|
||||
containerEl.createEl("h3", { text: t("Date Keywords Configuration") });
|
||||
new Setting(containerEl)
|
||||
.setName(t("Date Keywords Configuration"))
|
||||
.setDesc(t("Configure keywords for date parsing"))
|
||||
.setHeading();
|
||||
|
||||
// Start Date Keywords
|
||||
new Setting(containerEl)
|
||||
|
|
@ -154,8 +158,10 @@ export function renderTimeParsingSettingsTab(
|
|||
text.inputEl.rows = 2;
|
||||
});
|
||||
|
||||
// Time Format Configuration
|
||||
containerEl.createEl("h3", { text: t("Time Format Configuration") });
|
||||
new Setting(containerEl)
|
||||
.setName(t("Time Format Configuration"))
|
||||
.setDesc(t("Configure the format of time expressions"))
|
||||
.setHeading();
|
||||
|
||||
// Preferred Time Format
|
||||
new Setting(containerEl)
|
||||
|
|
@ -165,16 +171,24 @@ export function renderTimeParsingSettingsTab(
|
|||
dropdown
|
||||
.addOption("12h", t("12-hour format (1:30 PM)"))
|
||||
.addOption("24h", t("24-hour format (13:30)"))
|
||||
.setValue(pluginSettingTab.plugin.settings.timeParsing.timeDefaults?.preferredFormat || "24h")
|
||||
.setValue(
|
||||
pluginSettingTab.plugin.settings.timeParsing.timeDefaults
|
||||
?.preferredFormat || "24h",
|
||||
)
|
||||
.onChange(async (value: "12h" | "24h") => {
|
||||
if (!pluginSettingTab.plugin.settings.timeParsing.timeDefaults) {
|
||||
pluginSettingTab.plugin.settings.timeParsing.timeDefaults = {
|
||||
preferredFormat: value,
|
||||
defaultPeriod: "AM",
|
||||
midnightCrossing: "next-day",
|
||||
};
|
||||
if (
|
||||
!pluginSettingTab.plugin.settings.timeParsing
|
||||
.timeDefaults
|
||||
) {
|
||||
pluginSettingTab.plugin.settings.timeParsing.timeDefaults =
|
||||
{
|
||||
preferredFormat: value,
|
||||
defaultPeriod: "AM",
|
||||
midnightCrossing: "next-day",
|
||||
};
|
||||
} else {
|
||||
pluginSettingTab.plugin.settings.timeParsing.timeDefaults.preferredFormat = value;
|
||||
pluginSettingTab.plugin.settings.timeParsing.timeDefaults.preferredFormat =
|
||||
value;
|
||||
}
|
||||
pluginSettingTab.applySettingsUpdate();
|
||||
});
|
||||
|
|
@ -188,16 +202,24 @@ export function renderTimeParsingSettingsTab(
|
|||
dropdown
|
||||
.addOption("AM", t("AM (Morning)"))
|
||||
.addOption("PM", t("PM (Afternoon/Evening)"))
|
||||
.setValue(pluginSettingTab.plugin.settings.timeParsing.timeDefaults?.defaultPeriod || "AM")
|
||||
.setValue(
|
||||
pluginSettingTab.plugin.settings.timeParsing.timeDefaults
|
||||
?.defaultPeriod || "AM",
|
||||
)
|
||||
.onChange(async (value: "AM" | "PM") => {
|
||||
if (!pluginSettingTab.plugin.settings.timeParsing.timeDefaults) {
|
||||
pluginSettingTab.plugin.settings.timeParsing.timeDefaults = {
|
||||
preferredFormat: "24h",
|
||||
defaultPeriod: value,
|
||||
midnightCrossing: "next-day",
|
||||
};
|
||||
if (
|
||||
!pluginSettingTab.plugin.settings.timeParsing
|
||||
.timeDefaults
|
||||
) {
|
||||
pluginSettingTab.plugin.settings.timeParsing.timeDefaults =
|
||||
{
|
||||
preferredFormat: "24h",
|
||||
defaultPeriod: value,
|
||||
midnightCrossing: "next-day",
|
||||
};
|
||||
} else {
|
||||
pluginSettingTab.plugin.settings.timeParsing.timeDefaults.defaultPeriod = value;
|
||||
pluginSettingTab.plugin.settings.timeParsing.timeDefaults.defaultPeriod =
|
||||
value;
|
||||
}
|
||||
pluginSettingTab.applySettingsUpdate();
|
||||
});
|
||||
|
|
@ -206,22 +228,37 @@ export function renderTimeParsingSettingsTab(
|
|||
// Midnight Crossing Behavior
|
||||
new Setting(containerEl)
|
||||
.setName(t("Midnight Crossing Behavior"))
|
||||
.setDesc(t("How to handle time ranges that cross midnight (e.g., 23:00-01:00)"))
|
||||
.setDesc(
|
||||
t(
|
||||
"How to handle time ranges that cross midnight (e.g., 23:00-01:00)",
|
||||
),
|
||||
)
|
||||
.addDropdown((dropdown) => {
|
||||
dropdown
|
||||
.addOption("next-day", t("Next day (23:00 today - 01:00 tomorrow)"))
|
||||
.addOption(
|
||||
"next-day",
|
||||
t("Next day (23:00 today - 01:00 tomorrow)"),
|
||||
)
|
||||
.addOption("same-day", t("Same day (treat as error)"))
|
||||
.addOption("error", t("Show error"))
|
||||
.setValue(pluginSettingTab.plugin.settings.timeParsing.timeDefaults?.midnightCrossing || "next-day")
|
||||
.setValue(
|
||||
pluginSettingTab.plugin.settings.timeParsing.timeDefaults
|
||||
?.midnightCrossing || "next-day",
|
||||
)
|
||||
.onChange(async (value: "next-day" | "same-day" | "error") => {
|
||||
if (!pluginSettingTab.plugin.settings.timeParsing.timeDefaults) {
|
||||
pluginSettingTab.plugin.settings.timeParsing.timeDefaults = {
|
||||
preferredFormat: "24h",
|
||||
defaultPeriod: "AM",
|
||||
midnightCrossing: value,
|
||||
};
|
||||
if (
|
||||
!pluginSettingTab.plugin.settings.timeParsing
|
||||
.timeDefaults
|
||||
) {
|
||||
pluginSettingTab.plugin.settings.timeParsing.timeDefaults =
|
||||
{
|
||||
preferredFormat: "24h",
|
||||
defaultPeriod: "AM",
|
||||
midnightCrossing: value,
|
||||
};
|
||||
} else {
|
||||
pluginSettingTab.plugin.settings.timeParsing.timeDefaults.midnightCrossing = value;
|
||||
pluginSettingTab.plugin.settings.timeParsing.timeDefaults.midnightCrossing =
|
||||
value;
|
||||
}
|
||||
pluginSettingTab.applySettingsUpdate();
|
||||
});
|
||||
|
|
@ -232,19 +269,30 @@ export function renderTimeParsingSettingsTab(
|
|||
.setName(t("Time Range Separators"))
|
||||
.setDesc(t("Characters used to separate time ranges (comma-separated)"))
|
||||
.addTextArea((text) => {
|
||||
const separators = pluginSettingTab.plugin.settings.timeParsing.timePatterns?.rangeSeparators || ["-", "~", "~"];
|
||||
const separators = pluginSettingTab.plugin.settings.timeParsing
|
||||
.timePatterns?.rangeSeparators || ["-", "~", "~"];
|
||||
text.setValue(separators.join(", "))
|
||||
.setPlaceholder("-, ~, ~, ' - ', ' ~ '")
|
||||
.onChange(async (value) => {
|
||||
if (!pluginSettingTab.plugin.settings.timeParsing.timePatterns) {
|
||||
pluginSettingTab.plugin.settings.timeParsing.timePatterns = {
|
||||
singleTime: [],
|
||||
timeRange: [],
|
||||
rangeSeparators: value.split(",").map(s => s.trim()).filter(s => s.length > 0),
|
||||
};
|
||||
if (
|
||||
!pluginSettingTab.plugin.settings.timeParsing
|
||||
.timePatterns
|
||||
) {
|
||||
pluginSettingTab.plugin.settings.timeParsing.timePatterns =
|
||||
{
|
||||
singleTime: [],
|
||||
timeRange: [],
|
||||
rangeSeparators: value
|
||||
.split(",")
|
||||
.map((s) => s.trim())
|
||||
.filter((s) => s.length > 0),
|
||||
};
|
||||
} else {
|
||||
pluginSettingTab.plugin.settings.timeParsing.timePatterns.rangeSeparators =
|
||||
value.split(",").map(s => s.trim()).filter(s => s.length > 0);
|
||||
pluginSettingTab.plugin.settings.timeParsing.timePatterns.rangeSeparators =
|
||||
value
|
||||
.split(",")
|
||||
.map((s) => s.trim())
|
||||
.filter((s) => s.length > 0);
|
||||
}
|
||||
pluginSettingTab.applySettingsUpdate();
|
||||
});
|
||||
|
|
@ -252,7 +300,10 @@ export function renderTimeParsingSettingsTab(
|
|||
});
|
||||
|
||||
// Examples
|
||||
containerEl.createEl("h3", { text: t("Examples") });
|
||||
new Setting(containerEl)
|
||||
.setName(t("Examples"))
|
||||
.setDesc(t("Examples of time expressions"))
|
||||
.setHeading();
|
||||
const examplesEl = containerEl.createEl("div", {
|
||||
cls: "time-parsing-examples",
|
||||
});
|
||||
|
|
@ -265,10 +316,22 @@ export function renderTimeParsingSettingsTab(
|
|||
{ input: "明天开会", output: "开会 📅 2025-01-05" },
|
||||
{ input: "3天后完成", output: "完成 📅 2025-01-07" },
|
||||
// Time examples
|
||||
{ input: "meeting at 2:30 PM", output: "meeting 📅 2025-01-04 ⏰ 14:30" },
|
||||
{ input: "workshop 9:00-17:00", output: "workshop 📅 2025-01-04 ⏰ 09:00-17:00" },
|
||||
{ input: "call scheduled 12:00", output: "call 📅 2025-01-04 ⏰ 12:00" },
|
||||
{ input: "lunch 12:00~13:00", output: "lunch 📅 2025-01-04 ⏰ 12:00-13:00" },
|
||||
{
|
||||
input: "meeting at 2:30 PM",
|
||||
output: "meeting 📅 2025-01-04 ⏰ 14:30",
|
||||
},
|
||||
{
|
||||
input: "workshop 9:00-17:00",
|
||||
output: "workshop 📅 2025-01-04 ⏰ 09:00-17:00",
|
||||
},
|
||||
{
|
||||
input: "call scheduled 12:00",
|
||||
output: "call 📅 2025-01-04 ⏰ 12:00",
|
||||
},
|
||||
{
|
||||
input: "lunch 12:00~13:00",
|
||||
output: "lunch 📅 2025-01-04 ⏰ 12:00-13:00",
|
||||
},
|
||||
];
|
||||
|
||||
examples.forEach((example) => {
|
||||
|
|
|
|||
|
|
@ -674,6 +674,22 @@ export class MarkdownTaskParser {
|
|||
);
|
||||
}
|
||||
|
||||
// Fallback: If start date exists but no start time, and we have a due time (default context)
|
||||
// but no due date, assume the time belongs to the start date.
|
||||
// This handles cases like "🛫 2025-11-29 18:00" where the time defaults to "due" context
|
||||
// but should actually be associated with the start date.
|
||||
if (
|
||||
dates.startDate &&
|
||||
!timeComponents.startTime &&
|
||||
!dates.dueDate &&
|
||||
timeComponents.dueTime
|
||||
) {
|
||||
enhancedDates.startDateTime = combineDateTime(
|
||||
dates.startDate,
|
||||
timeComponents.dueTime,
|
||||
);
|
||||
}
|
||||
|
||||
// Combine due date with due time
|
||||
if (dates.dueDate && timeComponents.dueTime) {
|
||||
enhancedDates.dueDateTime = combineDateTime(
|
||||
|
|
|
|||
|
|
@ -82,7 +82,7 @@ export class FileTaskManagerImpl implements FileTaskManager {
|
|||
constructor(
|
||||
private app: App,
|
||||
private fileSourceConfig?: FileSourceConfiguration,
|
||||
timeParsingService?: TimeParsingService
|
||||
timeParsingService?: TimeParsingService,
|
||||
) {
|
||||
this.timeParsingService = timeParsingService;
|
||||
}
|
||||
|
|
@ -92,7 +92,7 @@ export class FileTaskManagerImpl implements FileTaskManager {
|
|||
*/
|
||||
entryToFileTask(
|
||||
entry: BasesEntry,
|
||||
mapping: FileTaskPropertyMapping = DEFAULT_FILE_TASK_MAPPING
|
||||
mapping: FileTaskPropertyMapping = DEFAULT_FILE_TASK_MAPPING,
|
||||
): FileTask {
|
||||
const properties = entry.properties || {};
|
||||
|
||||
|
|
@ -104,7 +104,7 @@ export class FileTaskManagerImpl implements FileTaskManager {
|
|||
// Log 10% of entries to avoid spam
|
||||
console.log(
|
||||
`[FileTaskManager] Available properties for ${entry.file.name}:`,
|
||||
Object.keys(properties)
|
||||
Object.keys(properties),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -132,29 +132,29 @@ export class FileTaskManagerImpl implements FileTaskManager {
|
|||
// Extract dates
|
||||
const createdDate = this.getDatePropertyValue(
|
||||
entry,
|
||||
mapping.createdDateProperty
|
||||
mapping.createdDateProperty,
|
||||
);
|
||||
const startDate = this.getDatePropertyValue(
|
||||
entry,
|
||||
mapping.startDateProperty
|
||||
mapping.startDateProperty,
|
||||
);
|
||||
const scheduledDate = this.getDatePropertyValue(
|
||||
entry,
|
||||
mapping.scheduledDateProperty
|
||||
mapping.scheduledDateProperty,
|
||||
);
|
||||
const dueDate = this.getDatePropertyValue(
|
||||
entry,
|
||||
mapping.dueDateProperty
|
||||
mapping.dueDateProperty,
|
||||
);
|
||||
const completedDate = this.getDatePropertyValue(
|
||||
entry,
|
||||
mapping.completedDateProperty
|
||||
mapping.completedDateProperty,
|
||||
);
|
||||
|
||||
// Extract other properties
|
||||
const recurrence = this.getPropertyValue(
|
||||
entry,
|
||||
mapping.recurrenceProperty
|
||||
mapping.recurrenceProperty,
|
||||
);
|
||||
const tags =
|
||||
this.getArrayPropertyValue(entry, mapping.tagsProperty) || [];
|
||||
|
|
@ -162,15 +162,15 @@ export class FileTaskManagerImpl implements FileTaskManager {
|
|||
const context = this.getPropertyValue(entry, mapping.contextProperty);
|
||||
const priority = this.getNumberPropertyValue(
|
||||
entry,
|
||||
mapping.priorityProperty
|
||||
mapping.priorityProperty,
|
||||
);
|
||||
const estimatedTime = this.getNumberPropertyValue(
|
||||
entry,
|
||||
mapping.estimatedTimeProperty
|
||||
mapping.estimatedTimeProperty,
|
||||
);
|
||||
const actualTime = this.getNumberPropertyValue(
|
||||
entry,
|
||||
mapping.actualTimeProperty
|
||||
mapping.actualTimeProperty,
|
||||
);
|
||||
|
||||
// Extract time components from content using enhanced time parsing
|
||||
|
|
@ -178,8 +178,8 @@ export class FileTaskManagerImpl implements FileTaskManager {
|
|||
|
||||
// Combine dates with time components to create enhanced datetime objects
|
||||
const enhancedDates = this.combineTimestampsWithTimeComponents(
|
||||
{startDate, dueDate, scheduledDate, completedDate},
|
||||
enhancedMetadata.timeComponents
|
||||
{ startDate, dueDate, scheduledDate, completedDate },
|
||||
enhancedMetadata.timeComponents,
|
||||
);
|
||||
|
||||
const fileTask: FileTask = {
|
||||
|
|
@ -193,21 +193,21 @@ export class FileTaskManagerImpl implements FileTaskManager {
|
|||
children: [], // File tasks don't have children by default
|
||||
|
||||
// Optional properties
|
||||
...(createdDate && {createdDate}),
|
||||
...(startDate && {startDate}),
|
||||
...(scheduledDate && {scheduledDate}),
|
||||
...(dueDate && {dueDate}),
|
||||
...(completedDate && {completedDate}),
|
||||
...(recurrence && {recurrence}),
|
||||
...(project && {project}),
|
||||
...(context && {context}),
|
||||
...(priority && {priority}),
|
||||
...(estimatedTime && {estimatedTime}),
|
||||
...(actualTime && {actualTime}),
|
||||
...(createdDate && { createdDate }),
|
||||
...(startDate && { startDate }),
|
||||
...(scheduledDate && { scheduledDate }),
|
||||
...(dueDate && { dueDate }),
|
||||
...(completedDate && { completedDate }),
|
||||
...(recurrence && { recurrence }),
|
||||
...(project && { project }),
|
||||
...(context && { context }),
|
||||
...(priority && { priority }),
|
||||
...(estimatedTime && { estimatedTime }),
|
||||
...(actualTime && { actualTime }),
|
||||
|
||||
// Enhanced time components
|
||||
...enhancedMetadata,
|
||||
...(enhancedDates && {enhancedDates}),
|
||||
...(enhancedDates && { enhancedDates }),
|
||||
},
|
||||
sourceEntry: entry,
|
||||
isFileTask: true,
|
||||
|
|
@ -222,7 +222,7 @@ export class FileTaskManagerImpl implements FileTaskManager {
|
|||
fileTaskToPropertyUpdates(
|
||||
task: FileTask,
|
||||
mapping: FileTaskPropertyMapping = DEFAULT_FILE_TASK_MAPPING,
|
||||
excludeContent: boolean = false
|
||||
excludeContent: boolean = false,
|
||||
): Record<string, any> {
|
||||
const updates: Record<string, any> = {};
|
||||
|
||||
|
|
@ -230,9 +230,10 @@ export class FileTaskManagerImpl implements FileTaskManager {
|
|||
// Skip content if it was already handled separately (e.g., in handleContentUpdate)
|
||||
if (!excludeContent) {
|
||||
const config = this.fileSourceConfig?.fileTaskProperties;
|
||||
if (config?.contentSource && config.contentSource !== 'filename') {
|
||||
if (config?.contentSource && config.contentSource !== "filename") {
|
||||
// Only update content property if it's not handled by file renaming
|
||||
const shouldUpdateProperty = this.shouldUpdateContentProperty(config);
|
||||
const shouldUpdateProperty =
|
||||
this.shouldUpdateContentProperty(config);
|
||||
if (shouldUpdateProperty) {
|
||||
updates[mapping.contentProperty] = task.content;
|
||||
}
|
||||
|
|
@ -249,7 +250,7 @@ export class FileTaskManagerImpl implements FileTaskManager {
|
|||
mapping.createdDateProperty
|
||||
) {
|
||||
updates[mapping.createdDateProperty] = this.formatDateForProperty(
|
||||
task.metadata.createdDate
|
||||
task.metadata.createdDate,
|
||||
);
|
||||
}
|
||||
if (
|
||||
|
|
@ -257,7 +258,7 @@ export class FileTaskManagerImpl implements FileTaskManager {
|
|||
mapping.startDateProperty
|
||||
) {
|
||||
updates[mapping.startDateProperty] = this.formatDateForProperty(
|
||||
task.metadata.startDate
|
||||
task.metadata.startDate,
|
||||
);
|
||||
}
|
||||
if (
|
||||
|
|
@ -265,12 +266,12 @@ export class FileTaskManagerImpl implements FileTaskManager {
|
|||
mapping.scheduledDateProperty
|
||||
) {
|
||||
updates[mapping.scheduledDateProperty] = this.formatDateForProperty(
|
||||
task.metadata.scheduledDate
|
||||
task.metadata.scheduledDate,
|
||||
);
|
||||
}
|
||||
if (task.metadata.dueDate !== undefined && mapping.dueDateProperty) {
|
||||
updates[mapping.dueDateProperty] = this.formatDateForProperty(
|
||||
task.metadata.dueDate
|
||||
task.metadata.dueDate,
|
||||
);
|
||||
}
|
||||
if (
|
||||
|
|
@ -278,7 +279,7 @@ export class FileTaskManagerImpl implements FileTaskManager {
|
|||
mapping.completedDateProperty
|
||||
) {
|
||||
updates[mapping.completedDateProperty] = this.formatDateForProperty(
|
||||
task.metadata.completedDate
|
||||
task.metadata.completedDate,
|
||||
);
|
||||
}
|
||||
if (
|
||||
|
|
@ -321,10 +322,10 @@ export class FileTaskManagerImpl implements FileTaskManager {
|
|||
*/
|
||||
async updateFileTask(
|
||||
task: FileTask,
|
||||
updates: Partial<FileTask>
|
||||
updates: Partial<FileTask>,
|
||||
): Promise<void> {
|
||||
// Merge updates into the task
|
||||
const updatedTask = {...task, ...updates};
|
||||
const updatedTask = { ...task, ...updates };
|
||||
let contentHandledSeparately = false;
|
||||
|
||||
// Handle content changes - re-extract time components if content changed
|
||||
|
|
@ -333,7 +334,9 @@ export class FileTaskManagerImpl implements FileTaskManager {
|
|||
contentHandledSeparately = true;
|
||||
|
||||
// Re-extract time components from updated content
|
||||
const enhancedMetadata = this.extractTimeComponents(updates.content);
|
||||
const enhancedMetadata = this.extractTimeComponents(
|
||||
updates.content,
|
||||
);
|
||||
|
||||
// Update the task's metadata with new time components
|
||||
if (enhancedMetadata.timeComponents) {
|
||||
|
|
@ -350,7 +353,7 @@ export class FileTaskManagerImpl implements FileTaskManager {
|
|||
scheduledDate: updatedTask.metadata.scheduledDate,
|
||||
completedDate: updatedTask.metadata.completedDate,
|
||||
},
|
||||
enhancedMetadata.timeComponents
|
||||
enhancedMetadata.timeComponents,
|
||||
);
|
||||
|
||||
if (enhancedDates) {
|
||||
|
|
@ -366,22 +369,26 @@ export class FileTaskManagerImpl implements FileTaskManager {
|
|||
const propertyUpdates = this.fileTaskToPropertyUpdates(
|
||||
updatedTask,
|
||||
DEFAULT_FILE_TASK_MAPPING,
|
||||
contentHandledSeparately
|
||||
contentHandledSeparately,
|
||||
);
|
||||
|
||||
console.log(
|
||||
`[FileTaskManager] Updating file task ${task.content} with properties:`,
|
||||
propertyUpdates
|
||||
propertyUpdates,
|
||||
);
|
||||
|
||||
// Update properties through the source entry
|
||||
for (const [key, value] of Object.entries(propertyUpdates)) {
|
||||
try {
|
||||
// Note: updateProperty might be async, so we await it
|
||||
if (typeof task.sourceEntry.updateProperty === 'function') {
|
||||
await Promise.resolve(task.sourceEntry.updateProperty(key, value));
|
||||
if (typeof task.sourceEntry.updateProperty === "function") {
|
||||
await Promise.resolve(
|
||||
task.sourceEntry.updateProperty(key, value),
|
||||
);
|
||||
} else {
|
||||
console.error(`updateProperty method not available on source entry for key: ${key}`);
|
||||
console.error(
|
||||
`updateProperty method not available on source entry for key: ${key}`,
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Failed to update property ${key}:`, error);
|
||||
|
|
@ -394,16 +401,16 @@ export class FileTaskManagerImpl implements FileTaskManager {
|
|||
*/
|
||||
private shouldUpdateContentProperty(config: any): boolean {
|
||||
switch (config.contentSource) {
|
||||
case 'title':
|
||||
case "title":
|
||||
// Only update property if preferFrontmatterTitle is enabled
|
||||
return config.preferFrontmatterTitle === true;
|
||||
case 'h1':
|
||||
case "h1":
|
||||
// H1 updates are handled by file content modification, not property updates
|
||||
return false;
|
||||
case 'custom':
|
||||
case "custom":
|
||||
// Custom fields are always updated via properties
|
||||
return true;
|
||||
case 'filename':
|
||||
case "filename":
|
||||
default:
|
||||
// Filename updates are handled by file renaming
|
||||
return false;
|
||||
|
|
@ -415,17 +422,19 @@ export class FileTaskManagerImpl implements FileTaskManager {
|
|||
*/
|
||||
private async handleContentUpdate(
|
||||
task: FileTask,
|
||||
newContent: string
|
||||
newContent: string,
|
||||
): Promise<void> {
|
||||
const config = this.fileSourceConfig?.fileTaskProperties;
|
||||
|
||||
if (!config) {
|
||||
console.warn('[FileTaskManager] No file source config available, skipping content update');
|
||||
console.warn(
|
||||
"[FileTaskManager] No file source config available, skipping content update",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
switch (config.contentSource) {
|
||||
case 'title':
|
||||
case "title":
|
||||
if (config.preferFrontmatterTitle) {
|
||||
await this.updateFrontmatterTitle(task, newContent);
|
||||
} else {
|
||||
|
|
@ -433,21 +442,27 @@ export class FileTaskManagerImpl implements FileTaskManager {
|
|||
}
|
||||
break;
|
||||
|
||||
case 'h1':
|
||||
case "h1":
|
||||
// For H1 content source, we need to update the first heading in the file
|
||||
await this.updateH1Heading(task, newContent);
|
||||
break;
|
||||
|
||||
case 'custom':
|
||||
case "custom":
|
||||
// For custom content source, update the custom field in frontmatter
|
||||
if (config.customContentField) {
|
||||
await this.updateCustomContentField(task, newContent, config.customContentField);
|
||||
await this.updateCustomContentField(
|
||||
task,
|
||||
newContent,
|
||||
config.customContentField,
|
||||
);
|
||||
} else {
|
||||
console.warn('[FileTaskManager] Custom content source specified but no customContentField configured');
|
||||
console.warn(
|
||||
"[FileTaskManager] Custom content source specified but no customContentField configured",
|
||||
);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'filename':
|
||||
case "filename":
|
||||
default:
|
||||
// For filename content source, rename the file
|
||||
await this.updateFileName(task, newContent);
|
||||
|
|
@ -460,21 +475,28 @@ export class FileTaskManagerImpl implements FileTaskManager {
|
|||
*/
|
||||
private async updateFrontmatterTitle(
|
||||
task: FileTask,
|
||||
newTitle: string
|
||||
newTitle: string,
|
||||
): Promise<void> {
|
||||
try {
|
||||
// Update the title property in frontmatter through the source entry
|
||||
// Note: updateProperty might be async, so we await it
|
||||
if (typeof task.sourceEntry.updateProperty === 'function') {
|
||||
await Promise.resolve(task.sourceEntry.updateProperty('title', newTitle));
|
||||
if (typeof task.sourceEntry.updateProperty === "function") {
|
||||
await Promise.resolve(
|
||||
task.sourceEntry.updateProperty("title", newTitle),
|
||||
);
|
||||
console.log(
|
||||
`[FileTaskManager] Updated frontmatter title for ${task.filePath} to: ${newTitle}`
|
||||
`[FileTaskManager] Updated frontmatter title for ${task.filePath} to: ${newTitle}`,
|
||||
);
|
||||
} else {
|
||||
throw new Error('updateProperty method not available on source entry');
|
||||
throw new Error(
|
||||
"updateProperty method not available on source entry",
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`[FileTaskManager] Failed to update frontmatter title:`, error);
|
||||
console.error(
|
||||
`[FileTaskManager] Failed to update frontmatter title:`,
|
||||
error,
|
||||
);
|
||||
// Fallback to file renaming if frontmatter update fails
|
||||
await this.updateFileName(task, newTitle);
|
||||
}
|
||||
|
|
@ -485,22 +507,24 @@ export class FileTaskManagerImpl implements FileTaskManager {
|
|||
*/
|
||||
private async updateH1Heading(
|
||||
task: FileTask,
|
||||
newHeading: string
|
||||
newHeading: string,
|
||||
): Promise<void> {
|
||||
try {
|
||||
const file = this.app.vault.getFileByPath(task.filePath);
|
||||
if (!file) {
|
||||
console.error(`[FileTaskManager] File not found: ${task.filePath}`);
|
||||
console.error(
|
||||
`[FileTaskManager] File not found: ${task.filePath}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const content = await this.app.vault.read(file);
|
||||
const lines = content.split('\n');
|
||||
const lines = content.split("\n");
|
||||
|
||||
// Find the first H1 heading
|
||||
let h1LineIndex = -1;
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
if (lines[i].startsWith('# ')) {
|
||||
if (lines[i].startsWith("# ")) {
|
||||
h1LineIndex = i;
|
||||
break;
|
||||
}
|
||||
|
|
@ -512,25 +536,30 @@ export class FileTaskManagerImpl implements FileTaskManager {
|
|||
} else {
|
||||
// Add new H1 at the beginning (after frontmatter if present)
|
||||
let insertIndex = 0;
|
||||
if (content.startsWith('---')) {
|
||||
if (content.startsWith("---")) {
|
||||
// Skip frontmatter
|
||||
const frontmatterEnd = content.indexOf('\n---\n', 3);
|
||||
const frontmatterEnd = content.indexOf("\n---\n", 3);
|
||||
if (frontmatterEnd >= 0) {
|
||||
const frontmatterLines = content.substring(0, frontmatterEnd + 5).split('\n').length - 1;
|
||||
const frontmatterLines =
|
||||
content.substring(0, frontmatterEnd + 5).split("\n")
|
||||
.length - 1;
|
||||
insertIndex = frontmatterLines;
|
||||
}
|
||||
}
|
||||
lines.splice(insertIndex, 0, `# ${newHeading}`, '');
|
||||
lines.splice(insertIndex, 0, `# ${newHeading}`, "");
|
||||
}
|
||||
|
||||
const newContent = lines.join('\n');
|
||||
const newContent = lines.join("\n");
|
||||
await this.app.vault.modify(file, newContent);
|
||||
|
||||
console.log(
|
||||
`[FileTaskManager] Updated H1 heading for ${task.filePath} to: ${newHeading}`
|
||||
`[FileTaskManager] Updated H1 heading for ${task.filePath} to: ${newHeading}`,
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(`[FileTaskManager] Failed to update H1 heading:`, error);
|
||||
console.error(
|
||||
`[FileTaskManager] Failed to update H1 heading:`,
|
||||
error,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -540,21 +569,28 @@ export class FileTaskManagerImpl implements FileTaskManager {
|
|||
private async updateCustomContentField(
|
||||
task: FileTask,
|
||||
newContent: string,
|
||||
fieldName: string
|
||||
fieldName: string,
|
||||
): Promise<void> {
|
||||
try {
|
||||
// Update the custom field in frontmatter through the source entry
|
||||
// Note: updateProperty might be async, so we await it
|
||||
if (typeof task.sourceEntry.updateProperty === 'function') {
|
||||
await Promise.resolve(task.sourceEntry.updateProperty(fieldName, newContent));
|
||||
if (typeof task.sourceEntry.updateProperty === "function") {
|
||||
await Promise.resolve(
|
||||
task.sourceEntry.updateProperty(fieldName, newContent),
|
||||
);
|
||||
console.log(
|
||||
`[FileTaskManager] Updated custom field '${fieldName}' for ${task.filePath} to: ${newContent}`
|
||||
`[FileTaskManager] Updated custom field '${fieldName}' for ${task.filePath} to: ${newContent}`,
|
||||
);
|
||||
} else {
|
||||
throw new Error('updateProperty method not available on source entry');
|
||||
throw new Error(
|
||||
"updateProperty method not available on source entry",
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`[FileTaskManager] Failed to update custom field '${fieldName}':`, error);
|
||||
console.error(
|
||||
`[FileTaskManager] Failed to update custom field '${fieldName}':`,
|
||||
error,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -563,7 +599,7 @@ export class FileTaskManagerImpl implements FileTaskManager {
|
|||
*/
|
||||
private async updateFileName(
|
||||
task: FileTask,
|
||||
newContent: string
|
||||
newContent: string,
|
||||
): Promise<void> {
|
||||
try {
|
||||
const file = this.app.vault.getFileByPath(task.filePath);
|
||||
|
|
@ -575,7 +611,7 @@ export class FileTaskManagerImpl implements FileTaskManager {
|
|||
? currentPath.substring(0, lastSlashIndex)
|
||||
: "";
|
||||
const extension = currentPath.substring(
|
||||
currentPath.lastIndexOf(".")
|
||||
currentPath.lastIndexOf("."),
|
||||
);
|
||||
|
||||
// Ensure newContent doesn't already have the extension
|
||||
|
|
@ -583,7 +619,7 @@ export class FileTaskManagerImpl implements FileTaskManager {
|
|||
if (cleanContent.endsWith(extension)) {
|
||||
cleanContent = cleanContent.substring(
|
||||
0,
|
||||
cleanContent.length - extension.length
|
||||
cleanContent.length - extension.length,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -597,7 +633,7 @@ export class FileTaskManagerImpl implements FileTaskManager {
|
|||
// Update the task's filePath to reflect the new path
|
||||
task.filePath = newPath;
|
||||
console.log(
|
||||
`[FileTaskManager] Renamed file from ${currentPath} to ${newPath}`
|
||||
`[FileTaskManager] Renamed file from ${currentPath} to ${newPath}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -611,7 +647,7 @@ export class FileTaskManagerImpl implements FileTaskManager {
|
|||
*/
|
||||
getFileTasksFromEntries(
|
||||
entries: BasesEntry[],
|
||||
mapping: FileTaskPropertyMapping = DEFAULT_FILE_TASK_MAPPING
|
||||
mapping: FileTaskPropertyMapping = DEFAULT_FILE_TASK_MAPPING,
|
||||
): FileTask[] {
|
||||
// Filter out non-markdown files with robust extension detection
|
||||
const markdownEntries = entries.filter((entry) => {
|
||||
|
|
@ -642,11 +678,11 @@ export class FileTaskManagerImpl implements FileTaskManager {
|
|||
});
|
||||
|
||||
console.log(
|
||||
`[FileTaskManager] Filtered ${entries.length} entries to ${markdownEntries.length} markdown files`
|
||||
`[FileTaskManager] Filtered ${entries.length} entries to ${markdownEntries.length} markdown files`,
|
||||
);
|
||||
|
||||
return markdownEntries.map((entry) =>
|
||||
this.entryToFileTask(entry, mapping)
|
||||
this.entryToFileTask(entry, mapping),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -665,7 +701,7 @@ export class FileTaskManagerImpl implements FileTaskManager {
|
|||
|
||||
private getPropertyValue(
|
||||
entry: BasesEntry,
|
||||
propertyName?: string
|
||||
propertyName?: string,
|
||||
): string | undefined {
|
||||
if (!propertyName) return undefined;
|
||||
// 1) Try Bases API
|
||||
|
|
@ -675,8 +711,7 @@ export class FileTaskManagerImpl implements FileTaskManager {
|
|||
name: propertyName,
|
||||
});
|
||||
if (value !== null && value !== undefined) return String(value);
|
||||
} catch {
|
||||
}
|
||||
} catch {}
|
||||
// 2) Fallback: direct properties/frontmatter/note.data
|
||||
try {
|
||||
const anyEntry: any = entry as any;
|
||||
|
|
@ -698,14 +733,13 @@ export class FileTaskManagerImpl implements FileTaskManager {
|
|||
) {
|
||||
return String(anyEntry.note.data[propertyName]);
|
||||
}
|
||||
} catch {
|
||||
}
|
||||
} catch {}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private getBooleanPropertyValue(
|
||||
entry: BasesEntry,
|
||||
propertyName?: string
|
||||
propertyName?: string,
|
||||
): boolean | undefined {
|
||||
if (!propertyName) return undefined;
|
||||
try {
|
||||
|
|
@ -726,7 +760,7 @@ export class FileTaskManagerImpl implements FileTaskManager {
|
|||
|
||||
private getNumberPropertyValue(
|
||||
entry: BasesEntry,
|
||||
propertyName?: string
|
||||
propertyName?: string,
|
||||
): number | undefined {
|
||||
if (!propertyName) return undefined;
|
||||
try {
|
||||
|
|
@ -743,7 +777,7 @@ export class FileTaskManagerImpl implements FileTaskManager {
|
|||
|
||||
private getDatePropertyValue(
|
||||
entry: BasesEntry,
|
||||
propertyName?: string
|
||||
propertyName?: string,
|
||||
): number | undefined {
|
||||
if (!propertyName) return undefined;
|
||||
try {
|
||||
|
|
@ -813,7 +847,7 @@ export class FileTaskManagerImpl implements FileTaskManager {
|
|||
|
||||
private getArrayPropertyValue(
|
||||
entry: BasesEntry,
|
||||
propertyName?: string
|
||||
propertyName?: string,
|
||||
): string[] | undefined {
|
||||
if (!propertyName) return undefined;
|
||||
try {
|
||||
|
|
@ -872,23 +906,32 @@ export class FileTaskManagerImpl implements FileTaskManager {
|
|||
/**
|
||||
* Extract time components from task content using enhanced time parsing
|
||||
*/
|
||||
private extractTimeComponents(content: string): Partial<EnhancedStandardTaskMetadata> {
|
||||
private extractTimeComponents(
|
||||
content: string,
|
||||
): Partial<EnhancedStandardTaskMetadata> {
|
||||
if (!this.timeParsingService) {
|
||||
return {};
|
||||
}
|
||||
|
||||
try {
|
||||
// Parse time components from content
|
||||
const {timeComponents, errors, warnings} = this.timeParsingService.parseTimeComponents(content);
|
||||
const { timeComponents, errors, warnings } =
|
||||
this.timeParsingService.parseTimeComponents(content);
|
||||
|
||||
// Log warnings if any
|
||||
if (warnings.length > 0) {
|
||||
console.warn(`[FileTaskManager] Time parsing warnings for "${content}":`, warnings);
|
||||
console.warn(
|
||||
`[FileTaskManager] Time parsing warnings for "${content}":`,
|
||||
warnings,
|
||||
);
|
||||
}
|
||||
|
||||
// Log errors if any (but don't fail)
|
||||
if (errors.length > 0) {
|
||||
console.warn(`[FileTaskManager] Time parsing errors for "${content}":`, errors);
|
||||
console.warn(
|
||||
`[FileTaskManager] Time parsing errors for "${content}":`,
|
||||
errors,
|
||||
);
|
||||
}
|
||||
|
||||
// Return enhanced metadata with time components
|
||||
|
|
@ -900,7 +943,10 @@ export class FileTaskManagerImpl implements FileTaskManager {
|
|||
|
||||
return enhancedMetadata;
|
||||
} catch (error) {
|
||||
console.error(`[FileTaskManager] Failed to extract time components from "${content}":`, error);
|
||||
console.error(
|
||||
`[FileTaskManager] Failed to extract time components from "${content}":`,
|
||||
error,
|
||||
);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
|
@ -915,7 +961,7 @@ export class FileTaskManagerImpl implements FileTaskManager {
|
|||
scheduledDate?: number;
|
||||
completedDate?: number;
|
||||
},
|
||||
timeComponents: EnhancedStandardTaskMetadata["timeComponents"]
|
||||
timeComponents: EnhancedStandardTaskMetadata["timeComponents"],
|
||||
): EnhancedStandardTaskMetadata["enhancedDates"] {
|
||||
if (!timeComponents) {
|
||||
return undefined;
|
||||
|
|
@ -924,7 +970,10 @@ export class FileTaskManagerImpl implements FileTaskManager {
|
|||
const enhancedDates: EnhancedStandardTaskMetadata["enhancedDates"] = {};
|
||||
|
||||
// Helper function to combine date and time component
|
||||
const combineDateTime = (dateTimestamp: number | undefined, timeComponent: TimeComponent | undefined): Date | undefined => {
|
||||
const combineDateTime = (
|
||||
dateTimestamp: number | undefined,
|
||||
timeComponent: TimeComponent | undefined,
|
||||
): Date | undefined => {
|
||||
if (!dateTimestamp || !timeComponent) {
|
||||
return undefined;
|
||||
}
|
||||
|
|
@ -936,7 +985,7 @@ export class FileTaskManagerImpl implements FileTaskManager {
|
|||
date.getDate(),
|
||||
timeComponent.hour,
|
||||
timeComponent.minute,
|
||||
timeComponent.second || 0
|
||||
timeComponent.second || 0,
|
||||
);
|
||||
|
||||
return combinedDate;
|
||||
|
|
@ -944,46 +993,80 @@ export class FileTaskManagerImpl implements FileTaskManager {
|
|||
|
||||
// Combine start date with start time
|
||||
if (dates.startDate && timeComponents.startTime) {
|
||||
enhancedDates.startDateTime = combineDateTime(dates.startDate, timeComponents.startTime);
|
||||
enhancedDates.startDateTime = combineDateTime(
|
||||
dates.startDate,
|
||||
timeComponents.startTime,
|
||||
);
|
||||
}
|
||||
|
||||
// Fallback: If start date exists but no start time, and we have a due time (default context)
|
||||
// but no due date, assume the time belongs to the start date.
|
||||
// This handles cases like "🛫 2025-11-29 18:00" where the time defaults to "due" context
|
||||
// but should actually be associated with the start date.
|
||||
if (
|
||||
dates.startDate &&
|
||||
!timeComponents.startTime &&
|
||||
!dates.dueDate &&
|
||||
timeComponents.dueTime
|
||||
) {
|
||||
enhancedDates.startDateTime = combineDateTime(
|
||||
dates.startDate,
|
||||
timeComponents.dueTime,
|
||||
);
|
||||
}
|
||||
|
||||
// Fallback for start datetime when explicit start date is missing
|
||||
if (
|
||||
!enhancedDates.startDateTime &&
|
||||
timeComponents.startTime
|
||||
) {
|
||||
if (!enhancedDates.startDateTime && timeComponents.startTime) {
|
||||
const fallbackDate =
|
||||
dates.dueDate ??
|
||||
dates.scheduledDate ??
|
||||
dates.completedDate;
|
||||
dates.dueDate ?? dates.scheduledDate ?? dates.completedDate;
|
||||
if (fallbackDate) {
|
||||
enhancedDates.startDateTime = combineDateTime(
|
||||
fallbackDate,
|
||||
timeComponents.startTime
|
||||
timeComponents.startTime,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Combine due date with due time
|
||||
if (dates.dueDate && timeComponents.dueTime) {
|
||||
enhancedDates.dueDateTime = combineDateTime(dates.dueDate, timeComponents.dueTime);
|
||||
enhancedDates.dueDateTime = combineDateTime(
|
||||
dates.dueDate,
|
||||
timeComponents.dueTime,
|
||||
);
|
||||
}
|
||||
|
||||
// Combine scheduled date with scheduled time
|
||||
if (dates.scheduledDate && timeComponents.scheduledTime) {
|
||||
enhancedDates.scheduledDateTime = combineDateTime(dates.scheduledDate, timeComponents.scheduledTime);
|
||||
enhancedDates.scheduledDateTime = combineDateTime(
|
||||
dates.scheduledDate,
|
||||
timeComponents.scheduledTime,
|
||||
);
|
||||
}
|
||||
|
||||
// If we have a due date but the time component is scheduledTime (common with "at" keyword),
|
||||
// create dueDateTime using scheduledTime
|
||||
if (dates.dueDate && !timeComponents.dueTime && timeComponents.scheduledTime) {
|
||||
enhancedDates.dueDateTime = combineDateTime(dates.dueDate, timeComponents.scheduledTime);
|
||||
if (
|
||||
dates.dueDate &&
|
||||
!timeComponents.dueTime &&
|
||||
timeComponents.scheduledTime
|
||||
) {
|
||||
enhancedDates.dueDateTime = combineDateTime(
|
||||
dates.dueDate,
|
||||
timeComponents.scheduledTime,
|
||||
);
|
||||
}
|
||||
|
||||
// If we have a scheduled date but the time component is dueTime,
|
||||
// create scheduledDateTime using dueTime
|
||||
if (dates.scheduledDate && !timeComponents.scheduledTime && timeComponents.dueTime) {
|
||||
enhancedDates.scheduledDateTime = combineDateTime(dates.scheduledDate, timeComponents.dueTime);
|
||||
if (
|
||||
dates.scheduledDate &&
|
||||
!timeComponents.scheduledTime &&
|
||||
timeComponents.dueTime
|
||||
) {
|
||||
enhancedDates.scheduledDateTime = combineDateTime(
|
||||
dates.scheduledDate,
|
||||
timeComponents.dueTime,
|
||||
);
|
||||
}
|
||||
|
||||
// Handle end time - if we have start date and end time, create end datetime
|
||||
|
|
@ -994,11 +1077,16 @@ export class FileTaskManagerImpl implements FileTaskManager {
|
|||
dates.scheduledDate ??
|
||||
dates.completedDate;
|
||||
if (endBaseDate) {
|
||||
enhancedDates.endDateTime = combineDateTime(endBaseDate, timeComponents.endTime);
|
||||
enhancedDates.endDateTime = combineDateTime(
|
||||
endBaseDate,
|
||||
timeComponents.endTime,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return Object.keys(enhancedDates).length > 0 ? enhancedDates : undefined;
|
||||
return Object.keys(enhancedDates).length > 0
|
||||
? enhancedDates
|
||||
: undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -1006,7 +1094,7 @@ export class FileTaskManagerImpl implements FileTaskManager {
|
|||
*/
|
||||
public validatePropertyMapping(
|
||||
entries: BasesEntry[],
|
||||
mapping: FileTaskPropertyMapping = DEFAULT_FILE_TASK_MAPPING
|
||||
mapping: FileTaskPropertyMapping = DEFAULT_FILE_TASK_MAPPING,
|
||||
): void {
|
||||
if (entries.length === 0) return;
|
||||
|
||||
|
|
@ -1033,7 +1121,7 @@ export class FileTaskManagerImpl implements FileTaskManager {
|
|||
Object.entries(mapping).forEach(([key, propName]) => {
|
||||
if (propName && !propertyUsage[propName]) {
|
||||
console.warn(
|
||||
`[FileTaskManager] Property "${propName}" (${key}) not found in any entries`
|
||||
`[FileTaskManager] Property "${propName}" (${key}) not found in any entries`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -439,7 +439,17 @@ export class TaskSpecificView extends ItemView {
|
|||
this.toggleTaskCompletion(task);
|
||||
},
|
||||
onEventContextMenu: (ev: MouseEvent, event: CalendarEvent) => {
|
||||
this.handleTaskContextMenu(ev, event);
|
||||
// Extract original task from metadata to ensure all fields (like filePath) are present
|
||||
const realTask = (event as any)?.metadata
|
||||
?.originalTask as Task;
|
||||
if (realTask) {
|
||||
this.handleTaskContextMenu(ev, realTask);
|
||||
} else {
|
||||
this.handleTaskContextMenu(
|
||||
ev,
|
||||
event as unknown as Task,
|
||||
);
|
||||
}
|
||||
},
|
||||
},
|
||||
);
|
||||
|
|
|
|||
|
|
@ -109,7 +109,10 @@ export class TaskView extends ItemView {
|
|||
this.applyCurrentFilter();
|
||||
}, 400); // 增加延迟到 400ms 减少频繁更新
|
||||
|
||||
constructor(leaf: WorkspaceLeaf, private plugin: TaskProgressBarPlugin) {
|
||||
constructor(
|
||||
leaf: WorkspaceLeaf,
|
||||
private plugin: TaskProgressBarPlugin,
|
||||
) {
|
||||
super(leaf);
|
||||
|
||||
this.tasks = this.plugin.preloadedTasks || [];
|
||||
|
|
@ -137,7 +140,7 @@ export class TaskView extends ItemView {
|
|||
getDisplayText(): string {
|
||||
const currentViewConfig = getViewSettingOrDefault(
|
||||
this.plugin,
|
||||
this.currentViewId
|
||||
this.currentViewId,
|
||||
);
|
||||
return currentViewConfig.name;
|
||||
}
|
||||
|
|
@ -145,7 +148,7 @@ export class TaskView extends ItemView {
|
|||
getIcon(): string {
|
||||
const currentViewConfig = getViewSettingOrDefault(
|
||||
this.plugin,
|
||||
this.currentViewId
|
||||
this.currentViewId,
|
||||
);
|
||||
return currentViewConfig.icon;
|
||||
}
|
||||
|
|
@ -168,23 +171,22 @@ export class TaskView extends ItemView {
|
|||
isDataflowEnabled(this.plugin) &&
|
||||
this.plugin.dataflowOrchestrator
|
||||
) {
|
||||
|
||||
this.registerEvent(
|
||||
on(this.app, Events.CACHE_READY, async () => {
|
||||
// 冷启动就绪,从快照加载,并更新视图
|
||||
await this.loadTasksFast(false);
|
||||
})
|
||||
}),
|
||||
);
|
||||
this.registerEvent(
|
||||
on(this.app, Events.TASK_CACHE_UPDATED, debouncedViewUpdate)
|
||||
on(this.app, Events.TASK_CACHE_UPDATED, debouncedViewUpdate),
|
||||
);
|
||||
} else {
|
||||
// Legacy: 兼容旧事件
|
||||
this.registerEvent(
|
||||
this.app.workspace.on(
|
||||
"task-genius:task-cache-updated",
|
||||
debouncedViewUpdate
|
||||
)
|
||||
debouncedViewUpdate,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -213,8 +215,8 @@ export class TaskView extends ItemView {
|
|||
|
||||
// 使用防抖函数应用过滤器,避免频繁更新
|
||||
this.debouncedApplyFilter();
|
||||
}
|
||||
)
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
// 监听视图配置变更事件(仅刷新侧边栏与当前视图可见性)
|
||||
|
|
@ -227,14 +229,14 @@ export class TaskView extends ItemView {
|
|||
if (
|
||||
this.sidebarComponent &&
|
||||
typeof this.sidebarComponent.renderSidebarItems ===
|
||||
"function"
|
||||
"function"
|
||||
) {
|
||||
this.sidebarComponent.renderSidebarItems();
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn(
|
||||
"Failed to render sidebar items on view-config-changed:",
|
||||
e
|
||||
e,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -249,12 +251,12 @@ export class TaskView extends ItemView {
|
|||
// 若当前视图被设为不可见,则切换到第一个可见视图(不强制刷新内容)
|
||||
const currentCfg =
|
||||
this.plugin.settings.viewConfiguration.find(
|
||||
(v) => v.id === this.currentViewId
|
||||
(v) => v.id === this.currentViewId,
|
||||
);
|
||||
if (!currentCfg?.visible) {
|
||||
const firstVisible =
|
||||
this.plugin.settings.viewConfiguration.find(
|
||||
(v) => v.visible
|
||||
(v) => v.visible,
|
||||
)?.id as ViewMode | undefined;
|
||||
if (
|
||||
firstVisible &&
|
||||
|
|
@ -262,23 +264,23 @@ export class TaskView extends ItemView {
|
|||
) {
|
||||
this.currentViewId = firstVisible;
|
||||
this.sidebarComponent?.setViewMode(
|
||||
this.currentViewId
|
||||
this.currentViewId,
|
||||
);
|
||||
// Ensure main content switches to the new visible view
|
||||
this.switchView(
|
||||
this.currentViewId,
|
||||
undefined,
|
||||
true
|
||||
true,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
// 2. 加载缓存的实时过滤状态
|
||||
const savedFilterState = this.app.loadLocalStorage(
|
||||
"task-genius-view-filter"
|
||||
"task-genius-view-filter",
|
||||
) as RootFilterState;
|
||||
console.log("savedFilterState", savedFilterState);
|
||||
|
||||
|
|
@ -301,14 +303,14 @@ export class TaskView extends ItemView {
|
|||
|
||||
// 4. 获取初始视图ID
|
||||
const savedViewId = this.app.loadLocalStorage(
|
||||
"task-genius:view-mode"
|
||||
"task-genius:view-mode",
|
||||
) as ViewMode;
|
||||
const initialViewId = this.plugin.settings.viewConfiguration.find(
|
||||
(v) => v.id === savedViewId && v.visible
|
||||
(v) => v.id === savedViewId && v.visible,
|
||||
)
|
||||
? savedViewId
|
||||
: this.plugin.settings.viewConfiguration.find((v) => v.visible)
|
||||
?.id || "inbox";
|
||||
?.id || "inbox";
|
||||
|
||||
this.currentViewId = initialViewId;
|
||||
this.sidebarComponent.setViewMode(this.currentViewId);
|
||||
|
|
@ -322,7 +324,7 @@ export class TaskView extends ItemView {
|
|||
} else {
|
||||
// If no tasks loaded yet, wait for background sync before rendering
|
||||
console.log(
|
||||
"No cached tasks found, waiting for background sync..."
|
||||
"No cached tasks found, waiting for background sync...",
|
||||
);
|
||||
await this.loadTasksWithSyncInBackground();
|
||||
this.switchView(this.currentViewId);
|
||||
|
|
@ -345,7 +347,7 @@ export class TaskView extends ItemView {
|
|||
|
||||
(this.leaf.tabHeaderEl as HTMLElement).toggleClass(
|
||||
"task-genius-tab-header",
|
||||
true
|
||||
true,
|
||||
);
|
||||
|
||||
this.tabActionButton = (
|
||||
|
|
@ -364,11 +366,11 @@ export class TaskView extends ItemView {
|
|||
this.plugin.app,
|
||||
this.plugin,
|
||||
{},
|
||||
true
|
||||
true,
|
||||
);
|
||||
modal.open();
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
this.register(() => {
|
||||
|
|
@ -389,7 +391,7 @@ export class TaskView extends ItemView {
|
|||
|
||||
const existingLeaves =
|
||||
this.plugin.app.workspace.getLeavesOfType(
|
||||
TASK_VIEW_TYPE
|
||||
TASK_VIEW_TYPE,
|
||||
);
|
||||
if (existingLeaves.length > 0) {
|
||||
// Focus the existing view
|
||||
|
|
@ -401,7 +403,7 @@ export class TaskView extends ItemView {
|
|||
this.plugin.activateTaskView().then(() => {
|
||||
const newView =
|
||||
this.plugin.app.workspace.getActiveViewOfType(
|
||||
TaskView
|
||||
TaskView,
|
||||
);
|
||||
if (newView) {
|
||||
newView.switchView(view.id);
|
||||
|
|
@ -437,7 +439,7 @@ export class TaskView extends ItemView {
|
|||
private initializeComponents() {
|
||||
this.sidebarComponent = new SidebarComponent(
|
||||
this.rootContainerEl,
|
||||
this.plugin
|
||||
this.plugin,
|
||||
);
|
||||
this.addChild(this.sidebarComponent);
|
||||
this.sidebarComponent.load();
|
||||
|
|
@ -460,14 +462,14 @@ export class TaskView extends ItemView {
|
|||
console.log(
|
||||
"TaskView onTaskUpdate",
|
||||
originalTask.content,
|
||||
updatedTask.content
|
||||
updatedTask.content,
|
||||
);
|
||||
await this.handleTaskUpdate(originalTask, updatedTask);
|
||||
},
|
||||
onTaskContextMenu: (event: MouseEvent, task: Task) => {
|
||||
this.handleTaskContextMenu(event, task);
|
||||
},
|
||||
}
|
||||
},
|
||||
);
|
||||
this.addChild(this.contentComponent);
|
||||
this.contentComponent.load();
|
||||
|
|
@ -487,14 +489,14 @@ export class TaskView extends ItemView {
|
|||
console.log(
|
||||
"TaskView onTaskUpdate",
|
||||
originalTask.content,
|
||||
updatedTask.content
|
||||
updatedTask.content,
|
||||
);
|
||||
await this.handleTaskUpdate(originalTask, updatedTask);
|
||||
},
|
||||
onTaskContextMenu: (event: MouseEvent, task: Task) => {
|
||||
this.handleTaskContextMenu(event, task);
|
||||
},
|
||||
}
|
||||
},
|
||||
);
|
||||
this.addChild(this.forecastComponent);
|
||||
this.forecastComponent.load();
|
||||
|
|
@ -517,7 +519,7 @@ export class TaskView extends ItemView {
|
|||
onTaskContextMenu: (event: MouseEvent, task: Task) => {
|
||||
this.handleTaskContextMenu(event, task);
|
||||
},
|
||||
}
|
||||
},
|
||||
);
|
||||
this.addChild(this.tagsComponent);
|
||||
this.tagsComponent.load();
|
||||
|
|
@ -540,7 +542,7 @@ export class TaskView extends ItemView {
|
|||
onTaskContextMenu: (event: MouseEvent, task: Task) => {
|
||||
this.handleTaskContextMenu(event, task);
|
||||
},
|
||||
}
|
||||
},
|
||||
);
|
||||
this.addChild(this.projectsComponent);
|
||||
this.projectsComponent.load();
|
||||
|
|
@ -563,7 +565,7 @@ export class TaskView extends ItemView {
|
|||
onTaskContextMenu: (event: MouseEvent, task: Task) => {
|
||||
this.handleTaskContextMenu(event, task);
|
||||
},
|
||||
}
|
||||
},
|
||||
);
|
||||
this.addChild(this.reviewComponent);
|
||||
this.reviewComponent.load();
|
||||
|
|
@ -582,9 +584,19 @@ export class TaskView extends ItemView {
|
|||
this.toggleTaskCompletion(task);
|
||||
},
|
||||
onEventContextMenu: (ev: MouseEvent, event: CalendarEvent) => {
|
||||
this.handleTaskContextMenu(ev, event);
|
||||
// Extract original task from metadata to ensure all fields (like filePath) are present
|
||||
const realTask = (event as any)?.metadata
|
||||
?.originalTask as Task;
|
||||
if (realTask) {
|
||||
this.handleTaskContextMenu(ev, realTask);
|
||||
} else {
|
||||
this.handleTaskContextMenu(
|
||||
ev,
|
||||
event as unknown as Task,
|
||||
);
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
);
|
||||
this.addChild(this.calendarComponent);
|
||||
this.calendarComponent.load();
|
||||
|
|
@ -602,7 +614,7 @@ export class TaskView extends ItemView {
|
|||
onTaskSelected: this.handleTaskSelection.bind(this),
|
||||
onTaskCompleted: this.toggleTaskCompletion.bind(this),
|
||||
onTaskContextMenu: this.handleTaskContextMenu.bind(this),
|
||||
}
|
||||
},
|
||||
);
|
||||
this.addChild(this.kanbanComponent);
|
||||
this.kanbanComponent.containerEl.hide();
|
||||
|
|
@ -614,7 +626,7 @@ export class TaskView extends ItemView {
|
|||
onTaskSelected: this.handleTaskSelection.bind(this),
|
||||
onTaskCompleted: this.toggleTaskCompletion.bind(this),
|
||||
onTaskContextMenu: this.handleTaskContextMenu.bind(this),
|
||||
}
|
||||
},
|
||||
);
|
||||
this.addChild(this.ganttComponent);
|
||||
this.ganttComponent.containerEl.hide();
|
||||
|
|
@ -626,7 +638,7 @@ export class TaskView extends ItemView {
|
|||
this.detailsComponent = new TaskDetailsComponent(
|
||||
this.rootContainerEl,
|
||||
this.app,
|
||||
this.plugin
|
||||
this.plugin,
|
||||
);
|
||||
this.addChild(this.detailsComponent);
|
||||
this.detailsComponent.load();
|
||||
|
|
@ -645,7 +657,7 @@ export class TaskView extends ItemView {
|
|||
this.handleKanbanTaskStatusUpdate.bind(this),
|
||||
onEventContextMenu: this.handleTaskContextMenu.bind(this),
|
||||
onTaskUpdate: this.handleTaskUpdate.bind(this),
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
this.addChild(this.viewComponentManager);
|
||||
|
|
@ -662,7 +674,7 @@ export class TaskView extends ItemView {
|
|||
|
||||
if (!toggleContainer) {
|
||||
console.error(
|
||||
"Could not find .view-header-nav-buttons to add sidebar toggle."
|
||||
"Could not find .view-header-nav-buttons to add sidebar toggle.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
|
@ -681,7 +693,7 @@ export class TaskView extends ItemView {
|
|||
|
||||
private createTaskMark() {
|
||||
// Check if task-mark feature is hidden in current workspace
|
||||
if (this.plugin.workspaceManager?.isFeatureHidden('task-mark')) {
|
||||
if (this.plugin.workspaceManager?.isFeatureHidden("task-mark")) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -690,40 +702,43 @@ export class TaskView extends ItemView {
|
|||
interpolation: {
|
||||
num: this.tasks.length,
|
||||
},
|
||||
})
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
private createActionButtons() {
|
||||
// Check if details-panel feature is hidden
|
||||
if (!this.plugin.workspaceManager?.isFeatureHidden('details-panel')) {
|
||||
if (!this.plugin.workspaceManager?.isFeatureHidden("details-panel")) {
|
||||
this.detailsToggleBtn = this.addAction(
|
||||
"panel-right-dashed",
|
||||
t("Details"),
|
||||
() => {
|
||||
this.toggleDetailsVisibility(!this.isDetailsVisible);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
this.detailsToggleBtn.toggleClass("panel-toggle-btn", true);
|
||||
this.detailsToggleBtn.toggleClass("is-active", this.isDetailsVisible);
|
||||
this.detailsToggleBtn.toggleClass(
|
||||
"is-active",
|
||||
this.isDetailsVisible,
|
||||
);
|
||||
}
|
||||
|
||||
// Check if quick-capture feature is hidden
|
||||
if (!this.plugin.workspaceManager?.isFeatureHidden('quick-capture')) {
|
||||
if (!this.plugin.workspaceManager?.isFeatureHidden("quick-capture")) {
|
||||
this.addAction("notebook-pen", t("Capture"), () => {
|
||||
const modal = new QuickCaptureModal(
|
||||
this.plugin.app,
|
||||
this.plugin,
|
||||
{},
|
||||
true
|
||||
true,
|
||||
);
|
||||
modal.open();
|
||||
});
|
||||
}
|
||||
|
||||
// Check if filter feature is hidden
|
||||
if (this.plugin.workspaceManager?.isFeatureHidden('filter')) {
|
||||
if (this.plugin.workspaceManager?.isFeatureHidden("filter")) {
|
||||
// Skip filter button creation
|
||||
this.updateActionButtons();
|
||||
return;
|
||||
|
|
@ -734,7 +749,7 @@ export class TaskView extends ItemView {
|
|||
const popover = new ViewTaskFilterPopover(
|
||||
this.plugin.app,
|
||||
undefined,
|
||||
this.plugin
|
||||
this.plugin,
|
||||
);
|
||||
|
||||
// 设置关闭回调 - 现在主要用于处理取消操作
|
||||
|
|
@ -754,18 +769,18 @@ export class TaskView extends ItemView {
|
|||
const filterState = this
|
||||
.liveFilterState as RootFilterState;
|
||||
popover.taskFilterComponent.loadFilterState(
|
||||
filterState
|
||||
filterState,
|
||||
);
|
||||
}
|
||||
}, 100);
|
||||
});
|
||||
|
||||
popover.showAtPosition({x: e.clientX, y: e.clientY});
|
||||
popover.showAtPosition({ x: e.clientX, y: e.clientY });
|
||||
} else {
|
||||
const modal = new ViewTaskFilterModal(
|
||||
this.plugin.app,
|
||||
this.leaf.id,
|
||||
this.plugin
|
||||
this.plugin,
|
||||
);
|
||||
|
||||
// 设置关闭回调 - 现在主要用于处理取消操作
|
||||
|
|
@ -797,7 +812,7 @@ export class TaskView extends ItemView {
|
|||
console.log(
|
||||
"应用当前过滤状态:",
|
||||
this.liveFilterState ? "有实时筛选器" : "无实时筛选器",
|
||||
this.currentFilterState ? "有过滤器" : "无过滤器"
|
||||
this.currentFilterState ? "有过滤器" : "无过滤器",
|
||||
);
|
||||
// 通过triggerViewUpdate重新加载任务
|
||||
this.triggerViewUpdate();
|
||||
|
|
@ -838,7 +853,7 @@ export class TaskView extends ItemView {
|
|||
undefined,
|
||||
(config) => {
|
||||
this.applySavedFilter(config);
|
||||
}
|
||||
},
|
||||
);
|
||||
modal.open();
|
||||
});
|
||||
|
|
@ -880,7 +895,7 @@ export class TaskView extends ItemView {
|
|||
new ConfirmModal(this.plugin, {
|
||||
title: t("Reindex"),
|
||||
message: t(
|
||||
"Are you sure you want to force reindex all tasks?"
|
||||
"Are you sure you want to force reindex all tasks?",
|
||||
),
|
||||
confirmText: t("Reindex"),
|
||||
cancelText: t("Cancel"),
|
||||
|
|
@ -891,13 +906,13 @@ export class TaskView extends ItemView {
|
|||
await this.plugin.dataflowOrchestrator.rebuild();
|
||||
} else {
|
||||
throw new Error(
|
||||
"Dataflow orchestrator not available"
|
||||
"Dataflow orchestrator not available",
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(
|
||||
"Failed to force reindex tasks:",
|
||||
error
|
||||
error,
|
||||
);
|
||||
new Notice(t("Failed to force reindex tasks"));
|
||||
}
|
||||
|
|
@ -913,7 +928,7 @@ export class TaskView extends ItemView {
|
|||
this.isSidebarCollapsed = !this.isSidebarCollapsed;
|
||||
this.rootContainerEl.toggleClass(
|
||||
"sidebar-collapsed",
|
||||
this.isSidebarCollapsed
|
||||
this.isSidebarCollapsed,
|
||||
);
|
||||
|
||||
this.sidebarComponent.setCollapsed(this.isSidebarCollapsed);
|
||||
|
|
@ -929,7 +944,7 @@ export class TaskView extends ItemView {
|
|||
this.detailsToggleBtn.toggleClass("is-active", visible);
|
||||
this.detailsToggleBtn.setAttribute(
|
||||
"aria-label",
|
||||
visible ? t("Hide Details") : t("Show Details")
|
||||
visible ? t("Hide Details") : t("Show Details"),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -946,12 +961,12 @@ export class TaskView extends ItemView {
|
|||
this.detailsComponent.onTaskEdit = (task: Task) => this.editTask(task);
|
||||
this.detailsComponent.onTaskUpdate = async (
|
||||
originalTask: Task,
|
||||
updatedTask: Task
|
||||
updatedTask: Task,
|
||||
) => {
|
||||
console.log(
|
||||
"triggered by detailsComponent",
|
||||
originalTask,
|
||||
updatedTask
|
||||
updatedTask,
|
||||
);
|
||||
await this.updateTask(originalTask, updatedTask);
|
||||
};
|
||||
|
|
@ -971,7 +986,7 @@ export class TaskView extends ItemView {
|
|||
private switchView(
|
||||
viewId: ViewMode,
|
||||
project?: string | null,
|
||||
forceRefresh: boolean = false
|
||||
forceRefresh: boolean = false,
|
||||
) {
|
||||
// Exit selection mode when switching views
|
||||
if (this.selectionManager.isSelectionMode) {
|
||||
|
|
@ -985,7 +1000,7 @@ export class TaskView extends ItemView {
|
|||
"Project:",
|
||||
project,
|
||||
"ForceRefresh:",
|
||||
forceRefresh
|
||||
forceRefresh,
|
||||
);
|
||||
|
||||
// Update sidebar to reflect current view
|
||||
|
|
@ -1026,7 +1041,7 @@ export class TaskView extends ItemView {
|
|||
this.app,
|
||||
this.plugin,
|
||||
twoColumnConfig,
|
||||
viewId
|
||||
viewId,
|
||||
);
|
||||
this.addChild(twoColumnComponent);
|
||||
|
||||
|
|
@ -1088,7 +1103,7 @@ export class TaskView extends ItemView {
|
|||
if (targetComponent) {
|
||||
console.log(
|
||||
`Activating component for view ${viewId}`,
|
||||
targetComponent.constructor.name
|
||||
targetComponent.constructor.name,
|
||||
);
|
||||
targetComponent.containerEl.show();
|
||||
if (typeof targetComponent.setTasks === "function") {
|
||||
|
|
@ -1112,13 +1127,13 @@ export class TaskView extends ItemView {
|
|||
this.tasks,
|
||||
viewId,
|
||||
this.plugin,
|
||||
filterOptions
|
||||
filterOptions,
|
||||
);
|
||||
|
||||
// Filter out badge tasks for forecast view - they should only appear in event view
|
||||
if (viewId === "forecast") {
|
||||
filteredTasks = filteredTasks.filter(
|
||||
(task) => !(task as any).badge
|
||||
(task) => !(task as any).badge,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -1126,12 +1141,12 @@ export class TaskView extends ItemView {
|
|||
"[TaskView] Calling setTasks with",
|
||||
filteredTasks.length,
|
||||
"filtered tasks, forceRefresh:",
|
||||
forceRefresh
|
||||
forceRefresh,
|
||||
);
|
||||
targetComponent.setTasks(
|
||||
filteredTasks,
|
||||
this.tasks,
|
||||
forceRefresh
|
||||
forceRefresh,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -1151,13 +1166,13 @@ export class TaskView extends ItemView {
|
|||
}
|
||||
|
||||
targetComponent.updateTasks(
|
||||
filterTasks(this.tasks, viewId, this.plugin, filterOptions)
|
||||
filterTasks(this.tasks, viewId, this.plugin, filterOptions),
|
||||
);
|
||||
}
|
||||
|
||||
if (typeof targetComponent.setViewMode === "function") {
|
||||
console.log(
|
||||
`Setting view mode for ${viewId} to ${modeForComponent} with project ${project}`
|
||||
`Setting view mode for ${viewId} to ${modeForComponent} with project ${project}`,
|
||||
);
|
||||
targetComponent.setViewMode(modeForComponent, project);
|
||||
}
|
||||
|
|
@ -1184,13 +1199,13 @@ export class TaskView extends ItemView {
|
|||
this.tasks,
|
||||
component.getViewId(),
|
||||
this.plugin,
|
||||
filterOptions
|
||||
filterOptions,
|
||||
);
|
||||
|
||||
// Filter out badge tasks for forecast view - they should only appear in event view
|
||||
if (component.getViewId() === "forecast") {
|
||||
filteredTasks = filteredTasks.filter(
|
||||
(task) => !(task as any).badge
|
||||
(task) => !(task as any).badge,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -1215,7 +1230,7 @@ export class TaskView extends ItemView {
|
|||
if (this.currentSelectedTaskId) {
|
||||
// Re-select the current task to maintain details panel visibility
|
||||
const currentTask = this.tasks.find(
|
||||
(t) => t.id === this.currentSelectedTaskId
|
||||
(t) => t.id === this.currentSelectedTaskId,
|
||||
);
|
||||
if (currentTask) {
|
||||
this.detailsComponent.showTaskDetails(currentTask);
|
||||
|
|
@ -1234,7 +1249,7 @@ export class TaskView extends ItemView {
|
|||
|
||||
private updateHeaderDisplay() {
|
||||
const config = getViewSettingOrDefault(this.plugin, this.currentViewId);
|
||||
this.leaf.setEphemeralState({title: config.name, icon: config.icon});
|
||||
this.leaf.setEphemeralState({ title: config.name, icon: config.icon });
|
||||
}
|
||||
|
||||
private handleTaskContextMenu(event: MouseEvent, task: Task) {
|
||||
|
|
@ -1277,7 +1292,7 @@ export class TaskView extends ItemView {
|
|||
},
|
||||
(el) => {
|
||||
createTaskCheckbox(mark, task, el);
|
||||
}
|
||||
},
|
||||
);
|
||||
item.titleEl.createEl("span", {
|
||||
cls: "status-option",
|
||||
|
|
@ -1357,28 +1372,28 @@ export class TaskView extends ItemView {
|
|||
|
||||
private async loadTasks(
|
||||
forceSync: boolean = false,
|
||||
skipViewUpdate: boolean = false
|
||||
skipViewUpdate: boolean = false,
|
||||
) {
|
||||
// Only use dataflow - TaskManager is deprecated
|
||||
if (!this.plugin.dataflowOrchestrator) {
|
||||
console.warn(
|
||||
"[TaskView] Dataflow orchestrator not available, waiting for initialization..."
|
||||
"[TaskView] Dataflow orchestrator not available, waiting for initialization...",
|
||||
);
|
||||
this.tasks = [];
|
||||
} else {
|
||||
try {
|
||||
console.log(
|
||||
"[TaskView] Loading tasks from dataflow orchestrator..."
|
||||
"[TaskView] Loading tasks from dataflow orchestrator...",
|
||||
);
|
||||
const queryAPI = this.plugin.dataflowOrchestrator.getQueryAPI();
|
||||
this.tasks = await queryAPI.getAllTasks();
|
||||
console.log(
|
||||
`[TaskView] Loaded ${this.tasks.length} tasks from dataflow`
|
||||
`[TaskView] Loaded ${this.tasks.length} tasks from dataflow`,
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
"[TaskView] Error loading tasks from dataflow:",
|
||||
error
|
||||
error,
|
||||
);
|
||||
this.tasks = [];
|
||||
}
|
||||
|
|
@ -1396,24 +1411,24 @@ export class TaskView extends ItemView {
|
|||
// Only use dataflow
|
||||
if (!this.plugin.dataflowOrchestrator) {
|
||||
console.warn(
|
||||
"[TaskView] Dataflow orchestrator not available for fast load"
|
||||
"[TaskView] Dataflow orchestrator not available for fast load",
|
||||
);
|
||||
this.tasks = [];
|
||||
} else {
|
||||
try {
|
||||
console.log(
|
||||
"[TaskView] Loading tasks fast from dataflow orchestrator..."
|
||||
"[TaskView] Loading tasks fast from dataflow orchestrator...",
|
||||
);
|
||||
const queryAPI = this.plugin.dataflowOrchestrator.getQueryAPI();
|
||||
// For fast loading, use regular getAllTasks (it should be cached)
|
||||
this.tasks = await queryAPI.getAllTasks();
|
||||
console.log(
|
||||
`[TaskView] Loaded ${this.tasks.length} tasks (fast from dataflow)`
|
||||
`[TaskView] Loaded ${this.tasks.length} tasks (fast from dataflow)`,
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
"[TaskView] Error loading tasks fast from dataflow:",
|
||||
error
|
||||
error,
|
||||
);
|
||||
this.tasks = [];
|
||||
}
|
||||
|
|
@ -1439,7 +1454,7 @@ export class TaskView extends ItemView {
|
|||
if (tasks.length !== this.tasks.length || tasks.length === 0) {
|
||||
this.tasks = tasks;
|
||||
console.log(
|
||||
`TaskView updated with ${this.tasks.length} tasks (dataflow sync)`
|
||||
`TaskView updated with ${this.tasks.length} tasks (dataflow sync)`,
|
||||
);
|
||||
// Don't trigger view update here as it will be handled by events
|
||||
}
|
||||
|
|
@ -1463,11 +1478,11 @@ export class TaskView extends ItemView {
|
|||
|
||||
// 如果当前视图已被设置为隐藏,则切换到第一个可见视图
|
||||
const currentCfg = this.plugin.settings.viewConfiguration.find(
|
||||
(v) => v.id === this.currentViewId
|
||||
(v) => v.id === this.currentViewId,
|
||||
);
|
||||
if (!currentCfg?.visible) {
|
||||
const firstVisible = this.plugin.settings.viewConfiguration.find(
|
||||
(v) => v.visible
|
||||
(v) => v.visible,
|
||||
)?.id as ViewMode | undefined;
|
||||
if (firstVisible && firstVisible !== this.currentViewId) {
|
||||
this.currentViewId = firstVisible;
|
||||
|
|
@ -1485,7 +1500,7 @@ export class TaskView extends ItemView {
|
|||
private updateActionButtons() {
|
||||
// 移除过滤器重置按钮(如果存在)
|
||||
const resetButton = this.leaf.view.containerEl.querySelector(
|
||||
".view-action.task-filter-reset"
|
||||
".view-action.task-filter-reset",
|
||||
);
|
||||
if (resetButton) {
|
||||
resetButton.remove();
|
||||
|
|
@ -1508,7 +1523,7 @@ export class TaskView extends ItemView {
|
|||
try {
|
||||
const lower = mark.toLowerCase();
|
||||
const completedCfg = String(
|
||||
this.plugin.settings.taskStatuses?.completed || "x"
|
||||
this.plugin.settings.taskStatuses?.completed || "x",
|
||||
);
|
||||
const completedSet = completedCfg
|
||||
.split("|")
|
||||
|
|
@ -1530,13 +1545,12 @@ export class TaskView extends ItemView {
|
|||
}
|
||||
}
|
||||
}
|
||||
} catch (_) {
|
||||
}
|
||||
} catch (_) {}
|
||||
return false;
|
||||
}
|
||||
|
||||
private async toggleTaskCompletion(task: Task) {
|
||||
const updatedTask = {...task, completed: !task.completed};
|
||||
const updatedTask = { ...task, completed: !task.completed };
|
||||
|
||||
if (updatedTask.completed) {
|
||||
updatedTask.metadata.completedDate = Date.now();
|
||||
|
|
@ -1564,7 +1578,7 @@ export class TaskView extends ItemView {
|
|||
*/
|
||||
private extractChangedFields(
|
||||
originalTask: Task,
|
||||
updatedTask: Task
|
||||
updatedTask: Task,
|
||||
): Partial<Task> {
|
||||
const changes: Partial<Task> = {};
|
||||
|
||||
|
|
@ -1637,14 +1651,14 @@ export class TaskView extends ItemView {
|
|||
originalTask.id,
|
||||
updatedTask.id,
|
||||
updatedTask,
|
||||
originalTask
|
||||
originalTask,
|
||||
);
|
||||
|
||||
try {
|
||||
// Extract only the changed fields
|
||||
const updates = this.extractChangedFields(
|
||||
originalTask,
|
||||
updatedTask
|
||||
updatedTask,
|
||||
);
|
||||
console.log("Extracted changes:", updates);
|
||||
|
||||
|
|
@ -1663,7 +1677,7 @@ export class TaskView extends ItemView {
|
|||
}
|
||||
|
||||
console.log(
|
||||
`Task ${updatedTask.id} updated successfully via handleTaskUpdate.`
|
||||
`Task ${updatedTask.id} updated successfully via handleTaskUpdate.`,
|
||||
);
|
||||
|
||||
// Update local task list immediately
|
||||
|
|
@ -1674,7 +1688,7 @@ export class TaskView extends ItemView {
|
|||
this.tasks[index] = updatedTask;
|
||||
} else {
|
||||
console.warn(
|
||||
"Updated task not found in local list, might reload."
|
||||
"Updated task not found in local list, might reload.",
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -1701,7 +1715,7 @@ export class TaskView extends ItemView {
|
|||
|
||||
private async updateTask(
|
||||
originalTask: Task,
|
||||
updatedTask: Task
|
||||
updatedTask: Task,
|
||||
): Promise<Task> {
|
||||
if (!this.plugin.writeAPI) {
|
||||
console.error("WriteAPI not available for updateTask");
|
||||
|
|
@ -1711,7 +1725,7 @@ export class TaskView extends ItemView {
|
|||
// Extract only the changed fields
|
||||
const updates = this.extractChangedFields(
|
||||
originalTask,
|
||||
updatedTask
|
||||
updatedTask,
|
||||
);
|
||||
console.log("Extracted changes:", updates);
|
||||
|
||||
|
|
@ -1737,7 +1751,7 @@ export class TaskView extends ItemView {
|
|||
this.tasks[index] = updatedTask;
|
||||
} else {
|
||||
console.warn(
|
||||
"Updated task not found in local list, might reload."
|
||||
"Updated task not found in local list, might reload.",
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -1853,7 +1867,7 @@ export class TaskView extends ItemView {
|
|||
if (deleteChildren && task.metadata?.children) {
|
||||
for (const childId of task.metadata.children) {
|
||||
const childIndex = this.tasks.findIndex(
|
||||
(t) => t.id === childId
|
||||
(t) => t.id === childId,
|
||||
);
|
||||
if (childIndex !== -1) {
|
||||
this.tasks.splice(childIndex, 1);
|
||||
|
|
@ -1872,8 +1886,8 @@ export class TaskView extends ItemView {
|
|||
} else {
|
||||
new Notice(
|
||||
t("Failed to delete task") +
|
||||
": " +
|
||||
(result.error || "Unknown error")
|
||||
": " +
|
||||
(result.error || "Unknown error"),
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
|
|
@ -1903,18 +1917,18 @@ export class TaskView extends ItemView {
|
|||
this.sidebarComponent.renderSidebarItems();
|
||||
} else {
|
||||
console.warn(
|
||||
"TaskView: SidebarComponent does not have renderSidebarItems method."
|
||||
"TaskView: SidebarComponent does not have renderSidebarItems method.",
|
||||
);
|
||||
}
|
||||
|
||||
// 检查当前视图的类型是否发生变化(比如从两列切换到单列)
|
||||
const currentViewConfig = this.plugin.settings.viewConfiguration.find(
|
||||
(v) => v.id === this.currentViewId
|
||||
(v) => v.id === this.currentViewId,
|
||||
);
|
||||
|
||||
// 如果当前是两列视图但配置已改为非两列,需要销毁两列组件
|
||||
const currentTwoColumn = this.twoColumnViewComponents.get(
|
||||
this.currentViewId
|
||||
this.currentViewId,
|
||||
);
|
||||
if (
|
||||
currentTwoColumn &&
|
||||
|
|
@ -1933,10 +1947,10 @@ export class TaskView extends ItemView {
|
|||
// Method to handle status updates originating from Kanban drag-and-drop
|
||||
private handleKanbanTaskStatusUpdate = async (
|
||||
taskId: string,
|
||||
newStatusMark: string
|
||||
newStatusMark: string,
|
||||
) => {
|
||||
console.log(
|
||||
`TaskView handling Kanban status update request for ${taskId} to mark ${newStatusMark}`
|
||||
`TaskView handling Kanban status update request for ${taskId} to mark ${newStatusMark}`,
|
||||
);
|
||||
const taskToUpdate = this.tasks.find((t) => t.id === taskId);
|
||||
|
||||
|
|
@ -1959,22 +1973,22 @@ export class TaskView extends ItemView {
|
|||
},
|
||||
});
|
||||
console.log(
|
||||
`Task ${taskId} status update processed by TaskView.`
|
||||
`Task ${taskId} status update processed by TaskView.`,
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`TaskView failed to update task status from Kanban callback for task ${taskId}:`,
|
||||
error
|
||||
error,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
console.log(
|
||||
`Task ${taskId} status (${newStatusMark}) already matches, no update needed.`
|
||||
`Task ${taskId} status (${newStatusMark}) already matches, no update needed.`,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
console.warn(
|
||||
`TaskView could not find task with ID ${taskId} for Kanban status update.`
|
||||
`TaskView could not find task with ID ${taskId} for Kanban status update.`,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
|
@ -1994,12 +2008,12 @@ export class TaskView extends ItemView {
|
|||
console.log("应用保存的筛选器:", config.name);
|
||||
this.liveFilterState = JSON.parse(JSON.stringify(config.filterState));
|
||||
this.currentFilterState = JSON.parse(
|
||||
JSON.stringify(config.filterState)
|
||||
JSON.stringify(config.filterState),
|
||||
);
|
||||
console.log("applySavedFilter", this.liveFilterState);
|
||||
this.app.saveLocalStorage(
|
||||
"task-genius-view-filter",
|
||||
this.liveFilterState
|
||||
this.liveFilterState,
|
||||
);
|
||||
this.applyCurrentFilter();
|
||||
this.updateActionButtons();
|
||||
|
|
|
|||
|
|
@ -1135,7 +1135,19 @@ export class TaskBasesView extends BasesView {
|
|||
onEventContextMenu: (
|
||||
ev: MouseEvent,
|
||||
event: CalendarEvent,
|
||||
) => this.handleTaskContextMenu(ev, event),
|
||||
) => {
|
||||
// Extract original task from metadata to ensure all fields (like filePath) are present
|
||||
const realTask = (event as any)?.metadata
|
||||
?.originalTask as Task;
|
||||
if (realTask) {
|
||||
this.handleTaskContextMenu(ev, realTask);
|
||||
} else {
|
||||
this.handleTaskContextMenu(
|
||||
ev,
|
||||
event as unknown as Task,
|
||||
);
|
||||
}
|
||||
},
|
||||
},
|
||||
);
|
||||
this.addChild(calendarComp);
|
||||
|
|
|
|||
|
|
@ -482,22 +482,28 @@ export class TimeParsingService {
|
|||
expression: string,
|
||||
index: number,
|
||||
): "start" | "due" | "scheduled" {
|
||||
// Get text before the expression (look back up to 20 characters)
|
||||
const beforeText = text
|
||||
.substring(Math.max(0, index - 20), index)
|
||||
.toLowerCase();
|
||||
// Get text before the expression (look back up to 200 characters to reliably capture emoji + date patterns)
|
||||
// 30 chars was insufficient for cases like "Task 🛫 2025-11-29 18:00" where emoji takes 2 chars
|
||||
const beforeTextRaw = text.substring(Math.max(0, index - 200), index);
|
||||
const beforeText = beforeTextRaw.toLowerCase();
|
||||
|
||||
// Get text after the expression (look ahead up to 20 characters)
|
||||
const afterText = text
|
||||
.substring(
|
||||
index + expression.length,
|
||||
Math.min(text.length, index + expression.length + 20),
|
||||
)
|
||||
.toLowerCase();
|
||||
const afterTextRaw = text.substring(
|
||||
index + expression.length,
|
||||
Math.min(text.length, index + expression.length + 20),
|
||||
);
|
||||
const afterText = afterTextRaw.toLowerCase();
|
||||
|
||||
// Combine surrounding context
|
||||
const context = beforeText + " " + afterText;
|
||||
|
||||
// Check for explicit emoji indicators (highest priority)
|
||||
// Use raw text (not lowercased) for emoji detection
|
||||
// These emojis are commonly used in task management to denote date types
|
||||
if (beforeTextRaw.includes("🛫")) return "start";
|
||||
if (beforeTextRaw.includes("📅")) return "due";
|
||||
if (beforeTextRaw.includes("⏳")) return "scheduled";
|
||||
|
||||
// Check for start keywords first (most specific)
|
||||
for (const keyword of this.config.dateKeywords.start) {
|
||||
if (context.includes(keyword.toLowerCase())) {
|
||||
|
|
@ -981,22 +987,28 @@ export class TimeParsingService {
|
|||
expression: string,
|
||||
index: number,
|
||||
): "start" | "due" | "scheduled" {
|
||||
// Get text before the expression (look back up to 20 characters)
|
||||
const beforeText = text
|
||||
.substring(Math.max(0, index - 20), index)
|
||||
.toLowerCase();
|
||||
// Get text before the expression (look back up to 200 characters to reliably capture emoji + date patterns)
|
||||
// 30 chars was insufficient for cases like "Task 🛫 2025-11-29 18:00" where emoji takes 2 chars
|
||||
const beforeTextRaw = text.substring(Math.max(0, index - 200), index);
|
||||
const beforeText = beforeTextRaw.toLowerCase();
|
||||
|
||||
// Get text after the expression (look ahead up to 20 characters)
|
||||
const afterText = text
|
||||
.substring(
|
||||
index + expression.length,
|
||||
Math.min(text.length, index + expression.length + 20),
|
||||
)
|
||||
.toLowerCase();
|
||||
const afterTextRaw = text.substring(
|
||||
index + expression.length,
|
||||
Math.min(text.length, index + expression.length + 20),
|
||||
);
|
||||
const afterText = afterTextRaw.toLowerCase();
|
||||
|
||||
// Combine surrounding context
|
||||
const context = beforeText + " " + afterText;
|
||||
|
||||
// Check for explicit emoji indicators (highest priority)
|
||||
// Use raw text (not lowercased) for emoji detection
|
||||
// These emojis are commonly used in task management to denote date types
|
||||
if (beforeTextRaw.includes("🛫")) return "start";
|
||||
if (beforeTextRaw.includes("📅")) return "due";
|
||||
if (beforeTextRaw.includes("⏳")) return "scheduled";
|
||||
|
||||
// Check for start keywords
|
||||
for (const keyword of this.config.dateKeywords.start) {
|
||||
if (context.includes(keyword.toLowerCase())) {
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@
|
|||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: var(--size-2-3) var(--size-4-4); /* Use Obsidian variables */
|
||||
border-bottom: 1px solid var(--background-modifier-border);
|
||||
/*border-bottom: 1px solid var(--background-modifier-border);*/
|
||||
flex-shrink: 0; /* Prevent header from shrinking */
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -102,7 +102,78 @@ export function tasksToCalendarEvents(tasks: Task[]): CalendarEvent[] {
|
|||
* @returns Object with start and optional end timestamp
|
||||
*/
|
||||
function calculateTaskDateRange(task: Task): TaskDateRange {
|
||||
const { startDate, dueDate, scheduledDate } = task.metadata;
|
||||
const enhancedDates = (task.metadata as any).enhancedDates;
|
||||
|
||||
// Use enhanced dates (with time) if available, otherwise fall back to standard dates
|
||||
const startDate = enhancedDates?.startDateTime
|
||||
? enhancedDates.startDateTime.getTime()
|
||||
: task.metadata.startDate;
|
||||
const dueDate = enhancedDates?.dueDateTime
|
||||
? enhancedDates.dueDateTime.getTime()
|
||||
: task.metadata.dueDate;
|
||||
const scheduledDate = enhancedDates?.scheduledDateTime
|
||||
? enhancedDates.scheduledDateTime.getTime()
|
||||
: task.metadata.scheduledDate;
|
||||
|
||||
// Default duration for timed events without explicit end time
|
||||
const DEFAULT_DURATION_MS = 30 * 60 * 1000; // 30 minutes
|
||||
|
||||
// Special case: explicit start and end time (e.g. from time range parsing like "10:00-12:00")
|
||||
// This handles cases where start and end are explicitly defined in the enhanced metadata
|
||||
if (enhancedDates?.startDateTime && enhancedDates?.endDateTime) {
|
||||
return {
|
||||
start: enhancedDates.startDateTime.getTime(),
|
||||
end: enhancedDates.endDateTime.getTime(),
|
||||
};
|
||||
}
|
||||
|
||||
// Handle single datetime with time info: add default 30-minute duration
|
||||
// This ensures tasks with only start time (e.g., "🛫 2025-11-29 23:35") still show on calendar
|
||||
if (enhancedDates?.startDateTime && !enhancedDates?.endDateTime) {
|
||||
const startTime = enhancedDates.startDateTime.getTime();
|
||||
// If there's a due date, use it as end; otherwise add 30 minutes
|
||||
if (dueDate && dueDate > startTime) {
|
||||
return { start: startTime, end: dueDate };
|
||||
}
|
||||
return { start: startTime, end: startTime + DEFAULT_DURATION_MS };
|
||||
}
|
||||
|
||||
if (enhancedDates?.dueDateTime && !enhancedDates?.startDateTime) {
|
||||
const dueTime = enhancedDates.dueDateTime.getTime();
|
||||
// If there's a start date, use it; otherwise subtract 30 minutes
|
||||
if (startDate && startDate < dueTime) {
|
||||
return { start: startDate, end: dueTime };
|
||||
}
|
||||
return { start: dueTime - DEFAULT_DURATION_MS, end: dueTime };
|
||||
}
|
||||
|
||||
if (
|
||||
enhancedDates?.scheduledDateTime &&
|
||||
!enhancedDates?.startDateTime &&
|
||||
!enhancedDates?.dueDateTime
|
||||
) {
|
||||
const scheduledTime = enhancedDates.scheduledDateTime.getTime();
|
||||
return {
|
||||
start: scheduledTime,
|
||||
end: scheduledTime + DEFAULT_DURATION_MS,
|
||||
};
|
||||
}
|
||||
|
||||
// FALLBACK: Handle cases where date was parsed with time directly into metadata
|
||||
// (without creating enhancedDates). Check if the timestamp has time component.
|
||||
// This happens when emoji parser extracts "🛫 2025-11-29 18:00" directly.
|
||||
if (startDate && !dueDate && !scheduledDate && !isDateOnly(startDate)) {
|
||||
return { start: startDate, end: startDate + DEFAULT_DURATION_MS };
|
||||
}
|
||||
if (dueDate && !startDate && !scheduledDate && !isDateOnly(dueDate)) {
|
||||
return { start: dueDate - DEFAULT_DURATION_MS, end: dueDate };
|
||||
}
|
||||
if (scheduledDate && !startDate && !dueDate && !isDateOnly(scheduledDate)) {
|
||||
return {
|
||||
start: scheduledDate,
|
||||
end: scheduledDate + DEFAULT_DURATION_MS,
|
||||
};
|
||||
}
|
||||
|
||||
// Priority 1: startDate → dueDate (multi-day span)
|
||||
if (startDate && dueDate && startDate !== dueDate && startDate < dueDate) {
|
||||
|
|
@ -188,6 +259,7 @@ function formatDateForCalendar(timestamp: number, isAllDay: boolean): string {
|
|||
if (isAllDay) {
|
||||
return format(date, "yyyy-MM-dd");
|
||||
}
|
||||
// Use space separator for calendar adapter compatibility (NativeDateAdapter/DateFnsAdapter)
|
||||
return format(date, "yyyy-MM-dd HH:mm");
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ export function formatDate(date: Date): string {
|
|||
*/
|
||||
export function parseLocalDate(
|
||||
dateString: string,
|
||||
customFormats?: string[]
|
||||
customFormats?: string[],
|
||||
): number | undefined {
|
||||
if (!dateString) return undefined;
|
||||
|
||||
|
|
@ -50,8 +50,26 @@ export function parseLocalDate(
|
|||
return undefined;
|
||||
}
|
||||
|
||||
// Check if the date string contains time information
|
||||
const hasTimeInfo = /\d{1,2}:\d{2}/.test(dateString);
|
||||
|
||||
// Define default format patterns to try with date-fns
|
||||
const defaultFormats = [
|
||||
// Date-time formats (with time) should be tried first when time info is present
|
||||
const dateTimeFormats = [
|
||||
"yyyy-MM-dd HH:mm", // ISO format with time
|
||||
"yyyy-MM-dd H:mm", // ISO format with single-digit hour
|
||||
"yyyy/MM/dd HH:mm", // YYYY/MM/DD with time
|
||||
"yyyy/MM/dd H:mm",
|
||||
"dd-MM-yyyy HH:mm", // DD-MM-YYYY with time
|
||||
"dd/MM/yyyy HH:mm", // DD/MM/YYYY with time
|
||||
"MM-dd-yyyy HH:mm", // MM-DD-YYYY with time
|
||||
"MM/dd/yyyy HH:mm", // MM/DD/YYYY with time
|
||||
"yyyy.MM.dd HH:mm", // YYYY.MM.DD with time
|
||||
"yyyyMMddHHmmss",
|
||||
"yyyyMMdd_HHmmss",
|
||||
];
|
||||
|
||||
const dateOnlyFormats = [
|
||||
"yyyy-MM-dd", // ISO format
|
||||
"yyyy/MM/dd", // YYYY/MM/DD
|
||||
"dd-MM-yyyy", // DD-MM-YYYY
|
||||
|
|
@ -65,10 +83,13 @@ export function parseLocalDate(
|
|||
"MMM dd, yyyy", // MMM DD, YYYY with leading zero
|
||||
"d MMM yyyy", // DD MMM YYYY (e.g., 15 Jan 2025)
|
||||
"dd MMM yyyy", // DD MMM YYYY with leading zero
|
||||
"yyyyMMddHHmmss",
|
||||
"yyyyMMdd_HHmmss",
|
||||
];
|
||||
|
||||
// Try date-time formats first if time info is present, otherwise try date-only formats
|
||||
const defaultFormats = hasTimeInfo
|
||||
? [...dateTimeFormats, ...dateOnlyFormats]
|
||||
: [...dateOnlyFormats, ...dateTimeFormats];
|
||||
|
||||
// Combine custom formats with default formats
|
||||
const allFormats = customFormats
|
||||
? [...customFormats, ...defaultFormats]
|
||||
|
|
@ -83,9 +104,15 @@ export function parseLocalDate(
|
|||
|
||||
// Check if the parsed date is valid
|
||||
if (isValid(parsedDate)) {
|
||||
// Set to start of day to match original behavior
|
||||
const normalizedDate = startOfDay(parsedDate);
|
||||
return normalizedDate.getTime();
|
||||
// Only normalize to start of day if the original string had no time info
|
||||
// and the format pattern also has no time component
|
||||
const formatHasTime = /[Hh]:mm/.test(formatString);
|
||||
if (!hasTimeInfo && !formatHasTime) {
|
||||
const normalizedDate = startOfDay(parsedDate);
|
||||
return normalizedDate.getTime();
|
||||
}
|
||||
// Preserve the time information
|
||||
return parsedDate.getTime();
|
||||
}
|
||||
} catch (e) {
|
||||
// Silently continue to next format
|
||||
|
|
@ -97,8 +124,12 @@ export function parseLocalDate(
|
|||
try {
|
||||
const isoDate = parseISO(dateString);
|
||||
if (isValid(isoDate)) {
|
||||
const normalizedDate = startOfDay(isoDate);
|
||||
return normalizedDate.getTime();
|
||||
// Only normalize to start of day if the original string had no time info
|
||||
if (!hasTimeInfo) {
|
||||
const normalizedDate = startOfDay(isoDate);
|
||||
return normalizedDate.getTime();
|
||||
}
|
||||
return isoDate.getTime();
|
||||
}
|
||||
} catch (e) {
|
||||
// Silently continue
|
||||
|
|
@ -144,7 +175,7 @@ export function getLocalDateString(date: Date): string {
|
|||
*/
|
||||
export function getRelativeTimeString(
|
||||
date: Date | number,
|
||||
lang = navigator.language
|
||||
lang = navigator.language,
|
||||
): string {
|
||||
// 允许传入日期对象或时间戳
|
||||
const timeMs = typeof date === "number" ? date : date.getTime();
|
||||
|
|
@ -159,7 +190,7 @@ export function getRelativeTimeString(
|
|||
|
||||
// 计算日期差(以天为单位)
|
||||
const deltaDays = Math.round(
|
||||
(targetDate.getTime() - today.getTime()) / (1000 * 60 * 60 * 24)
|
||||
(targetDate.getTime() - today.getTime()) / (1000 * 60 * 60 * 24),
|
||||
);
|
||||
|
||||
// 创建相对时间格式化器
|
||||
|
|
|
|||
856
styles.css
856
styles.css
File diff suppressed because one or more lines are too long
Loading…
Reference in a new issue