import spawn from "./promise/spawn";

let COMMAND = "yarnpkg";
export default class {
    constructor(workingdir, LOG) {
        this.cwd = workingdir;
        this.LOG = LOG;
    }

    install() {
        this.LOG("BEGIN yarnpkg install");
        return spawn(COMMAND, ["install"], { cwd: this.cwd })
            .then(() => this.LOG("END   yarnpkg install"));
    }

    outdated() {
        this.LOG("BEGIN yarnpkg outdated");
        return spawn(COMMAND, ["outdated", "--json"], { cwd: this.cwd })
            .then(out => {
                this.LOG("END   yarnpkg outdated");
                return out.stdout.trim();
            });
    }

    upgrade() {
        this.LOG("BEGIN yarnpkg upgrade");
        return spawn(COMMAND, ["upgrade", "--json"], { cwd: this.cwd })
            .then(out => {
                this.LOG("END   yarnpkg upgrade");
                return out.stdout.trim();
            });
    }
}

import spawn from "./promise/spawn";

let COMMAND = "yarnpkg";
export default class {
    constructor(workingdir, LOG) {
        this.cwd = workingdir;
        this.LOG = LOG;
    }

    install() {
        this.LOG("BEGIN yarnpkg install");
        return spawn(COMMAND, ["install"], { cwd: this.cwd })
            .then(() => this.LOG("END   yarnpkg install"));
    }

    outdated() {
        this.LOG("BEGIN yarnpkg outdated");
        return spawn(COMMAND, ["outdated", "--json"], { cwd: this.cwd })
            .then(out => {
                this.LOG("END   yarnpkg outdated");
                return out.stdout.trim();
            })
            .catch(out => {
                this.LOG("END   yarnpkg outdated");
                return out.stdout.trim();
            });
    }

    upgrade() {
        this.LOG("BEGIN yarnpkg upgrade");
        return spawn(COMMAND, ["upgrade", "--json"], { cwd: this.cwd })
            .then(out => {
                this.LOG("END   yarnpkg upgrade");
                return out.stdout.trim();
            });
    }
}

Fix error due to yarn outdated returns 1


/**
 * YepNope Path Filter
 *
 * Usage:
 *   yepnope.paths = {
 *	   'google': '//ajax.googleapis.com/ajax',
 *	   'my-cdn': '//cdn.myawesomesite.com'
 *   };
 *   yepnope({
 *	   load: ['google/jquery/1.7.2/jquery.min.js', 'google/jqueryui/1.8.18/jquery-ui.min.js', 'my-cdn/style.css', '/non/path/directory/file.js']
 *   });
 *
 * Official Yepnope Plugin
 *
 * WTFPL License
 *
 * by Kenneth Powers | mail@kenpowers.net
 */
(function () {
  var addPathFilter = function (yn) {
      // add each prefix
      yn.addFilter(function (resource) {
        // check each url for path
        for (path in yn.paths) {
          resource.url = resource.url.replace(new RegExp('^' + path), yn.paths[path]);
          return resource;
        }
        // carry on my wayward, son
        return resource;
      });
    };
  console.log(yepnope);
  if (yepnope) addPathFilter(yepnope);
  else if (modernizr.load) addPathFilter(modernizr.load);
})();

/**
 * YepNope Path Filter
 *
 * Usage:
 *   yepnope.paths = {
 *	   'google': '//ajax.googleapis.com/ajax',
 *	   'my-cdn': '//cdn.myawesomesite.com'
 *   };
 *   yepnope({
 *	   load: ['google/jquery/1.7.2/jquery.min.js', 'google/jqueryui/1.8.18/jquery-ui.min.js', 'my-cdn/style.css', '/non/path/directory/file.js']
 *   });
 *
 * Official Yepnope Plugin
 *
 * WTFPL License
 *
 * by Kenneth Powers | mail@kenpowers.net
 */
(function () {
  var addPathFilter = function (yn) {
      // add each prefix
      yn.addFilter(function (resource) {
        // check each url for path
        for (path in yn.paths) {
          resource.url = resource.url.replace(new RegExp('^' + path), yn.paths[path]);
          return resource;
        }
        // carry on my wayward, son
        return resource;
      });
    };
  if (yepnope) addPathFilter(yepnope);
  else if (modernizr.load) addPathFilter(modernizr.load);
})();

Remove spurious console.log from yepnope path filter


Controller.$inject = ['$http', 'qgrid'];
export default function Controller($http, qgrid) {
	const ctrl = this;
	ctrl.rows = [];
	ctrl.model = qgrid.model();

	ctrl.model.pagination({
		// Number of item in viewport
		size: 20
	});

	ctrl.model.scroll({
		mode: 'virtual'
	});

	$http.get('data/people/100000.json')
		.then(response => ctrl.rows = response.data);
}

Controller.$inject = ['$http', 'qgrid'];
export default function Controller($http, qgrid) {
	const ctrl = this;
	ctrl.rows = [];
	ctrl.model = qgrid.model();
	const service = qgrid.service(ctrl.model);

	ctrl.model.pagination({
		// Number of item in viewport
		size: 20
	});

	ctrl.model.scroll({
		mode: 'virtual'
	});

	const cancelBusy = service.busy();
	$http.get('data/people/100000.json')
		.then(response => {
			ctrl.rows = response.data;
			cancelBusy();
		});
}

Add busy indication on data load


#!/usr/bin/env node

'use strict';

const chalk = require('chalk');
const {exec} = require('child-process-promise');
const {logPromise} = require('../utils');

const runYarnTask = async (cwd, task, errorMessage) => {
  try {
    await exec(`yarn ${task}`, {cwd});
  } catch (error) {
    throw Error(
      chalk`
      ${errorMessage}

      {white ${error.stdout}}
    `
    );
  }
};

module.exports = async ({cwd}) => {
  await logPromise(runYarnTask(cwd, 'lint', 'Lint failed'), 'Running ESLint');
  await logPromise(
    runYarnTask(cwd, 'flow', 'Flow failed'),
    'Running Flow checks'
  );
  await logPromise(
    runYarnTask(cwd, 'jest', 'Jest failed'),
    'Running Jest tests',
    true
  );
};

#!/usr/bin/env node

'use strict';

const chalk = require('chalk');
const {exec} = require('child-process-promise');
const {logPromise} = require('../utils');

const runYarnTask = async (cwd, task, errorMessage) => {
  try {
    await exec(`yarn ${task}`, {cwd});
  } catch (error) {
    throw Error(
      chalk`
      ${errorMessage}

      {white ${error.stdout}}
    `
    );
  }
};

module.exports = async ({cwd}) => {
  await logPromise(runYarnTask(cwd, 'lint', 'Lint failed'), 'Running ESLint');
  await logPromise(
    runYarnTask(cwd, 'flow', 'Flow failed'),
    'Running Flow checks'
  );
  await logPromise(
    runYarnTask(cwd, 'test', 'Jest failed'),
    'Running Jest tests',
    true
  );
};

Fix Jest call in the release script


/*!
 * node-minify
 * Copyright(c) 2011-2019 Rodolphe Stoclin
 * MIT Licensed
 */

/**
 * Module dependencies.
 */
import uglifyES from 'uglify-es';
import { utils } from '@node-minify/utils';

/**
 * Run uglifyES.
 *
 * @param {Object} settings
 * @param {String} content
 * @param {Function} callback
 */
const minifyUglifyES = ({ settings, content, callback, index }) => {
  const contentMinified = uglifyES.minify(content, settings.options);
  if (contentMinified.error) {
    if (callback) {
      return callback(contentMinified.error);
    }
  }
  if (contentMinified.map && settings.options.sourceMap) {
    utils.writeFile({ file: `${settings.output}.map`, content: contentMinified.map, index });
  }
  utils.writeFile({ file: settings.output, content: contentMinified.code, index });
  if (callback) {
    return callback(null, contentMinified.code);
  }
  return contentMinified.code;
};

/**
 * Expose `minifyUglifyES()`.
 */
module.exports = minifyUglifyES;

/*!
 * node-minify
 * Copyright(c) 2011-2019 Rodolphe Stoclin
 * MIT Licensed
 */

/**
 * Module dependencies.
 */
import uglifyES from 'uglify-es';
import { utils } from '@node-minify/utils';

/**
 * Run uglifyES.
 *
 * @param {Object} settings
 * @param {String} content
 * @param {Function} callback
 */
const minifyUglifyES = ({ settings, content, callback, index }) => {
  if (settings.options.sourceMap) {
    content = { [settings.options.sourceMap.filename]: content };
  }
  const contentMinified = uglifyES.minify(content, settings.options);
  if (contentMinified.error) {
    if (callback) {
      return callback(contentMinified.error);
    }
  }
  if (contentMinified.map && settings.options.sourceMap) {
    utils.writeFile({ file: `${settings.output}.map`, content: contentMinified.map, index });
  }
  utils.writeFile({ file: settings.output, content: contentMinified.code, index });
  if (callback) {
    return callback(null, contentMinified.code);
  }
  return contentMinified.code;
};

/**
 * Expose `minifyUglifyES()`.
 */
module.exports = minifyUglifyES;

Fix source array is "0"


module.exports = {
  name: 'ember-cli-mocha',

  normalizeEntityName: function() {
    // this prevents an error when the entityName is
    // not specified (since that doesn't actually matter
    // to us
  },

  afterInstall: function() {
    var addonContext = this;

    return this.addBowerPackageToProject('mocha', '~1.17.0')
      .then(function() {
        return addonContext.addBowerPackageToProject('chai', '~1.9.1');
      })
      .then(function() {
        return addonContext.addBowerPackageToProject('ember-mocha-adapter', '0.2.1');
      })
      .then(function() {
        return addonContext.addBowerPackageToProject('ember-mocha', '~0.1.0');
      })
      .then(function() {
        return addonContext.addBowerPackageToProject('dgeb/ember-cli-test-loader', 'test-agnostic');
      })
      .then(function() {
        return addonContext.addBowerPackageToProject('stefanpenner/ember-cli-shims', '0.0.3');
      });
  }
};

var EOL = require('os').EOL;

module.exports = {
  name: 'ember-cli-mocha',

  normalizeEntityName: function() {
    // this prevents an error when the entityName is
    // not specified (since that doesn't actually matter
    // to us
  },

  afterInstall: function() {
    var addonContext = this;

    this.insertIntoFile('tests/.jshintrc',
      '    "mocha",' + EOL +
      '    "chai",' + EOL +
      '    "describe",' + EOL +
      '    "it",' + EOL +
      '    "before",' + EOL +
      '    "beforeEach",' + EOL +
      '    "after",' + EOL +
      '    "afterEach",',
      {after:'  "predef": ['+EOL});

    return this.addBowerPackageToProject('mocha', '~1.17.0')
      .then(function() {
        return addonContext.addBowerPackageToProject('chai', '~1.9.1');
      })
      .then(function() {
        return addonContext.addBowerPackageToProject('ember-mocha-adapter', '0.2.1');
      })
      .then(function() {
        return addonContext.addBowerPackageToProject('ember-mocha', '~0.1.0');
      })
      .then(function() {
        return addonContext.addBowerPackageToProject('dgeb/ember-cli-test-loader', 'test-agnostic');
      })
      .then(function() {
        return addonContext.addBowerPackageToProject('stefanpenner/ember-cli-shims', '0.0.3');
      });
  }
};

Add mocha / chai globals to tests/.jshintrc


module.exports = function(config) {
  config.set({
    basePath: '../',
    frameworks: ['jasmine'],
    files: [
      'bower_components/angular/angular.js',
      'bower_components/angular-mocks/angular-mocks.js',
      'src/*.js',
      'test/helpers/*.helper.js',
      'test/specs/*.spec.js'
    ],
    reporters: ['progress', 'coverage'],
    preprocessors: {
      'src/*.js': ['coverage']
    },
    coverageReporter: {
      reporters: [
        { type: 'lcovonly' },
        { type: 'text-summary' }
      ]
    },
    port: 8089,
    runnerPort: 9109,
    urlRoot: '/__karma/',
    colors: true,
    logLevel: config.LOG_INFO,
    autoWatch: true,
    autoWatchInterval: 0,
    browsers: ['Firefox'],
    singleRun: true
  });
};

module.exports = function(config) {
  config.set({
    basePath: '../',
    frameworks: ['jasmine'],
    files: [
      'bower_components/angular/angular.js',
      'bower_components/angular-mocks/angular-mocks.js',
      'src/*.js',
      'test/helpers/*.helper.js',
      'test/specs/*.spec.js'
    ],
    reporters: ['progress', 'coverage'],
    preprocessors: {
      'src/*.js': ['coverage']
    },
    coverageReporter: {
      reporters: [
        { type: 'lcov' },
        { type: 'text-summary' }
      ]
    },
    port: 8089,
    runnerPort: 9109,
    urlRoot: '/__karma/',
    colors: true,
    logLevel: config.LOG_INFO,
    autoWatch: true,
    autoWatchInterval: 0,
    browsers: ['Firefox'],
    singleRun: true
  });
};

Add html test coverage reporting


/*global angular*/
(function () {
    'use strict';

    // min-font-size attribute = minimum font size in pixels
    // max-font-size attribute = maximum font size in pixels
    // Directive must be used on the parent of an element with the .text-fill class
    angular.module('core').directive('textFill', ["$timeout", function ($timeout) {
            return {
                link: function postLink(scope, element, attrs) {
                    if ($(element).find('.text-fill').length === 0) {
                        return;
                    }

                    var minFontSize = parseFloat(attrs.minFontSize);
                    var maxFontSize = parseFloat(attrs.maxFontSize);

                    if (!minFontSize || !maxFontSize) {
                        return;
                    }

                    var applyTextFill = function() {
                        $(element).textfill({
                            minFontPixels: minFontSize,
                            maxFontPixels: maxFontSize,
                            innerTag: '.text-fill'
                        });
                    }

                    scope.$on('updateSlideContentPart', function (event, content, member, slidePartId) {
                        if (member === attrs.member) {
                            applyTextFill();
                        }
                    });

                    $timeout(applyTextFill, 100);
                }
            };
        }]);
}());

Fix indentation in the text-fill directive.


import moment from 'moment'

function display (content, format) {
  if (format === 'Date') return moment(content).format('YYYY-MM-DD HH:mm:ss')
  else if (format === 'GeoPoint') return `${content.longitude},${content.latitude}`
  return content
}
function asFile (content, format) {
  if (format === 'Date') return new Date(content)
  else if (format === 'GeoPoint') return `${content.longitude},${content.latitude}`
  return content
}

export default { display, asFile }

import moment from 'moment'

function display (content, format) {
  if (content === undefined) return ''
  else if (format === 'Date') return moment(content).format('YYYY-MM-DD HH:mm:ss')
  else if (format === 'GeoPoint') return `${content.longitude},${content.latitude}`
  return content
}
function asFile (content, format) {
  if (content === undefined) return ''
  else if (format === 'Date') return new Date(content)
  else if (format === 'GeoPoint') return `${content.longitude},${content.latitude}`
  return content
}

export default { display, asFile }

Fix error on displaying when no content


'use strict';

angular.module('chartsExample.directives',[])

.directive('chart', function () {
  return {
    restrict: 'E',
    template: '

',
    scope: {
        chartData: "=value",
        chartObj: "="
    },
    transclude:true,
    replace: true,

    link: function (scope, element, attrs) {

        //Update when charts data changes
        scope.$watch(function() { return scope.chartData; }, function(value) {
          if(!value) return;

            // use default values if nothing is specified in the given settings
            scope.chartData.chart.renderTo = scope.chartData.chart.renderTo || element[0];
            if (attrs.type) scope.chartData.chart.type = scope.chartData.chart.type || attrs.type;
            if (attrs.height) scope.chartData.chart.height = scope.chartData.chart.height || attrs.height;
            if (attrs.width) scope.chartData.chart.width = scope.chartData.chart.type || attrs.width;

            scope.chartObj = new Highcharts.Chart(scope.chartData);
        });
      }
    };

});

'use strict';

angular.module('chartsExample.directives', []).directive('chart', function() {
    return {
        restrict: 'E',
        template: '

',
        scope: {
            chartData: "=value",
            chartObj: "="
        },
        transclude: true,
        replace: true,
        link: function($scope, $element, $attrs) {

            //Update when charts data changes
            $scope.$watch('chartData', function(value) {
                if (!value)
                    return;

                // use default values if nothing is specified in the given settings
                $scope.chartData.chart.renderTo = $scope.chartData.chart.renderTo || $element[0];
                if ($attrs.type)
                    $scope.chartData.chart.type = $scope.chartData.chart.type || $attrs.type;
                if ($attrs.height)
                    $scope.chartData.chart.height = $scope.chartData.chart.height || $attrs.height;
                if ($attrs.width)
                    $scope.chartData.chart.width = $scope.chartData.chart.type || $attrs.width;

                $scope.chartObj = new Highcharts.Chart($scope.chartData);
            });
        }
    };

});

Use AngularJS best practice naming conventions


import SandboxBrowser from '../sandboxes/SandboxBrowser';
import AppSandboxBrowser from '../sandboxes/AppSandboxBrowser';
import Request from '../browser/Request';
import {RuntimeCatalogue, RuntimeCatalogueLocal} from 'service-framework/dist/RuntimeCatalogue';
import PersistenceManager from 'service-framework/dist/PersistenceManager';

const runtimeFactory = Object.create({
  createSandbox() {
    return new SandboxBrowser();
  },

  createAppSandbox() {
    return new AppSandboxBrowser();
  },

  createHttpRequest() {
    let request = new Request();
    return request;
  },

  atob(b64) {
    return atob(b64);
  },

  persistenceManager() {
    let localStorage = window.localStorage;
    return new PersistenceManager(localStorage);
  },

  createRuntimeCatalogue(development) {

    if (!this.catalogue)
      this.catalogue = development ? new RuntimeCatalogueLocal(this) : new RuntimeCatalogue(this);

    return this.catalogue;
  }

});

export default runtimeFactory;

import SandboxBrowser from '../sandboxes/SandboxBrowser';
import AppSandboxBrowser from '../sandboxes/AppSandboxBrowser';
import Request from '../browser/Request';
import {RuntimeCatalogue} from 'service-framework/dist/RuntimeCatalogue';
import PersistenceManager from 'service-framework/dist/PersistenceManager';

const runtimeFactory = Object.create({
  createSandbox() {
    return new SandboxBrowser();
  },

  createAppSandbox() {
    return new AppSandboxBrowser();
  },

  createHttpRequest() {
    let request = new Request();
    return request;
  },

  atob(b64) {
    return atob(b64);
  },

  persistenceManager() {
    let localStorage = window.localStorage;
    return new PersistenceManager(localStorage);
  },

  createRuntimeCatalogue(development) {

    if (!this.catalogue)
      this.catalogue = new RuntimeCatalogue(this);

    return this.catalogue;
  }

});

export default runtimeFactory;

Fix the use of runtimeCatalogueLocal


(function( $ ) {
    $.fn.searchify = function() {
        return this.each(function() {
            $(this).autocomplete({
                source: $(this).data("search-url"),
                select: function (event, ui) {
                    if (select_url = $(this).data("select-url")) {
                        for (element in ui.item)
                            select_url = select_url.replace('\(' + element + '\)', ui.item[element]);

                        window.location.href = select_url;
                    } else {
                        $(this).prev().val(ui.item.id);
                        $(this).data('value', ui.item.id)
                        $(this).blur();
                        $(this).focus();
                    }
                }
            });

            $(this).change( function (event, ui) {
                if ( $(this).data('value') != $(this).prev().val() ) {
                    $(this).val('');
                    $(this).prev().val('');
                }
            });
            $(this).focus( function (event, ui) {
                $(this).data('value', '');
            });
        });
    };
})( jQuery );

(function( $ ) {
    $.fn.searchify = function() {
        return this.each(function() {
            $(this).autocomplete({
                source: $(this).data("search-url"),
                select: function (event, ui) {
                    if (select_url = $(this).data("select-url")) {
                        for (element in ui.item)
                            select_url = select_url.replace('\(' + element + '\)', ui.item[element]);

                        window.location.href = select_url;
                    } else {
                        $(this).prev().val(ui.item.id);
                        $(this).data('value', ui.item.id)
                        $(this).blur();
                        $(this).focus();
                    }
                }
            });

            $(this).change( function (event, ui) {
                if ( $(this).prev().val() == '' || $(this).prev().val() != $(this).data('value') ) {
                    $(this).val('');
                    $(this).prev().val('');
                }
            });
            $(this).focus( function (event, ui) {
                $(this).data('value', '');
            });
        });
    };
})( jQuery );

Add worflow validation to prevent user replacement assumptions


define('app/views/backend_button', ['ember'],
    /**
     * Backend Button View
     *
     * @returns Class
     */
    function() {
        return Ember.View.extend({

            template: Ember.Handlebars.compile("{{title}}"),

            tagName: 'a',

            attributeBindings: ['data-role', 'data-theme', 'data-inline', 'data-role', 'data-icon'],

            didInsertElement: function() {
                if ('button' in $("#"+this.elementId)) {
                    Ember.run.next(this, function() {
                        $("#"+this.elementId).button();
                        $('#backend-buttons').controlgroup('refresh');
                    });
                } else {
                    Ember.run.later(this, function() {
                        this.didInsertElement();
                    }, 100);
                }
            },

            click: function() {
                var backend = this.backend;
                Mist.backendEditController.set('backend', backend);
                $('select.ui-slider-switch option[value=1]')[0].selected = backend.enabled;
                $('select.ui-slider-switch').slider('refresh');
                $("#edit-backend").popup('option', 'positionTo', '#' + this.elementId).popup('open', {transition: 'pop'});
            }
        });
    }
);

define('app/views/backend_button', ['ember'],
    /**
     * Backend Button View
     *
     * @returns Class
     */
    function() {
        return Ember.View.extend({

            template: Ember.Handlebars.compile("{{title}}"),

            tagName: 'a',

            attributeBindings: ['data-role', 'data-theme', 'data-inline', 'data-role', 'data-icon'],

            didInsertElement: function() {
                if ('button' in $("#"+this.elementId)) {
                    Ember.run.next(this, function() {
                        $("#"+this.elementId).button();
                        $('#backend-buttons').controlgroup('refresh');
                    });
                } else {
                    Ember.run.later(this, function() {
                        this.didInsertElement();
                    }, 100);
                }
            },

            click: function() {
                var backend = this.backend;
                $('#monitoring-message').hide();
                $('#backend-delete-confirm').hide();
                Mist.backendEditController.set('backend', backend);
                $('#backend-toggle option[value=1]')[0].selected = backend.enabled;
                $('#backend-toggle').slider('refresh');
                $("#edit-backend").popup('option', 'positionTo', '#' + this.elementId).popup('open', {transition: 'pop'});
            }
        });
    }
);

Optimize backend button click function


app.controller('NavController', ['$http', '$firebaseAuth', function($http, $firebaseAuth) {
  console.log('NavController running');
  var auth = $firebaseAuth();
  var self = this;

  self.logIn = function() {
    auth.$signInWithPopup("google").then(function(firebaseUser) {
      console.log("Firebase Authenticated as: ", firebaseUser.user.displayName);
    }).catch(function(error) {
      console.log("Authentication failed: ", error);
    });
  };

  (function() {
    'use strict';

    angular.module('MyApp',['ngMaterial', 'ngMessages', 'material.svgAssetsCache'])
    .controller('AppCtrl', AppCtrl);

    function AppCtrl($scope) {
      $scope.currentNavItem = 'page1';
    }
  })();

}]);

