<commit_before>// Copyright 2017, University of Colorado Boulder

/**
 * Bounds2 tests
 *
 * @author Michael Barlow (PhET Interactive Simulations)
 */

define( function( require ) {
  'use strict';

  // modules
  var OpenRange = require( 'DOT/OpenRange' );
  var Range = require( 'DOT/Range' );

  var minHalfOpenOptions = { openMax: false };
  var maxHalfOpenOptions = { openMin: false };

  QUnit.module( 'OpenRange');

  QUnit.test( 'half open min', function( assert ) {
    var testMinOpenRange = new OpenRange( 1, 10, minHalfOpenOptions );
    assert.notOk( testMinOpenRange.contains( 1 ), '(1, 10] does not contain 1' );
    assert.notOk( testMinOpenRange.intersects( new Range( 0, 1 ) ), '(1, 10] does not intersect [0, 1]' );
    assert.notOk( testMinOpenRange.containsRange( new Range( 1, 2 ), '(1, 10] does not contain [1, 2]' ) );
    assert.ok( testMinOpenRange.contains( 1.000000001 ), '(1, 10] contians 1.000000001' );
    assert.ok( testMinOpenRange.intersects( new Range( 0, 1.000000001 ) ), '(1, 10] intersects [0, 1.000000001]' );
    assert.ok( testMinOpenRange.containsRange( new Range( 1.000000001, 2 ) ), '(1, 10] contains [1.000000001, 2]' );
  } );

  QUnit.test( 'half open max', function( assert ) {
    var maxOpenRange = new OpenRange( 1, 10, maxHalfOpenOptions );
    assert.notOk( maxOpenRange.contains( 10 ), '[1, 10) does not contain 10' );
    assert.notOk( maxOpenRange.intersects( new Range( 10, 11 ) ), '[1, 10) does not intersect [10,11]' );
    assert.notOk( maxOpenRange.containsRange( new Range( 9, 10 ), '[1, 10) does not contiain [9, 10]' ) );
    assert.ok( maxOpenRange.contains( 9.999999999 ), '[1, 10) contians 9.999999999' );
    assert.ok( maxOpenRange.intersects( new Range( 9.999999999, 11 ) ), '[1, 10) intersects [9.999999999, 11]' );
    assert.ok( maxOpenRange.containsRange( new Range( 9, 9.999999999 ) ), '[1, 10) contians [9, 9.999999999]' );
  } );

  QUnit.test( 'fully open range', function( assert ) {
    var openRange = new OpenRange( 1, 10 );
    assert.notOk( openRange.contains( 1 ), '(1, 10) does not contain 1' );
    assert.notOk( openRange.contains( 10 ), '(1, 10) does not contain 10' );
    assert.notOk( openRange.intersects( new Range( 0, 1 ) ), '(1, 10) does not intersect [0, 1]' );
    assert.notOk( openRange.intersects( new Range( 10, 11 ) ), '(1, 10) does not intersect [10,11]' );
    assert.notOk( openRange.containsRange( new Range( 9, 10 ), '(1, 10) does not contiain [9, 10]' ) );
    assert.notOk( openRange.containsRange( new Range( 1, 2 ), '(1, 10) does not contain [1, 2]' ) );
    assert.ok( openRange.contains( 1.000000001 ), '(1, 10) contians 1.000000001' );
    assert.ok( openRange.contains( 9.999999999 ), '(1, 10) contians 9.999999999' );
    assert.ok( openRange.intersects( new Range( 0, 1.000000001 ) ), '(1, 10) intersects [0, 1.000000001]' );
    assert.ok( openRange.intersects( new Range( 9.999999999, 11 ) ), '(1, 10) intersects [9.999999999, 11]' );
    assert.ok( openRange.containsRange( new Range( 9, 9.999999999 ) ), '(1, 10) contians [9, 9.999999999]' );
    assert.ok( openRange.containsRange( new Range( 1.000000001, 2 ) ), '(1, 10) contains [1.000000001, 2]' );
  } );

  QUnit.test( 'assertion failures', function( assert ) {
    assert.throws( () => new OpenRange( 1, 10, { openMin: false, openMax: false } ), 'include both min and max throws an error' );
    assert.throws( () => new OpenRange( 1, 1, minHalfOpenOptions ), 'min open range with min === max throws an error' );
    assert.throws( () => new OpenRange( 1, 1, maxHalfOpenOptions ), 'max open range with min === max throws an error' );
    assert.throws( () => new OpenRange( 1, 1 ), 'full open range with min === max throws an error' );
  } );
} );<commit_msg>jsdoc update for OpenRange tests<commit_after>// Copyright 2017, University of Colorado Boulder

/**
 * OpenRange tests
 *
 * @author Michael Barlow (PhET Interactive Simulations)
 */

define( function( require ) {
  'use strict';

  // modules
  var OpenRange = require( 'DOT/OpenRange' );
  var Range = require( 'DOT/Range' );

  var minHalfOpenOptions = { openMax: false };
  var maxHalfOpenOptions = { openMin: false };

  QUnit.module( 'OpenRange');

  QUnit.test( 'half open min', function( assert ) {
    var testMinOpenRange = new OpenRange( 1, 10, minHalfOpenOptions );
    assert.notOk( testMinOpenRange.contains( 1 ), '(1, 10] does not contain 1' );
    assert.notOk( testMinOpenRange.intersects( new Range( 0, 1 ) ), '(1, 10] does not intersect [0, 1]' );
    assert.notOk( testMinOpenRange.containsRange( new Range( 1, 2 ), '(1, 10] does not contain [1, 2]' ) );
    assert.ok( testMinOpenRange.contains( 1.000000001 ), '(1, 10] contians 1.000000001' );
    assert.ok( testMinOpenRange.intersects( new Range( 0, 1.000000001 ) ), '(1, 10] intersects [0, 1.000000001]' );
    assert.ok( testMinOpenRange.containsRange( new Range( 1.000000001, 2 ) ), '(1, 10] contains [1.000000001, 2]' );
  } );

  QUnit.test( 'half open max', function( assert ) {
    var maxOpenRange = new OpenRange( 1, 10, maxHalfOpenOptions );
    assert.notOk( maxOpenRange.contains( 10 ), '[1, 10) does not contain 10' );
    assert.notOk( maxOpenRange.intersects( new Range( 10, 11 ) ), '[1, 10) does not intersect [10,11]' );
    assert.notOk( maxOpenRange.containsRange( new Range( 9, 10 ), '[1, 10) does not contiain [9, 10]' ) );
    assert.ok( maxOpenRange.contains( 9.999999999 ), '[1, 10) contians 9.999999999' );
    assert.ok( maxOpenRange.intersects( new Range( 9.999999999, 11 ) ), '[1, 10) intersects [9.999999999, 11]' );
    assert.ok( maxOpenRange.containsRange( new Range( 9, 9.999999999 ) ), '[1, 10) contians [9, 9.999999999]' );
  } );

  QUnit.test( 'fully open range', function( assert ) {
    var openRange = new OpenRange( 1, 10 );
    assert.notOk( openRange.contains( 1 ), '(1, 10) does not contain 1' );
    assert.notOk( openRange.contains( 10 ), '(1, 10) does not contain 10' );
    assert.notOk( openRange.intersects( new Range( 0, 1 ) ), '(1, 10) does not intersect [0, 1]' );
    assert.notOk( openRange.intersects( new Range( 10, 11 ) ), '(1, 10) does not intersect [10,11]' );
    assert.notOk( openRange.containsRange( new Range( 9, 10 ), '(1, 10) does not contiain [9, 10]' ) );
    assert.notOk( openRange.containsRange( new Range( 1, 2 ), '(1, 10) does not contain [1, 2]' ) );
    assert.ok( openRange.contains( 1.000000001 ), '(1, 10) contians 1.000000001' );
    assert.ok( openRange.contains( 9.999999999 ), '(1, 10) contians 9.999999999' );
    assert.ok( openRange.intersects( new Range( 0, 1.000000001 ) ), '(1, 10) intersects [0, 1.000000001]' );
    assert.ok( openRange.intersects( new Range( 9.999999999, 11 ) ), '(1, 10) intersects [9.999999999, 11]' );
    assert.ok( openRange.containsRange( new Range( 9, 9.999999999 ) ), '(1, 10) contians [9, 9.999999999]' );
    assert.ok( openRange.containsRange( new Range( 1.000000001, 2 ) ), '(1, 10) contains [1.000000001, 2]' );
  } );

  QUnit.test( 'assertion failures', function( assert ) {
    assert.throws( () => new OpenRange( 1, 10, { openMin: false, openMax: false } ), 'include both min and max throws an error' );
    assert.throws( () => new OpenRange( 1, 1, minHalfOpenOptions ), 'min open range with min === max throws an error' );
    assert.throws( () => new OpenRange( 1, 1, maxHalfOpenOptions ), 'max open range with min === max throws an error' );
    assert.throws( () => new OpenRange( 1, 1 ), 'full open range with min === max throws an error' );
  } );
} );<|endoftext|>


<commit_before>function getAverageVolume(array) {
	var values = 0;
	var average;

	var length = array.length;

	// get all the frequency amplitudes
	for (var i = 0; i < length; i++) {
		values += array[i];
	}

	average = values / length;
	return average;
}

// BirdSongPlayer
function BirdSongPlayer(audioContext, inCanvasID) {
	this.soundSource = null;

	this.sighting = null;
	this.sightingIndex = null;
	this.soundsForSighting = null;

	this.recordingIndex = null;
	this.recording = null;

	this.gain = audioContext.createGain();

	// see https://developer.mozilla.org/en-US/docs/Web/API/PannerNode
	this.panner = audioContext.createPanner();
	this.panner.panningModel = 'HRTF';
	this.panner.distanceModel = 'inverse';
	this.panner.refDistance = 1;
	this.panner.maxDistance = 10000;
	this.panner.rolloffFactor = 1;
	this.panner.coneInnerAngle = 360;
	this.panner.coneOuterAngle = 0;
	this.panner.coneOuterGain = 0;
	this.panner.setOrientation(1,0,0);
	this.panner.setPosition(2 * Math.random() - 1, 2 * Math.random() - 1, 2 * Math.random() - 1);
	this.panner.setVelocity(0, 0, 0);

	this.gain.connect(this.panner);

	var volumeMeterCanvas = document.getElementById(inCanvasID);
	var graphicsContext = volumeMeterCanvas.getContext('2d');

	// setup a analyzer
	var analyser = audioContext.createAnalyser();
	this.panner.connect(analyser);
	this.panner.connect(audioContext.destination);

	analyser.smoothingTimeConstant = 0.3;
	analyser.fftSize = 64;

	requestAnimationFrame(function vuMeter() {
		// get the average, bincount is fftsize / 2
		var array =  new Uint8Array(analyser.frequencyBinCount);
		analyser.getByteFrequencyData(array);
		var average = getAverageVolume(array);
		average = Math.max(Math.min(average, 125), 5);

		// clear the current state
		graphicsContext.fillStyle = 'rgb(220,220,220)'
		graphicsContext.fillRect(1, 5, 130, 15);

		// set the fill style
		graphicsContext.fillStyle = 'rgb(0,0,0)'

		// create the meters
		graphicsContext.fillRect(1, 5, average, 15);

		requestAnimationFrame(vuMeter);
	});
}

// mutes or unmutes the sound by toggling the gain between 0 and 1
BirdSongPlayer.prototype.toggleMute = function() {
	if (this.gain.gain.value > 0) {
		console.log('muting');
		this.gain.gain.value = 0;
	} else {
		console.log('unmuting');
		this.gain.gain.value = 1;
	}
}

// sets the X,Y,Z position of the Panner to random values between -1 and +1
BirdSongPlayer.prototype.randomizePanner = function() {
	this.panner.setPosition(2 * Math.random() - 1, 2 * Math.random() - 1, 2 * Math.random() - 1);
}

// Sets the playback rate for the current sound to a random value between 0.1 and 1.1
BirdSongPlayer.prototype.randomizePlaybackRate = function(inPlayerSelector) {
	if (this.soundSource) {
		this.soundSource.playbackRate.value = 0.1 + Math.random();
		$(inPlayerSelector).find('.playbackRate').text((Math.round(100 * this.soundSource.playbackRate.value) / 100.0) + "x");
	} else {
		console.log('cannot randomize, no sound source');
	}
}

