DEV: Plugin API for plugins to add links to sidebar topics section (#16732)

This commit is contained in:
Alan Guo Xiang Tan 2022-05-25 15:54:32 +08:00 committed by GitHub
parent 072faa08bb
commit f589d05cf9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 344 additions and 82 deletions

View File

@ -1,72 +1,31 @@
import I18n from "I18n";
import GlimmerComponent from "discourse/components/glimmer";
import Composer from "discourse/models/composer";
import { getOwner } from "discourse-common/lib/get-owner";
import PermissionType from "discourse/models/permission-type";
import discourseDebounce from "discourse-common/lib/debounce";
import { customSectionLinks } from "discourse/lib/sidebar/custom-topics-section-links";
import EverythingSectionLink from "discourse/lib/sidebar/topics-section/everything-section-link";
import TrackedSectionLink from "discourse/lib/sidebar/topics-section/tracked-section-link";
import BookmarkedSectionLink from "discourse/lib/sidebar/topics-section/bookmarked-section-link";
import { action } from "@ember/object";
import { next } from "@ember/runloop";
import { tracked } from "@glimmer/tracking";
const DEFAULT_SECTION_LINKS = [
EverythingSectionLink,
TrackedSectionLink,
BookmarkedSectionLink,
];
export default class SidebarTopicsSection extends GlimmerComponent {
@tracked totalUnread = 0;
@tracked totalNew = 0;
constructor(owner, args) {
super(owner, args);
this._refreshSectionCounts();
this.topicTrackingState.onStateChange(
this._topicTrackingStateUpdated.bind(this)
);
}
_topicTrackingStateUpdated() {
// refreshing section counts by looping through the states in topicTrackingState is an expensive operation so
// we debounce this.
discourseDebounce(this, this._refreshSectionCounts, 100);
}
_refreshSectionCounts() {
let totalUnread = 0;
let totalNew = 0;
this.topicTrackingState.forEachTracked((topic, isNew, isUnread) => {
if (isNew) {
totalNew += 1;
} else if (isUnread) {
totalUnread += 1;
get sectionLinks() {
return [...DEFAULT_SECTION_LINKS, ...customSectionLinks].map(
(sectionLinkClass) => {
return new sectionLinkClass({
topicTrackingState: this.topicTrackingState,
currentUser: this.currentUser,
});
}
});
this.totalUnread = totalUnread;
this.totalNew = totalNew;
}
get everythingSectionLinkBadgeText() {
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,
});
} else {
return;
}
}
get everythingSectionLinkRoute() {
if (this.totalUnread > 0) {
return "discovery.unread";
} else if (this.totalNew > 0) {
return "discovery.new";
} else {
return "discovery.latest";
}
);
}
@action

View File

@ -94,6 +94,7 @@ import {
import { CUSTOM_USER_SEARCH_OPTIONS } from "select-kit/components/user-chooser";
import { downloadCalendar } from "discourse/lib/download-calendar";
import { consolePrefix } from "discourse/lib/source-identifier";
import { addSectionLink } from "discourse/lib/sidebar/custom-topics-section-links";
// If you add any methods to the API ensure you bump up the version number
// based on Semantic Versioning 2.0.0. Please update the changelog at
@ -1622,6 +1623,44 @@ class PluginApi {
customizeComposerText(callbacks) {
registerCustomizationCallback(callbacks);
}
/**
* EXPERIMENTAL. Do not use.
* Support for adding a link under Sidebar topics section by returning a class which extends from the BaseSectionLink
* class interface. See `lib/sidebar/topics-section/base-section-link.js` for documentation on the BaseSectionLink class
* interface.
*
* ```
* api.addTopicsSectionLink((baseSectionLink) => {
* return class CustomSectionLink extends baseSectionLink {
* get name() {
* returns "bookmarked"
* }
*
* get route() {
* returns "userActivity.bookmarks"
* }
*
* get title() {
* return I18n.t("sidebar.sections.topics.links.bookmarked.title");
* }
*
* get text() {
* return I18n.t("sidebar.sections.topics.links.bookmarked.content");
* }
* }
* })
* ```
*
* @callback addTopicsSectionLinkCallback
* @param {BaseSectionLink} baseSectionLink Factory class to inherit from.
* @returns {BaseSectionLink} A class that extends BaseSectionLink.
*
* @param {addTopicsSectionLinkCallback} callback
*/
async addTopicsSectionLink(callback) {
addSectionLink(callback);
}
}
// from http://stackoverflow.com/questions/6832596/how-to-compare-software-version-number-using-js-only-number

View File

@ -0,0 +1,19 @@
import BaseSectionLink from "discourse/lib/sidebar/topics-section/base-section-link";
export let customSectionLinks = [];
/**
* Appends an additional section link under the topics section
* @callback addSectionLinkCallback
* @param {BaseSectionLink} baseSectionLink Factory class to inherit from.
* @returns {BaseSectionLink} A class that extends BaseSectionLink.
*
* @param {addTopicsSectionLinkCallback} callback
*/
export function addSectionLink(callback) {
customSectionLinks.push(callback.call(this, BaseSectionLink));
}
export function resetDefaultSectionLinks() {
customSectionLinks = [];
}

View File

@ -0,0 +1,63 @@
/**
* Base class representing a sidebar topics section link interface.
*/
export default class BaseSectionLink {
constructor({ topicTrackingState, currentUser } = {}) {
this.topicTrackingState = topicTrackingState;
this.currentUser = currentUser;
}
/**
* @returns {string} The name of the section link
*/
get name() {
this._notImplemented();
}
/**
* @returns {string} Ember route
*/
get route() {
this._notImplemented();
}
/**
* @returns {Object} Model for <LinkTo> component. See https://api.emberjs.com/ember/release/classes/Ember.Templates.components/methods/LinkTo?anchor=LinkTo
*/
get model() {}
/**
* @returns {Object} Query parameters for <LinkTo> component. See https://api.emberjs.com/ember/release/classes/Ember.Templates.components/methods/LinkTo?anchor=LinkTo
*/
get query() {
return {};
}
/**
* @returns {String} current-when for <LinkTo> component. See https://api.emberjs.com/ember/release/classes/Ember.Templates.components/methods/LinkTo?anchor=LinkTo
*/
get currentWhen() {}
/**
* @returns {string} Title for the link
*/
get title() {
this._notImplemented();
}
/**
* @returns {string} Text for the link
*/
get text() {
this._notImplemented();
}
/**
* @returns {string} Text for the badge within the link
*/
get badgeText() {}
_notImplemented() {
throw "not implemented";
}
}

View File

@ -0,0 +1,25 @@
import I18n from "I18n";
import BaseSectionLink from "discourse/lib/sidebar/topics-section/base-section-link";
export default class BookmarkedSectionLink extends BaseSectionLink {
get name() {
return "bookmarked";
}
get route() {
return "userActivity.bookmarks";
}
get model() {
return this.currentUser;
}
get title() {
return I18n.t("sidebar.sections.topics.links.bookmarked.title");
}
get text() {
return I18n.t("sidebar.sections.topics.links.bookmarked.content");
}
}

View File

@ -0,0 +1,87 @@
import I18n from "I18n";
import { tracked } from "@glimmer/tracking";
import discourseDebounce from "discourse-common/lib/debounce";
import BaseSectionLink from "discourse/lib/sidebar/topics-section/base-section-link";
export default class EverythingSectionLink extends BaseSectionLink {
@tracked totalUnread = 0;
@tracked totalNew = 0;
constructor() {
super(...arguments);
this._refreshCounts();
this.topicTrackingState.onStateChange(
this._topicTrackingStateUpdated.bind(this)
);
}
_topicTrackingStateUpdated() {
// refreshing section counts by looping through the states in topicTrackingState is an expensive operation so
// we debounce this.
discourseDebounce(this, this._refreshCounts, 100);
}
_refreshCounts() {
let totalUnread = 0;
let totalNew = 0;
this.topicTrackingState.forEachTracked((topic, isNew, isUnread) => {
if (isNew) {
totalNew += 1;
} else if (isUnread) {
totalUnread += 1;
}
});
this.totalUnread = totalUnread;
this.totalNew = totalNew;
}
get name() {
return "everything";
}
get query() {
return { f: undefined };
}
get title() {
return I18n.t("sidebar.sections.topics.links.everything.title");
}
get text() {
return I18n.t("sidebar.sections.topics.links.everything.content");
}
get currentWhen() {
return "discovery.latest discovery.new discovery.unread discovery.top";
}
get badgeText() {
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,
});
} else {
return;
}
}
get route() {
if (this.totalUnread > 0) {
return "discovery.unread";
} else if (this.totalNew > 0) {
return "discovery.new";
} else {
return "discovery.latest";
}
}
}

