<commit_before>$(function(){

	function sorting() {
		$(".clevel1 .clist").sortable({handle: ".cmove2", cancel: "", stop: saverecipe});
		$(".clevel2 .clist").sortable({handle: ".cmove3", cancel: "", stop: saverecipe});
		$(".clevel3 .clist").sortable({handle: ".cmove4", cancel: "", stop: saverecipe});
	}
	sorting();

	$("body").on("click", ".cadd", function(e){
		/* pressing one of the '+' buttons copies the corresponding proforma structure to just before the button */
		e.preventDefault(); e.stopPropagation();
		var jself = $(this);
		$("#"+jself.attr("proforma")).clone().removeClass("cproforma").
			removeAttr("id").appendTo(jself.prev("ul.clist"));
		saverecipe();
		sorting();
	});

	$("body").on("click", ".cremove", function(e){
		/* pressing one of the 'X' buttons removes the group of options containing the button */
		e.preventDefault(); e.stopPropagation();
		var jself = $(this);
		jself.closest(".cgroup").remove();
		saverecipe();
	});

	$("body").on("change", "select.cinput", function(e){
		/* some select choices come with further options; the furtheroptions attribute identifies these.
		   Make visible those and only those (in the same set of options) for this select */
		var jself = $(this);
		var f = jself.find("option:selected").attr("furtheroptions");
		if (typeof f != "string") { return; }
		var jparent = jself.closest(".coptionset");
		jparent.children(".cfurtheroption").not(jself).hide();
		if (f == "") { return; }
		f.split(",").forEach(function(el,idx){ jparent.find("."+el).show(); });
	});

	$("body").on("change", ".cinput", function(e){
		/* always keep the current state in storage so we can restore it on revisiting the page */
		saverecipe();
		if ($(this).attr("id") == "irecipename") { recipeselectoptions(); }
	});

	$("body").on("click", "a.chelp", function(e){
		/* show the help panel */
		e.preventDefault(); e.stopPropagation();
		$("#ihelpframe").attr({src: $(this).attr("href")}).closest("#ihelp").css({top: $(window).scrollTop()+70, height: $(window).height()-100}).show();
	});

	var isubmissiontop = $("#isubmissioncontainer").offset().top;

	$(window).scroll(function(e){
		$("#isubmissioncontainer").toggleClass("ctoplocked", $(window).scrollTop()+10 > isubmissiontop);
	})

	$("#ihelpclose").click(function(e){
		/* close the help panel */
		e.preventDefault(); e.stopPropagation();
		$("#ihelp").hide();
	});

	function toggleqif() {
		/* qif format has some differences in the options, particularly the field names, which are fixed */
		var qif = $("#ioutputformat").val() == "qif";
		$(".cqif").toggle(qif);
		$(".cnotqif").toggle(! qif);
	}

	$("#ioutputformat").change(toggleqif);

	var prefix = "recipe=";
	function recipestoragename(name) { return prefix+name; }
	function getlocalrecipejson(name){ return localStorage[recipestoragename(name)]; }
	function putlocalrecipejson(name, recipe){ localStorage[recipestoragename(name)] = recipe; }
	function haslocalrecipe(name){ return (recipestoragename(name) in localStorage); }
	function recipenames(){
		var names = [];
		$.each(localStorage, function(k, v){
			if (k.substr(0, prefix.length) != prefix) { return; }
			names.push(k.substr(prefix.length));
		});
		return names.sort(function(a,b){ return a.toLowerCase().localeCompare(b.toLowerCase()); });
	}

	$("#iloadselect").change(function(e){
		var name = $(this).val();
		localStorage.currentRecipe = name;
		loadrecipe(getlocalrecipejson(name));
		recipeselectoptions();
	});

	var defaultrecipe = JSON.stringify({
		recipeName: "(anonymous)",
		comment: "",
		outputTo: "attachment",
		outputFormat: "json",
		encoding: "auto",
		headerRows: 0,
		rowCount: 1
	});

	$("#ireset").click(function(e){
		e.preventDefault(); e.stopPropagation();
		loadrecipe(defaultrecipe);
	});

	$("#iexample").click(function(e){
		e.preventDefault(); e.stopPropagation();
		$.getJSON("/Example.jcomma.json", function(j){ $("#ipasterecipe").val(JSON.stringify(j)).change(); });
	});

	$("#ideleterecipe").click(function(e){
		e.preventDefault(); e.stopPropagation();
		var name = $("#irecipename").val();
		if (name == "") { alert("your recipe is anonymous"); return; }
		if (! haslocalrecipe(name)) { alert("your recipe is not stored"); return; }
		delete localStorage[recipestoragename(name)];
		loadrecipe(defaultrecipe);
		recipeselectoptions();
	});

	$("#idelimitertab").change(function(e){
		$("#idelimiter").val($(this).is(":checked") ? "\t" : ",");
		saverecipe();
	});
	$("#idelimiter").change(function(e){
		$("#idelimitertab").prop({checked: $(this).val() == "\t"});
	});

	function recurse(level, jel){
		/* use the DOM structure to create a corresponding JSON object to send to the API */
		var j = {};
		jel.find(".cinput"+level+":visible").each(function(idx,el){
			var v = $(el).val();
			if (v == "" || ($(el).attr("type") == "checkbox" && ! $(el).is(":checked"))) { return; }
			if ($(el).hasClass("cint")) { v = parseInt(v); }
			j[$(el).attr("name")] = v;
		});
		jel.find(".clevel"+(level+1)).each(function(idx,el){
			var name = $(el).attr("name");
			if (! (name in j)) { j[name] = []; }
			j[name].push(recurse(level+1, $(el)));
		});
		return j;
	}

	$("#isubmission").on("submit", function(e){
		/* invoke the API - we use the existing form which just
		   contains an input type file, and add to that the single
		   hidden field for the JSON recipe which says how to interpret
		   the file */
		if ($("#icsv").val() == "" && $("#icsvpaste").val() == "") {
			e.preventDefault();
			alert("you need to choose a csv file to process");
			return;
		} else if ($("#icsv").val() != "" && $("#icsvpaste").val() != "") {
			e.preventDefault();
			alert("choose either a csv file to process or paste one, but not both (to clear a previously chosen file, click Choose file, then Cancel)");
			return;
		}

		$("#isendrecipe").val(makerecipe());
		// and continue to submit
	});

	$("#isaverecipe").click(function(){
		/* rather than invoking the API, just save the options for re-use later */
		var name = $("#irecipename").val();
		if (name == "") { name = "(anonymous)"; }
		var s = makerecipe();
		if ($("#icopyrecipepretty").is(":checked")) {
			s = JSON.stringify(JSON.parse(s), null, 2);
		}
		$(this).attr({href: "data:application/json;charset=utf-8," + encodeURIComponent(s),
					  download: name+".jcomma.json"});
	});

	function populate(k, v, j){
		/* recursively fill in the form programmatically */
		if ($.isArray(v)) {
			$.each(v, function(idx, va){ populate(k, va, j); });
		} else if (typeof v == 'object') {
			var jp = $(".cproforma[name=\""+k+"\"]");
			var jb = j.find("button[proforma=\""+jp.attr("id")+"\"]");
			var jo = jp.clone().removeClass("cproforma").removeAttr("id").appendTo(jb.prev("ul.clist"));
			$.each(v, function(ko, vo){	populate(ko, vo, jo); });
		} else {
			var js = j.find("select").filter("[name=\""+k+"\"]").val(v).show();
			var f = js.find("option:selected").attr("furtheroptions");
			if (typeof f == "string") {
				var jparent = js.closest(".coptionset");
				jparent.children(".cfurtheroption").hide();
				if (f != "") { f.split(",").forEach(function(el,idx){ jparent.find("."+el).show(); }); }
			}
			j.find("input[type=text],textarea").filter("[name=\""+k+"\"]").val(v).show();
			j.find("input[type=checkbox]").filter("[name=\""+k+"\"]").prop({checked: true}).show();
		}
	}

	function makerecipe(){
		/* capture the form content and return as a JSON string (we may save this to a file or localStorage) */
		var data = recurse(1, $("#iform .coptions"));
		data.recipeVersion = 3;
		return JSON.stringify(data);
	}

	function saverecipe() {
		/* save to localStorage and the copy+paste field on each change */
		var name = $("#irecipename").val();
		if (name == "") { name = "(anonymous)"; }
		localStorage.currentRecipe = name;
		putlocalrecipejson(name, makerecipe());
		var s = localStorage[recipestoragename(name)];
		if ($("#icopyrecipepretty").is(":checked")) {
			s = JSON.stringify(JSON.parse(s), null, 2);
		}
		$("#icopyrecipe").val(s);
	}

	function loadrecipe(recipejson){
		/* given a recipe JSON string, set the form fields correspondingly */
		$("#iform .cremove").each(function(idx,el) { $(el).closest(".cgroup").remove(); });
		var recipe = JSON.parse(recipejson);
		if (! recipe) { return; }
		if (! ("recipeName" in recipe)) { recipe.recipeName = "(anonymous)"; }
		if (! ("delimiterChar" in recipe)) { recipe.delimiterChar = ','; }
		if (! ("enclosureChar" in recipe)) { recipe.enclosureChar = '"'; }
		$.each(recipe, function(k, v){ populate(k, v, $("#iform")); });
		$("#idelimitertab").prop({checked: $("#idelimiter").val() == "\t"});
		toggleqif();
		sorting();
		$("#icopyrecipe").val(recipejson);
		localStorage.currentRecipe = recipe.recipeName;
		$("#iyourrecipe").fadeOut(100, function(){ $(this).fadeIn(400); });
	}

	function recipeselectoptions(){
		var jselect = $("#iloadselect").empty();
		$("<option>").prop({readonly: true}).text("choose one").appendTo(jselect);
		$.each(recipenames(), function(idx, name){
			$("<option>").text(name).appendTo(jselect);
		});
	}

	$("#iloadrecipe").change(function(e1){
		/* load recipe from file in response to the choose file button for this */
		var f = $(this)[0].files[0];
		$(this).val("");
		if (f) {
			var r = new FileReader();
			r.onload = function(e2){
				loadrecipe(r.result);
				saverecipe();
				recipeselectoptions();
			};
			r.readAsText(f);
		}
	});

	$("#iloadrecipeurl").change(function(e){
		var encodedurl = encodeURIComponent($(this).val());
		if (encodedurl == "") { return; }
		window.history.pushState({}, "", ".?recipe="+encodedurl);
		getcloudrecipejson(encodedurl);
	});

	$("#ipasterecipe").change(function(e1){
		/* load recipe from contents */
		loadrecipe($(this).val());
		$(this).val("done")
	});

	$("#icopyrecipe,#ipasterecipe").focus(function(){
		$(this).select();
	});

	$("#icopyrecipepretty").change(function(e){
		saverecipe();
	})

	function getcloudrecipejson(encodedurl){
		$.ajax("/corsproxy.php?recipe="+encodedurl, {
			dataType: "json",
			method: 'GET',
			success: function(j){
				loadrecipe(JSON.stringify(j));
				saverecipe();
			},
			error: function(xhr, s, err){ alert("cannot fetch recipe from your given recipe URL"); }
		});
	}

	/* reload automatically from any previous use (except for the CSV file itself) */
	var fromURL = false;
	if (window.location.href.indexOf('?') !== -1) {
		var ps = window.location.href.slice(window.location.href.indexOf('?') + 1).split('&');
		$.each(ps, function(idx, p){
			var kv = p.split("=");
			if (kv[0] != "recipe") { return; }
			fromURL = true;
			$("#iloadrecipeurl").val(decodeURIComponent(kv[1]));
			getcloudrecipejson(kv[1]);
			return false;
		});
	}
	if (! fromURL && ("currentRecipe" in localStorage)) {
		loadrecipe(getlocalrecipejson(localStorage.currentRecipe));
	}
	recipeselectoptions();
});
<commit_msg>Highlight bad empty fields, and don't allow form submission if bad<commit_after>$(function(){

	function sorting() {
		$(".clevel1 .clist").sortable({handle: ".cmove2", cancel: "", stop: saverecipe});
		$(".clevel2 .clist").sortable({handle: ".cmove3", cancel: "", stop: saverecipe});
		$(".clevel3 .clist").sortable({handle: ".cmove4", cancel: "", stop: saverecipe});
	}
	sorting();

	$("body").on("click", ".cadd", function(e){
		/* pressing one of the '+' buttons copies the corresponding proforma structure to just before the button */
		e.preventDefault(); e.stopPropagation();
		var jself = $(this);
		$("#"+jself.attr("proforma")).clone().removeClass("cproforma").
			removeAttr("id").appendTo(jself.prev("ul.clist"));
		saverecipe();
		sorting();
	});

	$("body").on("click", ".cremove", function(e){
		/* pressing one of the 'X' buttons removes the group of options containing the button */
		e.preventDefault(); e.stopPropagation();
		var jself = $(this);
		jself.closest(".cgroup").remove();
		saverecipe();
	});

	$("body").on("change", "select.cinput", function(e){
		/* some select choices come with further options; the furtheroptions attribute identifies these.
		   Make visible those and only those (in the same set of options) for this select */
		var jself = $(this);
		var f = jself.find("option:selected").attr("furtheroptions");
		if (typeof f != "string") { return; }
		var jparent = jself.closest(".coptionset");
		jparent.children(".cfurtheroption").not(jself).hide();
		if (f == "") { return; }
		f.split(",").forEach(function(el,idx){ jparent.find("."+el).show(); });
	});

	$("body").on("change", ".cinput", function(e){
		/* always keep the current state in storage so we can restore it on revisiting the page */
		saverecipe();
		if ($(this).attr("id") == "irecipename") { recipeselectoptions(); }
	});

	$("body").on("click", "a.chelp", function(e){
		/* show the help panel */
		e.preventDefault(); e.stopPropagation();
		$("#ihelpframe").attr({src: $(this).attr("href")}).closest("#ihelp").css({top: $(window).scrollTop()+70, height: $(window).height()-100}).show();
	});

	var isubmissiontop = $("#isubmissioncontainer").offset().top;

	$(window).scroll(function(e){
		$("#isubmissioncontainer").toggleClass("ctoplocked", $(window).scrollTop()+10 > isubmissiontop);
	})

	$("#ihelpclose").click(function(e){
		/* close the help panel */
		e.preventDefault(); e.stopPropagation();
		$("#ihelp").hide();
	});

	function highlightbadfields(){
		$("#iform").find("select,.cfieldname,.crecordiffield,.cignorerowsname").each(function(idx,el){
			$(this).toggleClass("cbadfield", $(this).val() == "");
		});
	}

	function toggleqif() {
		/* qif format has some differences in the options, particularly the field names, which are fixed */
		var qif = $("#ioutputformat").val() == "qif";
		$(".cqif").toggle(qif);
		$(".cnotqif").toggle(! qif);
	}

	$("#ioutputformat").change(toggleqif);

	var prefix = "recipe=";
	function recipestoragename(name) { return prefix+name; }
	function getlocalrecipejson(name){ return localStorage[recipestoragename(name)]; }
	function putlocalrecipejson(name, recipe){ localStorage[recipestoragename(name)] = recipe; }
	function haslocalrecipe(name){ return (recipestoragename(name) in localStorage); }
	function recipenames(){
		var names = [];
		$.each(localStorage, function(k, v){
			if (k.substr(0, prefix.length) != prefix) { return; }
			names.push(k.substr(prefix.length));
		});
		return names.sort(function(a,b){ return a.toLowerCase().localeCompare(b.toLowerCase()); });
	}

	$("#iloadselect").change(function(e){
		var name = $(this).val();
		localStorage.currentRecipe = name;
		loadrecipe(getlocalrecipejson(name));
		recipeselectoptions();
	});

	var defaultrecipe = JSON.stringify({
		recipeName: "(anonymous)",
		comment: "",
		outputTo: "attachment",
		outputFormat: "json",
		encoding: "auto",
		headerRows: 0,
		rowCount: 1
	});

	$("#ireset").click(function(e){
		e.preventDefault(); e.stopPropagation();
		loadrecipe(defaultrecipe);
	});

	$("#iexample").click(function(e){
		e.preventDefault(); e.stopPropagation();
		$.getJSON("/Example.jcomma.json", function(j){ $("#ipasterecipe").val(JSON.stringify(j)).change(); });
	});

	$("#ideleterecipe").click(function(e){
		e.preventDefault(); e.stopPropagation();
		var name = $("#irecipename").val();
		if (name == "") { alert("your recipe is anonymous"); return; }
		if (! haslocalrecipe(name)) { alert("your recipe is not stored"); return; }
		delete localStorage[recipestoragename(name)];
		loadrecipe(defaultrecipe);
		recipeselectoptions();
	});

	$("#idelimitertab").change(function(e){
		$("#idelimiter").val($(this).is(":checked") ? "\t" : ",");
		saverecipe();
	});
	$("#idelimiter").change(function(e){
		$("#idelimitertab").prop({checked: $(this).val() == "\t"});
	});

	function recurse(level, jel){
		/* use the DOM structure to create a corresponding JSON object to send to the API */
		var j = {};
		jel.find(".cinput"+level+":visible").each(function(idx,el){
			var v = $(el).val();
			if (v == "" || ($(el).attr("type") == "checkbox" && ! $(el).is(":checked"))) { return; }
			if ($(el).hasClass("cint")) { v = parseInt(v); }
			j[$(el).attr("name")] = v;
		});
		jel.find(".clevel"+(level+1)).each(function(idx,el){
			var name = $(el).attr("name");
			if (! (name in j)) { j[name] = []; }
			j[name].push(recurse(level+1, $(el)));
		});
		return j;
	}

	$("#isubmission").on("submit", function(e){
		/* invoke the API - we use the existing form which just
		   contains an input type file, and add to that the single
		   hidden field for the JSON recipe which says how to interpret
		   the file */
		if ($("#icsv").val() == "" && $("#icsvpaste").val() == "") {
			e.preventDefault();
			alert("you need to choose a csv file to process");
			return;
		} else if ($("#icsv").val() != "" && $("#icsvpaste").val() != "") {
			e.preventDefault();
			alert("choose either a csv file to process or paste one, but not both (to clear a previously chosen file, click Choose file, then Cancel)");
			return;
		}

		highlightbadfields();
		if ($(".cbadfield").length > 0) {
			e.preventDefault();
			alert("you have an empty entry (highlighted in red)");
			return;
		}
		$("#isendrecipe").val(makerecipe());
		// and continue to submit
	});

	$("#isaverecipe").click(function(){
		/* rather than invoking the API, just save the options for re-use later */
		var name = $("#irecipename").val();
		if (name == "") { name = "(anonymous)"; }
		var s = makerecipe();
		if ($("#icopyrecipepretty").is(":checked")) {
			s = JSON.stringify(JSON.parse(s), null, 2);
		}
		$(this).attr({href: "data:application/json;charset=utf-8," + encodeURIComponent(s),
					  download: name+".jcomma.json"});
	});

	function populate(k, v, j){
		/* recursively fill in the form programmatically */
		if ($.isArray(v)) {
			$.each(v, function(idx, va){ populate(k, va, j); });
		} else if (typeof v == 'object') {
			var jp = $(".cproforma[name=\""+k+"\"]");
			var jb = j.find("button[proforma=\""+jp.attr("id")+"\"]");
			var jo = jp.clone().removeClass("cproforma").removeAttr("id").appendTo(jb.prev("ul.clist"));
			$.each(v, function(ko, vo){	populate(ko, vo, jo); });
		} else {
			var js = j.find("select").filter("[name=\""+k+"\"]").val(v).show();
			var f = js.find("option:selected").attr("furtheroptions");
			if (typeof f == "string") {
				var jparent = js.closest(".coptionset");
				jparent.children(".cfurtheroption").hide();
				if (f != "") { f.split(",").forEach(function(el,idx){ jparent.find("."+el).show(); }); }
			}
			j.find("input[type=text],textarea").filter("[name=\""+k+"\"]").val(v).show();
			j.find("input[type=checkbox]").filter("[name=\""+k+"\"]").prop({checked: true}).show();
		}
	}

	function makerecipe(){
		/* capture the form content and return as a JSON string (we may save this to a file or localStorage) */
		var data = recurse(1, $("#iform .coptions"));
		data.recipeVersion = 3;
		return JSON.stringify(data);
	}

	function saverecipe() {
		/* save to localStorage and the copy+paste field on each change */
		var name = $("#irecipename").val();
		if (name == "") { name = "(anonymous)"; }
		localStorage.currentRecipe = name;
		putlocalrecipejson(name, makerecipe());
		var s = localStorage[recipestoragename(name)];
		if ($("#icopyrecipepretty").is(":checked")) {
			s = JSON.stringify(JSON.parse(s), null, 2);
		}
		$("#icopyrecipe").val(s);
	}

	function loadrecipe(recipejson){
		/* given a recipe JSON string, set the form fields correspondingly */
		$("#iform .cremove").each(function(idx,el) { $(el).closest(".cgroup").remove(); });
		var recipe = JSON.parse(recipejson);
		if (! recipe) { return; }
		if (! ("recipeName" in recipe)) { recipe.recipeName = "(anonymous)"; }
		if (! ("delimiterChar" in recipe)) { recipe.delimiterChar = ','; }
		if (! ("enclosureChar" in recipe)) { recipe.enclosureChar = '"'; }
		$.each(recipe, function(k, v){ populate(k, v, $("#iform")); });
		$("#idelimitertab").prop({checked: $("#idelimiter").val() == "\t"});
		toggleqif();
		sorting();
		$("#icopyrecipe").val(recipejson);
		localStorage.currentRecipe = recipe.recipeName;
		$("#iyourrecipe").fadeOut(100, function(){ $(this).fadeIn(400); });
	}

	function recipeselectoptions(){
		var jselect = $("#iloadselect").empty();
		$("<option>").prop({readonly: true}).text("choose one").appendTo(jselect);
		$.each(recipenames(), function(idx, name){
			$("<option>").text(name).appendTo(jselect);
		});
	}

	$("#iloadrecipe").change(function(e1){
		/* load recipe from file in response to the choose file button for this */
		var f = $(this)[0].files[0];
		$(this).val("");
		if (f) {
			var r = new FileReader();
			r.onload = function(e2){
				loadrecipe(r.result);
				saverecipe();
				recipeselectoptions();
			};
			r.readAsText(f);
		}
	});

	$("#iloadrecipeurl").change(function(e){
		var encodedurl = encodeURIComponent($(this).val());
		if (encodedurl == "") { return; }
		window.history.pushState({}, "", ".?recipe="+encodedurl);
		getcloudrecipejson(encodedurl);
	});

	$("#ipasterecipe").change(function(e1){
		/* load recipe from contents */
		loadrecipe($(this).val());
		$(this).val("done")
	});

	$("#icopyrecipe,#ipasterecipe").focus(function(){
		$(this).select();
	});

	$("#icopyrecipepretty").change(function(e){
		saverecipe();
	})

	function getcloudrecipejson(encodedurl){
		$.ajax("/corsproxy.php?recipe="+encodedurl, {
			dataType: "json",
			method: 'GET',
			success: function(j){
				loadrecipe(JSON.stringify(j));
				saverecipe();
			},
			error: function(xhr, s, err){ alert("cannot fetch recipe from your given recipe URL"); }
		});
	}

	/* reload automatically from any previous use (except for the CSV file itself) */
	var fromURL = false;
	if (window.location.href.indexOf('?') !== -1) {
		var ps = window.location.href.slice(window.location.href.indexOf('?') + 1).split('&');
		$.each(ps, function(idx, p){
			var kv = p.split("=");
			if (kv[0] != "recipe") { return; }
			fromURL = true;
			$("#iloadrecipeurl").val(decodeURIComponent(kv[1]));
			getcloudrecipejson(kv[1]);
			return false;
		});
	}
	if (! fromURL && ("currentRecipe" in localStorage)) {
		loadrecipe(getlocalrecipejson(localStorage.currentRecipe));
	}
	recipeselectoptions();

});
<|endoftext|>


<commit_before>/*!
 * Greeed.js 1.0.0
 * MIT licensed
 *
 * Copyright (C) 2014 Vincent De Oliveira, http://iamvdo.me
 */
(function () {

	"use strict";

	var greeed;

	function goGreeed (grid, method, options) {

		var target = document.querySelector(grid);
		if (target !== null) {
			method.call(null, target, options);
		}
	}

	function add (grid, options) {

		greeed = new Greeed(grid, options);
		greeed.init();

	}

	function remove (grid) {

		greeed.destroy();

	}

	function Greeed (elem, options) {

		this.grid = elem;
		this.nbColumns = 0;
		this.childs = this.columns = this.options = [];

		this.rootFontSize = getComputedStyle(document.documentElement).getPropertyValue('font-size').replace('px','');

		for(var key in options){
			if(options.hasOwnProperty(key)){
				this.defaults[key] = options[key];
			}
		}
		this.options = this.defaults;

	}

	Greeed.prototype = {

		defaults: {
			units: 'fluid',
			elementColumn: 'li',
			classColumn: 'Greeed-column',
			elementColumnInner: 'ul',
			classColumnInner: 'Greeed-column-inner',
			classItem: 'Greeed-item',
			classFakeItem: 'Greeed-item--fake',
			fakeItem: true,
			inlineStyles: true
		},

		init: function () {

			// Get elements
			this.childs = Array.prototype.slice.call(this.grid.children);
			// DOM columns
			this.columnsDOM = [];

			this.checkMQ();

			var scope = this;
			this.startCheckMQ = function(event) { scope.checkMQ(event); };

			window.addEventListener('resize', this.startCheckMQ, false);

			if ( this.options.afterInit ) {
				this.options.afterInit();
			}

		},

		destroy: function () {
			window.removeEventListener('resize', this.startCheckMQ, false);
		},

		createColumns: function ( greeedWidth ) {

			// create an Array of columns
			this.columns = new Array(this.nbColumns);
			for (var i = 0; i < this.nbColumns; i++) {
				this.columns[i] = [];
				// set height
				this.columns[i]._offsetHeight = 0;
			}

			for (var i = 0; i < this.childs.length; i++) {

				var smallestColumnHeight = 0;
				var smallestColumnIndex = 0;
				// find the smallest column to place the next child
				for (var j = 0; j < this.columns.length; j++) {
					var columnHeight = this.columns[j]._offsetHeight;
					if(j === 0){
						smallestColumnHeight = columnHeight;
					}
					if(columnHeight === 0){
						smallestColumnIndex = j;
						break;
					}
					if(columnHeight < smallestColumnHeight){
						smallestColumnIndex = j;
					}
				}

				// add child to the smallest height column
				this.columns[smallestColumnIndex].push(this.childs[i]);

				// add an id (keep the old position)
				this.childs[i]._id = i;

				// update column height
				this.columns[smallestColumnIndex]._offsetHeight += this.childs[i].offsetHeight;

			}

			// find the max-height column
			var maxHeightColumn = 0;
			for (var i = 0; i < this.columns.length; i++) {

				var height = this.columns[i]._offsetHeight;

				if( height >= maxHeightColumn){
					maxHeightColumn = height;
				}

			}

			var grid = document.createDocumentFragment();

			for (var i = 0; i < this.columns.length; i++) {

				var column = document.createElement(this.options.elementColumn);
					column.className = this.options.classColumn;
					if (this.options.inlineStyles) {
						column.style.styleFloat = column.style.cssFloat = 'left';
						column.style.display = 'block';
						if (this.options.units === 'fixed') {
							column.style.width = Math.floor( greeedWidth / this.nbColumns ) + 'px';
						} else {
							column.style.width = (100 / this.nbColumns ) + '%';
						}
					}

				if (this.options.elementColumnInner) {
					var columnElement = document.createElement(this.options.elementColumnInner);
						columnElement.className = this.options.classColumnInner;
				} else {
					var columnElement = column;
				}

				for (var j = 0; j < this.columns[i].length; j++) {
					this.columns[i][j].classList.add(this.options.classItem);
					columnElement.appendChild(this.columns[i][j]);
				}

				if( this.columns[i]._offsetHeight < maxHeightColumn && this.options.fakeItem){

					var fake_elem = document.createElement(this.options.elementColumn);
						fake_elem.className = this.options.classItem + ' ' + this.options.classFakeItem;
						fake_elem.style.height = maxHeightColumn - this.columns[i]._offsetHeight + 'px';

						columnElement.appendChild(fake_elem);
				}

				if (this.options.elementColumnInner) {
					column.appendChild(columnElement);
				}

				// add the column to the DOM columns array
				this.columnsDOM[i] = column;

				grid.appendChild(column);

			}

			this.grid.innerHTML = '';
			this.grid.appendChild(grid);

			if( this.options.afterLayout ){
				this.options.afterLayout();
			}

		},

		setColumnsWidth: function ( greeedWidth ) {

			// update width to match parent width
			var newColumnWidth = Math.floor( greeedWidth / this.nbColumns );
			var greeedWidthCalc = newColumnWidth * this.nbColumns;
			var pixelsLeft = greeedWidth - greeedWidthCalc;

			for (var i = 0; i < this.columnsDOM.length; i++) {
				var colWidth = (pixelsLeft > 0) ? newColumnWidth + 1 : newColumnWidth;
				pixelsLeft--;
				this.columnsDOM[i].style.width = colWidth + 'px';
			}

		},

		checkMQ: function (event) {

			var lastNbColumns = this.nbColumns;

			this.windowWidth = window.innerWidth;

			// for each breakpoints
			for (var i = 0; i < this.options.breakpoints.length; i++) {
				var point = this.options.breakpoints[i],
					size = point * this.rootFontSize;

				// set how many columns to create
				if( window.innerWidth < size ) {
					this.nbColumns = i + 1;
					break;
				} else {
					this.nbColumns = i + 2;
				}

			}

			if (this.options.units === 'fixed') {

				// Get the width of the greeed, every time
				var greeedWidth = Math.floor(getComputedStyle(this.grid).getPropertyValue('width').replace('px',''));

				// create columns
				this.createColumns( greeedWidth );

				// set columns width, every time
				this.setColumnsWidth( greeedWidth );

			} else {

				// create columns
				this.createColumns();

			}

		}
	};

	window.greeed = {
		bind: function (elem, options) {
			goGreeed(elem, add, options);
		},
		unbind: function (elem) {
			goGreeed(elem, remove);
		}
	};

})();
<commit_msg>Switch to table layout by default Add an option to switch to float mode layout: 'table' | 'float'<commit_after>/*!
 * Greeed.js 1.0.0
 * MIT licensed
 *
 * Copyright (C) 2014 Vincent De Oliveira, http://iamvdo.me
 */
