From e880ede3d86b8f6b19ff4f1f7efd7c2e47f52edc Mon Sep 17 00:00:00 2001 From: Alan Guo Xiang Tan Date: Wed, 24 May 2023 09:50:54 +0900 Subject: [PATCH] DEV: Add experimental plugin API to replace tag icon in sidebar (#21675) Why this change? This change allows plugins or themes to replace the tag icon in the sidebar. The color of the icon can be customised as well. However, do note that this change is marked experimental as we intend to support custom icons for tags in the near term as part of Discourse core. Therefore, the plugin API will become obsolete once that happens and we are marking it experimental to avoid having to deprecate it. --- .../sidebar/anonymous/tags-section.hbs | 1 + .../components/sidebar/section-link-prefix.js | 7 ++- .../app/components/sidebar/section-link.js | 25 ++++++++-- .../components/sidebar/user/tags-section.hbs | 1 + .../discourse/app/lib/plugin-api.js | 33 +++++++++++++ .../tags-section/base-tag-section-link.js | 30 +++++++++++- .../user/tags-section/pm-tag-section-link.js | 5 -- .../user/tags-section/tag-section-link.js | 3 +- .../acceptance/sidebar-plugin-api-test.js | 49 +++++++++++++++++++ 9 files changed, 138 insertions(+), 16 deletions(-) diff --git a/app/assets/javascripts/discourse/app/components/sidebar/anonymous/tags-section.hbs b/app/assets/javascripts/discourse/app/components/sidebar/anonymous/tags-section.hbs index c8adf2686bb..644fb57316e 100644 --- a/app/assets/javascripts/discourse/app/components/sidebar/anonymous/tags-section.hbs +++ b/app/assets/javascripts/discourse/app/components/sidebar/anonymous/tags-section.hbs @@ -12,6 +12,7 @@ @currentWhen={{sectionLink.currentWhen}} @prefixType={{sectionLink.prefixType}} @prefixValue={{sectionLink.prefixValue}} + @prefixColor={{sectionLink.prefixColor}} @models={{sectionLink.models}} data-tag-name={{sectionLink.tagName}} /> diff --git a/app/assets/javascripts/discourse/app/components/sidebar/section-link-prefix.js b/app/assets/javascripts/discourse/app/components/sidebar/section-link-prefix.js index f0913b3d673..31ecf39dfb3 100644 --- a/app/assets/javascripts/discourse/app/components/sidebar/section-link-prefix.js +++ b/app/assets/javascripts/discourse/app/components/sidebar/section-link-prefix.js @@ -1,4 +1,5 @@ import Component from "@glimmer/component"; +import { isHex } from "discourse/components/sidebar/section-link"; export default class extends Component { get prefixValue() { @@ -11,8 +12,10 @@ export default class extends Component { let hexValues = this.args.prefixValue; hexValues = hexValues.reduce((acc, color) => { - if (color?.match(/^([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/)) { - acc.push(`#${color} 50%`); + const hexCode = isHex(color); + + if (hexCode) { + acc.push(`#${hexCode} 50%`); } return acc; diff --git a/app/assets/javascripts/discourse/app/components/sidebar/section-link.js b/app/assets/javascripts/discourse/app/components/sidebar/section-link.js index 6ced6142349..00e497d64e7 100644 --- a/app/assets/javascripts/discourse/app/components/sidebar/section-link.js +++ b/app/assets/javascripts/discourse/app/components/sidebar/section-link.js @@ -1,6 +1,21 @@ import Component from "@glimmer/component"; import { inject as service } from "@ember/service"; +/** + * Checks if a given string is a valid color hex code. + * + * @param {String|undefined} input Input string to check if it is a valid color hex code. Can be in the form of "FFFFFF" or "#FFFFFF" or "FFF" or "#FFF". + * @returns {String|undefined} Returns the matching color hex code without the leading `#` if it is valid, otherwise returns undefined. Example: "FFFFFF" or "FFF". + */ +export function isHex(input) { + const match = input?.match(/^#?([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/); + + if (match) { + return match[1]; + } else { + return; + } +} export default class SectionLink extends Component { @service currentUser; @@ -53,12 +68,12 @@ export default class SectionLink extends Component { } get prefixColor() { - const color = this.args.prefixColor; + const hexCode = isHex(this.args.prefixColor); - if (!color || !color.match(/^\w{6}$/)) { - return ""; + if (hexCode) { + return `#${hexCode}`; + } else { + return; } - - return "#" + color; } } diff --git a/app/assets/javascripts/discourse/app/components/sidebar/user/tags-section.hbs b/app/assets/javascripts/discourse/app/components/sidebar/user/tags-section.hbs index b09db23259e..7c997cb3488 100644 --- a/app/assets/javascripts/discourse/app/components/sidebar/user/tags-section.hbs +++ b/app/assets/javascripts/discourse/app/components/sidebar/user/tags-section.hbs @@ -22,6 +22,7 @@ @currentWhen={{sectionLink.currentWhen}} @prefixType={{sectionLink.prefixType}} @prefixValue={{sectionLink.prefixValue}} + @prefixColor={{sectionLink.prefixColor}} @badgeText={{sectionLink.badgeText}} @models={{sectionLink.models}} @suffixCSSClass={{sectionLink.suffixCSSClass}} diff --git a/app/assets/javascripts/discourse/app/lib/plugin-api.js b/app/assets/javascripts/discourse/app/lib/plugin-api.js index 4da8c699a14..4915ae94b53 100644 --- a/app/assets/javascripts/discourse/app/lib/plugin-api.js +++ b/app/assets/javascripts/discourse/app/lib/plugin-api.js @@ -109,6 +109,7 @@ import { registerCustomCategorySectionLinkPrefix, registerCustomCountable as registerUserCategorySectionLinkCountable, } from "discourse/lib/sidebar/user/categories-section/category-section-link"; +import { registerCustomTagSectionLinkPrefixIcon } from "discourse/lib/sidebar/user/tags-section/base-tag-section-link"; import { REFRESH_COUNTS_APP_EVENT_NAME as REFRESH_USER_SIDEBAR_CATEGORIES_SECTION_COUNTS_APP_EVENT_NAME } from "discourse/components/sidebar/user/categories-section"; import DiscourseURL from "discourse/lib/url"; import { registerNotificationTypeRenderer } from "discourse/lib/notification-types-manager"; @@ -1965,6 +1966,38 @@ class PluginApi { }); } + /** + * EXPERIMENTAL. Do not use. + * Register a custom prefix for a sidebar tag section link. + * + * Example: + * + * ``` + * api.registerCustomTagSectionLinkPrefixValue({ + * tagName: "tag1", + * prefixType: "icon", + * prefixValue: "wrench", + * prefixColor: "#FF0000" + * }); + * ``` + * + * @params {Object} arg - An object + * @params {string} arg.tagName - The name of the tag + * @params {string} arg.prefixValue - The name of a FontAwesome 5 icon. + * @params {string} arg.prefixColor - The color represented using hexadecimal to use for the prefix. Example: "#FF0000" or "#FFF". + */ + registerCustomTagSectionLinkPrefixIcon({ + tagName, + prefixValue, + prefixColor, + }) { + registerCustomTagSectionLinkPrefixIcon({ + tagName, + prefixValue, + prefixColor, + }); + } + /** * EXPERIMENTAL. Do not use. * Triggers a refresh of the counts for all category section links under the categories section for a logged in user. diff --git a/app/assets/javascripts/discourse/app/lib/sidebar/user/tags-section/base-tag-section-link.js b/app/assets/javascripts/discourse/app/lib/sidebar/user/tags-section/base-tag-section-link.js index 6a32ae90277..2c2c9967478 100644 --- a/app/assets/javascripts/discourse/app/lib/sidebar/user/tags-section/base-tag-section-link.js +++ b/app/assets/javascripts/discourse/app/lib/sidebar/user/tags-section/base-tag-section-link.js @@ -1,6 +1,28 @@ +let customTagSectionLinkPrefixIcons = {}; + +export function registerCustomTagSectionLinkPrefixIcon({ + tagName, + prefixValue, + prefixColor, +}) { + customTagSectionLinkPrefixIcons[tagName] = { + prefixValue, + prefixColor, + }; +} + +export function resetCustomTagSectionLinkPrefixIcons() { + for (let key in customTagSectionLinkPrefixIcons) { + if (customTagSectionLinkPrefixIcons.hasOwnProperty(key)) { + delete customTagSectionLinkPrefixIcons[key]; + } + } +} + export default class BaseTagSectionLink { - constructor({ tagName }) { + constructor({ tagName, currentUser }) { this.tagName = tagName; + this.currentUser = currentUser; } get name() { @@ -16,6 +38,10 @@ export default class BaseTagSectionLink { } get prefixValue() { - return "tag"; + return customTagSectionLinkPrefixIcons[this.tagName]?.prefixValue || "tag"; + } + + get prefixColor() { + return customTagSectionLinkPrefixIcons[this.tagName]?.prefixColor; } } diff --git a/app/assets/javascripts/discourse/app/lib/sidebar/user/tags-section/pm-tag-section-link.js b/app/assets/javascripts/discourse/app/lib/sidebar/user/tags-section/pm-tag-section-link.js index 7e10951d5b1..09d36e4d10c 100644 --- a/app/assets/javascripts/discourse/app/lib/sidebar/user/tags-section/pm-tag-section-link.js +++ b/app/assets/javascripts/discourse/app/lib/sidebar/user/tags-section/pm-tag-section-link.js @@ -1,11 +1,6 @@ import BaseTagSectionLink from "discourse/lib/sidebar/user/tags-section/base-tag-section-link"; export default class PMTagSectionLink extends BaseTagSectionLink { - constructor({ currentUser }) { - super(...arguments); - this.currentUser = currentUser; - } - get models() { return [this.currentUser, this.tagName]; } diff --git a/app/assets/javascripts/discourse/app/lib/sidebar/user/tags-section/tag-section-link.js b/app/assets/javascripts/discourse/app/lib/sidebar/user/tags-section/tag-section-link.js index 179ee6486a0..5f05849ba05 100644 --- a/app/assets/javascripts/discourse/app/lib/sidebar/user/tags-section/tag-section-link.js +++ b/app/assets/javascripts/discourse/app/lib/sidebar/user/tags-section/tag-section-link.js @@ -12,10 +12,9 @@ export default class TagSectionLink extends BaseTagSectionLink { @tracked hideCount = this.currentUser?.sidebarListDestination !== UNREAD_LIST_DESTINATION; - constructor({ topicTrackingState, currentUser }) { + constructor({ topicTrackingState }) { super(...arguments); this.topicTrackingState = topicTrackingState; - this.currentUser = currentUser; this.refreshCounts(); } 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 2223c1191b1..3a42c83f95a 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 @@ -15,6 +15,7 @@ import { resetCustomCategorySectionLinkPrefix, resetCustomCountables, } from "discourse/lib/sidebar/user/categories-section/category-section-link"; +import { resetCustomTagSectionLinkPrefixIcons } from "discourse/lib/sidebar/user/tags-section/base-tag-section-link"; import { UNREAD_LIST_DESTINATION } from "discourse/controllers/preferences/sidebar"; import { bind } from "discourse-common/utils/decorators"; @@ -22,6 +23,7 @@ acceptance("Sidebar - Plugin API", function (needs) { needs.user({}); needs.settings({ + tagging_enabled: true, navigation_menu: "sidebar", }); @@ -820,4 +822,51 @@ acceptance("Sidebar - Plugin API", function (needs) { resetCustomCategorySectionLinkPrefix(); } }); + + test("Customizing the prefix icon used in a tag section link for a particular tag", async function (assert) { + try { + return await withPluginApi(PLUGIN_API_VERSION, async (api) => { + updateCurrentUser({ + display_sidebar_tags: true, + sidebar_tags: [ + { name: "tag2", pm_only: false }, + { name: "tag1", pm_only: false }, + { name: "tag3", pm_only: false }, + ], + }); + + api.registerCustomTagSectionLinkPrefixIcon({ + tagName: "tag1", + prefixValue: "wrench", + prefixColor: "#FF0000", // rgb(255, 0, 0) + }); + + await visit("/"); + + assert.ok( + exists( + `.sidebar-section-link[data-tag-name="tag1"] .prefix-icon.d-icon-wrench` + ), + "wrench icon is displayed for tag1 section link's prefix icon" + ); + + assert.strictEqual( + query( + `.sidebar-section-link[data-tag-name="tag1"] .sidebar-section-link-prefix` + ).style.color, + "rgb(255, 0, 0)", + "tag1 section link's prefix icon has the right color" + ); + + assert.ok( + exists( + `.sidebar-section-link[data-tag-name="tag2"] .prefix-icon.d-icon-tag` + ), + "default tag icon is displayed for tag2 section link's prefix icon" + ); + }); + } finally { + resetCustomTagSectionLinkPrefixIcons(); + } + }); });