<commit_before>;(function () {

	// Only support chrome for now.
	var isSupportedBrowser = /chrome/.test(navigator.userAgent.toLowerCase());

	// Credits to angular-animate for the nice animation duration detection code.
	// Detect proper transitionend/animationend event names.
	var TRANSITION_PROP, ANIMATION_PROP;
	var DURATION_KEY = 'Duration';
	var PROPERTY_KEY = 'Property';
	var DELAY_KEY = 'Delay';
	var ANIMATION_ITERATION_COUNT_KEY = 'IterationCount';
	var ONE_SECOND = 1000;

	if (window.ontransitionend === undefined && window.onwebkittransitionend !== undefined) {
		TRANSITION_PROP = 'WebkitTransition';
	} else {
		TRANSITION_PROP = 'transition';
	}

	if (window.onanimationend === undefined && window.onwebkitanimationend !== undefined) {
		ANIMATION_PROP = 'WebkitAnimation';
	} else {
		ANIMATION_PROP = 'animation';
	}

	function parseMaxTime(str) {
		var maxValue = 0, value;
		var values = typeof(str) === 'string' ?
			str.split(/\s*,\s*/) :
			[];
		for (var i = values.length; i--;) {
			value = values[i];
			maxValue = Math.max(parseFloat(value) || 0, maxValue);
		}
		return maxValue;
	}

	function getAnimationTime(element) {
		var transitionDuration = 0;
		var transitionDelay = 0;
		var animationDuration = 0;
		var animationDelay = 0;
		var elementStyles = window.getComputedStyle(element) || {};

		var transitionDurationStyle = elementStyles[TRANSITION_PROP + DURATION_KEY];
		transitionDuration = Math.max(parseMaxTime(transitionDurationStyle), transitionDuration);

		var transitionDelayStyle = elementStyles[TRANSITION_PROP + DELAY_KEY];
		transitionDelay  = Math.max(parseMaxTime(transitionDelayStyle), transitionDelay);

		var animationDelayStyle = elementStyles[ANIMATION_PROP + DELAY_KEY];
		animationDelay   = Math.max(parseMaxTime(elementStyles[ANIMATION_PROP + DELAY_KEY]), animationDelay);

		var aDuration  = parseMaxTime(elementStyles[ANIMATION_PROP + DURATION_KEY]);

		if (aDuration > 0) {
			aDuration *= parseInt(elementStyles[ANIMATION_PROP + ANIMATION_ITERATION_COUNT_KEY], 10) || 1;
		}
		animationDuration = Math.max(aDuration, animationDuration);

		return animationDuration || transitionDuration;
	}

	var defaults = {
		duration: 0.3, // Duration for the animation to happen (seconds)

		// Duration in which the target will become visible, (seconds)
		targetShowDuration: 0,

		// Extra time just to ensure continuity between dummy element and target (seconds)
		extraTransitionDuration: 1,

		// Whether to position the dummy animating element relative to window (fixed positioned) or not.
		relativeToWindow: false
	};

	function cta(trigger, target, options, callback) {
		if (!isSupportedBrowser) {
			if (callback) {
				callback(target);
			}
			return;
		}

		var targetBackground,
			triggerBackground,
			targetBounds,
			triggerBounds,
			dummy,
			extraTransitionDuration = 1;

		// Support optional arguments
		if (typeof options === 'function') {
			callback = options;
			options = {};
		}
		options = options || {};
		options.duration = options.duration || defaults.duration;
		options.targetShowDuration = options.targetShowDuration || getAnimationTime(target) || defaults.targetShowDuration;
		options.relativeToWindow = options.relativeToWindow || defaults.relativeToWindow;

		// Set some properties to make the target visible so we can get its dimensions.
		// Set `display` to `block` only when its already hidden. Otherwise changing an already visible
		// element's `display` property can lead to its position getting changed.
		if (window.getComputedStyle(target).display === 'none') {
			target.style.setProperty('display', 'block', 'important');
		}

		// Calculate some property differences to animate.
		targetBackground = window.getComputedStyle(target).background;
		triggerBackground = window.getComputedStyle(trigger).background;
		targetBounds = target.getBoundingClientRect();
		triggerBounds = trigger.getBoundingClientRect();
		scaleXRatio = triggerBounds.width / targetBounds.width;
		scaleYRatio = triggerBounds.height / targetBounds.height;
		diffX = triggerBounds.left - targetBounds.left;
		diffY = triggerBounds.top - targetBounds.top;

		// Remove the props we put earlier.
		target.style.removeProperty('display');

		// Create a dummy element for transition.
		dummy = document.createElement('div');
		dummy.style.setProperty('pointer-events', 'none', 'important');
		dummy.style.setProperty('position', (options.relativeToWindow ? 'fixed' : 'absolute'), 'important');
		dummy.style.setProperty('-webkit-transform-origin', 'top left', 'important');
		dummy.style.setProperty('transform-origin', 'top left', 'important');
		dummy.style.setProperty('transition', options.duration + 's ease');

		// Set dummy element's dimensions to final state.
		dummy.style.setProperty('width', targetBounds.width + 'px', 'important');
		dummy.style.setProperty('height', targetBounds.height + 'px', 'important');
		dummy.style.setProperty('left', (targetBounds.left + (options.relativeToWindow ? 0 : window.pageXOffset)) + 'px', 'important');
		dummy.style.setProperty('top', (targetBounds.top + (options.relativeToWindow ? 0 : window.pageYOffset)) + 'px', 'important');
		dummy.style.setProperty('background', triggerBackground, 'important');

		// Apply a reverse transform to bring back dummy element to the dimensions of the trigger/starting element.
		// Credits: This technique is inspired by Paul Lewis: http://aerotwist.com/blog/flip-your-animations/ He is amazing!
		dummy.style.setProperty('-webkit-transform', 'translate(' + diffX + 'px, ' + diffY + 'px) scale(' + scaleXRatio + ', ' + scaleYRatio + ')', 'important');
		dummy.style.setProperty('transform', 'translate(' + diffX + 'px, ' + diffY + 'px) scale(' + scaleXRatio + ', ' + scaleYRatio + ')', 'important');
		document.body.appendChild(dummy);

		// Trigger a layout to let styles apply.
		var justReadIt = dummy.offsetTop;

		// We reverting everything to lets things animate.
		dummy.style.setProperty('background', targetBackground, 'important');

		// Remove the reverse transforms to get the dummy transition back to its normal/final state.
		dummy.style.removeProperty('-webkit-transform');
		dummy.style.removeProperty('transform');

		dummy.addEventListener('transitionend', function transitionEndCallback() {
			dummy.removeEventListener('transitionend', transitionEndCallback);

			if (callback) {
				callback(target);
			}
			// Animate the dummy element to zero opacity while the target is getting rendered.
			dummy.style.transitionDuration = (options.targetShowDuration + extraTransitionDuration) + 's';
			dummy.style.opacity = 0;
			setTimeout(function () {
				dummy.parentNode.removeChild(dummy);
			}, (options.targetShowDuration + extraTransitionDuration) * 1000);
		});

		// Return a reverse animation function for the called animation.
		return function (options, callback) {
			cta(target, trigger, options, callback);
		};
	}

	cta.isSupported = isSupportedBrowser;

	// open to the world.
	// commonjs
	if( typeof exports === 'object' )  {
		module.exports = cta;
	}
	// AMD module
	else if( typeof define === 'function' && define.amd ) {
		define([], cta);
	}
	// Browser global
	else {
		window.cta = cta;
	}
})();
<commit_msg>Make it work for Firefox and Safari. Fixes #4<commit_after>;(function () {

	// Only support chrome for now.
	var isSupportedBrowser = (function () {
		// Firefox doesn't have `ontransitionend` on window. Hence we check for `transition`
		// key in style object to check for unprefixed transition support.
		return window.ontransitionend !== undefined || document.documentElement.style.transition !== undefined;
	})();

	// Credits to angular-animate for the nice animation duration detection code.
	// Detect proper transitionend/animationend event names.
	var TRANSITION_PROP, ANIMATION_PROP;
	var DURATION_KEY = 'Duration';
	var PROPERTY_KEY = 'Property';
	var DELAY_KEY = 'Delay';
	var ANIMATION_ITERATION_COUNT_KEY = 'IterationCount';
	var ONE_SECOND = 1000;

	if (window.ontransitionend === undefined && window.onwebkittransitionend !== undefined) {
		TRANSITION_PROP = 'WebkitTransition';
	} else {
		TRANSITION_PROP = 'transition';
	}

	if (window.onanimationend === undefined && window.onwebkitanimationend !== undefined) {
		ANIMATION_PROP = 'WebkitAnimation';
	} else {
		ANIMATION_PROP = 'animation';
	}

	function parseMaxTime(str) {
		var maxValue = 0, value;
		var values = typeof(str) === 'string' ?
			str.split(/\s*,\s*/) :
			[];
		for (var i = values.length; i--;) {
			value = values[i];
			maxValue = Math.max(parseFloat(value) || 0, maxValue);
		}
		return maxValue;
	}

	function getAnimationTime(element) {
		var transitionDuration = 0;
		var transitionDelay = 0;
		var animationDuration = 0;
		var animationDelay = 0;
		var elementStyles = window.getComputedStyle(element) || {};

		var transitionDurationStyle = elementStyles[TRANSITION_PROP + DURATION_KEY];
		transitionDuration = Math.max(parseMaxTime(transitionDurationStyle), transitionDuration);

		var transitionDelayStyle = elementStyles[TRANSITION_PROP + DELAY_KEY];
		transitionDelay  = Math.max(parseMaxTime(transitionDelayStyle), transitionDelay);

		var animationDelayStyle = elementStyles[ANIMATION_PROP + DELAY_KEY];
		animationDelay   = Math.max(parseMaxTime(elementStyles[ANIMATION_PROP + DELAY_KEY]), animationDelay);

		var aDuration  = parseMaxTime(elementStyles[ANIMATION_PROP + DURATION_KEY]);

		if (aDuration > 0) {
			aDuration *= parseInt(elementStyles[ANIMATION_PROP + ANIMATION_ITERATION_COUNT_KEY], 10) || 1;
		}
		animationDuration = Math.max(aDuration, animationDuration);

		return animationDuration || transitionDuration;
	}

	function getBackgroundStyle(element) {
		var computedStyle = window.getComputedStyle(element);
		// Need to fallback to `backgroundColor` as `background` return nothing in Firefox.
		return computedStyle.background || computedStyle.backgroundColor;
	}

	var defaults = {
		duration: 0.3, // Duration for the animation to happen (seconds)

		// Duration in which the target will become visible, (seconds)
		targetShowDuration: 0,

		// Extra time just to ensure continuity between dummy element and target (seconds)
		extraTransitionDuration: 1,

		// Whether to position the dummy animating element relative to window (fixed positioned) or not.
		relativeToWindow: false
	};

	function cta(trigger, target, options, callback) {
		if (!isSupportedBrowser) {
			if (callback) {
				callback(target);
			}
			return;
		}

		var targetBackground,
			triggerBackground,
			targetBounds,
			triggerBounds,
			dummy,
			extraTransitionDuration = 1;

		// Support optional arguments
		if (typeof options === 'function') {
			callback = options;
			options = {};
		}
		options = options || {};
		options.duration = options.duration || defaults.duration;
		options.targetShowDuration = options.targetShowDuration || getAnimationTime(target) || defaults.targetShowDuration;
		options.relativeToWindow = options.relativeToWindow || defaults.relativeToWindow;

		// Set some properties to make the target visible so we can get its dimensions.
		// Set `display` to `block` only when its already hidden. Otherwise changing an already visible
		// element's `display` property can lead to its position getting changed.
		if (window.getComputedStyle(target).display === 'none') {
			target.style.setProperty('display', 'block', 'important');
		}

		// Calculate some property differences to animate.
		targetBackground = getBackgroundStyle(target);
		triggerBackground = getBackgroundStyle(trigger);
		targetBounds = target.getBoundingClientRect();
		triggerBounds = trigger.getBoundingClientRect();
		scaleXRatio = triggerBounds.width / targetBounds.width;
		scaleYRatio = triggerBounds.height / targetBounds.height;
		diffX = triggerBounds.left - targetBounds.left;
		diffY = triggerBounds.top - targetBounds.top;

		// Remove the props we put earlier.
		target.style.removeProperty('display');

		// Create a dummy element for transition.
		dummy = document.createElement('div');
		dummy.style.setProperty('pointer-events', 'none', 'important');
		dummy.style.setProperty('position', (options.relativeToWindow ? 'fixed' : 'absolute'), 'important');
		dummy.style.setProperty('-webkit-transform-origin', 'top left', 'important');
		dummy.style.setProperty('transform-origin', 'top left', 'important');
		dummy.style.setProperty('transition', options.duration + 's ease');

		// Set dummy element's dimensions to final state.
		dummy.style.setProperty('width', targetBounds.width + 'px', 'important');
		dummy.style.setProperty('height', targetBounds.height + 'px', 'important');
		dummy.style.setProperty('left', (targetBounds.left + (options.relativeToWindow ? 0 : window.pageXOffset)) + 'px', 'important');
		dummy.style.setProperty('top', (targetBounds.top + (options.relativeToWindow ? 0 : window.pageYOffset)) + 'px', 'important');
		dummy.style.setProperty('background', triggerBackground, 'important');

		// Apply a reverse transform to bring back dummy element to the dimensions of the trigger/starting element.
		// Credits: This technique is inspired by Paul Lewis: http://aerotwist.com/blog/flip-your-animations/ He is amazing!
		dummy.style.setProperty('-webkit-transform', 'translate(' + diffX + 'px, ' + diffY + 'px) scale(' + scaleXRatio + ', ' + scaleYRatio + ')', 'important');
		dummy.style.setProperty('transform', 'translate(' + diffX + 'px, ' + diffY + 'px) scale(' + scaleXRatio + ', ' + scaleYRatio + ')', 'important');
		document.body.appendChild(dummy);

		// Trigger a layout to let styles apply.
		var justReadIt = dummy.offsetTop;

		// Change properties to let things animate.
		dummy.style.setProperty('background', targetBackground, 'important');

		// Remove the reverse transforms to get the dummy transition back to its normal/final state.
		dummy.style.removeProperty('-webkit-transform');
		dummy.style.removeProperty('transform');

		dummy.addEventListener('transitionend', function transitionEndCallback() {
			dummy.removeEventListener('transitionend', transitionEndCallback);

			if (callback) {
				callback(target);
			}
			// Animate the dummy element to zero opacity while the target is getting rendered.
			dummy.style.transitionDuration = (options.targetShowDuration + extraTransitionDuration) + 's';
			dummy.style.opacity = 0;
			setTimeout(function () {
				dummy.parentNode.removeChild(dummy);
			}, (options.targetShowDuration + extraTransitionDuration) * 1000);
		});

		// Return a reverse animation function for the called animation.
		return function (options, callback) {
			cta(target, trigger, options, callback);
		};
	}

	cta.isSupported = isSupportedBrowser;

	// open to the world.
	// commonjs
	if( typeof exports === 'object' )  {
		module.exports = cta;
	}
	// AMD module
	else if( typeof define === 'function' && define.amd ) {
		define([], cta);
	}
	// Browser global
	else {
		window.cta = cta;
	}
})();
<|endoftext|>