(function () {

	"use strict";

	var greeed;

	function goGreeed (grid, method, options) {

		var target = document.querySelector(grid);
		if (target !== null) {
			method.call(null, target, options);
		}
	}

	function add (grid, options) {

		greeed = new Greeed(grid, options);
		greeed.init();

	}

	function remove (grid) {

		greeed.destroy();

	}

	function Greeed (elem, options) {

		this.grid = elem;
		this.nbColumns = 0;
		this.childs = this.columns = this.options = [];

		this.rootFontSize = getComputedStyle(document.documentElement).getPropertyValue('font-size').replace('px','');

		for(var key in options){
			if(options.hasOwnProperty(key)){
				this.defaults[key] = options[key];
			}
		}
		this.options = this.defaults;

	}

	Greeed.prototype = {

		defaults: {
			units: 'fluid',
			layout: 'table',
			elementColumn: 'li',
			classColumn: 'Greeed-column',
			elementColumnInner: 'ul',
			classColumnInner: 'Greeed-column-inner',
			classItem: 'Greeed-item',
			classFakeItem: 'Greeed-item--fake',
			fakeItem: true,
			inlineStyles: true
		},

		init: function () {

			// Get elements
			this.childs = Array.prototype.slice.call(this.grid.children);
			// DOM columns
			this.columnsDOM = [];

			this.checkMQ();

			var scope = this;
			this.startCheckMQ = function(event) { scope.checkMQ(event); };

			window.addEventListener('resize', this.startCheckMQ, false);

			if ( this.options.afterInit ) {
				this.options.afterInit();
			}

		},

		destroy: function () {
			window.removeEventListener('resize', this.startCheckMQ, false);
		},

		createColumns: function ( greeedWidth ) {

			// create an Array of columns
			this.columns = new Array(this.nbColumns);
			for (var i = 0; i < this.nbColumns; i++) {
				this.columns[i] = [];
				// set height
				this.columns[i]._offsetHeight = 0;
			}

			for (var i = 0; i < this.childs.length; i++) {

				var smallestColumnHeight = 0;
				var smallestColumnIndex = 0;
				// find the smallest column to place the next child
				for (var j = 0; j < this.columns.length; j++) {
					var columnHeight = this.columns[j]._offsetHeight;
					if(j === 0){
						smallestColumnHeight = columnHeight;
					}
					if(columnHeight === 0){
						smallestColumnIndex = j;
						break;
					}
					if(columnHeight < smallestColumnHeight){
						smallestColumnIndex = j;
					}
				}

				// add child to the smallest height column
				this.columns[smallestColumnIndex].push(this.childs[i]);

				// add an id (keep the old position)
				this.childs[i]._id = i;

				// update column height
				this.columns[smallestColumnIndex]._offsetHeight += this.childs[i].offsetHeight;

			}

			// find the max-height column
			var maxHeightColumn = 0;
			for (var i = 0; i < this.columns.length; i++) {

				var height = this.columns[i]._offsetHeight;

				if( height >= maxHeightColumn){
					maxHeightColumn = height;
				}

			}

			var grid = document.createDocumentFragment();

			for (var i = 0; i < this.columns.length; i++) {

				var column = document.createElement(this.options.elementColumn);
					column.className = this.options.classColumn;
					if (this.options.inlineStyles) {
						if (this.options.layout === 'table') {
							column.style.display = 'table-cell';
							column.style.verticalAlign = 'top';
						} else {
							column.style.styleFloat = column.style.cssFloat = 'left';
							column.style.display = 'block';
							if (this.options.units === 'fixed') {
								column.style.width = Math.floor( greeedWidth / this.nbColumns ) + 'px';
							} else {
								column.style.width = (100 / this.nbColumns ) + '%';
							}
						}

					}

				if (this.options.elementColumnInner) {
					var columnElement = document.createElement(this.options.elementColumnInner);
						columnElement.className = this.options.classColumnInner;
				} else {
					var columnElement = column;
				}

				for (var j = 0; j < this.columns[i].length; j++) {
					this.columns[i][j].classList.add(this.options.classItem);
					columnElement.appendChild(this.columns[i][j]);
				}

				if( this.columns[i]._offsetHeight < maxHeightColumn && this.options.fakeItem){

					var fake_elem = document.createElement(this.options.elementColumn);
						fake_elem.className = this.options.classItem + ' ' + this.options.classFakeItem;
						fake_elem.style.height = maxHeightColumn - this.columns[i]._offsetHeight + 'px';

						columnElement.appendChild(fake_elem);
				}

				if (this.options.elementColumnInner) {
					column.appendChild(columnElement);
				}

				// add the column to the DOM columns array
				this.columnsDOM[i] = column;

				grid.appendChild(column);

			}

			this.grid.innerHTML = '';
			this.grid.appendChild(grid);

			if (this.options.layout === 'table') {
				this.grid.style.display = 'table';
				this.grid.style.tableLayout = 'fixed';
			}

			if( this.options.afterLayout ){
				this.options.afterLayout();
			}

		},

		setColumnsWidth: function ( greeedWidth ) {

			// update width to match parent width
			var newColumnWidth = Math.floor( greeedWidth / this.nbColumns );
			var greeedWidthCalc = newColumnWidth * this.nbColumns;
			var pixelsLeft = greeedWidth - greeedWidthCalc;

			for (var i = 0; i < this.columnsDOM.length; i++) {
				var colWidth = (pixelsLeft > 0) ? newColumnWidth + 1 : newColumnWidth;
				pixelsLeft--;
				this.columnsDOM[i].style.width = colWidth + 'px';
			}

		},

		checkMQ: function (event) {

			var lastNbColumns = this.nbColumns;

			this.windowWidth = window.innerWidth;

			// for each breakpoints
			for (var i = 0; i < this.options.breakpoints.length; i++) {
				var point = this.options.breakpoints[i],
					size = point * this.rootFontSize;

				// set how many columns to create
				if( window.innerWidth < size ) {
					this.nbColumns = i + 1;
					break;
				} else {
					this.nbColumns = i + 2;
				}

			}

			if (this.options.units === 'fixed') {

				// Get the width of the greeed, every time
				var greeedWidth = Math.floor(getComputedStyle(this.grid).getPropertyValue('width').replace('px',''));

				// create columns
				this.createColumns( greeedWidth );

				// set columns width, every time
				this.setColumnsWidth( greeedWidth );

			} else {

				// create columns
				this.createColumns();

			}

		}
	};

	window.greeed = {
		bind: function (elem, options) {
			goGreeed(elem, add, options);
		},
		unbind: function (elem) {
			goGreeed(elem, remove);
		}
	};

})();
<|endoftext|>


