'use strict'
var pick = require('lodash.pick')
var url = require('url')
var connect = require('pull-ws/client')

module.exports = function (params, cb) {
  params = pick(params, 'host', 'port', 'path', 'protocol')
  var address = {
    protocol: params.protocol,
    port: params.port,
    pathname: params.path,
    slashes: true
  }
  if (params.path == null) {
    address.hostname = params.host
    address.port = params.port
  } else {
    address = 'ws+unix://' + params.path
  }

  var c = connect(url.format(address), function (err) {
    if (err) return cb(err)
    c.type = 'connection'
    if (cb) {
      cb(null, c)
      cb = null
    }
  })
}

'use strict'
var pick = require('lodash.pick')
var url = require('url')
var connect = require('pull-ws/client')
var util = require('../util')

module.exports = function (params, cb) {
  params = pick(params, 'host', 'port', 'path', 'protocol')
  var address = {
    protocol: params.protocol,
    port: params.port,
    pathname: params.path,
    slashes: true
  }
  if (params.path == null) {
    if(util.isWindows()){
      if(params.host ==='0.0.0.0')params.host='localhost'
      if(params.host ==='::')params.host='localhost'
    }
    address.hostname = params.host
    address.port = params.port
  } else {
    address = 'ws+unix://' + params.path
  }
  var c = connect(url.format(address), function (err) {
    if (err) return cb(err)
    c.type = 'connection'
    if (cb) {
      cb(null, c)
      cb = null
    }
  })
}

Fix for EADDRNOTAVAIL 0.0.0.0:5980 on windows ws


'use strict';

(function () {
    var app = angular.module('dailyMummApp',
        ['ngCookies',
            'ngResource',
            'ngSanitize',
            'ui.router',
            'angular-rating',
            'pascalprecht.translate']);

    // app.value("apiServer", "https://dailymumm.herokuapp.com");
    app.value("apiServer", "http://localhost");

    app.run(function ($rootScope, $state) { // just to hide the top bar in home public layout
        $rootScope.$on('$stateChangeSuccess',
            function (event, toState, toParams, fromState, fromParams) {
                if (toState.name == 'home') {
                    $rootScope.hideit = true;
                } else {
                    $rootScope.hideit = false;
                }
            });
    });
})();

'use strict';

(function () {
    var app = angular.module('dailyMummApp',
        ['ngCookies',
            'ngResource',
            'ngSanitize',
            'ui.router',
            'angular-rating',
            'pascalprecht.translate']);

    // app.value("apiServer", "https://dailymumm.herokuapp.com");
    // app.value("apiServer", "http://localhost");

    app.value("apiServer", location.origin);

    app.run(function ($rootScope, $state) { // just to hide the top bar in home public layout
        $rootScope.$on('$stateChangeSuccess',
            function (event, toState, toParams, fromState, fromParams) {
                if (toState.name == 'home') {
                    $rootScope.hideit = true;
                } else {
                    $rootScope.hideit = false;
                }
            });
    });
})();

Fix domain issue between local and production


function PickerView() {
	var self = Ti.UI.createWindow({});

	// create the picker
	var picker = Ti.UI.createPicker({});
	self.add(picker);

	// create fruit column
	var fruitColumn = Ti.UI.createPickerColumn();
	fruitColumn.addRow(Ti.UI.createPickerRow({
		title : 'Bananas',
		_value : 'Yellow'
	}));
	fruitColumn.addRow(Ti.UI.createPickerRow({
		title: 'Strawberries',
		_value : 'Red'
	}));
	fruitColumn.addRow(Ti.UI.createPickerRow({
		title: 'Oranges',
		_value : 'Orange'
	}));
	fruitColumn.addRow(Ti.UI.createPickerRow({
		title: 'Grapes',
		_value : 'Purple'
	}));

	// create color column
	var colorColumn = Ti.UI.createPickerColumn();
	colorColumn.addRow(Ti.UI.createPickerRow({
		backgroundColor : 'Yellow',
		width : 25,
		height : 25
	}));
	colorColumn.addRow(Ti.UI.createPickerRow({
		backgroundColor : 'Red',
		width : 25,
		height : 25
	}));
	colorColumn.addRow(Ti.UI.createPickerRow({
		backgroundColor : 'Orange',
		width : 25,
		height : 25
	}));
	colorColumn.addRow(Ti.UI.createPickerRow({
		backgroundColor : 'Purple',
		width : 25,
		height : 25
	}));

	var columns = [fruitColumn, colorColumn];

	picker.add(columns);
	picker.selectionIndicator = true;

	var selectedColor = null,
	selectedFruit = null;
	picker.addEventListener('change', function(e) {
		var columnIndex = e.columnIndex;
		var row = e.row;

		if (columnIndex === 0) {
			selectedFruit = row;
		} else {
			selectedColor = row;
		}

		if (selectedFruit && selectedColor) {
			if (selectedFruit._value === selectedColor.getBackgroundColor()) {
				alert('Match!');
			}
		}
	});

	return self;
};

module.exports = PickerView;

Complete lesson 10 - picker view


import md from 'components/md'

const Refs = () => md`
  ## Refs

  Passing a \`ref\` prop to a styled component will give you an instance of
  the \`StyledComponent\` wrapper, but not to the underlying DOM node.
  This is due to how refs work.
  It's not possible to call DOM methods, like \`focus\`, on our wrappers directly.

  To get a ref to the actual, wrapped DOM node, pass the callback to the \`innerRef\` prop instead.

  > We don't support string refs (i.e. \`innerRef="node"\`), since they're already deprecated in React.

  This example uses \`innerRef\` to save a ref to the styled input and focuses it once the user
  hovers over it.

  \`\`\`react
  const Input = styled.input\`
    padding: 0.5em;
    margin: 0.5em;
    color: palevioletred;
    background: papayawhip;
    border: none;
    border-radius: 3px;
  \`;

  const Form = () => (
    <Input
      placeholder="Hover here..."
      innerRef={x => this.input = x}
      onMouseEnter={() => this.input.focus()}
    />
  );

  render(
    <Form />
  );
  \`\`\`
`

export default Refs

import md from 'components/md'

const Refs = () => md`
  ## Refs

  Passing a \`ref\` prop to a styled component will give you an instance of
  the \`StyledComponent\` wrapper, but not to the underlying DOM node.
  This is due to how refs work.
  It's not possible to call DOM methods, like \`focus\`, on our wrappers directly.

  To get a ref to the actual, wrapped DOM node, pass the callback to the \`innerRef\` prop instead.

  > We don't support string refs (i.e. \`innerRef="node"\`), since they're already deprecated in React.

  This example uses \`innerRef\` to save a ref to the styled input and focuses it once the user
  hovers over it.

  \`\`\`react
  const Input = styled.input\`
    padding: 0.5em;
    margin: 0.5em;
    color: palevioletred;
    background: papayawhip;
    border: none;
    border-radius: 3px;
  \`;

  class Form extends React.Component {
    render() {
      return (
        <Input
          placeholder="Hover here..."
          innerRef={x => this.input = x}
          onMouseEnter={() => this.input.focus()}
        />
      );
    }
  }

  render(
    <Form />
  );
  \`\`\`
`

export default Refs

Update docs ref example so as to not promote bad practices


'use strict';

import React, { Component } from 'react';
import {
  View,
  ListView,
  Text,
} from 'react-native';

import globalStyles from '../config/globalStyles';
import { SearchBar, List, ListItem } from 'react-native-elements';

import { customersData } from '../scenes/Home';
import { customersDataNames } from '../scenes/Home';

export default class CustomerSearch extends Component {
  constructor(props) {
    super(props);
    const ds = new ListView.DataSource({rowHasChanged: (r1, r2) => r1 !== r2});
    this.state = {
      dataSource: ds.cloneWithRows(customersData),
    };
  }

  renderRow(rowData, sectionID) {
    return (
      <ListItem
        key={sectionID}
        title={rowData.lastname + ', ' + rowData.firstname}
        subtitle={rowData.email}
      />
    )
  }

  render() {
    return (
      <View style={{paddingTop: 64, flex: 1}}>
        <SearchBar
          onChangeText={null}
          placeholder="Placeholder 1"
          lightTheme
        />

        <List containerStyle={{flex: 1}}>
          <ListView
            automaticallyAdjustContentInsets={false} // otherwise additional 64px top-space (default)
            dataSource={this.state.dataSource}
            renderRow={this.renderRow}
          />
        </List>
      </View>
    );
  }
}

'use strict';

import React, { Component } from 'react';
import {
  View,
  ListView,
  Text,
} from 'react-native';

import globalStyles from '../config/globalStyles';
import { SearchBar, List, ListItem } from 'react-native-elements';

import { customersData } from '../scenes/Home';
import { customersDataNames } from '../scenes/Home';

export default class CustomerSearch extends Component {
  constructor(props) {
    super(props);
    const ds = new ListView.DataSource({rowHasChanged: (r1, r2) => r1 !== r2});
    this.state = {
      dataSource: ds.cloneWithRows(customersData),
    };
  }

  renderRow(rowData, sectionID) {
    return (
      <ListItem
        key={sectionID}
        title={rowData.lastname + ', ' + rowData.firstname}
        subtitle={rowData.email}
      />
    )
  }

  render() {
    return (
      <View style={{paddingTop: 64, flex: 1}}>
        <SearchBar
          onChangeText={null}
          placeholder="Placeholder 1"
          lightTheme
        />

        <ListView
          automaticallyAdjustContentInsets={false} // otherwise additional 64px top-space (default)
          dataSource={this.state.dataSource}
          renderRow={this.renderRow}
        />
      </View>
    );
  }
}

Fix unnecessary top space (this time produced by (redundant) <List/> from RN Elements)


'use strict';

// Starting date
global.startedAt = Date.now();

/**
 * Instantiate and expose a Strapi singleton
 * (maintains legacy support).
 */

module.exports = function(global) {
  try {
    return global.strapi = require('./Strapi'); // Strapi instance instanciated
  } catch (error) {
    console.log(error); // eslint-disable-line no-console
  }
}.call(this, global);

'use strict';

// Starting date
global.startedAt = Date.now();

const foo = "bar";

/**
 * Instantiate and expose a Strapi singleton
 * (maintains legacy support).
 */
module.exports = function(global) {
  try {
    return global.strapi = require('./Strapi'); // Strapi instance instanciated
  } catch (error) {
    console.log(error); // eslint-disable-line no-console
  }
}.call(this, global);

Introduce a linter error for testing CI


MeteorCamera.getPicture = function (options, callback) {
  // if options are not passed
  if (! callback) {
    callback = options;
    options = {};
  }

  var success = function (data) {
    callback(null, "data:image/jpeg;base64," + data);
  };

  var failure = function (error) {
    callback(new Meteor.Error("cordovaError", error));
  };

  navigator.camera.getPicture(success, failure,
    _.extend(options, {
      quality: options.quality || 49,
      targetWidth: options.width || 640,
      targetHeight: options.width || 480,
      destinationType: Camera.DestinationType.DATA_URL
    })
  );
};

MeteorCamera.getPicture = function (options, callback) {
  // if options are not passed
  if (! callback) {
    callback = options;
    options = {};
  }

  var success = function (data) {
    callback(null, "data:image/jpeg;base64," + data);
  };

  var failure = function (error) {
    callback(new Meteor.Error("cordovaError", error));
  };

  navigator.camera.getPicture(success, failure,
    _.extend(options, {
      quality: options.quality || 49,
      targetWidth: options.width || 640,
      targetHeight: options.height || 480,
      destinationType: Camera.DestinationType.DATA_URL
    })
  );
};

Fix setting targetHeight option for getPicture.


const jwt         = require('jsonwebtoken')
const config      = require('config')
const CurrentUser = require('./../../services/current-user')
const Messages    = require('./../../messages')

