Skip to content

Commit 633f561

Browse files
authored
refactor: Create Task.isBlocked() and Task.isBlocking() (#2632)
* test: Start a framework for TDDing Task.isBlocked() and Task.isBlocking() * feat: Simplistic implementation of Task.isBlocking() Using only dependency info, not status. * fix: 'Done' tasks (with status DONE, CANCELLED or NON_TASK) are never blocking. * fix: Ignore dependent 'Done' tasks in isBlocking logic * feat: Simplistic implementation of Task.isBlocked() Treat as blocked if there us a blockedBy value * fix: Ignore non-existent task ids in isBlocked() calculation * fix: 'Done' tasks (with status DONE, CANCELLED or NON_TASK) are never blocked. * fix: Done tasks don't ever block * comment: Clarify behaviour * test: Show behaviour of self-dependency * test: Add test that demos incorrect behaviour with duplicated ids * fix: isBlocked() correctly handles multiple tasks with same id * test: Make approved file easier to read at a glance * jsdoc: Clarify comment * test: Add test of behaviour of cyclic dependencies * test: Add test of mutually dependent tasks * test: Test blocking with all status types - also shows multiple dependencies * test: Align emojis in related test cases, for readability * test: . Move isBlocked() and isBlocking() to file scope * test: . Move isBlocked() and isBlocking() to Task.ts * refactor: - Create Task.isBlocked() and Task.isBlocking() * refactor: - Manually inline isBlocked() and isBlocking() * test: Demonstrate that only direct dependencies are considered
1 parent 2b24d7a commit 633f561

File tree

3 files changed

+274
-1
lines changed

3 files changed

+274
-1
lines changed

src/Task/Task.ts

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -450,6 +450,61 @@ export class Task {
450450
);
451451
}
452452

