netzke-basepack 0.4.2 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. data/.autotest +1 -0
  2. data/.gitignore +6 -0
  3. data/{CHANGELOG → CHANGELOG.rdoc} +26 -0
  4. data/README.rdoc +11 -11
  5. data/Rakefile +37 -11
  6. data/TODO.rdoc +8 -0
  7. data/VERSION +1 -0
  8. data/javascripts/basepack.js +71 -28
  9. data/lib/app/models/netzke_auto_column.rb +56 -0
  10. data/lib/netzke-basepack.rb +5 -3
  11. data/lib/netzke/accordion_panel.rb +69 -67
  12. data/lib/netzke/active_record/basepack.rb +104 -0
  13. data/lib/netzke/active_record/data_accessor.rb +33 -0
  14. data/lib/netzke/basic_app.rb +233 -124
  15. data/lib/netzke/border_layout_panel.rb +97 -98
  16. data/lib/netzke/configuration_panel.rb +24 -0
  17. data/lib/netzke/data_accessor.rb +71 -0
  18. data/lib/netzke/ext.rb +6 -0
  19. data/lib/netzke/field_model.rb +1 -1
  20. data/lib/netzke/fields_configurator.rb +62 -37
  21. data/lib/netzke/form_panel.rb +161 -51
  22. data/lib/netzke/form_panel_api.rb +74 -0
  23. data/lib/netzke/form_panel_js.rb +129 -0
  24. data/lib/netzke/grid_panel.rb +385 -80
  25. data/lib/netzke/grid_panel_api.rb +352 -0
  26. data/lib/netzke/grid_panel_extras/javascripts/rows-dd.js +280 -0
  27. data/lib/netzke/grid_panel_js.rb +721 -0
  28. data/lib/netzke/masquerade_selector.rb +53 -0
  29. data/lib/netzke/panel.rb +9 -0
  30. data/lib/netzke/plugins/configuration_tool.rb +121 -0
  31. data/lib/netzke/property_editor.rb +95 -7
  32. data/lib/netzke/property_editor_extras/helper_model.rb +55 -34
  33. data/lib/netzke/search_panel.rb +62 -0
  34. data/lib/netzke/tab_panel.rb +97 -37
  35. data/lib/netzke/table_editor.rb +49 -44
  36. data/lib/netzke/tree_panel.rb +15 -16
  37. data/lib/netzke/wrapper.rb +29 -5
  38. data/netzke-basepack.gemspec +151 -19
  39. data/stylesheets/basepack.css +5 -0
  40. data/test/app_root/app/models/book.rb +1 -1
  41. data/test/app_root/db/migrate/20081222035855_create_netzke_preferences.rb +1 -1
  42. data/test/unit/accordion_panel_test.rb +1 -2
  43. data/test/unit/active_record_basepack_test.rb +54 -0
  44. data/test/unit/grid_panel_test.rb +8 -12
  45. data/test/unit/helper_model_test.rb +30 -0
  46. metadata +69 -78
  47. data/Manifest +0 -86
  48. data/TODO +0 -3
  49. data/lib/app/models/netzke_hash_record.rb +0 -180
  50. data/lib/app/models/netzke_layout_item.rb +0 -11
  51. data/lib/netzke/ar_ext.rb +0 -269
  52. data/lib/netzke/configuration_tool.rb +0 -80
  53. data/lib/netzke/container.rb +0 -77
  54. data/lib/netzke/db_fields.rb +0 -44
  55. data/lib/netzke/fields_configurator_old.rb +0 -62
  56. data/lib/netzke/form_panel_extras/interface.rb +0 -56
  57. data/lib/netzke/form_panel_extras/js_builder.rb +0 -134
  58. data/lib/netzke/grid_panel_extras/interface.rb +0 -206
  59. data/lib/netzke/grid_panel_extras/js_builder.rb +0 -352
  60. data/test/unit/ar_ext_test.rb +0 -53
  61. data/test/unit/netzke_hash_record_test.rb +0 -52
  62. data/test/unit/netzke_layout_item_test.rb +0 -28
@@ -1,39 +1,57 @@
1
1
  module Netzke