app.controller('NavController', ['$http', '$firebaseAuth', function($http, $firebaseAuth) {
  console.log('NavController running');
  var auth = $firebaseAuth();
  var self = this;

  self.logIn = function() {
    auth.$signInWithPopup("google").then(function(firebaseUser) {
      console.log("Firebase Authenticated as: ", firebaseUser.user.displayName);
    }).catch(function(error) {
      console.log("Authentication failed: ", error);
    });
  };

  // demo code from Angular Material docs to auto select the current nav item of
  // the currently open window
  ////
  // (function() {
  //   'use strict';
  //
  //   angular.module('MyApp',['ngMaterial', 'ngMessages', 'material.svgAssetsCache'])
  //   .controller('AppCtrl', AppCtrl);
  //
  //   function AppCtrl($scope) {
  //     $scope.currentNavItem = 'page1';
  //   }
  // })();

}]);

Comment out some Angular Material demo code to refer to later


class LdapConfigService {
	async create(config, params) {
		const { verifyOnly } = params.query;

		const verificationResult = await this.verifyConfig(config);

		if (verificationResult.ok && !verifyOnly) {
			await this.saveConfig(config);
		}
		return verificationResult;
	}

	async verifyConfig(/* config */) {
		return {
			ok: true,
			users: {
				total: 1396,
				admin: 4,
				teacher: 20,
				student: 1372,
				sample: {},
			},
			classes: {
				total: 27,
				sample: {},
			},
		};
	}

	async saveConfig(/* config */) {
		return 42;
	}
}

module.exports = LdapConfigService;

const logger = require('../../logger');

class LdapConfigService {
	setup(app) {
		this.app = app;
	}

	async create(config, params) {
		const { verifyOnly } = params.query;

		const verificationResult = await this.verifyConfig(config);

		if (verificationResult.ok && !verifyOnly) {
			await this.saveConfig(config);
		}
		return verificationResult;
	}

	async verifyConfig(config) {
		const ldap = this.app.service('ldap');
		const result = {
			ok: false,
			users: {},
			classes: {},
		};
		try {
			const users = await ldap.getUsers(config);
			result.users = LdapConfigService.generateUserStats(users);
			const classes = await ldap.getClasses(config);
			result.classes = LdapConfigService.generateClassStats(classes);
			result.ok = true;
		} catch (error) {
			logger.warning('Error during LDAP config verification', { error });
		} finally {
			ldap.disconnect(config);
		}
		return result;
	}

	async saveConfig(/* config */) {
		return 42;
	}

	static generateUserStats(users = []) {
		const result = {
			total: users.length,
			admin: 0,
			teacher: 0,
			student: 0,
			sample: users.length > 0 ? users[0] : {},
		};
		for (const user of users) {
			if (user.roles.includes('administrator')) result.admin += 1;
			if (user.roles.includes('teacher')) result.teacher += 1;
			if (user.roles.includes('student')) result.student += 1;
		}
		return result;
	}

	static generateClassStats(classes = []) {
		return {
			total: classes.length,
			sample: classes.length > 0 ? classes[0] : {},
		};
	}
}

module.exports = LdapConfigService;

Connect to LDAP server to verify config


import Ember from 'ember';
import { task, timeout } from 'ember-concurrency';

export default Ember.Component.extend({
  autosave: task(function* (points){
    let intPoints = parseInt(points);
    this.get('score').set('points', intPoints);
    yield timeout(200);
    yield this.get('score').save();
  }).restartable()
});

import Ember from 'ember';
import { task, timeout } from 'ember-concurrency';

export default Ember.Component.extend({
  autosave: task(function* (points){
    let intPoints = parseInt(points);
    this.get('score').set('points', intPoints);
    yield timeout(800);
    yield this.get('score').save();
  }).restartable()
});

Increase timeout on score entry


define('app/controllers/notification', ['ember'],
	/**
	 * Notification controller
	 *
	 * @returns Class
	 */
	function() {
		return Ember.Object.extend({

			timeout: false,

			notify: function(message){
				if(this.timeout){
					clearTimeout(this.timeout);
				}
				log("notification: " + message);
				jQuery.mobile.showPageLoadingMsg(
						jQuery.mobile.pageLoadErrorMessageTheme,
						message, true);
				this.timeout = setTimeout(jQuery.mobile.hidePageLoadingMsg, 1500);
			}

		});
	}
);

define('app/controllers/notification', ['ember'],
    /**
     * Notification controller
     *
     * @returns Class
     */
    function() {
        return Ember.Object.extend({

            timeout: false,

            notify: function(message){
                if(this.timeout){
                    clearTimeout(this.timeout);
                }
                log("notification: " + message);
                $.mobile.loading( 'show', {
                            text: message,
                            textVisible: true,
                            textonly: true,
                            theme: $.mobile.pageLoadErrorMessageTheme
                });
                this.timeout = setTimeout($.mobile.loading( 'hide' ), 1500);
            }
        });
    }
);

Use new way of loading messages, the old one is deprecated


function ComponentTextfield( id ) {
	var self = _ComponentFormControl(id);

	self.setDefaultState('text-value');
	self.bind = function() {
		self.attachKeyUpAction( self.node(), self.identifier() );
	};
	self.registerAction('keyup', function() {
		self.setState('text-value', self._states['text-value'] = self.node().value);
	});
	self.select = function() {
		if( self.node() ) {
			self.node().select();
		}
	};
	self.textValue = function() {
		self.setState('text-value', self.node().value);
		return self.getState('text-value');
	};
	self.setTextValue = function( value ) {
		self.setState('text-value', value);
		self.node().value = value;
	};
	self.enable = function() {
		self._enabled = true;
		self.node().readOnly = false;
	};
	self.disable = function() {
		self._enabled = false;
		self.node().readOnly = true;
	};
	return self;
}

function ComponentTextfield( id ) {
	var self = _ComponentFormControl(id);

	self.setDefaultState('text-value');
	self.bind = function() {
		self.attachKeyUpAction( self.node(), self.identifier() );
	};
	self.registerAction('keyup', function() {
		self._states['text-value'] = self.node().value;
	});
	self.select = function() {
		if( self.node() ) {
			self.node().select();
		}
	};
	self.textValue = function() {
		self.setState('text-value', self.node().value);
		return self.getState('text-value');
	};
	self.setTextValue = function( value ) {
		self.setState('text-value', value);
		self.node().value = value;
	};
	self.enable = function() {
		self._enabled = true;
		self.node().readOnly = false;
	};
	self.disable = function() {
		self._enabled = false;
		self.node().readOnly = true;
	};
	return self;
}

Fix problem with system asking if you want to send empty answer


var app = require('ridge');

var FormView = require('ridge/views/form');

module.exports = FormView.extend({
	submit: function(e) {
		e.preventDefault();

		var query = [];

		_.each(this.$el.prop('elements'), function(elem) {
			if (elem.name && elem.value)
				query.push(encodeURIComponent(elem.name) + '=' + encodeURIComponent(elem.value).replace('%20', '+'));
		});

		var url = '/' + Backbone.history.fragment.split('?')[0];

		if (query.length) url += '?' + query.join('&');

		Backbone.history.navigate(url, { trigger: true });
	},

	initialize: function() {
		this.listenTo(this.state, 'change:query', this.attach);

		this.delegate('submit', this.submit.bind(this));
	},

	attach: function() {
		var params = this.state.get('params');

		_.each(this.$el.prop('elements'), function(elem) {
			if (elem.name)
				$(elem).val(params[elem.name]).trigger('change');
		});
	}
});

var app = require('ridge');

var FormView = require('ridge/views/form');

module.exports = FormView.extend({
	events: {
		'submit': 'submit'
	},

	submit: function(e) {
		e.preventDefault();

		var query = [];

		_.each(this.$el.prop('elements'), function(elem) {
			if (elem.name && elem.value)
				query.push(encodeURIComponent(elem.name) + '=' + encodeURIComponent(elem.value).replace('%20', '+'));
		});

		var url = '/' + Backbone.history.fragment.split('?')[0];

		if (query.length) url += '?' + query.join('&');

		Backbone.history.navigate(url, { trigger: true });
	},

	initialize: function() {
		this.listenTo(this.state, 'change:query', this.attach);
	},

	attach: function(model, value) {
		var query = this.state.get('query');

		_.each(this.$el.prop('elements'), function(elem) {
			if (elem.name)
				$(elem).val(query[elem.name]).trigger('change');
		});
	}
});

Use query instead of params (from router), and set up events instead of pointless delegate.


"use strict"

const BaseFormatSet = require('./baseFormatSet');

class BaseFormatSetTable extends BaseFormatSet {
  commit(subquery, formattedRows) {
    let output = '

';
    for (let field of Object.values(subquery.statement.fields)) {
      output += '' + (field.label ? `<div class="query_field_label">${field.label}</div>` : '') + '	';
    }
    output += '

';
    for (let fr of formattedRows) {
      output += this.commitRow(subquery, fr);
    }
    output += '
';
    return `<table>${output}</table>`;
  }
}

BaseFormatSetTable.id = 'table';

module.exports = BaseFormatSetTable;

"use strict"

const BaseFormatSet = require('./baseFormatSet');

class BaseFormatSetTable extends BaseFormatSet {
  commit(subquery, formattedRows) {
    let output = '

';
    for (let field of Object.values(subquery.statement.fields)) {
      output += `<th class="query_field_header query_field_header_${field.name} field_header__${field.nameAlias}>` + (field.label ? `<div class="query_field_label">${field.label}</div>` : '') + '	';
    }
    output += '

';
    for (let fr of formattedRows) {
      output += this.commitRow(subquery, fr);
    }
    output += '
';
    return `<table>${output}</table>`;
  }
}

BaseFormatSetTable.id = 'table';

module.exports = BaseFormatSetTable;

Fix th class of BaseFormatSet


import Ember from 'ember';
import CommentableMixin from 'ember-osf/mixins/commentable';
import TaggableMixin from 'ember-osf/mixins/taggable-mixin';
import NodeActionsMixin from 'ember-osf/mixins/node-actions';
import KeenTrackerMixin from 'ember-osf/mixins/keen-tracker';

export default Ember.Controller.extend(CommentableMixin, TaggableMixin, NodeActionsMixin, KeenTrackerMixin, {
    toast: Ember.inject.service(),
    propertiesVisible: false,
    isSaving: false,
    licenseToggle: true,
    serializedLicense: Ember.computed('model.license', function() {
        let license = this.get('model.license');
        return {
            licenseType: license,
            year: this.get('model.nodeLicense.year'),
            copyrightHolders: this.get('model.nodeLicense.copyright_holders') ? this.get('model.nodeLicense.copyright_holders').join(',') : null
        };
    }),
    actions: {
        toggleEditNode() {
            this.toggleProperty('propertiesVisible');
        },
        toggleLicense() {
            this.toggleProperty('licenseToggle');
        },
        editLicense() {
            // Would update node properly!
        },
        updateNode() {
            this.keenTrackFrontEndEvent('button', 'click', 'updateNode');
            this.set('isSaving', true);
            return this._super(...arguments)
                .then(() => {
                    this.set('isSaving', false);
                    this.get('toast').success('Node updated successfully');
                })
                .catch(() => this.set('isSaving', false));
        }
    }
});

import Ember from 'ember';
import CommentableMixin from 'ember-osf/mixins/commentable';
import TaggableMixin from 'ember-osf/mixins/taggable-mixin';
import NodeActionsMixin from 'ember-osf/mixins/node-actions';
import KeenTrackerMixin from 'ember-osf/mixins/keen-tracker';

export default Ember.Controller.extend(CommentableMixin, TaggableMixin, NodeActionsMixin, KeenTrackerMixin, {
    toast: Ember.inject.service(),
    propertiesVisible: false,
    isSaving: false,
    licenseToggle: true,
    serializedLicense: Ember.computed('model.license', function() {
        let license = this.get('model.license');
        return {
            licenseType: license,
            year: this.get('model.nodeLicense.year'),
            copyrightHolders: this.get('model.nodeLicense.copyright_holders') ? this.get('model.nodeLicense.copyright_holders').join(',') : null
        };
    }),
    actions: {
        toggleEditNode() {
            this.toggleProperty('propertiesVisible');
        },
        toggleLicense() {
            this.toggleProperty('licenseToggle');
        },
        editLicense() {
            // Would update node properly!
        },
        updateNode() {
            this.keenTrackFrontEndEvent({
                category:'button',
                action: 'click',
                label: 'Update Node'
            }, this.get('model'));

            this.set('isSaving', true);
            return this._super(...arguments)
                .then(() => {
                    this.set('isSaving', false);
                    this.get('toast').success('Node updated successfully');
                })
                .catch(() => this.set('isSaving', false));
        }
    }
});

Modify variables passing in in dummy app.


'use strict'

const getBrowserslistConfig = require('./lib/getBrowserslistConfig')
const mergeByEnv = require('./lib/mergeByEnv')

module.exports = mergeByEnv({
  presets: [
    [
      'babel-preset-env',
      {
        modules: false,
        targets: {
          browsers: getBrowserslistConfig()
        },
        useBuiltIns: true
      }
    ],
    'babel-preset-react'
  ],
  plugins: [
    'babel-plugin-react-css-modules',
    'babel-plugin-syntax-dynamic-import',
    'babel-plugin-transform-class-properties',
    'babel-plugin-transform-object-rest-spread',
    [
      'babel-plugin-transform-react-jsx',
      {
        pragma: 'createElement'
      }
    ]
  ],
  env: {
    production: {
      plugins: [
        'babel-plugin-transform-react-constant-elements',
        'babel-plugin-transform-react-inline-elements',
        'babel-plugin-transform-react-remove-prop-types'
      ]
    },
    test: {
      plugins: ['babel-plugin-istanbul']
    },
    development: {
      plugins: ['babel-plugin-flow-react-proptypes', 'babel-plugin-tcomb']
    }
  }
})

'use strict'

const getBrowserslistConfig = require('./lib/getBrowserslistConfig')
const mergeByEnv = require('./lib/mergeByEnv')

module.exports = mergeByEnv({
  presets: [
    [
      'babel-preset-env',
      {
        modules: false,
        targets: {
          browsers: getBrowserslistConfig()
        },
        useBuiltIns: true
      }
    ],
    'babel-preset-react'
  ],
  plugins: [
    'babel-plugin-react-css-modules',
    'babel-plugin-syntax-dynamic-import',
    'babel-plugin-transform-class-properties',
    'babel-plugin-transform-object-rest-spread',
    [
      'babel-plugin-transform-react-jsx',
      {
        pragma: 'createElement'
      }
    ]
  ],
  env: {
    production: {
      plugins: [
        'babel-plugin-transform-react-constant-elements',
        'babel-plugin-transform-react-inline-elements',
        ['babel-plugin-transform-react-remove-prop-types', {removeImport: true}]
      ]
    },
    test: {
      plugins: ['babel-plugin-istanbul']
    },
    development: {
      plugins: ['babel-plugin-flow-react-proptypes', 'babel-plugin-tcomb']
    }
  }
})

Remove prop-types import in production


module.exports = {
  root: true,
  env: {
    browser: true,
    node: true
  },
  extends: 'standard',
  plugins: [
    'html'
  ],
  'rules': {
    'arrow-parens': 0,
    'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0
  }
}

module.exports = {
  root: true,
  parserOptions: {
    sourceType: 'module'
  },
  env: {
    browser: true,
    node: true
  },
  extends: 'standard',
  plugins: [
    'html'
  ],
  'rules': {
    'arrow-parens': 0,
    'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0
  }
}

Fix errors in node 6 on Windows x64


module.exports = {
  parser: 'babel-eslint',

  plugins: [
    'flowtype',

    // Although we don't have any React or JSX in this project, we need to have
    // this plugin and some of the rules from this plugin enabled here so that
    // our tests work. We should probably look into a way of doing this so that
    // this configuration is scoped specifically for the tests that require it.
    'react',
  ],

  extends: [
    'airbnb-base',
    'plugin:react/recommended',
  ],

  rules: {
    'no-shadow': 0, // This is currently erroring because of flow type defs
    'no-underscore-dangle': ['error', { allowAfterThis: true }],

    'flowtype/define-flow-type': 2,
    'flowtype/require-parameter-type': 2,
    'flowtype/require-return-type': [2, 'always', {
      annotateUndefined: 'never'
    }],
    'flowtype/space-after-type-colon': [2, 'always'],
    'flowtype/space-before-type-colon': [2, 'never'],
    'flowtype/type-id-match': [2, "^([A-Z][a-z0-9]+)+Type$"],
    'flowtype/use-flow-type': 2,
  },

  settings: {
    flowtype: {
      onlyFilesWithFlowAnnotation: true,
    },
  },
};

module.exports = {
  parser: 'babel-eslint',

  plugins: [
    'flowtype',

    // Although we don't have any React or JSX in this project, we need to have
    // this plugin and some of the rules from this plugin enabled here so that
    // our tests work. We should probably look into a way of doing this so that
    // this configuration is scoped specifically for the tests that require it.
    'react',
  ],

  extends: [
    'airbnb-base',
    'plugin:react/recommended',
  ],

  rules: {
    'no-shadow': 0, // This is currently erroring because of flow type defs
    'no-underscore-dangle': ['error', { allowAfterThis: true }],

    'flowtype/define-flow-type': 2,
    'flowtype/generic-spacing': [2, 'never'],
    'flowtype/require-parameter-type': 2,
    'flowtype/require-return-type': [2, 'always', {
      annotateUndefined: 'never'
    }],
    'flowtype/require-valid-file-annotation': [2, 'always'],
    'flowtype/space-after-type-colon': [2, 'always'],
    'flowtype/space-before-type-colon': [2, 'never'],
    'flowtype/space-before-generic-bracket': [2, 'never'],
    'flowtype/union-intersection-spacing': [2, 'always'],
    'flowtype/type-id-match': [2, "^([A-Z][a-z0-9]+)+Type$"],
    'flowtype/use-flow-type': 2,
    'flowtype/valid-syntax': 2,
  },

  settings: {
    flowtype: {
      onlyFilesWithFlowAnnotation: true,
    },
  },
};

Enable new flowtype eslint rules


/* eslint-env node */

module.exports = {
  name: 'ilios-prerender',
  contentFor: function(type, config) { // eslint-disable-line no-unused-vars
    if (type === 'body-footer') {
      return '';
    }
    if (type === 'test-body-footer') {
      return '';
    }

    if( type === 'body') {
      return "
" +
        "" +
          "
" +
            "" +
          "
" +
        "" +
        "
" +
          "
" +
            "" +
          "
" +
          "
" +
            "It is possible that your browser is not supported by Ilios.  " +
            "Please refresh this page or try a different browser." +
          "
" +
          "
" +
        "
" +
      "
";
    }
  }
};

/* eslint-env node */

module.exports = {
  name: 'ilios-prerender',
  contentFor: function(type, config) { // eslint-disable-line no-unused-vars
    if (type === 'body-footer') {
      return `<script src="${config.rootURL}ilios-prerender/scripts.js"></script>`;
    }
    if (type === 'test-body-footer') {
      return `<script src="${config.rootURL}ilios-prerender/test-scripts.js"></script>`;
    }

    if( type === 'body') {
      return "
" +
        "" +
          "
" +
            "" +
          "
" +
        "" +
        "
" +
          "
" +
            "" +
          "
" +
          "
" +
            "It is possible that your browser is not supported by Ilios.  " +
            "Please refresh this page or try a different browser." +
          "
" +
          "
" +
        "
" +
      "
";
    }
  }
};

Use the rootURL in prerender to get assets


//= require store/spree_frontend
$(function() {
  var timezone = "US/Pacific";
  $.getJSON("http://json-time.appspot.com/time.json?tz="+timezone+"&callback=?",
    function(data){
      var wday = data.datetime.substring(0,3).toLowerCase();
      $("div#store-hours ul li."+wday).addClass("today");
    })
});

//= require store/spree_frontend
if (!window.SpreeStoreHours) {
    window.SpreeStoreHours = {};

    SpreeStoreHours.wday = function() {
        return ["sun","mon","tue","wed","thu","fri","sat"][(new Date()).getDay()];
    }
}

$(function() {
    $("div#store-hours ul li." + SpreeStoreHours.wday()).addClass("today");
});

Fix javascript day calculation to highlight current day


// Place all the behaviors and hooks related to the matching controller here.
// All this logic will automatically be available in application.js.
$(document).ready(function () {
  var currentMeetingNoticePeriod = parseInt($('span.meeting_notice_period').first().text());
  $('#change_meeting_notice_period_resolution_meeting_notice_period').observe_field(1, function () {
    var newMeetingNoticePeriod = parseInt(this.value);
    if (newMeetingNoticePeriod >= currentMeetingNoticePeriod) {
      $('.increase_meeting_notice_period').slideDown();
      $('.decrease_meeting_notice_period').slideUp();
    } else {
      $('.increase_meeting_notice_period').slideUp();
      $('.decrease_meeting_notice_period').slideDown();
    }
  });
  $('#change_meeting_notice_period_resolution_pass_immediately').change(function () {
    if ($('#change_meeting_notice_period_resolution_pass_immediately:checked').val() == '1') {
      $('.pass_immediately_true').show();
      $('.pass_immediately_false').hide();
    } else {
      $('.pass_immediately_true').hide();
      $('.pass_immediately_false').show();
    }
  });
});

// Place all the behaviors and hooks related to the matching controller here.
// All this logic will automatically be available in application.js.
$(document).ready(function () {
  $(document).on('change', '#change_meeting_notice_period_resolution_meeting_notice_period', function() {
    var currentMeetingNoticePeriod = parseInt($('span.meeting_notice_period').first().text(), 10);
    var newMeetingNoticePeriod = parseInt(this.value, 10);
    if (newMeetingNoticePeriod >= currentMeetingNoticePeriod) {
      $('.increase_meeting_notice_period').slideDown();
      $('.decrease_meeting_notice_period').slideUp();
    } else {
      $('.increase_meeting_notice_period').slideUp();
      $('.decrease_meeting_notice_period').slideDown();
    }
  });
  $('#change_meeting_notice_period_resolution_pass_immediately').change(function () {
    if ($('#change_meeting_notice_period_resolution_pass_immediately:checked').val() == '1') {
      $('.pass_immediately_true').show();
      $('.pass_immediately_false').hide();
    } else {
      $('.pass_immediately_true').hide();
      $('.pass_immediately_false').show();
    }
  });
});

Make JS observation on 'change notice period' form live.


import React from 'react';

const withCountry = component =>
	class extends React.Component {
		static displayName = `withCountry(${component.displayName
			|| component.name})`;

		constructor(props) {
			super(props);

			this.state = {
				country: 'US',
			};
		}

		async componentDidMount() {
			try {
				const response = await fetch('//freegeoip.net/json/');
				const data = await response.json();

				this.setState({country: data.country_code});
			}
			catch (err) {
				// Failed to fetch can happen if the user has a blocker (ad, tracker...)
				trackJs.track(`Error when getting the country: ${err.message}`);
			}
		}

		render() {
			const {country} = this.state;

			return React.createElement(component, {
				...this.props,
				country,
			});
		}
	};

export default withCountry;

import React from 'react';

const withCountry = component =>
	class extends React.Component {
		static displayName = `withCountry(${component.displayName
			|| component.name})`;

		constructor(props) {
			super(props);

			this.state = {
				country: 'US',
			};
		}

		async componentDidMount() {
			try {
				const response = await fetch('//ipinfo.io/json');
				const data = await response.json();

				this.setState({country: data.country});
			}
			catch (err) {
				// Failed to fetch can happen if the user has a blocker (ad, tracker...)
				window.trackJs.track(`Error when getting the country: ${err.message}`);

				// trying another server just to be sure
				try {
					const response = await fetch('//get.geojs.io/v1/ip/country.json');
					const data = await response.json();

					this.setState({country: data.country});
				}
				catch (err2) {
					// giving up
					window.trackJs.track(
						`Error when getting the country: ${err2.message}`,
					);
				}
			}
		}

		render() {
			const {country} = this.state;

			return React.createElement(component, {
				...this.props,
				country,
			});
		}
	};

