netzke-basepack 0.12.0 → 0.12.1

Sign up to get free protection for your applications and to get access to all the features.
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