Repository: obsidian-tasks-group/obsidian-tasks
Pull request #2497 (2023-12-11 22:08:29)

feature: Experimental changes to (unreleased) Postpone menu options
<!--- Provide a general summary of your changes in the Title above -->

# Description

In using the Postpone button a lot in my own vault, I really wanted to see the following, before actually postponing:

- the field name that was going to be edited (Due, Scheduled or Start)
- the new date, including the day of the week

So this experiments with new formats.

I also wanted a few more date options.

Here is what I have changed - showing the Before and After:

![image](https://github.com/obsidian-tasks-group/obsidian-tasks/assets/4840096/d778f86b-1ca6-4c66-9fd6-93ea3d8ef845)

**Important caveat: after doing this, I realised that the new names only made sense for postponing overdue tasks. For tasks already in the future, the name Postpone is still likely better.**

## Motivation and Context

<!--- Why is this change required? What problem does it solve? -->
<!--- If it fixes an open issue, please link to the issue here. -->

See above.

Also, I'm really keen to move on and release this feature!

## How has this been tested?

<!--- Please describe in detail how you tested your changes. -->
<!--- Include details of your testing environment, tests ran to see how -->
<!--- your change affects other areas of the code, etc. -->

- New and existing tests
- Exploratory testing in the demo vault
- Much day-to-day use in my own main vault.

## Screenshots (if appropriate)

See above.

## Types of changes

<!--- What types of changes does your code introduce? Put an `x` in all the boxes that apply: -->

Changes visible to users:

- [x] **New feature** (prefix: `feat` - non-breaking change which adds functionality)
    - But note that these are changes to an unreleased feature

Internal changes:

- [x] **Refactor** (prefix: `refactor` - non-breaking change which only improves the design or structure of existing code, and making no changes to its external behaviour)
- [x] **Tests** (prefix: `test` - additions and improvements to unit tests and the smoke tests)

## 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! -->

- [x] My code follows the code style of this project and passes `yarn run lint`.
- [x] My change requires a change to the documentation.
- [ ] 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

<!--
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/src/QueryRenderer.ts b/src/QueryRenderer.ts
index d825635c..583b61b8 100644
--- a/src/QueryRenderer.ts
+++ b/src/QueryRenderer.ts
@@ -16,6 +16,8 @@ import {
     type HappensDate,
     createPostponedTask,
     getDateFieldToPostpone,
+    postponeButtonTitle,
+    postponeMenuItemTitle,
     postponementSuccessMessage,
     shouldShowPostponeButton,
 } from './Scripting/Postponer';
@@ -404,10 +406,13 @@ class QueryRenderChild extends MarkdownRenderChild {
     }
 
     private addPostponeButton(listItem: HTMLElement, task: Task, shortMode: boolean) {
+        const amount = 1;
+        const timeUnit = 'day';
+        const buttonTooltipText = postponeButtonTitle(task, amount, timeUnit);
         const button = listItem.createEl('button', {
             attr: {
                 id: 'postpone-button',
-                title: 'ℹ️ Postpone the task (right-click for more options)',
+                title: buttonTooltipText,
             },
         });
 
@@ -415,26 +420,28 @@ class QueryRenderChild extends MarkdownRenderChild {
         button.addClasses(classNames);
         button.setText(' ⏩');
 
-        button.addEventListener('click', () => this.postponeOnClickCallback(button, task, 1, 'days'));
+        button.addEventListener('click', () => this.postponeOnClickCallback(button, task, amount, timeUnit));
 
         /** Open a context menu on right-click.
          * Give a choice of postponing for a week, month, or quarter.
          */
         button.addEventListener('contextmenu', async (ev: MouseEvent) => {
             const menu = new Menu();
-            const commonTitle = 'Postpone for';
 
             const postponeMenuItemCallback = (item: MenuItem, timeUnit: unitOfTime.DurationConstructor, amount = 1) => {
-                const amountOrArticle = amount > 1 ? amount : 'a';
-                item.setTitle(`${commonTitle} ${amountOrArticle} ${timeUnit}`).onClick(() =>
-                    this.postponeOnClickCallback(button, task, amount, timeUnit),
-                );
+                const title = postponeMenuItemTitle(task, amount, timeUnit);
+                item.setTitle(title).onClick(() => this.postponeOnClickCallback(button, task, amount, timeUnit));
             };
 
             menu.addItem((item) => postponeMenuItemCallback(item, 'days', 2));
             menu.addItem((item) => postponeMenuItemCallback(item, 'days', 3));
+            menu.addItem((item) => postponeMenuItemCallback(item, 'days', 4));
+            menu.addItem((item) => postponeMenuItemCallback(item, 'days', 5));
+            menu.addItem((item) => postponeMenuItemCallback(item, 'days', 6));
+            menu.addSeparator();
             menu.addItem((item) => postponeMenuItemCallback(item, 'week'));
             menu.addItem((item) => postponeMenuItemCallback(item, 'weeks', 2));
+            menu.addItem((item) => postponeMenuItemCallback(item, 'weeks', 3));
             menu.addItem((item) => postponeMenuItemCallback(item, 'month'));
 
             menu.showAtPosition({ x: ev.clientX, y: ev.clientY });
diff --git a/src/Scripting/Postponer.ts b/src/Scripting/Postponer.ts
index 4c3c4ec8..9c6daa55 100644
--- a/src/Scripting/Postponer.ts
+++ b/src/Scripting/Postponer.ts
@@ -51,3 +51,23 @@ export function postponementSuccessMessage(postponedDate: Moment, dateFieldToPos
     const postponedDateString = postponedDate?.format('DD MMM YYYY');
     return `Task's ${dateFieldToPostpone} postponed until ${postponedDateString}`;
 }
+
+export function postponeButtonTitle(task: Task, amount: number, timeUnit: unitOfTime.DurationConstructor) {
+    const buttonText = postponeMenuItemTitle(task, amount, timeUnit);
+    return `ℹ️ ${buttonText} (right-click for more options)`;
+}
+
+export function postponeMenuItemTitle(task: Task, amount: number, timeUnit: unitOfTime.DurationConstructor) {
+    function capitalizeFirstLetter(word: string) {
+        return word.charAt(0).toUpperCase() + word.slice(1);
+    }
+    const updatedDateType = getDateFieldToPostpone(task)!;
+    const dateToUpdate = task[updatedDateType] as Moment;
+    const updatedDateDisplayText = capitalizeFirstLetter(updatedDateType.replace('Date', ''));
+
+    const postponedDate = new TasksDate(dateToUpdate).postpone(timeUnit, amount);
+    const formattedNewDate = postponedDate.format('ddd Do MMM');
+
+    const amountOrArticle = amount > 1 ? amount : 'a';
+    return `${updatedDateDisplayText} in ${amountOrArticle} ${timeUnit}, on ${formattedNewDate}`;
+}


Test patch:
diff --git a/tests/Scripting/Postponer.test.ts b/tests/Scripting/Postponer.test.ts
index 3172a37f..16fc7a70 100644
--- a/tests/Scripting/Postponer.test.ts
+++ b/tests/Scripting/Postponer.test.ts
@@ -7,6 +7,8 @@ import {
     type HappensDate,
     createPostponedTask,
     getDateFieldToPostpone,
+    postponeButtonTitle,
+    postponeMenuItemTitle,
     postponementSuccessMessage,
     shouldShowPostponeButton,
 } from '../../src/Scripting/Postponer';
@@ -16,6 +18,16 @@ import { TaskBuilder } from '../TestingTools/TaskBuilder';
 
 window.moment = moment;
 
+const today = '2023-12-03';
+beforeEach(() => {
+    jest.useFakeTimers();
+    jest.setSystemTime(new Date(today));
+});
+
+afterEach(() => {
+    jest.useRealTimers();
+});
+
 describe('postpone - date field choice', () => {
     function checkPostponeField(taskBuilder: TaskBuilder, expected: HappensDate | null) {
         const task = taskBuilder.build();
@@ -146,16 +158,27 @@ describe('postpone - whether to show button', () => {
     });
 });
 
-describe('postpone - new task creation', () => {
-    beforeEach(() => {
-        jest.useFakeTimers();
-        jest.setSystemTime(new Date('2023-12-03'));
+describe('postpone - UI text', () => {
+    it('should include date type and new date in button tooltip', () => {
+        const task = new TaskBuilder().dueDate(today).build();
+        expect(postponeButtonTitle(task, 1, 'day')).toEqual(
+            'ℹ️ Due in a day, on Mon 4th Dec (right-click for more options)',
+        );
+        expect(postponeButtonTitle(task, 2, 'days')).toEqual(
+            'ℹ️ Due in 2 days, on Tue 5th Dec (right-click for more options)',
+        );
     });
 
-    afterEach(() => {
-        jest.useRealTimers();
+    it('should include date type and new date in context menu labels', () => {
+        const task = new TaskBuilder().dueDate(today).build();
+        // TODO This text is misleading if the date is already in the future.
+        //      In that case, it should still be 'Postpone'???
+        expect(postponeMenuItemTitle(task, 1, 'day')).toEqual('Due in a day, on Mon 4th Dec');
+        expect(postponeMenuItemTitle(task, 2, 'days')).toEqual('Due in 2 days, on Tue 5th Dec');
     });
+});
 
+describe('postpone - new task creation', () => {
     function testPostponedTaskAndDate(task: Task, expectedDateField: HappensDate, expectedPostponedDate: string) {
         const { postponedDate, postponedTask } = createPostponedTask(task, expectedDateField, 'day', 1);
         expect(postponedDate.format('YYYY-MM-DD')).toEqual(expectedPostponedDate);