Enable simple tray icon state changes

Enable simple tray icon state changes

Add a very simple tray icon state change: when there are unread
messages, use a distinct tray icon to provide a visual cue.

--- src/main.coffee
+++ src/main.coffee
@@ -58,7 +58,6 @@

 mainWindow = null
 tray = null
-hasTray = process.platform != 'darwin'

 # No more minimizing to tray, just close it
 readyToClose = false
@@ -107,22 +106,20 @@
         "min-width": 620
         "min-height": 420
         icon: path.join __dirname, 'icons', 'icon.png'
-        show: !hasTray or !nconf.get 'startminimized'
+        show: !nconf.get 'startminimized'
     }

     # Create the system tray
-    if hasTray
-        # Create tray
-        tray = new Tray path.join __dirname, 'icons', 'icon.png'
-        contextMenu = Menu.buildFromTemplate [
-            { label: 'Hide/show', click: toggleWindowVisible }
-            { label: 'Quit', click: quit}
-        ]
-        tray.setToolTip 'YakYak - Hangouts client'
-        tray.setContextMenu contextMenu
-
-        # Emitted when the tray icon is clicked
-        tray.on 'clicked', toggleWindowVisible
+    tray = new Tray path.join __dirname, 'icons', 'icon.png'
+    contextMenu = Menu.buildFromTemplate [
+        { label: 'Hide/show', click: toggleWindowVisible }
+        { label: 'Quit', click: quit}
+    ]
+    tray.setToolTip 'YakYak - Hangouts client'
+    tray.setContextMenu contextMenu
+
+    # Emitted when the tray icon is clicked
+    tray.on 'clicked', toggleWindowVisible

     # and load the index.html of the app. this may however be yanked
     # away if we must do auth.
@@ -268,6 +265,10 @@

     ipc.on 'updatebadge', (ev, value) ->
         app.dock.setBadge(value) if app.dock
+        if value > 0
+            tray.setImage path.join __dirname, 'icons', 'icon-unread.png'
+        else
+            tray.setImage path.join __dirname, 'icons', 'icon.png'

     ipc.on 'searchentities', (ev, query, max_results) ->
         promise = client.searchentities query, max_results
@@ -345,7 +346,7 @@
     # For OSX only hides the window if we're not force closing.
     # Prevent close, if minimizetotray is enabled.
     mainWindow.on 'close', (ev) ->
-        hideToTray = hasTray and !readyToClose and nconf.get 'minimizetotray'
+        hideToTray = !readyToClose and nconf.get 'minimizetotray'
         darwinHideOnly = process.platform == 'darwin' and not mainWindow?.forceClose

         if hideToTray or darwinHideOnly


automatically refresh display on changes

--- src/main.coffee
+++ src/main.coffee
@@ -216,6 +216,15 @@
       eventClick: event_click
       select: select

+    # Do not force a refresh on each event.
+    pending_refresh = null
+    refresh = ->
+      calendar 'refetchEvents'
+      pending_refresh = null
+
+    on_change = ->
+      pending_refresh ?= setTimeout refresh, 500
+
     start_replication = (url) ->
       if url isnt ''
         remotedb = new PouchDB url
@@ -227,6 +236,7 @@
           continuous: true
           complete: (err) ->
             logger err
+          onChange: on_change

     db.get 'replicate', (err,doc) ->
       if doc?.url?


Fix syntax error.

Fix syntax error.

--- src/aftership.coffee
+++ src/aftership.coffee
@@ -33,7 +33,7 @@
 printTrackingCurrentInfo = (tracking) ->
   ":package: Package #{tracking.id}. Current status is #{translateStatus(tracking.tag)}."

-printCheckPointsInfo = (checkpoints)
+printCheckPointsInfo = (checkpoints) ->
   msgs = checkpoints.reverse().map (checkpoint) ->
     "- #{moment(checkpoint.checkpoint_time).fromNow()} #{translateStatus(checkpoint.tag)} #{checkpoint.message}."
   msgs.join("\n")


Initial support for animation graphs

Initial support for animation graphs

--- src/animation.coffee
+++ src/animation.coffee
@@ -24,8 +24,8 @@
 class Animation extends EventEmitter

 	AnimationProperties: [
-		"view", "properties", "curve", "time",
-		"origin", "tolerance", "precision"
+		"view", "properties", "curve", "time",
+		"origin", "tolerance", "precision", "graph"
 	]
 	AnimatableCSSProperties: {
 		opacity: "",
@@ -33,10 +33,10 @@
 		height: "px",
 	}
 	AnimatableMatrixProperties: [
-		"x", "y", "z",
-		"scaleX", "scaleY", "scaleZ", # "scale",
+		"x", "y", "z",
+		"scaleX", "scaleY", "scaleZ", # "scale",
 		"rotateX", "rotateY", "rotateZ", # "rotate"
-	] # width, height, opacity
+	]

 	constructor: (args) ->

@@ -49,7 +49,7 @@
 		# Set all the defaults
 		@time ?= 1000
 		@curve ?= "linear"
-		@precision ?= 30
+		@precision ?= 40

 		@curveValues = @_parseCurve @curve
 		@count = 0
@@ -66,7 +66,8 @@

 		# See if we have other animations running on this view
 		# if @view._currentAnimations.length > 0
-		# 	console.log "Warning: Animation.start #{@animationName} already animations running on view #{@view.name}"
+		# 	console.log "Warning: Animation.start #{@animationName} already
+		# 	 animations running on view #{@view.name}"

 		# We stop all other animations on this view. Maybe we should revisit
@@ -79,7 +80,7 @@

 		# if @_running is true
 		# 	throw Error "Animation.start #{@animationName} already running"
-		#
+		#
 		# @_running = true

 		# TODO: test if we are trying to animate something that cannot animate
@@ -100,7 +101,7 @@

 		# Build up the matrix animation properties

-		for k in @AnimatableMatrixProperties
+		for k in @AnimatableMatrixProperties

 			@propertiesA[k] = propertiesA[k]

@@ -132,7 +133,11 @@
 				-webkit-animation-timing-function: linear;
 				-webkit-animation-fill-mode: both;
 			}"
-
+
+
+		if @graph
+			@_graphView = @graphView @, 10, 20, 20, @time
+
 		@view.addClass @animationName

 		finalize = =>
@@ -171,7 +176,7 @@
 		calculatedStyles = {}
 		computedStyles = @view.computedStyles
 		for k, v of @AnimatableCSSProperties
-			calculatedStyles[k] = computedStyles
+			calculatedStyles[k] = computedStyles
 		@view.style = calculatedStyles

 		@_cleanup()
@@ -199,6 +204,8 @@
 		@view.removeClass @animationName
 		@emit "end"

+		@_graphView?.visible = false
+
 		# console.log "_cleanup", @view._currentAnimations

 	_css: ->
@@ -216,10 +223,13 @@

 		# Pre-calculate the delta values
 		for propertyName, value of @propertiesA
-			deltas[propertyName] = (@propertiesB[propertyName] - @propertiesA[propertyName]) / 100.0
-
-		# We define this object outside of the loop to re-use it. In theory this should help a
-		# bit with perfomance, in practise I'm too lazy to prove it.
+			deltas[propertyName] = (
+				@propertiesB[propertyName] - @propertiesA[propertyName]
+			) / 100.0
+
+		# We define this object outside of the loop to re-use it.
+		# In theory this should help a bit with perfomance, in practise
+		# I'm too lazy to prove it.
 		m = new Matrix()

 		for springValue in @curveValues
@@ -275,5 +285,55 @@
 			console.log "Animation.parseCurve: could not parse curve '#{curve}'"
 			return bezier.defaults.Linear @precision, @time

+	graphView: (animation, x, y, h, time) ->
+
+		color = "rgba(50,150,200,.35)"
+		values = animation.curveValues
+
+		width = 300
+
+		graph = new View
+			y: y
+			x: x
+			width:100
+			height:h
+
+		# graph.style.backgroundColor = "rgba(0, 0, 0, 1)"
+		graph.clip = false
+
+		background = new View
+			y: 0 - h
+			height:h * 2 + 3
+			superView: graph
+
+		background.style.backgroundColor = "rgba(255,255,255,.85)"
+
+
+		for value, i in values
+			dot = new View
+				width:3, height:3
+				x: i * widthFactor, y: (100 - value) * (h / 100)
+				superView: graph
+			dot.style.borderRadius = "5px"
+			dot.style.backgroundColor = color
+
+		graph.width = dot.x
+		background.width = dot.x
+
+		if time
+			player = new View
+				x:0, y:-h
+				width: 2, height: h * 2
+				superView: graph
+
+			player.style.backgroundColor = color
+
+			player.animate
+				properties: {x:graph.width}
+				time: time
+
+		return graph
+
+

 exports.Animation = Animation


OHM-224: removed white spaces

OHM-224: removed white spaces


lt startの権限管理を修正

lt startの権限管理を修正

--- scripts/lt.coffee
+++ scripts/lt.coffee
@@ -7,6 +7,7 @@
 #   hubot lt next <number> - LTのナンバリングを設定する
 #   hubot lt last <number> - LTのナンバリングを設定する
 #   hubot lt info - 次回LTのナンバリングを確認する
+#   hubot lt check - lt startを実行する権限があるか確認する

 WebClient = require("slack-client").WebClient
@@ -48,7 +49,7 @@

 module.exports = (robot) ->
   robot.respond /LT START ?(.*)$/i, (msg) ->
-    if robot.auth.isAdmin(msg.envelope.user)
+    if robot.auth.hasRole(msg.envelope.user, "lt")
       if msg.match[1]
         channel_name = msg.match[1]
       else
@@ -77,3 +78,9 @@
   robot.respond /LT INFO ?(\d*)$/i, (msg) ->
     lt_next_num = robot.brain.get("lt.next") ? 1
     msg.reply "次回: lt#{zero_padding lt_next_num}"
+
+  robot.respond /LT CHECK$/i, (msg) ->
+    if robot.auth.hasRole(msg.envelope.user, "lt")
+      msg.reply "権限があります"
+    else
+      msg.reply "権限がありません"


Don't default menu value

Don't default menu value

--- build/tasks/compile-packages-slug-task.coffee
+++ build/tasks/compile-packages-slug-task.coffee
@@ -13,7 +13,7 @@
     menuPath = path.join(menusPath, "#{process.platform}.json")
     menu = CSON.readFileSync(menuPath) if fs.isFileSync(menuPath)
     rm menusPath
-    menu ? {}
+    menu

   getKeymaps = (appDir) ->
     keymapsPath = path.join(appDir, 'keymaps')


Improve log message

Improve log message

--- src/circle-ci.coffee
+++ src/circle-ci.coffee
@@ -153,8 +153,8 @@
     try
       robot.send user, formatBuildStatus(req.body.payload)

-      console.log "Send CircleCI build status message"
-
+      console.log "Sent CircleCI build status message"
+
     catch error
       console.log "circle hook error: #{error}. Payload: #{util.inspect(req.body.payload)}"


Don't lie in a comment

Don't lie in a comment

--- scripts/rc.coffee
+++ scripts/rc.coffee
@@ -166,8 +166,10 @@
       cb(false, this)

   # Invoke callback as:
-  # cb(error, [RCscore, ...]) with all RCScores found
-  # after `start` for `@who`
+  # cb(error, self) with all RCScores found stored as an array
+  # in @recent fully updated.
+  # `start` - the furthest back to look for RCScores
+  # `cb` - callback invoked as cb(err, self) with error == false on success
   fetch_recent: (start, cb) =>
     console.log "fetch_recent: #{start} for #{@who}"
     @storage.zrevrangebyscore @key("#{@who}:times"), '+inf', start, (err, res) =>


Fix missing key react warning

Fix missing key react warning

--- src/javascript/main.coffee
+++ src/javascript/main.coffee
@@ -10,6 +10,7 @@
 # * More/better raw stat formatters (minutes to H:M, etc)
 # * tick marks on HP Chart
 # * Fix 1 column layout, test on phone/tablet
+# * Partial analogy icons (0.6 of an avatar)

 React = require 'react'
 $ = require 'jquery'
@@ -366,7 +367,7 @@
     numShips = d3.round(@props.damage / ship.ehp, 1)
     stamps = []
     for i in [0...Math.floor(numShips)]
-      stamps.push dom.img {src: eveIconUrl(ship.id), className: 'pull-right'}
+      stamps.push dom.img {key: i, src: eveIconUrl(ship.id), className: 'pull-right'}

     dom.div null,
       dom.h4 {className: 'pull-right'},


Fixed typo, unbreaking damage analogy panel

--- src/javascript/main.coffee
+++ src/javascript/main.coffee
@@ -14,7 +14,7 @@
         callback_url: 'http://disda.in/eve/yir/'
       'spreadsheetsin.space':
         sso_client_id: 'ee5517f79a684842b0a5cf599d752411'
-        callback_url: 'http://spreadsheetsin.space/year-in-review/'
+        callback_url: 'https://spreadsheetsin.space/year-in-review/'
   live:
     sso_host: 'https://login.eveonline.com'
     sso_client_id: '67a0cc0f68d34e77b9751f8c75dd2e31'
