<commit_before>import React from 'react'
import { VictoryAxis, VictoryChart } from 'victory'
import { StaticVictoryLine } from 'components/StaticVictory'
import d3_time_format from 'd3-time-format'
import d3_scale from 'd3-scale'
import { MIN, MAX } from 'reducers/coOps'
import Moment from 'moment'

import './Chart.scss'

export default class Chart extends React.Component {
  static propTypes = {
    linePalette: React.PropTypes.array,
    data: React.PropTypes.array,
    years: React.PropTypes.array,
    hoverYear: React.PropTypes.number,
    selectedStationID: React.PropTypes.string.isRequired,
    width: React.PropTypes.number.isRequired,
    height: React.PropTypes.number.isRequired
  };

  static defaultProps = {
    linePalette: [ '#1CE6FF', '#FF34FF', '#FF4A46', '#008941', '#006FA6', '#A30059',
                   '#FFDBE5', '#7A4900', '#0000A6', '#63FFAC', '#B79762', '#004D43', '#8FB0FF', '#997D87',
                   '#5A0007', '#809693', '#FEFFE6', '#1B4400', '#4FC601', '#3B5DFF', '#4A3B53', '#FF2F80',
                   '#61615A', '#BA0900', '#6B7900', '#00C2A0', '#FFAA92', '#FF90C9', '#B903AA', '#D16100',
                   '#DDEFFF', '#000035', '#7B4F4B', '#A1C299', '#300018', '#0AA6D8', '#013349', '#00846F',
                   '#372101', '#FFB500', '#C2FFED', '#A079BF', '#CC0744', '#C0B9B2', '#C2FF99', '#001E09',
                   '#00489C', '#6F0062', '#0CBD66', '#EEC3FF', '#456D75', '#B77B68', '#7A87A1', '#788D66',
                   '#885578', '#FAD09F', '#FF8A9A', '#D157A0', '#BEC459', '#456648', '#0086ED', '#886F4C',
                   '#34362D', '#B4A8BD', '#00A6AA', '#452C2C', '#636375', '#A3C8C9', '#FF913F', '#938A81',
                   '#575329', '#00FECF', '#B05B6F', '#8CD0FF', '#3B9700', '#04F757', '#C8A1A1', '#1E6E00',
                   '#7900D7', '#A77500', '#6367A9', '#A05837', '#6B002C', '#772600', '#D790FF', '#9B9700',
                   '#549E79', '#FFF69F', '#201625', '#72418F', '#BC23FF', '#99ADC0', '#3A2465', '#922329',
                   '#5B4534', '#FDE8DC', '#404E55', '#0089A3', '#CB7E98', '#A4E804', '#324E72', '#6A3A4C' ]
  };

  constructor() {
    super()
    this.state = {
      chartData: [],
      availableYears: [],
      xTicks: this.makeXTicks(),
      yTicks: []
    }
  }

  makeXTicks() {
    var months = [];
    var start = Moment([2012])
    for (var i = 0; i < 12; i++) {
      months.push(start.clone().toDate())
      start.add(1, 'month')
    }
    return months;
  }

  componentWillReceiveProps(nextProps) {
    // Map to display data as new data is received.
    var chartData = nextProps.data.map(dataset => {
      var chartDataset = !this.state.chartData
          ? null
          : this.state.chartData.find(
            chartDataset =>
              chartDataset.year === dataset.year &&
              chartDataset.bound === dataset.bound
          );
      if (!chartDataset) {
        chartDataset = Object.assign({}, dataset)
        chartDataset.color = this.props.linePalette[dataset.year % this.props.linePalette.length]
      }
      chartDataset.visible = nextProps.years.indexOf(dataset.year) !== -1 || nextProps.hoverYear === dataset.year
      chartDataset.thin = !chartDataset.visible
      return chartDataset
    })

    var availableYears = this.state.availableYears
    nextProps.data.forEach(dataset => {
      if (availableYears.indexOf(dataset.year) === -1) {
        availableYears = availableYears
          .concat(dataset.year)
          .sort((a, b) => b - a)
      }
    });

    var yTicks = this.makeYTicks(nextProps.data)

    this.setState({
      chartData,
      availableYears,
      yTicks
    })
  }

  makeYTicks(data) {
    if (!data || data.length === 0) {
      return []
    }
    var min;
    var max;
    data.forEach(dataset => {
      if (dataset.bound === MIN && (!min || dataset.min < min)) {
        min = dataset.min
      }
      if (dataset.bound === MAX && (!max || dataset.max > max)) {
        max = dataset.max
      }
    })
    var ticks = [];
    for (var tick = Math.floor((min - 1) / 5) * 5; tick < max + 1; tick += 5) {
      ticks.push(tick)
    }
    ticks.push(tick)
    return ticks
  }

  render () {
    if (!this.state.chartData || this.state.chartData.length === 0) {
      return null
    }

    var width = this.props.width;
    if (width < 600) {
      width = 600;
    }

    var aspectRatio = this.props.height / this.props.width;
    aspectRatio = aspectRatio >= 1 ? 0.666 : (aspectRatio <= 0.5 ? 0.333 : 0.5)
    var height = width * aspectRatio

    return (
      <VictoryChart
        key={`${width} ${height}`}
        width={width}
        height={height}
        scale={{
          x: d3_scale.time(),
          y: d3_scale.linear()
        }}>
        {this.renderYAxis()}
        {this.renderXAxis()}
        {this.state.chartData.map(dataset => this.renderLine(dataset))}
      </VictoryChart>
    )
  }

  renderYAxis() {
    return (
      <VictoryAxis
        dependentAxis
        tickValues={this.state.yTicks}
        style={{
          grid: {strokeWidth: 1}
        }} />
    )
  }

  renderXAxis() {
    return (
      <VictoryAxis
        style={{
          grid: {strokeWidth: 1}
        }}
        tickValues={this.state.xTicks}
        tickFormat={d3_time_format.format('%B')}/>
    )
  }

  renderLine(dataset) {
    var instanceKey = this.props.selectedStationID +
        dataset.year +
        dataset.bound +
        this.state.yTicks[0] +
        this.state.yTicks[this.state.yTicks.length - 1]
    return (
       <StaticVictoryLine
         key={instanceKey}
         visible={dataset.visible}
         thin={dataset.thin}
         range={[this.state.yTicks[0], this.state.yTicks[this.state.yTicks.length - 1]]}
         interpolation='natural'
         label={dataset.visible && dataset.bound === MIN ? `${dataset.year}` : ''}
         data={dataset.data}
         style={{
           data: {
             stroke: dataset.visible ? dataset.color : 'lightGrey',
             'strokeWidth': dataset.visible ? 2 : dataset.thin ? 0.5 : 0
           },
           label: {color: dataset.color}
         }} />
    )
  }
}
<commit_msg>pass a domain to VictoryChart to avoid it recalculating for every render() pass<commit_after>import React from 'react'
import { VictoryAxis, VictoryChart } from 'victory'
import { StaticVictoryLine } from 'components/StaticVictory'
import d3_time_format from 'd3-time-format'
import d3_scale from 'd3-scale'
import { MIN, MAX } from 'reducers/coOps'
import Moment from 'moment'

import './Chart.scss'

export default class Chart extends React.Component {
  static propTypes = {
    linePalette: React.PropTypes.array,
    data: React.PropTypes.array,
    years: React.PropTypes.array,
    hoverYear: React.PropTypes.number,
    selectedStationID: React.PropTypes.string.isRequired,
    width: React.PropTypes.number.isRequired,
    height: React.PropTypes.number.isRequired
  };

  static defaultProps = {
    linePalette: [ '#1CE6FF', '#FF34FF', '#FF4A46', '#008941', '#006FA6', '#A30059',
                   '#FFDBE5', '#7A4900', '#0000A6', '#63FFAC', '#B79762', '#004D43', '#8FB0FF', '#997D87',
                   '#5A0007', '#809693', '#FEFFE6', '#1B4400', '#4FC601', '#3B5DFF', '#4A3B53', '#FF2F80',
                   '#61615A', '#BA0900', '#6B7900', '#00C2A0', '#FFAA92', '#FF90C9', '#B903AA', '#D16100',
                   '#DDEFFF', '#000035', '#7B4F4B', '#A1C299', '#300018', '#0AA6D8', '#013349', '#00846F',
                   '#372101', '#FFB500', '#C2FFED', '#A079BF', '#CC0744', '#C0B9B2', '#C2FF99', '#001E09',
                   '#00489C', '#6F0062', '#0CBD66', '#EEC3FF', '#456D75', '#B77B68', '#7A87A1', '#788D66',
                   '#885578', '#FAD09F', '#FF8A9A', '#D157A0', '#BEC459', '#456648', '#0086ED', '#886F4C',
                   '#34362D', '#B4A8BD', '#00A6AA', '#452C2C', '#636375', '#A3C8C9', '#FF913F', '#938A81',
                   '#575329', '#00FECF', '#B05B6F', '#8CD0FF', '#3B9700', '#04F757', '#C8A1A1', '#1E6E00',
                   '#7900D7', '#A77500', '#6367A9', '#A05837', '#6B002C', '#772600', '#D790FF', '#9B9700',
                   '#549E79', '#FFF69F', '#201625', '#72418F', '#BC23FF', '#99ADC0', '#3A2465', '#922329',
                   '#5B4534', '#FDE8DC', '#404E55', '#0089A3', '#CB7E98', '#A4E804', '#324E72', '#6A3A4C' ]
  };

  constructor() {
    super()
    this.state = {
      chartData: [],
      availableYears: [],
      xTicks: this.makeXTicks(),
      xDomain: this.makeXDomain(),
      yTicks: [],
      yDomain: []
    }
  }

  makeXTicks() {
    var months = [];
    var start = Moment([2012])
    for (var i = 0; i < 12; i++) {
      months.push(start.clone().toDate())
      start.add(1, 'month')
    }
    return months;
  }

  makeXDomain() {
    return [Moment([2012]), Moment([2012]).add(1, 'year')]
  }

  componentWillReceiveProps(nextProps) {
    // Map to display data as new data is received.
    var chartData = nextProps.data.map(dataset => {
      var chartDataset = !this.state.chartData
          ? null
          : this.state.chartData.find(
            chartDataset =>
              chartDataset.year === dataset.year &&
              chartDataset.bound === dataset.bound
          );
      if (!chartDataset) {
        chartDataset = Object.assign({}, dataset)
        chartDataset.color = this.props.linePalette[dataset.year % this.props.linePalette.length]
      }
      chartDataset.visible = nextProps.years.indexOf(dataset.year) !== -1 || nextProps.hoverYear === dataset.year
      chartDataset.thin = !chartDataset.visible
      return chartDataset
    })

    var availableYears = this.state.availableYears
    nextProps.data.forEach(dataset => {
      if (availableYears.indexOf(dataset.year) === -1) {
        availableYears = availableYears
          .concat(dataset.year)
          .sort((a, b) => b - a)
      }
    });

    var yTicks = this.makeYTicks(nextProps.data)
    var yDomain = this.makeYDomain(yTicks)

    this.setState({
      chartData,
      availableYears,
      yTicks,
      yDomain
    })
  }

  makeYTicks(data) {
    if (!data || data.length === 0) {
      return []
    }
    var min;
    var max;
    data.forEach(dataset => {
      if (dataset.bound === MIN && (!min || dataset.min < min)) {
        min = dataset.min
      }
      if (dataset.bound === MAX && (!max || dataset.max > max)) {
        max = dataset.max
      }
    })
    var ticks = [];
    for (var tick = Math.floor((min - 1) / 5) * 5; tick < max + 1; tick += 5) {
      ticks.push(tick)
    }
    ticks.push(tick)
    return ticks
  }

