/**
 * Bootstrap
 * Client entry point
 * @flow
 */

// Polyfills
import 'babel/polyfill'

// Modules
import React from 'react'
import ReactDOM from 'react-dom'
import { Router } from 'react-router'
import { createHistory } from 'history'
import ReactRouterRelay from 'react-router-relay'

// Routes
import routes from './routes'

// Initialize dependencies
const history = createHistory()

// Mount the app
ReactDOM.render(
  <Router
    createElement={ReactRouterRelay.createElement}
    history={history}
    routes={routes}
  />,
  document.getElementById('app')
)

/**
 * Bootstrap
 * Client entry point
 * @flow
 */

// Polyfills
import 'babel/polyfill'

// Modules
import React from 'react'
import ReactDOM from 'react-dom'
import { Router } from 'react-router'
import { createHistory } from 'history'
import ReactRouterRelay from 'react-router-relay'

// Routes
import routes from './routes'

// Initialize dependencies
const history = createHistory()

// Mount the app
const instance = ReactDOM.render(
  <Router
    createElement={ReactRouterRelay.createElement}
    history={history}
    routes={routes}
  />,
  document.getElementById('app')
)

// Fix hotloading for pure components
// @TODO: handle updating the routes
if (module.hot) {
  module.hot.accept('./routes', () => {
    instance.forceUpdate()
  })
}

Handle hot loading the instance


const styles = {
  display: "grid",
  gridTemplateRows: "1fr 1fr 7fr 1fr",
};

function Resume() {
  return (

Skills

JavaScript, Python, HTML, CSS, SQL, MongoDB

NodeJS, Express, React, jQuery, Bootstrap, GraphQL

AWS: Cloudfront, Route53, S3, Lambda, EC2, Cloudformation

          Meeting Facilitation, Story Mapping, Wireframing, Leading Continuous
          Improvement

          Download Resume

  );
}

export default Resume;

Fix for iframe in Safari


$(document).ready(function(){
    /*var box = document.getElementsByTagName('a-box');

    var changeColor = function() {
        this.setAttribute('color', 'green');
    };
    var returnColor = function() {
        this.setAttribute('color', '#4CC3D9');
    };

    for (var i = 0; i < box.length; i++) {
        box[i].addEventListener('mouseenter', changeColor, false);
        box[i].addEventListener('mouseleave', returnColor, false);
    }*/

    $( "a-box" ).hover(
        function(){
            $(this).setAttribute('color', 'green');
        },
        function(){
            $(this).setAttribute('color', '#4CC3D9');
        }
    );
});

$(document).ready(function(){
    var box = document.getElementsByTagName('a-box');

    var changeColor = function() {
        this.setAttribute('color', 'green');
    };
    var returnColor = function() {
        this.setAttribute('color', '#4CC3D9');
    };

    for (var i = 0; i < box.length; i++) {
        box[i].addEventListener('mouseenter', changeColor, false);
        box[i].addEventListener('mouseleave', returnColor, false);
    }
});

Revert to old JS, TODO improve performance


#!/usr/bin/env node

console.log('Hello World!')

#!/usr/bin/env node

const util = require('util');
const packageVersion = require('./package.json').version
const exec = util.promisify(require('child_process').exec)

;(async () => {
  const { stdout } = await exec("curl 'https://registry.hub.docker.com/v2/repositories/library/rethinkdb/tags/'")

  const tags = JSON.parse(stdout).results.map(i => i.name)

  if (tags.includes(packageVersion)) {
    console.error('Please update the package version before publishing.')
    process.exit(1)
  }
})()

Check if package version is contained in last 10 versions


import React from 'react';

const Main = () => {

	const articles = [
		{
			id: 1,
			title: 'Title 1'
		}, {
			id: 2,
			title: 'Title 2'
		}, {
			id: 3,
			title: 'Title 3'
		}
	];

	return (

			{articles.map(function(article, index) {
				return (

{ article.title }

				);
			})}

	);
}

export default Main;

Add a unique key to each child in an array


'use strict';

import React from 'react/addons';

import Icon from './Icon.js';

import LeftColumnActions from '../actions/LeftColumnActions.js';
import SearchActions from '../actions/SearchActions.js';

class MenuButton extends React.Component {
  constructor(props) {
    super(props);

    this._onClick = this._onClick.bind(this);
  }

  render() {
    return (

        <Icon icon="menu-hamburger"/>

    );
  }

  _onClick() {
    LeftColumnActions.toggleVisibility();
  }
}

export default class Top extends React.Component {
  constructor(props) {
    super(props);

    this._onChange = this._onChange.bind(this);
  }

  render() {
    return (

        <MenuButton />

          <Icon icon="search" />
          <input type="text" name="search" placeholder="Search Music" onChange={this._onChange} />

    );
  }

  _onChange(e) {
    SearchActions.search(e.currentTarget.value);
  }
}

'use strict';

import React from 'react/addons';

import Icon from './Icon.js';

import LeftColumnActions from '../actions/LeftColumnActions.js';
import SearchActions from '../actions/SearchActions.js';

class MenuButton extends React.Component {
  constructor(props) {
    super(props);

    this._onClick = this._onClick.bind(this);
  }

  render() {
    return (

        <Icon icon="menu-hamburger"/>

    );
  }

  _onClick() {
    LeftColumnActions.toggleVisibility();
  }
}

export default class Top extends React.Component {
  constructor(props) {
    super(props);

    this._onChange = this._onChange.bind(this);
    this._onClick = this._onClick.bind(this);
  }

  render() {
    return (

        <MenuButton />

          <Icon icon="search" />
          <input type="text" name="search" placeholder="Search Music" onChange={this._onChange} onClick={this._onClick} />

    );
  }

  _onChange(e) {
    SearchActions.search(e.currentTarget.value);
  }

  _onClick(e) {
    e.currentTarget.select();
  }
}

Select search text on click


'use strict';

module.exports = {
  gutenberg: {
    startId:  26158,
    maxId:    50576,
    excludes: [
      11799,
    ],
  },

  paragraphLimit: 1000,

  title: 'SEED DISPERSAL',
};

'use strict';

module.exports = {
  gutenberg: {
    startId:  26158,
    maxId:    46441,
    excludes: [
      11799,
    ],
  },

  paragraphLimit: 1000,

  title: 'SEED DISPERSAL',
};

Change the max id to GITenberg's max


'use strict';

// this Babel plugin removes configuration comments such as:
//   <!-- template-lint triple-curlies=false -->
//   <!-- template-lint enabled=false -->

module.exports = function() {
  function RemoveConfigurationHtmlCommentsPlugin() {}

  RemoveConfigurationHtmlCommentsPlugin.prototype.transform = function(ast) {
    this.syntax.traverse(ast, {
      CommentStatement: function(node) {
        if (node.value.trim().indexOf('template-lint ') === 0) {
          // remove the entry
          return null;
        }
      }
    });

    return ast;
  };

  return RemoveConfigurationHtmlCommentsPlugin;
};

'use strict';

// this Babel plugin removes configuration comments such as:
//   <!-- template-lint triple-curlies=false -->
//   <!-- template-lint enabled=false -->

var AstNodeInfo = require('ember-template-lint/lib/helpers/ast-node-info');

module.exports = function() {
  function RemoveConfigurationHtmlCommentsPlugin() {}

  RemoveConfigurationHtmlCommentsPlugin.prototype.transform = function(ast) {
    var walker = new this.syntax.Walker();
    var bodyEntry;

    walker.visit(ast, function(node) {
      if (node.type === 'Program') {
        for (var i = 0; i < node.body.length; i++) {
          bodyEntry = node.body[i];

          if(AstNodeInfo.isConfigurationHtmlComment(bodyEntry)) {
            // remove the entry
            node.body.splice(i, 1);
          }
        }
      }
    });

    return ast;
  };

  return RemoveConfigurationHtmlCommentsPlugin;
};

Revert "Use simpler traverse API for removing config comments."


import React from 'react';

import ConfigurationView from './ConfigurationView';
import Service from '../structs/Service';

class ServiceDetailConfigurationTab extends React.Component {

  render() {
    let {service} = this.props;
    let {currentVersionID} = service.getVersionInfo();

    return (
      <ConfigurationView
        headline={`Current Version (${currentVersionID})`}
        service={service}
        versionID={currentVersionID} />
    );
  }

}

ServiceDetailConfigurationTab.propTypes = {
  service: React.PropTypes.instanceOf(Service).isRequired
};

module.exports = ServiceDetailConfigurationTab;

import {Dropdown} from 'reactjs-components';
import React from 'react';

import ConfigurationView from './ConfigurationView';
import DCOSStore from '../stores/DCOSStore';
import Service from '../structs/Service';

const METHODS_TO_BIND = [
  'handleVersionSelection'
];

class ServiceDetailConfigurationTab extends React.Component {
  constructor() {
    super();

    this.state = {
      selectedVersionID: null
    };

    METHODS_TO_BIND.forEach(method => {
      this[method] = this[method].bind(this);
    });
  }

  componentWillMount() {
    this.setState({
      selectedVersionID: this.props.service.getVersion()
    });
    DCOSStore.fetchServiceVersions(this.props.service.getId());
  }

  handleVersionSelection(versionItem) {
    this.setState({
      selectedVersionID: versionItem.id
    });
  }

  getVersionsDropdown() {
    let {service} = this.props;
    let versions = service.getVersions();

    if (versions.size < 2) {
      return null;
    }

    let versionItems = [];
    for (let version of versions.keys()) {
      let itemCaption = version;
      if (version === service.getVersion()) {
        itemCaption = `Current version ${version}`;
      }

      versionItems.push({
        id: version,
        html: (

            {itemCaption}

        )
      });
    }

    return (
      <Dropdown
        buttonClassName="button button-inverse dropdown-toggle button-split-content"
        dropdownMenuClassName="dropdown-menu inverse"
        dropdownMenuListClassName="dropdown-menu-list"
        dropdownMenuListItemClassName="clickable"
        items={versionItems}
        onItemSelection={this.handleVersionSelection}
        persistentID={this.state.selectedVersionID}
        transition={true}
        transitionName="dropdown-menu"
        wrapperClassName="dropdown" />
     );
  }

  render() {
    let {service} = this.props;
    let {selectedVersionID} = this.state;

    let headline = `Current Version (${selectedVersionID})`;

    if (service.getVersion() !== selectedVersionID) {
      headline = `Version (${selectedVersionID})`;
    }

    return (

        {this.getVersionsDropdown()}
        <ConfigurationView
          headline={headline}
          service={service}
          versionID={selectedVersionID} />

    );
  }

}

ServiceDetailConfigurationTab.propTypes = {
  service: React.PropTypes.instanceOf(Service).isRequired
};

module.exports = ServiceDetailConfigurationTab;

Add version dropdown to configuration with apply logic


(function($) {
  $.fn.inputDefaults = function(options) {
    var opts;
    opts = $.extend({}, $.fn.inputDefaults.defaults, options);
    return this.each(function() {
      var $el, defaultValue;
      $el = $(this);
      defaultValue = $el.data("defaultValue") || opts.value;
      if ($el.val() === "") {
        $el.val(defaultValue);
      }
      return $el.focus(function() {
        if ($el.val() === defaultValue) {
          return $el.val("");
        }
      }).blur(function() {
        if ($el.val() === "") {
          return $el.val(defaultValue);
        }
      });
    });
  };
  return $.fn.inputDefaults.defaults = {
    value: "Please enter a value..."
  };
})(jQuery);

