UX: use "icon-picker" & "image-uploader" fields to set group flair. (#9779)

This commit is contained in:
Vinoth Kannan 2020-05-25 11:08:47 +05:30 committed by GitHub
parent 13d5ccedf5
commit 8e56197728
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 270 additions and 35 deletions

View File

@ -2,11 +2,12 @@ import I18n from "I18n";
import discourseComputed from "discourse-common/utils/decorators";
import { debounce } from "@ember/runloop";
import Component from "@ember/component";
import { observes } from "discourse-common/utils/decorators";
import { on, observes } from "discourse-common/utils/decorators";
import { escapeExpression } from "discourse/lib/utilities";
import { convertIconClass } from "discourse-common/lib/icon-library";
import { ajax } from "discourse/lib/ajax";
import { htmlSafe } from "@ember/template";
import { action } from "@ember/object";
export default Component.extend({
classNames: ["group-flair-inputs"],
@ -16,23 +17,26 @@ export default Component.extend({
return Discourse.getURL("/images/avatar.png");
},
@discourseComputed("model.flair_url")
flairPreviewIcon(flairURL) {
return flairURL && /fa(r|b?)-/.test(flairURL);
@discourseComputed("model.flair_type")
flairPreviewIcon(flairType) {
return flairType && flairType === "icon";
},
@discourseComputed("model.flair_url", "flairPreviewIcon")
flairPreviewIconUrl(flairURL, flairPreviewIcon) {
return flairPreviewIcon ? convertIconClass(flairURL) : "";
@discourseComputed("model.flair_icon")
flairPreviewIconUrl(flairIcon) {
return flairIcon ? convertIconClass(flairIcon) : "";
},
@observes("model.flair_url")
_loadSVGIcon() {
debounce(this, this._loadIcon, 1000);
@on("didInsertElement")
@observes("model.flair_icon")
_loadSVGIcon(flairIcon) {
if (flairIcon) {
debounce(this, this._loadIcon, 1000);
}
},
_loadIcon() {
const icon = convertIconClass(this.get("model.flair_url")),
const icon = convertIconClass(this.model.flair_icon),
c = "#svg-sprites",
h = "ajax-icon-holder",
singleIconEl = `${c} .${h}`;
@ -50,9 +54,14 @@ export default Component.extend({
}
},
@discourseComputed("model.flair_url", "flairPreviewIcon")
flairPreviewImage(flairURL, flairPreviewIcon) {
return flairURL && !flairPreviewIcon;
@discourseComputed("model.flair_type")
flairPreviewImage(flairType) {
return flairType && flairType === "image";
},
@discourseComputed("model.flair_url")
flairImageUrl(flairURL) {
return flairURL && flairURL.match(/\//) ? flairURL : null;
},
@discourseComputed(
@ -91,5 +100,21 @@ export default Component.extend({
flairPreviewLabel(flairPreviewImage) {
const key = flairPreviewImage ? "image" : "icon";
return I18n.t(`groups.flair_preview_${key}`);
},
@action
setFlairImage(upload) {
this.model.setProperties({
flair_url: Discourse.getURL(upload.url),
flair_upload_id: upload.id
});
},
@action
removeFlairImage() {
this.model.setProperties({
flair_url: null,
flair_upload_id: null
});
}
});

View File

@ -188,7 +188,8 @@ const Group = RestModel.extend({
primary_group: !!this.primary_group,
grant_trust_level: this.grant_trust_level,
incoming_email: this.incoming_email,
flair_url: this.flair_url,
flair_icon: null,
flair_upload_id: null,
flair_bg_color: this.flairBackgroundHexColor,
flair_color: this.flairHexColor,
bio_raw: this.bio_raw,
@ -201,6 +202,12 @@ const Group = RestModel.extend({
publish_read_state: this.publish_read_state
};
if (this.flair_type === "icon") {
attrs["flair_icon"] = this.flair_icon;
} else if (this.flair_type === "image") {
attrs["flair_upload_id"] = this.flair_upload_id;
}
if (!this.id) {
attrs["usernames"] = this.usernames;
attrs["owner_usernames"] = this.ownerUsernames;

View File

@ -1,13 +1,36 @@
<div class="control-group">
<label class="control-label" for="flair_url">{{i18n "groups.flair_url"}}</label>
{{text-field name="flair_url"
class="input-xxlarge"
value=model.flair_url
placeholderKey="groups.flair_url_placeholder"}}
<div class="control-instructions">
{{i18n "groups.flair_url_description"}}
<div class="radios">
<label class="radio-label" for="avatar-flair">
{{radio-button name="avatar-flair" value="icon" selection=model.flair_type}}
<b>{{i18n "groups.flair_type.icon"}}</b>
</label>
<label class="radio-label" for="avatar-flair">
{{radio-button name="avatar-flair" value="image" selection=model.flair_type}}
<b>{{i18n "groups.flair_type.image"}}</b>
</label>
</div>
{{#if flairPreviewIcon}}
{{icon-picker
name="icon"
value=model.flair_icon
options=(hash maximum=1)
onChange=(action (mut model.flair_icon))
}}
{{else if flairPreviewImage}}
{{image-uploader
imageUrl=flairImageUrl
onUploadDone=(action "setFlairImage")
onUploadDeleted=(action "removeFlairImage")
type="group_flair"
class="no-repeat contain-image"}}
<div class="control-instructions">
{{i18n "groups.flair_upload_description"}}
</div>
{{/if}}
</div>
<div class="control-group">
@ -40,7 +63,7 @@
{{#if flairPreviewImage}}
<div class="avatar-flair demo {{flairPreviewClasses}}" style={{flairPreviewStyle}}></div>
{{else}}
{{else if flairPreviewIcon}}
<div class="avatar-flair demo {{flairPreviewClasses}}" style={{flairPreviewStyle}} role="presentation">
{{d-icon flairPreviewIconUrl}}
</div>

View File

@ -8,7 +8,7 @@ createWidget("avatar-flair", {
isIcon(attrs) {
return (
attrs.primary_group_flair_url &&
attrs.primary_group_flair_url.includes("fa-")
!attrs.primary_group_flair_url.includes("/")
);
},

View File

@ -153,6 +153,22 @@ table.group-members {
background-color: #f4f4f4;
}
}
.radios {
margin-bottom: 5px;
}
.radio-label {
display: inline-flex;
padding-right: 10px;
margin-bottom: 0;
align-items: center;
}
.uploaded-image-preview {
height: 75px;
width: 275px;
}
}
.group-form-save {

View File

@ -163,7 +163,8 @@ class Admin::GroupsController < Admin::AdminController
:primary_group,
:grant_trust_level,
:incoming_email,
:flair_url,
:flair_icon,
:flair_upload_id,
:flair_bg_color,
:flair_color,
:bio_raw,

View File

@ -521,7 +521,8 @@ class GroupsController < ApplicationController
mentionable_level
messageable_level
title
flair_url
flair_icon
flair_upload_id
flair_bg_color
flair_color
bio_raw

View File

@ -0,0 +1,78 @@
# frozen_string_literal: true
module Jobs
class MigrateGroupFlairImages < ::Jobs::Onceoff
def execute_onceoff(args)
return if Group.column_names.exclude?("flair_url")
Group.where.not(flair_url: nil).each do |group|
if group.flair_upload.present?
g.update_column(:flair_url, nil)
next
end
old_url = group[:flair_url]
group_name = group.name
count = 0
file = nil
sleep_interval = 5
loop do
url = UrlHelper.absolute_without_cdn(old_url)
begin
file = FileHelper.download(
url,
max_file_size: [
SiteSetting.max_image_size_kb.kilobytes,
20.megabytes
].max,
tmp_file_name: 'tmp_group_flair_upload',
skip_rate_limit: true,
follow_redirect: true
)
rescue OpenURI::HTTPError,
OpenSSL::SSL::SSLError,
Net::OpenTimeout,
Net::ReadTimeout,
Errno::ECONNREFUSED,
EOFError,
SocketError,
Discourse::InvalidParameters => e
logger.warn(
"Error encountered when trying to download file " +
"for group '#{group_name}'.\n#{e.class}: #{e.message}\n#{e.backtrace.join("\n")}"
)
end
count += 1
break if file || (file.blank? && count >= 3)
logger.info(
"Failed to download upload from #{url} for group '#{group_name}'. Retrying..."
)
sleep(count * sleep_interval)
end
next if file.blank?
upload = UploadCreator.new(
file,
"group_#{group_name}",
origin: UrlHelper.absolute(old_url)
).create_for(Discourse.system_user.id)
group.update_columns(flair_upload_id: upload.id, flair_url: nil) if upload.present?
end
end
private
def logger
Rails.logger
end
end
end

View File

@ -68,6 +68,7 @@ module Jobs
.joins("LEFT JOIN custom_emojis ce ON ce.upload_id = uploads.id")
.joins("LEFT JOIN theme_fields tf ON tf.upload_id = uploads.id")
.joins("LEFT JOIN user_exports ue ON ue.upload_id = uploads.id")
.joins("LEFT JOIN groups g ON g.flair_upload_id = uploads.id")
.where("pu.upload_id IS NULL")
.where("u.uploaded_avatar_id IS NULL")
.where("ua.gravatar_upload_id IS NULL AND ua.custom_upload_id IS NULL")
@ -76,6 +77,7 @@ module Jobs
.where("ce.upload_id IS NULL")
.where("tf.upload_id IS NULL")
.where("ue.upload_id IS NULL")
.where("g.flair_upload_id IS NULL")
.where("ss.value IS NULL")
result = result.where("uploads.url NOT IN (?)", ignore_urls) if ignore_urls.present?

View File

@ -27,6 +27,8 @@ class Group < ActiveRecord::Base
has_many :category_reviews, class_name: 'Category', foreign_key: :reviewable_by_group_id, dependent: :nullify
has_many :reviewables, foreign_key: :reviewable_by_group_id, dependent: :nullify
belongs_to :flair_upload, class_name: 'Upload'
has_and_belongs_to_many :web_hooks
before_save :downcase_incoming_email
@ -62,7 +64,6 @@ class Group < ActiveRecord::Base
validate :automatic_membership_email_domains_format_validator
validate :incoming_email_validator
validate :can_allow_membership_requests, if: :allow_membership_requests
validates :flair_url, url: true, if: Proc.new { |g| g.flair_url && g.flair_url.exclude?('fa-') }
validate :validate_grant_trust_level, if: :will_save_change_to_grant_trust_level?
AUTO_GROUPS = {
@ -741,6 +742,15 @@ class Group < ActiveRecord::Base
end
end
def flair_type
return :icon if flair_icon.present?
return :image if flair_upload_id.present?
end
def flair_url
flair_icon.presence || flair_upload&.url || self[:flair_url].presence
end
protected
def name_format_validator

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true
class GroupShowSerializer < BasicGroupSerializer
attributes :is_group_user, :is_group_owner, :is_group_owner_display, :mentionable, :messageable
attributes :is_group_user, :is_group_owner, :is_group_owner_display, :mentionable, :messageable, :flair_icon, :flair_type
def include_is_group_user?
authenticated?
@ -43,6 +43,14 @@ class GroupShowSerializer < BasicGroupSerializer
Group.messageable(scope.user).exists?(id: object.id)
end
def include_flair_icon?
is_group_owner && flair_icon.present?
end
def include_flair_type?
is_group_owner && flair_type.present?
end
private
def authenticated?

View File

@ -751,14 +751,16 @@ en:
title: "Muted"
description: "You will not be notified of anything about messages in this group."
flair_url: "Avatar Flair Image"
flair_url_placeholder: "(Optional) Image URL or Font Awesome class"
flair_url_description: 'Use square images no smaller than 20px by 20px or FontAwesome icons (accepted formats: "fa-icon", "far fa-icon" or "fab fa-icon").'
flair_upload_description: "Use square images no smaller than 20px by 20px."
flair_bg_color: "Avatar Flair Background Color"
flair_bg_color_placeholder: "(Optional) Hex color value"
flair_color: "Avatar Flair Color"
flair_color_placeholder: "(Optional) Hex color value"
flair_preview_icon: "Preview Icon"
flair_preview_image: "Preview Image"
flair_type:
icon: "Select an icon"
image: "Upload an image"
user_action_groups:
"1": "Likes"

View File

@ -0,0 +1,36 @@
# frozen_string_literal: true
class AddMoreFlairColumnsToGroup < ActiveRecord::Migration[6.0]
def change
add_column :groups, :flair_icon, :string
add_column :groups, :flair_upload_id, :integer
reversible do |dir|
dir.up do
DB.exec(
<<~SQL
UPDATE groups SET flair_icon = REPLACE(REPLACE(flair_url, 'fas fa-', ''), ' fa-', '-')
WHERE flair_url LIKE 'fa%'
SQL
)
DB.exec(
<<~SQL
UPDATE groups g1
SET flair_upload_id = u.id
FROM groups g2
INNER JOIN uploads u ON g2.flair_url ~ CONCAT('\/', u.sha1, '[\.\w]*')
WHERE g1.id = g2.id
SQL
)
DB.exec(
<<~SQL
UPDATE groups SET flair_url = NULL
WHERE flair_icon IS NOT NULL OR flair_upload_id IS NOT NULL
SQL
)
end
end
end
end

View File

@ -13,7 +13,7 @@ class PrimaryGroupLookup
private
def self.lookup_columns
@lookup_columns ||= %i{id name flair_url flair_bg_color flair_color}
@lookup_columns ||= %i{id name flair_icon flair_upload_id flair_bg_color flair_color}
end
def users
@ -29,7 +29,7 @@ class PrimaryGroupLookup
group_ids = users_with_primary_group.map(&:primary_group_id)
group_ids.uniq!
Group.where(id: group_ids).select(self.class.lookup_columns)
Group.includes(:flair_upload).where(id: group_ids).select(self.class.lookup_columns)
.each { |g| group_lookup[g.id] = g }
hash = {}

View File

@ -388,7 +388,7 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL
end
def self.group_icons
Group.where("flair_url LIKE '%fa-%'").pluck(:flair_url).uniq
Group.pluck(:flair_icon).uniq
end
def self.theme_icons(theme_ids)

View File

@ -216,12 +216,12 @@ describe SvgSprite do
end
it "includes Font Awesome 4.7 icons as group flair" do
group = Fabricate(:group, flair_url: "fa-air-freshener")
group = Fabricate(:group, flair_icon: "fa-air-freshener")
expect(SvgSprite.bundle).to match(/air-freshener/)
end
it "includes Font Awesome 5 icons as group flair" do
group = Fabricate(:group, flair_url: "far fa-building")
group = Fabricate(:group, flair_icon: "far fa-building")
expect(SvgSprite.bundle).to match(/building/)
end
end

View File

@ -0,0 +1,26 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe Jobs::MigrateGroupFlairImages do
let(:image_url) { "https://omg.aws.somestack/test.png" }
let(:group) { Fabricate(:group, flair_url: image_url) }
before do
stub_request(:get, image_url).to_return(
status: 200, body: file_from_fixtures("smallest.png").read
)
end
it 'should migrate to the new group `flair_upload_id` column correctly' do
group
expect do
described_class.new.execute_onceoff({})
end.to change { Upload.count }.by(1)
group.reload
expect(group.flair_upload).to eq(Upload.last)
expect(group[:flair_url]).to eq(nil)
end
end

View File

@ -674,7 +674,7 @@ describe GroupsController do
incoming_email: 'test@mail.org',
flair_bg_color: 'FFF',
flair_color: 'BBB',
flair_url: 'fa-adjust',
flair_icon: 'fa-adjust',
bio_raw: 'testing',
full_name: 'awesome team',
public_admission: true,