Repository: obsidian-tasks-group/obsidian-tasks
Pull request #2045 (2023-06-15 12:19:55)

feat: 'group by status.type' names are now more readable
# Description

Remove numbers in StatusType group names: `2 TODO` -> `TODO` by including the number in comments. This way the sort order of the StatusType is not changed.


| Old name        | New name      |
| --------------- | ------------- |
| `1 IN_PROGRESS` | `IN_PROGRESS` |
| `2 TODO`        | `TODO`        |
| `3 DONE`        | `DONE`        |
| `4 CANCELLED`   | `CANCELLED`   |
| `5 NON_TASK`    | `NON_TASK`    |
| `6 EMPTY`       | `EMPTY`       |

## Motivation and Context

Provide shorter group names with only meaningful data.

## How has this been tested?

Unit testing (with fixes).

## 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] **Bug fix** (prefix: `fix` - non-breaking change which fixes an issue)
- [ ] **Breaking change** (prefix: `feat!!` or `fix!!` - fix or feature that would cause existing functionality to not work as expected)

## 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)


ilandikov: If my understanding is correct, the docs  in GettingStarted/Statuses/Status Types shall update automatically thanks to change in `tests/DocumentationSamples/DocsSamplesForStatuses.test.Status_Transitions_status-types.approved.md`. So I changed only the latter file.

Patch:
diff --git a/docs/Queries/Grouping.md b/docs/Queries/Grouping.md
index cd9fa7d7..b2fc9099 100644
--- a/docs/Queries/Grouping.md
+++ b/docs/Queries/Grouping.md
@@ -97,11 +97,11 @@ Since Tasks X.Y.Z, **[[Custom Grouping|custom grouping]] by status names** is no
 - `group by status.type`
   - This groups by the types you have given to your custom statuses.
   - In order to impose a useful sort order, the types are prefixed with a number, so the groups will appear in this order, and with these group names:
-    - `1 IN_PROGRESS`
-    - `2 TODO`
-    - `3 DONE`
-    - `4 CANCELLED`
-    - `5 NON_TASK`
+    - `IN_PROGRESS`
+    - `TODO`
+    - `DONE`
+    - `CANCELLED`
+    - `NON_TASK`
 
 > [!released]
 `group by status.type` was introduced in Tasks 1.23.0.
@@ -329,12 +329,12 @@ Since Tasks X.Y.Z, **[[Custom Grouping|custom grouping]] by description** is now
 
 - `group by priority`
   - The priority of the task, namely one of:
-    - `Priority 0: Highest`
-    - `Priority 1: High`
-    - `Priority 2: Medium`
-    - `Priority 3: None`
-    - `Priority 4: Low`
-    - `Priority 5: Lowest`
+    - `Highest priority`
+    - `High priority`
+    - `Medium priority`
+    - `Normal priority`
+    - `Low priority`
+    - `Lowest priority`
 
 > [!released]
 >
@@ -369,9 +369,8 @@ Using the priority number:
 ### Urgency
 
 - `group by urgency` ([[Urgency|urgency]])
-  - Currently, the groups run from the lowest urgency to highest.
+  - The groups run from the highest urgency to the lowest.
   - You can reverse this with `group by urgency reverse`.
-  - In a future release, the default group order will become from the highest urgency to lowest.
 
 > [!released]
 >
diff --git a/src/Config/GlobalFilter.ts b/src/Config/GlobalFilter.ts
index 593a1786..9158235a 100644
--- a/src/Config/GlobalFilter.ts
+++ b/src/Config/GlobalFilter.ts
@@ -34,6 +34,15 @@ export class GlobalFilter {
         return GlobalFilter.get() + ' ' + description;
     }
 
