netzke-basepack 0.7.4 → 0.7.5

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 (74) hide show
  1. data/.travis.yml +11 -0
  2. data/CHANGELOG.rdoc +10 -0
  3. data/README.md +36 -2
  4. data/Rakefile +1 -3
  5. data/config/ci/before-travis.sh +28 -0
  6. data/lib/netzke/active_record.rb +10 -8
  7. data/lib/netzke/active_record/attributes.rb +28 -17
  8. data/lib/netzke/active_record/relation_extensions.rb +3 -1
  9. data/lib/netzke/basepack.rb +10 -2
  10. data/lib/netzke/basepack/action_column.rb +6 -8
  11. data/lib/netzke/basepack/data_accessor.rb +11 -174
  12. data/lib/netzke/basepack/data_adapters/abstract_adapter.rb +164 -0
  13. data/lib/netzke/basepack/data_adapters/active_record_adapter.rb +279 -0
  14. data/lib/netzke/basepack/data_adapters/data_mapper_adapter.rb +264 -0
  15. data/lib/netzke/basepack/data_adapters/sequel_adapter.rb +260 -0
  16. data/lib/netzke/basepack/form_panel.rb +3 -3
  17. data/lib/netzke/basepack/form_panel/fields.rb +6 -10
  18. data/lib/netzke/basepack/form_panel/javascripts/form_panel.js +1 -0
  19. data/lib/netzke/basepack/form_panel/services.rb +15 -16
  20. data/lib/netzke/basepack/grid_panel.rb +16 -10
  21. data/lib/netzke/basepack/grid_panel/columns.rb +6 -7
  22. data/lib/netzke/basepack/grid_panel/javascripts/event_handling.js +29 -27
  23. data/lib/netzke/basepack/grid_panel/services.rb +13 -90
  24. data/lib/netzke/basepack/paging_form_panel.rb +3 -3
  25. data/lib/netzke/basepack/query_builder.rb +2 -0
  26. data/lib/netzke/basepack/query_builder/javascripts/query_builder.js +29 -19
  27. data/lib/netzke/basepack/search_panel.rb +6 -3
  28. data/lib/netzke/basepack/search_panel/javascripts/search_panel.js +2 -1
  29. data/lib/netzke/basepack/search_window.rb +2 -1
  30. data/lib/netzke/basepack/version.rb +1 -1
  31. data/lib/netzke/data_mapper.rb +18 -0
  32. data/lib/netzke/data_mapper/attributes.rb +273 -0
  33. data/lib/netzke/data_mapper/combobox_options.rb +11 -0
  34. data/lib/netzke/data_mapper/relation_extensions.rb +38 -0
  35. data/lib/netzke/sequel.rb +18 -0
  36. data/lib/netzke/sequel/attributes.rb +274 -0
  37. data/lib/netzke/sequel/combobox_options.rb +10 -0
  38. data/lib/netzke/sequel/relation_extensions.rb +40 -0
  39. data/netzke-basepack.gemspec +24 -13
  40. data/test/basepack_test_app/Gemfile +33 -8
  41. data/test/basepack_test_app/Gemfile.lock +98 -79
  42. data/test/basepack_test_app/Guardfile +46 -0
  43. data/test/basepack_test_app/app/components/book_grid_with_persistence.rb +3 -0
  44. data/test/basepack_test_app/app/components/extras/book_presentation.rb +10 -3
  45. data/test/basepack_test_app/app/models/address.rb +27 -1
  46. data/test/basepack_test_app/app/models/author.rb +28 -0
  47. data/test/basepack_test_app/app/models/book.rb +43 -0
  48. data/test/basepack_test_app/app/models/book_with_custom_primary_key.rb +22 -0
  49. data/test/basepack_test_app/app/models/role.rb +21 -0
  50. data/test/basepack_test_app/app/models/user.rb +24 -0
  51. data/test/basepack_test_app/config/database.yml.sample +11 -10
  52. data/test/basepack_test_app/config/database.yml.travis +15 -0
  53. data/test/basepack_test_app/config/initializers/data_mapper_logging.rb +3 -0
  54. data/test/basepack_test_app/config/initializers/sequel.rb +26 -0
  55. data/test/basepack_test_app/db/schema.rb +0 -3
  56. data/test/basepack_test_app/features/grid_panel.feature +28 -8
  57. data/test/basepack_test_app/features/grid_sorting.feature +6 -6
  58. data/test/basepack_test_app/features/paging_form_panel.feature +13 -13
  59. data/test/basepack_test_app/features/search_in_grid.feature +31 -31
  60. data/test/basepack_test_app/features/step_definitions/generic_steps.rb +3 -1
  61. data/test/basepack_test_app/features/support/env.rb +17 -4
  62. data/test/basepack_test_app/lib/tasks/travis.rake +7 -0
  63. data/test/basepack_test_app/spec/components/form_panel_spec.rb +2 -2
  64. data/test/basepack_test_app/spec/data_adapter/adapter_spec.rb +68 -0
  65. data/test/basepack_test_app/spec/{active_record → data_adapter}/attributes_spec.rb +12 -4
  66. data/test/basepack_test_app/spec/data_adapter/relation_extensions_spec.rb +125 -0
  67. data/test/basepack_test_app/spec/spec_helper.rb +9 -0
  68. data/test/unit/active_record_basepack_test.rb +1 -1
  69. data/test/unit/grid_panel_test.rb +1 -1
  70. metadata +26 -31
  71. data/app/models/netzke_field_list.rb +0 -261
  72. data/app/models/netzke_model_attr_list.rb +0 -21
  73. data/app/models/netzke_persistent_array_auto_model.rb +0 -57
  74. data/test/basepack_test_app/spec/active_record/relation_extensions_spec.rb +0 -44
