Add drag and drop to change column positions
This commit is contained in:
parent
2d27c36a71
commit
c8c1242c26
|
|
@ -3,7 +3,7 @@ Version 1.0.26 (unreleased)
|
||||||
|
|
||||||
New features:
|
New features:
|
||||||
|
|
||||||
* Add subtasks drag and drop
|
* Add drag and drop to change subtasks and columns positions
|
||||||
* Add file drag and drop and asynchronous upload
|
* Add file drag and drop and asynchronous upload
|
||||||
* Enable/Disable users
|
* Enable/Disable users
|
||||||
* Add setting option to disable private projects
|
* Add setting option to disable private projects
|
||||||
|
|
|
||||||
2
Makefile
2
Makefile
|
|
@ -4,7 +4,7 @@ CSS_APP = $(addprefix assets/css/src/, $(addsuffix .css, base links title table
|
||||||
CSS_PRINT = $(addprefix assets/css/src/, $(addsuffix .css, print links table board task comment subtask markdown))
|
CSS_PRINT = $(addprefix assets/css/src/, $(addsuffix .css, print links table board task comment subtask markdown))
|
||||||
CSS_VENDOR = $(addprefix assets/css/vendor/, $(addsuffix .css, jquery-ui.min jquery-ui-timepicker-addon.min chosen.min fullcalendar.min font-awesome.min c3.min))
|
CSS_VENDOR = $(addprefix assets/css/vendor/, $(addsuffix .css, jquery-ui.min jquery-ui-timepicker-addon.min chosen.min fullcalendar.min font-awesome.min c3.min))
|
||||||
|
|
||||||
JS_APP = $(addprefix assets/js/src/, $(addsuffix .js, Popover Dropdown Tooltip Markdown Search App Screenshot FileUpload Calendar Board Swimlane Gantt Task Project Subtask TaskRepartitionChart UserRepartitionChart CumulativeFlowDiagram BurndownChart AvgTimeColumnChart TaskTimeColumnChart LeadCycleTimeChart CompareHoursColumnChart Router))
|
JS_APP = $(addprefix assets/js/src/, $(addsuffix .js, Popover Dropdown Tooltip Markdown Search App Screenshot FileUpload Calendar Board Column Swimlane Gantt Task Project Subtask TaskRepartitionChart UserRepartitionChart CumulativeFlowDiagram BurndownChart AvgTimeColumnChart TaskTimeColumnChart LeadCycleTimeChart CompareHoursColumnChart Router))
|
||||||
JS_VENDOR = $(addprefix assets/js/vendor/, $(addsuffix .js, jquery-1.11.3.min jquery-ui.min jquery-ui-timepicker-addon.min jquery.ui.touch-punch.min chosen.jquery.min moment.min fullcalendar.min mousetrap.min mousetrap-global-bind.min jquery.textcomplete))
|
JS_VENDOR = $(addprefix assets/js/vendor/, $(addsuffix .js, jquery-1.11.3.min jquery-ui.min jquery-ui-timepicker-addon.min jquery.ui.touch-punch.min chosen.jquery.min moment.min fullcalendar.min mousetrap.min mousetrap-global-bind.min jquery.textcomplete))
|
||||||
JS_LANG = $(addprefix assets/js/vendor/lang/, $(addsuffix .js, cs da de es el fi fr hu id it ja nl nb pl pt pt-br ru sv sr th tr zh-cn))
|
JS_LANG = $(addprefix assets/js/vendor/lang/, $(addsuffix .js, cs da de es el fi fr hu id it ja nl nb pl pt pt-br ru sv sr th tr zh-cn))
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -117,22 +117,21 @@ class Column extends Base
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Move a column up or down
|
* Move column position
|
||||||
*
|
*
|
||||||
* @access public
|
* @access public
|
||||||
*/
|
*/
|
||||||
public function move()
|
public function move()
|
||||||
{
|
{
|
||||||
$this->checkCSRFParam();
|
|
||||||
$project = $this->getProject();
|
$project = $this->getProject();
|
||||||
$column_id = $this->request->getIntegerParam('column_id');
|
$values = $this->request->getJson();
|
||||||
$direction = $this->request->getStringParam('direction');
|
|
||||||
|
|
||||||
if ($direction === 'up' || $direction === 'down') {
|
if (! empty($values)) {
|
||||||
$this->board->{'move'.$direction}($project['id'], $column_id);
|
$result = $this->column->changePosition($project['id'], $values['column_id'], $values['position']);
|
||||||
|
return $this->response->json(array('result' => $result));
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->response->redirect($this->helper->url->to('column', 'index', array('project_id' => $project['id'])));
|
$this->forbidden();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -174,7 +174,7 @@ class Subtask extends Base
|
||||||
|
|
||||||
if (! empty($values) && $this->helper->user->hasProjectAccess('Subtask', 'movePosition', $project_id)) {
|
if (! empty($values) && $this->helper->user->hasProjectAccess('Subtask', 'movePosition', $project_id)) {
|
||||||
$result = $this->subtask->changePosition($task_id, $values['subtask_id'], $values['position']);
|
$result = $this->subtask->changePosition($task_id, $values['subtask_id'], $values['position']);
|
||||||
$this->response->json(array('result' => $result));
|
return $this->response->json(array('result' => $result));
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->forbidden();
|
$this->forbidden();
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,64 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Kanboard\Model;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Column Model
|
||||||
|
*
|
||||||
|
* @package model
|
||||||
|
* @author Frederic Guillot
|
||||||
|
*/
|
||||||
|
class Column extends Base
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* SQL table name
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
const TABLE = 'columns';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all columns sorted by position for a given project
|
||||||
|
*
|
||||||
|
* @access public
|
||||||
|
* @param integer $project_id Project id
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function getAll($project_id)
|
||||||
|
{
|
||||||
|
return $this->db->table(self::TABLE)->eq('project_id', $project_id)->asc('position')->findAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Change column position
|
||||||
|
*
|
||||||
|
* @access public
|
||||||
|
* @param integer $project_id
|
||||||
|
* @param integer $column_id
|
||||||
|
* @param integer $position
|
||||||
|
* @return boolean
|
||||||
|
*/
|
||||||
|
public function changePosition($project_id, $column_id, $position)
|
||||||
|
{
|
||||||
|
if ($position < 1 || $position > $this->db->table(self::TABLE)->eq('project_id', $project_id)->count()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$column_ids = $this->db->table(self::TABLE)->eq('project_id', $project_id)->neq('id', $column_id)->asc('position')->findAllByColumn('id');
|
||||||
|
$offset = 1;
|
||||||
|
$results = array();
|
||||||
|
|
||||||
|
foreach ($column_ids as $current_column_id) {
|
||||||
|
if ($offset == $position) {
|
||||||
|
$offset++;
|
||||||
|
}
|
||||||
|
|
||||||
|
$results[] = $this->db->table(self::TABLE)->eq('id', $current_column_id)->update(array('position' => $offset));
|
||||||
|
$offset++;
|
||||||
|
}
|
||||||
|
|
||||||
|
$results[] = $this->db->table(self::TABLE)->eq('id', $column_id)->update(array('position' => $position));
|
||||||
|
|
||||||
|
return !in_array(false, $results, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -262,27 +262,6 @@ class Subtask extends Base
|
||||||
return $this->db->table(self::TABLE)->eq('task_id', $task_id)->update(array('status' => self::STATUS_DONE));
|
return $this->db->table(self::TABLE)->eq('task_id', $task_id)->update(array('status' => self::STATUS_DONE));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Get subtasks with consecutive positions
|
|
||||||
*
|
|
||||||
* If you remove a subtask, the positions are not anymore consecutives
|
|
||||||
*
|
|
||||||
* @access public
|
|
||||||
* @param integer $task_id
|
|
||||||
* @return array
|
|
||||||
*/
|
|
||||||
public function getNormalizedPositions($task_id)
|
|
||||||
{
|
|
||||||
$subtasks = $this->db->hashtable(self::TABLE)->eq('task_id', $task_id)->asc('position')->getAll('id', 'position');
|
|
||||||
$position = 1;
|
|
||||||
|
|
||||||
foreach ($subtasks as $subtask_id => $subtask_position) {
|
|
||||||
$subtasks[$subtask_id] = $position++;
|
|
||||||
}
|
|
||||||
|
|
||||||
return $subtasks;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Save subtask position
|
* Save subtask position
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@ class ClassProvider implements ServiceProviderInterface
|
||||||
'Board',
|
'Board',
|
||||||
'Category',
|
'Category',
|
||||||
'Color',
|
'Color',
|
||||||
|
'Column',
|
||||||
'Comment',
|
'Comment',
|
||||||
'Config',
|
'Config',
|
||||||
'Currency',
|
'Currency',
|
||||||
|
|
|
||||||
|
|
@ -8,28 +8,34 @@
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<?php if (! empty($columns)): ?>
|
<?php if (empty($columns)): ?>
|
||||||
|
<p class="alert alert-error"><?= t('Your board doesn\'t have any columns!') ?></p>
|
||||||
<?php $first_position = $columns[0]['position']; ?>
|
<?php else: ?>
|
||||||
<?php $last_position = $columns[count($columns) - 1]['position']; ?>
|
<table
|
||||||
|
class="columns-table table-stripped"
|
||||||
<h3><?= t('Change columns') ?></h3>
|
data-save-position-url="<?= $this->url->href('Column', 'move', array('project_id' => $project['id'])) ?>">
|
||||||
<table>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th class="column-70"><?= t('Column title') ?></th>
|
<th class="column-70"><?= t('Column title') ?></th>
|
||||||
<th class="column-25"><?= t('Task limit') ?></th>
|
<th class="column-25"><?= t('Task limit') ?></th>
|
||||||
<th class="column-5"><?= t('Actions') ?></th>
|
<th class="column-5"><?= t('Actions') ?></th>
|
||||||
</tr>
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
<?php foreach ($columns as $column): ?>
|
<?php foreach ($columns as $column): ?>
|
||||||
<tr>
|
<tr data-column-id="<?= $column['id'] ?>">
|
||||||
<td><?= $this->e($column['title']) ?>
|
<td>
|
||||||
<?php if (! empty($column['description'])): ?>
|
<i class="fa fa-arrows-alt draggable-row-handle" title="<?= t('Move column position') ?>"></i>
|
||||||
<span class="tooltip" title='<?= $this->e($this->text->markdown($column['description'])) ?>'>
|
<?= $this->e($column['title']) ?>
|
||||||
<i class="fa fa-info-circle"></i>
|
<?php if (! empty($column['description'])): ?>
|
||||||
</span>
|
<span class="tooltip" title='<?= $this->e($this->text->markdown($column['description'])) ?>'>
|
||||||
<?php endif ?>
|
<i class="fa fa-info-circle"></i>
|
||||||
|
</span>
|
||||||
|
<?php endif ?>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<?= $this->e($column['task_limit']) ?>
|
||||||
</td>
|
</td>
|
||||||
<td><?= $this->e($column['task_limit']) ?></td>
|
|
||||||
<td>
|
<td>
|
||||||
<div class="dropdown">
|
<div class="dropdown">
|
||||||
<a href="#" class="dropdown-menu dropdown-menu-link-icon"><i class="fa fa-cog fa-fw"></i><i class="fa fa-caret-down"></i></a>
|
<a href="#" class="dropdown-menu dropdown-menu-link-icon"><i class="fa fa-cog fa-fw"></i><i class="fa fa-caret-down"></i></a>
|
||||||
|
|
@ -37,16 +43,6 @@
|
||||||
<li>
|
<li>
|
||||||
<?= $this->url->link(t('Edit'), 'column', 'edit', array('project_id' => $project['id'], 'column_id' => $column['id']), false, 'popover') ?>
|
<?= $this->url->link(t('Edit'), 'column', 'edit', array('project_id' => $project['id'], 'column_id' => $column['id']), false, 'popover') ?>
|
||||||
</li>
|
</li>
|
||||||
<?php if ($column['position'] != $first_position): ?>
|
|
||||||
<li>
|
|
||||||
<?= $this->url->link(t('Move Up'), 'column', 'move', array('project_id' => $project['id'], 'column_id' => $column['id'], 'direction' => 'up'), true) ?>
|
|
||||||
</li>
|
|
||||||
<?php endif ?>
|
|
||||||
<?php if ($column['position'] != $last_position): ?>
|
|
||||||
<li>
|
|
||||||
<?= $this->url->link(t('Move Down'), 'column', 'move', array('project_id' => $project['id'], 'column_id' => $column['id'], 'direction' => 'down'), true) ?>
|
|
||||||
</li>
|
|
||||||
<?php endif ?>
|
|
||||||
<li>
|
<li>
|
||||||
<?= $this->url->link(t('Remove'), 'column', 'confirm', array('project_id' => $project['id'], 'column_id' => $column['id']), false, 'popover') ?>
|
<?= $this->url->link(t('Remove'), 'column', 'confirm', array('project_id' => $project['id'], 'column_id' => $column['id']), false, 'popover') ?>
|
||||||
</li>
|
</li>
|
||||||
|
|
@ -55,6 +51,6 @@
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<?php endforeach ?>
|
<?php endforeach ?>
|
||||||
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
<?php endif ?>
|
<?php endif ?>
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,4 @@
|
||||||
<?php if (! empty($subtasks)): ?>
|
<?php if (! empty($subtasks)): ?>
|
||||||
|
|
||||||
<?php $first_position = $subtasks[0]['position']; ?>
|
|
||||||
<?php $last_position = $subtasks[count($subtasks) - 1]['position']; ?>
|
|
||||||
|
|
||||||
<table
|
<table
|
||||||
class="subtasks-table table-stripped"
|
class="subtasks-table table-stripped"
|
||||||
data-save-position-url="<?= $this->url->href('Subtask', 'movePosition', array('project_id' => $task['project_id'], 'task_id' => $task['id'])) ?>"
|
data-save-position-url="<?= $this->url->href('Subtask', 'movePosition', array('project_id' => $task['project_id'], 'task_id' => $task['id'])) ?>"
|
||||||
|
|
@ -63,8 +59,6 @@
|
||||||
<?= $this->render('subtask/menu', array(
|
<?= $this->render('subtask/menu', array(
|
||||||
'task' => $task,
|
'task' => $task,
|
||||||
'subtask' => $subtask,
|
'subtask' => $subtask,
|
||||||
'first_position' => $first_position,
|
|
||||||
'last_position' => $last_position,
|
|
||||||
)) ?>
|
)) ?>
|
||||||
</td>
|
</td>
|
||||||
<?php endif ?>
|
<?php endif ?>
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -9,6 +9,7 @@ function App() {
|
||||||
this.task = new Task();
|
this.task = new Task();
|
||||||
this.project = new Project();
|
this.project = new Project();
|
||||||
this.subtask = new Subtask(this);
|
this.subtask = new Subtask(this);
|
||||||
|
this.column = new Column(this);
|
||||||
this.file = new FileUpload(this);
|
this.file = new FileUpload(this);
|
||||||
this.keyboardShortcuts();
|
this.keyboardShortcuts();
|
||||||
this.chosen();
|
this.chosen();
|
||||||
|
|
@ -40,6 +41,7 @@ App.prototype.listen = function() {
|
||||||
this.task.listen();
|
this.task.listen();
|
||||||
this.swimlane.listen();
|
this.swimlane.listen();
|
||||||
this.subtask.listen();
|
this.subtask.listen();
|
||||||
|
this.column.listen();
|
||||||
this.file.listen();
|
this.file.listen();
|
||||||
this.search.focus();
|
this.search.focus();
|
||||||
this.autoComplete();
|
this.autoComplete();
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,59 @@
|
||||||
|
function Column(app) {
|
||||||
|
this.app = app;
|
||||||
|
}
|
||||||
|
|
||||||
|
Column.prototype.listen = function() {
|
||||||
|
this.dragAndDrop();
|
||||||
|
};
|
||||||
|
|
||||||
|
Column.prototype.dragAndDrop = function() {
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
$(".draggable-row-handle").mouseenter(function() {
|
||||||
|
$(this).parent().parent().addClass("draggable-item-hover");
|
||||||
|
}).mouseleave(function() {
|
||||||
|
$(this).parent().parent().removeClass("draggable-item-hover");
|
||||||
|
});
|
||||||
|
|
||||||
|
$(".columns-table tbody").sortable({
|
||||||
|
forcePlaceholderSize: true,
|
||||||
|
handle: "td:first i",
|
||||||
|
helper: function(e, ui) {
|
||||||
|
ui.children().each(function() {
|
||||||
|
$(this).width($(this).width());
|
||||||
|
});
|
||||||
|
|
||||||
|
return ui;
|
||||||
|
},
|
||||||
|
stop: function(event, ui) {
|
||||||
|
var column = ui.item;
|
||||||
|
column.removeClass("draggable-item-selected");
|
||||||
|
self.savePosition(column.data("column-id"), column.index() + 1);
|
||||||
|
},
|
||||||
|
start: function(event, ui) {
|
||||||
|
ui.item.addClass("draggable-item-selected");
|
||||||
|
}
|
||||||
|
}).disableSelection();
|
||||||
|
};
|
||||||
|
|
||||||
|
Column.prototype.savePosition = function(columnId, position) {
|
||||||
|
var url = $(".columns-table").data("save-position-url");
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
this.app.showLoadingIcon();
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
cache: false,
|
||||||
|
url: url,
|
||||||
|
contentType: "application/json",
|
||||||
|
type: "POST",
|
||||||
|
processData: false,
|
||||||
|
data: JSON.stringify({
|
||||||
|
"column_id": columnId,
|
||||||
|
"position": position
|
||||||
|
}),
|
||||||
|
complete: function() {
|
||||||
|
self.app.hideLoadingIcon();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,68 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
require_once __DIR__.'/../Base.php';
|
||||||
|
|
||||||
|
use Kanboard\Model\Project;
|
||||||
|
use Kanboard\Model\Column;
|
||||||
|
|
||||||
|
class ColumnTest extends Base
|
||||||
|
{
|
||||||
|
public function testChangePosition()
|
||||||
|
{
|
||||||
|
$projectModel = new Project($this->container);
|
||||||
|
$columnModel = new Column($this->container);
|
||||||
|
|
||||||
|
$this->assertEquals(1, $projectModel->create(array('name' => 'test1')));
|
||||||
|
|
||||||
|
$columns = $columnModel->getAll(1);
|
||||||
|
$this->assertEquals(1, $columns[0]['position']);
|
||||||
|
$this->assertEquals(1, $columns[0]['id']);
|
||||||
|
$this->assertEquals(2, $columns[1]['position']);
|
||||||
|
$this->assertEquals(2, $columns[1]['id']);
|
||||||
|
$this->assertEquals(3, $columns[2]['position']);
|
||||||
|
$this->assertEquals(3, $columns[2]['id']);
|
||||||
|
|
||||||
|
$this->assertTrue($columnModel->changePosition(1, 3, 2));
|
||||||
|
|
||||||
|
$columns = $columnModel->getAll(1);
|
||||||
|
$this->assertEquals(1, $columns[0]['position']);
|
||||||
|
$this->assertEquals(1, $columns[0]['id']);
|
||||||
|
$this->assertEquals(2, $columns[1]['position']);
|
||||||
|
$this->assertEquals(3, $columns[1]['id']);
|
||||||
|
$this->assertEquals(3, $columns[2]['position']);
|
||||||
|
$this->assertEquals(2, $columns[2]['id']);
|
||||||
|
|
||||||
|
$this->assertTrue($columnModel->changePosition(1, 2, 1));
|
||||||
|
|
||||||
|
$columns = $columnModel->getAll(1);
|
||||||
|
$this->assertEquals(1, $columns[0]['position']);
|
||||||
|
$this->assertEquals(2, $columns[0]['id']);
|
||||||
|
$this->assertEquals(2, $columns[1]['position']);
|
||||||
|
$this->assertEquals(1, $columns[1]['id']);
|
||||||
|
$this->assertEquals(3, $columns[2]['position']);
|
||||||
|
$this->assertEquals(3, $columns[2]['id']);
|
||||||
|
|
||||||
|
$this->assertTrue($columnModel->changePosition(1, 2, 2));
|
||||||
|
|
||||||
|
$columns = $columnModel->getAll(1);
|
||||||
|
$this->assertEquals(1, $columns[0]['position']);
|
||||||
|
$this->assertEquals(1, $columns[0]['id']);
|
||||||
|
$this->assertEquals(2, $columns[1]['position']);
|
||||||
|
$this->assertEquals(2, $columns[1]['id']);
|
||||||
|
$this->assertEquals(3, $columns[2]['position']);
|
||||||
|
$this->assertEquals(3, $columns[2]['id']);
|
||||||
|
|
||||||
|
$this->assertTrue($columnModel->changePosition(1, 4, 1));
|
||||||
|
|
||||||
|
$columns = $columnModel->getAll(1);
|
||||||
|
$this->assertEquals(1, $columns[0]['position']);
|
||||||
|
$this->assertEquals(4, $columns[0]['id']);
|
||||||
|
$this->assertEquals(2, $columns[1]['position']);
|
||||||
|
$this->assertEquals(1, $columns[1]['id']);
|
||||||
|
$this->assertEquals(3, $columns[2]['position']);
|
||||||
|
$this->assertEquals(2, $columns[2]['id']);
|
||||||
|
|
||||||
|
$this->assertFalse($columnModel->changePosition(1, 2, 0));
|
||||||
|
$this->assertFalse($columnModel->changePosition(1, 2, 5));
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue