netzke-basepack 0.5.5.1 → 0.5.6

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 (41) hide show
  1. data/CHANGELOG.rdoc +16 -1
  2. data/README.rdoc +31 -56
  3. data/Rakefile +2 -2
  4. data/TODO.rdoc +3 -0
  5. data/javascripts/basepack.js +70 -30
  6. data/lib/app/models/netzke_auto_column.rb +1 -53
  7. data/lib/app/models/netzke_auto_field.rb +4 -0
  8. data/lib/app/models/netzke_auto_table.rb +61 -0
  9. data/lib/netzke-basepack.rb +13 -7
  10. data/lib/netzke/active_record/basepack.rb +28 -0
  11. data/lib/netzke/active_record/data_accessor.rb +2 -0
  12. data/lib/netzke/basic_app.rb +3 -4
  13. data/lib/netzke/{basic_app_extras → basic_app}/statusbar_ext.js +0 -0
  14. data/lib/netzke/data_accessor.rb +2 -0
  15. data/lib/netzke/ext.rb +1 -1
  16. data/lib/netzke/fields_configurator.rb +22 -6
  17. data/lib/netzke/form_panel.rb +20 -11
  18. data/lib/netzke/form_panel/form_panel_api.rb +78 -0
  19. data/lib/netzke/form_panel/form_panel_js.rb +143 -0
  20. data/lib/netzke/{form_panel_extras → form_panel}/javascripts/netzkefileupload.js +0 -0
  21. data/lib/netzke/{form_panel_extras → form_panel}/javascripts/xcheckbox.js +0 -0
  22. data/lib/netzke/grid_panel.rb +33 -24
  23. data/lib/netzke/grid_panel/grid_panel_api.rb +358 -0
  24. data/lib/netzke/grid_panel/grid_panel_js.rb +747 -0
  25. data/lib/netzke/{grid_panel_extras → grid_panel}/javascripts/filters.js +0 -0
  26. data/lib/netzke/{grid_panel_extras → grid_panel}/javascripts/rows-dd.js +0 -0
  27. data/lib/netzke/grid_panel/record_form_window.rb +44 -0
  28. data/lib/netzke/panel.rb +1 -3
  29. data/lib/netzke/property_editor.rb +2 -0
  30. data/lib/netzke/{property_editor_extras → property_editor}/helper_model.rb +2 -2
  31. data/lib/netzke/search_panel.rb +3 -3
  32. data/lib/netzke/window.rb +9 -3
  33. data/test/unit/grid_panel_test.rb +0 -2
  34. data/test/unit/helper_model_test.rb +2 -2
  35. metadata +16 -15
  36. data/lib/netzke/field_model.rb +0 -131
  37. data/lib/netzke/form_panel_api.rb +0 -78
  38. data/lib/netzke/form_panel_js.rb +0 -141
  39. data/lib/netzke/grid_panel_api.rb +0 -356
  40. data/lib/netzke/grid_panel_extras/record_form_window.rb +0 -46
  41. data/lib/netzke/grid_panel_js.rb +0 -725
data/CHANGELOG.rdoc CHANGED
@@ -1,3 +1,18 @@
1
+ = v0.5.6
2
+ * Compatibility with latest netzke-core
3
+ * Compatibility with Ext JS v3.1
4
+ * Impr: Code reorganization
5
+ * Impr: Non-standard primary key support in GridPanel and FormPanel
6
+ * Impr: Search button in GridPanel indicates that records filtering is active
7
+ * Fix: by default, SearchPanel excludes boolean fields for now (until a 3-state checkbox is introduced)
8
+ * Impr: made it possible to create new AR records with assigned associations using double-underscore notation, e.g.: Clerk.new(:boss__last_name => "Aguraijuja ")
9
+ * Impr: specifying an item for Netzke::Window is now optional
10
+ * Fix: xtype field in FieldsConfigurator is now a combobox
11
+ * Fix: FieldsConfigurator was crashing when fired consequently for grid and form
12
+ * Depr: :data_class_name option is deprecated, use :model
13
+ * Impr: "gear" tool is now hidden on FieldsConfigurator
14
+ * Impr: Grid/FormPanel layouts are now not stored into the netzke_preferences table unless the defaults are modified (cleaner table)
15
+
1
16
  = v0.5.5.1 - 2009-11-09
