add tests for optional ui elements

add tests for optional ui elements

--- spec/IntegrationSpec.coffee
+++ spec/IntegrationSpec.coffee
@@ -39,14 +39,15 @@
     @$("a .epages-shop-product-name:contains(#{name})")

-describe "Integration", ->
+describe "Widget", ->
   jasmine.DEFAULT_TIMEOUT_INTERVAL = 10000

   beforeEach ->
     @$container = $ """

-
FIXTURE1

-
FIXTURE2

+
FIXTURE1

+
FIXTURE2

+
FIXTURE2

     """

@@ -66,7 +67,7 @@

   it "loads categories, product details and variations", (done) ->
     widget = new TestWidget
-      el: $(".epages-shop-widget:first")
+      el: $("#widget-with-categories")

     waitUntil widget.isReady, ->
       expect( widget.hasCategoryOption("Shoes") ).toBeTruthy()
@@ -76,5 +77,26 @@
         widget.productLink("Meindl RFS Tibet").click()

         waitUntil widget.variationsLoaded, ->
-          expect( $("label[for='epages-shop-variation-USSize']").is(':visible') ).toBeTruthy()
+          expect(
+            $("label[for='epages-shop-variation-USSize']").is(':visible')
+          ).toBeTruthy()
           done()
+
+  it "enabled search form and sort by default", (done) ->
+    widget = new TestWidget
+      el: $("#widget-default")
+
+    $(document).on "ajaxStop", ->
+      expect(widget.$(".epages-shop-category-list").html()).toEqual ""
+      expect(widget.$(".epages-shop-search-form form").length).toEqual 1
+      expect(widget.$(".epages-shop-sort a").length).toEqual 3
+      done()
+
+  it "allows disabled search form and sort", (done) ->
+    widget = new TestWidget
+      el: $("#widget-disabled")
+
+    $(document).on "ajaxStop", ->
+      expect(widget.$(".epages-shop-search-form").html()).toEqual ""
+      expect(widget.$(".epages-shop-sort").html()).toEqual ""
+      done()


Testes para updatePrice

Testes para updatePrice

--- spec/SkuSelectorSpec.coffee
+++ spec/SkuSelectorSpec.coffee
@@ -95,6 +95,21 @@
 		it 'should be instantiated', ->
 			expect(@ssr instanceof vtex.portalPlugins.SkuSelectorRenderer).toBe(true)

+		it 'should call updatePriceAvailable', ->
+			sku = {available: true}
+			spyOn(@ssr, 'updatePriceAvailable')
+			@ssr.updatePrice(sku)
+
+			expect(@ssr.updatePriceAvailable).toHaveBeenCalledWith(sku)
+
+		it 'should call updatePriceUnavailable', ->
+			sku = {available: false}
+			spyOn(@ssr, 'updatePriceUnavailable')
+			@ssr.updatePrice(sku)
+
+			expect(@ssr.updatePriceUnavailable).toHaveBeenCalled()
+
+
 	describe '$.skuSelector', ->
 		beforeEach ->
 			loadFixtures 'sku-selector.html'


6021: Refactored saved settings JS code

6021: Refactored saved settings JS code

--- app/assets/javascripts/backbone/views/reservation_calendar_view.coffee
+++ app/assets/javascripts/backbone/views/reservation_calendar_view.coffee
@@ -12,11 +12,12 @@
     @ruleSet = options.ruleSet
     @resourceId = options.resourceId
     @storageKey = "calendar#{@resourceId}Settings"
+    @savedSettings = @loadSettings()

     @calendar.fullCalendar
       events: options.feedUrl
       defaultView: @initialViewType(options.viewType, options.defaultViewType)
-      defaultDate: options.focusDate || @savedSettings().currentDate
+      defaultDate: options.focusDate || @savedSettings.currentDate
       height: 700
       allDaySlot: false
       eventOverlap: false
@@ -113,7 +114,7 @@
     window.location.href = "#{@newUrl}&#{$.param(@selection)}"

   initialViewType: (linkParam, defaultType) ->
-    type = @forceDay() && 'day' || linkParam || @savedSettings().viewType || defaultType || 'week'
+    type = @forceDay() && 'day' || linkParam || @savedSettings.viewType || defaultType || 'week'
     @URL_PARAMS_TO_VIEW_TYPES[type]

   permalink: ->
@@ -163,7 +164,7 @@

     nearest

-  savedSettings: ->
+  loadSettings: ->
     settings = JSON.parse(window.localStorage.getItem(@storageKey) || '{}')

     # currentDate setting expires after one hour


Be very careful about marking an update attempt finished

--- src/application.coffee
+++ src/application.coffee
@@ -275,7 +275,8 @@
 		if currentlyUpdating is 2
 			# If an update is required then schedule it
 			setTimeout(update)
-		# Set the updating as finished
+	.finally ->
+		# Set the updating as finished in its own block, so it never has to worry about other code stopping this.
 		currentlyUpdating = 0

 getDeviceID = do ->


Allow special env vars with a callback which don't cause an app restart

--- src/application.coffee
+++ src/application.coffee
@@ -292,6 +292,16 @@
 		err.message or err
 	"#{failures.length} error#{s}: #{messages.join(' - ')}"

+# Example callback function to enable/disable vpn
+# enableVpn = (val) ->
+# 	if val == '1' then console.log('Enabling VPN') else console.log('Disabling VPN')
+
+specialActionEnvVars = {
+	'RESIN_OVERRIDE_LOCK': null
+	# @Praneeth: maybe add your vars here with a callback as value? e.g:
+	# 'RESIN_ENABLE_VPN': enableVpn
+}
+
 UPDATE_IDLE = 0
 UPDATE_UPDATING = 1
 UPDATE_REQUIRED = 2
@@ -351,6 +361,8 @@
 					if app.environment_variable?
 						_.extend(env, app.environment_variable)
 					remoteAppEnvs[app.id] = env
+					env = _.pick env, (val, key) ->
+						!_.includes(key, _.keys(specialActionEnvVars))
 					return {
 						appId: '' + app.id
 						commit: app.commit
@@ -360,6 +372,13 @@

 				remoteApps = _.indexBy(remoteApps, 'appId')
 				remoteAppIds = _.keys(remoteApps)
+
+				# @Praneeth: looks like this might be a good place to run the special functions,
+				# like turning vpn on/off if remoteAppEnvs has the corresponding variable.
+				_.map specialActionEnvVars, (specialActionCallback, key) ->
+					_.map remoteAppIds, (appId) ->
+						if remoteAppEnvs[appId][key]? && specialActionCallback?
+							specialActionCallback(remoteAppEnvs[appId][key])

 				apps = _.indexBy(apps, 'appId')
 				localApps = _.mapValues apps, (app) ->


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/camera_fade.coffee
+++ src/camera_fade.coffee
@@ -10,7 +10,7 @@
 ###
 Camera.Fade = (I, self) ->
   Object.reverseMerge I,
-    alpha: 0
+    flalpha: 0

   defaultParams =
     duration: 30


Applying i18n for window title.

Applying i18n for window title.

--- battle-detail.coffee
+++ battle-detail.coffee
@@ -26,7 +26,8 @@
 window.i18n.resources.setLocale ?= (str) -> return
 window.__r = window.i18n.resources.__.bind(window.i18n.resources)

-# css
+
+document.title = __ 'Battle Detail'
 $('#font-awesome')?.setAttribute 'href', "#{ROOT}/components/font-awesome/css/font-awesome.min.css"


chars and escapes highlighted

chars and escapes highlighted

--- grammars/language-idris.cson
+++ grammars/language-idris.cson
@@ -57,7 +57,7 @@
       comment: 'Block comment'
     }
     {
-      name: 'string.quoted.idris'
+      name: 'string.quoted.double.idris'
       begin: '"'
       beginCaptures:
         0:
@@ -69,8 +69,24 @@
       patterns:
         [
           {
-            name: 'constant.character.escape.idris'
-            match: "\\\\\""
+            include: '#escape_characters'
+          }
+        ]
+    }
+    {
+      name: 'string.quoted.single.idris'
+      begin: "'"
+      beginCaptures:
+        0:
+          name: 'punctuation.definition.string.begin.idris'
+      end: "'"
+      endCaptures:
+        0:
+          name: 'punctuation.definition.string.end.idris'
+      patterns:
+        [
+          {
+            include: '#escape_characters'
           }
         ]
     }
@@ -117,6 +133,20 @@
     }
   ]
 repository:
-  {
-  }
+  escape_characters:
+    patterns:
+      [
+        {
+          name: 'constant.character.escape.ascii.idris'
+          match: '\\\\(NUL|SOH|STX|ETX|EOT|ENQ|ACK|BEL|BS|HT|LF|VT|FF|CR|SO|SI|DLE|DC1|DC2|DC3|DC4|NAK|SYN|ETB|CAN|EM|SUB|ESC|FS|GS|RS|US|SP|DEL|[abfnrtv\\\\\\"\'\\&])'
+        }
+        {
+          name: 'constant.character.escape.octal.idris'
+          match: '\\\\o[0-7]+|\\\\x[0-9A-Fa-f]+|\\\\[0-9]+'
+        }
+        {
+          name: 'constant.character.escape.control.idris'
+          match: '\\^[A-Z@\\[\\]\\\\\\^_]'
+        }
+      ]
 uuid: '4dd16092-ffa5-4ba4-8075-e5da9f368a72'


Fixed a bug with the music player where sometimes it wouldn't loop.

--- app/lib/surface/MusicPlayer.coffee
+++ app/lib/surface/MusicPlayer.coffee
@@ -23,7 +23,7 @@
     src = e.file
     if src
       src = "/file#{src}#{AudioPlayer.ext}"
-      return @currentMusic.play() if src is @currentMusic?.src
+      return @currentMusic.play('none', 0, 0, -1, 0.3) if src is @currentMusic?.src
       media = AudioPlayer.getStatus(src)
       if not media?.loaded
         AudioPlayer.preloadSound(src)


Clean up copyright statement

Clean up copyright statement

--- js-library/app/js/loader/loader_common.coffee
+++ js-library/app/js/loader/loader_common.coffee
@@ -1,7 +1,6 @@
 ###@license
-<%= pkg.title || pkg.name %> - v<%= pkg.version %> - <%= pkg.homepage ? " * " + pkg.homepage : "" %>
-Date: <%= grunt.template.today("m/d/yyyy") %>
-Copyright (c) <%= grunt.template.today("yyyy") %> <%= pkg.author %>
+Factlink JavaScript library - https://factlink.com
+Copyright (c) 2014 Factlink Team

 Copyright for the annotator subcomponent:


fix syntax error

fix syntax error

--- mechanics/003-twilight-zone/luminosity.coffee
+++ mechanics/003-twilight-zone/luminosity.coffee
@@ -27,7 +27,8 @@
             context.fillStyle = "#ff0000"
             context.fillRect(i, 1, 1, 1);

-            measure = 0, cnt = 0
+            measure = 0
+            cnt = 0
             for k in [0...10]
                 l = ls[10 * i + k]
                 if (l)


render error client side

render error client side

--- resources/assets/coffee/react/profile-page/top-ranks.coffee
+++ resources/assets/coffee/react/profile-page/top-ranks.coffee
@@ -49,7 +49,10 @@
     pagination = @props.pagination[key]
     scores = @props[key]

-    if scores?.length
+    if scores?.error
+      p className: 'profile-extra-entries', scores.error
+
+    else if scores?.length
       div className: 'profile-extra-entries',
         el PlayDetailList, scores: scores


Deselect highlights on annotation focus

Deselect highlights on annotation focus

--- client/publication.coffee
+++ client/publication.coffee
@@ -626,13 +626,15 @@
 focusAnnotation = (body) ->
   return unless body

+  currentPublication?._highlighter?._annotator?._deselectAllHighlights()
+
   if $(body).text().length > 0
     range = document.createRange()
-    sel = window.getSelection()
+    selection = window.getSelection()
     range.setStart body, 1
     range.collapse true
-    sel.removeAllRanges()
-    sel.addRange range
+    selection.removeAllRanges()
+    selection.addRange range

   body.focus()

@@ -752,7 +754,7 @@

 Template.annotationEditor.destroyed = ->
   @_rendered = false
-
+
 Template.highlightInvite.rendered = ->
   $(@findAll '.body').balanceText()


Two missed renames to FlashMessage.

Two missed renames to FlashMessage.

cc @Retronator

--- client/publication.coffee
+++ client/publication.coffee
@@ -647,7 +647,7 @@
     fields:
       _id: 1
       jobs: 1
-    transform: null # So that we don't have any complications passing it to Notify.error
+    transform: null # So that we don't have any complications passing it to FlashMessage.error

   # Only the latest job is pushed to the client, so index 0
   return unless publication?.jobs?[0]?.status is 'failed'
@@ -1092,7 +1092,7 @@
   'click .add': (event, template) ->
     Meteor.call 'create-annotation', Session.get('currentPublicationId'), (error, annotationId) =>
       # TODO: Does Meteor triggers removal if insertion was unsuccessful, so that we do not have to do anything?
-      return Notify.fromError error, true if error
+      return FlashMessage.fromError error, true if error

       Meteor.Router.toNew Meteor.Router.annotationPath Session.get('currentPublicationId'), Session.get('currentPublicationSlug'), annotationId


Avatar size hack clarification.

Avatar size hack clarification.

--- client/publication.coffee
+++ client/publication.coffee
@@ -1127,7 +1127,7 @@
   moment(@updatedAt).fromNow()

 Template.publicationAnnotationsItem.author = ->
-  # Because we cannot access parent templates we're modifying the data with an extra parameter
+  # Because we cannot send parameters to templates we're modifying the data with an extra parameter
   # TODO: Change when Meteor allows sending parameters to templates
   @author.avatarSize = 30
   @author
@@ -1308,7 +1308,7 @@
     return # Make sure CoffeeScript does not return anything

 Template.annotationCommentsListItem.author = ->
-  # Because we cannot access parent templates we're modifying the data with an extra parameter
+  # Because we cannot send parameters to templates we're modifying the data with an extra parameter
   # TODO: Change when Meteor allows sending parameters to templates
   @author.avatarSize = 30
   @author
@@ -1370,7 +1370,7 @@
     return # Make sure CoffeeScript does not return anything

 Template.annotationCommentEditor.currentPerson = ->
-  # Because we cannot access parent templates we're modifying the data with an extra parameter
+  # Because we cannot send parameters to templates we're modifying the data with an extra parameter
   # TODO: Change when Meteor allows sending parameters to templates
   _.extend Meteor.person(),
     avatarSize: 30


Fix bug in addition form

Fix bug in addition form

--- client/view/agenda.coffee
+++ client/view/agenda.coffee
@@ -18,15 +18,24 @@
     event.preventDefault()
     time = context.find('#time').value
     class1 = context.find('#class1').value
+    icon1 = context.find('#icon1').value
     class2 = context.find('#class2').value
+    icon2 = context.find('#icon2').value
     class3 = context.find('#class3').value
-    icon = context.find('#icon').value
+    icon3 = context.find('#icon3').value
     if not time then return
     time = new Date(time)
     if not time
       console.error('bad time string')
       return
-    AgendaItem.create({time: time, class1: class1, class2: class2, class3: class3, icon: icon})
+    AgendaItem.create
+      time: time
+      class1: class1
+      class2: class2
+      class3: class3
+      icon1: icon1
+      icon2: icon2
+      icon3: icon3

   'click #edit': ->
     Session.set('editingAgenda', not Session.get('editingAgenda'))


Add line/column to coffeescript error messages

--- tasks/coffee_build.coffee
+++ tasks/coffee_build.coffee
@@ -48,10 +48,14 @@
     if mt != entry?.mtime or outFile not of entry?.generated
       if mt != entry?.mtime
         src = grunt.file.read(file)
-        compiled = compile(src, {
-          sourceMap: options.sourceMap
-          bare: not options.wrap
-        })
+        try
+          compiled = compile(src, {
+            sourceMap: options.sourceMap
+            bare: not options.wrap
+          })
+        catch e
+          grunt.log.error("#{e.message}(line: #{e.location.last_line}, column: #{e.location.last_column})")
+          throw e
         grunt.log.writeln("Compiled #{file}")
         if options.sourceMap
           {js: compiled, v3SourceMap} = compiled
@@ -223,8 +227,12 @@
     if (mt = mtime(fp)) != buildCache.file[fp]?.mtime
       deps = []
       if (/\.coffee$/.test(fp))
-        {js, v3SourceMap} = compile(grunt.file.read(fp), {
-          sourceMap: true, bare: true})
+        try
+          {js, v3SourceMap} = compile(grunt.file.read(fp), {
+            sourceMap: true, bare: true})
+        catch e
+          grunt.log.error("#{e.message}(line: #{e.location.last_line}, column: #{e.location.last_column})")
+          throw e
       else # plain js
         js = grunt.file.read(fp)
         v3SourceMap = null


bugfix package name for UserData widget was accidentaly one2many

--- modules/gui/lib/widgets/users/UserList.coffee
+++ modules/gui/lib/widgets/users/UserList.coffee
@@ -97,7 +97,7 @@
                                 _cfg = new floyd.Config
                                     class: 'narrow'

-                                    type: 'one2many.widgets.users.UserData'
+                                    type: 'gui.widgets.users.UserData'

                                     data:
                                         newUser: true
@@ -130,7 +130,7 @@
                                 _cfg = new floyd.Config
                                     class: 'narrow'

-                                    type: 'one2many.widgets.users.UserData'
+                                    type: 'gui.widgets.users.UserData'

                                     data:
                                         admin: true


Tags now extracts the content of the tag from the tag object rather than relying on it already being a string.

--- src/Views/RecipeView.coffee
+++ src/Views/RecipeView.coffee
@@ -81,8 +81,8 @@
 		title = @has "title"
 		ingredients = (@has "ingredients").replace(/
/g, "\n")
 		instructions = (@has "instructions").replace(/
/g, "\n")
-		tags = @has "tags"
-
+		tags = (t["name"] for t in @has "tags")
+
 		editbtn = 		"


Port Pulsate to Process API

Port Pulsate to Process API

--- components/Pulsate.coffee
+++ components/Pulsate.coffee
@@ -1,71 +1,68 @@
 noflo = require 'noflo'

-class Pulsate extends noflo.Component
-  icon: 'rotate-right'
-  description: 'Pulsate lights naturally'
+calculateStep = (step, maxSteps) ->
+  # move value above 0
+  adjust = 1
+  # reduce value range between 1 and 0
+  rangeAdjust = 2
+  # Max RGB value
+  maxValue = 100

-  constructor: ->
-    @value = 0
-    @tracks = [0, 70, 50, 15]
-    @maxSteps = 100
-    @colors = [255, 255, 255]
-    @inPorts = new noflo.InPorts
-      step:
-        datatype: 'bang'
-        description: 'Calculate the next cycle of the animation'
-        required: yes
-      colors:
-        datatype: 'array'
-        description: 'Colors to make pulsing'
-        required: no
+  upperLimit = 0.5
+  lowerLimit = 0

-    @outPorts = new noflo.OutPorts
-      colors:
-        datatype: 'array'
-        description: 'Current color values'
-        required: yes
+  # convert to percentage of time units
+  percentage = (step * 100 / maxSteps) / 100

-    @inPorts.colors.on 'data', (@colors) =>
-    @inPorts.step.on 'data', =>
-      colors = []
-      for step, idx in @tracks
-        ledColors = []
-        for color in @colors
-          unless color
-            ledColors.push 0
-            continue
-          ledColors.push @step step
-        colors.push ledColors
-        @tracks[idx]++
-        if @tracks[idx] > @maxSteps
-          @tracks[idx] = 0
-      @outPorts.colors.send colors
-    @inPorts.step.on 'disconnect', =>
-      @outPorts.colors.disconnect()
+  # normalize to range between 1 and 0
+  x = ((Math.sin(2 * Math.PI * percentage)) + adjust) / rangeAdjust

-  step: (step) ->
-    # move value above 0
-    adjust = 1
-    # reduce value range between 1 and 0
-    rangeAdjust = 2
-    # Max RGB value
-    maxValue = 100
+  # returns a value between UPPER_LIMIT and LOWER_LIMIT
+  value = Math.max(lowerLimit, Math.min(x, upperLimit))

-    upperLimit = 0.5
-    lowerLimit = 0
+  # stretch to UPPER_LIMIT
+  valueStretch = value / upperLimit

-    # convert to percentage of time units
-    percentage = (step * 100 / @maxSteps) / 100
+  return Math.floor Math.abs (valueStretch * maxValue)

-    # normalize to range between 1 and 0
-    x = ((Math.sin(2 * Math.PI * percentage)) + adjust) / rangeAdjust
-
-    # returns a value between UPPER_LIMIT and LOWER_LIMIT
-    value = Math.max(lowerLimit, Math.min(x, upperLimit))
-
-    # stretch to UPPER_LIMIT
-    valueStretch = value / upperLimit
-
-    return Math.floor Math.abs (valueStretch * maxValue)
-
-exports.getComponent = -> new Pulsate
+exports.getComponent = ->
+  c = new noflo.Component
+  c.icon = 'rotate-right'
+  c.description = 'Pulsate lights naturally'
+  c.inPorts.add 'step',
+    datatype: 'bang'
+    description: 'Calculate the next cycle of the animation'
+    required: yes
+  c.inPorts.add 'colors',
+    datatype: 'array'
+    description: 'Colors to make pulsing'
+    required: no
+    control: yes
+  c.outPorts.add 'colors',
+    datatype: 'array'
+    description: 'Current color values'
+    required: yes
+  c.tracks = [0, 70, 50, 15]
+  c.maxSteps = 100
+  c.tearDown = (callback) ->
+    c.tracks = [0, 70, 50, 15]
+    do callback
+  c.process (input, output) ->
+    return unless input.hasData 'step', 'colors'
+    input.getData 'step'
+    colors = input.getData 'colors'
+    resultColors = []
+    for step, idx in c.tracks
+      ledColors = []
+      for color in colors
+        unless color
+          # Light is off, no need to calculate
+          ledColors.push 0
+          continue
+        ledColors.push calculateStep step, c.maxSteps
+      resultColors.push ledColors
+      c.tracks[idx]++
+      if c.tracks[idx] > c.maxSteps
+        c.tracks[idx] = 0
+    output.sendDone
+      colors: resultColors


Change notification supressesion logging from info->debug

--- src/adapters/generic.coffee
+++ src/adapters/generic.coffee
@@ -60,10 +60,10 @@
     users = [ users ] unless _(users).isArray()
     for user in users when user
       if _(@disabledUsers).contains user.id
-        @robot.logger.info "Github Notification surpressed for #{user.name}"
+        @robot.logger.debug "Github Notification surpressed for #{user.name}"
       else
         if message.author? and user.name is message.author.name
-          @robot.logger.info "Github Notification surpressed for #{user.name} because it would be a self-notification"
+          @robot.logger.debug "Github Notification surpressed for #{user.name} because it would be a self-notification"
           continue
         message.text += "\n#{message.footer}" if message.text and message.footer and @getDMCountFor(user) < 3
         @send message: room: user.name, message


allow flush to complete in acceptance test

--- services/document-updater/test/acceptance/coffee/DeletingAProjectTests.coffee
+++ services/document-updater/test/acceptance/coffee/DeletingAProjectTests.coffee
@@ -148,7 +148,7 @@
 						# after deleting the project and putting it in the queue, flush the queue
 						setTimeout () ->
 							DocUpdaterClient.flushOldProjects (error, res, body) =>
-								done()
+							setTimeout done, 1000 # allow time for the flush to complete
 						, 100
 				, 200


update tests

update tests

--- services/track-changes/test/unit/coffee/UpdatesManager/UpdatesManagerTests.coffee
+++ services/track-changes/test/unit/coffee/UpdatesManager/UpdatesManagerTests.coffee
@@ -333,52 +333,43 @@
 		it "should call the callback", ->
 			@callback.called.should.equal true