@@ -129,7 +129,7 @@ module Netzke
129
129
 
130
130
  if columns_from_config
131
131
  # automatically add a column that reflects the primary key (unless specified in the config)
132
- columns_from_config.insert(0, {:name => data_class.primary_key}) unless columns_from_config.any?{ |c| c[:name] == data_class.primary_key }
132
+ columns_from_config.insert(0, {:name => data_class.primary_key.to_s}) unless columns_from_config.any?{ |c| c[:name] == data_class.primary_key }
133
133
 
134
134
  # reverse-merge each column hash from config with each column hash from exposed_attributes
135
135
  # (columns from config have higher priority)
@@ -225,11 +225,10 @@ module Netzke
225
225
 
226
226
  # Detects an association column and sets up the proper editor.
227
227
  def set_default_association_editor(c)
228
- assoc, assoc_method = assoc_and_assoc_method_for_attr(c)
228
+ assoc, assoc_method = c[:name].split('__')
229
229
  return unless assoc
230
230
 
231
- assoc_column = assoc.klass.columns_hash[assoc_method]
232
- assoc_method_type = assoc_column.try(:type)
231
+ assoc_method_type = data_adapter.get_assoc_property_type assoc, assoc_method
233
232
 
234
233
  # if association column is boolean, display a checkbox (or alike), otherwise - a combobox (or alike)
235
234
  if c[:nested_attribute]
@@ -361,12 +360,12 @@ module Netzke
361
360
 
362
361
  def columns_default_values
363
362
  columns.inject({}) do |r,c|
364
- assoc, assoc_method = assoc_and_assoc_method_for_attr(c)
363
+ assoc_name, assoc_method = c[:name].split '__'
365
364
  if c[:default_value].nil?
366
365
  r
367
366
  else
368
- if assoc
369
- r.merge(assoc.options[:foreign_key] || assoc.name.to_s.foreign_key => c[:default_value])
367
+ if assoc_method
368
+ r.merge(data_adapter.foreign_key_for(assoc_name) || data_adapter.foreign_key_for(assoc_name) => c[:default_value])
370
369
  else
371
370
  r.merge(c[:name] => c[:default_value])
372
371
  end
@@ -37,38 +37,40 @@
37
37
  },
38
38
 
39
39
  onApply: function(){
40
- var newRecords = [],
41
- updatedRecords = [],
42
- store = this.getStore();
43
-
44
- Ext.each(store.getUpdatedRecords().concat(store.getNewRecords()),
45
- function(r) {
46
- if (r.isNew) {
47
- newRecords.push(r.data); // HACK: r.data seems private
48
- } else {
49
- updatedRecords.push(Ext.apply(r.getChanges(), {id:r.getId()}));
50
- }
51
- },
52
- this);
40
+ if (this.fireEvent('apply')) {
41
+ var newRecords = [],
42
+ updatedRecords = [],
43
+ store = this.getStore();
53
44
 
54
- if (newRecords.length > 0 || updatedRecords.length > 0) {
55
- var params = {};
45
+ Ext.each(store.getUpdatedRecords().concat(store.getNewRecords()),
46
+ function(r) {
47
+ if (r.isNew) {
48
+ newRecords.push(r.data); // HACK: r.data seems private
49
+ } else {
50
+ updatedRecords.push(Ext.apply(r.getChanges(), {id:r.getId()}));
51
+ }
52
+ },
53
+ this);
56
54
 
57
- if (newRecords.length > 0) {
58
- params.created_records = Ext.encode(newRecords);
59
- }
55
+ if (newRecords.length > 0 || updatedRecords.length > 0) {
56
+ var params = {};
60
57
 
61
- if (updatedRecords.length > 0) {
62
- params.updated_records = Ext.encode(updatedRecords);
63
- }
58
+ if (newRecords.length > 0) {
59
+ params.created_records = Ext.encode(newRecords);
60
+ }
64
61
 
65
- if (this.getStore().getProxy().extraParams !== {}) {
66
- params.base_params = Ext.encode(this.getStore().getProxy().extraParams);
67
- }
62
+ if (updatedRecords.length > 0) {
63
+ params.updated_records = Ext.encode(updatedRecords);
64
+ }
68
65
 
69
- this.postData(params);
70
- }
66
+ if (this.getStore().getProxy().extraParams !== {}) {
67
+ params.base_params = Ext.encode(this.getStore().getProxy().extraParams);
68
+ }
71
69
 
70
+ this.postData(params);
71
+ }
72
+ }
73
+ this.fireEvent('afterApply', this);
72
74
  },
73
75
 
74
76
  // Handlers for tools
@@ -174,4 +176,4 @@
174
176
  this.searchWindow.destroy();
175
177
  }
176
178
  }
177
- }
179
+ }
@@ -32,7 +32,7 @@ module Netzke
32
32
  endpoint :delete_data do |params|
33
33
  if !config[:prohibit_delete]
34
34
  record_ids = ActiveSupport::JSON.decode(params[:records])
35
- data_class.destroy(record_ids)
35
+ data_adapter.destroy(record_ids)
36
36
  on_data_changed
37
37
  {:netzke_feedback => I18n.t('netzke.basepack.grid_panel.deleted_n_records', :n => record_ids.size), :load_store_data => get_data}
38
38
  else
@@ -83,16 +83,7 @@ module Netzke
83
83
  end
84
84
 
85
85
  endpoint :move_rows do |params|
86
- if defined?(ActsAsList) && data_class.ancestors.include?(ActsAsList::InstanceMethods)
87
- ids = JSON.parse(params[:ids]).reverse
88
- ids.each_with_index do |id, i|
89
- r = data_class.find(id)
90
- r.insert_at(params[:new_index].to_i + i + 1)
91
- end
92
- on_data_changed
93
- else
94
- raise RuntimeError, "Data class should 'acts_as_list' to support moving rows"
95
- end
86
+ data_adapter.move_records(params)
96
87
  {}
97
88
  end
98
89
 
@@ -155,12 +146,8 @@ module Netzke
155
146
  if !config[:prohibit_read]
156
147
  {}.tap do |res|
157
148
  records = get_records(params)
158
- res[:data] = records.map{|r| r.to_array(columns(:with_meta => true))}
159
- res[:total] = records.total_entries if config[:enable_pagination]
160
-
161
- # provide association values for all records at once
162
- # assoc_values = get_association_values(records, columns)
163
- # res[:set_association_values] = assoc_values.literalize_keys if assoc_values.present?
149
+ res[:data] = records.map{|r| r.netzke_array(columns(:with_meta => true))}
150
+ res[:total] = count_records(params) if config[:enable_pagination]
164
151
  end
165
152
  else
166
153
  flash :error => "You don't have permissions to read data"
@@ -170,68 +157,18 @@ module Netzke
170
157
 
171
158
  protected
172
159
 
