Refactor Project Group editing to match other objects

Currently project groups have a separate "edit" view, which makes it
hard to route to them by name. This commit changes this so that editing
is done in the detail view, as it is for other objects like Projects or
Stories, in order to facilitate routing by name.

Depends-On: https://review.openstack.org/#/c/590047/
Change-Id: I7f26838ce255268653cd2f912a4f7bb2a13d2816
This commit is contained in:
Adam Coldrick 2018-08-31 00:39:35 +01:00
parent e6bf69db1e
commit a4614afd95
5 changed files with 356 additions and 418 deletions

View File

@ -20,7 +20,8 @@
*/
angular.module('sb.project_group').controller('ProjectGroupDetailController',
function ($scope, $stateParams, projectGroup, Story, Project,
Preference, SubscriptionList, CurrentUser, Subscription) {
Preference, SubscriptionList, CurrentUser, Subscription,
$q, ProjectGroupItem, ArrayUtil, $log) {
'use strict';
var projectPageSize = Preference.get(
@ -43,6 +44,12 @@ angular.module('sb.project_group').controller('ProjectGroupDetailController',
$scope.projects = [];
$scope.isSearchingProjects = false;
$scope.editMode = false;
$scope.toggleEdit = function() {
$scope.editMode = !$scope.editMode;
};
/**
* List the projects in this Project Group
*/
@ -205,6 +212,194 @@ angular.module('sb.project_group').controller('ProjectGroupDetailController',
};
/**
* UI flag, are we saving?
*
* @type {boolean}
*/
$scope.isSaving = false;
/**
* Project typeahead search method.
*/
$scope.searchProjects = function (value) {
var deferred = $q.defer();
Project.browse({name: value, limit: 10},
function (results) {
// Dedupe the results.
var idxList = [];
for (var i = 0; i < $scope.projects.length; i++) {
var project = $scope.projects[i];
if (!!project) {
idxList.push(project.id);
}
}
for (var j = results.length - 1; j >= 0; j--) {
var resultId = results[j].id;
if (idxList.indexOf(resultId) > -1) {
results.splice(j, 1);
}
}
deferred.resolve(results);
},
function (error) {
$log.error(error);
deferred.resolve([]);
});
return deferred.promise;
};
/**
* Formats the project name.
*/
$scope.formatProjectName = function (model) {
if (!!model) {
return model.name;
}
return '';
};
/**
* Remove a project from the list
*/
$scope.removeProject = function (index) {
$scope.projects.splice(index, 1);
};
/**
* Save the project and the associated groups
*/
$scope.save = function () {
$scope.isSaving = true;
ProjectGroupItem.browse({projectGroupId: $scope.projectGroup.id},
function(results) {
var loadedIds = [];
results.forEach(function (project) {
loadedIds.push(project.id);
});
var promises = [];
// Get the desired ID's.
var desiredIds = [];
$scope.projects.forEach(function (project) {
desiredIds.push(project.id);
});
// Intersect loaded vs. current to get a list of project
// reference to delete.
var idsToDelete = ArrayUtil.difference(
loadedIds, desiredIds);
idsToDelete.forEach(function (id) {
// Get a deferred promise...
var removeProjectDeferred = $q.defer();
// Construct the item.
var item = new ProjectGroupItem({
id: id,
projectGroupId: projectGroup.id
});
// Delete the item.
item.$delete(function (result) {
removeProjectDeferred.resolve(result);
},
function (error) {
removeProjectDeferred.reject(error);
}
);
promises.push(removeProjectDeferred.promise);
});
// Intersect current vs. loaded to get a list of project
// reference to add.
var idsToAdd = ArrayUtil.difference(desiredIds, loadedIds);
idsToAdd.forEach(function (id) {
// Get a deferred promise...
var addProjectDeferred = $q.defer();
// Construct the item.
var item = new ProjectGroupItem({
id: id,
projectGroupId: projectGroup.id
});
// Delete the item.
item.$create(function (result) {
addProjectDeferred.resolve(result);
},
function (error) {
addProjectDeferred.reject(error);
}
);
promises.push(addProjectDeferred.promise);
});
// Save the project group itself.
var deferred = $q.defer();
promises.push(deferred.promise);
$scope.projectGroup.$update(function (success) {
deferred.resolve(success);
}, function (error) {
$log.error(error);
deferred.reject(error);
});
// Roll all the promises into one big happy promise.
$q.all(promises).then(
function () {
$scope.editMode = false;
$scope.isSaving = false;
},
function (error) {
$log.error(error);
}
);
}
);
};
/**
* Add project.
*/
$scope.addProject = function () {
$scope.projects.push({});
};
/**
* Insert item into the project list.
*/
$scope.selectNewProject = function (index, model) {
// Put our model into the array
$scope.projects[index] = model;
};
/**
* Check that we have valid projects on the list
*/
$scope.checkValidProjects = function () {
if ($scope.projects.length === 0) {
return false;
}
// check if projects contain a valid project_id
for (var i = 0; i < $scope.projects.length; i++) {
var project = $scope.projects[i];
if (!project.id) {
return false;
}
}
return true;
};
$scope.listProjects();
$scope.filterStories();

View File

@ -1,222 +0,0 @@
/*
* 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.
*/
/**
* New Project Group edit controller.
*/
angular.module('sb.project_group').controller('ProjectGroupEditController',
function ($q, $log, $scope, $state, projectGroup, projects, Project,
ProjectGroupItem, ArrayUtil) {
'use strict';
/**
* The project group we're editing. Resolved by the route.
*/
$scope.projectGroup = projectGroup;
/**
* The list of projects in this group (Resolved by the route).
*/
$scope.projects = projects;
/**
* A collection of all project ID's that have been loaded on
* initialization. This list is used to determine the project member
* diff.
*/
var loadedIds = [];
$scope.projects.forEach(function (project) {
loadedIds.push(project.id);
});
/**
* UI flag, are we saving?
*
* @type {boolean}
*/
$scope.isSaving = false;
/**
* Project typeahead search method.
*/
$scope.searchProjects = function (value) {
var deferred = $q.defer();
Project.browse({name: value, limit: 10},
function (results) {
// Dedupe the results.
var idxList = [];
for (var i = 0; i < $scope.projects.length; i++) {
var project = $scope.projects[i];
if (!!project) {
idxList.push(project.id);
}
}
for (var j = results.length - 1; j >= 0; j--) {
var resultId = results[j].id;
if (idxList.indexOf(resultId) > -1) {
results.splice(j, 1);
}
}
deferred.resolve(results);
},
function (error) {
$log.error(error);
deferred.resolve([]);
});
return deferred.promise;
};
/**
* Formats the project name.
*/
$scope.formatProjectName = function (model) {
if (!!model) {
return model.name;
}
return '';
};
/**
* Remove a project from the list
*/
$scope.removeProject = function (index) {
$scope.projects.splice(index, 1);
};
/**
* Save the project and the associated groups
*/
$scope.save = function () {
$scope.isSaving = true;
var promises = [];
// Get the desired ID's.
var desiredIds = [];
$scope.projects.forEach(function (project) {
desiredIds.push(project.id);
});
// Intersect loaded vs. current to get a list of project
// reference to delete.
var idsToDelete = ArrayUtil.difference(loadedIds, desiredIds);
idsToDelete.forEach(function (id) {
// Get a deferred promise...
var removeProjectDeferred = $q.defer();
// Construct the item.
var item = new ProjectGroupItem({
id: id,
projectGroupId: projectGroup.id
});
// Delete the item.
item.$delete(function (result) {
removeProjectDeferred.resolve(result);
},
function (error) {
removeProjectDeferred.reject(error);
}
);
promises.push(removeProjectDeferred.promise);
});
// Intersect current vs. loaded to get a list of project
// reference to add.
var idsToAdd = ArrayUtil.difference(desiredIds, loadedIds);
idsToAdd.forEach(function (id) {
// Get a deferred promise...
var addProjectDeferred = $q.defer();
// Construct the item.
var item = new ProjectGroupItem({
id: id,
projectGroupId: projectGroup.id
});
// Delete the item.
item.$create(function (result) {
addProjectDeferred.resolve(result);
},
function (error) {
addProjectDeferred.reject(error);
}
);
promises.push(addProjectDeferred.promise);
});
// Save the project group itself.
var deferred = $q.defer();
promises.push(deferred.promise);
$scope.projectGroup.$update(function (success) {
deferred.resolve(success);
}, function (error) {
$log.error(error);
deferred.reject(error);
});
// Roll all the promises into one big happy promise.
$q.all(promises).then(
function () {
$state.go('sb.project_group.list', {});
},
function (error) {
$log.error(error);
}
);
};
/**
* Add project.
*/
$scope.addProject = function () {
$scope.projects.push({});
};
/**
* Insert item into the project list.
*/
$scope.selectNewProject = function (index, model) {
// Put our model into the array
$scope.projects[index] = model;
};
/**
* Check that we have valid projects on the list
*/
$scope.checkValidProjects = function () {
if ($scope.projects.length === 0) {
return false;
}
// check if projects contain a valid project_id
for (var i = 0; i < $scope.projects.length; i++) {
var project = $scope.projects[i];
if (!project.id) {
return false;
}
}
return true;
};
});

View File

@ -58,21 +58,6 @@ angular.module('sb.project_group',
deferred.reject(error);
});
return deferred.promise;
}
}
})
.state('sb.project_group.edit', {
url: '/{id:[0-9]+}/edit',
templateUrl: 'app/project_group/template/edit.html',
controller: 'ProjectGroupEditController',
resolve: {
projectGroup: function ($stateParams, ProjectGroup) {
return ProjectGroup.get({id: $stateParams.id}).$promise;
},
projects: function ($stateParams, ProjectGroupItem) {
return ProjectGroupItem.browse(
{projectGroupId: $stateParams.id}
).$promise;
},
isSuperuser: PermissionResolver
.requirePermission('is_superuser', true)

View File

@ -6,7 +6,7 @@
<i class="fa fa-sb-project-group"></i>
{{projectGroup.title}}
<small>
<a href="#!/project_group/{{projectGroup.id}}/edit" permission="is_superuser">
<a href="" ng-click="toggleEdit()" permission="is_superuser">
<i class="fa fa-pencil-alt"></i>
</a>
<subscribe resource="project_group"
@ -18,7 +18,8 @@
</h1>
</div>
</div>
<div class="row">
<div ng-include src="'/inline/project_group_edit.html'" ng-if="editMode"></div>
<div class="row" ng-if="!editMode">
<div class="col-xs-2 text-center text-muted">
<span class="hidden-xs">
<i class="fa fa-3x fa-sb-project"></i>
@ -57,7 +58,7 @@
</table>
</div>
</div>
<div class="row">
<div class="row" ng-if="!editMode">
<div class="col-xs-12">
<hr/>
</div>
@ -129,3 +130,159 @@
</div>
</div>
</div>
<script type="text/ng-template" id="/inline/project_group_edit.html">
<div class="row">
<div class="col-xs-12">
<form class="form-horizontal"
role="form"
name="projectGroupForm"
ng-class="{'has-error': projectGroupForm.title.$invalid}">
<div class="form-group">
<label for="title" class="col-sm-2 control-label">
Title:
</label>
<div class="col-sm-10">
<input id="title"
name="title"
type="text"
class="form-control"
ng-model="projectGroup.title"
required
ng-disabled="isSaving"
maxlength="255"
ng-minlength="3"
ng-pattern="PROJECT_NAME_REGEX"
placeholder="Project Group Title">
</input>
<div class="help-block text-danger"
ng-show="projectGroupForm.title.$invalid">
<span ng-show="projectGroupForm.title.$error.required">
A project group title is required.
</span>
<span ng-show="projectGroupForm.title.$error.pattern">
A project group title must begin with a letter, and may only
contain letters, numbers, forward slashes, periods, and
dashes. It should not start or end with a separator and
must not contain two or more sequential separators.
</span>
<span ng-show="projectGroupForm.title.$error.minlength">
A project group title must have at least 3 characters.
</span>
</div>
</div>
</div>
<div class="form-group">
<label for="name" class="col-sm-2 control-label">
URL:
</label>
<div class="col-sm-10">
<input id="name"
name="name"
type="text"
class="form-control"
ng-model="projectGroup.name"
required
ng-disabled="isSaving"
maxlength="100"
ng-minlength="3"
placeholder="URL Stub for the project group">
</input>
<div class="help-block text-danger"
ng-show="projectGroupForm.name.$invalid">
<span ng-show="projectGroupForm.name.$error.required">
A project group URL is required.
</span>
<span ng-show="projectGroupForm.name.$error.minlength">
A project group URL must have at least 3 characters.
</span>
</div>
</div>
</div>
</form>
</div>
</div>
<div class="row">
<div class="col-sm-2 text-right">
<label class="control-label">
Projects:
</label>
</div>
<div class="col-sm-10">
<form role="form" name="projectsForm">
<table class="table table-striped table-outlined">
<tbody>
<tr ng-repeat="(index, project) in projects"
ng-include
src="'/inline/project_row.html'">
</tr>
</tbody>
</table>
</form>
</div>
</div>
<div class="row">
<div class="col-xs-4 col-xs-offset-2">
<button type="button"
class="btn btn-default"
ng-disabled="isSaving"
ng-click="addProject()">
&plus;
Add another project
</button>
</div>
<div class="col-xs-6 text-right">
<button type="button"
class="btn btn-primary"
ng-click="save()"
ng-disabled="!projectGroupForm.$valid || !projectsForm.$valid || !checkValidProjects() || isSaving">
Save
</button>
<a href=""
ng-click="toggleEdit()"
ng-disabled="isSaving"
class="btn btn-default">
Cancel
</a>
</div>
</div>
</script>
<!-- Template for story metadata -->
<script type="text/ng-template" id="/inline/project_row.html">
<td class="col-xs-11">
<div class="has-feedback has-feedback-no-label">
<input id="project"
type="text"
placeholder="Select a Project"
autocomplete="off"
required
ng-model="project"
typeahead-wait-ms="200"
typeahead-editable="false"
typeahead="project as project.name for project
in searchProjects($viewValue)"
typeahead-loading="loadingProjects"
typeahead-input-formatter="formatProjectName($model)"
typeahead-on-select="selectNewProject(index, $model)"
class="form-control input-sm"
ng-disabled="isSaving"
/>
<span class="form-control-feedback text-muted
form-control-feedback-sm">
<i class="fa fa-sync fa-spin" ng-show="loadingProjects"></i>
<i class="fa fa-search" ng-hide="loadingProjects"></i>
</span>
</div>
</td>
<th class="col-xs-1"
ng-show="projects.length > 1">
<button type="button" class="close"
ng-click="removeProject(index)">
&times;
</button>
</th>
</script>

View File

@ -1,177 +0,0 @@
<!--
~ 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-fluid">
<div class="row">
<div class="col-xs-12">
<h1><i class="fa fa-sb-project-group"></i> {{projectGroup.title}}
</h1>
</div>
</div>
<div class="row">
<div class="col-xs-12">
<form class="form-horizontal"
role="form"
name="projectGroupForm"
ng-class="{'has-error': projectGroupForm.title.$invalid}">
<div class="form-group">
<label for="title" class="col-sm-2 control-label">
Title:
</label>
<div class="col-sm-10">
<input id="title"
name="title"
type="text"
class="form-control"
ng-model="projectGroup.title"
required
ng-disabled="isSaving"
maxlength="255"
ng-minlength="3"
ng-pattern="PROJECT_NAME_REGEX"
placeholder="Project Group Title">
</input>
<div class="help-block text-danger"
ng-show="projectGroupForm.title.$invalid">
<span ng-show="projectGroupForm.title.$error.required">
A project group title is required.
</span>
<span ng-show="projectGroupForm.title.$error.pattern">
A project group title must begin with a letter, and may only
contain letters, numbers, forward slashes, periods, and
dashes. It should not start or end with a separator and
must not contain two or more sequential separators.
</span>
<span ng-show="projectGroupForm.title.$error.minlength">
A project group title must have at least 3 characters.
</span>
</div>
</div>
</div>
<div class="form-group">
<label for="name" class="col-sm-2 control-label">
URL:
</label>
<div class="col-sm-10">
<input id="name"
name="name"
type="text"
class="form-control"
ng-model="projectGroup.name"
required
ng-disabled="isSaving"
maxlength="100"
ng-minlength="3"
placeholder="URL Stub for the project group">
</input>
<div class="help-block text-danger"
ng-show="projectGroupForm.name.$invalid">
<span ng-show="projectGroupForm.name.$error.required">
A project group URL is required.
</span>
<span ng-show="projectGroupForm.name.$error.minlength">
A project group URL must have at least 3 characters.
</span>
</div>
</div>
</div>
</form>
</div>
</div>
<div class="row">
<div class="col-sm-2 text-right">
<label class="control-label">
Projects:
</label>
</div>
<div class="col-sm-10">
<form role="form" name="projectsForm">
<table class="table table-striped table-outlined">
<tbody>
<tr ng-repeat="(index, project) in projects"
ng-include
src="'/inline/project_row.html'">
</tr>
</tbody>
</table>
</form>
</div>
</div>
<div class="row">
<div class="col-xs-4 col-xs-offset-2">
<button type="button"
class="btn btn-default"
ng-disabled="isSaving"
ng-click="addProject()">
&plus;
Add another project
</button>
</div>
<div class="col-xs-6 text-right">
<button type="button"
class="btn btn-primary"
ng-click="save()"
ng-disabled="!projectGroupForm.$valid || !projectsForm.$valid || !checkValidProjects() || isSaving">
Save
</button>
<a href="#!/project_group"
ng-disabled="isSaving"
class="btn btn-default">
Cancel
</a>
</div>
</div>
</div>
<!-- Template for story metadata -->
<script type="text/ng-template" id="/inline/project_row.html">
<td class="col-xs-11">
<div class="has-feedback has-feedback-no-label">
<input id="project"
type="text"
placeholder="Select a Project"
autocomplete="off"
required
ng-model="project"
typeahead-wait-ms="200"
typeahead-editable="false"
typeahead="project as project.name for project
in searchProjects($viewValue)"
typeahead-loading="loadingProjects"
typeahead-input-formatter="formatProjectName($model)"
typeahead-on-select="selectNewProject(index, $model)"
class="form-control input-sm"
ng-disabled="isSaving"
/>
<span class="form-control-feedback text-muted
form-control-feedback-sm">
<i class="fa fa-sync fa-spin" ng-show="loadingProjects"></i>
<i class="fa fa-search" ng-hide="loadingProjects"></i>
</span>
</div>
</td>
<th class="col-xs-1"
ng-show="projects.length > 1">
<button type="button" class="close"
ng-click="removeProject(index)">
&times;
</button>
</th>
</script>