-
-	# describe "getProjectUpdates", ->
-	# 	beforeEach ->
-	# 		@updates = ["mock-updates"]
-	# 		@options = { before: "mock-before", limit: "mock-limit" }
-	# 		@MongoManager.getProjectUpdates = sinon.stub().callsArgWith(2, null, @updates)
-	# 		@UpdatesManager.processUncompressedUpdatesForProject = sinon.stub().callsArg(1)
-	# 		@UpdatesManager.getProjectUpdates @project_id, @options, @callback
-
-	# 	it "should process any outstanding updates", ->
-	# 		@UpdatesManager.processUncompressedUpdatesForProject
-	# 			.calledWith(@project_id)
-	# 			.should.equal true
-
-	# 	it "should get the updates from the database", ->
-	# 		@MongoManager.getProjectUpdates
-	# 			.calledWith(@project_id, @options)
-	# 			.should.equal true
-
-	# 	it "should return the updates", ->
-	# 		@callback
-	# 			.calledWith(null, @updates)
-	# 			.should.equal true
-
-
-	# describe "getProjectUpdatesWithUserInfo", ->
-	# 	beforeEach ->
-	# 		@updates = ["mock-updates"]
-	# 		@options = { before: "mock-before", limit: "mock-limit" }
-	# 		@updatesWithUserInfo = ["updates-with-user-info"]
-	# 		@UpdatesManager.getProjectUpdates = sinon.stub().callsArgWith(2, null, @updates)
-	# 		@UpdatesManager.fillUserInfo = sinon.stub().callsArgWith(1, null, @updatesWithUserInfo)
-	# 		@UpdatesManager.getProjectUpdatesWithUserInfo @project_id, @options, @callback
-
-	# 	it "should get the updates", ->
-	# 		@UpdatesManager.getProjectUpdates
-	# 			.calledWith(@project_id, @options)
-	# 			.should.equal true
-
-	# 	it "should file the updates with the user info", ->
-	# 		@UpdatesManager.fillUserInfo
-	# 			.calledWith(@updates)
-	# 			.should.equal true
-
-	# 	it "should return the updates with the filled details", ->
-	# 		@callback.calledWith(null, @updatesWithUserInfo).should.equal true
+	describe "getSummarizedProjectUpdates", ->
+		beforeEach ->
+			@updates = [{doc_id: 123, v:456, op: "mock-updates", meta: {user_id: 123, start_ts: 1233, end_ts:1234}}]
+			@options = { before: "mock-before", limit: "mock-limit" }
+			@summarizedUpdates = [
+				{meta: {user_ids: [123], start_ts: 1233, end_ts:1234},docs:{"123":{fromV:456,toV:456}}}
+			]
+			@updatesWithUserInfo = ["updates-with-user-info"]
+			@done_state = false
+			@iterator =
+				next: (cb) =>
+					@done_state = true
+					cb(null, @updates)
+				done: () =>
+					@done_state
+			@PackManager.makeProjectIterator = sinon.stub().callsArgWith(2, null, @iterator)
+			@UpdatesManager.processUncompressedUpdatesForProject = sinon.stub().callsArg(1)
+			@UpdatesManager.fillSummarizedUserInfo = sinon.stub().callsArgWith(1, null, @updatesWithUserInfo)
+			@UpdatesManager.getSummarizedProjectUpdates @project_id, @options, @callback
+
+		it "should process any outstanding updates", ->
+			@UpdatesManager.processUncompressedUpdatesForProject
+				.calledWith(@project_id)
+				.should.equal true
+
+		it "should get the updates", ->
+			@PackManager.makeProjectIterator
+				.calledWith(@project_id, @options.before)
+				.should.equal true
+
+		it "should fill the updates with the user info", ->
+			@UpdatesManager.fillSummarizedUserInfo
+				.calledWith(@summarizedUpdates)
+				.should.equal true
+
+		it "should return the updates with the filled details", ->
+			@callback.calledWith(null, @updatesWithUserInfo).should.equal true

 	# describe "_extendBatchOfSummarizedUpdates", ->
 	# 	beforeEach ->
@@ -485,93 +476,93 @@
 	# 		it "should call the callback with the updates", ->
 	# 			@callback.calledWith(null, @updates, null).should.equal true

-	# describe "fillUserInfo", ->
-	# 	describe "with valid users", ->
-	# 		beforeEach (done) ->
-	# 			{ObjectId} = require "mongojs"
-	# 			@user_id_1 = ObjectId().toString()
-	# 			@user_id_2 = ObjectId().toString()
-	# 			@updates = [{
-	# 				meta:
-	# 					user_id: @user_id_1
-	# 				op: "mock-op-1"
-	# 			}, {
-	# 				meta:
-	# 					user_id: @user_id_1
-	# 				op: "mock-op-2"
-	# 			}, {
-	# 				meta:
-	# 					user_id: @user_id_2
-	# 				op: "mock-op-3"
-	# 			}]
-	# 			@user_info = {}
-	# 			@user_info[@user_id_1] = email: "user1@sharelatex.com"
-	# 			@user_info[@user_id_2] = email: "user2@sharelatex.com"
+	describe "fillUserInfo", ->
+		describe "with valid users", ->
+			beforeEach (done) ->
+				{ObjectId} = require "mongojs"
+				@user_id_1 = ObjectId().toString()
+				@user_id_2 = ObjectId().toString()
+				@updates = [{
+					meta:
+						user_id: @user_id_1
+					op: "mock-op-1"
+				}, {
+					meta:
+						user_id: @user_id_1
+					op: "mock-op-2"
+				}, {
+					meta:
+						user_id: @user_id_2
+					op: "mock-op-3"
+				}]
+				@user_info = {}
+				@user_info[@user_id_1] = email: "user1@sharelatex.com"
+				@user_info[@user_id_2] = email: "user2@sharelatex.com"

-	# 			@WebApiManager.getUserInfo = (user_id, callback = (error, userInfo) ->) =>
-	# 				callback null, @user_info[user_id]
-	# 			sinon.spy @WebApiManager, "getUserInfo"
-
-	# 			@UpdatesManager.fillUserInfo @updates, (error, @results) =>
-	# 				done()
-
-	# 		it "should only call getUserInfo once for each user_id", ->
-	# 			@WebApiManager.getUserInfo.calledTwice.should.equal true
-	# 			@WebApiManager.getUserInfo
-	# 				.calledWith(@user_id_1)
-	# 				.should.equal true
-	# 			@WebApiManager.getUserInfo
-	# 				.calledWith(@user_id_2)
-	# 				.should.equal true
-
-	# 		it "should return the updates with the user info filled", ->
-	# 			expect(@results).to.deep.equal [{
-	# 				meta:
-	# 					user:
-	# 						email: "user1@sharelatex.com"
-	# 				op: "mock-op-1"
-	# 			}, {
-	# 				meta:
-	# 					user:
-	# 						email: "user1@sharelatex.com"
-	# 				op: "mock-op-2"
-	# 			}, {
-	# 				meta:
-	# 					user:
-	# 						email: "user2@sharelatex.com"
-	# 				op: "mock-op-3"
-	# 			}]
-
-
-	# 	describe "with invalid user ids", ->
-	# 		beforeEach (done) ->
-	# 			@updates = [{
-	# 				meta:
-	# 					user_id: null
-	# 				op: "mock-op-1"
-	# 			}, {
-	# 				meta:
-	# 					user_id: "anonymous-user"
-	# 				op: "mock-op-2"
-	# 			}]
-	# 			@WebApiManager.getUserInfo = (user_id, callback = (error, userInfo) ->) =>
-	# 				callback null, @user_info[user_id]
-	# 			sinon.spy @WebApiManager, "getUserInfo"
-
-	# 			@UpdatesManager.fillUserInfo @updates, (error, @results) =>
-	# 				done()
-
-	# 		it "should not call getUserInfo", ->
-	# 			@WebApiManager.getUserInfo.called.should.equal false
-
-	# 		it "should return the updates without the user info filled", ->
-	# 			expect(@results).to.deep.equal [{
-	# 				meta: {}
-	# 				op: "mock-op-1"
-	# 			}, {
-	# 				meta: {}
-	# 				op: "mock-op-2"
-	# 			}]
+				@WebApiManager.getUserInfo = (user_id, callback = (error, userInfo) ->) =>
+					callback null, @user_info[user_id]
+				sinon.spy @WebApiManager, "getUserInfo"
+
+				@UpdatesManager.fillUserInfo @updates, (error, @results) =>
+					done()
+
+			it "should only call getUserInfo once for each user_id", ->
+				@WebApiManager.getUserInfo.calledTwice.should.equal true
+				@WebApiManager.getUserInfo
+					.calledWith(@user_id_1)
+					.should.equal true
+				@WebApiManager.getUserInfo
+					.calledWith(@user_id_2)
+					.should.equal true
+
+			it "should return the updates with the user info filled", ->
+				expect(@results).to.deep.equal [{
+					meta:
+						user:
+							email: "user1@sharelatex.com"
+					op: "mock-op-1"
+				}, {
+					meta:
+						user:
+							email: "user1@sharelatex.com"
+					op: "mock-op-2"
+				}, {
+					meta:
+						user:
+							email: "user2@sharelatex.com"
+					op: "mock-op-3"
+				}]
+
+
+		describe "with invalid user ids", ->
+			beforeEach (done) ->
+				@updates = [{
+					meta:
+						user_id: null
+					op: "mock-op-1"
+				}, {
+					meta:
+						user_id: "anonymous-user"
+					op: "mock-op-2"
+				}]
+				@WebApiManager.getUserInfo = (user_id, callback = (error, userInfo) ->) =>
+					callback null, @user_info[user_id]
+				sinon.spy @WebApiManager, "getUserInfo"
+
+				@UpdatesManager.fillUserInfo @updates, (error, @results) =>
+					done()
+
+			it "should not call getUserInfo", ->
+				@WebApiManager.getUserInfo.called.should.equal false
+
+			it "should return the updates without the user info filled", ->
+				expect(@results).to.deep.equal [{
+					meta: {}
+					op: "mock-op-1"
+				}, {
+					meta: {}
+					op: "mock-op-2"
+				}]

 	describe "_summarizeUpdates", ->
 		beforeEach ->


AuthorizationCodeFlow bugfix

AuthorizationCodeFlow bugfix

--- src/models/OAuth2/AuthorizationCodeFlow.coffee
+++ src/models/OAuth2/AuthorizationCodeFlow.coffee
@@ -10,7 +10,7 @@
     authorizationUrl = @options.authorizationUrl.concat @concatenateUrlParams(@xhrAuthorizationParams)

     authenticationLayer = new steroids.views.WebView { location: authorizationUrl }
-    steroids.modal.show({ layer: authenticationLayer })
+    steroids.modal.show({ view: authenticationLayer })

   finish: (callback)=>
     @xhrAccessTokenParams =


add set domain action

add set domain action

--- config/initializers/tools.coffee
+++ config/initializers/tools.coffee
@@ -1,6 +1,6 @@
 module.exports = (compound) ->
     app = compound.app
-
+
     compound.tools.database = ->
         {User, CozyInstance, Application} = compound.models

@@ -37,7 +37,7 @@
                              console.log "Cleaning instances failed."
                         else
                              destroyApplications()
-
+
                 destroyUsers = ->
                     User.destroyAll (error) ->
                         if error
@@ -45,8 +45,26 @@
                              console.log "Cleaning Users failed."
                         else
                              destroyInstances()
-
+
                 destroyUsers()
                 break
+            when 'setdomain'
+                domain = process.argv[4]
+                CozyInstance.all (err, instances) ->
+                    if err
+                        console.log err
+                        process.exit(0)
+                    else if instances.length is 0
+                        CozyInstance.create domain: domain, (err) ->
+                            if err then console.log err
+                            else console.log "Domain name set with #{domain}"
+                            process.exit(0)
+                    else
+                        instance = instances[0]
+                        instance.domain = domain
+                        instance.save (err) ->
+                            if err then console.log err
+                            else console.log "Domain name set with #{domain}"
+                            process.exit(0)
             else
                 console.log 'Usage: compound database [cleanuser|cleanapps]'


502 != 520

502 != 520

--- src/dibweb.coffee
+++ src/dibweb.coffee
@@ -24,9 +24,9 @@
         switch res.statusCode
           when 200
             msg.send body
-          when 502
-            msg.send body
           when 400
             msg.send body
+          when 520
+            msg.send body
           else
-            msg.send "Something strange happened!"
+            msg.send "Something strange happened!\n#{body}"


diffrender prototyping

diffrender prototyping

--- src/docpad.coffee
+++ src/docpad.coffee
@@ -1009,7 +1009,7 @@
 				if result
 					return result.get('contentRendered') or result.get('content')
 				else
-					warn = "The file #{relativeBase} was not found..."
+					warn = "The file #{subRelativePath} was not found..."
 					docpad.warn(warn)
 					return warn

@@ -1719,8 +1719,7 @@

 		# Extract document
 		if opts.filename
