diff --git a/app/assets/javascripts/discourse/app/components/sidebar.hbs b/app/assets/javascripts/discourse/app/components/sidebar.hbs
index 516b3d76941..2ccf92362e6 100644
--- a/app/assets/javascripts/discourse/app/components/sidebar.hbs
+++ b/app/assets/javascripts/discourse/app/components/sidebar.hbs
@@ -1,7 +1,26 @@
-
+ {{#if this.showMainPanel}}
+
+ {{else}}
+
+ {{/if}}
+
+ {{#each this.switchPanelButtons as |button|}}
+
+ {{/each}}
+
\ No newline at end of file
diff --git a/app/assets/javascripts/discourse/app/components/sidebar.js b/app/assets/javascripts/discourse/app/components/sidebar.js
index f4316c392a6..5fa3d37c529 100644
--- a/app/assets/javascripts/discourse/app/components/sidebar.js
+++ b/app/assets/javascripts/discourse/app/components/sidebar.js
@@ -1,11 +1,19 @@
import Component from "@glimmer/component";
+import { tracked } from "@glimmer/tracking";
import { bind } from "discourse-common/utils/decorators";
import { inject as service } from "@ember/service";
+import {
+ currentPanelKey,
+ customPanels as sidebarCustomPanels,
+} from "discourse/lib/sidebar/custom-sections";
+import { action } from "@ember/object";
export default class Sidebar extends Component {
@service appEvents;
@service site;
@service currentUser;
+ @service router;
+ @tracked currentPanelKey = currentPanelKey;
constructor() {
super(...arguments);
@@ -15,6 +23,24 @@ export default class Sidebar extends Component {
}
}
+ get showMainPanel() {
+ return this.currentPanelKey === "main";
+ }
+
+ get currentPanel() {
+ return sidebarCustomPanels.find(
+ (panel) => panel.key === this.currentPanelKey
+ );
+ }
+
+ get switchPanelButtons() {
+ if (sidebarCustomPanels.length === 1 || !this.currentUser) {
+ return [];
+ }
+
+ return sidebarCustomPanels.filter((panel) => panel !== this.currentPanel);
+ }
+
@bind
collapseSidebar(event) {
let shouldCollapseSidebar = false;
@@ -41,4 +67,16 @@ export default class Sidebar extends Component {
document.removeEventListener("click", this.collapseSidebar);
}
}
+
+ @action
+ switchPanel(panel) {
+ this.currentPanel.lastKnownURL = this.router.currentURL;
+ this.currentPanelKey = panel.key;
+ const url = panel.lastKnownURL || panel.switchButtonDefaultUrl;
+ if (url === "/") {
+ this.router.transitionTo("latest");
+ } else {
+ this.router.transitionTo(url);
+ }
+ }
}
diff --git a/app/assets/javascripts/discourse/app/components/sidebar/api-panels.hbs b/app/assets/javascripts/discourse/app/components/sidebar/api-panels.hbs
new file mode 100644
index 00000000000..613e55cd974
--- /dev/null
+++ b/app/assets/javascripts/discourse/app/components/sidebar/api-panels.hbs
@@ -0,0 +1,6 @@
+
\ No newline at end of file
diff --git a/app/assets/javascripts/discourse/app/components/sidebar/api-panels.js b/app/assets/javascripts/discourse/app/components/sidebar/api-panels.js
new file mode 100644
index 00000000000..000f5f991bf
--- /dev/null
+++ b/app/assets/javascripts/discourse/app/components/sidebar/api-panels.js
@@ -0,0 +1,20 @@
+import Component from "@glimmer/component";
+import { getOwner, setOwner } from "@ember/application";
+import { inject as service } from "@ember/service";
+
+export default class SidebarApiPanels extends Component {
+ @service siteSettings;
+ @service currentUser;
+ @service site;
+
+ constructor() {
+ super(...arguments);
+
+ this.customSections =
+ this.args.panel?.sections?.map((customSection) => {
+ const section = new customSection();
+ setOwner(section, getOwner(this));
+ return section;
+ }) || [];
+ }
+}
diff --git a/app/assets/javascripts/discourse/app/components/sidebar/api-sections.hbs b/app/assets/javascripts/discourse/app/components/sidebar/api-sections.hbs
new file mode 100644
index 00000000000..7ad30dda5b6
--- /dev/null
+++ b/app/assets/javascripts/discourse/app/components/sidebar/api-sections.hbs
@@ -0,0 +1,45 @@
+{{#each @sections as |customSection|}}
+
+
+ {{#each customSection.links as |link|}}
+
+ {{/each}}
+
+{{/each}}
\ No newline at end of file
diff --git a/app/assets/javascripts/discourse/app/components/sidebar/sections.hbs b/app/assets/javascripts/discourse/app/components/sidebar/sections.hbs
index e2f6b4a7ba7..b59e75a3b32 100644
--- a/app/assets/javascripts/discourse/app/components/sidebar/sections.hbs
+++ b/app/assets/javascripts/discourse/app/components/sidebar/sections.hbs
@@ -1,5 +1,8 @@
{{#if @currentUser}}
-
+
{{else}}
{{/if}}
\ No newline at end of file
diff --git a/app/assets/javascripts/discourse/app/components/sidebar/user/sections.hbs b/app/assets/javascripts/discourse/app/components/sidebar/user/sections.hbs
index 62e3fd261e7..7a382ee8e0f 100644
--- a/app/assets/javascripts/discourse/app/components/sidebar/user/sections.hbs
+++ b/app/assets/javascripts/discourse/app/components/sidebar/user/sections.hbs
@@ -10,49 +10,8 @@
{{/if}}
- {{#each this.customSections as |customSection|}}
-
-
- {{#each customSection.links as |link|}}
-
- {{/each}}
-
- {{/each}}
+
\ No newline at end of file
diff --git a/app/assets/javascripts/discourse/app/components/sidebar/user/sections.js b/app/assets/javascripts/discourse/app/components/sidebar/user/sections.js
index 3760431c554..12b93860332 100644
--- a/app/assets/javascripts/discourse/app/components/sidebar/user/sections.js
+++ b/app/assets/javascripts/discourse/app/components/sidebar/user/sections.js
@@ -1,21 +1,21 @@
import Component from "@glimmer/component";
-import { customSections as sidebarCustomSections } from "discourse/lib/sidebar/custom-sections";
import { getOwner, setOwner } from "@ember/application";
import { inject as service } from "@ember/service";
-import { cached } from "@glimmer/tracking";
export default class SidebarUserSections extends Component {
@service siteSettings;
@service currentUser;
@service site;
- @cached
- get customSections() {
- return sidebarCustomSections.map((customSection) => {
- const section = new customSection({ sidebar: this });
- setOwner(section, getOwner(this));
- return section;
- });
+ constructor() {
+ super(...arguments);
+
+ this.customSections =
+ this.args.panel?.sections?.map((customSection) => {
+ const section = new customSection();
+ setOwner(section, getOwner(this));
+ return section;
+ }) || [];
}
get enableMessagesSection() {
diff --git a/app/assets/javascripts/discourse/app/lib/plugin-api.js b/app/assets/javascripts/discourse/app/lib/plugin-api.js
index 77152443ff0..c4d935b5792 100644
--- a/app/assets/javascripts/discourse/app/lib/plugin-api.js
+++ b/app/assets/javascripts/discourse/app/lib/plugin-api.js
@@ -105,7 +105,11 @@ 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 as addCustomCommunitySectionLink } from "discourse/lib/sidebar/custom-community-section-links";
-import { addSidebarSection } from "discourse/lib/sidebar/custom-sections";
+import {
+ addSidebarPanel,
+ addSidebarSection,
+ setSidebarPanel,
+} from "discourse/lib/sidebar/custom-sections";
import {
registerCustomCategoryLockIcon,
registerCustomCategorySectionLinkPrefix,
@@ -126,7 +130,7 @@ import { _addBulkButton } from "discourse/controllers/topic-bulk-actions";
// based on Semantic Versioning 2.0.0. Please update the changelog at
// docs/CHANGELOG-JAVASCRIPT-PLUGIN-API.md whenever you change the version
// using the format described at https://keepachangelog.com/en/1.0.0/.
-export const PLUGIN_API_VERSION = "1.7.1";
+export const PLUGIN_API_VERSION = "1.8.0";
// This helper prevents us from applying the same `modifyClass` over and over in test mode.
function canModify(klass, type, resolverName, changes) {
@@ -2029,6 +2033,44 @@ class PluginApi {
);
}
+ /**
+ * EXPERIMENTAL. Do not use.
+ * Support for adding a Sidebar panel by returning a class which extends from the BaseCustomSidebarPanel
+ * class interface. See `lib/sidebar/user/base-custom-sidebar-panel.js` for documentation on the BaseCustomSidebarPanel class
+ * interface.
+ *
+ * ```
+ * api.addSidebarPanel((BaseCustomSidebarPanel) => {
+ * const ChatSidebarPanel = class extends BaseCustomSidebarPanel {
+ * get key() {
+ * return "chat";
+ * }
+ * get switchButtonLabel() {
+ * return I18n.t("sidebar.panels.chat.label");
+ * }
+ * get switchButtonIcon() {
+ * return "d-chat";
+ * }
+ * get switchButtonDefaultUrl() {
+ * return "/chat";
+ * }
+ * };
+ * return ChatSidebarPanel;
+ * });
+ * ```
+ */
+ addSidebarPanel(func) {
+ addSidebarPanel(func);
+ }
+
+ /**
+ * EXPERIMENTAL. Do not use.
+ * Support for setting a Sidebar panel.
+ */
+ setSidebarPanel(name) {
+ setSidebarPanel(name);
+ }
+
/**
* Support for adding a Sidebar section by returning a class which extends from the BaseCustomSidebarSection
* class interface. See `lib/sidebar/user/base-custom-sidebar-section.js` for documentation on the BaseCustomSidebarSection class
@@ -2148,8 +2190,8 @@ class PluginApi {
* })
* ```
*/
- addSidebarSection(func) {
- addSidebarSection(func);
+ addSidebarSection(func, panelKey = "main") {
+ addSidebarSection(func, panelKey);
}
/**
diff --git a/app/assets/javascripts/discourse/app/lib/sidebar/base-custom-sidebar-panel.js b/app/assets/javascripts/discourse/app/lib/sidebar/base-custom-sidebar-panel.js
new file mode 100644
index 00000000000..f9a8ceecbbe
--- /dev/null
+++ b/app/assets/javascripts/discourse/app/lib/sidebar/base-custom-sidebar-panel.js
@@ -0,0 +1,38 @@
+/**
+ * Base class representing a sidebar section header interface.
+ */
+export default class BaseCustomSidebarPanel {
+ sections = [];
+
+ /**
+ * @returns {string} Identifier for sidebar panel
+ */
+ get key() {
+ this.#notImplemented();
+ }
+
+ /**
+ * @returns {string} Text for the switch button
+ */
+ get switchButtonLabel() {
+ this.#notImplemented();
+ }
+
+ /**
+ * @returns {string} Icon for the switch button
+ */
+ get switchButtonIcon() {
+ this.#notImplemented();
+ }
+
+ /**
+ * @returns {string} Default path to panel
+ */
+ get switchButtonDefaultUrl() {
+ this.#notImplemented();
+ }
+
+ #notImplemented() {
+ throw "not implemented";
+ }
+}
diff --git a/app/assets/javascripts/discourse/app/lib/sidebar/base-custom-sidebar-section.js b/app/assets/javascripts/discourse/app/lib/sidebar/base-custom-sidebar-section.js
index 85dc82e1886..9a2ab6a493c 100644
--- a/app/assets/javascripts/discourse/app/lib/sidebar/base-custom-sidebar-section.js
+++ b/app/assets/javascripts/discourse/app/lib/sidebar/base-custom-sidebar-section.js
@@ -2,10 +2,6 @@
* Base class representing a sidebar section header interface.
*/
export default class BaseCustomSidebarSection {
- constructor({ sidebar } = {}) {
- this.sidebar = sidebar;
- }
-
/**
* @returns {string} The name of the section header. Needs to be dasherized and lowercase.
*/
diff --git a/app/assets/javascripts/discourse/app/lib/sidebar/custom-sections.js b/app/assets/javascripts/discourse/app/lib/sidebar/custom-sections.js
index 2284ca57fa2..e162dc30aeb 100644
--- a/app/assets/javascripts/discourse/app/lib/sidebar/custom-sections.js
+++ b/app/assets/javascripts/discourse/app/lib/sidebar/custom-sections.js
@@ -1,14 +1,55 @@
import BaseCustomSidebarSection from "discourse/lib/sidebar/base-custom-sidebar-section";
+import BaseCustomSidebarPanel from "discourse/lib/sidebar/base-custom-sidebar-panel";
import BaseCustomSidebarSectionLink from "discourse/lib/sidebar/base-custom-sidebar-section-link";
+import I18n from "I18n";
-export const customSections = [];
+class MainSidebarPanel {
+ sections = [];
-export function addSidebarSection(func) {
- customSections.push(
+ get key() {
+ return "main";
+ }
+
+ get switchButtonLabel() {
+ return I18n.t("sidebar.panels.forum.label");
+ }
+
+ get switchButtonIcon() {
+ return "random";
+ }
+
+ get switchButtonDefaultUrl() {
+ return "/latest";
+ }
+}
+
+export let customPanels = [new MainSidebarPanel()];
+
+export let currentPanelKey = "main";
+
+export function addSidebarPanel(func) {
+ const panelClass = func.call(this, BaseCustomSidebarPanel);
+ customPanels.push(new panelClass());
+}
+
+export function setSidebarPanel(name) {
+ currentPanelKey = name;
+}
+
+export function addSidebarSection(func, panelKey) {
+ const panel = customPanels.find((p) => p.key === panelKey);
+ if (!panel) {
+ // eslint-disable-next-line no-console
+ return console.warn(
+ `Error adding section to ${panelKey} because panel doens't exist. Check addSidebarPanel API.`
+ );
+ }
+ panel.sections.push(
func.call(this, BaseCustomSidebarSection, BaseCustomSidebarSectionLink)
);
}
-export function resetSidebarSection() {
- customSections.length = 0;
+export function resetSidebarPanels() {
+ customPanels = [new MainSidebarPanel()];
+ currentPanelKey = "main";
}
diff --git a/app/assets/javascripts/discourse/tests/acceptance/sidebar-plugin-api-test.js b/app/assets/javascripts/discourse/tests/acceptance/sidebar-plugin-api-test.js
index 879271a56ba..848d7a7c872 100644
--- a/app/assets/javascripts/discourse/tests/acceptance/sidebar-plugin-api-test.js
+++ b/app/assets/javascripts/discourse/tests/acceptance/sidebar-plugin-api-test.js
@@ -874,4 +874,122 @@ acceptance("Sidebar - Plugin API", function (needs) {
resetCustomTagSectionLinkPrefixIcons();
}
});
+
+ test("New custom sidebar panel and option to set default", async function (assert) {
+ withPluginApi(PLUGIN_API_VERSION, (api) => {
+ api.addSidebarPanel((BaseCustomSidebarPanel) => {
+ const ChatSidebarPanel = class extends BaseCustomSidebarPanel {
+ get key() {
+ return "new-panel";
+ }
+
+ get switchButtonLabel() {
+ "New panel";
+ }
+
+ get switchButtonIcon() {
+ return "d-chat";
+ }
+
+ get switchButtonDefaultUrl() {
+ return "/chat";
+ }
+ };
+ return ChatSidebarPanel;
+ });
+ api.addSidebarSection(
+ (BaseCustomSidebarSection, BaseCustomSidebarSectionLink) => {
+ return class extends BaseCustomSidebarSection {
+ get name() {
+ return "test-chat-channels";
+ }
+
+ get text() {
+ return "chat channels text";
+ }
+
+ get actionsIcon() {
+ return "cog";
+ }
+
+ get links() {
+ return [
+ new (class extends BaseCustomSidebarSectionLink {
+ get name() {
+ return "random-channel";
+ }
+
+ get classNames() {
+ return "my-class-name";
+ }
+
+ get route() {
+ return "topic";
+ }
+
+ get models() {
+ return ["some-slug", 1];
+ }
+
+ get title() {
+ return "random channel title";
+ }
+
+ get text() {
+ return "random channel text";
+ }
+
+ get prefixType() {
+ return "icon";
+ }
+
+ get prefixValue() {
+ return "d-chat";
+ }
+
+ get prefixColor() {
+ return "FF0000";
+ }
+
+ get prefixBadge() {
+ return "lock";
+ }
+
+ get suffixType() {
+ return "icon";
+ }
+
+ get suffixValue() {
+ return "circle";
+ }
+
+ get suffixCSSClass() {
+ return "unread";
+ }
+ })(),
+ ];
+ }
+ };
+ },
+ "new-panel"
+ );
+ api.setSidebarPanel("new-panel");
+ });
+
+ await visit("/");
+
+ assert.strictEqual(
+ query(
+ ".sidebar-section[data-section-name='test-chat-channels'] .sidebar-section-header-text"
+ ).textContent.trim(),
+ "chat channels text",
+ "displays header with correct text"
+ );
+
+ await click(".sidebar__panel-switch-button");
+
+ assert
+ .dom(".sidebar-section[data-section-name='test-chat-channels']")
+ .doesNotExist();
+ });
});
diff --git a/app/assets/javascripts/discourse/tests/helpers/qunit-helpers.js b/app/assets/javascripts/discourse/tests/helpers/qunit-helpers.js
index 732b0e3eef9..b1a418cb3a7 100644
--- a/app/assets/javascripts/discourse/tests/helpers/qunit-helpers.js
+++ b/app/assets/javascripts/discourse/tests/helpers/qunit-helpers.js
@@ -80,7 +80,7 @@ import {
import { clearTagsHtmlCallbacks } from "discourse/lib/render-tags";
import { clearToolbarCallbacks } from "discourse/components/d-editor";
import { clearExtraHeaderIcons } from "discourse/widgets/header";
-import { resetSidebarSection } from "discourse/lib/sidebar/custom-sections";
+import { resetSidebarPanels } from "discourse/lib/sidebar/custom-sections";
import { resetNotificationTypeRenderers } from "discourse/lib/notification-types-manager";
import { resetUserMenuTabs } from "discourse/lib/user-menu/tab";
import { reset as resetLinkLookup } from "discourse/lib/link-lookup";
@@ -214,7 +214,7 @@ export function testCleanup(container, app) {
clearResolverOptions();
clearTagsHtmlCallbacks();
clearToolbarCallbacks();
- resetSidebarSection();
+ resetSidebarPanels();
resetNotificationTypeRenderers();
clearExtraHeaderIcons();
resetOnKeyDownCallbacks();
diff --git a/app/assets/stylesheets/common/base/sidebar.scss b/app/assets/stylesheets/common/base/sidebar.scss
index 5dbd561c8c5..c4ba5a41698 100644
--- a/app/assets/stylesheets/common/base/sidebar.scss
+++ b/app/assets/stylesheets/common/base/sidebar.scss
@@ -281,3 +281,10 @@
}
}
}
+
+.sidebar__panel-switch-button {
+ margin: 1em 1.3em 0 1.3em;
+ &:last-of-type {
+ margin-bottom: 1em;
+ }
+}
diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml
index 42413681975..56acb422d4c 100644
--- a/config/locales/client.en.yml
+++ b/config/locales/client.en.yml
@@ -2041,7 +2041,7 @@ en:
summary:
in_progress: "Summarizing topic using AI..."
- summarized_on: "Summarized with %{method} on %{date}"
+ summarized_on: "Summarized with %{method} on %{date}"
enabled_description: "You're viewing this topic top replies: the most interesting posts as determined by the community."
description:
one: "There is %{count} reply."
@@ -4527,6 +4527,9 @@ en:
title: "Flagged posts and other queued items"
pending_count: "%{count} pending"
global_section: "Global section, visible to everyone"
+ panels:
+ forum:
+ label: Forum
welcome_topic_banner:
title: "Create your Welcome Topic"
diff --git a/plugins/chat/assets/javascripts/discourse/initializers/chat-sidebar.js b/plugins/chat/assets/javascripts/discourse/initializers/chat-sidebar.js
index 81ee9a3adfa..010698b56b1 100644
--- a/plugins/chat/assets/javascripts/discourse/initializers/chat-sidebar.js
+++ b/plugins/chat/assets/javascripts/discourse/initializers/chat-sidebar.js
@@ -110,10 +110,11 @@ export default {
};
const SidebarChatChannelsSection = class extends BaseCustomSidebarSection {
+ @service currentUser;
@tracked currentUserCanJoinPublicChannels =
- this.sidebar.currentUser &&
- (this.sidebar.currentUser.staff ||
- this.sidebar.currentUser.has_joinable_public_channels);
+ this.currentUser &&
+ (this.currentUser.staff ||
+ this.currentUser.has_joinable_public_channels);
constructor() {
super(...arguments);