feat(fluent): implement dual-mode projects view

Add support for two distinct projects view modes:
- Projects overview: displays all projects with ProjectsComponent, disables
  ProjectList to prevent conflicting navigation
- Single project: shows filtered tasks for selected project using
  ContentComponent, keeps ProjectList enabled

When navigating to projects view from Other Views menu, project selection
and filters are cleared to enable overview mode. ProjectList interaction
is controlled via CSS class and visual feedback.

Changes:
- FluentSidebar: remove projects exclusion, add setProjectListEnabled()
- ProjectList: add setEnabled() method for controlling interaction state
- FluentTaskView: clear selection on projects navigation, control list state
- FluentComponentManager: switch components based on project selection
- fluent-main.css: add disabled project list styles
This commit is contained in:
Quorafind 2025-10-20 16:38:50 +08:00
parent 8fdfc39912
commit 9aaabe716f
5 changed files with 110 additions and 6 deletions

View file

@ -337,8 +337,8 @@ export class FluentSidebar extends Component {
if (!Array.isArray(cfg)) return this.otherItems;
const primaryIds = new Set(this.primaryItems.map((i) => i.id));
// Exclude views that are represented elsewhere in the sidebar (e.g., Projects list)
const excludeIds = new Set<string>(["projects"]);
// No longer exclude projects view - users can access it from both Projects list and Other Views
const excludeIds = new Set<string>([]);
const seen = new Set<string>();
const items: FluentTaskNavigationItem[] = [];
@ -847,4 +847,13 @@ export class FluentSidebar extends Component {
this.workspaceSelector?.setWorkspace(workspaceId);
this.projectList?.refresh();
}
/**
* Enable or disable project list interaction
* Used when showing full projects overview to prevent conflicting navigation
*/
public setProjectListEnabled(enabled: boolean) {
if (!this.projectList) return;
this.projectList.setEnabled(enabled);
}
}

View file

@ -701,6 +701,18 @@ export class ProjectList extends Component {
this.loadProjects();
}
/**
* Enable or disable project list interaction
* Used when showing full projects overview to prevent conflicting navigation
*/
public setEnabled(enabled: boolean) {
if (enabled) {
this.containerEl.removeClass("tg-project-list-disabled");
} else {
this.containerEl.addClass("tg-project-list-disabled");
}
}
private handleAddProject(buttonEl: HTMLElement) {
// Clean up any existing popover
if (this.currentPopover) {

View file

@ -538,9 +538,22 @@ export class FluentComponentManager extends Component {
targetComponent = this.tagsComponent;
break;
case "projects":
// In V2, Projects is treated as a content-based view using global project filters
targetComponent = this.contentComponent;
modeForComponent = viewId;
// Two modes for projects view:
// 1. With project selection (from ProjectList) → use ContentComponent for filtered tasks
// 2. Without project selection (from view navigation) → use ProjectsComponent for overview
if (project) {
console.log(
"[FluentComponent] Projects view with selected project - using ContentComponent",
);
targetComponent = this.contentComponent;
modeForComponent = viewId;
} else {
console.log(
"[FluentComponent] Projects view without selection - using ProjectsComponent",
);
targetComponent = this.projectsComponent;
modeForComponent = viewId;
}
break;
case "review":
targetComponent = this.reviewComponent;
@ -591,7 +604,8 @@ 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)
if (viewId === "review" || viewId === "tags") {
// Projects overview mode (no project selected) needs all tasks to build project list
if (viewId === "review" || viewId === "tags" || (viewId === "projects" && !project)) {
console.log(
`[FluentComponent] Calling setTasks for ${viewId} with ALL tasks:`,
tasks.length,

View file

@ -473,6 +473,44 @@ export class FluentTaskView extends ItemView {
this.viewState.viewMode,
);
this.currentViewId = viewId;
// When navigating to projects view directly, clear project selection
// This enables the full project overview mode
if (viewId === "projects") {
console.log("[TG] Navigating to projects overview - clearing project selection");
this.viewState.selectedProject = undefined;
// Clear project filter from filter state
try {
if (this.liveFilterState) {
const nextState = { ...this.liveFilterState };
nextState.filterGroups = (nextState.filterGroups || [])
.map((g: any) => ({
...g,
filters: (g.filters || []).filter(
(f: any) => f.property !== "project",
),
}))
.filter((g: any) => g.filters && g.filters.length > 0);
this.liveFilterState = nextState as any;
this.currentFilterState = nextState as any;
this.app.saveLocalStorage(
"task-genius-view-filter",
nextState,
);
// Broadcast filter change
this.app.workspace.trigger(
"task-genius:filter-changed",
nextState,
);
}
} catch (e) {
console.warn("[TG] Failed to clear project filter", e);
}
}
const nextMode = this.ensureViewModeForView(viewId);
this.viewState.viewMode = nextMode;
this.recordViewModeForView(viewId, nextMode);
@ -1031,6 +1069,14 @@ export class FluentTaskView extends ItemView {
// Update sidebar active item
this.layoutManager.setSidebarActiveItem(this.currentViewId);
// Control project list interaction based on view state
// Disable project list when showing full projects overview (no project selected)
// Enable it when in other views or when a specific project is selected
const isProjectsOverview =
this.currentViewId === "projects" &&
!this.viewState.selectedProject;
this.layoutManager.sidebar?.setProjectListEnabled(!isProjectsOverview);
// Show loading state
if (this.isLoading) {
this.componentManager.renderLoadingState();

View file

@ -380,6 +380,29 @@ button.fluent-new-task-btn:hover {
background-color: var(--background-modifier-hover);
}
/* Disabled project list styles - used when showing full projects overview */
.tg-project-list-disabled {
pointer-events: none;
opacity: 0.5;
position: relative;
}
.tg-project-list-disabled::after {
content: "";
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: var(--background-modifier-cover);
opacity: 0.3;
z-index: 1;
}
.tg-project-list-disabled .fluent-project-item {
cursor: not-allowed;
}
/* Main Container */
.tg-fluent-main-container {
flex: 1;