(function() {

  (function($) {
    $.fn.inputDefaults = function(options) {
      var opts;
      opts = $.extend({}, $.fn.inputDefaults.defaults, options);
      return this.each(function() {
        var $el, defaultValue;
        $el = $(this);
        defaultValue = $el.data("defaultValue") || opts.value;
        if (!$el.val()) {
          $el.val(defaultValue);
        }
        return $el.focus(function() {
          if ($el.val() === defaultValue) {
            return $el.val("");
          }
        }).blur(function() {
          if (!$el.val()) {
            return $el.val(defaultValue);
          }
        });
      });
    };
    return $.fn.inputDefaults.defaults = {
      value: "Please enter a value..."
    };
  })(jQuery);

}).call(this);

Update to protect against no val().


'use strict';
/*
	effects.js

	Contains the functionality for the scrolling fixed menu
	and the popup menu toggle

*/
$(document).ready(function(){

	/* Menu Pop up Toggle */
	$('#popmenu').click( function(){
		$('#popup-menu').toggle('hide');
	});

	/* Smooth Scrolling */
	$(function() {
          $('a[href*="#"]:not([href="#"])').click(function() {
            if (location.pathname.replace(/^\//,'') == this.pathname.replace(/^\//,'') && location.hostname == this.hostname) {
              var target = $(this.hash);
              target = target.length ? target : $('[name=' + this.hash.slice(1) +']');
              if (target.length) {
                $('html, body').animate({
                  scrollTop: target.offset().top
                }, 1000);
                return false;
              }
            }
        });
    });

	/* Scroll Effect - can elements at different positions */
	$(window).on("scroll", function() {
    //if (window.matchMedia("(min-width: 400px)").matches) {
        if($(window).scrollTop() > 35) {
            $('header').css('background-color', '#fff');
        } else {
            $('header').css('background-color', 'transparent');
        }
    //} else {
            //do nothing
    //}

	});

});

'use strict';
/*
	effects.js

	Contains the functionality for the scrolling fixed menu
	and the popup menu toggle

*/
$(document).ready(function(){

	/* Menu Pop up Toggle */
	$('#popmenu').click( function(){
		$('#popup-menu').toggle('hide');
	});

	/* Smooth Scrolling */
	$(function() {
          $('a[href*="#"]:not([href="#"])').click(function() {
            if (location.pathname.replace(/^\//,'') == this.pathname.replace(/^\//,'') && location.hostname == this.hostname) {
              var target = $(this.hash);
              target = target.length ? target : $('[name=' + this.hash.slice(1) +']');
              if (target.length) {
                $('html, body').animate({
                  scrollTop: target.offset().top
                }, 1000);
                return false;
              }
            }
        });
    });

	/* Scroll Effect - can elements at different positions */
	$(window).on("scroll", function() {
	    if ($(window).scrollTop() > 35) {
	        $('#site-header').addClass('top-bar--opaque');
	    } else {
            $('#site-header').removeClass('top-bar--opaque');
	    }
	});

});

Improve visibility of nav bar text on page scroll


function (doc) {
    var media = 0, attachments = {}, value;

    if (doc.doc_type === "XFormInstance") {
        if (doc.xmlns) {
            for (var key in doc._attachments) {
                if (doc._attachments.hasOwnProperty(key) &&
                    doc._attachments[key].content_type !== "text/xml") {
                    media += 1;
                    attachments[key] = doc._attachments[key];
                }
            }
            if (media > 0) {
                value = {
                    xmlns: doc.xmlns,
                    attachments: attachments,
                    date: doc.received_on
                };
                emit([doc.domain, doc.app_id, doc.xmlns, doc.received_on], value);
            }
        }
    }
}

function (doc) {
    var media = 0, attachments = {}, value;

    if (doc.doc_type === "XFormInstance") {
        if (doc.xmlns) {
            for (var key in doc._attachments) {
                if (doc._attachments.hasOwnProperty(key) &&
                    doc._attachments[key].content_type !== "text/xml") {
                    media += 1;
                    attachments[key] = doc._attachments[key];
                }
            }
            for (var key in doc.external_blobs) {
                if (doc.external_blobs.hasOwnProperty(key) &&
                    doc.external_blobs[key].content_type !== "text/xml") {
                    media += 1;
                    attachments[key] = doc.external_blobs[key];
                }
            }
            if (media > 0) {
                value = {
                    xmlns: doc.xmlns,
                    attachments: attachments,
                    date: doc.received_on
                };
                emit([doc.domain, doc.app_id, doc.xmlns, doc.received_on], value);
            }
        }
    }
}

Update couch attachments view for external_blobs


import React, { PropTypes, Component } from 'react'
import moment from 'moment'

import style from './../../../styles/organisms/ThermostatForm.scss'

class ThermostatForm extends Component {

  getLastRemoteCheckin() {
    return moment().diff(this.props.lastCheckin, 'minutes')
  }

  render() {

    const minutesSinceLastCheckin = this.getLastRemoteCheckin.call(this)

    return (
      <form className={style.form} method="post" action={`/thermostat/${this.props.id}`}>

        <fieldset className={style.temperature}>
          <legend>Target Temperature</legend>
          <input type="hidden" name="_method" value="put" />
          <input name="temperature" type="number" required value={this.props.temperature} max="99" min="68" />
          °
        </fieldset>

        <input className={style.name} type="text" name="name" value={this.props.name} required />

        <button className="pure-button pure-button-primary">Update</button>

        {
          minutesSinceLastCheckin > 15 &&

            {"The remote hasn\'t checked in recently."}

        }

      </form>
    )

  }

}

ThermostatForm.propTypes = {
  id: PropTypes.number,
  temperature: PropTypes.string,
  name: PropTypes.string,
  lastCheckin: PropTypes.string
}

export default ThermostatForm

import React, { PropTypes, Component } from 'react'
import moment from 'moment'

import style from './../../../styles/organisms/ThermostatForm.scss'

class ThermostatForm extends Component {

  getLastRemoteCheckin() {
    return moment().diff(this.props.lastCheckin, 'minutes')
  }

  render() {

    const minutesSinceLastCheckin = this.getLastRemoteCheckin.call(this)

    return (
      <form
        title={`The remote last checked in ${minutesSinceLastCheckin} minutes ago.`}
        className={style.form}
        method="post"
        action={`/thermostat/${this.props.id}`}>

        <fieldset className={style.temperature}>
          <legend>Target Temperature</legend>
          <input type="hidden" name="_method" value="put" />
          <input name="temperature" type="number" required value={this.props.temperature} max="99" min="68" />
          °
        </fieldset>

        <input
          className={style.name}
          type="text" name="name"
          value={this.props.name} required />

        <button className="pure-button pure-button-primary">Update</button>

        {
          minutesSinceLastCheckin > 15 &&

            {"The remote hasn\'t checked in recently."}

        }

      </form>
    )

  }

}

ThermostatForm.propTypes = {
  id: PropTypes.number,
  temperature: PropTypes.string,
  name: PropTypes.string,
  lastCheckin: PropTypes.string
}

export default ThermostatForm

Add a tool tip to the form.


import fs from 'fs';
import path from 'path';
import Sequelize from 'sequelize';
import configs from '../config/config';

const basename = path.basename(module.filename);
const env = process.env.NODE_ENV || 'development';
const config = configs[env];

const db = {};
let sequelize;

if (config.use_env_variable) {
  sequelize = new Sequelize(process.env[config.use_env_variable]);
} else {
  sequelize = new Sequelize(config.database, config.username,
    config.password, config);
}

fs
  .readdirSync(__dirname)
  .filter((file) => {
    const fileName = (file.indexOf('.') !== 0) &&
                      (file !== basename) &&
                      (file.slice(-3) === '.js');
    return fileName;
  })
  .forEach((file) => {
    const model = sequelize.import(path.join(__dirname, file));
    db[model.name] = model;
  });

Object.keys(db).forEach((modelName) => {
  if (db[modelName].associate) {
    db[modelName].associate(db);
  }
});

db.sequelize = sequelize;

module.exports = db;

import fs from 'fs';
import path from 'path';
import Sequelize from 'sequelize';
import configs from '../config/config';

const basename = path.basename(module.filename);
const env = process.env.NODE_ENV || 'test';
const config = configs[env];

const db = {};
let sequelize;

if (config.use_env_variable) {
  sequelize = new Sequelize(process.env[config.use_env_variable]);
} else {
  sequelize = new Sequelize(config.database, config.username,
    config.password, config);
}

fs
  .readdirSync(__dirname)
  .filter((file) => {
    const fileName = (file.indexOf('.') !== 0) &&
                      (file !== basename) &&
                      (file.slice(-3) === '.js');
    return fileName;
  })
  .forEach((file) => {
    const model = sequelize.import(path.join(__dirname, file));
    db[model.name] = model;
  });

Object.keys(db).forEach((modelName) => {
  if (db[modelName].associate) {
    db[modelName].associate(db);
  }
});

db.sequelize = sequelize;

module.exports = db;

Fix bug on travis CI


import $ from 'jquery';

class ModalUI {
  constructor($node, options) {
    this.options = $.extend({}, {
      target: options.container || 'body',
    }, options);

    this.$modal = $node;
    this.$backdrop = $('<div class="note-modal-backdrop"/>');
  }

  show() {
    if (this.options.target === 'body') {
      this.$backdrop.css('position', 'fixed');
      this.$modal.css('position', 'fixed');
    } else {
      this.$backdrop.css('position', 'absolute');
      this.$modal.css('position', 'absolute');
    }

    this.$backdrop.appendTo(this.options.target).show();
    this.$modal.appendTo(this.options.target).addClass('open').show();

    this.$modal.trigger('note.modal.show');
    this.$modal.off('click', '.close').on('click', '.close', this.hide.bind(this));
  }

  hide() {
    this.$modal.removeClass('open').hide();
    this.$backdrop.hide();
    this.$modal.trigger('note.modal.hide');
  }
}

export default ModalUI;

import $ from 'jquery';

class ModalUI {
  constructor($node, options) {
    this.options = $.extend({}, {
      target: options.container || 'body',
    }, options);

    this.$modal = $node;
    this.$backdrop = $('<div class="note-modal-backdrop"/>');
  }

  show() {
    if (this.options.target === 'body') {
      this.$backdrop.css('position', 'fixed');
      this.$modal.css('position', 'fixed');
    } else {
      this.$backdrop.css('position', 'absolute');
      this.$modal.css('position', 'absolute');
    }

    this.$backdrop.appendTo(this.options.target).show();
    this.$modal.appendTo(this.options.target).addClass('open').show();

    this.$modal.trigger('note.modal.show');
    this.$modal.off('click', '.close').on('click', '.close', this.hide.bind(this));
    this.$modal.on('keydown', (event) => {
      if (event.which === 27) {
        event.preventDefault();
        this.hide();
      }
    });
  }

  hide() {
    this.$modal.removeClass('open').hide();
    this.$backdrop.hide();
    this.$modal.trigger('note.modal.hide');
    this.$modal.off('keydown');
  }
}

export default ModalUI;

Add closing lite dialog by pressing esc key


'use strict';

const fs = require('fs-extra');
const temp = require('temp');
const util = require('util');

const mkdirTemp = util.promisify(temp.mkdir);

function mkTmpDirIn(dir) {
  return fs.ensureDir(dir).then(() => mkdirTemp({ dir }));
}

module.exports = mkTmpDirIn;

'use strict';

const fs = require('fs-extra');
const temp = require('temp');
const util = require('util');

const mkdirTemp = util.promisify(temp.mkdir);

async function mkTmpDirIn(dir) {
  await fs.ensureDir(dir);
  return mkdirTemp({ dir });
}

module.exports = mkTmpDirIn;

Convert mkTmpDirIn to async/await syntax


'use strict';

angular.module('arethusa.core').directive('sidepanelFolder', [
  'sidepanel',
  function(sidepanel) {
    return {
      link: function (scope, element, attrs) {
        function addText() {
          var text = sidepanel.folded ? 'Show Panel' : 'Fold Panel';
          element.text(text);
        }

        element.on('click', function () {
          sidepanel.toggle();
          addText();
        });

        addText();
      }
    };
  }
]);

'use strict';

angular.module('arethusa.core').directive('sidepanelFolder', [
  'sidepanel',
  function(sidepanel) {
    return {
      scope: {},
      link: function (scope, element, attrs) {
        function addText() {
          var text = sidepanel.folded ? 'Show Panel' : 'Fold Panel';
          element.text(text);
        }

        element.on('click', function () {
          sidepanel.toggle();
          addText();
        });

        scope.sp = sidepanel;

        scope.$watch('sp.folded', function(newVal, oldVal) {
          addText();
        });
      }
    };
  }
]);

Use a watch on fold status in sidepanelFolder


var UploadController = Ember.Controller.extend({
    acceptEncoding: 'image/*',
    actions: {
        confirmAccept: function () {
            var self = this;

            this.get('model').save().then(function (model) {
                self.notifications.showSuccess('Saved');
                return model;
            }).catch(this.notifications.showErrors);
        },

        confirmReject: function () {
            return false;
        }
    }
});

export default UploadController;

var UploadController = Ember.Controller.extend({
    acceptEncoding: 'image/*',
    actions: {
        confirmAccept: function () {
            var self = this;

            this.get('model').save().then(function (model) {
                self.notifications.showSuccess('Saved');
                return model;
            }).catch(function (err) {
                self.notifications.showErrors(err);
            });
        },

        confirmReject: function () {
            return false;
        }
    }
});

export default UploadController;

Update Users API to handle role objects or ids


import React from 'react';
import ReactDOM from 'react-dom';
import App from './components/App';
import { applyMiddleware, createStore } from 'redux';
import { composeWithDevTools } from 'redux-devtools-extension';
import { Provider } from 'react-redux';
import createSagaMiddleware from 'redux-saga';
import {
    UPDATE,
} from './actiontypes';
import ukelonnReducer from './reducers';
import { rootSaga } from './sagas';
const sagaMiddleware = createSagaMiddleware();
import { createBrowserHistory } from 'history';
import { routerMiddleware } from 'connected-react-router';

const history = createBrowserHistory();
const store = createStore(ukelonnReducer,
                          composeWithDevTools(
                              applyMiddleware(
                                  sagaMiddleware,
                                  routerMiddleware(history)
                              )
                          )
                         );
sagaMiddleware.run(rootSaga);

if (typeof Notification !== 'undefined') {
    Notification.requestPermission().then(function(result) {
        store.dispatch(UPDATE({ notificationAvailable: true }));
        console.log(result);
    });
} else {
    store.dispatch(UPDATE({ notificationAvailable: false }));
}

ReactDOM.render(
    <Provider store={store}>
        <App />
    </Provider>,
    document.getElementById('root')
);

import React from 'react';
import ReactDOM from 'react-dom';
import App from './components/App';
import { configureStore } from 'redux-starter-kit';
import { Provider } from 'react-redux';
import createSagaMiddleware from 'redux-saga';
import {
    UPDATE,
} from './actiontypes';
import ukelonnReducer from './reducers';
import { rootSaga } from './sagas';
const sagaMiddleware = createSagaMiddleware();
import { createBrowserHistory } from 'history';
import { routerMiddleware } from 'connected-react-router';

const history = createBrowserHistory();
const store = configureStore({
    reducer: ukelonnReducer,
    middleware: [
        sagaMiddleware,
        routerMiddleware(history),
    ],
});
sagaMiddleware.run(rootSaga);

if (typeof Notification !== 'undefined') {
    Notification.requestPermission().then(function(result) {
        store.dispatch(UPDATE({ notificationAvailable: true }));
        console.log(result);
    });
} else {
    store.dispatch(UPDATE({ notificationAvailable: false }));
}

ReactDOM.render(
    <Provider store={store}>
        <App />
    </Provider>,
    document.getElementById('root')
);

Use redux-starter-kit to set up and configure the redux store


// This is a manifest file that'll be compiled into including all the files listed below.
// Add new JavaScript/Coffee code in separate files in this directory and they'll automatically
// be included in the compiled file accessible from http://example.com/assets/application.js
// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
// the compiled file.
//
//= require jquery
//= require jquery_ujs
//= require accounting.min
//= require spree/frontend

//= require_tree .

// This is a manifest file that'll be compiled into including all the files listed below.
// Add new JavaScript/Coffee code in separate files in this directory and they'll automatically
// be included in the compiled file accessible from http://example.com/assets/application.js
// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
// the compiled file.
//
//= require jquery
//= require jquery_ujs
//= require accounting.min
//= require js-routes
//= require spree/frontend

//= require_tree .

Include js routes in frontend


//Neutrine version
export {version} from "./src/version.js";

//Import global styles
import "./src/style.scss";

//Export helpers
export * from "./src/global.js";
export * from "./src/helpers.js";
export * from "./src/ready.js";

//Export basic components
export * from "./src/core/index.js";
export * from "./src/dashboard/index.js";
export * from "./src/experiments/index.js";
export * from "./src/future/index.js";
export * from "./src/icon/index.js";

//Loaders
export * from "./src/loaders/basic-loader/index.js";
export * from "./src/loaders/dna-loader/index.js";

//Widgets
export * from "./src/datatable/index.js";
//export * from "./src/text-editor/index.js";

//Export testing components
export * from "./src/test/index.js";

//Export utils components
export * from "./src/utils/choose.js";
export * from "./src/utils/for-each.js";
export * from "./src/utils/if.js";
export * from "./src/utils/renderer.js";

//Neutrine version
export {version} from "./src/version.js";

//Import global styles
import "./src/style.scss";

//Export helpers
export * from "./src/global.js";
export * from "./src/helpers.js";
export * from "./src/ready.js";

//Export basic components
export * from "./src/core/index.js";
export * from "./src/experiments/index.js";
export * from "./src/future/index.js";
export * from "./src/icon/index.js";
export * from "./src/widgets/index.js";

//Loaders
export * from "./src/loaders/basic-loader/index.js";
export * from "./src/loaders/dna-loader/index.js";

//Big components
export * from "./src/dashboard/index.js";
export * from "./src/datatable/index.js";

//Export testing components
export * from "./src/test/index.js";

//Export utils components
export * from "./src/utils/choose.js";
export * from "./src/utils/for-each.js";
export * from "./src/utils/if.js";
export * from "./src/utils/renderer.js";

Reorder exports and added widgets export


/* global requireShared */

const signToken = requireShared("utilities/signToken");

module.exports = (req, res) => {
    if (req.session.redirectUrl) {
        const url = `${req.session.redirectUrl}?accessToken=${signToken(req.user)}`;
        res.redirect(url);
    } else {
        res.status(202).send({accessToken: signToken(req.user)});
    }

    delete req.session.redirectUrl;
    return;
}

/* global requireShared */

const signToken = requireShared("utilities/signToken");
const {pick} = require("lodash");

module.exports = (req, res) => {

    const user = pick(req.user, ["_id", "email", "name"]);

    if (req.session.redirectUrl) {
        const url = `${req.session.redirectUrl}?accessToken=${signToken(user)}`;
        res.redirect(url);
    } else {
        res.status(202).send({accessToken: signToken(user)});
    }

    delete req.session.redirectUrl;
    return;
}

Add security so only selected value can be within the accesstoken (_id, email & name)


/**
 * Events.
 *
 * @mixin
 * @namespace Bolt.events
 *
 * @param {Object} bolt - The Bolt module.
 */
(function (bolt) {
    'use strict';

    /*
     * Bolt.events mixin container.
     *
     * @private
     * @type {Object}
     */
    var events = {};

    /*
     * Event broker object.
     *
     * @private
     * @type {Object}
     */
    var broker = $({});

    /**
     * Fires an event.
     *
     * @static
     * @function fire
     * @memberof Bolt.events
     *
     * @param {string} event       - Event type
     * @param {object} [parameter] - Additional parameters to pass along to the event handler
     */
    events.fire = function (event, parameter) {
        broker.trigger(event, parameter);
    };

    /**
     * Attach an event handler.
     *
     * @static
     * @function on
     * @memberof Bolt.events
     *
     * @param {string}   event   - Event type
     * @param {function} handler - Event handler
     */
    events.on = function (event, handler) {
        broker.on(event, handler);
    };

    // Apply mixin container
    bolt.events = events;

})(Bolt || {});

/**
 * Events.
 *
 * @mixin
 * @namespace Bolt.events
 *
 * @param {Object} bolt - The Bolt module.
 */
(function (bolt) {
    'use strict';

    /*
     * Bolt.events mixin container.
     *
     * @private
     * @type {Object}
     */
    var events = {};

    /*
     * Event broker object.
     *
     * @private
     * @type {Object}
     */
    var broker = $({});

    /**
     * Fires an event.
     *
     * @static
     * @function fire
     * @memberof Bolt.events
     *
     * @param {string} event       - Event type
     * @param {object} [parameter] - Additional parameters to pass along to the event handler
     */
    events.fire = function (event, parameter) {
        broker.triggerHandler(event, parameter);
    };

    /**
     * Attach an event handler.
     *
     * @static
     * @function on
     * @memberof Bolt.events
     *
     * @param {string}   event   - Event type
     * @param {function} handler - Event handler
     */
    events.on = function (event, handler) {
        broker.on(event, handler);
    };

    // Apply mixin container
    bolt.events = events;

})(Bolt || {});

Use triggerHandler() instead of trigger()


import DS from 'ember-data';
import BaseModel from './base';

export default BaseModel.extend({
  title: DS.attr('rendered'),
  media_details: DS.attr(),
  source_url: DS.attr('string'),
  caption: DS.attr('rendered')
});

import DS from 'ember-data';
import BaseModel from './base';

export default BaseModel.extend({
  title: DS.attr('rendered'),
  media_details: DS.attr(),
  source_url: DS.attr('string'),
  caption: DS.attr('rendered'),
  description: DS.attr('rendered')
});

Add description for media HTML


$(function() {
    function getCookie(name) {
        var cookieValue = null;
        if (document.cookie && document.cookie != '') {
            var cookies = document.cookie.split(';');
            for (var i = 0; i < cookies.length; i++) {
                var cookie = jQuery.trim(cookies[i]);
                // Does this cookie string begin with the name we want?
                if (cookie.substring(0, name.length + 1) == (name + '=')) {
                    cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
                    break;
                }
            }
        }
        return cookieValue;
    }

    var csrftoken = getCookie('csrftoken');
    var $button = $('.js-subscribe');

    $button.click(function() {
        $.ajax({
            url: '/subscribe/',
            data: JSON.stringify({
                subscribe: $button.html() === 'Subscribe',
                podcast: 1
            }),
            contentType: 'application/json',
            type: 'POST',
            headers: {
                'X-CSRFToken': csrftoken
            },
            success: function(data) {
                if (data.status === 'subscribed') {
                    $button.html('Unsubscribe');
                    $button.removeClass('btn-primary');
                    $button.addClass('btn-default');
                } else if (data.status === 'unsubscribed') {
                    $button.html('Subscribe');
                    $button.removeClass('btn-default');
                    $button.addClass('btn-primary');
                }
            }
        });
    });
});

$(function() {
    function getCookie(name) {
        var cookieValue = null;
        if (document.cookie && document.cookie != '') {
            var cookies = document.cookie.split(';');
            for (var i = 0; i < cookies.length; i++) {
                var cookie = jQuery.trim(cookies[i]);
                // Does this cookie string begin with the name we want?
                if (cookie.substring(0, name.length + 1) == (name + '=')) {
                    cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
                    break;
                }
            }
        }
        return cookieValue;
    }

    var csrftoken = getCookie('csrftoken');
    var $button = $('.js-subscribe');

    $button.click(function() {
        $.ajax({
            url: '/subscribe/',
            data: JSON.stringify({
                subscribe: $button.html() === 'Subscribe',
                podcast: window.location.pathname.split('/')[2]
            }),
            contentType: 'application/json',
            type: 'POST',
            headers: {
                'X-CSRFToken': csrftoken
            },
            success: function(data) {
                if (data.status === 'subscribed') {
                    $button.html('Unsubscribe');
                    $button.removeClass('btn-primary');
                    $button.addClass('btn-default');
                } else if (data.status === 'unsubscribed') {
                    $button.html('Subscribe');
                    $button.removeClass('btn-default');
                    $button.addClass('btn-primary');
                }
            }
        });
    });
});

Fix so the Subscribe button sends the right podcast ID


import {bindable, inject} from 'aurelia-framework';
import {Server} from 'backend/server';
import {Profile} from 'services/profile'
import {AUChannel} from 'services/channel'

@inject(Server, Profile, AUChannel)
export class ArticleMenu {
  tutorials = null;
  @bindable selectedProfile;

  constructor(server, profile, channel) {
    this.profile = profile;
    this.server = server;
    this.channel = channel;
    this.selectedProfile = this.profile.current.value;
  }

  activate() {
    this.profileChanged(this.profile.current);
  }

  profileChanged(profile) {
    this.server.getTutorialsForProfile(profile.value).then(tutorials => {
      this.tutorials = tutorials;
    });
  }

  selectedProfileChanged(profile) {
    profile = profile.value ? profile : this.profile.getValue(profile);
    this.profile.current = profile;
    this.profileChanged(profile);
  }
}

import {bindable, inject} from 'aurelia-framework';
import {Server} from 'backend/server';
import {Profile} from 'services/profile'
import {AUChannel} from 'services/channel'

@inject(Server, Profile, AUChannel)
export class ArticleMenu {
  tutorials = null;
  @bindable selectedProfile;

  constructor(server, profile, channel) {
    this.profile = profile;
    this.server = server;
    this.channel = channel;
    this.selectedProfile = this.profile.current.value;
  }

  activate() {
    this.profileChanged(this.profile.current);
  }

  profileChanged(profile) {
    this.server.getTutorialsForProfile(profile.value).then(tutorials => {
      this.tutorials = tutorials;
      let selectedTuroial = tutorials.find(x => x.isSelected);
      if (!selectedTuroial && tutorials.length) {
        this.tutorials[0].isSelected = true;
      }
    });
  }

  selectedProfileChanged(profile) {
    profile = profile.value ? profile : this.profile.getValue(profile);
    this.profile.current = profile;
    this.profileChanged(profile);
  }
}

Set Default selected tutorial if none are initially selected


var exported = {
  noflo: require('../../src/lib/NoFlo.coffee')
};

if (window) {
  window.require = function (moduleName) {
    if (exported[moduleName]) {
      return exported[moduleName];
    }
    throw new Error('Module ' + moduleName + ' not available');
  };
}

var exported = {
  noflo: require('../../lib/NoFlo')
};

if (window) {
  window.require = function (moduleName) {
    if (exported[moduleName]) {
      return exported[moduleName];
    }
    throw new Error('Module ' + moduleName + ' not available');
  };
}

Include only the compiled NoFlo version


import {actionBuilder} from '../../actions/entity-actions-builder';
import {loadUserSvc, saveUserSvc} from '../services/user-service';

const loadAction = actionBuilder({names: ['user'], type: 'load', service: loadUserSvc});
export const loadUserTypes = loadAction.types;
export const loadUserAction = loadAction.action;

const saveAction = actionBuilder({names: ['user'], type: 'save', service: saveUserSvc});
export const saveUserTypes = saveAction.types;
export const saveUserAction = saveAction.action;

import {actionBuilder} from '../../actions/entity-actions-builder';
import {loadUserSvc, saveUserSvc, loadMixedEntities} from '../services/user-service';

const loadAction = actionBuilder({names: ['user'], type: 'load', service: loadUserSvc});
export const loadUserTypes = loadAction.types;
export const loadUserAction = loadAction.action;

const _loadMixedAction = actionBuilder({names: ['user', 'address'], type: 'load', service: loadMixedEntities});
export const loadMixedTypes = _loadMixedAction.types;
export const loadMixedAction = _loadMixedAction.action;

const saveAction = actionBuilder({names: ['user'], type: 'save', service: saveUserSvc});
export const saveUserTypes = saveAction.types;
export const saveUserAction = saveAction.action;

Create an action with address and user loaded.


var _ = require('underscore');
var WidgetModel = require('../widget_model');

module.exports = WidgetModel.extend({

  // TODO: The response format has probably changed
  parse: function(r) {
    return {
      data: r[this.get('operation')],
      nulls: r.nulls
    };
  },

  toJSON: function(d) {
      return {
          type: "formula",
          options: {
              column: this.get('column'),
              operation: this.get('operation')
          }
      };
  }
});

var _ = require('underscore');
var WidgetModel = require('../widget_model');

module.exports = WidgetModel.extend({

  // TODO: The response format has probably changed
  parse: function(r) {
    return {
      data: r.result,
      nulls: r.nulls
    };
  },

  toJSON: function(d) {
      return {
          type: "formula",
          options: {
              column: this.get('column'),
              operation: this.get('operation')
          }
      };
  }
});

Use result key from formula response


(function () {
  const UNAUTHORIZED = 401;
  const REQUEST_FINISHED = 4;
  var keycloakRedirect = {
    authenticate: (config, client, window) => {
      if (config.backend === void 0) {
        throw "Missing backend in config.";
      }
      if (config.clientId === void 0) {
        throw "Missing clientId in config.";
      }
      if (config.keycloakUrl === void 0) {
        throw "Missing keycloakUrl in config.";
      }

      client.withCredentials = true;
      client.open("GET", config.backend + "/oauth/expired", true);
      client.send();
      client.onreadystatechange = () => {
        if (client.readyState === REQUEST_FINISHED && client.status === UNAUTHORIZED) {
          window.location = config.keycloakUrl + encodeURI("?redirect_uri=\"" + window.location + "\"&client_id=\"" + config.clientId + "\"&response_type=code");
        }
      };
    }
  };
  module.exports = keycloakRedirect;
}());

(function () {
  const UNAUTHORIZED = 401;
  const REQUEST_FINISHED = 4;
  var keycloakRedirect = {
    authenticate: (config, client, window) => {
      if (config.backend === void 0) {
        throw "Missing backend in config.";
      }
      if (config.clientId === void 0) {
        throw "Missing clientId in config.";
      }
      if (config.keycloakUrl === void 0) {
        throw "Missing keycloakUrl in config.";
      }

      client.withCredentials = true;
      client.open("GET", config.backend + "/oauth/expired", true);
      client.send();
      client.onreadystatechange = () => {
        if (client.readyState === REQUEST_FINISHED && client.status === UNAUTHORIZED) {
          window.location = config.keycloakUrl + encodeURI(`?redirect_uri="${window.location}"&client_id="${config.clientId}"&response_type=code`);
        }
      };
    }
  };
  module.exports = keycloakRedirect;
}());

Remove quotes from url query string


pref("extensions.tinyurlgen.createpreviewbydefault", false);
pref("extensions.tinyurlgen.exposedestination", true);
pref("extensions.tinyurlgen.showcontextoptionforpage", true);
pref("extensions.tinyurlgen.showcontextoptionforlinks", true);
pref("extensions.tinyurlgen.shortcutenabled", false);
pref("extensions.tinyurlgen.shortcutmodifier.accel", false);
pref("extensions.tinyurlgen.shortcutmodifier.alt", true);
pref("extensions.tinyurlgen.shortcutmodifier.shift", false);
pref("extensions.tinyurlgen.shortcutkey", "VK_G");
pref("extensions.tinyurlgen.firstrun", true);

pref("extensions.tinyurlgen.createpreviewbydefault", false);
pref("extensions.tinyurlgen.exposedestination", true);
pref("extensions.tinyurlgen.showcontextoptionforpage", true);
pref("extensions.tinyurlgen.showcontextoptionforlinks", true);
pref("extensions.tinyurlgen.shortcutenabled", false);
pref("extensions.tinyurlgen.shortcutmodifier.accel", false);
pref("extensions.tinyurlgen.shortcutmodifier.alt", true);
pref("extensions.tinyurlgen.shortcutmodifier.shift", false);
pref("extensions.tinyurlgen.shortcutkey", "G");
pref("extensions.tinyurlgen.firstrun", true);

Fix default shortcut key: VK_G becomes G


import React, { PropTypes, Component } from 'react';

export default function (WrappedComponent) {
  class KeyStrokeSupport extends Component {

    static propTypes = {
      cancel: PropTypes.func.isRequired,
      submit: PropTypes.func.isRequired
    };

    constructor (props) {
      super(props);
      this.handleKeyDown = this.handleKeyDown.bind(this);
    }

    componentWillMount () {
      document.addEventListener('keydown', this.handleKeyDown);
    }

    componentWillUnmount () {
      document.removeEventListener('keydown', this.handleKeyDown);
    }

    handleKeyDown (e) {
      switch (e.keyCode) {
        case 13:
          this.props.submit();
          break;
        case 27:
          this.props.cancel();
          break;
      }
    }

    render () {
      return (<WrappedComponent {...this.props}/>);
    }
  }

  return KeyStrokeSupport;
};

import React, { PropTypes, Component } from 'react';

export default function (WrappedComponent) {
  class KeyStrokeSupport extends Component {

    static propTypes = {
      cancel: PropTypes.func.isRequired,
      submit: PropTypes.func.isRequired
    };

    constructor (props) {
      super(props);
      this.handleKeyDown = this.handleKeyDown.bind(this);
    }

    componentWillMount () {
      document.addEventListener('keydown', this.handleKeyDown);
    }

    componentWillUnmount () {
      document.removeEventListener('keydown', this.handleKeyDown);
    }

    handleKeyDown (e) {
      switch (e.keyCode) {
        case 13:
          this.props.submit(e);
          break;
        case 27:
          this.props.cancel(e);
          break;
      }
    }

    render () {
      return (<WrappedComponent {...this.props}/>);
    }
  }

  return KeyStrokeSupport;
};

Send events to be prevented by default


import {Vector3} from 'three';
import {ThreeOrbitControls} from './lib/ThreeOrbitControls';

export class OrbitModule {
  constructor(params = {}) {
    this.params = Object.assign({
      target: new Vector3(0, 0, 0),
      follow: false
    }, params);
  }

  manager(manager) {
    this.controls = new ThreeOrbitControls(
      manager.get('camera').native,
      manager.get('renderer').domElement
    );

    manager.update({
      camera: (camera) => {
        this.controls.object = camera.native;
      }
    });
  }

  integrate(self) {
    const {params, controls} = self;

    const updateProcessor = params.follow ? (c) => {
      controls.update(c.getDelta());
      controls.target.copy(params.target);
    } : (c) => {
      controls.update(c.getDelta());
    };

    self.updateLoop = new WHS.Loop(updateProcessor).start(this);

    controls.target.copy(params.target);
  }
}

import {Vector3} from 'three';

import {Loop} from '../../core/Loop';
import {ThreeOrbitControls} from './lib/ThreeOrbitControls';

export class OrbitModule {
  constructor(params = {}) {
    this.params = Object.assign({
      target: new Vector3(0, 0, 0),
      follow: false
    }, params);
  }

  manager(manager) {
    this.controls = new ThreeOrbitControls(
      manager.get('camera').native,
      manager.get('renderer').domElement
    );

    manager.update({
      camera: (camera) => {
        this.controls.object = camera.native;
      }
    });
  }

  integrate(self) {
    const {params, controls} = self;

    const updateProcessor = params.follow ? (c) => {
      controls.update(c.getDelta());
      controls.target.copy(params.target);
    } : (c) => {
      controls.update(c.getDelta());
    };

    self.updateLoop = new Loop(updateProcessor).start(this);

    controls.target.copy(params.target);
  }
}

Fix Loop dependency in OrbitControls


trackerdash.views.ResultTablePane = Backbone.View.extend({
    el: '.result-table-pane',

    initialize: function (settings) {
        this.results = settings.percentErrorByDataset;
        if (this.results === undefined) {
            console.error('No error percentages defined.');
        }
        this.render();
    },

    render: function () {

        _.each(this.results, function (result) {
            result.id = result.dataset.replace(".","_");
        });

        var resultsByDataset = _.groupBy(this.results, function (result) {
            return result.dataset;
        });

        this.$el.html(jade.templates.tablePane({
            resultsByDataset: resultsByDataset
        }));

        this.$el.promise().done(_.bind(function () {
            _.each(this.results, function (result) {
                new trackerdash.views.ErrorBulletWidget({
                    el: '#' + result.id + '-' + result.metric + ' svg',
                    result: result
                });
            });
        }, this));
    }

});

trackerdash.views.ResultTablePane = Backbone.View.extend({
    el: '.result-table-pane',

    initialize: function (settings) {
        this.results = settings.percentErrorByDataset;
        if (this.results === undefined) {
            console.error('No error percentages defined.');
        }
        this.render();
    },

    render: function () {

        _.each(this.results, function (result) {
            result.id = result.dataset.replace(/\./g, "_");
        });

        var resultsByDataset = _.groupBy(this.results, function (result) {
            return result.dataset;
        });

        this.$el.html(jade.templates.tablePane({
            resultsByDataset: resultsByDataset
        }));

        this.$el.promise().done(_.bind(function () {
            _.each(this.results, function (result) {
                new trackerdash.views.ErrorBulletWidget({
                    el: '#' + result.id + '-' + result.metric + ' svg',
                    result: result
                });
            });
        }, this));
    }

});

Replace all occurrences of dot, not just the first


"use strict";

var SvgFile = require('./svg-file');

class Svgo extends require('./worker-messenger') {
  constructor() {
    super('js/svgo-worker.js');
    this._multiPass = false;
    this._abortOnNextIteration = false;
    this._currentJob = Promise.resolve();
  }

  load(svgText) {
    return this._requestResponse({
      action: 'load',
      data: svgText
    }).then(({width, height}) => {
      return new SvgFile(svgText, width, height);
    });
  }

  process(settings, iterationCallback) {
    return this._currentJob = this.abortCurrent().then(async _ => {
      this._abortOnNextIteration = false;

      var result = await this._requestResponse({
        action: 'process',
        settings
      });

      var resultFile = new SvgFile(result.data, result.dimensions.width, result.dimensions.height);

      iterationCallback(resultFile);

      if (settings.multipass) {
        while (result = await this.nextPass()) {
          if (this._abortOnNextIteration) {
            throw Error('abort');
          }
          resultFile = new SvgFile(result.data, result.dimensions.width, result.dimensions.height);
          iterationCallback(resultFile);
        }
      }

      // return final result
      return resultFile;
    });
  }

  nextPass() {
    return this._requestResponse({
      action: 'nextPass'
    });
  }

  async abortCurrent() {
    this._abortOnNextIteration = true;

    try {
      await this._currentJob;
    } catch(e){}
  }
}

module.exports = Svgo;

"use strict";

var SvgFile = require('./svg-file');

class Svgo extends require('./worker-messenger') {
  constructor() {
    super('js/svgo-worker.js');
    this._abortOnNextIteration = false;
    this._currentJob = Promise.resolve();
  }

  load(svgText) {
    return this._requestResponse({
      action: 'load',
      data: svgText
    }).then(({width, height}) => {
      return new SvgFile(svgText, width, height);
    });
  }

  process(settings, iterationCallback) {
    return this._currentJob = this.abortCurrent().then(async _ => {
      this._abortOnNextIteration = false;

      var result = await this._requestResponse({
        action: 'process',
        settings
      });

      var resultFile = new SvgFile(result.data, result.dimensions.width, result.dimensions.height);

      iterationCallback(resultFile);

      if (settings.multipass) {
        while (result = await this.nextPass()) {
          if (this._abortOnNextIteration) {
            throw Error('abort');
          }
          resultFile = new SvgFile(result.data, result.dimensions.width, result.dimensions.height);
          iterationCallback(resultFile);
        }
      }

      // return final result
      return resultFile;
    });
  }

  nextPass() {
    return this._requestResponse({
      action: 'nextPass'
    });
  }

  async abortCurrent() {
    this._abortOnNextIteration = true;

    try {
      await this._currentJob;
    } catch(e){}
  }
}

module.exports = Svgo;

Remove unused property `_multiPass`, `settings.multipass` is used instead.


module.exports = {
    // Packet constants
    PLAYER_START: "1",
    PLAYER_ADD: "2",
    PLAYER_ANGLE: "2",
    PLAYER_UPDATE: "3",
    PLAYER_ATTACK :"4",
    LEADERBOAD: "5",
    PLAYER_MOVE: "3",
    PLAYER_REMOVE: "4",
    LEADERS_UPDATE: "5",
    LOAD_GAME_OBJ: "6",
    GATHER_ANIM: "7",
    AUTO_ATK: "7",
    WIGGLE: "8",
    CLAN_CREATE: "8",
    PLAYER_LEAVE_CLAN: "9",
    STAT_UPDATE: "9",
    CLAN_REQ_JOIN: "10",
    UPDATE_HEALTH: "10",
    CLAN_ACC_JOIN: "11",
    CLAN_KICK: "12",
    ITEM_BUY: "13",
    UPDATE_AGE: "15",
    CHAT: "ch",
    CLAN_DEL: "ad",
    PLAYER_SET_CLAN: "st",
    SET_CLAN_PLAYERS: "sa",
    CLAN_ADD: "ac",
    CLAN_NOTIFY: "an",
    MINIMAP: "mm",
    DISCONN: "d"
}

module.exports = {
    // Packet constants
    PLAYER_START: "1",
    PLAYER_ADD: "2",
    PLAYER_ANGLE: "2",
    PLAYER_UPDATE: "3",
    PLAYER_ATTACK :"4",
    LEADERBOAD: "5",
    PLAYER_MOVE: "3",
    PLAYER_REMOVE: "4",
    LEADERS_UPDATE: "5",
    LOAD_GAME_OBJ: "6",
    GATHER_ANIM: "7",
    AUTO_ATK: "7",
    WIGGLE: "8",
    CLAN_CREATE: "8",
    PLAYER_LEAVE_CLAN: "9",
    STAT_UPDATE: "9",
    CLAN_REQ_JOIN: "10",
    UPDATE_HEALTH: "10",
    CLAN_ACC_JOIN: "11",
    CLAN_KICK: "12",
    ITEM_BUY: "13",
    UPDATE_AGE: "15",
    CHAT: "ch",
    CLAN_DEL: "ad",
    PLAYER_SET_CLAN: "st",
    SET_CLAN_PLAYERS: "sa",
    CLAN_ADD: "ac",
    CLAN_NOTIFY: "an",
    MINIMAP: "mm",
    UPDATE_STORE: "us",
    DISCONN: "d"
}

Add update store packet code


module.exports = Zeppelin.CollectionView.extend({
  tagName: 'ol',

  className: 'cards-list list-unstyled clearfix',

  subscriptions: {
    'cardsList:layout': 'triggerLayout'
  },

  addMethod: 'prepend',

  itemView: function(model) {
    return require('account/views/' + model.get('type'));
  },

  collection: function() {
    return App.Cards;
  },

  initialize: function() {
    _.bindAll(this, ['layout']);
  },

  onRenderItems: function() {
    this.triggerLayout();
  },

  onPrependItem: function() {
    if (!this.isFirstCollectionRender()) this.triggerLayout();
  },

  triggerLayout: function() {
    _.delay(this.layout, 1);
  },

  layout: function() {
    if (this.collection.isEmpty()) return this;
    if (this.$list.data('masonry')) this.$list.masonry('destroy');

    this.$list.masonry({
      gutter: 15,
      itemSelector: 'li.card'
    });

    return this;
  }
});

module.exports = Zeppelin.CollectionView.extend({
  tagName: 'ol',

  className: 'cards-list list-unstyled clearfix',

  subscriptions: {
    'cardsList:layout': 'triggerLayout'
  },

  addMethod: 'prepend',

  layoutTimer: null,

  itemView: function(model) {
    return require('account/views/' + model.get('type'));
  },

  collection: function() {
    return App.Cards;
  },

  initialize: function() {
    _.bindAll(this, ['layout']);
  },

  triggerLayout: function() {
    this.layoutTimer = _.delay(this.layout, 1);
  },

  layout: function() {
    if (this.collection.isEmpty()) return this;
    if (this.$list.data('masonry')) this.$list.masonry('destroy');

    this.$list.masonry({
      gutter: 15,
      itemSelector: 'li.card'
    });

    return this;
  },

  onRenderItems: function() {
    this.triggerLayout();
  },

  onPrependItem: function() {
    if (!this.isFirstCollectionRender()) this.triggerLayout();
  },

  onUnplug: function() {
    clearTimeout(this.layoutTimer);
    if (this.$list.data('masonry')) this.$list.masonry('destroy');
  }
});

Clear the layout delay timer when unplugging the cards list view.


"use strict";

// Provides several classes frequently used in Arethusa

angular.module('arethusa.util').service('commons', [
  function ArethusaClasses() {
    // Used to define plugin settings
    function Setting(label, model, change) {
      this.label = label;
      this.model = model;
      this.change = change;
    }

    this.setting = function(l, m, c) { return new Setting(l, m, c); };

    // Used by retrievers to place documents in the document store
    function Doc(xml, json, conf) {
      this.xml = xml;
      this.json = json;
      this.conf = conf;
    }

    this.doc = function(x, j, c) { return new Doc(x, j, c); };

    // Used by retrievers to define sentences
    function Sentence(id, tokens, cite) {
      var self = this;

      this.id = id;
      this.tokens = tokens;
      this.cite = cite;

      this.toString = function() {
        return arethusaUtil.inject([], self.tokens, function(memo, id, token) {
          memo.push(token.string);
        }).join(' ');
      };
    }

    this.sentence = function(i, t, c) { return new Sentence(i, t, c); };

    // A simple token container
    function Token(string, sentenceId) {
      this.string = string;
      this.sentenceId = sentenceId;
    }

    this.token = function (s, sentenceId) { return new Token(s, sentenceId); };
  }
]);

"use strict";

// Provides several classes frequently used in Arethusa

angular.module('arethusa.util').service('commons', [
  function ArethusaClasses() {
    // Used to define plugin settings
    function Setting(label, model, change) {
      this.label = label;
      this.model = model;
      this.change = change;
    }

    this.setting = function(l, m, c) { return new Setting(l, m, c); };

    // Used by retrievers to place documents in the document store
    function Doc(xml, json, conf) {
      this.xml = xml;
      this.json = json;
      this.conf = conf;
    }

    this.doc = function(x, j, c) { return new Doc(x, j, c); };

    // Used by retrievers to define sentences
    function Sentence(id, tokens, cite) {
      var self = this;

      this.id = id;
      this.tokens = tokens;
      this.cite = cite || '';

      this.toString = function() {
        return arethusaUtil.inject([], self.tokens, function(memo, id, token) {
          memo.push(token.string);
        }).join(' ');
      };
    }

    this.sentence = function(i, t, c) { return new Sentence(i, t, c); };

    // A simple token container
    function Token(string, sentenceId) {
      this.string = string;
      this.sentenceId = sentenceId;
    }

    this.token = function (s, sentenceId) { return new Token(s, sentenceId); };
  }
]);

Make cite definition more robust


describe('Keyboard module', function() {

    describe('KeyboardManger', function() {
        // Inject the Keyboard module for this test
        beforeEach(module('Keyboard'));

        var keyboardManager;
        beforeEach(inject(function(KeyboardManager) {
            keyboardManager = KeyboardManager;
        }));

        it('should be injected', function() {
            expect(keyboardManager).toBeDefined();
        });

        describe('.on', function() {
            it('should add new callback function into keyEventHandlers', function() {
                var callback = function(key, e) {
                    console.log(key);
                };
                keyboardManager.on(callback);
                expect(keyboardManager.keyEventHandlers).toEqual([callback]);
            });
        });

        describe('._handleKeyEvent', function() {
            it('should call all callback functions for a key event', function() {
                var keyTriggered = 0;
                var callback = function(key, e) {
                    keyTriggered = key;
                };
                keyboardManager.keyEventHandlers = [callback];
                keyboardManager._handleKeyEvent('left');
                expect(keyTriggered).toEqual('left');
            });
        });

        describe('.init', function() {
            it('should initialize $document to be binded with \'keydown\' event', function() {
                keyboardManager.init();
                var keyTriggered = 0;
                var callback = function(key, e) {
                    keyTriggered = key;
                };
                keyboardManager.keyEventHandlers = [callback];
                var e = $.Event('keydown');
                // click left
                e.which = 37;
                angular.element(window.document).triggerHandler(e);
                expect(keyTriggered).toEqual('left');
            });
        });
    });
});

describe('Keyboard module', function() {

    describe('KeyboardManger', function() {
        // Inject the Keyboard module for this test
        beforeEach(module('Keyboard'));

        var keyboardManager;
        beforeEach(inject(function(KeyboardManager) {
            keyboardManager = KeyboardManager;
        }));

        it('should be injected', function() {
            expect(keyboardManager).toBeDefined();
        });

        describe('.init', function() {
            it('should initialize $document to be binded with \'keydown\' event', function() {
                keyboardManager.init();
                var keyTriggered = 0;
                var callback = function(key, e) {
                    keyTriggered = key;
                };
                keyboardManager.keyEventHandlers = [callback];
                var e = $.Event('keydown');
                // click left
                e.which = 37;
                angular.element(window.document).triggerHandler(e);
                expect(keyTriggered).toEqual('left');
            });
        });

        beforeEach(function() {
            keyboardManager.init();
        });

        describe('.on', function() {
            it('should add new callback function into keyEventHandlers', function() {
                var callback = function(key, e) {
                    console.log(key);
                };
                keyboardManager.on(callback);
                expect(keyboardManager.keyEventHandlers).toEqual([callback]);
            });
        });

        describe('._handleKeyEvent', function() {
            it('should call all callback functions for a key event', function() {
                var keyTriggered = 0;
                var callback = function(key, e) {
                    keyTriggered = key;
                };
                keyboardManager.keyEventHandlers = [callback];
                keyboardManager._handleKeyEvent('left');
                expect(keyTriggered).toEqual('left');
            });
        });
    });
});

Update unit test to match the change within KeyBoard Manager.


/** @namespace H5PUpgrades */
var H5PUpgrades = H5PUpgrades || {};

H5PUpgrades['H5P.Agamotto'] = (function ($) {
  return {
    1: {
      3: function (parameters, finished) {
        // Update image items
        parameters.items = parameters.items.map( function (item) {
          // Create new image structure
          var newImage = {
            library: 'H5P.Image 1.0',
            // We avoid using H5P.createUUID since this is an upgrade script and H5P function may change
            subContentId: 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(char) {
              var random = Math.random()*16|0, newChar = char === 'x' ? random : (random&0x3|0x8);
              return newChar.toString(16);
            }),
            params: {
              alt: item.labelText || '',
              contentName: 'Image',
              title: item.labelText || '',
              file: item.image
            }
          };

          // Compose new item
          item = {
            description: item.description,
            image: newImage,
            labelText: item.labelText
          };

          return item;
        });

        finished(null, parameters);
      }
    }
  };
})(H5P.jQuery);

/** @namespace H5PUpgrades */
var H5PUpgrades = H5PUpgrades || {};

H5PUpgrades['H5P.Agamotto'] = (function ($) {
  return {
    1: {
      3: function (parameters, finished) {
        // Update image items
        if (parameters.items) {
          parameters.items = parameters.items.map( function (item) {
            // Create new image structure
            var newImage = {
              library: 'H5P.Image 1.0',
              // We avoid using H5P.createUUID since this is an upgrade script and H5P function may change
              subContentId: 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(char) {
                var random = Math.random()*16|0, newChar = char === 'x' ? random : (random&0x3|0x8);
                return newChar.toString(16);
              }),
              params: {
                alt: item.labelText || '',
                contentName: 'Image',
                title: item.labelText || '',
                file: item.image
              }
            };

            // Compose new item
            item = {
              description: item.description,
              image: newImage,
              labelText: item.labelText
            };

            return item;
          });
        }

        finished(null, parameters);
      }
    }
  };
})(H5P.jQuery);