-			document = @createDocument()
-			document.set(
+			document = @createDocument(
 				filename: opts.filename
 				fullPath: opts.filename
 				data: opts.content
@@ -1812,32 +1811,38 @@

 		# Change event handler
 		changeHandler = (eventName,filePath,fileCurrentStat,filePreviousStat) ->
-			###
-			# Differential Rendering?
-			if config.differentialRendering
-
-				# Handle the action
+			# Handle
+			tasks = new balUtil.Group (err) ->
+				docpad.error(err)  if err
+				logger.log 'Regenerated due to file watch at '+(new Date()).toLocaleString()
+
+			# Handle the action
+			if eventName is 'new'
+				# Create a file for it
+				# todo, need to check if it is dir, or whether a file or document
+				changedFile = docpad.createDocument(fullPath: filePath)
+				# Load and render this file
+				tasks.push (complete) ->
+					changedFile.action('load contextualize render write', complete)
+			else
+				# Fetch
+				changedFile = database.findOne(fullPath: filePath)
 				if eventName is 'unlink'
+					# Destroy this file
 					changedFile.destroy()
 				else if eventName is 'change'
-					# Re-render just this file
-					changedFile = database.findOne(fullPath: filePath)
-					docpad.prepareAndRender changedFile, docpad.getTemplateData(), ->
-						# Re-Render anything that references the changes
-						pendingFiles = database.findAll(references: $has: changedFile).render()
-						docpad.prepareAndRender pend
-
-				else if eventName is 'new'
-
-				# Re-Render anything that should always re-render
-				database.findAll(referencesOthers: true).render()
-
-			# Re-Render everything
-			else
-			###
-			docpad.action 'generate', (err) ->
-				docpad.error(err)  if err
-				logger.log 'Regenerated due to file watch at '+(new Date()).toLocaleString()
+					# Re load and render this file
+					tasks.push (complete) ->
+						changedFile.action('load contextualize render write', complete)
+
+			# Afterwards, re-render anything that should always re-render
+			database.findAll(referencesOthers:true).each (document) -> tasks.push (complete) ->
+				document.action('render write',complete)
+			# todo, do a second pass here, first pass should just be renders, second pass should be writes, second pass can be async
+
+			# Execute the tasks synchronously
+			tasks.sync()
+

 		# A fatal error occured
 		fatal = (err) ->


Privatize methods on Editor

Privatize methods on Editor

- ::isEqual
- ::destroyFoldsIntersectingBufferRange
- ::destroyMarker
- ::addCursor
- ::addSelection

--- src/editor.coffee
+++ src/editor.coffee
@@ -166,7 +166,7 @@
     else
       'untitled'

-  # Public: Compares two `Editor`s to determine equality.
+  # Compares two `Editor`s to determine equality.
   #
   # Equality is based on the condition that:
   #
@@ -590,7 +590,7 @@
   destroyFoldsContainingBufferRow: (bufferRow) ->
     @displayBuffer.destroyFoldsContainingBufferRow(bufferRow)

-  # Public: Removes any {Fold}s found that intersect the given buffer row.
+  # Removes any {Fold}s found that intersect the given buffer row.
   destroyFoldsIntersectingBufferRange: (bufferRange) ->
     for row in [bufferRange.start.row..bufferRange.end.row]
       @destroyFoldsContainingBufferRow(row)
@@ -799,7 +799,7 @@
   markBufferPosition: (args...) ->
     @displayBuffer.markBufferPosition(args...)

-  # Public: {Delegates to: DisplayBuffer.destroyMarker}
+  # {Delegates to: DisplayBuffer.destroyMarker}
   destroyMarker: (args...) ->
     @displayBuffer.destroyMarker(args...)

@@ -830,8 +830,7 @@
     @markBufferPosition(bufferPosition, @getSelectionMarkerAttributes())
     @getLastSelection().cursor

-  # Public: Adds and returns a cursor at the given {DisplayBufferMarker}
-  # position.
+  # Adds and returns a cursor based on the given {DisplayBufferMarker}.
   addCursor: (marker) ->
     cursor = new Cursor(editor: this, marker: marker)
     @cursors.push(cursor)
@@ -842,7 +841,7 @@
   removeCursor: (cursor) ->
     _.remove(@cursors, cursor)

-  # Public: Creates a new selection at the given marker.
+  # Adds and returns a {Selection} based on the given {DisplayBufferMarker}.
   #
   # marker  - The {DisplayBufferMarker} to highlight
   # options - An {Object} that pertains to the {Selection} constructor.


implement preformat for colors

implement preformat for colors

--- src/format.coffee
+++ src/format.coffee
@@ -147,6 +147,10 @@
     colors = _.clone(ScribeColorFormat.COLORS)
     delete colors['white']
     super(@root, 'background', 'background-color', colors, ScribeColorFormat.matchColor)
+
+  preformat: (color) ->
+    color = ScribeColorFormat.normalizeColor(color) if color
+    @root.ownerDocument.execCommand('backColor', false, color)

 class ScribeColorFormat extends ScribeStyleFormat
@@ -191,6 +195,10 @@
     delete colors['black']
     super(@root, 'color', 'color', colors, ScribeColorFormat.matchColor)

+  preformat: (color) ->
+    color = ScribeColorFormat.normalizeColor(color) if color
+    @root.ownerDocument.execCommand('foreColor', false, color)
+

 class ScribeFamilyFormat extends ScribeStyleFormat
   constructor: (@root) ->
@@ -203,8 +211,9 @@
       return false
     )

-  preformat: (value) ->
-    @root.ownerDocument.execCommand('fontName', false, @styles[value])
+  preformat: (family) ->
+    family = @styles[family] if family
+    @root.ownerDocument.execCommand('fontName', false, family)

 class ScribeSizeFormat extends ScribeStyleFormat
@@ -219,8 +228,8 @@
     container = super(value)
     return container

-  preformat: (value) ->
-    switch value
+  preformat: (size) ->
+    switch size
       when 'huge'   then size = 5
       when 'large'  then size = 3
       when 'small'  then size = 1


use background flush on disconnect

use background flush on disconnect

--- services/real-time/app/coffee/DocumentUpdaterManager.coffee
+++ services/real-time/app/coffee/DocumentUpdaterManager.coffee
@@ -36,9 +36,11 @@
 				callback err

 	flushProjectToMongoAndDelete: (project_id, callback = ()->) ->
+		# this method is called when the last connected user leaves the project
 		logger.log project_id:project_id, "deleting project from document updater"
 		timer = new metrics.Timer("delete.mongo.project")
-		url = "#{settings.apis.documentupdater.url}/project/#{project_id}"
+		# flush the project in the background when all users have left
+		url = "#{settings.apis.documentupdater.url}/project/#{project_id}?background=true"
 		request.del url, (err, res, body)->
 			timer.done()
 			if err?


added error logging

added error logging

--- services/web/app/coffee/Features/Chat/ChatController.coffee
+++ services/web/app/coffee/Features/Chat/ChatController.coffee
@@ -1,14 +1,21 @@
 ChatHandler = require("./ChatHandler")
 EditorRealTimeController = require("../Editor/EditorRealTimeController")
+logger = require("logger-sharelatex")

 module.exports =

 	sendMessage: (project_id, user_id, messageContent, callback)->
 		ChatHandler.sendMessage project_id, user_id, messageContent, (err, builtMessge)->
+			if err?
+				logger.err err:err, project_id:project_id, user_id:user_id, messageContent:messageContent, "problem sending message to chat api"
+				return callback(err)
 			EditorRealTimeController.emitToRoom project_id,  "new-chat-message", builtMessge, (err)->
 				callback(err)

 	getMessages: (project_id, query, callback)->
 		ChatHandler.getMessages project_id, query, (err)->
+			if err?
+				logger.err err:err, query:query, "problem getting messages from chat api"
+				return callback(err)
 			callback()


Update pipe sample

Update pipe sample

--- samples/pipe.coffee
+++ samples/pipe.coffee
@@ -2,23 +2,26 @@
 fs = require 'fs'
 buffered = require '..'

-# Prepare an file with content
-data = ''
-for i in [0...1000000]
-  data += "#{i} Lorem ipsum dolor sit amet, consectetur adipiscing elit.\n"
-fs.writeFile "#{__dirname}/pipe.in", data, (err) ->
-  return console.log err if err
+prepare = (callback) ->
+  # Prepare an file with content
+  data = ''
+  for i in [0...1000000]
+    data += "#{i} Lorem ipsum dolor sit amet, consectetur adipiscing elit.\n"
+  fs.writeFile "#{__dirname}/pipe.in", data, (err) ->
+    return console.log err if err
+    callback()

+run = ->
   # Prepare the pipe
   input = fs.createReadStream "#{__dirname}/pipe.in"
-  buffer = buffered 1024*1024
+  buffer = buffered 4*1024*1024
   output = fs.createWriteStream "#{__dirname}/pipe.out"
-
   # Call the pipe
   input.pipe(buffer).pipe(output)
-
   # See how that worked
   output.on 'error', (err)->
     console.log err
   output.on 'close', ->
     console.log 'done'
+
+prepare run


Added a generic homepage.

Added a generic homepage.

--- src/controllers.coffee
+++ src/controllers.coffee
@@ -1,2 +1,6 @@
 exports.apply = (server) ->

+	# A simple example view.
+	server.get '/', (req, res, next) ->
+		res.render 'home.jade'
+


Removed commented-out code

Removed commented-out code

--- src/io/web.coffee
+++ src/io/web.coffee
@@ -94,20 +94,7 @@
 			else null

 	renderMap: (x, y) ->
-		# c = @game.camera
 		map = @game.currentMap
-
-		# for cy in [0...c.viewport.h]
-		# 	sy = c.y + cy
-		# 	row = map.data[sy]
-
-			# to only get the part that's on-screen
-			# we slice from left to right edge of viewport
-			# row = row[c.x ... c.x+c.viewport.w]
-			# row = for t, tx in row[c.x ... c.x+c.viewport.w]
-			# 	if c.target.canSee {x: (c.x + tx), y: (c.y + cy)}
-			# 		t
-			# 	else ' '

 		log 'Hello world! Render time!'


Advanced menu, default to no-login-window,

--- src/js/app.coffee
+++ src/js/app.coffee
@@ -19,12 +19,6 @@

 signInWindow = undefined
 shows = undefined
-
-sleep = (ms) ->
-  unixtime_ms = new Date().getTime();
-  while(new Date().getTime() < unixtime_ms + ms)
-    1
-

 traktvRequest = (opt, success, failure) ->
   console.log "traktvRequest: opt: #{JSON.stringify opt}"
@@ -535,7 +529,7 @@
 initSettings = ->
   Settings.init()
   Settings.config {
-    url: "#{CONFIG_BASE_URL}login"
+    url: "#{CONFIG_BASE_URL}"
     autoSave: true
   }, (e) ->
     console.log "Returned from settings"
@@ -544,7 +538,7 @@

 initSettings()

-ADVANCED_MENU = false
+

 mainMenu = new UI.Menu
   sections: [
@@ -560,13 +554,11 @@
       title: 'My shows'
       icon: ICON_MENU_HOME
       id: 'myShows'
+    }, {
+      title: 'Advanced'
+      id: 'advanced'
     }]
   ]
-
-if ADVANCED_MENU
-  mainMenu.state.sections[0].items.push
-    title: 'Advanced'
-    id: 'advanced'

 mainMenu.show()

@@ -590,20 +582,16 @@
       advancedMenu = new UI.Menu
         sections: [
           items: [{
-            title: 'Reset localStorage'
+            title: 'Refresh shows'
+            action: -> refreshModels()
+            }, {
+            title: 'Reset local data'
             action: ->
               localStorage.clear()
               initSettings()
-              notificationCard = new UI.Card(
-                title: 'Sign-in required'
-                body: 'Open the Pebble App and configure Pebble Shows.'
-              )
-              notificationCard.show()
+              displaySignInWindow()

               console.log "Local storage cleared"
-          }, {
-            title: 'Refresh'
-            action: -> refreshModels()
           }]
         ]
       advancedMenu.on 'select', (e) -> e.item.action()


updated/working builtin.string.count

updated/working builtin.string.count

--- src/builtin/string.coffee
+++ src/builtin/string.coffee
@@ -40,11 +40,11 @@
     else
       new (type(@)) pad + @value + pad + fillchar

-  count: (sub, start = 0, end = @__len__()) ->
+  count: (sub, start = 0, end = @__len__().value) ->
     count = 0
     curIndex = 0
     slicedStr = @value.slice(start, end)
-    substringLen = (new Str(sub)).__len__()
+    substringLen = (new Str sub).__len__().value
     while curIndex < slicedStr.length
       substringIndex = slicedStr.indexOf(sub, curIndex)
       if substringIndex < 0
@@ -52,7 +52,7 @@
       else
         curIndex = substringIndex + substringLen
         count++
-    return count
+    return new Int count

   decode: -> return
   encode: -> return


Auto push

Auto push

--- app/src/Xjs/XTechnePanel.coffee
+++ app/src/Xjs/XTechnePanel.coffee
@@ -23,11 +23,14 @@
             @container.classList.add data.type
         @displaySurface.appendChild @container

+    # TODO: Use array of objects containing properties and HTML element rather
+    # than the element only and storing actions in the element (safer that way)
     initContent: (contents) ->
         @elements = new Array
         _this = @
         for elData in contents
             if not elData.type?
+                # TODO: Replace with future notification system call
                 console.warn "Missing panelItem type for #{JSON.stringify(elData)}"
                 continue
             el = document.createElement 'div'


slow down animation

slow down animation

--- src/my.coffee
+++ src/my.coffee
@@ -58,7 +58,7 @@
   device: IPAD
   margin: MARGIN
   grid: {size: HALF, split: 8}
-  duration: {step: 750, tone: 150}
+  duration: {step: 1000, tone: 150}
   row: {size: TOUCH + 2*MARGIN, spacing: TOUCH + 4*MARGIN}
   action_limit: 7
   speed: 1


