deal with missing results better

deal with missing results better

--- app/controllers/home.coffee
+++ app/controllers/home.coffee
@@ -79,27 +79,36 @@
           expectedToday = (f for f in forecast when f?.ParameterName in ['O3','PM2.5'] and f?.DateForecast.trim() is today)[0]
           expectedTomorrow = (f for f in forecast when f?.ParameterName in ['O3','PM2.5'] and f?.DateForecast.trim() is tomorrow)[0]

+          frames = [
+            {
+              index: 0
+              text: "Air Quality for #{zip} (#{expectedToday?.ReportingArea}, #{expectedToday?.StateCode})"
+              icon: cloudsIcon
+            }
+          ]
+
+          if now
+            frames.push {
+              index: 1
+              text: "Current AQI #{now.AQI} (#{now?.Category?.Name}) today (#{now.ParameterName})"
+              icon: levelIcons[now?.Category?.Name || "Unavailable"]
+            }
+
+          if expectedToday
+            frames.push {
+              index: 2
+              text: "AQI #{expectedToday.AQI} (#{expectedToday?.Category?.Name}) expected today (#{expectedToday.ParameterName})"
+              icon: levelIcons[expectedToday?.Category?.Name || "Unavailable"]
+            }
+
+          if expectedTomorrow
+            frames.push {
+              index: 3
+              text: "AQI #{expectedTomorrow?.AQI} (#{expectedTomorrow?.Category?.Name}) expected tomorrow (#{expectedTomorrow.ParameterName})"
+              icon: levelIcons[faqiForcastTomorrow?.Category?.Name || "Unavailable"]
+            }
+
+
           res.json
-            frames: [
-              {
-                index: 0
-                text: "Air Quality for #{expectedToday.ReportingArea}, #{expectedToday.StateCode} (#{zip})"
-                icon: cloudsIcon
-              },
-              {
-                index: 1
-                text: "Current AQI #{now.AQI} (#{now?.Category?.Name}) today (#{now.ParameterName})"
-                icon: levelIcons[now?.Category?.Name || "Unavailable"]
-              },
-              {
-                index: 2
-                text: "AQI #{expectedToday.AQI} (#{expectedToday?.Category?.Name}) expected today (#{expectedToday.ParameterName})"
-                icon: levelIcons[expectedToday?.Category?.Name || "Unavailable"]
-              },
-              {
-                index: 3
-                text: "AQI #{expectedTomorrow?.AQI} (#{expectedTomorrow?.Category?.Name}) expected tomorrow (#{expectedTomorrow.ParameterName})"
-                icon: levelIcons[faqiForcastTomorrow?.Category?.Name || "Unavailable"]
-              },
-            ]
+            frames: frames


:art: simplify

:art: simplify

--- lib/operation-stack.coffee
+++ lib/operation-stack.coffee
@@ -25,7 +25,7 @@
     klass = Base.getClass(klass) if _.isString(klass)
     try
       #  To support, `dd`, `cc` and a like.
-      if @isSameOperatorRepeated(klass)
+      if @stack.length and (@peekTop().constructor is klass)
         klass = Base.getClass('MoveToRelativeLine')
       op = new klass(@vimState, properties)
       if @vimState.isMode('visual') and _.isFunction(op.select)
@@ -42,9 +42,6 @@
         throw error
     finally
       @processing = false
-
-  isSameOperatorRepeated: (klass) ->
-    @vimState.isMode('operator-pending') and (@peekTop().constructor is klass)

   isProcessing: ->
     @processing


Now there is no chance to array of operation is pushed.

--- lib/operation-stack.coffee
+++ lib/operation-stack.coffee
@@ -15,42 +15,38 @@

   # Private: Push the given operations onto the operation stack, then process
   # it.
-  push: (operations) ->
-    return unless operations?
+  push: (op) ->
+    return unless op?
     if @isEmpty() and settings.debug()
       debug "#=== Start at #{new Date().toISOString()}"
       if settings.debugOutput() is 'console'
         console.clear()

     @withLock =>
-      operations = [operations] unless _.isArray(operations)
+      # Motions in visual mode perform their selections.
+      if @vimState.isVisualMode() and _.isFunction(op.select)
+        @stack.push(new Operators.Select(@vimState))

-      for operation in operations
-        # Motions in visual mode perform their selections.
-        if @vimState.isVisualMode() and _.isFunction(operation.select)
-          # unless operation.isRepeat()
-          @stack.push(new Operators.Select(@vimState))
+      #  To support, `dd`, `cc`, `yy` `>>`, `<<`, `==`
+      if @vimState.isOperatorPendingMode() and
+          (op.getKind() in completableOperators) and @isSameOperatorPending(op)
+        op = new MoveToRelativeLine(@vimState)

-        #  To support, `dd`, `cc`, `yy` `>>`, `<<`, `==`
-        if @vimState.isOperatorPendingMode() and
-            (operation.getKind() in completableOperators) and @isSameOperatorPending(operation)
-          operation = new MoveToRelativeLine(@vimState)
+      # if we have started an operation that responds to canComposeWith check if it can compose
+      # with the operation we're going to push onto the stack
+      if (topOperation = @peekTop())? and topOperation.canComposeWith? and not topOperation.canComposeWith(op)
+        @vimState.resetNormalMode()
+        @vimState.emitter.emit('failed-to-compose')
+        return

-        # if we have started an operation that responds to canComposeWith check if it can compose
-        # with the operation we're going to push onto the stack
-        if (topOperation = @peekTop())? and topOperation.canComposeWith? and not topOperation.canComposeWith(operation)
-          @vimState.resetNormalMode()
-          @vimState.emitter.emit('failed-to-compose')
-          break
+      @stack.push op

-        @stack.push operation
+      # If we've received an operator in visual mode, use inplict currentSelection textobject
+      # as a target of operator.
+      if @vimState.isVisualMode() and op.isOperator?()
+        @stack.push(new TextObjects.CurrentSelection(@vimState.editor, @vimState))

-        # If we've received an operator in visual mode, use inplict currentSelection textobject
-        # as a target of operator.
-        if @vimState.isVisualMode() and operation.isOperator?()
-          @stack.push(new TextObjects.CurrentSelection(@vimState.editor, @vimState))
-
-        @process()
+      @process()

     for cursor in @vimState.editor.getCursors()
       @vimState.ensureCursorIsWithinLine(cursor)


DRY up error messages

DRY up error messages

--- lib/package-manager.coffee
+++ lib/package-manager.coffee
@@ -32,21 +32,22 @@

   loadInstalled: (callback) ->
     args = ['ls', '--json']
+    errorMessage = 'Fetching local packages failed.'
     apmProcess = @runCommand args, (code, stdout, stderr) ->
       if code is 0
         try
           packages = JSON.parse(stdout)
         catch parseError
-          error = createJsonParseError('Fetching local packages failed.', parseError, stdout)
-          return callback(error)
-        callback(null, packages)
-      else
-        error = new Error('Fetching local packages failed.')
-        error.stdout = stdout
-        error.stderr = stderr
-        callback(error)
-
-    handleProcessErrors(apmProcess, 'Fetching local packages failed.', callback)
+          error = createJsonParseError(errorMessage, parseError, stdout)
+          return callback(error)
+        callback(null, packages)
+      else
+        error = new Error(errorMessage)
+        error.stdout = stdout
+        error.stderr = stderr
+        callback(error)
+
+    handleProcessErrors(apmProcess, errorMessage, callback)

   loadFeatured: (loadThemes, callback) ->
     unless callback
@@ -57,85 +58,89 @@
     version = atom.getVersion()
     args.push('--themes') if loadThemes
     args.push('--compatible', version) if semver.valid(version)
-
-    apmProcess = @runCommand args, (code, stdout, stderr) ->
-      if code is 0
-        try
-          packages = JSON.parse(stdout) ? []
-        catch parseError
-          error = createJsonParseError('Fetching featured packages failed.', parseError, stdout)
-          return callback(error)
-
-        callback(null, packages)
-      else
-        error = new Error('Fetching featured packages failed.')
-        error.stdout = stdout
-        error.stderr = stderr
-        callback(error)
-
-    handleProcessErrors(apmProcess, 'Fetching featured packages failed.', callback)
+    errorMessage = 'Fetching featured packages failed.'
+
+    apmProcess = @runCommand args, (code, stdout, stderr) ->
+      if code is 0
+        try
+          packages = JSON.parse(stdout) ? []
+        catch parseError
+          error = createJsonParseError(errorMessage, parseError, stdout)
+          return callback(error)
+
+        callback(null, packages)
+      else
+        error = new Error(errorMessage)
+        error.stdout = stdout
+        error.stderr = stderr
+        callback(error)
+
+    handleProcessErrors(apmProcess, errorMessage, callback)

   loadOutdated: (callback) ->
     args = ['outdated', '--json']
     version = atom.getVersion()
     args.push('--compatible', version) if semver.valid(version)
-
-    apmProcess = @runCommand args, (code, stdout, stderr) ->
-      if code is 0
-        try
-          packages = JSON.parse(stdout) ? []
-        catch parseError
-          error = createJsonParseError('Fetching outdated packages and themes failed.', parseError, stdout)
-          return callback(error)
-
-        callback(null, packages)
-      else
-        error = new Error('Fetching outdated packages and themes failed.')
-        error.stdout = stdout
-        error.stderr = stderr
-        callback(error)
-
-    handleProcessErrors(apmProcess, 'Fetching outdated packages and themes failed.', callback)
+    errorMessage = 'Fetching outdated packages and themes failed.'
+
+    apmProcess = @runCommand args, (code, stdout, stderr) ->
+      if code is 0
+        try
+          packages = JSON.parse(stdout) ? []
+        catch parseError
+          error = createJsonParseError(errorMessage, parseError, stdout)
+          return callback(error)
+
+        callback(null, packages)
+      else
+        error = new Error(errorMessage)
+        error.stdout = stdout
+        error.stderr = stderr
+        callback(error)
+
+    handleProcessErrors(apmProcess, errorMessage, callback)

   loadPackage: (packageName, callback) ->
     args = ['view', packageName, '--json']
-
-    apmProcess = @runCommand args, (code, stdout, stderr) ->
-      if code is 0
-        try
-          packages = JSON.parse(stdout) ? []
-        catch parseError
-          error = createJsonParseError("Fetching package '#{packageName}' failed.", parseError, stdout)
-          return callback(error)
-
-        callback(null, packages)
-      else
-        error = new Error("Fetching package '#{packageName}' failed.")
-        error.stdout = stdout
-        error.stderr = stderr
-        callback(error)
-
-    handleProcessErrors(apmProcess, "Fetching package '#{packageName}' failed.", callback)
+    errorMessage = "Fetching package '#{packageName}' failed."
+
+    apmProcess = @runCommand args, (code, stdout, stderr) ->
+      if code is 0
+        try
+          packages = JSON.parse(stdout) ? []
+        catch parseError
+          error = createJsonParseError(errorMessage, parseError, stdout)
+          return callback(error)
+
+        callback(null, packages)
+      else
+        error = new Error(errorMessage)
+        error.stdout = stdout
+        error.stderr = stderr
+        callback(error)
+
+    handleProcessErrors(apmProcess, errorMessage, callback)

   loadCompatiblePackageVersion: (packageName, callback) ->
     args = ['view', packageName, '--json', '--compatible', @normalizeVersion(atom.getVersion())]