<commit_before>/*
    The SerGIS Project - sergis-server

    Copyright (c) 2015, SerGIS Project Contributors. All rights reserved.
    Use of this source code is governed by the MIT License, which can be found
    in the LICENSE.txt file.
 */

// This file handles everything having to do with serving the socket for
// sergis-client

// node modules
var path = require("path");

// our modules
var config = require("../../config"),
    db = require("../db"),
    gameCommon = require(path.join(config.SERGIS_CLIENT, "lib", "backends", "game-common"));

/**
 * Report an error.
 */
function reportError(err) {
    if (err) {
        console.error("--------------------------------------------------------------------------------");
        console.error("SerGIS Server - Game Socket ERROR at " + (new Date()) + ":\n" + (err.stack || err) + "\n\n");
    }
}

/**
 * Initialize the handler for connections to the "/game" socket.
 * This is called each time a new connection is made to the "/game" socket.
 *
 * @param socket - The Socket instance.
 * @param {Function} next - The function to call once we have initialized
 *        the socket on our end.
 */
module.exports = function (socket, next) {
    // logIn handler
    socket.on("logIn", function (gameID, username, password, callback) {
        if (!gameID) {
            callback();
            return;
        }

        // Check the login info
        db.models.User.checkLoginInfo(username, password).then(function (user) {
            if (!user) {
                callback(false);
                return;
            }

            // We have the user; get the game
            return db.models.Game.findById(gameID).populate("owner")exec().then(function (game) {
                if (!game) {
                    callback();
                    return;
                }

                // We have the game; make a game token
                return db.models.GameToken.makeGameToken(game, user).then(function (gameToken) {
                    if (!gameToken) return callback();

                    // All good!
                    callback(gameToken.clientUserObject, gameToken.token);
                }, function (err) {
                    // The user probably doesn't have access to this game
                    callback(false, false);
                });
            });
        }).then(null, function (err) {
            reportError(err);
            callback();
        });
    });

    // getUser handler
    socket.on("getUser", function (gameID, sessionID, callback) {
        if (!gameID) {
            callback();
            return;
        }

        var game;
        Promise.resolve(db.models.Game.findById(gameID).populate("owner").exec()).then(function (_game) {
            game = _game;
            if (!game) return;

            // Get the session by its ID
            return db.getSessionByID(sessionID);
        }).then(function (session) {
            if (!session || !session.user_id) {
                // No session
                return;
            }

            // Get user from session username
            return db.models.User.findById(session.user_id).exec();
        }).then(function (user) {
            if (!game) return;

            // Whether we have a user or not, try making a game token
            return db.models.GameToken.makeGameToken(game, user);
        }).then(function (gameToken) {
            if (!gameToken) return callback();
            callback(gameToken.clientUserObject, gameToken.token);
        }).then(null, function (err) {
            reportError(err);
            callback();
        });
    });

    // game function handler
    socket.on("game", function (token, func, args, callback) {
        if (gameCommon.hasOwnProperty(func) && typeof gameCommon[func] == "function") {
            var gameToken, data;
            Promise.resolve(db.models.GameToken.findOne({token: token}).populate({
                path: "game",
                select: "+jsondata",
                options: {lean: true}
            }).exec()).then(function (_gameToken) {
                gameToken = _gameToken;
                if (!gameToken) {
                    callback(false, "Invalid token");
                    return Promise.reject();
                }

                var game = gameToken.game;
                if (!game.jsondata || !game.jsondata.promptList || !game.jsondata.promptList.length) {
                    callback(false, "Invalid game data");
                    return Promise.reject();
                }

                // Run the function (in gameCommon)
                args = [game.jsondata, gameToken.state].concat(args);
                return gameCommon[func].apply(gameCommon, args).then(null, function (data) {
                    // gameCommon...'s Promise rejected
                    callback(false, data);
                });
            }).then(function (_data) {
                data = _data;
                // gameCommon function's Promise resolved
                // If it was a "getGameOverContent", delete the token (we're done)
                if (func == "getGameOverContent") {
                    return gameToken.remove();
                } else {
                    // Otherwise, make sure state is updated
                    gameToken.markModified("state");
                    return gameToken.save();
                }
            }).then(function () {
                // Yay, all good!
                callback(true, data);
            }, function (err) {
                if (err) {
                    // Server DB error
                    reportError(err);
                    callback(false, "Server error");
                }
            });
        } else {
            return callback(false, func + " does not exist.");
        }
    });

    // Everything's initialized for us; move on!
    next();
};
<commit_msg>LOLZ dots are funny<commit_after>/*
    The SerGIS Project - sergis-server

    Copyright (c) 2015, SerGIS Project Contributors. All rights reserved.
    Use of this source code is governed by the MIT License, which can be found
    in the LICENSE.txt file.
 */

// This file handles everything having to do with serving the socket for
// sergis-client

// node modules
var path = require("path");

// our modules
var config = require("../../config"),
    db = require("../db"),
    gameCommon = require(path.join(config.SERGIS_CLIENT, "lib", "backends", "game-common"));

/**
 * Report an error.
 */
function reportError(err) {
    if (err) {
        console.error("--------------------------------------------------------------------------------");
        console.error("SerGIS Server - Game Socket ERROR at " + (new Date()) + ":\n" + (err.stack || err) + "\n\n");
    }
}

/**
 * Initialize the handler for connections to the "/game" socket.
 * This is called each time a new connection is made to the "/game" socket.
 *
 * @param socket - The Socket instance.
 * @param {Function} next - The function to call once we have initialized
 *        the socket on our end.
 */
module.exports = function (socket, next) {
    // logIn handler
    socket.on("logIn", function (gameID, username, password, callback) {
        if (!gameID) {
            callback();
            return;
        }

        // Check the login info
        db.models.User.checkLoginInfo(username, password).then(function (user) {
            if (!user) {
                callback(false);
                return;
            }

            // We have the user; get the game
            return db.models.Game.findById(gameID).populate("owner").exec().then(function (game) {
                if (!game) {
                    callback();
                    return;
                }

                // We have the game; make a game token
                return db.models.GameToken.makeGameToken(game, user).then(function (gameToken) {
                    if (!gameToken) return callback();

                    // All good!
                    callback(gameToken.clientUserObject, gameToken.token);
                }, function (err) {
                    // The user probably doesn't have access to this game
                    callback(false, false);
                });
            });
        }).then(null, function (err) {
            reportError(err);
            callback();
        });
    });

    // getUser handler
    socket.on("getUser", function (gameID, sessionID, callback) {
        if (!gameID) {
            callback();
            return;
        }

        var game;
        Promise.resolve(db.models.Game.findById(gameID).populate("owner").exec()).then(function (_game) {
            game = _game;
            if (!game) return;

            // Get the session by its ID
            return db.getSessionByID(sessionID);
        }).then(function (session) {
            if (!session || !session.user_id) {
                // No session
                return;
            }

            // Get user from session username
            return db.models.User.findById(session.user_id).exec();
        }).then(function (user) {
            if (!game) return;

            // Whether we have a user or not, try making a game token
            return db.models.GameToken.makeGameToken(game, user);
        }).then(function (gameToken) {
            if (!gameToken) return callback();
            callback(gameToken.clientUserObject, gameToken.token);
        }).then(null, function (err) {
            reportError(err);
            callback();
        });
    });

    // game function handler
    socket.on("game", function (token, func, args, callback) {
        if (gameCommon.hasOwnProperty(func) && typeof gameCommon[func] == "function") {
            var gameToken, data;
            Promise.resolve(db.models.GameToken.findOne({token: token}).populate({
                path: "game",
                select: "+jsondata",
                options: {lean: true}
            }).exec()).then(function (_gameToken) {
                gameToken = _gameToken;
                if (!gameToken) {
                    callback(false, "Invalid token");
                    return Promise.reject();
                }

                var game = gameToken.game;
                if (!game.jsondata || !game.jsondata.promptList || !game.jsondata.promptList.length) {
                    callback(false, "Invalid game data");
                    return Promise.reject();
                }

                // Run the function (in gameCommon)
                args = [game.jsondata, gameToken.state].concat(args);
                return gameCommon[func].apply(gameCommon, args).then(null, function (data) {
                    // gameCommon...'s Promise rejected
                    callback(false, data);
                });
            }).then(function (_data) {
                data = _data;
                // gameCommon function's Promise resolved
                // If it was a "getGameOverContent", delete the token (we're done)
                if (func == "getGameOverContent") {
                    return gameToken.remove();
                } else {
                    // Otherwise, make sure state is updated
                    gameToken.markModified("state");
                    return gameToken.save();
                }
            }).then(function () {
                // Yay, all good!
                callback(true, data);
            }, function (err) {
                if (err) {
                    // Server DB error
                    reportError(err);
                    callback(false, "Server error");
                }
            });
        } else {
            return callback(false, func + " does not exist.");
        }
    });

    // Everything's initialized for us; move on!
    next();
};
<|endoftext|>


