Skip to content

Commit d23f9d1

Browse files
abbiefallsclaude
andcommitted
Wire project folder badge to toggle inline subtasks
The folder/project badge on task cards called applyProjectSubtaskFilter(), a placeholder left from the v4 view-system removal (commit 96519c5) that always showed "Project subtask filtering not available" — a dead action with no setting to enable it, while its tooltip still advertised "click to filter subtasks". Wire the badge to toggle the inline subtask list instead, sharing expansion state with the chevron via expandedProjectsService so the two stay in sync. The subtask render is moved out from behind the showExpandableSubtasks gate so the folder works even when the chevron is disabled. Fix the misleading tooltip and remove the dead applyProjectSubtaskFilter / createProjectClickHandler / TaskListView.filterProjectSubtasks paths. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 7b17a26 commit d23f9d1

7 files changed

Lines changed: 139 additions & 114 deletions

File tree

src/bases/TaskListView.ts

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2515,9 +2515,6 @@ export class TaskListView extends BasesViewBase {
25152515
event
25162516
);
25172517
return;
2518-
case "filter-project-subtasks":
2519-
await this.filterProjectSubtasks(task);
2520-
return;
25212518
case "toggle-subtasks":
25222519
await this.toggleSubtasks(task, target);
25232520
return;
@@ -2753,19 +2750,6 @@ export class TaskListView extends BasesViewBase {
27532750
}
27542751
}
27552752

