Bower
Local
Server
Local
Server
Straight forward
enough
Until you start
working with others
You
Server
Teammate
Server
The Ritual
Update from
server
Edit
Upload
Double check if file has been updated
Graphic from Git website - https://git-scm.com
Commits from other devs
Commits you made
Merges changes to repository for you
branch
misc
tags
trunk
dist
src
assets
app
api
index.html
git-scm.com
"gitflow" branching mode image by Vincent Driessen
http://nvie.com/posts/a-successful-git-branching-model/
Complicated branching can seem convoluted and intimidating.
Each clone is a branch. Each clone can have branches in it.
Central origin, Distributed clones
Ugh, even more stuff I have to learn?
github.com
Separate from your repos, but integral to development
Jayne
You
two weeks later
Jayne
didn't check they version
Bower
bower.io
nodejs.org
# registered package
$ bower install angular
# GitHub shorthand <github-user>/<repo-name>
$ bower install jquery/jquery
# Github URL
$ bower install https://github.com/FortAwesome/Font-Awesome.git
# Git endpoint
$ bower install git://YOUR-GIT-SERVER.com/package.git
# URL
$ bower install http://example.com/script.js
$ bower install
{
"name": "roots-ualib",
"version": "2.1.2",
"homepage": "https://github.com/ualibweb/roots-ualib",
"license": "ECL-2.0",
"private": true,
"ignore": [
"**/.*",
"node_modules",
"assets/vendor"
],
"dependencies": {
"modernizr": "2.8.2",
"jquery": "1.11.1",
"bootstrap": "3.3.2",
"respond": "1.4.2",
"angular-filter": "~0.5.4",
"ng-file-upload": ">=4",
"yamm3": "https://github.com/geedmo/yamm3.git#~1.0.0",
"ualib-ui": "https://github.com/ualibweb/ui-components.git",
"onesearch": "https://github.com/ualibweb/oneSearch_ui.git",
"ualib-hours": "https://github.com/ualibweb/hours_ui.git",
"manage": "https://github.com/ualibweb/manage_ui.git",
"databases": "https://github.com/ualibweb/databases_ui.git",
"musicSearch": "https://github.com/ualibweb/musicSearch_ui.git",
"ualib_staffdir": "https://github.com/ualibweb/staffdir_ui.git",
"ualib-softwareList": "https://github.com/ualibweb/softwareList_ui.git",
"ualib-news": "https://github.com/ualibweb/news_ui.git",
"angular-carousel": "0.3.12",
"angular-ui-tinymce": "latest"
},
"devDependencies": {
"fontawesome": "~4.3.0",
"angular": ">=1 <1.3.0",
"angular-route": ">=1 <1.3.0",
"angular-resource": ">=1 <1.3.0",
"angular-animate": ">=1 <1.3.0",
"angular-sanitize": ">=1 <1.3.0"
},
"resolutions": {
"angular": ">=1 <1.3.0",
"angular-route": ">=1 <1.3.0",
"angular-resource": ">=1 <1.3.0",
"angular-animate": ">=1 <1.3.0",
"angular-sanitize": ">=1 <1.3.0"
}
}
bower.json
gruntjs.com
nodejs.org
/**
* Bento Service Provider
*
* This service uses the mediaTypes service to organize the engine results by media type
* and preloaded an engine's template and controller (if defined) if there are results for that engine.
*/
.service('Bento', ['$routeParams', '$rootScope','$q', 'oneSearch', 'mediaTypes', function($routeParams, $rootScope, $q, oneSearch, mediaTypes){
//variable representing 'this' for closure context
//this ensures function closure reference variables in the right context
var self = this;
/**
* Object to hold box data
* @Object = {
* // Box names are generated by media types defined by registered engines
* NAME: {
* // An Array of engine names (String) used to get engine templates/controllers and track when the box is done loading.
* // Once an engine is finished loading, the engine's name is removed from this array. The removed value is used to reference the loaded engine's preloaded
* // template/controller. Once this Array is empty the "box" is considered loaded.
* engines: Array,
*
* // The object's keys are the engine names and the values are the results returned using
* // the JSON path from an engine's resultsPath param
* results: {
* ENGINE_NAME1: {},
* ENGINE_NAME2: {},
* etc...
* }
* }
* }
*/
this.boxes = {};
this.boxMenu = [];
/**
* Object to hold pre-loaded engine templates and controllers.
* Templates and controllers are only pre-loaded if the engine yields results.
* @Object = {
* // Engine name, defined by engine's config registering with the oneSearchProvider
* NAME: {
* // the "tpl" key returns a Promise to retrieve the engine's template
* // The Promise is generated from Angular's $http Service (https://code.angularjs.org/1.3.0/docs/api/ng/service/$http),
* // which uses the promise methods from Angular's $q Service (https://code.angularjs.org/1.3.0/docs/api/ng/service/$q)
* tpl: Promise,
*
* // The "controller" key will return an instance of the engine's controller or "null" if no controller was defined
* controller: Controller Instance|null
* }
* }
*/
this.engines = {};
// Helper function that removes an engine's name from a box's "engines" Array
// Once the "engines" Array is empty, the box is considered "loaded"
function loadProgress(type, engine){
var i = self.boxes[type].engines.indexOf(engine);
if(i != -1) {
setResultLimit(type);
self.boxes[type].engines.splice(i, 1);
}
}
// Remove an engine from all boxes
function removeFromBoxes(engine){
angular.forEach(self.boxes, function(box, type){
loadProgress(type, engine);
})
}
function initResultLimit(box){
var numEngines = self.boxes[box]['engines'].length;
var limit = numEngines > 1 ? 1 : (numEngines < 2 ? 3 : 2);
self.boxes[box].resultLimit = limit;
}
function setResultLimit(box){
$q.when(self.boxes[box].results)
.then(function(results){
var numResults = Object.keys(results).length;
var numEngines = self.boxes[box]['engines'].length;
var expecting = numResults + numEngines;
if ((expecting < 2 && self.boxes[box].resultLimit < 3) || (expecting < 3 && self.boxes[box].resultLimit < 2)){
self.boxes[box].resultLimit++;
}
});
}
var engines;
// Gets all boxes
this.getBoxes = function(){
// Search all engines registered with the oneSearch Provider, giving the
// $routeParams object as the parameter (https://code.angularjs.org/1.3.0/docs/api/ngRoute/service/$routeParams)
engines = oneSearch.searchAll($routeParams);
// Deep copy media types defined by registered engines to the this.boxes object.
angular.copy(mediaTypes.types, self.boxes);
// Pre-define the "results" object for each media type - I only do this here so I don't have to check if it's defined later
angular.forEach(self.boxes, function(box, type){
initResultLimit(type);
self.boxes[type].results = {};
self.boxes[type].resourceLinks = {};
self.boxes[type].resourceLinkParams = {};
});
// Iterate over the Promises for each engine returned by the oneSearch.searchAll() function
angular.forEach(engines, function(engine, name){
engine.response
.then(function(data){ // If $http call was a success
// User the engine's results getter to get the results object
// The results getter is defined by the JSON path defined by the
// "resultsPath" param in an engine's config
var res = engine.getResults(data);
var link = engine.getResourceLink(data);
// Double check that the data is defined, in case the search API returned a '200' status with empty results.
if (isEmpty(res)){
//console.log(self.boxes);
removeFromBoxes(name);
//console.log(self.boxes);
}
else {
res = res.map(function(item, i){
var newItem = item;
newItem.position = i;
return newItem;
});
//console.log(res);
// Group the results by defined media types
var grouped = mediaTypes.groupBy(res, engine.mediaTypes);
// Iterate over the boxes.
Object.keys(self.boxes).forEach(function(type){
// If a box type matches a group in the grouped results
if (grouped.hasOwnProperty(type)){
// Put results in the boxes "results" object, referenced by the engine's name
// Ex: self.boxes['books'].results['catalog'] = group_result;
//
// Also, limit the number of results per group by 3
// and sort by generation position in the original results list
self.boxes[type].results[name] = grouped[type].sort(function(a, b){
if (a.position > b.position){
return 1;
}
if (a.position < b.position){
return -1;
}
return 0;
});
// set resource "more" link
self.boxes[type].resourceLinks[name] = decodeURIComponent(link[engine.id]);
// set resource link parameters by media type specified by the engine config
if (angular.isObject(engine.mediaTypes)){
self.boxes[type].resourceLinkParams[name] = engine.mediaTypes.types[type];
}
}
// update loading progress, setting engine as loaded for current box
loadProgress(type, name);
});
//preload the engine's template for easy access for directives
self.engines[name] = {};
self.engines[name].tpl = oneSearch.getEngineTemplate(engine);
self.engines[name].controller = oneSearch.getEngineController(engine);
}
}, function(msg){
// If error code return from $http, iterate through boxes object
// and remove any instance engine from a box's "engines" array
removeFromBoxes(name);
});
});
}
}])
.service("Bento", [ "$routeParams", "$rootScope", "$q", "oneSearch", "mediaTypes", function(a, b, c, d, e) {
function f(a, b) {
var c = j.boxes[a].engines.indexOf(b);
-1 != c && (i(a), j.boxes[a].engines.splice(c, 1));
}
function g(a) {
angular.forEach(j.boxes, function(b, c) {
f(c, a);
});
}
function h(a) {
var b = j.boxes[a].engines.length, c = b > 1 ? 1 : 2 > b ? 3 : 2;
j.boxes[a].resultLimit = c;
}
function i(a) {
c.when(j.boxes[a].results).then(function(b) {
var c = Object.keys(b).length, d = j.boxes[a].engines.length, e = c + d;
(2 > e && j.boxes[a].resultLimit < 3 || 3 > e && j.boxes[a].resultLimit < 2) && j.boxes[a].resultLimit++;
});
}
var j = this;
this.boxes = {}, this.boxMenu = [], this.engines = {};
var k;
this.getBoxes = function() {
k = d.searchAll(a), angular.copy(e.types, j.boxes), angular.forEach(j.boxes, function(a, b) {
h(b), j.boxes[b].results = {}, j.boxes[b].resourceLinks = {}, j.boxes[b].resourceLinkParams = {};
}), angular.forEach(k, function(a, b) {
a.response.then(function(c) {
var h = a.getResults(c), i = a.getResourceLink(c);
if (isEmpty(h)) g(b); else {
h = h.map(function(a, b) {
var c = a;
return c.position = b, c;
});
var k = e.groupBy(h, a.mediaTypes);
Object.keys(j.boxes).forEach(function(c) {
k.hasOwnProperty(c) && (j.boxes[c].results[b] = k[c].sort(function(a, b) {
return a.position > b.position ? 1 : a.position < b.position ? -1 : 0;
}), j.boxes[c].resourceLinks[b] = decodeURIComponent(i[a.id]), angular.isObject(a.mediaTypes) && (j.boxes[c].resourceLinkParams[b] = a.mediaTypes.types[c])),
f(c, b);
}), j.engines[b] = {}, j.engines[b].tpl = d.getEngineTemplate(a), j.engines[b].controller = d.getEngineController(a);
}
}, function() {
g(b);
});
});
};
} ])
.service("Bento", [ "$routeParams", "$rootScope", "$q", "oneSearch", "mediaTypes", function(a, b, c, d, e) {
function f(a, b) {
var c = j.boxes[a].engines.indexOf(b);
-1 != c && (i(a), j.boxes[a].engines.splice(c, 1));
}
function g(a) {
angular.forEach(j.boxes, function(b, c) {
f(c, a);
});
}
function h(a) {
var b = j.boxes[a].engines.length, c = b > 1 ? 1 : 2 > b ? 3 : 2;
j.boxes[a].resultLimit = c;
}
function i(a) {
c.when(j.boxes[a].results).then(function(b) {
var c = Object.keys(b).length, d = j.boxes[a].engines.length, e = c + d;
(2 > e && j.boxes[a].resultLimit < 3 || 3 > e && j.boxes[a].resultLimit < 2) && j.boxes[a].resultLimit++;
});
}
var j = this;
this.boxes = {}, this.boxMenu = [], this.engines = {};
var k;
this.getBoxes = function() {
k = d.searchAll(a), angular.copy(e.types, j.boxes), angular.forEach(j.boxes, function(a, b) {
h(b), j.boxes[b].results = {}, j.boxes[b].resourceLinks = {}, j.boxes[b].resourceLinkParams = {};
}), angular.forEach(k, function(a, b) {
a.response.then(function(c) {
var h = a.getResults(c), i = a.getResourceLink(c);
if (isEmpty(h)) g(b); else {
h = h.map(function(a, b) {
var c = a;
return c.position = b, c;
});
var k = e.groupBy(h, a.mediaTypes);
Object.keys(j.boxes).forEach(function(c) {
k.hasOwnProperty(c) && (j.boxes[c].results[b] = k[c].sort(function(a, b) {
return a.position > b.position ? 1 : a.position < b.position ? -1 : 0;
}), j.boxes[c].resourceLinks[b] = decodeURIComponent(i[a.id]), angular.isObject(a.mediaTypes) && (j.boxes[c].resourceLinkParams[b] = a.mediaTypes.types[c])),
f(c, b);
}), j.engines[b] = {}, j.engines[b].tpl = d.getEngineTemplate(a), j.engines[b].controller = d.getEngineController(a);
}
}, function() {
g(b);
});
});
};
} ])
.service("Bento",["$routeParams","$rootScope","$q","oneSearch","mediaTypes",
function(a,b,c,d,e){function f(a,b){var c=j.boxes[a].engines.indexOf(b);-1!=
c&&(i(a),j.boxes[a].engines.splice(c,1))}function g(a){angular.forEach(j.boxes,
function(b,c){f(c,a)})}function h(a){var b=j.boxes[a].engines.length,
c=b>1?1:2>b?3:2;j.boxes[a].resultLimit=c}function i(a){c.when(j.boxes[a].results)
.then(function(b){var c=Object.keys(b).length,d=j.boxes[a].engines.length,e=c+d;(2>e&&j.boxes[a].resultLimit<3||3>e&&j.boxes[a].resultLimit<2)&&j.boxes[a]
.resultLimit++})}var j=this;this.boxes={},this.boxMenu=[],this.engines={};
var k;this.getBoxes=function(){k=d.searchAll(a),angular.copy(e.types,j.boxes)
,angular.forEach(j.boxes,function(a,b){h(b),j.boxes[b].results={},j.boxes[b]
.resourceLinks={},j.boxes[b].resourceLinkParams={}}),angular.forEach(k,
function(a,b){a.response.then(function(c){var h=a.getResults(c),i=a.getResourceLink(c);if(isEmpty(h))g(b);
else{h=h.map(function(a,b){var c=a;return c.position=b,c});var k=e.groupBy(h,a.mediaTypes);
Object.keys(j.boxes).forEach(function(c){k.hasOwnProperty(c)&&(j.boxes[c].results[b]=k[c]
.sort(function(a,b){return a.position>b.position?1:a.position<b.position?-1:0}),j.boxes[c].resourceLinks[b]=decodeURIComponent(i[a.id]),angular.isObject
(a.mediaTypes)&&(j.boxes[c].resourceLinkParams[b]=a.mediaTypes.types[c])),
f(c,b)}),j.engines[b]={},j.engines[b].tpl=d.getEngineTemplate(a),j.engines[b]
.controller=d.getEngineController(a)}},function(){g(b)})})}}])
actually one line - no whitespace
concat: {
app: {
src: ['tmp/templates.js', 'src/app/**/*.js'],
dest: 'dist/onesearch.js'
},
index: {
src: 'src/index.html',
dest: 'dist/index.html',
options: {
process: true
}
}
},
uglify: {
options: {
mangle: true
},
app: {
files: {
'dist/onesearch.min.js': ['dist/onesearch.js']
}
}
}
/* Dev build task*/
grunt.registerTask('dev', ['concat']);
/* Live build task*/
grunt.registerTask('live', ['dev', 'uglify']);
# Run dev build task
$ grunt dev
# Run live build task
$ grunt live
Live/Dev repos
Git hook deployment
University of Alabama Libraries
Will Jones
Senior Web Developer,
University of Alabama Libraries,
Web Infrastructure and Development
Tools I like