const isBypassed = (req) => {
  const bypassed = [
    {
      'path':   '/auth',
      'method': 'POST'
    },
    {
      'path':   '/users',
      'method': 'POST'
    }
  ]

  return bypassed.filter((item) => {
    return req.path === item.path && req.method === item.method
  }).lengthw
}

const authMiddleware = (req, res, next) => {
  if (isBypassed(req)) { return next() }

  const token = req.header('Authorization').replace('Bearer ', '')

  jwt.verify(token, config.get('secret'), (error, decoded) => {
    if (error || !decoded) {
      console.error('[AUTHENTICATION] Invalid token')

      return res.status(401).send({ 'errors': { 'token': Messages.errors.token } })
    }

    console.log('[AUTHENTICATION] Authentication success')

    CurrentUser.fromToken(decoded)
      .then((user) => {
        req.currentUser = user
        next()
      })
      .catch((error) => {
        res.status(401).send(error)
      })
  })
}

module.exports = authMiddleware

const jwt         = require('jsonwebtoken')
const config      = require('config')
const CurrentUser = require('./../../services/current-user')
const Messages    = require('./../../messages')

const isBypassed = (req) => {
  const bypassed = [
    { 'path': '/auth',  'method': 'POST' },
    { 'path': '/users', 'method': 'POST' }
  ]

  return bypassed.filter((item) => {
    return req.path === item.path && req.method === item.method
  }).length
}

const authMiddleware = (req, res, next) => {
  if (isBypassed(req)) { return next() }

  const token = req.header('Authorization').replace('Bearer ', '')

  jwt.verify(token, config.get('secret'), (error, decoded) => {
    if (error || !decoded) {
      console.error('[AUTHENTICATION] Invalid token')

      return res.status(401).send({ 'errors': {
        'token': { 'invalid': Messages.errors.token.invalid }
      }})
    }

    console.log('[AUTHENTICATION] Authentication success')

    CurrentUser.fromToken(decoded)
      .then((user) => {
        req.currentUser = user
        next()
      })
      .catch((error) => {
        res.status(401).send(error)
      })
  })
}

module.exports = authMiddleware

Fix typo on bypass function and standardize invalid token message


import React, { Component, PropTypes } from 'react'
import Block from './Block'

class EmbedBlock extends Component {

  static propTypes = {
    data: PropTypes.shape({
      url: PropTypes.string,
      service: PropTypes.string,
      id: PropTypes.string,
      thumbnailLargeUrl: PropTypes.string,
      thumbnailSmallUrl: PropTypes.string,
    }),
  }

  static defaultProps = {
    data: {},
  }

  render() {
    const { data } = this.props
    return (
      <Block
        { ...this.props }
        children={ <img src={ data.thumbnailLargeUrl } /> }
        ref="block"
      />
    )
  }

}

export default EmbedBlock

import React, { Component, PropTypes } from 'react'
import Block from './Block'

class EmbedBlock extends Component {

  static propTypes = {
    data: PropTypes.shape({
      url: PropTypes.string,
      service: PropTypes.string,
      id: PropTypes.string,
      thumbnailLargeUrl: PropTypes.string,
      thumbnailSmallUrl: PropTypes.string,
    }),
  }

  static defaultProps = {
    data: {},
  }

  componentDidUpdate() {
    if (window.embetter) {
      window.embetter.reloadPlayers()
    }
  }

  render() {
    const { data: { service, url, thumbnailLargeUrl, id } } = this.props
    const children = typeof window !== 'undefined' ?
      window.embetter.utils.playerHTML(
        window.embetter.services[service],
        url,
        thumbnailLargeUrl,
        id
      ) :
      null
    return (
      <Block
        { ...this.props }
        dangerouslySetInnerHTML={{ __html: children }}
        ref="block"
      />
    )
  }

}

export default EmbedBlock

Use the embetter preview in an embed block.


import { testAsync } from './testHelpers'
import setupTestApp from './setupTestApp'

testAsync('Add Reference: TODO do serious stuff here', async (t) => {
  let { app } = await setupTestApp(t)
  let articlePanel = app.find('.sc-article-panel')
  // open the metadata panel
  articlePanel.send('updateViewName', 'metadata')
  // open the add drop down
  let addDropDown = articlePanel.find('.sc-tool-dropdown.sm-add')
  addDropDown.find('button').click()
  // click on the add-reference button
  addDropDown.find('.sc-menu-item.sm-add-reference').click()
  // TODO: continue here
  t.ok(Boolean(res))
  t.end()
})

import { testAsync } from './testHelpers'
import setupTestApp from './setupTestApp'

testAsync('Add Reference: TODO do serious stuff here', async (t) => {
  let { app } = await setupTestApp(t)
  let articlePanel = app.find('.sc-article-panel')
  // open the metadata panel
  articlePanel.send('updateViewName', 'metadata')
  // open the add drop down
  let addDropDown = articlePanel.find('.sc-tool-dropdown.sm-add')
  addDropDown.find('button').click()
  // click on the add-reference button
  addDropDown.find('.sc-menu-item.sm-add-reference').click()
  // check if the modal with a workflow got opened
  let workflow = articlePanel.find('.se-workflow-modal')
  t.notNil(workflow, 'There should be a workflow in modal')
  t.end()
})

Check if modal got opened.


'use strict';

var fs = require('fs');
var expect = require('chai').expect;
var bitmapReader = require('../lib/bitmapReader');

describe('bitmapReader', function() {
  before(function() {
    // remove ./img/newbmp.bmp if it exists.
    try{
      fs.unlinkSync('./img/newbmp.bmp');
    } catch(err) {}
  });

  it('should', function() {

  });
});

'use strict';

var fs = require('fs');
var expect = require('chai').expect;
var bitmapReader = require('../lib/bitmapReader');

describe('bitmapReader', function() {
  before(function() {
    // remove ./img/newbmp.bmp if it exists.
    try{
      fs.unlinkSync('./img/newbmp.bmp');
    } catch(err) {}
  });

  after(function() {
    // remove ./img/newbmp.bmp if it exists.
    try{
      fs.unlinkSync('./img/newbmp.bmp');
    } catch(err) {}
  });

  it('should', function() {
    var original = fs.readFileSync('./img/test.bmp');
    bitmapReader();
    var newbmp = fs.readFileSync('./img/newbmp.bmp');

    expect(original).to.not.eql(newbmp);
  });
});

Add new test: new != old


exports.config = {
    user: process.env.BROWSERSTACK_USER,
    key: process.env.BROWSERSTACK_KEY,
    specs: [
        './webdriver-tests/*Spec.js'
    ],
    capabilities: [
        {
            browserName: 'chrome',
            'browserstack.local': true
        }
    ],
    waitforTimeout: 100000,
    framework: 'jasmine',
    reporter: 'dot',
    jasmineNodeOpts: {
        defaultTimeoutInterval: 300000
    }
};

exports.config = {
    user: process.env.BROWSERSTACK_USER,
    key: process.env.BROWSERSTACK_KEY,
    specs: [
        './webdriver-tests/*Spec.js'
    ],
    capabilities: [
        {
            platform: 'WIN8',
            browserName: 'chrome',
            'browserstack.local': true
        }
    ],
    waitforTimeout: 100000,
    framework: 'jasmine',
    reporter: 'dot',
    jasmineNodeOpts: {
        defaultTimeoutInterval: 300000
    }
};

Fix intermittent build errors caused by Windows XP


function DropdownCtrl($scope, $http, Data, pubsub) {
    $scope.data = Data;
    $scope.pubsub = pubsub;
    $scope.indices = [];
    $scope.types = [];

    $scope.pubsub.subscribe('HOST_CHANGED', function(newHost){
      $scope.data.host = newHost;
      $scope.loadMappings();
    });

    $scope.loadMappings = function(){
      var path = $scope.data.host + "/_mapping";
      $http.get(path).then(function(response){
          $scope.data.mapping = response.data;

          for (i in response.data){

              $scope.indices.push(i);
              $scope.types[i] = [];
              for (j in response.data[i]){

                  $scope.types[i].push(j);
              }
          }

          path = $scope.data.host + "/_aliases";
          $http.get(path).then(function(response){

            for (i in response.data){
              for (j in response.data[i].aliases) {
                $scope.indices.push(j);
                $scope.types[j] = $scope.types[i];
                $scope.data.mapping[j] = $scope.data.mapping[i];
              }
            }

            $scope.indices.sort();
            $scope.types.sort();
          });
      });
    };
    $scope.loadMappings();
}

function DropdownCtrl($scope, $http, Data, pubsub) {
    $scope.data = Data;
    $scope.pubsub = pubsub;
    $scope.indices = [];
    $scope.types = [];

    $scope.pubsub.subscribe('HOST_CHANGED', function(newHost){
      $scope.data.host = newHost;
      $scope.loadMappings();
    });

    $scope.loadMappings = function(){
      var path = $scope.data.host + "/_mapping";
      $http.get(path).then(function(response){
          if($scope.indices && $scope.indices.length > 0){
              $scope.indicies.length = 0;
          }
          $scope.data.mapping = response.data;

          for (i in response.data){

              $scope.indices.push(i);
              $scope.types[i] = [];
              for (j in response.data[i]){

                  $scope.types[i].push(j);
              }
          }

          path = $scope.data.host + "/_aliases";
          $http.get(path).then(function(response){

            for (i in response.data){
              for (j in response.data[i].aliases) {
                $scope.indices.push(j);
                $scope.types[j] = $scope.types[i];
                $scope.data.mapping[j] = $scope.data.mapping[i];
              }
            }

            $scope.indices.sort();
            $scope.types.sort();
          });
      });
    };
    $scope.loadMappings();
}

Clear indices list before reload


import Ember from 'ember';

const keyCodeToEventMap = {
  27: 'esc',
  37: 'leftArrow',
  38: 'upArrow',
  39: 'rightArrow',
  40: 'downArrow'
};

export default Ember.Mixin.create({
  mergedProperties: ['keyEvents'],
  keyEvents: {},

  setupKeyHandling: function() {
    this.$(document).on(`keyup.${this.get('elementId')}`, (event) => {
      var key = keyCodeToEventMap[event.keyCode];
      var keyEvent = this.get('keyEvents')[key];

      if (keyEvent) {
        keyEvent.call(this, event);
      }
    });
  }.on('didInsertElement'),

  tearDownKeyHandling: function() {
    this.$(document).off(`keyup.${this.get('elementId')}`);
  }.on('willDestroyElement')
});

import Ember from 'ember';

const keyToEventMap = {
  27: 'esc',
  37: 'leftArrow',
  38: 'upArrow',
  39: 'rightArrow',
  40: 'downArrow'
};

export default Ember.Mixin.create({
  mergedProperties: ['keyEvents'],
  keyEvents: {},

  setupKeyHandling: function() {
    this.$(document).on(`keyup.${this.get('elementId')}`, (event) => {
      var key = (keyToEventMap[event.keyCode]) ? keyToEventMap[event.keyCode] : event.keyCode;
      var keyEvent = this.get('keyEvents')[key];

      if (keyEvent) {
        keyEvent.call(this, event);
      }
    });
  }.on('didInsertElement'),

  tearDownKeyHandling: function() {
    this.$(document).off(`keyup.${this.get('elementId')}`);
  }.on('willDestroyElement')
});

Allow passing other key codes, not just the ones from the list


define([
  'backbone', 'moment',
  'connect/models/Story'
], function(Backbone, moment, Story) {

  'use strict';

  var Stories = Backbone.Collection.extend({
    model: Story,

    //url: window.gfw.config.GFW_API_HOST_V2 + '/user/stories',
    url: 'http://api-gateway.globalforestwatch.dev:8000/story/user/57736ba451b9ca1300e39430',

    comparator: function(subscription) {
      return -moment(subscription.get('created')).unix();
    },

    sync: function(method, model, options) {
      options || (options = {});

      if (!options.crossDomain) {
        options.crossDomain = true;
      }

      if (!options.xhrFields) {
        options.xhrFields = {withCredentials:true};
      }

      return Backbone.sync.call(this, method, model, options);
    },

    parse: function(response) {
      return response.data;
    }

  });

  return Stories;

});