173
- # Returns all values for association columns, per column, per associated record id, e.g.:
174
- # {
175
- # :author__first_name => {1 => "Vladimir", 2 => "Herman"},
176
- # :author__last_name => {1 => "Nabokov", 2 => "Hesse"}
177
- # }
178
- # This is used to display the association by the specified method instead by the foreign key
179
- # def get_association_values(records, columns)
180
- # columns.select{ |c| c[:name].index("__") }.each.inject({}) do |r,c|
181
- # column_values = {}
182
- # records.each{ |r| column_values[r.value_for_attribute(c)] = r.value_for_attribute(c, true) }
183
- # r.merge(c[:name] => column_values)
184
- # end
185
- # end
186
-
187
160
  # Returns an array of records.
188
161
  def get_records(params)
162
+ params[:limit] = config[:rows_per_page] if config[:enable_pagination]
163
+ params[:scope] = config[:scope] # note, params[:scope] becomes ActiveSupport::HashWithIndifferentAccess
189
164
 
190
- # Restore params from component_session if requested
191
- if params[:with_last_params]
192
- params = component_session[:last_params]
193
- else
194
- # remember the last params
195
- component_session[:last_params] = params
196
- end
197
-
198
- # build initial relation based on passed params
199
- relation = get_relation(params)
165
+ data_adapter.get_records(params, columns)
166
+ end
200
167
 
201
- # addressing the n+1 query problem
202
- columns.each do |c|
203
- assoc, method = c[:name].split('__')
204
- relation = relation.includes(assoc.to_sym) if method
205
- end
168
+ def count_records(params)
169
+ params[:scope] = config[:scope] # note, params[:scope] becomes ActiveSupport::HashWithIndifferentAccess
206
170
 
207
- # apply sorting if needed
208
- if params[:sort] && sort_params = params[:sort].first
209
- assoc, method = sort_params["property"].split('__')
210
- dir = sort_params["direction"].downcase
211
-
212
- # if a sorting scope is set, call the scope with the given direction
213
- column = columns.detect { |c| c[:name] == sort_params["property"] }
214
- if column.has_key?(:sorting_scope)
215
- relation = relation.send(column[:sorting_scope].to_sym, dir.to_sym)
216
- ::Rails.logger.debug "!!! relation: #{relation.inspect}\n"
217
- else
218
- relation = if method.nil?
219
- relation.order("#{assoc} #{dir}")
220
- else
221
- assoc = data_class.reflect_on_association(assoc.to_sym)
222
- relation.joins(assoc.name).order("#{assoc.klass.table_name}.#{method} #{dir}")
223
- end
224
- end
225
- end
226
-
227
- # apply pagination if needed
228
- if config[:enable_pagination]
229
- per_page = config[:rows_per_page]
230
- page = params[:limit] ? params[:start].to_i/params[:limit].to_i + 1 : 1
231
- relation.paginate(:per_page => per_page, :page => page)
232
- else
233
- relation.all
234
- end
171
+ data_adapter.count_records(params, columns)
235
172
  end
236
173
 
237
174
  # Override this method to react on each operation that caused changing of data
@@ -258,7 +195,7 @@ module Netzke
258
195
  modified_records = 0
259
196
  data.each do |record_hash|
260
197
  id = record_hash.delete('id')
261
- record = operation == :create ? data_class.new : data_class.find(id)
198
+ record = operation == :create ? data_adapter.new_record : data_adapter.find_record(id)
262
199
  success = true
263
200
 
264
201
  # merge with strong default attirbutes
@@ -268,21 +205,8 @@ module Netzke
268
205
  record.set_value_for_attribute(columns_hash[k.to_sym].nil? ? {:name => k} : columns_hash[k.to_sym], v)
269
206
  end
270
207
 
271
- # process all attirubutes for this record
272
- #record_hash.each_pair do |k,v|
273
- #begin
274
- #record.send("#{k}=",v)
275
- #rescue ArgumentError => exc
276
- #flash :error => exc.message
277
- #success = false
278
- #break
279
- #end
280
- #end
281
-
282
208
  # try to save
283
- # modified_records += 1 if success && record.save
284
- mod_records[id] = record.to_array(columns(:with_meta => true)) if success && record.save
285
- # mod_record_ids << id if success && record.save
209
+ mod_records[id] = record.netzke_array(columns(:with_meta => true)) if success && record.save
286
210
 
287
211
  # flash eventual errors
288
212
  if !record.errors.empty?
@@ -292,7 +216,6 @@ module Netzke
292
216
  end
