DEV: Experiment plugin api to add custom count to category section link
This commit introduces the experimental `registerUserCategorySectionLinkCountable` and `refreshUserSidebarCategoriesSectionCounts` plugin APIs that allows a plugin to register custom countables to category section links on top of the defaults of unread and new.
This commit is contained in:
parent
c46cd1bd04
commit
f71e3c07dd
|
@ -1,28 +1,53 @@
|
|||
import { inject as service } from "@ember/service";
|
||||
import { action } from "@ember/object";
|
||||
import Category from "discourse/models/category";
|
||||
import { cached } from "@glimmer/tracking";
|
||||
|
||||
import Category from "discourse/models/category";
|
||||
import SidebarCommonCategoriesSection from "discourse/components/sidebar/common/categories-section";
|
||||
import discourseDebounce from "discourse-common/lib/debounce";
|
||||
|
||||
export const REFRESH_COUNTS_APP_EVENT_NAME =
|
||||
"sidebar:refresh-categories-section-counts";
|
||||
|
||||
export default class SidebarUserCategoriesSection extends SidebarCommonCategoriesSection {
|
||||
@service router;
|
||||
@service currentUser;
|
||||
@service appEvents;
|
||||
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
|
||||
this.callbackId = this.topicTrackingState.onStateChange(() => {
|
||||
this.sectionLinks.forEach((sectionLink) => {
|
||||
sectionLink.refreshCounts();
|
||||
});
|
||||
this.#refreshCounts();
|
||||
});
|
||||
|
||||
this.appEvents.on(REFRESH_COUNTS_APP_EVENT_NAME, this, this.#refreshCounts);
|
||||
}
|
||||
|
||||
willDestroy() {
|
||||
super.willDestroy(...arguments);
|
||||
|
||||
this.topicTrackingState.offStateChange(this.callbackId);
|
||||
|
||||
this.appEvents.off(
|
||||
REFRESH_COUNTS_APP_EVENT_NAME,
|
||||
this,
|
||||
this.#refreshCounts
|
||||
);
|
||||
}
|
||||
|
||||
#refreshCounts() {
|
||||
// TopicTrackingState changes or plugins can trigger this function so we debounce to ensure we're not refreshing
|
||||
// unnecessarily.
|
||||
discourseDebounce(
|
||||
this,
|
||||
() => {
|
||||
this.sectionLinks.forEach((sectionLink) => {
|
||||
sectionLink.refreshCounts();
|
||||
});
|
||||
},
|
||||
300
|
||||
);
|
||||
}
|
||||
|
||||
@cached
|
||||
|
|
|
@ -104,6 +104,8 @@ import { downloadCalendar } from "discourse/lib/download-calendar";
|
|||
import { consolePrefix } from "discourse/lib/source-identifier";
|
||||
import { addSectionLink as addCustomCommunitySectionLink } from "discourse/lib/sidebar/custom-community-section-links";
|
||||
import { addSidebarSection } from "discourse/lib/sidebar/custom-sections";
|
||||
import { registerCustomCountable as registerUserCategorySectionLinkCountable } from "discourse/lib/sidebar/user/categories-section/category-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";
|
||||
import { registerUserMenuTab } from "discourse/lib/user-menu/tab";
|
||||
|
@ -1809,6 +1811,87 @@ class PluginApi {
|
|||
addCustomCommunitySectionLink(arg, secondary);
|
||||
}
|
||||
|
||||
/**
|
||||
* EXPERIMENTAL. Do not use.
|
||||
* Registers a new countable for section links under Sidebar Categories section on top of the default countables of
|
||||
* unread topics count and new topics count.
|
||||
*
|
||||
* ```
|
||||
* api.registerUserCategorySectionLinkCountable({
|
||||
* badgeTextFunction: (count) => {
|
||||
* return I18n.t("custom.open_count", count: count");
|
||||
* },
|
||||
* route: "discovery.openCategory",
|
||||
* shouldRegister: ({ category } => {
|
||||
* return category.custom_fields.enable_open_topics_count;
|
||||
* }),
|
||||
* refreshCountFunction: ({ _topicTrackingState, category } => {
|
||||
* return category.open_topics_count;
|
||||
* }),
|
||||
* prioritizeDefaults: ({ currentUser, category } => {
|
||||
* return category.custom_fields.show_open_topics_count_first;
|
||||
* })
|
||||
* })
|
||||
* ```
|
||||
*
|
||||
* @callback badgeTextFunction
|
||||
* @param {Integer} count - The count as given by the `refreshCountFunction`.
|
||||
* @returns {String} - Text for the badge displayed in the section link.
|
||||
*
|
||||
* @callback shouldRegister
|
||||
* @param {Object} arg
|
||||
* @param {Category} arg.category - The category model for the sidebar section link.
|
||||
* @returns {Boolean} - Whether the countable should be registered for the sidebar section link.
|
||||
*
|
||||
* @callback refreshCountFunction
|
||||
* @param {Object} arg
|
||||
* @param {Category} arg.category - The category model for the sidebar section link.
|
||||
* @returns {integer} - The value used to set the property for the count.
|
||||
*
|
||||
* @callback prioritizeOverDefaults
|
||||
* @param {Object} arg
|
||||
* @param {Category} arg.category - The category model for the sidebar section link.
|
||||
* @param {User} arg.currentUser - The user model for the current user.
|
||||
* @returns {boolean} - Whether the countable should be prioritized over the defaults.
|
||||
*
|
||||
* @param {Object} arg - An object
|
||||
* @param {string} arg.badgeTextFunction - Function used to generate the text for the badge displayed in the section link.
|
||||
* @param {string} arg.route - The Ember route name to generate the href attribute for the link.
|
||||
* @param {Object=} arg.routeQuery - Object representing the query params that should be appended to the route generated.
|
||||
* @param {shouldRegister} arg.shouldRegister - Function used to determine if the countable should be registered for the category.
|
||||
* @param {refreshCountFunction} arg.refreshCountFunction - Function used to calculate the value used to set the property for the count whenever the sidebar section link refreshes.
|
||||
* @param {prioritizeOverDefaults} args.prioritizeOverDefaults - Function used to determine whether the countable should be prioritized over the default countables of unread/new.
|
||||
*/
|
||||
registerUserCategorySectionLinkCountable({
|
||||
badgeTextFunction,
|
||||
route,
|
||||
routeQuery,
|
||||
shouldRegister,
|
||||
refreshCountFunction,
|
||||
prioritizeOverDefaults,
|
||||
}) {
|
||||
registerUserCategorySectionLinkCountable({
|
||||
badgeTextFunction,
|
||||
route,
|
||||
routeQuery,
|
||||
shouldRegister,
|
||||
refreshCountFunction,
|
||||
prioritizeOverDefaults,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* EXPERIMENTAL. Do not use.
|
||||
* Triggers a refresh of the counts for all category section links under the categories section for a logged in user.
|
||||
*/
|
||||
refreshUserSidebarCategoriesSectionCounts() {
|
||||
const appEvents = this._lookupContainer("service:app-events");
|
||||
|
||||
appEvents?.trigger(
|
||||
REFRESH_USER_SIDEBAR_CATEGORIES_SECTION_COUNTS_APP_EVENT_NAME
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* EXPERIMENTAL. Do not use.
|
||||
* Support for adding a Sidebar section by returning a class which extends from the BaseCustomSidebarSection
|
||||
|
|
|
@ -1,35 +1,121 @@
|
|||
import I18n from "I18n";
|
||||
|
||||
import { tracked } from "@glimmer/tracking";
|
||||
import { get, set } from "@ember/object";
|
||||
|
||||
import { bind } from "discourse-common/utils/decorators";
|
||||
import Category from "discourse/models/category";
|
||||
import { UNREAD_LIST_DESTINATION } from "discourse/controllers/preferences/sidebar";
|
||||
|
||||
const DEFAULT_COUNTABLES = [
|
||||
{
|
||||
propertyName: "totalUnread",
|
||||
badgeTextFunction: (count) => {
|
||||
return I18n.t("sidebar.unread_count", { count });
|
||||
},
|
||||
route: "discovery.unreadCategory",
|
||||
refreshCountFunction: ({ topicTrackingState, category }) => {
|
||||
return topicTrackingState.countUnread({
|
||||
categoryId: category.id,
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
propertyName: "totalNew",
|
||||
badgeTextFunction: (count) => {
|
||||
return I18n.t("sidebar.new_count", { count });
|
||||
},
|
||||
route: "discovery.newCategory",
|
||||
refreshCountFunction: ({ topicTrackingState, category }) => {
|
||||
return topicTrackingState.countNew({
|
||||
categoryId: category.id,
|
||||
});
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const customCountables = [];
|
||||
|
||||
export function registerCustomCountable({
|
||||
badgeTextFunction,
|
||||
route,
|
||||
routeQuery,
|
||||
shouldRegister,
|
||||
refreshCountFunction,
|
||||
prioritizeOverDefaults,
|
||||
}) {
|
||||
const length = customCountables.length + 1;
|
||||
|
||||
customCountables.push({
|
||||
propertyName: `customCountableProperty${length}`,
|
||||
badgeTextFunction,
|
||||
route,
|
||||
routeQuery,
|
||||
shouldRegister,
|
||||
refreshCountFunction,
|
||||
prioritizeOverDefaults,
|
||||
});
|
||||
}
|
||||
|
||||
export function resetCustomCountables() {
|
||||
customCountables.length = 0;
|
||||
}
|
||||
|
||||
export default class CategorySectionLink {
|
||||
@tracked totalUnread = 0;
|
||||
@tracked totalNew = 0;
|
||||
@tracked hideCount =
|
||||
this.currentUser?.sidebarListDestination !== UNREAD_LIST_DESTINATION;
|
||||
@tracked activeCountable;
|
||||
|
||||
constructor({ category, topicTrackingState, currentUser }) {
|
||||
this.category = category;
|
||||
this.topicTrackingState = topicTrackingState;
|
||||
this.currentUser = currentUser;
|
||||
this.countables = this.#countables();
|
||||
|
||||
this.refreshCounts();
|
||||
}
|
||||
|
||||
#countables() {
|
||||
const countables = [...DEFAULT_COUNTABLES];
|
||||
|
||||
if (customCountables.length > 0) {
|
||||
customCountables.forEach((customCountable) => {
|
||||
if (
|
||||
!customCountable.shouldRegister ||
|
||||
customCountable.shouldRegister({ category: this.category })
|
||||
) {
|
||||
if (
|
||||
customCountable?.prioritizeOverDefaults({
|
||||
category: this.category,
|
||||
currentUser: this.currentUser,
|
||||
})
|
||||
) {
|
||||
countables.unshift(customCountable);
|
||||
} else {
|
||||
countables.push(customCountable);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return countables;
|
||||
}
|
||||
|
||||
get hideCount() {
|
||||
return this.currentUser?.sidebarListDestination !== UNREAD_LIST_DESTINATION;
|
||||
}
|
||||
|
||||
@bind
|
||||
refreshCounts() {
|
||||
this.totalUnread = this.topicTrackingState.countUnread({
|
||||
categoryId: this.category.id,
|
||||
});
|
||||
this.countables = this.#countables();
|
||||
|
||||
if (this.totalUnread === 0) {
|
||||
this.totalNew = this.topicTrackingState.countNew({
|
||||
categoryId: this.category.id,
|
||||
this.activeCountable = this.countables.find((countable) => {
|
||||
const count = countable.refreshCountFunction({
|
||||
topicTrackingState: this.topicTrackingState,
|
||||
category: this.category,
|
||||
});
|
||||
}
|
||||
|
||||
set(this, countable.propertyName, count);
|
||||
return count > 0;
|
||||
});
|
||||
}
|
||||
|
||||
get name() {
|
||||
|
@ -74,29 +160,38 @@ export default class CategorySectionLink {
|
|||
if (this.hideCount) {
|
||||
return;
|
||||
}
|
||||
if (this.totalUnread > 0) {
|
||||
return I18n.t("sidebar.unread_count", {
|
||||
count: this.totalUnread,
|
||||
});
|
||||
} else if (this.totalNew > 0) {
|
||||
return I18n.t("sidebar.new_count", {
|
||||
count: this.totalNew,
|
||||
});
|
||||
|
||||
const activeCountable = this.activeCountable;
|
||||
|
||||
if (activeCountable) {
|
||||
return activeCountable.badgeTextFunction(
|
||||
get(this, activeCountable.propertyName)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
get route() {
|
||||
if (this.currentUser?.sidebarListDestination === UNREAD_LIST_DESTINATION) {
|
||||
if (this.totalUnread > 0) {
|
||||
return "discovery.unreadCategory";
|
||||
}
|
||||
if (this.totalNew > 0) {
|
||||
return "discovery.newCategory";
|
||||
const activeCountable = this.activeCountable;
|
||||
|
||||
if (activeCountable) {
|
||||
return activeCountable.route;
|
||||
}
|
||||
}
|
||||
|
||||
return "discovery.category";
|
||||
}
|
||||
|
||||
get query() {
|
||||
if (this.currentUser?.sidebarListDestination === UNREAD_LIST_DESTINATION) {
|
||||
const activeCountable = this.activeCountable;
|
||||
|
||||
if (activeCountable?.routeQuery) {
|
||||
return activeCountable.routeQuery;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
get suffixCSSClass() {
|
||||
return "unread";
|
||||
}
|
||||
|
@ -106,7 +201,7 @@ export default class CategorySectionLink {
|
|||
}
|
||||
|
||||
get suffixValue() {
|
||||
if (this.hideCount && (this.totalUnread || this.totalNew)) {
|
||||
if (this.hideCount && this.activeCountable) {
|
||||
return "circle";
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,13 +1,17 @@
|
|||
import { test } from "qunit";
|
||||
import I18n from "I18n";
|
||||
import { click, visit } from "@ember/test-helpers";
|
||||
import { click, settled, visit } from "@ember/test-helpers";
|
||||
import {
|
||||
acceptance,
|
||||
exists,
|
||||
query,
|
||||
queryAll,
|
||||
updateCurrentUser,
|
||||
} from "discourse/tests/helpers/qunit-helpers";
|
||||
import { withPluginApi } from "discourse/lib/plugin-api";
|
||||
import Site from "discourse/models/site";
|
||||
import { resetCustomCountables } from "discourse/lib/sidebar/user/categories-section/category-section-link";
|
||||
import { UNREAD_LIST_DESTINATION } from "discourse/controllers/preferences/sidebar";
|
||||
import { bind } from "discourse-common/utils/decorators";
|
||||
|
||||
acceptance("Sidebar - Plugin API", function (needs) {
|
||||
|
@ -629,4 +633,115 @@ acceptance("Sidebar - Plugin API", function (needs) {
|
|||
"does not display the section"
|
||||
);
|
||||
});
|
||||
|
||||
test("Registering a custom countable for a section link in the user's sidebar categories section", async function (assert) {
|
||||
try {
|
||||
return await withPluginApi("1.6.0", async (api) => {
|
||||
const categories = Site.current().categories;
|
||||
const category1 = categories[0];
|
||||
const category2 = categories[1];
|
||||
|
||||
updateCurrentUser({
|
||||
sidebar_category_ids: [category1.id, category2.id],
|
||||
});
|
||||
|
||||
// User has one unread topic
|
||||
this.container.lookup("service:topic-tracking-state").loadStates([
|
||||
{
|
||||
topic_id: 2,
|
||||
highest_post_number: 12,
|
||||
last_read_post_number: 11,
|
||||
created_at: "2020-02-09T09:40:02.672Z",
|
||||
category_id: category1.id,
|
||||
notification_level: 2,
|
||||
created_in_new_period: false,
|
||||
treat_as_new_topic_start_date: "2022-05-09T03:17:34.286Z",
|
||||
},
|
||||
]);
|
||||
|
||||
api.registerUserCategorySectionLinkCountable({
|
||||
badgeTextFunction: (count) => {
|
||||
return `some custom ${count}`;
|
||||
},
|
||||
route: "discovery.latestCategory",
|
||||
routeQuery: { status: "open" },
|
||||
shouldRegister: ({ category }) => {
|
||||
if (category.name === category1.name) {
|
||||
return true;
|
||||
} else if (category.name === category2.name) {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
refreshCountFunction: ({ category }) => {
|
||||
return category.topic_count;
|
||||
},
|
||||
prioritizeOverDefaults: ({ category }) => {
|
||||
return category.topic_count > 1000;
|
||||
},
|
||||
});
|
||||
|
||||
await visit("/");
|
||||
|
||||
assert.ok(
|
||||
exists(
|
||||
`.sidebar-section-link-${category1.name} .sidebar-section-link-suffix.unread`
|
||||
),
|
||||
"the right suffix is displayed when custom countable is active"
|
||||
);
|
||||
|
||||
assert.strictEqual(
|
||||
query(`.sidebar-section-link-${category1.name}`).pathname,
|
||||
`/c/${category1.name}/${category1.id}`,
|
||||
"does not use route configured for custom countable when user has elected not to show any counts in sidebar"
|
||||
);
|
||||
|
||||
assert.notOk(
|
||||
exists(
|
||||
`.sidebar-section-link-${category2.name} .sidebar-section-link-suffix.unread`
|
||||
),
|
||||
"does not display suffix when custom countable is not registered"
|
||||
);
|
||||
|
||||
updateCurrentUser({
|
||||
sidebar_list_destination: UNREAD_LIST_DESTINATION,
|
||||
});
|
||||
|
||||
assert.strictEqual(
|
||||
query(
|
||||
`.sidebar-section-link-${category1.name} .sidebar-section-link-content-badge`
|
||||
).innerText.trim(),
|
||||
I18n.t("sidebar.unread_count", { count: 1 }),
|
||||
"displays the right badge text in section link when unread is present and custom countable is not prioritised over unread"
|
||||
);
|
||||
|
||||
category1.set("topic_count", 2000);
|
||||
|
||||
api.refreshUserSidebarCategoriesSectionCounts();
|
||||
|
||||
await settled();
|
||||
|
||||
assert.strictEqual(
|
||||
query(
|
||||
`.sidebar-section-link-${category1.name} .sidebar-section-link-content-badge`
|
||||
).innerText.trim(),
|
||||
`some custom ${category1.topic_count}`,
|
||||
"displays the right badge text in section link when unread is present but custom countable is prioritised over unread"
|
||||
);
|
||||
|
||||
assert.strictEqual(
|
||||
query(`.sidebar-section-link-${category1.name}`).pathname,
|
||||
`/c/${category1.name}/${category1.id}/l/latest`,
|
||||
"has the right pathname for section link"
|
||||
);
|
||||
|
||||
assert.strictEqual(
|
||||
query(`.sidebar-section-link-${category1.name}`).search,
|
||||
"?status=open",
|
||||
"has the right query params for section link"
|
||||
);
|
||||
});
|
||||
} finally {
|
||||
resetCustomCountables();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue