Repository: obsidian-tasks-group/obsidian-tasks
Pull request #2528 (2023-12-23 12:57:41)

feat: Show non-standard recurrence transitions in Mermaid diagrams
<!--- Provide a general summary of your changes in the Title above -->

# Description

Add an extra arrow to the Mermaid status diagrams, if Tasks overrides the status of a new recurrence of a recurring task to force the status type to be `TODO` or `IN_PROGRESS`.

## Motivation and Context

Improve the usefulness of the "Check your Statuses" report.

## 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. -->

- Reviewing the updated approval-test output mermaid diagrams in Obsidian
- Exploratory testing: reviewing the "Check your Statuses" in a vault that has statuses where recurrence status now gets overridden...

## Screenshots (if appropriate)

### Before:


```mermaid
flowchart LR

classDef TODO        stroke:#f33,stroke-width:3px;
classDef DONE        stroke:#0c0,stroke-width:3px;
classDef IN_PROGRESS stroke:#fa0,stroke-width:3px;
classDef CANCELLED   stroke:#ddd,stroke-width:3px;
classDef NON_TASK    stroke:#99e,stroke-width:3px;

1["'Todo'<br>[ ] -> [/]<br>(TODO)"]:::TODO
2["'Done'<br>[x] -> [-]<br>(DONE)"]:::DONE
3["'In Progress'<br>[/] -> [x]<br>(IN_PROGRESS)"]:::IN_PROGRESS
4["'Cancelled'<br>[-] -> [ ]<br>(CANCELLED)"]:::CANCELLED
1 --> 3
2 --> 4
3 --> 2
4 --> 1

linkStyle default stroke:gray
```


### After


```mermaid
flowchart LR

classDef TODO        stroke:#f33,stroke-width:3px;
classDef DONE        stroke:#0c0,stroke-width:3px;
classDef IN_PROGRESS stroke:#fa0,stroke-width:3px;
classDef CANCELLED   stroke:#ddd,stroke-width:3px;
classDef NON_TASK    stroke:#99e,stroke-width:3px;

1["'Todo'<br>[ ] -> [/]<br>(TODO)"]:::TODO
2["'Done'<br>[x] -> [-]<br>(DONE)"]:::DONE
3["'In Progress'<br>[/] -> [x]<br>(IN_PROGRESS)"]:::IN_PROGRESS
4["'Cancelled'<br>[-] -> [ ]<br>(CANCELLED)"]:::CANCELLED
1 --> 3
2 --> 4
2-. "🔁" .-> 1
3 --> 2
4 --> 1

linkStyle default stroke:gray
```


## 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)

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/StatusRegistry.ts b/src/StatusRegistry.ts
index 340fcee6..be9a575e 100644
--- a/src/StatusRegistry.ts
+++ b/src/StatusRegistry.ts
@@ -363,12 +363,16 @@ export class StatusRegistry {
 
             // Check the next status:
             const nextStatus = this.getNextStatus(status);
-            const nextStatusIndex = uniqueStatuses.findIndex((status) => status.symbol === nextStatus.symbol);
-            const nextStatusIsKnown = nextStatusIndex !== -1;
-            const nextStatusIsNotInternal = nextStatus.type !== StatusType.EMPTY;
-
-            if (nextStatusIsKnown && nextStatusIsNotInternal) {
-                edges.push(`${index + 1} --> ${nextStatusIndex + 1}`);
+            this.addEdgeIfNotToInternal(uniqueStatuses, nextStatus, edges, index, false);
+
+            // For recurring tasks, if Tasks would override the next status after a DONE task,
+            // to force it to be TODO or IN_PROGRESS, then we want to show this visually.
+            if (status.type === StatusType.DONE) {
+                const nextRecurringStatus = this.getNextRecurrenceStatusOrCreate(status);
+                const nextRecurringTypeDiffers = nextRecurringStatus.symbol !== nextStatus.symbol;
+                if (nextRecurringTypeDiffers) {
+                    this.addEdgeIfNotToInternal(uniqueStatuses, nextRecurringStatus, edges, index, true);
+                }
             }
         });
 
@@ -390,6 +394,29 @@ linkStyle default stroke:gray
 `;
     }
 
+    private addEdgeIfNotToInternal(
+        uniqueStatuses: Status[],
+        nextStatus: Status,
+        edges: string[],
+        index: number,
+        isForReccurenceOverride: boolean,
+    ) {
+        const nextStatusIndex = uniqueStatuses.findIndex((status) => status.symbol === nextStatus.symbol);
+        const nextStatusIsKnown = nextStatusIndex !== -1;
+        const nextStatusIsNotInternal = nextStatus.type !== StatusType.EMPTY;
+
+        if (nextStatusIsKnown && nextStatusIsNotInternal) {
+            let joiner;
+            if (isForReccurenceOverride) {
+                joiner = '-. "🔁" .-> ';
+            } else {
+                joiner = ' --> ';
+            }
+            const line = `${index + 1}${joiner}${nextStatusIndex + 1}`;
+            edges.push(line);
+        }
+    }
+
     private getMermaidNodeLabel(status: Status, includeDetails: boolean) {
         const statusName = htmlEncodeString(status.name);
         const statusType = status.type;


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
index 8540aca5..6d1d71d5 100644
--- 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
@@ -13,6 +13,7 @@ classDef NON_TASK    stroke:#99e,stroke-width:3px;
 3["'Cancelled'<br>[x] -> [ ]<br>(CANCELLED)"]:::CANCELLED
 1 --> 2
 2 --> 3
+2-. "🔁" .-> 1
 3 --> 1
 
 linkStyle default stroke:gray
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
index 9e328437..e018e0ff 100644
--- 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
@@ -14,6 +14,7 @@ classDef NON_TASK    stroke:#99e,stroke-width:3px;
 4["'Cancelled'<br>[-] -> [ ]<br>(CANCELLED)"]:::CANCELLED
 1 --> 3
 2 --> 4
+2-. "🔁" .-> 1
 3 --> 2
 4 --> 1