2756-
private async filterProjectSubtasks(task: TaskInfo): Promise<void> {
2757-
try {
2758-
await this.plugin.applyProjectSubtaskFilter(task);
2759-
} catch (error) {
2760-
tasknotesLogger.error("[TaskNotes][TaskListView] Failed to filter project subtasks", {
2761-
category: "persistence",
2762-
operation: "filter-project-subtasks",
2763-
error: error,
2764-
});
2765-
new Notice("Failed to filter project subtasks");
2766-
}
2767-
}
2768-
27692753
private async toggleSubtasks(task: TaskInfo, target: HTMLElement): Promise<void> {
27702754
try {
27712755
if (!this.plugin.expandedProjectsService) {

src/i18n/resources/en.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3273,7 +3273,7 @@ export const en: TranslationTree = {
32733273
reminderTooltipOne: "1 reminder set (click to manage)",
32743274
reminderTooltipMany: "{count} reminders set (click to manage)",
32753275
detailsTooltip: "Task has details",
3276-
projectTooltip: "This task is used as a project (click to filter subtasks)",
3276+
projectTooltip: "This task is used as a project (click to show/hide subtasks)",
32773277
expandSubtasks: "Expand subtasks",
32783278
collapseSubtasks: "Collapse subtasks",
32793279
dueToday: "{label}: Today",

src/main.ts

Lines changed: 0 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1267,30 +1267,6 @@ export default class TaskNotesPlugin extends Plugin {
12671267
await this.taskActionCoordinator.rolloverOverdueScheduledTasks();
12681268
}
12691269

1270-
/**
1271-
* Apply a filter to show subtasks of a project
1272-
*/
1273-
async applyProjectSubtaskFilter(projectTask: TaskInfo): Promise<void> {
1274-
try {
1275-
const file = this.app.vault.getAbstractFileByPath(projectTask.path);
1276-
if (!file) {
1277-
new Notice("Project file not found");
1278-
return;
1279-
}
1280-
1281-
// Note: This feature was part of the old view system (deprecated in v4)
1282-
// TODO: Re-implement for Bases views if needed
1283-
new Notice("Project subtask filtering not available");
1284-
} catch (error) {
1285-
tasknotesLogger.error("Error applying project subtask filter:", {
1286-
category: "persistence",
1287-
operation: "applying-project-subtask-filter",
1288-
error: error,
1289-
});
1290-
new Notice("Failed to apply project filter");
1291-
}
1292-
}
1293-
12941270
/**
12951271
* Starts a time tracking session for a task
12961272
*/

src/ui/taskCardActions.ts

Lines changed: 0 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -184,24 +184,3 @@ export function createReminderClickHandler(task: TaskInfo, plugin: TaskNotesPlug
184184
};
185185
}
186186

187-
/**
188-
* Creates a click handler for project indicators.
189-
*/
190-
export function createProjectClickHandler(task: TaskInfo, plugin: TaskNotesPlugin): () => void {
191-
return () => {
192-
void (async () => {
193-
const logger = getTaskCardActionLogger(plugin);
194-
try {
195-
await plugin.applyProjectSubtaskFilter(task);
196-
} catch (error) {
197-
logger.error("Error filtering project subtasks", {
198-
category: "internal",
199-
operation: "filter-project-subtasks",
200-
details: { taskPath: task.path },
201-
error,
202-
});
203-
new Notice("Failed to filter project subtasks");
204-
}
205-
})();
206-
};
207-
}

src/ui/taskCardSecondaryBadges.ts

Lines changed: 78 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import type TaskNotesPlugin from "../main";
33
import type { TaskInfo } from "../types";
44
import { createTaskNotesLogger } from "../utils/tasknotesLogger";
55
import {
6-
createProjectClickHandler,
76
createRecurrenceClickHandler,
87
createReminderClickHandler,
98
} from "./taskCardActions";
@@ -153,6 +152,51 @@ function createChevronClickHandler(
153152
};
154153
}
155154

155+
/**
156+
* Toggles the inline subtask list for a project task card. Used by the folder
157+
* (project) badge so it works whether or not the expand chevron is rendered,
158+
* keeping the chevron visual in sync when it is present. Shares expansion state
159+
* with the chevron via expandedProjectsService, so the two never diverge.
160+
*/
161+
function createProjectSubtasksToggleHandler(
162+
task: TaskInfo,
163+
plugin: TaskNotesPlugin,
164+
card: HTMLElement,
165+
handlers: TaskCardSecondaryBadgeHandlers
166+
): () => void {
167+
return () => {
168+
void (async () => {
169+
const logger = getTaskCardBadgeLogger(plugin);
170+
try {
171+
if (!plugin.expandedProjectsService) {
172+
new Notice("Service not available. Please try reloading the plugin.");
173+
return;
174+
}
175+
176+
const newExpanded = plugin.expandedProjectsService.toggle(
177+
task.path,
178+
shouldExpandSubtasksByDefault(plugin)
179+
);
180+
181+
const chevron = card.querySelector<HTMLElement>(".task-card__chevron");
182+
if (chevron) {
183+
updateChevronElement(chevron, plugin, newExpanded);
184+
}
185+
186+
await handlers.toggleSubtasks(card, task, newExpanded);
187+
} catch (error) {
188+
logger.error("Error toggling project subtasks", {
189+
category: "internal",
190+
operation: "toggle-project-subtasks",
191+
details: { taskPath: task.path },
192+
error,
193+
});
194+
new Notice("Failed to toggle subtasks");
195+
}
196+
})();
197+
};
198+
}
199+
156200
function createBlockingToggleClickHandler(
157201
task: TaskInfo,
158202
card: HTMLElement,
@@ -217,27 +261,31 @@ function renderProjectBadges(
217261
return;
218262
}
219263

264+
const isExpanded = isTaskCardSubtasksExpanded(task, plugin);
265+
266+
// The folder badge toggles the inline subtask list. It is always rendered for
267+
// a project and works on its own, so subtasks remain reachable from the card
268+
// even when the expand chevron is disabled.
220269
createBadgeIndicator({
221270
container: badgesContainer,
222271
className: "task-card__project-indicator",
223272
icon: "folder",
224273
tooltip: tTaskCard(plugin, "projectTooltip"),
225-
onClick: createProjectClickHandler(task, plugin),
274+
onClick: createProjectSubtasksToggleHandler(task, plugin, card, handlers),
226275
});
227276

228-
if (!plugin.settings?.showExpandableSubtasks) {
229-
return;
277+
if (plugin.settings?.showExpandableSubtasks) {
278+
createBadgeIndicator({
279+
container: badgesContainer,
280+
className: `task-card__chevron${isExpanded ? " task-card__chevron--expanded" : ""}`,
281+
icon: "chevron-right",
282+
tooltip: getChevronTooltip(plugin, isExpanded),
283+
onClick: createChevronClickHandler(task, plugin, card, handlers),
284+
});
230285
}
231286

232-
const isExpanded = isTaskCardSubtasksExpanded(task, plugin);
233-
createBadgeIndicator({
234-
container: badgesContainer,
235-
className: `task-card__chevron${isExpanded ? " task-card__chevron--expanded" : ""}`,
236-
icon: "chevron-right",
237-
tooltip: getChevronTooltip(plugin, isExpanded),
238-
onClick: createChevronClickHandler(task, plugin, card, handlers),
239-
});
240-
287+
// Render the subtask list immediately when expanded, regardless of whether the
288+
// chevron is shown — otherwise a folder-driven expansion would not re-render.
241289
if (isExpanded) {
242290
handlers.toggleSubtasks(card, task, true).catch((error: unknown) => {
243291
getTaskCardBadgeLogger(plugin).error("Error showing initial subtasks", {
@@ -346,14 +394,15 @@ function updateProjectBadges(options: UpdateTaskCardSecondaryBadgesOptions): voi
346394
className: "task-card__project-indicator",
347395
icon: "folder",
348396
tooltip: tTaskCard(plugin, "projectTooltip"),
349-
onClick: createProjectClickHandler(task, plugin),
397+
onClick: createProjectSubtasksToggleHandler(task, plugin, card, handlers),
350398
});
351399

352400
const showChevron = isProject && plugin.settings?.showExpandableSubtasks;
353401
const existingChevron = card.querySelector<HTMLElement>(".task-card__chevron");
402+
const isExpanded = isProject && isTaskCardSubtasksExpanded(task, plugin);
354403

404+
// Keep the chevron in sync with the setting (create / update / remove).
355405
if (showChevron && !existingChevron) {
356-
const isExpanded = isTaskCardSubtasksExpanded(task, plugin);
357406
createBadgeIndicator({
358407
container:
359408
card.querySelector<HTMLElement>(".task-card__badges") ?? mainRow ?? card,
@@ -362,35 +411,24 @@ function updateProjectBadges(options: UpdateTaskCardSecondaryBadgesOptions): voi
362411
tooltip: getChevronTooltip(plugin, isExpanded),
363412
onClick: createChevronClickHandler(task, plugin, card, handlers),
364413
});
365-
366-
if (isExpanded) {
367-
handlers.toggleSubtasks(card, task, true).catch((error: unknown) => {
368-
logger.error("Error showing initial subtasks in update", {
369-
category: "internal",
370-
operation: "show-initial-subtasks-update",
371-
details: { taskPath: task.path },
372-
error,
373-
});
374-
});
375-
}
376414
} else if (showChevron && existingChevron) {
377-
const isExpanded = isTaskCardSubtasksExpanded(task, plugin);
378415
updateChevronElement(existingChevron, plugin, isExpanded);
379-
380-
if (isExpanded) {
381-
handlers.toggleSubtasks(card, task, true).catch((error: unknown) => {
382-
logger.error("Error refreshing default-expanded subtasks", {
383-
category: "internal",
384-
operation: "refresh-default-expanded-subtasks",
385-
details: { taskPath: task.path },
386-
error,
387-
});
388-
});
389-
} else {
390-
removeRelationshipContainer(card, ".task-card__subtasks");
391-
}
392416
} else if (!showChevron && existingChevron) {
393417
existingChevron.remove();
418+
}
419+
420+
// Sync the inline subtask list independently of the chevron, so the
421+
// folder badge can drive expansion even when the chevron is disabled.
422+
if (isExpanded) {
423+
handlers.toggleSubtasks(card, task, true).catch((error: unknown) => {
424+
logger.error("Error showing subtasks in update", {
425+
category: "internal",
426+
operation: "show-subtasks-update",
427+
details: { taskPath: task.path },
428+
error,
429+
});
430+
});
431+
} else {
394432
removeRelationshipContainer(card, ".task-card__subtasks");
395433
}
396434
})

tests/unit/ui/taskCardActions.test.ts

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { Notice } from "obsidian";
22
import {
33
createPriorityClickHandler,
4-
createProjectClickHandler,
54
createRecurrenceClickHandler,
65
createReminderClickHandler,
76
createStatusCycleHandler,
@@ -67,7 +66,6 @@ function createPlugin(overrides: Partial<TaskNotesPlugin> = {}): TaskNotesPlugin
6766
...updatedTask,
6867
status: "done",
6968
})),
70-
applyProjectSubtaskFilter: jest.fn(async () => undefined),
7169
...overrides,
7270
} as unknown as TaskNotesPlugin;
7371
}
@@ -193,14 +191,4 @@ describe("taskCardActions", () => {
193191
expect(modalInstance.open).toHaveBeenCalled();
194192
expect(plugin.updateTaskProperty).toHaveBeenCalledWith(task, "reminders", reminders);
195193
});
196-
197-
it("wires project indicators to the project-subtask filter", async () => {
198-
const plugin = createPlugin();
199-
const handler = createProjectClickHandler(task, plugin);
200-
201-
handler();
202-
await flushAsyncHandlers();
203-
204-
expect(plugin.applyProjectSubtaskFilter).toHaveBeenCalledWith(task);
205-
});
206194
});