+    static removeAsWordFromDependingOnSettings(description: string): string {
+        const { removeGlobalFilter } = getSettings();
+        if (removeGlobalFilter) {
+            return GlobalFilter.removeAsWordFrom(description);
+        }
+
+        return description;
+    }
+
     /**
      * Search for the global filter for the purpose of removing it from the description, but do so only
      * if it is a separate word (preceding the beginning of line or a space and followed by the end of line
@@ -59,12 +68,4 @@ export class GlobalFilter {
         const globalFilter = GlobalFilter.get();
         return description.replace(globalFilter, '').trim();
     }
-
-    static removeAsSubstringFromDependingOnSettings(description: string): string {
-        const { removeGlobalFilter } = getSettings();
-        if (removeGlobalFilter) {
-            return GlobalFilter.removeAsSubstringFrom(description);
-        }
-        return description;
-    }
 }
diff --git a/src/Query/Filter/PriorityField.ts b/src/Query/Filter/PriorityField.ts
index d0215619..ada0abcd 100644
--- a/src/Query/Filter/PriorityField.ts
+++ b/src/Query/Filter/PriorityField.ts
@@ -95,8 +95,8 @@ export class PriorityField extends Field {
 
     public grouper(): GrouperFunction {
         return (task: Task) => {
-            const priorityName = PriorityTools.priorityNameUsingNone(task.priority);
-            return [`Priority ${task.priority}: ${priorityName}`];
+            const priorityName = PriorityTools.priorityNameUsingNormal(task.priority);
+            return [`%%${task.priority}%%${priorityName} priority`];
         };
     }
 }
diff --git a/src/Query/Filter/StatusTypeField.ts b/src/Query/Filter/StatusTypeField.ts
index 0aa9d316..306a6d48 100644
--- a/src/Query/Filter/StatusTypeField.ts
+++ b/src/Query/Filter/StatusTypeField.ts
@@ -134,6 +134,6 @@ export class StatusTypeField extends Field {
                 prefix = '6';
                 break;
         }
-        return prefix + ' ' + task.status.type;
+        return `%%${prefix}%%${task.status.type}`;
     }
 }
diff --git a/src/Query/Filter/UrgencyField.ts b/src/Query/Filter/UrgencyField.ts
index b60d3ea1..e4ef6e09 100644
--- a/src/Query/Filter/UrgencyField.ts
+++ b/src/Query/Filter/UrgencyField.ts
@@ -1,6 +1,6 @@
 import type { Comparator } from '../Sorter';
 import type { Task } from '../../Task';
-import type { GrouperFunction } from '../Grouper';
+import type { Grouper, GrouperFunction } from '../Grouper';
 import { Field } from './Field';
 import { FilterOrErrorMessage } from './Filter';
 
@@ -58,4 +58,16 @@ export class UrgencyField extends Field {
             return [`${task.urgency.toFixed(2)}`];
         };
     }
+
+    /**
+     * The {@link Field.createGrouper} creates a grouper that sorts by increasing values.
+     * For {@link UrgencyField} the group sorting shall be done by decreasing values, so
+     * the normal order here is the reverse of the regular one.
+     *
+     * @param reverse - false for normal group order (from most urgent to less urgent),
+     * true for reverse group order.
+     */
+    public createGrouper(reverse: boolean): Grouper {
+        return super.createGrouper(!reverse);
+    }
 }
