##// END OF EJS Templates
Build issue filters using javascript....
Jean-Philippe Lang -
r9979:1749fbf3e616
parent child
Show More
@@ -1,131 +1,134
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 4 # Copyright (C) 2006-2012 Jean-Philippe Lang
5 5 #
6 6 # This program is free software; you can redistribute it and/or
7 7 # modify it under the terms of the GNU General Public License
8 8 # as published by the Free Software Foundation; either version 2
9 9 # of the License, or (at your option) any later version.
10 10 #
11 11 # This program is distributed in the hope that it will be useful,
12 12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 14 # GNU General Public License for more details.
15 15 #
16 16 # You should have received a copy of the GNU General Public License
17 17 # along with this program; if not, write to the Free Software
18 18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 19
20 20 module QueriesHelper
21
22 def operators_for_select(filter_type)
23 Query.operators_by_filter_type[filter_type].collect {|o| [l(Query.operators[o]), o]}
21 def filters_options_for_select(query)
22 options = [[]]
23 options += query.available_filters.sort {|a,b| a[1][:order] <=> b[1][:order]}.map do |field, field_options|
24 [field_options[:name], field]
25 end
26 options_for_select(options)
24 27 end
25 28
26 29 def column_header(column)
27 30 column.sortable ? sort_header_tag(column.name.to_s, :caption => column.caption,
28 31 :default_order => column.default_order) :
29 32 content_tag('th', h(column.caption))
30 33 end
31 34
32 35 def column_content(column, issue)
33 36 value = column.value(issue)
34 37 if value.is_a?(Array)
35 38 value.collect {|v| column_value(column, issue, v)}.compact.sort.join(', ').html_safe
36 39 else
37 40 column_value(column, issue, value)
38 41 end
39 42 end
40 43
41 44 def column_value(column, issue, value)
42 45 case value.class.name
43 46 when 'String'
44 47 if column.name == :subject
45 48 link_to(h(value), :controller => 'issues', :action => 'show', :id => issue)
46 49 else
47 50 h(value)
48 51 end
49 52 when 'Time'
50 53 format_time(value)
51 54 when 'Date'
52 55 format_date(value)
53 56 when 'Fixnum', 'Float'
54 57 if column.name == :done_ratio
55 58 progress_bar(value, :width => '80px')
56 59 elsif column.name == :spent_hours
57 60 sprintf "%.2f", value
58 61 else
59 62 h(value.to_s)
60 63 end
61 64 when 'User'
62 65 link_to_user value
63 66 when 'Project'
64 67 link_to_project value
65 68 when 'Version'
66 69 link_to(h(value), :controller => 'versions', :action => 'show', :id => value)
67 70 when 'TrueClass'
68 71 l(:general_text_Yes)
69 72 when 'FalseClass'
70 73 l(:general_text_No)
71 74 when 'Issue'
72 75 link_to_issue(value, :subject => false)
73 76 else
74 77 h(value)
75 78 end
76 79 end
77 80
78 81 # Retrieve query from session or build a new query
79 82 def retrieve_query
80 83 if !params[:query_id].blank?
81 84 cond = "project_id IS NULL"
82 85 cond << " OR project_id = #{@project.id}" if @project
83 86 @query = Query.find(params[:query_id], :conditions => cond)
84 87 raise ::Unauthorized unless @query.visible?
85 88 @query.project = @project
86 89 session[:query] = {:id => @query.id, :project_id => @query.project_id}
87 90 sort_clear
88 91 elsif api_request? || params[:set_filter] || session[:query].nil? || session[:query][:project_id] != (@project ? @project.id : nil)
89 92 # Give it a name, required to be valid
90 93 @query = Query.new(:name => "_")
91 94 @query.project = @project
92 95 build_query_from_params
93 96 session[:query] = {:project_id => @query.project_id, :filters => @query.filters, :group_by => @query.group_by, :column_names => @query.column_names}
94 97 else
95 98 # retrieve from session
96 99 @query = Query.find_by_id(session[:query][:id]) if session[:query][:id]
97 100 @query ||= Query.new(:name => "_", :filters => session[:query][:filters], :group_by => session[:query][:group_by], :column_names => session[:query][:column_names])
98 101 @query.project = @project
99 102 end
100 103 end
101 104
102 105 def retrieve_query_from_session
103 106 if session[:query]
104 107 if session[:query][:id]
105 108 @query = Query.find_by_id(session[:query][:id])
106 109 return unless @query
107 110 else
108 111 @query = Query.new(:name => "_", :filters => session[:query][:filters], :group_by => session[:query][:group_by], :column_names => session[:query][:column_names])
109 112 end
110 113 if session[:query].has_key?(:project_id)
111 114 @query.project_id = session[:query][:project_id]
112 115 else
113 116 @query.project = @project
114 117 end
115 118 @query
116 119 end
117 120 end
118 121
119 122 def build_query_from_params
120 123 if params[:fields] || params[:f]
121 124 @query.filters = {}
122 125 @query.add_filters(params[:fields] || params[:f], params[:operators] || params[:op], params[:values] || params[:v])
123 126 else
124 127 @query.available_filters.keys.each do |field|
125 128 @query.add_short_filter(field, params[field]) if params[field]
126 129 end
127 130 end
128 131 @query.group_by = params[:group_by] || (params[:query] && params[:query][:group_by])
129 132 @query.column_names = params[:c] || (params[:query] && params[:query][:column_names])
130 133 end
131 134 end
@@ -1,914 +1,932
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2012 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 QueryColumn
19 19 attr_accessor :name, :sortable, :groupable, :default_order
20 20 include Redmine::I18n
21 21
22 22 def initialize(name, options={})
23 23 self.name = name
24 24 self.sortable = options[:sortable]
25 25 self.groupable = options[:groupable] || false
26 26 if groupable == true
27 27 self.groupable = name.to_s
28 28 end
29 29 self.default_order = options[:default_order]
30 30 @caption_key = options[:caption] || "field_#{name}"
31 31 end
32 32
33 33 def caption
34 34 l(@caption_key)
35 35 end
36 36
37 37 # Returns true if the column is sortable, otherwise false
38 38 def sortable?
39 39 !@sortable.nil?
40 40 end
41 41
42 42 def sortable
43 43 @sortable.is_a?(Proc) ? @sortable.call : @sortable
44 44 end
45 45
46 46 def value(issue)
47 47 issue.send name
48 48 end
49 49
50 50 def css_classes
51 51 name
52 52 end
53 53 end
54 54
55 55 class QueryCustomFieldColumn < QueryColumn
56 56
57 57 def initialize(custom_field)
58 58 self.name = "cf_#{custom_field.id}".to_sym
59 59 self.sortable = custom_field.order_statement || false
60 60 self.groupable = custom_field.group_statement || false
61 61 @cf = custom_field
62 62 end
63 63
64 64 def caption
65 65 @cf.name
66 66 end
67 67
68 68 def custom_field
69 69 @cf
70 70 end
71 71
72 72 def value(issue)
73 73 cv = issue.custom_values.select {|v| v.custom_field_id == @cf.id}.collect {|v| @cf.cast_value(v.value)}
74 74 cv.size > 1 ? cv : cv.first
75 75 end
76 76
77 77 def css_classes
78 78 @css_classes ||= "#{name} #{@cf.field_format}"
79 79 end
80 80 end
81 81
82 82 class Query < ActiveRecord::Base
83 83 class StatementInvalid < ::ActiveRecord::StatementInvalid
84 84 end
85 85
86 86 belongs_to :project
87 87 belongs_to :user
88 88 serialize :filters
89 89 serialize :column_names
90 90 serialize :sort_criteria, Array
91 91
92 92 attr_protected :project_id, :user_id
93 93
94 94 validates_presence_of :name
95 95 validates_length_of :name, :maximum => 255
96 96 validate :validate_query_filters
97 97
98 98 @@operators = { "=" => :label_equals,
99 99 "!" => :label_not_equals,
100 100 "o" => :label_open_issues,
101 101 "c" => :label_closed_issues,
102 102 "!*" => :label_none,
103 103 "*" => :label_all,
104 104 ">=" => :label_greater_or_equal,
105 105 "<=" => :label_less_or_equal,
106 106 "><" => :label_between,
107 107 "<t+" => :label_in_less_than,
108 108 ">t+" => :label_in_more_than,
109 109 "t+" => :label_in,
110 110 "t" => :label_today,
111 111 "w" => :label_this_week,
112 112 ">t-" => :label_less_than_ago,
113 113 "<t-" => :label_more_than_ago,
114 114 "t-" => :label_ago,
115 115 "~" => :label_contains,
116 116 "!~" => :label_not_contains }
117 117
118 118 cattr_reader :operators
119 119
120 120 @@operators_by_filter_type = { :list => [ "=", "!" ],
121 121 :list_status => [ "o", "=", "!", "c", "*" ],
122 122 :list_optional => [ "=", "!", "!*", "*" ],
123 123 :list_subprojects => [ "*", "!*", "=" ],
124 124 :date => [ "=", ">=", "<=", "><", "<t+", ">t+", "t+", "t", "w", ">t-", "<t-", "t-", "!*", "*" ],
125 125 :date_past => [ "=", ">=", "<=", "><", ">t-", "<t-", "t-", "t", "w", "!*", "*" ],
126 126 :string => [ "=", "~", "!", "!~", "!*", "*" ],
127 127 :text => [ "~", "!~", "!*", "*" ],
128 128 :integer => [ "=", ">=", "<=", "><", "!*", "*" ],
129 129 :float => [ "=", ">=", "<=", "><", "!*", "*" ] }
130 130
131 131 cattr_reader :operators_by_filter_type
132 132
133 133 @@available_columns = [
134 134 QueryColumn.new(:project, :sortable => "#{Project.table_name}.name", :groupable => true),
135 135 QueryColumn.new(:tracker, :sortable => "#{Tracker.table_name}.position", :groupable => true),
136 136 QueryColumn.new(:parent, :sortable => ["#{Issue.table_name}.root_id", "#{Issue.table_name}.lft ASC"], :default_order => 'desc', :caption => :field_parent_issue),
137 137 QueryColumn.new(:status, :sortable => "#{IssueStatus.table_name}.position", :groupable => true),
138 138 QueryColumn.new(:priority, :sortable => "#{IssuePriority.table_name}.position", :default_order => 'desc', :groupable => true),
139 139 QueryColumn.new(:subject, :sortable => "#{Issue.table_name}.subject"),
140 140 QueryColumn.new(:author, :sortable => lambda {User.fields_for_order_statement("authors")}, :groupable => true),
141 141 QueryColumn.new(:assigned_to, :sortable => lambda {User.fields_for_order_statement}, :groupable => true),
142 142 QueryColumn.new(:updated_on, :sortable => "#{Issue.table_name}.updated_on", :default_order => 'desc'),
143 143 QueryColumn.new(:category, :sortable => "#{IssueCategory.table_name}.name", :groupable => true),
144 144 QueryColumn.new(:fixed_version, :sortable => lambda {Version.fields_for_order_statement}, :groupable => true),
145 145 QueryColumn.new(:start_date, :sortable => "#{Issue.table_name}.start_date"),
146 146 QueryColumn.new(:due_date, :sortable => "#{Issue.table_name}.due_date"),
147 147 QueryColumn.new(:estimated_hours, :sortable => "#{Issue.table_name}.estimated_hours"),
148 148 QueryColumn.new(:done_ratio, :sortable => "#{Issue.table_name}.done_ratio", :groupable => true),
149 149 QueryColumn.new(:created_on, :sortable => "#{Issue.table_name}.created_on", :default_order => 'desc'),
150 150 ]
151 151 cattr_reader :available_columns
152 152
153 153 scope :visible, lambda {|*args|
154 154 user = args.shift || User.current
155 155 base = Project.allowed_to_condition(user, :view_issues, *args)
156 156 user_id = user.logged? ? user.id : 0
157 157 {
158 158 :conditions => ["(#{table_name}.project_id IS NULL OR (#{base})) AND (#{table_name}.is_public = ? OR #{table_name}.user_id = ?)", true, user_id],
159 159 :include => :project
160 160 }
161 161 }
162 162
163 163 def initialize(attributes=nil, *args)
164 164 super attributes
165 165 self.filters ||= { 'status_id' => {:operator => "o", :values => [""]} }
166 166 @is_for_all = project.nil?
167 167 end
168 168
169 169 def validate_query_filters
170 170 filters.each_key do |field|
171 171 if values_for(field)
172 172 case type_for(field)
173 173 when :integer
174 174 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^[+-]?\d+$/) }
175 175 when :float
176 176 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^[+-]?\d+(\.\d*)?$/) }
177 177 when :date, :date_past
178 178 case operator_for(field)
179 179 when "=", ">=", "<=", "><"
180 180 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && (!v.match(/^\d{4}-\d{2}-\d{2}$/) || (Date.parse(v) rescue nil).nil?) }
181 181 when ">t-", "<t-", "t-"
182 182 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^\d+$/) }
183 183 end
184 184 end
185 185 end
186 186
187 187 add_filter_error(field, :blank) unless
188 188 # filter requires one or more values
189 189 (values_for(field) and !values_for(field).first.blank?) or
190 190 # filter doesn't require any value
191 191 ["o", "c", "!*", "*", "t", "w"].include? operator_for(field)
192 192 end if filters
193 193 end
194 194
195 195 def add_filter_error(field, message)
196 196 m = label_for(field) + " " + l(message, :scope => 'activerecord.errors.messages')
197 197 errors.add(:base, m)
198 198 end
199 199
200 200 # Returns true if the query is visible to +user+ or the current user.
201 201 def visible?(user=User.current)
202 202 (project.nil? || user.allowed_to?(:view_issues, project)) && (self.is_public? || self.user_id == user.id)
203 203 end
204 204
205 205 def editable_by?(user)
206 206 return false unless user
207 207 # Admin can edit them all and regular users can edit their private queries
208 208 return true if user.admin? || (!is_public && self.user_id == user.id)
209 209 # Members can not edit public queries that are for all project (only admin is allowed to)
210 210 is_public && !@is_for_all && user.allowed_to?(:manage_public_queries, project)
211 211 end
212 212
213 213 def trackers
214 214 @trackers ||= project.nil? ? Tracker.find(:all, :order => 'position') : project.rolled_up_trackers
215 215 end
216 216
217 # Returns a hash of localized labels for all filter operators
218 def self.operators_labels
219 operators.inject({}) {|h, operator| h[operator.first] = l(operator.last); h}
220 end
221
217 222 def available_filters
218 223 return @available_filters if @available_filters
219 224
220 225 @available_filters = { "status_id" => { :type => :list_status, :order => 1, :values => IssueStatus.find(:all, :order => 'position').collect{|s| [s.name, s.id.to_s] } },
221 226 "tracker_id" => { :type => :list, :order => 2, :values => trackers.collect{|s| [s.name, s.id.to_s] } },
222 227 "priority_id" => { :type => :list, :order => 3, :values => IssuePriority.all.collect{|s| [s.name, s.id.to_s] } },
223 228 "subject" => { :type => :text, :order => 8 },
224 229 "created_on" => { :type => :date_past, :order => 9 },
225 230 "updated_on" => { :type => :date_past, :order => 10 },
226 231 "start_date" => { :type => :date, :order => 11 },
227 232 "due_date" => { :type => :date, :order => 12 },
228 233 "estimated_hours" => { :type => :float, :order => 13 },
229 234 "done_ratio" => { :type => :integer, :order => 14 }}
230 235
231 236 principals = []
232 237 if project
233 238 principals += project.principals.sort
234 239 unless project.leaf?
235 240 subprojects = project.descendants.visible.all
236 241 if subprojects.any?
237 242 @available_filters["subproject_id"] = { :type => :list_subprojects, :order => 13, :values => subprojects.collect{|s| [s.name, s.id.to_s] } }
238 243 principals += Principal.member_of(subprojects)
239 244 end
240 245 end
241 246 else
242 247 all_projects = Project.visible.all
243 248 if all_projects.any?
244 249 # members of visible projects
245 250 principals += Principal.member_of(all_projects)
246 251
247 252 # project filter
248 253 project_values = []
249 254 if User.current.logged? && User.current.memberships.any?
250 255 project_values << ["<< #{l(:label_my_projects).downcase} >>", "mine"]
251 256 end
252 257 Project.project_tree(all_projects) do |p, level|
253 258 prefix = (level > 0 ? ('--' * level + ' ') : '')
254 259 project_values << ["#{prefix}#{p.name}", p.id.to_s]
255 260 end
256 261 @available_filters["project_id"] = { :type => :list, :order => 1, :values => project_values} unless project_values.empty?
257 262 end
258 263 end
259 264 principals.uniq!
260 265 principals.sort!
261 266 users = principals.select {|p| p.is_a?(User)}
262 267
263 268 assigned_to_values = []
264 269 assigned_to_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
265 270 assigned_to_values += (Setting.issue_group_assignment? ? principals : users).collect{|s| [s.name, s.id.to_s] }
266 271 @available_filters["assigned_to_id"] = { :type => :list_optional, :order => 4, :values => assigned_to_values } unless assigned_to_values.empty?
267 272
268 273 author_values = []
269 274 author_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
270 275 author_values += users.collect{|s| [s.name, s.id.to_s] }
271 276 @available_filters["author_id"] = { :type => :list, :order => 5, :values => author_values } unless author_values.empty?
272 277
273 278 group_values = Group.all.collect {|g| [g.name, g.id.to_s] }
274 279 @available_filters["member_of_group"] = { :type => :list_optional, :order => 6, :values => group_values } unless group_values.empty?
275 280
276 281 role_values = Role.givable.collect {|r| [r.name, r.id.to_s] }
277 282 @available_filters["assigned_to_role"] = { :type => :list_optional, :order => 7, :values => role_values } unless role_values.empty?
278 283
279 284 if User.current.logged?
280 285 @available_filters["watcher_id"] = { :type => :list, :order => 15, :values => [["<< #{l(:label_me)} >>", "me"]] }
281 286 end
282 287
283 288 if project
284 289 # project specific filters
285 290 categories = project.issue_categories.all
286 291 unless categories.empty?
287 292 @available_filters["category_id"] = { :type => :list_optional, :order => 6, :values => categories.collect{|s| [s.name, s.id.to_s] } }
288 293 end
289 294 versions = project.shared_versions.all
290 295 unless versions.empty?
291 296 @available_filters["fixed_version_id"] = { :type => :list_optional, :order => 7, :values => versions.sort.collect{|s| ["#{s.project.name} - #{s.name}", s.id.to_s] } }
292 297 end
293 298 add_custom_fields_filters(project.all_issue_custom_fields)
294 299 else
295 300 # global filters for cross project issue list
296 301 system_shared_versions = Version.visible.find_all_by_sharing('system')
297 302 unless system_shared_versions.empty?
298 303 @available_filters["fixed_version_id"] = { :type => :list_optional, :order => 7, :values => system_shared_versions.sort.collect{|s| ["#{s.project.name} - #{s.name}", s.id.to_s] } }
299 304 end
300 305 add_custom_fields_filters(IssueCustomField.find(:all, :conditions => {:is_filter => true, :is_for_all => true}))
301 306 end
302 307
303 308 if User.current.allowed_to?(:set_issues_private, nil, :global => true) ||
304 309 User.current.allowed_to?(:set_own_issues_private, nil, :global => true)
305 310 @available_filters["is_private"] = { :type => :list, :order => 15, :values => [[l(:general_text_yes), "1"], [l(:general_text_no), "0"]] }
306 311 end
307 312
308 313 Tracker.disabled_core_fields(trackers).each {|field|
309 314 @available_filters.delete field
310 315 }
311 316
317 @available_filters.each do |field, options|
318 options[:name] ||= l("field_#{field}".gsub(/_id$/, ''))
319 end
320
312 321 @available_filters
313 322 end
314 323
324 # Returns a representation of the available filters for JSON serialization
325 def available_filters_as_json
326 json = {}
327 available_filters.each do |field, options|
328 json[field] = options.slice(:type, :name, :values).stringify_keys
329 end
330 json
331 end
332
315 333 def add_filter(field, operator, values)
316 334 # values must be an array
317 335 return unless values.nil? || values.is_a?(Array)
318 336 # check if field is defined as an available filter
319 337 if available_filters.has_key? field
320 338 filter_options = available_filters[field]
321 339 # check if operator is allowed for that filter
322 340 #if @@operators_by_filter_type[filter_options[:type]].include? operator
323 341 # allowed_values = values & ([""] + (filter_options[:values] || []).collect {|val| val[1]})
324 342 # filters[field] = {:operator => operator, :values => allowed_values } if (allowed_values.first and !allowed_values.first.empty?) or ["o", "c", "!*", "*", "t"].include? operator
325 343 #end
326 344 filters[field] = {:operator => operator, :values => (values || [''])}
327 345 end
328 346 end
329 347
330 348 def add_short_filter(field, expression)
331 349 return unless expression && available_filters.has_key?(field)
332 350 field_type = available_filters[field][:type]
333 351 @@operators_by_filter_type[field_type].sort.reverse.detect do |operator|
334 352 next unless expression =~ /^#{Regexp.escape(operator)}(.*)$/
335 353 add_filter field, operator, $1.present? ? $1.split('|') : ['']
336 354 end || add_filter(field, '=', expression.split('|'))
337 355 end
338 356
339 357 # Add multiple filters using +add_filter+
340 358 def add_filters(fields, operators, values)
341 359 if fields.is_a?(Array) && operators.is_a?(Hash) && (values.nil? || values.is_a?(Hash))
342 360 fields.each do |field|
343 361 add_filter(field, operators[field], values && values[field])
344 362 end
345 363 end
346 364 end
347 365
348 366 def has_filter?(field)
349 367 filters and filters[field]
350 368 end
351 369
352 370 def type_for(field)
353 371 available_filters[field][:type] if available_filters.has_key?(field)
354 372 end
355 373
356 374 def operator_for(field)
357 375 has_filter?(field) ? filters[field][:operator] : nil
358 376 end
359 377
360 378 def values_for(field)
361 379 has_filter?(field) ? filters[field][:values] : nil
362 380 end
363 381
364 382 def value_for(field, index=0)
365 383 (values_for(field) || [])[index]
366 384 end
367 385
368 386 def label_for(field)
369 387 label = available_filters[field][:name] if available_filters.has_key?(field)
370 388 label ||= l("field_#{field.to_s.gsub(/_id$/, '')}", :default => field)
371 389 end
372 390
373 391 def available_columns
374 392 return @available_columns if @available_columns
375 393 @available_columns = ::Query.available_columns.dup
376 394 @available_columns += (project ?
377 395 project.all_issue_custom_fields :
378 396 IssueCustomField.find(:all)
379 397 ).collect {|cf| QueryCustomFieldColumn.new(cf) }
380 398
381 399 if User.current.allowed_to?(:view_time_entries, project, :global => true)
382 400 index = nil
383 401 @available_columns.each_with_index {|column, i| index = i if column.name == :estimated_hours}
384 402 index = (index ? index + 1 : -1)
385 403 # insert the column after estimated_hours or at the end
386 404 @available_columns.insert index, QueryColumn.new(:spent_hours,
387 405 :sortable => "(SELECT COALESCE(SUM(hours), 0) FROM #{TimeEntry.table_name} WHERE #{TimeEntry.table_name}.issue_id = #{Issue.table_name}.id)",
388 406 :default_order => 'desc',
389 407 :caption => :label_spent_time
390 408 )
391 409 end
392 410
393 411 if User.current.allowed_to?(:set_issues_private, nil, :global => true) ||
394 412 User.current.allowed_to?(:set_own_issues_private, nil, :global => true)
395 413 @available_columns << QueryColumn.new(:is_private, :sortable => "#{Issue.table_name}.is_private")
396 414 end
397 415
398 416 disabled_fields = Tracker.disabled_core_fields(trackers).map {|field| field.sub(/_id$/, '')}
399 417 @available_columns.reject! {|column|
400 418 disabled_fields.include?(column.name.to_s)
401 419 }
402 420
403 421 @available_columns
404 422 end
405 423
406 424 def self.available_columns=(v)
407 425 self.available_columns = (v)
408 426 end
409 427
410 428 def self.add_available_column(column)
411 429 self.available_columns << (column) if column.is_a?(QueryColumn)
412 430 end
413 431
414 432 # Returns an array of columns that can be used to group the results
415 433 def groupable_columns
416 434 available_columns.select {|c| c.groupable}
417 435 end
418 436
419 437 # Returns a Hash of columns and the key for sorting
420 438 def sortable_columns
421 439 {'id' => "#{Issue.table_name}.id"}.merge(available_columns.inject({}) {|h, column|
422 440 h[column.name.to_s] = column.sortable
423 441 h
424 442 })
425 443 end
426 444
427 445 def columns
428 446 # preserve the column_names order
429 447 (has_default_columns? ? default_columns_names : column_names).collect do |name|
430 448 available_columns.find { |col| col.name == name }
431 449 end.compact
432 450 end
433 451
434 452 def default_columns_names
435 453 @default_columns_names ||= begin
436 454 default_columns = Setting.issue_list_default_columns.map(&:to_sym)
437 455
438 456 project.present? ? default_columns : [:project] | default_columns
439 457 end
440 458 end
441 459
442 460 def column_names=(names)
443 461 if names
444 462 names = names.select {|n| n.is_a?(Symbol) || !n.blank? }
445 463 names = names.collect {|n| n.is_a?(Symbol) ? n : n.to_sym }
446 464 # Set column_names to nil if default columns
447 465 if names == default_columns_names
448 466 names = nil
449 467 end
450 468 end
451 469 write_attribute(:column_names, names)
452 470 end
453 471
454 472 def has_column?(column)
455 473 column_names && column_names.include?(column.is_a?(QueryColumn) ? column.name : column)
456 474 end
457 475
458 476 def has_default_columns?
459 477 column_names.nil? || column_names.empty?
460 478 end
461 479
462 480 def sort_criteria=(arg)
463 481 c = []
464 482 if arg.is_a?(Hash)
465 483 arg = arg.keys.sort.collect {|k| arg[k]}
466 484 end
467 485 c = arg.select {|k,o| !k.to_s.blank?}.slice(0,3).collect {|k,o| [k.to_s, o == 'desc' ? o : 'asc']}
468 486 write_attribute(:sort_criteria, c)
469 487 end
470 488
471 489 def sort_criteria
472 490 read_attribute(:sort_criteria) || []
473 491 end
474 492
475 493 def sort_criteria_key(arg)
476 494 sort_criteria && sort_criteria[arg] && sort_criteria[arg].first
477 495 end
478 496
479 497 def sort_criteria_order(arg)
480 498 sort_criteria && sort_criteria[arg] && sort_criteria[arg].last
481 499 end
482 500
483 501 # Returns the SQL sort order that should be prepended for grouping
484 502 def group_by_sort_order
485 503 if grouped? && (column = group_by_column)
486 504 column.sortable.is_a?(Array) ?
487 505 column.sortable.collect {|s| "#{s} #{column.default_order}"}.join(',') :
488 506 "#{column.sortable} #{column.default_order}"
489 507 end
490 508 end
491 509
492 510 # Returns true if the query is a grouped query
493 511 def grouped?
494 512 !group_by_column.nil?
495 513 end
496 514
497 515 def group_by_column
498 516 groupable_columns.detect {|c| c.groupable && c.name.to_s == group_by}
499 517 end
500 518
501 519 def group_by_statement
502 520 group_by_column.try(:groupable)
503 521 end
504 522
505 523 def project_statement
506 524 project_clauses = []
507 525 if project && !project.descendants.active.empty?
508 526 ids = [project.id]
509 527 if has_filter?("subproject_id")
510 528 case operator_for("subproject_id")
511 529 when '='
512 530 # include the selected subprojects
513 531 ids += values_for("subproject_id").each(&:to_i)
514 532 when '!*'
515 533 # main project only
516 534 else
517 535 # all subprojects
518 536 ids += project.descendants.collect(&:id)
519 537 end
520 538 elsif Setting.display_subprojects_issues?
521 539 ids += project.descendants.collect(&:id)
522 540 end
523 541 project_clauses << "#{Project.table_name}.id IN (%s)" % ids.join(',')
524 542 elsif project
525 543 project_clauses << "#{Project.table_name}.id = %d" % project.id
526 544 end
527 545 project_clauses.any? ? project_clauses.join(' AND ') : nil
528 546 end
529 547
530 548 def statement
531 549 # filters clauses
532 550 filters_clauses = []
533 551 filters.each_key do |field|
534 552 next if field == "subproject_id"
535 553 v = values_for(field).clone
536 554 next unless v and !v.empty?
537 555 operator = operator_for(field)
538 556
539 557 # "me" value subsitution
540 558 if %w(assigned_to_id author_id watcher_id).include?(field)
541 559 if v.delete("me")
542 560 if User.current.logged?
543 561 v.push(User.current.id.to_s)
544 562 v += User.current.group_ids.map(&:to_s) if field == 'assigned_to_id'
545 563 else
546 564 v.push("0")
547 565 end
548 566 end
549 567 end
550 568
551 569 if field == 'project_id'
552 570 if v.delete('mine')
553 571 v += User.current.memberships.map(&:project_id).map(&:to_s)
554 572 end
555 573 end
556 574
557 575 if field =~ /^cf_(\d+)$/
558 576 # custom field
559 577 filters_clauses << sql_for_custom_field(field, operator, v, $1)
560 578 elsif respond_to?("sql_for_#{field}_field")
561 579 # specific statement
562 580 filters_clauses << send("sql_for_#{field}_field", field, operator, v)
563 581 else
564 582 # regular field
565 583 filters_clauses << '(' + sql_for_field(field, operator, v, Issue.table_name, field) + ')'
566 584 end
567 585 end if filters and valid?
568 586
569 587 filters_clauses << project_statement
570 588 filters_clauses.reject!(&:blank?)
571 589
572 590 filters_clauses.any? ? filters_clauses.join(' AND ') : nil
573 591 end
574 592
575 593 # Returns the issue count
576 594 def issue_count
577 595 Issue.visible.count(:include => [:status, :project], :conditions => statement)
578 596 rescue ::ActiveRecord::StatementInvalid => e
579 597 raise StatementInvalid.new(e.message)
580 598 end
581 599
582 600 # Returns the issue count by group or nil if query is not grouped
583 601 def issue_count_by_group
584 602 r = nil
585 603 if grouped?
586 604 begin
587 605 # Rails3 will raise an (unexpected) RecordNotFound if there's only a nil group value
588 606 r = Issue.visible.count(:group => group_by_statement, :include => [:status, :project], :conditions => statement)
589 607 rescue ActiveRecord::RecordNotFound
590 608 r = {nil => issue_count}
591 609 end
592 610 c = group_by_column
593 611 if c.is_a?(QueryCustomFieldColumn)
594 612 r = r.keys.inject({}) {|h, k| h[c.custom_field.cast_value(k)] = r[k]; h}
595 613 end
596 614 end
597 615 r
598 616 rescue ::ActiveRecord::StatementInvalid => e
599 617 raise StatementInvalid.new(e.message)
600 618 end
601 619
602 620 # Returns the issues
603 621 # Valid options are :order, :offset, :limit, :include, :conditions
604 622 def issues(options={})
605 623 order_option = [group_by_sort_order, options[:order]].reject {|s| s.blank?}.join(',')
606 624 order_option = nil if order_option.blank?
607 625
608 626 issues = Issue.visible.scoped(:conditions => options[:conditions]).find :all, :include => ([:status, :project] + (options[:include] || [])).uniq,
609 627 :conditions => statement,
610 628 :order => order_option,
611 629 :joins => joins_for_order_statement(order_option),
612 630 :limit => options[:limit],
613 631 :offset => options[:offset]
614 632
615 633 if has_column?(:spent_hours)
616 634 Issue.load_visible_spent_hours(issues)
617 635 end
618 636 issues
619 637 rescue ::ActiveRecord::StatementInvalid => e
620 638 raise StatementInvalid.new(e.message)
621 639 end
622 640
623 641 # Returns the issues ids
624 642 def issue_ids(options={})
625 643 order_option = [group_by_sort_order, options[:order]].reject {|s| s.blank?}.join(',')
626 644 order_option = nil if order_option.blank?
627 645
628 646 Issue.visible.scoped(:conditions => options[:conditions]).scoped(:include => ([:status, :project] + (options[:include] || [])).uniq,
629 647 :conditions => statement,
630 648 :order => order_option,
631 649 :joins => joins_for_order_statement(order_option),
632 650 :limit => options[:limit],
633 651 :offset => options[:offset]).find_ids
634 652 rescue ::ActiveRecord::StatementInvalid => e
635 653 raise StatementInvalid.new(e.message)
636 654 end
637 655
638 656 # Returns the journals
639 657 # Valid options are :order, :offset, :limit
640 658 def journals(options={})
641 659 Journal.visible.find :all, :include => [:details, :user, {:issue => [:project, :author, :tracker, :status]}],
642 660 :conditions => statement,
643 661 :order => options[:order],
644 662 :limit => options[:limit],
645 663 :offset => options[:offset]
646 664 rescue ::ActiveRecord::StatementInvalid => e
647 665 raise StatementInvalid.new(e.message)
648 666 end
649 667
650 668 # Returns the versions
651 669 # Valid options are :conditions
652 670 def versions(options={})
653 671 Version.visible.scoped(:conditions => options[:conditions]).find :all, :include => :project, :conditions => project_statement
654 672 rescue ::ActiveRecord::StatementInvalid => e
655 673 raise StatementInvalid.new(e.message)
656 674 end
657 675
658 676 def sql_for_watcher_id_field(field, operator, value)
659 677 db_table = Watcher.table_name
660 678 "#{Issue.table_name}.id #{ operator == '=' ? 'IN' : 'NOT IN' } (SELECT #{db_table}.watchable_id FROM #{db_table} WHERE #{db_table}.watchable_type='Issue' AND " +
661 679 sql_for_field(field, '=', value, db_table, 'user_id') + ')'
662 680 end
663 681
664 682 def sql_for_member_of_group_field(field, operator, value)
665 683 if operator == '*' # Any group
666 684 groups = Group.all
667 685 operator = '=' # Override the operator since we want to find by assigned_to
668 686 elsif operator == "!*"
669 687 groups = Group.all
670 688 operator = '!' # Override the operator since we want to find by assigned_to
671 689 else
672 690 groups = Group.find_all_by_id(value)
673 691 end
674 692 groups ||= []
675 693
676 694 members_of_groups = groups.inject([]) {|user_ids, group|
677 695 if group && group.user_ids.present?
678 696 user_ids << group.user_ids
679 697 end
680 698 user_ids.flatten.uniq.compact
681 699 }.sort.collect(&:to_s)
682 700
683 701 '(' + sql_for_field("assigned_to_id", operator, members_of_groups, Issue.table_name, "assigned_to_id", false) + ')'
684 702 end
685 703
686 704 def sql_for_assigned_to_role_field(field, operator, value)
687 705 case operator
688 706 when "*", "!*" # Member / Not member
689 707 sw = operator == "!*" ? 'NOT' : ''
690 708 nl = operator == "!*" ? "#{Issue.table_name}.assigned_to_id IS NULL OR" : ''
691 709 "(#{nl} #{Issue.table_name}.assigned_to_id #{sw} IN (SELECT DISTINCT #{Member.table_name}.user_id FROM #{Member.table_name}" +
692 710 " WHERE #{Member.table_name}.project_id = #{Issue.table_name}.project_id))"
693 711 when "=", "!"
694 712 role_cond = value.any? ?
695 713 "#{MemberRole.table_name}.role_id IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")" :
696 714 "1=0"
697 715
698 716 sw = operator == "!" ? 'NOT' : ''
699 717 nl = operator == "!" ? "#{Issue.table_name}.assigned_to_id IS NULL OR" : ''
700 718 "(#{nl} #{Issue.table_name}.assigned_to_id #{sw} IN (SELECT DISTINCT #{Member.table_name}.user_id FROM #{Member.table_name}, #{MemberRole.table_name}" +
701 719 " WHERE #{Member.table_name}.project_id = #{Issue.table_name}.project_id AND #{Member.table_name}.id = #{MemberRole.table_name}.member_id AND #{role_cond}))"
702 720 end
703 721 end
704 722
705 723 def sql_for_is_private_field(field, operator, value)
706 724 op = (operator == "=" ? 'IN' : 'NOT IN')
707 725 va = value.map {|v| v == '0' ? connection.quoted_false : connection.quoted_true}.uniq.join(',')
708 726
709 727 "#{Issue.table_name}.is_private #{op} (#{va})"
710 728 end
711 729
712 730 private
713 731
714 732 def sql_for_custom_field(field, operator, value, custom_field_id)
715 733 db_table = CustomValue.table_name
716 734 db_field = 'value'
717 735 filter = @available_filters[field]
718 736 if filter && filter[:format] == 'user'
719 737 if value.delete('me')
720 738 value.push User.current.id.to_s
721 739 end
722 740 end
723 741 not_in = nil
724 742 if operator == '!'
725 743 # Makes ! operator work for custom fields with multiple values
726 744 operator = '='
727 745 not_in = 'NOT'
728 746 end
729 747 "#{Issue.table_name}.id #{not_in} IN (SELECT #{Issue.table_name}.id FROM #{Issue.table_name} LEFT OUTER JOIN #{db_table} ON #{db_table}.customized_type='Issue' AND #{db_table}.customized_id=#{Issue.table_name}.id AND #{db_table}.custom_field_id=#{custom_field_id} WHERE " +
730 748 sql_for_field(field, operator, value, db_table, db_field, true) + ')'
731 749 end
732 750
733 751 # Helper method to generate the WHERE sql for a +field+, +operator+ and a +value+
734 752 def sql_for_field(field, operator, value, db_table, db_field, is_custom_filter=false)
735 753 sql = ''
736 754 case operator
737 755 when "="
738 756 if value.any?
739 757 case type_for(field)
740 758 when :date, :date_past
741 759 sql = date_clause(db_table, db_field, (Date.parse(value.first) rescue nil), (Date.parse(value.first) rescue nil))
742 760 when :integer
743 761 if is_custom_filter
744 762 sql = "(#{db_table}.#{db_field} <> '' AND CAST(#{db_table}.#{db_field} AS decimal(60,3)) = #{value.first.to_i})"
745 763 else
746 764 sql = "#{db_table}.#{db_field} = #{value.first.to_i}"
747 765 end
748 766 when :float
749 767 if is_custom_filter
750 768 sql = "(#{db_table}.#{db_field} <> '' AND CAST(#{db_table}.#{db_field} AS decimal(60,3)) BETWEEN #{value.first.to_f - 1e-5} AND #{value.first.to_f + 1e-5})"
751 769 else
752 770 sql = "#{db_table}.#{db_field} BETWEEN #{value.first.to_f - 1e-5} AND #{value.first.to_f + 1e-5}"
753 771 end
754 772 else
755 773 sql = "#{db_table}.#{db_field} IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")"
756 774 end
757 775 else
758 776 # IN an empty set
759 777 sql = "1=0"
760 778 end
761 779 when "!"
762 780 if value.any?
763 781 sql = "(#{db_table}.#{db_field} IS NULL OR #{db_table}.#{db_field} NOT IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + "))"
764 782 else
765 783 # NOT IN an empty set
766 784 sql = "1=1"
767 785 end
768 786 when "!*"
769 787 sql = "#{db_table}.#{db_field} IS NULL"
770 788 sql << " OR #{db_table}.#{db_field} = ''" if is_custom_filter
771 789 when "*"
772 790 sql = "#{db_table}.#{db_field} IS NOT NULL"
773 791 sql << " AND #{db_table}.#{db_field} <> ''" if is_custom_filter
774 792 when ">="
775 793 if [:date, :date_past].include?(type_for(field))
776 794 sql = date_clause(db_table, db_field, (Date.parse(value.first) rescue nil), nil)
777 795 else
778 796 if is_custom_filter
779 797 sql = "(#{db_table}.#{db_field} <> '' AND CAST(#{db_table}.#{db_field} AS decimal(60,3)) >= #{value.first.to_f})"
780 798 else
781 799 sql = "#{db_table}.#{db_field} >= #{value.first.to_f}"
782 800 end
783 801 end
784 802 when "<="
785 803 if [:date, :date_past].include?(type_for(field))
786 804 sql = date_clause(db_table, db_field, nil, (Date.parse(value.first) rescue nil))
787 805 else
788 806 if is_custom_filter
789 807 sql = "(#{db_table}.#{db_field} <> '' AND CAST(#{db_table}.#{db_field} AS decimal(60,3)) <= #{value.first.to_f})"
790 808 else
791 809 sql = "#{db_table}.#{db_field} <= #{value.first.to_f}"
792 810 end
793 811 end
794 812 when "><"
795 813 if [:date, :date_past].include?(type_for(field))
796 814 sql = date_clause(db_table, db_field, (Date.parse(value[0]) rescue nil), (Date.parse(value[1]) rescue nil))
797 815 else
798 816 if is_custom_filter
799 817 sql = "(#{db_table}.#{db_field} <> '' AND CAST(#{db_table}.#{db_field} AS decimal(60,3)) BETWEEN #{value[0].to_f} AND #{value[1].to_f})"
800 818 else
801 819 sql = "#{db_table}.#{db_field} BETWEEN #{value[0].to_f} AND #{value[1].to_f}"
802 820 end
803 821 end
804 822 when "o"
805 823 sql = "#{Issue.table_name}.status_id IN (SELECT id FROM #{IssueStatus.table_name} WHERE is_closed=#{connection.quoted_false})" if field == "status_id"
806 824 when "c"
807 825 sql = "#{Issue.table_name}.status_id IN (SELECT id FROM #{IssueStatus.table_name} WHERE is_closed=#{connection.quoted_true})" if field == "status_id"
808 826 when ">t-"
809 827 sql = relative_date_clause(db_table, db_field, - value.first.to_i, 0)
810 828 when "<t-"
811 829 sql = relative_date_clause(db_table, db_field, nil, - value.first.to_i)
812 830 when "t-"
813 831 sql = relative_date_clause(db_table, db_field, - value.first.to_i, - value.first.to_i)
814 832 when ">t+"
815 833 sql = relative_date_clause(db_table, db_field, value.first.to_i, nil)
816 834 when "<t+"
817 835 sql = relative_date_clause(db_table, db_field, 0, value.first.to_i)
818 836 when "t+"
819 837 sql = relative_date_clause(db_table, db_field, value.first.to_i, value.first.to_i)
820 838 when "t"
821 839 sql = relative_date_clause(db_table, db_field, 0, 0)
822 840 when "w"
823 841 first_day_of_week = l(:general_first_day_of_week).to_i
824 842 day_of_week = Date.today.cwday
825 843 days_ago = (day_of_week >= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week)
826 844 sql = relative_date_clause(db_table, db_field, - days_ago, - days_ago + 6)
827 845 when "~"
828 846 sql = "LOWER(#{db_table}.#{db_field}) LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
829 847 when "!~"
830 848 sql = "LOWER(#{db_table}.#{db_field}) NOT LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
831 849 else
832 850 raise "Unknown query operator #{operator}"
833 851 end
834 852
835 853 return sql
836 854 end
837 855
838 856 def add_custom_fields_filters(custom_fields)
839 857 @available_filters ||= {}
840 858
841 859 custom_fields.select(&:is_filter?).each do |field|
842 860 case field.field_format
843 861 when "text"
844 862 options = { :type => :text, :order => 20 }
845 863 when "list"
846 864 options = { :type => :list_optional, :values => field.possible_values, :order => 20}
847 865 when "date"
848 866 options = { :type => :date, :order => 20 }
849 867 when "bool"
850 868 options = { :type => :list, :values => [[l(:general_text_yes), "1"], [l(:general_text_no), "0"]], :order => 20 }
851 869 when "int"
852 870 options = { :type => :integer, :order => 20 }
853 871 when "float"
854 872 options = { :type => :float, :order => 20 }
855 873 when "user", "version"
856 874 next unless project
857 875 values = field.possible_values_options(project)
858 876 if User.current.logged? && field.field_format == 'user'
859 877 values.unshift ["<< #{l(:label_me)} >>", "me"]
860 878 end
861 879 options = { :type => :list_optional, :values => values, :order => 20}
862 880 else
863 881 options = { :type => :string, :order => 20 }
864 882 end
865 883 @available_filters["cf_#{field.id}"] = options.merge({ :name => field.name, :format => field.field_format })
866 884 end
867 885 end
868 886
869 887 # Returns a SQL clause for a date or datetime field.
870 888 def date_clause(table, field, from, to)
871 889 s = []
872 890 if from
873 891 from_yesterday = from - 1
874 892 from_yesterday_time = Time.local(from_yesterday.year, from_yesterday.month, from_yesterday.day)
875 893 if self.class.default_timezone == :utc
876 894 from_yesterday_time = from_yesterday_time.utc
877 895 end
878 896 s << ("#{table}.#{field} > '%s'" % [connection.quoted_date(from_yesterday_time.end_of_day)])
879 897 end
880 898 if to
881 899 to_time = Time.local(to.year, to.month, to.day)
882 900 if self.class.default_timezone == :utc
883 901 to_time = to_time.utc
884 902 end
885 903 s << ("#{table}.#{field} <= '%s'" % [connection.quoted_date(to_time.end_of_day)])
886 904 end
887 905 s.join(' AND ')
888 906 end
889 907
890 908 # Returns a SQL clause for a date or datetime field using relative dates.
891 909 def relative_date_clause(table, field, days_from, days_to)
892 910 date_clause(table, field, (days_from ? Date.today + days_from : nil), (days_to ? Date.today + days_to : nil))
893 911 end
894 912
895 913 # Additional joins required for the given sort options
896 914 def joins_for_order_statement(order_options)
897 915 joins = []
898 916
899 917 if order_options
900 918 if order_options.include?('authors')
901 919 joins << "LEFT OUTER JOIN #{User.table_name} authors ON authors.id = #{Issue.table_name}.author_id"
902 920 end
903 921 order_options.scan(/cf_\d+/).uniq.each do |name|
904 922 column = available_columns.detect {|c| c.name.to_s == name}
905 923 join = column && column.custom_field.join_for_order_statement
906 924 if join
907 925 joins << join
908 926 end
909 927 end
910 928 end
911 929
912 930 joins.any? ? joins.join(' ') : nil
913 931 end
914 932 end
@@ -1,53 +1,27
1 <%= javascript_tag do %>
2 var operatorLabels = <%= raw Query.operators_labels.to_json %>;
3 var operatorByType = <%= raw Query.operators_by_filter_type.to_json %>;
4 var availableFilters = <%= raw query.available_filters_as_json.to_json %>;
5 var labelDayPlural = "<%= raw escape_javascript(l(:label_day_plural)) %>";
6 $(document).ready(function(){
7 initFilters();
8 <% query.filters.each do |field, options| %>
9 addFilter("<%= field %>", <%= raw query.operator_for(field).to_json %>, <%= raw query.values_for(field).to_json %>);
10 <% end %>
11 });
12 <% end %>
13
1 14 <table style="width:100%">
2 15 <tr>
3 16 <td>
4 <table>
5 <% query.available_filters.sort{|a,b| a[1][:order]<=>b[1][:order]}.each do |filter| %>
6 <% field = filter[0]
7 options = filter[1] %>
8 <tr <%= 'style="display:none;"'.html_safe unless query.has_filter?(field) %> id="tr_<%= field %>" class="filter">
9 <td class="field">
10 <%= check_box_tag 'f[]', field, query.has_filter?(field), :onclick => "toggle_filter('#{field}');", :id => "cb_#{field}" %>
11 <label for="cb_<%= field %>"><%= filter[1][:name] || l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) %></label>
12 </td>
13 <td class="operator">
14 <%= label_tag "operators_#{field}", l(:description_filter), :class => "hidden-for-sighted" %>
15 <%= select_tag "op[#{field}]", options_for_select(operators_for_select(options[:type]),
16 query.operator_for(field)), :id => "operators_#{field}",
17 :onchange => "toggle_operator('#{field}');" %>
18 </td>
19 <td class="values">
20 <div id="div_values_<%= field %>" style="display:none;">
21 <% case options[:type]
22 when :list, :list_optional, :list_status, :list_subprojects %>
23 <span class="span_values_<%= field %>">
24 <%= select_tag "v[#{field}][]", options_for_select(options[:values], query.values_for(field)), :class => "values_#{field}", :id => "values_#{field}_1", :multiple => (query.values_for(field) && query.values_for(field).length > 1) %>
25 <%= link_to_function image_tag('bullet_toggle_plus.png'), "toggle_multi_select('values_#{field}_1');" %>
26 </span>
27 <% when :date, :date_past %>
28 <span class="span_values_<%= field %>"><%= text_field_tag "v[#{field}][]", query.value_for(field), :size => 10, :class => "values_#{field}", :id => "values_#{field}_1" %> <%= calendar_for "values_#{field}_1" %></span>
29 <span class="span_values_<%= field %>"><%= text_field_tag "v[#{field}][]", query.value_for(field, 1), :size => 10, :class => "values_#{field}", :id => "values_#{field}_2" %> <%= calendar_for "values_#{field}_2" %></span>
30 <span class="span_values_<%= field %>"><%= text_field_tag "v[#{field}][]", query.value_for(field), :size => 3, :class => "values_#{field}" %> <%= l(:label_day_plural) %></span>
31 <% when :string, :text %>
32 <span class="span_values_<%= field %>"><%= text_field_tag "v[#{field}][]", query.value_for(field), :class => "values_#{field}", :id => "values_#{field}", :size => 30 %></span>
33 <% when :integer, :float %>
34 <span class="span_values_<%= field %>"><%= text_field_tag "v[#{field}][]", query.value_for(field), :class => "values_#{field}", :id => "values_#{field}_1", :size => 6 %></span>
35 <span class="span_values_<%= field %>"><%= text_field_tag "v[#{field}][]", query.value_for(field, 1), :class => "values_#{field}", :id => "values_#{field}_2", :size => 6 %></span>
36 <% end %>
37 </div>
38 <script type="text/javascript">toggle_filter('<%= field %>');</script>
39 </td>
40 </tr>
41 <% end %>
17 <table id="filters-table">
42 18 </table>
43 19 </td>
44 20 <td class="add-filter">
45 21 <%= label_tag('add_filter_select', l(:label_filter_add)) %>
46 <%= select_tag 'add_filter_select', options_for_select([["",""]] + query.available_filters.sort{|a,b| a[1][:order]<=>b[1][:order]}.collect{|field| [ field[1][:name] || l(("field_"+field[0].to_s.gsub(/_id$/, "")).to_sym), field[0]] unless query.has_filter?(field[0])}.compact),
47 :onchange => "add_filter();",
48 :name => nil %>
22 <%= select_tag 'add_filter_select', filters_options_for_select(query), :name => nil %>
49 23 </td>
50 24 </tr>
51 25 </table>
52 26 <%= hidden_field_tag 'f[]', '' %>
53 <%= javascript_tag '$(document).ready(function(){observeIssueFilters();});' %>
27 <% include_calendar_headers_tags %>
@@ -1,52 +1,52
1 1 <%= error_messages_for 'query' %>
2 2
3 3 <div class="box">
4 4 <div class="tabular">
5 5 <p><label for="query_name"><%=l(:field_name)%></label>
6 6 <%= text_field 'query', 'name', :size => 80 %></p>
7 7
8 8 <% if User.current.admin? || User.current.allowed_to?(:manage_public_queries, @project) %>
9 9 <p><label for="query_is_public"><%=l(:field_is_public)%></label>
10 10 <%= check_box 'query', 'is_public',
11 11 :onchange => (User.current.admin? ? nil : 'if (this.checked) {$("#query_is_for_all").removeAttr("checked"); $("#query_is_for_all").attr("disabled", true);} else {$("#query_is_for_all").removeAttr("disabled");}') %></p>
12 12 <% end %>
13 13
14 14 <p><label for="query_is_for_all"><%=l(:field_is_for_all)%></label>
15 15 <%= check_box_tag 'query_is_for_all', 1, @query.project.nil?,
16 16 :disabled => (!@query.new_record? && (@query.project.nil? || (@query.is_public? && !User.current.admin?))) %></p>
17 17
18 18 <p><label for="query_default_columns"><%=l(:label_default_columns)%></label>
19 19 <%= check_box_tag 'default_columns', 1, @query.has_default_columns?, :id => 'query_default_columns',
20 20 :onclick => 'if (this.checked) {$("#columns").hide();} else {$("#columns").show();}' %></p>
21 21
22 22 <p><label for="query_group_by"><%= l(:field_group_by) %></label>
23 23 <%= select 'query', 'group_by', @query.groupable_columns.collect {|c| [c.caption, c.name.to_s]}, :include_blank => true %></p>
24 24 </div>
25 25
26 <fieldset><legend><%= l(:label_filter_plural) %></legend>
26 <fieldset id="filters"><legend><%= l(:label_filter_plural) %></legend>
27 27 <%= render :partial => 'queries/filters', :locals => {:query => query}%>
28 28 </fieldset>
29 29
30 30 <fieldset><legend><%= l(:label_sort) %></legend>
31 31 <% 3.times do |i| %>
32 32 <%= i+1 %>:
33 33 <%= label_tag "query_sort_criteria_attribute_" + i.to_s,
34 34 l(:description_query_sort_criteria_attribute), :class => "hidden-for-sighted" %>
35 35 <%= select_tag("query[sort_criteria][#{i}][]",
36 36 options_for_select([[]] + query.available_columns.select(&:sortable?).collect {|column| [column.caption, column.name.to_s]}, @query.sort_criteria_key(i)),
37 37 :id => "query_sort_criteria_attribute_" + i.to_s)%>
38 38 <%= label_tag "query_sort_criteria_direction_" + i.to_s,
39 39 l(:description_query_sort_criteria_direction), :class => "hidden-for-sighted" %>
40 40 <%= select_tag("query[sort_criteria][#{i}][]",
41 41 options_for_select([[], [l(:label_ascending), 'asc'], [l(:label_descending), 'desc']], @query.sort_criteria_order(i)),
42 42 :id => "query_sort_criteria_direction_" + i.to_s) %>
43 43 <br />
44 44 <% end %>
45 45 </fieldset>
46 46
47 47 <%= content_tag 'fieldset', :id => 'columns', :style => (query.has_default_columns? ? 'display:none;' : nil) do %>
48 48 <legend><%= l(:field_column_names) %></legend>
49 49 <%= render :partial => 'queries/columns', :locals => {:query => query}%>
50 50 <% end %>
51 51
52 52 </div>
@@ -1,499 +1,583
1 1 /* Redmine - project management software
2 2 Copyright (C) 2006-2012 Jean-Philippe Lang */
3 3
4 4 function checkAll(id, checked) {
5 5 if (checked) {
6 6 $('#'+id).find('input[type=checkbox]').attr('checked', true);
7 7 } else {
8 8 $('#'+id).find('input[type=checkbox]').removeAttr('checked');
9 9 }
10 10 }
11 11
12 12 function toggleCheckboxesBySelector(selector) {
13 13 var all_checked = true;
14 14 $(selector).each(function(index) {
15 15 if (!$(this).is(':checked')) { all_checked = false; }
16 16 });
17 17 $(selector).attr('checked', !all_checked)
18 18 }
19 19
20 20 function showAndScrollTo(id, focus) {
21 21 $('#'+id).show();
22 22 if (focus!=null) {
23 23 $('#'+focus).focus();
24 24 }
25 25 $('html, body').animate({scrollTop: $('#'+id).offset().top}, 100);
26 26 }
27 27
28 28 function toggleRowGroup(el) {
29 29 var tr = $(el).parents('tr').first();
30 30 var n = tr.next();
31 31 tr.toggleClass('open');
32 32 while (n.length && !n.hasClass('group')) {
33 33 n.toggle();
34 34 n = n.next('tr');
35 35 }
36 36 }
37 37
38 38 function collapseAllRowGroups(el) {
39 39 var tbody = $(el).parents('tbody').first();
40 40 tbody.children('tr').each(function(index) {
41 41 if ($(this).hasClass('group')) {
42 42 $(this).removeClass('open');
43 43 } else {
44 44 $(this).hide();
45 45 }
46 46 });
47 47 }
48 48
49 49 function expandAllRowGroups(el) {
50 50 var tbody = $(el).parents('tbody').first();
51 51 tbody.children('tr').each(function(index) {
52 52 if ($(this).hasClass('group')) {
53 53 $(this).addClass('open');
54 54 } else {
55 55 $(this).show();
56 56 }
57 57 });
58 58 }
59 59
60 60 function toggleAllRowGroups(el) {
61 61 var tr = $(el).parents('tr').first();
62 62 if (tr.hasClass('open')) {
63 63 collapseAllRowGroups(el);
64 64 } else {
65 65 expandAllRowGroups(el);
66 66 }
67 67 }
68 68
69 69 function toggleFieldset(el) {
70 70 var fieldset = $(el).parents('fieldset').first();
71 71 fieldset.toggleClass('collapsed');
72 72 fieldset.children('div').toggle();
73 73 }
74 74
75 75 function hideFieldset(el) {
76 76 var fieldset = $(el).parents('fieldset').first();
77 77 fieldset.toggleClass('collapsed');
78 78 fieldset.children('div').hide();
79 79 }
80 80
81 function add_filter() {
82 var select = $('#add_filter_select');
83 var field = select.val();
84 $('#tr_'+field).show();
85 var check_box = $('#cb_' + field);
86 check_box.attr('checked', true);
87 toggle_filter(field);
88 select.val('');
89
90 select.children('option').each(function(index) {
81 function initFilters(){
82 $('#add_filter_select').change(function(){
83 addFilter($(this).val(), '', []);
84 });
85 $('#filters-table td.field input[type=checkbox]').each(function(){
86 toggleFilter($(this).val());
87 });
88 $('#filters-table td.field input[type=checkbox]').live('click',function(){
89 toggleFilter($(this).val());
90 });
91 $('#filters-table .toggle-multiselect').live('click',function(){
92 toggleMultiSelect($(this).siblings('select'));
93 });
94 $('#filters-table input[type=text]').live('keypress', function(e){
95 if (e.keyCode == 13) submit_query_form("query_form");
96 });
97 }
98
99 function addFilter(field, operator, values) {
100 var fieldId = field.replace('.', '_');
101 var tr = $('#tr_'+fieldId);
102 if (tr.length > 0) {
103 tr.show();
104 } else {
105 buildFilterRow(field, operator, values);
106 }
107 $('#cb_'+fieldId).attr('checked', true);
108 toggleFilter(field);
109 $('#add_filter_select').val('').children('option').each(function(){
91 110 if ($(this).attr('value') == field) {
92 111 $(this).attr('disabled', true);
93 112 }
94 113 });
95 114 }
96 115
97 function toggle_filter(field) {
98 check_box = $('#cb_' + field);
99 if (check_box.is(':checked')) {
100 $("#operators_" + field).show().removeAttr('disabled');
101 toggle_operator(field);
116 function buildFilterRow(field, operator, values) {
117 var fieldId = field.replace('.', '_');
118 var filterTable = $("#filters-table");
119 var filterOptions = availableFilters[field];
120 var operators = operatorByType[filterOptions['type']];
121 var filterValues = filterOptions['values'];
122 var i, select;
123
124 var tr = $('<tr class="filter">').attr('id', 'tr_'+fieldId).html(
125 '<td class="field"><input checked="checked" id="cb_'+fieldId+'" name="f[]" value="'+field+'" type="checkbox"><label for="cb_'+fieldId+'"> '+filterOptions['name']+'</label></td>' +
126 '<td class="operator"><select id="operators_'+fieldId+'" name="op['+field+']"></td>' +
127 '<td class="values"></td>'
128 );
129 filterTable.append(tr);
130
131 select = tr.find('td.operator select');
132 for (i=0;i<operators.length;i++){
133 var option = $('<option>').val(operators[i]).html(operatorLabels[operators[i]]);
134 if (operators[i] == operator) {option.attr('selected', true)};
135 select.append(option);
136 }
137 select.change(function(){toggleOperator(field)});
138
139 switch (filterOptions['type']){
140 case "list":
141 case "list_optional":
142 case "list_status":
143 case "list_subprojects":
144 tr.find('td.values').append(
145 '<span style="display:none;"><select class="value" id="values_'+fieldId+'_1" name="v['+field+'][]"></select>' +
146 ' <span class="toggle-multiselect">&nbsp;</span></span>'
147 );
148 select = tr.find('td.values select');
149 if (values.length > 1) {select.attr('multiple', true)};
150 for (i=0;i<filterValues.length;i++){
151 var filterValue = filterValues[i];
152 var option = $('<option>');
153 if ($.isArray(filterValue)) {
154 option.val(filterValue[1]).html(filterValue[0]);
155 } else {
156 option.val(filterValue).html(filterValue);
157 }
158 if (values.indexOf(filterValues[i][1]) > -1) {option.attr('selected', true)};
159 select.append(option);
160 }
161 break;
162 case "date":
163 case "date_past":
164 tr.find('td.values').append(
165 '<span style="display:none;"><input type="text" name="v['+field+'][]" id="values_'+fieldId+'_1" size="10" class="value date_value" value="'+values[0]+'" /></span>' +
166 ' <span style="display:none;"><input type="text" name="v['+field+'][]" id="values_'+fieldId+'_2" size="10" class="value date_value" value="'+values[1]+'" /></span>' +
167 ' <span style="display:none;"><input type="text" name="v['+field+'][]" id="values_'+fieldId+'" size="3" class="value" value="'+values[0]+'" /> '+labelDayPlural+'</span>'
168 );
169 $('#values_'+fieldId+'_1').val(values[0]).datepicker(datepickerOptions);
170 $('#values_'+fieldId+'_2').val(values[1]).datepicker(datepickerOptions);
171 $('#values_'+fieldId).val(values[0]);
172 break;
173 case "string":
174 case "text":
175 tr.find('td.values').append(
176 '<span style="display:none;"><input type="text" name="v['+field+'][]" id="values_'+fieldId+'" size="30" class="value" value="'+values[0]+'" /></span>'
177 );
178 $('#values_'+fieldId).val(values[0]);
179 break;
180 case "integer":
181 case "float":
182 tr.find('td.values').append(
183 '<span style="display:none;"><input type="text" name="v['+field+'][]" id="values_'+fieldId+'_1" size="6" class="value" value="'+values[0]+'" /></span>' +
184 ' <span style="display:none;"><input type="text" name="v['+field+'][]" id="values_'+fieldId+'_2" size="6" class="value" value="'+values[1]+'" /></span>'
185 );
186 $('#values_'+fieldId+'_1').val(values[0]);
187 $('#values_'+fieldId+'_2').val(values[1]);
188 break;
189 }
190 }
191
192 function toggleFilter(field) {
193 var fieldId = field.replace('.', '_');
194 if ($('#cb_' + fieldId).is(':checked')) {
195 $("#operators_" + fieldId).show().removeAttr('disabled');
196 toggleOperator(field);
102 197 } else {
103 $("#operators_" + field).hide().attr('disabled', true);
198 $("#operators_" + fieldId).hide().attr('disabled', true);
104 199 enableValues(field, []);
105 200 }
106 201 }
107 202
108 203 function enableValues(field, indexes) {
109 $(".values_" + field).each(function(index) {
204 var fieldId = field.replace('.', '_');
205 $('#tr_'+fieldId+' td.values .value').each(function(index) {
110 206 if (indexes.indexOf(index) >= 0) {
111 207 $(this).removeAttr('disabled');
112 208 $(this).parents('span').first().show();
113 209 } else {
114 210 $(this).val('');
115 211 $(this).attr('disabled', true);
116 212 $(this).parents('span').first().hide();
117 213 }
118 214
119 215 if ($(this).hasClass('group')) {
120 216 $(this).addClass('open');
121 217 } else {
122 218 $(this).show();
123 219 }
124 220 });
125
126 if (indexes.length > 0) {
127 $("#div_values_" + field).show();
128 } else {
129 $("#div_values_" + field).hide();
130 }
131 221 }
132 222
133 function toggle_operator(field) {
134 operator = $("#operators_" + field);
223 function toggleOperator(field) {
224 var fieldId = field.replace('.', '_');
225 var operator = $("#operators_" + fieldId);
135 226 switch (operator.val()) {
136 227 case "!*":
137 228 case "*":
138 229 case "t":
139 230 case "w":
140 231 case "o":
141 232 case "c":
142 233 enableValues(field, []);
143 234 break;
144 235 case "><":
145 236 enableValues(field, [0,1]);
146 237 break;
147 238 case "<t+":
148 239 case ">t+":
149 240 case "t+":
150 241 case ">t-":
151 242 case "<t-":
152 243 case "t-":
153 244 enableValues(field, [2]);
154 245 break;
155 246 default:
156 247 enableValues(field, [0]);
157 248 break;
158 249 }
159 250 }
160 251
161 function toggle_multi_select(id) {
162 var select = $('#'+id);
163 if (select.attr('multiple')) {
164 select.removeAttr('multiple');
252 function toggleMultiSelect(el) {
253 if (el.attr('multiple')) {
254 el.removeAttr('multiple');
165 255 } else {
166 select.attr('multiple', true);
256 el.attr('multiple', true);
167 257 }
168 258 }
169 259
170 260 function submit_query_form(id) {
171 261 selectAllOptions("selected_columns");
172 262 $('#'+id).submit();
173 263 }
174 264
175 function observeIssueFilters() {
176 $('#query_form input[type=text]').keypress(function(e){
177 if (e.keyCode == 13) submit_query_form("query_form");
178 });
179 }
180
181 265 var fileFieldCount = 1;
182 266 function addFileField() {
183 267 var fields = $('#attachments_fields');
184 268 if (fields.children().length >= 10) return false;
185 269 fileFieldCount++;
186 270 var s = fields.children('span').first().clone();
187 271 s.children('input.file').attr('name', "attachments[" + fileFieldCount + "][file]").val('');
188 272 s.children('input.description').attr('name', "attachments[" + fileFieldCount + "][description]").val('');
189 273 fields.append(s);
190 274 }
191 275
192 276 function removeFileField(el) {
193 277 var fields = $('#attachments_fields');
194 278 var s = $(el).parents('span').first();
195 279 if (fields.children().length > 1) {
196 280 s.remove();
197 281 } else {
198 282 s.children('input.file').val('');
199 283 s.children('input.description').val('');
200 284 }
201 285 }
202 286
203 287 function checkFileSize(el, maxSize, message) {
204 288 var files = el.files;
205 289 if (files) {
206 290 for (var i=0; i<files.length; i++) {
207 291 if (files[i].size > maxSize) {
208 292 alert(message);
209 293 el.value = "";
210 294 }
211 295 }
212 296 }
213 297 }
214 298
215 299 function showTab(name) {
216 300 $('div#content .tab-content').hide();
217 301 $('div.tabs a').removeClass('selected');
218 302 $('#tab-content-' + name).show();
219 303 $('#tab-' + name).addClass('selected');
220 304 return false;
221 305 }
222 306
223 307 function moveTabRight(el) {
224 308 var lis = $(el).parents('div.tabs').first().find('ul').children();
225 309 var tabsWidth = 0;
226 310 var i = 0;
227 311 lis.each(function(){
228 312 if ($(this).is(':visible')) {
229 313 tabsWidth += $(this).width() + 6;
230 314 }
231 315 });
232 316 if (tabsWidth < $(el).parents('div.tabs').first().width() - 60) { return; }
233 317 while (i<lis.length && !lis.eq(i).is(':visible')) { i++; }
234 318 lis.eq(i).hide();
235 319 }
236 320
237 321 function moveTabLeft(el) {
238 322 var lis = $(el).parents('div.tabs').first().find('ul').children();
239 323 var i = 0;
240 324 while (i<lis.length && !lis.eq(i).is(':visible')) { i++; }
241 325 if (i>0) {
242 326 lis.eq(i-1).show();
243 327 }
244 328 }
245 329
246 330 function displayTabsButtons() {
247 331 var lis;
248 332 var tabsWidth = 0;
249 333 var el;
250 334 $('div.tabs').each(function() {
251 335 el = $(this);
252 336 lis = el.find('ul').children();
253 337 lis.each(function(){
254 338 if ($(this).is(':visible')) {
255 339 tabsWidth += $(this).width() + 6;
256 340 }
257 341 });
258 342 if ((tabsWidth < el.width() - 60) && (lis.first().is(':visible'))) {
259 343 el.find('div.tabs-buttons').hide();
260 344 } else {
261 345 el.find('div.tabs-buttons').show();
262 346 }
263 347 });
264 348 }
265 349
266 350 function setPredecessorFieldsVisibility() {
267 351 var relationType = $('#relation_relation_type');
268 352 if (relationType.val() == "precedes" || relationType.val() == "follows") {
269 353 $('#predecessor_fields').show();
270 354 } else {
271 355 $('#predecessor_fields').hide();
272 356 }
273 357 }
274 358
275 359 function showModal(id, width) {
276 360 var el = $('#'+id).first();
277 361 if (el.length == 0 || el.is(':visible')) {return;}
278 362 var title = el.find('h3.title').text();
279 363 el.dialog({
280 364 width: width,
281 365 modal: true,
282 366 resizable: false,
283 367 dialogClass: 'modal',
284 368 title: title
285 369 });
286 370 el.find("input[type=text], input[type=submit]").first().focus();
287 371 }
288 372
289 373 function hideModal(el) {
290 374 var modal;
291 375 if (el) {
292 376 modal = $(el).parents('.ui-dialog-content');
293 377 } else {
294 378 modal = $('#ajax-modal');
295 379 }
296 380 modal.dialog("close");
297 381 }
298 382
299 383 function submitPreview(url, form, target) {
300 384 $.ajax({
301 385 url: url,
302 386 type: 'post',
303 387 data: $('#'+form).serialize(),
304 388 success: function(data){
305 389 $('#'+target).html(data);
306 390 }
307 391 });
308 392 }
309 393
310 394 function collapseScmEntry(id) {
311 395 $('.'+id).each(function() {
312 396 if ($(this).hasClass('open')) {
313 397 collapseScmEntry($(this).attr('id'));
314 398 }
315 399 $(this).hide();
316 400 });
317 401 $('#'+id).removeClass('open');
318 402 }
319 403
320 404 function expandScmEntry(id) {
321 405 $('.'+id).each(function() {
322 406 $(this).show();
323 407 if ($(this).hasClass('loaded') && !$(this).hasClass('collapsed')) {
324 408 expandScmEntry($(this).attr('id'));
325 409 }
326 410 });
327 411 $('#'+id).addClass('open');
328 412 }
329 413
330 414 function scmEntryClick(id, url) {
331 415 el = $('#'+id);
332 416 if (el.hasClass('open')) {
333 417 collapseScmEntry(id);
334 418 el.addClass('collapsed');
335 419 return false;
336 420 } else if (el.hasClass('loaded')) {
337 421 expandScmEntry(id);
338 422 el.removeClass('collapsed');
339 423 return false;
340 424 }
341 425 if (el.hasClass('loading')) {
342 426 return false;
343 427 }
344 428 el.addClass('loading');
345 429 $.ajax({
346 430 url: url,
347 431 success: function(data){
348 432 el.after(data);
349 433 el.addClass('open').addClass('loaded').removeClass('loading');
350 434 }
351 435 });
352 436 return true;
353 437 }
354 438
355 439 function randomKey(size) {
356 440 var chars = new Array('0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z');
357 441 var key = '';
358 442 for (i = 0; i < size; i++) {
359 443 key += chars[Math.floor(Math.random() * chars.length)];
360 444 }
361 445 return key;
362 446 }
363 447
364 448 // Can't use Rails' remote select because we need the form data
365 449 function updateIssueFrom(url) {
366 450 $.ajax({
367 451 url: url,
368 452 type: 'post',
369 453 data: $('#issue-form').serialize()
370 454 });
371 455 }
372 456
373 457 function updateBulkEditFrom(url) {
374 458 $.ajax({
375 459 url: url,
376 460 type: 'post',
377 461 data: $('#bulk_edit_form').serialize()
378 462 });
379 463 }
380 464
381 465 function observeAutocompleteField(fieldId, url) {
382 466 $('#'+fieldId).autocomplete({
383 467 source: url,
384 468 minLength: 2,
385 469 });
386 470 }
387 471
388 472 function observeSearchfield(fieldId, targetId, url) {
389 473 $('#'+fieldId).each(function() {
390 474 var $this = $(this);
391 475 $this.attr('data-value-was', $this.val());
392 476 var check = function() {
393 477 var val = $this.val();
394 478 if ($this.attr('data-value-was') != val){
395 479 $this.attr('data-value-was', val);
396 480 if (val != '') {
397 481 $.ajax({
398 482 url: url,
399 483 type: 'get',
400 484 data: {q: $this.val()},
401 485 success: function(data){ $('#'+targetId).html(data); },
402 486 beforeSend: function(){ $this.addClass('ajax-loading'); },
403 487 complete: function(){ $this.removeClass('ajax-loading'); }
404 488 });
405 489 }
406 490 }
407 491 };
408 492 var reset = function() {
409 493 if (timer) {
410 494 clearInterval(timer);
411 495 timer = setInterval(check, 300);
412 496 }
413 497 };
414 498 var timer = setInterval(check, 300);
415 499 $this.bind('keyup click mousemove', reset);
416 500 });
417 501 }
418 502
419 503 function observeProjectModules() {
420 504 var f = function() {
421 505 /* Hides trackers and issues custom fields on the new project form when issue_tracking module is disabled */
422 506 if ($('#project_enabled_module_names_issue_tracking').attr('checked')) {
423 507 $('#project_trackers').show();
424 508 }else{
425 509 $('#project_trackers').hide();
426 510 }
427 511 };
428 512
429 513 $(window).load(f);
430 514 $('#project_enabled_module_names_issue_tracking').change(f);
431 515 }
432 516
433 517 function initMyPageSortable(list, url) {
434 518 $('#list-'+list).sortable({
435 519 connectWith: '.block-receiver',
436 520 tolerance: 'pointer',
437 521 update: function(){
438 522 $.ajax({
439 523 url: url,
440 524 type: 'post',
441 525 data: {'blocks': $.map($('#list-'+list).children(), function(el){return $(el).attr('id');})}
442 526 });
443 527 }
444 528 });
445 529 $("#list-top, #list-left, #list-right").disableSelection();
446 530 }
447 531
448 532 var warnLeavingUnsavedMessage;
449 533 function warnLeavingUnsaved(message) {
450 534 warnLeavingUnsavedMessage = message;
451 535
452 536 $('form').submit(function(){
453 537 $('textarea').removeData('changed');
454 538 });
455 539 $('textarea').change(function(){
456 540 $(this).data('changed', 'changed');
457 541 });
458 542 window.onbeforeunload = function(){
459 543 var warn = false;
460 544 $('textarea').blur().each(function(){
461 545 if ($(this).data('changed')) {
462 546 warn = true;
463 547 }
464 548 });
465 549 if (warn) {return warnLeavingUnsavedMessage;}
466 550 };
467 551 };
468 552
469 553 $(document).ready(function(){
470 554 $('#ajax-indicator').bind('ajaxSend', function(){
471 555 if ($('.ajax-loading').length == 0) {
472 556 $('#ajax-indicator').show();
473 557 }
474 558 });
475 559 $('#ajax-indicator').bind('ajaxStop', function(){
476 560 $('#ajax-indicator').hide();
477 561 });
478 562 });
479 563
480 564 function hideOnLoad() {
481 565 $('.hol').hide();
482 566 }
483 567
484 568 function addFormObserversForDoubleSubmit() {
485 569 $('form[method=post]').each(function() {
486 570 if (!$(this).hasClass('multiple-submit')) {
487 571 $(this).submit(function(form_submission) {
488 572 if ($(form_submission.target).attr('data-submitted')) {
489 573 form_submission.preventDefault();
490 574 } else {
491 575 $(form_submission.target).attr('data-submitted', true);
492 576 }
493 577 });
494 578 }
495 579 });
496 580 }
497 581
498 582 $(document).ready(hideOnLoad);
499 583 $(document).ready(addFormObserversForDoubleSubmit);
@@ -1,1124 +1,1125
1 1 html {overflow-y:scroll;}
2 2 body { font-family: Verdana, sans-serif; font-size: 12px; color:#484848; margin: 0; padding: 0; min-width: 900px; }
3 3
4 4 h1, h2, h3, h4 { font-family: "Trebuchet MS", Verdana, sans-serif;}
5 5 h1 {margin:0; padding:0; font-size: 24px;}
6 6 h2, .wiki h1 {font-size: 20px;padding: 2px 10px 1px 0px;margin: 0 0 10px 0; color: #444;}
7 7 h3, .wiki h2 {font-size: 16px;padding: 2px 10px 1px 0px;margin: 0 0 10px 0; color: #444;}
8 8 h4, .wiki h3 {font-size: 13px;padding: 2px 10px 1px 0px;margin-bottom: 5px; border-bottom: 1px dotted #bbbbbb; color: #444;}
9 9
10 10 /***** Layout *****/
11 11 #wrapper {background: white;}
12 12
13 13 #top-menu {background: #3E5B76; color: #fff; height:1.8em; font-size: 0.8em; padding: 2px 2px 0px 6px;}
14 14 #top-menu ul {margin: 0; padding: 0;}
15 15 #top-menu li {
16 16 float:left;
17 17 list-style-type:none;
18 18 margin: 0px 0px 0px 0px;
19 19 padding: 0px 0px 0px 0px;
20 20 white-space:nowrap;
21 21 }
22 22 #top-menu a {color: #fff; margin-right: 8px; font-weight: bold;}
23 23 #top-menu #loggedas { float: right; margin-right: 0.5em; color: #fff; }
24 24
25 25 #account {float:right;}
26 26
27 27 #header {height:5.3em;margin:0;background-color:#628DB6;color:#f8f8f8; padding: 4px 8px 0px 6px; position:relative;}
28 28 #header a {color:#f8f8f8;}
29 29 #header h1 a.ancestor { font-size: 80%; }
30 30 #quick-search {float:right;}
31 31
32 32 #main-menu {position: absolute; bottom: 0px; left:6px; margin-right: -500px;}
33 33 #main-menu ul {margin: 0; padding: 0;}
34 34 #main-menu li {
35 35 float:left;
36 36 list-style-type:none;
37 37 margin: 0px 2px 0px 0px;
38 38 padding: 0px 0px 0px 0px;
39 39 white-space:nowrap;
40 40 }
41 41 #main-menu li a {
42 42 display: block;
43 43 color: #fff;
44 44 text-decoration: none;
45 45 font-weight: bold;
46 46 margin: 0;
47 47 padding: 4px 10px 4px 10px;
48 48 }
49 49 #main-menu li a:hover {background:#759FCF; color:#fff;}
50 50 #main-menu li a.selected, #main-menu li a.selected:hover {background:#fff; color:#555;}
51 51
52 52 #admin-menu ul {margin: 0; padding: 0;}
53 53 #admin-menu li {margin: 0; padding: 0 0 6px 0; list-style-type:none;}
54 54
55 55 #admin-menu a { background-position: 0% 40%; background-repeat: no-repeat; padding-left: 20px; padding-top: 2px; padding-bottom: 3px;}
56 56 #admin-menu a.projects { background-image: url(../images/projects.png); }
57 57 #admin-menu a.users { background-image: url(../images/user.png); }
58 58 #admin-menu a.groups { background-image: url(../images/group.png); }
59 59 #admin-menu a.roles { background-image: url(../images/database_key.png); }
60 60 #admin-menu a.trackers { background-image: url(../images/ticket.png); }
61 61 #admin-menu a.issue_statuses { background-image: url(../images/ticket_edit.png); }
62 62 #admin-menu a.workflows { background-image: url(../images/ticket_go.png); }
63 63 #admin-menu a.custom_fields { background-image: url(../images/textfield.png); }
64 64 #admin-menu a.enumerations { background-image: url(../images/text_list_bullets.png); }
65 65 #admin-menu a.settings { background-image: url(../images/changeset.png); }
66 66 #admin-menu a.plugins { background-image: url(../images/plugin.png); }
67 67 #admin-menu a.info { background-image: url(../images/help.png); }
68 68 #admin-menu a.server_authentication { background-image: url(../images/server_key.png); }
69 69
70 70 #main {background-color:#EEEEEE;}
71 71
72 72 #sidebar{ float: right; width: 22%; position: relative; z-index: 9; padding: 0; margin: 0;}
73 73 * html #sidebar{ width: 22%; }
74 74 #sidebar h3{ font-size: 14px; margin-top:14px; color: #666; }
75 75 #sidebar hr{ width: 100%; margin: 0 auto; height: 1px; background: #ccc; border: 0; }
76 76 * html #sidebar hr{ width: 95%; position: relative; left: -6px; color: #ccc; }
77 77 #sidebar .contextual { margin-right: 1em; }
78 78
79 79 #content { width: 75%; background-color: #fff; margin: 0px; border-right: 1px solid #ddd; padding: 6px 10px 10px 10px; z-index: 10; }
80 80 * html #content{ width: 75%; padding-left: 0; margin-top: 0px; padding: 6px 10px 10px 10px;}
81 81 html>body #content { min-height: 600px; }
82 82 * html body #content { height: 600px; } /* IE */
83 83
84 84 #main.nosidebar #sidebar{ display: none; }
85 85 #main.nosidebar #content{ width: auto; border-right: 0; }
86 86
87 87 #footer {clear: both; border-top: 1px solid #bbb; font-size: 0.9em; color: #aaa; padding: 5px; text-align:center; background:#fff;}
88 88
89 89 #login-form table {margin-top:5em; padding:1em; margin-left: auto; margin-right: auto; border: 2px solid #FDBF3B; background-color:#FFEBC1; }
90 90 #login-form table td {padding: 6px;}
91 91 #login-form label {font-weight: bold;}
92 92 #login-form input#username, #login-form input#password { width: 300px; }
93 93
94 94 div.modal { border-radius:5px; background:#fff; z-index:50; padding:4px;}
95 95 div.modal h3.title {display:none;}
96 96 div.modal p.buttons {text-align:right; margin-bottom:0;}
97 97
98 98 input#openid_url { background: url(../images/openid-bg.gif) no-repeat; background-color: #fff; background-position: 0 50%; padding-left: 18px; }
99 99
100 100 .clear:after{ content: "."; display: block; height: 0; clear: both; visibility: hidden; }
101 101
102 102 /***** Links *****/
103 103 a, a:link, a:visited{ color: #169; text-decoration: none; }
104 104 a:hover, a:active{ color: #c61a1a; text-decoration: underline;}
105 105 a img{ border: 0; }
106 106
107 107 a.issue.closed, a.issue.closed:link, a.issue.closed:visited { color: #999; text-decoration: line-through; }
108 108 a.project.closed, a.project.closed:link, a.project.closed:visited { color: #999; }
109 109
110 110 #sidebar a.selected {line-height:1.7em; padding:1px 3px 2px 2px; margin-left:-2px; background-color:#9DB9D5; color:#fff; border-radius:2px;}
111 111 #sidebar a.selected:hover {text-decoration:none;}
112 112 #admin-menu a {line-height:1.7em;}
113 113 #admin-menu a.selected {padding-left: 20px !important; background-position: 2px 40%;}
114 114
115 115 a.collapsible {padding-left: 12px; background: url(../images/arrow_expanded.png) no-repeat -3px 40%;}
116 116 a.collapsible.collapsed {background: url(../images/arrow_collapsed.png) no-repeat -5px 40%;}
117 117
118 118 a#toggle-completed-versions {color:#999;}
119 119 /***** Tables *****/
120 120 table.list { border: 1px solid #e4e4e4; border-collapse: collapse; width: 100%; margin-bottom: 4px; }
121 121 table.list th { background-color:#EEEEEE; padding: 4px; white-space:nowrap; }
122 122 table.list td { vertical-align: top; }
123 123 table.list td.id { width: 2%; text-align: center;}
124 124 table.list td.checkbox { width: 15px; padding: 2px 0 0 0; }
125 125 table.list td.checkbox input {padding:0px;}
126 126 table.list td.buttons { width: 15%; white-space:nowrap; text-align: right; }
127 127 table.list td.buttons a { padding-right: 0.6em; }
128 128 table.list caption { text-align: left; padding: 0.5em 0.5em 0.5em 0; }
129 129
130 130 tr.project td.name a { white-space:nowrap; }
131 131 tr.project.closed, tr.project.archived { color: #aaa; }
132 132 tr.project.closed a, tr.project.archived a { color: #aaa; }
133 133
134 134 tr.project.idnt td.name span {background: url(../images/bullet_arrow_right.png) no-repeat 0 50%; padding-left: 16px;}
135 135 tr.project.idnt-1 td.name {padding-left: 0.5em;}
136 136 tr.project.idnt-2 td.name {padding-left: 2em;}
137 137 tr.project.idnt-3 td.name {padding-left: 3.5em;}
138 138 tr.project.idnt-4 td.name {padding-left: 5em;}
139 139 tr.project.idnt-5 td.name {padding-left: 6.5em;}
140 140 tr.project.idnt-6 td.name {padding-left: 8em;}
141 141 tr.project.idnt-7 td.name {padding-left: 9.5em;}
142 142 tr.project.idnt-8 td.name {padding-left: 11em;}
143 143 tr.project.idnt-9 td.name {padding-left: 12.5em;}
144 144
145 145 tr.issue { text-align: center; white-space: nowrap; }
146 146 tr.issue td.subject, tr.issue td.category, td.assigned_to, tr.issue td.string, tr.issue td.text { white-space: normal; }
147 147 tr.issue td.subject { text-align: left; }
148 148 tr.issue td.done_ratio table.progress { margin-left:auto; margin-right: auto;}
149 149
150 150 tr.issue.idnt td.subject a {background: url(../images/bullet_arrow_right.png) no-repeat 0 50%; padding-left: 16px;}
151 151 tr.issue.idnt-1 td.subject {padding-left: 0.5em;}
152 152 tr.issue.idnt-2 td.subject {padding-left: 2em;}
153 153 tr.issue.idnt-3 td.subject {padding-left: 3.5em;}
154 154 tr.issue.idnt-4 td.subject {padding-left: 5em;}
155 155 tr.issue.idnt-5 td.subject {padding-left: 6.5em;}
156 156 tr.issue.idnt-6 td.subject {padding-left: 8em;}
157 157 tr.issue.idnt-7 td.subject {padding-left: 9.5em;}
158 158 tr.issue.idnt-8 td.subject {padding-left: 11em;}
159 159 tr.issue.idnt-9 td.subject {padding-left: 12.5em;}
160 160
161 161 tr.entry { border: 1px solid #f8f8f8; }
162 162 tr.entry td { white-space: nowrap; }
163 163 tr.entry td.filename { width: 30%; }
164 164 tr.entry td.filename_no_report { width: 70%; }
165 165 tr.entry td.size { text-align: right; font-size: 90%; }
166 166 tr.entry td.revision, tr.entry td.author { text-align: center; }
167 167 tr.entry td.age { text-align: right; }
168 168 tr.entry.file td.filename a { margin-left: 16px; }
169 169 tr.entry.file td.filename_no_report a { margin-left: 16px; }
170 170
171 171 tr span.expander {background-image: url(../images/bullet_toggle_plus.png); padding-left: 8px; margin-left: 0; cursor: pointer;}
172 172 tr.open span.expander {background-image: url(../images/bullet_toggle_minus.png);}
173 173
174 174 tr.changeset { height: 20px }
175 175 tr.changeset ul, ol { margin-top: 0px; margin-bottom: 0px; }
176 176 tr.changeset td.revision_graph { width: 15%; background-color: #fffffb; }
177 177 tr.changeset td.author { text-align: center; width: 15%; white-space:nowrap;}
178 178 tr.changeset td.committed_on { text-align: center; width: 15%; white-space:nowrap;}
179 179
180 180 table.files tr.file td { text-align: center; }
181 181 table.files tr.file td.filename { text-align: left; padding-left: 24px; }
182 182 table.files tr.file td.digest { font-size: 80%; }
183 183
184 184 table.members td.roles, table.memberships td.roles { width: 45%; }
185 185
186 186 tr.message { height: 2.6em; }
187 187 tr.message td.subject { padding-left: 20px; }
188 188 tr.message td.created_on { white-space: nowrap; }
189 189 tr.message td.last_message { font-size: 80%; white-space: nowrap; }
190 190 tr.message.locked td.subject { background: url(../images/locked.png) no-repeat 0 1px; }
191 191 tr.message.sticky td.subject { background: url(../images/bullet_go.png) no-repeat 0 1px; font-weight: bold; }
192 192
193 193 tr.version.closed, tr.version.closed a { color: #999; }
194 194 tr.version td.name { padding-left: 20px; }
195 195 tr.version.shared td.name { background: url(../images/link.png) no-repeat 0% 70%; }
196 196 tr.version td.date, tr.version td.status, tr.version td.sharing { text-align: center; white-space:nowrap; }
197 197
198 198 tr.user td { width:13%; }
199 199 tr.user td.email { width:18%; }
200 200 tr.user td { white-space: nowrap; }
201 201 tr.user.locked, tr.user.registered { color: #aaa; }
202 202 tr.user.locked a, tr.user.registered a { color: #aaa; }
203 203
204 204 table.permissions td.role {color:#999;font-size:90%;font-weight:normal !important;text-align:center;vertical-align:bottom;}
205 205
206 206 tr.wiki-page-version td.updated_on, tr.wiki-page-version td.author {text-align:center;}
207 207
208 208 tr.time-entry { text-align: center; white-space: nowrap; }
209 209 tr.time-entry td.subject, tr.time-entry td.comments { text-align: left; white-space: normal; }
210 210 td.hours { text-align: right; font-weight: bold; padding-right: 0.5em; }
211 211 td.hours .hours-dec { font-size: 0.9em; }
212 212
213 213 table.plugins td { vertical-align: middle; }
214 214 table.plugins td.configure { text-align: right; padding-right: 1em; }
215 215 table.plugins span.name { font-weight: bold; display: block; margin-bottom: 6px; }
216 216 table.plugins span.description { display: block; font-size: 0.9em; }
217 217 table.plugins span.url { display: block; font-size: 0.9em; }
218 218
219 219 table.list tbody tr.group td { padding: 0.8em 0 0.5em 0.3em; font-weight: bold; border-bottom: 1px solid #ccc; }
220 220 table.list tbody tr.group span.count { color: #aaa; font-size: 80%; }
221 221 tr.group a.toggle-all { color: #aaa; font-size: 80%; font-weight: normal; display:none;}
222 222 tr.group:hover a.toggle-all { display:inline;}
223 223 a.toggle-all:hover {text-decoration:none;}
224 224
225 225 table.list tbody tr:hover { background-color:#ffffdd; }
226 226 table.list tbody tr.group:hover { background-color:inherit; }
227 227 table td {padding:2px;}
228 228 table p {margin:0;}
229 229 .odd {background-color:#f6f7f8;}
230 230 .even {background-color: #fff;}
231 231
232 232 a.sort { padding-right: 16px; background-position: 100% 50%; background-repeat: no-repeat; }
233 233 a.sort.asc { background-image: url(../images/sort_asc.png); }
234 234 a.sort.desc { background-image: url(../images/sort_desc.png); }
235 235
236 236 table.attributes { width: 100% }
237 237 table.attributes th { vertical-align: top; text-align: left; }
238 238 table.attributes td { vertical-align: top; }
239 239
240 240 table.boards a.board, h3.comments { background: url(../images/comment.png) no-repeat 0% 50%; padding-left: 20px; }
241 241 table.boards td.topic-count, table.boards td.message-count {text-align:center;}
242 242 table.boards td.last-message {font-size:80%;}
243 243
244 244 table.messages td.author, table.messages td.created_on, table.messages td.reply-count {text-align:center;}
245 245
246 246 table.query-columns {
247 247 border-collapse: collapse;
248 248 border: 0;
249 249 }
250 250
251 251 table.query-columns td.buttons {
252 252 vertical-align: middle;
253 253 text-align: center;
254 254 }
255 255
256 256 td.center {text-align:center;}
257 257
258 258 h3.version { background: url(../images/package.png) no-repeat 0% 50%; padding-left: 20px; }
259 259
260 260 div.issues h3 { background: url(../images/ticket.png) no-repeat 0% 50%; padding-left: 20px; }
261 261 div.members h3 { background: url(../images/group.png) no-repeat 0% 50%; padding-left: 20px; }
262 262 div.news h3 { background: url(../images/news.png) no-repeat 0% 50%; padding-left: 20px; }
263 263 div.projects h3 { background: url(../images/projects.png) no-repeat 0% 50%; padding-left: 20px; }
264 264
265 265 #watchers ul {margin: 0; padding: 0;}
266 266 #watchers li {list-style-type:none;margin: 0px 2px 0px 0px; padding: 0px 0px 0px 0px;}
267 267 #watchers select {width: 95%; display: block;}
268 268 #watchers a.delete {opacity: 0.4;}
269 269 #watchers a.delete:hover {opacity: 1;}
270 270 #watchers img.gravatar {margin: 0 4px 2px 0;}
271 271
272 272 span#watchers_inputs {overflow:auto; display:block;}
273 273 span.search_for_watchers {display:block;}
274 274 span.search_for_watchers, span.add_attachment {font-size:80%; line-height:2.5em;}
275 275 span.search_for_watchers a, span.add_attachment a {padding-left:16px; background: url(../images/bullet_add.png) no-repeat 0 50%; }
276 276
277 277
278 278 .highlight { background-color: #FCFD8D;}
279 279 .highlight.token-1 { background-color: #faa;}
280 280 .highlight.token-2 { background-color: #afa;}
281 281 .highlight.token-3 { background-color: #aaf;}
282 282
283 283 .box{
284 284 padding:6px;
285 285 margin-bottom: 10px;
286 286 background-color:#f6f6f6;
287 287 color:#505050;
288 288 line-height:1.5em;
289 289 border: 1px solid #e4e4e4;
290 290 }
291 291
292 292 div.square {
293 293 border: 1px solid #999;
294 294 float: left;
295 295 margin: .3em .4em 0 .4em;
296 296 overflow: hidden;
297 297 width: .6em; height: .6em;
298 298 }
299 299 .contextual {float:right; white-space: nowrap; line-height:1.4em;margin-top:5px; padding-left: 10px; font-size:0.9em;}
300 300 .contextual input, .contextual select {font-size:0.9em;}
301 301 .message .contextual { margin-top: 0; }
302 302
303 303 .splitcontent {overflow:auto;}
304 304 .splitcontentleft{float:left; width:49%;}
305 305 .splitcontentright{float:right; width:49%;}
306 306 form {display: inline;}
307 307 input, select {vertical-align: middle; margin-top: 1px; margin-bottom: 1px;}
308 308 fieldset {border: 1px solid #e4e4e4; margin:0;}
309 309 legend {color: #484848;}
310 310 hr { width: 100%; height: 1px; background: #ccc; border: 0;}
311 311 blockquote { font-style: italic; border-left: 3px solid #e0e0e0; padding-left: 0.6em; margin-left: 2.4em;}
312 312 blockquote blockquote { margin-left: 0;}
313 313 acronym { border-bottom: 1px dotted; cursor: help; }
314 314 textarea.wiki-edit { width: 99%; }
315 315 li p {margin-top: 0;}
316 316 div.issue {background:#ffffdd; padding:6px; margin-bottom:6px;border: 1px solid #d7d7d7;}
317 317 p.breadcrumb { font-size: 0.9em; margin: 4px 0 4px 0;}
318 318 p.subtitle { font-size: 0.9em; margin: -6px 0 12px 0; font-style: italic; }
319 319 p.footnote { font-size: 0.9em; margin-top: 0px; margin-bottom: 0px; }
320 320
321 321 div.issue div.subject div div { padding-left: 16px; }
322 322 div.issue div.subject p {margin: 0; margin-bottom: 0.1em; font-size: 90%; color: #999;}
323 323 div.issue div.subject>div>p { margin-top: 0.5em; }
324 324 div.issue div.subject h3 {margin: 0; margin-bottom: 0.1em;}
325 325 div.issue span.private { position:relative; bottom: 2px; text-transform: uppercase; background: #d22; color: #fff; font-weight:bold; padding: 0px 2px 0px 2px; font-size: 60%; margin-right: 2px; border-radius: 2px;}
326 326 div.issue .next-prev-links {color:#999;}
327 327 div.issue table.attributes th {width:22%;}
328 328 div.issue table.attributes td {width:28%;}
329 329
330 330 #issue_tree table.issues, #relations table.issues { border: 0; }
331 331 #issue_tree td.checkbox, #relations td.checkbox {display:none;}
332 332 #relations td.buttons {padding:0;}
333 333
334 334 fieldset.collapsible { border-width: 1px 0 0 0; font-size: 0.9em; }
335 335 fieldset.collapsible legend { padding-left: 16px; background: url(../images/arrow_expanded.png) no-repeat 0% 40%; cursor:pointer; }
336 336 fieldset.collapsible.collapsed legend { background-image: url(../images/arrow_collapsed.png); }
337 337
338 338 fieldset#date-range p { margin: 2px 0 2px 0; }
339 339 fieldset#filters table { border-collapse: collapse; }
340 340 fieldset#filters table td { padding: 0; vertical-align: middle; }
341 fieldset#filters tr.filter { height: 2em; }
341 fieldset#filters tr.filter { height: 2.1em; }
342 342 fieldset#filters td.field { width:200px; }
343 343 fieldset#filters td.operator { width:170px; }
344 344 fieldset#filters td.values { white-space:nowrap; }
345 345 fieldset#filters td.values select {min-width:130px;}
346 fieldset#filters td.values img { vertical-align: middle; margin-left:1px; }
346 fieldset#filters td.values input {height:1em;}
347 347 fieldset#filters td.add-filter { text-align: right; vertical-align: top; }
348 .toggle-multiselect {background: url(../images/bullet_toggle_plus.png) no-repeat 0% 40%; padding-left:8px; margin-left:0; cursor:pointer;}
348 349 .buttons { font-size: 0.9em; margin-bottom: 1.4em; margin-top: 1em; }
349 350
350 351 div#issue-changesets {float:right; width:45%; margin-left: 1em; margin-bottom: 1em; background: #fff; padding-left: 1em; font-size: 90%;}
351 352 div#issue-changesets div.changeset { padding: 4px;}
352 353 div#issue-changesets div.changeset { border-bottom: 1px solid #ddd; }
353 354 div#issue-changesets p { margin-top: 0; margin-bottom: 1em;}
354 355
355 356 .journal ul.details img {margin:0 0 -3px 4px;}
356 357
357 358 div#activity dl, #search-results { margin-left: 2em; }
358 359 div#activity dd, #search-results dd { margin-bottom: 1em; padding-left: 18px; font-size: 0.9em; }
359 360 div#activity dt, #search-results dt { margin-bottom: 0px; padding-left: 20px; line-height: 18px; background-position: 0 50%; background-repeat: no-repeat; }
360 361 div#activity dt.me .time { border-bottom: 1px solid #999; }
361 362 div#activity dt .time { color: #777; font-size: 80%; }
362 363 div#activity dd .description, #search-results dd .description { font-style: italic; }
363 364 div#activity span.project:after, #search-results span.project:after { content: " -"; }
364 365 div#activity dd span.description, #search-results dd span.description { display:block; color: #808080; }
365 366
366 367 #search-results dd { margin-bottom: 1em; padding-left: 20px; margin-left:0px; }
367 368
368 369 div#search-results-counts {float:right;}
369 370 div#search-results-counts ul { margin-top: 0.5em; }
370 371 div#search-results-counts li { list-style-type:none; float: left; margin-left: 1em; }
371 372
372 373 dt.issue { background-image: url(../images/ticket.png); }
373 374 dt.issue-edit { background-image: url(../images/ticket_edit.png); }
374 375 dt.issue-closed { background-image: url(../images/ticket_checked.png); }
375 376 dt.issue-note { background-image: url(../images/ticket_note.png); }
376 377 dt.changeset { background-image: url(../images/changeset.png); }
377 378 dt.news { background-image: url(../images/news.png); }
378 379 dt.message { background-image: url(../images/message.png); }
379 380 dt.reply { background-image: url(../images/comments.png); }
380 381 dt.wiki-page { background-image: url(../images/wiki_edit.png); }
381 382 dt.attachment { background-image: url(../images/attachment.png); }
382 383 dt.document { background-image: url(../images/document.png); }
383 384 dt.project { background-image: url(../images/projects.png); }
384 385 dt.time-entry { background-image: url(../images/time.png); }
385 386
386 387 #search-results dt.issue.closed { background-image: url(../images/ticket_checked.png); }
387 388
388 389 div#roadmap .related-issues { margin-bottom: 1em; }
389 390 div#roadmap .related-issues td.checkbox { display: none; }
390 391 div#roadmap .wiki h1:first-child { display: none; }
391 392 div#roadmap .wiki h1 { font-size: 120%; }
392 393 div#roadmap .wiki h2 { font-size: 110%; }
393 394 body.controller-versions.action-show div#roadmap .related-issues {width:70%;}
394 395
395 396 div#version-summary { float:right; width:28%; margin-left: 16px; margin-bottom: 16px; background-color: #fff; }
396 397 div#version-summary fieldset { margin-bottom: 1em; }
397 398 div#version-summary fieldset.time-tracking table { width:100%; }
398 399 div#version-summary th, div#version-summary td.total-hours { text-align: right; }
399 400
400 401 table#time-report td.hours, table#time-report th.period, table#time-report th.total { text-align: right; padding-right: 0.5em; }
401 402 table#time-report tbody tr.subtotal { font-style: italic; color:#777;}
402 403 table#time-report tbody tr.subtotal td.hours { color:#b0b0b0; }
403 404 table#time-report tbody tr.total { font-weight: bold; background-color:#EEEEEE; border-top:1px solid #e4e4e4;}
404 405 table#time-report .hours-dec { font-size: 0.9em; }
405 406
406 407 div.wiki-page .contextual a {opacity: 0.4}
407 408 div.wiki-page .contextual a:hover {opacity: 1}
408 409
409 410 form .attributes select { width: 60%; }
410 411 input#issue_subject { width: 99%; }
411 412 select#issue_done_ratio { width: 95px; }
412 413
413 414 ul.projects { margin: 0; padding-left: 1em; }
414 415 ul.projects.root { margin: 0; padding: 0; }
415 416 ul.projects ul.projects { border-left: 3px solid #e0e0e0; }
416 417 ul.projects li.root { list-style-type:none; margin-bottom: 1em; }
417 418 ul.projects li.child { list-style-type:none; margin-top: 1em;}
418 419 ul.projects div.root a.project { font-family: "Trebuchet MS", Verdana, sans-serif; font-weight: bold; font-size: 16px; margin: 0 0 10px 0; }
419 420 .my-project { padding-left: 18px; background: url(../images/fav.png) no-repeat 0 50%; }
420 421
421 422 #tracker_project_ids ul { margin: 0; padding-left: 1em; }
422 423 #tracker_project_ids li { list-style-type:none; }
423 424
424 425 #related-issues li img {vertical-align:middle;}
425 426
426 427 ul.properties {padding:0; font-size: 0.9em; color: #777;}
427 428 ul.properties li {list-style-type:none;}
428 429 ul.properties li span {font-style:italic;}
429 430
430 431 .total-hours { font-size: 110%; font-weight: bold; }
431 432 .total-hours span.hours-int { font-size: 120%; }
432 433
433 434 .autoscroll {overflow-x: auto; padding:1px; margin-bottom: 1.2em;}
434 435 #user_login, #user_firstname, #user_lastname, #user_mail, #my_account_form select, #user_form select, #user_identity_url { width: 90%; }
435 436
436 437 #workflow_copy_form select { width: 200px; }
437 438 table.transitions td.enabled {background: #bfb;}
438 439 table.fields_permissions select {font-size:90%}
439 440 table.fields_permissions td.readonly {background:#ddd;}
440 441 table.fields_permissions td.required {background:#d88;}
441 442
442 443 textarea#custom_field_possible_values {width: 99%}
443 444 input#content_comments {width: 99%}
444 445
445 446 .pagination {font-size: 90%}
446 447 p.pagination {margin-top:8px;}
447 448
448 449 /***** Tabular forms ******/
449 450 .tabular p{
450 451 margin: 0;
451 452 padding: 3px 0 3px 0;
452 453 padding-left: 180px; /* width of left column containing the label elements */
453 454 min-height: 1.8em;
454 455 clear:left;
455 456 }
456 457
457 458 html>body .tabular p {overflow:hidden;}
458 459
459 460 .tabular label{
460 461 font-weight: bold;
461 462 float: left;
462 463 text-align: right;
463 464 /* width of left column */
464 465 margin-left: -180px;
465 466 /* width of labels. Should be smaller than left column to create some right margin */
466 467 width: 175px;
467 468 }
468 469
469 470 .tabular label.floating{
470 471 font-weight: normal;
471 472 margin-left: 0px;
472 473 text-align: left;
473 474 width: 270px;
474 475 }
475 476
476 477 .tabular label.block{
477 478 font-weight: normal;
478 479 margin-left: 0px !important;
479 480 text-align: left;
480 481 float: none;
481 482 display: block;
482 483 width: auto;
483 484 }
484 485
485 486 .tabular label.inline{
486 487 float:none;
487 488 margin-left: 5px !important;
488 489 width: auto;
489 490 }
490 491
491 492 label.no-css {
492 493 font-weight: inherit;
493 494 float:none;
494 495 text-align:left;
495 496 margin-left:0px;
496 497 width:auto;
497 498 }
498 499 input#time_entry_comments { width: 90%;}
499 500
500 501 #preview fieldset {margin-top: 1em; background: url(../images/draft.png)}
501 502
502 503 .tabular.settings p{ padding-left: 300px; }
503 504 .tabular.settings label{ margin-left: -300px; width: 295px; }
504 505 .tabular.settings textarea { width: 99%; }
505 506
506 507 .settings.enabled_scm table {width:100%}
507 508 .settings.enabled_scm td.scm_name{ font-weight: bold; }
508 509
509 510 fieldset.settings label { display: block; }
510 511 fieldset#notified_events .parent { padding-left: 20px; }
511 512
512 513 span.required {color: #bb0000;}
513 514 .summary {font-style: italic;}
514 515
515 516 #attachments_fields input.description {margin-left: 8px; width:340px;}
516 517 #attachments_fields span {display:block; white-space:nowrap;}
517 518 #attachments_fields img {vertical-align: middle;}
518 519
519 520 div.attachments { margin-top: 12px; }
520 521 div.attachments p { margin:4px 0 2px 0; }
521 522 div.attachments img { vertical-align: middle; }
522 523 div.attachments span.author { font-size: 0.9em; color: #888; }
523 524
524 525 div.thumbnails {margin-top:0.6em;}
525 526 div.thumbnails div {background:#fff;border:2px solid #ddd;display:inline-block;margin-right:2px;}
526 527 div.thumbnails img {margin: 3px;}
527 528
528 529 p.other-formats { text-align: right; font-size:0.9em; color: #666; }
529 530 .other-formats span + span:before { content: "| "; }
530 531
531 532 a.atom { background: url(../images/feed.png) no-repeat 1px 50%; padding: 2px 0px 3px 16px; }
532 533
533 534 em.info {font-style:normal;font-size:90%;color:#888;display:block;}
534 535 em.info.error {padding-left:20px; background:url(../images/exclamation.png) no-repeat 0 50%;}
535 536
536 537 textarea.text_cf {width:90%;}
537 538
538 539 /* Project members tab */
539 540 div#tab-content-members .splitcontentleft, div#tab-content-memberships .splitcontentleft, div#tab-content-users .splitcontentleft { width: 64% }
540 541 div#tab-content-members .splitcontentright, div#tab-content-memberships .splitcontentright, div#tab-content-users .splitcontentright { width: 34% }
541 542 div#tab-content-members fieldset, div#tab-content-memberships fieldset, div#tab-content-users fieldset { padding:1em; margin-bottom: 1em; }
542 543 div#tab-content-members fieldset legend, div#tab-content-memberships fieldset legend, div#tab-content-users fieldset legend { font-weight: bold; }
543 544 div#tab-content-members fieldset label, div#tab-content-memberships fieldset label, div#tab-content-users fieldset label { display: block; }
544 545 div#tab-content-members fieldset div, div#tab-content-users fieldset div { max-height: 400px; overflow:auto; }
545 546
546 547 #users_for_watcher {height: 200px; overflow:auto;}
547 548 #users_for_watcher label {display: block;}
548 549
549 550 table.members td.group { padding-left: 20px; background: url(../images/group.png) no-repeat 0% 50%; }
550 551
551 552 input#principal_search, input#user_search {width:100%}
552 553 input#principal_search, input#user_search {
553 554 background: url(../images/magnifier.png) no-repeat 2px 50%; padding-left:20px;
554 555 border:1px solid #9EB1C2; border-radius:3px; height:1.5em; width:95%;
555 556 }
556 557 input#principal_search.ajax-loading, input#user_search.ajax-loading {
557 558 background-image: url(../images/loading.gif);
558 559 }
559 560
560 561 * html div#tab-content-members fieldset div { height: 450px; }
561 562
562 563 /***** Flash & error messages ****/
563 564 #errorExplanation, div.flash, .nodata, .warning, .conflict {
564 565 padding: 4px 4px 4px 30px;
565 566 margin-bottom: 12px;
566 567 font-size: 1.1em;
567 568 border: 2px solid;
568 569 }
569 570
570 571 div.flash {margin-top: 8px;}
571 572
572 573 div.flash.error, #errorExplanation {
573 574 background: url(../images/exclamation.png) 8px 50% no-repeat;
574 575 background-color: #ffe3e3;
575 576 border-color: #dd0000;
576 577 color: #880000;
577 578 }
578 579
579 580 div.flash.notice {
580 581 background: url(../images/true.png) 8px 5px no-repeat;
581 582 background-color: #dfffdf;
582 583 border-color: #9fcf9f;
583 584 color: #005f00;
584 585 }
585 586
586 587 div.flash.warning, .conflict {
587 588 background: url(../images/warning.png) 8px 5px no-repeat;
588 589 background-color: #FFEBC1;
589 590 border-color: #FDBF3B;
590 591 color: #A6750C;
591 592 text-align: left;
592 593 }
593 594
594 595 .nodata, .warning {
595 596 text-align: center;
596 597 background-color: #FFEBC1;
597 598 border-color: #FDBF3B;
598 599 color: #A6750C;
599 600 }
600 601
601 602 #errorExplanation ul { font-size: 0.9em;}
602 603 #errorExplanation h2, #errorExplanation p { display: none; }
603 604
604 605 .conflict-details {font-size:80%;}
605 606
606 607 /***** Ajax indicator ******/
607 608 #ajax-indicator {
608 609 position: absolute; /* fixed not supported by IE */
609 610 background-color:#eee;
610 611 border: 1px solid #bbb;
611 612 top:35%;
612 613 left:40%;
613 614 width:20%;
614 615 font-weight:bold;
615 616 text-align:center;
616 617 padding:0.6em;
617 618 z-index:100;
618 619 opacity: 0.5;
619 620 }
620 621
621 622 html>body #ajax-indicator { position: fixed; }
622 623
623 624 #ajax-indicator span {
624 625 background-position: 0% 40%;
625 626 background-repeat: no-repeat;
626 627 background-image: url(../images/loading.gif);
627 628 padding-left: 26px;
628 629 vertical-align: bottom;
629 630 }
630 631
631 632 /***** Calendar *****/
632 633 table.cal {border-collapse: collapse; width: 100%; margin: 0px 0 6px 0;border: 1px solid #d7d7d7;}
633 634 table.cal thead th {width: 14%; background-color:#EEEEEE; padding: 4px; }
634 635 table.cal thead th.week-number {width: auto;}
635 636 table.cal tbody tr {height: 100px;}
636 637 table.cal td {border: 1px solid #d7d7d7; vertical-align: top; font-size: 0.9em;}
637 638 table.cal td.week-number { background-color:#EEEEEE; padding: 4px; border:none; font-size: 1em;}
638 639 table.cal td p.day-num {font-size: 1.1em; text-align:right;}
639 640 table.cal td.odd p.day-num {color: #bbb;}
640 641 table.cal td.today {background:#ffffdd;}
641 642 table.cal td.today p.day-num {font-weight: bold;}
642 643 table.cal .starting a, p.cal.legend .starting {background: url(../images/bullet_go.png) no-repeat -1px -2px; padding-left:16px;}
643 644 table.cal .ending a, p.cal.legend .ending {background: url(../images/bullet_end.png) no-repeat -1px -2px; padding-left:16px;}
644 645 table.cal .starting.ending a, p.cal.legend .starting.ending {background: url(../images/bullet_diamond.png) no-repeat -1px -2px; padding-left:16px;}
645 646 p.cal.legend span {display:block;}
646 647
647 648 /***** Tooltips ******/
648 649 .tooltip{position:relative;z-index:24;}
649 650 .tooltip:hover{z-index:25;color:#000;}
650 651 .tooltip span.tip{display: none; text-align:left;}
651 652
652 653 div.tooltip:hover span.tip{
653 654 display:block;
654 655 position:absolute;
655 656 top:12px; left:24px; width:270px;
656 657 border:1px solid #555;
657 658 background-color:#fff;
658 659 padding: 4px;
659 660 font-size: 0.8em;
660 661 color:#505050;
661 662 }
662 663
663 664 img.ui-datepicker-trigger {
664 665 cursor: pointer;
665 666 vertical-align: middle;
666 667 margin-left: 4px;
667 668 }
668 669
669 670 /***** Progress bar *****/
670 671 table.progress {
671 672 border-collapse: collapse;
672 673 border-spacing: 0pt;
673 674 empty-cells: show;
674 675 text-align: center;
675 676 float:left;
676 677 margin: 1px 6px 1px 0px;
677 678 }
678 679
679 680 table.progress td { height: 1em; }
680 681 table.progress td.closed { background: #BAE0BA none repeat scroll 0%; }
681 682 table.progress td.done { background: #D3EDD3 none repeat scroll 0%; }
682 683 table.progress td.todo { background: #eee none repeat scroll 0%; }
683 684 p.pourcent {font-size: 80%;}
684 685 p.progress-info {clear: left; font-size: 80%; margin-top:-4px; color:#777;}
685 686
686 687 #roadmap table.progress td { height: 1.2em; }
687 688 /***** Tabs *****/
688 689 #content .tabs {height: 2.6em; margin-bottom:1.2em; position:relative; overflow:hidden;}
689 690 #content .tabs ul {margin:0; position:absolute; bottom:0; padding-left:0.5em; width: 2000px; border-bottom: 1px solid #bbbbbb;}
690 691 #content .tabs ul li {
691 692 float:left;
692 693 list-style-type:none;
693 694 white-space:nowrap;
694 695 margin-right:4px;
695 696 background:#fff;
696 697 position:relative;
697 698 margin-bottom:-1px;
698 699 }
699 700 #content .tabs ul li a{
700 701 display:block;
701 702 font-size: 0.9em;
702 703 text-decoration:none;
703 704 line-height:1.3em;
704 705 padding:4px 6px 4px 6px;
705 706 border: 1px solid #ccc;
706 707 border-bottom: 1px solid #bbbbbb;
707 708 background-color: #f6f6f6;
708 709 color:#999;
709 710 font-weight:bold;
710 711 border-top-left-radius:3px;
711 712 border-top-right-radius:3px;
712 713 }
713 714
714 715 #content .tabs ul li a:hover {
715 716 background-color: #ffffdd;
716 717 text-decoration:none;
717 718 }
718 719
719 720 #content .tabs ul li a.selected {
720 721 background-color: #fff;
721 722 border: 1px solid #bbbbbb;
722 723 border-bottom: 1px solid #fff;
723 724 color:#444;
724 725 }
725 726
726 727 #content .tabs ul li a.selected:hover {background-color: #fff;}
727 728
728 729 div.tabs-buttons { position:absolute; right: 0; width: 48px; height: 24px; background: white; bottom: 0; border-bottom: 1px solid #bbbbbb; }
729 730
730 731 button.tab-left, button.tab-right {
731 732 font-size: 0.9em;
732 733 cursor: pointer;
733 734 height:24px;
734 735 border: 1px solid #ccc;
735 736 border-bottom: 1px solid #bbbbbb;
736 737 position:absolute;
737 738 padding:4px;
738 739 width: 20px;
739 740 bottom: -1px;
740 741 }
741 742
742 743 button.tab-left {
743 744 right: 20px;
744 745 background: #eeeeee url(../images/bullet_arrow_left.png) no-repeat 50% 50%;
745 746 border-top-left-radius:3px;
746 747 }
747 748
748 749 button.tab-right {
749 750 right: 0;
750 751 background: #eeeeee url(../images/bullet_arrow_right.png) no-repeat 50% 50%;
751 752 border-top-right-radius:3px;
752 753 }
753 754
754 755 /***** Diff *****/
755 756 .diff_out { background: #fcc; }
756 757 .diff_out span { background: #faa; }
757 758 .diff_in { background: #cfc; }
758 759 .diff_in span { background: #afa; }
759 760
760 761 .text-diff {
761 762 padding: 1em;
762 763 background-color:#f6f6f6;
763 764 color:#505050;
764 765 border: 1px solid #e4e4e4;
765 766 }
766 767
767 768 /***** Wiki *****/
768 769 div.wiki table {
769 770 border-collapse: collapse;
770 771 margin-bottom: 1em;
771 772 }
772 773
773 774 div.wiki table, div.wiki td, div.wiki th {
774 775 border: 1px solid #bbb;
775 776 padding: 4px;
776 777 }
777 778
778 779 div.wiki .noborder, div.wiki .noborder td, div.wiki .noborder th {border:0;}
779 780
780 781 div.wiki .external {
781 782 background-position: 0% 60%;
782 783 background-repeat: no-repeat;
783 784 padding-left: 12px;
784 785 background-image: url(../images/external.png);
785 786 }
786 787
787 788 div.wiki a.new {color: #b73535;}
788 789
789 790 div.wiki ul, div.wiki ol {margin-bottom:1em;}
790 791
791 792 div.wiki pre {
792 793 margin: 1em 1em 1em 1.6em;
793 794 padding: 8px;
794 795 background-color: #fafafa;
795 796 border: 1px solid #e2e2e2;
796 797 width:auto;
797 798 overflow-x: auto;
798 799 overflow-y: hidden;
799 800 }
800 801
801 802 div.wiki ul.toc {
802 803 background-color: #ffffdd;
803 804 border: 1px solid #e4e4e4;
804 805 padding: 4px;
805 806 line-height: 1.2em;
806 807 margin-bottom: 12px;
807 808 margin-right: 12px;
808 809 margin-left: 0;
809 810 display: table
810 811 }
811 812 * html div.wiki ul.toc { width: 50%; } /* IE6 doesn't autosize div */
812 813
813 814 div.wiki ul.toc.right { float: right; margin-left: 12px; margin-right: 0; width: auto; }
814 815 div.wiki ul.toc.left { float: left; margin-right: 12px; margin-left: 0; width: auto; }
815 816 div.wiki ul.toc ul { margin: 0; padding: 0; }
816 817 div.wiki ul.toc li { list-style-type:none; margin: 0;}
817 818 div.wiki ul.toc li li { margin-left: 1.5em; }
818 819 div.wiki ul.toc li li li { font-size: 0.8em; }
819 820 div.wiki ul.toc a {
820 821 font-size: 0.9em;
821 822 font-weight: normal;
822 823 text-decoration: none;
823 824 color: #606060;
824 825 }
825 826 div.wiki ul.toc a:hover { color: #c61a1a; text-decoration: underline;}
826 827
827 828 a.wiki-anchor { display: none; margin-left: 6px; text-decoration: none; }
828 829 a.wiki-anchor:hover { color: #aaa !important; text-decoration: none; }
829 830 h1:hover a.wiki-anchor, h2:hover a.wiki-anchor, h3:hover a.wiki-anchor { display: inline; color: #ddd; }
830 831
831 832 div.wiki img { vertical-align: middle; }
832 833
833 834 /***** My page layout *****/
834 835 .block-receiver {
835 836 border:1px dashed #c0c0c0;
836 837 margin-bottom: 20px;
837 838 padding: 15px 0 15px 0;
838 839 }
839 840
840 841 .mypage-box {
841 842 margin:0 0 20px 0;
842 843 color:#505050;
843 844 line-height:1.5em;
844 845 }
845 846
846 847 .handle {cursor: move;}
847 848
848 849 a.close-icon {
849 850 display:block;
850 851 margin-top:3px;
851 852 overflow:hidden;
852 853 width:12px;
853 854 height:12px;
854 855 background-repeat: no-repeat;
855 856 cursor:pointer;
856 857 background-image:url('../images/close.png');
857 858 }
858 859 a.close-icon:hover {background-image:url('../images/close_hl.png');}
859 860
860 861 /***** Gantt chart *****/
861 862 .gantt_hdr {
862 863 position:absolute;
863 864 top:0;
864 865 height:16px;
865 866 border-top: 1px solid #c0c0c0;
866 867 border-bottom: 1px solid #c0c0c0;
867 868 border-right: 1px solid #c0c0c0;
868 869 text-align: center;
869 870 overflow: hidden;
870 871 }
871 872
872 873 .gantt_subjects { font-size: 0.8em; }
873 874 .gantt_subjects div { line-height:16px;height:16px;overflow:hidden;white-space:nowrap;text-overflow: ellipsis; }
874 875
875 876 .task {
876 877 position: absolute;
877 878 height:8px;
878 879 font-size:0.8em;
879 880 color:#888;
880 881 padding:0;
881 882 margin:0;
882 883 line-height:16px;
883 884 white-space:nowrap;
884 885 }
885 886
886 887 .task.label {width:100%;}
887 888 .task.label.project, .task.label.version { font-weight: bold; }
888 889
889 890 .task_late { background:#f66 url(../images/task_late.png); border: 1px solid #f66; }
890 891 .task_done { background:#00c600 url(../images/task_done.png); border: 1px solid #00c600; }
891 892 .task_todo { background:#aaa url(../images/task_todo.png); border: 1px solid #aaa; }
892 893
893 894 .task_todo.parent { background: #888; border: 1px solid #888; height: 3px;}
894 895 .task_late.parent, .task_done.parent { height: 3px;}
895 896 .task.parent.marker.starting { position: absolute; background: url(../images/task_parent_end.png) no-repeat 0 0; width: 8px; height: 16px; margin-left: -4px; left: 0px; top: -1px;}
896 897 .task.parent.marker.ending { position: absolute; background: url(../images/task_parent_end.png) no-repeat 0 0; width: 8px; height: 16px; margin-left: -4px; right: 0px; top: -1px;}
897 898
898 899 .version.task_late { background:#f66 url(../images/milestone_late.png); border: 1px solid #f66; height: 2px; margin-top: 3px;}
899 900 .version.task_done { background:#00c600 url(../images/milestone_done.png); border: 1px solid #00c600; height: 2px; margin-top: 3px;}
900 901 .version.task_todo { background:#fff url(../images/milestone_todo.png); border: 1px solid #fff; height: 2px; margin-top: 3px;}
901 902 .version.marker { background-image:url(../images/version_marker.png); background-repeat: no-repeat; border: 0; margin-left: -4px; margin-top: 1px; }
902 903
903 904 .project.task_late { background:#f66 url(../images/milestone_late.png); border: 1px solid #f66; height: 2px; margin-top: 3px;}
904 905 .project.task_done { background:#00c600 url(../images/milestone_done.png); border: 1px solid #00c600; height: 2px; margin-top: 3px;}
905 906 .project.task_todo { background:#fff url(../images/milestone_todo.png); border: 1px solid #fff; height: 2px; margin-top: 3px;}
906 907 .project.marker { background-image:url(../images/project_marker.png); background-repeat: no-repeat; border: 0; margin-left: -4px; margin-top: 1px; }
907 908
908 909 .version-behind-schedule a, .issue-behind-schedule a {color: #f66914;}
909 910 .version-overdue a, .issue-overdue a, .project-overdue a {color: #f00;}
910 911
911 912 /***** Icons *****/
912 913 .icon {
913 914 background-position: 0% 50%;
914 915 background-repeat: no-repeat;
915 916 padding-left: 20px;
916 917 padding-top: 2px;
917 918 padding-bottom: 3px;
918 919 }
919 920
920 921 .icon-add { background-image: url(../images/add.png); }
921 922 .icon-edit { background-image: url(../images/edit.png); }
922 923 .icon-copy { background-image: url(../images/copy.png); }
923 924 .icon-duplicate { background-image: url(../images/duplicate.png); }
924 925 .icon-del { background-image: url(../images/delete.png); }
925 926 .icon-move { background-image: url(../images/move.png); }
926 927 .icon-save { background-image: url(../images/save.png); }
927 928 .icon-cancel { background-image: url(../images/cancel.png); }
928 929 .icon-multiple { background-image: url(../images/table_multiple.png); }
929 930 .icon-folder { background-image: url(../images/folder.png); }
930 931 .open .icon-folder { background-image: url(../images/folder_open.png); }
931 932 .icon-package { background-image: url(../images/package.png); }
932 933 .icon-user { background-image: url(../images/user.png); }
933 934 .icon-projects { background-image: url(../images/projects.png); }
934 935 .icon-help { background-image: url(../images/help.png); }
935 936 .icon-attachment { background-image: url(../images/attachment.png); }
936 937 .icon-history { background-image: url(../images/history.png); }
937 938 .icon-time { background-image: url(../images/time.png); }
938 939 .icon-time-add { background-image: url(../images/time_add.png); }
939 940 .icon-stats { background-image: url(../images/stats.png); }
940 941 .icon-warning { background-image: url(../images/warning.png); }
941 942 .icon-fav { background-image: url(../images/fav.png); }
942 943 .icon-fav-off { background-image: url(../images/fav_off.png); }
943 944 .icon-reload { background-image: url(../images/reload.png); }
944 945 .icon-lock { background-image: url(../images/locked.png); }
945 946 .icon-unlock { background-image: url(../images/unlock.png); }
946 947 .icon-checked { background-image: url(../images/true.png); }
947 948 .icon-details { background-image: url(../images/zoom_in.png); }
948 949 .icon-report { background-image: url(../images/report.png); }
949 950 .icon-comment { background-image: url(../images/comment.png); }
950 951 .icon-summary { background-image: url(../images/lightning.png); }
951 952 .icon-server-authentication { background-image: url(../images/server_key.png); }
952 953 .icon-issue { background-image: url(../images/ticket.png); }
953 954 .icon-zoom-in { background-image: url(../images/zoom_in.png); }
954 955 .icon-zoom-out { background-image: url(../images/zoom_out.png); }
955 956 .icon-passwd { background-image: url(../images/textfield_key.png); }
956 957 .icon-test { background-image: url(../images/bullet_go.png); }
957 958
958 959 .icon-file { background-image: url(../images/files/default.png); }
959 960 .icon-file.text-plain { background-image: url(../images/files/text.png); }
960 961 .icon-file.text-x-c { background-image: url(../images/files/c.png); }
961 962 .icon-file.text-x-csharp { background-image: url(../images/files/csharp.png); }
962 963 .icon-file.text-x-java { background-image: url(../images/files/java.png); }
963 964 .icon-file.text-x-javascript { background-image: url(../images/files/js.png); }
964 965 .icon-file.text-x-php { background-image: url(../images/files/php.png); }
965 966 .icon-file.text-x-ruby { background-image: url(../images/files/ruby.png); }
966 967 .icon-file.text-xml { background-image: url(../images/files/xml.png); }
967 968 .icon-file.text-css { background-image: url(../images/files/css.png); }
968 969 .icon-file.text-html { background-image: url(../images/files/html.png); }
969 970 .icon-file.image-gif { background-image: url(../images/files/image.png); }
970 971 .icon-file.image-jpeg { background-image: url(../images/files/image.png); }
971 972 .icon-file.image-png { background-image: url(../images/files/image.png); }
972 973 .icon-file.image-tiff { background-image: url(../images/files/image.png); }
973 974 .icon-file.application-pdf { background-image: url(../images/files/pdf.png); }
974 975 .icon-file.application-zip { background-image: url(../images/files/zip.png); }
975 976 .icon-file.application-x-gzip { background-image: url(../images/files/zip.png); }
976 977
977 978 img.gravatar {
978 979 padding: 2px;
979 980 border: solid 1px #d5d5d5;
980 981 background: #fff;
981 982 vertical-align: middle;
982 983 }
983 984
984 985 div.issue img.gravatar {
985 986 float: left;
986 987 margin: 0 6px 0 0;
987 988 padding: 5px;
988 989 }
989 990
990 991 div.issue table img.gravatar {
991 992 height: 14px;
992 993 width: 14px;
993 994 padding: 2px;
994 995 float: left;
995 996 margin: 0 0.5em 0 0;
996 997 }
997 998
998 999 h2 img.gravatar {margin: -2px 4px -4px 0;}
999 1000 h3 img.gravatar {margin: -4px 4px -4px 0;}
1000 1001 h4 img.gravatar {margin: -6px 4px -4px 0;}
1001 1002 td.username img.gravatar {margin: 0 0.5em 0 0; vertical-align: top;}
1002 1003 #activity dt img.gravatar {float: left; margin: 0 1em 1em 0;}
1003 1004 /* Used on 12px Gravatar img tags without the icon background */
1004 1005 .icon-gravatar {float: left; margin-right: 4px;}
1005 1006
1006 1007 #activity dt, .journal {clear: left;}
1007 1008
1008 1009 .journal-link {float: right;}
1009 1010
1010 1011 h2 img { vertical-align:middle; }
1011 1012
1012 1013 .hascontextmenu { cursor: context-menu; }
1013 1014
1014 1015 /************* CodeRay styles *************/
1015 1016 .syntaxhl div {display: inline;}
1016 1017 .syntaxhl .line-numbers {padding: 2px 4px 2px 4px; background-color: #eee; margin:0px 5px 0px 0px;}
1017 1018 .syntaxhl .code pre { overflow: auto }
1018 1019 .syntaxhl .debug { color: white !important; background: blue !important; }
1019 1020
1020 1021 .syntaxhl .annotation { color:#007 }
1021 1022 .syntaxhl .attribute-name { color:#b48 }
1022 1023 .syntaxhl .attribute-value { color:#700 }
1023 1024 .syntaxhl .binary { color:#509 }
1024 1025 .syntaxhl .char .content { color:#D20 }
1025 1026 .syntaxhl .char .delimiter { color:#710 }
1026 1027 .syntaxhl .char { color:#D20 }
1027 1028 .syntaxhl .class { color:#258; font-weight:bold }
1028 1029 .syntaxhl .class-variable { color:#369 }
1029 1030 .syntaxhl .color { color:#0A0 }
1030 1031 .syntaxhl .comment { color:#385 }
1031 1032 .syntaxhl .comment .char { color:#385 }
1032 1033 .syntaxhl .comment .delimiter { color:#385 }
1033 1034 .syntaxhl .complex { color:#A08 }
1034 1035 .syntaxhl .constant { color:#258; font-weight:bold }
1035 1036 .syntaxhl .decorator { color:#B0B }
1036 1037 .syntaxhl .definition { color:#099; font-weight:bold }
1037 1038 .syntaxhl .delimiter { color:black }
1038 1039 .syntaxhl .directive { color:#088; font-weight:bold }
1039 1040 .syntaxhl .doc { color:#970 }
1040 1041 .syntaxhl .doc-string { color:#D42; font-weight:bold }
1041 1042 .syntaxhl .doctype { color:#34b }
1042 1043 .syntaxhl .entity { color:#800; font-weight:bold }
1043 1044 .syntaxhl .error { color:#F00; background-color:#FAA }
1044 1045 .syntaxhl .escape { color:#666 }
1045 1046 .syntaxhl .exception { color:#C00; font-weight:bold }
1046 1047 .syntaxhl .float { color:#06D }
1047 1048 .syntaxhl .function { color:#06B; font-weight:bold }
1048 1049 .syntaxhl .global-variable { color:#d70 }
1049 1050 .syntaxhl .hex { color:#02b }
1050 1051 .syntaxhl .imaginary { color:#f00 }
1051 1052 .syntaxhl .include { color:#B44; font-weight:bold }
1052 1053 .syntaxhl .inline { background-color: hsla(0,0%,0%,0.07); color: black }
1053 1054 .syntaxhl .inline-delimiter { font-weight: bold; color: #666 }
1054 1055 .syntaxhl .instance-variable { color:#33B }
1055 1056 .syntaxhl .integer { color:#06D }
1056 1057 .syntaxhl .key .char { color: #60f }
1057 1058 .syntaxhl .key .delimiter { color: #404 }
1058 1059 .syntaxhl .key { color: #606 }
1059 1060 .syntaxhl .keyword { color:#939; font-weight:bold }
1060 1061 .syntaxhl .label { color:#970; font-weight:bold }
1061 1062 .syntaxhl .local-variable { color:#963 }
1062 1063 .syntaxhl .namespace { color:#707; font-weight:bold }
1063 1064 .syntaxhl .octal { color:#40E }
1064 1065 .syntaxhl .operator { }
1065 1066 .syntaxhl .predefined { color:#369; font-weight:bold }
1066 1067 .syntaxhl .predefined-constant { color:#069 }
1067 1068 .syntaxhl .predefined-type { color:#0a5; font-weight:bold }
1068 1069 .syntaxhl .preprocessor { color:#579 }
1069 1070 .syntaxhl .pseudo-class { color:#00C; font-weight:bold }
1070 1071 .syntaxhl .regexp .content { color:#808 }
1071 1072 .syntaxhl .regexp .delimiter { color:#404 }
1072 1073 .syntaxhl .regexp .modifier { color:#C2C }
1073 1074 .syntaxhl .regexp { background-color:hsla(300,100%,50%,0.06); }
1074 1075 .syntaxhl .reserved { color:#080; font-weight:bold }
1075 1076 .syntaxhl .shell .content { color:#2B2 }
1076 1077 .syntaxhl .shell .delimiter { color:#161 }
1077 1078 .syntaxhl .shell { background-color:hsla(120,100%,50%,0.06); }
1078 1079 .syntaxhl .string .char { color: #46a }
1079 1080 .syntaxhl .string .content { color: #46a }
1080 1081 .syntaxhl .string .delimiter { color: #46a }
1081 1082 .syntaxhl .string .modifier { color: #46a }
1082 1083 .syntaxhl .symbol .content { color:#d33 }
1083 1084 .syntaxhl .symbol .delimiter { color:#d33 }
1084 1085 .syntaxhl .symbol { color:#d33 }
1085 1086 .syntaxhl .tag { color:#070 }
1086 1087 .syntaxhl .type { color:#339; font-weight:bold }
1087 1088 .syntaxhl .value { color: #088; }
1088 1089 .syntaxhl .variable { color:#037 }
1089 1090
1090 1091 .syntaxhl .insert { background: hsla(120,100%,50%,0.12) }
1091 1092 .syntaxhl .delete { background: hsla(0,100%,50%,0.12) }
1092 1093 .syntaxhl .change { color: #bbf; background: #007; }
1093 1094 .syntaxhl .head { color: #f8f; background: #505 }
1094 1095 .syntaxhl .head .filename { color: white; }
1095 1096
1096 1097 .syntaxhl .delete .eyecatcher { background-color: hsla(0,100%,50%,0.2); border: 1px solid hsla(0,100%,45%,0.5); margin: -1px; border-bottom: none; border-top-left-radius: 5px; border-top-right-radius: 5px; }
1097 1098 .syntaxhl .insert .eyecatcher { background-color: hsla(120,100%,50%,0.2); border: 1px solid hsla(120,100%,25%,0.5); margin: -1px; border-top: none; border-bottom-left-radius: 5px; border-bottom-right-radius: 5px; }
1098 1099
1099 1100 .syntaxhl .insert .insert { color: #0c0; background:transparent; font-weight:bold }
1100 1101 .syntaxhl .delete .delete { color: #c00; background:transparent; font-weight:bold }
1101 1102 .syntaxhl .change .change { color: #88f }
1102 1103 .syntaxhl .head .head { color: #f4f }
1103 1104
1104 1105 /***** Media print specific styles *****/
1105 1106 @media print {
1106 1107 #top-menu, #header, #main-menu, #sidebar, #footer, .contextual, .other-formats { display:none; }
1107 1108 #main { background: #fff; }
1108 1109 #content { width: 99%; margin: 0; padding: 0; border: 0; background: #fff; overflow: visible !important;}
1109 1110 #wiki_add_attachment { display:none; }
1110 1111 .hide-when-print { display: none; }
1111 1112 .autoscroll {overflow-x: visible;}
1112 1113 table.list {margin-top:0.5em;}
1113 1114 table.list th, table.list td {border: 1px solid #aaa;}
1114 1115 }
1115 1116
1116 1117 /* Accessibility specific styles */
1117 1118 .hidden-for-sighted {
1118 1119 position:absolute;
1119 1120 left:-10000px;
1120 1121 top:auto;
1121 1122 width:1px;
1122 1123 height:1px;
1123 1124 overflow:hidden;
1124 1125 }
General Comments 0
You need to be logged in to leave comments. Login now