tests/unit/ui/taskCardSecondaryBadges.test.ts

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,66 @@ describe("taskCardSecondaryBadges", () => {
129129
expect(handlers.toggleSubtasks).toHaveBeenCalledWith(card, task, true);
130130
});
131131

132+
it("toggles the inline subtask list when the project folder badge is clicked", async () => {
133+
const plugin = createPlugin();
134+
(plugin.projectSubtasksService.isTaskUsedAsProjectSync as jest.Mock).mockReturnValue(true);
135+
(plugin.expandedProjectsService?.isExpanded as jest.Mock).mockReturnValue(false);
136+
(plugin.expandedProjectsService?.toggle as jest.Mock).mockReturnValue(true);
137+
const handlers = createHandlers();
138+
const { card, badgesContainer } = createCard();
139+
const task = createTask();
140+
141+
renderTaskCardSecondaryBadges({
142+
card,
143+
badgesContainer,
144+
task,
145+
plugin,
146+
hasDetails: false,
147+
propertyOptions: {},
148+
handlers,
149+
});
150+
151+
const folder = card.querySelector<HTMLElement>(".task-card__project-indicator");
152+
expect(folder).not.toBeNull();
153+
154+
folder?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
155+
await Promise.resolve();
156+
await Promise.resolve();
157+
158+
expect(plugin.expandedProjectsService?.toggle).toHaveBeenCalledWith(task.path, false);
159+
expect(handlers.toggleSubtasks).toHaveBeenCalledWith(card, task, true);
160+
});
161+
162+
it("renders subtasks for an expanded project even when the chevron is disabled", () => {
163+
const plugin = createPlugin({
164+
settings: {
165+
showExpandableSubtasks: false,
166+
expandSubtasksByDefault: false,
167+
enableDebugLogging: false,
168+
},
169+
} as unknown as Partial<TaskNotesPlugin>);
170+
(plugin.projectSubtasksService.isTaskUsedAsProjectSync as jest.Mock).mockReturnValue(true);
171+
(plugin.expandedProjectsService?.isExpanded as jest.Mock).mockReturnValue(true);
172+
const handlers = createHandlers();
173+
const { card, badgesContainer } = createCard();
174+
const task = createTask();
175+
176+
renderTaskCardSecondaryBadges({
177+
card,
178+
badgesContainer,
179+
task,
180+
plugin,
181+
hasDetails: false,
182+
propertyOptions: {},
183+
handlers,
184+
});
185+
186+
// Folder badge is present, chevron is not, but subtasks still render.
187+
expect(card.querySelector(".task-card__project-indicator")).not.toBeNull();
188+
expect(card.querySelector(".task-card__chevron")).toBeNull();
189+
expect(handlers.toggleSubtasks).toHaveBeenCalledWith(card, task, true);
190+
});
191+
132192
it("keeps secondary badges omitted when the option is disabled", () => {
133193
const plugin = createPlugin();
134194
const handlers = createHandlers();

0 commit comments

Comments
 (0)