Display a modal when a card is clicked on

This modal shows some information about the task/story the
card represents, and also allows it all to be edited. This
includes changing task assignee and assigning due dates to
the task.

Only one due date can be displayed on a card at any given
time.

Change-Id: I69866756e4b6b569e7f88cf41bb00c45ca5dedb9
This commit is contained in:
Adam Coldrick 2016-02-24 17:25:14 +00:00
parent e8b8c9fe3c
commit 55712fc403
7 changed files with 764 additions and 6 deletions

View File

@ -378,6 +378,28 @@ angular.module('sb.board').controller('BoardDetailController',
}
};
$scope.showCardDetail = function(card) {
var modalInstance = $modal.open({
templateUrl: 'app/boards/template/card_details.html',
controller: 'CardDetailController',
resolve: {
card: function() {
return card;
},
board: function() {
return $scope.board;
},
permissions: function() {
return $scope.permissions;
}
}
});
modalInstance.result.finally(function() {
loadBoard();
});
};
/**
* Config for the lanes sortable.
*/

View File

@ -0,0 +1,256 @@
/*
* 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 card detail modal.
*/
angular.module('sb.board').controller('CardDetailController',
function ($scope, card, board, permissions, Story, Task, DueDate,
Worklist, $document, $timeout, $modalInstance, $modal) {
'use strict';
/**
* Story/Task title
*/
$scope.modifications = {
title: '',
description: ''
};
$scope.toggleEditTitle = function() {
if (!(permissions.moveCards || permissions.editBoard)) {
return false;
}
if (!$scope.editingTitle) {
if (card.item_type === 'story') {
$scope.modifications.title = card.story.title;
} else if (card.item_type === 'task') {
$scope.modifications.title = card.task.title;
}
}
$scope.editingTitle = !$scope.editingTitle;
};
$scope.editTitle = function() {
var params = {};
if (card.item_type === 'story') {
params = {
id: card.story.id,
title: $scope.modifications.title
};
Story.update(params, function(updated) {
$scope.toggleEditTitle();
card.story.title = updated.title;
});
} else if (card.item_type === 'task') {
params = {
id: card.task.id,
title: $scope.modifications.title
};
Task.update(params, function(updated) {
$scope.toggleEditTitle();
card.task.title = updated.title;
});
}
};
/**
* Story description
*/
$scope.toggleEditDescription = function() {
if (!(permissions.moveCards || permissions.editBoard)) {
return false;
}
if (!$scope.editingTitle) {
$scope.modifications.description = $scope.story.description;
}
$scope.editingDescription = !$scope.editingDescription;
};
$scope.editStoryDescription = function() {
var params = {
id: $scope.story.id,
description: $scope.modifications.description
};
Story.update(params, function(updated) {
$scope.toggleEditDescription();
$scope.story.description = updated.description;
});
};
/**
* Due dates
*/
$scope.noDate = {
id: -1,
date: null
};
$scope.getRelevantDueDates = function() {
$scope.relevantDates = [];
angular.forEach(board.due_dates, function(date) {
if (date.assignable) {
$scope.relevantDates.push(date);
}
});
};
$scope.toggleEditDueDate = function() {
if (permissions.moveCards || permissions.editBoard) {
$scope.editingDueDate = !$scope.editingDueDate;
}
};
$scope.toggleDueDateDropdown = function() {
var dropdown = $document[0].getElementById('due-dates-dropdown');
var button = dropdown.getElementsByTagName('button')[0];
$timeout(function() {
button.click();
}, 0);
};
function cardHasDate(date) {
for (var i = 0; i < card[card.item_type].due_dates.length; i++) {
if (card[card.item_type].due_dates[i] === date.id) {
return true;
}
}
return false;
}
function assignDueDate(date) {
if (card.item_type === 'task') {
date.tasks.push(card.task);
} else if (card.item_type === 'story') {
date.stories.push(card.story);
}
var params = {
id: date.id,
tasks: date.tasks,
stories: date.stories
};
DueDate.update(params).$promise.then(function(updated) {
if (card.item_type === 'task') {
card.task.due_dates.push(updated.id);
$scope.getRelevantDueDates(card.task.due_dates);
} else if (card.item_type === 'story') {
card.story.due_dates.push(updated.id);
$scope.getRelevantDueDates(card.story.due_dates);
}
$scope.assigningDueDate = false;
});
}
$scope.setDisplayDate = function(date) {
if (!cardHasDate(date) && date.id !== -1) {
assignDueDate(date);
}
card.resolved_due_date = date;
var params = {
id: card.list_id,
item_id: card.id,
list_position: card.list_position,
display_due_date: date.id
};
Worklist.ItemsController.update(params, function() {
$scope.editingDueDate = false;
});
};
$scope.validDueDate = function(dueDate) {
return dueDate && !(dueDate === $scope.noDate);
};
/**
* Task assignee
*/
$scope.toggleAssigneeTypeahead = function() {
var typeahead = $document[0].getElementById('assignee');
var assignLink = typeahead.getElementsByTagName('a')[0];
$timeout(function() {
assignLink.click();
}, 0);
};
$scope.toggleEditAssignee = function() {
$scope.editingAssignee = !$scope.editingAssignee;
};
$scope.updateTask = function(task) {
var params = {
id: task.id,
assignee_id: task.assignee_id
};
Task.update(params, function() {
$scope.editingAssignee = false;
});
};
/**
* Other
*/
$scope.deleteCard = function() {
Worklist.ItemsController.delete({
id: $scope.card.list_id,
item_id: $scope.card.id
}, function() {
$modalInstance.close('deleted');
});
};
$scope.close = function() {
$modalInstance.close('closed');
};
$scope.newDueDate = function() {
var modalInstance = $modal.open({
templateUrl: 'app/due_dates/template/new.html',
controller: 'DueDateNewController',
resolve: {
board: function() {
return board;
}
}
});
modalInstance.result.then(function(dueDate) {
if (dueDate.hasOwnProperty('date')) {
board.due_dates.push(dueDate);
}
$scope.getRelevantDueDates();
$scope.setDisplayDate(dueDate);
});
};
if (card.item_type === 'task') {
$scope.story = Story.get({id: card.task.story_id});
} else if (card.item_type === 'story') {
$scope.story = card.story;
}
$scope.getRelevantDueDates();
$scope.card = card;
$scope.board = board;
$scope.permissions = permissions;
$scope.showDescription = true;
$scope.assigningDueDate = false;
$scope.editingDueDate = false;
$scope.editingDescription = false;
$scope.editingAssignee = false;
}
);