2
+ # = FormPanel
3
+ #
4
+ # Represents Ext.form.FormPanel
5
+ #
6
+ # == Configuration
7
+ # * <tt>:data_class_name</tt> - name of the ActiveRecord model that provides data to this GridPanel.
8
+ # * <tt>:record</tt> - record to be displayd in the form. Takes precedence over <tt>:record_id</tt>
9
+ # * <tt>:record_id</tt> - id of the record to be displayd in the form. Also see <tt>:record</tt>
10
+ #
11
+ # In the <tt>:ext_config</tt> hash (see Netzke::Base) the following FormPanel specific options are available:
12
+ #
13
+ # * <tt>:mode</tt> - when set to <tt>:config</tt>, FormPanel loads in configuration mode
2
14
  class FormPanel < Base
15
+ include Netzke::FormPanelJs # javascript (client-side)
16
+ include Netzke::FormPanelApi # API (server-side)
17
+ include Netzke::DataAccessor # some code shared between GridPanel, FormPanel, and other widgets that use database attributes
18
+
3
19
  # Class-level configuration with defaults
4
20
  def self.config
5
21
  set_default_config({
6
- :config_tool_enabled_by_default => false,
7
- :persistent_layout_enabled_by_default => true,
8
- :persistent_config_enabled_by_default => true
22
+ :config_tool_available => true,
23
+
24
+ :default_config => {
25
+ :ext_config => {
26
+ :bbar => %w{ apply },
27
+ :tools => %w{ }
28
+ },
29
+ :persistent_config => false
30
+ }
9
31
  })
10
32
  end
11
-
12
- include Netzke::FormPanelExtras::JsBuilder
13
- include Netzke::FormPanelExtras::Interface
14
- include Netzke::DbFields # database field operations
15
33
 
16
- # extra javascripts
17
- js_include %w{ xcheckbox }.map{|js| "#{File.dirname(__FILE__)}/form_panel_extras/javascripts/#{js}.js"}
34
+ # Extra javascripts
35
+ def self.include_js
36
+ [
37
+ "#{File.dirname(__FILE__)}/form_panel_extras/javascripts/xcheckbox.js"
38
+ ]
39
+ end
18
40
 
19
- interface :submit, :load, :get_cb_choices
41
+ api :submit, :load, :get_combobox_options
20
42
 
21
- def self.widget_type
22
- :form
43
+ attr_accessor :record
44
+
45
+ def initialize(*args)
46
+ super
47
+ apply_helpers
48
+ @record = config[:record] || data_class && data_class.find_by_id(config[:record_id])
23
49
  end
24
50
 
25
- # default instance-level configuration
26
- def initial_config
27
- {
28
- :ext_config => {
29
- :config_tool => self.class.config[:config_tool_enabled_by_default],
30
- },
31
-
32
- :persistent_layout => self.class.config[:persistent_layout_enabled_by_default],
33
- :persistent_config => self.class.config[:persistent_config_enabled_by_default]
34
- }
51
+ def data_class
52
+ @data_class ||= config[:data_class_name] && config[:data_class_name].constantize
35
53
  end
36
-
54
+
37
55
  def configuration_widgets
38
56
  res = []
39
57
 
@@ -41,64 +59,156 @@ module Netzke
41
59
  :name => 'fields',
42
60
  :widget_class_name => "FieldsConfigurator",
43
61
  :active => true,
44
- :widget => self
45
- } if config[:persistent_layout]
62
+ :widget => self,
63
+ :persistent_config => true
64
+ }
46
65
 
47
66
  res << {
48
67
  :name => 'general',
49
68
  :widget_class_name => "PropertyEditor",
50
- :widget_name => id_name,
69
+ :widget => self,
51
70
  :ext_config => {:title => false}
52
71
  }
53
72
 
54
73
  res
55
74
  end
56
75
 
57
- def tools
58
- %w{ refresh }
59
- end
60
-
61
76
  def actions
62
77
  {
63
78
  :apply => {:text => 'Apply'}
64
79
  }
65
80
  end
66
81
 
67
- def bbar
68
- persistent_config[:bottom_bar] ||= config[:bbar] == false ? nil : config[:bbar] || %w{ apply }
82
+ def columns
83
+ @columns ||= get_columns.convert_keys{|k| k.to_sym}
69
84
  end
70
85
 
71
- def fields
72
- @fields ||= get_fields
73
- end
74
-
75
86
  # parameters used to instantiate the JS object
76
87
  def js_config
77
88
  res = super
78
- res.merge!(:fields => fields)
89
+ res.merge!(:clmns => columns)
79
90
  res.merge!(:data_class_name => config[:data_class_name])
80
- res.merge!(:record_data => config[:record].to_array(fields)) if config[:record]
81
91
  res
82
92
  end
83
93
 
84
- protected
94
+ # columns to be displayed by the FieldConfigurator (which is GridPanel-based)
95
+ def self.config_columns
96
+ [
97
+ {:name => :name, :type => :string, :editor => :combobox, :width => 200},
98
+ {:name => :hidden, :type => :boolean, :editor => :checkbox, :width => 40, :header => "Excl"},
99
+ {:name => :disabled, :type => :boolean, :editor => :checkbox, :width => 40, :header => "Dis"},
100
+ {:name => :xtype, :type => :string},
101
+ {:name => :value, :type => :string},
102
+ {:name => :field_label, :type => :string},
103
+ {:name => :input_type, :type => :string}
104
+ ]
105
+ end
106
+
107
+ def self.property_fields
108
+ res = [
109
+ {:name => :ext_config__title, :type => :string},
110
+ {:name => :ext_config__header, :type => :boolean, :default => true}
111
+ # {:name => :ext_config__bbar, :type => :json},
112
+ # {:name => :ext_config__prohibit_create, :type => :boolean},
113
+ # {:name => :ext_config__prohibit_update, :type => :boolean},
114
+ # {:name => :ext_config__prohibit_delete, :type => :boolean},
115
+ # {:name => :ext_config__prohibit_read, :type => :boolean}
116
+ ]
117
+
118
+ res
119
+
120
+ end
121
+
122
+ # Normalized columns
123
+ def normalized_columns
124
+ @normalized_columns ||= normalize_columns(columns)
125
+ end
85
126
 
86
- def get_fields
87
- if config[:persistent_layout]
88
- NetzkeLayoutItem.widget = id_name
89
- NetzkeLayoutItem.data = default_db_fields if NetzkeLayoutItem.all.empty?
90
- NetzkeLayoutItem.all
127
+
128
+ def get_columns
129
+ if config[:persistent_config]
130
+ persistent_config['layout__columns'] ||= default_columns
131
+ res = normalize_array_of_columns(persistent_config['layout__columns'])
91
132
  else
92
- default_db_fields
93
- end.map{ |r| r.reject{ |k,v| k == :id } }
133
+ res = default_columns
134
+ end
135
+
136
+ # merge values for each field if the record is specified
137
+ @record && res.map! do |c|
138
+ value = @record.send(normalize_column(c)[:name])
139
+ value.nil? ? c : normalize_column(c).merge(:value => value)
140
+ end
141
+
142
+ res
94
143
  end
95
-
96
144
 
97
- # def available_permissions
98
- # %w{ read update }
99
- # end
145
+ XTYPE_MAP = {
146
+ :integer => :numberfield,
147
+ :boolean => :xcheckbox,
148
+ :date => :datefield,
149
+ :datetime => :xdatetime,
150
+ :text => :textarea,
151
+ :json => :jsonfield
152
+ # :string => :textfield
153
+ }
100
154
 
101
- include ConfigurationTool # it will load aggregation with name :properties into a modal window
155
+ def default_columns
156
+ # columns specified in widget's config
157
+ columns_from_config = config[:columns] && normalize_columns(config[:columns])
102
158
 
159
+ if columns_from_config
160
+ # reverse-merge each column hash from config with each column hash from exposed_attributes (columns from config have higher priority)
161
+ for c in columns_from_config
162
+ corresponding_exposed_column = predefined_columns.find{ |k| k[:name] == c[:name] }
163
+ c.reverse_merge!(corresponding_exposed_column) if corresponding_exposed_column
164
+ end
165
+ columns_for_create = columns_from_config
166
+ elsif predefined_columns
167
+ # we didn't have columns configured in widget's config, so, use the columns from the data class
168
+ columns_for_create = predefined_columns
169
+ else
170
+ raise ArgumentError, "No columns specified for widget '#{id_name}'"
171
+ end
172
+
173
+ columns_for_create.map! do |c|
174
+ if data_class
175
+ # Try to figure out the configuration from data class
176
+ # detect :assoc__method
177
+ if c[:name].to_s.index('__')
178
+ assoc_name, method = c[:name].to_s.split('__').map(&:to_sym)
179
+ if assoc = data_class.reflect_on_association(assoc_name)
180
+ assoc_column = assoc.klass.columns_hash[method.to_s]
181
+ assoc_method_type = assoc_column.try(:type)
182
+ if assoc_method_type
183
+ c[:xtype] ||= XTYPE_MAP[assoc_method_type] == :xcheckbox ? :xcheckbox : :combobox
184
+ end
185
+ end
186
+ end
187
+
188
+ # detect association column (e.g. :category_id)
189
+ if assoc = data_class.reflect_on_all_associations.detect{|a| a.primary_key_name.to_sym == c[:name]}
190
+ c[:xtype] ||= :combobox
191
+ assoc_method = %w{name title label id}.detect{|m| (assoc.klass.instance_methods + assoc.klass.column_names).include?(m) } || assoc.klass.primary_key
192
+ c[:name] = "#{assoc.name}__#{assoc_method}".to_sym
193
+ end
194
+ c[:hidden] = true if c[:name] == data_class.primary_key.to_sym && c[:hidden].nil? # hide ID column by default
195
+
196
+ end
197
+
198
+ # detect column type
199
+ type = c[:type] || data_class && data_class.columns_hash[c[:name].to_s].try(:type) || :string
200
+ c[:type] ||= type
201
+
202
+ c[:xtype] ||= XTYPE_MAP[type] unless XTYPE_MAP[type].nil?
203
+
204
+ # if the column is finally simply {:name => "something"}, cut it down to "something"
205
+ c.reject{ |k,v| k == :name }.empty? ? c[:name] : c
206
+ end
207
+
208
+ columns_for_create
209
+
210
+ end
211
+
212
+ include Plugins::ConfigurationTool if config[:config_tool_available] # it will load ConfigurationPanel into a modal window
103
213
  end
