Repository: obsidian-tasks-group/obsidian-tasks
Pull request #2454 (2023-11-28 15:25:30)

feat: Enable postponing of start dates
# Description

Teach `getDateFieldToPostpone()` to postpone start date, treat it same as scheduled & due date.

## Motivation and Context

Expand postpone feature on happens date (start, due, scheduled).

## How has this been tested?

Modified existing unit tests, exploratory testing with vault from GitHub workflow.

## Types of changes

Changes visible to users:

- [x] **New feature** (prefix: `feat` - non-breaking change which adds functionality)

## Checklist

- [x] My code follows the code style of this project and passes `yarn run lint`.
- [ ] My change requires a change to the documentation.
  - I guess we will need a separate PR to add the docs for the whole feature
- [ ] 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

- [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)


claremacrae: > Modified existing unit tests.

It definitely needs manual exploratory testing - and I think that when you do that, you will find other code that also needs changing.
ilandikov: > > Modified existing unit tests.
> 
> It definitely needs manual exploratory testing - and I think that when you do that, you will find other code that also needs changing.

Ok, sure. Maybe you could share the markdown we used during our pairing?
ilandikov: > > Ok, sure. Maybe you could share the markdown we used during our pairing?
> 
> Oh yes, good idea - see [#2452 (comment)](https://github.com/obsidian-tasks-group/obsidian-tasks/discussions/2452#discussioncomment-7694517)
> 
> > TBH I don't see what could go wrong here but that could be a problem by itself =)
> 
> Well, there's two things:
> 
> 1. the general importance of always exploratory testing new features
> 2. whether there is anything obviously not yet updated for this PR

ah ok so making such test is more integration related, rather then just_this_small_change related

ilandikov: Ok, I see very well now why exploratory tests are needed =)

I tried our test file from the discussion also with tasks with start & due and start & scheduled date.

When only the start date is present, it is postponed. Otherwise another date is postponed
claremacrae: > Ok, I see very well now why exploratory tests are needed =)

Excellent!

> I tried our test file from the discussion also with tasks with start & due and start & scheduled date.
> 
> When only the start date is present, it is postponed. Otherwise another date is postponed

Thank you.

When you have time, would you mind editing the PR description to add the additional form of testing please?


ilandikov: Done =)

Patch:
diff --git a/src/QueryRenderer.ts b/src/QueryRenderer.ts
index 12f31eaa..d366346f 100644
--- a/src/QueryRenderer.ts
+++ b/src/QueryRenderer.ts
@@ -9,6 +9,7 @@ import { getTaskLineAndFile, replaceTaskWithTasks } from './File';
 
 import type { IQuery } from './IQuery';
 import {
+    type HappensDate,
     createPostponedTask,
     explainResults,
     getDateFieldToPostpone,
@@ -482,10 +483,11 @@ class QueryRenderChild extends MarkdownRenderChild {
         timeUnit: unitOfTime.DurationConstructor = 'days',
         amount = 1,
     ) {
-        const errorMessage = '⚠️ Postponement requires a date: due or scheduled.';
-        if (!task.dueDate && !task.scheduledDate) return new Notice(errorMessage, 10000);
         const dateTypeToUpdate = getDateFieldToPostpone(task);
-        if (dateTypeToUpdate === null) return;
+        if (dateTypeToUpdate === null) {
+            const errorMessage = '⚠️ Postponement requires a date: due, scheduled or start.';
+            return new Notice(errorMessage, 10000);
+        }
 
         const { postponedDate, newTasks } = createPostponedTask(task, dateTypeToUpdate, timeUnit, amount);
 
@@ -500,7 +502,7 @@ class QueryRenderChild extends MarkdownRenderChild {
 
     private onPostponeSuccessCallback(
         button: HTMLButtonElement,
-        updatedDateType: 'dueDate' | 'scheduledDate',
+        updatedDateType: HappensDate,
         postponedDateString: string,
     ) {
         // Disable the button to prevent update error due to the task not being reloaded yet.
diff --git a/src/lib/QueryRendererHelper.ts b/src/lib/QueryRendererHelper.ts
index 56414a2b..d8f46f3e 100644
--- a/src/lib/QueryRendererHelper.ts
+++ b/src/lib/QueryRendererHelper.ts
@@ -72,15 +72,33 @@ export function getQueryForQueryRenderer(source: string, globalQuery: GlobalQuer
     return globalQuery.query(path).append(tasksBlockQuery);
 }
 
-export function getDateFieldToPostpone(task: Task) {
-    const scheduledDateOrNull = task.scheduledDate ? 'scheduledDate' : null;
-    const dateTypeToUpdate = task.dueDate ? 'dueDate' : scheduledDateOrNull;
-    return dateTypeToUpdate;
+export type HappensDate = keyof Pick<Task, 'startDate' | 'scheduledDate' | 'dueDate'>;
+
+/**
+ * Gets a {@link HappensDate} field from a {@link Task} with the following priority: due > scheduled > start.
+ * If the task has no happens field {@link HappensDate}, null is returned.
+ *
+ * @param task
+ */
+export function getDateFieldToPostpone(task: Task): HappensDate | null {
+    if (task.dueDate) {
+        return 'dueDate';
+    }
+
+    if (task.scheduledDate) {
+        return 'scheduledDate';
+    }
+
+    if (task.startDate) {
+        return 'startDate';
+    }
+
+    return null;
 }
 
 export function createPostponedTask(
     task: Task,
-    dateTypeToUpdate: keyof Task,
+    dateTypeToUpdate: HappensDate,
     timeUnit: unitOfTime.DurationConstructor,
     amount: number,
 ) {


Test patch:
diff --git a/tests/lib/QueryRendererHelper.test.ts b/tests/lib/QueryRendererHelper.test.ts
index 185365b6..54f6468f 100644
--- a/tests/lib/QueryRendererHelper.test.ts
+++ b/tests/lib/QueryRendererHelper.test.ts
@@ -3,11 +3,15 @@
  */
 import moment from 'moment';
 import { Query } from '../../src/Query/Query';
-import { explainResults, getDateFieldToPostpone, getQueryForQueryRenderer } from '../../src/lib/QueryRendererHelper';
+import {
+    type HappensDate,
+    explainResults,
+    getDateFieldToPostpone,
+    getQueryForQueryRenderer,
+} from '../../src/lib/QueryRendererHelper';
 import { GlobalFilter } from '../../src/Config/GlobalFilter';
 import { GlobalQuery } from '../../src/Config/GlobalQuery';
 import { TaskBuilder } from '../TestingTools/TaskBuilder';
-import type { Task } from '../../src/Task';
 
 window.moment = moment;
 
@@ -130,7 +134,7 @@ describe('query used for QueryRenderer', () => {
 });
 
 describe('postpone - date field choice', () => {
-    function checkPostponeField(taskBuilder: TaskBuilder, expected: keyof Task | null) {
+    function checkPostponeField(taskBuilder: TaskBuilder, expected: HappensDate | null) {
         const task = taskBuilder.build();
         expect(getDateFieldToPostpone(task)).toEqual(expected);
     }
@@ -168,9 +172,9 @@ describe('postpone - date field choice', () => {
         checkPostponeField(taskBuilder, 'scheduledDate');
     });
 
-    it('should not postpone start date', () => {
+    it('should postpone start date', () => {
         const taskBuilder = new TaskBuilder().startDate(date);
-        checkDoesNotPostpone(taskBuilder);
+        checkPostponeField(taskBuilder, 'startDate');
     });
 
     it('should postpone due date in preference to start and scheduled dates', () => {