diff --git a/assets/images/icon_info_red.gif b/assets/images/icon_info_red.gif new file mode 100644 index 0000000000..34de8c6b98 Binary files /dev/null and b/assets/images/icon_info_red.gif differ diff --git a/assets/javascripts/cordinc_tooltip.js b/assets/javascripts/cordinc_tooltip.js new file mode 100644 index 0000000000..c4c2ee260c --- /dev/null +++ b/assets/javascripts/cordinc_tooltip.js @@ -0,0 +1,159 @@ +/* + * Copyright (c) 2009 Charles Cordingley (www.cordinc.com) + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +/* + * cordinc_tooltip.js, v1.0.2 - 27 August 2008 + * For help see www.cordinc.com/projects/tooltips.html + */ +var Tooltip = Class.create({ + initialize: function(target, tooltip) { + var options = Object.extend({ + start_effect: function(element) {}, + end_effect: function(element) {}, + zindex: 1000, + offset: {x:0, y:0}, + hook: {target:'topRight', tip:'bottomLeft'}, + trigger: false, + DOM_location: false, + className: false, + delay: {} + }, arguments[2] || {}); + this.target = $(target); + this.show_at = (options.show_at_id !== undefined) ? $(options.show_at_id) : undefined + this.tooltip = $(tooltip); + this.options = options; + this.event_target = this.options.trigger?$(this.options.trigger):this.target; + + if (this.options.className) { + this.tooltip.addClassName(this.options.className); + } + this.tooltip.hide(); + this.display=false; + + this.mouse_over = this.displayTooltip.bindAsEventListener(this); + this.mouse_out = this.removeTooltip.bindAsEventListener(this); + this.event_target.observe("mouseover", this.mouse_over); + this.event_target.observe("mouseout", this.mouse_out); + }, + + displayTooltip: function(event){ + event.stop(); + + if (this.display) {return;} + if (this.options.delay.start) { + var self = this; + this.timer_id = setTimeout(function(){self.timer_id = false; self.showTooltip(event);}, this.options.delay.start*1000); + } else { + this.showTooltip(event); + } + }, + + showTooltip: function(event) { + var show_at = (this.show_at !== undefined) ? this.show_at : this.target + this.display=true; + position = this.positionTooltip(event); + + this.clone = this.tooltip.cloneNode(true); + parentId = this.options.DOM_location?$(this.options.DOM_location.parentId):show_at.parentNode; + successorId = this.options.DOM_location?$(this.options.DOM_location.successorId):show_at; + parentId.insertBefore(this.clone, successorId); + + this.clone.setStyle({ + position: 'absolute', + top: position.top + "px", + left: position.left + "px", + display: "inline", + zIndex:this.options.zindex, + /* fix for ur dashboard */ + visibility: 'visible', + width: "400px" + }); + + if (this.options.start_effect) { + this.options.start_effect(this.clone); + } + }, + + positionTooltip: function(event) { + target_position = this.target.cumulativeOffset(); + + tooltip_dimensions = this.tooltip.getDimensions(); + target_dimensions = this.target.getDimensions(); + + this.positionModify(target_position, target_dimensions, this.options.hook.target, 1); + this.positionModify(target_position, tooltip_dimensions, this.options.hook.tip, -1); + + target_position.top += this.options.offset.y; + target_position.left += this.options.offset.x; + + return target_position; + }, + + positionModify: function(position, box, corner, neg) { + if (corner == 'topRight') { + position.left += box.width*neg; + } else if (corner == 'topLeft') { + } else if (corner == 'bottomLeft') { + position.top += box.height*neg; + } else if (corner == 'bottomRight') { + position.top += box.height*neg; + position.left += box.width*neg; + } else if (corner == 'topMid') { + position.left += (box.width/2)*neg; + } else if (corner == 'leftMid') { + position.top += (box.height/2)*neg; + } else if (corner == 'bottomMid') { + position.top += box.height*neg; + position.left += (box.width/2)*neg; + } else if (corner == 'rightMid') { + position.top += (box.height/2)*neg; + position.left += box.width*neg; + } + }, + + removeTooltip: function(event) { + if (this.timer_id) { + clearTimeout(this.timer_id); + this.timer_id = false; + return; + } + + if (this.options.end_effect) { + this.options.end_effect(this.clone); + } + + if (this.options.delay.end) { + var self = this; + setTimeout(function(){self.clearTooltip();}, this.options.delay.end*1000); + } else { + this.clearTooltip(); + } + }, + + clearTooltip: function() { + if (this.clone !== undefined && this.clone !== null) { + this.clone.remove(); + this.clone = null; + this.display=false; + } + }, + + destroy: function() { + this.event_target.stopObserving("mouseover", this.mouse_over); + this.event_target.stopObserving("mouseout", this.mouse_out); + this.clearTooltip(); + } +}) \ No newline at end of file diff --git a/assets/javascripts/reporting/filters.js b/assets/javascripts/reporting/filters.js index 1cd6785d50..29658c8cc2 100644 --- a/assets/javascripts/reporting/filters.js +++ b/assets/javascripts/reporting/filters.js @@ -293,7 +293,7 @@ Reporting.Filters = { // return an array of all filters that depend on the given filter plus the given filter dependent_for: function(field) { var deps = $$('.filters-select[data-all-dependents]').findAll(function(selectBox) { - return Reporting.Filters.get_dependents(selectBox).include(field) + return selectBox.up('tr').visible() && Reporting.Filters.get_dependents(selectBox).include(field) }).map(function(selectBox) { return selectBox.getAttribute("data-filter-name"); }); diff --git a/assets/stylesheets/help.css b/assets/stylesheets/help.css new file mode 100644 index 0000000000..4330734532 --- /dev/null +++ b/assets/stylesheets/help.css @@ -0,0 +1,55 @@ +.help { + margin-left: 5px; + margin-right: 5px; +} + +.tooltip { + position: absolute; + margin-top: 3px; + margin-bottom: 3px; + padding: 3px; + width: 400px; + z-index: 256; + color: #000000; + border: 1px solid #000000; + background: #FFFFCC; + font: 12px Verdana, sans-serif; + text-align: left; + padding: -50px; + line-height: 16px; + font-size: 11px; +} + +.filter-icon { + +} + +.filter-tip { + +} + +.group-by-icon { + float: right; + margin-right: 5px; +} + +.group-by-tip { + margin-top: -300px; + margin-left: -475px; +} + +.filter-legend-icon { + +} + +.filter-legend-tip { + margin-left: 10px; +} + +.group_by-legend-icon { + +} + +.group_by-legend-tip { + margin-left: 10px; +} \ No newline at end of file diff --git a/assets/stylesheets/reporting.css b/assets/stylesheets/reporting.css index 5bf167752d..b3976c8fca 100644 --- a/assets/stylesheets/reporting.css +++ b/assets/stylesheets/reporting.css @@ -306,12 +306,14 @@ fieldset#filter-settings table td > label { } .drag_container { - padding: 7px 0; + padding: 0; height: 22px; + margin-bottom: 1em; + background-color: #EEE; } .drag_container select { - margin-right: 20px; + margin-right: 3px; float: right; } diff --git a/config/locales/de.yml b/config/locales/de.yml index d2d7664105..cebf2a946f 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml @@ -16,9 +16,12 @@ de: label_count: Anzahl label_sum: Summe - label_none: "(no value)" + label_none: "(Keine Angabe)" + + label_help: Hilfe description_drill_down: Details anzeigen validation_failure_date: "ist kein gültiges Datum" validation_failure_integer: "ist keine ganze Zahl" + diff --git a/config/locales/en.yml b/config/locales/en.yml index a879c64de6..a48dae7e0a 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -16,7 +16,9 @@ en: label_count: Count label_sum: Sum - label_none: "(no value)" + label_none: "(no data)" + + label_help: Help description_drill_down: Show details diff --git a/lib/report.rb b/lib/report.rb index fccb529116..0b02ff69cf 100644 --- a/lib/report.rb +++ b/lib/report.rb @@ -77,7 +77,10 @@ class Report < ActiveRecord::Base def chain(klass = nil, options = {}) build_new_chain unless @chain - @chain = klass.new @chain, options if klass + if klass + @chain = klass.new @chain, options + @chain.engine = self.class + end @chain = @chain.parent until @chain.top? @chain end diff --git a/lib/report/chainable.rb b/lib/report/chainable.rb index 8bc4b5aa2b..38476e357a 100644 --- a/lib/report/chainable.rb +++ b/lib/report/chainable.rb @@ -309,7 +309,6 @@ class Report < ActiveRecord::Base value.to_s end - def self.mapping_for(field) @field_map ||= (engine::Filter.all + engine.GroupBy.all).inject(Hash.new {|h,k| h[k] = []}) do |hash,cbl| hash[cbl.field] << cbl.mapping @@ -317,5 +316,20 @@ class Report < ActiveRecord::Base @field_map[field] end + def help_text + self.class.help_text + end + + ## + # Sets a help text to be displayed for this kind of Chainable. + def self.help_text=(sym) + @help_text = sym + end + + def self.help_text(sym = nil) + @help_text = sym if sym + @help_text + end + end end diff --git a/lib/report/controller.rb b/lib/report/controller.rb index fc06739361..a693016214 100644 --- a/lib/report/controller.rb +++ b/lib/report/controller.rb @@ -65,7 +65,7 @@ module Report::Controller else raise ActiveRecord::RecordNotFound end - redirect_to :action => "index" + redirect_to :action => "index", :default => 1 end ## diff --git a/lib/report/query_utils.rb b/lib/report/query_utils.rb index ec18c7eceb..4b6d5116dd 100644 --- a/lib/report/query_utils.rb +++ b/lib/report/query_utils.rb @@ -32,8 +32,14 @@ module Report::QueryUtils # # @return [Class] subclass def engine - return self.class.engine unless is_a? Module - @engine ||= Object.const_get(name[/^[^:]+/] || :Report) + return @engine if @engine + if is_a? Module + @engine = Object.const_get(name[/^[^:]+/] || :Report) + elsif respond_to? :parent and parent.respond_to? :engine + parent.engine + else + self.class.engine + end end ## @@ -167,10 +173,39 @@ module Report::QueryUtils "-- code specific for #{adapter_name}\n\t" << super(field) end + ## + # Converts value with a given behavior, but treats nil differently. + # Params + # - value: the value to convert + # - weight_of_nil (optional): How a nil should be treated. + # :infinit - makes a nil weight really heavy, which will make it stay + # at the very end when sorting + # :negative_infinit - opposite of :infinit, let's the nil stay at the very beginning + # any other object - nil's will be replaced by thyt object + # - block (optional) - defines how to convert values which are not nil + # if no block is given, values stay untouched + def convert_unless_nil(value, weight_of_nil = :infinit) + if value.nil? + if weight_of_nil == :infinit + 1.0/0 # Infinity, which is greater than any string or number + elsif weight_of_nil == :negative_infinit + -1.0/0 # negative Infinity, which is smaller than any string or number + else + weight_of_nil + end + else + if block_given? + yield value + else + value + end + end + end + def map_field(key, value) case key.to_s - when "singleton_value", /_id$/ then value.to_i - else value.to_s + when "singleton_value", /_id$/ then convert_unless_nil(value) {|v| v.to_i } + else convert_unless_nil(value) {|v| v.to_s } end end diff --git a/lib/report/sql_statement.rb b/lib/report/sql_statement.rb index 9dd20c80f4..07e98d3b11 100644 --- a/lib/report/sql_statement.rb +++ b/lib/report/sql_statement.rb @@ -74,6 +74,7 @@ class Report::SqlStatement # FIXME I'm ugly @sql ||= begin sql = "\n-- BEGIN #{desc}\n" \ + "-- DB: #{ConnectionSwitcher.config_name}\n" \ "SELECT\n#{select.map { |e| "\t#{e}" }.join ",\n"}" \ "\nFROM\n\t#{from.gsub("\n", "\n\t")}" \ "\n\t#{joins.map { |e| e.gsub("\n", "\n\t") }.join "\n\t"}" \ diff --git a/lib/report/table.rb b/lib/report/table.rb index e4fc8470b3..271b945bee 100644 --- a/lib/report/table.rb +++ b/lib/report/table.rb @@ -35,7 +35,7 @@ class Report::Table ## # @param [Array] expected Fields expected - # @param [Array,Hash,Resul] given Fields/result to be tested + # @param [Array,Hash,Result] given Fields/result to be tested # @return [TrueClass,FalseClass] def satisfies?(type, expected, given) given = fields_from(given, type) if given.respond_to? :to_hash diff --git a/lib/widget/base.rb b/lib/widget/base.rb index b44567b4d6..a96299e4f9 100644 --- a/lib/widget/base.rb +++ b/lib/widget/base.rb @@ -11,10 +11,40 @@ class Widget::Base < Widget end def render_with_options(options = {}, &block) + self.help_text = options[:help_text] if canvas = options[:to] canvas << "\n" << render(&block) else render(&block) end end + + ## + # An optional help text. If defined the Help Widget + # displaying the given text is going to be placed + # next to this Widget, if it supports that. + def help_text + @help_text + end + + def help_text=(text) + @help_text = text + end + + ## + # Appends the Help Widget with this Widget's help text + # if it is defined to the input. + # If the help text is not defined the input is returned. + def maybe_with_help(html, options = {}) + text = options[:text] + text ||= help_text unless options[:ignore_default] + if text + help = render_widget Widget::Controls::Help, text do + options + end + html + help + else + html + end + end end diff --git a/lib/widget/controls/clear.rb b/lib/widget/controls/clear.rb index f460105a8d..c58570e7ab 100644 --- a/lib/widget/controls/clear.rb +++ b/lib/widget/controls/clear.rb @@ -1,5 +1,6 @@ class Widget::Controls::Clear < Widget::Base def render - link_to content_tag(:span, content_tag(:em, l(:"button_clear"), :class => "button-icon icon-clear")), '#', :id => 'query-link-clear', :class => 'button secondary' + html = link_to content_tag(:span, content_tag(:em, l(:"button_clear"), :class => "button-icon icon-clear")), '#', :id => 'query-link-clear', :class => 'button secondary' + maybe_with_help html end end diff --git a/lib/widget/controls/delete.rb b/lib/widget/controls/delete.rb index b60d6ad07a..af14559987 100644 --- a/lib/widget/controls/delete.rb +++ b/lib/widget/controls/delete.rb @@ -1,11 +1,19 @@ class Widget::Controls::Delete < Widget::Base def render return "" if @query.new_record? - button = link_to content_tag(:span, content_tag(:em, l(:button_delete), :class => "button-icon icon-delete")), "#", - :class => 'button secondary', - :id => 'query-icon-delete', - :title => l(:button_delete) - popup = content_tag :div, :id => "delete_form", :class => "button_form" do + render_button + render_popup + end + + def render_button + link_to(content_tag(:span, content_tag(:em, l(:button_delete), :class => "button-icon icon-delete")), + "#", + :class => 'button secondary', + :id => 'query-icon-delete', + :title => l(:button_delete)) + end + + def render_popup + content_tag :div, :id => "delete_form", :class => "button_form", :style => "display:none" do question = content_tag :p, l(:label_really_delete_question) options = content_tag :p do delete_button = content_tag :span do @@ -19,6 +27,5 @@ class Widget::Controls::Delete < Widget::Base end question + options end - button + popup end end diff --git a/lib/widget/controls/help.rb b/lib/widget/controls/help.rb new file mode 100644 index 0000000000..0283e9bc49 --- /dev/null +++ b/lib/widget/controls/help.rb @@ -0,0 +1,55 @@ +## +# Usgae: render_widget Widget::Controls::Help, :text +# +# Where :text is a i18n key. +class Widget::Controls::Help < Widget::Base + def render + id = "tip:#{@query}" + options = {:icon => {}, :tooltip => {}} + options.merge!(yield) if block_given? + sai = options[:show_at_id] ? ", show_at_id: '#{options[:show_at_id]}'" : "" + + icon = tag :img, :src => '/images/icon_info_red.gif', :id => "target:#{@query}" + tip = content_tag_string :div, l(@query), tip_config(options[:tooltip]), false + script = content_tag :script, + "new Tooltip('target:#{@query}', 'tip:#{@query}', {className: 'tooltip'#{sai}});", + {:type => 'text/javascript'}, false + target = content_tag :a, icon + tip, icon_config(options[:icon]) + target + script + end + + def icon_config(options) + add_class = lambda do |cl| + if cl + "help #{cl}" + else + "help" + end + end + options.mega_merge! :href => '#', :class => add_class + end + + def tip_config(options) + add_class = lambda do |cl| + if cl + "#{cl} tooltip" + else + "tooltip" + end + end + options.mega_merge! :id => "tip:#{@query}", :class => add_class + end +end + +class Hash + def mega_merge!(hash) + hash.each do |key, value| + if value.kind_of?(Proc) + self[key] = value.call(self[key]) + else + self[key] = value + end + end + self + end +end diff --git a/lib/widget/controls/save_as.rb b/lib/widget/controls/save_as.rb index 5ae48ed33e..8d6c86bb21 100644 --- a/lib/widget/controls/save_as.rb +++ b/lib/widget/controls/save_as.rb @@ -10,7 +10,7 @@ class Widget::Controls::SaveAs < Widget::Base button = link_to content_tag(:span, content_tag(:em, link_name, :class => "button-icon icon-save-as")), "#", :class => "button secondary", :id => 'query-icon-save-as', :title => link_name - button + render_popup + maybe_with_help(button) + render_popup end def render_popup_form @@ -39,7 +39,7 @@ class Widget::Controls::SaveAs < Widget::Base end def render_popup - content_tag :div, :id => 'save_as_form', :class => "button_form" do + content_tag :div, :id => 'save_as_form', :class => "button_form", :style => "display:none" do render_popup_form + render_popup_buttons end end diff --git a/lib/widget/filters.rb b/lib/widget/filters.rb index c924cdd107..48cc5256aa 100644 --- a/lib/widget/filters.rb +++ b/lib/widget/filters.rb @@ -12,10 +12,18 @@ class Widget::Filters < Widget::Base end end select = content_tag :div, :id => "add_filter_block" do - select_tag 'add_filter_select', + add_filter = select_tag 'add_filter_select', options_for_select([["-- #{l(:label_filter_add)} --",'']] + selectables), :class => "select-small", :name => nil + maybe_with_help add_filter, { + :icon => { + :class => 'filter-icon' + }, + :tooltip => { + :class => 'filter-tip' + } + } end content_tag(:div, table + select) end @@ -70,6 +78,20 @@ class Widget::Filters < Widget::Base render_widget Filters::MultiValues, f, :to => html end end + render_filter_help f, :to => html render_widget Filters::RemoveButton, f, :to => html end -end \ No newline at end of file + + def render_filter_help(filter, options = {}) + html = content_tag :td, :width => "25px" do + if filter.help_text + render_widget Widget::Controls::Help, filter.help_text + end + end + if canvas = options[:to] + canvas << "\n" << html + else + html + end + end +end diff --git a/lib/widget/group_bys.rb b/lib/widget/group_bys.rb index 5265204d6a..49cf7b84c7 100644 --- a/lib/widget/group_bys.rb +++ b/lib/widget/group_bys.rb @@ -24,7 +24,7 @@ class Widget::GroupBys < Widget::Base end end - def render_group(type, initially_selected) + def render_group(type, initially_selected, show_help = false) initially_selected = initially_selected.map do |group_by| [group_by.class.underscore_name, l(group_by.class.label)] end @@ -46,13 +46,24 @@ class Widget::GroupBys < Widget::Base end.join.html_safe content end - out.html_safe + if show_help + maybe_with_help out.html_safe, { + :icon => { + :class => 'group-by-icon' + }, + :tooltip => { + :class => 'group-by-tip' + } + } + else + out.html_safe + end end end def render content_tag :div, :id => 'group_by_area' do - out = render_group 'columns', @query.group_bys(:column) + out = render_group 'columns', @query.group_bys(:column), true out += render_group 'rows', @query.group_bys(:row) out.html_safe end diff --git a/lib/widget/settings.rb b/lib/widget/settings.rb index f134cef9c2..aa259f07ab 100644 --- a/lib/widget/settings.rb +++ b/lib/widget/settings.rb @@ -3,11 +3,13 @@ class Widget::Settings < Widget::Base form_tag("#", {:id => 'query_form', :method => :post}) do content_tag :div, :id => "query_form_content" do - fieldsets = render_widget Widget::Settings::Fieldset, @query, { :type => "filter" } do + fieldsets = render_widget Widget::Settings::Fieldset, @query, + { :type => "filter", :help_text => self.filter_help } do render_widget Widget::Filters, @query end - fieldsets += render_widget Widget::Settings::Fieldset, @query, { :type => "group_by" } do + fieldsets += render_widget Widget::Settings::Fieldset, @query, + { :type => "group_by", :help_text => self.group_by_help } do render_widget Widget::GroupBys, @query end @@ -18,9 +20,24 @@ class Widget::Settings < Widget::Base render_widget(Widget::Controls::Clear, @query, :to => widgets) render_widget(Widget::Controls::Delete, @query, :to => widgets) end - fieldsets + controls end end end + + def filter_help + if help_text.kind_of?(Array) + help_text[0] + else + nil + end + end + + def group_by_help + if help_text.kind_of?(Array) + help_text[1] + else + nil + end + end end diff --git a/lib/widget/settings/fieldset.rb b/lib/widget/settings/fieldset.rb index c4f71d2d22..82ea1f4ef6 100644 --- a/lib/widget/settings/fieldset.rb +++ b/lib/widget/settings/fieldset.rb @@ -7,8 +7,14 @@ class Widget::Settings::Fieldset < Widget::Base end def render + hash = self.hash content_tag :fieldset, :id => @id, :class => "collapsible collapsed" do - html = content_tag :legend, l(@label), :onclick => "toggleFieldset(this);" #FIXME: onclick + content = maybe_with_help l(@label), + :show_at_id => hash.to_s, + :icon => { :class => "#{@type}-legend-icon" }, + :tooltip => { :class => "#{@type}-legend-tip" } + html = content_tag :legend, content, + {:onclick => "toggleFieldset(this);", :id => hash.to_s}, false #FIXME: onclick html + yield end end