mirror of
https://gh.wpcy.net/https://github.com/discourse/discourse.git
synced 2026-05-21 19:12:39 +08:00
This commit replaces several instances of `.uniq`, `.uniqBy`, and related array deduplication methods with a new utility function `uniqueItemsFromArray`. This change ensures proper deduplication logic across the codebase while addressing the deprecation issues. **Main Changes:** * Created new utility function `uniqueItemsFromArray`: Located in a new file, `array-tools.js`, this function provides a reusable and configurable alternative for deduplication. * Replaced old methods: Updated multiple files to use the new utility function instead of deprecated or custom implementations for deduplication. * Replaced manual deduplication: Replace uses of `[...new Set(array)]` to standardize the deduplication logic across the codebase. * Added unit tests: Introduced thorough test coverage (`array-tools-test.js`) to validate functionality and edge cases for the utility function. * Updated deprecation workflow: Added logging for deprecated uniq and uniqBy methods to the `deprecation-workflow.js`. This change is primarily focused on code quality improvements, ensuring future-proof deduplication, and maintaining alignment with deprecation guidelines.
523 lines
13 KiB
Text
Vendored
523 lines
13 KiB
Text
Vendored
import Component from "@glimmer/component";
|
|
import { tracked } from "@glimmer/tracking";
|
|
import { hash } from "@ember/helper";
|
|
import { on } from "@ember/modifier";
|
|
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 { and } from "truth-helpers";
|
|
import DButton from "discourse/components/d-button";
|
|
import JsonSchemaEditorModal from "discourse/components/modal/json-schema-editor";
|
|
import basePath from "discourse/helpers/base-path";
|
|
import icon from "discourse/helpers/d-icon";
|
|
import { uniqueItemsFromArray } from "discourse/lib/array-tools";
|
|
import { bind } from "discourse/lib/decorators";
|
|
import { deepEqual } from "discourse/lib/object";
|
|
import { sanitize } from "discourse/lib/text";
|
|
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 JobStatus from "admin/components/site-settings/job-status";
|
|
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",
|
|
"locale_list",
|
|
"locale_enum",
|
|
];
|
|
|
|
export default class SiteSettingComponent extends Component {
|
|
@service modal;
|
|
@service router;
|
|
@service siteSettingChangeTracker;
|
|
@service messageBus;
|
|
@service site;
|
|
|
|
@tracked isSecret = null;
|
|
@tracked status = null;
|
|
@tracked progress = null;
|
|
updateExistingUsers = null;
|
|
|
|
constructor() {
|
|
super(...arguments);
|
|
this.isSecret = this.setting?.secret;
|
|
|
|
if (this.canSubscribeToSettingsJobs) {
|
|
this.messageBus.subscribe(
|
|
`/site_setting/${this.setting.setting}/process`,
|
|
this.onMessage
|
|
);
|
|
}
|
|
}
|
|
|
|
willDestroy() {
|
|
super.willDestroy(...arguments);
|
|
|
|
if (this.canSubscribeToSettingsJobs) {
|
|
this.messageBus.unsubscribe(
|
|
`/site_setting/${this.setting.setting}/process`,
|
|
this.onMessage
|
|
);
|
|
}
|
|
}
|
|
|
|
canSubscribeToSettingsJobs() {
|
|
const settingName = this.setting.setting;
|
|
return (
|
|
settingName.includes("default_categories") ||
|
|
settingName.includes("default_tags")
|
|
);
|
|
}
|
|
|
|
get defaultTheme() {
|
|
return this.site.user_themes.find((theme) => theme.default);
|
|
}
|
|
|
|
@bind
|
|
async onMessage(membership) {
|
|
this.status = membership.status;
|
|
this.progress = membership.progress;
|
|
}
|
|
|
|
@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.#valuesEqual(this.setting.default, this.buffered.get("value"));
|
|
}
|
|
|
|
get displayDescription() {
|
|
return this.componentType !== "bool";
|
|
}
|
|
|
|
get showThemeSiteSettingWarning() {
|
|
return this.setting.themeable;
|
|
}
|
|
|
|
get themeSiteSettingWarningText() {
|
|
return htmlSafe(
|
|
i18n("admin.theme_site_settings.site_setting_warning", {
|
|
basePath,
|
|
defaultThemeName: sanitize(this.defaultTheme.name),
|
|
defaultThemeId: this.defaultTheme.theme_id,
|
|
})
|
|
);
|
|
}
|
|
|
|
get dirty() {
|
|
let bufferVal = this.buffered.get("value");
|
|
let settingVal = this.setting?.value;
|
|
|
|
if (isNone(bufferVal)) {
|
|
bufferVal = "";
|
|
}
|
|
|
|
if (isNone(settingVal)) {
|
|
settingVal = "";
|
|
}
|
|
|
|
const dirty = !this.#valuesEqual(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 setting() {
|
|
return this.args.setting;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
get canUpdate() {
|
|
if (this.setting.themeable) {
|
|
return false;
|
|
}
|
|
|
|
if (!this.status || this.status === "completed") {
|
|
return true;
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
@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 {
|
|
// eslint-disable-next-line no-console
|
|
console.error(e);
|
|
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",
|
|
uniqueItemsFromArray(this.bufferedValues.concat(this.defaultValues)).join(
|
|
"|"
|
|
)
|
|
);
|
|
this.setting.validationMessage = null;
|
|
}
|
|
|
|
_save() {
|
|
const setting = this.buffered;
|
|
return SiteSetting.update(setting.get("setting"), setting.get("value"), {
|
|
updateExistingUsers: this.setting.updateExistingUsers,
|
|
});
|
|
}
|
|
|
|
#valuesEqual(a, b) {
|
|
if (
|
|
this.setting.json_schema ||
|
|
this.setting.schema ||
|
|
this.setting.objects_schema
|
|
) {
|
|
return deepEqual(a, b);
|
|
} else {
|
|
return a?.toString() === b?.toString();
|
|
}
|
|
}
|
|
|
|
<template>
|
|
<div
|
|
data-setting={{this.setting.setting}}
|
|
class="row setting {{this.typeClass}} {{if this.overridden 'overridden'}}"
|
|
...attributes
|
|
>
|
|
<div class="setting-label">
|
|
<h3>
|
|
{{this.settingName}}
|
|
|
|
{{#if this.staffLogFilter}}
|
|
<LinkTo
|
|
@route="adminLogs.staffActionLogs"
|
|
@query={{hash filters=this.staffLogFilter force_refresh=true}}
|
|
class="staff-action-log-link"
|
|
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}} />
|
|
<JobStatus @status={{this.status}} @progress={{this.progress}} />
|
|
{{else}}
|
|
<this.resolvedComponent
|
|
{{on "keydown" this._handleKeydown}}
|
|
@disabled={{this.setting.themeable}}
|
|
@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}} />
|
|
<JobStatus @status={{this.status}} @progress={{this.progress}} />
|
|
{{/if}}
|
|
{{#if this.showThemeSiteSettingWarning}}
|
|
<div class="setting-theme-warning">
|
|
<p class="setting-theme-warning__text">
|
|
{{icon "paintbrush"}}
|
|
{{this.themeSiteSettingWarningText}}
|
|
</p>
|
|
</div>
|
|
{{/if}}
|
|
{{/if}}
|
|
</div>
|
|
|
|
{{#if (and this.dirty this.canUpdate)}}
|
|
<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 (and this.overridden this.canUpdate)}}
|
|
{{#if this.setting.secret}}
|
|
<DButton
|
|
@action={{this.toggleSecret}}
|
|
@icon={{if this.isSecret "far-eye" "far-eye-slash"}}
|
|
@ariaLabel="admin.settings.unmask"
|
|
class="btn-default setting-toggle-secret"
|
|
/>
|
|
{{/if}}
|
|
|
|
<DButton
|
|
class="btn-default undo setting-controls__undo"
|
|
@action={{this.resetDefault}}
|
|
@icon="arrow-rotate-left"
|
|
@label="admin.settings.reset"
|
|
/>
|
|
{{/if}}
|
|
</div>
|
|
</template>
|
|
}
|