define([
  'backbone', 'moment',
  'connect/models/Story'
], function(Backbone, moment, Story) {

  'use strict';

  var Stories = Backbone.Collection.extend({
    model: Story,

    url: window.gfw.config.GFW_API_HOST_NEW_API + '/user/stories',

    comparator: function(subscription) {
      return -moment(subscription.get('created')).unix();
    },

    sync: function(method, model, options) {
      options || (options = {});

      if (!options.crossDomain) {
        options.crossDomain = true;
      }

      if (!options.xhrFields) {
        options.xhrFields = {withCredentials:true};
      }

      return Backbone.sync.call(this, method, model, options);
    },

    parse: function(response) {
      return response.data;
    }

  });

  return Stories;

});

Correct URL for user stories collection


var autocompleter = {
  look_up: function(value) {
    if(value == "") {
      this.clear_popup();
    }
    else {
      var self = this;
      $.ajax("/autocomplete", {
        data: { "query":value }
      }).done(function(data) {
        if(data.length > 0) {
          var html = "", $input = $('#input');
          for(var i=0; i<data.length; i++) {
            html += self.format_autocomplete_entry(data[i]);
          };
          html += '
[Add new command]
'
          $("#autocomplete").html(html).attr('top', $input.attr('bottom')).show();
        }
        else {
          self.clear_popup();
        }
      }).fail(function() {
        console.log("AJAX fail:", arguments);
      });
    }
  },

  clear_popup: function() {
    $("#autocomplete").hide();
  },

  format_autocomplete_entry: function(entry) {
    return '
'+entry.function.replace(/\{([^}]+)\}/g, '$1')+'
';
  }

};

var autocompleter = {
  look_up: function(value) {
    if(value == "") {
      this.clear_popup();
    }
    else {
      var self = this;
      $.ajax("/autocomplete", {
        data: { "query":value }
      }).done(function(data) {
        if(data.length > 0) {
          var container = $("#autocomplete").empty();
          var $input = $('#input');
          for(var i=0; i<data.length; i++) {
            container.append(self.format_autocomplete_entry(data[i]));
          };
          var new_cmd_el = document.createElement("li");
          new_cmd_el.className = "item new"
          new_cmd_el.addEventListener('click', function(e) {
            statement_list.new();
          });
          new_cmd_el.title="Create a new command and add it to this test";
          new_cmd_el.innerText = "[Add new command]";
          container.append(new_cmd_el);
          container.attr('top', $input.attr('bottom')).show();
        }
        else {
          self.clear_popup();
        }
      }).fail(function() {
        console.log("AJAX fail:", arguments);
      });
    }
  },

  clear_popup: function() {
    $("#autocomplete").hide();
  },

  format_autocomplete_entry: function(entry) {
    var el = document.createElement("li");
    el.className = "item";
    el.addEventListener('click', function(e) {
      statement_list.add(this.innerText);
    });
    el.title = entry.examples.slice(0,3).join("
");
    el.innerText = entry.function;
    el.innerHTML = el.innerHTML.replace(/\{([^}]+)\}/g, '$1');

    return el;
  }

};

Refactor to use DOM API instead of embedded HTML strings


const { TerminalReporter } = require('jasmine-tagged')

class JasmineListReporter extends TerminalReporter {
  fullDescription (spec) {
    let fullDescription = spec.description
    let currentSuite = spec.suite
    while (currentSuite) {
      fullDescription = currentSuite.description + ' > ' + fullDescription
      currentSuite = currentSuite.parentSuite
    }
    return fullDescription
  }

  reportSpecStarting (spec) {
    this.print_(this.fullDescription(spec) + ' ')
  }

  reportSpecResults (spec) {
    const result = spec.results()
    if (result.skipped) {
      return
    }

    let msg = ''
    if (result.passed()) {
      msg = this.stringWithColor_('[pass]', this.color_.pass())
    } else {
      msg = this.stringWithColor_('[FAIL]', this.color_.fail())
      this.addFailureToFailures_(spec)
    }
    this.printLine_(msg)
  }
}

module.exports = { JasmineListReporter }

const { TerminalReporter } = require('jasmine-tagged')

class JasmineListReporter extends TerminalReporter {
  fullDescription (spec) {
    let fullDescription = 'it ' + spec.description
    let currentSuite = spec.suite
    while (currentSuite) {
      fullDescription = currentSuite.description + ' > ' + fullDescription
      currentSuite = currentSuite.parentSuite
    }
    return fullDescription
  }

  reportSpecStarting (spec) {
    this.print_(this.fullDescription(spec) + ' ')
  }

  reportSpecResults (spec) {
    const result = spec.results()
    if (result.skipped) {
      return
    }

    let msg = ''
    if (result.passed()) {
      msg = this.stringWithColor_('[pass]', this.color_.pass())
    } else {
      msg = this.stringWithColor_('[FAIL]', this.color_.fail())
      this.addFailureToFailures_(spec)
    }
    this.printLine_(msg)
  }
}

module.exports = { JasmineListReporter }

Add 'it' in front of spec descriptions


var mongoose = require( "mongoose" );

var Schema = mongoose.Schema;

var TransitStopSchema = Schema ( {
    system: String,
    name: String,
    latitude: Number,
    longitude: Number,
    route_id: String,
    route_name: String,
    route_type: String,
    route_direction: String,
    route_direction_slug: String
} );

TransitStopSchema.index( {
    system: 1,
    route_type: 1,
    route_id: 1,
    route_direction_slug: 1
} );

TransitStopSchema.statics.forTrain = function ( filter, done ) {

    filter.route_type = "train";

    this.find( filter )
        .select( "name latitude longitude system route_direction" )
        .sort( { name: 1 } )
        .exec( done );

};

TransitStopSchema.statics.forBus = function ( filter, done ) {

    filter.route_type = "bus";

    this.find( filter )
        .select( "name latitude longitude system route_direction" )
        .sort( { name: 1 } )
        .exec( done );

};

exports.TransitStop = mongoose.model( "transit_stops", TransitStopSchema );

var mongoose = require( "mongoose" );

var Schema = mongoose.Schema;

var TransitStopSchema = Schema ( {
    system: String,
    name: String,
    latitude: Number,
    longitude: Number,
    route_id: String,
    route_name: String,
    route_type: String,
    route_direction: String,
    route_direction_slug: String
} );

TransitStopSchema.index( {
    system: 1,
    route_type: 1,
    route_id: 1,
    route_direction_slug: 1
} );

TransitStopSchema.statics.forTrain = function ( filter, done ) {

    filter.route_type = "train";

    this.find( filter )
        .select( "name latitude longitude system route_direction stop_order" )
        .sort( {
            route_id: 1,
            stop_order: 1
        } )
        .exec( done );

};

TransitStopSchema.statics.forBus = function ( filter, done ) {

    filter.route_type = "bus";

    this.find( filter )
        .select( "name latitude longitude system route_direction" )
        .sort( { name: 1 } )
        .exec( done );

};

exports.TransitStop = mongoose.model( "transit_stops", TransitStopSchema );

Sort trains by stop order as a default


'use strict';

/**
 * @ngdoc filter
 * @name dockstore.ui.filter:PreviewFilter
 * @function
 * @description
 * # PreviewFilter
 * Filter in the dockstore.ui.
 */
angular.module('dockstore.ui')
  .filter('PreviewFilter', [function () {
    return function (containers, contLimit) {
      if (!contLimit) return containers;
      var sortedByBuildTime = containers.sort(function(a, b) {
      	if (!a) a = Number.MAX_VALUE;
      	if (!b) b = Number.MAX_VALUE;
        return a.lastBuild - b.lastBuild;
      });
      return sortedByBuildTime.slice(0, contLimit - 1);
    };
  }]);

'use strict';

/**
 * @ngdoc filter
 * @name dockstore.ui.filter:PreviewFilter
 * @function
 * @description
 * # PreviewFilter
 * Filter in the dockstore.ui.
 */
angular.module('dockstore.ui')
  .filter('PreviewFilter', [function () {
    return function (containers, contLimit) {
      if (!contLimit) return containers;
      var sortedByBuildTime = containers.sort(function(a, b) {
      	if (!a.lastBuild) a.lastBuild.lastBuild = Number.MAX_VALUE;
      	if (!b.lastBuild) b.lastBuild = Number.MAX_VALUE;
        return a.lastBuild - b.lastBuild;
      });
      return sortedByBuildTime.slice(0, contLimit - 1);
    };
  }]);

Update PreviewFilter for invalid/non-returned timestamps (v2).


/*
 * Copyright 2015 Studentmediene i Trondheim AS
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
'use strict';

/**
 * @ngdoc service
 * @name barteguidenMarkedsWebApp.eventService
 * @description
 * # eventService
 * Factory in the barteguidenMarkedsWebApp.
 */
angular.module('barteguidenMarkedsWebApp.services')
  .factory('Event', function ($resource) {
    return $resource('http://localhost:4004/api/events/:id', { id: '@_id' }, {
      update: {
        method: 'PUT'
      },
      query: {
        method: 'GET',
        isArray: true
      }
    });
  });

/*
 * Copyright 2015 Studentmediene i Trondheim AS
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
'use strict';

/**
 * @ngdoc service
 * @name barteguidenMarkedsWebApp.eventService
 * @description
 * # eventService
 * Factory in the barteguidenMarkedsWebApp.
 */
angular.module('barteguidenMarkedsWebApp.services')
  .factory('Event', function ($resource) {
    return $resource('http://localhost:4004/api/events/:id', { id: '@_id' }, {
      update: {
        method: 'PUT'
      },
      query: {
        method: 'GET',
        isArray: true,
        params: { published: 'all' }
      }
    });
  });

Add `published=all` query param to client request


import AppConfig from 'scripts/config';

export default class User extends Backbone.Model {
  constructor(...args) {
    this.urlRoot = `${AppConfig.apiPath}/users`;

    this.validation = {
      username: {
        required: () => { return this.isSignup },
      },
      password: {
        required: true
      },
      password_confirmation: {
        equalTo: 'password',
        required: () => { return this.isSignup },
      },
      email: {
        pattern: 'email',
        required: true
      }
    };

    super(...args);
  }

  signIn() {
    this.isSignup = false;
    return this.send(`${this.url()}/sign_in`);
  }

  signUp() {
    this.isSignup = true;
    return this.send(`${this.url()}/sign_up`);
  }

  send(url) {
    let deferred = $.Deferred();

    if (this.isValid(true)) {
      this.save(null, {
        url,
        success(data) {
          deferred.resolve(data);
        },
        error(data) {
          deferred.reject(data);
        }
      });
    }
    else {
      deferred.reject();
    }

    return deferred.promise();
  }
}

import AppConfig from 'scripts/config';

export default class User extends Backbone.Model {
  constructor(...args) {
    this.urlRoot = `${AppConfig.apiPath}/users`;

    this.validation = {
      username: {
        required: () => { return this.isSignup; }
      },
      password: {
        required: true
      },
      password_confirmation: {
        equalTo: 'password',
        required: () => { return this.isSignup; }
      },
      email: {
        pattern: 'email',
        required: true
      }
    };

    super(...args);
  }

  signIn() {
    this.isSignup = false;
    return this.send(`${this.url()}/sign_in`);
  }

  signUp() {
    this.isSignup = true;
    return this.send(`${this.url()}/sign_up`);
  }

  send(url) {
    let deferred = $.Deferred();

    if (this.isValid(true)) {
      this.save(null, {
        url,
        success(data) {
          deferred.resolve(data);
        },
        error(data) {
          deferred.reject(data);
        }
      });
    }
    else {
      deferred.reject();
    }

    return deferred.promise();
  }
}

Fix syntax according to jshint


const logger = require('logger');
const mailService = require('services/mailService');

const DEFAULT_TEMPLATE = 'forest-change-notification';
const TEMPLATE_MAP = {
    'viirs-active-fires': 'forest-fires-notification-viirs',
    'imazon-alerts': 'forest-change-imazon-alerts',
    story: 'stories-alerts',
    'forma-alerts': 'forest-change-notification',
    forma250GFW: 'forest-change-notification',
    'terrai-alerts': 'forest-change-notification',
    'glad-alerts': 'forest-change-notification-glads',
    'monthly-summary': 'monthly-summary',
};

class EmailPublisher {

    static async publish(subscription, results, layer) {
        logger.info('Publishing email with results', results);
        let template = TEMPLATE_MAP[layer.slug] || DEFAULT_TEMPLATE;
        const language = subscription.language.toLowerCase().replace(/_/g, '-');
        template = `${template}-${language}`;
        logger.info('MAIL TEMPLATE', template);
        const recipients = [{
            address: {
                email: subscription.resource.content
            }
        }];
        mailService.sendMail(template, results, recipients);
    }

    static sendStats(emails, stats) {
        logger.info('Publishing email with stats', stats);
        const template = 'subscriptions-stats';
        logger.info('MAIL TEMPLATE', template);
        const recipients = emails.map((el) => ({ address: { email: el } }));
        mailService.sendMail(template, stats, recipients);
    }

}

module.exports = EmailPublisher;

const logger = require('logger');
const mailService = require('services/mailService');

const DEFAULT_TEMPLATE = 'forest-change-notification';
const TEMPLATE_MAP = {
    'viirs-active-fires': 'forest-fires-notification-viirs',
    'imazon-alerts': 'forest-change-imazon-alerts',
    story: 'stories-alerts',
    'forma-alerts': 'forest-change-notification',
    forma250GFW: 'forest-change-notification',
    'terrai-alerts': 'forest-change-notification',
    'glad-alerts': 'forest-change-notification-glads',
    'monthly-summary': 'monthly-summary',
};

class EmailPublisher {

    static async publish(subscription, results, layer) {
        logger.info('Publishing email with results', results);
        let template = TEMPLATE_MAP[layer.slug] || DEFAULT_TEMPLATE;
        const language = subscription.language.toLowerCase().replace(/_/g, '-');
        template = `${template}-${language}`;
        logger.info('MAIL TEMPLATE', template);
        const recipients = [{
            address: {
                email: subscription.resource.content
            }
        }];
        mailService.sendMail(template, results, recipients);
    }

    static sendStats(emails, stats) {
        logger.info('Publishing email with stats', stats);
        const template = 'subscriptions-stats';
        logger.info('MAIL TEMPLATE', template);
        const recipients = emails.map((el) => ({ address: { email: el } }));
        logger.info('Subscription stats email recipients', emails);
        mailService.sendMail(template, stats, recipients);
    }

}

module.exports = EmailPublisher;

Add log line for susbcription stats email recipients


import React, { Component } from 'react';
import Pollard from './Pollard';
import { createStore, combineReducers } from 'redux';
import { Provider } from 'react-redux';
import * as reducers from '../reducers';

const reducer = combineReducers(reducers);
const store = createStore(reducer);

export default class App extends Component {
  render() {
    return (
      <Provider store={store}>
        {() => <Pollard /> }
      </Provider>
    );
  }
}

import React, { Component } from 'react';
import Pollard from './Pollard';
import { createStore, combineReducers } from 'redux';
import { Provider } from 'react-redux';
import rootReducer from '../reducers/reducer.js';

// TODO
// remove combine reducers call
// const store = createStore(rootReducer);

debugger;
const reducer = combineReducers(rootReducer);
const store = createStore(reducer);

export default class App extends Component {
  render() {
    return (
      <Provider store={store}>
        {() => <Pollard /> }
      </Provider>
    );
  }
}

Add todo for removing combineReducer


import {CHANGE_FORM, SET_AUTH, SENDING_REQUEST} from '../actions/constants'

let assign = Object.assign

let initialState = {
  formState: {
    username: '',
    password: ''
  },
  currentlySending: false,
  loggedIn: false
}

function reducer (state = initialState, action) {
  switch (action.type) {
    case CHANGE_FORM:
      return assign({}, state, {
        formState: action.newState
      })
    case SET_AUTH:
      return assign({}, state, {
        loggedIn: action.newState
      })
    case SENDING_REQUEST:
      return assign({}, state, {
        currentlySending: action.sending
      })
    default:
      return state
  }
}

export default reducer

import {CHANGE_FORM, SET_AUTH, SENDING_REQUEST} from '../actions/constants'
import auth from '../auth';

let assign = Object.assign

let initialState = {
  formState: {
    username: '',
    password: ''
  },
  currentlySending: false,
  loggedIn: auth.loggedIn()
}

function reducer (state = initialState, action) {
  switch (action.type) {
    case CHANGE_FORM:
      return assign({}, state, {
        formState: action.newState
      })
    case SET_AUTH:
      return assign({}, state, {
        loggedIn: action.newState
      })
    case SENDING_REQUEST:
      return assign({}, state, {
        currentlySending: action.sending
      })
    default:
      return state
  }
}

export default reducer

Check auth status in initial state


var dbm = require('db-migrate');
var type = dbm.dataType;

exports.up = function(db, callback) {
  db.createTable('external_transactions', {
		id: { type: 'int', primaryKey: true, autoIncrement: true },
    deposit: { type: 'boolean', notNull: true },
    currency: { type: 'string', notNull: true },
    cash_amount: { type: 'decimal', notNull: true },
    external_account_id: { type: 'int', notNull: true },
    ripple_transaction_id: { type: 'int'},
    createdAt: { type: 'datetime', notNull: true },
    updatedAt: { type: 'datetime' }
  }, callback);
};

exports.down = function(db, callback) {
  db.dropTable('bank_transactions', callback);
};

var dbm = require('db-migrate');
var type = dbm.dataType;

exports.up = function(db, callback) {
  db.createTable('external_transactions', {
		id: { type: 'int', primaryKey: true, autoIncrement: true },
    deposit: { type: 'boolean', notNull: true },
    currency: { type: 'string', notNull: true },
    cash_amount: { type: 'decimal', notNull: true },
    external_account_id: { type: 'int', notNull: true },
    ripple_transaction_id: { type: 'int'},
    createdAt: { type: 'datetime', notNull: true },
    updatedAt: { type: 'datetime' }
  }, callback);
};

exports.down = function(db, callback) {
  db.dropTable('external_transactions', callback);
};

Rename drop_table table name from old bank_transactions.


Meteor.startup(function () {
  var scoreInterval = getSetting("scoreUpdateInterval") || 30;
  if(scoreInterval>0){

    // active items get updated every N seconds
    intervalId=Meteor.setInterval(function () {
      var updatedPosts = 0;
      var updatedComments = 0;
      // console.log('tick ('+scoreInterval+')');
      Posts.find({'status': 2,'inactive': {$ne : true}}).forEach(function (post) { // only run scoring on approved posts
        updatedPosts += updateScore({collection: Posts, item: post});
      });
      Comments.find({'inactive': {$ne : true}}).forEach(function (comment) {
        updatedComments += updateScore({collection: Comments, item: comment});
      });
      // console.log("Updated "+updatedPosts+"/"+Posts.find().count()+" Posts")
      // console.log("Updated "+updatedComments+"/"+Comments.find().count()+" Comments")
    }, scoreInterval * 1000);

    // inactive items get updated every hour
    inactiveIntervalId=Meteor.setInterval(function () {
      var updatedPosts = 0;
      var updatedComments = 0;
      Posts.find({'inactive': true}).forEach(function (post) {
        updatedPosts += updateScore({collection: Posts, item: post});
      });
      Comments.find({'inactive': true}).forEach(function (comment) {
        updatedComments += updateScore({collection: Comments, item: comment});
      });
    }, 3600 * 1000);

  }
});

Meteor.startup(function () {
  var scoreInterval = getSetting("scoreUpdateInterval") || 60;
  if(scoreInterval>0){

    // active items get updated every N seconds
    intervalId=Meteor.setInterval(function () {
      var updatedPosts = 0;
      var updatedComments = 0;
      // console.log('tick ('+scoreInterval+')');
      Posts.find({'status': 2,'inactive': {$ne : true}}).forEach(function (post) { // only run scoring on approved posts
        updatedPosts += updateScore({collection: Posts, item: post});
      });
      Comments.find({'inactive': {$ne : true}}).forEach(function (comment) {
        updatedComments += updateScore({collection: Comments, item: comment});
      });
      // console.log("Updated "+updatedPosts+"/"+Posts.find().count()+" Posts")
      // console.log("Updated "+updatedComments+"/"+Comments.find().count()+" Comments")
    }, scoreInterval * 1000);

    // inactive items get updated every hour
    inactiveIntervalId=Meteor.setInterval(function () {
      var updatedPosts = 0;
      var updatedComments = 0;
      Posts.find({'inactive': true}).forEach(function (post) {
        updatedPosts += updateScore({collection: Posts, item: post});
      });
      Comments.find({'inactive': true}).forEach(function (comment) {
        updatedComments += updateScore({collection: Comments, item: comment});
      });
    }, 3600 * 1000);

  }
});

Cut the rate of score decay in half


var Hapi = require('hapi');
var Path = require('path');
var routes = require('./lib/routes');

// Create server
var server = new Hapi.Server({
  connections: {
    router: { stripTrailingSlash: true },
    routes: {
      security: {
        xframe: {
          rule: 'allow-from',
          source: 'http://optimizely.com'
        }
      },
      files: { relativeTo: Path.join(__dirname, 'static') }
    }
  }
});

server.connection({
  port: process.env.PORT || 8000
});

// Route handlers
routes(server);

// Start listening
server.start(function () {
  console.log('Server running at:', server.info.uri);
});

var Hapi = require('hapi');
var Path = require('path');
var routes = require('./lib/routes');

// Create server
var server = new Hapi.Server({
  connections: {
    router: { stripTrailingSlash: true },
    routes: {
      security: {
        xframe: {
          rule: 'allow-from',
          source: 'https://optimizely.com'
        }
      },
      files: { relativeTo: Path.join(__dirname, 'static') }
    }
  }
});

server.connection({
  port: process.env.PORT || 8000
});

// Route handlers
routes(server);

// Start listening
server.start(function () {
  console.log('Server running at:', server.info.uri);
});

Use https for x-frame policy


import showdown from "showdown"

