mirror of
https://github.com/discourse/discourse.git
synced 2024-11-29 04:03:57 -06:00
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 { action } from "@ember/object";
|
||||
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_PANEL_COMPONENT = "user-menu/notifications-list";
|
||||
@ -123,12 +123,31 @@ export default class UserMenu extends GlimmerComponent {
|
||||
|
||||
get _topTabs() {
|
||||
const tabs = [];
|
||||
|
||||
CORE_TOP_TABS.forEach((tabClass) => {
|
||||
const tab = new tabClass(this.currentUser, this.siteSettings, this.site);
|
||||
if (tab.shouldDisplay) {
|
||||
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) => {
|
||||
tab.position = index;
|
||||
return tab;
|
||||
|
@ -99,6 +99,7 @@ import { addSectionLink as addCustomCommunitySectionLink } from "discourse/lib/s
|
||||
import { addSidebarSection } from "discourse/lib/sidebar/custom-sections";
|
||||
import DiscourseURL from "discourse/lib/url";
|
||||
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
|
||||
// based on Semantic Versioning 2.0.0. Please update the changelog at
|
||||
@ -1882,6 +1883,49 @@ class PluginApi {
|
||||
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
|
||||
|
@ -1,3 +1,6 @@
|
||||
/**
|
||||
* abstract class representing a tab in the user menu
|
||||
*/
|
||||
export default class UserMenuTab {
|
||||
constructor(currentUser, siteSettings, site) {
|
||||
this.currentUser = currentUser;
|
||||
@ -5,22 +8,37 @@ export default class UserMenuTab {
|
||||
this.site = site;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {boolean} Controls whether the tab should be rendered or not.
|
||||
*/
|
||||
get shouldDisplay() {
|
||||
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() {
|
||||
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() {
|
||||
throw new Error("not implemented");
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {string} ID for the tab. Must be unique across all visible tabs.
|
||||
*/
|
||||
get id() {
|
||||
throw new Error("not implemented");
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {string} Icon for the tab.
|
||||
*/
|
||||
get icon() {
|
||||
throw new Error("not implemented");
|
||||
}
|
||||
@ -34,3 +52,13 @@ export default class UserMenuTab {
|
||||
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,
|
||||
publishToMessageBus,
|
||||
query,
|
||||
queryAll,
|
||||
} from "discourse/tests/helpers/qunit-helpers";
|
||||
import { test } from "qunit";
|
||||
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 UserMenuFixtures from "discourse/tests/fixtures/user-menu";
|
||||
import TopicFixtures from "discourse/tests/fixtures/topic";
|
||||
import I18n from "I18n";
|
||||
|
||||
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 = {};
|
||||
|
||||
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"
|
||||
);
|
||||
});
|
||||
|
||||
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) {
|
||||
|
@ -74,6 +74,7 @@ import { clearToolbarCallbacks } from "discourse/components/d-editor";
|
||||
import { clearExtraHeaderIcons } from "discourse/widgets/header";
|
||||
import { resetSidebarSection } from "discourse/lib/sidebar/custom-sections";
|
||||
import { resetNotificationTypeRenderers } from "discourse/lib/notification-item";
|
||||
import { resetUserMenuTabs } from "discourse/lib/user-menu/tab";
|
||||
|
||||
export function currentUser() {
|
||||
return User.create(sessionFixtures["/session/current.json"].current_user);
|
||||
@ -204,6 +205,7 @@ export function testCleanup(container, app) {
|
||||
resetSidebarSection();
|
||||
resetNotificationTypeRenderers();
|
||||
clearExtraHeaderIcons();
|
||||
resetUserMenuTabs();
|
||||
}
|
||||
|
||||
export function discourseModule(name, options) {
|
||||
|
Loading…
Reference in New Issue
Block a user