flipper-ui 0.2.0.beta1

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 (80) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +19 -0
  3. data/.rspec +1 -0
  4. data/Gemfile +17 -0
  5. data/Guardfile +26 -0
  6. data/LICENSE +22 -0
  7. data/README.md +101 -0
  8. data/Rakefile +7 -0
  9. data/examples/basic.ru +44 -0
  10. data/examples/flipper.html +14 -0
  11. data/examples/flipper.png +0 -0
  12. data/flipper-ui.gemspec +21 -0
  13. data/lib/flipper-ui.rb +1 -0
  14. data/lib/flipper/ui.rb +23 -0
  15. data/lib/flipper/ui/action.rb +172 -0
  16. data/lib/flipper/ui/action_collection.rb +20 -0
  17. data/lib/flipper/ui/actions/features.rb +21 -0
  18. data/lib/flipper/ui/actions/file.rb +17 -0
  19. data/lib/flipper/ui/actions/gate.rb +143 -0
  20. data/lib/flipper/ui/actions/index.rb +17 -0
  21. data/lib/flipper/ui/assets/javascripts/application.coffee +305 -0
  22. data/lib/flipper/ui/assets/javascripts/spine/ajax.coffee +223 -0
  23. data/lib/flipper/ui/assets/javascripts/spine/list.coffee +43 -0
  24. data/lib/flipper/ui/assets/javascripts/spine/local.coffee +16 -0
  25. data/lib/flipper/ui/assets/javascripts/spine/manager.coffee +83 -0
  26. data/lib/flipper/ui/assets/javascripts/spine/relation.coffee +148 -0
  27. data/lib/flipper/ui/assets/javascripts/spine/route.coffee +146 -0
  28. data/lib/flipper/ui/assets/javascripts/spine/spine.coffee +542 -0
  29. data/lib/flipper/ui/assets/javascripts/spine/version +1 -0
  30. data/lib/flipper/ui/assets/stylesheets/application.scss +237 -0
  31. data/lib/flipper/ui/decorators/feature.rb +37 -0
  32. data/lib/flipper/ui/decorators/gate.rb +36 -0
  33. data/lib/flipper/ui/error.rb +10 -0
  34. data/lib/flipper/ui/eruby.rb +11 -0
  35. data/lib/flipper/ui/middleware.rb +66 -0
  36. data/lib/flipper/ui/public/css/application.css +183 -0
  37. data/lib/flipper/ui/public/css/images/animated-overlay.gif +0 -0
  38. data/lib/flipper/ui/public/css/images/ui-bg_diagonals-thick_18_b81900_40x40.png +0 -0
  39. data/lib/flipper/ui/public/css/images/ui-bg_diagonals-thick_20_666666_40x40.png +0 -0
  40. data/lib/flipper/ui/public/css/images/ui-bg_flat_10_000000_40x100.png +0 -0
  41. data/lib/flipper/ui/public/css/images/ui-bg_glass_100_f6f6f6_1x400.png +0 -0
  42. data/lib/flipper/ui/public/css/images/ui-bg_glass_100_fdf5ce_1x400.png +0 -0
  43. data/lib/flipper/ui/public/css/images/ui-bg_glass_65_ffffff_1x400.png +0 -0
  44. data/lib/flipper/ui/public/css/images/ui-bg_gloss-wave_35_f6a828_500x100.png +0 -0
  45. data/lib/flipper/ui/public/css/images/ui-bg_highlight-soft_100_eeeeee_1x100.png +0 -0
  46. data/lib/flipper/ui/public/css/images/ui-bg_highlight-soft_75_ffe45c_1x100.png +0 -0
  47. data/lib/flipper/ui/public/css/images/ui-icons_222222_256x240.png +0 -0
  48. data/lib/flipper/ui/public/css/images/ui-icons_228ef1_256x240.png +0 -0
  49. data/lib/flipper/ui/public/css/images/ui-icons_ef8c08_256x240.png +0 -0
  50. data/lib/flipper/ui/public/css/images/ui-icons_ffd27a_256x240.png +0 -0
  51. data/lib/flipper/ui/public/css/images/ui-icons_ffffff_256x240.png +0 -0
  52. data/lib/flipper/ui/public/css/jquery-ui-1.10.3.slider.min.css +5 -0
  53. data/lib/flipper/ui/public/images/logo.png +0 -0
  54. data/lib/flipper/ui/public/images/remove.png +0 -0
  55. data/lib/flipper/ui/public/js/application.js +544 -0
  56. data/lib/flipper/ui/public/js/handlebars.js +1992 -0
  57. data/lib/flipper/ui/public/js/jquery-ui-1.10.3.slider.min.js +6 -0
  58. data/lib/flipper/ui/public/js/jquery.js +9555 -0
  59. data/lib/flipper/ui/public/js/jquery.min.js +4 -0
  60. data/lib/flipper/ui/public/js/jquery.min.map +1 -0
  61. data/lib/flipper/ui/public/js/spine/ajax.js +320 -0
  62. data/lib/flipper/ui/public/js/spine/list.js +72 -0
  63. data/lib/flipper/ui/public/js/spine/local.js +29 -0
  64. data/lib/flipper/ui/public/js/spine/manager.js +157 -0
  65. data/lib/flipper/ui/public/js/spine/relation.js +260 -0
  66. data/lib/flipper/ui/public/js/spine/route.js +223 -0
  67. data/lib/flipper/ui/public/js/spine/spine.js +927 -0
  68. data/lib/flipper/ui/util.rb +12 -0
  69. data/lib/flipper/ui/version.rb +5 -0
  70. data/lib/flipper/ui/views/index.erb +9 -0
  71. data/lib/flipper/ui/views/layout.erb +161 -0
  72. data/script/bootstrap +21 -0
  73. data/script/server +19 -0
  74. data/script/test +30 -0
  75. data/spec/flipper/ui/decorators/feature_spec.rb +59 -0
  76. data/spec/flipper/ui/decorators/gate_spec.rb +47 -0
  77. data/spec/flipper/ui/util_spec.rb +18 -0
  78. data/spec/flipper/ui_spec.rb +470 -0
  79. data/spec/helper.rb +35 -0
  80. metadata +168 -0
