mirror of
https://github.com/discourse/discourse.git
synced 2025-02-25 18:55:32 -06:00
DEV: Plugin API for plugins to add links to sidebar topics section (#16732)
This commit is contained in:
committed by
GitHub
parent
072faa08bb
commit
f589d05cf9
@@ -1,72 +1,31 @@
|
|||||||
import I18n from "I18n";
|
|
||||||
|
|
||||||
import GlimmerComponent from "discourse/components/glimmer";
|
import GlimmerComponent from "discourse/components/glimmer";
|
||||||
import Composer from "discourse/models/composer";
|
import Composer from "discourse/models/composer";
|
||||||
import { getOwner } from "discourse-common/lib/get-owner";
|
import { getOwner } from "discourse-common/lib/get-owner";
|
||||||
import PermissionType from "discourse/models/permission-type";
|
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 { action } from "@ember/object";
|
||||||
import { next } from "@ember/runloop";
|
import { next } from "@ember/runloop";
|
||||||
import { tracked } from "@glimmer/tracking";
|
|
||||||
|
const DEFAULT_SECTION_LINKS = [
|
||||||
|
EverythingSectionLink,
|
||||||
|
TrackedSectionLink,
|
||||||
|
BookmarkedSectionLink,
|
||||||
|
];
|
||||||
|
|
||||||
export default class SidebarTopicsSection extends GlimmerComponent {
|
export default class SidebarTopicsSection extends GlimmerComponent {
|
||||||
@tracked totalUnread = 0;
|
get sectionLinks() {
|
||||||
@tracked totalNew = 0;
|
return [...DEFAULT_SECTION_LINKS, ...customSectionLinks].map(
|
||||||
|
(sectionLinkClass) => {
|
||||||
constructor(owner, args) {
|
return new sectionLinkClass({
|
||||||
super(owner, args);
|
topicTrackingState: this.topicTrackingState,
|
||||||
this._refreshSectionCounts();
|
currentUser: this.currentUser,
|
||||||
|
});
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
});
|
);
|
||||||
|
|
||||||
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
|
@action
|
||||||
|
|||||||
@@ -94,6 +94,7 @@ import {
|
|||||||
import { CUSTOM_USER_SEARCH_OPTIONS } from "select-kit/components/user-chooser";
|
import { CUSTOM_USER_SEARCH_OPTIONS } from "select-kit/components/user-chooser";
|
||||||
import { downloadCalendar } from "discourse/lib/download-calendar";
|
import { downloadCalendar } from "discourse/lib/download-calendar";
|
||||||
import { consolePrefix } from "discourse/lib/source-identifier";
|
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
|
// 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
|
// based on Semantic Versioning 2.0.0. Please update the changelog at
|
||||||
@@ -1622,6 +1623,44 @@ class PluginApi {
|
|||||||
customizeComposerText(callbacks) {
|
customizeComposerText(callbacks) {
|
||||||
registerCustomizationCallback(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
|
// 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}}
|
@route={{@route}}
|
||||||
@query={{@query}}
|
@query={{@query}}
|
||||||
@models={{if @model (array @model) (if @models @models (array))}}
|
@models={{if @model (array @model) (if @models @models (array))}}
|
||||||
@current-when={{@current-when}}
|
@current-when={{@currentWhen}}
|
||||||
@title={{@title}}
|
@title={{@title}}
|
||||||
>
|
>
|
||||||
{{@content}}
|
{{@content}}
|
||||||
|
|||||||
@@ -8,26 +8,15 @@
|
|||||||
@headerAction={{this.composeTopic}}
|
@headerAction={{this.composeTopic}}
|
||||||
@headerActionTitle={{i18n "sidebar.sections.topics.header_action_title"}}>
|
@headerActionTitle={{i18n "sidebar.sections.topics.header_action_title"}}>
|
||||||
|
|
||||||
<Sidebar::SectionLink
|
{{#each this.sectionLinks as |sectionLink|}}
|
||||||
@linkName="everything"
|
<Sidebar::SectionLink
|
||||||
@route={{this.everythingSectionLinkRoute}}
|
@linkName={{sectionLink.name}}
|
||||||
@query={{hash f=undefined}}
|
@route={{sectionLink.route}}
|
||||||
@title={{i18n "sidebar.sections.topics.links.everything.title"}}
|
@query={{sectionLink.query}}
|
||||||
@content={{i18n "sidebar.sections.topics.links.everything.content"}}
|
@title={{sectionLink.title}}
|
||||||
@current-when={{"discovery.latest discovery.new discovery.unread discovery.top"}}
|
@content={{sectionLink.text}}
|
||||||
@badgeText={{this.everythingSectionLinkBadgeText}} />
|
@currentWhen={{sectionLink.currentWhen}}
|
||||||
|
@badgeText={{sectionLink.badgeText}}
|
||||||
<Sidebar::SectionLink
|
@model={{sectionLink.model}} />
|
||||||
@linkName="tracked"
|
{{/each}}
|
||||||
@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"}} />
|
|
||||||
</Sidebar::Section>
|
</Sidebar::Section>
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
import { isLegacyEmber } from "discourse-common/config/environment";
|
import { isLegacyEmber } from "discourse-common/config/environment";
|
||||||
import topicFixtures from "discourse/tests/fixtures/discovery-fixtures";
|
import topicFixtures from "discourse/tests/fixtures/discovery-fixtures";
|
||||||
import { cloneJSON } from "discourse-common/lib/object";
|
import { cloneJSON } from "discourse-common/lib/object";
|
||||||
|
import { withPluginApi } from "discourse/lib/plugin-api";
|
||||||
|
|
||||||
acceptance("Sidebar - Topics Section", function (needs) {
|
acceptance("Sidebar - Topics Section", function (needs) {
|
||||||
needs.user({ experimental_sidebar_enabled: true });
|
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,
|
setTestPresence,
|
||||||
} from "discourse/lib/user-presence";
|
} from "discourse/lib/user-presence";
|
||||||
import PreloadStore from "discourse/lib/preload-store";
|
import PreloadStore from "discourse/lib/preload-store";
|
||||||
|
import { resetDefaultSectionLinks as resetTopicsSectionLinks } from "discourse/lib/sidebar/custom-topics-section-links";
|
||||||
|
|
||||||
const LEGACY_ENV = !setupApplicationTest;
|
const LEGACY_ENV = !setupApplicationTest;
|
||||||
|
|
||||||
@@ -186,6 +187,7 @@ function testCleanup(container, app) {
|
|||||||
clearPresenceCallbacks();
|
clearPresenceCallbacks();
|
||||||
}
|
}
|
||||||
restoreBaseUri();
|
restoreBaseUri();
|
||||||
|
resetTopicsSectionLinks();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function discourseModule(name, options) {
|
export function discourseModule(name, options) {
|
||||||
|
|||||||
Reference in New Issue
Block a user