Add product versions UI

This allows users to view/manage product versions on the
product page.

Change-Id: Id0e5ddb315b58531887c4b8cdda3d20c2d8938f8
This commit is contained in:
Paul Van Eck 2016-09-07 13:04:38 -07:00
parent 540bff5b13
commit 0ebff8d4fb
7 changed files with 342 additions and 26 deletions

View File

@ -12,5 +12,12 @@
</div>
</div>
<div ng-include src="'components/products/partials/management.html'"></div>
<div class="clearfix"></div>
<div ng-include src="'components/products/partials/versions.html'"></div>
</div>
</div>
<div ng-show="ctrl.showError" class="alert alert-danger" role="alert">
<span class="glyphicon glyphicon-exclamation-sign" aria-hidden="true"></span>
<span class="sr-only">Error:</span>
{{ctrl.error}}
</div>

View File

@ -12,5 +12,12 @@
</div>
</div>
<div ng-include src="'components/products/partials/management.html'"></div>
<div class="clearfix"></div>
<div ng-include src="'components/products/partials/versions.html'"></div>
</div>
</div>
<div ng-show="ctrl.showError" class="alert alert-danger" role="alert">
<span class="glyphicon glyphicon-exclamation-sign" aria-hidden="true"></span>
<span class="sr-only">Error:</span>
{{ctrl.error}}
</div>

View File

@ -0,0 +1,29 @@
<strong>Version(s) Available:</strong>
<span ng-repeat="item in ctrl.productVersions | orderBy:'version'">
<a ng-show="item.version && ctrl.product.can_manage" class="label label-info" ng-click="ctrl.openVersionModal(item)">
{{item.version}}
</a>
<span ng-hide="ctrl.product.can_manage" class="label label-info">{{item.version}}</span>
</span>
&nbsp;
<a ng-if="ctrl.product.can_manage"
title="Add a new product version."
ng-click="ctrl.showNewVersionInput = true">
<small><span class="glyphicon glyphicon-plus"></span></small>
</a>
<div ng-if="ctrl.showNewVersionInput" class="row" style="margin-top: 5px;">
<div class="col-md-2">
<div class="input-group">
<input ng-model="ctrl.newProductVersion"
type="text" class="form-control" placeholder="New Version">
<span class="input-group-btn">
<button
class="btn btn-default"
type="button"
ng-click="ctrl.addProductVersion()">
Add
</button>
</span>
</div>
</div>
</div>

View File

@ -0,0 +1,51 @@
<div class="modal-content">
<div class="modal-header">
<h4>Manage Version</h4>
</div>
<div class="modal-body">
<div class="pull-left">
<strong>Version:</strong> {{modal.version.version}}<br />
</div>
<div class="pull-right">
<a class="glyphicon glyphicon-trash"
ng-click="modal.deleteProductVersion()"
confirm="Are you sure you want to delete product version {{modal.version.version}}?">
</a>
</div>
<div class="clearfix"></div>
<br />
(Optional) Associate cloud provider ID (CPID) with product version for easier
test run associating.
<br />
<br />
<div class="row">
<div class="col-md-8">
<strong>CPID:</strong><br />
<div class="input-group">
<input type="text" class="form-control" ng-model="modal.version.cpid" />
<span class="input-group-btn">
<button
class="btn btn-default"
type="button"
ng-click="modal.saveChanges()">
Save
</button>
</span>
</div>
</div>
</div>
<div ng-show="modal.showError" class="alert alert-danger" role="alert">
<span class="glyphicon glyphicon-exclamation-sign" aria-hidden="true"></span>
<span class="sr-only">Error:</span>
{{modal.error}}
</div>
<div ng-show="modal.showSuccess" class="alert alert-success" role="success">
<span class="glyphicon glyphicon-ok" aria-hidden="true"></span>
<span class="sr-only">Success:</span>
Updated Successfully.
</div>
</div>
<div class="modal-footer">
<button class="btn btn-primary" type="button" ng-click="modal.close()">Close</button>
</div>
</div>

