Simple round trip API integration with storyboard-api

This commit makes use of previous API inclusions and resource frameworks to
demonstrate a simple list round-trip from server to client. To properly see
it in action, please retrieve CR 68540 and run that alongside grunt server.
It will allow you to create, edit, and list project groups with a simple UI.

Change-Id: Ie95685a6fd3cd3ab2b674bef3685b2896eb72f0d
This commit is contained in:
Michael Krotscheck 2014-01-22 17:01:57 -08:00
parent 2153ac56e5
commit 47b3f87fce
16 changed files with 804 additions and 15 deletions

View File

@ -0,0 +1,145 @@
/*
* Copyright (c) 2014 Hewlett-Packard Development Company, L.P.
*
* 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
*
* http://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.
*/
/**
* Project group detail & manipulation controller. Usable for any view that
* wants to view, edit, or delete a project group, though views don't have to
* use all the functions therein. Includes flags for busy time, error responses
* and more.
*
* This controller assumes that the $stateParams object is both injectable and
* contains an ":id" property that indicates which project should be loaded. At
* the moment it will only set a 'isLoading' flag to indicate that data is
* loading. If loading the data is anticipated to take longer than 3 seconds,
* this will need to be updated to display a sane progress.
*
* Do not allow loading of this (or any) controller to take longer than 10
* seconds. 3 is preferable.
*/
angular.module('sb.project_groups').controller('ProjectGroupDetailController',
function ($scope, $state, $stateParams, ProjectGroup) {
'use strict';
// Parse the ID
var id = $stateParams.hasOwnProperty('id') ?
parseInt($stateParams.id, 10) :
null;
/**
* The project group we're manipulating right now.
*
* @type ProjectGroup
*/
$scope.projectGroup = {};
/**
* UI flag for when we're initially loading the view.
*
* @type {boolean}
*/
$scope.isLoading = true;
/**
* UI view for when a change is round-tripping to the server.
*
* @type {boolean}
*/
$scope.isUpdating = false;
/**
* Any error objects returned from the services.
*
* @type {{}}
*/
$scope.error = {};
/**
* Generic service error handler. Assigns errors to the view's scope,
* and unsets our flags.
*/
function handleServiceError(error) {
// We've encountered an error.
$scope.error = error;
$scope.isLoading = false;
$scope.isUpdating = false;
}
// Sanity check, do we actually have an ID? (zero is falsy)
if (!id && id !== 0) {
// We should never reach this, however that logic lives outside
// of this controller which could be unknowningly refactored.
$scope.error = {
error: true,
error_code: 404,
error_message: 'You did not provide a valid ID.'
};
$scope.isLoading = false;
} else {
// We've got an ID, so let's load it...
ProjectGroup.read(
{'id': id},
function (result) {
// We've got a result, assign it to the view and unset our
// loading flag.
$scope.projectGroup = result;
$scope.isLoading = false;
},
handleServiceError
);
}
/**
* Scope method, invoke this when you want to update the project.
*/
$scope.update = function () {
// Set our progress flags and clear previous error conditions.
$scope.isUpdating = true;
$scope.error = {};
// Invoke the save method and wait for results.
$scope.projectGroup.$update(
function () {
// Unset our loading flag and navigate to the detail view.
$scope.isUpdating = false;
$state.go('project_groups.detail', {
id: $scope.projectGroup.id
});
},
handleServiceError
);
};
/**
* Scope method, invoke this when you'd like to delete this project.
*/
$scope.remove = function () {
// Set our progress flags and clear previous error conditions.
$scope.isUpdating = true;
$scope.error = {};
// Try to delete.
$scope.projectGroup.$delete(
function () {
// The deletion was successful, so head back to the list
// view.
$scope.isUpdating = false;
$state.go('project_groups.list');
},
handleServiceError
);
};
});

View File

@ -0,0 +1,62 @@
/*
* Copyright (c) 2014 Hewlett-Packard Development Company, L.P.
*
* 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
*
* http://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.
*/
/**
* The projectGroup list controller handles discovery for all projectGroups,
* including search. Note that it is assumed that we implemented a search
* (inclusive), rather than a browse (exclusive) approach.
*/
angular.module('sb.project_groups').controller('ProjectGroupListController',
function ($scope, ProjectGroup) {
'use strict';
// Variables and methods available to the template...
$scope.projectGroups = [];
$scope.searchQuery = '';
$scope.isSearching = false;
/**
* The search method.
*/
$scope.search = function () {
// Clear the scope and set the progress flag.
$scope.error = {};
$scope.isSearching = true;
$scope.projectGroups = [];
// Execute the projectGroup query.
ProjectGroup.search(
// Enable this once the API's there, mocks don't support
// searches yet
{/* q: $scope.searchQuery || '' */},
function (result) {
// Successful search results, apply the results to the
// scope and unset our progress flag.
$scope.projectGroups = result;
$scope.isSearching = false;
},
function (error) {
// Error search results, show the error in the UI and
// unset our progress flag.
$scope.error = error;
$scope.isSearching = false;
}
);
};
// Initialize the view with a default search.
$scope.search();
});

