Project

General

Profile

Feature #43087 » 0001-Allow-to-change-icons-sprite-from-theme-with-a-fallb.patch

Marius BĂLTEANU, 2026-02-04 04:14

View differences:

app/helpers/icons_helper.rb
18 18
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
19 19

  
20 20
module IconsHelper
21
  include Redmine::Themes::Helper
22

  
21 23
  DEFAULT_ICON_SIZE = "18"
22 24
  DEFAULT_SPRITE = "icons"
23 25

  
26
  def sprite_source(icon_name, sprite: DEFAULT_SPRITE, plugin: nil)
27
    if plugin
28
      "plugin_assets/#{plugin}/#{sprite}.svg"
29
    elsif current_theme && current_theme.icons(sprite).include?(icon_name)
30
      current_theme.image_path("#{sprite}.svg")
31
    else
32
      "#{sprite}.svg"
33
    end
34
  end
35

  
24 36
  def sprite_icon(icon_name, label = nil, icon_only: false, size: DEFAULT_ICON_SIZE, style: :outline, css_class: nil, sprite: DEFAULT_SPRITE, plugin: nil, rtl: false)
25
    sprite = plugin ? "plugin_assets/#{plugin}/#{sprite}.svg" : "#{sprite}.svg"
37
    sprite = sprite_source(icon_name, sprite: sprite, plugin: plugin)
26 38

  
27 39
    svg_icon = svg_sprite_icon(icon_name, size: size, style: style, css_class: css_class, sprite: sprite, rtl: rtl)
28 40

  
lib/redmine/themes.rb
110 110
        "themes/#{dir}/"
111 111
      end
112 112

  
113
      # Returns an array of icon names available in the given sprite
114
      def icons(sprite)
115
        asset = Rails.application.assets.load_path.find(image_path("#{sprite}.svg"))
116

  
117
        return [] unless asset
118

  
119
        ActionController::Base.cache_store.fetch("theme-icons/#{id}/#{sprite}/#{asset.digest}") do
120
          asset.content.scan(/id=['"]icon--([^'"]+)['"]/).flatten
121
        end
122
      end
123

  
113 124
      def asset_paths
114 125
        base_dir = Pathname.new(path)
115 126
        paths = base_dir.children.select do |child|
test/helpers/icons_helper_test.rb
43 43
    assert_match expected, icon
44 44
  end
45 45

  
46
  def test_sprite_source_without_theme_should_return_default_sprite
47
    stubs(:current_theme).returns(nil)
48

  
49
    assert_equal "icons.svg", sprite_source('edit')
50
  end
51

  
52
  def test_sprite_source_with_theme_and_sprite_image_should_return_theme_path_if_icon_exists
53
    theme = Redmine::Themes::Theme.new('/tmp/test')
54
    theme.stubs(:id).returns('test')
55
    theme.stubs(:image_path).with('icons.svg').returns('themes/test/icons.svg')
56

  
57
    asset = mock('asset')
58
    asset.stubs(:digest).returns('123456')
59
    asset.stubs(:content).returns('<symbol id="icon--edit"></symbol>')
60
    asset.stubs(:digested_path).returns('themes/test/icons-123456.svg')
61

  
62
    Rails.application.assets.load_path.stubs(:find).with('themes/test/icons.svg').returns(asset)
63
    stubs(:current_theme).returns(theme)
64

  
65
    assert_equal "themes/test/icons.svg", sprite_source('edit')
66
  end
67

  
68
  def test_sprite_source_with_theme_without_sprite_image_should_return_default_sprite
69
    theme = Redmine::Themes::Theme.new('/tmp/test')
70
    theme.stubs(:image_path).with('icons.svg').returns('themes/test/icons.svg')
71

  
72
    Rails.application.assets.load_path.stubs(:find).with('themes/test/icons.svg').returns(nil)
73
    stubs(:current_theme).returns(theme)
74

  
75
    assert_equal "icons.svg", sprite_source('edit')
76
  end
77

  
78
  def test_sprite_source_with_theme_and_sprite_image_but_missing_icon_should_fallback_to_default_sprite
79
    theme = Redmine::Themes::Theme.new('/tmp/test')
80
    theme.stubs(:id).returns('test')
81
    theme.stubs(:image_path).with('icons.svg').returns('themes/test/icons.svg')
82

  
83
    asset = mock('asset')
84
    asset.stubs(:digest).returns('123456')
85
    asset.stubs(:content).returns('<symbol id="icon--other"></symbol>')
86
    asset.stubs(:digested_path).returns('themes/test/icons-123456.svg')
87

  
88
    Rails.application.assets.load_path.stubs(:find).with('themes/test/icons.svg').returns(asset)
89
    stubs(:current_theme).returns(theme)
90

  
91
    assert_equal "icons.svg", sprite_source('edit')
92
  end
93

  
94
  def test_sprite_icon_with_theme_override_should_use_theme_sprite
95
    theme = Redmine::Themes::Theme.new('/tmp/test')
