mirror of
https://github.com/taskgenius/taskgenius-plugin.git
synced 2026-07-22 06:40:25 +00:00
feat(bulk-operations): optimize view updates to prevent list flashing
- Add BATCH_OPERATION_START/COMPLETE events - Pause view updates during batch operations - Refresh view once after batch operation completes - Add payload type definitions for type safety
This commit is contained in:
parent
add61b7777
commit
fc7600c93a
5 changed files with 153 additions and 83 deletions
|
|
@ -24,6 +24,9 @@ export class FluentDataManager extends Component {
|
|||
private onLoadingStateChanged?: (isLoading: boolean) => void;
|
||||
private onUpdateNeeded?: (source: string) => void;
|
||||
|
||||
// Batch operation state flag
|
||||
private isBatchOperating = false;
|
||||
|
||||
constructor(
|
||||
private plugin: TaskProgressBarPlugin,
|
||||
private getCurrentViewId: () => string,
|
||||
|
|
@ -269,6 +272,15 @@ export class FluentDataManager extends Component {
|
|||
// Add debounced view update to prevent rapid successive refreshes
|
||||
const debouncedViewUpdate = debounce(async () => {
|
||||
console.log("[FluentData] debouncedViewUpdate triggered");
|
||||
|
||||
// Skip update during batch operation to prevent list flashing
|
||||
if (this.isBatchOperating) {
|
||||
console.log(
|
||||
"[FluentData] Skipping update during batch operation",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.isInitializing()) {
|
||||
// Load tasks and notify parent
|
||||
await this.loadTasks(false);
|
||||
|
|
@ -289,6 +301,36 @@ export class FluentDataManager extends Component {
|
|||
isDataflowEnabled(this.plugin) &&
|
||||
this.plugin.dataflowOrchestrator
|
||||
) {
|
||||
// Listen for batch operation start
|
||||
this.registerEvent(
|
||||
on(this.plugin.app, Events.BATCH_OPERATION_START, (payload) => {
|
||||
console.log(
|
||||
`[FluentData] Batch operation started: ${payload.count} tasks`,
|
||||
);
|
||||
this.isBatchOperating = true;
|
||||
}),
|
||||
);
|
||||
|
||||
// Listen for batch operation complete
|
||||
this.registerEvent(
|
||||
on(
|
||||
this.plugin.app,
|
||||
Events.BATCH_OPERATION_COMPLETE,
|
||||
async (payload) => {
|
||||
console.log(
|
||||
`[FluentData] Batch operation complete: ${payload.successCount} succeeded, ${payload.failCount} failed`,
|
||||
);
|
||||
this.isBatchOperating = false;
|
||||
|
||||
// Immediately refresh view after batch operation (skip debounce)
|
||||
if (!this.isInitializing()) {
|
||||
await this.loadTasks(false);
|
||||
this.onUpdateNeeded?.("batch-operation-complete");
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
// Listen for cache ready event
|
||||
this.registerEvent(
|
||||
on(this.plugin.app, Events.CACHE_READY, async () => {
|
||||
|
|
|
|||
|
|
@ -55,6 +55,8 @@ export class ContentComponent extends Component {
|
|||
private pendingForceRefresh: boolean = false; // Track if a force refresh is pending
|
||||
private pendingVisibilityRetry: boolean = false; // Queue a retry when container becomes visible
|
||||
private visibilityRetryCount: number = 0; // Limit visibility retry loop
|
||||
private lastAllTasksSignature = "";
|
||||
private lastNotFilteredTasksSignature = "";
|
||||
constructor(
|
||||
private parentEl: HTMLElement,
|
||||
private app: App,
|
||||
|
|
@ -198,6 +200,12 @@ export class ContentComponent extends Component {
|
|||
notFilteredTasks: Task[],
|
||||
forceRefresh: boolean = false,
|
||||
) {
|
||||
const updateSignatures = () => {
|
||||
this.lastAllTasksSignature = this.computeTaskSignature(tasks);
|
||||
this.lastNotFilteredTasksSignature =
|
||||
this.computeTaskSignature(notFilteredTasks);
|
||||
};
|
||||
|
||||
// Allow forced refresh for cases where we know the data has changed
|
||||
if (forceRefresh) {
|
||||
console.log("ContentComponent: Forced refresh requested");
|
||||
|
|
@ -206,6 +214,7 @@ export class ContentComponent extends Component {
|
|||
this.pendingForceRefresh = true;
|
||||
this.allTasks = tasks;
|
||||
this.notFilteredTasks = notFilteredTasks;
|
||||
updateSignatures();
|
||||
this.applyFilters();
|
||||
this.refreshTaskList();
|
||||
return;
|
||||
|
|
@ -219,47 +228,23 @@ export class ContentComponent extends Component {
|
|||
return;
|
||||
}
|
||||
|
||||
// Prevent unnecessary refreshes if data hasn't actually changed
|
||||
// Check if the array reference has changed (which indicates an update)
|
||||
const nextAllSignature = this.computeTaskSignature(tasks);
|
||||
const nextNotFilteredSignature =
|
||||
this.computeTaskSignature(notFilteredTasks);
|
||||
if (
|
||||
this.allTasks === tasks &&
|
||||
this.notFilteredTasks === notFilteredTasks
|
||||
nextAllSignature === this.lastAllTasksSignature &&
|
||||
nextNotFilteredSignature === this.lastNotFilteredTasksSignature
|
||||
) {
|
||||
console.log(
|
||||
"ContentComponent: Same array references, skipping refresh",
|
||||
"ContentComponent: Task signatures unchanged, 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(5, tasks.length);
|
||||
let unchanged = true;
|
||||
for (let i = 0; i < sampleSize; i++) {
|
||||
if (
|
||||
this.allTasks[i]?.id !== tasks[i]?.id ||
|
||||
this.allTasks[i]?.originalMarkdown !==
|
||||
tasks[i]?.originalMarkdown
|
||||
) {
|
||||
unchanged = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (unchanged) {
|
||||
console.log(
|
||||
"ContentComponent: Tasks unchanged, skipping refresh",
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.allTasks = tasks;
|
||||
this.notFilteredTasks = notFilteredTasks;
|
||||
this.lastAllTasksSignature = nextAllSignature;
|
||||
this.lastNotFilteredTasksSignature = nextNotFilteredSignature;
|
||||
this.applyFilters();
|
||||
this.refreshTaskList();
|
||||
}
|
||||
|
|
@ -350,6 +335,21 @@ export class ContentComponent extends Component {
|
|||
this.refreshTaskList();
|
||||
}
|
||||
|
||||
private computeTaskSignature(tasks: Task[]): string {
|
||||
if (!tasks || tasks.length === 0) return "";
|
||||
return tasks
|
||||
.map((task) =>
|
||||
[
|
||||
task.id,
|
||||
task.completed ? "1" : "0",
|
||||
task.originalMarkdown ?? "",
|
||||
task.content ?? "",
|
||||
task.metadata ? JSON.stringify(task.metadata) : "",
|
||||
].join("|"),
|
||||
)
|
||||
.join(";");
|
||||
}
|
||||
|
||||
private cleanupComponents() {
|
||||
// Unload and clear previous components
|
||||
this.taskComponents.forEach((component) => this.removeChild(component));
|
||||
|
|
|
|||
|
|
@ -814,6 +814,11 @@ export class WriteAPI {
|
|||
argsList: UpdateTaskArgs[],
|
||||
): Promise<BulkOperationResult> {
|
||||
return this.enqueueWrite(async () => {
|
||||
// Emit batch operation start event
|
||||
emit(this.app, Events.BATCH_OPERATION_START, {
|
||||
count: argsList.length,
|
||||
});
|
||||
|
||||
const summary: BulkOperationResult = {
|
||||
successCount: 0,
|
||||
failCount: 0,
|
||||
|
|
@ -821,24 +826,32 @@ export class WriteAPI {
|
|||
totalCount: argsList.length,
|
||||
};
|
||||
|
||||
for (const args of argsList) {
|
||||
const originalTask = await Promise.resolve(
|
||||
this.getTaskById(args.taskId),
|
||||
);
|
||||
const updateResult = await this.performUpdateTask(args);
|
||||
if (updateResult.success) {
|
||||
summary.successCount++;
|
||||
} else {
|
||||
summary.failCount++;
|
||||
summary.errors.push({
|
||||
taskId: args.taskId,
|
||||
taskContent: originalTask?.content || "",
|
||||
error: updateResult.error || "Unknown error",
|
||||
});
|
||||
try {
|
||||
for (const args of argsList) {
|
||||
const originalTask = await Promise.resolve(
|
||||
this.getTaskById(args.taskId),
|
||||
);
|
||||
const updateResult = await this.performUpdateTask(args);
|
||||
if (updateResult.success) {
|
||||
summary.successCount++;
|
||||
} else {
|
||||
summary.failCount++;
|
||||
summary.errors.push({
|
||||
taskId: args.taskId,
|
||||
taskContent: originalTask?.content || "",
|
||||
error: updateResult.error || "Unknown error",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return summary;
|
||||
return summary;
|
||||
} finally {
|
||||
// Emit batch operation complete event (always, even if error occurred)
|
||||
emit(this.app, Events.BATCH_OPERATION_COMPLETE, {
|
||||
successCount: summary.successCount,
|
||||
failCount: summary.failCount,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -16,8 +16,20 @@ export const Events = {
|
|||
ICS_EVENTS_UPDATED: "task-genius:ics-events-updated",
|
||||
FILE_TASK_UPDATED: "task-genius:file-task-updated",
|
||||
FILE_TASK_REMOVED: "task-genius:file-task-removed",
|
||||
BATCH_OPERATION_START: "task-genius:batch-operation-start",
|
||||
BATCH_OPERATION_COMPLETE: "task-genius:batch-operation-complete",
|
||||
} as const;
|
||||
|
||||
// Batch operation payload types
|
||||
export interface BatchOperationStartPayload {
|
||||
count: number;
|
||||
}
|
||||
|
||||
export interface BatchOperationCompletePayload {
|
||||
successCount: number;
|
||||
failCount: number;
|
||||
}
|
||||
|
||||
export type SeqClock = { next(): number };
|
||||
|
||||
let _seq = 0;
|
||||
|
|
@ -45,3 +57,8 @@ export const onTaskCacheUpdated = (app: App, handler: (payload: any) => void) =>
|
|||
export const emitTaskCacheUpdated = (app: App, payload: any) =>
|
||||
emit(app, Events.TASK_CACHE_UPDATED, payload);
|
||||
|
||||
export const emitBatchOperationStart = (app: App, payload: BatchOperationStartPayload) =>
|
||||
emit(app, Events.BATCH_OPERATION_START, payload);
|
||||
export const emitBatchOperationComplete = (app: App, payload: BatchOperationCompletePayload) =>
|
||||
emit(app, Events.BATCH_OPERATION_COMPLETE, payload);
|
||||
|
||||
|
|
|
|||
|
|
@ -139,7 +139,7 @@ export class FluentTaskView extends ItemView {
|
|||
* Main initialization method
|
||||
*/
|
||||
async onOpen() {
|
||||
console.log("[TG-V2] onOpen started");
|
||||
console.log("[TG] onOpen started");
|
||||
this.isInitializing = true;
|
||||
|
||||
try {
|
||||
|
|
@ -148,7 +148,7 @@ export class FluentTaskView extends ItemView {
|
|||
// ====================
|
||||
if (this.DEBUG_MODE) {
|
||||
console.log(
|
||||
"[TG-V2] Initializing UI, managers, structure, and events...",
|
||||
"[TG] Initializing UI, managers, structure, and events...",
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -179,7 +179,7 @@ export class FluentTaskView extends ItemView {
|
|||
|
||||
if (this.DEBUG_MODE) {
|
||||
console.log(
|
||||
"[TG-V2] ✅ UI, managers, structure, and events initialized",
|
||||
"[TG] ✅ UI, managers, structure, and events initialized",
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -187,7 +187,7 @@ export class FluentTaskView extends ItemView {
|
|||
// PHASE 5: Restore Workspace State
|
||||
// ====================
|
||||
if (this.DEBUG_MODE) {
|
||||
console.log("[TG-V2] Restoring workspace state...");
|
||||
console.log("[TG] Restoring workspace state...");
|
||||
}
|
||||
|
||||
const savedWorkspaceId =
|
||||
|
|
@ -197,7 +197,7 @@ export class FluentTaskView extends ItemView {
|
|||
this.viewState.currentWorkspace = savedWorkspaceId;
|
||||
if (this.DEBUG_MODE) {
|
||||
console.log(
|
||||
`[TG-V2] Restored workspace ID: ${savedWorkspaceId}`,
|
||||
`[TG] Restored workspace ID: ${savedWorkspaceId}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -239,11 +239,11 @@ export class FluentTaskView extends ItemView {
|
|||
// ====================
|
||||
// PHASE 6: Load Data (KEY PHASE)
|
||||
// ====================
|
||||
console.log("[TG-V2] Loading tasks...");
|
||||
console.log("[TG] Loading tasks...");
|
||||
await this.dataManager.loadTasks(false); // Will trigger onTasksLoaded callback
|
||||
await this.dataManager.registerDataflowListeners();
|
||||
console.log(
|
||||
`[TG-V2] ✅ Loaded ${this.tasks.length} tasks, ${this.filteredTasks.length} after filters`,
|
||||
`[TG] ✅ Loaded ${this.tasks.length} tasks, ${this.filteredTasks.length} after filters`,
|
||||
);
|
||||
|
||||
// ====================
|
||||
|
|
@ -254,12 +254,12 @@ export class FluentTaskView extends ItemView {
|
|||
|
||||
// Check window size and auto-collapse sidebar if needed
|
||||
if (this.DEBUG_MODE) {
|
||||
console.log("[TG-V2] Checking sidebar collapse...");
|
||||
console.log("[TG] Checking sidebar collapse...");
|
||||
}
|
||||
this.layoutManager.checkAndCollapseSidebar();
|
||||
} catch (error) {
|
||||
console.error("[TG-V2] ❌ Initialization error:", error);
|
||||
console.error("[TG-V2] Error stack:", (error as Error).stack);
|
||||
console.error("[TG] ❌ Initialization error:", error);
|
||||
console.error("[TG] Error stack:", (error as Error).stack);
|
||||
this.loadError =
|
||||
(error as Error).message || "Failed to initialize view";
|
||||
} finally {
|
||||
|
|
@ -268,17 +268,17 @@ export class FluentTaskView extends ItemView {
|
|||
// ====================
|
||||
if (this.DEBUG_MODE) {
|
||||
console.log(
|
||||
`[TG-V2] Finalizing (isInitializing was ${this.isInitializing})`,
|
||||
`[TG] Finalizing (isInitializing was ${this.isInitializing})`,
|
||||
);
|
||||
}
|
||||
|
||||
this.isInitializing = false;
|
||||
|
||||
if (this.DEBUG_MODE) {
|
||||
console.log("[TG-V2] Calling final updateView()...");
|
||||
console.log("[TG] Calling final updateView()...");
|
||||
}
|
||||
this.updateView();
|
||||
console.log("[TG-V2] ✅ Initialization complete");
|
||||
console.log("[TG] ✅ Initialization complete");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -326,7 +326,7 @@ export class FluentTaskView extends ItemView {
|
|||
}
|
||||
},
|
||||
onUpdateNeeded: (source) => {
|
||||
console.log(`[TG-V2] Update needed from source: ${source}`);
|
||||
console.log(`[TG] Update needed from source: ${source}`);
|
||||
// Re-apply filters and update view
|
||||
this.filteredTasks = this.dataManager.applyFilters(this.tasks);
|
||||
this.updateView();
|
||||
|
|
@ -395,7 +395,7 @@ export class FluentTaskView extends ItemView {
|
|||
this.updateView();
|
||||
},
|
||||
onProjectSelected: (projectId) => {
|
||||
console.log(`[TG-V2] Project selected: ${projectId}`);
|
||||
console.log(`[TG] Project selected: ${projectId}`);
|
||||
this.viewState.selectedProject = projectId;
|
||||
|
||||
// Switch to projects view
|
||||
|
|
@ -451,7 +451,7 @@ export class FluentTaskView extends ItemView {
|
|||
);
|
||||
} catch (e) {
|
||||
console.warn(
|
||||
"[TG-V2] Failed to project-sync filter UI state",
|
||||
"[TG] Failed to project-sync filter UI state",
|
||||
e,
|
||||
);
|
||||
// If filter sync fails, still update the view
|
||||
|
|
@ -498,14 +498,14 @@ export class FluentTaskView extends ItemView {
|
|||
this.selectionManager = new TaskSelectionManager(this.app, this.plugin);
|
||||
this.addChild(this.selectionManager);
|
||||
|
||||
console.log("[TG-V2] Managers initialized");
|
||||
console.log("[TG] Managers initialized");
|
||||
}
|
||||
|
||||
/**
|
||||
* Build UI structure - MUST match original DOM structure for CSS
|
||||
*/
|
||||
private async buildUIStructure() {
|
||||
console.log("[TG-V2] Building UI structure");
|
||||
console.log("[TG] Building UI structure");
|
||||
|
||||
// Create layout structure (exact same as original)
|
||||
const layoutContainer = this.rootContainerEl.createDiv({
|
||||
|
|
@ -592,12 +592,12 @@ export class FluentTaskView extends ItemView {
|
|||
} else {
|
||||
sidebarEl.hide();
|
||||
console.log(
|
||||
"[TG-V2] Using workspace side leaves: skip in-view sidebar",
|
||||
"[TG] Using workspace side leaves: skip in-view sidebar",
|
||||
);
|
||||
}
|
||||
|
||||
// Create top navigation
|
||||
console.log("[TG-V2] Initializing top navigation");
|
||||
console.log("[TG] Initializing top navigation");
|
||||
this.topNavigation = new TopNavigation(
|
||||
topNavEl,
|
||||
this.plugin,
|
||||
|
|
@ -616,7 +616,7 @@ export class FluentTaskView extends ItemView {
|
|||
this.addChild(this.topNavigation);
|
||||
|
||||
// Initialize view components
|
||||
console.log("[TG-V2] Initializing view components");
|
||||
console.log("[TG] Initializing view components");
|
||||
this.componentManager = new FluentComponentManager(
|
||||
this.app,
|
||||
this.plugin,
|
||||
|
|
@ -654,7 +654,7 @@ export class FluentTaskView extends ItemView {
|
|||
this.componentManager.initializeViewComponents();
|
||||
|
||||
// Sidebar toggle in header and responsive collapse
|
||||
console.log("[TG-V2] Creating sidebar toggle");
|
||||
console.log("[TG] Creating sidebar toggle");
|
||||
this.layoutManager.createSidebarToggle();
|
||||
|
||||
// Create task count mark
|
||||
|
|
@ -677,10 +677,10 @@ export class FluentTaskView extends ItemView {
|
|||
});
|
||||
|
||||
// Create action buttons in Obsidian view header
|
||||
console.log("[TG-V2] Creating action buttons");
|
||||
console.log("[TG] Creating action buttons");
|
||||
this.layoutManager.createActionButtons();
|
||||
|
||||
console.log("[TG-V2] UI structure built");
|
||||
console.log("[TG] UI structure built");
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -772,14 +772,12 @@ export class FluentTaskView extends ItemView {
|
|||
!leafId.startsWith("view-config-") &&
|
||||
leafId !== "global-filter"
|
||||
) {
|
||||
console.log(
|
||||
"[TG-V2] Filter changed from live component",
|
||||
);
|
||||
console.log("[TG] Filter changed from live component");
|
||||
this.liveFilterState = filterState;
|
||||
this.currentFilterState = filterState;
|
||||
} else if (!leafId) {
|
||||
// No leafId means it's also a live filter change
|
||||
console.log("[TG-V2] Filter changed (no leafId)");
|
||||
console.log("[TG] Filter changed (no leafId)");
|
||||
this.liveFilterState = filterState;
|
||||
this.currentFilterState = filterState;
|
||||
}
|
||||
|
|
@ -807,7 +805,7 @@ export class FluentTaskView extends ItemView {
|
|||
}
|
||||
} catch (e) {
|
||||
console.warn(
|
||||
"[TG-V2] Failed to sync selectedProject from filter state",
|
||||
"[TG] Failed to sync selectedProject from filter state",
|
||||
e,
|
||||
);
|
||||
}
|
||||
|
|
@ -871,7 +869,7 @@ export class FluentTaskView extends ItemView {
|
|||
private updateView() {
|
||||
// Enhanced logging with all critical state
|
||||
console.log(
|
||||
`[TG-V2] updateView called: ` +
|
||||
`[TG] updateView called: ` +
|
||||
`isInitializing=${this.isInitializing}, ` +
|
||||
`viewId=${this.currentViewId}, ` +
|
||||
`tasks=${this.tasks.length}, ` +
|
||||
|
|
@ -881,12 +879,12 @@ export class FluentTaskView extends ItemView {
|
|||
);
|
||||
|
||||
if (this.isInitializing) {
|
||||
console.log("[TG-V2] ⏭️ Skip update during initialization");
|
||||
console.log("[TG] ⏭️ Skip update during initialization");
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(
|
||||
`[TG-V2] ▶️ Proceeding with view update for ${this.currentViewId}`,
|
||||
`[TG] ▶️ Proceeding with view update for ${this.currentViewId}`,
|
||||
);
|
||||
|
||||
// Update task count
|
||||
|
|
@ -934,7 +932,7 @@ export class FluentTaskView extends ItemView {
|
|||
* Reset all active filters
|
||||
*/
|
||||
private resetCurrentFilter(): void {
|
||||
console.log("[TG-V2] Resetting filter");
|
||||
console.log("[TG] Resetting filter");
|
||||
|
||||
// Clear filter states
|
||||
this.liveFilterState = null;
|
||||
|
|
@ -985,7 +983,7 @@ export class FluentTaskView extends ItemView {
|
|||
* Clean up on close
|
||||
*/
|
||||
async onClose() {
|
||||
console.log("[TG-V2] onClose started");
|
||||
console.log("[TG] onClose started");
|
||||
|
||||
// Save workspace layout before closing
|
||||
this.workspaceStateManager.saveWorkspaceLayout();
|
||||
|
|
@ -998,6 +996,6 @@ export class FluentTaskView extends ItemView {
|
|||
// Clear selection
|
||||
this.actionHandlers.clearSelection();
|
||||
|
||||
console.log("[TG-V2] onClose completed");
|
||||
console.log("[TG] onClose completed");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue