Add batch actions and categories to admin UI for custom emojis (#11793)

This commit is contained in:
Eugen Rochko 2019-09-09 22:44:17 +02:00 committed by GitHub
parent 39e904f9c1
commit 25fb124ee6
11 changed files with 281 additions and 176 deletions

View file

@ -2,19 +2,20 @@
module Admin module Admin
class CustomEmojisController < BaseController class CustomEmojisController < BaseController
before_action :set_custom_emoji, except: [:index, :new, :create]
before_action :set_filter_params
include ObfuscateFilename include ObfuscateFilename
obfuscate_filename [:custom_emoji, :image] obfuscate_filename [:custom_emoji, :image]
def index def index
authorize :custom_emoji, :index? authorize :custom_emoji, :index?
@custom_emojis = filtered_custom_emojis.eager_load(:local_counterpart).page(params[:page]) @custom_emojis = filtered_custom_emojis.eager_load(:local_counterpart).page(params[:page])
@form = Form::CustomEmojiBatch.new
end end
def new def new
authorize :custom_emoji, :create? authorize :custom_emoji, :create?
@custom_emoji = CustomEmoji.new @custom_emoji = CustomEmoji.new
end end
@ -31,69 +32,17 @@ module Admin
end end
end end
def update def batch
authorize @custom_emoji, :update? @form = Form::CustomEmojiBatch.new(form_custom_emoji_batch_params.merge(current_account: current_account, action: action_from_button))
@form.save
if @custom_emoji.update(resource_params) rescue ActionController::ParameterMissing
log_action :update, @custom_emoji flash[:alert] = I18n.t('admin.accounts.no_account_selected')
flash[:notice] = I18n.t('admin.custom_emojis.updated_msg') ensure
else redirect_to admin_custom_emojis_path(filter_params)
flash[:alert] = I18n.t('admin.custom_emojis.update_failed_msg')
end
redirect_to admin_custom_emojis_path(page: params[:page], **@filter_params)
end
def destroy
authorize @custom_emoji, :destroy?
@custom_emoji.destroy!
log_action :destroy, @custom_emoji
flash[:notice] = I18n.t('admin.custom_emojis.destroyed_msg')
redirect_to admin_custom_emojis_path(page: params[:page], **@filter_params)
end
def copy
authorize @custom_emoji, :copy?
emoji = CustomEmoji.find_or_initialize_by(domain: nil,
shortcode: @custom_emoji.shortcode)
emoji.image = @custom_emoji.image
if emoji.save
log_action :create, emoji
flash[:notice] = I18n.t('admin.custom_emojis.copied_msg')
else
flash[:alert] = I18n.t('admin.custom_emojis.copy_failed_msg')
end
redirect_to admin_custom_emojis_path(page: params[:page], **@filter_params)
end
def enable
authorize @custom_emoji, :enable?
@custom_emoji.update!(disabled: false)
log_action :enable, @custom_emoji
flash[:notice] = I18n.t('admin.custom_emojis.enabled_msg')
redirect_to admin_custom_emojis_path(page: params[:page], **@filter_params)
end
def disable
authorize @custom_emoji, :disable?
@custom_emoji.update!(disabled: true)
log_action :disable, @custom_emoji
flash[:notice] = I18n.t('admin.custom_emojis.disabled_msg')
redirect_to admin_custom_emojis_path(page: params[:page], **@filter_params)
end end
private private
def set_custom_emoji
@custom_emoji = CustomEmoji.find(params[:id])
end
def set_filter_params
@filter_params = filter_params.to_hash.symbolize_keys
end
def resource_params def resource_params
params.require(:custom_emoji).permit(:shortcode, :image, :visible_in_picker) params.require(:custom_emoji).permit(:shortcode, :image, :visible_in_picker)
end end
@ -103,12 +52,29 @@ module Admin
end end
def filter_params def filter_params
params.permit( params.slice(:local, :remote, :by_domain, :shortcode, :page).permit(:local, :remote, :by_domain, :shortcode, :page)
:local, end
:remote,
:by_domain, def action_from_button
:shortcode if params[:update]
) 'update'
elsif params[:list]
'list'
elsif params[:unlist]
'unlist'
elsif params[:enable]
'enable'
elsif params[:disable]
'disable'
elsif params[:copy]
'copy'
elsif params[:delete]
'delete'
end
end
def form_custom_emoji_batch_params
params.require(:form_custom_emoji_batch).permit(:action, :category_id, :category_name, custom_emoji_ids: [])
end end
end end
end end

View file

@ -180,6 +180,18 @@ a.table-action-link {
} }
} }
&__form {
padding: 16px;
border: 1px solid darken($ui-base-color, 8%);
border-top: 0;
background: $ui-base-color;
.fields-row {
padding-top: 0;
margin-bottom: 0;
}
}
&__row { &__row {
border: 1px solid darken($ui-base-color, 8%); border: 1px solid darken($ui-base-color, 8%);
border-top: 0; border-top: 0;
@ -210,6 +222,35 @@ a.table-action-link {
&--unpadded { &--unpadded {
padding: 0; padding: 0;
} }
&--with-image {
display: flex;
align-items: center;
}
&__image {
flex: 0 0 auto;
display: flex;
justify-content: center;
align-items: center;
margin-right: 10px;
.emojione {
width: 32px;
height: 32px;
}
}
&__text {
flex: 1 1 auto;
}
&__extra {
flex: 0 0 auto;
text-align: right;
color: $darker-text-color;
font-weight: 500;
}
} }
.directory__tag { .directory__tag {

View file

@ -59,6 +59,12 @@ class CustomEmoji < ApplicationRecord
:emoji :emoji
end end
def copy!
copy = self.class.find_or_initialize_by(domain: nil, shortcode: shortcode)
copy.image = image
copy.save!
end
class << self class << self
def from_text(text, domain) def from_text(text, domain)
return [] if text.blank? return [] if text.blank?

View file

@ -12,4 +12,6 @@
class CustomEmojiCategory < ApplicationRecord class CustomEmojiCategory < ApplicationRecord
has_many :emojis, class_name: 'CustomEmoji', foreign_key: 'category_id', inverse_of: :category has_many :emojis, class_name: 'CustomEmoji', foreign_key: 'category_id', inverse_of: :category
validates :name, presence: true, uniqueness: true
end end

View file

@ -11,6 +11,8 @@ class CustomEmojiFilter
scope = CustomEmoji.alphabetic scope = CustomEmoji.alphabetic
params.each do |key, value| params.each do |key, value|
next if key.to_s == 'page'
scope.merge!(scope_for(key, value)) if value.present? scope.merge!(scope_for(key, value)) if value.present?
end end
@ -22,13 +24,13 @@ class CustomEmojiFilter
def scope_for(key, value) def scope_for(key, value)
case key.to_s case key.to_s
when 'local' when 'local'
CustomEmoji.local CustomEmoji.local.left_joins(:category).reorder(Arel.sql('custom_emoji_categories.name ASC NULLS FIRST, custom_emojis.shortcode ASC'))
when 'remote' when 'remote'
CustomEmoji.remote CustomEmoji.remote
when 'by_domain' when 'by_domain'
CustomEmoji.where(domain: value.downcase) CustomEmoji.where(domain: value.strip.downcase)
when 'shortcode' when 'shortcode'
CustomEmoji.search(value) CustomEmoji.search(value.strip)
else else
raise "Unknown filter: #{key}" raise "Unknown filter: #{key}"
end end

View file

@ -0,0 +1,106 @@
# frozen_string_literal: true
class Form::CustomEmojiBatch
include ActiveModel::Model
include Authorization
include AccountableConcern
attr_accessor :custom_emoji_ids, :action, :current_account,
:category_id, :category_name, :visible_in_picker
def save
case action
when 'update'
update!
when 'list'
list!
when 'unlist'
unlist!
when 'enable'
enable!
when 'disable'
disable!
when 'copy'
copy!
when 'delete'
delete!
end
end
private
def custom_emojis
CustomEmoji.where(id: custom_emoji_ids)
end
def update!
custom_emojis.each { |custom_emoji| authorize(custom_emoji, :update?) }
category = begin
if category_id.present?
CustomEmojiCategory.find(category_id)
elsif category_name.present?
CustomEmojiCategory.create!(name: category_name)
end
end
custom_emojis.each do |custom_emoji|
custom_emoji.update(category_id: category&.id)
log_action :update, custom_emoji
end
end
def list!
custom_emojis.each { |custom_emoji| authorize(custom_emoji, :update?) }
custom_emojis.each do |custom_emoji|
custom_emoji.update(visible_in_picker: true)
log_action :update, custom_emoji
end
end
def unlist!
custom_emojis.each { |custom_emoji| authorize(custom_emoji, :update?) }
custom_emojis.each do |custom_emoji|
custom_emoji.update(visible_in_picker: false)
log_action :update, custom_emoji
end
end
def enable!
custom_emojis.each { |custom_emoji| authorize(custom_emoji, :enable?) }
custom_emojis.each do |custom_emoji|
custom_emoji.update(disabled: false)
log_action :enable, custom_emoji
end
end
def disable!
custom_emojis.each { |custom_emoji| authorize(custom_emoji, :disable?) }
custom_emojis.each do |custom_emoji|
custom_emoji.update(disabled: true)
log_action :disable, custom_emoji
end
end
def copy!
custom_emojis.each { |custom_emoji| authorize(custom_emoji, :copy?) }
custom_emojis.each do |custom_emoji|
copied_custom_emoji = custom_emoji.copy!
log_action :create, copied_custom_emoji
end
end
def delete!
custom_emojis.each { |custom_emoji| authorize(custom_emoji, :destroy?) }
custom_emojis.each do |custom_emoji|
custom_emoji.destroy
log_action :destroy, custom_emoji
end
end
end

View file

@ -1,28 +1,31 @@
%tr .batch-table__row
%td %label.batch-table__row__select.batch-table__row__select--aligned.batch-checkbox
= custom_emoji_tag(custom_emoji) = f.check_box :custom_emoji_ids, { multiple: true, include_hidden: false }, custom_emoji.id
%td .batch-table__row__content.batch-table__row__content--with-image
%samp= ":#{custom_emoji.shortcode}:" .batch-table__row__content__image
%td = custom_emoji_tag(custom_emoji)
- if custom_emoji.local?
= t('admin.accounts.location.local') .batch-table__row__content__text
- else %samp= ":#{custom_emoji.shortcode}:"
= link_to custom_emoji.domain, admin_custom_emojis_path(by_domain: custom_emoji.domain)
%td - if custom_emoji.local?
- if custom_emoji.local? %span.account-role.bot= custom_emoji.category&.name || t('admin.custom_emojis.uncategorized')
- if custom_emoji.visible_in_picker
= table_link_to 'eye', t('admin.custom_emojis.listed'), admin_custom_emoji_path(custom_emoji, custom_emoji: { visible_in_picker: false }, page: params[:page], **@filter_params), method: :patch .batch-table__row__content__extra
- if custom_emoji.local?
= t('admin.accounts.location.local')
- else - else
= table_link_to 'eye-slash', t('admin.custom_emojis.unlisted'), admin_custom_emoji_path(custom_emoji, custom_emoji: { visible_in_picker: true }, page: params[:page], **@filter_params), method: :patch = custom_emoji.domain
- else
- if custom_emoji.local_counterpart.present? %br/
= link_to safe_join([custom_emoji_tag(custom_emoji.local_counterpart), t('admin.custom_emojis.overwrite')]), copy_admin_custom_emoji_path(custom_emoji, page: params[:page], **@filter_params), method: :post, class: 'table-action-link'
- if custom_emoji.disabled?
= t('admin.custom_emojis.disabled')
- else - else
= table_link_to 'copy', t('admin.custom_emojis.copy'), copy_admin_custom_emoji_path(custom_emoji, page: params[:page], **@filter_params), method: :post = t('admin.custom_emojis.enabled')
%td - if custom_emoji.local?
- if custom_emoji.disabled? &bull;
= table_link_to 'power-off', t('admin.custom_emojis.enable'), enable_admin_custom_emoji_path(custom_emoji, page: params[:page], **@filter_params), method: :post, data: { confirm: t('admin.accounts.are_you_sure') } - if custom_emoji.visible_in_picker?
- else = t('admin.custom_emojis.listed')
= table_link_to 'power-off', t('admin.custom_emojis.disable'), disable_admin_custom_emoji_path(custom_emoji, page: params[:page], **@filter_params), method: :post, data: { confirm: t('admin.accounts.are_you_sure') } - else
%td = t('admin.custom_emojis.unlisted')
= table_link_to 'times', t('admin.custom_emojis.delete'), admin_custom_emoji_path(custom_emoji, page: params[:page], **@filter_params), method: :delete, data: { confirm: t('admin.accounts.are_you_sure') }

View file

@ -1,6 +1,9 @@
- content_for :page_title do - content_for :page_title do
= t('admin.custom_emojis.title') = t('admin.custom_emojis.title')
- content_for :header_tags do
= javascript_pack_tag 'admin', integrity: true, async: true, crossorigin: 'anonymous'
.filters .filters
.filter-subset .filter-subset
%strong= t('admin.accounts.location.title') %strong= t('admin.accounts.location.title')
@ -20,8 +23,7 @@
= form_tag admin_custom_emojis_url, method: 'GET', class: 'simple_form' do = form_tag admin_custom_emojis_url, method: 'GET', class: 'simple_form' do
.fields-group .fields-group
- Admin::FilterHelper::CUSTOM_EMOJI_FILTERS.each do |key| - Admin::FilterHelper::CUSTOM_EMOJI_FILTERS.each do |key|
- if params[key].present? = hidden_field_tag key, params[key] if params[key].present?
= hidden_field_tag key, params[key]
- %i(shortcode by_domain).each do |key| - %i(shortcode by_domain).each do |key|
.input.string.optional .input.string.optional
@ -31,18 +33,54 @@
%button= t('admin.accounts.search') %button= t('admin.accounts.search')
= link_to t('admin.accounts.reset'), admin_custom_emojis_path, class: 'button negative' = link_to t('admin.accounts.reset'), admin_custom_emojis_path, class: 'button negative'
.table-wrapper = form_for(@form, url: batch_admin_custom_emojis_path) do |f|
%table.table = hidden_field_tag :page, params[:page] || 1
%thead
%tr - Admin::FilterHelper::CUSTOM_EMOJI_FILTERS.each do |key|
%th= t('admin.custom_emojis.emoji') = hidden_field_tag key, params[key] if params[key].present?
%th= t('admin.custom_emojis.shortcode')
%th= t('admin.accounts.domain') .batch-table
%th .batch-table__toolbar
%th %label.batch-table__toolbar__select.batch-checkbox-all
%th = check_box_tag :batch_checkbox_all, nil, false
%tbody .batch-table__toolbar__actions
= render @custom_emojis - if params[:local] == '1'
= f.button safe_join([fa_icon('save'), t('generic.save_changes')]), name: :update, class: 'table-action-link', type: :submit, data: { confirm: t('admin.reports.are_you_sure') }
= f.button safe_join([fa_icon('eye'), t('admin.custom_emojis.list')]), name: :list, class: 'table-action-link', type: :submit, data: { confirm: t('admin.reports.are_you_sure') }
= f.button safe_join([fa_icon('eye-slash'), t('admin.custom_emojis.unlist')]), name: :unlist, class: 'table-action-link', type: :submit, data: { confirm: t('admin.reports.are_you_sure') }
= f.button safe_join([fa_icon('power-off'), t('admin.custom_emojis.enable')]), name: :enable, class: 'table-action-link', type: :submit, data: { confirm: t('admin.reports.are_you_sure') }
= f.button safe_join([fa_icon('power-off'), t('admin.custom_emojis.disable')]), name: :disable, class: 'table-action-link', type: :submit, data: { confirm: t('admin.reports.are_you_sure') }
= f.button safe_join([fa_icon('times'), t('admin.custom_emojis.delete')]), name: :delete, class: 'table-action-link', type: :submit, data: { confirm: t('admin.reports.are_you_sure') }
- unless params[:local] == '1'
= f.button safe_join([fa_icon('copy'), t('admin.custom_emojis.copy')]), name: :copy, class: 'table-action-link', type: :submit, data: { confirm: t('admin.reports.are_you_sure') }
- if params[:local] == '1'
.batch-table__form.simple_form
.fields-row
.fields-group.fields-row__column.fields-row__column-6
.input.select.optional
.label_input
= f.select :category_id, options_from_collection_for_select(CustomEmojiCategory.all, 'id', 'name'), prompt: t('admin.custom_emojis.assign_category'), class: 'select optional', 'aria-label': t('admin.custom_emojis.assign_category')
.fields-group.fields-row__column.fields-row__column-6
.input.string.optional
.label_input
= f.text_field :category_name, class: 'string optional', placeholder: t('admin.custom_emojis.create_new_category'), 'aria-label': t('admin.custom_emojis.create_new_category')
.batch-table__body
- if @custom_emojis.empty?
= nothing_here 'nothing-here--under-tabs'
- else
= render partial: 'custom_emoji', collection: @custom_emojis, locals: { f: f }
= paginate @custom_emojis = paginate @custom_emojis
%hr.spacer/
= link_to t('admin.custom_emojis.upload'), new_admin_custom_emoji_path, class: 'button' = link_to t('admin.custom_emojis.upload'), new_admin_custom_emoji_path, class: 'button'

View file

@ -225,10 +225,12 @@ en:
deleted_status: "(deleted status)" deleted_status: "(deleted status)"
title: Audit log title: Audit log
custom_emojis: custom_emojis:
assign_category: Assign category
by_domain: Domain by_domain: Domain
copied_msg: Successfully created local copy of the emoji copied_msg: Successfully created local copy of the emoji
copy: Copy copy: Copy
copy_failed_msg: Could not make a local copy of that emoji copy_failed_msg: Could not make a local copy of that emoji
create_new_category: Create new category
created_msg: Emoji successfully created! created_msg: Emoji successfully created!
delete: Delete delete: Delete
destroyed_msg: Emojo successfully destroyed! destroyed_msg: Emojo successfully destroyed!
@ -245,6 +247,7 @@ en:
shortcode: Shortcode shortcode: Shortcode
shortcode_hint: At least 2 characters, only alphanumeric characters and underscores shortcode_hint: At least 2 characters, only alphanumeric characters and underscores
title: Custom emojis title: Custom emojis
uncategorized: Uncategorized
unlisted: Unlisted unlisted: Unlisted
update_failed_msg: Could not update that emoji update_failed_msg: Could not update that emoji
updated_msg: Emoji successfully updated! updated_msg: Emoji successfully updated!

View file

@ -242,11 +242,9 @@ Rails.application.routes.draw do
resource :two_factor_authentication, only: [:destroy] resource :two_factor_authentication, only: [:destroy]
end end
resources :custom_emojis, only: [:index, :new, :create, :update, :destroy] do resources :custom_emojis, only: [:index, :new, :create] do
member do collection do
post :copy post :batch
post :enable
post :disable
end end
end end

View file

@ -52,64 +52,4 @@ describe Admin::CustomEmojisController do
end end
end end
end end
describe 'PUT #update' do
let(:custom_emoji) { Fabricate(:custom_emoji, shortcode: 'test') }
let(:image) { fixture_file_upload(Rails.root.join('spec', 'fixtures', 'files', 'emojo.png'), 'image/png') }
before do
put :update, params: { id: custom_emoji.id, custom_emoji: params }
end
context 'when parameter is valid' do
let(:params) { { shortcode: 'updated', image: image } }
it 'succeeds in updating custom emoji' do
expect(flash[:notice]).to eq I18n.t('admin.custom_emojis.updated_msg')
expect(custom_emoji.reload).to have_attributes(shortcode: 'updated')
end
end
context 'when parameter is invalid' do
let(:params) { { shortcode: 'u', image: image } }
it 'fails to update custom emoji' do
expect(flash[:alert]).to eq I18n.t('admin.custom_emojis.update_failed_msg')
expect(custom_emoji.reload).to have_attributes(shortcode: 'test')
end
end
end
describe 'POST #copy' do
subject { post :copy, params: { id: custom_emoji.id } }
let(:custom_emoji) { Fabricate(:custom_emoji, shortcode: 'test') }
it 'copies custom emoji' do
expect { subject }.to change { CustomEmoji.where(shortcode: 'test').count }.by(1)
expect(flash[:notice]).to eq I18n.t('admin.custom_emojis.copied_msg')
end
end
describe 'POST #enable' do
let(:custom_emoji) { Fabricate(:custom_emoji, shortcode: 'test', disabled: true) }
before { post :enable, params: { id: custom_emoji.id } }
it 'enables custom emoji' do
expect(response).to redirect_to admin_custom_emojis_path
expect(custom_emoji.reload).to have_attributes(disabled: false)
end
end
describe 'POST #disable' do
let(:custom_emoji) { Fabricate(:custom_emoji, shortcode: 'test', disabled: false) }
before { post :disable, params: { id: custom_emoji.id } }
it 'enables custom emoji' do
expect(response).to redirect_to admin_custom_emojis_path
expect(custom_emoji.reload).to have_attributes(disabled: true)
end
end
end end