Add the possibility to move tasks without drag and drop
This commit is contained in:
@@ -3,14 +3,16 @@ Version 1.0.33 (unreleased)
|
|||||||
|
|
||||||
New features:
|
New features:
|
||||||
|
|
||||||
|
* Move a task without drag and drop (mobiles)
|
||||||
* Add the possibility to unlock users from the user interface
|
* Add the possibility to unlock users from the user interface
|
||||||
* New API calls for task metadata
|
* New API calls for task metadata
|
||||||
* New automatic actions:
|
* New automatic actions:
|
||||||
- Define colour by Swimlane
|
- Define color by Swimlane
|
||||||
- Define priority by Swimlane
|
- Define priority by Swimlane
|
||||||
|
|
||||||
Improvements:
|
Improvements:
|
||||||
|
|
||||||
|
* Introduce Vue.js to manage user interface components
|
||||||
* Show both time spent and estimated on the board
|
* Show both time spent and estimated on the board
|
||||||
* Store board collapsed mode user preference in the database
|
* Store board collapsed mode user preference in the database
|
||||||
* Store comment sorting direction in the database
|
* Store comment sorting direction in the database
|
||||||
|
|||||||
43
app/Controller/TaskMovePositionController.php
Normal file
43
app/Controller/TaskMovePositionController.php
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Kanboard\Controller;
|
||||||
|
|
||||||
|
use Kanboard\Formatter\BoardFormatter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class TaskMovePositionController
|
||||||
|
*
|
||||||
|
* @package Kanboard\Controller
|
||||||
|
* @author Frederic Guillot
|
||||||
|
*/
|
||||||
|
class TaskMovePositionController extends BaseController
|
||||||
|
{
|
||||||
|
public function show()
|
||||||
|
{
|
||||||
|
$task = $this->getTask();
|
||||||
|
|
||||||
|
$this->response->html($this->template->render('task_move_position/show', array(
|
||||||
|
'task' => $task,
|
||||||
|
'board' => BoardFormatter::getInstance($this->container)
|
||||||
|
->withProjectId($task['project_id'])
|
||||||
|
->withQuery($this->taskFinderModel->getExtendedQuery())
|
||||||
|
->format()
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function save()
|
||||||
|
{
|
||||||
|
$task = $this->getTask();
|
||||||
|
$values = $this->request->getJson();
|
||||||
|
|
||||||
|
$result = $this->taskPositionModel->movePosition(
|
||||||
|
$task['project_id'],
|
||||||
|
$task['id'],
|
||||||
|
$values['column_id'],
|
||||||
|
$values['position'],
|
||||||
|
$values['swimlane_id']
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->response->json(array('result' => $result));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -78,6 +78,7 @@ class BoardColumnFormatter extends BaseFormatter implements FormatterInterface
|
|||||||
public function format()
|
public function format()
|
||||||
{
|
{
|
||||||
foreach ($this->columns as &$column) {
|
foreach ($this->columns as &$column) {
|
||||||
|
$column['id'] = (int) $column['id'];
|
||||||
$column['tasks'] = BoardTaskFormatter::getInstance($this->container)
|
$column['tasks'] = BoardTaskFormatter::getInstance($this->container)
|
||||||
->withTasks($this->tasks)
|
->withTasks($this->tasks)
|
||||||
->withTags($this->tags)
|
->withTags($this->tags)
|
||||||
|
|||||||
@@ -81,6 +81,7 @@ class BoardSwimlaneFormatter extends BaseFormatter implements FormatterInterface
|
|||||||
$nb_columns = count($this->columns);
|
$nb_columns = count($this->columns);
|
||||||
|
|
||||||
foreach ($this->swimlanes as &$swimlane) {
|
foreach ($this->swimlanes as &$swimlane) {
|
||||||
|
$swimlane['id'] = (int) $swimlane['id'];
|
||||||
$swimlane['columns'] = BoardColumnFormatter::getInstance($this->container)
|
$swimlane['columns'] = BoardColumnFormatter::getInstance($this->container)
|
||||||
->withSwimlaneId($swimlane['id'])
|
->withSwimlaneId($swimlane['id'])
|
||||||
->withColumns($this->columns)
|
->withColumns($this->columns)
|
||||||
|
|||||||
@@ -78,15 +78,21 @@
|
|||||||
<i class="fa fa-clone fa-fw"></i>
|
<i class="fa fa-clone fa-fw"></i>
|
||||||
<?= $this->url->link(t('Move to another project'), 'TaskDuplicationController', 'move', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'popover') ?>
|
<?= $this->url->link(t('Move to another project'), 'TaskDuplicationController', 'move', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'popover') ?>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<?php if ($task['is_active'] == 1): ?>
|
||||||
<?php if ($task['is_active'] == 1): ?>
|
<li>
|
||||||
|
<i class="fa fa-arrows fa-fw"></i>
|
||||||
|
<?= $this->url->link(t('Move position'), 'TaskMovePositionController', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'popover') ?>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
<i class="fa fa-times fa-fw"></i>
|
<i class="fa fa-times fa-fw"></i>
|
||||||
<?= $this->url->link(t('Close this task'), 'TaskStatusController', 'close', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'popover') ?>
|
<?= $this->url->link(t('Close this task'), 'TaskStatusController', 'close', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'popover') ?>
|
||||||
<?php else: ?>
|
</li>
|
||||||
|
<?php else: ?>
|
||||||
|
<li>
|
||||||
<i class="fa fa-check-square-o fa-fw"></i>
|
<i class="fa fa-check-square-o fa-fw"></i>
|
||||||
<?= $this->url->link(t('Open this task'), 'TaskStatusController', 'open', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'popover') ?>
|
<?= $this->url->link(t('Open this task'), 'TaskStatusController', 'open', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'popover') ?>
|
||||||
<?php endif ?>
|
</li>
|
||||||
</li>
|
<?php endif ?>
|
||||||
<?php if ($this->user->canRemoveTask($task)): ?>
|
<?php if ($this->user->canRemoveTask($task)): ?>
|
||||||
<li>
|
<li>
|
||||||
<i class="fa fa-trash-o fa-fw"></i>
|
<i class="fa fa-trash-o fa-fw"></i>
|
||||||
|
|||||||
43
app/Template/task_move_position/show.php
Normal file
43
app/Template/task_move_position/show.php
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
<div class="page-header">
|
||||||
|
<h2><?= t('Move task to another position on the board') ?></h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script type="x/templates" id="template-task-move-position">
|
||||||
|
<?= $this->form->label(t('Swimlane'), 'swimlane') ?>
|
||||||
|
<select v-model="swimlaneId" @change="onChangeSwimlane()" id="form-swimlane">
|
||||||
|
<option v-for="swimlane in board" v-bind:value="swimlane.id">
|
||||||
|
{{ swimlane.name }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<div v-if="columns.length > 0">
|
||||||
|
<?= $this->form->label(t('Column'), 'column') ?>
|
||||||
|
<select v-model="columnId" @change="onChangeColumn()" id="form-column">
|
||||||
|
<option v-for="column in columns" v-bind:value="column.id">
|
||||||
|
{{ column.title }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="tasks.length > 0">
|
||||||
|
<?= $this->form->label(t('Position'), 'position') ?>
|
||||||
|
<select v-model="position" id="form-position">
|
||||||
|
<option v-for="task in tasks" v-bind:value="task.position">
|
||||||
|
#{{ task.id }} - {{ task.title }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
<label><input type="radio" value="before" v-model="positionChoice"><?= t('Insert before this task') ?></label>
|
||||||
|
<label><input type="radio" value="after" v-model="positionChoice"><?= t('Insert after this task') ?></label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-actions">
|
||||||
|
<input type="button" value="<?= t('Save') ?>" class="btn btn-blue" @click="onSubmit">
|
||||||
|
<?= t('or') ?>
|
||||||
|
<?= $this->url->link(t('cancel'), 'TaskViewController', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'close-popover') ?>
|
||||||
|
</div>
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<task-move-position
|
||||||
|
save-url="<?= $this->url->href('TaskMovePositionController', 'save', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>"
|
||||||
|
:board='<?= json_encode($board, JSON_HEX_APOS) ?>'
|
||||||
|
></task-move-position>
|
||||||
4
assets/js/app.min.js
vendored
4
assets/js/app.min.js
vendored
File diff suppressed because one or more lines are too long
70
assets/js/components/task-move-position.js
Normal file
70
assets/js/components/task-move-position.js
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
Vue.component('task-move-position', {
|
||||||
|
props: ['board', 'saveUrl'],
|
||||||
|
template: '#template-task-move-position',
|
||||||
|
data: function () {
|
||||||
|
return {
|
||||||
|
swimlaneId: 0,
|
||||||
|
columnId: 0,
|
||||||
|
position: 1,
|
||||||
|
columns: [],
|
||||||
|
tasks: [],
|
||||||
|
positionChoice: 'before'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
ready: function () {
|
||||||
|
this.columns = this.board[0].columns;
|
||||||
|
this.columnId = this.columns[0].id;
|
||||||
|
this.tasks = this.columns[0].tasks;
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
onChangeSwimlane: function () {
|
||||||
|
var self = this;
|
||||||
|
this.columnId = 0;
|
||||||
|
this.position = 1;
|
||||||
|
this.columns = [];
|
||||||
|
this.tasks = [];
|
||||||
|
this.positionChoice = 'before';
|
||||||
|
|
||||||
|
this.board.forEach(function(swimlane) {
|
||||||
|
if (swimlane.id === self.swimlaneId) {
|
||||||
|
self.columns = swimlane.columns;
|
||||||
|
self.tasks = self.columns[0].tasks;
|
||||||
|
self.columnId = self.columns[0].id;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onChangeColumn: function () {
|
||||||
|
var self = this;
|
||||||
|
this.position = 1;
|
||||||
|
this.tasks = [];
|
||||||
|
this.positionChoice = 'before';
|
||||||
|
|
||||||
|
this.columns.forEach(function(column) {
|
||||||
|
if (column.id == self.columnId) {
|
||||||
|
self.tasks = column.tasks;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onSubmit: function () {
|
||||||
|
if (this.positionChoice == 'after') {
|
||||||
|
this.position++;
|
||||||
|
}
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
cache: false,
|
||||||
|
url: this.saveUrl,
|
||||||
|
contentType: "application/json",
|
||||||
|
type: "POST",
|
||||||
|
processData: false,
|
||||||
|
data: JSON.stringify({
|
||||||
|
"column_id": this.columnId,
|
||||||
|
"swimlane_id": this.swimlaneId,
|
||||||
|
"position": this.position
|
||||||
|
}),
|
||||||
|
complete: function() {
|
||||||
|
window.location.reload(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -148,4 +148,8 @@ Kanboard.Popover.prototype.afterOpen = function() {
|
|||||||
this.app.datePicker();
|
this.app.datePicker();
|
||||||
this.app.autoComplete();
|
this.app.autoComplete();
|
||||||
this.app.tagAutoComplete();
|
this.app.tagAutoComplete();
|
||||||
|
|
||||||
|
new Vue({
|
||||||
|
el: '#popover-container'
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
13
assets/js/vendor.min.js
vendored
13
assets/js/vendor.min.js
vendored
File diff suppressed because one or more lines are too long
@@ -6,6 +6,7 @@ var sass = require('gulp-sass');
|
|||||||
|
|
||||||
var src = {
|
var src = {
|
||||||
js: [
|
js: [
|
||||||
|
'assets/js/components/*.js',
|
||||||
'assets/js/src/Namespace.js',
|
'assets/js/src/Namespace.js',
|
||||||
'assets/js/src/!(Namespace|Bootstrap|BoardDragAndDrop)*.js',
|
'assets/js/src/!(Namespace|Bootstrap|BoardDragAndDrop)*.js',
|
||||||
'assets/js/src/BoardDragAndDrop.js',
|
'assets/js/src/BoardDragAndDrop.js',
|
||||||
@@ -49,7 +50,8 @@ var vendor = {
|
|||||||
'bower_components/simplemde/dist/simplemde.min.js',
|
'bower_components/simplemde/dist/simplemde.min.js',
|
||||||
'bower_components/d3/d3.min.js',
|
'bower_components/d3/d3.min.js',
|
||||||
'bower_components/c3/c3.min.js',
|
'bower_components/c3/c3.min.js',
|
||||||
'bower_components/isMobile/isMobile.min.js'
|
'bower_components/isMobile/isMobile.min.js',
|
||||||
|
'node_modules/vue/dist/vue.min.js'
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,8 @@
|
|||||||
"gulp-bower": "0.0.13",
|
"gulp-bower": "0.0.13",
|
||||||
"gulp-cli": "^1.2.1",
|
"gulp-cli": "^1.2.1",
|
||||||
"gulp-concat": "^2.6.0",
|
"gulp-concat": "^2.6.0",
|
||||||
|
"gulp-sass": "^2.3.2",
|
||||||
"gulp-uglify": "^1.5.3",
|
"gulp-uglify": "^1.5.3",
|
||||||
"gulp-sass": "^2.3.2"
|
"vue": "^1.0.26-csp"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,12 +46,17 @@ class BoardFormatterTest extends Base
|
|||||||
|
|
||||||
$this->assertCount(3, $board);
|
$this->assertCount(3, $board);
|
||||||
|
|
||||||
|
$this->assertSame(0, $board[0]['id']);
|
||||||
$this->assertEquals('Default swimlane', $board[0]['name']);
|
$this->assertEquals('Default swimlane', $board[0]['name']);
|
||||||
$this->assertCount(4, $board[0]['columns']);
|
$this->assertCount(4, $board[0]['columns']);
|
||||||
$this->assertEquals(3, $board[0]['nb_swimlanes']);
|
$this->assertEquals(3, $board[0]['nb_swimlanes']);
|
||||||
$this->assertEquals(4, $board[0]['nb_columns']);
|
$this->assertEquals(4, $board[0]['nb_columns']);
|
||||||
$this->assertEquals(6, $board[0]['nb_tasks']);
|
$this->assertEquals(6, $board[0]['nb_tasks']);
|
||||||
$this->assertEquals(10, $board[0]['score']);
|
$this->assertEquals(10, $board[0]['score']);
|
||||||
|
$this->assertSame(1, $board[0]['columns'][0]['id']);
|
||||||
|
$this->assertSame(2, $board[0]['columns'][1]['id']);
|
||||||
|
$this->assertSame(3, $board[0]['columns'][2]['id']);
|
||||||
|
$this->assertSame(4, $board[0]['columns'][3]['id']);
|
||||||
|
|
||||||
$this->assertEquals(4, $board[0]['columns'][0]['column_nb_tasks']);
|
$this->assertEquals(4, $board[0]['columns'][0]['column_nb_tasks']);
|
||||||
$this->assertEquals(1, $board[0]['columns'][1]['column_nb_tasks']);
|
$this->assertEquals(1, $board[0]['columns'][1]['column_nb_tasks']);
|
||||||
@@ -80,6 +85,7 @@ class BoardFormatterTest extends Base
|
|||||||
$this->assertEquals('Task 5', $board[0]['columns'][1]['tasks'][0]['title']);
|
$this->assertEquals('Task 5', $board[0]['columns'][1]['tasks'][0]['title']);
|
||||||
$this->assertEquals('Task 6', $board[0]['columns'][2]['tasks'][0]['title']);
|
$this->assertEquals('Task 6', $board[0]['columns'][2]['tasks'][0]['title']);
|
||||||
|
|
||||||
|
$this->assertSame(1, $board[1]['id']);
|
||||||
$this->assertEquals('Swimlane 1', $board[1]['name']);
|
$this->assertEquals('Swimlane 1', $board[1]['name']);
|
||||||
$this->assertCount(4, $board[1]['columns']);
|
$this->assertCount(4, $board[1]['columns']);
|
||||||
$this->assertEquals(3, $board[1]['nb_swimlanes']);
|
$this->assertEquals(3, $board[1]['nb_swimlanes']);
|
||||||
|
|||||||
Reference in New Issue
Block a user