discourse/spec/system/page_objects/pages/admin_site_settings.rb
Régis HANOL d56fca0fd7
FEATURE: Make can_permanently_delete visible with strong safeguards (#39179)
The `can_permanently_delete` site setting was hidden and could only be
enabled via the Rails console. There was no indication in the admin UI
that this capability existed, leading to confusion and wasted time for
admins trying to permanently delete content.

This commit unhides the setting and adds layered safeguards at every
level of the permanent deletion flow:

**Site setting visibility:**
- Remove `hidden: true` so the setting appears in the admin UI
- Add a `requires_confirmation` dialog when enabling (not when
disabling) via a new `simple_on_enable` confirmation type
- Add a proper setting description since it was missing

**Type-to-confirm on all permanent delete actions:**
- Replace the weak yes/no dialogs on post and revision permanent
deletion with a type-to-confirm pattern (type "permanently delete")
- Show context-aware titles and messages (post vs topic, with post count
for topics)
- Reusable `PermanentlyDeleteConfirm` dialog body component following
the `SecondFactorConfirmPhrase` pattern

**Server-side pre-check endpoint:**
- Add `GET /posts/:id/permanently_delete_check` (admin-only via
`AdminConstraint`) that uses the guardian to validate whether permanent
deletion is allowed before showing the confirmation dialog
- Returns the reason when denied (cooldown timer, undeleted posts) so
the admin gets immediate feedback instead of going through the full
confirmation flow only to be rejected
- Returns accurate `post_count` for topic deletion messages

**Refactors:**
- Extract `Topic#deletable_posts_count` to share the post counting query
between the guardian, `cannot_permanently_delete_reason`, and the new
endpoint
- Use exclusion (`NOT small_action`) instead of inclusion for post type
filtering, so plugin-added post types are counted correctly

Ref - t/181345

---------

Co-authored-by: Martin Brennan <martin@discourse.org>
2026-04-16 09:01:35 +02:00

199 lines
6.5 KiB
Ruby

# frozen_string_literal: true
module PageObjects
module Pages
class AdminSiteSettings < PageObjects::Pages::Base
def visit_filtered_plugin_setting(filter)
page.visit("/admin/site_settings/category/plugins?filter=#{filter}")
self
end
def visit(filter = nil)
if filter.present?
page.visit("/admin/site_settings?filter=#{filter}")
else
page.visit("/admin/site_settings")
end
self
end
def visit_category(category)
page.visit("/admin/site_settings/category/#{category}")
self
end
def navigate_to_category(category)
page.find("a.#{category}").click
self
end
def setting_row_selector(setting_name)
".row.setting[data-setting='#{setting_name}']"
end
def select_list_values(setting_name, values)
setting =
PageObjects::Components::SelectKit.new(
".row.setting[data-setting='#{setting_name}'] .list-setting",
)
setting.expand
values.each { |value| setting.select_row_by_value(value) }
self
end
def select_enum_value(setting_name, value)
setting =
PageObjects::Components::SelectKit.new(
".row.setting[data-setting='#{setting_name}'] .single-select",
)
setting.expand
setting.select_row_by_value(value)
self
end
def has_setting?(setting_name)
has_css?(".row.setting[data-setting=\"#{setting_name}\"]")
end
def find_setting(setting_name, overridden: false)
find(
".admin-detail #{setting_row_selector(setting_name)}#{overridden ? ".overridden" : ""}",
)
end
def fill_setting(setting_name, value)
setting = find_setting(setting_name)
setting.fill_in(with: value)
end
def toggle_setting(setting_name, text = "")
setting = find_setting(setting_name)
setting.find(".setting-value span", text: text).click
save_setting(setting)
end
def toggle_bool_setting(setting_name)
setting = find_setting(setting_name)
setting.find(".setting-value input[type='checkbox']").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
def select_from_emoji_list(setting_name, text = "", save_changes = true)
setting = find(".admin-detail .row.setting[data-setting='#{setting_name}']")
setting.find(".setting-value .value-list > .value button").click
find(".emoji-picker .emoji[title='#{text}']").click
save_setting(setting) if save_changes
end
def save_setting(setting)
setting = find_setting(setting) if setting.is_a?(String)
setting.find(".setting-controls button.ok").click
self
end
def has_overridden_setting?(setting_name, value: nil)
setting_field = find_setting(setting_name, overridden: true)
return setting_field.find(".setting-value input").value == value.to_s if value
true
end
def has_overridden_topic_setting?(setting_name, value: nil)
setting_field = find_setting(setting_name, overridden: true)
return setting_field.find(".selected-name")["data-value"] == value.to_s if value
true
end
def has_no_overridden_setting?(setting_name)
find_setting(setting_name, overridden: false)
end
def values_in_list(setting_name)
vals = []
setting = find(".admin-detail .row.setting[data-setting='#{setting_name}']")
setting
.all(:css, ".setting-value .values .value .value-input span")
.map { |e| vals << e.text }
vals
end
def type_in_search(input)
find("input#setting-filter").send_keys(input)
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)
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
def error_message(setting_name)
setting = find_setting(setting_name)
setting.find(".setting-value .validation-error").text
end
def has_theme_warning?(setting_name, theme_name, theme_id)
find_setting(setting_name).find(".setting-theme-warning__text").has_text?(theme_name) &&
find_setting(setting_name).find(".setting-theme-warning__text").has_link?(
href: "/admin/customize/themes/#{theme_id}",
)
end
def has_upcoming_change_default_warning?(setting_name, old_default:, new_default:)
find_setting(setting_name).find(".setting-upcoming-change-warning__text").has_text?(
"The default for this setting has changed from #{old_default} to #{new_default}",
) &&
find_setting(setting_name).find(".setting-upcoming-change-warning__text").has_link?(
href: "/admin/config/upcoming-changes?changeNamesFilter=enable_upload_debug_mode",
)
end
def has_disabled_input?(setting_name)
find_setting(setting_name).has_css?("input[disabled]")
end
def has_visible_reorder_buttons?(setting_name)
has_css?("#{setting_row_selector(setting_name)} .shift-up-value-btn", visible: :visible) &&
has_css?("#{setting_row_selector(setting_name)} .shift-down-value-btn", visible: :visible)
end
def has_hidden_reorder_buttons?(setting_name)
has_css?("#{setting_row_selector(setting_name)} .shift-up-value-btn", visible: :hidden) &&
has_css?("#{setting_row_selector(setting_name)} .shift-down-value-btn", visible: :hidden)
end
def tag_list_setting(setting_name)
PageObjects::Components::SelectKit.new("#{setting_row_selector(setting_name)} .tag-chooser")
end
def has_tags_in_setting?(setting_name, tags)
tag_chooser = tag_list_setting(setting_name)
tag_names = tags.map(&:name).sort
selected_names = tag_chooser.value&.split(",")&.sort || []
tag_names == selected_names
end
end
end
end