diff --git a/app/assets/javascripts/discourse/app/components/sidebar/categories-section.js b/app/assets/javascripts/discourse/app/components/sidebar/categories-section.js
index 1815b8804bc..21f48211064 100644
--- a/app/assets/javascripts/discourse/app/components/sidebar/categories-section.js
+++ b/app/assets/javascripts/discourse/app/components/sidebar/categories-section.js
@@ -1,3 +1,10 @@
import GlimmerComponent from "discourse/components/glimmer";
+import CategorySectionLink from "discourse/lib/sidebar/categories-section/category-section-link";
-export default class SidebarCategoriesSection extends GlimmerComponent {}
+export default class SidebarCategoriesSection extends GlimmerComponent {
+ get sectionLinks() {
+ return this.site.trackedCategoriesList.map((trackedCategory) => {
+ return new CategorySectionLink({ category: trackedCategory });
+ });
+ }
+}
diff --git a/app/assets/javascripts/discourse/app/lib/sidebar/categories-section/category-section-link.js b/app/assets/javascripts/discourse/app/lib/sidebar/categories-section/category-section-link.js
new file mode 100644
index 00000000000..17766f1ba13
--- /dev/null
+++ b/app/assets/javascripts/discourse/app/lib/sidebar/categories-section/category-section-link.js
@@ -0,0 +1,34 @@
+import { htmlSafe } from "@ember/template";
+
+import { categoryBadgeHTML } from "discourse/helpers/category-link";
+import Category from "discourse/models/category";
+
+export default class CategorySectionLink {
+ constructor({ category }) {
+ this.category = category;
+ }
+
+ get name() {
+ return this.category.slug;
+ }
+
+ get route() {
+ return "discovery.latestCategory";
+ }
+
+ get model() {
+ return `${Category.slugFor(this.category)}/${this.category.id}`;
+ }
+
+ get currentWhen() {
+ return "discovery.unreadCategory discovery.topCategory discovery.newCategory discovery.latestCategory";
+ }
+
+ get title() {
+ return this.category.description_excerpt;
+ }
+
+ get text() {
+ return htmlSafe(categoryBadgeHTML(this.category, { link: false }));
+ }
+}
diff --git a/app/assets/javascripts/discourse/app/models/category.js b/app/assets/javascripts/discourse/app/models/category.js
index 6e6f644314b..c5b473cccc3 100644
--- a/app/assets/javascripts/discourse/app/models/category.js
+++ b/app/assets/javascripts/discourse/app/models/category.js
@@ -187,6 +187,11 @@ const Category = RestModel.extend({
return seconds ? seconds / 60 : null;
},
+ @discourseComputed("notification_level")
+ isTracked(notificationLevel) {
+ return notificationLevel >= NotificationLevels.TRACKING;
+ },
+
save() {
const id = this.id;
const url = id ? `/categories/${id}` : "/categories";
diff --git a/app/assets/javascripts/discourse/app/models/site.js b/app/assets/javascripts/discourse/app/models/site.js
index 343ed0913bd..81dcc716e5c 100644
--- a/app/assets/javascripts/discourse/app/models/site.js
+++ b/app/assets/javascripts/discourse/app/models/site.js
@@ -11,7 +11,6 @@ import discourseComputed from "discourse-common/utils/decorators";
import { getOwner } from "discourse-common/lib/get-owner";
import { isEmpty } from "@ember/utils";
import { htmlSafe } from "@ember/template";
-import { NotificationLevels } from "discourse/lib/notification-levels";
const Site = RestModel.extend({
isReadOnly: alias("is_readonly"),
@@ -84,12 +83,12 @@ const Site = RestModel.extend({
: this.sortedCategories;
},
- @discourseComputed("categories.[]")
+ @discourseComputed("categories.[]", "categories.@each.notification_level")
trackedCategoriesList(categories) {
const trackedCategories = [];
for (const category of categories) {
- if (category.notification_level >= NotificationLevels.TRACKING) {
+ if (category.isTracked) {
trackedCategories.push(category);
}
}
diff --git a/app/assets/javascripts/discourse/app/templates/components/sidebar/categories-section.hbs b/app/assets/javascripts/discourse/app/templates/components/sidebar/categories-section.hbs
index 9dc65b326c0..8c0e720afbb 100644
--- a/app/assets/javascripts/discourse/app/templates/components/sidebar/categories-section.hbs
+++ b/app/assets/javascripts/discourse/app/templates/components/sidebar/categories-section.hbs
@@ -4,4 +4,21 @@
@headerLinkText={{i18n "sidebar.sections.categories.header_link_text"}}
@headerLinkTitle={{i18n "sidebar.sections.categories.header_link_title"}} >
+ {{#if (gt this.sectionLinks.length 0)}}
+ {{#each this.sectionLinks as |sectionLink|}}
+
+
+ {{/each}}
+ {{else}}
+
+ {{i18n "sidebar.sections.categories.no_tracked_categories"}}
+
+ {{/if}}
diff --git a/app/assets/javascripts/discourse/app/templates/components/sidebar/section-message.hbs b/app/assets/javascripts/discourse/app/templates/components/sidebar/section-message.hbs
new file mode 100644
index 00000000000..d3d92dad946
--- /dev/null
+++ b/app/assets/javascripts/discourse/app/templates/components/sidebar/section-message.hbs
@@ -0,0 +1,5 @@
+
diff --git a/app/assets/javascripts/discourse/tests/acceptance/sidebar-categories-section-test.js b/app/assets/javascripts/discourse/tests/acceptance/sidebar-categories-section-test.js
index c08a5f5b38c..d8a7f2d561e 100644
--- a/app/assets/javascripts/discourse/tests/acceptance/sidebar-categories-section-test.js
+++ b/app/assets/javascripts/discourse/tests/acceptance/sidebar-categories-section-test.js
@@ -1,14 +1,53 @@
+import I18n from "I18n";
+
import { click, currentURL, visit } from "@ember/test-helpers";
import {
acceptance,
conditionalTest,
+ exists,
+ query,
+ queryAll,
} from "discourse/tests/helpers/qunit-helpers";
+import selectKit from "discourse/tests/helpers/select-kit-helper";
import { isLegacyEmber } from "discourse-common/config/environment";
+import Site from "discourse/models/site";
+import { NotificationLevels } from "discourse/lib/notification-levels";
+import discoveryFixture from "discourse/tests/fixtures/discovery-fixtures";
+import categoryFixture from "discourse/tests/fixtures/category-fixtures";
+import { cloneJSON } from "discourse-common/lib/object";
acceptance("Sidebar - Categories Section", function (needs) {
needs.user({ experimental_sidebar_enabled: true });
+ needs.pretender((server, helper) => {
+ ["latest", "top", "new", "unread"].forEach((type) => {
+ server.get(`/c/:categorySlug/:categoryId/l/${type}.json`, () => {
+ return helper.response(
+ cloneJSON(discoveryFixture["/c/bug/1/l/latest.json"])
+ );
+ });
+ });
+
+ server.get("/c/:categorySlug/:categoryId/find_by_slug.json", () => {
+ return helper.response(cloneJSON(categoryFixture["/c/1/show.json"]));
+ });
+
+ server.post("/category/:categoryId/notifications", () => {
+ return helper.response({});
+ });
+ });
+
+ const setupTrackedCategories = function () {
+ const categories = Site.current().categories;
+ const category1 = categories[0];
+ const category2 = categories[1];
+ category1.set("notification_level", NotificationLevels.TRACKING);
+ category2.set("notification_level", NotificationLevels.TRACKING);
+
+ return { category1, category2 };
+ };
+
conditionalTest(
"clicking on section header link",
!isLegacyEmber(),
@@ -23,4 +62,205 @@ acceptance("Sidebar - Categories Section", function (needs) {
);
}
);
+
+ conditionalTest(
+ "category section links when user does not have any tracked categories",
+ !isLegacyEmber(),
+ async function (assert) {
+ await visit("/");
+
+ assert.strictEqual(
+ query(".sidebar-section-message").textContent.trim(),
+ I18n.t("sidebar.sections.categories.no_tracked_categories"),
+ "the no tracked categories message is displayed"
+ );
+ }
+ );
+
+ conditionalTest(
+ "category section links for tracked categories",
+ !isLegacyEmber(),
+ async function (assert) {
+ const { category1, category2 } = setupTrackedCategories();
+
+ await visit("/");
+
+ assert.strictEqual(
+ queryAll(".sidebar-section-categories .sidebar-section-link").length,
+ 2,
+ "there should only be two section link under the section"
+ );
+
+ assert.ok(
+ exists(`.sidebar-section-link-${category1.slug} .badge-category`),
+ "category1 section link is rendered with category badge"
+ );
+
+ assert.strictEqual(
+ query(`.sidebar-section-link-${category1.slug}`).textContent.trim(),
+ category1.name,
+ "displays category1's name for the link text"
+ );
+
+ await click(`.sidebar-section-link-${category1.slug}`);
+
+ assert.strictEqual(
+ currentURL(),
+ `/c/${category1.slug}/${category1.id}/l/latest`,
+ "it should transition to the category1's discovery page"
+ );
+
+ assert.strictEqual(
+ queryAll(".sidebar-section-categories .sidebar-section-link.active")
+ .length,
+ 1,
+ "only one link is marked as active"
+ );
+
+ assert.ok(
+ exists(`.sidebar-section-link-${category1.slug}.active`),
+ "the category1 section link is marked as active"
+ );
+
+ await click(`.sidebar-section-link-${category2.slug}`);
+
+ assert.strictEqual(
+ currentURL(),
+ `/c/${category2.slug}/${category2.id}/l/latest`,
+ "it should transition to the category2's discovery page"
+ );
+
+ assert.strictEqual(
+ queryAll(".sidebar-section-categories .sidebar-section-link.active")
+ .length,
+ 1,
+ "only one link is marked as active"
+ );
+
+ assert.ok(
+ exists(`.sidebar-section-link-${category2.slug}.active`),
+ "the category2 section link is marked as active"
+ );
+ }
+ );
+
+ conditionalTest(
+ "visiting category discovery new route for tracked categories",
+ !isLegacyEmber(),
+ async function (assert) {
+ const { category1 } = setupTrackedCategories();
+
+ await visit(`/c/${category1.slug}/${category1.id}/l/new`);
+
+ assert.strictEqual(
+ queryAll(".sidebar-section-categories .sidebar-section-link.active")
+ .length,
+ 1,
+ "only one link is marked as active"
+ );
+
+ assert.ok(
+ exists(`.sidebar-section-link-${category1.slug}.active`),
+ "the category1 section link is marked as active for the new route"
+ );
+ }
+ );
+
+ conditionalTest(
+ "visiting category discovery unread route for tracked categories",
+ !isLegacyEmber(),
+ async function (assert) {
+ const { category1 } = setupTrackedCategories();
+
+ await visit(`/c/${category1.slug}/${category1.id}/l/unread`);
+
+ assert.strictEqual(
+ queryAll(".sidebar-section-categories .sidebar-section-link.active")
+ .length,
+ 1,
+ "only one link is marked as active"
+ );
+
+ assert.ok(
+ exists(`.sidebar-section-link-${category1.slug}.active`),
+ "the category1 section link is marked as active for the unread route"
+ );
+ }
+ );
+
+ conditionalTest(
+ "visiting category discovery top route for tracked categories",
+ !isLegacyEmber(),
+ async function (assert) {
+ const { category1 } = setupTrackedCategories();
+
+ await visit(`/c/${category1.slug}/${category1.id}/l/top`);
+
+ assert.strictEqual(
+ queryAll(".sidebar-section-categories .sidebar-section-link.active")
+ .length,
+ 1,
+ "only one link is marked as active"
+ );
+
+ assert.ok(
+ exists(`.sidebar-section-link-${category1.slug}.active`),
+ "the category1 section link is marked as active for the top route"
+ );
+ }
+ );
+
+ conditionalTest(
+ "updating category notification level",
+ !isLegacyEmber(),
+ async function (assert) {
+ const { category1, category2 } = setupTrackedCategories();
+
+ await visit(`/c/${category1.slug}/${category1.id}/l/top`);
+
+ assert.ok(
+ exists(`.sidebar-section-link-${category1.slug}`),
+ `has ${category1.name} section link in sidebar`
+ );
+
+ assert.ok(
+ exists(`.sidebar-section-link-${category2.slug}`),
+ `has ${category2.name} section link in sidebar`
+ );
+
+ const notificationLevelsDropdown = selectKit(".notifications-button");
+
+ await notificationLevelsDropdown.expand();
+
+ await notificationLevelsDropdown.selectRowByValue(
+ NotificationLevels.REGULAR
+ );
+
+ assert.ok(
+ !exists(`.sidebar-section-link-${category1.slug}`),
+ `does not have ${category1.name} section link in sidebar`
+ );
+
+ assert.ok(
+ exists(`.sidebar-section-link-${category2.slug}`),
+ `has ${category2.name} section link in sidebar`
+ );
+
+ await notificationLevelsDropdown.expand();
+
+ await notificationLevelsDropdown.selectRowByValue(
+ NotificationLevels.TRACKING
+ );
+
+ assert.ok(
+ exists(`.sidebar-section-link-${category1.slug}`),
+ `has ${category1.name} section link in sidebar`
+ );
+
+ assert.ok(
+ exists(`.sidebar-section-link-${category2.slug}`),
+ `has ${category2.name} section link in sidebar`
+ );
+ }
+ );
});
diff --git a/app/assets/stylesheets/common/base/sidebar.scss b/app/assets/stylesheets/common/base/sidebar.scss
index 7ede8a5bf5e..cde1de56422 100644
--- a/app/assets/stylesheets/common/base/sidebar.scss
+++ b/app/assets/stylesheets/common/base/sidebar.scss
@@ -103,6 +103,21 @@
color: var(--primary);
font-weight: bold;
}
+
+ .badge-wrapper {
+ font-size: 100%;
+ }
+ }
+
+ .sidebar-section-message-wrapper {
+ display: flex;
+ margin-left: 1.5em;
+ }
+
+ .sidebar-section-message {
+ padding: 0.25em 0.5em;
+ font-size: var(--font-down-1);
+ color: var(--primary-high);
}
.sidebar-section-link-content-badge {
diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml
index 5f1cd46e365..1260392cb0e 100644
--- a/config/locales/client.en.yml
+++ b/config/locales/client.en.yml
@@ -4042,6 +4042,7 @@ en:
new_count: "%{count} new"
sections:
categories:
+ no_tracked_categories: "You are not tracking any categories."
header_link_title: "all categories"
header_link_text: "Categories"
topics: