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.
@@ -128,7 +128,9 @@ module Netzke
128
128
  attrs.merge!(config[:strong_default_attrs]) if config[:strong_default_attrs]
129
129
 
130
130
  attrs.each_pair do |k,v|
131
- data_adapter.set_record_value_for_attribute(record, final_columns_hash[k.to_sym].nil? ? {:name => k} : final_columns_hash[k.to_sym], v, config.role || :default)
131
+ attr = final_columns_hash[k.to_sym]
132
+ next if attr.nil?
133
+ data_adapter.set_record_value_for_attribute(record, attr, v)
132
134
  end
133
135
 
134
136
  if record.save
@@ -9,24 +9,24 @@ module Netzke
9
9
  i18n_path = "netzke.basepack.search_panel.%s"
10
10
 
11
11
  ATTRIBUTE_OPERATORS_MAP = {
12
- :integer => [
12
+ integer: [
13
13
  ["eq", I18n.t(i18n_path % 'equals')],
14
14
  ["gt", I18n.t(i18n_path % 'greater_than')],
15
15
  ["lt", I18n.t(i18n_path % 'less_than')]
16
16
  ],
17
- :text => [
17
+ text: [
18
18
  ["contains", I18n.t(i18n_path % 'contains')] # same as matches => %string%
19
19
  ],
20
- :string => [
20
+ string: [
21
21
  ["contains", I18n.t(i18n_path % 'contains')], # same as matches => %string%
22
22
  ["matches", I18n.t(i18n_path % 'matches')]
23
23
  ],
24
- :boolean => [
24
+ boolean: [
25
25
  ["is_any", I18n.t(i18n_path % 'is_true')],
26
26
  ["is_true", I18n.t(i18n_path % 'is_true')],
27
27
  ["is_false", I18n.t(i18n_path % 'is_false')]
28
28
  ],
29
- :date => [
29
+ date: [
30
30
  ["eq", I18n.t(i18n_path % 'date_equals')],
31
31
  ["gt", I18n.t(i18n_path % 'after')],
32
32
  ["lt", I18n.t(i18n_path % 'before')],
@@ -36,6 +36,8 @@ module Netzke
36
36
  }
37
37
 
38
38
  ATTRIBUTE_OPERATORS_MAP[:datetime] = ATTRIBUTE_OPERATORS_MAP[:date]
39
+ ATTRIBUTE_OPERATORS_MAP[:decimal] = ATTRIBUTE_OPERATORS_MAP[:integer]
40
+ ATTRIBUTE_OPERATORS_MAP[:float] = ATTRIBUTE_OPERATORS_MAP[:integer]
39
41
 
40
42
  js_configure do |c|
41
43
  c.extend = "Ext.form.FormPanel"
@@ -142,9 +142,8 @@ Ext.define('Netzke.classes.Netzke.Basepack.SearchPanel.ConditionField', {
142
142
  var idx = this.ownerCt.items.indexOf(this);
143
143
  var owner = this.ownerCt;
144
144
  var newSelf = Ext.createByAlias('widget.netzkebasepacksearchpanelconditionfield', Ext.apply(this.initialConfig, {name: attr, attrType: attrType, attr: attr.underscore(), ownerCt: this.ownerCt, operator: null}));
145
- owner.remove(this);
146
145
  owner.insert(idx, newSelf);
147
- owner.doLayout();
146
+ setTimeout(function() { this.destroy(); }.bind(this), 1); // this gives time for pending events to get handled without errors
148
147
  },
149
148
 
150
149
  // Returns true if it should be in the query
@@ -0,0 +1,223 @@
1
+ module Netzke
2
+ module Basepack
3
+ # Ext.tree.Panel-based component with the following features:
4
+ #
5
+ # * CRUD operations (only R is implemented atm)
6
+ #
7
+ # == Simple example
8
+ #
9
+ # class Files < Netzke::Basepack::Tree
10
+ # def configure(c)
11
+ # super
12
+ # c.model = "FileRecord"
13
+ # c.columns = [
14
+ # {name: :name, xtype: :treecolumn},
15
+ # :size
16
+ # ]
17
+ # end
18
+ # end
19
+ #
20
+ # == Instance configuration
21
+ #
22
+ # The following config options are supported:
23
+ #
24
+ # [model]
25
+ #
26
+ # Name of the ActiveRecord model that provides data to this Tree, e.g. "FileRecord"
27
+ # The model must respond to the following methods:
28
+ #
29
+ # * TreeModel.root - the root record
30
+ # * TreeModel#children - child records
31
+ #
32
+ # Note that the awesome_nested_set gem implements the above, so, feel free to use it.
33
+ #
34
+ # [columns]
35
+ #
36
+ # An array of columns to be displayed in the tree. See the "Columns" section in the `Netzke::Basepack::Grid`.
37
+ # Additionally, you probably will want to specify which column will have the tree nodes UI by providing the
38
+ # `xtype` config option set to `:treecolumn`.
39
+ #
40
+ # [root]
41
+ #
42
+ # By default, the component will pick whatever record is returned by `TreeModel.root`, and use it as the root
43
+ # record. However, sometimes the model table has multiple root records (which `parent_id` set to `nil`), and all
44
+ # of them should be shown in the panel. To achive this, you can define the `root` config option,
45
+ # which will serve as a virtual root record for those records. You may set it to `true`, or a hash of
46
+ # attributes, e.g.:
47
+ #
48
+ # c.root = {name: 'Root', size: 1000}
49
+ #
50
+ # Note, that the root record can be hidden from the tree by specifying the `Ext.tree.Panel`'s `root_visible`
51
+ # config option set to `false`, which is probably what you want when you have multiple root records.
52
+ class Tree < Netzke::Base
53
+ NODE_ATTRS = {
54
+ boolean: %w[leaf checked expanded expandable qtip qtitle],
55
+ string: %w[icon icon_cls href href_target qtip qtitle]
56
+ }
57
+
58
+ include Netzke::Basepack::Grid::Endpoints
59
+ include Netzke::Basepack::Grid::Services
60
+ include Netzke::Basepack::Columns
61
+ include Netzke::Basepack::DataAccessor
62
+
63
+ js_configure do |c|
64
+ c.extend = "Ext.tree.Panel"
65
+ c.mixin
66
+ c.mixins << "Netzke.mixins.Basepack.Columns"
67
+ c.mixins << "Netzke.mixins.Basepack.GridEventHandlers"
68
+ c.translate *%w[are_you_sure confirmation]
69
+ c.require :extensions
70
+ end
71
+
72
+ def self.server_side_config_options
73
+ super + [:model]
74
+ end
75
+
76
+ def configure(c)
77
+ set_defaults(c)
78
+ super
79
+ end
80
+
81
+ def columns
82
+ add_node_interface_methods(super)
83
+ end
84
+
85
+ def get_records(params)
86
+ if params[:id] == 'root'
87
+ data_adapter.find_root_records
88
+ else
89
+ data_adapter.find_record_children(data_adapter.find_record(params[:id]))
90
+ end
91
+ end
92
+
93
+ # Override Grid::Services#read so we send records as key-value JSON (instead of array)
94
+ def read(params = {})
95
+ {}.tap do |res|
96
+ records = get_records(params)
97
+ res[:data] = records.map{|r| data_adapter.record_to_hash(r, final_columns(:with_meta => true)).netzke_literalize_keys}
98
+ res[:total] = count_records(params) if config[:enable_pagination]
99
+ end
100
+ end
101
+
102
+ def js_configure(c)
103
+ super
104
+
105
+ c.title = c.title || self.class.js_config.properties[:title] || data_class.name.pluralize
106
+ c.bbar = bbar
107
+ # c.context_menu = context_menu
108
+ c.columns = js_columns
109
+ c.columns_order = columns_order
110
+ c.inline_data = read if c.load_inline_data
111
+ c.pri = data_adapter.primary_key
112
+
113
+ if c.default_filters
114
+ populate_cols_with_filters(c)
115
+ end
116
+
117
+ c.root ||= data_adapter.record_to_hash(data_adapter.root, final_columns(with_meta: true)).netzke_literalize_keys
118
+ end
119
+
120
+ action :add do |a|
121
+ a.handler = "onAddRecord" # overriding naming conventions as Ext 4 grid has its own onAdd method
122
+ a.icon = :add
123
+ end
124
+
125
+ action :edit do |a|
126
+ # a.disabled = true
127
+ a.handler = :onEdit
128
+ a.icon = :table_edit
129
+ end
130
+
131
+ action :apply do |a|
132
+ a.disabled = config[:prohibit_update] && config[:prohibit_create]
133
+ a.icon = :tick
134
+ end
135
+
136
+ action :del do |a|
137
+ # a.disabled = true
138
+ a.icon = :table_row_delete
139
+ end
140
+
141
+ component :add_window do |c|
142
+ preconfigure_record_window(c)
143
+ c.title = "Add #{data_class.model_name.human}"
144
+ c.items = [:add_form]
145
+ c.form_config.record = data_class.new(columns_default_values)
146
+ end
147
+
148
+ component :edit_window do |c|
149
+ preconfigure_record_window(c)
150
+ c.title = "Edit #{data_class.model_name.human}"
151
+ c.items = [:edit_form]
152
+ end
153
+
154
+ component :multi_edit_window do |c|
155
+ preconfigure_record_window(c)
156
+ c.title = "Edit #{data_class.model_name.human.pluralize}"
157
+ c.items = [:multi_edit_form]
158
+ end
159
+
160
+ endpoint :add_window__add_form__netzke_submit do |params, this|
161
+ data = ActiveSupport::JSON.decode(params[:data])
162
+ data["parent_id"] = params["parent_id"]
163
+ this.merge!(component_instance(:add_window).
164
+ component_instance(:add_form).
165
+ submit(data, this))
166
+ on_data_changed if this.set_form_values.present?
167
+ this.delete(:set_form_values)
168
+ end
169
+
170
+ protected
171
+
172
+ def bbar
173
+ config.has_key?(:bbar) ? config[:bbar] : default_bbar
174
+ end
175
+
176
+ # Override to change the default bottom toolbar
177
+ def default_bbar
178
+ %i[add edit apply del]
179
+ end
180
+
181
+ def preconfigure_record_window(c)
182
+ c.klass = RecordFormWindow
183
+
184
+ c.form_config = ActiveSupport::OrderedOptions.new.tap do |f|
185
+ f.model = config[:model]
186
+ f.persistent_config = config[:persistent_config]
187
+ f.strong_default_attrs = config[:strong_default_attrs]
188
+ f.mode = config[:mode]
189
+ f.items = default_fields_for_forms
190
+ end
191
+ end
192
+
193
+ private
194
+
195
+ # Adds attributes known to Ext.data.NodeInterface as meta columns (only those our model responds to)
196
+ def add_node_interface_methods(columns)
197
+ columns.clone.tap do |columns|
198
+ NODE_ATTRS.each do |type, attrs|
199
+ add_node_interface_methods_by_type!(columns, attrs, type)
200
+ end
201
+ end
202
+ end
203
+
204
+ def add_node_interface_methods_by_type!(columns, attrs, type)
205
+ attrs.each do |a|
206
+ next unless data_adapter.model_respond_to?(a.to_sym)
207
+ columns << {attr_type: type, name: a, meta: true}
208
+ end
209
+ end
210
+
211
+ def set_defaults(c)
212
+ # The nil? checks are needed because these can be already set in a subclass
213
+ c.enable_edit_in_form = true if c.enable_edit_in_form.nil?
214
+ c.enable_edit_inline = true if c.enable_edit_inline.nil?
215
+ c.enable_extended_search = true if c.enable_extended_search.nil?
216
+ c.enable_column_filters = true if c.enable_column_filters.nil?
217
+ c.enable_pagination = true if c.enable_pagination.nil?
218
+ c.rows_per_page = 30 if c.rows_per_page.nil?
219
+ c.tools = %w{ refresh } if c.tools.nil?
220
+ end
221
+ end
222
+ end
223
+ end
@@ -0,0 +1,101 @@
1
+ Ext.define('Netzke.classes.Basepack.Tree.Proxy', {
2
+ extend: 'Ext.data.proxy.Server',
3
+
4
+ batch: function(options) {
5
+ if (!options) return;
6
+ for (operation in options.operations) {
7
+ var op = new Ext.data.Operation({action: operation, records: options.operations[operation]});
8
+ this[op.action](op, Ext.emptyFn, this);
9
+ }
10
+ },
11
+
12
+ read: function(operation, callback, scope) {
13
+ this.grid.serverRead(this.paramsFromOperation(operation), function(res) {
14
+ this.processResponse(true, operation, {}, res, callback, scope);
15
+ }, this);
16
+ return {};
17
+ },
18
+
19
+ update: function(op, callback, scope) {
20
+ var data = Ext.Array.map(op.getRecords(), function(r) { return r.getData(); });
21
+
22
+ this.grid.serverUpdate(data, function(res) {
23
+ var errors = [];
24
+ Ext.each(op.records, function(r) {
25
+ var rid = r.getId(),
26
+ recordData = res[rid].record,
27
+ error = res[rid].error;
28
+ if (recordData) {
29
+ serverRecord = this.getReader().read({data: [res[rid].record]}).records[0];
30
+ r.copyFrom(serverRecord);
31
+ r.commit();
32
+ }
33
+ if (error) { errors.push(error); }
34
+ }, this);
35
+
36
+ if (errors.length == 0) {
37
+ // this.grid.getStore().load();
38
+ } else {
39
+ this.grid.netzkeFeedback(errors);
40
+ }
41
+ }, this);
42
+ },
43
+
44
+ destroy: function(op, callback, scope) {
45
+ var data = Ext.Array.map(op.getRecords(), function(r) { return r.getData().id; });
46
+ var store = this.grid.getStore();
47
+ this.grid.serverDelete(data, function(res){
48
+ var errors = [];
49
+ for (id in res) {
50
+ var error;
51
+ if (error = res[id].error) {
52
+ errors.push(error);
53
+ store.getRemovedRecords().forEach(function(record, i){
54
+ if (record.getId() == id) {
55
+ store.insert(record.index, record);
56
+ }
57
+ });
58
+ }
59
+ }
60
+
61
+ // clear store state
62
+ store.commitChanges();
63
+
64
+ if (errors.length > 0) {
65
+ this.grid.netzkeFeedback([errors]);
66
+ }
67
+
68
+ // this.grid.getStore().load();
69
+
70
+ }, this);
71
+ },
72
+ // Build consistent request params
73
+ paramsFromOperation: function(operation) {
74
+ var params = Ext.apply({id: operation.getId()}, this.getParams(operation));
75
+
76
+ if (params.filter) {
77
+ params.filters = Ext.decode(params.filter);
78
+ delete params.filter;
79
+ }
80
+
81
+ if (params.sort) {
82
+ params.sorters = Ext.decode(params.sort);
83
+ delete params.sort;
84
+ }
85
+
86
+ Ext.apply(params, this.extraParams);
87
+
88
+ return params;
89
+ }
90
+ });
91
+
92
+ /**
93
+ * A fix for CheckColumn
94
+ */
95
+ Ext.override(Ext.ux.CheckColumn, {
96
+ processEvent: function(type) {
97
+ // by returning true, we'll allow event propagation, so it reacts similarly to other columns
98
+ if (this.readOnly && type == 'mousedown') return true;
99
+ else return this.callOverridden(arguments);
100
+ }
101
+ });
@@ -0,0 +1,112 @@
1
+ {
2
+ multiSelect: true,
3
+
4
+ initComponent: function(){
5
+ this.netzkeProcessColumns();
6
+ this.netzkeBuildModel('Ext.data.TreeModel');
7
+ this.netzkeBuildStore();
8
+
9
+ delete this.root;
10
+
11
+ this.plugins = [];
12
+ this.plugins.push(Ext.create('Ext.grid.plugin.CellEditing', {pluginId: 'celleditor'}));
13
+
14
+ this.callParent();
15
+
16
+ this.setDynamicActionProperties(); // enable/disable actions (buttons) depending on selection
17
+ },
18
+
19
+ // Process selectionchange event to enable/disable actions
20
+ setDynamicActionProperties: function() {
21
+ this.getSelectionModel().on('selectionchange', function(selModel){
22
+ // if (this.actions.add) this.actions.add.setDisabled(selModel.getCount() != 1);
23
+ if (this.actions.edit) this.actions.edit.setDisabled(selModel.getCount() != 1);
24
+ }, this);
25
+ },
26
+
27
+ netzkeBuildStore: function() {
28
+ var store = Ext.create('Ext.data.TreeStore', Ext.apply({
29
+ proxy: this.netzkeBuildProxy(),
30
+ pruneModifiedRecords: true,
31
+ remoteSort: true,
32
+ remoteFilter: true,
33
+ pageSize: this.rowsPerPage,
34
+ root: this.root
35
+ }, this.dataStore));
36
+
37
+ delete this.dataStore;
38
+
39
+ store.getProxy().getReader().on('endpointcommands', function(commands) {
40
+ this.netzkeBulkExecute(commands);
41
+ }, this);
42
+
43
+ this.store = store;
44
+ return store; // for backward compatibility
45
+ },
46
+
47
+ netzkeBuildProxy: function() {
48
+ return Ext.create('Netzke.classes.Basepack.Tree.Proxy', {
49
+ reader: this.netzkeBuildReader(),
50
+ grid: this
51
+ });
52
+ },
53
+
54
+ netzkeBuildReader: function() {
55
+ var modelName = Netzke.modelName(this.id);
56
+ return Ext.create('Ext.data.reader.Json', {
57
+ model: modelName,
58
+ rootProperty: 'data'
59
+ });
60
+ },
61
+
62
+ // overriding
63
+ onAddRecord: function(){
64
+ var selected = this.getSelection()[0]
65
+
66
+ this.netzkeLoadComponent("add_window", {
67
+ callback: function(w){
68
+ w.show();
69
+ var form = w.items.first();
70
+ form.on('apply', function(){
71
+ if (!form.baseParams) form.baseParams = {};
72
+ form.baseParams.parent_id = (selected || {}).id;
73
+ }, this);
74
+
75
+ w.on('close', function(){
76
+ if (w.closeRes === "ok") {
77
+ if (selected) {
78
+ if (selected.isExpanded()) {
79
+ this.store.load({node: selected});
80
+ } else {
81
+ selected.expand();
82
+ }
83
+ } else {
84
+ this.store.load()
85
+ }
86
+ }
87
+ }, this);
88
+ }, scope: this
89
+ });
90
+ },
91
+
92
+ // overriding
93
+ onApply: function(){
94
+ var topModified = this.store.getModifiedRecords()[0]; // the most top-level modified record
95
+ this.store.sync();
96
+ this.store.load({node: topModified.parentNode});
97
+ },
98
+
99
+ // overriding
100
+ onDel: function() {
101
+ Ext.Msg.confirm(this.i18n.confirmation, this.i18n.areYouSure, function(btn){
102
+ if (btn == 'yes') {
103
+ var toDelete = this.getSelectionModel().getSelection();
104
+ store = this.getStore();
105
+ store.remove(toDelete);
106
+ store.removedNodes = toDelete; // HACK
107
+ store.sync();
108
+ }
109
+ }, this);
110
+ }
111
+
112
+ }