Project

General

Profile

Feature #43484 » 0004-Show-the-same-file-type-icon-whether-the-content-typ.patch

Go MAEDA, 2026-08-05 12:10

View differences:

app/helpers/icons_helper.rb
23 23
  DEFAULT_ICON_SIZE = "18"
24 24
  DEFAULT_SPRITE = "icons"
25 25

  
26
  # Content types covered by each icon. The same kind of file can be denoted by
27
  # several types, depending on whether the type was detected from the file
28
  # contents or taken from Redmine::MimeType, so list them all rather than
29
  # deriving the icon name from the type. An entry with no subtype (eg. audio)
30
  # covers every type of that top level type that is not listed elsewhere.
31
  ICON_MIME_TYPES = {
32
    'application-gzip' => %w(application/gzip),
33
    'application-javascript' => %w(application/javascript text/javascript),
34
    'application-pdf' => %w(application/pdf),
35
    'application-zip' => %w(application/zip),
36
    'file-music' => %w(audio),
37
    'movie' => %w(video),
38
    'photo' => %w(image),
39
    'text-css' => %w(text/css),
40
    'text-html' => %w(text/html),
41
    'text-plain' => %w(text application/sql application/x-csh application/x-sh),
42
    'text-x-c' => %w(text/x-c text/x-c++hdr text/x-c++src),
43
    'text-x-csharp' => %w(text/x-csharp),
44
    'text-x-java' => %w(text/x-java text/x-java-source),
45
    'text-x-php' => %w(text/x-php),
46
    'text-x-ruby' => %w(text/x-ruby),
47
    'text-xml' => %w(text/xml application/xml),
48
    # Microsoft Office Open XML documents
49
    # Do not add legacy Office formats (.doc, .xls, .ppt) here because
50
    # Redmine does not provide attachment preview for these formats.
51
    'file-type-docx' => %w(application/vnd.openxmlformats-officedocument.wordprocessingml.document),
52
    'file-type-ppt' => %w(application/vnd.openxmlformats-officedocument.presentationml.presentation),
53
    'file-type-xls' => %w(application/vnd.openxmlformats-officedocument.spreadsheetml.sheet)
54
  }.freeze
55

  
56
  MIME_TYPE_ICONS = ICON_MIME_TYPES.each_with_object({}) do |(icon, types), map|
57
    types.each {|type| map[type] = icon}
58
  end.freeze
59

  
26 60
  def sprite_source(icon_name, sprite: DEFAULT_SPRITE, plugin: nil)
27 61
    if plugin
28 62
      "plugin_assets/#{plugin}/#{sprite}.svg"
......
102 136
    sprite_icon(icon_name, **)
103 137
  end
104 138

  
105
  def file_type_icon(mime_type, ...)
106
    icon_name = icon_for_mime_type(mime_type)
107
    sprite_icon(icon_name, ...)
139
  def file_type_icon(mime_type, label = nil, filename: nil, **)
140
    icon_name = icon_for_mime_type(mime_type, filename)
141
    sprite_icon(icon_name, label, **)
108 142
  end
109 143

  
110 144
  private
......
125 159
    )
126 160
  end
127 161

  
128
  def icon_for_mime_type(mime)
129
    if %w(text/x-c text/x-csharp text/x-java text/x-php
130
          text/x-ruby text/xml text/css text/html text/css text/html
131
          application/pdf application/zip application/gzip application/javascript).include?(mime)
132
      icon_name = mime.tr('/', '-')
133
    else
134
      top_level_type, subtype = mime.to_s.split('/')
135
      icon_name =
136
        case top_level_type
137
        when 'audio' then 'file-music'
138
        when 'image' then 'photo'
139
        when 'text'
140
          %w(markdown plain x-textile).include?(subtype) ? 'text-plain' : nil
141
        when 'video' then 'movie'
142
        else
143
          # MIME type mapping
144
          {
145
            # Microsoft Office Open XML documents
146
            # Do not add legacy Office formats (.doc, .xls, .ppt) here because
147
            # Redmine does not provide attachment preview for these formats.
148
            'application/vnd.openxmlformats-officedocument.presentationml.presentation' => 'file-type-ppt',
149
            'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' => 'file-type-xls',
150
            'application/vnd.openxmlformats-officedocument.wordprocessingml.document' => 'file-type-docx'
151
          }[mime.to_s]
152
        end
162
  def icon_for_mime_type(mime, filename = nil)
163
    # Content type detection falls back to application/octet-stream when it
164
    # cannot tell the type from the file contents. In that case only, guess
165
    # the type from the filename, as AttachmentsController#detect_content_type
166
    # does.
