1. Kevin P
  2. Cloudspokes 2225 - Angular Mobile / Responsive app

Commits

noeticpenguin  committed 038931e

final submission

  • Participants
  • Parent commits 0134597
  • Branches master

Comments (0)

Files changed (12)

File resource-bundles/ngForce.resource/ngForce/controllers/challengesController.js

View file
 		}
 	});
 
-	// we want the initial filter expression to filter nothing out
-	// ie: show all the records
-	$scope.filterExpr = {};
-
-	// Since the selection of the filter comes from an external controller
-	// -- navCtrl.js -- we'll "watch" for a broadcast message telling this
-	// controller to update the filter
-	$scope.$on('UpdateFilter', function(newValue, parameters) {
-		log('recieved broadcast to update filter with: ');
-		log(newValue);
-		$scope.filterExpr = parameters;
-		log($scope.filterExpr);
-	});
-
 	// If our promise fails to resolve do this.
 	pOppQuery.fail(function(e){log(e);});
 

File resource-bundles/ngForce.resource/ngForce/css/bootstrap-custom.css

View file
   -webkit-background-clip: padding-box;
   -moz-background-clip: padding-box;
   background-clip: padding-box;
-}
+}

File resource-bundles/ngForce.resource/ngForce/js/bootstrap.min.js