// Creates a new soundSource using the given buffer
// preserves the old playback rate, if applicable
BirdSongPlayer.prototype.setSourceFromBuffer = function(inBuffer) {
	console.log('setSourceFromBuffer');
	var oldPlaybackRateValue = 1;

	if (this.soundSource) {
		oldPlaybackRateValue = this.soundSource.playbackRate.value;
		this.soundSource.stop(0);
		this.soundSource = null;
	}

	this.soundSource = gAudioContext.createBufferSource();
	this.soundSource.connect(this.gain);
	this.soundSource.buffer = inBuffer;
	// restore old playback rate value
	this.soundSource.playbackRate.value = oldPlaybackRateValue;
	// start playing immediately in a loop
	this.soundSource.loop = true;
	this.soundSource.start(0);
}

// Reverses the audio buffer so that the sound plays backwards
BirdSongPlayer.prototype.reversePlayback = function() {
	console.log('reversePlayback');

	var oldData = this.soundSource.buffer.getChannelData(0);
	console.log(oldData);
	var normalData = Array.prototype.slice.call( oldData );
	Array.prototype.reverse.call(normalData);
	console.log(normalData);

	var newBuffer = gAudioContext.createBuffer(1, this.soundSource.buffer.length, this.soundSource.buffer.sampleRate);
	var newData = newBuffer.getChannelData(0);

	for (i = 0; i < newBuffer.length; i++) {
		newData[i] = normalData[i];
	}

	this.setSourceFromBuffer(newBuffer);

	console.log(this.soundSource.buffer.getChannelData(0));
	console.log('DONE reversePlayback');
}

BirdSongPlayer.prototype.setBufferFromURL = function(inSoundDataURL, inPlayerSelector) {
	console.log('setBufferFromURL ' + inSoundDataURL);

	var mp3Request = new XMLHttpRequest();

	mp3Request.onerror = function(e) {
		console.log('ERROR downloading');
		console.log(e);
		$(inPlayerSelector).find('.status').text('error downloading');
	};

	mp3Request.onprogress = function(e) {
		$(inPlayerSelector).find('.status').text('downloading ' + Math.round(100 * e.loaded / e.total) + '%');
	};

	mp3Request.onload = function(e) {
		$(inPlayerSelector).find('.status').text('decoding');
		console.log(mp3Request.response);

	    gAudioContext.decodeAudioData(mp3Request.response, function(decodedBuffer) {
	    	this.setSourceFromBuffer(decodedBuffer);
			$(inPlayerSelector).find('.status').text(Math.round(decodedBuffer.duration) + 's');
			$(inPlayerSelector).find('.recordingLocation').text(this.recording.loc);
			$(inPlayerSelector).find('.recordist').text(this.recording.rec);

			var licenseIcon = '';

			if (this.recording.lic.indexOf('by-nc-nd') > 0) {
				licenseIcon = 'http://i.creativecommons.org/l/by-nc-nd/3.0/us/88x31.png';
			} else if (this.recording.lic.indexOf('by-nc-sa') > 0) {
				licenseIcon = 'http://i.creativecommons.org/l/by-nc-sa/3.0/us/88x31.png';
			}

			$(inPlayerSelector).find('.license').attr('src', licenseIcon);

		}.bind(this));
	}.bind(this);

	mp3Request.open("GET", inSoundDataURL, true);
	mp3Request.responseType = 'arraybuffer';
	mp3Request.send();
}

BirdSongPlayer.prototype.chooseRandomRecording = function(inPlayerSelector) {
	if (this.soundsForSighting == null || this.soundsForSighting.recordings.length == 0) {
		$(inPlayerSelector).find('.status').text('retrying');
		console.log('FAILED loading recording for, retrying');
		this.chooseSightingAndPlayRandomSound(inPlayerSelector);
	} else {
		this.recordingIndex = Math.floor(Math.random() * this.soundsForSighting.recordings.length);
		this.recording = this.soundsForSighting.recordings[this.recordingIndex];

		console.log(this.recording);

		// rewrite URL's from xeno-canto JSON, route through my own server due to missing CORS
		var soundURL = this.recording.file.replace('http://www.xeno-canto.org','/soundfile');
		console.log(soundURL);

		$(inPlayerSelector).find('.status').text('downloading #' + this.recordingIndex);
		this.setBufferFromURL(soundURL, inPlayerSelector);
	}
}

BirdSongPlayer.prototype.chooseSightingAndPlayRandomSound = function(inPlayerSelector) {
	this.sightingIndex = gBirds.chooseRandomSighting();
	this.sighting = gBirds.sightings[this.sightingIndex];

	console.log('chooseSightingAndPlayRandomSound random sighting ' + this.sightingIndex);
	console.log(this.sighting);

	$(inPlayerSelector).find('.speciesName').text(this.sighting.comName);
	$(inPlayerSelector).find('.locationName').text(this.sighting.locName);
	$(inPlayerSelector).find('.status').text('choosing');

	// get sounds for this species if needed, and pick one at random
	gBirds.getSoundsForSightingIndex(this.sightingIndex, function(soundsData) {
		if (soundsData == null) {
			console.log('NO SOUNDS');
			$(inPlayerSelector).find('.status').text('retrying');
			this.chooseSightingAndPlayRandomSound(inPlayerSelector);
		} else {
			this.soundsForSighting = soundsData;
			this.chooseRandomRecording(inPlayerSelector);
		}
	}.bind(this));
}

<commit_msg>UI tweaks, add sighting # and improve error reporting<commit_after>function getAverageVolume(array) {
	var values = 0;
	var average;

	var length = array.length;

	// get all the frequency amplitudes
	for (var i = 0; i < length; i++) {
		values += array[i];
	}

	average = values / length;
	return average;
}

// BirdSongPlayer
function BirdSongPlayer(audioContext, inCanvasID) {
	this.soundSource = null;

	this.sighting = null;
	this.sightingIndex = null;
	this.soundsForSighting = null;

	this.recordingIndex = null;
	this.recording = null;

	this.gain = audioContext.createGain();

	// see https://developer.mozilla.org/en-US/docs/Web/API/PannerNode
	this.panner = audioContext.createPanner();
	this.panner.panningModel = 'HRTF';
	this.panner.distanceModel = 'inverse';
	this.panner.refDistance = 1;
	this.panner.maxDistance = 10000;
	this.panner.rolloffFactor = 1;
	this.panner.coneInnerAngle = 360;
	this.panner.coneOuterAngle = 0;
	this.panner.coneOuterGain = 0;
	this.panner.setOrientation(1,0,0);
	this.panner.setPosition(2 * Math.random() - 1, 2 * Math.random() - 1, 2 * Math.random() - 1);
	this.panner.setVelocity(0, 0, 0);

	this.gain.connect(this.panner);

	var volumeMeterCanvas = document.getElementById(inCanvasID);
	var graphicsContext = volumeMeterCanvas.getContext('2d');

	// setup a analyzer
	var analyser = audioContext.createAnalyser();
	this.panner.connect(analyser);
	this.panner.connect(audioContext.destination);

	analyser.smoothingTimeConstant = 0.3;
	analyser.fftSize = 64;

	requestAnimationFrame(function vuMeter() {
		// get the average, bincount is fftsize / 2
		var array =  new Uint8Array(analyser.frequencyBinCount);
		analyser.getByteFrequencyData(array);
		var average = getAverageVolume(array);
		average = Math.max(Math.min(average, 125), 5);

		// clear the current state
		graphicsContext.fillStyle = 'rgb(220,220,220)'
		graphicsContext.fillRect(1, 5, 130, 15);

		// set the fill style
		graphicsContext.fillStyle = 'rgb(0,0,0)'

		// create the meters
		graphicsContext.fillRect(1, 5, average, 15);

		requestAnimationFrame(vuMeter);
	});
}

// mutes or unmutes the sound by toggling the gain between 0 and 1
BirdSongPlayer.prototype.toggleMute = function() {
	if (this.gain.gain.value > 0) {
		console.log('muting');
		this.gain.gain.value = 0;
	} else {
		console.log('unmuting');
		this.gain.gain.value = 1;
	}
}

// sets the X,Y,Z position of the Panner to random values between -1 and +1
BirdSongPlayer.prototype.randomizePanner = function() {
	this.panner.setPosition(2 * Math.random() - 1, 2 * Math.random() - 1, 2 * Math.random() - 1);
}

// Sets the playback rate for the current sound to a random value between 0.1 and 1.1
BirdSongPlayer.prototype.randomizePlaybackRate = function(inPlayerSelector) {
	if (this.soundSource) {
		this.soundSource.playbackRate.value = 0.1 + Math.random();
		$(inPlayerSelector).find('.playbackRate').text((Math.round(100 * this.soundSource.playbackRate.value) / 100.0) + "x");
	} else {
		console.log('cannot randomize, no sound source');
	}
}

// Creates a new soundSource using the given buffer
// preserves the old playback rate, if applicable
BirdSongPlayer.prototype.setSourceFromBuffer = function(inBuffer) {
	console.log('setSourceFromBuffer');
	var oldPlaybackRateValue = 1;

	if (this.soundSource) {
		oldPlaybackRateValue = this.soundSource.playbackRate.value;
		this.soundSource.stop(0);
		this.soundSource = null;
	}

	this.soundSource = gAudioContext.createBufferSource();
	this.soundSource.connect(this.gain);
	this.soundSource.buffer = inBuffer;
	// restore old playback rate value
	this.soundSource.playbackRate.value = oldPlaybackRateValue;
	// start playing immediately in a loop
	this.soundSource.loop = true;
	this.soundSource.start(0);
}

// Reverses the audio buffer so that the sound plays backwards
BirdSongPlayer.prototype.reversePlayback = function() {
	console.log('reversePlayback');

	var oldData = this.soundSource.buffer.getChannelData(0);
	console.log(oldData);
	var normalData = Array.prototype.slice.call( oldData );
	Array.prototype.reverse.call(normalData);
	console.log(normalData);

	var newBuffer = gAudioContext.createBuffer(1, this.soundSource.buffer.length, this.soundSource.buffer.sampleRate);
	var newData = newBuffer.getChannelData(0);

	for (i = 0; i < newBuffer.length; i++) {
		newData[i] = normalData[i];
	}

	this.setSourceFromBuffer(newBuffer);

	console.log(this.soundSource.buffer.getChannelData(0));
	console.log('DONE reversePlayback');
}

BirdSongPlayer.prototype.setBufferFromURL = function(inSoundDataURL, inPlayerSelector) {
	console.log('setBufferFromURL ' + inSoundDataURL);

	var mp3Request = new XMLHttpRequest();

	mp3Request.onerror = function(e) {
		$(inPlayerSelector).find('.status').text('error downloading');
	};

	mp3Request.onprogress = function(e) {
		$(inPlayerSelector).find('.status').text('downloading ' + Math.round(100 * e.loaded / e.total) + '%');
	};

	mp3Request.onload = function(e) {
		$(inPlayerSelector).find('.status').text('decoding');
		console.log(mp3Request.response);

	    gAudioContext.decodeAudioData(mp3Request.response, function(decodedBuffer) {
	    	this.setSourceFromBuffer(decodedBuffer);
			$(inPlayerSelector).find('.status').text(Math.round(decodedBuffer.duration) + 's');
			$(inPlayerSelector).find('.recordingLocation').text(this.recording.loc);
			$(inPlayerSelector).find('.recordist').text(this.recording.rec);

			var licenseIcon = '';

			if (this.recording.lic.indexOf('by-nc-nd') > 0) {
				licenseIcon = 'http://i.creativecommons.org/l/by-nc-nd/3.0/us/88x31.png';
			} else if (this.recording.lic.indexOf('by-nc-sa') > 0) {
				licenseIcon = 'http://i.creativecommons.org/l/by-nc-sa/3.0/us/88x31.png';
			}

			$(inPlayerSelector).find('.license').attr('src', licenseIcon);

		}.bind(this));
	}.bind(this);

	mp3Request.open("GET", inSoundDataURL, true);
	mp3Request.responseType = 'arraybuffer';
	mp3Request.send();
}

BirdSongPlayer.prototype.chooseRandomRecording = function(inPlayerSelector) {
	if (this.soundsForSighting == null || this.soundsForSighting.recordings.length == 0) {
		$(inPlayerSelector).find('.status').text('retrying');
		console.log('FAILED loading recording for, retrying');
		this.chooseSightingAndPlayRandomSound(inPlayerSelector);
	} else {
		this.recordingIndex = Math.floor(Math.random() * this.soundsForSighting.recordings.length);
		this.recording = this.soundsForSighting.recordings[this.recordingIndex];

		console.log(this.recording);

		// rewrite URL's from xeno-canto JSON, route through my own server due to missing CORS
		var soundURL = this.recording.file.replace('http://www.xeno-canto.org','/soundfile');
		console.log(soundURL);

		$(inPlayerSelector).find('.status').text('downloading #' + this.recordingIndex);
		this.setBufferFromURL(soundURL, inPlayerSelector);
	}
}

BirdSongPlayer.prototype.chooseSightingAndPlayRandomSound = function(inPlayerSelector) {
	this.sightingIndex = gBirds.chooseRandomSighting();
	this.sighting = gBirds.sightings[this.sightingIndex];

	console.log('chooseSightingAndPlayRandomSound random sighting ' + this.sightingIndex);
	console.log(this.sighting);

	$(inPlayerSelector).find('.speciesName').text(this.sightingIndex + '. ' + this.sighting.comName);
	$(inPlayerSelector).find('.locationName').text(this.sighting.locName);
	$(inPlayerSelector).find('.status').text('choosing');

	// get sounds for this species if needed, and pick one at random
	gBirds.getSoundsForSightingIndex(this.sightingIndex, function(soundsData) {
		if (soundsData == null) {
			console.log('NO SOUNDS');
			$(inPlayerSelector).find('.status').text('retrying');
			this.chooseSightingAndPlayRandomSound(inPlayerSelector);
		} else {
			this.soundsForSighting = soundsData;
			this.chooseRandomRecording(inPlayerSelector);
		}
	}.bind(this));
}

<|endoftext|>


<commit_before>/* @flow weak */
/*global StringView, saveAs, $, _, fl */
"use babel";
"use strict";

let flux = require("flukes");

let actions = flux.createActions({
  handle_conflicts: function (newFiles, different, missing, ignored, tooBig) {
    return {
      newFiles: newFiles,
      different: different,
      missing: missing,
      ignored: ignored,
      tooBig: tooBig
    };
  },
  jump_to_user: function (username) {
    return username;
  },
  kick: function (connectionIDorUsername) {
    return connectionIDorUsername;
  },
  pref: function (name, value, force) {
    if (this.prefs[name] === value && !force) {
      return new Error("Preference " + name + " already has value " + value);
    }
    this.prefs[name] = value;
    this.prefs.save(fl.editor_settings.prefs_path);
    return [this.prefs, name];
  },
});

module.exports = new actions();
<commit_msg>linter<commit_after>/*global  fl */
"use strict";

const flux = require("flukes");

const Actions = flux.createActions({
  handle_conflicts: function (newFiles, different, missing, ignored, tooBig) {
    return {
      newFiles: newFiles,
      different: different,
      missing: missing,
      ignored: ignored,
      tooBig: tooBig
    };
  },
  jump_to_user: function (username) {
    return username;
  },
  kick: function (connectionIDorUsername) {
    return connectionIDorUsername;
  },
  pref: function (name, value, force) {
    if (this.prefs[name] === value && !force) {
      return new Error("Preference " + name + " already has value " + value);
    }
    this.prefs[name] = value;
    this.prefs.save(fl.editor_settings.prefs_path);
    return [this.prefs, name];
  },
});

module.exports = new Actions();
<|endoftext|>


<commit_before>if(!Draw.loadImage) {
  Draw.loadImage = function(imageName, onLoad) {
    if(!Canvas.configured) {
      console.warn("KAPhy Warning - You must use Canvas.configure(); before you can draw!");
      return;
    }

    var imageToLoad = new Image();
    imageToLoad.onload = onLoad;
    imageToLoad.src = Images[imageName];
  };
}
<commit_msg>Update loadimage.js<commit_after>if(!Draw.loadImage) {
  Draw.loadImage = function(imageName, onLoad) {
    if(!Canvas.configured) {
      console.warn("KAPhy Warning - You must use Canvas.configure(); before you can draw!");
      return;
    }

    if(arguments.length === 0 || arguments.length > 2) {
      console.warn("KAPhy Warning - Draw.loadImage() takes 1 or 2 arguments.");
      return;
    }

    var imageToLoad = new Image();
    imageToLoad.onload = onLoad;
    imageToLoad.src = Images[imageName];
  };
}
<|endoftext|>


<commit_before>define(['backbone'], function(Backbone) {

    return Backbone.Model.extend({
        idAttribute: 'CONTAINERID',
        urlRoot: '/shipment/containers',

        validation: {
            NAME: {
                required: true,
                pattern: 'wwdash',
            },

            CAPACITY: {
                required: true,
                pattern: 'number',
            },

            CONTAINERTYPE: {
                required: true,
                pattern: 'word',
            },

            SCHEDULEID: {
                required: false,
                pattern: 'number',
            }
        },

    })

})<commit_msg>new field in container<commit_after>define(['backbone'], function(Backbone) {

    return Backbone.Model.extend({
        idAttribute: 'CONTAINERID',
        urlRoot: '/shipment/containers',

        validation: {
            NAME: {
                required: true,
                pattern: 'wwdash',
            },

            CAPACITY: {
                required: true,
                pattern: 'number',
            },

            CONTAINERTYPE: {
                required: true,
                pattern: 'word',
            },

            SCHEDULEID: {
                required: false,
                pattern: 'number',
            },

            REQUESTEDIMAGERID: {
                required: false,
                pattern: 'number',
            },
        },

    })

})<|endoftext|>


<commit_before>Ext.define('Cetera.field.UserSet', {

    extend: 'Cetera.field.Set',

    onAddItem: function() {

        if (!this.usersList) {
            this.usersList = Ext.create('Cetera.users.Window');
            this.usersList.on('select', function(res) {
                this.addItem({id: res.id, name: res.name});
            },this);
        }
        this.usersList.show();

    },

    initComponent : function(){

        if (!this.tpl) this.tpl = '
{name}
';

        this.buttons = [{
            xtype  : 'button',
            iconCls: 'icon-new',
            tooltip: Config.Lang.add,
            handler: this.onAddItem,
            scope  : this
        },{
            xtype  : 'button',
            iconCls: 'icon-delete',
            tooltip: Config.Lang.remove,
            handler: this.removeItem,
            scope  : this
        }];

        this.callParent();
    }
});<commit_msg>BackOffice. Исправлена ошибка в редакторе "Набор пользователей"<commit_after>Ext.define('Cetera.field.UserSet', {

    extend: 'Cetera.field.Set',

    onAddItem: function() {

        if (!this.usersList) {
            this.usersList = Ext.create('Cetera.users.Window');
            this.usersList.on('select', function(res) {
                this.addItem({id: res.id, name: res.name});
            },this);
        }
        this.usersList.show();

    },

    initComponent : function(){

        if (!this.tpl) this.tpl = '
{name}
';

        this.callParent();
    },

	getButtons: function() {
        return [{
            xtype  : 'button',
            iconCls: 'icon-new',
            tooltip: Config.Lang.add,
            handler: this.onAddItem,
            scope  : this
        },{
            xtype  : 'button',
            iconCls: 'icon-delete',
            tooltip: Config.Lang.remove,
            handler: this.removeItem,
            scope  : this
        }];
	}
});<|endoftext|>


<commit_before>const tabClass = `${prefix}-tab`;
const chartId = `${prefix}-kwchart`;

window.setInterval(() => {
    let campaignTabs = $('#campaign_detail_tab_set');
    if (campaignTabs.length && campaignTabs.find(`.${tabClass}`).length == 0) {
        addCampaignTabs(campaignTabs);
    }
}, 100);

function updateKeyword(keywordIdList, operation, dataValues, cb) {
    let entityId = getEntityId();
    let keywordIds = keywordIdList.join(',');
    let postData = Object.assign({operation, entityId, keywordIds}, dataValues);
    return $.ajax({
        url: 'https://ams.amazon.com/api/sponsored-products/updateKeywords/',
        method: 'POST',
        data: postData,
        dataType: 'json',
        success: (data, textStatus, xhr) => cb(Object.assign(data, dataValues)),
        error: (xhr, error, status) => cb({error}),
    });
}

function updateStatus(keywordIdList, enable, cb) {
    let operation = enable ? "ENABLE" : "PAUSE";
    return updateKeyword(keywordIdList, operation, {}, cb);
}

function updateBid(keywordIdList, bid, cb) {
    return updateKeyword(keywordIdList, 'UPDATE', {bid}, cb);
}

function addCampaignTabs(tabs) {
    // Add in the analytics tab
    let a = $('Keyword Analytics');
    let li = $(`<li class="a-tab-heading ${tabClass}"></li>`);
    li.append(a);
    a.click(function(evt) {
        // Hide all of the body content except the one we want
        li.addClass('a-active');
        li.siblings().removeClass('a-active');
        tabs.parent().children('div').addClass('a-hidden');
        tabs.parent().find('#ams-unlocked-keyword-analysis').removeClass('a-hidden');
        $(`#${chartId}`).show();
    });
    $(tabs.children()[0]).after(li);

    // Fetch the url we want in order to actually embed it in the page
    $.ajax({
        url: chrome.runtime.getURL('campaign/keywordAnalytics.html'),
        success: (data, textStatus, xhr) => tabs.parent().append(data),
    });

    let adGroupId = $('input[name=adGroupId]')[0].value;

    getKeywordData(getEntityId(), adGroupId, (data) => {
        renderKeywordChart(transformKeywordData(data), {});
        /* TODO: excluding these until we decide if/when they're actually useful
        renderSpendPieChart(data);
        renderClicksHistogram(data);
        renderImpressionsHistogram(data);
        */

        // We often don't want to display data for points with very low numbers
        // of impressions, so we set a "minimum meaningful impressions" value at
        // 10% of what would be the value if all keywords had the same number of
        // impressions.
        let totalImpressions = data.reduce((acc, val) => acc + val.impressions, 0);
        let minImpressions = totalImpressions / (data.length * 10);

        let salesQuartileCutoff = data.sort((a, b) => a.sales - b.sales)[Math.round(data.length / 4)];

        renderKeywordTable(data, {
            tableSelector: '#ams-unlocked-acos',
            columnTitle: 'ACOS',
            order: 'desc',
            filterFn: (x) => x.clicks && x.acos > 100,
            metricFn: (x) => x.acos,
            formatFn: (x) => x ? `${x}%` : "(no sales)",
        });
        renderKeywordTable(data, {
            tableSelector: '#ams-unlocked-click-ratio',
            columnTitle: 'Clicks per 10K impressions',
            order: 'asc',
            filterFn: (x) => x.clicks && x.impressions > minImpressions,
            metricFn: (x) => x.clicks/x.impressions,
            formatFn: (x) => `${Math.round(x*10000)}`,
        });
        renderKeywordTable(data, {
            tableSelector: '#ams-unlocked-spend',
            columnTitle: 'Spend',
            order: 'desc',
            filterFn: (x) => x.clicks && !x.sales,
            metricFn: (x) => x.spend,
            formatFn: moneyFmt,
        });
        renderKeywordTable(data, {
            tableSelector: '#ams-unlocked-impressions',
            columnTitle: 'Impressions',
            order: 'asc',
            filterFn: (x) => x.impressions < minImpressions,
            metricFn: (x) => x.impressions,
            formatFn: (x) => x || 0,
        });
        renderKeywordTable(data, {
            tableSelector: '#ams-unlocked-high-click-ratio',
            columnTitle: 'Clicks per 10K impressions',
            order: 'desc',
            filterFn: (x) => x.clicks && x.impressions > minImpressions,
            metricFn: (x) => x.clicks/x.impressions,
            formatFn: (x) => `${Math.round(x*10000)}`,
        });
        renderKeywordTable(data, {
            tableSelector: '#ams-unlocked-low-acos',
            columnTitle: 'ACOS',
            order: 'asc',
            filterFn: (x) => x.sales && x.acos < 100 && x.acos > 0,
            metricFn: (x) => x.acos,
            formatFn: (x) => `${x}%`,
        });
        renderKeywordTable(data, {
            tableSelector: '#ams-unlocked-high-sales',
            columnTitle: 'Sales',
            order: 'desc',
            filterFn: (x) => x.sales && x.sales >= salesQuartileCutoff.sales,
            metricFn: (x) => x.sales,
            formatFn: moneyFmt,
        });
    });
}

