FEATURE: Initial admin sidebar navigation (#24789)

This is v0 of admin sidebar navigation, which moves
all of the top-level admin nav from the top of the page
into a sidebar. This is hidden behind a enable_admin_sidebar_navigation
site setting, and is opt-in for now.

This sidebar is dynamically shown whenever the user enters an
admin route in the UI, and is hidden and replaced with either
the:

* Main forum sidebar
* Chat sidebar

Depending on where they navigate to. For now, custom sections
are not supported in the admin sidebar.

This commit removes the experimental admin sidebar generation rake
task but keeps the experimental sidebar UI for now for further
testing; it just uses the real nav as the default now.
This commit is contained in:
Martin Brennan 2023-12-18 11:48:25 +10:00 committed by GitHub
parent 194c84b217
commit 6de00f89c2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 252 additions and 367 deletions

View File

@ -10,6 +10,7 @@ import { ADMIN_NAV_MAP } from "discourse/lib/sidebar/admin-nav-map";
import { resetPanelSections } from "discourse/lib/sidebar/custom-sections"; import { resetPanelSections } from "discourse/lib/sidebar/custom-sections";
import { ADMIN_PANEL } from "discourse/services/sidebar-state"; import { ADMIN_PANEL } from "discourse/services/sidebar-state";
// TODO (martin) (2024-02-01) Remove this experimental UI.
export default class AdminConfigAreaSidebarExperiment extends Component { export default class AdminConfigAreaSidebarExperiment extends Component {
@service adminSidebarExperimentStateManager; @service adminSidebarExperimentStateManager;
@service toasts; @service toasts;

View File

@ -1,4 +1,5 @@
import Controller from "@ember/controller"; import Controller from "@ember/controller";
import { readOnly } from "@ember/object/computed";
import { inject as service } from "@ember/service"; import { inject as service } from "@ember/service";
import { dasherize } from "@ember/string"; import { dasherize } from "@ember/string";
import discourseComputed from "discourse-common/utils/decorators"; import discourseComputed from "discourse-common/utils/decorators";
@ -6,6 +7,8 @@ import discourseComputed from "discourse-common/utils/decorators";
export default class AdminController extends Controller { export default class AdminController extends Controller {
@service router; @service router;
@readOnly("siteSettings.enable_admin_sidebar_navigation") showAdminSidebar;
@discourseComputed("siteSettings.enable_group_directory") @discourseComputed("siteSettings.enable_group_directory")
showGroups(enableGroupDirectory) { showGroups(enableGroupDirectory) {
return !enableGroupDirectory; return !enableGroupDirectory;

View File

@ -14,12 +14,7 @@ export default class AdminRoute extends DiscourseRoute {
} }
activate() { activate() {
if ( if (!this.siteSettings.enable_admin_sidebar_navigation) {
!this.siteSettings.userInAnyGroups(
"enable_experimental_admin_ui_groups",
this.currentUser
)
) {
return DiscourseURL.redirectTo("/admin"); return DiscourseURL.redirectTo("/admin");
} }

View File

@ -1,23 +1,35 @@
import { inject as service } from "@ember/service"; import { inject as service } from "@ember/service";
import DiscourseRoute from "discourse/routes/discourse"; import DiscourseRoute from "discourse/routes/discourse";
import { MAIN_PANEL } from "discourse/services/sidebar-state"; import { ADMIN_PANEL, MAIN_PANEL } from "discourse/services/sidebar-state";
import I18n from "discourse-i18n"; import I18n from "discourse-i18n";
export default class AdminRoute extends DiscourseRoute { export default class AdminRoute extends DiscourseRoute {
@service sidebarState; @service sidebarState;
@service siteSettings;
titleToken() { titleToken() {
return I18n.t("admin_title"); return I18n.t("admin_title");
} }
activate() { activate() {
if (this.siteSettings.enable_admin_sidebar_navigation) {
this.sidebarState.setPanel(ADMIN_PANEL);
this.sidebarState.setSeparatedMode();
this.sidebarState.hideSwitchPanelButtons();
}
this.controllerFor("application").setProperties({ this.controllerFor("application").setProperties({
showTop: false, showTop: false,
}); });
} }
deactivate() { deactivate(transition) {
this.controllerFor("application").set("showTop", true); this.controllerFor("application").set("showTop", true);
this.sidebarState.setPanel(MAIN_PANEL);
if (this.siteSettings.enable_admin_sidebar_navigation) {
if (!transition?.to.name.startsWith("admin")) {
this.sidebarState.setPanel(MAIN_PANEL);
}
}
} }
} }

View File

@ -4,37 +4,40 @@
<div class="row"> <div class="row">
<div class="full-width"> <div class="full-width">
<div class="admin-main-nav"> {{#unless this.showAdminSidebar}}
<ul class="nav nav-pills"> <div class="admin-main-nav">
<NavItem @route="admin.dashboard" @label="admin.dashboard.title" /> <ul class="nav nav-pills">
{{#if this.currentUser.admin}} <NavItem @route="admin.dashboard" @label="admin.dashboard.title" />
<NavItem {{#if this.currentUser.admin}}
@route="adminSiteSettings" <NavItem
@label="admin.site_settings.title" @route="adminSiteSettings"
/> @label="admin.site_settings.title"
{{/if}} />
<NavItem @route="adminUsers" @label="admin.users.title" />
{{#if this.showGroups}}
<NavItem @route="groups" @label="admin.groups.title" />
{{/if}}
{{#if this.showBadges}}
<NavItem @route="adminBadges" @label="admin.badges.title" />
{{/if}}
{{#if this.currentUser.admin}}
<NavItem @route="adminEmail" @label="admin.email.title" />
{{/if}}
<NavItem @route="adminLogs" @label="admin.logs.title" />
<NavItem @route="adminCustomize" @label="admin.customize.title" />
{{#if this.currentUser.admin}}
<NavItem @route="adminApi" @label="admin.api.title" />
{{#if this.siteSettings.enable_backups}}
<NavItem @route="admin.backups" @label="admin.backups.title" />
{{/if}} {{/if}}
<NavItem @route="adminPlugins" @label="admin.plugins.title" /> <NavItem @route="adminUsers" @label="admin.users.title" />
{{/if}} {{#if this.showGroups}}
<PluginOutlet @name="admin-menu" /> <NavItem @route="groups" @label="admin.groups.title" />
</ul> {{/if}}
</div> {{#if this.showBadges}}
<NavItem @route="adminBadges" @label="admin.badges.title" />
{{/if}}
{{#if this.currentUser.admin}}
<NavItem @route="adminEmail" @label="admin.email.title" />
{{/if}}
<NavItem @route="adminLogs" @label="admin.logs.title" />
<NavItem @route="adminCustomize" @label="admin.customize.title" />
{{#if this.currentUser.admin}}
<NavItem @route="adminApi" @label="admin.api.title" />
{{#if this.siteSettings.enable_backups}}
<NavItem @route="admin.backups" @label="admin.backups.title" />
{{/if}}
<NavItem @route="adminPlugins" @label="admin.plugins.title" />
{{/if}}
{{! TODO: What do we do with this?? How many plugins are using? }}
<PluginOutlet @name="admin-menu" />
</ul>
</div>
{{/unless}}
<div class="boxed white admin-content"> <div class="boxed white admin-content">
<div class="admin-contents {{this.adminContentsClassName}}"> <div class="admin-contents {{this.adminContentsClassName}}">

View File

@ -4,6 +4,7 @@ import {
addSidebarSection, addSidebarSection,
} from "discourse/lib/sidebar/custom-sections"; } from "discourse/lib/sidebar/custom-sections";
import { ADMIN_PANEL } from "discourse/services/sidebar-state"; import { ADMIN_PANEL } from "discourse/services/sidebar-state";
import I18n from "discourse-i18n";
function defineAdminSectionLink(BaseCustomSidebarSectionLink) { function defineAdminSectionLink(BaseCustomSidebarSectionLink) {
const SidebarAdminSectionLink = class extends BaseCustomSidebarSectionLink { const SidebarAdminSectionLink = class extends BaseCustomSidebarSectionLink {
@ -33,7 +34,9 @@ function defineAdminSectionLink(BaseCustomSidebarSectionLink) {
} }
get text() { get text() {
return this.adminSidebarNavLink.text; return this.adminSidebarNavLink.label
? I18n.t(this.adminSidebarNavLink.label)
: this.adminSidebarNavLink.text;
} }
get prefixType() { get prefixType() {
@ -77,7 +80,9 @@ function defineAdminSection(
} }
get text() { get text() {
return this.adminNavSectionData.text; return this.adminNavSectionData.label
? I18n.t(this.adminNavSectionData.label)
: this.adminNavSectionData.text;
} }
get links() { get links() {
@ -103,22 +108,47 @@ export function useAdminNavConfig(navMap) {
hideSectionHeader: true, hideSectionHeader: true,
links: [ links: [
{ {
name: "Back to Forum", name: "back_to_forum",
route: "discovery.latest", route: "discovery.latest",
text: "Back to Forum", label: "admin.back_to_forum",
icon: "arrow-left", icon: "arrow-left",
}, },
{ {
name: "Lobby", name: "admin_dashboard",
route: "admin-revamp.lobby", route: "admin.dashboard",
text: "Lobby", label: "admin.dashboard.title",
icon: "home", icon: "home",
}, },
{ {
name: "legacy", name: "admin_site_settings",
route: "admin", route: "adminSiteSettings",
text: "Legacy Admin", label: "admin.site_settings.title",
icon: "wrench", icon: "cog",
},
{
name: "admin_users",
route: "adminUsers",
label: "admin.users.title",
icon: "users",
},
{
name: "admin_reports",
route: "adminReports",
label: "admin.dashboard.reports_tab",
icon: "chart-pie",
},
{
name: "admin_plugins",
route: "adminPlugins",
label: "admin.plugins.title",
icon: "puzzle-piece",
},
{
name: "admin_badges",
route: "adminBadges",
label: "admin.badges.title",
icon: "certificate",
}, },
], ],
}, },
@ -157,12 +187,7 @@ export default {
return; return;
} }
if ( if (!this.siteSettings.enable_admin_sidebar_navigation) {
!this.siteSettings.userInAnyGroups(
"enable_experimental_admin_ui_groups",
this.currentUser
)
) {
return; return;
} }
@ -179,7 +204,19 @@ export default {
); );
const savedConfig = this.adminSidebarExperimentStateManager.navConfig; const savedConfig = this.adminSidebarExperimentStateManager.navConfig;
const navConfig = useAdminNavConfig(savedConfig || ADMIN_NAV_MAP); const navMap = savedConfig || ADMIN_NAV_MAP;
if (this.siteSettings.experimental_form_templates) {
navMap.findBy("name", "customize").links.push({
name: "admin_customize_form_templates",
route: "adminCustomizeFormTemplates",
label: "admin.form_templates.nav_title",
icon: "list",
});
}
const navConfig = useAdminNavConfig(navMap);
buildAdminSidebar(navConfig, adminSectionLinkClass); buildAdminSidebar(navConfig, adminSectionLinkClass);
}, },
}; };

View File

@ -1,232 +1,182 @@
// DO NOT EDIT THIS FILE!!!
// Update it by running `rake javascript:update_constants`
export const ADMIN_NAV_MAP = [ 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: "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", name: "email",
text: "Email", text: "Email",
links: [ links: [
{ name: "admin_email_sent", route: "adminEmail.sent", text: "Sent" }, {
name: "admin_email",
route: "adminEmail.index",
label: "admin.email.settings",
icon: "cog",
},
{
name: "admin_email_sent",
route: "adminEmail.sent",
label: "admin.email.sent",
icon: "arrow-right",
},
{ {
name: "admin_email_skipped", name: "admin_email_skipped",
route: "adminEmail.skipped", route: "adminEmail.skipped",
text: "Skipped", label: "admin.email.skipped",
icon: "angle-double-right",
}, },
{ {
name: "admin_email_bounced", name: "admin_email_bounced",
route: "adminEmail.bounced", route: "adminEmail.bounced",
text: "Bounced", label: "admin.email.bounced",
icon: "times",
}, },
{ {
name: "admin_email_received", name: "admin_email_received",
route: "adminEmail.received", route: "adminEmail.received",
text: "Received", label: "admin.email.received",
icon: "inbox",
}, },
{ {
name: "admin_email_rejected", name: "admin_email_rejected",
route: "adminEmail.rejected", route: "adminEmail.rejected",
text: "Rejected", label: "admin.email.rejected",
icon: "ban",
}, },
{
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", name: "logs",
text: "Logs", label: "admin.logs.title",
links: [ links: [
{ {
name: "admin_logs_staff_action_logs", name: "admin_logs_staff_action_logs",
route: "adminLogs.staffActionLogs", route: "adminLogs.staffActionLogs",
text: "Staff Action Logs", label: "admin.logs.staff_actions.title",
icon: "user-shield",
}, },
{ {
name: "admin_logs_screened_emails", name: "admin_logs_screened_emails",
route: "adminLogs.screenedEmails", route: "adminLogs.screenedEmails",
text: "Screened Emails", label: "admin.logs.screened_emails.title",
icon: "envelope",
}, },
{ {
name: "admin_logs_screened_ip_addresses", name: "admin_logs_screened_ip_addresses",
route: "adminLogs.screenedIpAddresses", route: "adminLogs.screenedIpAddresses",
text: "Screened Ip Addresses", label: "admin.logs.screened_ips.title",
icon: "globe",
}, },
{ {
name: "admin_logs_screened_urls", name: "admin_logs_screened_urls",
route: "adminLogs.screenedUrls", route: "adminLogs.screenedUrls",
text: "Screened Urls", label: "admin.logs.screened_urls.title",
icon: "globe",
}, },
{ {
name: "admin_logs_search_logs", name: "admin_logs_search_logs",
route: "adminSearchLogs", route: "adminSearchLogs",
text: "Search Logs", label: "admin.logs.search_logs.title",
icon: "search",
}, },
{
name: "admin_logs_search_logs_term",
route: "adminSearchLogs.term",
text: "Search Term",
},
{ name: "admin_logs", route: "adminLogs", text: "Logs" },
], ],
}, },
{ {
name: "customize", name: "customize",
text: "Customize", label: "admin.customize.title",
links: [ links: [
{ name: "admin_customize", route: "adminCustomize", text: "Customize" },
{ {
name: "admin_customize_themes", name: "admin_customize_themes",
route: "adminCustomizeThemes", route: "adminCustomizeThemes",
text: "Themes", label: "admin.customize.theme.title",
icon: "paint-brush",
}, },
{ {
name: "admin_customize_colors", name: "admin_customize_colors",
route: "adminCustomize.colors", route: "adminCustomize.colors",
text: "Colors", label: "admin.customize.colors.title",
}, icon: "palette",
{
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", name: "admin_customize_site_texts",
route: "adminSiteText", route: "adminSiteText",
text: "Site Texts", label: "admin.site_text.title",
icon: "language",
}, },
{ {
name: "admin_customize_email_templates", name: "admin_customize_email_templates",
route: "adminCustomizeEmailTemplates", route: "adminCustomizeEmailTemplates",
text: "Email Templates", label: "admin.email.templates_title",
}, icon: "envelope",
{
name: "admin_customize_robots",
route: "adminCustomizeRobotsTxt",
text: "Robots",
}, },
{ {
name: "admin_customize_email_style", name: "admin_customize_email_style",
route: "adminCustomizeEmailStyle", route: "adminCustomizeEmailStyle",
text: "Email Style", label: "admin.customize.email_style.title",
icon: "envelope",
},
{
name: "admin_customize_user_fields",
route: "adminUserFields",
label: "admin.user_fields.title",
icon: "user-edit",
},
{
name: "admin_customize_emojis",
route: "adminEmojis",
label: "admin.emoji.title",
icon: "discourse-emojis",
},
{
name: "admin_customize_permalinks",
route: "adminPermalinks",
label: "admin.permalink.title",
icon: "link",
},
{
name: "admin_customize_embedding",
route: "adminEmbedding",
label: "admin.embedding.title",
icon: "code",
}, },
{ {
name: "admin_customize_watched_words", name: "admin_customize_watched_words",
route: "adminWatchedWords", route: "adminWatchedWords",
text: "Watched Words", label: "admin.watched_words.title",
}, icon: "eye",
],
},
{
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", name: "api",
text: "Api", label: "admin.api.title",
links: [ links: [
{ name: "admin_api_keys", route: "adminApiKeys", text: "Keys" }, {
name: "admin_api_keys",
route: "adminApiKeys",
icon: "key",
label: "admin.api.keys",
},
{ {
name: "admin_api_web_hooks", name: "admin_api_web_hooks",
route: "adminWebHooks", route: "adminWebHooks",
text: "Web Hooks", label: "admin.web_hooks.title",
icon: "globe",
}, },
{ name: "admin_api", route: "adminApi", text: "Api" },
], ],
}, },
{ {
name: "backups", name: "backups",
text: "Backups", label: "admin.backups.menu.backups",
links: [ links: [
{ name: "admin_backups_logs", route: "admin.backups.logs", text: "Logs" }, {
{ name: "admin_backups", route: "admin.backups", text: "Backups" }, name: "admin_backups",
route: "admin.backups.index",
label: "admin.backups.menu.backups",
icon: "archive",
},
{
name: "admin_backups_logs",
route: "admin.backups.logs",
label: "admin.backups.menu.logs",
icon: "stream",
},
], ],
}, },
{
name: "badges",
text: "Badges",
links: [{ name: "admin_badges", route: "adminBadges", text: "Badges" }],
},
]; ];

View File

@ -31,10 +31,7 @@ export default class AdminRevampSectionLink extends BaseSectionLink {
return ( return (
this.currentUser.staff && this.currentUser.staff &&
this.siteSettings.userInAnyGroups( this.siteSettings.enable_admin_sidebar_navigation
"enable_experimental_admin_ui_groups",
this.currentUser
)
); );
} }

View File

@ -22,12 +22,6 @@ class SidebarUrl < ActiveRecord::Base
}, },
{ name: "Review", path: "/review", icon: "flag", segment: SidebarUrl.segments["primary"] }, { name: "Review", path: "/review", icon: "flag", segment: SidebarUrl.segments["primary"] },
{ name: "Admin", path: "/admin", icon: "wrench", 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: "Users", path: "/u", icon: "users", segment: SidebarUrl.segments["secondary"] },
{ {
name: "About", name: "About",

View File

@ -4722,6 +4722,7 @@ en:
admin: admin:
title: "Discourse Admin" title: "Discourse Admin"
moderator: "Moderator" moderator: "Moderator"
back_to_forum: "Back to Forum"
tags: tags:
remove_muted_tags_from_latest: remove_muted_tags_from_latest:
@ -4911,6 +4912,7 @@ en:
user: "User" user: "User"
title: "API" title: "API"
key: "Key" key: "Key"
keys: "Keys"
created: Created created: Created
updated: Updated updated: Updated
last_used: Last Used last_used: Last Used
@ -5529,6 +5531,7 @@ en:
title: "Emails" title: "Emails"
settings: "Settings" settings: "Settings"
templates: "Templates" templates: "Templates"
templates_title: "Email Templates"
preview_digest: "Preview Summary" preview_digest: "Preview Summary"
advanced_test: advanced_test:
title: "Advanced Test" title: "Advanced Test"

View File

@ -2490,6 +2490,7 @@ en:
enable_experimental_lightbox: "EXPERIMENTAL: Replace the default image lightbox with the revamped design." enable_experimental_lightbox: "EXPERIMENTAL: Replace the default image lightbox with the revamped design."
experimental_form_templates: "EXPERIMENTAL: Enable the form templates feature. <b>After enabled,</b> manage the templates at <a href='%{base_path}/admin/customize/form-templates'>Customize / Templates</a>." experimental_form_templates: "EXPERIMENTAL: Enable the form templates feature. <b>After enabled,</b> manage the templates at <a href='%{base_path}/admin/customize/form-templates'>Customize / Templates</a>."
enable_admin_sidebar_navigation: "EXPERIMENTAL: Enable sidebar navigation for the admin UI, which replaces the top-level admin navigation buttons."
page_loading_indicator: "Configure the loading indicator which appears during page navigations within Discourse. 'Spinner' is a full page indicator. 'Slider' shows a narrow bar at the top of the screen." page_loading_indicator: "Configure the loading indicator which appears during page navigations within Discourse. 'Spinner' is a full page indicator. 'Slider' shows a narrow bar at the top of the screen."
show_user_menu_avatars: "Show user avatars in the user menu" show_user_menu_avatars: "Show user avatars in the user menu"

View File

@ -2280,6 +2280,9 @@ developer:
refresh: true refresh: true
hidden: true hidden: true
client: true client: true
enable_admin_sidebar_navigation:
default: false
client: true
lazy_load_categories: lazy_load_categories:
default: false default: false
client: true client: true

View File

@ -19,6 +19,7 @@ module SvgSprite
archive archive
arrow-down arrow-down
arrow-left arrow-left
arrow-right
arrow-up arrow-up
arrows-alt-h arrows-alt-h
arrows-alt-v arrows-alt-v
@ -171,6 +172,7 @@ module SvgSprite
mobile-alt mobile-alt
moon moon
paint-brush paint-brush
palette
paper-plane paper-plane
pause pause
pencil-alt pencil-alt

View File

@ -129,140 +129,6 @@ def absolute_sourcemap(dest)
end end
end end
def generate_admin_sidebar_nav_map
vague_categories = { "root" => [] }
admin_routes =
Rails
.application
.routes
.routes
.map do |route|
next if route.verb != "GET"
path = route.path.spec.to_s.gsub("(.:format)", "")
next if !path.include?("admin")
next if path.include?("/:") || path.include?("admin-login")
path
end
.compact
# TODO (martin): This will generate the engine routes based on installed plugins,
# so it is not generic enough to use here. Need to think of another way to do
# this and reconcile with the Ember routes from the client; maybe some button
# that does it at runtime for this experiment?
engine_routes = []
# engine_routes = Rails::Engine
# .subclasses
# .map do |engine|
# engine
# .routes
# .routes
# .map do |route|
# next if route.verb != "GET"
# path = route.path.spec.to_s.gsub("(.:format)", "")
# next if !path.include?("admin")
# next if path.include?("/:") || path.include?("admin-login")
# path
# end
# .compact
# end
# .flatten
admin_routes = admin_routes.concat(engine_routes)
admin_routes.each do |path|
split_path = path.split("/")
if split_path.length >= 3
vague_categories[split_path[2]] ||= []
vague_categories[split_path[2]] << { path: path }
else
vague_categories["root"] << { path: path }
end
end
# rubocop:disable Lint/Void
# Copy this JS to your browser to get the Ember routes.
<<~JS
let routeMap = {}
for (const [key, value] of Object.entries(
Object.fromEntries(
Object.entries(
Discourse.__container__.lookup("service:router")._router._routerMicrolib
.recognizer.names
).filter(([key]) => key.includes("admin"))
)
)) {
let route = value.segments
.map((s) => s.value)
.join("/")
.replace("//", "/");
if (
route.includes("dummy") ||
route.includes("loading") ||
route.includes("_id") ||
route.includes("admin-invite")
) {
continue;
}
routeMap[key] = route;
}
console.log(JSON.stringify(routeMap));
JS
# rubocop:enable Lint/Void
# Paste the output below between ROUTE_MAP.
#
ember_route_map = <<~ROUTE_MAP
{"admin.dashboard.general":"/admin/","admin.dashboard":"/admin/","admin":"/admin/","admin.dashboardModeration":"/admin/dashboard/moderation","admin.dashboardSecurity":"/admin/dashboard/security","admin.dashboardReports":"/admin/dashboard/reports","adminSiteSettings.index":"/admin/site_settings/","adminSiteSettings":"/admin/site_settings/","adminEmail.sent":"/admin/email/sent","adminEmail.skipped":"/admin/email/skipped","adminEmail.bounced":"/admin/email/bounced","adminEmail.received":"/admin/email/received","adminEmail.rejected":"/admin/email/rejected","adminEmail.previewDigest":"/admin/email/preview-digest","adminEmail.advancedTest":"/admin/email/advanced-test","adminEmail.index":"/admin/email/","adminEmail":"/admin/email/","adminCustomize.colors.index":"/admin/customize/colors/","adminCustomize.colors":"/admin/customize/colors/","adminCustomizeThemes.index":"/admin/customize/themes/","adminCustomizeThemes":"/admin/customize/themes/","adminSiteText.edit":"/admin/customize/site_texts/id","adminSiteText.index":"/admin/customize/site_texts/","adminSiteText":"/admin/customize/site_texts/","adminUserFields":"/admin/customize/user_fields","adminEmojis":"/admin/customize/emojis","adminPermalinks":"/admin/customize/permalinks","adminEmbedding":"/admin/customize/embedding","adminCustomizeEmailTemplates.edit":"/admin/customize/email_templates/id","adminCustomizeEmailTemplates.index":"/admin/customize/email_templates/","adminCustomizeEmailTemplates":"/admin/customize/email_templates/","adminCustomizeRobotsTxt":"/admin/customize/robots","adminCustomizeEmailStyle.edit":"/admin/customize/email_style/field_name","adminCustomizeEmailStyle.index":"/admin/customize/email_style/","adminCustomizeEmailStyle":"/admin/customize/email_style/","adminCustomizeFormTemplates.new":"/admin/customize/form-templates/new","adminCustomizeFormTemplates.edit":"/admin/customize/form-templates/id","adminCustomizeFormTemplates.index":"/admin/customize/form-templates/","adminCustomizeFormTemplates":"/admin/customize/form-templates/","adminWatchedWords.index":"/admin/customize/watched_words/","adminWatchedWords":"/admin/customize/watched_words/","adminCustomize.index":"/admin/customize/","adminCustomize":"/admin/customize/","adminApiKeys.new":"/admin/api/keys/new","adminApiKeys.index":"/admin/api/keys/","adminApiKeys":"/admin/api/keys/","adminWebHooks.index":"/admin/api/web_hooks/","adminWebHooks":"/admin/api/web_hooks/","adminApi.index":"/admin/api/","adminApi":"/admin/api/","admin.backups.logs":"/admin/backups/logs","admin.backups.index":"/admin/backups/","admin.backups":"/admin/backups/","adminReports.show":"/admin/reports/type","adminReports.index":"/admin/reports/","adminReports":"/admin/reports/","adminLogs.staffActionLogs":"/admin/logs/staff_action_logs","adminLogs.screenedEmails":"/admin/logs/screened_emails","adminLogs.screenedIpAddresses":"/admin/logs/screened_ip_addresses","adminLogs.screenedUrls":"/admin/logs/screened_urls","adminSearchLogs.index":"/admin/logs/search_logs/","adminSearchLogs":"/admin/logs/search_logs/","adminSearchLogs.term":"/admin/logs/search_logs/term","adminLogs.index":"/admin/logs/","adminLogs":"/admin/logs/","adminUsersList.show":"/admin/users/list/filter","adminUsersList.index":"/admin/users/list/","adminUsersList":"/admin/users/list/","adminUsers.index":"/admin/users/","adminUsers":"/admin/users/","adminBadges.index":"/admin/badges/","adminBadges":"/admin/badges/","adminPlugins.index":"/admin/plugins/","adminPlugins":"/admin/plugins/","admin-revamp.lobby":"/admin-revamp/","admin-revamp":"/admin-revamp/","admin-revamp.config.area":"/admin-revamp/config/area","admin-revamp.config.index":"/admin-revamp/config/","admin-revamp.config":"/admin-revamp/config/"}
ROUTE_MAP
ember_route_map = JSON.parse(ember_route_map)
# Match the Ember routes to the rails routes.
vague_categories.each do |category, route_data|
route_data.each do |rails_route|
ember_route_map.each do |ember_route_name, ember_path|
rails_route[:ember_route] = ember_route_name if ember_path == rails_route[:path] ||
ember_path == rails_route[:path] + "/"
end
end
end
# Remove all rails routes that don't have an Ember equivalent.
vague_categories.each do |category, route_data|
vague_categories[category] = route_data.reject { |rails_route| !rails_route.key?(:ember_route) }
end
# Remove all categories that don't have any routes (meaning they are all rails-only).
vague_categories.each do |category, route_data|
vague_categories.delete(category) if route_data.length == 0
end
# Output in the format needed for sidebar sections and links.
vague_categories.map do |category, route_data|
category_text = category.titleize.gsub("Admin ", "")
{
name: category,
text: category_text,
links:
route_data.map do |rails_route|
{
name: rails_route[:path].split("/").compact_blank.join("_").chomp,
route: rails_route[:ember_route],
text:
rails_route[:path]
.split("/")
.compact_blank
.join(" ")
.chomp
.titleize
.gsub("Admin ", "")
.gsub("#{category_text} ", ""),
}
end,
}
end
end
task "javascript:update_constants" => :environment do task "javascript:update_constants" => :environment do
task_name = "update_constants" task_name = "update_constants"
@ -296,10 +162,6 @@ task "javascript:update_constants" => :environment do
export const AUTO_GROUPS = #{auto_groups.to_json}; export const AUTO_GROUPS = #{auto_groups.to_json};
JS JS
write_template("discourse/app/lib/sidebar/admin-nav-map.js", task_name, <<~JS)
export const ADMIN_NAV_MAP = #{generate_admin_sidebar_nav_map.to_json}
JS
pretty_notifications = Notification.types.map { |n| " #{n[0]}: #{n[1]}," }.join("\n") pretty_notifications = Notification.types.map { |n| " #{n[0]}: #{n[1]}," }.join("\n")
write_template("discourse/tests/fixtures/concerns/notification-types.js", task_name, <<~JS) write_template("discourse/tests/fixtures/concerns/notification-types.js", task_name, <<~JS)

View File

@ -22,18 +22,7 @@ RSpec.describe SidebarSection do
expect(community_section.reload.title).to eq("Community") expect(community_section.reload.title).to eq("Community")
expect(community_section.sidebar_section_links.all.map { |link| link.linkable.name }).to eq( 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
end end

View File

@ -2,18 +2,29 @@
describe "Admin Revamp | Sidebar Navigation", type: :system do describe "Admin Revamp | Sidebar Navigation", type: :system do
fab!(:admin) fab!(:admin)
let(:sidebar_page) { PageObjects::Components::NavigationMenu::Sidebar.new } let(:sidebar) { PageObjects::Components::NavigationMenu::Sidebar.new }
before do before do
SiteSetting.enable_experimental_admin_ui_groups = Group::AUTO_GROUPS[:staff] SiteSetting.enable_admin_sidebar_navigation = true
SidebarSection.find_by(section_type: "community").reset_community!
sign_in(admin) sign_in(admin)
end end
it "navigates to the admin revamp from the sidebar" do it "shows the sidebar when navigating to an admin route and hides it when leaving" do
visit("/latest") visit("/latest")
sidebar_page.click_section_link("Admin Revamp") sidebar.click_link_in_section("community", "admin")
expect(page).to have_content("Lobby") expect(page).to have_current_path("/admin")
expect(page).to have_content("Legacy Admin") expect(sidebar).to be_visible
expect(page).to have_no_css(".admin-main-nav")
sidebar.click_link_in_section("admin-nav-section-root", "back_to_forum")
expect(page).to have_current_path("/latest")
expect(sidebar).to have_no_section("admin-nav-section-root")
end
it "does not show the admin sidebar if the setting is disabled" do
SiteSetting.enable_admin_sidebar_navigation = false
visit("/latest")
sidebar.click_link_in_section("community", "admin")
expect(page).to have_current_path("/admin")
expect(sidebar).to have_no_section("admin-nav-section-root")
end end
end end

View File

@ -4,16 +4,34 @@ module PageObjects
module Components module Components
module NavigationMenu module NavigationMenu
class Base < PageObjects::Components::Base class Base < PageObjects::Components::Base
def community_section SIDEBAR_SECTION_LINK_SELECTOR = "sidebar-section-link"
find(".sidebar-section[data-section-name='community']")
def visible?
has_css?("#d-sidebar.sidebar-container")
end end
SIDEBAR_SECTION_LINK_SELECTOR = "sidebar-section-link" def hidden?
has_no_css?("#d-sidebar.sidebar-container")
end
def community_section
find_section("community")
end
def find_section(name)
find(".sidebar-section[data-section-name='#{name}']")
end
def click_section_link(name) def click_section_link(name)
find(".#{SIDEBAR_SECTION_LINK_SELECTOR}", text: name).click find(".#{SIDEBAR_SECTION_LINK_SELECTOR}", text: name).click
end end
def click_link_in_section(section_name, link_name)
find_section(section_name.parameterize).find(
".#{SIDEBAR_SECTION_LINK_SELECTOR}[data-link-name=\"#{link_name.parameterize}\"]",
).click
end
def has_one_active_section_link? def has_one_active_section_link?
has_css?(".#{SIDEBAR_SECTION_LINK_SELECTOR}--active", count: 1) has_css?(".#{SIDEBAR_SECTION_LINK_SELECTOR}--active", count: 1)
end end
@ -30,6 +48,10 @@ module PageObjects
has_css?(".sidebar-sections [data-section-name='#{name.parameterize}']") has_css?(".sidebar-sections [data-section-name='#{name.parameterize}']")
end end
def has_no_section?(name)
has_no_css?(".sidebar-sections [data-section-name='#{name.parameterize}']")
end
def switch_to_chat def switch_to_chat
find(".sidebar__panel-switch-button[data-key='chat']").click find(".sidebar__panel-switch-button[data-key='chat']").click
end end