@@ -0,0 +1,223 @@
1
+ Spine = @Spine or require('spine')
2
+ $ = Spine.$
3
+ Model = Spine.Model
4
+ Queue = $({})
5
+
6
+ Ajax =
7
+ getURL: (object) ->
8
+ object and object.url?() or object.url
9
+
10
+ enabled: true
11
+
12
+ disable: (callback) ->
13
+ if @enabled
14
+ @enabled = false
15
+ try
16
+ do callback
17
+ catch e
18
+ throw e
19
+ finally
20
+ @enabled = true
21
+ else
22
+ do callback
23
+
24
+ queue: (request) ->
25
+ if request then Queue.queue(request) else Queue.queue()
26
+
27
+ clearQueue: ->
28
+ @queue []
29
+
30
+ class Base
31
+ defaults:
32
+ contentType: 'application/json'
33
+ dataType: 'json'
34
+ processData: false
35
+ headers: {'X-Requested-With': 'XMLHttpRequest'}
36
+
37
+ queue: Ajax.queue
38
+
39
+ ajax: (params, defaults) ->
40
+ $.ajax @ajaxSettings(params, defaults)
41
+
42
+ ajaxQueue: (params, defaults) ->
43
+ jqXHR = null
44
+ deferred = $.Deferred()
45
+
46
+ promise = deferred.promise()
47
+ return promise unless Ajax.enabled
48
+
49
+ settings = @ajaxSettings(params, defaults)
50
+
51
+ request = (next) ->
52
+ jqXHR = $.ajax(settings)
53
+ .done(deferred.resolve)
54
+ .fail(deferred.reject)
55
+ .then(next, next)
56
+
57
+ promise.abort = (statusText) ->
58
+ return jqXHR.abort(statusText) if jqXHR
59
+ index = $.inArray(request, @queue())
60
+ @queue().splice(index, 1) if index > -1
61
+
62
+ deferred.rejectWith(
63
+ settings.context or settings,
64
+ [promise, statusText, '']
65
+ )
66
+ promise
67
+
68
+ @queue request
69
+ promise
70
+
71
+ ajaxSettings: (params, defaults) ->
72
+ $.extend({}, @defaults, defaults, params)
73
+
74
+ class Collection extends Base
75
+ constructor: (@model) ->
76
+
77
+ find: (id, params) ->
78
+ record = new @model(id: id)
79
+ @ajaxQueue(
80
+ params,
81
+ type: 'GET',
82
+ url: Ajax.getURL(record)
83
+ ).done(@recordsResponse)
84
+ .fail(@failResponse)
85
+
86
+ all: (params) ->
87
+ @ajaxQueue(
88
+ params,
89
+ type: 'GET',
90
+ url: Ajax.getURL(@model)
91
+ ).done(@recordsResponse)
92
+ .fail(@failResponse)
93
+
94
+ fetch: (params = {}, options = {}) ->
95
+ if id = params.id
96
+ delete params.id
97
+ @find(id, params).done (record) =>
98
+ @model.refresh(record, options)
99
+ else
100
+ @all(params).done (records) =>
101
+ @model.refresh(records, options)
102
+
103
+ # Private
104
+
105
+ recordsResponse: (data, status, xhr) =>
106
+ @model.trigger('ajaxSuccess', null, status, xhr)
107
+
108
+ failResponse: (xhr, statusText, error) =>
109
+ @model.trigger('ajaxError', null, xhr, statusText, error)
110
+
111
+ class Singleton extends Base
112
+ constructor: (@record) ->
113
+ @model = @record.constructor
114
+
115
+ reload: (params, options) ->
116
+ @ajaxQueue(
117
+ params,
118
+ type: 'GET'
119
+ url: Ajax.getURL(@record)
120
+ ).done(@recordResponse(options))
121
+ .fail(@failResponse(options))
122
+
123
+ create: (params, options) ->
124
+ @ajaxQueue(
125
+ params,
126
+ type: 'POST'
127
+ data: JSON.stringify(@record)
128
+ url: Ajax.getURL(@model)
129
+ ).done(@recordResponse(options))
130
+ .fail(@failResponse(options))
131
+
132
+ update: (params, options) ->
133
+ @ajaxQueue(
134
+ params,
135
+ type: 'PUT'
136
+ data: JSON.stringify(@record)
137
+ url: Ajax.getURL(@record)
138
+ ).done(@recordResponse(options))
139
+ .fail(@failResponse(options))
140
+
141
+ destroy: (params, options) ->
142
+ @ajaxQueue(
143
+ params,
144
+ type: 'DELETE'
145
+ url: Ajax.getURL(@record)
146
+ ).done(@recordResponse(options))
147
+ .fail(@failResponse(options))
148
+
149
+ # Private
150
+
151
+ recordResponse: (options = {}) =>
152
+ (data, status, xhr) =>
153
+ if Spine.isBlank(data) or @record.destroyed
154
+ data = false
155
+ else
156
+ data = @model.fromJSON(data)
157
+
158
+ Ajax.disable =>
159
+ if data
160
+ # ID change, need to do some shifting
161
+ if data.id and @record.id isnt data.id
162
+ @record.changeID(data.id)
163
+
164
+ # Update with latest data
165
+ @record.updateAttributes(data.attributes())
166
+
167
+ @record.trigger('ajaxSuccess', data, status, xhr)
168
+ options.success?.apply(@record) # Deprecated
169
+ options.done?.apply(@record)
170
+
171
+ failResponse: (options = {}) =>
172
+ (xhr, statusText, error) =>
173
+ @record.trigger('ajaxError', xhr, statusText, error)
174
+ options.error?.apply(@record) # Deprecated
175
+ options.fail?.apply(@record)
176
+
177
+ # Ajax endpoint
178
+ Model.host = ''
179
+
180
+ Include =
181
+ ajax: -> new Singleton(this)
182
+
183
+ url: (args...) ->
184
+ url = Ajax.getURL(@constructor)
185
+ url += '/' unless url.charAt(url.length - 1) is '/'
186
+ url += encodeURIComponent(@id)
187
+ args.unshift(url)
188
+ args.join('/')
189
+
190
+ Extend =
191
+ ajax: -> new Collection(this)
192
+
193
+ url: (args...) ->
194
+ args.unshift(@className.toLowerCase() + 's')
195
+ args.unshift(Model.host)
196
+ args.join('/')
197
+
198
+ Model.Ajax =
199
+ extended: ->
200
+ @fetch @ajaxFetch
201
+ @change @ajaxChange
202
+
203
+ @extend Extend
204
+ @include Include
205
+
206
+ # Private
207
+
208
+ ajaxFetch: ->
209
+ @ajax().fetch(arguments...)
210
+
211
+ ajaxChange: (record, type, options = {}) ->
212
+ return if options.ajax is false
213
+ record.ajax()[type](options.ajax, options)
214
+
215
+ Model.Ajax.Methods =
216
+ extended: ->
217
+ @extend Extend
218
+ @include Include
219
+
220
+ # Globals
221
+ Ajax.defaults = Base::defaults
222
+ Spine.Ajax = Ajax
223
+ module?.exports = Ajax
@@ -0,0 +1,43 @@
1
+ Spine = @Spine or require('spine')
2
+ $ = Spine.$
3
+
4
+ class Spine.List extends Spine.Controller
5
+ events:
6
+ 'click .item': 'click'
7
+
8
+ selectFirst: false
9
+
10
+ constructor: ->
11
+ super
12
+ @bind 'change', @change
13
+
14
+ template: ->
15
+ throw 'Override template'
16
+
17
+ change: (item) =>
18
+ @current = item
19
+
20
+ unless @current
21
+ @children().removeClass('active')
22
+ return
23
+
24
+ @children().removeClass('active')
25
+ $(@children().get(@items.indexOf(@current))).addClass('active')
26
+
27
+ render: (items) ->
28
+ @items = items if items
29
+ @html @template(@items)
30
+ @change @current
31
+ if @selectFirst
32
+ unless @children('.active').length
33
+ @children(':first').click()
34
+
35
+ children: (sel) ->
36
+ @el.children(sel)
37
+
38
+ click: (e) ->
39
+ item = @items[$(e.currentTarget).index()]
40
+ @trigger('change', item)
41
+ true
42
+
43
+ module?.exports = Spine.List
@@ -0,0 +1,16 @@
1
+ Spine = @Spine or require('spine')
2
+
3
+ Spine.Model.Local =
4
+ extended: ->
5
+ @change @saveLocal
6
+ @fetch @loadLocal
7
+
8
+ saveLocal: ->
9
+ result = JSON.stringify(@)
10
+ localStorage[@className] = result
11
+
12
+ loadLocal: ->
13
+ result = localStorage[@className]
14
+ @refresh(result or [], clear: true)
15
+
16
+ module?.exports = Spine.Model.Local
@@ -0,0 +1,83 @@
1
+ Spine = @Spine or require('spine')
2
+ $ = Spine.$
3
+
4
+ class Spine.Manager extends Spine.Module
5
+ @include Spine.Events
6
+
7
+ constructor: ->
8
+ @controllers = []
9
+ @bind 'change', @change
10
+ @add(arguments...)
11
+
12
+ add: (controllers...) ->
13
+ @addOne(cont) for cont in controllers
14
+
15
+ addOne: (controller) ->
16
+ controller.bind 'active', (args...) =>
17
+ @trigger('change', controller, args...)
18
+ controller.bind 'release', =>
19
+ @controllers.splice(@controllers.indexOf(controller), 1)
20
+
21
+ @controllers.push(controller)
22
+
23
+ deactivate: ->
24
+ @trigger('change', false, arguments...)
25
+
26
+ # Private
27
+
28
+ change: (current, args...) ->
29
+ for cont in @controllers
30
+ if cont is current
31
+ cont.activate(args...)
32
+ else
33
+ cont.deactivate(args...)
34
+
35
+ Spine.Controller.include
36
+ active: (args...) ->
37
+ if typeof args[0] is 'function'
38
+ @bind('active', args[0])
39
+ else
40
+ args.unshift('active')
41
+ @trigger(args...)
42
+ @
43
+
44
+ isActive: ->
45
+ @el.hasClass('active')
46
+
47
+ activate: ->
48
+ @el.addClass('active')
49
+ @
50
+
51
+ deactivate: ->
52
+ @el.removeClass('active')
53
+ @
54
+
55
+ class Spine.Stack extends Spine.Controller
56
+ controllers: {}
57
+ routes: {}
58
+
59
+ className: 'spine stack'
60
+
61
+ constructor: ->
62
+ super
63
+
64
+ @manager = new Spine.Manager
65
+
66
+ for key, value of @controllers
67
+ @[key] = new value(stack: @)
68
+ @add(@[key])
69
+
70
+ for key, value of @routes
71
+ do (key, value) =>
72
+ callback = value if typeof value is 'function'
73
+ callback or= => @[value].active(arguments...)
74
+ @route(key, callback)
75
+
76
+ @[@default].active() if @default
77
+
78
+ add: (controller) ->
79
+ @manager.add(controller)
80
+ @append(controller)
81
+
82
+ module?.exports = Spine.Manager
83
+ module?.exports.Stack = Spine.Stack
@@ -0,0 +1,148 @@
1
+ Spine = @Spine or require('spine')
2
+ isArray = Spine.isArray
3
+ require = @require or ((value) -> eval(value))
4
+
5
+ class Collection extends Spine.Module
6
+ constructor: (options = {}) ->
7
+ for key, value of options
8
+ @[key] = value
9
+
10
+ all: ->
11
+ @model.select (rec) => @associated(rec)
12
+
13
+ first: ->
14
+ @all()[0]
15
+
16
+ last: ->
17
+ values = @all()
18
+ values[values.length - 1]
19
+
20
+ find: (id) ->
21
+ records = @select (rec) =>
22
+ "#{rec.id}" is "#{id}"
23
+ throw new Error("\"#{@model.className}\" model could not find a record for the ID \"#{id}\"") unless records[0]
24
+ records[0]
25
+
26
+ findAllByAttribute: (name, value) ->
27
+ @model.select (rec) =>
28
+ @associated(rec) and rec[name] is value
29
+
30
+ findByAttribute: (name, value) ->
31
+ @findAllByAttribute(name, value)[0]
32
+
33
+ select: (cb) ->
34
+ @model.select (rec) =>
35
+ @associated(rec) and cb(rec)
36
+
37
+ refresh: (values) ->
38
+ delete @model.records[record.id] for record in @all()
39
+ records = @model.fromJSON(values)
40
+
41
+ records = [records] unless isArray(records)
42
+
43
+ for record in records
44
+ record.newRecord = false
45
+ record[@fkey] = @record.id
46
+ @model.records[record.id] = record
47
+
48
+ @model.trigger('refresh', @model.cloneArray(records))
49
+
50
+ create: (record) ->
51
+ record[@fkey] = @record.id
52
+ @model.create(record)
53
+
54
+ # Private
55
+
56
+ associated: (record) ->
57
+ record[@fkey] is @record.id
58
+
59
+ class Instance extends Spine.Module
60
+ constructor: (options = {}) ->
61
+ for key, value of options
62
+ @[key] = value
63
+
64
+ exists: ->
65
+ @record[@fkey] and @model.exists(@record[@fkey])
66
+
67
+ update: (value) ->
68
+ unless value instanceof @model
69
+ value = new @model(value)
70
+ value.save() if value.isNew()
71
+ @record[@fkey] = value and value.id
72
+
73
+ class Singleton extends Spine.Module
74
+ constructor: (options = {}) ->
75
+ for key, value of options
76
+ @[key] = value
77
+
78
+ find: ->
79
+ @record.id and @model.findByAttribute(@fkey, @record.id)
80
+
81
+ update: (value) ->
82
+ unless value instanceof @model
83
+ value = @model.fromJSON(value)
84
+
85
+ value[@fkey] = @record.id
86
+ value.save()
87
+
88
+ singularize = (str) ->
89
+ str.replace(/s$/, '')
90
+
91
+ underscore = (str) ->
92
+ str.replace(/::/g, '/')
93
+ .replace(/([A-Z]+)([A-Z][a-z])/g, '$1_$2')
94
+ .replace(/([a-z\d])([A-Z])/g, '$1_$2')
95
+ .replace(/-/g, '_')
96
+ .toLowerCase()
97
+
98
+ Spine.Model.extend
99
+ hasMany: (name, model, fkey) ->
100
+ fkey ?= "#{underscore(this.className)}_id"
101
+
102
+ association = (record) ->
103
+ model = require(model) if typeof model is 'string'
104
+
105
+ new Collection(
106
+ name: name, model: model,
107
+ record: record, fkey: fkey
108
+ )
109
+
110
+ @::[name] = (value) ->
111
+ association(@).refresh(value) if value?
112
+ association(@)
113
+
114
+ belongsTo: (name, model, fkey) ->
115
+ fkey ?= "#{singularize(name)}_id"
116
+
117
+ association = (record) ->
118
+ model = require(model) if typeof model is 'string'
119
+
120
+ new Instance(
121
+ name: name, model: model,
122
+ record: record, fkey: fkey
123
+ )
124
+
125
+ @::[name] = (value) ->
126
+ association(@).update(value) if value?
127
+ association(@).exists()
128
+
129
+ @attributes.push(fkey)
130
+
131
+ hasOne: (name, model, fkey) ->
132
+ fkey ?= "#{underscore(@className)}_id"
133
+
134
+ association = (record) ->
135
+ model = require(model) if typeof model is 'string'
136
+
137
+ new Singleton(
138
+ name: name, model: model,
139
+ record: record, fkey: fkey
140
+ )
141
+
142
+ @::[name] = (value) ->
143
+ association(@).update(value) if value?
144
+ association(@).find()
145
+
146
+ Spine.Collection = Collection
147
+ Spine.Singleton = Singleton
148
+ Spine.Instance = Instance