mirror of
https://github.com/taskgenius/taskgenius-plugin.git
synced 2026-07-22 06:40:25 +00:00
refactor(dataflow): optimize single task updates and cache invalidation
- Make WriteAPI getTaskById async-compatible for better integration - Add direct single task update path to avoid full file re-parsing - Implement Repository.updateSingleTask() for efficient inline edits - Force cache invalidation for WriteAPI operations to ensure consistency - Emit TASK_UPDATED events for immediate dataflow updates - Improve task update performance by bypassing unnecessary file parsing - Fix tag formatting to prevent duplicate # prefixes in WriteAPI This change significantly improves performance for inline task editing by updating only the modified task instead of re-parsing entire files.
This commit is contained in:
parent
771d9f73b1
commit
0c6db2537b
12 changed files with 407 additions and 168 deletions
|
|
@ -1060,6 +1060,7 @@ export class InlineEditor extends Component {
|
|||
|
||||
this.isSaving = true;
|
||||
try {
|
||||
console.log("[InlineEditor] Calling onTaskUpdate:", this.originalTask.content, "->", this.task.content);
|
||||
await this.options.onTaskUpdate(this.originalTask, this.task);
|
||||
this.originalTask = {
|
||||
...this.task,
|
||||
|
|
|
|||
|
|
@ -54,6 +54,7 @@ export class ContentComponent extends Component {
|
|||
private focusFilter: string | null = null; // Keep focus filter if needed
|
||||
private isTreeView: boolean = false;
|
||||
private isRendering: boolean = false; // Guard against concurrent renders
|
||||
private pendingForceRefresh: boolean = false; // Track if a force refresh is pending
|
||||
|
||||
constructor(
|
||||
private parentEl: HTMLElement,
|
||||
|
|
@ -176,18 +177,44 @@ export class ContentComponent extends Component {
|
|||
this.refreshTaskList(); // Refresh list completely on view mode change
|
||||
}
|
||||
|
||||
public setTasks(tasks: Task[], notFilteredTasks: Task[]) {
|
||||
public setTasks(tasks: Task[], notFilteredTasks: Task[], forceRefresh: boolean = false) {
|
||||
// Allow forced refresh for cases where we know the data has changed
|
||||
if (forceRefresh) {
|
||||
console.log("ContentComponent: Forced refresh requested");
|
||||
// Cancel any ongoing rendering if force refresh is requested
|
||||
this.isRendering = false;
|
||||
this.pendingForceRefresh = true;
|
||||
this.allTasks = tasks;
|
||||
this.notFilteredTasks = notFilteredTasks;
|
||||
this.applyFilters();
|
||||
this.refreshTaskList();
|
||||
return;
|
||||
}
|
||||
|
||||
// If a force refresh is pending, skip non-forced updates
|
||||
if (this.pendingForceRefresh) {
|
||||
console.log("ContentComponent: Skipping non-forced update, force refresh is pending");
|
||||
return;
|
||||
}
|
||||
|
||||
// Prevent unnecessary refreshes if data hasn't actually changed
|
||||
if (this.allTasks.length === tasks.length &&
|
||||
// Check if the array reference has changed (which indicates an update)
|
||||
if (this.allTasks === tasks &&
|
||||
this.notFilteredTasks === notFilteredTasks) {
|
||||
console.log("ContentComponent: Same array references, skipping refresh");
|
||||
return;
|
||||
}
|
||||
|
||||
// Additional check for actual content changes
|
||||
if (this.allTasks.length === tasks.length &&
|
||||
this.notFilteredTasks.length === notFilteredTasks.length &&
|
||||
tasks.length > 0) {
|
||||
// Quick check - if same length and not empty, check if first few tasks are identical
|
||||
const sampleSize = Math.min(3, tasks.length);
|
||||
const sampleSize = Math.min(5, tasks.length);
|
||||
let unchanged = true;
|
||||
for (let i = 0; i < sampleSize; i++) {
|
||||
if (this.allTasks[i]?.id !== tasks[i]?.id ||
|
||||
this.allTasks[i]?.content !== tasks[i]?.content ||
|
||||
this.allTasks[i]?.status !== tasks[i]?.status) {
|
||||
if (this.allTasks[i]?.id !== tasks[i]?.id ||
|
||||
this.allTasks[i]?.originalMarkdown !== tasks[i]?.originalMarkdown) {
|
||||
unchanged = false;
|
||||
break;
|
||||
}
|
||||
|
|
@ -288,7 +315,13 @@ export class ContentComponent extends Component {
|
|||
}
|
||||
|
||||
private refreshTaskList() {
|
||||
// Prevent concurrent renders
|
||||
// Allow force refresh to override concurrent rendering
|
||||
if (this.pendingForceRefresh) {
|
||||
console.log("ContentComponent: Processing pending force refresh");
|
||||
this.isRendering = false; // Cancel any ongoing render
|
||||
}
|
||||
|
||||
// Prevent concurrent renders (unless force refresh)
|
||||
if (this.isRendering) {
|
||||
console.log("ContentComponent: Already rendering, skipping refresh");
|
||||
return;
|
||||
|
|
@ -326,6 +359,7 @@ export class ContentComponent extends Component {
|
|||
// Reset rendering flag after completion
|
||||
setTimeout(() => {
|
||||
this.isRendering = false;
|
||||
this.pendingForceRefresh = false; // Reset force refresh flag
|
||||
}, 50); // Small delay to prevent immediate re-entry
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -386,8 +386,13 @@ export class TaskDetailsComponent extends Component {
|
|||
const tagsField = this.createFormField(this.editFormEl, t("Tags"));
|
||||
const tagsInput = new TextComponent(tagsField);
|
||||
console.log("tagsInput", tagsInput, task.metadata.tags);
|
||||
// Remove # prefix from tags when displaying them
|
||||
tagsInput.setValue(
|
||||
task.metadata.tags ? task.metadata.tags.join(", ") : ""
|
||||
task.metadata.tags
|
||||
? task.metadata.tags
|
||||
.map(tag => tag.startsWith("#") ? tag.slice(1) : tag)
|
||||
.join(", ")
|
||||
: ""
|
||||
);
|
||||
tagsField
|
||||
.createSpan({ cls: "field-description" })
|
||||
|
|
@ -526,12 +531,13 @@ export class TaskDetailsComponent extends Component {
|
|||
metadata.project = task.metadata.project;
|
||||
}
|
||||
|
||||
// Parse and update tags
|
||||
// Parse and update tags (remove # prefix if present)
|
||||
const tagsValue = tagsInput.getValue();
|
||||
metadata.tags = tagsValue
|
||||
? tagsValue
|
||||
.split(",")
|
||||
.map((tag) => tag.trim())
|
||||
.map((tag) => tag.startsWith("#") ? tag.slice(1) : tag) // Remove # prefix if present
|
||||
.filter((tag) => tag)
|
||||
: [];
|
||||
|
||||
|
|
@ -667,6 +673,11 @@ export class TaskDetailsComponent extends Component {
|
|||
|
||||
// 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);
|
||||
} catch (error) {
|
||||
console.error("Failed to update task:", error);
|
||||
|
|
|
|||
|
|
@ -230,14 +230,31 @@ export class DataflowOrchestrator {
|
|||
// Listen for WriteAPI completion events to trigger re-processing
|
||||
this.eventRefs.push(
|
||||
on(this.app, Events.WRITE_OPERATION_COMPLETE, async (payload: any) => {
|
||||
const { path } = payload;
|
||||
console.log(`[DataflowOrchestrator] WRITE_OPERATION_COMPLETE: ${path}`);
|
||||
const { path, taskId } = payload;
|
||||
console.log(`[DataflowOrchestrator] WRITE_OPERATION_COMPLETE: ${path}, taskId: ${taskId}`);
|
||||
|
||||
// Process the file after WriteAPI completes
|
||||
const file = this.vault.getAbstractFileByPath(path) as TFile;
|
||||
if (file) {
|
||||
// Process immediately without debounce for WriteAPI operations
|
||||
await this.processFileImmediate(file);
|
||||
// If we have a taskId, it means a specific task was updated
|
||||
// We'll handle this through TASK_UPDATED event instead
|
||||
if (!taskId) {
|
||||
// No specific task, process the entire file
|
||||
const file = this.vault.getAbstractFileByPath(path) as TFile;
|
||||
if (file) {
|
||||
// Process immediately without debounce for WriteAPI operations
|
||||
// Pass true to force cache invalidation
|
||||
await this.processFileImmediate(file, true);
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
// Listen for direct task updates (from inline editing)
|
||||
this.eventRefs.push(
|
||||
on(this.app, Events.TASK_UPDATED, async (payload: any) => {
|
||||
const { task } = payload;
|
||||
if (task) {
|
||||
console.log(`[DataflowOrchestrator] TASK_UPDATED: ${task.id} in ${task.filePath}`);
|
||||
// Update the single task directly in the repository
|
||||
await this.repository.updateSingleTask(task);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
|
@ -280,7 +297,7 @@ export class DataflowOrchestrator {
|
|||
|
||||
const timeoutId = setTimeout(async () => {
|
||||
this.processingQueue.delete(filePath);
|
||||
await this.processFileImmediate(file);
|
||||
await this.processFileImmediate(file, false);
|
||||
}, this.DEBOUNCE_DELAY);
|
||||
|
||||
this.processingQueue.set(filePath, timeoutId);
|
||||
|
|
@ -288,8 +305,10 @@ export class DataflowOrchestrator {
|
|||
|
||||
/**
|
||||
* Process a file immediately without debouncing
|
||||
* @param file The file to process
|
||||
* @param forceInvalidate Force cache invalidation (for WriteAPI operations)
|
||||
*/
|
||||
private async processFileImmediate(file: TFile): Promise<void> {
|
||||
private async processFileImmediate(file: TFile, forceInvalidate: boolean = false): Promise<void> {
|
||||
const filePath = file.path;
|
||||
|
||||
try {
|
||||
|
|
@ -306,7 +325,8 @@ export class DataflowOrchestrator {
|
|||
let needsProcessing = false;
|
||||
|
||||
// Check if we can use fully cached augmented tasks
|
||||
if (rawCached && augmentedCached &&
|
||||
// Force invalidation for WriteAPI operations to ensure fresh parsing
|
||||
if (!forceInvalidate && rawCached && augmentedCached &&
|
||||
this.storage.isRawValid(filePath, rawCached, fileContent, mtime)) {
|
||||
// Use cached augmented tasks - file hasn't changed and we have augmented data
|
||||
console.log(`[DataflowOrchestrator] Using cached augmented tasks for ${filePath} (mtime match)`);
|
||||
|
|
@ -318,14 +338,18 @@ export class DataflowOrchestrator {
|
|||
let rawTasks: Task[];
|
||||
let projectData: any; // Type will be inferred from projectResolver.get
|
||||
|
||||
if (rawCached && this.storage.isRawValid(filePath, rawCached, fileContent, mtime)) {
|
||||
if (!forceInvalidate && rawCached && this.storage.isRawValid(filePath, rawCached, fileContent, mtime)) {
|
||||
// Use cached raw tasks but re-augment (project data might have changed)
|
||||
console.log(`[DataflowOrchestrator] Re-augmenting cached raw tasks for ${filePath}`);
|
||||
rawTasks = rawCached.data;
|
||||
projectData = await this.projectResolver.get(filePath);
|
||||
} else {
|
||||
// Parse the file from scratch
|
||||
console.log(`[DataflowOrchestrator] Parsing ${filePath} (cache miss or mtime mismatch)`);
|
||||
if (forceInvalidate) {
|
||||
console.log(`[DataflowOrchestrator] Parsing ${filePath} (forced invalidation from WriteAPI)`);
|
||||
} else {
|
||||
console.log(`[DataflowOrchestrator] Parsing ${filePath} (cache miss or mtime mismatch)`);
|
||||
}
|
||||
|
||||
// Get project data first for parsing
|
||||
projectData = await this.projectResolver.get(filePath);
|
||||
|
|
@ -736,6 +760,13 @@ export class DataflowOrchestrator {
|
|||
return this.queryAPI;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the repository for direct access
|
||||
*/
|
||||
getRepository(): Repository {
|
||||
return this.repository;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get statistics about the dataflow system
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -78,7 +78,7 @@ export class WriteAPI {
|
|||
private vault: Vault,
|
||||
private metadataCache: MetadataCache,
|
||||
private plugin: TaskProgressBarPlugin,
|
||||
private getTaskById: (id: string) => Task | null
|
||||
private getTaskById: (id: string) => Promise<Task | null> | Task | null
|
||||
) {}
|
||||
|
||||
/**
|
||||
|
|
@ -90,7 +90,7 @@ export class WriteAPI {
|
|||
completed?: boolean;
|
||||
}): Promise<{ success: boolean; task?: Task; error?: string }> {
|
||||
try {
|
||||
const task = this.getTaskById(args.taskId);
|
||||
const task = await Promise.resolve(this.getTaskById(args.taskId));
|
||||
if (!task) {
|
||||
return { success: false, error: "Task not found" };
|
||||
}
|
||||
|
|
@ -152,7 +152,7 @@ export class WriteAPI {
|
|||
*/
|
||||
async updateTask(args: UpdateTaskArgs): Promise<{ success: boolean; task?: Task; error?: string }> {
|
||||
try {
|
||||
const originalTask = this.getTaskById(args.taskId);
|
||||
const originalTask = await Promise.resolve(this.getTaskById(args.taskId));
|
||||
if (!originalTask) {
|
||||
return { success: false, error: "Task not found" };
|
||||
}
|
||||
|
|
@ -213,9 +213,21 @@ export class WriteAPI {
|
|||
// Notify about write operation
|
||||
emit(this.app, Events.WRITE_OPERATION_START, { path: file.path, taskId: args.taskId });
|
||||
await this.vault.modify(file, lines.join("\n"));
|
||||
|
||||
// Create the updated task object with the new content
|
||||
const updatedTaskObj: Task = {
|
||||
...originalTask,
|
||||
...args.updates,
|
||||
originalMarkdown: taskLine.replace(/^\s*[-*+]\s*\[[^\]]*\]\s*/, ""), // Remove checkbox prefix
|
||||
};
|
||||
|
||||
// Emit task updated event for direct update in dataflow
|
||||
emit(this.app, Events.TASK_UPDATED, { task: updatedTaskObj });
|
||||
|
||||
// Still emit write operation complete for compatibility
|
||||
emit(this.app, Events.WRITE_OPERATION_COMPLETE, { path: file.path, taskId: args.taskId });
|
||||
|
||||
return { success: true };
|
||||
return { success: true, task: updatedTaskObj };
|
||||
} catch (error) {
|
||||
console.error("WriteAPI: Error updating task:", error);
|
||||
return { success: false, error: String(error) };
|
||||
|
|
@ -295,7 +307,7 @@ export class WriteAPI {
|
|||
*/
|
||||
async deleteTask(args: DeleteTaskArgs): Promise<{ success: boolean; error?: string }> {
|
||||
try {
|
||||
const task = this.getTaskById(args.taskId);
|
||||
const task = await Promise.resolve(this.getTaskById(args.taskId));
|
||||
if (!task) {
|
||||
return { success: false, error: "Task not found" };
|
||||
}
|
||||
|
|
@ -398,7 +410,7 @@ export class WriteAPI {
|
|||
const updatedTasks: Task[] = [];
|
||||
|
||||
for (const taskId of args.taskIds) {
|
||||
const task = this.getTaskById(taskId);
|
||||
const task = await Promise.resolve(this.getTaskById(taskId));
|
||||
if (!task) continue;
|
||||
|
||||
const newContent = task.content.replace(args.findText, args.replaceText);
|
||||
|
|
@ -408,7 +420,7 @@ export class WriteAPI {
|
|||
});
|
||||
|
||||
if (result.success) {
|
||||
const updatedTask = this.getTaskById(taskId);
|
||||
const updatedTask = await Promise.resolve(this.getTaskById(taskId));
|
||||
if (updatedTask) {
|
||||
updatedTasks.push(updatedTask);
|
||||
}
|
||||
|
|
@ -422,7 +434,7 @@ export class WriteAPI {
|
|||
* Batch create subtasks
|
||||
*/
|
||||
async batchCreateSubtasks(args: BatchCreateSubtasksArgs): Promise<{ tasks: Task[] }> {
|
||||
const parentTask = this.getTaskById(args.parentTaskId);
|
||||
const parentTask = await Promise.resolve(this.getTaskById(args.parentTaskId));
|
||||
if (!parentTask) {
|
||||
return { tasks: [] };
|
||||
}
|
||||
|
|
@ -635,7 +647,10 @@ export class WriteAPI {
|
|||
if (useDataviewFormat) {
|
||||
metadata.push(`[tags:: ${args.tags.join(", ")}]`);
|
||||
} else {
|
||||
metadata.push(...args.tags.map(tag => `#${tag}`));
|
||||
// Ensure tags don't already have # prefix before adding one
|
||||
metadata.push(...args.tags.map(tag =>
|
||||
tag.startsWith("#") ? tag : `#${tag}`
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -81,6 +81,17 @@ export class CanvasParser {
|
|||
canvasData: CanvasData,
|
||||
filePath: string
|
||||
): ParsedCanvasContent {
|
||||
// Check if nodes exist
|
||||
if (!canvasData || !canvasData.nodes || !Array.isArray(canvasData.nodes)) {
|
||||
console.warn(`Canvas file ${filePath} has no nodes or invalid nodes structure`);
|
||||
return {
|
||||
canvasData,
|
||||
textContent: "",
|
||||
textNodes: [],
|
||||
filePath,
|
||||
};
|
||||
}
|
||||
|
||||
// Filter text nodes
|
||||
const textNodes = canvasData.nodes.filter(
|
||||
(node): node is CanvasTextData => node.type === "text"
|
||||
|
|
|
|||
|
|
@ -38,7 +38,9 @@ export class MarkdownTaskParser {
|
|||
}
|
||||
|
||||
// Public alias for extractMetadataAndTags
|
||||
public extractMetadataAndTags(content: string): [string, Record<string, string>, string[]] {
|
||||
public extractMetadataAndTags(
|
||||
content: string,
|
||||
): [string, Record<string, string>, string[]] {
|
||||
return this.extractMetadataAndTagsInternal(content);
|
||||
}
|
||||
|
||||
|
|
@ -47,7 +49,7 @@ export class MarkdownTaskParser {
|
|||
*/
|
||||
static createWithStatusMapping(
|
||||
config: TaskParserConfig,
|
||||
statusMapping: Record<string, string>
|
||||
statusMapping: Record<string, string>,
|
||||
): MarkdownTaskParser {
|
||||
const newConfig = { ...config, statusMapping };
|
||||
return new MarkdownTaskParser(newConfig);
|
||||
|
|
@ -61,7 +63,7 @@ export class MarkdownTaskParser {
|
|||
filePath: string = "",
|
||||
fileMetadata?: Record<string, any>,
|
||||
projectConfigData?: Record<string, any>,
|
||||
tgProject?: TgProject
|
||||
tgProject?: TgProject,
|
||||
): EnhancedTask[] {
|
||||
this.reset();
|
||||
this.fileMetadata = fileMetadata;
|
||||
|
|
@ -80,7 +82,7 @@ export class MarkdownTaskParser {
|
|||
parseIteration++;
|
||||
if (parseIteration > this.config.maxParseIterations) {
|
||||
console.warn(
|
||||
"Warning: Maximum parse iterations reached, stopping to prevent infinite loop"
|
||||
"Warning: Maximum parse iterations reached, stopping to prevent infinite loop",
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
|
@ -132,7 +134,7 @@ export class MarkdownTaskParser {
|
|||
const isSubtask = parentId !== undefined;
|
||||
const inheritedMetadata = this.inheritFileMetadata(
|
||||
metadata,
|
||||
isSubtask
|
||||
isSubtask,
|
||||
);
|
||||
|
||||
// Process inherited tags and merge with task's own tags
|
||||
|
|
@ -140,7 +142,7 @@ export class MarkdownTaskParser {
|
|||
if (inheritedMetadata.tags) {
|
||||
try {
|
||||
const inheritedTags = JSON.parse(
|
||||
inheritedMetadata.tags
|
||||
inheritedMetadata.tags,
|
||||
);
|
||||
if (Array.isArray(inheritedTags)) {
|
||||
finalTags = this.mergeTags(tags, inheritedTags);
|
||||
|
|
@ -163,23 +165,15 @@ export class MarkdownTaskParser {
|
|||
? this.extractMultilineComment(
|
||||
lines,
|
||||
i + 1,
|
||||
actualSpaces
|
||||
)
|
||||
actualSpaces,
|
||||
)
|
||||
: [undefined, 0];
|
||||
|
||||
i += linesToSkip;
|
||||
|
||||
// Debug: Log priority extraction for each task
|
||||
const extractedPriority = this.extractLegacyPriority(inheritedMetadata);
|
||||
if (process.env.NODE_ENV === 'development' || true) { // Always log for debugging
|
||||
console.log('[Parser] Task priority extraction:', {
|
||||
lineNumber: i + 1,
|
||||
content: cleanedContent.substring(0, 50),
|
||||
metadataPriority: inheritedMetadata.priority,
|
||||
extractedPriority,
|
||||
filePath
|
||||
});
|
||||
}
|
||||
const extractedPriority =
|
||||
this.extractLegacyPriority(inheritedMetadata);
|
||||
|
||||
const enhancedTask: EnhancedTask = {
|
||||
id: taskId,
|
||||
|
|
@ -208,23 +202,23 @@ export class MarkdownTaskParser {
|
|||
priority: extractedPriority,
|
||||
startDate: this.extractLegacyDate(
|
||||
inheritedMetadata,
|
||||
"startDate"
|
||||
"startDate",
|
||||
),
|
||||
dueDate: this.extractLegacyDate(
|
||||
inheritedMetadata,
|
||||
"dueDate"
|
||||
"dueDate",
|
||||
),
|
||||
scheduledDate: this.extractLegacyDate(
|
||||
inheritedMetadata,
|
||||
"scheduledDate"
|
||||
"scheduledDate",
|
||||
),
|
||||
completedDate: this.extractLegacyDate(
|
||||
inheritedMetadata,
|
||||
"completedDate"
|
||||
"completedDate",
|
||||
),
|
||||
createdDate: this.extractLegacyDate(
|
||||
inheritedMetadata,
|
||||
"createdDate"
|
||||
"createdDate",
|
||||
),
|
||||
recurrence: inheritedMetadata.recurrence,
|
||||
project: inheritedMetadata.project,
|
||||
|
|
@ -233,7 +227,7 @@ export class MarkdownTaskParser {
|
|||
|
||||
if (parentId && this.tasks.length > 0) {
|
||||
const parentTask = this.tasks.find(
|
||||
(t) => t.id === parentId
|
||||
(t) => t.id === parentId,
|
||||
);
|
||||
if (parentTask) {
|
||||
parentTask.childrenIds.push(taskId);
|
||||
|
|
@ -259,14 +253,14 @@ export class MarkdownTaskParser {
|
|||
filePath: string = "",
|
||||
fileMetadata?: Record<string, any>,
|
||||
projectConfigData?: Record<string, any>,
|
||||
tgProject?: TgProject
|
||||
tgProject?: TgProject,
|
||||
): Task[] {
|
||||
const enhancedTasks = this.parse(
|
||||
input,
|
||||
filePath,
|
||||
fileMetadata,
|
||||
projectConfigData,
|
||||
tgProject
|
||||
tgProject,
|
||||
);
|
||||
return enhancedTasks.map((task) => this.convertToLegacyTask(task));
|
||||
}
|
||||
|
|
@ -291,7 +285,7 @@ export class MarkdownTaskParser {
|
|||
}
|
||||
|
||||
private extractTaskLine(
|
||||
line: string
|
||||
line: string,
|
||||
): [number, number, string, string] | null {
|
||||
const trimmed = line.trim();
|
||||
const actualSpaces = line.length - trimmed.length;
|
||||
|
|
@ -352,7 +346,7 @@ export class MarkdownTaskParser {
|
|||
}
|
||||
|
||||
private extractMetadataAndTagsInternal(
|
||||
content: string
|
||||
content: string,
|
||||
): [string, Record<string, string>, string[]] {
|
||||
const metadata: Record<string, string> = {};
|
||||
const tags: string[] = [];
|
||||
|
|
@ -375,16 +369,20 @@ export class MarkdownTaskParser {
|
|||
if (bracketMatch) {
|
||||
const [key, value, newRemaining] = bracketMatch;
|
||||
metadata[key] = value;
|
||||
|
||||
|
||||
// Debug: Log dataview metadata extraction, especially priority
|
||||
if ((process.env.NODE_ENV === 'development' || true) && key === 'priority') { // Always log for debugging
|
||||
console.log('[Parser] Dataview priority found:', {
|
||||
if (
|
||||
(process.env.NODE_ENV === "development" || true) &&
|
||||
key === "priority"
|
||||
) {
|
||||
// Always log for debugging
|
||||
console.log("[Parser] Dataview priority found:", {
|
||||
key,
|
||||
value,
|
||||
remaining: remaining.substring(0, 50)
|
||||
remaining: remaining.substring(0, 50),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
remaining = newRemaining;
|
||||
foundMatch = true;
|
||||
continue;
|
||||
|
|
@ -486,7 +484,7 @@ export class MarkdownTaskParser {
|
|||
}
|
||||
|
||||
private extractDataviewMetadata(
|
||||
content: string
|
||||
content: string,
|
||||
): [string, string, string] | null {
|
||||
const start = content.indexOf("[");
|
||||
if (start === -1) return null;
|
||||
|
|
@ -532,7 +530,7 @@ export class MarkdownTaskParser {
|
|||
}
|
||||
|
||||
private extractEmojiMetadata(
|
||||
content: string
|
||||
content: string,
|
||||
): [string, string, string, string] | null {
|
||||
// Find the earliest emoji
|
||||
let earliestEmoji: { pos: number; emoji: string; key: string } | null =
|
||||
|
|
@ -551,7 +549,7 @@ export class MarkdownTaskParser {
|
|||
|
||||
const beforeEmoji = content.substring(0, earliestEmoji.pos);
|
||||
const afterEmoji = content.substring(
|
||||
earliestEmoji.pos + earliestEmoji.emoji.length
|
||||
earliestEmoji.pos + earliestEmoji.emoji.length,
|
||||
);
|
||||
|
||||
// Extract value after emoji
|
||||
|
|
@ -565,7 +563,7 @@ export class MarkdownTaskParser {
|
|||
// Check if we encounter other emojis or special characters
|
||||
if (
|
||||
Object.keys(this.config.emojiMapping).some((e) =>
|
||||
valuePart.substring(i).startsWith(e)
|
||||
valuePart.substring(i).startsWith(e),
|
||||
) ||
|
||||
char === "["
|
||||
) {
|
||||
|
|
@ -611,14 +609,15 @@ export class MarkdownTaskParser {
|
|||
// For priority emojis, use the emoji itself or the provided value
|
||||
// This ensures we can distinguish between different priority levels
|
||||
metadataValue = value || earliestEmoji.emoji;
|
||||
|
||||
|
||||
// Debug: Log priority emoji extraction
|
||||
if (process.env.NODE_ENV === 'development' || true) { // Always log for debugging
|
||||
console.log('[Parser] Priority emoji found:', {
|
||||
if (process.env.NODE_ENV === "development" || true) {
|
||||
// Always log for debugging
|
||||
console.log("[Parser] Priority emoji found:", {
|
||||
emoji: earliestEmoji.emoji,
|
||||
value,
|
||||
metadataValue,
|
||||
position: earliestEmoji.pos
|
||||
position: earliestEmoji.pos,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
|
|
@ -629,9 +628,15 @@ export class MarkdownTaskParser {
|
|||
|
||||
// Sanitize date-like emoji values to avoid trailing context (e.g., "2025-08-15 @work")
|
||||
if (
|
||||
["dueDate", "startDate", "scheduledDate", "completedDate", "createdDate", "cancelledDate"].includes(
|
||||
earliestEmoji.key as string
|
||||
) && typeof metadataValue === "string"
|
||||
[
|
||||
"dueDate",
|
||||
"startDate",
|
||||
"scheduledDate",
|
||||
"completedDate",
|
||||
"createdDate",
|
||||
"cancelledDate",
|
||||
].includes(earliestEmoji.key as string) &&
|
||||
typeof metadataValue === "string"
|
||||
) {
|
||||
const m = metadataValue.match(/\d{4}-\d{2}-\d{2}/);
|
||||
if (m) {
|
||||
|
|
@ -881,7 +886,7 @@ export class MarkdownTaskParser {
|
|||
}
|
||||
|
||||
private extractTagsOnly(
|
||||
content: string
|
||||
content: string,
|
||||
): [string, Record<string, string>, string[]] {
|
||||
const metadata: Record<string, string> = {};
|
||||
const tags: string[] = [];
|
||||
|
|
@ -983,7 +988,7 @@ export class MarkdownTaskParser {
|
|||
}
|
||||
|
||||
private findParentAndLevel(
|
||||
actualSpaces: number
|
||||
actualSpaces: number,
|
||||
): [string | undefined, number] {
|
||||
if (this.indentStack.length === 0 || actualSpaces === 0) {
|
||||
return [undefined, 0];
|
||||
|
|
@ -1006,7 +1011,7 @@ export class MarkdownTaskParser {
|
|||
private updateIndentStack(
|
||||
taskId: string,
|
||||
indentLevel: number,
|
||||
actualSpaces: number
|
||||
actualSpaces: number,
|
||||
): void {
|
||||
let stackOperations = 0;
|
||||
|
||||
|
|
@ -1014,7 +1019,7 @@ export class MarkdownTaskParser {
|
|||
stackOperations++;
|
||||
if (stackOperations > this.config.maxStackOperations) {
|
||||
console.warn(
|
||||
"Warning: Maximum stack operations reached, clearing stack"
|
||||
"Warning: Maximum stack operations reached, clearing stack",
|
||||
);
|
||||
this.indentStack = [];
|
||||
break;
|
||||
|
|
@ -1031,7 +1036,7 @@ export class MarkdownTaskParser {
|
|||
if (this.indentStack.length >= this.config.maxStackSize) {
|
||||
this.indentStack.splice(
|
||||
0,
|
||||
this.indentStack.length - this.config.maxStackSize + 1
|
||||
this.indentStack.length - this.config.maxStackSize + 1,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -1041,7 +1046,7 @@ export class MarkdownTaskParser {
|
|||
private getStatusFromMapping(rawStatus: string): string | undefined {
|
||||
// Find status name corresponding to raw character
|
||||
for (const [statusName, mappedChar] of Object.entries(
|
||||
this.config.statusMapping
|
||||
this.config.statusMapping,
|
||||
)) {
|
||||
if (mappedChar === rawStatus) {
|
||||
return statusName;
|
||||
|
|
@ -1078,7 +1083,7 @@ export class MarkdownTaskParser {
|
|||
private extractMultilineComment(
|
||||
lines: string[],
|
||||
startIndex: number,
|
||||
actualSpaces: number
|
||||
actualSpaces: number,
|
||||
): [string | undefined, number] {
|
||||
const commentLines: string[] = [];
|
||||
let i = startIndex;
|
||||
|
|
@ -1110,7 +1115,7 @@ export class MarkdownTaskParser {
|
|||
|
||||
// Legacy compatibility methods
|
||||
private extractLegacyPriority(
|
||||
metadata: Record<string, string>
|
||||
metadata: Record<string, string>,
|
||||
): number | undefined {
|
||||
if (!metadata.priority) return undefined;
|
||||
|
||||
|
|
@ -1144,19 +1149,21 @@ export class MarkdownTaskParser {
|
|||
}
|
||||
|
||||
// Then try to map string values (including emojis)
|
||||
const mappedPriority = priorityMap[metadata.priority.toLowerCase()] || priorityMap[metadata.priority];
|
||||
const mappedPriority =
|
||||
priorityMap[metadata.priority.toLowerCase()] ||
|
||||
priorityMap[metadata.priority];
|
||||
return mappedPriority;
|
||||
}
|
||||
|
||||
private extractLegacyDate(
|
||||
metadata: Record<string, string>,
|
||||
key: string
|
||||
key: string,
|
||||
): number | undefined {
|
||||
const dateStr = metadata[key];
|
||||
if (!dateStr) return undefined;
|
||||
|
||||
// Check cache first to avoid repeated date parsing
|
||||
const cacheKey = `${dateStr}_${(this.customDateFormats || []).join(',')}`;
|
||||
const cacheKey = `${dateStr}_${(this.customDateFormats || []).join(",")}`;
|
||||
const cachedDate = MarkdownTaskParser.dateCache.get(cacheKey);
|
||||
if (cachedDate !== undefined) {
|
||||
return cachedDate;
|
||||
|
|
@ -1241,8 +1248,8 @@ export class MarkdownTaskParser {
|
|||
heading: Array.isArray(enhancedTask.heading)
|
||||
? enhancedTask.heading
|
||||
: enhancedTask.heading
|
||||
? [enhancedTask.heading]
|
||||
: [],
|
||||
? [enhancedTask.heading]
|
||||
: [],
|
||||
parent: enhancedTask.parentId,
|
||||
tgProject: enhancedTask.tgProject,
|
||||
},
|
||||
|
|
@ -1377,18 +1384,18 @@ export class MarkdownTaskParser {
|
|||
* @returns Normalized tag with # prefix
|
||||
*/
|
||||
private normalizeTag(tag: string): string {
|
||||
if (typeof tag !== 'string') {
|
||||
if (typeof tag !== "string") {
|
||||
return tag;
|
||||
}
|
||||
|
||||
|
||||
// Trim whitespace
|
||||
const trimmed = tag.trim();
|
||||
|
||||
|
||||
// If empty or already starts with #, return as is
|
||||
if (!trimmed || trimmed.startsWith('#')) {
|
||||
if (!trimmed || trimmed.startsWith("#")) {
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
|
||||
// Add # prefix
|
||||
return `#${trimmed}`;
|
||||
}
|
||||
|
|
@ -1401,9 +1408,13 @@ export class MarkdownTaskParser {
|
|||
*/
|
||||
private mergeTags(baseTags: string[], inheritedTags: string[]): string[] {
|
||||
// Normalize all tags before merging
|
||||
const normalizedBaseTags = baseTags.map(tag => this.normalizeTag(tag));
|
||||
const normalizedInheritedTags = inheritedTags.map(tag => this.normalizeTag(tag));
|
||||
|
||||
const normalizedBaseTags = baseTags.map((tag) =>
|
||||
this.normalizeTag(tag),
|
||||
);
|
||||
const normalizedInheritedTags = inheritedTags.map((tag) =>
|
||||
this.normalizeTag(tag),
|
||||
);
|
||||
|
||||
const merged = [...normalizedBaseTags];
|
||||
|
||||
for (const tag of normalizedInheritedTags) {
|
||||
|
|
@ -1420,7 +1431,7 @@ export class MarkdownTaskParser {
|
|||
*/
|
||||
private inheritFileMetadata(
|
||||
taskMetadata: Record<string, string>,
|
||||
isSubtask: boolean = false
|
||||
isSubtask: boolean = false,
|
||||
): Record<string, string> {
|
||||
// Helper function to convert priority values to numbers
|
||||
const convertPriorityValue = (value: any): string => {
|
||||
|
|
@ -1465,7 +1476,8 @@ export class MarkdownTaskParser {
|
|||
}
|
||||
|
||||
// Try priority mapping (including emojis)
|
||||
const mappedPriority = priorityMap[strValue.toLowerCase()] || priorityMap[strValue];
|
||||
const mappedPriority =
|
||||
priorityMap[strValue.toLowerCase()] || priorityMap[strValue];
|
||||
if (mappedPriority !== undefined) {
|
||||
return String(mappedPriority);
|
||||
}
|
||||
|
|
@ -1535,7 +1547,7 @@ export class MarkdownTaskParser {
|
|||
|
||||
// Merge extracted metadata from tags
|
||||
for (const [tagKey, tagValue] of Object.entries(
|
||||
tagMetadata
|
||||
tagMetadata,
|
||||
)) {
|
||||
if (
|
||||
!nonInheritableFields.has(tagKey) &&
|
||||
|
|
@ -1563,7 +1575,9 @@ export class MarkdownTaskParser {
|
|||
inherited["tags"] === "")
|
||||
) {
|
||||
// Normalize tags before storing
|
||||
const normalizedTags = value.map(tag => this.normalizeTag(tag));
|
||||
const normalizedTags = value.map((tag) =>
|
||||
this.normalizeTag(tag),
|
||||
);
|
||||
inherited["tags"] = JSON.stringify(normalizedTags);
|
||||
}
|
||||
} else {
|
||||
|
|
@ -1593,7 +1607,7 @@ export class MarkdownTaskParser {
|
|||
// Inherit from project configuration data if available
|
||||
if (this.projectConfigCache) {
|
||||
for (const [key, value] of Object.entries(
|
||||
this.projectConfigCache
|
||||
this.projectConfigCache,
|
||||
)) {
|
||||
// Only inherit if:
|
||||
// 1. The field is not in the non-inheritable list
|
||||
|
|
@ -1642,10 +1656,10 @@ export class ConfigurableTaskParser extends MarkdownTaskParser {
|
|||
maxStackOperations: 1000,
|
||||
maxStackSize: 50,
|
||||
statusMapping: {
|
||||
"TODO": " ",
|
||||
"IN_PROGRESS": "/",
|
||||
"DONE": "x",
|
||||
"CANCELLED": "-"
|
||||
TODO: " ",
|
||||
IN_PROGRESS: "/",
|
||||
DONE: "x",
|
||||
CANCELLED: "-",
|
||||
},
|
||||
emojiMapping: {
|
||||
"📅": "dueDate",
|
||||
|
|
@ -1662,15 +1676,15 @@ export class ConfigurableTaskParser extends MarkdownTaskParser {
|
|||
"⏫": "priority",
|
||||
"🔼": "priority",
|
||||
"🔽": "priority",
|
||||
"⏬": "priority"
|
||||
"⏬": "priority",
|
||||
},
|
||||
metadataParseMode: MetadataParseMode.Both,
|
||||
specialTagPrefixes: {
|
||||
"project": "project",
|
||||
"@": "context"
|
||||
}
|
||||
project: "project",
|
||||
"@": "context",
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
super({ ...defaultConfig, ...config });
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -484,4 +484,69 @@ export class Repository {
|
|||
seq: this.lastSequence
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a task by its ID
|
||||
*/
|
||||
async getTaskById(taskId: string): Promise<Task | undefined> {
|
||||
// Get all tasks from the repository
|
||||
const allTasks = await this.all();
|
||||
|
||||
// Find the task by ID
|
||||
const task = allTasks.find(t => t.id === taskId);
|
||||
|
||||
return task;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a single task directly (for inline editing)
|
||||
* This avoids re-parsing the entire file
|
||||
*/
|
||||
async updateSingleTask(updatedTask: Task): Promise<void> {
|
||||
const filePath = updatedTask.filePath;
|
||||
if (!filePath) return;
|
||||
|
||||
console.log(`[Repository] Updating single task: ${updatedTask.id} in ${filePath}`);
|
||||
|
||||
// Load existing augmented tasks for the file
|
||||
const existingAugmented = await this.storage.loadAugmented(filePath);
|
||||
if (!existingAugmented) {
|
||||
console.warn(`[Repository] No existing tasks found for ${filePath}, cannot update single task`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Find and replace the task in the array
|
||||
const tasks = existingAugmented.data;
|
||||
const taskIndex = tasks.findIndex(t => t.id === updatedTask.id);
|
||||
|
||||
if (taskIndex === -1) {
|
||||
console.warn(`[Repository] Task ${updatedTask.id} not found in ${filePath}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Update the task
|
||||
tasks[taskIndex] = updatedTask;
|
||||
|
||||
// Update the index and storage
|
||||
await this.indexer.updateIndexWithTasks(filePath, tasks);
|
||||
await this.storage.storeAugmented(filePath, tasks);
|
||||
|
||||
// Schedule persist operation
|
||||
this.schedulePersist(filePath);
|
||||
|
||||
// Emit update event
|
||||
this.lastSequence = Seq.next();
|
||||
emit(this.app, Events.TASK_CACHE_UPDATED, {
|
||||
changedFiles: [filePath],
|
||||
stats: {
|
||||
total: await this.getTotalTaskCount(),
|
||||
changed: 1
|
||||
},
|
||||
timestamp: Date.now(),
|
||||
seq: this.lastSequence,
|
||||
sourceSeq: undefined
|
||||
});
|
||||
|
||||
console.log(`[Repository] Single task ${updatedTask.id} updated successfully`);
|
||||
}
|
||||
}
|
||||
|
|
@ -56,6 +56,16 @@ export class ObsidianSource {
|
|||
})
|
||||
);
|
||||
|
||||
// Clean up skip flag when write operation completes
|
||||
this.eventRefs.push(
|
||||
on(this.app, Events.WRITE_OPERATION_COMPLETE, ({ path }) => {
|
||||
// Delay cleanup slightly to ensure metadata changes are also skipped
|
||||
setTimeout(() => {
|
||||
this.skipNextModify.delete(path);
|
||||
}, 100);
|
||||
})
|
||||
);
|
||||
|
||||
// File system events
|
||||
this.eventRefs.push(
|
||||
this.vault.on('create', this.onFileCreate.bind(this)),
|
||||
|
|
@ -186,6 +196,13 @@ export class ObsidianSource {
|
|||
return;
|
||||
}
|
||||
|
||||
// Skip if this metadata change is from WriteAPI
|
||||
// WriteAPI operations can trigger metadata changes, but we handle them via TASK_UPDATED
|
||||
if (this.skipNextModify.has(file.path)) {
|
||||
console.log(`ObsidianSource: Skipping metadata change for ${file.path} (handled by WriteAPI)`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Clear existing timeout for this file
|
||||
const existingTimeout = this.pendingMetadataChanges.get(file.path);
|
||||
if (existingTimeout) {
|
||||
|
|
|
|||
13
src/index.ts
13
src/index.ts
|
|
@ -319,18 +319,21 @@ export default class TaskProgressBarPlugin extends Plugin {
|
|||
|
||||
// Initialize WriteAPI if dataflow is enabled
|
||||
if (this.settings?.experimental?.dataflowEnabled) {
|
||||
const getTaskById = (id: string) => {
|
||||
const getTaskById = async (id: string): Promise<Task | null> => {
|
||||
// Try dataflow first, fallback to taskManager
|
||||
if (this.dataflowOrchestrator) {
|
||||
try {
|
||||
const QueryAPI = require("./dataflow/api/QueryAPI").QueryAPI;
|
||||
const queryAPI = new QueryAPI(this.app, this.app.vault, this.app.metadataCache);
|
||||
return queryAPI.getRepository().getTaskById(id);
|
||||
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);
|
||||
}
|
||||
}
|
||||
return this.taskManager.getTaskById(id);
|
||||
const taskManagerResult = this.taskManager.getTaskById(id);
|
||||
return taskManagerResult || null;
|
||||
};
|
||||
|
||||
this.writeAPI = new WriteAPI(
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@ import {
|
|||
FilterGroup,
|
||||
RootFilterState,
|
||||
} from "../components/task-filter/ViewTaskFilter";
|
||||
import { isDataflowEnabled } from "../dataflow/createDataflow";
|
||||
|
||||
export const TASK_SPECIFIC_VIEW_TYPE = "task-genius-specific-view";
|
||||
|
||||
|
|
@ -175,17 +176,34 @@ export class TaskSpecificView extends ItemView {
|
|||
cls: "task-genius-container no-sidebar",
|
||||
});
|
||||
|
||||
// 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);
|
||||
}, 150); // 150ms debounce delay
|
||||
|
||||
// 1. 首先注册事件监听器,确保不会错过任何更新
|
||||
this.registerEvent(
|
||||
this.app.workspace.on(
|
||||
"task-genius:task-cache-updated",
|
||||
async () => {
|
||||
// Skip view update if currently editing in details panel
|
||||
const skipViewUpdate = this.detailsComponent?.isCurrentlyEditing() || false;
|
||||
await this.loadTasks(false, skipViewUpdate);
|
||||
}
|
||||
)
|
||||
);
|
||||
if (isDataflowEnabled(this.plugin) && this.plugin.dataflowOrchestrator) {
|
||||
// Dataflow: 订阅统一事件
|
||||
const { on, Events } = await import("../dataflow/events/Events");
|
||||
this.registerEvent(
|
||||
on(this.app, Events.CACHE_READY, async () => {
|
||||
// 冷启动就绪,从快照加载
|
||||
await this.loadTasksFast(true);
|
||||
})
|
||||
);
|
||||
this.registerEvent(
|
||||
on(this.app, Events.TASK_CACHE_UPDATED, debouncedViewUpdate)
|
||||
);
|
||||
} else {
|
||||
// Legacy: 兼容旧事件
|
||||
this.registerEvent(
|
||||
this.app.workspace.on(
|
||||
"task-genius:task-cache-updated",
|
||||
debouncedViewUpdate
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
this.registerEvent(
|
||||
this.app.workspace.on(
|
||||
|
|
@ -1242,17 +1260,10 @@ export class TaskSpecificView extends ItemView {
|
|||
}
|
||||
}
|
||||
|
||||
// 直接更新当前视图
|
||||
// Only switch view if not currently editing in details panel
|
||||
if (this.currentViewId && !this.detailsComponent.isCurrentlyEditing()) {
|
||||
// Always refresh the view after a successful update
|
||||
// The update operation itself means editing is complete
|
||||
if (this.currentViewId) {
|
||||
this.switchView(this.currentViewId, this.currentProject);
|
||||
} else if (this.currentViewId) {
|
||||
// Update task data in the current view without full re-render
|
||||
// Find active component and update its task list
|
||||
const activeComponent = this.getActiveComponent();
|
||||
if (activeComponent && typeof activeComponent.setTasks === "function") {
|
||||
activeComponent.setTasks(this.tasks, this.tasks);
|
||||
}
|
||||
}
|
||||
|
||||
return updatedTask;
|
||||
|
|
|
|||
|
|
@ -144,10 +144,8 @@ export class TaskView extends ItemView {
|
|||
|
||||
// Add debounced view update to prevent rapid successive refreshes
|
||||
const debouncedViewUpdate = debounce(async () => {
|
||||
const skipViewUpdate = this.detailsComponent?.isCurrentlyEditing() || false;
|
||||
if (!skipViewUpdate) {
|
||||
await this.loadTasks(false, false);
|
||||
}
|
||||
// Don't skip view updates - the detailsComponent will handle edit state properly
|
||||
await this.loadTasks(false, false);
|
||||
}, 150); // 150ms debounce delay
|
||||
|
||||
// 1. 首先注册事件监听器,确保不会错过任何更新
|
||||
|
|
@ -854,9 +852,9 @@ export class TaskView extends ItemView {
|
|||
};
|
||||
}
|
||||
|
||||
private switchView(viewId: ViewMode, project?: string | null) {
|
||||
private switchView(viewId: ViewMode, project?: string | null, forceRefresh: boolean = false) {
|
||||
this.currentViewId = viewId;
|
||||
console.log("Switching view to:", viewId, "Project:", project);
|
||||
console.log("[TaskView] Switching view to:", viewId, "Project:", project, "ForceRefresh:", forceRefresh);
|
||||
|
||||
// Update sidebar to reflect current view
|
||||
this.sidebarComponent.setViewMode(viewId);
|
||||
|
|
@ -977,10 +975,13 @@ export class TaskView extends ItemView {
|
|||
}
|
||||
|
||||
console.log("tasks", this.tasks);
|
||||
|
||||
|
||||
const filteredTasks = filterTasks(this.tasks, viewId, this.plugin, filterOptions);
|
||||
console.log("[TaskView] Calling setTasks with", filteredTasks.length, "filtered tasks, forceRefresh:", forceRefresh);
|
||||
targetComponent.setTasks(
|
||||
filterTasks(this.tasks, viewId, this.plugin, filterOptions),
|
||||
this.tasks
|
||||
filteredTasks,
|
||||
this.tasks,
|
||||
forceRefresh
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -1385,17 +1386,51 @@ export class TaskView extends ItemView {
|
|||
|
||||
try {
|
||||
// Use WriteAPI if dataflow is enabled
|
||||
let writeResult: { success: boolean; task?: Task; error?: string } | undefined;
|
||||
if (this.plugin.settings?.experimental?.dataflowEnabled && this.plugin.writeAPI) {
|
||||
const result = await this.plugin.writeAPI.updateTask({
|
||||
writeResult = await this.plugin.writeAPI.updateTask({
|
||||
taskId: updatedTask.id,
|
||||
updates: updatedTask
|
||||
});
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || "Failed to update task");
|
||||
if (!writeResult.success) {
|
||||
throw new Error(writeResult.error || "Failed to update task");
|
||||
}
|
||||
// Prefer the authoritative task returned by WriteAPI (includes updated originalMarkdown)
|
||||
if (writeResult.task) {
|
||||
updatedTask = writeResult.task;
|
||||
}
|
||||
} else {
|
||||
await taskManager.updateTask(updatedTask);
|
||||
}
|
||||
|
||||
console.log(`Task ${updatedTask.id} updated successfully via handleTaskUpdate.`);
|
||||
|
||||
// Update local task list immediately
|
||||
const index = this.tasks.findIndex((t) => t.id === originalTask.id);
|
||||
if (index !== -1) {
|
||||
// Create a new array to ensure ContentComponent detects the change
|
||||
this.tasks = [...this.tasks];
|
||||
this.tasks[index] = updatedTask;
|
||||
} else {
|
||||
console.warn(
|
||||
"Updated task not found in local list, might reload."
|
||||
);
|
||||
}
|
||||
|
||||
// Always refresh the view after a successful update
|
||||
// The update operation itself means editing is complete
|
||||
// Force refresh since we know the task has been updated
|
||||
this.switchView(this.currentViewId, undefined, true);
|
||||
|
||||
// Update details component if the updated task is currently selected
|
||||
if (this.currentSelectedTaskId === updatedTask.id) {
|
||||
if (this.detailsComponent.isCurrentlyEditing()) {
|
||||
// Update the current task reference without re-rendering UI
|
||||
this.detailsComponent.currentTask = updatedTask;
|
||||
} else {
|
||||
this.detailsComponent.showTaskDetails(updatedTask);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to update task:", error);
|
||||
// Re-throw the error so that the InlineEditor can handle it properly
|
||||
|
|
@ -1414,13 +1449,17 @@ export class TaskView extends ItemView {
|
|||
}
|
||||
try {
|
||||
// Use WriteAPI if dataflow is enabled
|
||||
let writeResult: { success: boolean; task?: Task; error?: string } | undefined;
|
||||
if (this.plugin.settings?.experimental?.dataflowEnabled && this.plugin.writeAPI) {
|
||||
const result = await this.plugin.writeAPI.updateTask({
|
||||
writeResult = await this.plugin.writeAPI.updateTask({
|
||||
taskId: updatedTask.id,
|
||||
updates: updatedTask
|
||||
});
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || "Failed to update task");
|
||||
if (!writeResult.success) {
|
||||
throw new Error(writeResult.error || "Failed to update task");
|
||||
}
|
||||
if (writeResult.task) {
|
||||
updatedTask = writeResult.task;
|
||||
}
|
||||
} else {
|
||||
await taskManager.updateTask(updatedTask);
|
||||
|
|
@ -1430,6 +1469,8 @@ export class TaskView extends ItemView {
|
|||
// 立即更新本地任务列表
|
||||
const index = this.tasks.findIndex((t) => t.id === originalTask.id);
|
||||
if (index !== -1) {
|
||||
// Create a new array to ensure ContentComponent detects the change
|
||||
this.tasks = [...this.tasks];
|
||||
this.tasks[index] = updatedTask;
|
||||
} else {
|
||||
console.warn(
|
||||
|
|
@ -1437,25 +1478,10 @@ export class TaskView extends ItemView {
|
|||
);
|
||||
}
|
||||
|
||||
// 如果任务在当前视图中,立即更新视图
|
||||
// Only skip view update if currently editing in details panel AND it's not a status change
|
||||
const isStatusChange = originalTask.status !== updatedTask.status ||
|
||||
originalTask.completed !== updatedTask.completed;
|
||||
|
||||
if (!this.detailsComponent.isCurrentlyEditing() || isStatusChange) {
|
||||
// Always refresh view for status changes or when not editing
|
||||
this.switchView(this.currentViewId);
|
||||
} else {
|
||||
// Update the task in the current view without re-rendering (only for content edits)
|
||||
// Use setTasks to update the components with the modified task list
|
||||
if (this.currentViewId === "inbox" || this.currentViewId === "projects") {
|
||||
this.contentComponent.setTasks(this.tasks, this.tasks);
|
||||
} else if (this.currentViewId === "forecast") {
|
||||
this.forecastComponent.setTasks(this.tasks);
|
||||
} else if (this.currentViewId === "tags") {
|
||||
this.tagsComponent.setTasks(this.tasks);
|
||||
}
|
||||
}
|
||||
// Always refresh the view after a successful update
|
||||
// The update operation itself means editing is complete
|
||||
// Force refresh since we know the task has been updated
|
||||
this.switchView(this.currentViewId, undefined, true);
|
||||
|
||||
if (this.currentSelectedTaskId === updatedTask.id) {
|
||||
if (this.detailsComponent.isCurrentlyEditing()) {
|
||||
|
|
|
|||
Loading…
Reference in a new issue