From c197daa04c3114184cbd3ec2f1cfade1b5701793 Mon Sep 17 00:00:00 2001
From: Joffrey JAFFEUX <j.jaffeux@gmail.com>
Date: Thu, 8 Aug 2024 17:33:23 +0200
Subject: [PATCH] DEV: allows to alter category name/description (#28263)

This commit adds two new getters to the category model:
- `displayName`
- `descriptionText`

These getters are used instead of `name` and `description_text` where appropriate.

On top of this two transformers have been added to allow plugins to alter these getters:

```javascript
api.registerValueTransformer(
  "category-display-name",
  ({ value, context }) =>
    value + "-" + context.category.id + "-transformed"
);
```

```javascript
api.registerValueTransformer(
  "category-description-text",
  ({ value, context }) =>
    value + "-" + context.category.id + "-transformed"
);
```
---
 .../admin/addon/components/modal/reseed.hbs   |  2 +-
 .../admin/addon/templates/site-settings.hbs   |  4 +--
 .../categories-boxes-with-topics.hbs          |  2 +-
 .../app/components/categories-boxes.hbs       |  2 +-
 .../app/components/category-title-link.hbs    |  2 +-
 .../discourse/app/helpers/category-link.js    |  4 +--
 .../discourse/app/lib/plugin-api.gjs          |  4 +--
 .../category-section-link.js                  |  4 +--
 .../discourse/app/lib/transformer/registry.js |  2 ++
 .../discourse/app/models/category.js          | 17 ++++++++++++
 .../app/routes/build-category-route.js        |  4 +--
 .../discourse/app/routes/tag-show.js          |  4 +--
 .../fields/image-previews/logo-small.js       |  2 +-
 .../styling-preview/-homepage-preview.js      | 14 ++++++----
 .../acceptance/sidebar-plugin-api-test.js     |  4 +--
 .../sidebar-user-categories-section-test.js   |  4 +--
 .../category-description-text-test.js         | 26 +++++++++++++++++++
 .../category-display-name-test.js             | 25 ++++++++++++++++++
 .../helpers/category-badge-test.js            |  2 +-
 .../addon/components/category-drop.js         |  2 +-
 .../addon/components/category-row.gjs         |  4 +--
 21 files changed, 104 insertions(+), 30 deletions(-)
 create mode 100644 app/assets/javascripts/discourse/tests/acceptance/transformers/category-description-text-test.js
 create mode 100644 app/assets/javascripts/discourse/tests/acceptance/transformers/category-display-name-test.js

diff --git a/app/assets/javascripts/admin/addon/components/modal/reseed.hbs b/app/assets/javascripts/admin/addon/components/modal/reseed.hbs
index 46d26972f48..eb746b98ada 100644
--- a/app/assets/javascripts/admin/addon/components/modal/reseed.hbs
+++ b/app/assets/javascripts/admin/addon/components/modal/reseed.hbs
@@ -19,7 +19,7 @@
                 @type="checkbox"
                 @checked={{category.selected}}
               />
-              <span>{{category.name}}</span>
+              <span>{{category.displayName}}</span>
             </label>
           {{/each}}
         </fieldset>
diff --git a/app/assets/javascripts/admin/addon/templates/site-settings.hbs b/app/assets/javascripts/admin/addon/templates/site-settings.hbs
index a8225709bf4..f264afee288 100644
--- a/app/assets/javascripts/admin/addon/templates/site-settings.hbs
+++ b/app/assets/javascripts/admin/addon/templates/site-settings.hbs
@@ -18,9 +18,9 @@
           @route="adminSiteSettingsCategory"
           @model={{category.nameKey}}
           class={{category.nameKey}}
-          title={{category.name}}
+          title={{category.displayName}}
         >
-          {{category.name}}
+          {{category.displayName}}
           {{#if category.count}}
             <span class="count">({{category.count}})</span>
           {{/if}}
diff --git a/app/assets/javascripts/discourse/app/components/categories-boxes-with-topics.hbs b/app/assets/javascripts/discourse/app/components/categories-boxes-with-topics.hbs
index c9ffc514d9b..5e0c8831124 100644
--- a/app/assets/javascripts/discourse/app/components/categories-boxes-with-topics.hbs
+++ b/app/assets/javascripts/discourse/app/components/categories-boxes-with-topics.hbs
@@ -19,7 +19,7 @@
             {{#if c.read_restricted}}
               {{d-icon this.lockIcon}}
             {{/if}}
-            {{c.name}}
+            {{c.displayName}}
           </h3>
         </a>
       </div>
diff --git a/app/assets/javascripts/discourse/app/components/categories-boxes.hbs b/app/assets/javascripts/discourse/app/components/categories-boxes.hbs
index d2a295d35ca..d5469e87332 100644
--- a/app/assets/javascripts/discourse/app/components/categories-boxes.hbs
+++ b/app/assets/javascripts/discourse/app/components/categories-boxes.hbs
@@ -29,7 +29,7 @@
               {{#if c.read_restricted}}
                 {{d-icon this.lockIcon}}
               {{/if}}
-              {{c.name}}
+              {{c.displayName}}
             </h3>
           </a>
         </div>
diff --git a/app/assets/javascripts/discourse/app/components/category-title-link.hbs b/app/assets/javascripts/discourse/app/components/category-title-link.hbs
index b5c08397d42..1284bf33c59 100644
--- a/app/assets/javascripts/discourse/app/components/category-title-link.hbs
+++ b/app/assets/javascripts/discourse/app/components/category-title-link.hbs
@@ -4,7 +4,7 @@
     {{#if this.category.read_restricted}}
       {{d-icon this.lockIcon}}
     {{/if}}
-    <span class="category-name">{{dir-span this.category.name}}</span>
+    <span class="category-name">{{dir-span this.category.displayName}}</span>
   </div>
   {{#if this.category.uploaded_logo.url}}
     <CategoryLogo @category={{this.category}} />
diff --git a/app/assets/javascripts/discourse/app/helpers/category-link.js b/app/assets/javascripts/discourse/app/helpers/category-link.js
index 91df2ebb679..9b65a52e38b 100644
--- a/app/assets/javascripts/discourse/app/helpers/category-link.js
+++ b/app/assets/javascripts/discourse/app/helpers/category-link.js
@@ -112,7 +112,7 @@ function buildTopicCount(count) {
 }
 
 export function defaultCategoryLinkRenderer(category, opts) {
-  let descriptionText = escapeExpression(get(category, "description_text"));
+  let descriptionText = escapeExpression(get(category, "descriptionText"));
   let restricted = get(category, "read_restricted");
   let url = opts.url
     ? opts.url
@@ -156,7 +156,7 @@ export function defaultCategoryLinkRenderer(category, opts) {
     ${descriptionText ? 'title="' + descriptionText + '" ' : ""}
   >`;
 
-  let categoryName = escapeExpression(get(category, "name"));
+  let categoryName = escapeExpression(get(category, "displayName"));
 
   if (siteSettings.support_mixed_text_direction) {
     categoryDir = 'dir="auto"';
diff --git a/app/assets/javascripts/discourse/app/lib/plugin-api.gjs b/app/assets/javascripts/discourse/app/lib/plugin-api.gjs
index f800b02e0aa..83e3d21fb51 100644
--- a/app/assets/javascripts/discourse/app/lib/plugin-api.gjs
+++ b/app/assets/javascripts/discourse/app/lib/plugin-api.gjs
@@ -1537,8 +1537,8 @@ class PluginApi {
    *   displayName: "bugs"
    *   href: "/c/bugs",
    *   init: (navItem, category) => { if (category) { navItem.set("category", category)  } }
-   *   customFilter: (category, args, router) => { return category && category.name !== 'bug' }
-   *   customHref: (category, args, router) => {  if (category && category.name) === 'not-a-bug') return "/a-feature"; },
+   *   customFilter: (category, args, router) => { return category && category.displayName !== 'bug' }
+   *   customHref: (category, args, router) => {  if (category && category.displayName) === 'not-a-bug') return "/a-feature"; },
    *   before: "top",
    *   forceActive: (category, args, router) => router.currentURL === "/a/b/c/d",
    * })
diff --git a/app/assets/javascripts/discourse/app/lib/sidebar/user/categories-section/category-section-link.js b/app/assets/javascripts/discourse/app/lib/sidebar/user/categories-section/category-section-link.js
index cd350e8b7c8..d14bd6de981 100644
--- a/app/assets/javascripts/discourse/app/lib/sidebar/user/categories-section/category-section-link.js
+++ b/app/assets/javascripts/discourse/app/lib/sidebar/user/categories-section/category-section-link.js
@@ -178,11 +178,11 @@ export default class CategorySectionLink {
   }
 
   get title() {
-    return this.category.description_text;
+    return this.category.descriptionText;
   }
 
   get text() {
-    return this.category.name;
+    return this.category.displayName;
   }
 
   get prefixType() {
diff --git a/app/assets/javascripts/discourse/app/lib/transformer/registry.js b/app/assets/javascripts/discourse/app/lib/transformer/registry.js
index 1f6ea052df3..f111349d31a 100644
--- a/app/assets/javascripts/discourse/app/lib/transformer/registry.js
+++ b/app/assets/javascripts/discourse/app/lib/transformer/registry.js
@@ -5,6 +5,8 @@ export const BEHAVIOR_TRANSFORMERS = Object.freeze([
 
 export const VALUE_TRANSFORMERS = Object.freeze([
   // use only lowercase names
+  "category-description-text",
+  "category-display-name",
   "header-notifications-avatar-size",
   "home-logo-href",
   "home-logo-image-url",
diff --git a/app/assets/javascripts/discourse/app/models/category.js b/app/assets/javascripts/discourse/app/models/category.js
index 44080ed62f7..a6cf7a908d6 100644
--- a/app/assets/javascripts/discourse/app/models/category.js
+++ b/app/assets/javascripts/discourse/app/models/category.js
@@ -3,6 +3,7 @@ import { computed, get } from "@ember/object";
 import { service } from "@ember/service";
 import { ajax } from "discourse/lib/ajax";
 import { NotificationLevels } from "discourse/lib/notification-levels";
+import { applyValueTransformer } from "discourse/lib/transformer";
 import PermissionType from "discourse/models/permission-type";
 import RestModel from "discourse/models/rest";
 import Site from "discourse/models/site";
@@ -475,6 +476,22 @@ export default class Category extends RestModel {
     }
   }
 
+  get descriptionText() {
+    return applyValueTransformer(
+      "category-description-text",
+      this.get("description_text"),
+      {
+        category: this,
+      }
+    );
+  }
+
+  get displayName() {
+    return applyValueTransformer("category-display-name", this.get("name"), {
+      category: this,
+    });
+  }
+
   @computed("parent_category_id", "site.categories.[]")
   get parentCategory() {
     if (this.parent_category_id) {
diff --git a/app/assets/javascripts/discourse/app/routes/build-category-route.js b/app/assets/javascripts/discourse/app/routes/build-category-route.js
index 678f7027534..f4a83abcf6d 100644
--- a/app/assets/javascripts/discourse/app/routes/build-category-route.js
+++ b/app/assets/javascripts/discourse/app/routes/build-category-route.js
@@ -117,11 +117,11 @@ class AbstractCategoryRoute extends DiscourseRoute {
       "filters." + this.filter(category).replace("/", ".") + ".title"
     );
 
-    let categoryName = category.name;
+    let categoryName = category.displayName;
     if (category.parent_category_id) {
       const list = Category.list();
       const parentCategory = list.findBy("id", category.parent_category_id);
-      categoryName = `${parentCategory.name}/${categoryName}`;
+      categoryName = `${parentCategory.displayName}/${categoryName}`;
     }
 
     return I18n.t("filters.with_category", {
diff --git a/app/assets/javascripts/discourse/app/routes/tag-show.js b/app/assets/javascripts/discourse/app/routes/tag-show.js
index c01eb9aa96a..fbd7ae19c68 100644
--- a/app/assets/javascripts/discourse/app/routes/tag-show.js
+++ b/app/assets/javascripts/discourse/app/routes/tag-show.js
@@ -179,7 +179,7 @@ export default class TagShowRoute extends DiscourseRoute {
         return I18n.t("tagging.filters.with_category", {
           filter: filterText,
           tag: model.tag.id,
-          category: model.category.name,
+          category: model.category.displayName,
         });
       } else {
         return I18n.t("tagging.filters.without_category", {
@@ -191,7 +191,7 @@ export default class TagShowRoute extends DiscourseRoute {
       if (model.category) {
         return I18n.t("tagging.filters.untagged_with_category", {
           filter: filterText,
-          category: model.category.name,
+          category: model.category.displayName,
         });
       } else {
         return I18n.t("tagging.filters.untagged_without_category", {
diff --git a/app/assets/javascripts/discourse/app/static/wizard/components/fields/image-previews/logo-small.js b/app/assets/javascripts/discourse/app/static/wizard/components/fields/image-previews/logo-small.js
index 18f271481fb..f33e49318c0 100644
--- a/app/assets/javascripts/discourse/app/static/wizard/components/fields/image-previews/logo-small.js
+++ b/app/assets/javascripts/discourse/app/static/wizard/components/fields/image-previews/logo-small.js
@@ -73,7 +73,7 @@ export default PreviewBaseComponent.extend({
     ctx.font = `Bold ${badgeSize * 1.2}px '${font}'`;
     ctx.fillStyle = colors.primary;
     ctx.fillText(
-      category.name,
+      category.displayName,
       afterLogo + badgeSize * 1.5,
       headerHeight * 0.7 + badgeSize * 0.9
     );
diff --git a/app/assets/javascripts/discourse/app/static/wizard/components/fields/styling-preview/-homepage-preview.js b/app/assets/javascripts/discourse/app/static/wizard/components/fields/styling-preview/-homepage-preview.js
index bb94e4c5930..d4a49e8dcd4 100644
--- a/app/assets/javascripts/discourse/app/static/wizard/components/fields/styling-preview/-homepage-preview.js
+++ b/app/assets/javascripts/discourse/app/static/wizard/components/fields/styling-preview/-homepage-preview.js
@@ -85,7 +85,11 @@ export default PreviewBaseComponent.extend({
       ctx.font = `Bold ${bodyFontSize * 1.3}em '${font}'`;
       ctx.fillStyle = colors.primary;
       ctx.textAlign = "center";
-      ctx.fillText(category.name, boxStartX + boxWidth / 2, boxStartY + 25);
+      ctx.fillText(
+        category.displayName,
+        boxStartX + boxWidth / 2,
+        boxStartY + 25
+      );
       ctx.textAlign = "left";
 
       if (opts.topics) {
@@ -167,7 +171,7 @@ export default PreviewBaseComponent.extend({
       const textPos = y + categoryHeight * 0.35;
       ctx.font = `Bold ${bodyFontSize * 1.1}em '${font}'`;
       ctx.fillStyle = colors.primary;
-      ctx.fillText(category.name, cols[0], textPos);
+      ctx.fillText(category.displayName, cols[0], textPos);
 
       ctx.font = `${bodyFontSize * 0.8}em '${font}'`;
       ctx.fillStyle = textColor;
@@ -263,7 +267,7 @@ export default PreviewBaseComponent.extend({
       const textPos = y + categoryHeight * 0.35;
       ctx.font = `Bold ${bodyFontSize * 1.1}em '${font}'`;
       ctx.fillStyle = colors.primary;
-      ctx.fillText(category.name, cols[0], textPos);
+      ctx.fillText(category.displayName, cols[0], textPos);
 
       ctx.font = `${bodyFontSize * 0.8}em '${font}'`;
       ctx.fillStyle = textColor;
@@ -328,7 +332,7 @@ export default PreviewBaseComponent.extend({
 
       ctx.fillStyle = colors.primary;
       ctx.fillText(
-        category.name,
+        category.displayName,
         cols[3] + badgeSize * 3,
         y + topicHeight * 0.76
       );
@@ -409,7 +413,7 @@ export default PreviewBaseComponent.extend({
 
       ctx.fillStyle = colors.primary;
       ctx.fillText(
-        category.name,
+        category.displayName,
         cols[0] + badgeSize * 2,
         y + rowHeight * 0.73
       );
diff --git a/app/assets/javascripts/discourse/tests/acceptance/sidebar-plugin-api-test.js b/app/assets/javascripts/discourse/tests/acceptance/sidebar-plugin-api-test.js
index 161af3e3be8..727be141555 100644
--- a/app/assets/javascripts/discourse/tests/acceptance/sidebar-plugin-api-test.js
+++ b/app/assets/javascripts/discourse/tests/acceptance/sidebar-plugin-api-test.js
@@ -543,9 +543,9 @@ acceptance("Sidebar - Plugin API", function (needs) {
           route: "discovery.latestCategory",
           routeQuery: { status: "open" },
           shouldRegister: ({ category }) => {
-            if (category.name === category1.name) {
+            if (category.displayName === category1.displayName) {
               return true;
-            } else if (category.name === category2.name) {
+            } else if (category.displayName === category2.displayName) {
               return false;
             }
           },
diff --git a/app/assets/javascripts/discourse/tests/acceptance/sidebar-user-categories-section-test.js b/app/assets/javascripts/discourse/tests/acceptance/sidebar-user-categories-section-test.js
index d50d6032d19..ba24ce52761 100644
--- a/app/assets/javascripts/discourse/tests/acceptance/sidebar-user-categories-section-test.js
+++ b/app/assets/javascripts/discourse/tests/acceptance/sidebar-user-categories-section-test.js
@@ -177,7 +177,7 @@ acceptance("Sidebar - Logged on user - Categories Section", function (needs) {
         exists(
           `.sidebar-section-link-wrapper[data-category-id=${category.id}]`
         ),
-        `${category.name} section link is shown`
+        `${category.displayName} section link is shown`
       );
     });
   });
@@ -677,7 +677,7 @@ acceptance("Sidebar - Logged on user - Categories Section", function (needs) {
       query(
         `.sidebar-section-link-wrapper[data-category-id="${category.id}"] a`
       ).title,
-      category.description_text,
+      category.descriptionText,
       "category description without HTML entity is used as the link's title"
     );
   });
diff --git a/app/assets/javascripts/discourse/tests/acceptance/transformers/category-description-text-test.js b/app/assets/javascripts/discourse/tests/acceptance/transformers/category-description-text-test.js
new file mode 100644
index 00000000000..9a447fdc215
--- /dev/null
+++ b/app/assets/javascripts/discourse/tests/acceptance/transformers/category-description-text-test.js
@@ -0,0 +1,26 @@
+import { visit } from "@ember/test-helpers";
+import { test } from "qunit";
+import { withPluginApi } from "discourse/lib/plugin-api";
+import { acceptance } from "discourse/tests/helpers/qunit-helpers";
+
+acceptance("category-description-text transformer", function () {
+  test("applying a value transformation", async function (assert) {
+    withPluginApi("1.34.0", (api) => {
+      api.registerValueTransformer(
+        "category-description-text",
+        ({ value, context }) =>
+          value[0] + "-" + context.category.id + "-transformed"
+      );
+    });
+
+    await visit("/");
+
+    assert
+      .dom("[data-topic-id='11994'] .badge-category")
+      .hasAttribute(
+        "title",
+        "A-1-transformed",
+        "it transforms the category description text"
+      );
+  });
+});
diff --git a/app/assets/javascripts/discourse/tests/acceptance/transformers/category-display-name-test.js b/app/assets/javascripts/discourse/tests/acceptance/transformers/category-display-name-test.js
new file mode 100644
index 00000000000..df1f8634f61
--- /dev/null
+++ b/app/assets/javascripts/discourse/tests/acceptance/transformers/category-display-name-test.js
@@ -0,0 +1,25 @@
+import { visit } from "@ember/test-helpers";
+import { test } from "qunit";
+import { withPluginApi } from "discourse/lib/plugin-api";
+import { acceptance } from "discourse/tests/helpers/qunit-helpers";
+
+acceptance("category-display-name transformer", function () {
+  test("applying a value transformation", async function (assert) {
+    withPluginApi("1.34.0", (api) => {
+      api.registerValueTransformer(
+        "category-display-name",
+        ({ value, context }) =>
+          value + "-" + context.category.id + "-transformed"
+      );
+    });
+
+    await visit("/");
+
+    assert
+      .dom("[data-topic-id='11997'] .badge-category__name")
+      .hasText(
+        "feature-2-transformed",
+        "it transforms the category display name"
+      );
+  });
+});
diff --git a/app/assets/javascripts/discourse/tests/integration/helpers/category-badge-test.js b/app/assets/javascripts/discourse/tests/integration/helpers/category-badge-test.js
index bbeaba056f0..2ae920de031 100644
--- a/app/assets/javascripts/discourse/tests/integration/helpers/category-badge-test.js
+++ b/app/assets/javascripts/discourse/tests/integration/helpers/category-badge-test.js
@@ -15,7 +15,7 @@ module("Integration | Helper | category-badge", function (hooks) {
 
     assert.strictEqual(
       query(".badge-category__name").innerText.trim(),
-      this.category.name
+      this.category.displayName
     );
   });
 
diff --git a/app/assets/javascripts/select-kit/addon/components/category-drop.js b/app/assets/javascripts/select-kit/addon/components/category-drop.js
index a5e1aa1cca7..703a5521280 100644
--- a/app/assets/javascripts/select-kit/addon/components/category-drop.js
+++ b/app/assets/javascripts/select-kit/addon/components/category-drop.js
@@ -152,7 +152,7 @@ export default ComboBoxComponent.extend({
     return content;
   },
 
-  parentCategoryName: readOnly("selectKit.options.parentCategory.name"),
+  parentCategoryName: readOnly("selectKit.options.parentCategory.displayName"),
 
   allCategoriesLabel: computed(
     "parentCategoryName",
diff --git a/app/assets/javascripts/select-kit/addon/components/category-row.gjs b/app/assets/javascripts/select-kit/addon/components/category-row.gjs
index aa2a44c017f..d775d34fdb8 100644
--- a/app/assets/javascripts/select-kit/addon/components/category-row.gjs
+++ b/app/assets/javascripts/select-kit/addon/components/category-row.gjs
@@ -83,11 +83,11 @@ export default class CategoryRow extends Component {
   }
 
   get categoryName() {
-    return this.category.name;
+    return this.category.displayName;
   }
 
   get categoryDescriptionText() {
-    return this.category.description_text;
+    return this.category.descriptionText;
   }
 
   @cached