Browse Source

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

master^2
Eugen Rochko 4 years ago
committed by GitHub
parent
commit
1110ea1a91
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 281 additions and 176 deletions
  1. +34
    -68
      app/controllers/admin/custom_emojis_controller.rb
  2. +41
    -0
      app/javascript/styles/mastodon/tables.scss
  3. +6
    -0
      app/models/custom_emoji.rb
  4. +2
    -0
      app/models/custom_emoji_category.rb
  5. +5
    -3
      app/models/custom_emoji_filter.rb
  6. +106
    -0
      app/models/form/custom_emoji_batch.rb
  7. +29
    -26
      app/views/admin/custom_emojis/_custom_emoji.html.haml
  8. +52
    -14
      app/views/admin/custom_emojis/index.html.haml
  9. +3
    -0
      config/locales/en.yml
  10. +3
    -5
      config/routes.rb
  11. +0
    -60
      spec/controllers/admin/custom_emojis_controller_spec.rb

+ 34
- 68
app/controllers/admin/custom_emojis_controller.rb 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

+ 41
- 0
app/javascript/styles/mastodon/tables.scss 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 {


+ 6
- 0
app/models/custom_emoji.rb 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?


+ 2
- 0
app/models/custom_emoji_category.rb 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

+ 5
- 3
app/models/custom_emoji_filter.rb 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


+ 106
- 0
app/models/form/custom_emoji_batch.rb 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

+ 29
- 26
app/views/admin/custom_emojis/_custom_emoji.html.haml View File

@@ -1,28 +1,31 @@
%tr
%td
= custom_emoji_tag(custom_emoji)
%td
%samp= ":#{custom_emoji.shortcode}:"
%td
- 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
.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)

.batch-table__row__content__text
%samp= ":#{custom_emoji.shortcode}:"

- 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
= 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'
= custom_emoji.domain

%br/

- if custom_emoji.disabled?
= t('admin.custom_emojis.disabled')
- 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
- 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') }
- 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')

+ 52
- 14
app/views/admin/custom_emojis/index.html.haml 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'

+ 3
- 0
config/locales/en.yml 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!


+ 3
- 5
config/routes.rb 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



+ 0
- 60
spec/controllers/admin/custom_emojis_controller_spec.rb 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

Loading…
Cancel
Save