Skip to content

Commit 63bd504

Browse files
authored
Merge pull request #3697 from ilandikov/refactor/filter-query-results
refactor: filter query results
2 parents 76c543b + bb3564d commit 63bd504

File tree

4 files changed

+165
-32
lines changed

4 files changed

+165
-32
lines changed

src/Query/QueryResult.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1+
import { TasksFile } from '../Scripting/TasksFile';
12
import type { Task } from '../Task/Task';
3+
import type { Filter } from './Filter/Filter';
24
import { TaskGroups } from './Group/TaskGroups';
35
import type { TaskGroup } from './Group/TaskGroup';
46
import { SearchInfo } from './SearchInfo';
@@ -96,4 +98,21 @@ export class QueryResult {
9698
public toFileLineString(task: Task): string {
9799
return `- [${task.status.symbol}] ${task.toString()}`;
98100
}
101+
102+
/**
103+
* This is known to not work reliably for filters that use query.allTasks and query.file.*
104+
*
105+
* @param filter
106+
*/
107+
public applyFilter(filter: Filter): QueryResult {
108+
const queryResultTasks = this.taskGroups.groups.flatMap((group) => group.tasks);
109+
const searchInfo = new SearchInfo(new TasksFile('fix_me.md'), queryResultTasks);
110+
const filterFunction = (task: Task) => filter.filterFunction(task, searchInfo);
111+
const filteredTasks = [...new Set(queryResultTasks.filter(filterFunction))];
112+
113+
return new QueryResult(
114+
new TaskGroups(this.taskGroups.groupers, filteredTasks, searchInfo),
115+
filteredTasks.length,
116+
);
117+
}
99118
}

tests/Query/QueryResult.test.ts

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,17 @@
22
* @jest-environment jsdom
33
*/
44
import moment from 'moment/moment';
5+
import { DescriptionField } from '../../src/Query/Filter/DescriptionField';
6+
import { TagsField } from '../../src/Query/Filter/TagsField';
57
import type { Grouper } from '../../src/Query/Group/Grouper';
68
import { TaskGroups } from '../../src/Query/Group/TaskGroups';
79
import { Query } from '../../src/Query/Query';
810
import { QueryResult } from '../../src/Query/QueryResult';
911
import { SearchInfo } from '../../src/Query/SearchInfo';
1012
import type { Task } from '../../src/Task/Task';
1113
import { readTasksFromSimulatedFile } from '../Obsidian/SimulatedFile';
14+
import { renderMarkdown } from '../Renderer/RenderingTestHelpers';
15+
import { TaskBuilder } from '../TestingTools/TaskBuilder';
1216
import { fromLine, fromLines } from '../TestingTools/TestHelpers';
1317