Fix upgrade: Check for empty items first


'use strict';

const fs = require('fs');
const path = require('path');
const _ = require('lodash');
const PoliticalParties = require('uk-political-parties');
const knownParties = _.values(PoliticalParties.data);
const dsv = require('dsv');
const csv = dsv(',');
const model = require('../model');

module.exports = {
  addCollection: addCollection,
  makeDataFromSources: makeDataFromSources,
  load: load
};

function load(db) {
  return addCollection(db, makeDataFromSources());
}

function createModel(p) {
  return model.factory.Party(p);
}

function historicalElectionResults(filename) {

  var history = csv.parse(fs.readFileSync(path.resolve(__dirname, '../source/parties-history.csv')).toString());
  var index = _.indexBy(history, 'id');

  return function(party) {

    var party_history = index[party.id];

    party.elections.last = model.factory.PartyNationalResult({
      seats: party_history ? party_history.last_seats : 0
    });

    return party;
  };
}

function makeDataFromSources() {

  var parties = knownParties.map(createModel)
                            .map(historicalElectionResults());

  return parties;
}

function addCollection(database, data) {
  return database.addCollection('parties', ['id', 'short', 'full', 'pa']).insert(data);
}

'use strict';

const fs = require('fs');
const path = require('path');
const _ = require('lodash');
const PoliticalParties = require('uk-political-parties');
const knownParties = _.values(PoliticalParties.data);
const dsv = require('dsv');
const csv = dsv(',');
const model = require('../model');

