Repository: obsidian-tasks-group/obsidian-tasks
Pull request #1451 (2023-01-01 14:40:47)

refactor: Add StatusConfiguration & Status from obsidian-tasks-x
<!--- Provide a general summary of your changes in the Title above -->

# Description

1. Add `StatusConfiguration` & `Status` and their tests from [obsidian-tasks-x](https://github.com/sytone/obsidian-tasks-x).

Behaviour is unchanged.

Credit: This is a copy of the code and tests in @sytone's fork https://github.com/sytone/obsidian-tasks-x on its `main-tasks-sql` branch.

Note: Initially only `Status.DONE` and `Status.TODO` have been copied over, to maintain parity with the existing behaviour in Tasks and keep this as a pure refactoring.

The revision copied from is:
https://github.com/sytone/obsidian-tasks-x/commit/2c0b659457cc80806ff18585c955496c76861b87

Co-Authored-By: Sytone <1399443+sytone@users.noreply.github.com>

2. Added `resources/sample_vaults/Tasks-Demo/Manual Testing/Custom Task Statuses.md`.

## Motivation and Context

This is preparation for adding support for more task status types - making the changes in small, testable steps.

## How has this been tested?

- By running existing and new tests
- By exploratory testing in the Tasks-Demo vault.

## 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:

- [x] **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))

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`.
- [ ] My change requires a change to the documentation.
- [ ] I have [updated the documentation](https://github.com/obsidian-tasks-group/obsidian-tasks/blob/main/CONTRIBUTING.md#updating-documentation) accordingly.
- [x] My change has adequate [Unit Test coverage](https://github.com/obsidian-tasks-group/obsidian-tasks/blob/main/CONTRIBUTING.md#maintaining-the-tests).

## Terms

<!--
By submitting this pull request, you must agree to follow our
[contributing guide](https://github.com/obsidian-tasks-group/obsidian-tasks/blob/main/CONTRIBUTING.md) 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://github.com/obsidian-tasks-group/obsidian-tasks/blob/main/CONTRIBUTING.md)
- [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/Query/Filter/StatusField.ts b/src/Query/Filter/StatusField.ts
index e55549d5..894e1200 100644
--- a/src/Query/Filter/StatusField.ts
+++ b/src/Query/Filter/StatusField.ts
@@ -24,9 +24,9 @@ export class StatusField extends FilterInstructionsBasedField {
      */
     public comparator(): Comparator {
         return (a: Task, b: Task) => {
-            if (a.status < b.status) {
+            if (a.status.name < b.status.name) {
                 return 1;
-            } else if (a.status > b.status) {
+            } else if (a.status.name > b.status.name) {
                 return -1;
             } else {
                 return 0;
diff --git a/src/Query/Group.ts b/src/Query/Group.ts
index 0001dae1..46ce4f16 100644
--- a/src/Query/Group.ts
+++ b/src/Query/Group.ts
@@ -188,7 +188,7 @@ export class Group {
     }
 
     private static groupByStatus(task: Task): string[] {
-        return [task.status];
+        return [task.status.name];
     }
 
     private static groupByHeading(task: Task): string[] {
diff --git a/src/Status.ts b/src/Status.ts
index 30412e9d..aabd2479 100644
--- a/src/Status.ts
+++ b/src/Status.ts
@@ -1,10 +1,153 @@
 /**
- * Collection of status types supported by the plugin.
- * TODO: Make this a class so it can support other types and easier mapping to status character.
+ * This is the object stored by the Obsidian configuration and used to create the status
+ * objects for the session
+ *
  * @export
- * @enum {number}
+ * @class StatusConfiguration
  */
-export enum Status {
-    TODO = 'Todo',
-    DONE = 'Done',
+export class StatusConfiguration {
+    /**
+     * The indicator used between the two square brackets in the markdown task.
+     *
+     * @type {string}
+     * @memberof Status
+     */
+    public readonly indicator: string;
+
+    /**
+     * Returns the name of the status for display purposes.
+     *
+     * @type {string}
+     * @memberof Status
+     */
+    public readonly name: string;
+
+    /**
+     * Returns the next status for a task when toggled.
+     *
+     * @type {string}
+     * @memberof Status
+     */
+    public readonly nextStatusIndicator: string;
+
+    /**
+     * If true then it is registered as a command that the user can map to.
+     *
+     * @type {boolean}
+     * @memberof Status
+     */
+    public readonly availableAsCommand: boolean;
+
+    /**
+     * Creates an instance of Status. The registry will be added later in the case
+     * of the default statuses.
+     *
+     * @param {string} indicator
+     * @param {string} name
+     * @param {Status} nextStatusIndicator
+     * @param {bool} availableAsCommand
+     * @memberof Status
+     */
+    constructor(indicator: string, name: string, nextStatusIndicator: string, availableAsCommand: boolean) {
+        this.indicator = indicator;
+        this.name = name;
+        this.nextStatusIndicator = nextStatusIndicator;
+        this.availableAsCommand = availableAsCommand;
+    }
+}
+
+/**
+ * Tracks the possible states that a task can be in.
+ *
+ * @export
+ * @class Status
+ */
+export class Status {
+    /**
+     * The default Done status. Goes to Todo when toggled.
+     *
+     * @static
+     * @type {Status}
+     * @memberof Status
+     */
+    public static DONE: Status = new Status(new StatusConfiguration('x', 'Done', ' ', true));
+
+    /**
+     * The default Todo status. Goes to In Progress when toggled.
+     *
+     * @static
+     * @type {Status}
+     * @memberof Status
+     */
+    public static TODO: Status = new Status(new StatusConfiguration(' ', 'Todo', '/', true));
+
+    /**
+     * The configuration stored in the data.json file.
+     *
+     * @type {StatusConfiguration}
+     * @memberof Status
+     */
+    private readonly configuration: StatusConfiguration;
+
+    /**
+     * The indicator used between the two square brackets in the markdown task.
+     *
+     * @type {string}
+     * @memberof Status
+     */
+    public get indicator(): string {
+        return this.configuration.indicator;
+    }
+
+    /**
+     * Returns the name of the status for display purposes.
+     *
+     * @type {string}
+     * @memberof Status
+     */
+    public get name(): string {
+        return this.configuration.name;
+    }
+
+    /**
+     * Returns the next status for a task when toggled.
+     *
+     * @type {string}
+     * @memberof Status
+     */
+    public get nextStatusIndicator(): string {
+        return this.configuration.nextStatusIndicator;
+    }
+
+    /**
+     * If true then it is registered as a command that the user can map to.
+     *
+     * @type {boolean}
+     * @memberof Status
+     */
+    public get availableAsCommand(): boolean {
+        return this.configuration.availableAsCommand;
+    }
+
+    /**
+     * Creates an instance of Status. The registry will be added later in the case
+     * of the default statuses.
+     *
+     * @param {StatusConfiguration} configuration
+     * @memberof Status
+     */
+    constructor(configuration: StatusConfiguration) {
+        this.configuration = configuration;
+    }
+
+    /**
+     * Returns the completion status for a task, this is only supported
+     * when the task is done/x.
+     *
+     * @return {*}  {boolean}
+     * @memberof Status
+     */
+    public isCompleted(): boolean {
+        return this.indicator === 'x' || this.indicator === 'X';
+    }
 }


Test patch:
diff --git a/resources/sample_vaults/Tasks-Demo/Manual Testing/Custom Task Statuses.md b/resources/sample_vaults/Tasks-Demo/Manual Testing/Custom Task Statuses.md
new file mode 100644
index 00000000..3fdb56bc
--- /dev/null
+++ b/resources/sample_vaults/Tasks-Demo/Manual Testing/Custom Task Statuses.md
@@ -0,0 +1,12 @@
+# Custom Task Statuses
+
+- [ ] #task Task 1
+- [-] #task Task 2
+- [x] #task Task 3
+
+## group by status
+
+```tasks
+filename includes Custom Task Statuses
+group by status
+```
diff --git a/tests/Status.test.ts b/tests/Status.test.ts
new file mode 100644
index 00000000..260c4023
--- /dev/null
+++ b/tests/Status.test.ts
@@ -0,0 +1,44 @@
+/**
+ * @jest-environment jsdom
+ */
+import moment from 'moment';
+import { Status, StatusConfiguration } from '../src/Status';
+
+jest.mock('obsidian');
+window.moment = moment;
+
+describe('Status', () => {
+    it('should initialize with valid properties', () => {
+        // Arrange
+        const indicator = '/';
+        const name = 'In Progress';
+        const next = 'x';
+
+        // Act
+        const status = new Status(new StatusConfiguration(indicator, name, next, false));
+
+        // Assert
+        expect(status).not.toBeNull();
+        expect(status!.indicator).toEqual(indicator);
+        expect(status!.name).toEqual(name);
+        expect(status!.nextStatusIndicator).toEqual(next);
+        expect(status!.isCompleted()).toEqual(false);
+    });
+
+    it('should be complete when indicator is "x"', () => {
+        // Arrange
+        const indicator = 'x';
+        const name = 'Done';
+        const next = ' ';
+
+        // Act
+        const status = new Status(new StatusConfiguration(indicator, name, next, false));
+
+        // Assert
+        expect(status).not.toBeNull();
+        expect(status!.indicator).toEqual(indicator);
+        expect(status!.name).toEqual(name);
+        expect(status!.nextStatusIndicator).toEqual(next);
+        expect(status!.isCompleted()).toEqual(true);
+    });
+});