Skip to content

Commit 7a8a85d

Browse files
authored
Merge pull request #2634 from obsidian-tasks-group/blocking-and-blocked-filters
feat: Add consistent searches for 'blocked' and 'blocked'
2 parents b7d32fc + 792e950 commit 7a8a85d

File tree

10 files changed

+279
-43
lines changed

10 files changed

+279
-43
lines changed

docs/Queries/Filters.md

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -453,8 +453,76 @@ Find any tasks that have status symbols you have not yet added to your Tasks set
453453

454454
## Filters for Task Dependencies
455455

456+
At a high level, task dependencies define the order in which you want to work on a set of tasks. You can read more about them in [[Task Dependencies]].
457+
458+
### Blocking Tasks
459+
460+
- `is blocking`
461+
- This shows tasks that you probably want to do first, as they are preventing other tasks from being done.
462+
- `is not blocking`
463+
- This shows tasks that are not preventing others from being done, so perhaps may be considered as lower priority.
464+
- This would typically be used with `not done`.
465+
466+
A task is treated as `blocking` if:
467+
468+
- it has an `id` value,
469+
- at least one other task in the vault has that `id` value in its `blockedBy` list,
470+
- both tasks have status type `TODO` or `IN_PROGRESS`.
471+
472+
For example:
473+
474+
```text
475+
- [ ] I am blocking 🆔 12345
476+
- [ ] I am not blocking ⛔️ 12345
477+
```
478+
479+
Note also:
480+
481+
- Only direct dependencies are considered.
482+
- Tasks with status type `DONE`, `CANCELLED` or `NON_TASK` are never treated as `blocked`.
483+
484+
For more information, see [[Task Dependencies]].
485+
486+
> [!released]
487+
>
488+
> - `is blocking` and `is not blocking` were introduced in Tasks X.Y.Z.
489+
490+
### Blocked Tasks
491+
492+
- `is blocked`
493+
- This shows tasks you cannot currently do, as they are waiting for another task to be completed.
494+
- `is not blocked`
495+
- This shows tasks that are not waiting for any other tasks to be completed.
496+
- This would typically be used with `not done`.
497+
498+
A task is treated as `blocked` if:
499+
500+
- it has one or more `blockedBy` values,
501+
- its `blockedBy` list includes the id any tasks in the vault,
502+
- both tasks have status type `TODO` or `IN_PROGRESS`.
503+
504+
For example:
505+
506+
```text
507+
- [ ] I am not blocked 🆔 12345
508+
- [ ] I am blocked ⛔️ 12345
509+
```
510+
511+
Note also:
512+
513+
- Only direct dependencies are considered.
514+
- Tasks with status type `DONE`, `CANCELLED` or `NON_TASK` are never treated as `blocked`.
515+
516+
For more information, see [[Task Dependencies]].
517+
518+
> [!released]
519+
>
520+
> - `is blocked` and `is not blocked` were introduced in Tasks X.Y.Z.
521+
456522
### Id
457523

524+
The `id` field adds an identifier to a task, so that other tasks may be marked as `blockedBy` that task.
525+
458526
- `has id`
459527
- `no id`
460528
- `id (includes|does not include) <string>`
@@ -473,6 +541,8 @@ Since Tasks X.Y.Z, **[[Custom Filters|custom filtering]] by Id** is now possible
473541

474542
### Blocked By
475543

544+
The `blockedBy` field allows a task to be marked as depending on the `id` of one or more other tasks. Multiple `id` values are separated by commas (`,`) with no spaces.
545+
476546
- `has blocked by`
477547
- `no blocked by`
478548

