FEATURE: Initial version of experimental admin search (#31299)

This feature allows admins to find what they are
looking for in the admin interface via a search modal.
This replaces the admin sidebar filter
as the focus of the Ctrl+/ command, but the sidebar
filter can also still be used. Perhaps at some point
we may remove it or change the shortcut.

The search modal presents the following data for filtering:

* A list of all admin pages, the same as the sidebar,
   except also showing "third level" pages like
   "Email > Skipped"
* All site settings
* Themes
* Components
* Reports

Admins can also filter which types of items are shown in the modal,
for example hiding Settings if they know they are looking for a Page.

In this PR, I also have the following fixes:

* Site setting filters now clear when moving between
   filtered site setting pages, previously it was super
   sticky from Ember
* Many translations were moved around, instead of being
   in various namespaces for the sidebar links and the admin
   page titles and descriptions, now everything is under
   `admin.config` namespace, this makes it way easier to reuse
   this text for pages, search, and sidebar, and if you change it
   in one place then it is changed everywhere.

---------

Co-authored-by: Ella <ella.estigoy@gmail.com>
This commit is contained in:
Martin Brennan
2025-02-21 11:59:24 +10:00
committed by GitHub
parent a77d469eeb
commit e26a1175d7
90 changed files with 1582 additions and 525 deletions

View File

@ -16,7 +16,6 @@ export default class AdminAreaSettings extends Component {
@service siteSettings;
@service router;
@tracked settings = [];
@tracked filter = "";
@tracked loading = false;
@tracked showBreadcrumb = this.args.showBreadcrumb ?? true;
@ -37,7 +36,6 @@ export default class AdminAreaSettings extends Component {
@bind
async #loadSettings() {
this.loading = true;
this.filter = this.args.filter;
try {
const result = await ajax("/admin/config/site_settings.json", {
data: {
@ -63,6 +61,10 @@ export default class AdminAreaSettings extends Component {
}
}
get filter() {
return this.args.filter ?? "";
}
@action
adminSettingsFilterChangedCallback(filterData) {
this.args.adminSettingsFilterChangedCallback(filterData.filter);

View File

@ -0,0 +1,27 @@
import { concat, fn, get } from "@ember/helper";
import DButton from "discourse/components/d-button";
import concatClass from "discourse/helpers/concat-class";
import { i18n } from "discourse-i18n";
const AdminSearchFilters = <template>
<div class="admin-search__filters">
{{#each @types as |type|}}
<span class={{concat "admin-search__filter --" type}}>
<DButton
class={{concatClass
"btn-small admin-search__filter-item"
(if (get @typeFilters type) "is-active")
}}
@translatedLabel={{i18n
(concat "admin.search.result_types." type)
count=2
}}
@icon={{if (get @typeFilters type) "check" "far-circle"}}
@action={{fn @toggleTypeFilter type}}
/>
</span>
{{/each}}
</div>
</template>;
export default AdminSearchFilters;

View File

@ -0,0 +1,124 @@
import Component from "@glimmer/component";
import { tracked } from "@glimmer/tracking";
import { on } from "@ember/modifier";
import { action } from "@ember/object";
import { service } from "@ember/service";
import { htmlSafe } from "@ember/template";
import { TrackedObject } from "@ember-compat/tracked-built-ins";
import ConditionalLoadingSpinner from "discourse/components/conditional-loading-spinner";
import DButton from "discourse/components/d-button";
import icon from "discourse/helpers/d-icon";
import discourseDebounce from "discourse/lib/debounce";
import { INPUT_DELAY } from "discourse/lib/environment";
import autoFocus from "discourse/modifiers/auto-focus";
import AdminSearchFilters from "admin/components/admin-search-filters";
import { RESULT_TYPES } from "admin/services/admin-search-data-source";
export default class AdminSearch extends Component {
@service adminSearchDataSource;
@tracked filter = "";
@tracked searchResults = [];
@tracked showFilters = false;
@tracked loading = false;
typeFilters = new TrackedObject({
page: true,
setting: true,
theme: true,
component: true,
report: true,
});
constructor() {
super(...arguments);
this.adminSearchDataSource.buildMap();
}
get visibleTypes() {
return Object.keys(this.typeFilters).filter(
(type) => this.typeFilters[type]
);
}
get showLoadingSpinner() {
return !this.adminSearchDataSource.isLoaded || this.loading;
}
@action
toggleFilters() {
this.showFilters = !this.showFilters;
}
@action
toggleTypeFilter(type) {
this.typeFilters[type] = !this.typeFilters[type];
this.search();
}
@action
changeSearchTerm(event) {
this.searchResults = [];
this.filter = event.target.value;
this.loading = true;
this.search();
}
@action
search() {
discourseDebounce(this, this.#search, INPUT_DELAY);
}
#search() {
this.searchResults = this.adminSearchDataSource.search(this.filter, {
types: this.visibleTypes,
});
this.loading = false;
}
<template>
<div class="admin-search__input-container">
<div class="admin-search__input-group">
{{icon "magnifying-glass" class="admin-search__input-icon"}}
<input
type="text"
class="admin-search__input-field"
{{autoFocus}}
{{on "input" this.changeSearchTerm}}
/>
</div>
<DButton class="btn-flat" @icon="filter" @action={{this.toggleFilters}} />
</div>
{{#if this.showFilters}}
<AdminSearchFilters
@toggleTypeFilter={{this.toggleTypeFilter}}
@typeFilters={{this.typeFilters}}
@types={{RESULT_TYPES}}
/>
{{/if}}
<div class="admin-search__results">
<ConditionalLoadingSpinner @condition={{this.showLoadingSpinner}}>
{{#each this.searchResults as |result|}}
<div class="admin-search__result">
<a href={{result.url}}>
<div class="admin-search__result-name">
{{#if result.icon}}
{{icon result.icon}}
{{/if}}
<span
class="admin-search__result-name-label"
>{{result.label}}</span>
</div>
{{#if result.description}}
<div class="admin-search__result-description">{{htmlSafe
result.description
}}</div>
{{/if}}
</a>
</div>
{{/each}}
</ConditionalLoadingSpinner>
</div>
</template>
}

View File

@ -0,0 +1,20 @@
import Component from "@glimmer/component";
import { service } from "@ember/service";
import DModal from "discourse/components/d-modal";
import AdminSearch from "admin/components/admin-search";
export default class AdminSearchModal extends Component {
@service currentUser;
<template>
<DModal
@closeModal={{@closeModal}}
class="admin-search-modal"
@title="admin.search.modal_title"
@inline={{@inline}}
@hideHeader={{true}}
>
<AdminSearch />
</DModal>
</template>
}

View File

@ -1,13 +1,19 @@
import { tracked } from "@glimmer/tracking";
import Controller from "@ember/controller";
import { action } from "@ember/object";
export default class AdminAreaSettingsBaseController extends Controller {
@tracked filter = "";
queryParams = ["filter"];
filter = "";
queryParams = [
{
filter: { replace: true },
},
];
@action
adminSettingsFilterChangedCallback(filter) {
this.filter = filter;
if (this.filter === filter) {
return;
}
this.set("filter", filter);
}
}

View File

@ -23,6 +23,8 @@ export default class AdminSiteSettingsController extends Controller {
return;
}
this.set("filter", filterData.filter);
if (isEmpty(filterData.filter) && !filterData.onlyOverridden) {
this.set("visibleSiteSettings", this.allSiteSettings);
if (this.categoryNameKey === "all_results") {
@ -31,8 +33,6 @@ export default class AdminSiteSettingsController extends Controller {
return;
}
this.set("filter", filterData.filter);
const matchesGroupedByCategory = this.siteSettingFilter.filterSettings(
filterData.filter,
{ onlyOverridden: filterData.onlyOverridden }

View File

@ -11,6 +11,7 @@ import { ajax } from "discourse/lib/ajax";
import { fmt, propertyNotEqual } from "discourse/lib/computed";
import { SITE_SETTING_REQUIRES_CONFIRMATION_TYPES } from "discourse/lib/constants";
import { deepEqual } from "discourse/lib/object";
import { humanizedSettingName } from "discourse/lib/site-settings-utils";
import { splitString } from "discourse/lib/utilities";
import { i18n } from "discourse-i18n";
import SiteSettingDefaultCategoriesModal from "../components/modal/site-setting-default-categories";
@ -82,79 +83,6 @@ const DEFAULT_USER_PREFERENCES = [
"default_sidebar_show_count_of_new_items",
];
const ACRONYMS = new Set([
"acl",
"ai",
"api",
"bg",
"cdn",
"cors",
"cta",
"dm",
"eu",
"faq",
"fg",
"ga",
"gb",
"gtm",
"hd",
"http",
"https",
"iam",
"id",
"imap",
"ip",
"jpg",
"json",
"kb",
"mb",
"oidc",
"pm",
"png",
"pop3",
"s3",
"smtp",
"svg",
"tl",
"tl0",
"tl1",
"tl2",
"tl3",
"tl4",
"tld",
"txt",
"url",
"ux",
]);
const MIXED_CASE = [
["adobe analytics", "Adobe Analytics"],
["android", "Android"],
["chinese", "Chinese"],
["discord", "Discord"],
["discourse", "Discourse"],
["discourse connect", "Discourse Connect"],
["discourse discover", "Discourse Discover"],
["discourse narrative bot", "Discourse Narrative Bot"],
["facebook", "Facebook"],
["github", "GitHub"],
["google", "Google"],
["gravatar", "Gravatar"],
["gravatars", "Gravatars"],
["ios", "iOS"],
["japanese", "Japanese"],
["linkedin", "LinkedIn"],
["oauth2", "OAuth2"],
["opengraph", "OpenGraph"],
["powered by discourse", "Powered by Discourse"],
["tiktok", "TikTok"],
["tos", "ToS"],
["twitter", "Twitter"],
["vimeo", "Vimeo"],
["wordpress", "WordPress"],
["youtube", "YouTube"],
];
export default Mixin.create({
modal: service(),
router: service(),
@ -215,29 +143,7 @@ export default Mixin.create({
}),
settingName: computed("setting.setting", "setting.label", function () {
const setting = this.setting?.setting;
const label = this.setting?.label;
const name = label || setting.replace(/\_/g, " ");
const formattedName = (name.charAt(0).toUpperCase() + name.slice(1))
.split(" ")
.map((word) =>
ACRONYMS.has(word.toLowerCase()) ? word.toUpperCase() : word
)
.map((word) => {
if (word.endsWith("s")) {
const singular = word.slice(0, -1).toLowerCase();
return ACRONYMS.has(singular) ? singular.toUpperCase() + "s" : word;
}
return word;
})
.join(" ");
return MIXED_CASE.reduce(
(acc, [key, value]) =>
acc.replaceAll(new RegExp(`\\b${key}\\b`, "gi"), value),
formattedName
);
return humanizedSettingName(this.setting.setting, this.setting.label);
}),
componentType: computed("type", function () {

View File

@ -4,7 +4,7 @@ import { i18n } from "discourse-i18n";
export default class AdminConfigAboutRoute extends DiscourseRoute {
titleToken() {
return i18n("admin.community.sidebar_link.about_your_site");
return i18n("admin.config.about.title");
}
model() {

View File

@ -1,8 +1,8 @@
import DiscourseRoute from "discourse/routes/discourse";
import { i18n } from "discourse-i18n";
import AdminConfigWithSettingsRoute from "./admin-config-with-settings-route";
export default class AdminConfigDeveloperRoute extends DiscourseRoute {
export default class AdminConfigDeveloperRoute extends AdminConfigWithSettingsRoute {
titleToken() {
return i18n("admin.advanced.sidebar_link.developer");
return i18n("admin.config.developer.title");
}
}

View File

@ -1,8 +1,8 @@
import DiscourseRoute from "discourse/routes/discourse";
import { i18n } from "discourse-i18n";
import AdminConfigWithSettingsRoute from "./admin-config-with-settings-route";
export default class AdminConfigExperimentalRoute extends DiscourseRoute {
export default class AdminConfigExperimentalRoute extends AdminConfigWithSettingsRoute {
titleToken() {
return i18n("admin.advanced.sidebar_link.experimental");
return i18n("admin.config.experimental.title");
}
}

View File

@ -1,8 +1,8 @@
import DiscourseRoute from "discourse/routes/discourse";
import { i18n } from "discourse-i18n";
import AdminConfigWithSettingsRoute from "./admin-config-with-settings-route";
export default class AdminConfigFilesRoute extends DiscourseRoute {
export default class AdminConfigFilesRoute extends AdminConfigWithSettingsRoute {
titleToken() {
return i18n("admin.advanced.sidebar_link.files");
return i18n("admin.config.files.title");
}
}

View File

@ -1,8 +1,14 @@
import DiscourseRoute from "discourse/routes/discourse";
import { i18n } from "discourse-i18n";
export default class AdminConfigFlagsIndexRoute extends DiscourseRoute {
export default class AdminConfigFlagsSettingsRoute extends DiscourseRoute {
titleToken() {
return i18n("admin.config_areas.flags.settings");
}
resetController(controller, isExiting) {
if (isExiting) {
controller.set("filter", "");
}
}
}

View File

@ -1,8 +1,8 @@
import DiscourseRoute from "discourse/routes/discourse";
import { i18n } from "discourse-i18n";
import AdminConfigWithSettingsRoute from "./admin-config-with-settings-route";
export default class AdminConfigFontsRoute extends DiscourseRoute {
export default class AdminConfigFontsRoute extends AdminConfigWithSettingsRoute {
titleToken() {
return i18n("admin.appearance.sidebar_link.font_style");
return i18n("admin.config.font_style.title");
}
}

View File

@ -1,8 +1,8 @@
import DiscourseRoute from "discourse/routes/discourse";
import { i18n } from "discourse-i18n";
import AdminConfigWithSettingsRoute from "./admin-config-with-settings-route";
export default class AdminConfigGroupPermissionsRoute extends DiscourseRoute {
export default class AdminConfigGroupPermissionsRoute extends AdminConfigWithSettingsRoute {
titleToken() {
return i18n("admin.community.sidebar_link.group_permissions");
return i18n("admin.config.group_permissions.title");
}
}

View File

@ -1,8 +1,8 @@
import DiscourseRoute from "discourse/routes/discourse";
import { i18n } from "discourse-i18n";
import AdminConfigWithSettingsRoute from "./admin-config-with-settings-route";
export default class AdminConfigLegalRoute extends DiscourseRoute {
export default class AdminConfigLegalRoute extends AdminConfigWithSettingsRoute {
titleToken() {
return i18n("admin.community.sidebar_link.legal");
return i18n("admin.config.legal.title");
}
}

View File

@ -1,8 +1,8 @@
import DiscourseRoute from "discourse/routes/discourse";
import { i18n } from "discourse-i18n";
import AdminConfigWithSettingsRoute from "./admin-config-with-settings-route";
export default class AdminConfigLocalizationRoute extends DiscourseRoute {
export default class AdminConfigLocalizationRoute extends AdminConfigWithSettingsRoute {
titleToken() {
return i18n("admin.community.sidebar_link.localization.title");
return i18n("admin.config.localization.title");
}
}

View File

@ -1,8 +1,8 @@
import DiscourseRoute from "discourse/routes/discourse";
import { i18n } from "discourse-i18n";
import AdminConfigWithSettingsRoute from "./admin-config-with-settings-route";
export default class AdminConfigLoginAndAuthenticationRoute extends DiscourseRoute {
export default class AdminConfigLoginAndAuthenticationRoute extends AdminConfigWithSettingsRoute {
titleToken() {
return i18n("admin.community.sidebar_link.login_and_authentication");
return i18n("admin.config.login_and_authentication.title");
}
}

View File

@ -1,8 +1,8 @@
import DiscourseRoute from "discourse/routes/discourse";
import { i18n } from "discourse-i18n";
import AdminConfigWithSettingsRoute from "./admin-config-with-settings-route";
export default class AdminConfigLogoRoute extends DiscourseRoute {
export default class AdminConfigLogoRoute extends AdminConfigWithSettingsRoute {
titleToken() {
return i18n("admin.appearance.sidebar_link.site_logo");
return i18n("admin.config.logo.title");
}
}

View File

@ -1,7 +1,7 @@
import { service } from "@ember/service";
import DiscourseRoute from "discourse/routes/discourse";
import AdminConfigWithSettingsRoute from "./admin-config-with-settings-route";
export default class AdminConfigLookAndFeelIndexRoute extends DiscourseRoute {
export default class AdminConfigLookAndFeelIndexRoute extends AdminConfigWithSettingsRoute {
@service router;
beforeModel() {

View File

@ -1,8 +1,8 @@
import DiscourseRoute from "discourse/routes/discourse";
import { i18n } from "discourse-i18n";
import AdminConfigWithSettingsRoute from "./admin-config-with-settings-route";
export default class AdminConfigNavigationRoute extends DiscourseRoute {
export default class AdminConfigNavigationRoute extends AdminConfigWithSettingsRoute {
titleToken() {
return i18n("admin.appearance.sidebar_link.navigation");
return i18n("admin.config.navigation.title");
}
}

View File

@ -1,8 +1,8 @@
import DiscourseRoute from "discourse/routes/discourse";
import { i18n } from "discourse-i18n";
import AdminConfigWithSettingsRoute from "./admin-config-with-settings-route";
export default class AdminConfigNotificationsRoute extends DiscourseRoute {
export default class AdminConfigNotificationsRoute extends AdminConfigWithSettingsRoute {
titleToken() {
return i18n("admin.community.sidebar_link.notifications");
return i18n("admin.config.notifications.title");
}
}

View File

@ -1,8 +1,8 @@
import DiscourseRoute from "discourse/routes/discourse";
import { i18n } from "discourse-i18n";
import AdminConfigWithSettingsRoute from "./admin-config-with-settings-route";
export default class AdminConfigOneboxRoute extends DiscourseRoute {
export default class AdminConfigOneboxRoute extends AdminConfigWithSettingsRoute {
titleToken() {
return i18n("admin.advanced.sidebar_link.onebox");
return i18n("admin.config.onebox.title");
}
}

View File

@ -1,8 +1,8 @@
import DiscourseRoute from "discourse/routes/discourse";
import { i18n } from "discourse-i18n";
import AdminConfigWithSettingsRoute from "./admin-config-with-settings-route";
export default class AdminConfigOtherRoute extends DiscourseRoute {
export default class AdminConfigOtherRoute extends AdminConfigWithSettingsRoute {
titleToken() {
return i18n("admin.advanced.sidebar_link.other_options");
return i18n("admin.config.other.title");
}
}

View File

@ -1,8 +1,8 @@
import DiscourseRoute from "discourse/routes/discourse";
import { i18n } from "discourse-i18n";
import AdminConfigWithSettingsRoute from "./admin-config-with-settings-route";
export default class AdminConfigRateLimitsRoute extends DiscourseRoute {
export default class AdminConfigRateLimitsRoute extends AdminConfigWithSettingsRoute {
titleToken() {
return i18n("admin.advanced.sidebar_link.rate_limits");
return i18n("admin.config.rate_limits.title");
}
}

View File

@ -1,8 +1,8 @@
import DiscourseRoute from "discourse/routes/discourse";
import { i18n } from "discourse-i18n";
import AdminConfigWithSettingsRoute from "./admin-config-with-settings-route";
export default class AdminConfigSearchRoute extends DiscourseRoute {
export default class AdminConfigSearchRoute extends AdminConfigWithSettingsRoute {
titleToken() {
return i18n("admin.advanced.sidebar_link.search");
return i18n("admin.config.search.title");
}
}

View File

@ -1,8 +1,8 @@
import DiscourseRoute from "discourse/routes/discourse";
import { i18n } from "discourse-i18n";
import AdminConfigWithSettingsRoute from "./admin-config-with-settings-route";
export default class AdminConfigSecurityRoute extends DiscourseRoute {
export default class AdminConfigSecurityRoute extends AdminConfigWithSettingsRoute {
titleToken() {
return i18n("admin.security.sidebar_link.security");
return i18n("admin.config.security.title");
}
}

View File

@ -1,8 +1,8 @@
import DiscourseRoute from "discourse/routes/discourse";
import { i18n } from "discourse-i18n";
import AdminConfigWithSettingsRoute from "./admin-config-with-settings-route";
export default class AdminConfigSpamRoute extends DiscourseRoute {
export default class AdminConfigSpamRoute extends AdminConfigWithSettingsRoute {
titleToken() {
return i18n("admin.security.sidebar_link.spam");
return i18n("admin.config.spam.title");
}
}

View File

@ -1,8 +1,8 @@
import DiscourseRoute from "discourse/routes/discourse";
import { i18n } from "discourse-i18n";
import AdminConfigWithSettingsRoute from "./admin-config-with-settings-route";
export default class AdminConfigTrustLevelsRoute extends DiscourseRoute {
export default class AdminConfigTrustLevelsRoute extends AdminConfigWithSettingsRoute {
titleToken() {
return i18n("admin.community.sidebar_link.trust_levels");
return i18n("admin.config.trust_levels.title");
}
}

View File

@ -1,8 +1,8 @@
import DiscourseRoute from "discourse/routes/discourse";
import { i18n } from "discourse-i18n";
import AdminConfigWithSettingsRoute from "./admin-config-with-settings-route";
export default class AdminConfigUserApiRoute extends DiscourseRoute {
export default class AdminConfigUserApiRoute extends AdminConfigWithSettingsRoute {
titleToken() {
return i18n("admin.advanced.sidebar_link.user_api");
return i18n("admin.config.user_api.title");
}
}

View File

@ -0,0 +1,15 @@
import DiscourseRoute from "discourse/routes/discourse";
export default class AdminConfigWithSettingsRoute extends DiscourseRoute {
resetController(controller, isExiting) {
// Have to do this because this is the parent route. We don't want to have
// to make a controller for every single settings route when we can reset
// the filter here.
const settingsController = this.controllerFor(
`${this.fullRouteName}.settings`
);
if (isExiting) {
settingsController.set("filter", "");
}
}
}

View File

@ -20,4 +20,10 @@ export default class AdminPluginsShowSettingsRoute extends DiscourseRoute {
titleToken() {
return i18n("admin.plugins.change_settings_short");
}
resetController(controller, isExiting) {
if (isExiting) {
controller.set("filter", "");
}
}
}

View File

@ -25,4 +25,10 @@ export default class AdminSiteSettingsRoute extends DiscourseRoute {
this.controllerFor("adminSiteSettings").set("model", settings);
});
}
resetController(controller, isExiting) {
if (isExiting) {
controller.set("filter", "");
}
}
}

View File

@ -1,7 +1,11 @@
import { tracked } from "@glimmer/tracking";
import { service } from "@ember/service";
import KeyboardShortcuts, {
PLATFORM_KEY_MODIFIER,
} from "discourse/lib/keyboard-shortcuts";
import DiscourseRoute from "discourse/routes/discourse";
import { i18n } from "discourse-i18n";
import AdminSearchModal from "admin/components/modal/admin-search";
export default class AdminRoute extends DiscourseRoute {
@service sidebarState;
@ -9,6 +13,7 @@ export default class AdminRoute extends DiscourseRoute {
@service store;
@service currentUser;
@service adminSidebarStateManager;
@service modal;
@tracked initialSidebarState;
titleToken() {
@ -16,6 +21,16 @@ export default class AdminRoute extends DiscourseRoute {
}
activate() {
if (this.currentUser.use_experimental_admin_search) {
KeyboardShortcuts.addShortcut(
`${PLATFORM_KEY_MODIFIER}+/`,
() => this.showAdminSearchModal(),
{
global: true,
}
);
}
this.adminSidebarStateManager.maybeForceAdminSidebar({
onlyIfAlreadyActive: false,
});
@ -28,10 +43,20 @@ export default class AdminRoute extends DiscourseRoute {
deactivate(transition) {
this.controllerFor("application").set("showTop", true);
if (this.currentUser.use_experimental_admin_search) {
KeyboardShortcuts.unbind({
[`${PLATFORM_KEY_MODIFIER}+/`]: this.showAdminSearchModal,
});
}
if (this.adminSidebarStateManager.currentUserUsingAdminSidebar) {
if (!transition?.to.name.startsWith("admin")) {
this.adminSidebarStateManager.stopForcingAdminSidebar();
}
}
}
showAdminSearchModal() {
this.modal.show(AdminSearchModal);
}
}

View File

@ -0,0 +1,320 @@
import { tracked } from "@glimmer/tracking";
import Service, { service } from "@ember/service";
import { adminRouteValid } from "discourse/lib/admin-utilities";
import { ajax } from "discourse/lib/ajax";
import escapeRegExp from "discourse/lib/escape-regexp";
import getURL from "discourse/lib/get-url";
import PreloadStore from "discourse/lib/preload-store";
import { ADMIN_NAV_MAP } from "discourse/lib/sidebar/admin-nav-map";
import { humanizedSettingName } from "discourse/lib/site-settings-utils";
import I18n, { i18n } from "discourse-i18n";
// TODO (martin) Move this to javascript.rake constants, use on server too
export const RESULT_TYPES = ["page", "setting", "theme", "component", "report"];
const SEPARATOR = ">";
const MIN_FILTER_LENGTH = 2;
const MAX_TYPE_RESULT_COUNT_LOW = 15;
const MAX_TYPE_RESULT_COUNT_HIGH = 50;
export default class AdminSearchDataSource extends Service {
@service router;
@service siteSettings;
plugins = {};
pageMapItems = [];
settingMapItems = [];
themeMapItems = [];
componentMapItems = [];
reportMapItems = [];
settingPageMap = {
categories: {},
areas: {},
};
@tracked _mapCached = false;
get isLoaded() {
return this._mapCached;
}
async buildMap() {
if (this._mapCached) {
return;
}
ADMIN_NAV_MAP.forEach((mapItem) => {
mapItem.links.forEach((link) => {
let parentLabel = this.#addPageLink(mapItem, link);
link.links?.forEach((subLink) => {
this.#addPageLink(mapItem, subLink, parentLabel);
});
});
});
// TODO (martin) Handle plugin enabling/disabling via MessageBus for this
// and the setting list?
(PreloadStore.get("visiblePlugins") || {}).forEach((plugin) => {
if (
plugin.admin_route &&
plugin.enabled &&
adminRouteValid(this.router, plugin.admin_route)
) {
this.plugins[plugin.name] = plugin;
}
});
const allItems = await ajax("/admin/search/all.json");
this.#processSettings(allItems.settings);
this.#processThemesAndComponents(allItems.themes_and_components);
// TODO (martin) Move this to all.json after refactoring reports controller
// into a service.
const reportItems = await ajax("/admin/reports.json");
this.#processReports(reportItems.reports);
this._mapCached = true;
}
search(filter, opts = {}) {
if (filter.length < MIN_FILTER_LENGTH) {
return [];
}
opts.types = opts.types || RESULT_TYPES;
const filteredResults = [];
const escapedFilterRegExp = escapeRegExp(filter.toLowerCase());
// Pointless to render heaps of settings if the filter is quite low.
const perTypeLimit =
filter.length < MIN_FILTER_LENGTH + 1
? MAX_TYPE_RESULT_COUNT_LOW
: MAX_TYPE_RESULT_COUNT_HIGH;
opts.types.forEach((type) => {
let typeItemCount = 0;
this[`${type}MapItems`].forEach((mapItem) => {
// TODO (martin) There is likely a much better way of doing this matching
// that will support fuzzy searches, for now let's go with the most basic thing.
if (
mapItem.keywords.match(escapedFilterRegExp) &&
typeItemCount <= perTypeLimit
) {
filteredResults.push(mapItem);
typeItemCount++;
}
});
});
return filteredResults;
}
#addPageLink(mapItem, link, parentLabel = "") {
let url;
if (link.routeModels) {
url = this.router.urlFor(link.route, ...link.routeModels);
} else {
url = this.router.urlFor(link.route);
}
const mapItemLabel = this.#labelOrText(mapItem);
const linkLabel = this.#labelOrText(link);
let label;
if (parentLabel) {
label = mapItemLabel;
if (mapItemLabel) {
label += ` ${SEPARATOR} `;
}
label += `${parentLabel} ${SEPARATOR} ${linkLabel}`;
} else {
label = mapItemLabel + (mapItemLabel ? ` ${SEPARATOR} ` : "") + linkLabel;
}
if (link.settings_area) {
this.settingPageMap.areas[link.settings_area] = link.multi_tabbed
? `${url}/settings`
: url;
}
if (link.settings_category) {
this.settingPageMap.categories[link.settings_category] = link.multi_tabbed
? `${url}/settings`
: url;
}
const linkKeywords = link.keywords
? i18n(link.keywords).toLowerCase().replaceAll("|", " ")
: "";
const linkDescription = link.description
? link.description.includes(" ")
? link.description
: i18n(link.description)
: "";
this.pageMapItems.push({
label,
url,
keywords: this.#buildKeywords(
linkKeywords,
url,
label.replace(SEPARATOR, "").toLowerCase().replace(/ +/g, " "),
linkDescription
),
type: "page",
icon: link.icon,
description: linkDescription,
});
return linkLabel;
}
#processSettings(settings) {
const settingPluginNames = {};
settings.forEach((setting) => {
let plugin;
let rootLabel;
if (setting.plugin) {
if (!settingPluginNames[setting.plugin]) {
settingPluginNames[setting.plugin] = setting.plugin.replaceAll(
"_",
"-"
);
}
plugin = this.plugins[settingPluginNames[setting.plugin]];
if (plugin) {
rootLabel = plugin.admin_route?.label
? i18n(plugin.admin_route?.label)
: i18n("admin.plugins.title");
} else {
rootLabel = i18n("admin.plugins.title");
}
} else if (setting.primary_area) {
rootLabel =
I18n.lookup(`admin.config.${setting.primary_area}.title`) ||
i18n(`admin.site_settings.categories.${setting.category}`);
} else {
rootLabel = i18n(`admin.site_settings.categories.${setting.category}`);
}
const label = `${rootLabel} ${SEPARATOR} ${humanizedSettingName(
setting.setting
)}`;
// TODO (martin) These URLs will need to change eventually to anchors
// to focus on a specific element on the page, for now though the filter is fine.
let url;
if (setting.plugin) {
if (plugin) {
url = plugin.admin_route.use_new_show_route
? this.router.urlFor(
`adminPlugins.show.settings`,
plugin.admin_route.location,
{ queryParams: { filter: setting.setting } }
)
: this.router.urlFor(`adminPlugins.${plugin.admin_route.location}`);
} else {
url = getURL(
`/admin/site_settings/category/all_results?filter=${setting.setting}`
);
}
} else if (this.settingPageMap.areas[setting.primary_area]) {
url =
this.settingPageMap.areas[setting.primary_area] +
`?filter=${setting.setting}`;
} else if (this.settingPageMap.categories[setting.category]) {
url =
this.settingPageMap.categories[setting.category] +
`?filter=${setting.setting}`;
} else {
url = getURL(
`/admin/site_settings/category/all_results?filter=${setting.setting}`
);
}
this.settingMapItems.push({
label,
description: setting.description,
url,
keywords: this.#buildKeywords(
setting.setting,
humanizedSettingName(setting.setting),
setting.description,
setting.keywords,
rootLabel
),
type: "setting",
icon: "gear",
});
});
}
#processThemesAndComponents(themesAndComponents) {
themesAndComponents.forEach((themeOrComponent) => {
if (themeOrComponent.component) {
this.componentMapItems.push({
label: themeOrComponent.name,
description: themeOrComponent.description,
url: getURL(`/admin/customize/components/${themeOrComponent.id}`),
keywords: this.#buildKeywords(
"component",
themeOrComponent.description,
themeOrComponent.name
),
type: "component",
icon: "puzzle-piece",
});
} else {
this.themeMapItems.push({
label: themeOrComponent.name,
description: themeOrComponent.description,
url: getURL(`/admin/customize/themes/${themeOrComponent.id}`),
keywords: this.#buildKeywords(
"theme",
themeOrComponent.description,
themeOrComponent.name
),
type: "theme",
icon: "paintbrush",
});
}
});
}
#processReports(reports) {
reports.forEach((report) => {
this.reportMapItems.push({
label: report.title,
description: report.description,
url: getURL(`/admin/reports/${report.type}`),
icon: "chart-bar",
keywords: this.#buildKeywords(
report.title,
report.description,
report.type
),
type: "report",
});
});
}
#labelOrText(obj, fallback = "") {
return obj.text || (obj.label ? i18n(obj.label) : fallback);
}
#buildKeywords(...keywords) {
return keywords
.map((kw) => {
if (Array.isArray(kw)) {
return kw.join(" ");
}
return kw;
})
.join(" ")
.toLowerCase();
}
}

View File

@ -1,14 +1,14 @@
<div class="badges">
<DPageHeader
@titleLabel={{i18n "admin.badges.title"}}
@descriptionLabel={{i18n "admin.badges.page_description"}}
@titleLabel={{i18n "admin.config.badges.title"}}
@descriptionLabel={{i18n "admin.config.badges.header_description"}}
@learnMoreUrl="https://meta.discourse.org/t/understanding-and-using-badges/32540"
>
<:breadcrumbs>
<DBreadcrumbsItem @path="/admin" @label={{i18n "admin_title"}} />
<DBreadcrumbsItem
@path="/admin/badges"
@label={{i18n "admin.badges.title"}}
@label={{i18n "admin.config.badges.title"}}
/>
</:breadcrumbs>
<:actions as |actions|>

View File

@ -7,15 +7,15 @@ import { i18n } from "discourse-i18n";
export default RouteTemplate(<template>
<PluginOutlet @name="admin-api-keys">
<DPageHeader
@titleLabel={{i18n "admin.api_keys.title"}}
@descriptionLabel={{i18n "admin.api_keys.description"}}
@titleLabel={{i18n "admin.config.api_keys.title"}}
@descriptionLabel={{i18n "admin.config.api_keys.header_description"}}
@hideTabs={{true}}
>
<:breadcrumbs>
<DBreadcrumbsItem @path="/admin" @label={{i18n "admin_title"}} />
<DBreadcrumbsItem
@path="/admin/api/keys"
@label={{i18n "admin.api_keys.title"}}
@label={{i18n "admin.config.api_keys.title"}}
/>
</:breadcrumbs>
<:actions as |actions|>

View File

@ -1,14 +1,14 @@
<div class="admin-backups admin-config-page">
<DPageHeader
@titleLabel={{i18n "admin.backups.title"}}
@descriptionLabel={{i18n "admin.backups.description"}}
@titleLabel={{i18n "admin.config.backups.title"}}
@descriptionLabel={{i18n "admin.config.backups.header_description"}}
@learnMoreUrl="https://meta.discourse.org/t/create-download-and-restore-a-backup-of-your-discourse-database/122710"
>
<:breadcrumbs>
<DBreadcrumbsItem @path="/admin" @label={{i18n "admin_title"}} />
<DBreadcrumbsItem
@path="/admin/backups"
@label={{i18n "admin.backups.title"}}
@label={{i18n "admin.config.backups.title"}}
/>
</:breadcrumbs>
<:actions as |actions|>

View File

@ -1,7 +1,7 @@
<DPageHeader
@titleLabel={{i18n "admin.config_areas.about.header"}}
@titleLabel={{i18n "admin.config.about.title"}}
@descriptionLabel={{i18n
"admin.config_areas.about.description"
"admin.config.about.header_description"
(hash basePath=(base-path))
}}
@hideTabs={{true}}
@ -11,7 +11,7 @@
<DBreadcrumbsItem @path="/admin" @label={{i18n "admin_title"}} />
<DBreadcrumbsItem
@path="/admin/config/about"
@label={{i18n "admin.config_areas.about.header"}}
@label={{i18n "admin.config.about.title"}}
/>
</:breadcrumbs>
</DPageHeader>

View File

@ -1,6 +1,6 @@
<DPageHeader
@titleLabel={{i18n "admin.customize.colors.long_title"}}
@descriptionLabel={{i18n "admin.customize.colors.description"}}
@titleLabel={{i18n "admin.config.color_palettes.long_title"}}
@descriptionLabel={{i18n "admin.config.color_palettes.header_description"}}
@learnMoreUrl="https://meta.discourse.org/t/allow-users-to-select-new-color-palettes/60857"
@hideTabs={{true}}
>
@ -8,7 +8,7 @@
<DBreadcrumbsItem @path="/admin" @label={{i18n "admin_title"}} />
<DBreadcrumbsItem
@path="/admin/customize/colors"
@label={{i18n "admin.customize.colors.long_title"}}
@label={{i18n "admin.config.color_palettes.title"}}
/>
</:breadcrumbs>
</DPageHeader>

View File

@ -1,6 +1,6 @@
<DPageHeader
@titleLabel={{i18n "admin.customize.email_style.heading"}}
@descriptionLabel={{i18n "admin.customize.email_style.instructions"}}
@titleLabel={{i18n "admin.config.email_appearance.title"}}
@descriptionLabel={{i18n "admin.config.email_appearance.header_description"}}
@shouldDisplay={{true}}
>
<:breadcrumbs>

View File

@ -1,7 +1,7 @@
{{#if (eq this.currentTab "themes")}}
<DPageHeader
@titleLabel={{i18n "admin.customize.theme.title"}}
@descriptionLabel={{i18n "admin.customize.theme.description"}}
@titleLabel={{i18n "admin.config.themes.title"}}
@descriptionLabel={{i18n "admin.config.themes.header_description"}}
@learnMoreUrl="https://meta.discourse.org/t/91966"
@hideTabs={{true}}
>
@ -9,21 +9,21 @@
<DBreadcrumbsItem @path="/admin" @label={{i18n "admin_title"}} />
<DBreadcrumbsItem
@path="/admin/customize/themes"
@label={{i18n "admin.customize.theme.title"}}
@label={{i18n "admin.config.themes.title"}}
/>
</:breadcrumbs>
</DPageHeader>
{{else}}
<DPageHeader
@titleLabel={{i18n "admin.customize.theme.components"}}
@descriptionLabel={{i18n "admin.customize.theme.components_description"}}
@titleLabel={{i18n "admin.config.components.title"}}
@descriptionLabel={{i18n "admin.config.components.header_description"}}
@hideTabs={{true}}
>
<:breadcrumbs>
<DBreadcrumbsItem @path="/admin" @label={{i18n "admin_title"}} />
<DBreadcrumbsItem
@path="/admin/customize/components"
@label={{i18n "admin.customize.theme.components"}}
@label={{i18n "admin.config.components.title"}}
/>
</:breadcrumbs>
</DPageHeader>

View File

@ -1,4 +1,8 @@
<p>{{i18n "admin.email.advanced_test.desc"}}</p>
<DPageSubheader
@descriptionLabel={{i18n
"admin.config.email.sub_pages.advanced_test.header_description"
}}
/>
<div class="email-advanced-test">
<label for="email">{{i18n "admin.email.advanced_test.email"}}</label>

View File

@ -1,4 +1,8 @@
<p>{{i18n "admin.email.preview_digest_desc"}}</p>
<DPageSubheader
@descriptionLabel={{i18n
"admin.config.email.sub_pages.preview_summary.header_description"
}}
/>
<div class="admin-controls email-preview">
<div class="controls">

View File

@ -1,31 +1,49 @@
<DPageHeader
@titleLabel={{i18n "admin.email.title"}}
@descriptionLabel={{i18n "admin.email.description"}}
@titleLabel={{i18n "admin.config.email.title"}}
@descriptionLabel={{i18n "admin.config.email.header_description"}}
@shouldDisplay={{true}}
>
<:breadcrumbs>
<DBreadcrumbsItem @path="/admin" @label={{i18n "admin_title"}} />
<DBreadcrumbsItem
@path="/admin/email"
@label={{i18n "admin.email.title"}}
@label={{i18n "admin.config.email.title"}}
/>
</:breadcrumbs>
<:tabs>
<NavItem @route="adminEmail.index" @label="admin.email.settings" />
<NavItem @route="adminEmail.index" @label="settings" />
<NavItem
@route="adminEmail.previewDigest"
@label="admin.email.preview_digest"
@label="admin.config.email.sub_pages.preview_summary.title"
/>
<NavItem
@route="adminEmail.advancedTest"
@label="admin.email.advanced_test.title"
@label="admin.config.email.sub_pages.advanced_test.title"
/>
<NavItem
@route="adminEmailTemplates"
@label="admin.config.email.sub_pages.templates.title"
/>
<NavItem
@route="adminEmail.sent"
@label="admin.config.email.sub_pages.sent.title"
/>
<NavItem
@route="adminEmail.skipped"
@label="admin.config.email.sub_pages.skipped.title"
/>
<NavItem
@route="adminEmail.bounced"
@label="admin.config.email.sub_pages.bounced.title"
/>
<NavItem
@route="adminEmail.received"
@label="admin.config.email.sub_pages.received.title"
/>
<NavItem
@route="adminEmail.rejected"
@label="admin.config.email.sub_pages.rejected.title"
/>
<NavItem @route="adminEmailTemplates" @label="admin.email.templates" />
<NavItem @route="adminEmail.sent" @label="admin.email.sent" />
<NavItem @route="adminEmail.skipped" @label="admin.email.skipped" />
<NavItem @route="adminEmail.bounced" @label="admin.email.bounced" />
<NavItem @route="adminEmail.received" @label="admin.email.received" />
<NavItem @route="adminEmail.rejected" @label="admin.email.rejected" />
</:tabs>
</DPageHeader>

View File

@ -1,14 +1,14 @@
<div class="admin-embedding admin-config-page">
<DPageHeader
@titleLabel={{i18n "admin.embedding.title"}}
@descriptionLabel={{i18n "admin.embedding.description"}}
@titleLabel={{i18n "admin.config.embedding.title"}}
@descriptionLabel={{i18n "admin.config.embedding.header_description"}}
@learnMoreUrl="https://meta.discourse.org/t/embed-discourse-comments-on-another-website-via-javascript/31963"
>
<:breadcrumbs>
<DBreadcrumbsItem @path="/admin" @label={{i18n "admin_title"}} />
<DBreadcrumbsItem
@path="/admin/customize/embedding"
@label={{i18n "admin.embedding.title"}}
@label={{i18n "admin.config.embedding.title"}}
/>
</:breadcrumbs>
<:actions as |actions|>

View File

@ -1,14 +1,14 @@
<div class="admin-emoji admin-config-page">
<DPageHeader
@titleLabel={{i18n "admin.emoji.title"}}
@descriptionLabel={{i18n "admin.emoji.description"}}
@titleLabel={{i18n "admin.config.emoji.title"}}
@descriptionLabel={{i18n "admin.config.emoji.header_description"}}
@hideTabs={{this.hideTabs}}
>
<:breadcrumbs>
<DBreadcrumbsItem @path="/admin" @label={{i18n "admin_title"}} />
<DBreadcrumbsItem
@path="/admin/config/emoji"
@label={{i18n "admin.emoji.title"}}
@label={{i18n "admin.config.emoji.title"}}
/>
</:breadcrumbs>
<:actions as |actions|>

View File

@ -1,32 +1,38 @@
<DPageHeader
@titleLabel={{i18n "admin.logs.title"}}
@descriptionLabel={{i18n "admin.logs.description"}}
@titleLabel={{i18n "admin.config.staff_action_logs.title"}}
@descriptionLabel={{i18n "admin.config.staff_action_logs.header_description"}}
@shouldDisplay={{true}}
>
<:breadcrumbs>
<DBreadcrumbsItem @path="/admin" @label={{i18n "admin_title"}} />
<DBreadcrumbsItem @path="/admin/logs" @label={{i18n "admin.logs.title"}} />
<DBreadcrumbsItem
@path="/admin/logs"
@label={{i18n "admin.config.staff_action_logs.title"}}
/>
</:breadcrumbs>
<:tabs>
<NavItem
@route="adminLogs.staffActionLogs"
@label="admin.logs.staff_actions.title"
@label="admin.config.staff_action_logs.title"
/>
{{#if this.currentUser.can_see_emails}}
<NavItem
@route="adminLogs.screenedEmails"
@label="admin.logs.screened_emails.title"
@label="admin.config.staff_action_logs.sub_pages.screened_emails.title"
/>
{{/if}}
<NavItem
@route="adminLogs.screenedIpAddresses"
@label="admin.logs.screened_ips.title"
@label="admin.config.staff_action_logs.sub_pages.screened_ips.title"
/>
<NavItem
@route="adminLogs.screenedUrls"
@label="admin.logs.screened_urls.title"
@label="admin.config.staff_action_logs.sub_pages.screened_urls.title"
/>
<NavItem
@route="adminSearchLogs"
@label="admin.config.staff_action_logs.sub_pages.search_logs.title"
/>
<NavItem @route="adminSearchLogs" @label="admin.logs.search_logs.title" />
{{#if this.currentUser.admin}}
<NavItem @path="/logs" @label="admin.logs.logster.title" />
{{/if}}

View File

@ -1,6 +1,8 @@
<p>
{{i18n "admin.logs.screened_emails.description"}}
</p>
<DPageSubheader
@descriptionLabel={{i18n
"admin.config.staff_action_logs.sub_pages.screened_emails.header_description"
}}
/>
<DButton
@action={{this.exportScreenedEmailList}}

View File

@ -1,4 +1,8 @@
<p>{{i18n "admin.logs.screened_ips.description"}}</p>
<DPageSubheader
@descriptionLabel={{i18n
"admin.config.staff_action_logs.sub_pages.screened_ips.header_description"
}}
/>
<div class="screened-ip-controls">
<div class="filter-screened-ip-address inline-form">

View File

@ -1,6 +1,9 @@
<p>
{{i18n "admin.logs.screened_urls.description"}}
</p>
<DPageSubheader
@descriptionLabel={{i18n
"admin.config.staff_action_logs.sub_pages.screened_urls.header_description"
}}
/>
<DButton
@action={{this.exportScreenedUrlList}}
@title="admin.export_csv.button_title.screened_url"

View File

@ -1,14 +1,14 @@
<div class="admin-permalinks admin-config-page">
<DPageHeader
@titleLabel={{i18n "admin.permalink.title"}}
@descriptionLabel={{i18n "admin.permalink.description"}}
@titleLabel={{i18n "admin.config.permalinks.title"}}
@descriptionLabel={{i18n "admin.config.permalinks.header_description"}}
@learnMoreUrl="https://meta.discourse.org/t/20930"
>
<:breadcrumbs>
<DBreadcrumbsItem @path="/admin" @label={{i18n "admin_title"}} />
<DBreadcrumbsItem
@path="/admin/config/permalinks"
@label={{i18n "admin.permalink.title"}}
@label={{i18n "admin.config.permalinks.title"}}
/>
</:breadcrumbs>
<:actions as |actions|>

View File

@ -1,8 +1,8 @@
<div class="admin-plugins-list-container">
<DPageHeader
@titleLabel={{i18n "admin.plugins.installed"}}
@descriptionLabel={{i18n "admin.plugins.description"}}
@titleLabel={{i18n "admin.config.plugins.title"}}
@descriptionLabel={{i18n "admin.config.plugins.header_description"}}
@learnMoreUrl="https://www.discourse.org/plugins"
>
<:breadcrumbs>

View File

@ -4,7 +4,7 @@
<DBreadcrumbsItem @path="/admin" @label={{i18n "admin_title"}} />
<DBreadcrumbsItem
@path="/admin/plugins"
@label={{i18n "admin.plugins.title"}}
@label={{i18n "admin.config.plugins.title"}}
/>
<div class="d-nav-submenu">
<HorizontalOverflowNav class="main-nav nav plugin-nav">

View File

@ -1,14 +1,14 @@
<DPageHeader
@titleLabel={{i18n "admin.reports.title"}}
@descriptionLabel={{i18n "admin.reports.meta_doc"}}
@titleLabel={{i18n "admin.config.reports.title"}}
@descriptionLabel={{i18n "admin.config.reports.header_description"}}
@learnMoreUrl="https://meta.discourse.org/t/-/240233"
@hideTabs={{true}}
>
<:breadcrumbs>
<DBreadcrumbsItem @path="/admin" @label={{i18n "admin_title"}} />
<DBreadcrumbsItem
@path="/admin/reports"
@label={{i18n "admin.reports.sidebar_title"}}
@path="/admin.config.reports"
@label={{i18n "admin.config.reports.title"}}
/>
</:breadcrumbs>
</DPageHeader>

View File

@ -1,12 +1,12 @@
<DPageHeader
@titleLabel={{i18n "admin.section_landing_pages.account.title"}}
@titleLabel={{i18n "admin.config_sections.account.title"}}
@hideTabs={{true}}
>
<:breadcrumbs>
<DBreadcrumbsItem @path="/admin" @label={{i18n "admin_title"}} />
<DBreadcrumbsItem
@path="/admin/section/account"
@label={{i18n "admin.section_landing_pages.account.title"}}
@label={{i18n "admin.config_sections.account.title"}}
/>
</:breadcrumbs>
</DPageHeader>
@ -14,14 +14,14 @@
<AdminSectionLandingWrapper>
<AdminSectionLandingItem
@icon="box-archive"
@titleLabel="admin.section_landing_pages.account.backups.title"
@descriptionLabel="admin.section_landing_pages.account.backups.description"
@titleLabel="admin.config.backups.title"
@descriptionLabel="admin.config.backups.header_description"
@titleRoute="admin.backups"
/>
<AdminSectionLandingItem
@icon="gift"
@titleLabel="admin.section_landing_pages.account.whats_new.title"
@descriptionLabel="admin.section_landing_pages.account.whats_new.description"
@titleLabel="admin.config.whats_new.title"
@descriptionLabel="admin.config.whats_new.header_description"
@titleRoute="admin.whatsNew"
/>
</AdminSectionLandingWrapper>

View File

@ -1,13 +1,13 @@
<DPageHeader
@titleLabel={{i18n "admin.site_settings.title"}}
@descriptionLabel={{i18n "admin.site_settings.description"}}
@titleLabel={{i18n "admin.config.site_settings.title"}}
@descriptionLabel={{i18n "admin.config.site_settings.header_description"}}
@hideTabs={{true}}
>
<:breadcrumbs>
<DBreadcrumbsItem @path="/admin" @label={{i18n "admin_title"}} />
<DBreadcrumbsItem
@path="/admin/site_settings"
@label={{i18n "admin.site_settings.title"}}
@label={{i18n "admin.config.site_settings.title"}}
/>
</:breadcrumbs>
</DPageHeader>

View File

@ -1,13 +1,13 @@
<DPageHeader
@titleLabel={{i18n "admin.site_text.title"}}
@descriptionLabel={{i18n "admin.site_text.description"}}
@titleLabel={{i18n "admin.config.site_texts.title"}}
@descriptionLabel={{i18n "admin.config.site_texts.header_description"}}
@hideTabs={{true}}
>
<:breadcrumbs>
<DBreadcrumbsItem @path="/admin" @label={{i18n "admin_title"}} />
<DBreadcrumbsItem
@path="/admin/customize/site_texts"
@label={{i18n "admin.site_text.title"}}
@label={{i18n "admin.config.site_texts.title"}}
/>
</:breadcrumbs>
</DPageHeader>

View File

@ -1,7 +1,7 @@
<div class="admin-user_fields admin-config-page">
<DPageHeader
@titleLabel={{i18n "admin.user_fields.title"}}
@descriptionLabel={{i18n "admin.user_fields.help"}}
@titleLabel={{i18n "admin.config.user_fields.title"}}
@descriptionLabel={{i18n "admin.config.user_fields.header_description"}}
@hideTabs={{true}}
@learnMoreUrl="https://meta.discourse.org/t/creating-and-configuring-custom-user-fields/113192"
>
@ -9,7 +9,7 @@
<DBreadcrumbsItem @path="/admin" @label={{i18n "admin_title"}} />
<DBreadcrumbsItem
@path="/admin/config/user-fields"
@label={{i18n "admin.user_fields.title"}}
@label={{i18n "admin.config.user_fields.title"}}
/>
</:breadcrumbs>
<:actions as |actions|>

View File

@ -1,14 +1,14 @@
<div class="admin-users admin-config-page">
<DPageHeader
@titleLabel={{i18n "admin.users.title"}}
@descriptionLabel={{i18n "admin.users.description"}}
@titleLabel={{i18n "admin.config.users.title"}}
@descriptionLabel={{i18n "admin.config.users.header_description"}}
@learnMoreUrl="https://meta.discourse.org/t/accessing-a-user-s-admin-page/311859"
>
<:breadcrumbs>
<DBreadcrumbsItem @path="/admin" @label={{i18n "admin_title"}} />
<DBreadcrumbsItem
@path="/admin/users/list"
@label={{i18n "admin.users.title"}}
@label={{i18n "admin.config.users.title"}}
/>
</:breadcrumbs>
<:actions as |actions|>

View File

@ -1,6 +1,6 @@
<DPageHeader
@titleLabel={{i18n "admin.watched_words.title"}}
@descriptionLabel={{i18n "admin.watched_words.description"}}
@titleLabel={{i18n "admin.config.watched_words.title"}}
@descriptionLabel={{i18n "admin.config.watched_words.header_description"}}
@learnMoreUrl="https://meta.discourse.org/t/241735"
@hideTabs={{true}}
>
@ -8,7 +8,7 @@
<DBreadcrumbsItem @path="/admin" @label={{i18n "admin_title"}} />
<DBreadcrumbsItem
@path="/admin/customize/watched_words"
@label={{i18n "admin.watched_words.title"}}
@label={{i18n "admin.config.watched_words.title"}}
/>
</:breadcrumbs>
</DPageHeader>

View File

@ -7,15 +7,15 @@ import { i18n } from "discourse-i18n";
export default RouteTemplate(<template>
<div class="admin-webhooks admin-config-page">
<DPageHeader
@titleLabel={{i18n "admin.web_hooks.title"}}
@descriptionLabel={{i18n "admin.web_hooks.instruction"}}
@titleLabel={{i18n "admin.config.webhooks.title"}}
@descriptionLabel={{i18n "admin.config.webhooks.header_description"}}
@hideTabs={{true}}
>
<:breadcrumbs>
<DBreadcrumbsItem @path="/admin" @label={{i18n "admin_title"}} />
<DBreadcrumbsItem
@path="/admin/api/web_hooks"
@label={{i18n "admin.web_hooks.title"}}
@label={{i18n "admin.config.webhooks.title"}}
/>
</:breadcrumbs>
<:actions as |actions|>

View File

@ -1,6 +1,6 @@
<DPageHeader
@titleLabel={{i18n "admin.dashboard.new_features.title"}}
@descriptionLabel={{i18n "admin.dashboard.new_features.subtitle"}}
@titleLabel={{i18n "admin.config.whats_new.title"}}
@descriptionLabel={{i18n "admin.config.whats_new.header_description"}}
@learnMoreUrl="https://meta.discourse.org/tags/c/announcements/67/release-notes"
@hideTabs={{true}}
>
@ -8,7 +8,7 @@
<DBreadcrumbsItem @path="/admin" @label={{i18n "admin_title"}} />
<DBreadcrumbsItem
@path="/admin/whats-new"
@label={{i18n "admin.dashboard.new_features.title"}}
@label={{i18n "admin.config.whats_new.title"}}
/>
</:breadcrumbs>
<:actions as |actions|>

View File

@ -21,6 +21,7 @@ export default class DPageSubheader extends Component {
<template>
<div class="d-page-subheader">
<div class="d-page-subheader__title-row">
{{#if @titleLabel}}
<h2 class="d-page-subheader__title">
{{#if @titleUrl}}
<a href={{@titleUrl}} class="d-page-subheader__title-link">
@ -30,6 +31,7 @@ export default class DPageSubheader extends Component {
{{@titleLabel}}
{{/if}}
</h2>
{{/if}}
{{#if (has-block "actions")}}
<div class="d-page-subheader__actions">
{{#if this.site.mobileView}}

View File

@ -496,8 +496,11 @@ export default {
if (filterInput) {
this._scrollTo(0);
if (!this.currentUser.use_experimental_admin_search) {
filterInput.focus();
}
}
},
fullscreenComposer() {

View File

@ -1,24 +1,84 @@
export const ADMIN_NAV_MAP = [
{
text: "",
name: "root",
hideSectionHeader: true,
links: [
{
name: "admin_home",
route: "admin.dashboard.general",
label: "admin.config.dashboard.title",
description: "admin.config.dashboard.header_description",
icon: "house",
moderator: true,
},
{
name: "admin_users",
route: "adminUsers",
label: "admin.config.users.title",
description: "admin.config.users.header_description",
icon: "users",
moderator: true,
},
{
name: "admin_groups",
route: "groups",
label: "admin.config.groups.title",
description: "admin.config.groups.header_description",
icon: "user-group",
moderator: true,
},
{
name: "admin_all_site_settings",
route: "adminSiteSettings",
label: "admin.config.site_settings.title",
description: "admin.config.site_settings.header_description",
icon: "gear",
},
{
name: "admin_whats_new",
route: "admin.whatsNew",
label: "admin.config.whats_new.title",
description: "admin.config.whats_new.header_description",
icon: "gift",
keywords: "admin.config.whats_new.keywords",
moderator: true,
},
],
},
{
name: "account",
label: "admin.account.title",
label: "admin.config_sections.account.title",
links: [
{
name: "admin_backups",
route: "admin.backups",
label: "admin.account.sidebar_link.backups",
label: "admin.config.backups.title",
description: "admin.config.backups.header_description",
icon: "box-archive",
settings_category: "backups",
multi_tabbed: true,
links: [
{
name: "admin_backups_logs",
route: "admin.backups.logs",
label: "admin.config.backups.sub_pages.logs.title",
description:
"admin.config.backups.sub_pages.logs.header_description",
},
],
},
],
},
{
name: "reports",
label: "admin.reports.sidebar_title",
label: "admin.config_sections.reports.title",
links: [
{
name: "admin_all_reports",
route: "adminReports.index",
label: "admin.reports.sidebar_link.all",
label: "admin.config.reports.title",
description: "admin.config.reports.header_description",
icon: "chart-bar",
moderator: true,
},
@ -26,269 +86,425 @@ export const ADMIN_NAV_MAP = [
},
{
name: "community",
label: "admin.community.title",
label: "admin.config_sections.community.title",
links: [
{
name: "admin_about_your_site",
route: "adminConfig.about",
label: "admin.community.sidebar_link.about_your_site",
label: "admin.config.about.title",
description: "admin.config.about.header_description",
icon: "gear",
settings_area: "about",
},
{
name: "admin_badges",
route: "adminBadges",
label: "admin.community.sidebar_link.badges",
label: "admin.config.badges.title",
description: "admin.config.badges.header_description",
icon: "certificate",
},
{
name: "admin_login_and_authentication",
route: "adminConfig.loginAndAuthentication.settings",
label: "admin.community.sidebar_link.login_and_authentication",
label: "admin.config.login_and_authentication.title",
description: "admin.config.login_and_authentication.header_description",
icon: "unlock",
settings_category: "login",
},
{
name: "admin_notifications",
route: "adminConfig.notifications.settings",
label: "admin.community.sidebar_link.notifications",
label: "admin.config.notifications.title",
description: "admin.config.notifications.header_description",
icon: "bell",
settings_area: "notifications",
},
{
name: "admin_localization",
route: "adminConfig.localization.settings",
label: "admin.community.sidebar_link.localization.title",
label: "admin.config.localization.title",
description: "admin.config.localization.header_description",
keywords: "admin.config.localization.keywords",
icon: "globe",
settings_area: "localization",
},
{
name: "admin_permalinks",
route: "adminPermalinks",
label: "admin.community.sidebar_link.permalinks",
label: "admin.config.permalinks.title",
description: "admin.config.permalinks.header_description",
icon: "link",
settings_area: "permalinks",
multi_tabbed: true,
},
{
name: "admin_trust_levels",
route: "adminConfig.trustLevels.settings",
label: "admin.community.sidebar_link.trust_levels",
label: "admin.config.trust_levels.title",
description: "admin.config.trust_levels.header_description",
icon: "user-shield",
settings_area: "trust_levels",
},
{
name: "admin_group_permissions",
route: "adminConfig.groupPermissions.settings",
label: "admin.community.sidebar_link.group_permissions",
label: "admin.config.group_permissions.title",
description: "admin.config.group_permissions.header_description",
icon: "user-gear",
settings_area: "group_permissions",
},
{
name: "admin_user_fields",
route: "adminUserFields",
label: "admin.community.sidebar_link.user_fields",
label: "admin.config.user_fields.title",
description: "admin.config.user_fields.header_description",
icon: "user-pen",
},
{
name: "admin_watched_words",
route: "adminWatchedWords",
label: "admin.community.sidebar_link.watched_words",
label: "admin.config.watched_words.title",
description: "admin.config.watched_words.header_description",
icon: "eye",
moderator: true,
},
{
name: "admin_legal",
route: "adminConfig.legal.settings",
label: "admin.community.sidebar_link.legal",
label: "admin.config.legal.title",
description: "admin.config.legal.header_description",
icon: "gavel",
},
{
name: "admin_moderation_flags",
route: "adminConfig.flags",
label: "admin.community.sidebar_link.moderation_flags.title",
keywords: "admin.community.sidebar_link.moderation_flags.keywords",
label: "admin.config.flags.title",
description: "admin.config.flags.header_description",
keywords: "admin.config.flags.keywords",
icon: "flag",
settings_area: "flags",
multi_tabbed: true,
},
],
},
{
name: "appearance",
label: "admin.appearance.title",
label: "admin.config_sections.appearance.title",
links: [
{
name: "admin_font_style",
route: "adminConfig.fonts.settings",
label: "admin.appearance.sidebar_link.font_style",
label: "admin.config.font_style.title",
description: "admin.config.font_style.header_description",
icon: "italic",
settings_area: "fonts",
},
{
name: "admin_site_logo",
route: "adminConfig.logo.settings",
label: "admin.appearance.sidebar_link.site_logo",
label: "admin.config.logo.title",
description: "admin.config.logo.header_description",
icon: "fab-discourse",
settings_category: "branding",
},
{
name: "admin_color_schemes",
name: "admin_color_palettes",
route: "adminCustomize.colors",
label: "admin.appearance.sidebar_link.color_schemes",
label: "admin.config.color_palettes.title",
description: "admin.config.color_palettes.header_description",
icon: "palette",
},
{
name: "admin_emoji",
route: "adminEmojis",
label: "admin.appearance.sidebar_link.emoji",
label: "admin.config.emoji.title",
description: "admin.config.emoji.header_description",
icon: "discourse-emojis",
settings_area: "emojis",
multi_tabbed: true,
},
{
name: "admin_navigation",
route: "adminConfig.navigation.settings",
label: "admin.appearance.sidebar_link.navigation",
label: "admin.config.navigation.title",
description: "admin.config.navigation.header_description",
icon: "diagram-project",
settings_area: "navigation",
},
{
name: "admin_themes",
route: "adminCustomizeThemes",
routeModels: ["themes"],
model: "themes",
label: "admin.appearance.sidebar_link.themes",
label: "admin.config.themes.title",
description: "admin.config.themes.header_description",
icon: "paintbrush",
},
{
name: "admin_components",
route: "adminCustomizeThemes",
routeModels: ["components"],
label: "admin.appearance.sidebar_link.components.title",
label: "admin.config.components.title",
description: "admin.config.components.header_description",
icon: "puzzle-piece",
keywords: "admin.appearance.sidebar_link.components.keywords",
keywords: "admin.config.components.keywords",
},
{
name: "admin_customize_site_texts",
route: "adminSiteText",
label: "admin.appearance.sidebar_link.site_texts",
label: "admin.config.site_texts.title",
description: "admin.config.site_texts.header_description",
icon: "language",
},
],
},
{
name: "email_settings",
label: "admin.email_settings.title",
label: "admin.config_sections.email.title",
links: [
{
name: "admin_server_setup",
route: "adminEmail",
label: "admin.email_settings.sidebar_link.server_setup.title",
label: "admin.config.email.title",
description: "admin.config.email.header_description",
keywords: "admin.config.email.keywords",
icon: "gear",
keywords: "admin.email_settings.sidebar_link.server_setup.keywords",
links: [
{
name: "admin_email_preview_summary",
route: "adminEmail.previewDigest",
label: "admin.config.email.sub_pages.preview_summary.title",
description:
"admin.config.email.sub_pages.preview_summary.header_description",
},
{
name: "admin_email_advanced_test",
route: "adminEmail.advancedTest",
label: "admin.config.email.sub_pages.advanced_test.title",
description:
"admin.config.email.sub_pages.advanced_test.header_description",
},
{
name: "admin_email_templates",
route: "adminEmailTemplates",
label: "admin.config.email.sub_pages.templates.title",
description:
"admin.config.email.sub_pages.templates.header_description",
},
{
name: "admin_email_sent",
route: "adminEmail.sent",
label: "admin.config.email.sub_pages.sent.title",
description: "admin.config.email.sub_pages.sent.header_description",
},
{
name: "admin_email_skipped",
route: "adminEmail.skipped",
label: "admin.config.email.sub_pages.skipped.title",
description:
"admin.config.email.sub_pages.skipped.header_description",
},
{
name: "admin_email_bounced",
route: "adminEmail.bounced",
label: "admin.config.email.sub_pages.bounced.title",
description:
"admin.config.email.sub_pages.bounced.header_description",
},
{
name: "admin_email_received",
route: "adminEmail.received",
label: "admin.config.email.sub_pages.received.title",
description:
"admin.config.email.sub_pages.received.header_description",
},
{
name: "admin_email_rejected",
route: "adminEmail.rejected",
label: "admin.config.email.sub_pages.rejected.title",
description:
"admin.config.email.sub_pages.rejected.header_description",
},
],
},
{
name: "admin_appearance",
route: "adminCustomizeEmailStyle",
label: "admin.email_settings.sidebar_link.appearance",
label: "admin.config.email_appearance.title",
description: "admin.config.email_appearance.header_description",
icon: "envelope",
},
],
},
{
name: "security",
label: "admin.security.title",
label: "admin.config_sections.security.title",
links: [
{
name: "admin_security",
route: "adminConfig.security.settings",
label: "admin.security.sidebar_link.security",
label: "admin.config.security.title",
description: "admin.config.security.header_description",
icon: "lock",
settings_category: "security",
},
{
name: "admin_spam",
route: "adminConfig.spam.settings",
label: "admin.security.sidebar_link.spam",
label: "admin.config.spam.title",
description: "admin.config.spam.header_description",
icon: "robot",
settings_category: "spam",
},
{
name: "admin_logs_staff_action_logs",
route: "adminLogs",
label: "admin.security.sidebar_link.staff_action_logs.title",
keywords: "admin.security.sidebar_link.staff_action_logs.keywords",
label: "admin.config.staff_action_logs.title",
description: "admin.config.staff_action_logs.header_description",
keywords: "admin.config.staff_action_logs.keywords",
icon: "user-shield",
moderator: true,
links: [
{
name: "admin_logs_screened_emails",
route: "adminLogs.screenedEmails",
label:
"admin.config.staff_action_logs.sub_pages.screened_emails.title",
description:
"admin.config.staff_action_logs.sub_pages.screened_emails.header_description",
},
{
name: "admin_logs_screened_ip_addresses",
route: "adminLogs.screenedIpAddresses",
label:
"admin.config.staff_action_logs.sub_pages.screened_ips.title",
description:
"admin.config.staff_action_logs.sub_pages.screened_ips.header_description",
},
{
name: "admin_logs_screened_urls",
route: "adminLogs.screenedUrls",
label:
"admin.config.staff_action_logs.sub_pages.screened_urls.title",
description:
"admin.config.staff_action_logs.sub_pages.screened_urls.header_description",
},
{
name: "admin_logs_search_logs",
route: "adminSearchLogs",
label: "admin.config.staff_action_logs.sub_pages.search_logs.title",
description:
"admin.config.staff_action_logs.sub_pages.search_logs.header_description",
},
],
},
],
},
{
name: "plugins",
label: "admin.plugins.title",
label: "admin.config_sections.plugins.title",
links: [
{
name: "admin_installed_plugins",
route: "adminPlugins.index",
label: "admin.plugins.sidebar_link.installed",
label: "admin.config.plugins.title",
description: "admin.config.plugins.header_description",
icon: "puzzle-piece",
},
],
},
{
name: "advanced",
label: "admin.advanced.title",
label: "admin.config_sections.advanced.title",
links: [
{
name: "admin_api_keys",
route: "adminApiKeys",
icon: "key",
label: "admin.advanced.sidebar_link.api_keys.title",
keywords: "admin.advanced.sidebar_link.api_keys.keywords",
label: "admin.config.api_keys.title",
description: "admin.config.api_keys.header_description",
keywords: "admin.config.api_keys.keywords",
},
{
name: "admin_webhooks",
route: "adminWebHooks",
icon: "arrows-rotate",
label: "admin.advanced.sidebar_link.webhooks",
label: "admin.config.webhooks.title",
description: "admin.config.webhooks.header_description",
keywords: "admin.config.webhooks.keywords",
},
{
name: "admin_developer",
route: "adminConfig.developer.settings",
label: "admin.advanced.sidebar_link.developer",
label: "admin.config.developer.title",
description: "admin.config.developer.header_description",
icon: "keyboard",
settings_category: "developer",
},
{
name: "admin_embedding",
route: "adminEmbedding",
label: "admin.advanced.sidebar_link.embedding",
label: "admin.config.embedding.title",
description: "admin.config.embedding.header_description",
icon: "code",
settings_area: "embedding",
},
{
name: "admin_rate_limits",
route: "adminConfig.rate-limits.settings",
label: "admin.advanced.sidebar_link.rate_limits",
label: "admin.config.rate_limits.title",
description: "admin.config.rate_limits.header_description",
icon: "rocket",
settings_category: "rate_limits",
},
{
name: "admin_user_api",
route: "adminConfig.user-api.settings",
label: "admin.advanced.sidebar_link.user_api",
label: "admin.config.user_api.title",
description: "admin.config.user_api.header_description",
icon: "shuffle",
settings_category: "user_api",
},
{
name: "admin_onebox",
route: "adminConfig.onebox.settings",
label: "admin.advanced.sidebar_link.onebox",
label: "admin.config.onebox.title",
description: "admin.config.onebox.header_description",
icon: "far-square",
settings_category: "onebox",
},
{
name: "admin_files",
route: "adminConfig.files.settings",
label: "admin.advanced.sidebar_link.files",
label: "admin.config.files.title",
description: "admin.config.files.header_description",
icon: "file",
settings_category: "files",
},
{
name: "admin_other_options",
route: "adminConfig.other.settings",
label: "admin.advanced.sidebar_link.other_options",
label: "admin.config.other.title",
description: "admin.config.other.header_description",
icon: "discourse-other-tab",
settings_category: "uncategorized",
},
{
name: "admin_search",
route: "adminConfig.search.settings",
label: "admin.advanced.sidebar_link.search",
label: "admin.config.search.title",
description: "admin.config.search.header_description",
icon: "magnifying-glass",
settings_category: "search",
},
{
name: "admin_experimental",
route: "adminConfig.experimental.settings",
label: "admin.advanced.sidebar_link.experimental",
label: "admin.config.experimental.title",
description: "admin.config.experimental.header_description",
icon: "discourse-sparkles",
settings_category: "experimental",
},
],
},

View File

@ -84,9 +84,8 @@ class SidebarAdminSectionLink extends BaseCustomSidebarSectionLink {
// for the plugin ID has its own nested routes defined in the plugin.
if (this.router.currentRoute.name === "adminPlugins.show.settings") {
if (
this.adminSidebarNavLink.route?.includes(
this.adminSidebarNavLink.route?.split(".").last ===
this.router.currentRoute.parent.params.plugin_id
)
) {
return this.router.currentRoute.name;
}
@ -184,53 +183,6 @@ function defineAdminSection(
}
export function useAdminNavConfig(navMap) {
const adminNavSections = [
{
text: "",
name: "root",
hideSectionHeader: true,
links: [
{
name: "admin_home",
route: "admin.dashboard.general",
label: "admin.dashboard.title",
icon: "house",
moderator: true,
},
{
name: "admin_users",
route: "adminUsers",
label: "admin.community.sidebar_link.users",
icon: "users",
moderator: true,
},
{
name: "admin_groups",
route: "groups",
label: "admin.community.sidebar_link.groups",
icon: "user-group",
moderator: true,
},
{
name: "admin_all_site_settings",
route: "adminSiteSettings",
label: "admin.advanced.sidebar_link.all_site_settings",
icon: "gear",
},
{
name: "admin_whats_new",
route: "admin.whatsNew",
label: "admin.account.sidebar_link.whats_new.title",
icon: "gift",
keywords: "admin.account.sidebar_link.whats_new.keywords",
moderator: true,
},
],
},
];
navMap = adminNavSections.concat(navMap);
for (const [sectionName, additionalLinks] of Object.entries(
additionalAdminSidebarSectionLinks
)) {
@ -320,6 +272,7 @@ function pluginAdminRouteLinks(router) {
label: plugin.admin_route.label,
text: plugin.humanized_name,
icon: "gear",
description: plugin.description,
};
});
}

View File

@ -0,0 +1,96 @@
const ACRONYMS = new Set([
"acl",
"ai",
"api",
"bg",
"cdn",
"cors",
"cta",
"dm",
"eu",
"faq",
"fg",
"ga",
"gb",
"gtm",
"hd",
"http",
"https",
"iam",
"id",
"imap",
"ip",
"jpg",
"json",
"kb",
"mb",
"oidc",
"pm",
"png",
"pop3",
"s3",
"smtp",
"svg",
"tl",
"tl0",
"tl1",
"tl2",
"tl3",
"tl4",
"tld",
"txt",
"url",
"ux",
]);
const MIXED_CASE = [
["adobe analytics", "Adobe Analytics"],
["android", "Android"],
["chinese", "Chinese"],
["discord", "Discord"],
["discourse", "Discourse"],
["discourse connect", "Discourse Connect"],
["discourse discover", "Discourse Discover"],
["discourse narrative bot", "Discourse Narrative Bot"],
["facebook", "Facebook"],
["github", "GitHub"],
["google", "Google"],
["gravatar", "Gravatar"],
["gravatars", "Gravatars"],
["ios", "iOS"],
["japanese", "Japanese"],
["linkedin", "LinkedIn"],
["oauth2", "OAuth2"],
["opengraph", "OpenGraph"],
["powered by discourse", "Powered by Discourse"],
["tiktok", "TikTok"],
["tos", "ToS"],
["twitter", "Twitter"],
["vimeo", "Vimeo"],
["wordpress", "WordPress"],
["youtube", "YouTube"],
];
export function humanizedSettingName(settingName, settingLabel) {
const name = settingLabel || settingName.replace(/\_/g, " ");
const formattedName = (name.charAt(0).toUpperCase() + name.slice(1))
.split(" ")
.map((word) =>
ACRONYMS.has(word.toLowerCase()) ? word.toUpperCase() : word
)
.map((word) => {
if (word.endsWith("s")) {
const singular = word.slice(0, -1).toLowerCase();
return ACRONYMS.has(singular) ? singular.toUpperCase() + "s" : word;
}
return word;
})
.join(" ");
return MIXED_CASE.reduce(
(acc, [key, value]) =>
acc.replaceAll(new RegExp(`\\b${key}\\b`, "gi"), value),
formattedName
);
}

View File

@ -112,9 +112,10 @@ acceptance("Admin - Site Settings", function (needs) {
await click(".nav.nav-pills li:nth-child(1) a");
assert.dom(".row.setting").exists({ count: 0 });
// navigate back to the "Settings" page
// navigate back to the "Settings" page, the title filter
// has been removed from navigation
await click(".nav.nav-pills li:nth-child(2) a");
assert.dom(".row.setting").exists({ count: 1 });
assert.dom(".row.setting").exists({ count: 4 });
});
test("filtering overridden settings", async function (assert) {

View File

@ -1225,6 +1225,7 @@ a.inline-editable-field {
@import "common/admin/plugins";
@import "common/admin/site-settings";
@import "common/admin/admin_config_area";
@import "common/admin/search";
@import "common/admin/admin_table";
@import "common/admin/admin_filter";
@import "common/admin/admin_reports";

View File

@ -0,0 +1,129 @@
.admin-search-modal .d-modal__body {
display: flex;
flex-direction: column;
gap: var(--space-2);
padding: var(--space-4);
}
// Search field
.admin-search {
&__input-container {
display: flex;
}
&__input-group {
display: flex;
align-items: center;
border: 1px solid var(--primary-400);
width: 100%;
box-sizing: border-box;
height: 42px;
padding: 0 var(--space-2);
&:focus,
&:focus-within {
border-color: var(--tertiary);
outline: 2px solid var(--tertiary);
outline-offset: -2px;
}
}
&__input-icon {
background: none !important;
color: var(--primary-medium);
}
&__input-field {
margin: 0 !important;
border: 0 !important;
appearance: none !important;
outline: none !important;
background: none !important;
width: 100% !important;
}
}
// Fitler
.admin-search__filters {
display: flex;
align-items: center;
gap: var(--space-1);
flex-wrap: wrap;
}
.admin-search__filter {
&-item {
position: relative;
display: inline-flex;
align-items: center;
padding: var(--space-1) var(--space-2);
background-color: var(--primary-low);
color: var(--primary-medium);
border-radius: var(--d-border-radius);
border: none;
cursor: pointer;
transition: background-color 0.2s;
&:hover {
color: var(--primary-low-mid);
}
&.is-active {
background-color: var(--tertiary);
color: var(--secondary);
.d-icon {
color: var(--tertiary-low);
}
}
&:not(.is-active) {
.d-icon {
color: var(--primary-low-mid);
}
}
}
}
// Results
.admin-search__result {
border-top: 1px solid var(--primary-low);
padding: var(--space-3) var(--space-2);
display: block;
&:first-child {
border-top: none;
}
&:last-child {
border-bottom: none;
}
&:hover {
background-color: var(--tertiary-very-low);
}
&-name {
color: var(--primary);
display: flex;
flex-wrap: wrap;
justify-content: space-between;
align-items: baseline;
gap: var(--space-3);
.d-icon {
color: var(--primary-high);
font-size: var(--font-down-1);
}
}
&-name-label {
flex: 1;
}
&-description {
font-size: var(--font-down-1);
color: var(--primary-high);
margin-top: var(--space-1);
}
}

View File

@ -0,0 +1,21 @@
# frozen_string_literal: true
class Admin::SearchController < Admin::AdminController
def index
# TODO (martin) Include reports here too, need to refact
# the reports controller into a reusable lookup service first.
render_json_dump(
settings:
SiteSetting.all_settings(
filter_names: params[:filter_names],
filter_area: params[:filter_area],
filter_plugin: params[:plugin],
filter_categories: Array.wrap(params[:categories]),
include_locale_setting: params[:filter_area] == "localization",
basic_attributes: true,
),
themes_and_components:
serialize_data(Theme.include_relations.order(:name), BasicThemeSerializer),
)
end
end

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true
class BasicThemeSerializer < ApplicationSerializer
attributes :id, :name, :created_at, :updated_at, :default, :component
attributes :id, :name, :description, :created_at, :updated_at, :default, :component
def include_default?
object.id == SiteSetting.default_theme_id
@ -10,4 +10,8 @@ class BasicThemeSerializer < ApplicationSerializer
def default
true
end
def description
object.internal_translations.find { |t| t.key == "theme_metadata.description" }&.value
end
end

View File

@ -73,6 +73,7 @@ class CurrentUserSerializer < BasicUserSerializer
:sidebar_sections,
:new_new_view_enabled?,
:use_admin_sidebar,
:use_experimental_admin_search,
:can_view_raw_email,
:login_method,
:has_unseen_features,
@ -137,6 +138,10 @@ class CurrentUserSerializer < BasicUserSerializer
object.staff? && object.in_any_groups?(SiteSetting.admin_sidebar_enabled_groups_map)
end
def use_experimental_admin_search
object.staff? && object.in_any_groups?(SiteSetting.experimental_admin_search_enabled_groups_map)
end
def include_use_admin_sidebar?
object.staff?
end

View File

@ -10,7 +10,6 @@ class ThemeSerializer < BasicThemeSerializer
:settings,
:errors,
:supported?,
:description,
:enabled?,
:disabled_at,
:theme_fields,
@ -84,10 +83,6 @@ class ThemeSerializer < BasicThemeSerializer
@errors.present?
end
def description
object.internal_translations.find { |t| t.key == "theme_metadata.description" }&.value
end
def include_disabled_at?
object.component? && !object.enabled?
end

View File

@ -5160,16 +5160,42 @@ en:
meta_doc: "Reports are a powerful tool to help you understand what’s happening on your site. They can help you identify trends, spot problems, and make decisions based on data."
sidebar_title: "Reports"
back: "Back to all reports"
sidebar_link:
all: "All reports"
config_sections:
account:
title: "Account"
reports:
title: "Reports"
community:
title: "Community"
appearance:
title: "Appearance"
email:
title: "Email"
security:
title: "Security"
plugins:
title: "Plugins"
advanced:
title: "Advanced"
config:
about:
title: "About your site"
header_description: "Provide information here about this site and your team so that people can learn what your community is about, who is behind it, and how to reach you in case there is an issue. Displayed on your site's <a href='%{basePath}/about'>About page</a>."
developer:
title: "Developer"
header_description: "Developer settings to control rate limits, multipliers and calculations, safe mode, and other advanced features"
experimental:
title: "Experimental"
header_description: "Toggle experimental features on or off for your site, most of these can be controlled on a group basis"
emojis:
title: "Emoji"
header_description: "Add new emoji that will be available to everyone. Select multiple files to create emojis using their file names. The selected group will be used for all files that are added at the same time"
flags:
title: "Moderation"
header_description: "The flagging system in Discourse helps you and your moderator team manage content and user behavior, keeping your community respectful and healthy. The defaults are suitable for most communities and you don’t have to change them. However, if your site has particular requirements you can disable flags you don’t need and add your own custom flags."
keywords: "flag|review|spam|illegal"
font_style:
title: "Font style"
header_description: "Customize the font styles used by your themes"
@ -5182,6 +5208,7 @@ en:
localization:
title: "Localization"
header_description: "Configure your community’s interface language and other localization options for your members"
keywords: "locale|language|timezone|unicode|ltr"
login_and_authentication:
title: "Login & authentication"
header_description: "Configure how users log in and authenticate, secrets and keys, OAuth2 providers, and more"
@ -5221,6 +5248,142 @@ en:
group_permissions:
title: "Group permissions"
header_description: "All group-based app permissions are managed here, which control access to various features within Discourse"
dashboard:
title: "Dashboard"
header_description: "The dashboard provides a snapshot of your community’s health, including traffic, user activity, and other key metrics"
users:
title: "Users"
header_description: "View and manage users, send invites, and export user data"
groups:
title: "Groups"
header_description: "Create and manage groups, set group permissions, and view group activity and inboxes"
site_settings:
title: "All site settings"
header_description: "Configure all settings for your Discourse site to customize its appearance, functionality, and user experience"
whats_new:
title: "What's new?"
header_description: "Stay up-to-date with the latest features and improvements in Discourse"
keywords: "changelog|feature|release"
backups:
title: "Backups"
header_description: "Discourse backups include the full site database, which contains everything on the site: topics, posts, users, groups, settings, themes, etc. Depending on how the backup file is created, it may or may not include uploads."
sub_pages:
logs:
title: "Logs"
header_description: "View and manage logs for backups, restores, and uploads"
reports:
title: "Reports"
header_description: "Reports are a powerful tool to help you understand what’s happening on your site. They can help you identify trends, spot problems, and make decisions based on data."
badges:
title: "Badges"
header_description: "Badges reward users for their activities, contributions, and achievements to recognize, validate, and encourage positive behavior and engagement within the community"
permalinks:
title: "Permalinks"
header_description: "Redirections to apply for URLs not known by the forum"
user_fields:
title: "User fields"
header_description: "Create custom user fields to collect extra details about your community members. You can choose what information is required during sign-up, what shows on profiles, and what users can update."
watched_words:
title: "Watched words"
header_description: "Watched words are moderation tools that can perform multiple different actions including blocking, censoring, linking, or flagging posts containing certain words"
color_palettes:
title: "Color palettes"
header_description: "Color palettes define the core colors used across your site’s interface, while themes can provide additional styling, layouts, and components. Both can work together to create your site’s unique look and feel, and both can be made available for users to select their preference"
emoji:
title: "Emoji"
header_description: "Add new emoji that will be available to everyone. Select multiple files to create emojis using their file names. The selected group will be used for all files that are added at the same time."
themes:
title: "Themes"
header_description: "Themes are expansive customizations that change multiple elements of the style of your forum design, and often also include additional front-end features"
components:
title: "Components"
header_description: "Components are smaller customizations that can be added to themes in order to change specific elements of the style of your forum design"
keywords: "theme|component|extension"
site_texts:
title: "Site texts"
header_description: "Customize any text used in Discourse to match your community’s voice and tone"
email:
title: "Server setup & logs"
header_description: "Customize the templates used to create emails, preview summary emails that will be sent out, and view email logs"
keywords: "email|smtp|mailgun|sendgrid|sent|skipped|bounced|received|rejected|email logs|preview summary"
sub_pages:
preview_summary:
title: "Preview summary"
header_description: "Preview the content of the summary emails sent to inactive users"
advanced_test:
title: "Advanced test"
header_description: "See how Discourse processes received emails"
templates:
title: "Templates"
header_description: "Customize the templates used to create emails"
sent:
title: "Sent"
header_description: "View a log of emails that have been sent"
skipped:
title: "Skipped"
header_description: "View a log of emails that have been skipped"
bounced:
title: "Bounced"
header_description: "View a log of emails that have bounced"
received:
title: "Received"
header_description: "View a log of emails that have been received"
rejected:
title: "Rejected"
header_description: "View a log of emails that have been rejected"
email_appearance:
title: "Appearance"
header_description: "Customize the template in which all html emails are rendered, and style using CSS"
staff_action_logs:
title: "Logs & screening"
header_description: "Logs and screening allow you to monitor and manage your community, ensuring that it remains safe and respectful. You can view logs of all actions taken by staff members, search logs, and user screening configuration"
keywords: "error logs|staff action|screened emails|screened ips|screened urls|search logs"
sub_pages:
screened_emails:
title: "Screened emails"
header_description: "When someone tries to create a new account, the following email addresses will be checked and the registration will be blocked, or some other action performed"
screened_ips:
title: "Screened IPs"
header_description: "IP addresses that are being watched. Individual IP addresses can be allowlisted."
screened_urls:
title: "Screened URLs"
header_description: "The URLs listed here were used in posts by users who have been identified as spammers"
search_logs:
title: "Search logs"
header_description: "View a log of searches that have been performed"
plugins:
title: "Installed plugins"
header_description: "Any Discourse plugins that you have installed, or plugins that come preinstalled with Discourse hosting, will appear in this list"
api_keys:
title: "API keys"
header_description: "The API keys feature lets you securely integrate Discourse with external systems and automate actions. Admins can create keys with specific scopes to control access to resources and sensitive data. Scopes limit functionality, ensuring enhanced security."
keywords: "token|scope"
webhooks:
title: "Webhooks"
header_description: "Webhooks allows Discourse to notify external services when certain events happen in your site. When the webhook is triggered, a POST request will send to URLs provided."
keywords: "event|payload|url"
embedding:
title: "Embedding"
header_description: "Discourse has the ability to embed the comments from a topic in a remote site using a Javascript API that creates an IFRAME"
search:
modal_title: "Search anything in admin"
result_types:
page:
one: "Page"
other: "Pages"
report:
one: "Report"
other: "Reports"
setting:
one: "Setting"
other: "Settings"
theme:
one: "Theme"
other: "Themes"
component:
one: "Component"
other: "Components"
new_features:
title: "What's new?"
@ -5702,81 +5865,9 @@ en:
failed: "Failed"
home:
title: "Home"
account:
title: "Account"
sidebar_link:
backups: "Backups"
whats_new:
title: "What's new?"
keywords: "changelog|feature|release"
community:
title: "Community"
sidebar_link:
about_your_site: "About your site"
badges: "Badges"
login_and_authentication: "Login & authentication"
notifications: "Notifications"
permalinks: "Permalinks"
trust_levels: "Trust levels"
group_permissions: "Group permissions"
users: "Users"
groups: "Groups"
localization:
title: "Localization"
keywords: "locale|language|timezone|unicode|ltr"
user_fields: "User fields"
watched_words: "Watched words"
legal: "Legal"
moderation_flags:
title: "Moderation"
keywords: "flag|review"
appearance:
title: "Appearance"
sidebar_link:
font_style: "Font style"
site_logo: "Site logo"
color_schemes: "Color palettes"
emoji: "Emoji"
navigation: "Navigation"
themes: "Themes"
components:
title: "Components"
keywords: "theme|extension"
site_texts: "Site texts"
email_settings:
title: "Email Settings"
sidebar_link:
appearance: "Appearance"
server_setup:
title: "Server setup & logs"
keywords: "email|smtp|mailgun|sendgrid|sent|skipped|bounced|received|rejected|email logs|preview summary"
security:
title: "Security"
sidebar_link:
security: "Security settings"
spam: "Spam settings"
staff_action_logs:
title: "Logs & screening"
keywords: "error logs|staff action|screened emails|screened ips|screened urls|search logs"
section_landing_pages:
account:
title: "Account"
backups:
title: "Backups"
description: "Take a backup of your site's data"
whats_new:
title: "What's new?"
description: "Discover new releases and improvements to Discourse"
config_areas:
about:
header: "About your site"
description: "Provide information here about this site and your team so that people can learn what your community is about, who is behind it, and how to reach you in case there is an issue. Displayed on your site's <a href='%{basePath}/about'>About page</a>."
general_settings: "General settings"
community_name: "Community name"
community_name_placeholder: "Example Community"
@ -5828,9 +5919,7 @@ en:
your_organization_saved: "Your organization saved"
saved: "saved!"
flags:
header: "Moderation"
edit_header: "Edit Flag"
subheader: "The flagging system in Discourse helps you and your moderator team manage content and user behavior, keeping your community respectful and healthy. The defaults are suitable for most communities and you don’t have to change them. However, if your site has particular requirements you can disable flags you don’t need and add your own custom flags."
description: "Description"
enabled: "Enabled?"
add: "Add flag"
@ -5873,8 +5962,6 @@ en:
more_options:
title: "More options"
look_and_feel:
title: "Look and feel"
description: "Customize and brand your Discourse site, giving it a distinctive style."
themes:
title: "Themes"
themes_intro: "Install a new theme to get started, or create your own from scratch using these resources."
@ -5896,8 +5983,6 @@ en:
save_successful: "User field saved."
plugins:
title: "Plugins"
installed: "Installed plugins"
description: "Any Discourse plugins that you have installed, or plugins that come preinstalled with Discourse hosting, will appear in this list."
name: "Name"
none_installed: "You don't have any plugins installed."
version: "Version"
@ -5912,26 +5997,6 @@ en:
author: "By %{author}"
experimental_badge: "experimental"
learn_more: "Learn more"
sidebar_link:
installed: "Installed"
advanced:
title: "Advanced"
sidebar_link:
api_keys:
title: "API keys"
keywords: "token"
webhooks: "Webhooks"
developer: "Developer"
embedding: "Embedding"
rate_limits: "Rate limits"
user_api: "User API"
onebox: "Onebox"
files: "Files"
other_options: "Other"
search: "Search"
experimental: "Experimental"
all_site_settings: "All site settings"
navigation_menu:
sidebar: "Sidebar"
@ -5941,7 +6006,6 @@ en:
backups:
title: "Backups"
files_title: "Backup files"
description: "Discourse backups include the full site database, which contains everything on the site: topics, posts, users, groups, settings, themes, etc. Depending on how the backup file is created, it may or may not include uploads."
learn_more_url: ""
menu:
backups: "Backups"

View File

@ -386,9 +386,11 @@ Discourse::Application.routes.draw do
post "preview" => "badges#preview"
end
end
get "search/all" => "search#index"
namespace :config, constraints: StaffConstraint.new do
resources :site_settings, only: %i[index]
get "developer" => "site_settings#index"
get "fonts" => "site_settings#index"
get "files" => "site_settings#index"

View File

@ -3532,6 +3532,15 @@ experimental:
allow_any: false
refresh: true
area: "group_permissions|navigation"
experimental_admin_search_enabled_groups:
hidden: true
client: true
type: group_list
list_type: compact
default: ""
allow_any: false
refresh: true
area: "group_permissions"
glimmer_topic_list_mode:
client: true
type: enum

View File

@ -101,6 +101,7 @@ class ApplicationLayoutPreloader
humanized_name: plugin.humanized_name,
admin_route: plugin.full_admin_route,
enabled: plugin.enabled?,
description: plugin.metadata.about,
}
end,
)

View File

@ -1451,7 +1451,12 @@ class Plugin::Instance
def setting_category_name
return if setting_category.blank? || setting_category == "plugins"
I18n.t("admin_js.admin.site_settings.categories.#{setting_category}")
I18n.t("admin_js.#{setting_category_label}")
end
def setting_category_label
return if setting_category.blank? || setting_category == "plugins"
"admin.site_settings.categories.#{setting_category}"
end
def validate_directory_column_name(column_name)
@ -1500,7 +1505,7 @@ class Plugin::Instance
def default_admin_route
{
label: "#{name.underscore}.title",
label: setting_category_label || "#{name.underscore}.title",
location: name,
use_new_show_route: true,
auto_generated: true,

View File

@ -201,6 +201,7 @@ module SiteSettingExtension
include_hidden: false,
include_locale_setting: true,
only_overridden: false,
basic_attributes: false,
filter_categories: nil,
filter_plugin: nil,
filter_names: nil,
@ -271,15 +272,22 @@ module SiteSettingExtension
setting: s,
description: description(s),
keywords: keywords(s),
category: categories[s],
primary_area: areas[s]&.first,
}
if !basic_attributes
opts.merge!(
default: default,
value: value.to_s,
category: categories[s],
preview: previews[s],
secret: secret_settings.include?(s),
placeholder: placeholder(s),
mandatory_values: mandatory_values[s],
requires_confirmation: requires_confirmation_settings[s],
}.merge!(type_hash)
)
opts.merge!(type_hash)
end
opts[:plugin] = plugins[s] if plugins[s]

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true
# name: chat
# about: Adds chat functionality to your site so it can natively support both long-form and short-form communication needs of your online community.
# about: Adds chat functionality to your site so it can natively support both long-form and short-form communication needs of your online community
# meta_topic_id: 230881
# version: 0.4
# authors: Kane York, Mark VanLandingham, Martin Brennan, Joffrey Jaffeux

View File

@ -31,6 +31,8 @@ RSpec.describe ApplicationController do
"auto_generated" => false,
},
"enabled" => true,
"description" =>
"Adds chat functionality to your site so it can natively support both long-form and short-form communication needs of your online community",
},
)
end

View File

@ -1,4 +1,9 @@
en:
admin_js:
admin:
site_settings:
categories:
discourse_details: "Discourse Details"
js:
details:
title: Hide Details

View File

@ -1,4 +1,9 @@
en:
admin_js:
admin:
site_settings:
categories:
discourse_local_dates: "Discourse Local Dates"
js:
discourse_local_dates:
relative_dates:

View File

@ -4,3 +4,8 @@ en:
welcome_post_type:
new_user_track: "Start the new user tutorial for all new users"
welcome_message: "Send all new users a welcome message with a quick start guide"
admin_js:
admin:
site_settings:
categories:
discourse_narrative_bot: "Discourse Narrative Bot"

View File

@ -1,4 +1,9 @@
en:
admin_js:
admin:
site_settings:
categories:
footnote: "Discourse Footnotes"
js:
footnote:
title: "Footnotes"

View File

@ -1,4 +1,9 @@
en:
admin_js:
admin:
site_settings:
categories:
poll: "Discourse Poll"
js:
poll:
voters:

View File

@ -1,4 +1,9 @@
en:
admin_js:
admin:
site_settings:
categories:
spoiler_alert: "Discourse Spoiler Alert"
js:
spoiler:
title: Blur Spoiler

View File

@ -1,4 +1,9 @@
en:
admin_js:
admin:
site_settings:
categories:
styleguide: "Discourse Styleguide"
js:
styleguide:
title: "Styleguide"

View File

@ -55,10 +55,10 @@ describe "Admin | Sidebar Navigation", type: :system do
expect(links.map(&:text)).to eq(
[
I18n.t("admin_js.admin.dashboard.title"),
I18n.t("admin_js.admin.community.sidebar_link.users"),
I18n.t("admin_js.admin.community.sidebar_link.groups"),
I18n.t("admin_js.admin.advanced.sidebar_link.all_site_settings"),
I18n.t("admin_js.admin.account.sidebar_link.whats_new.title"),
I18n.t("admin_js.admin.config.users.title"),
I18n.t("admin_js.admin.config.groups.title"),
I18n.t("admin_js.admin.config.site_settings.title"),
I18n.t("admin_js.admin.config.whats_new.title"),
],
)
end
@ -107,9 +107,9 @@ describe "Admin | Sidebar Navigation", type: :system do
links = page.all(".sidebar-section-link-content-text")
expect(links.map(&:text)).to eq(
[
I18n.t("admin_js.admin.community.sidebar_link.user_fields"),
I18n.t("admin_js.admin.community.sidebar_link.moderation_flags.title"),
I18n.t("admin_js.admin.email_settings.sidebar_link.server_setup.title"),
I18n.t("admin_js.admin.config.user_fields.title"),
I18n.t("admin_js.admin.config.flags.title"),
I18n.t("admin_js.admin.config.email.title"),
],
)
expect(page).to have_no_css(".sidebar-no-results")
@ -125,12 +125,13 @@ describe "Admin | Sidebar Navigation", type: :system do
expect(page).to have_css(".sidebar-sections__back-to-forum")
# When match section title, display all links
filter.filter("Email Sett")
filter.filter("Email")
links = page.all(".sidebar-section-link-content-text")
expect(links.map(&:text)).to eq(
[
I18n.t("admin_js.admin.email_settings.sidebar_link.server_setup.title"),
I18n.t("admin_js.admin.email_settings.sidebar_link.appearance"),
I18n.t("admin_js.admin.config.email.title"),
I18n.t("admin_js.admin.config.email_appearance.title"),
I18n.t("admin_js.admin.config.staff_action_logs.title"),
],
)
end
@ -192,10 +193,10 @@ describe "Admin | Sidebar Navigation", type: :system do
expect(links.map(&:text)).to eq(
[
I18n.t("admin_js.admin.dashboard.title"),
I18n.t("admin_js.admin.community.sidebar_link.users"),
I18n.t("admin_js.admin.community.sidebar_link.groups"),
I18n.t("admin_js.admin.advanced.sidebar_link.all_site_settings"),
I18n.t("admin_js.admin.account.sidebar_link.whats_new.title"),
I18n.t("admin_js.admin.config.users.title"),
I18n.t("admin_js.admin.config.groups.title"),
I18n.t("admin_js.admin.config.site_settings.title"),
I18n.t("admin_js.admin.config.whats_new.title"),
],
)
@ -203,9 +204,9 @@ describe "Admin | Sidebar Navigation", type: :system do
links = page.all(".sidebar-section-link-content-text")
expect(links.map(&:text)).to eq(
[
I18n.t("admin_js.admin.community.sidebar_link.user_fields"),
I18n.t("admin_js.admin.community.sidebar_link.moderation_flags.title"),
I18n.t("admin_js.admin.email_settings.sidebar_link.server_setup.title"),
I18n.t("admin_js.admin.config.user_fields.title"),
I18n.t("admin_js.admin.config.flags.title"),
I18n.t("admin_js.admin.config.email.title"),
],
)
@ -214,10 +215,10 @@ describe "Admin | Sidebar Navigation", type: :system do
expect(links.map(&:text)).to eq(
[
I18n.t("admin_js.admin.dashboard.title"),
I18n.t("admin_js.admin.community.sidebar_link.users"),
I18n.t("admin_js.admin.community.sidebar_link.groups"),
I18n.t("admin_js.admin.advanced.sidebar_link.all_site_settings"),
I18n.t("admin_js.admin.account.sidebar_link.whats_new.title"),
I18n.t("admin_js.admin.config.users.title"),
I18n.t("admin_js.admin.config.groups.title"),
I18n.t("admin_js.admin.config.site_settings.title"),
I18n.t("admin_js.admin.config.whats_new.title"),
],
)
end
@ -265,10 +266,10 @@ describe "Admin | Sidebar Navigation", type: :system do
expect(all(".sidebar-section-link-content-text").map(&:text)).to eq(
[
I18n.t("admin_js.admin.dashboard.title"),
I18n.t("admin_js.admin.community.sidebar_link.users"),
I18n.t("admin_js.admin.community.sidebar_link.groups"),
I18n.t("admin_js.admin.advanced.sidebar_link.all_site_settings"),
I18n.t("admin_js.admin.account.sidebar_link.whats_new.title"),
I18n.t("admin_js.admin.config.users.title"),
I18n.t("admin_js.admin.config.groups.title"),
I18n.t("admin_js.admin.config.site_settings.title"),
I18n.t("admin_js.admin.config.whats_new.title"),
],
)
@ -290,7 +291,7 @@ describe "Admin | Sidebar Navigation", type: :system do
filter.filter("csp_extension")
links = page.all(".sidebar-section-link-content-text")
expect(links.count).to eq(1)
expect(links.map(&:text)).to eq(["Installed"])
expect(links.map(&:text)).to eq([I18n.t("admin_js.admin.config.plugins.title")])
end
it "accepts components and themes keywords for filter" do
@ -328,12 +329,12 @@ describe "Admin | Sidebar Navigation", type: :system do
expect(links.map(&:text)).to eq(
[
I18n.t("admin_js.admin.dashboard.title"),
I18n.t("admin_js.admin.community.sidebar_link.users"),
I18n.t("admin_js.admin.community.sidebar_link.groups"),
I18n.t("admin_js.admin.account.sidebar_link.whats_new.title"),
I18n.t("admin_js.admin.reports.sidebar_link.all"),
I18n.t("admin_js.admin.community.sidebar_link.watched_words"),
I18n.t("admin_js.admin.security.sidebar_link.staff_action_logs.title"),
I18n.t("admin_js.admin.config.users.title"),
I18n.t("admin_js.admin.config.groups.title"),
I18n.t("admin_js.admin.config.whats_new.title"),
I18n.t("admin_js.admin.config.reports.title"),
I18n.t("admin_js.admin.config.watched_words.title"),
I18n.t("admin_js.admin.config.staff_action_logs.title"),
],
)
end