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,849 @@
1
+ angular.module('Engine2')
2
+ .directive 'e2Action', (E2Actions) ->
3
+ scope: true
4
+ controller: ($scope, $attrs, $parse, $element, $http) ->
5
+ if action_attr = $attrs.action
6
+ action_names = $parse(action_attr)($scope)
7
+ throw "Invalid action path: '#{action_attr}'" unless action_names
8
+ action_names = action_names.split('/') if _.isString(action_names)
9
+ create = (action) ->
10
+ action.create_action_path(action_names, $scope, $element).then (act) -> act.invoke() if $attrs.invoke
11
+
12
+ sc = $scope
13
+ if sc.action
14
+ sc = sc.$parent until sc.action instanceof E2Actions.action
15
+ create(sc.action)
16
+ else
17
+ bootstrap_action_off = $scope.$on "bootstrap_action", (evt, action) ->
18
+ bootstrap_action_off()
19
+ create(action)
20
+ else
21
+ $http.get("api/meta").then (mresponse) -> $scope.$broadcast "bootstrap_action",
22
+ $scope.action = new E2Actions.default_action(mresponse.data, $scope, null, $element, action_resource: 'api')
23
+
24
+ .factory 'E2Actions', (E2, $http, $timeout, $e2Modal, $injector, $compile, $templateCache, $q, localStorageService, $route, $window, $rootScope, $location) ->
25
+ action: class Action
26
+ constructor: (response, scope, parent, element, action_info) ->
27
+ @find_action_info = (name, raise = true) ->
28
+ act = response.actions[name]
29
+ throw "Undefined action '#{name}' for action #{@action_info().name} (under #{@parent().action_info().action_resource})" if raise && !act
30
+ act
31
+
32
+ _.each response.actions, (act, nm) -> act.name = nm
33
+ @meta = response.meta
34
+ (@scope = -> scope) if scope
35
+ @element = -> element
36
+ @action_info = -> action_info
37
+ @parent = -> parent
38
+
39
+ if @meta.panel
40
+ @default_action_name = _(response.actions).find((o) -> o.default)?.name
41
+
42
+ scope = scope.$new(true)
43
+ scope.$on "$destroy", (e) => @destroy(e)
44
+ scope.action = @
45
+
46
+ act = parent
47
+ act = act.parent() while act && !act.meta.panel
48
+ unless act # no modal for top level panels
49
+ @meta.panel.modal_action = false
50
+ if @meta.menus.panel_menu
51
+ _.remove(@meta.menus.panel_menu.entries, (m) -> m.name == 'cancel')
52
+
53
+ @initialize()
54
+
55
+ initialize: ->
56
+ @process_static_meta()
57
+ @process_meta()
58
+ console.log "CREATE #{@action_info().action_resource}"
59
+
60
+ process_static_meta: ->
61
+ if @meta.menus
62
+ _.each @meta.menus, (menu, name) => E2.process_menu @, name
63
+ process_meta: ->
64
+
65
+ handle_error: (err, action_info, element, create) ->
66
+ if err.status == 401
67
+ if action_info.access
68
+ $rootScope.$broadcast "relogin", element?, create
69
+ else
70
+ $e2Modal.error("#{err.status}: #{err.data.message}", err.data.cause || err.data.message)
71
+ $q.reject(err)
72
+
73
+ perform_invoke: (params) ->
74
+ info = @action_info()
75
+ get_invoke = if info.invokable
76
+ params.initial = true if !@action_invoked && params && info.method == 'get'
77
+ $http[info.method](info.action_resource, if info.method == 'post' then params else (params: params))
78
+ else $q.when(data: (response: {}))
79
+
80
+ get_invoke.then (response) =>
81
+ E2.merge(@meta, response.data.meta)
82
+ @process_meta()
83
+ _.assign(@, response.data.response)
84
+ if @meta.response
85
+ E2.merge(@, @meta.response)
86
+ delete @meta.response
87
+ @arguments = _.keys(response.data.response)
88
+ unless @meta.panel # persistent action
89
+ prnt = @parent()
90
+ throw "Attempted parent merge for root action: #{info.name}" unless prnt
91
+ E2.merge(prnt.meta, @meta)
92
+ _.assign(prnt, response.data.response)
93
+
94
+ # promise = if @meta.panel && !@action_invoked
95
+ # $q.when(promise) # .then -> response.data
96
+ if @meta.panel && !@action_invoked
97
+ @action_invoked = true
98
+ @panel_render()
99
+ # else $q.when()
100
+ ,
101
+ (err) =>
102
+ @parent().action_pending = false
103
+ @handle_error(err, info, @element())
104
+
105
+ create_action: (name, sc, el) ->
106
+ info = @find_action_info(name)
107
+ info.action_resource = "#{@action_info().action_resource}/#{info.name}"
108
+ get_meta = if !info.terminal || info.meta
109
+ $http.get("#{info.action_resource}/meta", cache: true).then (response) =>
110
+ if info.recheck_access
111
+ $http.get("#{info.action_resource}/meta", params: (access: true, parent_id: @current_id)).then (aresponse) ->
112
+ response.data.actions[k].access = v for k, v of aresponse.data
113
+ response
114
+ else response # $q.when ^
115
+ else $q.when(data: (meta: {}, actions: []))
116
+ E2A = $injector.get("E2Actions")
117
+ get_meta.then (mresponse) => new (E2A[info.meta_type] ? E2A.default_action)(mresponse.data, sc, @, el, info)
118
+ ,
119
+ (err) => @handle_error(err, info, el)
120
+
121
+ invoke_action: (name, arg) ->
122
+ @create_action(name, @scope()).then (act) -> act.invoke(arg)
123
+
124
+ create_action_path: (action_names, sc, elem) ->
125
+ last_name = action_names.pop()
126
+ _.reduce(action_names, ((pr, nm) -> pr.then (act) -> act.create_action(nm)), $q.when(@)).then (act) ->
127
+ act.create_action(last_name, sc, elem).then (act) -> sc.action = act
128
+
129
+ pre_invoke: ->
130
+ @parent().action_pending = true
131
+ # @parent().parent().action_pending = true if @parent().parent()
132
+ post_invoke: ->
133
+ delete @parent().action_pending # = false
134
+ # @parent().parent().action_pending = false if @parent().parent()
135
+ invoke: ->
136
+ args = arguments
137
+ @pre_invoke(args...)
138
+ @perform_invoke(args...).then (response) =>
139
+ @post_invoke(args...)
140
+ @
141
+
142
+ save_state: () ->
143
+ _.each @meta.state, (s) => localStorageService.set("#{@action_info().action_resource}/#{s}", @[s])
144
+ load_state: () ->
145
+ _.each @meta.state, (s) => E2.merge(@[s], localStorageService.get("#{@action_info().action_resource}/#{s}"))
146
+
147
+ destroy: (e) ->
148
+ console.log "DESTROY #{@action_info().action_resource}"
149
+
150
+ panel_render: ->
151
+ if @meta.panel.modal_action
152
+ if @element()
153
+ E2.fetch_panel(@meta.panel, true).then (template) =>
154
+ @panel_show?()
155
+ compiled = $compile(template)(@scope())
156
+ @element().empty().append(compiled.contents())
157
+ @panel_shown?()
158
+
159
+ else
160
+ $e2Modal.show(@)
161
+ else
162
+ @panel_scope?().$destroy()
163
+ act = @
164
+ act = act.parent() until act.element()
165
+ element = act.element() # @element()
166
+ is_modal = $e2Modal.is_modal() && !@element()
167
+ E2.fetch_panel(@meta.panel, is_modal).then (template) =>
168
+ @panel_show?()
169
+ # @panel_scope().$destroy()
170
+ # @panel_scope = -> @scope().$new(false)
171
+ # @scope().$broadcast "$destroy"
172
+ @panel_scope = -> @scope().$new()
173
+ compiled = $compile(template)(@panel_scope())
174
+ if is_modal
175
+ element.empty().append(compiled.contents())
176
+ else
177
+ # element.empty().$destroy()
178
+ element.empty().append(compiled)
179
+ @panel_shown?()
180
+
181
+ # panel_refresh: ->
182
+
183
+ panel_hidden: ->
184
+ @scope().$destroy()
185
+
186
+ panel_close: ->
187
+ if @meta.panel.modal_action
188
+ @modal_hide()
189
+ else
190
+ # @parent().panel_refresh()
191
+ @panel_hide?()
192
+ @panel_hidden()
193
+ @scope().$destroy()
194
+ @parent().action_invoked = false
195
+ @parent().invoke()
196
+
197
+ panel_menu_cancel: ->
198
+ @panel_close()
199
+
200
+ default_action: class DefaultAction extends Action
201
+ initialize: ->
202
+ super()
203
+ # console.log "DEFAULT ACTION: #{@action_info().action_resource}"
204
+
205
+ inspect: class InspectAction extends Action
206
+ initialize: ->
207
+ super()
208
+ @tree = actions: [name: 'api', number: 0, access: true]
209
+ @invoke_action('models')
210
+
211
+ open: (stack, node, collapsed, expand) ->
212
+ tree = @tree
213
+ path = []
214
+ _.each stack, (index) -> # fold ?
215
+ tree = tree.actions[index]
216
+ path.push tree.name
217
+
218
+ # if !expand
219
+ @number = tree.number
220
+ if _.size(stack) > 1
221
+ $http.get("#{_.dropRight(path).join('/')}/meta").then (response) => @action_json = _.toArray(response.data.actions)[_.last(stack)]
222
+ else
223
+ @action_json = {}
224
+
225
+ # if expand && collapsed
226
+ $http.get("#{path.join('/')}/meta").then (response) =>
227
+ get_meta = if tree.recheck_access
228
+ $http.get("#{path.join('/')}/meta", params: (access: true)).then (aresponse) ->
229
+ response.data.actions[k].access = v for k, v of aresponse.data
230
+ response
231
+ else $q.when(response)
232
+
233
+ get_meta.then (response) =>
234
+ _.each response.data.actions, (act, nm) -> act.name = nm
235
+ tree.actions ?= _.toArray(response.data.actions)
236
+ @meta_json = response.data.meta
237
+ @action_state = if @meta_json.state then _.zipObject(@meta_json.state.map (k) -> [k, localStorageService.get("#{path}/#{k}")]) else {}
238
+ ,
239
+ (err) =>
240
+ delete @meta_json
241
+ @handle_error(err, access: false)
242
+
243
+ has_assoc: (model) ->
244
+ _.size(model.assoc) > 0
245
+
246
+ menu: class MenuAction extends Action
247
+ process_static_meta: ->
248
+
249
+ initialize: ->
250
+ super()
251
+ $route.load_routes = =>
252
+ @invoke().then =>
253
+ _.each _.keys($route.routes), (k) -> delete $route.routes[k]
254
+ menu = @meta.menus.menu
255
+ $route.routes[null] = reloadOnSearch: true, redirectTo: '/' + (menu.properties.default ? menu.entries[0].name)
256
+ @register(menu.entries)
257
+ $route.reload() # $location.path('')
258
+ @scope().routes = menu.entries
259
+ out = if _.size(menu.entries) == 0 then angular.element("<div></div>") else $compile(@traverse(menu.entries))(@scope())
260
+ @element().replaceWith(out)
261
+ @element = -> out
262
+ $route.load_routes()
263
+
264
+ register: (routes) ->
265
+ _.each routes, (route) =>
266
+ if route.menu then @register(route.menu.entries) else
267
+ name = '/' + route.name
268
+ route.href = '#' + route.name
269
+ $templateCache.put(route.name, "<div e2-action='' action=\"'#{route.bootstrap}/#{route.name}'\" invoke='true'></div>") if route.bootstrap
270
+ $route.routes[name] =
271
+ reloadOnSearch: true
272
+ templateUrl: route.name
273
+ originalPath: name
274
+ regexp: new RegExp("^#{name}$")
275
+ keys: []
276
+
277
+ $route.routes[name + '/'] =
278
+ redirectTo: name
279
+ originalPath: name + '/'
280
+ regexp: new RegExp("^#{name}/$")
281
+ keys: []
282
+
283
+ traverse: (routes) ->
284
+ menu_tmpl = _.template("<li><a href='{{href}}'>{{icon}}{{aicon}} {{loc}}</a></li>")
285
+ menu_sub_tmpl = _.template("<li e2-drop-down='{{dropdown}}'><a href='javascript://'>{{icon}}{{aicon}}{{loc}}<span class='caret'></span></a></li>")
286
+ out = routes.map (route, i) ->
287
+ if route.menu
288
+ menu_sub_tmpl
289
+ icon: route.menu.icon && E2.icon(route.menu.icon) || ""
290
+ aicon: route.menu.aicon && E2.aicon(route.menu.aicon) || ""
291
+ loc: route.menu.loc
292
+ dropdown: "routes[#{i}].menu.entries"
293
+ else
294
+ menu_tmpl
295
+ href: route.href
296
+ loc: route.loc
297
+ icon: route.icon && E2.icon(route.icon) || ''
298
+ aicon: route.aicon && E2.aicon(route.aicon) || ''
299
+ out.join('')
300
+
301
+ # dummy: class DummyAction extends Action
302
+
303
+ list: class ListAction extends Action
304
+ initialize: ->
305
+ super()
306
+ @query = page: 0, asc: true, search: {} #, search_tab: 0
307
+ @ui_state = {}
308
+ @load_state()
309
+
310
+ delete @query.order unless _.includes(@meta.fields, @query.order)
311
+ _.each @query.search, ((sv, sn) => delete @query.search[sn] unless _.includes(@meta.search_fields, sn))
312
+
313
+ _.each @meta.info, (info, name) =>
314
+ if info.remote_onchange
315
+ @scope().$watch (=> @query.search?[name]), (n) => if n?
316
+ params = value: @query.search[name]
317
+ params.record = @query.search if info.remote_onchange_record
318
+ @invoke_action(info.remote_onchange, params)
319
+
320
+ if info.onchange
321
+ @scope().$watch (=> @query.search?[name]), (n) => if n?
322
+ @scope().$eval(info.onchange)
323
+
324
+ # $window.addEventListener 'beforeunload', (e, v) => @save_state()
325
+
326
+ destroy: ->
327
+ @save_state()
328
+ super()
329
+
330
+ process_meta: ->
331
+ super()
332
+ meta = @meta
333
+ meta.fields = meta.fields.filter((f) => !meta.info[f].hidden) if meta.fields
334
+
335
+ # confirm_create, view, confirm_modify, confirm_delete, assocs - implicit
336
+
337
+ menu_search_toggle: ->
338
+ @ui_state.search_active = !@ui_state.search_active
339
+ @save_state() unless @ui_state.search_active
340
+
341
+ menu_refresh: ->
342
+ @invoke(refresh: true)
343
+
344
+ menu_default_order: ->
345
+ delete @query.order
346
+ @invoke()
347
+
348
+ menu_select_toggle: ->
349
+ if @selection then delete @selection else @selection = {}
350
+ @scope().$broadcast 'render_table'
351
+
352
+ menu_show_meta: ->
353
+ $e2Modal.show
354
+ the_meta: @meta
355
+ meta: panel: (panel_template: "close_m", template_string: "<pre>{{action.the_meta | json}}</pre>", title: "Meta", class: "modal-huge")
356
+
357
+ # show_assoc: (index, assoc) ->
358
+ # # parent_id = E2.id_for(@entries[index], @meta)
359
+ # # @create_action(assoc, @scope(), null, parent_id).then (action) =>
360
+ # # action.query.parent_id = parent_id # E2.id_for(@entries[index], @meta)
361
+ # # action.invoke()
362
+ # @current_id = E2.id_for(@entries[index], @meta)
363
+ # @invoke_action(assoc)
364
+
365
+ list_cell: (e, f) ->
366
+ E2.render_field(e, f, @meta)
367
+
368
+ invoke: (arg = {}) ->
369
+ @save_state()
370
+ query = _.cloneDeep(@query)
371
+ delete query.search if _.isEmpty(E2.compact(query.search))
372
+ _.merge(query, arg)
373
+ super(query).then =>
374
+ @ui = _.pick @query, ['order', 'asc', 'page']
375
+ @scope().$broadcast 'render_table'
376
+
377
+ load_new: ->
378
+ @query.page = 0
379
+ @invoke()
380
+
381
+ order: (col) ->
382
+ @query.asc = if @query.order == col then !@query.asc else true
383
+ @query.order = col
384
+ @load_new()
385
+
386
+ prev_active: -> @query.page > 0
387
+ prev: ->
388
+ @query.page = Math.max(0, @query.page - @meta.config.per_page)
389
+ @invoke()
390
+
391
+ next_active: -> @entries.length == @meta.config.per_page
392
+ next: ->
393
+ @query.page += @meta.config.per_page # min & count
394
+ @invoke()
395
+
396
+ page_info: ->
397
+ page = @ui.page / @meta.config.per_page + 1
398
+ if @count then "#{page} / #{Math.ceil(@count / @meta.config.per_page)} (#{@count})" else page || ''
399
+
400
+ search_reset: ->
401
+ E2.clean(@query.search)
402
+ @scope().$broadcast "search_reset"
403
+ @load_new()
404
+
405
+ search_live: (f) ->
406
+ @load_new() if @meta.info[f].search_live
407
+
408
+ selected_class: (index) ->
409
+ (entry = @entries[index]) && @selection && @selection[E2.id_for(entry, @meta)] && 'info'
410
+
411
+ select: (index, ev) ->
412
+ if ev.target.nodeName == "TD"
413
+ if @selection
414
+ rec = @entries[index]
415
+ id = E2.id_for(rec, @meta)
416
+ if @selection[id] then delete @selection[id] else @selection[id] = rec
417
+
418
+ selected_size: ->
419
+ _.size(@selection)
420
+
421
+ bulk_delete: class BulkDeleteAction extends Action
422
+ invoke: ->
423
+ super(ids: [_.keys(@parent().parent().selection)]).then =>
424
+ @parent().parent().selection = {}
425
+
426
+ view: class ViewAction extends Action
427
+ view_cell: (e, f) ->
428
+ E2.render_field(e, f, @meta)
429
+
430
+ form_base_action: class FormBaseAction extends Action
431
+ initialize: ->
432
+ super()
433
+ _.each @meta.info, (info, name) =>
434
+ if info.remote_onchange
435
+ @scope().$watch (=> @record?[name]), (n) => if n? #if typeof(n) != "undefined"
436
+ params = value: @record[name]
437
+ params.record = @record if info.remote_onchange_record
438
+ @invoke_action(info.remote_onchange, params)
439
+
440
+ if info.onchange
441
+ @scope().$watch (=> @record?[name]), (n) => if n?
442
+ @scope().$eval(info.onchange)
443
+
444
+ if @meta.tabs
445
+ @scope().$watch "action.activeTab", (tab) => if tab? # && tab >= 0
446
+ @panel_shown()
447
+
448
+ @["panel_menu_#{@default_action_name}"] = -> @panel_menu_default_action()
449
+ @scope().$on "return_pressed", (e) => @panel_menu_default_action()
450
+
451
+ post_invoke: (args) ->
452
+ super()
453
+ _.each @meta.info, (info, name) =>
454
+ if _.isString(@record[name]) && !info.dont_strip
455
+ @record[name] = @record[name].trim()
456
+
457
+ panel_menu_default_action: ->
458
+ _.each @meta.info, (v, n) =>
459
+ @record[n] = null if @record[n] is undefined
460
+ params = record: @record
461
+ params.parent_id ?= @parent().query?.parent_id # and StarToManyList ?
462
+ @invoke_action(@default_action_name, params).then =>
463
+ dfd = $q.defer()
464
+ if @errors
465
+ if @meta.tabs
466
+ [i, first, curr] = [0, null, false]
467
+ for tab in @meta.tabs
468
+ if _(tab.fields).find((f) => @errors[f])
469
+ first = i if not first?
470
+ act = true if @activeTab == i
471
+ i++
472
+ @activeTab = first unless act
473
+
474
+ if @activeTab?
475
+ field = _(@meta.tabs[@activeTab].fields).find((f) => @errors[f])
476
+ # console.log field undefined ?
477
+ else
478
+ @activeTab = 0
479
+ @alert = @errors
480
+ else
481
+ field = _(@meta.fields).find((f) => @errors[f])
482
+ @alert = @errors if (!field || !@meta.info[field] || @meta.info[field].hidden) # ?
483
+ $timeout => @scope().$broadcast("focus_field", field)
484
+ #e.scope.$eval(meta.execute) if meta.execute # ?
485
+ dfd.reject(@errors)
486
+ else
487
+ @panel_close()
488
+ dfd.resolve(@record) # $q.when(true) ?
489
+ dfd.promise
490
+
491
+ panel_shown: ->
492
+ field = if @meta.tabs
493
+ tab = @meta.tabs[@activeTab]
494
+ if @errors
495
+ _(tab.fields).find((f) => @errors[f]) || _(tab.fields).find((f) => !@meta.info[f].hidden)
496
+ else
497
+ tab ?= @meta.tabs[0]
498
+ _(tab.fields).find((f) => !@meta.info[f].hidden && !@meta.info[f].disabled)
499
+ else
500
+ _(@meta.fields).find((f) => !@meta.info[f].hidden && !@meta.info[f].disabled)
501
+ $timeout (=> @scope().$broadcast("focus_field", field)), 300 # hack, on shown ?
502
+
503
+ infra: class InfraAction extends Action
504
+ initialize: ->
505
+ super()
506
+ @scope().$on "relogin", (evt, reload_routes, create) =>
507
+ if @user
508
+ @invoke_action('login_form').then (act) =>
509
+ act.record = name: @user.name
510
+ act.meta.info.name.disabled = true
511
+ act.dont_reload_routes = !reload_routes # true
512
+ else
513
+ @invoke().then => @set_access(true, true)
514
+
515
+ set_access: (login, load_routes) ->
516
+ @find_action_info('logout_form').access = login
517
+ @find_action_info('inspect_modal').access = login
518
+ @find_action_info('login_form').access = !login
519
+ $route.load_routes() if load_routes
520
+
521
+ login_form: class LoginFormAction extends FormBaseAction
522
+ panel_menu_default_action: ->
523
+ super().then =>
524
+ @parent().user = @user
525
+ @parent().set_access(true, !@dont_reload_routes)
526
+
527
+ logout_form: class LogoutForm extends Action
528
+ panel_menu_logout: ->
529
+ @invoke_action('logout').then =>
530
+ @parent().user = null
531
+ @parent().set_access(false, true)
532
+ @panel_close()
533
+
534
+ form: class FormAction extends FormBaseAction
535
+ panel_menu_default_action: ->
536
+ super().then => @parent().invoke()
537
+
538
+ create: class CreateAction extends FormAction
539
+ invoke: (args) ->
540
+ if parent_id = @parent().query.parent_id
541
+ args ?= {}
542
+ args.parent_id = parent_id
543
+ super(args)
544
+
545
+ modify: class ModifyAction extends FormAction
546
+ # invoke: (args) ->
547
+ # super(args).then =>
548
+ # _.each @meta.primary_fields, (f) => @meta.info[f].disabled = true
549
+
550
+ on_change: class OnChangeAction extends Action
551
+ post_invoke: ->
552
+ super()
553
+ @parent().scope().$eval(@meta.execute) if @meta.execute
554
+
555
+ confirm: class ConfirmAction extends Action
556
+ panel_menu_approve: ->
557
+ @initial_arguments ?= @arguments
558
+ @invoke_action(@default_action_name, _.pick(@, @initial_arguments)).then (act) =>
559
+ unless @errors
560
+ @parent().invoke()
561
+ @panel_close()
562
+
563
+ decode_action: class DecodeAction extends Action
564
+ initialize: ->
565
+ super()
566
+ @decode_field = @scope().f
567
+ @dinfo = @parentp().meta.info[@decode_field]
568
+ @scope().$on "search_reset", => @clean()
569
+
570
+ if_fk_values: (f) ->
571
+ fk_values = @dinfo.fields.map((f) => @record()[f])
572
+ f(fk_values) if _(fk_values).every((f) -> f?) # null_value
573
+
574
+ record: ->
575
+ @parentp().query?.search || @parentp().record
576
+
577
+ clear_record: ->
578
+ _.each @dinfo.fields, (fk) => @record()[fk] = null # null_value
579
+
580
+ reset: ->
581
+ @clean()
582
+ @parentp().search_live?(@decode_field)
583
+
584
+ decode_description: (entry) ->
585
+ fields = @meta.decode_fields ? @meta.fields
586
+ fields.map((f) => E2.render_field(entry, f, @meta)).join(@meta.separator)
587
+
588
+ parentp: ->
589
+ @parent().parent()
590
+
591
+ decode_list: class DecodeListAction extends DecodeAction
592
+ initialize: ->
593
+ super()
594
+ @multiple = @dinfo.render.multiple
595
+ @clear_selected()
596
+ @if_fk_values (fk_values) =>
597
+ @selected = if @multiple then E2.transpose(fk_values).map(E2.join_keys) else E2.join_keys(fk_values)
598
+ @invoke()
599
+
600
+ clear_selected: -> @selected = if @multiple then [] else null # no need to null complex keys
601
+
602
+ post_invoke: (args) ->
603
+ super()
604
+ @values = @entries.map (e) => id: E2.id_for(e, @meta), value: @decode_description(e)
605
+ delete @entries
606
+
607
+ change: ->
608
+ record = @record()
609
+ if @multiple
610
+ if @selected.length > 0
611
+ _.each @dinfo.fields, (fk) -> record[fk] = []
612
+ _.each @selected, (sel) =>
613
+ _(@dinfo.fields).zip(E2.split_keys(sel)).each(([fk, k]) => record[fk].push E2.parse_entry(k, @parentp().meta.info[fk])).value
614
+ else @clear_record()
615
+ else
616
+ if @selected
617
+ _(@dinfo.fields).zip(E2.split_keys(@selected)).each(([fk, k]) => record[fk] = E2.parse_entry(k, @parentp().meta.info[fk])).value
618
+ else @clear_record()
619
+
620
+ @parentp().search_live?(@decode_field)
621
+
622
+ clean: ->
623
+ @clear_selected()
624
+ @clear_record()
625
+
626
+ decode_entry: class DecodeEntryAction extends DecodeAction
627
+ initialize: ->
628
+ super()
629
+ @multiple = @dinfo.render.multiple
630
+ @if_fk_values (fk_values) =>
631
+ @invoke_decode (if @multiple then E2.transpose(fk_values) else [fk_values])
632
+
633
+ @scope().$on "picked", (ev, sel, sel_meta) =>
634
+ ev.stopPropagation()
635
+ record = @record()
636
+ if @multiple
637
+ _.each @dinfo.fields, (fk) => record[fk] = []
638
+ _.each sel, (rec, ids) =>
639
+ _(@dinfo.fields).zip(E2.split_keys(ids)).each(([k, v]) => record[k].push E2.parse_entry(v, @parentp().meta.info[k])).value
640
+ @invoke_decode _.values(sel)
641
+ delete @decode if _.isEmpty(sel)
642
+ else
643
+ [ids, rec] = _(sel).toPairs().head()
644
+ _(@dinfo.fields).zip(E2.split_keys(ids)).each(([k, v]) => record[k] = E2.parse_entry(v, @parentp().meta.info[k])).value
645
+ @invoke_decode [rec]
646
+ @parentp().search_live?(@decode_field)
647
+
648
+ invoke_decode: (recs, f) ->
649
+ if @multiple && _.size(recs) > @meta.show_max_selected
650
+ @decode = "#{_.size(recs)} #{@meta.decode_selected}"
651
+ else
652
+ decode_descriptions = (recs) => @decode = recs.map((fields) => @decode_description(fields)).join(' | ')
653
+ recs = recs.map (r) => if _.isArray(r) then E2.from_id(r, @meta) else r
654
+ if _(recs).every((r) => _(@meta.fields).every((f) -> r[f]?)) then decode_descriptions(recs) else
655
+ @invoke(ids: [recs.map((r) => @meta.primary_fields.map (k) -> r[k])]).then => decode_descriptions(@entries)
656
+
657
+ open: ->
658
+ fk_values = @dinfo.fields.map((f) => @record()[f]).filter((f) -> f?)
659
+ @create_action('list', @scope()).then (action) =>
660
+ if @multiple
661
+ action.selection = E2.transpose(fk_values).reduce(((rec, keys) => rec[E2.join_keys(keys)] = E2.from_id(keys, @meta); rec), {})
662
+ else
663
+ action.selection[E2.join_keys(fk_values)] = E2.from_id(fk_values, @meta) if fk_values.length > 0
664
+ action.invoke()
665
+
666
+ clean: ->
667
+ delete @decode
668
+ @clear_record()
669
+
670
+ typeahead: class TypeAheadAction extends DecodeAction
671
+ initialize: ->
672
+ super()
673
+ @if_fk_values (fk_values) =>
674
+ @invoke(id: E2.join_keys(fk_values)).then =>
675
+ if @entry
676
+ @decode = id: E2.id_for(@entry, @meta), value: @decode_description(@entry)
677
+
678
+ @scope().$on "$typeahead.select", (e, v, index) =>
679
+ e.stopPropagation()
680
+ _(@dinfo.fields).zip(E2.split_keys(@values[index].id)).each(([fk, k]) => @record()[fk] = E2.parse_entry(k, @parentp().meta.info[fk])).value
681
+ @parentp().search_live?(@decode_field)
682
+
683
+ @scope().$watch "action.decode", (e) => if e?
684
+ @reset() if e.length == 0
685
+
686
+ load: (value) ->
687
+ if value? && value.length > 0 && @key_pressed # check again after strap updates ?
688
+ @invoke(query: value).then =>
689
+ if @entries # ?
690
+ @values = @entries.map (e) => id: E2.id_for(e, @meta), value: @decode_description(e)
691
+ delete @entries
692
+ @values
693
+
694
+ clean: ->
695
+ delete @decode
696
+ @clear_record()
697
+
698
+ many_to_one_list: class ManyToOneListAction extends ListAction
699
+ initialize: ->
700
+ super()
701
+ @selection = {}
702
+
703
+ select: (index, ev) ->
704
+ if ev.target.nodeName == "TD"
705
+ @selection = {} unless @parent().multiple
706
+ super(index, ev)
707
+ unless @parent().multiple
708
+ @scope().$emit "picked", @selection, @meta
709
+ @panel_close()
710
+
711
+ panel_menu_choose: (e) ->
712
+ # if _.size(@selection) > 0
713
+ @scope().$emit "picked", @selection, @meta
714
+ @panel_close()
715
+
716
+ star_to_many_list: class StarToManyList extends ListAction
717
+ initialize: ->
718
+ super()
719
+ @query.parent_id = @parent().current_id
720
+
721
+ # link_list: implicit
722
+ item_menu_confirm_unlink: (index) ->
723
+ @invoke_action('confirm_unlink', id: E2.id_for(@entries[index], @meta), parent_id: @query.parent_id)
724
+
725
+ star_to_many_bulk_unlink: class StarToManyBulkUnlinkAction extends Action
726
+ invoke: ->
727
+ parent = @parent().parent()
728
+ super(ids: [_.keys(parent.selection)], parent_id: parent.query.parent_id).then =>
729
+ parent.selection = {}
730
+
731
+ star_to_many_link_list: class StarToManyLinkList extends ListAction
732
+ initialize: ->
733
+ super()
734
+ @query.negate = true
735
+ @query.parent_id = @parent().query.parent_id
736
+ @selection = {}
737
+
738
+ panel_menu_link: ->
739
+ selection = _.keys(@selection)
740
+ if selection.length > 0
741
+ @invoke_action('link', parent_id: @query.parent_id, ids: selection).then (act) =>
742
+ unless @errors
743
+ @parent().invoke()
744
+ @panel_close()
745
+
746
+ star_to_many_field: class StarToManyField extends ListAction
747
+ initialize: ->
748
+ super()
749
+ @query.parent_id = E2.id_for(@parent().record, @parent().meta)
750
+ links = @parent().record[@scope().$parent.f]
751
+ @links = links ? (linked: [], unlinked: [])
752
+ @invoke()
753
+
754
+ invoke: ->
755
+ @query.unlinked = [@links.unlinked]
756
+ @query.linked = [@links.linked]
757
+ super()
758
+
759
+ sync_record: ->
760
+ @parent().record[@scope().$parent.f] = @links
761
+
762
+ star_to_many_field_link_list: class StarToManyFieldLinkList extends ListAction
763
+ initialize: ->
764
+ super()
765
+ @query.negate = true
766
+ @query.parent_id = @parent().query.parent_id
767
+ @selection = {}
768
+
769
+ invoke: ->
770
+ @query.unlinked = [@parent().links.unlinked]
771
+ @query.linked = [@parent().links.linked]
772
+ super()
773
+
774
+ panel_menu_link: ->
775
+ if @selected_size() > 0
776
+ _.each @selection, (v, k) =>
777
+ id = k
778
+ if _.includes(@parent().links.unlinked, id) then _.pull(@parent().links.unlinked, id) else @parent().links.linked.push id
779
+ @parent().invoke()
780
+ @parent().sync_record()
781
+ @panel_close()
782
+
783
+ star_to_many_field_unlink: class StarToManyFieldUnlink extends Action
784
+ invoke: (arg) ->
785
+ id = arg.id
786
+ pparent = @parent().parent()
787
+ if _.includes(pparent.links.linked, id) then _.pull(pparent.links.linked, id) else pparent.links.unlinked.push id
788
+ pparent.sync_record()
789
+
790
+ file_store: class FileStoreAction extends Action
791
+ initialize: ->
792
+ super()
793
+ @progress = 0
794
+ id = E2.id_for(@parent().record, @parent().meta)
795
+ if id.length > 0
796
+ @invoke(owner: id).then => @sync_record()
797
+ else
798
+ @files = []
799
+ @sync_record()
800
+
801
+ sync_record: ->
802
+ @parent().record[@scope().f] = @files
803
+
804
+ select: (files) ->
805
+ _.each files, (file) =>
806
+ upload = $injector.get('Upload').upload url: "#{@action_info().action_resource}/upload", file: file
807
+ upload.progress (e) =>
808
+ @progress = parseInt(100.0 * e.loaded / e.total)
809
+ # @parent().action_pending = true
810
+ upload.success (data, status, headers, config) =>
811
+ @files.push mime: file.type, name: file.name, rackname: data.response.rackname, id: data.response.id
812
+ @message = "Wysłano, #{file.name}"
813
+ # @parent().action_pending = false
814
+ @sync_record()
815
+
816
+ delete_file: (file) ->
817
+ @scope().$broadcast 'confirm_delete',
818
+ confirm: =>
819
+ @sync_record()
820
+ file.deleted = true
821
+ @scope().$broadcast 'confirm_delete_close'
822
+
823
+ show_file: (file) ->
824
+ @file = file
825
+ @scope().$broadcast 'show_file', file: file
826
+
827
+ blob_store: class BlobStore extends Action
828
+ initialize: ->
829
+ super()
830
+ @record_id = E2.id_for(@parent().record, @parent().meta)
831
+ @progress = 0
832
+ if @record_id.length > 0
833
+ @invoke(owner: @record_id) # .then => @sync_record()
834
+ else
835
+ # @file = {}
836
+ # @sync_record()
837
+
838
+ sync_record: ->
839
+ @parent().record[@scope().f] = @file
840
+
841
+ select: (files) ->
842
+ _.each files, (file) =>
843
+ upload = $injector.get('Upload').upload url: "#{@action_info().action_resource}/upload", file: file
844
+ upload.progress (e) =>
845
+ @progress = parseInt(100.0 * e.loaded / e.total)
846
+ upload.success (data, status, headers, config) =>
847
+ @file = mime: file.type, name: file.name, rackname: data.response.rackname, id: data.response.id
848
+ @message = "Wysłano, #{file.name}"
849
+ @sync_record()