453+
/**
454+
* A task is treated as blocked if it depends on any existing task ids on tasks that are TODO or IN_PROGRESS.
455+
*
456+
* 'Done' tasks (with status DONE, CANCELLED or NON_TASK) are never blocked.
457+
* Only direct dependencies are considered.
458+
* @param allTasks - all the tasks in the vault. In custom queries, this is available via query.allTasks.
459+
*/
460+
public isBlocked(allTasks: Task[]) {
461+
if (this.blockedBy.length === 0) {
462+
return false;
463+
}
464+
465+
if (this.isDone) {
466+
return false;
467+
}
468+
469+
for (const depId of this.blockedBy) {
470+
const depTask = allTasks.find((task) => task.id === depId && !task.isDone);
471+
if (!depTask) {
472+
// There is no not-done task with this id.
473+
continue;
474+
}
475+
476+
// We found a not-done task that this depends on, meaning this one is blocked:
477+
return true;
478+
}
479+
480+
return false;
481+
}
482+
483+
/**
484+
* A Task is blocking if there is any other not-done task blockedBy value with its id.
485+
*
486+
* 'Done' tasks (with status DONE, CANCELLED or NON_TASK) are never blocking.
487+
* Only direct dependencies are considered.
488+
* @param allTasks - all the tasks in the vault. In custom queries, this is available via query.allTasks.
489+
*/
490+
public isBlocking(allTasks: Task[]) {
491+
if (this.id === '') {
492+
return false;
493+
}
494+
495+
if (this.isDone) {
496+
return false;
497+
}
498+
499+
return allTasks.some((task) => {
500+
if (task.isDone) {
501+
return false;
502+
}
503+
504+
return task.blockedBy.includes(this.id);
505+
});
506+
}
507+
453508
/**
454509
* Return the number of the Task's priority.
455510
* - Highest = 0
@@ -797,3 +852,33 @@ export class Task {
797852
return description.match(TaskRegularExpressions.hashTags)?.map((tag) => tag.trim()) ?? [];
798853
}
799854
}
855+
856+
/**
857+
* A task is treated as blocked if it depends on any existing task ids on tasks that are TODO or IN_PROGRESS.
858+
*
859+
* 'Done' tasks (with status DONE, CANCELLED or NON_TASK) are never blocked.
860+
* @param thisTask
861+
* @param allTasks
862+
*/
863+
export function isBlocked(thisTask: Task, allTasks: Task[]) {
864+
if (thisTask.blockedBy.length === 0) {
865+
return false;
866+
}
867+
868+
if (thisTask.isDone) {
869+
return false;
870+
}
871+
872+
for (const depId of thisTask.blockedBy) {
873+
const depTask = allTasks.find((task) => task.id === depId && !task.isDone);
874+
if (!depTask) {
875+
// There is no not-done task with this id.
876+
continue;
877+
}
878+
879+
// We found a not-done task that this depends on, meaning this one is blocked:
880+
return true;
881+
}
882+
883+
return false;
884+
}
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
Visualise blocking methods on Task, for a collection of tasks
2+
3+
4+
5+
- [ ] No dependency - TODO
6+
isBlocked(): ❌ false
7+
isBlocking(): ❌ false
8+
9+
- [x] No dependency - DONE
10+
isBlocked(): ❌ false
11+
isBlocking(): ❌ false
12+
13+
- [ ] scenario 1 - TODO depends on TODO 🆔 scenario1
14+
isBlocked(): ❌ false
15+
isBlocking(): ✅ true
16+
17+
- [ ] scenario 1 - TODO depends on TODO ⛔️ scenario1
18+
isBlocked(): ✅ true
19+
isBlocking(): ❌ false
20+
21+
- [x] scenario 2 - TODO depends on DONE 🆔 scenario2
22+
isBlocked(): ❌ false
23+
isBlocking(): ❌ false
24+
25+
- [ ] scenario 2 - TODO depends on DONE ⛔️ scenario2
26+
isBlocked(): ❌ false
27+
isBlocking(): ❌ false
28+
29+
- [ ] scenario 3 - DONE depends on TODO 🆔 scenario3
30+
isBlocked(): ❌ false
31+
isBlocking(): ❌ false
32+
33+
- [x] scenario 3 - DONE depends on TODO ⛔️ scenario3
34+
isBlocked(): ❌ false
35+
isBlocking(): ❌ false
36+
37+
- [x] scenario 4 - DONE depends on DONE 🆔 scenario4
38+
isBlocked(): ❌ false
39+
isBlocking(): ❌ false
40+
41+
- [x] scenario 4 - DONE depends on DONE ⛔️ scenario4
42+
isBlocked(): ❌ false
43+
isBlocking(): ❌ false
44+
45+
- [ ] scenario 5 - TODO depends on non-existing ID ⛔️ nosuchid
46+
isBlocked(): ❌ false
47+
isBlocking(): ❌ false
48+
49+
- [ ] scenario 6 - TODO depends on self 🆔 self ⛔️ self
50+
isBlocked(): ✅ true
51+
isBlocking(): ✅ true
52+
53+
- [x] scenario 7 - task with duplicated id - this is DONE - 🆔 scenario7
54+
isBlocked(): ❌ false
55+
isBlocking(): ❌ false
56+
57+
- [ ] scenario 7 - task with duplicated id - this is TODO - and is blocking - 🆔 scenario7
58+
isBlocked(): ❌ false
59+
isBlocking(): ✅ true
60+
61+
- [ ] scenario 7 - TODO depends on id that is duplicated - ensure all tasks are checked - ⛔️ scenario7
62+
isBlocked(): ✅ true
63+
isBlocking(): ❌ false
64+
65+
- [ ] scenario 8 - mutually dependant 🆔 scenario8a ⛔️ scenario8a
66+
isBlocked(): ✅ true
67+
isBlocking(): ✅ true
68+
69+
- [ ] scenario 8 - mutually dependant 🆔 scenario8b ⛔️ scenario8a
70+
isBlocked(): ✅ true
71+
isBlocking(): ❌ false
72+
73+
- [ ] scenario 9 - cyclic dependency 🆔 scenario9a ⛔️ scenario9c
74+
isBlocked(): ✅ true
75+
isBlocking(): ✅ true
76+
77+
- [ ] scenario 9 - cyclic dependency 🆔 scenario9b ⛔️ scenario9a
78+
isBlocked(): ✅ true
79+
isBlocking(): ✅ true
80+
81+
- [ ] scenario 9 - cyclic dependency 🆔 scenario9c ⛔️ scenario9b
82+
isBlocked(): ✅ true
83+
isBlocking(): ✅ true
84+
85+
- [ ] scenario 10 - multiple dependencies TODO - 🆔 scenario10a
86+
isBlocked(): ❌ false
87+
isBlocking(): ✅ true
88+
89+
- [/] scenario 10 - multiple dependencies IN_PROGRESS - 🆔 scenario10b
90+
isBlocked(): ❌ false
91+
isBlocking(): ✅ true
92+
93+
- [x] scenario 10 - multiple dependencies DONE - 🆔 scenario10c
94+
isBlocked(): ❌ false
95+
isBlocking(): ❌ false
96+
97+
- [-] scenario 10 - multiple dependencies CANCELLED - 🆔 scenario10d
98+
isBlocked(): ❌ false
99+
isBlocking(): ❌ false
100+
101+
- [Q] scenario 10 - multiple dependencies NON_TASK - 🆔 scenario10e
102+
isBlocked(): ❌ false
103+
isBlocking(): ❌ false
104+
105+
- [ ] scenario 10 - multiple dependencies - ⛔️ scenario10a,scenario10b,scenario10c,scenario10d,scenario10e
106+
isBlocked(): ✅ true
107+
isBlocking(): ❌ false
108+
109+
- [ ] scenario 11 - indirect dependency - indirect blocking of scenario11c ignored - 🆔 scenario11a
110+
isBlocked(): ❌ false
111+
isBlocking(): ❌ false
112+
113+
- [x] scenario 11 - indirect dependency - DONE - 🆔 scenario11b ⛔️ scenario11a
114+
isBlocked(): ❌ false
115+
isBlocking(): ❌ false
116+
117+
- [ ] scenario 11 - indirect dependency - indirect blocking of scenario11a ignored - 🆔 scenario11c ⛔️ scenario11b
118+
isBlocked(): ❌ false
119+
isBlocking(): ❌ false