export default withCountry;

Change endpoint for 2 other free services


import React, { Component } from 'react';
import {
  List,
  ListItem,
} from 'react-native-elements';

export default class RepoList extends Component {
  constructor(props) {
    super(props);
  }

  render() {
    return (
      <List>
        {
          this.props.list.map((item, i) => (
            <ListItem
              key={i}
              title={item.name} />
          ))
        }
      </List>
    );
  }
}

import React, { Component } from 'react';
import {
  List,
  ListItem,
} from 'react-native-elements';

export default class RepoList extends Component {
  constructor(props) {
    super(props);
  }

  render() {
    const { navigate } = this.props.navigation;
    return (
      <List>
        {
          this.props.list.map((item, i) => (
            <ListItem
              key={i}
              title={item.name}
              onPress={() => navigate('RepoView', {
                reponame: item.name,
                url: item.url
              })} />
          ))
        }
      </List>
    );
  }
}

Add onPress handlers for each repo list item generated Setup each list item to navigate to a new screen, passing the name of the repository being viewed and the repository's url which would be used by the API to get the README


class DDPNetworkInterface {
    constructor({
        connection,
        noRetry = true,
        method = '/graphql' } = {}
    ) {
        this.connection = connection;
        this.noRetry = noRetry;
        this.method = method;
    }

    query(request) {
        return new Promise((resolve, reject) => {
            this.connection.apply(this.method, [request], { noRetry: this.noRetry }, (err, data) => {
                if (err) {
                    reject(err);
                } else {
                    resolve(data);
                }
            });
        });
    }
}

export { DDPNetworkInterface };

class DDPNetworkInterface {
    constructor({
        connection,
        noRetry = true,
        method = '/graphql' } = {}
    ) {
        this.connection = connection;
        this.noRetry = noRetry;
        this.method = method;
    }

    query(request) {
        return new Promise((resolve, reject) => {
            this.connection.apply(this.method, [request], { noRetry: this.noRetry }, (error, data) => {
                if (error) {
                    resolve({ errors: [error] });
                } else {
                    resolve(data);
                }
            });
        });
    }
}

export { DDPNetworkInterface };

Resolve method error as a standard GraphQL error


const {Menu} = require('electron')
const menubar = require('menubar')
const platform = require('os').platform()

// Toggle with cmd + alt + i
require('electron-debug')({showDevTools: true})

const initialIcon = `${__dirname}/img/png/blank.png`

const mb = menubar({
  width: 220,
  height: 206,
  preloadWindow: true,
  icon: initialIcon
})

// Make menubar accessible to the renderer
global.sharedObject = {mb}

mb.on('ready', () => {
  console.log('app is ready')
  // Workaround to fix window position when statusbar at top for win32
  if (platform === 'win32') {
    if (mb.tray.getBounds().y < 5) {
      mb.setOption('windowPosition', 'trayCenter')
    }
  }
})

mb.on('after-create-window', () => {
  mb.window.loadURL(`file://${__dirname}/index.html`)

  const contextMenu = Menu.buildFromTemplate([
    {label: 'Quit', click: () => {
      mb.app.quit()
    }}
  ])
  mb.tray.on('right-click', () => {
    mb.tray.popUpContextMenu(contextMenu)
  })
})

const {Menu} = require('electron')
const platform = require('os').platform()
const menubar = require('menubar')

// Toggle with cmd + alt + i
require('electron-debug')({showDevTools: true})

const initialIcon = `${__dirname}/img/png/blank.png`

const mb = menubar({
  width: 220,
  height: 206,
  preloadWindow: true,
  icon: initialIcon
})

// Make menubar accessible to the renderer
global.sharedObject = {mb}

mb.on('ready', () => {
  console.log('app is ready')
  // Workaround to fix window position when statusbar at top for win32
  if (platform === 'win32') {
    if (mb.tray.getBounds().y < 5) {
      mb.setOption('windowPosition', 'trayCenter')
    }
  }
})

mb.on('after-create-window', () => {
  mb.window.loadURL(`file://${__dirname}/index.html`)

  const contextMenu = Menu.buildFromTemplate([
    {label: 'Quit', click: () => {
      mb.app.quit()
    }}
  ])
  mb.tray.on('right-click', () => {
    mb.tray.popUpContextMenu(contextMenu)
  })
})

Reorder imports for a happy linter


var app = require('app');  // Module to control application life.
var BrowserWindow = require('browser-window');  // Module to create native browser window.

// Report crashes to our server.
require('crash-reporter').start();

// Keep a global reference of the window object, if you don't, the window will
// be closed automatically when the JavaScript object is garbage collected.
var mainWindow = null;

// Quit when all windows are closed.
app.on('window-all-closed', function() {
  // On OS X it is common for applications and their menu bar
  // to stay active until the user quits explicitly with Cmd + Q
  if (process.platform != 'darwin') {
    app.quit();
  }
});

// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
app.on('ready', function() {
  // Create the browser window.
  mainWindow = new BrowserWindow({width: 1024, height: 600});

  // and load the index.html of the app.
  mainWindow.loadUrl('file://' + __dirname + '/build/index.html');

  // Emitted when the window is closed.
  mainWindow.on('closed', function() {
    // Dereference the window object, usually you would store windows
    // in an array if your app supports multi windows, this is the time
    // when you should delete the corresponding element.
    mainWindow = null;
  });
});

var app = require('app');  // Module to control application life.
var BrowserWindow = require('browser-window');  // Module to create native browser window.
var process = require('process');

// avoid https://github.com/atom/electron/issues/550
if (process.platform == "darwin") {
  process.env['PATH'] = "/usr/local/sbin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin"
}

// Report crashes to our server.
require('crash-reporter').start();

// Keep a global reference of the window object, if you don't, the window will
// be closed automatically when the JavaScript object is garbage collected.
var mainWindow = null;

// Quit when all windows are closed.
app.on('window-all-closed', function() {
  // On OS X it is common for applications and their menu bar
  // to stay active until the user quits explicitly with Cmd + Q
  if (process.platform != 'darwin') {
    app.quit();
  }
});

// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
app.on('ready', function() {
  // Create the browser window.
  mainWindow = new BrowserWindow({width: 1024, height: 600});

  // and load the index.html of the app.
  mainWindow.loadUrl('file://' + __dirname + '/build/index.html');

  // Emitted when the window is closed.
  mainWindow.on('closed', function() {
    // Dereference the window object, usually you would store windows
    // in an array if your app supports multi windows, this is the time
    // when you should delete the corresponding element.
    mainWindow = null;
  });
});

Add PATH env manipulation when platform is darwin


"use strict"

const qs = require('qs')
const moment = require('moment')
const templateStore = require('./templates/templateStore.js').templates

const GITHUB_API = "https://api.github.com"

async function searchGithub(search) {
    const searchParameters = {
        q: 'Vue.js',
        sort: 'stars',
        pushed: '>' + moment().subtract(1, 'week').format('YYYY-MM-DD'),
        order: 'desc'
    }
    const url = `${GITHUB_API}/search/repositories?${qs.stringify(searchParameters)}`
    console.log(url)
    const response = await fetch(url)
    return await response.json()
}

function renderResults(results) {
	console.log(results)
    const resultElement = document.querySelector('#results')
	resultElement.innerHTML = templateStore.searchResults(results)
}

function indicateError() {
    alert("System error") //TODO this should be rendered nicely in html
}

async function runSearch() {
    try {

        const searchResults = await searchGithub("Vue.js")
        renderResults(searchResults)

    } catch(e) {
        console.error(e)
        indicateError()
    }
}

document.addEventListener('DOMContentLoaded', function() {
    runSearch()
})

"use strict"

const qs = require('qs')
const moment = require('moment')
const appContext = {
	templateStore: require('./templates/templateStore.js').templates
}

const GITHUB_API = "https://api.github.com"

class SearchResultModel {
	constructor (data) {
		console.log(data)
		this.data = data
	}
}

class SearchResultsView {
	constructor (element, model, context) {
		this.element = element
		this.model = model
		this.template = context.templateStore.searchResults
	}

	render() {
		return this.element.innerHTML = this.template(this.model.data)
	}
}

async function searchGithub(search) {
    const searchParameters = {
        q: 'Vue.js',
        sort: 'stars',
        pushed: '>' + moment().subtract(1, 'week').format('YYYY-MM-DD'),
        order: 'desc'
    }
    const url = `${GITHUB_API}/search/repositories?${qs.stringify(searchParameters)}`
    const response = await fetch(url)
    return new SearchResultModel(await response.json())
}

function indicateError() {
    alert("System error") //TODO this should be rendered nicely in html
}

async function runSearch() {
    try {

        const model = await searchGithub("Vue.js")
		const view = new SearchResultsView(
			document.querySelector('#results'),
			model,
			appContext
		)
		view.render()
    } catch(e) {
        console.error(e)
        indicateError()
    }
}

document.addEventListener('DOMContentLoaded', function() {
    runSearch()
})

Use classes for a better defined architecture. Still not split in various files though, but that's minor


var logger = require('../support/logger');

var ItemSchema = require('../domain/item-schema');
var instanceSchemaCorrelator = require('../http/instance-schema-correlator');

var instanceRequest = module.exports = {
    handle: function(ljsReq, res, callback) {
        var collectionName = ljsReq.ljsUrl().collectionName;
        ItemSchema.findByCollectionName(collectionName, function(err, itemSchema) {
            if (err) {
                callback(err);
            } else {
                if (itemSchema) {
                    handleRequest(itemSchema, ljsReq);
                    handleResponse(itemSchema, ljsReq, res);
                }
                callback();
            }
        });
    }
};

function handleRequest(itemSchema, ljsReq) {
    itemSchema.registerLoopbackModel(ljsReq.req.app);
    logger.info('Loopback Model created for JSON Schema collectionName: ', itemSchema.collectionName);
};

function handleResponse(itemSchema, ljsReq, res) {
    instanceSchemaCorrelator.correlate(itemSchema, ljsReq, res);
};

var logger = require('../support/logger');

var ItemSchema = require('../domain/item-schema');
var instanceSchemaCorrelator = require('../http/instance-schema-correlator');

var instanceRequest = module.exports = {
    handle: function(ljsReq, res, callback) {
        var collectionName = ljsReq.ljsUrl().collectionName;
        ItemSchema.findByCollectionName(collectionName, function(err, itemSchema) {
            if (err) { return callback(err); };

            if (itemSchema) {
                handleRequest(itemSchema, ljsReq);
                handleResponse(itemSchema, ljsReq, res);
            }
            callback();
        });
    }
};

function handleRequest(itemSchema, ljsReq) {
    itemSchema.registerLoopbackModel(ljsReq.req.app);
    logger.info('Loopback Model created for JSON Schema collectionName: ', itemSchema.collectionName);
};

function handleResponse(itemSchema, ljsReq, res) {
    instanceSchemaCorrelator.correlate(itemSchema, ljsReq, res);
};

Improve readability on error handling.


'use strict'
// https://github.com/purposeindustries/window-or-global
module.exports = (typeof self === 'object' && self.self === self && self) ||
  (typeof global === 'object' && global.global === global && global) ||
  {}

'use strict'
// https://github.com/purposeindustries/window-or-global
module.exports = (typeof self === 'object' && self.self === self && self) ||
  (typeof global === 'object' && global.global === global && global) ||
  this

Change to Fix breaking window object


const gulp = require('gulp');
const autoprefixer = require('gulp-autoprefixer');
const concat = require('gulp-concat');
const sass = require('gulp-sass');

const sassPaths = [
	'bower_components/foundation-sites/scss'
];

const jsPaths = [
	'bower_components/jquery/dist/jquery.min.js',
	'bower_components/foundation-sites/dist/foundation.min.js',
	'js/*.js'
];

