flipper-ui 0.2.0.beta1

Sign up to get free protection for your applications and to get access to all the features.
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