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.
This commit is contained in:
parent
03f674070a
commit
cd8c97debc
|
@ -1,3 +1,10 @@
|
||||||
import GlimmerComponent from "discourse/components/glimmer";
|
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 });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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 }));
|
||||||
|
}
|
||||||
|
}
|
|
@ -187,6 +187,11 @@ const Category = RestModel.extend({
|
||||||
return seconds ? seconds / 60 : null;
|
return seconds ? seconds / 60 : null;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@discourseComputed("notification_level")
|
||||||
|
isTracked(notificationLevel) {
|
||||||
|
return notificationLevel >= NotificationLevels.TRACKING;
|
||||||
|
},
|
||||||
|
|
||||||
save() {
|
save() {
|
||||||
const id = this.id;
|
const id = this.id;
|
||||||
const url = id ? `/categories/${id}` : "/categories";
|
const url = id ? `/categories/${id}` : "/categories";
|
||||||
|
|
|
@ -11,7 +11,6 @@ import discourseComputed from "discourse-common/utils/decorators";
|
||||||
import { getOwner } from "discourse-common/lib/get-owner";
|
import { getOwner } from "discourse-common/lib/get-owner";
|
||||||
import { isEmpty } from "@ember/utils";
|
import { isEmpty } from "@ember/utils";
|
||||||
import { htmlSafe } from "@ember/template";
|
import { htmlSafe } from "@ember/template";
|
||||||
import { NotificationLevels } from "discourse/lib/notification-levels";
|
|
||||||
|
|
||||||
const Site = RestModel.extend({
|
const Site = RestModel.extend({
|
||||||
isReadOnly: alias("is_readonly"),
|
isReadOnly: alias("is_readonly"),
|
||||||
|
@ -84,12 +83,12 @@ const Site = RestModel.extend({
|
||||||
: this.sortedCategories;
|
: this.sortedCategories;
|
||||||
},
|
},
|
||||||
|
|
||||||
@discourseComputed("categories.[]")
|
@discourseComputed("categories.[]", "categories.@each.notification_level")
|
||||||
trackedCategoriesList(categories) {
|
trackedCategoriesList(categories) {
|
||||||
const trackedCategories = [];
|
const trackedCategories = [];
|
||||||
|
|
||||||
for (const category of categories) {
|
for (const category of categories) {
|
||||||
if (category.notification_level >= NotificationLevels.TRACKING) {
|
if (category.isTracked) {
|
||||||
trackedCategories.push(category);
|
trackedCategories.push(category);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,4 +4,21 @@
|
||||||
@headerLinkText={{i18n "sidebar.sections.categories.header_link_text"}}
|
@headerLinkText={{i18n "sidebar.sections.categories.header_link_text"}}
|
||||||
@headerLinkTitle={{i18n "sidebar.sections.categories.header_link_title"}} >
|
@headerLinkTitle={{i18n "sidebar.sections.categories.header_link_title"}} >
|
||||||
|
|
||||||
|
{{#if (gt this.sectionLinks.length 0)}}
|
||||||
|
{{#each this.sectionLinks as |sectionLink|}}
|
||||||
|
<Sidebar::SectionLink
|
||||||
|
@linkName={{sectionLink.name}}
|
||||||
|
@route={{sectionLink.route}}
|
||||||
|
@query={{sectionLink.query}}
|
||||||
|
@title={{sectionLink.title}}
|
||||||
|
@content={{sectionLink.text}}
|
||||||
|
@currentWhen={{sectionLink.currentWhen}}
|
||||||
|
@model={{sectionLink.model}}>
|
||||||
|
</Sidebar::SectionLink>
|
||||||
|
{{/each}}
|
||||||
|
{{else}}
|
||||||
|
<Sidebar::SectionMessage>
|
||||||
|
{{i18n "sidebar.sections.categories.no_tracked_categories"}}
|
||||||
|
</Sidebar::SectionMessage>
|
||||||
|
{{/if}}
|
||||||
</Sidebar::Section>
|
</Sidebar::Section>
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
<div class="sidebar-section-message-wrapper">
|
||||||
|
<div class="sidebar-section-message">
|
||||||
|
{{yield}}
|
||||||
|
</div>
|
||||||
|
</div>
|
|
@ -1,14 +1,53 @@
|
||||||
|
import I18n from "I18n";
|
||||||
|
|
||||||
import { click, currentURL, visit } from "@ember/test-helpers";
|
import { click, currentURL, visit } from "@ember/test-helpers";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
acceptance,
|
acceptance,
|
||||||
conditionalTest,
|
conditionalTest,
|
||||||
|
exists,
|
||||||
|
query,
|
||||||
|
queryAll,
|
||||||
} from "discourse/tests/helpers/qunit-helpers";
|
} from "discourse/tests/helpers/qunit-helpers";
|
||||||
|
import selectKit from "discourse/tests/helpers/select-kit-helper";
|
||||||
import { isLegacyEmber } from "discourse-common/config/environment";
|
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) {
|
acceptance("Sidebar - Categories Section", function (needs) {
|
||||||
needs.user({ experimental_sidebar_enabled: true });
|
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(
|
conditionalTest(
|
||||||
"clicking on section header link",
|
"clicking on section header link",
|
||||||
!isLegacyEmber(),
|
!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`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
|
@ -103,6 +103,21 @@
|
||||||
color: var(--primary);
|
color: var(--primary);
|
||||||
font-weight: bold;
|
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 {
|
.sidebar-section-link-content-badge {
|
||||||
|
|
|
@ -4042,6 +4042,7 @@ en:
|
||||||
new_count: "%{count} new"
|
new_count: "%{count} new"
|
||||||
sections:
|
sections:
|
||||||
categories:
|
categories:
|
||||||
|
no_tracked_categories: "You are not tracking any categories."
|
||||||
header_link_title: "all categories"
|
header_link_title: "all categories"
|
||||||
header_link_text: "Categories"
|
header_link_text: "Categories"
|
||||||
topics:
|
topics:
|
||||||
|
|
Loading…
Reference in New Issue