  makeYDomain(ticks) {
    return [ticks[0], ticks[ticks.length - 1]]
  }

  render () {
    if (!this.state.chartData || this.state.chartData.length === 0) {
      return null
    }

    var width = this.props.width;
    if (width < 600) {
      width = 600;
    }

    var aspectRatio = this.props.height / this.props.width;
    aspectRatio = aspectRatio >= 1 ? 0.666 : (aspectRatio <= 0.5 ? 0.333 : 0.5)
    var height = width * aspectRatio

    return (
      <VictoryChart
        key={`${width} ${height}`}
        width={width}
        height={height}
        scale={{
          x: d3_scale.time(),
          y: d3_scale.linear()
        }}
        domain={{
          x: this.state.xDomain,
          y: this.state.yDomain
        }}>
        {this.renderYAxis()}
        {this.renderXAxis()}
        {this.state.chartData.map(dataset => this.renderLine(dataset))}
      </VictoryChart>
    )
  }

  renderYAxis() {
    return (
      <VictoryAxis
        dependentAxis
        tickValues={this.state.yTicks}
        style={{
          grid: {strokeWidth: 1}
        }} />
    )
  }

  renderXAxis() {
    return (
      <VictoryAxis
        style={{
          grid: {strokeWidth: 1}
        }}
        tickValues={this.state.xTicks}
        tickFormat={d3_time_format.format('%B')}/>
    )
  }

  renderLine(dataset) {
    var instanceKey = this.props.selectedStationID +
        dataset.year +
        dataset.bound +
        this.state.yTicks[0] +
        this.state.yTicks[this.state.yTicks.length - 1]
    return (
       <StaticVictoryLine
         key={instanceKey}
         visible={dataset.visible}
         thin={dataset.thin}
         range={[this.state.yTicks[0], this.state.yTicks[this.state.yTicks.length - 1]]}
         interpolation='natural'
         label={dataset.visible && dataset.bound === MIN ? `${dataset.year}` : ''}
         data={dataset.data}
         style={{
           data: {
             stroke: dataset.visible ? dataset.color : 'lightGrey',
             'strokeWidth': dataset.visible ? 2 : dataset.thin ? 0.5 : 0
           },
           label: {color: dataset.color}
         }} />
    )
  }
}
<|endoftext|>


<commit_before>module.exports = {
  plugins: [
    require('autoprefixer'),
    require('postcss-custom-properties')
  ]
};
<commit_msg>ktl-657 fix: postcss configuration<commit_after>module.exports = {
  plugins: [
    'autoprefixer',
    'postcss-custom-properties'
  ]
}
<|endoftext|>


<commit_before>/* jshint node: true */
'use strict';

var path = require('path');

// default reload css extensions
var styleExtensions = ['css', 'scss', 'sass', 'less', 'styl'];
var reloadCssPattern = new RegExp('\.(' + styleExtensions.join('|') + ')$');

var noop = function(){};

module.exports = function StylesReloader(options){
  var options = options;
  var fsWatcher = options.watcher;
  var ui = options.ui;
  var _isRunning = false;
  var lsProxy = options.ssl ? require('https') : require('http');

  // build app style pattern
  var appStylePath = path.join(options.project.root, 'app', 'styles', '*');
  var appStylePattern = new RegExp('^' + appStylePath);
  var appStyleResource = options.project.pkg.name + '.css';

  // livereload hostname
  var liveReloadHostname = [
    (options.ssl ? 'https://' :'http://'),
    (options.liveReloadHost || options.host || "localhost"),
    ':',
    options.liveReloadPort
  ].join('');

  function shouldReload(filePath){
    return filePath.match(reloadCssPattern);
  };

  function getReloadResource(filePath){
    return filePath.match(appStylePattern) ? appStyleResource : 'vendor.css';
  };

  function fileDidChange(results){
    var filePath = results.filePath || '';

    // notify livereload server if needed
    if (shouldReload(filePath)){
      var reloadResource = getReloadResource(filePath);
      ui.writeLine('Reloading ' + reloadResource + ' only');
      lsProxy.get(liveReloadHostname + '/changed?files=' + reloadResource)
          .on('error', noop);
    }
  };

  function mergeReloadFilters(){
    options.project.liveReloadFilterPatterns.push(reloadCssPattern);
  };

  return {

    run: function(){
      if (!options.liveReload) {
        ui.writeLine('StylesReloader is disabled');
        return;
      }

      if (this.isRunning()){
        return;
      }

      ui.writeLine('StylesReloader watches ' + styleExtensions.join('|'));
      if (fsWatcher) {
        mergeReloadFilters();
        fsWatcher.on('change', fileDidChange.bind(this));
        _isRunning = !_isRunning;
      }
    },

    isRunning: function(){
      return _isRunning;
    }
  };
};
<commit_msg>Change to single quotes<commit_after>/* jshint node: true */
'use strict';

var path = require('path');

// default reload css extensions
var styleExtensions = ['css', 'scss', 'sass', 'less', 'styl'];
var reloadCssPattern = new RegExp('\.(' + styleExtensions.join('|') + ')$');

var noop = function(){};

module.exports = function StylesReloader(options){
  var options = options;
  var fsWatcher = options.watcher;
  var ui = options.ui;
  var _isRunning = false;
  var lsProxy = options.ssl ? require('https') : require('http');

  // build app style pattern
  var appStylePath = path.join(options.project.root, 'app', 'styles', '*');
  var appStylePattern = new RegExp('^' + appStylePath);
  var appStyleResource = options.project.pkg.name + '.css';

  // livereload hostname
  var liveReloadHostname = [
    (options.ssl ? 'https://' :'http://'),
    (options.liveReloadHost || options.host || 'localhost'),
    ':',
    options.liveReloadPort
  ].join('');

  function shouldReload(filePath){
    return filePath.match(reloadCssPattern);
  };

  function getReloadResource(filePath){
    return filePath.match(appStylePattern) ? appStyleResource : 'vendor.css';
  };

  function fileDidChange(results){
    var filePath = results.filePath || '';

    // notify livereload server if needed
    if (shouldReload(filePath)){
      var reloadResource = getReloadResource(filePath);
      ui.writeLine('Reloading ' + reloadResource + ' only');
      lsProxy.get(liveReloadHostname + '/changed?files=' + reloadResource)
          .on('error', noop);
    }
  };

  function mergeReloadFilters(){
    options.project.liveReloadFilterPatterns.push(reloadCssPattern);
  };

  return {

    run: function(){
      if (!options.liveReload) {
        ui.writeLine('StylesReloader is disabled');
        return;
      }

      if (this.isRunning()){
        return;
      }

      ui.writeLine('StylesReloader watches ' + styleExtensions.join('|'));
      if (fsWatcher) {
        mergeReloadFilters();
        fsWatcher.on('change', fileDidChange.bind(this));
        _isRunning = !_isRunning;
      }
    },

    isRunning: function(){
      return _isRunning;
    }
  };
};
<|endoftext|>


<commit_before>//////////////////////////////////////////////////////////
// Utility functions
function createXmlHttpRequest() {
	if (window.XMLHttpRequest) {
		return new XMLHttpRequest();
	} else if (window.ActiveXObject) {
		return new ActiveXObject("Microsoft.XMLHTTP");
	} else {
		throw "XMLHttpRequest not available";
	}
}
function parseQuery() {
	var parts = document.location.search.substr(1).split("&");
	var out = {};
	var tmp;
	for (var i in parts) {
		tmp = parts[i].split("=");
		out[tmp[0]] = tmp[1] ? tmp[1] : true;
	}
	return out;
}
function foreach(vs, f) {
	vs = toArray(vs);
	for (var i in vs) {
		f(vs[i]);
	}
}
function toArray(object) {
	if (!object) return [];
	else if (object.constructor == Array) return object;
	else return [object];
}
/**
 * Load a service into the current page.
 * If a service with the same name was loaded, it will be replaced.
 * If onLoad is provided, it names a function which will be called
 * when the service is ready.
 */
function loadService(name, url, onReady) {
	var old = document.getElementById(name);
	if (old != null) {
		old.parentNode.removeChild(old);
		delete old;
	}
	var head = document.getElementsByTagName("head")[0];
	var script = document.createElement('script');
	script.id = name;
	script.type = "text/javascript";
	script.src = url + "?js" + (onReady ? "&func="+encodeURIComponent(onReady) : "");
	head.appendChild(script);
}
function renderTimestamp(value) {
	document.getElementById("timestamp").innerHTML = value;
}
function renderPublication(p) {
	var pubs = document.getElementById("publications");
	pubs.innerHTML += '
' + publicationToHtml(p.value) + '
';
	renderTimestamp(p.timestamp);
	pubs.scrollTop = pubs.scrollHeight;
}
function renderTokenError(p) {
	var pubs = document.getElementById("publications");
	pubs.innerHTML += '
'
		+ p.message
		+ (p.location
			? ' at '
				+ p.location.filename
				+ ':' + p.location.line
				+ '(' + p.location.column + ')'
			: '')
		+ '
';
	renderTimestamp(p.timestamp);
	pubs.scrollTop = pubs.scrollHeight;
}
function renderPrintln(p) {
	var pubs = document.getElementById("publications");
	pubs.innerHTML += '
' + p.line + '
';
	renderTimestamp(p.timestamp);
	pubs.scrollTop = pubs.scrollHeight;
}
function escapeHtml(v) {
	v = v.replace(/&/g, '&');
	v = v.replace(/</g, '<');
	// FIXME: escape other special characters
	return v;
}
/**
 * Convert arbitrary JSON values to
 * pretty-printed HTML
 */
function jsonToHtml(v) {
	switch (typeof v) {
		case 'boolean':
		case 'number':
			return v+'';
		case 'string':
			return '"' + escapeHtml(v)
					.replace('"', '\\"')
					.replace('\\', '\\\\')
				+ '"';
		case 'object':
			if (v == null) return 'null';
			if (v.constructor == Array) {
				var tmp = [];
				for (var k in v) {
					tmp[k] = jsonToHtml(v[k]);
				}
				return '[' + tmp.join(', ') + ']';
			}
			if (v.constructor == Object) {
				var out = '{';
				for (var k in v) {
					out += jsonToHtml(k)+': ' + jsonToHtml(v[k]) + ', ';
				}
				return out.substring(0, out.length-2) + '}';
			}
			return '';
	}
}
/**
 * Convert publication values to HTML.
 * These have a bit more structure than arbitrary JSON.
 */
function publicationToHtml(v) {
	if (v == null) return null;
	switch (v["@xsi.type"]) {
		// XSD types
		case 'xs:string': return jsonToHtml(v.$);
		case 'xs:integer':
		case 'xs:long':
		case 'xs:short':
		case 'xs:int': return jsonToHtml(parseInt(v.$));
		case 'xs:double':
		case 'xs:decimal':
		case 'xs:float': return jsonToHtml(parseFloat(v.$));
		case 'xs:boolean': return jsonToHtml(v.$ == 'true');
		// OIL types
		// FIXME: server should use better namespace than ns2
		case 'ns2:constant': return publicationToHtml(v.value);
		case 'ns2:list':
			var tmp = [];
			foreach (v.element, function (e) {
				tmp[tmp.length] = publicationToHtml(e);
			});
			return '[' + tmp.join(', ') + ']';
		case 'ns2:tuple':
			var tmp = [];
			foreach (v.element, function (e) {
				tmp[tmp.length] = publicationToHtml(e);
			});
			return '(' + tmp.join(', ') + ')';
		default: return '' + jsonToHtml(v) + '';
	}
}
/**
 * Extract leading block comments from a program.
 */