1418
window.moment = moment;
@@ -249,3 +253,100 @@ group by id
249253
`);
250254
});
251255
});
256+
257+
describe('QueryResult - filters', () => {
258+
const taskBuilder = new TaskBuilder();
259+
const threeSimpleTasks = [
260+
taskBuilder.description('task 1').build(),
261+
taskBuilder.description('task 2').build(),
262+
taskBuilder.description('task 3').build(),
263+
];
264+
265+
it('should filter an ungrouped flat list result', async () => {
266+
const { markdown, rerenderWithFilter } = await renderMarkdown(
267+
'description does not include 3',
268+
threeSimpleTasks,
269+
);
270+
271+
expect(markdown).toEqual(`
272+
- [ ] task 1
273+
- [ ] task 2
274+
`);
275+
276+
const filter = new DescriptionField().createFilterOrErrorMessage('description includes 2');
277+
278+
const { filteredMarkdown } = await rerenderWithFilter(filter);
279+
280+
expect(filteredMarkdown).toEqual(`
281+
- [ ] task 2
282+
`);
283+
});
284+
285+
it('should filter a grouped flat list result', async () => {
286+
const { markdown, rerenderWithFilter } = await renderMarkdown(
287+
'group by function task.description',
288+
threeSimpleTasks,
289+
);
290+
291+
expect(markdown).toEqual(`
292+
#### task 1
293+
294+
- [ ] task 1
295+
296+
#### task 2
297+
298+
- [ ] task 2
299+
300+
#### task 3
301+
302+
- [ ] task 3
303+
`);
304+
305+
const filter = new DescriptionField().createFilterOrErrorMessage('description includes 2');
306+
307+
const { filteredMarkdown } = await rerenderWithFilter(filter);
308+
309+
expect(filteredMarkdown).toEqual(`
310+
#### task 2
311+
312+
- [ ] task 2
313+
`);
314+
});
315+
316+
it('should filter a grouped flat list result with a task in multiple groups', async () => {
317+
const taskBuilder = new TaskBuilder();
318+
const task1 = taskBuilder.description('task 1').tags(['#one', '#two']).build();
319+
const task2 = taskBuilder.description('task 2').tags(['#three']).build();
320+
const tasks = [task1, task2];
321+
322+
const { markdown, rerenderWithFilter } = await renderMarkdown('group by tags', tasks);
323+
324+
expect(markdown).toEqual(`
325+
#### #one
326+
327+
- [ ] task 1 #one #two
328+
329+
#### #three
330+
331+
- [ ] task 2 #three
332+
333+
#### #two
334+
335+
- [ ] task 1 #one #two
336+
`);
337+
338+
const filter = new TagsField().createFilterOrErrorMessage('tag includes two');
339+
340+
const { filteredMarkdown } = await rerenderWithFilter(filter);
341+
342+
expect(filteredMarkdown).toEqual(`
343+
#### #one
344+
345+
- [ ] task 1 #one #two
346+
347+
#### #two
348+
349+
- [ ] task 1 #one #two
350+
`);
351+
});
352+
});

tests/Renderer/MarkdownQueryResultsRenderer.test.ts

Lines changed: 12 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,14 @@
11
import moment from 'moment/moment';
2-
import type { Task } from 'Task/Task';
32
import { GlobalFilter } from '../../src/Config/GlobalFilter';
43
import { State } from '../../src/Obsidian/Cache';
5-
import { Query } from '../../src/Query/Query';
6-
import { MarkdownQueryResultsRenderer } from '../../src/Renderer/MarkdownQueryResultsRenderer';
7-
import { TasksFile } from '../../src/Scripting/TasksFile';
84
import { Priority } from '../../src/Task/Priority';
95
import { readTasksFromSimulatedFile } from '../Obsidian/SimulatedFile';
106
import { TaskBuilder } from '../TestingTools/TaskBuilder';
117
import { fromLines } from '../TestingTools/TestHelpers';
8+
import { createMarkdownRenderer, renderMarkdown } from './RenderingTestHelpers';
129

1310
window.moment = moment;
1411

15-
function createMarkdownRenderer(source: string) {
16-
const tasksFile = new TasksFile('query.md');
17-
const query = new Query(source, tasksFile);
18-
const renderer = new MarkdownQueryResultsRenderer({
19-
query: () => query,
20-
tasksFile: () => tasksFile,
21-
source: () => source,
22-
});
23-
return { renderer, query };
24-
}
25-
26-
async function renderMarkdown(source: string, tasks: Task[]) {
27-
const { renderer, query } = createMarkdownRenderer(source);
28-
await renderer.renderQuery(State.Warm, query.applyQueryToTasks(tasks));
29-
return '\n' + renderer.markdown;
30-
}
31-
3212
function readMarkdown(tasksMarkdown: string) {
3313
const lines = tasksMarkdown.split('\n').filter((line) => line.length > 0);
3414
return fromLines({ lines });
@@ -40,7 +20,7 @@ afterEach(() => {
4020

4121
describe('MarkdownQueryResultsRenderer tests', () => {
4222
it('should render single task', async () => {
43-
const markdown = await renderMarkdown('hide tree', [
23+
const { markdown } = await renderMarkdown('hide tree', [
4424
new TaskBuilder().description('hello').priority(Priority.Medium).build(),
4525
]);
4626
expect(markdown).toMatchInlineSnapshot(`
@@ -66,7 +46,7 @@ describe('MarkdownQueryResultsRenderer tests', () => {
6646
});
6747