View File

@ -0,0 +1,54 @@
/*
* Copyright (c) 2014 Hewlett-Packard Development Company, L.P.
*
* 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
*
* http://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.
*/
/**
* View controller for the new project group form. Includes an intermediary
* 'saving' flag as well as room for an error response (though until we get
* a real API that'll be a bit tricky to test).
*/
angular.module('sb.project_groups').controller('ProjectGroupNewController',
function ($scope, $state, ProjectGroup) {
'use strict';
// View parameters.
$scope.newProjectGroup = new ProjectGroup();
$scope.isCreating = false;
$scope.error = {};
/**
* Submits the newly created project. If an error response is received,
* assigns it to the view and unsets various flags. The template
* should know how to handle it.
*/
$scope.createProjectGroup = function () {
// Clear everything and set the progress flag...
$scope.isCreating = true;
$scope.error = {};
$scope.newProjectGroup.$create(
function () {
// Success!
$state.go('project_groups.list');
},
function (error) {
// Error received. Ho hum.
$scope.isCreating = false;
$scope.error = error;
}
);
};
});

View File

@ -0,0 +1,55 @@
/*
* Copyright (c) 2014 Hewlett-Packard Development Company, L.P.
*
* 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
*
* http://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.
*/
/**
* The Storyboard project_group submodule handles most activity surrounding the
* creation and management of project_groups.
*/
angular.module('sb.project_groups', ['ui.router', 'sb.services', 'sb.util'])
.config(function ($stateProvider, $urlRouterProvider) {
'use strict';
// URL Defaults.
$urlRouterProvider.when('/project_groups', '/project_groups/list');
// Set our page routes.
$stateProvider
.state('project_groups', {
abstract: true,
url: '/project_groups',
template: '<div ui-view></div>'
})
.state('project_groups.list', {
url: '/list',
templateUrl: 'app/templates/project_groups/list.html',
controller: 'ProjectGroupListController'
})
.state('project_groups.edit', {
url: '/{id:[0-9]+}/edit',
templateUrl: 'app/templates/project_groups/edit.html',
controller: 'ProjectGroupDetailController'
})
.state('project_groups.detail', {
url: '/{id:[0-9]+}',
templateUrl: 'app/templates/project_groups/detail.html',
controller: 'ProjectGroupDetailController'
})
.state('project_groups.new', {
url: '/new',
templateUrl: 'app/templates/project_groups/new.html',
controller: 'ProjectGroupNewController'
});
});

View File

@ -49,5 +49,5 @@ angular.module('sb.services')
// Neither of those work, so default to something sane on the current
// domain
$provide.constant(propertyName, '/api/v1');
$provide.constant(propertyName, '/v1');
});

View File

@ -40,14 +40,7 @@ angular.module('sb.services')
},
'search': {
method: 'GET',
isArray: true,
transformResponse: function (data) {
if (data.error) {
return data;
} else {
return data.results;
}
}
isArray: true
}
};
}

View File

