Skip to content

Commit

Permalink
DEV: Add new experimental admin UI route and sidebar (discourse#23952)
Browse files Browse the repository at this point in the history
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 <kotlarek.krzysztof@gmail.com>
  • Loading branch information
martin-brennan and lis2 authored Oct 19, 2023
1 parent 47b2667 commit 9ef3a18
Show file tree
Hide file tree
Showing 34 changed files with 606 additions and 23 deletions.
31 changes: 31 additions & 0 deletions app/assets/javascripts/admin/addon/controllers/admin-revamp.js
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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 };
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
@@ -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;
}
40 changes: 40 additions & 0 deletions app/assets/javascripts/admin/addon/routes/admin-revamp.js
Original file line number Diff line number Diff line change
@@ -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);
}
}
10 changes: 10 additions & 0 deletions app/assets/javascripts/admin/addon/routes/admin-route-map.js
Original file line number Diff line number Diff line change
Expand Up @@ -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" });
});
});
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<div class="admin-revamp__config-area">
Config Area ({{@model.area}})
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<div class="admin-revamp__config">
Config

{{outlet}}
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Admin Revamp Lobby
12 changes: 12 additions & 0 deletions app/assets/javascripts/admin/addon/templates/admin-revamp.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{{hide-application-footer}}
<AdminWrapper @class="container">
<div class="row">
<div class="full-width">
<div class="boxed white admin-content">
<div class="admin-contents {{this.adminContentsClassName}}">
{{outlet}}
</div>
</div>
</div>
</div>
</AdminWrapper>
2 changes: 1 addition & 1 deletion app/assets/javascripts/discourse/app/components/sidebar.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
@willDestroy={{this.section.willDestroy}}
@collapsable={{@collapsable}}
@displaySection={{this.section.displaySection}}
@hideSectionHeader={{this.section.hideSectionHeader}}
>

{{#each this.section.links as |link|}}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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
);
});
},
};
10 changes: 9 additions & 1 deletion app/assets/javascripts/discourse/app/lib/plugin-api.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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.
Expand Down
Loading

0 comments on commit 9ef3a18

Please sign in to comment.