@@ -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 |
@@ -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 { | |||
@@ -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? | |||
@@ -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 |
@@ -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 | |||
@@ -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 |
@@ -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? | |||
• | |||
- if custom_emoji.visible_in_picker? | |||
= t('admin.custom_emojis.listed') | |||
- else | |||
= t('admin.custom_emojis.unlisted') |
@@ -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' |
@@ -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! | |||
@@ -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 | |||
@@ -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 |