engine2 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (145) hide show
  1. checksums.yaml +7 -0
  2. data/Gemfile +2 -0
  3. data/Rakefile +138 -0
  4. data/conf/message.yaml +93 -0
  5. data/conf/message_pl.yaml +93 -0
  6. data/engine2.gemspec +34 -0
  7. data/lib/engine2.rb +34 -0
  8. data/lib/engine2/action.rb +217 -0
  9. data/lib/engine2/core.rb +572 -0
  10. data/lib/engine2/handler.rb +134 -0
  11. data/lib/engine2/meta.rb +969 -0
  12. data/lib/engine2/meta/decode_meta.rb +110 -0
  13. data/lib/engine2/meta/delete_meta.rb +73 -0
  14. data/lib/engine2/meta/form_meta.rb +144 -0
  15. data/lib/engine2/meta/infra_meta.rb +292 -0
  16. data/lib/engine2/meta/link_meta.rb +133 -0
  17. data/lib/engine2/meta/list_meta.rb +284 -0
  18. data/lib/engine2/meta/save_meta.rb +63 -0
  19. data/lib/engine2/meta/view_meta.rb +22 -0
  20. data/lib/engine2/model.rb +390 -0
  21. data/lib/engine2/models/Files.rb +38 -0
  22. data/lib/engine2/models/UserInfo.rb +24 -0
  23. data/lib/engine2/post_bootstrap.rb +83 -0
  24. data/lib/engine2/pre_bootstrap.rb +27 -0
  25. data/lib/engine2/scheme.rb +202 -0
  26. data/lib/engine2/templates.rb +229 -0
  27. data/lib/engine2/type_info.rb +342 -0
  28. data/lib/engine2/version.rb +9 -0
  29. data/public/assets/javascripts.js +13 -0
  30. data/public/assets/styles.css +4 -0
  31. data/public/css/angular-motion.css +1022 -0
  32. data/public/css/angular-ui-tree.min.css +1 -0
  33. data/public/css/app.css +196 -0
  34. data/public/css/bootstrap-additions.css +1560 -0
  35. data/public/css/bootstrap.min.css +11 -0
  36. data/public/css/font-awesome.min.css +4 -0
  37. data/public/favicon.ico +0 -0
  38. data/public/fonts/FontAwesome.otf +0 -0
  39. data/public/fonts/fontawesome-webfont.eot +0 -0
  40. data/public/fonts/fontawesome-webfont.svg +655 -0
  41. data/public/fonts/fontawesome-webfont.ttf +0 -0
  42. data/public/fonts/fontawesome-webfont.woff +0 -0
  43. data/public/fonts/fontawesome-webfont.woff2 +0 -0
  44. data/public/fonts/glyphicons-halflings-regular.eot +0 -0
  45. data/public/fonts/glyphicons-halflings-regular.svg +288 -0
  46. data/public/fonts/glyphicons-halflings-regular.ttf +0 -0
  47. data/public/fonts/glyphicons-halflings-regular.woff +0 -0
  48. data/public/fonts/glyphicons-halflings-regular.woff2 +0 -0
  49. data/public/images/file.png +0 -0
  50. data/public/images/folder-closed.png +0 -0
  51. data/public/images/folder.png +0 -0
  52. data/public/images/node-closed-2.png +0 -0
  53. data/public/images/node-closed-light.png +0 -0
  54. data/public/images/node-closed.png +0 -0
  55. data/public/images/node-opened-2.png +0 -0
  56. data/public/images/node-opened-light.png +0 -0
  57. data/public/images/node-opened.png +0 -0
  58. data/public/img/ajax-loader-dark.gif +0 -0
  59. data/public/img/ajax-loader-light.gif +0 -0
  60. data/public/img/ajax-loader.gif +0 -0
  61. data/public/js/angular-animate.js +4115 -0
  62. data/public/js/angular-cookies.js +322 -0
  63. data/public/js/angular-local-storage.js +455 -0
  64. data/public/js/angular-route.js +1022 -0
  65. data/public/js/angular-sanitize.js +717 -0
  66. data/public/js/angular-strap.js +4339 -0
  67. data/public/js/angular-strap.tpl.js +43 -0
  68. data/public/js/angular-ui-tree.js +1569 -0
  69. data/public/js/angular.js +30714 -0
  70. data/public/js/i18n/angular-locale_pl.js +115 -0
  71. data/public/js/lodash.custom.min.js +97 -0
  72. data/public/js/ng-file-upload-shim.min.js +2 -0
  73. data/public/js/ng-file-upload.min.js +3 -0
  74. data/views/app.coffee +3 -0
  75. data/views/engine2.coffee +557 -0
  76. data/views/engine2actions.coffee +849 -0
  77. data/views/engine2templates.coffee +0 -0
  78. data/views/fields/blob.slim +22 -0
  79. data/views/fields/bs_select.slim +10 -0
  80. data/views/fields/bsselect_picker.slim +18 -0
  81. data/views/fields/bsselect_picker_opt.slim +22 -0
  82. data/views/fields/checkbox.slim +11 -0
  83. data/views/fields/checkbox_buttons.slim +6 -0
  84. data/views/fields/checkbox_buttons_opt.slim +8 -0
  85. data/views/fields/currency.slim +10 -0
  86. data/views/fields/date.slim +21 -0
  87. data/views/fields/date_range.slim +44 -0
  88. data/views/fields/date_time.slim +42 -0
  89. data/views/fields/datetime.slim +42 -0
  90. data/views/fields/decimal.slim +11 -0
  91. data/views/fields/decimal_date.slim +22 -0
  92. data/views/fields/decimal_time.slim +26 -0
  93. data/views/fields/email.slim +13 -0
  94. data/views/fields/file_store.slim +61 -0
  95. data/views/fields/input_text.slim +14 -0
  96. data/views/fields/integer.slim +11 -0
  97. data/views/fields/list_bsselect.slim +18 -0
  98. data/views/fields/list_bsselect_opt.slim +21 -0
  99. data/views/fields/list_buttons.slim +3 -0
  100. data/views/fields/list_buttons_opt.slim +5 -0
  101. data/views/fields/list_select.slim +11 -0
  102. data/views/fields/list_select_opt.slim +15 -0
  103. data/views/fields/password.slim +14 -0
  104. data/views/fields/radio_checkbox.slim +10 -0
  105. data/views/fields/scaffold.slim +2 -0
  106. data/views/fields/scaffold_picker.slim +20 -0
  107. data/views/fields/select_picker.slim +12 -0
  108. data/views/fields/select_picker_opt.slim +16 -0
  109. data/views/fields/text_area.slim +10 -0
  110. data/views/fields/time.slim +22 -0
  111. data/views/fields/typeahead_picker.slim +25 -0
  112. data/views/index.slim +44 -0
  113. data/views/infra/index.slim +5 -0
  114. data/views/infra/inspect.slim +81 -0
  115. data/views/modals/close_m.slim +15 -0
  116. data/views/modals/confirm_m.slim +19 -0
  117. data/views/modals/empty_m.slim +12 -0
  118. data/views/modals/menu_m.slim +13 -0
  119. data/views/modals/yes_no_m.slim +19 -0
  120. data/views/panels/menu_m.slim +9 -0
  121. data/views/scaffold/confirm.slim +3 -0
  122. data/views/scaffold/fields.slim +10 -0
  123. data/views/scaffold/form.slim +11 -0
  124. data/views/scaffold/list.slim +42 -0
  125. data/views/scaffold/message.slim +3 -0
  126. data/views/scaffold/search.slim +20 -0
  127. data/views/scaffold/view.slim +18 -0
  128. data/views/search_fields/bsmselect_picker.slim +25 -0
  129. data/views/search_fields/bsselect_picker.slim +24 -0
  130. data/views/search_fields/checkbox.slim +11 -0
  131. data/views/search_fields/checkbox2.slim +14 -0
  132. data/views/search_fields/checkbox_buttons.slim +10 -0
  133. data/views/search_fields/date_range.slim +46 -0
  134. data/views/search_fields/decimal_date_range.slim +47 -0
  135. data/views/search_fields/input_text.slim +18 -0
  136. data/views/search_fields/integer.slim +18 -0
  137. data/views/search_fields/integer_range.slim +27 -0
  138. data/views/search_fields/list_bsmselect.slim +24 -0
  139. data/views/search_fields/list_bsselect.slim +22 -0
  140. data/views/search_fields/list_buttons.slim +8 -0
  141. data/views/search_fields/list_select.slim +17 -0
  142. data/views/search_fields/scaffold_picker.slim +19 -0
  143. data/views/search_fields/select_picker.slim +17 -0
  144. data/views/search_fields/typeahead_picker.slim +25 -0
  145. metadata +327 -0