2
17
  * Compatibility with latest netzke-core
3
18
 
@@ -5,7 +20,7 @@
5
20
  * Compatibility with latest netzke-core
6
21
  * Regression: pressing "enter" was not submitting the form (FormPanel)
7
22
  * Regression: "Restore defaults" button was not working in FieldsConfigurator and PropertyEditor
8
- * Fix: excluding columns in FieldConfigurator was causing inconsistent column behavior (move/hide/resize)
23
+ * Fix: excluding columns in FieldsConfigurator was causing inconsistent column behavior (move/hide/resize)
9
24
  * Fix: resolving conflicts with Ext.form.FormPanel's <tt>submit</tt> and <tt>load</tt> methods
10
25
  * New: rudimentary FileUploadField support in FormPanel (it will do a normal, non-AJAX, form submit)
11
26
  * New: Netzke::Window widget, supports persistent moving/resizing.
data/README.rdoc CHANGED
@@ -1,20 +1,30 @@
1
1
  = netzke-basepack
2
2
  A pack of basic Rails/ExtJS widgets as a part of the Netzke framework. Live demo/tutorials on http://blog.writelesscode.com. Introduction to the Netzke framework and the Wiki: http://github.com/skozlov/netzke
3
3
 
4
- Note that if you would like to modify this code or experiment with it, you may be better off cloning this project into your app's vendor/plugin directory - it will then behave as a Rails plugin.
5
-
6
4
  = Prerequisites
7
5
  1. Rails >= 2.2
8
- 2. Ext JS >= 3.0: its root by default must be accessible as RAILS_ROOT/public/extjs. You may symlink your Ext JS library here like this (from your app folder):
6
+ 2. ExtJS = 3.0.3. Its root by default must be accessible as RAILS_ROOT/public/extjs. You may symlink your Ext JS library here like this (from your app folder):
9
7
 
10
- cd public && ln -s ~/Developer/extjs/ext-3.0 extjs
8
+ cd public && ln -s ~/Developer/extjs/ext-3.0.3 extjs
11
9
 
12
- 3. acts_as_list plugin must be installed:
10
+ 3. acts_as_list plugin:
13
11
 
14
12
  ./script/plugin install git://github.com/rails/acts_as_list.git
15
13
 
14
+ 4. netzke-core gem or plugin. Gem:
15
+
16
+ gem install netzke-core
17
+
18
+ Plugin (for the "edge" stuff, recommended):
19
+
20
+ ./script/plugin install git://github.com/skozlov/netzke-core.git
21
+
16
22
  = Installation
17
- The gem is hosted on gemcutter. If you haven't yet enabled gemcutter, run the following:
23
+ For the "edge" stuff, install as plugin (recommended):
24
+
25
+ ./script/plugin install git://github.com/skozlov/netzke-basepack.git
26
+
27
+ Otherwise install as gem. The gem is hosted on gemcutter. If you haven't yet enabled gemcutter, run the following:
18
28
 
19
29
  sudo gem install gemcutter && gem tumble
20
30
 
@@ -22,74 +32,39 @@ Install the gem:
22
32
 
23
33
  gem install netzke-basepack
24
34
 
25
- Include it into environment.rb:
35
+ = Usage
36
+ If using as gem, include it into environment.rb:
26
37
 
27
38
  config.gem "netzke-basepack"
28
39
 
29
- Include mapping for Netzke controller providing *.js and *.css (in routes.rb):
40
+ Include mapping for Netzke controller providing Netzke javascripts and styles (in routes.rb):
30
41
 
31
42
  map.netzke
32
43
 
33
- = Usage
34
- First, run the generators to have the necessary migrations:
44
+ To be able to use persistent on-the-fly configuration of widgets, run netzke-core generators to have the necessary migrations:
35
45
 
36
46
  ./script/generate netzke_core
37
47
 
38
- Do the migrations:
48
+ ... then run the migrations:
39
49
 
40
50
  rake db:migrate
41
51
 
