Skip to content

Commit b4e8731

Browse files
authored
Merge pull request #3706 from ilandikov/fix/results-filtered-correctly
fix: Results filter now re-applied when settings or tasks edited
2 parents b95417f + 74659ff commit b4e8731

5 files changed

+215
-22
lines changed

src/Renderer/QueryResultsRenderer.ts

Lines changed: 27 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ export class QueryResultsRenderer {
6262
protected queryType: string; // whilst there is only one query type, there is no point logging this value
6363
public queryResult: QueryResult;
6464
public filteredQueryResult: QueryResult;
65+
private _filterString: string = '';
6566

6667
constructor(
6768
className: string,
@@ -119,6 +120,10 @@ export class QueryResultsRenderer {
119120
this.markdownRenderer = new MarkdownQueryResultsRenderer(getters);
120121
}
121122

123+
public get filterString(): string {
124+
return this._filterString;
125+
}
126+
122127
private makeQueryFromSourceAndTasksFile() {
123128
return getQueryForQueryRenderer(this.source, GlobalQuery.getInstance(), this.tasksFile);
124129
}
@@ -157,14 +162,14 @@ export class QueryResultsRenderer {
157162
public async render(state: State, tasks: Task[], content: HTMLDivElement) {
158163
this.performSearch(tasks);
159164
this.addToolbar(content);
160-
await this.renderQueryResult(state, this.queryResult, content);
165+
await this.renderQueryResult(state, this.filteredQueryResult, content);
161166
}
162167

163168
private performSearch(tasks: Task[]) {
164169
const measureSearch = new PerformanceTracker(`Search: ${this.query.queryId} - ${this.filePath}`);
165170
measureSearch.start();
166171
this.queryResult = this.query.applyQueryToTasks(tasks);
167-
this.filteredQueryResult = this.queryResult;
172+
this.filterResults();
168173
measureSearch.finish();
169174
}
170175

@@ -191,24 +196,21 @@ export class QueryResultsRenderer {
191196
const label = createAndAppendElement('label', toolbar);
192197
setIcon(label, 'lucide-filter');
193198
const searchBox = createAndAppendElement('input', label);
199+
searchBox.value = this._filterString;
194200
searchBox.placeholder = 'Filter by description...';
195201
setTooltip(searchBox, 'Filter results');
196202
searchBox.addEventListener('input', async () => {
197203
const filterString = searchBox.value;
198-
await this.applySearchBoxFilter(filterString, content);
204+
await this.applySearchBoxFilterAndRerender(filterString, content);
199205
});
200206
}
201207

202-
public async applySearchBoxFilter(filterString: string, content: HTMLDivElement) {
203-
const { filter, error } = new DescriptionField().createFilterOrErrorMessage(
204-
'description includes ' + filterString,
205-
);
206-
if (error) {
207-
new Notice('error searching for ' + filterString + ': ' + error);
208-
return;
209-
}
208+
public async applySearchBoxFilterAndRerender(filterString: string, content: HTMLDivElement) {
209+
this._filterString = filterString;
210210

211-
// We want to retain the Toolbar, to not lose the search string.
211+
this.filterResults();
212+
213+
// We want to retain the Toolbar, to not lose the cursor position in the search string.
212214
// But we need to delete any pre-existing headings, tasks and task count.
213215
// The following while loop relies on the Toolbar being the first element.
214216
while (content.firstElementChild !== content.lastElementChild) {
@@ -220,10 +222,22 @@ export class QueryResultsRenderer {
220222
lastChild.remove();
221223
}
222224

223-
this.filteredQueryResult = this.queryResult.applyFilter(filter!);
224225
await this.renderQueryResult(State.Warm, this.filteredQueryResult, content);
225226
}
226227

228+
private filterResults() {
229+
const { filter, error } = new DescriptionField().createFilterOrErrorMessage(
230+
'description includes ' + this._filterString,
231+
);
232+
if (error) {
233+
// If we can't create a filter, just silently show all the matching tasks
234+
this.filteredQueryResult = this.queryResult;
235+
return;
236+
}
237+
238+
this.filteredQueryResult = this.queryResult.applyFilter(filter!);
239+
}
240+
227241
private addCopyButton(toolbar: HTMLDivElement) {
228242
const copyButton = createAndAppendElement('button', toolbar);
229243
setIcon(copyButton, 'lucide-copy');

tests/Renderer/QueryResultsRenderer.test.QueryResultsRenderer_-_sequences_global_query_change_to_query_layout_option.approved.html

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
<h2>Initial results:</h2>
22

3+
<p>Results filter: ''</p>
34
<!--
45
- [ ] parent 📅 2025-12-01
56
- [ ] child 🆔 childID
@@ -70,6 +71,7 @@ <h2>Initial results:</h2>
7071
</div>
7172
<h2>Check that urgency is shown by global query:</h2>
7273

74+
<p>Results filter: ''</p>
7375
<!--
7476
- [ ] parent 📅 2025-12-01
7577
- [ ] child 🆔 childID

tests/Renderer/QueryResultsRenderer.test.QueryResultsRenderer_-_sequences_global_query_change_to_task_layout_option.approved.html

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
<h2>Initial results:</h2>
22

3+
<p>Results filter: ''</p>
34
<!--
45
- [ ] parent 📅 2025-12-01
56
- [ ] child 🆔 childID
@@ -70,6 +71,7 @@ <h2>Initial results:</h2>
7071
</div>
7172
<h2>Check that due date is hidden by global query:</h2>
7273

74+
<p>Results filter: ''</p>
7375
<!--
7476
- [ ] parent 📅 2025-12-01
7577
- [ ] child 🆔 childID
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
<h2>Initial results - expect 2 tasks:</h2>
2+
3+
<p>Results filter: ''</p>
4+
<!--
5+
- [ ] parent 📅 2025-12-01
6+
- [ ] child 🆔 childID
7+
-->
8+
9+
<div>
10+
<div class="plugin-tasks-toolbar">
11+
<label test-icon="lucide-filter">
12+
<input placeholder="Filter by description..." test-tooltip="Filter results" />
13+
</label>
14+
<button test-icon="lucide-copy" test-tooltip="Copy results"></button>
15+
</div>
16+
<ul class="contains-task-list plugin-tasks-query-result tasks-layout-hide-urgency">
17+
<li
18+
class="task-list-item plugin-tasks-list-item"
19+
data-task-due="today"
20+
data-task-priority="normal"
21+
data-task=""
22+
data-line="0"
23+
data-task-status-name="Todo"
24+
data-task-status-type="TODO">
25+
<input class="task-list-item-checkbox" type="checkbox" data-line="0" />
26+
<span class="tasks-list-text">
27+
<span class="task-description"><span>parent</span></span>
28+
<span class="task-due" data-task-due="today" title="Click to edit due date, Right-click for more options">
29+
<span>📅 2025-12-01</span>
30+
</span>
31+
</span>
32+
<span class="task-extras">
33+
<span class="tasks-backlink">
34+
(
35+
<a rel="noopener" target="_blank" class="internal-link">/</a>
36+
)
37+
</span>
38+
<a class="tasks-edit" title="Edit task" href="#"></a>
39+
<a class="tasks-postpone" title="ℹ️ Due tomorrow, on Tue 2nd Dec (right-click for more options)"></a>
40+
</span>
41+
</li>
42+
<li
43+
class="task-list-item plugin-tasks-list-item"
44+
data-task-priority="normal"
45+
data-task=""
46+
data-line="1"
47+
data-task-status-name="Todo"
48+
data-task-status-type="TODO">
49+
<input class="task-list-item-checkbox" type="checkbox" data-line="1" />
50+
<span class="tasks-list-text">
51+
<span class="task-description"><span>child</span></span>
52+
<span class="task-id"><span>🆔 childID</span></span>
53+
</span>
54+
<span class="task-extras">
55+
<span class="tasks-backlink">
56+
(
57+
<a rel="noopener" target="_blank" class="internal-link">/</a>
58+
)
59+
</span>
60+
<a class="tasks-edit" title="Edit task" href="#"></a>
61+
</span>
62+
</li>
63+
</ul>
64+
<div class="task-count">2 tasks</div>
65+
</div>
66+
<h2>Filtered results (parent) - expect 1 task:</h2>
67+
68+
<p>Results filter: 'parent'</p>
69+
<!--
70+
- [ ] parent 📅 2025-12-01
71+
- [ ] child 🆔 childID
72+
-->
73+
74+
<div>
75+
<div class="plugin-tasks-toolbar">
76+
<label test-icon="lucide-filter">
77+
<input placeholder="Filter by description..." test-tooltip="Filter results" />
78+
</label>
79+
<button test-icon="lucide-copy" test-tooltip="Copy results"></button>
80+
</div>
81+
<ul class="contains-task-list plugin-tasks-query-result tasks-layout-hide-urgency">
82+
<li
83+
class="task-list-item plugin-tasks-list-item"
84+
data-task-due="today"
85+
data-task-priority="normal"
86+
data-task=""
87+
data-line="0"
88+
data-task-status-name="Todo"
89+
data-task-status-type="TODO">
90+
<input class="task-list-item-checkbox" type="checkbox" data-line="0" />
91+
<span class="tasks-list-text">
92+
<span class="task-description"><span>parent</span></span>
93+
<span class="task-due" data-task-due="today" title="Click to edit due date, Right-click for more options">
94+
<span>📅 2025-12-01</span>
95+
</span>
96+
</span>
97+
<span class="task-extras">
98+
<span class="tasks-backlink">
99+
(
100+
<a rel="noopener" target="_blank" class="internal-link">/</a>
101+
)
102+
</span>
103+
<a class="tasks-edit" title="Edit task" href="#"></a>
104+
<a class="tasks-postpone" title="ℹ️ Due tomorrow, on Tue 2nd Dec (right-click for more options)"></a>
105+
</span>
106+
</li>
107+
</ul>
108+
<div class="task-count">1 task</div>
109+
</div>
110+
<h2>Filtered results after editing Global Query - expect same 1 task:</h2>
111+
112+
<p>Results filter: 'parent'</p>
113+
<!--
114+
- [ ] parent 📅 2025-12-01
115+
- [ ] child 🆔 childID
116+
-->
117+
118+
<div>
119+
<div class="plugin-tasks-toolbar">
120+
<label test-icon="lucide-filter">
121+
<input placeholder="Filter by description..." test-tooltip="Filter results" />
122+
</label>
123+
<button test-icon="lucide-copy" test-tooltip="Copy results"></button>
124+
</div>
125+
<ul class="contains-task-list plugin-tasks-query-result tasks-layout-hide-urgency">
126+
<li
127+
class="task-list-item plugin-tasks-list-item"
128+
data-task-due="today"
129+
data-task-priority="normal"
130+
data-task=""
131+
data-line="0"
132+
data-task-status-name="Todo"
133+
data-task-status-type="TODO">
134+
<input class="task-list-item-checkbox" type="checkbox" data-line="0" />
135+
<span class="tasks-list-text">
136+
<span class="task-description"><span>parent</span></span>
137+
<span class="task-due" data-task-due="today" title="Click to edit due date, Right-click for more options">
138+
<span>📅 2025-12-01</span>
139+
</span>
140+
</span>
141+
<span class="task-extras">
142+
<span class="tasks-backlink">
143+
(
144+
<a rel="noopener" target="_blank" class="internal-link">/</a>
145+
)
146+
</span>
147+
<a class="tasks-edit" title="Edit task" href="#"></a>
148+
<a class="tasks-postpone" title="ℹ️ Due tomorrow, on Tue 2nd Dec (right-click for more options)"></a>
149+
</span>
150+
</li>
151+
</ul>
152+
<div class="task-count">1 task</div>
153+
</div>

tests/Renderer/QueryResultsRenderer.test.ts

Lines changed: 31 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ describe('QueryResultsRenderer - accessing results', () => {
7979

8080
await renderer.render(State.Warm, twoTasks, document.createElement('div'));
8181

82-
await renderer.applySearchBoxFilter('another', document.createElement('div'));
82+
await renderer.applySearchBoxFilterAndRerender('another', document.createElement('div'));
8383

8484
expect(renderer.queryResult.totalTasksCount).toEqual(2);
8585
expect(renderer.filteredQueryResult.totalTasksCount).toEqual(1);
@@ -171,19 +171,25 @@ class RendererStoryboard {
171171
}
172172

173173
/**
174+
* This simulates QueryRenderer.renderResults()
174175
* Returns the prettified rendered HTML, to allow 'expect' calls to be added.
175176
* @param description
176177
*/
177-
public async addFrame(description: string): Promise<string> {
178-
this.output += `<h2>${description}:</h2>\n\n`;
179-
178+
public async renderAndAddFrame(description: string) {
180179
const container = document.createElement('div');
181180
await this.renderer.render(State.Warm, this.allTasks, container);
182181

182+
return this.addFrame(description, container);
183+
}
184+
185+
public addFrame(description: string, container: HTMLDivElement) {
186+
this.output += `<h2>${description}:</h2>\n\n`;
187+
this.output += `<p>Results filter: '${this.renderer.filterString}'</p>\n`;
188+
183189
const { tasksAsMarkdown, prettyHTML } = tasksMarkdownAndPrettifiedHtml(container, this.allTasks);
184190
this.output += tasksAsMarkdown + prettyHTML;
185191

186-
return prettyHTML;
192+
return { prettyHTML, container };
187193
}
188194

189195
public verify() {
@@ -203,15 +209,15 @@ describe('QueryResultsRenderer - sequences', () => {
203209
const dueDate = '📅 2025-12-01';
204210

205211
{
206-
const prettyHTML = await storyboard.addFrame('Initial results');
212+
const { prettyHTML } = await storyboard.renderAndAddFrame('Initial results');
207213
expect(prettyHTML).toContain(dueDate);
208214
}
209215

210216
GlobalQuery.getInstance().set('hide due date');
211217
storyboard.renderer.rereadQueryFromFile();
212218

213219
{
214-
const prettyHTML = await storyboard.addFrame('Check that due date is hidden by global query');
220+
const { prettyHTML } = await storyboard.renderAndAddFrame('Check that due date is hidden by global query');
215221
expect(prettyHTML).not.toContain(dueDate);
216222
}
217223

@@ -224,18 +230,34 @@ describe('QueryResultsRenderer - sequences', () => {
224230
const urgency = '<span class="tasks-urgency">10.75</span>';
225231

226232
{
227-
const prettyHTML = await storyboard.addFrame('Initial results');
233+
const { prettyHTML } = await storyboard.renderAndAddFrame('Initial results');
228234
expect(prettyHTML).not.toContain(urgency);
229235
}
230236

231237
GlobalQuery.getInstance().set('show urgency');
232238
storyboard.renderer.rereadQueryFromFile();
233239

234240
{
235-
const prettyHTML = await storyboard.addFrame('Check that urgency is shown by global query');
241+
const { prettyHTML } = await storyboard.renderAndAddFrame('Check that urgency is shown by global query');
236242
expect(prettyHTML).toContain(urgency);
237243
}
238244

239245
storyboard.verify();
240246
});
247+
248+
it('rerendered results retain the filter', async () => {
249+
const storyboard = new RendererStoryboard('', parentAndChild);
250+
251+
const { container } = await storyboard.renderAndAddFrame('Initial results - expect 2 tasks');
252+
253+
await storyboard.renderer.applySearchBoxFilterAndRerender('parent', container);
254+
storyboard.addFrame('Filtered results (parent) - expect 1 task', container);
255+
256+
GlobalQuery.getInstance().set('sort by function reverse task.description.length');
257+
storyboard.renderer.rereadQueryFromFile();
258+
259+
await storyboard.renderAndAddFrame('Filtered results after editing Global Query - expect same 1 task');
260+
261+
storyboard.verify();
262+
});
241263
});

0 commit comments

Comments
 (0)