mirror of
https://github.com/discourse/discourse.git
synced 2024-11-22 11:44:49 +08:00
FEATURE: Introduce site settings which require confirmation (#27315)
Many site settings can be distructive or have huge side-effects for a site that the admin may not be aware of when changing it. This commit introduces a `requires_confirmation` attribute that can be added to any site setting. When it is true, a confirmation dialog will open if that setting is changed in the admin UI, optionally with a custom message that is defined in client.en.yml. If the admin does not confirm, we reset the setting to its previous clean value and do not save the new value.
This commit is contained in:
parent
3ff7ce78e7
commit
83361b2fc5
|
@ -5,9 +5,11 @@ import Mixin from "@ember/object/mixin";
|
||||||
import { service } from "@ember/service";
|
import { service } from "@ember/service";
|
||||||
import { htmlSafe } from "@ember/template";
|
import { htmlSafe } from "@ember/template";
|
||||||
import { isNone } from "@ember/utils";
|
import { isNone } from "@ember/utils";
|
||||||
|
import { Promise } from "rsvp";
|
||||||
import JsonSchemaEditorModal from "discourse/components/modal/json-schema-editor";
|
import JsonSchemaEditorModal from "discourse/components/modal/json-schema-editor";
|
||||||
import { ajax } from "discourse/lib/ajax";
|
import { ajax } from "discourse/lib/ajax";
|
||||||
import { fmt, propertyNotEqual } from "discourse/lib/computed";
|
import { fmt, propertyNotEqual } from "discourse/lib/computed";
|
||||||
|
import { SITE_SETTING_REQUIRES_CONFIRMATION_TYPES } from "discourse/lib/constants";
|
||||||
import { splitString } from "discourse/lib/utilities";
|
import { splitString } from "discourse/lib/utilities";
|
||||||
import { deepEqual } from "discourse-common/lib/object";
|
import { deepEqual } from "discourse-common/lib/object";
|
||||||
import discourseComputed, { bind } from "discourse-common/utils/decorators";
|
import discourseComputed, { bind } from "discourse-common/utils/decorators";
|
||||||
|
@ -82,6 +84,7 @@ export default Mixin.create({
|
||||||
modal: service(),
|
modal: service(),
|
||||||
router: service(),
|
router: service(),
|
||||||
site: service(),
|
site: service(),
|
||||||
|
dialog: service(),
|
||||||
attributeBindings: ["setting.setting:data-setting"],
|
attributeBindings: ["setting.setting:data-setting"],
|
||||||
classNameBindings: [":row", ":setting", "overridden", "typeClass"],
|
classNameBindings: [":row", ":setting", "overridden", "typeClass"],
|
||||||
validationMessage: null,
|
validationMessage: null,
|
||||||
|
@ -204,10 +207,59 @@ export default Mixin.create({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
confirmChanges(settingKey) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
// Fallback is needed in case the setting does not have a custom confirmation
|
||||||
|
// prompt/confirm defined.
|
||||||
|
this.dialog.alert({
|
||||||
|
message: I18n.t(
|
||||||
|
`admin.site_settings.requires_confirmation_messages.${settingKey}.prompt`,
|
||||||
|
{
|
||||||
|
translatedFallback: I18n.t(
|
||||||
|
"admin.site_settings.requires_confirmation_messages.default.prompt"
|
||||||
|
),
|
||||||
|
}
|
||||||
|
),
|
||||||
|
buttons: [
|
||||||
|
{
|
||||||
|
label: I18n.t(
|
||||||
|
`admin.site_settings.requires_confirmation_messages.${settingKey}.confirm`,
|
||||||
|
{
|
||||||
|
translatedFallback: I18n.t(
|
||||||
|
"admin.site_settings.requires_confirmation_messages.default.confirm"
|
||||||
|
),
|
||||||
|
}
|
||||||
|
),
|
||||||
|
class: "btn-primary",
|
||||||
|
action: () => resolve(true),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: I18n.t("no_value"),
|
||||||
|
class: "btn-default",
|
||||||
|
action: () => resolve(false),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
@action
|
@action
|
||||||
async update() {
|
async update() {
|
||||||
const key = this.buffered.get("setting");
|
const key = this.buffered.get("setting");
|
||||||
|
|
||||||
|
let confirm = true;
|
||||||
|
if (
|
||||||
|
this.buffered.get("requires_confirmation") ===
|
||||||
|
SITE_SETTING_REQUIRES_CONFIRMATION_TYPES.simple
|
||||||
|
) {
|
||||||
|
confirm = await this.confirmChanges(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!confirm) {
|
||||||
|
this.cancel();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!DEFAULT_USER_PREFERENCES.includes(key)) {
|
if (!DEFAULT_USER_PREFERENCES.includes(key)) {
|
||||||
await this.save();
|
await this.save();
|
||||||
return;
|
return;
|
||||||
|
|
|
@ -84,9 +84,14 @@ export const TOPIC_VISIBILITY_REASONS = {
|
||||||
export const SYSTEM_FLAG_IDS = {
|
export const SYSTEM_FLAG_IDS = {
|
||||||
like: 2,
|
like: 2,
|
||||||
notify_user: 6,
|
notify_user: 6,
|
||||||
notify_moderators: 7,
|
|
||||||
off_topic: 3,
|
off_topic: 3,
|
||||||
inappropriate: 4,
|
inappropriate: 4,
|
||||||
spam: 8,
|
spam: 8,
|
||||||
illegal: 10,
|
illegal: 10,
|
||||||
|
notify_moderators: 7,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SITE_SETTING_REQUIRES_CONFIRMATION_TYPES = {
|
||||||
|
simple: "simple",
|
||||||
|
user_option: "user_option",
|
||||||
};
|
};
|
||||||
|
|
|
@ -6795,6 +6795,22 @@ en:
|
||||||
add_types_title: "Allow extensions %{types}"
|
add_types_title: "Allow extensions %{types}"
|
||||||
add_types_toast: "%{types} file types added"
|
add_types_toast: "%{types} file types added"
|
||||||
mandatory_group: "Group is mandatory"
|
mandatory_group: "Group is mandatory"
|
||||||
|
requires_confirmation_messages:
|
||||||
|
default:
|
||||||
|
prompt: "Changing this setting may have far-reaching or unintended consequences for your site. Are you sure you want to proceed?"
|
||||||
|
confirm: "Yes, I'm sure"
|
||||||
|
min_password_length:
|
||||||
|
prompt: "You’re about to change your password policy. This will affect all users changing their passwords from now on. Are you sure you want to proceed?"
|
||||||
|
confirm: "Yes, update password policy"
|
||||||
|
min_admin_password_length:
|
||||||
|
prompt: "You’re about to change your password policy. This will affect all admins changing their passwords from now on. Are you sure you want to proceed?"
|
||||||
|
confirm: "Yes, update password policy"
|
||||||
|
password_unique_charactes:
|
||||||
|
prompt: "You’re about to change your password policy. This will affect all users changing their passwords from now on. Are you sure you want to proceed?"
|
||||||
|
confirm: "Yes, update password policy"
|
||||||
|
block_common_passwords:
|
||||||
|
prompt: "You’re about to change your password policy. This will affect all users changing their passwords from now on. Are you sure you want to proceed?"
|
||||||
|
confirm: "Yes, update password policy"
|
||||||
|
|
||||||
badges:
|
badges:
|
||||||
title: Badges
|
title: Badges
|
||||||
|
|
|
@ -630,16 +630,21 @@ users:
|
||||||
default: 10
|
default: 10
|
||||||
min: 8
|
min: 8
|
||||||
max: 500
|
max: 500
|
||||||
|
requires_confirmation: "simple"
|
||||||
min_admin_password_length:
|
min_admin_password_length:
|
||||||
client: true
|
client: true
|
||||||
default: 15
|
default: 15
|
||||||
min: 8
|
min: 8
|
||||||
max: 500
|
max: 500
|
||||||
|
requires_confirmation: "simple"
|
||||||
password_unique_characters:
|
password_unique_characters:
|
||||||
default: 6
|
default: 6
|
||||||
min: 1
|
min: 1
|
||||||
max: 10
|
max: 10
|
||||||
block_common_passwords: true
|
requires_confirmation: "simple"
|
||||||
|
block_common_passwords:
|
||||||
|
default: true
|
||||||
|
requires_confirmation: "simple"
|
||||||
username_change_period: 3
|
username_change_period: 3
|
||||||
email_editable:
|
email_editable:
|
||||||
client: true
|
client: true
|
||||||
|
|
|
@ -95,6 +95,10 @@ module SiteSettingExtension
|
||||||
@shadowed_settings ||= []
|
@shadowed_settings ||= []
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def requires_confirmation_settings
|
||||||
|
@requires_confirmation_settings ||= {}
|
||||||
|
end
|
||||||
|
|
||||||
def hidden_settings_provider
|
def hidden_settings_provider
|
||||||
@hidden_settings_provider ||= SiteSettings::HiddenProvider.new
|
@hidden_settings_provider ||= SiteSettings::HiddenProvider.new
|
||||||
end
|
end
|
||||||
|
@ -244,6 +248,7 @@ module SiteSettingExtension
|
||||||
secret: secret_settings.include?(s),
|
secret: secret_settings.include?(s),
|
||||||
placeholder: placeholder(s),
|
placeholder: placeholder(s),
|
||||||
mandatory_values: mandatory_values[s],
|
mandatory_values: mandatory_values[s],
|
||||||
|
requires_confirmation: requires_confirmation_settings[s],
|
||||||
}.merge!(type_hash)
|
}.merge!(type_hash)
|
||||||
|
|
||||||
opts[:plugin] = plugins[s] if plugins[s]
|
opts[:plugin] = plugins[s] if plugins[s]
|
||||||
|
@ -654,6 +659,14 @@ module SiteSettingExtension
|
||||||
|
|
||||||
mandatory_values[name] = opts[:mandatory_values] if opts[:mandatory_values]
|
mandatory_values[name] = opts[:mandatory_values] if opts[:mandatory_values]
|
||||||
|
|
||||||
|
requires_confirmation_settings[name] = (
|
||||||
|
if SiteSettings::TypeSupervisor::REQUIRES_CONFIRMATION_TYPES.values.include?(
|
||||||
|
opts[:requires_confirmation],
|
||||||
|
)
|
||||||
|
opts[:requires_confirmation]
|
||||||
|
end
|
||||||
|
)
|
||||||
|
|
||||||
categories[name] = opts[:category] || :uncategorized
|
categories[name] = opts[:category] || :uncategorized
|
||||||
|
|
||||||
hidden_settings_provider.add_hidden(name) if opts[:hidden]
|
hidden_settings_provider.add_hidden(name) if opts[:hidden]
|
||||||
|
|
|
@ -20,12 +20,15 @@ class SiteSettings::TypeSupervisor
|
||||||
list_type
|
list_type
|
||||||
textarea
|
textarea
|
||||||
json_schema
|
json_schema
|
||||||
|
requires_confirmation
|
||||||
].freeze
|
].freeze
|
||||||
VALIDATOR_OPTS = %i[min max regex hidden regex_error json_schema].freeze
|
VALIDATOR_OPTS = %i[min max regex hidden regex_error json_schema].freeze
|
||||||
|
|
||||||
# For plugins, so they can tell if a feature is supported
|
# For plugins, so they can tell if a feature is supported
|
||||||
SUPPORTED_TYPES = %i[email username list enum].freeze
|
SUPPORTED_TYPES = %i[email username list enum].freeze
|
||||||
|
|
||||||
|
REQUIRES_CONFIRMATION_TYPES = { simple: "simple", user_option: "user_option" }.freeze
|
||||||
|
|
||||||
def self.types
|
def self.types
|
||||||
@types ||=
|
@types ||=
|
||||||
Enum.new(
|
Enum.new(
|
||||||
|
|
|
@ -166,6 +166,8 @@ task "javascript:update_constants" => :environment do
|
||||||
export const TOPIC_VISIBILITY_REASONS = #{Topic.visibility_reasons.to_json};
|
export const TOPIC_VISIBILITY_REASONS = #{Topic.visibility_reasons.to_json};
|
||||||
|
|
||||||
export const SYSTEM_FLAG_IDS = #{PostActionType.types.to_json}
|
export const SYSTEM_FLAG_IDS = #{PostActionType.types.to_json}
|
||||||
|
|
||||||
|
export const SITE_SETTING_REQUIRES_CONFIRMATION_TYPES = #{SiteSettings::TypeSupervisor::REQUIRES_CONFIRMATION_TYPES.to_json}
|
||||||
JS
|
JS
|
||||||
|
|
||||||
pretty_notifications = Notification.types.map { |n| " #{n[0]}: #{n[1]}," }.join("\n")
|
pretty_notifications = Notification.types.map { |n| " #{n[0]}: #{n[1]}," }.join("\n")
|
||||||
|
|
|
@ -892,6 +892,24 @@ RSpec.describe SiteSettingExtension do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe "requires_confirmation settings" do
|
||||||
|
it "returns 'simple' for settings that require confirmation with 'simple' type" do
|
||||||
|
expect(
|
||||||
|
SiteSetting.all_settings.find { |s| s[:setting] == :min_password_length }[
|
||||||
|
:requires_confirmation
|
||||||
|
],
|
||||||
|
).to eq("simple")
|
||||||
|
end
|
||||||
|
|
||||||
|
it "returns nil for settings that do not require confirmation" do
|
||||||
|
expect(
|
||||||
|
SiteSetting.all_settings.find { |s| s[:setting] == :display_local_time_in_user_card }[
|
||||||
|
:requires_confirmation
|
||||||
|
],
|
||||||
|
).to eq(nil)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe "_map extension for list settings" do
|
describe "_map extension for list settings" do
|
||||||
it "handles splitting group_list settings" do
|
it "handles splitting group_list settings" do
|
||||||
SiteSetting.personal_message_enabled_groups = "1|2"
|
SiteSetting.personal_message_enabled_groups = "1|2"
|
||||||
|
|
40
spec/system/admin_site_setting_requires_confirmation_spec.rb
Normal file
40
spec/system/admin_site_setting_requires_confirmation_spec.rb
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
describe "Admin Site Setting Requires Confirmation", type: :system do
|
||||||
|
let(:settings_page) { PageObjects::Pages::AdminSiteSettings.new }
|
||||||
|
let(:dialog) { PageObjects::Components::Dialog.new }
|
||||||
|
fab!(:admin)
|
||||||
|
|
||||||
|
before do
|
||||||
|
SiteSetting.min_password_length = 10
|
||||||
|
sign_in(admin)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "requires confirmation and shows the correct message" do
|
||||||
|
settings_page.visit("min_password_length")
|
||||||
|
settings_page.change_number_setting("min_password_length", 12)
|
||||||
|
expect(dialog).to be_open
|
||||||
|
expect(dialog).to have_content(
|
||||||
|
I18n.t(
|
||||||
|
"admin_js.admin.site_settings.requires_confirmation_messages.min_password_length.prompt",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
expect(dialog).to have_content(
|
||||||
|
I18n.t(
|
||||||
|
"admin_js.admin.site_settings.requires_confirmation_messages.min_password_length.confirm",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
dialog.click_yes
|
||||||
|
expect(dialog).to be_closed
|
||||||
|
expect(SiteSetting.min_password_length).to eq(12)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "does not save the new setting value if the admin cancels confirmation" do
|
||||||
|
settings_page.visit("min_password_length")
|
||||||
|
settings_page.change_number_setting("min_password_length", 12)
|
||||||
|
expect(dialog).to be_open
|
||||||
|
dialog.click_no
|
||||||
|
expect(dialog).to be_closed
|
||||||
|
expect(SiteSetting.min_password_length).to eq(10)
|
||||||
|
end
|
||||||
|
end
|
|
@ -1,7 +1,7 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
describe "Admin Site Setting Search", type: :system do
|
describe "Admin Site Setting Search", type: :system do
|
||||||
let(:settings_page) { PageObjects::Pages::AdminSettings.new }
|
let(:settings_page) { PageObjects::Pages::AdminSiteSettings.new }
|
||||||
fab!(:admin)
|
fab!(:admin)
|
||||||
|
|
||||||
before do
|
before do
|
||||||
|
|
|
@ -10,7 +10,7 @@ describe "Emoji deny list", type: :system do
|
||||||
|
|
||||||
describe "when editing admin settings" do
|
describe "when editing admin settings" do
|
||||||
before { SiteSetting.emoji_deny_list = "" }
|
before { SiteSetting.emoji_deny_list = "" }
|
||||||
let(:site_settings_page) { PageObjects::Pages::AdminSettings.new }
|
let(:site_settings_page) { PageObjects::Pages::AdminSiteSettings.new }
|
||||||
|
|
||||||
skip "should allow admin to update emoji deny list" do
|
skip "should allow admin to update emoji deny list" do
|
||||||
site_settings_page.visit_category("posting")
|
site_settings_page.visit_category("posting")
|
||||||
|
|
|
@ -2,14 +2,18 @@
|
||||||
|
|
||||||
module PageObjects
|
module PageObjects
|
||||||
module Pages
|
module Pages
|
||||||
class AdminSettings < PageObjects::Pages::Base
|
class AdminSiteSettings < PageObjects::Pages::Base
|
||||||
def visit_filtered_plugin_setting(filter)
|
def visit_filtered_plugin_setting(filter)
|
||||||
page.visit("/admin/site_settings/category/plugins?filter=#{filter}")
|
page.visit("/admin/site_settings/category/plugins?filter=#{filter}")
|
||||||
self
|
self
|
||||||
end
|
end
|
||||||
|
|
||||||
def visit
|
def visit(filter = nil)
|
||||||
|
if filter.present?
|
||||||
|
page.visit("/admin/site_settings?filter=#{filter}")
|
||||||
|
else
|
||||||
page.visit("/admin/site_settings")
|
page.visit("/admin/site_settings")
|
||||||
|
end
|
||||||
self
|
self
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -18,17 +22,31 @@ module PageObjects
|
||||||
self
|
self
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def find_setting(setting_name)
|
||||||
|
find(".admin-detail .row.setting[data-setting='#{setting_name}']")
|
||||||
|
end
|
||||||
|
|
||||||
def toggle_setting(setting_name, text = "")
|
def toggle_setting(setting_name, text = "")
|
||||||
setting = find(".admin-detail .row.setting[data-setting='#{setting_name}']")
|
setting = find_setting(setting_name)
|
||||||
setting.find(".setting-value span", text: text).click
|
setting.find(".setting-value span", text: text).click
|
||||||
setting.find(".setting-controls button.ok").click
|
save_setting(setting)
|
||||||
|
end
|
||||||
|
|
||||||
|
def change_number_setting(setting_name, value, save_changes = true)
|
||||||
|
setting = find_setting(setting_name)
|
||||||
|
setting.fill_in(with: value)
|
||||||
|
save_setting(setting) if save_changes
|
||||||
end
|
end
|
||||||
|
|
||||||
def select_from_emoji_list(setting_name, text = "", save_changes = true)
|
def select_from_emoji_list(setting_name, text = "", save_changes = true)
|
||||||
setting = find(".admin-detail .row.setting[data-setting='#{setting_name}']")
|
setting = find(".admin-detail .row.setting[data-setting='#{setting_name}']")
|
||||||
setting.find(".setting-value .value-list > .value button").click
|
setting.find(".setting-value .value-list > .value button").click
|
||||||
setting.find(".setting-value .emoji-picker .emoji[title='#{text}']").click
|
setting.find(".setting-value .emoji-picker .emoji[title='#{text}']").click
|
||||||
setting.find(".setting-controls button.ok").click if save_changes
|
save_setting(setting) if save_changes
|
||||||
|
end
|
||||||
|
|
||||||
|
def save_setting(setting_element)
|
||||||
|
setting_element.find(".setting-controls button.ok").click
|
||||||
end
|
end
|
||||||
|
|
||||||
def values_in_list(setting_name)
|
def values_in_list(setting_name)
|
||||||
|
@ -67,5 +85,10 @@ module PageObjects
|
||||||
assert_selector(".admin-detail .row.setting", minimum: count)
|
assert_selector(".admin-detail .row.setting", minimum: count)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# TODO (martin) Remove this after discourse-topic-voting no longer
|
||||||
|
# relies on this, it was renamed to AdminSiteSettings.
|
||||||
|
class AdminSettings < PageObjects::Pages::AdminSiteSettings
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
Loading…
Reference in New Issue
Block a user