From cd8c97debcdebc37303d1443905ed11606a2d038 Mon Sep 17 00:00:00 2001 From: Alan Guo Xiang Tan Date: Thu, 9 Jun 2022 11:14:01 +0800 Subject: [PATCH] FEATURE: Add section links to categories section to exp sidebar (#17035) This commit adds a section link to the categories section for each category that is tracked by the user in the experimental sidebar. --- .../components/sidebar/categories-section.js | 9 +- .../category-section-link.js | 34 +++ .../discourse/app/models/category.js | 5 + .../javascripts/discourse/app/models/site.js | 5 +- .../components/sidebar/categories-section.hbs | 17 ++ .../components/sidebar/section-message.hbs | 5 + .../sidebar-categories-section-test.js | 240 ++++++++++++++++++ .../stylesheets/common/base/sidebar.scss | 15 ++ config/locales/client.en.yml | 1 + 9 files changed, 327 insertions(+), 4 deletions(-) create mode 100644 app/assets/javascripts/discourse/app/lib/sidebar/categories-section/category-section-link.js create mode 100644 app/assets/javascripts/discourse/app/templates/components/sidebar/section-message.hbs 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: