import Ember from 'ember';

export default Ember.Component.extend({

  openConfirmRemove: false,

  actions: {
    cancel () {
      this.cleanErrors();

      this.get('organization').rollbackAttributes();
    },

    save () {
      let organization  = this.get('organization');

      organization.save().then(() => {

        this.cleanErrors();

        this.set('open', false);
      }).catch((error) => {
        this.set('organizationErrors', organization.get('errors'));
        this.set('requestErrors', error.errors);
      });
    },
  },

  cleanErrors () {
    this.set('organizationErrors', null);
    this.set('requestErrors', null);
  }
});

import Ember from 'ember';

export default Ember.Component.extend({

  openConfirmRemove: false,

  actions: {
    cancel () {
      this.cleanErrors();

      this.get('organization').rollbackAttributes();
    },

    save () {
      let organization  = this.get('organization');

      organization.save().then(() => {

        this.cleanErrors();

        this.set('open', false);
      }).catch((error) => {
        /* TODO
         * Better handling of errors
         * Currently it is dealing with
         * AdapterError.errors // => ["You need to sign in...."]
         * but also there are:
         * ErrorClass.errors // => [{ detail: 'The adapter ... invalid', title: 'AdapterError'}]
         * but maybe these should be prevented on client
         */
        this.set('organizationErrors', organization.get('errors'));
        this.set('requestErrors', error.errors);
      });
    },
  },

  cleanErrors () {
    this.set('organizationErrors', null);
    this.set('requestErrors', null);
  }
});

Add note on error handling


// It seems that not much documentation exists for the JQuery
// mini-Web-app that is the Wordpress admin menu editor. However,
// insofar as Polylang's business in that mini-Web-app is similar
// to ours, studying the js/nav-menu.js therefrom is helpful.

function initNavMenus ($) {
  var $metabox = $('div.add-external-menu');
  $('input.submit-add-to-menu', $metabox).click(function() {
    console.log("click");

   // For inspiration regarding wpNavMenu, look at wp-admin/js/nav-menu.js
   wpNavMenu.addItemToMenu(
     {'-1': {
       'menu-item-type': 'external-menu',
       'menu-item-url': 'https://example.com/restapi/menu?lang=en_JP'
     }},
     wpNavMenu.addMenuItemToBottom,
     function() {
       console.log('Added external menu to menu');
     });

  });  // submit button's .click()
}

function initExternalMenuList ($) {
    $('a.page-title-action').remove();
    $('h1.wp-heading-inline').after('<button class="page-title-action">' + wp.translations.refresh_button + '</button>');
    var $button = $('h1.wp-heading-inline').next();
    var spinning = false;
    $button.click(function() {
        if (spinning) return;
        var $form = window.EPFLMenus.asWPAdminPostForm('refresh');
        $form.submit();
        var $spinner = $('');
        $button.append($spinner);
        spinning = true;
    });
}

jQuery( document ).ready(function($) {
  if (window.wp.screen.base === 'nav-menus') {
    initNavMenus($);
  }

  if (window.wp.screen.base === 'edit' && window.wp.screen.post_type === 'epfl-external-menu' ) {
    initExternalMenuList($);
  }

  // If you see this, nothing threw or crashed (yet).
  console.log('epfl-menus-admin.js is on duty.');
});

function initExternalMenuList ($) {
    $('a.page-title-action').remove();
    $('h1.wp-heading-inline').after('<button class="page-title-action">' + wp.translations.refresh_button + '</button>');
    var $button = $('h1.wp-heading-inline').next();
    var spinning = false;
    $button.click(function() {
        if (spinning) return;
        var $form = window.EPFLMenus.asWPAdminPostForm('refresh');
        $form.submit();
        var $spinner = $('');
        $button.append($spinner);
        spinning = true;
    });
}

jQuery( document ).ready(function($) {
  if (window.wp.screen.base === 'edit' && window.wp.screen.post_type === 'epfl-external-menu' ) {
    initExternalMenuList($);
  }

  // If you see this, nothing threw or crashed (yet).
  console.log('epfl-menus-admin.js is on duty.');
});

Remove dead code in the JS app


import resourceReducer from './resource-reducer';
import actionTypes from './action-types';
import requestStatuses from './utils/request-statuses';
import setResourceMeta from './utils/set-resource-meta';
import upsertResources from './utils/upsert-resources';
import getStatus from './utils/get-status';
import getResources from './utils/get-resources';

export {
  resourceReducer,
  actionTypes,
  requestStatuses,
  setResourceMeta,
  upsertResources,
  getStatus,
  getResources
};

import resourceReducer from './resource-reducer';
import actionTypes from './action-types';
import requestStatuses from './utils/request-statuses';
import setResourceMeta from './utils/set-resource-meta';
import upsertResources from './utils/upsert-resources';
import getStatus from './utils/get-status';
import getResources from './utils/get-resources';

/*
* This is a dummy function to check if the function name has been altered by minification.
* If the function has been minified and NODE_ENV !== 'production', warn the user.
*/
function isCrushed() {}

if (
  process.env.NODE_ENV !== 'production' &&
  typeof isCrushed.name === 'string' &&
  isCrushed.name !== 'isCrushed'
) {
  console.error(
    'You are currently using minified code outside of NODE_ENV === \'production\'. ' +
    'This means that you are running a slower development build of Resourceful Redux. ' +
    'You can use loose-envify (https://github.com/zertosh/loose-envify) for browserify ' +
    'or DefinePlugin for webpack (http://stackoverflow.com/questions/30030031) ' +
    'to ensure you have the correct code for your production build.'
  );
}

export {
  resourceReducer,
  actionTypes,
  requestStatuses,
  setResourceMeta,
  upsertResources,
  getStatus,
  getResources
};

Check for minified code that isn't NODE_ENV=production


module.exports = function(question, callback) {

	var apiai = require('apiai');
	var app = apiai("76ccb7c7acea4a6884834f6687475222", "8b3e68f16ac6430cb8c40d49c315aa7a");

		var request = app.textRequest(question);

		request.on('response', function(response) {
		    var kantSucks = require('./deontologyYo')(response);
		    if(kantSucks == 0)
		    	if(response.result.metadata.speech)
		    		callback(response.result.metadata.speech)
		    	else
		    		callback("Unfortunately, I'm not smart enough to answer that question yet.")
		    else if(kantSucks == 1)
		    	callback("I'll check with that after I pass through my utility ethic function. Heres what I would say otherwise: " + response.result.metadata.speech);
		    else
		    	callback(kantSucks)
		});

		request.on('error', function(error) {
		    callback(error);
		});

		request.end()
};

module.exports = function(question, callback) {

	var apiai = require('apiai');
	var app = apiai("76ccb7c7acea4a6884834f6687475222", "8b3e68f16ac6430cb8c40d49c315aa7a");

		var request = app.textRequest(question);

		request.on('response', function(response) {
		    var kantSucks = require('./deontologyYo')(response);
		    if(kantSucks == 0)
		    	if(response.result.metadata.speech)
		    		callback(response.result.metadata.speech)
		    	else
		    		callback("Unfortunately, I'm not smart enough to answer that question yet.")
		    else if(kantSucks == 1)
		    	if(response.result.metadata.speech)
		    	callback("I'll check with that after I pass through my utility ethic function. Heres what I would say otherwise: " + response.result.metadata.speech);
		    else
		    	callback(kantSucks)
		});

		request.on('error', function(error) {
		    callback(error);
		});

		request.end()
};

Fix issue of blank responses for web ethic parsing.


var _ = require('lodash')
var findMissingNumberBinarySearch = require('./findMissingNumberBinarySearch')

// Takes a list of schedules and finds the first open year.
// If they go [2012, 2013, 2015] findFirstAvailableYear will
// return 2014. If it goes [2013, 2014, 2015] findFirstAvailableYear
// will return 2016. Etc.

function findFirstAvailableYear(schedules) {
	var years =
		_(schedules)
			.sortBy('year')
			.pluck('year')
			.uniq()
			.value()

	var missingNo = findMissingNumberBinarySearch(years)
	if (missingNo != -1) {
		return missingNo
	}

	return _.max(years) + 1
}

module.exports = findFirstAvailableYear

var _ = require('lodash')
var findMissingNumberBinarySearch = require('./findMissingNumberBinarySearch')

// Takes a list of schedules and finds the first open year. If they go [2012,
// 2013, 2015] findFirstAvailableYear will return 2014. If it goes [2013,
// 2014, 2015] findFirstAvailableYear will return 2016. If schedules is empty,
// it will return the current year.

function findFirstAvailableYear(schedules) {
	if (_.isEmpty(schedules)) {
		return new Date().getFullYear()
	}

	var years =
		_.chain(schedules)
			.sortBy('year')
			.pluck('year')
			.uniq()
			.value()

	var missingNo = findMissingNumberBinarySearch(years)
	if (missingNo != -1) {
		return missingNo
	}

	return _.max(years) + 1
}

module.exports = findFirstAvailableYear

Update finFirstAvailableYear to return the current year if no schedules


import path from 'path';
import webpack from 'webpack';

const paths = {
  ASSETS: path.resolve(__dirname, './assets'),
  SRC: path.resolve(__dirname),
  COMPONENTS: path.resolve(__dirname, '../src'),
  DIST: path.resolve(__dirname, './build')
};

module.exports = {

  plugins: [
    new webpack.ContextReplacementPlugin(/moment[\/\\]locale$/, /de|fr|en/)
  ],

  module: {
    loaders: [
      {
        test: /\.jsx?$/,
        loader: 'babel',
        exclude: [ paths.ASSETS, /src\/.+\/examples/, paths.DIST ],
        include: [
          paths.COMPONENTS,
          paths.SRC,
          /rc-datepicker/,
          /react-input-children/,
          /react-autosize-textarea/,
          /react-cookie-banner/,
          /react-flexview/
        ]
      },
      {
        test: /\.png$|\.gif$/,
        loader: 'file?name=[path][name].[ext]'
      }
    ],
    preLoaders: [
      {
        test: /\.jsx?$/,
        loader: 'eslint',
        include: [ paths.SRC, paths.COMPONENTS ],
        exclude: paths.ASSETS
      }
    ]
  }

};

import path from 'path';
import webpack from 'webpack';
import StyleLintPlugin from 'stylelint-webpack-plugin';

const paths = {
  ASSETS: path.resolve(__dirname, './assets'),
  SRC: path.resolve(__dirname),
  COMPONENTS: path.resolve(__dirname, '../src'),
  DIST: path.resolve(__dirname, './build')
};

module.exports = {

  plugins: [
    new webpack.ContextReplacementPlugin(/moment[\/\\]locale$/, /de|fr|en/),
    new StyleLintPlugin({
      files: '**/*.s?(a|c)ss',
      failOnError: false,
      syntax: 'scss'
    })
  ],

  module: {
    loaders: [
      {
        test: /\.jsx?$/,
        loader: 'babel',
        exclude: [ paths.ASSETS, /src\/.+\/examples/, paths.DIST ],
        include: [
          paths.COMPONENTS,
          paths.SRC,
          /rc-datepicker/,
          /react-input-children/,
          /react-autosize-textarea/,
          /react-cookie-banner/,
          /react-flexview/
        ]
      },
      {
        test: /\.png$|\.gif$/,
        loader: 'file?name=[path][name].[ext]'
      }
    ],
    preLoaders: [
      {
        test: /\.jsx?$/,
        loader: 'eslint',
        include: [ paths.SRC, paths.COMPONENTS ],
        exclude: paths.ASSETS
      }
    ]
  }

};

Define StyleLintPlugin in webpack base config.


var _ = require('lodash');

var periods = require('./periods');
var PRESTATIONS = require('../prestations');

module.exports = function reverseMap(openFiscaFamille, date, injectedRessources) {
    var period = periods.map(date);
    var prestationsToDisplay = _.cloneDeep(PRESTATIONS);
    var injectedPrestations = _.intersection(_.keys(prestationsToDisplay), injectedRessources);

    // Don't show prestations that have been injected by the user, and not calculated by the simulator
    _.forEach(injectedPrestations, function(resource) {
        delete prestationsToDisplay[resource];
    });

    return _.mapValues(prestationsToDisplay, function(format, prestationName) {
        var type = format.type,
            computedPrestation = openFiscaFamille[prestationName],
            result = computedPrestation[period];

        var uncomputabilityReason = openFiscaFamille[prestationName + '_non_calculable'] && openFiscaFamille[prestationName + '_non_calculable'][period];

        if (uncomputabilityReason) {
            return uncomputabilityReason;
        }

        if (format.montantAnnuel) {
            result *= 12;
        }

        if (type == Number) {
            result = Number(result.toFixed(2));
        }

        return result;
    });
};

var _ = require('lodash');

var periods = require('./periods');
var PRESTATIONS = require('../prestations');

module.exports = function reverseMap(openFiscaFamille, date, injectedRessources) {
    var period = periods.map(date);
    var injectedPrestations = _.intersection(_.keys(PRESTATIONS), injectedRessources)

    return _.mapValues(PRESTATIONS, function(format, prestationName) {

    // For prestations that have been injected by the user and not calculated by the simulator, put null to allow specific treatment in the ui
        if (_.contains(injectedPrestations, prestationName)) {
            return null;
        }

        var type = format.type,
            computedPrestation = openFiscaFamille[prestationName],
            result = computedPrestation[period];

        var uncomputabilityReason = openFiscaFamille[prestationName + '_non_calculable'] && openFiscaFamille[prestationName + '_non_calculable'][period];

        if (uncomputabilityReason) {
            return uncomputabilityReason;
        }

        if (format.montantAnnuel) {
            result *= 12;
        }

        if (type == Number) {
            result = Number(result.toFixed(2));
        }

        return result;
    });
};

Return null for injected prestations


import { takeLatest, put, call } from 'redux-saga/effects';

import {
  REQUEST_FEED_NOTIFICATIONS_SUCCESS,
  REQUEST_FEED_NOTIFICATIONS,
  REQUEST_FEED_NOTIFICATIONS_FAILURE,
} from 'actions/feedActions';
import moreNotificationsCall from 'api/feedApiCall';

export function* getFeedNotificationsFlow(action) {
  const receivedNotificationTypes = {
    deck_created: 'DECK_CREATED',
    deck_updated: 'DECK_UPDATED',
  };

  try {
    const responseListOfNotifications = yield call(
      moreNotificationsCall,
      action.meta,
    );
    if (!responseListOfNotifications) {
      throw new Error('Received undefined list.');
    }
    const listOfNotifications = responseListOfNotifications.map(
      responseNotification => ({
        timestamp: responseNotification.attributes.createdAt,
        type: receivedNotificationTypes[
          responseNotification.attributes.eventType.toLowerCase()
        ],
        targetDeck: responseNotification.attributes.deckName,
        concernedUser: responseNotification.attributes.userName,
      }),
    );

    yield put({
      type: REQUEST_FEED_NOTIFICATIONS_SUCCESS,
      payload: {
        listOfNotifications,
      },
    });
  } catch (error) {
    yield put({
      type: REQUEST_FEED_NOTIFICATIONS_FAILURE,
      payload: {
        message: error.message,
      },
    });
  }
}

function* feedUpdateWatcher() {
  yield takeLatest(REQUEST_FEED_NOTIFICATIONS, getFeedNotificationsFlow);
}

export default feedUpdateWatcher;

import { takeLatest, put, call } from 'redux-saga/effects';

import {
  REQUEST_FEED_NOTIFICATIONS_SUCCESS,
  REQUEST_FEED_NOTIFICATIONS,
  REQUEST_FEED_NOTIFICATIONS_FAILURE,
} from 'actions/feedActions';
import moreNotificationsCall from 'api/feedApiCall';

export function* getFeedNotificationsFlow(action) {
  const receivedNotificationTypes = {
    deck_created: 'DECK_CREATED',
    deck_updated: 'DECK_UPDATED',
  };

  try {
    const responseListOfNotifications = yield call(
      moreNotificationsCall,
      action.meta,
    );
    if (!responseListOfNotifications) {
      throw new Error('Received undefined list.');
    }
    const listOfNotifications = responseListOfNotifications.map(
      responseNotification => ({
        id: responseNotification.id,
        timestamp: responseNotification.attributes.createdAt,
        type:
          receivedNotificationTypes[
            responseNotification.attributes.eventType.toLowerCase()
          ],
        targetDeck: responseNotification.attributes.deckName,
        concernedUser: responseNotification.attributes.userName,
      }),
    );

    yield put({
      type: REQUEST_FEED_NOTIFICATIONS_SUCCESS,
      payload: {
        listOfNotifications,
      },
    });
  } catch (error) {
    yield put({
      type: REQUEST_FEED_NOTIFICATIONS_FAILURE,
      payload: {
        message: error.message,
      },
    });
  }
}

function* feedUpdateWatcher() {
  yield takeLatest(REQUEST_FEED_NOTIFICATIONS, getFeedNotificationsFlow);
}

export default feedUpdateWatcher;

Store the notification id when fetching notifications


let should = require('should');
let expect = require('chai').expect;
let jsdom = require('jsdom');
let {JSDOM} = jsdom;
let Shell;
describe('Shell', () => {
    let {document} = (new JSDOM('<!doctype html>')).window;
    global.document = document;
    global.window = document.defaultView;

    before(() => {
        Shell = require('../lib/shell');
    });

    it('should initialize without error', () => {
        (() => expect(new Shell()).to.be.an.instanceof(Shell) ).should.not.throw();
    });

    describe('Element', () => {
        context('when not present', () => {
            it('should be empty', () => {
                expect(new Shell()).to.have.property('el', null);
            });
        });

        context('when present', () => {
            let div;
            let shell;

            before(() => {
                div = document.createElement('div');
                div.setAttribute('id', 'shell');
                document.body.appendChild(div);
                shell = new Shell('#shell');
            });

            it('should be the DOM object of the element', () => {
                expect(shell.el[0]).to.not.be.undefined;
                expect(shell.el[0]).to.equal(div);
            });
        });
    });
});

let should = require('should');
let expect = require('chai').expect;
let jsdom = require('jsdom');
let {JSDOM} = jsdom;
let Shell;
describe('Shell', () => {
    let {document} = (new JSDOM('<!doctype html>')).window;
    global.document = document;
    global.window = document.defaultView;

    before(() => {
        Shell = require('../lib/shell.min');
    });

    it('should initialize without error', () => {
        (() => expect(new Shell()).to.be.an.instanceof(Shell) ).should.not.throw();
    });

    describe('Element', () => {
        context('when not present', () => {
            it('should be empty', () => {
                expect(new Shell()).to.have.property('el', null);
            });
        });

        context('when present', () => {
            let div;
            let shell;

            before(() => {
                div = document.createElement('div');
                div.setAttribute('id', 'shell');
                document.body.appendChild(div);
                shell = new Shell('#shell');
            });

            it('should be the DOM object of the element', () => {
                expect(shell.el[0]).to.not.be.undefined;
                expect(shell.el[0]).to.equal(div);
            });
        });
    });
});

Change required library with minified one


export const initialState = {
    user: {
        isAuthenticated: false,
        email: '',
        userType: '',
        isAdmin: false
    },
    global: {
        workingOn: {}
    },
    rootPath: "/florence",
    teams: {
        active: {},
        all: [],
        allIDsAndNames: [],
        users: []
    },
    datasets: {
        all: [],
        jobs: []
    },
    collections: {
        all: [],
        active: null,
        toDelete: {}
    },
    notifications: [],
    preview: {
        selectedPage: null
    }
};

export const initialState = {
    user: {
        isAuthenticated: false,
        email: '',
        userType: '',
        isAdmin: false
    },
    global: {
        workingOn: null
    },
    rootPath: "/florence",
    teams: {
        active: {},
        all: [],
        allIDsAndNames: [],
        users: []
    },
    datasets: {
        all: [],
        jobs: []
    },
    collections: {
        all: [],
        active: null,
        toDelete: {}
    },
    notifications: [],
    preview: {
        selectedPage: null
    }
};

Update workingOn initial state so it matches empty state


import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';
import Box from '../box';
import cx from 'classnames';
import theme from './theme.css';

class TabGroup extends PureComponent {
  render() {
    const { children, className, inverted, size, ...others } = this.props;

    const classNames = cx(theme['tab-group'], className);

    return (
      <Box data-teamleader-ui="tab-group" className={classNames} {...others}>
        {React.Children.map(children, (child) => React.cloneElement(child, { size }))}
      </Box>
    );
  }
}

TabGroup.propTypes = {
  children: PropTypes.node.isRequired,
  className: PropTypes.string,
  size: PropTypes.oneOf(['small', 'medium']),
};

export default TabGroup;

import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';
import Box from '../box';
import cx from 'classnames';
import theme from './theme.css';

class TabGroup extends PureComponent {
  scrollContainerRef = createRef();

  render() {
    const { children, className, inverted, size, ...others } = this.props;

    const classNames = cx(theme['tab-group'], className);

    return (
      <Box
        backgroundColor="neutral"
        backgroundTint="lightest"
        data-teamleader-ui="tab-group"
        className={classNames}
        {...others}
        display="flex"
        paddingHorizontal={1}
      >
        <Box className={theme['scroll-container']} display="flex" overflowX="scroll" ref={this.scrollContainerRef}>
          {React.Children.map(children, (child) => React.cloneElement(child, { size, marginHorizontal: 1 }))}
        </Box>
      </Box>
    );
  }
}

TabGroup.propTypes = {
  children: PropTypes.node.isRequired,
  className: PropTypes.string,
  size: PropTypes.oneOf(['small', 'medium']),
};

export default TabGroup;

Adjust markup and add a scrollable container with ref


import React, { PropTypes } from 'react'
import Promotion from '../assets/Promotion'
import SearchControl from '../forms/SearchControl'
import StreamContainer from '../../containers/StreamContainer'
import { TabListButtons } from '../tabs/TabList'
import { MainView } from '../views/MainView'

export const Search = ({
  coverDPI,
  isLoggedIn,
  onChange,
  onClickTrackCredits,
  onSubmit,
  promotion,
  streamAction,
  streamKey,
  tabs,
  terms,
  type,
  }) =>
  <MainView className="Search">
    <Promotion
      coverDPI={coverDPI}
      isLoggedIn={isLoggedIn}
      onClickTrackCredits={onClickTrackCredits}
      promotion={promotion}
    />
    <form className="SearchBar" onSubmit={onSubmit}>
      <SearchControl onChange={onChange} text={terms} />
      <TabListButtons
        activeType={type}
        className="SearchTabList"
        onTabClick={onChange}
        tabClasses="LabelTab SearchLabelTab"
        tabs={tabs}
      />
    </form>
    <StreamContainer key={streamKey} action={streamAction} />
  </MainView>

Search.propTypes = {
  coverDPI: PropTypes.string.isRequired,
  isLoggedIn: PropTypes.bool.isRequired,
  onChange: PropTypes.func.isRequired,
  onClickTrackCredits: PropTypes.func.isRequired,
  onSubmit: PropTypes.func.isRequired,
  promotion: PropTypes.object,
  streamAction: PropTypes.object,
  streamKey: PropTypes.string.isRequired,
  tabs: PropTypes.array.isRequired,
  terms: PropTypes.string.isRequired,
  type: PropTypes.string.isRequired,
}

export default Search

import React, { PropTypes } from 'react'
import Promotion from '../assets/Promotion'
import SearchControl from '../forms/SearchControl'
import StreamContainer from '../../containers/StreamContainer'
import { TabListButtons } from '../tabs/TabList'
import { MainView } from '../views/MainView'

export const Search = ({
  coverDPI,
  isLoggedIn,
  onChange,
  onClickTrackCredits,
  onSubmit,
  promotion,
  streamAction,
  streamKey,
  tabs,
  terms,
  type,
  }) =>
  <MainView className="Search">
    <Promotion
      coverDPI={coverDPI}
      isLoggedIn={isLoggedIn}
      onClickTrackCredits={onClickTrackCredits}
      promotion={promotion}
    />
    <form className="SearchBar" onSubmit={onSubmit}>
      <SearchControl key={terms} onChange={onChange} text={terms} />
      <TabListButtons
        activeType={type}
        className="SearchTabList"
        onTabClick={onChange}
        tabClasses="LabelTab SearchLabelTab"
        tabs={tabs}
      />
    </form>
    <StreamContainer key={streamKey} action={streamAction} />
  </MainView>

Search.propTypes = {
  coverDPI: PropTypes.string.isRequired,
  isLoggedIn: PropTypes.bool.isRequired,
  onChange: PropTypes.func.isRequired,
  onClickTrackCredits: PropTypes.func.isRequired,
  onSubmit: PropTypes.func.isRequired,
  promotion: PropTypes.object,
  streamAction: PropTypes.object,
  streamKey: PropTypes.string.isRequired,
  tabs: PropTypes.array.isRequired,
  terms: PropTypes.string.isRequired,
  type: PropTypes.string.isRequired,
}

export default Search

Update the search container when terms change.


import {PropTypes} from 'react'
import {getUnfulfilledRoleComponent} from './components/UnfulfilledRole'

function Sanity({roles}) {
  function getRole(roleName) {
    const fulfiller = roles[roleName]
    if (!fulfiller) {
      return null
    }

    return fulfiller.module.__esModule && fulfiller.module.default
      ? fulfiller.module.default
      : fulfiller.module
  }

  function getComponents(wanted) {
    return Object.keys(wanted).reduce((target, key) => {
      const role = typeof wanted[key] === 'string'
        ? {name: wanted[key]}
        : wanted[key]

      target[key] = getRole(role.name) || getUnfulfilledRoleComponent(role)

      return target
    }, {})
  }

  function getPluginForRole(roleName) {
    return roles[roleName] && roles[roleName].plugin
  }

  return {
    getRole,
    getComponents,
    getPluginForRole
  }
}

export const sanityShape = {
  sanity: PropTypes.shape({
    getRole: PropTypes.func.isRequired,
    getComponents: PropTypes.func.isRequired,
    getPluginForRole: PropTypes.func.isRequired
  })
}

export default Sanity

import {PropTypes} from 'react'
import {getUnfulfilledRoleComponent} from './components/UnfulfilledRole'

function Sanity({roles}) {
  function getRole(roleName) {
    const fulfiller = roles[roleName]
    if (!fulfiller) {
      return null
    }

    return Array.isArray(fulfiller)
      ? fulfiller.map(getModule)
      : getModule(fulfiller)
  }

  function getComponents(wanted) {
    return Object.keys(wanted).reduce((target, key) => {
      const role = typeof wanted[key] === 'string'
        ? {name: wanted[key]}
        : wanted[key]

      target[key] = getRole(role.name) || getUnfulfilledRoleComponent(role)

      return target
    }, {})
  }

  function getPluginForRole(roleName) {
    return roles[roleName] && roles[roleName].plugin
  }

  return {
    getRole,
    getComponents,
    getPluginForRole
  }
}

function getModule(fulfiller) {
  return fulfiller.module.__esModule && fulfiller.module.default
    ? fulfiller.module.default
    : fulfiller.module
}

export const sanityShape = {
  sanity: PropTypes.shape({
    getRole: PropTypes.func.isRequired,
    getComponents: PropTypes.func.isRequired,
    getPluginForRole: PropTypes.func.isRequired
  })
}

export default Sanity

Resolve ES6-modules in multi-fulfiller roles


require('babel-polyfill');

import test from 'tape';
import Server from './server';
import requestPromise from 'request-promise';

test('Basic server operation', async t => {
  // arrange
  const server = new Server();
  server.boot();
  // act
  const response = await requestPromise({
    uri: 'http://127.0.0.1:3002/health',
    resolveWithFullResponse: true,
    json: true
  });
  t.equal(response.statusCode, 200, '/health should return 200');
  t.deepEqual(
    response.body,
    { status: 'good' },
    '/health should return {status: good} in json format'
  );
  // teardown
  server.shutdown();
  t.end();
});

require('babel-polyfill');

import test from 'tape';
import Server from './server';
import requestPromise from 'request-promise';

test('Basic server operation', async t => {
  // arrange
  const server = new Server();
  server.boot();

  // act
  const healthResponse = await requestPromise({
    uri: 'http://127.0.0.1:3002/health',
    resolveWithFullResponse: true,
    json: true
  });
  t.equal(
    healthResponse.statusCode,
    200,
    '/health should return 200'
  );
  t.deepEqual(
    healthResponse.body,
    { status: 'good' },
    '/health should return {status: good} in json format'
  );

  // act
  const faviconResponse = await requestPromise({
    uri: 'http://127.0.0.1:3002/favicon.ico',
    resolveWithFullResponse: true
  });
  t.equal(
    faviconResponse.statusCode,
    200,
    '/favicon.ico should return 200'
  );
  t.deepEqual(
    faviconResponse.body,
    '',
    '/favicon.ico should return nothing but empty string'
  );

  // teardown
  server.shutdown();
  t.end();
});

Add test case for favicon.ico requests


import $ from 'jquery';
import * as Backbone from 'backbone';
import Events from '../helpers/backbone-events';

class SearchBoxView extends Backbone.View {

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

  get events() {
    return {
      'submit form': 'sendQuery'
    }
  }

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

  initialize() {
    this.listenTo(Events, 'search:noQuery', this.handleEmptyQuery);
  }

  sendQuery() {
    if( this.query !== '' ) {
      dispatcher.trigger('router:go', ('search/?q=' + this.query));
    }
  handleEmptyQuery() {
    window.console.log('query was empty');
  }

}

export default SearchBoxView;

import $ from 'jquery';
import * as Backbone from 'backbone';
import Events from '../helpers/backbone-events';

class SearchBoxView extends Backbone.View {

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

  get events() {
    return {
      'submit form': 'sendQuery'
    }
  }

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

  initialize() {
    this.listenTo(Events, 'search:noQuery', this.handleEmptyQuery);
  }

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

}

export default SearchBoxView;

Remove unneeded event for search box


import _ from 'underscore';
import moment from 'moment';

var TimeSegments = {

  // Segment an array of events by scale
  segment(events, scale, options) {
    scale = scale || 'weeks';
    options = options || {};
    _.defaults(options, {
      startAttribute: 'start',
      endAttribute: 'end'
    });
    events = _.chain(events).clone().sortBy(options.startAttribute).value();
    var segments = {};

    // Clone our events so that we're not modifying the original
    // objects. Loop through them, inserting the events into the
    // corresponding segments
    _.each(_.clone(events), e => {

      // Calculate the duration of the event; this determines
      // how many segments it will be in
      var startMoment = moment.utc(e[options.startAttribute]).startOf(scale);
      var endMoment = moment.utc(e[options.endAttribute]).endOf(scale).add(1, 'milliseconds');

      // For each duration, add the event to the corresponding segment
      var segmentStart;
      for(var i = 0, duration = endMoment.diff(startMoment, scale); i < duration; i++) {
        segmentStart = moment.utc(startMoment).add(i, scale).unix();
        if (!segments[segmentStart]) { segments[segmentStart] = []; }
        segments[segmentStart].push(_.clone(e));
      }
    });
    return segments;
  }
};

export default TimeSegments;

Add ES6 import to source file.


module.exports = [
  {
    name: 'Apples',
    notes: 'Fresh fresh apples',
    need_to_restock: false,
    user_id: null
  },
  {
    name: 'Pears',
    notes: 'I like pears',
    need_to_restock: false,
    user_id: null
  },
  {
    name: 'Milk',
    notes: 'MOO dairy',
    need_to_restock: false,
    user_id: null
  },
  {
    name: 'Bread',
    notes: '<3 carbs',
    need_to_restock: false,
    user_id: null
  }
];

module.exports = [
  {
    id: 1,
    itemname: 'Apples',
    notes: 'Fresh fresh apples',
    need_to_restock: false,
    user_id: null
  },
  {
    id: 2,
    itemname: 'Pears',
    notes: 'I like pears',
    need_to_restock: false,
    user_id: null
  },
  {
    id: 3,
    itemname: 'Milk',
    notes: 'MOO dairy',
    need_to_restock: false,
    user_id: null
  },
  {
    id: 4,
    itemname: 'Bread',
    notes: '<3 carbs',
    need_to_restock: false,
    user_id: null
  }
];

Change fields to align with schema design


var fs    = require('fs'),
    plist = require('plist'),
    CSON  = require('cson');

// Read grammar from plist
var psGrammarPlist = fs.readFileSync('vendor/EditorSyntax/PowerShellSyntax.tmLanguage', 'utf8')
var grammar = plist.parse(psGrammarPlist);

// Write out grammar as CSON
var csonGrammar = CSON.stringifySync(filterObject(grammar))
fs.writeFileSync('grammars/powershell.cson', csonGrammar, 'utf8')

// Helper function
// References: https://github.com/atom/apm/blob/c0d657af13a0da4acda6fd4be39eddded7aac1e3/src/package-converter.coffee#L73-75
function filterObject(obj) {
   delete obj.uuid
   delete obj.keyEquivalent
   return obj
}

var fs    = require('fs'),
    plist = require('plist'),
    CSON  = require('cson');

// Read grammar from plist
var psGrammarPlist = fs.readFileSync('vendor/EditorSyntax/PowerShellSyntax.tmLanguage', 'utf8')
var grammar = plist.parse(psGrammarPlist);

// Write out grammar as CSON
var csonGrammar = CSON.stringify(filterObject(grammar));
fs.writeFileSync('grammars/powershell.cson', csonGrammar, 'utf8');

// Helper function
// References: https://github.com/atom/apm/blob/c0d657af13a0da4acda6fd4be39eddded7aac1e3/src/package-converter.coffee#L73-75
function filterObject(obj) {
   delete obj.uuid
   delete obj.keyEquivalent
   return obj
}

Update script to utilize new method names


var pagination = require('hexo-pagination');

hexo.extend.generator.register('category', function(locals){
  var config = this.config;
  var perPage = config.category_generator.per_page;
  var paginationDir = config.pagination_dir || 'page';

  return locals.categories.reduce(function(result, category){
    if (!category.length) return result;

    var posts = category.posts.sort('-date');
    var data = pagination(category.path, posts, {
      perPage: perPage,
      layout: ['category', 'archive', 'index'],
      format: paginationDir + '/%d/',
      data: {
        category: category.name
      }
    });

    return result.concat(data);
  }, []);
});

var pagination = require('hexo-pagination');

hexo.extend.generator.register('category', function(locals){
  var config = this.config;
  if (config.category_generator) {
    var perPage = config.category_generator.per_page;
  } else {
    var perPage = 10;
  }
  var paginationDir = config.pagination_dir || 'page';

  return locals.categories.reduce(function(result, category){
    if (!category.length) return result;

    var posts = category.posts.sort('-date');
    var data = pagination(category.path, posts, {
      perPage: perPage,
      layout: ['category', 'archive', 'index'],
      format: paginationDir + '/%d/',
      data: {
        category: category.name
      }
    });

    return result.concat(data);
  }, []);
});

Fix TypeError in category generator


import '@material/mwc-dialog';
import '@material/mwc-button';
import '@material/mwc-textfield';

import '../shared/demo-header';

addEventListener('load', function() {
 document.body.classList.remove('unresolved');
});

const buttons = document.body.querySelectorAll('mwc-button[data-num]');

for (let i = 0; i < buttons.length; i++) {
  const button = buttons[i];
  const buttonNum = button.dataset.num;

  const listenerFactory = (numButton) => {
    return function() {
      const dialog = document.body.querySelector('#dialog' + numButton);
      dialog.open = true;
    };
  };

  const listener = listenerFactory(buttonNum);

  button.addEventListener('click', listener);
}

window.toggleActions.onclick = function() {
  const dialog = document.body.querySelector('#dialog4');
  const hideActionSpan = document.body.querySelector('#hideActionVal');

  const hideAction = !dialog.hideActions;
  dialog.hideActions = hideAction;
  hideActionSpan.innerText = hideAction;
};

const dialog = document.querySelector('#dialog8');
const textField = document.querySelector('#dialog8-text-field');
const primaryButton = document.querySelector('#dialog8-primary-action-button');

primaryButton.addEventListener('click', (e) => {
  // validate, possible asynchronous such as a server response
  const isValid = textField.checkValidity();
  if (isValid) {
    dialog.close();
    return;
  }

  textField.reportValidity();
});

import '@material/mwc-dialog';
import '@material/mwc-button';
import '@material/mwc-textfield';

import '../shared/demo-header';

addEventListener('load', function() {
 document.body.classList.remove('unresolved');
});

const buttons = document.body.querySelectorAll('mwc-button[data-num]');

for (let i = 0; i < buttons.length; i++) {
  const button = buttons[i];
  const buttonNum = button.dataset.num;

  const listenerFactory = (numButton) => {
    return function() {
      const dialog = document.body.querySelector('#dialog' + numButton);
      dialog.open = true;
    };
  };

  const listener = listenerFactory(buttonNum);

  button.addEventListener('click', listener);
}

window.toggleActions.onclick = function() {
  const dialog = document.body.querySelector('#dialog4');
  const hideActionSpan = document.body.querySelector('#hideActionVal');

  const hideAction = !dialog.hideActions;
  dialog.hideActions = hideAction;
  hideActionSpan.innerText = hideAction;
};

const dialog = document.querySelector('#dialog8');
const textField = document.querySelector('#dialog8-text-field');
const primaryButton = document.querySelector('#dialog8-primary-action-button');

primaryButton.addEventListener('click', () => {
  // validate, possible asynchronous such as a server response
  const isValid = textField.checkValidity();
  if (isValid) {
    dialog.close();
    return;
  }

  textField.reportValidity();
});

Remove uneccessary event reference from anonymous callback


import { createAsyncAction } from '../../lib/reduxHelpers';
import * as api from '../../lib/serverApi/topics';

export const FETCH_TOPIC_SPLIT_STORY_COUNT = 'FETCH_TOPIC_SENTENCE_COUNT';
// pass in topicId, snapshotId, timespanId
export const fetchTopicSplitStoryCounts = createAsyncAction(FETCH_TOPIC_SPLIT_STORY_COUNT, api.topicSplitStoryCounts);

export const FETCH_TOPIC_FOCAL_SET_SPLIT_STORY_COUNTS = 'FETCH_TOPIC_FOCAL_SET_SENTENCE_COUNTS';
// pass in topicId, focalSetId, filters
export const fetchTopicFocalSetSplitStoryCounts = createAsyncAction(FETCH_TOPIC_FOCAL_SET_SPLIT_STORY_COUNTS, api.topicFocalSetSplitStoryCounts);

import { createAsyncAction } from '../../lib/reduxHelpers';
import * as api from '../../lib/serverApi/topics';

export const FETCH_TOPIC_SPLIT_STORY_COUNT = 'FETCH_TOPIC_SPLIT_STORY_COUNT';
// pass in topicId, snapshotId, timespanId
export const fetchTopicSplitStoryCounts = createAsyncAction(FETCH_TOPIC_SPLIT_STORY_COUNT, api.topicSplitStoryCounts);

export const FETCH_TOPIC_FOCAL_SET_SPLIT_STORY_COUNTS = 'FETCH_TOPIC_FOCAL_SET_SPLIT_STORY_COUNTS';
// pass in topicId, focalSetId, filters
export const fetchTopicFocalSetSplitStoryCounts = createAsyncAction(FETCH_TOPIC_FOCAL_SET_SPLIT_STORY_COUNTS, api.topicFocalSetSplitStoryCounts);

Change constants from sentence to story


function coordinatesToString(x, y) {
  return x + ',' + y;
}

module.exports = function(sequelize, DataTypes) {
  var Game = sequelize.define('Game', {
    player1: {type: DataTypes.STRING},
    player2: {type: DataTypes.STRING},
    next: {type: DataTypes.STRING},
    lastMove: {type: DataTypes.STRING, allowNull: false, defaultValue: '-1,-1'},
    finished: {type: DataTypes.BOOLEAN, allowNull: false, defaultValue: false}
  }, {

    classMethods: {
      associate: function (models) {
        Game.hasMany(models.Board);
      }
    },

    instanceMethods: {
      fire: function (player, x, y) {
        if (player === this.player1) {
          this.next = this.player2;
          this.lastMove = coordinatesToString(x, y);
          this.save();

          return this.Boards[1].fire(x, y);
        } else if (player === this.player2) {
          this.next = this.player1;
          this.lastMove = coordinatesToString(x, y);
          this.save();

          return this.Boards[0].fire(x, y);
        }
      },
    },

    getterMethods: {
      gameOver: function() {
        return this.Boards[0].gameOver || this.Boards[1].gameOver;
      }
    }
  });

  return Game;
};

function coordinatesToString(x, y) {
  return x + ',' + y;
}

module.exports = function(sequelize, DataTypes) {
  var Game = sequelize.define('Game', {
    player1: {type: DataTypes.STRING},
    player2: {type: DataTypes.STRING},
    next: {type: DataTypes.STRING},
    lastMove: {type: DataTypes.STRING, allowNull: false, defaultValue: '-1,-1'},
    finished: {type: DataTypes.BOOLEAN, allowNull: false, defaultValue: false}
  }, {

    classMethods: {
      associate: function (models) {
        Game.hasMany(models.Board);
      }
    },

    instanceMethods: {
      fire: function (player, x, y) {
        function byId(game1, game2) {
          return game1.id > game2.id;
        }

        if (player === this.player1) {
          this.next = this.player2;
          this.lastMove = coordinatesToString(x, y);
          this.save();

          return this.Boards.sort(byId)[1].fire(x, y);
        } else if (player === this.player2) {
          this.next = this.player1;
          this.lastMove = coordinatesToString(x, y);
          this.save();

          return this.Boards.sort(byId)[0].fire(x, y);
        }
      },
    },

    getterMethods: {
      gameOver: function() {
        return this.Boards[0].gameOver || this.Boards[1].gameOver;
      }
    }
  });

  return Game;
};

Fix bug which caused shots to be fired at the wrong board


define([
    'jquery',
    'knockout',
    'knockout.validation',
    'app',
    'models/user',
    'models/user_session'
], function (
    $,
    ko,
    validation,
    app,
    User,
    UserSession
) {
    var registerPage = ko.validatedObservable({
        email: ko.observable(),
        register: function () {
            $.ajax({
                type: 'POST',
                url: '/api/users',
                data: JSON.stringify({
                    email: this.email()
                }),
                contentType: 'application/json',
                processData: false
            }).done(function (data) {
                var user = User.create(data);
                $.ajax({
                    type: 'POST',
                    url: user.user_sessions.uri(),
                    data: JSON.stringify({}),
                    contentType: 'application/json',
                    processData: false
                }).done(function (data) {
                    app.user(user);
                    app.session(UserSession.create(data));
                    app.router.redirect((app.pager.last_page().originalPath && app.pager.last_page().originalPath) || '/');
                });
            });
        }
    });

    return registerPage();
});

define([
    'jquery',
    'knockout',
    'knockout.validation',
    'app',
    'models/user',
    'models/user_session'
], function (
    $,
    ko,
    validation,
    app,
    User,
    UserSession
) {
    var registerPage = ko.validatedObservable({
        email: ko.observable(),
        register: function () {
            $.ajax({
                type: 'POST',
                url: '/api/users',
                data: JSON.stringify({
                    email: this.email()
                }),
                contentType: 'application/json',
                processData: false
            }).done(function (data) {
                if (data.error) {
                    alert(JSON.stringify(data.errors));
                } else {
                    var user = User.create(data);
                    $.ajax({
                        type: 'POST',
                        url: user.user_sessions().uri(),
                        data: JSON.stringify({}),
                        contentType: 'application/json',
                        processData: false
                    }).done(function (data) {
                        app.user(user);
                        app.session(UserSession.create(data));
                        app.router.redirect((app.pager.last_page() && app.pager.last_page().originalPath) || '/');
                    });
                }
            });
        }
    });

    return registerPage();
});

Fix registering (and add really bad error feedback)


// ==UserScript==
// @author      Robert Slifka (GitHub @rslifka)
// @connect     docs.google.com
// @connect     rslifka.github.io
// @copyright   2017-2018, Robert Slifka (https://github.com/rslifka/fate_of_all_fools)
// @description (DEVELOPMENT) Enhancements to the Destiny Item Manager
// @grant       GM_addStyle
// @grant       GM_getResourceText
// @grant       GM_getValue
// @grant       GM_log
// @grant       GM_setValue
// @grant       GM_xmlhttpRequest
// @homepage    https://github.com/rslifka/fate_of_all_fools
// @license     MIT; https://raw.githubusercontent.com/rslifka/fate_of_all_fools/master/LICENSE.txt
// @match       https://*.destinyitemmanager.com/*
// @name        (DEVELOPMENT) Fate of All Fools
// @require     file:///Users/rslifka/Dropbox/workspace/fate_of_all_fools/public/fateOfAllFools.js
// @resource    fateOfAllFoolsCSS file:///Users/rslifka/Dropbox/workspace/fate_of_all_fools/public/fateOfAllFools.css
// @run-at      document-idle
// @supportURL  https://github.com/rslifka/fate_of_all_fools/issues
// ==/UserScript==

// ==UserScript==
// @connect     docs.google.com
// @connect     rslifka.github.io
// @grant       GM_addStyle
// @grant       GM_getResourceText
// @grant       GM_getValue
// @grant       GM_log
// @grant       GM_setValue
// @grant       GM_xmlhttpRequest
// @match       https://*.destinyitemmanager.com/*
// @name        (DEVELOPMENT) Fate of All Fools
// @require     file:///Users/rslifka/Dropbox/workspace/fate_of_all_fools/public/fateOfAllFools.js
// @resource    fateOfAllFoolsCSS file:///Users/rslifka/Dropbox/workspace/fate_of_all_fools/public/fateOfAllFools.css
// @run-at      document-idle
// ==/UserScript==

Include only what's necessary to test locally


// @flow

/**
 * This module exists to promisify all the fs functionality
 */

import fs from 'fs';
// Polyfill for Node 8's util.promisify
import promisify from 'util.promisify';

import type { Stats } from 'fs';

type ReadFileFn = (filename: string, enc?: string) => Promise<string>;
type WriteFileFn = (
  filename: string,
  data: Buffer | string,
  options?: Object | string,
) => Promise<void>;
type UnlinkFn = (filename: string) => Promise<void>;

export const readFile: ReadFileFn = promisify(fs.readFile);
export const writeFile: WriteFileFn = promisify(fs.writeFile);
export const unlink: UnlinkFn = promisify(fs.unlink);

// TODO: @ryyppy Can we use something simpler like promisify(fs.access)?
export async function fileExists(file: string): Promise<boolean> {
  try {
    const stats: Stats = await promisify(fs.lstat)(file);

    if (stats.isFile()) {
      return true;
    }

    return false;
  } catch (err) {
    return false;
  }
}

export const statSync = fs.statSync;

// @flow

/**
 * This module exists to promisify all the fs functionality
 */

import fs from 'fs';
// Polyfill for Node 8's util.promisify
import promisify from 'util.promisify';

import type { Stats } from 'fs';

type ReadFileFn = (filename: string, enc?: string) => Promise<Buffer | string>;
type WriteFileFn = (
  filename: string,
  data: Buffer | string,
  options?: Object | string,
) => Promise<void>;
type UnlinkFn = (filename: string) => Promise<void>;

export const readFile: ReadFileFn = promisify(fs.readFile);
export const writeFile: WriteFileFn = promisify(fs.writeFile);
export const unlink: UnlinkFn = promisify(fs.unlink);

// TODO: @ryyppy Can we use something simpler like promisify(fs.access)?
export async function fileExists(file: string): Promise<boolean> {
  try {
    const stats: Stats = await promisify(fs.lstat)(file);

    if (stats.isFile()) {
      return true;
    }

    return false;
  } catch (err) {
    return false;
  }
}

export const statSync = fs.statSync;

Fix typing of readFile function


var expect        = require('expect.js');
var _             = require('lodash');
var sampleData    = require('./lib/sampleData');
var fruits        = sampleData.fruits;
var fns           = sampleData.fns;
var helpers       = require('./lib/helpers');
var setupModel    = helpers.getModelSetup(fns, '*', ['name', 'color']);

describe('Query', function () {
  describe('Basic functionality', function () {
    it('Returns list from non-empty collection querying for everything', function () {
      var model = setupModel({fruits: fruits});
      var view = model.at('fruits').view('yellowFruits');
      var query = view.query('a', 'z');
      query.ref('_page.filteredFruits');
      var expectedFruits = model.expectedResult({fruits: ['bananaId', 'lemonId']}, {array: true});
      expect(model.get('filteredFruits')).to.eql(expectedFruits);
    });
  });
});

var expect        = require('expect.js');
var _             = require('lodash');
var sampleData    = require('./lib/sampleData');
var fruits        = sampleData.fruits;
var fns           = sampleData.fns;
var helpers       = require('./lib/helpers');
var setupModel    = helpers.getModelSetup(fns, '*', ['name', 'color']);

describe('Query', function () {
  describe('Basic functionality', function () {
    it('Returns list from non-empty collection querying for everything', function () {
      var model = setupModel({fruits: fruits});
      var view = model.at('fruits').view('yellowFruits');
      var query = view.query('a', 'z');
      query.ref('_page.filteredFruits');
      var expectedFruits = model.expectedResult({fruits: ['bananaId', 'lemonId']}, {array: true});
      expect(model.get('filteredFruits')).to.eql(expectedFruits);
    });
  });

  describe('Updating item in collection', function () {
    it('Correct order after changing relevant property', function () {
      var model = setupModel({fruits: fruits});
      var view = model.at('fruits').view('yellowFruits');
      var query = view.query('a', 'z');
      query.ref('_page.filteredFruits');
      model.set('fruits.bananaId.name', 'Weird banana');
      var expectedFruits = model.expectedResult({fruits: ['lemonId', 'bananaId']}, {array: true});
      expect(model.get('filteredFruits')).to.eql(expectedFruits);
    });
  });
});

Add (currently failing) test for checking reactiveness of queries


(function () {
  'use strict';

  function fetchPonyfill(options) {
    var Promise = options && options.Promise || self.Promise;
    var XMLHttpRequest = options && options.XMLHttpRequest || self.XMLHttpRequest;
    var global = self;

    return (function () {
      var self = Object.create(global, {fetch: {value: undefined}});

// {{whatwgFetch}}

      return {
        fetch: self.fetch.bind(global),
        Headers: self.Headers,
        Request: self.Request,
        Response: self.Response
      };
    }());
  }

  if (typeof define === 'function' && define.amd) {
    define(function () {
      return fetchPonyfill;
    });
  } else if (typeof exports === 'object') {
    module.exports = fetchPonyfill;
  } else {
    self.fetchPonyfill = fetchPonyfill;
  }
}());

(function () {
  'use strict';

  function fetchPonyfill(options) {
    var Promise = options && options.Promise || self.Promise;
    var XMLHttpRequest = options && options.XMLHttpRequest || self.XMLHttpRequest;
    var global = self;

    return (function () {
      var self = Object.create(global, {
        fetch: {
          value: undefined,
          writable: true
        }
      });

// {{whatwgFetch}}

      return {
        fetch: self.fetch.bind(global),
        Headers: self.Headers,
        Request: self.Request,
        Response: self.Response
      };
    }());
  }

  if (typeof define === 'function' && define.amd) {
    define(function () {
      return fetchPonyfill;
    });
  } else if (typeof exports === 'object') {
    module.exports = fetchPonyfill;
  } else {
    self.fetchPonyfill = fetchPonyfill;
  }
}());

Fix fetch in new object not being writable


$(document).ready(function () {

  var center = new google.maps.LatLng(35.681298, 139.766247);

  var mapOptions = {
    zoom: 12,
    center: center
  };

  var map = new google.maps.Map(document.getElementById('map'), mapOptions);

  var marker = new google.maps.Marker({
    position: map.getCenter(),
    draggable: true,
    map: map
  });

  var geocoder = new google.maps.Geocoder();

  var circle = new google.maps.Circle({
    strokeColor: '#FF0000',
    strokeOpacity: 0.8,
    strokeWeight: 2,
    fillColor: '#FF0000',
    fillOpacity: 0.35,
    map: map,
    center: center,
    radius: 3000
  });

  $("#search").click(function(){

    var address = $("#address").val();

    geocoder.geocode({ 'address': address }, function (results, status) {
      if (status === google.maps.GeocoderStatus.OK) {

        var loc = results[0].geometry.location;
        map.setCenter(loc);
        marker.setPosition(loc);
        circle.setCenter(loc);

        $("#latitude").val(loc.lat());
        $("#longitude").val(loc.lng());
      }
    });
  });

});

$(document).ready(function () {

  var center = new google.maps.LatLng(35.681298, 139.766247);

  var mapOptions = {
    zoom: 12,
    center: center
  };

  var map = new google.maps.Map(document.getElementById('map'), mapOptions);

  var marker = new google.maps.Marker({
    position: map.getCenter(),
    draggable: true,
    map: map
  });
  marker.addListener("drag", function() {
    circle.setCenter(marker.getPosition());
  });

  var geocoder = new google.maps.Geocoder();

  var circle = new google.maps.Circle({
    strokeColor: '#FF0000',
    strokeOpacity: 0.8,
    strokeWeight: 2,
    fillColor: '#FF0000',
    fillOpacity: 0.35,
    map: map,
    center: center,
    radius: 3000
  });

  $("#search").click(function(){

    var address = $("#address").val();

    geocoder.geocode({ 'address': address }, function (results, status) {
      if (status === google.maps.GeocoderStatus.OK) {

        var loc = results[0].geometry.location;
        map.setCenter(loc);
        marker.setPosition(loc);
        circle.setCenter(loc);

        $("#latitude").val(loc.lat());
        $("#longitude").val(loc.lng());
      }
    });
  });

});

Set circle to follow the marker


var helpers = require('./helpers');
var db = helpers.db;

/* /users */
exports.getAllUsers = function(req, res) {
  helpers.getRequestingUser(req, function(err, user) {

    if ( err ) {
      res.status(500).end();
      console.log("getAllUsers:", err);
    } else {

      if ( !user ) {
        res.status(403).end();
      } else {

        // TODO filter for type
        helpers.getAllUsers(function(err, users) {
          if ( err ) {
            res.status(500).end();
          } else {

            res.end(JSON.stringify(users));

          }
        });

      }

    }

  });
};

/* /user/:id/cards */
exports.getCardsOfUser = function(req, res) {
  helpers.getRequestingUser(req, function(err, user) {

    if ( err ) {
      res.status(500).end();
    } else {

      if ( !user ) {
        res.status(403).end();
      } else {

        // TODO filter for type
        var id = req.params.id;
        helpers.getUser({
          id: id
        }, function(err, user) {

          if ( user ) {
            user.getCards().success(function(cards) {

              if ( cards ) {
                res.end(JSON.stringify(cards));
              } else {
                res.status(500).end();
              }

            });
          }

        });

      }

    }

  });
};

var helpers = require('./helpers');
var db = helpers.db;

/* /users */
exports.getAllUsers = function(req, res) {
  helpers.getRequestingUser(req, function(err, user) {

    if ( err ) {
      res.status(500).end();
      console.log("getAllUsers:", err);
    } else {

      if ( !user ) {
        res.status(403).end();
      } else {

        // TODO filter for type
        helpers.getAllUsers(function(err, users) {
          if ( err ) {
            res.status(500).end();
          } else {

            res.set('Content-Type', 'application/json');
            res.end(JSON.stringify(users));

          }
        });

      }

    }

  });
};

/* /user/:id/cards */
exports.getCardsOfUser = function(req, res) {
  helpers.getRequestingUser(req, function(err, user) {

    if ( err ) {
      res.status(500).end();
    } else {

      if ( !user ) {
        res.status(403).end();
      } else {

        // TODO filter for type
        var id = req.params.id;
        helpers.getUser({
          id: id
        }, function(err, user) {

          if ( user ) {
            user.getCards().success(function(cards) {

              if ( cards ) {
                res.set('Content-Type', 'application/json');
                res.end(JSON.stringify(cards));
              } else {
                res.status(500).end();
              }

            });
          }

        });

      }

    }

  });
};

Set content type of api response to JSON.


import injectAttr from "./runtime/attr";
import injectForOwn from "./runtime/for-own";
import toFunctionCall from "./ast/to-function-call";
import iDOMMethod from "./idom-method";

// Transforms an attribute array into sequential attr calls.
export default function attrsToAttrCalls(t, file, attrs) {
  const forOwn = injectForOwn(t, file);
  const forOwnAttr = injectAttr(t, file);
  let current = [];

  return attrs.reduce((calls, attr) => {
    if (t.isJSXSpreadAttribute(attr)) {
      calls.push(toFunctionCall(t, forOwn, [attr.argument, forOwnAttr]));
    } else {
      current.push(attr);
      if (current.length === 2) {
        calls.push(toFunctionCall(t, iDOMMethod(file, "attr"), current));
        current = [];
      }
    }

    return calls;
  }, []);
}

import injectAttr from "./runtime/attr";
import injectForOwn from "./runtime/for-own";
import toFunctionCall from "./ast/to-function-call";
import iDOMMethod from "./idom-method";

// Transforms an attribute array into sequential attr calls.
export default function attrsToAttrCalls(t, file, attrs) {
  const forOwn = injectForOwn(t, file);
  const forOwnAttr = injectAttr(t, file);
  let name = null;

  return attrs.reduce((calls, attr) => {
    if (t.isJSXSpreadAttribute(attr)) {
      calls.push(toFunctionCall(t, forOwn, [attr.argument, forOwnAttr]));
    } else {
      if (name) {
        calls.push(toFunctionCall(t, iDOMMethod(file, "attr"), [name, attr]));
        name = null;
      } else {
        name = attr;
      }
    }

    return calls;
  }, []);
}

Make attributes -> attr calls easier to follow


/**
 * @flow
 */

type ReqData = Object; //TODO
type DecodedPause = { startingPoints: StartingPoints, actions: Array<Action> };

/**
 * //TODO
 */
export function encode(pausePoints: StartingPoints, actions: Array<Action>, encodeState: EncodeStateFunc): ReqData {
	//TODO
	return {};
}

/**
 * //TODO
 */
export function decode(data: ReqData, decodeState: DecodeStateFunc): DecodedPause {
	//TODO
	return {
		startingPoints: {},
		actions: []
	};
}

/**
 * @flow
 */

type ReqData = {
	startingPoints: {[key: string]: { state: string, index: number }};
	actions: Array<Object>
};
type DecodedPause = { startingPoints: StartingPoints, actions: Array<Action> };

/**
 * Encodes the paused state of the dispatcher into json to be passed over an HTTP request.
 *
 * @oaram pausePoints	{StartingPoints}			The state and place the dispatcher was paused
 * @param actions		{Array<Actions>}			The actions to perform on the server
 * @param encodeState 	{(string, any) => string}	A function that will encode the state, of a given store, to be
 *													passed over an HTTP request
 *
 * @return				{ReqData}					The data to pass in the HTTP request
 */
export function encode(pausePoints: StartingPoints, actions: Array<Action>, encodeState: EncodeStateFunc): ReqData {
	//TODO
	return {};
}

/**
 * Decodes the data into the starting points and actions to perform on the server.
 *
 * @param data			{ReqData}					The data from the client
 * @param decodeState	{(string, string) => any}	A function that will decode the state of a given store
 *
 * @return				{DecodePause}				The starting points and actions to start the dispatch from
 */
export function decode(data: ReqData, decodeState: DecodeStateFunc): DecodedPause {
	//TODO
	return {
		startingPoints: {},
		actions: []
	};
}

Add Documentation for the Request's 'encode(...)'/'decode(...)' Functions


import { generateUUID } from 'react-native-database';
import { selectNewLocationId } from '../../selectors/Entities/location';

export const LOCATION_ACTIONS = {
  CREATE: 'LOCATION/create',
  UPDATE: 'LOCATION/update',
  SAVE_NEW: 'LOCATION/saveNew',
};

const createDefaultLocation = () => ({
  id: generateUUID(),
  description: '',
  code: '',
});

const create = () => ({
  type: LOCATION_ACTIONS.CREATE,
  payload: createDefaultLocation(),
});

const update = (id, field, value) => ({
  type: LOCATION_ACTIONS.UPDATE,
  payload: { id, field, value },
});

const saveNew = location => ({
  type: LOCATION_ACTIONS.SAVE_NEW,
  payload: { location },
});

const updateNew = (value, field) => (dispatch, getState) => {
  const newLocationId = selectNewLocationId(getState());
  dispatch(update(newLocationId, field, value));
};

export const LocationActions = {
  create,
  update,
  updateNew,
  saveNew,
};

import { generateUUID } from 'react-native-database';
import { selectNewLocationId } from '../../selectors/Entities/location';

export const LOCATION_ACTIONS = {
  CREATE: 'LOCATION/create',
  UPDATE: 'LOCATION/update',
  SAVE_NEW: 'LOCATION/saveNew',
  SAVE_EDITING: 'LOCATION/saveEditing',
  RESET: 'LOCATION/reset',
};

const createDefaultLocation = () => ({
  id: generateUUID(),
  description: '',
  code: '',
});

const create = () => ({
  type: LOCATION_ACTIONS.CREATE,
  payload: { location: createDefaultLocation() },
});

const reset = () => ({
  type: LOCATION_ACTIONS.RESET,
});

const update = (id, field, value) => ({
  type: LOCATION_ACTIONS.UPDATE,
  payload: { id, field, value },
});

const saveNew = location => ({
  type: LOCATION_ACTIONS.SAVE_NEW,
  payload: { location },
});

const saveEditing = location => ({
  type: LOCATION_ACTIONS.SAVE_EDITING,
  payload: { location },
});

const updateNew = (value, field) => (dispatch, getState) => {
  const newLocationId = selectNewLocationId(getState());
  dispatch(update(newLocationId, field, value));
};

export const LocationActions = {
  create,
  update,
  updateNew,
  saveNew,
  saveEditing,
  reset,
};

Add location action updates for saving and resetting


const { renameCompanyList } = require('../repos')
const urls = require('../../../lib/urls')
// istanbul ignore next: Covered by functional tests
async function handleEditCompanyList (req, res, next) {
  const { token } = req.session
  const { name, id } = req.body
  try {
    await renameCompanyList(token, name, id)
    req.flash('success', 'List updated')
    res.send()
  } catch (error) {
    next(error)
  }
}
// istanbul ignore next: Covered by functional tests
async function renderEditCompanyListPage (req, res, next) {
  const { companyList: { id, name } } = res.locals
  const props = {
    id,
    cancelUrl: `${urls.dashboard}`,
    maxLength: 30,
    listName: name,
    returnUrl: `${urls.dashboard}`,
  }

  try {
    res.breadcrumb('Edit list name').render('company-lists/views/edit-list', {
      props,
    })
  } catch (error) {
    next(error)
  }
}

module.exports = {
  renderEditCompanyListPage,
  handleEditCompanyList,
}

const { renameCompanyList } = require('../repos')
const urls = require('../../../lib/urls')
// istanbul ignore next: Covered by functional tests
async function handleEditCompanyList (req, res, next) {
  const { token } = req.session
  const { name, id } = req.body
  try {
    await renameCompanyList(token, name, id)
    req.flash('success', 'List updated')
    res.send()
  } catch (error) {
    next(error)
  }
}
// istanbul ignore next: Covered by functional tests
async function renderEditCompanyListPage (req, res, next) {
  const { companyList: { id, name } } = res.locals
  const props = {
    id,
    cancelUrl: `${urls.dashboard()}`,
    maxLength: 30,
    listName: name,
    returnUrl: `${urls.dashboard()}`,
  }

  try {
    res.breadcrumb('Edit list name').render('company-lists/views/edit-list', {
      props,
    })
  } catch (error) {
    next(error)
  }
}

module.exports = {
  renderEditCompanyListPage,
  handleEditCompanyList,
}

Call function to return url


import React, { Component, PropTypes } from 'react';
import { Spinner } from 'native-base';
import { connect } from 'react-redux';
import AuthenticationScreen from '../authentication-screen';

/*
  Renders children components if the user is authenticated.
  Otherwise an authentication screen is displayed.
*/
class Secure extends Component {
  static propTypes = {
    authenticated: PropTypes.bool.isRequired,
    loading: PropTypes.bool.isRequired,
    children: PropTypes.node.isRequired,
  }

  render() {
    const { authenticated, loading, children } = this.props;

    // Render children component(s) if the user is authenticated
    if (authenticated) {
      return children;
    }

    // Display loading spinner while authenticating
    if (loading) {
      return <Spinner color="blue" />;
    }

    // Else open the authentication screen
    return (
      <AuthenticationScreen />
    );
  }
}

// Props tied together with Redux state
const mapStateToProps = state => ({
  authenticated: state.session.token != null,
  loading: state.session.sessionLoading || state.user.userLoading,
});

// Connect class with Redux and export
export default connect(mapStateToProps)(Secure);

import React, { Component, PropTypes } from 'react';
import { connect } from 'react-redux';
import AuthenticationScreen from '../authentication-screen';
import JASpinner from '../../common/spinner/JASpinner';

/*
  Renders children components if the user is authenticated.
  Otherwise an authentication screen is displayed.
*/
class Secure extends Component {
  static propTypes = {
    authenticated: PropTypes.bool.isRequired,
    loading: PropTypes.bool.isRequired,
    children: PropTypes.node.isRequired,
  }

  render() {
    const { authenticated, loading, children } = this.props;

    // Render children component(s) if the user is authenticated
    if (authenticated) {
      return children;
    }

    // Display loading spinner while authenticating
    if (loading) {
      return (
        <JASpinner />
      );
    }

    // Else open the authentication screen
    return (
      <AuthenticationScreen />
    );
  }
}

// Props tied together with Redux state
const mapStateToProps = state => ({
  authenticated: state.session.token != null,
  loading: state.session.sessionLoading || state.user.userLoading,
});

// Connect class with Redux and export
export default connect(mapStateToProps)(Secure);

Make Secure component use JASpinner


/* globals define */
var charming = function() {

  'use strict';

  return function(elem, opts) {

    opts = opts || {};
    var tagName = opts.tagName || 'span',
        classPrefix = typeof opts.classPrefix !== 'undefined' ? opts.classPrefix : 'char',
        count = 1;

    var traverse = function(elem) {
      var childNodes = Array.prototype.slice.call(elem.childNodes); // static array of nodes
      for (var i = 0, len = childNodes.length; i < len; i++) {
        traverse(childNodes[i]);
      }
      if (elem.nodeType === Node.TEXT_NODE) {
        inject(elem);
      }
    };

    var inject = function(elem) {
      var str = elem.nodeValue,
          parentNode = elem.parentNode;
      for (var i = 0, len = str.length; i < len; i++) {
        var node = document.createElement(tagName);
        if (classPrefix) {
          node.className = classPrefix + count;
          count++;
        }
        node.appendChild(document.createTextNode(str[i]));
        parentNode.insertBefore(node, elem);
      }
      parentNode.removeChild(elem);
    };

    traverse(elem);

    return elem;

  };

};

(function(root, factory) {
  if (typeof define === 'function' && define.amd) { // istanbul ignore
    define(factory);
  } else if (typeof exports === 'object') { // istanbul ignore
    module.exports = factory;
  } else {
    root.charming = factory(root);
  }
})(this, charming);

(function(fn) {
  /* istanbul ignore else  */
  if (typeof module === 'undefined') {
    this.charming = fn;
  } else {
    module.exports = fn;
  }
})(function(elem, opts) {

  'use strict';

  opts = opts || {};
  var tagName = opts.tagName || 'span';
  var classPrefix = opts.classPrefix != null ? opts.classPrefix : 'char';

  var count = 1;

  var inject = function(elem) {
    var parentNode = elem.parentNode;
    var str = elem.nodeValue;
    var len = str.length;
    var i = -1;
    while (++i < len) {
      var node = document.createElement(tagName);
      if (classPrefix) {
        node.className = classPrefix + count;
        count++;
      }
      node.appendChild(document.createTextNode(str[i]));
      parentNode.insertBefore(node, elem);
    }
    parentNode.removeChild(elem);
  };

  (function traverse(elem) {
    var childNodes = [].slice.call(elem.childNodes); // static array of nodes
    var len = childNodes.length;
    var i = -1;
    while (++i < len) {
      traverse(childNodes[i]);
    }
    if (elem.nodeType === Node.TEXT_NODE) {
      inject(elem);
    }
  })(elem);

  return elem;

});

Refactor + remove AMD from boilerplate


/**
 * Ember responsive initializer
 *
 * Supports auto injecting media service app-wide.
 * Generated by the ember-responsive addon.
 */
export function initialize(application) {
  application.registerOptionsForType('breakpoints', { instantiate: false });
}

export default {
  name: 'responsive',
  initialize
};

/**
 * Ember responsive initializer
 *
 * Supports auto injecting media service app-wide.
 * Generated by the ember-responsive addon.
 */
export function initialize(application) {
  application.registerOptionsForType('breakpoints', { instantiate: false });
}

export default {
  name: 'ember-responsive-breakpoints',
  initialize
};

Choose a more unique name to avoid collisions


function BackgroundGenerator() {
  this.hue = Math.floor(Math.random()*360);
  this.pattern = this.PATTERNS[Math.floor(Math.random()*this.PATTERNS.length)];
}

BackgroundGenerator.prototype = {
  PATTERNS: ['checkerboard', 'dots', 'striped'],

  toClassName: function() {
    return `${this.pattern} bg-${this.hue}`;
  },
};

module.exports = BackgroundGenerator;

var PATTERNS = ['checkerboard', 'dots', 'striped'];

class BackgroundGenerator {
  constructor() {
    this.hue = Math.floor(Math.random()*360);
    this.pattern = PATTERNS[Math.floor(Math.random()*PATTERNS.length)];
  }