View File

@ -20,7 +20,7 @@
.controller('ProductController', ProductController);
ProductController.$inject = [
'$scope', '$http', '$state', '$stateParams', '$window',
'$scope', '$http', '$state', '$stateParams', '$window', '$uibModal',
'refstackApiUrl', 'raiseAlert'
];
@ -30,21 +30,27 @@
* view details of the product.
*/
function ProductController($scope, $http, $state, $stateParams,
$window, refstackApiUrl, raiseAlert) {
$window, $uibModal, refstackApiUrl, raiseAlert) {
var ctrl = this;
ctrl.getProduct = getProduct;
ctrl.getProductVersions = getProductVersions;
ctrl.deleteProduct = deleteProduct;
ctrl.deleteProductVersion = deleteProductVersion;
ctrl.switchProductPublicity = switchProductPublicity;
ctrl.addProductVersion = addProductVersion;
ctrl.openVersionModal = openVersionModal;
/** The product id extracted from the URL route. */
ctrl.id = $stateParams.id;
ctrl.productVersions = [];
if (!$scope.auth.isAuthenticated) {
$state.go('home');
}
ctrl.getProduct();
ctrl.getProductVersions();
/**
* This will contact the Refstack API to get a product information.
@ -52,32 +58,49 @@
function getProduct() {
ctrl.showError = false;
ctrl.product = null;
// Construct the API URL based on user-specified filters.
var content_url = refstackApiUrl + '/products/' + ctrl.id;
ctrl.productRequest =
ctrl.productRequest = $http.get(content_url).success(
function(data) {
ctrl.product = data;
ctrl.product_properties =
angular.fromJson(data.properties);
}
).error(function(error) {
ctrl.productRequest = $http.get(content_url).success(
function(data) {
ctrl.product = data;
ctrl.product_properties =
angular.fromJson(data.properties);
}
).error(function(error) {
ctrl.showError = true;
ctrl.error =
'Error retrieving from server: ' +
angular.toJson(error);
}).then(function() {
var url = refstackApiUrl + '/vendors/' +
ctrl.product.organization_id;
$http.get(url).success(function(data) {
ctrl.vendor = data;
}).error(function(error) {
ctrl.showError = true;
ctrl.error =
'Error retrieving from server: ' +
angular.toJson(error);
}).then(function() {
var url = refstackApiUrl + '/vendors/' +
ctrl.product.organization_id;
$http.get(url).success(function(data) {
ctrl.vendor = data;
}).error(function(error) {
ctrl.showError = true;
ctrl.error =
'Error retrieving from server: ' +
angular.toJson(error);
});
});
});
}
/**
* This will contact the Refstack API to get product versions.
*/
function getProductVersions() {
ctrl.showError = false;
var content_url = refstackApiUrl + '/products/' + ctrl.id +
'/versions';
ctrl.productVersionsRequest = $http.get(content_url).success(
function(data) {
ctrl.productVersions = data;
}
).error(function(error) {
ctrl.showError = true;
ctrl.error =
'Error retrieving versions from server: ' +
angular.toJson(error);
});
}
/**
@ -92,6 +115,38 @@
});
}
/**
* This will delete the given product versions.
*/
function deleteProductVersion(versionId) {
var url = [
refstackApiUrl, '/products/', ctrl.id,
'/versions/', versionId ].join('');
$http.delete(url).success(function () {
ctrl.getProductVersions();
}).error(function (error) {
raiseAlert('danger', 'Error: ', error.detail);
});
}
/**
* Set a POST request to the API server to add a new version for
* the product.
*/
function addProductVersion() {
var url = [refstackApiUrl, '/products/', ctrl.id,
'/versions'].join('');
ctrl.addVersionRequest = $http.post(url,
{'version': ctrl.newProductVersion})
.success(function (data) {
ctrl.productVersions.push(data);
ctrl.newProductVersion = '';
ctrl.showNewVersionInput = false;
}).error(function (error) {
raiseAlert('danger', error.title, error.detail);
});
}
/**
* This will switch public/private property of the product.
*/
@ -105,5 +160,90 @@
raiseAlert('danger', 'Error: ', error.detail);
});
}
/**
* This will open the modal that will allow a product version
* to be managed.
*/
function openVersionModal(version) {
$uibModal.open({
templateUrl: '/components/products/partials' +
'/versionsModal.html',
backdrop: true,
windowClass: 'modal',
animation: true,
controller: 'ProductVersionModalController as modal',
size: 'lg',
resolve: {
version: function () {
return version;
},
parent: function () {
return ctrl;
}
}
});
}
}
angular
.module('refstackApp')
.controller('ProductVersionModalController',
ProductVersionModalController);
ProductVersionModalController.$inject = [
'$uibModalInstance', '$http', 'refstackApiUrl', 'version', 'parent'
];
/**
* Product Version Modal Controller
* This controller is for the modal that appears if a user wants to
* manage a product version.
*/
function ProductVersionModalController($uibModalInstance, $http,
refstackApiUrl, version, parent) {
var ctrl = this;
ctrl.version = version;
ctrl.parent = parent;
ctrl.close = close;
ctrl.deleteProductVersion = deleteProductVersion;
ctrl.saveChanges = saveChanges;
/**
* This function will close/dismiss the modal.
*/
function close() {
$uibModalInstance.dismiss('exit');
}
/**
* Call the parent function to delete a version, then close the modal.
*/
function deleteProductVersion() {
ctrl.parent.deleteProductVersion(ctrl.version.id);
ctrl.close();
}
/**
* This will update the current version, saving changes.
*/
function saveChanges() {
ctrl.showSuccess = false;
ctrl.showError = false;
var url = [
refstackApiUrl, '/products/', ctrl.version.product_id,
'/versions/', ctrl.version.id ].join('');
var content = {'cpid': ctrl.version.cpid};
$http.put(url, content).success(function() {
ctrl.showSuccess = true;
}).error(function(error) {
ctrl.showError = true;
ctrl.error = error.detail;
});
}
}
})();

