Merge branch 'feature/widgets' into feature/partition

pull/6827/head
Markus Kahl 14 years ago
commit 6c5697edae
  1. BIN
      assets/images/icon_info_red.gif
  2. 159
      assets/javascripts/cordinc_tooltip.js
  3. 2
      assets/javascripts/reporting/filters.js
  4. 55
      assets/stylesheets/help.css
  5. 6
      assets/stylesheets/reporting.css
  6. 5
      config/locales/de.yml
  7. 4
      config/locales/en.yml
  8. 5
      lib/report.rb
  9. 16
      lib/report/chainable.rb
  10. 2
      lib/report/controller.rb
  11. 43
      lib/report/query_utils.rb
  12. 1
      lib/report/sql_statement.rb
  13. 2
      lib/report/table.rb
  14. 30
      lib/widget/base.rb
  15. 3
      lib/widget/controls/clear.rb
  16. 15
      lib/widget/controls/delete.rb
  17. 55
      lib/widget/controls/help.rb
  18. 4
      lib/widget/controls/save_as.rb
  19. 24
      lib/widget/filters.rb
  20. 15
      lib/widget/group_bys.rb
  21. 23
      lib/widget/settings.rb
  22. 8
      lib/widget/settings/fieldset.rb

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 B

@ -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();
}
})

@ -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");
});

@ -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;
}

@ -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;
}

@ -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"

@ -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

@ -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

@ -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

@ -65,7 +65,7 @@ module Report::Controller
else
raise ActiveRecord::RecordNotFound
end
redirect_to :action => "index"
redirect_to :action => "index", :default => 1
end
##

@ -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

@ -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"}" \

@ -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

@ -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

@ -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

@ -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")), "#",
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)
popup = content_tag :div, :id => "delete_form", :class => "button_form" do
: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

@ -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

@ -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

@ -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
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

@ -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
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

@ -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

@ -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

Loading…
Cancel
Save