Add a page to view a worklist

Add a worklist detail page, and various other modal templates for
interacting with the worklist. Also add ng-sortable[0] as a new
dependency. This is used to allow drag-and-drop rearrangement of
the worklist contents.

[0]: https://github.com/a5hik/ng-sortable

Change-Id: I5cc85e687f9ce60db158168a9f4c8325b1a022f6
This commit is contained in:
Adam Coldrick 2015-10-07 13:38:07 +00:00
parent 930813112b
commit 01bd40c183
18 changed files with 719 additions and 7 deletions

View File

@ -150,7 +150,8 @@ module.exports = function (grunt) {
dir.theme + '/storyboard/',
dir.bower + '/bootstrap/less/',
dir.bower + '/font-awesome/less/',
dir.bower + '/highlightjs/styles/'
dir.bower + '/highlightjs/styles/',
dir.bower + '/ng-sortable/dist/'
];
},
cleancss: true,

View File

@ -15,7 +15,8 @@
"angular-cache": "3.2.5",
"angularjs-viewhead": "0.0.1",
"marked": "0.3.4",
"highlightjs": "8.4"
"highlightjs": "8.4",
"ng-sortable": "1.3.1"
},
"devDependencies": {
"angular-mocks": "1.3.13",

View File

@ -1,5 +1,6 @@
<a ng-switch="match.model.type"
class="header-criteria-item">
class="header-criteria-item"
title="{{match.model.type}}: {{match.model.title}}">
<span ng-switch-when="Text">
<i class="fa fa-search text-muted"></i>&emsp;{{match.model.title}}
</span>
@ -29,6 +30,9 @@
<span ng-switch-when="User">
<i class="fa fa-sb-user text-muted"></i>&emsp;{{match.model.title}}
</span>
<span ng-switch-when="Task">
<i class="fa fa-tasks text-muted"></i>&emsp;{{match.model.title}}
</span>
<span ng-switch-default>
<i class="fa fa-question text-muted"></i>&emsp;{{match.model.type}}
</span>

View File

@ -33,7 +33,7 @@ angular.module('sb.services').factory('Task',
ResourceFactory.applySearch(
'Task',
resource,
null,
'title',
{
Text: 'q',
TaskStatus: 'status',

View File

@ -25,9 +25,10 @@
angular.module('storyboard',
[ 'sb.services', 'sb.templates', 'sb.dashboard', 'sb.pages', 'sb.projects',
'sb.auth', 'sb.story', 'sb.profile', 'sb.notification', 'sb.search',
'sb.admin', 'sb.subscription', 'sb.project_group', 'ui.router',
'ui.bootstrap', 'monospaced.elastic', 'angularMoment',
'angular-data.DSCacheFactory', 'viewhead', 'ngSanitize'])
'sb.admin', 'sb.subscription', 'sb.project_group', 'sb.worklist',
'ui.router', 'ui.bootstrap', 'monospaced.elastic',
'angularMoment', 'angular-data.DSCacheFactory', 'viewhead',
'ngSanitize', 'as.sortable'])
.constant('angularMomentConfig', {
preprocess: 'utc',
timezone: 'UTC'

View File

@ -0,0 +1,44 @@
/*
* Copyright (c) 2015 Codethink Limited
*
* 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.
*/
/**
* Controller for the "new worklist" modal popup.
*/
angular.module('sb.worklist').controller('AddWorklistController',
function ($scope, $modalInstance, $state, params, Worklist) {
'use strict';
/**
* Saves the worklist.
*/
$scope.save = function () {
$scope.worklist.$create(
function (result) {
$modalInstance.dismiss('success');
$state.go('sb.worklist.detail', {worklistID: result.id});
}
);
};
/**
* Close this modal without saving.
*/
$scope.close = function () {
$modalInstance.dismiss('cancel');
};
$scope.worklist = new Worklist({title: ''});
});

View File

@ -0,0 +1,129 @@
/*
* Copyright (c) 2015 Codethink Limited
*
* 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.
*/
/**
* Controller for "delete worklist" modal
*/
angular.module('sb.worklist').controller('WorklistAddItemController',
function ($log, $scope, $state, worklist, $modalInstance, Story, Task,
Criteria, Worklist, $q, valid) {
'use strict';
$scope.worklist = worklist;
$scope.items = [];
// Set our progress flags and clear previous error conditions.
$scope.loadingItems = false;
$scope.error = {};
$scope.save = function() {
var offset = $scope.worklist.items.length;
for (var i = 0; i < $scope.items.length; i++) {
var item = $scope.items[i];
var item_type = '';
if (item.type === 'Task') {
item_type = 'task';
} else if (item.type === 'Story') {
item_type = 'story';
}
var params = {
item_id: item.value,
id: $scope.worklist.id,
list_position: offset + i,
item_type: item_type
};
if (valid(item)) {
Worklist.ItemsController.create(params);
}
}
$modalInstance.dismiss('success');
};
/**
* Remove an item from the list of items to add.
*/
$scope.removeItem = function (item) {
var idx = $scope.items.indexOf(item);
$scope.items.splice(idx, 1);
};
/**
* Item typeahead search method.
*/
$scope.searchItems = function (value) {
var deferred = $q.defer();
var searchString = value || '';
var searches = [
Story.criteriaResolver(searchString, 5),
Task.criteriaResolver(searchString, 5)
];
$q.all(searches).then(function (searchResults) {
var criteria = [];
var addResult = function (item) {
if (valid(item)) {
criteria.push(item);
}
};
for (var i = 0; i < searchResults.length; i++) {
var results = searchResults[i];
if (!results) {
continue;
}
if (!!results.forEach) {
results.forEach(addResult);
} else {
addResult(results);
}
}
deferred.resolve(criteria);
});
return deferred.promise;
};
/**
* Formats the item name.
*/
$scope.formatItemName = function (model) {
if (!!model) {
return model.title;
}
return '';
};
/**
* Select a new item.
*/
$scope.selectNewItem = function (model) {
$scope.items.push(model);
$scope.asyncItem = '';
};
$scope.close = function () {
$modalInstance.dismiss('cancel');
};
});

View File

@ -0,0 +1,42 @@
/*
* Copyright (c) 2015 Codethink Limited
*
* 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.
*/
/**
* Controller for "delete worklist" modal
*/
angular.module('sb.worklist').controller('WorklistDeleteController',
function ($log, $scope, $state, worklist, $modalInstance) {
'use strict';
$scope.worklist = worklist;
// Set our progress flags and clear previous error conditions.
$scope.isUpdating = true;
$scope.error = {};
$scope.remove = function () {
$scope.worklist.$delete(
function () {
$modalInstance.dismiss('success');
$state.go('sb.dashboard.worklist');
}
);
};
$scope.close = function () {
$modalInstance.dismiss('cancel');
};
});

View File

@ -0,0 +1,140 @@
/*
* Copyright (c) 2015 Codethink Limited
*
* 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.
*/
/**
* A controller that manages the worklist detail page.
*/
angular.module('sb.worklist').controller('WorklistDetailController',
function ($scope, $modal, $timeout, $stateParams, Worklist) {
'use strict';
/**
* Load the worklist and its contents.
*/
function loadWorklist() {
var params = {id: $stateParams.worklistID};
Worklist.get(params).$promise.then(function(result) {
$scope.worklist = result;
Worklist.loadContents(result, true);
});
}
/**
* Save the worklist.
*/
function saveWorklist() {
$scope.worklist.$update().then(function() {
Worklist.loadContents($scope.worklist, true);
});
}
/**
* Toggle edit mode on the worklist. If going on->off then
* save changes.
*/
$scope.toggleEditMode = function() {
if (!$scope.worklist.editing) {
$scope.worklist.editing = true;
} else {
$scope.worklist.editing = false;
saveWorklist();
}
};
/**
* Show a modal to handle adding items to the worklist.
*/
function showAddItemModal() {
var modalInstance = $modal.open({
templateUrl: 'app/worklists/template/additem.html',
controller: 'WorklistAddItemController',
resolve: {
worklist: function() {
return $scope.worklist;
},
valid: function() {
return function() {
// No limit on the contents of worklists
return true;
};
}
}
});
return modalInstance.result;
}
/**
* Display the add-item modal and reload the worklist when
* it is closed.
*/
$scope.addItem = function() {
showAddItemModal().finally(loadWorklist);
};
/**
* Remove an item from the worklist.
*/
$scope.removeItem = function(item) {
Worklist.ItemsController.delete({
id: $scope.worklist.id,
item_id: item.list_item_id
}).$promise.then(function() {
var idx = $scope.worklist.items.indexOf(item);
$scope.worklist.items.splice(idx, 1);
});
};
/**
* Show a modal to handle archiving the worklist.
*/
$scope.remove = function() {
var modalInstance = $modal.open({
templateUrl: 'app/worklists/template/delete.html',
controller: 'WorklistDeleteController',
resolve: {
worklist: function() {
return $scope.worklist;
}
}
});
return modalInstance.result;
};
/**
* Config for worklist sortable.
*/
$scope.sortableOptions = {
accept: function (sourceHandle, dest) {
return sourceHandle.itemScope.sortableScope.$id === dest.$id;
},
orderChanged: function(result) {
var list = result.source.sortableScope.$parent.worklist;
for (var i = 0; i < list.items.length; i++) {
var item = list.items[i];
item.position = i;
Worklist.ItemsController.update({
id: list.id,
item_id: item.list_item_id,
list_position: item.position
});
}
}
};
// Load the worklist.
loadWorklist();
});

View File

@ -0,0 +1,41 @@
/**
* Copyright (c) 2015 Codethink Limited
*
* 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 story submodule handles most activity surrounding the
* creation and management of stories, their tasks, and comments.
*/
angular.module('sb.worklist', ['ui.router', 'sb.services', 'sb.util',
'ui.bootstrap'])
.config(function ($stateProvider, $urlRouterProvider) {
'use strict';
// URL Defaults.
$urlRouterProvider.when('/worklist', '/worklist/list');
// Set our page routes.
$stateProvider
.state('sb.worklist', {
abstract: true,
url: '/worklist',
template: '<div ui-view></div>'
})
.state('sb.worklist.detail', {
url: '/{worklistID:[0-9]+}',
controller: 'WorklistDetailController',
templateUrl: 'app/worklists/template/detail.html'
});
});

View File

@ -0,0 +1,89 @@
<!--
~ Copyright (c) 2015 Codethink Limited
~
~ 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="panel panel-default">
<div class="panel-heading">
<button type="button" class="close" aria-hidden="true"
ng-click="close()">&times;</button>
<h3 class="panel-title">Adding items to {{worklist.title}}</h3>
</div>
<div class="panel-body">
<div class="row">
<div class="col-xs-12">
<form class="form-horizontal" role="form" name="worklistForm">
<div class="form-group">
<label for="search" class="col-sm-2 control-label">
Search
</label>
<div class="has-feedback col-sm-10">
<input id="search"
type="text"
placeholder="Search for Stories and Tasks"
ng-model="asyncItem"
typeahead-wait-ms="200"
typeahead-editable="false"
typeahead="item as item.title for item
in searchItems($viewValue)"
typeahead-loading="loadingItems"
typeahead-input-formatter="formatItemName($model)"
typeahead-on-select="selectNewItem($model)"
typeahead-template-url="app/search/template/typeahead_criteria_item.html"
class="form-control"
/>
<span class="form-control-feedback text-muted
form-control-feedback-sm">
<i class="fa fa-refresh fa-spin" ng-show="loadingItems"></i>
<i class="fa fa-search" ng-hide="loadingItems"></i>
</span>
</div>
</div>
</form>
<table class="table table-striped table-bordered">
<tbody>
<tr ng-repeat="item in items">
<td ng-switch="item.type" class="header-criteria-item"
title="{{item.type}}: {{item.title}}">
<span ng-switch-when="Story">
<i class="fa fa-sb-story text-muted"></i>&emsp;{{item.title}}
</span>
<span ng-switch-when="Task">
<i class="fa fa-tasks text-muted"></i>&emsp;{{item.title}}
</span>
<button type="button" class="close" title="Remove"
ng-click="removeItem(item)">
&times;
</button>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="row">
<div class="col-xs-12 text-right">
<button type="button"
class="btn btn-primary"
ng-click="save()">
Save Changes
</button>
<button type="button"
ng-click="close()"
class="btn btn-default">
Cancel
</button>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,37 @@
<!--
~ Copyright (c) 2015 Codethink Limited
~
~ 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="panel panel-default">
<div class="panel-heading">
<button type="button" class="close" aria-hidden="true"
ng-click="close()">&times;</button>
<h3 class="panel-title">{{worklist.title}}</h3>
</div>
<div class="panel-body">
<div class="row">
<div class="col-sm-8 col-sm-offset-2">
<h2 class="text-danger text-center">
Are you certain that you want to archive this worklist?
</h2>
<div class="text-center">
<a href="" class="btn btn-danger" ng-click="remove()">
Archive this worklist
</a>
</div>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,118 @@
<!--
~ Copyright (c) 2015 Codethink Limited
~
~ 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-sm-12" ng-show="!worklist.editing">
<h1 class="no-margin-bottom" view-title>
{{worklist.title}}
<a ng-click="toggleEditMode()">
<i class="fa fa-pencil"></i>
</a>
<button type="button" class="pull-right close" title="Delete"
ng-click="remove()">
<i class="fa fa-times"></i>
</button>
</h1>
</div>
<div class="col-sm-12" ng-show="worklist.editing">
<h1 class="no margin-bottom">
<input type="text" class="form-control form-control-inline h1"
ng-model="worklist.title">
<a ng-click="toggleEditMode()">
<i class="fa fa-pencil"></i>
</a>
<button type="button" class="pull-right close" title="Delete"
ng-click="remove()">
<i class="fa fa-times"></i>
</button>
</h1>
</div>
</div>
<div class="row">
<div class="col-sm-12">
<table class="table table-striped table-hover">
<tbody as-sortable="sortableOptions" ng-model="worklist.items"
ng-show="!worklist.editing">
<tr as-sortable-item ng-repeat="item in worklist.items">
<td as-sortable-item-handle ng-switch="item.type" class="hover-row"
title="{{item.type}}: {{item.title}}">
<a href="#!/story/{{item.id}}" ng-switch-when="story">
<div class="col-sm-1">
<i class="fa fa-sb-story text-muted"></i>
</div>
<div class="col-sm-11">
{{item.title}}
</div>
</a>
<a href="#!/story/{{item.story_id}}" ng-switch-when="task">
<div class="col-sm-1">
<i class="fa fa-tasks text-muted"></i>
</div>
<div class="col-sm-11">
{{item.title}}
</div>
</a>
</td>
</tr>
</tbody>
<tbody ng-show="worklist.editing">
<tr ng-repeat="item in worklist.items">
<td ng-switch="item.type" class="hover-row"
title="{{item.type}}: {{item.title}}">
<a href="#!/story/{{item.value}}" ng-switch-when="story">
<div class="col-sm-1">
<i class="fa fa-sb-story text-muted"></i>
</div>
<div class="col-sm-10">
{{item.title}}
</div>
</a>
<a href="#!/story/{{item.story_id}}" ng-switch-when="task">
<div class="col-sm-1">
<i class="fa fa-tasks text-muted"></i>
</div>
<div class="col-sm-10">
{{item.title}}
</div>
</a>
<div class="col-sm-1">
<button type="button" class="close" title="Remove"
ng-click="removeItem(item)">
&times;
</button>
</div>
</td>
</tr>
</tbody>
<tbody ng-show="worklist.items.length == 0">
<tr>
<td>
This worklist is currently empty.
</td>
</tr>
</tbody>
<tbody>
<tr class="hover-row text-center" title="Add Item"
ng-click="addItem()">
<td>
Add Items
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>

View File

@ -41,6 +41,7 @@
<script src="angularjs-viewhead/angularjs-viewhead.js"></script>
<script src="marked/marked.min.js"></script>
<script src="highlightjs/highlight.pack.js"></script>
<script src="ng-sortable/dist/ng-sortable.js"></script>
<!-- endbuild -->
<link rel="stylesheet" href="styles/main.css">

View File

@ -0,0 +1,48 @@
/*
* Copyright (c) 2015 Codethink Limited
*
* 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.
*/
/**
* Board and worklist styles.
*/
.form-control-inline {
width: auto;
display: inline;
}
/* Worklist style */
.hover-row {
width: 100vw;
&:hover {
cursor: pointer;
}
}
/* ng-sortable style */
.as-sortable-dragging > .as-sortable-item {
width: inherit;
height: inherit;
}
.as-sortable-placeholder {
border: 1px dashed #ddd;
border-radius: 4px;
box-sizing: border-box;
background-color: #999;
}

View File

@ -42,3 +42,9 @@ table.table.table-clean {
}
}
}
.table-striped {
> tbody > tr:nth-of-type(even) {
background-color: @white;
}
}

View File

@ -207,3 +207,7 @@ td .form-group:last-child {
div .container-fluid {
margin: 0% 5%;
}
.modal-checkbox {
margin-left: 0px !important;
}

View File

@ -24,8 +24,13 @@
@import './bootstrap.less';
@import './base/bootstrap/navbar.less';
@import './font-awesome.less';
// HighlightJS theme
@import (less) './default.css';
// ng-sortable styles
@import (less) './ng-sortable.css';
// Theme
@import './theme.less';
// Addons to the bootstrap theme.
@ -43,3 +48,4 @@
@import './base/header.less';
@import './base/icons.less';
@import './base/edit_tasks.less';
@import './base/boards_worklists.less';