function renderSpendPieChart(data) {
    let target = 100;
    let sumSpend = (acc, x) => acc + x.spend;
    let spendOverTarget = data.filter(x => x.sales && x.acos > target).reduce(sumSpend, 0);
    let spendUnderTarget = data.filter(x => x.sales && x.acos <= target).reduce(sumSpend, 0);
    let spendNoSales = data.filter(x => !x.sales).reduce(sumSpend, 0);

    let chartData = {
        values: [spendOverTarget, spendUnderTarget, spendNoSales],
        labels: ['Keywords with ACOS over 100%', 'Keywords with ACOS under 100%', 'Keywords without sales'],
        type: 'pie',
    };

    Plotly.plot('ams-unlocked-spend-pie', [chartData], {height: 400, width: 400, showlegend: false});
}

function bucketize(data, binKey, valueKey) {
    let bins = {};
    for (let item of data) {
        let key = +item[binKey];
        let value = +item[valueKey];
        if (Number.isNaN(key) || Number.isNaN(value))
            continue;
        if (!bins[key])
            bins[key] = 0;
        bins[key] += value;
    }

    let binData = Object.keys(bins).map(x => parseFloat(x));
    let binValues = binData.map(x => bins[x]);

    return {x: binData, y: binValues};
}

function renderClicksHistogram(data) {
    let clickBins = bucketize(data, 'avgCpc', 'clicks');
    let chartData = {
        x: clickBins.x,
        y: clickBins.y,
        type: 'bar',
        marker: { color: 'lightblue' },
    };

    Plotly.plot('ams-unlocked-clicks-histo', [chartData], {height: 400, width: 400, showlegend: false});
}

function renderImpressionsHistogram(data) {
    let clickBins = bucketize(data.filter(x => x.avgCpc), 'avgCpc', 'impressions');
    let chartData = {
        x: clickBins.x,
        y: clickBins.y,
        type: 'bar',
        marker: { color: 'lightblue' },
    };

    Plotly.plot('ams-unlocked-impressions-histo', [chartData], {height: 400, width: 400, showlegend: false});
}

function getKeywordData(entityId, adGroupId, cb) {
    chrome.runtime.sendMessage({
        action: 'getKeywordData', // from our server
        entityId: entityId,
        adGroupId: adGroupId,
    },
    (response) => {
        // If we have data, return it immediately
        if (response.data.length) {
            cb(response.data);
        }

        // After querying our own (fast) servers, query Amazon. TODO: this is
        // gonna get moved to the cloud
        chrome.runtime.sendMessage({
            action: 'requestKeywordData',
            entityId: entityId,
            adGroupId: adGroupId,
        },
        (amsResponse) => {
            if (!response.data.length) {
                // Try our servers again
                chrome.runtime.sendMessage({
                    action: 'getKeywordData', // from our server
                    entityId: entityId,
                    adGroupId: adGroupId,
                },
                (response) => cb(response.data));
            }
        });
    });
};

function transformKeywordData(data, opt) {
    let kws = {
        kw: [],
        impressions: [],
        spend: [],
        sales: [],
        clicks: [],
        acos: [],
        avgCpc: [],
    };
    for (let k of data) {
        kws.kw.push(k.keyword);
        kws.impressions.push(k.impressions);
        kws.clicks.push(k.clicks);
        kws.spend.push(k.spend);
        kws.sales.push(k.sales);
        kws.acos.push(k.acos);
        kws.avgCpc.push(k.avgCpc);
    }

    return kws;
}

function renderKeywordChart(kws, opt) {
    let chartData = {
        mode: 'markers',
        x: kws.avgCpc,
        y: kws.clicks,
        text: kws.kw.map((kw, i) =>
            `"${kw}"<br />Impressions: ${kws.impressions[i]}<br />Clicks: ${kws.clicks[i]}<br />Avg CPC: $${moneyFmt(kws.avgCpc[i])}<br />Avg COS: ${kws.acos[i]}%`),
        hoverinfo: 'text',
        marker: {
            sizemode: 'diameter',
            size: kws.impressions.map(x => (Math.log2(x)+1) * 2),
            color: kws.acos,
            colorscale: [[0, 'rgb(0, 255, 0)'], [0.5, 'rgb(255, 255, 0)'], [1, 'rgb(255, 0, 0)']],
        },
    };
    let layout = {
        xaxis: {title: 'Average CPC'},
        yaxis: {title: 'Number of clicks'},
        margin: {t: 20},
        height: 600,
        width: $('.a-box-inner').width(),
        hovermode: 'closest'
    };
    Plotly.plot(chartId, [chartData], layout, {showLink: false});
}

function renderKeywordTable(data, opts) {
    data = data.filter(opts.filterFn ? opts.filterFn : x => true);

    let formatFn = opts.formatFn ? opts.formatFn : x => x;
    data = data.map(x => [
        x.keyword,
        formatFn(opts.metricFn(x)),
        renderKeywordStatus(x),
        renderKeywordBid(x),
    ]);

    $(opts.tableSelector).DataTable({
        data: data,
        order: [[1, opts.order || 'asc']],
        columns: [
            { title: "Keyword" },
            { title: opts.columnTitle },
            { title: "Status" },
            { title: "Bid" },
        ],
    });

    $(opts.tableSelector).width('100%'); // TODO: figure out why DataTables is setting this to 0
}

$(document).on('click', '.ams-unlocked-kwstatus', function() {
    let keyword = JSON.parse($(this).attr('data-ams-unlocked-keyword'));
    $(this).find('.a-button').hide();
    $(this).find('.loading-small').show();
    updateStatus([keyword.id], !keyword.enabled, (result) => {
        if (result.success) {
            keyword.enabled = !keyword.enabled;
            renderKeywordStatus(keyword, $(this));
        }
        else {
            console.error('problems updating status:', result);
        }
        $(this).find('.a-button').show();
        $(this).find('.loading-small').hide();
    });
});

$(document).on('click', '.ams-unlocked-kwbid input[name=save]', function() {
    let cell = $(this).parents('.ams-unlocked-kwbid');
    let keyword = JSON.parse(cell.attr('data-ams-unlocked-keyword'));
    let input = cell.find('input[name=keyword-bid]');
    cell.children().hide();
    cell.find('.loading-small').show();
    updateBid([keyword.id], input.val(), (result) => {
        if (result.success) {
            keyword.bid = result.bid;
            renderKeywordBid(keyword, cell);
        }
        else {
            console.error('problems updating status:', result);
        }
        cell.children().show();
        cell.find('.loading-small').hide();
    });
});

function renderKeywordBid(keyword, cell) {
    cell = cell || $('#ams-unlocked-kwbid').clone();
    cell.removeAttr('id');
    cell.attr('data-ams-unlocked-keyword', JSON.stringify(keyword));

    cell.find('input[name=keyword-bid]')
        .attr('value', keyword.bid);

    return cell[0].outerHTML;
}

function renderKeywordStatus(keyword, cell) {
    cell = cell || $('#ams-unlocked-kwstatus').clone();
    cell.removeAttr('id');
    cell.attr('data-ams-unlocked-keyword', JSON.stringify(keyword));

    let statusImg = cell.find('.ams-dropdown-status');
    let statusTxt = cell.find('.ams-unlocked-kwstatus-current');
    if (keyword.enabled) {
        statusImg.addClass('ams-status-active');
        statusImg.removeClass('ams-status-paused');
        statusTxt.text('Enabled');
    }
    else {
        statusImg.removeClass('ams-status-active');
        statusImg.addClass('ams-status-paused');
        statusTxt.text('Paused');
    }

    return cell[0].outerHTML;
}
<commit_msg>removed extra dollar sign from bubble chart<commit_after>const tabClass = `${prefix}-tab`;
const chartId = `${prefix}-kwchart`;

window.setInterval(() => {
    let campaignTabs = $('#campaign_detail_tab_set');
    if (campaignTabs.length && campaignTabs.find(`.${tabClass}`).length == 0) {
        addCampaignTabs(campaignTabs);
    }
}, 100);

function updateKeyword(keywordIdList, operation, dataValues, cb) {
    let entityId = getEntityId();
    let keywordIds = keywordIdList.join(',');
    let postData = Object.assign({operation, entityId, keywordIds}, dataValues);
    return $.ajax({
        url: 'https://ams.amazon.com/api/sponsored-products/updateKeywords/',
        method: 'POST',
        data: postData,
        dataType: 'json',
        success: (data, textStatus, xhr) => cb(Object.assign(data, dataValues)),
        error: (xhr, error, status) => cb({error}),
    });
}

function updateStatus(keywordIdList, enable, cb) {
    let operation = enable ? "ENABLE" : "PAUSE";
    return updateKeyword(keywordIdList, operation, {}, cb);
}

function updateBid(keywordIdList, bid, cb) {
    return updateKeyword(keywordIdList, 'UPDATE', {bid}, cb);
}

function addCampaignTabs(tabs) {
    // Add in the analytics tab
    let a = $('Keyword Analytics');
    let li = $(`<li class="a-tab-heading ${tabClass}"></li>`);
    li.append(a);
    a.click(function(evt) {
        // Hide all of the body content except the one we want
        li.addClass('a-active');
        li.siblings().removeClass('a-active');
        tabs.parent().children('div').addClass('a-hidden');
        tabs.parent().find('#ams-unlocked-keyword-analysis').removeClass('a-hidden');
        $(`#${chartId}`).show();
    });
    $(tabs.children()[0]).after(li);

    // Fetch the url we want in order to actually embed it in the page
    $.ajax({
        url: chrome.runtime.getURL('campaign/keywordAnalytics.html'),
        success: (data, textStatus, xhr) => tabs.parent().append(data),
    });

    let adGroupId = $('input[name=adGroupId]')[0].value;

    getKeywordData(getEntityId(), adGroupId, (data) => {
        renderKeywordChart(transformKeywordData(data), {});
        /* TODO: excluding these until we decide if/when they're actually useful
        renderSpendPieChart(data);
        renderClicksHistogram(data);
        renderImpressionsHistogram(data);
        */

        // We often don't want to display data for points with very low numbers
        // of impressions, so we set a "minimum meaningful impressions" value at
        // 10% of what would be the value if all keywords had the same number of
        // impressions.
        let totalImpressions = data.reduce((acc, val) => acc + val.impressions, 0);
        let minImpressions = totalImpressions / (data.length * 10);

        let salesQuartileCutoff = data.sort((a, b) => a.sales - b.sales)[Math.round(data.length / 4)];

        renderKeywordTable(data, {
            tableSelector: '#ams-unlocked-acos',
            columnTitle: 'ACOS',
            order: 'desc',
            filterFn: (x) => x.clicks && x.acos > 100,
            metricFn: (x) => x.acos,
            formatFn: (x) => x ? `${x}%` : "(no sales)",
        });
        renderKeywordTable(data, {
            tableSelector: '#ams-unlocked-click-ratio',
            columnTitle: 'Clicks per 10K impressions',
            order: 'asc',
            filterFn: (x) => x.clicks && x.impressions > minImpressions,
            metricFn: (x) => x.clicks/x.impressions,
            formatFn: (x) => `${Math.round(x*10000)}`,
        });
        renderKeywordTable(data, {
            tableSelector: '#ams-unlocked-spend',
            columnTitle: 'Spend',
            order: 'desc',
            filterFn: (x) => x.clicks && !x.sales,
            metricFn: (x) => x.spend,
            formatFn: moneyFmt,
        });
        renderKeywordTable(data, {
            tableSelector: '#ams-unlocked-impressions',
            columnTitle: 'Impressions',
            order: 'asc',
            filterFn: (x) => x.impressions < minImpressions,
            metricFn: (x) => x.impressions,
            formatFn: (x) => x || 0,
        });
        renderKeywordTable(data, {
            tableSelector: '#ams-unlocked-high-click-ratio',
            columnTitle: 'Clicks per 10K impressions',
            order: 'desc',
            filterFn: (x) => x.clicks && x.impressions > minImpressions,
            metricFn: (x) => x.clicks/x.impressions,
            formatFn: (x) => `${Math.round(x*10000)}`,
        });
        renderKeywordTable(data, {
            tableSelector: '#ams-unlocked-low-acos',
            columnTitle: 'ACOS',
            order: 'asc',
            filterFn: (x) => x.sales && x.acos < 100 && x.acos > 0,
            metricFn: (x) => x.acos,
            formatFn: (x) => `${x}%`,
        });
        renderKeywordTable(data, {
            tableSelector: '#ams-unlocked-high-sales',
            columnTitle: 'Sales',
            order: 'desc',
            filterFn: (x) => x.sales && x.sales >= salesQuartileCutoff.sales,
            metricFn: (x) => x.sales,
            formatFn: moneyFmt,
        });
    });
}

