mirror of
https://github.com/discourse/discourse.git
synced 2025-04-27 10:44:31 +08:00

Admin sidebar links can have either a `href` or a `route`, and the admin search was not handling this properly. Also, we should always use `getURL()` on the href in case the link is internal, for subfolder sites. This is hard to test right now, I plan on adding more extensive links for admin-search-data-source in another PR.
325 lines
9.2 KiB
JavaScript
325 lines
9.2 KiB
JavaScript
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 { ADMIN_SEARCH_RESULT_TYPES } from "discourse/lib/constants";
|
|
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";
|
|
|
|
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);
|
|
await Promise.resolve();
|
|
|
|
this._mapCached = true;
|
|
}
|
|
|
|
search(filter, opts = {}) {
|
|
if (filter.length < MIN_FILTER_LENGTH) {
|
|
return [];
|
|
}
|
|
|
|
opts.types = opts.types || ADMIN_SEARCH_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.route) {
|
|
if (link.routeModels) {
|
|
url = this.router.urlFor(link.route, ...link.routeModels);
|
|
} else {
|
|
url = this.router.urlFor(link.route);
|
|
}
|
|
} else if (link.href) {
|
|
url = getURL(link.href);
|
|
}
|
|
|
|
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();
|
|
}
|
|
}
|