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}}
+
+
+
\ 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