function renderSpendPieChart(data) {
    let target = 100;
    let sumSpend = (acc, x) => acc + x.spend;
    let spendOverTarget = data.filter(x => x.sales && x.acos > target).reduce(sumSpend, 0);
    let spendUnderTarget = data.filter(x => x.sales && x.acos <= target).reduce(sumSpend, 0);
    let spendNoSales = data.filter(x => !x.sales).reduce(sumSpend, 0);

    let chartData = {
        values: [spendOverTarget, spendUnderTarget, spendNoSales],
        labels: ['Keywords with ACOS over 100%', 'Keywords with ACOS under 100%', 'Keywords without sales'],
        type: 'pie',
    };

    Plotly.plot('ams-unlocked-spend-pie', [chartData], {height: 400, width: 400, showlegend: false});
}

function bucketize(data, binKey, valueKey) {
    let bins = {};
    for (let item of data) {
        let key = +item[binKey];
        let value = +item[valueKey];
        if (Number.isNaN(key) || Number.isNaN(value))
            continue;
        if (!bins[key])
            bins[key] = 0;
        bins[key] += value;
    }

    let binData = Object.keys(bins).map(x => parseFloat(x));
    let binValues = binData.map(x => bins[x]);

    return {x: binData, y: binValues};
}

function renderClicksHistogram(data) {
    let clickBins = bucketize(data, 'avgCpc', 'clicks');
    let chartData = {
        x: clickBins.x,
        y: clickBins.y,
        type: 'bar',
        marker: { color: 'lightblue' },
    };

    Plotly.plot('ams-unlocked-clicks-histo', [chartData], {height: 400, width: 400, showlegend: false});
}

function renderImpressionsHistogram(data) {
    let clickBins = bucketize(data.filter(x => x.avgCpc), 'avgCpc', 'impressions');
    let chartData = {
        x: clickBins.x,
        y: clickBins.y,
        type: 'bar',
        marker: { color: 'lightblue' },
    };

    Plotly.plot('ams-unlocked-impressions-histo', [chartData], {height: 400, width: 400, showlegend: false});
}

function getKeywordData(entityId, adGroupId, cb) {
    chrome.runtime.sendMessage({
        action: 'getKeywordData', // from our server
        entityId: entityId,
        adGroupId: adGroupId,
    },
    (response) => {
        // If we have data, return it immediately
        if (response.data.length) {
            cb(response.data);
        }

        // After querying our own (fast) servers, query Amazon. TODO: this is
        // gonna get moved to the cloud
        chrome.runtime.sendMessage({
            action: 'requestKeywordData',
            entityId: entityId,
            adGroupId: adGroupId,
        },
        (amsResponse) => {
            if (!response.data.length) {
                // Try our servers again
                chrome.runtime.sendMessage({
                    action: 'getKeywordData', // from our server
                    entityId: entityId,
                    adGroupId: adGroupId,
                },
                (response) => cb(response.data));
            }
        });
    });
};

function transformKeywordData(data, opt) {
    let kws = {
        kw: [],
        impressions: [],
        spend: [],
        sales: [],
        clicks: [],
        acos: [],
        avgCpc: [],
    };
    for (let k of data) {
        kws.kw.push(k.keyword);
        kws.impressions.push(k.impressions);
        kws.clicks.push(k.clicks);
        kws.spend.push(k.spend);
        kws.sales.push(k.sales);
        kws.acos.push(k.acos);
        kws.avgCpc.push(k.avgCpc);
    }

    return kws;
}

function renderKeywordChart(kws, opt) {
    let chartData = {
        mode: 'markers',
        x: kws.avgCpc,
        y: kws.clicks,
        text: kws.kw.map((kw, i) =>
            `"${kw}"<br />Impressions: ${kws.impressions[i]}<br />Clicks: ${kws.clicks[i]}<br />Avg CPC: ${moneyFmt(kws.avgCpc[i])}<br />Avg COS: ${kws.acos[i]}%`),
        hoverinfo: 'text',
        marker: {
            sizemode: 'diameter',
            size: kws.impressions.map(x => (Math.log2(x)+1) * 2),
            color: kws.acos,
            colorscale: [[0, 'rgb(0, 255, 0)'], [0.5, 'rgb(255, 255, 0)'], [1, 'rgb(255, 0, 0)']],
        },
    };
    let layout = {
        xaxis: {title: 'Average CPC'},
        yaxis: {title: 'Number of clicks'},
        margin: {t: 20},
        height: 600,
        width: $('.a-box-inner').width(),
        hovermode: 'closest'
    };
    Plotly.plot(chartId, [chartData], layout, {showLink: false});
}

function renderKeywordTable(data, opts) {
    data = data.filter(opts.filterFn ? opts.filterFn : x => true);

    let formatFn = opts.formatFn ? opts.formatFn : x => x;
    data = data.map(x => [
        x.keyword,
        formatFn(opts.metricFn(x)),
        renderKeywordStatus(x),
        renderKeywordBid(x),
    ]);

    $(opts.tableSelector).DataTable({
        data: data,
        order: [[1, opts.order || 'asc']],
        columns: [
            { title: "Keyword" },
            { title: opts.columnTitle },
            { title: "Status" },
            { title: "Bid" },
        ],
    });

    $(opts.tableSelector).width('100%'); // TODO: figure out why DataTables is setting this to 0
}

$(document).on('click', '.ams-unlocked-kwstatus', function() {
    let keyword = JSON.parse($(this).attr('data-ams-unlocked-keyword'));
    $(this).find('.a-button').hide();
    $(this).find('.loading-small').show();
    updateStatus([keyword.id], !keyword.enabled, (result) => {
        if (result.success) {
            keyword.enabled = !keyword.enabled;
            renderKeywordStatus(keyword, $(this));
        }
        else {
            console.error('problems updating status:', result);
        }
        $(this).find('.a-button').show();
        $(this).find('.loading-small').hide();
    });
});

$(document).on('click', '.ams-unlocked-kwbid input[name=save]', function() {
    let cell = $(this).parents('.ams-unlocked-kwbid');
    let keyword = JSON.parse(cell.attr('data-ams-unlocked-keyword'));
    let input = cell.find('input[name=keyword-bid]');
    cell.children().hide();
    cell.find('.loading-small').show();
    updateBid([keyword.id], input.val(), (result) => {
        if (result.success) {
            keyword.bid = result.bid;
            renderKeywordBid(keyword, cell);
        }
        else {
            console.error('problems updating status:', result);
        }
        cell.children().show();
        cell.find('.loading-small').hide();
    });
});

function renderKeywordBid(keyword, cell) {
    cell = cell || $('#ams-unlocked-kwbid').clone();
    cell.removeAttr('id');
    cell.attr('data-ams-unlocked-keyword', JSON.stringify(keyword));

    cell.find('input[name=keyword-bid]')
        .attr('value', keyword.bid);

    return cell[0].outerHTML;
}

function renderKeywordStatus(keyword, cell) {
    cell = cell || $('#ams-unlocked-kwstatus').clone();
    cell.removeAttr('id');
    cell.attr('data-ams-unlocked-keyword', JSON.stringify(keyword));

    let statusImg = cell.find('.ams-dropdown-status');
    let statusTxt = cell.find('.ams-unlocked-kwstatus-current');
    if (keyword.enabled) {
        statusImg.addClass('ams-status-active');
        statusImg.removeClass('ams-status-paused');
        statusTxt.text('Enabled');
    }
    else {
        statusImg.removeClass('ams-status-active');
        statusImg.addClass('ams-status-paused');
        statusTxt.text('Paused');
    }

    return cell[0].outerHTML;
}
<|endoftext|>


<commit_before>import { LightningElement, api, track, wire } from 'lwc';
import { fireEvent } from 'c/pubsubNoPageRef';
import { ShowToastEvent } from 'lightning/platformShowToastEvent';

import { getNestedProperty } from 'c/utilCommon';
import { getObjectInfo } from 'lightning/uiObjectInfoApi';
import { getRecord} from 'lightning/uiRecordApi';

import RECURRING_DONATION_INFO from '@salesforce/schema/npe03__Recurring_Donation__c';
import ACCOUNT_OBJECT from '@salesforce/schema/Account';
import CONTACT_OBJECT from '@salesforce/schema/Contact';

import FIELD_NAME from '@salesforce/schema/npe03__Recurring_Donation__c.Name';
import FIELD_ACCOUNT from '@salesforce/schema/npe03__Recurring_Donation__c.npe03__Organization__c';
import FIELD_CONTACT from '@salesforce/schema/npe03__Recurring_Donation__c.npe03__Contact__c';
import FIELD_DATE_ESTABLISHED from '@salesforce/schema/npe03__Recurring_Donation__c.npe03__Date_Established__c';
import FIELD_STATUS from '@salesforce/schema/npe03__Recurring_Donation__c.Status__c';
import FIELD_RECURRING_TYPE from '@salesforce/schema/npe03__Recurring_Donation__c.RecurringType__c';
import FIELD_INSTALLMENT from '@salesforce/schema/npe03__Recurring_Donation__c.npe03__Installments__c';
import FIELD_AMOUNT from '@salesforce/schema/npe03__Recurring_Donation__c.npe03__Amount__c';
import FIELD_PAYMENT_METHOD from '@salesforce/schema/npe03__Recurring_Donation__c.PaymentMethod__c';
import FIELD_INSTALLMENT_PERIOD from '@salesforce/schema/npe03__Recurring_Donation__c.npe03__Installment_Period__c';
import FIELD_INSTALLMENT_FREQUENCY from '@salesforce/schema/npe03__Recurring_Donation__c.InstallmentFrequency__c';
import FIELD_DAY_OF_MONTH from '@salesforce/schema/npe03__Recurring_Donation__c.Day_of_Month__c';
import FIELD_START_DATE from '@salesforce/schema/npe03__Recurring_Donation__c.StartDate__c';
import FIELD_CAMPAIGN from '@salesforce/schema/npe03__Recurring_Donation__c.npe03__Recurring_Donation_Campaign__c';

import cancelButtonLabel from '@salesforce/label/c.stgBtnCancel';
import saveButtonLabel from '@salesforce/label/c.stgBtnSave';
import unknownErrorLabel from '@salesforce/label/c.commonUnknownError';
import newHeaderLabel from '@salesforce/label/c.RD2_EntryFormHeader';
import editHeaderLabel from '@salesforce/label/c.commonEdit';
import currencyFieldLabel from '@salesforce/label/c.lblCurrency';
import donationSectionHeader from '@salesforce/label/c.RD2_EntryFormDonationSectionHeader';
import donorSectionHeader from '@salesforce/label/c.RD2_EntryFormDonorSectionHeader';
import otherSectionHeader from '@salesforce/label/c.RD2_EntryFormOtherSectionHeader';
import insertSuccessMessage from '@salesforce/label/c.RD2_EntryFormInsertSuccessMessage';
import updateSuccessMessage from '@salesforce/label/c.RD2_EntryFormUpdateSuccessMessage';

