diff options
author | Frederic Guillot <fred@kanboard.net> | 2015-07-19 17:03:06 -0400 |
---|---|---|
committer | Frederic Guillot <fred@kanboard.net> | 2015-07-19 17:03:06 -0400 |
commit | fcdd71af2cabdd1252172ac83a24be8672ca34cc (patch) | |
tree | 7c449d1c30bfa78e33499995ef73df8fdd424df8 | |
parent | 0dd17c6137b41ace3ce9ae58736326d1a7724c78 (diff) |
Prompt user when moving or duplicate a task to another project
33 files changed, 517 insertions, 176 deletions
diff --git a/app/Controller/Task.php b/app/Controller/Task.php index 6e525b13..1b9f9417 100644 --- a/app/Controller/Task.php +++ b/app/Controller/Task.php @@ -367,34 +367,6 @@ class Task extends Base } /** - * Duplicate a task - * - * @access public - */ - public function duplicate() - { - $task = $this->getTask(); - - if ($this->request->getStringParam('confirmation') === 'yes') { - - $this->checkCSRFParam(); - $task_id = $this->taskDuplication->duplicate($task['id']); - - if ($task_id) { - $this->session->flash(t('Task created successfully.')); - $this->response->redirect($this->helper->url->to('task', 'show', array('project_id' => $task['project_id'], 'task_id' => $task['id']))); - } else { - $this->session->flashError(t('Unable to create this task.')); - $this->response->redirect($this->helper->url->to('task', 'duplicate', array('project_id' => $task['project_id'], 'task_id' => $task['id']))); - } - } - - $this->response->html($this->taskLayout('task/duplicate', array( - 'task' => $task, - ))); - } - - /** * Edit description form * * @access public @@ -493,84 +465,6 @@ class Task extends Base } /** - * Move a task to another project - * - * @access public - */ - public function move() - { - $task = $this->getTask(); - $values = $task; - $errors = array(); - $projects_list = $this->projectPermission->getActiveMemberProjects($this->userSession->getId()); - - unset($projects_list[$task['project_id']]); - - if ($this->request->isPost()) { - - $values = $this->request->getValues(); - list($valid, $errors) = $this->taskValidator->validateProjectModification($values); - - if ($valid) { - - if ($this->taskDuplication->moveToProject($task['id'], $values['project_id'])) { - $this->session->flash(t('Task updated successfully.')); - $this->response->redirect($this->helper->url->to('task', 'show', array('project_id' => $task['project_id'], 'task_id' => $task['id']))); - } - else { - $this->session->flashError(t('Unable to update your task.')); - } - } - } - - $this->response->html($this->taskLayout('task/move_project', array( - 'values' => $values, - 'errors' => $errors, - 'task' => $task, - 'projects_list' => $projects_list, - ))); - } - - /** - * Duplicate a task to another project - * - * @access public - */ - public function copy() - { - $task = $this->getTask(); - $values = $task; - $errors = array(); - $projects_list = $this->projectPermission->getActiveMemberProjects($this->userSession->getId()); - - unset($projects_list[$task['project_id']]); - - if ($this->request->isPost()) { - - $values = $this->request->getValues(); - list($valid, $errors) = $this->taskValidator->validateProjectModification($values); - - if ($valid) { - $task_id = $this->taskDuplication->duplicateToProject($task['id'], $values['project_id']); - if ($task_id) { - $this->session->flash(t('Task created successfully.')); - $this->response->redirect($this->helper->url->to('task', 'show', array('project_id' => $task['project_id'], 'task_id' => $task['id']))); - } - else { - $this->session->flashError(t('Unable to create your task.')); - } - } - } - - $this->response->html($this->taskLayout('task/duplicate_project', array( - 'values' => $values, - 'errors' => $errors, - 'task' => $task, - 'projects_list' => $projects_list, - ))); - } - - /** * Display the time tracking details * * @access public diff --git a/app/Controller/Taskduplication.php b/app/Controller/Taskduplication.php new file mode 100644 index 00000000..91291b0d --- /dev/null +++ b/app/Controller/Taskduplication.php @@ -0,0 +1,143 @@ +<?php + +namespace Controller; + +/** + * Task Duplication controller + * + * @package controller + * @author Frederic Guillot + */ +class Taskduplication extends Base +{ + /** + * Duplicate a task + * + * @access public + */ + public function duplicate() + { + $task = $this->getTask(); + + if ($this->request->getStringParam('confirmation') === 'yes') { + + $this->checkCSRFParam(); + $task_id = $this->taskDuplication->duplicate($task['id']); + + if ($task_id > 0) { + $this->session->flash(t('Task created successfully.')); + $this->response->redirect($this->helper->url->to('task', 'show', array('project_id' => $task['project_id'], 'task_id' => $task['id']))); + } else { + $this->session->flashError(t('Unable to create this task.')); + $this->response->redirect($this->helper->url->to('taskduplication', 'duplicate', array('project_id' => $task['project_id'], 'task_id' => $task['id']))); + } + } + + $this->response->html($this->taskLayout('task_duplication/duplicate', array( + 'task' => $task, + ))); + } + + /** + * Move a task to another project + * + * @access public + */ + public function move() + { + $task = $this->getTask(); + + if ($this->request->isPost()) { + + $values = $this->request->getValues(); + list($valid, $errors) = $this->taskValidator->validateProjectModification($values); + + if ($valid && $this->taskDuplication->moveToProject($task['id'], + $values['project_id'], + $values['swimlane_id'], + $values['column_id'], + $values['category_id'], + $values['owner_id'])) { + + $this->session->flash(t('Task updated successfully.')); + $this->response->redirect($this->helper->url->to('task', 'show', array('project_id' => $values['project_id'], 'task_id' => $task['id']))); + } + + $this->session->flashError(t('Unable to update your task.')); + } + + $this->chooseDestination($task, 'task_duplication/move'); + } + + /** + * Duplicate a task to another project + * + * @access public + */ + public function copy() + { + $task = $this->getTask(); + + if ($this->request->isPost()) { + + $values = $this->request->getValues(); + list($valid, $errors) = $this->taskValidator->validateProjectModification($values); + + if ($valid && $this->taskDuplication->duplicateToProject($task['id'], + $values['project_id'], + $values['swimlane_id'], + $values['column_id'], + $values['category_id'], + $values['owner_id'])) { + + $this->session->flash(t('Task created successfully.')); + $this->response->redirect($this->helper->url->to('task', 'show', array('project_id' => $task['project_id'], 'task_id' => $task['id']))); + } + + $this->session->flashError(t('Unable to create your task.')); + } + + $this->chooseDestination($task, 'task_duplication/copy'); + } + + /** + * Choose destination when move/copy task to another project + * + * @access private + */ + private function chooseDestination(array $task, $template) + { + $values = array(); + $projects_list = $this->projectPermission->getActiveMemberProjects($this->userSession->getId()); + + unset($projects_list[$task['project_id']]); + + if (! empty($projects_list)) { + $dst_project_id = $this->request->getIntegerParam('dst_project_id', key($projects_list)); + + $swimlanes_list = $this->swimlane->getList($dst_project_id, false, true); + $columns_list = $this->board->getColumnsList($dst_project_id); + $categories_list = $this->category->getList($dst_project_id); + $users_list = $this->projectPermission->getMemberList($dst_project_id); + + $values = $this->taskDuplication->checkDestinationProjectValues($task); + $values['project_id'] = $dst_project_id; + } + else { + $swimlanes_list = array(); + $columns_list = array(); + $categories_list = array(); + $users_list = array(); + } + + $this->response->html($this->taskLayout($template, array( + 'values' => $values, + 'task' => $task, + 'projects_list' => $projects_list, + 'swimlanes_list' => $swimlanes_list, + 'columns_list' => $columns_list, + 'categories_list' => $categories_list, + 'users_list' => $users_list, + ))); + } +} diff --git a/app/Locale/da_DK/translations.php b/app/Locale/da_DK/translations.php index a85ef96c..6916b84e 100644 --- a/app/Locale/da_DK/translations.php +++ b/app/Locale/da_DK/translations.php @@ -999,4 +999,8 @@ return array( // 'New remote user' => '', // 'New local user' => '', // 'Default task color' => '', + // 'Hide sidebar' => '', + // 'Expand sidebar' => '', + // 'This feature does not work with all browsers.' => '', + // 'There is no destination project available.' => '', ); diff --git a/app/Locale/de_DE/translations.php b/app/Locale/de_DE/translations.php index cb6cc8e3..1b381157 100644 --- a/app/Locale/de_DE/translations.php +++ b/app/Locale/de_DE/translations.php @@ -999,4 +999,8 @@ return array( // 'New remote user' => '', // 'New local user' => '', // 'Default task color' => '', + // 'Hide sidebar' => '', + // 'Expand sidebar' => '', + // 'This feature does not work with all browsers.' => '', + // 'There is no destination project available.' => '', ); diff --git a/app/Locale/es_ES/translations.php b/app/Locale/es_ES/translations.php index 2f5e9b9a..867cc3db 100644 --- a/app/Locale/es_ES/translations.php +++ b/app/Locale/es_ES/translations.php @@ -999,4 +999,8 @@ return array( // 'New remote user' => '', // 'New local user' => '', // 'Default task color' => '', + // 'Hide sidebar' => '', + // 'Expand sidebar' => '', + // 'This feature does not work with all browsers.' => '', + // 'There is no destination project available.' => '', ); diff --git a/app/Locale/fi_FI/translations.php b/app/Locale/fi_FI/translations.php index ab857645..79124b15 100644 --- a/app/Locale/fi_FI/translations.php +++ b/app/Locale/fi_FI/translations.php @@ -999,4 +999,8 @@ return array( // 'New remote user' => '', // 'New local user' => '', // 'Default task color' => '', + // 'Hide sidebar' => '', + // 'Expand sidebar' => '', + // 'This feature does not work with all browsers.' => '', + // 'There is no destination project available.' => '', ); diff --git a/app/Locale/fr_FR/translations.php b/app/Locale/fr_FR/translations.php index db950a20..81159fcf 100644 --- a/app/Locale/fr_FR/translations.php +++ b/app/Locale/fr_FR/translations.php @@ -1001,4 +1001,8 @@ return array( 'New remote user' => 'Créer un utilisateur distant', 'New local user' => 'Créer un utilisateur local', 'Default task color' => 'Couleur par défaut des tâches', + 'Hide sidebar' => 'Cacher la barre latérale', + 'Expand sidebar' => 'Déplier la barre latérale', + 'This feature does not work with all browsers.' => 'Cette fonctionnalité n\'est pas compatible avec tous les navigateurs', + 'There is no destination project available.' => 'Il n\'y a pas de projet de destination disponible.', ); diff --git a/app/Locale/hu_HU/translations.php b/app/Locale/hu_HU/translations.php index ffc66324..cd2bca0a 100644 --- a/app/Locale/hu_HU/translations.php +++ b/app/Locale/hu_HU/translations.php @@ -999,4 +999,8 @@ return array( // 'New remote user' => '', // 'New local user' => '', // 'Default task color' => '', + // 'Hide sidebar' => '', + // 'Expand sidebar' => '', + // 'This feature does not work with all browsers.' => '', + // 'There is no destination project available.' => '', ); diff --git a/app/Locale/it_IT/translations.php b/app/Locale/it_IT/translations.php index b232bdcb..353630c3 100644 --- a/app/Locale/it_IT/translations.php +++ b/app/Locale/it_IT/translations.php @@ -999,4 +999,8 @@ return array( // 'New remote user' => '', // 'New local user' => '', // 'Default task color' => '', + // 'Hide sidebar' => '', + // 'Expand sidebar' => '', + // 'This feature does not work with all browsers.' => '', + // 'There is no destination project available.' => '', ); diff --git a/app/Locale/ja_JP/translations.php b/app/Locale/ja_JP/translations.php index 89d317ed..636df9a5 100644 --- a/app/Locale/ja_JP/translations.php +++ b/app/Locale/ja_JP/translations.php @@ -999,4 +999,8 @@ return array( // 'New remote user' => '', // 'New local user' => '', // 'Default task color' => '', + // 'Hide sidebar' => '', + // 'Expand sidebar' => '', + // 'This feature does not work with all browsers.' => '', + // 'There is no destination project available.' => '', ); diff --git a/app/Locale/nl_NL/translations.php b/app/Locale/nl_NL/translations.php index bf43a9a7..c0a6a032 100644 --- a/app/Locale/nl_NL/translations.php +++ b/app/Locale/nl_NL/translations.php @@ -999,4 +999,8 @@ return array( // 'New remote user' => '', // 'New local user' => '', // 'Default task color' => '', + // 'Hide sidebar' => '', + // 'Expand sidebar' => '', + // 'This feature does not work with all browsers.' => '', + // 'There is no destination project available.' => '', ); diff --git a/app/Locale/pl_PL/translations.php b/app/Locale/pl_PL/translations.php index 6b4b411b..9c4558d3 100644 --- a/app/Locale/pl_PL/translations.php +++ b/app/Locale/pl_PL/translations.php @@ -999,4 +999,8 @@ return array( // 'New remote user' => '', // 'New local user' => '', // 'Default task color' => '', + // 'Hide sidebar' => '', + // 'Expand sidebar' => '', + // 'This feature does not work with all browsers.' => '', + // 'There is no destination project available.' => '', ); diff --git a/app/Locale/pt_BR/translations.php b/app/Locale/pt_BR/translations.php index ebe5466f..b31f815c 100644 --- a/app/Locale/pt_BR/translations.php +++ b/app/Locale/pt_BR/translations.php @@ -999,4 +999,8 @@ return array( // 'New remote user' => '', // 'New local user' => '', // 'Default task color' => '', + // 'Hide sidebar' => '', + // 'Expand sidebar' => '', + // 'This feature does not work with all browsers.' => '', + // 'There is no destination project available.' => '', ); diff --git a/app/Locale/ru_RU/translations.php b/app/Locale/ru_RU/translations.php index 75dd15c0..9ce2ea6e 100644 --- a/app/Locale/ru_RU/translations.php +++ b/app/Locale/ru_RU/translations.php @@ -999,4 +999,8 @@ return array( // 'New remote user' => '', // 'New local user' => '', // 'Default task color' => '', + // 'Hide sidebar' => '', + // 'Expand sidebar' => '', + // 'This feature does not work with all browsers.' => '', + // 'There is no destination project available.' => '', ); diff --git a/app/Locale/sr_Latn_RS/translations.php b/app/Locale/sr_Latn_RS/translations.php index 706efbb0..7f90af2d 100644 --- a/app/Locale/sr_Latn_RS/translations.php +++ b/app/Locale/sr_Latn_RS/translations.php @@ -999,4 +999,8 @@ return array( // 'New remote user' => '', // 'New local user' => '', // 'Default task color' => '', + // 'Hide sidebar' => '', + // 'Expand sidebar' => '', + // 'This feature does not work with all browsers.' => '', + // 'There is no destination project available.' => '', ); diff --git a/app/Locale/sv_SE/translations.php b/app/Locale/sv_SE/translations.php index a03aadfd..67e07192 100644 --- a/app/Locale/sv_SE/translations.php +++ b/app/Locale/sv_SE/translations.php @@ -999,4 +999,8 @@ return array( // 'New remote user' => '', // 'New local user' => '', // 'Default task color' => '', + // 'Hide sidebar' => '', + // 'Expand sidebar' => '', + // 'This feature does not work with all browsers.' => '', + // 'There is no destination project available.' => '', ); diff --git a/app/Locale/th_TH/translations.php b/app/Locale/th_TH/translations.php index 8c24aa65..a44d0116 100644 --- a/app/Locale/th_TH/translations.php +++ b/app/Locale/th_TH/translations.php @@ -999,4 +999,8 @@ return array( // 'New remote user' => '', // 'New local user' => '', // 'Default task color' => '', + // 'Hide sidebar' => '', + // 'Expand sidebar' => '', + // 'This feature does not work with all browsers.' => '', + // 'There is no destination project available.' => '', ); diff --git a/app/Locale/tr_TR/translations.php b/app/Locale/tr_TR/translations.php index e9fb19cd..d394a67a 100644 --- a/app/Locale/tr_TR/translations.php +++ b/app/Locale/tr_TR/translations.php @@ -999,4 +999,8 @@ return array( // 'New remote user' => '', // 'New local user' => '', // 'Default task color' => '', + // 'Hide sidebar' => '', + // 'Expand sidebar' => '', + // 'This feature does not work with all browsers.' => '', + // 'There is no destination project available.' => '', ); diff --git a/app/Locale/zh_CN/translations.php b/app/Locale/zh_CN/translations.php index d1eec36f..4de3aeaf 100644 --- a/app/Locale/zh_CN/translations.php +++ b/app/Locale/zh_CN/translations.php @@ -999,4 +999,8 @@ return array( // 'New remote user' => '', // 'New local user' => '', // 'Default task color' => '', + // 'Hide sidebar' => '', + // 'Expand sidebar' => '', + // 'This feature does not work with all browsers.' => '', + // 'There is no destination project available.' => '', ); diff --git a/app/Model/Acl.php b/app/Model/Acl.php index b9c06e98..6ee78faa 100644 --- a/app/Model/Acl.php +++ b/app/Model/Acl.php @@ -41,6 +41,7 @@ class Acl extends Base 'activity' => '*', 'subtask' => '*', 'task' => '*', + 'taskduplication' => '*', 'tasklink' => '*', 'timer' => '*', 'calendar' => array('show', 'project'), diff --git a/app/Model/TaskDuplication.php b/app/Model/TaskDuplication.php index afcac4c7..8048f036 100755 --- a/app/Model/TaskDuplication.php +++ b/app/Model/TaskDuplication.php @@ -93,15 +93,22 @@ class TaskDuplication extends Base * Duplicate a task to another project * * @access public - * @param integer $task_id Task id - * @param integer $project_id Project id - * @return boolean|integer Duplicated task id + * @param integer $task_id + * @param integer $project_id + * @param integer $swimlane_id + * @param integer $column_id + * @param integer $category_id + * @param integer $owner_id + * @return boolean|integer */ - public function duplicateToProject($task_id, $project_id) + public function duplicateToProject($task_id, $project_id, $swimlane_id = null, $column_id = null, $category_id = null, $owner_id = null) { $values = $this->copyFields($task_id); $values['project_id'] = $project_id; - $values['column_id'] = $this->board->getFirstColumn($project_id); + $values['column_id'] = $column_id !== null ? $column_id : $this->board->getFirstColumn($project_id); + $values['swimlane_id'] = $swimlane_id !== null ? $swimlane_id : $values['swimlane_id']; + $values['category_id'] = $category_id !== null ? $category_id : $values['category_id']; + $values['owner_id'] = $owner_id !== null ? $owner_id : $values['owner_id']; $this->checkDestinationProjectValues($values); @@ -112,22 +119,26 @@ class TaskDuplication extends Base * Move a task to another project * * @access public - * @param integer $task_id Task id - * @param integer $project_id Project id + * @param integer $task_id + * @param integer $project_id + * @param integer $swimlane_id + * @param integer $column_id + * @param integer $category_id + * @param integer $owner_id * @return boolean */ - public function moveToProject($task_id, $project_id) + public function moveToProject($task_id, $project_id, $swimlane_id = null, $column_id = null, $category_id = null, $owner_id = null) { $task = $this->taskFinder->getById($task_id); $values = array(); $values['is_active'] = 1; $values['project_id'] = $project_id; - $values['column_id'] = $this->board->getFirstColumn($project_id); + $values['column_id'] = $column_id !== null ? $column_id : $this->board->getFirstColumn($project_id); $values['position'] = $this->taskFinder->countByColumnId($project_id, $values['column_id']) + 1; - $values['owner_id'] = $task['owner_id']; - $values['category_id'] = $task['category_id']; - $values['swimlane_id'] = $task['swimlane_id']; + $values['swimlane_id'] = $swimlane_id !== null ? $swimlane_id : $task['swimlane_id']; + $values['category_id'] = $category_id !== null ? $category_id : $task['category_id']; + $values['owner_id'] = $owner_id !== null ? $owner_id : $task['owner_id']; $this->checkDestinationProjectValues($values); @@ -144,10 +155,10 @@ class TaskDuplication extends Base /** * Check if the assignee and the category are available in the destination project * - * @access private + * @access public * @param array $values */ - private function checkDestinationProjectValues(&$values) + public function checkDestinationProjectValues(array &$values) { // Check if the assigned user is allowed for the destination project if ($values['owner_id'] > 0 && ! $this->projectPermission->isUserAllowed($values['project_id'], $values['owner_id'])) { @@ -169,6 +180,8 @@ class TaskDuplication extends Base $this->swimlane->getNameById($values['swimlane_id']) ); } + + return $values; } /** diff --git a/app/Template/task/duplicate_project.php b/app/Template/task/duplicate_project.php deleted file mode 100644 index 9a8e3c4a..00000000 --- a/app/Template/task/duplicate_project.php +++ /dev/null @@ -1,24 +0,0 @@ -<div class="page-header"> - <h2><?= t('Duplicate the task to another project') ?></h2> -</div> - -<?php if (empty($projects_list)): ?> - <p class="alert"><?= t('No project') ?></p> -<?php else: ?> - - <form method="post" action="<?= $this->url->href('task', 'copy', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>" autocomplete="off"> - - <?= $this->form->csrf() ?> - - <?= $this->form->hidden('id', $values) ?> - <?= $this->form->label(t('Project'), 'project_id') ?> - <?= $this->form->select('project_id', $projects_list, $values, $errors) ?><br/> - - <div class="form-actions"> - <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/> - <?= t('or') ?> - <?= $this->url->link(t('cancel'), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?> - </div> - </form> - -<?php endif ?>
\ No newline at end of file diff --git a/app/Template/task/move_project.php b/app/Template/task/move_project.php deleted file mode 100644 index b0b33f81..00000000 --- a/app/Template/task/move_project.php +++ /dev/null @@ -1,24 +0,0 @@ -<div class="page-header"> - <h2><?= t('Move the task to another project') ?></h2> -</div> - -<?php if (empty($projects_list)): ?> - <p class="alert"><?= t('No project') ?></p> -<?php else: ?> - - <form method="post" action="<?= $this->url->href('task', 'move', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>" autocomplete="off"> - - <?= $this->form->csrf() ?> - - <?= $this->form->hidden('id', $values) ?> - <?= $this->form->label(t('Project'), 'project_id') ?> - <?= $this->form->select('project_id', $projects_list, $values, $errors) ?><br/> - - <div class="form-actions"> - <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/> - <?= t('or') ?> - <?= $this->url->link(t('cancel'), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?> - </div> - </form> - -<?php endif ?>
\ No newline at end of file diff --git a/app/Template/task/sidebar.php b/app/Template/task/sidebar.php index e6a5517a..942e7d01 100644 --- a/app/Template/task/sidebar.php +++ b/app/Template/task/sidebar.php @@ -46,13 +46,13 @@ <?= $this->url->link(t('Add a screenshot'), 'file', 'screenshot', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?> </li> <li> - <?= $this->url->link(t('Duplicate'), 'task', 'duplicate', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?> + <?= $this->url->link(t('Duplicate'), 'taskduplication', 'duplicate', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?> </li> <li> - <?= $this->url->link(t('Duplicate to another project'), 'task', 'copy', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?> + <?= $this->url->link(t('Duplicate to another project'), 'taskduplication', 'copy', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?> </li> <li> - <?= $this->url->link(t('Move to another project'), 'task', 'move', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?> + <?= $this->url->link(t('Move to another project'), 'taskduplication', 'move', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?> </li> <li> <?php if ($task['is_active'] == 1): ?> diff --git a/app/Template/task_duplication/copy.php b/app/Template/task_duplication/copy.php new file mode 100644 index 00000000..f9106c1d --- /dev/null +++ b/app/Template/task_duplication/copy.php @@ -0,0 +1,43 @@ +<div class="page-header"> + <h2><?= t('Duplicate the task to another project') ?></h2> +</div> + +<?php if (empty($projects_list)): ?> + <p class="alert"><?= t('There is no destination project available.') ?></p> +<?php else: ?> + + <form method="post" action="<?= $this->url->href('taskduplication', 'copy', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>" autocomplete="off"> + + <?= $this->form->csrf() ?> + <?= $this->form->hidden('id', $values) ?> + + <?= $this->form->label(t('Project'), 'project_id') ?> + <?= $this->form->select( + 'project_id', + $projects_list, + $values, + array(), + array('data-redirect="'.$this->url->href('taskduplication', 'copy', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'dst_project_id' => 'PROJECT_ID')).'"'), + 'task-reload-project-destination' + ) ?> + + <?= $this->form->label(t('Swimlane'), 'swimlane_id') ?> + <?= $this->form->select('swimlane_id', $swimlanes_list, $values) ?> + + <?= $this->form->label(t('Column'), 'column_id') ?> + <?= $this->form->select('column_id', $columns_list, $values) ?> + + <?= $this->form->label(t('Category'), 'category_id') ?> + <?= $this->form->select('category_id', $categories_list, $values) ?> + + <?= $this->form->label(t('Assignee'), 'owner_id') ?> + <?= $this->form->select('owner_id', $users_list, $values) ?> + + <div class="form-actions"> + <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/> + <?= t('or') ?> + <?= $this->url->link(t('cancel'), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?> + </div> + </form> + +<?php endif ?>
\ No newline at end of file diff --git a/app/Template/task/duplicate.php b/app/Template/task_duplication/duplicate.php index e74d2906..4b50d9ca 100644 --- a/app/Template/task/duplicate.php +++ b/app/Template/task_duplication/duplicate.php @@ -8,7 +8,7 @@ </p> <div class="form-actions"> - <?= $this->url->link(t('Yes'), 'task', 'duplicate', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'confirmation' => 'yes'), true, 'btn btn-red') ?> + <?= $this->url->link(t('Yes'), 'taskduplication', 'duplicate', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'confirmation' => 'yes'), true, 'btn btn-red') ?> <?= t('or') ?> <?= $this->url->link(t('cancel'), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?> </div> diff --git a/app/Template/task_duplication/move.php b/app/Template/task_duplication/move.php new file mode 100644 index 00000000..e90424a2 --- /dev/null +++ b/app/Template/task_duplication/move.php @@ -0,0 +1,43 @@ +<div class="page-header"> + <h2><?= t('Move the task to another project') ?></h2> +</div> + +<?php if (empty($projects_list)): ?> + <p class="alert"><?= t('There is no destination project available.') ?></p> +<?php else: ?> + + <form method="post" action="<?= $this->url->href('taskduplication', 'move', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>" autocomplete="off"> + + <?= $this->form->csrf() ?> + <?= $this->form->hidden('id', $values) ?> + + <?= $this->form->label(t('Project'), 'project_id') ?> + <?= $this->form->select( + 'project_id', + $projects_list, + $values, + array(), + array('data-redirect="'.$this->url->href('taskduplication', 'move', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'dst_project_id' => 'PROJECT_ID')).'"'), + 'task-reload-project-destination' + ) ?> + + <?= $this->form->label(t('Swimlane'), 'swimlane_id') ?> + <?= $this->form->select('swimlane_id', $swimlanes_list, $values) ?> + + <?= $this->form->label(t('Column'), 'column_id') ?> + <?= $this->form->select('column_id', $columns_list, $values) ?> + + <?= $this->form->label(t('Category'), 'category_id') ?> + <?= $this->form->select('category_id', $categories_list, $values) ?> + + <?= $this->form->label(t('Assignee'), 'owner_id') ?> + <?= $this->form->select('owner_id', $users_list, $values) ?> + + <div class="form-actions"> + <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/> + <?= t('or') ?> + <?= $this->url->link(t('cancel'), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?> + </div> + </form> + +<?php endif ?>
\ No newline at end of file diff --git a/app/common.php b/app/common.php index 734f094b..815d2643 100644 --- a/app/common.php +++ b/app/common.php @@ -89,6 +89,12 @@ if (ENABLE_URL_REWRITE) { $container['router']->addRoute('project/:project_id/swimlane/:swimlane_id/column/:column_id', 'task', 'create', array('project_id', 'swimlane_id', 'column_id')); $container['router']->addRoute('public/task/:task_id/:token', 'task', 'readonly', array('task_id', 'token')); + $container['router']->addRoute('project/:project_id/task/:task_id/duplicate', 'taskduplication', 'duplicate', array('task_id', 'project_id')); + $container['router']->addRoute('project/:project_id/task/:task_id/copy', 'taskduplication', 'copy', array('task_id', 'project_id')); + $container['router']->addRoute('project/:project_id/task/:task_id/copy/:dst_project_id', 'taskduplication', 'copy', array('task_id', 'project_id', 'dst_project_id')); + $container['router']->addRoute('project/:project_id/task/:task_id/move', 'taskduplication', 'move', array('task_id', 'project_id')); + $container['router']->addRoute('project/:project_id/task/:task_id/move/:dst_project_id', 'taskduplication', 'move', array('task_id', 'project_id', 'dst_project_id')); + // Board routes $container['router']->addRoute('board/:project_id', 'board', 'show', array('project_id')); $container['router']->addRoute('b/:project_id', 'board', 'show', array('project_id')); diff --git a/assets/js/app.js b/assets/js/app.js index 8a15a8a6..10eab671 100644 --- a/assets/js/app.js +++ b/assets/js/app.js @@ -147,10 +147,11 @@ type:"POST",processData:!1,dataType:"html",data:JSON.stringify({text:h.val()})}) CheckSession:function(){$(".form-login").length||$.ajax({cache:!1,url:$("body").data("status-url"),statusCode:{401:function(){window.location=$("body").data("login-url")}}})},Init:function(){$(".chosen-select").chosen({width:"200px",no_results_text:$(".chosen-select").data("notfound"),disable_search_threshold:10});$("#board-selector").chosen({width:180,no_results_text:$("#board-selector").data("notfound")});$("#board-selector").change(function(){window.location=$(this).attr("data-board-url").replace(/PROJECT_ID/g, $(this).val())});window.setInterval(Kanboard.CheckSession,6E4);Mousetrap.bindGlobal("mod+enter",function(){$("form").submit()});Mousetrap.bind("b",function(b){b.preventDefault();$("#board-selector").trigger("chosen:open")});Mousetrap.bind("f",function(b){b.preventDefault();(b=document.getElementById("form-search"))&&b.focus()});Mousetrap.bind("v b",function(b){b=$(".view-board");b.length&&(window.location=b.attr("href"))});Mousetrap.bind("v c",function(b){b=$(".view-calendar");b.length&&(window.location= b.attr("href"))});Mousetrap.bind("v l",function(b){b=$(".view-listing");b.length&&(window.location=b.attr("href"))});$(document).on("focus","#form-search",function(){$("#form-search")[0].setSelectionRange&&$("#form-search")[0].setSelectionRange($("#form-search").val().length,$("#form-search").val().length)});$(document).on("click",".filter-helper",function(b){b.preventDefault();$("#form-search").val($(this).data("filter"));$("form.search").submit()});$(document).on("click",".sidebar-collapse",function(b){b.preventDefault(); -$(".sidebar-container").addClass("sidebar-collapsed");$(".sidebar-expand").show();$(".sidebar h2").hide();$(".sidebar ul").hide();$(".sidebar-collapse").hide()});$(document).on("click",".sidebar-expand",function(b){b.preventDefault();$(".sidebar-container").removeClass("sidebar-collapsed");$(".sidebar-collapse").show();$(".sidebar h2").show();$(".sidebar ul").show();$(".sidebar-expand").hide()});$.datepicker.setDefaults($.datepicker.regional[$("body").data("js-lang")]);$(".alert-fade-out").delay(4E3).fadeOut(800, -function(){$(this).remove()});Kanboard.InitAfterAjax()},InitAfterAjax:function(){$(document).on("click",".popover",Kanboard.Popover);$("[autofocus]").each(function(b,a){$(this).focus()});$(".form-date").datepicker({showOtherMonths:!0,selectOtherMonths:!0,dateFormat:"yy-mm-dd",constrainInput:!1});$(".form-datetime").datetimepicker({controlType:"select",oneLine:!0,dateFormat:"yy-mm-dd",constrainInput:!1});$("#markdown-preview").click(Kanboard.MarkdownPreview);$("#markdown-write").click(Kanboard.MarkdownWriter); -$(".auto-select").focus(function(){$(this).select()});$(".dropit-submenu").hide();$(".dropdown").not(".dropit").dropit({triggerParentEl:"span"});$(".task-autocomplete").length&&(""==$(".opposite_task_id").val()&&$(".task-autocomplete").parent().find("input[type=submit]").attr("disabled","disabled"),$(".task-autocomplete").autocomplete({source:$(".task-autocomplete").data("search-url"),minLength:1,select:function(b,a){var c=$(".task-autocomplete").data("dst-field");$("input[name="+c+"]").val(a.item.id); -$(".task-autocomplete").parent().find("input[type=submit]").removeAttr("disabled")}}));$(".tooltip").tooltip({content:function(){return'<div class="markdown">'+$(this).attr("title")+"</div>"},position:{my:"left-20 top",at:"center bottom+9",using:function(b,a){$(this).css(b);var c=a.target.left+a.target.width/2-a.element.left-20;$("<div>").addClass("tooltip-arrow").addClass(a.vertical).addClass(1>c?"align-left":"align-right").appendTo(this)}}});Kanboard.Exists("screenshot-zone")&&Kanboard.Screenshot.Init()}}}(); +$(".sidebar-container").addClass("sidebar-collapsed");$(".sidebar-expand").show();$(".sidebar h2").hide();$(".sidebar ul").hide();$(".sidebar-collapse").hide()});$(document).on("click",".sidebar-expand",function(b){b.preventDefault();$(".sidebar-container").removeClass("sidebar-collapsed");$(".sidebar-collapse").show();$(".sidebar h2").show();$(".sidebar ul").show();$(".sidebar-expand").hide()});$("select.task-reload-project-destination").change(function(){window.location=$(this).data("redirect").replace(/PROJECT_ID/g, +$(this).val())});$.datepicker.setDefaults($.datepicker.regional[$("body").data("js-lang")]);$(".alert-fade-out").delay(4E3).fadeOut(800,function(){$(this).remove()});Kanboard.InitAfterAjax()},InitAfterAjax:function(){$(document).on("click",".popover",Kanboard.Popover);$("[autofocus]").each(function(b,a){$(this).focus()});$(".form-date").datepicker({showOtherMonths:!0,selectOtherMonths:!0,dateFormat:"yy-mm-dd",constrainInput:!1});$(".form-datetime").datetimepicker({controlType:"select",oneLine:!0, +dateFormat:"yy-mm-dd",constrainInput:!1});$("#markdown-preview").click(Kanboard.MarkdownPreview);$("#markdown-write").click(Kanboard.MarkdownWriter);$(".auto-select").focus(function(){$(this).select()});$(".dropit-submenu").hide();$(".dropdown").not(".dropit").dropit({triggerParentEl:"span"});$(".task-autocomplete").length&&(""==$(".opposite_task_id").val()&&$(".task-autocomplete").parent().find("input[type=submit]").attr("disabled","disabled"),$(".task-autocomplete").autocomplete({source:$(".task-autocomplete").data("search-url"), +minLength:1,select:function(b,a){var c=$(".task-autocomplete").data("dst-field");$("input[name="+c+"]").val(a.item.id);$(".task-autocomplete").parent().find("input[type=submit]").removeAttr("disabled")}}));$(".tooltip").tooltip({content:function(){return'<div class="markdown">'+$(this).attr("title")+"</div>"},position:{my:"left-20 top",at:"center bottom+9",using:function(b,a){$(this).css(b);var c=a.target.left+a.target.width/2-a.element.left-20;$("<div>").addClass("tooltip-arrow").addClass(a.vertical).addClass(1> +c?"align-left":"align-right").appendTo(this)}}});Kanboard.Exists("screenshot-zone")&&Kanboard.Screenshot.Init()}}}(); (function(){function b(a){a.preventDefault();a.stopPropagation();Kanboard.Popover(a,Kanboard.InitAfterAjax)}function a(){Mousetrap.bind("n",function(){Kanboard.OpenPopover($("#board").data("task-creation-url"),Kanboard.InitAfterAjax)});Mousetrap.bind("s",function(){$.ajax({cache:!1,url:$('.filter-display-mode:not([style="display: none;"]) a').attr("href"),success:function(a){$("#board-container").remove();$("#main").append(a);Kanboard.InitAfterAjax();clearInterval(k);c();f();$(".filter-display-mode").toggle()}})}); Mousetrap.bind("c",function(){d()})}function c(){$(".column").sortable({delay:300,distance:5,connectWith:".column",placeholder:"draggable-placeholder",items:".draggable-item",stop:function(a,c){e(c.item.attr("data-task-id"),c.item.parent().attr("data-column-id"),c.item.index()+1,c.item.parent().attr("data-swimlane-id"))}});$("#board").on("click",".task-board-popover",b);$("#board").on("click",".task-board",function(){window.location=$(this).data("task-url")});$(".task-board-tooltip").tooltip({track:!1, position:{my:"left-20 top",at:"center bottom+9",using:function(a,c){$(this).css(a);var b=c.target.left+c.target.width/2-c.element.left-20;$("<div>").addClass("tooltip-arrow").addClass(c.vertical).addClass(1>b?"align-left":"align-right").appendTo(this)}},content:function(a){if(a=$(this).attr("data-href")){var c=this;$.get(a,function l(a){$(".ui-tooltip-content:visible").html(a);a=$(".ui-tooltip:visible");a.css({top:"",left:""});a.children(".tooltip-arrow").remove();var b=$(c).tooltip("option","position"); diff --git a/assets/js/src/base.js b/assets/js/src/base.js index 7bf8a091..6bd8a144 100644 --- a/assets/js/src/base.js +++ b/assets/js/src/base.js @@ -273,6 +273,10 @@ var Kanboard = (function() { $(".sidebar-expand").hide(); }); + $("select.task-reload-project-destination").change(function() { + window.location = $(this).data("redirect").replace(/PROJECT_ID/g, $(this).val()); + }); + // Datepicker translation $.datepicker.setDefaults($.datepicker.regional[$("body").data("js-lang")]); diff --git a/docs/duplicate-move-tasks.markdown b/docs/duplicate-move-tasks.markdown new file mode 100644 index 00000000..dcb01df5 --- /dev/null +++ b/docs/duplicate-move-tasks.markdown @@ -0,0 +1,58 @@ +Duplicate and move tasks +======================== + +Duplicate a task into the same project +-------------------------------------- + +Go to the task view and choose **Duplicate** on the left. + + + +A new task will be created with the same properties as the original. + +Duplicate a task to another project +----------------------------------- + +Go to the task view and choose **Duplicate to another project**. + + + +Only projects where you are member will be shown in the dropdown. + +Before to copy the tasks, Kanboard will ask you the destination properties that are not common between the source and destination project. + +Basically, you need to define: + +- The destination swimlane +- The column +- The category +- The assignee + +Move a task to another project +------------------------------ + +Go to the task view and choose **Move to another project**. + +Moving a task to another project work in the same way as the duplication, you have to choose the new properties of the task. + +List of fields duplicated +------------------------- + +Here are the list of properties duplicated: + +- title +- description +- date_due +- color_id +- project_id +- column_id +- owner_id +- score +- category_id +- time_estimated +- swimlane_id +- recurrence_status +- recurrence_trigger +- recurrence_factor +- recurrence_timeframe +- recurrence_basedate diff --git a/docs/index.markdown b/docs/index.markdown index 9277ea9b..014dfa58 100644 --- a/docs/index.markdown +++ b/docs/index.markdown @@ -26,6 +26,7 @@ Using Kanboard - [Creating tasks](creating-tasks.markdown) - [Closing tasks](closing-tasks.markdown) +- [Duplicate and move tasks](duplicate-move-tasks.markdown) - [Adding screenshots](screenshots.markdown) - [Task links](task-links.markdown) - [Transitions](transitions.markdown) diff --git a/tests/units/TaskDuplicationTest.php b/tests/units/TaskDuplicationTest.php index 4e44fd75..e87fe9cc 100644 --- a/tests/units/TaskDuplicationTest.php +++ b/tests/units/TaskDuplicationTest.php @@ -174,6 +174,45 @@ class TaskDuplicationTest extends Base $this->assertEquals('test', $task['title']); } + public function testDuplicateAnotherProjectWithPredefinedCategory() + { + $td = new TaskDuplication($this->container); + $tc = new TaskCreation($this->container); + $tf = new TaskFinder($this->container); + $p = new Project($this->container); + $c = new Category($this->container); + + // We create 2 projects + $this->assertEquals(1, $p->create(array('name' => 'test1'))); + $this->assertEquals(2, $p->create(array('name' => 'test2'))); + + $this->assertNotFalse($c->create(array('name' => 'Category #1', 'project_id' => 1))); + $this->assertNotFalse($c->create(array('name' => 'Category #1', 'project_id' => 2))); + $this->assertNotFalse($c->create(array('name' => 'Category #2', 'project_id' => 2))); + $this->assertTrue($c->exists(1, 1)); + $this->assertTrue($c->exists(2, 2)); + $this->assertTrue($c->exists(3, 2)); + + // We create a task + $this->assertEquals(1, $tc->create(array('title' => 'test', 'project_id' => 1, 'column_id' => 2, 'category_id' => 1))); + + // We duplicate our task to the 2nd project with no category + $this->assertEquals(2, $td->duplicateToProject(1, 2, null, null, 0)); + + // Check the values of the duplicated task + $task = $tf->getById(2); + $this->assertNotEmpty($task); + $this->assertEquals(0, $task['category_id']); + + // We duplicate our task to the 2nd project with a different category + $this->assertEquals(3, $td->duplicateToProject(1, 2, null, null, 3)); + + // Check the values of the duplicated task + $task = $tf->getById(3); + $this->assertNotEmpty($task); + $this->assertEquals(3, $task['category_id']); + } + public function testDuplicateAnotherProjectWithSwimlane() { $td = new TaskDuplication($this->container); @@ -240,6 +279,57 @@ class TaskDuplicationTest extends Base $this->assertEquals('test', $task['title']); } + public function testDuplicateAnotherProjectWithPredefinedSwimlane() + { + $td = new TaskDuplication($this->container); + $tc = new TaskCreation($this->container); + $tf = new TaskFinder($this->container); + $p = new Project($this->container); + $s = new Swimlane($this->container); + + // We create 2 projects + $this->assertEquals(1, $p->create(array('name' => 'test1'))); + $this->assertEquals(2, $p->create(array('name' => 'test2'))); + + $this->assertNotFalse($s->create(1, 'Swimlane #1')); + $this->assertNotFalse($s->create(2, 'Swimlane #1')); + $this->assertNotFalse($s->create(2, 'Swimlane #2')); + + // We create a task + $this->assertEquals(1, $tc->create(array('title' => 'test', 'project_id' => 1, 'column_id' => 2, 'swimlane_id' => 1))); + + // We duplicate our task to the 2nd project + $this->assertEquals(2, $td->duplicateToProject(1, 2, 3)); + + // Check the values of the duplicated task + $task = $tf->getById(2); + $this->assertNotEmpty($task); + $this->assertEquals(3, $task['swimlane_id']); + } + + public function testDuplicateAnotherProjectWithPredefinedColumn() + { + $td = new TaskDuplication($this->container); + $tc = new TaskCreation($this->container); + $tf = new TaskFinder($this->container); + $p = new Project($this->container); + + // We create 2 projects + $this->assertEquals(1, $p->create(array('name' => 'test1'))); + $this->assertEquals(2, $p->create(array('name' => 'test2'))); + + // We create a task + $this->assertEquals(1, $tc->create(array('title' => 'test', 'project_id' => 1, 'column_id' => 2))); + + // We duplicate our task to the 2nd project with a different column + $this->assertEquals(2, $td->duplicateToProject(1, 2, null, 7)); + + // Check the values of the duplicated task + $task = $tf->getById(2); + $this->assertNotEmpty($task); + $this->assertEquals(7, $task['column_id']); + } + public function testDuplicateAnotherProjectWithUser() { $td = new TaskDuplication($this->container); @@ -297,6 +387,30 @@ class TaskDuplicationTest extends Base $this->assertEquals(5, $task['column_id']); } + public function testDuplicateAnotherProjectWithPredefinedUser() + { + $td = new TaskDuplication($this->container); + $tc = new TaskCreation($this->container); + $tf = new TaskFinder($this->container); + $p = new Project($this->container); + $pp = new ProjectPermission($this->container); + + // We create 2 projects + $this->assertEquals(1, $p->create(array('name' => 'test1'))); + $this->assertEquals(2, $p->create(array('name' => 'test2'))); + + // We create a task + $this->assertEquals(1, $tc->create(array('title' => 'test', 'project_id' => 1, 'column_id' => 2, 'owner_id' => 2))); + + // We duplicate our task to the 2nd project + $this->assertEquals(2, $td->duplicateToProject(1, 2, null, null, null, 1)); + + // Check the values of the duplicated task + $task = $tf->getById(2); + $this->assertNotEmpty($task); + $this->assertEquals(1, $task['owner_id']); + } + public function onMoveProject($event) { $this->assertInstanceOf('Event\TaskEvent', $event); |