From fcdd71af2cabdd1252172ac83a24be8672ca34cc Mon Sep 17 00:00:00 2001
From: Frederic Guillot
Date: Sun, 19 Jul 2015 17:03:06 -0400
Subject: [PATCH] Prompt user when moving or duplicate a task to another
project
---
app/Controller/Task.php | 106 -------------
app/Controller/Taskduplication.php | 143 ++++++++++++++++++
app/Locale/da_DK/translations.php | 4 +
app/Locale/de_DE/translations.php | 4 +
app/Locale/es_ES/translations.php | 4 +
app/Locale/fi_FI/translations.php | 4 +
app/Locale/fr_FR/translations.php | 4 +
app/Locale/hu_HU/translations.php | 4 +
app/Locale/it_IT/translations.php | 4 +
app/Locale/ja_JP/translations.php | 4 +
app/Locale/nl_NL/translations.php | 4 +
app/Locale/pl_PL/translations.php | 4 +
app/Locale/pt_BR/translations.php | 4 +
app/Locale/ru_RU/translations.php | 4 +
app/Locale/sr_Latn_RS/translations.php | 4 +
app/Locale/sv_SE/translations.php | 4 +
app/Locale/th_TH/translations.php | 4 +
app/Locale/tr_TR/translations.php | 4 +
app/Locale/zh_CN/translations.php | 4 +
app/Model/Acl.php | 1 +
app/Model/TaskDuplication.php | 41 +++--
app/Template/task/duplicate_project.php | 24 ---
app/Template/task/move_project.php | 24 ---
app/Template/task/sidebar.php | 6 +-
app/Template/task_duplication/copy.php | 43 ++++++
.../{task => task_duplication}/duplicate.php | 2 +-
app/Template/task_duplication/move.php | 43 ++++++
app/common.php | 6 +
assets/js/app.js | 9 +-
assets/js/src/base.js | 4 +
docs/duplicate-move-tasks.markdown | 58 +++++++
docs/index.markdown | 1 +
tests/units/TaskDuplicationTest.php | 114 ++++++++++++++
33 files changed, 517 insertions(+), 176 deletions(-)
create mode 100644 app/Controller/Taskduplication.php
delete mode 100644 app/Template/task/duplicate_project.php
delete mode 100644 app/Template/task/move_project.php
create mode 100644 app/Template/task_duplication/copy.php
rename app/Template/{task => task_duplication}/duplicate.php (67%)
create mode 100644 app/Template/task_duplication/move.php
create mode 100644 docs/duplicate-move-tasks.markdown
diff --git a/app/Controller/Task.php b/app/Controller/Task.php
index 6e525b136..1b9f94171 100644
--- a/app/Controller/Task.php
+++ b/app/Controller/Task.php
@@ -366,34 +366,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
*
@@ -492,84 +464,6 @@ class Task extends Base
$this->response->html($this->taskLayout('task/edit_recurrence', $params));
}
- /**
- * 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
*
diff --git a/app/Controller/Taskduplication.php b/app/Controller/Taskduplication.php
new file mode 100644
index 000000000..91291b0d6
--- /dev/null
+++ b/app/Controller/Taskduplication.php
@@ -0,0 +1,143 @@
+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 a85ef96c2..6916b84e7 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 cb6cc8e3f..1b381157d 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 2f5e9b9a6..867cc3dba 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 ab857645b..79124b152 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 db950a207..81159fcfe 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 ffc66324e..cd2bca0ae 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 b232bdcbf..353630c37 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 89d317ed0..636df9a5f 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 bf43a9a72..c0a6a0326 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 6b4b411b1..9c4558d34 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 ebe5466fc..b31f815c9 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 75dd15c03..9ce2ea6e6 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 706efbb0a..7f90af2dc 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 a03aadfdf..67e071924 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 8c24aa65c..a44d01167 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 e9fb19cd3..d394a67af 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 d1eec36f8..4de3aeafb 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 b9c06e984..6ee78faa7 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 afcac4c71..8048f036f 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 9a8e3c4a1..000000000
--- a/app/Template/task/duplicate_project.php
+++ /dev/null
@@ -1,24 +0,0 @@
-
-
-
- = t('No project') ?>
-
-
-
-
-
\ 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 b0b33f817..000000000
--- a/app/Template/task/move_project.php
+++ /dev/null
@@ -1,24 +0,0 @@
-
-
-
- = t('No project') ?>
-
-
-
-
-
\ No newline at end of file
diff --git a/app/Template/task/sidebar.php b/app/Template/task/sidebar.php
index e6a5517a6..942e7d01c 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'])) ?>
- = $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'])) ?>
- = $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'])) ?>
- = $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'])) ?>
diff --git a/app/Template/task_duplication/copy.php b/app/Template/task_duplication/copy.php
new file mode 100644
index 000000000..f9106c1dc
--- /dev/null
+++ b/app/Template/task_duplication/copy.php
@@ -0,0 +1,43 @@
+
+
+
+ = t('There is no destination project available.') ?>
+
+
+
+
+
\ No newline at end of file
diff --git a/app/Template/task/duplicate.php b/app/Template/task_duplication/duplicate.php
similarity index 67%
rename from app/Template/task/duplicate.php
rename to app/Template/task_duplication/duplicate.php
index e74d29060..4b50d9ca0 100644
--- a/app/Template/task/duplicate.php
+++ b/app/Template/task_duplication/duplicate.php
@@ -8,7 +8,7 @@
- = $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'])) ?>
diff --git a/app/Template/task_duplication/move.php b/app/Template/task_duplication/move.php
new file mode 100644
index 000000000..e90424a22
--- /dev/null
+++ b/app/Template/task_duplication/move.php
@@ -0,0 +1,43 @@
+
+
+
+ = t('There is no destination project available.') ?>
+
+
+
+
+
\ No newline at end of file
diff --git a/app/common.php b/app/common.php
index 734f094ba..815d2643f 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 8a15a8a6d..10eab671f 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''+$(this).attr("title")+"
"},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;$("").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'
'+$(this).attr("title")+"
"},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;$("
").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;$("
").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 7bf8a0917..6bd8a1440 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 000000000..dcb01df58
--- /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 9277ea9bb..014dfa58c 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 4e44fd750..e87fe9cca 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);