Teamwork: Options are merging with defaults.

--- client/app/Applications/Teamwork.kdapplication/Views/teamworkapp.coffee
+++ client/app/Applications/Teamwork.kdapplication/Views/teamworkapp.coffee
@@ -4,31 +4,38 @@

     super options, data

+    options               = @getOptions()
     instanceName          = if location.hostname is "localhost" then "teamwork-local" else "kd-prod-1"
+    additionalButton      =
+      title               : "Environments"
+      cssClass            : "clean-gray"
+      callback            : (panel, workspace) => @showEnvironmentsModal panel, workspace
+
     @teamwork             = new TeamworkWorkspace
-      name                : "Teamwork"
-      version             : "0.1"
-      joinModalTitle      : "Join a coding session"
-      joinModalContent    : "
Paste the session key that you received and start coding together.
"
-      shareSessionKeyInfo : "
This is your session key, you can share this key with your friends to work together.
"
-      firebaseInstance    : instanceName
-      sessionKey          : @getOptions().sessionKey
+      name                : options.name                or "Teamwork"
+      joinModalTitle      : options.joinModalTitle      or "Join a coding session"
+      joinModalContent    : options.joinModalContent    or "
Paste the session key that you received and start coding together.
"
+      shareSessionKeyInfo : options.shareSessionKeyInfo or "
This is your session key, you can share this key with your friends to work together.
"
+      firebaseInstance    : options.firebaseInstance    or instanceName
+      sessionKey          : options.sessionKey
       panelClass          : TeamworkPanel
       delegate            : this
