Add Due Dates to boards

This patch adds a table of due dates in a given board on the
board detail view. This allows due dates to be created, edited,
and removed from the board (they will not be deleted in this way).

At the moment there is no way to assign a due date to a card, but
this patch adds CSS such that cards with impending due dates or
late due dates will show some indication of it.

Change-Id: Ib7905fc7d5ea2313219976f5c62ec38c012fde8e
This commit is contained in:
Adam Coldrick 2016-02-24 17:20:31 +00:00
parent 0a546b32e7
commit e8b8c9fe3c
12 changed files with 1000 additions and 47 deletions

View File

@ -19,7 +19,7 @@
*/
angular.module('sb.board').controller('BoardDetailController',
function ($scope, Worklist, $modal, Board, Project, $stateParams,
BoardHelper, $document, User, $q) {
BoardHelper, DueDate, $document, User, $q, moment) {
'use strict';
/**
@ -285,6 +285,99 @@ angular.module('sb.board').controller('BoardDetailController',
modelArray.splice(modelIdx, 1);
};
$scope.newDueDate = function() {
var modalInstance = $modal.open({
templateUrl: 'app/due_dates/template/new.html',
controller: 'DueDateNewController',
resolve: {
board: function() {
return $scope.board;
}
}
});
modalInstance.result.finally(function() {
loadBoard();
});
};
$scope.editDueDate = function(dueDate) {
var modalInstance = $modal.open({
templateUrl: 'app/due_dates/template/new.html',
controller: 'DueDateEditController',
resolve: {
board: function() {
return $scope.board;
},
dueDate: function() {
return dueDate;
}
}
});
modalInstance.result.finally(function() {
loadBoard();
});
};
$scope.removeDueDate = function(dueDate) {
var modalInstance = $modal.open({
templateUrl: 'app/due_dates/template/remove_from_board.html',
controller: 'DueDateRemoveController',
resolve: {
board: function() {
return $scope.board;
},
dueDate: function() {
return dueDate;
}
}
});
modalInstance.result.finally(function() {
loadBoard();
});
};
$scope.isDue = function(card) {
if (card.item_type === 'task') {
if (card.task.status === 'merged') {
return false;
}
}
var now = moment();
var tomorrow = now.clone();
tomorrow.add(1, 'day');
if (!card.resolved_due_date) {
return false;
}
if ((now.isSame(card.resolved_due_date.date) ||
now.isBefore(card.resolved_due_date.date)) &&
(tomorrow.isSame(card.resolved_due_date.date) ||
tomorrow.isAfter(card.resolved_due_date.date))) {
return true;
} else {
return false;
}
};
$scope.isLate = function(card) {
if (card.item_type === 'task') {
if (card.task.status === 'merged') {
return false;
}
}
var now = moment();
if (!card.resolved_due_date) {
return false;
}
if (now.isAfter(card.resolved_due_date.date)) {
return true;
} else {
return false;
}
};
/**
* Config for the lanes sortable.
*/

View File

@ -17,30 +17,73 @@
ng-model="lane.worklist.items"
class="kanban-lane-contents">
<div class="kanban-card"
ng-class="{'kanban-card-due': isDue(item), 'kanban-card-late': isLate(item)}"
as-sortable-item
ng-repeat="item in lane.worklist.items"
ng-switch="item.item_type">
<div as-sortable-item-handle ng-switch-when="story">
<button type="button" class="close" title="Remove"
ng-click="removeCard(lane.worklist, item)">
&times;
</button>
<i class="fa fa-sb-story text-muted"></i>
<a href="#!/story/{{item.story.id}}">
{{item.story.title}}
</a>
<div class="row">
<div class="col-xs-1">
<i class="fa fa-sb-story text-muted"></i>
</div>
<div class="col-xs-10">
<button type="button" class="close" title="Remove"
ng-click="removeCard(lane.worklist, item); $event.stopPropagation();">
&times;
</button>
<a href="#!/story/{{item.story.id}}">
{{item.story.title}}
</a>
</div>
</div>
<div class="row target-date" ng-show="item.resolved_due_date">
<div class="col-xs-1">
<i class="fa fa-clock-o text-muted"></i>
</div>
<div class="col-xs-10">
<span time-moment
eventdate="item.resolved_due_date.date"
format-string="'MMM Do, YYYY'">
</span>
<br/>
<span class="text-muted">{{item.resolved_due_date.name}}</span>
</div>
</div>
</div>
<div as-sortable-item-handle ng-switch-when="task">
<button type="button" class="close" title="Remove"
ng-click="removeCard(lane.worklist, item)">
&times;
</button>
<i class="fa fa-tasks text-muted"></i>
<a href="#!/story/{{item.task.story_id}}">
{{item.task.title}}
</a>
<user-typeahead ng-show="item.task.assignee_id"
ng-model="item.task.assignee_id"/>
<div class="row">
<div class="col-xs-1">
<i class="fa fa-tasks text-muted"></i>
</div>
<div class="col-xs-10">
<button type="button" class="close" title="Remove"
ng-click="removeCard(lane.worklist, item); $event.stopPropagation()">
&times;
</button>
<a href="#!/story/{{item.task.story_id}}">
{{item.task.title}}
</a>
</div>
</div>
<div class="row target-date" ng-show="item.resolved_due_date">
<div class="col-xs-1">
<i class="fa fa-clock-o text-muted"></i>
</div>
<div class="col-xs-10">
<span time-moment
eventdate="item.resolved_due_date.date"
format-string="'MMM Do, YYYY'">
</span>
<br/>
<span class="text-muted">{{item.resolved_due_date.name}}</span>
</div>
</div>
<div class="row">
<div class="col-xs-12">
<user-typeahead ng-show="item.task.assignee_id"
ng-model="item.task.assignee_id"/>
</div>
</div>
</div>
</div>
</div>

View File

@ -15,21 +15,64 @@
-->
<div class="kanban-lane-contents-readonly">
<div class="kanban-card-readonly"
ng-class="{'kanban-card-due': isDue(item), 'kanban-card-late': isLate(item)}"
ng-repeat="item in lane.worklist.items"
ng-switch="item.item_type">
<div ng-switch-when="story">
<i class="fa fa-sb-story text-muted"></i>
<a href="#!/story/{{item.story.id}}">
{{item.story.title}}
</a>
<div class="row">
<div class="col-xs-1">
<i class="fa fa-sb-story text-muted"></i>
</div>
<div class="col-xs-10">
<a href="#!/story/{{item.story.id}}">
{{item.story.title}}
</a>
</div>
</div>
<div class="row target-date" ng-show="item.resolved_due_date">
<div class="col-xs-1">
<i class="fa fa-clock-o text-muted"></i>
</div>
<div class="col-xs-10">
<span time-moment
eventdate="item.resolved_due_date.date"
format-string="'MMM Do, YYYY'">
</span>
<br/>
<span class="text-muted">{{item.resolved_due_date.name}}</span>
</div>
</div>
</div>
<div ng-switch-when="task">
<i class="fa fa-tasks text-muted"></i>
<a href="#!/story/{{item.task.story_id}}">
{{item.task.title}}
</a>
<user-typeahead ng-show="item.task.assignee_id"
ng-model="item.task.assignee_id"/>
<div class="row">
<div class="col-xs-1">
<i class="fa fa-tasks text-muted"></i>
</div>
<div class="col-xs-10">
<a href="#!/story/{{item.task.story_id}}">
{{item.task.title}}
</a>
</div>
</div>
<div class="row target-date" ng-show="item.resolved_due_date">
<div class="col-xs-1">
<i class="fa fa-clock-o text-muted"></i>
</div>
<div class="col-xs-10">
<span time-moment
eventdate="item.resolved_due_date.date"
format-string="'MMM Do, YYYY'">
</span>
<br/>
<span class="text-muted">{{item.resolved_due_date.name}}</span>
</div>
</div>
<div class="row">
<div class="col-xs-12">
<user-typeahead ng-show="item.task.assignee_id"
ng-model="item.task.assignee_id"/>
</div>
</div>
</div>
</div>
</div>

View File

@ -34,21 +34,77 @@
<!-- Template for the header and description -->
<script type="text/ng-template" id="/inline/board_detail.html">
<div class="row">
<div class="col-sm-12">
<h1 class="no-margin-bottom" view-title>
{{board.title}}
<small>
<a ng-click="toggleEditMode()"
ng-show="permissions.editBoard">
<i class="fa fa-pencil"></i>
</a>
</small>
</h1>
<div class="col-sm-8">
<div class="row">
<div class="col-sm-12">
<h1 class="no-margin-bottom" view-title>
{{board.title}}
<small>
<a ng-click="toggleEditMode()"
ng-show="permissions.editBoard">
<i class="fa fa-pencil"></i>
</a>
</small>
</h1>
</div>
</div>
<div class="row">
<div class="col-sm-12">
<insert-markdown ng-if="board.description"
content="board.description">
</insert-markdown>
</div>
</div>
</div>
<div class="col-sm-12">
<insert-markdown ng-if="board.description"
content="board.description">
</insert-markdown>
<div class="col-sm-4">
<small>
<table class="table table-striped table-condensed">
<thead>
<tr><th colspan="5">Due Dates</th></tr>
</thead>
<tbody>
<tr ng-repeat="dueDate in board.due_dates">
<td ng-if="dueDate.name"
class="col-xs-4">
{{dueDate.name}}
</td>
<td ng-if="dueDate.name"
class="col-xs-3">
<span time-moment eventdate="dueDate.date"></span>
</td>
<td colspan="2"
ng-if="!dueDate.name"
class="col-xs-8">
<span time-moment eventdate="dueDate.date"></span>
</td>
<td class="col-xs-3">
{{dueDate.count}} cards
</td>
<td class="col-xs-1 text-right">
<a href ng-click="editDueDate(dueDate)"
ng-if="dueDate.editable">
<i class="fa fa-pencil"></i>
</a>
</td>
<td class="col-xs-1 text-right">
<a href ng-click="removeDueDate(dueDate)"
ng-if="dueDate.editable">
<i class="fa fa-times"></i>
</a>
</td>
</tr>
<tr class="hover-row" ng-click="newDueDate()"
ng-if="permissions.moveCards || permissions.editBoard">
<td colspan="5" class="text-center">
<a href>
<i class="fa fa-plus-circle"></i>
New due date
</a>
</td>
</tr>
</tbody>
</table>
</small>
</div>
</div>
</script>

View File

@ -0,0 +1,128 @@
/*
* Copyright (c) 2016 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 due date" modal popup.
*/
angular.module('sb.due_date').controller('DueDateEditController',
function ($scope, $modalInstance, $state, $q, DueDate, User,
board, dueDate) {
'use strict';
$scope.owners = [];
$scope.users = [];
angular.forEach(dueDate.owners, function(id) {
$scope.owners.push(User.get({id: id}));
});
angular.forEach(dueDate.users, function(id) {
$scope.users.push(User.get({id: id}));
});
/**
* User typeahead search method.
*/
$scope.searchUsers = function (value, array) {
var deferred = $q.defer();
User.browse({full_name: value, limit: 10},
function(searchResults) {
var results = [];
angular.forEach(searchResults, function(result) {
if (array.indexOf(result.id) === -1) {
results.push(result);
}
});
deferred.resolve(results);
}
);
return deferred.promise;
};
/**
* Formats the user name.
*/
$scope.formatUserName = function (model) {
if (!!model) {
return model.name;
}
return '';
};
/**
* Add a new user to one of the permission levels.
*/
$scope.addUser = function (model, modelArray, idArray) {
idArray.push(model.id);
modelArray.push(model);
};
/**
* Remove a user from one of the permission levels.
*/
$scope.removeUser = function (model, modelArray, idArray) {
var idIdx = idArray.indexOf(model.id);
idArray.splice(idIdx, 1);
var modelIdx = modelArray.indexOf(model);
modelArray.splice(modelIdx, 1);
};
/**
* Save changes to the due date.
*/
function saveDueDate() {
DueDate.update($scope.dueDate, function (result) {
var params = {id: $scope.dueDate.id};
var owners = {
codename: 'edit_date',
users: $scope.dueDate.owners
};
var users = {
codename: 'assign_date',
users: $scope.dueDate.users
};
DueDate.Permissions.update(params, users)
.$promise.then(function() {
DueDate.Permissions.update(params, owners)
.$promise.then(function() {
$scope.isSaving = false;
$modalInstance.close(result);
});
});
}
);
}
/**
* Saves the due date.
*/
$scope.save = function() {
$scope.isSaving = true;
$scope.dueDate.date.second(0);
saveDueDate();
};
/**
* Close this modal without saving.
*/
$scope.close = function() {
$modalInstance.dismiss('cancel');
};
// Create a blank DueDate to begin with.
$scope.isSaving = false;
$scope.dueDate = dueDate;
$scope.modalTitle = 'Edit Due Date';
});

View File

@ -0,0 +1,168 @@
/*
* Copyright (c) 2016 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 due date" modal popup.
*/
angular.module('sb.due_date').controller('DueDateNewController',
function ($scope, $modalInstance, $state, $q, DueDate, board, CurrentUser,
User) {
'use strict';
var currentUser = CurrentUser.resolve();
$scope.owners = [];
$scope.users = [];
angular.forEach(board.owners, function(id) {
$scope.owners.push(User.get({id: id}));
});
angular.forEach(board.users, function(id) {
$scope.users.push(User.get({id: id}));
});
/**
* Create the new due date.
*/
function saveDueDate() {
if ($scope.mode === 'edit') {
$scope.dueDate.$create(
function (result) {
$scope.isSaving = false;
$modalInstance.close(result);
}
);
} else if ($scope.mode === 'find') {
DueDate.update({
id: $scope.selected.id,
board_id: board.id
}, function (result) {
$scope.isSaving = false;
$modalInstance.close(result);
});
}
}
/**
* User typeahead search method.
*/
$scope.searchUsers = function (value, array) {
var deferred = $q.defer();
User.browse({full_name: value, limit: 10},
function(searchResults) {
var results = [];
angular.forEach(searchResults, function(result) {
if (array.indexOf(result.id) === -1) {
results.push(result);
}
});
deferred.resolve(results);
}
);
return deferred.promise;
};
/**
* Formats the user name.
*/
$scope.formatUserName = function (model) {
if (!!model) {
return model.name;
}
return '';
};
/**
* Add a new user to one of the permission levels.
*/
$scope.addUser = function (model, modelArray, idArray) {
idArray.push(model.id);
modelArray.push(model);
};
/**
* Remove a user from one of the permission levels.
*/
$scope.removeUser = function (model, modelArray, idArray) {
var idIdx = idArray.indexOf(model.id);
idArray.splice(idIdx, 1);
var modelIdx = modelArray.indexOf(model);
modelArray.splice(modelIdx, 1);
};
/**
* Saves the due date.
*/
$scope.save = function() {
$scope.isSaving = true;
$scope.dueDate.date.second(0);
saveDueDate();
};
/**
* Close this modal without saving.
*/
$scope.close = function() {
$modalInstance.dismiss('cancel');
};
$scope.setMode = function(mode) {
$scope.mode = mode;
};
$scope.searchDueDates = function() {
DueDate.browse($scope.criteria, function(results) {
var existing = [];
angular.forEach(board.due_dates, function(date) {
existing.push(date.id);
});
$scope.results = results;
angular.forEach(results, function(result) {
if (existing.indexOf(result.id) !== -1) {
var idx = $scope.results.indexOf(result);
$scope.results.splice(idx, 1);
}
});
});
};
$scope.select = function(dueDate) {
$scope.selected = dueDate;
};
$scope.selected = {};
$scope.criteria = {};
currentUser.then(function(result) {
currentUser = result;
$scope.criteria = {
user: currentUser.id
};
});
// Create a blank DueDate to begin with.
$scope.isSaving = false;
$scope.newDueDate = true;
$scope.modalTitle = 'New Due Date';
$scope.mode = 'edit';
$scope.dueDate = new DueDate({
name: ''
});
if (!!board) {
$scope.dueDate.board_id = board.id;
$scope.dueDate.owners = board.owners;
$scope.dueDate.users = board.users;
}
});

View File

@ -0,0 +1,45 @@
/*
* 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 "archive board" modal
*/
angular.module('sb.board').controller('DueDateRemoveController',
function ($log, $scope, $state, board, dueDate, $modalInstance, DueDate) {
'use strict';
$scope.board = board;
$scope.dueDate = dueDate;
// Set our progress flags and clear previous error conditions.
$scope.isUpdating = true;
$scope.error = {};
$scope.remove = function () {
DueDate.delete({
id: dueDate.id,
board_id: board.id
},
function () {
$modalInstance.close('success');
}
);
};
$scope.close = function () {
$modalInstance.dismiss('cancel');
};
});

View File

@ -0,0 +1,41 @@
/*
* Copyright (c) 2016 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 due dates submodule handles the creation of task and story
* due dates.
*/
angular.module('sb.due_date', ['ui.router', 'sb.services', 'sb.util',
'ui.bootstrap'])
.config(function ($stateProvider, $urlRouterProvider) {
'use strict';
// URL Defaults.
$urlRouterProvider.when('/due_date', '/due_date/list');
// Set our page routes.
$stateProvider
.state('sb.due_date', {
abstract: true,
url: '/due_date',
template: '<div ui-view></div>'
})
.state('sb.due_date.detail', {
url: '/{dueDateID:[0-9]+}',
controller: 'DueDateDetailController',
templateUrl: 'app/due_dates/template/detail.html'
});
});

View File

@ -0,0 +1,232 @@
<!--
~ Copyright (c) 2015-2016 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 due-date-modal">
<div class="panel-heading">
<button type="button" class="close" aria-hidden="true"
ng-click="close()">&times;</button>
<h3 class="panel-title">{{modalTitle}}</h3>
</div>
<div class="panel-body">
<ul class="nav nav-tabs"
ng-if="newDueDate">
<li ng-class="{'active': mode === 'edit'}">
<a href ng-click="setMode('edit')">Create New</a>
</li>
<li ng-class="{'active': mode === 'find'}">
<a href ng-click="setMode('find')">Use Existing</a>
</li>
</ul>
<div class="row" ng-show="(mode === 'edit') || !newDueDate">
<div class="col-xs-12">
<form class="form-horizontal" role="form" name="dueDateForm">
<div class="form-group">
<label for="title" class="col-sm-2 control-label">
Name
</label>
<div class="col-sm-10">
<input id="title"
name="title"
type="text"
class="form-control"
ng-model="dueDate.name"
focus
maxlength="100"
placeholder="Due date name"
ng-disabled="isSaving">
</div>
</div>
<div class="form-group">
<label for="private" class="col-sm-2 control-label">
Private
</label>
<div class="col-sm-10 checkbox">
<input id="private"
type="checkbox"
class="modal-checkbox"
ng-model="dueDate.private"
ng-disabled="isSaving"
/>
</div>
</div>
<div class="form-group">
<calendar selected-date="dueDate.date"></calendar>
</div>
</form>
<div class="row" ng-include src="'/inline/permissions_tables.html'">
</div>
</div>
</div>
<div class="row" ng-show="(mode === 'find') && newDueDate">
<div class="col-xs-12">
<form class="form-horizontal" role="form" name="dueDateForm">
<div class="form-group">
<label for="title" class="col-sm-2 control-label">
Name
</label>
<div class="col-sm-10">
<input id="title"
name="title"
type="text"
class="form-control"
ng-model="criteria.name"
ng-change="searchDueDates()"
placeholder="Due date name"
ng-disabled="isSaving">
</div>
</div>
</form>
<div class="table-container">
<table class="table table-fixed table-striped table-hover">
<tbody>
<tr ng-repeat="result in results"
ng-click="select(result)"
ng-class="{'selected': selected === result}">
<td>{{result.name}}</td>
<td><span time-moment eventdate="result.date"></span></td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<div class="row">
<div class="col-xs-12 text-right">
<div class="btn" ng-show="isSaving">
<i class="fa fa-spinner fa-lg fa-spin"></i>
</div>
<button type="button"
class="btn btn-primary"
ng-click="save()"
ng-disabled="!dueDateForm.$valid || isSaving">
<span ng-show="mode != 'find'">
Save Due Date
</span>
<span ng-show="mode === 'find'">
Add Due Date
</span>
</button>
<button type="button"
ng-click="close()"
class="btn btn-default"
ng-disabled="isSaving">
Cancel
</button>
</div>
</div>
</div>
</div>
<script type="text/ng-template" id="/inline/permissions_tables.html">
<div class="col-sm-6">
<table class="table table-striped">
<thead>
<tr title="Owners can modify the name and date of the Due
Date, in addition to everything Users can do.">
<th>Owners</th>
<th class="text-right">
<small>
<a href
ng-click="showAddOwner = !showAddOwner"
ng-disabled="isUpdating">
<i class="fa fa-plus" ng-if="!showAddOwner"></i>
<i class="fa fa-minus" ng-if="showAddOwner"></i>
Add Owner
</a>
</small>
</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="owner in owners">
<td colspan="2">
{{owner.full_name}}
<a class="close"
ng-show="owners.length > 1"
ng-click="removeUser(owner, owners, dueDate.owners)">
&times;
</a>
</td>
</tr>
<tr ng-show="showAddOwner && !isUpdating">
<td colspan="2">
<input id="owner"
type="text"
placeholder="Click to add an owner"
ng-model="asyncOwner"
typeahead-wait-ms="200"
typeahead-editable="false"
typeahead="user as user.full_name for user in
searchUsers($viewValue, dueDate.owners)"
typeahead-loading="loadingUsers"
typeahead-input-formatter="formatUserName($model)"
typeahead-on-select="addUser($model, owners, dueDate.owners)"
class="form-control input-sm"
/>
</td>
</tr>
</tbody>
</table>
</div>
<div class="col-sm-6">
<table class="table table-striped">
<thead>
<tr title="Users can assign the Due Date to Tasks and Stories, and
add it to Boards or Worklists that they have access to.">
<th>Users</th>
<th class="text-right">
<small>
<a href
ng-click="showAddUser = !showAddUser"
ng-disabled="isUpdating">
<i class="fa fa-plus" ng-if="!showAddUser"></i>
<i class="fa fa-minus" ng-if="showAddUser"></i>
Add User
</a>
</small>
</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="user in users">
<td colspan="2">
{{user.full_name}}
<a class="close"
ng-click="removeUser(user, users, dueDate.users)">
&times;
</a>
</td>
</tr>
<tr ng-show="showAddUser && !isUpdating">
<td colspan="2">
<input id="user"
type="text"
placeholder="Click to add a user"
ng-model="asyncUser"
typeahead-wait-ms="200"
typeahead-editable="false"
typeahead="user as user.full_name for user in
searchUsers($viewValue, dueDate.users)"
typeahead-loading="loadingUsers"
typeahead-input-formatter="formatUserName($model)"
typeahead-on-select="addUser($model, users, dueDate.users)"
class="form-control input-sm"
/>
</td>
</tr>
</tbody>
</table>
</div>
</script>

View File

@ -0,0 +1,57 @@
<!--
~ 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">
Removing Due Date:
<span time-moment eventdate="dueDate.date"
format-string="'MMMM Do, YYYY'"></span>
<span ng-show="dueDate.name">({{dueDate.name}})</span>
</h3>
</div>
<div class="panel-body">
<div class="row">
<div class="col-sm-8 col-sm-offset-2 text-center">
<h2 class="text-danger">
Confirm removal of due date
</h2>
<p class="text-danger">
Are you certain that you want to remove the due date
</p>
<div>
<span time-moment eventdate="dueDate.date"
format-string="'MMMM Do, YYYY [at] HH:mm'"></span>
<br>
<span ng-show="dueDate.name" class="text-muted">{{dueDate.name}}</span>
</div>
<p class="text-danger">
from the board "{{board.title}}"?
</p>
<div class="text-center">
<a href="" class="btn btn-danger" ng-click="remove()">
Remove
</a>
<a href="" class="btn btn-default" ng-click="close()">
Cancel
</a>
</div>
</div>
</div>
</div>
</div>

View File

@ -26,9 +26,9 @@ 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', 'sb.worklist',
'sb.board', 'ui.router', 'ui.bootstrap', 'monospaced.elastic',
'angularMoment', 'angular-data.DSCacheFactory', 'viewhead',
'ngSanitize', 'as.sortable'])
'sb.board', 'sb.due_date', 'ui.router', 'ui.bootstrap',
'monospaced.elastic', 'angularMoment', 'angular-data.DSCacheFactory',
'viewhead', 'ngSanitize', 'as.sortable'])
.constant('angularMomentConfig', {
preprocess: 'utc',
timezone: 'UTC'

View File

@ -121,13 +121,39 @@
border: 1px solid #bbb;
& user-typeahead > .form-group {
margin-bottom: 0px;
margin-top: 10px;
margin-top: 0px;
> .form-control-static {
padding: 0px;
}
}
}
.kanban-card-due {
border-right: @border-radius-base solid #555;
& .target-date {
font-weight: bold;
}
}
.kanban-card-late {
border-right: @border-radius-base solid #555;
& .target-date {
font-weight: bold;
color: @navbar-default-color;
background-color: @brand-primary;
border-radius: 10px;
margin: 0px;
& .col-xs-1, & .col-xs-11 {
padding: 0px 5px;
}
& .text-muted {
color: @navbar-default-color;
}
}
}
.kanban-card-readonly {
.kanban-card;
@ -141,6 +167,10 @@
width: 100vw;
&:hover {
cursor: pointer;
& a {
color: darken(@brand-primary, 10%);
text-decoration: underline;
}
}
}
@ -162,3 +192,20 @@
box-sizing: border-box;
background-color: #999;
}
.due-date-modal {
& .nav-tabs {
margin-bottom: 10px;
}
& .table-hover {
& tr:hover {
background-color: #efefef;
cursor: pointer;
}
& tr.selected {
background-color: @brand-primary;
color: @navbar-default-color;
}
}
}