DEV: Plugin API for plugins to add links to sidebar topics section (#16732)
This commit is contained in:
parent
072faa08bb
commit
f589d05cf9
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 = [];
|
||||
}
|
|
@ -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";
|
||||
}
|
||||
}
|
|
@ -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");
|
||||
}
|
||||
}
|
|
@ -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";
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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");
|
||||
}
|
||||
}
|
|
@ -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}}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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"
|
||||
);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
|
|
@ -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) {
|
||||
|
|
Loading…
Reference in New Issue