docs/Quick Reference.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,9 @@ This table summarizes the filters and other options available inside a `tasks` b
2020
| | | | | `task.status.nextSymbol` |
2121
| **[[Task Dependencies]]** | | | | |
2222
| `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` |
23-
| `has blocked by`<br>`no blocked by` | | | `hide blocked by` | `task.blockedBy` |
24-
| `is not blocked` | | | | |
25-
| `is blocking` | | | | |
23+
| `has blocked by`<br>`no blocked by` | | | `hide depends on` | `task.blockedBy` |
24+
| `is blocked`<br>`is not blocked` | | | | |
25+
| `is blocking`<br>`is not blocking` | | | | |
2626
| **[[Filters#Filters for Dates in Tasks\|Dates]]** | | | | |
2727
| `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` |
2828
| `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` |

resources/sample_vaults/Tasks-Demo/Manual Testing/Dependencies Samples.md

Lines changed: 65 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -7,81 +7,127 @@
77
- [ ] #task Craft a conclusion 🆔 0wigip ⛔️ mvplec
88
- [ ] #task Proofread and edit 🆔 5ti6bf ⛔️ 0wigip
99
- [ ] #task Publish the article ⛔️ 5ti6bf
10+
- [ ] #task Do something on a different project
1011

1112
---
1213

13-
## Do Next
14+
## Real-world searches
15+
16+
### Blocking and Not Blocked - Do very soon
17+
18+
```tasks
19+
is blocking
20+
is not blocked
21+
# by definition these are all 'not done'
22+
23+
path includes {{query.file.path}}
24+
#explain
25+
```
26+
27+
### Not Blocking and Not Blocked - Do any time
1428

1529
```tasks
16-
((is blocking) AND (is not blocked)) OR (is not blocked)
30+
is not blocking
31+
is not blocked
32+
33+
# DONE, CANCELLED and NON_TASK are never blocking nor blocked,
34+
# so we need to exclude those:
1735
not done
1836
1937
path includes {{query.file.path}}
20-
explain
38+
#explain
39+
```
40+
41+
### Blocked - Cannot yet do
42+
43+
```tasks
44+
is blocked
45+
# by definition these are all 'not done'
46+
47+
path includes {{query.file.path}}
48+
#explain
2149
```
2250

2351
---
2452

25-
## Blocking Tasks
53+
## Demonstration searches - showing the Blocking instructions
2654

27-
### Blocking Tasks - Any Status
55+
### `is blocking` - Blocking Tasks - tasks which prevent others from being done
2856

2957
```tasks
3058
is blocking
3159
3260
path includes {{query.file.path}}
33-
explain
61+
#explain
3462
```
3563

36-
### Blocking Tasks - Not Done
64+
### `is not blocking` - Non-blocking Tasks - Not Done
3765

3866
```tasks
39-
is blocking
67+
is not blocking
4068
not done
4169
4270
path includes {{query.file.path}}
43-
explain
71+
#explain
72+
```
73+
74+
### `is not blocking` - Non-blocking Tasks - Any Status
75+
76+
```tasks
77+
is not blocking
78+
79+
path includes {{query.file.path}}
80+
#explain
4481
```
4582

4683
---
4784

48-
## Blocked Tasks
85+
## Demonstration searches - showing the Blocked instructions
4986

50-
### Blocked Tasks - Any Status
87+
### `is blocked` - Blocked Tasks - tasks which cannot be done yet
5188

5289
```tasks
53-
is not blocked
90+
is blocked
5491
5592
path includes {{query.file.path}}
56-
explain
93+
#explain
5794
```
5895

59-
### Blocked Tasks - Not Done
96+
### `is not blocked` - Non-blocked Tasks - Not Done
6097

6198
```tasks
6299
is not blocked
63100
not done
64101
65102
path includes {{query.file.path}}
66-
explain
103+
#explain
104+
```
105+
106+
### `is not blocked` - Non-blocked Tasks - Any Status
107+
108+
```tasks
109+
is not blocked
110+
111+
path includes {{query.file.path}}
112+
#explain
67113
```
68114

69115
---
70116

71117
## Show/Hide Instructions
72118

73-
### Hide Id
119+
### `hide id`
74120

75121
```tasks
76122
hide id
77123
path includes {{query.file.path}}
78-
explain
124+
#explain
79125
```
80126

81-
### Hide blockedBy
127+
### `hide depends on` ==TODO Rename to blocked by==
82128

83129
```tasks
84130
hide depends on
85131
path includes {{query.file.path}}
86-
explain
132+
#explain
87133
```

src/Query/Filter/BlockingField.ts

Lines changed: 9 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,24 +5,19 @@ export class BlockingField extends FilterInstructionsBasedField {
55
constructor() {
66
super();
77
this._filters.add('is blocking', (task, searchInfo: SearchInfo) => {
8-
if (task.id === '') return false;
9-
10-
return searchInfo.allTasks.some((cacheTask) => {
11-
return cacheTask.blockedBy.includes(task.id);
12-
});
8+
return task.isBlocking(searchInfo.allTasks);
139
});
14-
this._filters.add('is not blocked', (task, searchInfo: SearchInfo) => {
15-
if (task.blockedBy.length === 0) return true;
16-
17-
for (const depId of task.blockedBy) {
18-
const depTask = searchInfo.allTasks.find((task) => task.id === depId);
1910

20-
if (!depTask) continue;
11+
this._filters.add('is not blocking', (task, searchInfo: SearchInfo) => {
12+
return !task.isBlocking(searchInfo.allTasks);
13+
});
2114

22-
if (!depTask.status.isCompleted()) return false;
23-
}
15+
this._filters.add('is blocked', (task, searchInfo: SearchInfo) => {
16+
return task.isBlocked(searchInfo.allTasks);
17+
});
2418

25-
return true;
19+
this._filters.add('is not blocked', (task, searchInfo: SearchInfo) => {
20+
return !task.isBlocked(searchInfo.allTasks);
2621
});
2722
}
2823

src/Task/Task.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -457,7 +457,7 @@ export class Task {
457457
* Only direct dependencies are considered.
458458
* @param allTasks - all the tasks in the vault. In custom queries, this is available via query.allTasks.
459459
*/
460-
public isBlocked(allTasks: Task[]) {
460+
public isBlocked(allTasks: Readonly<Task[]>) {
461461
if (this.blockedBy.length === 0) {
462462
return false;
463463
}
@@ -487,7 +487,7 @@ export class Task {
487487
* Only direct dependencies are considered.
488488
* @param allTasks - all the tasks in the vault. In custom queries, this is available via query.allTasks.
489489
*/
490-
public isBlocking(allTasks: Task[]) {
490+
public isBlocking(allTasks: Readonly<Task[]>) {
491491
if (this.id === '') {
492492
return false;
493493
}

tests/CustomMatchers/CustomMatchersForFilters.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,10 @@ with filter: "${filter.instruction}"`,
174174
}
175175

176176
export function toMatchTaskInTaskList(filter: FilterOrErrorMessage, task: Task, allTasks: Task[]) {
177+
// Make sure that the task being filtered is actually in allTasks,
178+
// to guard against tests passing for the wrong reason:
179+
expect(allTasks.includes(task)).toEqual(true);
180+
177181
return toMatchTaskWithSearchInfo(filter, task, SearchInfo.fromAllTasks(allTasks));
178182
}
179183

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<!-- placeholder to force blank line before included text -->
2+
3+
| Task | `is blocking` | `is blocked` |
4+
| ----- | ----- | ----- |
5+
| `- [ ] No dependency - TODO` | ❌ false | ❌ false |
6+
| `- [x] No dependency - DONE` | ❌ false | ❌ false |
7+
| `- [ ] scenario 1 - TODO depends on TODO 🆔 scenario1` | ✅ true | ❌ false |
8+
| `- [ ] scenario 1 - TODO depends on TODO ⛔️ scenario1` | ❌ false | ✅ true |
9+
| `- [x] scenario 2 - TODO depends on DONE 🆔 scenario2` | ❌ false | ❌ false |
10+
| `- [ ] scenario 2 - TODO depends on DONE ⛔️ scenario2` | ❌ false | ❌ false |
11+
| `- [ ] scenario 3 - DONE depends on TODO 🆔 scenario3` | ❌ false | ❌ false |
12+
| `- [x] scenario 3 - DONE depends on TODO ⛔️ scenario3` | ❌ false | ❌ false |
13+
| `- [x] scenario 4 - DONE depends on DONE 🆔 scenario4` | ❌ false | ❌ false |
14+
| `- [x] scenario 4 - DONE depends on DONE ⛔️ scenario4` | ❌ false | ❌ false |
15+
| `- [ ] scenario 5 - TODO depends on non-existing ID ⛔️ nosuchid` | ❌ false | ❌ false |
16+
| `- [ ] scenario 6 - TODO depends on self 🆔 self ⛔️ self` | ✅ true | ✅ true |
17+
| `- [x] scenario 7 - task with duplicated id - this is DONE - 🆔 scenario7` | ❌ false | ❌ false |
18+
| `- [ ] scenario 7 - task with duplicated id - this is TODO - and is blocking - 🆔 scenario7` | ✅ true | ❌ false |
19+
| `- [ ] scenario 7 - TODO depends on id that is duplicated - ensure all tasks are checked - ⛔️ scenario7` | ❌ false | ✅ true |
20+
| `- [ ] scenario 8 - mutually dependant 🆔 scenario8a ⛔️ scenario8b` | ✅ true | ✅ true |
21+
| `- [ ] scenario 8 - mutually dependant 🆔 scenario8b ⛔️ scenario8a` | ✅ true | ✅ true |
22+
| `- [ ] scenario 9 - cyclic dependency 🆔 scenario9a ⛔️ scenario9c` | ✅ true | ✅ true |
23+
| `- [ ] scenario 9 - cyclic dependency 🆔 scenario9b ⛔️ scenario9a` | ✅ true | ✅ true |
24+
| `- [ ] scenario 9 - cyclic dependency 🆔 scenario9c ⛔️ scenario9b` | ✅ true | ✅ true |
25+
| `- [ ] scenario 10 - multiple dependencies TODO - 🆔 scenario10a` | ✅ true | ❌ false |
26+
| `- [/] scenario 10 - multiple dependencies IN_PROGRESS - 🆔 scenario10b` | ✅ true | ❌ false |
27+
| `- [x] scenario 10 - multiple dependencies DONE - 🆔 scenario10c` | ❌ false | ❌ false |
28+
| `- [-] scenario 10 - multiple dependencies CANCELLED - 🆔 scenario10d` | ❌ false | ❌ false |
29+
| `- [Q] scenario 10 - multiple dependencies NON_TASK - 🆔 scenario10e` | ❌ false | ❌ false |
30+
| `- [ ] scenario 10 - multiple dependencies - ⛔️ scenario10a,scenario10b,scenario10c,scenario10d,scenario10e` | ❌ false | ✅ true |
31+
| `- [ ] scenario 11 - indirect dependency - indirect blocking of scenario11c ignored - 🆔 scenario11a` | ❌ false | ❌ false |
32+
| `- [x] scenario 11 - indirect dependency - DONE - 🆔 scenario11b ⛔️ scenario11a` | ❌ false | ❌ false |
33+
| `- [ ] scenario 11 - indirect dependency - indirect blocking of scenario11a ignored - 🆔 scenario11c ⛔️ scenario11b` | ❌ false | ❌ false |
34+
35+
36+
<!-- placeholder to force blank line after included text -->

0 commit comments

Comments
 (0)