167
    if filename.present? && (mime.blank? || mime == 'application/octet-stream')
168
      mime = Redmine::MimeType.of(filename)
153 169
    end
154
    icon_name || 'file'
170

  
171
    MIME_TYPE_ICONS[mime] ||
172
      MIME_TYPE_ICONS[mime.to_s.split('/').first] ||
173
      'file'
155 174
  end
156 175
end
app/views/attachments/_links.html.erb
15 15
<% for attachment in attachments %>
16 16
<tr>
17 17
  <td>
18
    <%= link_to_attachment attachment, class: 'icon icon-attachment ', icon: icon_for_mime_type(attachment.content_type) -%>
18
    <%= link_to_attachment attachment, class: 'icon icon-attachment ', icon: icon_for_mime_type(attachment.content_type, attachment.filename) -%>
19 19
    <span class="size">(<%= number_to_human_size attachment.filesize %>)</span>
20 20
    <%= link_to_attachment attachment, class: 'icon-only icon-download ', title: l(:button_download), download: true, icon: 'download' -%>
21 21
  </td>
app/views/attachments/edit_all.html.erb
11 11
  <% @attachments.each do |attachment| %>
12 12
    <tr>
13 13
      <td colspan="2">
14
        <span class="icon icon-attachment"><%= file_type_icon(attachment.content_type_was, attachment.filename_was) %></span>
14
        <span class="icon icon-attachment"><%= file_type_icon(attachment.content_type_was, attachment.filename_was, filename: attachment.filename_was) %></span>
15 15
        <span class="size">(<%= number_to_human_size attachment.filesize %>)</span>
16 16
        <span class="author"><%= attachment.author %>, <%= format_time(attachment.created_on) %></span>
17 17
      </td>
app/views/issues/_edit.html.erb
55 55
        <div id="existing-attachments" style="<%= @issue.deleted_attachment_ids.blank? ? 'display:none;' : '' %>">
56 56
          <% @issue.attachments.each do |attachment| %>
57 57
          <span class="existing-attachment">
58
            <%= file_type_icon(attachment.content_type, size: 12) %>
58
            <%= file_type_icon(attachment.content_type, filename: attachment.filename, size: 12) %>
59 59
            <%= text_field_tag '', attachment.filename, :class => "icon icon-attachment filename", :disabled => true %>
60 60
            <label>
61 61
              <%= check_box_tag 'issue[deleted_attachment_ids][]',
app/views/wiki/edit.html.erb
41 41
<div id="existing-attachments" style="<%= @page.deleted_attachment_ids.blank? ? 'display:none;' : '' %>">
42 42
  <% @page.attachments.each do |attachment| %>
43 43
  <span class="existing-attachment">
44
    <%= file_type_icon(attachment.content_type, size: 12) %>
44
    <%= file_type_icon(attachment.content_type, filename: attachment.filename, size: 12) %>
45 45
    <%= text_field_tag '', attachment.filename, :class => "icon icon-attachment filename", :disabled => true %>
46 46
    <label class='inline'>
47 47
      <%= check_box_tag 'wiki_page[deleted_attachment_ids][]',
test/helpers/icons_helper_test.rb
242 242
    assert_equal 'movie', icon_for_mime_type('video/mp4')
243 243
  end
244 244

  
245
  def test_icon_for_mime_type_should_return_the_same_icon_for_equivalent_mime_types
246
    # Types detected from the file contents and their Redmine::MimeType counterparts
247
    {
248
      'text/javascript' => 'application/javascript',
249
      'application/xml' => 'text/xml',
250
      'text/x-c++src' => 'text/x-c',
251
      'text/x-java-source' => 'text/x-java',
252
      'text/x-diff' => 'text/plain',
253
      'application/sql' => 'text/plain'
254
    }.each do |detected, counterpart|
255
      assert_equal icon_for_mime_type(counterpart), icon_for_mime_type(detected),
256
                   "#{detected} should get the same icon as #{counterpart}"
257
    end
258
  end
259

  
260
  def test_icon_for_mime_type_should_fall_back_to_filename_only_when_type_is_undetermined
261
    assert_equal 'text-plain', icon_for_mime_type('application/octet-stream', 'readme.textile')
262
    assert_equal 'application-pdf', icon_for_mime_type(nil, 'document.pdf')
263
    # A type detected from the file contents takes precedence over the filename
264
    assert_equal 'text-html', icon_for_mime_type('text/html', 'not-really-an-image.png')
265
  end
266

  
245 267
  def test_icon_for_mime_type_should_return_generic_file_icon_for_unknown_mime_types
246 268
    assert_equal 'file', icon_for_mime_type('unknown-type')
247 269
  end
(8-8/10)