diff --git a/src/Renderer/QueryResultsRenderer.ts b/src/Renderer/QueryResultsRenderer.ts index 338e4f33a0..702d81757d 100644 --- a/src/Renderer/QueryResultsRenderer.ts +++ b/src/Renderer/QueryResultsRenderer.ts @@ -1,6 +1,7 @@ import type { Component, TFile } from 'obsidian'; import { GlobalFilter } from '../Config/GlobalFilter'; import { GlobalQuery } from '../Config/GlobalQuery'; +import { postponeButtonTitle, shouldShowPostponeButton } from '../DateTime/Postponer'; import type { IQuery } from '../IQuery'; import { QueryLayout } from '../Layout/QueryLayout'; import { TaskLayout } from '../Layout/TaskLayout'; @@ -10,9 +11,9 @@ import { State } from '../Obsidian/Cache'; import type { GroupDisplayHeading } from '../Query/Group/GroupDisplayHeading'; import type { TaskGroups } from '../Query/Group/TaskGroups'; import type { QueryResult } from '../Query/QueryResult'; -import { postponeButtonTitle, shouldShowPostponeButton } from '../DateTime/Postponer'; import type { TasksFile } from '../Scripting/TasksFile'; -import type { Task } from '../Task/Task'; +import type { ListItem } from '../Task/ListItem'; +import { Task } from '../Task/Task'; import { PostponeMenu } from '../ui/Menus/PostponeMenu'; import { TaskLineRenderer, type TextRenderer, createAndAppendElement } from './TaskLineRenderer'; @@ -188,14 +189,16 @@ export class QueryResultsRenderer { // will be empty, and no headings will be added. await this.addGroupHeadings(content, group.groupHeadings); - await this.createTaskList(group.tasks, content, queryRendererParameters); + const renderedTasks: Set = new Set(); + await this.createTaskList(group.tasks, content, queryRendererParameters, renderedTasks); } } private async createTaskList( - tasks: Task[], - content: HTMLDivElement, + tasks: ListItem[], + content: HTMLElement, queryRendererParameters: QueryRendererParameters, + renderedTasks: Set, ): Promise { const taskList = createAndAppendElement('ul', content); @@ -217,12 +220,80 @@ export class QueryResultsRenderer { }); for (const [taskIndex, task] of tasks.entries()) { - await this.addTask(taskList, taskLineRenderer, task, taskIndex, queryRendererParameters); + if (this.alreadyRendered(task, renderedTasks)) { + continue; + } + + if (this.willBeRenderedLater(task, renderedTasks, tasks)) { + continue; + } + + const listItem = await this.addTaskOrListItem( + taskList, + taskLineRenderer, + task, + taskIndex, + queryRendererParameters, + ); + renderedTasks.add(task); + + if (task.children.length > 0) { + await this.createTaskList(task.children, listItem, queryRendererParameters, renderedTasks); + task.children.forEach((childTask) => { + renderedTasks.add(childTask); + }); + } } content.appendChild(taskList); } + private willBeRenderedLater(task: ListItem, renderedTasks: Set, tasks: ListItem[]) { + // Try to find the closest parent that is a task + let closestParentTask = task.parent; + while (closestParentTask !== null && !(closestParentTask instanceof Task)) { + closestParentTask = closestParentTask.parent; + } + + if (!closestParentTask) { + return false; + } + + if (!renderedTasks.has(closestParentTask)) { + // This task is a direct or indirect child of another task that we are waiting to draw, + // so don't draw it yet, it will be done recursively later. + if (tasks.includes(closestParentTask)) { + return true; + } + } + + return false; + } + + private alreadyRendered(task: ListItem, renderedTasks: Set) { + return renderedTasks.has(task); + } + + private async addTaskOrListItem( + taskList: HTMLUListElement, + taskLineRenderer: TaskLineRenderer, + task: ListItem, + taskIndex: number, + queryRendererParameters: QueryRendererParameters, + ) { + if (task instanceof Task) { + return await this.addTask(taskList, taskLineRenderer, task, taskIndex, queryRendererParameters); + } + + return this.addListItem(taskList, task); + } + + private addListItem(taskList: HTMLUListElement, listItem: ListItem) { + const li = createAndAppendElement('li', taskList); + li.textContent = listItem.originalMarkdown; + return li; + } + private async addTask( taskList: HTMLUListElement, taskLineRenderer: TaskLineRenderer, @@ -259,6 +330,8 @@ export class QueryResultsRenderer { } taskList.appendChild(listItem); + + return listItem; } private addEditButton(listItem: HTMLElement, task: Task, queryRendererParameters: QueryRendererParameters) { diff --git a/tests/Renderer/QueryResultsRenderer.test.QueryResultsRenderer_tests_parent-child_items.approved.html b/tests/Renderer/QueryResultsRenderer.test.QueryResultsRenderer_tests_parent-child_items.approved.html index 78627a4aff..421ee93573 100644 --- a/tests/Renderer/QueryResultsRenderer.test.QueryResultsRenderer_tests_parent-child_items.approved.html +++ b/tests/Renderer/QueryResultsRenderer.test.QueryResultsRenderer_tests_parent-child_items.approved.html @@ -19,86 +19,104 @@ - -
  • - - - child 1 - - - - ( - inheritance_rendering_sample - ) - - - -
  • -
  • - - - grandchild 1 - - - - ( - inheritance_rendering_sample - ) - - - -
  • -
  • - - - child 2 - - - - ( - inheritance_rendering_sample - ) - - - -
  • -
  • - - - grand grand child - - - - ( - inheritance_rendering_sample - ) - - - +
  • + +
    6 tasks
    + diff --git a/tests/Renderer/QueryResultsRenderer.test.QueryResultsRenderer_tests_should_render_tasks_without_their_parents.approved.html b/tests/Renderer/QueryResultsRenderer.test.QueryResultsRenderer_tests_should_render_tasks_without_their_parents.approved.html new file mode 100644 index 0000000000..84b498debe --- /dev/null +++ b/tests/Renderer/QueryResultsRenderer.test.QueryResultsRenderer_tests_should_render_tasks_without_their_parents.approved.html @@ -0,0 +1,65 @@ +
    + +
    3 tasks
    +
    diff --git a/tests/Renderer/QueryResultsRenderer.test.ts b/tests/Renderer/QueryResultsRenderer.test.ts index 70eed7204a..e3f7e89a82 100644 --- a/tests/Renderer/QueryResultsRenderer.test.ts +++ b/tests/Renderer/QueryResultsRenderer.test.ts @@ -7,6 +7,7 @@ import { State } from '../../src/Obsidian/Cache'; import { QueryResultsRenderer } from '../../src/Renderer/QueryResultsRenderer'; import { TasksFile } from '../../src/Scripting/TasksFile'; import { inheritance_rendering_sample } from '../Obsidian/__test_data__/inheritance_rendering_sample'; +import { inheritance_task_2listitem_3task } from '../Obsidian/__test_data__/inheritance_task_2listitem_3task'; import { readTasksFromSimulatedFile } from '../Obsidian/SimulatedFile'; import { verifyWithFileExtension } from '../TestingTools/ApprovalTestHelpers'; import { prettifyHTML } from '../TestingTools/HTMLHelpers'; @@ -25,10 +26,10 @@ afterEach(() => { }); describe('QueryResultsRenderer tests', () => { - async function verifyRenderedTasksHTML(allTasks: Task[]) { + async function verifyRenderedTasksHTML(allTasks: Task[], source: string = '') { const renderer = new QueryResultsRenderer( 'block-language-tasks', - '', + source, new TasksFile('query.md'), () => Promise.resolve(), null, @@ -56,6 +57,17 @@ describe('QueryResultsRenderer tests', () => { it('parent-child items', async () => { const allTasks = readTasksFromSimulatedFile(inheritance_rendering_sample); - await verifyRenderedTasksHTML(allTasks); + await verifyRenderedTasksHTML(allTasks, 'sort by function task.lineNumber'); + }); + + it('parent-child items reverse sorted', async () => { + const allTasks = readTasksFromSimulatedFile(inheritance_rendering_sample); + await verifyRenderedTasksHTML(allTasks, 'sort by function reverse task.lineNumber'); + }); + + it('should render tasks without their parents', async () => { + // example chosen to match subtasks whose parents do not match the query + const allTasks = readTasksFromSimulatedFile(inheritance_task_2listitem_3task); + await verifyRenderedTasksHTML(allTasks, 'description includes grandchild'); }); });