##// END OF EJS Templates
Adds an helper for creating the context menu....
Jean-Philippe Lang -
r3428:bd5fe10c13b0
parent child
Show More
@@ -1,749 +1,760
1 1 # redMine - project management software
2 2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require 'coderay'
19 19 require 'coderay/helpers/file_type'
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
28 28 extend Forwardable
29 29 def_delegators :wiki_helper, :wikitoolbar_for, :heads_for_wiki_formatter
30 30
31 31 # Return true if user is authorized for controller/action, otherwise false
32 32 def authorize_for(controller, action)
33 33 User.current.allowed_to?({:controller => controller, :action => action}, @project)
34 34 end
35 35
36 36 # Display a link if user is authorized
37 37 def link_to_if_authorized(name, options = {}, html_options = nil, *parameters_for_method_reference)
38 38 link_to(name, options, html_options, *parameters_for_method_reference) if authorize_for(options[:controller] || params[:controller], options[:action])
39 39 end
40 40
41 41 # Display a link to remote if user is authorized
42 42 def link_to_remote_if_authorized(name, options = {}, html_options = nil)
43 43 url = options[:url] || {}
44 44 link_to_remote(name, options, html_options) if authorize_for(url[:controller] || params[:controller], url[:action])
45 45 end
46 46
47 47 # Displays a link to user's account page if active
48 48 def link_to_user(user, options={})
49 49 if user.is_a?(User)
50 50 name = h(user.name(options[:format]))
51 51 if user.active?
52 52 link_to name, :controller => 'users', :action => 'show', :id => user
53 53 else
54 54 name
55 55 end
56 56 else
57 57 h(user.to_s)
58 58 end
59 59 end
60 60
61 61 # Displays a link to +issue+ with its subject.
62 62 # Examples:
63 63 #
64 64 # link_to_issue(issue) # => Defect #6: This is the subject
65 65 # link_to_issue(issue, :truncate => 6) # => Defect #6: This i...
66 66 # link_to_issue(issue, :subject => false) # => Defect #6
67 67 # link_to_issue(issue, :project => true) # => Foo - Defect #6
68 68 #
69 69 def link_to_issue(issue, options={})
70 70 title = nil
71 71 subject = nil
72 72 if options[:subject] == false
73 73 title = truncate(issue.subject, :length => 60)
74 74 else
75 75 subject = issue.subject
76 76 if options[:truncate]
77 77 subject = truncate(subject, :length => options[:truncate])
78 78 end
79 79 end
80 80 s = link_to "#{issue.tracker} ##{issue.id}", {:controller => "issues", :action => "show", :id => issue},
81 81 :class => issue.css_classes,
82 82 :title => title
83 83 s << ": #{h subject}" if subject
84 84 s = "#{h issue.project} - " + s if options[:project]
85 85 s
86 86 end
87 87
88 88 # Generates a link to an attachment.
89 89 # Options:
90 90 # * :text - Link text (default to attachment filename)
91 91 # * :download - Force download (default: false)
92 92 def link_to_attachment(attachment, options={})
93 93 text = options.delete(:text) || attachment.filename
94 94 action = options.delete(:download) ? 'download' : 'show'
95 95
96 96 link_to(h(text), {:controller => 'attachments', :action => action, :id => attachment, :filename => attachment.filename }, options)
97 97 end
98 98
99 99 # Generates a link to a SCM revision
100 100 # Options:
101 101 # * :text - Link text (default to the formatted revision)
102 102 def link_to_revision(revision, project, options={})
103 103 text = options.delete(:text) || format_revision(revision)
104 104
105 105 link_to(text, {:controller => 'repositories', :action => 'revision', :id => project, :rev => revision}, :title => l(:label_revision_id, revision))
106 106 end
107 107
108 108 def toggle_link(name, id, options={})
109 109 onclick = "Element.toggle('#{id}'); "
110 110 onclick << (options[:focus] ? "Form.Element.focus('#{options[:focus]}'); " : "this.blur(); ")
111 111 onclick << "return false;"
112 112 link_to(name, "#", :onclick => onclick)
113 113 end
114 114
115 115 def image_to_function(name, function, html_options = {})
116 116 html_options.symbolize_keys!
117 117 tag(:input, html_options.merge({
118 118 :type => "image", :src => image_path(name),
119 119 :onclick => (html_options[:onclick] ? "#{html_options[:onclick]}; " : "") + "#{function};"
120 120 }))
121 121 end
122 122
123 123 def prompt_to_remote(name, text, param, url, html_options = {})
124 124 html_options[:onclick] = "promptToRemote('#{text}', '#{param}', '#{url_for(url)}'); return false;"
125 125 link_to name, {}, html_options
126 126 end
127 127
128 128 def format_activity_title(text)
129 129 h(truncate_single_line(text, :length => 100))
130 130 end
131 131
132 132 def format_activity_day(date)
133 133 date == Date.today ? l(:label_today).titleize : format_date(date)
134 134 end
135 135
136 136 def format_activity_description(text)
137 137 h(truncate(text.to_s, :length => 120).gsub(%r{[\r\n]*<(pre|code)>.*$}m, '...')).gsub(/[\r\n]+/, "<br />")
138 138 end
139 139
140 140 def format_version_name(version)
141 141 if version.project == @project
142 142 h(version)
143 143 else
144 144 h("#{version.project} - #{version}")
145 145 end
146 146 end
147 147
148 148 def due_date_distance_in_words(date)
149 149 if date
150 150 l((date < Date.today ? :label_roadmap_overdue : :label_roadmap_due_in), distance_of_date_in_words(Date.today, date))
151 151 end
152 152 end
153 153
154 154 def render_page_hierarchy(pages, node=nil)
155 155 content = ''
156 156 if pages[node]
157 157 content << "<ul class=\"pages-hierarchy\">\n"
158 158 pages[node].each do |page|
159 159 content << "<li>"
160 160 content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'index', :id => page.project, :page => page.title},
161 161 :title => (page.respond_to?(:updated_on) ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil))
162 162 content << "\n" + render_page_hierarchy(pages, page.id) if pages[page.id]
163 163 content << "</li>\n"
164 164 end
165 165 content << "</ul>\n"
166 166 end
167 167 content
168 168 end
169 169
170 170 # Renders flash messages
171 171 def render_flash_messages
172 172 s = ''
173 173 flash.each do |k,v|
174 174 s << content_tag('div', v, :class => "flash #{k}")
175 175 end
176 176 s
177 177 end
178 178
179 179 # Renders tabs and their content
180 180 def render_tabs(tabs)
181 181 if tabs.any?
182 182 render :partial => 'common/tabs', :locals => {:tabs => tabs}
183 183 else
184 184 content_tag 'p', l(:label_no_data), :class => "nodata"
185 185 end
186 186 end
187 187
188 188 # Renders the project quick-jump box
189 189 def render_project_jump_box
190 190 # Retrieve them now to avoid a COUNT query
191 191 projects = User.current.projects.all
192 192 if projects.any?
193 193 s = '<select onchange="if (this.value != \'\') { window.location = this.value; }">' +
194 194 "<option value=''>#{ l(:label_jump_to_a_project) }</option>" +
195 195 '<option value="" disabled="disabled">---</option>'
196 196 s << project_tree_options_for_select(projects, :selected => @project) do |p|
197 197 { :value => url_for(:controller => 'projects', :action => 'show', :id => p, :jump => current_menu_item) }
198 198 end
199 199 s << '</select>'
200 200 s
201 201 end
202 202 end
203 203
204 204 def project_tree_options_for_select(projects, options = {})
205 205 s = ''
206 206 project_tree(projects) do |project, level|
207 207 name_prefix = (level > 0 ? ('&nbsp;' * 2 * level + '&#187; ') : '')
208 208 tag_options = {:value => project.id}
209 209 if project == options[:selected] || (options[:selected].respond_to?(:include?) && options[:selected].include?(project))
210 210 tag_options[:selected] = 'selected'
211 211 else
212 212 tag_options[:selected] = nil
213 213 end
214 214 tag_options.merge!(yield(project)) if block_given?
215 215 s << content_tag('option', name_prefix + h(project), tag_options)
216 216 end
217 217 s
218 218 end
219 219
220 220 # Yields the given block for each project with its level in the tree
221 221 def project_tree(projects, &block)
222 222 ancestors = []
223 223 projects.sort_by(&:lft).each do |project|
224 224 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
225 225 ancestors.pop
226 226 end
227 227 yield project, ancestors.size
228 228 ancestors << project
229 229 end
230 230 end
231 231
232 232 def project_nested_ul(projects, &block)
233 233 s = ''
234 234 if projects.any?
235 235 ancestors = []
236 236 projects.sort_by(&:lft).each do |project|
237 237 if (ancestors.empty? || project.is_descendant_of?(ancestors.last))
238 238 s << "<ul>\n"
239 239 else
240 240 ancestors.pop
241 241 s << "</li>"
242 242 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
243 243 ancestors.pop
244 244 s << "</ul></li>\n"
245 245 end
246 246 end
247 247 s << "<li>"
248 248 s << yield(project).to_s
249 249 ancestors << project
250 250 end
251 251 s << ("</li></ul>\n" * ancestors.size)
252 252 end
253 253 s
254 254 end
255 255
256 256 def principals_check_box_tags(name, principals)
257 257 s = ''
258 258 principals.sort.each do |principal|
259 259 s << "<label>#{ check_box_tag name, principal.id, false } #{h principal}</label>\n"
260 260 end
261 261 s
262 262 end
263 263
264 264 # Truncates and returns the string as a single line
265 265 def truncate_single_line(string, *args)
266 266 truncate(string.to_s, *args).gsub(%r{[\r\n]+}m, ' ')
267 267 end
268 268
269 269 def html_hours(text)
270 270 text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>')
271 271 end
272 272
273 273 def authoring(created, author, options={})
274 274 l(options[:label] || :label_added_time_by, :author => link_to_user(author), :age => time_tag(created))
275 275 end
276 276
277 277 def time_tag(time)
278 278 text = distance_of_time_in_words(Time.now, time)
279 279 if @project
280 280 link_to(text, {:controller => 'projects', :action => 'activity', :id => @project, :from => time.to_date}, :title => format_time(time))
281 281 else
282 282 content_tag('acronym', text, :title => format_time(time))
283 283 end
284 284 end
285 285
286 286 def syntax_highlight(name, content)
287 287 type = CodeRay::FileType[name]
288 288 type ? CodeRay.scan(content, type).html : h(content)
289 289 end
290 290
291 291 def to_path_param(path)
292 292 path.to_s.split(%r{[/\\]}).select {|p| !p.blank?}
293 293 end
294 294
295 295 def pagination_links_full(paginator, count=nil, options={})
296 296 page_param = options.delete(:page_param) || :page
297 297 per_page_links = options.delete(:per_page_links)
298 298 url_param = params.dup
299 299 # don't reuse query params if filters are present
300 300 url_param.merge!(:fields => nil, :values => nil, :operators => nil) if url_param.delete(:set_filter)
301 301
302 302 html = ''
303 303 if paginator.current.previous
304 304 html << link_to_remote_content_update('&#171; ' + l(:label_previous), url_param.merge(page_param => paginator.current.previous)) + ' '
305 305 end
306 306
307 307 html << (pagination_links_each(paginator, options) do |n|
308 308 link_to_remote_content_update(n.to_s, url_param.merge(page_param => n))
309 309 end || '')
310 310
311 311 if paginator.current.next
312 312 html << ' ' + link_to_remote_content_update((l(:label_next) + ' &#187;'), url_param.merge(page_param => paginator.current.next))
313 313 end
314 314
315 315 unless count.nil?
316 316 html << " (#{paginator.current.first_item}-#{paginator.current.last_item}/#{count})"
317 317 if per_page_links != false && links = per_page_links(paginator.items_per_page)
318 318 html << " | #{links}"
319 319 end
320 320 end
321 321
322 322 html
323 323 end
324 324
325 325 def per_page_links(selected=nil)
326 326 url_param = params.dup
327 327 url_param.clear if url_param.has_key?(:set_filter)
328 328
329 329 links = Setting.per_page_options_array.collect do |n|
330 330 n == selected ? n : link_to_remote(n, {:update => "content",
331 331 :url => params.dup.merge(:per_page => n),
332 332 :method => :get},
333 333 {:href => url_for(url_param.merge(:per_page => n))})
334 334 end
335 335 links.size > 1 ? l(:label_display_per_page, links.join(', ')) : nil
336 336 end
337 337
338 338 def reorder_links(name, url)
339 339 link_to(image_tag('2uparrow.png', :alt => l(:label_sort_highest)), url.merge({"#{name}[move_to]" => 'highest'}), :method => :post, :title => l(:label_sort_highest)) +
340 340 link_to(image_tag('1uparrow.png', :alt => l(:label_sort_higher)), url.merge({"#{name}[move_to]" => 'higher'}), :method => :post, :title => l(:label_sort_higher)) +
341 341 link_to(image_tag('1downarrow.png', :alt => l(:label_sort_lower)), url.merge({"#{name}[move_to]" => 'lower'}), :method => :post, :title => l(:label_sort_lower)) +
342 342 link_to(image_tag('2downarrow.png', :alt => l(:label_sort_lowest)), url.merge({"#{name}[move_to]" => 'lowest'}), :method => :post, :title => l(:label_sort_lowest))
343 343 end
344 344
345 345 def breadcrumb(*args)
346 346 elements = args.flatten
347 347 elements.any? ? content_tag('p', args.join(' &#187; ') + ' &#187; ', :class => 'breadcrumb') : nil
348 348 end
349 349
350 350 def other_formats_links(&block)
351 351 concat('<p class="other-formats">' + l(:label_export_to))
352 352 yield Redmine::Views::OtherFormatsBuilder.new(self)
353 353 concat('</p>')
354 354 end
355 355
356 356 def page_header_title
357 357 if @project.nil? || @project.new_record?
358 358 h(Setting.app_title)
359 359 else
360 360 b = []
361 361 ancestors = (@project.root? ? [] : @project.ancestors.visible)
362 362 if ancestors.any?
363 363 root = ancestors.shift
364 364 b << link_to(h(root), {:controller => 'projects', :action => 'show', :id => root, :jump => current_menu_item}, :class => 'root')
365 365 if ancestors.size > 2
366 366 b << '&#8230;'
367 367 ancestors = ancestors[-2, 2]
368 368 end
369 369 b += ancestors.collect {|p| link_to(h(p), {:controller => 'projects', :action => 'show', :id => p, :jump => current_menu_item}, :class => 'ancestor') }
370 370 end
371 371 b << h(@project)
372 372 b.join(' &#187; ')
373 373 end
374 374 end
375 375
376 376 def html_title(*args)
377 377 if args.empty?
378 378 title = []
379 379 title << @project.name if @project
380 380 title += @html_title if @html_title
381 381 title << Setting.app_title
382 382 title.select {|t| !t.blank? }.join(' - ')
383 383 else
384 384 @html_title ||= []
385 385 @html_title += args
386 386 end
387 387 end
388 388
389 389 def accesskey(s)
390 390 Redmine::AccessKeys.key_for s
391 391 end
392 392
393 393 # Formats text according to system settings.
394 394 # 2 ways to call this method:
395 395 # * with a String: textilizable(text, options)
396 396 # * with an object and one of its attribute: textilizable(issue, :description, options)
397 397 def textilizable(*args)
398 398 options = args.last.is_a?(Hash) ? args.pop : {}
399 399 case args.size
400 400 when 1
401 401 obj = options[:object]
402 402 text = args.shift
403 403 when 2
404 404 obj = args.shift
405 405 attr = args.shift
406 406 text = obj.send(attr).to_s
407 407 else
408 408 raise ArgumentError, 'invalid arguments to textilizable'
409 409 end
410 410 return '' if text.blank?
411 411
412 412 text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr) { |macro, args| exec_macro(macro, obj, args) }
413 413
414 414 only_path = options.delete(:only_path) == false ? false : true
415 415
416 416 # when using an image link, try to use an attachment, if possible
417 417 attachments = options[:attachments] || (obj && obj.respond_to?(:attachments) ? obj.attachments : nil)
418 418
419 419 if attachments
420 420 attachments = attachments.sort_by(&:created_on).reverse
421 421 text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m|
422 422 filename, ext, alt, alttext = $1.downcase, $2, $3, $4
423 423
424 424 # search for the picture in attachments
425 425 if found = attachments.detect { |att| att.filename.downcase == filename }
426 426 image_url = url_for :only_path => only_path, :controller => 'attachments', :action => 'download', :id => found
427 427 desc = found.description.to_s.gsub('"', '')
428 428 if !desc.blank? && alttext.blank?
429 429 alt = " title=\"#{desc}\" alt=\"#{desc}\""
430 430 end
431 431 "src=\"#{image_url}\"#{alt}"
432 432 else
433 433 m
434 434 end
435 435 end
436 436 end
437 437
438 438
439 439 # different methods for formatting wiki links
440 440 case options[:wiki_links]
441 441 when :local
442 442 # used for local links to html files
443 443 format_wiki_link = Proc.new {|project, title, anchor| "#{title}.html" }
444 444 when :anchor
445 445 # used for single-file wiki export
446 446 format_wiki_link = Proc.new {|project, title, anchor| "##{title}" }
447 447 else
448 448 format_wiki_link = Proc.new {|project, title, anchor| url_for(:only_path => only_path, :controller => 'wiki', :action => 'index', :id => project, :page => title, :anchor => anchor) }
449 449 end
450 450
451 451 project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
452 452
453 453 # Wiki links
454 454 #
455 455 # Examples:
456 456 # [[mypage]]
457 457 # [[mypage|mytext]]
458 458 # wiki links can refer other project wikis, using project name or identifier:
459 459 # [[project:]] -> wiki starting page
460 460 # [[project:|mytext]]
461 461 # [[project:mypage]]
462 462 # [[project:mypage|mytext]]
463 463 text = text.gsub(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m|
464 464 link_project = project
465 465 esc, all, page, title = $1, $2, $3, $5
466 466 if esc.nil?
467 467 if page =~ /^([^\:]+)\:(.*)$/
468 468 link_project = Project.find_by_name($1) || Project.find_by_identifier($1)
469 469 page = $2
470 470 title ||= $1 if page.blank?
471 471 end
472 472
473 473 if link_project && link_project.wiki
474 474 # extract anchor
475 475 anchor = nil
476 476 if page =~ /^(.+?)\#(.+)$/
477 477 page, anchor = $1, $2
478 478 end
479 479 # check if page exists
480 480 wiki_page = link_project.wiki.find_page(page)
481 481 link_to((title || page), format_wiki_link.call(link_project, Wiki.titleize(page), anchor),
482 482 :class => ('wiki-page' + (wiki_page ? '' : ' new')))
483 483 else
484 484 # project or wiki doesn't exist
485 485 all
486 486 end
487 487 else
488 488 all
489 489 end
490 490 end
491 491
492 492 # Redmine links
493 493 #
494 494 # Examples:
495 495 # Issues:
496 496 # #52 -> Link to issue #52
497 497 # Changesets:
498 498 # r52 -> Link to revision 52
499 499 # commit:a85130f -> Link to scmid starting with a85130f
500 500 # Documents:
501 501 # document#17 -> Link to document with id 17
502 502 # document:Greetings -> Link to the document with title "Greetings"
503 503 # document:"Some document" -> Link to the document with title "Some document"
504 504 # Versions:
505 505 # version#3 -> Link to version with id 3
506 506 # version:1.0.0 -> Link to version named "1.0.0"
507 507 # version:"1.0 beta 2" -> Link to version named "1.0 beta 2"
508 508 # Attachments:
509 509 # attachment:file.zip -> Link to the attachment of the current object named file.zip
510 510 # Source files:
511 511 # source:some/file -> Link to the file located at /some/file in the project's repository
512 512 # source:some/file@52 -> Link to the file's revision 52
513 513 # source:some/file#L120 -> Link to line 120 of the file
514 514 # source:some/file@52#L120 -> Link to line 120 of the file's revision 52
515 515 # export:some/file -> Force the download of the file
516 516 # Forum messages:
517 517 # message#1218 -> Link to message with id 1218
518 518 text = text.gsub(%r{([\s\(,\-\>]|^)(!)?(attachment|document|version|commit|source|export|message|project)?((#|r)(\d+)|(:)([^"\s<>][^\s<>]*?|"[^"]+?"))(?=(?=[[:punct:]]\W)|,|\s|<|$)}) do |m|
519 519 leading, esc, prefix, sep, identifier = $1, $2, $3, $5 || $7, $6 || $8
520 520 link = nil
521 521 if esc.nil?
522 522 if prefix.nil? && sep == 'r'
523 523 if project && (changeset = project.changesets.find_by_revision(identifier))
524 524 link = link_to("r#{identifier}", {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => changeset.revision},
525 525 :class => 'changeset',
526 526 :title => truncate_single_line(changeset.comments, :length => 100))
527 527 end
528 528 elsif sep == '#'
529 529 oid = identifier.to_i
530 530 case prefix
531 531 when nil
532 532 if issue = Issue.visible.find_by_id(oid, :include => :status)
533 533 link = link_to("##{oid}", {:only_path => only_path, :controller => 'issues', :action => 'show', :id => oid},
534 534 :class => issue.css_classes,
535 535 :title => "#{truncate(issue.subject, :length => 100)} (#{issue.status.name})")
536 536 end
537 537 when 'document'
538 538 if document = Document.find_by_id(oid, :include => [:project], :conditions => Project.visible_by(User.current))
539 539 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
540 540 :class => 'document'
541 541 end
542 542 when 'version'
543 543 if version = Version.find_by_id(oid, :include => [:project], :conditions => Project.visible_by(User.current))
544 544 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
545 545 :class => 'version'
546 546 end
547 547 when 'message'
548 548 if message = Message.find_by_id(oid, :include => [:parent, {:board => :project}], :conditions => Project.visible_by(User.current))
549 549 link = link_to h(truncate(message.subject, :length => 60)), {:only_path => only_path,
550 550 :controller => 'messages',
551 551 :action => 'show',
552 552 :board_id => message.board,
553 553 :id => message.root,
554 554 :anchor => (message.parent ? "message-#{message.id}" : nil)},
555 555 :class => 'message'
556 556 end
557 557 when 'project'
558 558 if p = Project.visible.find_by_id(oid)
559 559 link = link_to h(p.name), {:only_path => only_path, :controller => 'projects', :action => 'show', :id => p},
560 560 :class => 'project'
561 561 end
562 562 end
563 563 elsif sep == ':'
564 564 # removes the double quotes if any
565 565 name = identifier.gsub(%r{^"(.*)"$}, "\\1")
566 566 case prefix
567 567 when 'document'
568 568 if project && document = project.documents.find_by_title(name)
569 569 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
570 570 :class => 'document'
571 571 end
572 572 when 'version'
573 573 if project && version = project.versions.find_by_name(name)
574 574 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
575 575 :class => 'version'
576 576 end
577 577 when 'commit'
578 578 if project && (changeset = project.changesets.find(:first, :conditions => ["scmid LIKE ?", "#{name}%"]))
579 579 link = link_to h("#{name}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => changeset.revision},
580 580 :class => 'changeset',
581 581 :title => truncate_single_line(changeset.comments, :length => 100)
582 582 end
583 583 when 'source', 'export'
584 584 if project && project.repository
585 585 name =~ %r{^[/\\]*(.*?)(@([0-9a-f]+))?(#(L\d+))?$}
586 586 path, rev, anchor = $1, $3, $5
587 587 link = link_to h("#{prefix}:#{name}"), {:controller => 'repositories', :action => 'entry', :id => project,
588 588 :path => to_path_param(path),
589 589 :rev => rev,
590 590 :anchor => anchor,
591 591 :format => (prefix == 'export' ? 'raw' : nil)},
592 592 :class => (prefix == 'export' ? 'source download' : 'source')
593 593 end
594 594 when 'attachment'
595 595 if attachments && attachment = attachments.detect {|a| a.filename == name }
596 596 link = link_to h(attachment.filename), {:only_path => only_path, :controller => 'attachments', :action => 'download', :id => attachment},
597 597 :class => 'attachment'
598 598 end
599 599 when 'project'
600 600 if p = Project.visible.find(:first, :conditions => ["identifier = :s OR LOWER(name) = :s", {:s => name.downcase}])
601 601 link = link_to h(p.name), {:only_path => only_path, :controller => 'projects', :action => 'show', :id => p},
602 602 :class => 'project'
603 603 end
604 604 end
605 605 end
606 606 end
607 607 leading + (link || "#{prefix}#{sep}#{identifier}")
608 608 end
609 609
610 610 text
611 611 end
612 612
613 613 # Same as Rails' simple_format helper without using paragraphs
614 614 def simple_format_without_paragraph(text)
615 615 text.to_s.
616 616 gsub(/\r\n?/, "\n"). # \r\n and \r -> \n
617 617 gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br
618 618 gsub(/([^\n]\n)(?=[^\n])/, '\1<br />') # 1 newline -> br
619 619 end
620 620
621 621 def lang_options_for_select(blank=true)
622 622 (blank ? [["(auto)", ""]] : []) +
623 623 valid_languages.collect{|lang| [ ll(lang.to_s, :general_lang_name), lang.to_s]}.sort{|x,y| x.last <=> y.last }
624 624 end
625 625
626 626 def label_tag_for(name, option_tags = nil, options = {})
627 627 label_text = l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) + (options.delete(:required) ? @template.content_tag("span", " *", :class => "required"): "")
628 628 content_tag("label", label_text)
629 629 end
630 630
631 631 def labelled_tabular_form_for(name, object, options, &proc)
632 632 options[:html] ||= {}
633 633 options[:html][:class] = 'tabular' unless options[:html].has_key?(:class)
634 634 form_for(name, object, options.merge({ :builder => TabularFormBuilder, :lang => current_language}), &proc)
635 635 end
636 636
637 637 def back_url_hidden_field_tag
638 638 back_url = params[:back_url] || request.env['HTTP_REFERER']
639 639 back_url = CGI.unescape(back_url.to_s)
640 640 hidden_field_tag('back_url', CGI.escape(back_url)) unless back_url.blank?
641 641 end
642 642
643 643 def check_all_links(form_name)
644 644 link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
645 645 " | " +
646 646 link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
647 647 end
648 648
649 649 def progress_bar(pcts, options={})
650 650 pcts = [pcts, pcts] unless pcts.is_a?(Array)
651 651 pcts = pcts.collect(&:round)
652 652 pcts[1] = pcts[1] - pcts[0]
653 653 pcts << (100 - pcts[1] - pcts[0])
654 654 width = options[:width] || '100px;'
655 655 legend = options[:legend] || ''
656 656 content_tag('table',
657 657 content_tag('tr',
658 658 (pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0]}%;", :class => 'closed') : '') +
659 659 (pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1]}%;", :class => 'done') : '') +
660 660 (pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2]}%;", :class => 'todo') : '')
661 661 ), :class => 'progress', :style => "width: #{width};") +
662 662 content_tag('p', legend, :class => 'pourcent')
663 663 end
664
665 def context_menu(url)
666 unless @context_menu_included
667 content_for :header_tags do
668 javascript_include_tag('context_menu') +
669 stylesheet_link_tag('context_menu')
670 end
671 @context_menu_included = true
672 end
673 javascript_tag "new ContextMenu('#{ url_for(url) }')"
674 end
664 675
665 676 def context_menu_link(name, url, options={})
666 677 options[:class] ||= ''
667 678 if options.delete(:selected)
668 679 options[:class] << ' icon-checked disabled'
669 680 options[:disabled] = true
670 681 end
671 682 if options.delete(:disabled)
672 683 options.delete(:method)
673 684 options.delete(:confirm)
674 685 options.delete(:onclick)
675 686 options[:class] << ' disabled'
676 687 url = '#'
677 688 end
678 689 link_to name, url, options
679 690 end
680 691
681 692 def calendar_for(field_id)
682 693 include_calendar_headers_tags
683 694 image_tag("calendar.png", {:id => "#{field_id}_trigger",:class => "calendar-trigger"}) +
684 695 javascript_tag("Calendar.setup({inputField : '#{field_id}', ifFormat : '%Y-%m-%d', button : '#{field_id}_trigger' });")
685 696 end
686 697
687 698 def include_calendar_headers_tags
688 699 unless @calendar_headers_tags_included
689 700 @calendar_headers_tags_included = true
690 701 content_for :header_tags do
691 702 start_of_week = case Setting.start_of_week.to_i
692 703 when 1
693 704 'Calendar._FD = 1;' # Monday
694 705 when 7
695 706 'Calendar._FD = 0;' # Sunday
696 707 else
697 708 '' # use language
698 709 end
699 710
700 711 javascript_include_tag('calendar/calendar') +
701 712 javascript_include_tag("calendar/lang/calendar-#{current_language.to_s.downcase}.js") +
702 713 javascript_tag(start_of_week) +
703 714 javascript_include_tag('calendar/calendar-setup') +
704 715 stylesheet_link_tag('calendar')
705 716 end
706 717 end
707 718 end
708 719
709 720 def content_for(name, content = nil, &block)
710 721 @has_content ||= {}
711 722 @has_content[name] = true
712 723 super(name, content, &block)
713 724 end
714 725
715 726 def has_content?(name)
716 727 (@has_content && @has_content[name]) || false
717 728 end
718 729
719 730 # Returns the avatar image tag for the given +user+ if avatars are enabled
720 731 # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>')
721 732 def avatar(user, options = { })
722 733 if Setting.gravatar_enabled?
723 734 options.merge!({:ssl => Setting.protocol == 'https', :default => Setting.gravatar_default})
724 735 email = nil
725 736 if user.respond_to?(:mail)
726 737 email = user.mail
727 738 elsif user.to_s =~ %r{<(.+?)>}
728 739 email = $1
729 740 end
730 741 return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil
731 742 end
732 743 end
733 744
734 745 private
735 746
736 747 def wiki_helper
737 748 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
738 749 extend helper
739 750 return self
740 751 end
741 752
742 753 def link_to_remote_content_update(text, url_params)
743 754 link_to_remote(text,
744 755 {:url => url_params, :method => :get, :update => 'content', :complete => 'window.scrollTo(0,0)'},
745 756 {:href => url_for(:params => url_params)}
746 757 )
747 758 end
748 759
749 760 end
@@ -1,86 +1,83
1 1 <div class="contextual">
2 2 <% if !@query.new_record? && @query.editable_by?(User.current) %>
3 3 <%= link_to l(:button_edit), {:controller => 'queries', :action => 'edit', :id => @query}, :class => 'icon icon-edit' %>
4 4 <%= link_to l(:button_delete), {:controller => 'queries', :action => 'destroy', :id => @query}, :confirm => l(:text_are_you_sure), :method => :post, :class => 'icon icon-del' %>
5 5 <% end %>
6 6 </div>
7 7
8 8 <h2><%= @query.new_record? ? l(:label_issue_plural) : h(@query.name) %></h2>
9 9 <% html_title(@query.new_record? ? l(:label_issue_plural) : @query.name) %>
10 10
11 11 <% form_tag({ :controller => 'queries', :action => 'new' }, :id => 'query_form') do %>
12 12 <%= hidden_field_tag('project_id', @project.to_param) if @project %>
13 13 <div id="query_form_content">
14 14 <fieldset id="filters" class="collapsible <%= @query.new_record? ? "" : "collapsed" %>">
15 15 <legend onclick="toggleFieldset(this);"><%= l(:label_filter_plural) %></legend>
16 16 <div style="<%= @query.new_record? ? "" : "display: none;" %>">
17 17 <%= render :partial => 'queries/filters', :locals => {:query => @query} %>
18 18 </div>
19 19 </fieldset>
20 20 <fieldset class="collapsible collapsed">
21 21 <legend onclick="toggleFieldset(this);"><%= l(:label_options) %></legend>
22 22 <div style="display: none;">
23 23 <table>
24 24 <tr>
25 25 <td><%= l(:field_column_names) %></td>
26 26 <td><%= render :partial => 'queries/columns', :locals => {:query => @query} %></td>
27 27 </tr>
28 28 <tr>
29 29 <td><%= l(:field_group_by) %></td>
30 30 <td><%= select_tag('group_by', options_for_select([[]] + @query.groupable_columns.collect {|c| [c.caption, c.name.to_s]}, @query.group_by)) %></td>
31 31 </tr>
32 32 </table>
33 33 </div>
34 34 </fieldset>
35 35 </div>
36 36 <p class="buttons">
37 37
38 38 <%= link_to_remote l(:button_apply),
39 39 { :url => { :set_filter => 1 },
40 40 :before => 'selectAllOptions("selected_columns");',
41 41 :update => "content",
42 42 :with => "Form.serialize('query_form')"
43 43 }, :class => 'icon icon-checked' %>
44 44
45 45 <%= link_to_remote l(:button_clear),
46 46 { :url => { :set_filter => 1, :project_id => @project },
47 47 :method => :get,
48 48 :update => "content",
49 49 }, :class => 'icon icon-reload' %>
50 50
51 51 <% if @query.new_record? && User.current.allowed_to?(:save_queries, @project, :global => true) %>
52 52 <%= link_to l(:button_save), {}, :onclick => "selectAllOptions('selected_columns'); $('query_form').submit(); return false;", :class => 'icon icon-save' %>
53 53 <% end %>
54 54 </p>
55 55 <% end %>
56 56
57 57 <%= error_messages_for 'query' %>
58 58 <% if @query.valid? %>
59 59 <% if @issues.empty? %>
60 60 <p class="nodata"><%= l(:label_no_data) %></p>
61 61 <% else %>
62 62 <%= render :partial => 'issues/list', :locals => {:issues => @issues, :query => @query} %>
63 63 <p class="pagination"><%= pagination_links_full @issue_pages, @issue_count %></p>
64 64 <% end %>
65 65
66 66 <% other_formats_links do |f| %>
67 67 <%= f.link_to 'Atom', :url => { :project_id => @project, :query_id => (@query.new_record? ? nil : @query), :key => User.current.rss_key } %>
68 68 <%= f.link_to 'CSV', :url => { :project_id => @project } %>
69 69 <%= f.link_to 'PDF', :url => { :project_id => @project } %>
70 70 <% end %>
71 71
72 72 <% end %>
73 73
74 74 <% content_for :sidebar do %>
75 75 <%= render :partial => 'issues/sidebar' %>
76 76 <% end %>
77 77
78 78 <% content_for :header_tags do %>
79 79 <%= auto_discovery_link_tag(:atom, {:query_id => @query, :format => 'atom', :page => nil, :key => User.current.rss_key}, :title => l(:label_issue_plural)) %>
80 80 <%= auto_discovery_link_tag(:atom, {:action => 'changes', :query_id => @query, :format => 'atom', :page => nil, :key => User.current.rss_key}, :title => l(:label_changes_details)) %>
81 <%= javascript_include_tag 'context_menu' %>
82 <%= stylesheet_link_tag 'context_menu' %>
83 81 <% end %>
84 82
85 <div id="context-menu" style="display: none;"></div>
86 <%= javascript_tag "new ContextMenu('#{url_for(:controller => 'issues', :action => 'context_menu')}')" %>
83 <%= context_menu :controller => 'issues', :action => 'context_menu' %>
@@ -1,42 +1,36
1 1 <div class="contextual">
2 2 <%= link_to l(:label_personalize_page), :action => 'page_layout' %>
3 3 </div>
4 4
5 5 <h2><%=l(:label_my_page)%></h2>
6 6
7 7 <div id="list-top">
8 8 <% @blocks['top'].each do |b|
9 9 next unless MyController::BLOCKS.keys.include? b %>
10 10 <div class="mypage-box">
11 11 <%= render :partial => "my/blocks/#{b}", :locals => { :user => @user } %>
12 12 </div>
13 13 <% end if @blocks['top'] %>
14 14 </div>
15 15
16 16 <div id="list-left" class="splitcontentleft">
17 17 <% @blocks['left'].each do |b|
18 18 next unless MyController::BLOCKS.keys.include? b %>
19 19 <div class="mypage-box">
20 20 <%= render :partial => "my/blocks/#{b}", :locals => { :user => @user } %>
21 21 </div>
22 22 <% end if @blocks['left'] %>
23 23 </div>
24 24
25 25 <div id="list-right" class="splitcontentright">
26 26 <% @blocks['right'].each do |b|
27 27 next unless MyController::BLOCKS.keys.include? b %>
28 28 <div class="mypage-box">
29 29 <%= render :partial => "my/blocks/#{b}", :locals => { :user => @user } %>
30 30 </div>
31 31 <% end if @blocks['right'] %>
32 32 </div>
33 33
34 <% content_for :header_tags do %>
35 <%= javascript_include_tag 'context_menu' %>
36 <%= stylesheet_link_tag 'context_menu' %>
37 <% end %>
38
39 <div id="context-menu" style="display: none;"></div>
40 <%= javascript_tag "new ContextMenu('#{url_for(:controller => 'issues', :action => 'context_menu')}')" %>
34 <%= context_menu :controller => 'issues', :action => 'context_menu' %>
41 35
42 36 <% html_title(l(:label_my_page)) -%>
@@ -1,221 +1,231
1 1 /* redMine - project management software
2 2 Copyright (C) 2006-2008 Jean-Philippe Lang */
3 3
4 4 var observingContextMenuClick;
5 5
6 6 ContextMenu = Class.create();
7 7 ContextMenu.prototype = {
8 8 initialize: function (url) {
9 9 this.url = url;
10
10 this.createMenu();
11
11 12 // prevent text selection in the issue list
12 13 var tables = $$('table.issues');
13 14 for (i=0; i<tables.length; i++) {
14 15 tables[i].onselectstart = function () { return false; } // ie
15 16 tables[i].onmousedown = function () { return false; } // mozilla
16 17 }
17 18
18 19 if (!observingContextMenuClick) {
19 20 Event.observe(document, 'click', this.Click.bindAsEventListener(this));
20 21 Event.observe(document, (window.opera ? 'click' : 'contextmenu'), this.RightClick.bindAsEventListener(this));
21 22 observingContextMenuClick = true;
22 23 }
23 24
24 25 this.unselectAll();
25 26 this.lastSelected = null;
26 27 },
27 28
28 29 RightClick: function(e) {
29 30 this.hideMenu();
30 31 // do not show the context menu on links
31 32 if (Event.element(e).tagName == 'A') { return; }
32 33 // right-click simulated by Alt+Click with Opera
33 34 if (window.opera && !e.altKey) { return; }
34 35 var tr = Event.findElement(e, 'tr');
35 36 if (tr == document || tr == undefined || !tr.hasClassName('hascontextmenu')) { return; }
36 37 Event.stop(e);
37 38 if (!this.isSelected(tr)) {
38 39 this.unselectAll();
39 40 this.addSelection(tr);
40 41 this.lastSelected = tr;
41 42 }
42 43 this.showMenu(e);
43 44 },
44 45
45 46 Click: function(e) {
46 47 this.hideMenu();
47 48 if (Event.element(e).tagName == 'A') { return; }
48 49 if (window.opera && e.altKey) { return; }
49 50 if (Event.isLeftClick(e) || (navigator.appVersion.match(/\bMSIE\b/))) {
50 51 var tr = Event.findElement(e, 'tr');
51 52 if (tr!=null && tr!=document && tr.hasClassName('hascontextmenu')) {
52 53 // a row was clicked, check if the click was on checkbox
53 54 var box = Event.findElement(e, 'input');
54 55 if (box!=document && box!=undefined) {
55 56 // a checkbox may be clicked
56 57 if (box.checked) {
57 58 tr.addClassName('context-menu-selection');
58 59 } else {
59 60 tr.removeClassName('context-menu-selection');
60 61 }
61 62 } else {
62 63 if (e.ctrlKey) {
63 64 this.toggleSelection(tr);
64 65 } else if (e.shiftKey) {
65 66 if (this.lastSelected != null) {
66 67 var toggling = false;
67 68 var rows = $$('.hascontextmenu');
68 69 for (i=0; i<rows.length; i++) {
69 70 if (toggling || rows[i]==tr) {
70 71 this.addSelection(rows[i]);
71 72 }
72 73 if (rows[i]==tr || rows[i]==this.lastSelected) {
73 74 toggling = !toggling;
74 75 }
75 76 }
76 77 } else {
77 78 this.addSelection(tr);
78 79 }
79 80 } else {
80 81 this.unselectAll();
81 82 this.addSelection(tr);
82 83 }
83 84 this.lastSelected = tr;
84 85 }
85 86 } else {
86 87 // click is outside the rows
87 88 var t = Event.findElement(e, 'a');
88 89 if ((t != document) && (Element.hasClassName(t, 'disabled') || Element.hasClassName(t, 'submenu'))) {
89 90 Event.stop(e);
90 91 }
91 92 }
92 93 }
93 94 else{
94 95 this.RightClick(e);
95 96 }
96 97 },
97 98
99 createMenu: function() {
100 if (!$('context-menu')) {
101 var menu = document.createElement("div");
102 menu.setAttribute("id", "context-menu");
103 menu.setAttribute("style", "display:none;");
104 document.getElementById("content").appendChild(menu);
105 }
106 },
107
98 108 showMenu: function(e) {
99 109 var mouse_x = Event.pointerX(e);
100 110 var mouse_y = Event.pointerY(e);
101 111 var render_x = mouse_x;
102 112 var render_y = mouse_y;
103 113 var dims;
104 114 var menu_width;
105 115 var menu_height;
106 116 var window_width;
107 117 var window_height;
108 118 var max_width;
109 119 var max_height;
110 120
111 121 $('context-menu').style['left'] = (render_x + 'px');
112 122 $('context-menu').style['top'] = (render_y + 'px');
113 123 Element.update('context-menu', '');
114 124
115 125 new Ajax.Updater({success:'context-menu'}, this.url,
116 126 {asynchronous:true,
117 127 evalScripts:true,
118 128 parameters:Form.serialize(Event.findElement(e, 'form')),
119 129 onComplete:function(request){
120 130 dims = $('context-menu').getDimensions();
121 131 menu_width = dims.width;
122 132 menu_height = dims.height;
123 133 max_width = mouse_x + 2*menu_width;
124 134 max_height = mouse_y + menu_height;
125 135
126 136 var ws = window_size();
127 137 window_width = ws.width;
128 138 window_height = ws.height;
129 139
130 140 /* display the menu above and/or to the left of the click if needed */
131 141 if (max_width > window_width) {
132 142 render_x -= menu_width;
133 143 $('context-menu').addClassName('reverse-x');
134 144 } else {
135 145 $('context-menu').removeClassName('reverse-x');
136 146 }
137 147 if (max_height > window_height) {
138 148 render_y -= menu_height;
139 149 $('context-menu').addClassName('reverse-y');
140 150 } else {
141 151 $('context-menu').removeClassName('reverse-y');
142 152 }
143 153 if (render_x <= 0) render_x = 1;
144 154 if (render_y <= 0) render_y = 1;
145 155 $('context-menu').style['left'] = (render_x + 'px');
146 156 $('context-menu').style['top'] = (render_y + 'px');
147 157
148 158 Effect.Appear('context-menu', {duration: 0.20});
149 159 if (window.parseStylesheets) { window.parseStylesheets(); } // IE
150 160 }})
151 161 },
152 162
153 163 hideMenu: function() {
154 164 Element.hide('context-menu');
155 165 },
156 166
157 167 addSelection: function(tr) {
158 168 tr.addClassName('context-menu-selection');
159 169 this.checkSelectionBox(tr, true);
160 170 },
161 171
162 172 toggleSelection: function(tr) {
163 173 if (this.isSelected(tr)) {
164 174 this.removeSelection(tr);
165 175 } else {
166 176 this.addSelection(tr);
167 177 }
168 178 },
169 179
170 180 removeSelection: function(tr) {
171 181 tr.removeClassName('context-menu-selection');
172 182 this.checkSelectionBox(tr, false);
173 183 },
174 184
175 185 unselectAll: function() {
176 186 var rows = $$('.hascontextmenu');
177 187 for (i=0; i<rows.length; i++) {
178 188 this.removeSelection(rows[i]);
179 189 }
180 190 },
181 191
182 192 checkSelectionBox: function(tr, checked) {
183 193 var inputs = Element.getElementsBySelector(tr, 'input');
184 194 if (inputs.length > 0) { inputs[0].checked = checked; }
185 195 },
186 196
187 197 isSelected: function(tr) {
188 198 return Element.hasClassName(tr, 'context-menu-selection');
189 199 }
190 200 }
191 201
192 202 function toggleIssuesSelection(el) {
193 203 var boxes = el.getElementsBySelector('input[type=checkbox]');
194 204 var all_checked = true;
195 205 for (i = 0; i < boxes.length; i++) { if (boxes[i].checked == false) { all_checked = false; } }
196 206 for (i = 0; i < boxes.length; i++) {
197 207 if (all_checked) {
198 208 boxes[i].checked = false;
199 209 boxes[i].up('tr').removeClassName('context-menu-selection');
200 210 } else if (boxes[i].checked == false) {
201 211 boxes[i].checked = true;
202 212 boxes[i].up('tr').addClassName('context-menu-selection');
203 213 }
204 214 }
205 215 }
206 216
207 217 function window_size() {
208 218 var w;
209 219 var h;
210 220 if (window.innerWidth) {
211 221 w = window.innerWidth;
212 222 h = window.innerHeight;
213 223 } else if (document.documentElement) {
214 224 w = document.documentElement.clientWidth;
215 225 h = document.documentElement.clientHeight;
216 226 } else {
217 227 w = document.body.clientWidth;
218 228 h = document.body.clientHeight;
219 229 }
220 230 return {width: w, height: h};
221 231 }
General Comments 0
You need to be logged in to leave comments. Login now