104
214
  end
@@ -0,0 +1,74 @@
1
+ module Netzke
2
+ module FormPanelApi
3
+ # API handling form submission
4
+ def submit(params)
5
+ data_hsh = ActiveSupport::JSON.decode(params[:data])
6
+ create_or_update_record(data_hsh)
7
+ end
8
+
9
+ # Creates/updates a record from hash
10
+ def create_or_update_record(hsh)
11
+ klass = config[:data_class_name].constantize
12
+ @record = klass.find_by_id(hsh.delete("id"))
13
+ success = true
14
+
15
+ @record = klass.new if @record.nil?
16
+
17
+ hsh.each_pair do |k,v|
18
+ begin
19
+ @record.send("#{k}=",v)
20
+ rescue StandardError => exc
21
+ logger.debug "!!! FormPanelApi#create_or_update_record exception: #{exc.inspect}\n"
22
+ flash :error => exc.message
23
+ success = false
24
+ break
25
+ end
26
+ end
27
+
28
+ if success && @record.save
29
+ {:set_form_values => array_of_values}
30
+ else
31
+ # flash eventual errors
32
+ @record.errors.each_full do |msg|
33
+ flash :error => msg
34
+ end
35
+ {:feedback => @flash}
36
+ end
37
+ end
38
+
39
+ # API handling form load
40
+ # def load(params)
41
+ # klass = config[:data_class_name].constantize
42
+ # case params[:neighbour]
43
+ # when "previous" then @record = klass.previous(params[:id])
44
+ # when "next" then @record = klass.next(params[:id])
45
+ # else @record = klass.find(params[:id])
46
+ # end
47
+ # {:data => [array_of_values]}
48
+ # end
49
+
50
+ def load(params)
51
+ @record = data_class && data_class.find_by_id(params[:id])
52
+ {:set_form_values => array_of_values}
53
+ end
54
+
55
+ # API that returns options for a combobox
56
+ def get_combobox_options(params)
57
+ column = params[:column]
58
+ query = params[:query]
59
+
60
+ {:data => config[:data_class_name].constantize.options_for(column, query).map{|s| [s]}}
61
+ end
62
+
63
+ def configuration_panel__fields__get_combobox_options(params)
64
+ query = params[:query]
65
+ {:data => (predefined_columns.map{ |c| c[:name].to_s }).grep(/^#{query}/).map{ |n| [n] }}.to_nifty_json
66
+ end
67
+
68
+ # Returns array of form values according to the configured columns
69
+ def array_of_values
70
+ @record && @record.to_array(columns, self)
71
+ end
72
+
73
+ end
74
+ end
@@ -0,0 +1,129 @@
1
+ module Netzke
2
+ module FormPanelJs
3
+ def self.included(base)
4
+ base.extend ClassMethods
5
+ end
6
+
7
+ module ClassMethods
8
+ def js_base_class
9
+ "Ext.FormPanel"
10
+ end
11
+
12
+ def js_extend_properties
13
+ {
14
+ :body_style => 'padding:5px 5px 0',
15
+ :auto_scroll => true,
16
+ :label_width => 150,
17
+ :default_type => 'textfield',
18
+ # :label_align => 'top',
19
+
20
+ :init_component => <<-END_OF_JAVASCRIPT.l,
21
+ function(){
22
+ var recordFields = []; // Record
23
+ this.items = [];
24
+ var index = 0;
25
+
26
+ // Process columns
27
+ Ext.each(this.clmns, function(field){
28
+ if (typeof field == 'string') field = {name:field}; // normalize field
29
+ if (!field.hidden || field.name == 'id') {
30
+ recordFields.push({name:field.name, mapping:index++});
31
+
32
+ var defaultColumnConfig = Ext.apply({}, this.defaultColumnConfig);
33
+ var columnConfig = Ext.apply(defaultColumnConfig, field);
34
+
35
+ // apply dynamically defined properties
36
+ Ext.apply(columnConfig, {
37
+ fieldLabel: columnConfig.fieldLabel || columnConfig.name.humanize(),
38
+ hideLabel: columnConfig.hidden, // completely hide fields marked "hidden"
39
+ parentId: this.id,
40
+ name: columnConfig.name,
41
+ checked: columnConfig.xtype == "xcheckbox" ? columnConfig.value : null // checkbox state
42
+ });
43
+
44
+ this.items.push(columnConfig);
45
+ }
46
+ }, this);
47
+
48
+ var Record = Ext.data.Record.create(recordFields);
49
+ this.reader = new Ext.data.RecordArrayReader({root:"data"}, Record);
50
+
51
+ delete this.clmns; // we don't need them anymore
52
+
53
+ // Now let Ext.form.FormPanel do the rest
54
+ Ext.netzke.cache.FormPanel.superclass.initComponent.call(this);
55
+
56
+ // Apply event
57
+ this.addEvents('apply');
58
+ }
59
+ END_OF_JAVASCRIPT
60
+
61
+ # Defaults for each field
62
+ :defaults => {
63
+ # :anchor => '-20', # to leave some space for the scrollbar
64
+ :width => 180,
65
+ :listeners => {
66
+ # On "return" key, submit the form
67
+ :specialkey => {
68
+ :fn => <<-END_OF_JAVASCRIPT.l
69
+ function(field, event){
70
+ if (event.getKey() == 13) this.ownerCt.apply();
71
+ }
72
+ END_OF_JAVASCRIPT
73
+ }
74
+ }
75
+ },
76
+
77
+ :default_column_config => config_columns.inject({}){ |r, c| r.merge!({
78
+ c[:name] => c[:default]
79
+ }) },
80
+
81
+ :set_form_values => <<-END_OF_JAVASCRIPT.l,
82
+ function(values){
83
+ this.form.loadRecord(this.reader.readRecords({data:[values]}).records[0]);
84
+ }
85
+ END_OF_JAVASCRIPT
86
+
87
+ :load_record => <<-END_OF_JAVASCRIPT.l,
88
+ function(id, neighbour){
89
+ this.load({id:id});
90
+ // var proxy = new Ext.data.HttpProxy({url:this.initialConfig.api.load});
91
+ // proxy.load({id:id, neighbour:neighbour}, this.reader, function(data){
92
+ // if (data){
93
+ // this.form.loadRecord(data.records[0])
94
+ // }
95
+ // }, this)
96
+ }
97
+ END_OF_JAVASCRIPT
98
+
99
+ # :previous => <<-END_OF_JAVASCRIPT.l,
100
+ # function() {
101
+ # var currentId = this.form.getValues().id;
102
+ # this.loadRecord(currentId, 'previous');
103
+ # }
104
+ # END_OF_JAVASCRIPT
105
+ #
106
+ # :next => <<-END_OF_JAVASCRIPT.l,
107
+ # function() {
108
+ # var currentId = this.form.getValues().id;
109
+ # this.loadRecord(currentId, 'next');
110
+ # }
111
+ # END_OF_JAVASCRIPT
112
+
113
+ :apply => <<-END_OF_JAVASCRIPT.l
114
+ function() {
115
+ if (this.fireEvent('apply', this)) {
116
+ var values = this.form.getValues();
117
+ for (var k in values) {
118
+ if (values[k] == "") {delete values[k]}
119
+ }
120
+ this.submit(Ext.apply((this.baseParams || {}), {data:Ext.encode(values)}));
121
+ }
122
+ }
123
+ END_OF_JAVASCRIPT
124
+ }
125
+ end
126
+
127
+ end
128
+ end
129
+ end