42
- The following example will provide you with a grid-based scaffold for ActiveRecord-model called Book. You may generate it like this:
43
-
44
- ./script/generate model Book title:string amount:integer
45
-
46
- (don't forget to re-run the migrations after it)
47
-
48
- In the controller declare the widget:
49
-
50
- class WelcomeController < ApplicationController
51
- netzke :books, :widget_class_name => 'GridPanel', :data_class_name => 'Book'
52
- end
53
-
54
- After a widget is declared in the controller, it can be accessed in 3 different ways: 1) loaded by means of an automatically created controller action which will produce a basic HTML-page with the widget (handy for testing), 2) embedded directly into a view (by means of helpers), 3) dynamically loaded by other widgets (like BasicApp-derived, if you want a desktop-like, AJAX-driven web-app).
52
+ == Embedding widgets into Rails' view
53
+ To enable Netzke widgets in Rails' views, you need to add the following helper into your layout template, inside the "head" tag:
55
54
 
56
- == Using automatically created controller action
57
- Without writing any more code, you can access the widget by http://localhost:3000/welcome/books_test. That is to say, you simply append _test to your widget's name (as declared in the controller) to get the action's name.
55
+ <%= netzke_init %>
56
+
57
+ This will include both ExtJS-related files (JavaScript and styles), and Netzke-related files.
58
58
 
59
- == Embedding a widget into a view
60
- netzke-core plugin provides the following 2 helpers to put inside your head-tag (use it in your layout):
59
+ Now let's embed a widget into a view. The following example will provide you with a grid-based scaffold for ActiveRecord-model called Book. You may generate it like this:
61
60
 
62
- 1. netzke_js_include - to include extjs and netzke javascript files
63
- 2. netzke_css_include - to include the css. This one can take a parameter to specify a color schema you wish for Ext JS, e.g.: netzke_css_include(:gray)
64
-
65
- Declaring a widget in the controller provides you with a couple of helpers that can be used in the view:
66
-
67
- 1. books_class_definition will contain the JavaScript code defining the code for the JS class.
68
- 2. books_widget_instance will declare a local JavaScript variable called 'book'.
69
- 3. books_widget_render will provide the JavaScript code that calls the "render" method on the variable declared by books_widget_instance; the widget will be rendered into a div with id "books-div".
61
+ ./script/generate model Book title:string amount:integer
70
62
 