View File

@ -19,7 +19,6 @@ RefStack
<li ng-class="{ active: header.isActive('/about')}"><a ui-sref="about">About</a></li>
<li ng-class="{ active: header.isActive('/guidelines')}"><a ui-sref="guidelines">DefCore Guidelines</a></li>
<li ng-class="{ active: header.isActive('/community_results')}"><a ui-sref="communityResults">Community Results</a></li>
<!---
<li ng-class="{ active: header.isCatalogActive('public')}" class="dropdown" uib-dropdown>
<a role="button" class="dropdown-toggle" uib-dropdown-toggle>
Catalog <strong class="caret"></strong>
@ -29,11 +28,9 @@ RefStack
<li><a ui-sref="publicProducts">Products</a></li>
</ul>
</li>
--->
</ul>
<ul class="nav navbar-nav navbar-right">
<li ng-class="{ active: header.isActive('/user_results')}" ng-if="auth.isAuthenticated"><a ui-sref="userResults">My Results</a></li>
<!---
<li ng-if="auth.isAuthenticated" ng-class="{ active: header.isCatalogActive('user')}" class="dropdown" uib-dropdown>
<a role="button" class="dropdown-toggle" uib-dropdown-toggle>
My Catalog <strong class="caret"></strong>
@ -43,7 +40,6 @@ RefStack
<li><a ui-sref="userProducts">My Products</a></li>
</ul>
</li>
--->
<li ng-class="{ active: header.isActive('/profile')}" ng-if="auth.isAuthenticated"><a ui-sref="profile">Profile</a></li>
<li ng-if="auth.isAuthenticated"><a href="" ng-click="auth.doSignOut()">Sign Out</a></li>
<li ng-if="!auth.isAuthenticated"><a href="" ng-click="auth.doSignIn()">Sign In / Sign Up</a></li>