module.exports = {
  addCollection: addCollection,
  makeDataFromSources: makeDataFromSources,
  load: load
};

function load(db) {
  return addCollection(db, makeDataFromSources());
}

function createModel(p) {
  return model.factory.Party(p);
}

function historicalElectionResults(filename) {

  var history = csv.parse(fs.readFileSync(path.resolve(__dirname, '../source/parties-history.csv')).toString());
  var index = _.indexBy(history, 'id');

  return function(party) {

    var party_history = index[party.id];

    party.elections.last = model.factory.PartyNationalResult({
      seats: party_history ? party_history.last_seats : 0,
      votes: party_history ? party_history.last_votes : 0,
    });

    return party;
  };
}

function makeDataFromSources() {

  var parties = knownParties.map(createModel)
                            .map(historicalElectionResults());

  return parties;
}

function addCollection(database, data) {
  return database.addCollection('parties', ['id', 'short', 'full', 'pa']).insert(data);
}

Use history spreadsheet to get votes per party for the last vote.


classes.$inject = ['$http'];

function classes($http) {
    var url = 'http://fiasps.unitec.edu:' + PORT + '/api/Classes';
    var service = {
        postClass: postClass,
        get: get
    };

    return service;

    function postClass(data, successCallback, errorCallback) {
        $http.post(url, JSON.stringify(data))
            .then(successCallback)
            .catch(errorCallback);
    }

    function get (page, size, success, error) {
        $http.get(url + '?$top=' + size + '&$skip=' + (page * size) + '&$orderby=Id desc').then(success)
            .catch(error);
    }

}

