discourse/app/assets/javascripts/admin/addon/components/site-setting.gjs
Martin Brennan 5b236ccc07
DEV: Move site setting name humanization to server side (#33311)
This commit moves the humanization of site setting names from the client
side to the server side, since this is becoming more widely used and
will be useful for specs to be able to access the same humanized name
formatting without duplicating the logic on the server.

There are only a couple of places in the admin UI that we send this
down in the JSON, the `site.sitSettings` object does not include these
humanized names as they are not needed.
2025-06-24 13:20:29 +10:00

423 lines
10 KiB
Text
Vendored

import { tracked } from "@glimmer/tracking";
import Component from "@ember/component";
import { hash } from "@ember/helper";
import { action } from "@ember/object";
import { dependentKeyCompat } from "@ember/object/compat";
import { getOwner } from "@ember/owner";
import { LinkTo } from "@ember/routing";
import { service } from "@ember/service";
import { htmlSafe } from "@ember/template";
import { isNone } from "@ember/utils";
import DButton from "discourse/components/d-button";
import JsonSchemaEditorModal from "discourse/components/modal/json-schema-editor";
import icon from "discourse/helpers/d-icon";
import { deepEqual } from "discourse/lib/object";
import { splitString } from "discourse/lib/utilities";
import { i18n } from "discourse-i18n";
import SettingValidationMessage from "admin/components/setting-validation-message";
import Description from "admin/components/site-settings/description";
import SiteSetting from "admin/models/site-setting";
const CUSTOM_TYPES = [
"bool",
"integer",
"enum",
"list",
"url_list",
"host_list",
"category_list",
"value_list",
"category",
"uploaded_image_list",
"compact_list",
"secret_list",
"upload",
"group_list",
"tag_list",
"tag_group_list",
"color",
"simple_list",
"emoji_list",
"named_list",
"file_size_restriction",
"file_types_list",
"font_list",
];
export default class SiteSettingComponent extends Component {
@service modal;
@service router;
@service dialog;
@service siteSettingChangeTracker;
@tracked setting = null;
@tracked isSecret = null;
updateExistingUsers = null;
// Classic component attributes
classNameBindings = [":row", ":setting", "overridden", "typeClass"];
attributeBindings = ["setting.setting:data-setting"];
constructor() {
super(...arguments);
this.isSecret = this.setting?.secret;
}
didInsertElement() {
super.didInsertElement(...arguments);
this.element.addEventListener("keydown", this._handleKeydown);
}
willDestroyElement() {
super.willDestroyElement(...arguments);
this.element.removeEventListener("keydown", this._handleKeydown);
}
@action
async _handleKeydown(event) {
if (
event.key === "Enter" &&
event.target.classList.contains("input-setting-string")
) {
await this.save();
}
}
get resolvedComponent() {
return getOwner(this).resolveRegistration(
`component:${this.componentName}`
);
}
@dependentKeyCompat
get buffered() {
return this.setting.buffered;
}
get componentName() {
return `site-settings/${this.typeClass}`;
}
get overridden() {
return this.setting.default !== this.buffered.get("value");
}
get displayDescription() {
return this.componentType !== "bool";
}
get dirty() {
let bufferVal = this.buffered.get("value");
let settingVal = this.setting?.value;
if (isNone(bufferVal)) {
bufferVal = "";
}
if (isNone(settingVal)) {
settingVal = "";
}
const dirty = !deepEqual(bufferVal, settingVal);
if (dirty) {
this.siteSettingChangeTracker.add(this.setting);
} else {
this.siteSettingChangeTracker.remove(this.setting);
}
return dirty;
}
get preview() {
const setting = this.setting;
const value = this.buffered.get("value");
const preview = setting.preview;
if (preview) {
const escapedValue = preview.replace(/\{\{value\}\}/g, value);
return htmlSafe(`<div class="preview">${escapedValue}</div>`);
}
return null;
}
get typeClass() {
const componentType = this.componentType;
return componentType.replace(/\_/g, "-");
}
get settingName() {
return this.setting.label || this.setting.humanized_name;
}
get componentType() {
const type = this.type;
return CUSTOM_TYPES.includes(type) ? type : "string";
}
get type() {
const setting = this.setting;
if (setting.type === "list" && setting.list_type) {
return `${setting.list_type}_list`;
}
return setting.type;
}
get allowAny() {
const anyValue = this.setting?.anyValue;
return anyValue !== false;
}
get bufferedValues() {
const value = this.buffered.get("value");
return splitString(value, "|");
}
get defaultValues() {
const value = this.setting?.defaultValues;
return splitString(value, "|");
}
get defaultIsAvailable() {
const defaultValues = this.defaultValues;
const bufferedValues = this.bufferedValues;
return (
defaultValues.length > 0 &&
!defaultValues.every((value) => bufferedValues.includes(value))
);
}
get settingEditButton() {
const setting = this.setting;
if (setting.json_schema) {
return {
action: () => {
this.modal.show(JsonSchemaEditorModal, {
model: {
updateValue: (value) => {
this.buffered.set("value", value);
},
value: this.buffered.get("value"),
settingName: setting.setting,
jsonSchema: setting.json_schema,
},
});
},
label: "admin.site_settings.json_schema.edit",
icon: "pencil",
};
} else if (setting.schema) {
return {
action: () => {
this.router.transitionTo("admin.schema", setting.setting);
},
label: "admin.site_settings.json_schema.edit",
icon: "pencil",
};
} else if (setting.objects_schema) {
return {
action: () => {
this.router.transitionTo(
"adminCustomizeThemes.show.schema",
setting.setting
);
},
label: "admin.customize.theme.edit_objects_theme_setting",
icon: "pencil",
};
}
return null;
}
get disableControls() {
return !!this.setting.isSaving;
}
get staffLogFilter() {
return this.setting.staffLogFilter;
}
@action
async update() {
if (this.setting.requiresConfirmation) {
const confirm = await this.siteSettingChangeTracker.confirmChanges(
this.setting
);
if (!confirm) {
return;
}
}
if (this.setting.affectsExistingUsers) {
await this.siteSettingChangeTracker.configureBackfill(this.setting);
}
await this.save();
}
@action
async save() {
try {
this.setting.isSaving = true;
await this._save();
this.setting.validationMessage = null;
this.buffered.applyChanges();
if (this.setting.requiresReload) {
this.siteSettingChangeTracker.refreshPage({
[this.setting.setting]: this.setting.value,
});
}
} catch (e) {
const json = e.jqXHR?.responseJSON;
if (json?.errors) {
let errorString = json.errors[0];
if (json.html_message) {
errorString = htmlSafe(errorString);
}
this.setting.validationMessage = errorString;
} else {
this.setting.validationMessage = i18n("generic_error");
}
} finally {
this.setting.isSaving = false;
}
}
@action
changeValueCallback(value) {
this.buffered.set("value", value);
}
@action
setValidationMessage(message) {
this.setting.validationMessage = message;
}
@action
cancel() {
this.buffered.discardChanges();
this.setting.validationMessage = null;
}
@action
resetDefault() {
this.buffered.set("value", this.setting.default);
this.setting.validationMessage = null;
}
@action
toggleSecret() {
this.isSecret = !this.isSecret;
}
@action
setDefaultValues() {
this.buffered.set(
"value",
this.bufferedValues.concat(this.defaultValues).uniq().join("|")
);
this.setting.validationMessage = null;
}
_save() {
const setting = this.buffered;
return SiteSetting.update(setting.get("setting"), setting.get("value"), {
updateExistingUsers: this.setting.updateExistingUsers,
});
}
<template>
<div class="setting-label">
<h3>
{{this.settingName}}
{{#if this.staffLogFilter}}
<LinkTo
@route="adminLogs.staffActionLogs"
@query={{hash filters=this.staffLogFilter force_refresh=true}}
title={{i18n "admin.settings.history"}}
>
<span class="history-icon">
{{icon "clock-rotate-left"}}
</span>
</LinkTo>
{{/if}}
</h3>
{{#if this.defaultIsAvailable}}
<DButton
class="btn-link"
@action={{this.setDefaultValues}}
@translatedLabel={{this.setting.setDefaultValuesLabel}}
/>
{{/if}}
</div>
<div class="setting-value">
{{#if this.settingEditButton}}
<DButton
@action={{this.settingEditButton.action}}
@icon={{this.settingEditButton.icon}}
@label={{this.settingEditButton.label}}
class="setting-value-edit-button"
/>
<Description @description={{this.setting.description}} />
{{else}}
<this.resolvedComponent
@setting={{this.setting}}
@value={{this.buffered.value}}
@preview={{this.preview}}
@isSecret={{this.isSecret}}
@allowAny={{this.allowAny}}
@changeValueCallback={{this.changeValueCallback}}
@setValidationMessage={{this.setValidationMessage}}
/>
<SettingValidationMessage @message={{this.setting.validationMessage}} />
{{#if this.displayDescription}}
<Description @description={{this.setting.description}} />
{{/if}}
{{/if}}
</div>
{{#if this.dirty}}
<div class="setting-controls">
<DButton
@action={{this.update}}
@icon="check"
@isLoading={{this.disableControls}}
@ariaLabel="admin.settings.save"
class="ok setting-controls__ok"
/>
<DButton
@action={{this.cancel}}
@icon="xmark"
@isLoading={{this.disableControls}}
@ariaLabel="admin.settings.cancel"
class="cancel setting-controls__cancel"
/>
</div>
{{else if this.overridden}}
{{#if this.setting.secret}}
<DButton
@action={{this.toggleSecret}}
@icon="far-eye-slash"
@ariaLabel="admin.settings.unmask"
class="setting-toggle-secret"
/>
{{/if}}
<DButton
class="btn-default undo setting-controls__undo"
@action={{this.resetDefault}}
@icon="arrow-rotate-left"
@label="admin.settings.reset"
/>
{{/if}}
</template>
}