module.exports = {
  getShowdownConverter: (mediaPath) => {
    return new showdown.Converter({
      extensions: [
        (converter) => [
          {
            type   : "lang",
            filter : md => md.trim().replace(/(^\b.+\b: .*|^---$)[\s\S]*?---/m, "")
          },
          {
            type    : "lang",
            regex   : /(!\[.*?\]\()((?!http)\/?)/g,
            replace : `$1file://${encodeURI(mediaPath)}/`
          }
        ]
      ]
    })
  }
}

import showdown from "showdown"

module.exports = {
  getShowdownConverter: (mediaPath) => {
    return new showdown.Converter({
      extensions: [
        (converter) => [
          {
            type   : "lang",
            filter : md => md.trimLeft().replace(/(^\b.+\b: .*|^---$)[\s\S]*?---/m, "")
          },
          {
            type    : "lang",
            regex   : /(!\[.*?\]\()((?!http)\/?)/g,
            replace : `$1file://${encodeURI(mediaPath)}/`
          }
        ]
      ]
    })
  }
}

Stop right trimming markdown strings


const path = require('path')
const nodeExternals = require('webpack-node-externals')
const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin')

const ROOT = path.join(__dirname, '/../../')

const config = {
  mode: process.env.NODE_ENV,
  target: 'node',
  node: {
    __dirname: true,
    __filename: true,
  },
  externals: [nodeExternals()],
  entry: path.join(ROOT, 'lib/app.ts'),
  output: {
    path: path.join(ROOT, 'dist/server'),
    filename: 'app.js',
  },
  resolve: {
    plugins: [new TsconfigPathsPlugin({ configFile: path.join(ROOT, 'tsconfig.server.json') })],
    modules: ['./node_modules'],
    extensions: ['.ts', '.js', '.json'],
  },
  module: {
    rules: [
      {
        test: /\.(js|ts)$/,
        exclude: /node_modules/,
        loader: 'ts-loader',
        options: {
          configFile: path.join(ROOT, 'tsconfig.server.json'),
        },
      },
    ],
  },
  // plugins: [new webpack.ContextReplacementPlugin(/local_modules/, (context) => {
  //   console.log(context)
  // })],
  stats: {
    colors: true,
    errorDetails: true,
  },
}

module.exports = config

const path = require('path')
const nodeExternals = require('webpack-node-externals')
const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin')

const ROOT = path.join(__dirname, '/../../')

const config = {
  mode: process.env.NODE_ENV,
  target: 'node',
  node: {
    __dirname: true,
    __filename: true,
  },
  externals: [nodeExternals()],
  entry: path.join(ROOT, 'lib/app.ts'),
  output: {
    path: path.join(ROOT, 'dist/server'),
    filename: 'app.js',
  },
  resolve: {
    plugins: [new TsconfigPathsPlugin({ configFile: path.join(ROOT, 'tsconfig.server.json') })],
    modules: ['./node_modules'],
    extensions: ['.ts', '.tsx', '.js', '.jsx', '.json'],
  },
  module: {
    rules: [
      {
        test: /.(ts|js)x?$/,
        exclude: /node_modules/,
        loader: 'ts-loader',
        options: {
          configFile: path.join(ROOT, 'tsconfig.server.json'),
        },
      },
    ],
  },
  // plugins: [new webpack.ContextReplacementPlugin(/local_modules/, (context) => {
  //   console.log(context)
  // })],
  stats: {
    colors: true,
    errorDetails: true,
  },
}

module.exports = config

Resolve tsx and jsx extentions for SSR


function (user, context, callback) {
  // /!\ DO NOT EDIT THIS FILE /!\
  // Please use http://github.com/mozilla-iam/auth0-rules instead

  // LDAP group okta_mfa requires MFA authentication everywhere.
  if (user.groups && user.groups.indexOf("okta_mfa") >= 0) {
    context.multifactor = {
      provider: 'duo',
      ikey: configuration.duo_ikey_mozilla,
      skey: configuration.duo_skey_mozilla,
      host: configuration.duo_apihost_mozilla,

      // optional:
      // Force DuoSecurity everytime this rule runs. Defaults to false.
      // If accepted by users the cookie lasts for 30 days - i.e. 30 days MFA session (this cannot be changed)
      ignoreCookie: false,
      username: user.email,
    };
  }
  callback(null, user, context);
}

function (user, context, callback) {
  // /!\ DO NOT EDIT THIS FILE /!\
  // Please use http://github.com/mozilla-iam/auth0-rules instead

  if (context.connectionStrategy === 'ad') {
    context.multifactor = {
      provider: 'duo',
      ikey: configuration.duo_ikey_mozilla,
      skey: configuration.duo_skey_mozilla,
      host: configuration.duo_apihost_mozilla,

      // optional:
      // Force DuoSecurity everytime this rule runs. Defaults to false.
      // If accepted by users the cookie lasts for 30 days - i.e. 30 days MFA session (this cannot be changed)
      ignoreCookie: false,
      username: user.email,
    };
  }
  callback(null, user, context);
}

Enforce MFA for ALL ldap users


'use strict';

const mirror = require('./mirror'),
    functions = require('firebase-functions');

module.exports = { mirror: functions.https.onRequest(mirror.app) };

'use strict';

const mirror = require('./mirror'),
    functions = require('firebase-functions');

module.exports = {
    mirror: functions
        .runWith({
            maxInstances: 10,
            timeoutSeconds: 540, // 9 min, max
            memory: "128MB", // min
        })
        .https
        .onRequest(mirror.app)
};

Set maxInstances to 10, timeout to 9min and memory to 128MB


"use strict";

/**
 * Creates and returns a JavaScript array containing WebGL compatible colors
 * from a grayscale palette, enough to cover every 3 vertices.
 * @param {faces} total number of vertices divided by 6.
 * @return {colors} colors to bind to vertices.
 */
function create_grayscale(faces)
{
    var colors = [];
    var step = 1.0 / faces;
    for(var i = 0 ; i < faces ; i++)
    {
	var i_step = i * step;
	for(var j = 0 ; j < 9 ; j++)
	    colors.push(i_step);
    }
    return colors;
}

/**
 * Creates and returns a JavaScript array containing WebGL compatible colors
 * from an rgb palette, enough to cover every 3 vertices.
 * @param {faces} total number of vertices divided by 6.
 * @return {colors} colors to bind to vertices.
 */
function create_rgb(faces)
{
    var colors = [];
    var step = 1.0 / faces;
    for(var i = 0 ; i < faces ; i++)
    {
	colors.push(0.0);
	colors.push(0.0);
	colors.push(1.0);
	colors.push(0.0);
	colors.push(1.0);
	colors.push(0.0);
	colors.push(1.0);
	colors.push(0.0);
	colors.push(0.0);
    }
    return colors;
}

"use strict";

/**
 * Returns a JavaScript array containing WebGL compatible colors
 * from a grayscale palette, enough to cover every 3 vertices.
 * @param {faces} total number of vertices divided by 6.
 * @param {colors} optional predefined JavaScript array to extend.
 * @return {colors} colors to bind to vertices.
 */
function create_grayscale(faces , colors)
{
    if(!colors)
	colors = [];
    var step = 1.0 / faces;
    for(var i = colors.length / 9 ; i < faces ; i++)
    {
	var i_step = i * step;
	for(var j = 0 ; j < 9 ; j++)
	    colors.push(i_step);
    }
    return colors;
}

/**
 * Returns a JavaScript array containing WebGL compatible colors
 * from an rgb palette, enough to cover every 3 vertices.
 * @param {faces} total number of vertices divided by 6.
 * @param {colors} optional predefined JavaScript array to extend.
 * @return {colors} colors to bind to vertices.
 */
function create_rgb(faces , colors)
{
    if(!colors)
	colors = [];
    var step = 1.0 / faces;
    for(var i = colors.length / 9 ; i < faces ; i++)
    {
	colors.push(0.0);
	colors.push(0.0);
	colors.push(1.0);
	colors.push(0.0);
	colors.push(1.0);
	colors.push(0.0);
	colors.push(1.0);
	colors.push(0.0);
	colors.push(0.0);
    }
    return colors;
}

Extend a preexisting color array.


#!/usr/bin/env node
var child = require("child_process");
var http = require("http");
var os = require("os");
var url = require("url");

var irc = require("../library/irc.js");
var settings = require("../settings.js");

var client;
var httpServer;

var bootstrap = function() {
    var restartRequestHandler = function(req, res) {
        var path = url.parse(req.url).pathname;

        res.end();

        if (path === "/restart/" + settings.webserver.password) {
            reboot();
        }
    };

    client = new irc.ircBot();
    client.connect();

    httpServer = http.createServer(restartRequestHandler);
    httpServer.listen(settings.webserver.port);
};

var reboot = function() {
    client.disconnect("Fetching updates from repository");
    console.log("=== Upgrading...");
    var result = child.execSync('git pull ' + (/^win/.test(os.platform) ? '&' : '&&') + ' npm install', {
        encoding: 'utf-8'
    });
    console.log(result);
    console.log("=== Upgrade ended");

    process.exit(8);
};

bootstrap();

#!/usr/bin/env node
var child = require("child_process");
var http = require("http");
var os = require("os");
var url = require("url");

var irc = require("../library/irc.js");
var settings = require("../settings.js");

var client;
var httpServer;

var bootstrap = function() {
    var restartRequestHandler = function(req, res) {
        var path = url.parse(req.url).pathname;

        if (path === "/restart/" + settings.webserver.password) {
            res.writeHead(200);
            res.end();
            reboot();
            return;
        }

        res.writeHead(404);
        res.end();
    };

    client = new irc.ircBot();
    client.connect();

    httpServer = http.createServer(restartRequestHandler);
    httpServer.listen(settings.webserver.port || 8080);
};

var reboot = function() {
    client.disconnect("Fetching updates from repository");
    console.log("=== Upgrading...");
    var result = child.execSync('git pull ' + (/^win/.test(os.platform) ? '&' : '&&') + ' npm install', {
        encoding: 'utf-8'
    });
    console.log(result);
    console.log("=== Upgrade ended");

    process.exit(8);
};

bootstrap();

Send 404 if page not found


var express = require('express');
var nconf = require('./config/nconf.js');
var fs = require('fs');
var https = require('https');

var adapter = require(nconf.get('RIPPLE_DATAMODEL_ADAPTER'));
var GatewayExpress = require(nconf.get('RIPPLE_EXPRESS_GATEWAY'));
var passport = require('./config/passport')(adapter);

app = new GatewayExpress(express(), passport, adapter);

app.use("/", express.static(__dirname + "/app"));

if (nconf.get('SSL')) {
  app = https.createServer({
    key: fs.readFileSync('./certs/server.key'),
    cert: fs.readFileSync('./certs/server.crt')
  }, app);
}

app.listen(nconf.get('PORT'), nconf.get('HOST'));

console.log('Serving '+ (nconf.get('SSL') ? 'HTTPS' : 'HTTP') +' on', (nconf.get('HOST') || 'localhost')+":"+nconf.get('PORT'));

var express = require('express');
var nconf = require('./config/nconf.js');
var fs = require('fs');
var https = require('https');

var adapter = require(nconf.get('RIPPLE_DATAMODEL_ADAPTER'));
var GatewayExpress = require(nconf.get('RIPPLE_EXPRESS_GATEWAY'));
var passport = require('./config/passport')(adapter);

app = new GatewayExpress(express(), passport, adapter);

app.use("/", express.static(__dirname + "/app"));

app.get('/ripple.txt', function(req, res) {
  res.set({ 'Content-Type': 'text/plain' });
  res.send("[accounts]\n"+nconf.get('gateway_cold_wallet'));
});

if (nconf.get('SSL')) {
  app = https.createServer({
    key: fs.readFileSync('./certs/server.key'),
    cert: fs.readFileSync('./certs/server.crt')
  }, app);
}

app.listen(nconf.get('PORT'), nconf.get('HOST'));

console.log('Serving '+ (nconf.get('SSL') ? 'HTTPS' : 'HTTP') +' on', (nconf.get('HOST') || 'localhost')+":"+nconf.get('PORT'));

Add ripple.txt support with cold wallet address.


module.exports = function (socket) {
  socket.on('connect', function () {
    console.log('Socket: connect');
  });
  socket.on('connect_error', function () {
    console.log('Socket: connect_error');
  });
  socket.on('connect_timeout', function () {
    console.log('Socket: connect_timeout');
  });
  socket.on('error', function () {
    console.log('Socket: error');
  });
  socket.on('disconnect', function () {
    console.log('Socket: disconnect');
  });
  socket.on('reconnect', function () {
    console.log('Socket: reconnect');
  });
  socket.on('reconnect_attempt', function () {
    console.log('Socket: reconnect_attempt');
  });
  socket.on('reconnecting', function () {
    console.log('Socket: reconnecting');
  });
  socket.on('reconnect_error', function () {
    console.log('Socket: reconnect_error');
  });
  socket.on('reconnect_failed', function () {
    console.log('Socket: reconnect_failed');
  });
};

module.exports = function (socket) {
  socket.on('connect', function () {
    console.log('Socket: connect');
  });
  socket.on('connect_error', function () {
    console.log('Socket: connect_error');
  });
  socket.on('connect_timeout', function () {
    console.log('Socket: connect_timeout');
  });
  socket.on('disconnect', function () {
    console.log('Socket: disconnect');
  });
  socket.on('reconnect', function () {
    console.log('Socket: reconnect');
  });
  socket.on('reconnect_attempt', function () {
    console.log('Socket: reconnect_attempt');
  });
  socket.on('reconnecting', function () {
    console.log('Socket: reconnecting');
  });
  socket.on('reconnect_error', function () {
    console.log('Socket: reconnect_error');
  });
  socket.on('reconnect_failed', function () {
    console.log('Socket: reconnect_failed');
  });
};

Remove legacy socket error handler


import React from 'react';
import {
  Animated,
  StyleSheet,
  View,
  Dimensions,
  TouchableHighlight
} from 'react-native';

const DEFAULT_ANIMATE_TIME = 300;
const styles = StyleSheet.create({
    fullOverlay: {
        top: 0,
        bottom: 0,
        left: 0,
        right: 0,
        backgroundColor: 'transparent',
        position: 'absolute'
    },
    emptyOverlay: {
        backgroundColor: 'transparent',
        position: 'absolute'
    }
});
export default class Overlay extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      fadeAnim: new Animated.Value(0),
      overlayStyle: styles.emptyOverlay
    }
  }
  onAnimatedEnd() {
    if(!this.props.visible) {
        this.setState({overlayStyle:styles.emptyOverlay});
    }
  }
  componentWillReceiveProps(newProps) {
    if(newProps.visible){
      this.setState({overlayStyle: styles.fullOverlay});
    }
    return Animated.timing(this.state.fadeAnim, {
        toValue: newProps.visible ? 1 : 0,
        duration: DEFAULT_ANIMATE_TIME
    }).start(this.onAnimatedEnd.bind(this));
  }
  render() {
    return (
      <Animated.View style={[this.state.overlayStyle, {opacity: this.state.fadeAnim}]}>
          {this.props.children}
      </Animated.View>
    );
  }
}