function extractComments(text) {
	var comments = "";
	var m;
	// extract block comments
	m = text.match(/^\s*\{-\s*([^]*?)\s*-\}\s*/);
	if (m) return [m[1], text.substr(m[0].length)];
	// extract line comments
	m = text.match(/^\s*((--.*\n)+)\s*/);
	if (m) return [m[1].replace(/--\s*/g, ""), text.substr(m[0].length)];
	return ["", text];
}

//////////////////////////////////////////////////////////
// Main event handlers

/**
 * Flag used to indicate that publications should
 * be wiped after the current job completes.
 */
var suppressPublications = false;

/**
 * The currently-running job.
 * Anybody who calls finish on it should unset
 * this variable to indicate that the job is no
 * longer valid.
 */
var currentJob = null;
function onJobServiceReady(job) {
	currentJob = job;
	suppressPublications = false;
	currentJob.onError = onError;
	/**
	 * Recursively listen for values
	 * until the job finishes.
	 */
	function onEvents(vs) {
		if (!vs) {
			if (currentJob) {
				// finish the job
				currentJob.finish({}, onJobFinish);
				currentJob = null;
			} else {
				// the job was already finished
				// (i.e. by onUnload)
			}
			return;
		}
		if (suppressPublications) return;
		foreach(vs, function (v) {
			switch (v["@xsi.type"]) {
			case "ns2:tokenErrorEvent":
				renderTokenError(v);
				break;
			case "ns2:publicationEvent":
				renderPublication(v);
				break;
			case "ns2:printlnEvent":
				renderPrintln(v);
				break;
			}
		});
		currentJob.listen({}, onEvents);
	}
	// Start the job and then listen for published values.
	currentJob.start({}, function () {
		document.getElementById("stopButton").disabled = false;
		document.getElementById("stopButton").style.visibility = "";
		currentJob.listen({}, onEvents);
	});
}
function onJobFinish() {
	document.getElementById("runButton").disabled = false;
	document.getElementById("stopButton").disabled = true;
	document.getElementById("stopButton").style.visibility = "hidden";
	document.getElementById("loading").style.visibility = "hidden";
	document.getElementById("timestamp").style.visibility = "hidden";
	currentJob = null;
}
function onRunButton() {
	document.getElementById("runButton").disabled = true;
	document.getElementById("publications").innerHTML = "";
	document.getElementById("loading").style.visibility = "";
	document.getElementById("timestamp").style.visibility = "";
	executorService.compileAndSubmit({program: program.getCode()}, function (url) {
		loadService("jobService", url, "onJobServiceReady");
	});
}
function onStopButton() {
	document.getElementById("stopButton").disabled = true;
	currentJob.halt({});
	// finish will be called by
	// the publish listener
}
function onUnload() {
	if (currentJob) currentJob.finish({});
}
var executorService = null;
function onExecutorServiceReady(service) {
	executorService = service;
	executorService.onError = onError;
	document.getElementById("runButton").disabled = false;
}
function onError(response, code, exception) {
	document.getElementById("stopButton").disabled = true;
	document.getElementById("stopButton").style.visibility = "hidden";
	document.getElementById("loading").style.visibility = "hidden";
	document.getElementById("timestamp").style.visibility = "hidden";
	// stop the current job, if possible
	var job = currentJob;
	onJobFinish();
	if (job) {
		// prevent this from running again
		// even if finish has an error
		currentJob = null;
		job.finish({});
	}
	// unwrap response if possible
	if (response) {
		if (response.faultstring) {
			response = response.faultstring;
		}
	} else {
		response = exception;
	}
	var pubs = document.getElementById("publications");
	pubs.innerHTML += '
Service error: ' + jsonToHtml(response) + '
';
	pubs.scrollTop = pubs.scrollHeight;
	document.getElementById("runButton").disabled = false;
}
function loadCode(filename) {
	if (currentJob) {
		suppressPublications = true;
		currentJob.halt({});
	}
	document.getElementById("publications").innerHTML = "";
	try {
		var xhr = createXmlHttpRequest();
		xhr.onreadystatechange = function () {
			if (xhr.readyState == 4) {
				var parts = extractComments(xhr.responseText);
				document.getElementById("comments").innerHTML =
					escapeHtml(parts[0]).replace("\n", "
");
				program.setCode(parts[1]);
				program.editor.syntaxHighlight('init');
			}
		};
		xhr.open("GET", filename, true);
		xhr.send("");
	} catch (e) {
		alert("Unable to load " + filename + " due to exception: " + e);
	}
}

//////////////////////////////////////////////////////////
// Configuration
CodePress.languages = { "orc-demo": "Orc-Demo", "orc": "Orc" };
var query = parseQuery();
var executorServiceUrl = query.mock
	? "mock-executor.js"
	: "json/executor";

//////////////////////////////////////////////////////////
// Go go gadget executor

loadService("executorService", executorServiceUrl, "onExecutorServiceReady");
<commit_msg>Flag to enable 'safe' demo mode which doesn't use IM<commit_after>//////////////////////////////////////////////////////////
// Utility functions
function createXmlHttpRequest() {
	if (window.XMLHttpRequest) {
		return new XMLHttpRequest();
	} else if (window.ActiveXObject) {
		return new ActiveXObject("Microsoft.XMLHTTP");
	} else {
		throw "XMLHttpRequest not available";
	}
}
function parseQuery() {
	var parts = document.location.search.substr(1).split("&");
	var out = {};
	var tmp;
	for (var i in parts) {
		tmp = parts[i].split("=");
		out[tmp[0]] = tmp[1] ? tmp[1] : true;
	}
	return out;
}
function foreach(vs, f) {
	vs = toArray(vs);
	for (var i in vs) {
		f(vs[i]);
	}
}
function toArray(object) {
	if (!object) return [];
	else if (object.constructor == Array) return object;
	else return [object];
}
/**
 * Load a service into the current page.
 * If a service with the same name was loaded, it will be replaced.
 * If onLoad is provided, it names a function which will be called
 * when the service is ready.
 */
function loadService(name, url, onReady) {
	var old = document.getElementById(name);
	if (old != null) {
		old.parentNode.removeChild(old);
		delete old;
	}
	var head = document.getElementsByTagName("head")[0];
	var script = document.createElement('script');
	script.id = name;
	script.type = "text/javascript";
	script.src = url + "?js" + (onReady ? "&func="+encodeURIComponent(onReady) : "");
	head.appendChild(script);
}
function renderTimestamp(value) {
	document.getElementById("timestamp").innerHTML = value;
}
function renderPublication(p) {
	var pubs = document.getElementById("publications");
	pubs.innerHTML += '
' + publicationToHtml(p.value) + '
';
	renderTimestamp(p.timestamp);
	pubs.scrollTop = pubs.scrollHeight;
}
function renderTokenError(p) {
	var pubs = document.getElementById("publications");
	pubs.innerHTML += '
'
		+ p.message
		+ (p.location
			? ' at '
				+ p.location.filename
				+ ':' + p.location.line
				+ '(' + p.location.column + ')'
			: '')
		+ '
';
	renderTimestamp(p.timestamp);
	pubs.scrollTop = pubs.scrollHeight;
}
function renderPrintln(p) {
	var pubs = document.getElementById("publications");
	pubs.innerHTML += '
' + p.line + '
';
	renderTimestamp(p.timestamp);
	pubs.scrollTop = pubs.scrollHeight;
}
function escapeHtml(v) {
	v = v.replace(/&/g, '&');
	v = v.replace(/</g, '<');
	// FIXME: escape other special characters
	return v;
}
/**
 * Convert arbitrary JSON values to
 * pretty-printed HTML
 */
function jsonToHtml(v) {
	switch (typeof v) {
		case 'boolean':
		case 'number':
			return v+'';
		case 'string':
			return '"' + escapeHtml(v)
					.replace('"', '\\"')
					.replace('\\', '\\\\')
				+ '"';
		case 'object':
			if (v == null) return 'null';
			if (v.constructor == Array) {
				var tmp = [];
				for (var k in v) {
					tmp[k] = jsonToHtml(v[k]);
				}
				return '[' + tmp.join(', ') + ']';
			}
			if (v.constructor == Object) {
				var out = '{';
				for (var k in v) {
					out += jsonToHtml(k)+': ' + jsonToHtml(v[k]) + ', ';
				}
				return out.substring(0, out.length-2) + '}';
			}
			return '';
	}
}
/**
 * Convert publication values to HTML.
 * These have a bit more structure than arbitrary JSON.
 */
function publicationToHtml(v) {
	if (v == null) return null;
	switch (v["@xsi.type"]) {
		// XSD types
		case 'xs:string': return jsonToHtml(v.$);
		case 'xs:integer':
		case 'xs:long':
		case 'xs:short':
		case 'xs:int': return jsonToHtml(parseInt(v.$));
		case 'xs:double':
		case 'xs:decimal':
		case 'xs:float': return jsonToHtml(parseFloat(v.$));
		case 'xs:boolean': return jsonToHtml(v.$ == 'true');
		// OIL types
		// FIXME: server should use better namespace than ns2
		case 'ns2:constant': return publicationToHtml(v.value);
		case 'ns2:list':
			var tmp = [];
			foreach (v.element, function (e) {
				tmp[tmp.length] = publicationToHtml(e);
			});
			return '[' + tmp.join(', ') + ']';
		case 'ns2:tuple':
			var tmp = [];
			foreach (v.element, function (e) {
				tmp[tmp.length] = publicationToHtml(e);
			});
			return '(' + tmp.join(', ') + ')';
		default: return '' + jsonToHtml(v) + '';
	}
}
/**
 * Extract leading block comments from a program.
 */
function extractComments(text) {
	var comments = "";
	var m;
	// extract block comments
	m = text.match(/^\s*\{-\s*([^]*?)\s*-\}\s*/);
	if (m) return [m[1], text.substr(m[0].length)];
	// extract line comments
	m = text.match(/^\s*((--.*\n)+)\s*/);
	if (m) return [m[1].replace(/--\s*/g, ""), text.substr(m[0].length)];
	return ["", text];
}

//////////////////////////////////////////////////////////
// Main event handlers

/**
 * Flag used to indicate that publications should
 * be wiped after the current job completes.
 */
var suppressPublications = false;

/**
 * The currently-running job.
 * Anybody who calls finish on it should unset
 * this variable to indicate that the job is no
 * longer valid.
 */
var currentJob = null;
function onJobServiceReady(job) {
	currentJob = job;
	suppressPublications = false;
	currentJob.onError = onError;
	/**
	 * Recursively listen for values
	 * until the job finishes.
	 */
	function onEvents(vs) {
		if (!vs) {
			if (currentJob) {
				// finish the job
				currentJob.finish({}, onJobFinish);
				currentJob = null;
			} else {
				// the job was already finished
				// (i.e. by onUnload)
			}
			return;
		}
		if (suppressPublications) return;
		foreach(vs, function (v) {
			switch (v["@xsi.type"]) {
			case "ns2:tokenErrorEvent":
				renderTokenError(v);
				break;
			case "ns2:publicationEvent":
				renderPublication(v);
				break;
			case "ns2:printlnEvent":
				renderPrintln(v);
				break;
			}
		});
		currentJob.listen({}, onEvents);
	}
	// Start the job and then listen for published values.
	currentJob.start({}, function () {
		document.getElementById("stopButton").disabled = false;
		document.getElementById("stopButton").style.visibility = "";
		currentJob.listen({}, onEvents);
	});
}
function onJobFinish() {
	document.getElementById("runButton").disabled = false;
	document.getElementById("stopButton").disabled = true;
	document.getElementById("stopButton").style.visibility = "hidden";
	document.getElementById("loading").style.visibility = "hidden";
	document.getElementById("timestamp").style.visibility = "hidden";
	currentJob = null;
}
function onRunButton() {
	document.getElementById("runButton").disabled = true;
	document.getElementById("publications").innerHTML = "";
	document.getElementById("loading").style.visibility = "";
	document.getElementById("timestamp").style.visibility = "";
	var code = program.getCode();
	if (query.safe) {
		code = 'include "prelude/demo_safe.inc"\n' + code;
	}
	executorService.compileAndSubmit({program: code}, function (url) {
		loadService("jobService", url, "onJobServiceReady");
	});
}
function onStopButton() {
	document.getElementById("stopButton").disabled = true;
	currentJob.halt({});
	// finish will be called by
	// the publish listener
}
function onUnload() {
	if (currentJob) currentJob.finish({});
}
var executorService = null;
function onExecutorServiceReady(service) {
	executorService = service;
	executorService.onError = onError;
	document.getElementById("runButton").disabled = false;
}
function onError(response, code, exception) {
	document.getElementById("stopButton").disabled = true;
	document.getElementById("stopButton").style.visibility = "hidden";
	document.getElementById("loading").style.visibility = "hidden";
	document.getElementById("timestamp").style.visibility = "hidden";
	// stop the current job, if possible
	var job = currentJob;
	onJobFinish();
	if (job) {
		// prevent this from running again
		// even if finish has an error
		currentJob = null;
		job.finish({});
	}
	// unwrap response if possible
	if (response) {
		if (response.faultstring) {
			response = response.faultstring;
		}
	} else {
		response = exception;
	}
	var pubs = document.getElementById("publications");
	pubs.innerHTML += '
Service error: ' + jsonToHtml(response) + '
';
	pubs.scrollTop = pubs.scrollHeight;
	document.getElementById("runButton").disabled = false;
}
function loadCode(filename) {
	if (currentJob) {
		suppressPublications = true;
		currentJob.halt({});
	}
	document.getElementById("publications").innerHTML = "";
	try {
		var xhr = createXmlHttpRequest();
		xhr.onreadystatechange = function () {
			if (xhr.readyState == 4) {
				var parts = extractComments(xhr.responseText);
				document.getElementById("comments").innerHTML =
					escapeHtml(parts[0]).replace("\n", "
");
				program.setCode(parts[1]);
				program.editor.syntaxHighlight('init');
			}
		};
		xhr.open("GET", filename, true);
		xhr.send("");
	} catch (e) {
		alert("Unable to load " + filename + " due to exception: " + e);
	}
}

//////////////////////////////////////////////////////////
// Configuration
CodePress.languages = { "orc-demo": "Orc-Demo", "orc": "Orc" };
var query = parseQuery();
var executorServiceUrl = query.mock
	? "mock-executor.js"
	: "json/executor";

//////////////////////////////////////////////////////////
// Go go gadget executor

loadService("executorService", executorServiceUrl, "onExecutorServiceReady");
<|endoftext|>


<commit_before>var methods = {};

methods._validateOne = function(fieldName, stopOnFirstError) {
  var doc = this;
  var Class = doc.constructor;

  try {
    // Get definition of the field.
    var field = Class.getField(fieldName);
    // Get value of the field.
    var fieldValue = doc._getOne(fieldName);

    // If value of the field is not required and it's null, then we passed
    // validation.
    if (_.isNull(fieldValue) && !field.required) {
      return true;
    }

    // Get a validator for the given field name and run validation if it exists.
    var fieldValidator = Class.getValidator(fieldName);
    if (fieldValidator) {
      fieldValidator.validate(
        doc,
        fieldName,
        fieldValue
      );
    }

    // Check whether the field is nested.
    if (field instanceof Astro.NestedField) {
      // Check whether the field has the class.
      if (field.getClass()) {
        // Depending on the nested field type execute validation on the nested
        // values.
        if (field instanceof Astro.fields.object) {
          return fieldValue.validate(stopOnFirstError);
        } else if (field instanceof Astro.fields.array) {
          return _.every(fieldValue, function(nestedDoc) {
            return nestedDoc.validate(stopOnFirstError);
          });
        }
      }
    }

    // Remove a validator error message when no error occured.
    doc._errors.delete(fieldName);

    return true;
  } catch (e) {
    // If the error is not an instance of the Astro.ValidationError then throw
    // error again.
    if (!(e instanceof Astro.ValidationError)) {
      throw e;
    }

    // Generate an error message from the validator that didn't pass.
    var errorMessage = e.fieldValidator.generateErrorMessage(
      doc, fieldName, fieldValue
    );

    // Set validation error for the field.
    doc._errors.set(fieldName, errorMessage);

    return false;
  }
};

methods._validateMany = function(fieldsNames, stopOnFirstError) {
  var doc = this;
  var Class = doc.constructor;

  // Get the validation order.
  var validationOrder = Class.getValidationOrder();
  fieldsNames = _.intersection(validationOrder, fieldsNames);

  // Run validation for each field. If the "stopOnFirstError" flag is set, then
  // we stop validating after the first error. Otherwise, we continue until we
  // reach the last validator.
  if (stopOnFirstError) {
    return _.every(fieldsNames, function(fieldName) {
      return doc._validateOne(fieldName, stopOnFirstError);
    });
  } else {
    var valid = true;
    _.each(fieldsNames, function(fieldName) {
      if (!doc._validateOne(fieldName, stopOnFirstError)) {
        valid = false;
      }
    });
    return valid;
  }
};

// Public.

methods.validate = function(fieldsNames, stopOnFirstError) {
  var doc = this;
  var Class = doc.constructor;

  if (arguments.length === 0) {

    // Get list of all fields
    fieldsNames = Class.getFieldsNames();

  } else if (arguments.length === 1) {

    if (_.isString(fieldsNames)) {
      fieldsNames = [fieldsNames];
    } else if (_.isBoolean(fieldsNames)) {
      // Rewrite value of the "fieldsNames" argument into the
      // "stopOnFirstError" argument.
      stopOnFirstError = fieldsNames;
      // Get list of all validators.
      fieldsNames = Class.getFieldsNames();
    }

  } else if (arguments.length === 2) {

    if (_.isString(fieldsNames)) {
      fieldsNames = [fieldsNames];
    }

  }

  // Set default value of the "stopOnFirstError" argument.
  if (_.isUndefined(stopOnFirstError)) {
    stopOnFirstError = true;
  }

  return doc._validateMany(fieldsNames, stopOnFirstError);
};

methods.validateAll = function() {
  var doc = this;

  Astro.errors.warn('validators.validate_all_deprecated');

  return doc.validate(false);
},

_.extend(Astro.BaseClass.prototype, methods);
<commit_msg>Get a class from the field directly<commit_after>var methods = {};

methods._validateOne = function(fieldName, stopOnFirstError) {
  var doc = this;
  var Class = doc.constructor;

  try {
    // Get definition of the field.
    var field = Class.getField(fieldName);
    // Get value of the field.
    var fieldValue = doc._getOne(fieldName);

    // If value of the field is not required and it's null, then we passed
    // validation.
    if (_.isNull(fieldValue) && !field.required) {
      return true;
    }

    // Get a validator for the given field name and run validation if it exists.
    var fieldValidator = Class.getValidator(fieldName);
    if (fieldValidator) {
      fieldValidator.validate(
        doc,
        fieldName,
        fieldValue
      );
    }

    // Check whether the field is nested.
    if (field instanceof Astro.NestedField) {
      // Check whether the field has the class.
      if (field.class) {
        // Depending on the nested field type execute validation on the nested
        // values.
        if (field instanceof Astro.fields.object) {
          return fieldValue.validate(stopOnFirstError);
        } else if (field instanceof Astro.fields.array) {
          return _.every(fieldValue, function(nestedDoc) {
            return nestedDoc.validate(stopOnFirstError);
          });
        }
      }
    }

    // Remove a validator error message when no error occured.
    doc._errors.delete(fieldName);

    return true;
  } catch (e) {
    // If the error is not an instance of the Astro.ValidationError then throw
    // error again.
    if (!(e instanceof Astro.ValidationError)) {
      throw e;
    }

    // Generate an error message from the validator that didn't pass.
    var errorMessage = e.fieldValidator.generateErrorMessage(
      doc, fieldName, fieldValue
    );

    // Set validation error for the field.
    doc._errors.set(fieldName, errorMessage);

    return false;
  }
};

methods._validateMany = function(fieldsNames, stopOnFirstError) {
  var doc = this;
  var Class = doc.constructor;

  // Get the validation order.
  var validationOrder = Class.getValidationOrder();
  fieldsNames = _.intersection(validationOrder, fieldsNames);

  // Run validation for each field. If the "stopOnFirstError" flag is set, then
  // we stop validating after the first error. Otherwise, we continue until we
  // reach the last validator.
  if (stopOnFirstError) {
    return _.every(fieldsNames, function(fieldName) {
      return doc._validateOne(fieldName, stopOnFirstError);
    });
  } else {
    var valid = true;
    _.each(fieldsNames, function(fieldName) {
      if (!doc._validateOne(fieldName, stopOnFirstError)) {
        valid = false;
      }
    });
    return valid;
  }
};

// Public.

methods.validate = function(fieldsNames, stopOnFirstError) {
  var doc = this;
  var Class = doc.constructor;

  if (arguments.length === 0) {

    // Get list of all fields
    fieldsNames = Class.getFieldsNames();

  } else if (arguments.length === 1) {

    if (_.isString(fieldsNames)) {
      fieldsNames = [fieldsNames];
    } else if (_.isBoolean(fieldsNames)) {
      // Rewrite value of the "fieldsNames" argument into the
      // "stopOnFirstError" argument.
      stopOnFirstError = fieldsNames;
      // Get list of all validators.
      fieldsNames = Class.getFieldsNames();
    }

  } else if (arguments.length === 2) {

    if (_.isString(fieldsNames)) {
      fieldsNames = [fieldsNames];
    }

  }

  // Set default value of the "stopOnFirstError" argument.
  if (_.isUndefined(stopOnFirstError)) {
    stopOnFirstError = true;
  }

  return doc._validateMany(fieldsNames, stopOnFirstError);
};

methods.validateAll = function() {
  var doc = this;

  Astro.errors.warn('validators.validate_all_deprecated');

  return doc.validate(false);
},

_.extend(Astro.BaseClass.prototype, methods);
<|endoftext|>


<commit_before>import React, {Component} from 'react';
import PropTypes from 'prop-types';
import ReactDOM from 'react-dom';
import EventListener from 'react-event-listener';
import keycode from 'keycode';
import autoPrefix from '../utils/autoPrefix';
import transitions from '../styles/transitions';
import Overlay from '../internal/Overlay';
import Paper from '../Paper';
import propTypes from '../utils/propTypes';

let openNavEventHandler = null;

class Drawer extends Component {
  static propTypes = {
    /**
     * The contents of the `Drawer`
     */
    children: PropTypes.node,
    /**
     * The CSS class name of the root element.
     */
    className: PropTypes.string,
    /**
     * The CSS class name of the container element.
     */
    containerClassName: PropTypes.string,
    /**
     * Override the inline-styles of the container element.
     */
    containerStyle: PropTypes.object,
    /**
     * If true, swiping sideways when the `Drawer` is closed will not open it.
     */
    disableSwipeToOpen: PropTypes.bool,
    /**
     * If true, the `Drawer` will be docked. In this state, the overlay won't show and
     * clicking on a menu item will not close the `Drawer`.
     */
    docked: PropTypes.bool,
    /**
     * Callback function fired when the `open` state of the `Drawer` is requested to be changed.
     *
     * @param {boolean} open If true, the `Drawer` was requested to be opened.
     * @param {string} reason The reason for the open or close request. Possible values are
     * 'swipe' for open requests; 'clickaway' (on overlay clicks),
     * 'escape' (on escape key press), and 'swipe' for close requests.
     */
    onRequestChange: PropTypes.func,
    /**
     * If true, the `Drawer` is opened.  Providing a value will turn the `Drawer`
     * into a controlled component.
     */
    open: PropTypes.bool,
    /**
     * If true, the `Drawer` is positioned to open from the opposite side.
     */
    openSecondary: PropTypes.bool,
    /**
     * The CSS class name to add to the `Overlay` component that is rendered behind the `Drawer`.
     */
    overlayClassName: PropTypes.string,
    /**
     * Override the inline-styles of the `Overlay` component that is rendered behind the `Drawer`.
     */
    overlayStyle: PropTypes.object,
    /**
     * Override the inline-styles of the root element.
     */
    style: PropTypes.object,
    /**
     * The width of the left most (or right most) area in pixels where the `Drawer` can be
     * swiped open from. Setting this to `null` spans that area to the entire page
     * (**CAUTION!** Setting this property to `null` might cause issues with sliders and
     * swipeable `Tabs`: use at your own risk).
     */
    swipeAreaWidth: PropTypes.number,
    /**
     * The width of the `Drawer` in pixels or percentage in string format ex. `50%` to fill
     * half of the window or `100%` and so on. Defaults to using the values from theme.
     */
    width: PropTypes.oneOfType([
      PropTypes.string,
      PropTypes.number,
    ]),
    /**
     * The zDepth of the `Drawer`.
     */
    zDepth: propTypes.zDepth,

  };

  static defaultProps = {
    disableSwipeToOpen: false,
    docked: true,
    open: null,
    openSecondary: false,
    swipeAreaWidth: 30,
    width: null,
    zDepth: 2,
  };

  static contextTypes = {
    muiTheme: PropTypes.object.isRequired,
  };

  componentWillMount() {
    this.maybeSwiping = false;
    this.touchStartX = null;
    this.touchStartY = null;
    this.swipeStartX = null;

    this.setState({
      open: (this.props.open !== null ) ? this.props.open : this.props.docked,
      swiping: null,
    });
  }

  componentDidMount() {
    this.enableSwipeHandling();
  }

  componentWillReceiveProps(nextProps) {
    // If controlled then the open prop takes precedence.
    if (nextProps.open !== null) {
      this.setState({
        open: nextProps.open,
      });
      // Otherwise, if docked is changed, change the open state for when uncontrolled.
    } else if (this.props.docked !== nextProps.docked) {
      this.setState({
        open: nextProps.docked,
      });
    }
  }

  componentDidUpdate() {
    this.enableSwipeHandling();
  }

  componentWillUnmount() {
    this.disableSwipeHandling();
  }

  getStyles() {
    const muiTheme = this.context.muiTheme;
    const theme = muiTheme.drawer;

    const x = this.getTranslateMultiplier() * (this.state.open ? 0 : this.getMaxTranslateX());

    const styles = {
      root: {
        height: '100%',
        width: this.getTranslatedWidth() || theme.width,
        position: 'fixed',
        zIndex: muiTheme.zIndex.drawer,
        left: 0,
        top: 0,
        transform: `translate(${x}px, 0)`,
        transition: !this.state.swiping && transitions.easeOut(null, 'transform', null),
        backgroundColor: theme.color,
        overflow: 'auto',
        WebkitOverflowScrolling: 'touch', // iOS momentum scrolling
      },
      overlay: {
        zIndex: muiTheme.zIndex.drawerOverlay,
        pointerEvents: this.state.open ? 'auto' : 'none', // Bypass mouse events when left nav is closing.
      },
      rootWhenOpenRight: {
        left: 'auto',
        right: 0,
      },
    };

    return styles;
  }

  shouldShow() {
    return this.state.open || !!this.state.swiping;  // component is swiping
  }

  close(reason) {
    if (this.props.open === null) this.setState({open: false});
    if (this.props.onRequestChange) this.props.onRequestChange(false, reason);
    return this;
  }

  open(reason) {
    if (this.props.open === null) this.setState({open: true});
    if (this.props.onRequestChange) this.props.onRequestChange(true, reason);
    return this;
  }

  handleTouchTapOverlay = (event) => {
    event.preventDefault();
    this.close('clickaway');
  };

  handleKeyUp = (event) => {
    if (this.state.open && !this.props.docked && keycode(event) === 'esc') {
      this.close('escape');
    }
  };

  getTranslatedWidth() {
    if (typeof this.props.width === 'string') {
      if (!/^\d+(\.\d+)?%$/.test(this.props.width)) {
        throw new Error('Not a valid percentage format.');
      }
      const width = parseFloat(this.props.width) / 100.0;
      // We are doing our best on the Server to render a consistent UI, hence the
      // default value of 10000
      return window ? width * window.innerWidth : 10000;
    } else {
      return this.props.width;
    }
  }

  getMaxTranslateX() {
    const width = this.getTranslatedWidth() || this.context.muiTheme.drawer.width;
    return width + 10;
  }

  getTranslateMultiplier() {
    return this.props.openSecondary ? 1 : -1;
  }

  enableSwipeHandling() {
    if (!this.props.docked) {
      document.body.addEventListener('touchstart', this.onBodyTouchStart);
      if (!openNavEventHandler) {
        openNavEventHandler = this.onBodyTouchStart;
      }
    } else {
      this.disableSwipeHandling();
    }
  }

  disableSwipeHandling() {
    document.body.removeEventListener('touchstart', this.onBodyTouchStart);
    if (openNavEventHandler === this.onBodyTouchStart) {
      openNavEventHandler = null;
    }
  }

  onBodyTouchStart = (event) => {
    const swipeAreaWidth = this.props.swipeAreaWidth;

    const touchStartX = event.touches[0].pageX;
    const touchStartY = event.touches[0].pageY;

    // Open only if swiping from far left (or right) while closed
    if (swipeAreaWidth !== null && !this.state.open) {
      if (this.props.openSecondary) {
        // If openSecondary is true calculate from the far right
        if (touchStartX < document.body.offsetWidth - swipeAreaWidth) return;
      } else {
        // If openSecondary is false calculate from the far left
        if (touchStartX > swipeAreaWidth) return;
      }
    }

    if (!this.state.open &&
         (openNavEventHandler !== this.onBodyTouchStart ||
          this.props.disableSwipeToOpen)
       ) {
      return;
    }

    this.maybeSwiping = true;
    this.touchStartX = touchStartX;
    this.touchStartY = touchStartY;

    document.body.addEventListener('touchmove', this.onBodyTouchMove);
    document.body.addEventListener('touchend', this.onBodyTouchEnd);
    document.body.addEventListener('touchcancel', this.onBodyTouchEnd);
  };

  setPosition(translateX) {
    const drawer = ReactDOM.findDOMNode(this.refs.clickAwayableElement);
    const transformCSS = `translate(${(this.getTranslateMultiplier() * translateX)}px, 0)`;
    this.refs.overlay.setOpacity(1 - translateX / this.getMaxTranslateX());
    autoPrefix.set(drawer.style, 'transform', transformCSS);
  }

  getTranslateX(currentX) {
    return Math.min(
             Math.max(
               this.state.swiping === 'closing' ?
                 this.getTranslateMultiplier() * (currentX - this.swipeStartX) :
                 this.getMaxTranslateX() - this.getTranslateMultiplier() * (this.swipeStartX - currentX),
               0
             ),
             this.getMaxTranslateX()
           );
  }

  onBodyTouchMove = (event) => {
    const currentX = event.touches[0].pageX;
    const currentY = event.touches[0].pageY;

    if (this.state.swiping) {
      event.preventDefault();
      this.setPosition(this.getTranslateX(currentX));
    } else if (this.maybeSwiping) {
      const dXAbs = Math.abs(currentX - this.touchStartX);
      const dYAbs = Math.abs(currentY - this.touchStartY);
      // If the user has moved his thumb ten pixels in either direction,
      // we can safely make an assumption about whether he was intending
      // to swipe or scroll.
      const threshold = 10;

      if (dXAbs > threshold && dYAbs <= threshold) {
        this.swipeStartX = currentX;
        this.setState({
          swiping: this.state.open ? 'closing' : 'opening',
        });
        this.setPosition(this.getTranslateX(currentX));
      } else if (dXAbs <= threshold && dYAbs > threshold) {
        this.onBodyTouchEnd();
      }
    }
  };

  onBodyTouchEnd = (event) => {
    if (this.state.swiping) {
      const currentX = event.changedTouches[0].pageX;
      const translateRatio = this.getTranslateX(currentX) / this.getMaxTranslateX();

      this.maybeSwiping = false;
      const swiping = this.state.swiping;
      this.setState({
        swiping: null,
      });

      // We have to open or close after setting swiping to null,
      // because only then CSS transition is enabled.
      if (translateRatio > 0.5) {
        if (swiping === 'opening') {
          this.setPosition(this.getMaxTranslateX());
        } else {
          this.close('swipe');
        }
      } else {
        if (swiping === 'opening') {
          this.open('swipe');
        } else {
          this.setPosition(0);
        }
      }
    } else {
      this.maybeSwiping = false;
    }

    document.body.removeEventListener('touchmove', this.onBodyTouchMove);
    document.body.removeEventListener('touchend', this.onBodyTouchEnd);
    document.body.removeEventListener('touchcancel', this.onBodyTouchEnd);
  };

  render() {
    const {
      children,
      className,
      containerClassName,
      containerStyle,
      docked,
      openSecondary,
      overlayClassName,
      overlayStyle,
      style,
      zDepth,
    } = this.props;

    const styles = this.getStyles();

    let overlay;
    if (!docked) {
      overlay = (
        <Overlay
          ref="overlay"
          show={this.shouldShow()}
          className={overlayClassName}
          style={Object.assign(styles.overlay, overlayStyle)}
          transitionEnabled={!this.state.swiping}
          onTouchTap={this.handleTouchTapOverlay}
        />
      );
    }

    return (
      <div
        className={className}
        style={style}
      >
        <EventListener target="window" onKeyUp={this.handleKeyUp} />
        {overlay}
        <Paper
          ref="clickAwayableElement"
          zDepth={zDepth}
          rounded={false}
          transitionEnabled={!this.state.swiping}
          className={containerClassName}
          style={Object.assign(styles.root, openSecondary && styles.rootWhenOpenRight, containerStyle)}
        >
          {children}
        </Paper>
      </div>
    );
  }
}

export default Drawer;
<commit_msg>[Drawer] Fix behavior on RTL layouts (#6857)<commit_after>import React, {Component} from 'react';
import PropTypes from 'prop-types';
import ReactDOM from 'react-dom';
import EventListener from 'react-event-listener';
import keycode from 'keycode';
import autoPrefix from '../utils/autoPrefix';
import transitions from '../styles/transitions';
import Overlay from '../internal/Overlay';
import Paper from '../Paper';
import propTypes from '../utils/propTypes';

let openNavEventHandler = null;

class Drawer extends Component {
  static propTypes = {
    /**
     * The contents of the `Drawer`
     */
    children: PropTypes.node,
    /**
     * The CSS class name of the root element.
     */
    className: PropTypes.string,
    /**
     * The CSS class name of the container element.
     */
    containerClassName: PropTypes.string,
    /**
     * Override the inline-styles of the container element.
     */
    containerStyle: PropTypes.object,
    /**
     * If true, swiping sideways when the `Drawer` is closed will not open it.
     */
    disableSwipeToOpen: PropTypes.bool,
    /**
     * If true, the `Drawer` will be docked. In this state, the overlay won't show and
     * clicking on a menu item will not close the `Drawer`.
     */
    docked: PropTypes.bool,
    /**
     * Callback function fired when the `open` state of the `Drawer` is requested to be changed.
     *
     * @param {boolean} open If true, the `Drawer` was requested to be opened.
     * @param {string} reason The reason for the open or close request. Possible values are
     * 'swipe' for open requests; 'clickaway' (on overlay clicks),
     * 'escape' (on escape key press), and 'swipe' for close requests.
     */
    onRequestChange: PropTypes.func,
    /**
     * If true, the `Drawer` is opened.  Providing a value will turn the `Drawer`
     * into a controlled component.
     */
    open: PropTypes.bool,
    /**
     * If true, the `Drawer` is positioned to open from the opposite side.
     */
    openSecondary: PropTypes.bool,
    /**
     * The CSS class name to add to the `Overlay` component that is rendered behind the `Drawer`.
     */
    overlayClassName: PropTypes.string,
    /**
     * Override the inline-styles of the `Overlay` component that is rendered behind the `Drawer`.
     */
    overlayStyle: PropTypes.object,
    /**
     * Override the inline-styles of the root element.
     */
    style: PropTypes.object,
    /**
     * The width of the left most (or right most) area in pixels where the `Drawer` can be
     * swiped open from. Setting this to `null` spans that area to the entire page
     * (**CAUTION!** Setting this property to `null` might cause issues with sliders and
     * swipeable `Tabs`: use at your own risk).
     */
    swipeAreaWidth: PropTypes.number,
    /**
     * The width of the `Drawer` in pixels or percentage in string format ex. `50%` to fill
     * half of the window or `100%` and so on. Defaults to using the values from theme.
     */
    width: PropTypes.oneOfType([
      PropTypes.string,
      PropTypes.number,
    ]),
    /**
     * The zDepth of the `Drawer`.
     */
    zDepth: propTypes.zDepth,

  };

  static defaultProps = {
    disableSwipeToOpen: false,
    docked: true,
    open: null,
    openSecondary: false,
    swipeAreaWidth: 30,
    width: null,
    zDepth: 2,
  };

  static contextTypes = {
    muiTheme: PropTypes.object.isRequired,
  };

  componentWillMount() {
    this.maybeSwiping = false;
    this.touchStartX = null;
    this.touchStartY = null;
    this.swipeStartX = null;

    this.setState({
      open: (this.props.open !== null ) ? this.props.open : this.props.docked,
      swiping: null,
    });
  }

  componentDidMount() {
    this.enableSwipeHandling();
  }

  componentWillReceiveProps(nextProps) {
    // If controlled then the open prop takes precedence.
    if (nextProps.open !== null) {
      this.setState({
        open: nextProps.open,
      });
      // Otherwise, if docked is changed, change the open state for when uncontrolled.
    } else if (this.props.docked !== nextProps.docked) {
      this.setState({
        open: nextProps.docked,
      });
    }
  }

  componentDidUpdate() {
    this.enableSwipeHandling();
  }

  componentWillUnmount() {
    this.disableSwipeHandling();
  }

  getStyles() {
    const muiTheme = this.context.muiTheme;
    const theme = muiTheme.drawer;

    const x = this.getTranslateMultiplier() * (this.state.open ? 0 : this.getMaxTranslateX());

    const styles = {
      root: {
        height: '100%',
        width: this.getTranslatedWidth() || theme.width,
        position: 'fixed',
        zIndex: muiTheme.zIndex.drawer,
        left: 0,
        top: 0,
        transform: `translate(${x}px, 0)`,
        transition: !this.state.swiping && transitions.easeOut(null, 'transform', null),
        backgroundColor: theme.color,
        overflow: 'auto',
        WebkitOverflowScrolling: 'touch', // iOS momentum scrolling
      },
      overlay: {
        zIndex: muiTheme.zIndex.drawerOverlay,
        pointerEvents: this.state.open ? 'auto' : 'none', // Bypass mouse events when left nav is closing.
      },
      rootWhenOpenRight: {
        left: 'auto',
        right: 0,
      },
    };

    return styles;
  }

  shouldShow() {
    return this.state.open || !!this.state.swiping;  // component is swiping
  }

  close(reason) {
    if (this.props.open === null) this.setState({open: false});
    if (this.props.onRequestChange) this.props.onRequestChange(false, reason);
    return this;
  }

  open(reason) {
    if (this.props.open === null) this.setState({open: true});
    if (this.props.onRequestChange) this.props.onRequestChange(true, reason);
    return this;
  }

  handleTouchTapOverlay = (event) => {
    event.preventDefault();
    this.close('clickaway');
  };

  handleKeyUp = (event) => {
    if (this.state.open && !this.props.docked && keycode(event) === 'esc') {
      this.close('escape');
    }
  };

  getTranslatedWidth() {
    if (typeof this.props.width === 'string') {
      if (!/^\d+(\.\d+)?%$/.test(this.props.width)) {
        throw new Error('Not a valid percentage format.');
      }
      const width = parseFloat(this.props.width) / 100.0;
      // We are doing our best on the Server to render a consistent UI, hence the
      // default value of 10000
      return window ? width * window.innerWidth : 10000;
    } else {
      return this.props.width;
    }
  }

  getMaxTranslateX() {
    const width = this.getTranslatedWidth() || this.context.muiTheme.drawer.width;
    return width + 10;
  }

  getTranslateMultiplier() {
    return this.props.openSecondary ? 1 : -1;
  }

  enableSwipeHandling() {
    if (!this.props.docked) {
      document.body.addEventListener('touchstart', this.onBodyTouchStart);
      if (!openNavEventHandler) {
        openNavEventHandler = this.onBodyTouchStart;
      }
    } else {
      this.disableSwipeHandling();
    }
  }

  disableSwipeHandling() {
    document.body.removeEventListener('touchstart', this.onBodyTouchStart);
    if (openNavEventHandler === this.onBodyTouchStart) {
      openNavEventHandler = null;
    }
  }

  onBodyTouchStart = (event) => {
    const swipeAreaWidth = this.props.swipeAreaWidth;

    const touchStartX = this.context.muiTheme.isRtl ?
      (document.body.offsetWidth - event.touches[0].pageX) :
      event.touches[0].pageX;
    const touchStartY = event.touches[0].pageY;

    // Open only if swiping from far left (or right) while closed
    if (swipeAreaWidth !== null && !this.state.open) {
      if (this.props.openSecondary) {
        // If openSecondary is true calculate from the far right
        if (touchStartX < document.body.offsetWidth - swipeAreaWidth) return;
      } else {
        // If openSecondary is false calculate from the far left
        if (touchStartX > swipeAreaWidth) return;
      }
    }

    if (!this.state.open &&
         (openNavEventHandler !== this.onBodyTouchStart ||
          this.props.disableSwipeToOpen)
       ) {
      return;
    }

    this.maybeSwiping = true;
    this.touchStartX = touchStartX;
    this.touchStartY = touchStartY;

    document.body.addEventListener('touchmove', this.onBodyTouchMove);
    document.body.addEventListener('touchend', this.onBodyTouchEnd);
    document.body.addEventListener('touchcancel', this.onBodyTouchEnd);
  };

  setPosition(translateX) {
    const rtlTranslateMultiplier = this.context.muiTheme.isRtl ? -1 : 1;
    const drawer = ReactDOM.findDOMNode(this.refs.clickAwayableElement);
    const transformCSS = `translate(${(this.getTranslateMultiplier() * rtlTranslateMultiplier * translateX)}px, 0)`;
    this.refs.overlay.setOpacity(1 - translateX / this.getMaxTranslateX());
    autoPrefix.set(drawer.style, 'transform', transformCSS);
  }

  getTranslateX(currentX) {
    return Math.min(
             Math.max(
               this.state.swiping === 'closing' ?
                 this.getTranslateMultiplier() * (currentX - this.swipeStartX) :
                 this.getMaxTranslateX() - this.getTranslateMultiplier() * (this.swipeStartX - currentX),
               0
             ),
             this.getMaxTranslateX()
           );
  }

  onBodyTouchMove = (event) => {
    const currentX = this.context.muiTheme.isRtl ?
      (document.body.offsetWidth - event.touches[0].pageX) :
      event.touches[0].pageX;
    const currentY = event.touches[0].pageY;

    if (this.state.swiping) {
      event.preventDefault();
      this.setPosition(this.getTranslateX(currentX));
    } else if (this.maybeSwiping) {
      const dXAbs = Math.abs(currentX - this.touchStartX);
      const dYAbs = Math.abs(currentY - this.touchStartY);
      // If the user has moved his thumb ten pixels in either direction,
      // we can safely make an assumption about whether he was intending
      // to swipe or scroll.
      const threshold = 10;

      if (dXAbs > threshold && dYAbs <= threshold) {
        this.swipeStartX = currentX;
        this.setState({
          swiping: this.state.open ? 'closing' : 'opening',
        });
        this.setPosition(this.getTranslateX(currentX));
      } else if (dXAbs <= threshold && dYAbs > threshold) {
        this.onBodyTouchEnd();
      }
    }
  };

  onBodyTouchEnd = (event) => {
    if (this.state.swiping) {
      const currentX = this.context.muiTheme.isRtl ?
        (document.body.offsetWidth - event.changedTouches[0].pageX) :
        event.changedTouches[0].pageX;
      const translateRatio = this.getTranslateX(currentX) / this.getMaxTranslateX();

      this.maybeSwiping = false;
      const swiping = this.state.swiping;
      this.setState({
        swiping: null,
      });

      // We have to open or close after setting swiping to null,
      // because only then CSS transition is enabled.
      if (translateRatio > 0.5) {
        if (swiping === 'opening') {
          this.setPosition(this.getMaxTranslateX());
        } else {
          this.close('swipe');
        }
      } else {
        if (swiping === 'opening') {
          this.open('swipe');
        } else {
          this.setPosition(0);
        }
      }
    } else {
      this.maybeSwiping = false;
    }

    document.body.removeEventListener('touchmove', this.onBodyTouchMove);
    document.body.removeEventListener('touchend', this.onBodyTouchEnd);
    document.body.removeEventListener('touchcancel', this.onBodyTouchEnd);
  };

  render() {
    const {
      children,
      className,
      containerClassName,
      containerStyle,
      docked,
      openSecondary,
      overlayClassName,
      overlayStyle,
      style,
      zDepth,
    } = this.props;

    const styles = this.getStyles();

    let overlay;
    if (!docked) {
      overlay = (
        <Overlay
          ref="overlay"
          show={this.shouldShow()}
          className={overlayClassName}
          style={Object.assign(styles.overlay, overlayStyle)}
          transitionEnabled={!this.state.swiping}
          onTouchTap={this.handleTouchTapOverlay}
        />
      );
    }

    return (
      <div
        className={className}
        style={style}
      >
        <EventListener target="window" onKeyUp={this.handleKeyUp} />
        {overlay}
        <Paper
          ref="clickAwayableElement"
          zDepth={zDepth}
          rounded={false}
          transitionEnabled={!this.state.swiping}
          className={containerClassName}
          style={Object.assign(styles.root, openSecondary && styles.rootWhenOpenRight, containerStyle)}
        >
          {children}
        </Paper>
      </div>
    );
  }
}

export default Drawer;
<|endoftext|>


<commit_before>const moment = require('moment-timezone');
const path = require('path');
const fs = require('fs-extra');
const membersCSV = require('@tryghost/members-csv');
const errors = require('@tryghost/errors');
const tpl = require('@tryghost/tpl');

const emailTemplate = require('./email-template');

const messages = {
    filenameCollision: 'Filename already exists, please try again.',
    jobAlreadyComplete: 'Job is already complete.'
};

module.exports = class MembersCSVImporter {
    /**
     * @param {Object} options
     * @param {string} options.storagePath - The path to store CSV's in before importing
     * @param {Function} options.getTimezone - function returning currently configured timezone
     * @param {() => Object} options.getMembersApi
     * @param {Function} options.sendEmail - function sending an email
     * @param {(string) => boolean} options.isSet - Method checking if specific feature is enabled
     * @param {({job, offloaded}) => void} options.addJob - Method registering an async job
     * @param {Object} options.knex - An instance of the Ghost Database connection
     * @param {Function} options.urlFor - function generating urls
     * @param {Object} options.context
     */
    constructor({storagePath, getTimezone, getMembersApi, sendEmail, isSet, addJob, knex, urlFor, context}) {
        this._storagePath = storagePath;
        this._getTimezone = getTimezone;
        this._getMembersApi = getMembersApi;
        this._sendEmail = sendEmail;
        this._isSet = isSet;
        this._addJob = addJob;
        this._knex = knex;
        this._urlFor = urlFor;
        this._context = context;
    }

    /**
     * @typedef {string} JobID
     */

    /**
     * @typedef {Object} Job
     * @prop {string} filename
     * @prop {JobID} id
     * @prop {string} status
     */

    /**
     * Get the Job for a jobCode
     * @param {JobID} jobId
     * @returns {Promise<Job>}
     */
    async getJob(jobId) {
        return {
            id: jobId,
            filename: jobId,
            status: 'pending'
        };
    }

    /**
     * Prepares a CSV file for import
     * - Maps headers based on headerMapping, this allows for a non standard CSV
     *   to be imported, so long as a mapping exists between it and a standard CSV
     * - Stores the CSV to be imported in the storagePath
     * - Creates a MemberImport Job and associated MemberImportBatch's
     *
     * @param {string} inputFilePath - The path to the CSV to prepare
     * @param {Object.<string, string>} headerMapping - An object whose keys are headers in the input CSV and values are the header to replace it with
     * @param {Array<string>} defaultLabels - A list of labels to apply to every member
     *
     * @returns {Promise<{id: JobID, batches: number, metadata: Object.<string, any>}>} - A promise resolving to the id of the MemberImport Job
     */
    async prepare(inputFilePath, headerMapping, defaultLabels) {
        const batchSize = 1;

        const siteTimezone = this._getTimezone();
        const currentTime = moment().tz(siteTimezone).format('YYYY-MM-DD HH:mm:ss.SSS');
        const outputFileName = `Members Import ${currentTime}.csv`;
        const outputFilePath = path.join(this._storagePath, '/', outputFileName);

        const pathExists = await fs.pathExists(outputFilePath);

        if (pathExists) {
            throw new errors.DataImportError({message: tpl(messages.filenameCollision)});
        }

        const inputMapping = Object.assign({}, headerMapping, {
            subscribed_to_emails: 'subscribed'
        });
        const rows = await membersCSV.parse(inputFilePath, inputMapping, defaultLabels);
        const columns = Object.keys(rows[0]);
        const numberOfBatches = Math.ceil(rows.length / batchSize);
        const mappedCSV = membersCSV.unparse(rows, columns);

        const hasStripeData = rows.find(function rowHasStripeData(row) {
            return !!row.stripe_customer_id || !!row.complimentary_plan;
        });

        await fs.writeFile(outputFilePath, mappedCSV);

        return {
            id: outputFilePath,
            batches: numberOfBatches,
            metadata: {
                hasStripeData
            }
        };
    }

    /**
     * Performs an import of a CSV file
     *
     * @param {JobID} id - The id of the job to perform
     */
    async perform(id) {
        const job = await this.getJob(id);

        if (job.status === 'complete') {
            throw new errors.BadRequestError({message: tpl(messages.jobAlreadyComplete)});
        }

        const rows = membersCSV.parse(job.filename);

        const membersApi = await this._getMembersApi();

        const defaultProductPage = await membersApi.productRepository.list({
            filter: 'type:paid',
            limit: 1
        });

        const defaultProduct = defaultProductPage.data[0];

        const result = await rows.reduce(async (resultPromise, row) => {
            const resultAccumulator = await resultPromise;

            // Use doNotReject config to reject `executionPromise` on rollback
            // https://github.com/knex/knex/blob/master/UPGRADING.md
            const trx = await this._knex.transaction(undefined, {doNotRejectOnRollback: false});
            const options = {
                transacting: trx,
                context: this._context
            };

            try {
                const existingMember = await membersApi.members.get({email: row.email}, {
                    ...options,
                    withRelated: ['labels']
                });
                let member;
                if (existingMember) {
                    const existingLabels = existingMember.related('labels') ? existingMember.related('labels').toJSON() : [];
                    member = await membersApi.members.update({
                        ...row,
                        labels: existingLabels.concat(row.labels)
                    }, {
                        ...options,
                        id: existingMember.id
                    });
                } else {
                    member = await membersApi.members.create(row, Object.assign({}, options, {
                        context: {
                            import: true
                        }
                    }));
                }

                if (row.stripe_customer_id && typeof row.stripe_customer_id === 'string') {
                    await membersApi.members.linkStripeCustomer({
                        customer_id: row.stripe_customer_id,
                        member_id: member.id
                    }, options);
                } else if (row.complimentary_plan) {
                    if (!row.products) {
                        await membersApi.members.update({
                            products: [{id: defaultProduct.id}]
                        }, {
                            ...options,
                            id: member.id
                        });
                    }
                }

                if (row.products) {
                    await membersApi.members.update({
                        products: row.products
                    }, {
                        ...options,
                        id: member.id
                    });
                }

                await trx.commit();
                return {
                    ...resultAccumulator,
                    imported: resultAccumulator.imported + 1
                };
            } catch (error) {
                // The model layer can sometimes throw arrays of errors
                const errorList = [].concat(error);
                const errorMessage = errorList.map(({message}) => message).join(', ');
                await trx.rollback();
                return {
                    ...resultAccumulator,
                    errors: [...resultAccumulator.errors, {
                        ...row,
                        error: errorMessage
                    }]
                };
            }
        }, Promise.resolve({
            imported: 0,
            errors: []
        }));

        return {
            total: result.imported + result.errors.length,
            ...result
        };
    }

    generateCompletionEmail(result, data) {
        const siteUrl = new URL(this._urlFor('home', null, true));
        const membersUrl = new URL('members', this._urlFor('admin', null, true));
        if (data.importLabel) {
            membersUrl.searchParams.set('label', data.importLabel.slug);
        }
        return emailTemplate({result, siteUrl, membersUrl, ...data});
    }

    generateErrorCSV(result) {
        const errorsWithFormattedMessages = result.errors.map((row) => {
            const formattedError = row.error
                .replace(
                    'Value in [members.email] cannot be blank.',
                    'Missing email address'
                )
                .replace(
                    'Value in [members.note] exceeds maximum length of 2000 characters.',
                    '"Note" exceeds maximum length of 2000 characters'
                )
                .replace(
                    'Value in [members.subscribed] must be one of true, false, 0 or 1.',
                    'Value in "Subscribed to emails" must be "true" or "false"'
                )
                .replace(
                    'Validation (isEmail) failed for email',
                    'Invalid email address'
                )
                .replace(
                    /No such customer:[^,]*/,
                    'Could not find Stripe customer'
                );

            return {
                ...row,
                error: formattedError
            };
        });
        return membersCSV.unparse(errorsWithFormattedMessages);
    }

    /**
     * Send email with attached CSV containing error rows info
     *
     * @param {Object} config
     * @param {String} config.emailRecipient - email recipient for error file
     * @param {String} config.emailSubject - email subject
     * @param {String} config.emailContent - html content of email
     * @param {String} config.errorCSV - error CSV content
     * @param {Object} config.emailSubject - email subject
     * @param {Object} config.importLabel -
     * @param {String} config.importLabel.name - label name
     */
    async sendErrorEmail({emailRecipient, emailSubject, emailContent, errorCSV, importLabel}) {
        await this._sendEmail({
            to: emailRecipient,
            subject: emailSubject,
            html: emailContent,
            forceTextContent: true,
            attachments: [{
                filename: `${importLabel.name} - Errors.csv`,
                content: errorCSV,
                contentType: 'text/csv',
                contentDisposition: 'attachment'
            }]
        });
        return;
    }

    /**
     * Processes CSV file and imports member&label records depending on the size of the imported set
     *
     * @param {Object} config
     * @param {String} config.pathToCSV - path where imported csv with members records is stored
     * @param {Object} config.headerMapping - mapping of CSV headers to member record fields
     * @param {Object} [config.globalLabels] - labels to be applied to whole imported members set
     * @param {Object} config.importLabel -
     * @param {String} config.importLabel.name - label name
     * @param {Object} config.user
     * @param {String} config.user.email - calling user email
     * @param {Object} config.LabelModel - instance of Ghosts Label model
     */
    async process({pathToCSV, headerMapping, globalLabels, importLabel, user, LabelModel}) {
        const meta = {};
        const job = await this.prepare(pathToCSV, headerMapping, globalLabels);

        meta.originalImportSize = job.batches;

        if (job.batches <= 500 && !job.metadata.hasStripeData) {
            const result = await this.perform(job.id);
            const importLabelModel = result.imported ? await LabelModel.findOne(importLabel) : null;

            return {
                meta: Object.assign(meta, {
                    stats: {
                        imported: result.imported,
                        invalid: result.errors
                    },
                    import_label: importLabelModel
                })
            };
        } else {
            const emailRecipient = user.email;
            this._addJob({
                job: async () => {
                    const result = await this.perform(job.id);
                    const importLabelModel = result.imported ? await LabelModel.findOne(importLabel) : null;
                    const emailContent = this.generateCompletionEmail(result, {
                        emailRecipient,
                        importLabel: importLabelModel ? importLabelModel.toJSON() : null
                    });
                    const errorCSV = this.generateErrorCSV(result);
                    const emailSubject = result.imported > 0 ? 'Your member import is complete' : 'Your member import was unsuccessful';

                    await this.sendErrorEmail({
                        emailRecipient,
                        emailSubject,
                        emailContent,
                        errorCSV,
                        importLabel
                    });
                },
                offloaded: false
            });

            return {
                meta
            };
        }
    }
};
<commit_msg>Added future investigation note<commit_after>const moment = require('moment-timezone');
const path = require('path');
const fs = require('fs-extra');
const membersCSV = require('@tryghost/members-csv');
const errors = require('@tryghost/errors');
const tpl = require('@tryghost/tpl');

const emailTemplate = require('./email-template');

const messages = {
    filenameCollision: 'Filename already exists, please try again.',
    jobAlreadyComplete: 'Job is already complete.'
};

module.exports = class MembersCSVImporter {
    /**
     * @param {Object} options
     * @param {string} options.storagePath - The path to store CSV's in before importing
     * @param {Function} options.getTimezone - function returning currently configured timezone
     * @param {() => Object} options.getMembersApi
     * @param {Function} options.sendEmail - function sending an email
     * @param {(string) => boolean} options.isSet - Method checking if specific feature is enabled
     * @param {({job, offloaded}) => void} options.addJob - Method registering an async job
     * @param {Object} options.knex - An instance of the Ghost Database connection
     * @param {Function} options.urlFor - function generating urls
     * @param {Object} options.context
     */
    constructor({storagePath, getTimezone, getMembersApi, sendEmail, isSet, addJob, knex, urlFor, context}) {
        this._storagePath = storagePath;
        this._getTimezone = getTimezone;
        this._getMembersApi = getMembersApi;
        this._sendEmail = sendEmail;
        this._isSet = isSet;
        this._addJob = addJob;
        this._knex = knex;
        this._urlFor = urlFor;
        this._context = context;
    }

    /**
     * @typedef {string} JobID
     */

    /**
     * @typedef {Object} Job
     * @prop {string} filename
     * @prop {JobID} id
     * @prop {string} status
     */

    /**
     * Get the Job for a jobCode
     * @param {JobID} jobId
     * @returns {Promise<Job>}
     */
    async getJob(jobId) {
        return {
            id: jobId,
            filename: jobId,
            status: 'pending'
        };
    }

    /**
     * Prepares a CSV file for import
     * - Maps headers based on headerMapping, this allows for a non standard CSV
     *   to be imported, so long as a mapping exists between it and a standard CSV
     * - Stores the CSV to be imported in the storagePath
     * - Creates a MemberImport Job and associated MemberImportBatch's
     *
     * @param {string} inputFilePath - The path to the CSV to prepare
     * @param {Object.<string, string>} headerMapping - An object whose keys are headers in the input CSV and values are the header to replace it with
     * @param {Array<string>} defaultLabels - A list of labels to apply to every member
     *
     * @returns {Promise<{id: JobID, batches: number, metadata: Object.<string, any>}>} - A promise resolving to the id of the MemberImport Job
     */
    async prepare(inputFilePath, headerMapping, defaultLabels) {
        // @NOTE: investigate why is it "1" and do we even need this concept anymore?
        const batchSize = 1;

        const siteTimezone = this._getTimezone();
        const currentTime = moment().tz(siteTimezone).format('YYYY-MM-DD HH:mm:ss.SSS');
        const outputFileName = `Members Import ${currentTime}.csv`;
        const outputFilePath = path.join(this._storagePath, '/', outputFileName);

        const pathExists = await fs.pathExists(outputFilePath);

        if (pathExists) {
            throw new errors.DataImportError({message: tpl(messages.filenameCollision)});
        }

        const inputMapping = Object.assign({}, headerMapping, {
            subscribed_to_emails: 'subscribed'
        });
        const rows = await membersCSV.parse(inputFilePath, inputMapping, defaultLabels);
        const columns = Object.keys(rows[0]);
        const numberOfBatches = Math.ceil(rows.length / batchSize);
        const mappedCSV = membersCSV.unparse(rows, columns);

        const hasStripeData = rows.find(function rowHasStripeData(row) {
            return !!row.stripe_customer_id || !!row.complimentary_plan;
        });

        await fs.writeFile(outputFilePath, mappedCSV);

        return {
            id: outputFilePath,
            batches: numberOfBatches,
            metadata: {
                hasStripeData
            }
        };
    }

    /**
     * Performs an import of a CSV file
     *
     * @param {JobID} id - The id of the job to perform
     */
    async perform(id) {
        const job = await this.getJob(id);

        if (job.status === 'complete') {
            throw new errors.BadRequestError({message: tpl(messages.jobAlreadyComplete)});
        }

        const rows = membersCSV.parse(job.filename);

        const membersApi = await this._getMembersApi();

        const defaultProductPage = await membersApi.productRepository.list({
            filter: 'type:paid',
            limit: 1
        });

        const defaultProduct = defaultProductPage.data[0];

        const result = await rows.reduce(async (resultPromise, row) => {
            const resultAccumulator = await resultPromise;

            // Use doNotReject config to reject `executionPromise` on rollback
            // https://github.com/knex/knex/blob/master/UPGRADING.md
            const trx = await this._knex.transaction(undefined, {doNotRejectOnRollback: false});
            const options = {
                transacting: trx,
                context: this._context
            };

            try {
                const existingMember = await membersApi.members.get({email: row.email}, {
                    ...options,
                    withRelated: ['labels']
                });
                let member;
                if (existingMember) {
                    const existingLabels = existingMember.related('labels') ? existingMember.related('labels').toJSON() : [];
                    member = await membersApi.members.update({
                        ...row,
                        labels: existingLabels.concat(row.labels)
                    }, {
                        ...options,
                        id: existingMember.id
                    });
                } else {
                    member = await membersApi.members.create(row, Object.assign({}, options, {
                        context: {
                            import: true
                        }
                    }));
                }

                if (row.stripe_customer_id && typeof row.stripe_customer_id === 'string') {
                    await membersApi.members.linkStripeCustomer({
                        customer_id: row.stripe_customer_id,
                        member_id: member.id
                    }, options);
                } else if (row.complimentary_plan) {
                    if (!row.products) {
                        await membersApi.members.update({
                            products: [{id: defaultProduct.id}]
                        }, {
                            ...options,
                            id: member.id
                        });
                    }
                }

                if (row.products) {
                    await membersApi.members.update({
                        products: row.products
                    }, {
                        ...options,
                        id: member.id
                    });
                }

                await trx.commit();
                return {
                    ...resultAccumulator,
                    imported: resultAccumulator.imported + 1
                };
            } catch (error) {
                // The model layer can sometimes throw arrays of errors
                const errorList = [].concat(error);
                const errorMessage = errorList.map(({message}) => message).join(', ');
                await trx.rollback();
                return {
                    ...resultAccumulator,
                    errors: [...resultAccumulator.errors, {
                        ...row,
                        error: errorMessage
                    }]
                };
            }
        }, Promise.resolve({
            imported: 0,
            errors: []
        }));

        return {
            total: result.imported + result.errors.length,
            ...result
        };
    }

    generateCompletionEmail(result, data) {
        const siteUrl = new URL(this._urlFor('home', null, true));
        const membersUrl = new URL('members', this._urlFor('admin', null, true));
        if (data.importLabel) {
            membersUrl.searchParams.set('label', data.importLabel.slug);
        }
        return emailTemplate({result, siteUrl, membersUrl, ...data});
    }

    generateErrorCSV(result) {
        const errorsWithFormattedMessages = result.errors.map((row) => {
            const formattedError = row.error
                .replace(
                    'Value in [members.email] cannot be blank.',
                    'Missing email address'
                )
                .replace(
                    'Value in [members.note] exceeds maximum length of 2000 characters.',
                    '"Note" exceeds maximum length of 2000 characters'
                )
                .replace(
                    'Value in [members.subscribed] must be one of true, false, 0 or 1.',
                    'Value in "Subscribed to emails" must be "true" or "false"'
                )
                .replace(
                    'Validation (isEmail) failed for email',
                    'Invalid email address'
                )
                .replace(
                    /No such customer:[^,]*/,
                    'Could not find Stripe customer'
                );

            return {
                ...row,
                error: formattedError
            };
        });
        return membersCSV.unparse(errorsWithFormattedMessages);
    }

    /**
     * Send email with attached CSV containing error rows info
     *
     * @param {Object} config
     * @param {String} config.emailRecipient - email recipient for error file
     * @param {String} config.emailSubject - email subject
     * @param {String} config.emailContent - html content of email
     * @param {String} config.errorCSV - error CSV content
     * @param {Object} config.emailSubject - email subject
     * @param {Object} config.importLabel -
     * @param {String} config.importLabel.name - label name
     */
    async sendErrorEmail({emailRecipient, emailSubject, emailContent, errorCSV, importLabel}) {
        await this._sendEmail({
            to: emailRecipient,
            subject: emailSubject,
            html: emailContent,
            forceTextContent: true,
            attachments: [{
                filename: `${importLabel.name} - Errors.csv`,
                content: errorCSV,
                contentType: 'text/csv',
                contentDisposition: 'attachment'
            }]
        });
        return;
    }

    /**
     * Processes CSV file and imports member&label records depending on the size of the imported set
     *
     * @param {Object} config
     * @param {String} config.pathToCSV - path where imported csv with members records is stored
     * @param {Object} config.headerMapping - mapping of CSV headers to member record fields
     * @param {Object} [config.globalLabels] - labels to be applied to whole imported members set
     * @param {Object} config.importLabel -
     * @param {String} config.importLabel.name - label name
     * @param {Object} config.user
     * @param {String} config.user.email - calling user email
     * @param {Object} config.LabelModel - instance of Ghosts Label model
     */
    async process({pathToCSV, headerMapping, globalLabels, importLabel, user, LabelModel}) {
        const meta = {};
        const job = await this.prepare(pathToCSV, headerMapping, globalLabels);

        meta.originalImportSize = job.batches;

        if (job.batches <= 500 && !job.metadata.hasStripeData) {
            const result = await this.perform(job.id);
            const importLabelModel = result.imported ? await LabelModel.findOne(importLabel) : null;

            return {
                meta: Object.assign(meta, {
                    stats: {
                        imported: result.imported,
                        invalid: result.errors
                    },
                    import_label: importLabelModel
                })
            };
        } else {
            const emailRecipient = user.email;
            this._addJob({
                job: async () => {
                    const result = await this.perform(job.id);
                    const importLabelModel = result.imported ? await LabelModel.findOne(importLabel) : null;
                    const emailContent = this.generateCompletionEmail(result, {
                        emailRecipient,
                        importLabel: importLabelModel ? importLabelModel.toJSON() : null
                    });
                    const errorCSV = this.generateErrorCSV(result);
                    const emailSubject = result.imported > 0 ? 'Your member import is complete' : 'Your member import was unsuccessful';

                    await this.sendErrorEmail({
                        emailRecipient,
                        emailSubject,
                        emailContent,
                        errorCSV,
                        importLabel
                    });
                },
                offloaded: false
            });

            return {
                meta
            };
        }
    }
};
<|endoftext|>


<commit_before>var path = require('path');
var webpack = require('webpack');

module.exports = {
  devtool: '#cheap-module-eval-source-map',
  entry: [
    'eventsource-polyfill', // necessary for hot reloading with IE
    'webpack-hot-middleware/client',
    './src/index'
  ],
  output: {
    path: path.join(__dirname, 'dist'),
    filename: 'bundle.js',
    publicPath: '/static/'
  },
  plugins: [
    new webpack.HotModuleReplacementPlugin(),
    new webpack.NoErrorsPlugin()
  ],
  module: {
    loaders: [
      {
        test: /\.less$/,
        loader: 'style-loader!css-loader!less'
      },
      {
        test: /\.css$/,
        loader: 'style-loader!css-loader'
      },
      {
        test: /\.jsx?/,
        loaders: ['babel'],
        include: path.join(__dirname, 'src')
      }
    ]
  }
};
<commit_msg>Fix webpack public path<commit_after>var path = require('path');
var webpack = require('webpack');

module.exports = {
  devtool: '#cheap-module-eval-source-map',
  entry: [
    'eventsource-polyfill', // necessary for hot reloading with IE
    'webpack-hot-middleware/client',
    './src/index'
  ],
  output: {
    path: path.join(__dirname, 'dist'),
    filename: 'bundle.js',
    publicPath: '/dist/'
  },
  plugins: [
    new webpack.HotModuleReplacementPlugin(),
    new webpack.NoErrorsPlugin()
  ],
  module: {
    loaders: [
      {
        test: /\.less$/,
        loader: 'style-loader!css-loader!less'
      },
      {
        test: /\.css$/,
        loader: 'style-loader!css-loader'
      },
      {
        test: /\.jsx?/,
        loaders: ['babel'],
        include: path.join(__dirname, 'src')
      }
    ]
  }
};
<|endoftext|>