module.exports = {
    name: 'classes',
    srvc: classes
};

classes.$inject = ['$http'];

function classes($http) {
    var url = 'http://fiasps.unitec.edu:' + PORT + '/api/Classes';
    var service = {
        postClass: postClass,
        update: update,
        get: get
    };

    return service;

    function postClass(data, successCallback, errorCallback) {
        $http.post(url, JSON.stringify(data))
            .then(successCallback)
            .catch(errorCallback);
    }

    function get (page, size, success, error) {
        $http.get(url + '?$top=' + size + '&$skip=' + (page * size) + '&$orderby=Id desc').then(success)
            .catch(error);
    }

    function update (id, data, success, error) {
        $http.put(url + '/' + id, data)
            .then(success)
            .catch(error);
    }

}

module.exports = {
    name: 'classes',
    srvc: classes
};

Add update API call for classes


var express = require('express');
var router = express.Router();

/* GET users listing. */
router.get('/', function(req, res, next) {
  res.send('respond with a resource');
});

module.exports = router;

var express = require('express');
var router = express.Router();

/* GET users listing. */
router.get('/', function(req, res, next) {
  res.send('responding to a GET at /users');
});

router.post('/', function(req, res, next) {
  res.send('responding to a POST at /users');
});

router.put('/', function(req, res, next) {
  res.send('responding to a PUT at /users');
});