<commit_before>const $ = require('jquery');
const { BatchTestUtil } = require('../../util/batch_test_util');
const { Context } = require('../../core/context');
const EventEmitter = require('events');
const fs = require('fs');
const { TestUtil } = require('../../core/test_util');
const { UITestUtil } = require('../common/ui_test_util');
const { WorkflowBatch } = require('../../core/workflow_batch');
const { WorkflowBatchController } = require('./workflow_batch_controller');

var t = new BatchTestUtil();

beforeAll(() => {
    t.saveGoodAndBadWorkflows();
    t.createBaggingDirectory();
});

afterAll(() => {
    TestUtil.deleteJsonFile('Workflow');
    TestUtil.deleteJsonFile('AppSettings');
});

afterEach(() => {
    if (t.tempCSVFile != '' && fs.existsSync(t.tempCSVFile)) {
        fs.unlinkSync(t.tempCSVFile);
    }
});

test('new() displays form with workflows and file chooser', done => {
    let controller = new WorkflowBatchController()
    UITestUtil.setDocumentBody(controller.new());
    setTimeout(function() {
        let workflowList = $('#workflowBatchForm_workflowId');
        let options = $("#workflowBatchForm_workflowId option")
        expect(workflowList.length).toEqual(1);
        expect(options.length).toBeGreaterThan(1);
        expect($('#pathToCSVFile').length).toEqual(1);
        done();
    }, 500);
});

test('runBatch with missing workflow and batch', done => {
    let controller = new WorkflowBatchController()
    UITestUtil.setDocumentBody(controller.new());

    setTimeout(function() {
        UITestUtil.setDocumentBody(controller.runBatch());
    }, 500);
    setTimeout(function() {
        let div = $('#batchValidation');
        expect(div.length).toEqual(1);
        expect(div.css('display')).toEqual('block');

        // Errors are as follows, but may be translated.
        //
        // Workflow Id cannot be empty.
        // Path To C S V File cannot be empty.
        // DART cannot find the workflow you want to run.
        let errors = $('#batchValidation ul li');
        expect(errors.length).toEqual(3);
        done();
    }, 1000);
});

test('runBatch with invalid workflow', done => {
    let expected = [
	    'Line 2: path does not exist: /home/dev/dart/bagit',
	    'Line 3: path does not exist: /home/dev/dart/core',
	    'Line 4: path does not exist: /home/dev/dart/migrations',
	    Context.y18n.__("Cannot find BagIt profile for workflow '%s'", 'Invalid Workflow for Batch Testing'),
];
    let controller = new WorkflowBatchController()
    UITestUtil.setDocumentBody(controller.new());
    setTimeout(function() {
        // Choose a workflow with no name and missing/invalid attributes
        $("#workflowBatchForm_workflowId").val(t.emptyWorkflowId)
        controller._injectedCSVFilePath = t.csvFile
        UITestUtil.setDocumentBody(controller.runBatch());
    }, 500);
    setTimeout(function() {
        let div = $('#batchValidation');
        expect(div.length).toEqual(1);
        expect(div.css('display')).toEqual('block');
        let errors = $('#batchValidation ul li');
        expect(errors.length).toEqual(expected.length);
        for (let i=0; i < errors.length; i++) {
            let actualMsg = $(errors[i]).text();
            let expectedMsg = expected[i];
            expect(actualMsg).toEqual(expectedMsg);
        }
        done();
    }, 1000);
});

test('runBatch with valid workflow and files', done => {
    let controller = new WorkflowBatchController()

    // Set file paths in the CSV file to actual existing paths.
    t.fixPaths();

    UITestUtil.setDocumentBody(controller.new());
    setTimeout(function() {
        // Choose a workflow with no name and missing/invalid attributes
        $("#workflowBatchForm_workflowId").val(t.goodWorkflowId)
        controller._injectedCSVFilePath = t.tempCSVFile
        // This action does not return new HTML. It alters the existing DOM.
        controller.runBatch();
    }, 200);

    // This is where we check the actual results.
    // We need controllers or ui/application.js to emit events
    // so we can stop using setTimeout.
    setTimeout(function() {
        let alertDiv = $('#batchCompleted');
        expect(alertDiv.length).toEqual(1);
        expect(alertDiv.css('display')).toEqual('block');
        expect($(alertDiv).text().trim()).toEqual(Context.y18n.__("All jobs have completed. Check the results below."));

        let resultsDiv = $('#workflowResults');
        expect(resultsDiv.length).toEqual(1);
        expect(resultsDiv.css('display')).toEqual('block');

        // There are three jobs in this batch.
        let specificResults = $('div.row.batch-result');
        expect(specificResults.length).toEqual(3);

        // Results include a green icon for each success
        // and a red icon for each failure. They should all be green.
        // See ui/templates/partials/workflow_job_succeeded.html
        // and ui/templates/partials/workflow_job_failed.html
        let resultsHTML = $(resultsDiv).html();
        expect(resultsHTML).toContain("color: green;");
        expect(resultsHTML).not.toContain("color: red;");

        done();
    }, 4000);
});
<commit_msg>Try longer timeout for Travis. Need to remove test timeouts anyway.<commit_after>const $ = require('jquery');
const { BatchTestUtil } = require('../../util/batch_test_util');
const { Context } = require('../../core/context');
const EventEmitter = require('events');
const fs = require('fs');
const { TestUtil } = require('../../core/test_util');
const { UITestUtil } = require('../common/ui_test_util');
const { WorkflowBatch } = require('../../core/workflow_batch');
const { WorkflowBatchController } = require('./workflow_batch_controller');

var t = new BatchTestUtil();

beforeAll(() => {
    t.saveGoodAndBadWorkflows();
    t.createBaggingDirectory();
});

afterAll(() => {
    TestUtil.deleteJsonFile('Workflow');
    TestUtil.deleteJsonFile('AppSettings');
});

afterEach(() => {
    if (t.tempCSVFile != '' && fs.existsSync(t.tempCSVFile)) {
        fs.unlinkSync(t.tempCSVFile);
    }
});

test('new() displays form with workflows and file chooser', done => {
    let controller = new WorkflowBatchController()
    UITestUtil.setDocumentBody(controller.new());
    setTimeout(function() {
        let workflowList = $('#workflowBatchForm_workflowId');
        let options = $("#workflowBatchForm_workflowId option")
        expect(workflowList.length).toEqual(1);
        expect(options.length).toBeGreaterThan(1);
        expect($('#pathToCSVFile').length).toEqual(1);
        done();
    }, 500);
});

test('runBatch with missing workflow and batch', done => {
    let controller = new WorkflowBatchController()
    UITestUtil.setDocumentBody(controller.new());

    setTimeout(function() {
        UITestUtil.setDocumentBody(controller.runBatch());
    }, 500);
    setTimeout(function() {
        let div = $('#batchValidation');
        expect(div.length).toEqual(1);
        expect(div.css('display')).toEqual('block');

        // Errors are as follows, but may be translated.
        //
        // Workflow Id cannot be empty.
        // Path To C S V File cannot be empty.
        // DART cannot find the workflow you want to run.
        let errors = $('#batchValidation ul li');
        expect(errors.length).toEqual(3);
        done();
    }, 1000);
});

