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 @@
Results filter: ''
+ +Results filter: 'parent'
+ + +Results filter: 'parent'
+ + +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(); + }); });