netzke-basepack 0.12.0 → 0.12.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.
data/Gemfile CHANGED
@@ -7,6 +7,8 @@ gem 'sqlite3'
7
7
  gem 'yard'
8
8
  gem 'rake'
9
9
 
10
+ gem 'awesome_nested_set'
11
+
10
12
  group :test do
11
13
  gem 'rspec'
12
14
  gem 'rspec-rails'
@@ -21,5 +23,6 @@ group :development, :test do
21
23
  gem 'web-console', '~> 2.0'
22
24
  # gem 'spring' # troubles...
23
25
  gem 'pry-rails'
24
- gem 'netzke-testing'
26
+ gem 'netzke-core', github: 'netzke/netzke-core'
27
+ gem 'netzke-testing', github: 'netzke/netzke-testing'
25
28
  end
data/README.md CHANGED
@@ -14,6 +14,7 @@ A pack of pre-built [Netzke](http://netzke.org) components that can be used as b
14
14
  Basepack includes the following components:
15
15
 
16
16
  * [Grid](http://rdoc.info/github/netzke/netzke-basepack/Netzke/Basepack/Grid) - a grid panel with a thick bag of features
17
+ * [Tree](http://rdoc.info/github/netzke/netzke-basepack/Netzke/Basepack/Tree) - a tree panel with features similar to the Grid
17
18
  * [Form](http://rdoc.info/github/netzke/netzke-basepack/Netzke/Basepack/Form) - a form panel with automatic binding of fields
18
19
  * [TabPanel](http://rdoc.info/github/netzke/netzke-basepack/Netzke/Basepack/TabPanel) - a tab panel with support for lazy loading of nested components
19
20
  * [Accordion](http://rdoc.info/github/netzke/netzke-basepack/Netzke/Basepack/Accordion) - an accordion panel with support for lazy loading of nested components
@@ -28,7 +29,7 @@ enhancing any grid with live search functionality
28
29
 
29
30
  ## Requirements
30
31
 
31
- * Ruby > 2.0.0
32
+ * Ruby > 1.9.3
32
33
  * Rails ~> 4.2.0
33
34
  * Ext JS = 5.1.0
34
35
 
@@ -1,5 +1,4 @@
1
- Ext.ns("Netzke.pre");
2
- Ext.ns("Netzke.pre.Basepack");
1
+ Ext.ns("Netzke.mixins.Basepack");
3
2
  Ext.ns("Ext.ux.grid");
4
3
 
5
4
  Ext.util.Format.mask = function(v){
@@ -34,3 +33,11 @@ Netzke.isModelDefined = function(name) {
34
33
  Netzke.modelName = function(name){
35
34
  return "Netzke.models." + name;
36
35
  };
36
+
37
+ // Fix 2-digit precision in the numeric filter
38
+ Ext.define('Ext.grid.filters.filter.Number', {
39
+ override: 'Ext.grid.filters.filter.Number',
40
+ getItemDefaults: function() {
41
+ return Ext.apply(this.itemDefaults, { decimalPrecision: 10 });
42
+ }
43
+ });
@@ -0,0 +1,224 @@
1
+ /* Shared column-related functionality, used in Tree and Grid */
2
+ Ext.define("Netzke.mixins.Basepack.Columns", {
3
+ netzkeBuildModel: function(modelClass) {
4
+ if (!Netzke.isModelDefined(this.id)) {
5
+ Ext.define(Netzke.modelName(this.id), {
6
+ extend: modelClass,
7
+ idProperty: this.pri, // Primary key
8
+ fields: this.fields
9
+ });
10
+ }
11
+ delete this.pri;
12
+ },
13
+
14
+ netzkeProcessColumns: function() {
15
+ this.fields = [];
16
+
17
+ // Run through columns and set up different configuration for each
18
+ Ext.each(this.columns, function(c, i){
19
+
20
+ this.netzkeNormalizeRenderer(c);
21
+
22
+ // Build the field configuration for this column
23
+ var fieldConfig = {name: c.name, defaultValue: c.defaultValue, allowNull: true};
24
+
25
+ if (c.name !== 'meta') fieldConfig.type = this.netzkeFieldTypeForAttrType(c.attrType); // field type (grid editors need this to function well)
26
+
27
+ if (c.attrType == 'datetime') {
28
+ fieldConfig.dateFormat = 'Y-m-d H:i:s'; // set the format in which we receive datetime from the server (so that the model can parse it)
29
+
30
+ // While for 'date' columns the renderer is set up automatically (through using column's xtype), there's no appropriate xtype for our custom datetime column.
31
+ // Thus, we need to set the renderer manually.
32
+ // NOTE: for Ext there's no distinction b/w date and datetime; date fields can include time.
33
+ if (!c.renderer) {
34
+ // format in which the data will be rendered; if c.format is nil, Ext.Date.defaultFormat extended with time will be used
35
+ c.renderer = Ext.util.Format.dateRenderer(c.format || Ext.Date.defaultFormat + " H:i:s");
36
+ }
37
+ };
38
+
39
+ if (c.attrType == 'date') {
40
+ // If no dateFormat given for date attrType, Timezone translation can subtract zone offset from 00:00:00 causing previous day.
41
+ fieldConfig.dateFormat = 'Y-m-d';
42
+ };
43
+
44
+ // because checkcolumn doesn't care about editor (not) being set, we need to explicitely set readOnly here
45
+ if (c.xtype == 'checkcolumn' && !c.editor) {
46
+ c.readOnly = true;
47
+ }
48
+
49
+ this.fields.push(fieldConfig);
50
+
51
+ // We will not use meta columns as actual columns (not even hidden) - only to create the records
52
+ if (c.meta) {
53
+ this.metaColumn = c;
54
+ return;
55
+ }
56
+
57
+ // Set rendeder for association columns (the one displaying associations by the specified method instead of id)
58
+ if (c.assoc) {
59
+ // Editor for association column
60
+ if (c.editor) c.editor = Ext.apply({ name: c.name }, c.editor);
61
+
62
+ // Renderer for association column
63
+ this.netzkeNormalizeAssociationRenderer(c);
64
+ }
65
+
66
+ if (c.editor) {
67
+ Ext.applyIf(c.editor, {selectOnFocus: true, netzkeParent: this});
68
+ }
69
+
70
+ // Setting the default filter type
71
+ if (c.filterable != false && !c.filter) {
72
+ c.filter = {type: this.netzkeFilterTypeForAttrType(c.attrType)};
73
+ }
74
+
75
+ // setting dataIndex
76
+ c.dataIndex = c.name;
77
+
78
+ }, this);
79
+
80
+ // Now reorder columns as specified in this.columnsOrder
81
+ this.orderColumns();
82
+ },
83
+
84
+ // Build column model config with columns in the correct order; columns out of order go to the end.
85
+ orderColumns: function(){
86
+ var colModelConfig = [];
87
+
88
+ Ext.each(this.columnsOrder, function(c) {
89
+ var mainColConfig;
90
+
91
+ Ext.each(this.columns, function(oc) {
92
+ if (c.name === oc.name) {
93
+ mainColConfig = Ext.apply({}, oc);
94
+ return false;
95
+ }
96
+ });
97
+
98
+ colModelConfig.push(Ext.apply(mainColConfig, c));
99
+ }, this);
100
+
101
+ // We don't need original columns any longer
102
+ delete this.columns;
103
+
104
+ // ... instead, define own column model
105
+ this.columns = colModelConfig;
106
+ },
107
+
108
+ // Normalizes the renderer for a column.
109
+ // Renderer may be:
110
+ // 1) a string that contains the name of the function to be used as renderer.
111
+ // 2) an array, where the first element is the function name, and the rest - the arguments
112
+ // that will be passed to that function along with the value to be rendered.
113
+ // The function is searched in the following objects: 1) Ext.util.Format, 2) this.
114
+ // If not found, it is simply evaluated. Handy, when as renderer we receive an inline JS function,
115
+ // or reference to a function in some other scope.
116
+ // So, these will work:
117
+ // * "uppercase"
118
+ // * ["ellipsis", 10]
119
+ // * ["substr", 3, 5]
120
+ // * "myRenderer" (if this.myRenderer is a function)
121
+ // * ["Some.scope.Format.customRenderer", 10, 20, 30] (if Some.scope.Format.customRenderer is a function)
122
+ // * "function(v){ return 'Value: ' + v; }"
123
+ netzkeNormalizeRenderer: function(c) {
124
+ if (!c.renderer) return;
125
+
126
+ var name, args = [];
127
+
128
+ if ('string' === typeof c.renderer) {
129
+ name = c.renderer.camelize(true);
130
+ } else {
131
+ name = c.renderer[0];
132
+ args = c.renderer.slice(1);
133
+ }
134
+
135
+ // First check whether Ext.util.Format has it
136
+ if (Ext.isFunction(Ext.util.Format[name])) {
137
+ c.renderer = Ext.Function.bind(Ext.util.Format[name], this, args, 1);
138
+ } else if (Ext.isFunction(this[name])) {
139
+ // ... then if our own class has it
140
+ c.renderer = Ext.Function.bind(this[name], this, args, 1);
141
+ } else {
142
+ // ... and, as last resort, evaluate it (allows passing inline javascript function as renderer)
143
+ eval("c.renderer = " + c.renderer + ";");
144
+ }
145
+ },
146
+
147
+ /*
148
+ Set a renderer that displayes association values instead of association record ID.
149
+ The association values are passed in the meta-column under associationValues hash.
150
+ */
151
+ netzkeNormalizeAssociationRenderer: function(c) {
152
+ var passedRenderer = c.renderer, // renderer we got from netzkeNormalizeRenderer
153
+ metaData, assocValue;
154
+ c.scope = this;
155
+ c.renderer = function(value, a, r, ri, ci, store, view){
156
+ var column = view.headerCt.items.getAt(ci),
157
+ editor = column.getEditor && column.getEditor(),
158
+ recordFromStore = editor && editor.isXType('combobox') && editor.getStore().findRecord('value', value),
159
+ renderedValue;
160
+
161
+ if (recordFromStore) {
162
+ renderedValue = recordFromStore.get('text');
163
+ } else if (metaData = r.get('meta')) {
164
+ assocValue = metaData.associationValues[c.name];
165
+ renderedValue = (assocValue == undefined) ? c.emptyText : assocValue;
166
+ } else {
167
+ renderedValue = value;
168
+ }
169
+
170
+ return passedRenderer ? passedRenderer.call(this, renderedValue) : renderedValue;
171
+ };
172
+ },
173
+
174
+ netzkeSaveColumns: function(){
175
+ var cols = [];
176
+ this.getView().getHeaderCt().items.each(function(c){
177
+ cols.push({name: c.name, width: c.width, hidden: c.hidden});
178
+ });
179
+
180
+ this.serverSaveColumns(cols);
181
+ },
182
+
183
+ // Tries editing the first editable (i.e. not hidden, not read-only) sell
184
+ netzkeTryStartEditing: function(r){
185
+ var editableIndex = 0;
186
+ Ext.each(this.initialConfig.columns, function(c){
187
+ // skip columns that cannot be edited
188
+ if (!(c.hidden == true || !c.editor || c.attrType == 'boolean')) {
189
+ return false;
190
+ }
191
+ editableIndex++;
192
+ });
193
+
194
+ if (editableIndex < this.initialConfig.columns.length) {this.getPlugin('celleditor').startEdit(r, this.columns[editableIndex]);}
195
+ },
196
+
197
+ netzkeFilterTypeForAttrType: function(attrType){
198
+ var map = {
199
+ integer : 'number',
200
+ decimal : 'number',
201
+ float : 'number',
202
+ datetime : 'date',
203
+ date : 'date',
204
+ string : 'string',
205
+ text : 'string',
206
+ 'boolean' : 'boolean'
207
+ };
208
+ return map[attrType] || 'string';
209
+ },
210
+
211
+ netzkeFieldTypeForAttrType: function(attrType){
212
+ var map = {
213
+ integer : 'int',
214
+ decimal : 'float',
215
+ float : 'float',
216
+ datetime : 'date',
217
+ date : 'date',
218
+ string : 'string',
219
+ text : 'string',
220
+ 'boolean' : 'boolean'
221
+ };
222
+ return map[attrType] || 'string';
223
+ }
224
+ });
@@ -1,6 +1,6 @@
1
- {
1
+ Ext.define("Netzke.mixins.Basepack.GridEventHandlers", {
2
2
  // Handler for the 'add' button
3
- onAddInline: function(){
3
+ onAddRecord: function(){
4
4
  if (this.enableEditInForm && !this.enableEditInline) {
5
5
  this.onAddInForm();
6
6
  } else {
@@ -18,12 +18,11 @@
18
18
  onDel: function() {
19
19
  Ext.Msg.confirm(this.i18n.confirmation, this.i18n.areYouSure, function(btn){
20
20
  if (btn == 'yes') {
21
- var ids = [], records = [];
22
- this.getSelectionModel().selected.each(function(r){
23
- this.store.remove(r);
24
- }, this);
25
-
26
- this.store.sync();
21
+ var toDelete = this.getSelectionModel().getSelection();
22
+ store = this.getStore();
23
+ store.remove(toDelete);
24
+ store.removedNodes = toDelete; // HACK
25
+ store.sync();
27
26
  }
28
27
  }, this);
29
28
  },
@@ -86,5 +85,55 @@
86
85
  if (this.searchWindow) {
87
86
  this.searchWindow.destroy();
88
87
  }
88
+ },
89
+
90
+ onEditInForm: function(){
91
+ var selModel = this.getSelectionModel();
92
+ if (selModel.getCount() > 1) {
93
+ var recordId = selModel.selected.first().getId();
94
+ this.netzkeLoadComponent("multi_edit_window", {
95
+ params: {record_id: recordId},
96
+ callback: function(w){
97
+ w.show();
98
+ var form = w.items.first();
99
+ form.on('apply', function(){
100
+ var ids = [];
101
+ selModel.selected.each(function(r){
102
+ ids.push(r.getId());
103
+ });
104
+ if (!form.baseParams) form.baseParams = {};
105
+ form.baseParams.ids = Ext.encode(ids);
106
+ }, this);
107
+
108
+ w.on('close', function(){
109
+ if (w.closeRes === "ok") {
110
+ this.store.load();
111
+ }
112
+ }, this);
113
+ }, scope: this});
114
+ } else {
115
+ var recordId = selModel.selected.first().getId();
116
+ this.netzkeLoadComponent("edit_window", {
117
+ clientConfig: {record_id: recordId},
118
+ callback: function(w){
119
+ w.show();
120
+ w.on('close', function(){
121
+ if (w.closeRes === "ok") {
122
+ this.store.load();
123
+ }
124
+ }, this);
125
+ }, scope: this});
126
+ }
127
+ },
128
+
129
+ onAddInForm: function(){
130
+ this.netzkeLoadComponent("add_window", {callback: function(w){
131
+ w.show();
132
+ w.on('close', function(){
133
+ if (w.closeRes === "ok") {
134
+ this.store.load();
135
+ }
136
+ }, this);
137
+ }, scope: this});
89
138
  }
90
- }
139
+ });
@@ -16,10 +16,9 @@ module Netzke
16
16
  mattr_accessor :icons_uri
17
17
 
18
18
  class << self
19
-
20
19
  # Called from netzke-basepack.rb
21
20
  def init
22
- %w[netzkeremotecombo xdatetime basepack].each do |name|
21
+ %w[netzkeremotecombo xdatetime basepack columns mixins/grid_event_handlers].each do |name|
23
22
  Netzke::Core.ext_javascripts << "#{File.dirname(__FILE__)}/../../javascripts/#{name}.js"
24
23
  end
25
24
 
@@ -35,6 +34,5 @@ module Netzke
35
34
  yield self
36
35
  end
37
36
  end
38
-
39
37
  end
40
38
  end
@@ -142,8 +142,6 @@ module Netzke
142
142
  {name: c.name, text: c.text, attr_type: c.attr_type}.tap do |a|
143
143
  if c[:assoc]
144
144
  a[:text].sub!(" ", " ")
145
- a[:assoc] = true
146
- a[:attr_type] = :string
147
145
  end
148
146
  end
149
147
  end
@@ -225,6 +225,24 @@ module Netzke::Basepack::DataAdapters
225
225
  name.to_s.humanize
226
226
  end
227
227
 
228
+ # Return root record for tree-like data
229
+ def root
230
+ model_class.root
231
+ end
232
+
233
+ def find_record_children(r)
234
+ r.children
235
+ end
236
+
237
+ def find_root_records
238
+ model_class.where(parent_id: nil)
239
+ end
240
+
241
+ # Does record respond to given method?
242
+ def model_respond_to?(method)
243
+ @model_class.instance_methods.include?(method)
244
+ end
245
+
228
246
  # -- End of overridable methods
229
247
 
230
248
  # Abstract-adapter specifics
@@ -39,7 +39,9 @@ module Netzke::Basepack::DataAdapters
39
39
  end
40
40
 
41
41
  def attr_type(attr_name)
42
- association_attr?(attr_name) ? :integer : (@model_class.columns_hash[attr_name.to_s].try(:type) || :string)
42
+ method, assoc = method_and_assoc(attr_name)
43
+ klass = assoc.nil? ? @model_class : assoc.klass
44
+ klass.columns_hash[method].try(:type) || :string
43
45
  end
44
46
 
45
47
  # Implementation for {AbstractAdapter#get_records}
@@ -113,17 +115,17 @@ module Netzke::Basepack::DataAdapters
113
115
 
114
116
  def virtual_attribute?(c)
115
117
  assoc_name, asso = c[:name].split('__')
116
- assoc, assoc_method = assoc_and_assoc_method_for_attr(c[:name])
118
+ method, assoc = method_and_assoc(c[:name])
117
119
 
118
120
  if assoc
119
- return !assoc.klass.column_names.include?(assoc_method)
121
+ return !assoc.klass.column_names.include?(method)
120
122
  else
121
123
  return !@model_class.column_names.include?(c[:name])
122
124
  end
123
125
  end
124
126
 
125
127
  def combo_data(attr, query = "")
126
- assoc, assoc_method = assoc_and_assoc_method_for_attr(attr[:name])
128
+ method, assoc = method_and_assoc(attr[:name])
127
129
 
128
130
  if assoc
129
131
  # Options for an asssociation attribute
@@ -131,16 +133,16 @@ module Netzke::Basepack::DataAdapters
131
133
  relation = assoc.klass.all
132
134
  relation = relation.extend_with(attr[:scope]) if attr[:scope]
133
135
 
134
- if assoc.klass.column_names.include?(assoc_method)
136
+ if assoc.klass.column_names.include?(method)
135
137
  # apply query
136
138
  assoc_arel_table = assoc.klass.arel_table
137
139
 
138
- relation = relation.where(assoc_arel_table[assoc_method].matches("%#{query}%")) if query.present?
139
- relation.to_a.map{ |r| [r.id, r.send(assoc_method)] }
140
+ relation = relation.where(assoc_arel_table[method].matches("%#{query}%")) if query.present?
141
+ relation.to_a.map{ |r| [r.id, r.send(method)] }
140
142
  else
141
143
  query.downcase!
142
144
  # an expensive search!
143
- relation.to_a.map{ |r| [r.id, r.send(assoc_method)] }.select{ |id,value| value.to_s.downcase.include?(query) }
145
+ relation.to_a.map{ |r| [r.id, r.send(method)] }.select{ |id,value| value.to_s.downcase.include?(query) }
144
146
  end
145
147
 
146
148
  else
@@ -240,7 +242,7 @@ module Netzke::Basepack::DataAdapters
240
242
  end
241
243
  # the composite_primary_keys gem produces [Key1,Key2...] and [Value1,Value2...]
242
244
  # on primary_key and id requests. Basepack::AttrConfig converts the keys-array to an String.
243
- elsif r.class.primary_key.try(:to_s) == a[:name]
245
+ elsif primary_key.try(:to_s) == a[:name]
244
246
  r.id # return 'val1,val2...' on 'key1,key2...' composite_primary_keys
245
247
  end
246
248
 
@@ -296,11 +298,12 @@ module Netzke::Basepack::DataAdapters
296
298
  end
297
299
  end
298
300
 
299
- # Returns association and association method for a column
300
- def assoc_and_assoc_method_for_attr(column_name)
301
- assoc_name, assoc_method = column_name.split('__')
302
- assoc = @model_class.reflect_on_association(assoc_name.to_sym) if assoc_method
303
- [assoc, assoc_method]
301
+ # If association attribute is given, returns [method, association]
302
+ # Else returns [attr_name]
303
+ def method_and_assoc(attr_name)
304
+ assoc_name, method = attr_name.to_s.split('__')
305
+ assoc = @model_class.reflect_on_association(assoc_name.to_sym) if method
306
+ assoc.nil? ? [attr_name] : [method, assoc]
304
307
  end
305
308
 
306
309
  # An ActiveRecord::Relation instance encapsulating all the necessary conditions.
@@ -353,16 +356,10 @@ module Netzke::Basepack::DataAdapters
353
356
  predicates = conditions.map do |q|
354
357
  q = HashWithIndifferentAccess.new(q)
355
358
 
356
- assoc, method = q["attr"].split('__')
357
- if method
358
- assoc = @model_class.reflect_on_association(assoc.to_sym)
359
- assoc_arel = assoc.klass.arel_table
360
- attr = method
361
- arel_table = Arel::Table.new(assoc.klass.table_name.to_sym)
362
- else
363
- attr = assoc
364
- arel_table = @model_class.arel_table
365
- end
359
+ attr = q[:attr]
360
+ method, assoc = method_and_assoc(attr)
361
+
362
+ arel_table = assoc ? Arel::Table.new(assoc.klass.table_name.to_sym) : @model_class.arel_table
366
363
 
367
364
  value = q["value"]
368
365
  op = q["operator"]
@@ -371,15 +368,15 @@ module Netzke::Basepack::DataAdapters
371
368
 
372
369
  case attr_type
373
370
  when :datetime
374
- update_predecate_for_datetime(arel_table[attr], op, value.to_date)
371
+ update_predecate_for_datetime(arel_table[method], op, value.to_date)
375
372
  when :string, :text
376
- update_predecate_for_string(arel_table[attr], op, value)
373
+ update_predecate_for_string(arel_table[method], op, value)
377
374
  when :boolean
378
- update_predecate_for_boolean(arel_table[attr], op, value)
375
+ update_predecate_for_boolean(arel_table[method], op, value)
379
376
  when :date
380
- update_predecate_for_rest(arel_table[attr], op, value.to_date)
377
+ update_predecate_for_rest(arel_table[method], op, value.to_date)
381
378
  else
382
- update_predecate_for_rest(arel_table[attr], op, value)
379
+ update_predecate_for_rest(arel_table[method], op, value)
383
380
  end
384
381
  end
385
382