@@ -0,0 +1,572 @@
1
+ #coding: utf-8
2
+
3
+ module PrettyJSON
4
+ def to_json_pretty
5
+ JSON.pretty_generate(self)
6
+ end
7
+ end
8
+
9
+ class BigDecimal
10
+ def to_json(*)
11
+ # super
12
+ to_s('f')
13
+ end
14
+ end
15
+
16
+ class Object
17
+ def instance_variables_hash
18
+ instance_variables.inject({}) do |h, i|
19
+ h[i] = instance_variable_get(i)
20
+ h
21
+ end
22
+ end
23
+ end
24
+
25
+ class Proc
26
+ def to_json(*)
27
+ loc = source_location
28
+ "\"#<Proc:#{loc.first[/\w+.rb/]}:#{loc.last}>\""
29
+ end
30
+ end
31
+
32
+ class Hash
33
+ include PrettyJSON
34
+
35
+ def rmerge!(other_hash)
36
+ merge!(other_hash) do |key, oldval, newval|
37
+ oldval.class == self.class ? oldval.rmerge!(newval) : newval
38
+ end
39
+ end
40
+
41
+ def rmerge(other_hash)
42
+ r = {}
43
+ merge(other_hash) do |key, oldval, newval|
44
+ r[key] = oldval.class == self.class ? oldval.rmerge(newval) : newval
45
+ end
46
+ end
47
+
48
+ def rmerge2(other_hash)
49
+ r = {}
50
+ merge(other_hash) do |key, oldval, newval|
51
+ r[key] = oldval.class == self.class ? oldval.rmerge2(newval) : (oldval == nil ? newval : oldval)
52
+ end
53
+ end
54
+
55
+ def rmerge2!(other_hash)
56
+ r = {}
57
+ merge!(other_hash) do |key, oldval, newval|
58
+ r[key] = oldval.class == self.class ? oldval.rmerge2!(newval) : (oldval == nil ? newval : oldval)
59
+ end
60
+ end
61
+
62
+ def rdup
63
+ duplicate = self.dup
64
+ duplicate.each_pair do |k,v|
65
+ tv = duplicate[k]
66
+ duplicate[k] = tv.is_a?(Hash) && v.is_a?(Hash) ? tv.rdup : v
67
+ end
68
+ duplicate
69
+ end
70
+
71
+ def path *a
72
+ h = self
73
+ i = 0
74
+ while h && i != a.length
75
+ h = h[a[i]]
76
+ i += 1
77
+ end
78
+ h
79
+ end
80
+
81
+ def path! *a, v
82
+ h = self
83
+ i = 0
84
+ while i < a.length - 1
85
+ h = h[a[i]] ||= {}
86
+ i += 1
87
+ end
88
+ h[a[i]] = v
89
+ end
90
+
91
+ end
92
+
93
+ class String
94
+ def limit_length num
95
+ s = self.strip
96
+ if s.length > num
97
+ s[0..num] + "..."
98
+ else
99
+ s
100
+ end
101
+ end
102
+ end
103
+
104
+ class Symbol
105
+ def icon
106
+ "<span class='glyphicon glyphicon-#{self}'></span>"
107
+ end
108
+
109
+ def aicon
110
+ "<i class='fa fa-#{self}'></i>"
111
+ end
112
+ end
113
+
114
+ class << Sequel
115
+ attr_accessor :alias_tables_in_joins
116
+
117
+ def split_keys id
118
+ id.split('|')
119
+ end
120
+ end
121
+
122
+ class Sequel::Database
123
+ attr_accessor :models, :default_schema
124
+
125
+ def cache_file
126
+ "#{APP_LOCATION}/#{opts[:orig_opts][:name]}.dump"
127
+ end
128
+
129
+ def load_schema_cache_from_file
130
+ self.models = {}
131
+ load_schema_cache? cache_file if adapter_scheme
132
+ end
133
+
134
+ def dump_schema_cache_to_file
135
+ dump_schema_cache? cache_file if adapter_scheme
136
+ end
137
+ end
138
+
139
+ Sequel.quote_identifiers = false
140
+ Sequel.extension :core_extensions
141
+ Sequel::Inflections.clear
142
+ Sequel.alias_tables_in_joins = true
143
+ # Sequel::Model.plugin :json_serializer, :naked => true
144
+ # Sequel::Model.plugin :timestamps
145
+ # Sequel::Model.plugin :validation_class_methods
146
+ # Sequel::Model.raise_on_typecast_failure = false
147
+ # Sequel::Model.raise_on_save_failure = false
148
+ # Sequel::Model.unrestrict_primary_key
149
+ # Sequel::Model.plugin :validation_helpers
150
+ Sequel::Database::extension :schema_caching
151
+
152
+ module E2Model
153
+ module InstanceMethods
154
+ attr_accessor :skip_save_refresh, :validate_fields
155
+
156
+ def has_primary_key?
157
+ pk = self.pk
158
+ pk.is_a?(Array) ? !pk.all?{|k|k.nil?} : !pk.nil?
159
+ end
160
+
161
+ def primary_key_values
162
+ model.primary_keys.map{|k|@values[k]}
163
+ end
164
+
165
+ def _save_refresh
166
+ super unless skip_save_refresh
167
+ end
168
+
169
+ def validation
170
+ end
171
+
172
+ def before_save
173
+ super
174
+ model.before_save_processors.each_pair do |name, proc|
175
+ proc.(self, name, model.type_info.fetch(name))
176
+ end if model.before_save_processors
177
+
178
+ unless model.dummies.empty?
179
+ dummies = {}
180
+ model.dummies.each do |d|
181
+ dummies[d] = values.delete(d)
182
+ end
183
+ @dummy_fields = dummies
184
+ end
185
+
186
+ unless self.pk
187
+ sequence = model.type_info[model.primary_key][:sequence]
188
+ self[model.primary_key] = sequence.lit if sequence
189
+ end
190
+ end
191
+
192
+ def after_save
193
+ unless model.dummies.empty?
194
+ @values.merge!(@dummy_fields)
195
+ @dummy_fields = nil
196
+ end
197
+ model.after_save_processors.each_pair do |name, proc|
198
+ proc.(self, name, model.type_info.fetch(name))
199
+ end if model.after_save_processors
200
+
201
+ super
202
+ end
203
+
204
+ def before_destroy
205
+ model.before_destroy_processors.each_pair do |name, proc|
206
+ proc.(self, name, model.type_info.fetch(name))
207
+ end if model.before_destroy_processors
208
+ super
209
+ end
210
+
211
+ def after_destroy
212
+ model.after_destroy_processors.each_pair do |name, proc|
213
+ proc.(self, name, model.type_info.fetch(name))
214
+ end if model.after_destroy_processors
215
+ super
216
+ end
217
+
218
+ def validate
219
+ super
220
+ auto_validate
221
+ validation
222
+ end
223
+
224
+ def auto_validate
225
+ type_info = model.type_info
226
+ @validate_fields.each do |name| # || type_info.keys
227
+ info = type_info[name]
228
+ next if info[:primary_key] && !model.natural_key
229
+
230
+ value = values[name].to_s
231
+ value.strip! unless info[:dont_strip]
232
+ if value.empty?
233
+ if req = info[:required]
234
+ errors.add(name, req[:message]) if !req[:if] || req[:if].(self)
235
+ end
236
+ else
237
+ info[:validations].each_pair do |validation, args|
238
+ validation_proc = Engine2::Validations[validation] || args[:lambda] # swap ?
239
+ raise "Validation not found for field '#{name}' of type #{validation}" unless validation_proc
240
+ if result = validation_proc.(self, name, info)
241
+ errors.add(name, result)
242
+ break
243
+ end
244
+ end
245
+ end
246
+ end
247
+
248
+ # if errors.empty? && model.natural_key && new?
249
+ # unless model.dataset.where(model.primary_keys_hash(primary_key_values)).empty? # optimize the keys part
250
+ # model.primary_keys.each{|pk| errors.add(pk, "must be unique")}
251
+ # end
252
+ # end
253
+ end
254
+ end
255
+
256
+ module ClassMethods
257
+ attr_reader :natural_key
258
+
259
+ def set_natural_key key
260
+ set_primary_key key
261
+ @natural_key = true
262
+ end
263
+
264
+ def primary_keys
265
+ # cache it ?
266
+ key = primary_key
267
+ key.is_a?(Array) ? key : [key]
268
+ end
269
+
270
+ def primary_keys_qualified
271
+ # cache it ?
272
+ primary_keys.map{|k|k.qualify(table_name)}
273
+ end
274
+
275
+ def primary_keys_hash id
276
+ Hash[primary_keys.zip(id)]
277
+ end
278
+
279
+ def primary_keys_hash_qualified id
280
+ Hash[primary_keys_qualified.zip(id)]
281
+ end
282
+ end
283
+
284
+ module DatasetMethods
285
+
286
+ def ensure_primary_key
287
+ pk = @model.primary_keys
288
+
289
+ if opts_select = @opts[:select]
290
+ sel_pk = []
291
+ opts_select.each do |sel|
292
+ name = case sel
293
+ when Symbol
294
+ sel.to_s =~ /\w+__(\w+)/ ? $1.to_sym : sel
295
+ when Sequel::SQL::QualifiedIdentifier
296
+ sel.column
297
+ when Sequel::SQL::AliasedExpression
298
+ sel
299
+ # nil #sel.aliaz # ?
300
+ # sel.expression
301
+ end
302
+ sel_pk << name if name && pk.include?(name)
303
+ end
304
+
305
+ if pk.length == sel_pk.length
306
+ self
307
+ else
308
+ sels = (pk - sel_pk).map{|k| k.qualify(@model.table_name)}
309
+ select_more(*sels)
310
+ end
311
+ else
312
+ select(*pk.map{|k| k.qualify(@model.table_name)})
313
+ end
314
+
315
+ end
316
+
317
+ def setup! fields
318
+ joins = {}
319
+ type_info = model.type_info
320
+ model_table_name = model.table_name
321
+
322
+ @opts[:select].map! do |sel|
323
+ extract_select sel do |table, name, aliaz|
324
+ if table
325
+ if table == model_table_name
326
+ m = model
327
+ else
328
+ a = model.many_to_one_associations[table] # || model.one_to_one_associations[table]
329
+ raise Engine2::E2Error.new("Association #{table} not found for model #{model}") unless a
330
+ m = Object.const_get(a[:class_name])
331
+ end
332
+ # raise Engine2::E2Error.new("Model not found for table #{table} in model #{model}") unless m
333
+ info = m.type_info
334
+ else
335
+ info = type_info
336
+ end
337
+
338
+ f_info = info[name]
339
+ raise Engine2::E2Error.new("Column #{name} not found for table #{table || model_table_name}") unless f_info
340
+
341
+ table ||= model_table_name
342
+
343
+ if table == model_table_name
344
+ fields << name
345
+ else
346
+ fields << :"#{table}__#{name}"
347
+ assoc = model.many_to_one_associations[table]
348
+ unless assoc
349
+ # fail
350
+ end
351
+ joins[table] = assoc
352
+ end
353
+
354
+ if f_info[:dummy]
355
+ nil
356
+ # elsif f_info[:type] == :blob_store
357
+ # # (~{name => nil}).as :name
358
+ # # Sequel.char_length(name).as name
359
+ # nil
360
+ else
361
+ if table != model_table_name
362
+ if Sequel.alias_tables_in_joins
363
+ name.qualify(table).as(:"#{table}__#{name}")
364
+ else
365
+ name.qualify(table)
366
+ end
367
+ else
368
+ name.qualify(table)
369
+ end
370
+ end
371
+ end
372
+ end
373
+
374
+ @opts[:select].compact!
375
+
376
+ joins.reduce(self) do |joined, (table, assoc)|
377
+ assoc = model.many_to_one_associations[table] # || model.one_to_one_associations[table]
378
+ m = Object.const_get(assoc[:class_name])
379
+ keys = assoc[:qualified_key]
380
+ keys = [keys] unless keys.is_a?(Array)
381
+ joined.left_join(table, m.primary_keys.zip(keys))
382
+ end
383
+ end
384
+
385
+ def extract_select sel, al = nil, &blk
386
+ case sel
387
+ when Symbol
388
+ if sel.to_s =~ /^(\w+)__(\w+?)(?:___(\w+))?$/
389
+ yield $1.to_sym, $2.to_sym, $3 ? $3.to_sym : nil
390
+ else
391
+ yield nil, sel, al
392
+ end
393
+ when Sequel::SQL::QualifiedIdentifier
394
+ yield sel.table, sel.column, al
395
+ when Sequel::SQL::AliasedExpression
396
+ sel
397
+ # extract_select sel.expression, sel.aliaz, &blk
398
+ # expr = sel.expression
399
+ # yield expr.table, expr.column
400
+ else
401
+ raise Engine2::E2Error.new("Unknown selection #{sel}")
402
+ end
403
+ end
404
+
405
+ def get_opts
406
+ @opts
407
+ end
408
+
409
+ def with_proc &blk
410
+ ds = clone
411
+ ds.row_proc = blk
412
+ ds
413
+ end
414
+ end
415
+ end
416
+
417
+ Sequel::Model.plugin E2Model
418
+
419
+ module Sequel
420
+ class DestroyFailed < Error
421
+ attr_reader :error
422
+
423
+ def initialize error
424
+ @error = error
425
+ end
426
+ end
427
+
428
+ end
429
+
430
+ module Engine2
431
+ LOCS ||= Hash.new{|h, k| ":#{k}:"}
432
+ PATH ||= File.expand_path('../..', File.dirname(__FILE__))
433
+
434
+ class << self
435
+ attr_accessor :core_loading
436
+ end
437
+
438
+ self.core_loading = true
439
+
440
+ def self.database name
441
+ Object.const_set(name, yield) unless Object.const_defined?(name)
442
+ end
443
+
444
+ def self.connect *args
445
+ db = Sequel.connect *args
446
+ db.models = {}
447
+ db
448
+ end
449
+
450
+ e2_db_file = (defined? JRUBY_VERSION) ? "jdbc:sqlite:#{APP_LOCATION}/engine2.db" : "sqlite://#{APP_LOCATION}/engine2.db"
451
+ E2DB ||= connect e2_db_file, loggers: [Logger.new($stdout)], convert_types: false, name: :engine2
452
+ DUMMYDB ||= Sequel::Database.new uri: 'dummy'
453
+ def DUMMYDB.synchronize *args;end
454
+
455
+ def self.boot &blk
456
+ @boot_blk = blk
457
+ end
458
+
459
+ def self.bootstrap app = APP_LOCATION
460
+ require 'engine2/pre_bootstrap'
461
+ t = Time.now
462
+ Action.count = 0
463
+ SCHEMES.clear
464
+
465
+ load "#{app}/boot.rb"
466
+
467
+ Sequel::DATABASES.each &:load_schema_cache_from_file
468
+ load 'engine2/models/Files.rb'
469
+ load 'engine2/models/UserInfo.rb'
470
+ Dir["#{app}/models/*"].each{|m| load m}
471
+ puts "MODELS, Time: #{Time.now - t}"
472
+ Sequel::DATABASES.each &:dump_schema_cache_to_file
473
+
474
+ SCHEMES.merge!
475
+ Engine2.send(:remove_const, :ROOT) if defined? ROOT
476
+ Engine2.const_set(:ROOT, Action.new(nil, :api, DummyMeta, {}))
477
+
478
+ @boot_blk.(ROOT)
479
+ ROOT.setup_action_tree
480
+ puts "BOOTSTRAP #{app}, Time: #{Time.new - t}"
481
+ self.core_loading = false
482
+ require 'engine2/post_bootstrap'
483
+ end
484
+
485
+ class E2Error < RuntimeError
486
+ def initialize msg
487
+ super
488
+ end
489
+ end
490
+
491
+ class MenuBuilder
492
+ attr_accessor :name
493
+ attr_reader :entries
494
+
495
+ def initialize name, properties = {}
496
+ @name = name
497
+ @properties = properties
498
+ @entries = []
499
+ end
500
+
501
+ def properties props = nil
502
+ props ? @properties.merge!(props) : @properties
503
+ end
504
+
505
+ def option name, properties = {}, index = @entries.size, &blk
506
+ if blk
507
+ entries = MenuBuilder.new(name, properties)
508
+ entries.instance_eval(&blk)
509
+ @entries.insert index, entries
510
+ else
511
+ @entries.insert index, {name: name}.merge(properties)
512
+ end
513
+ end
514
+
515
+ def option_before iname, name, properties = {}, &blk
516
+ option name, properties, option_index(iname), &blk
517
+ end
518
+
519
+ def option_after iname, name, properties = {}, &blk
520
+ option name, properties, option_index(iname) + 1, &blk
521
+ end
522
+
523
+ def option_at index, name, properties = {}, &blk
524
+ option name, properties, index, &blk
525
+ end
526
+
527
+ def option_index iname
528
+ index = @entries.index{|e| (e.is_a?(MenuBuilder) ? e.name : e[:name]) == iname}
529
+ raise E2Error.new("No menu option #{iname} found") unless index
530
+ index
531
+ end
532
+
533
+ def modify_option name, properties
534
+ index = option_index(name)
535
+ entry = @entries[index]
536
+ props = entry.is_a?(MenuBuilder) ? entry.properties : entry
537
+ props.merge!(properties)
538
+ end
539
+
540
+ def divider
541
+ @entries << {divider: true}
542
+ end
543
+
544
+ def to_a
545
+ @entries.map do |m|
546
+ if m.is_a? MenuBuilder
547
+ h = {entries: m.to_a}.merge(m.properties)
548
+ h[:loc] ||= LOCS[m.name]
549
+ {menu: h}
550
+ else
551
+ m[:loc] ? m : m.merge(loc: LOCS[m[:name]])
552
+ end
553
+ end
554
+ end
555
+
556
+ def each &blk
557
+ @entries.each do |m|
558
+ if m.is_a? MenuBuilder
559
+ m.each &blk
560
+ else
561
+ yield m
562
+ end
563
+ end
564
+ end
565
+ end
566
+
567
+ class ActionMenuBuilder < MenuBuilder
568
+ def option name, properties = {}, index = @entries.size, &blk
569
+ super
570
+ end
571
+ end
572
+ end