Do not refresh the whole page when changing subtask status (work in progress)

This commit is contained in:
Frederic Guillot 2016-02-04 21:38:53 -05:00
parent 346151e103
commit 0f2b46dd6a
21 changed files with 243 additions and 254 deletions

View File

@ -22,6 +22,7 @@ New features:
Improvements:
* Do not refresh the whole page when changing subtask status (work in progress)
* Add dropdown menu with inline popup for all task actions
* Change sidebar style
* Change task summary layout

View File

@ -214,8 +214,7 @@ abstract class Base extends \Kanboard\Core\Base
$project = $this->project->getByIdWithOwner($project_id);
if (empty($project)) {
$this->flash->failure(t('Project not found.'));
$this->response->redirect($this->helper->url->to('project', 'index'));
$this->notfound();
}
return $project;
@ -242,6 +241,23 @@ abstract class Base extends \Kanboard\Core\Base
return $user;
}
/**
* Get the current subtask
*
* @access protected
* @return array
*/
protected function getSubtask()
{
$subtask = $this->subtask->getById($this->request->getIntegerParam('subtask_id'));
if (empty($subtask)) {
$this->notfound();
}
return $subtask;
}
/**
* Common method to get project filters
*

View File

@ -107,7 +107,7 @@ class Comment extends Base
public function update()
{
$task = $this->getTask();
$comment = $this->getComment();
$this->getComment();
$values = $this->request->getValues();
list($valid, $errors) = $this->commentValidator->validateModification($values);

View File

@ -2,8 +2,6 @@
namespace Kanboard\Controller;
use Kanboard\Model\Subtask as SubtaskModel;
/**
* Subtask controller
*
@ -12,23 +10,6 @@ use Kanboard\Model\Subtask as SubtaskModel;
*/
class Subtask extends Base
{
/**
* Get the current subtask
*
* @access private
* @return array
*/
private function getSubtask()
{
$subtask = $this->subtask->getById($this->request->getIntegerParam('subtask_id'));
if (empty($subtask)) {
$this->notfound();
}
return $subtask;
}
/**
* Show list of subtasks
*/
@ -181,98 +162,6 @@ class Subtask extends Base
$this->response->redirect($this->helper->url->to('task', 'show', array('project_id' => $task['project_id'], 'task_id' => $task['id'])), true);
}
/**
* Change status to the next status: Toto -> In Progress -> Done
*
* @access public
*/
public function toggleStatus()
{
$task = $this->getTask();
$subtask = $this->getSubtask();
$redirect = $this->request->getStringParam('redirect', 'task');
$this->subtask->toggleStatus($subtask['id']);
if ($redirect === 'board') {
$this->sessionStorage->hasSubtaskInProgress = $this->subtask->hasSubtaskInProgress($this->userSession->getId());
$this->response->html($this->template->render('board/tooltip_subtasks', array(
'subtasks' => $this->subtask->getAll($task['id']),
'task' => $task,
)));
}
$this->toggleRedirect($task, $redirect);
}
/**
* Handle subtask restriction (popover)
*
* @access public
*/
public function subtaskRestriction()
{
$task = $this->getTask();
$subtask = $this->getSubtask();
$this->response->html($this->template->render('subtask/restriction_change_status', array(
'status_list' => array(
SubtaskModel::STATUS_TODO => t('Todo'),
SubtaskModel::STATUS_DONE => t('Done'),
),
'subtask_inprogress' => $this->subtask->getSubtaskInProgress($this->userSession->getId()),
'subtask' => $subtask,
'task' => $task,
'redirect' => $this->request->getStringParam('redirect'),
)));
}
/**
* Change status of the in progress subtask and the other subtask
*
* @access public
*/
public function changeRestrictionStatus()
{
$task = $this->getTask();
$subtask = $this->getSubtask();
$values = $this->request->getValues();
// Change status of the previous in progress subtask
$this->subtask->update(array(
'id' => $values['id'],
'status' => $values['status'],
));
// Set the current subtask to in pogress
$this->subtask->update(array(
'id' => $subtask['id'],
'status' => SubtaskModel::STATUS_INPROGRESS,
));
$this->toggleRedirect($task, $values['redirect']);
}
/**
* Redirect to the right page
*
* @access private
*/
private function toggleRedirect(array $task, $redirect)
{
switch ($redirect) {
case 'board':
$this->response->redirect($this->helper->url->to('board', 'show', array('project_id' => $task['project_id'])));
case 'dashboard':
$this->response->redirect($this->helper->url->to('app', 'index'));
case 'subtask':
$this->response->redirect($this->helper->url->to('subtask', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])));
default:
$this->response->redirect($this->helper->url->to('task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), 'subtasks'));
}
}
/**
* Move subtask position
*

View File

@ -0,0 +1,61 @@
<?php
namespace Kanboard\Controller;
use Kanboard\Model\Subtask as SubtaskModel;
/**
* Subtask Restriction
*
* @package controller
* @author Frederic Guillot
*/
class SubtaskRestriction extends Base
{
/**
* Show popup
*
* @access public
*/
public function popover()
{
$task = $this->getTask();
$subtask = $this->getSubtask();
$this->response->html($this->template->render('subtask_restriction/popover', array(
'status_list' => array(
SubtaskModel::STATUS_TODO => t('Todo'),
SubtaskModel::STATUS_DONE => t('Done'),
),
'subtask_inprogress' => $this->subtask->getSubtaskInProgress($this->userSession->getId()),
'subtask' => $subtask,
'task' => $task,
)));
}
/**
* Change status of the in progress subtask and the other subtask
*
* @access public
*/
public function update()
{
$task = $this->getTask();
$subtask = $this->getSubtask();
$values = $this->request->getValues();
// Change status of the previous "in progress" subtask
$this->subtask->update(array(
'id' => $values['id'],
'status' => $values['status'],
));
// Set the current subtask to "in progress"
$this->subtask->update(array(
'id' => $subtask['id'],
'status' => SubtaskModel::STATUS_INPROGRESS,
));
$this->response->redirect($this->helper->url->to('task', 'show', array('project_id' => $task['project_id'], 'task_id' => $task['id'])), true);
}
}

