mirror of
https://github.com/discourse/discourse.git
synced 2024-11-22 08:57:10 -06:00
FEATURE: Allow site settings to be edited throughout admin UI (#26154)
This commit makes it so the site settings filter controls and the list of settings input editors themselves can be used elsewhere in the admin UI outside of /admin/site_settings This allows us to provide more targeted groups of settings in different UI areas where it makes sense to provide them, such as on plugin pages. You could open a single page for a plugin where you can see information about that plugin, change settings, and configure it with custom UIs in the one place. In future we will do this in "config areas" for other parts of the admin UI.
This commit is contained in:
parent
d0d659e733
commit
78bafb331a
@ -0,0 +1,75 @@
|
||||
import Component from "@glimmer/component";
|
||||
import { tracked } from "@glimmer/tracking";
|
||||
import { action } from "@ember/object";
|
||||
import { cancel } from "@ember/runloop";
|
||||
import { inject as service } from "@ember/service";
|
||||
import { isEmpty } from "@ember/utils";
|
||||
import ConditionalLoadingSpinner from "discourse/components/conditional-loading-spinner";
|
||||
import SiteSettingFilter from "discourse/lib/site-setting-filter";
|
||||
import i18n from "discourse-common/helpers/i18n";
|
||||
import discourseDebounce from "discourse-common/lib/debounce";
|
||||
import AdminSiteSettingsFilterControls from "admin/components/admin-site-settings-filter-controls";
|
||||
import SiteSetting from "admin/components/site-setting";
|
||||
|
||||
export default class AdminPluginFilteredSiteSettings extends Component {
|
||||
@service currentUser;
|
||||
@tracked visibleSettings;
|
||||
@tracked loading = true;
|
||||
|
||||
siteSettingFilter = new SiteSettingFilter(this.args.settings);
|
||||
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
this.filterChanged({ filter: "", onlyOverridden: false });
|
||||
}
|
||||
|
||||
filterSettings(filterData) {
|
||||
this.args.onFilterChanged(filterData);
|
||||
this.visibleSettings = this.siteSettingFilter.filterSettings(
|
||||
filterData.filter,
|
||||
{
|
||||
includeAllCategory: false,
|
||||
onlyOverridden: filterData.onlyOverridden,
|
||||
}
|
||||
)[0]?.siteSettings;
|
||||
this.loading = false;
|
||||
}
|
||||
|
||||
@action
|
||||
filterChanged(filterData) {
|
||||
this._debouncedOnChangeFilter(filterData);
|
||||
}
|
||||
|
||||
get noResults() {
|
||||
return isEmpty(this.visibleSettings) && !this.loading;
|
||||
}
|
||||
|
||||
_debouncedOnChangeFilter(filterData) {
|
||||
cancel(this.onChangeFilterHandler);
|
||||
this.onChangeFilterHandler = discourseDebounce(
|
||||
this,
|
||||
this.filterSettings,
|
||||
filterData,
|
||||
100
|
||||
);
|
||||
}
|
||||
|
||||
<template>
|
||||
<AdminSiteSettingsFilterControls
|
||||
@onChangeFilter={{this.filterChanged}}
|
||||
@initialFilter={{@initialFilter}}
|
||||
/>
|
||||
|
||||
<ConditionalLoadingSpinner @condition={{this.loading}}>
|
||||
<section class="form-horizontal settings">
|
||||
{{#each this.visibleSettings as |setting|}}
|
||||
<SiteSetting @setting={{setting}} />
|
||||
{{/each}}
|
||||
|
||||
{{#if this.noResults}}
|
||||
{{i18n "admin.site_settings.no_results"}}
|
||||
{{/if}}
|
||||
</section>
|
||||
</ConditionalLoadingSpinner>
|
||||
</template>
|
||||
}
|
@ -0,0 +1,87 @@
|
||||
import Component from "@glimmer/component";
|
||||
import { tracked } from "@glimmer/tracking";
|
||||
import { Input } from "@ember/component";
|
||||
import { on } from "@ember/modifier";
|
||||
import { action } from "@ember/object";
|
||||
import didInsert from "@ember/render-modifiers/modifiers/did-insert";
|
||||
import DButton from "discourse/components/d-button";
|
||||
import TextField from "discourse/components/text-field";
|
||||
import i18n from "discourse-common/helpers/i18n";
|
||||
|
||||
export default class AdminSiteSettingsFilterControls extends Component {
|
||||
@tracked filter = this.args.initialFilter || "";
|
||||
@tracked onlyOverridden = false;
|
||||
|
||||
@action
|
||||
clearFilter() {
|
||||
this.filter = "";
|
||||
this.onlyOverridden = false;
|
||||
this.onChangeFilter();
|
||||
}
|
||||
|
||||
@action
|
||||
onChangeFilter() {
|
||||
this.args.onChangeFilter({
|
||||
filter: this.filter,
|
||||
onlyOverridden: this.onlyOverridden,
|
||||
});
|
||||
}
|
||||
|
||||
@action
|
||||
onToggleOverridden(event) {
|
||||
this.onlyOverridden = event.target.checked;
|
||||
this.onChangeFilter();
|
||||
}
|
||||
|
||||
@action
|
||||
runInitialFilter() {
|
||||
this.onChangeFilter();
|
||||
}
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="admin-controls admin-site-settings-filter-controls"
|
||||
{{didInsert this.runInitialFilter}}
|
||||
>
|
||||
<div class="controls">
|
||||
<div class="inline-form">
|
||||
{{#if @showMenu}}
|
||||
<DButton
|
||||
@action={{@onToggleMenu}}
|
||||
@icon="bars"
|
||||
class="menu-toggle"
|
||||
/>
|
||||
{{/if}}
|
||||
<TextField
|
||||
@type="text"
|
||||
@value={{this.filter}}
|
||||
placeholder={{i18n "type_to_filter"}}
|
||||
@onChange={{this.onChangeFilter}}
|
||||
class="no-blur"
|
||||
id="setting-filter"
|
||||
autocomplete="off"
|
||||
/>
|
||||
<DButton
|
||||
@action={{this.clearFilter}}
|
||||
@label="admin.site_settings.clear_filter"
|
||||
id="clear-filter"
|
||||
class="btn-default"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="search controls">
|
||||
<label>
|
||||
<Input
|
||||
@type="checkbox"
|
||||
@checked={{this.onlyOverridden}}
|
||||
class="toggle-overridden"
|
||||
id="setting-filter-toggle-overridden"
|
||||
{{on "click" this.onToggleOverridden}}
|
||||
/>
|
||||
{{i18n "admin.settings.show_overriden"}}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
}
|
@ -1,7 +1,12 @@
|
||||
{{#if this.setting.textarea}}
|
||||
<Textarea @value={{this.value}} class="input-setting-textarea" />
|
||||
{{else if this.isSecret}}
|
||||
<Input @type="password" @value={{this.value}} class="input-setting-string" />
|
||||
<Input
|
||||
@type="password"
|
||||
@value={{this.value}}
|
||||
class="input-setting-string"
|
||||
autocomplete="new-password"
|
||||
/>
|
||||
{{else}}
|
||||
<TextField @value={{this.value}} @classNames="input-setting-string" />
|
||||
{{/if}}
|
||||
|
@ -0,0 +1,11 @@
|
||||
import Controller from "@ember/controller";
|
||||
import { action } from "@ember/object";
|
||||
|
||||
export default class AdminSiteSettingsController extends Controller {
|
||||
filter = "";
|
||||
|
||||
@action
|
||||
filterChanged(filterData) {
|
||||
this.set("filter", filterData.filter);
|
||||
}
|
||||
}
|
@ -3,151 +3,27 @@ import { action } from "@ember/object";
|
||||
import { alias } from "@ember/object/computed";
|
||||
import { service } from "@ember/service";
|
||||
import { isEmpty } from "@ember/utils";
|
||||
import { observes } from "@ember-decorators/object";
|
||||
import SiteSettingFilter from "discourse/lib/site-setting-filter";
|
||||
import { INPUT_DELAY } from "discourse-common/config/environment";
|
||||
import { debounce } from "discourse-common/utils/decorators";
|
||||
import I18n from "discourse-i18n";
|
||||
|
||||
export default class AdminSiteSettingsController extends Controller {
|
||||
@service router;
|
||||
|
||||
filter = "";
|
||||
|
||||
@alias("model") allSiteSettings;
|
||||
|
||||
filter = "";
|
||||
visibleSiteSettings = null;
|
||||
onlyOverridden = false;
|
||||
siteSettingFilter = null;
|
||||
|
||||
get maxResults() {
|
||||
return 100;
|
||||
}
|
||||
filterContentNow(filterData, category) {
|
||||
this.siteSettingFilter ??= new SiteSettingFilter(this.allSiteSettings);
|
||||
|
||||
sortSettings(settings) {
|
||||
// Sort the site settings so that fuzzy results are at the bottom
|
||||
// and ordered by their gap count asc.
|
||||
return settings.sort((a, b) => {
|
||||
const aWeight = a.weight === undefined ? 0 : a.weight;
|
||||
const bWeight = b.weight === undefined ? 0 : b.weight;
|
||||
return aWeight - bWeight;
|
||||
});
|
||||
}
|
||||
|
||||
performSearch(filter, allSiteSettings, onlyOverridden) {
|
||||
let pluginFilter;
|
||||
|
||||
if (filter) {
|
||||
filter = filter
|
||||
.toLowerCase()
|
||||
.split(" ")
|
||||
.filter((word) => {
|
||||
if (word.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (word.startsWith("plugin:")) {
|
||||
pluginFilter = word.slice("plugin:".length).trim();
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
})
|
||||
.join(" ")
|
||||
.trim();
|
||||
}
|
||||
|
||||
const all = {
|
||||
nameKey: "all_results",
|
||||
name: I18n.t("admin.site_settings.categories.all_results"),
|
||||
siteSettings: [],
|
||||
};
|
||||
|
||||
const matchesGroupedByCategory = [all];
|
||||
const matches = [];
|
||||
|
||||
const strippedQuery = filter.replace(/[^a-z0-9]/gi, "");
|
||||
let fuzzyRegex;
|
||||
let fuzzyRegexGaps;
|
||||
|
||||
if (strippedQuery.length > 2) {
|
||||
fuzzyRegex = new RegExp(strippedQuery.split("").join(".*"), "i");
|
||||
fuzzyRegexGaps = new RegExp(strippedQuery.split("").join("(.*)"), "i");
|
||||
}
|
||||
|
||||
allSiteSettings.forEach((settingsCategory) => {
|
||||
let fuzzyMatches = [];
|
||||
|
||||
const siteSettings = settingsCategory.siteSettings.filter((item) => {
|
||||
if (onlyOverridden && !item.get("overridden")) {
|
||||
return false;
|
||||
}
|
||||
if (pluginFilter && item.plugin !== pluginFilter) {
|
||||
return false;
|
||||
}
|
||||
if (filter) {
|
||||
const setting = item.get("setting").toLowerCase();
|
||||
let filterResult =
|
||||
setting.includes(filter) ||
|
||||
setting.replace(/_/g, " ").includes(filter) ||
|
||||
item.get("description").toLowerCase().includes(filter) ||
|
||||
(item.get("keywords") || "")
|
||||
.replace(/_/g, " ")
|
||||
.toLowerCase()
|
||||
.includes(filter.replace(/_/g, " ")) ||
|
||||
(item.get("value") || "").toString().toLowerCase().includes(filter);
|
||||
if (!filterResult && fuzzyRegex && fuzzyRegex.test(setting)) {
|
||||
// Tightens up fuzzy search results a bit.
|
||||
const fuzzySearchLimiter = 25;
|
||||
const strippedSetting = setting.replace(/[^a-z0-9]/gi, "");
|
||||
if (
|
||||
strippedSetting.length <=
|
||||
strippedQuery.length + fuzzySearchLimiter
|
||||
) {
|
||||
const gapResult = strippedSetting.match(fuzzyRegexGaps);
|
||||
if (gapResult) {
|
||||
item.weight = gapResult.filter((gap) => gap !== "").length;
|
||||
}
|
||||
fuzzyMatches.push(item);
|
||||
}
|
||||
}
|
||||
return filterResult;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
if (fuzzyMatches.length > 0) {
|
||||
siteSettings.pushObjects(fuzzyMatches);
|
||||
}
|
||||
|
||||
if (siteSettings.length > 0) {
|
||||
matches.pushObjects(siteSettings);
|
||||
matchesGroupedByCategory.pushObject({
|
||||
nameKey: settingsCategory.nameKey,
|
||||
name: I18n.t(
|
||||
"admin.site_settings.categories." + settingsCategory.nameKey
|
||||
),
|
||||
siteSettings: this.sortSettings(siteSettings),
|
||||
count: siteSettings.length,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
all.siteSettings.pushObjects(matches.slice(0, this.maxResults));
|
||||
all.siteSettings = this.sortSettings(all.siteSettings);
|
||||
|
||||
all.hasMore = matches.length > this.maxResults;
|
||||
all.count = all.hasMore ? `${this.maxResults}+` : matches.length;
|
||||
all.maxResults = this.maxResults;
|
||||
|
||||
return matchesGroupedByCategory;
|
||||
}
|
||||
|
||||
filterContentNow(category) {
|
||||
if (isEmpty(this.allSiteSettings)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isEmpty(this.filter) && !this.onlyOverridden) {
|
||||
if (isEmpty(filterData.filter) && !filterData.onlyOverridden) {
|
||||
this.set("visibleSiteSettings", this.allSiteSettings);
|
||||
if (this.categoryNameKey === "all_results") {
|
||||
this.router.transitionTo("adminSiteSettings");
|
||||
@ -155,10 +31,11 @@ export default class AdminSiteSettingsController extends Controller {
|
||||
return;
|
||||
}
|
||||
|
||||
const matchesGroupedByCategory = this.performSearch(
|
||||
this.filter,
|
||||
this.allSiteSettings,
|
||||
this.onlyOverridden
|
||||
this.set("filter", filterData.filter);
|
||||
|
||||
const matchesGroupedByCategory = this.siteSettingFilter.filterSettings(
|
||||
filterData.filter,
|
||||
{ onlyOverridden: filterData.onlyOverridden }
|
||||
);
|
||||
|
||||
const categoryMatches = matchesGroupedByCategory.findBy(
|
||||
@ -177,25 +54,20 @@ export default class AdminSiteSettingsController extends Controller {
|
||||
);
|
||||
}
|
||||
|
||||
@observes("filter", "onlyOverridden", "model")
|
||||
optsChanged() {
|
||||
this.filterContent();
|
||||
}
|
||||
|
||||
@debounce(INPUT_DELAY)
|
||||
filterContent() {
|
||||
filterContent(filterData) {
|
||||
if (this._skipBounce) {
|
||||
this.set("_skipBounce", false);
|
||||
} else {
|
||||
if (!this.isDestroyed) {
|
||||
this.filterContentNow(this.categoryNameKey);
|
||||
this.filterContentNow(filterData, this.categoryNameKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
clearFilter() {
|
||||
this.setProperties({ filter: "", onlyOverridden: false });
|
||||
filterChanged(filterData) {
|
||||
this.filterContent(filterData);
|
||||
}
|
||||
|
||||
@action
|
||||
|
@ -5,8 +5,10 @@ import I18n from "discourse-i18n";
|
||||
import Setting from "admin/mixins/setting-object";
|
||||
|
||||
export default class SiteSetting extends EmberObject.extend(Setting) {
|
||||
static findAll() {
|
||||
return ajax("/admin/site_settings").then(function (settings) {
|
||||
static findAll(params = {}) {
|
||||
return ajax("/admin/site_settings", { data: params }).then(function (
|
||||
settings
|
||||
) {
|
||||
// Group the results by category
|
||||
const categories = {};
|
||||
settings.site_settings.forEach(function (s) {
|
||||
|
@ -0,0 +1,18 @@
|
||||
import Route from "@ember/routing/route";
|
||||
import { inject as service } from "@ember/service";
|
||||
import SiteSetting from "admin/models/site-setting";
|
||||
|
||||
export default class AdminPluginsShowSettingsRoute extends Route {
|
||||
@service router;
|
||||
|
||||
queryParams = {
|
||||
filter: { replace: true },
|
||||
};
|
||||
|
||||
model(params) {
|
||||
const plugin = this.modelFor("adminPlugins.show");
|
||||
return SiteSetting.findAll({ plugin: plugin.name }).then((settings) => {
|
||||
return { plugin, settings, initialFilter: params.filter };
|
||||
});
|
||||
}
|
||||
}
|
@ -1 +1,10 @@
|
||||
<div class="content-body admin-plugin-config-area__settings"></div>
|
||||
<div
|
||||
class="content-body admin-plugin-config-area__settings admin-detail pull-left"
|
||||
>
|
||||
<AdminPluginFilteredSiteSettings
|
||||
@initialFilter={{@model.initialFilter}}
|
||||
@plugin={{@model.plugin}}
|
||||
@settings={{@model.settings}}
|
||||
@onFilterChanged={{this.filterChanged}}
|
||||
/>
|
||||
</div>
|
@ -1,33 +1,9 @@
|
||||
<div class="admin-controls">
|
||||
<div class="controls">
|
||||
<div class="inline-form">
|
||||
<DButton @action={{this.toggleMenu}} @icon="bars" class="menu-toggle" />
|
||||
<TextField
|
||||
@id="setting-filter"
|
||||
@value={{this.filter}}
|
||||
@placeholderKey="type_to_filter"
|
||||
class="no-blur"
|
||||
/>
|
||||
<DButton
|
||||
@action={{this.clearFilter}}
|
||||
@label="admin.site_settings.clear_filter"
|
||||
id="clear-filter"
|
||||
class="btn-default"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="search controls">
|
||||
<label>
|
||||
<Input
|
||||
@type="checkbox"
|
||||
@checked={{this.onlyOverridden}}
|
||||
class="toggle-overridden"
|
||||
/>
|
||||
{{i18n "admin.settings.show_overriden"}}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<AdminSiteSettingsFilterControls
|
||||
@initialFilter={{this.filter}}
|
||||
@onChangeFilter={{this.filterChanged}}
|
||||
@showMenu={{true}}
|
||||
@onToggleMenu={{this.toggleMenu}}
|
||||
/>
|
||||
|
||||
<div class="admin-nav admin-site-settings-category-nav pull-left">
|
||||
<ul class="nav nav-stacked">
|
||||
|
144
app/assets/javascripts/discourse/app/lib/site-setting-filter.js
Normal file
144
app/assets/javascripts/discourse/app/lib/site-setting-filter.js
Normal file
@ -0,0 +1,144 @@
|
||||
import { bind } from "discourse-common/utils/decorators";
|
||||
import I18n from "discourse-i18n";
|
||||
|
||||
export default class SiteSettingFilter {
|
||||
constructor(siteSettings) {
|
||||
this.siteSettings = siteSettings;
|
||||
}
|
||||
|
||||
filterSettings(filter, opts = {}) {
|
||||
opts.maxResults ??= 100;
|
||||
opts.onlyOverridden ??= false;
|
||||
|
||||
return this.performSearch(filter, opts);
|
||||
}
|
||||
|
||||
@bind
|
||||
performSearch(filter, opts) {
|
||||
opts.includeAllCategory ??= true;
|
||||
|
||||
let pluginFilter;
|
||||
|
||||
if (filter) {
|
||||
filter = filter
|
||||
.toLowerCase()
|
||||
.split(" ")
|
||||
.filter((word) => {
|
||||
if (!word.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (word.startsWith("plugin:")) {
|
||||
pluginFilter = word.slice("plugin:".length).trim();
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
})
|
||||
.join(" ")
|
||||
.trim();
|
||||
}
|
||||
|
||||
const matchesGroupedByCategory = [];
|
||||
const matches = [];
|
||||
|
||||
let all;
|
||||
if (opts.includeAllCategory) {
|
||||
all = {
|
||||
nameKey: "all_results",
|
||||
name: I18n.t("admin.site_settings.categories.all_results"),
|
||||
siteSettings: [],
|
||||
};
|
||||
|
||||
matchesGroupedByCategory.push(all);
|
||||
}
|
||||
|
||||
const strippedQuery = filter.replace(/[^a-z0-9]/gi, "");
|
||||
let fuzzyRegex;
|
||||
let fuzzyRegexGaps;
|
||||
|
||||
if (strippedQuery.length > 2) {
|
||||
fuzzyRegex = new RegExp(strippedQuery.split("").join(".*"), "i");
|
||||
fuzzyRegexGaps = new RegExp(strippedQuery.split("").join("(.*)"), "i");
|
||||
}
|
||||
|
||||
this.siteSettings.forEach((settingsCategory) => {
|
||||
let fuzzyMatches = [];
|
||||
|
||||
const siteSettings = settingsCategory.siteSettings.filter((item) => {
|
||||
if (opts.onlyOverridden && !item.get("overridden")) {
|
||||
return false;
|
||||
}
|
||||
if (pluginFilter && item.plugin !== pluginFilter) {
|
||||
return false;
|
||||
}
|
||||
if (filter) {
|
||||
const setting = item.get("setting").toLowerCase();
|
||||
let filterResult =
|
||||
setting.includes(filter) ||
|
||||
setting.replace(/_/g, " ").includes(filter) ||
|
||||
item.get("description").toLowerCase().includes(filter) ||
|
||||
(item.get("keywords") || "")
|
||||
.replace(/_/g, " ")
|
||||
.toLowerCase()
|
||||
.includes(filter.replace(/_/g, " ")) ||
|
||||
(item.get("value") || "").toString().toLowerCase().includes(filter);
|
||||
if (!filterResult && fuzzyRegex && fuzzyRegex.test(setting)) {
|
||||
// Tightens up fuzzy search results a bit.
|
||||
const fuzzySearchLimiter = 25;
|
||||
const strippedSetting = setting.replace(/[^a-z0-9]/gi, "");
|
||||
if (
|
||||
strippedSetting.length <=
|
||||
strippedQuery.length + fuzzySearchLimiter
|
||||
) {
|
||||
const gapResult = strippedSetting.match(fuzzyRegexGaps);
|
||||
if (gapResult) {
|
||||
item.weight = gapResult.filter((gap) => gap !== "").length;
|
||||
}
|
||||
fuzzyMatches.push(item);
|
||||
}
|
||||
}
|
||||
return filterResult;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
if (fuzzyMatches.length > 0) {
|
||||
siteSettings.pushObjects(fuzzyMatches);
|
||||
}
|
||||
|
||||
if (siteSettings.length > 0) {
|
||||
matches.pushObjects(siteSettings);
|
||||
matchesGroupedByCategory.pushObject({
|
||||
nameKey: settingsCategory.nameKey,
|
||||
name: I18n.t(
|
||||
"admin.site_settings.categories." + settingsCategory.nameKey
|
||||
),
|
||||
siteSettings: this.sortSettings(siteSettings),
|
||||
count: siteSettings.length,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if (opts.includeAllCategory) {
|
||||
all.siteSettings.pushObjects(matches.slice(0, opts.maxResults));
|
||||
all.siteSettings = this.sortSettings(all.siteSettings);
|
||||
|
||||
all.hasMore = matches.length > opts.maxResults;
|
||||
all.count = all.hasMore ? `${opts.maxResults}+` : matches.length;
|
||||
all.maxResults = opts.maxResults;
|
||||
}
|
||||
|
||||
return matchesGroupedByCategory;
|
||||
}
|
||||
|
||||
@bind
|
||||
sortSettings(settings) {
|
||||
// Sort the site settings so that fuzzy results are at the bottom
|
||||
// and ordered by their gap count asc.
|
||||
return settings.sort((a, b) => {
|
||||
return (a.weight || 0) - (b.weight || 0);
|
||||
});
|
||||
}
|
||||
}
|
@ -212,4 +212,29 @@ acceptance("Admin - Site Settings", function (needs) {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test("can perform fuzzy search", async function (assert) {
|
||||
await visit("/admin/site_settings");
|
||||
|
||||
await fillIn("#setting-filter", "top_menu");
|
||||
assert.dom(".row.setting").exists({ count: 1 });
|
||||
|
||||
await fillIn("#setting-filter", "tmenu");
|
||||
assert.dom(".row.setting").exists({ count: 1 });
|
||||
|
||||
// ensures fuzzy search limiter is in place
|
||||
await fillIn("#setting-filter", "obo");
|
||||
assert.dom(".row.setting").exists({ count: 1 });
|
||||
assert.dom(".row.setting").hasText(/onebox/);
|
||||
|
||||
// ensures fuzzy search limiter doesn't limit too much
|
||||
await fillIn("#setting-filter", "blocked_onebox_domains");
|
||||
assert.dom(".row.setting").exists({ count: 1 });
|
||||
assert.dom(".row.setting").hasText(/onebox/);
|
||||
|
||||
// ensures keyword search is working
|
||||
await fillIn("#setting-filter", "blah");
|
||||
assert.dom(".row.setting").exists({ count: 1 });
|
||||
assert.dom(".row.setting").hasText(/username/);
|
||||
});
|
||||
});
|
||||
|
@ -21,6 +21,7 @@ export default {
|
||||
preview: null,
|
||||
secret: false,
|
||||
type: "username",
|
||||
keywords: "blah blah",
|
||||
},
|
||||
{
|
||||
setting: "logo",
|
||||
|
@ -1,81 +0,0 @@
|
||||
import { setupTest } from "ember-qunit";
|
||||
import { module, test } from "qunit";
|
||||
import SiteSetting from "admin/models/site-setting";
|
||||
|
||||
module("Unit | Controller | admin-site-settings", function (hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
test("can perform fuzzy search", async function (assert) {
|
||||
const controller = this.owner.lookup("controller:admin-site-settings");
|
||||
const settings = await SiteSetting.findAll();
|
||||
|
||||
let results = controller.performSearch("top_menu", settings);
|
||||
assert.deepEqual(results[0].siteSettings.length, 1);
|
||||
|
||||
results = controller.performSearch("tmenu", settings);
|
||||
assert.deepEqual(results[0].siteSettings.length, 1);
|
||||
|
||||
const settings2 = [
|
||||
{
|
||||
name: "Required",
|
||||
nameKey: "required",
|
||||
siteSettings: [
|
||||
SiteSetting.create({
|
||||
description: "",
|
||||
value: "",
|
||||
setting: "hpello world",
|
||||
}),
|
||||
SiteSetting.create({
|
||||
description: "",
|
||||
value: "",
|
||||
setting: "hello world",
|
||||
}),
|
||||
SiteSetting.create({
|
||||
description: "",
|
||||
value: "",
|
||||
setting: "digest_logo",
|
||||
keywords: "capybara",
|
||||
}),
|
||||
SiteSetting.create({
|
||||
description: "",
|
||||
value: "",
|
||||
setting: "pending_users_reminder_delay_minutes",
|
||||
}),
|
||||
SiteSetting.create({
|
||||
description: "",
|
||||
value: "",
|
||||
setting: "min_personal_message_post_length",
|
||||
}),
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
results = controller.performSearch("hello world", settings2);
|
||||
assert.deepEqual(results[0].siteSettings.length, 2);
|
||||
// ensures hello world shows up before fuzzy hpello world
|
||||
assert.deepEqual(results[0].siteSettings[0].setting, "hello world");
|
||||
|
||||
results = controller.performSearch("world", settings2);
|
||||
assert.deepEqual(results[0].siteSettings.length, 2);
|
||||
// ensures hello world shows up before fuzzy hpello world with "world" search
|
||||
assert.deepEqual(results[0].siteSettings[0].setting, "hello world");
|
||||
|
||||
// ensures fuzzy search limiter is in place
|
||||
results = controller.performSearch("digest", settings2);
|
||||
assert.deepEqual(results[0].siteSettings.length, 1);
|
||||
assert.deepEqual(results[0].siteSettings[0].setting, "digest_logo");
|
||||
|
||||
// ensures fuzzy search limiter doesn't limit too much
|
||||
results = controller.performSearch("min length", settings2);
|
||||
assert.strictEqual(results[0].siteSettings.length, 1);
|
||||
assert.strictEqual(
|
||||
results[0].siteSettings[0].setting,
|
||||
"min_personal_message_post_length"
|
||||
);
|
||||
|
||||
// ensures keyword search is working
|
||||
results = controller.performSearch("capybara", settings2);
|
||||
assert.deepEqual(results[0].siteSettings.length, 1);
|
||||
assert.deepEqual(results[0].siteSettings[0].setting, "digest_logo");
|
||||
});
|
||||
});
|
@ -98,3 +98,17 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.admin-plugin-filtered-site-settings {
|
||||
&__filter {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.admin-plugin-config-area {
|
||||
&__settings {
|
||||
.admin-site-settings-filter-controls {
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -6,7 +6,15 @@ class Admin::SiteSettingsController < Admin::AdminController
|
||||
end
|
||||
|
||||
def index
|
||||
render_json_dump(site_settings: SiteSetting.all_settings)
|
||||
params.permit(:categories, :plugin)
|
||||
|
||||
render_json_dump(
|
||||
site_settings:
|
||||
SiteSetting.all_settings(
|
||||
filter_categories: params[:categories],
|
||||
filter_plugin: params[:plugin],
|
||||
),
|
||||
)
|
||||
end
|
||||
|
||||
def update
|
||||
|
@ -176,7 +176,13 @@ module SiteSettingExtension
|
||||
end
|
||||
|
||||
# Retrieve all settings
|
||||
def all_settings(include_hidden: false)
|
||||
def all_settings(
|
||||
include_hidden: false,
|
||||
include_locale_setting: true,
|
||||
only_overridden: false,
|
||||
filter_categories: nil,
|
||||
filter_plugin: nil
|
||||
)
|
||||
locale_setting_hash = {
|
||||
setting: "default_locale",
|
||||
default: SiteSettings::DefaultsProvider::DEFAULT_LOCALE,
|
||||
@ -189,12 +195,28 @@ module SiteSettingExtension
|
||||
translate_names: LocaleSiteSetting.translate_names?,
|
||||
}
|
||||
|
||||
include_locale_setting = false if filter_categories.present? || filter_plugin.present?
|
||||
|
||||
defaults
|
||||
.all(default_locale)
|
||||
.reject do |setting_name, _|
|
||||
plugins[name] && !Discourse.plugins_by_name[plugins[name]].configurable?
|
||||
end
|
||||
.reject { |setting_name, _| !include_hidden && hidden_settings.include?(setting_name) }
|
||||
.select do |setting_name, _|
|
||||
if filter_categories && filter_categories.any?
|
||||
filter_categories.include?(categories[setting_name])
|
||||
else
|
||||
true
|
||||
end
|
||||
end
|
||||
.select do |setting_name, _|
|
||||
if filter_plugin
|
||||
plugins[setting_name] == filter_plugin
|
||||
else
|
||||
true
|
||||
end
|
||||
end
|
||||
.map do |s, v|
|
||||
type_hash = type_supervisor.type_hash(s)
|
||||
default = defaults.get(s, default_locale).to_s
|
||||
@ -222,7 +244,15 @@ module SiteSettingExtension
|
||||
|
||||
opts
|
||||
end
|
||||
.unshift(locale_setting_hash)
|
||||
.select do |setting|
|
||||
if only_overridden
|
||||
setting[:value] != setting[:default]
|
||||
else
|
||||
true
|
||||
end
|
||||
end
|
||||
.unshift(include_locale_setting && !only_overridden ? locale_setting_hash : nil)
|
||||
.compact
|
||||
end
|
||||
|
||||
def description(setting)
|
||||
|
14
plugins/chat/spec/lib/site_setting_extension_spec.rb
Normal file
14
plugins/chat/spec/lib/site_setting_extension_spec.rb
Normal file
@ -0,0 +1,14 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
RSpec.describe SiteSettingExtension do
|
||||
describe "#all_settings" do
|
||||
it "allows filtering settings by plugin via filter_plugin" do
|
||||
settings = YAML.safe_load(File.read(Rails.root.join("plugins/chat/config/settings.yml")))
|
||||
expect(
|
||||
SiteSetting
|
||||
.all_settings(include_hidden: true, filter_plugin: "chat")
|
||||
.map { |s| s[:setting] },
|
||||
).to match_array(settings["chat"].keys.map(&:to_sym))
|
||||
end
|
||||
end
|
||||
end
|
@ -171,4 +171,29 @@ RSpec.describe SiteSetting do
|
||||
|
||||
expect(settings.test_setting).to eq(value)
|
||||
end
|
||||
|
||||
describe "#all_settings" do
|
||||
it "does not include the `default_locale` setting if include_locale_setting is false" do
|
||||
expect(SiteSetting.all_settings.map { |s| s[:setting] }).to include("default_locale")
|
||||
expect(
|
||||
SiteSetting.all_settings(include_locale_setting: false).map { |s| s[:setting] },
|
||||
).not_to include("default_locale")
|
||||
end
|
||||
|
||||
it "does not include the `default_locale` setting if filter_categories are specified" do
|
||||
expect(
|
||||
SiteSetting.all_settings(filter_categories: ["branding"]).map { |s| s[:setting] },
|
||||
).not_to include("default_locale")
|
||||
end
|
||||
|
||||
it "does not include the `default_locale` setting if filter_plugin is specified" do
|
||||
expect(
|
||||
SiteSetting.all_settings(filter_plugin: "chat").map { |s| s[:setting] },
|
||||
).not_to include("default_locale")
|
||||
end
|
||||
|
||||
it "includes only settings for the specified category" do
|
||||
expect(SiteSetting.all_settings(filter_categories: ["required"]).count).to eq(12)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -9,6 +9,23 @@ describe "Admin Site Setting Search", type: :system do
|
||||
sign_in(admin)
|
||||
end
|
||||
|
||||
it "clears the filter" do
|
||||
settings_page.visit
|
||||
settings_page.type_in_search("min personal message post length")
|
||||
expect(settings_page).to have_n_results(1)
|
||||
settings_page.clear_search
|
||||
expect(settings_page).to have_greater_than_n_results(1)
|
||||
end
|
||||
|
||||
it "can show only overridden settings" do
|
||||
overridden_setting_count = SiteSetting.all_settings(only_overridden: true).length
|
||||
settings_page.visit
|
||||
settings_page.toggle_only_show_overridden
|
||||
assert_selector(".admin-detail .row.setting.overridden", count: overridden_setting_count)
|
||||
settings_page.toggle_only_show_overridden
|
||||
expect(settings_page).to have_greater_than_n_results(overridden_setting_count)
|
||||
end
|
||||
|
||||
describe "when searching for keywords" do
|
||||
it "finds the associated site setting" do
|
||||
settings_page.visit
|
||||
|
@ -45,8 +45,26 @@ module PageObjects
|
||||
self
|
||||
end
|
||||
|
||||
def clear_search
|
||||
find("#setting-filter").click
|
||||
self
|
||||
end
|
||||
|
||||
def toggle_only_show_overridden
|
||||
find("#setting-filter-toggle-overridden").click
|
||||
self
|
||||
end
|
||||
|
||||
def has_search_result?(setting)
|
||||
page.has_selector?("div[data-setting='#{setting}']")
|
||||
has_css?("div[data-setting='#{setting}']")
|
||||
end
|
||||
|
||||
def has_n_results?(count)
|
||||
has_css?(".admin-detail .row.setting", count: count)
|
||||
end
|
||||
|
||||
def has_greater_than_n_results?(count)
|
||||
assert_selector(".admin-detail .row.setting", minimum: count)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
Loading…
Reference in New Issue
Block a user