test('runBatch with invalid workflow', done => {
    let expected = [
	    'Line 2: path does not exist: /home/dev/dart/bagit',
	    'Line 3: path does not exist: /home/dev/dart/core',
	    'Line 4: path does not exist: /home/dev/dart/migrations',
	    Context.y18n.__("Cannot find BagIt profile for workflow '%s'", 'Invalid Workflow for Batch Testing'),
];
    let controller = new WorkflowBatchController()
    UITestUtil.setDocumentBody(controller.new());
    setTimeout(function() {
        // Choose a workflow with no name and missing/invalid attributes
        $("#workflowBatchForm_workflowId").val(t.emptyWorkflowId)
        controller._injectedCSVFilePath = t.csvFile
        UITestUtil.setDocumentBody(controller.runBatch());
    }, 500);
    setTimeout(function() {
        let div = $('#batchValidation');
        expect(div.length).toEqual(1);
        expect(div.css('display')).toEqual('block');
        let errors = $('#batchValidation ul li');
        expect(errors.length).toEqual(expected.length);
        for (let i=0; i < errors.length; i++) {
            let actualMsg = $(errors[i]).text();
            let expectedMsg = expected[i];
            expect(actualMsg).toEqual(expectedMsg);
        }
        done();
    }, 1000);
});

test('runBatch with valid workflow and files', done => {
    let controller = new WorkflowBatchController()

    // Set file paths in the CSV file to actual existing paths.
    t.fixPaths();

    UITestUtil.setDocumentBody(controller.new());
    setTimeout(function() {
        // Choose a workflow with no name and missing/invalid attributes
        $("#workflowBatchForm_workflowId").val(t.goodWorkflowId)
        controller._injectedCSVFilePath = t.tempCSVFile
        // This action does not return new HTML. It alters the existing DOM.
        controller.runBatch();
    }, 200);

    // This is where we check the actual results.
    // We need controllers or ui/application.js to emit events
    // so we can stop using setTimeout.
    setTimeout(function() {
        let alertDiv = $('#batchCompleted');
        expect(alertDiv.length).toEqual(1);
        expect(alertDiv.css('display')).toEqual('block');
        expect($(alertDiv).text().trim()).toEqual(Context.y18n.__("All jobs have completed. Check the results below."));

        let resultsDiv = $('#workflowResults');
        expect(resultsDiv.length).toEqual(1);
        expect(resultsDiv.css('display')).toEqual('block');

        // There are three jobs in this batch.
        let specificResults = $('div.row.batch-result');
        expect(specificResults.length).toEqual(3);

        // Results include a green icon for each success
        // and a red icon for each failure. They should all be green.
        // See ui/templates/partials/workflow_job_succeeded.html
        // and ui/templates/partials/workflow_job_failed.html
        let resultsHTML = $(resultsDiv).html();
        expect(resultsHTML).toContain("color: green;");
        expect(resultsHTML).not.toContain("color: red;");

        done();
    }, 4500);
});
<|endoftext|>


<commit_before>import React, { Component } from "react";
import { Handler } from "vega-tooltip";
import * as vl from "vega-lite";
import Vega from "react-vega";
import { withStyles } from "@material-ui/core/styles";

import colors from "libs/colors";
import "./HistogramFacet.css";

const styles = {
  timeSeriesHistogram: {
    overflowX: "visible",
    overflowY: "auto"
  },
  vega: {
    textAlign: "center"
  }
};

// If more than 120px, facet value name will be cut off with "..."
const facetValueNameWidthLimit = 120;

function isCategorical(es_field_type) {
  return es_field_type === "text" || es_field_type === "samples_overview";
}

// From https://stackoverflow.com/questions/39342575
function maxCount(arr) {
  let maxRow = arr.map(function(row) {
    return Math.max.apply(Math, row);
  });
  return Math.max.apply(null, maxRow);
}

// Creates small multiples plots for displaying time series data. See
// https://i.imgur.com/YsAV5P6.png for an example.
class TimeSeriesHistograms extends Component {
  constructor(props) {
    super(props);
    this.isValueDimmed = this.isValueDimmed.bind(this);
    this.onClick = this.onClick.bind(this);
    this.onNewView = this.onNewView.bind(this);
  }

  render() {
    const { classes } = this.props;

    const baseVegaLiteSpec = {
      $schema: "https://vega.github.io/schema/vega-lite/v3.json",
      usermeta: {
        embedOptions: {
          // Don't show menu with export buttons 'Save as SVG', etc.
          actions: false
        }
      },
      config: {
        axis: {
          // Config that applies to both axes go here
          domainColor: "#aeb3ba",
          gridColor: "#ebedef",
          labelColor: colors.dark(),
          labelFont: "Montserrat",
          labelFontWeight: 500,
          labelPadding: 9,
          ticks: false
        },
        facet: {
          spacing: 8
        }
      },
      encoding: {
        color: {
          field: "dimmed",
          type: "nominal",
          scale: {
            range: [
              // Default bar color
              "#4cabd4",
              // Unselected bar
              "#bfe1f0"
            ]
          },
          legend: null
        },
        tooltip: {
          field: "text",
          type: "nominal"
        },
        x: {},
        y: {},
        column: {},
        // opacity is needed for creating transparent bars.
        opacity: {
          field: "opaque",
          type: "nominal",
          scale: {
            range: [0, 1]
          },
          legend: null
        }
      },
      mark: {
        type: "bar",
        cursor: "pointer"
      },
      padding: {
        left: 11,
        top: 17,
        right: 30,
        bottom: 16
      },
      width: 50
    };

    let facetValueNames = this.props.facet.value_names.slice();
    const vegaLiteSpec = Object.assign({}, baseVegaLiteSpec);
    if (!isCategorical(this.props.facet.es_field_type)) {
      // For numeric facets, higher numbers should be higher on the y-axis
      facetValueNames.reverse();
    }

    const facetValueNameAxis = {
      field: "facet_value",
      type: "nominal",
      title: this.props.facet.name,
      sort: facetValueNames,
      axis: {
        labelFontSize: 12,
        labelLimit: facetValueNameWidthLimit,
        titleColor: colors.dark(),
        titleFont: "Montserrat",
        titleFontWeight: 500,
        titleFontSize: 14,
        titlePadding: 20
      },
      scale: {
        // Bar height (18px) + whitespace height (13px) = 31px
        rangeStep: 31,
        // Proportion of step that is whitespace; 13/31 = .42
        paddingInner: 0.42,
        // There should be 7 pixels of whitespace under bottom bar
        //. 7/31/2 ~= .12
        paddingOuter: 0.12
      }
    };

    const facetValueCountAxis = {
      axis: {
        labels: false
      },
      field: "count",
      type: "quantitative",
      title: "",
      stack: null
    };

    const facetValueTimeAxis = {
      field: "time_series_value",
      type: "nominal",
      title: this.props.timeSeriesUnit,
      header: {
        labelColor: colors.dark(),
        labelFont: "Montserrat",
        labelFontWeight: 500,
        labelFontSize: 12,
        labelPadding: 12,
        labelOrient: "bottom",
        titleColor: colors.dark(),
        titleFont: "Montserrat",
        titleFontWeight: 500,
        titleFontSize: 14,
        titlePadding: 8,
        titleOrient: "bottom"
      }
    };

    // Make bars horizontal, to allow for more space for facet value names for
    // categorical facets.
    vegaLiteSpec.encoding.x = facetValueCountAxis;
    vegaLiteSpec.encoding.y = facetValueNameAxis;
    vegaLiteSpec.encoding.column = facetValueTimeAxis;

    const data = { values: [] };
    for (let ti = 0; ti < this.props.facet.time_names.length; ti++) {
      for (let vi = 0; vi < this.props.facet.value_names.length; vi++) {
        let name = this.props.facet.value_names[vi];
        let count = this.props.facet.time_series_value_counts[ti][vi];
        let time = this.props.facet.time_names[ti];
        // To compute elasticsearch field name with this time, need to
        // replace decimal point with underscore as is done in the
        // index.
        let tsv_es_field_name =
          this.props.facet.es_field_name + "." + time.replace(".", "_");
        data.values.push({
          facet_value: name,
          count: count,
          time_series_value: time === "Unknown" ? time : parseFloat(time),
          tsv_es_field_name: tsv_es_field_name,
          dimmed: this.isValueDimmed(name, tsv_es_field_name),
          text: `${name}: ${count}`,
          opaque: true
        });
      }
    }

    // Create transparent bar that extends the entire length of the chart. This
    // makes tooltip/selection easier for facet values that have very low count.
    let maxFacetValue = maxCount(this.props.facet.time_series_value_counts);
    data.values = data.values.concat(
      data.values.map(v => {
        const invisible = Object.assign({}, v);
        invisible.opaque = false;
        invisible.count = maxFacetValue;
        return invisible;
      })
    );

    // vega-lite spec is easier to construct than vega spec. But certain
    // properties aren't available in vega-lite spec (vega-lite is a subset of
    // vega). So construct vega-lite spec, compile to vega spec, edit properties
    // that are only available in vega, then render Vega component.
    vegaLiteSpec.data = data;
    const vegaSpec = vl.compile(vegaLiteSpec).spec;

    // Setting align removes whitespace over top bar.
    // When https://github.com/vega/vega-lite/issues/4741 is fixed, set align
    // normally.
    vegaSpec.scales[1].align = 0;

    let vega = (
      <Vega
        spec={vegaSpec}
        tooltip={new Handler({ theme: "custom" }).call}
        onNewView={this.onNewView}
      />
    );

    return (

 {vega}

    );
  }

  isValueDimmed(facetValueName, tsv_es_field_name) {
    let selectedValues = this.props.selectedFacetValues.get(tsv_es_field_name);
    return (
      selectedValues != null &&
      selectedValues.length > 0 &&
      !selectedValues.includes(facetValueName)
    );
  }

  onClick(event, item) {
    // Ignore clicks which are not located on histogram
    // bars.
    if (item && item.datum && item.datum.facet_value) {
      let selectedValues = this.props.selectedFacetValues.get(
        item.datum.tsv_es_field_name
      );
      // facetValue is a string, eg "female"
      // If bar was clicked, item.datum.facet_value is populated.
      // If axis label was clicked, item.datum.value is populated.
      const facetValue =
        "facet_value" in item.datum ? item.datum.facet_value : item.datum.value;
      let isSelected;
      // selectedValues contains what was selected before the click.
      // isSelected contains if facet value was selected after the click.
      if (
        selectedValues != null &&
        selectedValues.length > 0 &&
        selectedValues.includes(facetValue)
      ) {
        isSelected = false;
      } else {
        isSelected = true;
      }

      this.props.updateFacets(
        item.datum.tsv_es_field_name,
        facetValue,
        isSelected
      );
    }
  }

  onNewView(view) {
    view.addEventListener("click", this.onClick);
  }
}

