engine2 1.0.0

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 (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