96
    theme.stubs(:id).returns('test')
97
    theme.stubs(:image_path).with('icons.svg').returns('themes/test/icons.svg')
98

  
99
    asset = mock('asset')
100
    asset.stubs(:digest).returns('123456')
101
    asset.stubs(:content).returns('<symbol id="icon--edit"></symbol>')
102
    asset.stubs(:digested_path).returns('themes/test/icons-123456.svg')
103

  
104
    Rails.application.assets.load_path.stubs(:find).with('themes/test/icons.svg').returns(asset)
105
    stubs(:current_theme).returns(theme)
106

  
107
    expected = %r{<svg class="s18 icon-svg" aria-hidden="true"><use href="/assets/themes/test/icons(-123456)?\.svg#icon--edit"></use></svg>$}
108
    assert_match expected, sprite_icon('edit')
109
  end
110

  
111
  def test_sprite_icon_with_theme_missing_icon_should_fallback_to_default_sprite
112
    theme = Redmine::Themes::Theme.new('/tmp/test')
113
    theme.stubs(:id).returns('test')
114
    theme.stubs(:image_path).with('icons.svg').returns('themes/test/icons.svg')
115

  
116
    asset = mock('asset')
117
    asset.stubs(:digest).returns('123456')
118
    asset.stubs(:content).returns('<symbol id="icon--other"></symbol>')
119
    asset.stubs(:digested_path).returns('themes/test/icons-123456.svg')
120

  
121
    Rails.application.assets.load_path.stubs(:find).with('themes/test/icons.svg').returns(asset)
122

  
123
    default_asset = mock('asset')
124
    default_asset.stubs(:digested_path).returns('icons-default.svg')
125
    Rails.application.assets.load_path.stubs(:find).with('icons.svg').returns(default_asset)
126

  
127
    stubs(:current_theme).returns(theme)
128

  
129
    expected = %r{<svg class="s18 icon-svg" aria-hidden="true"><use href="/assets/icons(-\w+)?\.svg#icon--edit"></use></svg>$}
130
    assert_match expected, sprite_icon('edit')
131
  end
132

  
46 133
  def test_sprite_icon_should_return_svg_with_custom_size
47 134
    expected = %r{<svg class="s24 icon-svg" aria-hidden="true"><use href="/assets/icons-\w+.svg#icon--edit"></use></svg>$}
48 135
    icon = sprite_icon('edit', size: '24')
test/unit/lib/redmine/themes_test.rb
59 59
  ensure
60 60
    Redmine::Themes.rescan
61 61
  end
62

  
63
  def test_icons_should_return_available_icons
64
    theme = Redmine::Themes::Theme.new('/tmp/test')
65
    theme.stubs(:image_path).with('icons.svg').returns('themes/test/icons.svg')
66

  
67
    asset = mock('asset')
68
    asset.stubs(:content).returns('<svg><symbol id="icon--edit"></symbol><symbol id=\'icon--delete\'></symbol></svg>')
69
    asset.stubs(:digest).returns('123456')
70

  
71
    Rails.application.assets.load_path.stubs(:find).with('themes/test/icons.svg').returns(asset)
72

  
73
    assert_equal ['edit', 'delete'], theme.icons('icons')
74
  end
75

  
76
  def test_icons_should_return_empty_array_if_asset_missing
77
    theme = Redmine::Themes::Theme.new('/tmp/test')
78
    theme.stubs(:image_path).with('icons.svg').returns('themes/test/icons.svg')
79

  
80
    Rails.application.assets.load_path.stubs(:find).with('themes/test/icons.svg').returns(nil)
81

  
82
    assert_equal [], theme.icons('icons')
83
  end
84

  
85
  def test_icons_should_be_cached
86
    theme = Redmine::Themes::Theme.new('/tmp/test')
87
    theme.stubs(:id).returns('test')
88
    theme.stubs(:image_path).with('icons.svg').returns('themes/test/icons.svg')
89

  
90
    asset = mock('asset')
91
    asset.stubs(:content).returns('<symbol id="icon--edit"></symbol>')
92
    asset.stubs(:digest).returns('123456')
93

  
94
    Rails.application.assets.load_path.stubs(:find).with('themes/test/icons.svg').returns(asset)
95

  
96
    # Use a memory store for this test since the test environment uses null_store
97
    memory_store = ActiveSupport::Cache.lookup_store(:memory_store)
98
    ActionController::Base.stubs(:cache_store).returns(memory_store)
99

  
100
    # First call - cache miss
101
    assert_equal ['edit'], theme.icons('icons')
102

  
103
    # Second call - verify it's in the cache
104
    cache_key = "theme-icons/test/icons/123456"
105
    assert_equal ['edit'], memory_store.read(cache_key)
106

  
107
    # If digest changes, it should miss cache
108
    asset.stubs(:digest).returns('789')
109
    asset.stubs(:content).returns('<symbol id="icon--new"></symbol>')
110
    assert_equal ['new'], theme.icons('icons')
111
  end
62 112
end
(4-4/4)