View File

@ -0,0 +1,25 @@
import I18n from "I18n";
import BaseSectionLink from "discourse/lib/sidebar/topics-section/base-section-link";
export default class TrackedSectionLink extends BaseSectionLink {
get name() {
return "tracked";
}
get route() {
return "discovery.latest";
}
get query() {
return { f: "tracked" };
}
get title() {
return I18n.t("sidebar.sections.topics.links.tracked.title");
}
get text() {
return I18n.t("sidebar.sections.topics.links.tracked.content");
}
}

View File

@ -4,7 +4,7 @@
@route={{@route}}
@query={{@query}}
@models={{if @model (array @model) (if @models @models (array))}}
@current-when={{@current-when}}
@current-when={{@currentWhen}}
@title={{@title}}
>
{{@content}}

View File

@ -8,26 +8,15 @@
@headerAction={{this.composeTopic}}
@headerActionTitle={{i18n "sidebar.sections.topics.header_action_title"}}>
<Sidebar::SectionLink
@linkName="everything"
@route={{this.everythingSectionLinkRoute}}
@query={{hash f=undefined}}
@title={{i18n "sidebar.sections.topics.links.everything.title"}}
@content={{i18n "sidebar.sections.topics.links.everything.content"}}
@current-when={{"discovery.latest discovery.new discovery.unread discovery.top"}}
@badgeText={{this.everythingSectionLinkBadgeText}} />
<Sidebar::SectionLink
@linkName="tracked"
@route="discovery.latest"
@query={{hash f="tracked"}}
@title={{i18n "sidebar.sections.topics.links.tracked.title"}}
@content={{i18n "sidebar.sections.topics.links.tracked.content"}} />
<Sidebar::SectionLink
@linkName="bookmarked"
@route="userActivity.bookmarks"
@model={{this.currentUser}}
@title={{i18n "sidebar.sections.topics.links.bookmarked.title"}}
@content={{i18n "sidebar.sections.topics.links.bookmarked.content"}} />
{{#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}}
@badgeText={{sectionLink.badgeText}}
@model={{sectionLink.model}} />
{{/each}}
</Sidebar::Section>

View File

@ -12,6 +12,7 @@ import {
import { isLegacyEmber } from "discourse-common/config/environment";
import topicFixtures from "discourse/tests/fixtures/discovery-fixtures";
import { cloneJSON } from "discourse-common/lib/object";
import { withPluginApi } from "discourse/lib/plugin-api";
acceptance("Sidebar - Topics Section", function (needs) {
needs.user({ experimental_sidebar_enabled: true });
@ -407,4 +408,57 @@ acceptance("Sidebar - Topics Section", function (needs) {
);
}
);
conditionalTest(
"adding section link via plugin API",
!isLegacyEmber(),
async function (assert) {
withPluginApi("1.2.0", (api) => {
api.addTopicsSectionLink((baseSectionLink) => {
return class CustomSectionLink extends baseSectionLink {
get name() {
return "user-summary";
}
get route() {
return "user.summary";
}
get model() {
return this.currentUser;
}
get title() {
return `${this.currentUser.username} summary`;
}
get text() {
return "my summary";
}
};
});
});
await visit("/");
await click(".sidebar-section-link-user-summary");
assert.strictEqual(
currentURL(),
"/u/eviltrout/summary",
"links to the right URL"
);
assert.strictEqual(
query(".sidebar-section-link-user-summary").textContent.trim(),
"my summary",
"displays the right text for the link"
);
assert.strictEqual(
query(".sidebar-section-link-user-summary").title,
"eviltrout summary",
"displays the right title for the link"
);
}
);
});

View File

@ -63,6 +63,7 @@ import {
setTestPresence,
} from "discourse/lib/user-presence";
import PreloadStore from "discourse/lib/preload-store";
import { resetDefaultSectionLinks as resetTopicsSectionLinks } from "discourse/lib/sidebar/custom-topics-section-links";
const LEGACY_ENV = !setupApplicationTest;
@ -186,6 +187,7 @@ function testCleanup(container, app) {
clearPresenceCallbacks();
}
restoreBaseUri();
resetTopicsSectionLinks();
}
export function discourseModule(name, options) {