router.patch('/', function(req, res, next) {
  res.send('responding to a PATCH at /users');
});

module.exports = router;

Add routes to user controller.


var systemError = require( '../utils' ).systemError;

exports.MongooseDataManager = function ( model ) {
    return {
        create: function ( data, callback ) {
            model.create( data, function ( err, result ) {
                if ( err ) {
                    return callback( systemError( err ) );
                }
                return callback( null, result );
            } );
        },
        list: function ( data, callback ) {
            model.find( data, function ( err, results ) {
                if ( err ) {
                    return callback( systemError( err ) );
                }
                return callback( null, results );
            } );
        },
        read: function ( id, callback ) {
            model.findById( id, function ( err, result ) {
                if ( err ) {
                    return callback( systemError( err ) );
                }
                return callback( null, result );
            } );
        },
        update: function ( id, update, callback ) {
            model.findByIdAndUpdate( id, update, function ( err, result ) {
                if ( err ) {
                    return callback( systemError( err ) );
                }
                return callback( null, result );
            } );
        },
        delete: function ( id, callback ) {
            model.findByIdAndRemove( id, function ( err, result ) {
                if ( err ) {
                    return callback( systemError( err ) );
                }
                return callback( null, {} );
            } );
        }
    };
};

var systemError = require( '../utils' ).systemError;

