Repository: obsidian-tasks-group/obsidian-tasks
Pull request #2352 (2023-10-20 22:04:28)

docs: Add page 'Recurring Tasks and Custom Statuses'
<!--- Provide a general summary of your changes in the Title above -->

# Description

<!--- Describe your changes in detail -->

## Motivation and Context

- Add a major new page 'Recurring Tasks and Custom Statuses'.
- Add test code to generate more detailed Mermaid diagrams of statuses

## How has this been tested?

It's apparent from a few user support questions that I didn't explain properly how recurring tasks interact with custom statuses.

## Screenshots (if appropriate)

A part of the new page:

![image](https://github.com/obsidian-tasks-group/obsidian-tasks/assets/4840096/26e77956-109c-46f7-aea3-9ed41891bc51)


## 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] **Documentation** (prefix: `docs` - improvements to any documentation content **for users**)

Internal changes:

- [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.
- [x] 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/docs/Getting Started/Recurring Tasks.md b/docs/Getting Started/Recurring Tasks.md
index ee8e0d61..917e7c2e 100644
--- a/docs/Getting Started/Recurring Tasks.md
+++ b/docs/Getting Started/Recurring Tasks.md
@@ -52,6 +52,11 @@ Use this setting to control where the recurring task is inserted. The default is
 > [!released]
 > Control of the location (or order) of the new task was introduced in Tasks 3.8.0
 
+### Recurring Tasks with Custom Statuses
+
+> [!Warning]
+> If you use [[Custom Statuses]], please see [[Recurring Tasks and Custom Statuses]] for important information about how these two facilities interact.
+
 ## Repeating a Task Based on the Original Due Date or the Completion Date
 
 When you create a recurring task, you can decide whether the next occurrence should be based on the original dates or the date when you completed the task.
diff --git a/docs/Getting Started/Statuses.md b/docs/Getting Started/Statuses.md
index 5c2ee128..5151890b 100644
--- a/docs/Getting Started/Statuses.md
+++ b/docs/Getting Started/Statuses.md
@@ -90,6 +90,8 @@ It is the Task Status Type changing **from** `DONE` that controls when:
 
 - tasks **lose** their Done dates (if Done dates are enabled in settings).
 
+See [[Recurring Tasks and Custom Statuses]] for a more thorough explanation of the above.
+
 ## What can Statuses do?
 
 Now we have seen what is in a Status, what can we do with them?
diff --git a/docs/Getting Started/Statuses/About Statuses.md b/docs/Getting Started/Statuses/About Statuses.md
index 12480e8f..4ea25937 100644
--- a/docs/Getting Started/Statuses/About Statuses.md
+++ b/docs/Getting Started/Statuses/About Statuses.md
@@ -19,6 +19,10 @@ This section provides more detail to flesh out the introductory material in [[St
 - [[Status Settings]]
 - [[Editing a Status]]
 
+## Statuses in Use
+
+- [[Recurring Tasks and Custom Statuses]]
+
 ## Ideas - what you can do with Statuses
 
 - [[Example Statuses]]
diff --git a/docs/Getting Started/Statuses/Recurring Tasks and Custom Statuses.md b/docs/Getting Started/Statuses/Recurring Tasks and Custom Statuses.md
new file mode 100644
index 00000000..6ff104f7
--- /dev/null
+++ b/docs/Getting Started/Statuses/Recurring Tasks and Custom Statuses.md
@@ -0,0 +1,170 @@
+---
+publish: true
+---
+
+# Recurring Tasks and Custom Statuses
+
+> [!Note]
+> This page assumes basic familiarity with [[Recurring Tasks]].
+>
+> Also, for examples in this page we adjusted the Tasks settings so that [[Recurring Tasks#Order of the new task|new task recurrences are added *below* the previous one.]]
+
+## Determining the next status
+
+When you click on the checkbox of a task, the Tasks plugin applies the following logic: ^[Except when the task status has type `NON_TASK`. See [[#When the current status type is NON_TASK]] for an example.]
+
+1. When a task line checkbox is clicked, the next status symbol is looked up in the user's Task plugin's [[Status Settings]].
+2. If the next status symbol is of type `DONE`:
+    - A [[Dates#Done date|Done date]] is added (if enabled in user settings).
+    - If the task line has a Recurrence rule, an new task is created, with the next status symbol after the `DONE` symbol.
+
+> [!Important]
+> Every time Tasks detects you clicked on a task checkbox, the plugin reads the current status symbol - inside the `[` and `]` - and looks it up in your Tasks status settings, to determine what the next status symbol will be.
+
+## Worked example, with recurring task
+
+### Starting point
+
+Suppose that we have adjusted the default Tasks settings slightly, to give us the **following Tasks status settings**:
+
+<!-- include: DocsSamplesForStatuses.test.DefaultStatuses_todo-in_progress-done.approved.detailed.mermaid.md -->
+```mermaid
+flowchart LR
+1["'Todo'<br>[ ] -> [/]<br>(TODO)"]
+2["'In Progress'<br>[/] -> [x]<br>(IN_PROGRESS)"]
+3["'Done'<br>[x] -> [ ]<br>(DONE)"]
+1 --> 2
+2 --> 3
+3 --> 1
+```
+<!-- endInclude -->
+
+And we have selected the Tasks setting to put [[#Order of the new task|the new recurrence below the initial one]].
+
+Now imagine we have the **following task line**:
+
+```text
+- [ ] Do something 🔁 every day 📅 2023-10-15
+```
+
+- It is a recurring task.
+- It recurs every day.
+- And it is due on `2023-10-15` (today).
+
+### First click - advances to IN_PROGRESS
+
+The **first time we click on the task's checkbox** or run Tasks' **Toggle task done** command, the line becomes:
+
+```text
+- [/] Do something 🔁 every day 📅 2023-10-15
+```
+
+- Notice that `[ ]` has become `[/]` ...
+  - ... because the next status symbol after `space` is `/`.
+- From the settings, we see the status type is now `IN_PROGRESS`.
+
+### Second click - advances to DONE and recurs
+
+The **next time we click on the task's checkbox** or run Tasks' **Toggle task done** command we toggle the task, the line becomes **two lines**:
+
+```text
+- [x] Do something 🔁 every day 📅 2023-10-15 ✅ 2023-10-15
+- [ ] Do something 🔁 every day 📅 2023-10-16
+```
+
+Let's look at that in more detail.
+
+### Breaking down the result
+
+Let's look at the **first of those two lines:**
+
+```text
+- [x] Do something 🔁 every day 📅 2023-10-15 ✅ 2023-10-15
+```
+
+- Notice that `[/]` has become `[x]` ...
+  - ... because the next status symbol after `/` is `x`.
+- From the settings, we see the status type is now `DONE`.
+- So a [[Dates#Done date|Done date]] is added (if enabled in user settings).
+
+Let's look at the **second of those two lines**:
+
+```text
+- [ ] Do something 🔁 every day 📅 2023-10-16
+```
+
+- This new task line was created because:
+  - the original task had a Recurrence rule,
+  - and its status type had advanced to `DONE`.
+- Notice that in the new task line, the status is `[ ]` ...
+  - ... because the next status symbol after `x` is ``.
+- And the Due date has advanced a day.
+
+## When DONE is not followed by TODO
+
+In the following example, `DONE` is followed by `CANCELLED`.
+
+<!-- include: DocsSamplesForStatuses.test.DefaultStatuses_done-toggles-to-cancelled.approved.detailed.mermaid.md -->
+```mermaid
+flowchart LR
+1["'Todo'<br>[ ] -> [/]<br>(TODO)"]
+2["'Done'<br>[x] -> [-]<br>(DONE)"]
+3["'In Progress'<br>[/] -> [x]<br>(IN_PROGRESS)"]
+4["'Cancelled'<br>[-] -> [ ]<br>(CANCELLED)"]
+1 --> 3
+2 --> 4
+3 --> 2
+4 --> 1
+```
+<!-- endInclude -->
+
+This means that new recurrence of any completed recurring tasks will always be `CANCELLED`.
+
+For example, when the following task is toggled:
+
+```text
+- [/] Do something 🔁 every day 📅 2024-10-16
+```
+
+... it will become ...
+
+```text
+- [x] Do something 🔁 every day 📅 2024-10-16 ✅ 2023-10-16
+- [-] Do something 🔁 every day 📅 2024-10-17
+```
+
+Note that the new task has `CANCELLED` status, so will not typically show up in standard Tasks searches.
+
+> [!Tip]
+> Currently, if you plan to use recurring tasks, you should always ensure that any statuses of type `DONE` are always followed by statuses of type `TODO`.
+
+> [!Note]
+> When [[Custom Statuses]] were introduced, and we had to figure out how integrate them with recurring tasks, we expected that users would always following `DONE` with `TODO`.
+>
+> It turns out that (at least) two users have opted to follow `DONE` with `CANCELLED`, with recurring tasks.
+>
+> At some point we may adjust the Recurrence code so that:
+>
+> - instead of just taking the next status after the `DONE` task ...
+> - ... it will proceed through the statuses until it finds a `TODO` task.
+>
+> We are tracking this in [issue #2089](https://github.com/obsidian-tasks-group/obsidian-tasks/issues/2089) and [issue #2304](https://github.com/obsidian-tasks-group/obsidian-tasks/issues/2304).
+
+## When the current status type is NON_TASK
+
+[[Status Types#NON_TASK|NON_TASK]] is a special status type to use for checklists that do not represent tasks.
+
+<!-- include: DocsSamplesForStatuses.test.DefaultStatuses_pro-con-cycle.approved.detailed.mermaid.md -->
+```mermaid
+flowchart LR
+1["'Pro'<br>[P] -> [C]<br>(NON_TASK)"]
+2["'Con'<br>[C] -> [P]<br>(NON_TASK)"]
+1 --> 2
+2 --> 1
+```
+<!-- endInclude -->
+
+With the above custom statuses in Tasks settings, clicking on task lines with checkboxes `[P]` and `[C]` will:
+
+- never add a Done date
+- never create a new instance, even if the task line has a recurrence rule.
diff --git a/docs/Getting Started/Statuses/Status Types.md b/docs/Getting Started/Statuses/Status Types.md
index 6ec65ba9..004575db 100644
--- a/docs/Getting Started/Statuses/Status Types.md
+++ b/docs/Getting Started/Statuses/Status Types.md
@@ -36,6 +36,8 @@ The `DONE` type is used to determine:
 - whether to add or remove the Done date when toggling a task.
 - whether to create new instances of recurring tasks, when they are done.
 
+The page [[Recurring Tasks and Custom Statuses]] has several worked examples to explain the above points fully.
+
 ### CANCELLED
 
 This is typically used for tasks that you originally thought needed to be done, but have since discovered are not needed for some reason.
@@ -48,6 +50,11 @@ Custom checkbox styles are often used for non-task concepts, such as 'Pro' and '
 
 You may wish to assign this type to some of your statuses, to allow you to write filters to exclude them from your task searches.
 
+When you click on checkboxes for `NON_TASK` statuses, the task line will:
+
+- never acquire a Done date
+- never create a new instance, even if the task line has a [[Recurring Tasks|recurrence rule]].
+
 ## Status Types-related instructions
 
 - Status types are searchable with `status.type`, for example `status.type is IN_PROGRESS`.
diff --git a/docs/Introduction.md b/docs/Introduction.md
index 4e1dc799..48a20a5c 100644
--- a/docs/Introduction.md
+++ b/docs/Introduction.md
@@ -16,6 +16,7 @@ _In recent [releases](https://github.com/obsidian-tasks-group/obsidian-tasks/rel
 - X.Y.X: 🔥 Add [[Line Continuations|line continuations]].
   - **Warning**: This is a [[Line Continuations#Appendix Updating pre-X.Y.Z searches with trailing backslashes|potentially breaking change]] if you search for backslash (`\`) characters.
 - X.Y.X: 🔥 Document [[Comments#Inline comments|inline comments]]
+- X.Y.Z: 🔥 Document [[Recurring Tasks and Custom Statuses|recurring tasks and custom statuses]]
 - X.Y.Z: 🔥 Add new Help pages [[Known Limitations]] and [[Breaking Changes]].
 
 <!--
diff --git a/src/StatusRegistry.ts b/src/StatusRegistry.ts
index 041bcf07..e0744d19 100644
--- a/src/StatusRegistry.ts
+++ b/src/StatusRegistry.ts
@@ -293,9 +293,11 @@ export class StatusRegistry {
      * The text can be pasted in to an Obsidian note to visualise the transitions between
      * statuses.
      *
-     * Note: Any of the 'next status symbols' that are not in the registry are ignored, and invisble.
+     * Note: Any of the 'next status symbols' that are not in the registry are ignored, and invisible.
+     *
+     * @param {boolean} includeDetails - whether to include the status symbols and types in the diagram. Defaults to false.
      */
-    public mermaidDiagram() {
+    public mermaidDiagram(includeDetails = false) {
         const uniqueStatuses = this.registeredStatuses;
 
         const language = 'mermaid';
@@ -303,7 +305,15 @@ export class StatusRegistry {
         const nodes: string[] = [];
         const edges: string[] = [];
         uniqueStatuses.forEach((status, index) => {
-            nodes.push(`${index + 1}[${status.name}]`);
+            if (includeDetails) {
+                const transition = `[${status.symbol}] -> [${status.nextStatusSymbol}]`;
+                const statusName = `'${status.name}'`;
+                const statusType = `(${status.type})`;
+                const text = `${index + 1}["${statusName}<br>${transition}<br>${statusType}"]`;
+                nodes.push(text);
+            } else {
+                nodes.push(`${index + 1}[${status.name}]`);
+            }
 
             // Check the next status:
             const nextStatus = this.getNextStatus(status);


Test patch:
diff --git a/tests/DocumentationSamples/DocsSamplesForStatuses.test.DefaultStatuses_done-toggles-to-cancelled-with-unconventional-symbols.approved.detailed.mermaid.md b/tests/DocumentationSamples/DocsSamplesForStatuses.test.DefaultStatuses_done-toggles-to-cancelled-with-unconventional-symbols.approved.detailed.mermaid.md
new file mode 100644
index 00000000..c953c326
--- /dev/null
+++ b/tests/DocumentationSamples/DocsSamplesForStatuses.test.DefaultStatuses_done-toggles-to-cancelled-with-unconventional-symbols.approved.detailed.mermaid.md
@@ -0,0 +1,10 @@
+
+```mermaid
+flowchart LR
+1["'Todo'<br>[ ] -> [*]<br>(TODO)"]
+2["'Done'<br>[*] -> [x]<br>(DONE)"]
+3["'Cancelled'<br>[x] -> [ ]<br>(CANCELLED)"]
+1 --> 2
+2 --> 3
+3 --> 1
+```
diff --git a/tests/DocumentationSamples/DocsSamplesForStatuses.test.DefaultStatuses_done-toggles-to-cancelled.approved.detailed.mermaid.md b/tests/DocumentationSamples/DocsSamplesForStatuses.test.DefaultStatuses_done-toggles-to-cancelled.approved.detailed.mermaid.md
new file mode 100644
index 00000000..22c8810c
--- /dev/null
+++ b/tests/DocumentationSamples/DocsSamplesForStatuses.test.DefaultStatuses_done-toggles-to-cancelled.approved.detailed.mermaid.md
@@ -0,0 +1,12 @@
+
+```mermaid
+flowchart LR
+1["'Todo'<br>[ ] -> [/]<br>(TODO)"]
+2["'Done'<br>[x] -> [-]<br>(DONE)"]
+3["'In Progress'<br>[/] -> [x]<br>(IN_PROGRESS)"]
+4["'Cancelled'<br>[-] -> [ ]<br>(CANCELLED)"]
+1 --> 3
+2 --> 4
+3 --> 2
+4 --> 1
+```
diff --git a/tests/DocumentationSamples/DocsSamplesForStatuses.test.DefaultStatuses_pro-con-cycle.approved.detailed.mermaid.md b/tests/DocumentationSamples/DocsSamplesForStatuses.test.DefaultStatuses_pro-con-cycle.approved.detailed.mermaid.md
new file mode 100644
index 00000000..b084506d
--- /dev/null
+++ b/tests/DocumentationSamples/DocsSamplesForStatuses.test.DefaultStatuses_pro-con-cycle.approved.detailed.mermaid.md
@@ -0,0 +1,8 @@
+
+```mermaid
+flowchart LR
+1["'Pro'<br>[P] -> [C]<br>(NON_TASK)"]
+2["'Con'<br>[C] -> [P]<br>(NON_TASK)"]
+1 --> 2
+2 --> 1
+```
diff --git a/tests/DocumentationSamples/DocsSamplesForStatuses.test.DefaultStatuses_todo-in_progress-done.approved.detailed.mermaid.md b/tests/DocumentationSamples/DocsSamplesForStatuses.test.DefaultStatuses_todo-in_progress-done.approved.detailed.mermaid.md
new file mode 100644
index 00000000..ea64712d
--- /dev/null
+++ b/tests/DocumentationSamples/DocsSamplesForStatuses.test.DefaultStatuses_todo-in_progress-done.approved.detailed.mermaid.md
@@ -0,0 +1,10 @@
+
+```mermaid
+flowchart LR
+1["'Todo'<br>[ ] -> [/]<br>(TODO)"]
+2["'In Progress'<br>[/] -> [x]<br>(IN_PROGRESS)"]
+3["'Done'<br>[x] -> [ ]<br>(DONE)"]
+1 --> 2
+2 --> 3
+3 --> 1
+```
diff --git a/tests/DocumentationSamples/DocsSamplesForStatuses.test.ts b/tests/DocumentationSamples/DocsSamplesForStatuses.test.ts
index ac2c75e9..5f72852f 100644
--- a/tests/DocumentationSamples/DocsSamplesForStatuses.test.ts
+++ b/tests/DocumentationSamples/DocsSamplesForStatuses.test.ts
@@ -77,13 +77,21 @@ function verifyStatusesInMultipleFormats(statuses: Status[], showQueryInstructio
     verifyStatusesAsMermaidDiagram(statuses);
 }
 
-function verifyStatusesAsMermaidDiagram(statuses: Status[]) {
+function verifyStatusesAsMermaidDiagramImpl(statuses: Status[], detailed: boolean, extensionWithoutDot: string) {
     // Set the registry up to exactly match the supplied statuses
     const registry = new StatusRegistry();
     registry.set(statuses);
 
-    const markdown = registry.mermaidDiagram();
-    verifyWithFileExtension(markdown, 'mermaid.md');
+    const markdown = registry.mermaidDiagram(detailed);
+    verifyWithFileExtension(markdown, extensionWithoutDot);
+}
+
+function verifyStatusesAsMermaidDiagram(statuses: Status[]) {
+    verifyStatusesAsMermaidDiagramImpl(statuses, false, 'mermaid.md');
+}
+
+function verifyStatusesAsDetailedMermaidDiagram(statuses: Status[]) {
+    verifyStatusesAsMermaidDiagramImpl(statuses, true, 'detailed.mermaid.md');
 }
 
 describe('DefaultStatuses', () => {
@@ -113,6 +121,7 @@ describe('DefaultStatuses', () => {
             ['x', 'Done', ' ', 'DONE'],
         ];
         verifyStatusesInMultipleFormats(constructStatuses(importantCycle), false);
+        verifyStatusesAsDetailedMermaidDiagram(constructStatuses(importantCycle));
     });
 
     it('pro-con-cycle', () => {
@@ -121,6 +130,7 @@ describe('DefaultStatuses', () => {
             ['C', 'Con', 'P', 'NON_TASK'],
         ];
         verifyStatusesInMultipleFormats(constructStatuses(importantCycle), false);
+        verifyStatusesAsDetailedMermaidDiagram(constructStatuses(importantCycle));
     });
 
     it('toggle-does-nothing', () => {
@@ -133,6 +143,31 @@ describe('DefaultStatuses', () => {
         ];
         verifyStatusesInMultipleFormats(constructStatuses(importantCycle), false);
     });
+
+    it('done-toggles-to-cancelled', () => {
+        // See issue #2089.
+        // DONE is followed by CANCELLED, which currently causes unexpected behaviour in recurrent tasks.
+        // This uses the 4 default statuses, and just customises their order.
+        const statuses: StatusCollection = [
+            [' ', 'Todo', '/', 'TODO'],
+            ['x', 'Done', '-', 'DONE'],
+            ['/', 'In Progress', 'x', 'IN_PROGRESS'],
+            ['-', 'Cancelled', ' ', 'CANCELLED'],
+        ];
+        verifyStatusesAsDetailedMermaidDiagram(constructStatuses(statuses));
+    });
+
+    it('done-toggles-to-cancelled-with-unconventional-symbols', () => {
+        // See issue #2304.
+        // DONE is followed by CANCELLED, which currently causes unexpected behaviour in recurrent tasks.
+        // This doesn't follow the standard convention of 'x' means DONE. It has 'x' means CANCELLED.
+        const statuses: StatusCollection = [
+            [' ', 'Todo', '*', 'TODO'],
+            ['*', 'Done', 'x', 'DONE'],
+            ['x', 'Cancelled', ' ', 'CANCELLED'],
+        ];
+        verifyStatusesAsDetailedMermaidDiagram(constructStatuses(statuses));
+    });
 });
 
 describe('Theme', () => {
diff --git a/tests/StatusRegistry.test.ts b/tests/StatusRegistry.test.ts
index 9c4f72cf..27144163 100644
--- a/tests/StatusRegistry.test.ts
+++ b/tests/StatusRegistry.test.ts
@@ -167,11 +167,9 @@ describe('StatusRegistry', () => {
         // Arrange
         const statusRegistry = new StatusRegistry();
 
-        // Act
-        const mermaidText = statusRegistry.mermaidDiagram();
-
         // Assert
-        expect(mermaidText).toMatchInlineSnapshot(`
+        // Without detail:
+        expect(statusRegistry.mermaidDiagram(false)).toMatchInlineSnapshot(`
             "
             \`\`\`mermaid
             flowchart LR
@@ -186,6 +184,23 @@ describe('StatusRegistry', () => {
             \`\`\`
             "
         `);
+
+        // With detail:
+        expect(statusRegistry.mermaidDiagram(true)).toMatchInlineSnapshot(`
+            "
+            \`\`\`mermaid
+            flowchart LR
+            1["'Todo'<br>[ ] -> [x]<br>(TODO)"]
+            2["'In Progress'<br>[/] -> [x]<br>(IN_PROGRESS)"]
+            3["'Done'<br>[x] -> [ ]<br>(DONE)"]
+            4["'Cancelled'<br>[-] -> [ ]<br>(CANCELLED)"]
+            1 --> 3
+            2 --> 3
+            3 --> 1
+            4 --> 1
+            \`\`\`
+            "
+        `);
     });
 
     it('should not include unknown nextStatusSymbols in mermaid diagrams', () => {