Skip to content

Commit

Permalink
feat: Implement Task Dependencies (#2568)
Browse files Browse the repository at this point in the history
* basic chip

* deletable chips

* addable chips

* filters out already added tasks from search results

* better input floating

* selectable with keyboard

* ui and keyboard fixes

* lefthook

* Old sequential boolean code

* rename to id

* implement id serializing

* implement id deserializing

* search results favour tasks in same file

* svelte commenting

* depends on field

* blocking field

* Adds dependency on existing id

* addDependency returns parent and child

* Add id to child task if needed

* Doesn't create new task unless it needs to

* simplify code

* should not create duplicate dependency

* doesn't create new parent unless it needs to

* simplify code

* can remove dependency

* break out id checker`

* setDependencies test

* dont create unnecessary new parent

* rename deps function

* ensure task has id tests

* Refactor task dependency logic

* Passing in existing ids

* new (probably unique) id can be generated

* todo finish id generation

* persist new task id working

* writing depends on is working!

* fixed depends on regex

* svelte loads task dependencies

* refactor id generation to only happen on submit

* add duplicate id checking

* EditTask modal shows blocking

* EditTask can add and remove blocking

* remove unnecessary change to cache.ts

* small formatting fixes

* Query checkboxes look clickable

* Fixed id generation

* fixed indexes not being correctly reset

* variable name shortening

* fixed blocking dependency not being serialized

* fixed waitingOn dependency not being serialized

* Working dropdown fullwidth

* Fix console error spams

* task results proximity sorting

* highlights first result onfocus

* filters out itself from results

* reorder file

* call correct line number parameter

* fiddling with styles

* convert dropdowns to buttons

* Add access keys

* Ignore accessibility warning instead

* fix: dropdown now clickable

* test blocking filter using id

* test blocking filter using dependsOn

* make it better with some()

* refactor blockingField to own file

* created new getFilterParser() function

* make fieldCreators private

* add allTasks attribute

* move fieldCreators inside function

* connect is blocking in query

* fix: Add @ts-expect-error comments on 3 lines that currently don't compile

So I can fix them in separate steps.

* test: Add comment explaining why I think EditTask.test.ts tests are failing

* comment: Add some TODOs with areas to fix...

* re-enable lefthook

* fix: Can now add dependencies via Pencil icon in Tasks search results

* Make the cache private again in TasksPlugin

* fix: Remove need for @ts-expect-error

* fix: Remove need for @ts-expect-error

* Passed allTasks into FilterParser.ts functions

* Query.ts accepts allTasks param

* Starting to modify QueryRendererHelper.ts

* refactor: Make call to filter.filterFunction() explicit in Query.applyQueryToTasks()

* refactor: Make more calls to filter.filterFunction() explicit

* refactor: Make more calls to filter.filterFunction() explicit

* test: Extract new custom matcher toMatchTaskWithTaskList()

* refactor: 'is blocking' command now uses the allTasks list passed in to the filter

* refactor: BlockingField no longer needs to receive allTasks via constructor

* refactor: No longer need to pass allTasks in to FilterParser functions

* refactor: No longer need to pass allTasks in Query constructor

* test: Add a test of 'is blocking' with circular dependencies

* test: Reduce scope of variables

* test: Rename shared filter to clarify its meaning

* test: Add missing task to the allTasks list

* fix: Make BooleanField work with BlockingField

* create is not blocked filter

* Filters task with no deps

* Filters task if there is an incomplete dep

* Case-insensitive deps search

* Fix failing svelte test suite

* Remove unnecessary packages

* Indicate which blocked on are complete and incomplete

* fix: search results filters out blocked by and blocking

* docs: Beginning of task dependencies explanation

* docs: Add blocked by image

* fix: dependency results self-filtering

* add show/hide for new dependency fields

* Add status to task search results and chips

* vault: Add 'Dependencies Samples.md' for exploratory testing

* test: Add 'is not blocked' to Query.test.ts

* vault: Add examples of all the new query instructions

* feat: Add CSS for id and dependsOn fields

* vault: Add more sample searches in Dependencies Samples.md

* docs: Document the generic CSS classes for new fields 'id' and 'dependsOn'

* comment: Note required documentation if adding CSS classes & data attributes

* docs: first draft of deps documentation

* fix: Move 'only future dates' up to rest of dates options

* fix: long task names are clipped, with popover containing full name

* fix: long task names/file names are clipped in dropdown

* fix: small dropdown behaviour issues

* fix: more responsive dropdown, removed delay

* fix: blocking dropdown not hiding

* feat: disable dependency fields if task list is empty

* fix: Edge case dropdown visual bug

* fix: Grid display bug on mobile widths

* Migrate wording from waiting on to blocked by

* fix: small visual glitches

* fix: task names in same file are full width

* feat: search results file name and location popups

* feat: expand dropdown to 20 results

* refactor: DRY up dropdown code

* fix: Edits that should have been included in the merge commit

* fix: Edits that should have been included in the merge commit but got lost

* fix: Remove empty file VerifyMarkdownTable.ts - remnant of merge

* comment: Remove some HTML I accidentally copied from an error message

* chore: Update yarn.lock

* chore: migrate from dependsOn to blockedBy

* chore: migrate blockedBy symbol to ⛔️

* tests: Reinstate tests I disabled during the merge of 'upstream/main'

* fix: Correct the layout of Created, Done and Cancelled fields

Their CSS classes needed updating, when they were merged in from main.

* docs: Update snippets for addition of dependency emojis

* refactor: . Extract function symbolAndDateValue()

* refactor: . Extract function

* refactor: . Extract function symbolAndStringValue()

* refactor: . Fix misleading parameter name startDateSymbol

* refactor: - Simplify the serialisation of task.blockedBy

* fix: Don't render task.blockedBy values in 'short mode'

* fix: Don't render task.id value in 'short mode'

* refactor: . Remove some wasted work in symbolAndDateValue()

* refactor: - Simplify code for serialising recurrence rule

* test: Generate sample files of dependency fields in markdown

* docs: Add samples of dependency fields to Format reference docs

* docs: Minor updates to 'Task Dependencies.md'

* docs: Add limitations of dependency feature to 'Known Limitations' page

* test: Create table of Task Property values for documentation

* docs: Document task properties available for scripting.

* test: Fix duplicate test names

---------

Co-authored-by: Clare Macrae <github@cfmacrae.fastmail.co.uk>
  • Loading branch information
DanielTMolloy919 and claremacrae committed Jan 20, 2024
1 parent a3663e9 commit 3c9383b
Show file tree
Hide file tree
Showing 59 changed files with 1,439 additions and 69 deletions.
2 changes: 2 additions & 0 deletions docs/Advanced/Styling.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,8 @@ The generic classes are:
- `task-cancelled`
- `task-done`
- `task-recurring`
- `task-id`
- `task-blockedBy`

In addition to the generic classes, there are [**data attributes**](https://developer.mozilla.org/en-US/docs/Learn/HTML/Howto/Use_data_attributes) that represent the content of the various task components.

Expand Down
91 changes: 91 additions & 0 deletions docs/Getting Started/Task Dependencies.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
---
publish: true
---

# Task Dependencies

> [!released]
> Introduced in Tasks X.Y.Z.
## Introduction

At a high level, task dependencies define the order in which you want to work on a set of tasks.
This can be useful for mapping out projects, where one part needs to be completed before the other.
By specifying these dependencies, Obsidian Tasks can streamline your workflow by displaying only the tasks that are actionable at any given moment.

> [!NOTE]
> Obsidian tasks exclusively allows for Finish to start (FS) dependencies, meaning Task A needs to be finished before you start on Task B. You can learn more about this concept [on Wikipedia](https://en.wikipedia.org/wiki/Dependency_(project_management)).
## Example

To illustrate the concept of task dependencies, let's consider a scenario where we are outlining the tasks required to develop an application. Two tasks are identified:

```text
- [ ] Build a first draft
- [ ] Test with users
```

In this scenario, testing with users can only occur after the initial draft is completed. To establish this relationship, you can create a dependency between the two tasks using either of the following methods.

1. Open the 'Build a first draft' task in the Edit Task Modal and specify 'Test with users' as a 'Blocking' task
2. Alternatively, open the 'Test with users' task in the Edit Task Modal and add 'Build a first draft' as a 'Blocked By' task
![[task-dependencies-blocked-by-example.png]]

By implementing either of these methods, the task list is updated to reflect the dependency relationship:

```text
- [ ] Build a first draft 🆔 4ijuhy
- [ ] Test with users ⛔️ 4ijuhy
```

Then, if the query `is not blocked` is used

```tasks
is not blocked
```

We only see 'Build a first draft'

```text
- [ ] Build a first draft 🆔 4ijuhy
```

Until this task is marked as complete, at which time Obsidian Tasks sees that 'Test with users' is no longer blocked, and displays it as well

```text
- [x] Build a first draft 🆔 4ijuhy
- [ ] Test with users ⛔️ 4ijuhyz
```

## Nomenclature

Fields:

- `blockedBy`
- `id`

UI:

- Blocked by [implies an id of another task]
- Blocks

Query

- blocking
- blocked

## Adding Dependencies

## Searching For Dependencies

`is not blocked`

`is blocking`

![[Pasted image 20231011181837.png]]

## Known Limitations

- It's not yet possible to directly navigate from a task to the tasks it depends on.
- Outside of the edit task modal, it is not possible to see the descriptions of the blocking tasks.
- It is not yet possible to visualise the relationships in a graph viewer.
2 changes: 1 addition & 1 deletion docs/How To/Find tasks with invalid data.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ The following tasks block lists any tasks that have emoji in the description, wh
````text
```tasks
# These description instructions need to be all on one line:
(description includes 🔺) OR (description includes ⏫) OR (description includes 🔼) OR (description includes 🔽) OR (description includes ⏬) OR (description includes 🛫) OR (description includes ➕) OR (description includes ⏳) OR (description includes 📅) OR (description includes ✅) OR (description includes ❌) OR (description includes 🔁)
(description includes 🔺) OR (description includes ⏫) OR (description includes 🔼) OR (description includes 🔽) OR (description includes ⏬) OR (description includes 🛫) OR (description includes ➕) OR (description includes ⏳) OR (description includes 📅) OR (description includes ✅) OR (description includes ❌) OR (description includes 🔁) OR (description includes ⛔️) OR (description includes 🆔)
# Optionally, uncomment this line and exclude your templates location
# path does not include _templates
Expand Down
11 changes: 11 additions & 0 deletions docs/Reference/Task Formats/Dataview Format.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,17 @@ For more information, see [[Priority]].

For more information, see [[Recurring Tasks]].

### Dataview Format for Dependencies

<!-- snippet: DocsSamplesForTaskFormats.test.Serializer_Dependencies_dataview-snippet.approved.md -->
```md
- [ ] do this first [id:: dcf64c]
- [ ] do this after first and some other task [blockedBy:: dcf64c,0h17ye]
```
<!-- endSnippet -->

For more information, see [[Task Dependencies]].

## Auto-Suggest and Dataview format

The Dataview format fully supports Tasks' [[Auto-Suggest]] feature, but requires users to manually type out surrounding brackets (`[]` or `()`). This works best with `Settings > Editor > Autopair Brackets` enabled.
Expand Down
11 changes: 11 additions & 0 deletions docs/Reference/Task Formats/Tasks Emoji Format.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,17 @@ For more information, see [[Priority]].

For more information, see [[Recurring Tasks]].

## Tasks Emoji Format for Dependencies

<!-- snippet: DocsSamplesForTaskFormats.test.Serializer_Dependencies_tasksPluginEmoji-snippet.approved.md -->
```md
- [ ] do this first 🆔 dcf64c
- [ ] do this after first and some other task ⛔️ dcf64c,0h17ye
```
<!-- endSnippet -->

For more information, see [[Task Dependencies]].

## Limitations of Tasks Emoji Format

### Non-breaking spaces: NBSP characters
Expand Down
16 changes: 15 additions & 1 deletion docs/Scripting/Task Properties.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,20 @@ For more information, including adding your own customised statuses, see [[Statu
- The `Invalid date` category was added in Tasks 6.0.0.
1. The `fromNow` properties were added in Tasks 4.9.0.

## Values for Task Dependencies

<!-- placeholder to force blank line before included text --><!-- include: TaskProperties.test.task_dependency_fields.approved.md -->

| Field | Type 1 | Example 1 | Type 2 | Example 2 |
| ----- | ----- | ----- | ----- | ----- |
| `task.id` | `string` | `'abcdef'` | `string` | `''` |
| `task.blockedBy` | `string[]` | `['123456', 'abc123']` | `any[]` | `[]` |

<!-- placeholder to force blank line after included text --><!-- endInclude -->

1. See the page [[Task Dependencies]], which explains the dependencies facility.
1. Task Dependencies were released in Tasks X.Y.Z.

## Values for Other Task Properties

<!-- placeholder to force blank line before included text --><!-- include: TaskProperties.test.task_other_fields.approved.md -->
Expand All @@ -128,7 +142,7 @@ For more information, including adding your own customised statuses, see [[Statu
| `task.isRecurring` | `boolean` | `true` | `boolean` | `false` |
| `task.recurrenceRule` | `string` | `'every day when done'` | `string` | `''` |
| `task.tags` | `string[]` | `['#todo', '#health']` | `any[]` | `[]` |
| `task.originalMarkdown` | `string` | `' - [ ] Do exercises #todo #health 🔼 🔁 every day when done ➕ 2023-07-01 🛫 2023-07-02 ⏳ 2023-07-03 📅 2023-07-04 ❌ 2023-07-06 ✅ 2023-07-05 ^dcf64c'` | `string` | `'- [/] minimal task'` |
| `task.originalMarkdown` | `string` | `' - [ ] Do exercises #todo #health 🔼 🔁 every day when done ➕ 2023-07-01 🛫 2023-07-02 ⏳ 2023-07-03 📅 2023-07-04 ❌ 2023-07-06 ✅ 2023-07-05 ⛔️ 123456,abc123 🆔 abcdef ^dcf64c'` | `string` | `'- [/] minimal task'` |

<!-- placeholder to force blank line after included text --><!-- endInclude -->

Expand Down
4 changes: 4 additions & 0 deletions docs/Support and Help/Known Limitations.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ This page gathers together all the documentation on known limitations of the plu

![[Getting Started#Limitations and warnings]]

## Writing Tasks: Dependencies

![[Task Dependencies#Known Limitations]]

## Writing Tasks: Task Formats

![[About Task Formats#Limitations of task format support]]
Expand Down
Binary file added docs/images/Pasted image 20231011181837.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@
"typescript": "^5.0.4"
},
"dependencies": {
"@floating-ui/dom": "^1.5.3",
"boon-js": "^2.0.4",
"chrono-node": "2.3.9",
"eventemitter2": "^6.4.5",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# Dependencies Samples

- [ ] #task Choose a topic 🆔 ya44g5
- [ ] #task Research the subject ⛔️ ya44g5 🆔 g7317o
- [ ] #task Create an outline ⛔️ g7317o 🆔 rot7gb
- [ ] #task Develop main points ⛔️ rot7gb 🆔 mvplec
- [ ] #task Craft a conclusion ⛔️ mvplec 🆔 0wigip
- [ ] #task Proofread and edit ⛔️ 0wigip 🆔 5ti6bf
- [ ] #task Publish the article ⛔️ 5ti6bf

---

## Do Next

```tasks
((is blocking) AND (is not blocked)) OR (is not blocked)
not done
path includes {{query.file.path}}
explain
```

---

## Blocking Tasks

### Blocking Tasks - Any Status

```tasks
is blocking
path includes {{query.file.path}}
explain
```

### Blocking Tasks - Not Done

```tasks
is blocking
not done
path includes {{query.file.path}}
explain
```

---

## Blocked Tasks

### Blocked Tasks - Any Status

```tasks
is not blocked
path includes {{query.file.path}}
explain
```

### Blocked Tasks - Not Done

```tasks
is not blocked
not done
path includes {{query.file.path}}
explain
```

---

## Show/Hide Instructions

### Hide Id

```tasks
hide id
path includes {{query.file.path}}
explain
```

### Hide blockedBy

```tasks
hide depends on
path includes {{query.file.path}}
explain
```
9 changes: 8 additions & 1 deletion src/Api/createTaskLineModalHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,12 @@ export const defaultTaskModalFactory: taskModalFactory = (
onSubmit: (updatedTasks: Task[]) => void,
): ITaskModal => {
const task = taskFromLine({ line: '', path: '' });
return new TaskModal({ app, task, onSubmit }) as ITaskModal;
// TODO This is going to need some thought. It is missing the Cache argument.
// This file is part of the Tasks API that allows users to open the Edit Task modal from JavaScript:
// https://publish.obsidian.md/tasks/Advanced/Tasks+Api
// As a published API, to change the parameters would be a breaking change.
// One option is to make the allTasks parameter to the Edit task modal be optional,
// and if it's not provided, then hide the dependency fields in the modal.
// For now, we pass in an empty list of tasks.
return new TaskModal({ app, task, onSubmit, allTasks: [] }) as ITaskModal;
};
3 changes: 2 additions & 1 deletion src/Commands/CreateOrEdit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import type { Task } from '../Task';
import { DateFallback } from '../DateFallback';
import { taskFromLine } from './CreateOrEditTaskParser';

export const createOrEdit = (checking: boolean, editor: Editor, view: View, app: App) => {
export const createOrEdit = (checking: boolean, editor: Editor, view: View, app: App, allTasks: Task[]) => {
if (checking) {
return view instanceof MarkdownView;
}
Expand Down Expand Up @@ -36,6 +36,7 @@ export const createOrEdit = (checking: boolean, editor: Editor, view: View, app:
app,
task,
onSubmit,
allTasks,
});
taskModal.open();
};
4 changes: 4 additions & 0 deletions src/Commands/CreateOrEditTaskParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,8 @@ export const taskFromLine = ({ line, path }: { line: string; path: string }): Ta
doneDate: null,
cancelledDate: null,
recurrence: null,
blockedBy: [],
id: '',
blockLink: '',
tags: [],
originalMarkdown: '',
Expand Down Expand Up @@ -131,5 +133,7 @@ export const taskFromLine = ({ line, path }: { line: string; path: string }): Ta
originalMarkdown: '',
// Not needed since the inferred status is always re-computed after submitting.
scheduledDateIsInferred: false,
id: '',
blockedBy: [],
});
};
10 changes: 6 additions & 4 deletions src/Commands/index.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,26 @@
import type { App, Editor, MarkdownFileInfo, MarkdownView, Plugin, View } from 'obsidian';
import type { App, Editor, MarkdownFileInfo, MarkdownView, View } from 'obsidian';
import type TasksPlugin from '../main';
import { createOrEdit } from './CreateOrEdit';

import { toggleDone } from './ToggleDone';

export class Commands {
private readonly plugin: Plugin;
private readonly plugin: TasksPlugin;

private get app(): App {
return this.plugin.app;
}

constructor({ plugin }: { plugin: Plugin }) {
constructor({ plugin }: { plugin: TasksPlugin }) {
this.plugin = plugin;

plugin.addCommand({
id: 'edit-task',
name: 'Create or edit task',
icon: 'pencil',
editorCheckCallback: (checking: boolean, editor: Editor, view: MarkdownView | MarkdownFileInfo) => {
return createOrEdit(checking, editor, view as View, this.app);
// TODO Need to explore what happens if a tasks code block is rendered before the Cache has been created.
return createOrEdit(checking, editor, view as View, this.app, this.plugin.getTasks()!);
},
});

Expand Down
32 changes: 32 additions & 0 deletions src/Query/Filter/BlockingField.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import type { SearchInfo } from '../SearchInfo';
import { FilterInstructionsBasedField } from './FilterInstructionsBasedField';

export class BlockingField extends FilterInstructionsBasedField {
constructor() {
super();
this._filters.add('is blocking', (task, searchInfo: SearchInfo) => {
if (task.id === '') return false;

return searchInfo.allTasks.some((cacheTask) => {
return cacheTask.blockedBy.includes(task.id);
});
});
this._filters.add('is not blocked', (task, searchInfo: SearchInfo) => {
if (task.blockedBy.length === 0) return true;

for (const depId of task.blockedBy) {
const depTask = searchInfo.allTasks.find((task) => task.id === depId);

if (!depTask) continue;

if (!depTask.status.isCompleted()) return false;
}

return true;
});
}

fieldName(): string {
return 'blocking';
}
}

0 comments on commit 3c9383b

Please sign in to comment.