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.
@@ -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
+ }