View File

@ -0,0 +1,28 @@
<?php
namespace Kanboard\Controller;
/**
* Subtask Status
*
* @package controller
* @author Frederic Guillot
*/
class SubtaskStatus extends Base
{
/**
* Change status to the next status: Toto -> In Progress -> Done
*
* @access public
*/
public function change()
{
$task = $this->getTask();
$subtask = $this->getSubtask();
$status = $this->subtask->toggleStatus($subtask['id']);
$subtask['status'] = $status;
$this->response->html($this->helper->subtask->toggleStatus($subtask, $task['project_id']));
}
}

View File

@ -10,38 +10,40 @@ namespace Kanboard\Helper;
*/
class Subtask extends \Kanboard\Core\Base
{
public function getTitle(array $subtask)
{
if ($subtask['status'] == 0) {
$html = '<i class="fa fa-square-o fa-fw"></i>';
} elseif ($subtask['status'] == 1) {
$html = '<i class="fa fa-gears fa-fw"></i>';
} else {
$html = '<i class="fa fa-check-square-o fa-fw"></i>';
}
return $html.$this->helper->e($subtask['title']);
}
/**
* Get the link to toggle subtask status
*
* @access public
* @param array $subtask
* @param string $redirect
* @param integer $project_id
* @return string
*/
public function toggleStatus(array $subtask, $redirect, $project_id = 0)
public function toggleStatus(array $subtask, $project_id)
{
if ($project_id > 0 && ! $this->helper->user->hasProjectAccess('subtask', 'edit', $project_id)) {
return trim($this->template->render('subtask/icons', array('subtask' => $subtask))) . $this->helper->e($subtask['title']);
if (! $this->helper->user->hasProjectAccess('subtask', 'edit', $project_id)) {
return $this->getTitle($subtask);
}
$params = array('task_id' => $subtask['task_id'], 'subtask_id' => $subtask['id']);
if ($subtask['status'] == 0 && isset($this->sessionStorage->hasSubtaskInProgress) && $this->sessionStorage->hasSubtaskInProgress) {
return $this->helper->url->link(
trim($this->template->render('subtask/icons', array('subtask' => $subtask))) . $this->helper->e($subtask['title']),
'subtask',
'subtaskRestriction',
array('task_id' => $subtask['task_id'], 'subtask_id' => $subtask['id'], 'redirect' => $redirect),
false,
'popover task-board-popover'
);
return $this->helper->url->link($this->getTitle($subtask), 'SubtaskRestriction', 'popover', $params, false, 'popover');
}
return $this->helper->url->link(
trim($this->template->render('subtask/icons', array('subtask' => $subtask))) . $this->helper->e($subtask['title']),
'subtask',
'toggleStatus',
array('task_id' => $subtask['task_id'], 'subtask_id' => $subtask['id'], 'redirect' => $redirect)
);
return $this->helper->url->link($this->getTitle($subtask), 'SubtaskStatus', 'change', $params, false, 'ajax-replace');
}
public function selectTitle(array $values, array $errors = array(), array $attributes = array())

View File

@ -353,15 +353,16 @@ class Subtask extends Base
*
* @access public
* @param integer $subtask_id
* @return bool
* @return boolean|integer
*/
public function toggleStatus($subtask_id)
{
$subtask = $this->getById($subtask_id);
$status = ($subtask['status'] + 1) % 3;
$values = array(
'id' => $subtask['id'],
'status' => ($subtask['status'] + 1) % 3,
'status' => $status,
'task_id' => $subtask['task_id'],
);
@ -369,7 +370,7 @@ class Subtask extends Base
$values['user_id'] = $this->userSession->getId();
}
return $this->update($values);
return $this->update($values) ? $status : false;
}
/**

View File

@ -83,6 +83,8 @@ class AuthenticationProvider implements ServiceProviderInterface
$acl->add('ProjectEdit', '*', Role::PROJECT_MANAGER);
$acl->add('Projectuser', '*', Role::PROJECT_MANAGER);
$acl->add('Subtask', '*', Role::PROJECT_MEMBER);
$acl->add('SubtaskRestriction', '*', Role::PROJECT_MEMBER);
$acl->add('SubtaskStatus', '*', Role::PROJECT_MEMBER);
$acl->add('Swimlane', '*', Role::PROJECT_MANAGER);
$acl->add('Task', 'remove', Role::PROJECT_MEMBER);
$acl->add('Taskcreation', '*', Role::PROJECT_MEMBER);

View File

@ -24,7 +24,7 @@
<?= $this->url->link($this->e($subtask['task_name']), 'task', 'show', array('task_id' => $subtask['task_id'], 'project_id' => $subtask['project_id'])) ?>
</td>
<td>
<?= $this->subtask->toggleStatus($subtask, 'dashboard') ?>
<?= $this->subtask->toggleStatus($subtask, $subtask['project_id']) ?>
</td>
<td>
<?php if (! empty($subtask['time_spent'])): ?>

View File

@ -1,7 +1,12 @@
<section id="tooltip-subtasks">
<table class="table-stripped">
<?php foreach ($subtasks as $subtask): ?>
<?= $this->subtask->toggleStatus($subtask, 'board', $task['project_id']) ?>
<?= $this->e(empty($subtask['username']) ? '' : ' ['.$this->user->getFullname($subtask).']') ?>
<br>
<tr>
<td class="column-80">
<?= $this->subtask->toggleStatus($subtask, $task['project_id']) ?>
</td>
<td>
<?= $this->e($subtask['username'] ?: $this->user->getFullname($subtask)) ?>
</td>
</tr>
<?php endforeach ?>
</section>
</table>

View File

@ -1,7 +0,0 @@
<?php if ($subtask['status'] == 0): ?>
<i class="fa fa-square-o fa-fw"></i>
<?php elseif ($subtask['status'] == 1): ?>
<i class="fa fa-gears fa-fw"></i>
<?php else: ?>
<i class="fa fa-check-square-o fa-fw"></i>
<?php endif ?>

View File

@ -3,12 +3,12 @@
<ul>
<?php if ($subtask['position'] != $first_position): ?>
<li>
<?= $this->url->link(t('Move Up'), 'subtask', 'movePosition', array('project_id' => $project['id'], 'task_id' => $subtask['task_id'], 'subtask_id' => $subtask['id'], 'direction' => 'up', 'redirect' => $redirect), true) ?>
<?= $this->url->link(t('Move Up'), 'subtask', 'movePosition', array('project_id' => $task['id'], 'task_id' => $subtask['task_id'], 'subtask_id' => $subtask['id'], 'direction' => 'up'), true) ?>
</li>
<?php endif ?>
<?php if ($subtask['position'] != $last_position): ?>
<li>
<?= $this->url->link(t('Move Down'), 'subtask', 'movePosition', array('project_id' => $project['id'], 'task_id' => $subtask['task_id'], 'subtask_id' => $subtask['id'], 'direction' => 'down', 'redirect' => $redirect), true) ?>
<?= $this->url->link(t('Move Down'), 'subtask', 'movePosition', array('project_id' => $task['id'], 'task_id' => $subtask['task_id'], 'subtask_id' => $subtask['id'], 'direction' => 'down'), true) ?>
</li>
<?php endif ?>
<li>

View File

@ -2,82 +2,9 @@
<h2><?= t('Sub-Tasks') ?></h2>
</div>
<div id="subtasks" class="task-show-section">
<div id="subtasks">
<?php if (! empty($subtasks)): ?>
<?php $first_position = $subtasks[0]['position']; ?>
<?php $last_position = $subtasks[count($subtasks) - 1]['position']; ?>
<table class="subtasks-table">
<tr>
<th class="column-40"><?= t('Title') ?></th>
<th><?= t('Assignee') ?></th>
<th><?= t('Time tracking') ?></th>
<?php if ($editable): ?>
<th class="column-5"></th>
<?php endif ?>
</tr>
<?php foreach ($subtasks as $subtask): ?>
<tr>
<td>
<?php if ($editable): ?>
<?= $this->subtask->toggleStatus($subtask, $redirect) ?>
<?php else: ?>
<?= $this->render('subtask/icons', array('subtask' => $subtask)) . $this->e($subtask['title']) ?>
<?php endif ?>
</td>
<td>
<?php if (! empty($subtask['username'])): ?>
<?php if ($editable): ?>
<?= $this->url->link($this->e($subtask['name'] ?: $subtask['username']), 'user', 'show', array('user_id' => $subtask['user_id'])) ?>
<?php else: ?>
<?= $this->e($subtask['name'] ?: $subtask['username']) ?>
<?php endif ?>
<?php endif ?>
</td>
<td>
<ul class="no-bullet">
<li>
<?php if (! empty($subtask['time_spent'])): ?>
<strong><?= $this->e($subtask['time_spent']).'h' ?></strong> <?= t('spent') ?>
<?php endif ?>
<?php if (! empty($subtask['time_estimated'])): ?>
<strong><?= $this->e($subtask['time_estimated']).'h' ?></strong> <?= t('estimated') ?>
<?php endif ?>
</li>
<?php if ($editable && $subtask['user_id'] == $this->user->getId()): ?>
<li>
<?php if ($subtask['is_timer_started']): ?>
<i class="fa fa-pause"></i>
<?= $this->url->link(t('Stop timer'), 'timer', 'subtask', array('timer' => 'stop', 'project_id' => $task['project_id'], 'task_id' => $subtask['task_id'], 'subtask_id' => $subtask['id'])) ?>
(<?= $this->dt->age($subtask['timer_start_date']) ?>)
<?php else: ?>
<i class="fa fa-play-circle-o"></i>
<?= $this->url->link(t('Start timer'), 'timer', 'subtask', array('timer' => 'start', 'project_id' => $task['project_id'], 'task_id' => $subtask['task_id'], 'subtask_id' => $subtask['id'])) ?>
<?php endif ?>
</li>
<?php endif ?>
</ul>
</td>
<?php if ($editable): ?>
<td>
<?= $this->render('subtask/menu', array(
'project' => $project,
'task' => $task,
'subtask' => $subtask,
'redirect' => $redirect,
'first_position' => $first_position,
'last_position' => $last_position,
)) ?>
</td>
<?php endif ?>
</tr>
<?php endforeach ?>
</table>
<?php else: ?>
<p class="alert"><?= t('There is no subtask at the moment.') ?></p>
<?php endif ?>
<?= $this->render('subtask/table', array('subtasks' => $subtasks, 'task' => $task, 'editable' => $editable)) ?>
<?php if ($editable && $this->user->hasProjectAccess('subtask', 'save', $task['project_id'])): ?>
<form method="post" action="<?= $this->url->href('subtask', 'save', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>" autocomplete="off">

View File

@ -0,0 +1,69 @@
<?php if (! empty($subtasks)): ?>
<?php $first_position = $subtasks[0]['position']; ?>
<?php $last_position = $subtasks[count($subtasks) - 1]['position']; ?>
<table class="subtasks-table">
<tr>
<th class="column-40"><?= t('Title') ?></th>
<th><?= t('Assignee') ?></th>
<th><?= t('Time tracking') ?></th>
<?php if ($editable): ?>
<th class="column-5"></th>
<?php endif ?>
</tr>
<?php foreach ($subtasks as $subtask): ?>
<tr>
<td>
<?php if ($editable): ?>
<?= $this->subtask->toggleStatus($subtask, $task['project_id']) ?>
<?php else: ?>
<?= $this->subtask->getTitle($subtask) ?>
<?php endif ?>
</td>
<td>
<?php if (! empty($subtask['username'])): ?>
<?= $this->e($subtask['name'] ?: $subtask['username']) ?>
<?php endif ?>
</td>
<td>
<ul class="no-bullet">
<li>
<?php if (! empty($subtask['time_spent'])): ?>
<strong><?= $this->e($subtask['time_spent']).'h' ?></strong> <?= t('spent') ?>
<?php endif ?>
<?php if (! empty($subtask['time_estimated'])): ?>
<strong><?= $this->e($subtask['time_estimated']).'h' ?></strong> <?= t('estimated') ?>
<?php endif ?>
</li>
<?php if ($editable && $subtask['user_id'] == $this->user->getId()): ?>
<li>
<?php if ($subtask['is_timer_started']): ?>
<i class="fa fa-pause"></i>
<?= $this->url->link(t('Stop timer'), 'timer', 'subtask', array('timer' => 'stop', 'project_id' => $task['project_id'], 'task_id' => $subtask['task_id'], 'subtask_id' => $subtask['id'])) ?>
(<?= $this->dt->age($subtask['timer_start_date']) ?>)
<?php else: ?>
<i class="fa fa-play-circle-o"></i>
<?= $this->url->link(t('Start timer'), 'timer', 'subtask', array('timer' => 'start', 'project_id' => $task['project_id'], 'task_id' => $subtask['task_id'], 'subtask_id' => $subtask['id'])) ?>
<?php endif ?>
</li>
<?php endif ?>
</ul>
</td>
<?php if ($editable): ?>
<td>
<?= $this->render('subtask/menu', array(
'task' => $task,
'subtask' => $subtask,
'first_position' => $first_position,
'last_position' => $last_position,
)) ?>
</td>
<?php endif ?>
</tr>
<?php endforeach ?>
</table>
<?php else: ?>
<p class="alert"><?= t('There is no subtask at the moment.') ?></p>
<?php endif ?>

View File

@ -1,18 +1,16 @@
<div class="page-header">
<h2><?= t('You already have one subtask in progress') ?></h2>
</div>
<form action="<?= $this->url->href('subtask', 'changeRestrictionStatus', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'subtask_id' => $subtask['id'])) ?>" method="post">
<form class="popover-form" action="<?= $this->url->href('SubtaskRestriction', 'update', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'subtask_id' => $subtask['id'])) ?>" method="post">
<?= $this->form->csrf() ?>
<?= $this->form->hidden('redirect', array('redirect' => $redirect)) ?>
<p><?= t('Select the new status of the subtask: "%s"', $subtask_inprogress['title']) ?></p>
<?= $this->form->radios('status', $status_list) ?>
<?= $this->form->hidden('id', $subtask_inprogress) ?>
<div class="form-actions">
<input type="submit" value="<?= t('Save') ?>" class="btn btn-red"/>
<input type="submit" value="<?= t('Save') ?>" class="btn btn-red">
<?= t('or') ?>
<a href="#" class="close-popover"><?= t('cancel') ?></a>
</div>

View File

@ -18,15 +18,14 @@
'subtasks' => $subtasks,
'project' => $project,
'users_list' => isset($users_list) ? $users_list : array(),
'editable' => $this->user->hasProjectAccess('subtask', 'edit', $project['id']),
'redirect' => 'task',
'editable' => true,
)) ?>
<?= $this->render('tasklink/show', array(
'task' => $task,
'links' => $links,
'link_label_list' => $link_label_list,
'editable' => $this->user->hasProjectAccess('tasklink', 'edit', $project['id']),
'editable' => true,
'is_public' => false,
)) ?>

File diff suppressed because one or more lines are too long

View File

@ -41,6 +41,19 @@ App.prototype.listen = function() {
this.autoComplete();
this.datePicker();
this.focus();
$(document).on("click", ".ajax-replace", function(e) {
e.preventDefault();
var el = $(this);
$.ajax({
cache: false,
url: el.attr("href"),
success: function(data) {
el.replaceWith(data);
}
});
});
};
App.prototype.refresh = function() {

View File

@ -48,21 +48,6 @@ Tooltip.prototype.listen = function() {
var position = $(_this).tooltip("option", "position");
position.of = $(_this);
tooltip.position(position);
// Toggle subtasks status
$('#tooltip-subtasks a').not(".popover").click(function(e) {
e.preventDefault();
e.stopPropagation();
if ($(this).hasClass("popover-subtask-restriction")) {
self.app.popover.open($(this).attr('href'));
$(_this).tooltip('close');
}
else {
$.get($(this).attr('href'), setTooltipContent);
}
});
});
return '<i class="fa fa-spinner fa-spin"></i>';

View File

@ -159,7 +159,7 @@ class SubtaskTest extends Base
$this->assertEquals(0, $subtask['user_id']);
$this->assertEquals(1, $subtask['task_id']);
$this->assertTrue($s->toggleStatus(1));
$this->assertEquals(Subtask::STATUS_INPROGRESS, $s->toggleStatus(1));
$subtask = $s->getById(1);
$this->assertNotEmpty($subtask);
@ -167,7 +167,7 @@ class SubtaskTest extends Base
$this->assertEquals(0, $subtask['user_id']);
$this->assertEquals(1, $subtask['task_id']);
$this->assertTrue($s->toggleStatus(1));
$this->assertEquals(Subtask::STATUS_DONE, $s->toggleStatus(1));
$subtask = $s->getById(1);
$this->assertNotEmpty($subtask);
@ -175,7 +175,7 @@ class SubtaskTest extends Base
$this->assertEquals(0, $subtask['user_id']);
$this->assertEquals(1, $subtask['task_id']);
$this->assertTrue($s->toggleStatus(1));
$this->assertEquals(Subtask::STATUS_TODO, $s->toggleStatus(1));
$subtask = $s->getById(1);
$this->assertNotEmpty($subtask);
@ -205,7 +205,7 @@ class SubtaskTest extends Base
// Set the current logged user
$this->container['sessionStorage']->user = array('id' => 1);
$this->assertTrue($s->toggleStatus(1));
$this->assertEquals(Subtask::STATUS_INPROGRESS, $s->toggleStatus(1));
$subtask = $s->getById(1);
$this->assertNotEmpty($subtask);
@ -213,7 +213,7 @@ class SubtaskTest extends Base
$this->assertEquals(1, $subtask['user_id']);
$this->assertEquals(1, $subtask['task_id']);
$this->assertTrue($s->toggleStatus(1));
$this->assertEquals(Subtask::STATUS_DONE, $s->toggleStatus(1));
$subtask = $s->getById(1);
$this->assertNotEmpty($subtask);
@ -221,7 +221,7 @@ class SubtaskTest extends Base
$this->assertEquals(1, $subtask['user_id']);
$this->assertEquals(1, $subtask['task_id']);
$this->assertTrue($s->toggleStatus(1));
$this->assertEquals(Subtask::STATUS_TODO, $s->toggleStatus(1));
$subtask = $s->getById(1);
$this->assertNotEmpty($subtask);