DEV: Add `registerUserMenuTab` plugin API (#17851)
Co-authored-by: OsamaSayegh <asooomaasoooma90@gmail.com>
This commit is contained in:
parent
424e968538
commit
23520b88c2
|
@ -2,7 +2,7 @@ import GlimmerComponent from "discourse/components/glimmer";
|
||||||
import { tracked } from "@glimmer/tracking";
|
import { tracked } from "@glimmer/tracking";
|
||||||
import { action } from "@ember/object";
|
import { action } from "@ember/object";
|
||||||
import { NO_REMINDER_ICON } from "discourse/models/bookmark";
|
import { NO_REMINDER_ICON } from "discourse/models/bookmark";
|
||||||
import UserMenuTab from "discourse/lib/user-menu/tab";
|
import UserMenuTab, { CUSTOM_TABS_CLASSES } from "discourse/lib/user-menu/tab";
|
||||||
|
|
||||||
const DEFAULT_TAB_ID = "all-notifications";
|
const DEFAULT_TAB_ID = "all-notifications";
|
||||||
const DEFAULT_PANEL_COMPONENT = "user-menu/notifications-list";
|
const DEFAULT_PANEL_COMPONENT = "user-menu/notifications-list";
|
||||||
|
@ -123,12 +123,31 @@ export default class UserMenu extends GlimmerComponent {
|
||||||
|
|
||||||
get _topTabs() {
|
get _topTabs() {
|
||||||
const tabs = [];
|
const tabs = [];
|
||||||
|
|
||||||
CORE_TOP_TABS.forEach((tabClass) => {
|
CORE_TOP_TABS.forEach((tabClass) => {
|
||||||
const tab = new tabClass(this.currentUser, this.siteSettings, this.site);
|
const tab = new tabClass(this.currentUser, this.siteSettings, this.site);
|
||||||
if (tab.shouldDisplay) {
|
if (tab.shouldDisplay) {
|
||||||
tabs.push(tab);
|
tabs.push(tab);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let reviewQueueTabIndex = tabs.findIndex(
|
||||||
|
(tab) => tab.id === REVIEW_QUEUE_TAB_ID
|
||||||
|
);
|
||||||
|
|
||||||
|
CUSTOM_TABS_CLASSES.forEach((tabClass) => {
|
||||||
|
const tab = new tabClass(this.currentUser, this.siteSettings, this.site);
|
||||||
|
if (tab.shouldDisplay) {
|
||||||
|
// ensure the review queue tab is always last
|
||||||
|
if (reviewQueueTabIndex === -1) {
|
||||||
|
tabs.push(tab);
|
||||||
|
} else {
|
||||||
|
tabs.insertAt(reviewQueueTabIndex, tab);
|
||||||
|
reviewQueueTabIndex++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
return tabs.map((tab, index) => {
|
return tabs.map((tab, index) => {
|
||||||
tab.position = index;
|
tab.position = index;
|
||||||
return tab;
|
return tab;
|
||||||
|
|
|
@ -99,6 +99,7 @@ import { addSectionLink as addCustomCommunitySectionLink } from "discourse/lib/s
|
||||||
import { addSidebarSection } from "discourse/lib/sidebar/custom-sections";
|
import { addSidebarSection } from "discourse/lib/sidebar/custom-sections";
|
||||||
import DiscourseURL from "discourse/lib/url";
|
import DiscourseURL from "discourse/lib/url";
|
||||||
import { registerNotificationTypeRenderer } from "discourse/lib/notification-item";
|
import { registerNotificationTypeRenderer } from "discourse/lib/notification-item";
|
||||||
|
import { registerUserMenuTab } from "discourse/lib/user-menu/tab";
|
||||||
|
|
||||||
// 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
|
||||||
|
@ -1882,6 +1883,49 @@ class PluginApi {
|
||||||
registerNotificationTypeRenderer(notificationType, func) {
|
registerNotificationTypeRenderer(notificationType, func) {
|
||||||
registerNotificationTypeRenderer(notificationType, func);
|
registerNotificationTypeRenderer(notificationType, func);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* EXPERIMENTAL. Do not use.
|
||||||
|
* Registers a new tab in the user menu. This API method expects a callback
|
||||||
|
* that should return a class inheriting from the class (UserMenuTab) that's
|
||||||
|
* passed to the callback. See discourse/app/lib/user-menu/tab.js for
|
||||||
|
* documentation of UserMenuTab.
|
||||||
|
*
|
||||||
|
* ```
|
||||||
|
* api.registerUserMenuTab((UserMenuTab) => {
|
||||||
|
* return class extends UserMenuTab {
|
||||||
|
* get id() {
|
||||||
|
* return "custom-tab-id";
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* get shouldDisplay() {
|
||||||
|
* return this.siteSettings.enable_custom_tab && this.currentUser.admin;
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* get count() {
|
||||||
|
* return this.currentUser.my_custom_notification_count;
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* get panelComponent() {
|
||||||
|
* return "your-custom-glimmer-component";
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* get icon() {
|
||||||
|
* return "some-fa5-icon";
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* });
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* @callback customTabRegistererCallback
|
||||||
|
* @param {UserMenuTab} The base class from which the returned class should inherit.
|
||||||
|
* @returns {UserMenuTab} A class that inherits from UserMenuTab.
|
||||||
|
*
|
||||||
|
* @param {customTabRegistererCallback} func - Callback function that returns a subclass from the class it receives as its argument.
|
||||||
|
*/
|
||||||
|
registerUserMenuTab(func) {
|
||||||
|
registerUserMenuTab(func);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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
|
||||||
|
|
|
@ -1,3 +1,6 @@
|
||||||
|
/**
|
||||||
|
* abstract class representing a tab in the user menu
|
||||||
|
*/
|
||||||
export default class UserMenuTab {
|
export default class UserMenuTab {
|
||||||
constructor(currentUser, siteSettings, site) {
|
constructor(currentUser, siteSettings, site) {
|
||||||
this.currentUser = currentUser;
|
this.currentUser = currentUser;
|
||||||
|
@ -5,22 +8,37 @@ export default class UserMenuTab {
|
||||||
this.site = site;
|
this.site = site;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {boolean} Controls whether the tab should be rendered or not.
|
||||||
|
*/
|
||||||
get shouldDisplay() {
|
get shouldDisplay() {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {number} Controls the blue badge (aka bubble) count that's rendered on top of the tab. If count is zero, no badge is shown.
|
||||||
|
*/
|
||||||
get count() {
|
get count() {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {string} Dasherized version of the component name that should be rendered in the panel area when the tab is active.
|
||||||
|
*/
|
||||||
get panelComponent() {
|
get panelComponent() {
|
||||||
throw new Error("not implemented");
|
throw new Error("not implemented");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {string} ID for the tab. Must be unique across all visible tabs.
|
||||||
|
*/
|
||||||
get id() {
|
get id() {
|
||||||
throw new Error("not implemented");
|
throw new Error("not implemented");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {string} Icon for the tab.
|
||||||
|
*/
|
||||||
get icon() {
|
get icon() {
|
||||||
throw new Error("not implemented");
|
throw new Error("not implemented");
|
||||||
}
|
}
|
||||||
|
@ -34,3 +52,13 @@ export default class UserMenuTab {
|
||||||
return this.currentUser.get(key) || 0;
|
return this.currentUser.get(key) || 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const CUSTOM_TABS_CLASSES = [];
|
||||||
|
|
||||||
|
export function registerUserMenuTab(func) {
|
||||||
|
CUSTOM_TABS_CLASSES.push(func(UserMenuTab));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resetUserMenuTabs() {
|
||||||
|
CUSTOM_TABS_CLASSES.clear();
|
||||||
|
}
|
||||||
|
|
|
@ -5,16 +5,21 @@ import {
|
||||||
loggedInUser,
|
loggedInUser,
|
||||||
publishToMessageBus,
|
publishToMessageBus,
|
||||||
query,
|
query,
|
||||||
|
queryAll,
|
||||||
} from "discourse/tests/helpers/qunit-helpers";
|
} from "discourse/tests/helpers/qunit-helpers";
|
||||||
import { test } from "qunit";
|
import { test } from "qunit";
|
||||||
import { cloneJSON } from "discourse-common/lib/object";
|
import { cloneJSON } from "discourse-common/lib/object";
|
||||||
|
import { withPluginApi } from "discourse/lib/plugin-api";
|
||||||
import { NOTIFICATION_TYPES } from "discourse/tests/fixtures/concerns/notification-types";
|
import { NOTIFICATION_TYPES } from "discourse/tests/fixtures/concerns/notification-types";
|
||||||
import UserMenuFixtures from "discourse/tests/fixtures/user-menu";
|
import UserMenuFixtures from "discourse/tests/fixtures/user-menu";
|
||||||
import TopicFixtures from "discourse/tests/fixtures/topic";
|
import TopicFixtures from "discourse/tests/fixtures/topic";
|
||||||
import I18n from "I18n";
|
import I18n from "I18n";
|
||||||
|
|
||||||
acceptance("User menu", function (needs) {
|
acceptance("User menu", function (needs) {
|
||||||
needs.user({ redesigned_user_menu_enabled: true });
|
needs.user({
|
||||||
|
redesigned_user_menu_enabled: true,
|
||||||
|
unread_high_priority_notifications: 73,
|
||||||
|
});
|
||||||
let requestHeaders = {};
|
let requestHeaders = {};
|
||||||
|
|
||||||
needs.pretender((server, helper) => {
|
needs.pretender((server, helper) => {
|
||||||
|
@ -44,6 +49,133 @@ acceptance("User menu", function (needs) {
|
||||||
"the Discourse-Clear-Notifications request header is set to the notification id in the next ajax request"
|
"the Discourse-Clear-Notifications request header is set to the notification id in the next ajax request"
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("tabs added via the plugin API", async function (assert) {
|
||||||
|
withPluginApi("0.1", (api) => {
|
||||||
|
api.registerUserMenuTab((UserMenuTab) => {
|
||||||
|
return class extends UserMenuTab {
|
||||||
|
get id() {
|
||||||
|
return "custom-tab-1";
|
||||||
|
}
|
||||||
|
|
||||||
|
get count() {
|
||||||
|
return this.currentUser.get("unread_high_priority_notifications");
|
||||||
|
}
|
||||||
|
|
||||||
|
get icon() {
|
||||||
|
return "wrench";
|
||||||
|
}
|
||||||
|
|
||||||
|
get panelComponent() {
|
||||||
|
return "d-button";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
api.registerUserMenuTab((UserMenuTab) => {
|
||||||
|
return class extends UserMenuTab {
|
||||||
|
get id() {
|
||||||
|
return "custom-tab-2";
|
||||||
|
}
|
||||||
|
|
||||||
|
get count() {
|
||||||
|
return 29;
|
||||||
|
}
|
||||||
|
|
||||||
|
get icon() {
|
||||||
|
return "plus";
|
||||||
|
}
|
||||||
|
|
||||||
|
get panelComponent() {
|
||||||
|
return "d-button";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
await visit("/");
|
||||||
|
await click(".d-header-icons .current-user");
|
||||||
|
|
||||||
|
const customTab1 = query("#user-menu-button-custom-tab-1");
|
||||||
|
const customTab2 = query("#user-menu-button-custom-tab-2");
|
||||||
|
|
||||||
|
assert.ok(customTab1, "first custom tab is rendered");
|
||||||
|
assert.ok(customTab2, "second custom tab is rendered");
|
||||||
|
|
||||||
|
assert.strictEqual(
|
||||||
|
customTab1.dataset.tabNumber,
|
||||||
|
"5",
|
||||||
|
"custom tab has the right tab number"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.strictEqual(
|
||||||
|
customTab2.dataset.tabNumber,
|
||||||
|
"6",
|
||||||
|
"custom tab has the right tab number"
|
||||||
|
);
|
||||||
|
|
||||||
|
const reviewQueueTab = query("#user-menu-button-review-queue");
|
||||||
|
|
||||||
|
assert.strictEqual(
|
||||||
|
reviewQueueTab.dataset.tabNumber,
|
||||||
|
"7",
|
||||||
|
"review queue tab comes after the custom tabs"
|
||||||
|
);
|
||||||
|
|
||||||
|
const tabs = [...queryAll(".tabs-list .btn")]; // top and bottom tabs
|
||||||
|
|
||||||
|
assert.deepEqual(
|
||||||
|
tabs.map((t) => t.dataset.tabNumber),
|
||||||
|
["0", "1", "2", "3", "4", "5", "6", "7", "8"],
|
||||||
|
"data-tab-number of the tabs has no gaps when custom tabs are added"
|
||||||
|
);
|
||||||
|
|
||||||
|
let customTab1Bubble = query(
|
||||||
|
"#user-menu-button-custom-tab-1 .badge-notification"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.strictEqual(
|
||||||
|
customTab1Bubble.textContent.trim(),
|
||||||
|
"73",
|
||||||
|
"bubble shows the right count"
|
||||||
|
);
|
||||||
|
|
||||||
|
const customTab2Bubble = query(
|
||||||
|
"#user-menu-button-custom-tab-2 .badge-notification"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.strictEqual(
|
||||||
|
customTab2Bubble.textContent.trim(),
|
||||||
|
"29",
|
||||||
|
"bubble shows the right count"
|
||||||
|
);
|
||||||
|
|
||||||
|
await publishToMessageBus(`/notification/${loggedInUser().id}`, {
|
||||||
|
unread_high_priority_notifications: 18,
|
||||||
|
});
|
||||||
|
|
||||||
|
customTab1Bubble = query(
|
||||||
|
"#user-menu-button-custom-tab-1 .badge-notification"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.strictEqual(
|
||||||
|
customTab1Bubble.textContent.trim(),
|
||||||
|
"18",
|
||||||
|
"displayed bubble count updates when the value is changed"
|
||||||
|
);
|
||||||
|
|
||||||
|
await click("#user-menu-button-custom-tab-1");
|
||||||
|
|
||||||
|
assert.ok(
|
||||||
|
exists("#user-menu-button-custom-tab-1.active"),
|
||||||
|
"custom tabs can be clicked on and become active"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.ok(
|
||||||
|
exists("#quick-access-custom-tab-1 button.btn"),
|
||||||
|
"the tab's content is now displayed in the panel"
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
acceptance("User menu - Dismiss button", function (needs) {
|
acceptance("User menu - Dismiss button", function (needs) {
|
||||||
|
|
|
@ -74,6 +74,7 @@ import { clearToolbarCallbacks } from "discourse/components/d-editor";
|
||||||
import { clearExtraHeaderIcons } from "discourse/widgets/header";
|
import { clearExtraHeaderIcons } from "discourse/widgets/header";
|
||||||
import { resetSidebarSection } from "discourse/lib/sidebar/custom-sections";
|
import { resetSidebarSection } from "discourse/lib/sidebar/custom-sections";
|
||||||
import { resetNotificationTypeRenderers } from "discourse/lib/notification-item";
|
import { resetNotificationTypeRenderers } from "discourse/lib/notification-item";
|
||||||
|
import { resetUserMenuTabs } from "discourse/lib/user-menu/tab";
|
||||||
|
|
||||||
export function currentUser() {
|
export function currentUser() {
|
||||||
return User.create(sessionFixtures["/session/current.json"].current_user);
|
return User.create(sessionFixtures["/session/current.json"].current_user);
|
||||||
|
@ -204,6 +205,7 @@ export function testCleanup(container, app) {
|
||||||
resetSidebarSection();
|
resetSidebarSection();
|
||||||
resetNotificationTypeRenderers();
|
resetNotificationTypeRenderers();
|
||||||
clearExtraHeaderIcons();
|
clearExtraHeaderIcons();
|
||||||
|
resetUserMenuTabs();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function discourseModule(name, options) {
|
export function discourseModule(name, options) {
|
||||||
|
|
Loading…
Reference in New Issue