refactor(fluent): streamline filter panel integration and fix view mode initialization

- Remove standalone filter panel in favor of Obsidian view header integration
- Fix view mode initialization to default to list view for content-based views
- Add setIsTreeView method for external view mode control
- Improve component initialization logging for better debugging
- Remove unused patches documentation file
- Fix TypeScript type issues with filter options
This commit is contained in:
Quorafind 2025-09-21 08:28:03 +08:00
parent 6b779f84a5
commit 846d3bbeb6
5 changed files with 474 additions and 189 deletions

View file

@ -61,7 +61,7 @@ export class ContentComponent extends Component {
private parentEl: HTMLElement,
private app: App,
private plugin: TaskProgressBarPlugin,
private params: ContentComponentParams = {}
private params: ContentComponentParams = {},
) {
super();
}
@ -147,7 +147,7 @@ export class ContentComponent extends Component {
{
root: this.taskListEl, // Observe within the task list container
threshold: 0.1, // Trigger when 10% of the marker is visible
}
},
);
}
@ -158,11 +158,11 @@ export class ContentComponent extends Component {
this.isTreeView = getInitialViewMode(
this.app,
this.plugin,
this.currentViewId
this.currentViewId,
);
// Update the toggle button icon to match the initial state
const viewToggleBtn = this.headerEl?.querySelector(
".view-toggle-btn"
".view-toggle-btn",
) as HTMLElement;
if (viewToggleBtn) {
setIcon(viewToggleBtn, this.isTreeView ? "git-branch" : "list");
@ -172,7 +172,7 @@ export class ContentComponent extends Component {
private toggleViewMode() {
this.isTreeView = !this.isTreeView;
const viewToggleBtn = this.headerEl.querySelector(
".view-toggle-btn"
".view-toggle-btn",
) as HTMLElement;
if (viewToggleBtn) {
setIcon(viewToggleBtn, this.isTreeView ? "git-branch" : "list");
@ -182,10 +182,23 @@ export class ContentComponent extends Component {
this.refreshTaskList(); // Refresh list completely on view mode change
}
public setIsTreeView(isTree: boolean) {
if (this.isTreeView !== isTree) {
this.isTreeView = isTree;
const viewToggleBtn = this.headerEl?.querySelector(
".view-toggle-btn",
) as HTMLElement;
if (viewToggleBtn) {
setIcon(viewToggleBtn, this.isTreeView ? "git-branch" : "list");
}
this.refreshTaskList();
}
}
public setTasks(
tasks: Task[],
notFilteredTasks: Task[],
forceRefresh: boolean = false
forceRefresh: boolean = false,
) {
// Allow forced refresh for cases where we know the data has changed
if (forceRefresh) {
@ -203,7 +216,7 @@ export class ContentComponent extends Component {
// If a force refresh is pending, skip non-forced updates
if (this.pendingForceRefresh) {
console.log(
"ContentComponent: Skipping non-forced update, force refresh is pending"
"ContentComponent: Skipping non-forced update, force refresh is pending",
);
return;
}
@ -215,7 +228,7 @@ export class ContentComponent extends Component {
this.notFilteredTasks === notFilteredTasks
) {
console.log(
"ContentComponent: Same array references, skipping refresh"
"ContentComponent: Same array references, skipping refresh",
);
return;
}
@ -241,7 +254,7 @@ export class ContentComponent extends Component {
}
if (unchanged) {
console.log(
"ContentComponent: Tasks unchanged, skipping refresh"
"ContentComponent: Tasks unchanged, skipping refresh",
);
return;
}
@ -282,17 +295,17 @@ export class ContentComponent extends Component {
this.allTasks,
this.currentViewId,
this.plugin,
{ textQuery: this.filterInput?.value } // Pass text query from input
{ textQuery: this.filterInput?.value }, // Pass text query from input
);
const sortCriteria = this.plugin.settings.viewConfiguration.find(
(view) => view.id === this.currentViewId
(view) => view.id === this.currentViewId,
)?.sortCriteria;
if (sortCriteria && sortCriteria.length > 0) {
this.filteredTasks = sortTasks(
this.filteredTasks,
sortCriteria,
this.plugin.settings
this.plugin.settings,
);
} else {
// Default sorting: completed tasks last, then by priority, due date, content,
@ -320,7 +333,7 @@ export class ContentComponent extends Component {
});
const contentCmp = collator.compare(
a.content ?? "",
b.content ?? ""
b.content ?? "",
);
if (contentCmp !== 0) return contentCmp;
// Lowest-priority tie-breakers to ensure stability across files
@ -366,7 +379,7 @@ export class ContentComponent extends Component {
// Defer rendering if container is not visible yet (e.g., view hidden during init)
if (!this.isContainerVisible()) {
console.warn(
"ContentComponent: Cannot render: Container not visible. Queuing refresh..."
"ContentComponent: Cannot render: Container not visible. Queuing refresh...",
);
this.pendingForceRefresh = true;
if (!this.pendingVisibilityRetry) {
@ -388,7 +401,7 @@ export class ContentComponent extends Component {
this.pendingVisibilityRetry = false;
this.visibilityRetryCount = 0;
console.warn(
"ContentComponent: Container still not visible after retries; will wait for next trigger."
"ContentComponent: Container still not visible after retries; will wait for next trigger.",
);
}
};
@ -400,7 +413,7 @@ export class ContentComponent extends Component {
// If a render is already in progress, queue a refresh instead of skipping
if (this.isRendering) {
console.log(
"ContentComponent: Already rendering, queueing a refresh"
"ContentComponent: Already rendering, queueing a refresh",
);
this.pendingForceRefresh = true;
return;
@ -429,19 +442,19 @@ export class ContentComponent extends Component {
const taskMap = new Map<string, Task>();
// Add all non-filtered tasks to the taskMap
this.notFilteredTasks.forEach((task) =>
taskMap.set(task.id, task)
taskMap.set(task.id, task),
);
this.rootTasks = tasksToTree(this.filteredTasks); // Calculate root tasks
// Sort roots according to view's sort criteria (fallback to sensible defaults)
const viewSortCriteria =
this.plugin.settings.viewConfiguration.find(
(view) => view.id === this.currentViewId
(view) => view.id === this.currentViewId,
)?.sortCriteria;
if (viewSortCriteria && viewSortCriteria.length > 0) {
this.rootTasks = sortTasks(
this.rootTasks,
viewSortCriteria,
this.plugin.settings
this.plugin.settings,
);
} else {
// Default sorting: completed tasks last, then by priority, due date, content,
@ -470,12 +483,12 @@ export class ContentComponent extends Component {
});
const contentCmp = collator.compare(
a.content ?? "",
b.content ?? ""
b.content ?? "",
);
if (contentCmp !== 0) return contentCmp;
// Lowest-priority tie-breakers to ensure stability across files
const fp = (a.filePath || "").localeCompare(
b.filePath || ""
b.filePath || "",
);
if (fp !== 0) return fp;
return (a.line ?? 0) - (b.line ?? 0);
@ -518,7 +531,7 @@ export class ContentComponent extends Component {
const containerRect = container.getBoundingClientRect();
// Find first visible task item
const items = Array.from(
container.querySelectorAll<HTMLElement>(".task-item")
container.querySelectorAll<HTMLElement>(".task-item"),
);
for (const el of items) {
const rect = el.getBoundingClientRect();
@ -543,7 +556,7 @@ export class ContentComponent extends Component {
// Try anchor-based restoration first
if (state.anchorId) {
const anchorEl = container.querySelector<HTMLElement>(
`[data-task-id="${state.anchorId}"]`
`[data-task-id="${state.anchorId}"]`,
);
if (anchorEl) {
const desiredOffset = state.anchorOffset;
@ -573,7 +586,7 @@ export class ContentComponent extends Component {
task,
this.currentViewId, // Pass currentViewId
this.app,
this.plugin
this.plugin,
);
// Attach event handlers
@ -586,7 +599,7 @@ export class ContentComponent extends Component {
console.log(
"ContentComponent onTaskUpdate",
originalTask.content,
updatedTask.content
updatedTask.content,
);
await this.params.onTaskUpdate(originalTask, updatedTask);
}
@ -623,7 +636,7 @@ export class ContentComponent extends Component {
for (let i = start; i < end; i++) {
const rootTask = this.rootTasks[i];
const childTasks = this.notFilteredTasks.filter(
(task) => task.metadata.parent === rootTask.id
(task) => task.metadata.parent === rootTask.id,
);
const treeComponent = new TaskTreeItemComponent(
@ -633,7 +646,7 @@ export class ContentComponent extends Component {
0,
childTasks,
taskMap,
this.plugin
this.plugin,
);
// Attach event handlers
@ -707,7 +720,7 @@ export class ContentComponent extends Component {
// );
const taskMap = new Map<string, Task>();
this.filteredTasks.forEach((task) =>
taskMap.set(task.id, task)
taskMap.set(task.id, task),
);
this.loadRootTaskBatch(taskMap);
} else {
@ -769,12 +782,12 @@ export class ContentComponent extends Component {
public updateTask(updatedTask: Task) {
// 1) Update sources
const taskIndexAll = this.allTasks.findIndex(
(t) => t.id === updatedTask.id
(t) => t.id === updatedTask.id,
);
if (taskIndexAll !== -1)
this.allTasks[taskIndexAll] = { ...updatedTask };
const taskIndexNotFiltered = this.notFilteredTasks.findIndex(
(t) => t.id === updatedTask.id
(t) => t.id === updatedTask.id,
);
if (taskIndexNotFiltered !== -1)
this.notFilteredTasks[taskIndexNotFiltered] = { ...updatedTask };
@ -785,17 +798,17 @@ export class ContentComponent extends Component {
const prevLen = this.filteredTasks.length;
this.applyFilters();
const taskFromFiltered = this.filteredTasks.find(
(t) => t.id === updatedTask.id
(t) => t.id === updatedTask.id,
);
const taskStillVisible = !!taskFromFiltered;
// Helper: insert list item at correct position (list view)
const insertListItem = (taskToInsert: Task) => {
const compIds = new Set(
this.taskComponents.map((c) => c.getTask().id)
this.taskComponents.map((c) => c.getTask().id),
);
const sortedIndex = this.filteredTasks.findIndex(
(t) => t.id === taskToInsert.id
(t) => t.id === taskToInsert.id,
);
// Find the next rendered neighbor after sortedIndex
let nextComp: any = null;
@ -803,7 +816,7 @@ export class ContentComponent extends Component {
const id = this.filteredTasks[i].id;
if (compIds.has(id)) {
nextComp = this.taskComponents.find(
(c) => c.getTask().id === id
(c) => c.getTask().id === id,
);
break;
}
@ -813,7 +826,7 @@ export class ContentComponent extends Component {
taskToInsert,
this.currentViewId,
this.app,
this.plugin
this.plugin,
);
// Attach events
taskComponent.onTaskSelected = this.selectTask.bind(this);
@ -833,7 +846,7 @@ export class ContentComponent extends Component {
if (nextComp) {
this.taskListEl.insertBefore(
taskComponent.element,
nextComp.element
nextComp.element,
);
const idx = this.taskComponents.indexOf(nextComp);
this.taskComponents.splice(idx, 0, taskComponent);
@ -846,7 +859,7 @@ export class ContentComponent extends Component {
// Helper: remove list item
const removeListItem = (taskId: string) => {
const idx = this.taskComponents.findIndex(
(c) => c.getTask().id === taskId
(c) => c.getTask().id === taskId,
);
if (idx >= 0) {
const comp = this.taskComponents[idx];
@ -867,7 +880,7 @@ export class ContentComponent extends Component {
if (!this.isTreeView) {
// List view: update in place or insert if new to view
const comp = this.taskComponents.find(
(c) => c.getTask().id === updatedTask.id
(c) => c.getTask().id === updatedTask.id,
);
if (comp) {
comp.updateTask(taskFromFiltered!);
@ -877,7 +890,7 @@ export class ContentComponent extends Component {
} else {
// Tree view: update existing subtree or insert to parent/root
const comp = this.treeComponents.find(
(c) => c.getTask().id === updatedTask.id
(c) => c.getTask().id === updatedTask.id,
);
if (comp) {
comp.updateTask(taskFromFiltered!);
@ -902,7 +915,7 @@ export class ContentComponent extends Component {
const newChildren =
this.notFilteredTasks.filter(
(t) =>
t.metadata.parent === parentId
t.metadata.parent === parentId,
);
parentComp.updateChildTasks(newChildren);
updated = true;
@ -913,11 +926,11 @@ export class ContentComponent extends Component {
// Root insertion
const taskMap = new Map<string, Task>();
this.notFilteredTasks.forEach((t) =>
taskMap.set(t.id, t)
taskMap.set(t.id, t),
);
const childTasks = this.notFilteredTasks.filter(
(t) =>
t.metadata.parent === taskFromFiltered!.id
t.metadata.parent === taskFromFiltered!.id,
);
const newRoot = new TaskTreeItemComponent(
taskFromFiltered!,
@ -926,7 +939,7 @@ export class ContentComponent extends Component {
0,
childTasks,
taskMap,
this.plugin
this.plugin,
);
newRoot.onTaskSelected = this.selectTask.bind(this);
newRoot.onTaskCompleted = (t) => {
@ -951,7 +964,7 @@ export class ContentComponent extends Component {
if (
rootComparator(
taskFromFiltered!,
this.treeComponents[i].getTask()
this.treeComponents[i].getTask(),
) < 0
) {
insertAt = i;
@ -961,12 +974,12 @@ export class ContentComponent extends Component {
if (insertAt < this.treeComponents.length) {
this.taskListEl.insertBefore(
newRoot.element,
this.treeComponents[insertAt].element
this.treeComponents[insertAt].element,
);
this.treeComponents.splice(
insertAt,
0,
newRoot
newRoot,
);
} else {
this.taskListEl.appendChild(newRoot.element);
@ -988,7 +1001,7 @@ export class ContentComponent extends Component {
// Tree view removal
// If root component exists, remove it
const idx = this.treeComponents.findIndex(
(c) => c.getTask().id === updatedTask.id
(c) => c.getTask().id === updatedTask.id,
);
if (idx >= 0) {
const comp = this.treeComponents[idx];

File diff suppressed because it is too large Load diff

View file

@ -27,7 +27,6 @@ export class V2FilterPanel {
private onFilterChange: (filters: FilterOptions) => void,
private tasks: Task[] = [],
) {
this.createPanel();
this.extractAvailableOptions();
}

View file

@ -483,7 +483,9 @@ export function filterTasks(
if (!isCalendarView && !isForecastView) {
filtered = filtered.filter((task) => {
// 识别 ICS 事件任务(优先从 metadata.source 读取,兼容 legacy source 字段)
const metaSourceType = (task as any).metadata?.source?.type ?? (task as any).source?.type;
const metaSourceType =
(task as any).metadata?.source?.type ??
(task as any).source?.type;
const isIcsTask = metaSourceType === "ics";
// 非 ICS 保留ICS 在此类视图中过滤掉
return !isIcsTask;
@ -702,16 +704,47 @@ export function filterTasks(
if (Object.keys(filterRules).length === 0) {
// Only apply default logic if no rules were defined for this view
switch (viewId) {
case "inbox":
case "inbox": {
filtered = filtered.filter((task) => !hasProject(task));
break;
case "flagged":
}
case "today": {
const today = moment().startOf("day");
const isToday = (d?: string | number | Date) =>
d ? moment(d).isSame(today, "day") : false;
filtered = filtered.filter(
(task) =>
isToday(task.metadata?.dueDate) ||
isToday(task.metadata?.scheduledDate) ||
isToday(task.metadata?.startDate)
);
break;
}
case "upcoming": {
const start = moment().startOf("day");
const end = moment().add(7, "days").endOf("day");
const inNext7Days = (d?: string | number | Date) =>
d
? moment(d).isAfter(start, "day") &&
moment(d).isSameOrBefore(end, "day")
: false;
filtered = filtered.filter(
(task) =>
inNext7Days(task.metadata?.dueDate) ||
inNext7Days(task.metadata?.scheduledDate) ||
inNext7Days(task.metadata?.startDate)
);
break;
}
case "flagged": {
filtered = filtered.filter(
(task) =>
(task.metadata.priority ?? 0) >= 3 ||
task.metadata.tags?.includes("flagged")
(task.metadata.tags?.includes("flagged") ?? false) ||
(task.metadata.tags?.includes("#flagged") ?? false)
);
break;
}
// Projects, Tags, Review logic are handled by their specific components / options
}
}