taskgenius_taskgenius-plugin/src/experimental/v2/components/V2TopNavigation.ts
Quorafind 1e162c219a feat(fluent-nav): hide entire navigation when no view tabs are needed
- Add configurable availableModes support to TopNavigation
- Hide complete navigation bar when availableModes is empty
- Dynamically show/hide navigation based on view requirements
- Update rendering logic to be conditional on available modes

This improves UI clarity for special views that don't require tab-based navigation
2025-09-23 09:50:20 +08:00

296 lines
7.7 KiB
TypeScript

import { setIcon, Menu, Notice, SearchComponent } from "obsidian";
import TaskProgressBarPlugin from "@/index";
import { Task } from "@/types/task";
import { t } from "@/translations/helper";
export type ViewMode = "list" | "kanban" | "tree" | "calendar";
export class TopNavigation {
private containerEl: HTMLElement;
private plugin: TaskProgressBarPlugin;
private searchInput: HTMLInputElement;
private currentViewMode: ViewMode = "list";
private notificationCount = 0;
private availableModes: ViewMode[] = ["list", "kanban", "tree", "calendar"];
private viewTabsContainer: HTMLElement | null = null;
constructor(
containerEl: HTMLElement,
plugin: TaskProgressBarPlugin,
private onSearch: (query: string) => void,
private onViewModeChange: (mode: ViewMode) => void,
private onFilterClick: () => void,
private onSortClick: () => void,
private onSettingsClick: () => void,
availableModes?: ViewMode[]
) {
this.containerEl = containerEl;
this.plugin = plugin;
if (availableModes) {
this.availableModes = availableModes;
// Ensure current mode is valid
if (!this.availableModes.includes(this.currentViewMode)) {
this.currentViewMode = this.availableModes[0] || "list";
}
}
this.updateNotificationCount();
this.render();
}
private async updateNotificationCount() {
let tasks: Task[] = [];
if (this.plugin.dataflowOrchestrator) {
const queryAPI = this.plugin.dataflowOrchestrator.getQueryAPI();
tasks = await queryAPI.getAllTasks();
} else {
tasks = this.plugin.preloadedTasks || [];
}
const today = new Date();
today.setHours(0, 0, 0, 0);
this.notificationCount = tasks.filter((task: Task) => {
if (task.completed) return false;
const dueDate = task.metadata?.dueDate
? new Date(task.metadata.dueDate)
: null;
return dueDate && dueDate <= today;
}).length;
this.updateNotificationBadge();
}
private render() {
this.containerEl.empty();
this.containerEl.addClass("v2-top-navigation");
// Hide entire navigation if no view modes are available
if (this.availableModes.length === 0) {
this.containerEl.style.display = "none";
return;
}
// Show navigation when modes are available
this.containerEl.style.display = "";
// Left section - Search
const leftSection = this.containerEl.createDiv({ cls: "v2-nav-left" });
const searchContainer = leftSection.createDiv({
cls: "v2-search-container",
});
new SearchComponent(searchContainer)
.setPlaceholder(t("Search tasks, projects ..."))
.onChange((value) => {
this.onSearch(value);
});
// Center section - View mode tabs
const centerSection = this.containerEl.createDiv({
cls: "v2-nav-center",
});
// Render view tabs (we know modes are available at this point)
this.viewTabsContainer = centerSection.createDiv({ cls: "v2-view-tabs" });
this.renderViewTabs();
// Right section - Notifications and Settings
const rightSection = this.containerEl.createDiv({
cls: "v2-nav-right",
});
// Notification button
const notificationBtn = rightSection.createDiv({
cls: "v2-nav-icon-button",
});
setIcon(notificationBtn, "bell");
const badge = notificationBtn.createDiv({
cls: "v2-notification-badge",
text: String(this.notificationCount),
});
if (this.notificationCount === 0) {
badge.hide();
}
notificationBtn.addEventListener("click", (e) =>
this.showNotifications(e)
);
// Settings button
const settingsBtn = rightSection.createDiv({
cls: "v2-nav-icon-button",
});
setIcon(settingsBtn, "settings");
settingsBtn.addEventListener("click", () => this.onSettingsClick());
}
private createViewTab(
container: HTMLElement,
mode: ViewMode,
icon: string,
label: string
) {
const tab = container.createEl("button", {
cls: ["v2-view-tab", "clickable-icon"],
attr: { "data-mode": mode },
});
if (mode === this.currentViewMode) {
tab.addClass("is-active");
}
setIcon(tab.createDiv({ cls: "v2-view-tab-icon" }), icon);
tab.createSpan({ text: label });
tab.addEventListener("click", () => {
this.setViewMode(mode);
this.onViewModeChange(mode);
});
}
private setViewMode(mode: ViewMode) {
this.currentViewMode = mode;
this.containerEl.querySelectorAll(".v2-view-tab").forEach((tab) => {
tab.removeClass("is-active");
});
const activeTab = this.containerEl.querySelector(
`[data-mode="${mode}"]`
);
if (activeTab) {
activeTab.addClass("is-active");
}
}
private async showNotifications(event: MouseEvent) {
const menu = new Menu();
let tasks: Task[] = [];
if (this.plugin.dataflowOrchestrator) {
const queryAPI = this.plugin.dataflowOrchestrator.getQueryAPI();
tasks = await queryAPI.getAllTasks();
} else {
tasks = this.plugin.preloadedTasks || [];
}
const today = new Date();
today.setHours(0, 0, 0, 0);
const overdueTasks = tasks
.filter((task: Task) => {
if (task.completed) return false;
const dueDate = task.metadata?.dueDate
? new Date(task.metadata.dueDate)
: null;
return dueDate && dueDate <= today;
})
.slice(0, 10);
if (overdueTasks.length === 0) {
menu.addItem((item) => {
item.setTitle("No overdue tasks").setDisabled(true);
});
} else {
menu.addItem((item) => {
item.setTitle(
`${overdueTasks.length} overdue tasks`
).setDisabled(true);
});
menu.addSeparator();
overdueTasks.forEach((task) => {
menu.addItem((item) => {
item.setTitle(task.content || t("Untitled task"))
.setIcon("alert-circle")
.onClick(() => {
new Notice(t("Task: {{content}}", { content: task.content || "" }));
});
});
});
}
menu.showAtMouseEvent(event);
}
private updateNotificationBadge() {
const badge = this.containerEl.querySelector(".v2-notification-badge");
if (badge instanceof HTMLElement) {
badge.textContent = String(this.notificationCount);
if (this.notificationCount === 0) {
badge.hide();
} else {
badge.show();
}
}
}
public refresh() {
this.updateNotificationCount();
}
public getSearchQuery(): string {
return this.searchInput?.value || "";
}
public clearSearch() {
if (this.searchInput) {
this.searchInput.value = "";
this.onSearch("");
}
}
private renderViewTabs() {
if (!this.viewTabsContainer) return;
this.viewTabsContainer.empty();
const modeConfig: Record<ViewMode, { icon: string; label: string }> = {
list: { icon: "list", label: "List" },
kanban: { icon: "layout-grid", label: "Kanban" },
tree: { icon: "git-branch", label: "Tree" },
calendar: { icon: "calendar", label: "Calendar" },
};
for (const mode of this.availableModes) {
const config = modeConfig[mode];
if (config) {
this.createViewTab(this.viewTabsContainer, mode, config.icon, config.label);
}
}
}
public updateAvailableModes(modes: ViewMode[]) {
this.availableModes = modes;
// Hide entire navigation if no modes available
if (modes.length === 0) {
this.containerEl.style.display = "none";
return;
}
// Show navigation when modes are available
this.containerEl.style.display = "";
// If current mode is no longer available, switch to first available mode
if (!modes.includes(this.currentViewMode)) {
this.currentViewMode = modes[0];
// Notify about the mode change
this.onViewModeChange(this.currentViewMode);
}
// Update center section visibility (this should always be visible now since we handle empty modes above)
const centerSection = this.containerEl.querySelector(".v2-nav-center") as HTMLElement;
if (centerSection) {
centerSection.style.display = "";
// Re-render the view tabs
if (!this.viewTabsContainer) {
this.viewTabsContainer = centerSection.createDiv({ cls: "v2-view-tabs" });
}
this.renderViewTabs();
}
}
public getCurrentViewMode(): ViewMode {
return this.currentViewMode;
}
}