Skip to content

Commit e3d25e5

Browse files
authored
Merge pull request #3705 from ilandikov/fix/copy-filtered-results
fix: copy results provides filtered results
2 parents 5e0eefb + 3d9f9ce commit e3d25e5

File tree

2 files changed

+84
-33
lines changed

2 files changed

+84
-33
lines changed

src/Renderer/QueryResultsRenderer.ts

Lines changed: 48 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import type { IQuery } from '../IQuery';
44
import { PerformanceTracker } from '../lib/PerformanceTracker';
55
import { State } from '../Obsidian/Cache';
66
import { DescriptionField } from '../Query/Filter/DescriptionField';
7+
import { Query } from '../Query/Query';
78
import { getQueryForQueryRenderer } from '../Query/QueryRendererHelper';
89
import type { QueryResult } from '../Query/QueryResult';
910
import type { TasksFile } from '../Scripting/TasksFile';
@@ -58,6 +59,8 @@ export class QueryResultsRenderer {
5859
private _tasksFile: TasksFile;
5960

6061
public query: IQuery;
62+
public queryResult: QueryResult;
63+
public filteredQueryResult: QueryResult;
6164
protected queryType: string; // whilst there is only one query type, there is no point logging this value
6265

6366
constructor(
@@ -79,6 +82,10 @@ export class QueryResultsRenderer {
7982
this.source = source;
8083
this._tasksFile = tasksFile;
8184

85+
// Store empty query result for now
86+
this.queryResult = new Query('').applyQueryToTasks([]);
87+
this.filteredQueryResult = this.queryResult;
88+
8289
// The engine is chosen on the basis of the code block language. Currently,
8390
// there is only the main engine for the plugin, this allows others to be
8491
// added later.
@@ -148,18 +155,17 @@ export class QueryResultsRenderer {
148155
}
149156

150157
public async render(state: State, tasks: Task[], content: HTMLDivElement) {
151-
const queryResult = this.performSearch(tasks);
152-
153-
this.addToolbar(queryResult, content);
154-
await this.renderQueryResult(state, queryResult, content);
158+
this.performSearch(tasks);
159+
this.addToolbar(content);
160+
await this.renderQueryResult(state, this.queryResult, content);
155161
}
156162

157163
private performSearch(tasks: Task[]) {
158164
const measureSearch = new PerformanceTracker(`Search: ${this.query.queryId} - ${this.filePath}`);
159165
measureSearch.start();
160-
const queryResult = this.query.applyQueryToTasks(tasks);
166+
this.queryResult = this.query.applyQueryToTasks(tasks);
167+
this.filteredQueryResult = this.queryResult;
161168
measureSearch.finish();
162-
return queryResult;
163169
}
164170

165171
private async renderQueryResult(state: State, queryResult: QueryResult, content: HTMLDivElement) {
@@ -170,58 +176,67 @@ export class QueryResultsRenderer {
170176
measureRender.finish();
171177
}
172178

173-
private addToolbar(queryResult: QueryResult, content: HTMLDivElement) {
179+
private addToolbar(content: HTMLDivElement) {
174180
if (this.query.queryLayoutOptions.hideToolbar) {
175181
return;
176182
}
177183

178184
const toolbar = createAndAppendElement('div', content);
179185
toolbar.classList.add('plugin-tasks-toolbar');
180-
this.addSearchBox(toolbar, queryResult, content);
181-
this.addCopyButton(toolbar, queryResult);
186+
this.addSearchBox(toolbar, content);
187+
this.addCopyButton(toolbar);
182188
}
183189

184-
private addSearchBox(toolbar: HTMLDivElement, queryResult: QueryResult, content: HTMLDivElement) {
190+
private addSearchBox(toolbar: HTMLDivElement, content: HTMLDivElement) {
185191
const label = createAndAppendElement('label', toolbar);
186192
setIcon(label, 'lucide-filter');
187193
const searchBox = createAndAppendElement('input', label);
188194
searchBox.placeholder = 'Filter by description...';
189195
setTooltip(searchBox, 'Filter results');
190196
searchBox.addEventListener('input', async () => {
191-
const { filter, error } = new DescriptionField().createFilterOrErrorMessage(
192-
'description includes ' + searchBox.value,
193-
);
194-
if (error) {
195-
new Notice('error searching for ' + searchBox.value + ': ' + error);
196-
return;
197-
}
197+
const filterString = searchBox.value;
198+
await this.applySearchBoxFilter(filterString, content);
199+
});
200+
}
198201

199-
// We want to retain the Toolbar, to not lose the search string.
200-
// But we need to delete any pre-existing headings, tasks and task count.
201-
// The following while loop relies on the Toolbar being the first element.
202-
while (content.firstElementChild !== content.lastElementChild) {
203-
const lastChild = content.lastChild;
204-
if (lastChild === null) {
205-
break;
206-
}
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+
}
207210

208-
lastChild.remove();
211+
// We want to retain the Toolbar, to not lose the search string.
212+
// But we need to delete any pre-existing headings, tasks and task count.
213+
// The following while loop relies on the Toolbar being the first element.
214+
while (content.firstElementChild !== content.lastElementChild) {
215+
const lastChild = content.lastChild;
216+
if (lastChild === null) {
217+
break;
209218
}
210219

211-
const filteredQueryResult = queryResult.applyFilter(filter!);
212-
await this.renderQueryResult(State.Warm, filteredQueryResult, content);
213-
});
220+
lastChild.remove();
221+
}
222+
223+
this.filteredQueryResult = this.queryResult.applyFilter(filter!);
224+
await this.renderQueryResult(State.Warm, this.filteredQueryResult, content);
214225
}
215226

216-
private addCopyButton(toolbar: HTMLDivElement, queryResult: QueryResult) {
227+
private addCopyButton(toolbar: HTMLDivElement) {
217228
const copyButton = createAndAppendElement('button', toolbar);
218229
setIcon(copyButton, 'lucide-copy');
219230
setTooltip(copyButton, 'Copy results');
220231
copyButton.addEventListener('click', async () => {
221-
// TODO reimplement this using QueryResult.asMarkdown() when it supports trees and list items.
222-
await this.markdownRenderer.renderQuery(State.Warm, queryResult);
223-
await navigator.clipboard.writeText(this.markdownRenderer.markdown);
232+
const markdown = await this.resultsAsMarkdown();
233+
await navigator.clipboard.writeText(markdown);
224234
new Notice('Results copied to clipboard');
225235
});
226236
}
237+
238+
public async resultsAsMarkdown() {
239+
await this.markdownRenderer.renderQuery(State.Warm, this.filteredQueryResult);
240+
return this.markdownRenderer.markdown;
241+
}
227242
}

tests/Renderer/QueryResultsRenderer.test.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,42 @@ function makeQueryResultsRenderer(source: string, tasksFile: TasksFile, allTasks
4545
);
4646
}
4747

48+
describe('QueryResultsRenderer - accessing results', () => {
49+
const aTask = [new TaskBuilder().description('task').build()];
50+
const twoTasks = [...aTask, new TaskBuilder().description('another task').build()];
51+
52+
it('should have empty results before rendering', () => {
53+
const renderer = makeQueryResultsRenderer('', new TasksFile('file.md'), aTask);
54+
55+
expect(renderer.queryResult.totalTasksCount).toEqual(0);
56+
expect(renderer.filteredQueryResult.totalTasksCount).toEqual(0);
57+
});
58+
59+
it('should have actual results after rendering', async () => {
60+
const renderer = makeQueryResultsRenderer('', new TasksFile('file.md'), aTask);
61+
62+
await renderer.render(State.Warm, aTask, document.createElement('div'));
63+
64+
expect(renderer.queryResult.totalTasksCount).toEqual(1);
65+
expect(renderer.filteredQueryResult.totalTasksCount).toEqual(1);
66+
});
67+
68+
it('should have actual result after filtering results', async () => {
69+
const renderer = makeQueryResultsRenderer('', new TasksFile('file.md'), twoTasks);
70+
71+
await renderer.render(State.Warm, twoTasks, document.createElement('div'));
72+
73+
await renderer.applySearchBoxFilter('another', document.createElement('div'));
74+
75+
expect(renderer.queryResult.totalTasksCount).toEqual(2);
76+
expect(renderer.filteredQueryResult.totalTasksCount).toEqual(1);
77+
expect(await renderer.resultsAsMarkdown()).toMatchInlineSnapshot(`
78+
"- [ ] another task
79+
"
80+
`);
81+
});
82+
});
83+
4884
describe('QueryResultsRenderer - rendering queries', () => {
4985
it('should render the toolbar', async () => {
5086
const source = 'show toolbar';

0 commit comments

Comments
 (0)