DEV: Automatically generate all admin links for app for new sidebar (#24175)

NOTE: Most of this is experimental and will be removed at a later
time, which is why things like translations have not been added.

The new /admin-revamp UI uses a sidebar for admin nav. This initial
step adds a script to generate a map of all the current admin nav
into a format the sidebar to read. Then, people can experiment
with different changes to this structure.

The structure can then be edited from `/admin-revamp/config/sidebar-experiment`,
and it is saved to local storage so people can visually experiment with different ways
of showing the admin sidebar links.
This commit is contained in:
Martin Brennan
2023-11-02 10:34:37 +10:00
committed by GitHub
parent 1c395e1a01
commit b53449eac9
15 changed files with 632 additions and 80 deletions
@@ -0,0 +1,35 @@
<div
class="admin-config-area-sidebar-experiment"
{{did-insert this.loadDefaultNavConfig}}
>
<h4>Sidebar Experiment</h4>
<p>Changes you make here will be applied to the admin sidebar and persist
between reloads
<em>on this device only</em>. Note that in addition to the
<code>text</code>
and
<code>route</code>
options, you can also specify a
<code>icon</code>
or a
<code>href</code>, if you want to link to a specific page but don't know the
Ember route.</p>
<DButton
@action={{this.resetToDefault}}
@translatedLabel="Reset to Default"
/>
<DButton
class="btn-primary"
@action={{this.applyConfig}}
@translatedLabel="Apply Config"
/>
<div class="admin-config-area-sidebar-experiment__editor">
<AceEditor
@content={{this.editedNavConfig}}
@editorId="admin-config-area-sidebar-experiment"
@save={{this.applyNav}}
/>
</div>
</div>
@@ -0,0 +1,66 @@
import Component from "@glimmer/component";
import { tracked } from "@glimmer/tracking";
import { action } from "@ember/object";
import { inject as service } from "@ember/service";
import {
buildAdminSidebar,
useAdminNavConfig,
} from "discourse/instance-initializers/admin-sidebar";
import { ADMIN_NAV_MAP } from "discourse/lib/sidebar/admin-nav-map";
import { resetPanelSections } from "discourse/lib/sidebar/custom-sections";
import { ADMIN_PANEL } from "discourse/services/sidebar-state";
export default class AdminConfigAreaSidebarExperiment extends Component {
@service adminSidebarExperimentStateManager;
@service toasts;
@tracked editedNavConfig;
get defaultAdminNav() {
return JSON.stringify(ADMIN_NAV_MAP, null, 2);
}
@action
loadDefaultNavConfig() {
const savedConfig = this.adminSidebarExperimentStateManager.navConfig;
this.editedNavConfig = savedConfig
? JSON.stringify(savedConfig, null, 2)
: this.defaultAdminNav;
}
@action
resetToDefault() {
this.editedNavConfig = this.defaultAdminNav;
this.#saveConfig(ADMIN_NAV_MAP);
}
@action
applyConfig() {
let config = null;
try {
config = JSON.parse(this.editedNavConfig);
} catch {
this.toasts.error({
duration: 3000,
data: {
message: "There was an error, make sure the structure is valid JSON.",
},
});
return;
}
this.#saveConfig(config);
}
#saveConfig(config) {
this.adminSidebarExperimentStateManager.navConfig = config;
resetPanelSections(
ADMIN_PANEL,
useAdminNavConfig(config),
buildAdminSidebar
);
this.toasts.success({
duration: 3000,
data: { message: "Sidebar navigation applied successfully!" },
});
}
}
@@ -1,10 +1,19 @@
import Route from "@ember/routing/route";
import { inject as service } from "@ember/service";
import { dasherize } from "@ember/string";
import AdminConfigAreaSidebarExperiment from "admin/components/admin-config-area-sidebar-experiment";
const CONFIG_AREA_COMPONENT_MAP = {
"sidebar-experiment": AdminConfigAreaSidebarExperiment,
};
export default class AdminRevampConfigAreaRoute extends Route {
@service router;
async model(params) {
return { area: params.area };
return {
area: params.area,
configAreaComponent: CONFIG_AREA_COMPONENT_MAP[dasherize(params.area)],
};
}
}
@@ -32,8 +32,10 @@ export default class AdminRoute extends DiscourseRoute {
});
}
deactivate() {
deactivate(transition) {
this.controllerFor("application").set("showTop", true);
this.sidebarState.setPanel(MAIN_PANEL);
if (!transition?.to.name.startsWith("admin")) {
this.sidebarState.setPanel(MAIN_PANEL);
}
}
}
@@ -1,7 +1,11 @@
import { inject as service } from "@ember/service";
import DiscourseRoute from "discourse/routes/discourse";
import { MAIN_PANEL } from "discourse/services/sidebar-state";
import I18n from "discourse-i18n";
export default class AdminRoute extends DiscourseRoute {
@service sidebarState;
titleToken() {
return I18n.t("admin_title");
}
@@ -14,5 +18,6 @@ export default class AdminRoute extends DiscourseRoute {
deactivate() {
this.controllerFor("application").set("showTop", true);
this.sidebarState.setPanel(MAIN_PANEL);
}
}
@@ -0,0 +1,16 @@
import Service from "@ember/service";
import KeyValueStore from "discourse/lib/key-value-store";
export default class AdminSidebarExperimentStateManager extends Service {
STORE_NAMESPACE = "discourse_admin_sidebar_experiment_";
store = new KeyValueStore(this.STORE_NAMESPACE);
get navConfig() {
return this.store.getObject("navConfig");
}
set navConfig(value) {
this.store.setObject({ key: "navConfig", value });
}
}
@@ -1,3 +1,5 @@
<div class="admin-revamp__config-area">
Config Area ({{@model.area}})
{{#if @model.configAreaComponent}}
<@model.configAreaComponent />
{{/if}}
</div>
@@ -1,5 +1,3 @@
<div class="admin-revamp__config">
Config
{{outlet}}
</div>
@@ -1,3 +1,4 @@
import { ADMIN_NAV_MAP } from "discourse/lib/sidebar/admin-nav-map";
import {
addSidebarPanel,
addSidebarSection,
@@ -23,6 +24,10 @@ function defineAdminSectionLink(BaseCustomSidebarSectionLink) {
return this.adminSidebarNavLink.route;
}
get href() {
return this.adminSidebarNavLink.href;
}
get models() {
return this.adminSidebarNavLink.routeModels;
}
@@ -90,14 +95,81 @@ function defineAdminSection(
return AdminNavSection;
}
export function useAdminNavConfig(navMap) {
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",
},
],
},
];
return adminNavSections.concat(navMap);
}
let adminSectionLinkClass = null;
export function buildAdminSidebar(navConfig) {
navConfig.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
);
});
}
export default {
initialize(owner) {
this.currentUser = owner.lookup("service:currentUser");
this.siteSettings = owner.lookup("service:site-settings");
if (!this.currentUser?.staff) {
return;
}
if (
!this.siteSettings.userInAnyGroups(
"enable_experimental_admin_ui_groups",
this.currentUser
)
) {
return;
}
this.adminSidebarExperimentStateManager = owner.lookup(
"service:admin-sidebar-experiment-state-manager"
);
addSidebarPanel(
(BaseCustomSidebarPanel) =>
class AdminSidebarPanel extends BaseCustomSidebarPanel {
@@ -106,71 +178,8 @@ export default {
}
);
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
);
});
const savedConfig = this.adminSidebarExperimentStateManager.navConfig;
const navConfig = useAdminNavConfig(savedConfig || ADMIN_NAV_MAP);
buildAdminSidebar(navConfig, adminSectionLinkClass);
},
};
@@ -0,0 +1,245 @@
// DO NOT EDIT THIS FILE!!!
// Update it by running `rake javascript:update_constants`
export const ADMIN_NAV_MAP = [
{
name: "root",
text: "Root",
links: [
{ name: "admin-revamp", route: "admin-revamp", text: "Revamp" },
{ name: "admin", route: "admin", text: "Admin" },
],
},
{
name: "plugins",
text: "Plugins",
links: [
{ name: "admin_plugins", route: "adminPlugins", text: "Plugins" },
{ name: "admin_plugins_chat", route: "adminPlugins.chat", text: "Chat" },
{
name: "admin_plugins_discourse-automation",
route: "adminPlugins.discourse-automation",
text: "Discourse Automation",
},
{
name: "admin_plugins_discourse-automation_new",
route: "adminPlugins.discourse-automation.new",
text: "Discourse Automation New",
},
],
},
{
name: "site_settings",
text: "Site Settings",
links: [
{
name: "admin_site_settings",
route: "adminSiteSettings",
text: "Site Settings",
},
],
},
{
name: "reports",
text: "Reports",
links: [{ name: "admin_reports", route: "adminReports", text: "Reports" }],
},
{
name: "users",
text: "Users",
links: [
{ name: "admin_users_list", route: "adminUsersList", text: "List" },
{ name: "admin_users", route: "adminUsers", text: "Users" },
],
},
{
name: "email",
text: "Email",
links: [
{ name: "admin_email_sent", route: "adminEmail.sent", text: "Sent" },
{
name: "admin_email_skipped",
route: "adminEmail.skipped",
text: "Skipped",
},
{
name: "admin_email_bounced",
route: "adminEmail.bounced",
text: "Bounced",
},
{
name: "admin_email_received",
route: "adminEmail.received",
text: "Received",
},
{
name: "admin_email_rejected",
route: "adminEmail.rejected",
text: "Rejected",
},
{
name: "admin_email_preview-digest",
route: "adminEmail.previewDigest",
text: "Preview Digest",
},
{
name: "admin_email_advanced-test",
route: "adminEmail.advancedTest",
text: "Advanced Test",
},
{ name: "admin_email", route: "adminEmail", text: "Email" },
],
},
{
name: "logs",
text: "Logs",
links: [
{
name: "admin_logs_staff_action_logs",
route: "adminLogs.staffActionLogs",
text: "Staff Action Logs",
},
{
name: "admin_logs_screened_emails",
route: "adminLogs.screenedEmails",
text: "Screened Emails",
},
{
name: "admin_logs_screened_ip_addresses",
route: "adminLogs.screenedIpAddresses",
text: "Screened Ip Addresses",
},
{
name: "admin_logs_screened_urls",
route: "adminLogs.screenedUrls",
text: "Screened Urls",
},
{
name: "admin_logs_search_logs",
route: "adminSearchLogs",
text: "Search Logs",
},
{
name: "admin_logs_search_logs_term",
route: "adminSearchLogs.term",
text: "Search Term",
},
{ name: "admin_logs", route: "adminLogs", text: "Logs" },
],
},
{
name: "customize",
text: "Customize",
links: [
{ name: "admin_customize", route: "adminCustomize", text: "Customize" },
{
name: "admin_customize_themes",
route: "adminCustomizeThemes",
text: "Themes",
},
{
name: "admin_customize_colors",
route: "adminCustomize.colors",
text: "Colors",
},
{
name: "admin_customize_permalinks",
route: "adminPermalinks",
text: "Permalinks",
},
{
name: "admin_customize_embedding",
route: "adminEmbedding",
text: "Embedding",
},
{
name: "admin_customize_user_fields",
route: "adminUserFields",
text: "User Fields",
},
{ name: "admin_customize_emojis", route: "adminEmojis", text: "Emojis" },
{
name: "admin_customize_form-templates",
route: "adminCustomizeFormTemplates",
text: "Form Templates",
},
{
name: "admin_customize_form-templates_new",
route: "adminCustomizeFormTemplates.new",
text: "Form Templates New",
},
{
name: "admin_customize_site_texts",
route: "adminSiteText",
text: "Site Texts",
},
{
name: "admin_customize_email_templates",
route: "adminCustomizeEmailTemplates",
text: "Email Templates",
},
{
name: "admin_customize_robots",
route: "adminCustomizeRobotsTxt",
text: "Robots",
},
{
name: "admin_customize_email_style",
route: "adminCustomizeEmailStyle",
text: "Email Style",
},
{
name: "admin_customize_watched_words",
route: "adminWatchedWords",
text: "Watched Words",
},
],
},
{
name: "dashboard",
text: "Dashboard",
links: [
{
name: "admin_dashboard_moderation",
route: "admin.dashboardModeration",
text: "Moderation",
},
{
name: "admin_dashboard_security",
route: "admin.dashboardSecurity",
text: "Security",
},
{
name: "admin_dashboard_reports",
route: "admin.dashboardReports",
text: "Reports",
},
],
},
{
name: "api",
text: "Api",
links: [
{ name: "admin_api_keys", route: "adminApiKeys", text: "Keys" },
{
name: "admin_api_web_hooks",
route: "adminWebHooks",
text: "Web Hooks",
},
{ name: "admin_api", route: "adminApi", text: "Api" },
],
},
{
name: "backups",
text: "Backups",
links: [
{ name: "admin_backups_logs", route: "admin.backups.logs", text: "Logs" },
{ name: "admin_backups", route: "admin.backups", text: "Backups" },
],
},
{
name: "badges",
text: "Badges",
links: [{ name: "admin_badges", route: "adminBadges", text: "Badges" }],
},
];
@@ -1,8 +1,10 @@
import { tracked } from "@glimmer/tracking";
/**
* Base class representing a sidebar section header interface.
*/
export default class BaseCustomSidebarPanel {
sections = [];
@tracked sections = [];
/**
* @returns {boolean} Controls whether the panel is hidden, which means that
@@ -33,7 +33,7 @@ export function addSidebarPanel(func) {
}
export function addSidebarSection(func, panelKey) {
const panel = customPanels.find((p) => p.key === panelKey);
const panel = customPanels.findBy("key", panelKey);
if (!panel) {
// eslint-disable-next-line no-console
return console.warn(
@@ -45,6 +45,20 @@ export function addSidebarSection(func, panelKey) {
);
}
export function resetPanelSections(
panelKey,
newSections = null,
sectionBuilder = null
) {
const panel = customPanels.findBy("key", panelKey);
if (newSections) {
panel.sections = [];
sectionBuilder(newSections);
} else {
panel.sections = [];
}
}
export function resetSidebarPanels() {
customPanels = [new MainSidebarPanel()];
currentPanelKey = "main";
@@ -1,12 +1,27 @@
.admin-revamp {
&__config {
padding: 1em;
background-color: var(--primary-low);
}
&__config-area {
padding: 1em;
margin: 1em 0;
background-color: var(--primary-very-low);
}
}
.admin-config-area-sidebar-experiment {
&__editor {
margin-top: 1em;
.ace-wrapper {
position: relative;
width: 100%;
height: calc(100vh);
min-height: 500px;
.ace_editor {
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
}
}
}
}