Browse Source

New backlog UI finished, now for the Taskboard.

This includes minor changes to how the dashboard widgets for types are
displayed. Previously, each widget, e.g. Projects, would filter to it's
type by the issue_types.project config value. Now, the type roles are
used to load all types with the Project role. This provides behavior
consistent with the new backlog.
Alan Hardman 3 years ago
parent
commit
d08bf6d29c
10 changed files with 1679 additions and 341 deletions
  1. 27 40
      app/controller/backlog.php
  2. 46 28
      app/helper/dashboard.php
  3. 111 104
      app/view/backlog/index.html
  4. 18 26
      css/backlog.css
  5. 2 2
      db/16.11.23.sql
  6. 1 1
      db/16.11.25.sql
  7. 6 0
      db/16.11.29.sql
  8. 19 22
      db/database.sql
  9. 40 118
      js/backlog.js
  10. 1409 0
      js/jquery.fn.sortable.js

+ 27 - 40
app/controller/backlog.php

@@ -16,7 +16,6 @@ class Backlog extends \Controller {
 	 * @param \Base $f3
 	 */
 	public function index($f3) {
-
 		$sprint_model = new \Model\Sprint();
 		$sprints = $sprint_model->find(array("end_date >= ?", $this->now(false)), array("order" => "start_date ASC"));
 
@@ -31,49 +30,41 @@ class Backlog extends \Controller {
 
 		$issue = new \Model\Issue\Detail();
 
-		$sprint_details = array();
+		$sprint_details = [];
 		foreach($sprints as $sprint) {
 			$projects = $issue->find(
 				array("deleted_date IS NULL AND sprint_id = ? AND type_id IN ($typeStr)", $sprint->id),
 				array('order' => 'priority DESC, due_date')
 			);
 
-			if(!empty($groupId)) {
-				// Add sorted projects
-				$sprintBacklog = array();
-				$sortModel = new \Model\Issue\Backlog;
-				$sortOrders = $sortModel->find(array("user_id = ? AND sprint_id = ? AND type_id IN ($typeStr)", $groupId, $sprint->id));
-				$sortArray = array();
-				if($sortOrders) {
-					$orders = array();
-					foreach($sortOrders as $order) {
-						$orders[] = json_decode($order->issues) ?: array();
-					}
-					$sortArray = \Helper\Matrix::instance()->merge($orders);
-					foreach($sortArray as $id) {
-						foreach($projects as $p) {
-							if($p->id == $id) {
-								$sprintBacklog[] = $p;
-							}
+			// Add sorted projects
+			$sprintBacklog = [];
+			$sortOrder = new \Model\Issue\Backlog;
+			$sortOrder->load(array("sprint_id = ?", $sprint->id));
+			if($sortOrder->id) {
+				$sortArray = json_decode($sortOrder->issues) ?: [];
+				$sortArray = array_unique($sortArray);
+				foreach($sortArray as $id) {
+					foreach($projects as $p) {
+						if($p->id == $id) {
+							$sprintBacklog[] = $p;
 						}
 					}
 				}
+			}
 
-				// Add remaining projects
-				foreach($projects as $p) {
-					if(!in_array($p->id, $sortArray)) {
-						$sprintBacklog[] = $p;
-					}
+			// Add remaining projects
+			foreach($projects as $p) {
+				if(!in_array($p->id, $sortArray)) {
+					$sprintBacklog[] = $p;
 				}
-			} else {
-				$sprintBacklog = $projects;
 			}
 
 			$sprint_details[] = $sprint->cast() + array("projects" => $sprintBacklog);
 		}
 
 		$large_projects = $f3->get("db.instance")->exec("SELECT i.parent_id FROM issue i JOIN issue_type t ON t.id = i.type_id WHERE i.parent_id IS NOT NULL AND t.role = 'project'");
-		$large_project_ids = array();
+		$large_project_ids = [];
 		foreach($large_projects as $p) {
 			$large_project_ids[] = $p["parent_id"];
 		}
@@ -93,16 +84,12 @@ class Backlog extends \Controller {
 		}
 
 		// Add sorted projects
-		$backlog = array();
-		$sortModel = new \Model\Issue\Backlog;
-		$sortOrders = $sortModel->find("sprint_id IS NULL");
-		$sortArray = array();
-		if($sortOrders) {
-			$orders = array();
-			foreach($sortOrders as $order) {
-				$orders[] = json_decode($order->issues) ?: array();
-			}
-			$sortArray = \Helper\Matrix::instance()->merge($orders);
+		$backlog = [];
+		$sortOrder = new \Model\Issue\Backlog;
+		$sortOrder->load(array("sprint_id IS NULL"));
+		if($sortOrder->id) {
+			$sortArray = json_decode($sortOrder->issues) ?: [];
+			$sortArray = array_unique($sortArray);
 			foreach($sortArray as $id) {
 				foreach($unset_projects as $p) {
 					if($p->id == $id) {
@@ -113,7 +100,7 @@ class Backlog extends \Controller {
 		}
 
 		// Add remaining projects
-		$unsorted = array();
+		$unsorted = [];
 		foreach($unset_projects as $p) {
 			if(!in_array($p->id, $sortArray)) {
 				$unsorted[] = $p;
@@ -152,8 +139,8 @@ class Backlog extends \Controller {
 	public function edit($f3) {
 		$post = $f3->get("POST");
 		$issue = new \Model\Issue();
-		$issue->load($post["itemId"]);
-		$issue->sprint_id = empty($post["reciever"]["receiverId"]) ? null : $post["reciever"]["receiverId"];
+		$issue->load($post["id"]);
+		$issue->sprint_id = empty($post["sprint_id"]) ? null : $post["sprint_id"];
 		$issue->save();
 		$this->_printJson($issue);
 	}

+ 46 - 28
app/helper/dashboard.php

@@ -17,13 +17,13 @@ class Dashboard extends \Prefab {
 	}
 
 	public function getOwnerIds() {
-		if($this->_ownerIds) {
+		if ($this->_ownerIds) {
 			return $this->_ownerIds;
 		}
 		$f3 = \Base::instance();
 		$this->_ownerIds = array($f3->get("user.id"));
 		$groups = new \Model\User\Group();
-		foreach($groups->find(array("user_id = ?", $f3->get("user.id"))) as $r) {
+		foreach ($groups->find(array("user_id = ?", $f3->get("user.id"))) as $r) {
 			$this->_ownerIds[] = $r->group_id;
 		}
 		return $this->_ownerIds;
@@ -32,10 +32,16 @@ class Dashboard extends \Prefab {
 	public function projects() {
 		$f3 = \Base::instance();
 		$ownerString = implode(",", $this->getOwnerIds());
+		$typeIds = [];
+		foreach ($f3->get('issue_types') as $t) {
+			if ($t->role == 'project') {
+				$typeIds[] = $t->id;
+			}
+		}
+		$typeIdStr = implode(",", $typeIds);
 		$this->_projects = $this->getIssue()->find(
 			array(
-				"owner_id IN ($ownerString) AND type_id=:type AND deleted_date IS NULL AND closed_date IS NULL AND status_closed = 0",
-				":type" => $f3->get("issue_type.project"),
+				"owner_id IN ($ownerString) AND type_id IN ($typeIdStr) AND deleted_date IS NULL AND closed_date IS NULL AND status_closed = 0",
 			),
 			array("order" => $this->_order)
 		);
@@ -43,14 +49,14 @@ class Dashboard extends \Prefab {
 	}
 
 	public function subprojects() {
-		if($this->_projects === null) {
+		if ($this->_projects === null) {
 			$this->projects();
 		}
 
 		$projects = $this->_projects;
 		$subprojects = array();
-		foreach($projects as $i=>$project) {
-			if($project->parent_id) {
+		foreach ($projects as $i=>$project) {
+			if ($project->parent_id) {
 				$subprojects[] = $project;
 				unset($projects[$i]);
 			}
@@ -62,10 +68,16 @@ class Dashboard extends \Prefab {
 	public function bugs() {
 		$f3 = \Base::instance();
 		$ownerString = implode(",", $this->getOwnerIds());
+		$typeIds = [];
+		foreach ($f3->get('issue_types') as $t) {
+			if ($t->role == 'bug') {
+				$typeIds[] = $t->id;
+			}
+		}
+		$typeIdStr = implode(",", $typeIds);
 		return $this->getIssue()->find(
 			array(
-				"owner_id IN ($ownerString) AND type_id=:type AND deleted_date IS NULL AND closed_date IS NULL AND status_closed = 0",
-				":type" => $f3->get("issue_type.bug"),
+				"owner_id IN ($ownerString) AND type_id IN ($typeIdStr) AND deleted_date IS NULL AND closed_date IS NULL AND status_closed = 0",
 			),
 			array("order" => $this->_order)
 		);
@@ -88,10 +100,16 @@ class Dashboard extends \Prefab {
 	public function tasks() {
 		$f3 = \Base::instance();
 		$ownerString = implode(",", $this->getOwnerIds());
+		$typeIds = [];
+		foreach ($f3->get('issue_types') as $t) {
+			if ($t->role == 'task') {
+				$typeIds[] = $t->id;
+			}
+		}
+		$typeIdStr = implode(",", $typeIds);
 		return $this->getIssue()->find(
 			array(
-				"owner_id IN ($ownerString) AND type_id=:type AND deleted_date IS NULL AND closed_date IS NULL AND status_closed = 0",
-				":type" => $f3->get("issue_type.task"),
+				"owner_id IN ($ownerString) AND type_id IN ($typeIdStr) AND deleted_date IS NULL AND closed_date IS NULL AND status_closed = 0",
 			),
 			array("order" => $this->_order)
 		);
@@ -109,12 +127,12 @@ class Dashboard extends \Prefab {
 		$issue = new \Model\Issue;
 		$ownerString = implode(",", $this->getOwnerIds());
 		$issues = $issue->find(array("owner_id IN ($ownerString) OR author_id = ? AND deleted_date IS NULL", $f3->get("user.id")));
-		if(!$issues) {
+		if (!$issues) {
 			return array();
 		}
 
 		$ids = array();
-		foreach($issues as $item) {
+		foreach ($issues as $item) {
 			$ids[] = $item->id;
 		}
 
@@ -129,12 +147,12 @@ class Dashboard extends \Prefab {
 		$issue = new \Model\Issue;
 		$ownerString = implode(",", $this->getOwnerIds());
 		$issues = $issue->find(array("(owner_id IN ($ownerString) OR author_id = ?) AND closed_date IS NULL AND deleted_date IS NULL", $f3->get("user.id")));
-		if(!$issues) {
+		if (!$issues) {
 			return array();
 		}
 
 		$ids = array();
-		foreach($issues as $item) {
+		foreach ($issues as $item) {
 			$ids[] = $item->id;
 		}
 
@@ -158,24 +176,24 @@ class Dashboard extends \Prefab {
 		$issues = array();
 		$assigned_ids = array();
 		$missing_ids = array();
-		foreach($assigned as $iss) {
+		foreach ($assigned as $iss) {
 			$issues[] = $iss->cast();
 			$assigned_ids[] = $iss->id;
 		}
-		foreach($issues as $iss) {
-			if($iss["parent_id"] && !in_array($iss["parent_id"], $assigned_ids)) {
+		foreach ($issues as $iss) {
+			if ($iss["parent_id"] && !in_array($iss["parent_id"], $assigned_ids)) {
 				$missing_ids[] = $iss["parent_id"];
 			}
 		}
 		while(!empty($missing_ids)) {
 			$parents = $issue->find("id IN (" . implode(",", $missing_ids) . ")");
-			foreach($parents as $iss) {
+			foreach ($parents as $iss) {
 				if (($key = array_search($iss->id, $missing_ids)) !== false) {
 					unset($missing_ids[$key]);
 				}
 				$issues[] = $iss->cast();
 				$assigned_ids[] = $iss->id;
-				if($iss->parent_id && !in_array($iss->parent_id, $assigned_ids)) {
+				if ($iss->parent_id && !in_array($iss->parent_id, $assigned_ids)) {
 					$missing_ids[] = $iss->parent_id;
 				}
 			}
@@ -190,12 +208,12 @@ class Dashboard extends \Prefab {
 		 * @var     callable $renderTree This function, required for recursive calls
 		 */
 		$renderTree = function(&$issue, $level = 0) use(&$renderTree) {
-			if(!empty($issue['id'])) {
+			if (!empty($issue['id'])) {
 				$f3 = \Base::instance();
 				$hive = array("issue" => $issue, "dict" => $f3->get("dict"), "BASE" => $f3->get("BASE"), "level" => $level, "issue_type" => $f3->get("issue_type"));
 				echo \Helper\View::instance()->render("issues/project/tree-item.html", "text/html", $hive);
-				if(!empty($issue['children'])) {
-					foreach($issue['children'] as $item) {
+				if (!empty($issue['children'])) {
+					foreach ($issue['children'] as $item) {
 						$renderTree($item, $level + 1);
 					}
 				}
@@ -216,21 +234,21 @@ class Dashboard extends \Prefab {
 		$tree = array();
 
 		// Create an associative array with each key being the ID of the item
-		foreach($array as $k => &$v) {
+		foreach ($array as $k => &$v) {
 			$tree[$v['id']] = &$v;
 		}
 
 		// Loop over the array and add each child to their parent
-		foreach($tree as $k => &$v) {
-			if(empty($v['parent_id'])) {
+		foreach ($tree as $k => &$v) {
+			if (empty($v['parent_id'])) {
 				continue;
 			}
 			$tree[$v['parent_id']]['children'][] = &$v;
 		}
 
 		// Loop over the array again and remove any items that don't have a parent of 0;
-		foreach($tree as $k => &$v) {
-			if(empty($v['parent_id'])) {
+		foreach ($tree as $k => &$v) {
+			if (empty($v['parent_id'])) {
 				continue;
 			}
 			unset($tree[$k]);

+ 111 - 104
app/view/backlog/index.html

@@ -1,127 +1,134 @@
 <!DOCTYPE html>
 <html lang="{{ @this->lang() }}">
+
 <head>
 	<include href="blocks/head.html" />
 	<link rel="stylesheet" href="{{ @BASE }}/css/backlog.css">
 	<style type="text/css">
-		.hidden-group, .hidden-type {
-			display: none;
-			visibility: hidden;
-		}
 	</style>
 </head>
-<body>
-<include href="blocks/navbar.html" />
-<div class="container">
-	<div class="row" id="backlog">
-		<div class="col-md-6">
-			 <div class="panel panel-default">
-				<div class="panel-heading has-buttons">
-					{{ @dict.backlog }}&ensp;
-					<div class="btn-group btn-group-xs">
-						<div class="btn-group">
-							<button type="button" class="btn btn-default btn-xs dropdown-toggle" data-toggle="dropdown">
-								<span class="fa fa-filter"></span>&ensp;
-								{{ @dict.groups }} <span class="caret"></span>
-							</button>
-							<ul class="dropdown-menu" role="menu">
-								<li>
-									<a href="#" data-group-id="all" data-user-ids="all">
-										{{ @dict.all_projects }}
-									</a>
-								</li>
-								<li class="active">
-									<a href="#" data-my-groups data-user-ids="{{ implode(',', @user_obj->getSharedGroupUserIds()) }}">
-										{{ @dict.my_groups }}
-									</a>
-								</li>
-								<li>
-									<a href="#" data-group-id="{{ @user.id }}" data-user-ids="{{ @user.id }}">
-										{{ @dict.my_projects }}
-									</a>
-								</li>
-								<check if="{{ count(@groups) }}">
-									<li class="divider"></li>
-									<repeat group="{{ @groups }}" value="{{ @group }}">
-										<li>
-											<a href="#" data-group-id="{{ @group->id }}" data-user-ids="{{ implode(',', @group->getGroupUserIds()) }}">
-												{{ @group.name | esc }}
-											</a>
-										</li>
-									</repeat>
-								</check>
-							</ul>
-						</div>
-						<div class="btn-group">
-							<button type="button" class="btn btn-default btn-xs dropdown-toggle" data-toggle="dropdown">
-								{{ @dict.cols.type }} <span class="caret"></span>
-							</button>
-							<ul class="dropdown-menu" role="menu">
-								<F3:repeat group="{{ @project_types }}" value="{{ @type }}">
+
+<body class="is-loading {{ @user.rank >= \Model\User::RANK_MANAGER ? 'is-sortable' : '' }}">
+	<include href="blocks/navbar.html" />
+	<div class="container">
+		<div class="row" id="backlog">
+			<div class="col-md-6">
+				<div class="panel panel-default">
+					<div class="panel-heading has-buttons">
+						{{ @dict.backlog }}&ensp;
+						<div class="btn-group btn-group-xs">
+							<div class="btn-group">
+								<button type="button" class="btn btn-default btn-xs dropdown-toggle" data-toggle="dropdown">
+									<span class="fa fa-filter"></span>&ensp; {{ @dict.groups }}
+									<span class="caret"></span>
+								</button>
+								<ul class="dropdown-menu" role="menu">
+									<li>
+										<a href="#" data-group-id="all" data-user-ids="all">
+											{{ @dict.all_projects }}
+										</a>
+									</li>
 									<li class="active">
-										<a href="#" data-type-id="{{ @type.id }}">
-											{{ isset(@dict[@type.name]) ? @dict[@type.name] : str_replace('_', ' ', @type.name) }}
+										<a href="#" data-my-groups data-user-ids="{{ implode(',', @user_obj->getSharedGroupUserIds()) }}">
+											{{ @dict.my_groups }}
 										</a>
 									</li>
-								</F3:repeat>
-							</ul>
-						</div>
-					</div>
-					<a href="{{ @BASE }}/issues/new/{{ @issue_type.project }}" class="btn btn-default btn-xs pull-right">
-						<span class="fa fa-plus"></span> {{ @dict.add_project }}
-					</a>
-				</div>
-				<div class="panel-body in" id="panel-0">
-					<ul class="list-group sortable" data-list-id="0">
-						<repeat group="{{ @backlog }}" value="{{ @project }}">
-							<include href="backlog/item.html" />
-						</repeat>
-					</ul>
-				</div>
-			</div>
-		</div>
-		<div class="col-md-6">
-			<ul class="nav nav-tabs">
-				<li class="active"><a href="#tab-sprints" data-toggle="tab">{{ @dict.sprints }}</a></li>
-				<li><a href="#tab-unsorted" data-toggle="tab">{{ @dict.unsorted_items }}</a></li>
-			</ul>
-			<div class="tab-content">
-				<div class="tab-pane active" id="tab-sprints">
-					<repeat group="{{ @sprints }}" key="{{ @key }}" value="{{ @sprint }}">
-						<div class="panel panel-default">
-							<div class="panel-heading has-buttons">
-								<a class="{{ @key ? 'collapsed' : '' }}" data-toggle="collapse" href="#panel-{{ @sprint.id }}">{{ @sprint.name }} {{ date('n/j', strtotime(@sprint.start_date)) }}-{{ date('n/j', strtotime(@sprint.end_date)) }}</a>
-								<a href="{{ @BASE }}/taskboard/{{ @sprint.id }}/{{ @@@groupid ?: @filter }}" class="btn btn-default btn-xs pull-right">
-									<span class="fa fa-list"></span> {{ @dict.taskboard }}
-								</a>
+									<li>
+										<a href="#" data-group-id="{{ @user.id }}" data-user-ids="{{ @user.id }}">
+											{{ @dict.my_projects }}
+										</a>
+									</li>
+									<check if="{{ count(@groups) }}">
+										<li class="divider"></li>
+										<repeat group="{{ @groups }}" value="{{ @group }}">
+											<li>
+												<a href="#" data-group-id="{{ @group->id }}" data-user-ids="{{ implode(',', @group->getGroupUserIds()) }}">
+													{{ @group.name | esc }}
+												</a>
+											</li>
+										</repeat>
+									</check>
+								</ul>
 							</div>
-							<div class="panel-body {{ @key ? 'collapse' : 'in' }}" id="panel-{{ @sprint.id }}">
-								<ul class="list-group sortable" data-list-id="{{ @sprint.id }}">
-									<repeat group="{{ @sprint.projects }}" value="{{ @project }}">
-										<include href="backlog/item.html" />
-									</repeat>
+							<div class="btn-group">
+								<button type="button" class="btn btn-default btn-xs dropdown-toggle" data-toggle="dropdown">
+									{{ @dict.cols.type }}
+									<span class="caret"></span>
+								</button>
+								<ul class="dropdown-menu" role="menu">
+									<F3:repeat group="{{ @project_types }}" value="{{ @type }}">
+										<li class="active">
+											<a href="#" data-type-id="{{ @type.id }}">
+												{{ isset(@dict[@type.name]) ? @dict[@type.name] : str_replace('_', ' ', @type.name) }}
+											</a>
+										</li>
+									</F3:repeat>
 								</ul>
 							</div>
 						</div>
-					</repeat>
-					<p class="text-center">
-						<a href="{{ @BASE }}/backlog/old">{{ @dict.show_previous_sprints }}</a>
-					</p>
+						<a href="{{ @BASE }}/issues/new/{{ @issue_type.project }}" class="btn btn-default btn-xs pull-right">
+							<span class="fa fa-plus"></span> {{ @dict.add_project }}
+						</a>
+					</div>
+					<div class="panel-body in" id="panel-0">
+						<ul class="list-group sortable" data-list-id="0">
+							<repeat group="{{ @backlog }}" value="{{ @project }}">
+								<include href="backlog/item.html" />
+							</repeat>
+						</ul>
+					</div>
 				</div>
-				<div class="tab-pane" id="tab-unsorted">
-					<ul class="list-group sortable">
-						<repeat group="{{ @unsorted }}" value="{{ @project }}" data-list-id="0">
-							<include href="backlog/item.html" />
+			</div>
+			<div class="col-md-6">
+				<ul class="nav nav-tabs">
+					<li class="active">
+						<a href="#tab-sprints" data-toggle="tab">{{ @dict.sprints }}</a>
+					</li>
+					<li>
+						<a href="#tab-unsorted" data-toggle="tab">{{ @dict.unsorted_items }}</a>
+					</li>
+				</ul>
+				<div class="tab-content">
+					<div class="tab-pane active" id="tab-sprints">
+						<repeat group="{{ @sprints }}" key="{{ @key }}" value="{{ @sprint }}">
+							<div class="panel panel-default">
+								<div class="panel-heading has-buttons">
+									<a class="{{ @key ? 'collapsed' : '' }}" data-toggle="collapse" href="#panel-{{ @sprint.id }}">
+										{{ @sprint.name }} &mdash; {{ date('n/j', strtotime(@sprint.start_date)) }}-{{ date('n/j', strtotime(@sprint.end_date)) }}
+									</a>
+									<a href="{{ @BASE }}/taskboard/{{ @sprint.id }}/{{ @@@groupid ?: @filter }}" class="btn btn-default btn-xs pull-right">
+										<span class="fa fa-list"></span> {{ @dict.taskboard }}
+									</a>
+								</div>
+								<div class="panel-body {{ @key ? 'collapse' : 'in' }}" id="panel-{{ @sprint.id }}">
+									<ul class="list-group sortable" data-list-id="{{ @sprint.id }}">
+										<repeat group="{{ @sprint.projects }}" value="{{ @project }}">
+											<include href="backlog/item.html" />
+										</repeat>
+									</ul>
+								</div>
+							</div>
 						</repeat>
-					</ul>
+						<p class="text-center">
+							<a href="{{ @BASE }}/backlog/old">{{ @dict.show_previous_sprints }}</a>
+						</p>
+					</div>
+					<div class="tab-pane" id="tab-unsorted">
+						<ul class="list-group sortable">
+							<repeat group="{{ @unsorted }}" value="{{ @project }}" data-list-id="0">
+								<include href="backlog/item.html" />
+							</repeat>
+						</ul>
+					</div>
 				</div>
 			</div>
 		</div>
+		<include href="blocks/footer.html" />
+		<check if="{{ @user.rank >= \Model\User::RANK_MANAGER }}">
+			<script type="text/javascript">var sortBacklog = true;</script>
+		</check>
+		<script src="{{ @BASE }}/js/jquery.fn.sortable.js"></script>
+		<script src="{{ @BASE }}/minify/js/backlog.js"></script>
 	</div>
-	<include href="blocks/footer.html" />
-	<script src="{{ @BASE }}/js/jquery-ui-dragsort.min.js"></script>
-	<script src="{{ @BASE }}/js/jquery.ui.touch-punch.min.js"></script>
-	<script src="{{ @BASE }}/minify/js/backlog.js"></script>
-</div>
 </body>
 </html>

+ 18 - 26
css/backlog.css

@@ -1,16 +1,30 @@
-#backlog .list-group {
+body.is-loading #backlog .list-group.sortable {
+	opacity: 0;
+}
+#backlog .list-group.sortable {
 	min-height: 80px;
+	transition: all 0.3s ease;
 	margin: 0;
 }
 #backlog .list-group-item {
 	-webkit-box-sizing: content-box;
 	-moz-box-sizing: content-box;
 	box-sizing: content-box;
-	cursor: move;
 	position: relative;
 	padding: 6px 6px 6px 14px;
 }
-#backlog .list-group-item:hover {
+body.is-sortable #backlog .list-group-item {
+	cursor: move;
+}
+#backlog .hidden-group,
+#backlog .hidden-type {
+	display: none;
+	visibility: hidden;
+}
+#backlog .list-group-item.placeholder {
+	opacity: 0.5;
+}
+body.is-sortable #backlog .list-group-item:hover {
 	background-image: url(../img/backlog/i_has_grip.png);
 	background-position: 2px center;
 	background-repeat: no-repeat;
@@ -18,30 +32,8 @@
 #backlog .list-group-item.completed {
 	text-decoration: line-through;
 }
-#backlog .spinner {
-	position: absolute;
-	left: 0;
-	right: 0;
-	top: 0;
-	bottom: 0;
-	z-index: 50;
-	background-image: url(../img/ajax-loader.gif);
-	background-position: center center;
-	background-repeat: no-repeat
-}
-#backlog .error {
-	position: absolute;
-	left: 0;
-	right: 0;
-	top: 0;
-	bottom: 0;
-	z-index: 50;
-	background-image: url(../img/taskboard/warning.png);
-	background-position: right bottom;
-	background-repeat: no-repeat
-}
 #backlog .panel-heading {
-	position: relative
+	position: relative;
 }
 #backlog .panel-heading > a:first-child {
 	cursor: pointer;

+ 2 - 2
db/16.11.23.sql

@@ -1,6 +1,6 @@
 # Update issue_type to support roles
 ALTER TABLE `issue_type`
-  ADD COLUMN `role` ENUM('task','project','bug') DEFAULT 'task' NOT NULL
+  ADD COLUMN `role` ENUM('task','project','bug') DEFAULT 'task' NOT NULL,
   ADD INDEX `issue_type_role` (`role`);
 
 UPDATE issue_type
@@ -11,4 +11,4 @@ UPDATE issue_type
 JOIN config ON config.value = issue_type.id AND config.attribute = 'issue_type.bug'
 SET issue_type.role = 'bug';
 
-UPDATE `config` SET `value` = '16.19.23' WHERE `attribute` = 'version';
+UPDATE `config` SET `value` = '16.11.23' WHERE `attribute` = 'version';

+ 1 - 1
db/16.11.25.sql

@@ -21,4 +21,4 @@ SELECT * FROM issue_backlog_converting;
 
 DROP TEMPORARY TABLE issue_backlog_converting;
 
-UPDATE `config` SET `value` = '16.19.25' WHERE `attribute` = 'version';
+UPDATE `config` SET `value` = '16.11.25' WHERE `attribute` = 'version';

+ 6 - 0
db/16.11.29.sql

@@ -0,0 +1,6 @@
+# New backlog uses only one row per sprint
+ALTER TABLE issue_backlog
+  DROP INDEX issue_backlog_sprint_id,
+  ADD UNIQUE INDEX issue_backlog_sprint_id (sprint_id);
+
+UPDATE `config` SET `value` = '16.11.29' WHERE `attribute` = 'version';

+ 19 - 22
db/database.sql

@@ -73,16 +73,11 @@ CREATE TABLE `issue` (
 DROP TABLE IF EXISTS `issue_backlog`;
 CREATE TABLE `issue_backlog` (
 	`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
-	`user_id` int(10) unsigned NOT NULL,
-	`type_id` INT(10) unsigned NOT NULL,
 	`sprint_id` int(10) unsigned DEFAULT NULL,
 	`issues` blob NOT NULL,
 	PRIMARY KEY (`id`),
-	KEY `issue_backlog_user_id` (`user_id`),
-	KEY `issue_backlog_sprint_id` (`sprint_id`),
-	CONSTRAINT `issue_backlog_sprint_id` FOREIGN KEY (`sprint_id`) REFERENCES `sprint` (`id`) ON DELETE CASCADE ON UPDATE CASCADE,
-	CONSTRAINT `issue_backlog_user_id` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) ON DELETE CASCADE ON UPDATE CASCADE,
-	CONSTRAINT `issue_backlog_type_id` FOREIGN KEY (`type_id`) REFERENCES `issue_type`(`id`) ON UPDATE CASCADE ON DELETE CASCADE
+	UNIQUE KEY `issue_backlog_sprint_id` (`sprint_id`),
+	CONSTRAINT `issue_backlog_sprint_id` FOREIGN KEY (`sprint_id`) REFERENCES `sprint` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
 
 DROP TABLE IF EXISTS `issue_comment`;
@@ -153,13 +148,15 @@ DROP TABLE IF EXISTS `issue_type`;
 CREATE TABLE `issue_type` (
 	`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
 	`name` varchar(32) NOT NULL,
-	PRIMARY KEY (`id`)
+	`role` ENUM('task','project','bug') DEFAULT 'task' NOT NULL,
+	PRIMARY KEY (`id`),
+	UNIQUE KEY issue_backlog_sprint_id (sprint_id)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
 
-INSERT INTO `issue_type` (`id`, `name`) VALUES
-(1, 'Task'),
-(2, 'Project'),
-(3, 'Bug');
+INSERT INTO `issue_type` (`id`, `name`, `role`) VALUES
+(1, 'Task', 'task'),
+(2, 'Project', 'project'),
+(3, 'Bug', 'bug');
 
 DROP TABLE IF EXISTS `issue_update`;
 CREATE TABLE `issue_update` (
@@ -208,15 +205,15 @@ CREATE TABLE `issue_tag`(
 
 DROP TABLE IF EXISTS `issue_dependency`;
 CREATE TABLE `issue_dependency` (
-	  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
-	  `issue_id` int(10) unsigned NOT NULL,
-	  `dependency_id` int(11) unsigned NOT NULL,
-	  `dependency_type` char(2) COLLATE utf8_unicode_ci NOT NULL,
-	  PRIMARY KEY (`id`),
-	  UNIQUE KEY `issue_id_dependency_id` (`issue_id`,`dependency_id`),
-	  KEY `dependency_id` (`dependency_id`),
-	  CONSTRAINT `issue_dependency_ibfk_2` FOREIGN KEY (`issue_id`) REFERENCES `issue` (`id`) ON DELETE CASCADE ON UPDATE CASCADE,
-	  CONSTRAINT `issue_dependency_ibfk_3` FOREIGN KEY (`dependency_id`) REFERENCES `issue` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
+	`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
+	`issue_id` int(10) unsigned NOT NULL,
+	`dependency_id` int(11) unsigned NOT NULL,
+	`dependency_type` char(2) COLLATE utf8_unicode_ci NOT NULL,
+	PRIMARY KEY (`id`),
+	UNIQUE KEY `issue_id_dependency_id` (`issue_id`,`dependency_id`),
+	KEY `dependency_id` (`dependency_id`),
+	CONSTRAINT `issue_dependency_ibfk_2` FOREIGN KEY (`issue_id`) REFERENCES `issue` (`id`) ON DELETE CASCADE ON UPDATE CASCADE,
+	CONSTRAINT `issue_dependency_ibfk_3` FOREIGN KEY (`dependency_id`) REFERENCES `issue` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
 
 DROP TABLE IF EXISTS `sprint`;
@@ -305,4 +302,4 @@ CREATE TABLE `config` (
 	UNIQUE KEY `attribute` (`attribute`)
 );
 
-INSERT INTO `config` (`attribute`, `value`) VALUES ('version', '16.09.12');
+INSERT INTO `config` (`attribute`, `value`) VALUES ('version', '16.11.29');

+ 40 - 118
js/backlog.js

@@ -20,10 +20,29 @@ function getQueryVariable(variable) {
 }
 
 var Backlog = {
-	updateUrl: BASE + '/backlog/edit',
-	projectReceived: 0,
 	init: function() {
-		Backlog.makeSortable('.sortable');
+		// Initialize sorting
+		if (window.sortBacklog) {
+			$('.sortable').sortable({
+				group: 'backlog',
+				ghostClass: 'placeholder',
+				filter: '.hidden-group,.hidden-type',
+				onAdd: function(event) {
+					var $item = $(event.item);
+					$.post(BASE + '/backlog/edit', {
+						id: $item.attr('data-id'),
+						sprint_id: $item.parents('.list-group').attr('data-list-id')
+					}).fail(function() {
+						console.error('Failed to save new sprint assignment');
+					});
+				},
+				onSort: function(event) {
+					Backlog.saveSortOrder(event.target);
+				}
+			});
+		}
+
+		// Open issue in new window on double-click
 		$('.sortable').on('dblclick', 'li', function() {
 			window.open(BASE + '/issues/' + $(this).data('id'));
 		});
@@ -45,7 +64,7 @@ var Backlog = {
 				});
 			}
 
-			Backlog.replaceUrl();
+			Backlog.updateUrl();
 			e.preventDefault();
 		});
 
@@ -55,7 +74,7 @@ var Backlog = {
 				typeId = $this.attr('data-type-id');
 			$this.parents('li').toggleClass('active');
 			$('.list-group-item[data-type-id=' + typeId + ']').toggleClass('hidden-type');
-			Backlog.replaceUrl();
+			Backlog.updateUrl();
 			e.preventDefault();
 		});
 
@@ -75,8 +94,11 @@ var Backlog = {
 				$('.list-group-item[data-type-id=' + val + ']').removeClass('hidden-type');
 			});
 		}
+
+		// Un-hide backlog
+		$('body').removeClass('is-loading');
 	},
-	replaceUrl: function() {
+	updateUrl: function() {
 		if (window.history && history.replaceState) {
 			var state = {};
 			state.groupId = $('.dropdown-menu .active a[data-user-ids]').attr('data-group-id');
@@ -102,124 +124,24 @@ var Backlog = {
 			history.replaceState(state, '', BASE + path);
 		}
 	},
-	makeSortable: function(selector) {
-		$(selector).sortable({
-			items: 'li:not(.unsortable)',
-			connectWith: '.sortable',
-			start: function(event, ui) {
-				// Fade out non-matching types
-				/*if($(ui.item).attr('data-type-id')) {
-					$('.sortable .list-group-item')
-						.filter(':not([data-type-id="' + $(ui.item).attr('data-type-id') + '"])')
-						.fadeTo(200, 0.25);
-				}*/
-			},
-			receive: function(event, ui) {
-				Backlog.projectReceive($(ui.item), $(ui.sender));
-				Backlog.projectReceived = true; // keep from repeating if changed lists
-			},
-			stop: function(event, ui) {
-				// Fade in all items
-				/*$('.sortable .list-group-item').fadeTo(150, 1);
-				if (Backlog.projectReceived !== true) {
-					Backlog.sameReceive($(ui.item));
-				} else {
-					Backlog.projectReceived = false;
-				}*/
-			}
-		}).disableSelection();
-	},
-	projectReceive: function(item, sender) {
-		var itemId = $(item).attr('data-id'),
-			receiverId = $(item).parent().attr('data-list-id'),
-			senderId = $(sender).attr('data-list-id');
-		if ($(item).parent().attr('data-list-id') !== undefined) {
-			var data = {
-					itemId: itemId,
-					sender: {
-						senderId: senderId
-					},
-					reciever: {
-						receiverId: receiverId
-					}
-				};
+	saveSortOrder: function(element) {
+		var $el = $(element),
+			items = [];
 
-			Backlog.ajaxUpdateBacklog(data, item);
-			Backlog.saveSortOrder([sender, $(item).parents('.sortable')]);
+		if ($el.attr('data-list-id') === undefined) {
+			return;
 		}
-	},
-	sameReceive: function(item) {
-		var itemId = $(item).attr('data-id'),
-			receiverId = $(item).parent().attr('data-list-id'),
-			data = {
-				itemId: itemId,
-				reciever: {
-					receiverId: receiverId
-				}
-			};
 
-		Backlog.ajaxUpdateBacklog(data, item);
-		Backlog.saveSortOrder([$(item).parents('.sortable')]);
-	},
-	ajaxUpdateBacklog: function(data, item) {
-		var projectId = data.itemId;
-		Backlog.block(projectId, item);
-		$.ajax({
-			type: 'POST',
-			url: Backlog.updateUrl,
-			data: data,
-			success: function() {
-				Backlog.unBlock(projectId, item);
-			},
-			error: function() {
-				Backlog.unBlock(projectId, item);
-				Backlog.showError(projectId, item);
-			}
+		$el.find('.list-group-item').each(function() {
+			items.push(parseInt($(this).attr('data-id')));
 		});
-	},
-	saveSortOrder: function(elements) {
-		console.log(elements);
-
-		$(elements).each(function() {
-			var $el = $(this),
-				items = [];
-
-			if ($el.attr('data-list-id') === undefined) {
-				return;
-			}
-
-			$el.find('.list-group-item').each(function() {
-				items.push(parseInt($(this).attr('data-id')));
-			});
 
-			$.post(BASE + '/backlog/sort', {
-				sprint_id: $el.attr('data-list-id'),
-				items: JSON.stringify(items)
-			}).error(function() {
-				console.error('An error occurred saving the sort order.');
-			});
-		});
-	},
-	block: function(projectId, item) {
-		var project = $('#project_' + projectId);
-		project.append('<div class="spinner"></div>');
-		item.addClass('unsortable');
-		Backlog.makeSortable('.sortable'); //redo this so it is disabled
-	},
-	unBlock: function(projectId, item) {
-		var project = $('#project_' + projectId);
-		project.find('.spinner').remove();
-		item.removeClass('unsortable');
-		Backlog.makeSortable('.sortable'); //redo this so it is disabled
-	},
-	showError: function(projectId, item) {
-		var project = $('#project_' + projectId);
-		project.css({
-			'opacity': '.8'
+		$.post(BASE + '/backlog/sort', {
+			sprint_id: $el.attr('data-list-id'),
+			items: JSON.stringify(items)
+		}).fail(function() {
+			console.error('An error occurred saving the sort order.');
 		});
-		project.append('<div class="error" title="An error occured while saving the task!"></div>');
-		item.addClass('unsortable');
-		Backlog.makeSortable('.sortable'); //redo this so it is disabled
 	}
 };
 

File diff suppressed because it is too large
+ 1409 - 0
js/jquery.fn.sortable.js