refactor(fluent): improve view mode persistence and configuration

- Add per-view view mode persistence to remember mode for each navigation item
- Make workspace side leaves optional with default disabled
- Improve project filtering with normalized comparison logic
- Expose dataflow orchestrator early for event subscription
- Enhance type safety across fluent components
- Add missing event listeners for cache updates in ProjectList
This commit is contained in:
Quorafind 2025-10-19 23:24:11 +08:00
parent fc7600c93a
commit 2f2f59acf8
19 changed files with 517 additions and 226 deletions

View file

@ -1731,6 +1731,23 @@ export const DEFAULT_SETTINGS: TaskProgressBarSettings = {
caseSensitive: false,
},
},
fluentView: {
enableFluent: true,
workspaces: [
{
id: "default",
name: "Default",
color: "#3498db",
},
],
fluentConfig: {
enableWorkspaces: true,
defaultWorkspace: "default",
maxOtherViewsBeforeOverflow: 5,
},
useWorkspaceSideLeaves: false,
},
};
// Helper function to get view settings safely

View file

@ -35,15 +35,17 @@ export class FluentIntegration {
(leaf: WorkspaceLeaf) => new FluentTaskView(leaf, this.plugin),
);
if (this.plugin.settings.fluentView?.useWorkspaceSideLeaves) {
this.plugin.registerView(
TG_LEFT_SIDEBAR_VIEW_TYPE,
(leaf: WorkspaceLeaf) => new LeftSidebarView(leaf, this.plugin),
);
this.plugin.registerView(
TG_RIGHT_DETAIL_VIEW_TYPE,
(leaf: WorkspaceLeaf) => new RightDetailView(leaf, this.plugin),
);
}
// Register side leaf views for new architecture
this.plugin.registerView(
TG_LEFT_SIDEBAR_VIEW_TYPE,
(leaf: WorkspaceLeaf) => new LeftSidebarView(leaf, this.plugin),
);
this.plugin.registerView(
TG_RIGHT_DETAIL_VIEW_TYPE,
(leaf: WorkspaceLeaf) => new RightDetailView(leaf, this.plugin),
);
// When any of the fluent views becomes active, reveal the other side leaves without focusing them
this.plugin.registerEvent(
@ -102,7 +104,7 @@ export class FluentIntegration {
/**
* Open the Fluent view
*/
private async openV2View() {
private async openFluentView() {
const { workspace } = this.plugin.app;
// Check if Fluent view is already open
@ -131,7 +133,25 @@ export class FluentIntegration {
private async ensureSideLeavesIfEnabled() {
const useSideLeaves =
!!this.plugin.settings.fluentView?.useWorkspaceSideLeaves;
if (!useSideLeaves) return;
if (!useSideLeaves) {
const leftSidebarLeaves = this.plugin.app.workspace.getLeavesOfType(
TG_LEFT_SIDEBAR_VIEW_TYPE,
);
if (leftSidebarLeaves.length > 0) {
this.plugin.app.workspace.detachLeavesOfType(
TG_LEFT_SIDEBAR_VIEW_TYPE,
);
}
const rightSidebarLeaves =
this.plugin.app.workspace.getLeavesOfType(
TG_RIGHT_DETAIL_VIEW_TYPE,
);
if (rightSidebarLeaves.length > 0) {
this.plugin.app.workspace.detachLeavesOfType(
TG_RIGHT_DETAIL_VIEW_TYPE,
);
}
}
const ws = this.plugin.app.workspace as Workspace;
// Left sidebar
@ -161,15 +181,15 @@ export class FluentIntegration {
}
// Default workspace configuration
if (!this.plugin.settings.fluentView!.workspaces) {
this.plugin.settings.fluentView!.workspaces = [
if (!this.plugin.settings.fluentView.workspaces) {
this.plugin.settings.fluentView.workspaces = [
{ id: "default", name: t("Default"), color: "#3498db" },
];
}
// Default Fluent configuration
if (this.plugin.settings.fluentView!.fluentConfig === undefined) {
this.plugin.settings.fluentView!.fluentConfig = {
if (this.plugin.settings.fluentView.fluentConfig === undefined) {
this.plugin.settings.fluentView.fluentConfig = {
enableWorkspaces: true,
defaultWorkspace: "default",
maxOtherViewsBeforeOverflow: 5,
@ -177,9 +197,9 @@ export class FluentIntegration {
}
// Backfill extra experimental flag without touching types
const v2c = this.plugin.settings.fluentView;
if (v2c.useWorkspaceSideLeaves === undefined)
v2c.useWorkspaceSideLeaves = true;
const fluentSetting = this.plugin.settings.fluentView;
if (fluentSetting.useWorkspaceSideLeaves === undefined)
fluentSetting.useWorkspaceSideLeaves = false;
await this.plugin.saveSettings();
}
@ -204,13 +224,13 @@ export class FluentIntegration {
enableFluent: false,
};
}
this.plugin.settings.fluentView!.enableFluent =
!this.plugin.settings.fluentView!.enableFluent;
this.plugin.settings.fluentView.enableFluent =
!this.plugin.settings.fluentView.enableFluent;
await this.plugin.saveSettings();
// Open the appropriate view
if (this.plugin.settings.fluentView?.enableFluent) {
await this.openV2View();
await this.openFluentView();
} else {
// Open V1 view
const leaf = workspace.getLeaf("tab");

View file

@ -1,6 +1,17 @@
import { App, Component, setIcon, Menu, Notice, Modal, Platform } from "obsidian";
import {
App,
Component,
setIcon,
Menu,
Notice,
Modal,
Platform,
} from "obsidian";
import { WorkspaceSelector } from "./WorkspaceSelector";
import { ProjectList } from "@/components/features/fluent/components/ProjectList";
import {
Project,
ProjectList,
} from "@/components/features/fluent/components/ProjectList";
import { FluentTaskNavigationItem } from "@/types/fluent-types";
import { WorkspaceData } from "@/types/workspace";
import {
@ -12,11 +23,7 @@ import TaskProgressBarPlugin from "@/index";
import { t } from "@/translations/helper";
import { ViewConfigModal } from "@/components/features/task/view/modals/ViewConfigModal";
import { TASK_SPECIFIC_VIEW_TYPE } from "@/pages/TaskSpecificView";
import {
ViewConfig,
ViewFilterRule,
ViewMode,
} from "@/common/setting-definition";
import { ViewConfig, ViewFilterRule } from "@/common/setting-definition";
export class FluentSidebar extends Component {
private containerEl: HTMLElement;
@ -46,22 +53,7 @@ export class FluentSidebar extends Component {
{ id: "flagged", label: t("Flagged"), icon: "flag", type: "primary" },
];
private otherItems: FluentTaskNavigationItem[] = [
{
id: "calendar",
label: t("Calendar"),
icon: "calendar",
type: "other",
},
{ id: "gantt", label: t("Gantt"), icon: "git-branch", type: "other" },
{
id: "review",
label: t("Review"),
icon: "check-square",
type: "other",
},
{ id: "tags", label: t("Tags"), icon: "tag", type: "other" },
];
private otherItems: FluentTaskNavigationItem[] = [];
constructor(
containerEl: HTMLElement,
@ -111,9 +103,9 @@ export class FluentSidebar extends Component {
// New Task Button
const newTaskBtn = header.createEl("button", {
cls: "fluent-new-task-btn",
text: t("New Task"),
});
setIcon(newTaskBtn.createDiv({ cls: "fluent-new-task-icon" }), "plus");
newTaskBtn.createDiv({ cls: "fluent-new-task-text" }, t("New Task"));
this.registerDomEvent(newTaskBtn, "click", () =>
this.onNavigate("new-task"),
);
@ -265,7 +257,7 @@ export class FluentSidebar extends Component {
});
if (remainingOther.length > 0) {
const moreBtn = this.railEl!.createDiv({
const moreBtn = this.railEl.createDiv({
cls: "fluent-rail-btn",
attr: { "aria-label": t("More views") },
});
@ -276,7 +268,7 @@ export class FluentSidebar extends Component {
}
// Projects menu button
const projBtn = this.railEl!.createDiv({
const projBtn = this.railEl.createDiv({
cls: "fluent-rail-btn",
attr: { "aria-label": t("Projects") },
});
@ -286,7 +278,7 @@ export class FluentSidebar extends Component {
);
// Add (New Task) button
const addBtn = this.railEl!.createDiv({
const addBtn = this.railEl.createDiv({
cls: "fluent-rail-btn",
attr: { "aria-label": t("New Task") },
});
@ -534,14 +526,14 @@ export class FluentSidebar extends Component {
private showProjectMenu(event: MouseEvent) {
// Try to use existing project list data; if missing, build a temporary one
let projects: any[] = [];
const anyList: any = this.projectList as any;
let projects: Project[] = [];
const anyList: ProjectList = this.projectList as ProjectList;
if (anyList && typeof anyList.getProjects === "function") {
projects = anyList.getProjects();
} else {
const temp = createDiv();
const tempList: any = new ProjectList(
temp as any,
const tempList: ProjectList = new ProjectList(
temp,
this.plugin,
this.onProjectSelect,
);
@ -555,7 +547,7 @@ export class FluentSidebar extends Component {
item.setTitle(p.name)
.setIcon("folder")
.onClick(() => {
this.onProjectSelect(p.id);
this.onProjectSelect(p.filterKey);
});
});
});

View file

@ -199,7 +199,7 @@ export class TopNavigation extends Component {
});
}
private setViewMode(mode: ViewMode) {
public setViewMode(mode: ViewMode) {
this.currentViewMode = mode;
this.containerEl.querySelectorAll(".fluent-view-tab").forEach((tab) => {

View file

@ -1,6 +1,7 @@
import { Component, Platform, setIcon, Menu, Modal, App } from "obsidian";
import TaskProgressBarPlugin from "@/index";
import { Task } from "@/types/task";
import { getEffectiveProject } from "@/utils/task/task-operations";
import {
ProjectPopover,
ProjectModal,
@ -9,10 +10,12 @@ import {
import type { CustomProject } from "@/common/setting-definition";
import { t } from "@/translations/helper";
import { onWorkspaceSwitched } from "@/components/features/fluent/events/ui-event";
import { Events, on } from "@/dataflow/events/Events";
interface Project {
export interface Project {
id: string;
name: string;
filterKey: string;
displayName?: string;
color: string;
taskCount: number;
@ -87,6 +90,23 @@ export class ProjectList extends Component {
this.refreshWithDelay();
}),
);
// Refresh when dataflow cache becomes ready or updates
this.registerEvent(
on(this.plugin.app, Events.CACHE_READY, () => {
this.refreshWithDelay();
}),
);
this.registerEvent(
on(this.plugin.app, Events.TASK_CACHE_UPDATED, () => {
this.refreshWithDelay();
}),
);
this.registerEvent(
on(this.plugin.app, Events.PROJECT_DATA_UPDATED, () => {
this.refreshWithDelay();
}),
);
}
private refreshTimeoutId: NodeJS.Timeout | null = null;
@ -132,23 +152,28 @@ export class ProjectList extends Component {
const projectMap = new Map<string, Project>();
tasks.forEach((task: Task) => {
const projectName = task.metadata?.project;
if (projectName) {
if (!projectMap.has(projectName)) {
// Convert dashes back to spaces for display
const displayName = projectName.replace(/-/g, " ");
projectMap.set(projectName, {
id: projectName,
name: projectName,
displayName: displayName,
color: this.generateColorForProject(projectName),
taskCount: 0,
});
}
const project = projectMap.get(projectName);
if (project) {
project.taskCount++;
}
const projectName = getEffectiveProject(task);
if (!projectName) {
return;
}
const projectId = projectName;
if (!projectMap.has(projectId)) {
// Convert dashes back to spaces for display to match legacy behaviour
const displayName = projectName.replace(/-/g, " ");
projectMap.set(projectId, {
id: projectId,
name: projectId,
filterKey: projectId,
displayName: displayName,
color: this.generateColorForProject(projectId),
taskCount: 0,
});
}
const project = projectMap.get(projectId);
if (project) {
project.taskCount++;
}
});
@ -272,6 +297,7 @@ export class ProjectList extends Component {
: {
id: currentPath,
name: currentPath,
filterKey: currentPath,
displayName: segment,
color: this.generateColorForProject(
currentPath,
@ -301,6 +327,7 @@ export class ProjectList extends Component {
} else if (isLeaf && node.project.isVirtual) {
// Update virtual node with actual project data
node.project = project;
node.project.filterKey = project.filterKey;
}
parentNode = node;
@ -513,19 +540,19 @@ export class ProjectList extends Component {
treeNode?: ProjectTreeNode,
) {
const projectItem = container.createDiv({
cls: "fluent-project-item",
attr: {
"data-project-id": project.id,
"data-level": String(level),
},
});
cls: "fluent-project-item",
attr: {
"data-project-id": project.filterKey,
"data-level": String(level),
},
});
// Add virtual class for styling
if (project.isVirtual) {
projectItem.addClass("is-virtual");
// Add virtual class for styling
if (project.isVirtual) {
projectItem.addClass("is-virtual");
}
if (this.activeProjectId === project.id) {
if (this.activeProjectId === project.filterKey) {
projectItem.addClass("is-active");
}
@ -610,8 +637,8 @@ export class ProjectList extends Component {
if (project.isVirtual && treeNode) {
this.selectVirtualNode(treeNode);
} else {
this.setActiveProject(project.id);
this.onProjectSelect(project.id);
this.setActiveProject(project.filterKey);
this.onProjectSelect(project.filterKey);
}
}
});
@ -634,7 +661,7 @@ export class ProjectList extends Component {
const projectIds: string[] = [];
const collectProjects = (n: ProjectTreeNode) => {
if (!n.project.isVirtual) {
projectIds.push(n.project.id);
projectIds.push(n.project.filterKey);
}
n.children.forEach((child) => collectProjects(child));
};
@ -769,6 +796,7 @@ export class ProjectList extends Component {
this.projects.push({
id: customProject.id,
name: customProject.name,
filterKey: customProject.name,
displayName:
customProject.displayName || customProject.name,
color: customProject.color,
@ -779,6 +807,7 @@ export class ProjectList extends Component {
} else {
// Update existing project with custom color
this.projects[existingIndex].id = customProject.id;
this.projects[existingIndex].filterKey = customProject.name;
this.projects[existingIndex].color = customProject.color;
this.projects[existingIndex].displayName =
customProject.displayName || customProject.name;
@ -1038,7 +1067,7 @@ export class ProjectList extends Component {
await this.plugin.saveSettings();
// If this was the active project, clear selection
if (this.activeProjectId === project.id) {
if (this.activeProjectId === project.filterKey) {
this.setActiveProject(null);
this.onProjectSelect("");
}

View file

@ -184,6 +184,9 @@ export class FluentDataManager extends Component {
const viewId = this.getCurrentViewId();
let result = [...tasks]; // Copy array to avoid mutation
const normalizeProjectId = (value?: string | null): string =>
(value ?? "").trim().toLowerCase();
// Status filter
if (filters.status && filters.status !== "all") {
switch (filters.status) {
@ -217,17 +220,28 @@ export class FluentDataManager extends Component {
// Project filter - Skip for Inbox view
if (filters.project && viewId !== "inbox") {
result = result.filter(
(task) => task.metadata?.project === filters.project,
);
const targetProject = normalizeProjectId(filters.project);
result = result.filter((task) => {
const directProject = normalizeProjectId(
task.metadata?.project,
);
const tgProject = normalizeProjectId(
task.metadata?.tgProject?.name,
);
return (
targetProject.length > 0 &&
(targetProject === directProject ||
targetProject === tgProject)
);
});
}
// Tags filter
if (filters.tags && filters.tags.length > 0) {
result = result.filter((task) => {
if (!task.metadata?.tags) return false;
return filters.tags!.some((tag: string) =>
task.metadata!.tags!.includes(tag),
return filters.tags.some((tag: string) =>
task.metadata.tags.includes(tag),
);
});
}
@ -239,7 +253,7 @@ export class FluentDataManager extends Component {
if (!task.metadata?.dueDate) return false;
return (
new Date(task.metadata.dueDate) >=
filters.dateRange!.start!
filters.dateRange.start
);
});
}
@ -247,8 +261,7 @@ export class FluentDataManager extends Component {
result = result.filter((task) => {
if (!task.metadata?.dueDate) return false;
return (
new Date(task.metadata.dueDate) <=
filters.dateRange!.end!
new Date(task.metadata.dueDate) <= filters.dateRange.end
);
});
}
@ -297,10 +310,7 @@ export class FluentDataManager extends Component {
}, 400);
// Register dataflow event listeners
if (
isDataflowEnabled(this.plugin) &&
this.plugin.dataflowOrchestrator
) {
if (isDataflowEnabled(this.plugin)) {
// Listen for batch operation start
this.registerEvent(
on(this.plugin.app, Events.BATCH_OPERATION_START, (payload) => {

View file

@ -5,6 +5,7 @@ import {
SelectableCard,
SelectableCardConfig,
} from "@/components/features/onboarding/ui/SelectableCard";
import { FluentViewSettings } from "@/common/setting-definition";
export function renderInterfaceSettingsTab(
settingTab: TaskProgressBarSettingTab,
@ -113,7 +114,7 @@ export function renderInterfaceSettingsTab(
// Use workspace side leaves for Sidebar & Details
new Setting(fluentSettingsContainer)
.setName(t("Use Workspace Side Leaves"))
.setName(t("Use workspace side leaves"))
.setDesc(
t(
"Use left/right workspace side leaves for Sidebar and Details. When enabled, the main fluent view won't render in-view sidebar or details.",
@ -122,7 +123,7 @@ export function renderInterfaceSettingsTab(
.addToggle((toggle) => {
const current =
settingTab.plugin.settings.fluentView
?.useWorkspaceSideLeaves ?? true;
?.useWorkspaceSideLeaves ?? false;
toggle.setValue(current).onChange(async (value) => {
if (!settingTab.plugin.settings.fluentView) {
settingTab.plugin.settings.fluentView = {
@ -137,7 +138,8 @@ export function renderInterfaceSettingsTab(
}
// Store via 'any' to avoid typing constraints for experimental backfill
(
settingTab.plugin.settings.fluentView as any
settingTab.plugin.settings
.fluentView as FluentViewSettings
).useWorkspaceSideLeaves = value;
await settingTab.plugin.saveSettings();
new Notice(t("Saved. Reopen the view to apply."));
@ -146,7 +148,7 @@ export function renderInterfaceSettingsTab(
// Max Other Views before overflow threshold
new Setting(fluentSettingsContainer)
.setName(t("Max Other Views before overflow"))
.setName(t("Max other views before overflow"))
.setDesc(
t(
"Number of 'Other Views' to show before grouping the rest into an overflow menu (ellipsis)",

View file

@ -3,7 +3,7 @@
* Manages multi-selection state for tasks and coordinates bulk operations
*/
import { App, Component, Workspace } from "obsidian";
import { App, Component, ItemView, Workspace } from "obsidian";
import { Task } from "@/types/task";
import {
SelectionState,
@ -23,6 +23,7 @@ export class TaskSelectionManager extends Component {
constructor(
private app: App,
private plugin: TaskProgressBarPlugin,
private view?: ItemView,
) {
super();
@ -211,7 +212,7 @@ export class TaskSelectionManager extends Component {
reason,
};
(this.app.workspace as any).trigger(
(this.app.workspace as Workspace).trigger(
"task-genius:selection-mode-changed",
eventData,
);

View file

@ -1119,12 +1119,12 @@ export class DataflowOrchestrator {
let inlineCandidates: string[] = Array.from(
this.suppressedInline,
).filter((p) =>
this.fileFilterManager!.shouldIncludePath(p, "inline"),
this.fileFilterManager?.shouldIncludePath(p, "inline"),
);
const fileTaskCandidates: string[] = Array.from(
this.suppressedFileTasks,
).filter((p) =>
this.fileFilterManager!.shouldIncludePath(p, "file"),
this.fileFilterManager?.shouldIncludePath(p, "file"),
);
// Fallback: if we have no suppressed inline candidates (e.g., previous session), derive from cache keys
@ -1142,7 +1142,7 @@ export class DataflowOrchestrator {
inlineCandidates = Array.from(union).filter(
(p) =>
!indexed.has(p) &&
this.fileFilterManager!.shouldIncludePath(
this.fileFilterManager?.shouldIncludePath(
p,
"inline",
),

View file

@ -28,6 +28,8 @@ export async function createDataflow(
plugin,
projectOptions
);
// Expose orchestrator immediately so early consumers can subscribe to events
plugin.dataflowOrchestrator = orchestrator;
console.log("[createDataflow] Initializing dataflow orchestrator...");
try {
@ -36,6 +38,9 @@ export async function createDataflow(
console.log(`[createDataflow] Dataflow orchestrator ready (took ${elapsed}ms)`);
} catch (error) {
console.error("[createDataflow] Failed to initialize orchestrator:", error);
if (plugin.dataflowOrchestrator === orchestrator) {
plugin.dataflowOrchestrator = undefined;
}
throw error;
}
@ -47,4 +52,4 @@ export async function createDataflow(
*/
export function isDataflowEnabled(plugin: TaskProgressBarPlugin): boolean {
return plugin.settings.enableIndexer ?? true;
}
}

View file

@ -333,11 +333,15 @@ export default class TaskProgressBarPlugin extends Plugin {
const deferSpecificLeaves = this.app.workspace.getLeavesOfType(
TASK_SPECIFIC_VIEW_TYPE,
);
[...deferWorkspaceLeaves, ...deferSpecificLeaves].forEach(
(leaf) => {
leaf.loadIfDeferred();
},
);
const deferTaskGeniusLeaves =
this.app.workspace.getLeavesOfType(FLUENT_TASK_VIEW);
[
...deferWorkspaceLeaves,
...deferSpecificLeaves,
...deferTaskGeniusLeaves,
].forEach((leaf) => {
leaf.loadIfDeferred();
});
// Initialize Task Genius Icon Manager
this.taskGeniusIconManager = new TaskGeniusIconManager(this);
this.addChild(this.taskGeniusIconManager);
@ -511,10 +515,7 @@ export default class TaskProgressBarPlugin extends Plugin {
this.registerOnboardingView();
this.registerView(
TASK_VIEW_TYPE,
(leaf) => new TaskView(leaf, this),
);
this.registerView(TASK_VIEW_TYPE, (leaf) => new TaskView(leaf, this));
this.registerView(
TASK_SPECIFIC_VIEW_TYPE,
@ -596,10 +597,7 @@ export default class TaskProgressBarPlugin extends Plugin {
}
const isBeta = targetVersion.toLowerCase().includes("beta");
void this.changelogManager.openChangelog(
targetVersion,
isBeta,
);
void this.changelogManager.openChangelog(targetVersion, isBeta);
},
});
}

View file

@ -19,6 +19,10 @@ import { Events, on } from "@/dataflow/events/Events";
import { RootFilterState } from "@/components/features/task/filter/ViewTaskFilter";
import { Platform } from "obsidian";
import { t } from "@/translations/helper";
import {
getInitialViewMode,
saveViewMode,
} from "@/utils/ui/view-mode-utils";
// Import managers
import { FluentDataManager } from "@/components/features/fluent/managers/FluentDataManager";
@ -79,6 +83,7 @@ export class FluentTaskView extends ItemView {
private viewState: FluentTaskViewState = {
currentWorkspace: "",
viewMode: "list",
viewModeByViewId: {},
searchQuery: "",
filters: {},
filterInputValue: "",
@ -135,6 +140,65 @@ export class FluentTaskView extends ItemView {
return !!this.plugin.settings.fluentView?.useWorkspaceSideLeaves;
}
private ensureViewModeForView(viewId: string): ViewMode {
const availableModes =
this.componentManager?.getAvailableModesForView(viewId) ?? [];
const storedMode = this.viewState.viewModeByViewId?.[viewId];
const pickFallback = (): ViewMode => {
if (availableModes.length === 0) {
return storedMode ?? "list";
}
if (availableModes.includes("list")) {
return "list";
}
return availableModes[0];
};
if (storedMode) {
if (
availableModes.length === 0 ||
availableModes.includes(storedMode)
) {
return storedMode;
}
}
if (availableModes.includes("tree") || availableModes.includes("list")) {
const prefersTree = getInitialViewMode(
this.app,
this.plugin,
viewId,
);
if (prefersTree && availableModes.includes("tree")) {
return "tree";
}
if (availableModes.includes("list")) {
return "list";
}
}
return pickFallback();
}
private recordViewModeForView(viewId: string, mode: ViewMode): void {
if (!this.viewState.viewModeByViewId) {
this.viewState.viewModeByViewId = {};
}
const availableModes =
this.componentManager?.getAvailableModesForView(viewId);
if (
availableModes &&
availableModes.length > 0 &&
!availableModes.includes(mode)
) {
return;
}
this.viewState.viewModeByViewId[viewId] = mode;
}
/**
* Main initialization method
*/
@ -225,7 +289,28 @@ export class FluentTaskView extends ItemView {
}) => {
this.viewState.filters = filters;
this.viewState.selectedProject = selectedProject;
this.viewState.viewMode = viewMode;
const normalizedMode =
(() => {
const availableModes =
this.componentManager?.getAvailableModesForView(
this.currentViewId,
) ?? [];
if (
availableModes.length > 0 &&
!availableModes.includes(viewMode)
) {
return this.ensureViewModeForView(
this.currentViewId,
);
}
return viewMode;
})();
this.viewState.viewMode = normalizedMode;
this.recordViewModeForView(
this.currentViewId,
normalizedMode,
);
this.topNavigation?.setViewMode(normalizedMode);
if (clearSearch) {
this.viewState.searchQuery = "";
this.viewState.filterInputValue = "";
@ -384,7 +469,15 @@ export class FluentTaskView extends ItemView {
this.updateView();
},
onNavigateToView: (viewId) => {
this.recordViewModeForView(
this.currentViewId,
this.viewState.viewMode,
);
this.currentViewId = viewId;
const nextMode = this.ensureViewModeForView(viewId);
this.viewState.viewMode = nextMode;
this.recordViewModeForView(viewId, nextMode);
this.topNavigation?.setViewMode(nextMode);
this.updateView();
},
onSearchQueryChanged: (query) => {
@ -399,7 +492,19 @@ export class FluentTaskView extends ItemView {
this.viewState.selectedProject = projectId;
// Switch to projects view
this.recordViewModeForView(
this.currentViewId,
this.viewState.viewMode,
);
this.currentViewId = "projects";
const projectsViewMode =
this.ensureViewModeForView(this.currentViewId);
this.viewState.viewMode = projectsViewMode;
this.recordViewModeForView(
this.currentViewId,
projectsViewMode,
);
this.topNavigation?.setViewMode(projectsViewMode);
// Reflect selection into the Filter UI state so the top Filter button shows active and can be reset via "X"
try {
@ -463,6 +568,10 @@ export class FluentTaskView extends ItemView {
},
onViewModeChanged: (mode) => {
this.viewState.viewMode = mode;
this.recordViewModeForView(this.currentViewId, mode);
if (mode === "list" || mode === "tree") {
saveViewMode(this.app, this.currentViewId, mode === "tree");
}
this.updateView();
// Save to workspace
this.workspaceStateManager.saveFilterStateToWorkspace();
@ -495,7 +604,11 @@ export class FluentTaskView extends ItemView {
this.addChild(this.workspaceStateManager);
// 4. TaskSelectionManager - Multi-selection and bulk operations
this.selectionManager = new TaskSelectionManager(this.app, this.plugin);
this.selectionManager = new TaskSelectionManager(
this.app,
this.plugin,
this,
);
this.addChild(this.selectionManager);
console.log("[TG] Managers initialized");
@ -725,7 +838,30 @@ export class FluentTaskView extends ItemView {
this.viewState.filters = filters;
this.viewState.selectedProject =
selectedProject;
this.viewState.viewMode = viewMode;
const normalizedMode =
(() => {
const availableModes =
this.componentManager?.getAvailableModesForView(
this.currentViewId,
) ?? [];
if (
availableModes.length > 0 &&
!availableModes.includes(viewMode)
) {
return this.ensureViewModeForView(
this.currentViewId,
);
}
return viewMode;
})();
this.viewState.viewMode = normalizedMode;
this.recordViewModeForView(
this.currentViewId,
normalizedMode,
);
this.topNavigation?.setViewMode(
normalizedMode,
);
if (clearSearch) {
this.viewState.searchQuery = "";
this.viewState.filterInputValue = "";
@ -917,6 +1053,13 @@ export class FluentTaskView extends ItemView {
// This is critical for bulk operations to work
this.selectionManager.updateTaskCache(this.filteredTasks);
const resolvedMode = this.ensureViewModeForView(this.currentViewId);
if (resolvedMode !== this.viewState.viewMode) {
this.viewState.viewMode = resolvedMode;
this.recordViewModeForView(this.currentViewId, resolvedMode);
this.topNavigation?.setViewMode(resolvedMode);
}
// Switch to appropriate component
this.componentManager.switchView(
this.currentViewId,

View file

@ -3,13 +3,9 @@ import {
PluginSettingTab,
setIcon,
ButtonComponent,
Setting,
Platform,
requireApiVersion,
debounce,
Menu,
Modal,
Notice,
} from "obsidian";
import TaskProgressBarPlugin from ".";
@ -226,7 +222,7 @@ export class TaskProgressBarSettingTab extends PluginSettingTab {
// icon: "beaker",
// category: "advanced",
// },
{id: "about", name: t("About"), icon: "info", category: "info"},
{ id: "about", name: t("About"), icon: "info", category: "info" },
];
constructor(app: App, plugin: TaskProgressBarPlugin) {
@ -242,7 +238,7 @@ export class TaskProgressBarSettingTab extends PluginSettingTab {
if (plugin.dataflowOrchestrator) {
// Call async updateSettings and await to ensure incremental reindex completes
await plugin.dataflowOrchestrator.updateSettings(
plugin.settings
plugin.settings,
);
}
@ -253,7 +249,7 @@ export class TaskProgressBarSettingTab extends PluginSettingTab {
await plugin.triggerViewUpdate();
},
100,
true
true,
);
this.debouncedApplyNotifications = debounce(
@ -264,7 +260,7 @@ export class TaskProgressBarSettingTab extends PluginSettingTab {
// Minimal view updates are unnecessary here
},
100,
true
true,
);
}
@ -284,7 +280,7 @@ export class TaskProgressBarSettingTab extends PluginSettingTab {
}
this.searchComponent = new SettingsSearchComponent(
this,
this.containerEl
this.containerEl,
);
}
@ -297,7 +293,7 @@ export class TaskProgressBarSettingTab extends PluginSettingTab {
// Group tabs by category
const categories = {
core: {name: t("Core Settings"), tabs: [] as typeof this.tabs},
core: { name: t("Core Settings"), tabs: [] as typeof this.tabs },
display: {
name: t("Display & Progress"),
tabs: [] as typeof this.tabs,
@ -318,8 +314,8 @@ export class TaskProgressBarSettingTab extends PluginSettingTab {
name: t("Integration"),
tabs: [] as typeof this.tabs,
},
advanced: {name: t("Advanced"), tabs: [] as typeof this.tabs},
info: {name: t("Information"), tabs: [] as typeof this.tabs},
advanced: { name: t("Advanced"), tabs: [] as typeof this.tabs },
info: { name: t("Information"), tabs: [] as typeof this.tabs },
};
// Group tabs by category
@ -379,9 +375,9 @@ export class TaskProgressBarSettingTab extends PluginSettingTab {
labelEl.addClass("settings-tab-label");
labelEl.setText(
tab.name +
(tab.id === "about"
? " v" + this.plugin.manifest.version
: "")
(tab.id === "about"
? " v" + this.plugin.manifest.version
: ""),
);
// Add click handler
@ -414,7 +410,7 @@ export class TaskProgressBarSettingTab extends PluginSettingTab {
// Show active section, hide others
const sections = this.containerEl.querySelectorAll(
".settings-tab-section"
".settings-tab-section",
);
sections.forEach((section) => {
if (section.getAttribute("data-tab-id") === tabId) {
@ -428,10 +424,10 @@ export class TaskProgressBarSettingTab extends PluginSettingTab {
// Handle tab container and header visibility based on selected tab
const tabsContainer = this.containerEl.querySelector(
".settings-tabs-categorized-container"
".settings-tabs-categorized-container",
);
const settingsHeader = this.containerEl.querySelector(
".task-genius-settings-header"
".task-genius-settings-header",
);
if (tabId === "general") {
@ -456,10 +452,11 @@ export class TaskProgressBarSettingTab extends PluginSettingTab {
if (tabId === "workspaces") {
// Make sure the workspace section is visible even though the tab is hidden
const workspaceSection = this.containerEl.querySelector(
'[data-tab-id="workspaces"]'
'[data-tab-id="workspaces"]',
);
if (workspaceSection) {
(workspaceSection as unknown as HTMLElement).style.display = "block";
(workspaceSection as unknown as HTMLElement).style.display =
"block";
workspaceSection.addClass("settings-tab-section-active");
}
}
@ -476,7 +473,7 @@ export class TaskProgressBarSettingTab extends PluginSettingTab {
public navigateToTab(
tabId: string,
section?: string,
search?: string
search?: string,
): void {
// Set the current tab
this.currentTab = tabId;
@ -510,14 +507,14 @@ export class TaskProgressBarSettingTab extends PluginSettingTab {
headerText &&
headerText.includes(sectionId.replace("-", " "))
) {
header.scrollIntoView({behavior: "smooth", block: "start"});
header.scrollIntoView({ behavior: "smooth", block: "start" });
}
});
// Special handling for MCP sections
if (sectionId === "cursor" && this.currentTab === "mcp-integration") {
const cursorSection = this.containerEl.querySelector(
".mcp-client-section"
".mcp-client-section",
);
if (cursorSection) {
const header =
@ -537,7 +534,7 @@ export class TaskProgressBarSettingTab extends PluginSettingTab {
private createTabSection(tabId: string): HTMLElement {
// Get the sections container
const sectionsContainer = this.containerEl.querySelector(
".settings-tab-sections"
".settings-tab-sections",
);
if (!sectionsContainer) return this.containerEl;
@ -570,7 +567,7 @@ export class TaskProgressBarSettingTab extends PluginSettingTab {
new IframeModal(
this.app,
url,
`How to use — ${tabInfo?.name ?? tabId}`
`How to use — ${tabInfo?.name ?? tabId}`,
).open();
} catch (e) {
window.open(url);
@ -607,7 +604,7 @@ export class TaskProgressBarSettingTab extends PluginSettingTab {
}
display(): void {
const {containerEl} = this;
const { containerEl } = this;
containerEl.empty();
@ -698,7 +695,7 @@ export class TaskProgressBarSettingTab extends PluginSettingTab {
// Notifications Tab
const notificationsSection = this.createTabSection(
"desktop-integration"
"desktop-integration",
);
this.displayDesktopIntegrationSettings(notificationsSection);
@ -862,7 +859,7 @@ export class TaskProgressBarSettingTab extends PluginSettingTab {
() => {
this.currentTab = "general";
this.display();
}
},
);
icsSettingsComponent.display();
}
@ -873,7 +870,7 @@ export class TaskProgressBarSettingTab extends PluginSettingTab {
private displayMcpSettings(containerEl: HTMLElement): void {
renderMcpIntegrationSettingsTab(containerEl, this.plugin, () =>
this.applySettingsUpdate()
this.applySettingsUpdate(),
);
}
@ -909,5 +906,4 @@ export class TaskProgressBarSettingTab extends PluginSettingTab {
private renderTaskTimerSettingsTab(containerEl: HTMLElement): void {
renderTaskTimerSettingTab(this, containerEl);
}
}

View file

@ -280,6 +280,13 @@
gap: var(--size-4-2); /* Add spacing between events */
}
.full-calendar-container .calendar-timeline-events-container.is-empty {
color: var(--text-faint);
font-style: italic;
text-align: center;
padding: var(--size-4-8);
}
/* Style adjustments for events previously timed */
.full-calendar-container .calendar-event-timed {
/* Remove absolute positioning */
@ -292,6 +299,37 @@
/* Using gap on parent container now */
/* margin-bottom: var(--size-4-2); */
width: 100%; /* Ensure it takes full width */
border-radius: var(--radius-s);
transition: transform 0.15s ease, box-shadow 0.15s ease;
}
.full-calendar-container .calendar-event-timed:hover {
transform: translateY(-1px);
box-shadow: var(--shadow-s);
}
/* Specific styles for day view timed events */
.full-calendar-container .calendar-event-day-timed {
position: relative;
width: 100%;
min-height: 2.5em;
padding: var(--size-4-2);
background-color: var(--interactive-accent);
color: var(--text-on-accent);
border-radius: var(--radius-s);
border: 1px solid var(--background-modifier-border);
}
.full-calendar-container .calendar-event-day-timed:hover {
background-color: var(--interactive-accent-hover);
transform: translateY(-1px);
box-shadow: var(--shadow-s);
}
.full-calendar-container .calendar-event-day-timed.is-completed {
background-color: var(--background-modifier-success-hover);
opacity: 0.7;
text-decoration: line-through;
}
/* Keep internal styles */
@ -300,16 +338,13 @@
font-weight: bold;
padding: 1px 3px;
background-color: rgba(0, 0, 0, 0.1); /* Slight background for time */
border-radius: var(--radius-s);
margin-bottom: var(--size-4-1);
display: inline-block;
width: fit-content;
}
.full-calendar-container .calendar-event-title {
font-size: var(--font-ui-small);
padding: 2px 3px;
flex-grow: 1;
/* Allow text wrapping within the event box */
white-space: normal;
word-wrap: break-word;
font-size: var(--font-ui-small);
padding: 2px 3px;
flex-grow: 1;

View file

@ -432,6 +432,7 @@
margin-bottom: 1rem;
color: var(--text-muted);
opacity: 0.5;
--icon-size: 48px;
}
.tg-fluent-empty-title {
@ -574,43 +575,43 @@
height: 300px;
}
.tg-fluent-button-primary {
button.tg-fluent-button-primary {
background: var(--interactive-accent);
color: var(--text-on-accent);
}
.tg-fluent-button-primary:hover {
button.tg-fluent-button-primary:hover {
background: var(--interactive-accent-hover);
transform: translateY(-1px);
box-shadow: var(--tg-shadow);
}
.tg-fluent-button-secondary {
button.tg-fluent-button-secondary {
background: var(--background-secondary);
color: var(--text-normal);
border: 1px solid var(--background-modifier-border);
}
.tg-fluent-button-secondary:hover {
button.tg-fluent-button-secondary:hover {
background: var(--background-modifier-hover);
border-color: var(--interactive-accent);
}
.tg-fluent-button-ghost {
button.tg-fluent-button-ghost {
background: transparent;
color: var(--text-normal);
}
.tg-fluent-button-ghost:hover {
button.tg-fluent-button-ghost:hover {
background: var(--background-modifier-hover);
}
.tg-fluent-button-danger {
button.tg-fluent-button-danger {
background: var(--text-error);
color: white;
}
.tg-fluent-button-danger:hover {
button.tg-fluent-button-danger:hover {
background: #dc2626;
transform: translateY(-1px);
box-shadow: var(--tg-shadow);

View file

@ -2,6 +2,10 @@ export interface FluentTaskViewState {
currentWorkspace: string;
selectedProject?: string | null;
viewMode: "list" | "kanban" | "tree" | "calendar";
viewModeByViewId?: Record<
string,
"list" | "kanban" | "tree" | "calendar"
>;
searchQuery?: string;
filterInputValue?: string;
filters?: any;

View file

@ -13,6 +13,7 @@ import {
import { Component } from "obsidian";
import { HabitProps } from "./habit-card";
import { RootFilterState } from "@/components/features/task/filter/ViewTaskFilter";
import { SelectionModeChangeEventData } from "@/types/selection";
interface Token extends EditorRange {
/** @todo Documentation incomplete. */
@ -458,6 +459,11 @@ declare module "obsidian" {
count: number;
},
): void;
trigger(
event: "task-genius:selection-mode-changed",
payload: SelectionModeChangeEventData,
): void;
}
interface WorkspaceLeaf {

View file

@ -83,7 +83,7 @@ function parseDateFilterString(dateString: string): moment.Moment | null {
export function isNotCompleted(
plugin: TaskProgressBarPlugin,
task: Task,
viewId: ViewMode
viewId: ViewMode,
): boolean {
const viewConfig = getViewSettingOrDefault(plugin, viewId);
const abandonedStatus = plugin.settings.taskStatuses.abandoned.split("|");
@ -111,7 +111,7 @@ export function isNotCompleted(
export function isBlank(
plugin: TaskProgressBarPlugin,
task: Task,
viewId: ViewMode
viewId: ViewMode,
): boolean {
const viewConfig = getViewSettingOrDefault(plugin, viewId);
@ -130,7 +130,7 @@ export function isBlank(
*/
export function applyAdvancedFilter(
task: Task,
filterState: RootFilterState
filterState: RootFilterState,
): boolean {
// 如果没有过滤器组或过滤器组为空,返回所有任务
if (!filterState.filterGroups || filterState.filterGroups.length === 0) {
@ -205,7 +205,7 @@ function applyFilter(task: Task, filter: Filter): boolean {
return applyPriorityFilter(
task.metadata.priority,
condition,
value
value,
);
case "dueDate":
return applyDateFilter(task.metadata.dueDate, condition, value);
@ -215,7 +215,7 @@ function applyFilter(task: Task, filter: Filter): boolean {
return applyDateFilter(
task.metadata.scheduledDate,
condition,
value
value,
);
case "tags":
return applyTagsFilter(task.metadata.tags, condition, value);
@ -237,7 +237,7 @@ function applyFilter(task: Task, filter: Filter): boolean {
function applyContentFilter(
content: string,
condition: string,
value?: string
value?: string,
): boolean {
if (!content) content = "";
if (!value) value = "";
@ -270,7 +270,7 @@ function applyContentFilter(
function applyStatusFilter(
status: string,
condition: string,
value?: string
value?: string,
): boolean {
if (!status) status = "";
if (!value) value = "";
@ -299,7 +299,7 @@ function applyStatusFilter(
function applyPriorityFilter(
priority: number | undefined,
condition: string,
value?: string
value?: string,
): boolean {
// 如果没有设置优先级将其视为0
const taskPriority = typeof priority === "number" ? priority : 0;
@ -339,7 +339,7 @@ function applyPriorityFilter(
function applyDateFilter(
date: number | undefined,
condition: string,
value?: string
value?: string,
): boolean {
// 处理空值条件
switch (condition) {
@ -390,7 +390,7 @@ function applyDateFilter(
function applyTagsFilter(
tags: string[],
condition: string,
value?: string
value?: string,
): boolean {
if (!tags) tags = [];
if (!value) value = "";
@ -417,7 +417,7 @@ function applyTagsFilter(
function applyFilePathFilter(
filePath: string,
condition: string,
value?: string
value?: string,
): boolean {
if (!filePath) filePath = "";
if (!value) value = "";
@ -450,7 +450,7 @@ function applyFilePathFilter(
function applyProjectFilter(
project: string | undefined,
condition: string,
value?: string
value?: string,
): boolean {
const proj = (project ?? "").toLowerCase();
const val = (value ?? "").toLowerCase();
@ -499,7 +499,7 @@ export function filterTasks(
allTasks: Task[],
viewId: ViewMode,
plugin: TaskProgressBarPlugin,
options: FilterOptions = {}
options: FilterOptions = {},
): Task[] {
let filtered = [...allTasks];
const viewConfig = getViewSettingOrDefault(plugin, viewId);
@ -544,7 +544,7 @@ export function filterTasks(
) {
console.log("应用全局筛选器:", globalFilterRules.advancedFilter);
filtered = filtered.filter((task) =>
applyAdvancedFilter(task, globalFilterRules.advancedFilter!)
applyAdvancedFilter(task, globalFilterRules.advancedFilter!),
);
}
@ -555,10 +555,10 @@ export function filterTasks(
) {
console.log(
"应用视图配置中的基础高级过滤器:",
filterRules.advancedFilter
filterRules.advancedFilter,
);
filtered = filtered.filter((task) =>
applyAdvancedFilter(task, filterRules.advancedFilter!)
applyAdvancedFilter(task, filterRules.advancedFilter!),
);
}
@ -569,13 +569,13 @@ export function filterTasks(
) {
console.log("应用传入的实时高级过滤器:", options.advancedFilter);
filtered = filtered.filter((task) =>
applyAdvancedFilter(task, options.advancedFilter!)
applyAdvancedFilter(task, options.advancedFilter!),
);
// 如果有实时高级过滤器,应用基本规则后直接返回
// 应用 isNotCompleted 过滤器(基于视图配置的 hideCompletedAndAbandonedTasks
filtered = filtered.filter((task) =>
isNotCompleted(plugin, task, viewId)
isNotCompleted(plugin, task, viewId),
);
// 应用 isBlank 过滤器(基于视图配置的 filterBlanks
@ -590,8 +590,8 @@ export function filterTasks(
task.metadata.project?.toLowerCase().includes(textFilter) ||
task.metadata.context?.toLowerCase().includes(textFilter) ||
task.metadata.tags?.some((tag) =>
tag.toLowerCase().includes(textFilter)
)
tag.toLowerCase().includes(textFilter),
),
);
}
@ -605,16 +605,16 @@ export function filterTasks(
if (filterRules.textContains) {
const query = filterRules.textContains.toLowerCase();
filtered = filtered.filter((task) =>
task.content.toLowerCase().includes(query)
task.content.toLowerCase().includes(query),
);
}
if (filterRules.tagsInclude && filterRules.tagsInclude.length > 0) {
filtered = filtered.filter((task) =>
filterRules.tagsInclude?.some((tag) =>
task.metadata.tags.some(
(taskTag) => typeof taskTag === "string" && taskTag === tag
)
)
(taskTag) => typeof taskTag === "string" && taskTag === tag,
),
),
);
}
if (filterRules.tagsExclude && filterRules.tagsExclude.length > 0) {
@ -625,11 +625,11 @@ export function filterTasks(
// Convert task tags to lowercase for case-insensitive comparison
const taskTagsLower = task.metadata.tags.map((tag) =>
tag.toLowerCase()
tag.toLowerCase(),
);
// Check if any excluded tag is in the task's tags
return !filterRules.tagsExclude!.some((excludeTag) => {
return !filterRules.tagsExclude?.some((excludeTag) => {
const tagLower = excludeTag.toLowerCase();
return (
taskTagsLower.includes(tagLower) ||
@ -641,7 +641,7 @@ export function filterTasks(
if (filterRules.project) {
filtered = filtered.filter(
(task) =>
task.metadata.project?.trim() === filterRules.project?.trim()
task.metadata.project?.trim() === filterRules.project?.trim(),
);
}
if (filterRules.priority !== undefined) {
@ -662,12 +662,12 @@ export function filterTasks(
}
if (filterRules.statusInclude && filterRules.statusInclude.length > 0) {
filtered = filtered.filter((task) =>
filterRules.statusInclude!.includes(task.status)
filterRules.statusInclude?.includes(task.status),
);
}
if (filterRules.statusExclude && filterRules.statusExclude.length > 0) {
filtered = filtered.filter(
(task) => !filterRules.statusExclude!.includes(task.status)
(task) => !filterRules.statusExclude?.includes(task.status),
);
}
// Path filters (Added based on content.ts logic)
@ -677,7 +677,7 @@ export function filterTasks(
.filter((p) => p.trim() !== "")
.map((p) => p.trim().toLowerCase());
filtered = filtered.filter((task) =>
query.some((q) => task.filePath.toLowerCase().includes(q))
query.some((q) => task.filePath.toLowerCase().includes(q)),
);
}
@ -699,7 +699,7 @@ export function filterTasks(
filtered = filtered.filter((task) =>
task.metadata.dueDate
? moment(task.metadata.dueDate).isSame(targetDueDate, "day")
: false
: false,
);
}
}
@ -710,24 +710,24 @@ export function filterTasks(
task.metadata.startDate
? moment(task.metadata.startDate).isSame(
targetStartDate,
"day"
)
: false
"day",
)
: false,
);
}
}
if (filterRules.scheduledDate) {
const targetScheduledDate = parseDateFilterString(
filterRules.scheduledDate
filterRules.scheduledDate,
);
if (targetScheduledDate) {
filtered = filtered.filter((task) =>
task.metadata.scheduledDate
? moment(task.metadata.scheduledDate).isSame(
targetScheduledDate,
"day"
)
: false
"day",
)
: false,
);
}
}
@ -751,7 +751,7 @@ export function filterTasks(
(task) =>
isToday(task.metadata?.dueDate) ||
isToday(task.metadata?.scheduledDate) ||
isToday(task.metadata?.startDate)
isToday(task.metadata?.startDate),
);
break;
}
@ -761,13 +761,13 @@ export function filterTasks(
const inNext7Days = (d?: string | number | Date) =>
d
? moment(d).isAfter(start, "day") &&
moment(d).isSameOrBefore(end, "day")
moment(d).isSameOrBefore(end, "day")
: false;
filtered = filtered.filter(
(task) =>
inNext7Days(task.metadata?.dueDate) ||
inNext7Days(task.metadata?.scheduledDate) ||
inNext7Days(task.metadata?.startDate)
inNext7Days(task.metadata?.startDate),
);
break;
}
@ -776,7 +776,7 @@ export function filterTasks(
(task) =>
(task.metadata.priority ?? 0) >= 3 ||
(task.metadata.tags?.includes("flagged") ?? false) ||
(task.metadata.tags?.includes("#flagged") ?? false)
(task.metadata.tags?.includes("#flagged") ?? false),
);
break;
}
@ -801,8 +801,8 @@ export function filterTasks(
task.metadata.project?.toLowerCase().includes(textFilter) ||
task.metadata.context?.toLowerCase().includes(textFilter) ||
task.metadata.tags?.some((tag) =>
tag.toLowerCase().includes(textFilter)
)
tag.toLowerCase().includes(textFilter),
),
);
}

File diff suppressed because one or more lines are too long