View File

@ -20,7 +20,8 @@
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">
ng-switch="item.item_type"
ng-click="showCardDetail(item)">
<div as-sortable-item-handle ng-switch-when="story">
<div class="row">
<div class="col-xs-1">
@ -31,7 +32,7 @@
ng-click="removeCard(lane.worklist, item); $event.stopPropagation();">
&times;
</button>
<a href="#!/story/{{item.story.id}}">
<a>
{{item.story.title}}
</a>
</div>
@ -60,7 +61,7 @@
ng-click="removeCard(lane.worklist, item); $event.stopPropagation()">
&times;
</button>
<a href="#!/story/{{item.task.story_id}}">
<a>
{{item.task.title}}
</a>
</div>

View File

@ -17,14 +17,15 @@
<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">
ng-switch="item.item_type"
ng-click="showCardDetail(item)">
<div ng-switch-when="story">
<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}}">
<a>
{{item.story.title}}
</a>
</div>
@ -49,7 +50,7 @@
<i class="fa fa-tasks text-muted"></i>
</div>
<div class="col-xs-10">
<a href="#!/story/{{item.task.story_id}}">
<a>
{{item.task.title}}
</a>
</div>

View File

@ -0,0 +1,337 @@
<!--
~ 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.
-->
<div class="panel panel-default card-modal">
<div class="panel-heading clearfix">
<button type="button" class="close" aria-hidden="true"
ng-click="close()">&times;</button>
<h3 class="panel-title pull-left">
Card #{{card.id}} -
<span ng-if="card.item_type === 'task'">
{{card.task.title}}
</span>
<span ng-if="card.item_type === 'story'">
{{card.story.title}}
</span>
</h3>
</div>
<div ng-if="card.item_type == 'task'"
ng-include src="'/inline/task_body.html'">
</div>
<div ng-if="card.item_type == 'story'"
ng-include src="'/inline/story_body.html'">
</div>
</div>
<script type="text/ng-template" id="/inline/task_body.html">
<div class="panel-body">
<div class="row">
<div class="col-xs-12 card-heading">
<h2>
<span ng-show="!editingTitle">
<i class="fa fa-tasks"></i>
{{card.task.title}}
<small ng-show="permissions.moveCards || permissions.editBoard">
<a href ng-click="toggleEditTitle()">
<i class="fa fa-pencil"></i>
</a>
</small>
</span>
<div class="input-group" ng-show="editingTitle">
<input type="text"
class="form-control"
placeholder="Task title"
ng-model="modifications.title" />
<span class="input-group-btn">
<button class="btn btn-default"
type="button"
ng-click="editTitle()"
ng-disabled="newTitle.length < 3">
<i class="fa fa-check"></i>
<span class="hidden-xs">Save</span>
</button>
<button class="btn btn-danger"
type="button"
ng-click="toggleEditTitle()">
<i class="fa fa-times"></i>
<span class="hidden-xs">Cancel</span>
</button>
</span>
</div>
</h2>
<div class="text-muted">
Task in the story
<a href="#!/story/{{story.id}}">"{{story.title}}"</a>.
</div>
</div>
</div>
<div class="row card-detail">
<div class="col-xs-12 item-heading">
<label>
Story description
(<a href
ng-click="showDescription = !showDescription"
>{{showDescription ? 'hide' : 'show'}}</a>)
</label>
</div>
</div>
<div class="row card-detail">
<div class="col-xs-12 card-detail-item" ng-show="showDescription"
ng-click="toggleEditDescription()"
ng-class="{'editing': editingDescription}">
<div class="row">
<div class="col-xs-12">
<insert-markdown content="story.description"
ng-show="!editingDescription || previewDescription">
</insert-markdown>
<insert-markdown content="modifications.description"
ng-show="editingDescription && previewDescription">
</insert-markdown>
</div>
</div>
<div class="row" ng-show="editingDescription">
<div class="col-xs-12">
<textarea placeholder="Enter a story description here"
class="form-control context-edit"
msd-elastic
rows="3"
required
focus-on-show
ng-disabled="isUpdating"
ng-model="modifications.description"
ng-show="editingDescription"
ng-click="$event.stopPropagation()">
</textarea>
</div>
</div>
<div class="row" ng-show="editingDescription"
ng-click="$event.stopPropagation()">
<div class="col-xs-6">
<button class="btn btn-primary" type="button"
ng-click="previewDescription = !previewDescription">
Preview
</button>
</div>
<div class="col-xs-6 text-right">
<button class="btn btn-primary" type="button"
ng-click="editStoryDescription()">
Save
</button>
<button class="btn btn-default" type="button"
ng-click="toggleEditDescription()">
Cancel
</button>
</div>
</div>
</div>
</div>
<hr />
<div class="row card-detail">
<div class="col-xs-12 item-heading">
<label>Due date</label>
</div>
</div>
<div class="row card-detail">
<div class="col-xs-12 card-detail-item"
ng-class="{'editing': editingDueDate}"
ng-click="toggleEditDueDate(); toggleDueDateDropdown()">
<div class="row">
<div class="col-xs-1">
<i class="fa fa-clock-o"></i>
</div>
<div class="col-xs-11" ng-show="!editingDueDate">
<span ng-show="validDueDate(card.resolved_due_date)">
<span time-moment
eventdate="card.resolved_due_date.date"
format-string="'MMM Do, YYYY [at] H:mm'">
</span>
&nbsp;-&nbsp;
<span class="text-muted">
{{card.resolved_due_date.name}}
</span>
</span>
<span ng-show="!validDueDate(card.resolved_due_date)">
No due date
</span>
</div>
<div class="col-xs-11"
ng-show="editingDueDate"
ng-include src="'app/boards/template/card_details/edit_due_date.html'">
</div>
</div>
</div>
</div>
<hr />
<div class="row card-detail">
<div class="col-xs-12 item-heading">
<label>Assignee</label>
</div>
</div>
<div class="row card-detail">
<div class="col-xs-12 card-detail-item"
ng-class="{'editing': editingAssignee}"
ng-click="toggleAssigneeTypeahead()">
<user-typeahead ng-model="card.task.assignee_id"
enabled="isLoggedIn"
on-change="updateTask(card.task)"
empty-prompt="Click to assign someone to this task."
empty-disabled-prompt="Task is unassigned."
as-inline="true"
id="assignee"
on-blur="toggleEditAssignee()"
on-focus="toggleEditAssignee()"
/>
</div>
</div>
</div>
</script>
<script type="text/ng-template" id="/inline/story_body.html">
<div class="panel-body">
<div class="row">
<div class="col-xs-12 card-heading">
<h2>
<span ng-show="!editingTitle">
<i class="fa fa-sb-story"></i>
{{card.story.title}}
<small>
<a href ng-click="toggleEditTitle()">
<i class="fa fa-pencil"></i>
</a>
</small>
</span>
<div class="input-group" ng-show="editingTitle">
<input type="text"
class="form-control"
placeholder="Story title"
ng-model="modifications.title" />
<span class="input-group-btn">
<button class="btn btn-default"
type="button"
ng-click="editTitle()"
ng-disabled="newTitle.length < 3">
<i class="fa fa-check"></i>
<span class="hidden-xs">Save</span>
</button>
<button class="btn btn-danger"
type="button"
ng-click="toggleEditTitle()">
<i class="fa fa-times"></i>
<span class="hidden-xs">Cancel</span>
</button>
</span>
</div>
</h2>
</div>
</div>
<div class="row card-detail">
<div class="col-xs-12 item-heading">
<label>
Story description
(<a href
ng-click="showDescription = !showDescription"
>{{showDescription ? 'hide' : 'show'}}</a>)
</label>
</div>
</div>
<div class="row card-detail">
<div class="col-xs-12 card-detail-item" ng-show="showDescription"
ng-click="toggleEditDescription()"
ng-class="{'editing': editingDescription}">
<div class="row">
<div class="col-xs-12">
<insert-markdown content="story.description"
ng-show="!editingDescription">
</insert-markdown>
<insert-markdown content="modifications.description"
ng-show="editingDescription && previewDescription">
</insert-markdown>
</div>
</div>
<div class="row" ng-show="editingDescription">
<div class="col-xs-12">
<textarea placeholder="Enter a story description here"
class="form-control context-edit"
msd-elastic
rows="3"
required
focus-on-show
ng-disabled="isUpdating"
ng-model="modifications.description"
ng-show="editingDescription"
ng-click="$event.stopPropagation()">
</textarea>
</div>
</div>
<div class="row" ng-show="editingDescription"
ng-click="$event.stopPropagation()">
<div class="col-xs-6">
<button class="btn btn-primary" type="button"
ng-click="previewDescription = !previewDescription">
Preview
</button>
</div>
<div class="col-xs-6 text-right">
<button class="btn btn-primary" type="button"
ng-click="editStoryDescription()">
Save
</button>
<button class="btn btn-default" type="button"
ng-click="toggleEditDescription()">
Cancel
</button>
</div>
</div>
</div>
</div>
<hr />
<div class="row card-detail">
<div class="col-xs-12 item-heading">
<label>Due date</label>
</div>
</div>
<div class="row card-detail">
<div class="col-xs-12 card-detail-item"
ng-class="{'editing': editingDueDate}"
ng-click="toggleEditDueDate(); toggleDueDateDropdown()">
<div class="row">
<div class="col-xs-1">
<i class="fa fa-clock-o"></i>
</div>
<div class="col-xs-11" ng-show="!editingDueDate">
<span ng-show="validDueDate(card.resolved_due_date)">
<span time-moment
eventdate="card.resolved_due_date.date"
format-string="'MMM Do, YYYY [at] H:mm'">
</span>
&nbsp;-&nbsp;
<span class="text-muted">
{{card.resolved_due_date.name}}
</span>
</span>
<span ng-show="!validDueDate(card.resolved_due_date)">
No due date
</span>
</div>
<div class="col-xs-11"
ng-show="editingDueDate"
ng-include src="'app/boards/template/card_details/edit_due_date.html'">
</div>
</div>
</div>
</div>
</div>
</script>

