fix(fluent): resolve view mode availability and navigation rendering

- Fix navigation not re-rendering when transitioning from empty to non-empty modes
- Make projects view mode availability dynamic based on project selection
- Ensure filtered tasks are recomputed when navigating between views
- Clean up trailing commas for code consistency
This commit is contained in:
Quorafind 2025-10-26 15:44:12 +08:00
parent e9e8979b07
commit 3fbdf4f1d2
3 changed files with 162 additions and 137 deletions

View file

@ -32,7 +32,7 @@ export class TopNavigation extends Component {
private onSortClick: () => void,
private onSettingsClick: () => void,
availableModes?: ViewMode[],
private onToggleSidebar?: () => void,
private onToggleSidebar?: () => void
) {
super();
this.containerEl = containerEl;
@ -60,14 +60,14 @@ export class TopNavigation extends Component {
this.registerEvent(
on(this.plugin.app, Events.CACHE_READY, () => {
this.updateNotificationCount();
}),
})
);
// Listen for task cache updates
this.registerEvent(
on(this.plugin.app, Events.TASK_CACHE_UPDATED, () => {
this.updateNotificationCount();
}),
})
);
}
@ -116,7 +116,7 @@ export class TopNavigation extends Component {
} catch (error) {
console.warn(
"[FluentTopNavigation] Failed to update notification count:",
error,
error
);
}
}
@ -180,7 +180,7 @@ export class TopNavigation extends Component {
badge.show();
}
this.registerDomEvent(notificationBtn, "click", (e) =>
this.showNotifications(e),
this.showNotifications(e)
);
// Settings button
@ -189,7 +189,7 @@ export class TopNavigation extends Component {
});
setIcon(settingsBtn, "settings");
this.registerDomEvent(settingsBtn, "click", () =>
this.onSettingsClick(),
this.onSettingsClick()
);
}
@ -197,7 +197,7 @@ export class TopNavigation extends Component {
container: HTMLElement,
mode: ViewMode,
icon: string,
label: string,
label: string
) {
const tab = container.createEl("button", {
cls: ["fluent-view-tab", "clickable-icon"],
@ -228,7 +228,7 @@ export class TopNavigation extends Component {
});
const activeTab = this.containerEl.querySelector(
`[data-mode="${mode}"]`,
`[data-mode="${mode}"]`
);
if (activeTab) {
activeTab.addClass("is-active");
@ -278,7 +278,7 @@ export class TopNavigation extends Component {
} else {
menu.addItem((item) => {
item.setTitle(
`${overdueTasks.length} overdue tasks`,
`${overdueTasks.length} overdue tasks`
).setDisabled(true);
});
@ -312,7 +312,7 @@ export class TopNavigation extends Component {
private updateNotificationBadge() {
const badge = this.containerEl.querySelector(
".fluent-notification-badge",
".fluent-notification-badge"
);
if (badge instanceof HTMLElement) {
badge.textContent = String(this.notificationCount);
@ -358,13 +358,14 @@ export class TopNavigation extends Component {
this.viewTabsContainer,
mode,
config.icon,
config.label,
config.label
);
}
}
}
public updateAvailableModes(modes: ViewMode[]) {
const wasEmpty = this.availableModes.length === 0;
this.availableModes = modes;
// Hide entire navigation if no modes available
@ -373,6 +374,12 @@ export class TopNavigation extends Component {
return;
}
// If transitioning from empty to non-empty, need to re-render entire UI
if (wasEmpty && modes.length > 0) {
this.render();
return;
}
// Show navigation when modes are available
this.containerEl.show();
@ -385,7 +392,7 @@ export class TopNavigation extends Component {
// Update center section visibility (this should always be visible now since we handle empty modes above)
const centerSection = this.containerEl.querySelector(
".fluent-nav-center",
".fluent-nav-center"
) as HTMLElement;
if (centerSection) {
centerSection.show();

View file

@ -38,7 +38,8 @@ const VIEW_MODE_CONFIG: Record<string, ViewMode[]> = {
today: ["list", "tree", "kanban", "calendar"],
upcoming: ["list", "tree", "kanban", "calendar"],
flagged: ["list", "tree", "kanban", "calendar"],
projects: ["list", "tree", "kanban", "calendar"],
// Projects: empty by default (overview mode), but FluentTaskView enables modes when a project is selected
projects: [],
// Specialized views with limited or no modes
tags: [],
@ -92,7 +93,7 @@ export class FluentComponentManager extends Component {
onTaskContextMenu: (event: MouseEvent, task: Task) => void;
onKanbanTaskStatusUpdate: (
taskId: string,
newStatusMark: string,
newStatusMark: string
) => void;
};
@ -106,15 +107,15 @@ export class FluentComponentManager extends Component {
onTaskCompleted: (task: Task) => void;
onTaskUpdate: (
originalTask: Task,
updatedTask: Task,
updatedTask: Task
) => Promise<void>;
onTaskContextMenu: (event: MouseEvent, task: Task) => void;
onKanbanTaskStatusUpdate: (
taskId: string,
newStatusMark: string,
newStatusMark: string
) => void;
},
private selectionManager?: TaskSelectionManager,
private selectionManager?: TaskSelectionManager
) {
super();
this.viewHandlers = viewHandlers;
@ -144,7 +145,7 @@ export class FluentComponentManager extends Component {
this.app,
this.plugin,
this.contentArea,
viewHandlers,
viewHandlers
);
this.parentView.addChild(this.viewComponentManager);
@ -165,7 +166,7 @@ export class FluentComponentManager extends Component {
if (task) this.viewHandlers.onTaskContextMenu(event, task);
},
selectionManager: this.selectionManager,
},
}
);
this.parentView.addChild(this.contentComponent);
this.contentComponent.load();
@ -186,14 +187,14 @@ export class FluentComponentManager extends Component {
if (originalTask && updatedTask) {
await this.viewHandlers.onTaskUpdate(
originalTask,
updatedTask,
updatedTask
);
}
},
onTaskContextMenu: (event, task) => {
if (task) this.viewHandlers.onTaskContextMenu(event, task);
},
},
}
);
this.parentView.addChild(this.forecastComponent);
this.forecastComponent.load();
@ -214,14 +215,14 @@ export class FluentComponentManager extends Component {
if (originalTask && updatedTask) {
await this.viewHandlers.onTaskUpdate(
originalTask,
updatedTask,
updatedTask
);
}
},
onTaskContextMenu: (event, task) => {
if (task) this.viewHandlers.onTaskContextMenu(event, task);
},
},
}
);
this.parentView.addChild(this.tagsComponent);
this.tagsComponent.load();
@ -242,14 +243,14 @@ export class FluentComponentManager extends Component {
if (originalTask && updatedTask) {
await this.viewHandlers.onTaskUpdate(
originalTask,
updatedTask,
updatedTask
);
}
},
onTaskContextMenu: (event, task) => {
if (task) this.viewHandlers.onTaskContextMenu(event, task);
},
},
}
);
this.parentView.addChild(this.projectsComponent);
this.projectsComponent.load();
@ -270,14 +271,14 @@ export class FluentComponentManager extends Component {
if (originalTask && updatedTask) {
await this.viewHandlers.onTaskUpdate(
originalTask,
updatedTask,
updatedTask
);
}
},
onTaskContextMenu: (event, task) => {
if (task) this.viewHandlers.onTaskContextMenu(event, task);
},
},
}
);
this.parentView.addChild(this.reviewComponent);
this.reviewComponent.load();
@ -303,7 +304,7 @@ export class FluentComponentManager extends Component {
onEventContextMenu: (ev: MouseEvent, event: Task) => {
if (event) this.viewHandlers.onTaskContextMenu(ev, event);
},
},
}
);
this.parentView.addChild(this.calendarComponent);
@ -317,7 +318,7 @@ export class FluentComponentManager extends Component {
onTaskStatusUpdate: async (taskId, newStatusMark) => {
this.viewHandlers.onKanbanTaskStatusUpdate(
taskId,
newStatusMark,
newStatusMark
);
},
onTaskSelected: (task) => {
@ -329,7 +330,7 @@ export class FluentComponentManager extends Component {
onTaskContextMenu: (event, task) => {
if (task) this.viewHandlers.onTaskContextMenu(event, task);
},
},
}
);
this.parentView.addChild(this.kanbanComponent);
@ -344,7 +345,7 @@ export class FluentComponentManager extends Component {
this.viewHandlers.onTaskCompleted(task),
onTaskContextMenu: (event: MouseEvent, task: Task) =>
this.viewHandlers.onTaskContextMenu(event, task),
},
}
);
this.parentView.addChild(this.ganttComponent);
this.ganttComponent.load();
@ -375,7 +376,7 @@ export class FluentComponentManager extends Component {
// Smart hiding - only hide currently visible component (unless initial hide)
if (!isInitialHide && this.currentVisibleComponent) {
console.log(
"[FluentComponent] Smart hide - only hiding current visible component",
"[FluentComponent] Smart hide - only hiding current visible component"
);
this.currentVisibleComponent.containerEl?.hide();
this.currentVisibleComponent = null;
@ -383,7 +384,7 @@ export class FluentComponentManager extends Component {
// Hide all components
console.log(
"[FluentComponent] Hiding all components",
isInitialHide ? "(initial hide)" : "",
isInitialHide ? "(initial hide)" : ""
);
this.contentComponent?.containerEl.hide();
this.forecastComponent?.containerEl.hide();
@ -416,20 +417,20 @@ export class FluentComponentManager extends Component {
filteredTasks: Task[],
currentFilterState: RootFilterState | null,
viewMode: ViewMode,
project?: string | null,
project?: string | null
): void {
console.log(
"[FluentComponent] switchView called with:",
viewId,
"viewMode:",
viewMode,
viewMode
);
// Remove transient overlays (loading/error/empty) before showing components
if (this.contentArea) {
this.contentArea
.querySelectorAll(
".tg-fluent-loading, .tg-fluent-error-state, .tg-fluent-empty-state",
".tg-fluent-loading, .tg-fluent-error-state, .tg-fluent-empty-state"
)
.forEach((el) => el.remove());
}
@ -439,38 +440,24 @@ export class FluentComponentManager extends Component {
this.hideAllComponents();
// Check if current view supports multiple view modes and we're in a non-list mode
const viewModes = this.getAvailableModesForView(viewId);
const viewModes = this.getAvailableModesForView(viewId, project);
// If the current view mode is not available for this view, reset to first available or list
if (viewModes.length > 0 && !viewModes.includes(viewMode)) {
viewMode = viewModes[0];
}
console.log(
"[FluentComponent] Is content-based view?",
this.isContentBasedView(viewId),
"View mode:",
viewMode,
"Available modes:",
viewModes,
);
if (
this.isContentBasedView(viewId) &&
viewModes.length > 0 &&
viewMode !== "list" &&
viewMode !== "tree"
) {
// For content-based views in kanban/calendar mode, use special rendering
console.log(
"[FluentComponent] Using renderContentWithViewMode for non-list/tree mode:",
viewMode,
);
this.renderContentWithViewMode(
viewId,
tasks,
filteredTasks,
viewMode,
viewMode
);
return;
}
@ -491,7 +478,7 @@ export class FluentComponentManager extends Component {
this.app,
this.plugin,
twoColumnConfig,
viewId,
viewId
);
this.parentView.addChild(twoColumnComponent);
@ -509,7 +496,7 @@ export class FluentComponentManager extends Component {
const twoColumnComponent = this.twoColumnViewComponents.get(viewId);
if (!twoColumnComponent) {
console.warn(
`[FluentComponent] Missing two column component for view ${viewId}`,
`[FluentComponent] Missing two column component for view ${viewId}`
);
return;
}
@ -521,7 +508,7 @@ export class FluentComponentManager extends Component {
// Check if it's a special view managed by ViewComponentManager
if (this.viewComponentManager.isSpecialView(viewId)) {
targetComponent = this.viewComponentManager.showComponent(
viewId,
viewId
) as ManagedViewComponent | null;
} else if (
specificViewType === "forecast" ||
@ -543,13 +530,13 @@ export class FluentComponentManager extends Component {
// 2. Without project selection (from view navigation) → use ProjectsComponent for overview
if (project) {
console.log(
"[FluentComponent] Projects view with selected project - using ContentComponent",
"[FluentComponent] Projects view with selected project - using ContentComponent"
);
targetComponent = this.contentComponent;
modeForComponent = viewId;
} else {
console.log(
"[FluentComponent] Projects view without selection - using ProjectsComponent",
"[FluentComponent] Projects view without selection - using ProjectsComponent"
);
targetComponent = this.projectsComponent;
modeForComponent = viewId;
@ -582,13 +569,13 @@ export class FluentComponentManager extends Component {
console.log(
"[FluentComponent] Target component determined:",
targetComponent?.constructor?.name,
targetComponent?.constructor?.name
);
if (targetComponent) {
console.log(
`[FluentComponent] Activating component for view ${viewId}:`,
targetComponent.constructor.name,
targetComponent.constructor.name
);
targetComponent.containerEl.show();
this.currentVisibleComponent = targetComponent;
@ -596,7 +583,7 @@ export class FluentComponentManager extends Component {
// Set view mode first for ContentComponent
if (typeof targetComponent.setViewMode === "function") {
console.log(
`[FluentComponent] Setting view mode for ${viewId} to ${modeForComponent}`,
`[FluentComponent] Setting view mode for ${viewId} to ${modeForComponent}`
);
targetComponent.setViewMode(modeForComponent as any, project);
}
@ -608,7 +595,7 @@ export class FluentComponentManager extends Component {
if (viewId === "review" || viewId === "tags") {
console.log(
`[FluentComponent] Calling setTasks for ${viewId} with ALL tasks:`,
tasks.length,
tasks.length
);
targetComponent.setTasks(tasks);
} else if (viewId === "projects" && !project) {
@ -616,7 +603,7 @@ export class FluentComponentManager extends Component {
// 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})`,
`[FluentComponent] Calling setTasks for projects with ALL tasks (${tasks.length}) and FILTERED tasks (${filteredTasks.length})`
);
targetComponent.setTasks(tasks, filteredTasks);
} else {
@ -625,14 +612,14 @@ export class FluentComponentManager extends Component {
// Forecast view: remove badge-only items
if (viewId === "forecast") {
filteredTasksLocal = filteredTasksLocal.filter(
(task) => !(task as any).badge,
(task) => !(task as any).badge
);
}
console.log(
"[FluentComponent] Calling setTasks with filtered:",
filteredTasksLocal.length,
"all:",
tasks.length,
tasks.length
);
targetComponent.setTasks(filteredTasksLocal, tasks);
}
@ -654,8 +641,8 @@ export class FluentComponentManager extends Component {
tasks,
viewId as any,
this.plugin,
filterOptions,
),
filterOptions
)
);
}
@ -668,7 +655,7 @@ export class FluentComponentManager extends Component {
}
} else {
console.warn(
`[FluentComponent] No target component found for viewId: ${viewId}`,
`[FluentComponent] No target component found for viewId: ${viewId}`
);
}
}
@ -680,11 +667,11 @@ export class FluentComponentManager extends Component {
viewId: string,
tasks: Task[],
filteredTasks: Task[],
viewMode: ViewMode,
viewMode: ViewMode
): void {
console.log(
"[FluentComponent] renderContentWithViewMode called, viewMode:",
viewMode,
viewMode
);
// Hide current component
@ -703,7 +690,7 @@ export class FluentComponentManager extends Component {
console.log(
"[FluentComponent] Setting tasks to ContentComponent, filtered:",
filteredTasks.length,
filteredTasks.length
);
this.contentComponent.setTasks(filteredTasks, tasks);
this.currentVisibleComponent = this.contentComponent;
@ -718,7 +705,7 @@ export class FluentComponentManager extends Component {
console.log(
"[FluentComponent] Setting",
filteredTasks.length,
"tasks to kanban",
"tasks to kanban"
);
this.kanbanComponent.setTasks(filteredTasks);
this.currentVisibleComponent = this.kanbanComponent;
@ -727,11 +714,11 @@ export class FluentComponentManager extends Component {
case "calendar":
// Use CalendarComponent
console.log(
"[FluentComponent] Calendar mode in renderContentWithViewMode",
"[FluentComponent] Calendar mode in renderContentWithViewMode"
);
if (!this.calendarComponent) {
console.log(
"[FluentComponent] No calendar component available!",
"[FluentComponent] No calendar component available!"
);
return;
}
@ -742,7 +729,7 @@ export class FluentComponentManager extends Component {
console.log(
"[FluentComponent] Setting",
filteredTasks.length,
"tasks to calendar",
"tasks to calendar"
);
this.calendarComponent.setTasks(filteredTasks);
this.currentVisibleComponent = this.calendarComponent;
@ -758,7 +745,7 @@ export class FluentComponentManager extends Component {
viewId: string,
tasks: Task[],
filteredTasks: Task[],
viewMode: ViewMode,
viewMode: ViewMode
): void {
// Content-based views (list/tree/kanban/calendar)
if (this.isContentBasedView(viewId)) {
@ -775,7 +762,7 @@ export class FluentComponentManager extends Component {
this.contentComponent?.setTasks?.(
filteredTasks,
tasks,
true,
true
);
break;
}
@ -904,7 +891,7 @@ export class FluentComponentManager extends Component {
emptyEl.createDiv({
cls: "tg-fluent-empty-description",
text: t(
"Create your first task to get started with Task Genius",
"Create your first task to get started with Task Genius"
),
});
@ -936,11 +923,14 @@ export class FluentComponentManager extends Component {
/**
* Get available view modes for a specific view
*/
getAvailableModesForView(viewId: string): ViewMode[] {
getAvailableModesForView(
viewId: string,
selectedProject?: string | null
): ViewMode[] {
// Check for special two-column views
const viewConfig = getViewSettingOrDefault(
this.plugin,
viewId as ViewMode,
viewId as ViewMode
);
if (viewConfig?.specificConfig?.viewType === "twocolumn") {
return [];
@ -951,6 +941,12 @@ export class FluentComponentManager extends Component {
return [];
}
// Special handling for projects view
// When a project is selected, enable all view modes (uses ContentComponent)
if (viewId === "projects" && selectedProject) {
return ["list", "tree", "kanban", "calendar"];
}
// Return the configured modes for the view, or empty array
return VIEW_MODE_CONFIG[viewId] || [];
}

View file

@ -140,7 +140,7 @@ export class FluentTaskView extends ItemView {
updateWorkspaceLeafWidth = debounce(() => {
this.app.workspace.trigger(
"task-genius:leaf-width-updated",
this.leaf.width,
this.leaf.width
);
}, 200);
@ -190,7 +190,7 @@ export class FluentTaskView extends ItemView {
} catch (e) {
console.warn(
"[TG] Failed to sync selectedProject from filter state",
e,
e
);
}
@ -202,7 +202,7 @@ export class FluentTaskView extends ItemView {
this.filteredTasks = this.dataManager.applyFilters(this.tasks);
this.updateView();
},
150,
150
);
onResize(): void {
@ -211,7 +211,10 @@ export class FluentTaskView extends ItemView {
private ensureViewModeForView(viewId: string): ViewMode {
const availableModes =
this.componentManager?.getAvailableModesForView(viewId) ?? [];
this.componentManager?.getAvailableModesForView(
viewId,
this.viewState.selectedProject
) ?? [];
const storedMode = this.viewState.viewModeByViewId?.[viewId];
const pickFallback = (): ViewMode => {
@ -240,7 +243,7 @@ export class FluentTaskView extends ItemView {
const prefersTree = getInitialViewMode(
this.app,
this.plugin,
viewId,
viewId
);
if (prefersTree && availableModes.includes("tree")) {
return "tree";
@ -258,8 +261,10 @@ export class FluentTaskView extends ItemView {
this.viewState.viewModeByViewId = {};
}
const availableModes =
this.componentManager?.getAvailableModesForView(viewId);
const availableModes = this.componentManager?.getAvailableModesForView(
viewId,
this.viewState.selectedProject
);
if (
availableModes &&
availableModes.length > 0 &&
@ -284,14 +289,14 @@ export class FluentTaskView extends ItemView {
// ====================
if (this.DEBUG_MODE) {
console.log(
"[TG] Initializing UI, managers, structure, and events...",
"[TG] Initializing UI, managers, structure, and events..."
);
}
this.contentEl.empty();
this.contentEl.toggleClass(
["task-genius-fluent-view", "task-genius-view"],
true,
true
);
// Create root container (use exact same class as original)
@ -315,7 +320,7 @@ export class FluentTaskView extends ItemView {
if (this.DEBUG_MODE) {
console.log(
"[TG] ✅ UI, managers, structure, and events initialized",
"[TG] ✅ UI, managers, structure, and events initialized"
);
}
@ -333,7 +338,7 @@ export class FluentTaskView extends ItemView {
this.viewState.currentWorkspace = savedWorkspaceId;
if (this.DEBUG_MODE) {
console.log(
`[TG] Restored workspace ID: ${savedWorkspaceId}`,
`[TG] Restored workspace ID: ${savedWorkspaceId}`
);
}
}
@ -350,7 +355,7 @@ export class FluentTaskView extends ItemView {
this.liveFilterState = state;
this.app.saveLocalStorage(
"task-genius-view-filter",
state || null,
state || null
);
},
setCurrentFilterState: (state) => {
@ -368,13 +373,14 @@ export class FluentTaskView extends ItemView {
const availableModes =
this.componentManager?.getAvailableModesForView(
this.currentViewId,
selectedProject
) ?? [];
if (
availableModes.length > 0 &&
!availableModes.includes(viewMode)
) {
return this.ensureViewModeForView(
this.currentViewId,
this.currentViewId
);
}
return viewMode;
@ -382,7 +388,7 @@ export class FluentTaskView extends ItemView {
this.viewState.viewMode = normalizedMode;
this.recordViewModeForView(
this.currentViewId,
normalizedMode,
normalizedMode
);
this.topNavigation?.setViewMode(normalizedMode);
if (clearSearch) {
@ -402,7 +408,7 @@ export class FluentTaskView extends ItemView {
await this.dataManager.loadTasks(false); // Will trigger onTasksLoaded callback
await this.dataManager.registerDataflowListeners();
console.log(
`[TG] ✅ Loaded ${this.tasks.length} tasks, ${this.filteredTasks.length} after filters`,
`[TG] ✅ Loaded ${this.tasks.length} tasks, ${this.filteredTasks.length} after filters`
);
// ====================
@ -427,7 +433,7 @@ export class FluentTaskView extends ItemView {
// ====================
if (this.DEBUG_MODE) {
console.log(
`[TG] Finalizing (isInitializing was ${this.isInitializing})`,
`[TG] Finalizing (isInitializing was ${this.isInitializing})`
);
}
@ -457,7 +463,7 @@ export class FluentTaskView extends ItemView {
searchQuery: this.viewState.searchQuery || "",
filterInputValue: this.viewState.filterInputValue || "",
}),
() => this.isInitializing,
() => this.isInitializing
);
this.dataManager.setCallbacks({
onTasksLoaded: (tasks, error) => {
@ -473,7 +479,7 @@ export class FluentTaskView extends ItemView {
this.isLoading = false;
// Apply filters immediately after loading
this.filteredTasks = this.dataManager.applyFilters(
this.tasks,
this.tasks
);
this.updateView();
}
@ -498,7 +504,7 @@ export class FluentTaskView extends ItemView {
this.app,
this.plugin,
() => this.workspaceId,
() => this.useSideLeaves(),
() => this.useSideLeaves()
);
this.actionHandlers.setCallbacks({
onTaskSelectionChanged: (task) => {
@ -514,7 +520,7 @@ export class FluentTaskView extends ItemView {
this.tasks[index] = updatedTask;
// Re-apply filters
this.filteredTasks = this.dataManager.applyFilters(
this.tasks,
this.tasks
);
this.updateView();
}
@ -545,7 +551,7 @@ export class FluentTaskView extends ItemView {
onNavigateToView: (viewId) => {
this.recordViewModeForView(
this.currentViewId,
this.viewState.viewMode,
this.viewState.viewMode
);
this.currentViewId = viewId;
@ -553,7 +559,7 @@ export class FluentTaskView extends ItemView {
// This enables the full project overview mode
if (viewId === "projects") {
console.log(
"[TG] Navigating to projects overview - clearing project selection",
"[TG] Navigating to projects overview - clearing project selection"
);
this.viewState.selectedProject = undefined;
@ -567,25 +573,25 @@ export class FluentTaskView extends ItemView {
.map((g: any) => ({
...g,
filters: (g.filters || []).filter(
(f: any) => f.property !== "project",
(f: any) => f.property !== "project"
),
}))
.filter(
(g: any) =>
g.filters && g.filters.length > 0,
g.filters && g.filters.length > 0
);
this.liveFilterState = nextState as any;
this.currentFilterState = nextState as any;
this.app.saveLocalStorage(
"task-genius-view-filter",
nextState,
nextState
);
// Broadcast filter change
this.app.workspace.trigger(
"task-genius:filter-changed",
nextState,
nextState
);
}
} catch (e) {
@ -593,6 +599,9 @@ export class FluentTaskView extends ItemView {
}
}
// Recompute filtered tasks for the newly selected view so all modes stay in sync
this.filteredTasks = this.dataManager.applyFilters(this.tasks);
const nextMode = this.ensureViewModeForView(viewId);
this.viewState.viewMode = nextMode;
this.recordViewModeForView(viewId, nextMode);
@ -613,16 +622,17 @@ export class FluentTaskView extends ItemView {
// Switch to projects view
this.recordViewModeForView(
this.currentViewId,
this.viewState.viewMode,
this.viewState.viewMode
);
this.currentViewId = "projects";
this.filteredTasks = this.dataManager.applyFilters(this.tasks);
const projectsViewMode = this.ensureViewModeForView(
this.currentViewId,
this.currentViewId
);
this.viewState.viewMode = projectsViewMode;
this.recordViewModeForView(
this.currentViewId,
projectsViewMode,
projectsViewMode
);
this.topNavigation?.setViewMode(projectsViewMode);
@ -639,7 +649,7 @@ export class FluentTaskView extends ItemView {
.map((g: any) => ({
...g,
filters: (g.filters || []).filter(
(f: any) => f.property !== "project",
(f: any) => f.property !== "project"
),
}))
.filter((g: any) => g.filters && g.filters.length > 0); // Remove empty groups
@ -665,23 +675,23 @@ export class FluentTaskView extends ItemView {
this.currentFilterState = nextState as any;
this.app.saveLocalStorage(
"task-genius-view-filter",
nextState,
nextState
);
// Broadcast so any open filter UI reacts and header button shows reset
// The filter-changed event listener will handle applyFilters and updateView
this.app.workspace.trigger(
"task-genius:filter-changed",
nextState,
nextState
);
} catch (e) {
console.warn(
"[TG] Failed to project-sync filter UI state",
e,
e
);
// If filter sync fails, still update the view
this.filteredTasks = this.dataManager.applyFilters(
this.tasks,
this.tasks
);
this.updateView();
}
@ -719,7 +729,7 @@ export class FluentTaskView extends ItemView {
viewMode: this.viewState.viewMode,
}),
() => this.currentFilterState,
() => this.liveFilterState,
() => this.liveFilterState
);
this.addChild(this.workspaceStateManager);
@ -727,7 +737,7 @@ export class FluentTaskView extends ItemView {
this.selectionManager = new TaskSelectionManager(
this.app,
this.plugin,
this,
this
);
this.addChild(this.selectionManager);
@ -795,7 +805,7 @@ export class FluentTaskView extends ItemView {
this.rootContainerEl,
this.headerEl, // Obsidian's view header
this.titleEl, // Obsidian's view title element
() => this.filteredTasks.length,
() => this.filteredTasks.length
);
this.layoutManager.setOnSidebarNavigate((viewId) => {
this.actionHandlers.handleNavigate(viewId);
@ -813,7 +823,7 @@ export class FluentTaskView extends ItemView {
onTaskUpdate: async (originalTask, updatedTask) => {
await this.actionHandlers.handleTaskUpdate(
originalTask,
updatedTask,
updatedTask
);
},
});
@ -833,12 +843,11 @@ export class FluentTaskView extends ItemView {
} else {
sidebarEl.hide();
console.log(
"[TG] Using workspace side leaves: skip in-view sidebar",
"[TG] Using workspace side leaves: skip in-view sidebar"
);
}
// Create top navigation
console.log("[TG] Initializing top navigation");
// Initialize with empty modes first, will be updated in updateView
this.topNavigation = new TopNavigation(
topNavEl,
this.plugin,
@ -852,6 +861,7 @@ export class FluentTaskView extends ItemView {
// Sort click callback (future extension point)
},
() => this.actionHandlers.handleSettingsClick(),
[] // Start with empty modes, will be set by updateView
);
this.addChild(this.topNavigation);
@ -872,7 +882,7 @@ export class FluentTaskView extends ItemView {
onTaskUpdate: async (originalTask, updatedTask) => {
await this.actionHandlers.handleTaskUpdate(
originalTask,
updatedTask,
updatedTask
);
},
onTaskContextMenu: (event, task) => {
@ -883,12 +893,12 @@ export class FluentTaskView extends ItemView {
if (task) {
this.actionHandlers.handleKanbanTaskStatusUpdate(
task,
newStatusMark,
newStatusMark
);
}
},
},
this.selectionManager,
this.selectionManager
);
this.addChild(this.componentManager);
this.componentManager.initializeViewComponents();
@ -937,7 +947,7 @@ export class FluentTaskView extends ItemView {
this.workspaceStateManager.captureFilterStateSnapshot();
if (snapshot) {
await this.workspaceStateManager.saveFilterStateImmediately(
snapshot,
snapshot
);
}
@ -959,7 +969,7 @@ export class FluentTaskView extends ItemView {
this.liveFilterState = state;
this.app.saveLocalStorage(
"task-genius-view-filter",
state || null,
state || null
);
},
setCurrentFilterState: (state) => {
@ -978,13 +988,14 @@ export class FluentTaskView extends ItemView {
const availableModes =
this.componentManager?.getAvailableModesForView(
this.currentViewId,
selectedProject
) ?? [];
if (
availableModes.length > 0 &&
!availableModes.includes(viewMode)
) {
return this.ensureViewModeForView(
this.currentViewId,
this.currentViewId
);
}
return viewMode;
@ -992,7 +1003,7 @@ export class FluentTaskView extends ItemView {
this.viewState.viewMode = normalizedMode;
this.recordViewModeForView(
this.currentViewId,
normalizedMode,
normalizedMode
);
this.topNavigation?.setViewMode(normalizedMode);
if (clearSearch) {
@ -1008,7 +1019,7 @@ export class FluentTaskView extends ItemView {
// Reload tasks
await this.dataManager.loadTasks();
}
}),
})
);
// Workspace overrides saved event
@ -1018,7 +1029,7 @@ export class FluentTaskView extends ItemView {
await this.workspaceStateManager.applyWorkspaceSettings();
await this.dataManager.loadTasks();
}
}),
})
);
// Settings changed event (skip for filter state changes)
@ -1026,7 +1037,7 @@ export class FluentTaskView extends ItemView {
on(this.app, Events.SETTINGS_CHANGED, async () => {
// Reload on settings change (unless caused by filter state save)
await this.dataManager.loadTasks();
}),
})
);
}
@ -1036,8 +1047,8 @@ export class FluentTaskView extends ItemView {
"task-genius:filter-changed",
(filterState: RootFilterState, leafId?: string) => {
this.handleFilterChanged(filterState, leafId);
},
),
}
)
);
// Sidebar selection changed (when using side leaves)
@ -1050,6 +1061,9 @@ export class FluentTaskView extends ItemView {
payload.selectionId
) {
this.currentViewId = payload.selectionId;
this.filteredTasks = this.dataManager.applyFilters(
this.tasks
);
this.updateView();
}
if (
@ -1058,11 +1072,11 @@ export class FluentTaskView extends ItemView {
) {
// Use the full project selection logic via actionHandlers
this.actionHandlers.handleProjectSelect(
payload.selectionId || "",
payload.selectionId || ""
);
}
}
}),
})
);
}
@ -1092,7 +1106,7 @@ export class FluentTaskView extends ItemView {
`tasks=${this.tasks.length}, ` +
`filtered=${this.filteredTasks.length}, ` +
`isLoading=${this.isLoading}, ` +
`hasError=${!!this.loadError}`,
`hasError=${!!this.loadError}`
);
if (this.isInitializing) {
@ -1101,9 +1115,17 @@ export class FluentTaskView extends ItemView {
}
console.log(
`[TG] ▶️ Proceeding with view update for ${this.currentViewId}`,
`[TG] ▶️ Proceeding with view update for ${this.currentViewId}`
);
// Update top navigation available modes based on current view
const availableModes =
this.componentManager?.getAvailableModesForView(
this.currentViewId,
this.viewState.selectedProject
) ?? [];
this.topNavigation?.updateAvailableModes(availableModes);
// Update task count
this.layoutManager.updateTaskMark();
@ -1156,7 +1178,7 @@ export class FluentTaskView extends ItemView {
this.filteredTasks,
this.currentFilterState,
this.viewState.viewMode,
this.viewState.selectedProject,
this.viewState.selectedProject
);
}