View File

@ -1103,6 +1103,10 @@ describe('Refstack controllers', function () {
'type': 0,
'id': '1234',
'description': 'some description'};
var fakeVersionResp = [{'id': 'asdf',
'cpid': null,
'version': '1.0',
'product_id': '1234'}];
var fakeVendorResp = {'id': 'fake-org-id',
'type': 3,
'can_manage': true,
@ -1128,6 +1132,8 @@ describe('Refstack controllers', function () {
);
$httpBackend.when('GET', fakeApiUrl +
'/products/1234').respond(fakeProdResp);
$httpBackend.when('GET', fakeApiUrl +
'/products/1234/versions').respond(fakeVersionResp);
$httpBackend.when('GET', fakeApiUrl +
'/vendors/fake-org-id').respond(fakeVendorResp);
}));
@ -1144,6 +1150,16 @@ describe('Refstack controllers', function () {
expect(ctrl.vendor).toEqual(fakeVendorResp);
});
it('should have a function to get a list of product versions',
function () {
$httpBackend
.expectGET(fakeApiUrl + '/products/1234/versions')
.respond(200, fakeVersionResp);
ctrl.getProductVersions();
$httpBackend.flush();
expect(ctrl.productVersions).toEqual(fakeVersionResp);
});
it('should have a function to delete a product',
function () {
$httpBackend.expectDELETE(fakeApiUrl + '/products/1234')
@ -1153,6 +1169,26 @@ describe('Refstack controllers', function () {
expect(fakeWindow.location.href).toEqual('/');
});
it('should have a function to delete a product version',
function () {
$httpBackend
.expectDELETE(fakeApiUrl + '/products/1234/versions/abc')
.respond(204, '');
ctrl.deleteProductVersion('abc');
$httpBackend.flush();
});
it('should have a function to add a product version',
function () {
ctrl.newProductVersion = 'abc';
$httpBackend.expectPOST(
fakeApiUrl + '/products/1234/versions',
{version: 'abc'})
.respond(200, {'id': 'foo'});
ctrl.addProductVersion();
$httpBackend.flush();
});
it('should have a function to switch the publicity of a project',
function () {
ctrl.product = {'public': true};
@ -1162,5 +1198,55 @@ describe('Refstack controllers', function () {
ctrl.switchProductPublicity();
$httpBackend.flush();
});
it('should have a method to open a modal for version management',
function () {
var modal;
inject(function ($uibModal) {
modal = $uibModal;
});
spyOn(modal, 'open');
ctrl.openVersionModal();
expect(modal.open).toHaveBeenCalled();
});
});
describe('ProductVersionModalController', function() {
var ctrl, modalInstance, state, parent;
var fakeVersion = {'id': 'asdf', 'cpid': null,
'version': '1.0','product_id': '1234'};
beforeEach(inject(function ($controller) {
modalInstance = {
dismiss: jasmine.createSpy('modalInstance.dismiss')
};
parent = {
deleteProductVersion: jasmine.createSpy('deleteProductVersion')
};
ctrl = $controller('ProductVersionModalController',
{$uibModalInstance: modalInstance, $state: state,
version: fakeVersion, parent: parent}
);
}));
it('should have a function to prompt a version deletion',
function () {
ctrl.deleteProductVersion();
expect(parent.deleteProductVersion)
.toHaveBeenCalledWith('asdf');
expect(modalInstance.dismiss).toHaveBeenCalledWith('exit');
});
it('should have a function to save changes',
function () {
ctrl.version.cpid = 'some-cpid';
var expectedContent = { 'cpid': 'some-cpid'};
$httpBackend.expectPUT(
fakeApiUrl + '/products/1234/versions/asdf',
expectedContent).respond(200, '');
ctrl.saveChanges();
$httpBackend.flush();
});
});
});