@ -24,7 +24,7 @@
*/
angular.module('storyboard',
[ 'sb.services', 'sb.templates', 'sb.pages', 'sb.projects', 'sb.auth',
'ui.router']
'sb.teams', 'sb.project_groups', 'ui.router']
)
.config(function ($provide, $stateProvider, $urlRouterProvider,
$locationProvider, $httpProvider) {

View File

@ -0,0 +1,62 @@
/*
* Copyright (c) 2014 Hewlett-Packard Development Company, L.P.
*
* 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
*
* http://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.
*/
/**
* The team list controller handles discovery for all teams, including
* search. Note that it is assumed that we implemented a search (inclusive),
* rather than a browse (exclusive) approach.
*/
angular.module('sb.teams').controller('TeamsListController',
function ($scope, Team) {
'use strict';
// Variables and methods available to the template...
$scope.teams = [];
$scope.searchQuery = '';
$scope.isSearching = false;
/**
* The search method.
*/
$scope.search = function () {
// Clear the scope and set the progress flag.
$scope.error = {};
$scope.isSearching = true;
$scope.teams = [];
// Execute the team search.
Team.search(
// Enable this once the API's there, mocks don't support
// searches yet
{/* q: $scope.searchQuery || '' */},
function (result) {
// Successful search results, apply the results to the
// scope and unset our progress flag.
$scope.teams = result;
$scope.isSearching = false;
},
function (error) {
// Error search results, show the error in the UI and
// unset our progress flag.
$scope.error = error;
$scope.isSearching = false;
}
);
};
// Initialize the view with a default search.
$scope.search();
});

40
src/app/teams/module.js Normal file
View File

@ -0,0 +1,40 @@
/*
* Copyright (c) 2014 Hewlett-Packard Development Company, L.P.
*
* 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
*
* http://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.
*/
/**
* The Storyboard team submodule handles most activity surrounding the
* creation and management of project teams.
*/
angular.module('sb.teams', ['ui.router', 'sb.services', 'sb.util'])
.config(function ($stateProvider, $urlRouterProvider) {
'use strict';
// URL Defaults.
$urlRouterProvider.when('/teams', '/teams/list');
// Set our page routes.
$stateProvider
.state('teams', {
abstract: true,
url: '/teams',
template: '<div ui-view></div>'
})
.state('teams.list', {
url: '/list',
templateUrl: 'app/templates/teams/list.html',
controller: 'TeamsListController'
});
});

View File

@ -28,11 +28,17 @@
</div>
<nav class="collapse navbar-collapse sb-navbar-collapse" role="navigation">
<ul class="nav navbar-nav">
<li>
<a href="#!/project_groups">Project Groups</a>
</li>
<li>
<a href="#!/project">Projects</a>
</li>
<li>
<a href="#!/stories">Stories</a>
<a href="#!/teams">Teams</a>
</li>
<li>
<a href="#!/stories">Stories</a>
</li>
<li>
<a href="#!/page/about">About</a>

View File

@ -0,0 +1,39 @@
<!--
~ Copyright (c) 2014 Hewlett-Packard Development Company, L.P.
~
~ 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
~
~ http://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.
-->
<div class="container" ng-show="isLoading">
<div class="col-xs-12">
<p class="text-center">
<i class="fa fa-refresh fa-spin"></i>
</p>
</div>
</div>
<div class="container" ng-hide="isLoading">
<div class="row">
<div class="col-xs-12">
<h1>{{projectGroup.name}}</h1>
<p>{{projectGroup.title}}</p>
<hr/>
</div>
</div>
<div class="row">
<div class="col-xs-12">
Project List TBD.
</div>
</div>
</div>

View File

@ -0,0 +1,85 @@
<!--
~ Copyright (c) 2014 Hewlett-Packard Development Company, L.P.
~
~ 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
~
~ http://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.
-->
<div class="container" ng-show="isLoading">
<div class="col-xs-12">
<p class="text-center">
<i class="fa fa-refresh fa-2x fa-spin"></i>
</p>
</div>
</div>
<div class="container" ng-hide="isLoading">
<div class="row">
<div class="col-xs-12">
<h1>Project Group: {{projectGroup.title}}</h1>
<hr/>
</div>
</div>
<div class="row">
<div class="col-xs-12">
<form class="form-horizontal" role="form" name="projectForm">
<div class="form-group">
<label for="name" class="col-sm-2 control-label">
Group Name:
</label>
<div class="col-sm-10">
<input id="name"
type="text"
class="form-control"
ng-model="projectGroup.name"
required
placeholder="Group Name">
</div>
</div>
<div class="form-group">
<label for="title"
class="col-sm-2 control-label">
Group Title
</label>
<div class="col-sm-10">
<input id="title"
type="text"
class="form-control"
ng-model="projectGroup.title"
required
placeholder="Group Title">
</div>
</div>
<div class="form-group">
<div class="col-sm-offset-2 col-sm-10">
<button type="button"
ng-click="update()"
class="btn btn-primary"
ng-disabled="!projectForm.$valid">
Save Changes
</button>
<button ng-click="remove()"
class="btn btn-danger">
Delete Group
</button>
<a href="#!/project_groups/list"
class="btn btn-default">
Cancel
</a>
</div>
</div>
</form>
</div>
</div>
</div>

View File

@ -0,0 +1,87 @@
<!--
~ Copyright (c) 2013 Hewlett-Packard Development Company, L.P.
~
~ 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
~
~ http://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.
-->
<div class="container">
<div class="row">
<div class="col-sm-8 col-md-9">
<h3 class="no-margin">
<a href="#!/project_groups/new" class="btn btn-default">
<i class="fa fa-plus"></i>
</a>
Project Groups
</h3>
<br class="visible-xs"/>
</div>
<div class="col-sm-4 col-md-3">
<div class="input-group">
<input type="text" class="form-control"
placeholder="Search Project Groups"
ng-disabled="isSearching"
ng-enter="search()"
ng-model="searchQuery"/>
<span class="input-group-btn">
<button type="button" ng-click="search()"
ng-disabled="isSearching"
class="btn btn-default">
<i class="fa fa-refresh fa-spin"
ng-show="isSearching"></i>
<i class="fa fa-search"
ng-hide="isSearching"></i>
</button>
</span>
</div>
</div>
</div>
<div class="row">
<div class="col-sm-12">
<hr/>
</div>
</div>
<div class="row">
<div class="col-sm-12">
<div ng-show="isSearching">
<hr/>
<p class="text-center">
<i class="fa fa-refresh fa-spin fa-lg"></i>
</p>
</div>
<table class="table table-striped table-hover table-responsive"
ng-hide="isSearching">
<tbody>
<tr ng-repeat="projectGroup in projectGroups">
<td>
<div class="pull-right">
<a href="#!/project_groups/{{projectGroup.id}}/edit">
<i class="fa fa-edit"></i>
</a>
</div>
<a href="#!/project_groups/{{projectGroup.id}}">
<strong>{{projectGroup.name}}</strong>
</a>
<br/>
{{projectGroup.title}}
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>

View File

@ -0,0 +1,74 @@
<!--
~ Copyright (c) 2014 Hewlett-Packard Development Company, L.P.
~
~ 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
~
~ http://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.
-->
<div class="container">
<div class="row">
<div class="col-xs-12">
<h1>Create a new project group</h1>
<hr/>
</div>
</div>
<div class="row">
<div class="col-xs-12">
<form class="form-horizontal" role="form" name="projectGroupForm">
<div class="form-group">
<label for="name" class="col-sm-2 control-label">
Group Name:
</label>
<div class="col-sm-10">
<input id="name"
type="text"
class="form-control"
ng-model="newProjectGroup.name"
required
placeholder="Group Name">
</div>
</div>
<div class="form-group">
<label for="title"
class="col-sm-2 control-label">
Group Title:
</label>
<div class="col-sm-10">
<input id="title"
type="text"
class="form-control"
ng-model="newProjectGroup.title"
required
placeholder="Group Title">
</textarea>
</div>
</div>
<div class="form-group">
<div class="col-sm-offset-2 col-sm-10">
<button type="button"
ng-click="createProjectGroup()"
class="btn btn-primary"
ng-disabled="!projectGroupForm.$valid">
Create project group
</button>
<a href="#!/project_groups/list"
class="btn btn-default">
Cancel
</a>
</div>
</div>
</form>
</div>
</div>
</div>

View File

@ -0,0 +1,87 @@
<!--
~ Copyright (c) 2014 Hewlett-Packard Development Company, L.P.
~
~ 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
~
~ http://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.
-->
<div class="container">
<div class="row">
<div class="col-sm-8 col-md-9">
<h3 class="no-margin">
<a href="#!/team/new" class="btn btn-default">
<i class="fa fa-plus"></i>
</a>
Teams
</h3>
<br class="visible-xs"/>
</div>
<div class="col-sm-4 col-md-3">
<div class="input-group">
<input type="text" class="form-control"
placeholder="Search Teams"
ng-disabled="isSearching"
ng-enter="search()"
ng-model="searchQuery"/>
<span class="input-group-btn">
<button type="button" ng-click="search()"
ng-disabled="isSearching"
class="btn btn-default">
<i class="fa fa-refresh fa-spin"
ng-show="isSearching"></i>
<i class="fa fa-search"
ng-hide="isSearching"></i>
</button>
</span>
</div>
</div>
</div>
<div class="row">
<div class="col-sm-12">
<hr/>
</div>
</div>
<div class="row">
<div class="col-sm-12">
<div ng-show="isSearching">
<hr/>
<p class="text-center">
<i class="fa fa-refresh fa-spin fa-lg"></i>
</p>
</div>
<table class="table table-striped table-hover table-responsive"
ng-hide="isSearching">
<tbody>
<tr ng-repeat="team in teams">
<td>
<div class="pull-right">
<a href="#!/team/{{team.id}}/edit">
<i class="fa fa-edit"></i>
</a>
</div>
<a href="#!/team/{{team.id}}">
<strong>{{team.name}}</strong>
</a>
<br/>
{{team.description}}
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>

View File

@ -21,23 +21,23 @@
describe('storyboardApiBase', function () {
'use strict';
it('should default to /api/v1', function () {
it('should default to /v1', function () {
module('sb.services');
inject(function (storyboardApiBase) {
expect(storyboardApiBase).toEqual('/api/v1');
expect(storyboardApiBase).toEqual('/v1');
});
});
it('should detect a value in window.ENV', function () {
window.ENV = {
storyboardApiBase: 'https://localhost:8080/api/v1'
storyboardApiBase: 'https://localhost:8080/v1'
};
module('sb.services');
inject(function (storyboardApiBase) {
expect(storyboardApiBase).toEqual('https://localhost:8080/api/v1');
expect(storyboardApiBase).toEqual('https://localhost:8080/v1');
});
delete window.ENV;