import getSetting from '@salesforce/apex/RD2_entryFormController.getSetting';

export default class rdEntryForm extends LightningElement {

    @api parentId;
    @api recordId;
    listenerEvent = 'rd2EntryFormEvent';

    customLabels = Object.freeze({
        cancelButtonLabel,
        saveButtonLabel,
        donationSectionHeader,
        donorSectionHeader,
        otherSectionHeader
    });

    @track fields = {};
    @track record;
    recurringDonationInfo;
    fieldInfos;

    @track header = newHeaderLabel;

    @track isAutoNamingEnabled;
    @track isMultiCurrencyEnabled;

    @track contactId;
    @track accountId;

    @track isLoading = true;
    @track isRecordReady = false;
    isSettingReady = false;

    @track hasError = false;
    @track errorMessage = {};

    /*******************************************************************************
    * @description Dynamic render edit form CSS to show/hide the edit form
    */
    get cssEditForm() {
        return (!this.isLoading && this.isSettingReady && this.isRecordReady)
            ? ''
            : 'slds-hide';
    }

    /*******************************************************************************
    * @description Get Recurring Donation SObject API name
    */
    get recurringDonationName() {
        return getNestedProperty(RECURRING_DONATION_INFO, 'objectApiName');
    }

    /*******************************************************************************
    * @description Retrieve Recurring Donation Object info
    */
    @wire(getObjectInfo, {objectApiName: '$recurringDonationName' })
    wiredRDObjectInfo(response) {
        if (response.data) {
            this.recurringDonationInfo = response.data;
            this.buildDisplayedFields(this.recurringDonationInfo.fields);
            this.fieldInfos = this.buildFieldDescribesForWiredMethod(
                this.recurringDonationInfo.fields,
                this.recurringDonationInfo.apiName);

            if (this.recordId == null) {
                this.isRecordReady = true;
            }
        }

        if (response.error) {
            this.isRecordReady = true;
            handleError(response.error);
        }
    }

    /*******************************************************************************
    * @description Method converts field describe info into objects that the
    * getRecord method can accept into its 'fields' parameter.
    */
    buildFieldDescribesForWiredMethod(fields, objectApiName) {
        return Object.keys(fields).map((fieldApiName) => {
            return {
                fieldApiName: fieldApiName,
                objectApiName: objectApiName
            }
        });
    }

    /*******************************************************************************
    * @description Contrcut any needed field info from the Sobject Info
    */
    buildDisplayedFields(fieldInfos) {
        this.fields.name = this.extractFieldInfo(fieldInfos[FIELD_NAME.fieldApiName]);
        this.fields.account = this.extractFieldInfo(fieldInfos[FIELD_ACCOUNT.fieldApiName]);
        this.fields.contact = this.extractFieldInfo(fieldInfos[FIELD_CONTACT.fieldApiName]);
        this.fields.recurringType = this.extractFieldInfo(fieldInfos[FIELD_RECURRING_TYPE.fieldApiName]);
        this.fields.dateEstablished = this.extractFieldInfo(fieldInfos[FIELD_DATE_ESTABLISHED.fieldApiName]);
        this.fields.status = this.extractFieldInfo(fieldInfos[FIELD_STATUS.fieldApiName]);
        this.fields.amount = this.extractFieldInfo(fieldInfos[FIELD_AMOUNT.fieldApiName]);
        this.fields.paymentMethod = this.extractFieldInfo(fieldInfos[FIELD_PAYMENT_METHOD.fieldApiName]);
        this.fields.period = this.extractFieldInfo(fieldInfos[FIELD_INSTALLMENT_PERIOD.fieldApiName]);
        this.fields.installmentFrequency = this.extractFieldInfo(fieldInfos[FIELD_INSTALLMENT_FREQUENCY.fieldApiName]);
        this.fields.dayOfMonth = this.extractFieldInfo(fieldInfos[FIELD_DAY_OF_MONTH.fieldApiName]);
        this.fields.startDate = this.extractFieldInfo(fieldInfos[FIELD_START_DATE.fieldApiName]);
        this.fields.campaign = this.extractFieldInfo(fieldInfos[FIELD_CAMPAIGN.fieldApiName]);
        this.fields.currency = {label: currencyFieldLabel, apiName: 'CurrencyIsoCode'};
        this.fields.installment = this.extractFieldInfo(fieldInfos[FIELD_INSTALLMENT.fieldApiName]);
    }

    /*******************************************************************************
    * @description Method converts field describe info into a object that is easily accessiable from the front end
    */
    extractFieldInfo(field) {
        return {
            apiName : field.apiName,
            label : field.label,
            inlineHelpText : field.inlineHelpText,
            dataType : field.dataType
        };
    }

    /*******************************************************************************
    * @description Retrieve Recurring Donation record value
    */
    @wire(getRecord, { recordId: '$recordId', fields: '$fieldInfos' })
    wiredRecurringDonationRecord(response) {
        if (response.data) {
            this.record = response.data;
            this.modidifyEditHeader();
            this.isRecordReady = true;
        }

        if (response.error) {
            this.isRecordReady = true;
            handleError(response.error);
        }
    }

    /*******************************************************************************
    * @description Modify edit modal header
    */
    modidifyEditHeader() {
        this.header = editHeaderLabel + ' ' + this.record.fields.Name.value;
    }

    /*******************************************************************************
    * @description Get org setting on init of the component
    */
    connectedCallback() {
        getSetting({parentId: this.parentId})
        .then(response => {
            this.isAutoNamingEnabled = response.isAutoNamingEnabled;
            this.isMultiCurrencyEnabled = response.isMultiCurrencyEnabled;
            this.handleParentIdType(response.parentSObjectType);
            this.isSettingReady = true;
            this.isLoading = false;
        })
        .catch((error) => {
            this.isSettingReady = true;
            this.handleError(error);
        });
    }

    /*******************************************************************************
    * @description Determine the parentId Sobject Type
    */
    handleParentIdType(parentSObjecType) {
        if (parentSObjecType == null) {
            return;
        }

        if (parentSObjecType === ACCOUNT_OBJECT.objectApiName) {
            this.accountId = this.parentId;
        } else if (parentSObjecType === CONTACT_OBJECT.objectApiName) {
            this.contactId = this.parentId;
        }
    }

    /*******************************************************************************
    * @description Override the standard submit. Add any fields before the submittion
    */
    handleSubmit(event){
        this.isLoading = true;
        this.template.querySelector("[data-id='submitButton']").disabled = true;

        event.preventDefault();
        const fields = event.detail.fields;
        this.template.querySelector('lightning-record-edit-form').submit(fields);
    }

    /*******************************************************************************
    * @description Handle lwc when errors occur
    */
    handleError(error) {
        this.errorMessage = this.constructErrorMessage(error);
        this.hasError = true;
        this.isLoading = false;

        this.template.querySelector("[data-id='submitButton']").disabled = false;

        this.template.querySelector(".slds-modal__header").scrollIntoView();
    }

    /*******************************************************************************
    * @description Fires an event to utilDedicatedListener with the cancel action
    */
    handleCancel() {
        fireEvent(this.pageRef, this.listenerEvent, { action: 'cancel', recordId : this.recordId });
    }

    /*******************************************************************************
    * @description Fires an event to utilDedicatedListener with the success action
    */
    handleSuccess(event) {
        this.showSuccessToast(event.detail.fields.Name.value);
        fireEvent(this.pageRef, this.listenerEvent, { action: 'success', recordId: event.detail.id});
    }

    /*******************************************************************************
    * @description Contruct error wrapper from the error event
    *   error.body is the error from apex calls
    *   error.body.output.errors is for AuraHandledException messages
    *   error.body.message errors is the error from wired service
    *   error.detail.output.errors is the error from record-edit-forms
    */
   constructErrorMessage(error) {
        let header;
        let message;

        if (typeof error === 'string' || error instanceof String) {
            message = error;

        } else if (error.message) {
            message = error.message;

        } else if ((error.body && error.body.output)) {
            if (Array.isArray(error.body) &&
                !error.body.output.errors) {
                message = error.body.map(e => e.message).join(', ');

            } else if (typeof error.body.message === 'string' &&
                !error.body.output.errors) {
                message = error.body.message;

            } else if (error.body.output &&
                Array.isArray(error.body.output.errors)) {
                message = error.body.output.errors.map(e => e.message).join(', ');
            }

        } else if (error.detail && error.detail.output && Array.isArray(error.detail.output.errors)) {
            header = error.detail.message;
            message = error.detail.output.errors.map(e => e.message).join(', ');

        } else if (error.body && error.body.message) {
            message = error.body.message;
        }

        return {
            header: header || unknownErrorLabel,
            detail: message || unknownErrorLabel
        };
    }

    /*******************************************************************************
    * @description Fires a toast event to display success message
    */
    showSuccessToast(recordName) {
        let succeedMessage = (this.recordId)
            ? updateSuccessMessage.replace("{0}", recordName)
            : insertSuccessMessage.replace("{0}", recordName);

        const event = new ShowToastEvent({
            title: succeedMessage,
            variant: 'success',
        });
        this.dispatchEvent(event);
    }
}<commit_msg>renmae to rd2<commit_after>import { LightningElement, api, track, wire } from 'lwc';
import { fireEvent } from 'c/pubsubNoPageRef';
import { ShowToastEvent } from 'lightning/platformShowToastEvent';

import { getNestedProperty } from 'c/utilCommon';
import { getObjectInfo } from 'lightning/uiObjectInfoApi';
import { getRecord} from 'lightning/uiRecordApi';

import RECURRING_DONATION_INFO from '@salesforce/schema/npe03__Recurring_Donation__c';
import ACCOUNT_OBJECT from '@salesforce/schema/Account';
import CONTACT_OBJECT from '@salesforce/schema/Contact';

import FIELD_NAME from '@salesforce/schema/npe03__Recurring_Donation__c.Name';
import FIELD_ACCOUNT from '@salesforce/schema/npe03__Recurring_Donation__c.npe03__Organization__c';
import FIELD_CONTACT from '@salesforce/schema/npe03__Recurring_Donation__c.npe03__Contact__c';
import FIELD_DATE_ESTABLISHED from '@salesforce/schema/npe03__Recurring_Donation__c.npe03__Date_Established__c';
import FIELD_STATUS from '@salesforce/schema/npe03__Recurring_Donation__c.Status__c';
import FIELD_RECURRING_TYPE from '@salesforce/schema/npe03__Recurring_Donation__c.RecurringType__c';
import FIELD_INSTALLMENT from '@salesforce/schema/npe03__Recurring_Donation__c.npe03__Installments__c';
import FIELD_AMOUNT from '@salesforce/schema/npe03__Recurring_Donation__c.npe03__Amount__c';
import FIELD_PAYMENT_METHOD from '@salesforce/schema/npe03__Recurring_Donation__c.PaymentMethod__c';
import FIELD_INSTALLMENT_PERIOD from '@salesforce/schema/npe03__Recurring_Donation__c.npe03__Installment_Period__c';
import FIELD_INSTALLMENT_FREQUENCY from '@salesforce/schema/npe03__Recurring_Donation__c.InstallmentFrequency__c';
import FIELD_DAY_OF_MONTH from '@salesforce/schema/npe03__Recurring_Donation__c.Day_of_Month__c';
import FIELD_START_DATE from '@salesforce/schema/npe03__Recurring_Donation__c.StartDate__c';
import FIELD_CAMPAIGN from '@salesforce/schema/npe03__Recurring_Donation__c.npe03__Recurring_Donation_Campaign__c';

import cancelButtonLabel from '@salesforce/label/c.stgBtnCancel';
import saveButtonLabel from '@salesforce/label/c.stgBtnSave';
import unknownErrorLabel from '@salesforce/label/c.commonUnknownError';
import newHeaderLabel from '@salesforce/label/c.RD2_EntryFormHeader';
import editHeaderLabel from '@salesforce/label/c.commonEdit';
import currencyFieldLabel from '@salesforce/label/c.lblCurrency';
import donationSectionHeader from '@salesforce/label/c.RD2_EntryFormDonationSectionHeader';
import donorSectionHeader from '@salesforce/label/c.RD2_EntryFormDonorSectionHeader';
import otherSectionHeader from '@salesforce/label/c.RD2_EntryFormOtherSectionHeader';
import insertSuccessMessage from '@salesforce/label/c.RD2_EntryFormInsertSuccessMessage';
import updateSuccessMessage from '@salesforce/label/c.RD2_EntryFormUpdateSuccessMessage';