export default withStyles(styles)(TimeSeriesHistograms);
<commit_msg>Fixed sorting of time series columns (#354)<commit_after>import React, { Component } from "react";
import { Handler } from "vega-tooltip";
import * as vl from "vega-lite";
import Vega from "react-vega";
import { withStyles } from "@material-ui/core/styles";

import colors from "libs/colors";
import "./HistogramFacet.css";

const styles = {
  timeSeriesHistogram: {
    overflowX: "visible",
    overflowY: "auto"
  },
  vega: {
    textAlign: "center"
  }
};

// If more than 120px, facet value name will be cut off with "..."
const facetValueNameWidthLimit = 120;

function isCategorical(es_field_type) {
  return es_field_type === "text" || es_field_type === "samples_overview";
}

// From https://stackoverflow.com/questions/39342575
function maxCount(arr) {
  let maxRow = arr.map(function(row) {
    return Math.max.apply(Math, row);
  });
  return Math.max.apply(null, maxRow);
}

// Creates small multiples plots for displaying time series data. See
// https://i.imgur.com/YsAV5P6.png for an example.
class TimeSeriesHistograms extends Component {
  constructor(props) {
    super(props);
    this.isValueDimmed = this.isValueDimmed.bind(this);
    this.onClick = this.onClick.bind(this);
    this.onNewView = this.onNewView.bind(this);
  }

  render() {
    const { classes } = this.props;

    const baseVegaLiteSpec = {
      $schema: "https://vega.github.io/schema/vega-lite/v3.json",
      usermeta: {
        embedOptions: {
          // Don't show menu with export buttons 'Save as SVG', etc.
          actions: false
        }
      },
      config: {
        axis: {
          // Config that applies to both axes go here
          domainColor: "#aeb3ba",
          gridColor: "#ebedef",
          labelColor: colors.dark(),
          labelFont: "Montserrat",
          labelFontWeight: 500,
          labelPadding: 9,
          ticks: false
        },
        facet: {
          spacing: 8
        }
      },
      encoding: {
        color: {
          field: "dimmed",
          type: "nominal",
          scale: {
            range: [
              // Default bar color
              "#4cabd4",
              // Unselected bar
              "#bfe1f0"
            ]
          },
          legend: null
        },
        tooltip: {
          field: "text",
          type: "nominal"
        },
        x: {},
        y: {},
        column: {},
        // opacity is needed for creating transparent bars.
        opacity: {
          field: "opaque",
          type: "nominal",
          scale: {
            range: [0, 1]
          },
          legend: null
        }
      },
      mark: {
        type: "bar",
        cursor: "pointer"
      },
      padding: {
        left: 11,
        top: 17,
        right: 30,
        bottom: 16
      },
      width: 50
    };

    let facetValueNames = this.props.facet.value_names.slice();
    const vegaLiteSpec = Object.assign({}, baseVegaLiteSpec);
    if (!isCategorical(this.props.facet.es_field_type)) {
      // For numeric facets, higher numbers should be higher on the y-axis
      facetValueNames.reverse();
    }

    const facetValueNameAxis = {
      field: "facet_value",
      type: "nominal",
      title: this.props.facet.name,
      sort: facetValueNames,
      axis: {
        labelFontSize: 12,
        labelLimit: facetValueNameWidthLimit,
        titleColor: colors.dark(),
        titleFont: "Montserrat",
        titleFontWeight: 500,
        titleFontSize: 14,
        titlePadding: 20
      },
      scale: {
        // Bar height (18px) + whitespace height (13px) = 31px
        rangeStep: 31,
        // Proportion of step that is whitespace; 13/31 = .42
        paddingInner: 0.42,
        // There should be 7 pixels of whitespace under bottom bar
        //. 7/31/2 ~= .12
        paddingOuter: 0.12
      }
    };

    const facetValueCountAxis = {
      axis: {
        labels: false
      },
      field: "count",
      type: "quantitative",
      title: "",
      stack: null
    };

    const facetValueTimeAxis = {
      field: "time_series_value",
      type: "ordinal",
      sort: this.props.facet.time_names.map(time => this.getVegaTime(time)),
      title: this.props.timeSeriesUnit,
      header: {
        labelColor: colors.dark(),
        labelFont: "Montserrat",
        labelFontWeight: 500,
        labelFontSize: 12,
        labelPadding: 12,
        labelOrient: "bottom",
        titleColor: colors.dark(),
        titleFont: "Montserrat",
        titleFontWeight: 500,
        titleFontSize: 14,
        titlePadding: 8,
        titleOrient: "bottom"
      }
    };

    // Make bars horizontal, to allow for more space for facet value names for
    // categorical facets.
    vegaLiteSpec.encoding.x = facetValueCountAxis;
    vegaLiteSpec.encoding.y = facetValueNameAxis;
    vegaLiteSpec.encoding.column = facetValueTimeAxis;

    const data = { values: [] };
    for (let ti = 0; ti < this.props.facet.time_names.length; ti++) {
      for (let vi = 0; vi < this.props.facet.value_names.length; vi++) {
        let name = this.props.facet.value_names[vi];
        let count = this.props.facet.time_series_value_counts[ti][vi];
        let time = this.props.facet.time_names[ti];
        // To compute elasticsearch field name with this time, need to
        // replace decimal point with underscore as is done in the
        // index.
        let tsv_es_field_name =
          this.props.facet.es_field_name + "." + time.replace(".", "_");
        data.values.push({
          facet_value: name,
          count: count,
          time_series_value: this.getVegaTime(time),
          tsv_es_field_name: tsv_es_field_name,
          dimmed: this.isValueDimmed(name, tsv_es_field_name),
          text: `${name}: ${count}`,
          opaque: true
        });
      }
    }

    // Create transparent bar that extends the entire length of the chart. This
    // makes tooltip/selection easier for facet values that have very low count.
    let maxFacetValue = maxCount(this.props.facet.time_series_value_counts);
    data.values = data.values.concat(
      data.values.map(v => {
        const invisible = Object.assign({}, v);
        invisible.opaque = false;
        invisible.count = maxFacetValue;
        return invisible;
      })
    );

    // vega-lite spec is easier to construct than vega spec. But certain
    // properties aren't available in vega-lite spec (vega-lite is a subset of
    // vega). So construct vega-lite spec, compile to vega spec, edit properties
    // that are only available in vega, then render Vega component.
    vegaLiteSpec.data = data;
    const vegaSpec = vl.compile(vegaLiteSpec).spec;

    // Setting align removes whitespace over top bar.
    // When https://github.com/vega/vega-lite/issues/4741 is fixed, set align
    // normally.
    vegaSpec.scales[1].align = 0;

    let vega = (
      <Vega
        spec={vegaSpec}
        tooltip={new Handler({ theme: "custom" }).call}
        onNewView={this.onNewView}
      />
    );

    return (

 {vega}

    );
  }

  getVegaTime(time) {
    return time === "Unknown" ? time : parseFloat(time);
  }

  isValueDimmed(facetValueName, tsv_es_field_name) {
    let selectedValues = this.props.selectedFacetValues.get(tsv_es_field_name);
    return (
      selectedValues != null &&
      selectedValues.length > 0 &&
      !selectedValues.includes(facetValueName)
    );
  }

  onClick(event, item) {
    // Ignore clicks which are not located on histogram
    // bars.
    if (item && item.datum && item.datum.facet_value) {
      let selectedValues = this.props.selectedFacetValues.get(
        item.datum.tsv_es_field_name
      );
      // facetValue is a string, eg "female"
      // If bar was clicked, item.datum.facet_value is populated.
      // If axis label was clicked, item.datum.value is populated.
      const facetValue =
        "facet_value" in item.datum ? item.datum.facet_value : item.datum.value;
      let isSelected;
      // selectedValues contains what was selected before the click.
      // isSelected contains if facet value was selected after the click.
      if (
        selectedValues != null &&
        selectedValues.length > 0 &&
        selectedValues.includes(facetValue)
      ) {
        isSelected = false;
      } else {
        isSelected = true;
      }

      this.props.updateFacets(
        item.datum.tsv_es_field_name,
        facetValue,
        isSelected
      );
    }
  }

  onNewView(view) {
    view.addEventListener("click", this.onClick);
  }
}

export default withStyles(styles)(TimeSeriesHistograms);
<|endoftext|>


<commit_before>'use strict';
const _ = require('lodash');
const askName = require('inquirer-npm-name');
const parseAuthor = require('parse-author');
const Base = require('yeoman-generator').Base;
const patchPackageJSON = require('../../utils').patchPackageJSON;
const originUrl = require('git-remote-origin-url');

// based on https://github.com/yeoman/generator-node/blob/master/generators/app/index.js

class PackageJSONGenerator extends Base {

  constructor(args, options) {
    super(args, options);

    // readme content
    this.option('readme');
    this.option('longDescription');
  }

  initializing() {
    const pkg = this.fs.readJSON(this.destinationPath('package.json'), {});

    this.props = {
      description: pkg.description,
      homepage: pkg.homepage
    };

    var authorName = 'Caleydo Team';
    if (_.isObject(pkg.author)) {
      authorName = pkg.author.name;
      this.props.authorEmail = pkg.author.email;
      this.props.authorUrl = pkg.author.url;
    } else if (_.isString(pkg.author)) {
      let info = parseAuthor(pkg.author);
      authorName = info.name;
      this.props.authorEmail = info.email;
      this.props.authorUrl = info.url;
    }

    this.config.defaults({
      name: pkg.name || this.determineAppname(),
      author: authorName,
      today: (new Date()).toUTCString(),
      githubAccount: this.github ? this.github.username() : 'phovea'
    });

    return originUrl(this.destinationPath()).then((url) => {
      this.originUrl = url;
    }, () => {
      this.originUrl = '';
    });
  }

  _promptForName() {
    return askName({
      name: 'name',
      message: 'Plugin Name',
      default: this.config.get('name'),
      filter: _.snakeCase
    }, this).then((props) => {
      this.config.set('name', props.name);
    });
  }

  _promptDescription() {
    return this.prompt([{
      name: 'description',
      message: 'Description',
      default: this.props.description
    }, {
      name: 'homepage',
      message: 'Project homepage url',
      default: 'https://phovea.caleydo.org'
    }, {
      name: 'authorName',
      message: 'Author\'s Name',
      default: this.user.git.name(),
      store: true
    }, {
      name: 'authorEmail',
      message: 'Author\'s Email',
      default: this.user.git.email(),
      store: true
    }, {
      name: 'authorUrl',
      message: 'Author\'s Homepage',
      store: true
    }, {
      name: 'githubAccount',
      message: 'GitHub username or organization',
      default: this.config.get('githubAccount'),
      store: true
    }]).then((props) => {
      this.props.description = props.description;
      this.props.homepage = props.homepage;
      this.config.set('author', props.authorName);
      this.props.authorEmail = props.authorEmail;
      this.props.authorUrl = props.authorUrl;
      this.config.set('githubAccount', props.githubAccount);
    });
  }

  prompting() {
    return this._promptForName().then(() => this._promptDescription());
  }

  writing() {
    const config = _.extend({}, this.props, this.config.getAll());

    if (this.originUrl) {
      config.repository = this.originUrl;
    } else {
      config.repository = `https://github.com/${config.githubAccount}/${config.name}.git`;
    }
    patchPackageJSON.call(this, config);

    config.content = this.options.readme || '';
    config.longDescription = tihs.options.longDescription || this.props.description || '';
    this.fs.copyTpl(this.templatePath('README.tmpl.md'), this.destinationPath('README.md'), config);

    const includeDot = {
      globOptions: {
        dot: true
      }
    };
    this.fs.copy(this.templatePath('plain/**/*'), this.destinationPath(), includeDot);
  }

  end() {
    this.spawnCommandSync('git', ['init'], {
      cwd: this.destinationPath()
    });
  }
}

module.exports = PackageJSONGenerator;
<commit_msg>fix typo<commit_after>'use strict';
const _ = require('lodash');
const askName = require('inquirer-npm-name');
const parseAuthor = require('parse-author');
const Base = require('yeoman-generator').Base;
const patchPackageJSON = require('../../utils').patchPackageJSON;
const originUrl = require('git-remote-origin-url');

// based on https://github.com/yeoman/generator-node/blob/master/generators/app/index.js

class PackageJSONGenerator extends Base {

  constructor(args, options) {
    super(args, options);

    // readme content
    this.option('readme');
    this.option('longDescription');
  }

  initializing() {
    const pkg = this.fs.readJSON(this.destinationPath('package.json'), {});

    this.props = {
      description: pkg.description,
      homepage: pkg.homepage
    };

    var authorName = 'Caleydo Team';
    if (_.isObject(pkg.author)) {
      authorName = pkg.author.name;
      this.props.authorEmail = pkg.author.email;
      this.props.authorUrl = pkg.author.url;
    } else if (_.isString(pkg.author)) {
      let info = parseAuthor(pkg.author);
      authorName = info.name;
      this.props.authorEmail = info.email;
      this.props.authorUrl = info.url;
    }

    this.config.defaults({
      name: pkg.name || this.determineAppname(),
      author: authorName,
      today: (new Date()).toUTCString(),
      githubAccount: this.github ? this.github.username() : 'phovea'
    });

    return originUrl(this.destinationPath()).then((url) => {
      this.originUrl = url;
    }, () => {
      this.originUrl = '';
    });
  }

  _promptForName() {
    return askName({
      name: 'name',
      message: 'Plugin Name',
      default: this.config.get('name'),
      filter: _.snakeCase
    }, this).then((props) => {
      this.config.set('name', props.name);
    });
  }

  _promptDescription() {
    return this.prompt([{
      name: 'description',
      message: 'Description',
      default: this.props.description
    }, {
      name: 'homepage',
      message: 'Project homepage url',
      default: 'https://phovea.caleydo.org'
    }, {
      name: 'authorName',
      message: 'Author\'s Name',
      default: this.user.git.name(),
      store: true
    }, {
      name: 'authorEmail',
      message: 'Author\'s Email',
      default: this.user.git.email(),
      store: true
    }, {
      name: 'authorUrl',
      message: 'Author\'s Homepage',
      store: true
    }, {
      name: 'githubAccount',
      message: 'GitHub username or organization',
      default: this.config.get('githubAccount'),
      store: true
    }]).then((props) => {
      this.props.description = props.description;
      this.props.homepage = props.homepage;
      this.config.set('author', props.authorName);
      this.props.authorEmail = props.authorEmail;
      this.props.authorUrl = props.authorUrl;
      this.config.set('githubAccount', props.githubAccount);
    });
  }

  prompting() {
    return this._promptForName().then(() => this._promptDescription());
  }

  writing() {
    const config = _.extend({}, this.props, this.config.getAll());

    if (this.originUrl) {
      config.repository = this.originUrl;
    } else {
      config.repository = `https://github.com/${config.githubAccount}/${config.name}.git`;
    }
    patchPackageJSON.call(this, config);

    config.content = this.options.readme || '';
    config.longDescription = this.options.longDescription || this.props.description || '';
    this.fs.copyTpl(this.templatePath('README.tmpl.md'), this.destinationPath('README.md'), config);

    const includeDot = {
      globOptions: {
        dot: true
      }
    };
    this.fs.copy(this.templatePath('plain/**/*'), this.destinationPath(), includeDot);
  }

  end() {
    this.spawnCommandSync('git', ['init'], {
      cwd: this.destinationPath()
    });
  }
}

module.exports = PackageJSONGenerator;
<|endoftext|>


<commit_before>(function() {
    'use strict';

    var app = angular.module('xml2js', []);

    app.factory('x2js', function() {
        return new X2JS({
            escapeMode: true
        });
    });

})();
<commit_msg>Removed config defaults<commit_after>(function() {
    'use strict';

    var app = angular.module('xml2js', []);

    app.factory('x2js', function() {
        return new X2JS();
    });

})();
<|endoftext|>


<commit_before>import EventEmitter from 'eventemitter3';
import {
  reconcile,
} from '@theclinician/selectors';
import Socket from './socket';
import Method from './Method.js';
import Subscription from './Subscription.js';
import {
  once,
  omit,
  uniqueId,
} from './utils';
import {
  DDPError,
} from './errors.js';
import * as multiStorage from './multiStorage.js';

const DDP_VERSION = '1';
const DEFAULT_RECONNECT_INTERVAL = 10000;

const errors = [
  'error',
  'loginError',
  'resumeLoginError',
  'logoutError',
];

class DDP extends EventEmitter {
  static registerModel(Model, collection) {
    if (!collection) {
      throw Error('Method registerModel() requires collection name as the second argument');
    }
    if (this.models[collection]) {
      console.error(`Overwriting model ${this.models[collection].name} bound to collection ${collection}; new model is ${Model.name}`);
    }
    this.models[Model.collection] = Model;
  }

  constructor(options) {
    super();

    errors.forEach((name) => {
      this.on(name, (...args) => {
        if (this.listenerCount(name) <= 1) {
          console.error(...args);
        }
      });
    });

    this.status = 'disconnected';
    this.endpoint = options.endpoint;

    // Default `autoConnect` and `autoReconnect` to true
    this.autoConnect = (options.autoConnect !== false);
    this.autoReconnect = (options.autoReconnect !== false);
    this.reconnectInterval = options.reconnectInterval || DEFAULT_RECONNECT_INTERVAL;
    this.collections = {};
    this.subscriptionsToRestore = [];
    this.storage = options.storage || multiStorage;
    this.getTransform = options.getTransform || this.constructor.defaultGetTransform;

    // Methods/ subscriptions handlers
    this.subscriptions = {};
    this.methods = {};
    this.handleLoginResume = options.handleLoginResume || this.constructor.defaultHandleLoginResume;
    this.handleLogoutFailure = options.handleLogoutFailure || this.constructor.defaultHandleLogoutFailure;

    this.methodsInQueue = [];
    this.methodsPending = {};
    this.currentMethodId = null;

    // Socket
    this.socket = new Socket(options.SocketConstructor, options.endpoint);

    if (options.debug) {
      this.socket.on('message:out', message => console.warn('DDP/OUT', message));
      this.socket.on('message:in', message => console.warn('DDP/IN', message));
    }

    this.socket.on('open', () => {
      // When the socket opens, send the `connect` message
      // to establish the DDP connection
      this.socket.send({
        msg: 'connect',
        version: DDP_VERSION,
        support: [DDP_VERSION],
      });
    });

    this.socket.on('close', () => {
      this.status = 'disconnected';

      this.emit('disconnected');
      this.subscriptionsToRestore = Object.keys(this.subscriptions);
      if (this.autoReconnect) {
        // Schedule a connection
        setTimeout(
          this.socket.open.bind(this.socket),
          this.reconnectInterval,
        );
      }
    });

    this.socket.on('message:in', (message) => {
      switch (message.msg) {
        case 'connected':
          this.status = 'connected';
          // It's important to call it before resumeLogin because
          // when login returns it calls "emptyQueue" immediately.
          this.discardCancelableMethods();

          // NOTE: Restore subscriptions ensures that all subscriptions
          //       that were active before we lost the connection are now
          //       re-created. However, there might be elements that are automatically published
          //       by meteor with Meteor.publish(null), e.g. current user details.
          //       To ensure they're not lost, instead of clearing cache inside restoreSubscriptions()
          //       we do it right here, immediately after receiving "connected" message.
          this.oldCollections = this.collections;
          this.collections = {};

          this.resumeLogin().then(() => {
            // NOTE: Here we call again methods that were
            //       already called before. Theoretically it might be better
            //       to re-queue them instead of calling immediately, but it's
            //       not obvious what the logic should exactly be.
            this.restorePendingMethods();
            this.emptyQueue();
            this.restoreSubscriptions();
            this.emit('connected');
          });
          break;
        case 'ping':
          // Reply with a `pong` message to prevent the server from
          // closing the connection
          this.socket.send({ msg: 'pong', id: message.id });
          break;
        case 'ready':
          this.emit('ready', { subs: message.subs });
          message.subs.forEach(id => this.subscriptions[id] && this.subscriptions[id].ready());
          break;
        case 'nosub':
          if (this.subscriptions[message.id]) {
            this.subscriptions[message.id].nosub(message.error);
          }
          break;
        case 'result':
          if (this.methods[message.id]) {
            const { error, result } = message;
            this.methods[message.id].result({
              error,
              result,
            });
          }
          break;
        case 'updated':
          this.emit('updated', { methods: message.methods });
          message.methods.forEach(id => this.methods[id] && this.methods[id].updated());
          break;
        case 'error':
          this.emit('error', new DDPError('DDPError', message.reason));
          break;
        case 'added':
        case 'changed':
        case 'removed':
          this[message.msg](message);
          break;
        default:
          // ignore the message ...
      }
    });

    if (this.autoConnect) {
      this.connect();
    }
  }

  connect() {
    this.socket.open();
  }

  maybeEmitDataUpdated() {
    if (!this.oldCollections) {
      this.emit('dataUpdated', this.collections);
    }
  }

  added({ collection, id, fields }) {
    const transform = this.getTransform(collection);
    if (transform) {
      this.collections = {
        ...this.collections,
        [collection]: {
          ...this.collections[collection],
          [id]: transform({
            _id: id,
            ...fields,
          }),
        },
      };
      this.maybeEmitDataUpdated();
    }
    this.emit('added', { collection, id, fields });
  }

  changed({
    collection,
    id,
    fields,
    cleared,
  }) {
    const transform = this.getTransform(collection);
    if (transform) {
      this.collections = {
        ...this.collections,
        [collection]: {
          ...this.collections[collection],
          [id]: transform(omit({
            // NOTE: Theoretically this should not happen, i.e. there
            //       should always be "added" prior to "changed", but
            //       sometimes it does not seem to be the case, so better
            //       fallback by interpreting the first "changed" as "added".
            _id: id,
            ...this.collections[collection] &&
               this.collections[collection][id],
            ...fields,
          }, cleared)),
        },
      };
      this.maybeEmitDataUpdated();
    }
    this.emit('changed', { collection, id, fields });
  }

  removed({ collection, id }) {
    const transform = this.getTransform(collection);
    if (transform) {
      this.collections = {
        ...this.collections,
        [collection]: this.collections[collection]
          ? omit(this.collections[collection], [id])
          : {},
      };
      this.maybeEmitDataUpdated();
    }
    this.emit('removed', { collection, id });
  }

  disconnect() {
    /**
     * If `disconnect` is called, the caller likely doesn't want the
     * the instance to try to auto-reconnect. Therefore we set the
     * `autoReconnect` flag to false.
     */
    this.autoReconnect = false;
    this.socket.close();
  }

  discardCancelableMethods() {
    Object.keys(this.methodsPending).forEach((id) => {
      const method = this.methods[id];
      if (method &&
          method.noRetry) {
        method.cancel();
      }
    });

    const methodsInQueue = [];
    this.methodsInQueue.forEach((queued) => {
      if (queued.cancelOnReconnect) {
        const method = this.methods[queued.id];
        if (method) {
          method.cancel();
          delete this.methods[queued.id];
        }
      } else {
        methodsInQueue.push(queued);
      }
    });
    this.methodsInQueue = methodsInQueue;
  }

  emptyQueue() {
    if (this.currentMethodId || this.status !== 'connected') {
      return;
    }
    while (this.methodsInQueue.length > 0) {
      const { id, wait } = this.methodsInQueue.shift();
      const method = this.methods[id];
      if (method) {
        if (method.wasCanceled) {
          delete this.methods[id];
        } else {
          this.socket.send(method.toDDPMessage(id));
          this.methodsPending[id] = method;
          method.setCallback(() => {
            delete this.methods[id];
            delete this.methodsPending[id];
            if (wait) {
              this.currentMethodId = null;
              this.emptyQueue();
            }
          });
          if (wait) {
            this.currentMethodId = id;
            return;
          }
        }
      }
    }
  }

  restorePendingMethods() {
    const allMethodsIds = Object.keys(this.methodsPending);
    allMethodsIds.forEach((id) => {
      const method = this.methods[id];
      if (method &&
          !method.wasCanceled &&
          !method.methodResult) {
        this.socket.send(method.toDDPMessage(id));
      }
    });
    this.once('restored', () => {
      allMethodsIds.forEach((id) => {
        const method = this.methods[id];
        if (method &&
            method.methodResult &&
            !method.wasCanceled &&
            !method.dataVisible) {
          method.updated();
        }
      });
    });
  }

  restoreSubscriptions() {
    const {
      subscriptionsToRestore,
    } = this;
    // NOTE: We don't want to restore the same subscriptions more than once.
    this.subscriptionsToRestore = [];

    let numberOfPending = subscriptionsToRestore.length;

    const reconcileCollections = () => {
      if (this.oldCollections) {
        this.collections = reconcile(this.oldCollections, this.collections);
        if (this.collections !== this.oldCollections) {
          this.emit('dataUpdated', this.collections);
        }
        delete this.oldCollections;
      }
    };

    const cb = () => {
      numberOfPending -= 1;
      if (numberOfPending === 0) {
        reconcileCollections();
        this.emit('restored');
      }
    };

    if (numberOfPending > 0 && this.status === 'connected') {
      this.emit('restoring');
      subscriptionsToRestore.forEach((id) => {
        this.subscriptions[id].setCallback(cb);
        this.socket.send(this.subscriptions[id].toDDPMessage(id));
      });
    } else {
      reconcileCollections();
    }
  }

  subscribe(name, params, { onStop, onReady } = {}) {
    const id = uniqueId();

    this.subscriptions[id] = new Subscription({
      name,
      params,
      onStop,
      onReady,
    });

    if (this.status === 'connected') {
      this.socket.send(this.subscriptions[id].toDDPMessage(id));
    } else {
      this.subscriptionsToRestore.push(id);
    }

    return {
      stop: once(() => {
        if (this.status === 'connected') {
          this.socket.send({ id, msg: 'unsub' });
        }
        delete this.subscriptions[id];
        if (onStop) {
          onStop();
        }
      }),
    };
  }

  apply(name, params, {
    wait,
    cancelOnReconnect,
    noRetry,
    skipQueue,
    onResultReceived,
    throwStubExceptions,
  } = {}, asyncCallback) {
    if (!asyncCallback) {
      return new Promise((resolve, reject) => {
        this.apply(name, params, {
          wait,
          cancelOnReconnect,
          noRetry,
          skipQueue,
          onResultReceived,
          throwStubExceptions,
        }, (error, result) => (error ? reject(error) : resolve(result)));
      });
    }

    const id = uniqueId();
    this.methods[id] = new Method({
      name,
      params,
      noRetry,
      onResultReceived,
      asyncCallback,
    });

    const queued = {
      id,
      wait,
      cancelOnReconnect,
    };
    if (skipQueue) {
      this.methodsInQueue.unshift(queued);
    } else {
      this.methodsInQueue.push(queued);
    }
    this.emptyQueue();

    return undefined;
  }

  handleLogin({ id, token }) {
    this.userId = id;
    return (token
      ? this.storage.set(`${this.endpoint}__login_token__`, token)
      : Promise.resolve()
    )
      .then(this.emit.bind(this, 'loggedIn', id))
      .then(() => id);
  }

  handleLogout() {
    this.userId = null;
    return this.storage
      .del(`${this.endpoint}__login_token__`)
      .then(this.emit.bind(this, 'loggedOut'))
      .then(() => null);
  }

  resumeLogin() {
    // NOTE: This promise never rejects
    return Promise.resolve()
      .then(this.emit.bind(this, 'loggingIn'))
      .then(this.handleLoginResume.bind(this))
      .catch(err => (
        Promise.resolve()
          .then(this.handleLogout.bind(this))
          .then(this.emit.bind(this, 'resumeLoginError', err))
      ));
  }

  login(options, { skipQueue } = {}) {
    return this.executeLoginRoutine('login', [options], { skipQueue });
  }

  logout() {
    return Promise.resolve()
      .then(this.emit.bind(this, 'loggingIn'))
      .then(this.apply.bind(this, 'logout', []))
      .then(this.handleLogout.bind(this))
      .catch(err => (
        Promise.resolve()
          .then(this.handleLogoutFailure.bind(this, err))
          .then(this.emit.bind(this, 'logoutError', err))
          .then(() => Promise.reject(err))
      ));
  }

  executeLoginRoutine(name, params, { skipQueue = false } = {}) {
    let getLoginPromise;
    if (typeof name === 'function') {
      getLoginPromise = name;
    } else {
      getLoginPromise = () => this.apply(name, params, { skipQueue, wait: true });
    }
    return Promise.resolve()
      .then(this.emit.bind(this, 'loggingIn'))
      .then(() => getLoginPromise())
      .then(this.handleLogin.bind(this))
      .catch(err => (
        Promise.resolve()
          .then(this.emit.bind(this, 'loginError', err))
          .then(this.handleLogout.bind(this))
          .then(() => Promise.reject(err))
      ));
  }
}

const identity = x => x;

DDP.models = {};
DDP.defaultGetTransform = (collection) => {
  const Model = DDP.models[collection];
  if (Model) {
    return doc => new Model(doc);
  }
  return identity;
};

DDP.defaultHandleLogoutFailure = function () {
  return this.handleLogout();
};

DDP.defaultHandleLoginResume = function () {
  return this.storage.get(`${this.endpoint}__login_token__`)
    .then((resume) => {
      if (!resume) {
        // NOTE: It's important to emit loginError here, because we've already
        //       triggered loggingIn. So by triggering error as well we are
        //       indicating that the login procedure was actually terminated.
        this.emit('loginError', new Error('No login token'));
        return undefined;
      }
      return this.login({ resume }, { skipQueue: true });
    });
};

export default DDP;
<commit_msg>Fix error when subscription id is deleted early<commit_after>import EventEmitter from 'eventemitter3';
import {
  reconcile,
} from '@theclinician/selectors';
import Socket from './socket';
import Method from './Method.js';
import Subscription from './Subscription.js';
import {
  once,
  omit,
  uniqueId,
} from './utils';
import {
  DDPError,
} from './errors.js';
import * as multiStorage from './multiStorage.js';

const DDP_VERSION = '1';
const DEFAULT_RECONNECT_INTERVAL = 10000;

const errors = [
  'error',
  'loginError',
  'resumeLoginError',
  'logoutError',
];

class DDP extends EventEmitter {
  static registerModel(Model, collection) {
    if (!collection) {
      throw Error('Method registerModel() requires collection name as the second argument');
    }
    if (this.models[collection]) {
      console.error(`Overwriting model ${this.models[collection].name} bound to collection ${collection}; new model is ${Model.name}`);
    }
    this.models[Model.collection] = Model;
  }

  constructor(options) {
    super();

    errors.forEach((name) => {
      this.on(name, (...args) => {
        if (this.listenerCount(name) <= 1) {
          console.error(...args);
        }
      });
    });

    this.status = 'disconnected';
    this.endpoint = options.endpoint;

    // Default `autoConnect` and `autoReconnect` to true
    this.autoConnect = (options.autoConnect !== false);
    this.autoReconnect = (options.autoReconnect !== false);
    this.reconnectInterval = options.reconnectInterval || DEFAULT_RECONNECT_INTERVAL;
    this.collections = {};
    this.subscriptionsToRestore = [];
    this.storage = options.storage || multiStorage;
    this.getTransform = options.getTransform || this.constructor.defaultGetTransform;

    // Methods/ subscriptions handlers
    this.subscriptions = {};
    this.methods = {};
    this.handleLoginResume = options.handleLoginResume || this.constructor.defaultHandleLoginResume;
    this.handleLogoutFailure = options.handleLogoutFailure || this.constructor.defaultHandleLogoutFailure;

    this.methodsInQueue = [];
    this.methodsPending = {};
    this.currentMethodId = null;

    // Socket
    this.socket = new Socket(options.SocketConstructor, options.endpoint);

    if (options.debug) {
      this.socket.on('message:out', message => console.warn('DDP/OUT', message));
      this.socket.on('message:in', message => console.warn('DDP/IN', message));
    }

    this.socket.on('open', () => {
      // When the socket opens, send the `connect` message
      // to establish the DDP connection
      this.socket.send({
        msg: 'connect',
        version: DDP_VERSION,
        support: [DDP_VERSION],
      });
    });

    this.socket.on('close', () => {
      this.status = 'disconnected';

      this.emit('disconnected');
      this.subscriptionsToRestore = Object.keys(this.subscriptions);
      if (this.autoReconnect) {
        // Schedule a connection
        setTimeout(
          this.socket.open.bind(this.socket),
          this.reconnectInterval,
        );
      }
    });

    this.socket.on('message:in', (message) => {
      switch (message.msg) {
        case 'connected':
          this.status = 'connected';
          // It's important to call it before resumeLogin because
          // when login returns it calls "emptyQueue" immediately.
          this.discardCancelableMethods();

          // NOTE: Restore subscriptions ensures that all subscriptions
          //       that were active before we lost the connection are now
          //       re-created. However, there might be elements that are automatically published
          //       by meteor with Meteor.publish(null), e.g. current user details.
          //       To ensure they're not lost, instead of clearing cache inside restoreSubscriptions()
          //       we do it right here, immediately after receiving "connected" message.
          this.oldCollections = this.collections;
          this.collections = {};

          this.resumeLogin().then(() => {
            // NOTE: Here we call again methods that were
            //       already called before. Theoretically it might be better
            //       to re-queue them instead of calling immediately, but it's
            //       not obvious what the logic should exactly be.
            this.restorePendingMethods();
            this.emptyQueue();
            this.restoreSubscriptions();
            this.emit('connected');
          });
          break;
        case 'ping':
          // Reply with a `pong` message to prevent the server from
          // closing the connection
          this.socket.send({ msg: 'pong', id: message.id });
          break;
        case 'ready':
          this.emit('ready', { subs: message.subs });
          message.subs.forEach(id => this.subscriptions[id] && this.subscriptions[id].ready());
          break;
        case 'nosub':
          if (this.subscriptions[message.id]) {
            this.subscriptions[message.id].nosub(message.error);
          }
          break;
        case 'result':
          if (this.methods[message.id]) {
            const { error, result } = message;
            this.methods[message.id].result({
              error,
              result,
            });
          }
          break;
        case 'updated':
          this.emit('updated', { methods: message.methods });
          message.methods.forEach(id => this.methods[id] && this.methods[id].updated());
          break;
        case 'error':
          this.emit('error', new DDPError('DDPError', message.reason));
          break;
        case 'added':
        case 'changed':
        case 'removed':
          this[message.msg](message);
          break;
        default:
          // ignore the message ...
      }
    });

    if (this.autoConnect) {
      this.connect();
    }
  }

  connect() {
    this.socket.open();
  }

  maybeEmitDataUpdated() {
    if (!this.oldCollections) {
      this.emit('dataUpdated', this.collections);
    }
  }

  added({ collection, id, fields }) {
    const transform = this.getTransform(collection);
    if (transform) {
      this.collections = {
        ...this.collections,
        [collection]: {
          ...this.collections[collection],
          [id]: transform({
            _id: id,
            ...fields,
          }),
        },
      };
      this.maybeEmitDataUpdated();
    }
    this.emit('added', { collection, id, fields });
  }

  changed({
    collection,
    id,
    fields,
    cleared,
  }) {
    const transform = this.getTransform(collection);
    if (transform) {
      this.collections = {
        ...this.collections,
        [collection]: {
          ...this.collections[collection],
          [id]: transform(omit({
            // NOTE: Theoretically this should not happen, i.e. there
            //       should always be "added" prior to "changed", but
            //       sometimes it does not seem to be the case, so better
            //       fallback by interpreting the first "changed" as "added".
            _id: id,
            ...this.collections[collection] &&
               this.collections[collection][id],
            ...fields,
          }, cleared)),
        },
      };
      this.maybeEmitDataUpdated();
    }
    this.emit('changed', { collection, id, fields });
  }

  removed({ collection, id }) {
    const transform = this.getTransform(collection);
    if (transform) {
      this.collections = {
        ...this.collections,
        [collection]: this.collections[collection]
          ? omit(this.collections[collection], [id])
          : {},
      };
      this.maybeEmitDataUpdated();
    }
    this.emit('removed', { collection, id });
  }

  disconnect() {
    /**
     * If `disconnect` is called, the caller likely doesn't want the
     * the instance to try to auto-reconnect. Therefore we set the
     * `autoReconnect` flag to false.
     */
    this.autoReconnect = false;
    this.socket.close();
  }

  discardCancelableMethods() {
    Object.keys(this.methodsPending).forEach((id) => {
      const method = this.methods[id];
      if (method &&
          method.noRetry) {
        method.cancel();
      }
    });

    const methodsInQueue = [];
    this.methodsInQueue.forEach((queued) => {
      if (queued.cancelOnReconnect) {
        const method = this.methods[queued.id];
        if (method) {
          method.cancel();
          delete this.methods[queued.id];
        }
      } else {
        methodsInQueue.push(queued);
      }
    });
    this.methodsInQueue = methodsInQueue;
  }

  emptyQueue() {
    if (this.currentMethodId || this.status !== 'connected') {
      return;
    }
    while (this.methodsInQueue.length > 0) {
      const { id, wait } = this.methodsInQueue.shift();
      const method = this.methods[id];
      if (method) {
        if (method.wasCanceled) {
          delete this.methods[id];
        } else {
          this.socket.send(method.toDDPMessage(id));
          this.methodsPending[id] = method;
          method.setCallback(() => {
            delete this.methods[id];
            delete this.methodsPending[id];
            if (wait) {
              this.currentMethodId = null;
              this.emptyQueue();
            }
          });
          if (wait) {
            this.currentMethodId = id;
            return;
          }
        }
      }
    }
  }

  restorePendingMethods() {
    const allMethodsIds = Object.keys(this.methodsPending);
    allMethodsIds.forEach((id) => {
      const method = this.methods[id];
      if (method &&
          !method.wasCanceled &&
          !method.methodResult) {
        this.socket.send(method.toDDPMessage(id));
      }
    });
    this.once('restored', () => {
      allMethodsIds.forEach((id) => {
        const method = this.methods[id];
        if (method &&
            method.methodResult &&
            !method.wasCanceled &&
            !method.dataVisible) {
          method.updated();
        }
      });
    });
  }

  restoreSubscriptions() {
    const {
      subscriptionsToRestore,
    } = this;
    // NOTE: We don't want to restore the same subscriptions more than once.
    this.subscriptionsToRestore = [];

    let numberOfPending = subscriptionsToRestore.length;

    const reconcileCollections = () => {
      if (this.oldCollections) {
        this.collections = reconcile(this.oldCollections, this.collections);
        if (this.collections !== this.oldCollections) {
          this.emit('dataUpdated', this.collections);
        }
        delete this.oldCollections;
      }
    };

    const cb = () => {
      numberOfPending -= 1;
      if (numberOfPending === 0) {
        reconcileCollections();
        this.emit('restored');
      }
    };

    if (numberOfPending > 0 && this.status === 'connected') {
      this.emit('restoring');
      subscriptionsToRestore.forEach((id) => {
        // NOTE: Double check if this is still a valid subscription just in case,
        //       e.g. it could have been manually stopped by the time we got here.
        if (this.subscriptions[id]) {
          this.subscriptions[id].setCallback(cb);
          this.socket.send(this.subscriptions[id].toDDPMessage(id));
        }
      });
    } else {
      reconcileCollections();
    }
  }

  subscribe(name, params, { onStop, onReady } = {}) {
    const id = uniqueId();

    this.subscriptions[id] = new Subscription({
      name,
      params,
      onStop,
      onReady,
    });

    if (this.status === 'connected') {
      this.socket.send(this.subscriptions[id].toDDPMessage(id));
    } else {
      this.subscriptionsToRestore.push(id);
    }

    return {
      stop: once(() => {
        if (this.status === 'connected') {
          this.socket.send({ id, msg: 'unsub' });
        }
        const i = this.subscriptionsToRestore.indexOf(id);
        if (i >= 0) {
          this.subscriptionsToRestore.splice(i, 1);
        }
        delete this.subscriptions[id];
        if (onStop) {
          onStop();
        }
      }),
    };
  }

  apply(name, params, {
    wait,
    cancelOnReconnect,
    noRetry,
    skipQueue,
    onResultReceived,
    throwStubExceptions,
  } = {}, asyncCallback) {
    if (!asyncCallback) {
      return new Promise((resolve, reject) => {
        this.apply(name, params, {
          wait,
          cancelOnReconnect,
          noRetry,
          skipQueue,
          onResultReceived,
          throwStubExceptions,
        }, (error, result) => (error ? reject(error) : resolve(result)));
      });
    }

    const id = uniqueId();
    this.methods[id] = new Method({
      name,
      params,
      noRetry,
      onResultReceived,
      asyncCallback,
    });

    const queued = {
      id,
      wait,
      cancelOnReconnect,
    };
    if (skipQueue) {
      this.methodsInQueue.unshift(queued);
    } else {
      this.methodsInQueue.push(queued);
    }
    this.emptyQueue();

    return undefined;
  }

  handleLogin({ id, token }) {
    this.userId = id;
    return (token
      ? this.storage.set(`${this.endpoint}__login_token__`, token)
      : Promise.resolve()
    )
      .then(this.emit.bind(this, 'loggedIn', id))
      .then(() => id);
  }

  handleLogout() {
    this.userId = null;
    return this.storage
      .del(`${this.endpoint}__login_token__`)
      .then(this.emit.bind(this, 'loggedOut'))
      .then(() => null);
  }

  resumeLogin() {
    // NOTE: This promise never rejects
    return Promise.resolve()
      .then(this.emit.bind(this, 'loggingIn'))
      .then(this.handleLoginResume.bind(this))
      .catch(err => (
        Promise.resolve()
          .then(this.handleLogout.bind(this))
          .then(this.emit.bind(this, 'resumeLoginError', err))
      ));
  }

  login(options, { skipQueue } = {}) {
    return this.executeLoginRoutine('login', [options], { skipQueue });
  }

  logout() {
    return Promise.resolve()
      .then(this.emit.bind(this, 'loggingIn'))
      .then(this.apply.bind(this, 'logout', []))
      .then(this.handleLogout.bind(this))
      .catch(err => (
        Promise.resolve()
          .then(this.handleLogoutFailure.bind(this, err))
          .then(this.emit.bind(this, 'logoutError', err))
          .then(() => Promise.reject(err))
      ));
  }

  executeLoginRoutine(name, params, { skipQueue = false } = {}) {
    let getLoginPromise;
    if (typeof name === 'function') {
      getLoginPromise = name;
    } else {
      getLoginPromise = () => this.apply(name, params, { skipQueue, wait: true });
    }
    return Promise.resolve()
      .then(this.emit.bind(this, 'loggingIn'))
      .then(() => getLoginPromise())
      .then(this.handleLogin.bind(this))
      .catch(err => (
        Promise.resolve()
          .then(this.emit.bind(this, 'loginError', err))
          .then(this.handleLogout.bind(this))
          .then(() => Promise.reject(err))
      ));
  }
}

const identity = x => x;

DDP.models = {};
DDP.defaultGetTransform = (collection) => {
  const Model = DDP.models[collection];
  if (Model) {
    return doc => new Model(doc);
  }
  return identity;
};

DDP.defaultHandleLogoutFailure = function () {
  return this.handleLogout();
};

DDP.defaultHandleLoginResume = function () {
  return this.storage.get(`${this.endpoint}__login_token__`)
    .then((resume) => {
      if (!resume) {
        // NOTE: It's important to emit loginError here, because we've already
        //       triggered loggingIn. So by triggering error as well we are
        //       indicating that the login procedure was actually terminated.
        this.emit('loginError', new Error('No login token'));
        return undefined;
      }
      return this.login({ resume }, { skipQueue: true });
    });
};

export default DDP;
<|endoftext|>


<commit_before>'use strict';
var yeoman = require('yeoman-generator');
var prompts = require('./prompts');
var _ = require('lodash');

module.exports = yeoman.Base.extend({
  initializing: function() {
    this.appName = this.fs.readJSON(this.destinationPath('package.json')).name;
  },

  prompting: prompts,

  writing: function () {
    if (this.envFile) {
      this.fs.write(this.destinationPath('.env'), this.envFile);
    } else {
      this.fs.copyTpl(
        this.templatePath('.env'),
        this.destinationPath('.env'),
        { appName: _.snakeCase(this.appName) }
      );
    }
  },

  install: function () {
    this.npmInstall();

    this.spawnCommand('gem', [ 'install', 'bundler', '--conservative' ])
      .on('exit', (code) => {
        if (!code) {
          this.spawnCommand('ruby', [ this.destinationPath('bin', 'bundle'), 'install' ])
            .on('exit', (code) => {
              if (!code) {
                this.spawnCommand('ruby', [ this.destinationPath('bin', 'rails'), 'db:setup' ]);
                this.spawnCommand('ruby', [ this.destinationPath('bin', 'rails'), 'log:clear', 'tmp:clear' ]);
              }
            });
        }
      });
  }
});
<commit_msg>Add a basic initial git setup on the new project<commit_after>'use strict';
var yeoman = require('yeoman-generator');
var prompts = require('./prompts');
var _ = require('lodash');

module.exports = yeoman.Base.extend({
  initializing: function() {
    this.appName = this.fs.readJSON(this.destinationPath('package.json')).name;
  },

  prompting: prompts,

  writing: function () {
    if (this.envFile) {
      this.fs.write(this.destinationPath('.env'), this.envFile);
    } else {
      this.fs.copyTpl(
        this.templatePath('.env'),
        this.destinationPath('.env'),
        { appName: _.snakeCase(this.appName) }
      );
    }
  },

  install: function () {
    this.spawnCommand('git', [ 'init' ])
      .on('exit', () => {
        this.spawnCommand('git', [ 'add', '.' ])
          .on('exit', () => {
            this.spawnCommand('git', [ 'commit', '-m', 'Initial commit' ]);
          });
      });

    this.npmInstall();

    this.spawnCommand('gem', [ 'install', 'bundler', '--conservative' ])
      .on('exit', (code) => {
        if (!code) {
          this.spawnCommand('ruby', [ this.destinationPath('bin', 'bundle'), 'install' ])
            .on('exit', (code) => {
              if (!code) {
                this.spawnCommand('ruby', [ this.destinationPath('bin', 'rails'), 'db:setup' ]);
                this.spawnCommand('ruby', [ this.destinationPath('bin', 'rails'), 'log:clear', 'tmp:clear' ]);
              }
            });
        }
      });
  }
});
<|endoftext|>


<commit_before>/**
 * @file Upload
 * @author nemo <luoxincheng@baidu.com>
 */

import san from 'san';

const _method = {
	repeatSet: function(obj, fn) {
		for (let i in obj) {
			fn(i, obj[i])
		}
	},
	initXHR: function(file) {
		let self = this
		let xhr = new XMLHttpRequest()
    	let formData = new FormData()

    	let fileIndex = self.fileList.push(file) - 1
    	self.data.set('fileList', self.fileList)

		let opt = self.data.get('opt')
    	opt.data[opt.name] = file
    	opt['on-change'](file, self.fileList)
		_method.repeatSet(opt.data, formData.append.bind(formData))

		xhr.upload.onprogress = function(e) {
		    let percentage = 0;

		    if ( e.lengthComputable ) {
		        percentage = e.loaded / e.total
		    }
		    opt['on-progress'](e, file, self.fileList)
		    file.progressCss = {width: 100 * percentage + '%'}
		    self.data.set('fileList', self.fileList)

		};
		xhr.onreadystatechange = function() {
			if (xhr.readyState === 4) {
				if (/20/.test(xhr.status)) {
					self.fileList[fileIndex]['response'] = JSON.parse(xhr.response)
					opt['on-success'](JSON.parse(xhr.response), file, self.fileList)
					file.progressCss = {width: '100%'}
					self.data.set('fileList', self.fileList)
				} else {
					opt['on-error']('error', file, self.fileList)
				}
				opt['on-change'](file, self.fileList)
			}
		}

		xhr.withCredentials = opt['with-credentials']
	    self.xhrList.push({
	    	fn: function() {
		    	if (opt['before-upload'](file)) {
					xhr.open('POST', opt.action)
					_method.repeatSet(Object.assign({'Content-Type': 'application/x-www-form-urlencoded'}, opt.headers), xhr.setRequestHeader.bind(xhr))
		    		xhr.send(formData)
		    	}
		    },
		    xhr: xhr
	    })
	}
}

let initOpts = {
	action: '',
	headers: {},
	multiple: false,
	data: {},
	name: 'file',
	'with-credentials': false,
	'show-file-list': true,
	drag: false,
	accept: '',
	'on-preview': function() {},
	'on-remove': function() {},
	'on-success': function() {},
	'on-error': function() {},
	'on-progress': function() {},
	'on-change': function() {},
	'before-upload': function() { return true },
	'auto-upload': true,
	disabled: false
}

export default san.defineComponent({
    template: `

	        	上传
	        	选取文件
	        	<input san-if="!multiple && !disabled" type="file" on-change="reciveFile($event)" accept="{{accept}}"/>
	        	<input san-if="multiple && !disabled" type="file" on-change="reciveFile($event)" accept="{{accept}}" multiple/>

开始上传

	        		{{ file.name }}
	        		+

    `,
    components: {

    },
    initData() {
    	return {
    		fileList: []
    	}
    },
    computed: {
    	multiple() {
    		return this.data.get('opt')['multiple']
    	},
    	showFileList() {
    		return this.data.get('opt')['show-file-list']
    	},
    	autoUpload() {
    		return this.data.get('opt')['auto-upload']
    	},
    	accept() {
    		return this.data.get('opt')['accept']
    	},
    	drag() {
    		return this.data.get('opt')['drag']
    	},
    	disabled() {
    		return this.data.get('opt')['disabled']
    	},
    	fileList() {
    		return this.fileList
    	}
    },
    inited() {
		this.fileList = []
		this.xhrList = []
    	this.data.set('opt', Object.assign({}, initOpts, this.data.get('opt')))
    },
    fileListClick(index) {
    	this.data.get('opt')['on-preview'](this.fileList[index])
    },
    excuteUpload() {
    	this.xhrList.forEach(one => one.fn())
    },
    removeFile(index) {
    	this.xhrList[index].xhr.abort()
    	let file = this.fileList.splice(index, 1)
    	this.data.get('opt')['on-remove'](file, this.fileList)
    	this.data.set('fileList', this.fileList)
    },
    dragEnter(event) {
    	event.preventDefault();
    	event.stopPropagation();
    },
    dragOver(event) {
    	event.preventDefault();
    	event.stopPropagation();
    },
    dropFile(event) {
    	event.preventDefault();
    	event.stopPropagation();
    	!this.data.get('opt').disabled && Array.prototype.slice.call(event.dataTransfer.files).forEach(file => {
			_method.initXHR.call(this, file)
    	})
    	this.xhrList.forEach(one => one.fn())
    },
    reciveFile(event) {
    	Array.prototype.slice.call(event.target.files).forEach(file => {
			_method.initXHR.call(this, file)
    	})
    	console.log(this.data.get('opt'))
    	this.data.get('opt')['auto-upload'] && this.xhrList.forEach(one => one.fn())
    }
});

<commit_msg>clear input field which causes re-choose bug<commit_after>/**
 * @file Upload
 * @author nemo <luoxincheng@baidu.com>
 */

import san from 'san';

const _method = {
	repeatSet: function(obj, fn) {
		for (let i in obj) {
			fn(i, obj[i])
		}
	},
	initXHR: function(file) {
		let self = this
		let xhr = new XMLHttpRequest()
    	let formData = new FormData()

    	let fileIndex = self.fileList.push(file) - 1
    	self.data.set('fileList', self.fileList)

		let opt = self.data.get('opt')
    	opt.data[opt.name] = file
    	opt['on-change'](file, self.fileList)
		_method.repeatSet(opt.data, formData.append.bind(formData))

		xhr.upload.onprogress = function(e) {
		    let percentage = 0;

		    if ( e.lengthComputable ) {
		        percentage = e.loaded / e.total
		    }
		    opt['on-progress'](e, file, self.fileList)
		    file.progressCss = {width: 100 * percentage + '%'}
		    self.data.set('fileList', self.fileList)

		};
		xhr.onreadystatechange = function() {
			if (xhr.readyState === 4) {
				if (/20/.test(xhr.status)) {
					self.fileList[fileIndex]['response'] = JSON.parse(xhr.response)
					opt['on-success'](JSON.parse(xhr.response), file, self.fileList)
					file.progressCss = {width: '100%'}
					self.data.set('fileList', self.fileList)
				} else {
					opt['on-error']('error', file, self.fileList)
				}
				file.uploaded = true
				opt['on-change'](file, self.fileList)
			}
		}

		xhr.withCredentials = opt['with-credentials']
	    self.xhrList.push({
	    	fn: function() {
		    	if (opt['before-upload'](file) && !file.uploaded) {
					xhr.open('POST', opt.action)
					_method.repeatSet(Object.assign({'Content-Type': 'application/x-www-form-urlencoded'}, opt.headers), xhr.setRequestHeader.bind(xhr))
		    		xhr.send(formData)
		    	}
		    },
		    xhr: xhr
	    })
	}
}

let initOpts = {
	action: '',
	headers: {},
	multiple: false,
	data: {},
	name: 'file',
	'with-credentials': false,
	'show-file-list': true,
	drag: false,
	accept: '',
	'on-preview': function() {},
	'on-remove': function() {},
	'on-success': function() {},
	'on-error': function() {},
	'on-progress': function() {},
	'on-change': function() {},
	'before-upload': function() { return true },
	'auto-upload': true,
	disabled: false
}

export default san.defineComponent({
    template: `

	        	上传
	        	选取文件
	        	<input san-if="!multiple && !disabled" type="file" on-change="reciveFile($event)" accept="{{accept}}"/>
	        	<input san-if="multiple && !disabled" type="file" on-change="reciveFile($event)" accept="{{accept}}" multiple/>

开始上传

	        		{{ file.name }}
	        		+

    `,
    components: {

    },
    initData() {
    	return {
    		fileList: []
    	}
    },
    computed: {
    	multiple() {
    		return this.data.get('opt')['multiple']
    	},
    	showFileList() {
    		return this.data.get('opt')['show-file-list']
    	},
    	autoUpload() {
    		return this.data.get('opt')['auto-upload']
    	},
    	accept() {
    		return this.data.get('opt')['accept']
    	},
    	drag() {
    		return this.data.get('opt')['drag']
    	},
    	disabled() {
    		return this.data.get('opt')['disabled']
    	},
    	fileList() {
    		return this.fileList
    	}
    },
    inited() {
		this.fileList = []
		this.xhrList = []
    	this.data.set('opt', Object.assign({}, initOpts, this.data.get('opt')))
    },
    fileListClick(index) {
    	this.data.get('opt')['on-preview'](this.fileList[index])
    },
    excuteUpload() {
    	this.xhrList.forEach(one => one.fn())
    },
    removeFile(index) {
    	this.xhrList[index].xhr.abort()
    	let file = this.fileList.splice(index, 1)
    	this.data.get('opt')['on-remove'](file, this.fileList)
    	this.data.set('fileList', this.fileList)
    },
    dragEnter(event) {
    	event.preventDefault();
    	event.stopPropagation();
    },
    dragOver(event) {
    	event.preventDefault();
    	event.stopPropagation();
    },
    dropFile(event) {
    	event.preventDefault();
    	event.stopPropagation();
    	!this.data.get('opt').disabled && Array.prototype.slice.call(event.dataTransfer.files).forEach(file => {
			_method.initXHR.call(this, file)
    	})
    	this.xhrList.forEach(one => one.fn())
    },
    reciveFile(event) {
    	Array.prototype.slice.call(event.target.files).forEach(file => {
			_method.initXHR.call(this, file)
    	})
    	event.target.value = ''
    	this.data.get('opt')['auto-upload'] && this.xhrList.forEach(one => one.fn())
    }
});

<|endoftext|>