import React from 'react';
import {
  Animated,
  StyleSheet,
  View,
  Dimensions,
  TouchableHighlight
} from 'react-native';

const DEFAULT_ANIMATE_TIME = 300;
const styles = StyleSheet.create({
    fullOverlay: {
        top: 0,
        bottom: 0,
        left: 0,
        right: 0,
        backgroundColor: 'transparent',
        position: 'absolute'
    },
    emptyOverlay: {
        width: 0,
        height: 0,
        backgroundColor: 'transparent',
        position: 'absolute'
    }
});
export default class Overlay extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      fadeAnim: new Animated.Value(0),
      overlayStyle: styles.emptyOverlay
    }
  }
  onAnimatedEnd() {
    if(!this.props.visible) {
        this.setState({overlayStyle:styles.emptyOverlay});
    }
  }
  componentWillReceiveProps(newProps) {
    if(newProps.visible){
      this.setState({overlayStyle: styles.fullOverlay});
    }
    return Animated.timing(this.state.fadeAnim, {
        toValue: newProps.visible ? 1 : 0,
        duration: DEFAULT_ANIMATE_TIME
    }).start(this.onAnimatedEnd.bind(this));
  }
  render() {
    return (
      <Animated.View style={[this.state.overlayStyle, {opacity: this.state.fadeAnim}]}>
          {this.props.children}
      </Animated.View>
    );
  }
}

Remove width and height when the overlay is invisible


// @flow
import type { Store } from 'redux'
import type { RoutesMap } from '../flow-types'

export default (routesMap: RoutesMap, selectLocationState: Function) => ({
  dispatch,
  getState
}: Store<*, *>): Promise<any> => {
  const { type } = selectLocationState(getState())
  const route = routesMap[type]

  if (route && typeof route.thunk === 'function') {
    return Promise.resolve(route.thunk(dispatch, getState))
  }

  return Promise.resolve()
}

// @flow
import type { Store } from 'redux'
import type { RoutesMap, SelectLocationState } from '../flow-types'

export default (
  routesMap: RoutesMap,
  selectLocationState: SelectLocationState
) => ({ dispatch, getState }: Store<*, *>): Promise<*> => {
  const { type } = selectLocationState(getState())
  const route = routesMap[type]

  if (route && typeof route.thunk === 'function') {
    return Promise.resolve(route.thunk(dispatch, getState))
  }

  return Promise.resolve()
}

Update some weak typing to fix code climate


var PermissionConstructor = require('./PermissionConstructor').PermissionConstructor;

function WebService(currentUser, dependencies) {
	this.currentUser = currentUser;
	this.dependencies = dependencies;
}

WebService.prototype.run = function (name, allowedTypeAccount, params) {
	var self = this;
	var permissionsInit = new PermissionConstructor(allowedTypeAccount, this.currentUser);

	return function (req, res) {
		var webServices;

		params.req = req;
		params.res = res;
		webServices = require('../ServiceDirectories/Web/Public');

		if (permissionsInit.check()) {
			webServices[name].call(undefined, self.dependencies, params);
		} else {
			webServices['accessDenied'].call(undefined, self.dependencies, params);
		}
	};
};

module.exports.WebService = WebService;

var PermissionConstructor = require('./PermissionConstructor').PermissionConstructor;

function WebService(currentUser) {
	this._currentUser = currentUser;
	this._dependencies = {};
}

WebService.prototype.run = function (name, allowedTypeAccount, params) {
	var self = this;
	var permissionsInit = new PermissionConstructor(allowedTypeAccount, this._currentUser);
	var webServices = require('../ServiceDirectories/Web/Public');

	if (permissionsInit.check()) {
		webServices[name].call(undefined, self._dependencies, params);
	} else {
		webServices['accessDenied'].call(undefined, self._dependencies, params);
	}
};

WebService.prototype.addDependency = function (name, value) {
	this._dependencies[name] = value;
};

module.exports.WebService = WebService;

Change method of transmission dependency and returns value.


import React from 'react';

import timeFormatter from './../../utils/timeformatter';

const style = {

    container: {
        position: 'absolute',
        left: '0',
        bottom: '0',
        width: '100%'
    },

    barContainer: {
        background: '#0B486B',
        height: '1rem',
        width: '100%'
    },

    time: {
        color: '#FFF',
        fontFamily: 'OpenSans, sans-serif',
        padding: '0.5rem 1rem'
    },

    left: {
        float: 'left'
    },

    right: {
        float: 'right'
    }

};

export default class ScrubBar extends React.Component {

    constructor (params) {
        super(params);

        this.state = {
            position: 0,
            duration: params.duration
        }
    }

    render () {
        return (

                {timeFormatter(this.state.position)}
                {timeFormatter(this.state.duration)}

        )
    }

}

import React from 'react';

import timeFormatter from './../../utils/timeformatter';

const style = {

    container: {
        position: 'absolute',
        left: '0',
        bottom: '0',
        width: '100%'
    },

    barContainer: {
        background: '#0B486B',
        height: '0.5rem',
        width: '100%'
    },

    barFill: {
        background: '#3B8686',
        height: '0.5rem',
        width: '0%'
    },

    time: {
        color: '#FFF',
        fontFamily: 'OpenSans, sans-serif',
        padding: '0.5rem 1rem'
    },

    left: {
        float: 'left'
    },

    right: {
        float: 'right'
    },

    clear: {
        clear: 'both'
    }

};

export default class ScrubBar extends React.Component {

    constructor (params) {
        super(params);

        this.state = {
            position: 0,
            duration: params.duration
        }
    }

    render () {
        let progress = this.state.position / (this.state.duration / 100) + '%';

        return (

                {timeFormatter(this.state.position)}
                {timeFormatter(this.state.duration)}

        )
    }

}

Add fill to progress bar


"use strict";

import placeToEat from "../domain/placeToEat";

// First implementation always return the same in memory hardcoded place :)
function selectAPlaceToEat() {
    return () => placeToEat({ id: 123, name: "My House", cuisine: "American" });
};

// Enums. // Maybe move to their own module.
selectAPlaceToEat.RANDOM = "random";
selectAPlaceToEat.POPULAR = "popular";
selectAPlaceToEat.CUISINE = "cuisine";

export default selectAPlaceToEat;

"use strict";

import domain from "../domain";

// First implementation always return the same in memory hardcoded place :)
function selectAPlaceToEat() {
    let repo = domain.getPlacesRepo();
    return () => repo.fetchRandom();
};

// Enums. // Maybe move to their own module.
selectAPlaceToEat.RANDOM = "random";
selectAPlaceToEat.POPULAR = "popular";
selectAPlaceToEat.CUISINE = "cuisine";

export default selectAPlaceToEat;

Update action to use domain register for places repo.


'use strict';

var fs             = require('fs');
var express        = require('express');
var app            = express();
var multer         = require('multer');
var path           = require('path');
var upload         = multer({
    dest: 'sounds/',
    storage: multer.diskStorage({
        destination: path.join(__dirname, './sounds'),
        filename: function (req, file, cb) {
            cb(null, file.originalname);
        }
    })
});

module.exports = function(nodecg) {
    // When a POST is made to /lfg-sounds/upload...
    app.put('/lfg-sounds/upload',

        // Check if the user is authorized
        nodecg.util.authCheck,

        // Then receive the files they are sending, up to a max of 16
        upload.array('file', 16),

        // Then send a response.
        function (req, res) {
            if (req.files) {
                res.status(200).send('Success');
            } else {
                res.status(400).send('Bad Request');
            }
        }
    );

    app.get('/lfg-sounds/:filename', nodecg.util.authCheck, function (req, res, next) {
        var resName = req.params.filename;
        var fileLocation = path.join(__dirname, '../sounds/', resName);

        // Check if the file exists
        if (!fs.existsSync(fileLocation)) {
            next();
            return;
        }

        res.sendFile(fileLocation);
    });

    nodecg.mount(app);
};

'use strict';

var fs             = require('fs');
var express        = require('express');
var app            = express();
var multer         = require('multer');
var path           = require('path');
var upload         = multer({
    dest: 'sounds/',
    storage: multer.diskStorage({
        destination: path.join(__dirname, '../sounds'),
        filename: function (req, file, cb) {
            cb(null, file.originalname);
        }
    })
});

module.exports = function(nodecg) {
    // When a POST is made to /lfg-sounds/upload...
    app.put('/lfg-sounds/upload',

        // Check if the user is authorized
        nodecg.util.authCheck,

        // Then receive the files they are sending, up to a max of 16
        upload.array('file', 16),

        // Then send a response.
        function (req, res) {
            if (req.files) {
                res.status(200).send('Success');
            } else {
                res.status(400).send('Bad Request');
            }
        }
    );

    app.get('/lfg-sounds/:filename', nodecg.util.authCheck, function (req, res, next) {
        var resName = req.params.filename;
        var fileLocation = path.join(__dirname, '../sounds/', resName);

        // Check if the file exists
        if (!fs.existsSync(fileLocation)) {
            next();
            return;
        }

        res.sendFile(fileLocation);
    });

    nodecg.mount(app);
};

Fix sounds being uploaded to the wrong path


/* HTML utilities */
define(function () {
    function parseDocument(html) {
        /*
        var parser = new DOMParser();
        return parser.parseFromString(html, 'text/html');
        */
        var doc = document.implementation.createHTMLDocument('');
        doc.open();
        doc.write(html);
        doc.close();
        return doc;
    }

    function load(url, processResponse) {
        var xhr = new XMLHttpRequest();
        xhr.open('GET', url, true);  // async
        xhr.responseType = 'document';
        xhr.onreadystatechange = function () {
            if (xhr.readyState === 4) {  // response received and loaded
                processResponse({
                    status: xhr.status,
                    document: xhr.response
                });
            }
        };
        xhr.send(null);
    }

    return {
        parseDocument: parseDocument,
        load: load
    };
});

/* HTML utilities */
define(function () {
    function parseDocument(html) {
        // The HTML parsing is not supported on all the browsers, maybe we
        // should use a polyfill?
        var parser = new DOMParser();
        return parser.parseFromString(html, 'text/html');
    }

    function load(url, processResponse) {
        var xhr = new XMLHttpRequest();
        xhr.open('GET', url, true);  // async
        xhr.responseType = 'document';
        xhr.onreadystatechange = function () {
            if (xhr.readyState === 4) {  // response received and loaded
                processResponse({
                    status: xhr.status,
                    document: xhr.response
                });
            }
        };
        xhr.send(null);
    }

    return {
        parseDocument: parseDocument,
        load: load
    };
});

