fix(fluent): resolve filter state loading and flickering issues

Fix three critical issues in FluentTaskView filtering system:

1. Filter button state loading: Remove incorrect onLayoutReady wrapper
   that only fired once during initial layout. Now uses direct setTimeout
   to load filter state every time popover is opened.

2. Flickering during filtering: Add 150ms debounced handleFilterChanged
   method to prevent rapid re-renders. Previously filter changes triggered
   2-3 consecutive updates, now consolidated to single update.

3. Projects view filtering: Fix dual-mode filtering where project list
   should show all projects (left sidebar) while task list respects
   global filter (right panel). Modified ProjectsComponent.setTasks to
   accept both tasks and filteredTasks parameters.

Affected files:
- FluentLayoutManager.ts: Fix popover state loading timing
- FluentTaskView.ts: Add debounced filter handler
- FluentComponentManager.ts: Pass both parameters to Projects view
- projects.ts: Support external filtered tasks
This commit is contained in:
Quorafind 2025-10-23 16:58:48 +08:00
parent 0f3726d7d2
commit 8c89618e10
4 changed files with 111 additions and 78 deletions

View file

@ -604,13 +604,21 @@ export class FluentComponentManager extends Component {
// Set tasks on the component
if (typeof targetComponent.setTasks === "function") {
// Special handling for components that need only all tasks (single parameter)
// Projects overview mode (no project selected) needs all tasks to build project list
if (viewId === "review" || viewId === "tags" || (viewId === "projects" && !project)) {
// Review and tags views need all tasks to build their indices
if (viewId === "review" || viewId === "tags") {
console.log(
`[FluentComponent] Calling setTasks for ${viewId} with ALL tasks:`,
tasks.length,
);
targetComponent.setTasks(tasks);
} else if (viewId === "projects" && !project) {
// Projects overview mode: pass ALL tasks to build project index
// and FILTERED tasks to apply filter to project task lists
// This ensures: left sidebar shows all projects, right panel shows filtered tasks
console.log(
`[FluentComponent] Calling setTasks for projects with ALL tasks (${tasks.length}) and FILTERED tasks (${filteredTasks.length})`,
);
targetComponent.setTasks(tasks, filteredTasks);
} else {
// Use filtered tasks
let filteredTasksLocal = [...filteredTasks];

View file

@ -564,17 +564,21 @@ export class FluentLayoutManager extends Component {
this.plugin,
);
// Set up filter state when opening
this.app.workspace.onLayoutReady(() => {
setTimeout(() => {
const liveFilterState = this.getLiveFilterState?.();
if (liveFilterState && popover.taskFilterComponent) {
popover.taskFilterComponent.loadFilterState(
liveFilterState,
);
}
}, 100);
});
// Set onClose callback for consistency (realtime event listeners handle the actual updates)
popover.onClose = (filterState) => {
// Realtime event listeners already handle filter changes
// This callback is kept for potential future extensions
};
// Load current filter state after popover is shown
setTimeout(() => {
const liveFilterState = this.getLiveFilterState?.();
if (liveFilterState && popover.taskFilterComponent) {
popover.taskFilterComponent.loadFilterState(
liveFilterState,
);
}
}, 100);
popover.showAtPosition({ x: e.clientX, y: e.clientY });
} else {
@ -586,15 +590,15 @@ export class FluentLayoutManager extends Component {
modal.open();
// Set initial filter state
const liveFilterState = this.getLiveFilterState?.();
if (liveFilterState && modal.taskFilterComponent) {
setTimeout(() => {
// Load current filter state after modal is opened
setTimeout(() => {
const liveFilterState = this.getLiveFilterState?.();
if (liveFilterState && modal.taskFilterComponent) {
modal.taskFilterComponent.loadFilterState(
liveFilterState,
);
}, 100);
}
}
}, 100);
}
});

View file

@ -54,6 +54,7 @@ export class ProjectsComponent extends Component {
// State
private allTasks: Task[] = [];
private filteredTasks: Task[] = [];
private externalFilteredTasks: Task[] | null = null; // Externally filtered tasks from FluentView
private selectedProjects: SelectedProjects = {
projects: [],
tasks: [],
@ -290,8 +291,9 @@ export class ProjectsComponent extends Component {
this.updateTgProjectPropsButton(null);
}
public setTasks(tasks: Task[]) {
public setTasks(tasks: Task[], filteredTasks?: Task[]) {
this.allTasks = tasks;
this.externalFilteredTasks = filteredTasks || null;
this.allTasksMap = new Map(
this.allTasks.map((task) => [task.id, task]),
);
@ -674,6 +676,17 @@ export class ProjectsComponent extends Component {
});
}
// Apply external filter if provided (from FluentView filter system)
// This ensures project tasks respect global filter conditions
if (this.externalFilteredTasks) {
const externalFilteredIds = new Set(
this.externalFilteredTasks.map((t) => t.id),
);
this.filteredTasks = this.filteredTasks.filter((task) =>
externalFilteredIds.has(task.id),
);
}
// Update the task list using the renderer
this.renderTaskList();
}

View file

@ -144,6 +144,67 @@ export class FluentTaskView extends ItemView {
);
}, 200);
/**
* Debounced filter change handler to prevent rapid re-renders
* Delay: 150ms to balance responsiveness and performance
*/
private handleFilterChanged = debounce(
(filterState: RootFilterState, leafId?: string) => {
// Only update if it's from a live filter component
if (
leafId &&
!leafId.startsWith("view-config-") &&
leafId !== "global-filter"
) {
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] Filter changed (no leafId)");
this.liveFilterState = filterState;
this.currentFilterState = filterState;
}
// Sync selectedProject with filter UI state (if project filter is present)
try {
const groups = filterState?.filterGroups || [];
const projectFilters: string[] = [];
for (const g of groups) {
for (const f of g.filters || []) {
if (
f.property === "project" &&
f.condition === "is" &&
typeof f.value === "string" &&
f.value.trim() !== ""
) {
projectFilters.push(f.value);
}
}
}
if (projectFilters.length > 0) {
this.viewState.selectedProject = projectFilters[0];
} else {
this.viewState.selectedProject = undefined;
}
} catch (e) {
console.warn(
"[TG] Failed to sync selectedProject from filter state",
e,
);
}
// Persist and update header UI
this.workspaceStateManager.saveFilterStateToWorkspace();
this.layoutManager.updateActionButtons();
// Apply filters and update view
this.filteredTasks = this.dataManager.applyFilters(this.tasks);
this.updateView();
},
150,
);
onResize(): void {
this.updateWorkspaceLeafWidth();
}
@ -784,12 +845,11 @@ export class FluentTaskView extends ItemView {
(query: string) => this.actionHandlers.handleSearch(query),
(mode: ViewMode) => this.actionHandlers.handleViewModeChange(mode),
() => {
// Filter click - open filter modal/popover
// TODO: Implement filter modal
// Filter click callback (currently unused)
// Note: Filter functionality is implemented in FluentLayoutManager via addAction
},
() => {
// Sort click
// TODO: Implement sort
// Sort click callback (future extension point)
},
() => this.actionHandlers.handleSettingsClick(),
);
@ -970,64 +1030,12 @@ export class FluentTaskView extends ItemView {
);
}
// Listen for filter change events
// Listen for filter change events (with debouncing to prevent flickering)
this.registerEvent(
this.app.workspace.on(
"task-genius:filter-changed",
(filterState: RootFilterState, leafId?: string) => {
// Only update if it's from a live filter component
if (
leafId &&
!leafId.startsWith("view-config-") &&
leafId !== "global-filter"
) {
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] Filter changed (no leafId)");
this.liveFilterState = filterState;
this.currentFilterState = filterState;
}
// Sync selectedProject with filter UI state (if project filter is present)
try {
const groups = filterState?.filterGroups || [];
const projectFilters: string[] = [];
for (const g of groups) {
for (const f of g.filters || []) {
if (
f.property === "project" &&
f.condition === "is" &&
typeof f.value === "string" &&
f.value.trim() !== ""
) {
projectFilters.push(f.value);
}
}
}
if (projectFilters.length > 0) {
this.viewState.selectedProject = projectFilters[0];
} else {
this.viewState.selectedProject = undefined;
}
} catch (e) {
console.warn(
"[TG] Failed to sync selectedProject from filter state",
e,
);
}
// Persist and update header UI
this.workspaceStateManager.saveFilterStateToWorkspace();
this.layoutManager.updateActionButtons();
// Apply filters and update view
this.filteredTasks = this.dataManager.applyFilters(
this.tasks,
);
this.updateView();
this.handleFilterChanged(filterState, leafId);
},
),
);