gulp.task('sass', function() {
  return gulp.src('scss/*.scss').pipe(
	  sass({
    	includePaths: sassPaths
  	})
		.on('error', sass.logError))
  	.pipe(autoprefixer({
  		browsers: ['last 2 versions', 'ie >= 9']
  	}))
		.pipe(concat('app.css'))
		.pipe(gulp.dest('public/assets/css'));
});

gulp.task('scripts', function() {
	return gulp.src(jsPaths)
		.pipe(concat('app.js'))
		.pipe(gulp.dest('public/assets/js'));
});

gulp.task('watch', function() {
	gulp.watch(['scss/**/*.scss', 'js/*.js'], ['sass','scripts']);
});

gulp.task('default', ['sass', 'scripts']);

const gulp = require('gulp');
const autoprefixer = require('gulp-autoprefixer');
const concat = require('gulp-concat');
const sass = require('gulp-sass');

const sassPaths = [
	'bower_components/foundation-sites/scss'
];

const jsPaths = [
	'bower_components/jquery/dist/jquery.min.js',
	'bower_components/foundation-sites/dist/js/foundation.min.js',
	'js/*.js'
];

gulp.task('sass', function() {
  return gulp.src('scss/*.scss').pipe(
	  sass({
    	includePaths: sassPaths
  	})
		.on('error', sass.logError))
  	.pipe(autoprefixer({
  		browsers: ['last 2 versions', 'ie >= 9']
  	}))
		.pipe(concat('app.css'))
		.pipe(gulp.dest('public/assets/css'));
});

gulp.task('scripts', function() {
	return gulp.src(jsPaths)
		.pipe(concat('app.js'))
		.pipe(gulp.dest('public/assets/js'));
});

gulp.task('watch', function() {
	gulp.watch(['scss/**/*.scss', 'js/*.js'], ['sass','scripts']);
});

gulp.task('default', ['sass', 'scripts']);

Fix foundation-sites not being loaded


const organisationUnitConstants = require('../constants/organisation-unit')

module.exports = function (pageTitle, subNav, organisationLevel) {
  let selectedSubNavTitle = ''
  subNav.forEach(function (item) {
    if (item.active) {
      selectedSubNavTitle = item.title
    }
  })

  let selectedAreaTitle = pageTitle
  if (organisationLevel === organisationUnitConstants.NATIONAL.name) {
    selectedAreaTitle = 'National'
  }

  return {
    first: selectedSubNavTitle,
    second: selectedAreaTitle,
    third: 'HMPPS'
  }
}

const organisationUnitConstants = require('../constants/organisation-unit')

module.exports = function (pageTitle, subNav, organisationLevel) {
  let selectedSubNavTitle = ''
  subNav?.forEach(function (item) {
    if (item.active) {
      selectedSubNavTitle = item.title
    }
  })

  let selectedAreaTitle = pageTitle
  if (organisationLevel === organisationUnitConstants.NATIONAL.name) {
    selectedAreaTitle = 'National'
  }

  return {
    first: selectedSubNavTitle,
    second: selectedAreaTitle,
    third: 'HMPPS'
  }
}

Add null check to get unit tests passing


"use strict";

require('./gulp/unit-tests');
require('./gulp/documentation');

"use strict";

const gulp = require('gulp');

require('./gulp/unit-tests');
require('./gulp/documentation');

gulp.task('default', ['test', 'doc']);

Add gulp default task to run unit tests and build documentation


import Ember from 'ember';

export default Ember.Service.extend({
  currency () {
    return '$';
  }
});

import Ember from 'ember';

export default Ember.Service.extend({
  currency () {
    return '$';
  },

  monthBegins () {
    return 5;
  }
});

Add user settings method to return day that month begins


define(['backbone'], function(B) {
  var M = {};

  M.Entry = B.Model.extend({});
  M.Entries = B.Collection.extend({
    model: M.Entry,
    url: 'entries/data.json'
  });

  return M;
});

define(['backbone', 'marked'], function(B, marked) {
  var M = {};

  M.Entry = B.Model.extend({
    fetchSrc: function(cbs) {
      require(['text!' + this.get('src')], function(src) {
        this.set('content', marked(src));
        _.finish(cbs);
      });
    }
  });
  M.Entries = B.Collection.extend({
    model: M.Entry,
    url: 'entries/data.json'
  });

  return M;
});

Add fetchSrc method to entry model.


var kamo = require('../src/kamo');
var assert = require('assert');

describe('kamo.Stream', function () {
  beforeEach(function () {
    this.stream = new kamo.Stream();
  });

  describe('#publish', function () {
    it('publishes a message to its subscribers in the order defined', function (done) {
      var index = 0;
      this.stream.subscribe(function (message) {
        assert.equal(index, 0);
        index++;
      });
      this.stream.subscribe(function (message) {
        assert.equal(index, 1);
        index++;
        done();
      });
      this.stream.publish();
    });
  });

  describe('#subscribe', function () {
    it('subscribes a callback called with the published message', function (done) {
      var publishedMessage = 'message';
      this.stream.subscribe(function (message) {
        assert.equal(message, publishedMessage);
        done();
      })
      this.stream.publish(publishedMessage);
    });
  });
});

var kamo = require('../src/kamo');
var assert = require('assert');

describe('kamo.Stream', function () {
  beforeEach(function () {
    this.stream = new kamo.Stream();
  });

  describe('.fromEventHandlerSetter', function () {
    it('creates a new Stream from an object and its setter', function (done) {
      var object = {};
      kamo.Stream.fromEventHandlerSetter(object, 'eventHandlerName').subscribe(function (message) {
        done();
      });
      object.eventHandlerName('message');
    });
  });

  describe('.fromEventHandlerFunction', function () {
    it('creates a new Stream from an object, its function name, and optional arguments', function (done) {
      var object = {
        eventHandlerName: function (publisher, argument1, argument2) {
          this.publisher = publisher;
          this.argument1 = 'argument1';
          this.argument2 = 'argument2';
        }
      };
      kamo.Stream.fromEventHandlerFunction(
        object,
        'eventHandlerName',
        'argument1',
        'argument2'
      ).subscribe(function (message) {
        assert.equal(message, 'message');
        done();
      });
      assert.equal(object.argument1, 'argument1');
      assert.equal(object.argument2, 'argument2');
      object.publisher('message');
    });
  });

  describe('#publish', function () {
    it('publishes a message to its subscribers in the order defined', function (done) {
      var index = 0;
      this.stream.subscribe(function (message) {
        assert.equal(index, 0);
        index++;
      });
      this.stream.subscribe(function (message) {
        assert.equal(index, 1);
        index++;
        done();
      });
      this.stream.publish('message');
    });
  });

  describe('#subscribe', function () {
    it('subscribes a callback called with the published message', function (done) {
      var publishedMessage = 'message';
      this.stream.subscribe(function (message) {
        assert.equal(message, publishedMessage);
        done();
      })
      this.stream.publish(publishedMessage);
    });
  });
});

Add unit tests for the class methods of Stream


(function() {
  'use strict';

  var app = angular.module('radar.users');

  app.factory('UserInfoController', ['ModelDetailController', '$injector', function(ModelDetailController, $injector) {
    function UserInfoController($scope) {
      var self = this;

      $injector.invoke(ModelDetailController, self, {
        $scope: $scope,
        params: {}
      });

      self.load($scope.user).then(function() {
        self.view();
      });
    }

    UserInfoController.$inject = ['$scope'];
    UserInfoController.prototype = Object.create(ModelDetailController.prototype);

    return UserInfoController;
  }]);

  app.directive('userInfoComponent', ['UserInfoController', function(UserInfoController) {
    return {
      scope: {
        user: '='
      },
      controller: UserInfoController,
      templateUrl: 'app/users/user-info-component.html'
    };
  }]);
})();

(function() {
  'use strict';

  var app = angular.module('radar.users');

  app.factory('UserPermission', ['AdminPermission', function(AdminPermission) {
    return AdminPermission;
  }]);

  app.factory('UserInfoController', ['ModelDetailController', '$injector', 'UserPermission', function(ModelDetailController, $injector, UserPermission) {
    function UserInfoController($scope) {
      var self = this;

      $injector.invoke(ModelDetailController, self, {
        $scope: $scope,
        params: {
          permission: new UserPermission()
        }
      });

      self.load($scope.user).then(function() {
        self.view();
      });
    }

    UserInfoController.$inject = ['$scope'];
    UserInfoController.prototype = Object.create(ModelDetailController.prototype);

    return UserInfoController;
  }]);

  app.directive('userInfoComponent', ['UserInfoController', function(UserInfoController) {
    return {
      scope: {
        user: '='
      },
      controller: UserInfoController,
      templateUrl: 'app/users/user-info-component.html'
    };
  }]);
})();

Add client side user detail permission


// Copyright 2016, EMC, Inc.

'use strict';

module.exports = {
    friendlyName: 'base-uefi',
    injectableName: 'Task.Base.uefi',
    runJob: 'Job.Uefi',
    requiredOptions: [
        'profile'
    ],
    requiredProperties: {},
    properties: {}
};

// Copyright 2016, EMC, Inc.

'use strict';

module.exports = {
    friendlyName: 'base-uefi',
    injectableName: 'Task.Base.uefi',
    runJob: 'Job.Uefi',
    requiredOptions: [
        'profile',
        'repo',
        'uefitool'
    ],
    requiredProperties: {},
    properties: {}
};

Add repo and uefi to required options list


function formatTime(num){
    var minute = '0' + String(Math.floor(num / 60));
    var second = '0' + String(num % 60);

    return minute.substr(-2) + ':' + second.substr(-2);
}

function resetTimer(){
    var timer = $('#timer');
    timer.text('00:00');
    timer.css('color', 'lime');
}

function count(current, maximum){
    var timer = $('#timer');
    timer.text(formatTime(current));

    var timeoutId = window.setTimeout(count, 1000, current + 1, maximum);
    $('#stop').click('click', function(){
        window.clearTimeout(timeoutId);
    });
    $('#reset').click(function(){
        window.clearTimeout(timeoutId);
        resetTimer();
    })
    if (current === maximum){
        timer.css('color', 'red');
        document.getElementById('finished').play();
    }
    if (current === maximum - 60){
        document.getElementById('finished').play();
    }
}

function extendFontSize(){
    var height = $(window).height();
    var width = $(window).width();
    $('#timer').css('font-size', (Math.min(width / 3 | 0, height)) + 'px');
}

$(function(){
    extendFontSize();
    $('#start').click(function(){
        resetTimer();
        var minute = Math.floor($('#minute').val());
        var second = Math.floor($('#second').val());
        count(0, minute * 60 + second);
    });
});

function formatTime(num){
    var minute = '0' + String(Math.floor(num / 60));
    var second = '0' + String(num % 60);

    return minute.substr(-2) + ':' + second.substr(-2);
}

function resetTimer(){
    var timer = $('#timer');
    timer.text('00:00');
    timer.css('color', 'lime');
}

function count(current, maximum){
    var timer = $('#timer');
    timer.text(formatTime(current));

    var timeoutId = window.setTimeout(count, 1000, current + 1, maximum);
    $('#stop').click('click', function(){
        window.clearTimeout(timeoutId);
    });
    $('#reset').click(function(){
        window.clearTimeout(timeoutId);
        resetTimer();
    })
    if (current === maximum){
        timer.css('color', 'red');
        document.getElementById('finished').play();
    }
    if (current === maximum - 60){
        document.getElementById('finished').play();
    }
}

function extendFontSize(){
    var height = $(window).height();
    var width = $(window).width();
    $('#timer').css('font-size', (Math.min(width / 3 | 0, height)) + 'px');
}

$(function(){
    extendFontSize();
    $(window).resize(extendFontSize);
    $('#start').click(function(){
        resetTimer();
        var minute = Math.floor($('#minute').val());
        var second = Math.floor($('#second').val());
        count(0, minute * 60 + second);
    });
});

Change font-size even when the window is resized