293
217
  end
294
218
  end
295
- # flash :notice => "#{operation.to_s.capitalize}d #{modified_records} record(s)"
296
219
  else
297
220
  success = false
298
221
  flash :error => "You don't have permissions to #{operation} data"
@@ -23,7 +23,7 @@ module Netzke
23
23
 
24
24
  # override
25
25
  def record
26
- @record ||= get_relation.first
26
+ @record ||= data_adapter.first
27
27
  end
28
28
 
29
29
  # Pass total records amount and the first record to the JS constructor
@@ -34,7 +34,7 @@ module Netzke
34
34
  end
35
35
 
36
36
  endpoint :get_data do |params|
37
- @record = get_relation(params).offset(params[:start].to_i).limit(1).first
37
+ @record = data_adapter.get_records(params).first
38
38
  record_hash = @record && js_record_data
39
39
  {:records => record_hash && [record_hash] || [], :total => total_records(params)}
40
40
  end
@@ -64,7 +64,7 @@ module Netzke
64
64
  protected
65
65
 
66
66
  def total_records(params = {})
67
- @total_records ||= get_relation(params).count
67
+ @total_records ||= data_adapter.count_records(params, [])
68
68
  end
69
69
 
70
70
  end
@@ -13,6 +13,7 @@ module Netzke
13
13
  {
14
14
  :class_name => "Netzke::Basepack::SearchPanel",
15
15
  :model => config[:model],
16
+ :fields => config[:fields],
16
17
  :preset_query => config[:query],
17
18
  :auto_scroll => config[:auto_scroll]
18
19
  }