import getSetting from '@salesforce/apex/RD2_entryFormController.getSetting';

export default class rd2EntryForm extends LightningElement {

    @api parentId;
    @api recordId;
    listenerEvent = 'rd2EntryFormEvent';

    customLabels = Object.freeze({
        cancelButtonLabel,
        saveButtonLabel,
        donationSectionHeader,
        donorSectionHeader,
        otherSectionHeader
    });

    @track fields = {};
    @track record;
    recurringDonationInfo;
    fieldInfos;

    @track header = newHeaderLabel;

    @track isAutoNamingEnabled;
    @track isMultiCurrencyEnabled;

    @track contactId;
    @track accountId;

    @track isLoading = true;
    @track isRecordReady = false;
    isSettingReady = false;

    @track hasError = false;
    @track errorMessage = {};

    /*******************************************************************************
    * @description Dynamic render edit form CSS to show/hide the edit form
    */
    get cssEditForm() {
        return (!this.isLoading && this.isSettingReady && this.isRecordReady)
            ? ''
            : 'slds-hide';
    }

    /*******************************************************************************
    * @description Get Recurring Donation SObject API name
    */
    get recurringDonationName() {
        return getNestedProperty(RECURRING_DONATION_INFO, 'objectApiName');
    }

    /*******************************************************************************
    * @description Retrieve Recurring Donation Object info
    */
    @wire(getObjectInfo, {objectApiName: '$recurringDonationName' })
    wiredRDObjectInfo(response) {
        if (response.data) {
            this.recurringDonationInfo = response.data;
            this.buildDisplayedFields(this.recurringDonationInfo.fields);
            this.fieldInfos = this.buildFieldDescribesForWiredMethod(
                this.recurringDonationInfo.fields,
                this.recurringDonationInfo.apiName);

            if (this.recordId == null) {
                this.isRecordReady = true;
            }
        }

        if (response.error) {
            this.isRecordReady = true;
            handleError(response.error);
        }
    }

    /*******************************************************************************
    * @description Method converts field describe info into objects that the
    * getRecord method can accept into its 'fields' parameter.
    */
    buildFieldDescribesForWiredMethod(fields, objectApiName) {
        return Object.keys(fields).map((fieldApiName) => {
            return {
                fieldApiName: fieldApiName,
                objectApiName: objectApiName
            }
        });
    }

    /*******************************************************************************
    * @description Contrcut any needed field info from the Sobject Info
    */
    buildDisplayedFields(fieldInfos) {
        this.fields.name = this.extractFieldInfo(fieldInfos[FIELD_NAME.fieldApiName]);
        this.fields.account = this.extractFieldInfo(fieldInfos[FIELD_ACCOUNT.fieldApiName]);
        this.fields.contact = this.extractFieldInfo(fieldInfos[FIELD_CONTACT.fieldApiName]);
        this.fields.recurringType = this.extractFieldInfo(fieldInfos[FIELD_RECURRING_TYPE.fieldApiName]);
        this.fields.dateEstablished = this.extractFieldInfo(fieldInfos[FIELD_DATE_ESTABLISHED.fieldApiName]);
        this.fields.status = this.extractFieldInfo(fieldInfos[FIELD_STATUS.fieldApiName]);
        this.fields.amount = this.extractFieldInfo(fieldInfos[FIELD_AMOUNT.fieldApiName]);
        this.fields.paymentMethod = this.extractFieldInfo(fieldInfos[FIELD_PAYMENT_METHOD.fieldApiName]);
        this.fields.period = this.extractFieldInfo(fieldInfos[FIELD_INSTALLMENT_PERIOD.fieldApiName]);
        this.fields.installmentFrequency = this.extractFieldInfo(fieldInfos[FIELD_INSTALLMENT_FREQUENCY.fieldApiName]);
        this.fields.dayOfMonth = this.extractFieldInfo(fieldInfos[FIELD_DAY_OF_MONTH.fieldApiName]);
        this.fields.startDate = this.extractFieldInfo(fieldInfos[FIELD_START_DATE.fieldApiName]);
        this.fields.campaign = this.extractFieldInfo(fieldInfos[FIELD_CAMPAIGN.fieldApiName]);
        this.fields.currency = {label: currencyFieldLabel, apiName: 'CurrencyIsoCode'};
        this.fields.installment = this.extractFieldInfo(fieldInfos[FIELD_INSTALLMENT.fieldApiName]);
    }

    /*******************************************************************************
    * @description Method converts field describe info into a object that is easily accessiable from the front end
    */
    extractFieldInfo(field) {
        return {
            apiName : field.apiName,
            label : field.label,
            inlineHelpText : field.inlineHelpText,
            dataType : field.dataType
        };
    }

    /*******************************************************************************
    * @description Retrieve Recurring Donation record value
    */
    @wire(getRecord, { recordId: '$recordId', fields: '$fieldInfos' })
    wiredRecurringDonationRecord(response) {
        if (response.data) {
            this.record = response.data;
            this.modidifyEditHeader();
            this.isRecordReady = true;
        }

        if (response.error) {
            this.isRecordReady = true;
            handleError(response.error);
        }
    }

    /*******************************************************************************
    * @description Modify edit modal header
    */
    modidifyEditHeader() {
        this.header = editHeaderLabel + ' ' + this.record.fields.Name.value;
    }

    /*******************************************************************************
    * @description Get org setting on init of the component
    */
    connectedCallback() {
        getSetting({parentId: this.parentId})
        .then(response => {
            this.isAutoNamingEnabled = response.isAutoNamingEnabled;
            this.isMultiCurrencyEnabled = response.isMultiCurrencyEnabled;
            this.handleParentIdType(response.parentSObjectType);
            this.isSettingReady = true;
            this.isLoading = false;
        })
        .catch((error) => {
            this.isSettingReady = true;
            this.handleError(error);
        });
    }

    /*******************************************************************************
    * @description Determine the parentId Sobject Type
    */
    handleParentIdType(parentSObjecType) {
        if (parentSObjecType == null) {
            return;
        }

        if (parentSObjecType === ACCOUNT_OBJECT.objectApiName) {
            this.accountId = this.parentId;
        } else if (parentSObjecType === CONTACT_OBJECT.objectApiName) {
            this.contactId = this.parentId;
        }
    }

    /*******************************************************************************
    * @description Override the standard submit. Add any fields before the submittion
    */
    handleSubmit(event){
        this.isLoading = true;
        this.template.querySelector("[data-id='submitButton']").disabled = true;

        event.preventDefault();
        const fields = event.detail.fields;
        this.template.querySelector('lightning-record-edit-form').submit(fields);
    }

    /*******************************************************************************
    * @description Handle lwc when errors occur
    */
    handleError(error) {
        this.errorMessage = this.constructErrorMessage(error);
        this.hasError = true;
        this.isLoading = false;

        this.template.querySelector("[data-id='submitButton']").disabled = false;

        this.template.querySelector(".slds-modal__header").scrollIntoView();
    }

    /*******************************************************************************
    * @description Fires an event to utilDedicatedListener with the cancel action
    */
    handleCancel() {
        fireEvent(this.pageRef, this.listenerEvent, { action: 'cancel', recordId : this.recordId });
    }

    /*******************************************************************************
    * @description Fires an event to utilDedicatedListener with the success action
    */
    handleSuccess(event) {
        this.showSuccessToast(event.detail.fields.Name.value);
        fireEvent(this.pageRef, this.listenerEvent, { action: 'success', recordId: event.detail.id});
    }

    /*******************************************************************************
    * @description Contruct error wrapper from the error event
    *   error.body is the error from apex calls
    *   error.body.output.errors is for AuraHandledException messages
    *   error.body.message errors is the error from wired service
    *   error.detail.output.errors is the error from record-edit-forms
    */
   constructErrorMessage(error) {
        let header;
        let message;

        if (typeof error === 'string' || error instanceof String) {
            message = error;

        } else if (error.message) {
            message = error.message;

        } else if ((error.body && error.body.output)) {
            if (Array.isArray(error.body) &&
                !error.body.output.errors) {
                message = error.body.map(e => e.message).join(', ');

            } else if (typeof error.body.message === 'string' &&
                !error.body.output.errors) {
                message = error.body.message;

            } else if (error.body.output &&
                Array.isArray(error.body.output.errors)) {
                message = error.body.output.errors.map(e => e.message).join(', ');
            }

        } else if (error.detail && error.detail.output && Array.isArray(error.detail.output.errors)) {
            header = error.detail.message;
            message = error.detail.output.errors.map(e => e.message).join(', ');

        } else if (error.body && error.body.message) {
            message = error.body.message;
        }

        return {
            header: header || unknownErrorLabel,
            detail: message || unknownErrorLabel
        };
    }

    /*******************************************************************************
    * @description Fires a toast event to display success message
    */
    showSuccessToast(recordName) {
        let succeedMessage = (this.recordId)
            ? updateSuccessMessage.replace("{0}", recordName)
            : insertSuccessMessage.replace("{0}", recordName);

        const event = new ShowToastEvent({
            title: succeedMessage,
            variant: 'success',
        });
        this.dispatchEvent(event);
    }
}<|endoftext|>


<commit_before>import { EMPTY, InstanceCache, ComponentTreeCache, VTree, Transaction } from './util/types';
import globalThis from './util/global';
import onceEnded from './once-ended';
import componentWillUnmount from './lifecycle/component-will-unmount';
import renderComponent from './render-component';

const { assign } = Object;

/**
 * @param {VTree} oldTree
 * @param {VTree} newTree
 *
 * @returns {VTree | null}
 */
function render(oldTree, newTree) {
  let oldComponentTree = null;

  // When there is an oldTree and it has childNodes, attempt to look up first
  // by the top-level element, or by the first element.
  if (oldTree && oldTree.childNodes) {
    // First try and lookup the old tree as a component.
    oldComponentTree = ComponentTreeCache.get(oldTree);

    // If that fails, try looking up its first child.
    if (!oldComponentTree) {
      oldComponentTree = ComponentTreeCache.get(oldTree.childNodes[0]);
    }
  }

  // If there is no old component, or if the components do not match, then we
  // are rendering a brand new component.
  if (!oldComponentTree || oldComponentTree.rawNodeName !== newTree.rawNodeName) {
    return renderComponent(newTree);
  }

  // Otherwise re-use the existing component if the constructors are the same.
  if (oldComponentTree) {
    // Update the incoming props/attrs.
    assign(oldComponentTree.attributes, newTree.attributes);

    return renderComponent(oldComponentTree);
  }

  return oldTree;
}

/**
 * @param {VTree} vTree
 */
const releaseHook = vTree => componentWillUnmount(vTree);

/**
 * @param {VTree} vTree
 */
const createTreeHook = vTree => {
  const { customElements } = globalThis;
  const Constructor = customElements && customElements.get(vTree.nodeName);

  if (typeof vTree.rawNodeName === 'function' || Constructor) {
    vTree.attributes.children = vTree.attributes.children || vTree.childNodes;
  }
};

/**
 * @param {VTree} vTree
 */
const createNodeHook = vTree => {
  // Only look up elements with a dash in the name.
  if (!vTree.nodeName.includes('-')) return;

  // Convert this to globalThis
  const { customElements } = globalThis;
  const Constructor = customElements && customElements.get(vTree.nodeName);

  if (Constructor) {
    return new Constructor(vTree.attributes);
  }
};

/**
 * @param {VTree} oldTree
 * @param {VTree} newTree
 */