/*
 * grunt-contrib-watch
 * http://gruntjs.com/
 *
 * Copyright (c) 2014 "Cowboy" Ben Alman, contributors
 * Licensed under the MIT license.
 */

'use strict';

module.exports = function(grunt) {
  grunt.initConfig({
    jshint: {
      all: [
        'Gruntfile.js',
        'tasks/**/*.js',
        '<%= nodeunit.tests %>',
      ],
      options: {
        jshintrc: '.jshintrc',
      },
    },
    watch: {
      all: {
        files: ['<%= jshint.all %>'],
        tasks: ['jshint', 'nodeunit'],
      },
    },
    nodeunit: {
      tests: ['test/tasks/*_test.js'],
    },
  });

  // Dynamic alias task to nodeunit. Run individual tests with: grunt test:events
  grunt.registerTask('test', function(file) {
    grunt.config('nodeunit.tests', String(grunt.config('nodeunit.tests')).replace('*', file || '*'));
    grunt.task.run('nodeunit');
  });

  grunt.loadTasks('tasks');

  grunt.loadNpmTasks('grunt-contrib-jshint');
  grunt.loadNpmTasks('grunt-contrib-nodeunit');
  grunt.loadNpmTasks('grunt-contrib-internal');

  grunt.registerTask('test', ['jshint', 'nodeunit']);
  grunt.registerTask('default', ['test', 'build-contrib']);
};

/*
 * grunt-contrib-watch
 * http://gruntjs.com/
 *
 * Copyright (c) 2014 "Cowboy" Ben Alman, contributors
 * Licensed under the MIT license.
 */

'use strict';

module.exports = function(grunt) {
  grunt.initConfig({
    jshint: {
      all: [
        'Gruntfile.js',
        'tasks/**/*.js',
        '<%= nodeunit.tests %>',
      ],
      options: {
        jshintrc: '.jshintrc',
      },
    },
    watch: {
      all: {
        files: ['<%= jshint.all %>'],
        tasks: ['jshint', 'nodeunit'],
      },
    },
    nodeunit: {
      tests: ['test/tasks/*_test.js'],
    },
  });

  // Dynamic alias task to nodeunit. Run individual tests with: grunt test:events
  grunt.registerTask('test', function(file) {
    grunt.task.run('jshint');
    grunt.config('nodeunit.tests', String(grunt.config('nodeunit.tests')).replace('*', file || '*'));
    grunt.task.run('nodeunit');
  });

  grunt.loadTasks('tasks');

  grunt.loadNpmTasks('grunt-contrib-jshint');
  grunt.loadNpmTasks('grunt-contrib-nodeunit');
  grunt.loadNpmTasks('grunt-contrib-internal');

  grunt.registerTask('default', ['test', 'build-contrib']);
};

Fix running individual tests with 'test' task


module.exports = function(grunt) {
  grunt.initConfig({
    buildcontrol: {
      options: {
        dir: 'demo',
        commit: true,
        push: true,
        message: 'Built %sourceName% from commit %sourceCommit% on branch %sourceBranch%.'
      },
      pages: {
        options: {
          remote: 'git@https://git.corp.adobe.com/amos/spindle.git',
          branch: 'gh-pages'
        }
      }
    }
  });
  grunt.loadNpmTasks('grunt-build-control');
  grunt.registerTask('deploy', ['buildcontrol:pages']);
}

module.exports = function(grunt) {
  grunt.initConfig({
    buildcontrol: {
      options: {
        dir: 'demo',
        commit: true,
        push: true,
        message: 'Built %sourceName% from commit %sourceCommit% on branch %sourceBranch%.'
      },
      pages: {
        options: {
          remote: 'git@http://git.corp.adobe.com/amos/spindle.git',
          branch: 'gh-pages'
        }
      }
    }
  });
  grunt.loadNpmTasks('grunt-build-control');
  grunt.registerTask('deploy', ['buildcontrol:pages']);
}

Use http instead of https for grunt deployment.


var utils = require('./utils')

function api (opbeat, queuedCommands) {
  this.q = []

  this.opbeat = opbeat
  this.execute = utils.functionBind(this.execute, this)
  this.push = utils.functionBind(this.push, this)

  if (queuedCommands) {
    for (var i = 0; i < queuedCommands.length; i++) {
      var cmd = queuedCommands[i]
      this.push(cmd)
    }
  }
}

api.prototype.execute = function (cmd, args) {
  return this.opbeat[cmd].apply(this.opbeat, args)
}

api.prototype.push = function () {
  var argsArray = Array.prototype.slice.call(arguments)

  var cmd = argsArray.slice(0, 1)[0]
  var args = argsArray.slice(1)

  this.execute(cmd, args)
}

module.exports = api

var utils = require('./utils')

function api (opbeat, queuedCommands) {
  this.q = []

  this.opbeat = opbeat
  this.execute = utils.functionBind(this.execute, this)
  this.push = utils.functionBind(this.push, this)

  if (queuedCommands) {
    for (var i = 0; i < queuedCommands.length; i++) {
      var cmd = queuedCommands[i]
      this.push.apply(this, cmd)
    }
  }
}

api.prototype.execute = function (cmd, args) {
  return this.opbeat[cmd].apply(this.opbeat, args)
}

api.prototype.push = function () {
  var argsArray = Array.prototype.slice.call(arguments)

  var cmd = argsArray.slice(0, 1)[0]
  var args = argsArray.slice(1)

  this.execute(cmd, args)
}

module.exports = api

Fix 'Cannot read property 'apply' of undefined' introduced in daaae82


import $ from 'jquery';
import BaseView from '../../utils/_base-view';

class SearchBoxView extends BaseView {

  get el() { return '#search-box'; }

  get query() {
    return $('#search-box input').val();
  }

  displayQuery(queryString) {
    let displayString = decodeURIComponent(queryString.replace(/\+/g, '%20'));
    $('#search-box input').val(displayString);
  }

  initialize(options = {}) {
    this.options = options;
    if(!this.options.query) {
      this.handleEmptyQuery();
    } else {
      this.displayQuery(this.options.query);
    }
  }

  handleEmptyQuery() {
    //window.console.log('query was empty');
  }

}

export default SearchBoxView;

import $ from 'jquery';
import BaseView from '../../utils/_base-view';
import Messages from '../../utils/messenger';

class SearchBoxView extends BaseView {

  get el() { return '.site-search-form'; }

  get queryInput() {
    return $('#search-box input').val();
  }

  displayQuery(queryString) {
    let displayString = decodeURIComponent(queryString.replace(/\+/g, '%20'));
    $('#search-box input').val(displayString);
  }

  initialize(options = {}) {
    this.options = options;
    if(!this.options.query) {
      this.handleEmptyQuery();
    } else {
      this.displayQuery(this.options.query);
    }

    this.listenTo(Messages, 'search:getInput', this.rebootSearch);
  }

  handleEmptyQuery() {
    //window.console.log('query was empty');
  }

  rebootSearch() {
    // Get the form input & send it up to the topmost search view
    Messages.trigger('search:all', this.queryInput);
  }

}

export default SearchBoxView;

Add a little more behavior to search box view


import {
  ADD,
  SHOW,
  DISMISS
} from './actions.js'

const INITIAL_STATE = {
  items: [],
  current: null
}

function show (state, payload) {
  return {
    ...state,
    current: Object.assign({}, payload)
  }
}

function add (state, payload) {
  if (!state.current) {
    return {
      ...state,
      current: Object.assign({}, payload)
    }
  }

  return {
    ...state,
    items: state.items.concat([payload])
  }
}

function dismiss (state) {
  if (!state.items.length) {
    return {
      ...state,
      current: null
    }
  }

  const [current, ...items] = state.items

  return {
    ...state,
    items,
    current
  }
}

export default function snackBarReducer (state = INITIAL_STATE, action) {
  switch (action.type) {
    case SHOW: return show(state, action.payload)
    case ADD: return add(state, action.payload)
    case DISMISS: return dismiss(state)
    default: return state
  }
}

import {
  ADD,
  SHOW,
  DISMISS
} from './actions.js'

const INITIAL_STATE = {
  items: [],
  current: null
}

function transformProps (item) {
  const { title } = item
  const transformedItem = Object.assign({}, item)

  if (title) {
    transformedItem.children = title
  }

  return transformedItem
}

function show (state, payload) {
  return {
    ...state,
    current: transformProps(payload)
  }
}

function add (state, payload) {
  const item = transformProps(payload)

  if (!state.current && state.items.length) {
    const [current, ...items] = state.items

    return {
      ...state,
      items: items.concat([item]),
      current
    }
  }

  if (!state.current) {
    return {
      ...state,
      current: item
    }
  }

  return {
    ...state,
    items: state.items.concat([item])
  }
}

function dismiss (state) {
  if (!state.items.length) {
    return {
      ...state,
      current: null
    }
  }

  const [current, ...items] = state.items

  return {
    ...state,
    items,
    current
  }
}

export default function snackBarReducer (state = INITIAL_STATE, action) {
  switch (action.type) {
    case SHOW: return show(state, action.payload)
    case ADD: return add(state, action.payload)
    case DISMISS: return dismiss(state)
    default: return state
  }
}

Support title props in addition to children


export default {
  customDiffConfig: {
    thresholdType: 'pixel',
    threshold: 1,
  },
};

export default {
  customDiffConfig: {
    thresholdType: 'pixel',
    threshold: 5,
  },
};

Change blink-diff threshold 1 to 5


var _cache = [];

function EventHandler(token, eventName, handler, enabled) {
  this.elementToken = token;
  this.eventName = 'click.namespacename';
  this.eventType = 'click';
  this.namespace = 'namespace';
  this.handler = handler;
  this.enabled = enabled;
}

module.exports = {
  add : function(token, eventName, handler) {
    _cache.push(new EventHandler(token, eventName, handler, true));
  },
  removeHandler : function(token, handler) {
    var index = _cache.findIndex(function(entry) {
      return entry.elementToken === token && entry.handler === handler;
    });
    _cache.splice(index, 1);
  },
  getHandlers : function(token, eventName) {
    return _cache
      .filter(function(entry) {
        return entry.elementToken === token && entry.eventName === eventName;
      })
      .map(function(h) {
        return h.handler;
      });
  }
};

var _cache = [];

function EventHandler(token, eventName, handler, enabled) {
  this.elementToken = token;
  this.eventName = 'click.namespacename';
  this.eventType = 'click';
  this.namespace = 'namespace';
  this.handler = handler;
  this.enabled = enabled;
}

module.exports = {
  add : function(token, eventName, handler) {
    _cache.push(new EventHandler(token, eventName, handler, true));
  },
  removeHandler : function(token, handler) {
    var index = _cache.findIndex(function(entry) {
      return entry.elementToken === token && entry.handler === handler;
    });
    _cache.splice(index, 1);
  },
  getHandlers : function(token, eventName) {
    return _cache.reduce(function(acc, entry) {
      if (entry.elementToken === token && entry.eventName === eventName) {
        acc.push(entry.handler);
      }
    }, []);
  }
};

Use an accumulator instead and only do one iteration - much more efficient


/**
 * Returns a very important number
 * @returns {number}
 */
export default function theDefaultExport() {
  return 42;
}

/**
 * Value that can be incremented
 * @type {number}
 */
export let value = 0;

/**
 * Increments the value by one.
 */
export function incrementValue() {
  value++;
}

export function feature() {
  return 'cool feature';
}

export function something() {
  return 'something';
}

/**
 * Returns a very important number
 * @returns {number}
 */
export default function theDefaultExport() {
  return 42;
}

/**
 * Value that can be incremented
 * @type {number}
 */
export let value = 0;

/**
 * Increments the value by one.
 */
export function incrementValue() {
  value++;
}

export function feature() {
  return 'cool';
}

export function something() {
  return 'something';
}

Revert "fix: add missing feature"


//TODO INSTLALL ESLINT THINGS