Prevent multiple listeners on message stream per room

--- packages/rocketchat-ui/lib/RoomManager.coffee
+++ packages/rocketchat-ui/lib/RoomManager.coffee
@@ -62,7 +62,7 @@
 					sub.stop()

 			if openedRooms[typeName].rid?
-				msgStream.removeListener openedRooms[typeName].rid
+				msgStream.removeAllListeners openedRooms[typeName].rid
 				RocketChat.Notifications.unRoom openedRooms[typeName].rid, 'deleteMessage', onDeleteMessageStream

 			openedRooms[typeName].ready = false
@@ -108,21 +108,23 @@
 						record.ready = RoomHistoryManager.isLoading(room._id) is false
 						Dep.changed()

-						msgStream.on openedRooms[typeName].rid, (msg) ->
-
-							# Should not send message to room if room has not loaded all the current messages
-							if RoomHistoryManager.hasMoreNext(openedRooms[typeName].rid) is false
-
-								# Do not load command messages into channel
-								if msg.t isnt 'command'
-									ChatMessage.upsert { _id: msg._id }, msg
-
-								Meteor.defer ->
-									RoomManager.updateMentionsMarksOfRoom typeName
-
-								RocketChat.callbacks.run 'streamMessage', msg
-
-						RocketChat.Notifications.onRoom openedRooms[typeName].rid, 'deleteMessage', onDeleteMessageStream
+						if openedRooms[typeName].streamActive isnt true
+							openedRooms[typeName].streamActive = true
+							msgStream.on openedRooms[typeName].rid, (msg) ->
+
+								# Should not send message to room if room has not loaded all the current messages
+								if RoomHistoryManager.hasMoreNext(openedRooms[typeName].rid) is false
+
+									# Do not load command messages into channel
+									if msg.t isnt 'command'
+										ChatMessage.upsert { _id: msg._id }, msg
+
+									Meteor.defer ->
+										RoomManager.updateMentionsMarksOfRoom typeName
+
+									RocketChat.callbacks.run 'streamMessage', msg
+
+							RocketChat.Notifications.onRoom openedRooms[typeName].rid, 'deleteMessage', onDeleteMessageStream

 				Dep.changed()


Resolve bootstrap script to make it absolute

--- src/atom-application.coffee
+++ src/atom-application.coffee
@@ -224,7 +224,7 @@
       openedWindow = existingWindow
       openedWindow.openPath(pathToOpen, initialLine)
     else
-      bootstrapScript = 'window-bootstrap'
+      bootstrapScript = require.resolve('./window-bootstrap')
       if devMode
         resourcePath = global.devResourcePath
       else


Be more defensive about parsing line numbers

--- src/atom-application.coffee
+++ src/atom-application.coffee
@@ -211,8 +211,9 @@
   #    + devMode:
   #      Boolean to control the opened window's dev mode.
   openPath: ({pathToOpen, pidToKillWhenClosed, newWindow, devMode}={}) ->
-    [pathToOpen, initialLine] = pathToOpen.split(':')
-    initialLine -= 1 # Convert line numbers to a base of 0
+    [basename, initialLine] = path.basename(pathToOpen).split(':')
+    pathToOpen = "#{path.dirname(pathToOpen)}/#{basename}"
+    initialLine -= 1 if initialLine # Convert line numbers to a base of 0

     unless devMode
       existingWindow = @windowForPath(pathToOpen) unless pidToKillWhenClosed or newWindow


Listen for keyup event instead of keypress to update FileFinder list.

--- src/atom/file-finder.coffee
+++ src/atom/file-finder.coffee
@@ -7,7 +7,7 @@
   content: ->
     @div class: 'file-finder', =>
       @ol outlet: 'urlList'
-      @input outlet: 'input', keypress: 'populateUrlList'
+      @input outlet: 'input', keyup: 'populateUrlList'

   viewProperties:
     urls: null


Just ignore known and enumerated top-level folders.

--- background_scripts/completion.coffee
+++ background_scripts/completion.coffee
@@ -137,25 +137,25 @@
       @bookmarks = @traverseBookmarks(bookmarks).filter((bookmark) -> bookmark.url?)
       @onBookmarksLoaded()

+  ignoreTopLevel:
+    'Other Bookmarks': true
+    'Mobile Bookmarks': true
+    'Bookmarks Bar': true
+
   # Traverses the bookmark hierarchy, and returns a flattened list of all bookmarks.
   traverseBookmarks: (bookmarks) ->
     results = []
     bookmarks.forEach (folder) =>
-      # folder is a chrome virtual root folder.
-      folder.children.forEach((folder) =>
-        # folder is one of the chrome, built-in folders ("Other Bookmarks", "Mobile Bookmarks", ...).
-        folder.children.forEach((bookmark) =>
-          # bookmark is a user-defined folder, or itself a bookmark.
-          @traverseBookmarksRecursive bookmark, results))
-          #
-          # We do not add the (possible) bookmark itself.  The user has used the "/" key to begin searching
-          # within bookmark folders.  It's not clear what that even means for top-level bookmarks.
-          # results.push bookmark
+      @traverseBookmarksRecursive folder, results
     results

   # Recursive helper for `traverseBookmarks`.
   traverseBookmarksRecursive: (bookmark, results, parent={pathAndTitle:""}) ->
-    bookmark.pathAndTitle = if bookmark.title then parent.pathAndTitle + @folderSeparator + bookmark.title else parent.pathAndTitle
+    bookmark.pathAndTitle =
+      if bookmark.title and not (parent.pathAndTitle == "" and @ignoreTopLevel[bookmark.title])
+        parent.pathAndTitle + @folderSeparator + bookmark.title
+      else
+        parent.pathAndTitle
     results.push bookmark
     bookmark.children.forEach((child) => @traverseBookmarksRecursive child, results, bookmark) if bookmark.children


Custom-only: always filter for the custom search engine.

--- background_scripts/completion.coffee
+++ background_scripts/completion.coffee
@@ -472,37 +472,24 @@
     # This filter is applied to all of the suggestions from all of the completers, after they have been
     # aggregated by the MultiCompleter.
     filter = (suggestions) ->
-      if custom and haveCompletionEngine
-        # We only accept suggestions:
-        #   - from this completer, or
-        #   - from other completers, but then only if their URL matches this search engine and matches this
-        #     query (that is only if their URL could have been generated by this search engine).
-        suggestions.filter (suggestion) ->
-          suggestion.type == description or
-            # This is a suggestion for the same search engine.
-            (suggestion.url.startsWith(engine.searchUrlPrefix) and
-              # And the URL suffix (which must contain the query part) matches the current query.
-              RankingUtils.matches queryTerms, suggestion.url[engine.searchUrlPrefix.length..])
-
-      else if not custom
-        # Filter out any suggestion which is just what the user would get if they hit <Enter> anyway.  For
-        # example, don't offer "https://www.google.com/search?q=vimium" if the query is "vimium".
-        defaultUrl = Utils.createSearchUrl queryTerms, searchUrl
-        defaultQuery = queryTerms.join " "
-        suggestions.filter (suggestion) -> Utils.extractQuery(searchUrl, suggestion.url) != defaultQuery
-      else
-        suggestions
+      suggestions.filter (suggestion) ->
+        # We only keep suggestions which either *were* generated by this completion engine, or *could have
+        # been* generated by this completion engine.
+        suggestion.isSearchSuggestion or suggestion.isCustomSearch or Utils.extractQuery searchUrl, suggestion.url

     # If a previous suggestion still matches the query, then we keep it (even if the completion engine may not
     # return it for the current query).  This allows the user to pick suggestions by typing fragments of their
     # text, without regard to whether the completion engine can complete the actual text of the query.
     previousSuggestions =
-      for url, suggestion of @previousSuggestions
-        continue unless RankingUtils.matches queryTerms, suggestion.title
-        # Reset various fields, they may not be correct wrt. the current query.
-        extend suggestion, relevancy: null, html: null, highlightTerms: true, queryTerms: queryTerms
-        suggestion.relevancy = null
-        suggestion
+      if queryTerms.length == 0
+        []
+      else
+        for url, suggestion of @previousSuggestions
+          continue unless RankingUtils.matches queryTerms, suggestion.title
+          # Reset various fields, they may not be correct wrt. the current query.
+          extend suggestion, relevancy: null, html: null, highlightTerms: true, queryTerms: queryTerms
+          suggestion.relevancy = null
+          suggestion

     primarySuggestion = new Suggestion
       queryTerms: queryTerms


Pass rerun port to manager

Pass rerun port to manager

--- script/rerun.coffee
+++ script/rerun.coffee
@@ -40,7 +40,7 @@
     r.end()
     return
   console.log "Seed: #{seed}"
-  manager = new Manager { slice }
+  manager = new Manager { slice, port }
   require(argv.w).init manager
   manager.run()
   manager.on 'ready', ->


This is dev :dog2:

This is dev :dog2:

--- scripts/dees.coffee
+++ scripts/dees.coffee
@@ -3,5 +3,5 @@
 module.exports = (robot) ->
   robot.enter (msg) ->
     # If its dusya optionally follwed by ANYTHING
-    if /^dusya/.test(msg.user)
+    if /^dusya/.test(msg.user.name.toLowerCase())
       msg.send "YOUR NUTS"


Sort hash of options.

Sort hash of options.

--- app/assets/javascripts/app/controllers/_application_controller_form.js.coffee
+++ app/assets/javascripts/app/controllers/_application_controller_form.js.coffee
@@ -1066,7 +1066,6 @@

     return if !attribute.options
     return if _.isArray( attribute.options )
-
     options_by_name = []
     for i in attribute.options
       options_by_name.push i['name'].toString().toLowerCase()
@@ -1099,8 +1098,13 @@
           row.name = App.i18n.translateInline( row.name )
         attribute.options.push row
     else
