FIX: Include all categories in sortedCategories (#14979)

FIX: Include all categories in sortedCategories (#14979)

Fixes the issue where categories over two levels deep were missing.

diff --git a/app/assets/javascripts/discourse/app/models/site.js b/app/assets/javascripts/discourse/app/models/site.js
index 9b92108..50e9ee0 100644
--- a/app/assets/javascripts/discourse/app/models/site.js
+++ b/app/assets/javascripts/discourse/app/models/site.js
@@ -57,30 +57,21 @@ const Site = RestModel.extend({
 
   // Sort subcategories under parents
   @discourseComputed("categoriesByCount", "categories.[]")
-  sortedCategories(cats) {
-    const result = [],
-      remaining = {};
-
-    cats.forEach((c) => {
-      const parentCategoryId = parseInt(c.get("parent_category_id"), 10);
-      if (!parentCategoryId) {
-        result.pushObject(c);
-      } else {
-        remaining[parentCategoryId] = remaining[parentCategoryId] || [];
-        remaining[parentCategoryId].pushObject(c);
-      }
-    });
+  sortedCategories(categories) {
+    const children = new Map();
 
-    Object.keys(remaining).forEach((parentCategoryId) => {
-      const category = result.findBy("id", parseInt(parentCategoryId, 10)),
-        index = result.indexOf(category);
+    categories.forEach((category) => {
+      const parentId = parseInt(category.parent_category_id, 10) || -1;
+      const group = children.get(parentId) || [];
+      group.pushObject(category);
 
-      if (index !== -1) {
-        result.replace(index + 1, 0, remaining[parentCategoryId]);
-      }
+      children.set(parentId, group);
     });
 
-    return result;
+    const reduce = (values) =>
+      values.flatMap((c) => [c, reduce(children.get(c.id) || [])]).flat();
+
+    return reduce(children.get(-1));
   },
 
   // Returns it in the correct order, by setting
diff --git a/app/assets/javascripts/discourse/tests/unit/models/site-test.js b/app/assets/javascripts/discourse/tests/unit/models/site-test.js
index e4b5be2..5fcd1ad 100644
--- a/app/assets/javascripts/discourse/tests/unit/models/site-test.js
+++ b/app/assets/javascripts/discourse/tests/unit/models/site-test.js
@@ -29,45 +29,74 @@ module("Unit | Model | site", function () {
     const store = createStore();
     const site = store.createRecord("site", {
       categories: [
-        { id: 1234, name: "Test" },
         { id: 3456, name: "Test Subcategory", parent_category_id: 1234 },
+        { id: 1234, name: "Test" },
         { id: 3458, name: "Invalid Subcategory", parent_category_id: 6666 },
       ],
     });
 
-    const categories = site.get("categories");
-    site.get("sortedCategories");
+    assert.present(site.categories, "The categories are present");
+    assert.deepEqual(site.categories.mapBy("name"), [
+      "Test Subcategory",
+      "Test",
+      "Invalid Subcategory",
+    ]);
 
-    assert.present(categories, "The categories are present");
-    assert.strictEqual(categories.length, 3, "it loaded all three categories");
+    assert.deepEqual(site.sortedCategories.mapBy("name"), [
+      "Test",
+      "Test Subcategory",
+    ]);
 
-    const parent = categories.findBy("id", 1234);
+    const parent = site.categories.findBy("id", 1234);
     assert.present(parent, "it loaded the parent category");
-    assert.blank(parent.get("parentCategory"), "it has no parent category");
+    assert.blank(parent.parentCategory, "it has no parent category");
 
-    assert.strictEqual(parent.get("subcategories").length, 1);
+    assert.strictEqual(parent.subcategories.length, 1);
 
-    const subcategory = categories.findBy("id", 3456);
+    const subcategory = site.categories.findBy("id", 3456);
     assert.present(subcategory, "it loaded the subcategory");
     assert.strictEqual(
-      subcategory.get("parentCategory"),
+      subcategory.parentCategory,
       parent,
       "it has associated the child with the parent"
     );
 
     // remove invalid category and child
-    categories.removeObject(categories[2]);
-    categories.removeObject(categories[1]);
+    site.categories.removeObject(site.categories[2]);
+    site.categories.removeObject(site.categories[0]);
 
     assert.strictEqual(
-      categories.length,
-      site.get("categoriesByCount").length,
-      "categories by count should change on removal"
+      site.categoriesByCount.length,
+      site.categories.length,
+      "categoriesByCount should change on removal"
     );
     assert.strictEqual(
-      categories.length,
-      site.get("sortedCategories").length,
-      "sorted categories should change on removal"
+      site.sortedCategories.length,
+      site.categories.length,
+      "sortedCategories should change on removal"
     );
   });
+
+  test("deeply nested categories", function (assert) {
+    const store = createStore();
+    const site = store.createRecord("site", {
+      categories: [
+        { id: 1003, name: "Test Sub Sub", parent_category_id: 1002 },
+        { id: 1001, name: "Test" },
+        { id: 1004, name: "Test Sub Sub Sub", parent_category_id: 1003 },
+        { id: 1002, name: "Test Sub", parent_category_id: 1001 },
+        { id: 1005, name: "Test Sub Sub Sub2", parent_category_id: 1003 },
+        { id: 1006, name: "Test2" },
+      ],
+    });
+
+    assert.deepEqual(site.sortedCategories.mapBy("name"), [
+      "Test",
+      "Test Sub",
+      "Test Sub Sub",
+      "Test Sub Sub Sub",
+      "Test Sub Sub Sub2",
+      "Test2",
+    ]);
+  });
 });

GitHub sha: bf33d2cd4b27bbb63720c8691f4508e4ea0e8568

This commit appears in #14979 which was approved by eviltrout. It was merged by CvX.