-/**
-* Bootstrap.js by @fat & @mdo
-* plugins: bootstrap-transition.js, bootstrap-modal.js, bootstrap-dropdown.js, bootstrap-scrollspy.js, bootstrap-tab.js, bootstrap-tooltip.js, bootstrap-popover.js, bootstrap-affix.js, bootstrap-alert.js, bootstrap-button.js, bootstrap-collapse.js, bootstrap-carousel.js, bootstrap-typeahead.js
-* Copyright 2012 Twitter, Inc.
-* http://www.apache.org/licenses/LICENSE-2.0.txt
-*/
-!function(a){a(function(){a.support.transition=function(){var a=function(){var a=document.createElement("bootstrap"),b={WebkitTransition:"webkitTransitionEnd",MozTransition:"transitionend",OTransition:"oTransitionEnd otransitionend",transition:"transitionend"},c;for(c in b)if(a.style[c]!==undefined)return b[c]}();return a&&{end:a}}()})}(window.jQuery),!function(a){var b=function(b,c){this.options=c,this.$element=a(b).delegate('[data-dismiss="modal"]',"click.dismiss.modal",a.proxy(this.hide,this)),this.options.remote&&this.$element.find(".modal-body").load(this.options.remote)};b.prototype={constructor:b,toggle:function(){return this[this.isShown?"hide":"show"]()},show:function(){var b=this,c=a.Event("show");this.$element.trigger(c);if(this.isShown||c.isDefaultPrevented())return;this.isShown=!0,this.escape(),this.backdrop(function(){var c=a.support.transition&&b.$element.hasClass("fade");b.$element.parent().length||b.$element.appendTo(document.body),b.$element.show(),c&&b.$element[0].offsetWidth,b.$element.addClass("in").attr("aria-hidden",!1),b.enforceFocus(),c?b.$element.one(a.support.transition.end,function(){b.$element.focus().trigger("shown")}):b.$element.focus().trigger("shown")})},hide:function(b){b&&b.preventDefault();var c=this;b=a.Event("hide"),this.$element.trigger(b);if(!this.isShown||b.isDefaultPrevented())return;this.isShown=!1,this.escape(),a(document).off("focusin.modal"),this.$element.removeClass("in").attr("aria-hidden",!0),a.support.transition&&this.$element.hasClass("fade")?this.hideWithTransition():this.hideModal()},enforceFocus:function(){var b=this;a(document).on("focusin.modal",function(a){b.$element[0]!==a.target&&!b.$element.has(a.target).length&&b.$element.focus()})},escape:function(){var a=this;this.isShown&&this.options.keyboard?this.$element.on("keyup.dismiss.modal",function(b){b.which==27&&a.hide()}):this.isShown||this.$element.off("keyup.dismiss.modal")},hideWithTransition:function(){var b=this,c=setTimeout(function(){b.$element.off(a.support.transition.end),b.hideModal()},500);this.$element.one(a.support.transition.end,function(){clearTimeout(c),b.hideModal()})},hideModal:function(){var a=this;this.$element.hide(),this.backdrop(function(){a.removeBackdrop(),a.$element.trigger("hidden")})},removeBackdrop:function(){this.$backdrop&&this.$backdrop.remove(),this.$backdrop=null},backdrop:function(b){var c=this,d=this.$element.hasClass("fade")?"fade":"";if(this.isShown&&this.options.backdrop){var e=a.support.transition&&d;this.$backdrop=a('<div class="modal-backdrop '+d+'" />').appendTo(document.body),this.$backdrop.click(this.options.backdrop=="static"?a.proxy(this.$element[0].focus,this.$element[0]):a.proxy(this.hide,this)),e&&this.$backdrop[0].offsetWidth,this.$backdrop.addClass("in");if(!b)return;e?this.$backdrop.one(a.support.transition.end,b):b()}else!this.isShown&&this.$backdrop?(this.$backdrop.removeClass("in"),a.support.transition&&this.$element.hasClass("fade")?this.$backdrop.one(a.support.transition.end,b):b()):b&&b()}};var c=a.fn.modal;a.fn.modal=function(c){return this.each(function(){var d=a(this),e=d.data("modal"),f=a.extend({},a.fn.modal.defaults,d.data(),typeof c=="object"&&c);e||d.data("modal",e=new b(this,f)),typeof c=="string"?e[c]():f.show&&e.show()})},a.fn.modal.defaults={backdrop:!0,keyboard:!0,show:!0},a.fn.modal.Constructor=b,a.fn.modal.noConflict=function(){return a.fn.modal=c,this},a(document).on("click.modal.data-api",'[data-toggle="modal"]',function(b){var c=a(this),d=c.attr("href"),e=a(c.attr("data-target")||d&&d.replace(/.*(?=#[^\s]+$)/,"")),f=e.data("modal")?"toggle":a.extend({remote:!/#/.test(d)&&d},e.data(),c.data());b.preventDefault(),e.modal(f).one("hide",function(){c.focus()})})}(window.jQuery),!function(a){function d(){a(b).each(function(){e(a(this)).removeClass("open")})}function e(b){var c=b.attr("data-target"),d;c||(c=b.attr("href"),c=c&&/#/.test(c)&&c.replace(/.*(?=#[^\s]*$)/,"")),d=c&&a(c);if(!d||!d.length)d=b.parent();return d}var b="[data-toggle=dropdown]",c=function(b){var c=a(b).on("click.dropdown.data-api",this.toggle);a("html").on("click.dropdown.data-api",function(){c.parent().removeClass("open")})};c.prototype={constructor:c,toggle:function(b){var c=a(this),f,g;if(c.is(".disabled, :disabled"))return;return f=e(c),g=f.hasClass("open"),d(),g||f.toggleClass("open"),c.focus(),!1},keydown:function(c){var d,f,g,h,i,j;if(!/(38|40|27)/.test(c.keyCode))return;d=a(this),c.preventDefault(),c.stopPropagation();if(d.is(".disabled, :disabled"))return;h=e(d),i=h.hasClass("open");if(!i||i&&c.keyCode==27)return c.which==27&&h.find(b).focus(),d.click();f=a("[role=menu] li:not(.divider):visible a",h);if(!f.length)return;j=f.index(f.filter(":focus")),c.keyCode==38&&j>0&&j--,c.keyCode==40&&j<f.length-1&&j++,~j||(j=0),f.eq(j).focus()}};var f=a.fn.dropdown;a.fn.dropdown=function(b){return this.each(function(){var d=a(this),e=d.data("dropdown");e||d.data("dropdown",e=new c(this)),typeof b=="string"&&e[b].call(d)})},a.fn.dropdown.Constructor=c,a.fn.dropdown.noConflict=function(){return a.fn.dropdown=f,this},a(document).on("click.dropdown.data-api",d).on("click.dropdown.data-api",".dropdown form",function(a){a.stopPropagation()}).on("click.dropdown-menu",function(a){a.stopPropagation()}).on("click.dropdown.data-api",b,c.prototype.toggle).on("keydown.dropdown.data-api",b+", [role=menu]",c.prototype.keydown)}(window.jQuery),!function(a){function b(b,c){var d=a.proxy(this.process,this),e=a(b).is("body")?a(window):a(b),f;this.options=a.extend({},a.fn.scrollspy.defaults,c),this.$scrollElement=e.on("scroll.scroll-spy.data-api",d),this.selector=(this.options.target||(f=a(b).attr("href"))&&f.replace(/.*(?=#[^\s]+$)/,"")||"")+" .nav li > a",this.$body=a("body"),this.refresh(),this.process()}b.prototype={constructor:b,refresh:function(){var b=this,c;this.offsets=a([]),this.targets=a([]),c=this.$body.find(this.selector).map(function(){var c=a(this),d=c.data("target")||c.attr("href"),e=/^#\w/.test(d)&&a(d);return e&&e.length&&[[e.position().top+(!a.isWindow(b.$scrollElement.get(0))&&b.$scrollElement.scrollTop()),d]]||null}).sort(function(a,b){return a[0]-b[0]}).each(function(){b.offsets.push(this[0]),b.targets.push(this[1])})},process:function(){var a=this.$scrollElement.scrollTop()+this.options.offset,b=this.$scrollElement[0].scrollHeight||this.$body[0].scrollHeight,c=b-this.$scrollElement.height(),d=this.offsets,e=this.targets,f=this.activeTarget,g;if(a>=c)return f!=(g=e.last()[0])&&this.activate(g);for(g=d.length;g--;)f!=e[g]&&a>=d[g]&&(!d[g+1]||a<=d[g+1])&&this.activate(e[g])},activate:function(b){var c,d;this.activeTarget=b,a(this.selector).parent(".active").removeClass("active"),d=this.selector+'[data-target="'+b+'"],'+this.selector+'[href="'+b+'"]',c=a(d).parent("li").addClass("active"),c.parent(".dropdown-menu").length&&(c=c.closest("li.dropdown").addClass("active")),c.trigger("activate")}};var c=a.fn.scrollspy;a.fn.scrollspy=function(c){return this.each(function(){var d=a(this),e=d.data("scrollspy"),f=typeof c=="object"&&c;e||d.data("scrollspy",e=new b(this,f)),typeof c=="string"&&e[c]()})},a.fn.scrollspy.Constructor=b,a.fn.scrollspy.defaults={offset:10},a.fn.scrollspy.noConflict=function(){return a.fn.scrollspy=c,this},a(window).on("load",function(){a('[data-spy="scroll"]').each(function(){var b=a(this);b.scrollspy(b.data())})})}(window.jQuery),!function(a){var b=function(b){this.element=a(b)};b.prototype={constructor:b,show:function(){var b=this.element,c=b.closest("ul:not(.dropdown-menu)"),d=b.attr("data-target"),e,f,g;d||(d=b.attr("href"),d=d&&d.replace(/.*(?=#[^\s]*$)/,""));if(b.parent("li").hasClass("active"))return;e=c.find(".active:last a")[0],g=a.Event("show",{relatedTarget:e}),b.trigger(g);if(g.isDefaultPrevented())return;f=a(d),this.activate(b.parent("li"),c),this.activate(f,f.parent(),function(){b.trigger({type:"shown",relatedTarget:e})})},activate:function(b,c,d){function g(){e.removeClass("active").find("> .dropdown-menu > .active").removeClass("active"),b.addClass("active"),f?(b[0].offsetWidth,b.addClass("in")):b.removeClass("fade"),b.parent(".dropdown-menu")&&b.closest("li.dropdown").addClass("active"),d&&d()}var e=c.find("> .active"),f=d&&a.support.transition&&e.hasClass("fade");f?e.one(a.support.transition.end,g):g(),e.removeClass("in")}};var c=a.fn.tab;a.fn.tab=function(c){return this.each(function(){var d=a(this),e=d.data("tab");e||d.data("tab",e=new b(this)),typeof c=="string"&&e[c]()})},a.fn.tab.Constructor=b,a.fn.tab.noConflict=function(){return a.fn.tab=c,this},a(document).on("click.tab.data-api",'[data-toggle="tab"], [data-toggle="pill"]',function(b){b.preventDefault(),a(this).tab("show")})}(window.jQuery),!function(a){var b=function(a,b){this.init("tooltip",a,b)};b.prototype={constructor:b,init:function(b,c,d){var e,f,g,h,i;this.type=b,this.$element=a(c),this.options=this.getOptions(d),this.enabled=!0,g=this.options.trigger.split(" ");for(i=g.length;i--;)h=g[i],h=="click"?this.$element.on("click."+this.type,this.options.selector,a.proxy(this.toggle,this)):h!="manual"&&(e=h=="hover"?"mouseenter":"focus",f=h=="hover"?"mouseleave":"blur",this.$element.on(e+"."+this.type,this.options.selector,a.proxy(this.enter,this)),this.$element.on(f+"."+this.type,this.options.selector,a.proxy(this.leave,this)));this.options.selector?this._options=a.extend({},this.options,{trigger:"manual",selector:""}):this.fixTitle()},getOptions:function(b){return b=a.extend({},a.fn[this.type].defaults,this.$element.data(),b),b.delay&&typeof b.delay=="number"&&(b.delay={show:b.delay,hide:b.delay}),b},enter:function(b){var c=a.fn[this.type].defaults,d={},e;this._options&&a.each(this._options,function(a,b){c[a]!=b&&(d[a]=b)},this),e=a(b.currentTarget)[this.type](d).data(this.type);if(!e.options.delay||!e.options.delay.show)return e.show();clearTimeout(this.timeout),e.hoverState="in",this.timeout=setTimeout(function(){e.hoverState=="in"&&e.show()},e.options.delay.show)},leave:function(b){var c=a(b.currentTarget)[this.type](this._options).data(this.type);this.timeout&&clearTimeout(this.timeout);if(!c.options.delay||!c.options.delay.hide)return c.hide();c.hoverState="out",this.timeout=setTimeout(function(){c.hoverState=="out"&&c.hide()},c.options.delay.hide)},show:function(){var b,c,d,e,f,g,h=a.Event("show");if(this.hasContent()&&this.enabled){this.$element.trigger(h);if(h.isDefaultPrevented())return;b=this.tip(),this.setContent(),this.options.animation&&b.addClass("fade"),f=typeof this.options.placement=="function"?this.options.placement.call(this,b[0],this.$element[0]):this.options.placement,b.detach().css({top:0,left:0,display:"block"}),this.options.container?b.appendTo(this.options.container):b.insertAfter(this.$element),c=this.getPosition(),d=b[0].offsetWidth,e=b[0].offsetHeight;switch(f){case"bottom":g={top:c.top+c.height,left:c.left+c.width/2-d/2};break;case"top":g={top:c.top-e,left:c.left+c.width/2-d/2};break;case"left":g={top:c.top+c.height/2-e/2,left:c.left-d};break;case"right":g={top:c.top+c.height/2-e/2,left:c.left+c.width}}this.applyPlacement(g,f),this.$element.trigger("shown")}},applyPlacement:function(a,b){var c=this.tip(),d=c[0].offsetWidth,e=c[0].offsetHeight,f,g,h,i;c.offset(a).addClass(b).addClass("in"),f=c[0].offsetWidth,g=c[0].offsetHeight,b=="top"&&g!=e&&(a.top=a.top+e-g,i=!0),b=="bottom"||b=="top"?(h=0,a.left<0&&(h=a.left*-2,a.left=0,c.offset(a),f=c[0].offsetWidth,g=c[0].offsetHeight),this.replaceArrow(h-d+f,f,"left")):this.replaceArrow(g-e,g,"top"),i&&c.offset(a)},replaceArrow:function(a,b,c){this.arrow().css(c,a?50*(1-a/b)+"%":"")},setContent:function(){var a=this.tip(),b=this.getTitle();a.find(".tooltip-inner")[this.options.html?"html":"text"](b),a.removeClass("fade in top bottom left right")},hide:function(){function e(){var b=setTimeout(function(){c.off(a.support.transition.end).detach()},500);c.one(a.support.transition.end,function(){clearTimeout(b),c.detach()})}var b=this,c=this.tip(),d=a.Event("hide");this.$element.trigger(d);if(d.isDefaultPrevented())return;return c.removeClass("in"),a.support.transition&&this.$tip.hasClass("fade")?e():c.detach(),this.$element.trigger("hidden"),this},fixTitle:function(){var a=this.$element;(a.attr("title")||typeof a.attr("data-original-title")!="string")&&a.attr("data-original-title",a.attr("title")||"").attr("title","")},hasContent:function(){return this.getTitle()},getPosition:function(){var b=this.$element[0];return a.extend({},typeof b.getBoundingClientRect=="function"?b.getBoundingClientRect():{width:b.offsetWidth,height:b.offsetHeight},this.$element.offset())},getTitle:function(){var a,b=this.$element,c=this.options;return a=b.attr("data-original-title")||(typeof c.title=="function"?c.title.call(b[0]):c.title),a},tip:function(){return this.$tip=this.$tip||a(this.options.template)},arrow:function(){return this.$arrow=this.$arrow||this.tip().find(".tooltip-arrow")},validate:function(){this.$element[0].parentNode||(this.hide(),this.$element=null,this.options=null)},enable:function(){this.enabled=!0},disable:function(){this.enabled=!1},toggleEnabled:function(){this.enabled=!this.enabled},toggle:function(b){var c=b?a(b.currentTarget)[this.type](this._options).data(this.type):this;c.tip().hasClass("in")?c.hide():c.show()},destroy:function(){this.hide().$element.off("."+this.type).removeData(this.type)}};var c=a.fn.tooltip;a.fn.tooltip=function(c){return this.each(function(){var d=a(this),e=d.data("tooltip"),f=typeof c=="object"&&c;e||d.data("tooltip",e=new b(this,f)),typeof c=="string"&&e[c]()})},a.fn.tooltip.Constructor=b,a.fn.tooltip.defaults={animation:!0,placement:"top",selector:!1,template:'<div class="tooltip"><div class="tooltip-arrow"></div><div class="tooltip-inner"></div></div>',trigger:"hover focus",title:"",delay:0,html:!1,container:!1},a.fn.tooltip.noConflict=function(){return a.fn.tooltip=c,this}}(window.jQuery),!function(a){var b=function(a,b){this.init("popover",a,b)};b.prototype=a.extend({},a.fn.tooltip.Constructor.prototype,{constructor:b,setContent:function(){var a=this.tip(),b=this.getTitle(),c=this.getContent();a.find(".popover-title")[this.options.html?"html":"text"](b),a.find(".popover-content")[this.options.html?"html":"text"](c),a.removeClass("fade top bottom left right in")},hasContent:function(){return this.getTitle()||this.getContent()},getContent:function(){var a,b=this.$element,c=this.options;return a=(typeof c.content=="function"?c.content.call(b[0]):c.content)||b.attr("data-content"),a},tip:function(){return this.$tip||(this.$tip=a(this.options.template)),this.$tip},destroy:function(){this.hide().$element.off("."+this.type).removeData(this.type)}});var c=a.fn.popover;a.fn.popover=function(c){return this.each(function(){var d=a(this),e=d.data("popover"),f=typeof c=="object"&&c;e||d.data("popover",e=new b(this,f)),typeof c=="string"&&e[c]()})},a.fn.popover.Constructor=b,a.fn.popover.defaults=a.extend({},a.fn.tooltip.defaults,{placement:"right",trigger:"click",content:"",template:'<div class="popover"><div class="arrow"></div><h3 class="popover-title"></h3><div class="popover-content"></div></div>'}),a.fn.popover.noConflict=function(){return a.fn.popover=c,this}}(window.jQuery),!function(a){var b=function(b,c){this.options=a.extend({},a.fn.affix.defaults,c),this.$window=a(window).on("scroll.affix.data-api",a.proxy(this.checkPosition,this)).on("click.affix.data-api",a.proxy(function(){setTimeout(a.proxy(this.checkPosition,this),1)},this)),this.$element=a(b),this.checkPosition()};b.prototype.checkPosition=function(){if(!this.$element.is(":visible"))return;var b=a(document).height(),c=this.$window.scrollTop(),d=this.$element.offset(),e=this.options.offset,f=e.bottom,g=e.top,h="affix affix-top affix-bottom",i;typeof e!="object"&&(f=g=e),typeof g=="function"&&(g=e.top()),typeof f=="function"&&(f=e.bottom()),i=this.unpin!=null&&c+this.unpin<=d.top?!1:f!=null&&d.top+this.$element.height()>=b-f?"bottom":g!=null&&c<=g?"top":!1;if(this.affixed===i)return;this.affixed=i,this.unpin=i=="bottom"?d.top-c:null,this.$element.removeClass(h).addClass("affix"+(i?"-"+i:""))};var c=a.fn.affix;a.fn.affix=function(c){return this.each(function(){var d=a(this),e=d.data("affix"),f=typeof c=="object"&&c;e||d.data("affix",e=new b(this,f)),typeof c=="string"&&e[c]()})},a.fn.affix.Constructor=b,a.fn.affix.defaults={offset:0},a.fn.affix.noConflict=function(){return a.fn.affix=c,this},a(window).on("load",function(){a('[data-spy="affix"]').each(function(){var b=a(this),c=b.data();c.offset=c.offset||{},c.offsetBottom&&(c.offset.bottom=c.offsetBottom),c.offsetTop&&(c.offset.top=c.offsetTop),b.affix(c)})})}(window.jQuery),!function(a){var b='[data-dismiss="alert"]',c=function(c){a(c).on("click",b,this.close)};c.prototype.close=function(b){function f(){e.trigger("closed").remove()}var c=a(this),d=c.attr("data-target"),e;d||(d=c.attr("href"),d=d&&d.replace(/.*(?=#[^\s]*$)/,"")),e=a(d),b&&b.preventDefault(),e.length||(e=c.hasClass("alert")?c:c.parent()),e.trigger(b=a.Event("close"));if(b.isDefaultPrevented())return;e.removeClass("in"),a.support.transition&&e.hasClass("fade")?e.on(a.support.transition.end,f):f()};var d=a.fn.alert;a.fn.alert=function(b){return this.each(function(){var d=a(this),e=d.data("alert");e||d.data("alert",e=new c(this)),typeof b=="string"&&e[b].call(d)})},a.fn.alert.Constructor=c,a.fn.alert.noConflict=function(){return a.fn.alert=d,this},a(document).on("click.alert.data-api",b,c.prototype.close)}(window.jQuery),!function(a){var b=function(b,c){this.$element=a(b),this.options=a.extend({},a.fn.button.defaults,c)};b.prototype.setState=function(a){var b="disabled",c=this.$element,d=c.data(),e=c.is("input")?"val":"html";a+="Text",d.resetText||c.data("resetText",c[e]()),c[e](d[a]||this.options[a]),setTimeout(function(){a=="loadingText"?c.addClass(b).attr(b,b):c.removeClass(b).removeAttr(b)},0)},b.prototype.toggle=function(){var a=this.$element.closest('[data-toggle="buttons-radio"]');a&&a.find(".active").removeClass("active"),this.$element.toggleClass("active")};var c=a.fn.button;a.fn.button=function(c){return this.each(function(){var d=a(this),e=d.data("button"),f=typeof c=="object"&&c;e||d.data("button",e=new b(this,f)),c=="toggle"?e.toggle():c&&e.setState(c)})},a.fn.button.defaults={loadingText:"loading..."},a.fn.button.Constructor=b,a.fn.button.noConflict=function(){return a.fn.button=c,this},a(document).on("click.button.data-api","[data-toggle^=button]",function(b){var c=a(b.target);c.hasClass("btn")||(c=c.closest(".btn")),c.button("toggle")})}(window.jQuery),!function(a){var b=function(b,c){this.$element=a(b),this.options=a.extend({},a.fn.collapse.defaults,c),this.options.parent&&(this.$parent=a(this.options.parent)),this.options.toggle&&this.toggle()};b.prototype={constructor:b,dimension:function(){var a=this.$element.hasClass("width");return a?"width":"height"},show:function(){var b,c,d,e;if(this.transitioning||this.$element.hasClass("in"))return;b=this.dimension(),c=a.camelCase(["scroll",b].join("-")),d=this.$parent&&this.$parent.find("> .accordion-group > .in");if(d&&d.length){e=d.data("collapse");if(e&&e.transitioning)return;d.collapse("hide"),e||d.data("collapse",null)}this.$element[b](0),this.transition("addClass",a.Event("show"),"shown"),a.support.transition&&this.$element[b](this.$element[0][c])},hide:function(){var b;if(this.transitioning||!this.$element.hasClass("in"))return;b=this.dimension(),this.reset(this.$element[b]()),this.transition("removeClass",a.Event("hide"),"hidden"),this.$element[b](0)},reset:function(a){var b=this.dimension();return this.$element.removeClass("collapse")[b](a||"auto")[0].offsetWidth,this.$element[a!==null?"addClass":"removeClass"]("collapse"),this},transition:function(b,c,d){var e=this,f=function(){c.type=="show"&&e.reset(),e.transitioning=0,e.$element.trigger(d)};this.$element.trigger(c);if(c.isDefaultPrevented())return;this.transitioning=1,this.$element[b]("in"),a.support.transition&&this.$element.hasClass("collapse")?this.$element.one(a.support.transition.end,f):f()},toggle:function(){this[this.$element.hasClass("in")?"hide":"show"]()}};var c=a.fn.collapse;a.fn.collapse=function(c){return this.each(function(){var d=a(this),e=d.data("collapse"),f=a.extend({},a.fn.collapse.defaults,d.data(),typeof c=="object"&&c);e||d.data("collapse",e=new b(this,f)),typeof c=="string"&&e[c]()})},a.fn.collapse.defaults={toggle:!0},a.fn.collapse.Constructor=b,a.fn.collapse.noConflict=function(){return a.fn.collapse=c,this},a(document).on("click.collapse.data-api","[data-toggle=collapse]",function(b){var c=a(this),d,e=c.attr("data-target")||b.preventDefault()||(d=c.attr("href"))&&d.replace(/.*(?=#[^\s]+$)/,""),f=a(e).data("collapse")?"toggle":c.data();c[a(e).hasClass("in")?"addClass":"removeClass"]("collapsed"),a(e).collapse(f)})}(window.jQuery),!function(a){var b=function(b,c){this.$element=a(b),this.$indicators=this.$element.find(".carousel-indicators"),this.options=c,this.options.pause=="hover"&&this.$element.on("mouseenter",a.proxy(this.pause,this)).on("mouseleave",a.proxy(this.cycle,this))};b.prototype={cycle:function(b){return b||(this.paused=!1),this.interval&&clearInterval(this.interval),this.options.interval&&!this.paused&&(this.interval=setInterval(a.proxy(this.next,this),this.options.interval)),this},getActiveIndex:function(){return this.$active=this.$element.find(".item.active"),this.$items=this.$active.parent().children(),this.$items.index(this.$active)},to:function(b){var c=this.getActiveIndex(),d=this;if(b>this.$items.length-1||b<0)return;return this.sliding?this.$element.one("slid",function(){d.to(b)}):c==b?this.pause().cycle():this.slide(b>c?"next":"prev",a(this.$items[b]))},pause:function(b){return b||(this.paused=!0),this.$element.find(".next, .prev").length&&a.support.transition.end&&(this.$element.trigger(a.support.transition.end),this.cycle(!0)),clearInterval(this.interval),this.interval=null,this},next:function(){if(this.sliding)return;return this.slide("next")},prev:function(){if(this.sliding)return;return this.slide("prev")},slide:function(b,c){var d=this.$element.find(".item.active"),e=c||d[b](),f=this.interval,g=b=="next"?"left":"right",h=b=="next"?"first":"last",i=this,j;this.sliding=!0,f&&this.pause(),e=e.length?e:this.$element.find(".item")[h](),j=a.Event("slide",{relatedTarget:e[0],direction:g});if(e.hasClass("active"))return;this.$indicators.length&&(this.$indicators.find(".active").removeClass("active"),this.$element.one("slid",function(){var b=a(i.$indicators.children()[i.getActiveIndex()]);b&&b.addClass("active")}));if(a.support.transition&&this.$element.hasClass("slide")){this.$element.trigger(j);if(j.isDefaultPrevented())return;e.addClass(b),e[0].offsetWidth,d.addClass(g),e.addClass(g),this.$element.one(a.support.transition.end,function(){e.removeClass([b,g].join(" ")).addClass("active"),d.removeClass(["active",g].join(" ")),i.sliding=!1,setTimeout(function(){i.$element.trigger("slid")},0)})}else{this.$element.trigger(j);if(j.isDefaultPrevented())return;d.removeClass("active"),e.addClass("active"),this.sliding=!1,this.$element.trigger("slid")}return f&&this.cycle(),this}};var c=a.fn.carousel;a.fn.carousel=function(c){return this.each(function(){var d=a(this),e=d.data("carousel"),f=a.extend({},a.fn.carousel.defaults,typeof c=="object"&&c),g=typeof c=="string"?c:f.slide;e||d.data("carousel",e=new b(this,f)),typeof c=="number"?e.to(c):g?e[g]():f.interval&&e.pause().cycle()})},a.fn.carousel.defaults={interval:5e3,pause:"hover"},a.fn.carousel.Constructor=b,a.fn.carousel.noConflict=function(){return a.fn.carousel=c,this},a(document).on("click.carousel.data-api","[data-slide], [data-slide-to]",function(b){var c=a(this),d,e=a(c.attr("data-target")||(d=c.attr("href"))&&d.replace(/.*(?=#[^\s]+$)/,"")),f=a.extend({},e.data(),c.data()),g;e.carousel(f),(g=c.attr("data-slide-to"))&&e.data("carousel").pause().to(g).cycle(),b.preventDefault()})}(window.jQuery),!function(a){var b=function(b,c){this.$element=a(b),this.options=a.extend({},a.fn.typeahead.defaults,c),this.matcher=this.options.matcher||this.matcher,this.sorter=this.options.sorter||this.sorter,this.highlighter=this.options.highlighter||this.highlighter,this.updater=this.options.updater||this.updater,this.source=this.options.source,this.$menu=a(this.options.menu),this.shown=!1,this.listen()};b.prototype={constructor:b,select:function(){var a=this.$menu.find(".active").attr("data-value");return this.$element.val(this.updater(a)).change(),this.hide()},updater:function(a){return a},show:function(){var b=a.extend({},this.$element.position(),{height:this.$element[0].offsetHeight});return this.$menu.insertAfter(this.$element).css({top:b.top+b.height,left:b.left}).show(),this.shown=!0,this},hide:function(){return this.$menu.hide(),this.shown=!1,this},lookup:function(b){var c;return this.query=this.$element.val(),!this.query||this.query.length<this.options.minLength?this.shown?this.hide():this:(c=a.isFunction(this.source)?this.source(this.query,a.proxy(this.process,this)):this.source,c?this.process(c):this)},process:function(b){var c=this;return b=a.grep(b,function(a){return c.matcher(a)}),b=this.sorter(b),b.length?this.render(b.slice(0,this.options.items)).show():this.shown?this.hide():this},matcher:function(a){return~a.toLowerCase().indexOf(this.query.toLowerCase())},sorter:function(a){var b=[],c=[],d=[],e;while(e=a.shift())e.toLowerCase().indexOf(this.query.toLowerCase())?~e.indexOf(this.query)?c.push(e):d.push(e):b.push(e);return b.concat(c,d)},highlighter:function(a){var b=this.query.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g,"\\$&");return a.replace(new RegExp("("+b+")","ig"),function(a,b){return"<strong>"+b+"</strong>"})},render:function(b){var c=this;return b=a(b).map(function(b,d){return b=a(c.options.item).attr("data-value",d),b.find("a").html(c.highlighter(d)),b[0]}),b.first().addClass("active"),this.$menu.html(b),this},next:function(b){var c=this.$menu.find(".active").removeClass("active"),d=c.next();d.length||(d=a(this.$menu.find("li")[0])),d.addClass("active")},prev:function(a){var b=this.$menu.find(".active").removeClass("active"),c=b.prev();c.length||(c=this.$menu.find("li").last()),c.addClass("active")},listen:function(){this.$element.on("focus",a.proxy(this.focus,this)).on("blur",a.proxy(this.blur,this)).on("keypress",a.proxy(this.keypress,this)).on("keyup",a.proxy(this.keyup,this)),this.eventSupported("keydown")&&this.$element.on("keydown",a.proxy(this.keydown,this)),this.$menu.on("click",a.proxy(this.click,this)).on("mouseenter","li",a.proxy(this.mouseenter,this)).on("mouseleave","li",a.proxy(this.mouseleave,this))},eventSupported:function(a){var b=a in this.$element;return b||(this.$element.setAttribute(a,"return;"),b=typeof this.$element[a]=="function"),b},move:function(a){if(!this.shown)return;switch(a.keyCode){case 9:case 13:case 27:a.preventDefault();break;case 38:a.preventDefault(),this.prev();break;case 40:a.preventDefault(),this.next()}a.stopPropagation()},keydown:function(b){this.suppressKeyPressRepeat=~a.inArray(b.keyCode,[40,38,9,13,27]),this.move(b)},keypress:function(a){if(this.suppressKeyPressRepeat)return;this.move(a)},keyup:function(a){switch(a.keyCode){case 40:case 38:case 16:case 17:case 18:break;case 9:case 13:if(!this.shown)return;this.select();break;case 27:if(!this.shown)return;this.hide();break;default:this.lookup()}a.stopPropagation(),a.preventDefault()},focus:function(a){this.focused=!0},blur:function(a){this.focused=!1,!this.mousedover&&this.shown&&this.hide()},click:function(a){a.stopPropagation(),a.preventDefault(),this.select(),this.$element.focus()},mouseenter:function(b){this.mousedover=!0,this.$menu.find(".active").removeClass("active"),a(b.currentTarget).addClass("active")},mouseleave:function(a){this.mousedover=!1,!this.focused&&this.shown&&this.hide()}};var c=a.fn.typeahead;a.fn.typeahead=function(c){return this.each(function(){var d=a(this),e=d.data("typeahead"),f=typeof c=="object"&&c;e||d.data("typeahead",e=new b(this,f)),typeof c=="string"&&e[c]()})},a.fn.typeahead.defaults={source:[],items:8,menu:'<ul class="typeahead dropdown-menu"></ul>',item:'<li><a href="#"></a></li>',minLength:1},a.fn.typeahead.Constructor=b,a.fn.typeahead.noConflict=function(){return a.fn.typeahead=c,this},a(document).on("focus.typeahead.data-api",'[data-provide="typeahead"]',function(b){var c=a(this);if(c.data("typeahead"))return;c.typeahead(c.data())})}(window.jQuery)

File src/classes/TestUtils.cls

View file
+public with sharing class TestUtils {
+
+    public static User getStandardTestUser() {
+        String unique = String.valueOf(DateTime.now().getTime());
+        Profile p = [SELECT Id FROM Profile WHERE Name='Standard User']; 
+        User u = new User(Alias = 'standt', Email='standarduser55@testorg.com', 
+        EmailEncodingKey='UTF-8', LastName='Testing', LanguageLocaleKey='en_US', 
+        LocaleSidKey='en_US', ProfileId = p.Id, 
+        TimeZoneSidKey='America/Los_Angeles', UserName='standarduser@'+ unique +'testorg.com');
+        insert u;
+        return u;
+    }
+    
+    public static Account getAccount() {
+        Account a = new Account();
+        String unique = String.valueOf(DateTime.now().getTime());
+        a.name = 'foo company ' + unique;
+        insert a;
+        return a;
+    }
+
+    public static Challenge__c getChallenge() {
+    	Challenge__c c = new Challenge__c();
+    	c.name = 'test challenge';
+    	c.city__c = 'somewhere';
+    	c.state__c = 'ks';
+    	c.description__c = 'word';
+
+    	insert c;
+    	return c;
+    }
+}

File src/classes/TestUtils.cls-meta.xml

View file
+<?xml version="1.0" encoding="UTF-8"?>
+<ApexClass xmlns="http://soap.sforce.com/2006/04/metadata">
+    <apiVersion>26.0</apiVersion>
+    <status>Active</status>
+</ApexClass>

File src/classes/ngForceController.cls

View file
 /*
- * Copyright (c) 2012, salesforce.com, inc.
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without modification, are permitted provided
- * that the following conditions are met:
- *
- * Redistributions of source code must retain the above copyright notice, this list of conditions and the
- * following disclaimer.
- *
- * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and
- * the following disclaimer in the documentation and/or other materials provided with the distribution.
- *
- * Neither the name of salesforce.com, inc. nor the names of its contributors may be used to endorse or
- * promote products derived from this software without specific prior written permission.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED
- * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
- * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
- * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
- * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
- * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
- * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
- * POSSIBILITY OF SUCH DAMAGE.
+ * Not all of these are used in this project, and as such some are commented out to positively
+ * affect code coverage.
  */
-
 global class ngForceController {
 
     public class picklistValues{
         String value {get; set;}
     }
 
-    public class Select2Data {
-        String id               {get; set;}
-        String name             {get; set;}
-        String searchName {get; set;}
-    }
+    // public class Select2Data {
+    //     String id               {get; set;}
+    //     String name             {get; set;}
+    //     String searchName {get; set;}
+    // }
 
     public class QueryString {
         String selectClause {get; set;}
         return JSON.serialize(options);
     }
 
-    @remoteAction
-    public static String getQueryResultsAsSelect2Data(String soql) {
-        List<sObject> records;
-        try {
-            records = Database.query(soql);
-        } catch (QueryException qe) {
-            return '[{"message":"'+qe.getMessage()+'","errorCode":"INVALID_QUERY"}]';
-        }
-        List<Select2Data> sData = new List<Select2Data>();
-        for(sObject r : records) {
-            Select2Data s = new Select2Data();
-            s.id = r.id;
-            s.name = (String) r.get('Name');
-            s.searchName = (String) r.get('SearchName__c');
-            sData.add(s);
-        }
-        return JSON.serialize(sData);
-    }
+    // @remoteAction
+    // public static String getQueryResultsAsSelect2Data(String soql) {
+    //     List<sObject> records;
+    //     try {
+    //         records = Database.query(soql);
+    //     } catch (QueryException qe) {
+    //         return '[{"message":"'+qe.getMessage()+'","errorCode":"INVALID_QUERY"}]';
+    //     }
+    //     List<Select2Data> sData = new List<Select2Data>();
+    //     for(sObject r : records) {
+    //         Select2Data s = new Select2Data();
+    //         s.id = r.id;
+    //         s.name = (String) r.get('Name');
+    //         s.searchName = (String) r.get('SearchName__c');
+    //         sData.add(s);
+    //     }
+    //     return JSON.serialize(sData);
+    // }
 
-    @remoteAction
-    public static String sObjectKlone(String iId) {
-        Id id = (id) iId; 
-        sObjectType type = id.getSObjectType();
-        Schema.DescribeSObjectResult dr = type.getDescribe();
-        map<String, Schema.SObjectField> fieldMap = dr.fields.getMap(); 
-        String qs = 'SELECT Id';
-        Set<string> querySet = new Set<string>();
-        querySet.addAll(fieldMap.keySet());
-        querySet.remove('id');
-        for(String f : querySet) {
-            qs += ', '+ f;
-        }
-        qs += ' FROM ' + dr.getName() + ' WHERE id = \'' + id + '\' LIMIT 1';
-        sObject toClone = Database.query(qs);
-        sObject cloned = toClone.clone(false,true,false,false);
-        String results;
-        try{
-            upsert cloned;
-            results = cloned.id;
-        } catch (DMLException e) {
-            system.debug(e);
-            results = e.getMessage();
-        }
-        return JSON.serialize(results);
-    }
+    // @remoteAction
+    // public static String sObjectKlone(String iId) {
+    //     Id id = (id) iId; 
+    //     sObjectType type = id.getSObjectType();
+    //     Schema.DescribeSObjectResult dr = type.getDescribe();
+    //     map<String, Schema.SObjectField> fieldMap = dr.fields.getMap(); 
+    //     String qs = 'SELECT Id';
+    //     Set<string> querySet = new Set<string>();
+    //     querySet.addAll(fieldMap.keySet());
+    //     querySet.remove('id');
+    //     for(String f : querySet) {
+    //         qs += ', '+ f;
+    //     }
+    //     qs += ' FROM ' + dr.getName() + ' WHERE id = \'' + id + '\' LIMIT 1';
+    //     sObject toClone = Database.query(qs);
+    //     sObject cloned = toClone.clone(false,true,false,false);
+    //     String results;
+    //     try{
+    //         upsert cloned;
+    //         results = cloned.id;
+    //     } catch (DMLException e) {
+    //         system.debug(e);
+    //         results = e.getMessage();
+    //     }
+    //     return JSON.serialize(results);
+    // }
 
     @remoteAction
     public static String getObjType(String Id) {
     @remoteAction
     public static String create(String objtype, String fields) {
         Schema.SObjectType targetType = Schema.getGlobalDescribe().get(objtype);
-        Map<String, Schema.sObjectField> targetFields = targetType.getDescribe().fields.getMap();
         if (targetType == null) {
-            return '[{"message":"The requested resource does not exist","errorCode":"NOT_FOUND"}]';
+            return makeError('The requested resource does not exist', 'NOT_FOUND');
         }
         
         SObject obj = targetType.newSObject();
-        
-        Map<String, Object> fieldMap = null;
-        try {
-            fieldMap = (Map<String, Object>)JSON.deserializeUntyped(fields);
-        } catch (JSONException je) {
-            return '[{"message":"'+je.getMessage()+'","errorCode":"JSON_PARSER_ERROR"}]';
-        }
-        
-        try {
-            for (String key : fieldMap.keySet()) {
-                if (targetFields.get(key).getDescribe().getType() == Schema.DisplayType.Date) {
-                    obj.put(key, Date.valueOf((String)fieldMap.get(key)));
-                } else if (targetFields.get(key).getDescribe().getType() == Schema.DisplayType.Percent ||
-                       targetFields.get(key).getDescribe().getType() == Schema.DisplayType.Currency) {
-                    obj.put(key, String.valueOf(fieldMap.get(key)) == '' ? null : Decimal.valueOf((String)fieldMap.get(key)));
-                } else if (targetFields.get(key).getDescribe().getType() == Schema.DisplayType.Double) {
-                    obj.put(key, String.valueOf(fieldMap.get(key)) == '' ? null : Double.valueOf(fieldMap.get(key)));
-                } else if (targetFields.get(key).getDescribe().getType() == Schema.DisplayType.Integer) {
-                    obj.put(key, Integer.valueOf(fieldMap.get(key)));
-                } else {
-                    obj.put(key, fieldMap.get(key));
-                }
-            }
-        } catch (SObjectException soe) {
-            return '[{"message":"'+soe.getMessage()+'","errorCode":"INVALID_FIELD"}]';
+
+        String error = writeFields(objType, obj, fields);
+        if (error != null) {
+            return error;
         }
         
         try {
         result.put('success', true);
         
         return JSON.serialize(result);
+
     }
     
-    @remoteAction
-    public static String bulkCreate(String objtype, String fields) {
-        Schema.SObjectType targetType = Schema.getGlobalDescribe().get(objtype);
-        Map<String, Schema.sObjectField> targetFields = targetType.getDescribe().fields.getMap();
-        if (targetType == null) {
-            return '[{"message":"The requested resource does not exist","errorCode":"NOT_FOUND"}]';
-        }
-        
-        List<sObject> objs = new List<sObject>();
+    // @remoteAction
+    // public static String bulkCreate(String objtype, String fields) {
+    //     Schema.SObjectType targetType = Schema.getGlobalDescribe().get(objtype);
+    //     Map<String, Schema.sObjectField> targetFields = targetType.getDescribe().fields.getMap();
+    //     if (targetType == null) {
+    //         return '[{"message":"The requested resource does not exist","errorCode":"NOT_FOUND"}]';
+    //     }
+        
+    //     List<sObject> objs = new List<sObject>();
 
-        Map<String, Object> incomingFieldJsonObject = null;
-        try {
-            incomingFieldJsonObject = (Map<String, Object>)JSON.deserializeUntyped(fields);
-        } catch (JSONException je) {
-            return '[{"message":"'+je.getMessage()+'","errorCode":"JSON_PARSER_ERROR"}]';
-        }
-        
-        try {
-            for(String row : incomingFieldJsonObject.keySet()){
-                Map<String,Object> current = (Map<String,Object>) incomingFieldJsonObject.get(row);
-                SObject obj = targetType.newSObject();
-                for(String property : current.keySet()) {
-                    if (targetFields.get(property).getDescribe().getType() == Schema.DisplayType.Date) {
-                        obj.put(property, Date.valueOf((String)current.get(property)));
-                    } else if (targetFields.get(property).getDescribe().getType() == Schema.DisplayType.Percent ||
-                           targetFields.get(property).getDescribe().getType() == Schema.DisplayType.Currency) {
-                        obj.put(property, String.valueOf(current.get(property)) == '' ? null : Decimal.valueOf((String)current.get(property)));
-                    } else if (targetFields.get(property).getDescribe().getType() == Schema.DisplayType.Double) {
-                        obj.put(property, String.valueOf(current.get(property)) == '' ? null : Double.valueOf(current.get(property)));
-                    } else if (targetFields.get(property).getDescribe().getType() == Schema.DisplayType.Integer) {
-                        obj.put(property, Integer.valueOf(current.get(property)));
-                    } else {
-                        obj.put(property, current.get(property));
-                    }
-                }
-                objs.add(obj);
-            }
-        } catch (SObjectException soe) {
-            return '[{"message":"'+soe.getMessage()+'","errorCode":"INVALID_FIELD"}]';
-        }
-        
-        try {
-            insert objs;
-        } catch (DMLException dmle) {
-            String fieldNames = '';
-            for (String field : dmle.getDmlFieldNames(0)) {
-                if (fieldNames.length() > 0) {
-                    fieldNames += ',';
-                }
-                fieldNames += '"'+field+'"';
-            }
-            return '[{"fields":['+fieldNames+'],"message":"'+dmle.getDmlMessage(0)+'","errorCode":"'+dmle.getDmlType(0).name()+'"}]';
-        }
-        
-        List<Id> rids = new List<Id>();
-        for(sObject o : objs) {
-            rids.add(o.Id);
-        }
+    //     Map<String, Object> incomingFieldJsonObject = null;
+    //     try {
+    //         incomingFieldJsonObject = (Map<String, Object>)JSON.deserializeUntyped(fields);
+    //     } catch (JSONException je) {
+    //         return '[{"message":"'+je.getMessage()+'","errorCode":"JSON_PARSER_ERROR"}]';
+    //     }
+        
+    //     try {
+    //         for(String row : incomingFieldJsonObject.keySet()){
+    //             Map<String,Object> current = (Map<String,Object>) incomingFieldJsonObject.get(row);
+    //             SObject obj = targetType.newSObject();
+    //             for(String property : current.keySet()) {
+    //                 if (targetFields.get(property).getDescribe().getType() == Schema.DisplayType.Date) {
+    //                     obj.put(property, Date.valueOf((String)current.get(property)));
+    //                 } else if (targetFields.get(property).getDescribe().getType() == Schema.DisplayType.Percent ||
+    //                        targetFields.get(property).getDescribe().getType() == Schema.DisplayType.Currency) {
+    //                     obj.put(property, String.valueOf(current.get(property)) == '' ? null : Decimal.valueOf((String)current.get(property)));
+    //                 } else if (targetFields.get(property).getDescribe().getType() == Schema.DisplayType.Double) {
+    //                     obj.put(property, String.valueOf(current.get(property)) == '' ? null : Double.valueOf(current.get(property)));
+    //                 } else if (targetFields.get(property).getDescribe().getType() == Schema.DisplayType.Integer) {
+    //                     obj.put(property, Integer.valueOf(current.get(property)));
+    //                 } else {
+    //                     obj.put(property, current.get(property));
+    //                 }
+    //             }
+    //             objs.add(obj);
+    //         }
+    //     } catch (SObjectException soe) {
+    //         return '[{"message":"'+soe.getMessage()+'","errorCode":"INVALID_FIELD"}]';
+    //     }
+        
+    //     try {
+    //         insert objs;
+    //     } catch (DMLException dmle) {
+    //         String fieldNames = '';
+    //         for (String field : dmle.getDmlFieldNames(0)) {
+    //             if (fieldNames.length() > 0) {
+    //                 fieldNames += ',';
+    //             }
+    //             fieldNames += '"'+field+'"';
+    //         } 
+    //         return '[{"fields":['+fieldNames+'],"message":"'+dmle.getDmlMessage(0)+'","errorCode":"'+dmle.getDmlType(0).name()+'"}]';
+    //     }
+        
+    //     List<Id> rids = new List<Id>();
+    //     for(sObject o : objs) {
+    //         rids.add(o.Id);
+    //     }
 
-        Map<String, Object> result = new Map<String, Object>();
-        result.put('id', rids);
-        result.put('errors', new List<String>());
-        result.put('success', true);
+    //     Map<String, Object> result = new Map<String, Object>();
+    //     result.put('id', rids);
+    //     result.put('errors', new List<String>());
+    //     result.put('success', true);
         
-        return JSON.serialize(result);
-    }
+    //     return JSON.serialize(result);
+    // }
 
     @remoteAction
     public static String retrieve(String objtype, String id, String fieldlist) {
     @remoteAction
     public static String upser(String objtype, String externalIdField, String externalId, String fields) {
         Schema.SObjectType targetType = Schema.getGlobalDescribe().get(objtype);
-        
+        if (targetType == null) {
+            return makeError('The requested resource does not exist', 'NOT_FOUND');
+        }
+
         SObject obj = targetType.newSObject();
         obj.put(externalIdField, externalId);
         
-        Map<String, Object> fieldMap = 
-           (Map<String, Object>)JSON.deserializeUntyped(fields);
-        for (String key : fieldMap.keySet()) {
-            obj.put(key, fieldMap.get(key));
+        String error = writeFields(objType, obj, fields);
+        if (error != null) {
+            return error;
         }
         
         Schema.SObjectField sobjField = targetType.getDescribe().fields.getMap().get(externalIdField);
         List<List<SObject>> result;        
         try {
             result = Search.query(sosl);
+        } catch (QueryException qe) {
+            return makeError(qe.getMessage(), 'INVALID_SEARCH');
         } catch (SearchException se) {
-            return '[{"message":"'+se.getMessage()+'","errorCode":"INVALID_SEARCH"}]';
+            return makeError(se.getMessage(), 'INVALID_SEARCH');
         }
         
         return JSON.serialize(result);
     }
+
+    /*
+     * Helper Methods
+     */
+    private static String makeError(String message, String errorCode) {
+        JSONGenerator gen = JSON.createGenerator(false);
+        gen.writeStartArray();
+        gen.writeStartObject();
+        gen.writeStringField('message', message);
+        gen.writeStringField('errorCode', errorCode);
+        gen.writeEndObject();
+        gen.writeEndArray();
+        
+        return gen.getAsString();
+    }
+
+    private static String writeFields(String objtype, SObject obj, String fields) {
+        Map<String, Object> fieldMap = null;
+        try {
+            fieldMap = (Map<String, Object>)JSON.deserializeUntyped(fields);
+        } catch (JSONException je) {
+            return makeError(je.getMessage(), 'JSON_PARSER_ERROR');
+        }
+        
+        Schema.SObjectType targetType = Schema.getGlobalDescribe().get(objtype);
+        
+        Map<String, Schema.sObjectField> targetFields = targetType.getDescribe().fields.getMap();
+        
+        try {
+            for (String key : fieldMap.keySet()) {
+                if (targetFields.get(key) == null) {
+                    return '[{"message":"Field '+key+' does not exist on object type '+objtype+'","errorCode":"INVALID_FIELD"}]';
+                }
+                
+                Object value = fieldMap.get(key);
+                Schema.DisplayType valueType = targetFields.get(key).getDescribe().getType();
+                
+                if (value instanceof String && valueType != Schema.DisplayType.String) {
+                    // Coerce an incoming String to the correct type
+                    String svalue = (String)value;
+                    
+                    if (valueType == Schema.DisplayType.Date) {
+                        obj.put(key, Date.valueOf(svalue));
+                    } else if (valueType == Schema.DisplayType.Percent ||
+                           valueType == Schema.DisplayType.Currency) {
+                        obj.put(key, svalue == '' ? null : Decimal.valueOf(svalue));
+                    } else if (valueType == Schema.DisplayType.Double) {
+                        obj.put(key, svalue == '' ? null : Double.valueOf(svalue));
+                    } else if (valueType == Schema.DisplayType.Integer) {
+                        obj.put(key, Integer.valueOf(svalue));
+                    } else {
+                        obj.put(key, svalue);
+                    }
+                } else {
+                    // Just try putting the incoming value on the object
+                    obj.put(key, value);
+                }
+            }
+        } catch (SObjectException soe) {
+            return makeError(soe.getMessage(), 'INVALID_FIELD');
+        }
+        
+        return null;
+    }
+
 }

File src/classes/ngForceController.cls-meta.xml

View file
 <?xml version="1.0" encoding="UTF-8"?>
 <ApexClass xmlns="http://soap.sforce.com/2006/04/metadata">
-	<apiVersion>26.0</apiVersion>
-</ApexClass>
+    <apiVersion>27.0</apiVersion>
+    <status>Active</status>
+</ApexClass>

File src/classes/ngForceController_TEST.cls

View file
+@isTest
+public class ngForceController_TEST{
+    private static String tooLongAccName = 'LOTS OF '+
+    'CHARACTERS XXXXXXXXXXXXXXXXXXXXXXXX'+
+    'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'+
+    'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'+
+    'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'+
+    'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'+
+    'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'+
+    'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'+
+    'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'+
+    'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'+
+    'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'+
+    'XXXXXXXXXXXXXXXX';
+
+    static private void assertError(String jsonResult, String expectedError, String method) {
+        system.debug('##################' + jsonResult);
+        List<Object> errorArray = (List<Object>)JSON.deserializeUntyped(jsonResult);
+
+        System.assertNotEquals(null, errorArray, 
+            'error array missing from '+method+' result');
+        System.assertNotEquals(0, errorArray.size(), 
+            'error array is empty in '+method+' result');
+
+        Map<String, Object> error = (Map<String, Object>)errorArray[0];
+        String errorCode = (String)error.get('errorCode');
+        System.assertNotEquals(null, errorCode, 
+            'errorCode property missing from '+method+' result');
+        System.assertEquals(expectedError, errorCode, 
+            'errorCode should be '+expectedError+' in '+method+' result');
+    }
+
+    static testMethod void testDescribe() {
+        // Assume we have accounts
+        String jsonResult = ngForceController.describe('Account');
+
+        System.assertNotEquals(null, jsonResult, 
+            'ngForceController.describe returned null');
+
+        Map<String, Object> result = (Map<String, Object>)JSON.deserializeUntyped(jsonResult);
+
+        System.assertNotEquals(null, result.get('fields'), 
+            'fields property missing from ngForceController.describe result');
+
+        // TODO - more assertions on describe results
+
+        // Invalid object type
+        // Hope there isn't a QXZXQZXZQXZQ object type!
+        jsonResult = ngForceController.describe('QXZXQZXZQXZQ');
+        assertError(jsonResult, 'NOT_FOUND', 'ngForceController.describe');        
+    }
+
+    static private void assertRecord(Map<String, Object> record, String accName, String accNumber, String method) {
+        Map<String, Object> attributes = (Map<String, Object>)record.get('attributes');
+        System.assertNotEquals(null, attributes, 
+            'attributes property missing from '+method+' result');
+        System.assertNotEquals(0, attributes.keySet().size(), 
+            'empty attributes object in '+method+' result');
+
+        String type = (String)attributes.get('type');
+        System.assertNotEquals(null, type, 
+            'type property missing from '+method+' result');
+        System.assertEquals('Account', type, 
+            'Wrong type in '+method+' result');
+
+        String url = (String)attributes.get('url');
+        System.assertNotEquals(null, url, 
+            'url property missing from '+method+' result');
+
+        Id id = (Id)record.get('Id');
+        System.assertNotEquals(null, id, 
+            'Id property missing from '+method+' result');
+        Account account = [SELECT Id, Name FROM Account WHERE Id = :id LIMIT 1];
+        System.assertNotEquals(null, account, 
+            'Couldn\'t find account record identified by '+method+' result');
+        System.assertEquals(accName, account.Name, 
+            'Account name doesn\'t match in '+method+' result');
+
+        String name = (String)record.get('Name');
+        System.assertNotEquals(null, name, 
+            'Name property missing from '+method+' result');
+        System.assertEquals(accName, name, 
+            'Wrong account name in '+method+' result');
+
+        String accountNumber = (String)record.get('AccountNumber');
+        System.assertNotEquals(null, name, 
+            'AccountNumber property missing from '+method+' result');
+        System.assertEquals(accNumber, accountNumber, 
+            'Wrong account number in '+method+' result');
+    }
+
+    static private Id testCreate(String accName, String accNumber, String fields) {
+        // Assume we can create an account
+
+        // Try with data in correct types
+        String jsonResult = ngForceController.create('Account', 
+            '{"Name": "'+accName+'", '+
+            '"AccountNumber" : "'+accNumber+'",'+
+            fields+'}');
+
+        System.assertNotEquals(null, jsonResult, 
+            'ngForceController.create returned null');
+
+        Map<String, Object> result = (Map<String, Object>)JSON.deserializeUntyped(jsonResult);
+
+        Boolean success = (Boolean)result.get('success');
+        System.assertNotEquals(null, success, 
+            'success property missing from ngForceController.create result');
+        System.assertNotEquals(false, success, 
+            'success is false in ngForceController.create result');
+
+        List<Object> errors = (List<Object>)result.get('errors');
+        System.assertNotEquals(null, errors, 
+            'errors property missing from ngForceController.create result');
+        System.assertEquals(0, errors.size(), 
+            'errors array is not empty in ngForceController.create result');
+
+        Id id = (Id)result.get('id');
+        System.assertNotEquals(null, id, 
+            'id property missing from ngForceController.create result');
+        Account account = [SELECT Id, Name, AccountNumber FROM Account LIMIT 1];
+        System.assertNotEquals(null, account, 
+            'Couldn\'t find account record created by ngForceController.create result');
+        System.assertEquals(accName, account.Name, 
+            'Account name doesn\'t match in ngForceController.create result');
+        System.assertEquals(accNumber, account.AccountNumber, 
+            'Account number doesn\'t match in ngForceController.create result');
+
+        jsonResult = ngForceController.create('QXZXQZXZQXZQ', '{"Name": "'+accName+'"}');
+        assertError(jsonResult, 'NOT_FOUND', 'ngForceController.create');
+
+        jsonResult = ngForceController.create('Account', '{"Name" "'+accName+'"}');
+        assertError(jsonResult, 'JSON_PARSER_ERROR', 'ngForceController.create');
+
+        jsonResult = ngForceController.create('Account', '{"XQZXQZXQZXQZ" : "'+accName+'"}');
+        assertError(jsonResult, 'INVALID_FIELD', 'ngForceController.create');
+
+        jsonResult = ngForceController.create('Account', '{"Name" : "'+tooLongAccName+'"}');
+        assertError(jsonResult, 'STRING_TOO_LONG', 'ngForceController.create');
+
+        return id;
+    }
+
+    static private void testRetrieve(String accName, String accNumber, Id id) {
+        String jsonResult = ngForceController.retrieve('Account', id, 'Name, AccountNumber');
+
+        System.assertNotEquals(null, jsonResult, 
+            'ngForceController.retrieve returned null');
+
+        Map<String, Object> result = (Map<String, Object>)JSON.deserializeUntyped(jsonResult);
+
+        assertRecord(result, accName, accNumber, 'ngForceController.retrieve');        
+
+        // TODO - test negative paths for retrieve
+    }
+
+    static private void testQuery(String accName, String accNumber) {
+        String jsonResult = ngForceController.query('SELECT Id, Name, AccountNumber FROM Account WHERE Name = \''+accName+'\'');
+
+        System.assertNotEquals(null, jsonResult, 
+            'ngForceController.query returned null');
+
+        Map<String, Object> result = (Map<String, Object>)JSON.deserializeUntyped(jsonResult);
+
+        List<Object> records = (List<Object>)result.get('records');
+        System.assertNotEquals(null, records, 
+            'records property missing from ngForceController.query result');
+        System.assertEquals(1, records.size(), 
+            'records array should have single record in ngForceController.query result');
+
+        Map<String, Object> record = (Map<String, Object>)records[0];
+
+        assertRecord(record, accName, accNumber, 'ngForceController.query');        
+
+        Integer totalSize = (Integer)result.get('totalSize');
+        System.assertNotEquals(null, totalSize, 
+            'totalSize property missing from ngForceController.query result');
+        System.assertEquals(1, totalSize, 
+            'totalSize should be 1 in ngForceController.query result');
+
+        Boolean done = (Boolean)result.get('done');
+        System.assertNotEquals(null, done, 
+            'done property missing from ngForceController.query result');
+        System.assertEquals(true, done, 
+            'done should be true in ngForceController.query result');
+
+        jsonResult = ngForceController.query('SSSSSS Id, Name FROM Account WHERE Name = \''+accName+'\'');
+        assertError(jsonResult, 'INVALID_QUERY', 'ngForceController.query');
+    }
+
+    static private void testSearch(String accName, String accNumber, Id id) {
+        Id [] fixedSearchResults= new Id[1];
+        fixedSearchResults[0] = id;
+        Test.setFixedSearchResults(fixedSearchResults);
+        String jsonResult = ngForceController.search('FIND {'+accName+'} IN ALL FIELDS RETURNING Account (Id, Name, AccountNumber)');
+
+        System.assertNotEquals(null, jsonResult, 
+            'ngForceController.search returned null');
+
+        List<Object> result = (List<Object>)JSON.deserializeUntyped(jsonResult);
+
+        List<Object> records = (List<Object>)result[0];
+
+        Map<String, Object> record = (Map<String, Object>)records[0];
+
+        assertRecord(record, accName, accNumber, 'ngForceController.search'); 
+
+        jsonResult = ngForceController.search('FFFF {'+accName+'} IN ALL FIELDS RETURNING Account (Id, Name)');
+        assertError(jsonResult, 'INVALID_SEARCH', 'ngForceController.search');
+    }
+
+    static private void testUpdate(String accName, String accNumber, Id id, String fields) {
+        String jsonResult = ngForceController.updat('Account', id, '{"Name":"'+accName+'", "AccountNumber":"'+accNumber+'"}'); 
+        System.assertEquals(null, jsonResult, 
+            'Non-null result from ngForceController.updat');
+        Account account = [SELECT Id, Name, AccountNumber FROM Account WHERE Id = :id LIMIT 1];
+        System.assertNotEquals(null, account, 
+            'Couldn\'t find account record after ngForceController.updat');
+        System.assertEquals(accName, account.Name, 
+            'Account name doesn\'t match after ngForceController.updat');
+        System.assertEquals(accNumber, account.AccountNumber, 
+            'Account number doesn\'t match after ngForceController.updat');
+
+        jsonResult = ngForceController.updat('QXZXQZXZQXZQ', id, '{"Name":"'+accName+'"}');
+        assertError(jsonResult, 'NOT_FOUND', 'ngForceController.updat');
+
+        jsonResult = ngForceController.updat('Account', id, '{"XQZXQZXQZXQZ" : "'+accName+'"}');
+        assertError(jsonResult, 'INVALID_FIELD', 'ngForceController.updat');
+
+    }
+
+    static private void testUpsert(String accName, String accNumber, String id, String fields) {
+        String jsonResult = ngForceController.upser('Account', 
+            'Id', 
+            (String)id, 
+            '{"Name":"'+accName+'", '+
+            '"AccountNumber":"'+accNumber+'",'+
+            fields+'}');
+        System.assertEquals(null, jsonResult, 
+            'Non-null result from ngForceController.upser');
+        Account account = [SELECT Id, Name, AccountNumber FROM Account WHERE id = :id LIMIT 1];
+        System.assertNotEquals(null, account, 
+            'Couldn\'t find account record after ngForceController.upser');
+        System.assertEquals(accName, account.Name, 
+            'Account name doesn\'t match after ngForceController.upser');
+        System.assertEquals(accNumber, account.AccountNumber, 
+            'Account number doesn\'t match after ngForceController.upser');
+    } 
+
+    static private void testDelete(Id id) {
+        String jsonResult = ngForceController.del('QXZXQZXZQXZQ', id);
+        assertError(jsonResult, 'NOT_FOUND', 'ngForceController.del');
+
+        jsonResult = ngForceController.del('Account', id); 
+        System.assertEquals(null, jsonResult, 
+            'Non-null result from ngForceController.del');
+        List<Account> accounts = [SELECT Id, Name FROM Account WHERE Id = :id];
+        System.assertEquals(0, accounts.size(), 
+            'Account record was not deleted by ngForceController.del');
+
+        jsonResult = ngForceController.del('Account', id); 
+        assertError(jsonResult, 'ENTITY_IS_DELETED', 'ngForceController.del');
+    }
+
+    static testMethod void testGetObjType() {
+        Account a = TestUtils.getAccount();
+        String jsonResult = ngForceController.getObjType(a.id);
+        System.assertNotEquals(null, jsonResult, 
+            'ngForceController.getObjType returned null');
+        Map<String, Object> result = (Map<String, Object>)JSON.deserializeUntyped(jsonResult);
+        System.assertEquals('Account', result.get('type'));
+    }
+
+    static testMethod void testDescribeFieldSet() {
+        //warning this test is un-optimal and fragile
+        //I cannot create a fieldset via apex, so this test relies
+        //on a given object and a given fieldset existing!!!
+        
+        String jsonResult = ngForceController.describeFieldSet('Challenge__c', 'details');
+        System.assertNotEquals(null, jsonResult);
+    }
+
+    static testMethod void testSoqlFromFieldSet() {
+        //warning this test is un-optimal and fragile
+        //I cannot create a fieldset via apex, so this test relies
+        //on a given object and a given fieldset existing!!!
+        
+        String jsonResult = ngForceController.soqlFromFieldSet('Challenge__c', 'details');
+        System.assertNotEquals(null, jsonResult);
+        Map<String, Object> result = (Map<String, Object>)JSON.deserializeUntyped(jsonResult);
+        System.assertNotEquals(null, result.get('selectClause'));
+        System.assertNotEquals(null, result.get('fromClause'));
+    }
+
+    static testMethod void testQueryFromFieldSet() {
+        //warning this test is un-optimal and fragile
+        //I cannot create a fieldset via apex, so this test relies
+        //on a given object and a given fieldset existing!!!
+
+        Challenge__c li = TestUtils.getChallenge();
+
+        String jsonResult = ngForceController.queryFromFieldSet(li.Id, 'details');
+        System.assertNotEquals(null, jsonResult);
+        List<Object> result = (List<Object>)JSON.deserializeUntyped(jsonResult);
+        System.assertNotEquals(null, result);
+    }
+
+    static testMethod void testGetPicklistValues() {
+        String jsonResult = ngForceController.getPicklistValues('Opportunity', 'StageName');
+        System.assertNotEquals(null, jsonResult);
+        List<Object> result = (List<Object>)JSON.deserializeUntyped(jsonResult);
+        System.assertNotEquals(null, result);
+    }
+
+    // static testMethod void testQueryResultsAsSelect2Data() {
+    //     Component__c c = TestUtils.getComponent();
+    //     String soql = 'Select id, Name, SearchName__c from Component__c';
+    //     String jsonResult = ngForceController.getQueryResultsAsSelect2Data(soql);
+    //     System.assertNotEquals(null, jsonResult);
+    //     List<Object> result = (List<Object>)JSON.deserializeUntyped(jsonResult);
+    //     System.assertNotEquals(null, result);
+    // }
+
+    static testMethod void testCRUD() {
+        String accName = 'Test1';
+        String accNumber = '1234';
+
+        // String field values
+        Id id = testCreate(accName, accNumber, '"AnnualRevenue" : "10000.00",'+
+            '"NumberOfEmployees" : "1000",'+
+            '"Phone" : "(111) 222-3333"');
+        testDelete(id);
+
+            // Integer field values
+        id = testCreate(accName, accNumber, '"AnnualRevenue" : 1000000,'+
+             '"NumberOfEmployees" : 1000,'+
+             '"Phone" : "(111) 222-3333"');
+        testRetrieve(accName, accNumber, id);
+        testQuery(accName, accNumber);
+        testSearch(accName, accNumber, id);
+        testUpdate(accName+'1', accNumber+'1', id, '"AnnualRevenue" : "1100000",'+
+             '"NumberOfEmployees" : "1100",'+
+             '"Phone" : "(112) 222-3333"');
+        testUpdate(accName+'2', accNumber+'2', id, '"AnnualRevenue" : "2000000",'+
+             '"NumberOfEmployees" : "2000",'+
+             '"Phone" : "(222) 222-3333"');
+        testUpsert(accName+'3', accNumber+'3', id, '"AnnualRevenue" : 3000000,'+
+             '"NumberOfEmployees" : 3000,'+
+             '"Phone" : "(333) 222-3333"');
+        testUpsert(accName+'4', accNumber+'4', id, '"AnnualRevenue" : 4000000,'+
+             '"NumberOfEmployees" : 4000,'+
+             '"Phone" : "(444) 222-3333"');
+        testDelete(id);
+    }
+}

File src/classes/ngForceController_TEST.cls-meta.xml

View file
+<?xml version="1.0" encoding="UTF-8"?>
+<ApexClass xmlns="http://soap.sforce.com/2006/04/metadata">
+    <apiVersion>27.0</apiVersion>
+    <status>Active</status>
+</ApexClass>

File src/pages/ChallengeListTemplate.page

View file
 <apex:page docType="html-5.0" showHeader="false" standardstylesheets="false" sidebar="false">
 	<ul class="thumbnails">
-		<li class="span4" ng-repeat="challenge in challenges">
+		<li class="span4" ng-repeat="challenge in challenges | filter:searchText">
 		  <div class="thumbnail">
 		  	<div ng-bind-html-unsafe="challenge.Photo__c" style="text-align: center;"></div>
 		    <div class="caption">

File src/pages/Challenges.page

View file
 					<span class="icon-bar"></span>
 					<span class="icon-bar"></span>
 				</button>
-				<a class="brand" href="#">Challenges</a>
+				<a class="brand" href="/apex/challenges">Challenges</a>
 				<div class="nav-collapse collapse">
 					<ul class="nav">
-						<li><a href="#">Show all Challenges</a></li>
 						<li><a href="/a00/o">Return to Standard View</a></li>
 						<li><a href="http://www.twitter.com/codefriar">@codefriar</a></li>
 						<li>
 							<form class="navbar-search form-search">
-								<div class="input-append">
-									<input type="text" name="q" class="search-query" placeholder="Search Challenge Names"/>
-									<button type="submit" class="btn">Search</button>
-								</div>
+									<input type="text" name="q" ng-model="searchText" class="search-query" placeholder="Search Challenge Names"/>
 							</form>
 						</li>
 					</ul>
 			<challenge-list/>
 		</div>
 	</div>
-    <style>
-/*      @media (min-width: 650px) {
-	      body {
-	        padding-top: 60px;
-	        padding-bottom: 40px;
-	      }
-      }*/
-    </style>
+	<style>
+.row-fluid ul.thumbnails li.span12 + li { margin-left : 0px;clear:left }
+.row-fluid ul.thumbnails li.span6:nth-child(2n + 3) { margin-left : 0px;clear:left }
+.row-fluid ul.thumbnails li.span4:nth-child(3n + 4) { margin-left : 0px;clear:left }
+.row-fluid ul.thumbnails li.span3:nth-child(4n + 5) { margin-left : 0px; clear:left}
+.row-fluid ul.thumbnails li.span2:nth-child(6n + 7) { margin-left : 0px;clear:left }
+.row-fluid ul.thumbnails li.span1:nth-child(12n + 13) { margin-left : 0px;clear:left }
+		.first-in-row { margin-left: 0 !important; }​
+	</style>
+	<script type="text/javascript">
+
+		jQuery(document).ready(function($) {
+			console.log('foooooo');
+			$('.row-fluid .thumbnails').each(function () {
+				var $thumbnails = $(this).children(),
+					previousOffsetLeft = $thumbnails.first().offset().left;
+				$thumbnails.removeClass('first-in-row');
+				$thumbnails.first().addClass('first-in-row');
+				$thumbnails.each(function () {
+					var $thumbnail = $(this), offsetLeft = $thumbnail.offset().left;
+					if (offsetLeft < previousOffsetLeft) {
+						$thumbnail.addClass('first-in-row');
+					}
+					previousOffsetLeft = offsetLeft;
+				});
+			});
+		});
+
+	</script>
 	</body>
 </apex:page>

File src/staticresources/ngForce.resource

Binary file modified.