diff --git a/src/TaskLineRenderer.ts b/src/TaskLineRenderer.ts
index 55b20865..561d2c3c 100644
--- a/src/TaskLineRenderer.ts
+++ b/src/TaskLineRenderer.ts
@@ -138,7 +138,7 @@ async function taskToHtml(
         let componentString = emojiSerializer.componentToString(task, taskLayout, component);
         if (componentString) {
             if (component === 'description') {
-                componentString = GlobalFilter.removeAsSubstringFromDependingOnSettings(componentString);
+                componentString = GlobalFilter.removeAsWordFromDependingOnSettings(componentString);
             }
             // Create the text span that will hold the rendered component
             const span = document.createElement('span');


Test patch:
diff --git a/tests/DocumentationSamples/DocsSamplesForStatuses.test.Status_Transitions_status-types.approved.md b/tests/DocumentationSamples/DocsSamplesForStatuses.test.Status_Transitions_status-types.approved.md
index 8aa9f2d8..d0b80318 100644
--- a/tests/DocumentationSamples/DocsSamplesForStatuses.test.Status_Transitions_status-types.approved.md
+++ b/tests/DocumentationSamples/DocsSamplesForStatuses.test.Status_Transitions_status-types.approved.md
@@ -15,7 +15,7 @@
 | Matches `status.name includes done` | no | no | YES | no | no |
 | Matches `status.name includes cancelled` | no | no | no | YES | no |
 | Name for `group by status` | Todo | Done | Done | Done | Done |
-| Name for `group by status.type` | 2 TODO | 1 IN_PROGRESS | 3 DONE | 4 CANCELLED | 5 NON_TASK |
+| Name for `group by status.type` | %%2%%TODO | %%1%%IN_PROGRESS | %%3%%DONE | %%4%%CANCELLED | %%5%%NON_TASK |
 | Name for `group by status.name` | Todo | In Progress | Done | Cancelled | My custom status |
 
 
diff --git a/tests/GlobalFilter.test.ts b/tests/GlobalFilter.test.ts
index 1a0432ea..5b8838dd 100644
--- a/tests/GlobalFilter.test.ts
+++ b/tests/GlobalFilter.test.ts
@@ -1,5 +1,5 @@
-import { GlobalFilter } from '../src/Config/GlobalFilter';
 import { resetSettings, updateSettings } from '../src/Config/Settings';
+import { GlobalFilter } from '../src/Config/GlobalFilter';
 
 describe('Global Filter tests', () => {
     afterEach(() => {
@@ -105,7 +105,7 @@ describe('Global Filter tests with Remove Global Filter Setting', () => {
         updateSettings({ removeGlobalFilter: false });
 
         // Assert
-        expect(GlobalFilter.removeAsSubstringFromDependingOnSettings('This is absolutely todo')).toEqual(
+        expect(GlobalFilter.removeAsWordFromDependingOnSettings('This is absolutely todo')).toEqual(
             'This is absolutely todo',
         );
     });
@@ -116,10 +116,30 @@ describe('Global Filter tests with Remove Global Filter Setting', () => {
         updateSettings({ removeGlobalFilter: true });
 
         // Assert
-        expect(GlobalFilter.removeAsSubstringFromDependingOnSettings('This is absolutely todo')).toEqual(
+        expect(GlobalFilter.removeAsWordFromDependingOnSettings('This is absolutely todo')).toEqual(
             'This is absolutely',
         );
     });
+
+    it.each([
+        // Global Filter is a tag
+        ['#task', '#task/stage at the beginning'],
+        ['#task', 'in the #task/stage middle'],
+        ['#task', 'at the end #task/stage'],
+
+        // Global Filter is not a tag
+        ['TODO', 'TODO/maybe at the beginning'],
+        ['TODO', 'in the TODO/maybe middle'],
+        ['TODO', 'at the end TODO/maybe'],
+    ])(
+        'should not remove Global Filter (%s) if it is in a substring for a task "- [ ] %s"',
+        async (globalFilter: string, description: string) => {
+            updateSettings({ removeGlobalFilter: true });
+            GlobalFilter.set(globalFilter);
+
+            expect(GlobalFilter.removeAsWordFrom(description)).toEqual(description);
+        },
+    );
 });
 
 describe('check removal of the global filter', () => {
diff --git a/tests/Query/Filter/PriorityField.test.ts b/tests/Query/Filter/PriorityField.test.ts
index 233affbb..80fffb26 100644
--- a/tests/Query/Filter/PriorityField.test.ts
+++ b/tests/Query/Filter/PriorityField.test.ts
@@ -200,12 +200,12 @@ describe('grouping by priority', () => {
     });
 
     it.each([
-        ['- [ ] a 🔺', ['Priority 0: Highest']],
-        ['- [ ] a ⏫', ['Priority 1: High']],
-        ['- [ ] a 🔼', ['Priority 2: Medium']],
-        ['- [ ] a', ['Priority 3: None']],
-        ['- [ ] a 🔽', ['Priority 4: Low']],
-        ['- [ ] a ⏬', ['Priority 5: Lowest']],
+        ['- [ ] a 🔺', ['%%0%%Highest priority']],
+        ['- [ ] a ⏫', ['%%1%%High priority']],
+        ['- [ ] a 🔼', ['%%2%%Medium priority']],
+        ['- [ ] a', ['%%3%%Normal priority']],
+        ['- [ ] a 🔽', ['%%4%%Low priority']],
+        ['- [ ] a ⏬', ['%%5%%Lowest priority']],
     ])('task "%s" should have groups: %s', (taskLine: string, groups: string[]) => {
         // Arrange
         const grouper = new PriorityField().createNormalGrouper().grouper;
@@ -219,12 +219,12 @@ describe('grouping by priority', () => {
         const tasks = SampleTasks.withAllPriorities();
 
         expect({ grouper, tasks }).groupHeadingsToBe([
-            'Priority 0: Highest',
-            'Priority 1: High',
-            'Priority 2: Medium',
-            'Priority 3: None',
-            'Priority 4: Low',
-            'Priority 5: Lowest',
+            '%%0%%Highest priority',
+            '%%1%%High priority',
+            '%%2%%Medium priority',
+            '%%3%%Normal priority',
+            '%%4%%Low priority',
+            '%%5%%Lowest priority',
         ]);
     });
 });
diff --git a/tests/Query/Filter/StatusTypeField.test.ts b/tests/Query/Filter/StatusTypeField.test.ts
index 4804c166..ba63b729 100644
--- a/tests/Query/Filter/StatusTypeField.test.ts
+++ b/tests/Query/Filter/StatusTypeField.test.ts
@@ -152,13 +152,13 @@ describe('grouping by status.type', () => {
         const grouper = new StatusTypeField().createNormalGrouper();
 
         // // Assert
-        expect(grouper.grouper(inprTask)).toEqual(['1 IN_PROGRESS']);
-        expect(grouper.grouper(todoTask)).toEqual(['2 TODO']);
-        expect(grouper.grouper(unknTask)).toEqual(['2 TODO']);
-        expect(grouper.grouper(doneTask)).toEqual(['3 DONE']);
-        expect(grouper.grouper(cancTask)).toEqual(['4 CANCELLED']);
-        expect(grouper.grouper(non_Task)).toEqual(['5 NON_TASK']);
-        expect(grouper.grouper(emptTask)).toEqual(['6 EMPTY']); // won't be seen by users
+        expect(grouper.grouper(inprTask)).toEqual(['%%1%%IN_PROGRESS']);
+        expect(grouper.grouper(todoTask)).toEqual(['%%2%%TODO']);
+        expect(grouper.grouper(unknTask)).toEqual(['%%2%%TODO']);
+        expect(grouper.grouper(doneTask)).toEqual(['%%3%%DONE']);
+        expect(grouper.grouper(cancTask)).toEqual(['%%4%%CANCELLED']);
+        expect(grouper.grouper(non_Task)).toEqual(['%%5%%NON_TASK']);
+        expect(grouper.grouper(emptTask)).toEqual(['%%6%%EMPTY']); // won't be seen by users
     });
 
     it('should sort groups for StatusTypeField', () => {
@@ -166,12 +166,12 @@ describe('grouping by status.type', () => {
         const tasks = SampleTasks.withAllStatusTypes();
 
         expect({ grouper, tasks }).groupHeadingsToBe([
-            '1 IN_PROGRESS',
-            '2 TODO',
-            '3 DONE',
-            '4 CANCELLED',
-            '5 NON_TASK',
-            '6 EMPTY',
+            '%%1%%IN_PROGRESS',
+            '%%2%%TODO',
+            '%%3%%DONE',
+            '%%4%%CANCELLED',
+            '%%5%%NON_TASK',
+            '%%6%%EMPTY',
         ]);
     });
 });
diff --git a/tests/Query/Filter/UrgencyField.test.ts b/tests/Query/Filter/UrgencyField.test.ts
index aa2dc751..2a05cd7f 100644
--- a/tests/Query/Filter/UrgencyField.test.ts
+++ b/tests/Query/Filter/UrgencyField.test.ts
@@ -98,7 +98,7 @@ describe('grouping by urgency', () => {
     });
 
     it('should sort groups for UrgencyField', () => {
-        const grouper = new UrgencyField().createNormalGrouper();
+        const grouper = new UrgencyField().createReverseGrouper();
         const taskLines = ['- [ ] a ⏫', '- [ ] a 🔼', '- [ ] a', '- [ ] a 🔽'];
         const tasks = taskLines.map((taskLine) => fromLine({ line: taskLine }));
 
diff --git a/tests/TaskGroups.test.ts b/tests/TaskGroups.test.ts
index 5202b2f8..b23f725e 100644
--- a/tests/TaskGroups.test.ts
+++ b/tests/TaskGroups.test.ts
@@ -361,20 +361,20 @@ describe('Grouping tasks', () => {
             - status.type
             - happens
 
-            Group names: [2 TODO,2022-09-19 Monday]
-            #### [status.type] 2 TODO
+            Group names: [%%2%%TODO,2022-09-19 Monday]
+            #### [status.type] %%2%%TODO
             ##### [happens] 2022-09-19 Monday
             - [ ] Task a - early date 📅 2022-09-19
 
             ---
 
-            Group names: [2 TODO,2022-10-06 Thursday]
+            Group names: [%%2%%TODO,2022-10-06 Thursday]
             ##### [happens] 2022-10-06 Thursday
             - [ ] Task c - intermediate date ⏳ 2022-10-06
 
             ---
 
-            Group names: [2 TODO,2022-12-06 Tuesday]
+            Group names: [%%2%%TODO,2022-12-06 Tuesday]
             ##### [happens] 2022-12-06 Tuesday
             - [ ] Task b - later date ⏳ 2022-12-06
 
diff --git a/tests/TaskLineRenderer.test.ts b/tests/TaskLineRenderer.test.ts
index 891c58f7..4cf398f9 100644
--- a/tests/TaskLineRenderer.test.ts
+++ b/tests/TaskLineRenderer.test.ts
@@ -109,24 +109,32 @@ describe('task line rendering', () => {
         expect((internalDescriptionSpan as HTMLSpanElement).innerText).toEqual('This is a simple task');
     });
 
-    it('hides the global filter if and only if required', async () => {
-        const getDescriptionTest = async () => {
-            const taskLine = '- [ ] This is a simple task with a #global filter';
-            const task = fromLine({
-                line: taskLine,
-            });
-            const parentRender = await createMockParentAndRender(task);
-            return getDescriptionText(parentRender);
-        };
+    const getDescriptionTest = async (taskLine: string) => {
+        const task = fromLine({
+            line: taskLine,
+        });
+        const parentRender = await createMockParentAndRender(task);
+        return getDescriptionText(parentRender);
+    };
+
+    it('should render Global Filter when the Remove Global Filter is off', async () => {
+        updateSettings({ removeGlobalFilter: false });
+        GlobalFilter.set('#global');
+
+        const taskLine = '- [ ] This is a simple task with a #global filter';
+        const descriptionWithFilter = await getDescriptionTest(taskLine);
 
-        const descriptionWithFilter = await getDescriptionTest();
         expect(descriptionWithFilter).toEqual('This is a simple task with a #global filter');
+    });
 
+    it('should not render Global Filter when the Remove Global Filter is on', async () => {
         updateSettings({ removeGlobalFilter: true });
         GlobalFilter.set('#global');
-        const descriptionWithoutFilter = await getDescriptionTest();
-        expect(descriptionWithoutFilter).toEqual('This is a simple task with a  filter');
-        resetSettings();
+
+        const taskLine = '- [ ] #global/subtag-shall-stay This is a simple task with a #global filter';
+        const descriptionWithoutFilter = await getDescriptionTest(taskLine);
+
+        expect(descriptionWithoutFilter).toEqual('#global/subtag-shall-stay This is a simple task with a filter');
     });
 
     const testLayoutOptions = async (