<commit_before>/* @flow */
/* global fetch */
import url from 'url';

import FormData from '@willdurand/isomorphic-formdata';
import utf8 from 'utf8';
import { oneLine } from 'common-tags';
import config from 'config';

import languages from 'amo/languages';
import { initialApiState } from 'amo/reducers/api';
import log from 'amo/logger';
import {
  addVersionCompatibilityToFilters,
  convertFiltersToQueryParams,
  fixFiltersForClientApp,
} from 'amo/searchUtils';
import { addQueryParams } from 'amo/utils/url';
import type { ApiState } from 'amo/reducers/api';
import type { ExternalSuggestion } from 'amo/reducers/autocomplete';
import type { ExternalAddonType } from 'amo/types/addons';
import type { LocalizedString, PaginatedApiResponse } from 'amo/types/api';
import type { ErrorHandlerType } from 'amo/types/errorHandler';
import type { ReactRouterLocationType } from 'amo/types/router';

const API_BASE = `${config.get('apiHost')}${config.get('apiPath')}`;

export const DEFAULT_API_PAGE_SIZE = 25;
export const REGION_CODE_HEADER = 'X-Country-Code';

export function makeQueryString(query: {
  [key: string]: string | null,
}): string {
  const resolvedQuery = { ...query };
  Object.keys(resolvedQuery).forEach((key) => {
    const value = resolvedQuery[key];
    if (value === undefined || value === null || value === '') {
      // Make sure we don't turn this into ?key= (empty string) because
      // sending an empty string to the API sometimes triggers bugs.
      delete resolvedQuery[key];
    }
  });
  return url.format({ query: resolvedQuery });
}

type CreateApiErrorParams = {|
  apiURL?: string,
  response: { status: number },
  jsonResponse?: Object,
|};

export function createApiError({
  apiURL,
  response,
  jsonResponse,
}: CreateApiErrorParams): Error {
  let urlId = '[unknown URL]';
  if (apiURL) {
    // Strip the host since we already know that.
    urlId = apiURL.replace(config.get('apiHost'), '');
    // Strip query string params since lang will vary quite a lot.
    urlId = urlId.split('?')[0];
  }
  const apiError = new Error(
    `Error calling: ${urlId} (status: ${response.status})`,
  );
  // $FlowFixMe: turn Error into a custom ApiError class.
  apiError.response = {
    apiURL,
    status: response.status,
    data: jsonResponse,
  };
  return apiError;
}

type CallApiParams = {|
  apiState?: ApiState,
  auth?: boolean,
  body?: Object | typeof FormData,
  credentials?: boolean,
  endpoint: string,
  errorHandler?: ErrorHandlerType,
  method?: 'GET' | 'POST' | 'DELETE' | 'HEAD' | 'OPTIONS' | 'PUT' | 'PATCH',
  params?: Object,
  _config?: typeof config,
  version?: string,
  _log?: typeof log,
|};

export function callApi({
  endpoint,
  params = {},
  auth = false,
  apiState = initialApiState,
  method = 'GET',
  body,
  credentials,
  errorHandler,
  _config = config,
  version = _config.get('apiVersion'),
  _log = log,
}: CallApiParams): Promise<Object> {
  if (!endpoint) {
    return Promise.reject(
      new Error(`endpoint URL cannot be falsy: "${endpoint}"`),
    );
  }
  if (errorHandler) {
    errorHandler.clear();
  }

  const apiPath = `${config.get('apiPath')}${version}`;

  const parsedUrl = url.parse(endpoint, true);
  let adjustedEndpoint = parsedUrl.pathname || '';
  if (!parsedUrl.host) {
    // If it's a relative URL, add the API prefix.
    const slash = !adjustedEndpoint.startsWith('/') ? '/' : '';
    adjustedEndpoint = `${apiPath}${slash}${adjustedEndpoint}`;
  } else if (!adjustedEndpoint.startsWith(apiPath)) {
    // If it's an absolute URL, it must have the correct prefix.
    return Promise.reject(
      new Error(`Absolute URL "${endpoint}" has an unexpected prefix.`),
    );
  }

  // Preserve the original query string if there is one.
  // This might happen when we parse `next` URLs returned by the API.
  const queryString = makeQueryString({
    ...parsedUrl.query,
    ...params,
    lang: apiState.lang,
  });

  const options = {
    headers: {},
    // Always make sure the method is upper case so that the browser won't
    // complain about CORS problems.
    method: method.toUpperCase(),
    credentials: undefined,
    body: undefined,
  };
  if (credentials) {
    options.credentials = 'include';
  }
  if (body) {
    if (body instanceof FormData) {
      options.body = body;
      // Let the browser sets this header, including the boundary value.
      // $FlowIgnore
      delete options.headers['Content-type'];
    } else {
      options.body = JSON.stringify(body);
      options.headers['Content-type'] = 'application/json';
    }
  }
  if (auth) {
    if (apiState.token) {
      options.headers.authorization = `Bearer ${apiState.token}`;
    }
  }
  if (apiState.regionCode) {
    options.headers[REGION_CODE_HEADER] = apiState.regionCode;
  }

  adjustedEndpoint = adjustedEndpoint.endsWith('/')
    ? adjustedEndpoint
    : `${adjustedEndpoint}/`;
  let apiURL = `${config.get('apiHost')}${adjustedEndpoint}${queryString}`;
  if (_config.get('server')) {
    _log.debug('Encoding `apiURL` in UTF8 before fetch().');
    // Workaround for https://github.com/bitinn/node-fetch/issues/245
    apiURL = utf8.encode(apiURL);
  }

  // Flow expects headers['Content-type'] to be a string, but we sometimes
  // delete it at line 148, above.
  // $FlowIgnore
  return fetch(apiURL, options)
    .then((response) => {
      // There isn't always a 'Content-Type' in headers, e.g., with a DELETE
      // method or 5xx responses.
      let contentType = response.headers.get('Content-Type');
      contentType = contentType && contentType.toLowerCase();

      // This is a bit paranoid, but we ensure the API returns a JSON response
      // (see https://github.com/mozilla/addons-frontend/issues/1701).
      // If not we'll store the text response in JSON and log an error.
      // If the JSON parsing fails; we log the error and return an "unknown
      // error".
      if (contentType === 'application/json') {
        return response
          .json()
          .then((jsonResponse) => ({ response, jsonResponse }));
      }

      return response.text().then((text) => {
        // eslint-disable-next-line amo/only-log-strings
        _log.warn(
          oneLine`Response from API was not JSON (was Content-Type:
          ${contentType}) %o`,
          {
            body: text ? text.substring(0, 100) : '[empty]',
            status: response.status || '[unknown]',
            url: response.url || '[unknown]',
          },
        );

        // jsonResponse should be an empty object in this case. Otherwise, its
        // keys could be treated as generic API errors.
        return { jsonResponse: {}, response };
      });
    })
    .then(
      ({ response, jsonResponse }) => {
        if (response.ok) {
          return jsonResponse;
        }

        // If response is not ok we'll throw an error.
        const apiError = createApiError({ apiURL, response, jsonResponse });
        if (errorHandler) {
          errorHandler.handle(apiError);
        }
        throw apiError;
      },
      (fetchError) => {
        // This actually handles the case when the call to fetch() is
        // rejected, say, for a network connection error, etc.
        if (errorHandler) {
          errorHandler.handle(fetchError);
        }
        throw fetchError;
      },
    );
}

export type FetchAddonParams = {|
  api: ApiState,
  showGroupedRatings?: boolean,
  slug: string,
|};

export function fetchAddon({
  api,
  showGroupedRatings = false,
  slug,
}: FetchAddonParams): Promise<ExternalAddonType> {
  const { clientApp, userAgentInfo } = api;
  const appVersion = userAgentInfo.browser.version;
  if (!appVersion) {
    log.warn(
      `Failed to parse appversion for client app ${clientApp || '[empty]'}`,
    );
  }

  return callApi({
    endpoint: addQueryParams(`addons/addon/${slug}`, {
      app: clientApp,
      appversion: appVersion || '',
      show_grouped_ratings: String(showGroupedRatings),
    }),
    auth: true,
    apiState: api,
  });
}

export function startLoginUrl({
  _config = config,
  location,
}: {|
  _config?: typeof config,
  location: ReactRouterLocationType,
|}): string {
  const params = {
    config: _config.get('fxaConfig'),
    to: url.format({ ...location }),
  };
  const query = makeQueryString(params);

  return `${API_BASE}${_config.get(
    'apiVersion',
  )}/accounts/login/start/${query}`;
}

export function logOutFromServer({
  api,
}: {|
  api: ApiState,
|}): Promise<Object> {
  return callApi({
    auth: true,
    credentials: true,
    endpoint: 'accounts/session',
    method: 'DELETE',
    apiState: api,
  });
}

export type AutocompleteParams = {|
  _fixFiltersForClientApp?: typeof fixFiltersForClientApp,
  api: ApiState,
  filters: {|
    query: string,
    addonType?: string,
  |},
|};

export function autocomplete({
  _fixFiltersForClientApp = fixFiltersForClientApp,
  api,
  filters,
}: AutocompleteParams): Promise<Array<ExternalSuggestion>> {
  const filtersWithAppVersion = addVersionCompatibilityToFilters({
    filters: _fixFiltersForClientApp({ api, filters }),
    userAgentInfo: api.userAgentInfo,
  });

  return callApi({
    endpoint: 'addons/autocomplete',
    params: {
      app: api.clientApp,
      ...convertFiltersToQueryParams(filtersWithAppVersion),
    },
    apiState: api,
  });
}

type GetNextResponseType = (
  nextURL?: string,
) => Promise<PaginatedApiResponse<any>>;

type AllPagesOptions = {| pageLimit: number |};

export const allPages = async (
  getNextResponse: GetNextResponseType,
  { pageLimit = 100 }: AllPagesOptions = {},
): Promise<PaginatedApiResponse<any>> => {
  let results = [];
  let nextURL;
  let count = 0;
  let pageSize = 0;

  for (let page = 1; page <= pageLimit; page++) {
    const response = await getNextResponse(nextURL);
    if (!count) {
      // Every response page returns a count for all results.
      count = response.count;
    }
    if (!pageSize) {
      pageSize = response.page_size;
    }
    results = results.concat(response.results);

    if (response.next) {
      nextURL = response.next;
      log.debug(`Fetching next page "${nextURL}"`);
    } else {
      return { count, page_size: pageSize, results };
    }
  }

  // If we get this far the callback may not be advancing pages correctly.
  throw new Error(`Fetched too many pages (the limit is ${pageLimit})`);
};

