diff --git a/docs/Introduction.md b/docs/Introduction.md index a0f24eefd4..07545af732 100644 --- a/docs/Introduction.md +++ b/docs/Introduction.md @@ -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. ```javascript @@ -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]]. + ```javascript @@ -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]]. + ```javascript @@ -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**). @@ -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]]. + ```javascript diff --git a/docs/Scripting/Custom Filters.md b/docs/Scripting/Custom Filters.md index 18e989e732..2b11d00089 100644 --- a/docs/Scripting/Custom Filters.md +++ b/docs/Scripting/Custom Filters.md @@ -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 @@ -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 @@ -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**). diff --git a/src/Query/Filter/FunctionField.ts b/src/Query/Filter/FunctionField.ts index c4f63d032b..827e1fd9c6 100644 --- a/src/Query/Filter/FunctionField.ts +++ b/src/Query/Filter/FunctionField.ts @@ -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!); @@ -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. diff --git a/src/Scripting/TaskExpression.ts b/src/Scripting/TaskExpression.ts index 54d0c38182..22f259a430 100644 --- a/src/Scripting/TaskExpression.ts +++ b/src/Scripting/TaskExpression.ts @@ -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}"`, diff --git a/tests/Query/Filter/FunctionField.test.ts b/tests/Query/Filter/FunctionField.test.ts index 6379df39fb..f49911844c 100644 --- a/tests/Query/Filter/FunctionField.test.ts +++ b/tests/Query/Filter/FunctionField.test.ts @@ -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; @@ -23,7 +24,7 @@ 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'); @@ -31,6 +32,21 @@ describe('FunctionField - filtering', () => { 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); diff --git a/tests/Scripting/ScriptingReference/CustomFiltering/CustomFilteringExamples.test.file_properties_task.file.folder_docs.approved.md b/tests/Scripting/ScriptingReference/CustomFiltering/CustomFilteringExamples.test.file_properties_task.file.folder_docs.approved.md index 7c56ef4f12..4839f6c5ff 100644 --- a/tests/Scripting/ScriptingReference/CustomFiltering/CustomFilteringExamples.test.file_properties_task.file.folder_docs.approved.md +++ b/tests/Scripting/ScriptingReference/CustomFiltering/CustomFilteringExamples.test.file_properties_task.file.folder_docs.approved.md @@ -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**). diff --git a/tests/Scripting/ScriptingReference/CustomFiltering/CustomFilteringExamples.test.file_properties_task.file.folder_results.approved.txt b/tests/Scripting/ScriptingReference/CustomFiltering/CustomFilteringExamples.test.file_properties_task.file.folder_results.approved.txt index ea0916b978..2bbea7f10e 100644 --- a/tests/Scripting/ScriptingReference/CustomFiltering/CustomFilteringExamples.test.file_properties_task.file.folder_results.approved.txt +++ b/tests/Scripting/ScriptingReference/CustomFiltering/CustomFilteringExamples.test.file_properties_task.file.folder_results.approved.txt @@ -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 @@ -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 diff --git a/tests/Scripting/ScriptingReference/CustomFiltering/CustomFilteringExamples.test.ts b/tests/Scripting/ScriptingReference/CustomFiltering/CustomFilteringExamples.test.ts index c63bae476b..c39a444eb2 100644 --- a/tests/Scripting/ScriptingReference/CustomFiltering/CustomFilteringExamples.test.ts +++ b/tests/Scripting/ScriptingReference/CustomFiltering/CustomFilteringExamples.test.ts @@ -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**)', ], [ diff --git a/tests/Scripting/ScriptingReference/VerifyFunctionFieldSamples.ts b/tests/Scripting/ScriptingReference/VerifyFunctionFieldSamples.ts index 293fd568fd..55fcf9f2df 100644 --- a/tests/Scripting/ScriptingReference/VerifyFunctionFieldSamples.ts +++ b/tests/Scripting/ScriptingReference/VerifyFunctionFieldSamples.ts @@ -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) {