tests/Task/Task.test.ts

Lines changed: 70 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
* @jest-environment jsdom
33
*/
44
import moment from 'moment';
5+
import { verifyAll } from 'approvals/lib/Providers/Jest/JestApprovals';
56
import { Status } from '../../src/Statuses/Status';
67
import { Task } from '../../src/Task/Task';
78
import { resetSettings, updateSettings } from '../../src/Config/Settings';
@@ -10,7 +11,7 @@ import type { StatusCollection } from '../../src/Statuses/StatusCollection';
1011
import { StatusRegistry } from '../../src/Statuses/StatusRegistry';
1112
import { TaskLocation } from '../../src/Task/TaskLocation';
1213
import { StatusConfiguration, StatusType } from '../../src/Statuses/StatusConfiguration';
13-
import { fromLine } from '../TestingTools/TestHelpers';
14+
import { fromLine, fromLines } from '../TestingTools/TestHelpers';
1415
import { TaskBuilder } from '../TestingTools/TaskBuilder';
1516
import { RecurrenceBuilder } from '../TestingTools/RecurrenceBuilder';
1617
import { Priority } from '../../src/Task/Priority';
@@ -550,6 +551,74 @@ describe('properties for scripting', () => {
550551
});
551552
});
552553

554+
describe('task dependencies', () => {
555+
beforeEach(() => {
556+
const nonTaskStatus = new StatusConfiguration('Q', 'Question', 'A', true, StatusType.NON_TASK);
557+
StatusRegistry.getInstance().add(nonTaskStatus);
558+
});
559+
560+
afterEach(() => {
561+
StatusRegistry.getInstance().resetToDefaultStatuses();
562+
});
563+
564+
function toEmoji(boolean: boolean) {
565+
return boolean ? '✅ true' : '❌ false';
566+
}
567+
568+
it('blocking and blocked', () => {
569+
const lines = [
570+
'- [ ] No dependency - TODO',
571+
'- [x] No dependency - DONE',
572+
//
573+
'- [ ] scenario 1 - TODO depends on TODO 🆔 scenario1',
574+
'- [ ] scenario 1 - TODO depends on TODO ⛔️ scenario1',
575+
//
576+
'- [x] scenario 2 - TODO depends on DONE 🆔 scenario2',
577+
'- [ ] scenario 2 - TODO depends on DONE ⛔️ scenario2',
578+
//
579+
'- [ ] scenario 3 - DONE depends on TODO 🆔 scenario3',
580+
'- [x] scenario 3 - DONE depends on TODO ⛔️ scenario3',
581+
//
582+
'- [x] scenario 4 - DONE depends on DONE 🆔 scenario4',
583+
'- [x] scenario 4 - DONE depends on DONE ⛔️ scenario4',
584+
//
585+
'- [ ] scenario 5 - TODO depends on non-existing ID ⛔️ nosuchid',
586+
//
587+
'- [ ] scenario 6 - TODO depends on self 🆔 self ⛔️ self',
588+
//
589+
'- [x] scenario 7 - task with duplicated id - this is DONE - 🆔 scenario7',
590+
'- [ ] scenario 7 - task with duplicated id - this is TODO - and is blocking - 🆔 scenario7',
591+
'- [ ] scenario 7 - TODO depends on id that is duplicated - ensure all tasks are checked - ⛔️ scenario7',
592+
//
593+
'- [ ] scenario 8 - mutually dependant 🆔 scenario8a ⛔️ scenario8a',
594+
'- [ ] scenario 8 - mutually dependant 🆔 scenario8b ⛔️ scenario8a',
595+
//
596+
'- [ ] scenario 9 - cyclic dependency 🆔 scenario9a ⛔️ scenario9c',
597+
'- [ ] scenario 9 - cyclic dependency 🆔 scenario9b ⛔️ scenario9a',
598+
'- [ ] scenario 9 - cyclic dependency 🆔 scenario9c ⛔️ scenario9b',
599+
//
600+
'- [ ] scenario 10 - multiple dependencies TODO - 🆔 scenario10a',
601+
'- [/] scenario 10 - multiple dependencies IN_PROGRESS - 🆔 scenario10b',
602+
'- [x] scenario 10 - multiple dependencies DONE - 🆔 scenario10c',
603+
'- [-] scenario 10 - multiple dependencies CANCELLED - 🆔 scenario10d',
604+
'- [Q] scenario 10 - multiple dependencies NON_TASK - 🆔 scenario10e',
605+
'- [ ] scenario 10 - multiple dependencies - ⛔️ scenario10a,scenario10b,scenario10c,scenario10d,scenario10e',
606+
//
607+
'- [ ] scenario 11 - indirect dependency - indirect blocking of scenario11c ignored - 🆔 scenario11a',
608+
'- [x] scenario 11 - indirect dependency - DONE - 🆔 scenario11b ⛔️ scenario11a',
609+
'- [ ] scenario 11 - indirect dependency - indirect blocking of scenario11a ignored - 🆔 scenario11c ⛔️ scenario11b',
610+
];
611+
const tasks = fromLines({ lines });
612+
613+
verifyAll('Visualise blocking methods on Task, for a collection of tasks', tasks, (task) => {
614+
return `
615+
${task.toFileLineString()}
616+
isBlocked(): ${toEmoji(task.isBlocked(tasks))}
617+
isBlocking(): ${toEmoji(task.isBlocking(tasks))}`;
618+
});
619+
});
620+
});
621+
553622
describe('backlinks', () => {
554623
function shouldGiveLinkText(
555624
path: string,

0 commit comments

Comments
 (0)