refactor(components): improve view management and ICS integration

- Optimize ViewComponentManager with better lifecycle management
- Improve calendar view rendering and layout optimization
- Fix task filter date range handling for better performance
- Clean up FileSource for better dataflow integration
- Enhance task details editor with optimized debouncing (800ms)
- Remove deprecated configurable-task-parser
- Improve IcsSource with retry mechanism for manager availability
- Add 30-second fallback timeout for ICS initialization
- Pass plugin instance to IcsManager for proper context access
- Update tests to match new IcsManager constructor signature

This improves overall component performance and reliability,
particularly for calendar views and ICS event synchronization.
This commit is contained in:
Quorafind 2025-08-21 21:20:50 +08:00
parent 0c6db2537b
commit d3a850b029
15 changed files with 465 additions and 334 deletions

View file

@ -1285,7 +1285,7 @@ describe("TaskParsingService Integration", () => {
// Clear cache before test
const { MarkdownTaskParser } = await import(
"../parsers/configurable-task-parser"
"../dataflow/core/ConfigurableTaskParser"
);
MarkdownTaskParser.clearDateCache();
@ -1335,7 +1335,7 @@ describe("TaskParsingService Integration", () => {
it("should handle date cache size limit correctly", async () => {
const { MarkdownTaskParser } = await import(
"../parsers/configurable-task-parser"
"../dataflow/core/ConfigurableTaskParser"
);
// Clear cache before test

View file

@ -225,7 +225,7 @@ END:VCALENDAR`;
beforeEach(async () => {
mockComponent = new MockComponent();
icsManager = new IcsManager(testConfig, mockPluginSettings);
icsManager = new IcsManager(testConfig, mockPluginSettings, {} as any);
await icsManager.initialize();
});

View file

@ -68,7 +68,7 @@ describe("ICS Manager", () => {
beforeEach(async () => {
mockComponent = new MockComponent();
icsManager = new IcsManager(testConfig, mockSettings);
icsManager = new IcsManager(testConfig, mockSettings, {} as any);
await icsManager.initialize();
});
@ -265,7 +265,8 @@ describe("ICS Manager", () => {
...testConfig,
sources: [sourceWithReplacements],
},
mockSettings
mockSettings,
{} as any
);
// Convert event to task (this will apply text replacements)
@ -323,7 +324,8 @@ describe("ICS Manager", () => {
...testConfig,
sources: [sourceWithMultipleReplacements],
},
mockSettings
mockSettings,
{} as any
);
const task = testManager.convertEventsToTasks([mockEvent])[0];
@ -370,7 +372,8 @@ describe("ICS Manager", () => {
...testConfig,
sources: [sourceWithAllTarget],
},
mockSettings
mockSettings,
{} as any
);
const task = testManager.convertEventsToTasks([mockEvent])[0];
@ -428,7 +431,8 @@ describe("ICS Manager", () => {
...testConfig,
sources: [sourceWithDisabledRule],
},
mockSettings
mockSettings,
{} as any
);
const task = testManager.convertEventsToTasks([mockEvent])[0];
@ -473,7 +477,8 @@ describe("ICS Manager", () => {
...testConfig,
sources: [sourceWithInvalidRegex],
},
mockSettings
mockSettings,
{} as any
);
// Should not throw an error, and text should remain unchanged
@ -519,7 +524,8 @@ describe("ICS Manager", () => {
...testConfig,
sources: [sourceWithCaptureGroups],
},
mockSettings
mockSettings,
{} as any
);
const task = testManager.convertEventsToTasks([mockEvent])[0];
@ -556,7 +562,8 @@ describe("ICS Manager", () => {
...testConfig,
sources: [sourceWithoutReplacements],
},
mockSettings
mockSettings,
{} as any
);
const task = testManager.convertEventsToTasks([mockEvent])[0];
@ -628,7 +635,7 @@ describe("ICS Manager Integration", () => {
defaultEventColor: "#3b82f6",
};
const manager = new IcsManager(config, mockSettings);
const manager = new IcsManager(config, mockSettings, {} as any);
await manager.initialize();
try {

View file

@ -86,7 +86,7 @@ describe("ICS Timeout Fix", () => {
beforeEach(async () => {
mockComponent = new MockComponent();
icsManager = new IcsManager(testConfig, mockSettings);
icsManager = new IcsManager(testConfig, mockSettings, {} as any);
await icsManager.initialize();
});

View file

@ -30,7 +30,7 @@ interface ViewEventHandlers {
onTaskContextMenu?: (event: MouseEvent, task: Task) => void;
onTaskStatusUpdate?: (
taskId: string,
newStatusMark: string
newStatusMark: string,
) => Promise<void>;
onEventContextMenu?: (ev: MouseEvent, event: CalendarEvent) => void;
onTaskUpdate?: (originalTask: Task, updatedTask: Task) => Promise<void>;
@ -44,7 +44,7 @@ class ViewComponentFactory {
app: App,
plugin: TaskProgressBarPlugin,
parentEl: HTMLElement,
handlers: ViewEventHandlers
handlers: ViewEventHandlers,
): ViewComponentInterface | null {
const viewConfig = getViewSettingOrDefault(plugin, viewId);
@ -61,7 +61,7 @@ class ViewComponentFactory {
onTaskCompleted: handlers.onTaskCompleted,
onTaskContextMenu: handlers.onTaskContextMenu,
},
viewId
viewId,
);
case "calendar":
@ -75,7 +75,7 @@ class ViewComponentFactory {
onTaskCompleted: handlers.onTaskCompleted,
onEventContextMenu: handlers.onEventContextMenu,
},
viewId
viewId,
);
case "gantt":
@ -87,7 +87,7 @@ class ViewComponentFactory {
onTaskCompleted: handlers.onTaskCompleted,
onTaskContextMenu: handlers.onTaskContextMenu,
},
viewId
viewId,
);
case "twocolumn":
@ -97,7 +97,7 @@ class ViewComponentFactory {
app,
plugin,
viewConfig.specificConfig,
viewId
viewId,
);
}
return null;
@ -123,19 +123,27 @@ class ViewComponentFactory {
onTaskContextMenu: handlers.onTaskContextMenu,
onTaskUpdated: async (task: Task) => {
// Handle task updates through WriteAPI if dataflow is enabled
if (plugin.settings?.experimental?.dataflowEnabled && plugin.writeAPI) {
const result = await plugin.writeAPI.updateTask({
taskId: task.id,
updates: task
});
if (
plugin.settings?.experimental
?.dataflowEnabled &&
plugin.writeAPI
) {
const result =
await plugin.writeAPI.updateTask({
taskId: task.id,
updates: task,
});
if (!result.success) {
console.error("Failed to update task:", result.error);
console.error(
"Failed to update task:",
result.error,
);
}
} else if (plugin.taskManager) {
await plugin.taskManager.updateTask(task);
}
},
}
},
);
}
return null;
@ -153,20 +161,29 @@ class ViewComponentFactory {
onTaskContextMenu: handlers.onTaskContextMenu,
onTaskUpdated: async (task: Task) => {
// Handle task updates through WriteAPI if dataflow is enabled
if (plugin.settings?.experimental?.dataflowEnabled && plugin.writeAPI) {
const result = await plugin.writeAPI.updateTask({
taskId: task.id,
updates: task
});
if (
plugin.settings?.experimental
?.dataflowEnabled &&
plugin.writeAPI
) {
const result = await plugin.writeAPI.updateTask(
{
taskId: task.id,
updates: task,
},
);
if (!result.success) {
console.error("Failed to update task:", result.error);
console.error(
"Failed to update task:",
result.error,
);
}
} else if (plugin.taskManager) {
await plugin.taskManager.updateTask(task);
}
},
},
viewId
viewId,
);
default:
@ -189,7 +206,7 @@ export class ViewComponentManager extends Component {
app: App,
plugin: TaskProgressBarPlugin,
parentEl: HTMLElement,
handlers: ViewEventHandlers
handlers: ViewEventHandlers,
) {
super();
this.parentComponent = parentComponent;
@ -240,7 +257,7 @@ export class ViewComponentManager extends Component {
this.app,
this.plugin,
this.parentEl,
this.handlers
this.handlers,
);
if (component) {
@ -296,14 +313,14 @@ export class ViewComponentManager extends Component {
viewId,
specificViewType,
["calendar", "kanban", "gantt", "forecast", "table"].includes(
viewId
)
viewId,
),
);
return !!(
specificViewType ||
["calendar", "kanban", "gantt", "forecast", "table"].includes(
viewId
viewId,
)
);
}

View file

@ -461,8 +461,13 @@ export class CalendarComponent extends Component {
const icsTask = isIcsTask ? (task as IcsTask) : null; // Type assertion for IcsTask
const showAsBadge = icsTask?.icsEvent?.source?.showType === "badge";
// If ICS is configured as badge, do NOT add a full event; badges will be
// provided via getBadgeEventsForDate from the raw tasks list to avoid duplication.
if (isIcsTask && showAsBadge) {
return; // skip adding to this.events
}
// Determine the date to use based on priority (dueDate > scheduledDate > startDate)
// This logic might need refinement based on exact requirements in PRD 4.2
let eventDate: number | null = null;
let isAllDay = true; // Assume tasks are all-day unless time info exists
@ -471,9 +476,6 @@ export class CalendarComponent extends Component {
eventDate = icsTask.icsEvent.dtstart.getTime();
isAllDay = icsTask.icsEvent.allDay;
} else {
// Use the first available date field based on preference.
// The PRD mentions using dueDate primarily, with an option for scheduled/start.
// Let's stick to dueDate for now as primary.
if (task.metadata[primaryDateField]) {
eventDate = task.metadata[primaryDateField];
} else if (task.metadata.scheduledDate) {
@ -482,44 +484,31 @@ export class CalendarComponent extends Component {
eventDate = task.metadata.startDate;
}
}
// We could add completedDate here if we want to show completed tasks based on completion time
if (eventDate) {
const startMoment = moment(eventDate);
// For ICS events, preserve the original time if not all-day
const start = isAllDay
? startMoment.startOf("day").toDate()
: startMoment.toDate();
// Handle multi-day? PRD mentions if startDate and dueDate are available.
let end: Date | undefined = undefined;
let effectiveStart = start; // Use the primary date as start by default
if (isIcsTask && icsTask?.icsEvent?.dtend) {
// For ICS events, use the end date from the event
end = icsTask.icsEvent.dtend;
} else if (
task.metadata.startDate &&
task.metadata.dueDate &&
task.metadata.startDate !== task.metadata.dueDate
) {
// Ensure start is actually before due date
const sMoment = moment(task.metadata.startDate).startOf(
"day"
);
const dMoment = moment(task.metadata.dueDate).startOf(
"day"
);
const sMoment = moment(task.metadata.startDate).startOf("day");
const dMoment = moment(task.metadata.dueDate).startOf("day");
if (sMoment.isBefore(dMoment)) {
// FullCalendar and similar often expect the 'end' date to be exclusive
// for all-day events. So an event ending on the 15th would have end=16th.
end = dMoment.add(1, "day").toDate();
// The 'start' should likely be the startDate in this case
effectiveStart = sMoment.toDate(); // Re-assign start if using date range
effectiveStart = sMoment.toDate();
}
}
// Determine color for the event
let eventColor: string | undefined;
if (isIcsTask && icsTask?.icsEvent?.source?.color) {
eventColor = icsTask.icsEvent.source.color;
@ -528,16 +517,14 @@ export class CalendarComponent extends Component {
}
this.events.push({
...task, // Spread all properties from the original task
title: task.content, // Use task content as title by default
...task,
title: task.content,
start: effectiveStart,
end: end, // Add end date if calculated
end: end,
allDay: isAllDay,
color: eventColor,
badge: showAsBadge, // Mark badge events for special handling
});
}
// Else: Task has no relevant date, ignore for now (PRD: maybe "unscheduled" panel)
});
// Sort events for potentially easier rendering later (e.g., agenda)

View file

@ -440,11 +440,11 @@ export class TaskDetailsComponent extends Component {
cls: "date-input",
});
if (task.metadata.dueDate) {
// Use UTC methods to avoid timezone issues
// Use local date components to avoid off-by-one due to timezone
const date = new Date(task.metadata.dueDate);
const year = date.getUTCFullYear();
const month = String(date.getUTCMonth() + 1).padStart(2, "0");
const day = String(date.getUTCDate()).padStart(2, "0");
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, "0");
const day = String(date.getDate()).padStart(2, "0");
dueDateInput.value = `${year}-${month}-${day}`;
} // Start date
const startDateField = this.createFormField(
@ -514,10 +514,6 @@ export class TaskDetailsComponent extends Component {
// Update task properties
const newContent = contentInput.getValue();
updatedTask.content = newContent;
// Also update originalMarkdown if content has changed
if (task.content !== newContent) {
updatedTask.originalMarkdown = newContent;
}
// Update metadata properties
const metadata = { ...updatedTask.metadata };
@ -671,14 +667,10 @@ export class TaskDetailsComponent extends Component {
try {
await this.onTaskUpdate(task, updatedTask);
// Update the current task reference but don't redraw the UI
// 更新本地引用并立即重绘详情,避免显示“上一次”的值
this.currentTask = updatedTask;
// Reset editing state after successful update to allow view refreshes
// Use setTimeout to ensure the reset happens after any immediate WriteAPI events
setTimeout(() => {
this.isEditing = false;
}, 50);
console.log("updatedTask", updatedTask);
this.isEditing = false;
this.showTaskDetails(updatedTask);
} catch (error) {
console.error("Failed to update task:", error);
// TODO: Show error message to user

View file

@ -414,19 +414,20 @@ export class FileSource {
// Generate task content based on configuration
const content = this.generateTaskContent(filePath, fileContent, fileCache);
const safeContent = (typeof content === "string") ? content : (filePath.split('/').pop() || filePath);
// Extract metadata from frontmatter
const metadata = this.extractTaskMetadata(filePath, fileContent, fileCache, strategy);
// Create the file task
const fileTask: Task<FileSourceTaskMetadata> = {
id: `file-source:${filePath}`,
content,
content: safeContent,
filePath,
line: 0, // File tasks are at line 0
completed: metadata.status === 'x' || metadata.status === 'X',
status: metadata.status || config.fileTaskProperties.defaultStatus,
originalMarkdown: `[${content}](${filePath})`,
originalMarkdown: `[${safeContent}](${filePath})`,
metadata: {
...metadata,
source: "file-source",

View file

@ -1,6 +1,6 @@
/**
* IcsSource - Event source for ICS calendar data
*
*
* This source integrates external calendar events into the dataflow architecture.
* It listens to IcsManager updates and emits standardized dataflow events.
*/
@ -27,15 +27,33 @@ export class IcsSource {
if (this.isInitialized) return;
console.log("[IcsSource] Initializing ICS event source...");
// Initial load of ICS events
this.loadAndEmitIcsEvents();
// Subscribe to ICS manager updates
// Subscribe to ICS manager updates first so we don't miss early signals
this.subscribeToIcsUpdates();
// Initial load of ICS events (may be no-op if manager not ready yet)
this.loadAndEmitIcsEvents();
// Fallback: retry until ICS manager becomes available (up to ~30s)
this.ensureManagerAndLoad(0);
this.isInitialized = true;
}
/**
* Ensure ICS manager becomes available shortly after startup and then load
*/
private ensureManagerAndLoad(attempt: number): void {
const maxAttempts = 30; // ~30s with 1s interval
if (this.getIcsManager()) {
this.loadAndEmitIcsEvents();
return;
}
if (attempt >= maxAttempts) {
console.warn("[IcsSource] ICS manager not available after retries");
return;
}
setTimeout(() => this.ensureManagerAndLoad(attempt + 1), 1000);
}
/**
* Subscribe to ICS manager update events
@ -67,19 +85,9 @@ export class IcsSource {
try {
// Get all ICS events with sync
const icsEvents = await icsManager.getAllEventsWithSync();
// Convert ICS events to Task format with proper source marking
const icsTasks: Task[] = icsEvents.map((event: any) => ({
...event,
metadata: {
...event.metadata,
source: {
type: 'ics',
id: event.source?.id || 'unknown',
name: event.source?.name || 'ICS Calendar'
}
}
}));
// Convert ICS events to IcsTask format via manager to ensure proper shape
const icsTasks: Task[] = icsManager.convertEventsToTasks(icsEvents);
console.log(`[IcsSource] Loaded ${icsTasks.length} ICS events`);
@ -99,7 +107,7 @@ export class IcsSource {
} catch (error) {
console.error("[IcsSource] Error loading ICS events:", error);
// Emit empty update on error to clear stale data
emit(this.app, Events.ICS_EVENTS_UPDATED, {
events: [],
@ -115,12 +123,12 @@ export class IcsSource {
*/
private getSourceStats(events: Task[]): Record<string, number> {
const stats: Record<string, number> = {};
for (const event of events) {
const sourceId = event.metadata?.source?.id || 'unknown';
stats[sourceId] = (stats[sourceId] || 0) + 1;
}
return stats;
}
@ -150,13 +158,13 @@ export class IcsSource {
*/
destroy(): void {
console.log("[IcsSource] Destroying ICS source...");
// Clear event listeners
for (const ref of this.eventRefs) {
this.app.vault.offref(ref);
}
this.eventRefs = [];
// Emit clear event
emit(this.app, Events.ICS_EVENTS_UPDATED, {
events: [],
@ -164,7 +172,7 @@ export class IcsSource {
seq: Seq.next(),
destroyed: true
});
this.isInitialized = false;
}
}

View file

@ -104,7 +104,10 @@ import { VersionManager } from "./managers/version-manager";
import { RebuildProgressManager } from "./managers/rebuild-progress-manager";
import { OnboardingConfigManager } from "./managers/onboarding-manager";
import { SettingsChangeDetector } from "./services/settings-change-detector";
import { OnboardingView, ONBOARDING_VIEW_TYPE } from "./components/onboarding/OnboardingView";
import {
OnboardingView,
ONBOARDING_VIEW_TYPE,
} from "./components/onboarding/OnboardingView";
import { TaskTimerExporter } from "./services/timer-export-service";
import { TaskTimerManager } from "./managers/timer-manager";
import { McpServerManager } from "./mcp/McpServerManager";
@ -135,7 +138,7 @@ class TaskProgressBarPopover extends HoverPopover {
},
parent: HoverParent,
targetEl: HTMLElement,
waitTime: number = 1000
waitTime: number = 1000,
) {
super(parent, targetEl, waitTime);
@ -159,7 +162,7 @@ class TaskProgressBarPopover extends HoverPopover {
`,
this.hoverEl,
"",
this.plugin
this.plugin,
);
}
}
@ -181,7 +184,7 @@ export const showPopoverWithProgressBar = (
planned: string;
};
view: EditorView;
}
},
) => {
const editor = view.state.field(editorInfoField);
if (!editor) return;
@ -195,7 +198,7 @@ export default class TaskProgressBarPlugin extends Plugin {
// Dataflow orchestrator instance (experimental)
dataflowOrchestrator?: DataflowOrchestrator;
// Write API for dataflow architecture
writeAPI?: WriteAPI;
@ -283,7 +286,9 @@ export default class TaskProgressBarPlugin extends Plugin {
// Initialize dataflow orchestrator if enabled (experimental)
if (isDataflowEnabled(this)) {
try {
console.log("[Plugin] Dataflow architecture enabled - initializing...");
console.log(
"[Plugin] Dataflow architecture enabled - initializing...",
);
// Wait for dataflow initialization to complete before proceeding
this.dataflowOrchestrator = await createDataflow(
this.app,
@ -292,11 +297,16 @@ export default class TaskProgressBarPlugin extends Plugin {
this,
{
// ProjectConfigManagerOptions is narrower; pass only known properties
}
},
);
console.log(
"[Plugin] Dataflow orchestrator initialized successfully",
);
console.log("[Plugin] Dataflow orchestrator initialized successfully");
} catch (error) {
console.error("[Plugin] Failed to initialize dataflow orchestrator:", error);
console.error(
"[Plugin] Failed to initialize dataflow orchestrator:",
error,
);
// Continue without dataflow, fallback to TaskManager
}
}
@ -311,37 +321,43 @@ export default class TaskProgressBarPlugin extends Plugin {
{
useWorkers: true,
debug: true, // Set to true for debugging
}
},
);
this.addChild(this.taskManager);
console.log("[Plugin] TaskManager initialized");
// Initialize WriteAPI if dataflow is enabled
if (this.settings?.experimental?.dataflowEnabled) {
const getTaskById = async (id: string): Promise<Task | null> => {
const getTaskById = async (
id: string,
): Promise<Task | null> => {
// Try dataflow first, fallback to taskManager
if (this.dataflowOrchestrator) {
try {
const repository = this.dataflowOrchestrator.getRepository();
const repository =
this.dataflowOrchestrator.getRepository();
const task = await repository.getTaskById(id);
if (task) {
return task;
}
} catch (e) {
console.warn("Failed to get task from dataflow, falling back to taskManager", e);
console.warn(
"Failed to get task from dataflow, falling back to taskManager",
e,
);
}
}
const taskManagerResult = this.taskManager.getTaskById(id);
return taskManagerResult || null;
};
this.writeAPI = new WriteAPI(
this.app,
this.app.vault,
this.app.metadataCache,
this,
getTaskById
getTaskById,
);
}
}
@ -377,18 +393,18 @@ export default class TaskProgressBarPlugin extends Plugin {
item.setTitle(
`${t("Set priority")}: ${
priority.text
}`
}`,
);
item.setIcon("arrow-big-up-dash");
item.onClick(() => {
setPriorityAtCursor(
editor,
priority.emoji
priority.emoji,
);
});
});
}
}
},
);
submenu.addSeparator();
@ -398,17 +414,17 @@ export default class TaskProgressBarPlugin extends Plugin {
([key, priority]) => {
submenu.addItem((item) => {
item.setTitle(
`${t("Set priority")}: ${key}`
`${t("Set priority")}: ${key}`,
);
item.setIcon("a-arrow-up");
item.onClick(() => {
setPriorityAtCursor(
editor,
`[#${key}]`
`[#${key}]`,
);
});
});
}
},
);
// Remove priority command
@ -428,7 +444,7 @@ export default class TaskProgressBarPlugin extends Plugin {
if (this.settings.workflow.enableWorkflow) {
updateWorkflowContextMenu(menu, editor, this);
}
})
}),
);
this.app.workspace.onLayoutReady(() => {
@ -485,35 +501,36 @@ export default class TaskProgressBarPlugin extends Plugin {
this.initializeTaskManagerWithVersionCheck().catch((error) => {
console.error(
"Failed to initialize task manager with version check:",
error
error,
);
});
// Register the TaskView
this.registerView(
TASK_VIEW_TYPE,
(leaf) => new TaskView(leaf, this)
(leaf) => new TaskView(leaf, this),
);
this.registerView(
TASK_SPECIFIC_VIEW_TYPE,
(leaf) => new TaskSpecificView(leaf, this)
(leaf) => new TaskSpecificView(leaf, this),
);
// Register the Timeline Sidebar View
this.registerView(
TIMELINE_SIDEBAR_VIEW_TYPE,
(leaf) => new TimelineSidebarView(leaf, this)
(leaf) => new TimelineSidebarView(leaf, this),
);
// Register the Onboarding View
this.registerView(
ONBOARDING_VIEW_TYPE,
(leaf) => new OnboardingView(leaf, this, () => {
console.log("Onboarding completed successfully");
// Close the onboarding view and refresh views
leaf.detach();
})
(leaf) =>
new OnboardingView(leaf, this, () => {
console.log("Onboarding completed successfully");
// Close the onboarding view and refresh views
leaf.detach();
}),
);
// Add a ribbon icon for opening the TaskView
@ -522,7 +539,7 @@ export default class TaskProgressBarPlugin extends Plugin {
t("Open Task Genius view"),
() => {
this.activateTaskView();
}
},
);
// Add a command to open the TaskView
this.addCommand({
@ -561,7 +578,8 @@ export default class TaskProgressBarPlugin extends Plugin {
if (this.settings.icsIntegration.sources.length > 0) {
this.icsManager = new IcsManager(
this.settings.icsIntegration,
this.settings
this.settings,
this,
);
this.addChild(this.icsManager);
@ -581,7 +599,7 @@ export default class TaskProgressBarPlugin extends Plugin {
this.activateTimelineSidebarView().catch((error) => {
console.error(
"Failed to auto-open timeline sidebar:",
error
error,
);
});
}, 1000);
@ -598,11 +616,11 @@ export default class TaskProgressBarPlugin extends Plugin {
(preset: any) => {
if (preset.options) {
preset.options = migrateOldFilterOptions(
preset.options
preset.options,
);
}
return preset;
}
},
);
await this.saveSettings();
}
@ -637,7 +655,7 @@ export default class TaskProgressBarPlugin extends Plugin {
if (view) {
view.dispatch({
effects: toggleTaskFilter.of(
!view.state.field(taskFilterState)
!view.state.field(taskFilterState),
),
});
}
@ -657,14 +675,14 @@ export default class TaskProgressBarPlugin extends Plugin {
const changes = sortTasksInDocument(
editorView,
this,
false
false,
);
if (changes) {
new Notice(
t(
"Tasks sorted (using settings). Change application needs refinement."
)
"Tasks sorted (using settings). Change application needs refinement.",
),
);
} else {
// Notice is already handled within sortTasksInDocument if no changes or sorting disabled
@ -688,11 +706,11 @@ export default class TaskProgressBarPlugin extends Plugin {
return changes;
});
new Notice(
t("Entire document sorted (using settings).")
t("Entire document sorted (using settings)."),
);
} else {
new Notice(
t("Tasks already sorted or no tasks found.")
t("Tasks already sorted or no tasks found."),
);
}
},
@ -725,28 +743,44 @@ export default class TaskProgressBarPlugin extends Plugin {
callback: async () => {
try {
new Notice(t("Refreshing task index..."));
// Check if dataflow is enabled
if (this.settings?.experimental?.dataflowEnabled && this.dataflowOrchestrator) {
if (
this.settings?.experimental?.dataflowEnabled &&
this.dataflowOrchestrator
) {
// Use dataflow orchestrator for refresh
console.log("[Command] Refreshing task index via dataflow");
console.log(
"[Command] Refreshing task index via dataflow",
);
// Re-scan all files to refresh the index
const files = this.app.vault.getMarkdownFiles();
const canvasFiles = this.app.vault.getFiles().filter(f => f.extension === "canvas");
const canvasFiles = this.app.vault
.getFiles()
.filter((f) => f.extension === "canvas");
const allFiles = [...files, ...canvasFiles];
// Process files in batches
const batchSize = 50;
for (let i = 0; i < allFiles.length; i += batchSize) {
for (
let i = 0;
i < allFiles.length;
i += batchSize
) {
const batch = allFiles.slice(i, i + batchSize);
await Promise.all(batch.map(file =>
(this.dataflowOrchestrator as any).processFileImmediate(file)
));
await Promise.all(
batch.map((file) =>
(
this.dataflowOrchestrator as any
).processFileImmediate(file),
),
);
}
// Refresh ICS events if available
const icsSource = (this.dataflowOrchestrator as any).icsSource;
const icsSource = (this.dataflowOrchestrator as any)
.icsSource;
if (icsSource) {
await icsSource.refresh();
}
@ -754,7 +788,7 @@ export default class TaskProgressBarPlugin extends Plugin {
// Use legacy task manager
await this.taskManager.initialize();
}
new Notice(t("Task index refreshed"));
} catch (error) {
console.error("Failed to refresh task index:", error);
@ -770,20 +804,30 @@ export default class TaskProgressBarPlugin extends Plugin {
callback: async () => {
try {
// Check if dataflow is enabled
if (this.settings?.experimental?.dataflowEnabled && this.dataflowOrchestrator) {
if (
this.settings?.experimental?.dataflowEnabled &&
this.dataflowOrchestrator
) {
// Use dataflow orchestrator for force reindex
console.log("[Command] Force reindexing via dataflow");
new Notice(t("Clearing task cache and rebuilding index..."));
console.log(
"[Command] Force reindexing via dataflow",
);
new Notice(
t(
"Clearing task cache and rebuilding index...",
),
);
// Clear all caches and rebuild from scratch
await this.dataflowOrchestrator.rebuild();
// Refresh ICS events after rebuild
const icsSource = (this.dataflowOrchestrator as any).icsSource;
const icsSource = (this.dataflowOrchestrator as any)
.icsSource;
if (icsSource) {
await icsSource.refresh();
}
new Notice(t("Task index completely rebuilt"));
} else {
// Use legacy task manager
@ -854,7 +898,7 @@ export default class TaskProgressBarPlugin extends Plugin {
editor,
ctx,
this,
"allCompleted"
"allCompleted",
);
},
});
@ -869,7 +913,7 @@ export default class TaskProgressBarPlugin extends Plugin {
editor,
ctx,
this,
"directChildren"
"directChildren",
);
},
});
@ -884,7 +928,7 @@ export default class TaskProgressBarPlugin extends Plugin {
editor,
ctx,
this,
"all"
"all",
);
},
});
@ -900,7 +944,7 @@ export default class TaskProgressBarPlugin extends Plugin {
editor,
ctx,
this,
"allCompleted"
"allCompleted",
);
},
});
@ -908,7 +952,7 @@ export default class TaskProgressBarPlugin extends Plugin {
this.addCommand({
id: "auto-move-direct-completed-subtasks",
name: t(
"Auto-move direct completed subtasks to default file"
"Auto-move direct completed subtasks to default file",
),
editorCheckCallback: (checking, editor, ctx) => {
return autoMoveCompletedTasksCommand(
@ -916,7 +960,7 @@ export default class TaskProgressBarPlugin extends Plugin {
editor,
ctx,
this,
"directChildren"
"directChildren",
);
},
});
@ -930,7 +974,7 @@ export default class TaskProgressBarPlugin extends Plugin {
editor,
ctx,
this,
"all"
"all",
);
},
});
@ -949,7 +993,7 @@ export default class TaskProgressBarPlugin extends Plugin {
editor,
ctx,
this,
"allIncompleted"
"allIncompleted",
);
},
});
@ -964,7 +1008,7 @@ export default class TaskProgressBarPlugin extends Plugin {
editor,
ctx,
this,
"directIncompletedChildren"
"directIncompletedChildren",
);
},
});
@ -980,7 +1024,7 @@ export default class TaskProgressBarPlugin extends Plugin {
editor,
ctx,
this,
"allIncompleted"
"allIncompleted",
);
},
});
@ -988,7 +1032,7 @@ export default class TaskProgressBarPlugin extends Plugin {
this.addCommand({
id: "auto-move-direct-incomplete-subtasks",
name: t(
"Auto-move direct incomplete subtasks to default file"
"Auto-move direct incomplete subtasks to default file",
),
editorCheckCallback: (checking, editor, ctx) => {
return autoMoveCompletedTasksCommand(
@ -996,7 +1040,7 @@ export default class TaskProgressBarPlugin extends Plugin {
editor,
ctx,
this,
"directIncompletedChildren"
"directIncompletedChildren",
);
},
});
@ -1062,8 +1106,8 @@ export default class TaskProgressBarPlugin extends Plugin {
} catch (e) {
new Notice(
t(
"Could not open quick capture panel in the current editor"
)
"Could not open quick capture panel in the current editor",
),
);
}
}, 100);
@ -1082,7 +1126,7 @@ export default class TaskProgressBarPlugin extends Plugin {
checking,
editor,
ctx,
this
this,
);
},
});
@ -1095,7 +1139,7 @@ export default class TaskProgressBarPlugin extends Plugin {
checking,
editor,
ctx,
this
this,
);
},
});
@ -1108,7 +1152,7 @@ export default class TaskProgressBarPlugin extends Plugin {
checking,
editor,
ctx,
this
this,
);
},
});
@ -1121,7 +1165,7 @@ export default class TaskProgressBarPlugin extends Plugin {
checking,
editor,
ctx,
this
this,
);
},
});
@ -1134,7 +1178,7 @@ export default class TaskProgressBarPlugin extends Plugin {
checking,
editor,
ctx,
this
this,
);
},
});
@ -1147,7 +1191,7 @@ export default class TaskProgressBarPlugin extends Plugin {
checking,
editor,
ctx,
this
this,
);
},
});
@ -1166,20 +1210,25 @@ export default class TaskProgressBarPlugin extends Plugin {
return;
}
const jsonData = this.taskTimerExporter.exportToJSON(true);
const jsonData =
this.taskTimerExporter.exportToJSON(true);
// Create a blob and download link
const blob = new Blob([jsonData], {type: 'application/json'});
const blob = new Blob([jsonData], {
type: "application/json",
});
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
const a = document.createElement("a");
a.href = url;
a.download = `task-timer-data-${new Date().toISOString().split('T')[0]}.json`;
a.download = `task-timer-data-${new Date().toISOString().split("T")[0]}.json`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
new Notice(`Exported ${stats.activeTimers} timer records`);
new Notice(
`Exported ${stats.activeTimers} timer records`,
);
} catch (error) {
console.error("Error exporting timer data:", error);
new Notice("Failed to export timer data");
@ -1193,25 +1242,34 @@ export default class TaskProgressBarPlugin extends Plugin {
callback: async () => {
try {
// Create file input for JSON import
const input = document.createElement('input');
input.type = 'file';
input.accept = '.json';
const input = document.createElement("input");
input.type = "file";
input.accept = ".json";
input.onchange = async (e) => {
const file = (e.target as HTMLInputElement).files?.[0];
const file = (e.target as HTMLInputElement)
.files?.[0];
if (!file) return;
try {
const text = await file.text();
const success = this.taskTimerExporter.importFromJSON(text);
const success =
this.taskTimerExporter.importFromJSON(text);
if (success) {
new Notice("Timer data imported successfully");
new Notice(
"Timer data imported successfully",
);
} else {
new Notice("Failed to import timer data - invalid format");
new Notice(
"Failed to import timer data - invalid format",
);
}
} catch (error) {
console.error("Error importing timer data:", error);
console.error(
"Error importing timer data:",
error,
);
new Notice("Failed to import timer data");
}
};
@ -1235,22 +1293,30 @@ export default class TaskProgressBarPlugin extends Plugin {
return;
}
const yamlData = this.taskTimerExporter.exportToYAML(true);
const yamlData =
this.taskTimerExporter.exportToYAML(true);
// Create a blob and download link
const blob = new Blob([yamlData], {type: 'text/yaml'});
const blob = new Blob([yamlData], {
type: "text/yaml",
});
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
const a = document.createElement("a");
a.href = url;
a.download = `task-timer-data-${new Date().toISOString().split('T')[0]}.yaml`;
a.download = `task-timer-data-${new Date().toISOString().split("T")[0]}.yaml`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
new Notice(`Exported ${stats.activeTimers} timer records to YAML`);
new Notice(
`Exported ${stats.activeTimers} timer records to YAML`,
);
} catch (error) {
console.error("Error exporting timer data to YAML:", error);
console.error(
"Error exporting timer data to YAML:",
error,
);
new Notice("Failed to export timer data to YAML");
}
},
@ -1261,14 +1327,17 @@ export default class TaskProgressBarPlugin extends Plugin {
name: "Create task timer backup",
callback: async () => {
try {
const backupData = this.taskTimerExporter.createBackup();
const backupData =
this.taskTimerExporter.createBackup();
// Create a blob and download link
const blob = new Blob([backupData], {type: 'application/json'});
const blob = new Blob([backupData], {
type: "application/json",
});
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
const a = document.createElement("a");
a.href = url;
a.download = `task-timer-backup-${new Date().toISOString().replace(/[:.]/g, '-')}.json`;
a.download = `task-timer-backup-${new Date().toISOString().replace(/[:.]/g, "-")}.json`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
@ -1319,24 +1388,26 @@ export default class TaskProgressBarPlugin extends Plugin {
if (this.settings.taskTimer?.enabled) {
// Initialize task timer manager and exporter
if (!this.taskTimerManager) {
this.taskTimerManager = new TaskTimerManager(this.settings.taskTimer);
this.taskTimerManager = new TaskTimerManager(
this.settings.taskTimer,
);
}
if (!this.taskTimerExporter) {
this.taskTimerExporter = new TaskTimerExporter(this.taskTimerManager);
this.taskTimerExporter = new TaskTimerExporter(
this.taskTimerManager,
);
}
this.registerEditorExtension([
taskTimerExtension(this),
]);
this.registerEditorExtension([taskTimerExtension(this)]);
}
this.settings.taskGutter.enableTaskGutter &&
this.registerEditorExtension([taskGutterExtension(this.app, this)]);
this.registerEditorExtension([taskGutterExtension(this.app, this)]);
this.settings.enableTaskStatusSwitcher &&
this.settings.enableCustomTaskMarks &&
this.registerEditorExtension([
taskStatusSwitcherExtension(this.app, this),
]);
this.settings.enableCustomTaskMarks &&
this.registerEditorExtension([
taskStatusSwitcherExtension(this.app, this),
]);
// Add priority picker extension
if (this.settings.enablePriorityPicker) {
@ -1372,7 +1443,7 @@ export default class TaskProgressBarPlugin extends Plugin {
if (this.settings.quickCapture.enableMinimalMode) {
this.minimalQuickCaptureSuggest = new MinimalQuickCaptureSuggest(
this.app,
this
this,
);
this.registerEditorSuggest(this.minimalQuickCaptureSuggest);
}
@ -1402,7 +1473,10 @@ export default class TaskProgressBarPlugin extends Plugin {
// Clean up dataflow orchestrator (experimental)
if (this.dataflowOrchestrator) {
this.dataflowOrchestrator.cleanup().catch((error) => {
console.error("Error cleaning up dataflow orchestrator:", error);
console.error(
"Error cleaning up dataflow orchestrator:",
error,
);
});
}
@ -1425,15 +1499,20 @@ export default class TaskProgressBarPlugin extends Plugin {
private async checkAndShowOnboarding(): Promise<void> {
try {
// Check if this is the first install and onboarding hasn't been completed
const versionResult = await this.versionManager.checkVersionChange();
const versionResult =
await this.versionManager.checkVersionChange();
const isFirstInstall = versionResult.versionInfo.isFirstInstall;
const shouldShowOnboarding = this.onboardingConfigManager.shouldShowOnboarding();
const shouldShowOnboarding =
this.onboardingConfigManager.shouldShowOnboarding();
// For existing users with changes, let the view handle the async detection
// For new users, show onboarding directly
if ((isFirstInstall && shouldShowOnboarding) ||
(!isFirstInstall && shouldShowOnboarding && this.settingsChangeDetector.hasUserMadeChanges())) {
if (
(isFirstInstall && shouldShowOnboarding) ||
(!isFirstInstall &&
shouldShowOnboarding &&
this.settingsChangeDetector.hasUserMadeChanges())
) {
// Small delay to ensure UI is ready
setTimeout(() => {
this.openOnboardingView();
@ -1448,7 +1527,7 @@ export default class TaskProgressBarPlugin extends Plugin {
* Open the onboarding view in a new leaf
*/
async openOnboardingView(): Promise<void> {
const {workspace} = this.app;
const { workspace } = this.app;
// Check if onboarding view is already open
const existingLeaf = workspace.getLeavesOfType(ONBOARDING_VIEW_TYPE)[0];
@ -1460,7 +1539,7 @@ export default class TaskProgressBarPlugin extends Plugin {
// Create a new leaf in the main area and open the onboarding view
const leaf = workspace.getLeaf("tab");
await leaf.setViewState({type: ONBOARDING_VIEW_TYPE});
await leaf.setViewState({ type: ONBOARDING_VIEW_TYPE });
workspace.revealLeaf(leaf);
}
@ -1517,10 +1596,10 @@ export default class TaskProgressBarPlugin extends Plugin {
// Add any missing default views to user settings
defaultViews.forEach((defaultView) => {
const existingView = this.settings.viewConfiguration.find(
(v) => v.id === defaultView.id
(v) => v.id === defaultView.id,
);
if (!existingView) {
this.settings.viewConfiguration.push({...defaultView});
this.settings.viewConfiguration.push({ ...defaultView });
}
});
@ -1530,7 +1609,7 @@ export default class TaskProgressBarPlugin extends Plugin {
// Helper method to set priority at cursor position
async activateTaskView() {
const {workspace} = this.app;
const { workspace } = this.app;
// Check if view is already open
const existingLeaf = workspace.getLeavesOfType(TASK_VIEW_TYPE)[0];
@ -1543,16 +1622,16 @@ export default class TaskProgressBarPlugin extends Plugin {
// Otherwise, create a new leaf in the right split and open the view
const leaf = workspace.getLeaf("tab");
await leaf.setViewState({type: TASK_VIEW_TYPE});
await leaf.setViewState({ type: TASK_VIEW_TYPE });
workspace.revealLeaf(leaf);
}
async activateTimelineSidebarView() {
const {workspace} = this.app;
const { workspace } = this.app;
// Check if view is already open
const existingLeaf = workspace.getLeavesOfType(
TIMELINE_SIDEBAR_VIEW_TYPE
TIMELINE_SIDEBAR_VIEW_TYPE,
)[0];
if (existingLeaf) {
@ -1564,7 +1643,7 @@ export default class TaskProgressBarPlugin extends Plugin {
// Open in the right sidebar
const leaf = workspace.getRightLeaf(false);
if (leaf) {
await leaf.setViewState({type: TIMELINE_SIDEBAR_VIEW_TYPE});
await leaf.setViewState({ type: TIMELINE_SIDEBAR_VIEW_TYPE });
workspace.revealLeaf(leaf);
}
}
@ -1584,7 +1663,7 @@ export default class TaskProgressBarPlugin extends Plugin {
// Update Timeline Sidebar Views
const timelineViewLeaves = this.app.workspace.getLeavesOfType(
TIMELINE_SIDEBAR_VIEW_TYPE
TIMELINE_SIDEBAR_VIEW_TYPE,
);
if (timelineViewLeaves.length > 0) {
for (const leaf of timelineViewLeaves) {
@ -1617,7 +1696,7 @@ export default class TaskProgressBarPlugin extends Plugin {
if (!diagnosticInfo.canWrite) {
throw new Error(
"Cannot write to version storage - storage may be corrupted"
"Cannot write to version storage - storage may be corrupted",
);
}
@ -1626,7 +1705,7 @@ export default class TaskProgressBarPlugin extends Plugin {
diagnosticInfo.previousVersion
) {
console.warn(
"Invalid version data detected, attempting recovery"
"Invalid version data detected, attempting recovery",
);
await this.versionManager.recoverFromCorruptedVersion();
}
@ -1644,13 +1723,13 @@ export default class TaskProgressBarPlugin extends Plugin {
.filter(
(file) =>
file.extension === "md" ||
file.extension === "canvas"
file.extension === "canvas",
);
// Start rebuild progress tracking
this.rebuildProgressManager.startRebuild(
allFiles.length,
versionResult.rebuildReason
versionResult.rebuildReason,
);
// Force clear all caches before rebuild
@ -1660,7 +1739,7 @@ export default class TaskProgressBarPlugin extends Plugin {
} catch (clearError) {
console.warn(
"Error clearing cache, attempting to recreate storage:",
clearError
clearError,
);
await this.taskManager.persister.recreate();
}
@ -1668,7 +1747,7 @@ export default class TaskProgressBarPlugin extends Plugin {
// Set progress manager for the task manager
this.taskManager.setProgressManager(
this.rebuildProgressManager
this.rebuildProgressManager,
);
// Initialize task manager (this will trigger the rebuild)
@ -1692,19 +1771,19 @@ export default class TaskProgressBarPlugin extends Plugin {
retryCount++;
console.error(
`Error during task manager initialization (attempt ${retryCount}/${maxRetries}):`,
error
error,
);
if (retryCount >= maxRetries) {
// Final attempt failed, trigger emergency rebuild
console.error(
"All initialization attempts failed, triggering emergency rebuild"
"All initialization attempts failed, triggering emergency rebuild",
);
try {
const emergencyResult =
await this.versionManager.handleEmergencyRebuild(
`Initialization failed after ${maxRetries} attempts: ${error.message}`
`Initialization failed after ${maxRetries} attempts: ${error.message}`,
);
// Get all supported files for progress tracking
@ -1713,13 +1792,13 @@ export default class TaskProgressBarPlugin extends Plugin {
.filter(
(file) =>
file.extension === "md" ||
file.extension === "canvas"
file.extension === "canvas",
);
// Start emergency rebuild
this.rebuildProgressManager.startRebuild(
allFiles.length,
emergencyResult.rebuildReason
emergencyResult.rebuildReason,
);
// Force recreate storage
@ -1729,7 +1808,7 @@ export default class TaskProgressBarPlugin extends Plugin {
// Set progress manager for the task manager
this.taskManager.setProgressManager(
this.rebuildProgressManager
this.rebuildProgressManager,
);
// Initialize with minimal error handling
@ -1739,7 +1818,7 @@ export default class TaskProgressBarPlugin extends Plugin {
const finalTaskCount =
this.taskManager.getAllTasks().length;
this.rebuildProgressManager.completeRebuild(
finalTaskCount
finalTaskCount,
);
// Store current version
@ -1750,19 +1829,19 @@ export default class TaskProgressBarPlugin extends Plugin {
} catch (emergencyError) {
console.error(
"Emergency rebuild also failed:",
emergencyError
emergencyError,
);
this.rebuildProgressManager.failRebuild(
`Emergency rebuild failed: ${emergencyError.message}`
`Emergency rebuild failed: ${emergencyError.message}`,
);
throw new Error(
`Task manager initialization failed completely: ${emergencyError.message}`
`Task manager initialization failed completely: ${emergencyError.message}`,
);
}
} else {
// Wait before retry
await new Promise((resolve) =>
setTimeout(resolve, 1000 * retryCount)
setTimeout(resolve, 1000 * retryCount),
);
}
}
@ -1773,7 +1852,7 @@ export default class TaskProgressBarPlugin extends Plugin {
function setPriorityAtCursor(editor: Editor, priority: string) {
const cursor = editor.getCursor();
const line = editor.getLine(cursor.line);
const lineStart = editor.posToOffset({line: cursor.line, ch: 0});
const lineStart = editor.posToOffset({ line: cursor.line, ch: 0 });
// Check if this line has a task
const taskRegex =
@ -1820,7 +1899,7 @@ function setPriorityAtCursor(editor: Editor, priority: string) {
function removePriorityAtCursor(editor: Editor) {
const cursor = editor.getCursor();
const line = editor.getLine(cursor.line);
const lineStart = editor.posToOffset({line: cursor.line, ch: 0});
const lineStart = editor.posToOffset({ line: cursor.line, ch: 0 });
// Check if this line has a task with priority
const priorityRegex = /(?:🔺|⏫|🔼|🔽|⏬️|\[#[A-C]\])/;

View file

@ -21,6 +21,7 @@ import { HolidayDetector } from "../parsers/holiday-detector";
import { StatusMapper } from "../parsers/ics-status-mapper";
import { WebcalUrlConverter } from "../parsers/webcal-converter";
import { TaskProgressBarSettings } from "../common/setting-definition";
import TaskProgressBarPlugin from "src";
export class IcsManager extends Component {
private config: IcsManagerConfig;
@ -30,13 +31,17 @@ export class IcsManager extends Component {
private onEventsUpdated?: (sourceId: string, events: IcsEvent[]) => void;
private pluginSettings: TaskProgressBarSettings;
private plugin: TaskProgressBarPlugin;
constructor(
config: IcsManagerConfig,
pluginSettings: TaskProgressBarSettings
pluginSettings: TaskProgressBarSettings,
plugin: TaskProgressBarPlugin,
) {
super();
this.config = config;
this.pluginSettings = pluginSettings;
this.plugin = plugin;
}
/**
@ -57,6 +62,18 @@ export class IcsManager extends Component {
}
console.log("ICS Manager initialized");
// Notify listeners (e.g., IcsSource) that ICS is ready/config updated
// try {
// this.plugin.app?.workspace?.trigger?.(
// "task-genius:ics-config-changed",
// );
// } catch (e) {
// console.warn(
// "[IcsManager] Failed to trigger ics-config-changed on initialize",
// e,
// );
// }
}
/**
@ -92,13 +109,24 @@ export class IcsManager extends Component {
} else {
this.stopBackgroundRefresh();
}
// try {
// this.plugin.app?.workspace?.trigger?.(
// "task-genius:ics-config-changed",
// );
// } catch (e) {
// console.warn(
// "[IcsManager] Failed to trigger ics-config-changed",
// e,
// );
// }
}
/**
* Set event update callback
*/
setOnEventsUpdated(
callback: (sourceId: string, events: IcsEvent[]) => void
callback: (sourceId: string, events: IcsEvent[]) => void,
): void {
this.onEventsUpdated = callback;
}
@ -129,7 +157,7 @@ export class IcsManager extends Component {
// Apply filters if configured
const filteredEvents = this.applyFilters(
cacheEntry.events,
source
source,
);
console.log("filteredEvents count", filteredEvents.length);
allEvents.push(...filteredEvents);
@ -150,11 +178,11 @@ export class IcsManager extends Component {
console.log(
"getAllEventsWithHolidayDetection: cache size",
this.cache.size
this.cache.size,
);
console.log(
"getAllEventsWithHolidayDetection: config sources",
this.config.sources
this.config.sources,
);
for (const [sourceId, cacheEntry] of this.cache) {
@ -164,7 +192,7 @@ export class IcsManager extends Component {
"Processing source:",
sourceId,
"enabled:",
source?.enabled
source?.enabled,
);
console.log("Cache entry events count:", cacheEntry.events.length);
@ -172,7 +200,7 @@ export class IcsManager extends Component {
// Apply filters first
const filteredEvents = this.applyFilters(
cacheEntry.events,
source
source,
);
console.log("Filtered events count:", filteredEvents.length);
@ -183,7 +211,7 @@ export class IcsManager extends Component {
processedEvents =
HolidayDetector.processEventsWithHolidayDetection(
filteredEvents,
source.holidayConfig
source.holidayConfig,
);
} else {
// Convert to IcsEventWithHoliday format without holiday detection
@ -201,7 +229,7 @@ export class IcsManager extends Component {
console.log(
"getAllEventsWithHolidayDetection: total events",
allEvents.length
allEvents.length,
);
return allEvents;
}
@ -324,7 +352,7 @@ export class IcsManager extends Component {
const mappedStatus = StatusMapper.applyStatusMapping(
event,
event.source.statusMapping,
this.pluginSettings
this.pluginSettings,
);
const task: IcsTask = {
@ -371,7 +399,7 @@ export class IcsManager extends Component {
* Convert single ICS event with holiday detection to Task format
*/
private convertEventWithHolidayToTask(
event: IcsEventWithHoliday
event: IcsEventWithHoliday,
): Task<ExtendedMetadata> & {
icsEvent: IcsEvent;
readonly: true;
@ -394,7 +422,7 @@ export class IcsManager extends Component {
const mappedStatus = StatusMapper.applyStatusMapping(
event,
event.source.statusMapping,
this.pluginSettings
this.pluginSettings,
);
const task: IcsTask = {
@ -458,7 +486,7 @@ export class IcsManager extends Component {
* Map ICS priority to task priority
*/
private mapIcsPriorityToTaskPriority(
icsPriority?: number
icsPriority?: number,
): number | undefined {
if (icsPriority === undefined) return undefined;
@ -507,12 +535,24 @@ export class IcsManager extends Component {
// Notify listeners
this.onEventsUpdated?.(sourceId, result.data.events);
// Broadcast workspace event so IcsSource can reload
// try {
// this.plugin.app?.workspace?.trigger?.(
// "task-genius:ics-cache-updated",
// );
// } catch (e) {
// console.warn(
// "[IcsManager] Failed to trigger ics-cache-updated",
// e,
// );
// }
} else {
// Handle different types of errors with appropriate logging
const errorType = this.categorizeError(result.error);
console.warn(
`ICS sync failed for source ${sourceId} (${errorType}):`,
result.error
result.error,
);
this.updateSyncStatus(sourceId, {
@ -529,7 +569,7 @@ export class IcsManager extends Component {
console.warn(
`ICS sync exception for source ${sourceId} (${errorType}):`,
error
error,
);
this.updateSyncStatus(sourceId, {
@ -598,7 +638,7 @@ export class IcsManager extends Component {
try {
// Convert webcal URL if needed
const conversionResult = WebcalUrlConverter.convertWebcalUrl(
source.url
source.url,
);
if (!conversionResult.success) {
@ -626,18 +666,16 @@ export class IcsManager extends Component {
case "basic":
if (source.auth.username && source.auth.password) {
const credentials = btoa(
`${source.auth.username}:${source.auth.password}`
`${source.auth.username}:${source.auth.password}`,
);
requestParams.headers![
"Authorization"
] = `Basic ${credentials}`;
requestParams.headers!["Authorization"] =
`Basic ${credentials}`;
}
break;
case "bearer":
if (source.auth.token) {
requestParams.headers![
"Authorization"
] = `Bearer ${source.auth.token}`;
requestParams.headers!["Authorization"] =
`Bearer ${source.auth.token}`;
}
break;
}
@ -659,8 +697,8 @@ export class IcsManager extends Component {
setTimeout(() => {
reject(
new Error(
`Request timeout after ${this.config.networkTimeout} seconds`
)
`Request timeout after ${this.config.networkTimeout} seconds`,
),
);
}, timeoutMs);
});
@ -735,14 +773,14 @@ export class IcsManager extends Component {
const beforeFilter = filteredEvents.length;
filteredEvents = filteredEvents.filter((event) => !event.allDay);
console.log(
`Filtered out all-day events: ${beforeFilter} -> ${filteredEvents.length}`
`Filtered out all-day events: ${beforeFilter} -> ${filteredEvents.length}`,
);
}
if (!source.showTimedEvents) {
const beforeFilter = filteredEvents.length;
filteredEvents = filteredEvents.filter((event) => event.allDay);
console.log(
`Filtered out timed events: ${beforeFilter} -> ${filteredEvents.length}`
`Filtered out timed events: ${beforeFilter} -> ${filteredEvents.length}`,
);
}
@ -758,28 +796,31 @@ export class IcsManager extends Component {
shouldInclude =
shouldInclude &&
include.summary.some((pattern) =>
this.matchesPattern(event.summary, pattern)
this.matchesPattern(event.summary, pattern),
);
}
if (include.description?.length && event.description) {
shouldInclude =
shouldInclude &&
include.description.some((pattern) =>
this.matchesPattern(event.description!, pattern)
this.matchesPattern(
event.description!,
pattern,
),
);
}
if (include.location?.length && event.location) {
shouldInclude =
shouldInclude &&
include.location.some((pattern) =>
this.matchesPattern(event.location!, pattern)
this.matchesPattern(event.location!, pattern),
);
}
if (include.categories?.length && event.categories) {
shouldInclude =
shouldInclude &&
include.categories.some((category) =>
event.categories!.includes(category)
event.categories!.includes(category),
);
}
@ -793,7 +834,7 @@ export class IcsManager extends Component {
if (exclude.summary?.length) {
if (
exclude.summary.some((pattern) =>
this.matchesPattern(event.summary, pattern)
this.matchesPattern(event.summary, pattern),
)
) {
return false;
@ -802,7 +843,10 @@ export class IcsManager extends Component {
if (exclude.description?.length && event.description) {
if (
exclude.description.some((pattern) =>
this.matchesPattern(event.description!, pattern)
this.matchesPattern(
event.description!,
pattern,
),
)
) {
return false;
@ -811,7 +855,7 @@ export class IcsManager extends Component {
if (exclude.location?.length && event.location) {
if (
exclude.location.some((pattern) =>
this.matchesPattern(event.location!, pattern)
this.matchesPattern(event.location!, pattern),
)
) {
return false;
@ -820,7 +864,7 @@ export class IcsManager extends Component {
if (exclude.categories?.length && event.categories) {
if (
exclude.categories.some((category) =>
event.categories!.includes(category)
event.categories!.includes(category),
)
) {
return false;
@ -839,7 +883,7 @@ export class IcsManager extends Component {
.sort((a, b) => b.dtstart.getTime() - a.dtstart.getTime()) // 倒序:最新的事件在前
.slice(0, this.config.maxEventsPerSource);
console.log(
`Limited events: ${beforeLimit} -> ${filteredEvents.length} (max: ${this.config.maxEventsPerSource}) - keeping newest events`
`Limited events: ${beforeLimit} -> ${filteredEvents.length} (max: ${this.config.maxEventsPerSource}) - keeping newest events`,
);
}
@ -899,14 +943,14 @@ export class IcsManager extends Component {
case "summary":
processedSummary = processedSummary.replace(
regex,
rule.replacement
rule.replacement,
);
break;
case "description":
if (processedDescription) {
processedDescription = processedDescription.replace(
regex,
rule.replacement
rule.replacement,
);
}
break;
@ -914,25 +958,25 @@ export class IcsManager extends Component {
if (processedLocation) {
processedLocation = processedLocation.replace(
regex,
rule.replacement
rule.replacement,
);
}
break;
case "all":
processedSummary = processedSummary.replace(
regex,
rule.replacement
rule.replacement,
);
if (processedDescription) {
processedDescription = processedDescription.replace(
regex,
rule.replacement
rule.replacement,
);
}
if (processedLocation) {
processedLocation = processedLocation.replace(
regex,
rule.replacement
rule.replacement,
);
}
break;
@ -940,7 +984,7 @@ export class IcsManager extends Component {
} catch (error) {
console.warn(
`Invalid regex pattern in text replacement rule "${rule.name}": ${rule.pattern}`,
error
error,
);
}
}
@ -957,7 +1001,7 @@ export class IcsManager extends Component {
*/
private updateSyncStatus(
sourceId: string,
updates: Partial<IcsSyncStatus>
updates: Partial<IcsSyncStatus>,
): void {
const current = this.syncStatuses.get(sourceId) || {
sourceId,
@ -1021,14 +1065,17 @@ export class IcsManager extends Component {
if (source.enabled) {
const interval =
source.refreshInterval || this.config.globalRefreshInterval;
const intervalId = setInterval(() => {
this.syncSource(source.id).catch((error) => {
console.error(
`Background sync failed for source ${source.id}:`,
error
);
});
}, interval * 60 * 1000); // Convert minutes to milliseconds
const intervalId = setInterval(
() => {
this.syncSource(source.id).catch((error) => {
console.error(
`Background sync failed for source ${source.id}:`,
error,
);
});
},
interval * 60 * 1000,
); // Convert minutes to milliseconds
this.refreshIntervals.set(source.id, intervalId as any);
}

View file

@ -68,7 +68,7 @@ export class TaskManager extends Component {
/** Options for the task manager */
private options: TaskManagerOptions;
/** Whether the manager has been initialized */
private initialized: boolean = false;
private initialized: boolean = false;
/** Whether initialization is currently in progress */
private isInitializing: boolean = false;
/** Whether we should trigger update events after initialization */

View file

@ -144,8 +144,9 @@ export class TaskView extends ItemView {
// Add debounced view update to prevent rapid successive refreshes
const debouncedViewUpdate = debounce(async () => {
// Don't skip view updates - the detailsComponent will handle edit state properly
await this.loadTasks(false, false);
// For external/editor updates, force a view refresh to avoid false "unchanged" skips
await this.loadTasks(false, true); // skip internal triggerViewUpdate
this.switchView(this.currentViewId, undefined, true); // forceRefresh
}, 150); // 150ms debounce delay
// 1. 首先注册事件监听器,确保不会错过任何更新

View file

@ -1,3 +0,0 @@
// Legacy re-export shim for tests and old imports
export { MarkdownTaskParser } from "../dataflow/core/ConfigurableTaskParser";

View file

@ -471,27 +471,22 @@ export function filterTasks(
const filterRules = viewConfig.filterRules || {};
const globalFilterRules = plugin.settings.globalFilterRules || {};
// --- 过滤 badge 类型的 ICS 任务(仅在非日历视图中) ---
// Badge 任务只应该在日历视图中显示,其他视图应该过滤掉
// 检查是否为日历相关的视图ID
// --- 过滤 ICS 事件在不应展示的视图中(例如 inbox---
// ICS 事件应仅在日历/日程类视图calendar/forecast中展示
const isCalendarView =
viewId === "calendar" ||
(typeof viewId === "string" && viewId.startsWith("calendar"));
const isForecastView =
viewId === "forecast" ||
(typeof viewId === "string" && viewId.startsWith("forecast"));
if (!isCalendarView) {
if (!isCalendarView && !isForecastView) {
filtered = filtered.filter((task) => {
// 检查是否为 ICS 任务
const isIcsTask = (task as any).source?.type === "ics";
if (!isIcsTask) {
return true; // 非 ICS 任务保留
}
// 检查是否为 badge 类型
const icsTask = task as any; // 类型断言为包含 icsEvent 的任务
const showAsBadge = icsTask?.icsEvent?.source?.showType === "badge";
// 如果是 badge 类型的 ICS 任务,则过滤掉(返回 false
return !showAsBadge;
// 识别 ICS 事件任务(优先从 metadata.source 读取,兼容 legacy source 字段)
const metaSourceType = (task as any).metadata?.source?.type ?? (task as any).source?.type;
const isIcsTask = metaSourceType === "ics";
// 非 ICS 保留ICS 在此类视图中过滤掉
return !isIcsTask;
});
}