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
class CustomEmojisController < BaseController
before_action :set_custom_emoji, except: [:index, :new, :create]
before_action :set_filter_params
include ObfuscateFilename
obfuscate_filename [:custom_emoji, :image]
def index
authorize :custom_emoji, :index?
@custom_emojis = filtered_custom_emojis.eager_load(:local_counterpart).page(params[:page])
@form = Form::CustomEmojiBatch.new
end
def new
authorize :custom_emoji, :create?
@custom_emoji = CustomEmoji.new
end
@ -31,69 +32,17 @@ module Admin
end
end
def update
authorize @custom_emoji, :update?
if @custom_emoji.update(resource_params)
log_action :update, @custom_emoji
flash[:notice] = I18n.t('admin.custom_emojis.updated_msg')
else
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)
def batch
@form = Form::CustomEmojiBatch.new(form_custom_emoji_batch_params.merge(current_account: current_account, action: action_from_button))
@form.save
rescue ActionController::ParameterMissing
flash[:alert] = I18n.t('admin.accounts.no_account_selected')
ensure
redirect_to admin_custom_emojis_path(filter_params)
end
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
params.require(:custom_emoji).permit(:shortcode, :image, :visible_in_picker)
end
@ -103,12 +52,29 @@ module Admin
end
def filter_params
params.permit(
:local,
:remote,
:by_domain,
:shortcode
)
params.slice(:local, :remote, :by_domain, :shortcode, :page).permit(:local, :remote, :by_domain, :shortcode, :page)
end
def action_from_button
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

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 {
border: 1px solid darken($ui-base-color, 8%);
border-top: 0;
@ -210,6 +222,35 @@ a.table-action-link {
&--unpadded {
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 {

View file

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

View file

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

View file

@ -11,6 +11,8 @@ class CustomEmojiFilter
scope = CustomEmoji.alphabetic
params.each do |key, value|
next if key.to_s == 'page'
scope.merge!(scope_for(key, value)) if value.present?
end
@ -22,13 +24,13 @@ class CustomEmojiFilter
def scope_for(key, value)
case key.to_s
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'
CustomEmoji.remote
when 'by_domain'
CustomEmoji.where(domain: value.downcase)
CustomEmoji.where(domain: value.strip.downcase)
when 'shortcode'
CustomEmoji.search(value)
CustomEmoji.search(value.strip)
else
raise "Unknown filter: #{key}"
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
%td
.batch-table__row
%label.batch-table__row__select.batch-table__row__select--aligned.batch-checkbox
= f.check_box :custom_emoji_ids, { multiple: true, include_hidden: false }, custom_emoji.id
.batch-table__row__content.batch-table__row__content--with-image
.batch-table__row__content__image
= custom_emoji_tag(custom_emoji)
%td
.batch-table__row__content__text
%samp= ":#{custom_emoji.shortcode}:"
%td
- if custom_emoji.local?
%span.account-role.bot= custom_emoji.category&.name || t('admin.custom_emojis.uncategorized')
.batch-table__row__content__extra
- if custom_emoji.local?
= t('admin.accounts.location.local')
- else
= link_to custom_emoji.domain, admin_custom_emojis_path(by_domain: custom_emoji.domain)
%td
- if custom_emoji.local?
- 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
- 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
- else
- if custom_emoji.local_counterpart.present?
= 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'
- else
= table_link_to 'copy', t('admin.custom_emojis.copy'), copy_admin_custom_emoji_path(custom_emoji, page: params[:page], **@filter_params), method: :post
%td
= custom_emoji.domain
%br/
- if custom_emoji.disabled?
= 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') }
= t('admin.custom_emojis.disabled')
- else
= 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') }
%td
= 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') }
= t('admin.custom_emojis.enabled')
- if custom_emoji.local?
&bull;
- if custom_emoji.visible_in_picker?
= t('admin.custom_emojis.listed')
- else
= t('admin.custom_emojis.unlisted')

View file

@ -1,6 +1,9 @@
- content_for :page_title do
= t('admin.custom_emojis.title')
- content_for :header_tags do
= javascript_pack_tag 'admin', integrity: true, async: true, crossorigin: 'anonymous'
.filters
.filter-subset
%strong= t('admin.accounts.location.title')
@ -20,8 +23,7 @@
= form_tag admin_custom_emojis_url, method: 'GET', class: 'simple_form' do
.fields-group
- Admin::FilterHelper::CUSTOM_EMOJI_FILTERS.each do |key|
- if params[key].present?
= hidden_field_tag key, params[key]
= hidden_field_tag key, params[key] if params[key].present?
- %i(shortcode by_domain).each do |key|
.input.string.optional
@ -31,18 +33,54 @@
%button= t('admin.accounts.search')
= link_to t('admin.accounts.reset'), admin_custom_emojis_path, class: 'button negative'
.table-wrapper
%table.table
%thead
%tr
%th= t('admin.custom_emojis.emoji')
%th= t('admin.custom_emojis.shortcode')
%th= t('admin.accounts.domain')
%th
%th
%th
%tbody
= render @custom_emojis
= form_for(@form, url: batch_admin_custom_emojis_path) do |f|
= hidden_field_tag :page, params[:page] || 1
- Admin::FilterHelper::CUSTOM_EMOJI_FILTERS.each do |key|
= hidden_field_tag key, params[key] if params[key].present?
.batch-table
.batch-table__toolbar
%label.batch-table__toolbar__select.batch-checkbox-all
= check_box_tag :batch_checkbox_all, nil, false
.batch-table__toolbar__actions
- 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
%hr.spacer/
= 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)"
title: Audit log
custom_emojis:
assign_category: Assign category
by_domain: Domain
copied_msg: Successfully created local copy of the emoji
copy: Copy
copy_failed_msg: Could not make a local copy of that emoji
create_new_category: Create new category
created_msg: Emoji successfully created!
delete: Delete
destroyed_msg: Emojo successfully destroyed!
@ -245,6 +247,7 @@ en:
shortcode: Shortcode
shortcode_hint: At least 2 characters, only alphanumeric characters and underscores
title: Custom emojis
uncategorized: Uncategorized
unlisted: Unlisted
update_failed_msg: Could not update that emoji
updated_msg: Emoji successfully updated!

View file

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

View file

@ -52,64 +52,4 @@ describe Admin::CustomEmojisController do
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