-      for key, value of selection
-        name_new = value
+      order = _.sortBy(
+        _.keys( selection, (item) ->
+          selection[item].toString().toLowerCase()
+        )
+      ).reverse()
+      for key in order
+        name_new = selection[key]
         if attribute.translate
           name_new = App.i18n.translateInline( name_new )
         attribute.options.push {


Removed debug info.

Removed debug info.

--- app/assets/javascripts/app/controllers/_application_controller_generic.coffee
+++ app/assets/javascripts/app/controllers/_application_controller_generic.coffee
@@ -1101,7 +1101,6 @@
     # rerender, e. g. on language change
     if @globalRerender
       @bind('ui:rerender', =>
-        console.log('REREN')
         @lastAttributres = undefined
         object = App[@model].fullLocal(@object_id)
         @maybeRender(object)


:sos:

:sos:

--- scripts/help.coffee
+++ scripts/help.coffee
@@ -76,6 +76,16 @@
           return msg.send "Here are '#{filter}' commands: #{data.html_url}"
         msg.send "Full list of commands: #{data.html_url}"

+  robot.router.get "/#{robot.name}/help", (req, res) ->
+    cmds = renamedHelpCommands(robot).map (cmd) ->
+      cmd.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>')
+
+    emit = "
#{cmds.join '

'}
"
+
+    emit = emit.replace new RegExp("#{robot.name}", "ig"), "#{robot.name}"
+
+    res.setHeader 'content-type', 'text/html'
+    res.end helpContents robot.name, emit

 renamedHelpCommands = (robot) ->
   help_commands = robot.helpCommands().map (command) ->


Don't apply null to h2 on media-preview

Don't apply null to h2 on media-preview

This is a trivial change.

--- scripts/home.coffee
+++ scripts/home.coffee
@@ -1,415 +1,415 @@
-worker = new Worker('/static/worker.js')
-window.backgroundWorker = worker
-albumAttached = false
-maxConcurrentUploads = 3
-
-window.addEventListener('DOMContentLoaded', ->
-    window.addEventListener('dragenter', dragNop, false)
-    window.addEventListener('dragleave', dragNop, false)
-    window.addEventListener('dragover', dragNop, false)
-    window.addEventListener('drop', handleDragDrop, false)
-    document.getElementById('browse-link').addEventListener('click', (e) ->
-        e.preventDefault()
-        document.getElementById('browse').click()
-    , false)
-    document.getElementById('browse').addEventListener('change', (e) ->
-        handleFiles(e.target.files)
-    , false)
-    albumUI = document.getElementById('albumUI')
-    albumUI.querySelector('.button').addEventListener('click', (e) ->
-        e.preventDefault()
-        albumUI.querySelector('.button').classList.add('hidden')
-        albumUI.querySelector('.status').classList.remove('hidden')
-        albumUI.querySelector('.status').textContent = 'Processing, please wait...'
-        albumAttached = true
-        createAlbum()
-    , false)
-
-    worker.addEventListener('message', handleWorkerMessage)
-    worker.postMessage({ action: 'load' })
-    window.statusChange = (file, status, oldStatus) ->
-        if oldStatus == 'uploading'
-            uploadPendingItems()
-    # We kick this manually every so often to make sure nothing gets abandoned in the 'Pending' state
-    setInterval(uploadPendingFiles, 10000)
-
-    pasteTarget = document.getElementById('paste-target')
-    pasteTarget.addEventListener('paste', handlePaste, false)
-    forceFocus()
-
-    historyEnabled = document.getElementById('historyEnabled')
-    if not UserHistory.getHistoryEnabled()
-        historyEnabled.textContent = 'Enable local history'
-    historyEnabled.addEventListener('click', (e) ->
-        e.preventDefault()
-        if UserHistory.toggleHistoryEnabled()
-            historyEnabled.textContent = 'Disable local history'
-        else
-            historyEnabled.textContent = 'Enable local history'
-    , false)
-
-    items = UserHistory.getHistory()[..].reverse()[..4]
-    historyContainer = document.getElementById('history')
-    historyList = historyContainer.querySelector('ul')
-    blurb = document.getElementById('blurb')
-    if items.length != 0
-        spinner = document.createElement('div')
-        spinner.className = 'progress'
-        blurb.appendChild(spinner)
-        UserHistory.loadDetailedHistory(items, (result) ->
-            blurb.classList.add('hidden')
-            historyContainer.classList.remove('hidden')
-            spinner.parentElement.removeChild(spinner)
-            for item in items
-                if result[item]
-                    historyList.appendChild(createHistoryItem({ item: result[item], hash: item }))
-        )
-, false)
-
-forceFocus = ->
-    if document.activeElement.tagName in ['TEXTAREA', 'INPUT', 'IFRAME']
-        setTimeout(forceFocus, 250)
-        return
-    pasteTarget = document.getElementById('paste-target')
-    pasteTarget.focus()
-    setTimeout(forceFocus, 250)
-
-createHistoryItem = (h, noLink = false) ->
-    item = h.item
-    container = null
-    if h.base?
-        container = document.createElement(data.base)
-    else
-        container = document.createElement('li')
-    if item.blob_type == 'video'
-        preview = document.createElement('video')
-        preview.setAttribute('loop', 'true')
-        preview.poster = '/' + item.hash + '.jpg'
-        for file in item.files
-            if file.type.indexOf('video/') == 0
-                source = document.createElement('source')
-                source.src = window.cdn + file.file
-                source.type = file.type
-                preview.appendChild(source)
-        preview.volume = 0
-        preview.className = 'item-view'
-        preview.onmouseenter = (e) ->
-            e.target.play()
-        preview.onmouseleave = (e) ->
-            e.target.pause()
-    else if item.blob_type == 'image'
-        preview = document.createElement('img')
-        for file in item.files
-            if not file.type.indexOf('image/') == 0
-                continue
-            preview.src = window.cdn + file.file
-            break
-        preview.className = 'item-view'
-    else if item.blob_type == 'audio'
-        preview = document.createElement('img')
-        preview.src = '/static/audio-player-narrow.png'
-        preview.className = 'item-view'
-    else if item.type == 'application/album'
-        preview = document.createElement('div')
-        preview.className = 'album-preview'
-        for file in item.files
-            preview.appendChild(createHistoryItem(file, true))
-    if preview
-        if not noLink
-            a = document.createElement('a')
-            a.href = '/' + h.hash
-            a.target = '_blank'
-            a.appendChild(preview)
-            container.appendChild(a)
-    return container
-
-window.onbeforeunload = ->
-    for f of uploadedFiles
-        if uploadedFiles[f].status not in ['done', 'error', 'ready']
-            return 'If you leave this page, your uploads will be cancelled.'
-
-handleWorkerMessage = (e) ->
-    if e.data.execute?
-        eval(e.data.execute)
-    if e.data.event?
-        switch e.data.event
-            when 'file-status-change' then fileStatusChanged(e.data)
-
-dragNop = (e) ->
-    e.stopPropagation()
-    e.preventDefault()
-
-handleDragDrop = (e) ->
-    dragNop(e)
-    droparea = document.getElementById('droparea')
-    droparea.classList.remove('hover') if droparea.classList.contains('hover')
-    files = e.dataTransfer.files
-    handleFiles(files) if files.length > 0
-
-pendingFiles = []
-updateQueue = ->
-    files = pendingFiles.splice(0, 5)
-    urls = pendingUrls.splice(0, 5)
-    fileList = document.getElementById('files')
-    scrollingContainer = document.getElementById('droparea')
-    for file in files
-        ((file) ->
-            mediaFile = new MediaFile(file)
-            mediaFile.preview = createPreview(file.name)
-            _ = scrollingContainer.scrollTop
-            fileList.appendChild(mediaFile.preview)
-            scrollingContainer.scrollTop = _
-            mediaFile.preview = fileList.lastElementChild
-            mediaFile.loadPreview()
-            mediaFile.hash = new String(guid())
-            mediaFile.updateStatus('local-pending')
-            uploadedFiles[mediaFile.hash] = mediaFile
-        )(file)
-    for url in urls
-        ((url) ->
-            mediaFile = new MediaFile(url)
-            mediaFile.preview = createPreview(mediaFile.name)
-            _ = scrollingContainer.scrollTop
-            fileList.appendChild(mediaFile.preview)
-            scrollingContainer.scrollTop = _
-            mediaFile.preview = fileList.lastElementChild
-            mediaFile.loadPreview()
-            mediaFile.hash = new String(guid())
-            mediaFile.updateStatus('local-pending')
-            uploadedFiles[mediaFile.hash] = mediaFile
-        )(url)
-    if pendingFiles.length + pendingUrls.length > 0
-        setTimeout(updateQueue, 500)
-    if files.length > 0
-        uploadPendingFiles()
-
-handleFiles = (files) ->
-    if albumAttached
-        albumUI.querySelector('.button').classList.add('hidden')
-        albumUI.querySelector('.status').classList.remove('hidden')
-        albumUI.querySelector('.status').textContent = 'Processing, please wait...'
-        albumUI.querySelector('.result').classList.add('hidden')
-    if Object.keys(uploadedFiles).length == 0
-        document.getElementById('files').innerHTML = ''
-        dropArea = document.getElementById('droparea')
-        dropArea.style.overflowY = 'scroll'
-        dropArea.classList.add('files')
-    pendingFiles.push(file) for file in files
-    updateQueue()
-window.handleFiles = handleFiles
-
-uploadPendingFiles = ->
-    toUpload = []
-    uploading = 0
-    for hash, file of uploadedFiles
-        if file.status in ['preparing', 'uploading']
-            uploading++
-            return if uploading >= maxConcurrentUploads
-        else if file.status == 'local-pending' and toUpload.length < 5
-            toUpload.push(file)
-    for file in toUpload
-        ((file) ->
-            if file.isUrl
-                uploadUrlFile(file)
-            else
-                reader = new FileReader()
-                reader.onloadend = (e) ->
-                    try
-                        data = e.target.result
-                        file.updateStatus('preparing')
-                        worker.postMessage({ action: 'compute-hash', data: data, callback: 'hashCompleted', id: file.hash })
-                    catch e # Too large
-                        uploadFile(file)
-                reader.readAsBinaryString(file.file)
-        )(file)
-
-hashCompleted = (id, result) ->
-    file = uploadedFiles[id]
-    file.hash = result
-    delete uploadedFiles[id]
-    uploadedFiles[result] = file
-    file.isHashed = true
-    uploadFile(file)
-
-handlePaste = (e) ->
-    target = document.getElementById('paste-target')
-    if e.clipboardData
-        text = e.clipboardData.getData('text/plain')
-        if text
-            if text.indexOf('http://') == 0 or text.indexOf('https://') == 0
-                urls = text.split('\n')
-                uploadUrls(url.trim() for url in urls when url.indexOf('http://') == 0 or url.indexOf('https://') == 0)
-                target.innerHTML = ''
-            else
-                # todo: plaintext
-        else
-            if e.clipboardData.items # webkit
-                for item in e.clipboardData.items
-                    if item.type.indexOf('image/') == 0
-                        file = item.getAsFile
-                        file.name = 'Clipboard'
-                        handleFiles([ file ])
-            else # not webkit
-                check = ->
-                    if target.innerHTML != ''
-                        img = target.firstChild.src
-                        if img.indexOf('data:image/png;base64,') == 0
-                            blob = dataURItoBlob(img)
-                            blob.name = 'Clipboard'
-                            handleFiles([ blob ])
-                        target.innerHTML = ''
-                    else
-                        setTimeout(check, 100)
-                check()
-
-pendingUrls = []
-uploadUrls = (urls) ->
-    if albumAttached
-        albumUI.querySelector('.button').classList.add('hidden')
-        albumUI.querySelector('.status').classList.remove('hidden')
-        albumUI.querySelector('.status').textContent = 'Processing, please wait...'
-        albumUI.querySelector('.result').classList.add('hidden')
-    if Object.keys(uploadedFiles).length == 0
-        document.getElementById('files').innerHTML = ''
-        dropArea = document.getElementById('droparea')
-        dropArea.style.overflowY = 'scroll'
-        dropArea.classList.add('files')
-    pendingUrls.push(url) for url in urls
-    updateQueue()
-
-uploadUrlFile = (file) ->
-    oldHash = file.hash
-    API.uploadUrl(file, (result) ->
-        uploadedFiles[file.hash] = file
-        if result.error?
-            file.setError(result.error)
-            return
-        file = result.file
-        delete uploadedFiles[oldHash]
-        uploadedFiles[file.hash] = file
-        if file.status == 'done'
-            finish(file)
-        else
-            file.isUserOwned = true
-            worker.postMessage({ action: 'monitor-status', hash: file.hash })
-    )
-
-uploadFile = (file) ->
-    oldHash = file.hash
-    upload = ->
-        file.updateStatus('uploading')
-        API.uploadFile(file, (e) ->
-            if e.lengthComputable
-                file.updateProgress(e.loaded / e.total)
-        , (result) ->
-            file.file = null
-            if result.error?
-                file.setError(result.error)
-                return
-            if file.hash != oldHash # for larger files, the server does the hashing for us
-                delete uploadedFiles[oldHash]
-                uploadedFiles[file.hash] = file
-            if file.status == 'done'
-                finish(file)
-            else
-                file.isUserOwned = true
-                worker.postMessage({ action: 'monitor-status', hash: file.hash })
-        )
-    if file.isHashed
-        API.checkExists(file, (exists) ->
-            if exists
-                file.file = null
-                file.isUserOwned = false
-                file.updateStatus('done')
-                finish(file)
-                return
-            else
-                upload()
-        )
-
-fileStatusChanged = (e) ->
-    uploadedFiles[e.hash].updateStatus(e.status)
-    if e.file? and e.file.flags?
-        uploadedFiles[e.hash].setFlags(e.file.flags)
-    if e.status in ['ready', 'done']
-        uploadedFiles[e.hash].blob = e.file
-        finish(uploadedFiles[e.hash])
-
-finish = (file) ->
-    file.finish()
-    updateAlbumUI()
-
-updateAlbumUI = ->
-    if albumAttached
-        createAlbum()
-    else
-        keys = []
-        for f, v of uploadedFiles
-            if v.status in ['processing', 'pending', 'ready', 'done', 'uploading']
-                keys.push(f)
-        albumUI = document.getElementById('albumUI')
-        if keys.length >= 2
-            albumUI.querySelector('.button').classList.remove('hidden')
-        else
-            albumUI.querySelector('.button').classList.add('hidden')
-
-createAlbum = ->
-    keys = []
-    for f, v of uploadedFiles
-        if v.status in ['done', 'ready']
-            keys.push(v)
-        else
-            return
-    return unless keys.length >= 2
-    keys.sort((a, b) -> a.index - b.index)
-    keys = (a.hash for a in keys)
-    API.createAlbum(keys, (result) ->
-        albumUI = document.getElementById('albumUI')
-        if result.error?
-            albumUI.querySelector('.status').classList.remove('hidden')
-            albumUI.querySelector('.status').textContent = 'An error occured creating this album.'
-            albumUI.querySelector('.button').classList.remove('hidden')
-            albumUI.querySelector('.button').textContent = 'Try again'
-        else
-            albumUI.querySelector('.status').classList.add('hidden')
-            albumUI.querySelector('.result a').textContent = window.location.origin + "/#{result.hash}"
-            albumUI.querySelector('.result a').href = window.location.origin + "/#{result.hash}"
-            albumUI.querySelector('.result').classList.remove('hidden')
-    )
-
-window.statusHook = (file, status, oldStatus) ->
-    if oldStatus?
-        return if status == 'ready' and oldStatus == 'done'
-    updateAlbumUI()
-
-createPreview = (name) ->
-    create = (element, className) ->
-        _ = document.createElement(element)
-        _.className = className
-        return _
-    container = create('div', 'media-preview')
-    preview = create('div', 'preview')
-    fade = create('div', 'respondive-fade')
-    title = create('h2', null)
-    title.title = name
-    title.textContent = name
-    flags = create('div', 'flags hidden')
-    status = create('div', 'status')
-    error = create('div', 'error hidden')
-    link = create('a', 'link hidden')
-    deleteLink = create('a', 'delete hidden')
-    deleteLink.textContent = 'Delete'
-    fullSize = create('a', 'full-size hidden')
-    progress = create('div', 'progress')
-    progress.style.width = 0
-    container.appendChild(preview)
-    container.appendChild(fade)
-    container.appendChild(title)
-    container.appendChild(flags)
-    container.appendChild(status)
-    container.appendChild(error)
-    container.appendChild(link)
-    container.appendChild(deleteLink)
-    container.appendChild(fullSize)
-    container.appendChild(progress)
-    return container
+worker = new Worker('/static/worker.js')
+window.backgroundWorker = worker
+albumAttached = false
+maxConcurrentUploads = 3
+
+window.addEventListener('DOMContentLoaded', ->
+    window.addEventListener('dragenter', dragNop, false)
+    window.addEventListener('dragleave', dragNop, false)
+    window.addEventListener('dragover', dragNop, false)
+    window.addEventListener('drop', handleDragDrop, false)
+    document.getElementById('browse-link').addEventListener('click', (e) ->
+        e.preventDefault()
+        document.getElementById('browse').click()
+    , false)
+    document.getElementById('browse').addEventListener('change', (e) ->
+        handleFiles(e.target.files)
+    , false)
+    albumUI = document.getElementById('albumUI')
+    albumUI.querySelector('.button').addEventListener('click', (e) ->
+        e.preventDefault()
+        albumUI.querySelector('.button').classList.add('hidden')
+        albumUI.querySelector('.status').classList.remove('hidden')
+        albumUI.querySelector('.status').textContent = 'Processing, please wait...'
+        albumAttached = true
+        createAlbum()
+    , false)
+
+    worker.addEventListener('message', handleWorkerMessage)
+    worker.postMessage({ action: 'load' })
+    window.statusChange = (file, status, oldStatus) ->
+        if oldStatus == 'uploading'
+            uploadPendingItems()
+    # We kick this manually every so often to make sure nothing gets abandoned in the 'Pending' state
+    setInterval(uploadPendingFiles, 10000)
+
+    pasteTarget = document.getElementById('paste-target')
+    pasteTarget.addEventListener('paste', handlePaste, false)
+    forceFocus()
+
+    historyEnabled = document.getElementById('historyEnabled')
+    if not UserHistory.getHistoryEnabled()
+        historyEnabled.textContent = 'Enable local history'
+    historyEnabled.addEventListener('click', (e) ->
+        e.preventDefault()
+        if UserHistory.toggleHistoryEnabled()
+            historyEnabled.textContent = 'Disable local history'
+        else
+            historyEnabled.textContent = 'Enable local history'
+    , false)
+
+    items = UserHistory.getHistory()[..].reverse()[..4]
+    historyContainer = document.getElementById('history')
+    historyList = historyContainer.querySelector('ul')
+    blurb = document.getElementById('blurb')
+    if items.length != 0
+        spinner = document.createElement('div')
+        spinner.className = 'progress'
+        blurb.appendChild(spinner)
+        UserHistory.loadDetailedHistory(items, (result) ->
+            blurb.classList.add('hidden')
+            historyContainer.classList.remove('hidden')
+            spinner.parentElement.removeChild(spinner)
+            for item in items
+                if result[item]
+                    historyList.appendChild(createHistoryItem({ item: result[item], hash: item }))
+        )
+, false)
+
+forceFocus = ->
+    if document.activeElement.tagName in ['TEXTAREA', 'INPUT', 'IFRAME']
+        setTimeout(forceFocus, 250)
+        return
+    pasteTarget = document.getElementById('paste-target')
+    pasteTarget.focus()
+    setTimeout(forceFocus, 250)
+
+createHistoryItem = (h, noLink = false) ->
+    item = h.item
+    container = null
+    if h.base?
+        container = document.createElement(data.base)
+    else
+        container = document.createElement('li')
+    if item.blob_type == 'video'
+        preview = document.createElement('video')
+        preview.setAttribute('loop', 'true')
+        preview.poster = '/' + item.hash + '.jpg'
+        for file in item.files
+            if file.type.indexOf('video/') == 0
+                source = document.createElement('source')
+                source.src = window.cdn + file.file
+                source.type = file.type
+                preview.appendChild(source)
+        preview.volume = 0
+        preview.className = 'item-view'
+        preview.onmouseenter = (e) ->
+            e.target.play()
+        preview.onmouseleave = (e) ->
+            e.target.pause()
+    else if item.blob_type == 'image'
+        preview = document.createElement('img')
+        for file in item.files
+            if not file.type.indexOf('image/') == 0
+                continue
+            preview.src = window.cdn + file.file
+            break
+        preview.className = 'item-view'
+    else if item.blob_type == 'audio'
+        preview = document.createElement('img')
+        preview.src = '/static/audio-player-narrow.png'
+        preview.className = 'item-view'
+    else if item.type == 'application/album'
+        preview = document.createElement('div')
+        preview.className = 'album-preview'
+        for file in item.files
+            preview.appendChild(createHistoryItem(file, true))
+    if preview
+        if not noLink
+            a = document.createElement('a')
+            a.href = '/' + h.hash
+            a.target = '_blank'
+            a.appendChild(preview)
+            container.appendChild(a)
+    return container
+
+window.onbeforeunload = ->
+    for f of uploadedFiles
+        if uploadedFiles[f].status not in ['done', 'error', 'ready']
+            return 'If you leave this page, your uploads will be cancelled.'
+
+handleWorkerMessage = (e) ->
+    if e.data.execute?
+        eval(e.data.execute)
+    if e.data.event?
+        switch e.data.event
+            when 'file-status-change' then fileStatusChanged(e.data)
+
+dragNop = (e) ->
+    e.stopPropagation()
+    e.preventDefault()
+
+handleDragDrop = (e) ->
+    dragNop(e)
+    droparea = document.getElementById('droparea')
+    droparea.classList.remove('hover') if droparea.classList.contains('hover')
+    files = e.dataTransfer.files
+    handleFiles(files) if files.length > 0
+
+pendingFiles = []
+updateQueue = ->
+    files = pendingFiles.splice(0, 5)
+    urls = pendingUrls.splice(0, 5)
+    fileList = document.getElementById('files')
+    scrollingContainer = document.getElementById('droparea')
+    for file in files
+        ((file) ->
+            mediaFile = new MediaFile(file)
+            mediaFile.preview = createPreview(file.name)
+            _ = scrollingContainer.scrollTop
+            fileList.appendChild(mediaFile.preview)
+            scrollingContainer.scrollTop = _
+            mediaFile.preview = fileList.lastElementChild
+            mediaFile.loadPreview()
+            mediaFile.hash = new String(guid())
+            mediaFile.updateStatus('local-pending')
+            uploadedFiles[mediaFile.hash] = mediaFile
+        )(file)
+    for url in urls
+        ((url) ->
+            mediaFile = new MediaFile(url)
+            mediaFile.preview = createPreview(mediaFile.name)
+            _ = scrollingContainer.scrollTop
+            fileList.appendChild(mediaFile.preview)
+            scrollingContainer.scrollTop = _
+            mediaFile.preview = fileList.lastElementChild
+            mediaFile.loadPreview()
+            mediaFile.hash = new String(guid())
+            mediaFile.updateStatus('local-pending')
+            uploadedFiles[mediaFile.hash] = mediaFile
+        )(url)
+    if pendingFiles.length + pendingUrls.length > 0
+        setTimeout(updateQueue, 500)
+    if files.length > 0
+        uploadPendingFiles()
+
+handleFiles = (files) ->
+    if albumAttached
+        albumUI.querySelector('.button').classList.add('hidden')
+        albumUI.querySelector('.status').classList.remove('hidden')
+        albumUI.querySelector('.status').textContent = 'Processing, please wait...'
+        albumUI.querySelector('.result').classList.add('hidden')
+    if Object.keys(uploadedFiles).length == 0
+        document.getElementById('files').innerHTML = ''
+        dropArea = document.getElementById('droparea')
+        dropArea.style.overflowY = 'scroll'
+        dropArea.classList.add('files')
+    pendingFiles.push(file) for file in files
+    updateQueue()
+window.handleFiles = handleFiles
+
+uploadPendingFiles = ->
+    toUpload = []
+    uploading = 0
+    for hash, file of uploadedFiles
+        if file.status in ['preparing', 'uploading']
+            uploading++
+            return if uploading >= maxConcurrentUploads
+        else if file.status == 'local-pending' and toUpload.length < 5
+            toUpload.push(file)
+    for file in toUpload
+        ((file) ->
+            if file.isUrl
+                uploadUrlFile(file)
+            else
+                reader = new FileReader()
+                reader.onloadend = (e) ->
+                    try
+                        data = e.target.result
+                        file.updateStatus('preparing')
+                        worker.postMessage({ action: 'compute-hash', data: data, callback: 'hashCompleted', id: file.hash })
+                    catch e # Too large
+                        uploadFile(file)
+                reader.readAsBinaryString(file.file)
+        )(file)
+
+hashCompleted = (id, result) ->
+    file = uploadedFiles[id]
+    file.hash = result
+    delete uploadedFiles[id]
+    uploadedFiles[result] = file
+    file.isHashed = true
+    uploadFile(file)
+
+handlePaste = (e) ->
+    target = document.getElementById('paste-target')
+    if e.clipboardData
+        text = e.clipboardData.getData('text/plain')
+        if text
+            if text.indexOf('http://') == 0 or text.indexOf('https://') == 0
+                urls = text.split('\n')
+                uploadUrls(url.trim() for url in urls when url.indexOf('http://') == 0 or url.indexOf('https://') == 0)
+                target.innerHTML = ''
+            else
+                # todo: plaintext
+        else
+            if e.clipboardData.items # webkit
+                for item in e.clipboardData.items
+                    if item.type.indexOf('image/') == 0
+                        file = item.getAsFile
+                        file.name = 'Clipboard'
+                        handleFiles([ file ])
+            else # not webkit
+                check = ->
+                    if target.innerHTML != ''
+                        img = target.firstChild.src
+                        if img.indexOf('data:image/png;base64,') == 0
+                            blob = dataURItoBlob(img)
+                            blob.name = 'Clipboard'
+                            handleFiles([ blob ])
+                        target.innerHTML = ''
+                    else
+                        setTimeout(check, 100)
+                check()
+
+pendingUrls = []
+uploadUrls = (urls) ->
+    if albumAttached
+        albumUI.querySelector('.button').classList.add('hidden')
+        albumUI.querySelector('.status').classList.remove('hidden')
+        albumUI.querySelector('.status').textContent = 'Processing, please wait...'
+        albumUI.querySelector('.result').classList.add('hidden')
+    if Object.keys(uploadedFiles).length == 0
+        document.getElementById('files').innerHTML = ''
+        dropArea = document.getElementById('droparea')
+        dropArea.style.overflowY = 'scroll'
+        dropArea.classList.add('files')
+    pendingUrls.push(url) for url in urls
+    updateQueue()
+
+uploadUrlFile = (file) ->
+    oldHash = file.hash
+    API.uploadUrl(file, (result) ->
+        uploadedFiles[file.hash] = file
+        if result.error?
+            file.setError(result.error)
+            return
+        file = result.file
+        delete uploadedFiles[oldHash]
+        uploadedFiles[file.hash] = file
+        if file.status == 'done'
+            finish(file)
+        else
+            file.isUserOwned = true
+            worker.postMessage({ action: 'monitor-status', hash: file.hash })
+    )
+
+uploadFile = (file) ->
+    oldHash = file.hash
+    upload = ->
+        file.updateStatus('uploading')
+        API.uploadFile(file, (e) ->
+            if e.lengthComputable
+                file.updateProgress(e.loaded / e.total)
+        , (result) ->
+            file.file = null
+            if result.error?
+                file.setError(result.error)
+                return
+            if file.hash != oldHash # for larger files, the server does the hashing for us
+                delete uploadedFiles[oldHash]
+                uploadedFiles[file.hash] = file
+            if file.status == 'done'
+                finish(file)
+            else
+                file.isUserOwned = true
+                worker.postMessage({ action: 'monitor-status', hash: file.hash })
+        )
+    if file.isHashed
+        API.checkExists(file, (exists) ->
+            if exists
+                file.file = null
+                file.isUserOwned = false
+                file.updateStatus('done')
+                finish(file)
+                return
+            else
+                upload()
+        )
+
+fileStatusChanged = (e) ->
+    uploadedFiles[e.hash].updateStatus(e.status)
+    if e.file? and e.file.flags?
+        uploadedFiles[e.hash].setFlags(e.file.flags)
+    if e.status in ['ready', 'done']
+        uploadedFiles[e.hash].blob = e.file
+        finish(uploadedFiles[e.hash])
+
+finish = (file) ->
+    file.finish()
+    updateAlbumUI()
+
+updateAlbumUI = ->
+    if albumAttached
+        createAlbum()
+    else
+        keys = []
+        for f, v of uploadedFiles
+            if v.status in ['processing', 'pending', 'ready', 'done', 'uploading']
+                keys.push(f)
+        albumUI = document.getElementById('albumUI')
+        if keys.length >= 2
+            albumUI.querySelector('.button').classList.remove('hidden')
+        else
+            albumUI.querySelector('.button').classList.add('hidden')
+
+createAlbum = ->
+    keys = []
+    for f, v of uploadedFiles
+        if v.status in ['done', 'ready']
+            keys.push(v)
+        else
+            return
+    return unless keys.length >= 2
+    keys.sort((a, b) -> a.index - b.index)
+    keys = (a.hash for a in keys)
+    API.createAlbum(keys, (result) ->
+        albumUI = document.getElementById('albumUI')
+        if result.error?
+            albumUI.querySelector('.status').classList.remove('hidden')
+            albumUI.querySelector('.status').textContent = 'An error occured creating this album.'
+            albumUI.querySelector('.button').classList.remove('hidden')
+            albumUI.querySelector('.button').textContent = 'Try again'
+        else
+            albumUI.querySelector('.status').classList.add('hidden')
+            albumUI.querySelector('.result a').textContent = window.location.origin + "/#{result.hash}"
+            albumUI.querySelector('.result a').href = window.location.origin + "/#{result.hash}"
+            albumUI.querySelector('.result').classList.remove('hidden')
+    )
+
+window.statusHook = (file, status, oldStatus) ->
+    if oldStatus?
+        return if status == 'ready' and oldStatus == 'done'
+    updateAlbumUI()
+
+createPreview = (name) ->
+    create = (element, className) ->
+        _ = document.createElement(element)
+        _.className = className if className?
+        return _
+    container = create('div', 'media-preview')
+    preview = create('div', 'preview')
+    fade = create('div', 'respondive-fade')
+    title = create('h2')
+    title.title = name
+    title.textContent = name
+    flags = create('div', 'flags hidden')
+    status = create('div', 'status')
+    error = create('div', 'error hidden')
+    link = create('a', 'link hidden')
+    deleteLink = create('a', 'delete hidden')
+    deleteLink.textContent = 'Delete'
+    fullSize = create('a', 'full-size hidden')
+    progress = create('div', 'progress')
+    progress.style.width = 0
+    container.appendChild(preview)
+    container.appendChild(fade)
+    container.appendChild(title)
+    container.appendChild(flags)
+    container.appendChild(status)
+    container.appendChild(error)
+    container.appendChild(link)
+    container.appendChild(deleteLink)
+    container.appendChild(fullSize)
+    container.appendChild(progress)
+    return container


planのリファクタリング

planのリファクタリング

--- scripts/plan.coffee
+++ scripts/plan.coffee
@@ -67,8 +67,11 @@

   robot.adapter.client.on 'raw_message', (message) ->
     return if message.type isnt 'reaction_added' && message.type isnt 'reaction_removed'
+    return if message.reaction isnt 'o'

     robotId = robot.adapter.client.getUserByName(robot.name).id
+    return if message.user is robotId
+
     user = robot.adapter.client.getUserByID(message.user)
     parties = storage.getParties(robot)
     ts = message.item.ts
@@ -78,22 +81,15 @@
       if item.timestamp is ts && item.channelId is channelId
         party = item

-    if party == null
-      return
+    return if party == null

-    if (/^reaction_added$/.test message.type) && (message.user isnt robotId)
-      if /^o$/.test message.reaction
-        ms = "#{user.name}さんが参加しました。"
-        postMessage(ms, message.item.channel)
-        party.participants.push(user.name)
-        storage.updateParty(robot, party)
-      #else if /^x$/.test message.reaction
-        #postMessage('不参加', message.item.channel)
-    else if (/^reaction_removed$/.test message.type) && (message.user isnt robotId)
-      if /^o$/.test message.reaction
-        ms = "#{user.name}さんがキャンセルしました。"
-        postMessage(ms, message.item.channel)
-        party.participants.pop(user.name)
-        storage.updateParty(robot, party)
-      #else if /^x$/.test message.reaction
-        #postMessage('不参加キャンセル', message.item.channel)
+    if message.type is 'reaction_added'
+      ms = "#{user.name}さんが参加しました。"
+      postMessage(ms, message.item.channel)
+      party.participants.push(user.name)
+      storage.updateParty(robot, party)
+    else if message.type is 'reaction_removed'
+      ms = "#{user.name}さんがキャンセルしました。"
+      postMessage(ms, message.item.channel)
+      party.participants.pop(user.name)
+      storage.updateParty(robot, party)


it woooorrrrrksssss.

it woooorrrrrksssss.

--- src/device/device-mailer.coffee
+++ src/device/device-mailer.coffee
@@ -34,10 +34,10 @@
       return callback(error) if error?
       meshblu.updateDangerously userDeviceUuid, {$set: {encryptedOptions: encryptedOptions}, $unset: {options: true}}, callback

-  @processMessage: ({auth, encryptedOptions, message}, callback) ->
+  @processMessage: ({userDeviceUuid, auth, encryptedOptions, message}, callback) ->
     meshblu = new MeshbluHttp auth
     unless encryptedOptions?
-      meshblu.message {devices: ['*'], result: {error: 'encrypted options not found'}}, {}, callback
+      meshblu.message {devices: ['*'], result: {error: 'encrypted options not found'}}, as: userDeviceUuid, callback

     MailerService._decryptOptions encryptedOptions, (error, options) =>
       {transportOptions, transporter} = options
@@ -45,7 +45,7 @@
         transportOptions = require("nodemailer-#{transporter}-transport")(transportOptions)

       nodemailer.createTransport(transportOptions).sendMail message, (err, info) =>
-        meshblu.message {devices: ['*'], result: {error: err?.message,info}}, {}, callback
+        meshblu.message {devices: ['*'], result: {error: err?.message,info}}, as: userDeviceUuid, callback

   @_encryptOptions: (options, callback) =>
     encryptedOptions = AESCrypt.encrypt JSON.stringify options


:fire: Remove unused require

:fire: Remove unused require

--- src/components/sidebar.cjsx
+++ src/components/sidebar.cjsx
@@ -1,5 +1,4 @@
 React = require 'react'
-ReactDOM = require 'react-dom'

 Application = require '../flux/common/application'
 ApplicationActions = require '../flux/actions/application'


Allow either one or two digits

Allow either one or two digits

--- src/components/tags/lo.cjsx
+++ src/components/tags/lo.cjsx
@@ -8,8 +8,8 @@
     exerciseId: React.PropTypes.string.isRequired

   validateInput: (value) ->
-    'Must match CNX feature' unless value.match(
-      /^\d+-\d+-\d+$/
+    'Must match LO pattern of dd-dd-dd' unless value.match(
+      /^\d{1,2}-\d{1,2}-\d{1,2}$/
     )

   cleanInput: (val) ->


fix(menus): Adopt new `pasteandmatchstyle` menu role

--- src/components/contenteditable/contenteditable.cjsx
+++ src/components/contenteditable/contenteditable.cjsx
@@ -409,9 +409,7 @@
     menu.append(new MenuItem({ label: 'Cut', role: 'cut'}))
     menu.append(new MenuItem({ label: 'Copy', role: 'copy'}))
     menu.append(new MenuItem({ label: 'Paste', role: 'paste'}))
-    menu.append(new MenuItem({ label: 'Paste and Match Style', click: =>
-      NylasEnv.getCurrentWindow().webContents.pasteAndMatchStyle()
-    }))
+    menu.append(new MenuItem({ label: 'Paste and Match Style', role: 'pasteandmatchstyle'}))
     menu.popup(remote.getCurrentWindow())


Modal

Modal

- New parameters: fill_screen_button_tooltip, cancel_tooltip

--- src/elements/Modal/Modal.coffee
+++ src/elements/Modal/Modal.coffee
@@ -14,11 +14,12 @@
 	constructor: (@opts={}) ->
 		super(@opts)

-		@__addHeaderButton("fill_screen_button", Pane.getToggleFillScreenButton())
+		@__addHeaderButton("fill_screen_button", Pane.getToggleFillScreenButton(tooltip: @_fill_screen_button_tooltip))

 		@__addHeaderButton "cancel",
 			class: "ez5-modal-close-button"
 			icon:  "close"
+			tooltip: @_cancel_tooltip
 			appearance: if CUI.__ng__ then "normal" else "flat"
 			onClick: (ev) =>
 				@doCancel(ev)
@@ -36,11 +37,15 @@
 			cancel_action:
 				default: "destroy"
 				check: ["destroy", "hide"]
+			cancel_tooltip:
+				check: "PlainObject"
 			onCancel:
 				check: Function
 			# show a fill screen button
 			fill_screen_button:
 				check: Boolean
+			fill_screen_button_tooltip:
+				check: "PlainObject"
 			onToggleFillScreen:
 				check: Function


CSS optimization & minify added by CSSO when compile production.

--- app/templates/_Gruntfile.coffee
+++ app/templates/_Gruntfile.coffee
@@ -77,7 +77,7 @@
         options:
           report: 'min'
         files:
-          'app/compile/css/application.min.css' : ['app/compile/css/application.css']
+          'app/production/css/application.css' : ['app/production/css/application.css']

     imagemin:
       dynamic:
@@ -95,7 +95,7 @@
     concat_css:
       all:
         src: 'app/compile/css/**/*.css'
-        dest: 'app/production/css/concat_application.css'
+        dest: 'app/production/css/application.css'

     watch:
       options:
@@ -121,7 +121,7 @@

   <% if (datauri) { %>grunt.loadNpmTasks "grunt-datauri"<% } %>

-  grunt.registerTask "make_production", ["concat_css", "imagemin"]
+  grunt.registerTask "make_production", ["concat_css", "csso", "imagemin"]

   grunt.registerTask "server", "connect"
   grunt.registerTask "default", "watch"


Special-handling for cross_validation_models

--- src/ext/modules/routines.coffee
+++ src/ext/modules/routines.coffee
@@ -130,12 +130,24 @@
     target[i] = if element? then element else undefined
   target

-parseAndFormat = (source) ->
+parseAndFormatArray = (source) ->
   target = new Array source.length
   for element, i in source
     target[i] = if element?
       if isNumber element
         format6fi element
+      else
+        element
+    else
+      undefined
+  target
+
+parseAndFormatObjectArray = (source) ->
+  target = new Array source.length
+  for element, i in source
+    target[i] = if element?
+      if element.__meta?.schema_type is 'Key<Keyed>'
+        "#{escape element.name}"
       else
         element
     else
@@ -468,7 +480,12 @@
       origin: origin

   inspectRawArray_ = (name, origin, description, array) -> ->
-    createDataframe name, [createList name, parseAndFormat array], (sequence array.length), null,
+    createDataframe name, [createList name, parseAndFormatArray array], (sequence array.length), null,
+      description: ''
+      origin: origin
+
+  inspectObjectArray_ = (name, origin, description, array) -> ->
+    createDataframe name, [createList name, parseAndFormatObjectArray array], (sequence array.length), null,
       description: ''
       origin: origin

@@ -556,7 +573,10 @@
           inspections["#{name} - #{v.name}"] = inspectTwoDimTable_ origin, "#{name} - #{v.name}", v
         else
           if isArray v
-            inspections[k] = inspectRawArray_ k, origin, k, v
+            if k is 'cross_validation_models' # megahack
+              inspections[k] = inspectObjectArray_ k, origin, k, v
+            else
+              inspections[k] = inspectRawArray_ k, origin, k, v
           else if isObject v
             if meta = v.__meta
               if meta.schema_type is 'Key<Frame>'
@@ -591,9 +611,6 @@
     render_ model, H2O.ModelOutput, model

   extendModels = (models) ->
-    # for model in models
-    #  extendModel model
-
     inspections = {}

     algos = unique (model.algo for model in models)


GameObject.Effect

GameObject.Effect

--- src/floating_text_effect.coffee
+++ src/floating_text_effect.coffee
@@ -10,7 +10,8 @@
       y: 50

 @see TextEffect
-@name FloatingTextEffect
+@name Floating
+@fieldOf
 @constructor
 ###
 TextEffect.Floating = (I={}) ->