mirror of
https://github.com/discourse/discourse.git
synced 2025-03-14 10:33:43 +00:00
There are a number of minor changes in this commit : 1. Combine the "Themes" and "Components" links in the admin sidebar into a single tab labelled "Themes and components" 2. The combined tab links to the `/admin/config/customize/themes` page (titled as "Themes and components") 3. Add a new "Components" tab to the "Themes and components" page. There's already an existing "Themes" tab 4. Add a "back to" link at the top of individual theme/component page to navigate back to the respective tab in the "Themes and components" page 5. Remove the themes/components list/sidebar that currently serves for navigating between themes/components 6. Remove the header in the theme/component page Changes 4–6 apply only if the admin sidebar is enabled; they have no effect otherwise. Internal topic: t/146006.
536 lines
12 KiB
Ruby
536 lines
12 KiB
Ruby
# frozen_string_literal: true
|
|
require_relative "deprecated_icon_handler"
|
|
|
|
module SvgSprite
|
|
SVG_ICONS =
|
|
Set.new(
|
|
%w[
|
|
a
|
|
address-book
|
|
align-left
|
|
anchor
|
|
angle-down
|
|
angle-left
|
|
angle-right
|
|
angle-up
|
|
angles-down
|
|
angles-left
|
|
angles-right
|
|
angles-up
|
|
arrow-down
|
|
arrow-left
|
|
arrow-right
|
|
arrow-rotate-left
|
|
arrow-rotate-right
|
|
arrow-up
|
|
arrows-rotate
|
|
asterisk
|
|
at
|
|
backward
|
|
backward-fast
|
|
backward-step
|
|
ban
|
|
bars
|
|
bars-staggered
|
|
bed
|
|
bell
|
|
bell-slash
|
|
bold
|
|
book
|
|
book-open-reader
|
|
bookmark
|
|
bookmark-delete
|
|
box-archive
|
|
briefcase
|
|
bullseye
|
|
calendar-days
|
|
caret-down
|
|
caret-left
|
|
caret-right
|
|
caret-up
|
|
certificate
|
|
chart-bar
|
|
chart-pie
|
|
check
|
|
chevron-down
|
|
chevron-left
|
|
chevron-right
|
|
chevron-up
|
|
circle
|
|
circle-check
|
|
circle-chevron-down
|
|
circle-exclamation
|
|
circle-half-stroke
|
|
circle-info
|
|
circle-minus
|
|
circle-plus
|
|
circle-question
|
|
circle-xmark
|
|
clock
|
|
clock-rotate-left
|
|
cloud-arrow-up
|
|
code
|
|
comment
|
|
compress
|
|
copy
|
|
crosshairs
|
|
cube
|
|
desktop
|
|
diagram-project
|
|
discourse-amazon
|
|
discourse-bell-exclamation
|
|
discourse-bell-one
|
|
discourse-bell-slash
|
|
discourse-bookmark-clock
|
|
discourse-chevron-collapse
|
|
discourse-chevron-expand
|
|
discourse-compress
|
|
discourse-dnd
|
|
discourse-emojis
|
|
discourse-expand
|
|
discourse-other-tab
|
|
discourse-sidebar
|
|
discourse-sparkles
|
|
discourse-threads
|
|
download
|
|
earth-americas
|
|
ellipsis
|
|
ellipsis-vertical
|
|
envelope
|
|
eye
|
|
fab-android
|
|
fab-apple
|
|
fab-chrome
|
|
fab-discord
|
|
fab-discourse
|
|
fab-facebook
|
|
fab-facebook-square
|
|
fab-github
|
|
fab-instagram
|
|
fab-linkedin-in
|
|
fab-linux
|
|
fab-markdown
|
|
fab-threads
|
|
fab-threads-square
|
|
fab-twitter
|
|
fab-twitter-square
|
|
fab-x-twitter
|
|
fab-wikipedia-w
|
|
fab-windows
|
|
far-bell
|
|
far-bell-slash
|
|
far-calendar-plus
|
|
far-chart-bar
|
|
far-circle
|
|
far-circle-dot
|
|
far-clipboard
|
|
far-clock
|
|
far-comment
|
|
far-comments
|
|
far-copyright
|
|
far-envelope
|
|
far-eye
|
|
far-eye-slash
|
|
far-face-frown
|
|
far-face-meh
|
|
far-face-smile
|
|
far-file-lines
|
|
far-heart
|
|
far-image
|
|
far-moon
|
|
far-pen-to-square
|
|
far-rectangle-list
|
|
far-square
|
|
far-square-check
|
|
far-star
|
|
far-sun
|
|
far-thumbs-down
|
|
far-thumbs-up
|
|
far-trash-can
|
|
file
|
|
file-lines
|
|
filter
|
|
flag
|
|
flask
|
|
folder
|
|
folder-open
|
|
forward
|
|
forward-fast
|
|
forward-step
|
|
gavel
|
|
gear
|
|
gift
|
|
globe
|
|
grip-lines
|
|
hand-point-right
|
|
handshake-angle
|
|
hashtag
|
|
heart
|
|
hourglass-start
|
|
house
|
|
id-card
|
|
image
|
|
images
|
|
inbox
|
|
italic
|
|
key
|
|
keyboard
|
|
language
|
|
layer-group
|
|
left-right
|
|
link
|
|
link-slash
|
|
list
|
|
list-check
|
|
list-ol
|
|
list-ul
|
|
location-dot
|
|
lock
|
|
magnifying-glass
|
|
magnifying-glass-minus
|
|
magnifying-glass-plus
|
|
microphone-slash
|
|
minus
|
|
mobile-screen-button
|
|
moon
|
|
paintbrush
|
|
palette
|
|
paper-plane
|
|
pause
|
|
pencil
|
|
play
|
|
plug
|
|
plus
|
|
power-off
|
|
puzzle-piece
|
|
question
|
|
quote-left
|
|
quote-right
|
|
reply
|
|
right-from-bracket
|
|
right-left
|
|
right-to-bracket
|
|
robot
|
|
rocket
|
|
rotate
|
|
scroll
|
|
share
|
|
shield-halved
|
|
shuffle
|
|
signal
|
|
sliders
|
|
spinner
|
|
square-check
|
|
square-envelope
|
|
square-full
|
|
square-plus
|
|
star
|
|
sun
|
|
table
|
|
table-cells
|
|
table-columns
|
|
tag
|
|
tags
|
|
temperature-three-quarters
|
|
thumbs-down
|
|
thumbs-up
|
|
thumbtack
|
|
tippy-rounded-arrow
|
|
toggle-off
|
|
toggle-on
|
|
trash-can
|
|
triangle-exclamation
|
|
truck-medical
|
|
unlock
|
|
unlock-keyhole
|
|
up-down
|
|
up-right-from-square
|
|
upload
|
|
user
|
|
user-gear
|
|
user-group
|
|
user-pen
|
|
user-plus
|
|
user-secret
|
|
user-shield
|
|
user-xmark
|
|
users
|
|
wand-magic
|
|
wrench
|
|
xmark
|
|
],
|
|
)
|
|
|
|
CORE_SVG_SPRITES = Dir.glob("#{Rails.root}/vendor/assets/svg-icons/**/*.svg")
|
|
|
|
THEME_SPRITE_VAR_NAME = "icons-sprite"
|
|
|
|
MAX_THEME_SPRITE_SIZE = 1024.kilobytes
|
|
|
|
def self.preload
|
|
settings_icons
|
|
group_icons
|
|
badge_icons
|
|
end
|
|
|
|
def self.symbols_for(svg_filename, sprite, strict:)
|
|
if strict
|
|
Nokogiri.XML(sprite) { |config| config.options = Nokogiri::XML::ParseOptions::NOBLANKS }
|
|
else
|
|
Nokogiri.XML(sprite)
|
|
end.css("symbol")
|
|
.filter_map do |sym|
|
|
icon_id = prepare_symbol(sym, svg_filename)
|
|
if icon_id.present?
|
|
sym.attributes["id"].value = icon_id
|
|
sym.css("title").each(&:remove)
|
|
[icon_id, sym.to_xml]
|
|
end
|
|
end
|
|
.to_h
|
|
end
|
|
|
|
def self.core_svgs
|
|
@core_svgs ||=
|
|
CORE_SVG_SPRITES.reduce({}) do |symbols, path|
|
|
symbols.merge!(symbols_for(File.basename(path, ".svg"), File.read(path), strict: true))
|
|
end
|
|
end
|
|
|
|
# Just used in tests
|
|
def self.clear_plugin_svg_sprite_cache!
|
|
@plugin_svgs = nil
|
|
end
|
|
|
|
def self.plugin_svgs
|
|
@plugin_svgs ||=
|
|
begin
|
|
plugin_paths = []
|
|
Discourse
|
|
.plugins
|
|
.map { |plugin| File.dirname(plugin.path) }
|
|
.each { |path| plugin_paths << "#{path}/svg-icons/*.svg" }
|
|
|
|
custom_sprite_paths = Dir.glob(plugin_paths)
|
|
|
|
custom_sprite_paths.reduce({}) do |symbols, path|
|
|
symbols.merge!(symbols_for(File.basename(path, ".svg"), File.read(path), strict: true))
|
|
end
|
|
end
|
|
end
|
|
|
|
def self.theme_svgs(theme_id)
|
|
if theme_id.present?
|
|
cache
|
|
.defer_get_set_bulk(
|
|
Theme.transform_ids(theme_id),
|
|
lambda { |theme_id| "theme_svg_sprites_#{theme_id}" },
|
|
) do |theme_ids|
|
|
theme_field_uploads =
|
|
ThemeField.where(
|
|
type_id: ThemeField.types[:theme_upload_var],
|
|
name: THEME_SPRITE_VAR_NAME,
|
|
theme_id: theme_ids,
|
|
).pluck(:upload_id)
|
|
|
|
theme_sprites =
|
|
ThemeSvgSprite.where(theme_id: theme_ids).pluck(:theme_id, :upload_id, :sprite)
|
|
missing_sprites = (theme_field_uploads - theme_sprites.map(&:second))
|
|
|
|
if missing_sprites.present?
|
|
Rails.logger.warn(
|
|
"Missing ThemeSvgSprites for theme #{theme_id}, uploads #{missing_sprites.join(", ")}",
|
|
)
|
|
end
|
|
|
|
theme_sprites
|
|
.map do |(theme_id, upload_id, sprite)|
|
|
begin
|
|
[theme_id, symbols_for("theme_#{theme_id}_#{upload_id}.svg", sprite, strict: false)]
|
|
rescue => e
|
|
Rails.logger.warn(
|
|
"Bad XML in custom sprite in theme with ID=#{theme_id}. Error info: #{e.inspect}",
|
|
)
|
|
end
|
|
end
|
|
.compact
|
|
.to_h
|
|
.values_at(*theme_ids)
|
|
end
|
|
.values
|
|
.compact
|
|
.reduce({}) { |a, b| a.merge!(b) }
|
|
else
|
|
{}
|
|
end
|
|
end
|
|
|
|
def self.custom_svgs(theme_id)
|
|
plugin_svgs.merge(theme_svgs(theme_id))
|
|
end
|
|
|
|
def self.all_icons(theme_id = nil)
|
|
get_set_cache("icons_#{Theme.transform_ids(theme_id).join(",")}") do
|
|
Set
|
|
.new()
|
|
.merge(settings_icons)
|
|
.merge(plugin_icons)
|
|
.merge(badge_icons)
|
|
.merge(group_icons)
|
|
.merge(theme_icons(theme_id))
|
|
.merge(custom_icons(theme_id))
|
|
.delete_if { |i| i.blank? || i.include?("/") }
|
|
.map! { |i| process(i.dup) }
|
|
.merge(SVG_ICONS)
|
|
.sort
|
|
end
|
|
end
|
|
|
|
def self.version(theme_id = nil)
|
|
get_set_cache("version_#{Theme.transform_ids(theme_id).join(",")}") do
|
|
Digest::SHA1.hexdigest(bundle(theme_id))
|
|
end
|
|
end
|
|
|
|
def self.path(theme_id = nil)
|
|
"/svg-sprite/#{Discourse.current_hostname}/svg-#{theme_id}-#{version(theme_id)}.js"
|
|
end
|
|
|
|
def self.expire_cache
|
|
cache&.clear
|
|
end
|
|
|
|
def self.svgs_for(theme_id)
|
|
svgs = core_svgs
|
|
svgs = svgs.merge(custom_svgs(theme_id)) if theme_id.present?
|
|
svgs
|
|
end
|
|
|
|
def self.bundle(theme_id = nil)
|
|
icons = all_icons(theme_id)
|
|
|
|
svg_subset =
|
|
"" \
|
|
"<!--
|
|
Discourse SVG subset of Font Awesome Free by @fontawesome - https://fontawesome.com
|
|
License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
|
|
-->
|
|
<svg xmlns='http://www.w3.org/2000/svg' style='display: none;'>
|
|
" \
|
|
"".dup
|
|
|
|
svg_subset << core_svgs.slice(*icons).values.join
|
|
svg_subset << custom_svgs(theme_id).values.join
|
|
|
|
svg_subset << "</svg>"
|
|
end
|
|
|
|
def self.search(searched_icon)
|
|
searched_icon = process(searched_icon.dup)
|
|
|
|
svgs_for(SiteSetting.default_theme_id)[searched_icon] || false
|
|
end
|
|
|
|
def self.icon_picker_search(keyword, only_available = false)
|
|
symbols = svgs_for(SiteSetting.default_theme_id)
|
|
symbols.slice!(*all_icons(SiteSetting.default_theme_id)) if only_available
|
|
symbols.reject! { |icon_id, _sym| !icon_id.include?(keyword) } if keyword.present?
|
|
symbols.sort_by(&:first).map { |id, symbol| { id:, symbol: } }
|
|
end
|
|
|
|
# For use in no_ember .html.erb layouts
|
|
def self.raw_svg(name)
|
|
get_set_cache("raw_svg_#{name}") do
|
|
symbol = search(name)
|
|
break "" unless symbol
|
|
symbol = Nokogiri.XML(symbol).children.first
|
|
symbol.name = "svg"
|
|
<<~HTML
|
|
<svg class="fa d-icon svg-icon svg-node" aria-hidden="true">#{symbol}</svg>
|
|
HTML
|
|
end.html_safe
|
|
end
|
|
|
|
def self.theme_sprite_variable_name
|
|
THEME_SPRITE_VAR_NAME
|
|
end
|
|
|
|
def self.prepare_symbol(symbol, svg_filename = nil)
|
|
icon_id = symbol.attr("id")
|
|
|
|
case svg_filename
|
|
when "regular"
|
|
icon_id = icon_id.prepend("far-")
|
|
when "brands"
|
|
icon_id = icon_id.prepend("fab-")
|
|
end
|
|
|
|
icon_id
|
|
end
|
|
|
|
def self.settings_icons
|
|
get_set_cache("settings_icons") do
|
|
# includes svg_icon_subset and any settings containing _icon (incl. plugin settings)
|
|
site_setting_icons = []
|
|
|
|
SiteSetting.settings_hash.select do |key, value|
|
|
site_setting_icons |= value.split("|") if key.to_s.include?("_icon") && String === value
|
|
end
|
|
|
|
site_setting_icons
|
|
end
|
|
end
|
|
|
|
def self.plugin_icons
|
|
DiscoursePluginRegistry.svg_icons
|
|
end
|
|
|
|
def self.badge_icons
|
|
get_set_cache("badge_icons") { Badge.pluck(:icon).uniq }
|
|
end
|
|
|
|
def self.group_icons
|
|
get_set_cache("group_icons") { Group.pluck(:flair_icon).uniq }
|
|
end
|
|
|
|
def self.theme_icons(theme_id)
|
|
return [] if theme_id.blank?
|
|
|
|
theme_icon_settings = []
|
|
theme_ids = Theme.transform_ids(theme_id)
|
|
|
|
# Need to load full records for default values
|
|
Theme
|
|
.where(id: theme_ids)
|
|
.each do |theme|
|
|
_settings =
|
|
theme.cached_settings.each do |key, value|
|
|
if key.to_s.include?("_icon") && String === value
|
|
theme_icon_settings |= value.split("|")
|
|
end
|
|
end
|
|
end
|
|
|
|
theme_icon_settings |= ThemeModifierHelper.new(theme_ids: theme_ids).svg_icons
|
|
|
|
theme_icon_settings
|
|
end
|
|
|
|
def self.custom_icons(theme_id)
|
|
# Automatically register icons in sprites added via themes or plugins
|
|
custom_svgs(theme_id).keys
|
|
end
|
|
|
|
def self.process(icon_name)
|
|
DeprecatedIconHandler.convert_icon(icon_name.strip)
|
|
end
|
|
|
|
def self.get_set_cache(key, &block)
|
|
cache.defer_get_set(key, &block)
|
|
end
|
|
|
|
def self.cache
|
|
@cache ||= DistributedCache.new("svg_sprite")
|
|
end
|
|
end
|