Add customizable user roles (#18641)
* Add customizable user roles * Various fixes and improvements * Add migration for old settings and fix tootctl role management
This commit is contained in:
@ -4,45 +4,36 @@
|
||||
- content_for :header_tags do
|
||||
= javascript_pack_tag 'admin', async: true, crossorigin: 'anonymous'
|
||||
|
||||
.filters
|
||||
.filter-subset
|
||||
%strong= t('admin.accounts.location.title')
|
||||
%ul
|
||||
%li= filter_link_to t('generic.all'), origin: nil
|
||||
%li= filter_link_to t('admin.accounts.location.local'), origin: 'local'
|
||||
%li= filter_link_to t('admin.accounts.location.remote'), origin: 'remote'
|
||||
.filter-subset
|
||||
%strong= t('admin.accounts.moderation.title')
|
||||
%ul
|
||||
%li= filter_link_to t('generic.all'), status: nil
|
||||
%li= filter_link_to t('admin.accounts.moderation.active'), status: 'active'
|
||||
%li= filter_link_to t('admin.accounts.moderation.suspended'), status: 'suspended'
|
||||
%li= filter_link_to safe_join([t('admin.accounts.moderation.pending'), "(#{number_with_delimiter(User.pending.count)})"], ' '), status: 'pending'
|
||||
.filter-subset
|
||||
%strong= t('admin.accounts.role')
|
||||
%ul
|
||||
%li= filter_link_to t('admin.accounts.moderation.all'), permissions: nil
|
||||
%li= filter_link_to t('admin.accounts.roles.staff'), permissions: 'staff'
|
||||
.filter-subset
|
||||
%strong= t 'generic.order_by'
|
||||
%ul
|
||||
%li= filter_link_to t('relationships.most_recent'), order: nil
|
||||
%li= filter_link_to t('relationships.last_active'), order: 'active'
|
||||
|
||||
= form_tag admin_accounts_url, method: 'GET', class: 'simple_form' do
|
||||
.fields-group
|
||||
- (AccountFilter::KEYS - %i(origin status permissions)).each do |key|
|
||||
- if params[key].present?
|
||||
= hidden_field_tag key, params[key]
|
||||
.filters
|
||||
.filter-subset.filter-subset--with-select
|
||||
%strong= t('admin.accounts.location.title')
|
||||
.input.select.optional
|
||||
= select_tag :origin, options_for_select([[t('admin.accounts.location.local'), 'local'], [t('admin.accounts.location.remote'), 'remote']], params[:origin]), prompt: I18n.t('generic.all')
|
||||
.filter-subset.filter-subset--with-select
|
||||
%strong= t('admin.accounts.moderation.title')
|
||||
.input.select.optional
|
||||
= select_tag :status, options_for_select([[t('admin.accounts.moderation.active'), 'active'], [t('admin.accounts.moderation.silenced'), 'silenced'], [t('admin.accounts.moderation.suspended'), 'suspended'], [safe_join([t('admin.accounts.moderation.pending'), "(#{number_with_delimiter(User.pending.count)})"], ' '), 'pending']], params[:status]), prompt: I18n.t('generic.all')
|
||||
.filter-subset.filter-subset--with-select
|
||||
%strong= t('admin.accounts.role')
|
||||
.input.select.optional
|
||||
= select_tag :role_ids, options_from_collection_for_select(UserRole.assignable, :id, :name, params[:role_ids]), prompt: I18n.t('admin.accounts.moderation.all')
|
||||
.filter-subset.filter-subset--with-select
|
||||
%strong= t 'generic.order_by'
|
||||
.input.select
|
||||
= select_tag :order, options_for_select([[t('relationships.most_recent'), nil], [t('relationships.last_active'), 'active']], params[:order])
|
||||
|
||||
.fields-group
|
||||
- %i(username by_domain display_name email ip).each do |key|
|
||||
- unless key == :by_domain && params[:origin] != 'remote'
|
||||
.input.string.optional
|
||||
= text_field_tag key, params[key], class: 'string optional', placeholder: I18n.t("admin.accounts.#{key}")
|
||||
|
||||
.actions
|
||||
%button.button= t('admin.accounts.search')
|
||||
= link_to t('admin.accounts.reset'), admin_accounts_path, class: 'button negative'
|
||||
.actions
|
||||
%button.button= t('admin.accounts.search')
|
||||
= link_to t('admin.accounts.reset'), admin_accounts_path, class: 'button negative'
|
||||
|
||||
%hr.spacer/
|
||||
|
||||
= form_for(@form, url: batch_admin_accounts_path) do |f|
|
||||
= hidden_field_tag :page, params[:page] || 1
|
||||
|
@ -92,10 +92,13 @@
|
||||
|
||||
%tr
|
||||
%th= t('admin.accounts.role')
|
||||
%td= t("admin.accounts.roles.#{@account.user&.role}")
|
||||
%td
|
||||
= table_link_to 'angle-double-up', t('admin.accounts.promote'), promote_admin_account_role_path(@account.id), method: :post, data: { confirm: t('admin.accounts.are_you_sure') } if can?(:promote, @account.user)
|
||||
= table_link_to 'angle-double-down', t('admin.accounts.demote'), demote_admin_account_role_path(@account.id), method: :post, data: { confirm: t('admin.accounts.are_you_sure') } if can?(:demote, @account.user)
|
||||
- if @account.user_role&.everyone?
|
||||
= t('admin.accounts.no_role_assigned')
|
||||
- else
|
||||
= @account.user_role&.name
|
||||
%td
|
||||
= table_link_to 'vcard', t('admin.accounts.change_role.label'), admin_user_role_path(@account.user) if can?(:change_role, @account.user)
|
||||
|
||||
%tr
|
||||
%th{ rowspan: can?(:create, :email_domain_block) ? 3 : 2 }= t('admin.accounts.email')
|
||||
|
@ -11,7 +11,7 @@
|
||||
.filter-subset.filter-subset--with-select
|
||||
%strong= t('admin.action_logs.filter_by_user')
|
||||
.input.select.optional
|
||||
= select_tag :account_id, options_from_collection_for_select(Account.joins(:user).merge(User.staff), :id, :username, params[:account_id]), prompt: I18n.t('admin.accounts.moderation.all')
|
||||
= select_tag :account_id, options_from_collection_for_select(@auditable_accounts, :id, :username, params[:account_id]), prompt: I18n.t('admin.accounts.moderation.all')
|
||||
|
||||
.filter-subset.filter-subset--with-select
|
||||
%strong= t('admin.action_logs.filter_by_action')
|
||||
|
@ -4,32 +4,33 @@
|
||||
- content_for :header_tags do
|
||||
= javascript_pack_tag 'admin', async: true, crossorigin: 'anonymous'
|
||||
|
||||
- content_for :heading_actions do
|
||||
= l(@time_period.first)
|
||||
= ' - '
|
||||
= l(@time_period.last)
|
||||
- if current_user.can?(:view_dashboard)
|
||||
- content_for :heading_actions do
|
||||
= l(@time_period.first)
|
||||
= ' - '
|
||||
= l(@time_period.last)
|
||||
|
||||
%p
|
||||
= fa_icon 'info fw'
|
||||
= t('admin.instances.totals_time_period_hint_html')
|
||||
%p
|
||||
= fa_icon 'info fw'
|
||||
= t('admin.instances.totals_time_period_hint_html')
|
||||
|
||||
.dashboard
|
||||
.dashboard__item
|
||||
= react_admin_component :counter, measure: 'instance_accounts', start_at: @time_period.first, end_at: @time_period.last, params: { domain: @instance.domain }, label: t('admin.instances.dashboard.instance_accounts_measure'), href: admin_accounts_path(origin: 'remote', by_domain: @instance.domain)
|
||||
.dashboard__item
|
||||
= react_admin_component :counter, measure: 'instance_statuses', start_at: @time_period.first, end_at: @time_period.last, params: { domain: @instance.domain }, label: t('admin.instances.dashboard.instance_statuses_measure')
|
||||
.dashboard__item
|
||||
= react_admin_component :counter, measure: 'instance_media_attachments', start_at: @time_period.first, end_at: @time_period.last, params: { domain: @instance.domain }, label: t('admin.instances.dashboard.instance_media_attachments_measure')
|
||||
.dashboard__item
|
||||
= react_admin_component :counter, measure: 'instance_follows', start_at: @time_period.first, end_at: @time_period.last, params: { domain: @instance.domain }, label: t('admin.instances.dashboard.instance_follows_measure')
|
||||
.dashboard__item
|
||||
= react_admin_component :counter, measure: 'instance_followers', start_at: @time_period.first, end_at: @time_period.last, params: { domain: @instance.domain }, label: t('admin.instances.dashboard.instance_followers_measure')
|
||||
.dashboard__item
|
||||
= react_admin_component :counter, measure: 'instance_reports', start_at: @time_period.first, end_at: @time_period.last, params: { domain: @instance.domain }, label: t('admin.instances.dashboard.instance_reports_measure'), href: admin_reports_path(by_target_domain: @instance.domain)
|
||||
.dashboard__item
|
||||
= react_admin_component :dimension, dimension: 'instance_accounts', start_at: @time_period.first, end_at: @time_period.last, params: { domain: @instance.domain }, limit: 8, label: t('admin.instances.dashboard.instance_accounts_dimension')
|
||||
.dashboard__item
|
||||
= react_admin_component :dimension, dimension: 'instance_languages', start_at: @time_period.first, end_at: @time_period.last, params: { domain: @instance.domain }, limit: 8, label: t('admin.instances.dashboard.instance_languages_dimension')
|
||||
.dashboard
|
||||
.dashboard__item
|
||||
= react_admin_component :counter, measure: 'instance_accounts', start_at: @time_period.first, end_at: @time_period.last, params: { domain: @instance.domain }, label: t('admin.instances.dashboard.instance_accounts_measure'), href: admin_accounts_path(origin: 'remote', by_domain: @instance.domain)
|
||||
.dashboard__item
|
||||
= react_admin_component :counter, measure: 'instance_statuses', start_at: @time_period.first, end_at: @time_period.last, params: { domain: @instance.domain }, label: t('admin.instances.dashboard.instance_statuses_measure')
|
||||
.dashboard__item
|
||||
= react_admin_component :counter, measure: 'instance_media_attachments', start_at: @time_period.first, end_at: @time_period.last, params: { domain: @instance.domain }, label: t('admin.instances.dashboard.instance_media_attachments_measure')
|
||||
.dashboard__item
|
||||
= react_admin_component :counter, measure: 'instance_follows', start_at: @time_period.first, end_at: @time_period.last, params: { domain: @instance.domain }, label: t('admin.instances.dashboard.instance_follows_measure')
|
||||
.dashboard__item
|
||||
= react_admin_component :counter, measure: 'instance_followers', start_at: @time_period.first, end_at: @time_period.last, params: { domain: @instance.domain }, label: t('admin.instances.dashboard.instance_followers_measure')
|
||||
.dashboard__item
|
||||
= react_admin_component :counter, measure: 'instance_reports', start_at: @time_period.first, end_at: @time_period.last, params: { domain: @instance.domain }, label: t('admin.instances.dashboard.instance_reports_measure'), href: admin_reports_path(by_target_domain: @instance.domain)
|
||||
.dashboard__item
|
||||
= react_admin_component :dimension, dimension: 'instance_accounts', start_at: @time_period.first, end_at: @time_period.last, params: { domain: @instance.domain }, limit: 8, label: t('admin.instances.dashboard.instance_accounts_dimension')
|
||||
.dashboard__item
|
||||
= react_admin_component :dimension, dimension: 'instance_languages', start_at: @time_period.first, end_at: @time_period.last, params: { domain: @instance.domain }, limit: 8, label: t('admin.instances.dashboard.instance_languages_dimension')
|
||||
|
||||
%hr.spacer/
|
||||
|
||||
|
37
app/views/admin/roles/_form.html.haml
Normal file
37
app/views/admin/roles/_form.html.haml
Normal file
@ -0,0 +1,37 @@
|
||||
= simple_form_for @role, url: @role.new_record? ? admin_roles_path : admin_role_path(@role) do |f|
|
||||
= render 'shared/error_messages', object: @role
|
||||
|
||||
- if @role.everyone?
|
||||
.flash-message.info
|
||||
= t('admin.roles.everyone_full_description_html')
|
||||
- else
|
||||
.fields-group
|
||||
= f.input :name, wrapper: :with_label
|
||||
|
||||
.fields-group
|
||||
= f.input :position, wrapper: :with_label
|
||||
|
||||
.fields-group
|
||||
= f.input :color, wrapper: :with_label, input_html: { placeholder: '#000000' }
|
||||
|
||||
%hr.spacer/
|
||||
|
||||
.fields-group
|
||||
= f.input :highlighted, wrapper: :with_label
|
||||
|
||||
%hr.spacer/
|
||||
|
||||
.field-group
|
||||
.input.with_block_label
|
||||
%label= t('simple_form.labels.user_role.permissions_as_keys')
|
||||
%span.hint= t('simple_form.hints.user_role.permissions_as_keys')
|
||||
|
||||
- (@role.everyone? ? UserRole::Flags::CATEGORIES.slice(:invites) : UserRole::Flags::CATEGORIES).each do |category, permissions|
|
||||
%h4= t(category, scope: 'admin.roles.categories')
|
||||
|
||||
= f.input :permissions_as_keys, collection: permissions, wrapper: :with_block_label, include_blank: false, label_method: lambda { |privilege| safe_join([t("admin.roles.privileges.#{privilege}"), content_tag(:span, t("admin.roles.privileges.#{privilege}_description"), class: 'hint')]) }, required: false, as: :check_boxes, collection_wrapper_tag: 'ul', item_wrapper_tag: 'li', label: false, hint: false
|
||||
|
||||
%hr.spacer/
|
||||
|
||||
.actions
|
||||
= f.button :button, @role.new_record? ? t('admin.roles.add_new') : t('generic.save_changes'), type: :submit
|
18
app/views/admin/roles/_role.html.haml
Normal file
18
app/views/admin/roles/_role.html.haml
Normal file
@ -0,0 +1,18 @@
|
||||
.announcements-list__item
|
||||
= link_to edit_admin_role_path(role), class: 'announcements-list__item__title' do
|
||||
%span.user-role{ class: "user-role-#{role.id}" }
|
||||
= fa_icon 'users fw'
|
||||
|
||||
- if role.everyone?
|
||||
= t('admin.roles.everyone')
|
||||
- else
|
||||
= role.name
|
||||
|
||||
.announcements-list__item__action-bar
|
||||
.announcements-list__item__meta
|
||||
- if role.everyone?
|
||||
= t('admin.roles.everyone_full_description_html')
|
||||
- else
|
||||
= link_to t('admin.roles.assigned_users', count: role.users.count), admin_accounts_path(role_id: role.id)
|
||||
•
|
||||
%abbr{ title: role.permissions_as_keys.map { |privilege| I18n.t("admin.roles.privileges.#{privilege}") }.join(', ') }= t('admin.roles.permissions_count', count: role.permissions_as_keys.size)
|
8
app/views/admin/roles/edit.html.haml
Normal file
8
app/views/admin/roles/edit.html.haml
Normal file
@ -0,0 +1,8 @@
|
||||
- content_for :page_title do
|
||||
= t('admin.roles.edit', name: @role.everyone? ? t('admin.roles.everyone') : @role.name)
|
||||
|
||||
- content_for :heading_actions do
|
||||
= link_to t('admin.roles.delete'), admin_role_path(@role), method: :delete, data: { confirm: t('admin.accounts.are_you_sure') }, class: 'button button--destructive' if can?(:destroy, @role)
|
||||
|
||||
= render partial: 'form'
|
||||
|
17
app/views/admin/roles/index.html.haml
Normal file
17
app/views/admin/roles/index.html.haml
Normal file
@ -0,0 +1,17 @@
|
||||
- content_for :page_title do
|
||||
= t('admin.roles.title')
|
||||
|
||||
- content_for :heading_actions do
|
||||
= link_to t('admin.roles.add_new'), new_admin_role_path, class: 'button' if can?(:create, :user_role)
|
||||
|
||||
%p= t('admin.roles.description_html')
|
||||
|
||||
%hr.spacer/
|
||||
|
||||
.applications-list
|
||||
= render partial: 'role', collection: @roles.select(&:everyone?)
|
||||
|
||||
%hr.spacer/
|
||||
|
||||
.applications-list
|
||||
= render partial: 'role', collection: @roles.reject(&:everyone?)
|
4
app/views/admin/roles/new.html.haml
Normal file
4
app/views/admin/roles/new.html.haml
Normal file
@ -0,0 +1,4 @@
|
||||
- content_for :page_title do
|
||||
= t('admin.roles.add_new')
|
||||
|
||||
= render partial: 'form'
|
@ -61,9 +61,6 @@
|
||||
.fields-group
|
||||
= f.input :show_known_fediverse_at_about_page, as: :boolean, wrapper: :with_label, label: t('admin.settings.show_known_fediverse_at_about_page.title'), hint: t('admin.settings.show_known_fediverse_at_about_page.desc_html')
|
||||
|
||||
.fields-group
|
||||
= f.input :show_staff_badge, as: :boolean, wrapper: :with_label, label: t('admin.settings.show_staff_badge.title'), hint: t('admin.settings.show_staff_badge.desc_html')
|
||||
|
||||
.fields-group
|
||||
= f.input :open_deletion, as: :boolean, wrapper: :with_label, label: t('admin.settings.registrations.deletion.title'), hint: t('admin.settings.registrations.deletion.desc_html')
|
||||
|
||||
@ -91,9 +88,6 @@
|
||||
|
||||
%hr.spacer/
|
||||
|
||||
.fields-group
|
||||
= f.input :min_invite_role, wrapper: :with_label, collection: %i(disabled user moderator admin), label: t('admin.settings.registrations.min_invite_role.title'), label_method: lambda { |role| role == :disabled ? t('admin.settings.registrations.min_invite_role.disabled') : t("admin.accounts.roles.#{role}") }, include_blank: false, collection_wrapper_tag: 'ul', item_wrapper_tag: 'li'
|
||||
|
||||
.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'
|
||||
|
@ -4,49 +4,50 @@
|
||||
- content_for :page_title do
|
||||
= "##{@tag.name}"
|
||||
|
||||
- content_for :heading_actions do
|
||||
= l(@time_period.first)
|
||||
= ' - '
|
||||
= l(@time_period.last)
|
||||
- if current_user.can?(:view_dashboard)
|
||||
- content_for :heading_actions do
|
||||
= l(@time_period.first)
|
||||
= ' - '
|
||||
= l(@time_period.last)
|
||||
|
||||
.dashboard
|
||||
.dashboard__item
|
||||
= react_admin_component :counter, measure: 'tag_accounts', start_at: @time_period.first, end_at: @time_period.last, params: { id: @tag.id }, label: t('admin.trends.tags.dashboard.tag_accounts_measure'), href: tag_url(@tag), target: '_blank'
|
||||
.dashboard__item
|
||||
= react_admin_component :counter, measure: 'tag_uses', start_at: @time_period.first, end_at: @time_period.last, params: { id: @tag.id }, label: t('admin.trends.tags.dashboard.tag_uses_measure')
|
||||
.dashboard__item
|
||||
= react_admin_component :counter, measure: 'tag_servers', start_at: @time_period.first, end_at: @time_period.last, params: { id: @tag.id }, label: t('admin.trends.tags.dashboard.tag_servers_measure')
|
||||
.dashboard__item
|
||||
= react_admin_component :dimension, dimension: 'tag_servers', start_at: @time_period.first, end_at: @time_period.last, params: { id: @tag.id }, limit: 8, label: t('admin.trends.tags.dashboard.tag_servers_dimension')
|
||||
.dashboard__item
|
||||
= react_admin_component :dimension, dimension: 'tag_languages', start_at: @time_period.first, end_at: @time_period.last, params: { id: @tag.id }, limit: 8, label: t('admin.trends.tags.dashboard.tag_languages_dimension')
|
||||
.dashboard__item
|
||||
= link_to admin_tag_path(@tag.id), class: ['dashboard__quick-access', @tag.usable? ? 'positive' : 'negative'] do
|
||||
- if @tag.usable?
|
||||
%span= t('admin.trends.tags.usable')
|
||||
= fa_icon 'check fw'
|
||||
- else
|
||||
%span= t('admin.trends.tags.not_usable')
|
||||
= fa_icon 'lock fw'
|
||||
.dashboard
|
||||
.dashboard__item
|
||||
= react_admin_component :counter, measure: 'tag_accounts', start_at: @time_period.first, end_at: @time_period.last, params: { id: @tag.id }, label: t('admin.trends.tags.dashboard.tag_accounts_measure'), href: tag_url(@tag), target: '_blank'
|
||||
.dashboard__item
|
||||
= react_admin_component :counter, measure: 'tag_uses', start_at: @time_period.first, end_at: @time_period.last, params: { id: @tag.id }, label: t('admin.trends.tags.dashboard.tag_uses_measure')
|
||||
.dashboard__item
|
||||
= react_admin_component :counter, measure: 'tag_servers', start_at: @time_period.first, end_at: @time_period.last, params: { id: @tag.id }, label: t('admin.trends.tags.dashboard.tag_servers_measure')
|
||||
.dashboard__item
|
||||
= react_admin_component :dimension, dimension: 'tag_servers', start_at: @time_period.first, end_at: @time_period.last, params: { id: @tag.id }, limit: 8, label: t('admin.trends.tags.dashboard.tag_servers_dimension')
|
||||
.dashboard__item
|
||||
= react_admin_component :dimension, dimension: 'tag_languages', start_at: @time_period.first, end_at: @time_period.last, params: { id: @tag.id }, limit: 8, label: t('admin.trends.tags.dashboard.tag_languages_dimension')
|
||||
.dashboard__item
|
||||
= link_to admin_tag_path(@tag.id), class: ['dashboard__quick-access', @tag.usable? ? 'positive' : 'negative'] do
|
||||
- if @tag.usable?
|
||||
%span= t('admin.trends.tags.usable')
|
||||
= fa_icon 'check fw'
|
||||
- else
|
||||
%span= t('admin.trends.tags.not_usable')
|
||||
= fa_icon 'lock fw'
|
||||
|
||||
= link_to admin_tag_path(@tag.id), class: ['dashboard__quick-access', @tag.trendable? ? 'positive' : 'negative'] do
|
||||
- if @tag.trendable?
|
||||
%span= t('admin.trends.tags.trendable')
|
||||
= fa_icon 'check fw'
|
||||
- else
|
||||
%span= t('admin.trends.tags.not_trendable')
|
||||
= fa_icon 'lock fw'
|
||||
= link_to admin_tag_path(@tag.id), class: ['dashboard__quick-access', @tag.trendable? ? 'positive' : 'negative'] do
|
||||
- if @tag.trendable?
|
||||
%span= t('admin.trends.tags.trendable')
|
||||
= fa_icon 'check fw'
|
||||
- else
|
||||
%span= t('admin.trends.tags.not_trendable')
|
||||
= fa_icon 'lock fw'
|
||||
|
||||
|
||||
= link_to admin_tag_path(@tag.id), class: ['dashboard__quick-access', @tag.listable? ? 'positive' : 'negative'] do
|
||||
- if @tag.listable?
|
||||
%span= t('admin.trends.tags.listable')
|
||||
= fa_icon 'check fw'
|
||||
- else
|
||||
%span= t('admin.trends.tags.not_listable')
|
||||
= fa_icon 'lock fw'
|
||||
= link_to admin_tag_path(@tag.id), class: ['dashboard__quick-access', @tag.listable? ? 'positive' : 'negative'] do
|
||||
- if @tag.listable?
|
||||
%span= t('admin.trends.tags.listable')
|
||||
= fa_icon 'check fw'
|
||||
- else
|
||||
%span= t('admin.trends.tags.not_listable')
|
||||
= fa_icon 'lock fw'
|
||||
|
||||
%hr.spacer/
|
||||
%hr.spacer/
|
||||
|
||||
= simple_form_for @tag, url: admin_tag_path(@tag.id) do |f|
|
||||
= render 'shared/error_messages', object: @tag
|
||||
|
9
app/views/admin/users/roles/show.html.haml
Normal file
9
app/views/admin/users/roles/show.html.haml
Normal file
@ -0,0 +1,9 @@
|
||||
- content_for :page_title do
|
||||
= t('admin.accounts.change_role.title', username: @user.account.username)
|
||||
|
||||
= simple_form_for @user, url: admin_user_role_path(@user) do |f|
|
||||
.fields-group
|
||||
= f.association :role, wrapper: :with_block_label, collection: UserRole.assignable, label_method: :name, include_blank: I18n.t('admin.accounts.change_role.no_role')
|
||||
|
||||
.actions
|
||||
= f.button :button, t('generic.save_changes'), type: :submit
|
Reference in New Issue
Block a user