const syncTreeHook = (oldTree, newTree) => {
  // Render components during synchronization.
  if (
    // When child is a Component
    typeof newTree.rawNodeName === 'function' &&
    // If there is an oldTree and it's not the existing component, trigger a
    // render.
    (oldTree && oldTree.rawNodeName ? oldTree.rawNodeName !== newTree.rawNodeName : false)
  ) {
    return render(oldTree, newTree);
  }

  if (!newTree.childNodes) {
    return oldTree;
  }

  // Loop through childNodes seeking out components to render.
  for (let i = 0; i < newTree.childNodes.length; i++) {
    const newChildTree = newTree.childNodes[i];
    const oldChildTree = (oldTree.childNodes && oldTree.childNodes[i]) || EMPTY.OBJ;

    // Search through the DOM tree for more components to render.
    if (typeof newChildTree.rawNodeName === 'function') {
      const renderTree = render(oldChildTree, newChildTree);

      // If nothing was rendered, return the oldTree.
      if (!renderTree) {
        return oldTree;
      }

      // Inject the rendered tree into the position.
      if (renderTree) {
        newTree.childNodes[i] = renderTree;

        // If the rendered tree is a fragment, splice in the children, as this
        // is simply a container for the nodes.
        if (renderTree.nodeType === 11) {
          // If a function was returned, re-run the inspection over this
          // element.
          if (typeof renderTree.rawNodeName === 'function') {
            i = i - 1;
          }
          // Replace the fragment with the rendered elements. This reduces and
          // flattens the fragments into their respective nodes. If there are
          // none, then they are removed from the DOM and nothing is rendered.
          else {
            newTree.childNodes.splice(i, 1, ...renderTree.childNodes);
          }
        }
      }
    }
  }
};

export default () => assign(
  (/** @type {Transaction} */transaction) => transaction.onceEnded(onceEnded),
  {
    displayName: 'componentTask',
    syncTreeHook,
    createNodeHook,
    createTreeHook,
    releaseHook,
  },
);
<commit_msg>Fix build<commit_after>import { EMPTY, ComponentTreeCache, VTree, Transaction } from './util/types';
import globalThis from './util/global';
import onceEnded from './once-ended';
import componentWillUnmount from './lifecycle/component-will-unmount';
import renderComponent from './render-component';

const { assign } = Object;

/**
 * @param {VTree} oldTree
 * @param {VTree} newTree
 *
 * @returns {VTree | null}
 */
function render(oldTree, newTree) {
  let oldComponentTree = null;

  // When there is an oldTree and it has childNodes, attempt to look up first
  // by the top-level element, or by the first element.
  if (oldTree && oldTree.childNodes) {
    // First try and lookup the old tree as a component.
    oldComponentTree = ComponentTreeCache.get(oldTree);

    // If that fails, try looking up its first child.
    if (!oldComponentTree) {
      oldComponentTree = ComponentTreeCache.get(oldTree.childNodes[0]);
    }
  }

  // If there is no old component, or if the components do not match, then we
  // are rendering a brand new component.
  if (!oldComponentTree || oldComponentTree.rawNodeName !== newTree.rawNodeName) {
    return renderComponent(newTree);
  }

  // Otherwise re-use the existing component if the constructors are the same.
  if (oldComponentTree) {
    // Update the incoming props/attrs.
    assign(oldComponentTree.attributes, newTree.attributes);

    return renderComponent(oldComponentTree);
  }

  return oldTree;
}

/**
 * @param {VTree} vTree
 */
const releaseHook = vTree => componentWillUnmount(vTree);

/**
 * @param {VTree} vTree
 */
const createTreeHook = vTree => {
  const { customElements } = globalThis;
  const Constructor = customElements && customElements.get(vTree.nodeName);

  if (typeof vTree.rawNodeName === 'function' || Constructor) {
    vTree.attributes.children = vTree.attributes.children || vTree.childNodes;
  }
};

/**
 * @param {VTree} vTree
 */
const createNodeHook = vTree => {
  // Only look up elements with a dash in the name.
  if (!vTree.nodeName.includes('-')) return;

  // Convert this to globalThis
  const { customElements } = globalThis;
  const Constructor = customElements && customElements.get(vTree.nodeName);

  if (Constructor) {
    return new Constructor(vTree.attributes);
  }
};

/**
 * @param {VTree} oldTree
 * @param {VTree} newTree
 */
const syncTreeHook = (oldTree, newTree) => {
  // Render components during synchronization.
  if (
    // When child is a Component
    typeof newTree.rawNodeName === 'function' &&
    // If there is an oldTree and it's not the existing component, trigger a
    // render.
    (oldTree && oldTree.rawNodeName ? oldTree.rawNodeName !== newTree.rawNodeName : false)
  ) {
    return render(oldTree, newTree);
  }

  if (!newTree.childNodes) {
    return oldTree;
  }

  // Loop through childNodes seeking out components to render.
  for (let i = 0; i < newTree.childNodes.length; i++) {
    const newChildTree = newTree.childNodes[i];
    const oldChildTree = (oldTree.childNodes && oldTree.childNodes[i]) || EMPTY.OBJ;

    // Search through the DOM tree for more components to render.
    if (typeof newChildTree.rawNodeName === 'function') {
      const renderTree = render(oldChildTree, newChildTree);

      // If nothing was rendered, return the oldTree.
      if (!renderTree) {
        return oldTree;
      }

      // Inject the rendered tree into the position.
      if (renderTree) {
        newTree.childNodes[i] = renderTree;

        // If the rendered tree is a fragment, splice in the children, as this
        // is simply a container for the nodes.
        if (renderTree.nodeType === 11) {
          // If a function was returned, re-run the inspection over this
          // element.
          if (typeof renderTree.rawNodeName === 'function') {
            i = i - 1;
          }
          // Replace the fragment with the rendered elements. This reduces and
          // flattens the fragments into their respective nodes. If there are
          // none, then they are removed from the DOM and nothing is rendered.
          else {
            newTree.childNodes.splice(i, 1, ...renderTree.childNodes);
          }
        }
      }
    }
  }
};

export default () => assign(
  (/** @type {Transaction} */transaction) => transaction.onceEnded(onceEnded),
  {
    displayName: 'componentTask',
    syncTreeHook,
    createNodeHook,
    createTreeHook,
    releaseHook,
  },
);
<|endoftext|>


<commit_before>/*
  This plugin provides a websocket which can be used for communicating from plugins to a running client.

  Plugins can get hold of the `send` method by adding the following hook:

  compiler.plugin('websocket-send-available', send => {
    ...
  })

  During code generation, the variable __webpack_websocket_port__ is replaced with the port number at the
  other end of the `send` method. This allows clients to open a connection to that port and listen to the
  message from `send`.
*/

const ConstDependency = require('webpack/lib/dependencies/ConstDependency')
const NullFactory = require('webpack/lib/NullFactory')
const ParserHelpers = require('webpack/lib/ParserHelpers')
const net = require('net')
const ws = require('ws')

module.exports = function websocketCommunicationPlugin() {

  return {
    apply: compiler => {
      const freePort = findFreePort()
      const webSocketServer = freePort.then(startWebSocketServer)

      function send(message) { webSocketServer.then(wss => wss.send(message)) }

      let port

      // provide the send function
      compiler.plugin('environment', () => {
        compiler.applyPlugins('websocket-send-available', send)
      })

      // wait for a free port before we start compiling
      compiler.plugin('before-compile', (params, done) => {
        freePort.then(found => { port = found }).then(_ => { done() }).catch(done)
      })

      // make sure the __webpack_websocket_port__ is available in modules (code copied from ExtendedApiPlugin)
      compiler.plugin('compilation', (compilation, { normalModuleFactory }) => {
        compilation.dependencyFactories.set(ConstDependency, new NullFactory())
        compilation.dependencyTemplates.set(ConstDependency, new ConstDependency.Template())
        compilation.mainTemplate.plugin('require-extensions', function(source, chunk, hash) {
          const buf = [
            source,
            '',
            '// __webpack_websocket_port__',
            `${this.requireFn}.wsp = ${port};`
          ]
          return this.asString(buf)
        })
        compilation.mainTemplate.plugin('global-hash', () => true)
        normalModuleFactory.plugin('parser', (parser, parserOptions) => {
          parser.plugin(`expression __webpack_websocket_port__`, ParserHelpers.toConstantDependency('__webpack_require__.wsp'))
          parser.plugin(`evaluate typeof __webpack_websocket_port__`, ParserHelpers.evaluateToString('string'))
        })
      })
    }
  }
}

function findFreePort(retries = 2) {
  return new Promise((resolve, reject) => {
    const server = net.createServer()
    let port
    server.on('listening', () => {
      port = server.address().port
      server.close()
    })
    server.on('close', () => { resolve(port) })
    server.on('error', (err) => { reject(err) })
    server.listen(0, 'localhost')
  }).catch(e => retries
    ? findFreePort(retries - 1)
    : Promise.reject(e)
  )
}

function startWebSocketServer(port) {
  console.log(`WebSocket opened on port ${port}`)
  const wss = new ws.Server({ port })

  return {
    send: message => {
      wss.clients.forEach(client => {
        if (client.readyState === ws.OPEN) client.send(JSON.stringify(message))
      })
    }
  }
}
<commit_msg>Ignore network errors like `ECONNRESET`, `EPIPE` - fixes #106<commit_after>/*
  This plugin provides a websocket which can be used for communicating from plugins to a running client.

  Plugins can get hold of the `send` method by adding the following hook:

  compiler.plugin('websocket-send-available', send => {
    ...
  })

  During code generation, the variable __webpack_websocket_port__ is replaced with the port number at the
  other end of the `send` method. This allows clients to open a connection to that port and listen to the
  message from `send`.
*/

const ConstDependency = require('webpack/lib/dependencies/ConstDependency')
const NullFactory = require('webpack/lib/NullFactory')
const ParserHelpers = require('webpack/lib/ParserHelpers')
const net = require('net')
const ws = require('ws')

module.exports = function websocketCommunicationPlugin() {

  return {
    apply: compiler => {
      const freePort = findFreePort()
      const webSocketServer = freePort.then(startWebSocketServer)

      function send(message) { webSocketServer.then(wss => wss.send(message)) }

      let port

      // provide the send function
      compiler.plugin('environment', () => {
        compiler.applyPlugins('websocket-send-available', send)
      })

      // wait for a free port before we start compiling
      compiler.plugin('before-compile', (params, done) => {
        freePort.then(found => { port = found }).then(_ => { done() }).catch(done)
      })

      // make sure the __webpack_websocket_port__ is available in modules (code copied from ExtendedApiPlugin)
      compiler.plugin('compilation', (compilation, { normalModuleFactory }) => {
        compilation.dependencyFactories.set(ConstDependency, new NullFactory())
        compilation.dependencyTemplates.set(ConstDependency, new ConstDependency.Template())
        compilation.mainTemplate.plugin('require-extensions', function(source, chunk, hash) {
          const buf = [
            source,
            '',
            '// __webpack_websocket_port__',
            `${this.requireFn}.wsp = ${port};`
          ]
          return this.asString(buf)
        })
        compilation.mainTemplate.plugin('global-hash', () => true)
        normalModuleFactory.plugin('parser', (parser, parserOptions) => {
          parser.plugin(`expression __webpack_websocket_port__`, ParserHelpers.toConstantDependency('__webpack_require__.wsp'))
          parser.plugin(`evaluate typeof __webpack_websocket_port__`, ParserHelpers.evaluateToString('string'))
        })
      })
    }
  }
}

function findFreePort(retries = 2) {
  return new Promise((resolve, reject) => {
    const server = net.createServer()
    let port
    server.on('listening', () => {
      port = server.address().port
      server.close()
    })
    server.on('close', () => { resolve(port) })
    server.on('error', (err) => { reject(err) })
    server.listen(0, 'localhost')
  }).catch(e => retries
    ? findFreePort(retries - 1)
    : Promise.reject(e)
  )
}

function startWebSocketServer(port) {
  const wss = new ws.Server({ port })
  wss.on('connection', ws => {
    ws.on('error', err => {
      // Ignore network errors like `ECONNRESET`, `EPIPE`, etc., fixes https://github.com/kaliberjs/build/issues/106
      if (err.errno) return
      throw err
    })
  })

  return {
    send: message => {
      wss.clients.forEach(client => {
        if (client.readyState === ws.OPEN) client.send(JSON.stringify(message))
      })
    }
  }
}
<|endoftext|>