-
-    apmProcess = @runCommand args, (code, stdout, stderr) ->
-      if code is 0
-        try
-          packages = JSON.parse(stdout) ? []
-        catch parseError
-          error = createJsonParseError("Fetching package '#{packageName}' failed.", parseError, stdout)
-          return callback(error)
-
-        callback(null, packages)
-      else
-        error = new Error("Fetching package '#{packageName}' failed.")
-        error.stdout = stdout
-        error.stderr = stderr
-        callback(error)
-
-    handleProcessErrors(apmProcess, "Fetching package '#{packageName}' failed.", callback)
+    errorMessage = "Fetching package '#{packageName}' failed."
+
+    apmProcess = @runCommand args, (code, stdout, stderr) ->
+      if code is 0
+        try
+          packages = JSON.parse(stdout) ? []
+        catch parseError
+          error = createJsonParseError(errorMessage, parseError, stdout)
+          return callback(error)
+
+        callback(null, packages)
+      else
+        error = new Error(errorMessage)
+        error.stdout = stdout
+        error.stderr = stderr
+        callback(error)
+
+    handleProcessErrors(apmProcess, errorMessage, callback)

   getInstalled: ->
     Q.nbind(@loadInstalled, this)()
@@ -167,23 +172,25 @@
     else if options.packages
       args.push '--packages'

+    errorMessage = "Searching for \u201C#{query}\u201D failed."
+
     apmProcess = @runCommand args, (code, stdout, stderr) ->
       if code is 0
         try
           packages = JSON.parse(stdout) ? []
           deferred.resolve(packages)
         catch parseError
-          error = createJsonParseError("Searching for \u201C#{query}\u201D failed.", parseError, stdout)
+          error = createJsonParseError(errorMessage, parseError, stdout)
           deferred.reject(error)
       else
-        error = new Error("Searching for \u201C#{query}\u201D failed.")
+        error = new Error(errorMessage)
         error.stdout = stdout
         error.stderr = stderr
         deferred.reject(error)

     apmProcess.onWillThrowError ({error, handle}) ->
         handle()
-        deferred.reject(createProcessError("Searching for \u201C#{query}\u201D failed.", error))
+        deferred.reject(createProcessError(errorMessage, error))

     deferred.promise

@@ -262,6 +269,7 @@

     atom.packages.deactivatePackage(name) if atom.packages.isPackageActive(name)

+    errorMessage = "Uninstalling \u201C#{name}\u201D failed."
     onError = (error) =>
       @emitPackageEvent 'uninstall-failed', pack, error
       callback(error)
@@ -272,12 +280,12 @@
         callback?()
         @emitPackageEvent 'uninstalled', pack
       else
-        error = new Error("Uninstalling \u201C#{name}\u201D failed.")
+        error = new Error(errorMessage)
         error.stdout = stdout
         error.stderr = stderr
         onError(error)

-    handleProcessErrors(apmProcess, "Uninstalling \u201C#{name}\u201D failed.", onError)
+    handleProcessErrors(apmProcess, errorMessage, onError)

   canUpgrade: (installedPackage, availableVersion) ->
     return false unless installedPackage?


Apps can define own obj identity func for patches

--- src/code/providers/document-store-provider.coffee
+++ src/code/providers/document-store-provider.coffee
@@ -263,7 +263,9 @@

   _createDiff: (json1, json2) ->
     try
-      diff = jiff.diff(JSON.parse(json1), JSON.parse(json2))
+      opts =
+        hash: @options.patchObjectHash if typeof @options.patchObjectHash is "function"
+      diff = jiff.diff(JSON.parse(json1), JSON.parse(json2), opts)
       return JSON.stringify diff
     catch
       return null


Removed document store tab when 'saving elsewhere'

--- src/code/providers/document-store-provider.coffee
+++ src/code/providers/document-store-provider.coffee
@@ -164,6 +164,14 @@
     else
       null

+  filterTabComponent: (capability, defaultComponent) ->
+    # allow the save elsewhere button to hide the document provider tab in save
+    if capability is 'save' and @disableForNextSave
+      @disableForNextSave = false
+      null
+    else
+      defaultComponent
+
   handleUrlParams: ->
     if @urlParams.recordid
       @client.openProviderFile @name, @urlParams.recordid
@@ -356,6 +364,7 @@
       yesTitle: tr '~CONCORD_CLOUD_DEPRECATION.CONFIRM_SAVE_ELSEWHERE'
       noTitle: tr '~CONCORD_CLOUD_DEPRECATION.CONFIRM_DO_IT_LATER'
       callback: =>
+        @disableForNextSave = true
         @client.saveFileAsDialog content
       rejectCallback: =>
         if deprecationPhase > 1


Rename update -> set

Rename update -> set

Move previous _update function on class as _set

--- src/coffee/cilantro/models/context/manager.coffee
+++ src/coffee/cilantro/models/context/manager.coffee
@@ -7,16 +7,6 @@
     class Events

     _.extend Events::, Backbone.Events
-
-
-    # Light wrapper for fixing attrs for the top-level branch node. If `attrs`
-    # has ID fields, it is added as a child to the branch. This is primarily
-    # to fix legacy data that did not conform to this structure.
-    _fix = (node, attrs, options) ->
-        if attrs.concept? or attrs.field?
-            node.children.add(attrs, options)
-        else
-            node.set(attrs, options)

     class ContextTreeManager extends Events
@@ -33,11 +23,11 @@
             @upstream = new nodes.BranchNodeModel null,
                 manager: @

-            # Initialize trees and any time a successful sync occurs
-            @update(@model.get('json'))
+            # Set trees with  and any time a successful sync occurs
+            @set(@model.get('json'))

             @model.on 'sync', (model, resp, options) =>
-                @update(resp.json)
+                @set(resp.json)

         # Traverses the working tree and extracts all applied nodes and
         # falls back to the upstream contents.
@@ -81,26 +71,35 @@
         toJSON: (options) ->
             @_toJSON(@working, options)

-        # Update the active and working trees. Nodes may not be removed from
-        # the working tree since this would remove local state that controls
-        # may be relying on.
-        update: (attrs) ->
-            # Ensure the active and working trees are not empty, otherwise
-            # change events do not fire.
-            attrs = _.extend
-                children: []
-                type: 'and'
-            , attrs
+        # Light wrapper for fixing attrs for the top-level branch node. If `attrs`
+        # has ID fields, it is added as a child to the branch. This is primarily
+        # to fix legacy data that did not conform to this structure.
+        # [DEPRECATE 2.2]
+        _set: (node, attrs, options) ->
+            if attrs.concept? or attrs.field?
+                node.children.add(attrs, options)
+            else
+                node.set(attrs, options)

-            # Update `upstream` tree with server response
-            _fix @upstream, attrs,
+        # Updates the working and upstream trees with the server's response
+        set: (attrs) ->
+            if not attrs?
+                @upstream.clear(reset: true)
+                return
+
+            # Update `upstream` tree with server response. Other than
+            # annotations on nodes themselves, nothing should change. The
+            # upstream tree may have not synced nodes so nodes should not be
+            # removed from the tree.
+            @_set @upstream, attrs,
                 manager: @
                 identKeys: @options.identKeys
                 validate: false

             # Augment the `working` tree with the server's response. Prevent
-            # nodes from being removed
-            _fix @working, attrs,
+            # nodes from being removed since this reprsents all nodes that
+            # have been accessed
+            @_set @working, attrs,
                 manager: @
                 identKeys: @options.identKeys
                 validate: false


fix allow insert single column table

fix allow insert single column table

--- lib/insert-table-view.coffee
+++ lib/insert-table-view.coffee
@@ -70,5 +70,5 @@
   # at least 2 row + 2 columns
   isValidRange: (row, col) ->
     return false if isNaN(row) || isNaN(col)
-    return false if row < 2 || col < 2
+    return false if row < 2 || col < 1
     return true


Update 'all tasks' count when a task is completed

--- source/scripts/views/list/all.coffee
+++ source/scripts/views/list/all.coffee
@@ -9,7 +9,7 @@

     @bind $ '.all.list'

-    Task.on 'change', @updateCount
+    Task.on 'change change:completed:model', @updateCount

     @updateCount()


Workspace: Added user id to blacklist at invite autocomplete.

--- client/app/CommonViews/workspace/collaborativeworkspaceuserlist.coffee
+++ client/app/CommonViews/workspace/collaborativeworkspaceuserlist.coffee
@@ -122,6 +122,7 @@
       dataSource          : (args, callback) =>
         {inputValue} = args
         blacklist    = (data.getId() for data in @userController.getSelectedItemData())
+        blacklist.push KD.whoami()._id
         KD.remote.api.JAccount.byRelevance inputValue, {blacklist}, (err, accounts) =>
           callback accounts


Add separate error link (depends on ps PR)

--- lib/linter-purescript.coffee
+++ lib/linter-purescript.coffee
@@ -40,6 +40,12 @@
     range: [[err.position.startLine-1, err.position.startColumn-1], [err.position.endLine-1, err.position.endColumn-1]] if err.position
     multiline: /\n/.test(err.message)
     source: err
+    trace: [
+      {
+        type: "Link"
+        html: "More info (wiki)"
+      }
+    ] if err.errorLink?

   res = []
   result.split '\n'


Revert "Temporarily disable width/height stuff until I can figure out quirks"

This reverts commit dacc22b3c412630e399951e0ea04341162f77add.

--- lib/render.coffee
+++ lib/render.coffee
@@ -15,19 +15,11 @@

 render = ->
   result = page.evaluate ->
-    result = {}
+    title: document.title
+    width: document.body?.clientWidth or width
+    height: document.body?.clientHeight or height

-    result.title          = document.title if document?.title?
-    # result.documentWidth  = document.width if document?.width?
-    # result.documentHeight = document.height if document?.height?
-    # result.innerWidth     = window.innerWidth if window?.innerWidth?
-    # result.innerHeight    = window.innerHeight if window?.innerHeight?
-    # result.clientWidth    = document.body.clientWidth if document?.body?.clientWidth?
-    # result.clientHeight   = document.body.clientHeight if document?.body?.clientHeight?
-
-    result
-
-  if crop #and height and result.height > height
+  if crop and height and result.height > height
     # Cropping isn't exactly what we want, but PhantomJS does
     # not yet have a "window size" concept (See NOTE above).
     page.clipRect =
@@ -36,11 +28,8 @@
       width: width
       height: height

-    # result.height = height
-    # result.width  = width
-
-  # delete result.height unless result.height
-  # delete result.width  unless result.width
+    result.height = height
+    result.width = width

   result.imageData = page.renderBase64(format)
   result.format = format


Members: add follow state to followButton

--- client/Social/Members/groupmemberspagelistitemview.coffee
+++ client/Social/Members/groupmemberspagelistitemview.coffee
@@ -20,6 +20,9 @@
           following    :
             title      : "unfollow"
             cssClass   : "solid red medium"
+          follow       :
+            title      : "follow"
+            cssClass   : "solid green medium"
         dataType       : 'JAccount'
       , data


Remove email in emailList : remove 2nd panel

--- client/app/components/toolbar_messageslist_actions.coffee
+++ client/app/components/toolbar_messageslist_actions.coffee
@@ -7,8 +7,14 @@
 LayoutActionCreator  = require '../actions/layout_action_creator'
 MessageActionCreator = require '../actions/message_action_creator'