Fix parseDocument (was broken on Firefox)


// This is a manifest file that'll be compiled into application.js, which will include all the files
// listed below.
//
// Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts,
// or any plugin's vendor/assets/javascripts directory can be referenced here using a relative path.
//
// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
// compiled file.
//
// Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details
// about supported directives.
//
//= require jquery
//= require jquery_ujs
//= require turbolinks
//= require_tree .

// This is a manifest file that'll be compiled into application.js, which will include all the files
// listed below.
//
// Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts,
// or any plugin's vendor/assets/javascripts directory can be referenced here using a relative path.
//
// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
// compiled file.
//
// Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details
// about supported directives.
//
//// require jquery
//// require jquery_ujs
//// require turbolinks
//// require_tree .

Comment out some unused assets loading


$(document).ready(function() {
  // setup html editor
  var imgUploadHtml = $('#html-editor-image-upload-tab').html();
  var handler;

  if (imgUploadHtml) {
    var options = {
        "html": true,
      customTemplates: {
        image: function(locale) {
          return imgUploadHtml;
        }
      }
    };
    handler = function() { $(this).wysihtml5(options); };
  } else {
    handler = function() { $(this).wysihtml5(); };
  }

  $(document).on('DOMNodeInserted', function(e) {
      $('textarea.html-editor', e.target).each(handler);
  })
});

$(document).ready(function() {
  // setup html editor
  var imgUploadHtml = $('#html-editor-image-upload-tab').html();
  var options = undefined;

  if (imgUploadHtml) {
    options = {
      html: true,
      customTemplates: {
        image: function(locale) {
          return imgUploadHtml;
        }
      }
    };
  }

  var handler = function() {
    var $this = $(this);
    if ($this.data('wysihtml5')) {
      // We need to reinitialise the component; otherwise the iframe is as good as dead
      // see https://github.com/xing/wysihtml5/issues/148
      $this.css('display', 'block');
      var input = $this.val();
      var $parent = $this.parent();

      $this.siblings('.wysihtml5-sandbox, .wysihtml5-toolbar, input[name="_wysihtml5_mode"]').remove();
      $('', $parent).remove();
      $this.val(input);
    }

    $(this).wysihtml5(options);
  };

  $(document).on('DOMNodeInserted', function(e) {
    $('textarea.html-editor', e.target).each(handler);
  });
});

Allow html editor components to be moved around the page.


import manifest from '../../../package.json';
import debug from 'debug';
import path from 'path';
import app from 'app';

export function debugLogger(filename) {
  const name = path.basename(filename, '.js');
  const namespace = manifest.name + ':' + name;
  return debug(namespace);
}

export function errorLogger(filename, fatal) {
  const fakePagePath = filename.replace(app.getAppPath(), '');
  return function(...args) {
    args = args.map(a => a instanceof Error ? a.stack : a);
    console.error(`[${fakePagePath}]`, ...args);
    const analytics = require('./analytics').default;
    if (analytics) {
      analytics
        .pageview(fakePagePath)
        .exception(args.join(' '), fatal)
        .send();
    }
  };
}

import manifest from '../../../package.json';
import debug from 'debug';
import path from 'path';
import app from 'app';

export function debugLogger(filename) {
  const name = path.basename(filename, '.js');
  const namespace = manifest.name + ':' + name;
  return debug(namespace);
}

export function errorLogger(filename, fatal) {
  const fakePagePath = filename.replace(app.getAppPath(), '');
  return function(...args) {
    args = args.map(a => a instanceof Error ? a.stack : a);
    console.error(`[${new Date().toUTCString()}] ${fakePagePath}:`, ...args);
    const analytics = require('./analytics').default;
    if (analytics) {
      analytics
        .pageview(fakePagePath)
        .exception(args.join(' '), fatal)
        .send();
    }
  };
}

Include date in time in errorLogger


export default {
  // Get an array specifying the tools included in the current step.
  getStepTools: state => {
    if (state.steps && state.steps[state.activeStep].tools) {
      return state.steps[state.activeStep].tools
    } else {
      return ['pan', 'circle', 'rectangle', 'path', 'pencil', 'move', 'node', 'count', 'delete', 'megas', 'filter']
    }
  }
}

export default {
  // Get an array specifying the tools included in the current step.
  getStepTools: state => {
    if (state.steps && state.steps[state.activeStep].tools) {
      return state.steps[state.activeStep].tools
    } else {
      return ['pan', 'circle', 'rectangle', 'path', 'pencil', 'polygon', 'move', 'node', 'count', 'delete', 'megas', 'filter']
    }
  }
}

Add polygon tool to default set


// This code is to implement the tabs on the home page

// navigate to the selected tab or the first tab
function tabNavigation(e) {
    var activeTab = $('[href="' + location.hash + '"]');
    if (activeTab.length) {
        activeTab.tab('show');
    } else {
        var firstTab = $('.nav-tabs a:first');
        // select the first tab if it has an id and is expected to be selected
        if ((firstTab[0] !== undefined) && (firstTab[0].id != "")){
          $(firstTab).tab('show');
        }
    }
}

Blacklight.onLoad(function () {
  // When we visit a link to a tab, open that tab.
  var url = document.location.toString();
  if (url.match('#')) {
    $('.nav-tabs a[href="#' + url.split('#')[1] + '"]').tab('show');
  }

  // Change the url when a tab is clicked.
  $('a[data-toggle="tab"]').on('click', function(e) {
    history.pushState(null, null, $(this).attr('href'));
  });
  // navigate to a tab when the history changes (back button)
  window.addEventListener("popstate", tabNavigation);
});

// This code is to implement the tabs on the home page

// navigate to the selected tab or the first tab
function tabNavigation(e) {
    var activeTab = $('[href="' + location.hash + '"]');
    if (activeTab.length) {
        activeTab.tab('show');
    } else {
        var firstTab = $('.nav-tabs a:first');
        // select the first tab if it has an id and is expected to be selected
        if ((firstTab[0] !== undefined) && (firstTab[0].id != "")){
          $(firstTab).tab('show');
        }
    }
}

Blacklight.onLoad(function () {
  // When we visit a link to a tab, open that tab.
  var url = document.location.toString();
  if (url.match('#')) {
    $('.nav-tabs a[href="#' + url.split('#')[1] + '"]').tab('show');
  }

  // Change the url when a tab is clicked.
  $('a[data-toggle="tab"]').on('click', function(e) {
    // Set turbolinks: true so that turbolinks can handle the back requests
    // See https://github.com/turbolinks/turbolinks/blob/master/src/turbolinks/history.coffee#L28
    history.pushState({turbolinks: true}, null, $(this).attr('href'));
  });
  // navigate to a tab when the history changes (back button)
  window.addEventListener("popstate", tabNavigation);
});

Allow turbolinks to handle our custom push states


import reducer from "./reducers";
import { getEqfData } from "./selectors";
describe("getEqfData", () => {
  it("can extract EQF data from the current state", () => {
    const state = reducer();
    //state = reducer(state, { type: "SET_BAND_VALUE", band: 60, value: 100 });
    const actual = getEqfData(state);
    const expected = {
      presets: [
        {
          hz60: 32,
          hz170: 32,
          hz310: 32,
          hz600: 32,
          hz1000: 32,
          hz3000: 32,
          hz12000: 32,
          hz14000: 32,
          hz16000: 32,
          hz6000: 32,
          name: "Entry1",
          preamp: 32
        }
      ],
      type: "Winamp EQ library file v1.1"
    };
    expect(actual).toEqual(expected);
  });
});

import reducer from "./reducers";
import { getEqfData } from "./selectors";
describe("getEqfData", () => {
  it("can extract EQF data from the current state", () => {
    const state = reducer(undefined, { type: "@@INIT" });
    //state = reducer(state, { type: "SET_BAND_VALUE", band: 60, value: 100 });
    const actual = getEqfData(state);
    const expected = {
      presets: [
        {
          hz60: 32,
          hz170: 32,
          hz310: 32,
          hz600: 32,
          hz1000: 32,
          hz3000: 32,
          hz12000: 32,
          hz14000: 32,
          hz16000: 32,
          hz6000: 32,
          name: "Entry1",
          preamp: 32
        }
      ],
      type: "Winamp EQ library file v1.1"
    };
    expect(actual).toEqual(expected);
  });
});

Fix getting the default state.


import axios from 'axios'
import { useQuery, useQueryClient, useMutation } from 'react-query'

import { getCSRFToken, setCSRFToken } from 'utils/csrfToken'

const getCurrentUser = () =>
  axios.get('/api/v1/current_user').then(response => response.data)

const login = async user => {
  const { data, headers } = await axios.post(
    '/api/users/sign_in',
    { user },
    {
      headers: {
        Accept: 'application/json',
        'Content-Type': 'application/json',
        'X-CSRF-Token': getCSRFToken()
      }
    }
  )

  setCSRFToken(headers['new-csrf-token'])

  return data
}

const logout = async () => {
  const { headers } = await axios.delete('/api/users/sign_out', {
    headers: {
      Accept: 'application/json',
      'X-CSRF-Token': getCSRFToken()
    }
  })

  setCSRFToken(headers['new-csrf-token'])
}

const queryKey = ['currentUser']

export const useCurrentUserQuery = () => useQuery({ queryKey, queryFn: getCurrentUser })

export const useLoginMutation = () => {
  const queryClient = useQueryClient()

  return useMutation(login, {
    onSuccess(data) {
      queryClient.setQueryData(queryKey, data)
    }
  })
}

export const useLogoutMutation = () => {
  const queryClient = useQueryClient()

  return useMutation(logout, {
    onSuccess() {
      queryClient.removeQueries(queryKey)
    }
  })
}

import axios from 'axios'
import { useQuery, useQueryClient, useMutation } from 'react-query'

import { getCSRFToken, setCSRFToken } from 'utils/csrfToken'

const getCurrentUser = () =>
  axios.get('/api/v1/current_user').then(response => response.data)

const login = async user => {
  const { data, headers } = await axios.post(
    '/api/users/sign_in',
    { user },
    {
      headers: {
        Accept: 'application/json',
        'Content-Type': 'application/json',
        'X-CSRF-Token': getCSRFToken()
      }
    }
  )

  setCSRFToken(headers['new-csrf-token'])

  return data
}

const logout = async () => {
  const { headers } = await axios.delete('/api/users/sign_out', {
    headers: {
      Accept: 'application/json',
      'X-CSRF-Token': getCSRFToken()
    }
  })

  setCSRFToken(headers['new-csrf-token'])
}

const queryKey = ['currentUser']

export const useCurrentUserQuery = () => useQuery({ queryKey, queryFn: getCurrentUser })

export const useLoginMutation = () => {
  const queryClient = useQueryClient()

  return useMutation(login, {
    onSuccess(data) {
      queryClient.clear()
      queryClient.setQueryData(queryKey, data)
    }
  })
}

export const useLogoutMutation = () => {
  const queryClient = useQueryClient()

  return useMutation(logout, {
    onSuccess() {
      queryClient.clear()
    }
  })
}

Clear query cache after sign-in, sign-out


import _ from 'lodash';

const IfPermission = props =>
  (_.get(props, ['currentPerms', props.perm]) ? props.children : null);

export default IfPermission;

import _ from 'lodash';

const IfPermission = props =>
  (props.stripes.hasPerm(props.perm) ? props.children : null);

export default IfPermission;

Use stripes-core v0.3.0 API for permissions.


//= require ./core
//= require ./static_config
//= require_self

