From 9ef3a18ce4dacb81746dd129a9f5b04fe3074fef Mon Sep 17 00:00:00 2001 From: Martin Brennan Date: Thu, 19 Oct 2023 14:23:41 +1000 Subject: [PATCH] DEV: Add new experimental admin UI route and sidebar (#23952) This commit adds a new admin UI under the route `/admin-revamp`, which is only accessible if the user is in a group defined by the new `enable_experimental_admin_ui_groups` site setting. It also adds a special `admin` sidebar panel that is shown instead of the `main` forum one when the admin is in this area. ![image](https://github.com/discourse/discourse/assets/920448/fa0f25e1-e178-4d94-aa5f-472fd3efd787) We also add an "Admin Revamp" sidebar link to the community section, which will only appear if the user is in the setting group: ![image](https://github.com/discourse/discourse/assets/920448/ec05ca8b-5a54-442b-ba89-6af35695c104) Within this there are subroutes defined like `/admin-revamp/config/:area`, these areas could contain any UI imaginable, this is just laying down an initial idea of the structure and how the sidebar will work. Sidebar links are currently hardcoded. Some other changes: * Changed the `main` and `chat` panels sidebar panel keys to use exported const values for reuse * Allowed custom sidebar sections to hide their headers with the `hideSectionHeader` option * Add a `groupSettingArray` setting on `this.siteSettings` in JS, which accepts a group site setting name and splits it by `|` then converts the items in the array to integers, similar to the `_map` magic for ruby group site settings * Adds a `hidden` option for sidebar panels which prevents them from showing in separated mode and prevents the switch button from being shown --------- Co-authored-by: Krzysztof Kotlarek --- .../admin/addon/controllers/admin-revamp.js | 31 +++ .../addon/routes/admin-revamp-config-area.js | 10 + .../admin/addon/routes/admin-revamp-config.js | 6 + .../admin/addon/routes/admin-revamp-lobby.js | 6 + .../admin/addon/routes/admin-revamp.js | 40 ++++ .../admin/addon/routes/admin-route-map.js | 10 + .../templates/admin-revamp-config-area.hbs | 3 + .../addon/templates/admin-revamp-config.hbs | 5 + .../addon/templates/admin-revamp-lobby.hbs | 1 + .../admin/addon/templates/admin-revamp.hbs | 12 ++ .../discourse/app/components/sidebar.js | 2 +- .../app/components/sidebar/api-section.hbs | 1 + .../app/components/sidebar/api-sections.js | 5 +- .../instance-initializers/admin-sidebar.js | 176 ++++++++++++++++++ .../discourse/app/lib/plugin-api.js | 10 +- .../lib/sidebar/base-custom-sidebar-panel.js | 21 ++- .../common/community-section/section.js | 2 + .../admin-revamp-section-link.js | 45 +++++ .../discourse/app/services/sidebar-state.js | 3 +- .../discourse/app/services/site-settings.js | 17 +- .../acceptance/sidebar-plugin-api-test.js | 114 ++++++++++++ .../stylesheets/common/admin/admin_base.scss | 3 + .../common/admin/admin_revamp.scss | 12 ++ app/models/sidebar_url.rb | 6 + config/routes.rb | 8 + config/site_settings.yml | 8 + docs/CHANGELOG-JAVASCRIPT-PLUGIN-API.md | 7 + .../discourse/initializers/chat-sidebar.js | 9 +- .../discourse/lib/init-sidebar-state.js | 9 +- .../javascripts/discourse/routes/chat.js | 7 +- .../discourse/services/chat-state-manager.js | 5 +- .../javascripts/discourse/services/chat.js | 4 +- spec/models/sidebar_section_spec.rb | 13 +- .../admin_revamp_sidebar_navigation_spec.rb | 18 ++ 34 files changed, 606 insertions(+), 23 deletions(-) create mode 100644 app/assets/javascripts/admin/addon/controllers/admin-revamp.js create mode 100644 app/assets/javascripts/admin/addon/routes/admin-revamp-config-area.js create mode 100644 app/assets/javascripts/admin/addon/routes/admin-revamp-config.js create mode 100644 app/assets/javascripts/admin/addon/routes/admin-revamp-lobby.js create mode 100644 app/assets/javascripts/admin/addon/routes/admin-revamp.js create mode 100644 app/assets/javascripts/admin/addon/templates/admin-revamp-config-area.hbs create mode 100644 app/assets/javascripts/admin/addon/templates/admin-revamp-config.hbs create mode 100644 app/assets/javascripts/admin/addon/templates/admin-revamp-lobby.hbs create mode 100644 app/assets/javascripts/admin/addon/templates/admin-revamp.hbs create mode 100644 app/assets/javascripts/discourse/app/instance-initializers/admin-sidebar.js create mode 100644 app/assets/javascripts/discourse/app/lib/sidebar/user/community-section/admin-revamp-section-link.js create mode 100644 app/assets/stylesheets/common/admin/admin_revamp.scss create mode 100644 spec/system/admin_revamp_sidebar_navigation_spec.rb diff --git a/app/assets/javascripts/admin/addon/controllers/admin-revamp.js b/app/assets/javascripts/admin/addon/controllers/admin-revamp.js new file mode 100644 index 00000000000..d7ba6a28f06 --- /dev/null +++ b/app/assets/javascripts/admin/addon/controllers/admin-revamp.js @@ -0,0 +1,31 @@ +import Controller from "@ember/controller"; +import { inject as service } from "@ember/service"; +import { dasherize } from "@ember/string"; +import discourseComputed from "discourse-common/utils/decorators"; + +export default class AdminRevampController extends Controller { + @service router; + + @discourseComputed("router._router.currentPath") + adminContentsClassName(currentPath) { + let cssClasses = currentPath + .split(".") + .filter((segment) => { + return ( + segment !== "index" && + segment !== "loading" && + segment !== "show" && + segment !== "admin" + ); + }) + .map(dasherize) + .join(" "); + + // this is done to avoid breaking css customizations + if (cssClasses.includes("dashboard")) { + cssClasses = `${cssClasses} dashboard-next`; + } + + return cssClasses; + } +} diff --git a/app/assets/javascripts/admin/addon/routes/admin-revamp-config-area.js b/app/assets/javascripts/admin/addon/routes/admin-revamp-config-area.js new file mode 100644 index 00000000000..6751bb0aa61 --- /dev/null +++ b/app/assets/javascripts/admin/addon/routes/admin-revamp-config-area.js @@ -0,0 +1,10 @@ +import Route from "@ember/routing/route"; +import { inject as service } from "@ember/service"; + +export default class AdminRevampConfigAreaRoute extends Route { + @service router; + + async model(params) { + return { area: params.area }; + } +} diff --git a/app/assets/javascripts/admin/addon/routes/admin-revamp-config.js b/app/assets/javascripts/admin/addon/routes/admin-revamp-config.js new file mode 100644 index 00000000000..ee0e05f7b0c --- /dev/null +++ b/app/assets/javascripts/admin/addon/routes/admin-revamp-config.js @@ -0,0 +1,6 @@ +import Route from "@ember/routing/route"; +import { inject as service } from "@ember/service"; + +export default class AdminRevampConfigRoute extends Route { + @service router; +} diff --git a/app/assets/javascripts/admin/addon/routes/admin-revamp-lobby.js b/app/assets/javascripts/admin/addon/routes/admin-revamp-lobby.js new file mode 100644 index 00000000000..b4fbfadb9ac --- /dev/null +++ b/app/assets/javascripts/admin/addon/routes/admin-revamp-lobby.js @@ -0,0 +1,6 @@ +import Route from "@ember/routing/route"; +import { inject as service } from "@ember/service"; + +export default class AdminRevampLobbyRoute extends Route { + @service router; +} diff --git a/app/assets/javascripts/admin/addon/routes/admin-revamp.js b/app/assets/javascripts/admin/addon/routes/admin-revamp.js new file mode 100644 index 00000000000..a21b1676155 --- /dev/null +++ b/app/assets/javascripts/admin/addon/routes/admin-revamp.js @@ -0,0 +1,40 @@ +import { inject as service } from "@ember/service"; +import DiscourseURL from "discourse/lib/url"; +import DiscourseRoute from "discourse/routes/discourse"; +import { ADMIN_PANEL, MAIN_PANEL } from "discourse/services/sidebar-state"; +import I18n from "discourse-i18n"; + +export default class AdminRoute extends DiscourseRoute { + @service siteSettings; + @service currentUser; + @service sidebarState; + + titleToken() { + return I18n.t("admin_title"); + } + + activate() { + if ( + !this.currentUser.isInAnyGroups( + this.siteSettings.groupSettingArray( + "enable_experimental_admin_ui_groups" + ) + ) + ) { + return DiscourseURL.redirectTo("/admin"); + } + + this.sidebarState.setPanel(ADMIN_PANEL); + this.sidebarState.setSeparatedMode(); + this.sidebarState.hideSwitchPanelButtons(); + + this.controllerFor("application").setProperties({ + showTop: false, + }); + } + + deactivate() { + this.controllerFor("application").set("showTop", true); + this.sidebarState.setPanel(MAIN_PANEL); + } +} diff --git a/app/assets/javascripts/admin/addon/routes/admin-route-map.js b/app/assets/javascripts/admin/addon/routes/admin-route-map.js index ef5d507922d..c180f64dab8 100644 --- a/app/assets/javascripts/admin/addon/routes/admin-route-map.js +++ b/app/assets/javascripts/admin/addon/routes/admin-route-map.js @@ -211,4 +211,14 @@ export default function () { } ); }); + + // EXPERIMENTAL: These admin routes are hidden behind an `enable_experimental_admin_ui_groups` + // site setting and are subject to constant change. + this.route("admin-revamp", { resetNamespace: true }, function () { + this.route("lobby", { path: "/" }, function () {}); + + this.route("config", { path: "config" }, function () { + this.route("area", { path: "/:area" }); + }); + }); } diff --git a/app/assets/javascripts/admin/addon/templates/admin-revamp-config-area.hbs b/app/assets/javascripts/admin/addon/templates/admin-revamp-config-area.hbs new file mode 100644 index 00000000000..13526e8a57e --- /dev/null +++ b/app/assets/javascripts/admin/addon/templates/admin-revamp-config-area.hbs @@ -0,0 +1,3 @@ +
+ Config Area ({{@model.area}}) +
\ No newline at end of file diff --git a/app/assets/javascripts/admin/addon/templates/admin-revamp-config.hbs b/app/assets/javascripts/admin/addon/templates/admin-revamp-config.hbs new file mode 100644 index 00000000000..a70240c2105 --- /dev/null +++ b/app/assets/javascripts/admin/addon/templates/admin-revamp-config.hbs @@ -0,0 +1,5 @@ +
+ Config + + {{outlet}} +
\ No newline at end of file diff --git a/app/assets/javascripts/admin/addon/templates/admin-revamp-lobby.hbs b/app/assets/javascripts/admin/addon/templates/admin-revamp-lobby.hbs new file mode 100644 index 00000000000..57d2c28438f --- /dev/null +++ b/app/assets/javascripts/admin/addon/templates/admin-revamp-lobby.hbs @@ -0,0 +1 @@ +Admin Revamp Lobby \ No newline at end of file diff --git a/app/assets/javascripts/admin/addon/templates/admin-revamp.hbs b/app/assets/javascripts/admin/addon/templates/admin-revamp.hbs new file mode 100644 index 00000000000..9e53b938259 --- /dev/null +++ b/app/assets/javascripts/admin/addon/templates/admin-revamp.hbs @@ -0,0 +1,12 @@ +{{hide-application-footer}} + +
+
+
+
+ {{outlet}} +
+
+
+
+
\ 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 94884b7c6ad..7043231b821 100644 --- a/app/assets/javascripts/discourse/app/components/sidebar.js +++ b/app/assets/javascripts/discourse/app/components/sidebar.js @@ -31,7 +31,7 @@ export default class Sidebar extends Component { } return this.sidebarState.panels.filter( - (panel) => panel !== this.sidebarState.currentPanel + (panel) => panel !== this.sidebarState.currentPanel && !panel.hidden ); } diff --git a/app/assets/javascripts/discourse/app/components/sidebar/api-section.hbs b/app/assets/javascripts/discourse/app/components/sidebar/api-section.hbs index acb73faa18a..c48f0c4093a 100644 --- a/app/assets/javascripts/discourse/app/components/sidebar/api-section.hbs +++ b/app/assets/javascripts/discourse/app/components/sidebar/api-section.hbs @@ -7,6 +7,7 @@ @willDestroy={{this.section.willDestroy}} @collapsable={{@collapsable}} @displaySection={{this.section.displaySection}} + @hideSectionHeader={{this.section.hideSectionHeader}} > {{#each this.section.links as |link|}} diff --git a/app/assets/javascripts/discourse/app/components/sidebar/api-sections.js b/app/assets/javascripts/discourse/app/components/sidebar/api-sections.js index e9ef60b5023..28e5c7b50eb 100644 --- a/app/assets/javascripts/discourse/app/components/sidebar/api-sections.js +++ b/app/assets/javascripts/discourse/app/components/sidebar/api-sections.js @@ -6,7 +6,10 @@ export default class SidebarApiSections extends Component { get sections() { if (this.sidebarState.combinedMode) { - return this.sidebarState.panels.map((panel) => panel.sections).flat(); + return this.sidebarState.panels + .filter((panel) => !panel.hidden) + .map((panel) => panel.sections) + .flat(); } else { return this.sidebarState.currentPanel.sections; } diff --git a/app/assets/javascripts/discourse/app/instance-initializers/admin-sidebar.js b/app/assets/javascripts/discourse/app/instance-initializers/admin-sidebar.js new file mode 100644 index 00000000000..d3b448050a1 --- /dev/null +++ b/app/assets/javascripts/discourse/app/instance-initializers/admin-sidebar.js @@ -0,0 +1,176 @@ +import { + addSidebarPanel, + addSidebarSection, +} from "discourse/lib/sidebar/custom-sections"; +import { ADMIN_PANEL } from "discourse/services/sidebar-state"; + +function defineAdminSectionLink(BaseCustomSidebarSectionLink) { + const SidebarAdminSectionLink = class extends BaseCustomSidebarSectionLink { + constructor({ adminSidebarNavLink }) { + super(...arguments); + this.adminSidebarNavLink = adminSidebarNavLink; + } + + get name() { + return this.adminSidebarNavLink.name; + } + + get classNames() { + return "admin-sidebar-nav-link"; + } + + get route() { + return this.adminSidebarNavLink.route; + } + + get models() { + return this.adminSidebarNavLink.routeModels; + } + + get text() { + return this.adminSidebarNavLink.text; + } + + get prefixType() { + return "icon"; + } + + get prefixValue() { + return this.adminSidebarNavLink.icon; + } + + get title() { + return this.adminSidebarNavLink.text; + } + }; + + return SidebarAdminSectionLink; +} + +function defineAdminSection( + adminNavSectionData, + BaseCustomSidebarSection, + adminSectionLinkClass +) { + const AdminNavSection = class extends BaseCustomSidebarSection { + constructor() { + super(...arguments); + this.adminNavSectionData = adminNavSectionData; + this.hideSectionHeader = adminNavSectionData.hideSectionHeader; + } + + get sectionLinks() { + return this.adminNavSectionData.links; + } + + get name() { + return `admin-nav-section-${this.adminNavSectionData.name}`; + } + + get title() { + return this.adminNavSectionData.text; + } + + get text() { + return this.adminNavSectionData.text; + } + + get links() { + return this.sectionLinks.map( + (sectionLinkData) => + new adminSectionLinkClass({ adminSidebarNavLink: sectionLinkData }) + ); + } + + get displaySection() { + return true; + } + }; + + return AdminNavSection; +} + +export default { + initialize(owner) { + this.currentUser = owner.lookup("service:currentUser"); + + if (!this.currentUser?.staff) { + return; + } + + addSidebarPanel( + (BaseCustomSidebarPanel) => + class AdminSidebarPanel extends BaseCustomSidebarPanel { + key = ADMIN_PANEL; + hidden = true; + } + ); + + let adminSectionLinkClass = null; + + // HACK: This is just an example, we need a better way of defining this data. + const adminNavSections = [ + { + text: "", + name: "root", + hideSectionHeader: true, + links: [ + { + name: "Back to Forum", + route: "discovery.latest", + text: "Back to Forum", + icon: "arrow-left", + }, + { + name: "Lobby", + route: "admin-revamp.lobby", + text: "Lobby", + icon: "home", + }, + { + name: "legacy", + route: "admin", + text: "Legacy Admin", + icon: "wrench", + }, + ], + }, + { + text: "Community", + name: "community", + links: [ + { + name: "Item 1", + route: "admin-revamp.config.area", + routeModels: [{ area: "item-1" }], + text: "Item 1", + }, + { + name: "Item 2", + route: "admin-revamp.config.area", + routeModels: [{ area: "item-2" }], + text: "Item 2", + }, + ], + }, + ]; + + adminNavSections.forEach((adminNavSectionData) => { + addSidebarSection( + (BaseCustomSidebarSection, BaseCustomSidebarSectionLink) => { + // We only want to define the link class once even though we have many different sections. + adminSectionLinkClass = + adminSectionLinkClass || + defineAdminSectionLink(BaseCustomSidebarSectionLink); + + return defineAdminSection( + adminNavSectionData, + BaseCustomSidebarSection, + adminSectionLinkClass + ); + }, + ADMIN_PANEL + ); + }); + }, +}; diff --git a/app/assets/javascripts/discourse/app/lib/plugin-api.js b/app/assets/javascripts/discourse/app/lib/plugin-api.js index 27fb8847508..9a348b2ffa7 100644 --- a/app/assets/javascripts/discourse/app/lib/plugin-api.js +++ b/app/assets/javascripts/discourse/app/lib/plugin-api.js @@ -136,7 +136,7 @@ import { modifySelectKit } from "select-kit/mixins/plugin-api"; // 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.14.0"; +export const PLUGIN_API_VERSION = "1.15.0"; // This helper prevents us from applying the same `modifyClass` over and over in test mode. function canModify(klass, type, resolverName, changes) { @@ -2207,6 +2207,14 @@ class PluginApi { this._lookupContainer("service:sidebar-state")?.setPanel(name); } + /** + * EXPERIMENTAL. Do not use. + * Support for getting the current Sidebar panel. + */ + getSidebarPanel() { + return this._lookupContainer("service:sidebar-state")?.currentPanel; + } + /** * EXPERIMENTAL. Do not use. * Set combined sidebar section mode. In this mode, sections from all panels are displayed together. 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 index f9a8ceecbbe..c941a792292 100644 --- 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 @@ -4,6 +4,15 @@ export default class BaseCustomSidebarPanel { sections = []; + /** + * @returns {boolean} Controls whether the panel is hidden, which means that + * it will not show up in combined sidebar mode, and its switch button will + * never show either. + */ + get hidden() { + return false; + } + /** * @returns {string} Identifier for sidebar panel */ @@ -12,24 +21,24 @@ export default class BaseCustomSidebarPanel { } /** - * @returns {string} Text for the switch button + * @returns {string} Text for the switch button. Obsolete when panel is hidden. */ get switchButtonLabel() { - this.#notImplemented(); + this.hidden || this.#notImplemented(); } /** - * @returns {string} Icon for the switch button + * @returns {string} Icon for the switch button. Obsolete when panel is hidden. */ get switchButtonIcon() { - this.#notImplemented(); + this.hidden || this.#notImplemented(); } /** - * @returns {string} Default path to panel + * @returns {string} Default path to panel. Obsolete when panel is hidden. */ get switchButtonDefaultUrl() { - this.#notImplemented(); + this.hidden || this.#notImplemented(); } #notImplemented() { diff --git a/app/assets/javascripts/discourse/app/lib/sidebar/common/community-section/section.js b/app/assets/javascripts/discourse/app/lib/sidebar/common/community-section/section.js index ad81a2bbeef..4b1e5067afd 100644 --- a/app/assets/javascripts/discourse/app/lib/sidebar/common/community-section/section.js +++ b/app/assets/javascripts/discourse/app/lib/sidebar/common/community-section/section.js @@ -12,6 +12,7 @@ import { secondaryCustomSectionLinks, } from "discourse/lib/sidebar/custom-community-section-links"; import SectionLink from "discourse/lib/sidebar/section-link"; +import AdminRevampSectionLink from "discourse/lib/sidebar/user/community-section/admin-revamp-section-link"; import AdminSectionLink from "discourse/lib/sidebar/user/community-section/admin-section-link"; import MyPostsSectionLink from "discourse/lib/sidebar/user/community-section/my-posts-section-link"; import ReviewSectionLink from "discourse/lib/sidebar/user/community-section/review-section-link"; @@ -25,6 +26,7 @@ const SPECIAL_LINKS_MAP = { "/review": ReviewSectionLink, "/badges": BadgesSectionLink, "/admin": AdminSectionLink, + "/admin-revamp": AdminRevampSectionLink, "/g": GroupsSectionLink, }; diff --git a/app/assets/javascripts/discourse/app/lib/sidebar/user/community-section/admin-revamp-section-link.js b/app/assets/javascripts/discourse/app/lib/sidebar/user/community-section/admin-revamp-section-link.js new file mode 100644 index 00000000000..c38cc9b4f16 --- /dev/null +++ b/app/assets/javascripts/discourse/app/lib/sidebar/user/community-section/admin-revamp-section-link.js @@ -0,0 +1,45 @@ +import { inject as service } from "@ember/service"; +import BaseSectionLink from "discourse/lib/sidebar/base-community-section-link"; +import I18n from "discourse-i18n"; + +export default class AdminRevampSectionLink extends BaseSectionLink { + @service siteSettings; + + get name() { + return "admin-revamp"; + } + + get route() { + return "admin-revamp"; + } + + get title() { + return I18n.t("sidebar.sections.community.links.admin.content"); + } + + get text() { + return I18n.t( + `sidebar.sections.community.links.${this.overridenName.toLowerCase()}.content`, + { defaultValue: this.overridenName } + ); + } + + get shouldDisplay() { + if (!this.currentUser) { + return false; + } + + return ( + this.currentUser.staff && + this.currentUser.isInAnyGroups( + this.siteSettings.groupSettingArray( + "enable_experimental_admin_ui_groups" + ) + ) + ); + } + + get defaultPrefixValue() { + return "star"; + } +} diff --git a/app/assets/javascripts/discourse/app/services/sidebar-state.js b/app/assets/javascripts/discourse/app/services/sidebar-state.js index 4ca6f11c074..6e12bad088e 100644 --- a/app/assets/javascripts/discourse/app/services/sidebar-state.js +++ b/app/assets/javascripts/discourse/app/services/sidebar-state.js @@ -8,7 +8,8 @@ import { const COMBINED_MODE = "combined"; const SEPARATED_MODE = "separated"; -const MAIN_PANEL = "main"; +export const MAIN_PANEL = "main"; +export const ADMIN_PANEL = "admin"; @disableImplicitInjections export default class SidebarState extends Service { diff --git a/app/assets/javascripts/discourse/app/services/site-settings.js b/app/assets/javascripts/discourse/app/services/site-settings.js index 733e530a592..85778e3661a 100644 --- a/app/assets/javascripts/discourse/app/services/site-settings.js +++ b/app/assets/javascripts/discourse/app/services/site-settings.js @@ -7,6 +7,21 @@ export default class SiteSettingsService { static isServiceFactory = true; static create() { - return new TrackedObject(PreloadStore.get("siteSettings")); + const settings = new TrackedObject(PreloadStore.get("siteSettings")); + + settings.groupSettingArray = (groupSetting) => { + const setting = settings[groupSetting]; + if (!setting) { + return []; + } + + return setting + .toString() + .split("|") + .filter(Boolean) + .map((groupId) => parseInt(groupId, 10)); + }; + + return settings; } } 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 2384ea75122..4a34b24f3c3 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 @@ -1084,4 +1084,118 @@ acceptance("Sidebar - Plugin API", function (needs) { await visit("/"); assert.dom(".sidebar__panel-switch-button").exists(); }); + + test("New hidden custom sidebar panel", async function (assert) { + withPluginApi(PLUGIN_API_VERSION, (api) => { + api.addSidebarPanel((BaseCustomSidebarPanel) => { + const AdminSidebarPanel = class extends BaseCustomSidebarPanel { + get key() { + return "admin-panel"; + } + + get hidden() { + return true; + } + }; + return AdminSidebarPanel; + }); + api.addSidebarSection( + (BaseCustomSidebarSection, BaseCustomSidebarSectionLink) => { + return class extends BaseCustomSidebarSection { + get name() { + return "test-admin-section"; + } + + get text() { + return "test admin section"; + } + + get actionsIcon() { + return "cog"; + } + + get links() { + return [ + new (class extends BaseCustomSidebarSectionLink { + get name() { + return "admin-link"; + } + + get classNames() { + return "my-class-name"; + } + + get route() { + return "topic"; + } + + get models() { + return ["some-slug", 1]; + } + + get title() { + return "admin link"; + } + + get text() { + return "admin link"; + } + + get prefixType() { + return "icon"; + } + + get prefixValue() { + return "cog"; + } + + get prefixColor() { + return "FF0000"; + } + + get prefixBadge() { + return "lock"; + } + + get suffixType() { + return "icon"; + } + + get suffixValue() { + return "circle"; + } + + get suffixCSSClass() { + return "unread"; + } + })(), + ]; + } + }; + }, + "admin-panel" + ); + api.setSidebarPanel("admin-panel"); + api.setSeparatedSidebarMode(); + }); + + await visit("/"); + + assert.strictEqual( + query( + ".sidebar-section[data-section-name='test-admin-section'] .sidebar-section-header-text" + ).textContent.trim(), + "test admin section", + "displays header with correct text" + ); + withPluginApi(PLUGIN_API_VERSION, (api) => { + api.setSidebarPanel("main-panel"); + api.setCombinedSidebarMode(); + }); + await visit("/"); + assert.dom(".sidebar__panel-switch-button").doesNotExist(); + assert + .dom(".sidebar-section[data-section-name='test-admin-section']") + .doesNotExist(); + }); }); diff --git a/app/assets/stylesheets/common/admin/admin_base.scss b/app/assets/stylesheets/common/admin/admin_base.scss index 5d566df986c..2cabf8157ed 100644 --- a/app/assets/stylesheets/common/admin/admin_base.scss +++ b/app/assets/stylesheets/common/admin/admin_base.scss @@ -1052,3 +1052,6 @@ a.inline-editable-field { @import "common/admin/admin_intro"; @import "common/admin/admin_emojis"; @import "common/admin/mini_profiler"; + +// EXPERIMENTAL: Revamped admin styles, probably can be split up later down the line. +@import "common/admin/admin_revamp"; diff --git a/app/assets/stylesheets/common/admin/admin_revamp.scss b/app/assets/stylesheets/common/admin/admin_revamp.scss new file mode 100644 index 00000000000..f23e723854c --- /dev/null +++ b/app/assets/stylesheets/common/admin/admin_revamp.scss @@ -0,0 +1,12 @@ +.admin-revamp { + &__config { + padding: 1em; + background-color: var(--primary-low); + } + + &__config-area { + padding: 1em; + margin: 1em 0; + background-color: var(--primary-very-low); + } +} diff --git a/app/models/sidebar_url.rb b/app/models/sidebar_url.rb index 6e21a3e76c5..76a498f6c50 100644 --- a/app/models/sidebar_url.rb +++ b/app/models/sidebar_url.rb @@ -22,6 +22,12 @@ class SidebarUrl < ActiveRecord::Base }, { name: "Review", path: "/review", icon: "flag", segment: SidebarUrl.segments["primary"] }, { name: "Admin", path: "/admin", icon: "wrench", segment: SidebarUrl.segments["primary"] }, + { + name: "Admin Revamp", + path: "/admin-revamp", + icon: "star", + segment: SidebarUrl.segments["primary"], + }, { name: "Users", path: "/u", icon: "users", segment: SidebarUrl.segments["secondary"] }, { name: "About", diff --git a/config/routes.rb b/config/routes.rb index c5055da4e67..1d4ffc9f891 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -100,6 +100,14 @@ Discourse::Application.routes.draw do get "wizard/steps/:id" => "wizard#index" put "wizard/steps/:id" => "steps#update" + namespace :admin_revamp, + path: "admin-revamp", + module: "admin", + constraints: StaffConstraint.new do + get "" => "admin#index" + get "config/:area" => "admin#index" + end + namespace :admin, constraints: StaffConstraint.new do get "" => "admin#index" diff --git a/config/site_settings.yml b/config/site_settings.yml index 66036e5f9f3..c0e91b0c4d4 100644 --- a/config/site_settings.yml +++ b/config/site_settings.yml @@ -2178,6 +2178,14 @@ developer: instrument_gc_stat_per_request: default: false hidden: true + enable_experimental_admin_ui_groups: + type: group_list + list_type: compact + default: "" + allow_any: false + refresh: true + hidden: true + client: true lazy_load_categories: default: false client: true diff --git a/docs/CHANGELOG-JAVASCRIPT-PLUGIN-API.md b/docs/CHANGELOG-JAVASCRIPT-PLUGIN-API.md index 43e7c475d86..a6aab86a274 100644 --- a/docs/CHANGELOG-JAVASCRIPT-PLUGIN-API.md +++ b/docs/CHANGELOG-JAVASCRIPT-PLUGIN-API.md @@ -7,6 +7,13 @@ in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.15.0] - 2023-10-18 + +### Added + +- Added `hidden` option to `addSidebarPanel`, this can be used to remove the panel from combined sidebar mode as well as hiding its switch button. Useful for cases where only one sidebar should be shown at a time regardless of other panels. +- Added `getSidebarPanel` function, which returns the current sidebar panel object for comparison. + ## [1.14.0] - 2023-10-06 ### Added diff --git a/plugins/chat/assets/javascripts/discourse/initializers/chat-sidebar.js b/plugins/chat/assets/javascripts/discourse/initializers/chat-sidebar.js index f51308bcdf1..abb06ed438e 100644 --- a/plugins/chat/assets/javascripts/discourse/initializers/chat-sidebar.js +++ b/plugins/chat/assets/javascripts/discourse/initializers/chat-sidebar.js @@ -11,7 +11,10 @@ import getURL from "discourse-common/lib/get-url"; import { bind } from "discourse-common/utils/decorators"; import I18n from "discourse-i18n"; import ChatModalNewMessage from "discourse/plugins/chat/discourse/components/chat/modal/new-message"; -import { initSidebarState } from "discourse/plugins/chat/discourse/lib/init-sidebar-state"; +import { + CHAT_PANEL, + initSidebarState, +} from "discourse/plugins/chat/discourse/lib/init-sidebar-state"; export default { name: "chat-sidebar", @@ -28,7 +31,7 @@ export default { api.addSidebarPanel( (BaseCustomSidebarPanel) => class ChatSidebarPanel extends BaseCustomSidebarPanel { - key = "chat"; + key = CHAT_PANEL; switchButtonLabel = I18n.t("sidebar.panels.chat.label"); switchButtonIcon = "d-chat"; switchButtonDefaultUrl = getURL("/chat"); @@ -196,7 +199,7 @@ export default { return SidebarChatChannelsSection; }, - "chat" + CHAT_PANEL ); } diff --git a/plugins/chat/assets/javascripts/discourse/lib/init-sidebar-state.js b/plugins/chat/assets/javascripts/discourse/lib/init-sidebar-state.js index 4706366fc84..68ff26cf142 100644 --- a/plugins/chat/assets/javascripts/discourse/lib/init-sidebar-state.js +++ b/plugins/chat/assets/javascripts/discourse/lib/init-sidebar-state.js @@ -1,7 +1,14 @@ +import { ADMIN_PANEL, MAIN_PANEL } from "discourse/services/sidebar-state"; import { getUserChatSeparateSidebarMode } from "discourse/plugins/chat/discourse/lib/get-user-chat-separate-sidebar-mode"; +export const CHAT_PANEL = "chat"; + export function initSidebarState(api, user) { - api.setSidebarPanel("main"); + if (api.getSidebarPanel()?.key === ADMIN_PANEL) { + return; + } + + api.setSidebarPanel(MAIN_PANEL); const chatSeparateSidebarMode = getUserChatSeparateSidebarMode(user); if (chatSeparateSidebarMode.fullscreen) { diff --git a/plugins/chat/assets/javascripts/discourse/routes/chat.js b/plugins/chat/assets/javascripts/discourse/routes/chat.js index 731dc7ef3e8..e4ffe65c6c8 100644 --- a/plugins/chat/assets/javascripts/discourse/routes/chat.js +++ b/plugins/chat/assets/javascripts/discourse/routes/chat.js @@ -6,7 +6,10 @@ import { scrollTop } from "discourse/mixins/scroll-top"; import DiscourseRoute from "discourse/routes/discourse"; import I18n from "discourse-i18n"; import { getUserChatSeparateSidebarMode } from "discourse/plugins/chat/discourse/lib/get-user-chat-separate-sidebar-mode"; -import { initSidebarState } from "discourse/plugins/chat/discourse/lib/init-sidebar-state"; +import { + CHAT_PANEL, + initSidebarState, +} from "discourse/plugins/chat/discourse/lib/init-sidebar-state"; export default class ChatRoute extends DiscourseRoute { @service chat; @@ -62,7 +65,7 @@ export default class ChatRoute extends DiscourseRoute { activate() { withPluginApi("1.8.0", (api) => { - api.setSidebarPanel("chat"); + api.setSidebarPanel(CHAT_PANEL); const chatSeparateSidebarMode = getUserChatSeparateSidebarMode( this.currentUser diff --git a/plugins/chat/assets/javascripts/discourse/services/chat-state-manager.js b/plugins/chat/assets/javascripts/discourse/services/chat-state-manager.js index 86a7c6c60b2..54ba27f5686 100644 --- a/plugins/chat/assets/javascripts/discourse/services/chat-state-manager.js +++ b/plugins/chat/assets/javascripts/discourse/services/chat-state-manager.js @@ -4,6 +4,7 @@ import KeyValueStore from "discourse/lib/key-value-store"; import { withPluginApi } from "discourse/lib/plugin-api"; import { defaultHomepage } from "discourse/lib/utilities"; import Site from "discourse/models/site"; +import { MAIN_PANEL } from "discourse/services/sidebar-state"; import getURL from "discourse-common/lib/get-url"; import { getUserChatSeparateSidebarMode } from "discourse/plugins/chat/discourse/lib/get-user-chat-separate-sidebar-mode"; @@ -60,7 +61,7 @@ export default class ChatStateManager extends Service { didOpenDrawer(url = null) { withPluginApi("1.8.0", (api) => { if (getUserChatSeparateSidebarMode(this.currentUser).always) { - api.setSidebarPanel("main"); + api.setSidebarPanel(MAIN_PANEL); api.setSeparatedSidebarMode(); api.hideSidebarSwitchPanelButtons(); } else { @@ -81,7 +82,7 @@ export default class ChatStateManager extends Service { didCloseDrawer() { withPluginApi("1.8.0", (api) => { - api.setSidebarPanel("main"); + api.setSidebarPanel(MAIN_PANEL); const chatSeparateSidebarMode = getUserChatSeparateSidebarMode( this.currentUser diff --git a/plugins/chat/assets/javascripts/discourse/services/chat.js b/plugins/chat/assets/javascripts/discourse/services/chat.js index 6bbd3e7593d..4e523e10be0 100644 --- a/plugins/chat/assets/javascripts/discourse/services/chat.js +++ b/plugins/chat/assets/javascripts/discourse/services/chat.js @@ -68,9 +68,7 @@ export default class Chat extends Service { return ( this.currentUser.staff || this.currentUser.isInAnyGroups( - (this.siteSettings.direct_message_enabled_groups || "11") // trust level 1 auto group - .split("|") - .map((groupId) => parseInt(groupId, 10)) + this.siteSettings.groupSettingArray("direct_message_enabled_groups") ) ); } diff --git a/spec/models/sidebar_section_spec.rb b/spec/models/sidebar_section_spec.rb index 557e2a3d262..d9960da4336 100644 --- a/spec/models/sidebar_section_spec.rb +++ b/spec/models/sidebar_section_spec.rb @@ -22,7 +22,18 @@ RSpec.describe SidebarSection do expect(community_section.reload.title).to eq("Community") expect(community_section.sidebar_section_links.all.map { |link| link.linkable.name }).to eq( - ["Topics", "My Posts", "Review", "Admin", "Users", "About", "FAQ", "Groups", "Badges"], + [ + "Topics", + "My Posts", + "Review", + "Admin", + "Admin Revamp", + "Users", + "About", + "FAQ", + "Groups", + "Badges", + ], ) end end diff --git a/spec/system/admin_revamp_sidebar_navigation_spec.rb b/spec/system/admin_revamp_sidebar_navigation_spec.rb new file mode 100644 index 00000000000..08bfe633a10 --- /dev/null +++ b/spec/system/admin_revamp_sidebar_navigation_spec.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +describe "Admin Revamp | Sidebar Naviagion", type: :system do + fab!(:admin) { Fabricate(:admin) } + let(:sidebar_page) { PageObjects::Components::NavigationMenu::Sidebar.new } + + before do + SiteSetting.enable_experimental_admin_ui_groups = Group::AUTO_GROUPS[:staff] + SidebarSection.find_by(section_type: "community").reset_community! + sign_in(admin) + end + + it "navigates to the admin revamp from the sidebar" do + visit("/latest") + sidebar_page.click_section_link("Admin Revamp") + expect(page).to have_content("Admin Revamp Lobby") + end +end