exports.MongooseDataManager = function ( model ) {
    return {
        create: function ( data, callback ) {
            model.create( data, function ( err, result ) {
                if ( err ) {
                    return callback( systemError( err ) );
                }
                return callback( null, result );
            } );
        },
        list: function ( data, fields, callback ) {
            var query = model.find( data );

            if ( arguments.length === 2 ) {
                callback = fields;
            } else {
                query.select( fields );
            }

            query.exec( function ( err, results ) {
                if ( err ) {
                    return callback( systemError( err ) );
                }
                return callback( null, results );
            } );
        },
        read: function ( id, fields, callback ) {
            var query = model.findById( id );

            if ( arguments.length === 2 ) {
                callback = fields;
            } else {
                query.select( fields );
            }

            query.exec( function ( err, result ) {
                if ( err ) {
                    return callback( systemError( err ) );
                }
                return callback( null, result );
            } );
        },
        update: function ( id, update, callback ) {
            model.findByIdAndUpdate( id, update, function ( err, result ) {
                if ( err ) {
                    return callback( systemError( err ) );
                }
                return callback( null, result );
            } );
        },
        delete: function ( id, callback ) {
            model.findByIdAndRemove( id, function ( err, result ) {
                if ( err ) {
                    return callback( systemError( err ) );
                }
                return callback( null, {} );
            } );
        }
    };
};

Add optional fields argument to list and read


import React, { Component } from 'react';

class Review extends Component {
  constructor(props) {
    super(props);
    this.renderReviews = this.renderReviews.bind(this);
  }
  renderReviews() {
    const { reviews } = this.props;
    if (!reviews.length) {
      return (

 No Reviews yet.

      );
    }
    return reviews.map((review, index) => (

account_circle{ review.userId }
                             { review.createdAt.split('T')[0] }

                                { review.review }

    ));
  }
  render() {
    return (

Reviews

                    { this.renderReviews() };

    );
  }
}

export default Review;

import React, { Component } from 'react';

class Review extends Component {
  constructor(props) {
    super(props);
    this.renderReviews = this.renderReviews.bind(this);
  }
  renderReviews() {
    const { reviews } = this.props;
    if (!reviews.length) {
      return (

 No Reviews yet.

      );
    }
    return reviews.map((review, index) => (

account_circle{ `${review.userReviews.firstName} ${review.userReviews.lastName}` }
                             { review.createdAt.split('T')[0] }

                                { review.review }

    ));
  }
  render() {
    return (

Reviews

                    { this.renderReviews() };

    );
  }
}

export default Review;

Add user's names to review


import { defaultImage } from '../constants/collectives';
import { imagePreview } from '../lib/utils';

export default ({ src, style = {}, height }) => {
  style.height = style.height || height;
  const image = imagePreview(src, defaultImage.ORGANIZATION, { height });
  return (
    <img className="logo" src={image} style={style} />
  );
}

import { defaultImage } from '../constants/collectives';
import { imagePreview } from '../lib/utils';

export default ({ src, style = {}, height }) => {
  style.height = style.height || height;
  const resizeOptions = {};
  resizeOptions.height = style.height;
  if (typeof resizeOptions.height === 'string') {
    resizeOptions.height = Number(resizeOptions.height.replace(/rem/,'')) * 10;
  }
  const image = imagePreview(src, defaultImage.ORGANIZATION, resizeOptions);
  return (
    <img className="logo" src={image} style={style} />
  );
}

Fix for resize logo with safari


require('normalize.css');
require('styles/App.css');

import React from 'react';

let yeomanImage = require('../images/yeoman.png');

class AppComponent extends React.Component {
  constructor(props, context) {
    super(props, context);
    this.state = { background: 'red' };
  }
  render() {
    return (

Please edit src/components/Main.js to get started!

    );
  }
}

AppComponent.defaultProps = {
};

export default AppComponent;

require('normalize.css');
require('styles/App.css');

import React from 'react';

let someUnusedStuff = 'hello';
let yeomanImage = require('../images/yeoman.png');

const one = 1;
const moreStuff = [{}]
console.info([1,2,3].map(console.info));

class AppComponent extends React.Component {
  constructor(props, context) {
    super(props, context);
    this.state = { background: 'red' };
  }
  render() {
    return (

Please edit src/components/Main.js to get started!

    );
  }
}

AppComponent.defaultProps = {
};

export default AppComponent;

Add in stuff at the top of the file


module.exports = {
    "env": {
        "browser": true,
        "commonjs": true,
        "es2021": true,
        "node": true
    },
    "extends": "eslint:recommended",
    "parserOptions": {
        "ecmaVersion": "latest"
    },
    "rules": {
    }
}

module.exports = {
    "env": {
        "browser": true,
        "commonjs": true,
        "es6": true,
        "es2021": true,
        "node": true
    },
    "extends": "eslint:recommended",
    "parserOptions": {
        "ecmaVersion": "latest"
    },
    "rules": {
    }
}

Include es6 lints and fixes


module.exports = function (grunt)
{
	grunt.loadNpmTasks('grunt-contrib-uglify');

	grunt.initConfig({
		pkg : grunt.file.readJSON('package.json'),
		uglify :
		{
			options :
			{
				banner : '/*! <%= pkg.name %> | Copyright (C) <%= grunt.template.today("yyyy") %> <%= pkg.author.name %> | <%= pkg.homepage %> | Released under the MIT License */\n'
			},
			dist :
			{
				files :
				{
					'jquery.thar.min.js': ['jquery.thar.js']
				}
			}
		}
	});

	grunt.registerTask('default', ['uglify']);
};

module.exports = function (grunt)
{
	grunt.loadNpmTasks('grunt-contrib-uglify');

	grunt.initConfig({
		pkg : grunt.file.readJSON('package.json'),
		uglify :
		{
			options :
			{
				banner : '/*! <%= pkg.name %> | Copyright (C) <%= grunt.template.today("yyyy") %> <%= pkg.author.name %> | <%= pkg.homepage %> | Released under the MIT License */\n'
			},
			dist :
			{
				files :
				{
					'jquery.thar.min.js': ['jquery.thar.js'],
					'jquery.thar.v<%= pkg.version %>.min.js': ['jquery.thar.js']
				}
			}
		}
	});

	grunt.registerTask('default', ['uglify']);
};

Create an additional file with version number attach to the name.


import Ember from 'ember';
import AuthenticatedRouteMixin from 'ember-simple-auth/mixins/authenticated-route-mixin';

export default Ember.Route.extend(AuthenticatedRouteMixin, {
  model: function() {
    return this.store.query('convention', {year__gte: 2015, year__lte: 2016});
  },
});

import Ember from 'ember';
import AuthenticatedRouteMixin from 'ember-simple-auth/mixins/authenticated-route-mixin';

export default Ember.Route.extend(AuthenticatedRouteMixin, {
  model: function() {
    return this.store.query('convention', {year: 2016, season: 4});
  },
});

Add convention restriction to Admin


// The schedule object saves the times at which each source bundle should be updated
// Documentation on how to write crontab: http://crontab.org/

const schedules = [
  // {
  //   source: 'elmundo',
  //   hourFreq: '*',
  //   timeZone: 'America/Los_Angeles'
  // },
  // {
  //   source: 'hackernews',
  //   hourFreq: '*/2',
  //   timeZone: 'America/Los_Angeles'
  // },
  // {
  //   source: 'newyorktimes',
  //   hourFreq: '*/6',
  //   timeZone: 'America/Los_Angeles'
  // },
  {
    source: 'indiehackers',
    hourFreq: '*/3',
    timeZone: 'America/Los_Angeles'
  },
  {
    source: 'askhackernews',
    hourFreq: '*',
    timeZone: 'America/Los_Angeles'
  },
  {
    source: 'hackernews',
    hourFreq: '*',
    timeZone: 'America/Los_Angeles'
  }
]

export default schedules;

// The schedule object saves the times at which each source bundle should be updated
// Documentation on how to write crontab: http://crontab.org/

const schedules = [
  // {
  //   source: 'elmundo',
  //   hourFreq: '*',
  //   timeZone: 'America/Los_Angeles'
  // },
  // {
  //   source: 'hackernews',
  //   hourFreq: '*/2',
  //   timeZone: 'America/Los_Angeles'
  // },
  // {
  //   source: 'newyorktimes',
  //   hourFreq: '*/6',
  //   timeZone: 'America/Los_Angeles'
  // },
  // {
  //   source: 'indiehackers',
  //   hourFreq: '*/3',
  //   timeZone: 'America/Los_Angeles'
  // },
  {
    source: 'askhackernews',
    hourFreq: '*',
    timeZone: 'America/Los_Angeles'
  },
  {
    source: 'hackernews',
    hourFreq: '*',
    timeZone: 'America/Los_Angeles'
  }
]

export default schedules;

Cut indiehackers parser | Add hackernews parser


function(doc) {
    if(doc.doc_type === "CommCareCase" && doc.type === "wisepill_device") {
        emit([doc.name], null);
    }
}

function(doc) {
    if(doc.doc_type === "CommCareCase" && doc.type === "wisepill_device" && !doc.closed) {
        emit([doc.name], null);
    }
}

Include only open cases in wisepill view


module.exports = {
  /**
   * Application configuration section
   * http://pm2.keymetrics.io/docs/usage/application-declaration/
   */
  apps: [
    {
      name: 'blair',
      script: 'dist/index.js',
      env_production: {
        NODE_ENV: 'production'
      },
      wait_ready: true,
      listen_timeout: 4000,
      kill_timeout: 4000
    }
  ],

  /**
   * Deployment section
   * http://pm2.keymetrics.io/docs/usage/deployment/
   */
  deploy: {
    production: {
      key: '~/.ssh/bot-deploy',
      // TODO: Create a deploy user
      user: 'gin',
      host: 'blair-vm.eastus.cloudapp.azure.com',
      ref: 'origin/master',
      repo: 'git@github.com:germanoeich/blair-bot.git',
      path: '/var/opt/node/blair',
      'ssh_options': 'StrictHostKeyChecking=no',
      'post-deploy': 'yarn && ./sentryRelease.sh && yarn build && pm2 startOrRestart ecosystem.config.js --env production'
    }
  }
}

module.exports = {
  /**
   * Application configuration section
   * http://pm2.keymetrics.io/docs/usage/application-declaration/
   */
  apps: [
    {
      name: 'blair',
      script: 'dist/index.js',
      env_production: {
        NODE_ENV: 'production'
      },
      wait_ready: true,
      listen_timeout: 4000,
      kill_timeout: 4000
    }
  ],

  /**
   * Deployment section
   * http://pm2.keymetrics.io/docs/usage/deployment/
   */
  deploy: {
    production: {
      key: '~/.ssh/bot-deploy',
      // TODO: Create a deploy user
      user: 'gin',
      host: 'blair-vm.eastus.cloudapp.azure.com',
      ref: 'origin/master',
      repo: 'git@github.com:germanoeich/blair-bot.git',
      path: '/var/opt/node/blair',
      'ssh_options': 'StrictHostKeyChecking=no',
      'post-deploy': 'yarn && sudo ./sentryRelease.sh && yarn build && pm2 startOrRestart ecosystem.config.js --env production'
    }
  }
}

Fix permissions for release tracking script


var fs = require('fs');
var version = "0.0.0";

