##// END OF EJS Templates
Added AJAX based context menu on the project issue list that provide shortcuts for editing, re-assigning, changing the status or the priority, moving or deleting an issue....
Jean-Philippe Lang -
r859:bb4acc02d06d
parent child
Show More
@@ -0,0 +1,38
1 <% back_to = url_for(:controller => 'projects', :action => 'list_issues', :id => @project) %>
2 <ul>
3 <li><%= context_menu_link l(:button_edit), {:controller => 'issues', :action => 'edit', :id => @issue},
4 :class => 'icon-edit', :disabled => !@can[:edit] %></li>
5 <li class="folder">
6 <a href="#" class="submenu" onclick="return false;"><%= l(:field_status) %></a>
7 <ul>
8 <% @statuses.each do |s| %>
9 <li><%= context_menu_link s.name, {:controller => 'issues', :action => 'change_status', :id => @issue, :new_status_id => s},
10 :selected => (s == @issue.status), :disabled => !(@can[:change_status] && @allowed_statuses.include?(s)) %></li>
11 <% end %>
12 </ul>
13 </li>
14 <li class="folder">
15 <a href="#" class="submenu"><%= l(:field_priority) %></a>
16 <ul>
17 <% @priorities.each do |p| %>
18 <li><%= context_menu_link p.name, {:controller => 'issues', :action => 'edit', :id => @issue, 'issue[priority_id]' => p, :back_to => back_to}, :method => :post,
19 :selected => (p == @issue.priority), :disabled => !@can[:edit] %></li>
20 <% end %>
21 </ul>
22 </li>
23 <li class="folder">
24 <a href="#" class="submenu"><%= l(:field_assigned_to) %></a>
25 <ul>
26 <% @assignables.each do |u| %>
27 <li><%= context_menu_link u.name, {:controller => 'issues', :action => 'edit', :id => @issue, 'issue[assigned_to_id]' => u, :back_to => back_to}, :method => :post,
28 :selected => (u == @issue.assigned_to), :disabled => !(@can[:edit] || @can[:change_status]) %></li>
29 <% end %>
30 <li><%= context_menu_link l(:label_nobody), {:controller => 'issues', :action => 'edit', :id => @issue, 'issue[assigned_to_id]' => '', :back_to => back_to}, :method => :post,
31 :selected => @issue.assigned_to.nil?, :disabled => !(@can[:edit] || @can[:change_status]) %></li>
32 </ul>
33 </li>
34 <li><%= context_menu_link l(:button_move), {:controller => 'projects', :action => 'move_issues', :id => @project, "issue_ids[]" => @issue.id },
35 :class => 'icon-move', :disabled => !@can[:move] %>
36 <li><%= context_menu_link l(:button_delete), {:controller => 'issues', :action => 'destroy', :id => @issue},
37 :method => :post, :confirm => l(:text_are_you_sure), :class => 'icon-del', :disabled => !@can[:delete] %></li>
38 </ul>
1 NO CONTENT: new file 100644, binary diff hidden
@@ -0,0 +1,44
1 ContextMenu = Class.create();
2 ContextMenu.prototype = {
3 initialize: function (options) {
4 this.options = Object.extend({selector: '.hascontextmenu'}, options || { });
5
6 Event.observe(document, 'click', function(e){
7 var t = Event.findElement(e, 'a');
8 if ((t != document) && (Element.hasClassName(t, 'disabled') || Element.hasClassName(t, 'submenu'))) {
9 Event.stop(e);
10 } else {
11 $('context-menu').hide();
12 if (this.selection) {
13 this.selection.removeClassName('context-menu-selection');
14 }
15 }
16
17 }.bind(this));
18
19 $$(this.options.selector).invoke('observe', (window.opera ? 'click' : 'contextmenu'), function(e){
20 if (window.opera && !e.ctrlKey) {
21 return;
22 }
23 this.show(e);
24 }.bind(this));
25
26 },
27 show: function(e) {
28 Event.stop(e);
29 Element.hide('context-menu');
30 if (this.selection) {
31 this.selection.removeClassName('context-menu-selection');
32 }
33 $('context-menu').style['left'] = (Event.pointerX(e) + 'px');
34 $('context-menu').style['top'] = (Event.pointerY(e) + 'px');
35 Element.update('context-menu', '');
36
37 var tr = Event.findElement(e, 'tr');
38 tr.addClassName('context-menu-selection');
39 this.selection = tr;
40 var id = tr.id.substring(6, tr.id.length);
41 /* TODO: do not hard code path */
42 new Ajax.Updater('context-menu', '../../issues/context_menu/' + id, {asynchronous:true, evalScripts:true, onComplete:function(request){Effect.Appear('context-menu', {duration: 0.20})}})
43 }
44 }
@@ -0,0 +1,52
1 #context-menu { position: absolute; }
2
3 #context-menu ul, #context-menu li, #context-menu a {
4 display:block;
5 margin:0;
6 padding:0;
7 border:0;
8 }
9
10 #context-menu ul {
11 width:150px;
12 border-top:1px solid #ddd;
13 border-left:1px solid #ddd;
14 border-bottom:1px solid #777;
15 border-right:1px solid #777;
16 background:white;
17 list-style:none;
18 }
19
20 #context-menu li {
21 position:relative;
22 padding:1px;
23 z-index:9;
24 }
25 #context-menu li.folder ul {
26 position:absolute;
27 left:128px; /* IE */
28 top:-2px;
29 }
30 #context-menu li.folder>ul { left:148px; }
31
32 #context-menu a {
33 border:1px solid white;
34 text-decoration:none;
35 background-repeat: no-repeat;
36 background-position: 1px 50%;
37 padding: 2px 0px 2px 20px;
38 width:100%; /* IE */
39 }
40 #context-menu li>a { width:auto; } /* others */
41 #context-menu a.disabled, #context-menu a.disabled:hover {color: #ccc;}
42 #context-menu li a.submenu { background:url("../images/sub.gif") right no-repeat; }
43 #context-menu a:hover { border-color:gray; background-color:#eee; color:#2A5685; }
44 #context-menu li.folder a:hover { background-color:#eee; }
45 #context-menu li.folder:hover { z-index:10; }
46 #context-menu ul ul, #context-menu li:hover ul ul { display:none; }
47 #context-menu li:hover ul, #context-menu li:hover li:hover ul { display:block; }
48
49 /* selected element */
50 .context-menu-selection { background-color:#507AAA !important; color:#f8f8f8 !important; }
51 .context-menu-selection a, .context-menu-selection a:hover { color:#f8f8f8 !important; }
52 .context-menu-selection:hover { background-color:#507AAA !important; color:#f8f8f8 !important; }
@@ -1,201 +1,216
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 class IssuesController < ApplicationController
19 19 layout 'base', :except => :export_pdf
20 20 before_filter :find_project, :authorize, :except => [:index, :preview]
21 21 accept_key_auth :index
22 22
23 23 cache_sweeper :issue_sweeper, :only => [ :edit, :change_status, :destroy ]
24 24
25 25 helper :projects
26 26 include ProjectsHelper
27 27 helper :custom_fields
28 28 include CustomFieldsHelper
29 29 helper :ifpdf
30 30 include IfpdfHelper
31 31 helper :issue_relations
32 32 include IssueRelationsHelper
33 33 helper :watchers
34 34 include WatchersHelper
35 35 helper :attachments
36 36 include AttachmentsHelper
37 37 helper :queries
38 38 helper :sort
39 39 include SortHelper
40 40
41 41 def index
42 42 sort_init "#{Issue.table_name}.id", "desc"
43 43 sort_update
44 44 retrieve_query
45 45 if @query.valid?
46 46 @issue_count = Issue.count(:include => [:status, :project], :conditions => @query.statement)
47 47 @issue_pages = Paginator.new self, @issue_count, 25, params['page']
48 48 @issues = Issue.find :all, :order => sort_clause,
49 49 :include => [ :assigned_to, :status, :tracker, :project, :priority, :category ],
50 50 :conditions => @query.statement,
51 51 :limit => @issue_pages.items_per_page,
52 52 :offset => @issue_pages.current.offset
53 53 end
54 54 respond_to do |format|
55 55 format.html { render :layout => false if request.xhr? }
56 56 format.atom { render_feed(@issues, :title => l(:label_issue_plural)) }
57 57 end
58 58 end
59 59
60 60 def show
61 61 @custom_values = @issue.custom_values.find(:all, :include => :custom_field)
62 62 @journals = @issue.journals.find(:all, :include => [:user, :details], :order => "#{Journal.table_name}.created_on ASC")
63 63
64 64 if params[:format]=='pdf'
65 65 @options_for_rfpdf ||= {}
66 66 @options_for_rfpdf[:file_name] = "#{@project.identifier}-#{@issue.id}.pdf"
67 67 render :template => 'issues/show.rfpdf', :layout => false
68 68 else
69 69 @status_options = @issue.status.find_new_statuses_allowed_to(logged_in_user.role_for_project(@project), @issue.tracker) if logged_in_user
70 70 render :template => 'issues/show.rhtml'
71 71 end
72 72 end
73 73
74 74 def edit
75 75 @priorities = Enumeration::get_values('IPRI')
76 76 if request.get?
77 77 @custom_values = @project.custom_fields_for_issues(@issue.tracker).collect { |x| @issue.custom_values.find_by_custom_field_id(x.id) || CustomValue.new(:custom_field => x, :customized => @issue) }
78 78 else
79 79 begin
80 80 @issue.init_journal(self.logged_in_user)
81 81 # Retrieve custom fields and values
82 @custom_values = @project.custom_fields_for_issues(@issue.tracker).collect { |x| CustomValue.new(:custom_field => x, :customized => @issue, :value => params["custom_fields"][x.id.to_s]) }
83 @issue.custom_values = @custom_values
82 if params["custom_fields"]
83 @custom_values = @project.custom_fields_for_issues(@issue.tracker).collect { |x| CustomValue.new(:custom_field => x, :customized => @issue, :value => params["custom_fields"][x.id.to_s]) }
84 @issue.custom_values = @custom_values
85 end
84 86 @issue.attributes = params[:issue]
85 87 if @issue.save
86 88 flash[:notice] = l(:notice_successful_update)
87 redirect_to :action => 'show', :id => @issue
89 redirect_to(params[:back_to] || {:action => 'show', :id => @issue})
88 90 end
89 91 rescue ActiveRecord::StaleObjectError
90 92 # Optimistic locking exception
91 93 flash[:error] = l(:notice_locking_conflict)
92 94 end
93 95 end
94 96 end
95 97
96 98 def add_note
97 99 journal = @issue.init_journal(User.current, params[:notes])
98 100 params[:attachments].each { |file|
99 101 next unless file.size > 0
100 102 a = Attachment.create(:container => @issue, :file => file, :author => logged_in_user)
101 103 journal.details << JournalDetail.new(:property => 'attachment',
102 104 :prop_key => a.id,
103 105 :value => a.filename) unless a.new_record?
104 106 } if params[:attachments] and params[:attachments].is_a? Array
105 107 if journal.save
106 108 flash[:notice] = l(:notice_successful_update)
107 109 Mailer.deliver_issue_edit(journal) if Setting.notified_events.include?('issue_updated')
108 110 redirect_to :action => 'show', :id => @issue
109 111 return
110 112 end
111 113 show
112 114 end
113 115
114 116 def change_status
115 117 @status_options = @issue.status.find_new_statuses_allowed_to(logged_in_user.role_for_project(@project), @issue.tracker) if logged_in_user
116 118 @new_status = IssueStatus.find(params[:new_status_id])
117 119 if params[:confirm]
118 120 begin
119 121 journal = @issue.init_journal(self.logged_in_user, params[:notes])
120 122 @issue.status = @new_status
121 123 if @issue.update_attributes(params[:issue])
122 124 # Save attachments
123 125 params[:attachments].each { |file|
124 126 next unless file.size > 0
125 127 a = Attachment.create(:container => @issue, :file => file, :author => logged_in_user)
126 128 journal.details << JournalDetail.new(:property => 'attachment',
127 129 :prop_key => a.id,
128 130 :value => a.filename) unless a.new_record?
129 131 } if params[:attachments] and params[:attachments].is_a? Array
130 132
131 133 # Log time
132 134 if current_role.allowed_to?(:log_time)
133 135 @time_entry ||= TimeEntry.new(:project => @project, :issue => @issue, :user => logged_in_user, :spent_on => Date.today)
134 136 @time_entry.attributes = params[:time_entry]
135 137 @time_entry.save
136 138 end
137 139
138 140 flash[:notice] = l(:notice_successful_update)
139 141 Mailer.deliver_issue_edit(journal) if Setting.notified_events.include?('issue_updated')
140 142 redirect_to :action => 'show', :id => @issue
141 143 end
142 144 rescue ActiveRecord::StaleObjectError
143 145 # Optimistic locking exception
144 146 flash[:error] = l(:notice_locking_conflict)
145 147 end
146 148 end
147 149 @assignable_to = @project.members.find(:all, :include => :user).collect{ |m| m.user }
148 150 @activities = Enumeration::get_values('ACTI')
149 151 end
150 152
151 153 def destroy
152 154 @issue.destroy
153 155 redirect_to :controller => 'projects', :action => 'list_issues', :id => @project
154 156 end
155 157
156 158 def destroy_attachment
157 159 a = @issue.attachments.find(params[:attachment_id])
158 160 a.destroy
159 161 journal = @issue.init_journal(self.logged_in_user)
160 162 journal.details << JournalDetail.new(:property => 'attachment',
161 163 :prop_key => a.id,
162 164 :old_value => a.filename)
163 165 journal.save
164 166 redirect_to :action => 'show', :id => @issue
165 167 end
168
169 def context_menu
170 @priorities = Enumeration.get_values('IPRI').reverse
171 @statuses = IssueStatus.find(:all, :order => 'position')
172 @allowed_statuses = @issue.status.find_new_statuses_allowed_to(User.current.role_for_project(@project), @issue.tracker)
173 @assignables = @issue.assignable_users
174 @assignables << @issue.assigned_to if @issue.assigned_to && !@assignables.include?(@issue.assigned_to)
175 @can = {:edit => User.current.allowed_to?(:edit_issues, @project),
176 :change_status => User.current.allowed_to?(:change_issue_status, @project),
177 :move => User.current.allowed_to?(:move_issues, @project),
178 :delete => User.current.allowed_to?(:delete_issues, @project)}
179 render :layout => false
180 end
166 181
167 182 def preview
168 183 issue = Issue.find_by_id(params[:id])
169 184 @attachements = issue.attachments if issue
170 185 @text = params[:issue][:description]
171 186 render :partial => 'common/preview'
172 187 end
173 188
174 189 private
175 190 def find_project
176 191 @issue = Issue.find(params[:id], :include => [:project, :tracker, :status, :author, :priority, :category])
177 192 @project = @issue.project
178 193 rescue ActiveRecord::RecordNotFound
179 194 render_404
180 195 end
181 196
182 197 # Retrieve query from session or build a new query
183 198 def retrieve_query
184 199 if params[:set_filter] or !session[:query] or session[:query].project_id
185 200 # Give it a name, required to be valid
186 201 @query = Query.new(:name => "_", :executed_by => logged_in_user)
187 202 if params[:fields] and params[:fields].is_a? Array
188 203 params[:fields].each do |field|
189 204 @query.add_filter(field, params[:operators][field], params[:values][field])
190 205 end
191 206 else
192 207 @query.available_filters.keys.each do |field|
193 208 @query.add_short_filter(field, params[field]) if params[field]
194 209 end
195 210 end
196 211 session[:query] = @query
197 212 else
198 213 @query = session[:query]
199 214 end
200 215 end
201 216 end
@@ -1,353 +1,369
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 module ApplicationHelper
19 19
20 20 def current_role
21 21 @current_role ||= User.current.role_for_project(@project)
22 22 end
23 23
24 24 # Return true if user is authorized for controller/action, otherwise false
25 25 def authorize_for(controller, action)
26 26 User.current.allowed_to?({:controller => controller, :action => action}, @project)
27 27 end
28 28
29 29 # Display a link if user is authorized
30 30 def link_to_if_authorized(name, options = {}, html_options = nil, *parameters_for_method_reference)
31 31 link_to(name, options, html_options, *parameters_for_method_reference) if authorize_for(options[:controller] || params[:controller], options[:action])
32 32 end
33 33
34 34 # Display a link to user's account page
35 35 def link_to_user(user)
36 36 link_to user.name, :controller => 'account', :action => 'show', :id => user
37 37 end
38 38
39 39 def link_to_issue(issue)
40 40 link_to "#{issue.tracker.name} ##{issue.id}", :controller => "issues", :action => "show", :id => issue
41 41 end
42 42
43 43 def toggle_link(name, id, options={})
44 44 onclick = "Element.toggle('#{id}'); "
45 45 onclick << (options[:focus] ? "Form.Element.focus('#{options[:focus]}'); " : "this.blur(); ")
46 46 onclick << "return false;"
47 47 link_to(name, "#", :onclick => onclick)
48 48 end
49 49
50 50 def show_and_goto_link(name, id, options={})
51 51 onclick = "Element.show('#{id}'); "
52 52 onclick << (options[:focus] ? "Form.Element.focus('#{options[:focus]}'); " : "this.blur(); ")
53 53 onclick << "location.href='##{id}-anchor'; "
54 54 onclick << "return false;"
55 55 link_to(name, "#", options.merge(:onclick => onclick))
56 56 end
57 57
58 58 def image_to_function(name, function, html_options = {})
59 59 html_options.symbolize_keys!
60 60 tag(:input, html_options.merge({
61 61 :type => "image", :src => image_path(name),
62 62 :onclick => (html_options[:onclick] ? "#{html_options[:onclick]}; " : "") + "#{function};"
63 63 }))
64 64 end
65 65
66 66 def prompt_to_remote(name, text, param, url, html_options = {})
67 67 html_options[:onclick] = "promptToRemote('#{text}', '#{param}', '#{url_for(url)}'); return false;"
68 68 link_to name, {}, html_options
69 69 end
70 70
71 71 def format_date(date)
72 72 return nil unless date
73 73 @date_format ||= (Setting.date_format.to_i == 0 ? l(:general_fmt_date) : "%Y-%m-%d")
74 74 date.strftime(@date_format)
75 75 end
76 76
77 77 def format_time(time)
78 78 return nil unless time
79 79 @date_format_setting ||= Setting.date_format.to_i
80 80 time = time.to_time if time.is_a?(String)
81 81 @date_format_setting == 0 ? l_datetime(time) : (time.strftime("%Y-%m-%d") + ' ' + l_time(time))
82 82 end
83 83
84 84 def authoring(created, author)
85 85 time_tag = content_tag('acronym', distance_of_time_in_words(Time.now, created), :title => format_time(created))
86 86 l(:label_added_time_by, author.name, time_tag)
87 87 end
88 88
89 89 def day_name(day)
90 90 l(:general_day_names).split(',')[day-1]
91 91 end
92 92
93 93 def month_name(month)
94 94 l(:actionview_datehelper_select_month_names).split(',')[month-1]
95 95 end
96 96
97 97 def pagination_links_full(paginator, options={}, html_options={})
98 98 page_param = options.delete(:page_param) || :page
99 99
100 100 html = ''
101 101 html << link_to_remote(('&#171; ' + l(:label_previous)),
102 102 {:update => "content", :url => options.merge(page_param => paginator.current.previous)},
103 103 {:href => url_for(:params => options.merge(page_param => paginator.current.previous))}) + ' ' if paginator.current.previous
104 104
105 105 html << (pagination_links_each(paginator, options) do |n|
106 106 link_to_remote(n.to_s,
107 107 {:url => {:params => options.merge(page_param => n)}, :update => 'content'},
108 108 {:href => url_for(:params => options.merge(page_param => n))})
109 109 end || '')
110 110
111 111 html << ' ' + link_to_remote((l(:label_next) + ' &#187;'),
112 112 {:update => "content", :url => options.merge(page_param => paginator.current.next)},
113 113 {:href => url_for(:params => options.merge(page_param => paginator.current.next))}) if paginator.current.next
114 114 html
115 115 end
116 116
117 117 def set_html_title(text)
118 118 @html_header_title = text
119 119 end
120 120
121 121 def html_title
122 122 title = []
123 123 title << @project.name if @project
124 124 title << @html_header_title
125 125 title << Setting.app_title
126 126 title.compact.join(' - ')
127 127 end
128 128
129 129 ACCESSKEYS = {:edit => 'e',
130 130 :preview => 'r',
131 131 :quick_search => 'f',
132 132 :search => '4',
133 133 }.freeze
134 134
135 135 def accesskey(s)
136 136 ACCESSKEYS[s]
137 137 end
138 138
139 139 # format text according to system settings
140 140 def textilizable(text, options = {})
141 141 return "" if text.blank?
142 142
143 143 # when using an image link, try to use an attachment, if possible
144 144 attachments = options[:attachments]
145 145 if attachments
146 146 text = text.gsub(/!([<>=]*)(\S+\.(gif|jpg|jpeg|png))!/) do |m|
147 147 align = $1
148 148 filename = $2
149 149 rf = Regexp.new(filename, Regexp::IGNORECASE)
150 150 # search for the picture in attachments
151 151 if found = attachments.detect { |att| att.filename =~ rf }
152 152 image_url = url_for :controller => 'attachments', :action => 'download', :id => found.id
153 153 "!#{align}#{image_url}!"
154 154 else
155 155 "!#{align}#{filename}!"
156 156 end
157 157 end
158 158 end
159 159
160 160 text = (Setting.text_formatting == 'textile') ?
161 161 Redmine::WikiFormatting.to_html(text) : simple_format(auto_link(h(text)))
162 162
163 163 # different methods for formatting wiki links
164 164 case options[:wiki_links]
165 165 when :local
166 166 # used for local links to html files
167 167 format_wiki_link = Proc.new {|project, title| "#{title}.html" }
168 168 when :anchor
169 169 # used for single-file wiki export
170 170 format_wiki_link = Proc.new {|project, title| "##{title}" }
171 171 else
172 172 format_wiki_link = Proc.new {|project, title| url_for :controller => 'wiki', :action => 'index', :id => project, :page => title }
173 173 end
174 174
175 175 project = options[:project] || @project
176 176
177 177 # turn wiki links into html links
178 178 # example:
179 179 # [[mypage]]
180 180 # [[mypage|mytext]]
181 181 # wiki links can refer other project wikis, using project name or identifier:
182 182 # [[project:]] -> wiki starting page
183 183 # [[project:|mytext]]
184 184 # [[project:mypage]]
185 185 # [[project:mypage|mytext]]
186 186 text = text.gsub(/\[\[([^\]\|]+)(\|([^\]\|]+))?\]\]/) do |m|
187 187 link_project = project
188 188 page = $1
189 189 title = $3
190 190 if page =~ /^([^\:]+)\:(.*)$/
191 191 link_project = Project.find_by_name($1) || Project.find_by_identifier($1)
192 192 page = title || $2
193 193 title = $1 if page.blank?
194 194 end
195 195
196 196 if link_project && link_project.wiki
197 197 # check if page exists
198 198 wiki_page = link_project.wiki.find_page(page)
199 199 link_to((title || page), format_wiki_link.call(link_project, Wiki.titleize(page)),
200 200 :class => ('wiki-page' + (wiki_page ? '' : ' new')))
201 201 else
202 202 # project or wiki doesn't exist
203 203 title || page
204 204 end
205 205 end
206 206
207 207 # turn issue and revision ids into links
208 208 # example:
209 209 # #52 -> <a href="/issues/show/52">#52</a>
210 210 # r52 -> <a href="/repositories/revision/6?rev=52">r52</a> (project.id is 6)
211 211 text = text.gsub(%r{([\s,-^])(#|r)(\d+)(?=[[:punct:]]|\s|<|$)}) do |m|
212 212 leading, otype, oid = $1, $2, $3
213 213 link = nil
214 214 if otype == 'r'
215 215 if project && (changeset = project.changesets.find_by_revision(oid))
216 216 link = link_to("r#{oid}", {:controller => 'repositories', :action => 'revision', :id => project.id, :rev => oid}, :class => 'changeset',
217 217 :title => truncate(changeset.comments, 100))
218 218 end
219 219 else
220 220 if issue = Issue.find_by_id(oid.to_i, :include => [:project, :status], :conditions => Project.visible_by(User.current))
221 221 link = link_to("##{oid}", {:controller => 'issues', :action => 'show', :id => oid}, :class => 'issue',
222 222 :title => "#{truncate(issue.subject, 100)} (#{issue.status.name})")
223 223 link = content_tag('del', link) if issue.closed?
224 224 end
225 225 end
226 226 leading + (link || "#{otype}#{oid}")
227 227 end
228 228
229 229 text
230 230 end
231 231
232 232 # Same as Rails' simple_format helper without using paragraphs
233 233 def simple_format_without_paragraph(text)
234 234 text.to_s.
235 235 gsub(/\r\n?/, "\n"). # \r\n and \r -> \n
236 236 gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br
237 237 gsub(/([^\n]\n)(?=[^\n])/, '\1<br />') # 1 newline -> br
238 238 end
239 239
240 240 def error_messages_for(object_name, options = {})
241 241 options = options.symbolize_keys
242 242 object = instance_variable_get("@#{object_name}")
243 243 if object && !object.errors.empty?
244 244 # build full_messages here with controller current language
245 245 full_messages = []
246 246 object.errors.each do |attr, msg|
247 247 next if msg.nil?
248 248 msg = msg.first if msg.is_a? Array
249 249 if attr == "base"
250 250 full_messages << l(msg)
251 251 else
252 252 full_messages << "&#171; " + (l_has_string?("field_" + attr) ? l("field_" + attr) : object.class.human_attribute_name(attr)) + " &#187; " + l(msg) unless attr == "custom_values"
253 253 end
254 254 end
255 255 # retrieve custom values error messages
256 256 if object.errors[:custom_values]
257 257 object.custom_values.each do |v|
258 258 v.errors.each do |attr, msg|
259 259 next if msg.nil?
260 260 msg = msg.first if msg.is_a? Array
261 261 full_messages << "&#171; " + v.custom_field.name + " &#187; " + l(msg)
262 262 end
263 263 end
264 264 end
265 265 content_tag("div",
266 266 content_tag(
267 267 options[:header_tag] || "span", lwr(:gui_validation_error, full_messages.length) + ":"
268 268 ) +
269 269 content_tag("ul", full_messages.collect { |msg| content_tag("li", msg) }),
270 270 "id" => options[:id] || "errorExplanation", "class" => options[:class] || "errorExplanation"
271 271 )
272 272 else
273 273 ""
274 274 end
275 275 end
276 276
277 277 def lang_options_for_select(blank=true)
278 278 (blank ? [["(auto)", ""]] : []) +
279 279 GLoc.valid_languages.collect{|lang| [ ll(lang.to_s, :general_lang_name), lang.to_s]}.sort{|x,y| x.first <=> y.first }
280 280 end
281 281
282 282 def label_tag_for(name, option_tags = nil, options = {})
283 283 label_text = l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) + (options.delete(:required) ? @template.content_tag("span", " *", :class => "required"): "")
284 284 content_tag("label", label_text)
285 285 end
286 286
287 287 def labelled_tabular_form_for(name, object, options, &proc)
288 288 options[:html] ||= {}
289 289 options[:html].store :class, "tabular"
290 290 form_for(name, object, options.merge({ :builder => TabularFormBuilder, :lang => current_language}), &proc)
291 291 end
292 292
293 293 def check_all_links(form_name)
294 294 link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
295 295 " | " +
296 296 link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
297 297 end
298 298
299 def context_menu_link(name, url, options={})
300 options[:class] ||= ''
301 if options.delete(:selected)
302 options[:class] << ' icon-checked disabled'
303 options[:disabled] = true
304 end
305 if options.delete(:disabled)
306 options.delete(:method)
307 options.delete(:confirm)
308 options.delete(:onclick)
309 options[:class] << ' disabled'
310 url = '#'
311 end
312 link_to name, url, options
313 end
314
299 315 def calendar_for(field_id)
300 316 image_tag("calendar.png", {:id => "#{field_id}_trigger",:class => "calendar-trigger"}) +
301 317 javascript_tag("Calendar.setup({inputField : '#{field_id}', ifFormat : '%Y-%m-%d', button : '#{field_id}_trigger' });")
302 318 end
303 319
304 320 def wikitoolbar_for(field_id)
305 321 return '' unless Setting.text_formatting == 'textile'
306 322 javascript_include_tag('jstoolbar') + javascript_tag("var toolbar = new jsToolBar($('#{field_id}')); toolbar.draw();")
307 323 end
308 324
309 325 def content_for(name, content = nil, &block)
310 326 @has_content ||= {}
311 327 @has_content[name] = true
312 328 super(name, content, &block)
313 329 end
314 330
315 331 def has_content?(name)
316 332 (@has_content && @has_content[name]) || false
317 333 end
318 334 end
319 335
320 336 class TabularFormBuilder < ActionView::Helpers::FormBuilder
321 337 include GLoc
322 338
323 339 def initialize(object_name, object, template, options, proc)
324 340 set_language_if_valid options.delete(:lang)
325 341 @object_name, @object, @template, @options, @proc = object_name, object, template, options, proc
326 342 end
327 343
328 344 (field_helpers - %w(radio_button hidden_field) + %w(date_select)).each do |selector|
329 345 src = <<-END_SRC
330 346 def #{selector}(field, options = {})
331 347 return super if options.delete :no_label
332 348 label_text = l(options[:label]) if options[:label]
333 349 label_text ||= l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym)
334 350 label_text << @template.content_tag("span", " *", :class => "required") if options.delete(:required)
335 351 label = @template.content_tag("label", label_text,
336 352 :class => (@object && @object.errors[field] ? "error" : nil),
337 353 :for => (@object_name.to_s + "_" + field.to_s))
338 354 label + super
339 355 end
340 356 END_SRC
341 357 class_eval src, __FILE__, __LINE__
342 358 end
343 359
344 360 def select(field, choices, options = {}, html_options = {})
345 361 label_text = l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) + (options.delete(:required) ? @template.content_tag("span", " *", :class => "required"): "")
346 362 label = @template.content_tag("label", label_text,
347 363 :class => (@object && @object.errors[field] ? "error" : nil),
348 364 :for => (@object_name.to_s + "_" + field.to_s))
349 365 label + super
350 366 end
351 367
352 368 end
353 369
@@ -1,25 +1,25
1 1 <div id="bulk-edit"></div>
2 2 <table class="list">
3 3 <thead><tr>
4 4 <th><%= link_to_remote(image_tag('edit.png'),
5 5 {:url => { :controller => 'projects', :action => 'bulk_edit_issues', :id => @project },
6 6 :method => :get},
7 7 {:title => l(:label_bulk_edit_selected_issues)}) if @project && User.current.allowed_to?(:edit_issues, @project) %>
8 8 </th>
9 9 <%= sort_header_tag("#{Issue.table_name}.id", :caption => '#') %>
10 10 <% query.columns.each do |column| %>
11 11 <%= column_header(column) %>
12 12 <% end %>
13 13 </tr></thead>
14 14 <tbody>
15 15 <% issues.each do |issue| %>
16 <tr class="issue <%= cycle('odd', 'even') %> <%= "status-#{issue.status.position} priority-#{issue.priority.position}" %>">
16 <tr id="issue-<%= issue.id %>" class="issue hascontextmenu <%= cycle('odd', 'even') %> <%= "status-#{issue.status.position} priority-#{issue.priority.position}" %>">
17 17 <td class="checkbox"><%= check_box_tag("issue_ids[]", issue.id, false, :id => "issue_#{issue.id}", :disabled => (!@project || @project != issue.project)) %></td>
18 18 <td><%= link_to issue.id, :controller => 'issues', :action => 'show', :id => issue %></td>
19 19 <% query.columns.each do |column| %>
20 20 <%= content_tag 'td', column_content(column, issue), :class => column.name %>
21 21 <% end %>
22 22 </tr>
23 23 <% end %>
24 24 </tbody>
25 25 </table>
@@ -1,67 +1,72
1 1 <% if @query.new_record? %>
2 2 <h2><%=l(:label_issue_plural)%></h2>
3 3 <% set_html_title l(:label_issue_plural) %>
4 4
5 5 <% form_tag({ :controller => 'queries', :action => 'new', :project_id => @project }, :id => 'query_form') do %>
6 6 <%= render :partial => 'queries/filters', :locals => {:query => @query} %>
7 7 <div class="contextual">
8 8 <%= link_to_remote l(:button_apply),
9 9 { :url => { :controller => 'projects', :action => 'list_issues', :id => @project, :set_filter => 1 },
10 10 :update => "content",
11 11 :with => "Form.serialize('query_form')"
12 12 }, :class => 'icon icon-edit' %>
13 13
14 14 <%= link_to_remote l(:button_clear),
15 15 { :url => {:controller => 'projects', :action => 'list_issues', :id => @project, :set_filter => 1},
16 16 :update => "content",
17 17 }, :class => 'icon icon-reload' %>
18 18
19 19 <% if current_role.allowed_to?(:save_queries) %>
20 20 <%= link_to l(:button_save), {}, :onclick => "$('query_form').submit(); return false;", :class => 'icon icon-save' %>
21 21 <% end %>
22 22 </div>
23 23 <br />
24 24 &nbsp;
25 25 <% end %>
26 26 <% else %>
27 27 <div class="contextual">
28 28 <% if @query.editable_by?(User.current) %>
29 29 <%= link_to l(:button_edit), {:controller => 'queries', :action => 'edit', :id => @query}, :class => 'icon icon-edit' %>
30 30 <%= link_to l(:button_delete), {:controller => 'queries', :action => 'destroy', :id => @query}, :confirm => l(:text_are_you_sure), :method => :post, :class => 'icon icon-del' %>
31 31 <% end %>
32 32 </div>
33 33
34 34 <h2><%= @query.name %></h2>
35 35 <div id="query_form"></div>
36 36 <% set_html_title @query.name %>
37 37 <% end %>
38 38 <%= error_messages_for 'query' %>
39 39 <% if @query.valid? %>
40 40 <% if @issues.empty? %>
41 41 <p class="nodata"><%= l(:label_no_data) %></p>
42 42 <% else %>
43 43 <% form_tag({:controller => 'projects', :action => 'bulk_edit_issues', :id => @project}, :id => 'issues_form', :onsubmit => "if (!checkBulkEdit(this)) {alert('#{l(:notice_no_issue_selected)}'); return false;}" ) do %>
44 44 <%= render :partial => 'issues/list', :locals => {:issues => @issues, :query => @query} %>
45 45 <div class="contextual">
46 46 <%= l(:label_export_to) %>
47 47 <%= link_to 'CSV', {:action => 'export_issues_csv', :id => @project}, :class => 'icon icon-csv' %>,
48 48 <%= link_to 'PDF', {:action => 'export_issues_pdf', :id => @project}, :class => 'icon icon-pdf' %>
49 49 </div>
50 50 <p>
51 51 <%= pagination_links_full @issue_pages %>
52 52 [ <%= @issue_pages.current.first_item %> - <%= @issue_pages.current.last_item %> / <%= @issue_count %> ]
53 53 </p>
54 54 <% end %>
55 55 <% end %>
56 56 <% end %>
57 57
58 58 <% content_for :sidebar do %>
59 59 <%= render :partial => 'issues/sidebar' %>
60 60 <% end %>
61 61
62 62 <% content_for :header_tags do %>
63 63 <%= javascript_include_tag 'calendar/calendar' %>
64 64 <%= javascript_include_tag "calendar/lang/calendar-#{current_language}.js" %>
65 65 <%= javascript_include_tag 'calendar/calendar-setup' %>
66 66 <%= stylesheet_link_tag 'calendar' %>
67 <%= javascript_include_tag 'context_menu' %>
68 <%= stylesheet_link_tag 'context_menu' %>
67 69 <% end %>
70
71 <div id="context-menu" style="display: none;"></div>
72 <%= javascript_tag 'new ContextMenu({})' %>
@@ -1,103 +1,103
1 1 require 'redmine/access_control'
2 2 require 'redmine/menu_manager'
3 3 require 'redmine/mime_type'
4 4 require 'redmine/themes'
5 5 require 'redmine/plugin'
6 6
7 7 begin
8 8 require_library_or_gem 'RMagick' unless Object.const_defined?(:Magick)
9 9 rescue LoadError
10 10 # RMagick is not available
11 11 end
12 12
13 13 REDMINE_SUPPORTED_SCM = %w( Subversion Darcs Mercurial Cvs )
14 14
15 15 # Permissions
16 16 Redmine::AccessControl.map do |map|
17 17 map.permission :view_project, {:projects => [:show, :activity]}, :public => true
18 18 map.permission :search_project, {:search => :index}, :public => true
19 19 map.permission :edit_project, {:projects => [:settings, :edit]}, :require => :member
20 20 map.permission :select_project_modules, {:projects => :modules}, :require => :member
21 21 map.permission :manage_members, {:projects => :settings, :members => [:new, :edit, :destroy]}, :require => :member
22 22 map.permission :manage_versions, {:projects => [:settings, :add_version], :versions => [:edit, :destroy]}, :require => :member
23 23
24 24 map.project_module :issue_tracking do |map|
25 25 # Issue categories
26 26 map.permission :manage_categories, {:projects => [:settings, :add_issue_category], :issue_categories => [:edit, :destroy]}, :require => :member
27 27 # Issues
28 28 map.permission :view_issues, {:projects => [:list_issues, :export_issues_csv, :export_issues_pdf, :changelog, :roadmap],
29 :issues => :show,
29 :issues => [:show, :context_menu],
30 30 :queries => :index,
31 31 :reports => :issue_report}, :public => true
32 32 map.permission :add_issues, {:projects => :add_issue}, :require => :loggedin
33 33 map.permission :edit_issues, {:projects => :bulk_edit_issues,
34 34 :issues => [:edit, :destroy_attachment]}, :require => :loggedin
35 35 map.permission :manage_issue_relations, {:issue_relations => [:new, :destroy]}, :require => :loggedin
36 36 map.permission :add_issue_notes, {:issues => :add_note}, :require => :loggedin
37 37 map.permission :change_issue_status, {:issues => :change_status}, :require => :loggedin
38 38 map.permission :move_issues, {:projects => :move_issues}, :require => :loggedin
39 39 map.permission :delete_issues, {:issues => :destroy}, :require => :member
40 40 # Queries
41 41 map.permission :manage_public_queries, {:queries => [:new, :edit, :destroy]}, :require => :member
42 42 map.permission :save_queries, {:queries => [:new, :edit, :destroy]}, :require => :loggedin
43 43 # Gantt & calendar
44 44 map.permission :view_gantt, :projects => :gantt
45 45 map.permission :view_calendar, :projects => :calendar
46 46 end
47 47
48 48 map.project_module :time_tracking do |map|
49 49 map.permission :log_time, {:timelog => :edit}, :require => :loggedin
50 50 map.permission :view_time_entries, :timelog => [:details, :report]
51 51 end
52 52
53 53 map.project_module :news do |map|
54 54 map.permission :manage_news, {:projects => :add_news, :news => [:edit, :destroy, :destroy_comment]}, :require => :member
55 55 map.permission :view_news, {:projects => :list_news, :news => :show}, :public => true
56 56 map.permission :comment_news, {:news => :add_comment}, :require => :loggedin
57 57 end
58 58
59 59 map.project_module :documents do |map|
60 60 map.permission :manage_documents, {:projects => :add_document, :documents => [:edit, :destroy, :add_attachment, :destroy_attachment]}, :require => :loggedin
61 61 map.permission :view_documents, :projects => :list_documents, :documents => [:show, :download]
62 62 end
63 63
64 64 map.project_module :files do |map|
65 65 map.permission :manage_files, {:projects => :add_file, :versions => :destroy_file}, :require => :loggedin
66 66 map.permission :view_files, :projects => :list_files, :versions => :download
67 67 end
68 68
69 69 map.project_module :wiki do |map|
70 70 map.permission :manage_wiki, {:wikis => [:edit, :destroy]}, :require => :member
71 71 map.permission :rename_wiki_pages, {:wiki => :rename}, :require => :member
72 72 map.permission :delete_wiki_pages, {:wiki => :destroy}, :require => :member
73 73 map.permission :view_wiki_pages, :wiki => [:index, :history, :diff, :special]
74 74 map.permission :edit_wiki_pages, :wiki => [:edit, :preview, :add_attachment, :destroy_attachment]
75 75 end
76 76
77 77 map.project_module :repository do |map|
78 78 map.permission :manage_repository, {:repositories => [:edit, :destroy]}, :require => :member
79 79 map.permission :browse_repository, :repositories => [:show, :browse, :entry, :changes, :diff, :stats, :graph]
80 80 map.permission :view_changesets, :repositories => [:show, :revisions, :revision]
81 81 end
82 82
83 83 map.project_module :boards do |map|
84 84 map.permission :manage_boards, {:boards => [:new, :edit, :destroy]}, :require => :member
85 85 map.permission :view_messages, {:boards => [:index, :show], :messages => [:show]}, :public => true
86 86 map.permission :add_messages, {:messages => [:new, :reply]}, :require => :loggedin
87 87 end
88 88 end
89 89
90 90 # Project menu configuration
91 91 Redmine::MenuManager.map :project_menu do |menu|
92 92 menu.push :label_overview, :controller => 'projects', :action => 'show'
93 93 menu.push :label_activity, :controller => 'projects', :action => 'activity'
94 94 menu.push :label_roadmap, :controller => 'projects', :action => 'roadmap'
95 95 menu.push :label_issue_plural, :controller => 'projects', :action => 'list_issues'
96 96 menu.push :label_news_plural, :controller => 'projects', :action => 'list_news'
97 97 menu.push :label_document_plural, :controller => 'projects', :action => 'list_documents'
98 98 menu.push :label_wiki, { :controller => 'wiki', :action => 'index', :page => nil }, :if => Proc.new { |p| p.wiki && !p.wiki.new_record? }
99 99 menu.push :label_board_plural, { :controller => 'boards', :action => 'index', :id => nil }, :param => :project_id, :if => Proc.new { |p| p.boards.any? }
100 100 menu.push :label_attachment_plural, :controller => 'projects', :action => 'list_files'
101 101 menu.push :label_repository, { :controller => 'repositories', :action => 'show' }, :if => Proc.new { |p| p.repository && !p.repository.new_record? }
102 102 menu.push :label_settings, :controller => 'projects', :action => 'settings'
103 103 end
@@ -1,477 +1,478
1 1 body { font-family: Verdana, sans-serif; font-size: 12px; color:#484848; margin: 0; padding: 0; min-width: 900px; }
2 2
3 3 h1, h2, h3, h4 { font-family: "Trebuchet MS", Verdana, sans-serif;}
4 4 h1 {margin:0; padding:0; font-size: 24px;}
5 5 h2, .wiki h1 {font-size: 20px;padding: 2px 10px 1px 0px;margin: 0 0 10px 0; border-bottom: 1px solid #bbbbbb; color: #444;}
6 6 h3, .wiki h2 {font-size: 16px;padding: 2px 10px 1px 0px;margin: 0 0 10px 0; border-bottom: 1px solid #bbbbbb; color: #444;}
7 7 h4, .wiki h3 {font-size: 12px;padding: 2px 10px 1px 0px;margin-bottom: 5px; border-bottom: 1px dotted #bbbbbb; color: #444;}
8 8
9 9 /***** Layout *****/
10 10 #top-menu {background: #2C4056;color: #fff;height:1.5em; padding: 2px 6px 0px 6px;}
11 11 #top-menu a {color: #fff; padding-right: 4px;}
12 12 #account {float:right;}
13 13
14 14 #header {height:5.3em;margin:0;background-color:#507AAA;color:#f8f8f8; padding: 4px 8px 0px 6px; position:relative;}
15 15 #header a {color:#f8f8f8;}
16 16 #quick-search {float:right;}
17 17
18 18 #main-menu {position: absolute; bottom: 0px; left:6px;}
19 19 #main-menu ul {margin: 0; padding: 0;}
20 20 #main-menu li {
21 21 float:left;
22 22 list-style-type:none;
23 23 margin: 0px 10px 0px 0px;
24 24 padding: 0px 0px 0px 0px;
25 25 white-space:nowrap;
26 26 }
27 27 #main-menu li a {
28 28 display: block;
29 29 color: #fff;
30 30 text-decoration: none;
31 31 margin: 0;
32 32 padding: 4px 4px 4px 4px;
33 33 background: #2C4056;
34 34 }
35 35 #main-menu li a:hover {background:#759FCF;}
36 36
37 37 #main {background: url(../images/mainbg.png) repeat-x; background-color:#EEEEEE;}
38 38
39 39 #sidebar{ float: right; width: 17%; position: relative; z-index: 9; min-height: 600px; padding: 0; margin: 0;}
40 40 * html #sidebar{ width: 17%; }
41 41 #sidebar h3{ font-size: 14px; margin-top:14px; color: #666; }
42 42 #sidebar hr{ width: 100%; margin: 0 auto; height: 1px; background: #ccc; border: 0; }
43 43 * html #sidebar hr{ width: 95%; position: relative; left: -6px; color: #ccc; }
44 44
45 #content { width: 80%; background: url(../images/contentbg.png) repeat-x; background-color: #fff; margin: 0px; border-right: 1px solid #ddd; padding: 6px 10px 10px 10px; position: relative; z-index: 10; height:600px; min-height: 600px;}
45 #content { width: 80%; background: url(../images/contentbg.png) repeat-x; background-color: #fff; margin: 0px; border-right: 1px solid #ddd; padding: 6px 10px 10px 10px; z-index: 10; height:600px; min-height: 600px;}
46 46 * html #content{ width: 80%; padding-left: 0; margin-top: 0px; padding: 6px 10px 10px 10px;}
47 47 html>body #content {
48 48 height: auto;
49 49 min-height: 600px;
50 50 }
51 51
52 52 #main.nosidebar #sidebar{ display: none; }
53 53 #main.nosidebar #content{ width: auto; border-right: 0; }
54 54
55 55 #footer {clear: both; border-top: 1px solid #bbb; font-size: 0.9em; color: #aaa; padding: 5px; text-align:center; background:#fff;}
56 56
57 57 #login-form table {margin-top:5em; padding:1em; margin-left: auto; margin-right: auto; border: 2px solid #FDBF3B; background-color:#FFEBC1; }
58 58 #login-form table td {padding: 6px;}
59 59 #login-form label {font-weight: bold;}
60 60
61 61 .clear:after{ content: "."; display: block; height: 0; clear: both; visibility: hidden; }
62 62
63 63 /***** Links *****/
64 64 a, a:link, a:visited{ color: #2A5685; text-decoration: none; }
65 65 a:hover, a:active{ color: #c61a1a; text-decoration: underline;}
66 66 a img{ border: 0; }
67 67
68 68 /***** Tables *****/
69 69 table.list { border: 1px solid #e4e4e4; border-collapse: collapse; width: 100%; margin-bottom: 4px; }
70 70 table.list th { background-color:#EEEEEE; padding: 4px; white-space:nowrap; }
71 71 table.list td { overflow: hidden; text-overflow: ellipsis; vertical-align: top;}
72 72 table.list td.id { width: 2%; text-align: center;}
73 73 table.list td.checkbox { width: 15px; padding: 0px;}
74 74
75 75 tr.issue { text-align: center; white-space: nowrap; }
76 76 tr.issue td.subject, tr.issue td.category { white-space: normal; }
77 77 tr.issue td.subject { text-align: left; }
78 78
79 79 table.list tbody tr:hover { background-color:#ffffdd; }
80 80 table td {padding:2px;}
81 81 table p {margin:0;}
82 82 .odd {background-color:#f6f7f8;}
83 83 .even {background-color: #fff;}
84 84
85 85 .highlight { background-color: #FCFD8D;}
86 86 .highlight.token-1 { background-color: #faa;}
87 87 .highlight.token-2 { background-color: #afa;}
88 88 .highlight.token-3 { background-color: #aaf;}
89 89
90 90 .box{
91 91 padding:6px;
92 92 margin-bottom: 10px;
93 93 background-color:#f6f6f6;
94 94 color:#505050;
95 95 line-height:1.5em;
96 96 border: 1px solid #e4e4e4;
97 97 }
98 98
99 99 div.square {
100 100 border: 1px solid #999;
101 101 float: left;
102 102 margin: .3em .4em 0 .4em;
103 103 overflow: hidden;
104 104 width: .6em; height: .6em;
105 105 }
106 106
107 107 .contextual {float:right; white-space: nowrap; line-height:1.4em;margin-top:5px;font-size:0.9em;}
108 108 .splitcontentleft{float:left; width:49%;}
109 109 .splitcontentright{float:right; width:49%;}
110 110 form {display: inline;}
111 111 input, select {vertical-align: middle; margin-top: 1px; margin-bottom: 1px;}
112 112 fieldset {border: 1px solid #e4e4e4; margin:0;}
113 113 legend {color: #484848;}
114 114 hr { width: 100%; height: 1px; background: #ccc; border: 0;}
115 115 textarea.wiki-edit { width: 99%; }
116 116 li p {margin-top: 0;}
117 117 div.issue {background:#ffffdd; padding:6px; margin-bottom:6px;border: 1px solid #d7d7d7;}
118 118 .autoscroll {overflow-x: auto; padding:1px; width:100%;}
119 119 #user_firstname, #user_lastname, #user_mail, #notification_option { width: 90%; }
120 120
121 121 /***** Tabular forms ******/
122 122 .tabular p{
123 123 margin: 0;
124 124 padding: 5px 0 8px 0;
125 125 padding-left: 180px; /*width of left column containing the label elements*/
126 126 height: 1%;
127 127 clear:left;
128 128 }
129 129
130 130 .tabular label{
131 131 font-weight: bold;
132 132 float: left;
133 133 text-align: right;
134 134 margin-left: -180px; /*width of left column*/
135 135 width: 175px; /*width of labels. Should be smaller than left column to create some right
136 136 margin*/
137 137 }
138 138
139 139 .tabular label.floating{
140 140 font-weight: normal;
141 141 margin-left: 0px;
142 142 text-align: left;
143 143 width: 200px;
144 144 }
145 145
146 146 #preview fieldset {margin-top: 1em; background: url(../images/draft.png)}
147 147
148 148 #settings .tabular p{ padding-left: 300px; }
149 149 #settings .tabular label{ margin-left: -300px; width: 295px; }
150 150
151 151 .required {color: #bb0000;}
152 152 .summary {font-style: italic;}
153 153
154 154 div.attachments p { margin:4px 0 2px 0; }
155 155
156 156 /***** Flash & error messages ****/
157 #errorExplanation, div.flash, div.nodata {
157 #errorExplanation, div.flash, .nodata {
158 158 padding: 4px 4px 4px 30px;
159 159 margin-bottom: 12px;
160 160 font-size: 1.1em;
161 161 border: 2px solid;
162 162 }
163 163
164 164 div.flash {margin-top: 8px;}
165 165
166 166 div.flash.error, #errorExplanation {
167 167 background: url(../images/false.png) 8px 5px no-repeat;
168 168 background-color: #ffe3e3;
169 169 border-color: #dd0000;
170 170 color: #550000;
171 171 }
172 172
173 173 div.flash.notice {
174 174 background: url(../images/true.png) 8px 5px no-repeat;
175 175 background-color: #dfffdf;
176 176 border-color: #9fcf9f;
177 177 color: #005f00;
178 178 }
179 179
180 180 .nodata {
181 181 text-align: center;
182 182 background-color: #FFEBC1;
183 183 border-color: #FDBF3B;
184 184 color: #A6750C;
185 185 }
186 186
187 187 #errorExplanation ul { font-size: 0.9em;}
188 188
189 189 /***** Ajax indicator ******/
190 190 #ajax-indicator {
191 191 position: absolute; /* fixed not supported by IE */
192 192 background-color:#eee;
193 193 border: 1px solid #bbb;
194 194 top:35%;
195 195 left:40%;
196 196 width:20%;
197 197 font-weight:bold;
198 198 text-align:center;
199 199 padding:0.6em;
200 200 z-index:100;
201 201 filter:alpha(opacity=50);
202 202 -moz-opacity:0.5;
203 203 opacity: 0.5;
204 204 -khtml-opacity: 0.5;
205 205 }
206 206
207 207 html>body #ajax-indicator { position: fixed; }
208 208
209 209 #ajax-indicator span {
210 210 background-position: 0% 40%;
211 211 background-repeat: no-repeat;
212 212 background-image: url(../images/loading.gif);
213 213 padding-left: 26px;
214 214 vertical-align: bottom;
215 215 }
216 216
217 217 /***** Calendar *****/
218 218 table.cal {border-collapse: collapse; width: 100%; margin: 8px 0 6px 0;border: 1px solid #d7d7d7;}
219 219 table.cal thead th {width: 14%;}
220 220 table.cal tbody tr {height: 100px;}
221 221 table.cal th { background-color:#EEEEEE; padding: 4px; }
222 222 table.cal td {border: 1px solid #d7d7d7; vertical-align: top; font-size: 0.9em;}
223 223 table.cal td p.day-num {font-size: 1.1em; text-align:right;}
224 224 table.cal td.odd p.day-num {color: #bbb;}
225 225 table.cal td.today {background:#ffffdd;}
226 226 table.cal td.today p.day-num {font-weight: bold;}
227 227
228 228 /***** Tooltips ******/
229 229 .tooltip{position:relative;z-index:24;}
230 230 .tooltip:hover{z-index:25;color:#000;}
231 231 .tooltip span.tip{display: none; text-align:left;}
232 232
233 233 div.tooltip:hover span.tip{
234 234 display:block;
235 235 position:absolute;
236 236 top:12px; left:24px; width:270px;
237 237 border:1px solid #555;
238 238 background-color:#fff;
239 239 padding: 4px;
240 240 font-size: 0.8em;
241 241 color:#505050;
242 242 }
243 243
244 244 /***** Progress bar *****/
245 245 .progress {
246 246 border: 1px solid #D7D7D7;
247 247 border-collapse: collapse;
248 248 border-spacing: 0pt;
249 249 empty-cells: show;
250 250 padding: 3px;
251 251 width: 40em;
252 252 text-align: center;
253 253 }
254 254
255 255 .progress td { height: 1em; }
256 256 .progress .closed { background: #BAE0BA none repeat scroll 0%; }
257 257 .progress .open { background: #FFF none repeat scroll 0%; }
258 258
259 259 /***** Tabs *****/
260 260 #content .tabs{height: 2.6em;}
261 261 #content .tabs ul{margin:0;}
262 262 #content .tabs ul li{
263 263 float:left;
264 264 list-style-type:none;
265 265 white-space:nowrap;
266 266 margin-right:8px;
267 267 background:#fff;
268 268 }
269 269 #content .tabs ul li a{
270 270 display:block;
271 271 font-size: 0.9em;
272 272 text-decoration:none;
273 273 line-height:1em;
274 274 padding:4px;
275 275 border: 1px solid #c0c0c0;
276 276 }
277 277
278 278 #content .tabs ul li a.selected, #content .tabs ul li a:hover{
279 279 background-color: #507AAA;
280 280 border: 1px solid #507AAA;
281 281 color: #fff;
282 282 text-decoration:none;
283 283 }
284 284
285 285 /***** Diff *****/
286 286 .diff_out { background: #fcc; }
287 287 .diff_in { background: #cfc; }
288 288
289 289 /***** Wiki *****/
290 290 div.wiki table {
291 291 border: 1px solid #505050;
292 292 border-collapse: collapse;
293 293 }
294 294
295 295 div.wiki table, div.wiki td, div.wiki th {
296 296 border: 1px solid #bbb;
297 297 padding: 4px;
298 298 }
299 299
300 300 div.wiki .external {
301 301 background-position: 0% 60%;
302 302 background-repeat: no-repeat;
303 303 padding-left: 12px;
304 304 background-image: url(../images/external.png);
305 305 }
306 306
307 307 div.wiki a.new {
308 308 color: #b73535;
309 309 }
310 310
311 311 div.wiki pre {
312 312 margin: 1em 1em 1em 1.6em;
313 313 padding: 2px;
314 314 background-color: #fafafa;
315 315 border: 1px solid #dadada;
316 316 width:95%;
317 317 overflow-x: auto;
318 318 }
319 319
320 320 div.wiki div.toc {
321 321 background-color: #ffffdd;
322 322 border: 1px solid #e4e4e4;
323 323 padding: 4px;
324 324 line-height: 1.2em;
325 325 margin-bottom: 12px;
326 326 margin-right: 12px;
327 327 display: table
328 328 }
329 329 * html div.wiki div.toc { width: 50%; } /* IE6 doesn't autosize div */
330 330
331 331 div.wiki div.toc.right { float: right; margin-left: 12px; margin-right: 0; width: auto; }
332 332 div.wiki div.toc.left { float: left; margin-right: 12px; margin-left: 0; width: auto; }
333 333
334 334 div.wiki div.toc a {
335 335 display: block;
336 336 font-size: 0.9em;
337 337 font-weight: normal;
338 338 text-decoration: none;
339 339 color: #606060;
340 340 }
341 341 div.wiki div.toc a:hover { color: #c61a1a; text-decoration: underline;}
342 342
343 343 div.wiki div.toc a.heading2 { margin-left: 6px; }
344 344 div.wiki div.toc a.heading3 { margin-left: 12px; font-size: 0.8em; }
345 345
346 346 /***** My page layout *****/
347 347 .block-receiver {
348 348 border:1px dashed #c0c0c0;
349 349 margin-bottom: 20px;
350 350 padding: 15px 0 15px 0;
351 351 }
352 352
353 353 .mypage-box {
354 354 margin:0 0 20px 0;
355 355 color:#505050;
356 356 line-height:1.5em;
357 357 }
358 358
359 359 .handle {
360 360 cursor: move;
361 361 }
362 362
363 363 a.close-icon {
364 364 display:block;
365 365 margin-top:3px;
366 366 overflow:hidden;
367 367 width:12px;
368 368 height:12px;
369 369 background-repeat: no-repeat;
370 370 cursor:pointer;
371 371 background-image:url('../images/close.png');
372 372 }
373 373
374 374 a.close-icon:hover {
375 375 background-image:url('../images/close_hl.png');
376 376 }
377 377
378 378 /***** Gantt chart *****/
379 379 .gantt_hdr {
380 380 position:absolute;
381 381 top:0;
382 382 height:16px;
383 383 border-top: 1px solid #c0c0c0;
384 384 border-bottom: 1px solid #c0c0c0;
385 385 border-right: 1px solid #c0c0c0;
386 386 text-align: center;
387 387 overflow: hidden;
388 388 }
389 389
390 390 .task {
391 391 position: absolute;
392 392 height:8px;
393 393 font-size:0.8em;
394 394 color:#888;
395 395 padding:0;
396 396 margin:0;
397 397 line-height:0.8em;
398 398 }
399 399
400 400 .task_late { background:#f66 url(../images/task_late.png); border: 1px solid #f66; }
401 401 .task_done { background:#66f url(../images/task_done.png); border: 1px solid #66f; }
402 402 .task_todo { background:#aaa url(../images/task_todo.png); border: 1px solid #aaa; }
403 403 .milestone { background-image:url(../images/milestone.png); background-repeat: no-repeat; border: 0; }
404 404
405 405 /***** Icons *****/
406 406 .icon {
407 407 background-position: 0% 40%;
408 408 background-repeat: no-repeat;
409 409 padding-left: 20px;
410 410 padding-top: 2px;
411 411 padding-bottom: 3px;
412 412 }
413 413
414 414 .icon22 {
415 415 background-position: 0% 40%;
416 416 background-repeat: no-repeat;
417 417 padding-left: 26px;
418 418 line-height: 22px;
419 419 vertical-align: middle;
420 420 }
421 421
422 422 .icon-add { background-image: url(../images/add.png); }
423 423 .icon-edit { background-image: url(../images/edit.png); }
424 424 .icon-del { background-image: url(../images/delete.png); }
425 425 .icon-move { background-image: url(../images/move.png); }
426 426 .icon-save { background-image: url(../images/save.png); }
427 427 .icon-cancel { background-image: url(../images/cancel.png); }
428 428 .icon-pdf { background-image: url(../images/pdf.png); }
429 429 .icon-csv { background-image: url(../images/csv.png); }
430 430 .icon-html { background-image: url(../images/html.png); }
431 431 .icon-image { background-image: url(../images/image.png); }
432 432 .icon-txt { background-image: url(../images/txt.png); }
433 433 .icon-file { background-image: url(../images/file.png); }
434 434 .icon-folder { background-image: url(../images/folder.png); }
435 435 .open .icon-folder { background-image: url(../images/folder_open.png); }
436 436 .icon-package { background-image: url(../images/package.png); }
437 437 .icon-home { background-image: url(../images/home.png); }
438 438 .icon-user { background-image: url(../images/user.png); }
439 439 .icon-mypage { background-image: url(../images/user_page.png); }
440 440 .icon-admin { background-image: url(../images/admin.png); }
441 441 .icon-projects { background-image: url(../images/projects.png); }
442 442 .icon-logout { background-image: url(../images/logout.png); }
443 443 .icon-help { background-image: url(../images/help.png); }
444 444 .icon-attachment { background-image: url(../images/attachment.png); }
445 445 .icon-index { background-image: url(../images/index.png); }
446 446 .icon-history { background-image: url(../images/history.png); }
447 447 .icon-feed { background-image: url(../images/feed.png); }
448 448 .icon-time { background-image: url(../images/time.png); }
449 449 .icon-stats { background-image: url(../images/stats.png); }
450 450 .icon-warning { background-image: url(../images/warning.png); }
451 451 .icon-fav { background-image: url(../images/fav.png); }
452 452 .icon-fav-off { background-image: url(../images/fav_off.png); }
453 453 .icon-reload { background-image: url(../images/reload.png); }
454 454 .icon-lock { background-image: url(../images/locked.png); }
455 455 .icon-unlock { background-image: url(../images/unlock.png); }
456 456 .icon-note { background-image: url(../images/note.png); }
457 .icon-checked { background-image: url(../images/true.png); }
457 458
458 459 .icon22-projects { background-image: url(../images/22x22/projects.png); }
459 460 .icon22-users { background-image: url(../images/22x22/users.png); }
460 461 .icon22-tracker { background-image: url(../images/22x22/tracker.png); }
461 462 .icon22-role { background-image: url(../images/22x22/role.png); }
462 463 .icon22-workflow { background-image: url(../images/22x22/workflow.png); }
463 464 .icon22-options { background-image: url(../images/22x22/options.png); }
464 465 .icon22-notifications { background-image: url(../images/22x22/notifications.png); }
465 466 .icon22-authent { background-image: url(../images/22x22/authent.png); }
466 467 .icon22-info { background-image: url(../images/22x22/info.png); }
467 468 .icon22-comment { background-image: url(../images/22x22/comment.png); }
468 469 .icon22-package { background-image: url(../images/22x22/package.png); }
469 470 .icon22-settings { background-image: url(../images/22x22/settings.png); }
470 471 .icon22-plugin { background-image: url(../images/22x22/plugin.png); }
471 472
472 473 /***** Media print specific styles *****/
473 474 @media print {
474 475 #top-menu, #header, #main-menu, #sidebar, #footer, .contextual { display:none; }
475 476 #main { background: #fff; }
476 477 #content { width: 99%; margin: 0; padding: 0; border: 0; background: #fff; }
477 478 }
General Comments 0
You need to be logged in to leave comments. Login now