6848
it('should render two tasks', async () => {
69-
const markdown = await renderMarkdown('hide tree\nsort by priority reverse', [
49+
const { markdown } = await renderMarkdown('hide tree\nsort by priority reverse', [
7050
new TaskBuilder().description('hello').priority(Priority.Medium).build(),
7151
new TaskBuilder().description('bye').priority(Priority.High).build(),
7252
]);
@@ -85,7 +65,7 @@ describe('MarkdownQueryResultsRenderer tests', () => {
8565
- [ ] 55555
8666
`);
8767

88-
const markdown = await renderMarkdown('hide tree\ngroup by function task.description.length', tasks);
68+
const { markdown } = await renderMarkdown('hide tree\ngroup by function task.description.length', tasks);
8969
expect(markdown).toMatchInlineSnapshot(`
9070
"
9171
#### 3
@@ -113,7 +93,7 @@ describe('MarkdownQueryResultsRenderer tests', () => {
11393
- [ ] 6 🆔 id6
11494
`);
11595

116-
const markdown = await renderMarkdown(
96+
const { markdown } = await renderMarkdown(
11797
`
11898
group by function task.tags.join(',')
11999
group by priority
@@ -163,7 +143,7 @@ group by id
163143
it('should remove indentation for nested tasks', async () => {
164144
const tasks = readTasksFromSimulatedFile('inheritance_2roots_listitem_listitem_task');
165145

166-
const markdown = await renderMarkdown('', tasks);
146+
const { markdown } = await renderMarkdown('', tasks);
167147
expect(markdown).toMatchInlineSnapshot(`
168148
"
169149
- [ ] grandchild task 1
@@ -177,7 +157,7 @@ group by id
177157
'inheritance_1parent2children2grandchildren1sibling_start_with_heading',
178158
);
179159

180-
const markdown = await renderMarkdown('show tree', tasks);
160+
const { markdown } = await renderMarkdown('show tree', tasks);
181161
expect(markdown).toMatchInlineSnapshot(`
182162
"
183163
- [ ] #task parent task
@@ -193,7 +173,7 @@ group by id
193173
it('should indent nested list items', async () => {
194174
const tasks = readTasksFromSimulatedFile('inheritance_task_2listitem_3task');
195175

196-
const markdown = await renderMarkdown('show tree', tasks);
176+
const { markdown } = await renderMarkdown('show tree', tasks);
197177
expect(markdown).toMatchInlineSnapshot(`
198178
"
199179
- [ ] parent task
@@ -210,7 +190,7 @@ group by id
210190
GlobalFilter.getInstance().set('#task');
211191
const tasks = readTasksFromSimulatedFile('inheritance_non_task_child');
212192

213-
const markdown = await renderMarkdown('show tree', tasks);
193+
const { markdown } = await renderMarkdown('show tree', tasks);
214194
expect(markdown).toMatchInlineSnapshot(`
215195
"
216196
- [ ] #task task parent
@@ -225,7 +205,7 @@ group by id
225205
it('should use hyphen as list marker', async () => {
226206
const tasks = readTasksFromSimulatedFile('mixed_list_markers');
227207

228-
const markdown = await renderMarkdown('', tasks);
208+
const { markdown } = await renderMarkdown('', tasks);
229209

230210
expect(markdown).toMatchInlineSnapshot(`
231211
"
@@ -240,7 +220,7 @@ group by id
240220
it('should remove callout prefixes', async () => {
241221
const tasks = readTasksFromSimulatedFile('callout_labelled');
242222

243-
const markdown = await renderMarkdown('', tasks);
223+
const { markdown } = await renderMarkdown('', tasks);
244224

245225
expect(markdown).toMatchInlineSnapshot(`
246226
"
@@ -253,7 +233,7 @@ group by id
253233
it('should render the explanation', async () => {
254234
const tasks = readTasksFromSimulatedFile('callout_labelled');
255235

256-
const markdown = await renderMarkdown('explain\ndescription includes indented', tasks);
236+
const { markdown } = await renderMarkdown('explain\ndescription includes indented', tasks);
257237

258238
expect(markdown).toMatchInlineSnapshot(`
259239
"

tests/Renderer/RenderingTestHelpers.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
import type { App } from 'obsidian';
2+
import { State } from '../../src/Obsidian/Cache';
3+
import type { FilterOrErrorMessage } from '../../src/Query/Filter/FilterOrErrorMessage';
4+
import { Query } from '../../src/Query/Query';
5+
import { MarkdownQueryResultsRenderer } from '../../src/Renderer/MarkdownQueryResultsRenderer';
26
import type { QueryRendererParameters } from '../../src/Renderer/QueryResultsRenderer';
7+
import { TasksFile } from '../../src/Scripting/TasksFile';
38
import type { Task } from '../../src/Task/Task';
49

510
export const mockHTMLRenderer = async (_obsidianApp: App, text: string, element: HTMLSpanElement, _path: string) => {
@@ -23,3 +28,31 @@ export function makeQueryRendererParameters(allTasks: Task[]): QueryRendererPara
2328
editTaskPencilClickHandler: () => {},
2429
};
2530
}
31+
32+
export function createMarkdownRenderer(source: string) {
33+
const tasksFile = new TasksFile('query.md');
34+
const query = new Query(source, tasksFile);
35+
const renderer = new MarkdownQueryResultsRenderer({
36+
query: () => query,
37+
tasksFile: () => tasksFile,
38+
source: () => source,
39+
});
40+
return { renderer, query };
41+
}
42+
43+
export async function renderMarkdown(source: string, tasks: Task[]) {
44+
const { renderer, query } = createMarkdownRenderer(source);
45+
const queryResult = query.applyQueryToTasks(tasks);
46+
await renderer.renderQuery(State.Warm, queryResult);
47+
return {
48+
markdown: '\n' + renderer.markdown,
49+
queryResult,
50+
rerenderWithFilter: async (filter: FilterOrErrorMessage) => {
51+
expect(filter).toBeValid();
52+
53+
const filteredResult = queryResult.applyFilter(filter.filter!);
54+
await renderer.renderQuery(State.Warm, filteredResult);
55+
return { filteredMarkdown: '\n' + renderer.markdown };
56+
},
57+
};
58+
}

0 commit comments

Comments
 (0)