View File

@ -0,0 +1,78 @@
<!--
~ 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.
-->
<div class="row">
<div class="col-sm-4">
<label>Displayed date:</label>&nbsp;
</div>
<div class="col-sm-8">
<span class="text-muted"
ng-show="!relevantDates.length">
No visible due dates.
</span>
<span id="due-dates-dropdown" class="dropdown" dropdown
ng-show="relevantDates.length"
ng-click="$event.stopPropagation();">
<button class="btn btn-sm btn-default dropdown-toggle"
dropdown-toggle>
<span ng-show="validDueDate(card.resolved_due_date)">
<span time-moment
eventdate="card.resolved_due_date.date"
format-string="'MMM Do, YYYY [at] H:mm'">
</span>
<br/>
<span class="text-muted">
{{card.resolved_due_date.name}}
</span>
</span>
<span ng-show="!validDueDate(card.resolved_due_date)">
No due date
</span>
&nbsp;<i class="fa fa-caret-down"></i>
</button>
<ul class="dropdown-menu">
<li ng-click="setDisplayDate(noDate)"
dropdown-toggle>
<a>
No due date
</a>
</li>
<li class="divider" role="separator"></li>
<li ng-repeat="date in relevantDates"
ng-click="setDisplayDate(date)"
dropdown-toggle>
<a>
<span time-moment
eventdate="date.date"
format-string="'MMM Do, YYYY [at] H:mm'">
</span>
<br/>
<span class="text-muted">
{{date.name}}
</span>
</a>
</li>
<li class="divider" role="separator"></li>
<li ng-click="newDueDate()">
<a>
<i class="fa fa-plus-circle"></i>
&nbsp;New due date
</a>
</li>
</ul>
</span>
</div>
</div>

View File

@ -193,6 +193,69 @@
background-color: #999;
}
/* Card modal style */
.card-modal h2 {
margin-top: 0px;
}
.card-heading {
margin-bottom: 20px;
}
.card-detail {
margin-left: 15px;
margin-right: 15px;
& div.item-heading > label {
margin-left: -10px;
}
& label {
margin-bottom: 0px;
}
};
/* TODO: declare this somewhere it can be overridden */
@card-detail-item-hover: #f6f6f6;
.card-detail-item {
border-radius: @border-radius-base;
padding: 10px 15px;
&:hover {
background-color: @card-detail-item-hover;
cursor: pointer;
}
& p:last-child {
margin-bottom: 0px;
}
& user-typeahead .form-group {
margin-bottom: 0px;
margin-top: 0px;
& .form-control-static {
padding: 0px;
}
}
&.editing {
background-color: @card-detail-item-hover;
}
& .dropdown-menu {
margin-bottom: 30px;
}
& .row {
margin-bottom: 5px;
&:last-child {
margin-bottom: 0px;
}
}
}
.due-date-modal {
& .nav-tabs {
margin-bottom: 10px;