##// END OF EJS Templates
Improved responsiveness for versions and roadmap (#19097)....
Jean-Philippe Lang -
r14469:ecb1f660ac4d
parent child
Show More
@@ -1,1338 +1,1337
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 4 # Copyright (C) 2006-2015 Jean-Philippe Lang
5 5 #
6 6 # This program is free software; you can redistribute it and/or
7 7 # modify it under the terms of the GNU General Public License
8 8 # as published by the Free Software Foundation; either version 2
9 9 # of the License, or (at your option) any later version.
10 10 #
11 11 # This program is distributed in the hope that it will be useful,
12 12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 14 # GNU General Public License for more details.
15 15 #
16 16 # You should have received a copy of the GNU General Public License
17 17 # along with this program; if not, write to the Free Software
18 18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 19
20 20 require 'forwardable'
21 21 require 'cgi'
22 22
23 23 module ApplicationHelper
24 24 include Redmine::WikiFormatting::Macros::Definitions
25 25 include Redmine::I18n
26 26 include GravatarHelper::PublicMethods
27 27 include Redmine::Pagination::Helper
28 28 include Redmine::SudoMode::Helper
29 29 include Redmine::Themes::Helper
30 30 include Redmine::Hook::Helper
31 31
32 32 extend Forwardable
33 33 def_delegators :wiki_helper, :wikitoolbar_for, :heads_for_wiki_formatter
34 34
35 35 # Return true if user is authorized for controller/action, otherwise false
36 36 def authorize_for(controller, action)
37 37 User.current.allowed_to?({:controller => controller, :action => action}, @project)
38 38 end
39 39
40 40 # Display a link if user is authorized
41 41 #
42 42 # @param [String] name Anchor text (passed to link_to)
43 43 # @param [Hash] options Hash params. This will checked by authorize_for to see if the user is authorized
44 44 # @param [optional, Hash] html_options Options passed to link_to
45 45 # @param [optional, Hash] parameters_for_method_reference Extra parameters for link_to
46 46 def link_to_if_authorized(name, options = {}, html_options = nil, *parameters_for_method_reference)
47 47 link_to(name, options, html_options, *parameters_for_method_reference) if authorize_for(options[:controller] || params[:controller], options[:action])
48 48 end
49 49
50 50 # Displays a link to user's account page if active
51 51 def link_to_user(user, options={})
52 52 if user.is_a?(User)
53 53 name = h(user.name(options[:format]))
54 54 if user.active? || (User.current.admin? && user.logged?)
55 55 link_to name, user_path(user), :class => user.css_classes
56 56 else
57 57 name
58 58 end
59 59 else
60 60 h(user.to_s)
61 61 end
62 62 end
63 63
64 64 # Displays a link to +issue+ with its subject.
65 65 # Examples:
66 66 #
67 67 # link_to_issue(issue) # => Defect #6: This is the subject
68 68 # link_to_issue(issue, :truncate => 6) # => Defect #6: This i...
69 69 # link_to_issue(issue, :subject => false) # => Defect #6
70 70 # link_to_issue(issue, :project => true) # => Foo - Defect #6
71 71 # link_to_issue(issue, :subject => false, :tracker => false) # => #6
72 72 #
73 73 def link_to_issue(issue, options={})
74 74 title = nil
75 75 subject = nil
76 76 text = options[:tracker] == false ? "##{issue.id}" : "#{issue.tracker} ##{issue.id}"
77 77 if options[:subject] == false
78 78 title = issue.subject.truncate(60)
79 79 else
80 80 subject = issue.subject
81 81 if truncate_length = options[:truncate]
82 82 subject = subject.truncate(truncate_length)
83 83 end
84 84 end
85 85 only_path = options[:only_path].nil? ? true : options[:only_path]
86 86 s = link_to(text, issue_url(issue, :only_path => only_path),
87 87 :class => issue.css_classes, :title => title)
88 88 s << h(": #{subject}") if subject
89 89 s = h("#{issue.project} - ") + s if options[:project]
90 90 s
91 91 end
92 92
93 93 # Generates a link to an attachment.
94 94 # Options:
95 95 # * :text - Link text (default to attachment filename)
96 96 # * :download - Force download (default: false)
97 97 def link_to_attachment(attachment, options={})
98 98 text = options.delete(:text) || attachment.filename
99 99 route_method = options.delete(:download) ? :download_named_attachment_url : :named_attachment_url
100 100 html_options = options.slice!(:only_path)
101 101 options[:only_path] = true unless options.key?(:only_path)
102 102 url = send(route_method, attachment, attachment.filename, options)
103 103 link_to text, url, html_options
104 104 end
105 105
106 106 # Generates a link to a SCM revision
107 107 # Options:
108 108 # * :text - Link text (default to the formatted revision)
109 109 def link_to_revision(revision, repository, options={})
110 110 if repository.is_a?(Project)
111 111 repository = repository.repository
112 112 end
113 113 text = options.delete(:text) || format_revision(revision)
114 114 rev = revision.respond_to?(:identifier) ? revision.identifier : revision
115 115 link_to(
116 116 h(text),
117 117 {:controller => 'repositories', :action => 'revision', :id => repository.project, :repository_id => repository.identifier_param, :rev => rev},
118 118 :title => l(:label_revision_id, format_revision(revision)),
119 119 :accesskey => options[:accesskey]
120 120 )
121 121 end
122 122
123 123 # Generates a link to a message
124 124 def link_to_message(message, options={}, html_options = nil)
125 125 link_to(
126 126 message.subject.truncate(60),
127 127 board_message_url(message.board_id, message.parent_id || message.id, {
128 128 :r => (message.parent_id && message.id),
129 129 :anchor => (message.parent_id ? "message-#{message.id}" : nil),
130 130 :only_path => true
131 131 }.merge(options)),
132 132 html_options
133 133 )
134 134 end
135 135
136 136 # Generates a link to a project if active
137 137 # Examples:
138 138 #
139 139 # link_to_project(project) # => link to the specified project overview
140 140 # link_to_project(project, {:only_path => false}, :class => "project") # => 3rd arg adds html options
141 141 # link_to_project(project, {}, :class => "project") # => html options with default url (project overview)
142 142 #
143 143 def link_to_project(project, options={}, html_options = nil)
144 144 if project.archived?
145 145 h(project.name)
146 146 else
147 147 link_to project.name,
148 148 project_url(project, {:only_path => true}.merge(options)),
149 149 html_options
150 150 end
151 151 end
152 152
153 153 # Generates a link to a project settings if active
154 154 def link_to_project_settings(project, options={}, html_options=nil)
155 155 if project.active?
156 156 link_to project.name, settings_project_path(project, options), html_options
157 157 elsif project.archived?
158 158 h(project.name)
159 159 else
160 160 link_to project.name, project_path(project, options), html_options
161 161 end
162 162 end
163 163
164 164 # Generates a link to a version
165 165 def link_to_version(version, options = {})
166 166 return '' unless version && version.is_a?(Version)
167 167 options = {:title => format_date(version.effective_date)}.merge(options)
168 168 link_to_if version.visible?, format_version_name(version), version_path(version), options
169 169 end
170 170
171 171 # Helper that formats object for html or text rendering
172 172 def format_object(object, html=true, &block)
173 173 if block_given?
174 174 object = yield object
175 175 end
176 176 case object.class.name
177 177 when 'Array'
178 178 object.map {|o| format_object(o, html)}.join(', ').html_safe
179 179 when 'Time'
180 180 format_time(object)
181 181 when 'Date'
182 182 format_date(object)
183 183 when 'Fixnum'
184 184 object.to_s
185 185 when 'Float'
186 186 sprintf "%.2f", object
187 187 when 'User'
188 188 html ? link_to_user(object) : object.to_s
189 189 when 'Project'
190 190 html ? link_to_project(object) : object.to_s
191 191 when 'Version'
192 192 html ? link_to_version(object) : object.to_s
193 193 when 'TrueClass'
194 194 l(:general_text_Yes)
195 195 when 'FalseClass'
196 196 l(:general_text_No)
197 197 when 'Issue'
198 198 object.visible? && html ? link_to_issue(object) : "##{object.id}"
199 199 when 'CustomValue', 'CustomFieldValue'
200 200 if object.custom_field
201 201 f = object.custom_field.format.formatted_custom_value(self, object, html)
202 202 if f.nil? || f.is_a?(String)
203 203 f
204 204 else
205 205 format_object(f, html, &block)
206 206 end
207 207 else
208 208 object.value.to_s
209 209 end
210 210 else
211 211 html ? h(object) : object.to_s
212 212 end
213 213 end
214 214
215 215 def wiki_page_path(page, options={})
216 216 url_for({:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title}.merge(options))
217 217 end
218 218
219 219 def thumbnail_tag(attachment)
220 220 link_to image_tag(thumbnail_path(attachment)),
221 221 named_attachment_path(attachment, attachment.filename),
222 222 :title => attachment.filename
223 223 end
224 224
225 225 def toggle_link(name, id, options={})
226 226 onclick = "$('##{id}').toggle(); "
227 227 onclick << (options[:focus] ? "$('##{options[:focus]}').focus(); " : "this.blur(); ")
228 228 onclick << "return false;"
229 229 link_to(name, "#", :onclick => onclick)
230 230 end
231 231
232 232 def format_activity_title(text)
233 233 h(truncate_single_line_raw(text, 100))
234 234 end
235 235
236 236 def format_activity_day(date)
237 237 date == User.current.today ? l(:label_today).titleize : format_date(date)
238 238 end
239 239
240 240 def format_activity_description(text)
241 241 h(text.to_s.truncate(120).gsub(%r{[\r\n]*<(pre|code)>.*$}m, '...')
242 242 ).gsub(/[\r\n]+/, "<br />").html_safe
243 243 end
244 244
245 245 def format_version_name(version)
246 246 if version.project == @project
247 247 h(version)
248 248 else
249 249 h("#{version.project} - #{version}")
250 250 end
251 251 end
252 252
253 253 def due_date_distance_in_words(date)
254 254 if date
255 255 l((date < Date.today ? :label_roadmap_overdue : :label_roadmap_due_in), distance_of_date_in_words(Date.today, date))
256 256 end
257 257 end
258 258
259 259 # Renders a tree of projects as a nested set of unordered lists
260 260 # The given collection may be a subset of the whole project tree
261 261 # (eg. some intermediate nodes are private and can not be seen)
262 262 def render_project_nested_lists(projects, &block)
263 263 s = ''
264 264 if projects.any?
265 265 ancestors = []
266 266 original_project = @project
267 267 projects.sort_by(&:lft).each do |project|
268 268 # set the project environment to please macros.
269 269 @project = project
270 270 if (ancestors.empty? || project.is_descendant_of?(ancestors.last))
271 271 s << "<ul class='projects #{ ancestors.empty? ? 'root' : nil}'>\n"
272 272 else
273 273 ancestors.pop
274 274 s << "</li>"
275 275 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
276 276 ancestors.pop
277 277 s << "</ul></li>\n"
278 278 end
279 279 end
280 280 classes = (ancestors.empty? ? 'root' : 'child')
281 281 s << "<li class='#{classes}'><div class='#{classes}'>"
282 282 s << h(block_given? ? capture(project, &block) : project.name)
283 283 s << "</div>\n"
284 284 ancestors << project
285 285 end
286 286 s << ("</li></ul>\n" * ancestors.size)
287 287 @project = original_project
288 288 end
289 289 s.html_safe
290 290 end
291 291
292 292 def render_page_hierarchy(pages, node=nil, options={})
293 293 content = ''
294 294 if pages[node]
295 295 content << "<ul class=\"pages-hierarchy\">\n"
296 296 pages[node].each do |page|
297 297 content << "<li>"
298 298 content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title, :version => nil},
299 299 :title => (options[:timestamp] && page.updated_on ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil))
300 300 content << "\n" + render_page_hierarchy(pages, page.id, options) if pages[page.id]
301 301 content << "</li>\n"
302 302 end
303 303 content << "</ul>\n"
304 304 end
305 305 content.html_safe
306 306 end
307 307
308 308 # Renders flash messages
309 309 def render_flash_messages
310 310 s = ''
311 311 flash.each do |k,v|
312 312 s << content_tag('div', v.html_safe, :class => "flash #{k}", :id => "flash_#{k}")
313 313 end
314 314 s.html_safe
315 315 end
316 316
317 317 # Renders tabs and their content
318 318 def render_tabs(tabs, selected=params[:tab])
319 319 if tabs.any?
320 320 unless tabs.detect {|tab| tab[:name] == selected}
321 321 selected = nil
322 322 end
323 323 selected ||= tabs.first[:name]
324 324 render :partial => 'common/tabs', :locals => {:tabs => tabs, :selected_tab => selected}
325 325 else
326 326 content_tag 'p', l(:label_no_data), :class => "nodata"
327 327 end
328 328 end
329 329
330 330 # Renders the project quick-jump box
331 331 def render_project_jump_box
332 332 return unless User.current.logged?
333 333 projects = User.current.projects.active.select(:id, :name, :identifier, :lft, :rgt).to_a
334 334 if projects.any?
335 335 options =
336 336 ("<option value=''>#{ l(:label_jump_to_a_project) }</option>" +
337 337 '<option value="" disabled="disabled">---</option>').html_safe
338 338
339 339 options << project_tree_options_for_select(projects, :selected => @project) do |p|
340 340 { :value => project_path(:id => p, :jump => current_menu_item) }
341 341 end
342 342
343 343 content_tag( :span, nil, :class => 'jump-box-arrow') +
344 344 select_tag('project_quick_jump_box', options, :onchange => 'if (this.value != \'\') { window.location = this.value; }')
345 345 end
346 346 end
347 347
348 348 def project_tree_options_for_select(projects, options = {})
349 349 s = ''.html_safe
350 350 if blank_text = options[:include_blank]
351 351 if blank_text == true
352 352 blank_text = '&nbsp;'.html_safe
353 353 end
354 354 s << content_tag('option', blank_text, :value => '')
355 355 end
356 356 project_tree(projects) do |project, level|
357 357 name_prefix = (level > 0 ? '&nbsp;' * 2 * level + '&#187; ' : '').html_safe
358 358 tag_options = {:value => project.id}
359 359 if project == options[:selected] || (options[:selected].respond_to?(:include?) && options[:selected].include?(project))
360 360 tag_options[:selected] = 'selected'
361 361 else
362 362 tag_options[:selected] = nil
363 363 end
364 364 tag_options.merge!(yield(project)) if block_given?
365 365 s << content_tag('option', name_prefix + h(project), tag_options)
366 366 end
367 367 s.html_safe
368 368 end
369 369
370 370 # Yields the given block for each project with its level in the tree
371 371 #
372 372 # Wrapper for Project#project_tree
373 373 def project_tree(projects, &block)
374 374 Project.project_tree(projects, &block)
375 375 end
376 376
377 377 def principals_check_box_tags(name, principals)
378 378 s = ''
379 379 principals.each do |principal|
380 380 s << "<label>#{ check_box_tag name, principal.id, false, :id => nil } #{h principal}</label>\n"
381 381 end
382 382 s.html_safe
383 383 end
384 384
385 385 # Returns a string for users/groups option tags
386 386 def principals_options_for_select(collection, selected=nil)
387 387 s = ''
388 388 if collection.include?(User.current)
389 389 s << content_tag('option', "<< #{l(:label_me)} >>", :value => User.current.id)
390 390 end
391 391 groups = ''
392 392 collection.sort.each do |element|
393 393 selected_attribute = ' selected="selected"' if option_value_selected?(element, selected) || element.id.to_s == selected
394 394 (element.is_a?(Group) ? groups : s) << %(<option value="#{element.id}"#{selected_attribute}>#{h element.name}</option>)
395 395 end
396 396 unless groups.empty?
397 397 s << %(<optgroup label="#{h(l(:label_group_plural))}">#{groups}</optgroup>)
398 398 end
399 399 s.html_safe
400 400 end
401 401
402 402 def option_tag(name, text, value, selected=nil, options={})
403 403 content_tag 'option', value, options.merge(:value => value, :selected => (value == selected))
404 404 end
405 405
406 406 def truncate_single_line_raw(string, length)
407 407 string.to_s.truncate(length).gsub(%r{[\r\n]+}m, ' ')
408 408 end
409 409
410 410 # Truncates at line break after 250 characters or options[:length]
411 411 def truncate_lines(string, options={})
412 412 length = options[:length] || 250
413 413 if string.to_s =~ /\A(.{#{length}}.*?)$/m
414 414 "#{$1}..."
415 415 else
416 416 string
417 417 end
418 418 end
419 419
420 420 def anchor(text)
421 421 text.to_s.gsub(' ', '_')
422 422 end
423 423
424 424 def html_hours(text)
425 425 text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>').html_safe
426 426 end
427 427
428 428 def authoring(created, author, options={})
429 429 l(options[:label] || :label_added_time_by, :author => link_to_user(author), :age => time_tag(created)).html_safe
430 430 end
431 431
432 432 def time_tag(time)
433 433 text = distance_of_time_in_words(Time.now, time)
434 434 if @project
435 435 link_to(text, project_activity_path(@project, :from => User.current.time_to_date(time)), :title => format_time(time))
436 436 else
437 437 content_tag('abbr', text, :title => format_time(time))
438 438 end
439 439 end
440 440
441 441 def syntax_highlight_lines(name, content)
442 442 lines = []
443 443 syntax_highlight(name, content).each_line { |line| lines << line }
444 444 lines
445 445 end
446 446
447 447 def syntax_highlight(name, content)
448 448 Redmine::SyntaxHighlighting.highlight_by_filename(content, name)
449 449 end
450 450
451 451 def to_path_param(path)
452 452 str = path.to_s.split(%r{[/\\]}).select{|p| !p.blank?}.join("/")
453 453 str.blank? ? nil : str
454 454 end
455 455
456 456 def reorder_links(name, url, method = :post)
457 457 link_to(image_tag('2uparrow.png', :alt => l(:label_sort_highest)),
458 458 url.merge({"#{name}[move_to]" => 'highest'}),
459 459 :method => method, :title => l(:label_sort_highest)) +
460 460 link_to(image_tag('1uparrow.png', :alt => l(:label_sort_higher)),
461 461 url.merge({"#{name}[move_to]" => 'higher'}),
462 462 :method => method, :title => l(:label_sort_higher)) +
463 463 link_to(image_tag('1downarrow.png', :alt => l(:label_sort_lower)),
464 464 url.merge({"#{name}[move_to]" => 'lower'}),
465 465 :method => method, :title => l(:label_sort_lower)) +
466 466 link_to(image_tag('2downarrow.png', :alt => l(:label_sort_lowest)),
467 467 url.merge({"#{name}[move_to]" => 'lowest'}),
468 468 :method => method, :title => l(:label_sort_lowest))
469 469 end
470 470
471 471 def breadcrumb(*args)
472 472 elements = args.flatten
473 473 elements.any? ? content_tag('p', (args.join(" \xc2\xbb ") + " \xc2\xbb ").html_safe, :class => 'breadcrumb') : nil
474 474 end
475 475
476 476 def other_formats_links(&block)
477 477 concat('<p class="other-formats">'.html_safe + l(:label_export_to))
478 478 yield Redmine::Views::OtherFormatsBuilder.new(self)
479 479 concat('</p>'.html_safe)
480 480 end
481 481
482 482 def page_header_title
483 483 if @project.nil? || @project.new_record?
484 484 h(Setting.app_title)
485 485 else
486 486 b = []
487 487 ancestors = (@project.root? ? [] : @project.ancestors.visible.to_a)
488 488 if ancestors.any?
489 489 root = ancestors.shift
490 490 b << link_to_project(root, {:jump => current_menu_item}, :class => 'root')
491 491 if ancestors.size > 2
492 492 b << "\xe2\x80\xa6"
493 493 ancestors = ancestors[-2, 2]
494 494 end
495 495 b += ancestors.collect {|p| link_to_project(p, {:jump => current_menu_item}, :class => 'ancestor') }
496 496 end
497 497 b << h(@project)
498 498 b.join(" \xc2\xbb ").html_safe
499 499 end
500 500 end
501 501
502 502 # Returns a h2 tag and sets the html title with the given arguments
503 503 def title(*args)
504 504 strings = args.map do |arg|
505 505 if arg.is_a?(Array) && arg.size >= 2
506 506 link_to(*arg)
507 507 else
508 508 h(arg.to_s)
509 509 end
510 510 end
511 511 html_title args.reverse.map {|s| (s.is_a?(Array) ? s.first : s).to_s}
512 512 content_tag('h2', strings.join(' &#187; ').html_safe)
513 513 end
514 514
515 515 # Sets the html title
516 516 # Returns the html title when called without arguments
517 517 # Current project name and app_title and automatically appended
518 518 # Exemples:
519 519 # html_title 'Foo', 'Bar'
520 520 # html_title # => 'Foo - Bar - My Project - Redmine'
521 521 def html_title(*args)
522 522 if args.empty?
523 523 title = @html_title || []
524 524 title << @project.name if @project
525 525 title << Setting.app_title unless Setting.app_title == title.last
526 526 title.reject(&:blank?).join(' - ')
527 527 else
528 528 @html_title ||= []
529 529 @html_title += args
530 530 end
531 531 end
532 532
533 533 # Returns the theme, controller name, and action as css classes for the
534 534 # HTML body.
535 535 def body_css_classes
536 536 css = []
537 537 if theme = Redmine::Themes.theme(Setting.ui_theme)
538 538 css << 'theme-' + theme.name
539 539 end
540 540
541 541 css << 'project-' + @project.identifier if @project && @project.identifier.present?
542 542 css << 'controller-' + controller_name
543 543 css << 'action-' + action_name
544 544 css.join(' ')
545 545 end
546 546
547 547 def accesskey(s)
548 548 @used_accesskeys ||= []
549 549 key = Redmine::AccessKeys.key_for(s)
550 550 return nil if @used_accesskeys.include?(key)
551 551 @used_accesskeys << key
552 552 key
553 553 end
554 554
555 555 # Formats text according to system settings.
556 556 # 2 ways to call this method:
557 557 # * with a String: textilizable(text, options)
558 558 # * with an object and one of its attribute: textilizable(issue, :description, options)
559 559 def textilizable(*args)
560 560 options = args.last.is_a?(Hash) ? args.pop : {}
561 561 case args.size
562 562 when 1
563 563 obj = options[:object]
564 564 text = args.shift
565 565 when 2
566 566 obj = args.shift
567 567 attr = args.shift
568 568 text = obj.send(attr).to_s
569 569 else
570 570 raise ArgumentError, 'invalid arguments to textilizable'
571 571 end
572 572 return '' if text.blank?
573 573 project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
574 574 @only_path = only_path = options.delete(:only_path) == false ? false : true
575 575
576 576 text = text.dup
577 577 macros = catch_macros(text)
578 578 text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr)
579 579
580 580 @parsed_headings = []
581 581 @heading_anchors = {}
582 582 @current_section = 0 if options[:edit_section_links]
583 583
584 584 parse_sections(text, project, obj, attr, only_path, options)
585 585 text = parse_non_pre_blocks(text, obj, macros) do |text|
586 586 [:parse_inline_attachments, :parse_wiki_links, :parse_redmine_links].each do |method_name|
587 587 send method_name, text, project, obj, attr, only_path, options
588 588 end
589 589 end
590 590 parse_headings(text, project, obj, attr, only_path, options)
591 591
592 592 if @parsed_headings.any?
593 593 replace_toc(text, @parsed_headings)
594 594 end
595 595
596 596 text.html_safe
597 597 end
598 598
599 599 def parse_non_pre_blocks(text, obj, macros)
600 600 s = StringScanner.new(text)
601 601 tags = []
602 602 parsed = ''
603 603 while !s.eos?
604 604 s.scan(/(.*?)(<(\/)?(pre|code)(.*?)>|\z)/im)
605 605 text, full_tag, closing, tag = s[1], s[2], s[3], s[4]
606 606 if tags.empty?
607 607 yield text
608 608 inject_macros(text, obj, macros) if macros.any?
609 609 else
610 610 inject_macros(text, obj, macros, false) if macros.any?
611 611 end
612 612 parsed << text
613 613 if tag
614 614 if closing
615 615 if tags.last && tags.last.casecmp(tag) == 0
616 616 tags.pop
617 617 end
618 618 else
619 619 tags << tag.downcase
620 620 end
621 621 parsed << full_tag
622 622 end
623 623 end
624 624 # Close any non closing tags
625 625 while tag = tags.pop
626 626 parsed << "</#{tag}>"
627 627 end
628 628 parsed
629 629 end
630 630
631 631 def parse_inline_attachments(text, project, obj, attr, only_path, options)
632 632 return if options[:inline_attachments] == false
633 633
634 634 # when using an image link, try to use an attachment, if possible
635 635 attachments = options[:attachments] || []
636 636 attachments += obj.attachments if obj.respond_to?(:attachments)
637 637 if attachments.present?
638 638 text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpe|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m|
639 639 filename, ext, alt, alttext = $1.downcase, $2, $3, $4
640 640 # search for the picture in attachments
641 641 if found = Attachment.latest_attach(attachments, CGI.unescape(filename))
642 642 image_url = download_named_attachment_url(found, found.filename, :only_path => only_path)
643 643 desc = found.description.to_s.gsub('"', '')
644 644 if !desc.blank? && alttext.blank?
645 645 alt = " title=\"#{desc}\" alt=\"#{desc}\""
646 646 end
647 647 "src=\"#{image_url}\"#{alt}"
648 648 else
649 649 m
650 650 end
651 651 end
652 652 end
653 653 end
654 654
655 655 # Wiki links
656 656 #
657 657 # Examples:
658 658 # [[mypage]]
659 659 # [[mypage|mytext]]
660 660 # wiki links can refer other project wikis, using project name or identifier:
661 661 # [[project:]] -> wiki starting page
662 662 # [[project:|mytext]]
663 663 # [[project:mypage]]
664 664 # [[project:mypage|mytext]]
665 665 def parse_wiki_links(text, project, obj, attr, only_path, options)
666 666 text.gsub!(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m|
667 667 link_project = project
668 668 esc, all, page, title = $1, $2, $3, $5
669 669 if esc.nil?
670 670 if page =~ /^([^\:]+)\:(.*)$/
671 671 identifier, page = $1, $2
672 672 link_project = Project.find_by_identifier(identifier) || Project.find_by_name(identifier)
673 673 title ||= identifier if page.blank?
674 674 end
675 675
676 676 if link_project && link_project.wiki
677 677 # extract anchor
678 678 anchor = nil
679 679 if page =~ /^(.+?)\#(.+)$/
680 680 page, anchor = $1, $2
681 681 end
682 682 anchor = sanitize_anchor_name(anchor) if anchor.present?
683 683 # check if page exists
684 684 wiki_page = link_project.wiki.find_page(page)
685 685 url = if anchor.present? && wiki_page.present? && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version)) && obj.page == wiki_page
686 686 "##{anchor}"
687 687 else
688 688 case options[:wiki_links]
689 689 when :local; "#{page.present? ? Wiki.titleize(page) : ''}.html" + (anchor.present? ? "##{anchor}" : '')
690 690 when :anchor; "##{page.present? ? Wiki.titleize(page) : title}" + (anchor.present? ? "_#{anchor}" : '') # used for single-file wiki export
691 691 else
692 692 wiki_page_id = page.present? ? Wiki.titleize(page) : nil
693 693 parent = wiki_page.nil? && obj.is_a?(WikiContent) && obj.page && project == link_project ? obj.page.title : nil
694 694 url_for(:only_path => only_path, :controller => 'wiki', :action => 'show', :project_id => link_project,
695 695 :id => wiki_page_id, :version => nil, :anchor => anchor, :parent => parent)
696 696 end
697 697 end
698 698 link_to(title.present? ? title.html_safe : h(page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new')))
699 699 else
700 700 # project or wiki doesn't exist
701 701 all
702 702 end
703 703 else
704 704 all
705 705 end
706 706 end
707 707 end
708 708
709 709 # Redmine links
710 710 #
711 711 # Examples:
712 712 # Issues:
713 713 # #52 -> Link to issue #52
714 714 # Changesets:
715 715 # r52 -> Link to revision 52
716 716 # commit:a85130f -> Link to scmid starting with a85130f
717 717 # Documents:
718 718 # document#17 -> Link to document with id 17
719 719 # document:Greetings -> Link to the document with title "Greetings"
720 720 # document:"Some document" -> Link to the document with title "Some document"
721 721 # Versions:
722 722 # version#3 -> Link to version with id 3
723 723 # version:1.0.0 -> Link to version named "1.0.0"
724 724 # version:"1.0 beta 2" -> Link to version named "1.0 beta 2"
725 725 # Attachments:
726 726 # attachment:file.zip -> Link to the attachment of the current object named file.zip
727 727 # Source files:
728 728 # source:some/file -> Link to the file located at /some/file in the project's repository
729 729 # source:some/file@52 -> Link to the file's revision 52
730 730 # source:some/file#L120 -> Link to line 120 of the file
731 731 # source:some/file@52#L120 -> Link to line 120 of the file's revision 52
732 732 # export:some/file -> Force the download of the file
733 733 # Forum messages:
734 734 # message#1218 -> Link to message with id 1218
735 735 # Projects:
736 736 # project:someproject -> Link to project named "someproject"
737 737 # project#3 -> Link to project with id 3
738 738 #
739 739 # Links can refer other objects from other projects, using project identifier:
740 740 # identifier:r52
741 741 # identifier:document:"Some document"
742 742 # identifier:version:1.0.0
743 743 # identifier:source:some/file
744 744 def parse_redmine_links(text, default_project, obj, attr, only_path, options)
745 745 text.gsub!(%r{<a( [^>]+?)?>(.*?)</a>|([\s\(,\-\[\>]|^)(!)?(([a-z0-9\-_]+):)?(attachment|document|version|forum|news|message|project|commit|source|export)?(((#)|((([a-z0-9\-_]+)\|)?(r)))((\d+)((#note)?-(\d+))?)|(:)([^"\s<>][^\s<>]*?|"[^"]+?"))(?=(?=[[:punct:]][^A-Za-z0-9_/])|,|\s|\]|<|$)}) do |m|
746 746 tag_content, leading, esc, project_prefix, project_identifier, prefix, repo_prefix, repo_identifier, sep, identifier, comment_suffix, comment_id = $2, $3, $4, $5, $6, $7, $12, $13, $10 || $14 || $20, $16 || $21, $17, $19
747 747 if tag_content
748 748 $&
749 749 else
750 750 link = nil
751 751 project = default_project
752 752 if project_identifier
753 753 project = Project.visible.find_by_identifier(project_identifier)
754 754 end
755 755 if esc.nil?
756 756 if prefix.nil? && sep == 'r'
757 757 if project
758 758 repository = nil
759 759 if repo_identifier
760 760 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
761 761 else
762 762 repository = project.repository
763 763 end
764 764 # project.changesets.visible raises an SQL error because of a double join on repositories
765 765 if repository &&
766 766 (changeset = Changeset.visible.
767 767 find_by_repository_id_and_revision(repository.id, identifier))
768 768 link = link_to(h("#{project_prefix}#{repo_prefix}r#{identifier}"),
769 769 {:only_path => only_path, :controller => 'repositories',
770 770 :action => 'revision', :id => project,
771 771 :repository_id => repository.identifier_param,
772 772 :rev => changeset.revision},
773 773 :class => 'changeset',
774 774 :title => truncate_single_line_raw(changeset.comments, 100))
775 775 end
776 776 end
777 777 elsif sep == '#'
778 778 oid = identifier.to_i
779 779 case prefix
780 780 when nil
781 781 if oid.to_s == identifier &&
782 782 issue = Issue.visible.find_by_id(oid)
783 783 anchor = comment_id ? "note-#{comment_id}" : nil
784 784 link = link_to("##{oid}#{comment_suffix}",
785 785 issue_url(issue, :only_path => only_path, :anchor => anchor),
786 786 :class => issue.css_classes,
787 787 :title => "#{issue.tracker.name}: #{issue.subject.truncate(100)} (#{issue.status.name})")
788 788 end
789 789 when 'document'
790 790 if document = Document.visible.find_by_id(oid)
791 791 link = link_to(document.title, document_url(document, :only_path => only_path), :class => 'document')
792 792 end
793 793 when 'version'
794 794 if version = Version.visible.find_by_id(oid)
795 795 link = link_to(version.name, version_url(version, :only_path => only_path), :class => 'version')
796 796 end
797 797 when 'message'
798 798 if message = Message.visible.find_by_id(oid)
799 799 link = link_to_message(message, {:only_path => only_path}, :class => 'message')
800 800 end
801 801 when 'forum'
802 802 if board = Board.visible.find_by_id(oid)
803 803 link = link_to(board.name, project_board_url(board.project, board, :only_path => only_path), :class => 'board')
804 804 end
805 805 when 'news'
806 806 if news = News.visible.find_by_id(oid)
807 807 link = link_to(news.title, news_url(news, :only_path => only_path), :class => 'news')
808 808 end
809 809 when 'project'
810 810 if p = Project.visible.find_by_id(oid)
811 811 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
812 812 end
813 813 end
814 814 elsif sep == ':'
815 815 # removes the double quotes if any
816 816 name = identifier.gsub(%r{^"(.*)"$}, "\\1")
817 817 name = CGI.unescapeHTML(name)
818 818 case prefix
819 819 when 'document'
820 820 if project && document = project.documents.visible.find_by_title(name)
821 821 link = link_to(document.title, document_url(document, :only_path => only_path), :class => 'document')
822 822 end
823 823 when 'version'
824 824 if project && version = project.versions.visible.find_by_name(name)
825 825 link = link_to(version.name, version_url(version, :only_path => only_path), :class => 'version')
826 826 end
827 827 when 'forum'
828 828 if project && board = project.boards.visible.find_by_name(name)
829 829 link = link_to(board.name, project_board_url(board.project, board, :only_path => only_path), :class => 'board')
830 830 end
831 831 when 'news'
832 832 if project && news = project.news.visible.find_by_title(name)
833 833 link = link_to(news.title, news_url(news, :only_path => only_path), :class => 'news')
834 834 end
835 835 when 'commit', 'source', 'export'
836 836 if project
837 837 repository = nil
838 838 if name =~ %r{^(([a-z0-9\-_]+)\|)(.+)$}
839 839 repo_prefix, repo_identifier, name = $1, $2, $3
840 840 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
841 841 else
842 842 repository = project.repository
843 843 end
844 844 if prefix == 'commit'
845 845 if repository && (changeset = Changeset.visible.where("repository_id = ? AND scmid LIKE ?", repository.id, "#{name}%").first)
846 846 link = link_to h("#{project_prefix}#{repo_prefix}#{name}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :repository_id => repository.identifier_param, :rev => changeset.identifier},
847 847 :class => 'changeset',
848 848 :title => truncate_single_line_raw(changeset.comments, 100)
849 849 end
850 850 else
851 851 if repository && User.current.allowed_to?(:browse_repository, project)
852 852 name =~ %r{^[/\\]*(.*?)(@([^/\\@]+?))?(#(L\d+))?$}
853 853 path, rev, anchor = $1, $3, $5
854 854 link = link_to h("#{project_prefix}#{prefix}:#{repo_prefix}#{name}"), {:only_path => only_path, :controller => 'repositories', :action => (prefix == 'export' ? 'raw' : 'entry'), :id => project, :repository_id => repository.identifier_param,
855 855 :path => to_path_param(path),
856 856 :rev => rev,
857 857 :anchor => anchor},
858 858 :class => (prefix == 'export' ? 'source download' : 'source')
859 859 end
860 860 end
861 861 repo_prefix = nil
862 862 end
863 863 when 'attachment'
864 864 attachments = options[:attachments] || []
865 865 attachments += obj.attachments if obj.respond_to?(:attachments)
866 866 if attachments && attachment = Attachment.latest_attach(attachments, name)
867 867 link = link_to_attachment(attachment, :only_path => only_path, :download => true, :class => 'attachment')
868 868 end
869 869 when 'project'
870 870 if p = Project.visible.where("identifier = :s OR LOWER(name) = :s", :s => name.downcase).first
871 871 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
872 872 end
873 873 end
874 874 end
875 875 end
876 876 (leading + (link || "#{project_prefix}#{prefix}#{repo_prefix}#{sep}#{identifier}#{comment_suffix}"))
877 877 end
878 878 end
879 879 end
880 880
881 881 HEADING_RE = /(<h(\d)( [^>]+)?>(.+?)<\/h(\d)>)/i unless const_defined?(:HEADING_RE)
882 882
883 883 def parse_sections(text, project, obj, attr, only_path, options)
884 884 return unless options[:edit_section_links]
885 885 text.gsub!(HEADING_RE) do
886 886 heading = $1
887 887 @current_section += 1
888 888 if @current_section > 1
889 889 content_tag('div',
890 890 link_to(image_tag('edit.png'), options[:edit_section_links].merge(:section => @current_section)),
891 891 :class => 'contextual',
892 892 :title => l(:button_edit_section),
893 893 :id => "section-#{@current_section}") + heading.html_safe
894 894 else
895 895 heading
896 896 end
897 897 end
898 898 end
899 899
900 900 # Headings and TOC
901 901 # Adds ids and links to headings unless options[:headings] is set to false
902 902 def parse_headings(text, project, obj, attr, only_path, options)
903 903 return if options[:headings] == false
904 904
905 905 text.gsub!(HEADING_RE) do
906 906 level, attrs, content = $2.to_i, $3, $4
907 907 item = strip_tags(content).strip
908 908 anchor = sanitize_anchor_name(item)
909 909 # used for single-file wiki export
910 910 anchor = "#{obj.page.title}_#{anchor}" if options[:wiki_links] == :anchor && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version))
911 911 @heading_anchors[anchor] ||= 0
912 912 idx = (@heading_anchors[anchor] += 1)
913 913 if idx > 1
914 914 anchor = "#{anchor}-#{idx}"
915 915 end
916 916 @parsed_headings << [level, anchor, item]
917 917 "<a name=\"#{anchor}\"></a>\n<h#{level} #{attrs}>#{content}<a href=\"##{anchor}\" class=\"wiki-anchor\">&para;</a></h#{level}>"
918 918 end
919 919 end
920 920
921 921 MACROS_RE = /(
922 922 (!)? # escaping
923 923 (
924 924 \{\{ # opening tag
925 925 ([\w]+) # macro name
926 926 (\(([^\n\r]*?)\))? # optional arguments
927 927 ([\n\r].*?[\n\r])? # optional block of text
928 928 \}\} # closing tag
929 929 )
930 930 )/mx unless const_defined?(:MACROS_RE)
931 931
932 932 MACRO_SUB_RE = /(
933 933 \{\{
934 934 macro\((\d+)\)
935 935 \}\}
936 936 )/x unless const_defined?(:MACRO_SUB_RE)
937 937
938 938 # Extracts macros from text
939 939 def catch_macros(text)
940 940 macros = {}
941 941 text.gsub!(MACROS_RE) do
942 942 all, macro = $1, $4.downcase
943 943 if macro_exists?(macro) || all =~ MACRO_SUB_RE
944 944 index = macros.size
945 945 macros[index] = all
946 946 "{{macro(#{index})}}"
947 947 else
948 948 all
949 949 end
950 950 end
951 951 macros
952 952 end
953 953
954 954 # Executes and replaces macros in text
955 955 def inject_macros(text, obj, macros, execute=true)
956 956 text.gsub!(MACRO_SUB_RE) do
957 957 all, index = $1, $2.to_i
958 958 orig = macros.delete(index)
959 959 if execute && orig && orig =~ MACROS_RE
960 960 esc, all, macro, args, block = $2, $3, $4.downcase, $6.to_s, $7.try(:strip)
961 961 if esc.nil?
962 962 h(exec_macro(macro, obj, args, block) || all)
963 963 else
964 964 h(all)
965 965 end
966 966 elsif orig
967 967 h(orig)
968 968 else
969 969 h(all)
970 970 end
971 971 end
972 972 end
973 973
974 974 TOC_RE = /<p>\{\{((<|&lt;)|(>|&gt;))?toc\}\}<\/p>/i unless const_defined?(:TOC_RE)
975 975
976 976 # Renders the TOC with given headings
977 977 def replace_toc(text, headings)
978 978 text.gsub!(TOC_RE) do
979 979 left_align, right_align = $2, $3
980 980 # Keep only the 4 first levels
981 981 headings = headings.select{|level, anchor, item| level <= 4}
982 982 if headings.empty?
983 983 ''
984 984 else
985 985 div_class = 'toc'
986 986 div_class << ' right' if right_align
987 987 div_class << ' left' if left_align
988 988 out = "<ul class=\"#{div_class}\"><li>"
989 989 root = headings.map(&:first).min
990 990 current = root
991 991 started = false
992 992 headings.each do |level, anchor, item|
993 993 if level > current
994 994 out << '<ul><li>' * (level - current)
995 995 elsif level < current
996 996 out << "</li></ul>\n" * (current - level) + "</li><li>"
997 997 elsif started
998 998 out << '</li><li>'
999 999 end
1000 1000 out << "<a href=\"##{anchor}\">#{item}</a>"
1001 1001 current = level
1002 1002 started = true
1003 1003 end
1004 1004 out << '</li></ul>' * (current - root)
1005 1005 out << '</li></ul>'
1006 1006 end
1007 1007 end
1008 1008 end
1009 1009
1010 1010 # Same as Rails' simple_format helper without using paragraphs
1011 1011 def simple_format_without_paragraph(text)
1012 1012 text.to_s.
1013 1013 gsub(/\r\n?/, "\n"). # \r\n and \r -> \n
1014 1014 gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br
1015 1015 gsub(/([^\n]\n)(?=[^\n])/, '\1<br />'). # 1 newline -> br
1016 1016 html_safe
1017 1017 end
1018 1018
1019 1019 def lang_options_for_select(blank=true)
1020 1020 (blank ? [["(auto)", ""]] : []) + languages_options
1021 1021 end
1022 1022
1023 1023 def labelled_form_for(*args, &proc)
1024 1024 args << {} unless args.last.is_a?(Hash)
1025 1025 options = args.last
1026 1026 if args.first.is_a?(Symbol)
1027 1027 options.merge!(:as => args.shift)
1028 1028 end
1029 1029 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
1030 1030 form_for(*args, &proc)
1031 1031 end
1032 1032
1033 1033 def labelled_fields_for(*args, &proc)
1034 1034 args << {} unless args.last.is_a?(Hash)
1035 1035 options = args.last
1036 1036 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
1037 1037 fields_for(*args, &proc)
1038 1038 end
1039 1039
1040 1040 def error_messages_for(*objects)
1041 1041 html = ""
1042 1042 objects = objects.map {|o| o.is_a?(String) ? instance_variable_get("@#{o}") : o}.compact
1043 1043 errors = objects.map {|o| o.errors.full_messages}.flatten
1044 1044 if errors.any?
1045 1045 html << "<div id='errorExplanation'><ul>\n"
1046 1046 errors.each do |error|
1047 1047 html << "<li>#{h error}</li>\n"
1048 1048 end
1049 1049 html << "</ul></div>\n"
1050 1050 end
1051 1051 html.html_safe
1052 1052 end
1053 1053
1054 1054 def delete_link(url, options={})
1055 1055 options = {
1056 1056 :method => :delete,
1057 1057 :data => {:confirm => l(:text_are_you_sure)},
1058 1058 :class => 'icon icon-del'
1059 1059 }.merge(options)
1060 1060
1061 1061 link_to l(:button_delete), url, options
1062 1062 end
1063 1063
1064 1064 def preview_link(url, form, target='preview', options={})
1065 1065 content_tag 'a', l(:label_preview), {
1066 1066 :href => "#",
1067 1067 :onclick => %|submitPreview("#{escape_javascript url_for(url)}", "#{escape_javascript form}", "#{escape_javascript target}"); return false;|,
1068 1068 :accesskey => accesskey(:preview)
1069 1069 }.merge(options)
1070 1070 end
1071 1071
1072 1072 def link_to_function(name, function, html_options={})
1073 1073 content_tag(:a, name, {:href => '#', :onclick => "#{function}; return false;"}.merge(html_options))
1074 1074 end
1075 1075
1076 1076 # Helper to render JSON in views
1077 1077 def raw_json(arg)
1078 1078 arg.to_json.to_s.gsub('/', '\/').html_safe
1079 1079 end
1080 1080
1081 1081 def back_url
1082 1082 url = params[:back_url]
1083 1083 if url.nil? && referer = request.env['HTTP_REFERER']
1084 1084 url = CGI.unescape(referer.to_s)
1085 1085 end
1086 1086 url
1087 1087 end
1088 1088
1089 1089 def back_url_hidden_field_tag
1090 1090 url = back_url
1091 1091 hidden_field_tag('back_url', url, :id => nil) unless url.blank?
1092 1092 end
1093 1093
1094 1094 def check_all_links(form_name)
1095 1095 link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
1096 1096 " | ".html_safe +
1097 1097 link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
1098 1098 end
1099 1099
1100 1100 def toggle_checkboxes_link(selector)
1101 1101 link_to_function image_tag('toggle_check.png'),
1102 1102 "toggleCheckboxesBySelector('#{selector}')",
1103 1103 :title => "#{l(:button_check_all)} / #{l(:button_uncheck_all)}"
1104 1104 end
1105 1105
1106 1106 def progress_bar(pcts, options={})
1107 1107 pcts = [pcts, pcts] unless pcts.is_a?(Array)
1108 1108 pcts = pcts.collect(&:round)
1109 1109 pcts[1] = pcts[1] - pcts[0]
1110 1110 pcts << (100 - pcts[1] - pcts[0])
1111 width = options[:width] || '100px;'
1112 1111 legend = options[:legend] || ''
1113 1112 content_tag('table',
1114 1113 content_tag('tr',
1115 1114 (pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0]}%;", :class => 'closed') : ''.html_safe) +
1116 1115 (pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1]}%;", :class => 'done') : ''.html_safe) +
1117 1116 (pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2]}%;", :class => 'todo') : ''.html_safe)
1118 ), :class => "progress progress-#{pcts[0]}", :style => "width: #{width};").html_safe +
1117 ), :class => "progress progress-#{pcts[0]}").html_safe +
1119 1118 content_tag('p', legend, :class => 'percent').html_safe
1120 1119 end
1121 1120
1122 1121 def checked_image(checked=true)
1123 1122 if checked
1124 1123 @checked_image_tag ||= image_tag('toggle_check.png')
1125 1124 end
1126 1125 end
1127 1126
1128 1127 def context_menu(url)
1129 1128 unless @context_menu_included
1130 1129 content_for :header_tags do
1131 1130 javascript_include_tag('context_menu') +
1132 1131 stylesheet_link_tag('context_menu')
1133 1132 end
1134 1133 if l(:direction) == 'rtl'
1135 1134 content_for :header_tags do
1136 1135 stylesheet_link_tag('context_menu_rtl')
1137 1136 end
1138 1137 end
1139 1138 @context_menu_included = true
1140 1139 end
1141 1140 javascript_tag "contextMenuInit('#{ url_for(url) }')"
1142 1141 end
1143 1142
1144 1143 def calendar_for(field_id)
1145 1144 include_calendar_headers_tags
1146 1145 javascript_tag("$(function() { $('##{field_id}').addClass('date').datepicker(datepickerOptions); });")
1147 1146 end
1148 1147
1149 1148 def include_calendar_headers_tags
1150 1149 unless @calendar_headers_tags_included
1151 1150 tags = ''.html_safe
1152 1151 @calendar_headers_tags_included = true
1153 1152 content_for :header_tags do
1154 1153 start_of_week = Setting.start_of_week
1155 1154 start_of_week = l(:general_first_day_of_week, :default => '1') if start_of_week.blank?
1156 1155 # Redmine uses 1..7 (monday..sunday) in settings and locales
1157 1156 # JQuery uses 0..6 (sunday..saturday), 7 needs to be changed to 0
1158 1157 start_of_week = start_of_week.to_i % 7
1159 1158 tags << javascript_tag(
1160 1159 "var datepickerOptions={dateFormat: 'yy-mm-dd', firstDay: #{start_of_week}, " +
1161 1160 "showOn: 'button', buttonImageOnly: true, buttonImage: '" +
1162 1161 path_to_image('/images/calendar.png') +
1163 1162 "', showButtonPanel: true, showWeek: true, showOtherMonths: true, " +
1164 1163 "selectOtherMonths: true, changeMonth: true, changeYear: true, " +
1165 1164 "beforeShow: beforeShowDatePicker};")
1166 1165 jquery_locale = l('jquery.locale', :default => current_language.to_s)
1167 1166 unless jquery_locale == 'en'
1168 1167 tags << javascript_include_tag("i18n/datepicker-#{jquery_locale}.js")
1169 1168 end
1170 1169 tags
1171 1170 end
1172 1171 end
1173 1172 end
1174 1173
1175 1174 # Overrides Rails' stylesheet_link_tag with themes and plugins support.
1176 1175 # Examples:
1177 1176 # stylesheet_link_tag('styles') # => picks styles.css from the current theme or defaults
1178 1177 # stylesheet_link_tag('styles', :plugin => 'foo) # => picks styles.css from plugin's assets
1179 1178 #
1180 1179 def stylesheet_link_tag(*sources)
1181 1180 options = sources.last.is_a?(Hash) ? sources.pop : {}
1182 1181 plugin = options.delete(:plugin)
1183 1182 sources = sources.map do |source|
1184 1183 if plugin
1185 1184 "/plugin_assets/#{plugin}/stylesheets/#{source}"
1186 1185 elsif current_theme && current_theme.stylesheets.include?(source)
1187 1186 current_theme.stylesheet_path(source)
1188 1187 else
1189 1188 source
1190 1189 end
1191 1190 end
1192 1191 super *sources, options
1193 1192 end
1194 1193
1195 1194 # Overrides Rails' image_tag with themes and plugins support.
1196 1195 # Examples:
1197 1196 # image_tag('image.png') # => picks image.png from the current theme or defaults
1198 1197 # image_tag('image.png', :plugin => 'foo) # => picks image.png from plugin's assets
1199 1198 #
1200 1199 def image_tag(source, options={})
1201 1200 if plugin = options.delete(:plugin)
1202 1201 source = "/plugin_assets/#{plugin}/images/#{source}"
1203 1202 elsif current_theme && current_theme.images.include?(source)
1204 1203 source = current_theme.image_path(source)
1205 1204 end
1206 1205 super source, options
1207 1206 end
1208 1207
1209 1208 # Overrides Rails' javascript_include_tag with plugins support
1210 1209 # Examples:
1211 1210 # javascript_include_tag('scripts') # => picks scripts.js from defaults
1212 1211 # javascript_include_tag('scripts', :plugin => 'foo) # => picks scripts.js from plugin's assets
1213 1212 #
1214 1213 def javascript_include_tag(*sources)
1215 1214 options = sources.last.is_a?(Hash) ? sources.pop : {}
1216 1215 if plugin = options.delete(:plugin)
1217 1216 sources = sources.map do |source|
1218 1217 if plugin
1219 1218 "/plugin_assets/#{plugin}/javascripts/#{source}"
1220 1219 else
1221 1220 source
1222 1221 end
1223 1222 end
1224 1223 end
1225 1224 super *sources, options
1226 1225 end
1227 1226
1228 1227 def sidebar_content?
1229 1228 content_for?(:sidebar) || view_layouts_base_sidebar_hook_response.present?
1230 1229 end
1231 1230
1232 1231 def view_layouts_base_sidebar_hook_response
1233 1232 @view_layouts_base_sidebar_hook_response ||= call_hook(:view_layouts_base_sidebar)
1234 1233 end
1235 1234
1236 1235 def email_delivery_enabled?
1237 1236 !!ActionMailer::Base.perform_deliveries
1238 1237 end
1239 1238
1240 1239 # Returns the avatar image tag for the given +user+ if avatars are enabled
1241 1240 # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>')
1242 1241 def avatar(user, options = { })
1243 1242 if Setting.gravatar_enabled?
1244 1243 options.merge!({:ssl => (request && request.ssl?), :default => Setting.gravatar_default})
1245 1244 email = nil
1246 1245 if user.respond_to?(:mail)
1247 1246 email = user.mail
1248 1247 elsif user.to_s =~ %r{<(.+?)>}
1249 1248 email = $1
1250 1249 end
1251 1250 return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil
1252 1251 else
1253 1252 ''
1254 1253 end
1255 1254 end
1256 1255
1257 1256 # Returns a link to edit user's avatar if avatars are enabled
1258 1257 def avatar_edit_link(user, options={})
1259 1258 if Setting.gravatar_enabled?
1260 1259 url = "https://gravatar.com"
1261 1260 link_to avatar(user, {:title => l(:button_edit)}.merge(options)), url, :target => '_blank'
1262 1261 end
1263 1262 end
1264 1263
1265 1264 def sanitize_anchor_name(anchor)
1266 1265 anchor.gsub(%r{[^\s\-\p{Word}]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
1267 1266 end
1268 1267
1269 1268 # Returns the javascript tags that are included in the html layout head
1270 1269 def javascript_heads
1271 1270 tags = javascript_include_tag('jquery-1.11.1-ui-1.11.0-ujs-3.1.4', 'application', 'responsive')
1272 1271 unless User.current.pref.warn_on_leaving_unsaved == '0'
1273 1272 tags << "\n".html_safe + javascript_tag("$(window).load(function(){ warnLeavingUnsaved('#{escape_javascript l(:text_warn_on_leaving_unsaved)}'); });")
1274 1273 end
1275 1274 tags
1276 1275 end
1277 1276
1278 1277 def favicon
1279 1278 "<link rel='shortcut icon' href='#{favicon_path}' />".html_safe
1280 1279 end
1281 1280
1282 1281 # Returns the path to the favicon
1283 1282 def favicon_path
1284 1283 icon = (current_theme && current_theme.favicon?) ? current_theme.favicon_path : '/favicon.ico'
1285 1284 image_path(icon)
1286 1285 end
1287 1286
1288 1287 # Returns the full URL to the favicon
1289 1288 def favicon_url
1290 1289 # TODO: use #image_url introduced in Rails4
1291 1290 path = favicon_path
1292 1291 base = url_for(:controller => 'welcome', :action => 'index', :only_path => false)
1293 1292 base.sub(%r{/+$},'') + '/' + path.sub(%r{^/+},'')
1294 1293 end
1295 1294
1296 1295 def robot_exclusion_tag
1297 1296 '<meta name="robots" content="noindex,follow,noarchive" />'.html_safe
1298 1297 end
1299 1298
1300 1299 # Returns true if arg is expected in the API response
1301 1300 def include_in_api_response?(arg)
1302 1301 unless @included_in_api_response
1303 1302 param = params[:include]
1304 1303 @included_in_api_response = param.is_a?(Array) ? param.collect(&:to_s) : param.to_s.split(',')
1305 1304 @included_in_api_response.collect!(&:strip)
1306 1305 end
1307 1306 @included_in_api_response.include?(arg.to_s)
1308 1307 end
1309 1308
1310 1309 # Returns options or nil if nometa param or X-Redmine-Nometa header
1311 1310 # was set in the request
1312 1311 def api_meta(options)
1313 1312 if params[:nometa].present? || request.headers['X-Redmine-Nometa']
1314 1313 # compatibility mode for activeresource clients that raise
1315 1314 # an error when deserializing an array with attributes
1316 1315 nil
1317 1316 else
1318 1317 options
1319 1318 end
1320 1319 end
1321 1320
1322 1321 def generate_csv(&block)
1323 1322 decimal_separator = l(:general_csv_decimal_separator)
1324 1323 encoding = l(:general_csv_encoding)
1325 1324 end
1326 1325
1327 1326 private
1328 1327
1329 1328 def wiki_helper
1330 1329 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
1331 1330 extend helper
1332 1331 return self
1333 1332 end
1334 1333
1335 1334 def link_to_content_update(text, url_params = {}, html_options = {})
1336 1335 link_to(text, url_params, html_options)
1337 1336 end
1338 1337 end
@@ -1,516 +1,516
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 4 # Copyright (C) 2006-2015 Jean-Philippe Lang
5 5 #
6 6 # This program is free software; you can redistribute it and/or
7 7 # modify it under the terms of the GNU General Public License
8 8 # as published by the Free Software Foundation; either version 2
9 9 # of the License, or (at your option) any later version.
10 10 #
11 11 # This program is distributed in the hope that it will be useful,
12 12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 14 # GNU General Public License for more details.
15 15 #
16 16 # You should have received a copy of the GNU General Public License
17 17 # along with this program; if not, write to the Free Software
18 18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 19
20 20 module IssuesHelper
21 21 include ApplicationHelper
22 22 include Redmine::Export::PDF::IssuesPdfHelper
23 23
24 24 def issue_list(issues, &block)
25 25 ancestors = []
26 26 issues.each do |issue|
27 27 while (ancestors.any? && !issue.is_descendant_of?(ancestors.last))
28 28 ancestors.pop
29 29 end
30 30 yield issue, ancestors.size
31 31 ancestors << issue unless issue.leaf?
32 32 end
33 33 end
34 34
35 35 def grouped_issue_list(issues, query, issue_count_by_group, &block)
36 36 previous_group, first = false, true
37 37 totals_by_group = query.totalable_columns.inject({}) do |h, column|
38 38 h[column] = query.total_by_group_for(column)
39 39 h
40 40 end
41 41 issue_list(issues) do |issue, level|
42 42 group_name = group_count = nil
43 43 if query.grouped?
44 44 group = query.group_by_column.value(issue)
45 45 if first || group != previous_group
46 46 if group.blank? && group != false
47 47 group_name = "(#{l(:label_blank_value)})"
48 48 else
49 49 group_name = format_object(group)
50 50 end
51 51 group_name ||= ""
52 52 group_count = issue_count_by_group[group]
53 53 group_totals = totals_by_group.map {|column, t| total_tag(column, t[group] || 0)}.join(" ").html_safe
54 54 end
55 55 end
56 56 yield issue, level, group_name, group_count, group_totals
57 57 previous_group, first = group, false
58 58 end
59 59 end
60 60
61 61 # Renders a HTML/CSS tooltip
62 62 #
63 63 # To use, a trigger div is needed. This is a div with the class of "tooltip"
64 64 # that contains this method wrapped in a span with the class of "tip"
65 65 #
66 66 # <div class="tooltip"><%= link_to_issue(issue) %>
67 67 # <span class="tip"><%= render_issue_tooltip(issue) %></span>
68 68 # </div>
69 69 #
70 70 def render_issue_tooltip(issue)
71 71 @cached_label_status ||= l(:field_status)
72 72 @cached_label_start_date ||= l(:field_start_date)
73 73 @cached_label_due_date ||= l(:field_due_date)
74 74 @cached_label_assigned_to ||= l(:field_assigned_to)
75 75 @cached_label_priority ||= l(:field_priority)
76 76 @cached_label_project ||= l(:field_project)
77 77
78 78 link_to_issue(issue) + "<br /><br />".html_safe +
79 79 "<strong>#{@cached_label_project}</strong>: #{link_to_project(issue.project)}<br />".html_safe +
80 80 "<strong>#{@cached_label_status}</strong>: #{h(issue.status.name)}<br />".html_safe +
81 81 "<strong>#{@cached_label_start_date}</strong>: #{format_date(issue.start_date)}<br />".html_safe +
82 82 "<strong>#{@cached_label_due_date}</strong>: #{format_date(issue.due_date)}<br />".html_safe +
83 83 "<strong>#{@cached_label_assigned_to}</strong>: #{h(issue.assigned_to)}<br />".html_safe +
84 84 "<strong>#{@cached_label_priority}</strong>: #{h(issue.priority.name)}".html_safe
85 85 end
86 86
87 87 def issue_heading(issue)
88 88 h("#{issue.tracker} ##{issue.id}")
89 89 end
90 90
91 91 def render_issue_subject_with_tree(issue)
92 92 s = ''
93 93 ancestors = issue.root? ? [] : issue.ancestors.visible.to_a
94 94 ancestors.each do |ancestor|
95 95 s << '<div>' + content_tag('p', link_to_issue(ancestor, :project => (issue.project_id != ancestor.project_id)))
96 96 end
97 97 s << '<div>'
98 98 subject = h(issue.subject)
99 99 if issue.is_private?
100 100 subject = content_tag('span', l(:field_is_private), :class => 'private') + ' ' + subject
101 101 end
102 102 s << content_tag('h3', subject)
103 103 s << '</div>' * (ancestors.size + 1)
104 104 s.html_safe
105 105 end
106 106
107 107 def render_descendants_tree(issue)
108 108 s = '<form><table class="list issues">'
109 109 issue_list(issue.descendants.visible.preload(:status, :priority, :tracker).sort_by(&:lft)) do |child, level|
110 110 css = "issue issue-#{child.id} hascontextmenu"
111 111 css << " idnt idnt-#{level}" if level > 0
112 112 s << content_tag('tr',
113 113 content_tag('td', check_box_tag("ids[]", child.id, false, :id => nil), :class => 'checkbox') +
114 114 content_tag('td', link_to_issue(child, :project => (issue.project_id != child.project_id)), :class => 'subject', :style => 'width: 50%') +
115 115 content_tag('td', h(child.status)) +
116 116 content_tag('td', link_to_user(child.assigned_to)) +
117 content_tag('td', progress_bar(child.done_ratio, :width => '80px')),
117 content_tag('td', progress_bar(child.done_ratio)),
118 118 :class => css)
119 119 end
120 120 s << '</table></form>'
121 121 s.html_safe
122 122 end
123 123
124 124 def issue_estimated_hours_details(issue)
125 125 if issue.total_estimated_hours.present?
126 126 if issue.total_estimated_hours == issue.estimated_hours
127 127 l_hours_short(issue.estimated_hours)
128 128 else
129 129 s = issue.estimated_hours.present? ? l_hours_short(issue.estimated_hours) : ""
130 130 s << " (#{l(:label_total)}: #{l_hours_short(issue.total_estimated_hours)})"
131 131 s.html_safe
132 132 end
133 133 end
134 134 end
135 135
136 136 def issue_spent_hours_details(issue)
137 137 if issue.total_spent_hours > 0
138 138 if issue.total_spent_hours == issue.spent_hours
139 139 link_to(l_hours_short(issue.spent_hours), issue_time_entries_path(issue))
140 140 else
141 141 s = issue.spent_hours > 0 ? l_hours_short(issue.spent_hours) : ""
142 142 s << " (#{l(:label_total)}: #{link_to l_hours_short(issue.total_spent_hours), issue_time_entries_path(issue)})"
143 143 s.html_safe
144 144 end
145 145 end
146 146 end
147 147
148 148 # Returns an array of error messages for bulk edited issues
149 149 def bulk_edit_error_messages(issues)
150 150 messages = {}
151 151 issues.each do |issue|
152 152 issue.errors.full_messages.each do |message|
153 153 messages[message] ||= []
154 154 messages[message] << issue
155 155 end
156 156 end
157 157 messages.map { |message, issues|
158 158 "#{message}: " + issues.map {|i| "##{i.id}"}.join(', ')
159 159 }
160 160 end
161 161
162 162 # Returns a link for adding a new subtask to the given issue
163 163 def link_to_new_subtask(issue)
164 164 attrs = {
165 165 :tracker_id => issue.tracker,
166 166 :parent_issue_id => issue
167 167 }
168 168 link_to(l(:button_add), new_project_issue_path(issue.project, :issue => attrs))
169 169 end
170 170
171 171 class IssueFieldsRows
172 172 include ActionView::Helpers::TagHelper
173 173
174 174 def initialize
175 175 @left = []
176 176 @right = []
177 177 end
178 178
179 179 def left(*args)
180 180 args.any? ? @left << cells(*args) : @left
181 181 end
182 182
183 183 def right(*args)
184 184 args.any? ? @right << cells(*args) : @right
185 185 end
186 186
187 187 def size
188 188 @left.size > @right.size ? @left.size : @right.size
189 189 end
190 190
191 191 def to_html
192 192 content =
193 193 content_tag('div', @left.reduce(&:+), :class => 'splitcontentleft') +
194 194 content_tag('div', @right.reduce(&:+), :class => 'splitcontentleft')
195 195
196 196 content_tag('div', content, :class => 'splitcontent')
197 197 end
198 198
199 199 def cells(label, text, options={})
200 200 options[:class] = [options[:class] || "", 'attribute'].join(' ')
201 201 content_tag 'div',
202 202 content_tag('div', label + ":", :class => 'label') + content_tag('div', text, :class => 'value'),
203 203 options
204 204 end
205 205 end
206 206
207 207 def issue_fields_rows
208 208 r = IssueFieldsRows.new
209 209 yield r
210 210 r.to_html
211 211 end
212 212
213 213 def render_custom_fields_rows(issue)
214 214 values = issue.visible_custom_field_values
215 215 return if values.empty?
216 216 half = (values.size / 2.0).ceil
217 217 issue_fields_rows do |rows|
218 218 values.each_with_index do |value, i|
219 219 css = "cf_#{value.custom_field.id}"
220 220 m = (i < half ? :left : :right)
221 221 rows.send m, custom_field_name_tag(value.custom_field), show_value(value), :class => css
222 222 end
223 223 end
224 224 end
225 225
226 226 # Returns the path for updating the issue form
227 227 # with project as the current project
228 228 def update_issue_form_path(project, issue)
229 229 options = {:format => 'js'}
230 230 if issue.new_record?
231 231 if project
232 232 new_project_issue_path(project, options)
233 233 else
234 234 new_issue_path(options)
235 235 end
236 236 else
237 237 edit_issue_path(issue, options)
238 238 end
239 239 end
240 240
241 241 # Returns the number of descendants for an array of issues
242 242 def issues_descendant_count(issues)
243 243 ids = issues.reject(&:leaf?).map {|issue| issue.descendants.ids}.flatten.uniq
244 244 ids -= issues.map(&:id)
245 245 ids.size
246 246 end
247 247
248 248 def issues_destroy_confirmation_message(issues)
249 249 issues = [issues] unless issues.is_a?(Array)
250 250 message = l(:text_issues_destroy_confirmation)
251 251
252 252 descendant_count = issues_descendant_count(issues)
253 253 if descendant_count > 0
254 254 message << "\n" + l(:text_issues_destroy_descendants_confirmation, :count => descendant_count)
255 255 end
256 256 message
257 257 end
258 258
259 259 # Returns an array of users that are proposed as watchers
260 260 # on the new issue form
261 261 def users_for_new_issue_watchers(issue)
262 262 users = issue.watcher_users
263 263 if issue.project.users.count <= 20
264 264 users = (users + issue.project.users.sort).uniq
265 265 end
266 266 users
267 267 end
268 268
269 269 def sidebar_queries
270 270 unless @sidebar_queries
271 271 @sidebar_queries = IssueQuery.visible.
272 272 order("#{Query.table_name}.name ASC").
273 273 # Project specific queries and global queries
274 274 where(@project.nil? ? ["project_id IS NULL"] : ["project_id IS NULL OR project_id = ?", @project.id]).
275 275 to_a
276 276 end
277 277 @sidebar_queries
278 278 end
279 279
280 280 def query_links(title, queries)
281 281 return '' if queries.empty?
282 282 # links to #index on issues/show
283 283 url_params = controller_name == 'issues' ? {:controller => 'issues', :action => 'index', :project_id => @project} : params
284 284
285 285 content_tag('h3', title) + "\n" +
286 286 content_tag('ul',
287 287 queries.collect {|query|
288 288 css = 'query'
289 289 css << ' selected' if query == @query
290 290 content_tag('li', link_to(query.name, url_params.merge(:query_id => query), :class => css))
291 291 }.join("\n").html_safe,
292 292 :class => 'queries'
293 293 ) + "\n"
294 294 end
295 295
296 296 def render_sidebar_queries
297 297 out = ''.html_safe
298 298 out << query_links(l(:label_my_queries), sidebar_queries.select(&:is_private?))
299 299 out << query_links(l(:label_query_plural), sidebar_queries.reject(&:is_private?))
300 300 out
301 301 end
302 302
303 303 def email_issue_attributes(issue, user)
304 304 items = []
305 305 %w(author status priority assigned_to category fixed_version).each do |attribute|
306 306 unless issue.disabled_core_fields.include?(attribute+"_id")
307 307 items << "#{l("field_#{attribute}")}: #{issue.send attribute}"
308 308 end
309 309 end
310 310 issue.visible_custom_field_values(user).each do |value|
311 311 items << "#{value.custom_field.name}: #{show_value(value, false)}"
312 312 end
313 313 items
314 314 end
315 315
316 316 def render_email_issue_attributes(issue, user, html=false)
317 317 items = email_issue_attributes(issue, user)
318 318 if html
319 319 content_tag('ul', items.map{|s| content_tag('li', s)}.join("\n").html_safe)
320 320 else
321 321 items.map{|s| "* #{s}"}.join("\n")
322 322 end
323 323 end
324 324
325 325 # Returns the textual representation of a journal details
326 326 # as an array of strings
327 327 def details_to_strings(details, no_html=false, options={})
328 328 options[:only_path] = (options[:only_path] == false ? false : true)
329 329 strings = []
330 330 values_by_field = {}
331 331 details.each do |detail|
332 332 if detail.property == 'cf'
333 333 field = detail.custom_field
334 334 if field && field.multiple?
335 335 values_by_field[field] ||= {:added => [], :deleted => []}
336 336 if detail.old_value
337 337 values_by_field[field][:deleted] << detail.old_value
338 338 end
339 339 if detail.value
340 340 values_by_field[field][:added] << detail.value
341 341 end
342 342 next
343 343 end
344 344 end
345 345 strings << show_detail(detail, no_html, options)
346 346 end
347 347 if values_by_field.present?
348 348 multiple_values_detail = Struct.new(:property, :prop_key, :custom_field, :old_value, :value)
349 349 values_by_field.each do |field, changes|
350 350 if changes[:added].any?
351 351 detail = multiple_values_detail.new('cf', field.id.to_s, field)
352 352 detail.value = changes[:added]
353 353 strings << show_detail(detail, no_html, options)
354 354 end
355 355 if changes[:deleted].any?
356 356 detail = multiple_values_detail.new('cf', field.id.to_s, field)
357 357 detail.old_value = changes[:deleted]
358 358 strings << show_detail(detail, no_html, options)
359 359 end
360 360 end
361 361 end
362 362 strings
363 363 end
364 364
365 365 # Returns the textual representation of a single journal detail
366 366 def show_detail(detail, no_html=false, options={})
367 367 multiple = false
368 368 show_diff = false
369 369
370 370 case detail.property
371 371 when 'attr'
372 372 field = detail.prop_key.to_s.gsub(/\_id$/, "")
373 373 label = l(("field_" + field).to_sym)
374 374 case detail.prop_key
375 375 when 'due_date', 'start_date'
376 376 value = format_date(detail.value.to_date) if detail.value
377 377 old_value = format_date(detail.old_value.to_date) if detail.old_value
378 378
379 379 when 'project_id', 'status_id', 'tracker_id', 'assigned_to_id',
380 380 'priority_id', 'category_id', 'fixed_version_id'
381 381 value = find_name_by_reflection(field, detail.value)
382 382 old_value = find_name_by_reflection(field, detail.old_value)
383 383
384 384 when 'estimated_hours'
385 385 value = "%0.02f" % detail.value.to_f unless detail.value.blank?
386 386 old_value = "%0.02f" % detail.old_value.to_f unless detail.old_value.blank?
387 387
388 388 when 'parent_id'
389 389 label = l(:field_parent_issue)
390 390 value = "##{detail.value}" unless detail.value.blank?
391 391 old_value = "##{detail.old_value}" unless detail.old_value.blank?
392 392
393 393 when 'is_private'
394 394 value = l(detail.value == "0" ? :general_text_No : :general_text_Yes) unless detail.value.blank?
395 395 old_value = l(detail.old_value == "0" ? :general_text_No : :general_text_Yes) unless detail.old_value.blank?
396 396
397 397 when 'description'
398 398 show_diff = true
399 399 end
400 400 when 'cf'
401 401 custom_field = detail.custom_field
402 402 if custom_field
403 403 label = custom_field.name
404 404 if custom_field.format.class.change_as_diff
405 405 show_diff = true
406 406 else
407 407 multiple = custom_field.multiple?
408 408 value = format_value(detail.value, custom_field) if detail.value
409 409 old_value = format_value(detail.old_value, custom_field) if detail.old_value
410 410 end
411 411 end
412 412 when 'attachment'
413 413 label = l(:label_attachment)
414 414 when 'relation'
415 415 if detail.value && !detail.old_value
416 416 rel_issue = Issue.visible.find_by_id(detail.value)
417 417 value = rel_issue.nil? ? "#{l(:label_issue)} ##{detail.value}" :
418 418 (no_html ? rel_issue : link_to_issue(rel_issue, :only_path => options[:only_path]))
419 419 elsif detail.old_value && !detail.value
420 420 rel_issue = Issue.visible.find_by_id(detail.old_value)
421 421 old_value = rel_issue.nil? ? "#{l(:label_issue)} ##{detail.old_value}" :
422 422 (no_html ? rel_issue : link_to_issue(rel_issue, :only_path => options[:only_path]))
423 423 end
424 424 relation_type = IssueRelation::TYPES[detail.prop_key]
425 425 label = l(relation_type[:name]) if relation_type
426 426 end
427 427 call_hook(:helper_issues_show_detail_after_setting,
428 428 {:detail => detail, :label => label, :value => value, :old_value => old_value })
429 429
430 430 label ||= detail.prop_key
431 431 value ||= detail.value
432 432 old_value ||= detail.old_value
433 433
434 434 unless no_html
435 435 label = content_tag('strong', label)
436 436 old_value = content_tag("i", h(old_value)) if detail.old_value
437 437 if detail.old_value && detail.value.blank? && detail.property != 'relation'
438 438 old_value = content_tag("del", old_value)
439 439 end
440 440 if detail.property == 'attachment' && value.present? &&
441 441 atta = detail.journal.journalized.attachments.detect {|a| a.id == detail.prop_key.to_i}
442 442 # Link to the attachment if it has not been removed
443 443 value = link_to_attachment(atta, :download => true, :only_path => options[:only_path])
444 444 if options[:only_path] != false && atta.is_text?
445 445 value += link_to(
446 446 image_tag('magnifier.png'),
447 447 :controller => 'attachments', :action => 'show',
448 448 :id => atta, :filename => atta.filename
449 449 )
450 450 end
451 451 else
452 452 value = content_tag("i", h(value)) if value
453 453 end
454 454 end
455 455
456 456 if show_diff
457 457 s = l(:text_journal_changed_no_detail, :label => label)
458 458 unless no_html
459 459 diff_link = link_to 'diff',
460 460 {:controller => 'journals', :action => 'diff', :id => detail.journal_id,
461 461 :detail_id => detail.id, :only_path => options[:only_path]},
462 462 :title => l(:label_view_diff)
463 463 s << " (#{ diff_link })"
464 464 end
465 465 s.html_safe
466 466 elsif detail.value.present?
467 467 case detail.property
468 468 when 'attr', 'cf'
469 469 if detail.old_value.present?
470 470 l(:text_journal_changed, :label => label, :old => old_value, :new => value).html_safe
471 471 elsif multiple
472 472 l(:text_journal_added, :label => label, :value => value).html_safe
473 473 else
474 474 l(:text_journal_set_to, :label => label, :value => value).html_safe
475 475 end
476 476 when 'attachment', 'relation'
477 477 l(:text_journal_added, :label => label, :value => value).html_safe
478 478 end
479 479 else
480 480 l(:text_journal_deleted, :label => label, :old => old_value).html_safe
481 481 end
482 482 end
483 483
484 484 # Find the name of an associated record stored in the field attribute
485 485 def find_name_by_reflection(field, id)
486 486 unless id.present?
487 487 return nil
488 488 end
489 489 @detail_value_name_by_reflection ||= Hash.new do |hash, key|
490 490 association = Issue.reflect_on_association(key.first.to_sym)
491 491 name = nil
492 492 if association
493 493 record = association.klass.find_by_id(key.last)
494 494 if record
495 495 name = record.name.force_encoding('UTF-8')
496 496 end
497 497 end
498 498 hash[key] = name
499 499 end
500 500 @detail_value_name_by_reflection[[field, id]]
501 501 end
502 502
503 503 # Renders issue children recursively
504 504 def render_api_issue_children(issue, api)
505 505 return if issue.leaf?
506 506 api.array :children do
507 507 issue.children.each do |child|
508 508 api.issue(:id => child.id) do
509 509 api.tracker(:id => child.tracker_id, :name => child.tracker.name) unless child.tracker.nil?
510 510 api.subject child.subject
511 511 render_api_issue_children(child, api)
512 512 end
513 513 end
514 514 end
515 515 end
516 516 end
@@ -1,246 +1,246
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 4 # Copyright (C) 2006-2015 Jean-Philippe Lang
5 5 #
6 6 # This program is free software; you can redistribute it and/or
7 7 # modify it under the terms of the GNU General Public License
8 8 # as published by the Free Software Foundation; either version 2
9 9 # of the License, or (at your option) any later version.
10 10 #
11 11 # This program is distributed in the hope that it will be useful,
12 12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 14 # GNU General Public License for more details.
15 15 #
16 16 # You should have received a copy of the GNU General Public License
17 17 # along with this program; if not, write to the Free Software
18 18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 19
20 20 module QueriesHelper
21 21 include ApplicationHelper
22 22
23 23 def filters_options_for_select(query)
24 24 ungrouped = []
25 25 grouped = {}
26 26 query.available_filters.map do |field, field_options|
27 27 if [:tree, :relation].include?(field_options[:type])
28 28 group = :label_related_issues
29 29 elsif field =~ /^(.+)\./
30 30 # association filters
31 31 group = "field_#{$1}"
32 32 elsif %w(member_of_group assigned_to_role).include?(field)
33 33 group = :field_assigned_to
34 34 elsif field_options[:type] == :date_past || field_options[:type] == :date
35 35 group = :label_date
36 36 end
37 37 if group
38 38 (grouped[group] ||= []) << [field_options[:name], field]
39 39 else
40 40 ungrouped << [field_options[:name], field]
41 41 end
42 42 end
43 43 # Don't group dates if there's only one (eg. time entries filters)
44 44 if grouped[:label_date].try(:size) == 1
45 45 ungrouped << grouped.delete(:label_date).first
46 46 end
47 47 s = options_for_select([[]] + ungrouped)
48 48 if grouped.present?
49 49 localized_grouped = grouped.map {|k,v| [l(k), v]}
50 50 s << grouped_options_for_select(localized_grouped)
51 51 end
52 52 s
53 53 end
54 54
55 55 def query_filters_hidden_tags(query)
56 56 tags = ''.html_safe
57 57 query.filters.each do |field, options|
58 58 tags << hidden_field_tag("f[]", field, :id => nil)
59 59 tags << hidden_field_tag("op[#{field}]", options[:operator], :id => nil)
60 60 options[:values].each do |value|
61 61 tags << hidden_field_tag("v[#{field}][]", value, :id => nil)
62 62 end
63 63 end
64 64 tags
65 65 end
66 66
67 67 def query_columns_hidden_tags(query)
68 68 tags = ''.html_safe
69 69 query.columns.each do |column|
70 70 tags << hidden_field_tag("c[]", column.name, :id => nil)
71 71 end
72 72 tags
73 73 end
74 74
75 75 def query_hidden_tags(query)
76 76 query_filters_hidden_tags(query) + query_columns_hidden_tags(query)
77 77 end
78 78
79 79 def available_block_columns_tags(query)
80 80 tags = ''.html_safe
81 81 query.available_block_columns.each do |column|
82 82 tags << content_tag('label', check_box_tag('c[]', column.name.to_s, query.has_column?(column), :id => nil) + " #{column.caption}", :class => 'inline')
83 83 end
84 84 tags
85 85 end
86 86
87 87 def available_totalable_columns_tags(query)
88 88 tags = ''.html_safe
89 89 query.available_totalable_columns.each do |column|
90 90 tags << content_tag('label', check_box_tag('t[]', column.name.to_s, query.totalable_columns.include?(column), :id => nil) + " #{column.caption}", :class => 'inline')
91 91 end
92 92 tags
93 93 end
94 94
95 95 def query_available_inline_columns_options(query)
96 96 (query.available_inline_columns - query.columns).reject(&:frozen?).collect {|column| [column.caption, column.name]}
97 97 end
98 98
99 99 def query_selected_inline_columns_options(query)
100 100 (query.inline_columns & query.available_inline_columns).reject(&:frozen?).collect {|column| [column.caption, column.name]}
101 101 end
102 102
103 103 def render_query_columns_selection(query, options={})
104 104 tag_name = (options[:name] || 'c') + '[]'
105 105 render :partial => 'queries/columns', :locals => {:query => query, :tag_name => tag_name}
106 106 end
107 107
108 108 def render_query_totals(query)
109 109 return unless query.totalable_columns.present?
110 110 totals = query.totalable_columns.map do |column|
111 111 total_tag(column, query.total_for(column))
112 112 end
113 113 content_tag('p', totals.join(" ").html_safe, :class => "query-totals")
114 114 end
115 115
116 116 def total_tag(column, value)
117 117 label = content_tag('span', "#{column.caption}:")
118 118 value = content_tag('span', format_object(value), :class => 'value')
119 119 content_tag('span', label + " " + value, :class => "total-for-#{column.name.to_s.dasherize}")
120 120 end
121 121
122 122 def column_header(column)
123 123 column.sortable ? sort_header_tag(column.name.to_s, :caption => column.caption,
124 124 :default_order => column.default_order) :
125 125 content_tag('th', h(column.caption))
126 126 end
127 127
128 128 def column_content(column, issue)
129 129 value = column.value_object(issue)
130 130 if value.is_a?(Array)
131 131 value.collect {|v| column_value(column, issue, v)}.compact.join(', ').html_safe
132 132 else
133 133 column_value(column, issue, value)
134 134 end
135 135 end
136 136
137 137 def column_value(column, issue, value)
138 138 case column.name
139 139 when :id
140 140 link_to value, issue_path(issue)
141 141 when :subject
142 142 link_to value, issue_path(issue)
143 143 when :parent
144 144 value ? (value.visible? ? link_to_issue(value, :subject => false) : "##{value.id}") : ''
145 145 when :description
146 146 issue.description? ? content_tag('div', textilizable(issue, :description), :class => "wiki") : ''
147 147 when :done_ratio
148 progress_bar(value, :width => '80px')
148 progress_bar(value)
149 149 when :relations
150 150 content_tag('span',
151 151 value.to_s(issue) {|other| link_to_issue(other, :subject => false, :tracker => false)}.html_safe,
152 152 :class => value.css_classes_for(issue))
153 153 else
154 154 format_object(value)
155 155 end
156 156 end
157 157
158 158 def csv_content(column, issue)
159 159 value = column.value_object(issue)
160 160 if value.is_a?(Array)
161 161 value.collect {|v| csv_value(column, issue, v)}.compact.join(', ')
162 162 else
163 163 csv_value(column, issue, value)
164 164 end
165 165 end
166 166
167 167 def csv_value(column, object, value)
168 168 format_object(value, false) do |value|
169 169 case value.class.name
170 170 when 'Float'
171 171 sprintf("%.2f", value).gsub('.', l(:general_csv_decimal_separator))
172 172 when 'IssueRelation'
173 173 value.to_s(object)
174 174 when 'Issue'
175 175 if object.is_a?(TimeEntry)
176 176 "#{value.tracker} ##{value.id}: #{value.subject}"
177 177 else
178 178 value.id
179 179 end
180 180 else
181 181 value
182 182 end
183 183 end
184 184 end
185 185
186 186 def query_to_csv(items, query, options={})
187 187 options ||= {}
188 188 columns = (options[:columns] == 'all' ? query.available_inline_columns : query.inline_columns)
189 189 query.available_block_columns.each do |column|
190 190 if options[column.name].present?
191 191 columns << column
192 192 end
193 193 end
194 194
195 195 Redmine::Export::CSV.generate do |csv|
196 196 # csv header fields
197 197 csv << columns.map {|c| c.caption.to_s}
198 198 # csv lines
199 199 items.each do |item|
200 200 csv << columns.map {|c| csv_content(c, item)}
201 201 end
202 202 end
203 203 end
204 204
205 205 # Retrieve query from session or build a new query
206 206 def retrieve_query
207 207 if !params[:query_id].blank?
208 208 cond = "project_id IS NULL"
209 209 cond << " OR project_id = #{@project.id}" if @project
210 210 @query = IssueQuery.where(cond).find(params[:query_id])
211 211 raise ::Unauthorized unless @query.visible?
212 212 @query.project = @project
213 213 session[:query] = {:id => @query.id, :project_id => @query.project_id}
214 214 sort_clear
215 215 elsif api_request? || params[:set_filter] || session[:query].nil? || session[:query][:project_id] != (@project ? @project.id : nil)
216 216 # Give it a name, required to be valid
217 217 @query = IssueQuery.new(:name => "_")
218 218 @query.project = @project
219 219 @query.build_from_params(params)
220 220 session[:query] = {:project_id => @query.project_id, :filters => @query.filters, :group_by => @query.group_by, :column_names => @query.column_names, :totalable_names => @query.totalable_names}
221 221 else
222 222 # retrieve from session
223 223 @query = nil
224 224 @query = IssueQuery.find_by_id(session[:query][:id]) if session[:query][:id]
225 225 @query ||= IssueQuery.new(:name => "_", :filters => session[:query][:filters], :group_by => session[:query][:group_by], :column_names => session[:query][:column_names], :totalable_names => session[:query][:totalable_names])
226 226 @query.project = @project
227 227 end
228 228 end
229 229
230 230 def retrieve_query_from_session
231 231 if session[:query]
232 232 if session[:query][:id]
233 233 @query = IssueQuery.find_by_id(session[:query][:id])
234 234 return unless @query
235 235 else
236 236 @query = IssueQuery.new(:name => "_", :filters => session[:query][:filters], :group_by => session[:query][:group_by], :column_names => session[:query][:column_names], :totalable_names => session[:query][:totalable_names])
237 237 end
238 238 if session[:query].has_key?(:project_id)
239 239 @query.project_id = session[:query][:project_id]
240 240 else
241 241 @query.project = @project
242 242 end
243 243 @query
244 244 end
245 245 end
246 246 end
@@ -1,162 +1,162
1 1 <%= render :partial => 'action_menu' %>
2 2
3 3 <h2><%= issue_heading(@issue) %></h2>
4 4
5 5 <div class="<%= @issue.css_classes %> details">
6 6 <% if @prev_issue_id || @next_issue_id %>
7 7 <div class="next-prev-links contextual">
8 8 <%= link_to_if @prev_issue_id,
9 9 "\xc2\xab #{l(:label_previous)}",
10 10 (@prev_issue_id ? issue_path(@prev_issue_id) : nil),
11 11 :title => "##{@prev_issue_id}",
12 12 :accesskey => accesskey(:previous) %> |
13 13 <% if @issue_position && @issue_count %>
14 14 <span class="position"><%= l(:label_item_position, :position => @issue_position, :count => @issue_count) %></span> |
15 15 <% end %>
16 16 <%= link_to_if @next_issue_id,
17 17 "#{l(:label_next)} \xc2\xbb",
18 18 (@next_issue_id ? issue_path(@next_issue_id) : nil),
19 19 :title => "##{@next_issue_id}",
20 20 :accesskey => accesskey(:next) %>
21 21 </div>
22 22 <% end %>
23 23
24 24 <%= avatar(@issue.author, :size => "50") %>
25 25
26 26 <div class="subject">
27 27 <%= render_issue_subject_with_tree(@issue) %>
28 28 </div>
29 29 <p class="author">
30 30 <%= authoring @issue.created_on, @issue.author %>.
31 31 <% if @issue.created_on != @issue.updated_on %>
32 32 <%= l(:label_updated_time, time_tag(@issue.updated_on)).html_safe %>.
33 33 <% end %>
34 34 </p>
35 35
36 36 <div class="attributes">
37 37 <%= issue_fields_rows do |rows|
38 38 rows.left l(:field_status), @issue.status.name, :class => 'status'
39 39 rows.left l(:field_priority), @issue.priority.name, :class => 'priority'
40 40
41 41 unless @issue.disabled_core_fields.include?('assigned_to_id')
42 42 rows.left l(:field_assigned_to), avatar(@issue.assigned_to, :size => "14").to_s.html_safe + (@issue.assigned_to ? link_to_user(@issue.assigned_to) : "-"), :class => 'assigned-to'
43 43 end
44 44 unless @issue.disabled_core_fields.include?('category_id') || (@issue.category.nil? && @issue.project.issue_categories.none?)
45 45 rows.left l(:field_category), (@issue.category ? @issue.category.name : "-"), :class => 'category'
46 46 end
47 47 unless @issue.disabled_core_fields.include?('fixed_version_id') || (@issue.fixed_version.nil? && @issue.assignable_versions.none?)
48 48 rows.left l(:field_fixed_version), (@issue.fixed_version ? link_to_version(@issue.fixed_version) : "-"), :class => 'fixed-version'
49 49 end
50 50
51 51 unless @issue.disabled_core_fields.include?('start_date')
52 52 rows.right l(:field_start_date), format_date(@issue.start_date), :class => 'start-date'
53 53 end
54 54 unless @issue.disabled_core_fields.include?('due_date')
55 55 rows.right l(:field_due_date), format_date(@issue.due_date), :class => 'due-date'
56 56 end
57 57 unless @issue.disabled_core_fields.include?('done_ratio')
58 rows.right l(:field_done_ratio), progress_bar(@issue.done_ratio, :width => '80px', :legend => "#{@issue.done_ratio}%"), :class => 'progress'
58 rows.right l(:field_done_ratio), progress_bar(@issue.done_ratio, :legend => "#{@issue.done_ratio}%"), :class => 'progress'
59 59 end
60 60 unless @issue.disabled_core_fields.include?('estimated_hours')
61 61 if @issue.estimated_hours.present? || @issue.total_estimated_hours.to_f > 0
62 62 rows.right l(:field_estimated_hours), issue_estimated_hours_details(@issue), :class => 'estimated-hours'
63 63 end
64 64 end
65 65 if User.current.allowed_to_view_all_time_entries?(@project)
66 66 if @issue.total_spent_hours > 0
67 67 rows.right l(:label_spent_time), issue_spent_hours_details(@issue), :class => 'spent-time'
68 68 end
69 69 end
70 70 end %>
71 71 <%= render_custom_fields_rows(@issue) %>
72 72 <%= call_hook(:view_issues_show_details_bottom, :issue => @issue) %>
73 73 </div>
74 74
75 75 <% if @issue.description? || @issue.attachments.any? -%>
76 76 <hr />
77 77 <% if @issue.description? %>
78 78 <div class="description">
79 79 <div class="contextual">
80 80 <%= link_to l(:button_quote), quoted_issue_path(@issue), :remote => true, :method => 'post', :class => 'icon icon-comment' if authorize_for('issues', 'edit') %>
81 81 </div>
82 82
83 83 <p><strong><%=l(:field_description)%></strong></p>
84 84 <div class="wiki">
85 85 <%= textilizable @issue, :description, :attachments => @issue.attachments %>
86 86 </div>
87 87 </div>
88 88 <% end %>
89 89 <%= link_to_attachments @issue, :thumbnails => true %>
90 90 <% end -%>
91 91
92 92 <%= call_hook(:view_issues_show_description_bottom, :issue => @issue) %>
93 93
94 94 <% if !@issue.leaf? || User.current.allowed_to?(:manage_subtasks, @project) %>
95 95 <hr />
96 96 <div id="issue_tree">
97 97 <div class="contextual">
98 98 <%= link_to_new_subtask(@issue) if User.current.allowed_to?(:manage_subtasks, @project) %>
99 99 </div>
100 100 <p><strong><%=l(:label_subtask_plural)%></strong></p>
101 101 <%= render_descendants_tree(@issue) unless @issue.leaf? %>
102 102 </div>
103 103 <% end %>
104 104
105 105 <% if @relations.present? || User.current.allowed_to?(:manage_issue_relations, @project) %>
106 106 <hr />
107 107 <div id="relations">
108 108 <%= render :partial => 'relations' %>
109 109 </div>
110 110 <% end %>
111 111
112 112 </div>
113 113
114 114 <% if @changesets.present? %>
115 115 <div id="issue-changesets">
116 116 <h3><%=l(:label_associated_revisions)%></h3>
117 117 <%= render :partial => 'changesets', :locals => { :changesets => @changesets} %>
118 118 </div>
119 119 <% end %>
120 120
121 121 <% if @journals.present? %>
122 122 <div id="history">
123 123 <h3><%=l(:label_history)%></h3>
124 124 <%= render :partial => 'history', :locals => { :issue => @issue, :journals => @journals } %>
125 125 </div>
126 126 <% end %>
127 127
128 128
129 129 <div style="clear: both;"></div>
130 130 <%= render :partial => 'action_menu' %>
131 131
132 132 <div style="clear: both;"></div>
133 133 <% if @issue.editable? %>
134 134 <div id="update" style="display:none;">
135 135 <h3><%= l(:button_edit) %></h3>
136 136 <%= render :partial => 'edit' %>
137 137 </div>
138 138 <% end %>
139 139
140 140 <% other_formats_links do |f| %>
141 141 <%= f.link_to 'Atom', :url => {:key => User.current.rss_key} %>
142 142 <%= f.link_to 'PDF' %>
143 143 <% end %>
144 144
145 145 <% html_title "#{@issue.tracker.name} ##{@issue.id}: #{@issue.subject}" %>
146 146
147 147 <% content_for :sidebar do %>
148 148 <%= render :partial => 'issues/sidebar' %>
149 149
150 150 <% if User.current.allowed_to?(:add_issue_watchers, @project) ||
151 151 (@issue.watchers.present? && User.current.allowed_to?(:view_issue_watchers, @project)) %>
152 152 <div id="watchers">
153 153 <%= render :partial => 'watchers/watchers', :locals => {:watched => @issue} %>
154 154 </div>
155 155 <% end %>
156 156 <% end %>
157 157
158 158 <% content_for :header_tags do %>
159 159 <%= auto_discovery_link_tag(:atom, {:format => 'atom', :key => User.current.rss_key}, :title => "#{@issue.project} - #{@issue.tracker} ##{@issue.id}: #{@issue.subject}") %>
160 160 <% end %>
161 161
162 162 <%= context_menu issues_context_menu_path %>
@@ -1,33 +1,32
1 1 <%= form_tag({}, :id => "status_by_form") do -%>
2 2 <fieldset>
3 3 <legend>
4 4 <%= l(:label_issues_by,
5 5 select_tag('status_by',
6 6 status_by_options_for_select(criteria),
7 7 :id => 'status_by_select',
8 8 :data => {:remote => true, :method => 'post', :url => status_by_version_path(version)})).html_safe %>
9 9 </legend>
10 10 <% if counts.empty? %>
11 11 <p><em><%= l(:label_no_data) %></em></p>
12 12 <% else %>
13 13 <table>
14 14 <% counts.each do |count| %>
15 15 <tr>
16 16 <td style="width:130px; text-align:right;">
17 17 <% if count[:group] -%>
18 18 <%= link_to(count[:group], project_issues_path(version.project, :set_filter => 1, :status_id => '*', :fixed_version_id => version, "#{criteria}_id" => count[:group])) %>
19 19 <% else -%>
20 20 <%= link_to(l(:label_none), project_issues_path(version.project, :set_filter => 1, :status_id => '*', :fixed_version_id => version, "#{criteria}_id" => "!*")) %>
21 21 <% end %>
22 22 </td>
23 23 <td style="width:240px;">
24 24 <%= progress_bar((count[:closed].to_f / count[:total])*100,
25 :legend => "#{count[:closed]}/#{count[:total]}",
26 :width => "#{(count[:total].to_f / max * 200).floor}px;") %>
25 :legend => "#{count[:closed]}/#{count[:total]}") %>
27 26 </td>
28 27 </tr>
29 28 <% end %>
30 29 </table>
31 30 <% end %>
32 31 </fieldset>
33 32 <% end %>
@@ -1,33 +1,35
1 <div class="version-overview">
1 2 <% if version.completed? %>
2 3 <p><%= format_date(version.effective_date) %></p>
3 4 <% elsif version.effective_date %>
4 5 <p><strong><%= due_date_distance_in_words(version.effective_date) %></strong> (<%= format_date(version.effective_date) %>)</p>
5 6 <% end %>
6 7
7 8 <p><%=h version.description %></p>
8 9 <% if version.custom_field_values.any? %>
9 10 <ul>
10 11 <% render_custom_field_values(version) do |custom_field, formatted| %>
11 12 <li><span class="label"><%= custom_field.name %>:</span> <%= formatted %></li>
12 13 <% end %>
13 14 </ul>
14 15 <% end %>
15 16
16 17 <% if version.issues_count > 0 %>
17 18 <%= progress_bar([version.closed_percent, version.completed_percent],
18 :width => '40em', :legend => ('%0.0f%' % version.completed_percent)) %>
19 :legend => ('%0.0f%' % version.completed_percent)) %>
19 20 <p class="progress-info">
20 21 <%= link_to(l(:label_x_issues, :count => version.issues_count),
21 22 version_filtered_issues_path(version, :status_id => '*')) %>
22 23 &nbsp;
23 24 (<%= link_to_if(version.closed_issues_count > 0,
24 25 l(:label_x_closed_issues_abbr, :count => version.closed_issues_count),
25 26 version_filtered_issues_path(version, :status_id => 'c')) %>
26 27 &#8212;
27 28 <%= link_to_if(version.open_issues_count > 0,
28 29 l(:label_x_open_issues_abbr, :count => version.open_issues_count),
29 30 version_filtered_issues_path(version, :status_id => 'o')) %>)
30 31 </p>
31 32 <% else %>
32 33 <p class="progress-info"><%= l(:label_roadmap_no_issues) %></p>
33 34 <% end %>
35 </div>
@@ -1,1262 +1,1265
1 1 html {overflow-y:scroll;}
2 2 body { font-family: Verdana, sans-serif; font-size: 12px; color:#333; margin: 0; padding: 0; min-width: 900px; }
3 3
4 4 h1, h2, h3, h4 {font-family: "Trebuchet MS", Verdana, sans-serif;padding: 2px 10px 1px 0px;margin: 0 0 10px 0;}
5 5 #content h1, h2, h3, h4 {color: #555;}
6 6 h2, .wiki h1 {font-size: 20px;}
7 7 h3, .wiki h2 {font-size: 16px;}
8 8 h4, .wiki h3 {font-size: 13px;}
9 9 h4 {border-bottom: 1px dotted #bbb;}
10 10 pre, code {font-family: Consolas, Menlo, "Liberation Mono", Courier, monospace;}
11 11
12 12 /***** Layout *****/
13 13 #wrapper {background: white;}
14 14
15 15 #top-menu {background: #3E5B76; color: #fff; height:1.8em; font-size: 0.8em; padding: 2px 2px 0px 6px;}
16 16 #top-menu ul {margin: 0; padding: 0;}
17 17 #top-menu li {
18 18 float:left;
19 19 list-style-type:none;
20 20 margin: 0px 0px 0px 0px;
21 21 padding: 0px 0px 0px 0px;
22 22 white-space:nowrap;
23 23 }
24 24 #top-menu a {color: #fff; margin-right: 8px; font-weight: bold;}
25 25 #top-menu #loggedas { float: right; margin-right: 0.5em; color: #fff; }
26 26
27 27 #account {float:right;}
28 28
29 29 #header {min-height:5.3em;margin:0;background-color:#628DB6;color:#f8f8f8; padding: 4px 8px 20px 6px; position:relative;}
30 30 #header a {color:#f8f8f8;}
31 31 #header h1 a.ancestor { font-size: 80%; }
32 32 #quick-search {float:right;}
33 33
34 34 #main-menu {position: absolute; bottom: 0px; left:6px; margin-right: -500px;}
35 35 #main-menu ul {margin: 0; padding: 0;}
36 36 #main-menu li {
37 37 float:left;
38 38 list-style-type:none;
39 39 margin: 0px 2px 0px 0px;
40 40 padding: 0px 0px 0px 0px;
41 41 white-space:nowrap;
42 42 }
43 43 #main-menu li a {
44 44 display: block;
45 45 color: #fff;
46 46 text-decoration: none;
47 47 font-weight: bold;
48 48 margin: 0;
49 49 padding: 4px 10px 4px 10px;
50 50 }
51 51 #main-menu li a:hover {background:#759FCF; color:#fff;}
52 52 #main-menu li a.selected, #main-menu li a.selected:hover {background:#fff; color:#555;}
53 53
54 54 #admin-menu ul {margin: 0; padding: 0;}
55 55 #admin-menu li {margin: 0; padding: 0 0 6px 0; list-style-type:none;}
56 56
57 57 #admin-menu a { background-position: 0% 40%; background-repeat: no-repeat; padding-left: 20px; padding-top: 2px; padding-bottom: 3px;}
58 58 #admin-menu a.projects { background-image: url(../images/projects.png); }
59 59 #admin-menu a.users { background-image: url(../images/user.png); }
60 60 #admin-menu a.groups { background-image: url(../images/group.png); }
61 61 #admin-menu a.roles { background-image: url(../images/database_key.png); }
62 62 #admin-menu a.trackers { background-image: url(../images/ticket.png); }
63 63 #admin-menu a.issue_statuses { background-image: url(../images/ticket_edit.png); }
64 64 #admin-menu a.workflows { background-image: url(../images/ticket_go.png); }
65 65 #admin-menu a.custom_fields { background-image: url(../images/textfield.png); }
66 66 #admin-menu a.enumerations { background-image: url(../images/text_list_bullets.png); }
67 67 #admin-menu a.settings { background-image: url(../images/changeset.png); }
68 68 #admin-menu a.plugins { background-image: url(../images/plugin.png); }
69 69 #admin-menu a.info { background-image: url(../images/help.png); }
70 70 #admin-menu a.server_authentication { background-image: url(../images/server_key.png); }
71 71
72 72 #main {background-color:#EEEEEE;}
73 73
74 74 #sidebar{ float: right; width: 22%; position: relative; z-index: 9; padding: 0; margin: 0;}
75 75 * html #sidebar{ width: 22%; }
76 76 #sidebar h3{ font-size: 14px; margin-top:14px; color: #666; }
77 77 #sidebar hr{ width: 100%; margin: 0 auto; height: 1px; background: #ccc; border: 0; }
78 78 * html #sidebar hr{ width: 95%; position: relative; left: -6px; color: #ccc; }
79 79 #sidebar .contextual { margin-right: 1em; }
80 80 #sidebar ul, ul.flat {margin: 0; padding: 0;}
81 81 #sidebar ul li, ul.flat li {list-style-type:none;margin: 0px 2px 0px 0px; padding: 0px 0px 0px 0px;}
82 82
83 83 #content { width: 75%; background-color: #fff; margin: 0px; border-right: 1px solid #ddd; padding: 6px 10px 10px 10px; z-index: 10; }
84 84 * html #content{ width: 75%; padding-left: 0; margin-top: 0px; padding: 6px 10px 10px 10px;}
85 85 html>body #content { min-height: 600px; }
86 86 * html body #content { height: 600px; } /* IE */
87 87
88 88 #main.nosidebar #sidebar{ display: none; }
89 89 #main.nosidebar #content{ width: auto; border-right: 0; }
90 90
91 91 #footer {clear: both; border-top: 1px solid #bbb; font-size: 0.9em; color: #aaa; padding: 5px; text-align:center; background:#fff;}
92 92
93 93 #login-form table {margin-top:5em; padding:1em; margin-left: auto; margin-right: auto; border: 2px solid #FDBF3B; background-color:#FFEBC1; }
94 94 #login-form table td {padding: 6px;}
95 95 #login-form label {font-weight: bold;}
96 96 #login-form input#username, #login-form input#password { width: 300px; }
97 97
98 98 div.modal { border-radius:5px; background:#fff; z-index:50; padding:4px;}
99 99 div.modal h3.title {display:none;}
100 100 div.modal p.buttons {text-align:right; margin-bottom:0;}
101 101 div.modal .box p {margin: 0.3em 0;}
102 102
103 103 input#openid_url { background: url(../images/openid-bg.gif) no-repeat; background-color: #fff; background-position: 0 50%; padding-left: 18px; }
104 104
105 105 .clear:after{ content: "."; display: block; height: 0; clear: both; visibility: hidden; }
106 106
107 107 /***** Links *****/
108 108 a, a:link, a:visited{ color: #169; text-decoration: none; }
109 109 a:hover, a:active{ color: #c61a1a; text-decoration: underline;}
110 110 a img{ border: 0; }
111 111
112 112 a.issue.closed, a.issue.closed:link, a.issue.closed:visited { color: #999; text-decoration: line-through; }
113 113 a.project.closed, a.project.closed:link, a.project.closed:visited { color: #999; }
114 114 a.user.locked, a.user.locked:link, a.user.locked:visited {color: #999;}
115 115
116 116 #sidebar a.selected {line-height:1.7em; padding:1px 3px 2px 2px; margin-left:-2px; background-color:#9DB9D5; color:#fff; border-radius:2px;}
117 117 #sidebar a.selected:hover {text-decoration:none;}
118 118 #admin-menu a {line-height:1.7em;}
119 119 #admin-menu a.selected {padding-left: 20px !important; background-position: 2px 40%;}
120 120
121 121 a.collapsible {padding-left: 12px; background: url(../images/arrow_expanded.png) no-repeat -3px 40%;}
122 122 a.collapsible.collapsed {background: url(../images/arrow_collapsed.png) no-repeat -5px 40%;}
123 123
124 124 a#toggle-completed-versions {color:#999;}
125 125 /***** Tables *****/
126 126 table.list { border: 1px solid #e4e4e4; border-collapse: collapse; width: 100%; margin-bottom: 4px; }
127 127 table.list th { background-color:#EEEEEE; padding: 4px; white-space:nowrap; }
128 128 table.list td {text-align:center; vertical-align:top; padding-right:10px;}
129 129 table.list td.id { width: 2%; text-align: center;}
130 130 table.list td.name, table.list td.description, table.list td.subject, table.list td.comments, table.list td.roles {text-align: left;}
131 131 table.list td.tick {width:15%}
132 132 table.list td.checkbox { width: 15px; padding: 2px 0 0 0; }
133 133 table.list td.checkbox input {padding:0px;}
134 134 table.list td.buttons { width: 15%; white-space:nowrap; text-align: right; }
135 135 table.list td.buttons a { padding-right: 0.6em; }
136 136 table.list td.buttons img {vertical-align:middle;}
137 137 table.list td.reorder {width:15%; white-space:nowrap; text-align:center; }
138 138 table.list caption { text-align: left; padding: 0.5em 0.5em 0.5em 0; }
139 139
140 140 tr.project td.name a { white-space:nowrap; }
141 141 tr.project.closed, tr.project.archived { color: #aaa; }
142 142 tr.project.closed a, tr.project.archived a { color: #aaa; }
143 143
144 144 tr.project.idnt td.name span {background: url(../images/bullet_arrow_right.png) no-repeat 0 50%; padding-left: 16px;}
145 145 tr.project.idnt-1 td.name {padding-left: 0.5em;}
146 146 tr.project.idnt-2 td.name {padding-left: 2em;}
147 147 tr.project.idnt-3 td.name {padding-left: 3.5em;}
148 148 tr.project.idnt-4 td.name {padding-left: 5em;}
149 149 tr.project.idnt-5 td.name {padding-left: 6.5em;}
150 150 tr.project.idnt-6 td.name {padding-left: 8em;}
151 151 tr.project.idnt-7 td.name {padding-left: 9.5em;}
152 152 tr.project.idnt-8 td.name {padding-left: 11em;}
153 153 tr.project.idnt-9 td.name {padding-left: 12.5em;}
154 154
155 155 tr.issue { text-align: center; white-space: nowrap; }
156 156 tr.issue td.subject, tr.issue td.category, td.assigned_to, tr.issue td.string, tr.issue td.text, tr.issue td.relations, tr.issue td.parent { white-space: normal; }
157 157 tr.issue td.relations { text-align: left; }
158 158 tr.issue td.done_ratio table.progress { margin-left:auto; margin-right: auto;}
159 159 tr.issue td.relations span {white-space: nowrap;}
160 160 table.issues td.description {color:#777; font-size:90%; padding:4px 4px 4px 24px; text-align:left; white-space:normal;}
161 161 table.issues td.description pre {white-space:normal;}
162 162
163 163 tr.issue.idnt td.subject a {background: url(../images/bullet_arrow_right.png) no-repeat 0 50%; padding-left: 16px;}
164 164 tr.issue.idnt-1 td.subject {padding-left: 0.5em;}
165 165 tr.issue.idnt-2 td.subject {padding-left: 2em;}
166 166 tr.issue.idnt-3 td.subject {padding-left: 3.5em;}
167 167 tr.issue.idnt-4 td.subject {padding-left: 5em;}
168 168 tr.issue.idnt-5 td.subject {padding-left: 6.5em;}
169 169 tr.issue.idnt-6 td.subject {padding-left: 8em;}
170 170 tr.issue.idnt-7 td.subject {padding-left: 9.5em;}
171 171 tr.issue.idnt-8 td.subject {padding-left: 11em;}
172 172 tr.issue.idnt-9 td.subject {padding-left: 12.5em;}
173 173
174 174 table.issue-report {table-layout:fixed;}
175 175
176 176 tr.entry { border: 1px solid #f8f8f8; }
177 177 tr.entry td { white-space: nowrap; }
178 178 tr.entry td.filename {width:30%; text-align:left;}
179 179 tr.entry td.filename_no_report {width:70%; text-align:left;}
180 180 tr.entry td.size { text-align: right; font-size: 90%; }
181 181 tr.entry td.revision, tr.entry td.author { text-align: center; }
182 182 tr.entry td.age { text-align: right; }
183 183 tr.entry.file td.filename a { margin-left: 16px; }
184 184 tr.entry.file td.filename_no_report a { margin-left: 16px; }
185 185
186 186 tr span.expander {background-image: url(../images/bullet_toggle_plus.png); padding-left: 8px; margin-left: 0; cursor: pointer;}
187 187 tr.open span.expander {background-image: url(../images/bullet_toggle_minus.png);}
188 188
189 189 tr.changeset { height: 20px }
190 190 tr.changeset ul, ol { margin-top: 0px; margin-bottom: 0px; }
191 191 tr.changeset td.revision_graph { width: 15%; background-color: #fffffb; }
192 192 tr.changeset td.author { text-align: center; width: 15%; white-space:nowrap;}
193 193 tr.changeset td.committed_on { text-align: center; width: 15%; white-space:nowrap;}
194 194
195 195 table.files tbody th {text-align:left;}
196 196 table.files tr.file td.filename { text-align: left; padding-left: 24px; }
197 197 table.files tr.file td.digest { font-size: 80%; }
198 198
199 199 table.members td.roles, table.memberships td.roles { width: 45%; }
200 200
201 201 tr.message { height: 2.6em; }
202 202 tr.message td.subject { padding-left: 20px; }
203 203 tr.message td.created_on { white-space: nowrap; }
204 204 tr.message td.last_message { font-size: 80%; white-space: nowrap; }
205 205 tr.message.locked td.subject { background: url(../images/locked.png) no-repeat 0 1px; }
206 206 tr.message.sticky td.subject { background: url(../images/bullet_go.png) no-repeat 0 1px; font-weight: bold; }
207 207
208 208 tr.version.closed, tr.version.closed a { color: #999; }
209 209 tr.version td.name { padding-left: 20px; }
210 210 tr.version.shared td.name { background: url(../images/link.png) no-repeat 0% 70%; }
211 211 tr.version td.date, tr.version td.status, tr.version td.sharing { text-align: center; white-space:nowrap; }
212 212
213 213 tr.user td {width:13%;white-space: nowrap;}
214 214 td.username, td.firstname, td.lastname, td.email {text-align:left !important;}
215 215 tr.user td.email { width:18%; }
216 216 tr.user.locked, tr.user.registered { color: #aaa; }
217 217 tr.user.locked a, tr.user.registered a { color: #aaa; }
218 218
219 219 table.permissions td.role {color:#999;font-size:90%;font-weight:normal !important;text-align:center;vertical-align:bottom;}
220 220
221 221 tr.wiki-page-version td.updated_on, tr.wiki-page-version td.author {text-align:center;}
222 222
223 223 tr.time-entry { text-align: center; white-space: nowrap; }
224 224 tr.time-entry td.issue, tr.time-entry td.comments, tr.time-entry td.subject, tr.time-entry td.activity { text-align: left; white-space: normal; }
225 225 td.hours { text-align: right; font-weight: bold; padding-right: 0.5em; }
226 226 td.hours .hours-dec { font-size: 0.9em; }
227 227
228 228 table.plugins td { vertical-align: middle; }
229 229 table.plugins td.configure { text-align: right; padding-right: 1em; }
230 230 table.plugins span.name { font-weight: bold; display: block; margin-bottom: 6px; }
231 231 table.plugins span.description { display: block; font-size: 0.9em; }
232 232 table.plugins span.url { display: block; font-size: 0.9em; }
233 233
234 234 tr.group td { padding: 0.8em 0 0.5em 0.3em; border-bottom: 1px solid #ccc; text-align:left; }
235 235 tr.group span.name {font-weight:bold;}
236 236 tr.group span.count {font-weight:bold; position:relative; top:-1px; color:#fff; font-size:10px; background:#9DB9D5; padding:0px 6px 1px 6px; border-radius:3px; margin-left:4px;}
237 237 tr.group span.totals {color: #aaa; font-size: 80%;}
238 238 tr.group span.totals .value {font-weight:bold; color:#777;}
239 239 tr.group a.toggle-all { color: #aaa; font-size: 80%; display:none; float:right; margin-right:4px;}
240 240 tr.group:hover a.toggle-all { display:inline;}
241 241 a.toggle-all:hover {text-decoration:none;}
242 242
243 243 table.list tbody tr:hover { background-color:#ffffdd; }
244 244 table.list tbody tr.group:hover { background-color:inherit; }
245 245 table td {padding:2px;}
246 246 table p {margin:0;}
247 247 .odd {background-color:#f6f7f8;}
248 248 .even {background-color: #fff;}
249 249
250 250 tr.builtin td.name {font-style:italic;}
251 251
252 252 a.sort { padding-right: 16px; background-position: 100% 50%; background-repeat: no-repeat; }
253 253 a.sort.asc { background-image: url(../images/sort_asc.png); }
254 254 a.sort.desc { background-image: url(../images/sort_desc.png); }
255 255
256 256 table.boards a.board, h3.comments { background: url(../images/comment.png) no-repeat 0% 50%; padding-left: 20px; }
257 257 table.boards td.last-message {text-align:left;font-size:80%;}
258 258
259 259 table.messages td.last_message {text-align:left;}
260 260
261 261 #query_form_content {font-size:90%;}
262 262
263 263 .query_sort_criteria_count {
264 264 display: inline-block;
265 265 min-width: 1em;
266 266 }
267 267
268 268 table.query-columns {
269 269 border-collapse: collapse;
270 270 border: 0;
271 271 }
272 272
273 273 table.query-columns td.buttons {
274 274 vertical-align: middle;
275 275 text-align: center;
276 276 }
277 277 table.query-columns td.buttons input[type=button] {width:35px;}
278 278 .query-totals {text-align:right; margin-top:-2.3em;}
279 279 .query-totals>span {margin-left:0.6em;}
280 280 .query-totals .value {font-weight:bold;}
281 281
282 282 td.center {text-align:center;}
283 283
284 284 h3.version { background: url(../images/package.png) no-repeat 0% 50%; padding-left: 20px; }
285 285
286 286 div.issues h3 { background: url(../images/ticket.png) no-repeat 0% 50%; padding-left: 20px; }
287 287 div.members h3 { background: url(../images/group.png) no-repeat 0% 50%; padding-left: 20px; }
288 288 div.news h3 { background: url(../images/news.png) no-repeat 0% 50%; padding-left: 20px; }
289 289 div.projects h3 { background: url(../images/projects.png) no-repeat 0% 50%; padding-left: 20px; }
290 290
291 291 #watchers select {width: 95%; display: block;}
292 292 #watchers a.delete {opacity: 0.4; vertical-align: middle;}
293 293 #watchers a.delete:hover {opacity: 1;}
294 294 #watchers img.gravatar {margin: 0 4px 2px 0;}
295 295
296 296 span#watchers_inputs {overflow:auto; display:block;}
297 297 span.search_for_watchers {display:block;}
298 298 span.search_for_watchers, span.add_attachment {font-size:80%; line-height:2.5em;}
299 299 span.search_for_watchers a, span.add_attachment a {padding-left:16px; background: url(../images/bullet_add.png) no-repeat 0 50%; }
300 300
301 301
302 302 .highlight { background-color: #FCFD8D;}
303 303 .highlight.token-1 { background-color: #faa;}
304 304 .highlight.token-2 { background-color: #afa;}
305 305 .highlight.token-3 { background-color: #aaf;}
306 306
307 307 .box{
308 308 padding:6px;
309 309 margin-bottom: 10px;
310 310 background-color:#f6f6f6;
311 311 color:#505050;
312 312 line-height:1.5em;
313 313 border: 1px solid #e4e4e4;
314 314 word-wrap: break-word;
315 315 border-radius: 3px;
316 316 }
317 317
318 318 div.square {
319 319 border: 1px solid #999;
320 320 float: left;
321 321 margin: .3em .4em 0 .4em;
322 322 overflow: hidden;
323 323 width: .6em; height: .6em;
324 324 }
325 325 .contextual {float:right; white-space: nowrap; line-height:1.4em;margin-top:5px; padding-left: 10px; font-size:0.9em;}
326 326 .contextual input, .contextual select {font-size:0.9em;}
327 327 .message .contextual { margin-top: 0; }
328 328
329 329 .splitcontent {overflow:auto;}
330 330 .splitcontentleft{float:left; width:49%;}
331 331 .splitcontentright{float:right; width:49%;}
332 332 form {display: inline;}
333 333 input, select {vertical-align: middle; margin-top: 1px; margin-bottom: 1px;}
334 334 fieldset {border: 1px solid #e4e4e4; margin:0;}
335 335 legend {color: #333;}
336 336 hr { width: 100%; height: 1px; background: #ccc; border: 0;}
337 337 blockquote { font-style: italic; border-left: 3px solid #e0e0e0; padding-left: 0.6em; margin-left: 2.4em;}
338 338 blockquote blockquote { margin-left: 0;}
339 339 abbr, span.field-description[title] { border-bottom: 1px dotted #aaa; cursor: help; }
340 340 textarea.wiki-edit {width:99%; resize:vertical;}
341 341 li p {margin-top: 0;}
342 342 div.issue {background:#ffffdd; padding:6px; margin-bottom:6px; border: 1px solid #d7d7d7; border-radius:3px;}
343 343 p.breadcrumb { font-size: 0.9em; margin: 4px 0 4px 0;}
344 344 p.subtitle { font-size: 0.9em; margin: -6px 0 12px 0; font-style: italic; }
345 345 p.footnote { font-size: 0.9em; margin-top: 0px; margin-bottom: 0px; }
346 346 .ltr {direction:ltr !important; unicode-bidi:bidi-override;}
347 347 .rtl {direction:rtl !important; unicode-bidi:bidi-override;}
348 348
349 349 div.issue div.subject div div { padding-left: 16px; }
350 350 div.issue div.subject p {margin: 0; margin-bottom: 0.1em; font-size: 90%; color: #999;}
351 351 div.issue div.subject>div>p { margin-top: 0.5em; }
352 352 div.issue div.subject h3 {margin: 0; margin-bottom: 0.1em;}
353 353 div.issue span.private, div.journal span.private { position:relative; bottom: 2px; text-transform: uppercase; background: #d22; color: #fff; font-weight:bold; padding: 0px 2px 0px 2px; font-size: 60%; margin-right: 2px; border-radius: 2px;}
354 354 div.issue .next-prev-links {color:#999;}
355 355 div.issue .attributes {margin-top: 2em;}
356 356 div.issue .attribute {padding-left:180px; clear:left; min-height: 1.8em;}
357 357 div.issue .attribute .label {width: 170px; margin-left:-180px; font-weight:bold; float:left;}
358 358 div.issue.overdue .due-date .value { color: #c22; }
359 359
360 360 #issue_tree table.issues, #relations table.issues { border: 0; }
361 361 #issue_tree td.checkbox, #relations td.checkbox {display:none;}
362 362 #relations td.buttons {padding:0;}
363 363
364 364 fieldset.collapsible {border-width: 1px 0 0 0;}
365 365 fieldset.collapsible>legend { padding-left: 16px; background: url(../images/arrow_expanded.png) no-repeat 0% 40%; cursor:pointer; }
366 366 fieldset.collapsible.collapsed>legend { background-image: url(../images/arrow_collapsed.png); }
367 367
368 368 fieldset#date-range p { margin: 2px 0 2px 0; }
369 369 fieldset#filters table { border-collapse: collapse; }
370 370 fieldset#filters table td { padding: 0; vertical-align: middle; }
371 371 fieldset#filters tr.filter { height: 2.1em; }
372 372 fieldset#filters td.field { width:230px; }
373 373 fieldset#filters td.operator { width:180px; }
374 374 fieldset#filters td.operator select {max-width:170px;}
375 375 fieldset#filters td.values { white-space:nowrap; }
376 376 fieldset#filters td.values select {min-width:130px;}
377 377 fieldset#filters td.values input {height:1em;}
378 378
379 379 #filters-table {width:60%; float:left;}
380 380 .add-filter {width:35%; float:right; text-align: right; vertical-align: top;}
381 381
382 382 #issue_is_private_wrap {float:right; margin-right:1em;}
383 383 .toggle-multiselect {background: url(../images/bullet_toggle_plus.png) no-repeat 0% 40%; padding-left:8px; margin-left:0; cursor:pointer;}
384 384 .buttons { font-size: 0.9em; margin-bottom: 1.4em; margin-top: 1em; }
385 385
386 386 div#issue-changesets {float:right; width:45%; margin-left: 1em; margin-bottom: 1em; background: #fff; padding-left: 1em; font-size: 90%;}
387 387 div#issue-changesets div.changeset { padding: 4px;}
388 388 div#issue-changesets div.changeset { border-bottom: 1px solid #ddd; }
389 389 div#issue-changesets p { margin-top: 0; margin-bottom: 1em;}
390 390
391 391 .journal ul.details img {margin:0 0 -3px 4px;}
392 392 div.journal {overflow:auto;}
393 393 div.journal.private-notes {border-left:2px solid #d22; padding-left:4px; margin-left:-6px;}
394 394 div.journal ul.details {color:#959595; margin-bottom: 1.5em;}
395 395 div.journal ul.details a {color:#70A7CD;}
396 396 div.journal ul.details a:hover {color:#D14848;}
397 397
398 398 div#activity dl, #search-results { margin-left: 2em; }
399 399 div#activity dd, #search-results dd { margin-bottom: 1em; padding-left: 18px; font-size: 0.9em; }
400 400 div#activity dt, #search-results dt { margin-bottom: 0px; padding-left: 20px; line-height: 18px; background-position: 0 50%; background-repeat: no-repeat; }
401 401 div#activity dt.me .time { border-bottom: 1px solid #999; }
402 402 div#activity dt .time { color: #777; font-size: 80%; }
403 403 div#activity dd .description, #search-results dd .description { font-style: italic; }
404 404 div#activity span.project:after, #search-results span.project:after { content: " -"; }
405 405 div#activity dd span.description, #search-results dd span.description { display:block; color: #808080; }
406 406 div#activity dt.grouped {margin-left:5em;}
407 407 div#activity dd.grouped {margin-left:9em;}
408 408
409 409 #search-results dd { margin-bottom: 1em; padding-left: 20px; margin-left:0px; }
410 410
411 411 div#search-results-counts {float:right;}
412 412 div#search-results-counts ul { margin-top: 0.5em; }
413 413 div#search-results-counts li { list-style-type:none; float: left; margin-left: 1em; }
414 414
415 415 dt.issue { background-image: url(../images/ticket.png); }
416 416 dt.issue-edit { background-image: url(../images/ticket_edit.png); }
417 417 dt.issue-closed { background-image: url(../images/ticket_checked.png); }
418 418 dt.issue-note { background-image: url(../images/ticket_note.png); }
419 419 dt.changeset { background-image: url(../images/changeset.png); }
420 420 dt.news { background-image: url(../images/news.png); }
421 421 dt.message { background-image: url(../images/message.png); }
422 422 dt.reply { background-image: url(../images/comments.png); }
423 423 dt.wiki-page { background-image: url(../images/wiki_edit.png); }
424 424 dt.attachment { background-image: url(../images/attachment.png); }
425 425 dt.document { background-image: url(../images/document.png); }
426 426 dt.project { background-image: url(../images/projects.png); }
427 427 dt.time-entry { background-image: url(../images/time.png); }
428 428
429 429 #search-results dt.issue.closed { background-image: url(../images/ticket_checked.png); }
430 430
431 431 div#roadmap .related-issues { margin-bottom: 1em; }
432 432 div#roadmap .related-issues td.checkbox { display: none; }
433 433 div#roadmap .wiki h1:first-child { display: none; }
434 434 div#roadmap .wiki h1 { font-size: 120%; }
435 435 div#roadmap .wiki h2 { font-size: 110%; }
436 436 body.controller-versions.action-show div#roadmap .related-issues {width:70%;}
437 437
438 438 div#version-summary { float:right; width:28%; margin-left: 16px; margin-bottom: 16px; background-color: #fff; }
439 439 div#version-summary fieldset { margin-bottom: 1em; }
440 440 div#version-summary fieldset.time-tracking table { width:100%; }
441 441 div#version-summary th, div#version-summary td.total-hours { text-align: right; }
442 442
443 443 table#time-report td.hours, table#time-report th.period, table#time-report th.total { text-align: right; padding-right: 0.5em; }
444 444 table#time-report tbody tr.subtotal { font-style: italic; color:#777;}
445 445 table#time-report tbody tr.subtotal td.hours { color:#b0b0b0; }
446 446 table#time-report tbody tr.total { font-weight: bold; background-color:#EEEEEE; border-top:1px solid #e4e4e4;}
447 447 table#time-report .hours-dec { font-size: 0.9em; }
448 448
449 449 div.wiki-page .contextual a {opacity: 0.4}
450 450 div.wiki-page .contextual a:hover {opacity: 1}
451 451
452 452 form .attributes select { width: 60%; }
453 453 input#issue_subject, input#document_title { width: 99%; }
454 454 select#issue_done_ratio { width: 95px; }
455 455
456 456 ul.projects {margin:0; padding-left:1em;}
457 457 ul.projects ul {padding-left:1.6em;}
458 458 ul.projects.root {margin:0; padding:0;}
459 459 ul.projects li {list-style-type:none;}
460 460
461 461 #projects-index ul.projects ul.projects { border-left: 3px solid #e0e0e0; padding-left:1em;}
462 462 #projects-index ul.projects li.root {margin-bottom: 1em;}
463 463 #projects-index ul.projects li.child {margin-top: 1em;}
464 464 #projects-index ul.projects div.root a.project { font-family: "Trebuchet MS", Verdana, sans-serif; font-weight: bold; font-size: 16px; margin: 0 0 10px 0; }
465 465 .my-project { padding-left: 18px; background: url(../images/fav.png) no-repeat 0 50%; }
466 466
467 467 #notified-projects>ul, #tracker_project_ids>ul, #custom_field_project_ids>ul {max-height:250px; overflow-y:auto;}
468 468
469 469 #related-issues li img {vertical-align:middle;}
470 470
471 471 ul.properties {padding:0; font-size: 0.9em; color: #777;}
472 472 ul.properties li {list-style-type:none;}
473 473 ul.properties li span {font-style:italic;}
474 474
475 475 .total-hours { font-size: 110%; font-weight: bold; }
476 476 .total-hours span.hours-int { font-size: 120%; }
477 477
478 478 .autoscroll {overflow-x: auto; padding:1px; margin-bottom: 1.2em;}
479 479 #user_login, #user_firstname, #user_lastname, #user_mail, #my_account_form select, #user_form select, #user_identity_url { width: 90%; }
480 480
481 481 #workflow_copy_form select { width: 200px; }
482 482 table.transitions td.enabled {background: #bfb;}
483 483 #workflow_form table select {font-size:90%; max-width:100px;}
484 484 table.fields_permissions td.readonly {background:#ddd;}
485 485 table.fields_permissions td.required {background:#d88;}
486 486
487 487 select.expandable {vertical-align:top;}
488 488
489 489 textarea#custom_field_possible_values {width: 95%; resize:vertical}
490 490 textarea#custom_field_default_value {width: 95%; resize:vertical}
491 491 .sort-handle {display:inline-block; vertical-align:middle;}
492 492
493 493 input#content_comments {width: 99%}
494 494
495 495 p.pagination {margin-top:8px; font-size: 90%}
496 496
497 497 #search-form fieldset p {margin:0.2em 0;}
498 498
499 499 /***** Tabular forms ******/
500 500 .tabular p{
501 501 margin: 0;
502 502 padding: 3px 0 3px 0;
503 503 padding-left: 180px; /* width of left column containing the label elements */
504 504 min-height: 1.8em;
505 505 clear:left;
506 506 }
507 507
508 508 html>body .tabular p {overflow:hidden;}
509 509
510 510 .tabular input, .tabular select {max-width:95%}
511 511 .tabular textarea {width:95%; resize:vertical;}
512 512
513 513 .tabular label{
514 514 font-weight: bold;
515 515 float: left;
516 516 text-align: right;
517 517 /* width of left column */
518 518 margin-left: -180px;
519 519 /* width of labels. Should be smaller than left column to create some right margin */
520 520 width: 175px;
521 521 }
522 522
523 523 .tabular label.floating{
524 524 font-weight: normal;
525 525 margin-left: 0px;
526 526 text-align: left;
527 527 width: 270px;
528 528 }
529 529
530 530 .tabular label.block{
531 531 font-weight: normal;
532 532 margin-left: 0px !important;
533 533 text-align: left;
534 534 float: none;
535 535 display: block;
536 536 width: auto !important;
537 537 }
538 538
539 539 .tabular label.inline{
540 540 font-weight: normal;
541 541 float:none;
542 542 margin-left: 5px !important;
543 543 width: auto;
544 544 }
545 545
546 546 label.no-css {
547 547 font-weight: inherit;
548 548 float:none;
549 549 text-align:left;
550 550 margin-left:0px;
551 551 width:auto;
552 552 }
553 553 input#time_entry_comments { width: 90%;}
554 554
555 555 #preview fieldset {margin-top: 1em; background: url(../images/draft.png)}
556 556
557 557 .tabular.settings p{ padding-left: 300px; }
558 558 .tabular.settings label{ margin-left: -300px; width: 295px; }
559 559 .tabular.settings textarea { width: 99%; }
560 560
561 561 .settings.enabled_scm table {width:100%}
562 562 .settings.enabled_scm td.scm_name{ font-weight: bold; }
563 563
564 564 fieldset.settings label { display: block; }
565 565 fieldset#notified_events .parent { padding-left: 20px; }
566 566
567 567 span.required {color: #bb0000;}
568 568 .summary {font-style: italic;}
569 569
570 570 .check_box_group {
571 571 display:block;
572 572 width:95%;
573 573 max-height:300px;
574 574 overflow-y:auto;
575 575 padding:2px 4px 4px 2px;
576 576 background:#fff;
577 577 border:1px solid #9EB1C2;
578 578 border-radius:2px
579 579 }
580 580 .check_box_group label {
581 581 font-weight: normal;
582 582 margin-left: 0px !important;
583 583 text-align: left;
584 584 float: none;
585 585 display: block;
586 586 width: auto;
587 587 }
588 588 .check_box_group.bool_cf {border:0; background:inherit;}
589 589 .check_box_group.bool_cf label {display: inline;}
590 590
591 591 #attachments_fields input.description {margin-left:4px; width:340px;}
592 592 #attachments_fields span {display:block; white-space:nowrap;}
593 593 #attachments_fields input.filename {border:0; height:1.8em; width:250px; color:#555; background-color:inherit; background:url(../images/attachment.png) no-repeat 1px 50%; padding-left:18px;}
594 594 #attachments_fields .ajax-waiting input.filename {background:url(../images/hourglass.png) no-repeat 0px 50%;}
595 595 #attachments_fields .ajax-loading input.filename {background:url(../images/loading.gif) no-repeat 0px 50%;}
596 596 #attachments_fields div.ui-progressbar { width: 100px; height:14px; margin: 2px 0 -5px 8px; display: inline-block; }
597 597 a.remove-upload {background: url(../images/delete.png) no-repeat 1px 50%; width:1px; display:inline-block; padding-left:16px;}
598 598 a.remove-upload:hover {text-decoration:none !important;}
599 599
600 600 div.fileover { background-color: lavender; }
601 601
602 602 div.attachments { margin-top: 12px; }
603 603 div.attachments p { margin:4px 0 2px 0; }
604 604 div.attachments img { vertical-align: middle; }
605 605 div.attachments span.author { font-size: 0.9em; color: #888; }
606 606
607 607 div.thumbnails {margin-top:0.6em;}
608 608 div.thumbnails div {background:#fff;border:2px solid #ddd;display:inline-block;margin-right:2px;}
609 609 div.thumbnails img {margin: 3px; vertical-align: middle;}
610 610 #history div.thumbnails {margin-left: 2em;}
611 611
612 612 p.other-formats { text-align: right; font-size:0.9em; color: #666; }
613 613 .other-formats span + span:before { content: "| "; }
614 614
615 615 a.atom { background: url(../images/feed.png) no-repeat 1px 50%; padding: 2px 0px 3px 16px; }
616 616
617 617 em.info {font-style:normal;font-size:90%;color:#888;display:block;}
618 618 em.info.error {padding-left:20px; background:url(../images/exclamation.png) no-repeat 0 50%;}
619 619
620 620 textarea.text_cf {width:95%; resize:vertical;}
621 621 input.string_cf, input.link_cf {width:95%;}
622 622 select.bool_cf {width:auto !important;}
623 623
624 624 #tab-content-modules fieldset p {margin:3px 0 4px 0;}
625 625
626 626 #tab-content-users .splitcontentleft {width: 64%;}
627 627 #tab-content-users .splitcontentright {width: 34%;}
628 628 #tab-content-users fieldset {padding:1em; margin-bottom: 1em;}
629 629 #tab-content-users fieldset legend {font-weight: bold;}
630 630 #tab-content-users fieldset label {display: block;}
631 631 #tab-content-users #principals {max-height: 400px; overflow: auto;}
632 632
633 633 #users_for_watcher {height: 200px; overflow:auto;}
634 634 #users_for_watcher label {display: block;}
635 635
636 636 table.members td.name {padding-left: 20px;}
637 637 table.members td.group, table.members td.groupnonmember, table.members td.groupanonymous {background: url(../images/group.png) no-repeat 0% 1px;}
638 638
639 639 input#principal_search, input#user_search {width:90%}
640 640 .roles-selection label {display:inline-block; width:210px;}
641 641
642 642 input.autocomplete {
643 643 background: #fff url(../images/magnifier.png) no-repeat 2px 50%; padding-left:20px !important;
644 644 border:1px solid #9EB1C2; border-radius:2px; height:1.5em;
645 645 }
646 646 input.autocomplete.ajax-loading {
647 647 background-image: url(../images/loading.gif);
648 648 }
649 649
650 650 .role-visibility {padding-left:2em;}
651 651
652 652 .objects-selection {
653 653 height: 300px;
654 654 overflow: auto;
655 655 }
656 656
657 657 .objects-selection label {
658 658 display: block;
659 659 }
660 660
661 661 .objects-selection>div {
662 662 column-count: auto;
663 663 column-width: 200px;
664 664 -webkit-column-count: auto;
665 665 -webkit-column-width: 200px;
666 666 -webkit-column-gap : 0.5rem;
667 667 -webkit-column-rule: 1px solid #ccc;
668 668 -moz-column-count: auto;
669 669 -moz-column-width: 200px;
670 670 -moz-column-gap : 0.5rem;
671 671 -moz-column-rule: 1px solid #ccc;
672 672 }
673 673
674 674 /***** Flash & error messages ****/
675 675 #errorExplanation, div.flash, .nodata, .warning, .conflict {
676 676 padding: 4px 4px 4px 30px;
677 677 margin-bottom: 12px;
678 678 font-size: 1.1em;
679 679 border: 2px solid;
680 680 border-radius: 3px;
681 681 }
682 682
683 683 div.flash {margin-top: 8px;}
684 684
685 685 div.flash.error, #errorExplanation {
686 686 background: url(../images/exclamation.png) 8px 50% no-repeat;
687 687 background-color: #ffe3e3;
688 688 border-color: #dd0000;
689 689 color: #880000;
690 690 }
691 691
692 692 div.flash.notice {
693 693 background: url(../images/true.png) 8px 5px no-repeat;
694 694 background-color: #dfffdf;
695 695 border-color: #9fcf9f;
696 696 color: #005f00;
697 697 }
698 698
699 699 div.flash.warning, .conflict {
700 700 background: url(../images/warning.png) 8px 5px no-repeat;
701 701 background-color: #FFEBC1;
702 702 border-color: #FDBF3B;
703 703 color: #A6750C;
704 704 text-align: left;
705 705 }
706 706
707 707 .nodata, .warning {
708 708 text-align: center;
709 709 background-color: #FFEBC1;
710 710 border-color: #FDBF3B;
711 711 color: #A6750C;
712 712 }
713 713
714 714 #errorExplanation ul { font-size: 0.9em;}
715 715 #errorExplanation h2, #errorExplanation p { display: none; }
716 716
717 717 .conflict-details {font-size:80%;}
718 718
719 719 /***** Ajax indicator ******/
720 720 #ajax-indicator {
721 721 position: absolute; /* fixed not supported by IE */
722 722 background-color:#eee;
723 723 border: 1px solid #bbb;
724 724 top:35%;
725 725 left:40%;
726 726 width:20%;
727 727 font-weight:bold;
728 728 text-align:center;
729 729 padding:0.6em;
730 730 z-index:100;
731 731 opacity: 0.5;
732 732 }
733 733
734 734 html>body #ajax-indicator { position: fixed; }
735 735
736 736 #ajax-indicator span {
737 737 background-position: 0% 40%;
738 738 background-repeat: no-repeat;
739 739 background-image: url(../images/loading.gif);
740 740 padding-left: 26px;
741 741 vertical-align: bottom;
742 742 }
743 743
744 744 /***** Calendar *****/
745 745 table.cal {border-collapse: collapse; width: 100%; margin: 0px 0 6px 0;border: 1px solid #d7d7d7;}
746 746 table.cal thead th {width: 14%; background-color:#EEEEEE; padding: 4px; }
747 747 table.cal thead th.week-number {width: auto;}
748 748 table.cal tbody tr {height: 100px;}
749 749 table.cal td {border: 1px solid #d7d7d7; vertical-align: top; font-size: 0.9em;}
750 750 table.cal td.week-number { background-color:#EEEEEE; padding: 4px; border:none; font-size: 1em;}
751 751 table.cal td p.day-num {font-size: 1.1em; text-align:right;}
752 752 table.cal td.odd p.day-num {color: #bbb;}
753 753 table.cal td.today {background:#ffffdd;}
754 754 table.cal td.today p.day-num {font-weight: bold;}
755 755 table.cal .starting a, p.cal.legend .starting {background: url(../images/bullet_go.png) no-repeat -1px -2px; padding-left:16px;}
756 756 table.cal .ending a, p.cal.legend .ending {background: url(../images/bullet_end.png) no-repeat -1px -2px; padding-left:16px;}
757 757 table.cal .starting.ending a, p.cal.legend .starting.ending {background: url(../images/bullet_diamond.png) no-repeat -1px -2px; padding-left:16px;}
758 758 p.cal.legend span {display:block;}
759 759
760 760 /***** Tooltips ******/
761 761 .tooltip{position:relative;z-index:24;}
762 762 .tooltip:hover{z-index:25;color:#000;}
763 763 .tooltip span.tip{display: none; text-align:left;}
764 764
765 765 div.tooltip:hover span.tip{
766 766 display:block;
767 767 position:absolute;
768 768 top:12px; left:24px; width:270px;
769 769 border:1px solid #555;
770 770 background-color:#fff;
771 771 padding: 4px;
772 772 font-size: 0.8em;
773 773 color:#505050;
774 774 }
775 775
776 776 img.ui-datepicker-trigger {
777 777 cursor: pointer;
778 778 vertical-align: middle;
779 779 margin-left: 4px;
780 780 }
781 781
782 782 /***** Progress bar *****/
783 783 table.progress {
784 784 border-collapse: collapse;
785 785 border-spacing: 0pt;
786 786 empty-cells: show;
787 787 text-align: center;
788 788 float:left;
789 789 margin: 1px 6px 1px 0px;
790 790 }
791 791
792 table.progress {width:80px;}
792 793 table.progress td { height: 1em; }
793 794 table.progress td.closed { background: #BAE0BA none repeat scroll 0%; }
794 795 table.progress td.done { background: #D3EDD3 none repeat scroll 0%; }
795 796 table.progress td.todo { background: #eee none repeat scroll 0%; }
796 797 p.percent {font-size: 80%; margin:0;}
797 798 p.progress-info {clear: left; font-size: 80%; margin-top:-4px; color:#777;}
798 799
799 #roadmap table.progress td { height: 1.2em; }
800 .version-overview table.progress {width:40em;}
801 .version-overview table.progress td { height: 1.2em; }
802
800 803 /***** Tabs *****/
801 804 #content .tabs {height: 2.6em; margin-bottom:1.2em; position:relative; overflow:hidden;}
802 805 #content .tabs ul {margin:0; position:absolute; bottom:0; padding-left:0.5em; width: 2000px; border-bottom: 1px solid #bbbbbb;}
803 806 #content .tabs ul li {
804 807 float:left;
805 808 list-style-type:none;
806 809 white-space:nowrap;
807 810 margin-right:4px;
808 811 background:#fff;
809 812 position:relative;
810 813 margin-bottom:-1px;
811 814 }
812 815 #content .tabs ul li a{
813 816 display:block;
814 817 font-size: 0.9em;
815 818 text-decoration:none;
816 819 line-height:1.3em;
817 820 padding:4px 6px 4px 6px;
818 821 border: 1px solid #ccc;
819 822 border-bottom: 1px solid #bbbbbb;
820 823 background-color: #f6f6f6;
821 824 color:#999;
822 825 font-weight:bold;
823 826 border-top-left-radius:3px;
824 827 border-top-right-radius:3px;
825 828 }
826 829
827 830 #content .tabs ul li a:hover {
828 831 background-color: #ffffdd;
829 832 text-decoration:none;
830 833 }
831 834
832 835 #content .tabs ul li a.selected {
833 836 background-color: #fff;
834 837 border: 1px solid #bbbbbb;
835 838 border-bottom: 1px solid #fff;
836 839 color:#444;
837 840 }
838 841
839 842 #content .tabs ul li a.selected:hover {background-color: #fff;}
840 843
841 844 div.tabs-buttons { position:absolute; right: 0; width: 48px; height: 24px; background: white; bottom: 0; border-bottom: 1px solid #bbbbbb; }
842 845
843 846 button.tab-left, button.tab-right {
844 847 font-size: 0.9em;
845 848 cursor: pointer;
846 849 height:24px;
847 850 border: 1px solid #ccc;
848 851 border-bottom: 1px solid #bbbbbb;
849 852 position:absolute;
850 853 padding:4px;
851 854 width: 20px;
852 855 bottom: -1px;
853 856 }
854 857
855 858 button.tab-left {
856 859 right: 20px;
857 860 background: #eeeeee url(../images/bullet_arrow_left.png) no-repeat 50% 50%;
858 861 border-top-left-radius:3px;
859 862 }
860 863
861 864 button.tab-right {
862 865 right: 0;
863 866 background: #eeeeee url(../images/bullet_arrow_right.png) no-repeat 50% 50%;
864 867 border-top-right-radius:3px;
865 868 }
866 869
867 870 /***** Diff *****/
868 871 .diff_out { background: #fcc; }
869 872 .diff_out span { background: #faa; }
870 873 .diff_in { background: #cfc; }
871 874 .diff_in span { background: #afa; }
872 875
873 876 .text-diff {
874 877 padding: 1em;
875 878 background-color:#f6f6f6;
876 879 color:#505050;
877 880 border: 1px solid #e4e4e4;
878 881 }
879 882
880 883 /***** Wiki *****/
881 884 div.wiki table {
882 885 border-collapse: collapse;
883 886 margin-bottom: 1em;
884 887 }
885 888
886 889 div.wiki table, div.wiki td, div.wiki th {
887 890 border: 1px solid #bbb;
888 891 padding: 4px;
889 892 }
890 893
891 894 div.wiki .noborder, div.wiki .noborder td, div.wiki .noborder th {border:0;}
892 895
893 896 div.wiki .external {
894 897 background-position: 0% 60%;
895 898 background-repeat: no-repeat;
896 899 padding-left: 12px;
897 900 background-image: url(../images/external.png);
898 901 }
899 902
900 903 div.wiki a {word-wrap: break-word;}
901 904 div.wiki a.new {color: #b73535;}
902 905
903 906 div.wiki ul, div.wiki ol {margin-bottom:1em;}
904 907 div.wiki li>ul, div.wiki li>ol {margin-bottom: 0;}
905 908
906 909 div.wiki pre {
907 910 margin: 1em 1em 1em 1.6em;
908 911 padding: 8px;
909 912 background-color: #fafafa;
910 913 border: 1px solid #e2e2e2;
911 914 border-radius: 3px;
912 915 width:auto;
913 916 overflow-x: auto;
914 917 overflow-y: hidden;
915 918 }
916 919
917 920 div.wiki ul.toc {
918 921 background-color: #ffffdd;
919 922 border: 1px solid #e4e4e4;
920 923 padding: 4px;
921 924 line-height: 1.2em;
922 925 margin-bottom: 12px;
923 926 margin-right: 12px;
924 927 margin-left: 0;
925 928 display: table
926 929 }
927 930 * html div.wiki ul.toc { width: 50%; } /* IE6 doesn't autosize div */
928 931
929 932 div.wiki ul.toc.right { float: right; margin-left: 12px; margin-right: 0; width: auto; }
930 933 div.wiki ul.toc.left { float: left; margin-right: 12px; margin-left: 0; width: auto; }
931 934 div.wiki ul.toc ul { margin: 0; padding: 0; }
932 935 div.wiki ul.toc li {list-style-type:none; margin: 0; font-size:12px;}
933 936 div.wiki ul.toc li li {margin-left: 1.5em; font-size:10px;}
934 937 div.wiki ul.toc a {
935 938 font-size: 0.9em;
936 939 font-weight: normal;
937 940 text-decoration: none;
938 941 color: #606060;
939 942 }
940 943 div.wiki ul.toc a:hover { color: #c61a1a; text-decoration: underline;}
941 944
942 945 a.wiki-anchor { display: none; margin-left: 6px; text-decoration: none; }
943 946 a.wiki-anchor:hover { color: #aaa !important; text-decoration: none; }
944 947 h1:hover a.wiki-anchor, h2:hover a.wiki-anchor, h3:hover a.wiki-anchor { display: inline; color: #ddd; }
945 948
946 949 div.wiki img {vertical-align:middle; max-width:100%;}
947 950
948 951 /***** My page layout *****/
949 952 .block-receiver {
950 953 border:1px dashed #c0c0c0;
951 954 margin-bottom: 20px;
952 955 padding: 15px 0 15px 0;
953 956 }
954 957
955 958 .mypage-box {
956 959 margin:0 0 20px 0;
957 960 color:#505050;
958 961 line-height:1.5em;
959 962 }
960 963
961 964 .handle {cursor: move;}
962 965
963 966 a.close-icon {
964 967 display:block;
965 968 margin-top:3px;
966 969 overflow:hidden;
967 970 width:12px;
968 971 height:12px;
969 972 background-repeat: no-repeat;
970 973 cursor:pointer;
971 974 background-image:url('../images/close.png');
972 975 }
973 976 a.close-icon:hover {background-image:url('../images/close_hl.png');}
974 977
975 978 /***** Gantt chart *****/
976 979 .gantt_hdr {
977 980 position:absolute;
978 981 top:0;
979 982 height:16px;
980 983 border-top: 1px solid #c0c0c0;
981 984 border-bottom: 1px solid #c0c0c0;
982 985 border-right: 1px solid #c0c0c0;
983 986 text-align: center;
984 987 overflow: hidden;
985 988 }
986 989
987 990 .gantt_hdr.nwday {background-color:#f1f1f1; color:#999;}
988 991
989 992 .gantt_subjects { font-size: 0.8em; }
990 993 .gantt_subjects div { line-height:16px;height:16px;overflow:hidden;white-space:nowrap;text-overflow: ellipsis; }
991 994
992 995 .task {
993 996 position: absolute;
994 997 height:8px;
995 998 font-size:0.8em;
996 999 color:#888;
997 1000 padding:0;
998 1001 margin:0;
999 1002 line-height:16px;
1000 1003 white-space:nowrap;
1001 1004 }
1002 1005
1003 1006 .task.label {width:100%;}
1004 1007 .task.label.project, .task.label.version { font-weight: bold; }
1005 1008
1006 1009 .task_late { background:#f66 url(../images/task_late.png); border: 1px solid #f66; }
1007 1010 .task_done { background:#00c600 url(../images/task_done.png); border: 1px solid #00c600; }
1008 1011 .task_todo { background:#aaa url(../images/task_todo.png); border: 1px solid #aaa; }
1009 1012
1010 1013 .task_todo.parent { background: #888; border: 1px solid #888; height: 3px;}
1011 1014 .task_late.parent, .task_done.parent { height: 3px;}
1012 1015 .task.parent.marker.starting { position: absolute; background: url(../images/task_parent_end.png) no-repeat 0 0; width: 8px; height: 16px; margin-left: -4px; left: 0px; top: -1px;}
1013 1016 .task.parent.marker.ending { position: absolute; background: url(../images/task_parent_end.png) no-repeat 0 0; width: 8px; height: 16px; margin-left: -4px; right: 0px; top: -1px;}
1014 1017
1015 1018 .version.task_late { background:#f66 url(../images/milestone_late.png); border: 1px solid #f66; height: 2px; margin-top: 3px;}
1016 1019 .version.task_done { background:#00c600 url(../images/milestone_done.png); border: 1px solid #00c600; height: 2px; margin-top: 3px;}
1017 1020 .version.task_todo { background:#fff url(../images/milestone_todo.png); border: 1px solid #fff; height: 2px; margin-top: 3px;}
1018 1021 .version.marker { background-image:url(../images/version_marker.png); background-repeat: no-repeat; border: 0; margin-left: -4px; margin-top: 1px; }
1019 1022
1020 1023 .project.task_late { background:#f66 url(../images/milestone_late.png); border: 1px solid #f66; height: 2px; margin-top: 3px;}
1021 1024 .project.task_done { background:#00c600 url(../images/milestone_done.png); border: 1px solid #00c600; height: 2px; margin-top: 3px;}
1022 1025 .project.task_todo { background:#fff url(../images/milestone_todo.png); border: 1px solid #fff; height: 2px; margin-top: 3px;}
1023 1026 .project.marker { background-image:url(../images/project_marker.png); background-repeat: no-repeat; border: 0; margin-left: -4px; margin-top: 1px; }
1024 1027
1025 1028 .version-behind-schedule a, .issue-behind-schedule a {color: #f66914;}
1026 1029 .version-overdue a, .issue-overdue a, .project-overdue a {color: #f00;}
1027 1030
1028 1031 /***** Icons *****/
1029 1032 .icon {
1030 1033 background-position: 0% 50%;
1031 1034 background-repeat: no-repeat;
1032 1035 padding-left: 20px;
1033 1036 padding-top: 2px;
1034 1037 padding-bottom: 3px;
1035 1038 }
1036 1039
1037 1040 .icon-add { background-image: url(../images/add.png); }
1038 1041 .icon-edit { background-image: url(../images/edit.png); }
1039 1042 .icon-copy { background-image: url(../images/copy.png); }
1040 1043 .icon-duplicate { background-image: url(../images/duplicate.png); }
1041 1044 .icon-del { background-image: url(../images/delete.png); }
1042 1045 .icon-move { background-image: url(../images/move.png); }
1043 1046 .icon-save { background-image: url(../images/save.png); }
1044 1047 .icon-cancel { background-image: url(../images/cancel.png); }
1045 1048 .icon-multiple { background-image: url(../images/table_multiple.png); }
1046 1049 .icon-folder { background-image: url(../images/folder.png); }
1047 1050 .open .icon-folder { background-image: url(../images/folder_open.png); }
1048 1051 .icon-package { background-image: url(../images/package.png); }
1049 1052 .icon-user { background-image: url(../images/user.png); }
1050 1053 .icon-projects { background-image: url(../images/projects.png); }
1051 1054 .icon-help { background-image: url(../images/help.png); }
1052 1055 .icon-attachment { background-image: url(../images/attachment.png); }
1053 1056 .icon-history { background-image: url(../images/history.png); }
1054 1057 .icon-time { background-image: url(../images/time.png); }
1055 1058 .icon-time-add { background-image: url(../images/time_add.png); }
1056 1059 .icon-stats { background-image: url(../images/stats.png); }
1057 1060 .icon-warning { background-image: url(../images/warning.png); }
1058 1061 .icon-fav { background-image: url(../images/fav.png); }
1059 1062 .icon-fav-off { background-image: url(../images/fav_off.png); }
1060 1063 .icon-reload { background-image: url(../images/reload.png); }
1061 1064 .icon-lock { background-image: url(../images/locked.png); }
1062 1065 .icon-unlock { background-image: url(../images/unlock.png); }
1063 1066 .icon-checked { background-image: url(../images/true.png); }
1064 1067 .icon-details { background-image: url(../images/zoom_in.png); }
1065 1068 .icon-report { background-image: url(../images/report.png); }
1066 1069 .icon-comment { background-image: url(../images/comment.png); }
1067 1070 .icon-summary { background-image: url(../images/lightning.png); }
1068 1071 .icon-server-authentication { background-image: url(../images/server_key.png); }
1069 1072 .icon-issue { background-image: url(../images/ticket.png); }
1070 1073 .icon-zoom-in { background-image: url(../images/zoom_in.png); }
1071 1074 .icon-zoom-out { background-image: url(../images/zoom_out.png); }
1072 1075 .icon-passwd { background-image: url(../images/textfield_key.png); }
1073 1076 .icon-test { background-image: url(../images/bullet_go.png); }
1074 1077 .icon-email-add { background-image: url(../images/email_add.png); }
1075 1078
1076 1079 .icon-file { background-image: url(../images/files/default.png); }
1077 1080 .icon-file.text-plain { background-image: url(../images/files/text.png); }
1078 1081 .icon-file.text-x-c { background-image: url(../images/files/c.png); }
1079 1082 .icon-file.text-x-csharp { background-image: url(../images/files/csharp.png); }
1080 1083 .icon-file.text-x-java { background-image: url(../images/files/java.png); }
1081 1084 .icon-file.text-x-javascript { background-image: url(../images/files/js.png); }
1082 1085 .icon-file.text-x-php { background-image: url(../images/files/php.png); }
1083 1086 .icon-file.text-x-ruby { background-image: url(../images/files/ruby.png); }
1084 1087 .icon-file.text-xml { background-image: url(../images/files/xml.png); }
1085 1088 .icon-file.text-css { background-image: url(../images/files/css.png); }
1086 1089 .icon-file.text-html { background-image: url(../images/files/html.png); }
1087 1090 .icon-file.image-gif { background-image: url(../images/files/image.png); }
1088 1091 .icon-file.image-jpeg { background-image: url(../images/files/image.png); }
1089 1092 .icon-file.image-png { background-image: url(../images/files/image.png); }
1090 1093 .icon-file.image-tiff { background-image: url(../images/files/image.png); }
1091 1094 .icon-file.application-pdf { background-image: url(../images/files/pdf.png); }
1092 1095 .icon-file.application-zip { background-image: url(../images/files/zip.png); }
1093 1096 .icon-file.application-x-gzip { background-image: url(../images/files/zip.png); }
1094 1097
1095 1098 img.gravatar {
1096 1099 padding: 2px;
1097 1100 border: solid 1px #d5d5d5;
1098 1101 background: #fff;
1099 1102 vertical-align: middle;
1100 1103 }
1101 1104
1102 1105 div.issue img.gravatar {
1103 1106 float: left;
1104 1107 margin: 0 6px 0 0;
1105 1108 padding: 5px;
1106 1109 }
1107 1110
1108 1111 div.issue table img.gravatar {
1109 1112 height: 14px;
1110 1113 width: 14px;
1111 1114 padding: 2px;
1112 1115 float: left;
1113 1116 margin: 0 0.5em 0 0;
1114 1117 }
1115 1118
1116 1119 h2 img.gravatar {margin: -2px 4px -4px 0;}
1117 1120 h3 img.gravatar {margin: -4px 4px -4px 0;}
1118 1121 h4 img.gravatar {margin: -6px 4px -4px 0;}
1119 1122 td.username img.gravatar {margin: 0 0.5em 0 0; vertical-align: top;}
1120 1123 #activity dt img.gravatar {float: left; margin: 0 1em 1em 0;}
1121 1124 /* Used on 12px Gravatar img tags without the icon background */
1122 1125 .icon-gravatar {float: left; margin-right: 4px;}
1123 1126
1124 1127 #activity dt, .journal {clear: left;}
1125 1128
1126 1129 .journal-link {float: right;}
1127 1130
1128 1131 h2 img { vertical-align:middle; }
1129 1132
1130 1133 .hascontextmenu { cursor: context-menu; }
1131 1134
1132 1135 .sample-data {border:1px solid #ccc; border-collapse:collapse; background-color:#fff; margin:0.5em;}
1133 1136 .sample-data td {border:1px solid #ccc; padding: 2px 4px; font-family: Consolas, Menlo, "Liberation Mono", Courier, monospace;}
1134 1137 .sample-data tr:first-child td {font-weight:bold; text-align:center;}
1135 1138
1136 1139 .ui-progressbar {position: relative;}
1137 1140 #progress-label {
1138 1141 position: absolute; left: 50%; top: 4px;
1139 1142 font-weight: bold;
1140 1143 color: #555; text-shadow: 1px 1px 0 #fff;
1141 1144 }
1142 1145
1143 1146 /* Custom JQuery styles */
1144 1147 .ui-datepicker-title select {width:70px !important; margin-top:-2px !important; margin-right:4px !important;}
1145 1148
1146 1149
1147 1150 /************* CodeRay styles *************/
1148 1151 .syntaxhl div {display: inline;}
1149 1152 .syntaxhl .code pre { overflow: auto }
1150 1153
1151 1154 .syntaxhl .annotation { color:#007 }
1152 1155 .syntaxhl .attribute-name { color:#b48 }
1153 1156 .syntaxhl .attribute-value { color:#700 }
1154 1157 .syntaxhl .binary { color:#549 }
1155 1158 .syntaxhl .binary .char { color:#325 }
1156 1159 .syntaxhl .binary .delimiter { color:#325 }
1157 1160 .syntaxhl .char { color:#D20 }
1158 1161 .syntaxhl .char .content { color:#D20 }
1159 1162 .syntaxhl .char .delimiter { color:#710 }
1160 1163 .syntaxhl .class { color:#258; font-weight:bold }
1161 1164 .syntaxhl .class-variable { color:#369 }
1162 1165 .syntaxhl .color { color:#0A0 }
1163 1166 .syntaxhl .comment { color:#385 }
1164 1167 .syntaxhl .comment .char { color:#385 }
1165 1168 .syntaxhl .comment .delimiter { color:#385 }
1166 1169 .syntaxhl .constant { color:#258; font-weight:bold }
1167 1170 .syntaxhl .decorator { color:#B0B }
1168 1171 .syntaxhl .definition { color:#099; font-weight:bold }
1169 1172 .syntaxhl .delimiter { color:black }
1170 1173 .syntaxhl .directive { color:#088; font-weight:bold }
1171 1174 .syntaxhl .docstring { color:#D42; }
1172 1175 .syntaxhl .doctype { color:#34b }
1173 1176 .syntaxhl .done { text-decoration: line-through; color: gray }
1174 1177 .syntaxhl .entity { color:#800; font-weight:bold }
1175 1178 .syntaxhl .error { color:#F00; background-color:#FAA }
1176 1179 .syntaxhl .escape { color:#666 }
1177 1180 .syntaxhl .exception { color:#C00; font-weight:bold }
1178 1181 .syntaxhl .float { color:#06D }
1179 1182 .syntaxhl .function { color:#06B; font-weight:bold }
1180 1183 .syntaxhl .function .delimiter { color:#024; font-weight:bold }
1181 1184 .syntaxhl .global-variable { color:#d70 }
1182 1185 .syntaxhl .hex { color:#02b }
1183 1186 .syntaxhl .id { color:#33D; font-weight:bold }
1184 1187 .syntaxhl .include { color:#B44; font-weight:bold }
1185 1188 .syntaxhl .inline { background-color: hsla(0,0%,0%,0.07); color: black }
1186 1189 .syntaxhl .inline-delimiter { font-weight: bold; color: #666 }
1187 1190 .syntaxhl .instance-variable { color:#33B }
1188 1191 .syntaxhl .integer { color:#06D }
1189 1192 .syntaxhl .imaginary { color:#f00 }
1190 1193 .syntaxhl .important { color:#D00 }
1191 1194 .syntaxhl .key { color: #606 }
1192 1195 .syntaxhl .key .char { color: #60f }
1193 1196 .syntaxhl .key .delimiter { color: #404 }
1194 1197 .syntaxhl .keyword { color:#939; font-weight:bold }
1195 1198 .syntaxhl .label { color:#970; font-weight:bold }
1196 1199 .syntaxhl .local-variable { color:#950 }
1197 1200 .syntaxhl .map .content { color:#808 }
1198 1201 .syntaxhl .map .delimiter { color:#40A}
1199 1202 .syntaxhl .map { background-color:hsla(200,100%,50%,0.06); }
1200 1203 .syntaxhl .namespace { color:#707; font-weight:bold }
1201 1204 .syntaxhl .octal { color:#40E }
1202 1205 .syntaxhl .operator { }
1203 1206 .syntaxhl .predefined { color:#369; font-weight:bold }
1204 1207 .syntaxhl .predefined-constant { color:#069 }
1205 1208 .syntaxhl .predefined-type { color:#0a8; font-weight:bold }
1206 1209 .syntaxhl .preprocessor { color:#579 }
1207 1210 .syntaxhl .pseudo-class { color:#00C; font-weight:bold }
1208 1211 .syntaxhl .regexp { background-color:hsla(300,100%,50%,0.06); }
1209 1212 .syntaxhl .regexp .content { color:#808 }
1210 1213 .syntaxhl .regexp .delimiter { color:#404 }
1211 1214 .syntaxhl .regexp .modifier { color:#C2C }
1212 1215 .syntaxhl .reserved { color:#080; font-weight:bold }
1213 1216 .syntaxhl .shell { background-color:hsla(120,100%,50%,0.06); }
1214 1217 .syntaxhl .shell .content { color:#2B2 }
1215 1218 .syntaxhl .shell .delimiter { color:#161 }
1216 1219 .syntaxhl .string .char { color: #46a }
1217 1220 .syntaxhl .string .content { color: #46a }
1218 1221 .syntaxhl .string .delimiter { color: #46a }
1219 1222 .syntaxhl .string .modifier { color: #46a }
1220 1223 .syntaxhl .symbol { color:#d33 }
1221 1224 .syntaxhl .symbol .content { color:#d33 }
1222 1225 .syntaxhl .symbol .delimiter { color:#d33 }
1223 1226 .syntaxhl .tag { color:#070; font-weight:bold }
1224 1227 .syntaxhl .type { color:#339; font-weight:bold }
1225 1228 .syntaxhl .value { color: #088 }
1226 1229 .syntaxhl .variable { color:#037 }
1227 1230
1228 1231 .syntaxhl .insert { background: hsla(120,100%,50%,0.12) }
1229 1232 .syntaxhl .delete { background: hsla(0,100%,50%,0.12) }
1230 1233 .syntaxhl .change { color: #bbf; background: #007 }
1231 1234 .syntaxhl .head { color: #f8f; background: #505 }
1232 1235 .syntaxhl .head .filename { color: white; }
1233 1236
1234 1237 .syntaxhl .delete .eyecatcher { background-color: hsla(0,100%,50%,0.2); border: 1px solid hsla(0,100%,45%,0.5); margin: -1px; border-bottom: none; border-top-left-radius: 5px; border-top-right-radius: 5px; }
1235 1238 .syntaxhl .insert .eyecatcher { background-color: hsla(120,100%,50%,0.2); border: 1px solid hsla(120,100%,25%,0.5); margin: -1px; border-top: none; border-bottom-left-radius: 5px; border-bottom-right-radius: 5px; }
1236 1239
1237 1240 .syntaxhl .insert .insert { color: #0c0; background:transparent; font-weight:bold }
1238 1241 .syntaxhl .delete .delete { color: #c00; background:transparent; font-weight:bold }
1239 1242 .syntaxhl .change .change { color: #88f }
1240 1243 .syntaxhl .head .head { color: #f4f }
1241 1244
1242 1245 /***** Media print specific styles *****/
1243 1246 @media print {
1244 1247 #top-menu, #header, #main-menu, #sidebar, #footer, .contextual, .other-formats { display:none; }
1245 1248 #main { background: #fff; }
1246 1249 #content { width: 99%; margin: 0; padding: 0; border: 0; background: #fff; overflow: visible !important;}
1247 1250 #wiki_add_attachment { display:none; }
1248 1251 .hide-when-print { display: none; }
1249 1252 .autoscroll {overflow-x: visible;}
1250 1253 table.list {margin-top:0.5em;}
1251 1254 table.list th, table.list td {border: 1px solid #aaa;}
1252 1255 }
1253 1256
1254 1257 /* Accessibility specific styles */
1255 1258 .hidden-for-sighted {
1256 1259 position:absolute;
1257 1260 left:-10000px;
1258 1261 top:auto;
1259 1262 width:1px;
1260 1263 height:1px;
1261 1264 overflow:hidden;
1262 1265 }
@@ -1,783 +1,785
1 1 /*----------------------------------------*\
2 2 RESPONSIVE CSS
3 3 \*----------------------------------------*/
4 4
5 5
6 6 /*
7 7
8 8 CONTENTS
9 9
10 10 A) BASIC MOBILE RESETS
11 11 B) HEADER & TOP MENUS
12 12 C) MAIN CONTENT & SIDEBAR
13 13 D) TOGGLE BUTTON & FLYOUT MENU
14 14 E) UX ELEMENTS
15 15 F) PAGE SPECIFIC STYLES
16 16 G) FORMS
17 17
18 18 */
19 19
20 20
21 21 /* Hide new elements (toggle button and flyout menu) above 900px */
22 22 .mobile-toggle-button,
23 23 .flyout-menu
24 24 {
25 25 display: none;
26 26 }
27 27
28 28 /*
29 29 redmine's body is set to min-width: 900px
30 30 add first breakpoint here and start adding responsiveness
31 31 */
32 32
33 33 @media all and (max-width: 899px)
34 34 {
35 35 /*----------------------------------------*\
36 36 A) BASIC MOBILE RESETS
37 37 \*----------------------------------------*/
38 38
39 39 /*
40 40 apply natural border box, see: http://www.paulirish.com/2012/box-sizing-border-box-ftw/
41 41 this helps us to better deal with percentages and padding / margin
42 42 */
43 43 *,
44 44 *:before,
45 45 *:after
46 46 {
47 47 -webkit-box-sizing: border-box;
48 48 -moz-box-sizing: border-box;
49 49 box-sizing: border-box;
50 50 }
51 51
52 52 body,
53 53 html
54 54 {
55 55 height: 100%;
56 56 margin: 0;
57 57 padding: 0;
58 58 }
59 59
60 60 html
61 61 {
62 62 overflow-y: auto; /* avoid 2nd scrollbar on desktop */
63 63 }
64 64
65 65 body
66 66 {
67 67 min-width: 0; /* reset the min-width of 900px */
68 68
69 69 -webkit-overflow-scrolling: touch;
70 70 }
71 71
72 72
73 73 body,
74 74 input,
75 75 select,
76 76 textarea,
77 77 button
78 78 {
79 79 font-size: 14px; /* Set font-size for standard elements to 14px */
80 80 }
81 81
82 82
83 83 select
84 84 {
85 85 max-width: 100%; /* prevent long names within select menues from breaking content */
86 86 }
87 87
88 88
89 89 #wrapper
90 90 {
91 91 position: relative;
92 92
93 93 overflow-x: hidden; /* hide horizontal overflow */
94 94
95 95 max-width: 100%;
96 96 }
97 97
98 98 #wrapper,
99 99 #wrapper2
100 100 {
101 101 margin: 0;
102 102 }
103 103
104 104 /*----------------------------------------*\
105 105 B) HEADER & TOP MENUS
106 106 \*----------------------------------------*/
107 107
108 108 #header
109 109 {
110 110 width: 100%;
111 111 height: 64px; /* the height of our header on mobile */
112 112 min-height: 0;
113 113 margin: 0;
114 114 padding: 0;
115 115
116 116 border: none;
117 117 background-color: #628db6;
118 118 }
119 119
120 120 /* Hide project name on mobile (project name is still visible in select menu) */
121 121 #header h1
122 122 {
123 123 display: none !important;
124 124 }
125 125
126 126 /* reset #header a color for mobile toggle button */
127 127 #header a.mobile-toggle-button
128 128 {
129 129 color: #f8f8f8;
130 130 }
131 131
132 132
133 133 /* Hide top-menu and main-menu on mobile, because it's placed in our flyout menu */
134 134 #top-menu,
135 135 #header #main-menu
136 136 {
137 137 display: none;
138 138 }
139 139
140 140 /* the quick search within header holding search form and #project_quick_jump_box box*/
141 141 #header #quick-search
142 142 {
143 143 float: none;
144 144 clear: none; /* there are themes which set clear property, this resets it */
145 145
146 146 max-width: 100%; /* reset max-width */
147 147 margin: 0;
148 148
149 149 background: inherit;
150 150 }
151 151
152 152 /* this represents the dropdown arrow to left of the mobile project menu */
153 153 #header .jump-box-arrow:before
154 154 {
155 155 /* set a font-size in order to achive same result in different themes */
156 156 font-family: Verdana, sans-serif;
157 157 font-size: 2em;
158 158 line-height: 64px;
159 159
160 160 position: absolute;
161 161 left: 0;
162 162
163 163 width: 2em;
164 164 padding: 0 .5em;
165 165 /* achieve dropdwon arrow by scaling a caret character */
166 166
167 167 content: '^';
168 168 -webkit-transform: scale(1,-.8);
169 169 -ms-transform: scale(1,-.8);
170 170 transform: scale(1,-.8);
171 171 text-align: right;
172 172 pointer-events: none;
173 173
174 174 opacity: .6;
175 175 }
176 176
177 177 /* styles for combobox within quick-search (#project_quick_jump_box) */
178 178 #header #quick-search select
179 179 {
180 180 font-size: 1.5em;
181 181 font-weight: bold;
182 182 line-height: 1.2;
183 183
184 184 position: absolute;
185 185 top: 15px;
186 186 left: 0;
187 187
188 188 float: left;
189 189
190 190 width: 100%;
191 191 max-width: 100%;
192 192 height: 2em;
193 193 height: 35px;
194 194 padding: 5px;
195 195 padding-right: 72px;
196 196 padding-left: 50px;
197 197
198 198 text-indent: .01px;
199 199
200 200 color: inherit;
201 201 border: 0;
202 202 -webkit-border-radius: 0;
203 203 border-radius: 0;
204 204 background: none;
205 205 -webkit-box-shadow: none;
206 206 box-shadow: none;
207 207 /* hide default browser arrow */
208 208
209 209 -webkit-appearance: none;
210 210 -moz-appearance: none;
211 211 }
212 212
213 213 #header #quick-search form
214 214 {
215 215 display: none;
216 216 }
217 217
218 218 /*----------------------------------------*\
219 219 C) MAIN CONTENT & SIDEBAR
220 220 \*----------------------------------------*/
221 221
222 222 #main
223 223 {
224 224 padding: 0;
225 225 }
226 226
227 227 #main.nosidebar #content,
228 228 div#content
229 229 {
230 230 width: 100%;
231 231 min-height: 0; /* reset min-height of #content */
232 232 margin: 0;
233 233 }
234 234
235 235
236 236 /* hide sidebar and sidebar switch panel, since it's placed in mobile flyout menu */
237 237 #sidebar,
238 238 #sidebar-switch-panel
239 239 {
240 240 display: none;
241 241 }
242 242
243 243 .splitcontentleft
244 244 {
245 245 width: 100%; /* use full width */
246 246 }
247 247
248 248 .splitcontentright
249 249 {
250 250 width: 100%; /* use full width */
251 251 }
252 252
253 253 /*----------------------------------------*\
254 254 D) TOGGLE BUTTON & FLYOUT MENU
255 255 \*----------------------------------------*/
256 256
257 257 /* Mobile toggle button */
258 258
259 259 .mobile-toggle-button
260 260 {
261 261 font-size: 42px;
262 262 line-height: 64px;
263 263
264 264 position: relative;
265 265 z-index: 10;
266 266
267 267 display: block; /* remove display: none; of non-mobile version */
268 268 float: right;
269 269
270 270 width: 60px;
271 271 height: 64px;
272 272 margin-top: 0;
273 273
274 274 text-align: center;
275 275
276 276 border-left: 1px solid #ddd;
277 277 }
278 278
279 279 .mobile-toggle-button:hover,
280 280 .mobile-toggle-button:active
281 281 {
282 282 text-decoration: none;
283 283 }
284 284
285 285 .mobile-toggle-button:after
286 286 {
287 287 font-family: Verdana, sans-serif;
288 288
289 289 display: block;
290 290
291 291 margin-top: -3px;
292 292
293 293 content: '\2261';
294 294 }
295 295
296 296 /* search magnifier icon */
297 297 .search-magnifier
298 298 {
299 299 font-family: Verdana;
300 300
301 301 cursor: pointer;
302 302 -webkit-transform: rotate(-45deg);
303 303 -moz-transform: rotate(45deg);
304 304 -o-transform: rotate(45deg);
305 305
306 306 color: #bbb;
307 307 }
308 308
309 309 .search-magnifier--flyout
310 310 {
311 311 font-size: 25px;
312 312 line-height: 54px;
313 313
314 314 position: absolute;
315 315 z-index: 1;
316 316 left: 12px;
317 317 }
318 318
319 319
320 320 /* Flyout Menu */
321 321
322 322 .flyout-menu
323 323 {
324 324 position: absolute;
325 325 right: -250px;
326 326
327 327 display: block; /* remove display: none; of non-mobile version */
328 328 overflow-x: hidden;
329 329
330 330 width: 250px;
331 331 height: 100%;
332 332 margin: 0; /* reset margin for themes that define it */
333 333 padding: 0; /* reset padding for themes that define it */
334 334
335 335 color: white;
336 336 background-color: #3e5b76;
337 337 }
338 338
339 339
340 340 /* avoid zoom on search input focus for ios devices */
341 341 .flyout-menu input[type='text']
342 342 {
343 343 font-size: 16px;
344 344 }
345 345
346 346 .flyout-menu h3
347 347 {
348 348 font-size: 11px;
349 349 line-height: 19px;
350 350
351 351 height: 20px;
352 352 margin: 0;
353 353 padding: 0;
354 354
355 355 letter-spacing: .1em;
356 356 text-transform: uppercase;
357 357
358 358 color: white;
359 359 border-top: 1px solid #506a83;
360 360 border-bottom: 1px solid #506a83;
361 361 background-color: #628db6;
362 362 }
363 363
364 364 .flyout-menu h4
365 365 {
366 366 color: white;
367 367 }
368 368
369 369 .flyout-menu h3,
370 370 .flyout-menu h4,
371 371 .flyout-menu > p,
372 372 .flyout-menu > a,
373 373 .flyout-menu ul li a,
374 374 .flyout-menu__search,
375 375 .flyout-menu__sidebar > div,
376 376 .flyout-menu__sidebar > p,
377 377 .flyout-menu__sidebar > a,
378 378 .flyout-menu__sidebar > form,
379 379 .flyout-menu > div,
380 380 .flyout-menu > form
381 381 {
382 382 padding-left: 8px;
383 383 }
384 384
385 385 .flyout-menu .flyout-menu__avatar
386 386 {
387 387 margin-top: -1px; /* move avatar up 1px */
388 388 padding-left: 0;
389 389 }
390 390
391 391 .flyout-menu__sidebar > form
392 392 {
393 393 display: block;
394 394 }
395 395
396 396 .flyout-menu__sidebar > form h3
397 397 {
398 398 margin-left: -8px;
399 399 }
400 400
401 401 .flyout-menu__sidebar > form label
402 402 {
403 403 display: inline-block;
404 404
405 405 margin: 8px 0;
406 406 }
407 407
408 408 .flyout-menu__sidebar > form br br
409 409 {
410 410 display: none;
411 411 }
412 412
413 413 .flyout-menu ul
414 414 {
415 415 margin: 0;
416 416 padding: 0;
417 417
418 418 list-style: none;
419 419 }
420 420
421 421 .flyout-menu #watchers
422 422 {
423 423 display: -webkit-flex;
424 424 display: -ms-flexbox;
425 425 display: -webkit-box;
426 426 display: flex;
427 427 flex-direction: column;
428 428
429 429 -webkit-flex-direction: column;
430 430 -ms-flex-direction: column;
431 431 -webkit-box-orient: vertical;
432 432 -webkit-box-direction: normal;
433 433 }
434 434
435 435 .flyout-menu #watchers .contextual
436 436 {
437 437 -webkit-box-ordinal-group: 4;
438 438 -webkit-order: 3;
439 439 -ms-flex-order: 3;
440 440 order: 3;
441 441 }
442 442
443 443 .flyout-menu #watchers h3
444 444 {
445 445 margin-left: -8px;
446 446 }
447 447
448 448 .flyout-menu #watchers ul li
449 449 {
450 450 display: -webkit-flex;
451 451 display: -ms-flexbox;
452 452 display: -webkit-box;
453 453 display: flex;
454 454 flex-direction: row;
455 455
456 456 -webkit-flex-direction: row;
457 457 -ms-flex-direction: row;
458 458 -webkit-box-orient: horizontal;
459 459 -webkit-box-direction: normal;
460 460 -webkit-align-items: center;
461 461 -ms-flex-align: center;
462 462 -webkit-box-align: center;
463 463 align-items: center;
464 464 }
465 465
466 466 .flyout-menu ul li a
467 467 {
468 468 line-height: 40px;
469 469
470 470 display: block;
471 471 overflow: hidden;
472 472
473 473 height: 40px;
474 474
475 475 white-space: nowrap;
476 476 text-overflow: ellipsis;
477 477
478 478 border-top: 1px solid rgba(255,255,255,.1);
479 479 }
480 480
481 481 .flyout-menu ul li:first-child a
482 482 {
483 483 line-height: 39px;
484 484
485 485 height: 39px;
486 486
487 487 border-top: none;
488 488 }
489 489
490 490 .flyout-menu a
491 491 {
492 492 color: white;
493 493 }
494 494
495 495 .flyout-menu ul li a:hover
496 496 {
497 497 text-decoration: none;
498 498 }
499 499
500 500 .flyout-menu ul li a.new-object,
501 501 .new-object ~ .menu-children
502 502 {
503 503 display: none;
504 504 }
505 505
506 506 /* Left flyout search container */
507 507 .flyout-menu__search
508 508 {
509 509 line-height: 54px;
510 510
511 511 height: 64px;
512 512 padding-top: 3px;
513 513 padding-right: 8px;
514 514 }
515 515
516 516 .flyout-menu__search input[type='text']
517 517 {
518 518 line-height: 2;
519 519
520 520 width: 100%;
521 521 height: 38px;
522 522 padding-left: 27px;
523 523
524 524 vertical-align: middle;
525 525
526 526 border: none;
527 527 -webkit-border-radius: 3px;
528 528 border-radius: 3px;
529 529 background-color: #fff;
530 530 }
531 531
532 532 .flyout-menu__avatar
533 533 {
534 534 display: -webkit-box;
535 535 display: -webkit-flex;
536 536 display: -ms-flexbox;
537 537 display: flex;
538 538
539 539 width: 100%;
540 540
541 541 border-top: 1px solid rgba(255,255,255,.1);
542 542 }
543 543
544 544
545 545 .flyout-menu__avatar img.gravatar
546 546 {
547 547 width: 40px;
548 548 height: 40px;
549 549 padding: 0;
550 550
551 551 vertical-align: top;
552 552
553 553 border-width: 0;
554 554 }
555 555
556 556 .flyout-menu__avatar a
557 557 {
558 558 line-height: 40px;
559 559
560 560 height: auto;
561 561 height: 40px;
562 562
563 563 text-decoration: none;
564 564
565 565 color: white;
566 566 }
567 567
568 568 /* avatar */
569 569 .flyout-menu__avatar a:first-child
570 570 {
571 571 line-height: 0;
572 572
573 573 width: 40px;
574 574 padding: 0;
575 575 }
576 576
577 577 .flyout-menu__avatar .user
578 578 {
579 579 padding-left: 15px;
580 580 }
581 581
582 582 /* user link when no avatar is present */
583 583 .flyout-menu__avatar--no-avatar a.user
584 584 {
585 585 line-height: 40px;
586 586
587 587 padding-left: 8px;
588 588 }
589 589
590 590
591 591 .flyout-is-active body
592 592 {
593 593 overflow: hidden; /* for body not to have scrollbars when left flyout menu is active */
594 594 }
595 595
596 596 html.flyout-is-active
597 597 {
598 598 overflow: hidden;
599 599 }
600 600
601 601
602 602 .flyout-is-active #wrapper
603 603 {
604 604 right: 250px; /* when left flyout is active, move body to the right (same amount like flyout-menu's width) */
605 605
606 606 overflow: visible;
607 607
608 608 height: 100%;
609 609 }
610 610
611 611 .flyout-is-active .mobile-toggle-button:after
612 612 {
613 613 content: '\00D7'; /* close glyph */
614 614 }
615 615
616 616 .flyout-is-active #wrapper2
617 617 {
618 618
619 619 /*
620 620 * only relevant for devices with cursor when flyout it active, in order to show,
621 621 * that whole wrapper content is clickable and closes flyout menu
622 622 */
623 623 cursor: pointer;
624 624 }
625 625
626 626
627 627 #admin-menu
628 628 {
629 629 padding-left: 0;
630 630 }
631 631
632 632 #admin-menu li
633 633 {
634 634 padding-bottom: 0;
635 635 }
636 636
637 637 #admin-menu a,
638 638 #admin-menu a.selected
639 639 {
640 640 line-height: 40px;
641 641
642 642 padding: 0;
643 643 padding-left: 32px !important;
644 644
645 645 background-position: 8px 50%;
646 646 }
647 647
648 648 /*----------------------------------------*\
649 649 E) UX ELEMENTS
650 650 \*----------------------------------------*/
651 651
652 652 /* Contextual Buttons */
653 653
654 654 #content>.contextual
655 655 {
656 656 width: 100%;
657 657 margin-bottom: .5em;
658 658 padding-left: 0; /* reset left padding in order to use whole space */
659 659
660 660 white-space: normal;
661 661
662 662 color: transparent;
663 663 }
664 664
665 665 #content>.contextual a,
666 666 p.buttons a
667 667 {
668 668 font-weight: bold;
669 669
670 670 display: inline-block;
671 671
672 672 margin: 5px 0;
673 673 margin-right: 2px;
674 674 padding: 9px 9px 9px 9px;
675 675
676 676 border: 1px solid #ddd;
677 677 -webkit-border-radius: 3px;
678 678 border-radius: 3px;
679 679 background-color: transparent;
680 680 background-position-x: 4px;
681 681 }
682 682
683 683 #content>.contextual a.icon,
684 684 p.buttons a.icon
685 685 {
686 686 padding-left: 25px;
687 687 }
688 688
689 689 .flyout-menu .contextual
690 690 {
691 691 float: none;
692 692 }
693 693
694 694 /* loading indicator */
695 695 #ajax-indicator {
696 696 width: 60%;
697 697 left: 20%;
698 698 }
699 699
700 700 /* jquery ui dialogs */
701 701 .ui-dialog
702 702 {
703 703 max-width: 98%;
704 704 margin: 1%;
705 705 }
706 706
707 707 .ui-dialog .ui-dialog-content
708 708 {
709 709 padding-left: 0;
710 710 padding-right: 0;
711 711 }
712 712
713 713 #filters-table {width:100%; float:none;}
714 714 .add-filter {width:100%; float:none; text-align: left; margin-top: 8px;}
715 715
716 716 /*----------------------------------------*\
717 717 F) PAGE SPECIFIC STYLES
718 718 \*----------------------------------------*/
719 719
720 720 /* page /login */
721 721
722 722 #login-form table
723 723 {
724 724 width: 100%;
725 725 }
726 726
727 727 #login-form input#username,
728 728 #login-form input#password,
729 729 #login-form input#openid_url
730 730 {
731 731 width: 100%;
732 732 height: auto;
733 733 }
734 734
735 735 /* some themes add a margin to login page, remove it on mobile */
736 736 .action-login #main
737 737 {
738 738 margin: 0;
739 739 }
740 740
741 741 div#activity dl, #search-results { margin-left: 0; }
742 742
743 .version-overview table.progress {width:75%;}
743 744 div#version-summary {float:none; width:100%; margin-left:0;}
745 body.controller-versions.action-show div#roadmap .related-issues {width:100%;}
744 746
745 747 /*----------------------------------------*\
746 748 G) FORMS
747 749 \*----------------------------------------*/
748 750
749 751 /* tabular forms resets for mobile */
750 752 .tabular p, .tabular.settings p {
751 753 padding-left: 0;
752 754 }
753 755
754 756 .tabular label, .tabular.settings label {
755 757 display: block;
756 758 width: 100%;
757 759 margin-left: 0;
758 760 text-align: left;
759 761 }
760 762
761 763 .tabular input, .tabular select, .tabular textarea {
762 764 width: 100%;
763 765 max-width: 100%;
764 766 }
765 767
766 768 .tabular input[type="checkbox"], .tabular input.date {
767 769 width: auto;
768 770 max-width: 95%;
769 771 }
770 772
771 773 /* new issue form */
772 774 #all_attributes p:first-child {
773 775 float: none !important;
774 776 }
775 777
776 778 #issue_is_private_label {
777 779 display: inline;
778 780 }
779 781
780 782 span#watchers_inputs {
781 783 width: 100%;
782 784 }
783 785 }
General Comments 0
You need to be logged in to leave comments. Login now