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);