@@ -292,7 +292,7 @@

     iskPerHour = @state.stats.perHour 'iskIn'
     iskPerHourPull = dom.h4 {className: 'pull-left'},
-      dom.em null, "Your earned #{humanizeLargeNum iskPerHour} ISK per hour."
+      dom.em null, "You earned #{humanizeLargeNum iskPerHour} ISK per hour."

     marketPanel = React.createElement(MarketPanel, {stats: @state.stats})

@@ -441,7 +441,7 @@
       redirect_uri: SSO_CALLBACK_URL
       client_id: SSO_CLIENT_ID
       scope: 'publicData characterStatisticsRead'
-      state: 'testState'
+      state: ''

     ssoUrl = "#{SSO_HOST}/oauth/authorize/?#{qs.stringify ssoParams}"

@@ -906,7 +906,7 @@
     return {
       ship: null
     }
-  componentDidMount: ->
+  determineShipToUse: ->
     shipToUse = null
     numShips = 0
     # find a good candidate ship to show, first ship that results in less than 100
@@ -917,6 +917,8 @@
       if numShips < 100
         break
     @setState {ship: shipToUse}
+  componentDidMount: ->
+    @determineShipToUse()
   render: ->
     if @props.damage == 0
       return null
@@ -1695,7 +1697,6 @@
     mobydick:
       name: 'Crime and Punishment'
       words: 211591
-      link: 'http://www.gutenberg.org/files/2701/2701.txt'
     atlasshrugged:
       name: 'Atlas Shrugged'
       words: 561996


:memo: Make methods public by default

:memo: Make methods public by default

--- lib/minimap.coffee
+++ lib/minimap.coffee
@@ -80,10 +80,12 @@
   ViewManagement.includeInto(this)
   PluginManagement.includeInto(this)

-  # Public: The minimap package version
+  ### Public ###
+
+  # The minimap package version
   version: require('../package.json').version

-  # Public: The default minimap settings
+  # The default minimap settings
   configDefaults:
     plugins: {}
     autoToggle: false
@@ -100,7 +102,7 @@
   # Internal: The activation state of the minimap package.
   active: false

-  # Public: Activates the minimap package.
+  # Activates the minimap package.
   activate: ->
     atom.workspaceView.command 'minimap:toggle', => @toggleNoDebug()
     atom.workspaceView.command 'minimap:toggle-debug', => @toggleDebug()
@@ -114,22 +116,22 @@

     @toggleNoDebug() if atom.config.get 'minimap.autoToggle'

-  # Public: Deactivates the minimap package.
+  # Deactivates the minimap package.
   deactivate: ->
     @destroyViews()
     @emit('deactivated')

-  # Public: Toggles the minimap activation state with debug turned on.
+  # Toggles the minimap activation state with debug turned on.
   toggleDebug: ->
     @getChannel().activate()
     @toggle()

-  # Public: Toggles the minimap activation state with debug turned off.
+  # Toggles the minimap activation state with debug turned off.
   toggleNoDebug: ->
     @getChannel().deactivate()
     @toggle()

-  # Public: Verifies that the passed-in version expression is satisfied by
+  # Verifies that the passed-in version expression is satisfied by
   # the current minimap version.
   #
   # `expectedVersion` - A [semver](https://github.com/npm/node-semver)


Verbiage changes

Verbiage changes

--- app/lib/en-us.coffee
+++ app/lib/en-us.coffee
@@ -80,7 +80,7 @@
     doneLabel: 'Done'

     welcome:
-      header: 'Welcome to the Leaves Project!'
+      header: 'Welcome to the MicroPlants Project!'
       content: 'This quick tutorial will guide you through your first classification.'
     whatIsThis:
       header: 'What are we looking at?'
@@ -112,7 +112,7 @@
       overview: 'Overview'
       aboutBryophytes: 'About Bryophytes'
       bryophyteImportance: 'Importance of Bryophytes'
-      origin: 'Origin of Images'
+      origin: 'From the field to The Field'
       howCanIHelp: 'How can I help?'

     content:
@@ -147,8 +147,8 @@

The significance of biological collections of museums and academic institutions make countless contributions to science and society in general, especially in documenting, describing and discovering diversity. In this project, dried plant specimens are collected in the field from habitats ranging from tropical cloud forests to temperate tundra.

-
-
+
+

Where are the specimens stored?

@@ -158,9 +158,9 @@

Liverworts grow all over the world, but these Frullania images have typically been collected from the southern hemisphere in areas like Fiji, New Caledonia, and Chile. Specimens were collected in the field, brought back to the museum, and painstakingly mounted onto slides. From there, they imaged with a microscope by our many interns. Images were taken by interns at the Field Museum from Northeastern Illinois University, DePaul University, Harold Washington Chicago City College, and The Ohio State University. You might be the first person ever to study and analyze a particular image!

-
-
-
+
+
+

Where have we collected?