+RouterMixin = require '../mixins/router_mixin'
+
 module.exports = ActionsToolbarMessagesList = React.createClass
     displayName: 'ActionsToolbarMessagesList'
+
+    mixins: [
+        RouterMixin
+    ]

     propTypes:
         settings:             React.PropTypes.object.isRequired
@@ -83,6 +89,9 @@

             @props.afterAction() if @props.afterAction?

+            # Close Detail Panel for deleted message
+            @redirect (url = @buildClosePanelUrl 'second')
+
         unless @props.settings.get 'messageConfirmDelete'
             doDelete()
         else


exchange: always prefer cache

exchange: always prefer cache

--- lib/routes.coffee
+++ lib/routes.coffee
@@ -15,6 +15,7 @@
         return res.json 503, {error: {message : "Exchange rate unavailable"} } unless rate

         res.json rate
+      , max_age=900
     )

   app.get '/search/:location/:category', (req, res) ->


Fix stray deprecated call

Fix stray deprecated call

--- lib/scroll.coffee
+++ lib/scroll.coffee
@@ -40,10 +40,10 @@
     @editor.scrollToScreenPosition([firstScreenRow - count, 0])

 class ScrollCursor extends Scroll
-  constructor: (@editor, @opts={}) ->
+  constructor: (@editorElement, @opts={}) ->
     super
     cursor = @editor.getCursorScreenPosition()
-    @pixel = @editor.pixelPositionForScreenPosition(cursor).top
+    @pixel = @editorElement.pixelPositionForScreenPosition(cursor).top

 class ScrollCursorToTop extends ScrollCursor
   execute: ->


beter detection for script auto-init params

--- src/client/script-tag-config.coffee
+++ src/client/script-tag-config.coffee
@@ -83,9 +83,8 @@

   if value? then value else param.default

-
 module.exports = ->
-  hull_js_sdk = document.getElementById('hull-js-sdk')
+  hull_js_sdk = document.getElementById('hull-js-sdk') || document.querySelector('[org-url][platform-id]')

   return unless hull_js_sdk


Added TODO for implementation breakdown

Added TODO for implementation breakdown

--- lib/server/incoming.coffee
+++ lib/server/incoming.coffee
@@ -28,8 +28,6 @@
     @on 'hook::ready', @_startServer()

   _startServer: ->
-    console.log '_startServer'
-
     @_loadParsers()

     http.createServer (req, res) =>
@@ -92,6 +90,8 @@
     if @parsers[name]
       return

+    # TODO: Split implementations to own modules, so we can just spawn them
+
     opts = @implementations[name]
     config = _.extend {name: "#{name}-parser", debug: @debug}, opts.config
     parser = require("../implementations/#{name}/parser")


Activity: show more item click to cause scrollview update

--- client/Social/Activity/views/activitylistitem.coffee
+++ client/Social/Activity/views/activitylistitem.coffee
@@ -275,6 +275,9 @@
         click    : ->
           article.style['max-height'] = "#{scrollHeight}px"
           article.classList.remove 'tall'
+          # FIXME: this is a hack to cause a mutation
+          # for scrollview to readjust - SY
+          KD.utils.wait 500, -> article.innerHTML += ' '
           @destroy()

       article.classList.add 'tall'


client: removed bullet from "more"

client: removed bullet from "more"

--- client/Social/Activity/views/universitywidget.coffee
+++ client/Social/Activity/views/universitywidget.coffee
@@ -18,7 +18,7 @@

What is Koding?

Getting started with IDE Workspaces

What ports are open on my VM?

-
more...

-

