Skip to content

Commit

Permalink
Merge pull request #2378 from obsidian-tasks-group/use-query.file-in-…
Browse files Browse the repository at this point in the history
…custom-filters

feat: Custom filters access Query Properties directly, without Placeholders
  • Loading branch information
claremacrae authored Oct 29, 2023
2 parents ce3818c + c2b9a8d commit 68268f4
Show file tree
Hide file tree
Showing 10 changed files with 64 additions and 30 deletions.
2 changes: 1 addition & 1 deletion docs/Introduction.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ _In recent [releases](https://github.com/obsidian-tasks-group/obsidian-tasks/rel
Move the older ones down to the top of the comment block below...
-->

- X.Y.Z: 🔥 Enable [[Custom Grouping|custom grouping]] to use [[Query Properties|query properties]] directly - no placeholders required.
- X.Y.Z: 🔥 Enable [[Custom Filters|custom filters]] and [[Custom Grouping|custom grouping]] to use [[Query Properties|query properties]] directly - no placeholders required.

<!--
- 5.0.0: 🔥 Add [[Line Continuations|line continuations]].
Expand Down
27 changes: 24 additions & 3 deletions docs/Queries/Filters.md
Original file line number Diff line number Diff line change
Expand Up @@ -1137,6 +1137,12 @@ Since Tasks 4.2.0, **[[Custom Filters|custom filtering]] by file path** is now p
In Tasks 4.8.0 `task.file.pathWithoutExtension` was added.
Since Tasks X.Y.Z, the query's file path can be used conveniently in custom filters:
- `query.file.path` or
- `query.file.pathWithoutExtension`
- Useful reading: [[Query Properties]].
<!-- placeholder to force blank line before included text --><!-- include: CustomFilteringExamples.test.file_properties_task.file.path_docs.approved.md -->
```javascript
Expand Down Expand Up @@ -1181,6 +1187,11 @@ The `root` is the top-level folder of the file that contains the task, that is,
Since Tasks 4.2.0, **[[Custom Filters|custom filtering]] by root folder** is now possible, using `task.file.root`.
Since Tasks X.Y.Z, the query's file root can be used conveniently in custom filters:
- `query.file.root`
- Useful reading: [[Query Properties]].
<!-- placeholder to force blank line before included text --><!-- include: CustomFilteringExamples.test.file_properties_task.file.root_docs.approved.md -->
```javascript
Expand Down Expand Up @@ -1219,6 +1230,11 @@ This is the `folder` to the file that contains the task, which will be `/` for f
Since Tasks 4.2.0, **[[Custom Filters|custom filtering]] by folder** is now possible, using `task.file.folder`.
Since Tasks X.Y.Z, the query's file root can be used conveniently in custom filters:
- `query.file.root`
- Useful reading: [[Query Properties]].
<!-- placeholder to force blank line before included text --><!-- include: CustomFilteringExamples.test.file_properties_task.file.folder_docs.approved.md -->
```javascript
Expand All @@ -1235,14 +1251,13 @@ filter by function task.file.folder.includes("Work/Projects/")
- Find tasks in files in a specific folder **and any sub-folders**.

```javascript
filter by function task.file.folder.includes( '{{query.file.folder}}' )
filter by function task.file.folder.includes( query.file.folder )
```

- Find tasks in files in the folder that contains the query **and any sub-folders**.
- Note that the placeholder text is expanded to a raw string, so needs to be inside quotes.

```javascript
filter by function task.file.folder === '{{query.file.folder}}'
filter by function task.file.folder === query.file.folder
```

- Find tasks in files in the folder that contains the query only (**not tasks in any sub-folders**).
Expand Down Expand Up @@ -1279,6 +1294,12 @@ Since Tasks 4.2.0, **[[Custom Filters|custom filtering]] by file name** is now p
In Tasks 4.8.0 `task.file.filenameWithoutExtension` was added.
Since Tasks X.Y.Z, the query's file name can be used conveniently in custom filters:
- `query.file.filename` or
- `query.file.filenameWithoutExtension`
- Useful reading: [[Query Properties]].
<!-- placeholder to force blank line before included text --><!-- include: CustomFilteringExamples.test.file_properties_task.file.filename_docs.approved.md -->
```javascript
Expand Down
16 changes: 8 additions & 8 deletions docs/Scripting/Custom Filters.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,9 @@ publish: true
- The expression must evaluate to a `boolean`, so `true` or `false`.
- There are loads of examples in [[Filters]].
- Search for `filter by function` in that file.
- Find all the supported tasks properties in [[Task Properties]] and [[Quick Reference]].
- Find all the **supported tasks properties** in [[Task Properties]] and [[Quick Reference]].
- A number of properties are only available for custom filters and grouping, and not for built-in grouping instructions.
- Find all the **supported query properties** in [[Query Properties]].
- Learn a bit about how expressions work in [[Expressions]].

## Custom filters introduction
Expand All @@ -40,12 +41,12 @@ The available task properties are also shown in the [[Quick Reference]] table.

### Available Query Properties

The Reference section [[Query Properties]] shows all the query properties available for use via [[Placeholders]] in custom filters.

Any placeholders in custom filters must be surrounded by quotes.
The Reference section [[Query Properties]] shows all the query properties available for use in custom filters.

> [!released]
> Query properties and placeholders were introduced in Tasks 4.7.0.
>
> - Query properties and placeholders were introduced in Tasks 4.7.0, accessible via Placeholders.
> - Direct access to Query properties was introduced in Tasks X.Y.Z.
### Expressions

Expand Down Expand Up @@ -195,14 +196,13 @@ filter by function task.file.folder.includes("Work/Projects/")
- Find tasks in files in a specific folder **and any sub-folders**.

```javascript
filter by function task.file.folder.includes( '{{query.file.folder}}' )
filter by function task.file.folder.includes( query.file.folder )
```

- Find tasks in files in the folder that contains the query **and any sub-folders**.
- Note that the placeholder text is expanded to a raw string, so needs to be inside quotes.

```javascript
filter by function task.file.folder === '{{query.file.folder}}'
filter by function task.file.folder === query.file.folder
```

- Find tasks in files in the folder that contains the query only (**not tasks in any sub-folders**).
Expand Down
10 changes: 5 additions & 5 deletions src/Query/Filter/FunctionField.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ export class FunctionField extends Field {
}

const expression = match[1];
// TODO When filters are allowed to start using the QueryContext, will need to pass in actual QueryContext
const taskExpression = new TaskExpression(expression);
if (!taskExpression.isValid()) {
return FilterOrErrorMessage.fromError(line, taskExpression.parseError!);
Expand Down Expand Up @@ -80,15 +79,16 @@ export class FunctionField extends Field {
// -----------------------------------------------------------------------------------------------------------------

function createFilterFunctionFromLine(expression: TaskExpression): FilterFunction {
return (task: Task) => {
return filterByFunction(expression, task);
return (task: Task, searchInfo: SearchInfo) => {
const queryContext = searchInfo.queryContext();
return filterByFunction(expression, task, queryContext);
};
}

export function filterByFunction(expression: TaskExpression, task: Task): boolean {
export function filterByFunction(expression: TaskExpression, task: Task, queryContext?: QueryContext): boolean {
// Allow exceptions to propagate to caller, since this will be called in a tight loop.
// In searches, it will be caught by Query.applyQueryToTasks().
const result = expression.evaluate(task, undefined); // TODO when supporting query.file in filtering, add a QueryContext parameter
const result = expression.evaluate(task, queryContext);

// We insist that 'filter by function' returns booleans,
// to avoid users having to understand truthy and falsey values.
Expand Down
1 change: 0 additions & 1 deletion src/Scripting/TaskExpression.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,6 @@ export class TaskExpression {
* @see evaluateOrCatch
*/
public evaluate(task: Task, queryContext?: QueryContext) {
// TODO When 'filter by function' supports query properties, make queryContext non-optional and simplify its use below.
if (!this.isValid()) {
throw Error(
`Error: Cannot evaluate an expression which is not valid: "${this.line}" gave error: "${this.parseError}"`,
Expand Down
18 changes: 17 additions & 1 deletion tests/Query/Filter/FunctionField.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
} from '../../CustomMatchers/CustomMatchersForGrouping';
import { TaskBuilder } from '../../TestingTools/TaskBuilder';
import { SearchInfo } from '../../../src/Query/SearchInfo';
import type { Task } from '../../../src/Task';

window.moment = moment;

Expand All @@ -23,14 +24,29 @@ window.moment = moment;
describe('FunctionField - filtering', () => {
const functionField = new FunctionField();

it('filter by function - with valid query', () => {
it('filter by function - with valid query of Task property', () => {
const filter = functionField.createFilterOrErrorMessage('filter by function task.description.length > 5');
expect(filter).toBeValid();
expect(filter).toMatchTaskWithDescription('123456');
expect(filter).not.toMatchTaskWithDescription('12345');
expect(filter).not.toMatchTaskWithDescription('1234');
});

it('filter by function - with valid query of Query property', () => {
const tasksInSameFileAsQuery = functionField.createFilterOrErrorMessage(
'filter by function task.file.path === query.file.path',
);
expect(tasksInSameFileAsQuery).toBeValid();
const queryFilePath = '/a/b/query.md';

const taskInQueryFile: Task = new TaskBuilder().path(queryFilePath).build();
const taskNotInQueryFile: Task = new TaskBuilder().path('some other path.md').build();
const searchInfo = new SearchInfo(queryFilePath, [taskInQueryFile, taskNotInQueryFile]);

expect(tasksInSameFileAsQuery.filterFunction!(taskInQueryFile, searchInfo)).toEqual(true);
expect(tasksInSameFileAsQuery.filterFunction!(taskNotInQueryFile, searchInfo)).toEqual(false);
});

it('filter by function - should report syntax errors via FilterOrErrorMessage', () => {
const instructionWithExtraClosingParen = 'filter by function task.status.name.toUpperCase())';
const filter = functionField.createFilterOrErrorMessage(instructionWithExtraClosingParen);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,13 @@ filter by function task.file.folder.includes("Work/Projects/")
- Find tasks in files in a specific folder **and any sub-folders**.

```javascript
filter by function task.file.folder.includes( '{{query.file.folder}}' )
filter by function task.file.folder.includes( query.file.folder )
```

- Find tasks in files in the folder that contains the query **and any sub-folders**.
- Note that the placeholder text is expanded to a raw string, so needs to be inside quotes.

```javascript
filter by function task.file.folder === '{{query.file.folder}}'
filter by function task.file.folder === query.file.folder
```

- Find tasks in files in the folder that contains the query only (**not tasks in any sub-folders**).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,8 @@ Find tasks in files in a specific folder **and any sub-folders**.
====================================================================================


filter by function task.file.folder.includes( '{{query.file.folder}}' )
filter by function task.file.folder.includes( query.file.folder )
Find tasks in files in the folder that contains the query **and any sub-folders**.
Note that the placeholder text is expanded to a raw string, so needs to be inside quotes.
=>
- [ ] xyz in a/b.md
- [ ] xyz in a/b/c.md
Expand All @@ -31,7 +30,7 @@ Note that the placeholder text is expanded to a raw string, so needs to be insid
====================================================================================


filter by function task.file.folder === '{{query.file.folder}}'
filter by function task.file.folder === query.file.folder
Find tasks in files in the folder that contains the query only (**not tasks in any sub-folders**).
=>
- [ ] xyz in a/b.md
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -235,12 +235,11 @@ describe('file properties', () => {
'Find tasks in files in a specific folder **and any sub-folders**',
],
[
"filter by function task.file.folder.includes( '{{query.file.folder}}' )",
'filter by function task.file.folder.includes( query.file.folder )',
'Find tasks in files in the folder that contains the query **and any sub-folders**',
'Note that the placeholder text is expanded to a raw string, so needs to be inside quotes.',
],
[
"filter by function task.file.folder === '{{query.file.folder}}'",
'filter by function task.file.folder === query.file.folder',
'Find tasks in files in the folder that contains the query only (**not tasks in any sub-folders**)',
],
[
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,13 +85,14 @@ export function verifyFunctionFieldFilterSamplesOnTasks(filters: QueryInstructio
const instruction = filter[0];
const comment = filter.slice(1);

const expandedInstruction = preprocessSingleInstruction(instruction, 'a/b.md');
const path = 'a/b.md';
const expandedInstruction = preprocessSingleInstruction(instruction, path);
const filterOrErrorMessage = new FunctionField().createFilterOrErrorMessage(expandedInstruction);
expect(filterOrErrorMessage).toBeValid();

const filterFunction = filterOrErrorMessage.filterFunction!;
const matchingTasks: string[] = [];
const searchInfo = SearchInfo.fromAllTasks(tasks);
const searchInfo = new SearchInfo(path, tasks);
for (const task of tasks) {
const matches = filterFunction(task, searchInfo);
if (matches) {
Expand Down

0 comments on commit 68268f4

Please sign in to comment.