import React from 'react'
import { render } from 'react-dom'
import { Router, Route, Redirect, IndexRoute } from 'react-router'
import { Provider } from 'react-redux'
import createHistory from 'history/lib/createBrowserHistory'
// import { createHistory } from 'history'
// import createHistory from 'history/lib/createHashHistory'
import useScroll from 'scroll-behavior/lib/useStandardScroll'
import configureStore from 'store/configureStore'

// TODO: use fbgraph, add like/share buttons

/* Pages */
import Home from 'pages/Home'
import Team from 'pages/Team'
import Releve from 'pages/Releve'
import NotFound from 'pages/NotFound'

import DevTools from 'containers/DevTools'

/* Global styles */
import 'assets/css/normalize.css'
import 'assets/css/skeleton.css'
import 'assets/styles/global.css'

const App = ({ children }) => children
// const history = createHistory({ queryKey: false })
const history = useScroll(createHistory)()

const INDEX = (
  <Provider store={ configureStore(history) }>
    <div>
      <Router history={ history }>
        <Route path="/" component={ App }>
          <IndexRoute component={ Home } />
          <Redirect from="home" to="/" />
          <Route path="team" component={ Team } />
          <Route path="releve" component={ Releve } />
          <Route path="*" component={ NotFound } />
        </Route>
      </Router>
      { IN_DEV_MODE && false ? <DevTools /> : null }
    </div>
  </Provider>
)

render(
  INDEX,
  document.getElementById('content')
)

//TODO INSTLALL ESLINT THINGS

import React from 'react'
import { render } from 'react-dom'
import { Router, Route, Redirect, IndexRoute } from 'react-router'
import { Provider } from 'react-redux'
import createHistory from 'history/lib/createHashHistory'

import configureStore from 'store/configureStore'

// TODO: use fbgraph, add like/share buttons

/* Pages */
import Home from 'pages/Home'
import Team from 'pages/Team'
import Releve from 'pages/Releve'
import NotFound from 'pages/NotFound'

/* Global styles */
import 'assets/css/normalize.css'
import 'assets/css/skeleton.css'
import 'assets/css/custom.css'

const App = ({ children }) => children
const history = createHistory({ queryKey: false })

const INDEX = (
  <Provider store={ configureStore(history) }>
    <Router history={ history }>
      <Route path="/" component={ App }>
        <IndexRoute component={ Home } />
        <Redirect from="home" to="/" />
        <Route path="team" component={ Team } />
        <Route path="releve" component={ Releve } />
        <Route path="*" component={ NotFound } />
      </Route>
    </Router>
  </Provider>
)

render(
  INDEX,
  document.getElementById('content')
)

Use hashhistory, no scroll thing, remove devtools.


import copySitecoreLibraries from './tasks/copy-sitecore-libraries';
import deploy from './tasks/deploy';
import nugetRestore from './tasks/nuget-restore';
import packageWebsite from './tasks/package-website';
import publishProjects from './tasks/publish-projects';
import publishTDS from './tasks/publish-tds';
import transforms from './tasks/transforms';

export {
  copySitecoreLibraries,
  deploy,
  nugetRestore,
  packageWebsite,
  publishProjects,
  publishTDS,
  transforms,
};

import copySitecoreLibraries from './tasks/copy-sitecore-libraries';
import deploy from './tasks/deploy';
import nugetRestore from './tasks/nuget-restore';
import packageWebsite from './tasks/package-website';
import publishProjects from './tasks/publish-projects';
import publishTDS from './tasks/publish-tds';
import transforms from './tasks/transforms';

export default {
  copySitecoreLibraries,
  deploy,
  nugetRestore,
  packageWebsite,
  publishProjects,
  publishTDS,
  transforms,
};

Set the export as default


const { createLogger, LEVELS: { INFO } } = require('./loggers')
const LoggerConsole = require('./loggers/console')
const Cluster = require('./cluster')
const createProducer = require('./producer')
const createConsumer = require('./consumer')

const { assign } = Object

module.exports = class Client {
  constructor({
    brokers,
    ssl,
    sasl,
    clientId,
    connectionTimeout,
    retry,
    logLevel = INFO,
    logCreator = LoggerConsole,
  }) {
    this.logger = createLogger({ level: logLevel, logCreator })
    this.createCluster = () =>
      new Cluster({
        logger: this.logger,
        brokers,
        ssl,
        sasl,
        clientId,
        connectionTimeout,
        retry,
      })
  }

  /**
   * @public
   */
  producer({ createPartitioner, retry } = {}) {
    const cluster = this.createCluster()
    return createProducer({
      retry: assign({}, cluster.retry, retry),
      logger: this.logger,
      cluster,
      createPartitioner,
    })
  }

  /**
   * @public
   */
  consumer(
    {
      groupId,
      createPartitionAssigner,
      sessionTimeout,
      heartbeatInterval,
      maxBytesPerPartition,
      minBytes,
      maxBytes,
      maxWaitTimeInMs,
      retry,
    } = {}
  ) {
    const cluster = this.createCluster()
    return createConsumer({
      retry: assign({}, cluster.retry, retry),
      logger: this.logger,
      cluster,
      groupId,
      createPartitionAssigner,
      sessionTimeout,
      heartbeatInterval,
      maxBytesPerPartition,
      minBytes,
      maxBytes,
      maxWaitTimeInMs,
    })
  }
}

const { createLogger, LEVELS: { INFO } } = require('./loggers')
const LoggerConsole = require('./loggers/console')
const Cluster = require('./cluster')
const createProducer = require('./producer')
const createConsumer = require('./consumer')

const { assign } = Object

module.exports = class Client {
  constructor({
    brokers,
    ssl,
    sasl,
    clientId,
    connectionTimeout,
    retry,
    logLevel = INFO,
    logCreator = LoggerConsole,
  }) {
    this.logger = createLogger({ level: logLevel, logCreator })
    this.createCluster = () =>
      new Cluster({
        logger: this.logger,
        brokers,
        ssl,
        sasl,
        clientId,
        connectionTimeout,
        retry,
      })
  }

  /**
   * @public
   */
  producer({ createPartitioner, retry } = {}) {
    const cluster = this.createCluster()
    return createProducer({
      retry: assign({}, cluster.retry, retry),
      logger: this.logger,
      cluster,
      createPartitioner,
    })
  }

  /**
   * @public
   */
  consumer(
    {
      groupId,
      partitionAssigners,
      sessionTimeout,
      heartbeatInterval,
      maxBytesPerPartition,
      minBytes,
      maxBytes,
      maxWaitTimeInMs,
      retry,
    } = {}
  ) {
    const cluster = this.createCluster()
    return createConsumer({
      retry: assign({}, cluster.retry, retry),
      logger: this.logger,
      cluster,
      groupId,
      partitionAssigners,
      sessionTimeout,
      heartbeatInterval,
      maxBytesPerPartition,
      minBytes,
      maxBytes,
      maxWaitTimeInMs,
    })
  }
}

Allow partitionAssigners to be configured


import 'pixi';
import 'p2';
import 'phaser';

import { STATES } from 'constants';

import Boot from 'states/boot';
import Play from 'states/play';
import End from 'states/end';
import Title from 'states/title';

const settings = {
  width: 960,
  height: 960 / 8 * 5, // Original CGA used 320 x 200, so aspect ratio is 8:5
  renderer: Phaser.AUTO,
  parent: '',
  state: null,
  transparent: false,
  antialias: false,
  physicsConfig: { arcade: true },
};

const args = Object.values(settings);
const game = new Phaser.Game(...args);

game.state.add(STATES.BOOT, Boot);
game.state.add(STATES.TITLE, Title);
game.state.add(STATES.PLAY, Play);
game.state.add(STATES.END, End);

game.state.start(STATES.BOOT);

import 'pixi';
import 'p2';
import 'phaser';

import { STATES } from 'constants';

import Boot from 'states/boot';
import Play from 'states/play';
import End from 'states/end';
import Title from 'states/title';

const config = {
  width: 960,
  height: 960 / 8 * 5, // Original CGA used 320 x 200, so aspect ratio is 8:5
  renderer: Phaser.AUTO,
  parent: '',
  state: null,
  transparent: false,
  antialias: false,
  physicsConfig: { arcade: true },
};

const game = new Phaser.Game(config);

game.state.add(STATES.BOOT, Boot);
game.state.add(STATES.TITLE, Title);
game.state.add(STATES.PLAY, Play);
game.state.add(STATES.END, End);

game.state.start(STATES.BOOT);

Remove need to spread config object


import Vue from 'vue'
import Router from 'vue-router'
import Index from '@/views/Index'
import Group from '@/views/Group'

Vue.use(Router)

export default new Router({
  routes: [
    {
      path: '/',
      name: 'index',
      component: Index,
    },
    {
      path: '/group/:groupId',
      name: 'group',
      component: Group,
      props: true,
    },
    {
      path: '/group/:groupId/:groupName',
      component: Group,
      alias: '/group/:groupId',
      props: true,
    },
  ],
})

import Vue from 'vue'
import Router from 'vue-router'
import Index from '@/views/Index'
import Group from '@/views/Group'
import NotFound from '@/views/NotFound'

Vue.use(Router)

export default new Router({
  routes: [
    {
      path: '/',
      name: 'index',
      component: Index,
    },
    {
      path: '/group/:groupId',
      name: 'group',
      component: Group,
      props: true,
    },
    {
      path: '/group/:groupId/:groupName',
      component: Group,
      alias: '/group/:groupId',
      props: true,
    },
    {
      path: '/*',
      name: 'not-found',
      component: NotFound,
    },
  ],
})

Add route to 404 page


// imports
var gulp = require('gulp');
var browserify  = require('browserify');
var source      = require('vinyl-source-stream');
var uglify = require('gulp-uglify');
var buffer      = require('vinyl-buffer');

// build src
gulp.task('browserify', function(cb){
 	return browserify('./src/app.js', {
            debug: true
        })
        .bundle()
        .pipe(source('bundle.js'))
        .pipe(gulp.dest('./www/'));

    cb();
});

// build in release mode
gulp.task('browserify:release', function(cb){

 	return browserify('./src/app.js')
        .bundle()
        .pipe(source('bundle.js'))
        .pipe(buffer())
        .pipe(uglify())
        .pipe(gulp.dest('./www/'));

    cb();
});

// copy fonts
gulp.task('fonts', function(cb){
 	return gulp.src('node_modules/ionic-framework/release/fonts/**')
        .pipe(gulp.dest('./www/fonts/'));
    cb();
});

// copy assets
gulp.task('assets', function(cb){
 	return gulp.src('./assets/**')
        .pipe(gulp.dest('./www/'));
    cb();
});

// copy templates
gulp.task('templates', function(cb){
    return gulp.src('./src/**/*.html')
        .pipe(gulp.dest('./www/'));
    cb();
});

// imports
var gulp = require('gulp');
var browserify  = require('browserify');
var source      = require('vinyl-source-stream');
var uglify = require('gulp-uglify');
var buffer      = require('vinyl-buffer');

// build src
gulp.task('browserify', function(cb){
 	return browserify('./src/app.js', {
            debug: true
        })
        .bundle()
        .pipe(source('bundle.js'))
        .pipe(gulp.dest('./www/'));

    cb();
});

// build in release mode
gulp.task('browserify:release', function(cb){

 	return browserify('./src/app.js')
        .bundle()
        .pipe(source('bundle.js'))
        .pipe(buffer())
        .pipe(uglify())
        .pipe(gulp.dest('./www/'));

    cb();
});

// copy fonts
gulp.task('fonts', function(cb){
 	return gulp.src('node_modules/ionic-npm/fonts/**')
        .pipe(gulp.dest('./www/fonts/'));
    cb();
});

// copy assets
gulp.task('assets', function(cb){
 	return gulp.src('./assets/**')
        .pipe(gulp.dest('./www/'));
    cb();
});

// copy templates
gulp.task('templates', function(cb){
    return gulp.src('./src/**/*.html')
        .pipe(gulp.dest('./www/'));
    cb();
});

Update path fonts gulp task