Repository: obsidian-tasks-group/obsidian-tasks
Pull request #2677 (2024-02-26 14:28:31)

docs: Use dates other than 'today' with core Daily Notes core plugin
I think this hint would be helpful for some users.

# Description

Add an example of workaround that allows user to use default `Daily-notes` plugin.

If there are better ways to give out this information, please feel free to close this PR and create a new one.

## Motivation and Context

It's possible to express "due before date+14d" without using `Calendar` or `Periodic Notes`, and I think it's worth mentioning
(I think it could be a good alternative for those who turns on "Open daily note on startup" option).

## How has this been tested?

Previewed in Obsidian and links tested.

## 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:

- [ ] **Bug fix** (prefix: `fix` - non-breaking change which fixes an issue)
- [ ] **New feature** (prefix: `feat` - non-breaking change which adds functionality)
- [ ] **Breaking change** (prefix: `feat!!` or `fix!!` - fix or feature that would cause existing functionality to not work as expected)
- [x] **Documentation** (prefix: `docs` - improvements to any documentation content **for users**)
- [ ] **Sample vault** (prefix: `vault` - improvements to the [Tasks-Demo sample vault](https://github.com/obsidian-tasks-group/obsidian-tasks/tree/main/resources/sample_vaults/Tasks-Demo))
- [ ] **Contributing Guidelines** (prefix: `contrib` - any improvements to documentation content **for contributors** - see [Contributing to Tasks](https://publish.obsidian.md/tasks-contributing/))

Internal changes:

- [ ] **Refactor** (prefix: `refactor` - non-breaking change which only improves the design or structure of existing code, and making no changes to its external behaviour)
- [ ] **Tests** (prefix: `test` - additions and improvements to unit tests and the smoke tests)
- [ ] **Infrastructure** (prefix: `chore` - examples include GitHub Actions, issue templates)

## 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! -->

- [ ] My code follows the code style of this project and passes `yarn run lint`.
- [ ] My change requires a change to the documentation.
- [x] I have [updated the documentation](https://publish.obsidian.md/tasks-contributing/Documentation/About+Documentation) accordingly.
- [ ] 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/Advanced/Daily Agenda.md b/docs/Advanced/Daily Agenda.md
index 117260b1..dd5b12da 100644
--- a/docs/Advanced/Daily Agenda.md
+++ b/docs/Advanced/Daily Agenda.md
@@ -22,6 +22,13 @@ make sure new notes are created via one of these two plugins, and not `Daily-not
 | template syntax | `due on {{date+14d:YYYY-MM-DD}}` | `due on {{date+14d:YYYY-MM-DD}}` | `due on {{date+14d:YYYY-MM-DD}}` |
 | output          | `due on {{date+14d:YYYY-MM-DD}}` | `due on 2021-08-28`              | `due on 2021-08-28`              |
 
+If you want to use the default `Daily-notes` plugin, the workaround is replacing unsupported syntax with [[Custom Filters|custom filtering]] expression.
+For example, `due before {{date+14d:YYYY-MM-DD}}` can be replaced with
+
+```markdown
+filter by function task.due.moment?.isBefore(moment("{{date:YYYY-MM-DD}}").add(14, 'days'), 'day') || false
+```
+
 ## Example Daily Agenda **template**
 
     ## Tasks
diff --git a/src/ui/Dependency.svelte b/src/ui/Dependency.svelte
index 9b580c7e..8030dc90 100644
--- a/src/ui/Dependency.svelte
+++ b/src/ui/Dependency.svelte
@@ -2,6 +2,7 @@
     import type { Task } from "../Task/Task";
     import { computePosition, flip, offset, shift, size } from "@floating-ui/dom";
     import type { EditableTask } from "./EditableTask";
+    import { searchForCandidateTasksForDependency } from './DependencyHelpers';
 
     export let task: Task;
     export let editableTask: EditableTask;
@@ -11,8 +12,6 @@
     export let accesskey: (key: string) => string | null;
     export let accesskeyLetter: string = '';
 
-    const MAX_SEARCH_RESULTS = 20;
-
     let search: string = '';
     let searchResults: Task[] | null = null;
     let searchIndex: number | null = 0;
@@ -74,39 +73,7 @@
         if (!search && !showDropdown) return [];
 
         showDropdown = false;
-
-        let results = allTasks.filter(task => task.description.toLowerCase().includes(search.toLowerCase()));
-
-        // remove itself, and tasks this task already has a relationship with from results
-        results = results.filter((item) => {
-            // line number is unavailable for the task being edited
-            // Known issue - filters out duplicate lines in task file
-            const sameFile = item.description === task.description &&
-                item.taskLocation.path === task.taskLocation.path &&
-                item.originalMarkdown === task.originalMarkdown
-
-            return ![...editableTask.blockedBy, ...editableTask.blocking].includes(item) && !sameFile;
-        });
-
-        // search results favour tasks from the same file as this task
-        results.sort((a, b) => {
-            const aInSamePath = a.taskLocation.path === task.taskLocation.path;
-            const bInSamePath = b.taskLocation.path === task.taskLocation.path;
-
-            // prioritise tasks close to this task in the same file
-            if (aInSamePath && bInSamePath) {
-                return Math.abs(a.taskLocation.lineNumber - task.taskLocation.lineNumber)
-                    - Math.abs(b.taskLocation.lineNumber - task.taskLocation.lineNumber);
-            } else if (aInSamePath) {
-                return -1;
-            } else if (bInSamePath) {
-                return 1;
-            } else {
-                return 0;
-            }
-        });
-
-        return results.slice(0,MAX_SEARCH_RESULTS);
+        return searchForCandidateTasksForDependency(search, allTasks, task, editableTask.blockedBy, editableTask.blocking);
     }
 
     function onFocused() {
diff --git a/src/ui/DependencyHelpers.ts b/src/ui/DependencyHelpers.ts
new file mode 100644
index 00000000..5aa0204e
--- /dev/null
+++ b/src/ui/DependencyHelpers.ts
@@ -0,0 +1,57 @@
+import type { Task } from '../Task/Task';
+
+const MAX_SEARCH_RESULTS = 20;
+
+export function searchForCandidateTasksForDependency(
+    search: string,
+    allTasks: Task[],
+    task: Task,
+    blockedBy: Task[],
+    blocking: Task[],
+) {
+    let results = allTasks.filter((task) => task.description.toLowerCase().includes(search.toLowerCase()));
+
+    results = results.filter((item) => {
+        // Do not offer to depend on DONE, CANCELLED or NON_TASK tasks:
+        if (item.isDone) {
+            return false;
+        }
+
+        // Do not show any tasks that look like templates:
+        if (item.description.includes('<%') && item.description.includes('%>')) {
+            return false;
+        }
+
+        // remove itself, and tasks this task already has a relationship with from results
+        // line number is unavailable for the task being edited
+        // Known issue - filters out duplicate lines in task file
+        const sameFile =
+            item.description === task.description &&
+            item.taskLocation.path === task.taskLocation.path &&
+            item.originalMarkdown === task.originalMarkdown;
+
+        return ![...blockedBy, ...blocking].includes(item) && !sameFile;
+    });
+
+    // search results favour tasks from the same file as this task
+    results.sort((a, b) => {
+        const aInSamePath = a.taskLocation.path === task.taskLocation.path;
+        const bInSamePath = b.taskLocation.path === task.taskLocation.path;
+
+        // prioritise tasks close to this task in the same file
+        if (aInSamePath && bInSamePath) {
+            return (
+                Math.abs(a.taskLocation.lineNumber - task.taskLocation.lineNumber) -
+                Math.abs(b.taskLocation.lineNumber - task.taskLocation.lineNumber)
+            );
+        } else if (aInSamePath) {
+            return -1;
+        } else if (bInSamePath) {
+            return 1;
+        } else {
+            return 0;
+        }
+    });
+
+    return results.slice(0, MAX_SEARCH_RESULTS);
+}


Test patch:
diff --git a/tests/ui/DependencyHelpers.test.ts b/tests/ui/DependencyHelpers.test.ts
new file mode 100644
index 00000000..665bdfae
--- /dev/null
+++ b/tests/ui/DependencyHelpers.test.ts
@@ -0,0 +1,120 @@
+import { createTasksFromMarkdown } from '../TestingTools/TestHelpers';
+import { searchForCandidateTasksForDependency } from '../../src/ui/DependencyHelpers';
+import { StatusRegistry } from '../../src/Statuses/StatusRegistry';
+import { StatusConfiguration, StatusType } from '../../src/Statuses/StatusConfiguration';
+import type { Task } from '../../src/Task/Task';
+
+function offersTheseCandidatesForTasks(
+    taskBeingEdited: Task,
+    allTasks: Task[],
+    descriptionsOfCandidateTasks: string[],
+) {
+    const blockedBy: Task[] = [];
+
+    // TODO: Put this code in somewhere that it is tested and re-usable:
+    for (const taskId of taskBeingEdited.dependsOn) {
+        const depTask = allTasks.find((cacheTask) => cacheTask.id === taskId);
+        if (depTask) {
+            blockedBy.push(depTask);
+        }
+    }
+
+    // TODO: Put this code in somewhere that it is tested and re-usable:
+    const blocking: Task[] = allTasks.filter((t) => t.dependsOn.includes(taskBeingEdited.id));
+
+    const suggestions = searchForCandidateTasksForDependency('', allTasks, taskBeingEdited, blockedBy, blocking);
+
+    expect(suggestions.map((task) => task.description)).toEqual(descriptionsOfCandidateTasks);
+}
+
+function offersTheseCandidates(markdown: string, descriptionsOfCandidateTasks: string[]) {
+    const allTasks = createTasksFromMarkdown(markdown, 'filename.md', 'header');
+    offersTheseCandidatesForTasks(allTasks[0], allTasks, descriptionsOfCandidateTasks);
+}
+
+describe('searching for tasks', () => {
+    beforeEach(() => {
+        StatusRegistry.getInstance().resetToDefaultStatuses();
+        const nonTaskStatus = new StatusConfiguration('Q', 'Question', 'A', true, StatusType.NON_TASK);
+        StatusRegistry.getInstance().add(nonTaskStatus);
+    });
+
+    afterEach(() => {
+        StatusRegistry.getInstance().resetToDefaultStatuses();
+    });
+
+    it('should show tasks other than the one being edited', () => {
+        offersTheseCandidates(
+            `
+                - [ ] Task being edited
+                - [ ] Some other task
+            `,
+            ['Some other task'],
+        );
+    });
+
+    it('should sort files in same file before those in others', () => {
+        // Arrange
+        const taskInOtherFile = createTasksFromMarkdown(
+            `
+                - [ ] Task from other file - should be offered after task from file being edited
+            `,
+            'other file.md',
+            'header',
+        );
+        const tasksInFileBeingEdited = createTasksFromMarkdown(
+            `
+                - [ ] Task being edited
+                - [ ] In same file as task being edited - should be offered first
+            `,
+            'this file.md',
+            'header',
+        );
+
+        const allTasks = [...taskInOtherFile].concat(tasksInFileBeingEdited);
+        // Check that task from other file is first, to avoid passing by luck:
+        expect(allTasks[0]).toBe(taskInOtherFile[0]);
+
+        // Act, Assert:
+        offersTheseCandidatesForTasks(tasksInFileBeingEdited[0], allTasks, [
+            'In same file as task being edited - should be offered first',
+            'Task from other file - should be offered after task from file being edited',
+        ]);
+    });
+
+    it('should not offer DONE, CANCELLED and NON_TASK tasks', () => {
+        offersTheseCandidates(
+            `
+                - [ ] Task being edited
+                - [ ] Todo
+                - [/] In Progress
+                - [-] Cancelled
+                - [x] Done
+                - [Q] Non-Task
+            `,
+            ['Todo', 'In Progress'],
+        );
+    });
+
+    it('should exclude tasks which depend on the edited task', () => {
+        offersTheseCandidates(
+            `
+                - [ ] Task being edited 🆔 12345
+                - [ ] Depends on task being edited️ ⛔️ 12345
+                - [ ] Does not depend on task being edited️
+            `,
+            ['Does not depend on task being edited️'],
+        );
+    });
+
+    it('should exclude tasks which the task being edited depends on', () => {
+        offersTheseCandidates(
+            `
+                - [ ] Task being edited ⛔️ 12345
+                - [ ] Task being edited️ depends on this 🆔 12345
+                - [ ] Is not depended on by the task being edited
+            `,
+            ['Is not depended on by the task being edited'],
+        );
+    });
+});