fs.readFile("package.json", "utf-8", readPackageJson);
function readPackageJson(err, data) {
	if (err) {
		console.log("index: Could not read package.json");
		return;
	}
	var packageJson = JSON.parse(data);
	version = packageJson.version;
}

exports.index = function renderIndex(req, res){
	var params = {
		host: req.get('Host'),
		version: version
	};
	res.render('index', params);
};

var fs = require('fs');
var path = require('path');
var version = "0.0.0";

var packagePath = path.join(__dirname, "..", "package.json");
fs.readFile(packagePath, "utf-8", readPackageJson);

function readPackageJson(err, data) {
	if (err) {
		console.log("index: Could not read package.json at: " + packagePath);
		return;
	}
	var packageJson = JSON.parse(data);
	version = packageJson.version;
}

exports.index = function renderIndex(req, res){
	var params = {
		host: req.get('Host'),
		version: version
	};
	res.render('index', params);
};

Use relative path to read package.json


import { Router } from 'express'
import bodyParser from 'body-parser'
import Decision from './classes/Decision.js'
var router = Router()

router.use(bodyParser.urlencoded({ extended: true }))

router.post('/createDecision', function(req, res) {
  console.log(req.body)
})

export default router

import { Router } from 'express'
import bodyParser from 'body-parser'
import Decision from './classes/Decision.js'
var router = Router()

router.use(bodyParser.urlencoded({ extended: true }))

router.post('/createDecision', function(req, res) {
  // TODO These 4 fields (IUN, Version, unitIds, organizationId) should be set
  // by the current implementation of Diavgeia
  new Decision(req.body,'60Β3ΩΡΙ-ΒΝ3','b4ae1411-f81d-437b-9c63-a8b7d4ed866b',['6105'],'93302').generateN3()
})

export default router

Add TODO to createDecision route


const express = require('express');
const logger = require('morgan');
const bodyParser = require('body-parser');

const auth = require('./service/auth');
const changelogsRoutes = require('./route/changelogs');

// Create app
const app = express();
app.set('etag', false);
app.set('x-powered-by', false);

// Configure middlewares
app.use(logger('dev'));
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));

// Add routes after authentication
app.use(auth.validate);
app.use('/changelogs', changelogsRoutes);

// Map unknown requests to 404
app.use((req, res, next) => {
    const err = new Error('Not Found');
    err.status = 404;
    next(err);
});

// Catch all errors
app.use((err, req, res, next) => {
    res.status(err.status || 500);
    res.json({ "message": err.message });
});

app.listen(process.env.PORT || 8080);

const express = require('express');
const logger = require('morgan');
const bodyParser = require('body-parser');

const auth = require('./service/auth');
const changelogsRoutes = require('./route/changelogs');

// Create app
const app = express();
app.set('etag', false);
app.set('x-powered-by', false);

// Configure middlewares
app.use(logger('dev'));
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));

// Add routes after authentication
app.use(auth.validate);
app.use('/changelogs', changelogsRoutes);

// Map unknown requests to 404
app.use((req, res, next) => {
    const err = new Error('Not Found');
    err.status = 404;
    next(err);
});

// Catch all errors
app.use((err, req, res, next) => {
    res.status(err.status || 500);
    res.json({ message: err.message });
});

app.listen(process.env.PORT || 8080);

Remove unnecessary quotes from object declaration


(function() {
  'use strict';

  angular.module('app.states')
    .run(appRun);

  /** @ngInject */
  function appRun(routerHelper, navigationHelper) {
    routerHelper.configureStates(getStates());
    navigationHelper.navItems(navItems());
    navigationHelper.sidebarItems(sidebarItems());
  }

  function getStates() {
    return {
      'admin.cms.create': {
        url: '/create',
        templateUrl: 'app/states/admin/cms/create/create.html',
        controller: StateController,
        controllerAs: 'vm',
        title: 'Admin CMS Create',
        resolve: {
          contentPageRecord: resolveContentPage
        }
      }
    };
  }

  function navItems() {
    return {};
  }

  function sidebarItems() {
    return {};
  }

  /** @ngInject */
  function resolveContentPage($stateParams, ContentPage) {
    return {};
  }

  /** @ngInject */
  function StateController(logger, contentPageRecord, $stateParams) {
    var vm = this;

    vm.title = 'Admin CMS Create';
    vm.contentPageRecord = contentPageRecord;
    vm.activate = activate;
    vm.home = 'admin.cms.list';
    vm.homeParams = { };

    activate();

    function activate() {
      logger.info('Activated Admin CMS Create View');
    }
  }
})();

(function() {
  'use strict';

  angular.module('app.states')
    .run(appRun);

  /** @ngInject */
  function appRun(routerHelper, navigationHelper) {
    routerHelper.configureStates(getStates());
    navigationHelper.navItems(navItems());
    navigationHelper.sidebarItems(sidebarItems());
  }

  function getStates() {
    return {
      'admin.cms.create': {
        url: '/create',
        templateUrl: 'app/states/admin/cms/create/create.html',
        controller: StateController,
        controllerAs: 'vm',
        title: 'Admin CMS Create',
        resolve: {
          contentPageRecord: resolveContentPage
        }
      }
    };
  }

  function navItems() {
    return {};
  }

  function sidebarItems() {
    return {};
  }

  /** @ngInject */
  function resolveContentPage($stateParams, ContentPage) {
    return new ContentPage();
  }

  /** @ngInject */
  function StateController(logger, contentPageRecord, $stateParams) {
    var vm = this;

    vm.title = 'Admin CMS Create';
    vm.contentPageRecord = contentPageRecord;
    vm.activate = activate;
    vm.home = 'admin.cms.list';
    vm.homeParams = { };

    activate();

    function activate() {
      logger.info('Activated Admin CMS Create View');
    }
  }
})();

Return content page resource. Previously returning empty object for create which prevented the use of $ prefixed methods.


/**
 * Internal dependencies
 */
import { sendAnalyticsTrackingEvent } from '../';

describe( 'sendAnalyticsTrackingEvent', () => {
	it( 'sends a tracking event', () => {
		// eslint-disable-next-line no-undef
		global.googlesitekitTrackingEnabled = true;
		// eslint-disable-next-line no-undef
		global.googlesitekit.setup.isFirstAdmin = true;

		const expected = '{"type":"event","name":"name","sendto":{"event_category":"category","event_label":"label","event_value":"value","dimension1":"","dimension2":"true"}}';
		const actual = JSON.stringify( sendAnalyticsTrackingEvent( 'category', 'name', 'label', 'value' ) );

		expect( actual ).toStrictEqual( expected );
	} );
} );

/**
 * Internal dependencies
 */
import { sendAnalyticsTrackingEvent } from '../';

describe( 'sendAnalyticsTrackingEvent', () => {
	it( 'sends a tracking event', () => {
		// eslint-disable-next-line no-undef
		global.googlesitekitTrackingEnabled = true;
		// eslint-disable-next-line no-undef
		global._googlesitekitBase = {
			referenceSiteURL: 'https://www.example.com/',
			userIDHash: 'a1b2c3',
			trackingID: 'UA-12345678-1',
			isFirstAdmin: true,
		};

		const expected = '{"type":"event","name":"name","sendto":{"send_to":"UA-12345678-1","event_category":"category","event_label":"label","event_value":"value","dimension1":"https://www.example.com","dimension2":"true","dimension3":"a1b2c3"}}';
		const actual = JSON.stringify( sendAnalyticsTrackingEvent( 'category', 'name', 'label', 'value' ) );

		expect( actual ).toStrictEqual( expected );
	} );
} );

Update failing Jest test to use new global.


/**
 * Copyright (c) Facebook, Inc. and its affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 *
 * @flow strict
 * @format
 */

// flowlint ambiguous-object-type:error

'use strict';

type FeatureFlags = {|
  ENABLE_VARIABLE_CONNECTION_KEY: boolean,
  ENABLE_PARTIAL_RENDERING_DEFAULT: boolean,
  ENABLE_RELAY_CONTAINERS_SUSPENSE: boolean,
  ENABLE_PRECISE_TYPE_REFINEMENT: boolean,
  ENABLE_REACT_FLIGHT_COMPONENT_FIELD: boolean,
  ENABLE_REQUIRED_DIRECTIVES: boolean | string,
  ENABLE_GETFRAGMENTIDENTIFIER_OPTIMIZATION: boolean,
|};

const RelayFeatureFlags: FeatureFlags = {
  ENABLE_VARIABLE_CONNECTION_KEY: false,
  ENABLE_PARTIAL_RENDERING_DEFAULT: false,
  ENABLE_RELAY_CONTAINERS_SUSPENSE: false,
  ENABLE_PRECISE_TYPE_REFINEMENT: false,
  ENABLE_REACT_FLIGHT_COMPONENT_FIELD: false,
  ENABLE_REQUIRED_DIRECTIVES: false,
  ENABLE_GETFRAGMENTIDENTIFIER_OPTIMIZATION: false,
};

module.exports = RelayFeatureFlags;

/**
 * Copyright (c) Facebook, Inc. and its affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 *
 * @flow strict
 * @format
 */

// flowlint ambiguous-object-type:error

'use strict';

type FeatureFlags = {|
  ENABLE_VARIABLE_CONNECTION_KEY: boolean,
  ENABLE_PARTIAL_RENDERING_DEFAULT: boolean,
  ENABLE_RELAY_CONTAINERS_SUSPENSE: boolean,
  ENABLE_PRECISE_TYPE_REFINEMENT: boolean,
  ENABLE_REACT_FLIGHT_COMPONENT_FIELD: boolean,
  ENABLE_REQUIRED_DIRECTIVES: boolean | string,
  ENABLE_GETFRAGMENTIDENTIFIER_OPTIMIZATION: boolean,
  ENABLE_FRIENDLY_QUERY_NAME_GQL_URL: boolean,
|};

const RelayFeatureFlags: FeatureFlags = {
  ENABLE_VARIABLE_CONNECTION_KEY: false,
  ENABLE_PARTIAL_RENDERING_DEFAULT: false,
  ENABLE_RELAY_CONTAINERS_SUSPENSE: false,
  ENABLE_PRECISE_TYPE_REFINEMENT: false,
  ENABLE_REACT_FLIGHT_COMPONENT_FIELD: false,
  ENABLE_REQUIRED_DIRECTIVES: false,
  ENABLE_GETFRAGMENTIDENTIFIER_OPTIMIZATION: false,
  ENABLE_FRIENDLY_QUERY_NAME_GQL_URL: false,
};

module.exports = RelayFeatureFlags;

Append friendly query name to gql request


/**
* ViewData.js
*
* @description :: This model represents the data for all the video statistics.
* @docs        :: http://sailsjs.org/#!documentation/models
*/

module.exports = {
	attributes: {
		coordinates: {
			type: 'array',
			required: true
		},

		width: {
			type: 'float',
			required: true
		},

		videoTime: {
			type: 'float',
			required: true
		},

		videoTotalTime: {
			type: 'float',
			required: true
		},

		// Add a reference to ViewSession
		sessionObj: {
			model: 'ViewSession'
		}
	}
}

/**
* ViewData.js
*
* @description :: This model represents the data for all the video statistics.
* @docs        :: http://sailsjs.org/#!documentation/models
*/

module.exports = {
	attributes: {
		coordinates: {
			type: 'array',
			required: true
		},

		width: {
			type: 'float',
			required: true
		},

		videoTime: {
			type: 'float',
			required: true
		},

		videoTotalTime: {
			type: 'float'
		},

		// Add a reference to ViewSession
		sessionObj: {
			model: 'ViewSession'
		}
	}
}

Remove required flag for videoTotalTime attribute