diff --git a/src/Renderer/QueryResultsRenderer.ts b/src/Renderer/QueryResultsRenderer.ts index 5c15e1a1a0..44316722c5 100644 --- a/src/Renderer/QueryResultsRenderer.ts +++ b/src/Renderer/QueryResultsRenderer.ts @@ -62,6 +62,7 @@ export class QueryResultsRenderer { protected queryType: string; // whilst there is only one query type, there is no point logging this value public queryResult: QueryResult; public filteredQueryResult: QueryResult; + private _filterString: string = ''; constructor( className: string, @@ -119,6 +120,10 @@ export class QueryResultsRenderer { this.markdownRenderer = new MarkdownQueryResultsRenderer(getters); } + public get filterString(): string { + return this._filterString; + } + private makeQueryFromSourceAndTasksFile() { return getQueryForQueryRenderer(this.source, GlobalQuery.getInstance(), this.tasksFile); } @@ -157,14 +162,14 @@ export class QueryResultsRenderer { public async render(state: State, tasks: Task[], content: HTMLDivElement) { this.performSearch(tasks); this.addToolbar(content); - await this.renderQueryResult(state, this.queryResult, content); + await this.renderQueryResult(state, this.filteredQueryResult, content); } private performSearch(tasks: Task[]) { const measureSearch = new PerformanceTracker(`Search: ${this.query.queryId} - ${this.filePath}`); measureSearch.start(); this.queryResult = this.query.applyQueryToTasks(tasks); - this.filteredQueryResult = this.queryResult; + this.filterResults(); measureSearch.finish(); } @@ -191,24 +196,21 @@ export class QueryResultsRenderer { const label = createAndAppendElement('label', toolbar); setIcon(label, 'lucide-filter'); const searchBox = createAndAppendElement('input', label); + searchBox.value = this._filterString; searchBox.placeholder = 'Filter by description...'; setTooltip(searchBox, 'Filter results'); searchBox.addEventListener('input', async () => { const filterString = searchBox.value; - await this.applySearchBoxFilter(filterString, content); + await this.applySearchBoxFilterAndRerender(filterString, content); }); } - public async applySearchBoxFilter(filterString: string, content: HTMLDivElement) { - const { filter, error } = new DescriptionField().createFilterOrErrorMessage( - 'description includes ' + filterString, - ); - if (error) { - new Notice('error searching for ' + filterString + ': ' + error); - return; - } + public async applySearchBoxFilterAndRerender(filterString: string, content: HTMLDivElement) { + this._filterString = filterString; - // We want to retain the Toolbar, to not lose the search string. + this.filterResults(); + + // We want to retain the Toolbar, to not lose the cursor position in the search string. // But we need to delete any pre-existing headings, tasks and task count. // The following while loop relies on the Toolbar being the first element. while (content.firstElementChild !== content.lastElementChild) { @@ -220,10 +222,22 @@ export class QueryResultsRenderer { lastChild.remove(); } - this.filteredQueryResult = this.queryResult.applyFilter(filter!); await this.renderQueryResult(State.Warm, this.filteredQueryResult, content); } + private filterResults() { + const { filter, error } = new DescriptionField().createFilterOrErrorMessage( + 'description includes ' + this._filterString, + ); + if (error) { + // If we can't create a filter, just silently show all the matching tasks + this.filteredQueryResult = this.queryResult; + return; + } + + this.filteredQueryResult = this.queryResult.applyFilter(filter!); + } + private addCopyButton(toolbar: HTMLDivElement) { const copyButton = createAndAppendElement('button', toolbar); setIcon(copyButton, 'lucide-copy'); diff --git a/tests/Renderer/QueryResultsRenderer.test.QueryResultsRenderer_-_sequences_global_query_change_to_query_layout_option.approved.html b/tests/Renderer/QueryResultsRenderer.test.QueryResultsRenderer_-_sequences_global_query_change_to_query_layout_option.approved.html index 22fd8993d3..346f3e9bf4 100644 --- a/tests/Renderer/QueryResultsRenderer.test.QueryResultsRenderer_-_sequences_global_query_change_to_query_layout_option.approved.html +++ b/tests/Renderer/QueryResultsRenderer.test.QueryResultsRenderer_-_sequences_global_query_change_to_query_layout_option.approved.html @@ -1,5 +1,6 @@

Initial results:

+

Results filter: ''

+ +
+
+ + +
+ +
2 tasks
+
+

Filtered results (parent) - expect 1 task:

+ +

Results filter: 'parent'

+ + +
+
+ + +
+ +
1 task
+
+

Filtered results after editing Global Query - expect same 1 task:

+ +

Results filter: 'parent'

+ + +
+
+ + +
+ +
1 task
+
diff --git a/tests/Renderer/QueryResultsRenderer.test.ts b/tests/Renderer/QueryResultsRenderer.test.ts index a760101136..4d2a688c4f 100644 --- a/tests/Renderer/QueryResultsRenderer.test.ts +++ b/tests/Renderer/QueryResultsRenderer.test.ts @@ -79,7 +79,7 @@ describe('QueryResultsRenderer - accessing results', () => { await renderer.render(State.Warm, twoTasks, document.createElement('div')); - await renderer.applySearchBoxFilter('another', document.createElement('div')); + await renderer.applySearchBoxFilterAndRerender('another', document.createElement('div')); expect(renderer.queryResult.totalTasksCount).toEqual(2); expect(renderer.filteredQueryResult.totalTasksCount).toEqual(1); @@ -171,19 +171,25 @@ class RendererStoryboard { } /** + * This simulates QueryRenderer.renderResults() * Returns the prettified rendered HTML, to allow 'expect' calls to be added. * @param description */ - public async addFrame(description: string): Promise { - this.output += `

${description}:

\n\n`; - + public async renderAndAddFrame(description: string) { const container = document.createElement('div'); await this.renderer.render(State.Warm, this.allTasks, container); + return this.addFrame(description, container); + } + + public addFrame(description: string, container: HTMLDivElement) { + this.output += `

${description}:

\n\n`; + this.output += `

Results filter: '${this.renderer.filterString}'

\n`; + const { tasksAsMarkdown, prettyHTML } = tasksMarkdownAndPrettifiedHtml(container, this.allTasks); this.output += tasksAsMarkdown + prettyHTML; - return prettyHTML; + return { prettyHTML, container }; } public verify() { @@ -203,7 +209,7 @@ describe('QueryResultsRenderer - sequences', () => { const dueDate = '📅 2025-12-01'; { - const prettyHTML = await storyboard.addFrame('Initial results'); + const { prettyHTML } = await storyboard.renderAndAddFrame('Initial results'); expect(prettyHTML).toContain(dueDate); } @@ -211,7 +217,7 @@ describe('QueryResultsRenderer - sequences', () => { storyboard.renderer.rereadQueryFromFile(); { - const prettyHTML = await storyboard.addFrame('Check that due date is hidden by global query'); + const { prettyHTML } = await storyboard.renderAndAddFrame('Check that due date is hidden by global query'); expect(prettyHTML).not.toContain(dueDate); } @@ -224,7 +230,7 @@ describe('QueryResultsRenderer - sequences', () => { const urgency = '10.75'; { - const prettyHTML = await storyboard.addFrame('Initial results'); + const { prettyHTML } = await storyboard.renderAndAddFrame('Initial results'); expect(prettyHTML).not.toContain(urgency); } @@ -232,10 +238,26 @@ describe('QueryResultsRenderer - sequences', () => { storyboard.renderer.rereadQueryFromFile(); { - const prettyHTML = await storyboard.addFrame('Check that urgency is shown by global query'); + const { prettyHTML } = await storyboard.renderAndAddFrame('Check that urgency is shown by global query'); expect(prettyHTML).toContain(urgency); } storyboard.verify(); }); + + it('rerendered results retain the filter', async () => { + const storyboard = new RendererStoryboard('', parentAndChild); + + const { container } = await storyboard.renderAndAddFrame('Initial results - expect 2 tasks'); + + await storyboard.renderer.applySearchBoxFilterAndRerender('parent', container); + storyboard.addFrame('Filtered results (parent) - expect 1 task', container); + + GlobalQuery.getInstance().set('sort by function reverse task.description.length'); + storyboard.renderer.rereadQueryFromFile(); + + await storyboard.renderAndAddFrame('Filtered results after editing Global Query - expect same 1 task'); + + storyboard.verify(); + }); });