71
- Use these helpers inside of the script-tag like the following (in the view):
72
-
73
- <script type="text/javascript" charset="utf-8">
74
- <%= books_class_definition %>
75
- Ext.onReady(function(){
76
- <%= books_widget_instance %>
77
- <%= books_widget_render %>
78
- })
79
- </script>
80
- <div id="books-div">the widget will be rendered in this div</div>
63
+ (don't forget to re-run the migrations after it)
81
64
 
82
- If your layout already calls yield :javascripts wrapped in the <script>-tag, you can have all javascript-code in one place on the page:
65
+ Now embed a widget into a view like this:
83
66
 
84
- <% content_for :javascripts do %>
85
- <%= books_class_definition %>
86
- Ext.onReady(function(){
87
- <%= books_widget_instance %>
88
- books.render("books");
89
- })
90
- <% end %>
91
- <p>... your page content here ...</p>
92
- <div id="books-div">the widget will be rendered in this div</div>
67
+ <%= netzke :books, :class_name => 'GridPanel', :model => 'Book' %>
93
68
 
94
69
  == Dynamic loading of widgets
95
70
  TODO: this part will be covered later
data/Rakefile CHANGED
@@ -1,7 +1,7 @@
1
1
  begin
2
2
  require 'jeweler'
3
3
  Jeweler::Tasks.new do |gemspec|
4
- gemspec.version = "0.5.5.1"
4
+ gemspec.version = "0.5.6"
5
5
  gemspec.name = "netzke-basepack"
6
6
  gemspec.summary = "Pre-built Rails + ExtJS widgets for your RIA"
7
7
  gemspec.description = "A set of full-featured extendible Netzke widgets (such as FormPanel, GridPanel, Window, BorderLayoutPanel, etc) which can be used as building block for your RIA"
@@ -9,7 +9,7 @@ begin
9
9
  gemspec.homepage = "http://github.com/skozlov/netzke-basepack"
10
10
  gemspec.rubyforge_project = "netzke-basepack"
11
11
  gemspec.authors = ["Sergei Kozlov"]
12
- gemspec.add_dependency("netzke-core", ">=0.4.5.2")
12
+ gemspec.add_dependency("netzke-core", ">=0.5.0")
13
13
  gemspec.add_dependency("searchlogic", ">=2.0.0")
14
14
  gemspec.add_dependency("will_paginate", ">=2.0.0")
15
15
  end
data/TODO.rdoc CHANGED
@@ -9,5 +9,8 @@
9
9
  == Optimizations
10
10
  * Check persistent_config-related queries (aren't they too many?)
11
11
 
12
+ == Code design
13
+ * Replace <tt>initial_late_aggregatees</tt> method with a set of methods for each aggregatee, so that more logic would be possible at the moment of on-demand aggregatee loading.
14
+
12
15
  == One day
13
16
  * Replace xcheckbox with checkbox in FormPanel
@@ -22,7 +22,7 @@ Ext.netzke.ComboBox = Ext.extend(Ext.form.ComboBox, {
22
22
  } else {
23
23
  var row = Ext.data.Record.create([{name:'id'}]);
24
24
  var store = new Ext.data.Store({
25
- proxy : new Ext.data.HttpProxy({url:this.parentId+"__get_combobox_options", jsonData:{column:this.name}}),
25
+ proxy : new Ext.data.HttpProxy({url: Ext.getCmp(this.parentId).buildApiUrl("get_combobox_options"), jsonData:{column:this.name}}),
26
26
  reader : new Ext.data.ArrayReader({root:'data', id:0}, row)
27
27
  });
28
28
 
@@ -794,35 +794,75 @@ Ext.grid.HeaderDropZone.prototype.onNodeDrop = function(n, dd, e, data){
794
794
  return true;
795
795
  }
796
796
  return false;
797
- }
798
-
799
- // Feedback Ghost
800
- Netzke.FeedbackGhost = function(){};
801
- Ext.apply(Netzke.FeedbackGhost.prototype, {
802
- showFeedback: function(msg){
803
- var createBox = function(s, l){
804
- return ['<div class="msg">',
805
- '<div class="x-box-tl"><div class="x-box-tr"><div class="x-box-tc"></div></div></div>',
806
- '<div class="x-box-ml"><div class="x-box-mr"><div class="x-box-mc">', s, '</div></div></div>',
807
- '<div class="x-box-bl"><div class="x-box-br"><div class="x-box-bc"></div></div></div>',
808
- '</div>'].join('');
809
- }
797
+ };
810
798
 
811
- var showBox = function(msg, lvl){
812
- if (!lvl) {lvl = 'notice'};
813
- var msgCt = Ext.DomHelper.insertFirst(document.body, {'class':'netzke-feedback'}, true);
814
- var m = Ext.DomHelper.append(msgCt, {html:createBox(msg,lvl)}, true);
815
- m.slideIn('t').pause(2).ghost("b", {remove:true});
816
- }
799
+ // Temporary fix for Ext 3.1 resize problem:
800
+ Ext.override(Ext.Panel, {
801
+
802
+ // private
803
+ onResize : function(w, h, rw, rh){
804
+ if(Ext.isDefined(w) || Ext.isDefined(h)){
805
+ if(!this.collapsed){
806
+ // First, set the the Panel's body width.
807
+ // If we have auto-widthed it, get the resulting full offset width so we can size the Toolbars to match
808
+ // The Toolbars must not buffer this resize operation because we need to know their heights.
809
+
810
+ if(Ext.isNumber(w)){
811
+ this.body.setWidth(w = this.adjustBodyWidth(w - this.getFrameWidth()));
812
+ } else if (w == 'auto') {
813
+ w = this.body.setWidth('auto').dom.offsetWidth;
814
+ } else {
815
+ w = this.body.dom.offsetWidth;
816
+ }
817
817
 
818
- if (typeof msg != 'string') {
819
- var compoundMsg = "";
820
- Ext.each(msg, function(m){
821
- compoundMsg += m.msg + '<br>';
822
- });
823
- if (compoundMsg != "") showBox(compoundMsg, null); // the second parameter will be level
824
- } else {
825
- showBox(msg);
818
+ if(this.tbar){
819
+ this.tbar.setWidth(w);
820
+ if(this.topToolbar){
821
+ this.topToolbar.setSize(w);
822
+ }
823
+ }
824
+ if(this.bbar){
825
+ this.bbar.setWidth(w);
826
+ if(this.bottomToolbar){
827
+ this.bottomToolbar.setSize(w);
828
+ // The bbar does not move on resize without this.
829
+ if (Ext.isIE) {
830
+ this.bbar.setStyle('position', 'static');
831
+ this.bbar.setStyle('position', '');
832
+ }
833
+ }
834
+ }
835
+ if(this.footer){
836
+ this.footer.setWidth(w);
837
+ if(this.fbar){
838
+ this.fbar.setSize(Ext.isIE ? (w - this.footer.getFrameWidth('lr')) : 'auto');
839
+ }
840
+ }
841
+
842
+ // At this point, the Toolbars must be layed out for getFrameHeight to find a result.
843
+ if(Ext.isNumber(h)){
844
+ h = Math.max(0, this.adjustBodyHeight(h - this.getFrameHeight()));
845
+ this.body.setHeight(h);
846
+ }else if(h == 'auto'){
847
+ this.body.setHeight(h);
848
+ }
849
+
850
+ if(this.disabled && this.el._mask){
851
+ this.el._mask.setSize(this.el.dom.clientWidth, this.el.getHeight());
852
+ }
853
+ }else{
854
+ this.queuedBodySize = {width: w, height: h};
855
+ if(!this.queuedExpand && this.allowQueuedExpand !== false){
856
+ this.queuedExpand = true;
857
+ this.on('expand', function(){
858
+ delete this.queuedExpand;
859
+ this.onResize(this.queuedBodySize.width, this.queuedBodySize.height);
860
+ }, this, {single:true});
861
+ }
862
+ }
863
+ this.onBodyResize(w, h);
864
+ }
865
+ this.syncShadow();
866
+ Ext.Panel.superclass.onResize.call(this, w, h, rw, rh);
826
867
  }
827
- }
828
- });
868
+ });
@@ -1,56 +1,4 @@
1
- require 'acts_as_list'
2
- class NetzkeAutoColumn < ActiveRecord::Base
3
-
1
+ class NetzkeAutoColumn < NetzkeAutoTable
4
2
  acts_as_list
5
3
  default_scope :order => "position"
6
-
7
- # Returns an array of column configuration hashes (without the "id" attribute)
8
- def self.all_columns
9
- self.all.map do |c|
10
- column_hash = c.attributes.reject{ |k,v| k == 'id' }
11
- column_hash.each_pair do |k,v|
12
- # try to detect JSON format
13
- begin
14
- normalized_value = v.is_a?(String) ? ActiveSupport::JSON.decode(v) : v
15
- rescue ActiveSupport::JSON::ParseError
16
- normalized_value = v
17
- end
18
- column_hash[k] = normalized_value
19
- end
20
- column_hash
21
- end
22
- end
23
-
24
- # Build the table with columns for this widget
25
- def self.rebuild_table
26
- connection.drop_table('netzke_auto_columns') if table_exists?
27
-
28
- normalized_config_columns = []
29
-
30
- @@widget.class.config_columns.each do |mc|
31
- column_hash = mc.is_a?(Symbol) ? {:name => mc} : mc
32
- column_hash[:type] ||= :string
33
- normalized_config_columns << column_hash
34
- end
35
-
36
- # create the table with the fields
37
- self.connection.create_table('netzke_auto_columns') do |t|
38
- normalized_config_columns.each do |mc|
39
- t.column mc[:name], mc[:type], :default => mc[:default]
40
- end
41
- t.column :position, :integer
42
- end
43
-
44
- # populate the table with data
45
- NetzkeAutoColumn.create @@widget.normalized_columns.map(&:deebeefy_values) rescue ActiveRecord::UnknownAttributeError
46
-
47
- end
48
-
49
- def self.widget=(widget)
50
- @@widget = widget
51
- if Netzke::Base.session["netzke_auto_column_last_widget"] != @@widget.global_id
52
- rebuild_table
53
- Netzke::Base.session["netzke_auto_column_last_widget"] = @@widget.global_id
54
- end
55
- end
56
4
  end
@@ -0,0 +1,4 @@
1
+ class NetzkeAutoField < NetzkeAutoTable
2
+ acts_as_list
3
+ default_scope :order => "position"
4
+ end
@@ -0,0 +1,61 @@
1
+ require 'acts_as_list'
2
+
3
+ class NetzkeAutoTable < ActiveRecord::Base
4
+
5
+ # Returns an array of column configuration hashes (without the "id" attribute)
6
+ def self.all_columns
7
+ self.all.map do |c|
8
+ column_hash = c.attributes.reject{ |k,v| k == 'id' }
9
+ column_hash.each_pair do |k,v|
10
+ # try to detect JSON format
11
+ begin
12
+ normalized_value = v.is_a?(String) ? ActiveSupport::JSON.decode(v) : v
13
+ rescue ActiveSupport::JSON::ParseError
14
+ normalized_value = v
15
+ end
16
+ column_hash[k] = normalized_value
17
+ end
18
+ column_hash
19
+ end
20
+ end
21
+
22
+ # Build the table with columns for this widget
23
+ def self.rebuild_table
24
+ connection.drop_table(table_name) if table_exists?
25
+
26
+ normalized_config_columns = []
27
+
28
+ widget = read_inheritable_attribute(:widget)
29
+
30
+ widget.class.config_columns.each do |mc|
31
+ column_hash = mc.is_a?(Symbol) ? {:name => mc} : mc
32
+ column_hash[:type] ||= :string
33
+ normalized_config_columns << column_hash
34
+ end
35
+
36
+ # create the table with the fields
37
+ self.connection.create_table(table_name) do |t|
38
+ normalized_config_columns.each do |mc|
39
+ t.column mc[:name], mc[:type], :default => mc[:default]
40
+ end
41
+ t.column :position, :integer
42
+ end
43
+
44
+ # populate the table with data
45
+ create widget.normalized_columns.map(&:deebeefy_values) # rescue ActiveRecord::UnknownAttributeError
46
+
47
+ end
48
+
49
+ def self.table_name
50
+ self.name.tableize
51
+ end
52
+
53
+ def self.widget=(widget)
54
+ write_inheritable_attribute(:widget, widget)
55
+ if Netzke::Base.session["#{table_name}_last_widget"] != widget.global_id
56
+ rebuild_table
57
+ Netzke::Base.session["#{table_name}_last_widget"] = widget.global_id
58
+ end
59
+ end
60
+
61
+ end
@@ -1,11 +1,18 @@
1
1
  # External dependencies
2
+ require 'active_support'
2
3
  require 'netzke-core'
3
- require 'searchlogic'
4
- require 'will_paginate'
5
4
 
6
- require 'netzke/active_record/basepack'
5
+ # ExtJS-related constants
7
6
  require 'netzke/ext'
8
7
 
8
+ # Make widget classes auto-loadable with help of ActiveSupport
9
+ path = File.dirname(__FILE__)
10
+ ActiveSupport::Dependencies.load_paths << path
11
+
12
+ # Make this plugin auto-reloadable for easier development
13
+ ActiveSupport::Dependencies.load_once_paths.delete(path)
14
+
15
+ # Make gem's models auto-loadable
9
16
  %w{ models }.each do |dir|
10
17
  path = File.join(File.dirname(__FILE__), 'app', dir)
11
18
  $LOAD_PATH << path
@@ -13,11 +20,10 @@ require 'netzke/ext'
13
20
  ActiveSupport::Dependencies.load_once_paths.delete(path)
14
21
  end
15
22
 
16
- # Make this plugin reloadable at app restart for easier development
17
- ActiveSupport::Dependencies.load_once_paths.delete(File.join(File.dirname(__FILE__)))
18
-
19
23
  # Include javascript & styles required by all basepack widgets.
20
24
  # These files will get loaded at the initial load of the framework (along with Ext and Netzke-core).
21
25
  Netzke::Base.config[:javascripts] << "#{File.dirname(__FILE__)}/../javascripts/basepack.js"
22
26
  Netzke::Base.config[:stylesheets] << "#{File.dirname(__FILE__)}/../stylesheets/basepack.css"
23
- Netzke::Base.config[:stylesheets] << Netzke::Base.config[:ext_location] + "/examples/form/file-upload.css"
27
+
28
+ # FIXME: doesn't belong here
29
+ Netzke::Base.config[:stylesheets] << Netzke::Base.config[:ext_location] + "/examples/ux/fileuploadfield/css/fileuploadfield.css" if Netzke::Base.config[:ext_location]