Change settings area to be separated into categories in admin UI (#19407)

And update all descriptions
This commit is contained in:
Eugen Rochko 2022-10-22 11:44:41 +02:00 committed by GitHub
parent abf6c87ee8
commit 7c152acb2c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
29 changed files with 528 additions and 296 deletions

View file

@ -0,0 +1,9 @@
# frozen_string_literal: true
class Admin::Settings::AboutController < Admin::SettingsController
private
def after_update_redirect_path
admin_settings_about_path
end
end

View file

@ -0,0 +1,9 @@
# frozen_string_literal: true
class Admin::Settings::AppearanceController < Admin::SettingsController
private
def after_update_redirect_path
admin_settings_appearance_path
end
end

View file

@ -0,0 +1,9 @@
# frozen_string_literal: true
class Admin::Settings::BrandingController < Admin::SettingsController
private
def after_update_redirect_path
admin_settings_branding_path
end
end

View file

@ -0,0 +1,9 @@
# frozen_string_literal: true
class Admin::Settings::ContentRetentionController < Admin::SettingsController
private
def after_update_redirect_path
admin_settings_content_retention_path
end
end

View file

@ -0,0 +1,9 @@
# frozen_string_literal: true
class Admin::Settings::DiscoveryController < Admin::SettingsController
private
def after_update_redirect_path
admin_settings_discovery_path
end
end

View file

@ -0,0 +1,9 @@
# frozen_string_literal: true
class Admin::Settings::RegistrationsController < Admin::SettingsController
private
def after_update_redirect_path
admin_settings_registrations_path
end
end

View file

@ -2,7 +2,7 @@
module Admin module Admin
class SettingsController < BaseController class SettingsController < BaseController
def edit def show
authorize :settings, :show? authorize :settings, :show?
@admin_settings = Form::AdminSettings.new @admin_settings = Form::AdminSettings.new
@ -15,14 +15,18 @@ module Admin
if @admin_settings.save if @admin_settings.save
flash[:notice] = I18n.t('generic.changes_saved_msg') flash[:notice] = I18n.t('generic.changes_saved_msg')
redirect_to edit_admin_settings_path redirect_to after_update_redirect_path
else else
render :edit render :show
end end
end end
private private
def after_update_redirect_path
raise NotImplementedError
end
def settings_params def settings_params
params.require(:form_admin_settings).permit(*Form::AdminSettings::KEYS) params.require(:form_admin_settings).permit(*Form::AdminSettings::KEYS)
end end

View file

@ -9,7 +9,7 @@ module Admin
@site_upload.destroy! @site_upload.destroy!
redirect_to edit_admin_settings_path, notice: I18n.t('admin.site_uploads.destroyed_msg') redirect_to admin_settings_path, notice: I18n.t('admin.site_uploads.destroyed_msg')
end end
private private

View file

@ -1,11 +1,4 @@
# frozen_string_literal: true # frozen_string_literal: true
module Admin::SettingsHelper module Admin::SettingsHelper
def site_upload_delete_hint(hint, var)
upload = SiteUpload.find_by(var: var.to_s)
return hint unless upload
link = link_to t('admin.site_uploads.delete'), admin_site_upload_path(upload), data: { method: :delete }
safe_join([hint, link], '<br/>'.html_safe)
end
end end

View file

@ -188,21 +188,70 @@ $content-width: 840px;
padding-top: 30px; padding-top: 30px;
} }
&-heading { &__heading {
display: flex;
padding-bottom: 36px; padding-bottom: 36px;
border-bottom: 1px solid lighten($ui-base-color, 8%); border-bottom: 1px solid lighten($ui-base-color, 8%);
margin: -15px -15px 40px 0; margin-bottom: 40px;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
& > * { &__row {
margin-top: 15px; display: flex;
margin-right: 15px; flex-wrap: wrap;
align-items: center;
justify-content: space-between;
margin: -15px -15px 0 0;
& > * {
margin-top: 15px;
margin-right: 15px;
}
} }
&-actions { &__tabs {
margin-top: 30px;
margin-bottom: -31px;
& > div {
display: flex;
gap: 10px;
}
a {
font-size: 14px;
display: inline-flex;
align-items: center;
padding: 7px 15px;
border-radius: 4px;
color: $darker-text-color;
text-decoration: none;
position: relative;
font-weight: 500;
gap: 5px;
white-space: nowrap;
&.selected {
font-weight: 700;
color: $primary-text-color;
&::after {
content: "";
display: block;
width: 100%;
border-bottom: 1px solid $ui-highlight-color;
position: absolute;
bottom: -5px;
left: 0;
}
}
&:hover,
&:focus,
&:active {
background: lighten($ui-base-color, 4%);
}
}
}
&__actions {
display: inline-flex; display: inline-flex;
& > :not(:first-child) { & > :not(:first-child) {
@ -228,11 +277,7 @@ $content-width: 840px;
color: $secondary-text-color; color: $secondary-text-color;
font-size: 24px; font-size: 24px;
line-height: 36px; line-height: 36px;
font-weight: 400; font-weight: 700;
@media screen and (max-width: $no-columns-breakpoint) {
font-weight: 700;
}
} }
h3 { h3 {
@ -437,6 +482,11 @@ body,
} }
} }
& > div {
display: flex;
gap: 5px;
}
strong { strong {
font-weight: 500; font-weight: 500;
text-transform: uppercase; text-transform: uppercase;
@ -1143,7 +1193,7 @@ a.name-tag,
path:first-child { path:first-child {
fill: rgba($highlight-text-color, 0.25) !important; fill: rgba($highlight-text-color, 0.25) !important;
fill-opacity: 1 !important; fill-opacity: 100% !important;
} }
path:last-child { path:last-child {

View file

@ -29,6 +29,11 @@
background: transparent; background: transparent;
padding: 0; padding: 0;
cursor: pointer; cursor: pointer;
text-decoration: none;
&--destructive {
color: $error-value-color;
}
&:hover, &:hover,
&:active { &:active {

View file

@ -254,7 +254,7 @@ code {
& > label { & > label {
font-family: inherit; font-family: inherit;
font-size: 16px; font-size: 14px;
color: $primary-text-color; color: $primary-text-color;
display: block; display: block;
font-weight: 500; font-weight: 500;
@ -291,6 +291,20 @@ code {
.input:last-child { .input:last-child {
margin-bottom: 0; margin-bottom: 0;
} }
&__thumbnail {
display: block;
margin: 0;
margin-bottom: 10px;
max-width: 100%;
height: auto;
border-radius: 4px;
background: url("images/void.png");
&:last-child {
margin-bottom: 0;
}
}
} }
.fields-row { .fields-row {

View file

@ -8,7 +8,6 @@ class Form::AdminSettings
site_contact_email site_contact_email
site_title site_title
site_short_description site_short_description
site_description
site_extended_description site_extended_description
site_terms site_terms
registrations_mode registrations_mode
@ -53,45 +52,55 @@ class Form::AdminSettings
attr_accessor(*KEYS) attr_accessor(*KEYS)
validates :site_short_description, :site_description, html: { wrap_with: :p } validates :registrations_mode, inclusion: { in: %w(open approved none) }, if: -> { defined?(@registrations_mode) }
validates :site_extended_description, :site_terms, :closed_registrations_message, html: true validates :site_contact_email, :site_contact_username, presence: true, if: -> { defined?(@site_contact_username) || defined?(@site_contact_email) }
validates :registrations_mode, inclusion: { in: %w(open approved none) } validates :site_contact_username, existing_username: true, if: -> { defined?(@site_contact_username) }
validates :site_contact_email, :site_contact_username, presence: true validates :bootstrap_timeline_accounts, existing_username: { multiple: true }, if: -> { defined?(@bootstrap_timeline_accounts) }
validates :site_contact_username, existing_username: true validates :show_domain_blocks, inclusion: { in: %w(disabled users all) }, if: -> { defined?(@show_domain_blocks) }
validates :bootstrap_timeline_accounts, existing_username: { multiple: true } validates :show_domain_blocks_rationale, inclusion: { in: %w(disabled users all) }, if: -> { defined?(@show_domain_blocks_rationale) }
validates :show_domain_blocks, inclusion: { in: %w(disabled users all) } validates :media_cache_retention_period, :content_cache_retention_period, :backups_retention_period, numericality: { only_integer: true }, allow_blank: true, if: -> { defined?(@media_cache_retention_period) || defined?(@content_cache_retention_period) || defined?(@backups_retention_period) }
validates :show_domain_blocks_rationale, inclusion: { in: %w(disabled users all) } validates :site_short_description, length: { maximum: 200 }, if: -> { defined?(@site_short_description) }
validates :media_cache_retention_period, :content_cache_retention_period, :backups_retention_period, numericality: { only_integer: true }, allow_blank: true
def initialize(_attributes = {}) KEYS.each do |key|
super define_method(key) do
initialize_attributes return instance_variable_get("@#{key}") if instance_variable_defined?("@#{key}")
stored_value = begin
if UPLOAD_KEYS.include?(key)
SiteUpload.where(var: key).first_or_initialize(var: key)
else
Setting.public_send(key)
end
end
instance_variable_set("@#{key}", stored_value)
end
end
UPLOAD_KEYS.each do |key|
define_method("#{key}=") do |file|
value = public_send(key)
value.file = file
end
end end
def save def save
return false unless valid? return false unless valid?
KEYS.each do |key| KEYS.each do |key|
value = instance_variable_get("@#{key}") next unless instance_variable_defined?("@#{key}")
if UPLOAD_KEYS.include?(key) && !value.nil? if UPLOAD_KEYS.include?(key)
upload = SiteUpload.where(var: key).first_or_initialize(var: key) public_send(key).save
upload.update(file: value)
else else
setting = Setting.where(var: key).first_or_initialize(var: key) setting = Setting.where(var: key).first_or_initialize(var: key)
setting.update(value: typecast_value(key, value)) setting.update(value: typecast_value(key, instance_variable_get("@#{key}")))
end end
end end
end end
private private
def initialize_attributes
KEYS.each do |key|
instance_variable_set("@#{key}", Setting.public_send(key)) if instance_variable_get("@#{key}").nil?
end
end
def typecast_value(key, value) def typecast_value(key, value)
if BOOLEAN_KEYS.include?(key) if BOOLEAN_KEYS.include?(key)
value == '1' value == '1'

View file

@ -8,6 +8,16 @@ class REST::ExtendedDescriptionSerializer < ActiveModel::Serializer
end end
def content def content
object.text if object.text.present?
markdown.render(object.text)
else
''
end
end
private
def markdown
@markdown ||= Redcarpet::Markdown.new(Redcarpet::Render::HTML)
end end
end end

View file

@ -0,0 +1,33 @@
- content_for :header_tags do
= javascript_pack_tag 'admin', async: true, crossorigin: 'anonymous'
- content_for :page_title do
= t('admin.settings.about.title')
- content_for :heading do
%h2= t('admin.settings.title')
= render partial: 'admin/settings/shared/links'
= simple_form_for @admin_settings, url: admin_settings_about_path, html: { method: :patch } do |f|
= render 'shared/error_messages', object: @admin_settings
%p.lead= t('admin.settings.about.preamble')
.fields-group
= f.input :site_extended_description, wrapper: :with_block_label, as: :text, input_html: { rows: 8 }
%p.hint
= t 'admin.settings.about.rules_hint'
= link_to t('admin.settings.about.manage_rules'), admin_rules_path
.fields-row
.fields-row__column.fields-row__column-6.fields-group
= f.input :show_domain_blocks, wrapper: :with_label, collection: %i(disabled users all), label_method: lambda { |value| t("admin.settings.domain_blocks.#{value}") }, include_blank: false, collection_wrapper_tag: 'ul', item_wrapper_tag: 'li'
.fields-row__column.fields-row__column-6.fields-group
= f.input :show_domain_blocks_rationale, wrapper: :with_label, collection: %i(disabled users all), label_method: lambda { |value| t("admin.settings.domain_blocks.#{value}") }, include_blank: false, collection_wrapper_tag: 'ul', item_wrapper_tag: 'li'
.fields-group
= f.input :site_terms, wrapper: :with_block_label, as: :text, input_html: { rows: 8 }
.actions
= f.button :button, t('generic.save_changes'), type: :submit

View file

@ -0,0 +1,34 @@
- content_for :header_tags do
= javascript_pack_tag 'admin', async: true, crossorigin: 'anonymous'
- content_for :page_title do
= t('admin.settings.appearance.title')
- content_for :heading do
%h2= t('admin.settings.title')
= render partial: 'admin/settings/shared/links'
= simple_form_for @admin_settings, url: admin_settings_appearance_path, html: { method: :patch } do |f|
= render 'shared/error_messages', object: @admin_settings
%p.lead= t('admin.settings.appearance.preamble')
.fields-group
= f.input :theme, collection: Themes.instance.names, label_method: lambda { |theme| I18n.t("themes.#{theme}", default: theme) }, wrapper: :with_label, include_blank: false
.fields-group
= f.input :custom_css, wrapper: :with_block_label, as: :text, input_html: { rows: 8 }
.fields-row
.fields-row__column.fields-row__column-6.fields-group
= f.input :mascot, as: :file, wrapper: :with_block_label
.fields-row__column.fields-row__column-6.fields-group
- if @admin_settings.mascot.persisted?
= image_tag @admin_settings.mascot.file.url, class: 'fields-group__thumbnail'
= link_to admin_site_upload_path(@admin_settings.mascot), data: { method: :delete }, class: 'link-button link-button--destructive' do
= fa_icon 'trash fw'
= t('admin.site_uploads.delete')
.actions
= f.button :button, t('generic.save_changes'), type: :submit

View file

@ -0,0 +1,39 @@
- content_for :header_tags do
= javascript_pack_tag 'admin', async: true, crossorigin: 'anonymous'
- content_for :page_title do
= t('admin.settings.branding.title')
- content_for :heading do
%h2= t('admin.settings.title')
= render partial: 'admin/settings/shared/links'
= simple_form_for @admin_settings, url: admin_settings_branding_path, html: { method: :patch } do |f|
= render 'shared/error_messages', object: @admin_settings
%p.lead= t('admin.settings.branding.preamble')
.fields-group
= f.input :site_title, wrapper: :with_label
.fields-row
.fields-row__column.fields-row__column-6.fields-group
= f.input :site_contact_username, wrapper: :with_label
.fields-row__column.fields-row__column-6.fields-group
= f.input :site_contact_email, wrapper: :with_label
.fields-group
= f.input :site_short_description, wrapper: :with_block_label, as: :text, input_html: { rows: 2, maxlength: 200 }
.fields-row
.fields-row__column.fields-row__column-6.fields-group
= f.input :thumbnail, as: :file, wrapper: :with_block_label
.fields-row__column.fields-row__column-6.fields-group
- if @admin_settings.thumbnail.persisted?
= image_tag @admin_settings.thumbnail.file.url(:'@1x'), class: 'fields-group__thumbnail'
= link_to admin_site_upload_path(@admin_settings.thumbnail), data: { method: :delete }, class: 'link-button link-button--destructive' do
= fa_icon 'trash fw'
= t('admin.site_uploads.delete')
.actions
= f.button :button, t('generic.save_changes'), type: :submit

View file

@ -0,0 +1,22 @@
- content_for :header_tags do
= javascript_pack_tag 'admin', async: true, crossorigin: 'anonymous'
- content_for :page_title do
= t('admin.settings.content_retention.title')
- content_for :heading do
%h2= t('admin.settings.title')
= render partial: 'admin/settings/shared/links'
= simple_form_for @admin_settings, url: admin_settings_content_retention_path, html: { method: :patch } do |f|
= render 'shared/error_messages', object: @admin_settings
%p.lead= t('admin.settings.content_retention.preamble')
.fields-group
= f.input :media_cache_retention_period, wrapper: :with_block_label, input_html: { pattern: '[0-9]+' }
= f.input :content_cache_retention_period, wrapper: :with_block_label, input_html: { pattern: '[0-9]+' }
= f.input :backups_retention_period, wrapper: :with_block_label, input_html: { pattern: '[0-9]+' }
.actions
= f.button :button, t('generic.save_changes'), type: :submit

View file

@ -0,0 +1,40 @@
- content_for :header_tags do
= javascript_pack_tag 'admin', async: true, crossorigin: 'anonymous'
- content_for :page_title do
= t('admin.settings.discovery.title')
- content_for :heading do
%h2= t('admin.settings.title')
= render partial: 'admin/settings/shared/links'
= simple_form_for @admin_settings, url: admin_settings_discovery_path, html: { method: :patch } do |f|
= render 'shared/error_messages', object: @admin_settings
%p.lead= t('admin.settings.discovery.preamble')
%h4= t('admin.settings.discovery.trends')
.fields-group
= f.input :trends, as: :boolean, wrapper: :with_label
.fields-group
= f.input :trendable_by_default, as: :boolean, wrapper: :with_label
%h4= t('admin.settings.discovery.public_timelines')
.fields-group
= f.input :timeline_preview, as: :boolean, wrapper: :with_label
%h4= t('admin.settings.discovery.follow_recommendations')
.fields-group
= f.input :bootstrap_timeline_accounts, wrapper: :with_block_label
%h4= t('admin.settings.discovery.profile_directory')
.fields-group
= f.input :profile_directory, as: :boolean, wrapper: :with_label
.actions
= f.button :button, t('generic.save_changes'), type: :submit

View file

@ -1,102 +0,0 @@
- content_for :header_tags do
= javascript_pack_tag 'admin', async: true, crossorigin: 'anonymous'
- content_for :page_title do
= t('admin.settings.title')
- content_for :heading_actions do
= button_tag t('generic.save_changes'), class: 'button', form: 'edit_admin'
= simple_form_for @admin_settings, url: admin_settings_path, html: { method: :patch, id: 'edit_admin' } do |f|
= render 'shared/error_messages', object: @admin_settings
.fields-group
= f.input :site_title, wrapper: :with_label, label: t('admin.settings.site_title')
.fields-row
.fields-row__column.fields-row__column-6.fields-group
= f.input :theme, collection: Themes.instance.names, label: t('simple_form.labels.defaults.setting_theme'), label_method: lambda { |theme| I18n.t("themes.#{theme}", default: theme) }, wrapper: :with_label, include_blank: false
.fields-row__column.fields-row__column-6.fields-group
= f.input :registrations_mode, collection: %w(open approved none), wrapper: :with_label, label: t('admin.settings.registrations_mode.title'), include_blank: false, label_method: lambda { |mode| I18n.t("admin.settings.registrations_mode.modes.#{mode}") }
.fields-row
.fields-row__column.fields-row__column-6.fields-group
= f.input :site_contact_username, wrapper: :with_label, label: t('admin.settings.contact_information.username')
.fields-row__column.fields-row__column-6.fields-group
= f.input :site_contact_email, wrapper: :with_label, label: t('admin.settings.contact_information.email')
.fields-group
= f.input :site_short_description, wrapper: :with_block_label, as: :text, label: t('admin.settings.site_short_description.title'), hint: t('admin.settings.site_short_description.desc_html'), input_html: { rows: 2 }
.fields-group
= f.input :site_description, wrapper: :with_block_label, as: :text, label: t('admin.settings.site_description.title'), hint: t('admin.settings.site_description.desc_html'), input_html: { rows: 2 }
.fields-row
.fields-row__column.fields-row__column-6.fields-group
= f.input :thumbnail, as: :file, wrapper: :with_block_label, label: t('admin.settings.thumbnail.title'), hint: site_upload_delete_hint(t('admin.settings.thumbnail.desc_html'), :thumbnail)
.fields-row
.fields-row__column.fields-row__column-6.fields-group
= f.input :mascot, as: :file, wrapper: :with_block_label, label: t('admin.settings.mascot.title'), hint: site_upload_delete_hint(t('admin.settings.mascot.desc_html'), :mascot)
%hr.spacer/
.fields-group
= f.input :require_invite_text, as: :boolean, wrapper: :with_label, label: t('admin.settings.registrations.require_invite_text.title'), hint: t('admin.settings.registrations.require_invite_text.desc_html'), disabled: !approved_registrations?
%hr.spacer/
.fields-group
= f.input :bootstrap_timeline_accounts, wrapper: :with_block_label, label: t('admin.settings.bootstrap_timeline_accounts.title'), hint: t('admin.settings.bootstrap_timeline_accounts.desc_html')
%hr.spacer/
- unless whitelist_mode?
.fields-group
= f.input :timeline_preview, as: :boolean, wrapper: :with_label, label: t('admin.settings.timeline_preview.title'), hint: t('admin.settings.timeline_preview.desc_html')
- unless whitelist_mode?
.fields-group
= f.input :activity_api_enabled, as: :boolean, wrapper: :with_label, label: t('admin.settings.activity_api_enabled.title'), hint: t('admin.settings.activity_api_enabled.desc_html'), recommended: true
.fields-group
= f.input :peers_api_enabled, as: :boolean, wrapper: :with_label, label: t('admin.settings.peers_api_enabled.title'), hint: t('admin.settings.peers_api_enabled.desc_html'), recommended: true
.fields-group
= f.input :preview_sensitive_media, as: :boolean, wrapper: :with_label, label: t('admin.settings.preview_sensitive_media.title'), hint: t('admin.settings.preview_sensitive_media.desc_html')
.fields-group
= f.input :profile_directory, as: :boolean, wrapper: :with_label, label: t('admin.settings.profile_directory.title'), hint: t('admin.settings.profile_directory.desc_html')
.fields-group
= f.input :trends, as: :boolean, wrapper: :with_label, label: t('admin.settings.trends.title'), hint: t('admin.settings.trends.desc_html')
.fields-group
= f.input :trendable_by_default, as: :boolean, wrapper: :with_label, label: t('admin.settings.trendable_by_default.title'), hint: t('admin.settings.trendable_by_default.desc_html'), recommended: :not_recommended
.fields-group
= f.input :noindex, as: :boolean, wrapper: :with_label, label: t('admin.settings.default_noindex.title'), hint: t('admin.settings.default_noindex.desc_html')
%hr.spacer/
.fields-row
.fields-row__column.fields-row__column-6.fields-group
= f.input :show_domain_blocks, wrapper: :with_label, collection: %i(disabled users all), label: t('admin.settings.domain_blocks.title'), label_method: lambda { |value| t("admin.settings.domain_blocks.#{value}") }, include_blank: false, collection_wrapper_tag: 'ul', item_wrapper_tag: 'li'
.fields-row__column.fields-row__column-6.fields-group
= f.input :show_domain_blocks_rationale, wrapper: :with_label, collection: %i(disabled users all), label: t('admin.settings.domain_blocks_rationale.title'), label_method: lambda { |value| t("admin.settings.domain_blocks.#{value}") }, include_blank: false, collection_wrapper_tag: 'ul', item_wrapper_tag: 'li'
.fields-group
= f.input :site_extended_description, wrapper: :with_block_label, as: :text, label: t('admin.settings.site_description_extended.title'), hint: t('admin.settings.site_description_extended.desc_html'), input_html: { rows: 8 } unless whitelist_mode?
= f.input :closed_registrations_message, as: :text, wrapper: :with_block_label, label: t('admin.settings.registrations.closed_message.title'), hint: t('admin.settings.registrations.closed_message.desc_html'), input_html: { rows: 8 }
= f.input :site_terms, wrapper: :with_block_label, as: :text, label: t('admin.settings.site_terms.title'), hint: t('admin.settings.site_terms.desc_html'), input_html: { rows: 8 }
= f.input :custom_css, wrapper: :with_block_label, as: :text, input_html: { rows: 8 }, label: t('admin.settings.custom_css.title'), hint: t('admin.settings.custom_css.desc_html')
%hr.spacer/
.fields-group
= f.input :media_cache_retention_period, wrapper: :with_block_label, input_html: { pattern: '[0-9]+' }
= f.input :content_cache_retention_period, wrapper: :with_block_label, input_html: { pattern: '[0-9]+' }
= f.input :backups_retention_period, wrapper: :with_block_label, input_html: { pattern: '[0-9]+' }
.actions
= f.button :button, t('generic.save_changes'), type: :submit

View file

@ -0,0 +1,27 @@
- content_for :header_tags do
= javascript_pack_tag 'admin', async: true, crossorigin: 'anonymous'
- content_for :page_title do
= t('admin.settings.registrations.title')
- content_for :heading do
%h2= t('admin.settings.title')
= render partial: 'admin/settings/shared/links'
= simple_form_for @admin_settings, url: admin_settings_branding_path, html: { method: :patch } do |f|
= render 'shared/error_messages', object: @admin_settings
%p.lead= t('admin.settings.registrations.preamble')
.fields-row
.fields-row__column.fields-row__column-6.fields-group
= f.input :registrations_mode, collection: %w(open approved none), wrapper: :with_label, include_blank: false, label_method: lambda { |mode| I18n.t("admin.settings.registrations_mode.modes.#{mode}") }
.fields-row__column.fields-row__column-6.fields-group
= f.input :require_invite_text, as: :boolean, wrapper: :with_label, disabled: !approved_registrations?
.fields-group
= f.input :closed_registrations_message, as: :text, wrapper: :with_block_label, input_html: { rows: 2 }
.actions
= f.button :button, t('generic.save_changes'), type: :submit

View file

@ -0,0 +1,8 @@
.content__heading__tabs
= render_navigation renderer: :links do |primary|
- primary.item :branding, safe_join([fa_icon('pencil fw'), t('admin.settings.branding.title')]), admin_settings_branding_path
- primary.item :about, safe_join([fa_icon('file-text fw'), t('admin.settings.about.title')]), admin_settings_about_path
- primary.item :registrations, safe_join([fa_icon('users fw'), t('admin.settings.registrations.title')]), admin_settings_registrations_path
- primary.item :discovery, safe_join([fa_icon('search fw'), t('admin.settings.discovery.title')]), admin_settings_discovery_path
- primary.item :content_retention, safe_join([fa_icon('history fw'), t('admin.settings.content_retention.title')]), admin_settings_content_retention_path
- primary.item :appearance, safe_join([fa_icon('desktop fw'), t('admin.settings.appearance.title')]), admin_settings_appearance_path

View file

@ -22,15 +22,16 @@
.content-wrapper .content-wrapper
.content .content
.content-heading .content__heading
- if content_for?(:heading) - if content_for?(:heading)
= yield :heading = yield :heading
- else - else
%h2= yield :page_title .content__heading__row
%h2= yield :page_title
- if :heading_actions - if content_for?(:heading_actions)
.content-heading-actions .content__heading__actions
= yield :heading_actions = yield :heading_actions
= render 'application/flashes' = render 'application/flashes'

View file

@ -667,79 +667,40 @@ en:
empty: No server rules have been defined yet. empty: No server rules have been defined yet.
title: Server rules title: Server rules
settings: settings:
activity_api_enabled: about:
desc_html: Counts of locally published posts, active users, and new registrations in weekly buckets manage_rules: Manage server rules
title: Publish aggregate statistics about user activity in the API preamble: Provide in-depth information about how the server is operated, moderated, funded.
bootstrap_timeline_accounts: rules_hint: There is a dedicated area for rules that your users are expected to adhere to.
desc_html: Separate multiple usernames by comma. These accounts will be guaranteed to be shown in follow recommendations title: About
title: Recommend these accounts to new users appearance:
contact_information: preamble: Customize Mastodon's web interface.
email: Business e-mail title: Appearance
username: Contact username branding:
custom_css: preamble: Your server's branding differentiates it from other servers in the network. This information may be displayed across a variety of environments, such as Mastodon's web interface, native applications, in link previews on other websites and within messaging apps, and so on. For this reason, it is best to keep this information clear, short and concise.
desc_html: Modify the look with CSS loaded on every page title: Branding
title: Custom CSS content_retention:
default_noindex: preamble: Control how user-generated content is stored in Mastodon.
desc_html: Affects all users who have not changed this setting themselves title: Content retention
title: Opt users out of search engine indexing by default discovery:
follow_recommendations: Follow recommendations
preamble: Surfacing interesting content is instrumental in onboarding new users who may not know anyone Mastodon. Control how various discovery features work on your server.
profile_directory: Profile directory
public_timelines: Public timelines
title: Discovery
trends: Trends
domain_blocks: domain_blocks:
all: To everyone all: To everyone
disabled: To no one disabled: To no one
title: Show domain blocks
users: To logged-in local users users: To logged-in local users
domain_blocks_rationale:
title: Show rationale
mascot:
desc_html: Displayed on multiple pages. At least 293×205px recommended. When not set, falls back to default mascot
title: Mascot image
peers_api_enabled:
desc_html: Domain names this server has encountered in the fediverse
title: Publish list of discovered servers in the API
preview_sensitive_media:
desc_html: Link previews on other websites will display a thumbnail even if the media is marked as sensitive
title: Show sensitive media in OpenGraph previews
profile_directory:
desc_html: Allow users to be discoverable
title: Enable profile directory
registrations: registrations:
closed_message: preamble: Control who can create an account on your server.
desc_html: Displayed on frontpage when registrations are closed. You can use HTML tags title: Registrations
title: Closed registration message
require_invite_text:
desc_html: When registrations require manual approval, make the “Why do you want to join?” text input mandatory rather than optional
title: Require new users to enter a reason to join
registrations_mode: registrations_mode:
modes: modes:
approved: Approval required for sign up approved: Approval required for sign up
none: Nobody can sign up none: Nobody can sign up
open: Anyone can sign up open: Anyone can sign up
title: Registrations mode title: Server Settings
site_description:
desc_html: Introductory paragraph on the API. Describe what makes this Mastodon server special and anything else important. You can use HTML tags, in particular <code>&lt;a&gt;</code> and <code>&lt;em&gt;</code>.
title: Server description
site_description_extended:
desc_html: A good place for your code of conduct, rules, guidelines and other things that set your server apart. You can use HTML tags
title: Custom extended information
site_short_description:
desc_html: Displayed in sidebar and meta tags. Describe what Mastodon is and what makes this server special in a single paragraph.
title: Short server description
site_terms:
desc_html: You can write your own privacy policy. You can use HTML tags
title: Custom privacy policy
site_title: Server name
thumbnail:
desc_html: Used for previews via OpenGraph and API. 1200x630px recommended
title: Server thumbnail
timeline_preview:
desc_html: Display link to public timeline on landing page and allow API access to the public timeline without authentication
title: Allow unauthenticated access to public timeline
title: Site settings
trendable_by_default:
desc_html: Specific trending content can still be explicitly disallowed
title: Allow trends without prior review
trends:
desc_html: Publicly display previously reviewed content that is currently trending
title: Trends
site_uploads: site_uploads:
delete: Delete uploaded file delete: Delete uploaded file
destroyed_msg: Site upload successfully deleted! destroyed_msg: Site upload successfully deleted!

View file

@ -75,8 +75,25 @@ en:
warn: Hide the filtered content behind a warning mentioning the filter's title warn: Hide the filtered content behind a warning mentioning the filter's title
form_admin_settings: form_admin_settings:
backups_retention_period: Keep generated user archives for the specified number of days. backups_retention_period: Keep generated user archives for the specified number of days.
bootstrap_timeline_accounts: These accounts will be pinned to the top of new users' follow recommendations.
closed_registrations_message: Displayed when sign-ups are closed
content_cache_retention_period: Posts from other servers will be deleted after the specified number of days when set to a positive value. This may be irreversible. content_cache_retention_period: Posts from other servers will be deleted after the specified number of days when set to a positive value. This may be irreversible.
custom_css: You can apply custom styles on the web version of Mastodon.
mascot: Overrides the illustration in the advanced web interface.
media_cache_retention_period: Downloaded media files will be deleted after the specified number of days when set to a positive value, and re-downloaded on demand. media_cache_retention_period: Downloaded media files will be deleted after the specified number of days when set to a positive value, and re-downloaded on demand.
profile_directory: The profile directory lists all users who have opted-in to be discoverable.
require_invite_text: When sign-ups require manual approval, make the “Why do you want to join?” text input mandatory rather than optional
site_contact_email: How people can reach you for legal or support inquiries.
site_contact_username: How people can reach you on Mastodon.
site_extended_description: Any additional information that may be useful to visitors and your users. Can be structured with Markdown syntax.
site_short_description: A short description to help uniquely identify your server. Who is running it, who is it for?
site_terms: Use your own privacy policy or leave blank to use the default. Can be structured with Markdown syntax.
site_title: How people may refer to your server besides its domain name.
theme: Theme that logged out visitors and new users see.
thumbnail: A roughly 2:1 image displayed alongside your server information.
timeline_preview: Logged out visitors will be able to browse the most recent public posts available on the server.
trendable_by_default: Skip manual review of trending content. Individual items can still be removed from trends after the fact.
trends: Trends show which posts, hashtags and news stories are gaining traction on your server.
form_challenge: form_challenge:
current_password: You are entering a secure area current_password: You are entering a secure area
imports: imports:
@ -213,8 +230,28 @@ en:
warn: Hide with a warning warn: Hide with a warning
form_admin_settings: form_admin_settings:
backups_retention_period: User archive retention period backups_retention_period: User archive retention period
bootstrap_timeline_accounts: Always recommend these accounts to new users
closed_registrations_message: Custom message when sign-ups are not available
content_cache_retention_period: Content cache retention period content_cache_retention_period: Content cache retention period
custom_css: Custom CSS
mascot: Custom mascot (legacy)
media_cache_retention_period: Media cache retention period media_cache_retention_period: Media cache retention period
profile_directory: Enable profile directory
registrations_mode: Who can sign-up
require_invite_text: Require a reason to join
show_domain_blocks: Show domain blocks
show_domain_blocks_rationale: Show why domains were blocked
site_contact_email: Contact e-mail
site_contact_username: Contact username
site_extended_description: Extended description
site_short_description: Server description
site_terms: Privacy Policy
site_title: Server name
theme: Default theme
thumbnail: Server thumbnail
timeline_preview: Allow unauthenticated access to public timelines
trendable_by_default: Allow trends without prior review
trends: Enable trends
interactions: interactions:
must_be_follower: Block notifications from non-followers must_be_follower: Block notifications from non-followers
must_be_following: Block notifications from people you don't follow must_be_following: Block notifications from people you don't follow

View file

@ -52,7 +52,7 @@ SimpleNavigation::Configuration.run do |navigation|
n.item :admin, safe_join([fa_icon('cogs fw'), t('admin.title')]), nil, if: -> { current_user.can?(:view_dashboard, :manage_settings, :manage_rules, :manage_announcements, :manage_custom_emojis, :manage_webhooks, :manage_federation) } do |s| n.item :admin, safe_join([fa_icon('cogs fw'), t('admin.title')]), nil, if: -> { current_user.can?(:view_dashboard, :manage_settings, :manage_rules, :manage_announcements, :manage_custom_emojis, :manage_webhooks, :manage_federation) } do |s|
s.item :dashboard, safe_join([fa_icon('tachometer fw'), t('admin.dashboard.title')]), admin_dashboard_path, if: -> { current_user.can?(:view_dashboard) } s.item :dashboard, safe_join([fa_icon('tachometer fw'), t('admin.dashboard.title')]), admin_dashboard_path, if: -> { current_user.can?(:view_dashboard) }
s.item :settings, safe_join([fa_icon('cogs fw'), t('admin.settings.title')]), edit_admin_settings_path, if: -> { current_user.can?(:manage_settings) }, highlights_on: %r{/admin/settings} s.item :settings, safe_join([fa_icon('cogs fw'), t('admin.settings.title')]), admin_settings_path, if: -> { current_user.can?(:manage_settings) }, highlights_on: %r{/admin/settings}
s.item :rules, safe_join([fa_icon('gavel fw'), t('admin.rules.title')]), admin_rules_path, highlights_on: %r{/admin/rules}, if: -> { current_user.can?(:manage_rules) } s.item :rules, safe_join([fa_icon('gavel fw'), t('admin.rules.title')]), admin_rules_path, highlights_on: %r{/admin/rules}, if: -> { current_user.can?(:manage_rules) }
s.item :roles, safe_join([fa_icon('vcard fw'), t('admin.roles.title')]), admin_roles_path, highlights_on: %r{/admin/roles}, if: -> { current_user.can?(:manage_roles) } s.item :roles, safe_join([fa_icon('vcard fw'), t('admin.roles.title')]), admin_roles_path, highlights_on: %r{/admin/roles}, if: -> { current_user.can?(:manage_roles) }
s.item :announcements, safe_join([fa_icon('bullhorn fw'), t('admin.announcements.title')]), admin_announcements_path, highlights_on: %r{/admin/announcements}, if: -> { current_user.can?(:manage_announcements) } s.item :announcements, safe_join([fa_icon('bullhorn fw'), t('admin.announcements.title')]), admin_announcements_path, highlights_on: %r{/admin/announcements}, if: -> { current_user.can?(:manage_announcements) }

View file

@ -241,7 +241,18 @@ Rails.application.routes.draw do
end end
end end
resource :settings, only: [:edit, :update] get '/settings', to: redirect('/admin/settings/branding')
get '/settings/edit', to: redirect('/admin/settings/branding')
namespace :settings do
resource :branding, only: [:show, :update], controller: 'branding'
resource :registrations, only: [:show, :update], controller: 'registrations'
resource :content_retention, only: [:show, :update], controller: 'content_retention'
resource :about, only: [:show, :update], controller: 'about'
resource :appearance, only: [:show, :update], controller: 'appearance'
resource :discovery, only: [:show, :update], controller: 'discovery'
end
resources :site_uploads, only: [:destroy] resources :site_uploads, only: [:destroy]
resources :invites, only: [:index, :create, :destroy] do resources :invites, only: [:index, :create, :destroy] do

View file

@ -0,0 +1,53 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe Admin::Settings::BrandingController, type: :controller do
render_views
describe 'When signed in as an admin' do
before do
sign_in Fabricate(:user, role: UserRole.find_by(name: 'Admin')), scope: :user
end
describe 'GET #show' do
it 'returns http success' do
get :show
expect(response).to have_http_status(200)
end
end
describe 'PUT #update' do
before do
allow_any_instance_of(Form::AdminSettings).to receive(:valid?).and_return(true)
end
around do |example|
before = Setting.site_short_description
Setting.site_short_description = nil
example.run
Setting.site_short_description = before
Setting.new_setting_key = nil
end
it 'cannot create a setting value for a non-admin key' do
expect(Setting.new_setting_key).to be_blank
patch :update, params: { form_admin_settings: { new_setting_key: 'New key value' } }
expect(response).to redirect_to(admin_settings_branding_path)
expect(Setting.new_setting_key).to be_nil
end
it 'creates a settings value that didnt exist before for eligible key' do
expect(Setting.site_short_description).to be_blank
patch :update, params: { form_admin_settings: { site_short_description: 'New key value' } }
expect(response).to redirect_to(admin_settings_branding_path)
expect(Setting.site_short_description).to eq 'New key value'
end
end
end
end

View file

@ -1,71 +0,0 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe Admin::SettingsController, type: :controller do
render_views
describe 'When signed in as an admin' do
before do
sign_in Fabricate(:user, role: UserRole.find_by(name: 'Admin')), scope: :user
end
describe 'GET #edit' do
it 'returns http success' do
get :edit
expect(response).to have_http_status(200)
end
end
describe 'PUT #update' do
before do
allow_any_instance_of(Form::AdminSettings).to receive(:valid?).and_return(true)
end
describe 'for a record that doesnt exist' do
around do |example|
before = Setting.site_extended_description
Setting.site_extended_description = nil
example.run
Setting.site_extended_description = before
Setting.new_setting_key = nil
end
it 'cannot create a setting value for a non-admin key' do
expect(Setting.new_setting_key).to be_blank
patch :update, params: { form_admin_settings: { new_setting_key: 'New key value' } }
expect(response).to redirect_to(edit_admin_settings_path)
expect(Setting.new_setting_key).to be_nil
end
it 'creates a settings value that didnt exist before for eligible key' do
expect(Setting.site_extended_description).to be_blank
patch :update, params: { form_admin_settings: { site_extended_description: 'New key value' } }
expect(response).to redirect_to(edit_admin_settings_path)
expect(Setting.site_extended_description).to eq 'New key value'
end
end
context do
around do |example|
site_title = Setting.site_title
example.run
Setting.site_title = site_title
end
it 'updates a settings value' do
Setting.site_title = 'Original'
patch :update, params: { form_admin_settings: { site_title: 'New title' } }
expect(response).to redirect_to(edit_admin_settings_path)
expect(Setting.site_title).to eq 'New title'
end
end
end
end
end