Repository: obsidian-tasks-group/obsidian-tasks
Pull request #2615 (2024-01-24 20:50:14)

feat: Add 'blocked by' presence and absence instructions
<!--- Provide a general summary of your changes in the Title above -->

# Description

- Add 'blocked by' presence and absence instructions
- Record the option to use `task.blockedBy` in custom filters, sorts and groups.
- New instructions added to documentation.

## Motivation and Context

This is part of getting the Dependencies feature ready to release.

## How has this been tested?

- Add new tests
- Exploratory testing

## Screenshots (if appropriate)

## Types of changes

<!--- What types of changes does your code introduce? Put an `x` in all the boxes that apply: -->

Changes visible to users:

- [x] **New feature** (prefix: `feat` - non-breaking change which adds functionality)
- [x] **Documentation** (prefix: `docs` - improvements to any documentation content **for users**)

Internal changes:

- [x] **Tests** (prefix: `test` - additions and improvements to unit tests and the smoke tests)

## Checklist

<!--- Go over all the following points, and put an `x` in all the boxes that apply. -->
<!--- If you're unsure about any of these, don't hesitate to ask. We're here to help! -->

- [x] My code follows the code style of this project and passes `yarn run lint`.
- [x] My change requires a change to the documentation.
- [x] I have [updated the documentation](https://publish.obsidian.md/tasks-contributing/Documentation/About+Documentation) accordingly.
- [x] My change has adequate [Unit Test coverage](https://publish.obsidian.md/tasks-contributing/Testing/About+Testing).

## Terms

<!--
By submitting this pull request, you must agree to follow our
[contributing guide](https://publish.obsidian.md/tasks-contributing) and
[Code of Conduct](https://github.com/obsidian-tasks-group/obsidian-tasks/blob/main/CODE_OF_CONDUCT.md).
Put an x in the boxes to confirm you agree.
-->

- [x] My contribution follow this project's [contributing guide](https://publish.obsidian.md/tasks-contributing)
- [x] I agree to follow this project's [Code of Conduct](https://github.com/obsidian-tasks-group/obsidian-tasks/blob/main/CODE_OF_CONDUCT.md)



Patch:
diff --git a/docs/Queries/Filters.md b/docs/Queries/Filters.md
index a86a60ea..efedcc4f 100644
--- a/docs/Queries/Filters.md
+++ b/docs/Queries/Filters.md
@@ -471,6 +471,19 @@ For more information, see [[Task Dependencies]].
 
 Since Tasks X.Y.Z, **[[Custom Filters|custom filtering]] by Id** is now possible, using `task.id`.
 
+### Blocked By
+
+- `has blocked by`
+- `no blocked by`
+
+For more information, see [[Task Dependencies]].
+
+> [!released]
+>
+> - Task Blocked By was introduced in Tasks X.Y.Z.
+
+Since Tasks X.Y.Z, **[[Custom Filters|custom filtering]] by Blocked By** is now possible, using `task.blockedBy`.
+
 ## Filters for Dates in Tasks
 
 ### Due Date
diff --git a/docs/Queries/Grouping.md b/docs/Queries/Grouping.md
index e9bdf700..4c9229e8 100644
--- a/docs/Queries/Grouping.md
+++ b/docs/Queries/Grouping.md
@@ -175,7 +175,19 @@ For more information, see [[Task Dependencies]].
 >
 > - Task Id was introduced in Tasks X.Y.Z.
 
-Since Tasks X.Y.Z, **[[Custom Grouping|custom grouping]]] by Id** is now possible, using `task.id`.
+Since Tasks X.Y.Z, **[[Custom Grouping|custom grouping]] by Id** is now possible, using `task.id`.
+
+### Blocked By
+
+There is no built-in instruction to group by 'Blocked By'.
+
+For more information, see [[Task Dependencies]].
+
+> [!released]
+>
+> - Task Blocked By was introduced in Tasks X.Y.Z.
+
+Since Tasks X.Y.Z, **[[Custom Grouping|custom grouping]]  by Blocked By** is now possible, using `task.blockedBy`.
 
 ## Group by Dates in Tasks
 
diff --git a/docs/Queries/Sorting.md b/docs/Queries/Sorting.md
index ee502572..4564d3cd 100644
--- a/docs/Queries/Sorting.md
+++ b/docs/Queries/Sorting.md
@@ -172,6 +172,18 @@ For more information, see [[Task Dependencies]].
 
 Since Tasks X.Y.Z, **[[Custom Sorting|custom sorting]] by Id** is now possible, using `task.id`.
 
+### Blocked By
+
+There is no built-in instruction to sort by 'Blocked By'.
+
+For more information, see [[Task Dependencies]].
+
+> [!released]
+>
+> - Task Blocked By was introduced in Tasks X.Y.Z.
+
+Since Tasks X.Y.Z, **[[Custom Sorting|custom sorting]]  by Blocked By** is now possible, using `task.blockedBy`.
+
 ## Sort by Dates in Tasks
 
 ### How dates are sorted
diff --git a/docs/Quick Reference.md b/docs/Quick Reference.md
index bdabe939..8264bef4 100644
--- a/docs/Quick Reference.md
+++ b/docs/Quick Reference.md
@@ -19,8 +19,10 @@ This table summarizes the filters and other options available inside a `tasks` b
 |                                                                                                                                                                                                                                                                                                                                                      |                                             |                        |                        | `task.status.symbol`                                                                                                           |
 |                                                                                                                                                                                                                                                                                                                                                      |                                             |                        |                        | `task.status.nextSymbol`                                                                                                       |
 | **[[Task Dependencies]]**                                                                                                                                                                                                                                                                                                                            |                                             |                        |                        |                                                                                                                                |
-| `id (includes, does not include) <string>`<br>`id (regex matches, regex does not match) /regex/i`<br>`has id`<br>`no id`<br>`is blocking`                                                                                                                                                                                                            | `sort by id`                                | `group by id`          | `hide id`              | `task.id`                                                                                                                      |
-| `is not blocked`                                                                                                                                                                                                                                                                                                                                     |                                             |                        | `hide blocked by`      | `task.blockedBy`                                                                                                               |
+| `id (includes, does not include) <string>`<br>`id (regex matches, regex does not match) /regex/i`<br>`has id`<br>`no id`                                                                                                                                                                                                                             | `sort by id`                                | `group by id`          | `hide id`              | `task.id`                                                                                                                      |
+| `has blocked by`<br>`no blocked by`                                                                                                                                                                                                                                                                                                                  |                                             |                        | `hide blocked by`      | `task.blockedBy`                                                                                                               |
+| `is not blocked`                                                                                                                                                                                                                                                                                                                                     |                                             |                        |                        |                                                                                                                                |
+| `is blocking`                                                                                                                                                                                                                                                                                                                                        |                                             |                        |                        |                                                                                                                                |
 | **[[Filters#Filters for Dates in Tasks\|Dates]]**                                                                                                                                                                                                                                                                                                    |                                             |                        |                        |                                                                                                                                |
 | `done (on, before, after, on or before, on or after) <date>`<br>`done (in, before, after, in or before, in or after) ...`<br>`... YYYY-MM-DD YYYY-MM-DD`<br>`... (last, this, next) (week, month, quarter, year)`<br>`... (YYYY-Www,YYYY-mm, YYYY-Qq, YYYY)`<br>`has done date`<br>`no done date`<br>`done date is invalid`                          | `sort by done`                              | `group by done`        | `hide done date`       | `task.done`                                                                                                                    |
 | `created (on, before, after, on or before, on or after) <date>`<br>`created (in, before, after, in or before, in or after) ...`<br>`... YYYY-MM-DD YYYY-MM-DD`<br>`... (last, this, next) (week, month, quarter, year)`<br>`... (YYYY-Www,YYYY-mm, YYYY-Qq, YYYY)`<br>`has created date`<br>`no created date`<br>`created date is invalid`           | `sort by created`                           | `group by created`     | `hide created date`    | `task.created`                                                                                                                 |
diff --git a/src/Query/Filter/BlockedByField.ts b/src/Query/Filter/BlockedByField.ts
new file mode 100644
index 00000000..c12fd76e
--- /dev/null
+++ b/src/Query/Filter/BlockedByField.ts
@@ -0,0 +1,43 @@
+import type { Task } from '../../Task/Task';
+import { Field } from './Field';
+import { FilterOrErrorMessage } from './FilterOrErrorMessage';
+import { FilterInstructions } from './FilterInstructions';
+
+export class BlockedByField extends Field {
+    private readonly filterInstructions: FilterInstructions = new FilterInstructions();
+
+    constructor() {
+        super();
+        this.filterInstructions.add('has blocked by', (task: Task) => task.blockedBy.length > 0);
+        this.filterInstructions.add('no blocked by', (task: Task) => task.blockedBy.length === 0);
+    }
+
+    // -----------------------------------------------------------------------------------------------------------------
+    // Filtering
+    // -----------------------------------------------------------------------------------------------------------------
+
+    public canCreateFilterForLine(line: string): boolean {
+        if (this.filterInstructions.canCreateFilterForLine(line)) {
+            return true;
+        }
+
+        return super.canCreateFilterForLine(line);
+    }
+
+    public createFilterOrErrorMessage(line: string): FilterOrErrorMessage {
+        const filterResult = this.filterInstructions.createFilterOrErrorMessage(line);
+        if (filterResult.filter !== undefined) {
+            return filterResult;
+        }
+
+        return FilterOrErrorMessage.fromError(line, 'Unknown instruction');
+    }
+
+    public fieldName(): string {
+        return 'blocked by';
+    }
+
+    protected filterRegExp(): RegExp | null {
+        return null;
+    }
+}
diff --git a/src/Query/FilterParser.ts b/src/Query/FilterParser.ts
index 01635295..38d16b15 100644
--- a/src/Query/FilterParser.ts
+++ b/src/Query/FilterParser.ts
@@ -30,6 +30,7 @@ import { BacklinkField } from './Filter/BacklinkField';
 import { CancelledDateField } from './Filter/CancelledDateField';
 import { BlockingField } from './Filter/BlockingField';
 import { IdField } from './Filter/IdField';
+import { BlockedByField } from './Filter/BlockedByField';
 
 // When parsing a query the fields are tested one by one according to this order.
 // Since BooleanField is a meta-field, which needs to aggregate a few fields together, it is intended to
@@ -63,6 +64,7 @@ export const fieldCreators: EndsWith<BooleanField> = [
     () => new RecurrenceField(),
     () => new FunctionField(),
     () => new IdField(),
+    () => new BlockedByField(),
     () => new BlockingField(),
     () => new BooleanField(), // --- Please make sure to keep BooleanField last (see comment above) ---
 ];


Test patch:
diff --git a/tests/Query/Filter/BlockedByField.test.ts b/tests/Query/Filter/BlockedByField.test.ts
new file mode 100644
index 00000000..c9be722a
--- /dev/null
+++ b/tests/Query/Filter/BlockedByField.test.ts
@@ -0,0 +1,31 @@
+import { testFilter } from '../../TestingTools/FilterTestHelpers';
+import { TaskBuilder } from '../../TestingTools/TaskBuilder';
+import { BlockedByField } from '../../../src/Query/Filter/BlockedByField';
+
+describe('id', () => {
+    const blockedByField = new BlockedByField();
+
+    it('should supply field name', () => {
+        expect(blockedByField.fieldName()).toEqual('blocked by');
+    });
+
+    it('by blocked by presence', () => {
+        // Arrange
+        const filter = new BlockedByField().createFilterOrErrorMessage('has blocked by');
+
+        // Act, Assert
+        testFilter(filter, new TaskBuilder().blockedBy([]), false);
+        testFilter(filter, new TaskBuilder().blockedBy(['abcdef']), true);
+    });
+
+    it('by blocked by absence', () => {
+        // Arrange
+        const line = 'no blocked by';
+        const filter = new BlockedByField().createFilterOrErrorMessage(line);
+        expect(blockedByField.canCreateFilterForLine(line)).toEqual(true);
+
+        // Act, Assert
+        testFilter(filter, new TaskBuilder().blockedBy([]), true);
+        testFilter(filter, new TaskBuilder().blockedBy(['abcdef']), false);
+    });
+});
diff --git a/tests/Query/Query.test.ts b/tests/Query/Query.test.ts
index eba750fe..9325be96 100644
--- a/tests/Query/Query.test.ts
+++ b/tests/Query/Query.test.ts
@@ -82,6 +82,7 @@ describe('Query parsing', () => {
         'happens in 2021-12-27 2021-12-29',
         'happens on 2021-12-27',
         'happens this week',
+        'has blocked by',
         'has cancelled date',
         'has created date',
         'has done date',
@@ -102,6 +103,7 @@ describe('Query parsing', () => {
         'is not blocked',
         'is not recurring',
         'is recurring',
+        'no blocked by',
         'no cancelled date',
         'no created date',
         'no due date',