@@ -63,6 +64,7 @@ module Netzke
63
64
  s[:bbar] = (config[:bbar] || []) + [:clear_all.action, :reset.action, "->",
64
65
  I18n.t('netzke.basepack.query_builder.presets'),
65
66
  {
67
+ :itemId => "presetsCombo",
66
68
  :xtype => "combo",
67
69
  :triggerAction => "all",
68
70
  :value => super[:load_last_preset] && last_preset.try(:fetch, "name"),
@@ -2,18 +2,20 @@
2
2
  initComponent: function() {
3
3
  this.callParent();
4
4
 
5
+ this.presetsCombo = this.getDockedItems()[1].getComponent('presetsCombo');
6
+
5
7
  this.add({title: "+"});
6
8
 
7
9
  this.on('beforetabchange', function(c, newTab, curentTab){
8
10
  if (newTab.title === '+') {
9
- this.addTab(true);
11
+ this.addTab(true, true);
10
12
  return false;
11
13
  } else {
12
14
  if (this.maxTabHeight) newTab.setHeight(this.maxTabHeight);
13
15
  }
14
16
  }, this);
15
17
 
16
- this.addTab(true);
18
+ this.addTab(true, false);
17
19
 
18
20
  this.addEvents('conditionsupdate');
19
21
 
@@ -24,22 +26,31 @@
24
26
 
25
27
  if (query.length !== 0) {
26
28
  Ext.each(query, function(f, i){
27
- if (this.items.getCount() < i + 2) { this.addTab(); }
28
- this.items.get(i).buildFormFromQuery(query[i]);
29
+ var closable = i === 0 ? false : true;
30
+ if (this.items.getCount() < i + 2) { this.addTab(false, closable); }
31
+ this.items.get(i).items.first().buildFormFromQuery(query[i]);
29
32
  }, this);
30
33
  }
31
34
 
32
35
  this.doLayout();
33
36
  },
34
37
 
35
- addTab: function(activate){
38
+ addTab: function(activate, closable){
36
39
  var newTabConfig = Ext.apply({}, this.netzkeComponents.searchPanel);
37
40
  newTabConfig.id = Ext.id(); // We need a unique ID every time
38
- newTabConfig.title = "OR";
39
- newTabConfig.closable = true;
40
- var newTab = Ext.createByAlias(newTabConfig.alias, newTabConfig);
41
-
42
- this.insert(this.items.getCount() - 1, newTab);
41
+ newTabConfig.preventHeader = true;
42
+ newTabConfig.border = false;
43
+
44
+ var newTab = this.insert(
45
+ this.items.getCount() - 1,
46
+ {
47
+ title: "OR",
48
+ closable: closable,
49
+ items: [
50
+ Ext.createByAlias(newTabConfig.alias, newTabConfig)
51
+ ]
52
+ }
53
+ );
43
54
 
44
55
  if (activate) {
45
56
  this.suspendEvents();
@@ -51,8 +62,8 @@
51
62
  getQuery: function(all) {
52
63
  var query = [];
53
64
  this.eachTab(function(i) {
54
- var q = i.getQuery();
55
- if (q.length > 0) query.push(i.getQuery(all));
65
+ var q = i.items.first().getQuery();
66
+ if (q.length > 0) query.push(i.items.first().getQuery(all));
56
67
  });
57
68
  return query;
58
69
  },
@@ -66,8 +77,8 @@
66
77
  },
67
78
 
68
79
  eachTab: function(fn, scope) {
69
- this.items.each(function(f, i) {
70
- if (this.items.last() !== f) {
80
+ this.items.each(function(f) {
81
+ if (f.title !== "+") {
71
82
  fn.call(scope || f, f);
72
83
  }
73
84
  }, this);
@@ -75,7 +86,7 @@
75
86
 
76
87
  onClearAll: function() {
77
88
  this.removeAllTabs(true);
78
- this.items.first().onClearAll();
89
+ this.getActiveTab().items.first().onClearAll();
79
90
  },
80
91
 
81
92
  onReset: function() {
@@ -94,7 +105,7 @@
94
105
  var existingPresetIndex = presetsComboStore.find('field2', searchName);
95
106
  if (existingPresetIndex !== -1) {
96
107
  // overwriting
97
- Ext.Msg.confirm(this.i18n.overwriteConfirmTitle, String.format(this.i18n.overwriteConfirm, searchName), function(btn, text){
108
+ Ext.Msg.confirm(this.i18n.overwriteConfirmTitle, this.i18n.overwriteConfirm, function(btn, text){
98
109
  if (btn == 'yes') {
99
110
  var r = presetsComboStore.getAt(existingPresetIndex);
100
111
  r.set('field1', this.getQuery(true));
@@ -104,8 +115,7 @@
104
115
  }, this);
105
116
  } else {
106
117
  this.doSavePreset(searchName);
107
- var r = new presetsComboStore.recordType({field1: this.getQuery(true), field2: searchName});
108
- presetsComboStore.add(r);
118
+ presetsComboStore.add({field1: this.getQuery(true), field2: searchName});
109
119
  }
110
120
  }
111
121
  },
@@ -120,7 +130,7 @@
120
130
  onDeletePreset: function(){
121
131
  var searchName = this.presetsCombo.getRawValue();
122
132
  if (searchName !== "") {
123
- Ext.Msg.confirm(this.i18n.deleteConfirmTitle, String.format(this.i18n.overwriteConfirm, searchName), function(btn, text){
133
+ Ext.Msg.confirm(this.i18n.deleteConfirmTitle, this.i18n.overwriteConfirm, function(btn, text){
124
134
  if (btn == 'yes') {
125
135
  this.removePresetFromList(searchName);
126
136
  this.deletePreset({
@@ -4,6 +4,8 @@ module Netzke
4
4
  # +load_last_preset+ - on load, tries to load the latest saved preset
5
5
  class SearchPanel < Base
6
6
 
7
+ include Netzke::Basepack::DataAccessor
8
+
7
9
  js_base_class "Ext.form.FormPanel"
8
10
 
9
11
  js_properties(
@@ -56,14 +58,15 @@ module Netzke
56
58
  def js_config
57
59
  super.merge(
58
60
  :attrs => attributes,
59
- :attrs_hash => data_class.column_names.inject({}){ |hsh,c| hsh.merge(c => data_class.columns_hash[c].type) },
61
+ :attrs_hash => data_class.column_names.inject({}){ |hsh,c|
62
+ hsh.merge(c => data_adapter.get_property_type(data_class.columns_hash[c])) },
60
63
  :preset_query => (config[:load_last_preset] ? last_preset.try(:fetch, "query") : config[:query]) || []
61
64
  )
62
65
  end
63
66
 
64
67
  def attributes
65
- data_class.column_names.map do |name|
66
- [name, data_class.human_attribute_name(name)]
68
+ config[:fields].map do |field|
69
+ [field[:name], field[:field_label]]
67
70
  end
68
71
  end
69
72