(function () {

	if (!Drop.config.JSON_CONFIG_URL) {
		throw Error("json_config_url is required!");
	}

	Drop.fetchConfig = function () {
		new Marbles.HTTP({
			method: 'GET',
			url: Drop.config.JSON_CONFIG_URL,
			middleware: [
				Marbles.HTTP.Middleware.WithCredentials,
				Marbles.HTTP.Middleware.SerializeJSON
			],
			callback: function (res, xhr) {
				if (xhr.status !== 200) {
					throw Error("failed to fetch json config: " + xhr.status + " - " + JSON.stringify(res));
				}

				if (!Drop.config) {
					Drop.config = {};
				}

				for (var key in res) {
					Drop.config[key] = res[key];
				}

				Drop.config_ready = true;
				Drop.trigger('config:ready');
			}
		});
	};

	Drop.fetchConfig();

})();

//= require ./core
//= require ./static_config
//= require_self

(function () {

	if (!Drop.config.JSON_CONFIG_URL) {
		throw Error("json_config_url is required!");
	}

	Drop.fetchConfig = function () {
		new Marbles.HTTP({
			method: 'GET',
			url: Drop.config.JSON_CONFIG_URL,
			middleware: [
				Marbles.HTTP.Middleware.WithCredentials,
				Marbles.HTTP.Middleware.SerializeJSON
			],
			callback: function (res, xhr) {
				if (xhr.status !== 200) {
					throw Error("failed to fetch json config: " + xhr.status + " - " + JSON.stringify(res));
				}

				if (!Drop.config) {
					Drop.config = {};
				}

				for (var key in res) {
					Drop.config[key] = res[key];
				}

				if (!Drop.config.meta) {
					throw Error("invalid config! missing meta post: " + JSON.stringify(res));
				}

				Drop.config_ready = true;
				Drop.trigger('config:ready');
			}
		});
	};

	Drop.fetchConfig();

})();

Throw error if app isn't authenticated


// global_config.js

var Parse = require('parse/node').Parse,
    PromiseRouter = require('./PromiseRouter');

var router = new PromiseRouter();

function getGlobalConfig(req) {
  return req.config.database.rawCollection('_GlobalConfig')
    .then(coll => coll.findOne({'_id': 1}))
    .then(globalConfig => ({response: { params: globalConfig.params }}))
    .catch(() => ({
      status: 404,
      response: {
        code: Parse.Error.INVALID_KEY_NAME,
        error: 'config does not exist',
      }
    }));
}

function updateGlobalConfig(req) {
  if (!req.auth.isMaster) {
    return Promise.resolve({
      status: 401,
      response: {error: 'unauthorized'},
    });
  }

  return req.config.database.rawCollection('_GlobalConfig')
    .then(coll => coll.findOneAndUpdate({ _id: 1 }, { $set: req.body }, { returnOriginal: false }))
    .then(response => {
      return { response: { params: response.value.params } }
    })
    .catch(() => ({
      status: 404,
      response: {
        code: Parse.Error.INVALID_KEY_NAME,
        error: 'config cannot be updated',
      }
    }));
}

router.route('GET', '/config', getGlobalConfig);
router.route('PUT', '/config', updateGlobalConfig);

module.exports = router;

// global_config.js

var Parse = require('parse/node').Parse,
    PromiseRouter = require('./PromiseRouter');

var router = new PromiseRouter();

function getGlobalConfig(req) {
  return req.config.database.rawCollection('_GlobalConfig')
    .then(coll => coll.findOne({'_id': 1}))
    .then(globalConfig => ({response: { params: globalConfig.params }}))
    .catch(() => ({
      status: 404,
      response: {
        code: Parse.Error.INVALID_KEY_NAME,
        error: 'config does not exist',
      }
    }));
}

function updateGlobalConfig(req) {
  if (!req.auth.isMaster) {
    return Promise.resolve({
      status: 401,
      response: {error: 'unauthorized'},
    });
  }

  return req.config.database.rawCollection('_GlobalConfig')
    .then(coll => coll.findOneAndUpdate({ _id: 1 }, { $set: req.body }, { returnOriginal: false }))
    .then(response => {
      return { response: { result: true } }
    })
    .catch(() => ({
      status: 404,
      response: {
        code: Parse.Error.INVALID_KEY_NAME,
        error: 'config cannot be updated',
      }
    }));
}

router.route('GET', '/config', getGlobalConfig);
router.route('PUT', '/config', updateGlobalConfig);

module.exports = router;

Update PUT response to align with current dashboard


module.exports = function (grunt) {
	grunt.registerTask('compileAssetsProd', [
    'compileAssets'
    // 'requirejs:prod'
	]);
}

module.exports = function (grunt) {
	grunt.registerTask('compileAssetsProd', [
    'compileAssets',
    'requirejs:prod'
	]);
}

Revert "Stopped minification because something isn't working."


function unveil() {
  $('img').unveil()
}

$(document).on('content:update', () => {
  unveil()

  $('body').on('shown.bs.modal', () => {
    unveil()
  })
})

function unveil() {
  $('img').unveil()
}

$(document).on('content:update', function() {
  unveil()

  $('body').on('shown.bs.modal', function() {
    unveil()
  })
})

Use ES5 in Sprocket compiled JS


if (typeof SharedArrayBuffer !== 'function' || typeof Atomics !== 'object') {
    document.getElementById('output').textContent = 'This browser does not support SharedArrayBuffers';
    return;
}

const worker = new Worker('worker.js');
worker.addEventListener('message', function (event) {
    document.getElementById('output').textContent = event.data;
});

const sharedBuffer = new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT);
const sharedArray = new Int32Array(sharedBuffer);
Lock.initialize(sharedArray, 0);
const lock = new Lock(sharedArray, 0);
lock.lock();
document.getElementById('unlock').addEventListener('click', event => {
    event.preventDefault();
    lock.unlock();
});

// Old API: transfer
worker.postMessage({sharedBuffer}, [sharedBuffer]);

// New API: clone
// worker.postMessage({sharedBuffer});

Add a warning if a browser does not support SharedArrayBuffers.


var getDefaultClientConfig = require('./getDefaultClientConfig');
var getDefaultProductionClientConfig = require('./getDefaultProductionClientConfig');
var getDefaultServerConfig = require('./getDefaultServerConfig');
var getDefaultProductionServerConfig = require('./getDefaultProductionServerConfig');
var getDefaultTestingConfig = require('./getDefaultTestingConfig');

function getClientConfig(isProduction, options) {
  var options = options || {};
  return isProduction ? getDefaultProductionClientConfig(options) : getDefaultClientConfig(options);
}

function getServerConfig(isProduction, options) {
  return isProduction ? getDefaultProductionServerConfig(options) : getDefaultServerConfig(options);
}

module.exports = {
  getDefaultClientConfig,
  getDefaultProductionClientConfig,
  getDefaultServerConfig,
  getDefaultProductionServerConfig,
  getDefaultTestingConfig,
  getClientConfig,
  getServerConfig,
};

var getDefaultClientConfig = require('./getDefaultClientConfig');
var getDefaultProductionClientConfig = require('./getDefaultProductionClientConfig');
var getDefaultServerConfig = require('./getDefaultServerConfig');
var getDefaultProductionServerConfig = require('./getDefaultProductionServerConfig');
var getDefaultTestingConfig = require('./getDefaultTestingConfig');

function getClientConfig(isProduction, options) {
  var passedOptions = options || {};
  return isProduction ? getDefaultProductionClientConfig(passedOptions) : getDefaultClientConfig(passedOptions);
}

function getServerConfig(isProduction, options) {
  var passedOptions = options || {};
  return isProduction ? getDefaultProductionServerConfig(passedOptions) : getDefaultServerConfig(passedOptions);
}

module.exports = {
  getDefaultClientConfig,
  getDefaultProductionClientConfig,
  getDefaultServerConfig,
  getDefaultProductionServerConfig,
  getDefaultTestingConfig,
  getClientConfig,
  getServerConfig,
};

Patch over bugs in sentry patch to upgrade version


import yargs from 'yargs';

export default {
  /**
   * Task name
   * @type {String}
   */
  name: 'sitecore:copy-sitecore-libraries',

  /**
   * Task description
   * @type {String}
   */
  description: 'Copy the sitecore libaries from the website to lib directory.',

  /**
   * Task default configuration
   * @type {Object}
   */
  config: {
    dest: './src',
    deps: [],
  },

  /**
   * Task help options
   * @type {Object}
   */
  help: {
    'src, -s': 'Source directory for sitecore libraries',
    'dest, -d': 'Destination directory',
  },

  /**
   * Task function
   * @param {object} config
   * @param {Function} end
   * @param {Function} error
   */
  fn(config, end, error) {
    if (!config) {
      error();
    }

    /* eslint no-unused-vars: 0 */
    const sample = yargs.argv.sample || config.sample;

    end();
  },
};

import gulp from 'gulp';
import yargs from 'yargs';

export default {
  /**
   * Task name
   * @type {String}
   */
  name: 'sitecore:copy-sitecore-libraries',

  /**
   * Task description
   * @type {String}
   */
  description: 'Copy the sitecore libaries from the website to lib directory.',

  /**
   * Task default configuration
   * @type {Object}
   */
  config: {
    dest: './lib/Sitecore',
    deps: [],
  },

  /**
   * Task help options
   * @type {Object}
   */
  help: {
    'src, -s': 'Source directory for sitecore libraries',
    'dest, -d': 'Destination directory',
  },

  /**
   * Task function
   * @param {object} config
   * @param {Function} end
   * @param {Function} error
   */
  fn(config, end, error) {
    const src = yargs.argv.src || yargs.argv.s || config.src;
    const dest = yargs.argv.dest || yargs.argv.d || config.dest;

    if (!src) {
      error({ message: 'Error: There is no `src` provided.' });

      return;
    }

    if (!dest) {
      error({ message: 'Error: There is no `dest` provided.' });

      return;
    }

    gulp.src(`${src}/**/*`)
      .pipe(gulp.dest(dest))
      .on('end', end);
  },
};

Add copy sitecore libraries task


module.exports = function(config) {
    var configuration = {

        client: {
            captureConsole: false
        },

        plugins: [
            'karma-chai',
            'karma-sinon',
            'karma-mocha',
            'karma-chrome-launcher'
        ],

        frameworks: ['mocha', 'sinon', 'chai'],

        files: [
            //- lib
            "lib/three.js/build/three.js",
            "lib/ColladaLoader/index.js",
            "lib/isMobile/isMobile.min.js",
            "lib/detector/index.js",

            //- app
            "src/javascript/helpers/*.js",
            "src/javascript/control/*.js",
            "src/javascript/models/*.js",
            "src/javascript/scene/*.js",
            "src/javascript/simulator/*.js",

            //- test
            'test/utils/**/*.js',
            'test/unit/**/*.js'
        ],

        reporters: ['progress'],
        browsers: ['Chrome'],

        singleRun: false
    }

    if(process.env.TRAVIS){
        configuration.browsers = ['Chrome_travis_ci'];
    }

    config.set(configuration);
}

module.exports = function(config) {
    var configuration = {

        client: {
            captureConsole: false
        },

        plugins: [
            'karma-chai',
            'karma-sinon',
            'karma-mocha',
            'karma-chrome-launcher'
        ],

        frameworks: ['mocha', 'sinon', 'chai'],

        files: [
            //- lib
            "lib/three.js/build/three.js",
            "lib/ColladaLoader/index.js",
            "lib/isMobile/isMobile.min.js",
            "lib/detector/index.js",

            //- app
            "src/javascript/helpers/*.js",
            "src/javascript/control/*.js",
            "src/javascript/models/*.js",
            "src/javascript/scene/*.js",
            "src/javascript/simulator/*.js",

            //- test
            'test/utils/**/*.js',
            'test/unit/**/*.js'
        ],

        reporters: ['progress'],
        browsers: ['Chrome'],

        customLaunchers: {
            chromeTravis: {
                base: 'Chrome',
                flags: ['--no-sandbox']
            }
        },

        singleRun: false
    }

    if(process.env.TRAVIS){
        configuration.browsers = ['chromeTravis'];
    }

    config.set(configuration);
}

Add GUI support for Travis