+
More guides on Koding University...
+

     """


account/shortcuts: revert commit 87dafe0 (see: https://github.com/koding/koding/pull/3396#discussion-diff-28712401)

--- client/account/lib/views/accounteditshortcuts.coffee
+++ client/account/lib/views/accounteditshortcuts.coffee
@@ -16,11 +16,6 @@
 module.exports =

 class AccountEditShortcuts extends kd.View
-
-  INSTRUCTIONS_CSS_CLASS = 'instructions'
-  INSTRUCTIONS_PARTIAL = 'To change a shortcut, click on it, then type the new keys.'
-  RESTORE_BUTTON_TITLE = 'Restore Defaults'
-  RESTORE_BUTTON_CLASS_NAME = 'solid light-gray medium restore'

   destroy: ->

@@ -49,8 +44,8 @@
         collection : collection

     @addSubView new kd.View
-      cssClass : INSTRUCTIONS_CSS_CLASS
-      partial  : INSTRUCTIONS_PARTIAL
+      cssClass : 'instructions'
+      partial  : 'To change a shortcut, click on it, then type the new keys.'

     @addSubView @tabView = new kd.TabView
       tabClass             : Pane
@@ -61,8 +56,8 @@
     @tabView.on 'PaneDidShow', recorder.cancel

     @addSubView new kd.ButtonView
-      title    : RESTORE_BUTTON_TITLE
-      style    : RESTORE_BUTTON_CLASS_NAME
+      title    : 'Restore Defaults'
+      style    : 'solid light-gray medium restore'
       callback : restoreDefaults

     @tabView.showPaneByIndex 0


Client: Activity: Fixed like link text change problem and comment input onChange problem

--- client/activity/lib/components/feeditem/index.coffee
+++ client/activity/lib/components/feeditem/index.coffee
@@ -82,8 +82,8 @@
   handleCommentInputChange: (event) ->

     hasValue = no
-    value    = event.target.value.trim()
-    hasValue = yes  if value
+    value    = event.target.value
+    hasValue = yes  if value.trim()
     @setState { hasValue: hasValue, commentValue: value }

@@ -136,6 +136,7 @@

     { message } = @props
     imAccount   = toImmutable({ id: whoami()._id, constructorName: 'JAccount'})
+    likeText = if message.getIn ['interactions', 'like',  'isInteracted'] then 'Unlike' else 'Like'

     <div className={kd.utils.curry 'FeedItem', @props.className}>
       <header className="FeedItem-header">
@@ -164,7 +165,7 @@
             tooltip={no}
             messageId={message.get('id')}
             interactions={message.get('interactions').toJS()}>
-            Like
+            {likeText}
           </ActivityLikeLink>
           <Link onClick={@bound 'sendComment'}>Comment</Link>
           <Link onClick={@bound 'toggleSharePopupVisibility'}>Share</Link>


Client: Activity: Updated handleSetLastMessageEditMode to update only editing message not all collection

--- client/activity/lib/flux/stores/messagesstore.coffee
+++ client/activity/lib/flux/stores/messagesstore.coffee
@@ -230,16 +230,15 @@

     isLastMessageSetFound = no

-    messages
+    return messages
       .sortBy (message) -> message.get 'createdAt'
       .reverse()
       .map (message) ->
         if (message.getIn(['account', '_id']) is accountId) and (isLastMessageSetFound is no)
           isLastMessageSetFound = yes
-          messageId = message.get 'id'
-          messages = messages.setIn [messageId, '__isEditing'], yes
-
-    return messages
+          return message.set '__isEditing', yes
+        else
+          return message

   ###*


Optimized reference to reduce()

Optimized reference to reduce()

Now detects and utilizes native Array.prototype.reduce (when available) when the class is first defined, rather than on each Promise.reduce() invocation.

--- src/coffee/Deft/util/Promise.coffee
+++ src/coffee/Deft/util/Promise.coffee
@@ -96,9 +96,6 @@
 		@private
 		###
 		reduceArray: ( reduceFunction, initialValue ) ->
-			if Array.prototype.reduce?
-				return Array.prototype.reduce.apply( @, arguments )
-
 			# ES5 reduce implementation if native not available
 			# See: http://es5.github.com/#x15.4.4.21 as there are many specifics and edge cases.
 			# ES5 dictates that reduce.length === 1
@@ -154,4 +151,10 @@
 	###
 	cancel: ( reason ) ->
 		return @deferred.cancel( reason )
+,
+	->
+		# Use native reduce implementation, if available.
+		if Array.prototype.reduce?
+			@reduceArray = Array.prototype.reduce
+		return
 )


Ace: Handled ace content change events.

Ace: Handled ace content change events.

--- client/app/Applications/Ace.kdapplication/ace.coffee
+++ client/app/Applications/Ace.kdapplication/ace.coffee
@@ -31,6 +31,8 @@
           if contents
             @setContents contents
             @lastSavedContents = contents
+          @editor.on "change", =>
+            if @isContentChanged() then @emit "FileContentChanged" else @emit "FileContentSynced"
           @editor.gotoLine 0
           @focus()
           @show()
@@ -62,6 +64,7 @@
       unless err
         @notify "Successfully saved!", "success"
         @lastSavedContents = @lastContentsSentForSave
+        @emit "FileContentSynced"
         unless @askedForSave
           log "this file has changed, put a modal and block editing @fatihacet!"
         @askedForSave = no
@@ -69,6 +72,9 @@
     file.on "fs.save.started", =>
       @lastContentsSentForSave = @getContents()

+    file.on "fs.saveAs.finished", =>
+      @emit "FileContentSynced"
+      @emit "FileHasBeenSavedAs", file

     if @getOptions().enableShortcuts
       @addKeyCombo "save", "Ctrl-S", @bound "requestSave"
@@ -106,24 +112,24 @@
         mac   : macKey
       exec    : => callback?()

+  isContentChanged: ->
+    return @getContents() isnt @lastSavedContents
+
   ###
   FS REQUESTS
   ###

   requestSave:->
-
     contents = @getContents()
-    return @notify "Nothing to save!" unless contents isnt @lastSavedContents
+    return @notify "Nothing to save!" unless @isContentChanged()
     @askedForSave = yes
     @emit "ace.requests.save", contents

   requestSaveAs:->
-
     contents = @getContents()
     @emit "ace.requests.saveAs", contents

   fetchContents:(callback)->
-
     file = @getData()
     unless /localfile:/.test file.path
       @notify "Loading...", null, null, 10000


remove empty parameter list

remove empty parameter list

--- lib/sprout.coffee
+++ lib/sprout.coffee
@@ -4,7 +4,7 @@
 mkdirp        = require 'mkdirp'
 fs            = require 'fs'

-module.exports = () ->
+module.exports = ->
   p = path.join(osenv.home(), '.config/roots/templates')
   if not fs.existsSync(p) then mkdirp.sync(p)
   new Sprout(p)


Debug stuff

Debug stuff

--- components.coffee
+++ components.coffee
@@ -114,13 +114,21 @@

         # Set up @reactives
         @reactives = {} # reactiveName: autoVar
+        depth = 0
         for reactiveName, reactiveSpec of componentSpec.reactives ? {}
             @reactives[reactiveName] = do (reactiveName, reactiveSpec) => J.AutoVar(
                 =>
-                    # console.log "#{@toString()}.#{reactiveName}()"
-                    reactiveSpec.val.call @
+                    dots = "....................".substring(0, depth * 4)
+                    spaces = "                    ".substring(0, depth * 4)
+                    # console.log "#{dots} #{@toString()}.#{reactiveName}()"
+                    depth += 1
+                    ret = reactiveSpec.val.call @
+                    depth -= 1
+                    # console.log "#{spaces}    =", ret
+                    ret
                 (
                     if reactiveSpec.onChange? then =>
+                        J.assert not Tracker.active
                         # console.log "#{@toString()}.#{reactiveName}.onChange", arguments
                         reactiveSpec.onChange.apply @, arguments
                     else null


Allow to set host

Allow to set host

--- src/coffee/run.coffee
+++ src/coffee/run.coffee
@@ -11,6 +11,7 @@
   .describe('projectKey', 'your SPHERE.IO project-key')
   .describe('clientId', 'your OAuth client id for the SPHERE.IO API')
   .describe('clientSecret', 'your OAuth client secret for the SPHERE.IO API')
+  .describe('sphereHost', 'SPHERE.IO API host to connecto to')
   .describe('fetchHours', 'Number of hours to fetch modified orders')
   .describe('standardShippingMethod', 'Allows to define the fallback shipping method name of order has none')
   .describe('useExportTmpDir', 'whether to use a tmp folder to store resulting XML files in or not (if no, files will be created under \'./exports\')')
@@ -92,6 +93,8 @@
     user_agent: "#{package_json.name} - #{package_json.version}"
     logConfig:
       logger: logger.bunyanLogger
+
+  options.host = argv.sphereHost if argv.sphereHost

   orderExport = new OrderExport options
   orderExport.standardShippingMethod = argv.standardShippingMethod


Fix log file name.

Fix log file name.

--- src/coffee/run.coffee
+++ src/coffee/run.coffee
@@ -37,7 +37,7 @@
   name: "#{package_json.name}-#{package_json.version}"
   streams: [
     { level: 'error', stream: process.stderr }
-    { level: argv.logLevel, path: "#{argv.logDir}/sphere-order-xml-import.log" }
+    { level: argv.logLevel, path: "#{argv.logDir}/sphere-order-xml-export.log" }
   ]
 logOptions.silent = argv.logSilent if argv.logSilent
 logger = new ExtendedLogger
@@ -181,4 +181,4 @@
   logger.error err, 'Problems on getting client credentials from config files.'
   # process.exit(1)
   @exitCode = 1
-.done()+.done()


Add collection clone method to fix bug in klass attribute duplicate definition

--- src/collection.coffee
+++ src/collection.coffee
@@ -8,7 +8,7 @@
   # @return [Collection] the built Collection
   @build: (array = []) ->
     if array.isA?(this)
-      array
+      array.clone()
     else if array.length?
       new this(array)
     else
@@ -157,3 +157,9 @@
   # @return [Object] the first resource that returned true in the predicate
   detect: (predicate) ->
     _.detect(@__collection, predicate)
+
+  # Duplicates the items of the collection into a new collection
+  #
+  # @return [Collection] the cloned collection of original items
+  clone: ->
+    ActiveResource::Collection.build(_.map(@__collection, (i) => i))


In NodeJS, don't taint anything.

In NodeJS, don't taint anything.

--- lib/wesabe.coffee
+++ lib/wesabe.coffee
@@ -38,6 +38,8 @@

   is: (object, klass) ->
     object?.constructor == klass
+
+  isTainted: -> false

   ## LOGGING


Prevent the resource combiner from iterating on the content array string when checking completion

--- lib/resource-combiner.coffee
+++ lib/resource-combiner.coffee
@@ -26,6 +26,9 @@
       for deferred in resource.deferred
         deferred resource.content

+      # Clear the deferred list so future requests will be handled directly
+      resource.deferred = undefined
+
     for content in resource.content
       do (content) ->
         chunks = []
@@ -48,8 +51,11 @@

     resource.href = virtual.create
       content: (callback) ->
-        resource.deferred.push callback
-        checkComplete()
+        if resource.deferred
+          resource.deferred.push callback
+          checkComplete()
+        else
+          callback resource.content
       contentType: contentType
       prefix: prefix


cleanup and improve to preserve fold on `g v`

--- lib/selection-wrapper.coffee
+++ lib/selection-wrapper.coffee
@@ -69,8 +69,9 @@

   setReversedState: (reversed) ->
     return if @selection.isReversed() is reversed
-    {head, tail, wise} = @getProperties()
-    if head? and tail?
+
+    if @hasProperties()
+      {head, tail, wise} = @getProperties()
       @setProperties(head: tail, tail: head, wise: wise)

     options = {autoscroll: true, reversed, preserveFolds: true}
@@ -141,23 +142,17 @@
     @setProperties(properties)

   fixPropertiesForLinewise: ->
-    selectionProperties = @getProperties()
-    return unless selectionProperties
-    {head, tail, wise} = selectionProperties
+    return unless @hasProperties()
+    {head, tail} = @getProperties()
     if @selection.isReversed()
       [start, end] = [head, tail]
     else
       [start, end] = [tail, head]
-    [startRow, endRow] = @selection.getBufferRowRange()
-
-    start.row = startRow unless startRow is start.row
-    end.row = endRow unless endRow is end.row
-
+    [start.row, end.row] = @selection.getBufferRowRange()

   setWise: (value) ->
     @saveProperties() unless @hasProperties()
-    properties = @getProperties()
-    properties.wise = value
+    @getProperties().wise = value

   getWise: ->
     @getProperties()?.wise ? 'characterwise'
@@ -208,12 +203,13 @@
       properties.tail.column = tailMaxColumn

   captureProperties: ->
-    head: @selection.getHeadBufferPosition()
-    tail: @selection.getTailBufferPosition()
+    head = @selection.getHeadBufferPosition()
+    tail = @selection.getTailBufferPosition()
+    {head, tail}

   selectByProperties: ({head, tail}) ->
     # No problem if head is greater than tail, Range constructor swap start/end.
-    @setBufferRange([tail, head])
+    @setBufferRange([tail, head], preserveFolds: true)
     @setReversedState(head.isLessThan(tail))

   # Return true if selection was non-empty and non-reversed selection.
@@ -225,9 +221,8 @@

   restoreFromProperties: ->
     return unless @hasProperties()
-    {head, tail} = @getProperties()
     @withKeepingGoalColumn =>
-      @setBufferRange([tail, head], preserveFolds: true)
+      @selectByProperties(@getProperties())

   # Only for setting autoscroll option to false by default
   setBufferRange: (range, options={}) ->


Remove start page.

Remove start page.

--- js/flight.coffee
+++ js/flight.coffee
@@ -2,7 +2,6 @@
   # Private
   version: '0.0.1'
   isTransitioning: false
-  startPage: ''
   currentPage: ''
   targetPage: ''
   pageHistory: [""]
@@ -39,16 +38,13 @@
     if not @currentPage
       # No current panel set, app just started, make start panel visible
       @targetPage.style.display = "block"
-      @pageHistory = [window.location.hash];
-      @startPage = [window.location.hash];
+      @pageHistory = [window.location.hash]
       @currentPage = @targetPage
       return

     # if already transitioning then return
     if @isTransitioning is true then return else @isTransitioning = true

-    if @targetPage is @startPage
-      @back = true
     if @pageHistory.length > 1 and window.location.hash is @pageHistory[@pageHistory.length - 2]
       @back = true


Selective Bulk Delete (purge)

Selective Bulk Delete (purge)

--- modules/admin/index.coffee
+++ modules/admin/index.coffee
@@ -36,11 +36,13 @@
       catch e
         Core.log(e, 1)
         msg.reply locale.admin.cantDelete
-    @registerCommand 'purge', admin, ({ msg, args, locale })=>
-      limit = parseInt(args)
+    @registerCommand 'purge', { adminOnly: true, argSeparator: ' ' }, ({ msg, args, locale })=>
+      limit = parseInt(args[0])
       if isFinite(limit) and limit > 0
         try
           messages = await msg.channel.fetchMessages { limit }
+          if msg.mentions.members.first()?
+            messages = messages.filter (m)-> m.member.id is msg.mentions.members.first()
           await msg.channel.bulkDelete(messages)
         catch e
           Core.log(e, 1)


fix but when objects have the same property

--- src/observer.coffee
+++ src/observer.coffee
@@ -1,8 +1,3 @@
-
-# @class
-# @private
-class Sirius.StateContain
-  constructor: (@object, @clb) ->

 # hacks for observer when property or text changed into DOM

@@ -46,11 +41,10 @@

       current_prop = from.prop.split(".").join("-")
       if @constructor._clbs[current_prop]?
-        @constructor._clbs[current_prop].push(clb)
+        @constructor._clbs[current_prop].push([from.object, clb])
         # because already have handler
-        return
       else
-        @constructor._clbs[current_prop] = [clb]
+        @constructor._clbs[current_prop] = [[from.object, clb]]

       clbs = @constructor._clbs

@@ -60,7 +54,7 @@
           text: newvalue
           previous: oldvalue

-        clbs[current_prop].forEach((f) -> f.call(null, result))
+        clbs[current_prop].filter((a) -> a[0] == from.object).forEach((a) -> a[1].call(null, result))
         newvalue

       my_watch = (object, prop, handler) ->


refactor agda-mode.coffee

refactor agda-mode.coffee

--- lib/agda-mode.coffee
+++ lib/agda-mode.coffee
@@ -25,7 +25,7 @@

     atom.workspaceView.eachEditorView (editorView) =>
       editor = editorView.getModel()
-      if @isAgdaFile editor
+      if isAgdaFile editor
         editor.agda = new Agda editorView

         # deactivated on default
@@ -37,7 +37,7 @@
         # apply highlighting after we deactivated it.
         editor.on 'grammar-changed', =>
           grammarIsAgda = editor.getGrammar().name is 'Agda'
-          shouldHighlight = editor.agda.passed and editor.agda.passed
+          shouldHighlight = editor.agda.loaded and editor.agda.passed
           if grammarIsAgda and not shouldHighlight
             # fuck you atom
             editor.agda.syntax.deactivate()
@@ -47,86 +47,18 @@
         editor.on 'became-inactive', =>
           editor.agda.emit 'deactivate'

-
-    # load
-    atom.workspaceView.command 'agda-mode:load', =>
-      if @isAgdaFile()
-        editor = @getTheFuckingEditor()
-        editor.agda.load()
-
-    # quit
-    atom.workspaceView.command 'agda-mode:quit', =>
-      if @isAgdaFile()
-        editor = @getTheFuckingEditor()
-        editor.agda.quit()
-
-
-    # restart
-    atom.workspaceView.command 'agda-mode:restart', =>
-      if @isAgdaFile()
-        editor = @getTheFuckingEditor()
-        editor.agda.restart()
-
-    # next goal
-    atom.workspaceView.command 'agda-mode:next-goal', =>
-      if @isAgdaFile()
-        editor = @getTheFuckingEditor()
-        editor.agda.nextGoal()
-
-    # previous goal
-    atom.workspaceView.command 'agda-mode:previous-goal', =>
-      if @isAgdaFile()
-        editor = @getTheFuckingEditor()
-        editor.agda.previousGoal()
-
-    # give
-    atom.workspaceView.command 'agda-mode:give', =>
-      if @isAgdaFile()
-        editor = @getTheFuckingEditor()
-        editor.agda.give()
-
-    # goal type
-    atom.workspaceView.command 'agda-mode:goal-type', =>
-      if @isAgdaFile()
-        editor = @getTheFuckingEditor()
-        editor.agda.goalType()
-
-    # context
-    atom.workspaceView.command 'agda-mode:context', =>
-      if @isAgdaFile()
-        editor = @getTheFuckingEditor()
-        editor.agda.context()
-
-    # goal type and context
-    atom.workspaceView.command 'agda-mode:goal-type-and-context', =>
-      if @isAgdaFile()
-        editor = @getTheFuckingEditor()
-        editor.agda.goalTypeAndContext()
-
-    # goal type and inferred type
-    atom.workspaceView.command 'agda-mode:goal-type-and-inferred-type', =>
-      if @isAgdaFile()
-        editor = @getTheFuckingEditor()
-        editor.agda.goalTypeAndInferredType()
-
-    # refine
-    atom.workspaceView.command 'agda-mode:refine', =>
-      if @isAgdaFile()
-        editor = @getTheFuckingEditor()
-        editor.agda.refine()
-
-
-  getTheFuckingEditor: ->
-    atom.workspace.getActivePaneItem()
-
-
-  # if end with ".agda"
-  isAgdaFile: (editor) ->
-    if editor
-      filePath = editor.getPath?()
-    else
-      filePath = @getTheFuckingEditor().getPath()
-    /\.agda$/.test filePath
+    [
+      'agda-mode:load'
+      'agda-mode:quit'
+      'agda-mode:restart'
+      'agda-mode:next-goal'
+      'agda-mode:previous-goal'
+      'agda-mode:give'
+      'agda-mode:goal-type'
+      'agda-mode:context'
+      'agda-mode:goal-type-and-context'
+      'agda-mode:refine'
+    ].forEach registerCommand

   deactivate: ->
@@ -135,3 +67,34 @@

   serialize: ->
     # agdaModeViewState: @agdaModeView.serialize()
+
+
+registerCommand = (command) =>
+  atom.workspaceView.command command, =>
+    if isAgdaFile()
+      editor = getTheFuckingEditor()
+      editor.agda[toCamalCase command]()
+
+getTheFuckingEditor = ->
+  atom.workspace.getActivePaneItem()
+
+
+# if end with ".agda"
+isAgdaFile = (editor) ->
+  if editor
+    filePath = editor.getPath?()
+  else
+    filePath = getTheFuckingEditor().getPath()
+  /\.agda$/.test filePath
+
+
+toCamalCase = (str) ->
+  str
+    .substr(10)   # strip "agda-mode:"
+    .split('-')
+    .map (str, i) =>
+      if i is 0
+        str
+      else
+        str.charAt(0).toUpperCase() + str.slice(1)
+    .join('')


change outbound category

change outbound category

--- lib/analytics.coffee
+++ lib/analytics.coffee
@@ -24,7 +24,7 @@
   ga? 'send', 'pageview'

 module.exports.trackOutboundLink = (url, cb = $.noop) =>
-  ga? 'send', 'event', 'outbound', 'click', url,
+  ga? 'send', 'event', 'Outbound link', 'click', url,
     'transport': 'beacon'
     'hitCallback': cb


Fix gutter height

Fix gutter height

--- src/controller.coffee
+++ src/controller.coffee
@@ -3577,6 +3577,9 @@
     if changedBox
       @gutter.style.height = "#{Math.max @mainScroller.offsetHeight, treeView.totalBounds.height}px"

+  hook 'resize', 0, ->
+    @gutter.style.height = "#{Math.max @iceElement.offsetHeight, @view.getViewNodeFor(@tree).totalBounds?.height ? 0}px"
+
   # OVRFLOW BIT
   # ================================


fixed broken loop

fixed broken loop

--- source/lib/element.coffee
+++ source/lib/element.coffee
@@ -7,7 +7,9 @@
 module.exports = class Element extends require './evented'
   constructor: (args = {}) ->
     super
-    this[key] = args[key] if args[key] for key in ['origin', 'size', 'state', 'style']
+    for key in ['origin', 'size', 'state', 'style']
+      this[key] = args[key] if args[key]
+
     (@styles ||= {}) and @styles[key] = value for key, value of args.styles
     (@states ||= {}) and @states[key] = value for key, value of args.states


Provide name when creating players

Provide name when creating players

--- game.coffee
+++ game.coffee
@@ -34,10 +34,10 @@
       @listeners = []

     start: ->
-      @player0 = new Player(new Position([Game.WIDTH/2, 0], Game.SOUTH, 0))
-      @player1 = new Player(new Position([Game.WIDTH, Game.HEIGHT/2], Game.WEST, 0))
-      @player2 = new Player(new Position([Game.WIDTH/2, Game.HEIGHT], Game.NORTH, 0))
-      @player3 = new Player(new Position([0, Game.HEIGHT/2], Game.EAST, 0))
+      @player0 = new Player("Player0", new Position([Game.WIDTH/2, 0], Game.SOUTH, 0))
+      @player1 = new Player("Player1", new Position([Game.WIDTH, Game.HEIGHT/2], Game.WEST, 0))
+      @player2 = new Player("Player2", new Position([Game.WIDTH/2, Game.HEIGHT], Game.NORTH, 0))
+      @player3 = new Player("Player3", new Position([0, Game.HEIGHT/2], Game.EAST, 0))

       @players = [@player0, @player1, @player2, @player3]


Serialization conversion

Serialization conversion

--- lib/directory.coffee
+++ lib/directory.coffee
@@ -26,6 +26,16 @@
     @isRoot ?= false
     @expansionState ?= {}
     @expansionState.isExpanded ?= false
+
+    # TODO: This can be removed after a sufficient amount
+    # of time has passed since @expansionState.entries
+    # has been converted to a Map
+    unless @expansionState.entries instanceof Map
+      temp = new Map()
+      for name, entry of @expansionState.entries
+        temp.set(name, entry)
+      @expansionState.entries = temp
+
     @expansionState.entries ?= new Map()
     @status = null
     @entries = new Map()


[REST] added basic filters for get request

--- src/rest/api.coffee
+++ src/rest/api.coffee
@@ -34,8 +34,21 @@
         @app.get '/api/' + model, (req, res) =>
             res.set 'Access-Control-Allow-Origin', 'http://localhost:4242'
             # TODO set filters, expands etc.
-            @db.models[model].all (err, all) =>
-                res.json all
+            if req.query
+                objs = {}
+                for k, v of req.query
+                    objs[k] = v
+                try
+                    @db.models[model].all {where: objs}, (err, objs) =>
+                        if objs?
+                            res.json objs
+                        else
+                            res.send 404
+                catch e
+                    res.send 400
+            else
+                @db.models[model].all (err, all) =>
+                    res.json all
         @app.get '/api/' + model + '/:id', (req, res) =>
             res.set 'Access-Control-Allow-Origin', 'http://localhost:4242'
             try


Replace % encoding with $ encoding to work around Inkscape bugs

Unfortunately, Inkscape seems to ignore any %-encoded symbols; see
https://bugs.launchpad.net/inkscape/+bug/1737778
So we replace '%' with '$', an allowed character that's already escaped.

--- src/svgtiler.coffee
+++ src/svgtiler.coffee
@@ -187,24 +187,30 @@
       console.warn "Failed to detect #{warnings.join ' and '} of SVG for symbol '#{@key}'"
     @zIndex = zIndex @xml.documentElement
   id: ->
-    ## id/href follows the IRI spec [https://tools.ietf.org/html/rfc3987]:
-    ##   ifragment      = *( ipchar / "/" / "?" )
-    ##   ipchar         = iunreserved / pct-encoded / sub-delims / ":" / "@"
-    ##   iunreserved    = ALPHA / DIGIT / "-" / "." / "_" / "~" / ucschar
-    ##   pct-encoded    = "%" HEXDIG HEXDIG
-    ##   sub-delims     = "!" / "$" / "&" / "'" / "(" / ")"
-    ##                  / "*" / "+" / "," / ";" / "="
-    ##   ucschar        = %xA0-D7FF / %xF900-FDCF / #%xFDF0-FFEF
-    ##                  / %x10000-1FFFD / %x20000-2FFFD / %x30000-3FFFD
-    ##                  / %x40000-4FFFD / %x50000-5FFFD / %x60000-6FFFD
-    ##                  / %x70000-7FFFD / %x80000-8FFFD / %x90000-9FFFD
-    ##                  / %xA0000-AFFFD / %xB0000-BFFFD / %xC0000-CFFFD
-    ##                  / %xD0000-DFFFD / %xE1000-EFFFD
-    ## We also want to escape colon (:) which seems to cause trouble.
-    ## We use encodeURIComponent which escapes everything except
-    ##   A-Z a-z 0-9 - _ . ! ~ * ' ( )
-    ## [https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent]
+    ###
+    id/href follows the IRI spec [https://tools.ietf.org/html/rfc3987]:
+      ifragment      = *( ipchar / "/" / "?" )
+      ipchar         = iunreserved / pct-encoded / sub-delims / ":" / "@"
+      iunreserved    = ALPHA / DIGIT / "-" / "." / "_" / "~" / ucschar
+      pct-encoded    = "%" HEXDIG HEXDIG
+      sub-delims     = "!" / "$" / "&" / "'" / "(" / ")"
+                     / "*" / "+" / "," / ";" / "="
+      ucschar        = %xA0-D7FF / %xF900-FDCF / #%xFDF0-FFEF
+                     / %x10000-1FFFD / %x20000-2FFFD / %x30000-3FFFD
+                     / %x40000-4FFFD / %x50000-5FFFD / %x60000-6FFFD
+                     / %x70000-7FFFD / %x80000-8FFFD / %x90000-9FFFD
+                     / %xA0000-AFFFD / %xB0000-BFFFD / %xC0000-CFFFD
+                     / %xD0000-DFFFD / %xE1000-EFFFD
+    We also want to escape colon (:) which seems to cause trouble.
+    We use encodeURIComponent which escapes everything except
+      A-Z a-z 0-9 - _ . ! ~ * ' ( )
+    [https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent]
+    Unfortunately, Inkscape seems to ignore any %-encoded symbols; see
+    https://bugs.launchpad.net/inkscape/+bug/1737778
+    So we replace '%' with '$', an allowed character that's already escaped.
+    ###
     encodeURIComponent @key
+    .replace /%/g, '$'
   #use: -> @  ## do nothing for static symbol

 class DynamicSymbol extends Symbol


Add use method to StaticSymbol; replace with usesContext performance hint

--- src/svgtiler.coffee
+++ src/svgtiler.coffee
@@ -288,7 +288,8 @@
     ###
     encodeURIComponent @key
     .replace /%/g, '$'
-  #use: -> @  ## do nothing for static symbol
+  use: -> @  ## do nothing for static symbol
+  usesContext: false

 class DynamicSymbol extends Symbol
   constructor: (@key, @func, @dirname) ->
@@ -308,6 +309,7 @@
     else
       @versions[string] =
         Symbol.parse "#{@key}-v#{@nversions++}", result, @dirname
+  usesContext: true

 ## Symbol to fall back to when encountering an unrecognized symbol.
 ## Path from https://commons.wikimedia.org/wiki/File:Replacement_character.svg
@@ -522,9 +524,11 @@
     symbols =
       for row, i in symbols
         for symbol, j in row
-          if symbol?.use?
+          if symbol.usesContext
             symbol = symbol.use new Context symbols, i, j
-          symbolsByKey[symbol?.key] = symbol
+          else
+            symbol = symbol.use()
+          symbolsByKey[symbol.key] = symbol
     ## Include all used symbols in SVG
     for key, symbol of symbolsByKey
       continue unless symbol?


improved messaging when registering plugins

--- src/sys/game.coffee
+++ src/sys/game.coffee
@@ -39,7 +39,11 @@

     if @plugins?
       for plugin in @plugins
-        @registerPlugin(new plugin(@)) if plugin.supported?
+        if plugin.supported?
+          @registerPlugin(new plugin(@))
+          console.log("Plugin: #{plugin.getName()} << enabled >>")
+        else
+          console.log("Plugin: #{plugin.getName()} << not supported or is disabled in the current environment >>")

     @_timer  = new Fz2D.Timer()
     @_ctx    = new Fz2D.Renderer(@w, @h, @bg, @selector)


tooltip div attached to selection rather than "body"

--- src/tooltips.coffee
+++ src/tooltips.coffee
@@ -1,7 +1,7 @@
 # tool tips

 d3panels.tooltip_create = (selection, objects, options, tooltip_func) ->
-    tipdiv = d3.select("body").append("div")
+    tipdiv = selection.append("div")
                .attr("class", "d3panels-tooltip #{options.tipclass}")
                .style("opacity", 0)


First attempt at addressing issue #67.

First attempt at addressing issue #67.

Allows instrumenting the error path with a custom callback to examine the
returned error and decide whether it should be bubbled up (return true)
or handled by Backbone.dualStorage (return false).

--- backbone.dualstorage.coffee
+++ backbone.dualstorage.coffee
@@ -242,6 +242,11 @@
   options.success = callbackTranslator.forBackboneCaller(options.success)
   options.error   = callbackTranslator.forBackboneCaller(options.error)
   backboneSync(method, model, options)
+
+bubbleError = (response, options) ->
+  inspector = options.inspector or Backbone.inspector;
+  if inspector then return inspector response
+  return false

 dualsync = (method, model, options) ->
   options.storeName = getStoreName(model.collection, model)
@@ -288,7 +293,10 @@
           success(resp, status, xhr)

         options.error = (resp) ->
-          success localsync(method, model, options)
+          if bubbleError resp, options
+            error resp
+          else
+            success localsync(method, model, options)

         onlineSync(method, model, options)

@@ -298,8 +306,11 @@
         localsync(method, updatedModel, options)
         success(resp, status, xhr)
       options.error = (resp) ->
-        options.dirty = true
-        success localsync(method, model, options)
+        if bubbleError resp, options
+          error resp
+        else
+          options.dirty = true
+          success localsync(method, model, options)

       onlineSync(method, model, options)

@@ -314,9 +325,12 @@
           localsync('create', updatedModel, options)
           success(resp, status, xhr)
         options.error = (resp) ->
-          options.dirty = true
-          model.set model.idAttribute, temporaryId, silent: true
-          success localsync(method, model, options)
+          if bubbleError resp, options
+            error resp
+          else
+            options.dirty = true
+            model.set model.idAttribute, temporaryId, silent: true
+            success localsync(method, model, options)

         model.set model.idAttribute, null, silent: true
         onlineSync('create', model, options)
@@ -326,8 +340,11 @@
           localsync(method, updatedModel, options)
           success(resp, status, xhr)
         options.error = (resp) ->
-          options.dirty = true
-          success localsync(method, model, options)
+          if bubbleError resp, options
+            error resp
+          else
+            options.dirty = true
+            success localsync(method, model, options)

         onlineSync(method, model, options)

@@ -339,8 +356,11 @@
           localsync(method, model, options)
           success(resp, status, xhr)
         options.error = (resp) ->
-          options.dirty = true
-          success localsync(method, model, options)
+          if bubbleError resp, options
+            error resp
+          else
+            options.dirty = true
+            success localsync(method, model, options)

         onlineSync(method, model, options)


socialapi/workers: delete worker list

socialapi/workers: delete worker list

--- config/workers.coffee
+++ config/workers.coffee
@@ -236,12 +236,6 @@
           run           : "#{GOBIN}/notification"
           watch         : "#{GOBIN}/watcher -run socialapi/workers/cmd/notification -watch socialapi/workers/notification"

-    pinnedpost          :
-      group             : "socialapi"
-      supervisord       :
-        command         :
-          run           : "#{GOBIN}/pinnedpost"
-          watch         : "#{GOBIN}/watcher -run socialapi/workers/cmd/pinnedpost -watch socialapi/workers/pinnedpost"

     realtime            :
       group             : "socialapi"
@@ -249,41 +243,6 @@
         command         :
           run           : "#{GOBIN}/realtime"
           watch         : "#{GOBIN}/watcher -run socialapi/workers/cmd/realtime -watch socialapi/workers/realtime"
-
-    sitemapfeeder       :
-      group             : "socialapi"
-      supervisord       :
-        command         :
-          run           : "#{GOBIN}/sitemapfeeder"
-          watch         : "#{GOBIN}/watcher -run socialapi/workers/cmd/sitemapfeeder -watch socialapi/workers/sitemapfeeder"
-
-    sitemapgenerator    :
-      group             : "socialapi"
-      supervisord       :
-        command         :
-          run           : "#{GOBIN}/sitemapgenerator"
-          watch         : "#{GOBIN}/watcher -run socialapi/workers/cmd/sitemapgenerator -watch socialapi/workers/sitemapgenerator"
-
-    activityemail       :
-      group             : "socialapi"
-      supervisord       :
-        command         :
-          run           : "#{GOBIN}/activityemail"
-          watch         : "#{GOBIN}/watcher -run socialapi/workers/cmd/email/activityemail -watch socialapi/workers/email/activityemail"
-
-    privatemessageemailfeeder:
-      group             : "socialapi"
-      supervisord       :
-        command         :
-          run           : "#{GOBIN}/privatemessageemailfeeder"
-          watch         : "#{GOBIN}/watcher -run socialapi/workers/cmd/email/privatemessageemailfeeder -watch socialapi/workers/email/privatemessageemailfeeder"
-
-    privatemessageemailsender:
-      group             : "socialapi"
-      supervisord       :
-        command         :
-          run           : "#{GOBIN}/privatemessageemailsender"
-          watch         : "#{GOBIN}/watcher -run socialapi/workers/cmd/email/privatemessageemailsender -watch socialapi/workers/email/privatemessageemailsender"

     presence            :
       group             : "socialapi"


Removed ChannelViewLayout and moved its functionality to subclasses.

--- core/app/backbone/views/channel_view.coffee
+++ core/app/backbone/views/channel_view.coffee
@@ -1,18 +1,14 @@
 #= require jquery.hoverIntent

-class window.ChannelViewLayout extends Backbone.Marionette.Layout
+class window.ChannelView extends Backbone.Marionette.Layout
   template: 'channels/channel'
+
   regions:
     factList: '#facts_for_channel'
-    activityList: '#activity_for_channel'
     subChannelsRegion: '.js-subchannels-region'
     addToChannelRegion: '.add-to-channel-region'
     creatorProfileRegion: ".created_by_region"

-  templateHelpers: ->
-    activities_link: -> @link + '/activities'
-
-class window.ChannelView extends ChannelViewLayout
   initialize: (opts) ->
     @on 'render', =>
       @renderSubChannels()
@@ -38,7 +34,12 @@
   onRender: ->
     @factList.show @getFactsView()

-class window.ChannelActivitiesView extends ChannelViewLayout
+class window.ChannelActivitiesView extends Backbone.Marionette.Layout
+  template: 'channels/channel'
+
+  regions:
+    activityList: '#activity_for_channel'
+
   events:
     "click .js-activities-refresh": "refresh"


Fix mapper link in beatmapset page

Fix mapper link in beatmapset page

--- resources/assets/coffee/react/_components/beatmapset-mapping.coffee
+++ resources/assets/coffee/react/_components/beatmapset-mapping.coffee
@@ -40,7 +40,7 @@
             __html: osu.trans 'beatmapsets.show.details.mapped_by',
               mapper: laroute.link_to_route 'users.show',
                 user.username
-                { user: user.user_id }
+                { user: user.id }
                 class: "#{bn}__user js-usercard"
                 'data-user-id': user.id


More class conversion

More class conversion

--- resources/assets/coffee/react/beatmap-discussions/discussion.coffee
+++ resources/assets/coffee/react/beatmap-discussions/discussion.coffee
@@ -21,28 +21,28 @@

 bn = 'beatmap-discussion'

-BeatmapDiscussions.Discussion = React.createClass
-  mixins: [React.addons.PureRenderMixin]
-
-
-  componentWillMount: ->
+class BeatmapDiscussions.Discussion extends React.PureComponent
+  constructor: (props) ->
+    super props
+
     @eventId = "beatmap-discussion-entry-#{@props.discussion.id}"

+    @state =
+      collapsed: false
+      highlighted: false
+
+
+  componentWillMount: =>
     $.subscribe "beatmapDiscussionEntry:collapse.#{@eventId}", @setCollapse
     $.subscribe "beatmapDiscussionEntry:highlight.#{@eventId}", @setHighlight

-  componentWillUnmount: ->
+  componentWillUnmount: =>
     $.unsubscribe ".#{@eventId}"
     @voteXhr?.abort()

-  getInitialState: ->
-    collapsed: false
-    highlighted: false
-
-
-  render: ->
+  render: =>
     return div() if @props.discussion.beatmap_discussion_posts.length == 0

     topClasses = "#{bn} js-beatmap-discussion-jump"
@@ -97,7 +97,7 @@
         div className: lineClasses

-  displayVote: (type) ->
+  displayVote: (type) =>
     vbn = 'beatmap-discussion-vote'

     [baseScore, icon] = switch type
@@ -122,7 +122,7 @@
         @props.discussion.votes[type]

-  doVote: (e) ->
+  doVote: (e) =>
     LoadingOverlay.show()

     @voteXhr?.abort()
@@ -141,11 +141,11 @@
     .always LoadingOverlay.hide

-  emitSetHighlight: ->
+  emitSetHighlight: =>
     $.publish 'beatmapDiscussionEntry:highlight', id: @props.discussion.id

-  post: (post, type) ->
+  post: (post, type) =>
     return if !post.id?

     elementName = if post.system then 'SystemPost' else 'Post'
@@ -165,7 +165,7 @@
       currentUser: @props.currentUser

-  setCollapse: (_e, {collapse}) ->
+  setCollapse: (_e, {collapse}) =>
     return unless @props.visible

     newState = collapse == 'collapse'
@@ -175,7 +175,7 @@
     @setState collapsed: newState

-  setHighlight: (_e, {id}) ->
+  setHighlight: (_e, {id}) =>
     newState = id == @props.discussion.id

     return if @state.highlighted == newState
@@ -183,7 +183,7 @@
     @setState highlighted: newState

-  timestamp: ->
+  timestamp: =>
     tbn = 'beatmap-discussion-timestamp'

     div className: tbn,
@@ -203,5 +203,5 @@
           BeatmapDiscussionHelper.formatTimestamp @props.discussion.timestamp

-  toggleExpand: ->
+  toggleExpand: =>
     @setState collapsed: !@state.collapsed


Secure VM object concurrent suppression

Secure VM object concurrent suppression

--- app/modules/vm/index.coffee
+++ app/modules/vm/index.coffee
@@ -31,6 +31,13 @@
     $window.bytesToSize = bytesToSizeFilter # FIXME dirty workaround to custom a Chart.js tooltip template
     {get} = xoApi

+    checkMainObject = ->
+      if !$scope.VM
+        $state.go('index')
+        return false
+      else
+        return true
+
     pool = null
     host = null
     vm = null
@@ -104,7 +111,8 @@
             () => this.stop(),
             this.baseTimeOut
           )
-          return $scope.refreshStats($scope.VM.id)
+          promise = if $scope.VM?.id then $scope.refreshStats($scope.VM.id) else $q.reject()
+          return promise
           .then () => this._reset()
           .catch (err) =>
             if !this.running || this.attempt >= 2 || $scope.VM.power_state isnt 'Running' || $scope.isVMWorking($scope.VM)
@@ -576,7 +584,7 @@
       if not vdi?
         return
       for vbd in vdi.$VBDs
-        rVbd = vbd if (get vbd).VM is $scope.VM.id
+        rVbd = vbd if (get vbd)?.VM is $scope.VM?.id
       return rVbd || null

     $scope.disconnectVBD = (vdi) ->
@@ -758,6 +766,7 @@
       xo.docker.unpause VM, container

     $scope.addVdi = (vdi, readonly, bootable) ->
+      return unless checkMainObject()

       $scope.addWaiting = true # disables form fields
       position = $scope.maxPos + 1
@@ -787,6 +796,7 @@
       return free

     $scope.createVdi = (name, size, sr, bootable, readonly) ->
+      return unless checkMainObject

       $scope.createVdiWaiting = true # disables form fields
       position = $scope.maxPos + 1
@@ -820,6 +830,7 @@
       $scope.newInterfaceMTU = network && network.MTU

     $scope.createInterface = (network, mtu, automac, mac) ->
+      return unless checkMainObject()

       $scope.createVifWaiting = true # disables form fields

@@ -853,19 +864,19 @@

     $scope.canAdmin = (id = undefined) ->
       if id == undefined
-        id = $scope.VM && $scope.VM.id
+        id = $scope.VM?.id

       return id && xoApi.canInteract(id, 'administrate') || false

     $scope.canOperate = (id = undefined) ->
       if id == undefined
-        id = $scope.VM && $scope.VM.id
+        id = $scope.VM?.id

       return id && xoApi.canInteract(id, 'operate') || false

     $scope.canView = (id = undefined) ->
       if id == undefined
-        id = $scope.VM && $scope.VM.id
+        id = $scope.VM?.id

       return id && xoApi.canInteract(id, 'view') || false


Confirm workflow deletion

Confirm workflow deletion

--- app/pages/lab/workflow.cjsx
+++ app/pages/lab/workflow.cjsx
@@ -45,6 +45,8 @@
     selectedTaskKey: @props.workflow.first_task
     goldStandardFilesToImport: null
     forceReloader: 0
+    deletionInProgress: false
+    deletionError: null

   workflowLink: ->
     [owner, name] = @props.project.slug.split('/')
@@ -234,12 +236,12 @@

-              <button type="button" className="minor-button" disabled={@state.deleteInProgress} data-busy={@state.deleteInProgress || null} onClick={@handleDelete}>
+              <button type="button" className="minor-button" disabled={@state.deletionInProgress} data-busy={@state.deletionInProgress || null} onClick={@handleDelete}>
                 Delete this workflow
               </button>
             {' '}
-            {if @state.deleteError?
-              {@state.deleteError.message}}
+            {if @state.deletionError?
+              {@state.deletionError.message}}

@@ -344,9 +346,21 @@
     @props.workflow.addLink 'subject_sets', [DEMO_SUBJECT_SET_ID]

   handleDelete: ->
-    @props.workflow.delete().then =>
-      @props.project.uncacheLink 'workflows'
-      @history.pushState(null, "/lab/#{@props.project.id}")
+    @setState deletionError: null
+
+    confirmed = confirm 'Really delete this workflow and all its tasks?'
+
+    if confirmed
+      @setState deletionInProgress: true
+
+      @props.workflow.delete().then =>
+        @props.project.uncacheLink 'workflows'
+        @history.pushState(null, "/lab/#{@props.project.id}")
+      .catch (error) =>
+        @setState deletionError: error
+      .then =>
+        if @isMounted()
+          @setState deletionInProgress: false

   handleTaskChange: (taskKey, path, value) ->
     changes = {}


remove outdated home.cjsx

remove outdated home.cjsx

--- app/pages/project/home.cjsx
+++ app/pages/project/home.cjsx
@@ -1,87 +0,0 @@
-React = require 'react'
-{Markdown} = (require 'markdownz').default
-FinishedBanner = require './finished-banner'
-`import ProjectMetadata from './metadata'`
-getWorkflowsInOrder = require '../../lib/get-workflows-in-order'
-{Link} = require 'react-router'
-{TextSplit} = require('seven-ten')
-
-module.exports = React.createClass
-  displayName: 'ProjectHomePage'
-
-  contextTypes:
-    geordi: React.PropTypes.object
-
-  getDefaultProps: ->
-    activeWorkflows: []
-    owner: {}
-    project: {}
-    user: null
-
-  handleWorkflowSelection: (workflow) ->
-    @props.onChangePreferences 'preferences.selected_workflow', workflow?.id
-    undefined # Don't prevent default Link behavior.
-
-  render: ->
-

-

-        <FinishedBanner project={@props.project} />
-
-

-          <TextSplit splitKey="landing.text"
-            textKey="description"
-            splits={@props.splits}
-            default={@props.project.description} />
-

-        {if @props.project.workflow_description? and @props.project.workflow_description isnt ''
-
{@props.project.workflow_description}
}
-
-        {if @props.project.redirect
-
-            Visit the project

-            at {@props.project.redirect}
-
-        # For Gravity Spy
-        else if @props.project.experimental_tools.indexOf('workflow assignment') isnt -1
-          if @props.user?
-            currentWorkflowAtLevel = @props.activeWorkflows?.filter (workflow) =>
-              workflow if workflow.id is @props.preferences?.settings.workflow_id
-            currentLevel = if currentWorkflowAtLevel.length > 0 then currentWorkflowAtLevel[0].configuration.level else 1
-            @props.activeWorkflows?.map (workflow) =>
-              if workflow.configuration.level <= currentLevel and workflow.configuration.level?
-                <Link
-                  to={"/projects/#{@props.project.slug}/classify"}
-                  key={workflow.id + Math.random()}
-                  className="call-to-action standard-button"
-                  onClick={@handleWorkflowSelection.bind this, workflow}
-                >
-                  You've unlocked level {workflow.display_name}
-                </Link>
-          else
-            <Link to={"/projects/#{@props.project.slug}/classify"} className="call-to-action standard-button">
-              Get started!
-            </Link>
-        else if @props.project.configuration?.user_chooses_workflow
-          @props.activeWorkflows.map (workflow) =>
-            <Link
-              to={"/projects/#{@props.project.slug}/classify"}
-              key={workflow.id + Math.random()}
-              className="call-to-action standard-button"
-              onClick={@handleWorkflowSelection.bind this, workflow}
-            >
-              {workflow.display_name}
-            </Link>
-        else
-          <Link to={"/projects/#{@props.project.slug}/classify"} className="call-to-action standard-button">
-            Get started!
-          </Link>}
-

-
-

-

-
About {@props.project.display_name}

-        <Markdown project={@props.project}>{@props.project.introduction ? ''}</Markdown>
-

-
-      <ProjectMetadata project={@props.project} />
-


fix bug in structService-select for extended

--- app/scripts/extended.coffee
+++ app/scripts/extended.coffee
@@ -29,16 +29,15 @@
     # - escape: boolean, will be used by the escaper-directive
     structServiceSelect:
         template: selectTemplate
-        controller: ["$scope", "$timeout", "structService", ($scope, $timeout, structService) ->
+        controller: ["$scope", "structService", ($scope, structService) ->
             attribute = $scope.$parent.tokenValue.value
-            selectedCorpora = settings.corpusListing.getSelectedCorpora()
+            selectedCorpora = settings.corpusListing.selected

             # check which corpora support attributes
             corpora = []
-            for corpus in selectedCorpora
-                corpusSettings = settings.corpora[corpus]
+            for corpusSettings in selectedCorpora
                 if attribute of corpusSettings.structAttributes or (attribute of corpusSettings.attributes)
-                    corpora.push corpus
+                    corpora.push corpusSettings.id

             structService.getStructValues(corpora, [attribute], {count: false, returnByCorpora: false}).then((data) ->
                 localizer = localize($scope)


Remove utils.rangesFromLocator()

Remove utils.rangesFromLocator()

--- src/readmill/utils.coffee
+++ src/readmill/utils.coffee
@@ -107,23 +107,6 @@
     xpath: annotation.ranges[0]
     file_id: annotation.page

-  # Nasty extraction method for getting the range start offset and end offset
-  # using just the pre and post strings. Leaving this here for one commit for
-  # posterity.
-  rangesFromLocator: (locator) ->
-    {pre, xpath} = locator
-    endOffset = if xpath.start == xpath.end then pre.length else 0
-    range =
-      start: locator.xpath.start
-      startOffset: locator.pre.length
-      end: locator.xpath.end
-      endOffset: endOffset
-
-    sniffed = Annotator.Range.sniff(range)
-    browserRange = sniffed.normalize(jQuery('article div')[0]).toRange()
-    range.endOffset = jQuery(browserRange.endContainer).text().lastIndexOf(locator.post)
-    [range]
-
   # Public: Takes an annotation object and returns a highlight object
   # suitable for submission to the Readmill server.
   #
@@ -135,8 +118,8 @@
   #     highlight = utils.highlightFromAnnotation(ann)
   #
   # Returns a highlight object.
-  highlightFromAnnotation: (annotation, root) ->
-    locator = @locatorsFromAnnotation(annotation, root)
+  highlightFromAnnotation: (annotation) ->
+    locator = @locatorsFromAnnotation(annotation)
     # See: https://github.com/Readmill/API/wiki/Readings
     {
       id: annotation.id
@@ -183,8 +166,8 @@
   # Returns a jQuery.Deferred() promise.
   annotationFromHighlight: (highlight, client) ->
     # Try and extract annotation metadata from the locators object.
-    meta = (try ranges: @rangesFromLocator(highlight.locators) catch e then null)
-    meta = (try ranges: JSON.parse(highlight.pre) catch e then null) unless meta
+    meta = (try ranges: [highlight.locators.xpath] catch e then null) if highlight.locators.xpath
+    meta = (try ranges: JSON.parse(highlight.pre) catch e then null)  unless meta
     deferred = new jQuery.Deferred()

     if meta


fix #39

fix #39

--- src/js/core/engines/audioCore.coffee
+++ src/js/core/engines/audioCore.coffee
@@ -53,7 +53,9 @@
             # 用于HACK Audio在IOS上的限制, 参考: http://www.ibm.com/developerworks/library/wa-ioshtml5/
             if opts.needPlayEmpty
                 playEmpty = () =>
-                    @setUrl(opts.emptyMP3).play()
+                    # 当前没有set过url时才set一个空音频，以免影响到成功自动播放的后续交互
+                    if not @getUrl()
+                        @setUrl(opts.emptyMP3).play()
                     win.removeEventListener('touchstart', playEmpty, false)
                 win.addEventListener('touchstart', playEmpty, false)


:racehorse: Find elements by id in spec reporter

Previously classes containing the spec/suite id were used which
caused major amounts of time finding DOM nodes using class selectors
instead of id selectors.

This decreases the Editor spec from ~90s to ~30s.

--- spec/atom-reporter.coffee
+++ spec/atom-reporter.coffee
@@ -130,7 +130,7 @@
     bundledPackageSpecs = 0
     userPackageSpecs = 0
     for spec in specs
-      symbol = $$ -> @li class: "spec-summary pending spec-summary-#{spec.id}"
+      symbol = $$ -> @li id: "spec-summary-#{spec.id}", class: "spec-summary pending"
       switch spec.specType
         when 'core'
           coreSpecs++
@@ -159,7 +159,7 @@
     @runningSpecCount++

   specComplete: (spec) ->
-    specSummaryElement = $(".spec-summary-#{spec.id}")
+    specSummaryElement = $("#spec-summary-#{spec.id}")
     specSummaryElement.removeClass('pending')
     specSummaryElement.data("description", spec.getFullName())

@@ -185,7 +185,7 @@
   suite: null

   initialize: (@suite) ->
-    @addClass("suite-view-#{@suite.id}")
+    @attr('id', "suite-view-#{@suite.id}")
     @description.html @suite.description

   attach: ->
@@ -194,7 +194,7 @@
   parentSuiteView: ->
     return unless @suite.parentSuite

-    if not suiteView = $(".suite-view-#{@suite.parentSuite.id}").view()
+    if not suiteView = $("#suite-view-#{@suite.parentSuite.id}").view()
       suiteView = new SuiteResultView(@suite.parentSuite)
       suiteView.attach()

@@ -222,7 +222,7 @@
     @parentSuiteView().append this

   parentSuiteView: ->
-    if not suiteView = $(".suite-view-#{@spec.suite.id}").view()
+    if not suiteView = $("#suite-view-#{@spec.suite.id}").view()
       suiteView = new SuiteResultView(@spec.suite)
       suiteView.attach()


Added find or initialize function to the backbone clouds collection

--- app/assets/javascripts/collections/clouds.js.coffee
+++ app/assets/javascripts/collections/clouds.js.coffee
@@ -2,4 +2,15 @@

   model: Cloudsdale.Models.Cloud
   url: -> "/v1/clouds"
-    +
+  # Public: Tries to find the model inside of the collection
+  # if it can't be found it tries to initialize it from the server.
+  #
+  # Examples:
+  #
+  # collection.findOrInitialize('..someid...')
+  # # => Cloud
+  #
+  # Returns an instance of Cloudsdale.Models.Cloud if found, otherwise undefined.
+  findOrInitialize: (id) ->
+    cloud = @get(id) || new Cloudsdale.Models.Cloud(id: id)


Made to blink faster.

Made to blink faster.

--- blink.coffee
+++ blink.coffee
@@ -19,7 +19,7 @@
         console.log 'Off'
         if err
           console.log 'Error turning pin off', err
-  , 1000
+  , 100

 process.on 'SIGINT', ->
   gpio.close(pin) ->


Actually focus on search input

Actually focus on search input

--- app/assets/javascripts/modules/search/bar.js.coffee
+++ app/assets/javascripts/modules/search/bar.js.coffee
@@ -3,7 +3,7 @@
     constructor: (@options) ->
       @$el = $('.search-bar')
       @$triggerEl = $('.search-button')
-      @$inputEl = $('.search-input')
+      @$inputEl = $('header .search-input')
       @options ||= {}

       @addEventListeners()


whoops

whoops

--- modules/player/commands.coffee
+++ modules/player/commands.coffee
@@ -40,7 +40,7 @@
       if not @permissions.isDJ(msg.member) and msg.author.id isnt queue.nowPlaying.requestedBy.id
         return msg.reply "You are not allowed to skip songs." if not data.data.voteSkip
         return msg.reply 'You must be in a voice channel.' if not msg.member.getVoiceChannel()
-      return msg.reply 'You must be in the same voice channel the bot is in.' if queue.nowPlaying.voiceChannel.id isnt msg.member.getVoiceChannel().id
+        return msg.reply 'You must be in the same voice channel the bot is in.' if queue.nowPlaying.voiceChannel.id isnt msg.member.getVoiceChannel().id
         queue.nowPlaying.voteSkip = [] if not queue.nowPlaying.voteSkip

         return msg.reply 'Did you really try to skip this song **again**?' if msg.author.id in queue.nowPlaying.voteSkip


Clarified a few specs.

Clarified a few specs.

--- spec/src/grid_spec.coffee
+++ spec/src/grid_spec.coffee
@@ -1,7 +1,7 @@
 "use strict"

 describe "ring", ->
-  it "returns coordinates for a ring", ->
+  it "returns coordinates for a ring two (2) tiles from center", ->
     grid = new Grid()
     result = grid.ring(-1, -1, 2)
     expect(result).toContain({ q:  1, r: -1 })
@@ -21,7 +21,7 @@
   beforeEach ->
     @grid = new Grid()

-  it "returns coordinates for a solid hexagonal shape", ->
+  it "returns coordinates for a solid hexagonal shape stretching two (2) tiles from center", ->
     result = @grid.hexagon(-1, -1, 2, true)
     expect(result).toContain({ q: -1, r: -1 })
     expect(result).toContain({ q:  0, r: -1 })
@@ -44,7 +44,7 @@
     expect(result).toContain({ q:  0, r:  0 })
     expect(result.length).toEqual(19)

-  it "returns coordinates for a hexagonal shape without the center coordinate", ->
+  it "returns coordinates for a hexagonal shape stretching two (2) tiles from center without the center coordinate", ->
     result = @grid.hexagon(-1, -1, 2)
     expect(result).toContain({ q:  0, r: -1 })
     expect(result).toContain({ q:  0, r: -2 })


losing tests

losing tests

--- src/app/rounds/rounds.spec.coffee
+++ src/app/rounds/rounds.spec.coffee
@@ -71,7 +71,8 @@

 		store = {
 			"current_round": 1,
-			"score": 0
+			"score": 0,
+			"lives": 3
 		}

 		$provide.constant "gameStorage",
@@ -170,6 +171,32 @@
 				expect(storage.increment).not.toHaveBeenCalled

+		describe "Timer runs out without a correct guess", ->
+			it "decrements lives", ->
+				spyOn(storage, "increment").andCallThrough()
+				scope.secondsLeft = 1
+				scope.onTimeout()
+				expect(storage.increment).toHaveBeenCalledWith 'lives', -1
+			it "updates status variable", ->
+				scope.secondsLeft = 1
+				scope.onTimeout()
+				expect(scope.status).toEqual 'lost'
+			it "redirects to end state if there are lives left", ->
+				spyOn(scope.$state, "transitionTo").andCallThrough()
+				scope.secondsLeft = 1
+				scope.onTimeout()
+				timeout.flush()
+				expect(scope.$state.transitionTo).toHaveBeenCalledWith "end", tag : 'lotr', before: '1391212800000', win: false
+			it "redirects to lose state if there are no lives left", ->
+				spyOn(scope.$state, "transitionTo").andCallThrough()
+				storage.increment('lives',-2)
+				scope.secondsLeft = 1
+				scope.onTimeout()
+				timeout.flush()
+				expect(scope.$state.transitionTo).toHaveBeenCalledWith "lose"
+
+
+
 	describe "End controller", ->

 		beforeEach angular.mock.inject(($controller) ->


Social: add message by slug function and export it

--- workers/social/lib/social/models/socialapi/message.coffee
+++ workers/social/lib/social/models/socialapi/message.coffee
@@ -17,6 +17,8 @@
     sharedMethods :
       static   :
         byId   :
+          (signature Object, Function)
+        bySlug :
           (signature Object, Function)
         post   :
           (signature Object, Function)
@@ -90,6 +92,11 @@
     fnName  : 'messageById'
     validate: ["id"]

+  # bySlug -get message by slug
+  @bySlug = secureRequest
+    fnName  : 'messageBySlug'
+    validate: ["slug"]
+
   @listReplies = secureRequest
     fnName   : 'listReplies'
     validate : ['messageId']


Social: SocialMessage fetch function is added

--- workers/social/lib/social/models/socialapi/message.coffee
+++ workers/social/lib/social/models/socialapi/message.coffee
@@ -31,6 +31,8 @@
         sendPrivateMessage:
           (signature Object, Function)
         fetchPrivateMessages:
+          (signature Object, Function)
+        fetch  :
           (signature Object, Function)

     schema          :
@@ -115,6 +117,14 @@
         requests[funcName] options, callback

+  @fetch = permit 'read posts',
+    success: (client, data, callback)->
+      {connection:{delegate}} = client
+      unless data.id
+        return callback {message: "Request is not valid for reading a message"}
+      {fetchMessage} = require './requests'
+      fetchMessage data, callback
+
   @ensureGroupChannel = (client, callback)->
     fetchGroup client, (err, group)->
       return callback err  if err


update url for push notification

update url for push notification

--- config/gcm.coffee
+++ config/gcm.coffee
@@ -2,7 +2,7 @@

 module.exports =
   push:
-    url: "https://#{domain}/mobile/api/push"
+    url: "#{process.env.MOBILEURL}/api/push"
     data:
       url: '/chat/<%=roster.type%>/<%=roster.user ? roster.user.id : roster.group.id%>'
       title: '<%=roster.name()%>'


fix: update atom config

fix: update atom config

--- configs/atom.cson
+++ configs/atom.cson
@@ -16,6 +16,8 @@
     softWrap: true
   "exception-reporting":
     userId: "c3e419a6-250a-4e79-85f8-48fdda7d7718"
+  filesize:
+    UseDecimal: true
   "ide-flowtype":
     tryFlowBin: true
   "linter-stylelint":


removed unnecessary requirement

removed unnecessary requirement


Update installation instructions link for elm-format

--- src/beautifiers/elm-format.coffee
+++ src/beautifiers/elm-format.coffee
@@ -19,7 +19,7 @@
         '--yes',
         name
         ],
-        { help: { link: 'https://github.com/avh4/elm-format' } }
+        { help: { link: 'https://github.com/avh4/elm-format#installation-' } }
       )
       .then () =>
         @readFile(name)


Removed parens

Removed parens

--- index.coffee
+++ index.coffee
@@ -35,5 +35,5 @@
   frame
     .format 'none'
     .color 'none', 'none'
-    .restore('screen')
+    .restore 'screen'
   process.exit()


Add method to check if git is rebasing.

Add method to check if git is rebasing.

--- index.coffee
+++ index.coffee
@@ -66,6 +66,18 @@
       impromptu.exec 'git symbolic-ref HEAD 2>/dev/null', (err) ->
         done err, !! err

+  git.register 'isRebasing',
+    update: (done) ->
+      git.root (err, root) ->
+        return done err if err
+        impromptu.exec "test -d #{root}/.git/rebase-merge -o -d #{root}/.git/rebase-apply", (err) ->
+          if not err
+            done null, true
+          else if err.code is 1
+            done null, false
+          else
+            done err, null
+
   git.register 'remoteBranch',
     update: (done) ->
       tracking_branch_command = "git for-each-ref --format='%(upstream:short)' $(git symbolic-ref -q HEAD)"


Disable voxel-virus for now, depends on this.game.materials

--- index.coffee
+++ index.coffee
@@ -113,7 +113,7 @@
     'voxel-carry': {inventoryWidth:10, inventoryRows:5}
     'voxel-bucket': {fluids: ['water', 'lava']}
     'voxel-fluid': {}
-    'voxel-virus': {materialSource: 'water', material: 'waterFlow', isWater: true}
+    #'voxel-virus': {materialSource: 'water', material: 'waterFlow', isWater: true} # requires this.game.materials
     'voxel-skyhook': {}
     'voxel-blockdata': {}
     'voxel-chest': {}