update education page content (resolves #267)

--- app/lib/en-us.coffee
+++ app/lib/en-us.coffee
@@ -504,7 +504,7 @@
     mainHeader         : 'Education'
     tabs:
       educatorGuide    : 'Educator Guide'
-      classroom        : 'Classroom'
+      classroom        : 'Other Classroom Resources'
       calculator       : 'Calculator'

     section:
@@ -541,7 +541,7 @@

What resources are available for the classroom?

-              Here are some resources that can be great tools for introducing students to Planet Hunters. Here are a couple of our favorites:
+              Here are some resources that can be great tools for introducing students to Planet Hunters.

                   NASA Kepler Missions website:
@@ -556,19 +556,6 @@

                   National Science Foundation's Science Behind The News: Extrasolar Planets, describes the Transit and Doppler methods.
-

-

-                  NASA has also provided tools you can use in the classroom related to Exoplanets:
-                  https://www.youtube.com/watch?v=0WatNUka7OA
-

-

-

-
-

-              Educators Guides, here are lessons that are focused on exoplanets:
-

-

-                  Coming Fall 2014, Planet Hunters Educators Guide! Zooniverse educators have developed lessons to show you how you can best utilize Planet Hunters in your classroom.

                   Educator Resource Guide provided by NASA:


Update ja.coffee

Update ja.coffee

--- app/locale/ja.coffee
+++ app/locale/ja.coffee
@@ -736,7 +736,7 @@
     subs_only: "課金者限定"
     create_clan: "新規クラン作成"
     private_preview: "プレビュー"
-#    private_clans: "Private Clans"
+    private_clans: "非公開クランmptクラン"
     public_clans: "公開クラン"
     my_clans: "マイ・クラン"
     clan_name: "クラン名"
@@ -767,8 +767,8 @@
     complete_2: "完了"
     started_2: "進行中"
     not_started_2: "未着手"
-    view_solution: "クリックしてソリューションを見る。"
-#    view_attempt: "Click to view attempt."
+    view_solution: "ソリューションを見る。"
+    view_attempt: "アテンプトを見る."
     latest_achievement: "最新業績"
     playtime: "プレイ時間"
     last_played: "最終プレイ"
@@ -1079,18 +1079,18 @@
     duplicate: "重複"
     stop_duplicate: "重複をやめる"
     rotate: "回転"
-#    level_settings_title: "Settings"
-#    level_component_tab_title: "Current Components"
-#    level_component_btn_new: "Create New Component"
-#    level_systems_tab_title: "Current Systems"
-#    level_systems_btn_new: "Create New System"
-#    level_systems_btn_add: "Add System"
-#    level_components_title: "Back to All Thangs"
-#    level_components_type: "Type"
-#    level_component_edit_title: "Edit Component"
-#    level_component_config_schema: "Config Schema"
-#    level_component_settings: "Settings"
-#    level_system_edit_title: "Edit System"
+    level_settings_title: "設定"
+    level_component_tab_title: "現在の部品"
+    level_component_btn_new: "新しい部品を作る"
+    level_systems_tab_title: "現在のシステム"
+    level_systems_btn_new: "新しいシステムを作る"
+    level_systems_btn_add: "システムの追加"
+    level_components_title: "全てびサングに戻る"
+    level_components_type: "タイプ"
+    level_component_edit_title: "部品の編集"
+    level_component_config_schema: "スキーマの設定"
+    level_component_settings: "設定"
+    level_system_edit_title: "システムの編集"
 #    create_system_title: "Create New System"
 #    new_component_title: "Create New Component"
 #    new_component_field_system: "System"


just some RU translated strings are being added else (#5881)

* some new trings have been translated

* fixes

--- app/locale/ru.coffee
+++ app/locale/ru.coffee
@@ -27,10 +27,10 @@
 #    century_skills_subblurb3: "Throughout the game, there are opportunities for students to collaborate when they get stuck and to work together using our pair programming guide."
 #    century_skills_quote4: "I’ve always had aspirations of designing video games and learning how to code ... this is giving me a great starting point."
     century_skills_subtitle4: "Коммуникация"
-    century_skills_subblurb4: "Кодирование требует от детей практиковать новые формы общения, включая общение с самим компьютером и передачу своих идей с использованием наиболее эффективного кода."
+    century_skills_subblurb4: "Программирование требует от детей практиковать новые формы общения, включая общение с самим компьютером и передачу своих идей с использованием наиболее эффективного кода."
     classroom_in_box_title: "Мы стремимся:"
-    classroom_in_box_blurb1: "Вовлеките каждого ученика, чтобы он поверил, что кодирование для них."
-    classroom_in_box_blurb2: "Дайте возможность любому преподавателю чувствовать себя уверенно при обучении кодированию."
+    classroom_in_box_blurb1: "Вовлеките каждого ученика, чтобы он поверил, что программирование им по зубам."
+    classroom_in_box_blurb2: "Дайте возможность любому преподавателю чувствовать себя уверенно при обучении программированию."
     classroom_in_box_blurb3: "Вдохновите всех школьных лидеров создать программу компьютерных наук мирового класса."
 #    classroom_in_box_blurb4: ""
 #    covid19_banner_blurb: "We are offering free access to our full curriculum to educators and schools affected by school closures."
@@ -59,7 +59,7 @@
     for_leaders_subtitle3: "Гибкие варианты использования"
 #    for_leaders_subblurb3: "Whether you want to build a Middle School coding elective, a CTE pathway, or an AP Computer Science Principles class, CodeCombat is tailored to suit your needs."
     for_leaders_subtitle4: "Навыки реального мира"
-    for_leaders_subblurb4: "Студенты создают навыки и развивают мышление роста с помощью задач кодирования, которые готовят их к работе на 500K+ открытых вычислительных площадках."
+#    for_leaders_subblurb4: "Студенты создают навыки и развивают мышление роста с помощью задач кодирования, которые готовят их к работе на 500K+ открытых вычислительных площадках."
     for_teachers_title: "Для учителей"
     for_teachers_blurb: "Инструменты для раскрытия потенциала ученика"
     for_teachers_subtitle1: "Проектное обучение"
@@ -72,7 +72,7 @@
 #    for_teachers_subblurb4: "Engage all learners in a diverse classroom with practice levels that adapt to each student's learning needs."
 #    game_based_blurb: "CodeCombat is a game-based computer science program where students type real code and see their characters react in real time."
     get_started: "Начать"
-#    global_title: "Join Our Global Community of Learners and Educators"
+    global_title: "Присоединяйся к нашему глобальному сообществу учеников и преподавателей"
     global_subtitle1: "Учащиеся"
     global_subtitle2: "Строки с кодом"
     global_subtitle3: "Учителя"
@@ -209,7 +209,7 @@
     resource_hub: "Ресурсы"
     apcsp: "AP CS Принципы"
     parent: "Родителям"
-#    browser_recommendation: "For the best experience we recommend using the latest version of Chrome. Download the browser here!"
+    browser_recommendation: "Для наилучшего опыта мы рекомендуем использоватеть последнюю версию Chrome. Скачай браузер здесь!"

   modal:
     close: "Закрыть"
@@ -1611,7 +1611,7 @@
 #    web_dev_language_transition: "All classes program in HTML / JavaScript for this course.  Classes that have been using Python will start with extra JavaScript intro levels to ease the transition.  Classes that are already using JavaScript will skip the intro levels."
     course_membership_required_to_play: "You'll need to join a course to play this level."
     license_required_to_play: "Попроси своего учителя назначить тебе лицензию для продолжения игры на CodeCombat!"
-#    update_old_classroom: "New school year, new levels!"
+    update_old_classroom: "Новый учебный год, новые уровни!"
 #    update_old_classroom_detail: "To make sure you're getting the most up-to-date levels, make sure you create a new class for this semester by clicking Create a New Class on your"
 #    teacher_dashboard: "teacher dashboard"
 #    update_old_classroom_detail_2: "and giving students the new Class Code that appears."
@@ -1620,7 +1620,7 @@
 #    view_ranking: "view ranking"
 #    ranking_position: "Position"
 #    ranking_players: "Players"
-#    ranking_completed_leves: "Completed levels"
+    ranking_completed_leves: "Пройденные уровни"
     challenge: "Испытание:"
 #    challenge_level: "Challenge Level:"
 #    status: "Status:"
@@ -1673,7 +1673,7 @@
 #    appreciation_week_blurb4: "whether you'd like licenses for Week 1 (May 6-10) or Week 2 (May 13-17)"
 #    hoc_happy_ed_week: "Happy Computer Science Education Week!"
 #    hoc_blurb1: "Learn about the free"
-#    hoc_blurb2: "Code, Play, Share"
+    hoc_blurb2: "Программируй, играй, делись"
 #    hoc_blurb3: "activity, download a new teacher lesson plan, and tell your students to log in to play!"
 #    hoc_button_text: "View Activity"
     no_code_yet: "Студент еще не писал код для этого уровня."
@@ -3058,23 +3058,23 @@

 #  hoc_2018:
 #    banner: "Welcome to Hour of Code 2019!"
-#    page_heading: "Your students will learn to code by building their own game!"
-#    step_1: "Step 1: Watch Video Overview"
-#    step_2: "Step 2: Try it Yourself"
-#    step_3: "Step 3: Download Lesson Plan"
+    page_heading: "Ваши ученики будут учиться программировать, создавая свою собственную игру"
+    step_1: "Шаг 1: Посмотрите обзор"
+    step_2: "Шаг 2: Попробуй сам"
+    step_3: "Шаг 3: Скачай план урока"
 #    try_activity: "Try Activity"
-#    download_pdf: "Download PDF"
-#    teacher_signup_heading: "Turn Hour of Code into a Year of Code"
-#    teacher_signup_blurb: "Everything you need to teach computer science, no prior experience needed."
-#    teacher_signup_input_blurb: "Get first course free:"
-#    teacher_signup_input_placeholder: "Teacher email address"
+    download_pdf: "Скачать PDF"
+    teacher_signup_heading: "Преврати Час кода в Год кода"
+    teacher_signup_blurb: "Всё, что тебе нужно — изучать компьютерные науки, предшествующий опыт не нужен."
+    teacher_signup_input_blurb: "Получи первый курс бесплатно:"
+    teacher_signup_input_placeholder: "Email учителя"
 #    teacher_signup_input_button: "Get CS1 Free"
 #    activities_header: "More Hour of Code Activities"
 #    activity_label_1: "Escape the Dungeon!"
 #    activity_label_2: " Beginner: Build a Game!"
 #    activity_label_3: "Advanced: Build an Arcade Game!"
 #    activity_button_1: "View Lesson"
-#    about: "About CodeCombat"
+    about: "О CodeCombat"
 #    about_copy: "A game-based, standards-aligned computer science program that teaches real, typed Python and JavaScript."
 #    point1: "✓ Scaffolded"
 #    point2: "✓ Differentiated"
@@ -3089,18 +3089,18 @@
 #    welcome: "Welcome to CodeCombat's Hour of Code 2019!"
 #    educator: "I'm an educator"
 #    show_resources: "Show me teacher resources!"
-#    student: "I'm a student"
-#    ready_to_code: "I'm ready to code!"
+    student: "Я ученик"
+    ready_to_code: "Я готов программировать!"

 #  hoc_2018_completion:
 #    congratulations: "Congratulations on completing <b>Code, Play, Share!</b>"
 #    send: "Send your Hour of Code game to friends and family!"
-#    copy: "Copy URL"
+    copy: "Копировать URL"
 #    get_certificate: "Get a certificate of completion to celebrate with your class!"
 #    get_cert_btn: "Get Certificate"
 #    first_name: "First Name"
 #    last_initial: "Last Initial"
-#    teacher_email: "Teacher's email address"
+    teacher_email: "Email учителя"

 #  school_administrator:
 #    title: "School Administrator Dashboard"
@@ -3137,7 +3137,7 @@
 #    fill_boxes: "Whoops, make sure to fill all boxes before hitting \"Submit.\""

 #  browser_recommendation:
-#    title: "CodeCombat works best on Chrome!"
+    title: "CodeCombat наилучшим образом работает через Chrome!"
 #    pitch_body: "For the best CodeCombat experience we recommend using the latest version of Chrome. Download the latest version of chrome by clicking the button below!"
-#    download: "Download Chrome"
+#    download: "Скачать Chrome"
 #    ignore: "Ignore"


Update web_dev section in uk.cofee (#4014)

Proposed translation of the web_dev section. "картинки" has been chosen intentionally as less formal than "зображення" and thus more children-friendly.

--- app/locale/uk.coffee
+++ app/locale/uk.coffee
@@ -1545,20 +1545,20 @@
 #  game_dev:
 #    creator: "Creator"

-#  web_dev:
-#    image_gallery_title: "Image Gallery"
-#    select_an_image: "Select an image you want to use"
-#    scroll_down_for_more_images: "(Scroll down for more images)"
-#    copy_the_url: "Copy the URL below"
-#    copy_the_url_description: "Useful if you want to replace an existing image."
-#    copy_the_img_tag: "Copy the <img> tag"
-#    copy_the_img_tag_description: "Useful if you want to insert a new image."
-#    copy_url: "Copy URL"
-#    copy_img: "Copy <img>"
-#    how_to_copy_paste: "How to Copy/Paste"
-#    copy: "Copy"
-#    paste: "Paste"
-#    back_to_editing: "Back to Editing"
+  web_dev:
+    image_gallery_title: "Галерея картинок"
+    select_an_image: "Вибери картинку, яку хочеш використати"
+    scroll_down_for_more_images: "(Прокрути вниз, щоб побачити більше картинок)"
+    copy_the_url: "Скопіюй URL нижче"
+    copy_the_url_description: "Корисно, якщо ти хочешь замінити існуючу картинку."
+    copy_the_img_tag: "Скопіюй <img> тег"
+    copy_the_img_tag_description: "Корисно, якщо ти хочешь вставити нову картинку."
+    copy_url: "Скопіювати URL"
+    copy_img: "Скопіювати <img>"
+    how_to_copy_paste: "Як скопіювати/вставити"
+    copy: "Скопіювати"
+    paste: "Вставити"
+    back_to_editing: "Назад до редагування"

   classes:
     archmage_title: "Архімаг"


Update Grunt pages task. [ci skip]

Update Grunt pages task. [ci skip]

--- build/gh-pages.coffee
+++ build/gh-pages.coffee
@@ -20,7 +20,8 @@
       'dist/**/*'
       'docs/**/*'
       'lib/**/*'
-      'tests/**/*.{css,html,js}'
+      'tests/**/*'
+      '!tests/**/*.{scss,coffee}'
       'README.md'
     ]
     dest: 'gh-pages/'


More cleanups

More cleanups

--- lib/motions.coffee
+++ lib/motions.coffee
@@ -262,49 +262,43 @@
     @view = new VimCommandModeInputView(@)

   execute: (count=1) ->
-    _.times count, =>
-      @findOrBeep (pos) =>
-        @editor.setCursorBufferPosition(pos)
+    @match count, (pos) =>
+      @editor.setCursorBufferPosition(pos)

   select: (count=1) ->
     cur = @editor.getCursorBufferPosition()
-    _.times count, =>
-      @findOrBeep (pos) =>
-        @editor.setSelectedBufferRange([cur, pos])
-        true
+    @match count, (pos) =>
+      @editor.setSelectedBufferRange([cur, pos])
+    [true]

   confirm: (view) =>
+    @scan(view.value)
     @editor.trigger 'vim-mode:search-complete'

-  findOrBeep: (callback)->
-    pos = @findNext(@view.value)
+  match: (count, callback) ->
+    window.matches = @matches
+    window.count = count
+
+    pos = @matches[(count - 1) % @matches.length]
     if pos?
       callback(pos)
     else
       shell.beep()

-  findNext: (term) ->
+  scan: (term) ->
     regexp = new RegExp(term, 'g')
-    result = null
-
-    pos = @editor.getCursorBufferPosition()
+    cur = @editor.getCursorBufferPosition()
     matchPoints = []
-
     iterator = (item) =>
       matchPoints.push(item.range.start)

     @editor.activeEditSession.scan(regexp, iterator)

-    previous = (p for p in matchPoints when p.compare(pos) <= 0)
-    after = (p for p in matchPoints when p.compare(pos) == 1)
-
-    if after.length > 0
-      return after[0]
-    else if previous.length > 0
-      return previous[0]
-    else
-      shell.beep()
-      return null
+    previous = _.filter matchPoints, (point) ->
+      point.compare(cur) <= 0
+    after = _.difference(matchPoints, previous)
+    after.push(previous...)
+    @matches = after

 module.exports = { Motion, CurrentSelection, SelectLeft, SelectRight, MoveLeft,
   MoveRight, MoveUp, MoveDown, MoveToPreviousWord, MoveToNextWord,


One forward-directed search working

One forward-directed search working

--- lib/motions.coffee
+++ lib/motions.coffee
@@ -1,4 +1,5 @@
 {_, $$, Point, Range} = require 'atom'
+shell = require 'shell'
 VimCommandModeInputView = require './vim-command-input-view'

 class Motion
@@ -265,17 +266,23 @@
     _.times @count, =>

       pos = @editor.getCursorBufferPosition()
+      matchPoints = []

       iterator = (item) =>
-        console.log item.range.start
-        console.log pos
-        if (start = item.range.start) > pos
-          @editor.setCursorBufferPosition(start)
-          item.stop()
-
-      @editor.scanInBufferRange(regexp
-                                [[0,0],[Infinity,Infinity]],
-                                iterator)
+        matchPoints.push(item.range.start)
+
+      @editor.activeEditSession.scan(regexp, iterator)
+
+      previous = (p for p in matchPoints when p < pos)
+      after = (p for p in matchPoints when p >= pos)
+      if after.length > 0
+        @editor.setCursorBufferPosition(after[0])
+      else if previous.length > 0
+        @editor.setCursorBufferPosition(previous[0])
+      else
+
+      window.matchPoints = matchPoints
+      window.pos = pos

   select: (count=1) ->
     @count = count


Layout fix

Layout fix

--- app/assets/javascripts/rglossa/react/components/centre/search_inputs/cwb_multiword_term.jsx.coffee
+++ app/assets/javascripts/rglossa/react/components/centre/search_inputs/cwb_multiword_term.jsx.coffee
@@ -20,8 +20,8 @@
                 {isFirst ? null :

Interval

-                  <input type="text" className="interval" value={term.min} min />

-                  <input type="text" className="interval" value={term.max} max />
+                  <input type="text" className="interval" value={term.min} /> min

+                  <input type="text" className="interval" value={term.max} /> max

                 }

@@ -58,9 +58,11 @@

                     <input type="checkbox" checked={term.isLemma} /> Lemma

+

                     <input type="checkbox" title="Start of word" checked={term.isStart} /> Start

+

                     <input type="checkbox" title="End of word" checked={term.isEnd} /> End


If geolocation is too inaccurate, pretend we don't have location at all

--- src/p13n.coffee
+++ src/p13n.coffee
@@ -94,6 +94,8 @@
             window.i18n_debug = i18n

         _handle_location: (pos) =>
+            if pos.coords.accuracy > 10000
+                return
             @last_position = pos
             @trigger 'position', pos
             if not @get 'location_requested'


[#19] Fix .DCM header in KiCad generator

--- src/kicad-generator.coffee
+++ src/kicad-generator.coffee
@@ -40,7 +40,7 @@

     log.start "KiCad library doc '#{@name}.dcm'"
     fd = fs.openSync "#{dir}/#{@name}.dcm", 'w'
-    fs.writeSync fd, "EESchema-DOCLIB Version 2.0 Date: #{timestamp}\n#\n"
+    fs.writeSync fd, "EESchema-DOCLIB  Version 2.0 Date: #{timestamp}\n#\n"
     for element in @library.elements
       @_generateDoc fd, element
     fs.writeSync fd, '# End Doc Library\n'


remove processor

remove processor

--- src/layout/behavior.coffee
+++ src/layout/behavior.coffee
@@ -15,10 +15,8 @@

 {my} = require '../my'
 {make} = require '../render/make'
-# {processor} = require './processor'
 {programs} = require './programs'

-#
 # TODO: Editing Issues
 # * add new programs
 # * rename events
@@ -26,15 +24,6 @@
 # * reorder commmands within and between programs
 # * drag commands from above into a program
 # * drag programs into the commands view!
-#
-# TODO: Behavior Issues
-# * step through a program
-# * send next command to sprite
-# * detect and handle interruptions
-#   * hitting boundaries
-#   * collisions
-#   * finishing levels
-#

 exports.behavior = (sprite) ->
   rows = make.rows 'behavior', programs(sprite)
@@ -42,9 +31,7 @@
   my.assert default_program, "no default_program"

   initial_label = default_program._LABEL
-  my.extend rows, # processor(initial_label, sprite),
-    {
-#      _EXPORTS: ['step']
-      y: (world) -> world.index * my.row.spacing
-      class: 'program'
-    }
+  my.extend rows, {
+    y: (world) -> world.index * my.row.spacing
+    class: 'program'
+  }


(fix) Fix pages array.

(fix) Fix pages array.

--- app/scripts/corrector.coffee
+++ app/scripts/corrector.coffee
@@ -1,20 +1,21 @@
 app = require './app'
 ko = require 'knockout'

-app.router.pages
-    {
-      path: 'login'
-      component: require('./pages/login/login')()
-    }
-    {
-      path: 'overview'
-      component: require('./pages/exercises/exercises')()
-    }
-    {
-      path: /exercise\/?(.*)/
-      as: ['id'] #name the parameters
-      component: require('./pages/editor/editor')()
-    }
+app.router.pages [
+  {
+    path: 'login'
+    component: require('./pages/login/login')()
+  }
+  {
+    path: 'overview'
+    component: require('./pages/exercises/exercises')()
+  }
+  {
+    path: /exercise\/?(.*)/
+    as: ['id'] #name the parameters
+    component: require('./pages/editor/editor')()
+  }
+]

 app.router.goto 'login'


Routes: fix bugs, impl. more API, improve code, etc.

--- lib/routes._coffee
+++ lib/routes._coffee
@@ -5,9 +5,11 @@
 config = require '../config'
 Content = require './content'
 Embed = require './embed'
+Errors = require './errors'
 Fetcher = require './fetcher'
 path = require 'path'
 Processor = require './processor'
+URL = require 'url'

 # Constants
 PIPELINE_PATH = path.join __dirname, '..', 'pipeline'
@@ -34,31 +36,54 @@

 #
 # Pre-reqs:
-# - req.params.id
+# - `id` param
 #
 @getContentById = (req, res, _) ->
-    id = parseInt req.params.id, 10
-    if not id? or isNaN id
-        return res.json 404, error:
-            code: 404
-            message: 'Not found'
+    id = req.param 'id'
+    if not id
+        errorAPI res, 404, Errors.MISSING_ID_OR_URL
+        return

     content = Content.getById id, _
     if not content?
-        return res.json 404, error:
-            code: 404
-            message: 'Not found'
+        errorAPI res, 404, Errors.NO_CONTENT_WITH_ID.replace '{ID}', id
+        return
+
     res.json 200, content

-
-#
-# Pre-reqs:
-# - req.params.url
+#
+# Pre-reqs:
+# - `url` param
 #
 @getContentByURL = (req, res, _) ->
-    # FIXME: This sometimes redirects to the view page; this route should
-    # always return API data and redirect to the content API route.
-    handleURL res, req.params.url, _
+    url = req.param 'url'
+    if not url
+        errorAPI res, 404, Errors.MISSING_ID_OR_URL
+        return
+    if not validateURL url
+        errorAPI res, 400, Errors.MALFORMED_URL
+        return
+
+    # Get or create this content, enqueueing it for conversion if it's new.
+    # But support us rejecting new content, e.g. if we're overloaded.
+    # TODO: Implement config for rejecting new content.
+    #
+    # TODO: This logic should probably be abstracted in the Content class,
+    # since it's core business logic, used by (and currently duplicated by)
+    # both our API and our website. But in order to do that, we'd want it to
+    # throw semantic error codes, so that error handling can remain custom.
+    #
+    content = Content.getByURL url, _
+    if not content
+        if false
+            errorAPI res, 503, Errors.SERVICE_UNAVAILABLE
+            return
+        else
+            content or= Content.fromURL url, _
+            enqueueForConversion content
+
+    # HACK: Hardcoding knowledge of URL from app._coffee.
+    redirectAPI res, 301, "/v1/content/#{content.id}", content

 ## UI:
@@ -66,69 +91,143 @@
 #
 # Pre-reqs: (none)
 #
-# TODO: Zoom.it back-compat of ?url query string support.
-#
-@getHomepage = (req, res, _) ->
-    res.render 'home'
-
-#
-# Pre-reqs:
-# - req.params.id
+# But supports passing a URL for conversion, via ?url query string param.
+# This was part of Zoom.it's original UI / informal website API.
+#
+@getHomepage = (req, res, _) =>
+    if req.query.url
+        @submitURL req, res, _
+    else
+        res.render 'home'
+
+#
+# Pre-reqs:
+# - `id` param
 #
 @getEmbed = (req, res, _) ->
-    id = parseInt req.params.id, 10
-    if not id? or isNaN id
-        return res.send 404
+    id = req.param 'id'
+    if not id
+        errorHTML res, 404, Errors.MISSING_ID_OR_URL
+        return
+
+    res.type 'application/javascript'
     res.send embed.generate id, _

 #
 # Pre-reqs:
-# - req.params.id
+# - `id` param
 #
 @getViewer = (req, res, _) ->
-    id = parseInt req.params.id, 10
-    if not id? or isNaN id
-        return res.send 404
+    id = req.param 'id'
+    if not id
+        errorHTML res, 404, Errors.MISSING_ID_OR_URL
+        return
+
     content = Content.getById id, _
     if not content?
-        return res.send 404
+        errorHTML res, 404, Errors.NO_CONTENT_WITH_ID.replace '{ID}', id
+        return
+
     res.render 'view', {content}

 #
 # Pre-reqs:
-# - req.params.url
+# - `url` param
 #
 @submitURL = (req, res, _) ->
-    # FIXME: This sometimes returns API JSON; this route should always
-    # redirect to the view page.
-    handleURL res, req.params.url, _
+    url = req.param 'url'
+    if not url
+        errorHTML res, 404, Errors.MISSING_ID_OR_URL
+        return
+    if not validateURL url
+        errorHTML res, 400, Errors.MALFORMED_URL
+        return
+
+    # Get or create this content, enqueueing it for conversion if it's new.
+    # But support us rejecting new content, e.g. if we're overloaded.
+    # TODO: Implement config for rejecting new content.
+    #
+    # TODO: This logic should probably be abstracted in the Content class,
+    # since it's core business logic, used by (and currently duplicated by)
+    # both our API and our website. But in order to do that, we'd want it to
+    # throw semantic error codes, so that error handling can remain custom.
+    #
+    content = Content.getByURL url, _
+    if not content
+        if false
+            errorHTML res, 503, Errors.SERVICE_UNAVAILABLE
+            return
+        else
+            content or= Content.fromURL url, _
+            enqueueForConversion content
+
+    # HACK: Hardcoding knowledge of URL from app._coffee.
+    res.redirect 301, "/#{content.id}"

 ## HELPERS:

-# Helper for the two different routes for URLs:
-handleURL = (res, url, _) ->
-    if not url?
-        res.json 400, error:
-            message: 'Please give us the full URL,
-                including `http://` or `https://`.'
-        return false
-
-    content = Content.getByURL url, _
-    if content?
-        res.redirect content.shareUrl
-        return false
-
-    content = Content.fromURL url, _
-    # Redirect to metadata
-    res.redirect content.self
-
-    # Async operations:
-
-    # Fetch source
+#
+# Return an API error response with the given status code and message.
+#
+errorAPI = (res, status, message) ->
+    res.status status
+    res.type 'text/plain'
+    res.send message
+
+#
+# Render an HTML error page with the given status code and message.
+#
+errorHTML = (res, status, message) ->
+    # TODO: Render nice error view instead of this plaintext error message.
+    res.send status, message
+
+#
+# Return a redirect API response with a JSON body for convenience.
+#
+redirectAPI = (res, status, location, body={}) ->
+    res.status status
+    res.set 'Location', location
+    res.json body
+
+#
+# Returns whether the given URL is well-formed or not.
+#
+# TODO: This should probably be encapsulated in the Content class,
+# as validation is core business logic, not just for our API layer.
+#
+validateURL = (url) ->
+    # Use Node's built-in URL parser, and duck-type / feature-detect:
+    uri = URL.parse url
+    uri.protocol in ['http:', 'https:'] and !!uri.host and !!uri.path
+        # TODO: Do we also want to reject e.g. localhost, etc.?
+        # If we do that, we should differentiate via semantic error codes.
+
+#
+# Enqueues the given content for conversion.
+#
+# TODO: Should this be abstracted away in a more business logic layer?
+# Rather than this file which defines API & website routes?
+#
+enqueueForConversion = (content) ->
+    # TODO: We should properly use a queue and workers for conversion!
+    # For now, converting directly on our web servers.
+    convertContent content, (err) ->
+        if err
+            console.error "(Async) Error converting content!
+                (ID: #{content.id}) #{err.stack or err}"
+        else
+            console.log "(Async) Successfully converted content.
+                (ID: #{content.id})"
+
+#
+# Converts the given content.
+#
+# TODO: Should this be abstracted away in a more business logic layer?
+# Rather than this file which defines API & website routes?
+#
+convertContent = (content, _) ->
     source = fetcher.fetch content, _
-
-    # Create DZI
     destination = processor.process source, _


Fixes steelbrain/atom-hack#25

Fixes steelbrain/atom-hack#25

--- lib/tooltip.coffee
+++ lib/tooltip.coffee
@@ -16,13 +16,15 @@

     # show expression type if mouse stopped somewhere
     @subscriber.subscribe @scroll, 'mousemove', (e) =>
-      @hideDetail()
+      @clearTooltip()
       @Timeout = setTimeout =>
         @showDetail(e);
       , 100
     @subscriber.subscribe @scroll, 'mouseout',=>
-      @hideDetail()
-  hideDetail:->
+      @clearTooltip()
+    atom.workspace.onDidChangeActivePaneItem =>
+      @clearTooltip()
+  clearTooltip:->
     clearTimeout @Timeout
     if @TooltipInstance?
       @TooltipInstance.remove()


social: remove privatemessate participants from chat header in real-time

--- client/Social/Activity/views/privatemessage/pane.coffee
+++ client/Social/Activity/views/privatemessage/pane.coffee
@@ -49,6 +49,7 @@
     super channel

     channel.on 'AddedToChannel', @bound 'addParticipant'
+    channel.on 'RemovedFromChannel', @bound 'removeParticipant'

   realtimeMessageArrived: (message) ->
@@ -263,17 +264,25 @@
   addParticipant: (participant) ->

     return  unless participant
-    return  if @participantMap[participant._id]
+    return  if @participantMap[participant._id]?

     participant.id = participant._id

-    @heads.addSubView new AvatarView
+    @heads.addSubView avatar = new AvatarView
       size      :
         width   : 30
         height  : 30
       origin    : participant

-    @participantMap[participant._id] = yes
+    @participantMap[participant._id] = avatar
+
+
+  removeParticipant: (participant) ->
+    return  unless participant
+    return  unless @participantMap[participant._id]?
+
+    @participantMap[participant._id].destroy()
+    delete @participantMap[participant._id]

   createPreviousLink: ->


using @_id instead of pulling id from DOM

--- client/controllers/incidentReports/incidentTable.coffee
+++ client/controllers/incidentReports/incidentTable.coffee
@@ -18,8 +18,7 @@

 Template.incidentTable.events
   'click table.incident-table tr': (event, instance) ->
-    currentId = $(event.target).parent('tr').children('.count').data("incident-id")
-    instance.data.incidents.update({_id: currentId}, {$set: {selected: true}})
+    instance.data.incidents.update({_id: @_id}, {$set: {selected: true}})

   'click .reject': (event, instance) ->
     changeIncidentStatus(false, instance)


in order to notifyPathsUsed() uses xpub cache

--- app/src/wallet/wallet.coffee
+++ app/src/wallet/wallet.coffee
@@ -232,8 +232,7 @@
     index = parseInt(index) if _.isString(index)
     @_account.currentPublicIndex = index + 1
     @save()
-    ledger.wallet.pa
-    ledger.app.dongle?.getPublicAddress @getCurrentPublicAddressPath(), callback
+    ledger.wallet.pathsToAddresses [@getCurrentPublicAddressPath()], callback

   shiftCurrentChangeAddressPath: (callback) ->
     logger().info 'shift change'
@@ -242,7 +241,7 @@
     index = parseInt(index) if _.isString(index)
     @_account.currentChangeIndex = index + 1
     @save()
-    ledger.app.dongle?.getPublicAddress @getCurrentChangeAddressPath(), callback
+    ledger.wallet.pathsToAddresses [@getCurrentChangeAddressPath()], callback

   importPublicAddressPath: (addressPath) ->
     @_account.importedPublicPaths ?= []


rm console

rm console

--- apps/artist/test/meta.coffee
+++ apps/artist/test/meta.coffee
@@ -67,7 +67,6 @@
   describe 'with no blurb or artworks', ->
     beforeEach ->
       @artist = _.extend artistJSON, counts: { artworks: 0 }, blurb: null
-      console.log 'counts', @artist.counts, @artist.blurb
       @html = jade.render fs.readFileSync(@file).toString(),
         artist: @artist
         sd: @sd


Tooltips: minor code styling, removed unnecessary param

--- src/core/view.coffee
+++ src/core/view.coffee
@@ -849,9 +849,12 @@
   removeOverlay:->
     @overlay?.destroy()

-  unsetTooltip:(o = {})->
+
+  unsetTooltip: ->
+
     @tooltip?.destroy()
-    delete @tooltip
+    @tooltip = null
+

   setTooltip:(o = {})->


fix safari flicker when scrolling near the bottom of the page

--- apps/home/client/view.coffee
+++ apps/home/client/view.coffee
@@ -133,11 +133,24 @@

     # add / remove the 'bottom mode' state
     if @scrollTop > @documentHeight - (@browserHeight * 2)
+      @setHeroBottomMode true
+    else
+      @setHeroBottomMode false
+
+  # we hide the element then add the class then defer a show.
+  # this fixes flickering in safari when modifying covered dom nodes
+  setHeroBottomMode: (enabled) ->
+    if enabled
       unless @$hero.hasClass 'bottom-mode'
-        @$hero.addClass 'bottom-mode'
+        @$hero.hide().addClass 'bottom-mode'
+        _.defer =>
+          @$hero.show()
         @carouselView.showFirstSplashImage()
-    else
-      @$hero.removeClass 'bottom-mode'
+
+    else if @$hero.hasClass 'bottom-mode'
+      @$hero.hide().removeClass 'bottom-mode'
+      _.defer =>
+        @$hero.show()

   animate: =>
     newScrollTop = @$window.scrollTop()


Fix tests

Fix tests

--- test/models/account_spec.coffee
+++ test/models/account_spec.coffee
@@ -57,8 +57,8 @@
         rdio_secret: 'secret'
         number_of_tracks_to_sync: 3
         auto_sync: false
-        last_synced_at: new Date('2013-02-01')
-        sync_type: 'n/a'
+        last_synced_at: null
+        sync_type: 'playCount'
       @sut = new Account @attributes

     it 'should include the all the attributes', ->
@@ -68,4 +68,3 @@
       new_attributes = @sut.toJSON()
       new_attributes.username = 'modnar'
       expect(@sut.toJSON().username).to.equal 'random'
-


organized breaking tests' search results so the ones that should return are before those that shouldn't

--- test/search-devices-spec.coffee
+++ test/search-devices-spec.coffee
@@ -31,24 +31,20 @@
     beforeEach (done) ->
       sabers = [
         {
-          uuid: 'curve-hilted'
-          type: 'light-saber'
-          color: 'blue'
-        }
-        {
           uuid: 'underwater-lightsaber'
           owner: 'darth-vader'
-          type: 'light-saber'
-        }
-        {
-          uuid: 'great-lightsaber'
-          configureWhitelist: ['*']
           type: 'light-saber'
         }
         {
           uuid: 'fire-saber'
           type: 'light-saber'
           discoverWhitelist: ['darth-vader']
+        }
+        {
+          uuid: 'dual-phase-lightsaber'
+          type: 'light-saber'
+          color: 'red'
+          discoverWhitelist: ['*']
         }
         {
           uuid: 'heart-saber'
@@ -61,10 +57,14 @@
                   '*': true
         }
         {
-          uuid: 'dual-phase-lightsaber'
+          uuid: 'curve-hilted'
           type: 'light-saber'
-          color: 'red'
-          discoverWhitelist: ['*']
+          color: 'blue'
+        }
+        {
+          uuid: 'great-lightsaber'
+          configureWhitelist: ['*']
+          type: 'light-saber'
         }
         {
           uuid: 'darth-vader'
@@ -79,7 +79,7 @@

       it 'should return 4 devices', ->
         expect(@devices.length).to.equal 4
-
+
       it 'should return the correct devices', ->
         expect(@devices).to.containSubset [
           {uuid: 'underwater-lightsaber'}


Updated test-generic-parser

Updated test-generic-parser

--- test/test-generic-parser.coffee
+++ test/test-generic-parser.coffee
@@ -5,6 +5,17 @@

 describe 'Generic Parser', ->

+  it 'collectUntilFalse', ->
+
+    input = 'fffff rrrerrrem Something</a>'
+
+    parser = new GenericParser input
+
+    val = parser.takeIfChar 'f'
+    expect(val).to.equal('f')
+
+    val = parser.collectUntilFalse => parser.takeIfChar 'f'
+    expect(val).to.equal('ffff')

   it 'takeUnlessChar, takeUntilChar, takeUntilString', ->


Fix code that was preventing default route from working.

--- assets/coffee/menubar.coffee
+++ assets/coffee/menubar.coffee
@@ -17,7 +17,7 @@
     class MenubarCtrl
       constructor: (@Session, @$scope, @$route) ->
         @$scope.$on '$routeChangeSuccess', @updateMenubar
-        @$scope.$on '$routeChangeStart', @preventNullRoute
+        # @$scope.$on '$routeChangeStart', @preventNullRoute
         @$scope.$on 'login-status', @_updateLoginStatus
         @$scope.view = "reprocess"
         @authenticated = false


Add selected syntax tag to the list by default

--- components/autocomplete/KDAutoCompleteController.coffee
+++ components/autocomplete/KDAutoCompleteController.coffee
@@ -107,7 +107,7 @@
     dropdownWrapper.setClass "kdautocomplete hidden #{@getOptions().listWrapperCssClass}"
     KDView.appendToDOMBody dropdownWrapper

-  hideDropdown:->
+  hideDropdown:->
     dropdownWrapper = @dropdown.getView()
     dropdownWrapper.$().fadeOut 75

@@ -117,7 +117,7 @@
     dropdownWrapper.unsetClass "hidden"
     input  = @getView()
     offset = input.$().offset()
-
+
     offset.top += input.getHeight()
     dropdownWrapper.$().css offset

@@ -280,6 +280,7 @@

     path = @getCollectionPath()

+    itemName  = "#{name}-#{@selectedItemCounter++}"
     if form
       collection = form.getCustomData path
       collection = [] unless collection?
@@ -292,9 +293,9 @@
         else
           $suggest          : itemValue
       )
-      @selectedItemCounter++
+      if item.getOptions().userInput is not ""
+        @selectedItemCounter++
     else
-      itemName  = "#{name}-#{@selectedItemCounter++}"
       @addHiddenInputItem path.join('.'),itemValue

     @addSelectedItemData data
@@ -368,11 +369,11 @@
       else
         @dropdown.getListView().addItemView view

-  getNoItemFoundView: ->
+  getNoItemFoundView:(suggestion) ->
     {nothingFoundItemClass} = @getOptions()
     view = new nothingFoundItemClass
       delegate: @dropdown.getListView()
-      userInput: @getView().getValue()
+      userInput: if suggestion then suggestion else @getView().getValue()

   showNoDataFound: ->
     noItemFoundView = @getNoItemFoundView()


code optimization

code optimization

--- alight/core.coffee
+++ alight/core.coffee
@@ -1,5 +1,5 @@
 # Angular light
-# version: 0.8.14 / 2015-03-18
+# version: 0.8.14 / 2015-03-19

 # init
 alight.version = '0.8.14'
@@ -381,7 +381,7 @@
         scope = new NScope();
     } else scope = new Scope();
     `
-    scope.$system =
+    scope.$system = sys =
         watches: {}
         watchList: []
         watchAny: []
@@ -396,31 +396,31 @@
         finishBinding_lock: false

     if typeof(conf.useObserver) is 'boolean'
-        scope.$system.useObserver = conf.useObserver
+        sys.useObserver = conf.useObserver
     else
-        scope.$system.useObserver = alight.debug.useObserver
-
-    if scope.$system.useObserver
-        scope.$system.useObserver = !!Object.observe
-
-    if scope.$system.useObserver
+        sys.useObserver = alight.debug.useObserver
+
+    if sys.useObserver
+        sys.useObserver = !!Object.observe
+
+    if sys.useObserver
         # chain for active scopes
-        scope.$system.lineHead = null
-        scope.$system.lineTail = null
-        scope.$system.lineActive = false
-        scope.$system.prevSibling = null
-        scope.$system.nextSibling = null
-
-        scope.$system.obList = []  # contain fired watchers
-        scope.$system.obFire = {}
-        scope.$system.ob = ob = alight.observer.observe scope,
+        sys.lineHead = null
+        sys.lineTail = null
+        sys.lineActive = false
+        sys.prevSibling = null
+        sys.nextSibling = null
+
+        sys.obList = []  # contain fired watchers
+        sys.obFire = {}
+        sys.ob = ob = alight.observer.observe scope,
             rootEvent: (key, value) ->
                 if alight.debug.observer
                     console.warn 'Reobserve', key
                 for child in scope.$system.children
                     child.$$rebuildObserve key, value
                 null
-        scope.$system.observers = [ob]  # list of all observers
+        sys.observers = [ob]  # list of all observers
     scope

 alight.Scope = Scope
@@ -442,29 +442,30 @@
     else
         if not scope.$system.ChildScope
             scope.$system.ChildScope = ->
-                @.$system =
+                root = scope.$system.root
+                @.$system = sys =
                     watches: {}
                     watchList: []
                     watchAny: []
                     watchFinishScan: []
-                    root: scope.$system.root
+                    root: root
                     children: []
                     destroy_callbacks: []
                 @.$parent = null
-                if scope.$system.root.$system.useObserver
+                if root.$system.useObserver
                     cscope = @
-                    @.$system.prevSibling = null
-                    @.$system.nextSibling = null
-                    @.$system.lineActive = false
-                    @.$system.obFire = {}
-                    @.$system.ob = ob = alight.observer.observe @,
+                    sys.prevSibling = null
+                    sys.nextSibling = null
+                    sys.lineActive = false
+                    sys.obFire = {}
+                    sys.ob = ob = alight.observer.observe @,
                         rootEvent: (key, value) ->
                             if alight.debug.observer
                                 console.warn 'Reobserve', key
                             for i in cscope.$system.children
                                 i.$$rebuildObserve key, value
                             null
-                    scope.$system.root.$system.observers.push ob
+                    root.$system.observers.push ob
                 @

             scope.$system.ChildScope:: = scope
@@ -494,14 +495,15 @@
 ###

 injectToRootLine = (scope) ->
-    if scope.$system.lineActive
+    sys = scope.$system
+    if sys.lineActive
         return
-    rootSys = scope.$system.root.$system
-    scope.$system.lineActive = true
+    rootSys = sys.root.$system
+    sys.lineActive = true
     t = rootSys.lineTail
     if t
         rootSys.lineTail = t.$system.nextSibling = scope
-        scope.$system.prevSibling = t
+        sys.prevSibling = t
     else
         rootSys.lineHead = rootSys.lineTail = scope

@@ -511,21 +513,22 @@
         @.cb = callback

     watchAny = (scope, lkey, rkey, callback) ->
-        root = scope.$system.root
+        sys = scope.$system
+        rootSys = sys.root.$system

         wa = new WA callback

-        scope.$system[lkey].push wa
-        root.$system[rkey].push wa
+        sys[lkey].push wa
+        rootSys[rkey].push wa

         return {
             stop: ->
-                l = scope.$system[lkey]
+                l = sys[lkey]
                 i = l.indexOf wa
                 if i >= 0
                     l.splice i, 1

-                l = root.$system[rkey]
+                l = rootSys[rkey]
                 i = l.indexOf wa
                 if i >= 0
                     l.splice i, 1
@@ -534,7 +537,8 @@

     Scope::$watch = (name, callback, option) ->
         scope = @
-        rootSys = scope.$system.root.$system
+        sys = scope.$system
+        rootSys = sys.root.$system
         if option is true
             option =
                 isArray: true
@@ -559,7 +563,7 @@
             if key is '$finishScan'
                 return watchAny scope, 'watchFinishScan', 'watchFinishScanAll', callback
             if key is '$destroy'
-                return scope.$system.destroy_callbacks.push callback
+                return sys.destroy_callbacks.push callback
             if key is '$finishBinding'
                 return rootSys.finishBinding_callbacks.push callback
             if option.deep
@@ -572,7 +576,7 @@
         if alight.debug.watch
             console.log '$watch', name

-        d = scope.$system.watches[key]
+        d = sys.watches[key]
         if d
             if not option.readOnly
                 d.extraLoop = true
@@ -588,7 +592,7 @@
             if option.deep
                 value = alight.utilits.clone value
                 option.isArray = false
-            scope.$system.watches[key] = d =
+            sys.watches[key] = d =
                 isArray: Boolean option.isArray
                 extraLoop: not option.readOnly
                 deep: option.deep
@@ -613,10 +617,10 @@
                         if isObserved
                             d.isObserved = true
                             for variable in ce.simpleVariables
-                                ob = alight.observer.watch @.$system.ob, variable, ->
-                                    if scope.$system.obFire[key]
+                                ob = alight.observer.watch sys.ob, variable, ->
+                                    if sys.obFire[key]
                                         return
-                                    scope.$system.obFire[key] = true
+                                    sys.obFire[key] = true
                                     rootSys.obList.push [scope, d]

             if option.isArray and not isObserved
@@ -627,7 +631,7 @@
                 returnValue = d.value

             if not isObserved
-                scope.$system.watchList.push d
+                sys.watchList.push d
                 injectToRootLine scope

         r =
@@ -650,10 +654,10 @@
                 if d.callbacks.length isnt 0
                     return
                 # remove watch
-                delete scope.$system.watches[key]
-                i = scope.$system.watchList.indexOf d
+                delete sys.watches[key]
+                i = sys.watchList.indexOf d
                 if i >= 0
-                    scope.$system.watchList.splice i, 1
+                    sys.watchList.splice i, 1

         if option.init
             callback r.value
@@ -762,18 +766,18 @@
 Scope::$destroy = () ->
     scope = this
     sys = scope.$system
-    root = scope.$system.root
+    rootSys = scope.$system.root.$system

     # fire callbacks
-    for cb in scope.$system.destroy_callbacks
+    for cb in sys.destroy_callbacks
         cb scope
-    scope.$system.destroy_callbacks = []
-
-    if root.$system.useObserver
+    sys.destroy_callbacks = []
+
+    if rootSys.useObserver
         do ->
             # remove observer from root
-            ob = scope.$system.ob
-            l = scope.$system.root.$system.observers
+            ob = sys.ob
+            l = rootSys.observers
             i = l.indexOf ob
             if i >= 0
                 l.splice i, 1
@@ -788,15 +792,15 @@
                 p.$system.nextSibling = n
             else
                 # first scope
-                root.$system.lineHead = n
+                rootSys.lineHead = n
             if n
                 n.$system.prevSibling = p
             else
                 # last scope
-                root.$system.lineTail = p
+                rootSys.lineTail = p

     # remove children
-    for it in scope.$system.children.slice()
+    for it in sys.children.slice()
         it.$destroy()

     # remove from parent
@@ -806,17 +810,17 @@

     # remove watch
     scope.$parent = null
-    scope.$system.watches = {}
-    scope.$system.watchList = []
+    sys.watches = {}
+    sys.watchList = []

     # remove watchAny
     cleanWatchAny = (l, r) ->
-        lst = root.$system[r]
-        for w in scope.$system[l]
+        lst = rootSys[r]
+        for w in sys[l]
             i = lst.indexOf w
             if i >= 0
                 lst.splice i, 1
-        scope.$system[l].length = 0
+        sys[l].length = 0
     cleanWatchAny 'watchAny', 'watchAnyAll'
     cleanWatchAny 'watchFinishScan', 'watchFinishScanAll'

@@ -913,10 +917,12 @@
     total = 0
     obTotal = 0

+    rootSys = root.$system
+
     # observed
-    for ob in root.$system.observers
+    for ob in rootSys.observers
         alight.observer.deliver ob
-    for x in root.$system.obList
+    for x in rootSys.obList
         scope = x[0]
         w = x[1]

@@ -933,10 +939,10 @@
                 extraLoop = true
             for callback in w.callbacks.slice()
                 callback.call scope, value
-    obTotal += root.$system.obList.length
-    root.$system.obList.length = 0
-
-    scope = root.$system.lineHead
+    obTotal += rootSys.obList.length
+    rootSys.obList.length = 0
+
+    scope = rootSys.lineHead
     while scope
         sys = scope.$system

@@ -1000,24 +1006,25 @@
         cfg =
             callback: cfg
     root = this.$system.root
+    rootSys = root.$system
     if cfg.callback
-        root.$system.scan_callbacks.push cfg.callback
+        rootSys.scan_callbacks.push cfg.callback
     if cfg.late
-        if root.$system.lateScan
+        if rootSys.lateScan
             return
-        root.$system.lateScan = true
+        rootSys.lateScan = true
         alight.nextTick ->
-            if root.$system.lateScan
+            if rootSys.lateScan
                 root.$scan()
         return
-    if root.$system.status is 'scaning'
-        root.$system.extraLoop = true
+    if rootSys.status is 'scaning'
+        rootSys.extraLoop = true
         return
-    root.$system.lateScan = false
-    root.$system.status = 'scaning'
+    rootSys.lateScan = false
+    rootSys.status = 'scaning'
     # take scan_callbacks
-    scan_callbacks = root.$system.scan_callbacks.slice()
-    root.$system.scan_callbacks.length = 0
+    scan_callbacks = rootSys.scan_callbacks.slice()
+    rootSys.scan_callbacks.length = 0

     if alight.debug.scan
@@ -1035,18 +1042,18 @@
         while mainLoop
             mainLoop--

-            root.$system.extraLoop = false
-
-            if root.$system.useObserver
+            rootSys.extraLoop = false
+
+            if rootSys.useObserver
                 scan_core2 root, result
             else
                 scan_core root, result

             # call $any
             if result.changes
-                for cb in root.$system.watchAnyAll
+                for cb in rootSys.watchAnyAll
                     cb()
-            if not result.extraLoop and not root.$system.extraLoop
+            if not result.extraLoop and not rootSys.extraLoop
                 break
         if alight.debug.scan
             duration = get_time() - start
@@ -1056,10 +1063,10 @@
             src: result.src
             result: result
     finally
-        root.$system.status = null
+        rootSys.status = null
         for callback in scan_callbacks
             callback.call root
-        for callback in root.$system.watchFinishScanAll
+        for callback in rootSys.watchFinishScanAll
             callback()

     if mainLoop is 0
@@ -1230,12 +1237,13 @@
 ###
 Scope::$watchText = (name, callback, config) ->
     scope = @
+    sys = scope.$system
     config = config or {}

     if alight.debug.watchText
         console.log '$watchText', name

-    w = scope.$system.watches;
+    w = sys.watches;
     d = w[name]
     if d
         if not config.readOnly
@@ -1261,9 +1269,9 @@
                     # remove watch
                     d.callbacks.length = 0
                     delete w[name]
-                    i = scope.$system.watchList.indexOf d
+                    i = sys.watchList.indexOf d
                     if i >= 0
-                        scope.$system.watchList.splice i, 1
+                        sys.watchList.splice i, 1

                 # call listeners
                 for cb in d.onStatic
@@ -1279,17 +1287,17 @@
         d.value = ct.fn scope
         w[name] = d

-        if ct.isSimple and scope.$system.root.$system.useObserver
+        if ct.isSimple and sys.root.$system.useObserver
             d.isObserved = true

             for variable in ct.simpleVariables
-                ob = alight.observer.watch scope.$system.ob, variable, ->
-                    if scope.$system.obFire[name]
+                ob = alight.observer.watch sys.ob, variable, ->
+                    if sys.obFire[name]
                         return
-                    scope.$system.obFire[name] = true
-                    scope.$system.root.$system.obList.push [scope, d]
+                    sys.obFire[name] = true
+                    sys.root.$system.obList.push [scope, d]
         else
-            scope.$system.watchList.push d
+            sys.watchList.push d
             injectToRootLine scope

     if config.onStatic
@@ -1309,9 +1317,9 @@
                     return
                 # remove watch
                 delete w[name]
-                i = scope.$system.watchList.indexOf d
+                i = sys.watchList.indexOf d
                 if i >= 0
-                    scope.$system.watchList.splice i, 1
+                    sys.watchList.splice i, 1

     r

@@ -1391,18 +1399,20 @@
     if not scope
         scope = new alight.Scope()

-    finishBinding = not scope.$system.root.$system.finishBinding_lock
+    rootSys = scope.$system.root.$system
+
+    finishBinding = not rootSys.finishBinding_lock
     if finishBinding
-        scope.$system.root.$system.finishBinding_lock = true
+        rootSys.finishBinding_lock = true

     config = config or {}

     process scope, element, config

     if finishBinding
-        scope.$system.root.$system.finishBinding_lock = false
-        lst = scope.$system.root.$system.finishBinding_callbacks.slice()
-        scope.$system.root.$system.finishBinding_callbacks.length = 0
+        rootSys.finishBinding_lock = false
+        lst = rootSys.finishBinding_callbacks.slice()
+        rootSys.finishBinding_callbacks.length = 0
         for cb in lst
             cb()
     null


remove need for env.ENGINE_CANVAS_ERROR

remove need for env.ENGINE_CANVAS_ERROR

--- src/greenhorn.coffee
+++ src/greenhorn.coffee
@@ -194,7 +194,7 @@
         @set "leftPanel", "innerHTML", env.ENGINE_LEFT_PANEL
         @set "rightPanel", "innerHTML", env.ENGINE_RIGHT_PANEL
         @set "bottomPanel", "innerHTML", env.ENGINE_BOTTOM_PANEL
-        @set "canvas", "innerHTML", env.ENGINE_CANVAS_ERROR
+        @set "canvas", "innerHTML", "your browser does not support the <canvas> tag"

         #start running _masterUpdate at env.FRAME_RATE frames/sec
         _masterID = setInterval _masterUpdate, Math.ceil 1000 / env.FRAME_RATE


rainbow bugfix

rainbow bugfix

Former-commit-id: 9e6443dd180c9e4fa4ecbab2c1b8558cbbfa5668 [formerly 3f8c28f203a21551d605741ee41a14bdf6f2e605] [formerly 4e724e102a815e885ae375f0d249b93a4c80f367 [formerly ef738de1ba669bf68c0b657a1cb496e63efc52fd]] [formerly a15dd50cc37e7e8b80ded352e651671d26de793b [formerly da442ad69b7ac1c53537fbee6a964d8f2f6be122] [formerly ad557091420a65323b34b0e74a71fade65d2aae6 [formerly ad557091420a65323b34b0e74a71fade65d2aae6 [formerly 9c60344bc096b21b7779b164ad5aea7a9f07cd0a]]]]
Former-commit-id: 5149f26d8888a124cc913ddb3c9e02fffe6c3675 [formerly 894efe848528b986ab7e3f63b1cf1683cd09a453] [formerly 102376c564093321930b1965dfae1745c96346a1 [formerly 025783a388eff681d737d84c2d3f68eb337c6db0]]
Former-commit-id: bcb489b8242d2f92217408b829016ff7ac747eb9 [formerly 915c3afca8a4500b5252f7ef07464ef5b77ff197]
Former-commit-id: 42bfed05a64f56a4348dc05c31121ca9c102b24e
Former-commit-id: f79360527cbe317421bcbde899f4ec2fb6031c0f

--- @client/state_dash.coffee
+++ @client/state_dash.coffee
@@ -65,8 +65,8 @@
           return key
       return null

-    best_guess = first_key(search_results.key_matches) or \
-                 first_key(search_results.data_matches)
+    dash.curr_obj = first_key(search_results.key_matches) or \
+                    first_key(search_results.data_matches)

     DIV className: 'state_dash',
       STYLE null,
@@ -109,7 +109,7 @@

       # Render the object
       PRE className: 'right', ref: 'json_preview',
-        JSON.stringify(arest.cache[best_guess], undefined, 3)
+        JSON.stringify(arest.cache[dash.curr_obj], undefined, 3)

   # Other methods
   search_results: () ->
@@ -143,9 +143,10 @@
     true

   componentDidUpdate: () ->
+    dash = fetch('state_dash')
     el = @refs.json_preview?.getDOMNode()
-    if el?.innerHTML.length < 10000
-      el.innerHTML = rainbows(el.innerHTML)
+    if el?.innerHTML.length < 50000
+      el.innerHTML = rainbows(JSON.stringify(arest.cache[dash.curr_obj], undefined, 3))
 reset_selection = () ->
   dash = fetch('state_dash')
   dash.selected = {owner: null, name: null, number: null}


spec: Fix flags in fs.open

spec: Fix flags in fs.open

--- spec/asar-spec.coffee
+++ spec/asar-spec.coffee
@@ -328,7 +328,7 @@

       it 'throws ENOENT error when can not find file', (done) ->
         p = path.join fixtures, 'asar', 'a.asar', 'not-exist'
-        fs.open p, (err, stats) ->
+        fs.open p, 'r', (err, stats) ->
           assert.equal err.code, 'ENOENT'
           done()


refactor: ignore long test for Travis.

refactor: ignore long test for Travis.

--- spec/cube.spec.coffee
+++ spec/cube.spec.coffee
@@ -47,7 +47,8 @@
     moves = Cube.inverse "F B' R"
     expect(moves).toBe "R' B F'"

-  it 'should solve a solved cube :) ', ->
+  # ignore because Travis is slow
+  xit 'should solve a solved cube :) ', ->
     Cube.initSolver()
     cube = new Cube
     expect(cube.solve()).toBe "R L U2 R L F2 R2 U2 R2 F2 R2 U2 F2 L2"


Add spec for #481.

Add spec for #481.

--- spec/node-spec.coffee
+++ spec/node-spec.coffee
@@ -3,6 +3,7 @@
 fs            = require 'fs'
 path          = require 'path'
 os            = require 'os'
+remote        = require 'remote'

 describe 'node feature', ->
   fixtures = path.join __dirname, 'fixtures'
@@ -33,7 +34,7 @@
         child.send 'message'

       it 'works in browser process', (done) ->
-        fork = require('remote').require('child_process').fork
+        fork = remote.require('child_process').fork
         child = fork path.join(fixtures, 'module', 'ping.js')
         child.on 'message', (msg) ->
           assert.equal msg, 'message'
@@ -63,6 +64,10 @@
           done()
         fs.readFile __filename, ->
           throw error
+
+    describe 'setTimeout called under Chromium event loop in browser process', ->
+      it 'can be scheduled in time', (done) ->
+        remote.getGlobal('setTimeout')(done, 0)

   describe 'message loop', ->
     describe 'process.nextTick', ->


contextual w/ left-handed ::

contextual w/ left-handed ::

--- spec/preparser.coffee
+++ spec/preparser.coffee
@@ -219,6 +219,7 @@
       """
         selector {
             width: == ::[intrinsic-width];
+            width : == ::[intrinsic-width];
             ::[width] == ::[intrinsic-width];
         }
       """,
@@ -227,6 +228,7 @@
           type:'ruleset'
           selectors: ['selector']
           rules: [
+            {type:'constraint', cssText:'::[width] == ::[intrinsic-width];'}
             {type:'constraint', cssText:'::[width] == ::[intrinsic-width];'}
             {type:'constraint', cssText:'::[width] == ::[intrinsic-width];'}
           ]


:memo: Convert Pane docs

:memo: Convert Pane docs

--- src/pane.coffee
+++ src/pane.coffee
@@ -8,6 +8,40 @@
 # Public: A container for multiple items, one of which is *active* at a given
 # time. With the default packages, a tab is displayed for each item and the
 # active item's view is displayed.
+#
+# ## Events
+# ### activated
+#
+# Extended: Emit when this pane as been activated
+#
+# ### item-added
+#
+# Extended: Emit when an item was added to the pane
+#
+# * `item` The pane item that has been added
+# * `index` {Number} Index in the pane
+#
+# ### before-item-destroyed
+#
+# Extended: Emit before the item is destroyed
+#
+# * `item` The pane item that will be destoryed
+#
+# ### item-removed
+#
+# Extended: Emit when the item was removed from the pane
+#
+# * `item` The pane item that was removed
+# * `index` {Number} Index in the pane
+# * `destroying` {Boolean} `true` when the item is being removed because of destruction
+#
+# ### item-moved
+#
+# Extended: Emit when an item was moved within the pane
+#
+# * `item` The pane item that was moved
+# * `newIndex` {Number} Index that the item was moved to
+#
 module.exports =
 class Pane extends Model
   atom.deserializers.add(this)
@@ -130,9 +164,9 @@

   # Public: Adds the item to the pane.
   #
-  # item - The item to add. It can be a model with an associated view or a view.
-  # index - An optional index at which to add the item. If omitted, the item is
-  #         added after the current active item.
+  # * `item` The item to add. It can be a model with an associated view or a view.
+  # * `index` (optional) {Number} at which to add the item. If omitted, the item is
+  #   added after the current active item.
   #
   # Returns the added item
   addItem: (item, index=@getActiveItemIndex() + 1) ->
@@ -145,11 +179,11 @@

   # Public: Adds the given items to the pane.
   #
-  # items - An {Array} of items to add. Items can be models with associated
-  #         views or views. Any items that are already present in items will
-  #         not be added.
-  # index - An optional index at which to add the item. If omitted, the item is
-  #         added after the current active item.
+  # * `items` An {Array} of items to add. Items can be models with associated
+  #   views or views. Any items that are already present in items will
+  #   not be added.
+  # * `index` (optional) {Number} index at which to add the item. If omitted, the item is
+  #   added after the current active item.
   #
   # Returns an {Array} of the added items
   addItems: (items, index=@getActiveItemIndex() + 1) ->
@@ -246,9 +280,8 @@

   # Public: Saves the specified item.
   #
-  # item - The item to save.
-  # nextAction - An optional function which will be called after the item is
-  #              saved.
+  # * `item` The item to save.
+  # * `nextAction` (optional) {Function} which will be called after the item is saved.
   saveItem: (item, nextAction) ->
     if item?.getUri?()
       item.save?()
@@ -258,9 +291,8 @@

   # Public: Saves the given item at a prompted-for location.
   #
-  # item - The item to save.
-  # nextAction - An optional function which will be called after the item is
-  #              saved.
+  # * `item` The item to save.
+  # * `nextAction` (optional) {Function} which will be called after the item is saved.
   saveItemAs: (item, nextAction) ->
     return unless item?.saveAs?

@@ -294,8 +326,8 @@

   # Public: Creates a new pane to the left of the receiver.
   #
-  # params - An object with keys:
-  #   :items - An optional array of items with which to construct the new pane.
+  # * `params` {Object} with keys
+  #   * `items` (optional) {Array} of items with which to construct the new pane.
   #
   # Returns the new {Pane}.
   splitLeft: (params) ->
@@ -303,8 +335,8 @@

   # Public: Creates a new pane to the right of the receiver.
   #
-  # params - An object with keys:
-  #   :items - An optional array of items with which to construct the new pane.
+  # * `params` {Object} with keys:
+  #   * `items` (optional) {Array} of items with which to construct the new pane.
   #
   # Returns the new {Pane}.
   splitRight: (params) ->
@@ -312,8 +344,8 @@

   # Public: Creates a new pane above the receiver.
   #
-  # params - An object with keys:
-  #   :items - An optional array of items with which to construct the new pane.
+  # * `params` {Object} with keys:
+  #   * `items` (optional) {Array} of items with which to construct the new pane.
   #
   # Returns the new {Pane}.
   splitUp: (params) ->
@@ -321,8 +353,8 @@

   # Public: Creates a new pane below the receiver.
   #
-  # params - An object with keys:
-  #   :items - An optional array of items with which to construct the new pane.
+  # * `params` {Object} with keys:
+  #   * `items` (optional) {Array} of items with which to construct the new pane.
   #
   # Returns the new {Pane}.
   splitDown: (params) ->


Update specs to use new custom element names

--- spec/tabs-spec.coffee
+++ spec/tabs-spec.coffee
@@ -19,28 +19,28 @@
   describe ".activate()", ->
     it "appends a tab bar all existing and new panes", ->
       expect(workspaceElement.querySelectorAll('.pane').length).toBe 1
-      expect(workspaceElement.querySelectorAll('.pane > .tab-bar').length).toBe 1
+      expect(workspaceElement.querySelectorAll('.pane > tabs-bar').length).toBe 1

       pane = atom.workspace.getActivePane()
       pane.splitRight()

       expect(workspaceElement.querySelectorAll('.pane').length).toBe 2
-      expect(workspaceElement.querySelectorAll('.pane > .tab-bar').length).toBe 2
+      expect(workspaceElement.querySelectorAll('.pane > tabs-bar').length).toBe 2

   describe ".deactivate()", ->
     it "removes all tab bar views and stops adding them to new panes", ->
       pane = atom.workspace.getActivePane()
       pane.splitRight()
       expect(workspaceElement.querySelectorAll('.pane').length).toBe 2
-      expect(workspaceElement.querySelectorAll('.pane > .tab-bar').length).toBe 2
+      expect(workspaceElement.querySelectorAll('.pane > tabs-bar').length).toBe 2

       atom.packages.deactivatePackage('tabs')
       expect(workspaceElement.querySelectorAll('.pane').length).toBe 2
-      expect(workspaceElement.querySelectorAll('.pane > .tab-bar').length).toBe 0
+      expect(workspaceElement.querySelectorAll('.pane > tabs-bar').length).toBe 0

       pane.splitRight()
       expect(workspaceElement.querySelectorAll('.pane').length).toBe 3
-      expect(workspaceElement.querySelectorAll('.pane > .tab-bar').length).toBe 0
+      expect(workspaceElement.querySelectorAll('.pane > tabs-bar').length).toBe 0

 describe "TabBarView", ->
   [deserializerDisposable, item1, item2, editor1, pane, tabBar] = []
@@ -90,39 +90,39 @@
   describe ".initialize(pane)", ->
     it "creates a tab for each item on the tab bar's parent pane", ->
       expect(pane.getItems().length).toBe 3
-      expect(tabBar.find('.tab').length).toBe 3
-
-      expect(tabBar.find('.tab:eq(0) .title').text()).toBe item1.getTitle()
-      expect(tabBar.find('.tab:eq(0) .title')).not.toHaveAttr('data-name')
-      expect(tabBar.find('.tab:eq(0) .title')).not.toHaveAttr('data-path')
-
-      expect(tabBar.find('.tab:eq(1) .title').text()).toBe editor1.getTitle()
-      expect(tabBar.find('.tab:eq(1) .title')).toHaveAttr('data-name', path.basename(editor1.getPath()))
-      expect(tabBar.find('.tab:eq(1) .title')).toHaveAttr('data-path', editor1.getPath())
-
-      expect(tabBar.find('.tab:eq(2) .title').text()).toBe item2.getTitle()
-      expect(tabBar.find('.tab:eq(2) .title')).not.toHaveAttr('data-name')
-      expect(tabBar.find('.tab:eq(2) .title')).not.toHaveAttr('data-path')
+      expect(tabBar.find('tabs-tab').length).toBe 3
+
+      expect(tabBar.find('tabs-tab:eq(0) .title').text()).toBe item1.getTitle()
+      expect(tabBar.find('tabs-tab:eq(0) .title')).not.toHaveAttr('data-name')
+      expect(tabBar.find('tabs-tab:eq(0) .title')).not.toHaveAttr('data-path')
+
+      expect(tabBar.find('tabs-tab:eq(1) .title').text()).toBe editor1.getTitle()
+      expect(tabBar.find('tabs-tab:eq(1) .title')).toHaveAttr('data-name', path.basename(editor1.getPath()))
+      expect(tabBar.find('tabs-tab:eq(1) .title')).toHaveAttr('data-path', editor1.getPath())
+
+      expect(tabBar.find('tabs-tab:eq(2) .title').text()).toBe item2.getTitle()
+      expect(tabBar.find('tabs-tab:eq(2) .title')).not.toHaveAttr('data-name')
+      expect(tabBar.find('tabs-tab:eq(2) .title')).not.toHaveAttr('data-path')

     it "highlights the tab for the active pane item", ->
-      expect(tabBar.find('.tab:eq(2)')).toHaveClass 'active'
+      expect(tabBar.find('tabs-tab:eq(2)')).toHaveClass 'active'

   describe "when the active pane item changes", ->
     it "highlights the tab for the new active pane item", ->
       pane.activateItem(item1)
       expect(tabBar.find('.active').length).toBe 1
-      expect(tabBar.find('.tab:eq(0)')).toHaveClass 'active'
+      expect(tabBar.find('tabs-tab:eq(0)')).toHaveClass 'active'

       pane.activateItem(item2)
       expect(tabBar.find('.active').length).toBe 1
-      expect(tabBar.find('.tab:eq(2)')).toHaveClass 'active'
+      expect(tabBar.find('tabs-tab:eq(2)')).toHaveClass 'active'

   describe "when a new item is added to the pane", ->
     it "adds a tab for the new item at the same index as the item in the pane", ->
       pane.activateItem(item1)
       item3 = new TestView('Item 3')
       pane.activateItem(item3)
-      expect(tabBar.find('.tab').length).toBe 4
+      expect(tabBar.find('tabs-tab').length).toBe 4
       expect($(tabBar.tabAtIndex(1)).find('.title')).toHaveText 'Item 3'

     it "adds the 'modified' class to the new tab if the item is initially modified", ->
@@ -140,7 +140,7 @@
     it "removes the item's tab from the tab bar", ->
       pane.destroyItem(item2)
       expect(tabBar.getTabs().length).toBe 2
-      expect(tabBar.find('.tab:contains(Item 2)')).not.toExist()
+      expect(tabBar.find('tabs-tab:contains(Item 2)')).not.toExist()

     it "updates the titles of the remaining tabs", ->
       expect(tabBar.tabForItem(item2)).toHaveText 'Item 2'
@@ -173,7 +173,7 @@
       expect(pane.getItems().indexOf(editor1)).toBe -1
       expect(editor1.destroyed).toBeTruthy()
       expect(tabBar.getTabs().length).toBe 2
-      expect(tabBar.find('.tab:contains(sample.js)')).not.toExist()
+      expect(tabBar.find('tabs-tab:contains(sample.js)')).not.toExist()

   describe "when a tab's close icon is clicked", ->
     it "destroys the tab's item on the pane", ->
@@ -182,7 +182,7 @@
       expect(pane.getItems().indexOf(editor1)).toBe -1
       expect(editor1.destroyed).toBeTruthy()
       expect(tabBar.getTabs().length).toBe 2
-      expect(tabBar.find('.tab:contains(sample.js)')).not.toExist()
+      expect(tabBar.find('tabs-tab:contains(sample.js)')).not.toExist()

   describe "when a tab item's title changes", ->
     it "updates the title of the item's tab", ->
@@ -209,20 +209,20 @@

   describe "when an item has an icon defined", ->
     it "displays the icon on the tab", ->
-      expect(tabBar.find('.tab:eq(0) .title')).toHaveClass "icon"
-      expect(tabBar.find('.tab:eq(0) .title')).toHaveClass "icon-squirrel"
+      expect(tabBar.find('tabs-tab:eq(0) .title')).toHaveClass "icon"
+      expect(tabBar.find('tabs-tab:eq(0) .title')).toHaveClass "icon-squirrel"

     it "hides the icon from the tab if the icon is removed", ->
       item1.getIconName = null
       item1.emitIconChanged()
-      expect(tabBar.find('.tab:eq(0) .title')).not.toHaveClass "icon"
-      expect(tabBar.find('.tab:eq(0) .title')).not.toHaveClass "icon-squirrel"
+      expect(tabBar.find('tabs-tab:eq(0) .title')).not.toHaveClass "icon"
+      expect(tabBar.find('tabs-tab:eq(0) .title')).not.toHaveClass "icon-squirrel"

     it "updates the icon on the tab if the icon is changed", ->
       item1.getIconName = -> "zap"
       item1.emitIconChanged()
-      expect(tabBar.find('.tab:eq(0) .title')).toHaveClass "icon"
-      expect(tabBar.find('.tab:eq(0) .title')).toHaveClass "icon-zap"
+      expect(tabBar.find('tabs-tab:eq(0) .title')).toHaveClass "icon"
+      expect(tabBar.find('tabs-tab:eq(0) .title')).toHaveClass "icon-zap"

     describe "when showIcon is set to true in package settings", ->
       beforeEach ->
@@ -237,7 +237,7 @@
           tabBar.tabForItem(item1).updateIconVisibility.reset()

       it "doesn't hide the icon", ->
-        expect(tabBar.find('.tab:eq(0) .title')).not.toHaveClass "hide-icon"
+        expect(tabBar.find('tabs-tab:eq(0) .title')).not.toHaveClass "hide-icon"

       it "hides the icon from the tab when showIcon is changed to false", ->
         atom.config.set("tabs.showIcons", false)
@@ -246,7 +246,7 @@
           tabBar.tabForItem(item1).updateIconVisibility.callCount > 0

         runs ->
-          expect(tabBar.find('.tab:eq(0) .title')).toHaveClass "hide-icon"
+          expect(tabBar.find('tabs-tab:eq(0) .title')).toHaveClass "hide-icon"

     describe "when showIcon is set to false in package settings", ->
       beforeEach ->
@@ -261,7 +261,7 @@
           tabBar.tabForItem(item1).updateIconVisibility.reset()

       it "hides the icon", ->
-        expect(tabBar.find('.tab:eq(0) .title')).toHaveClass "hide-icon"
+        expect(tabBar.find('tabs-tab:eq(0) .title')).toHaveClass "hide-icon"

       it "shows the icon on the tab when showIcon is changed to true", ->
         atom.config.set("tabs.showIcons", true)
@@ -269,18 +269,18 @@
         waitsFor ->
           tabBar.tabForItem(item1).updateIconVisibility.callCount > 0

-        expect(tabBar.find('.tab:eq(0) .title')).not.toHaveClass "hide-icon"
+        expect(tabBar.find('tabs-tab:eq(0) .title')).not.toHaveClass "hide-icon"

   describe "when the item doesn't have an icon defined", ->
     it "doesn't display an icon on the tab", ->
-      expect(tabBar.find('.tab:eq(2) .title')).not.toHaveClass "icon"
-      expect(tabBar.find('.tab:eq(2) .title')).not.toHaveClass "icon-squirrel"
+      expect(tabBar.find('tabs-tab:eq(2) .title')).not.toHaveClass "icon"
+      expect(tabBar.find('tabs-tab:eq(2) .title')).not.toHaveClass "icon-squirrel"

     it "shows the icon on the tab if an icon is defined", ->
       item2.getIconName = -> "squirrel"
       item2.emitIconChanged()
-      expect(tabBar.find('.tab:eq(2) .title')).toHaveClass "icon"
-      expect(tabBar.find('.tab:eq(2) .title')).toHaveClass "icon-squirrel"
+      expect(tabBar.find('tabs-tab:eq(2) .title')).toHaveClass "icon"
+      expect(tabBar.find('tabs-tab:eq(2) .title')).toHaveClass "icon-squirrel"

   describe "when a tab item's modified status changes", ->
     it "adds or removes the 'modified' class to the tab based on the status", ->
@@ -316,7 +316,7 @@
         expect(pane.getItems().length).toBe 2
         expect(pane.getItems().indexOf(item2)).toBe -1
         expect(tabBar.getTabs().length).toBe 2
-        expect(tabBar.find('.tab:contains(Item 2)')).not.toExist()
+        expect(tabBar.find('tabs-tab:contains(Item 2)')).not.toExist()

     describe "when tabs:close-other-tabs is fired", ->
       it "closes all other tabs except the active tab", ->
@@ -324,8 +324,8 @@
         atom.commands.dispatch(tabBar.element, 'tabs:close-other-tabs')
         expect(pane.getItems().length).toBe 1
         expect(tabBar.getTabs().length).toBe 1
-        expect(tabBar.find('.tab:contains(sample.js)')).not.toExist()
-        expect(tabBar.find('.tab:contains(Item 2)')).toExist()
+        expect(tabBar.find('tabs-tab:contains(sample.js)')).not.toExist()
+        expect(tabBar.find('tabs-tab:contains(Item 2)')).toExist()

     describe "when tabs:close-tabs-to-right is fired", ->
       it "closes only the tabs to the right of the active tab", ->
@@ -334,8 +334,8 @@
         atom.commands.dispatch(tabBar.element, 'tabs:close-tabs-to-right')
         expect(pane.getItems().length).toBe 2
         expect(tabBar.getTabs().length).toBe 2
-        expect(tabBar.find('.tab:contains(Item 2)')).not.toExist()
-        expect(tabBar.find('.tab:contains(Item 1)')).toExist()
+        expect(tabBar.find('tabs-tab:contains(Item 2)')).not.toExist()
+        expect(tabBar.find('tabs-tab:contains(Item 1)')).toExist()

     describe "when tabs:close-all-tabs is fired", ->
       it "closes all the tabs", ->


Use IMPROMPTU_DIR to determine location of prompt file.

--- src/impromptu.coffee
+++ src/impromptu.coffee
@@ -8,7 +8,7 @@

 class Impromptu
   @VERSION: npmConfig.version
-  @CONFIG_DIR: "#{process.env.HOME}/.impromptu"
+  @CONFIG_DIR: process.env.IMPROMPTU_DIR || "#{process.env.HOME}/.impromptu"

   constructor: (@options = {}) ->
     @path =


disabled doubling of ' in session

disabled doubling of ' in session

--- client/session.coffee
+++ client/session.coffee
@@ -19,8 +19,8 @@
     return

   cm = CodeMirror ($e = $ e)[0],
-    autofocus: true, mode: '', matchBrackets: true, autoCloseBrackets: true, readOnly: true, keyMap: 'dyalog',
-    lineWrapping: !!localStorage.sessionLineWrapping
+    autofocus: true, mode: '', matchBrackets: true, autoCloseBrackets: {pairs: '()[]{}', explode: '{}'}
+    readOnly: true, keyMap: 'dyalog', lineWrapping: !!localStorage.sessionLineWrapping
     extraKeys: Tab: -> c = cm.getCursor(); opts.autocomplete? cm.getLine(c.line), c.ch

   cm.dyalogCommands =


Throw `Error` objects

Throw `Error` objects

--- src/pool.coffee
+++ src/pool.coffee
@@ -36,7 +36,7 @@
   @description Remove an object from the recycle pool
   ###
   remove: (objectOrIndex) ->
-    throw 'Must specify an object/index to remove' if objectOrIndex is undefined
+    throw new Error('Must specify an object/index to remove') if objectOrIndex is undefined

     index = if typeof objectOrIndex != 'number' then @children.indexOf objectOrIndex else objectOrIndex
     return if index is -1
@@ -69,7 +69,7 @@
       @children[@length - 1].reset() if typeof @children[@length - 1].reset == 'function'
       return @children[@length - 1]

-    throw 'Pools need a factory function' if typeof @factory != 'function'
+    throw new Error('Pools need a factory function') if typeof @factory != 'function'

     @children.push @factory()
     @length += 1


Graph: Also emit before state on change{Node,Edge,*Port,Properties}

This is required to have invertible operations in the graph journal.

--- src/lib/Graph.coffee
+++ src/lib/Graph.coffee
@@ -12,6 +12,30 @@
 # On browser we use Component's EventEmitter implementation
 else
   EventEmitter = require 'emitter'
+
+# From CS cookbook
+clone = (obj) ->
+  if not obj? or typeof obj isnt 'object'
+    return obj
+
+  if obj instanceof Date
+    return new Date(obj.getTime())
+
+  if obj instanceof RegExp
+    flags = ''
+    flags += 'g' if obj.global?
+    flags += 'i' if obj.ignoreCase?
+    flags += 'm' if obj.multiline?
+    flags += 'y' if obj.sticky?
+    return new RegExp(obj.source, flags)
+
+  newInstance = new obj.constructor()
+
+  for key of obj
+    newInstance[key] = clone obj[key]
+
+  return newInstance
+

 # This class represents an abstract NoFlo graph containing nodes
 # connected to each other with edges.
@@ -86,9 +110,10 @@
   # This method allows changing properties of the graph.
   setProperties: (properties) ->
     @checkTransactionStart()
+    before = clone @properties
     for item, val of properties
       @properties[item] = val
-    @emit 'changeProperties', @properties
+    @emit 'changeProperties', @properties, before
     @checkTransactionEnd()

   # ## Exporting a port from subgraph
@@ -161,10 +186,11 @@
     return unless @inports[publicPort]

     @checkTransactionStart()
+    before = clone @inports[publicPort].metadata
     @inports[publicPort].metadata = {} unless @inports[publicPort].metadata
     for item, val of metadata
       @inports[publicPort].metadata[item] = val
-    @emit 'changeInport', @inports[publicPort]
+    @emit 'changeInport', @inports[publicPort], before
     @checkTransactionEnd()

   addOutport: (publicPort, nodeKey, portKey, metadata) ->
@@ -206,10 +232,11 @@
     return unless @outports[publicPort]

     @checkTransactionStart()
+    before = clone @outports[publicPort].metadata
     @outports[publicPort].metadata = {} unless @outports[publicPort].metadata
     for item, val of metadata
       @outports[publicPort].metadata[item] = val
-    @emit 'changeOutport', @outports[publicPort]
+    @emit 'changeOutport', @outports[publicPort], before
     @checkTransactionEnd()

   # ## Grouping nodes in a graph
@@ -242,9 +269,10 @@
     for group in @groups
       continue unless group
       continue unless group.name is groupName
+      before = clone group.metadata
       for item, val of metadata
         group.metadata[item] = val
-      @emit 'changeGroup', group
+      @emit 'changeGroup', group, before
     @checkTransactionEnd()

   # ## Adding a node to the graph
@@ -385,12 +413,13 @@

     @checkTransactionStart()

+    before = clone node.metadata
     node.metadata = {} unless node.metadata

     for item, val of metadata
       node.metadata[item] = val

-    @emit 'changeNode', node
+    @emit 'changeNode', node, before
     @checkTransactionEnd()

   # ## Connecting nodes
@@ -479,12 +508,13 @@
     return unless edge

     @checkTransactionStart()
+    before = clone edge.metadata
     edge.metadata = {} unless edge.metadata

     for item, val of metadata
       edge.metadata[item] = val

-    @emit 'changeEdge', edge
+    @emit 'changeEdge', edge, before
     @checkTransactionEnd()

   # ## Adding Initial Information Packets


Remove testing TODO

Remove testing TODO

--- src/repl.coffee
+++ src/repl.coffee
@@ -21,7 +21,6 @@
     catch err
       cb err

-# TODO: how to test?
 addMultilineHandler = (repl) ->
   {rli, inputStream, outputStream} = repl


[grunt] use `gettext` instead `i18n` for Gettext instance

--- gruntfile.coffee
+++ gruntfile.coffee
@@ -1,10 +1,10 @@
 module.exports = (grunt) ->
   'use strict'

-  _       = require('lodash')
-  Gettext = require('node-gettext')
-  i18n    = new Gettext()
-  path    = require('path')
+  _           = require('lodash')
+  NodeGettext = require('node-gettext')
+  gettext     = new NodeGettext()
+  path        = require('path')
   # Track execution time
   require('time-grunt') grunt
   # Load grunt tasks automatically
@@ -111,9 +111,9 @@
       domain   = if domain == defaultDomain then locale else locale + ':' + domain
       messages = grunt.file.read(localesDir + '/' + locale + '/' + filepath, { encoding: null })

-      i18n.addTextdomain(domain, messages)
+      gettext.addTextdomain(domain, messages)

-  grunt.config.set 'i18n.gettext', i18n
+  grunt.config.set 'i18n.gettext', gettext
   grunt.config.set 'i18n.locales.list', localesList

   grunt.config.set 'data', require('./' + grunt.config('path.source.data'))(grunt)


Add isConfigured()

Add isConfigured()

--- src/Routes.coffee
+++ src/Routes.coffee
@@ -44,6 +44,8 @@

     @_isConfigured = true

+  isConfigured: -> @_isConfigured
+
   crudRoute: (collection, args) ->
     args = _.extend({}, args)


* [bug] the default value can not be applied.

--- src/load-data.coffee
+++ src/load-data.coffee
@@ -18,7 +18,7 @@
   return reject(new TypeError 'Missing Model') unless Model

   Promise.map data, (item)->
-    Model.upsert item
+    Model.create item
     .then (result)->
       if result
         delayed = []