-      panels              : [
+      environment         : options.environment         or null
+      panels              : options.panels              or [
         title             : "Teamwork"
         hint              : "
This is a collaborative coding environment where you can team up with others and work on the same code.
"
         buttons           : [
           {
             title         : "Tools"
-            cssClass      : "clean-gray"
-            callback      : (panel, workspace) => @showToolsModal panel, workspace
+            cssClass      : "clean-gray tw-tools-button"
+            callback      : => @showToolsModal @teamwork.getActivePanel(), @teamwork
           }
+          # additionalButton
         ]
         floatingPanes     : [ "chat" , "terminal", "preview" ]
         layout            :
           direction       : "vertical"
-          sizes           : [ "25%", null ]
+          sizes           : [ "250px", null ]
           splitName       : "BaseSplit"
           views           : [
             {


FinderController: Minor check to use FinderController with any other tree controller instance.

--- client/landing/app/MainApp/filetree/controllers/findercontroller.coffee
+++ client/landing/app/MainApp/filetree/controllers/findercontroller.coffee
@@ -22,7 +22,8 @@

     super options, data

-    @treeController = new NFinderTreeController treeOptions, []
+    TreeControllerClass = options.treeControllerClass or NFinderTreeController
+    @treeController     = new TreeControllerClass treeOptions, []

     if options.useStorage


sample json for test

sample json for test

--- test/schema-generator-test.coffee
+++ test/schema-generator-test.coffee
@@ -7,35 +7,32 @@

 sampleJson = '''
 {
-  "items": [
-    [ { "url": "/shopping-cart/1", "product":"2ZY48XPZ", "quantity": 1, "name": "New socks", "price": 1.25 },
-      { "url": "/shopping-cart/2", "product":"2ZY48XPZaa", "quantity": 2, "name": "New socks2", "price": 1.99 }
-    ],
-    [ { "url": "/shopping-cart/1", "product":"2ZY48XPZ", "quantity": 1, "name": "New socks", "price": 1.25 },
-    { "url": "/shopping-cart/2", "product":"2ZY48XPZaa", "quantity": 2, "name": "New socks2", "price": 1.99 }
-    ]
+  "simple_key_value_pair": "simple_key_value_pair_value",
+  "complex_key_value_pair": {
+    "complex_key_value_pair_key1" : "complex_key_value_pair_value1",
+    "complex_key_value_pair_key2" : "complex_key_value_pair_value2",
+    "complex_key_value_pair_key3" : {
+      "complex_key_value_pair_key1_in_nested_hash": "complex_key_value_pair_value1_in_nested_hash",
+      "complex_key_value_pair_key2_in_nested_hash": "complex_key_value_pair_value2_in_nested_hash"
+      }
+    },
+
+  "array_of_hashes": [
+    {
+      "array_of_hashes_item1_key1": "array_of_hashes_item1_value1",
+      "array_of_hashes_item1_key2": "array_of_hashes_item1_value2"
+    },
+    {
+      "array_of_hashes_item2_key1": "array_of_hashes_item2_value1",
+      "array_of_hashes_item2_key2": "array_of_hashes_item2_value2"
+    }
+
   ],
-  "item2" : {
-  "item2_item1" : {"nested1": 1},
-  "item2_item1" : {"nested2": {"a": 2}}
-  }
+  "array_of_mixed_simple_types": [1,2,"a","b"],
+  "array_of_same_simple_types": ["a","b"]
 }
 '''

-sampleJsonWrong = '''{
-"items": [
-  [ { "url": "/shopping-cart/1", "product":"2ZY48XaPZ", "quantity": 1, "name": "New socks", "price": 1.25 },
-    { "url": "/shopping-cart/2", "product":"2ZY48XPZaa", "quantity": 2, "name": "New socks2", "price": 1.99 }
-  ],
-  [ { "url": "/shopping-cart/1", "product":"2ZY48XPZ", "quantity": 1, "name": "New socks", "price": 1.25 },
-    { "url": "/shopping-cart/2", "product":"2ZY48XPZaa", "quantity": 2, "name": "New socks2", "price": 1.99 }
-  ]
-],
-"item2" : {
-"item2_item1" : {"nested1": 1},
-"item2_item1" : {"nested2": {"a": 2}}
-}
-}'''

 {SchemaGenerator, SchemaProperties} = require('../src/schema-generator')


panelframe: revise treatment of box

panelframe: revise treatment of box

--- src/panelframe.coffee
+++ src/panelframe.coffee
@@ -209,18 +209,19 @@
         # background rectangle boxes
         boxes2include = (i for i of boxes.left when boxes.width[i] > 0 and boxes.height[i]>0)
         box = svg.append("g").attr("id", "box")
-                 .selectAll("empty")
-                 .data(boxes2include)
-                 .enter()
-                 .append("rect")
-                 .attr("x", (i) -> boxes.left[i])
-                 .attr("y", (i) -> boxes.top[i])
-                 .attr("height", (i) -> boxes.height[i])
-                 .attr("width", (i) -> boxes.width[i])
-                 .attr("fill", "none")
-                 .attr("stroke", boxcolor)
-                 .attr("stroke-width", boxwidth)
-                 .attr("shape-rendering", "crispEdges")
+
+        box.selectAll("empty")
+           .data(boxes2include)
+           .enter()
+           .append("rect")
+           .attr("x", (i) -> boxes.left[i])
+           .attr("y", (i) -> boxes.top[i])
+           .attr("height", (i) -> boxes.height[i])
+           .attr("width", (i) -> boxes.width[i])
+           .attr("fill", "none")
+           .attr("stroke", boxcolor)
+           .attr("stroke-width", boxwidth)
+           .attr("shape-rendering", "crispEdges")

     # functions to grab stuff
     chart.xscale = () -> xscale_wnull


Remove unnecessary template line in directive test

--- karma/test-directives.coffee
+++ karma/test-directives.coffee
@@ -66,7 +66,6 @@
   describe('cpTooltip', () ->

     it('adds an element with a tooltip to the dom', () ->
-      $templateCache.put('/sitestatic/templates/tooltip.html')
       $scope = $rootScope.$new()

       template = $compile('<cp-tooltip display-text="hello" tooltip-text="world" position="top" ></cp-tooltip>')


Increase Game.VELOCITY to 100

Increase Game.VELOCITY to 100

--- game.coffee
+++ game.coffee
@@ -26,7 +26,7 @@
     @WIDTH:  200
     @HEIGHT:  300

-    @VELOCITY = 1
+    @VELOCITY = 100

     @use_collisions = true


Enable test for plain argument

Enable test for plain argument

--- test/elvis-backbone-test.coffee
+++ test/elvis-backbone-test.coffee
@@ -1,12 +1,12 @@
 el = @elvis

 describe 'el.backbone.model', ->
-  # it 'can handle basic bindings', ->
-  #   model = new Backbone.Model(foo: 'bar')
-  #   element = el('div', el.bind(model, 'foo'))
-  #   expect(element.innerHTML).to.equal('bar')
-  #   model.set(foo: 'quux')
-  #   expect(element.innerHTML).to.equal('quux')
+  it 'can handle basic bindings', ->
+    model = new Backbone.Model(foo: 'bar')
+    element = el('div', el.bind(model, 'foo'))
+    expect(element.innerHTML).to.equal('bar')
+    model.set(foo: 'quux')
+    expect(element.innerHTML).to.equal('quux')

   it 'can handle binding in array', ->
     model = new Backbone.Model(foo: 'bar')


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- test/function_extensions.coffee
+++ test/function_extensions.coffee
@@ -27,7 +27,7 @@
   addScore = ->
     score += 100

-  onceScore =
+  onceScore = score.once

   onceScore()
   onceScore()


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- test/function_extensions.coffee
+++ test/function_extensions.coffee
@@ -21,5 +21,13 @@
   testFn()
   ok calledAfter, "Called after"

+asyncTest "#debounce", 1, ->
+  fn = (-> ok true; start()).debounce(50)
+
+  # Though called multiple times the function is only triggered once
+  fn()
+  fn()
+  fn()
+
 module()


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- test/function_extensions.coffee
+++ test/function_extensions.coffee
@@ -24,7 +24,7 @@
 test "#once", ->
   score = 0

-  ad
+  addScore =

   onceScore = ->
     ( ->


Agent pie: start/end angle pie transition work

--- widgets/fullpie_agent/fullpie_agent.coffee
+++ widgets/fullpie_agent/fullpie_agent.coffee
@@ -53,7 +53,7 @@
         @update(data.data)

     update: (dataSet) ->
-
+        @testData = @testData + 100
         # Remove any previous svg
         $(@node).children('svg').remove()
         # Define the container


fullpieagent testing

fullpieagent testing

--- widgets/fullpie_agent/fullpie_agent.coffee
+++ widgets/fullpie_agent/fullpie_agent.coffee
@@ -79,8 +79,8 @@
         if totalLabel.empty()
             totalLabel = svg.append('g').attr('class', 'totalLabel')

-        path = pathGroup.selectAll('path.pie').data(piedata)
-        path.enter().append('path').attr('class', 'pie').attr('fill', (d, i) -> color i).attr('filter','url(#dropshadow)')
+        path = pathGroup.selectAll('path.pie').data(piedata).attr('filter','url(#dropshadow)')
+        path.enter().append('path').attr('class', 'pie').attr('fill', (d, i) -> color i)

         path.transition().duration(1500).attrTween 'd', (d,i) ->
             `var i`


fullpieagent testing

fullpieagent testing

--- widgets/fullpie_agent/fullpie_agent.coffee
+++ widgets/fullpie_agent/fullpie_agent.coffee
@@ -2,11 +2,8 @@

     @accessor 'data'

-    ready: ->
-        @oldPieData = ''
-
     onData: (data) ->
-        #$(@node).fadeOut().fadeIn()
+        $(@node).fadeOut().fadeIn()
         @update(data.data)

     update: (dataSet) ->
@@ -87,7 +84,7 @@

         path.transition().duration(1500).attrTween('d', (d,i) ->
             `var i`
-            theOldDataInPie = @oldPieData ? piedata
+            theOldDataInPie = oldPieData ? piedata
             # Interpolate the arcs in data space
             s0 = undefined
             e0 = undefined
@@ -277,5 +274,5 @@
             .attr('alignment-baseline', 'mathematical')
             .attr('filter','url(#dropshadow)')

-        @oldPieData = piedata
+        oldPieData = piedata
         return


fullpieagent testing

fullpieagent testing

--- widgets/fullpie_agent/fullpie_agent.coffee
+++ widgets/fullpie_agent/fullpie_agent.coffee
@@ -190,8 +190,7 @@
       d.y = -Math.cos(a) * (that.radius - 20)
       d.b = d.oy = d.y + 5
       d.t = d.y - bbox.height - 5
-      console.log 'd.x', d.x
-      console.log 'd.y', d.y
+      console.log 'd', d
       ### check whether the default position
          overlaps any other labels
       ###


fullpieagent testing

fullpieagent testing

--- widgets/fullpie_agent/fullpie_agent.coffee
+++ widgets/fullpie_agent/fullpie_agent.coffee
@@ -23,8 +23,8 @@
         radiuso = 130                           # Outer radius of the pie
         radiusi = 65                            # Inner radius of the pie (zero = pie, non-zero = donut)
         color = d3.scale.category20()           # Color scale for pie slices
-        dropshadowx = 4                         # X-offset for drop shadow filter
-        dropshadowy = 4                         # Y-offset for drop shadow filter
+        dropshadowx = 3                         # X-offset for drop shadow filter
+        dropshadowy = 3                         # Y-offset for drop shadow filter
         dropshadowblur = "1.1"                  # [STRING] Blur value for drop shadow filter

         pie = d3.layout.pie().value((d) -> d.value)
@@ -109,7 +109,7 @@
             (t) ->
                 b = i(t)
                 return arc b
-        ).attr('filter','url(#dropshadow)')
+        )
         path.exit()
             .transition()
             .duration(300)


Closes #18

Closes #18

--- lib/taxonomies/taxonomies.service.coffee
+++ lib/taxonomies/taxonomies.service.coffee
@@ -37,7 +37,7 @@

         if slug
             $filter('translate') trans,
-                name: slug
+                name: decodeURIComponent slug
         else
             $filter('translate') trans


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- test/engine.coffee
+++ test/engine.coffee
@@ -64,6 +64,8 @@

   engine.add "GameObject",
     test: true
+
+

   ok engine.first("GameObject")


Don't throw errors from LDAP unless they're real errors (ldapjs throws errors for login failures, which is not ideal)

--- server/models/user.coffee
+++ server/models/user.coffee
@@ -120,6 +120,8 @@
   )
 )

+KNOWN_LDAP_ERRORS = ['InvalidCredentialsError']
+
 userSchema.methods.loginFromLDAP = (password, done) ->
   ldap = require('ldapjs')
   ldapConfig = getLDAPConfig()
@@ -130,7 +132,10 @@

   client.bind(@distinguishedName, password, (err) =>
     if err?
-      done(err, false)
+      if err.name in KNOWN_LDAP_ERRORS
+        done(null, false, {message: "Incorrect username or password"})
+      else
+        done(err, false)
     else
       done(null, @)
   )


cover usage of the "last" argument

cover usage of the "last" argument

--- test/phabs_commands_test.coffee
+++ test/phabs_commands_test.coffee
@@ -965,6 +965,12 @@
           it 'reports the status as open', ->
             expect(hubotResponse(3)).to.eql 'Ok, T42 now has status open.'

+        context 'phab open', ->
+          hubot 'phab T42', 'user_with_phid'
+          hubot 'phab last open', 'user_with_phid'
+          it 'reports the status as open', ->
+            expect(hubotResponse(3)).to.eql 'Ok, T42 now has status open.'
+

       context 'phab T42 is open', ->
         beforeEach ->
@@ -978,6 +984,11 @@

         context 'phab open', ->
           hubot 'phab open'
+          it 'warns the user that there is no active task in memory', ->
+            expect(hubotResponse()).to.eql "Sorry, you don't have any task active right now."
+
+        context 'phab last open', ->
+          hubot 'phab last open'
           it 'warns the user that there is no active task in memory', ->
             expect(hubotResponse()).to.eql "Sorry, you don't have any task active right now."


Follow mixin

Follow mixin

--- test/follow.coffee
+++ test/follow.coffee
@@ -37,5 +37,6 @@
     height: 10

   rightEnemy.include(Follow)
+  rightEne

 module()


Minor fixes to email system

Minor fixes to email system

--- server/routes/mail.coffee
+++ server/routes/mail.coffee
@@ -42,7 +42,7 @@
   lockDurationMs = 2 * 60 * 1000
   currentDate = new Date()
   timeRanges = []
-  for weekPair in [[4, 2,'two weeks'], [8, 4, 'four weeks'], [8, 52, 'eight weeks']]
+  for weekPair in [[4, 2,'two weeks'], [8, 4, 'four weeks'], [52, 8, 'eight weeks']]
     timeRanges.push
       start: generateWeekOffset currentDate, weekPair[0]
       end: generateWeekOffset currentDate, weekPair[1]
@@ -252,7 +252,7 @@

 employersEmailedDigestMoreThanWeekAgoFilter = (employer, cb) ->
   if employer.emails?.employerNotes?.enabled is false
-    return sentEmailFilterCallback true
+    return cb true
   findParameters =
     "user": employer._id
     "mailTask": @mailTaskName


new query experiment

new query experiment

--- server/search_core.coffee
+++ server/search_core.coffee
@@ -18,7 +18,6 @@
 if nconf.env().get('NODE_ENV')
   console.log 'Running Production Mode'
   nconf.env().argv().file process.env.PWD  + '/config.json'
-#  app.use express.static process.env.PWD  + '/../presentation/dist'
 else
   console.log 'Running Dev Mode'.red
   nconf.env().argv().file process.env.PWD  + '/devconfig.json'
@@ -38,8 +37,17 @@
     res.send 500, 'error' if err
     return console.error("could not connect to postgres", err)  if err

-    statement = "SELECT word, similarity(word, $1) AS similarity FROM word_table WHERE lang='en' AND word % $1 AND occurrence > 0 ORDER BY similarity DESC LIMIT 3"
-
+    # statement = "SELECT word, similarity(word, $1) AS similarity FROM word_table WHERE lang='en' AND word % $1 AND occurrence > 0 ORDER BY similarity DESC LIMIT 3"
+    statement = """
+                WITH word_query AS (
+                  SELECT word, occurrence, similarity(word, '$1') AS similarity
+                  FROM word_table WHERE word % $1 AND occurrence > 2
+                  ORDER BY similarity DESC LIMIT 10
+                )
+                SELECT word, occurrence, similarity
+                FROM word_query
+                ORDER BY occurrence DESC LIMIT 10;
+                """

     client.query statement, [req.params.query], (err, result) ->
       done()


Updated test-generic-parser

Updated test-generic-parser

--- test/test-generic-parser.coffee
+++ test/test-generic-parser.coffee
@@ -16,6 +16,33 @@

     val = parser.collectUntilFalse => parser.takeIfChar 'f'
     expect(val).to.equal('ffff')
+
+  it 'takeUntilInCharArray, takeUntilInStringArray', ->
+
+    input = 'fffffrggggggggq'
+
+    parser = new GenericParser input
+
+    val = parser.takeUntilInCharArray [ 'r', 'q' ]
+    expect(val).to.equal('fffff')
+
+    parser.moveForward()
+
+    val = parser.takeUntilInCharArray [ 'r', 'q' ]
+    expect(val).to.equal('gggggggg')
+
+    input = 'fffkiffkioggggggggqte'
+
+    parser = new GenericParser input
+
+    val = parser.takeUntilInStringArray [ 'kio', 'qte' ]
+    expect(val).to.equal('fffkiff')
+
+    parser.moveForward() for i in [0...3]
+
+    val = parser.takeUntilInStringArray [  'kio', 'qte' ]
+    expect(val).to.equal('gggggggg')
+

   it 'takeUnlessChar, takeUntilChar, takeUntilString', ->


Only deb for first alpha

Only deb for first alpha

--- tasks/publish.coffee
+++ tasks/publish.coffee
@@ -5,7 +5,7 @@

 env(__dirname + '/../.env')
 # Upload every file in ./dist to GitHub
-gulp.task 'publish:github', ['pack:linux32:deb', 'pack:linux64:deb', 'pack:linux32:rpm', 'pack:linux64:rpm'], ->
+gulp.task 'publish:github', ['pack:linux32:deb', 'pack:linux64:deb'], ->
   if not process.env.GITHUB_TOKEN
     return console.warn 'GITHUB_TOKEN env var not set.'


Fixes #33: Look for the opening curly brace when defining a request in the pouchdb's adapter.

--- src/pouchmodel.coffee
+++ src/pouchmodel.coffee
@@ -191,14 +191,17 @@
         {map, reduce} = request

         qs = map.toString()
-        qs = qs.substring 'function(doc) {'.length
+        qs = qs.substring qs.indexOf '{'
         qs = qs.substring 0, (qs.length - 1)
         stringquery = "if (doc.docType.toLowerCase() === " + \
                       "\"#{docType}\") #{qs.toString()}};"
         stringquery = stringquery.replace '\n', ''
         ### jshint ignore: start ###
         # Function is dangerous, check if we can remove it
-        map = new Function "doc", stringquery
+        try
+            map = new Function "doc", stringquery
+        catch error
+            callback new Error "Error when parsing request's body: #{error}"
         ### jshint ignore: end ###
         view = map: map.toString()
         view.reduce = reduce.toString() if reduce?


Magic consts

Magic consts

--- src/predefined.coffee
+++ src/predefined.coffee
@@ -273,6 +273,15 @@
     'this': 'variable'
     'THIS': 'variable'

+    ### Magic consts ###
+    '__LINE__': 'const'
+    '__FILE__': 'const'
+    '__DIR__': 'const'
+    '__FUNCTION__': 'const'
+    '__CLASS__': 'const'
+    '__TRAIT__': 'const'
+    '__METHOD__': 'const'
+    '__NAMESPACE__': 'const'

     ### Variable and Type Related Extensions ###

@@ -3838,4 +3847,4 @@
     'calculhmac': 'function'
     'nthmac': 'function'
     'signeurlpaiement': 'function'
-}+}


Refactor

Refactor

--- src/properties.coffee
+++ src/properties.coffee
@@ -75,8 +75,9 @@
             checkDependenciesFor(name)
       checkDependenciesFor(prop) for prop in changedProperties

+    changes = {}
     for name in changedProperties
-      @trigger?("change:#{name}", @get(name))
-    obj = {}
-    extend(obj, pairToObject(name, @get(name))) for name in changedProperties
-    @trigger?("change", obj)
+      value = @get(name)
+      @trigger?("change:#{name}", value)
+      changes[name] = value
+    @trigger?("change", changes)


Cleanup pipe to tar.gz

Cleanup pipe to tar.gz

--- src/bundle.coffee
+++ src/bundle.coffee
@@ -44,12 +44,19 @@

       bundlePath = path.join tmpdir, 'bundle.tar.gz'
       bundleOutput = fstream.Writer bundlePath
-      reader = fstream.Reader { path: dir, type: 'directory', filter }
-      # After a lot of poking around in the tar module I found
-      # this was needed to tar the files without including th
-      # root directory.
-      reader.root = true
-      reader.pipe(tar.Pack()).pipe(zlib.Gzip()).pipe(bundleOutput)
+
+      reader = fstream.Reader({
+        path: dir
+        type: 'directory'
+        # After a lot of poking around in the tar module I found
+        # this was needed to tar the files without including th
+        # root directory.
+        root: true
+        filter
+      }).pipe(tar.Pack())
+        .pipe(zlib.Gzip())
+        .pipe(bundleOutput)
+
       bundleOutput.on 'close', ->
         callback null, bundlePath


fix(away-php): trim `&post=...` from urls in groups

--- src/away-php/index.coffee
+++ src/away-php/index.coffee
@@ -26,7 +26,7 @@
       if true is settings.get "common.awayPhp"
         if element.matches "a[href^='/away.php']"
           element.setAttribute "vkx-orig-href", element.href
-          href = element.href.match( /to=(.*)/ )[ 1 ]
+          href = element.href.match( /to=([^&]*)/ )[ 1 ]
           cp1251 = require "@vk-x/cp1251"
           decodedHref = cp1251.decode href
           element.href = decodedHref


further tests around run argument parsing

--- test/runner.coffee
+++ test/runner.coffee
@@ -44,3 +44,74 @@

           it "transforms the values correctly", ->
             expect(@arg).to.eql ["-e ENV1=input", "-e ENV2=val2"]
+
+  describe "dependencies", ->
+    describe "with no group override", ->
+      beforeEach (done) ->
+        object =
+          aliases: ["dep1", "alias2"]
+        Runner.getArg "dependencies", ["dep1", "dep2"], object, (err, @arg) => done()
+
+      it "transforms the values correctly", ->
+        expect(@arg).to.eql ["-link dep1:dep1", "-link dep2:alias2"]
+
+    describe "with a group override", ->
+      beforeEach (done) ->
+        object =
+          group: "dev"
+          aliases: ["dep1", "alias2"]
+        Runner.getArg "dependencies", ["dep1", "dep2"], object, (err, @arg) => done()
+
+      it "applies the correct container namespace to each link", ->
+        expect(@arg).to.eql ["-link dep1.dev:dep1", "-link dep2.dev:alias2"]
+
+  describe "port", ->
+    beforeEach (done) ->
+      Runner.getArg "port", ["8080:8080", "8888"], {}, (err, @arg) => done()
+
+    it "transforms the values correctly", ->
+      expect(@arg).to.eql ["-p 8080:8080", "-p 8888"]
+
+  describe "privileged", ->
+    describe "when falsy", ->
+      beforeEach (done) ->
+        Runner.getArg "privileged", false, {}, (err, @arg) => done()
+
+      it "does not apply the flag", ->
+        expect(@arg).to.eql []
+
+    describe "when truthy", ->
+      beforeEach (done) ->
+        Runner.getArg "privileged", true, {}, (err, @arg) => done()
+
+      it "applies the flag", ->
+        expect(@arg).to.eql ["-privileged"]
+
+  describe "mount", ->
+    describe "with string literals", ->
+      beforeEach (done) ->
+        Runner.getArg "mount", ["/path/to/host:/path/to/container"], {}, (err, @arg) => done()
+
+      it "transforms the values correctly", ->
+        expect(@arg).to.eql ["-v /path/to/host:/path/to/container"]
+
+    describe "with the special dot value", ->
+      beforeEach (done) ->
+        Runner.getArg "mount", [".:/path/to/container"], {}, (err, @arg) => done()
+
+      it "replaces the dot with the current working directory", ->
+        expect(@arg).to.eql ["-v #{process.cwd()}:/path/to/container"]
+
+    describe "with the special dot value at the start of the string", ->
+      beforeEach (done) ->
+        Runner.getArg "mount", ["./test:/path/to/container"], {}, (err, @arg) => done()
+
+      it "replaces the dot but keeps the path after it", ->
+        expect(@arg).to.eql ["-v #{process.cwd()}/test:/path/to/container"]
+
+    describe "with the dot value anywhere else in string", ->
+      beforeEach (done) ->
+        Runner.getArg "mount", ["/path/to/.ssh:/path/to/container"], {}, (err, @arg) => done()
+
+      it "does not replace the dot value", ->
+        expect(@arg).to.eql ["-v /path/to/.ssh:/path/to/container"]


:lipstick: Additional Cleanup

:lipstick: Additional Cleanup

--- lib/gofmt.coffee
+++ lib/gofmt.coffee
@@ -1,4 +1,4 @@
-spawn = require('child_process').spawn
+{spawn} = require 'child_process'
 {Subscriber, Emitter} = require 'emissary'
 _ = require 'underscore-plus'


Move and rename Expectation@_all_expectations to all_expectations.

--- src/TinyMockJS.coffee
+++ src/TinyMockJS.coffee
@@ -1,6 +1,7 @@
+all_expectations = []
+
+
 class Expectation
-
-  @_all_expectations: []

   #
   @verify: ->
@@ -9,7 +10,7 @@

   #
   @reset: ->
-    @_all_expectations.length = 0
+    all_expectations.length = 0

   constructor: (object, method_name) ->
     @_object = object
@@ -20,7 +21,7 @@
     @called = false
     @_original_method = undefined
     _install_mock_method(@, object, method_name)
-    Expectation._all_expectations.push(@)
+    all_expectations.push(@)

   #
   # mock (my_mock) ->
@@ -113,23 +114,22 @@
     expectation._returns

 _find_expectation = (object, method_name, args...) ->
-  for expectation in Expectation._all_expectations when expectation.matches(object, method_name, args...)
+  for expectation in all_expectations when expectation.matches(object, method_name, args...)
     return expectation
   undefined

 _check_for_duplicate_expectations = (mock) ->
   # TODO: use each with index and slice to avoid last element
-  expectations = Expectation._all_expectations
-  return if expectations.length < 2
-  for outer in [0..expectations.length-2]
-    for inner in [outer+1..expectations.length-1]
-      if expectations[outer].equals( expectations[inner] )
-        _throw_duplicate_expectation("#{expectations[outer].method_name}(#{expectations[outer]._args})")
+  return if all_expectations.length < 2
+  for outer in [0..all_expectations.length-2]
+    for inner in [outer+1..all_expectations.length-1]
+      if all_expectations[outer].equals( all_expectations[inner] )
+        _throw_duplicate_expectation("#{all_expectations[outer].method_name}(#{all_expectations[outer]._args})")

 _build_errors = (expectation) ->
   # TODO: use a mapping function?
   errors = ""
-  for expectation in expectation._all_expectations when expectation.called == false
+  for expectation in all_expectations when expectation.called == false
     errors += "'#{expectation.method_name}(#{expectation._args})' was never called\n"
   errors

@@ -202,7 +202,7 @@
     Expectation.verify()
   finally
     delete Object.prototype.expects
-    for expectation in Expectation._all_expectations     # TODO: do this in reverse
+    for expectation in all_expectations     # TODO: do this in reverse
       object = if typeof expectation._object == 'function' then expectation._object.prototype else expectation._object
       if expectation._original_method?
         object[ expectation.method_name ] = expectation._original_method


Extract method _save_returns().

Extract method _save_returns().

--- src/TinyMockJS.coffee
+++ src/TinyMockJS.coffee
@@ -55,7 +55,7 @@
   #
   returns: (value) ->
     _check_returns_usage(@, value)
-    @_returns = value
+    _save_returns(@, value)
     @

   #
@@ -240,6 +240,9 @@

 _set_return_for_current_expectation = (mock, value) ->
   _current_expectation(mock)._returns = value
+
+_save_returns = (expectation, value) ->
+  expectation._returns = value

 _set_throws_for_current_expectation = (mock, error) ->
   _current_expectation(mock).throws = error


Use 'throw new Error( "a message" )' instead of 'throw "a message"'.

--- src/TinyMockJS.coffee
+++ src/TinyMockJS.coffee
@@ -42,7 +42,7 @@
 expects = (method_name) ->
   check_expects_usage(@, method_name)
   if typeof @ == 'function' and not @.prototype[ method_name ]?
-    throw new Error("'#{method_name}' is not an existing method; you can only mock existing methods on classes")
+    throw new Error( "'#{method_name}' is not an existing method; you can only mock existing methods on classes" )
   start_new_expectation(@, method_name)

 check_expects_usage = (object, method_name) ->
@@ -60,13 +60,13 @@
   new Expectation(object, method_name)

 throw_expects_usage = ->
-  throw "you need to supply a method name to expects(), e.g. my_mock.expects('my_method')"
+  throw new Error( "you need to supply a method name to expects(), e.g. my_mock.expects('my_method')" )

 throw_reserved_word = (reserved_word) ->
-  throw "you cannot use my_mock.expects('#{reserved_word}'); '#{reserved_word}' is a reserved method name"
+  throw new Error( "you cannot use my_mock.expects('#{reserved_word}'); '#{reserved_word}' is a reserved method name" )

 throw_pre_existing_property = (property_name) ->
-  throw new Error("'#{property_name}' is an existing property; you can only mock functions")
+  throw new Error( "'#{property_name}' is an existing property; you can only mock functions" )

 #
@@ -79,7 +79,7 @@

 verify_all_expectations = ->
   errors = build_errors()
-  throw new Error(errors) unless errors == ""
+  throw new Error( errors ) unless errors == ""

 build_errors = ->
   (build_not_called_error(expectation) for expectation in all_expectations when not expectation.called).join("")
@@ -114,10 +114,10 @@
         throw_duplicate_expectation("#{all_expectations[outer].method_name}(#{all_expectations[outer]._args})")

 throw_unknown_expectation = (expectation) ->
-  throw "#{expectation} does not match any expectations"
+  throw new Error( "#{expectation} does not match any expectations" )

 throw_duplicate_expectation = (expectation) ->
-  throw "#{expectation} is a duplicate expectation"
+  throw new Error( "#{expectation} is a duplicate expectation" )

 #
@@ -218,22 +218,22 @@
     object[ method_name ] = build_mocked_method(method_name)

 _throw_args_usage = ->
-  throw "you need to supply at least one argument to args(), e.g. my_mock.expects('my_method').args(42)"
+  throw new Error( "you need to supply at least one argument to args(), e.g. my_mock.expects('my_method').args(42)" )

 _throw_args_used_more_than_once = ->
-  throw new Error("you specified args() more than once, e.g. my_mock.expects('my_method').args(1).args(2); use it once per expectation")
+  throw new Error( "you specified args() more than once, e.g. my_mock.expects('my_method').args(1).args(2); use it once per expectation" )

 _throw_returns_usage = ->
-  throw "you need to supply an argument to returns(), e.g. my_mock.expects('my_method').returns(123)"
+  throw new Error( "you need to supply an argument to returns(), e.g. my_mock.expects('my_method').returns(123)" )

 _throw_returns_used_more_than_once = ->
-  throw new Error("you specified returns() more than once, e.g. my_mock.expects('my_method').returns(1).returns(2); use it once per expectation")
+  throw new Error( "you specified returns() more than once, e.g. my_mock.expects('my_method').returns(1).returns(2); use it once per expectation" )

 _throw_throws_usage = ->
-  throw "you need to supply an argument to throws(), e.g. my_mock.expects('my_method').throws('an error')"
+  throw new Error( "you need to supply an argument to throws(), e.g. my_mock.expects('my_method').throws('an error')" )

 _throw_throws_used_more_than_once = ->
-  throw new Error("you specified throws() more than once, e.g. my_mock.expects('my_method').throws('something').throws('something else'); use it once per expectation")
+  throw new Error( "you specified throws() more than once, e.g. my_mock.expects('my_method').throws('something').throws('something else'); use it once per expectation" )

 _throw_returns_and_throws_both_used = ->
-  throw new Error("you specified both returns() and throws() on the same expectation; use one or the other on an expectation")
+  throw new Error( "you specified both returns() and throws() on the same expectation; use one or the other on an expectation" )


expose config.userPackagesDirPath

expose config.userPackagesDirPath

--- src/app/config.coffee
+++ src/app/config.coffee
@@ -18,6 +18,7 @@
   configDirPath: configDirPath
   themeDirPaths: [userThemesDirPath, bundledThemesDirPath, vendoredThemesDirPath]
   packageDirPaths: [userPackagesDirPath, vendoredPackagesDirPath, bundledPackagesDirPath]
+  userPackagesDirPath: userPackagesDirPath
   defaultSettings: null
   settings: null


Kill console.log

Kill console.log

--- src/app/editor.coffee
+++ src/app/editor.coffee
@@ -889,7 +889,6 @@

   buildLineElementForScreenRow: (screenRow) ->
     div = document.createElement('div')
-    console.log screenRow, @activeEditSession.lineForScreenRow(screenRow)
     div.innerHTML = @buildLineHtml(@activeEditSession.lineForScreenRow(screenRow))
     div.firstChild


Move DM channel logic into client

Move DM channel logic into client

--- src/client.coffee
+++ src/client.coffee
@@ -229,9 +229,22 @@
     getUserDirectMessageChannel: (userID, callback) ->
         postData =
             user_id: userID
-        @_apiCall 'POST', '/channels/create_direct', postData, (data, header) =>
+        @_apiCall 'POST', '/channels/create_direct', postData, (data, headers) =>
             @logger.debug 'Requested new DM channel for user ' + userID + '.'
-            callback(data)
+
+            # If there's no error, return the new DM channel ID
+            unless data.error
+                return callback({'id': data.id}, {})
+
+            # Mattermost throws an error if a DM channel already exists
+            # We can look through all the channels and find the one that only
+            # contains us and the target user and has a total of 2 members
+            for channel, details of @getAllChannels()
+                @getChannelInfo channel, 2, (info) =>
+                    return if info.member_count is not 2
+                    for member in info.members
+                        if member.id is userID
+                            callback({'id': info.id}, {})

     getChannelInfo: (channelID, memberLimit, callback) ->
         @_apiCall 'GET', '/channels/' + channelID + '/extra_info/' + memberLimit, null, (data, header) =>


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/color2.coffee
+++ src/color2.coffee
@@ -234,9 +234,9 @@
         saturation = (if lightness > 0.5 then chroma / (2 - max - min) else chroma / (max + min))

         switch max
-          when r then hue = (g - b) / chroma + (if g < b then 6 else 0)
-          when g then hue = (b - r) / chroma + 2
-          when b then hue = (r - g) / chroma + 4
+          when r then hue = ((g - b) / chroma) + (if g < b then 6 else 0)
+          when g then hue = ((b - r) / chroma) + 2
+          when b then hue = ((r - g) / chroma) + 4

         hue *= 60


Updating comments.

Updating comments.

--- src/config.coffee
+++ src/config.coffee
@@ -1,3 +1,8 @@
+# The `Config` class is responsible for managing the `~/.snailer` JSON
+# configuration file. This configuration maintains all the settings configured
+# via the `snailer` CLI. The Snailer daemon uses `Config` to read settings.
+
+# External dependencies.
 fs      = require 'fs'
 path    = require 'path'
 _       = require 'underscore'


Hydrator (config): fix bug that treats actions as collections.

--- src/config.coffee
+++ src/config.coffee
@@ -7,5 +7,5 @@
   api:
     specialProperties: ['link', 'action', 'special_objects']
     link: 'link'
-    action: 'action'
+    action: 'actions'
     specialObjects: 'special_objects'


:lipstick: in :memo:

:lipstick: in :memo:

--- src/cursor.coffee
+++ src/cursor.coffee
@@ -17,7 +17,7 @@
   visible: true
   needsAutoscroll: null

-  # Instantiated by an {TextEditor}
+  # Instantiated by a {TextEditor}
   constructor: ({@editor, @marker, id}) ->
     @emitter = new Emitter


don't return deleted docs by checking for type and properly handle queries with an _id

--- lib/mongo.coffee
+++ lib/mongo.coffee
@@ -1,4 +1,9 @@
 # A super simple mongodb adapter for itsalive
+
+shallowClone = (object) ->
+  out = {}
+  out[key] = object[key] for key of object
+  return out

 # mongo is a mongoskin client. Create with:
 #  mongo.db('localhost:27017/tx?auto_reconnect', safe:true)
@@ -29,10 +34,12 @@
   query: (cName, query, callback) ->
     return callback 'db already closed' if @closed

+    # Clone the query so we don't edit the original
+    query = shallowClone query
+    query.type = {$ne: null} unless query.type
+
     skip = query.$skip
-    #delete query.$skip
     limit = query.$limit
-    #delete query.$limit

     mongo.collection(cName).find query, (err, cursor) ->
       return callback err if err
@@ -53,8 +60,13 @@

   queryDoc: (cName, docName, query, callback) ->
-    query = JSON.parse JSON.stringify query # clone the query so we don't edit the original
-    query._id = docName
+    if query._id
+      return callback() if query._id isnt docName
+    else
+      # Clone the query so we don't edit the original
+      query = shallowClone query
+      query._id = docName unless query._id
+
     mongo.collection(cName).findOne query, (err, result) ->
       if result
         result.docName = docName


Fix logic in setMochaArguments method.

Fix logic in setMochaArguments method.

--- lib/mycha.coffee
+++ lib/mycha.coffee
@@ -18,9 +18,9 @@
     @mochaArgs.push '--reporter', options.reporter if options.reporter
     @mochaArgs.push '--timeout', options.timeout if options.timeout
     for argument_name, argument_value of options.mocha
-      return if argument_value is false
+      continue if argument_value is false
       @mochaArgs.push "--#{argument_name}"
-      @mochaArgs.push argument_value if argument_value is true
+      @mochaArgs.push argument_value if argument_value isnt true

   getTestFiles: ->


(- new API) Sequence#at() dives into wrapped expressions

--- Paws/Source/parser.coffee
+++ Paws/Source/parser.coffee
@@ -32,9 +32,17 @@
 # A simple container for a series of sequentially-executed `Expression`s.
 exports.Sequence = Sequence =
 delegated('expressions', Array) class Sequence
+   # A convenience method that simply wraps a single result of `Expression#from` in a `Sequence`.
+   from: (representation)->
+      return new Sequence Expression.from(representation)
+
    constructor: (@expressions...)->

-   at: (idx)-> @expressions[idx]
+   # Can either grab an expression out of this sequence (one index passed), or an object out of a
+   # specific expression (two indices passed.)
+   at: (expr_idx, idx)->
+      expression = @expressions[expr_idx]
+      return if idx? then expression.at(idx) else expression

 # Represents a single expression (or sub-expression). Contains `words`, each of which may be either
 # a Paws `Thing`, or an array of sub-`Expression`s. JavaScript strings will be constructed into


lowercase html id

lowercase html id

--- src/editor.coffee
+++ src/editor.coffee
@@ -1,7 +1,7 @@
 class ScribeEditor extends EventEmitter2
   @editors: []

-  @CONTAINER_ID: 'Scribe-container'
+  @CONTAINER_ID: 'scribe-container'
   @ID_PREFIX: 'editor-'
   @CURSOR_PREFIX: 'cursor-'
   @DEFAULTS:


fix failing test where localDelta branch was erroring

--- src/editor.coffee
+++ src/editor.coffee
@@ -30,20 +30,19 @@
   applyDelta: (delta, source) ->
     localDelta = this._update()
     if localDelta
-      @delta = @delta.compose(localDelta)
       tempDelta = localDelta
       localDelta = localDelta.transform(delta, true)
       delta = delta.transform(tempDelta, false)
+      @delta = @doc.toDelta()   # Only for our error check below, otherwise dont need to update yet
     unless delta.isIdentity()   # Follows may have turned delta into the identity
-      throw new Error("Trying to apply delta to incorrect doc length") unless delta.startLength == @delta.endLength
+      throw new Error("Trying to apply delta to incorrect doc length") if delta.startLength != @delta.endLength
       delta.apply(this._insertAt, this._deleteAt, this._formatAt, this)
       @selection.shiftAfter(0, 0, _.bind(@doc.optimizeLines, @doc))
-      oldDelta = @delta
-      @delta = oldDelta.compose(delta)
+      @delta = @doc.toDelta()
+      @innerHTML = @root.innerHTML
       @quill.emit(@quill.constructor.events.TEXT_CHANGE, delta, source) unless source == 'silent'
     if localDelta and !localDelta.isIdentity() and source != 'silent'
       @quill.emit(@quill.constructor.events.TEXT_CHANGE, localDelta, 'user')
-    @innerHTML = @root.innerHTML

   checkUpdate: (source = 'user') ->
     return clearInterval(@timer) if !@renderer.iframe.parentNode? or !@root.parentNode?


Use cachedCharWidth instead of cachedVal

--- src/editor.coffee
+++ src/editor.coffee
@@ -1598,10 +1598,10 @@
         oldLeft = left

         scopes = @scopesForColumn(tokenizedLine, index)
-        cachedVal = @getCharacterWidthCache(scopes, char)
-
-        if cachedVal?
-          left = oldLeft + cachedVal
+        cachedCharWidth = @getCharacterWidthCache(scopes, char)
+
+        if cachedCharWidth?
+          left = oldLeft + cachedCharWidth
         else
           # i + 1 to measure to the end of the current character
           MeasureRange.setEnd(textNode, i + 1)
@@ -1611,12 +1611,12 @@
           left = rects[0].left - Math.floor(@scrollView.offset().left) + Math.floor(@scrollLeft())

           if scopes?
-            cachedVal = left - oldLeft
-            @setCharacterWidthCache(scopes, char, cachedVal)
+            cachedCharWidth = left - oldLeft
+            @setCharacterWidthCache(scopes, char, cachedCharWidth)

         # Assume all the characters are the same width when dealing with long
         # lines :racehorse:
-        return column * cachedVal if index > LongLineLength
+        return column * cachedCharWidth if index > LongLineLength

         index++


updating references to PixieCanvas

updating references to PixieCanvas

--- src/engine.coffee
+++ src/engine.coffee
@@ -71,7 +71,7 @@
   @name beforeDraw
   @methodOf Engine#
   @event
-  @params {PowerCanvas} canvas A reference to the canvas to draw on.
+  @params {PixieCanvas} canvas A reference to the canvas to draw on.
   ###

   ###*
@@ -80,13 +80,16 @@

   engine.bind "draw", (canvas) ->
     # print some directions for the player
-    canvas.fillText("Go this way =>", 200, 200)
+    canvas.drawText
+      text: "Go this way =>"
+      x: 200
+      y: 200

   @name draw
   @methodOf Engine#
   @event
-  @params {PowerCanvas} canvas A reference to the canvas to draw on.
+  @params {PixieCanvas} canvas A reference to the canvas to draw on.
   ###

   ###*
@@ -99,14 +102,19 @@
   engine.bind "overlay", (canvas) ->
     # print the player's health. This will be
     # positioned absolutely according to the viewport.
-    canvas.fillText("HEALTH:", 20, 20)
-    canvas.fillText(player.health(), 50, 20)
+    canvas.drawText
+      text: "HEALTH:"
+      position: Point(20, 20)
+
+    canvas.drawText
+      text: player.health()
+      position: Point(50, 20)

   @name overlay
   @methodOf Engine#
   @event
-  @params {PowerCanvas} canvas A reference to the canvas to draw on.
+  @params {PixieCanvas} canvas A reference to the canvas to draw on.
   ###

   Engine = (I) ->


follow docs

follow docs

--- src/follow.coffee
+++ src/follow.coffee
@@ -1,7 +1,7 @@
 ###*
 The Follow module provides a simple method to set an object's
 velocity so that it will approach another object. This method
-relies on the objects having `bounds` methods.
+relies on the objects having `bounds` methods.

 player = GameObject


follow docs

follow docs

--- src/follow.coffee
+++ src/follow.coffee
@@ -1,7 +1,7 @@
 ###*
 The Follow module provides a simple method to set an object's
 velocity so that it will approach another object. This method
-relies on the objects having `bounds` methods.
+relies on the objects having `bounds` methods. GameObject pro

 player = GameObject


Resolved a bug with after_alls being called even without tests. We are now faking test reports.. Which is not fully implemented anyway.

--- lib/suite.coffee
+++ lib/suite.coffee
@@ -32,7 +32,11 @@
     @_tests_and_suites = []

   # (callback) -> undefined
-  _run: (callback, before_alls=[], before_eachs=[], after_eachs=[], index=0) =>
+  _run: (callback,
+      before_alls=[], before_eachs=[], after_eachs=[], index=0,
+      report={
+        total_tests: 0
+        }) =>

     if index is 0
       before_alls.push @_before_alls...
@@ -42,14 +46,19 @@
     item = @_tests_and_suites[index]

     if not item?
-      @_run_runners @_after_alls, callback
+      if report.total_tests > 0
+        @_run_runners @_after_alls, callback
+      else
+          callback report
       return

     before_alls_callback = (reports) =>
       before_alls = []
       item.run test_callback

-    test_callback = (report) =>
+    test_callback = (test_report) =>
+      # We're just faking the report for now.
+      report.total_tests += 1
       @_run_runners after_eachs, after_eachs_callback

     after_eachs_callback = (report) =>
@@ -155,7 +164,7 @@
   #   Run all the tests found in this suite, and any suites added to this.
   run: (callback=->) ->
     @_run (reports) ->
-      callback()
+      callback reports


typo

typo

I think. Just stumbled upon that, but haven't seen any errors about it.

--- lib/utils.coffee
+++ lib/utils.coffee
@@ -6,7 +6,7 @@

 module.exports =
   homedir: ->
-    if process.plaform == 'win32'
+    if process.platform == 'win32'
       process.env.USERPROFILE
     else
       process.env.HOME


fiddling with html

fiddling with html

--- src/directive.coffee
+++ src/directive.coffee
@@ -129,6 +129,10 @@

+

+
 Form is empty

+
 Drap and drop components here to build intake forms

+

         """
     link: (scope, element, attrs) ->
@@ -385,12 +389,12 @@
     restrict: 'A'
     template:
         """
-
 1" class="nav nav-tabs nav-justified">
+
 1" class="nav nav-tabs nav-justified" style='margin-top:10px'>

                 {{group}}

-

+


change UI to not use last used any more

change UI to not use last used any more

--- extension/chrome/js/src/content.coffee
+++ extension/chrome/js/src/content.coffee
@@ -29,7 +29,7 @@
 		@TYPE_NAV: 		'nav'

 		@STORE:
-			LASTUSED:		'lastUsed'
+			# LASTUSED:		'lastUsed'
 			BOX_SETTING:	'settings'
 			DEBUG:			'debug'
 			EMAIL:			'email'
@@ -47,7 +47,7 @@
 		currentGmail: null
 		settingEmail: null
 		settingProps: {}
-		lastUsed: []
+		# lastUsed: []

 		currentView: null

@@ -81,16 +81,16 @@
 				return

 			# Get the last used presets
-			chrome.storage.sync.get M.STORE.LASTUSED, (items) =>
-				@lastUsed = items[M.STORE.LASTUSED] ? []
-				return
+			# chrome.storage.sync.get M.STORE.LASTUSED, (items) =>
+			#	@lastUsed = items[M.STORE.LASTUSED] ? []
+			#	return

 			# Listen to changes
 			chrome.storage.onChanged.addListener (changes, namespace) =>
 				switch namespace
 					when 'sync'
-						if M.STORE.LASTUSED of changes
-							@lastUsed = changes[M.STORE.LASTUSED].newValue
+						#if M.STORE.LASTUSED of changes
+						#	@lastUsed = changes[M.STORE.LASTUSED].newValue
 						if M.STORE.BOX_SETTING of changes
 							@settingProps = changes[M.STORE.BOX_SETTING].newValue
 					when 'local'
@@ -254,15 +254,15 @@

 			props =
 				noanswer: 	false
-				unread:		false
+				unread:		true
 				star:		false
-				inbox:		false
-				archive:	false
+				inbox:		true
+				archive:	true
 				#when:		_delta _1m

 			_.each props, (v, op) =>
-				selected = !! @settingProps?[op]
-				props[op] = selected
+				hasSetting = @settingProps and typeof @settingProps[op] isnt 'undefined'
+				props[op] = !! @settingProps?[op] if hasSetting
 				return

 			schedule = (wen) =>
@@ -306,10 +306,11 @@

 			presets = {}
 			presets.minutes 	= [1,5] if @debug
-			presets.hours 		= [2,4]
+			presets.hours 		= [4]
+			presets.hours.unshift 2 if @debug
 			presets.tomorrow 	= [8,14]
 			presets.days 		= [2,7,14]
-			presets.months 		= [1]
+			presets.months 		= [1] if @debug

 			# UI

@@ -381,53 +382,51 @@
 			timeSection.append new GMailUI.PopupLabel __msg 'menuTime'

-			unless _.isEmpty @lastUsed
-				lastUsedSection = timeSection.append new GMailUI.Section
-
-				_.each @lastUsed, (tuple) =>
-					key = tuple.key
-					time = tuple.time
-					# Remove the last used presets from the list
-					presets[key] = _.without presets[key], time if presets[key]
-
-					[label, title] = @getTexts key, time
-					button = lastUsedSection.append new GMailUI.Button label, title
-					timeFn = @generateTimeFn key
-					button.on 'click', (e) =>
-						@storeLastUsed key, time
-						wen = timeFn time
-						log "schedule: #{time}, #{key}: #{wen}"
-						schedule wen
-						return
-					return
-				lastUsedSection.append new GMailUI.Separator
-
-			timeSection.append 				(new GMailUI.PopupMenuItem pickerMenu, (__msg 'menuTimePresetSpecifiedDate'),	'',	'',	true)
-			presetItem = timeSection.append (new GMailUI.PopupMenuItem presetMenu, (__msg 'menuTimePresetCloseFuture'), 	'', '',	true)
-
+			#unless _.isEmpty @lastUsed
+			#	lastUsedSection = timeSection.append new GMailUI.Section
+
+			#	_.each @lastUsed, (tuple) =>
+			#		key = tuple.key
+			#		time = tuple.time
+			#		# Remove the last used presets from the list
+			#		presets[key] = _.without presets[key], time if presets[key]
+
+			#		[label, title] = @getTexts key, time
+			#		button = lastUsedSection.append new GMailUI.Button label, title
+			#		timeFn = @generateTimeFn key
+			#		button.on 'click', (e) =>
+			#			@storeLastUsed key, time
+			#			wen = timeFn time
+			#			log "schedule: #{time}, #{key}: #{wen}"
+			#			schedule wen
+			#			return
+			#		return
+			#	lastUsedSection.append new GMailUI.Separator
+
+
+			#presetItem = timeSection.append (new GMailUI.PopupMenuItem presetMenu, (__msg 'menuTimePresetCloseFuture'), 	'', '',	true)

 			# Presets

 			sep = null
 			_.each presets, (times, key) =>
 				unless _.isEmpty times
-					presetMenu.append sep if sep
+					timeSection.append sep if sep
 					_.each times, (time) =>
 						timeFn = @generateTimeFn key
 						[label, title] = @getTexts key, time
-						item = new GMailUI.PopupMenuItem presetItem, label, title, '', false
-						onChange = (e, checked) =>
-							if checked
-								@storeLastUsed key, time
+						item = timeSection.append new GMailUI.Button label, title
+						item.on 'click', =>
 								wen = timeFn time
 								log "schedule: #{time}, #{key}: #{wen}"
 								schedule wen
 							return
-						item.addOnChange onChange, true
-						presetMenu.append item
 						return
 					sep = new GMailUI.Separator
 				return
+
+			timeSection.append new GMailUI.Separator
+			timeSection.append 	(new GMailUI.PopupMenuItem pickerMenu, (__msg 'menuTimePresetSpecifiedDate'),	'',	'',	true)

 			button = bar.append new GMailUI.ButtonBarPopupButton popup, '', (__msg 'extName')


Add octicons to fuzzy-finder

Add octicons to fuzzy-finder

Also place file base name first followed by
directory path in darker color

--- src/file-view.coffee
+++ src/file-view.coffee
@@ -2,28 +2,9 @@
 $ = require 'jquery'
 Git = require 'git'
 fs = require 'fs'
-_ = require 'underscore'

 module.exports =
 class FileView extends View
-
-  @COMPRESSED_EXTENSIONS: [
-    '.gz'
-    '.jar'
-    '.tar'
-    '.zip'
-  ]
-
-  @IMAGE_EXTENSIONS: [
-    '.gif'
-    '.jpeg'
-    '.jpg'
-    '.png'
-  ]
-
-  @PDF_EXTENSIONS: [
-    '.pdf'
-  ]

   @content: (file) ->
     @li class: 'file entry', =>
@@ -35,11 +16,11 @@
   initialize: (@file) ->
     path = @getPath()
     extension = fs.extension(path)
-    if _.contains(FileView.COMPRESSED_EXTENSIONS, extension)
+    if fs.isCompressedExtension(extension)
       @fileName.addClass('compressed-name')
-    else if _.contains(FileView.IMAGE_EXTENSIONS, extension)
+    else if fs.isImageExtension(extension)
       @fileName.addClass('image-name')
-    else if _.contains(FileView.PDF_EXTENSIONS, extension)
+    else if fs.isPdfExtension(extension)
       @fileName.addClass('pdf-name')
     else
       @fileName.addClass('text-name')


split out creating run contexts

split out creating run contexts

--- src/notebook.coffee
+++ src/notebook.coffee
@@ -301,16 +301,21 @@

   run = (input_cell, string) ->
     output_cell = create_output_cell input_cell.notebook
+
+    run_context = create_run_context input_cell, output_cell
+
+    run_in_context run_context, string
+
+    run_context.scroll_to_top()
+
+    output_cell
+
+  create_run_context = (input_cell, output_cell) ->
     $top = input_cell.$el

-    scroll_to_result = ($result)->
-      top = if $result?
-        $result.offset().top
-      else
-        $(document).height()
-
+    scroll_to_top = ->
       setTimeout ->
-        $('html, body').scrollTop top
+        $('html, body').scrollTop $top.offset().top
       , 10

     result_handlers =[
@@ -339,6 +344,7 @@
       current_options: {}
       default_options: notebook.default_options
       output: output output_cell.$el
+      scroll_to_top: scroll_to_top
       functions: lead.define_functions {}, graphite_function_names
       vars: lead: {github, graphite, colors}
       set_code: (code) ->
@@ -413,18 +419,14 @@
         promise = fn.call(nested_context)
         promise.done ->
           $item.attr 'data-async-status', "loaded in #{duration()}"
-          scroll_to_result $top
+          scroll_to_top()
         promise.fail ->
           $item.attr 'data-async-status', "failed in #{duration()}"
-          scroll_to_result $top
+          scroll_to_top()

     run_context.cli = cli = bind_cli run_context

-    run_in_context run_context, string
-
-    scroll_to_result $top
-
-    output_cell
+    run_context

   run_in_context = (run_context, string) ->
     try


Tiny bug fixed

Tiny bug fixed

--- javascript/nota-client-init.coffee
+++ javascript/nota-client-init.coffee
@@ -60,7 +60,7 @@
     template = JSON.parse template
     # If we got here the template definition is available and we proceeed by
     # loading the template (and it's dependencies) and starting it up.
-    dependencies = if Nota.phantomRutime
+    dependencies = if Nota.phantomRuntime
       template.dependencies
     else
       # If the Nota god is not present, we'll have to fix our own data. For
@@ -73,13 +73,14 @@
     require dependencies, ->
       # 'unpack' the view, model and test data from the arguments that this
       # require yielded.
-      testJSON = JSON.parse arguments[dependencies.length-1]
       template.view = arguments[dependencies.indexOf(template.view)]
       template.model = arguments[dependencies.indexOf(template.model)]
+      if not Nota.phantomRuntime
+        previewJSON = JSON.parse arguments[dependencies.indexOf(template.previewJSON)]
       # Start nota with this template
       try
         Nota.init
           withTemplate: template
-          withData: testJSON #if not Nota.phantomRutime
+          withData: previewJSON #if not Nota.phantomRutime
       catch error
         console.log error


Make `gc` more readable by extracting hasListeners property

--- src/property.coffee
+++ src/property.coffee
@@ -110,10 +110,14 @@
     for dependency in @definition.localDependencies
       @object[dependency + "_property"]?.registerGlobal()

+  def @prototype, "hasListeners", get: ->
+    @listeners.length is 0 and not @dependentProperties.find((prop) => prop.listeners.length isnt 0)
+
   gc: ->
-    if @listeners.length is 0 and not @dependentProperties.find((prop) => prop.listeners.length isnt 0)
+    if @hasListeners
       fn() for fn in @_gcQueue
       @_isRegistered = false
+      @_gcQueue = []

     for dependency in @definition.localDependencies
       @object[dependency + "_property"]?.gc()


fixed null input exception

fixed null input exception

--- source/assets/js/_levels.coffee
+++ source/assets/js/_levels.coffee
@@ -32,8 +32,8 @@
           @_focus_console()

   _update_scene: ->
-
-    @$dialog.append("

#{@levels[@level][@scene]['output']}

")
+    if @levels[@level][@scene]['output']
+      @$dialog.append("

#{@levels[@level][@scene]['output']}

")

   _init_console: ->
     # jQuery console


revert changes

revert changes

--- mechanics/001-binary-stars/csmain.coffee
+++ mechanics/001-binary-stars/csmain.coffee
@@ -28,8 +28,8 @@
         a = parseFloat(ps['a'])
         e = parseFloat(ps['e'])
         M = m1 + m2
-        x1 = m2 / M * a * 2
-        x2 = - m1 / M * a * 2
+        x1 = m2 / M * a
+        x2 = - m1 / M * a

         v  = Math.sqrt(au.G * M / a * (1 - e) / (1 + e))
         v1 = v / M * m2


Fix bug where toolbars wheelchair status button tooltips were not displayed.

--- app/assets/javascripts/views/components/status-filter-button.component.js.coffee
+++ app/assets/javascripts/views/components/status-filter-button.component.js.coffee
@@ -10,9 +10,9 @@
   popoverOptions:
     trigger: 'hover'
     placement: () ->
-      if window.innerWidth <= 767 then 'top' else 'bottom'
+      if window.innerWidth <= 767 then 'top' else 'bottom' # Responsivness
     delay: { show: 400 }
-    container: '#toolbar .status-filter' # Need for not having little spaces between status buttons
+    container: '.toolbar-status-filter' # Need for not having little spaces between status buttons

   isActive: (()->
     @get('activeFilters').findBy('key', @get('wheelchair'))?


Finding item index bug fixed

Finding item index bug fixed

--- style.coffee
+++ style.coffee
@@ -540,6 +540,26 @@

 		return;

+	findItemIndex = (node, item, parent) ->
+		tmpIndexNode = node
+		parent = parent.children.target or parent
+		while tmpIndexNode
+			tmpSiblingNode = tmpIndexNode
+			while tmpSiblingNode = tmpSiblingNode._nextSibling
+				if tmpSiblingNode._documentStyle?.item?.parent is parent
+					item.index = tmpSiblingNode._documentStyle.item.index
+					return
+
+			# tmpSiblingNode = tmpIndexNode
+			# while tmpSiblingNode = tmpSiblingNode._previousSibling
+			# 	if tmpSiblingNode._documentStyle?.item?.parent is parent
+			# 		item.index = tmpSiblingNode._documentStyle.item.index + 1
+			# 		return
+			if tmpIndexNode isnt node and tmpIndexNode.style
+				return
+			tmpIndexNode = tmpIndexNode._parent
+		return
+
 	findItemParent: ->
 		if @waiting
 			return
@@ -560,17 +580,7 @@
 					@item.parent = item

 					# find index
-					tmpIndexNode = node
-					item = item._sourceItem or item
-					while tmpIndexNode
-						tmpSiblingNode = tmpIndexNode
-						while tmpSiblingNode = tmpSiblingNode._nextSibling
-							if tmpSiblingNode._documentStyle?.item?.parent is item
-								@item.index = tmpSiblingNode._documentStyle.item.index
-								break
-						if tmpIndexNode isnt node and tmpIndexNode.style
-							break
-						tmpIndexNode = tmpIndexNode._parent
+					findItemIndex node, @item, item
 					break
 				tmpNode = tmpNode._parent


added http connection agent to isolate from the global agent used by pimatic. This should simplify tracking unhandled exceptions, issue #17

--- http-app-protocol.coffee
+++ http-app-protocol.coffee
@@ -3,6 +3,7 @@

   Promise = env.require 'bluebird'
   rest = require('restler-promise')(Promise)
+  http = require 'http'
   parseXmlString = Promise.promisify require('xml2js').parseString
   retry = require('promise-retryer')(Promise)
   commons = require('pimatic-plugin-commons')(env)
@@ -14,6 +15,8 @@
       @host = @config.host
       @port = @config.port || 80
       @debug = @config.debug || false
+      @opts =
+        agent: new http.Agent()
       @base = commons.base @, 'HttpAppProtocol'
       @on "newListener", =>
         @base.debug "Status response event listeners: #{1 + @listenerCount 'response'}"
@@ -56,7 +59,7 @@

     _requestUpdate: (command, param="") =>
       @base.debug "http://#{@host}:#{@port}/goform/#{@_mapZoneToUrlPath command}XmlStatusLite.xml"
-      return rest.get "http://#{@host}:#{@port}/goform/#{@_mapZoneToUrlPath command}XmlStatusLite.xml"
+      return rest.get "http://#{@host}:#{@port}/goform/#{@_mapZoneToUrlPath command}XmlStatusLite.xml", @opts
       .then (response) =>
         if response.data.length isnt 0
           @base.debug response.data
@@ -73,6 +76,8 @@
             @_triggerResponse "#{prefix}SI", dom.item.InputFuncSelect[0].value[0].toUpperCase()
         else
           throw new Error "Empty result received for status request"
+      .catch (err) =>
+        @base.error err
       .finally =>
         delete @scheduledUpdates[@_mapZoneToObjectKey command] if @scheduledUpdates[@_mapZoneToObjectKey command]?

@@ -92,7 +97,7 @@
       return new Promise (resolve, reject) =>
         if param isnt '?'
           @base.debug "http://#{@host}:#{@port}/goform/formiPhoneAppDirect.xml?#{command}#{param}"
-          promise = rest.get "http://#{@host}:#{@port}/goform/formiPhoneAppDirect.xml?#{command}#{param}"
+          promise = rest.get "http://#{@host}:#{@port}/goform/formiPhoneAppDirect.xml?#{command}#{param}", @opts
           .then =>
             @_triggerResponse command, param
         else


Remove rxv-specific code

Remove rxv-specific code

--- src/reactive.coffee
+++ src/reactive.coffee
@@ -746,6 +746,7 @@
           _.isString(arg1) or
           _.isNumber(arg1) or
           arg1 instanceof Element or
+          arg1 instanceof SVGElement or
           arg1 instanceof RawHtml or
           arg1 instanceof $ or
           _.isArray(arg1) or
@@ -760,7 +761,7 @@
         if child?
           if _.isString(child) or _.isNumber(child)
             document.createTextNode(child)
-          else if child instanceof Element
+          else if child instanceof Element or child instanceof SVGElement
             child
           else if child instanceof RawHtml
             parsed = $(child.html)
@@ -773,7 +774,7 @@
             throw new Error("Unknown element type in array: #{child.constructor.name} (must be string, number, Element, RawHtml, or jQuery objects)")

     updateContents = (elt, contents) ->
-      elt.html('')
+      elt.html('') if elt.html
       if _.isArray(contents)
         nodes = toNodes(contents)
         elt.append(nodes)
@@ -788,11 +789,20 @@
               .width($(node).width()).height($(node).height())
           setTimeout (-> $(cover).remove() for cover in covers), 2000
       else if _.isString(contents) or _.isNumber(contents) or contents instanceof Element or
-          contents instanceof RawHtml or contents instanceof $
+          contents instanceof SVGElement or contents instanceof RawHtml or contents instanceof $
         updateContents(elt, [contents])
       else
         throw new Error("Unknown type for element contents: #{contents.constructor.name} (accepted types: string, number, Element, RawHtml, jQuery object of single element, or array of the aforementioned)")

+    autoSubContents = (elt, contents) ->
+      rx.autoSub contents.onChange, ([index, removed, added]) ->
+        elt.contents().slice(index, index + removed.length).remove()
+        toAdd = toNodes(added)
+        if index == elt.contents().length
+          elt.append(toAdd)
+        else
+          elt.contents().eq(index).before(toAdd)
+
     rxt.mktag = mktag = (tag) ->
       (arg1, arg2) ->
         [attrs, contents] = normalizeTagArgs(arg1, arg2)
@@ -802,13 +812,7 @@
           setDynProp(elt, name, value)
         if contents?
           if contents instanceof ObsArray
-            rx.autoSub contents.onChange, ([index, removed, added]) ->
-              elt.contents().slice(index, index + removed.length).remove()
-              toAdd = toNodes(added)
-              if index == elt.contents().length
-                elt.append(toAdd)
-              else
-                elt.contents().eq(index).before(toAdd)
+            autoSubContents(elt, contents)
           else if contents instanceof ObsCell
             # TODO: make this more efficient by checking each element to see if it
             # changed (i.e. layer a MappedDepArray over this, and make DepArrays
@@ -840,91 +844,13 @@
       'th', 'form', 'fieldset', 'legend', 'fieldset', 'label', 'input', 'button',
       'select', 'datalist', 'optgroup', 'option', 'select', 'datalist', 'textarea',
       'keygen', 'output', 'progress', 'meter', 'details', 'summary', 'details',
-      'menuitem', 'menu']
+      'menuitem', 'menu',
+      "a", "altglyph", "altglyphdef", "altglyphitem", "animate", "animatecolor", "animatemotion", "animatetransform", "circle", "clippath", "color-profile", "cursor", "defs", "desc", "ellipse", "feblend", "fecolormatrix", "fecomponenttransfer", "fecomposite", "feconvolvematrix", "fediffuselighting", "fedisplacementmap", "fedistantlight", "feflood", "fefunca", "fefuncb", "fefuncg", "fefuncr", "fegaussianblur", "feimage", "femerge", "femergenode", "femorphology", "feoffset", "fepointlight", "fespecularlighting", "fespotlight", "fetile", "feturbulence", "filter", "font", "font-face", "font-face-format", "font-face-name", "font-face-src", "font-face-uri", "foreignobject", "g", "glyph", "glyphref", "hkern", "image", "line", "lineargradient", "marker", "mask", "metadata", "missing-glyph", "mpath", "path", "pattern", "polygon", "polyline", "radialgradient", "rect", "script", "set", "stop", "style", "svg", "switch", "symbol", "text", "textpath", "title", "tref", "tspan", "use", "view", "vkern"]
+    # From <https://developer.mozilla.org/en-US/docs/Web/SVG/Element>

     rxt.tags = _.object([tag, rxt.mktag(tag)] for tag in tags)
     rxt.rawHtml = (html) -> new RawHtml(html)
     rxt.importTags = (x) => _(x ? this).extend(rxt.tags)
-
-    #
-    # reactive vector graphics DSL
-    #
-
-    rxv = {}
-
-    setSVGProp = (elt, prop, val) ->
-      elt.setAttribute prop, val
-
-    # TODO: Remove duplicate mktag?
-    rxv.mktag = mktag = (tag) ->
-      (arg1, arg2) ->
-        # arguments are either (), (attrs: Object), (contents: non-Object), or
-        # (attrs: Object, contents: non-Object)
-        [attrs, contents] =
-          if not arg1? and not arg2?
-            [{}, null]
-          else if arg2?
-            [arg1, arg2]
-          else if _.isString(arg1) or arg1 instanceof SVGElement or _.isArray(arg1) or arg1 instanceof ObsCell or arg1 instanceof ObsArray
-              [{}, arg1]
-          else
-            [arg1, null]
-
-        elt = document.createElementNS('http://www.w3.org/2000/svg', tag)
-        for name, value of _.omit(attrs, _.keys(specialAttrs))
-            if value instanceof ObsCell
-              do (name) ->
-                value.onSet.sub ([old, val]) -> setSVGProp(elt, name, val)
-            else
-              setSVGProp(elt, name, value)
-
-        if contents?
-          toNodes = (contents) ->
-            for child in contents
-              if _.isString(child)
-                document.createTextNode(child)
-              else if child instanceof SVGElement
-                child
-              else
-                throw 'Unknown element type in array: ' + child.constructor.name
-
-          updateContents = (contents) ->
-            (elt.removeChild elt.firstChild) while elt.firstChild
-            if _.isArray(contents)
-              (elt.appendChild node) for node in toNodes(contents)
-            else if _.isString(contents)
-                updateContents([contents])
-              else
-                  throw 'Unknown type for contents: ' + contents.constructor.name
-
-          if contents instanceof ObsArray
-            contents.onChange.sub ([index, removed, added]) ->
-              (elt.removeChild elt.childNodes[index]) for i in [0...removed.length]
-              toAdd = toNodes(added)
-              if index == elt.childNodes.length
-                (elt.appendChild node) for node in toAdd
-              else
-                (elt.childNodes[index].insertBefore node) for node in toAdd
-
-
-          else if contents instanceof ObsCell
-            contents.onSet.sub(([old, val]) -> updateContents(val))
-
-          else
-            updateContents(contents)
-
-        for key of attrs when key of specialAttrs
-          specialAttrs[key](elt, attrs[key], attrs, contents)
-        elt
-
-
-    # From <https://developer.mozilla.org/en-US/docs/Web/SVG/Element>
-
-    svg_tags =  ["a", "altglyph", "altglyphdef", "altglyphitem", "animate", "animatecolor", "animatemotion", "animatetransform", "circle", "clippath", "color-profile", "cursor", "defs", "desc", "ellipse", "feblend", "fecolormatrix", "fecomponenttransfer", "fecomposite", "feconvolvematrix", "fediffuselighting", "fedisplacementmap", "fedistantlight", "feflood", "fefunca", "fefuncb", "fefuncg", "fefuncr", "fegaussianblur", "feimage", "femerge", "femergenode", "femorphology", "feoffset", "fepointlight", "fespecularlighting", "fespotlight", "fetile", "feturbulence", "filter", "font", "font-face", "font-face-format", "font-face-name", "font-face-src", "font-face-uri", "foreignobject", "g", "glyph", "glyphref", "hkern", "image", "line", "lineargradient", "marker", "mask", "metadata", "missing-glyph", "mpath", "path", "pattern", "polygon", "polyline", "radialgradient", "rect", "script", "set", "stop", "style", "svg", "switch", "symbol", "text", "textpath", "title", "tref", "tspan", "use", "view", "vkern"]
-    rxv.tags = _.object([tag, rxv.mktag(tag)] for tag in svg_tags)
-    rxv.importTags = (x) => _(x ? this).extend(rxv.tags)
-
-    rx.rxv = rxv

     #
     # rxt utilities


Add console logging

Add console logging

--- src/reactive.coffee
+++ src/reactive.coffee
@@ -875,8 +875,9 @@
         elt = document.createElementNS('http://www.w3.org/2000/svg', tag)
         console.log("svg_mktag createElementNS #{elt}")
         for name, value of _.omit(attrs, _.keys(specialAttrs))
+          console.log('svg_mktag +setSVGProp')
           setSVGProp(elt, name, value)
-        console.log("svg_mktag setSVGProp #{elt}")
+        console.log("svg_mktag -setSVGProp #{elt}")
         if contents?
           console.log("svg_mktag contents #{elt}")
           if contents instanceof ObsArray


remove junk

remove junk

--- src/client/src/fib.coffee
+++ src/client/src/fib.coffee
@@ -1,31 +0,0 @@
-{div,ul,li} = React.DOM
-
-Fib = React.createClass {
-  getInitialState: ->
-    {fibs: [1]}
-
-  clicked: ->
-    fibs = @state.fibs
-    n = fibs.length
-    if n == 0 or n == 1
-      i = 1
-    else
-      i = fibs[n-1] + fibs[n-2]
-    fibs.push(i)
-    @setState fibs: fibs
-
-  render: ->
-    items = for n in @state.fibs
-      li({},"fib(#{_i}): #{n}")
-    div(
-      {onClick: @clicked},
-      @props.children,
-      ul({className: "fibs"},items),
-      div({},"clicked #{@state.fibs.length} times")
-      )
-  }
-
-$ ->
-  React.renderComponent \
-    Fib(),
-    $("#react-sandbox")[0]


Add an option to disable livereload

Add an option to disable livereload


Improved controls layout for pages preset

--- src/coffee/presets.coffee
+++ src/coffee/presets.coffee
@@ -72,8 +72,8 @@
       visibleItems: 1
       controls:
         margin:
-          x: 5
-          y: 5
+          x: 0
+          y: 0
       strings:
         prev: ' '
         next: ' '
@@ -189,8 +189,8 @@
         stepSize: 9 # Should be same as group size in special options
         style:
           width: 292
-          right: 0
-          bottom: 0
+          right: 3
+          bottom: 5

       funcDiff: (d, r, i) ->
         Math.abs d


Preset "products" is now called "scroller" as in the docs

--- src/coffee/presets.coffee
+++ src/coffee/presets.coffee
@@ -1,6 +1,6 @@
 ###!
   Presets for jQuery rondell plugin
-
+
   @author Sebastian Helzle (sebastian@helzle.net or @sebobo)
   @category jQuery plugin
   @copyright (c) 2009-2012 Sebastian Helzle (www.sebastianhelzle.net)
@@ -10,49 +10,49 @@
 (($) ->
   $.rondell ||= {}
   $.rondell.presets =
-
+
     carousel:
       autoRotation:
         enabled: true
         direction: 1
         once: false
         delay: 5000
-      radius:
+      radius:
         x: 240
         y: 50
       center:
-        left: 340
+        left: 340
         top: 160
-      controls:
-        margin:
+      controls:
+        margin:
           x: 130
           y: 260
       randomStart: true
       currentLayer: 1
       funcSize: (l, r, i) ->
         1 / Math.abs(l)
-
-    products:
+
+    scroller:
       repeating: false
       alwaysShowCaption: true
       visibleItems: 4
       theme: "dark"
       itemProperties:
         delay: 0
-        size:
+        size:
           width: 100
           height: 200
-        sizeFocused:
+        sizeFocused:
           width: 300
           height: 200
       center:
-        left: 400
+        left: 400
         top: 100
-      size:
+      size:
         width: 800
         height: 200
-      controls:
-        margin:
+      controls:
+        margin:
           x: 210
           y: 158
       funcTop: (l, r, i) ->
@@ -63,10 +63,10 @@
         r.center.left + (l - 0.5) * r.itemProperties.size.width
       funcOpacity: (l, r, i) ->
         0.8
-
+
     pages:
-      radius:
-        x: 0
+      radius:
+        x: 0
         y: 0
       scaling: 1
       theme: "page"
@@ -75,15 +75,15 @@
         margin:
           x: 0
           y: 0
-      strings:
+      strings:
         prev: ' '
         next: ' '
-      center:
-        left: 200
+      center:
+        left: 200
         top: 200
-      itemProperties:
-        size:
-          width: 400
+      itemProperties:
+        size:
+          width: 400
           height: 400
       funcTop: (l, r, i) ->
           r.center.top - r.itemProperties.size.height / 2
@@ -91,18 +91,18 @@
           r.center.left + l * r.itemProperties.size.width
       funcDiff: (l, r, i) ->
           Math.abs(l) + 0.5
-
+
     cubic:
-      center:
-        left: 300
+      center:
+        left: 300
         top: 200
       visibleItems: 5
-      itemProperties:
-        size:
+      itemProperties:
+        size:
           width: 350
-          height: 350
-        sizeFocused:
-          width: 350
+          height: 350
+        sizeFocused:
+          width: 350
           height: 350
       controls:
         margin:
@@ -114,10 +114,10 @@
           r.center.left - r.itemProperties.size.width / 2 + Math.sin(l) * r.radius.x
       funcSize: (l, r, i) ->
           Math.pow((Math.PI - Math.abs(l)) / Math.PI, 3)
-
+
     gallery:
       # Custom options
-      special:
+      special:
         itemPadding: 2
       # Standard rondell options
       visibleItems: 5
@@ -135,12 +135,12 @@
           y: 255
       itemProperties:
         delay: 0
-        sizeFocused:
+        sizeFocused:
           width: 480
           height: 280
         size:
           width: 80
-          height: 100
+          height: 100
       funcTop: (l, r, i) ->
         r.size.height - r.itemProperties.size.height - r.special.itemPadding
       funcDiff: (d, r, i) ->
@@ -152,14 +152,14 @@

     thumbGallery:
       # Custom options
-      special:
+      special:
         columns: 3
         rows: 3
         groupSize: 9
         itemPadding: 5
         thumbsOffset:
           x: 500
-          y: 0
+          y: 0
       # Standard rondell options
       visibleItems: 9
       wrapIndices: false
@@ -179,12 +179,12 @@
           y: 255
       itemProperties:
         delay: 40
-        sizeFocused:
+        sizeFocused:
           width: 480
           height: 420
         size:
           width: 94
-          height: 126
+          height: 126
       scrollbar:
         enabled: true
         stepSize: 9 # Should be same as group size in special options
@@ -230,7 +230,7 @@
         r.special.thumbsOffset.x + r.special.itemPadding \
           + (column + r.special.columns * groupOffset) \
           * (r.itemProperties.size.width + r.special.itemPadding)
-
+
     slider:
       theme: 'slider'
       visibleItems: 1
@@ -238,14 +238,14 @@
       opacityMin: 0.01
       autoRotation:
         enabled: true
-      center:
+      center:
         top: 150
         left: 300
       size:
         height: 300
         width: 600
       controls:
-        margin:
+        margin:
           x: -1
           y: 135
       strings:
@@ -264,5 +264,5 @@
         0
       funcOpacity: (l, r, i) ->
         0.02
-
-)(jQuery)
+
+)(jQuery)


Fix "brunch watch" error.

Fix "brunch watch" error.

--- src/compilers/base.coffee
+++ src/compilers/base.coffee
@@ -42,4 +42,5 @@
     clearTimeout @timeout if @timeout?
     @timeout = setTimeout (=> @clearQueue callback), 20

-  matchesFile: (file) -> _.any @patterns(), (pt) -> file.match pt
+  matchesFile: (file) ->
+    @patterns().some (pattern) -> file.match pattern


Attempt some colon consistency in object literals.

--- src/coffeelint.coffee
+++ src/coffeelint.coffee
@@ -125,7 +125,7 @@
             idiomatic CoffeeScript.
             """

-    no_trailing_semicolons:
+    no_trailing_semicolons :
         level : ERROR
         message : 'Line contains a trailing semicolon'
         description: """
@@ -142,7 +142,7 @@
             Trailing semicolons are forbidden by default.
             """

-    no_plusplus:
+    no_plusplus :
         level : IGNORE
         message : 'The increment and decrement operators are forbidden'
         description: """
@@ -153,7 +153,7 @@
             This rule is disabled by default.
             """

-    no_throwing_strings:
+    no_throwing_strings :
         level : ERROR
         message : 'Throwing strings is forbidden'
         description: """
@@ -177,12 +177,12 @@
             This rule is enabled by default.
             """

-    cyclomatic_complexity:
+    cyclomatic_complexity :
         value : 10
         level : IGNORE
         message : 'The cyclomatic complexity is too damn high'

-    no_backticks:
+    no_backticks :
         level : ERROR
         message : 'Backticks are forbidden'
         description: """
@@ -194,7 +194,7 @@
             This rule is enabled by default.
             """

-    line_endings:
+    line_endings :
         level : IGNORE
         value : 'unix' # or 'windows'
         message : 'Line contains incorrect line endings'
@@ -301,15 +301,15 @@
 regexes =
     trailingWhitespace : /[^\s]+[\t ]+\r?$/
     lineHasComment : /^\s*[^\#]*\#/
-    indentation: /\S/
-    longUrlComment: ///
+    indentation : /\S/
+    longUrlComment : ///
       ^\s*\# # indentation, up to comment
       \s*
       http[^\s]+$ # Link that takes up the rest of the line without spaces.
     ///
-    camelCase: /^[A-Z][a-zA-Z\d]*$/
-    trailingSemicolon: /;\r?$/
-    configStatement: /coffeelint:\s*(disable|enable)(?:=([\w\s,]*))?/
+    camelCase : /^[A-Z][a-zA-Z\d]*$/
+    trailingSemicolon : /;\r?$/
+    configStatement : /coffeelint:\s*(disable|enable)(?:=([\w\s,]*))?/

 # Patch the source properties onto the destination.
@@ -338,8 +338,8 @@

 # Store suppressions in the form of { line #: type }
 block_config =
-    enable: {}
-    disable: {}
+    enable : {}
+    disable : {}

 #
 # A class that performs regex checks on each line of the source.
@@ -362,9 +362,9 @@
         #   classIndents: the number of indents within a class
         @context = {
             class: {
-                inClass: false
-                lastUnemptyLineInClass: null
-                classIndents: null
+                inClass : false
+                lastUnemptyLineInClass : null
+                classIndents : null
             }
         }

@@ -519,7 +519,7 @@
         @tokensByLine[@lineNumber] || []

     # maintain the contextual information for class-related stuff
-    maintainClassContext: () ->
+    maintainClassContext : () ->
         if @context.class.inClass
             if @lineHasToken 'INDENT'
                 @context.class.classIndents++
@@ -604,7 +604,7 @@
                 @lintMath(token)
             else null

-    lintUnary: (token) ->
+    lintUnary : (token) ->
         if token[1] is 'new'
             # Find the last chained identifier, e.g. Bar in new foo.bar.Bar().
             identifierIndex = 1
@@ -681,7 +681,7 @@
         else
             null

-    lintMath: (token) ->
+    lintMath : (token) ->
         if not token.spaced and not token.newLine
             @createLexError('space_operators', {context: token[1]})
         else
@@ -709,7 +709,7 @@
         else
             null

-    lintIdentifier: (token) ->
+    lintIdentifier : (token) ->
         key = token[1]

         # Class names might not be in a scope
@@ -754,7 +754,7 @@
         else
             return null

-    lintJavascript :(token) ->
+    lintJavascript : (token) ->
         @createLexError('no_backticks')

     lintThrow : (token) ->
@@ -768,7 +768,7 @@
         attrs = {context : "found '#{token[0]}'"}
         @createLexError('no_plusplus', attrs)

-    lintStandaloneAt: (token) ->
+    lintStandaloneAt : (token) ->
         nextToken = @peek()
         spaced = token.spaced
         isIdentifier = nextToken[0] == 'IDENTIFIER'


Add failing test for #41

Add failing test for #41

--- spec/tabs-to-spaces-spec.coffee
+++ spec/tabs-to-spaces-spec.coffee
@@ -215,3 +215,16 @@
         editor.setText('    foo\n    bar\n\n')
         editor.save()
         expect(editor.getText()).toBe '    foo\n    bar\n\n'
+
+  describe 'invariants', ->
+    beforeEach ->
+      editor.setText(fs.readFileSync(__filename, 'utf8'))
+
+    it 'does not move the position of the cursor', ->
+      editor.setCursorBufferPosition([0, 5])
+      TabsToSpaces.tabify(editor)
+      TabsToSpaces.untabify(editor)
+
+      pos = editor.getCursorBufferPosition()
+      expect(pos.row).toBe 0
+      expect(pos.column).toBe 5


JavaScript, text getter: Only force add a semicolon at the end if non-empty.

--- lib/assets/JavaScript.coffee
+++ lib/assets/JavaScript.coffee
@@ -52,7 +52,7 @@
         text = outputStream.get()

         # Always end with a semicolon like the UglifyJS binary
-        text = text.replace(/;*$/, ';')
+        if text.length > 0 then text = text.replace(/;*$/, ';')

         # Workaround for https://github.com/mishoo/UglifyJS2/issues/180
         if parseTree.end and parseTree.end.comments_before and not parseTree.end._comments_dumped


Pseudocommit to push stuff to server

Pseudocommit to push stuff to server

--- src/controller.coffee
+++ src/controller.coffee
@@ -129,6 +129,9 @@

       # Scroll offset
       @scrollOffset = new draw.Point 0, 0
+
+      # Touchscreen scrolling fields
+      @touchScrollAnchor = null

       offset = null
       highlight = null
@@ -754,7 +757,7 @@
           when event.offsetX? then new draw.Point event.offsetX - PALETTE_WIDTH, event.offsetY + @scrollOffset.y
           when event.layerX then new draw.Point event.layerX - PALETTE_WIDTH, event.layerY + @scrollOffset.y

-      performNormalMouseDown = (point) =>
+      performNormalMouseDown = (point, isTouchEvent) =>

         # See what we picked up
         @ephemeralSelection = hitTestFloating(point) ?
@@ -764,27 +767,31 @@
           hitTestPalette(point)

         if not @ephemeralSelection?
-          # If we haven't clicked on any clickable element, then LASSO SELECT, indicated by (@lassoAnchor?)
-
-          # If there is already a selection, remove it.
-          if @lassoSegment?
-
-            # First, check to see if the block is floating
-            flag = false
-            for float in @floatingBlocks
-              if float.block is @lassoSegment
-                flag = true
-                break
-
-            # Don't remove the segment if it's floating, because it still needs to hold those blocks together
-            unless flag
-              @lassoSegment.remove()
-
-            @lassoSegment = null
-            @redraw()
-
-          # Set the lasso anchor
-          @lassoAnchor = point
+          if isTouchEvent
+            @touchScrollAnchor = point
+
+          else
+            # If we haven't clicked on any clickable element, then LASSO SELECT, indicated by (@lassoAnchor?)
+
+            # If there is already a selection, remove it.
+            if @lassoSegment?
+
+              # First, check to see if the block is floating
+              flag = false
+              for float in @floatingBlocks
+                if float.block is @lassoSegment
+                  flag = true
+                  break
+
+              # Don't remove the segment if it's floating, because it still needs to hold those blocks together
+              unless flag
+                @lassoSegment.remove()
+
+              @lassoSegment = null
+              @redraw()
+
+            # Set the lasso anchor
+            @lassoAnchor = point

         else if @ephemeralSelection.type is 'socket'
           # If we have clicked on a socket, then TEXT INPUT, indicated by (@isEditingText())
@@ -823,14 +830,14 @@
         # the focus from any sockets
         @hiddenInput.blur()

-        performNormalMouseDown getPointFromEvent event
+        performNormalMouseDown getPointFromEvent(event), false

       track.addEventListener 'touchstart', (event) =>
         event.preventDefault()

         @hiddenInput.blur()

-        performNormalMouseDown getPointFromEvent event.changedTouches[0]
+        performNormalMouseDown getPointFromEvent(event.changedTouches[0]), true

       # ## Mouse events for NORMAL DRAG ##

@@ -915,6 +922,8 @@
           drag.style.webkitTransform =
             drag.style.mozTransform =
             drag.style.transform = "translate(#{fixedDest.x}px, #{fixedDest.y}px)"
+        else if @touchScrollAnchor?
+          @scrollOffset.x = @touchScrollAnchor.from(point).x

       # Bind this mousemove function to mousemove. We need to add some
       # wrapper functions for touchmove, since multitouch works slightly differently
@@ -1022,6 +1031,8 @@

       track.addEventListener 'touchend', (event) =>
         event.preventDefault()
+
+        @touchScrollAnchor = null

         performNormalMouseUp event.changedTouches[0]


Add default callbacks to request

Add default callbacks to request

--- src/controller.coffee
+++ src/controller.coffee
@@ -44,6 +44,11 @@

   request: (url, options = {}) =>
     options = Utils.reverseMerge options,
+      onProgress: -> {}
+      onRequestStart: -> {}
+      onRequestError: -> {}
+      onRequestEnd: -> {}
+      onProgress: -> {}
       pushState: true
       queue: 'sync'


make setEditorState idempotent

make setEditorState idempotent

--- src/controller.coffee
+++ src/controller.coffee
@@ -3854,7 +3854,8 @@

   Editor::setEditorState = (useBlocks) ->
     if useBlocks
-      @setValue @getAceValue()
+      unless @currentlyUsingBlocks
+        @setValue @getAceValue()

       @dropletElement.style.top = '0px'
       if @paletteEnabled
@@ -3873,13 +3874,15 @@
         @highlightCanvas.opacity = 1

       @resizeBlockMode(); @redrawMain()
-
+
     else
       paletteVisibleInNewState = @paletteEnabled and @showPaletteInTextMode

       oldScrollTop = @aceEditor.session.getScrollTop()

-      @setAceValue @getValue()
+      if @currentlyUsingBlocks
+        @setAceValue @getValue()
+
       @aceEditor.resize true

       @aceEditor.session.setScrollTop oldScrollTop


Add guards against the default plugin (d3.tip) being absent

--- src/d3-flame-graph.coffee
+++ src/d3-flame-graph.coffee
@@ -118,7 +118,7 @@
         "rgb(#{r}, #{g}, #{b})"
       @_tooltipEnabled = true
       @_zoomEnabled = true
-      @_tooltipPlugin = d3.tip()
+      @_tooltipPlugin = d3.tip() if @_tooltipEnabled and d3.tip

       # initial processing of data
       @console.time('augment')
@@ -279,6 +279,7 @@
       @

     _renderTooltip: () ->
+      return @ if not @_tooltipPlugin or not @_tooltipEnabled
       @tip = @_tooltipPlugin
         .attr('class', 'd3-tip')
         .html(@tooltip())


Improve drag magic numbers

Improve drag magic numbers

--- src/controller.coffee
+++ src/controller.coffee
@@ -7,13 +7,13 @@
   # ## Magic constants
   PALETTE_TOP_MARGIN = 5
   PALETTE_MARGIN = 5
-  MIN_DRAG_DISTANCE = 5
+  MIN_DRAG_DISTANCE = 1
   PALETTE_LEFT_MARGIN = 5
   DEFAULT_INDENT_DEPTH = '  '
   ANIMATION_FRAME_RATE = 60
   TOP_TAB_HEIGHT = 20
   DISCOURAGE_DROP_TIMEOUT = 1000
-  MAX_DROP_DISTANCE = 50
+  MAX_DROP_DISTANCE = 100

   ANY_DROP = 0
   BLOCK_ONLY = 1
@@ -159,8 +159,8 @@
         indentTongueHeight: 20
         tabOffset: 10
         tabWidth: 15
-        tabHeight: 5
-        tabSideWidth: 0.125
+        tabHeight: 4
+        tabSideWidth: 1 / 4
         dropAreaHeight: 20
         indentDropAreaMinWidth: 50
         emptySocketWidth: 20
@@ -922,7 +922,7 @@

       mainPoint = @trackerPointToMain(position)

-      best = null; min = MAX_DROP_DISTANCE
+      best = null; min = Infinity

       # Find the closest droppable block
       testPoints = @dropPointQuadTree.retrieve {
@@ -932,8 +932,9 @@
         h: MAX_DROP_DISTANCE * 2
       }, (point) =>
         unless point._ice_needs_shift and not @shiftKeyPressed
-          distance = mainPoint.from(point).magnitude()
-          if distance < min
+          distance = mainPoint.from(point)
+          distance.y *= 2; distance = distance.magnitude()
+          if distance < min and mainPoint.from(point).magnitude() < MAX_DROP_DISTANCE
             best = point._ice_node
             min = distance


Tighten pagination.

Tighten pagination.

--- app/assets/javascripts/application/pods_list.js.coffee
+++ app/assets/javascripts/application/pods_list.js.coffee
@@ -15,19 +15,21 @@
   previous_offset: ->
     @index - @maxPerPage
   paginationPosition: (index) ->
+    innerPadding = 3
+    outerPadding = 1
     if index == @index
       'current'
     else if index > @index
-      barrier = (@index + 3*@maxPerPage)
-      if index < barrier || index > (@totalCount - 3*@maxPerPage)
+      barrier = (@index + innerPadding*@maxPerPage)
+      if index < barrier || index > (@totalCount - outerPadding*@maxPerPage)
         'within'
       else if index == barrier
         'barrier'
       else
         'without'
     else if index < @index
-      barrier = (@index - 3*@maxPerPage)
-      if index > barrier || index < 3*@maxPerPage
+      barrier = (@index - innerPadding*@maxPerPage)
+      if index > barrier || index < outerPadding*@maxPerPage
         'within'
       else if index == barrier
         'barrier'


fix onchange listener

fix onchange listener

--- backend/replication.coffee
+++ backend/replication.coffee
@@ -45,7 +45,8 @@
         .nodeify callback

-    # Specify which way to replicate
+    # Give the right pouch function to run the replication depending on
+    # parameters.
     getReplicator: (toRemote, fromRemote) ->
         if fromRemote and not toRemote
             log.info "Running replication from remote database"
@@ -98,7 +99,8 @@
                 filesystem.buildTree null, unlockFileSystemAndReturn

         onChange = (info) ->
-            changeMessage = "DB change: #{info.change.docs_written} doc(s) written"
+            console.log info
+            changeMessage = "DB change: #{info.docs_written} doc(s) written"

             # Specify direction
             if info.direction
@@ -106,7 +108,7 @@

             # Find out if filesystem tree needs a rebuild
             if (not info.direction? and fromRemote and info.docs_written > 0) \
-            or (info.direction is 'pull' and info.change.docs_written > 0)
+            or (info.direction is 'pull' and info.docs_written > 0)
                 needTreeRebuild = rebuildFs

             log.info changeMessage


Use arrow functions when inserting specs

--- user/snippets.cson
+++ user/snippets.cson
@@ -72,10 +72,10 @@
 		body: "console.info($1);$0"
 	Spec:
 		prefix: "it"
-		body: 'it("$1", function(){\n\t$2\n});'
+		body: 'it("$1", () => {\n\t$2\n});'
 	"Spec Description":
 		prefix: "ds"
-		body: 'describe("$1", function(){\n\t$2\n});'
+		body: 'describe("$1", () => {\n\t$2\n});'
 	Strict:
 		prefix: "js"
 		body: '"use strict";\n\n$1'


minor solution text tweak

minor solution text tweak

--- planck.coffee
+++ planck.coffee
@@ -330,7 +330,7 @@

               init.append "text"
                 .classed solution: true
-                .attr x: sliderCircle.r * 3, dy: ".35em"
+                .attr x: sliderCircle.r * 2.5, dy: ".35em"
                 .style fill: temperatureColor

           slider.append "g"
@@ -344,7 +344,7 @@
                   .attr dy: ".35em", x: sliderCircle.cx, "text-anchor": "middle"
                 slideHandle.append "text"
                   .classed solution: true
-                  .attr "text-anchor": "end", dy: ".35em"
+                  .attr "text-anchor": "end", dy: ".35em", dx: -5

       sliderReferences = slider.selectAll ".reference"
       slideHandle = slider.select ".handle"


Add test for failing case in bug report

Add test for failing case in bug report


Extend list of recognised file-extensions

--- grammars/roff.cson
+++ grammars/roff.cson
@@ -17,15 +17,19 @@
 	"mandoc",
 	"mdoc",
 	"me",
+	"mmn", "mmt",
 	"ms",
 	"mom",
 	"n",
+	"nroff",
 	"rnh",
 	"rno",
 	"roff",
 	"run",
 	"t",
-	"tr"
+	"tmac", "tmac-u",
+	"tr",
+	"troff"
 ]
 firstLineMatch: """(?x)


adding test for #2037; should've included it in the first place

--- test/comments.coffee
+++ test/comments.coffee
@@ -202,3 +202,6 @@
     b: ->

   ok B.prototype.a instanceof Function
+
+test "#2037: herecomments shouldn't imply line terminators", ->
+  do (-> ### ###; fail)


better comment

better comment

--- public/coffeescripts/renderer.coffee
+++ public/coffeescripts/renderer.coffee
@@ -116,7 +116,8 @@
   edgeHitTest: (edge, point, thresholdInPixels) ->
     # we have an edge hit if
     #   1) distance < threshold AND
-    #   2) angle between edge and click vectors is acute (otherwise click is "behind" attached nodes)
+    #   2) angle between edge and click vectors is acute
+    #      (otherwise click is "behind" attached nodes; the distance calculation assumes infinite edge length)
     sys = particleSystem
     p1 = sys.toScreen edge.source.p
     p2 = sys.toScreen edge.target.p


added key attribute to elements in array and iterator.

--- imports/ui/appstore.cjsx
+++ imports/ui/appstore.cjsx
@@ -67,13 +67,13 @@
                   <button type="button" className="btn btn-default" disabled=true>version: {@state.selectedApp.version}</button>
                   {if @props.definitions.length > 1
                     [
-                      <button type="button" className="btn btn-default dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
+                      <button key="button" type="button" className="btn btn-default dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">

                         Select version
                       </button>
-

+

                       {@props.definitions.map (app) =>
-
{app.version}

+
{app.version}

                       }

                     ]
@@ -83,8 +83,8 @@

                 {if Helpers.isAuthenticated()
                   [
-                    <button type="button" className="btn btn-success btn-lg dropdown-toggle" data-toggle="dropdown">file_download Install</button>
-                    <form role="form" className="dropdown-menu dropdown-menu-right" style={padding:10}>
+                    <button key="button" type="button" className="btn btn-success btn-lg dropdown-toggle" data-toggle="dropdown">file_download Install</button>
+                    <form key="form" role="form" className="dropdown-menu dropdown-menu-right" style={padding:10}>

                             Installing may overwrite an existing app called {@state.selectedApp.name} with version {@state.selectedApp.version}.