  toClassName() {
    return `${this.pattern} bg-${this.hue}`;
  }
}

module.exports = BackgroundGenerator;

Use ES6 class syntax for BackgroundGenerator


/*
#
# i2mx.js - v0.1-Alpha
# Apache v2 License
#
*/

// Create img2musicXML (abbreviated as i2mx) namespace
var i2mx = i2mx || { };

// Create JsCheckup class (@TODO: Move to inside namespace)
var JsCheckup = function() {
	this.divId = "i2mx-checkup";

	this.activate = function() {
		var div = document.getElementId(this.divId);

		div.innerHTML = "No checklist for now, but img2musicXML loaded successfully!"; // @TODO: Multi-line
	}
}

var jsCheckup = new JsCheckup();

// @TODO: Use event listener instead of onload
window.onload = function() {
	jsCheckup.activate();
}

/*
#
# i2mx.js - v0.1-Alpha
# Apache v2 License
#
*/

// Create img2musicXML (abbreviated as i2mx) namespace
var i2mx = i2mx || { };

// Create JsCheckup class (@TODO: Move to inside namespace)
var JsCheckup = function() {
	this.divId = "i2mx-checkup";

	this.activate = function() {
		var div = document.getElementById(this.divId);

		div.innerHTML = "No checklist for now, but img2musicXML loaded successfully!"; // @TODO: Multi-line
	}
}

var jsCheckup = new JsCheckup();

// @TODO: Use event listener instead of onload
window.onload = function() {
	jsCheckup.activate();
}

Fix wrong function name. Again.


var errors = require('./errors');
var validRe = /^[\w-]+$/;

function validateFeedId(feedId) {
  /*
  	 * Validate that the feedId matches the spec user:1
  	 */
  var parts = feedId.split(':');
  if (parts.length !== 2) {
    throw new errors.FeedError('Invalid feedId, expected something like user:1 got ' + feedId);
  }

  var feedSlug = parts[0];
  var userId = parts[1];
  validateFeedSlug(feedSlug);
  validateUserId(userId);
  return feedId;
}

exports.validateFeedId = validateFeedId;

function validateFeedSlug(feedSlug) {
  /*
  	 * Validate that the feedSlug matches \w
  	 */
  var valid = validRe.test(feedSlug);
  if (!valid) {
    throw new errors.FeedError('Invalid feedSlug, please use letters, numbers or _ got: ' + feedSlug);
  }

  return feedSlug;
}

exports.validateFeedSlug = validateFeedSlug;

function validateUserId(userId) {
  /*
  	 * Validate the userId matches \w
  	 */
  var valid = validRe.test(userId);
  if (!valid) {
    throw new errors.FeedError('Invalid feedSlug, please use letters, numbers or _ got: ' + userId);
  }

  return userId;
}

exports.validateUserId = validateUserId;

function rfc3986(str) {
  return str.replace(/[!'()*]/g, function(c) {
    return '%' + c.charCodeAt(0).toString(16).toUpperCase();
  });
}

exports.rfc3986 = rfc3986;

var errors = require('./errors');
var validRe = /^[\w-]+$/;

function validateFeedId(feedId) {
  /*
  	 * Validate that the feedId matches the spec user:1
  	 */
  var parts = feedId.split(':');
  if (parts.length !== 2) {
    throw new errors.FeedError('Invalid feedId, expected something like user:1 got ' + feedId);
  }

  var feedSlug = parts[0];
  var userId = parts[1];
  validateFeedSlug(feedSlug);
  validateUserId(userId);
  return feedId;
}

exports.validateFeedId = validateFeedId;

function validateFeedSlug(feedSlug) {
  /*
  	 * Validate that the feedSlug matches \w
  	 */
  var valid = validRe.test(feedSlug);
  if (!valid) {
    throw new errors.FeedError('Invalid feedSlug, please use letters, numbers or _ got: ' + feedSlug);
  }

  return feedSlug;
}

exports.validateFeedSlug = validateFeedSlug;

function validateUserId(userId) {
  /*
  	 * Validate the userId matches \w
  	 */
  var valid = validRe.test(userId);
  if (!valid) {
    throw new errors.FeedError('Invalid userId, please use letters, numbers or _ got: ' + userId);
  }

  return userId;
}

exports.validateUserId = validateUserId;

function rfc3986(str) {
  return str.replace(/[!'()*]/g, function(c) {
    return '%' + c.charCodeAt(0).toString(16).toUpperCase();
  });
}

exports.rfc3986 = rfc3986;

Update userId validation message to say 'userId' instead of 'feedSlug'


// Place all the behaviors and hooks related to the matching controller here.
// All this logic will automatically be available in application.js.

var toggler = function() {
  var display = {
    errorTextToggle: document.querySelector('.error-text-toggle'),
    errorText: document.querySelector('.error-text'),

    doToggle: function(e) {
      e.preventDefault();
      this.errorText.classList.toggle('expanded');
    }
  };

  display.errorTextToggle.addEventListener('click', function(e) { display.doToggle(e); });
};

toggler();

// Place all the behaviors and hooks related to the matching controller here.
// All this logic will automatically be available in application.js.

var toggler = function() {
  var display = {
    errorTextToggle: document.querySelector('.error-text-toggle'),
    errorText: document.querySelector('.error-text'),

    doToggle: function(e) {
      e.preventDefault();
      this.errorText.classList.toggle('expanded');
    }
  };

  if (display.errorTextToggle) {
    display.errorTextToggle.addEventListener('click', function(e) { display.doToggle(e); });
  }
};

toggler();

Fix javascript on non-500 pages


'use strict';

// Node modules
const He = require('he');
const Marked = require('marked');
const Striptags = require('striptags');
const Trim = require('trim');

// Create a custom renderer
let renderer = new Marked.Renderer();

// Convert code blocks to
 instead of

renderer.code = (code, lang) => {
  let pre;

  pre = lang ? '
' : '
';
  pre += He.encode(code, { useNamedReferences: true });
  pre += '
';

  return pre;
};

const self = {

  //
  // Converts a markdown string to HTML.
  //
  //  text* (string) - The markdown string to convert.
  //
  // Returns an HTML string.
  //
  toHtml: (text) => {
    text = typeof text === 'string' ? text : '';

    return Marked(text, {
      renderer: renderer
    });
  },

  //
  // Converts a markdown string to plain text.
  //
  //  text* (string) - The markdown string to convert.
  //
  toText: (text) => {
    text = typeof text === 'string' ? text : '';

    return Trim(Striptags(He.decode(Marked(text, {
      sanitize: true
    }))));
  }

};

module.exports = self;

Fix bug where Quick Post generated wrong markup for code blocks


'use strict';

const { get } = require('lodash') ;
const ERROR_TEMPLATES = require('./errors');
const releaseMsg = require('./release');

module.exports = (config) => {
  return (releaseInfo, filterLabels) => {
    const user = get(config, 'github.user');
    const githubToSlakUsernames = get(config, 'slack.githubUsers');

    // TODO: add a config setting to notify only if there are filterred issues
    const attachments = releaseInfo.reduce((acc, repoInfo) => {
      if (isFailedReleaseAndFilteredChannel(repoInfo.failReason, filterLabels)) return acc;
      const attachment = repoInfo.failReason
        ? ERROR_TEMPLATES[repoInfo.failReason] || ERROR_TEMPLATES.UNkNOWN_ERROR
        : releaseMsg(repoInfo, filterLabels, user, githubToSlakUsernames);
      if (attachment) {
        attachment.title = repoInfo.repo;
        attachment.title_link = `https://github.com/${user}/${repoInfo.repo}`;
        acc.push(attachment);
      }
      return acc;
    }, []);

    if (attachments.length > 0) {
      return {
        attachments
      };
    }
  };

  function isFailedReleaseAndFilteredChannel(failReason, filterLabels) {
    return failReason && filterLabels && filterLabels.length > 0;
  }
};

'use strict';

const { get } = require('lodash') ;
const ERROR_TEMPLATES = require('./errors');
const releaseMsg = require('./release');

module.exports = (config) => {
  return (releaseInfo, filterLabels) => {
    const user = get(config, 'github.user');
    const githubToSlakUsernames = get(config, 'slack.githubUsers');

    // TODO: add a config setting to notify only if there are filterred issues
    const attachments = releaseInfo.reduce((acc, repoInfo) => {
      if (isFailedReleaseAndFilteredChannel(repoInfo.failReason, filterLabels)) return acc;
      const attachment = repoInfo.failReason
        ? ERROR_TEMPLATES[repoInfo.failReason] || ERROR_TEMPLATES.UNkNOWN_ERROR
        : releaseMsg(repoInfo, filterLabels, user, githubToSlakUsernames);
      if (attachment) {
        acc.push(Object.assign({}, attachment, {
          title: repoInfo.repo,
          title_link: `https://github.com/${user}/${repoInfo.repo}`
        }));
      }
      return acc;
    }, []);

    if (attachments.length > 0) {
      return {
        attachments
      };
    }
  };

  function isFailedReleaseAndFilteredChannel(failReason, filterLabels) {
    return failReason && filterLabels && filterLabels.length > 0;
  }
};

Fix the same bug for release templates


var gulp = require('gulp');
var less = require('gulp-less');
var cleanCSS = require('gulp-clean-css');
var sourcemaps = require('gulp-sourcemaps');

gulp.task('default', ['less', 'watch']);

gulp.task('less', function () {
  return gulp.src('assets/less/**/style.less')
          .pipe(sourcemaps.init())
          .pipe(less())
          .pipe(cleanCSS({
            compatibility: 'ie8'
          }))
          .pipe(sourcemaps.write())
          .pipe(gulp.dest('assets/css'));
});

gulp.task('watch', function () {
  gulp.watch(['assets/**/*.less'], ['less']);
  // gulp.watch(['assets/js/inc/*.js'], ['js']);
});

var gulp = require('gulp');
var less = require('gulp-less');
var cleanCSS = require('gulp-clean-css');
var sourcemaps = require('gulp-sourcemaps');

// The default Gulp.js task
gulp.task('default', ['bootstrap-fonts', 'bootstrap-js', 'less', 'watch']);

// Rebuild CSS from LESS
gulp.task('less', function () {
    return gulp.src('assets/less/**/style.less')
            .pipe(sourcemaps.init())
            .pipe(less())
            .pipe(cleanCSS({
                compatibility: 'ie8'
            }))
            .pipe(sourcemaps.write())
            .pipe(gulp.dest('assets/css'));
});

// Copy Bootstrap js assets in assets/js
gulp.task('bootstrap-js', function () {
    return gulp.src('node_modules/bootstrap/dist/js/bootstrap.min.js')
            .pipe(gulp.dest('assets/js'));
});

// Copy Bootstrap font files in assets/fonts
gulp.task('bootstrap-fonts', function () {
    return gulp.src('node_modules/bootstrap/dist/fonts/*')
            .pipe(gulp.dest('assets/fonts/'));
});

// Watch for LESS and JS file changes
gulp.task('watch', function () {
    gulp.watch(['assets/**/*.less'], ['less']);
    // gulp.watch(['assets/js/*.js'], ['js']);
});

Add bootstrap-js and bootstrap-fonts tasks


"use strict";

//# aims to be similiar to this "official" word count example
//# https://github.com/apache/kafka/blob/0.10.0/streams/examples/src/main/java/org/apache/kafka/streams/examples/wordcount/WordCountDemo.java

/*
    the input topic could look like this:

    "fruit banana"
    "fruit cherry"
    "vegetable broccoli"
    "fruit strawberry"
    "vegetable lettuce"

    the output topic would then look like this:

    "fruit 3"
*/

const {KafkaStreams} = require("./../index.js");
const config = require("./../test/test-config.js");

const kafkaStreams = new KafkaStreams(config);
const stream = kafkaStreams.getKStream("my-input-topic");

stream
    .map(keyValueMapperEtl)
    .countByKey("key", "count")
    .filter(kv => kv.count >= 3)
    .map(kv => kv.key + " " + kv.count)
    .tap(kv => console.log(kv))
    .to("my-output-topic");

stream.start();

//consume & produce for 5 seconds
setTimeout(kafkaStreams.closeAll().bind(kafkaStreams), 5000);

function keyValueMapperEtl(message){
    const elements = message.toLowerCase().split(" ");
    return {
        key: elements[0],
        value: elements[1]
    };
}

//# alternatively checkout ../test/unit/WordCount.test.js for a working example without kafka broker

"use strict";

//# aims to be similiar to this "official" word count example
//# https://github.com/apache/kafka/blob/0.10.0/streams/examples/src/main/java/org/apache/kafka/streams/examples/wordcount/WordCountDemo.java

/*
    the input topic could look like this:

    "fruit banana"
    "fruit cherry"
    "vegetable broccoli"
    "fruit strawberry"
    "vegetable lettuce"

    the output topic would then look like this:

    "fruit 3"
*/

const {KafkaStreams} = require("./../index.js");
const config = require("./../test/test-config.js");

const kafkaStreams = new KafkaStreams(config);
const stream = kafkaStreams.getKStream("my-input-topic");

stream
    .map(keyValueMapperEtl)
    .countByKey("key", "count")
    .filter(kv => kv.count >= 3)
    .map(kv => kv.key + " " + kv.count)
    .tap(kv => console.log(kv))
    .to("my-output-topic");

stream.start();

//consume & produce for 5 seconds
setTimeout(kafkaStreams.closeAll.bind(kafkaStreams), 5000);

function keyValueMapperEtl(message){
    const elements = message.toLowerCase().split(" ");
    return {
        key: elements[0],
        value: elements[1]
    };
}

//# alternatively checkout ../test/unit/WordCount.test.js for a working example without kafka broker

Fix bug in word count example


const mergeDirs = require('merge-dirs').default;
const helpers = require('../../lib/helpers');
const path = require('path');
const shell = require('shelljs');
const latestVersion = require('latest-version');

module.exports = latestVersion('@storybook/react-native').then(version => {
  // copy all files from the template directory to project directory
  mergeDirs(path.resolve(__dirname, 'template/'), '.', 'overwrite');

  // set correct project name on entry files if possible
  const dirname = shell.ls('-d', 'ios/*.xcodeproj').stdout;
  const projectName =
    dirname && dirname.slice('ios/'.length, dirname.length - '.xcodeproj'.length - 1);
  if (projectName) {
    shell.sed('-i', '%APP_NAME%', projectName, 'storybook/index.ios.js');
    shell.sed('-i', '%APP_NAME%', projectName, 'storybook/index.android.js');
  }

  const packageJson = helpers.getPackageJson();

  packageJson.devDependencies = packageJson.devDependencies || {};
  packageJson.devDependencies['@storybook/react-native'] = `^${version}`;

  packageJson.scripts = packageJson.scripts || {};
  packageJson.scripts['storybook'] = 'storybook start -p 7007';

  helpers.writePackageJson(packageJson);
});

const mergeDirs = require('merge-dirs').default;
const helpers = require('../../lib/helpers');
const path = require('path');
const shell = require('shelljs');
const latestVersion = require('latest-version');

module.exports = latestVersion('@storybook/react-native').then(version => {
  // copy all files from the template directory to project directory
  mergeDirs(path.resolve(__dirname, 'template/'), '.', 'overwrite');

  // set correct project name on entry files if possible
  const dirname = shell.ls('-d', 'ios/*.xcodeproj').stdout;
  const projectName =
    dirname && dirname.slice('ios/'.length, dirname.length - '.xcodeproj'.length - 1);
  if (projectName) {
    shell.sed('-i', '%APP_NAME%', projectName, 'storybook/index.ios.js');
    shell.sed('-i', '%APP_NAME%', projectName, 'storybook/index.android.js');
  }

  const packageJson = helpers.getPackageJson();

  packageJson.dependencies = packageJson.dependencies || {};
  packageJson.devDependencies = packageJson.devDependencies || {};

  packageJson.devDependencies['@storybook/react-native'] = `^${version}`;

  if (!packageJson.dependencies['react-dom'] && !packageJson.devDependencies['react-dom']) {
    packageJson.devDependencies['react-dom'] = '^15.5.4';
  }

  packageJson.scripts = packageJson.scripts || {};
  packageJson.scripts['storybook'] = 'storybook start -p 7007';

  helpers.writePackageJson(packageJson);
});

Add react-dom to the devDependency list for React Native projects


'use strict';

const database = require('./data.json');

// Starts connection
const connect = function () {
  return database;
};

module.exports = {
  connect,
};

'use strict';

const { throwError } = require('../../../error');

const database = require('./data.json');

// Starts connection
const connect = function ({ runOpts }) {
  validateEnv({ runOpts });

  return database;
};

const validateEnv = function ({ runOpts }) {
  if (runOpts.env === 'dev') { return; }

  const message = 'Memory database must not be used in production, i.e. option \'env\' must be equal to \'dev\'';
  throwError(message, { reason: 'CONF_VALIDATION' });
};

module.exports = {
  connect,
};

Validate that memory database is not used in production


var gulp = require('gulp'),
  babel = require('gulp-babel'),
  runSequence = require('run-sequence'),
  rename = require('gulp-rename'),
  electron  = require('gulp-atom-electron'),
  del = require('del'),
  run = require('gulp-run');

gulp.task('transpile:app', function() {
  return gulp.src('main/index.es6.js')
    .pipe(babel())
    .pipe(rename('index.js'))
    .pipe(gulp.dest('main'));
});

gulp.task('clean', function(){
    return del('package', {force: true});
});

gulp.task('copy:app', ['clean'], function(){
    return gulp.src(['main/**/*', 'browser/**/*', 'installs/**/*', 'package.json'], {base: '.'})
        .pipe(gulp.dest('package'));
});

gulp.task('run', ['transpile:app'], function() {
  return run('electron .').exec();
});

gulp.task('build', function() {
  return gulp.src('package/**')
        .pipe(electron({
          version: '0.30.3',
          platform: 'win32',
          arch: 'x64' }))
        .pipe(electron.zfsdest('dist/developer-platform-install.zip'));
});

gulp.task('default', function(){
    return runSequence('clean', 'transpile:app', 'copy:app','build');
  });

var gulp = require('gulp'),
  babel = require('gulp-babel'),
  runSequence = require('run-sequence'),
  rename = require('gulp-rename'),
  electron  = require('gulp-atom-electron'),
  del = require('del'),
  run = require('gulp-run');

gulp.task('transpile:app', function() {
  return gulp.src('main/index.es6.js')
    .pipe(babel())
    .pipe(rename('index.js'))
    .pipe(gulp.dest('main'));
});

gulp.task('clean', function(){
    return del('package', {force: true});
});

gulp.task('copy:app', ['clean'], function(){
    return gulp.src(['main/**/*', 'browser/**/*', 'installs/**/*', 'package.json'], {base: '.'})
        .pipe(gulp.dest('package'));
});

gulp.task('run', ['transpile:app'], function() {
  return run('electron .').exec();
});

gulp.task('build', function() {
  return gulp.src('package/**')
        .pipe(electron({
          version: '0.33.1',
          platform: 'win32',
          arch: 'x64' }))
        .pipe(electron.zfsdest('dist/developer-platform-install.zip'));
});

gulp.task('default', function(){
    return runSequence('clean', 'transpile:app', 'copy:app','build');
  });

Bring prebuilt and install versions of Electron into line


var gulp = require('gulp');
var rename = require('gulp-rename');
var loopbackAngular = require('gulp-loopback-sdk-angular');

gulp.task('default', function() {
  return gulp.src('./server/server.js')
    .pipe(loopbackAngular())
    .pipe(rename('lb-services.js'))
    .pipe(gulp.dest('./client/js'));
});

gulp.doneCallback = function(err) {
  process.exit(err ? 1 : 0);
};

Fix for Gulp default task hangs


'use strict';

var source = require('vinyl-source-stream')
var streamify = require('gulp-streamify')
var browserify = require('browserify')
var babelify = require('babelify')
var uglify = require('gulp-uglify')
var rename = require('gulp-rename')
var gulp = require('gulp')
var del = require('del')

var paths = {
	scripts: ['src/**.js']
};

gulp.task('clean', function() {
	return del(['build']);
});

gulp.task('default', ['clean'], function() {
	return browserify('./src/index.js')
		.transform('babelify')
		.bundle()
		.on('error', function (err) {
			console.log(err.toString());
			this.emit('end');
		})
		.pipe(source('index.js'))
		.pipe(rename('yaje.js'))
		.pipe(gulp.dest('build'))
		.pipe(streamify(uglify()))
		.pipe(rename('yaje.min.js'))
		.pipe(gulp.dest('build'))
});

gulp.task('watch', function() {
	gulp.watch(paths.scripts, ['default']);
});

'use strict';

var source = require('vinyl-source-stream')
var streamify = require('gulp-streamify')
var browserify = require('browserify')
var babelify = require('babelify')
var uglify = require('gulp-uglify')
var rename = require('gulp-rename')
var gulp = require('gulp')
var del = require('del')

var exitStatus = 0;
process.on('exit', function () {
	if (exitStatus !== 0) {
		process.exit(exitStatus);
	}
});

var paths = {
	scripts: ['src/**.js']
};

gulp.task('clean', function() {
	return del(['build']);
});

gulp.task('default', ['clean'], function() {
	return browserify('./src/index.js')
		.transform('babelify')
		.bundle()
		.on('error', function (err) {
			exitStatus = 1;
			console.log(err.toString());
			this.emit('end');
		})
		.pipe(source('index.js'))
		.pipe(rename('yaje.js'))
		.pipe(gulp.dest('build'))
		.pipe(streamify(uglify()))
		.pipe(rename('yaje.min.js'))
		.pipe(gulp.dest('build'))
});

gulp.task('watch', function() {
	gulp.watch(paths.scripts, ['default']);
});

Return 1 in gulp when the build failed


import React, { Component, PropTypes } from 'react';
import { Link } from 'react-router';
import Caret from '../Caret';

class Breadcrumb extends Component {
  render() {
    const { url, label } = this.props;

    return (
      <Link to={url} className="breadcrumb" title={label ? '« #{label}' : null}>
        <Caret left />
        {label ? {label} : null}
      </Link>
    );
  }
}

export default Breadcrumb;

import React, { Component, PropTypes } from 'react';
import { Link } from 'react-router';
import Caret from '../Caret';

class Breadcrumb extends Component {
  render() {
    const { url, label } = this.props;

    return (
      <Link to={url} className="breadcrumb" title={label ? `« ${label}` : null}>
        <Caret left />
        {label ? {label} : null}
      </Link>
    );
  }
}

export default Breadcrumb;

Fix string template format in header breadcrumb


var _ = require('lodash');

var MemberOfRestriction = module.exports = function(dsl, source_path) {
    this.dsl = dsl;
    this.source_path = source_path;
};

MemberOfRestriction.prototype.check_key = function(path, key, data_wrapper) {
    if (data_wrapper.get_keys(this.source_path).indexOf(key) < 0) {
        this.dsl.error(path, "Reference to unexisting key " + key + " at path " + this.source_path);
    }
};

Remove lodash from MemberOfRestrictions source


zeus.simpleModule('apps.projects.views', function (views, Marionette, $) {

    views.ProjectLayout = Marionette.Layout.extend({
        template: "#project-layout-template",

        regions: {
            contentRegion: "#project-content-region"
        }
    });

    views.ProjectDetails = zeus.views.View.extend({
        template: "#project-details-template",
        modelContextName: "project",

        events: {
            "click .show-buildset": "showBuildset"
        },

        showBuildset: function (event) {
            event.preventDefault();
            event.stopPropagation();
            var number = this.getBuildsetNumber(event);
            zeus.trigger('show:buildset', this.model.get('name'), number)
        },

        getBuildsetNumber: function (event) {
            var el = $(event.target);
            return el.attr('buildsetNumber');
        }
    });

    views.BuildsetDetails = zeus.views.View.extend({
        template: "#buildset-details-template",
        modelContextName: "buildset"
    });

}, Marionette, $);

zeus.simpleModule('apps.projects.views', function (views, Marionette, $) {

    views.ProjectLayout = Marionette.Layout.extend({
        template: "#project-layout-template",

        regions: {
            contentRegion: "#project-content-region"
        }
    });

    views.ProjectDetails = zeus.views.View.extend({
        template: "#project-details-template",
        modelContextName: "project",

        events: {
            "click .show-buildset": "showBuildset"
        },

        showBuildset: function (event) {
            event.preventDefault();
            event.stopPropagation();
            var url = $(event.target).attr('href');
            zeus.navigate(url, {trigger: true});
        }
    });

    views.BuildsetDetails = zeus.views.View.extend({
        template: "#buildset-details-template",
        modelContextName: "buildset"
    });

}, Marionette, $);

Use router to navigate to buildset


import React from 'react';
import PropTypes from 'prop-types';

import DayPickerInput from 'react-day-picker/DayPickerInput';
import 'react-day-picker/lib/style.css';

function CustomOverlay({ classNames, selectedDay, children }) {
  return (

Hello day picker!

          {selectedDay
            ? `You picked: ${selectedDay.toLocaleDateString()}`
            : 'Please pick a day'}

        {children}

  );
}

CustomOverlay.propTypes = {
  classNames: PropTypes.object.isRequired,
  selectedDay: PropTypes.oneOfType([Date]),
  children: PropTypes.number.isRequired,
};

export default function Example() {
  return (
    <DayPickerInput
      overlayComponent={CustomOverlay}
      dayPickerProps={{
        todayButton: 'Today',
      }}
    />
  );
}

import React from 'react';
import PropTypes from 'prop-types';

import DayPickerInput from 'react-day-picker/DayPickerInput';
import 'react-day-picker/lib/style.css';

function CustomOverlay({ classNames, selectedDay, children, ...props }) {
  return (

Hello day picker!

        <button onClick={() => console.log('clicked!')}>button</button>

          {selectedDay
            ? `You picked: ${selectedDay.toLocaleDateString()}`
            : 'Please pick a day'}

        {children}

  );
}

CustomOverlay.propTypes = {
  classNames: PropTypes.object.isRequired,
  selectedDay: PropTypes.instanceOf(Date),
  children: PropTypes.node.isRequired,
};

export default function Example() {
  return (
    <DayPickerInput
      overlayComponent={CustomOverlay}
      dayPickerProps={{
        todayButton: 'Today',
      }}
    />
  );
}

Make sure we pass the correct props to the overlay