export const validateLocalizedString = (localizedString: LocalizedString) => {
  if (typeof localizedString !== 'object') {
    throw new Error(`Expected an object type, got "${typeof localizedString}"`);
  }
  Object.keys(localizedString).forEach((localeKey) => {
    if (typeof languages[localeKey] === 'undefined') {
      throw new Error(`Unknown locale: "${localeKey}"`);
    }
  });
};
<commit_msg>Downgrade a log statement from warn to debug (#10511)<commit_after>/* @flow */
/* global fetch */
import url from 'url';

import FormData from '@willdurand/isomorphic-formdata';
import utf8 from 'utf8';
import { oneLine } from 'common-tags';
import config from 'config';

import languages from 'amo/languages';
import { initialApiState } from 'amo/reducers/api';
import log from 'amo/logger';
import {
  addVersionCompatibilityToFilters,
  convertFiltersToQueryParams,
  fixFiltersForClientApp,
} from 'amo/searchUtils';
import { addQueryParams } from 'amo/utils/url';
import type { ApiState } from 'amo/reducers/api';
import type { ExternalSuggestion } from 'amo/reducers/autocomplete';
import type { ExternalAddonType } from 'amo/types/addons';
import type { LocalizedString, PaginatedApiResponse } from 'amo/types/api';
import type { ErrorHandlerType } from 'amo/types/errorHandler';
import type { ReactRouterLocationType } from 'amo/types/router';

const API_BASE = `${config.get('apiHost')}${config.get('apiPath')}`;

export const DEFAULT_API_PAGE_SIZE = 25;
export const REGION_CODE_HEADER = 'X-Country-Code';

export function makeQueryString(query: {
  [key: string]: string | null,
}): string {
  const resolvedQuery = { ...query };
  Object.keys(resolvedQuery).forEach((key) => {
    const value = resolvedQuery[key];
    if (value === undefined || value === null || value === '') {
      // Make sure we don't turn this into ?key= (empty string) because
      // sending an empty string to the API sometimes triggers bugs.
      delete resolvedQuery[key];
    }
  });
  return url.format({ query: resolvedQuery });
}

type CreateApiErrorParams = {|
  apiURL?: string,
  response: { status: number },
  jsonResponse?: Object,
|};

export function createApiError({
  apiURL,
  response,
  jsonResponse,
}: CreateApiErrorParams): Error {
  let urlId = '[unknown URL]';
  if (apiURL) {
    // Strip the host since we already know that.
    urlId = apiURL.replace(config.get('apiHost'), '');
    // Strip query string params since lang will vary quite a lot.
    urlId = urlId.split('?')[0];
  }
  const apiError = new Error(
    `Error calling: ${urlId} (status: ${response.status})`,
  );
  // $FlowFixMe: turn Error into a custom ApiError class.
  apiError.response = {
    apiURL,
    status: response.status,
    data: jsonResponse,
  };
  return apiError;
}

type CallApiParams = {|
  apiState?: ApiState,
  auth?: boolean,
  body?: Object | typeof FormData,
  credentials?: boolean,
  endpoint: string,
  errorHandler?: ErrorHandlerType,
  method?: 'GET' | 'POST' | 'DELETE' | 'HEAD' | 'OPTIONS' | 'PUT' | 'PATCH',
  params?: Object,
  _config?: typeof config,
  version?: string,
  _log?: typeof log,
|};

export function callApi({
  endpoint,
  params = {},
  auth = false,
  apiState = initialApiState,
  method = 'GET',
  body,
  credentials,
  errorHandler,
  _config = config,
  version = _config.get('apiVersion'),
  _log = log,
}: CallApiParams): Promise<Object> {
  if (!endpoint) {
    return Promise.reject(
      new Error(`endpoint URL cannot be falsy: "${endpoint}"`),
    );
  }
  if (errorHandler) {
    errorHandler.clear();
  }

  const apiPath = `${config.get('apiPath')}${version}`;

  const parsedUrl = url.parse(endpoint, true);
  let adjustedEndpoint = parsedUrl.pathname || '';
  if (!parsedUrl.host) {
    // If it's a relative URL, add the API prefix.
    const slash = !adjustedEndpoint.startsWith('/') ? '/' : '';
    adjustedEndpoint = `${apiPath}${slash}${adjustedEndpoint}`;
  } else if (!adjustedEndpoint.startsWith(apiPath)) {
    // If it's an absolute URL, it must have the correct prefix.
    return Promise.reject(
      new Error(`Absolute URL "${endpoint}" has an unexpected prefix.`),
    );
  }

  // Preserve the original query string if there is one.
  // This might happen when we parse `next` URLs returned by the API.
  const queryString = makeQueryString({
    ...parsedUrl.query,
    ...params,
    lang: apiState.lang,
  });

  const options = {
    headers: {},
    // Always make sure the method is upper case so that the browser won't
    // complain about CORS problems.
    method: method.toUpperCase(),
    credentials: undefined,
    body: undefined,
  };
  if (credentials) {
    options.credentials = 'include';
  }
  if (body) {
    if (body instanceof FormData) {
      options.body = body;
      // Let the browser sets this header, including the boundary value.
      // $FlowIgnore
      delete options.headers['Content-type'];
    } else {
      options.body = JSON.stringify(body);
      options.headers['Content-type'] = 'application/json';
    }
  }
  if (auth) {
    if (apiState.token) {
      options.headers.authorization = `Bearer ${apiState.token}`;
    }
  }
  if (apiState.regionCode) {
    options.headers[REGION_CODE_HEADER] = apiState.regionCode;
  }

  adjustedEndpoint = adjustedEndpoint.endsWith('/')
    ? adjustedEndpoint
    : `${adjustedEndpoint}/`;
  let apiURL = `${config.get('apiHost')}${adjustedEndpoint}${queryString}`;
  if (_config.get('server')) {
    _log.debug('Encoding `apiURL` in UTF8 before fetch().');
    // Workaround for https://github.com/bitinn/node-fetch/issues/245
    apiURL = utf8.encode(apiURL);
  }

  // Flow expects headers['Content-type'] to be a string, but we sometimes
  // delete it at line 148, above.
  // $FlowIgnore
  return fetch(apiURL, options)
    .then((response) => {
      // There isn't always a 'Content-Type' in headers, e.g., with a DELETE
      // method or 5xx responses.
      let contentType = response.headers.get('Content-Type');
      contentType = contentType && contentType.toLowerCase();

      // This is a bit paranoid, but we ensure the API returns a JSON response
      // (see https://github.com/mozilla/addons-frontend/issues/1701).
      // If not we'll store the text response in JSON and log an error.
      // If the JSON parsing fails; we log the error and return an "unknown
      // error".
      if (contentType === 'application/json') {
        return response
          .json()
          .then((jsonResponse) => ({ response, jsonResponse }));
      }

      return response.text().then((text) => {
        // eslint-disable-next-line amo/only-log-strings
        _log.warn(
          oneLine`Response from API was not JSON (was Content-Type:
          ${contentType}) %o`,
          {
            body: text ? text.substring(0, 100) : '[empty]',
            status: response.status || '[unknown]',
            url: response.url || '[unknown]',
          },
        );

        // jsonResponse should be an empty object in this case. Otherwise, its
        // keys could be treated as generic API errors.
        return { jsonResponse: {}, response };
      });
    })
    .then(
      ({ response, jsonResponse }) => {
        if (response.ok) {
          return jsonResponse;
        }

        // If response is not ok we'll throw an error.
        const apiError = createApiError({ apiURL, response, jsonResponse });
        if (errorHandler) {
          errorHandler.handle(apiError);
        }
        throw apiError;
      },
      (fetchError) => {
        // This actually handles the case when the call to fetch() is
        // rejected, say, for a network connection error, etc.
        if (errorHandler) {
          errorHandler.handle(fetchError);
        }
        throw fetchError;
      },
    );
}

export type FetchAddonParams = {|
  api: ApiState,
  showGroupedRatings?: boolean,
  slug: string,
|};

export function fetchAddon({
  api,
  showGroupedRatings = false,
  slug,
}: FetchAddonParams): Promise<ExternalAddonType> {
  const { clientApp, userAgentInfo } = api;
  const appVersion = userAgentInfo.browser.version;
  if (!appVersion) {
    log.debug(
      `Failed to parse appversion for client app ${clientApp || '[empty]'}`,
    );
  }

  return callApi({
    endpoint: addQueryParams(`addons/addon/${slug}`, {
      app: clientApp,
      appversion: appVersion || '',
      show_grouped_ratings: String(showGroupedRatings),
    }),
    auth: true,
    apiState: api,
  });
}

export function startLoginUrl({
  _config = config,
  location,
}: {|
  _config?: typeof config,
  location: ReactRouterLocationType,
|}): string {
  const params = {
    config: _config.get('fxaConfig'),
    to: url.format({ ...location }),
  };
  const query = makeQueryString(params);

  return `${API_BASE}${_config.get(
    'apiVersion',
  )}/accounts/login/start/${query}`;
}

export function logOutFromServer({
  api,
}: {|
  api: ApiState,
|}): Promise<Object> {
  return callApi({
    auth: true,
    credentials: true,
    endpoint: 'accounts/session',
    method: 'DELETE',
    apiState: api,
  });
}

export type AutocompleteParams = {|
  _fixFiltersForClientApp?: typeof fixFiltersForClientApp,
  api: ApiState,
  filters: {|
    query: string,
    addonType?: string,
  |},
|};

export function autocomplete({
  _fixFiltersForClientApp = fixFiltersForClientApp,
  api,
  filters,
}: AutocompleteParams): Promise<Array<ExternalSuggestion>> {
  const filtersWithAppVersion = addVersionCompatibilityToFilters({
    filters: _fixFiltersForClientApp({ api, filters }),
    userAgentInfo: api.userAgentInfo,
  });

  return callApi({
    endpoint: 'addons/autocomplete',
    params: {
      app: api.clientApp,
      ...convertFiltersToQueryParams(filtersWithAppVersion),
    },
    apiState: api,
  });
}

type GetNextResponseType = (
  nextURL?: string,
) => Promise<PaginatedApiResponse<any>>;

type AllPagesOptions = {| pageLimit: number |};

export const allPages = async (
  getNextResponse: GetNextResponseType,
  { pageLimit = 100 }: AllPagesOptions = {},
): Promise<PaginatedApiResponse<any>> => {
  let results = [];
  let nextURL;
  let count = 0;
  let pageSize = 0;

  for (let page = 1; page <= pageLimit; page++) {
    const response = await getNextResponse(nextURL);
    if (!count) {
      // Every response page returns a count for all results.
      count = response.count;
    }
    if (!pageSize) {
      pageSize = response.page_size;
    }
    results = results.concat(response.results);

    if (response.next) {
      nextURL = response.next;
      log.debug(`Fetching next page "${nextURL}"`);
    } else {
      return { count, page_size: pageSize, results };
    }
  }

  // If we get this far the callback may not be advancing pages correctly.
  throw new Error(`Fetched too many pages (the limit is ${pageLimit})`);
};

export const validateLocalizedString = (localizedString: LocalizedString) => {
  if (typeof localizedString !== 'object') {
    throw new Error(`Expected an object type, got "${typeof localizedString}"`);
  }
  Object.keys(localizedString).forEach((localeKey) => {
    if (typeof languages[localeKey] === 'undefined') {
      throw new Error(`Unknown locale: "${localeKey}"`);
    }
  });
};
<|endoftext|>


<commit_before>/* global angular */

(function () {
    angular
        .module('profile')
        .component('profileForm', {
            templateUrl: 'partials/profile-form.html',
            controller: profileForm,
            controllerAs: 'vm',
            bindings: {
                onFailure: '&',
                onSubmit: '&',
                checkAndClose: '&'
            }
        });

    profileForm.$inject = [
        'UsersService',
        'AuthService',
        'ProfileService',
        'Notification',
        '$timeout',
        '$scope',
        'DateService'
    ];

    function profileForm(UsersService, AuthService, ProfileService, Notification, $timeout, $scope, DateService) {
        const vm = this;

        vm.index = 0;
        vm.cancel = cancel;
        vm.count = 0;
        vm.profile = {};
        vm.errors = {};
        vm.changed = {};
        vm.hasAboutMeErrors = false;
        vm.unsavedData = false;
        vm.profileIsLoaded = false;
        vm.selectedItem = '0';
        vm.profileImage = false;
        vm.areSaveButtonsEnabled = false;

        let originalProfileJson = '';

        let profileWatcher;

        vm.$onInit = function () {
            vm.user = AuthService.user;
            getEditProfile();
        };

        vm.$onDestroy = function () {
            if (profileWatcher) {
                profileWatcher();
            }
        };

        vm.removeItem = (options) => {
            ProfileService.removeItem(options);
        };

        vm.addItem = (options) => {
            ProfileService.addItem(options);
        };

        vm.save = (close = false) => {
            UsersService.saveProfile(AuthService.user.researchEntity, vm.profile, vm.profileImage).then(response => {

                vm.basicInformationHasErrors = false;
                vm.aboutMeHasErrors = false;

                if (response.profile) {
                    vm.profile = response.profile;
                }

                if (response.errors && !_.isEmpty(response.errors)) {
                    vm.errors = response.errors;

                    if (
                        _.has(vm.errors, 'username') ||
                        _.has(vm.errors, 'name') ||
                        _.has(vm.errors, 'surname') ||
                        _.has(vm.errors, 'jobTitle') ||
                        _.has(vm.errors, 'phone') ||
                        _.has(vm.errors, 'groups')
                    ) {
                        vm.basicInformationHasErrors = true;
                    }

                    if (
                        _.has(vm.errors, 'image') ||
                        _.has(vm.errors, 'titles') ||
                        _.has(vm.errors, 'description') ||
                        _.has(vm.errors, 'role') ||
                        _.has(vm.errors, 'website') ||
                        _.has(vm.errors, 'address') ||
                        _.has(vm.errors, 'interests')
                    ) {
                        vm.aboutMeHasErrors = true;
                    }

                    $timeout(() => {
                        const errorTab = document.querySelector('.js-profile-tabs .nav-item.has-error');
                        if (errorTab) {
                            const errorTabIndex = errorTab.getAttribute('index');
                            $scope.active = parseInt(errorTabIndex);
                        }
                    });
                } else {
                    vm.errors = {};
                    vm.changed = {};
                }

                if (response.message) {
                    if (!_.isEmpty(response.count) || response.count > 0) {
                        vm.count = response.count;
                        Notification.error(response.message);
                    } else {
                        vm.count = 0;
                        Notification.success(response.message);

                        originalProfileJson = angular.toJson(vm.profile);

                        if (close) {
                            cancel();
                        }
                    }
                }
            });
        };

        vm.selectChanged = () => {
            vm.active = parseInt(vm.selectedItem);
        };

        vm.formatDate = DateService.format;
        vm.isBefore2015 = DateService.isBefore2015;

        vm.getType = (experience) => {
            if (experience.lines.filter(line => line.institute).length > 0) {
                return 'institute';
            }

            if (experience.lines.filter(line => line.office).length > 0) {
                return 'department';
            }

            return 'research line';
        };

        vm.getInternalExperiencesWithoutOffice = (experience) => {
            return experience.lines.filter(line => !line.office);
        };

        vm.getInternalExperiencesWithOffice = (experience) => {
            return experience.lines.filter(line => line.office);
        };

        function getEditProfile() {

            UsersService.getProfile(AuthService.user.researchEntity, true).then(response => {
                vm.profile = response.plain();
                vm.profileIsLoaded = true;
                originalProfileJson = angular.toJson(vm.profile);

                if (profileWatcher) {
                    profileWatcher();
                }

                if (!_.isEmpty(vm.profile)) {
                    vm.areSaveButtonsEnabled = true;
                }

                profileWatcher = $scope.$watch('vm.profile', function(evt){
                    vm.changed['basic-info'] = isChanged('basic-info');
                    vm.changed['about-me'] = isChanged('about-me');
                    vm.changed.socials = isChanged('socials');
                    vm.changed.experiences = isChanged('experiences');
                    vm.changed.education = isChanged('education');
                    vm.changed.certificates = isChanged('certificates');
                    vm.changed.skills = isChanged('skills');
                    vm.changed['documents-accomplishments'] = isChanged('documents-accomplishments');
                    vm.changed['public-website'] = isChanged('public-website');
                    vm.changed['export'] = isChanged('export');
                }, true);

                $scope.$broadcast('setupBasicInformation', vm.profile);
            }).catch(() => {
                Notification.error('Something went wrong when receiving your profile, please try again!');
            });
        }

        function isChanged(category) {
            const originalProfile = JSON.parse(originalProfileJson);

            switch (category) {
                case 'basic-info':
                    if (
                        angular.toJson(originalProfile.username) !== angular.toJson(vm.profile.username) ||
                        angular.toJson(originalProfile.name) !== angular.toJson(vm.profile.name) ||
                        angular.toJson(originalProfile.surname) !== angular.toJson(vm.profile.surname) ||
                        angular.toJson(originalProfile.jobTitle) !== angular.toJson(vm.profile.jobTitle) ||
                        angular.toJson(originalProfile.phone) !== angular.toJson(vm.profile.phone) ||
                        angular.toJson(originalProfile.groups) !== angular.toJson(vm.profile.groups)
                    ) {
                        return true;
                    }
                    return false;
                case 'about-me':
                    if (
                        (originalProfile.image && angular.toJson(originalProfile.image.value) !== angular.toJson(vm.profile.image.value)) ||
                        (originalProfile.image && angular.toJson(originalProfile.image.privacy) !== angular.toJson(vm.profile.image.privacy)) ||
                        vm.profile.image.file ||
                        angular.toJson(originalProfile.titles) !== angular.toJson(vm.profile.titles) ||
                        angular.toJson(originalProfile.description) !== angular.toJson(vm.profile.description) ||
                        angular.toJson(originalProfile.role) !== angular.toJson(vm.profile.role) ||
                        angular.toJson(originalProfile.website) !== angular.toJson(vm.profile.website) ||
                        angular.toJson(originalProfile.address) !== angular.toJson(vm.profile.address) ||
                        angular.toJson(originalProfile.interests) !== angular.toJson(vm.profile.interests)
                    ) {
                        return true;
                    }
                    return false;
                case 'socials':
                    if (angular.toJson(originalProfile.socials) !== angular.toJson(vm.profile.socials)) {
                        return true;
                    }
                    return false;
                case 'experiences':
                    if (angular.toJson(originalProfile.experiencesExternal) !== angular.toJson(vm.profile.experiencesExternal)) {
                        return true;
                    }
                    return false;
                case 'education':
                    if (angular.toJson(originalProfile.education) !== angular.toJson(vm.profile.education)) {
                        return true;
                    }
                    return false;
                case 'certificates':
                    if (angular.toJson(originalProfile.certificates) !== angular.toJson(vm.profile.certificates)) {
                        return true;
                    }
                    return false;
                case 'skills':
                    if (angular.toJson(originalProfile.skillCategories) !== angular.toJson(vm.profile.skillCategories)) {
                        return true;
                    }
                    return false;
                case 'documents-accomplishments':
                    if (
                        angular.toJson(originalProfile.documents) !== angular.toJson(vm.profile.documents) ||
                        angular.toJson(originalProfile.accomplishments) !== angular.toJson(vm.profile.accomplishments)
                    ) {
                        return true;
                    }
                    return false;
                case 'public-website':
                    if (angular.toJson(originalProfile.publicWebsite) !== angular.toJson(vm.profile.publicWebsite)) {
                        return true;
                    }
                    return false;
                case 'export':
                    if (angular.toJson(originalProfile.export) !== angular.toJson(vm.profile.export)) {
                        return true;
                    }
                    return false;
                default:
                    return false;
            }
        }

        function cancel() {
            if (_.isFunction(vm.checkAndClose())) {
                vm.checkAndClose()(() => angular.toJson(vm.profile) === originalProfileJson);
            }
        }

        function executeOnFailure() {
            if (_.isFunction(vm.onFailure())) {
                vm.onFailure()();
            }
        }
    }
})();
<commit_msg>Ignore changes if profile is empty<commit_after>/* global angular */

(function () {
    angular
        .module('profile')
        .component('profileForm', {
            templateUrl: 'partials/profile-form.html',
            controller: profileForm,
            controllerAs: 'vm',
            bindings: {
                onFailure: '&',
                onSubmit: '&',
                checkAndClose: '&'
            }
        });

    profileForm.$inject = [
        'UsersService',
        'AuthService',
        'ProfileService',
        'Notification',
        '$timeout',
        '$scope',
        'DateService'
    ];

    function profileForm(UsersService, AuthService, ProfileService, Notification, $timeout, $scope, DateService) {
        const vm = this;

        vm.index = 0;
        vm.cancel = cancel;
        vm.count = 0;
        vm.profile = {};
        vm.errors = {};
        vm.changed = {};
        vm.hasAboutMeErrors = false;
        vm.unsavedData = false;
        vm.profileIsLoaded = false;
        vm.selectedItem = '0';
        vm.profileImage = false;
        vm.areSaveButtonsEnabled = false;

        let originalProfileJson = '';

        let profileWatcher;

        vm.$onInit = function () {
            vm.user = AuthService.user;
            getEditProfile();
        };

        vm.$onDestroy = function () {
            if (profileWatcher) {
                profileWatcher();
            }
        };

        vm.removeItem = (options) => {
            ProfileService.removeItem(options);
        };

        vm.addItem = (options) => {
            ProfileService.addItem(options);
        };

        vm.save = (close = false) => {
            UsersService.saveProfile(AuthService.user.researchEntity, vm.profile, vm.profileImage).then(response => {

                vm.basicInformationHasErrors = false;
                vm.aboutMeHasErrors = false;

                if (response.profile) {
                    vm.profile = response.profile;
                }

                if (response.errors && !_.isEmpty(response.errors)) {
                    vm.errors = response.errors;

                    if (
                        _.has(vm.errors, 'username') ||
                        _.has(vm.errors, 'name') ||
                        _.has(vm.errors, 'surname') ||
                        _.has(vm.errors, 'jobTitle') ||
                        _.has(vm.errors, 'phone') ||
                        _.has(vm.errors, 'groups')
                    ) {
                        vm.basicInformationHasErrors = true;
                    }

                    if (
                        _.has(vm.errors, 'image') ||
                        _.has(vm.errors, 'titles') ||
                        _.has(vm.errors, 'description') ||
                        _.has(vm.errors, 'role') ||
                        _.has(vm.errors, 'website') ||
                        _.has(vm.errors, 'address') ||
                        _.has(vm.errors, 'interests')
                    ) {
                        vm.aboutMeHasErrors = true;
                    }

                    $timeout(() => {
                        const errorTab = document.querySelector('.js-profile-tabs .nav-item.has-error');
                        if (errorTab) {
                            const errorTabIndex = errorTab.getAttribute('index');
                            $scope.active = parseInt(errorTabIndex);
                        }
                    });
                } else {
                    vm.errors = {};
                    vm.changed = {};
                }

                if (response.message) {
                    if (!_.isEmpty(response.count) || response.count > 0) {
                        vm.count = response.count;
                        Notification.error(response.message);
                    } else {
                        vm.count = 0;
                        Notification.success(response.message);

                        originalProfileJson = angular.toJson(vm.profile);

                        if (close) {
                            cancel();
                        }
                    }
                }
            });
        };

        vm.selectChanged = () => {
            vm.active = parseInt(vm.selectedItem);
        };

        vm.formatDate = DateService.format;
        vm.isBefore2015 = DateService.isBefore2015;

        vm.getType = (experience) => {
            if (experience.lines.filter(line => line.institute).length > 0) {
                return 'institute';
            }

            if (experience.lines.filter(line => line.office).length > 0) {
                return 'department';
            }

            return 'research line';
        };

        vm.getInternalExperiencesWithoutOffice = (experience) => {
            return experience.lines.filter(line => !line.office);
        };

        vm.getInternalExperiencesWithOffice = (experience) => {
            return experience.lines.filter(line => line.office);
        };

        function getEditProfile() {

            UsersService.getProfile(AuthService.user.researchEntity, true).then(response => {
                vm.profile = response.plain();
                vm.profileIsLoaded = true;
                originalProfileJson = angular.toJson(vm.profile);

                if (profileWatcher) {
                    profileWatcher();
                }

                if (!_.isEmpty(vm.profile)) {
                    vm.areSaveButtonsEnabled = true;
                }

                profileWatcher = $scope.$watch('vm.profile', function(evt){
                    vm.changed['basic-info'] = isChanged('basic-info');
                    vm.changed['about-me'] = isChanged('about-me');
                    vm.changed.socials = isChanged('socials');
                    vm.changed.experiences = isChanged('experiences');
                    vm.changed.education = isChanged('education');
                    vm.changed.certificates = isChanged('certificates');
                    vm.changed.skills = isChanged('skills');
                    vm.changed['documents-accomplishments'] = isChanged('documents-accomplishments');
                    vm.changed['public-website'] = isChanged('public-website');
                    vm.changed['export'] = isChanged('export');
                }, true);

                $scope.$broadcast('setupBasicInformation', vm.profile);
            }).catch(() => {
                Notification.error('Something went wrong when receiving your profile, please try again!');
            });
        }

        function isChanged(category) {
            const originalProfile = JSON.parse(originalProfileJson);

            switch (category) {
                case 'basic-info':
                    if (
                        angular.toJson(originalProfile.username) !== angular.toJson(vm.profile.username) ||
                        angular.toJson(originalProfile.name) !== angular.toJson(vm.profile.name) ||
                        angular.toJson(originalProfile.surname) !== angular.toJson(vm.profile.surname) ||
                        angular.toJson(originalProfile.jobTitle) !== angular.toJson(vm.profile.jobTitle) ||
                        angular.toJson(originalProfile.phone) !== angular.toJson(vm.profile.phone) ||
                        angular.toJson(originalProfile.groups) !== angular.toJson(vm.profile.groups)
                    ) {
                        return true;
                    }
                    return false;
                case 'about-me':
                    if (
                        (originalProfile.image && angular.toJson(originalProfile.image.value) !== angular.toJson(vm.profile.image.value)) ||
                        (originalProfile.image && angular.toJson(originalProfile.image.privacy) !== angular.toJson(vm.profile.image.privacy)) ||
                        vm.profile.image.file ||
                        angular.toJson(originalProfile.titles) !== angular.toJson(vm.profile.titles) ||
                        angular.toJson(originalProfile.description) !== angular.toJson(vm.profile.description) ||
                        angular.toJson(originalProfile.role) !== angular.toJson(vm.profile.role) ||
                        angular.toJson(originalProfile.website) !== angular.toJson(vm.profile.website) ||
                        angular.toJson(originalProfile.address) !== angular.toJson(vm.profile.address) ||
                        angular.toJson(originalProfile.interests) !== angular.toJson(vm.profile.interests)
                    ) {
                        return true;
                    }
                    return false;
                case 'socials':
                    if (angular.toJson(originalProfile.socials) !== angular.toJson(vm.profile.socials)) {
                        return true;
                    }
                    return false;
                case 'experiences':
                    if (angular.toJson(originalProfile.experiencesExternal) !== angular.toJson(vm.profile.experiencesExternal)) {
                        return true;
                    }
                    return false;
                case 'education':
                    if (angular.toJson(originalProfile.education) !== angular.toJson(vm.profile.education)) {
                        return true;
                    }
                    return false;
                case 'certificates':
                    if (angular.toJson(originalProfile.certificates) !== angular.toJson(vm.profile.certificates)) {
                        return true;
                    }
                    return false;
                case 'skills':
                    if (angular.toJson(originalProfile.skillCategories) !== angular.toJson(vm.profile.skillCategories)) {
                        return true;
                    }
                    return false;
                case 'documents-accomplishments':
                    if (
                        angular.toJson(originalProfile.documents) !== angular.toJson(vm.profile.documents) ||
                        angular.toJson(originalProfile.accomplishments) !== angular.toJson(vm.profile.accomplishments)
                    ) {
                        return true;
                    }
                    return false;
                case 'public-website':
                    if (angular.toJson(originalProfile.publicWebsite) !== angular.toJson(vm.profile.publicWebsite)) {
                        return true;
                    }
                    return false;
                case 'export':
                    if (angular.toJson(originalProfile.export) !== angular.toJson(vm.profile.export)) {
                        return true;
                    }
                    return false;
                default:
                    return false;
            }
        }

        function cancel() {
            if (_.isFunction(vm.checkAndClose())) {
                if (vm.areSaveButtonsEnabled) {
                    vm.checkAndClose()(() => angular.toJson(vm.profile) === originalProfileJson);
                } else {
                    vm.checkAndClose()(() => true);
                }
            }
        }

        function executeOnFailure() {
            if (_.isFunction(vm.onFailure())) {
                vm.onFailure()();
            }
        }
    }
})();
<|endoftext|>


<commit_before>/**
 * ErrorHandler component.
 *
 * Google Site Kit, Copyright 2020 Google LLC
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/**
 * External dependencies
 */
import PropTypes from 'prop-types';

/**
 * WordPress dependencies
 */
import { Component, createRef } from '@wordpress/element';
import { Dashicon } from '@wordpress/components';
import { __ } from '@wordpress/i18n';

/**
 * Internal dependencies
 */
import Notification from '../notifications/notification';
import Link from '../link';
import Button from '../button';

class ErrorHandler extends Component {
	constructor( props ) {
		super( props );

		this.state = {
			error: null,
			info: null,
			copied: false,
		};

		this.errorElement = createRef();
		this.timeoutId = 0;

		this.onErrorClick = this.onErrorClick.bind( this );
		this.setCopiedState = this.changeCopiedState.bind( this, true );
		this.unsetCopiedState = this.changeCopiedState.bind( this, false );
	}

	componentDidCatch( error, info ) {
		global.console.error( 'Caught an error:', error, info );

		this.setState( { error, info } );
	}

	onErrorClick() {
		if ( ! this.errorElement || ! this.errorElement.current ) {
			return;
		}

		const range = document.createRange();
		range.selectNodeContents( this.errorElement.current );

		const selection = global.getSelection();
		selection.removeAllRanges();
		selection.addRange( range );

		document.execCommand( 'copy' );

		selection.removeAllRanges();

		if ( this.timeoutId ) {
			clearTimeout( this.timeoutId );
		}

		this.setCopiedState();
		this.timeoutId = setTimeout( this.unsetCopiedState, 1500 );
	}

	changeCopiedState( copied ) {
		this.setState( { copied } );
	}

	render() {
		const { children } = this.props;
		const { error, info, copied } = this.state;

		// If there is no caught error, render the children components normally.
		if ( ! error ) {
			return children;
		}

		const reportLink = (
			<Link href="https://wordpress.org/support/plugin/google-site-kit/" external>
				{ __( 'Report this problem', 'google-site-kit' ) }
			</Link>
		);

		const icon = (
			<Dashicon
				className="googlesitekit-margin-right-1 googlesitekit-dashicons-fill-white"
				icon={ copied ? 'yes' : 'clipboard' }
			/>
		);

		return (
			<Notification
				id="googlesitekit-error"
				title={ __( 'Site Kit encountered an error', 'google-site-kit' ) }
				description={ reportLink }
				isDismissable={ false }
				format="small"
				type="win-error"
			>

					{ error.message }
					{ info.componentStack }

				<Button icon={ icon } onClick={ this.onErrorClick }>
					{ __( 'Copy error to clipboard', 'google-site-kit' ) }
				</Button>
			</Notification>
		);
	}
}

ErrorHandler.propTypes = {
	/** @ignore */
	children: PropTypes.node.isRequired,
};

export default ErrorHandler;
<commit_msg>Replace Dashicon with @wordpress/icons.<commit_after>/**
 * ErrorHandler component.
 *
 * Google Site Kit, Copyright 2020 Google LLC
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/**
 * External dependencies
 */
import PropTypes from 'prop-types';

/**
 * WordPress dependencies
 */
import { Component, createRef } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
import { Icon, check, stack } from '@wordpress/icons';

/**
 * Internal dependencies
 */
import Notification from '../notifications/notification';
import Link from '../link';
import Button from '../button';

class ErrorHandler extends Component {
	constructor( props ) {
		super( props );

		this.state = {
			error: null,
			info: null,
			copied: false,
		};

		this.errorElement = createRef();
		this.timeoutId = 0;

		this.onErrorClick = this.onErrorClick.bind( this );
		this.setCopiedState = this.changeCopiedState.bind( this, true );
		this.unsetCopiedState = this.changeCopiedState.bind( this, false );
	}

	componentDidCatch( error, info ) {
		global.console.error( 'Caught an error:', error, info );

		this.setState( { error, info } );
	}

	onErrorClick() {
		if ( ! this.errorElement || ! this.errorElement.current ) {
			return;
		}

		const range = document.createRange();
		range.selectNodeContents( this.errorElement.current );

		const selection = global.getSelection();
		selection.removeAllRanges();
		selection.addRange( range );

		document.execCommand( 'copy' );

		selection.removeAllRanges();

		if ( this.timeoutId ) {
			clearTimeout( this.timeoutId );
		}

		this.setCopiedState();
		this.timeoutId = setTimeout( this.unsetCopiedState, 1500 );
	}

	changeCopiedState( copied ) {
		this.setState( { copied } );
	}

	render() {
		const { children } = this.props;
		const { error, info, copied } = this.state;

		// If there is no caught error, render the children components normally.
		if ( ! error ) {
			return children;
		}

		const reportLink = (
			<Link href="https://wordpress.org/support/plugin/google-site-kit/" external>
				{ __( 'Report this problem', 'google-site-kit' ) }
			</Link>
		);

		const icon = <Icon icon={ copied ? check : stack } />;

		return (
			<Notification
				id="googlesitekit-error"
				title={ __( 'Site Kit encountered an error', 'google-site-kit' ) }
				description={ reportLink }
				isDismissable={ false }
				format="small"
				type="win-error"
			>

					{ error.message }
					{ info.componentStack }

				<Button icon={ icon } onClick={ this.onErrorClick }>
					{ __( 'Copy error to clipboard', 'google-site-kit' ) }
				</Button>
			</Notification>
		);
	}
}

ErrorHandler.propTypes = {
	/** @ignore */
	children: PropTypes.node.isRequired,
};

export default ErrorHandler;
<|endoftext|>