summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
Diffstat (limited to 'app')
-rw-r--r--app/Controller/App.php29
-rw-r--r--app/Controller/Base.php8
-rw-r--r--app/Controller/Board.php14
-rw-r--r--app/Controller/Calendar.php62
-rw-r--r--app/Controller/Link.php162
-rw-r--r--app/Controller/Task.php13
-rw-r--r--app/Controller/Tasklink.php116
-rw-r--r--app/Core/Helper.php29
-rw-r--r--app/Locale/da_DK/translations.php45
-rw-r--r--app/Locale/de_DE/translations.php45
-rw-r--r--app/Locale/es_ES/translations.php45
-rw-r--r--app/Locale/fi_FI/translations.php45
-rw-r--r--app/Locale/fr_FR/translations.php45
-rw-r--r--app/Locale/hu_HU/translations.php447
-rw-r--r--app/Locale/it_IT/translations.php45
-rw-r--r--app/Locale/ja_JP/translations.php45
-rw-r--r--app/Locale/pl_PL/translations.php45
-rw-r--r--app/Locale/pt_BR/translations.php45
-rw-r--r--app/Locale/ru_RU/translations.php45
-rw-r--r--app/Locale/sv_SE/translations.php45
-rw-r--r--app/Locale/th_TH/translations.php45
-rw-r--r--app/Locale/zh_CN/translations.php45
-rw-r--r--app/Model/Acl.php4
-rw-r--r--app/Model/Authentication.php7
-rw-r--r--app/Model/Base.php2
-rw-r--r--app/Model/Link.php234
-rw-r--r--app/Model/ProjectActivity.php8
-rw-r--r--app/Model/ProjectPermission.php19
-rw-r--r--app/Model/SubtaskTimeTracking.php133
-rw-r--r--app/Model/TaskFilter.php32
-rw-r--r--app/Model/TaskFinder.php1
-rw-r--r--app/Model/TaskLink.php142
-rw-r--r--app/Model/User.php12
-rw-r--r--app/Schema/Mysql.php47
-rw-r--r--app/Schema/Postgres.php43
-rw-r--r--app/Schema/Sqlite.php47
-rw-r--r--app/ServiceProvider/ClassProvider.php2
-rw-r--r--app/Subscriber/Base.php1
-rw-r--r--app/Template/board/filters.php13
-rw-r--r--app/Template/board/show.php54
-rw-r--r--app/Template/board/swimlane.php14
-rw-r--r--app/Template/board/task.php161
-rw-r--r--app/Template/board/task_footer.php45
-rw-r--r--app/Template/board/task_public.php30
-rw-r--r--app/Template/board/tasklinks.php15
-rw-r--r--app/Template/calendar/show.php2
-rw-r--r--app/Template/config/about.php25
-rw-r--r--app/Template/config/sidebar.php3
-rw-r--r--app/Template/file/show.php2
-rw-r--r--app/Template/layout.php4
-rw-r--r--app/Template/link/create.php18
-rw-r--r--app/Template/link/edit.php21
-rw-r--r--app/Template/link/index.php33
-rw-r--r--app/Template/link/remove.php15
-rw-r--r--app/Template/task/close.php4
-rw-r--r--app/Template/task/public.php7
-rw-r--r--app/Template/task/show.php3
-rw-r--r--app/Template/task/sidebar.php3
-rw-r--r--app/Template/tasklink/create.php27
-rw-r--r--app/Template/tasklink/remove.php15
-rw-r--r--app/Template/tasklink/show.php41
-rw-r--r--app/functions.php5
62 files changed, 2332 insertions, 427 deletions
diff --git a/app/Controller/App.php b/app/Controller/App.php
index ef0a08a9..46731e7c 100644
--- a/app/Controller/App.php
+++ b/app/Controller/App.php
@@ -2,7 +2,8 @@
namespace Controller;
-use Model\Subtask as SubTaskModel;
+use Model\Subtask as SubtaskModel;
+use Model\Task as TaskModel;
/**
* Application controller
@@ -39,7 +40,7 @@ class App extends Base
*/
public function index($user_id = 0, $action = 'index')
{
- $status = array(SubTaskModel::STATUS_TODO, SubTaskModel::STATUS_INPROGRESS);
+ $status = array(SubTaskModel::STATUS_TODO, SubtaskModel::STATUS_INPROGRESS);
$user_id = $user_id ?: $this->userSession->getId();
$projects = $this->projectPermission->getActiveMemberProjects($user_id);
$project_ids = array_keys($projects);
@@ -88,11 +89,8 @@ class App extends Base
if (empty($payload['text'])) {
$this->response->html('<p>'.t('Nothing to preview...').'</p>');
}
- else {
- $this->response->html(
- $this->template->markdown($payload['text'])
- );
- }
+
+ $this->response->html($this->template->markdown($payload['text']));
}
/**
@@ -104,4 +102,21 @@ class App extends Base
{
$this->response->css($this->color->getCss());
}
+
+ /**
+ * Task autocompletion (Ajax)
+ *
+ * @access public
+ */
+ public function autocomplete()
+ {
+ $this->response->json(
+ $this->taskFilter
+ ->create()
+ ->filterByProjects($this->projectPermission->getActiveMemberProjectIds($this->userSession->getId()))
+ ->excludeTasks(array($this->request->getIntegerParam('exclude_task_id')))
+ ->filterByTitle($this->request->getStringParam('term'))
+ ->toAutoCompletion()
+ );
+ }
}
diff --git a/app/Controller/Base.php b/app/Controller/Base.php
index 7f65e882..548fdb40 100644
--- a/app/Controller/Base.php
+++ b/app/Controller/Base.php
@@ -17,11 +17,13 @@ use Symfony\Component\EventDispatcher\Event;
* @package controller
* @author Frederic Guillot
*
+ * @property \Core\Helper $helper
* @property \Core\Session $session
* @property \Core\Template $template
* @property \Core\Paginator $paginator
* @property \Integration\GithubWebhook $githubWebhook
* @property \Integration\GitlabWebhook $gitlabWebhook
+ * @property \Integration\BitbucketWebhook $bitbucketWebhook
* @property \Model\Acl $acl
* @property \Model\Authentication $authentication
* @property \Model\Action $action
@@ -43,6 +45,7 @@ use Symfony\Component\EventDispatcher\Event;
* @property \Model\Subtask $subtask
* @property \Model\Swimlane $swimlane
* @property \Model\Task $task
+ * @property \Model\Link $link
* @property \Model\TaskCreation $taskCreation
* @property \Model\TaskModification $taskModification
* @property \Model\TaskDuplication $taskDuplication
@@ -54,6 +57,7 @@ use Symfony\Component\EventDispatcher\Event;
* @property \Model\TaskPermission $taskPermission
* @property \Model\TaskStatus $taskStatus
* @property \Model\TaskValidator $taskValidator
+ * @property \Model\TaskLink $taskLink
* @property \Model\CommentHistory $commentHistory
* @property \Model\SubtaskHistory $subtaskHistory
* @property \Model\SubtaskTimeTracking $subtaskTimeTracking
@@ -139,7 +143,7 @@ abstract class Base
private function sendHeaders($action)
{
// HTTP secure headers
- $this->response->csp(array('style-src' => "'self' 'unsafe-inline'"));
+ $this->response->csp(array('style-src' => "'self' 'unsafe-inline'", 'img-src' => '*'));
$this->response->nosniff();
$this->response->xss();
@@ -199,7 +203,7 @@ abstract class Base
{
$project_id = $this->request->getIntegerParam('project_id');
$task_id = $this->request->getIntegerParam('task_id');
-
+
// Allow urls without "project_id"
if ($task_id > 0 && $project_id === 0) {
$project_id = $this->taskFinder->getProjectId($task_id);
diff --git a/app/Controller/Board.php b/app/Controller/Board.php
index e859348e..90b7f357 100644
--- a/app/Controller/Board.php
+++ b/app/Controller/Board.php
@@ -402,6 +402,20 @@ class Board extends Base
}
/**
+ * Get links on mouseover
+ *
+ * @access public
+ */
+ public function tasklinks()
+ {
+ $task = $this->getTask();
+ $this->response->html($this->template->render('board/tasklinks', array(
+ 'links' => $this->taskLink->getLinks($task['id']),
+ 'task' => $task,
+ )));
+ }
+
+ /**
* Get subtasks on mouseover
*
* @access public
diff --git a/app/Controller/Calendar.php b/app/Controller/Calendar.php
index abbcab7f..1c7ac7c0 100644
--- a/app/Controller/Calendar.php
+++ b/app/Controller/Calendar.php
@@ -2,7 +2,7 @@
namespace Controller;
-use Model\Task;
+use Model\Task as TaskModel;
/**
* Project Calendar controller
@@ -41,24 +41,27 @@ class Calendar extends Base
*
* @access public
*/
- public function events()
+ public function project()
{
- $this->response->json(
- $this->taskFilter
- ->create()
- ->filterByProject($this->request->getIntegerParam('project_id'))
- ->filterByCategory($this->request->getIntegerParam('category_id', -1))
- ->filterByOwner($this->request->getIntegerParam('owner_id', -1))
- ->filterByColumn($this->request->getIntegerParam('column_id', -1))
- ->filterBySwimlane($this->request->getIntegerParam('swimlane_id', -1))
- ->filterByColor($this->request->getStringParam('color_id'))
- ->filterByStatus($this->request->getIntegerParam('is_active', -1))
- ->filterByDueDateRange(
- $this->request->getStringParam('start'),
- $this->request->getStringParam('end')
- )
- ->toCalendarEvents()
- );
+ $project_id = $this->request->getIntegerParam('project_id');
+ $start = $this->request->getStringParam('start');
+ $end = $this->request->getStringParam('end');
+
+ $due_tasks = $this->taskFilter
+ ->create()
+ ->filterByProject($project_id)
+ ->filterByCategory($this->request->getIntegerParam('category_id', -1))
+ ->filterByOwner($this->request->getIntegerParam('owner_id', -1))
+ ->filterByColumn($this->request->getIntegerParam('column_id', -1))
+ ->filterBySwimlane($this->request->getIntegerParam('swimlane_id', -1))
+ ->filterByColor($this->request->getStringParam('color_id'))
+ ->filterByStatus($this->request->getIntegerParam('is_active', -1))
+ ->filterByDueDateRange($start, $end)
+ ->toCalendarEvents();
+
+ $subtask_timeslots = $this->subtaskTimeTracking->getProjectCalendarEvents($project_id, $start, $end);
+
+ $this->response->json(array_merge($due_tasks, $subtask_timeslots));
}
/**
@@ -69,18 +72,19 @@ class Calendar extends Base
public function user()
{
$user_id = $this->request->getIntegerParam('user_id');
+ $start = $this->request->getStringParam('start');
+ $end = $this->request->getStringParam('end');
+
+ $due_tasks = $this->taskFilter
+ ->create()
+ ->filterByOwner($user_id)
+ ->filterByStatus(TaskModel::STATUS_OPEN)
+ ->filterByDueDateRange($start, $end)
+ ->toCalendarEvents();
+
+ $subtask_timeslots = $this->subtaskTimeTracking->getUserCalendarEvents($user_id, $start, $end);
- $this->response->json(
- $this->taskFilter
- ->create()
- ->filterByOwner($user_id)
- ->filterByStatus(Task::STATUS_OPEN)
- ->filterByDueDateRange(
- $this->request->getStringParam('start'),
- $this->request->getStringParam('end')
- )
- ->toCalendarEvents()
- );
+ $this->response->json(array_merge($due_tasks, $subtask_timeslots));
}
/**
diff --git a/app/Controller/Link.php b/app/Controller/Link.php
new file mode 100644
index 00000000..ec9c6195
--- /dev/null
+++ b/app/Controller/Link.php
@@ -0,0 +1,162 @@
+<?php
+
+namespace Controller;
+
+/**
+ * Link controller
+ *
+ * @package controller
+ * @author Olivier Maridat
+ * @author Frederic Guillot
+ */
+class Link extends Base
+{
+ /**
+ * Common layout for config views
+ *
+ * @access private
+ * @param string $template Template name
+ * @param array $params Template parameters
+ * @return string
+ */
+ private function layout($template, array $params)
+ {
+ $params['board_selector'] = $this->projectPermission->getAllowedProjects($this->userSession->getId());
+ $params['config_content_for_layout'] = $this->template->render($template, $params);
+
+ return $this->template->layout('config/layout', $params);
+ }
+
+ /**
+ * Get the current link
+ *
+ * @access private
+ * @return array
+ */
+ private function getLink()
+ {
+ $link = $this->link->getById($this->request->getIntegerParam('link_id'));
+
+ if (! $link) {
+ $this->notfound();
+ }
+
+ return $link;
+ }
+
+ /**
+ * List of links
+ *
+ * @access public
+ */
+ public function index(array $values = array(), array $errors = array())
+ {
+ $this->response->html($this->layout('link/index', array(
+ 'links' => $this->link->getMergedList(),
+ 'values' => $values,
+ 'errors' => $errors,
+ 'title' => t('Settings').' &gt; '.t('Task\'s links'),
+ )));
+ }
+
+ /**
+ * Validate and save a new link
+ *
+ * @access public
+ */
+ public function save()
+ {
+ $values = $this->request->getValues();
+ list($valid, $errors) = $this->link->validateCreation($values);
+
+ if ($valid) {
+
+ if ($this->link->create($values['label'], $values['opposite_label'])) {
+ $this->session->flash(t('Link added successfully.'));
+ $this->response->redirect($this->helper->url('link', 'index'));
+ }
+ else {
+ $this->session->flashError(t('Unable to create your link.'));
+ }
+ }
+
+ $this->index($values, $errors);
+ }
+
+ /**
+ * Edit form
+ *
+ * @access public
+ */
+ public function edit(array $values = array(), array $errors = array())
+ {
+ $link = $this->getLink();
+ $link['label'] = t($link['label']);
+
+ $this->response->html($this->layout('link/edit', array(
+ 'values' => $values ?: $link,
+ 'errors' => $errors,
+ 'labels' => $this->link->getList($link['id']),
+ 'link' => $link,
+ 'title' => t('Link modification')
+ )));
+ }
+
+ /**
+ * Edit a link (validate the form and update the database)
+ *
+ * @access public
+ */
+ public function update()
+ {
+ $values = $this->request->getValues();
+ list($valid, $errors) = $this->link->validateModification($values);
+
+ if ($valid) {
+ if ($this->link->update($values)) {
+ $this->session->flash(t('Link updated successfully.'));
+ $this->response->redirect($this->helper->url('link', 'index'));
+ }
+ else {
+ $this->session->flashError(t('Unable to update your link.'));
+ }
+ }
+
+ $this->edit($values, $errors);
+ }
+
+ /**
+ * Confirmation dialog before removing a link
+ *
+ * @access public
+ */
+ public function confirm()
+ {
+ $link = $this->getLink();
+
+ $this->response->html($this->layout('link/remove', array(
+ 'link' => $link,
+ 'title' => t('Remove a link')
+ )));
+ }
+
+ /**
+ * Remove a link
+ *
+ * @access public
+ */
+ public function remove()
+ {
+ $this->checkCSRFParam();
+ $link = $this->getLink();
+
+ if ($this->link->remove($link['id'])) {
+ $this->session->flash(t('Link removed successfully.'));
+ }
+ else {
+ $this->session->flashError(t('Unable to remove this link.'));
+ }
+
+ $this->response->redirect($this->helper->url('link', 'index'));
+ }
+}
diff --git a/app/Controller/Task.php b/app/Controller/Task.php
index fdd20b5e..741db61e 100644
--- a/app/Controller/Task.php
+++ b/app/Controller/Task.php
@@ -36,6 +36,7 @@ class Task extends Base
'project' => $project,
'comments' => $this->comment->getAll($task['id']),
'subtasks' => $this->subtask->getAll($task['id']),
+ 'links' => $this->taskLink->getLinks($task['id']),
'task' => $task,
'columns_list' => $this->board->getColumnsList($task['project_id']),
'colors_list' => $this->color->getList(),
@@ -70,6 +71,7 @@ class Task extends Base
'files' => $this->file->getAll($task['id']),
'comments' => $this->comment->getAll($task['id']),
'subtasks' => $subtasks,
+ 'links' => $this->taskLink->getLinks($task['id']),
'task' => $task,
'values' => $values,
'columns_list' => $this->board->getColumnsList($task['project_id']),
@@ -249,6 +251,7 @@ class Task extends Base
public function close()
{
$task = $this->getTask();
+ $redirect = $this->request->getStringParam('redirect');
if ($this->request->getStringParam('confirmation') === 'yes') {
@@ -260,15 +263,23 @@ class Task extends Base
$this->session->flashError(t('Unable to close this task.'));
}
- if ($this->request->getStringParam('redirect') === 'board') {
+ if ($redirect === 'board') {
$this->response->redirect($this->helper->url('board', 'show', array('project_id' => $task['project_id'])));
}
$this->response->redirect($this->helper->url('task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])));
}
+ if ($this->request->isAjax()) {
+ $this->response->html($this->template->render('task/close', array(
+ 'task' => $task,
+ 'redirect' => $redirect,
+ )));
+ }
+
$this->response->html($this->taskLayout('task/close', array(
'task' => $task,
+ 'redirect' => $redirect,
)));
}
diff --git a/app/Controller/Tasklink.php b/app/Controller/Tasklink.php
new file mode 100644
index 00000000..61b7fab8
--- /dev/null
+++ b/app/Controller/Tasklink.php
@@ -0,0 +1,116 @@
+<?php
+
+namespace Controller;
+
+/**
+ * TaskLink controller
+ *
+ * @package controller
+ * @author Olivier Maridat
+ * @author Frederic Guillot
+ */
+class Tasklink extends Base
+{
+ /**
+ * Get the current link
+ *
+ * @access private
+ * @return array
+ */
+ private function getTaskLink()
+ {
+ $link = $this->taskLink->getById($this->request->getIntegerParam('link_id'));
+
+ if (! $link) {
+ $this->notfound();
+ }
+
+ return $link;
+ }
+
+ /**
+ * Creation form
+ *
+ * @access public
+ */
+ public function create(array $values = array(), array $errors = array())
+ {
+ $task = $this->getTask();
+
+ if (empty($values)) {
+ $values = array(
+ 'task_id' => $task['id'],
+ );
+ }
+
+ $this->response->html($this->taskLayout('tasklink/create', array(
+ 'values' => $values,
+ 'errors' => $errors,
+ 'task' => $task,
+ 'labels' => $this->link->getList(0, false),
+ 'title' => t('Add a new link')
+ )));
+ }
+
+ /**
+ * Validation and creation
+ *
+ * @access public
+ */
+ public function save()
+ {
+ $task = $this->getTask();
+ $values = $this->request->getValues();
+
+ list($valid, $errors) = $this->taskLink->validateCreation($values);
+
+ if ($valid) {
+
+ if ($this->taskLink->create($values['task_id'], $values['opposite_task_id'], $values['link_id'])) {
+ $this->session->flash(t('Link added successfully.'));
+ $this->response->redirect($this->helper->url('task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])).'#links');
+ }
+ else {
+ $this->session->flashError(t('Unable to create your link.'));
+ }
+ }
+
+ $this->create($values, $errors);
+ }
+
+ /**
+ * Confirmation dialog before removing a link
+ *
+ * @access public
+ */
+ public function confirm()
+ {
+ $task = $this->getTask();
+ $link = $this->getTaskLink();
+
+ $this->response->html($this->taskLayout('tasklink/remove', array(
+ 'link' => $link,
+ 'task' => $task,
+ )));
+ }
+
+ /**
+ * Remove a link
+ *
+ * @access public
+ */
+ public function remove()
+ {
+ $this->checkCSRFParam();
+ $task = $this->getTask();
+
+ if ($this->taskLink->remove($this->request->getIntegerParam('link_id'))) {
+ $this->session->flash(t('Link removed successfully.'));
+ }
+ else {
+ $this->session->flashError(t('Unable to remove this link.'));
+ }
+
+ $this->response->redirect($this->helper->url('task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])));
+ }
+}
diff --git a/app/Core/Helper.php b/app/Core/Helper.php
index d60e29d4..187fcfbb 100644
--- a/app/Core/Helper.php
+++ b/app/Core/Helper.php
@@ -50,6 +50,33 @@ class Helper
}
/**
+ * Get the age of an item in quasi human readable format.
+ * It's in this format: <1h , NNh, NNd
+ *
+ * @access public
+ * @param integer $timestamp Unix timestamp of the artifact for which age will be calculated
+ * @param integer $now Compare with this timestamp (Default value is the current unix timestamp)
+ * @return string
+ */
+ public function getTaskAge($timestamp, $now = null)
+ {
+ if ($now === null) {
+ $now = time();
+ }
+
+ $diff = $now - $timestamp;
+
+ if ($diff < 3600) {
+ return t('<1h');
+ }
+ else if ($diff < 86400) {
+ return t('%dh', $diff / 3600);
+ }
+
+ return t('%dd', ($now - $timestamp) / 86400);
+ }
+
+ /**
* Proxy cache helper for acl::isManagerActionAllowed()
*
* @access public
@@ -639,7 +666,7 @@ class Helper
'subtaskRestriction',
array('task_id' => $subtask['task_id'], 'subtask_id' => $subtask['id'], 'redirect' => $redirect),
false,
- 'popover-subtask-restriction'
+ 'popover'
);
}
diff --git a/app/Locale/da_DK/translations.php b/app/Locale/da_DK/translations.php
index d0bebe0c..e09810b2 100644
--- a/app/Locale/da_DK/translations.php
+++ b/app/Locale/da_DK/translations.php
@@ -686,4 +686,49 @@ return array(
// 'Task age in days' => '',
// 'Days in this column' => '',
// '%dd' => '',
+ // 'Add a link' => '',
+ // 'Add a new link' => '',
+ // 'Do you really want to remove this link: "%s"?' => '',
+ // 'Do you really want to remove this link with task #%d?' => '',
+ // 'Field required' => '',
+ // 'Link added successfully.' => '',
+ // 'Link updated successfully.' => '',
+ // 'Link removed successfully.' => '',
+ // 'Link labels' => '',
+ // 'Link modification' => '',
+ // 'Links' => '',
+ // 'Link settings' => '',
+ // 'Opposite label' => '',
+ // 'Remove a link' => '',
+ // 'Task\'s links' => '',
+ // 'The labels must be different' => '',
+ // 'There is no link.' => '',
+ // 'This label must be unique' => '',
+ // 'Unable to create your link.' => '',
+ // 'Unable to update your link.' => '',
+ // 'Unable to remove this link.' => '',
+ // 'relates to' => '',
+ // 'blocks' => '',
+ // 'is blocked by' => '',
+ // 'duplicates' => '',
+ // 'is duplicated by' => '',
+ // 'is a child of' => '',
+ // 'is a parent of' => '',
+ // 'targets milestone' => '',
+ // 'is a milestone of' => '',
+ // 'fixes' => '',
+ // 'is fixed by' => '',
+ // 'This task' => '',
+ // '<1h' => '',
+ // '%dh' => '',
+ // '%b %e' => '',
+ // 'Expand tasks' => '',
+ // 'Collapse tasks' => '',
+ // 'Expand/collapse tasks' => '',
+ // 'Close dialog box' => '',
+ // 'Submit a form' => '',
+ // 'Board view' => '',
+ // 'Keyboard shortcuts' => '',
+ // 'Open board switcher' => '',
+ // 'Application' => '',
);
diff --git a/app/Locale/de_DE/translations.php b/app/Locale/de_DE/translations.php
index 42db9bcd..d4fbc8c8 100644
--- a/app/Locale/de_DE/translations.php
+++ b/app/Locale/de_DE/translations.php
@@ -686,4 +686,49 @@ return array(
// 'Task age in days' => '',
// 'Days in this column' => '',
// '%dd' => '',
+ // 'Add a link' => '',
+ // 'Add a new link' => '',
+ // 'Do you really want to remove this link: "%s"?' => '',
+ // 'Do you really want to remove this link with task #%d?' => '',
+ // 'Field required' => '',
+ // 'Link added successfully.' => '',
+ // 'Link updated successfully.' => '',
+ // 'Link removed successfully.' => '',
+ // 'Link labels' => '',
+ // 'Link modification' => '',
+ // 'Links' => '',
+ // 'Link settings' => '',
+ // 'Opposite label' => '',
+ // 'Remove a link' => '',
+ // 'Task\'s links' => '',
+ // 'The labels must be different' => '',
+ // 'There is no link.' => '',
+ // 'This label must be unique' => '',
+ // 'Unable to create your link.' => '',
+ // 'Unable to update your link.' => '',
+ // 'Unable to remove this link.' => '',
+ // 'relates to' => '',
+ // 'blocks' => '',
+ // 'is blocked by' => '',
+ // 'duplicates' => '',
+ // 'is duplicated by' => '',
+ // 'is a child of' => '',
+ // 'is a parent of' => '',
+ // 'targets milestone' => '',
+ // 'is a milestone of' => '',
+ // 'fixes' => '',
+ // 'is fixed by' => '',
+ // 'This task' => '',
+ // '<1h' => '',
+ // '%dh' => '',
+ // '%b %e' => '',
+ // 'Expand tasks' => '',
+ // 'Collapse tasks' => '',
+ // 'Expand/collapse tasks' => '',
+ // 'Close dialog box' => '',
+ // 'Submit a form' => '',
+ // 'Board view' => '',
+ // 'Keyboard shortcuts' => '',
+ // 'Open board switcher' => '',
+ // 'Application' => '',
);
diff --git a/app/Locale/es_ES/translations.php b/app/Locale/es_ES/translations.php
index e921c10f..56b1a8c4 100644
--- a/app/Locale/es_ES/translations.php
+++ b/app/Locale/es_ES/translations.php
@@ -686,4 +686,49 @@ return array(
// 'Task age in days' => '',
// 'Days in this column' => '',
// '%dd' => '',
+ // 'Add a link' => '',
+ // 'Add a new link' => '',
+ // 'Do you really want to remove this link: "%s"?' => '',
+ // 'Do you really want to remove this link with task #%d?' => '',
+ // 'Field required' => '',
+ // 'Link added successfully.' => '',
+ // 'Link updated successfully.' => '',
+ // 'Link removed successfully.' => '',
+ // 'Link labels' => '',
+ // 'Link modification' => '',
+ // 'Links' => '',
+ // 'Link settings' => '',
+ // 'Opposite label' => '',
+ // 'Remove a link' => '',
+ // 'Task\'s links' => '',
+ // 'The labels must be different' => '',
+ // 'There is no link.' => '',
+ // 'This label must be unique' => '',
+ // 'Unable to create your link.' => '',
+ // 'Unable to update your link.' => '',
+ // 'Unable to remove this link.' => '',
+ // 'relates to' => '',
+ // 'blocks' => '',
+ // 'is blocked by' => '',
+ // 'duplicates' => '',
+ // 'is duplicated by' => '',
+ // 'is a child of' => '',
+ // 'is a parent of' => '',
+ // 'targets milestone' => '',
+ // 'is a milestone of' => '',
+ // 'fixes' => '',
+ // 'is fixed by' => '',
+ // 'This task' => '',
+ // '<1h' => '',
+ // '%dh' => '',
+ // '%b %e' => '',
+ // 'Expand tasks' => '',
+ // 'Collapse tasks' => '',
+ // 'Expand/collapse tasks' => '',
+ // 'Close dialog box' => '',
+ // 'Submit a form' => '',
+ // 'Board view' => '',
+ // 'Keyboard shortcuts' => '',
+ // 'Open board switcher' => '',
+ // 'Application' => '',
);
diff --git a/app/Locale/fi_FI/translations.php b/app/Locale/fi_FI/translations.php
index 0ed1031b..8f9ecdfa 100644
--- a/app/Locale/fi_FI/translations.php
+++ b/app/Locale/fi_FI/translations.php
@@ -686,4 +686,49 @@ return array(
// 'Task age in days' => '',
// 'Days in this column' => '',
// '%dd' => '',
+ // 'Add a link' => '',
+ // 'Add a new link' => '',
+ // 'Do you really want to remove this link: "%s"?' => '',
+ // 'Do you really want to remove this link with task #%d?' => '',
+ // 'Field required' => '',
+ // 'Link added successfully.' => '',
+ // 'Link updated successfully.' => '',
+ // 'Link removed successfully.' => '',
+ // 'Link labels' => '',
+ // 'Link modification' => '',
+ // 'Links' => '',
+ // 'Link settings' => '',
+ // 'Opposite label' => '',
+ // 'Remove a link' => '',
+ // 'Task\'s links' => '',
+ // 'The labels must be different' => '',
+ // 'There is no link.' => '',
+ // 'This label must be unique' => '',
+ // 'Unable to create your link.' => '',
+ // 'Unable to update your link.' => '',
+ // 'Unable to remove this link.' => '',
+ // 'relates to' => '',
+ // 'blocks' => '',
+ // 'is blocked by' => '',
+ // 'duplicates' => '',
+ // 'is duplicated by' => '',
+ // 'is a child of' => '',
+ // 'is a parent of' => '',
+ // 'targets milestone' => '',
+ // 'is a milestone of' => '',
+ // 'fixes' => '',
+ // 'is fixed by' => '',
+ // 'This task' => '',
+ // '<1h' => '',
+ // '%dh' => '',
+ // '%b %e' => '',
+ // 'Expand tasks' => '',
+ // 'Collapse tasks' => '',
+ // 'Expand/collapse tasks' => '',
+ // 'Close dialog box' => '',
+ // 'Submit a form' => '',
+ // 'Board view' => '',
+ // 'Keyboard shortcuts' => '',
+ // 'Open board switcher' => '',
+ // 'Application' => '',
);
diff --git a/app/Locale/fr_FR/translations.php b/app/Locale/fr_FR/translations.php
index 7fc5e842..5d32030e 100644
--- a/app/Locale/fr_FR/translations.php
+++ b/app/Locale/fr_FR/translations.php
@@ -688,4 +688,49 @@ return array(
'Task age in days' => 'Age de la tâche en jours',
'Days in this column' => 'Jours dans cette colonne',
'%dd' => '%dj',
+ 'Add a link' => 'Ajouter un lien',
+ 'Add a new link' => 'Ajouter un nouveau lien',
+ 'Do you really want to remove this link: "%s"?' => 'Voulez-vous vraiment supprimer ce lien : « %s » ?',
+ 'Do you really want to remove this link with task #%d?' => 'Voulez-vous vraiment supprimer ce lien avec la tâche n°%s ?',
+ 'Field required' => 'Champ obligatoire',
+ 'Link added successfully.' => 'Lien créé avec succès.',
+ 'Link updated successfully.' => 'Lien mis à jour avec succès.',
+ 'Link removed successfully.' => 'Lien supprimé avec succès.',
+ 'Link labels' => 'Libellé des liens',
+ 'Link modification' => 'Modification d\'un lien',
+ 'Links' => 'Liens',
+ 'Link settings' => 'Paramètres des liens',
+ 'Opposite label' => 'Nom du libellé opposé',
+ 'Remove a link' => 'Supprimer un lien',
+ 'Task\'s links' => 'Liens des tâches',
+ 'The labels must be different' => 'Les libellés doivent être différents',
+ 'There is no link.' => 'Il n\'y a aucun lien.',
+ 'This label must be unique' => 'Ce libellé doit être unique',
+ 'Unable to create your link.' => 'Impossible d\'ajouter ce lien.',
+ 'Unable to update your link.' => 'Impossible de mettre à jour ce lien.',
+ 'Unable to remove this link.' => 'Impossible de supprimer ce lien.',
+ 'relates to' => 'est liée à',
+ 'blocks' => 'bloque',
+ 'is blocked by' => 'est bloquée par',
+ 'duplicates' => 'duplique',
+ 'is duplicated by' => 'est dupliquée par',
+ 'is a child of' => 'est un enfant de',
+ 'is a parent of' => 'est un parent de',
+ 'targets milestone' => 'vise l\'étape importante',
+ 'is a milestone of' => 'est une étape importante de',
+ 'fixes' => 'corrige',
+ 'is fixed by' => 'est corrigée par',
+ 'This task' => 'Cette tâche',
+ '<1h' => '<1h',
+ '%dh' => '%dh',
+ '%b %e' => '%e %b',
+ 'Expand tasks' => 'Déplier les tâches',
+ 'Collapse tasks' => 'Replier les tâches',
+ 'Expand/collapse tasks' => 'Plier/déplier les tâches',
+ 'Close dialog box' => 'Fermer une boite de dialogue',
+ 'Submit a form' => 'Enregistrer un formulaire',
+ 'Board view' => 'Page du tableau',
+ 'Keyboard shortcuts' => 'Raccourcis clavier',
+ 'Open board switcher' => 'Ouvrir le sélecteur de tableau',
+ 'Application' => 'Application',
);
diff --git a/app/Locale/hu_HU/translations.php b/app/Locale/hu_HU/translations.php
index 3ca6cf28..ea4b36b1 100644
--- a/app/Locale/hu_HU/translations.php
+++ b/app/Locale/hu_HU/translations.php
@@ -1,23 +1,23 @@
<?php
return array(
- 'None' => 'Semelyik',
+ 'None' => 'Nincs',
'edit' => 'szerkesztés',
'Edit' => 'Szerkesztés',
- 'remove' => 'eltávolít',
- 'Remove' => 'Eltávolít',
+ 'remove' => 'eltávolítás',
+ 'Remove' => 'Eltávolítás',
'Update' => 'Frissítés',
'Yes' => 'Igen',
- 'No' => 'Nincs',
- 'cancel' => 'mégsem',
+ 'No' => 'Nem',
+ 'cancel' => 'Mégsem',
'or' => 'vagy',
- 'Yellow' => 'sárga',
- 'Blue' => 'kék',
- 'Green' => 'zöld',
- 'Purple' => 'ibolya',
- 'Red' => 'piros',
- 'Orange' => 'narancs',
- 'Grey' => 'szürke',
+ 'Yellow' => 'Sárga',
+ 'Blue' => 'Kék',
+ 'Green' => 'Zöld',
+ 'Purple' => 'Lila',
+ 'Red' => 'Piros',
+ 'Orange' => 'Narancs',
+ 'Grey' => 'Szürke',
'Save' => 'Mentés',
'Login' => 'Bejelentkezés',
'Official website:' => 'Hivatalos honlap:',
@@ -25,7 +25,7 @@ return array(
'View this task' => 'Feladat megtekintése',
'Remove user' => 'Felhasználó törlése',
'Do you really want to remove this user: "%s"?' => 'Tényleg törli ezt a felhasználót: "%s"?',
- 'New user' => 'új felhasználó',
+ 'New user' => 'Új felhasználó',
'All users' => 'Minden felhasználó',
'Username' => 'Felhasználónév',
'Password' => 'Jelszó',
@@ -56,17 +56,17 @@ return array(
'Active' => 'Aktív',
'Column %d' => 'Oszlop %d',
'Add this column' => 'Oszlop hozzáadása',
- '%d tasks on the board' => 'A táblán %d feladat',
+ '%d tasks on the board' => '%d feladat a táblán',
'%d tasks in total' => 'Összesen %d feladat',
'Unable to update this board.' => 'Nem lehet frissíteni a táblát.',
'Edit board' => 'Tábla szerkesztése',
- 'Disable' => 'Letilt',
- 'Enable' => 'Engedélyez',
+ 'Disable' => 'Letiltás',
+ 'Enable' => 'Engedélyezés',
'New project' => 'Új projekt',
'Do you really want to remove this project: "%s"?' => 'Valóban törölni akarja ezt a projektet: "%s"?',
'Remove project' => 'Projekt törlése',
'Boards' => 'Táblák',
- 'Edit the board for "%s"' => 'Tábla szerkesztése "%s"',
+ 'Edit the board for "%s"' => 'Tábla szerkesztése: "%s"',
'All projects' => 'Minden projekt',
'Change columns' => 'Oszlop módosítása',
'Add a new column' => 'Új oszlop',
@@ -92,7 +92,7 @@ return array(
'(VACUUM command)' => '(VACUUM parancs)',
'(Gzip compressed Sqlite file)' => '(Gzip tömörített SQLite fájl)',
'User settings' => 'Felhasználói beállítások',
- 'My default project:' => 'Alapértelmezett project:',
+ 'My default project:' => 'Alapértelmezett projekt: ',
'Close a task' => 'Feladat lezárása',
'Do you really want to close this task: "%s"?' => 'Tényleg le akarja zárni ezt a feladatot: "%s"?',
'Edit a task' => 'Feladat módosítása',
@@ -100,17 +100,17 @@ return array(
'Color' => 'Szín',
'Assignee' => 'Felelős',
'Create another task' => 'Új feladat létrehozása',
- 'New task' => 'új feladat',
- 'Open a task' => 'Feladat megnyitása',
+ 'New task' => 'Új feladat',
+ 'Open a task' => 'Feladat felnyitás',
'Do you really want to open this task: "%s"?' => 'Tényleg meg akarja nyitni ezt a feladatot: "%s"?',
'Back to the board' => 'Vissza a táblához',
- 'Created on %B %e, %Y at %k:%M %p' => 'Létrehozva: %Y.%m.%d %k:%M %p',
+ 'Created on %B %e, %Y at %k:%M %p' => 'Létrehozva: %Y.%m.%d %H:%M',
'There is nobody assigned' => 'Nincs felelős',
- 'Column on the board:' => 'Tábla oszlopa:',
- 'Status is open' => 'Állapot nyitva',
- 'Status is closed' => 'Állapot zárva',
- 'Close this task' => 'Feladat bezárása',
- 'Open this task' => 'Feladat megnyitása',
+ 'Column on the board:' => 'Tábla oszlopa: ',
+ 'Status is open' => 'Nyitott állapot',
+ 'Status is closed' => 'Zárt állapot',
+ 'Close this task' => 'Feladat lezárása',
+ 'Open this task' => 'Feladat felnyitása',
'There is no description.' => 'Nincs elérhető leírás.',
'Add a new task' => 'Új feladat hozzáadása',
'The username is required' => 'Felhasználói név szükséges',
@@ -133,38 +133,38 @@ return array(
'The title is required' => 'A címet meg kell adni',
'The language is required' => 'A nyelvet meg kell adni',
'There is no active project, the first step is to create a new project.' => 'Nincs aktív projekt. Először létre kell hozni egy projektet.',
- 'Settings saved successfully.' => 'A beállítások sikeresen mentve.',
- 'Unable to save your settings.' => 'Beállítások mentése nem sikerült.',
+ 'Settings saved successfully.' => 'A beállítások mentése sikeres.',
+ 'Unable to save your settings.' => 'A beállítások mentése sikertelen.',
'Database optimization done.' => 'Adatbázis optimalizálás kész.',
- 'Your project have been created successfully.' => 'A projekt sikeresen elkészült.',
- 'Unable to create your project.' => 'Projekt létrehozása nem sikerült.',
- 'Project updated successfully.' => 'Projekt sikeres frissítve.',
- 'Unable to update this project.' => 'Projekt frissítése nem sikerült.',
- 'Unable to remove this project.' => 'Projekt törlése nem sikerült.',
+ 'Your project have been created successfully.' => 'Projekt sikeresen létrehozva',
+ 'Unable to create your project.' => 'Projekt létrehozása sikertelen.',
+ 'Project updated successfully.' => 'Projekt frissítése sikeres.',
+ 'Unable to update this project.' => 'Projekt frissítése sikertelen.',
+ 'Unable to remove this project.' => 'Projekt törlése sikertelen.',
'Project removed successfully.' => 'Projekt sikeresen törölve.',
- 'Project activated successfully.' => 'Projekt sikeresen aktiválta.',
- 'Unable to activate this project.' => 'Projekt aktiválása nem sikerült.',
+ 'Project activated successfully.' => 'Projekt sikeresen aktiválva.',
+ 'Unable to activate this project.' => 'Projekt aktiválása sikertelen.',
'Project disabled successfully.' => 'Projekt sikeresen letiltva.',
- 'Unable to disable this project.' => 'Projekt letiltása nem sikerült.',
- 'Unable to open this task.' => 'A feladat megnyitása nem sikerült.',
+ 'Unable to disable this project.' => 'Projekt letiltása sikertelen.',
+ 'Unable to open this task.' => 'A feladat felnyitása nem sikerült.',
'Task opened successfully.' => 'Feladat sikeresen megnyitva .',
- 'Unable to close this task.' => 'A feladat lezárása nem sikerült.',
+ 'Unable to close this task.' => 'A feladat lezárása sikertelen.',
'Task closed successfully.' => 'Feladat sikeresen lezárva.',
- 'Unable to update your task.' => 'A feladat frissítése nem sikerült.',
+ 'Unable to update your task.' => 'Feladat frissítése sikertelen.',
'Task updated successfully.' => 'Feladat sikeresen frissítve.',
- 'Unable to create your task.' => 'A feladat létrehozása nem sikerült.',
+ 'Unable to create your task.' => 'Feladat létrehozása sikertelen.',
'Task created successfully.' => 'Feladat sikeresen létrehozva.',
- 'User created successfully.' => 'Felhasználó létrehozva .',
- 'Unable to create your user.' => 'Felhasználó létrehozása nem sikerült.',
+ 'User created successfully.' => 'Felhasználó létrehozva.',
+ 'Unable to create your user.' => 'Felhasználó létrehozása sikertelen.',
'User updated successfully.' => 'Felhasználó sikeresen frissítve.',
- 'Unable to update your user.' => 'Felhasználó frissítése nem sikerült.',
+ 'Unable to update your user.' => 'Felhasználó frissítése sikertelen.',
'User removed successfully.' => 'Felhasználó sikeresen törölve.',
- 'Unable to remove this user.' => 'Felhasználó törlése nem sikerült.',
+ 'Unable to remove this user.' => 'Felhasználó törlése sikertelen.',
'Board updated successfully.' => 'Tábla sikeresen frissítve.',
- 'Ready' => 'Kész',
+ 'Ready' => 'Előkészítés',
'Backlog' => 'Napló',
- 'Work in progress' => 'Dolgozom',
- 'Done' => 'Csinált',
+ 'Work in progress' => 'Folyamatban',
+ 'Done' => 'Kész',
'Application version:' => 'Alkalmazás verzió:',
'Completed on %B %e, %Y at %k:%M %p' => 'Elkészült %Y.%m.%d %H:%M ..',
'%B %e, %Y at %k:%M %p' => '%Y.%m.%d %H:%M',
@@ -174,7 +174,7 @@ return array(
'No task' => 'Nincs feladat',
'Completed tasks' => 'Elvégzett feladatok',
'List of projects' => 'Projektek listája',
- 'Completed tasks for "%s"' => 'Elvégzett feladatok "%s"',
+ 'Completed tasks for "%s"' => 'Elvégzett feladatok: %s',
'%d closed tasks' => '%d lezárt feladat',
'No task for this project' => 'Nincs feladat ebben a projektben',
'Public link' => 'Nyilvános link',
@@ -213,14 +213,14 @@ return array(
'Invalid date' => 'Érvénytelen dátum',
'Must be done before %B %e, %Y' => 'Kész kell lennie %Y.%m.%d előtt',
'%B %e, %Y' => '%Y.%m.%d',
- // '%b %e, %Y' => '',
+ '%b %e, %Y' => '%Y.%m.%d',
'Automatic actions' => 'Automatikus intézkedések',
'Your automatic action have been created successfully.' => 'Az automatikus intézkedés sikeresen elkészült.',
'Unable to create your automatic action.' => 'Automatikus intézkedés létrehozása nem lehetséges.',
'Remove an action' => 'Intézkedés törlése',
'Unable to remove this action.' => 'Intézkedés törlése nem lehetséges.',
'Action removed successfully.' => 'Intézkedés sikeresen törölve.',
- 'Automatic actions for the project "%s"' => 'Automatikus intézkedések a projektben "%s"',
+ 'Automatic actions for the project "%s"' => 'Automatikus intézkedések a projektben: "%s"',
'Defined actions' => 'Intézkedések',
'Add an action' => 'Intézkedés létrehozása',
'Event name' => 'Esemény neve',
@@ -242,15 +242,15 @@ return array(
'Move a task to another position in the same column' => 'Feladat mozgatása oszlopon belül',
'Task modification' => 'Feladat módosítása',
'Task creation' => 'Feladat létrehozása',
- 'Open a closed task' => 'Lezárt feladat megnyitása',
+ 'Open a closed task' => 'Lezárt feladat felnyitása',
'Closing a task' => 'Feladat lezárása',
'Assign a color to a specific user' => 'Szín hozzárendelése a felhasználóhoz',
'Column title' => 'Oszlopfejléc',
'Position' => 'Pozíció',
'Move Up' => 'Fel',
'Move Down' => 'Le',
- 'Duplicate to another project' => 'Másold egy másik projektbe',
- 'Duplicate' => 'Másolat',
+ 'Duplicate to another project' => 'Másolás másik projektbe',
+ 'Duplicate' => 'Másolás',
'link' => 'link',
'Update this comment' => 'Hozzászólás frissítése',
'Comment updated successfully.' => 'Megjegyzés sikeresen frissítve.',
@@ -261,7 +261,7 @@ return array(
'Do you really want to remove this comment?' => 'Valóban törölni szeretné ezt a megjegyzést?',
'Only administrators or the creator of the comment can access to this page.' => 'Csak a rendszergazdák és a megjegyzés létrehozója férhet hozzá az oldalhoz.',
'Details' => 'Részletek',
- 'Current password for the user "%s"' => 'Felhasználó jelenlegi jelszava "%s"',
+ 'Current password for the user "%s"' => 'Felhasználó jelenlegi jelszava: "%s"',
'The current password is required' => 'A jelenlegi jelszót meg kell adni',
'Wrong password' => 'Hibás jelszó',
'Reset all tokens' => 'Reseteld az összes tokent',
@@ -279,14 +279,14 @@ return array(
'Creation date' => 'Létrehozás dátuma',
'Filter by user' => 'Szűrés felhasználó szerint',
'Filter by due date' => 'Szűrés határidő szerint',
- 'Everybody' => 'Mindenki',
+ 'Everybody' => 'Minden felhasználó',
'Open' => 'Nyitott',
'Closed' => 'Lezárt',
- 'Search' => 'Keres',
- 'Nothing found.' => 'Semmit sem találtam.',
- 'Search in the project "%s"' => 'Keresés a projektben "%s"',
+ 'Search' => 'Keresés',
+ 'Nothing found.' => 'Nincs találat.',
+ 'Search in the project "%s"' => 'Keresés a projektben: "%s"',
'Due date' => 'Határidő',
- 'Others formats accepted: %s and %s' => 'Egyéb érvényes formátumok: %s és %s',
+ 'Others formats accepted: %s and %s' => 'Egyéb érvényes formátumok: "%s" és "%s"',
'Description' => 'Leírás',
'%d comments' => '%d megjegyzés',
'%d comment' => '%d megjegyzés',
@@ -302,7 +302,7 @@ return array(
'Login with my Google Account' => 'Jelentkezzen be Google fiókkal',
'Project not found.' => 'A projekt nem található.',
'Task #%d' => 'Feladat #%d.',
- 'Task removed successfully.' => 'Feladat törlése sikerült.',
+ 'Task removed successfully.' => 'Feladat sikeresen törölve.',
'Unable to remove this task.' => 'A feladatot nem lehet törölni.',
'Remove a task' => 'Feladat törlése',
'Do you really want to remove this task: "%s"?' => 'Valóban törölni akarja ezt a feladatot: "%s"?',
@@ -313,7 +313,7 @@ return array(
'Category:' => 'Kategória:',
'Categories' => 'Kategóriák',
'Category not found.' => 'Kategória nem található.',
- 'Your category have been created successfully.' => 'Kategória sikeresen létrejött.',
+ 'Your category have been created successfully.' => 'Kategória sikeresen létrehozva.',
'Unable to create your category.' => 'A kategória létrehozása nem lehetséges.',
'Your category have been updated successfully.' => 'Kategória sikeresen frissítve.',
'Unable to update your category.' => 'Kategória frissítése nem lehetséges.',
@@ -324,14 +324,14 @@ return array(
'Category Name' => 'Kategória neve',
'Categories for the project "%s"' => 'Projekt kategóriák "%s"',
'Add a new category' => 'Új kategória',
- 'Do you really want to remove this category: "%s"?' => 'Valóban törölni akarja ezt a kategóriát "%s"?',
- 'Filter by category' => 'Szűrés kategóriára',
+ 'Do you really want to remove this category: "%s"?' => 'Valóban törölni akarja ezt a kategóriát: "%s"?',
+ 'Filter by category' => 'Szűrés kategória szerint',
'All categories' => 'Minden kategória',
'No category' => 'Nincs kategória',
'The name is required' => 'A név megadása kötelező',
'Remove a file' => 'Fájl törlése',
'Unable to remove this file.' => 'Fájl törlése nem lehetséges.',
- 'File removed successfully.' => 'A fájl törlése sikerült.',
+ 'File removed successfully.' => 'Fájl sikeresen törölve.',
'Attach a document' => 'Fájl csatolása',
'Do you really want to remove this file: "%s"?' => 'Valóban törölni akarja a fájlt: "%s"?',
'open' => 'nyitott',
@@ -344,12 +344,12 @@ return array(
'Time tracking' => 'Idő követés',
'Estimate:' => 'Becsült:',
'Spent:' => 'Eltöltött:',
- 'Do you really want to remove this sub-task?' => 'Valóban törölni akarja ezt a részfeladatot "%s"?',
+ 'Do you really want to remove this sub-task?' => 'Valóban törölni akarja ezt a részfeladatot?',
'Remaining:' => 'Hátralévő:',
'hours' => 'óra',
'spent' => 'eltöltött',
'estimated' => 'becsült',
- 'Sub-Tasks' => 'részfeladatok',
+ 'Sub-Tasks' => 'Részfeladatok',
'Add a sub-task' => 'Részfeladat létrehozása',
'Original estimate' => 'Eredeti időbecslés',
'Create another sub-task' => 'További részfeladat létrehozása',
@@ -364,8 +364,8 @@ return array(
'Sub-task updated successfully.' => 'Részfeladat sikeresen frissítve.',
'Unable to update your sub-task.' => 'Részfeladat frissítése nem lehetséges.',
'Unable to create your sub-task.' => 'Részfeladat létrehozása nem lehetséges.',
- 'Sub-task added successfully.' => 'Részfeladat sikeresen létrejött.',
- 'Maximum size: ' => 'Maximális méret:',
+ 'Sub-task added successfully.' => 'Részfeladat sikeresen létrehozva.',
+ 'Maximum size: ' => 'Maximális méret: ',
'Unable to upload the file.' => 'Fájl feltöltése nem lehetséges.',
'Display another project' => 'Másik projekt megjelenítése',
'Your GitHub account was successfully linked to your profile.' => 'GitHub fiók sikeresen csatolva a profilhoz.',
@@ -377,9 +377,9 @@ return array(
'Link my GitHub Account' => 'GitHub fiók csatolása',
'Unlink my GitHub Account' => 'GitHub fiók leválasztása',
'Created by %s' => 'Készítette: %s',
- 'Last modified on %B %e, %Y at %k:%M %p' => 'Utolsó módosítás %Y.%m.%d %H:%M',
+ 'Last modified on %B %e, %Y at %k:%M %p' => 'Utolsó módosítás: %Y.%m.%d %H:%M',
'Tasks Export' => 'Feladatok exportálása',
- 'Tasks exportation for "%s"' => 'Feladatok exportálása "%s" részére',
+ 'Tasks exportation for "%s"' => 'Feladatok exportálása: "%s"',
'Start Date' => 'Kezdés dátuma',
'End Date' => 'Befejezés dátuma',
'Execute' => 'Végrehajt',
@@ -390,31 +390,31 @@ return array(
'Webhook URL for task creation' => 'Webhook URL a feladat létrehozásakor',
'Webhook URL for task modification' => 'Webhook URL a feladatot módosításakor',
'Clone' => 'Másolat',
- 'Clone Project' => 'Projekt megkettőzése',
- 'Project cloned successfully.' => 'A projekt sikeresen megkettőzve.',
- 'Unable to clone this project.' => 'Projekt megkettőzése nem sikerült.',
+ 'Clone Project' => 'Projekt másolása',
+ 'Project cloned successfully.' => 'A projekt másolása sikeres',
+ 'Unable to clone this project.' => 'A projekt másolása sikertelen.',
'Email notifications' => 'E-mail értesítések',
- 'Enable email notifications' => 'Engedélyezze az e-mail értesítéseket',
+ 'Enable email notifications' => 'E-mail értesítések engedélyezése',
'Task position:' => 'Feladat helye:',
'The task #%d have been opened.' => 'Feladat #%d megnyitva.',
'The task #%d have been closed.' => 'Feladat #%d lezárva.',
'Sub-task updated' => 'Részfeladat frissítve',
'Title:' => 'Cím',
- 'Status:' => 'Állapot',
+ 'Status:' => 'Állapot:',
'Assignee:' => 'Felelős:',
'Time tracking:' => 'Idő követés:',
'New sub-task' => 'Új részfeladat',
'New attachment added "%s"' => 'Új melléklet "%s" hozzáadva.',
'Comment updated' => 'Megjegyzés frissítve',
'New comment posted by %s' => 'Új megjegyzés %s',
- 'List of due tasks for the project "%s"' => 'Projekt esedékes feladatai "%s"',
- // 'New attachment' => '',
- // 'New comment' => '',
- // 'New subtask' => '',
- // 'Subtask updated' => '',
- // 'Task updated' => '',
- // 'Task closed' => '',
- // 'Task opened' => '',
+ 'List of due tasks for the project "%s"' => 'Projekt esedékes feladatai: "%s"',
+ 'New attachment' => 'Új melléklet',
+ 'New comment' => 'Új megjegyzés',
+ 'New subtask' => 'Új részfeladat',
+ 'Subtask updated' => 'Részfeladat frissítve',
+ 'Task updated' => 'Feladat frissítve',
+ 'Task closed' => 'Feladat lezárva',
+ 'Task opened' => 'Feladat megnyitva',
'[%s][Due tasks]' => '[%s] [Esedékes feladatok]',
'[Kanboard] Notification' => '[Kanboard] értesítés',
'I want to receive notifications only for those projects:' => 'Csak ezekről a projektekről kérek értesítést:',
@@ -432,27 +432,27 @@ return array(
'Do you really want to duplicate this project: "%s"?' => 'Tényleg szeretné megkettőzni ezt a projektet: "%s"',
'Do you really want to enable this project: "%s"?' => 'Tényleg szeretné engedélyezni ezt a projektet: "%s"',
'Project activation' => 'Projekt aktiválás',
- 'Move the task to another project' => 'Feladatot mozgatása másik projektbe',
- 'Move to another project' => 'Másik projektbe',
+ 'Move the task to another project' => 'Feladat áthelyezése másik projektbe',
+ 'Move to another project' => 'Áthelyezés másik projektbe',
'Do you really want to duplicate this task?' => 'Tényleg szeretné megkettőzni ezt a feladatot?',
'Duplicate a task' => 'Feladat megkettőzése',
'External accounts' => 'Külső fiókok',
- 'Account type' => 'Fiók típus',
+ 'Account type' => 'Fiók típusa',
'Local' => 'Helyi',
'Remote' => 'Távoli',
'Enabled' => 'Engedélyezve',
'Disabled' => 'Letiltva',
'Google account linked' => 'Google fiók összekapcsolva',
'Github account linked' => 'GitHub fiók összekapcsolva',
- 'Username:' => 'Felhasználónév',
- 'Name:' => 'Név',
- 'Email:' => 'E-mail',
+ 'Username:' => 'Felhasználónév:',
+ 'Name:' => 'Név:',
+ 'Email:' => 'E-mail:',
'Default project:' => 'Alapértelmezett projekt:',
'Notifications:' => 'Értesítések:',
'Notifications' => 'Értesítések',
'Group:' => 'Csoport:',
'Regular user' => 'Default User',
- 'Account type:' => 'Fiók típus:',
+ 'Account type:' => 'Fiók típusa:',
'Edit profile' => 'Profil szerkesztése',
'Change password' => 'Jelszó módosítása',
'Password modification' => 'Jelszó módosítása',
@@ -462,9 +462,9 @@ return array(
'Never connected.' => 'Sosem csatlakozva.',
'No account linked.' => 'Nincs csatlakoztatott fiók.',
'Account linked.' => 'Fiók csatlakoztatva.',
- 'No external authentication enabled.' => 'Külső azonosítás nincs engedélyezve.',
- 'Password modified successfully.' => 'Jelszó sikeresen módosítva.',
- 'Unable to change the password.' => 'Jelszó módosítás sikertelen.',
+ 'No external authentication enabled.' => 'A külső azonosítás nincs engedélyezve.',
+ 'Password modified successfully.' => 'A jelszó sikeresen módosítva.',
+ 'Unable to change the password.' => 'A jelszó módosítása sikertelen.',
'Change category for the task "%s"' => 'Feladat kategória módosítása "%s"',
'Change category' => 'Kategória módosítása',
'%s updated the task %s' => '%s frissítette a feladatot %s',
@@ -479,7 +479,7 @@ return array(
'Not assigned, estimate of %sh' => 'Nincs kiosztva, becsült idő: %s óra',
'%s updated a comment on the task %s' => '%s frissítette a megjegyzését a feladatban %s',
'%s commented the task %s' => '%s megjegyzést fűzött a feladathoz %s',
- '%s\'s activity' => '%s tevékenysége',
+ '%s\'s activity' => 'Tevékenységek: %s',
'No activity.' => 'Nincs tevékenység.',
'RSS feed' => 'RSS feed',
'%s updated a comment on the task #%d' => '%s frissített egy megjegyzést a feladatban #%d',
@@ -492,20 +492,20 @@ return array(
'%s open the task #%d' => '%s megnyitotta a feladatot #%d',
'%s moved the task #%d to the column "%s"' => '%s átmozgatta a feladatot #%d a "%s" oszlopba',
'%s moved the task #%d to the position %d in the column "%s"' => '%s átmozgatta a feladatot #%d a %d pozícióba a "%s" oszlopban',
- 'Activity' => 'Tevékenység',
- 'Default values are "%s"' => 'Az alapértelmezett értékek "%s"',
+ 'Activity' => 'Tevékenységek',
+ 'Default values are "%s"' => 'Az alapértelmezett értékek: %s',
'Default columns for new projects (Comma-separated)' => 'Alapértelmezett oszlopok az új projektekben (vesszővel elválasztva)',
'Task assignee change' => 'Felelős módosítása',
'%s change the assignee of the task #%d to %s' => '%s a felelőst módosította #%d %s',
'%s changed the assignee of the task %s to %s' => '%s a felelőst %s módosította: %s',
- // 'Column Change' => '',
- // 'Position Change' => '',
- // 'Assignee Change' => '',
- 'New password for the user "%s"' => 'Felhasználó új jelszava "%s"',
+ 'Column Change' => 'Oszlop változtatás',
+ 'Position Change' => 'Pozíció változtatás',
+ 'Assignee Change' => 'Felelős változtatás',
+ 'New password for the user "%s"' => 'Felhasználó új jelszava: %s',
'Choose an event' => 'Válasszon eseményt',
'Github commit received' => 'GitHub commit érkezett',
- 'Github issue opened' => 'GitHub issue nyílt',
- 'Github issue closed' => 'GitHub issue zárt',
+ 'Github issue opened' => 'GitHub issue nyitás',
+ 'Github issue closed' => 'GitHub issue zárás',
'Github issue reopened' => 'GitHub issue újranyitva',
'Github issue assignee change' => 'GitHub issue felelős változás',
'Github issue label change' => 'GitHub issue címke változás',
@@ -522,23 +522,23 @@ return array(
'URL and token' => 'URL és tokenek',
'Webhook settings' => 'Webhook beállítások',
'URL for task creation:' => 'Feladat létrehozás URL:',
- 'Reset token' => 'Reset token',
- 'API endpoint:' => 'API endpoint:',
+ 'Reset token' => 'Token újragenerálása',
+ 'API endpoint:' => 'API végpont:',
'Refresh interval for private board' => 'Privát táblák frissítési intervalluma',
'Refresh interval for public board' => 'Nyilvános táblák frissítési intervalluma',
'Task highlight period' => 'Feladat kiemelés időtartama',
'Period (in second) to consider a task was modified recently (0 to disable, 2 days by default)' => 'Mennyi ideig tekintendő egy feladat "mostanában" módosítottnak (másodpercben) (0: funkció letiltva, alapértelmezés szerint 2 nap)',
- 'Frequency in second (60 seconds by default)' => 'Infó másodpercben (alapértelmezett 60 másodperc)',
- 'Frequency in second (0 to disable this feature, 10 seconds by default)' => 'Infó másodpercben (0 funkció letiltva, alapértelmezés szerint 10 másodperc)',
+ 'Frequency in second (60 seconds by default)' => 'Gyakoriság másodpercben (alapértelmezetten 60 másodperc)',
+ 'Frequency in second (0 to disable this feature, 10 seconds by default)' => 'Gyakoriság másodpercben (0 funkció letiltva, alapértelmezetten 10 másodperc)',
'Application URL' => 'Alkalmazás URL',
- 'Example: http://example.kanboard.net/ (used by email notifications)' => 'Példa: http://example.kanboard.net/ (e-mail értesítőben)',
+ 'Example: http://example.kanboard.net/ (used by email notifications)' => 'Példa: http://example.kanboard.net/ (e-mail értesítőben használt)',
'Token regenerated.' => 'Token újragenerálva.',
'Date format' => 'Dátum formátum',
'ISO format is always accepted, example: "%s" and "%s"' => 'ISO formátum mindig elfogadott, pl: "%s" és "%s"',
'New private project' => 'Új privát projekt',
'This project is private' => 'Ez egy privát projekt',
'Type here to create a new sub-task' => 'Ide írva létrehozhat egy új részfeladatot',
- 'Add' => 'Hozzáad',
+ 'Add' => 'Hozzáadás',
'Estimated time: %s hours' => 'Becsült idő: %s óra',
'Time spent: %s hours' => 'Eltöltött idő: %s óra',
'Started on %B %e, %Y' => 'Elkezdve: %Y.%m.%d',
@@ -547,9 +547,9 @@ return array(
'There is nothing assigned to you.' => 'Nincs kiosztott feladat.',
'My tasks' => 'Feladataim',
'Activity stream' => 'Legutóbbi tevékenységek',
- 'Dashboard' => 'Műszerfal',
+ 'Dashboard' => 'Vezérlőpult',
'Confirmation' => 'Megerősítés',
- 'Allow everybody to access to this project' => 'Engedélyezze a projekt elérését mindenkinek',
+ 'Allow everybody to access to this project' => 'A projekt elérése mindenkinek engedélyezett',
'Everybody have access to this project.' => 'Mindenki elérheti a projektet',
'Webhooks' => 'Webhook',
'API' => 'API',
@@ -558,7 +558,7 @@ return array(
'Help on Github webhooks' => 'Github Webhook súgó',
'Create a comment from an external provider' => 'Megjegyzés létrehozása külső felhasználótól',
'Github issue comment created' => 'Github issue megjegyzés létrehozva',
- 'Configure' => 'Konfigurál',
+ 'Configure' => 'Beállítások',
'Project management' => 'Projekt menedzsment',
'My projects' => 'Projektjeim',
'Columns' => 'Oszlopok',
@@ -568,12 +568,12 @@ return array(
'Number of tasks' => 'A feladatok száma',
'Task distribution' => 'Feladatelosztás',
'Reportings' => 'Jelentések',
- 'Task repartition for "%s"' => 'Feladat újraosztása "%s" számára',
+ 'Task repartition for "%s"' => 'Feladat újraosztása: %s',
'Analytics' => 'Analitika',
'Subtask' => 'Részfeladat',
'My subtasks' => 'Részfeladataim',
'User repartition' => 'Felhasználó újrafelosztás',
- 'User repartition for "%s"' => 'Felhasználó újrafelosztás "%s" számára',
+ 'User repartition for "%s"' => 'Felhasználó újrafelosztás: %s',
'Clone this project' => 'Projekt megkettőzése',
'Column removed successfully.' => 'Oszlop sikeresen eltávolítva.',
'Edit Project' => 'Projekt szerkesztése',
@@ -592,98 +592,143 @@ return array(
'This value must be numeric' => 'Ez a mező csak szám lehet',
'Unable to create this task.' => 'A feladat nem hozható létre,',
'Cumulative flow diagram' => 'Kumulatív Flow Diagram',
- 'Cumulative flow diagram for "%s"' => 'Kumulatív Flow Diagram "%s" számára',
+ 'Cumulative flow diagram for "%s"' => 'Kumulatív Flow Diagram: %s',
'Daily project summary' => 'Napi projektösszefoglaló',
'Daily project summary export' => 'Napi projektösszefoglaló exportálása',
- 'Daily project summary export for "%s"' => 'Napi projektösszefoglaló exportálása "%s" számára',
+ 'Daily project summary export for "%s"' => 'Napi projektösszefoglaló exportálása: %s',
'Exports' => 'Exportálások',
'This export contains the number of tasks per column grouped per day.' => 'Ez az export tartalmazza a feladatok számát oszloponként összesítve, napokra lebontva.',
'Nothing to preview...' => 'Nincs semmi az előnézetben ...',
'Preview' => 'Előnézet',
- 'Write' => 'Írd',
- // 'Active swimlanes' => '',
- // 'Add a new swimlane' => '',
- // 'Change default swimlane' => '',
- // 'Default swimlane' => '',
- // 'Do you really want to remove this swimlane: "%s"?' => '',
- // 'Inactive swimlanes' => '',
- // 'Set project manager' => '',
- // 'Set project member' => '',
- // 'Remove a swimlane' => '',
- // 'Rename' => '',
- // 'Show default swimlane' => '',
- // 'Swimlane modification for the project "%s"' => '',
- // 'Swimlane not found.' => '',
- // 'Swimlane removed successfully.' => '',
- // 'Swimlanes' => '',
- // 'Swimlane updated successfully.' => '',
- // 'The default swimlane have been updated successfully.' => '',
- // 'Unable to create your swimlane.' => '',
- // 'Unable to remove this swimlane.' => '',
- // 'Unable to update this swimlane.' => '',
- // 'Your swimlane have been created successfully.' => '',
- // 'Example: "Bug, Feature Request, Improvement"' => '',
- // 'Default categories for new projects (Comma-separated)' => '',
- // 'Gitlab commit received' => '',
- // 'Gitlab issue opened' => '',
- // 'Gitlab issue closed' => '',
- // 'Gitlab webhooks' => '',
- // 'Help on Gitlab webhooks' => '',
- // 'Integrations' => '',
- // 'Integration with third-party services' => '',
- // 'Role for this project' => '',
- // 'Project manager' => '',
- // 'Project member' => '',
- // 'A project manager can change the settings of the project and have more privileges than a standard user.' => '',
- // 'Gitlab Issue' => '',
- // 'Subtask Id' => '',
- // 'Subtasks' => '',
- // 'Subtasks Export' => '',
- // 'Subtasks exportation for "%s"' => '',
- // 'Task Title' => '',
- // 'Untitled' => '',
- // 'Application default' => '',
- // 'Language:' => '',
- // 'Timezone:' => '',
- // 'All columns' => '',
- // 'Calendar for "%s"' => '',
- // 'Filter by column' => '',
- // 'Filter by status' => '',
- // 'Calendar' => '',
- // 'Next' => '',
- // '#%d' => '',
- // 'Filter by color' => '',
- // 'Filter by swimlane' => '',
- // 'All swimlanes' => '',
- // 'All colors' => '',
- // 'All status' => '',
- // 'Add a comment logging moving the task between columns' => '',
- // 'Moved to column %s' => '',
- // 'Change description' => '',
- // 'User dashboard' => '',
- // 'Allow only one subtask in progress at the same time for a user' => '',
- // 'Edit column "%s"' => '',
- // 'Enable time tracking for subtasks' => '',
- // 'Select the new status of the subtask: "%s"' => '',
- // 'Subtask timesheet' => '',
- // 'There is nothing to show.' => '',
- // 'Time Tracking' => '',
- // 'You already have one subtask in progress' => '',
- // 'Which parts of the project do you want to duplicate?' => '',
- // 'Change dashboard view' => '',
- // 'Show/hide activities' => '',
- // 'Show/hide projects' => '',
- // 'Show/hide subtasks' => '',
- // 'Show/hide tasks' => '',
- // 'Disable login form' => '',
- // 'Show/hide calendar' => '',
- // 'User calendar' => '',
- // 'Bitbucket commit received' => '',
- // 'Bitbucket webhooks' => '',
- // 'Help on Bitbucket webhooks' => '',
- // 'Start' => '',
- // 'End' => '',
- // 'Task age in days' => '',
- // 'Days in this column' => '',
- // '%dd' => '',
+ 'Write' => 'Szerkesztés',
+ 'Active swimlanes' => 'Aktív folyamatok',
+ 'Add a new swimlane' => 'Új folyamat',
+ 'Change default swimlane' => 'Alapértelmezett folyamat változtatás',
+ 'Default swimlane' => 'Alapértelmezett folyamat',
+ 'Do you really want to remove this swimlane: "%s"?' => 'Valóban törli a folyamatot:%s ?',
+ 'Inactive swimlanes' => 'Inaktív folyamatok',
+ 'Set project manager' => 'Beállítás projekt kezelőnek',
+ 'Set project member' => 'Beállítás projekt felhasználónak',
+ 'Remove a swimlane' => 'Folyamat törlés',
+ 'Rename' => 'Átnevezés',
+ 'Show default swimlane' => 'Alapértelmezett folyamat megjelenítése',
+ 'Swimlane modification for the project "%s"' => '%s projekt folyamatainak módosítása',
+ 'Swimlane not found.' => 'Folyamat nem található',
+ 'Swimlane removed successfully.' => 'Folyamat sikeresen törölve.',
+ 'Swimlanes' => 'Folyamatok',
+ 'Swimlane updated successfully.' => 'Folyamat sikeresn frissítve',
+ 'The default swimlane have been updated successfully.' => 'Az alapértelmezett folyamat sikeresen frissítve.',
+ 'Unable to create your swimlane.' => 'A folyamat létrehozása sikertelen.',
+ 'Unable to remove this swimlane.' => 'A folyamat törlése sikertelen.',
+ 'Unable to update this swimlane.' => 'A folyamat frissítése sikertelen.',
+ 'Your swimlane have been created successfully.' => 'A folyamat sikeresen létrehozva.',
+ 'Example: "Bug, Feature Request, Improvement"' => 'Például: Hiba, Új funkció, Fejlesztés',
+ 'Default categories for new projects (Comma-separated)' => 'Alapértelmezett kategóriák az új projektekben (Vesszővel elválasztva)',
+ 'Gitlab commit received' => 'Gitlab commit érkezett',
+ 'Gitlab issue opened' => 'Gitlab issue nyitás',
+ 'Gitlab issue closed' => 'Gitlab issue zárás',
+ 'Gitlab webhooks' => 'Gitlab webhooks',
+ 'Help on Gitlab webhooks' => 'Gitlab webhooks súgó',
+ 'Integrations' => 'Integráció',
+ 'Integration with third-party services' => 'Integráció harmadik féllel',
+ 'Role for this project' => 'Projekt szerepkör',
+ 'Project manager' => 'Projekt kezelő',
+ 'Project member' => 'Projekt felhasználó',
+ 'A project manager can change the settings of the project and have more privileges than a standard user.' => 'A projekt kezelő képes megváltoztatni a projekt beállításait és több joggal rendelkezik mint az alap felhasználók.',
+ 'Gitlab Issue' => 'Gitlab issue',
+ 'Subtask Id' => 'Részfeladat id',
+ 'Subtasks' => 'Részfeladatok',
+ 'Subtasks Export' => 'Részfeladat exportálás',
+ 'Subtasks exportation for "%s"' => 'Részfeladatok exportálása: %s',
+ 'Task Title' => 'Feladat címe',
+ 'Untitled' => 'Névtelen',
+ 'Application default' => 'Alkalmazás alapértelmezett',
+ 'Language:' => 'Nyelv:',
+ 'Timezone:' => 'Időzóna:',
+ 'All columns' => 'Minden oszlop',
+ 'Calendar for "%s"' => 'Naptár: %s',
+ 'Filter by column' => 'Szűrés oszlop szerint',
+ 'Filter by status' => 'Szűrés állapot szerint',
+ 'Calendar' => 'Naptár',
+ 'Next' => 'Következő',
+ '#%d' => '#%d',
+ 'Filter by color' => 'Szűrés szín szerint',
+ 'Filter by swimlane' => 'Szűrés folyamat szerint',
+ 'All swimlanes' => 'Minden folyamat',
+ 'All colors' => 'Minden szín',
+ 'All status' => 'Minden állapot',
+ 'Add a comment logging moving the task between columns' => 'Feladat oszlopok közötti mozgatását megjegyzésben feltüntetni',
+ 'Moved to column %s' => '%s oszlopba áthelyezve',
+ 'Change description' => 'Leírás szerkesztés',
+ 'User dashboard' => 'Felhasználói vezérlőpult',
+ 'Allow only one subtask in progress at the same time for a user' => 'Egyszerre csak egy folyamatban levő részfeladat engedélyezése a felhasználóknak',
+ 'Edit column "%s"' => 'Oszlop szerkesztés: %s',
+ 'Enable time tracking for subtasks' => 'Idő követés engedélyezése a részfeladatokhoz',
+ 'Select the new status of the subtask: "%s"' => 'Részfeladat állapot változtatás: %s',
+ 'Subtask timesheet' => 'Részfeladat idővonal',
+ 'There is nothing to show.' => 'Nincs megjelenítendő adat.',
+ 'Time Tracking' => 'Idő követés',
+ 'You already have one subtask in progress' => 'Már van egy folyamatban levő részfeladata',
+ 'Which parts of the project do you want to duplicate?' => 'A projekt mely részeit szeretné duplikálni?',
+ 'Change dashboard view' => 'Vezérlőpult megjelenítés változtatás',
+ 'Show/hide activities' => 'Tevékenységek megjelenítése/elrejtése',
+ 'Show/hide projects' => 'Projektek megjelenítése/elrejtése',
+ 'Show/hide subtasks' => 'Részfeladatok megjelenítése/elrejtése',
+ 'Show/hide tasks' => 'Feladatok megjelenítése/elrejtése',
+ 'Disable login form' => 'Bejelentkező képernyő tiltása',
+ 'Show/hide calendar' => 'Naptár megjelenítés/elrejtés',
+ 'User calendar' => 'Naptár',
+ 'Bitbucket commit received' => 'Bitbucket commit érkezett',
+ 'Bitbucket webhooks' => 'Bitbucket webhooks',
+ 'Help on Bitbucket webhooks' => 'Bitbucket webhooks súgó',
+ 'Start' => 'Kezdet',
+ 'End' => 'Vég',
+ 'Task age in days' => 'Feladat életkora napokban',
+ 'Days in this column' => 'Napok ebben az oszlopban',
+ '%dd' => '%dd',
+ 'Add a link' => 'Hivatkozás hozzáadása',
+ 'Add a new link' => 'Új hivatkozás hozzáadása',
+ 'Do you really want to remove this link: "%s"?' => 'Biztos törölni akarja a hivatkozást: "%s"?',
+ 'Do you really want to remove this link with task #%d?' => 'Biztos törölni akarja a(z) #%s. feladatra mutató hivatkozást?',
+ 'Field required' => 'Kötelező mező',
+ 'Link added successfully.' => 'Hivatkozás sikeresen létrehozva.',
+ 'Link updated successfully.' => 'Hivatkozás sikeresen frissítve.',
+ 'Link removed successfully.' => 'Hivatkozás sikeresen törölve.',
+ 'Link labels' => 'Hivatkozás címkék',
+ 'Link modification' => 'Hivatkozás módosítás',
+ 'Links' => 'Hivatkozások',
+ 'Link settings' => 'Hivatkozás beállítasok',
+ 'Opposite label' => 'Ellenekező címke',
+ 'Remove a link' => 'Hivatkozás törlése',
+ 'Task\'s links' => 'Feladat hivatkozások',
+ 'The labels must be different' => 'A címkék nem lehetnek azonosak',
+ 'There is no link.' => 'Nincs hivatkozás.',
+ 'This label must be unique' => 'A címkének egyedinek kell lennie.',
+ 'Unable to create your link.' => 'Hivatkozás létrehozása sikertelen.',
+ 'Unable to update your link.' => 'Hivatkozás frissítése sikertelen.',
+ 'Unable to remove this link.' => 'Hivatkozás törlése sikertelen.',
+ 'relates to' => 'hozzá tartozik:',
+ 'blocks' => 'letiltva:',
+ 'is blocked by' => 'letitoltta:',
+ 'duplicates' => 'eredeti:',
+ 'is duplicated by' => 'másolat:',
+ 'is a child of' => 'szülője:',
+ 'is a parent of' => 'gyermeke:',
+ 'targets milestone' => 'megcélzott mérföldkő:',
+ 'is a milestone of' => 'ehhez a mérföldkőhöz tartozik:',
+ 'fixes' => 'javítás:',
+ 'is fixed by' => 'javította:',
+ 'This task' => 'Ez a feladat',
+ '<1h' => '<1ó',
+ '%dh' => '%dó',
+ '%b %e' => '%b %e',
+ 'Expand tasks' => 'Feladatok lenyitása',
+ 'Collapse tasks' => 'Feladatok összecsukása',
+ 'Expand/collapse tasks' => 'Feladatok lenyitása/összecsukása',
+ 'Close dialog box' => 'Ablak bezárása',
+ 'Submit a form' => 'Űrlap beküldése',
+ 'Board view' => 'Tábla nézet',
+ 'Keyboard shortcuts' => 'Billentyű kombináció',
+ 'Open board switcher' => 'Tábla választó lenyitása',
+ // 'Application' => '',
);
diff --git a/app/Locale/it_IT/translations.php b/app/Locale/it_IT/translations.php
index 5c3ef47b..6b392c2c 100644
--- a/app/Locale/it_IT/translations.php
+++ b/app/Locale/it_IT/translations.php
@@ -686,4 +686,49 @@ return array(
// 'Task age in days' => '',
// 'Days in this column' => '',
// '%dd' => '',
+ // 'Add a link' => '',
+ // 'Add a new link' => '',
+ // 'Do you really want to remove this link: "%s"?' => '',
+ // 'Do you really want to remove this link with task #%d?' => '',
+ // 'Field required' => '',
+ // 'Link added successfully.' => '',
+ // 'Link updated successfully.' => '',
+ // 'Link removed successfully.' => '',
+ // 'Link labels' => '',
+ // 'Link modification' => '',
+ // 'Links' => '',
+ // 'Link settings' => '',
+ // 'Opposite label' => '',
+ // 'Remove a link' => '',
+ // 'Task\'s links' => '',
+ // 'The labels must be different' => '',
+ // 'There is no link.' => '',
+ // 'This label must be unique' => '',
+ // 'Unable to create your link.' => '',
+ // 'Unable to update your link.' => '',
+ // 'Unable to remove this link.' => '',
+ // 'relates to' => '',
+ // 'blocks' => '',
+ // 'is blocked by' => '',
+ // 'duplicates' => '',
+ // 'is duplicated by' => '',
+ // 'is a child of' => '',
+ // 'is a parent of' => '',
+ // 'targets milestone' => '',
+ // 'is a milestone of' => '',
+ // 'fixes' => '',
+ // 'is fixed by' => '',
+ // 'This task' => '',
+ // '<1h' => '',
+ // '%dh' => '',
+ // '%b %e' => '',
+ // 'Expand tasks' => '',
+ // 'Collapse tasks' => '',
+ // 'Expand/collapse tasks' => '',
+ // 'Close dialog box' => '',
+ // 'Submit a form' => '',
+ // 'Board view' => '',
+ // 'Keyboard shortcuts' => '',
+ // 'Open board switcher' => '',
+ // 'Application' => '',
);
diff --git a/app/Locale/ja_JP/translations.php b/app/Locale/ja_JP/translations.php
index 91030bee..b2f1a85f 100644
--- a/app/Locale/ja_JP/translations.php
+++ b/app/Locale/ja_JP/translations.php
@@ -686,4 +686,49 @@ return array(
// 'Task age in days' => '',
// 'Days in this column' => '',
// '%dd' => '',
+ // 'Add a link' => '',
+ // 'Add a new link' => '',
+ // 'Do you really want to remove this link: "%s"?' => '',
+ // 'Do you really want to remove this link with task #%d?' => '',
+ // 'Field required' => '',
+ // 'Link added successfully.' => '',
+ // 'Link updated successfully.' => '',
+ // 'Link removed successfully.' => '',
+ // 'Link labels' => '',
+ // 'Link modification' => '',
+ // 'Links' => '',
+ // 'Link settings' => '',
+ // 'Opposite label' => '',
+ // 'Remove a link' => '',
+ // 'Task\'s links' => '',
+ // 'The labels must be different' => '',
+ // 'There is no link.' => '',
+ // 'This label must be unique' => '',
+ // 'Unable to create your link.' => '',
+ // 'Unable to update your link.' => '',
+ // 'Unable to remove this link.' => '',
+ // 'relates to' => '',
+ // 'blocks' => '',
+ // 'is blocked by' => '',
+ // 'duplicates' => '',
+ // 'is duplicated by' => '',
+ // 'is a child of' => '',
+ // 'is a parent of' => '',
+ // 'targets milestone' => '',
+ // 'is a milestone of' => '',
+ // 'fixes' => '',
+ // 'is fixed by' => '',
+ // 'This task' => '',
+ // '<1h' => '',
+ // '%dh' => '',
+ // '%b %e' => '',
+ // 'Expand tasks' => '',
+ // 'Collapse tasks' => '',
+ // 'Expand/collapse tasks' => '',
+ // 'Close dialog box' => '',
+ // 'Submit a form' => '',
+ // 'Board view' => '',
+ // 'Keyboard shortcuts' => '',
+ // 'Open board switcher' => '',
+ // 'Application' => '',
);
diff --git a/app/Locale/pl_PL/translations.php b/app/Locale/pl_PL/translations.php
index 7acb1497..274ecb80 100644
--- a/app/Locale/pl_PL/translations.php
+++ b/app/Locale/pl_PL/translations.php
@@ -686,4 +686,49 @@ return array(
// 'Task age in days' => '',
// 'Days in this column' => '',
// '%dd' => '',
+ // 'Add a link' => '',
+ // 'Add a new link' => '',
+ // 'Do you really want to remove this link: "%s"?' => '',
+ // 'Do you really want to remove this link with task #%d?' => '',
+ // 'Field required' => '',
+ // 'Link added successfully.' => '',
+ // 'Link updated successfully.' => '',
+ // 'Link removed successfully.' => '',
+ // 'Link labels' => '',
+ // 'Link modification' => '',
+ // 'Links' => '',
+ // 'Link settings' => '',
+ // 'Opposite label' => '',
+ // 'Remove a link' => '',
+ // 'Task\'s links' => '',
+ // 'The labels must be different' => '',
+ // 'There is no link.' => '',
+ // 'This label must be unique' => '',
+ // 'Unable to create your link.' => '',
+ // 'Unable to update your link.' => '',
+ // 'Unable to remove this link.' => '',
+ // 'relates to' => '',
+ // 'blocks' => '',
+ // 'is blocked by' => '',
+ // 'duplicates' => '',
+ // 'is duplicated by' => '',
+ // 'is a child of' => '',
+ // 'is a parent of' => '',
+ // 'targets milestone' => '',
+ // 'is a milestone of' => '',
+ // 'fixes' => '',
+ // 'is fixed by' => '',
+ // 'This task' => '',
+ // '<1h' => '',
+ // '%dh' => '',
+ // '%b %e' => '',
+ // 'Expand tasks' => '',
+ // 'Collapse tasks' => '',
+ // 'Expand/collapse tasks' => '',
+ // 'Close dialog box' => '',
+ // 'Submit a form' => '',
+ // 'Board view' => '',
+ // 'Keyboard shortcuts' => '',
+ // 'Open board switcher' => '',
+ // 'Application' => '',
);
diff --git a/app/Locale/pt_BR/translations.php b/app/Locale/pt_BR/translations.php
index ac865046..7aa79c53 100644
--- a/app/Locale/pt_BR/translations.php
+++ b/app/Locale/pt_BR/translations.php
@@ -686,4 +686,49 @@ return array(
// 'Task age in days' => '',
// 'Days in this column' => '',
// '%dd' => '',
+ // 'Add a link' => '',
+ // 'Add a new link' => '',
+ // 'Do you really want to remove this link: "%s"?' => '',
+ // 'Do you really want to remove this link with task #%d?' => '',
+ // 'Field required' => '',
+ // 'Link added successfully.' => '',
+ // 'Link updated successfully.' => '',
+ // 'Link removed successfully.' => '',
+ // 'Link labels' => '',
+ // 'Link modification' => '',
+ // 'Links' => '',
+ // 'Link settings' => '',
+ // 'Opposite label' => '',
+ // 'Remove a link' => '',
+ // 'Task\'s links' => '',
+ // 'The labels must be different' => '',
+ // 'There is no link.' => '',
+ // 'This label must be unique' => '',
+ // 'Unable to create your link.' => '',
+ // 'Unable to update your link.' => '',
+ // 'Unable to remove this link.' => '',
+ // 'relates to' => '',
+ // 'blocks' => '',
+ // 'is blocked by' => '',
+ // 'duplicates' => '',
+ // 'is duplicated by' => '',
+ // 'is a child of' => '',
+ // 'is a parent of' => '',
+ // 'targets milestone' => '',
+ // 'is a milestone of' => '',
+ // 'fixes' => '',
+ // 'is fixed by' => '',
+ // 'This task' => '',
+ // '<1h' => '',
+ // '%dh' => '',
+ // '%b %e' => '',
+ // 'Expand tasks' => '',
+ // 'Collapse tasks' => '',
+ // 'Expand/collapse tasks' => '',
+ // 'Close dialog box' => '',
+ // 'Submit a form' => '',
+ // 'Board view' => '',
+ // 'Keyboard shortcuts' => '',
+ // 'Open board switcher' => '',
+ // 'Application' => '',
);
diff --git a/app/Locale/ru_RU/translations.php b/app/Locale/ru_RU/translations.php
index 624a17f1..a31cd63b 100644
--- a/app/Locale/ru_RU/translations.php
+++ b/app/Locale/ru_RU/translations.php
@@ -686,4 +686,49 @@ return array(
// 'Task age in days' => '',
// 'Days in this column' => '',
// '%dd' => '',
+ // 'Add a link' => '',
+ // 'Add a new link' => '',
+ // 'Do you really want to remove this link: "%s"?' => '',
+ // 'Do you really want to remove this link with task #%d?' => '',
+ // 'Field required' => '',
+ // 'Link added successfully.' => '',
+ // 'Link updated successfully.' => '',
+ // 'Link removed successfully.' => '',
+ // 'Link labels' => '',
+ // 'Link modification' => '',
+ // 'Links' => '',
+ // 'Link settings' => '',
+ // 'Opposite label' => '',
+ // 'Remove a link' => '',
+ // 'Task\'s links' => '',
+ // 'The labels must be different' => '',
+ // 'There is no link.' => '',
+ // 'This label must be unique' => '',
+ // 'Unable to create your link.' => '',
+ // 'Unable to update your link.' => '',
+ // 'Unable to remove this link.' => '',
+ // 'relates to' => '',
+ // 'blocks' => '',
+ // 'is blocked by' => '',
+ // 'duplicates' => '',
+ // 'is duplicated by' => '',
+ // 'is a child of' => '',
+ // 'is a parent of' => '',
+ // 'targets milestone' => '',
+ // 'is a milestone of' => '',
+ // 'fixes' => '',
+ // 'is fixed by' => '',
+ // 'This task' => '',
+ // '<1h' => '',
+ // '%dh' => '',
+ // '%b %e' => '',
+ // 'Expand tasks' => '',
+ // 'Collapse tasks' => '',
+ // 'Expand/collapse tasks' => '',
+ // 'Close dialog box' => '',
+ // 'Submit a form' => '',
+ // 'Board view' => '',
+ // 'Keyboard shortcuts' => '',
+ // 'Open board switcher' => '',
+ // 'Application' => '',
);
diff --git a/app/Locale/sv_SE/translations.php b/app/Locale/sv_SE/translations.php
index b77a8a5a..2083fc18 100644
--- a/app/Locale/sv_SE/translations.php
+++ b/app/Locale/sv_SE/translations.php
@@ -686,4 +686,49 @@ return array(
// 'Task age in days' => '',
// 'Days in this column' => '',
// '%dd' => '',
+ // 'Add a link' => '',
+ // 'Add a new link' => '',
+ // 'Do you really want to remove this link: "%s"?' => '',
+ // 'Do you really want to remove this link with task #%d?' => '',
+ // 'Field required' => '',
+ // 'Link added successfully.' => '',
+ // 'Link updated successfully.' => '',
+ // 'Link removed successfully.' => '',
+ // 'Link labels' => '',
+ // 'Link modification' => '',
+ // 'Links' => '',
+ // 'Link settings' => '',
+ // 'Opposite label' => '',
+ // 'Remove a link' => '',
+ // 'Task\'s links' => '',
+ // 'The labels must be different' => '',
+ // 'There is no link.' => '',
+ // 'This label must be unique' => '',
+ // 'Unable to create your link.' => '',
+ // 'Unable to update your link.' => '',
+ // 'Unable to remove this link.' => '',
+ // 'relates to' => '',
+ // 'blocks' => '',
+ // 'is blocked by' => '',
+ // 'duplicates' => '',
+ // 'is duplicated by' => '',
+ // 'is a child of' => '',
+ // 'is a parent of' => '',
+ // 'targets milestone' => '',
+ // 'is a milestone of' => '',
+ // 'fixes' => '',
+ // 'is fixed by' => '',
+ // 'This task' => '',
+ // '<1h' => '',
+ // '%dh' => '',
+ // '%b %e' => '',
+ // 'Expand tasks' => '',
+ // 'Collapse tasks' => '',
+ // 'Expand/collapse tasks' => '',
+ // 'Close dialog box' => '',
+ // 'Submit a form' => '',
+ // 'Board view' => '',
+ // 'Keyboard shortcuts' => '',
+ // 'Open board switcher' => '',
+ // 'Application' => '',
);
diff --git a/app/Locale/th_TH/translations.php b/app/Locale/th_TH/translations.php
index 64e0170e..42d731d0 100644
--- a/app/Locale/th_TH/translations.php
+++ b/app/Locale/th_TH/translations.php
@@ -686,4 +686,49 @@ return array(
// 'Task age in days' => '',
// 'Days in this column' => '',
// '%dd' => '',
+ // 'Add a link' => '',
+ // 'Add a new link' => '',
+ // 'Do you really want to remove this link: "%s"?' => '',
+ // 'Do you really want to remove this link with task #%d?' => '',
+ // 'Field required' => '',
+ // 'Link added successfully.' => '',
+ // 'Link updated successfully.' => '',
+ // 'Link removed successfully.' => '',
+ // 'Link labels' => '',
+ // 'Link modification' => '',
+ // 'Links' => '',
+ // 'Link settings' => '',
+ // 'Opposite label' => '',
+ // 'Remove a link' => '',
+ // 'Task\'s links' => '',
+ // 'The labels must be different' => '',
+ // 'There is no link.' => '',
+ // 'This label must be unique' => '',
+ // 'Unable to create your link.' => '',
+ // 'Unable to update your link.' => '',
+ // 'Unable to remove this link.' => '',
+ // 'relates to' => '',
+ // 'blocks' => '',
+ // 'is blocked by' => '',
+ // 'duplicates' => '',
+ // 'is duplicated by' => '',
+ // 'is a child of' => '',
+ // 'is a parent of' => '',
+ // 'targets milestone' => '',
+ // 'is a milestone of' => '',
+ // 'fixes' => '',
+ // 'is fixed by' => '',
+ // 'This task' => '',
+ // '<1h' => '',
+ // '%dh' => '',
+ // '%b %e' => '',
+ // 'Expand tasks' => '',
+ // 'Collapse tasks' => '',
+ // 'Expand/collapse tasks' => '',
+ // 'Close dialog box' => '',
+ // 'Submit a form' => '',
+ // 'Board view' => '',
+ // 'Keyboard shortcuts' => '',
+ // 'Open board switcher' => '',
+ // 'Application' => '',
);
diff --git a/app/Locale/zh_CN/translations.php b/app/Locale/zh_CN/translations.php
index addf7fa6..6868cd16 100644
--- a/app/Locale/zh_CN/translations.php
+++ b/app/Locale/zh_CN/translations.php
@@ -686,4 +686,49 @@ return array(
// 'Task age in days' => '',
// 'Days in this column' => '',
// '%dd' => '',
+ // 'Add a link' => '',
+ // 'Add a new link' => '',
+ // 'Do you really want to remove this link: "%s"?' => '',
+ // 'Do you really want to remove this link with task #%d?' => '',
+ // 'Field required' => '',
+ // 'Link added successfully.' => '',
+ // 'Link updated successfully.' => '',
+ // 'Link removed successfully.' => '',
+ // 'Link labels' => '',
+ // 'Link modification' => '',
+ // 'Links' => '',
+ // 'Link settings' => '',
+ // 'Opposite label' => '',
+ // 'Remove a link' => '',
+ // 'Task\'s links' => '',
+ // 'The labels must be different' => '',
+ // 'There is no link.' => '',
+ // 'This label must be unique' => '',
+ // 'Unable to create your link.' => '',
+ // 'Unable to update your link.' => '',
+ // 'Unable to remove this link.' => '',
+ // 'relates to' => '',
+ // 'blocks' => '',
+ // 'is blocked by' => '',
+ // 'duplicates' => '',
+ // 'is duplicated by' => '',
+ // 'is a child of' => '',
+ // 'is a parent of' => '',
+ // 'targets milestone' => '',
+ // 'is a milestone of' => '',
+ // 'fixes' => '',
+ // 'is fixed by' => '',
+ // 'This task' => '',
+ // '<1h' => '',
+ // '%dh' => '',
+ // '%b %e' => '',
+ // 'Expand tasks' => '',
+ // 'Collapse tasks' => '',
+ // 'Expand/collapse tasks' => '',
+ // 'Close dialog box' => '',
+ // 'Submit a form' => '',
+ // 'Board view' => '',
+ // 'Keyboard shortcuts' => '',
+ // 'Open board switcher' => '',
+ // 'Application' => '',
);
diff --git a/app/Model/Acl.php b/app/Model/Acl.php
index b6c0e6b8..4d4a22a7 100644
--- a/app/Model/Acl.php
+++ b/app/Model/Acl.php
@@ -38,7 +38,8 @@ class Acl extends Base
'project' => array('show', 'tasks', 'search', 'activity'),
'subtask' => '*',
'task' => '*',
- 'calendar' => array('show', 'events', 'save'),
+ 'tasklink' => '*',
+ 'calendar' => array('show', 'project', 'save'),
);
/**
@@ -67,6 +68,7 @@ class Acl extends Base
'app' => array('dashboard'),
'user' => array('index', 'create', 'save', 'remove'),
'config' => '*',
+ 'link' => '*',
'project' => array('remove'),
);
diff --git a/app/Model/Authentication.php b/app/Model/Authentication.php
index 92898cd5..86c1c43f 100644
--- a/app/Model/Authentication.php
+++ b/app/Model/Authentication.php
@@ -42,6 +42,13 @@ class Authentication extends Base
// If the user is already logged it's ok
if ($this->userSession->isLogged()) {
+ // Check if the user session match an existing user
+ if (! $this->user->exists($this->userSession->getId())) {
+ $this->backend('rememberMe')->destroy($this->userSession->getId());
+ $this->session->close();
+ return false;
+ }
+
// We update each time the RememberMe cookie tokens
if ($this->backend('rememberMe')->hasCookie()) {
$this->backend('rememberMe')->refresh();
diff --git a/app/Model/Base.php b/app/Model/Base.php
index 319e53fc..f836231c 100644
--- a/app/Model/Base.php
+++ b/app/Model/Base.php
@@ -25,6 +25,7 @@ use Pimple\Container;
* @property \Model\File $file
* @property \Model\Helper $helper
* @property \Model\LastLogin $lastLogin
+ * @property \Model\Link $link
* @property \Model\Notification $notification
* @property \Model\Project $project
* @property \Model\ProjectDuplication $projectDuplication
@@ -38,6 +39,7 @@ use Pimple\Container;
* @property \Model\TaskExport $taskExport
* @property \Model\TaskFinder $taskFinder
* @property \Model\TaskHistory $taskHistory
+ * @property \Model\TaskLink $taskLink
* @property \Model\TaskPosition $taskPosition
* @property \Model\TaskValidator $taskValidator
* @property \Model\TimeTracking $timeTracking
diff --git a/app/Model/Link.php b/app/Model/Link.php
new file mode 100644
index 00000000..87ba49c4
--- /dev/null
+++ b/app/Model/Link.php
@@ -0,0 +1,234 @@
+<?php
+
+namespace Model;
+
+use PDO;
+use SimpleValidator\Validator;
+use SimpleValidator\Validators;
+
+/**
+ * Link model
+ *
+ * @package model
+ * @author Olivier Maridat
+ * @author Frederic Guillot
+ */
+class Link extends Base
+{
+ /**
+ * SQL table name
+ *
+ * @var string
+ */
+ const TABLE = 'links';
+
+ /**
+ * Get a link by id
+ *
+ * @access public
+ * @param integer $link_id Link id
+ * @return array
+ */
+ public function getById($link_id)
+ {
+ return $this->db->table(self::TABLE)->eq('id', $link_id)->findOne();
+ }
+
+ /**
+ * Get a link by name
+ *
+ * @access public
+ * @param string $label
+ * @return array
+ */
+ public function getByLabel($label)
+ {
+ return $this->db->table(self::TABLE)->eq('label', $label)->findOne();
+ }
+
+ /**
+ * Get the opposite link id
+ *
+ * @access public
+ * @param integer $link_id Link id
+ * @return integer
+ */
+ public function getOppositeLinkId($link_id)
+ {
+ $link = $this->getById($link_id);
+ return $link['opposite_id'] ?: $link_id;
+ }
+
+ /**
+ * Get all links
+ *
+ * @access public
+ * @return array
+ */
+ public function getAll()
+ {
+ return $this->db->table(self::TABLE)->findAll();
+ }
+
+ /**
+ * Get merged links
+ *
+ * @access public
+ * @return array
+ */
+ public function getMergedList()
+ {
+ return $this->db
+ ->execute('
+ SELECT
+ links.id, links.label, opposite.label as opposite_label
+ FROM links
+ LEFT JOIN links AS opposite ON opposite.id=links.opposite_id
+ ')
+ ->fetchAll(PDO::FETCH_ASSOC);
+ }
+
+ /**
+ * Get label list
+ *
+ * @access public
+ * @param integer $exclude_id Exclude this link
+ * @param booelan $prepend Prepend default value
+ * @return array
+ */
+ public function getList($exclude_id = 0, $prepend = true)
+ {
+ $labels = $this->db->hashtable(self::TABLE)->neq('id', $exclude_id)->asc('id')->getAll('id', 'label');
+
+ foreach ($labels as &$value) {
+ $value = t($value);
+ }
+
+ return $prepend ? array('') + $labels : $labels;
+ }
+
+ /**
+ * Create a new link label
+ *
+ * @access public
+ * @param string $label
+ * @param string $opposite_label
+ * @return boolean
+ */
+ public function create($label, $opposite_label = '')
+ {
+ $this->db->startTransaction();
+
+ if (! $this->db->table(self::TABLE)->insert(array('label' => $label))) {
+ $this->db->cancelTransaction();
+ return false;
+ }
+
+ if ($opposite_label !== '') {
+ $this->createOpposite($opposite_label);
+ }
+
+ $this->db->closeTransaction();
+
+ return true;
+ }
+
+ /**
+ * Create the opposite label (executed inside create() method)
+ *
+ * @access private
+ * @param string $label
+ */
+ private function createOpposite($label)
+ {
+ $label_id = $this->db->getConnection()->getLastId();
+
+ $this->db
+ ->table(self::TABLE)
+ ->insert(array(
+ 'label' => $label,
+ 'opposite_id' => $label_id,
+ ));
+
+ $this->db
+ ->table(self::TABLE)
+ ->eq('id', $label_id)
+ ->update(array(
+ 'opposite_id' => $this->db->getConnection()->getLastId()
+ ));
+ }
+
+ /**
+ * Update a link
+ *
+ * @access public
+ * @param array $values
+ * @return boolean
+ */
+ public function update(array $values)
+ {
+ return $this->db
+ ->table(self::TABLE)
+ ->eq('id', $values['id'])
+ ->update(array(
+ 'label' => $values['label'],
+ 'opposite_id' => $values['opposite_id'],
+ ));
+ }
+
+ /**
+ * Remove a link a the relation to its opposite
+ *
+ * @access public
+ * @param integer $link_id
+ * @return boolean
+ */
+ public function remove($link_id)
+ {
+ $this->db->table(self::TABLE)->eq('opposite_id', $link_id)->update(array('opposite_id' => 0));
+ return $this->db->table(self::TABLE)->eq('id', $link_id)->remove();
+ }
+
+ /**
+ * Validate creation
+ *
+ * @access public
+ * @param array $values Form values
+ * @return array $valid, $errors [0] = Success or not, [1] = List of errors
+ */
+ public function validateCreation(array $values)
+ {
+ $v = new Validator($values, array(
+ new Validators\Required('label', t('Field required')),
+ new Validators\Unique('label', t('This label must be unique'), $this->db->getConnection(), self::TABLE),
+ new Validators\NotEquals('label', 'opposite_label', t('The labels must be different')),
+ ));
+
+ return array(
+ $v->execute(),
+ $v->getErrors()
+ );
+ }
+
+ /**
+ * Validate modification
+ *
+ * @access public
+ * @param array $values Form values
+ * @return array $valid, $errors [0] = Success or not, [1] = List of errors
+ */
+ public function validateModification(array $values)
+ {
+ $v = new Validator($values, array(
+ new Validators\Required('id', t('Field required')),
+ new Validators\Required('opposite_id', t('Field required')),
+ new Validators\Required('label', t('Field required')),
+ new Validators\Unique('label', t('This label must be unique'), $this->db->getConnection(), self::TABLE),
+ ));
+
+ return array(
+ $v->execute(),
+ $v->getErrors()
+ );
+ }
+}
diff --git a/app/Model/ProjectActivity.php b/app/Model/ProjectActivity.php
index 7762d0fd..652cc842 100644
--- a/app/Model/ProjectActivity.php
+++ b/app/Model/ProjectActivity.php
@@ -62,7 +62,7 @@ class ProjectActivity extends Base
*/
public function getProject($project_id, $limit = 50, $start = null, $end = null)
{
- return $this->getProjects(array($project_id), $limit, $start = null, $end = null);
+ return $this->getProjects(array($project_id), $limit, $start, $end);
}
/**
@@ -91,15 +91,15 @@ class ProjectActivity extends Base
->join(User::TABLE, 'id', 'creator_id')
->desc(self::TABLE.'.id')
->limit($limit);
-
+
if(!is_null($start)){
$query->gte('date_creation', $start);
}
-
+
if(!is_null($end)){
$query->lte('date_creation', $end);
}
-
+
$events = $query->findAll();
foreach ($events as &$event) {
diff --git a/app/Model/ProjectPermission.php b/app/Model/ProjectPermission.php
index fb6316b6..12bd9309 100644
--- a/app/Model/ProjectPermission.php
+++ b/app/Model/ProjectPermission.php
@@ -326,7 +326,7 @@ class ProjectPermission extends Base
*
* @access public
* @param integer $user_id User id
- * @return []integer
+ * @return array
*/
public function getMemberProjectIds($user_id)
{
@@ -338,6 +338,23 @@ class ProjectPermission extends Base
}
/**
+ * Return a list of active project ids where the user is member
+ *
+ * @access public
+ * @param integer $user_id User id
+ * @return array
+ */
+ public function getActiveMemberProjectIds($user_id)
+ {
+ return $this->db
+ ->table(Project::TABLE)
+ ->eq('user_id', $user_id)
+ ->eq(Project::TABLE.'.is_active', Project::ACTIVE)
+ ->join(self::TABLE, 'project_id', 'id')
+ ->findAllByColumn('projects.id');
+ }
+
+ /**
* Return a list of active projects where the user is member
*
* @access public
diff --git a/app/Model/SubtaskTimeTracking.php b/app/Model/SubtaskTimeTracking.php
index 8093cf80..1e7e252e 100644
--- a/app/Model/SubtaskTimeTracking.php
+++ b/app/Model/SubtaskTimeTracking.php
@@ -21,7 +21,7 @@ class SubtaskTimeTracking extends Base
* Get query for user timesheet (pagination)
*
* @access public
- * @param integer $user_id User id
+ * @param integer $user_id User id
* @return \PicoDb\Table
*/
public function getUserQuery($user_id)
@@ -36,7 +36,8 @@ class SubtaskTimeTracking extends Base
Subtask::TABLE.'.task_id',
Subtask::TABLE.'.title AS subtask_title',
Task::TABLE.'.title AS task_title',
- Task::TABLE.'.project_id'
+ Task::TABLE.'.project_id',
+ Task::TABLE.'.color_id'
)
->join(Subtask::TABLE, 'id', 'subtask_id')
->join(Task::TABLE, 'id', 'task_id', Subtask::TABLE)
@@ -44,10 +45,10 @@ class SubtaskTimeTracking extends Base
}
/**
- * Get query for task (pagination)
+ * Get query for task timesheet (pagination)
*
* @access public
- * @param integer $task_id Task id
+ * @param integer $task_id Task id
* @return \PicoDb\Table
*/
public function getTaskQuery($task_id)
@@ -73,6 +74,36 @@ class SubtaskTimeTracking extends Base
}
/**
+ * Get query for project timesheet (pagination)
+ *
+ * @access public
+ * @param integer $project_id Project id
+ * @return \PicoDb\Table
+ */
+ public function getProjectQuery($project_id)
+ {
+ return $this->db
+ ->table(self::TABLE)
+ ->columns(
+ self::TABLE.'.id',
+ self::TABLE.'.subtask_id',
+ self::TABLE.'.end',
+ self::TABLE.'.start',
+ self::TABLE.'.user_id',
+ Subtask::TABLE.'.task_id',
+ Subtask::TABLE.'.title AS subtask_title',
+ Task::TABLE.'.project_id',
+ Task::TABLE.'.color_id',
+ User::TABLE.'.username',
+ User::TABLE.'.name AS user_fullname'
+ )
+ ->join(Subtask::TABLE, 'id', 'subtask_id')
+ ->join(Task::TABLE, 'id', 'task_id', Subtask::TABLE)
+ ->join(User::TABLE, 'id', 'user_id', self::TABLE)
+ ->eq(Task::TABLE.'.project_id', $project_id);
+ }
+
+ /**
* Get all recorded time slots for a given user
*
* @access public
@@ -88,6 +119,98 @@ class SubtaskTimeTracking extends Base
}
/**
+ * Get user calendar events
+ *
+ * @access public
+ * @param integer $user_id
+ * @param integer $start
+ * @param integer $end
+ * @return array
+ */
+ public function getUserCalendarEvents($user_id, $start, $end)
+ {
+ $result = $this->getUserQuery($user_id)
+ ->addCondition($this->getCalendarCondition($start, $end))
+ ->findAll();
+
+ return $this->toCalendarEvents($result);
+ }
+
+ /**
+ * Get project calendar events
+ *
+ * @access public
+ * @param integer $project_id
+ * @param integer $start
+ * @param integer $end
+ * @return array
+ */
+ public function getProjectCalendarEvents($project_id, $start, $end)
+ {
+ $result = $this->getProjectQuery($project_id)
+ ->addCondition($this->getCalendarCondition($start, $end))
+ ->findAll();
+
+ return $this->toCalendarEvents($result);
+ }
+
+ /**
+ * Get time slots that should be displayed in the calendar time range
+ *
+ * @access private
+ * @param string $start ISO8601 start date
+ * @param string $end ISO8601 end date
+ * @return string
+ */
+ private function getCalendarCondition($start, $end)
+ {
+ $start_time = $this->dateParser->getTimestampFromIsoFormat($start);
+ $end_time = $this->dateParser->getTimestampFromIsoFormat($end);
+ $start_column = $this->db->escapeIdentifier('start');
+ $end_column = $this->db->escapeIdentifier('end');
+
+ $conditions = array(
+ "($start_column >= '$start_time' AND $start_column <= '$end_time')",
+ "($start_column <= '$start_time' AND $end_column >= '$start_time')",
+ "($start_column <= '$start_time' AND $end_column = '0')",
+ );
+
+ return '('.implode(' OR ', $conditions).')';
+ }
+
+ /**
+ * Convert a record set to calendar events
+ *
+ * @access private
+ * @param array $rows
+ * @return array
+ */
+ private function toCalendarEvents(array $rows)
+ {
+ $events = array();
+
+ foreach ($rows as $row) {
+
+ $user = isset($row['username']) ? ' ('.($row['user_fullname'] ?: $row['username']).')' : '';
+
+ $events[] = array(
+ 'id' => $row['id'],
+ 'subtask_id' => $row['subtask_id'],
+ 'title' => t('#%d', $row['task_id']).' '.$row['subtask_title'].$user,
+ 'start' => date('Y-m-d\TH:i:s', $row['start']),
+ 'end' => date('Y-m-d\TH:i:s', $row['end'] ?: time()),
+ 'backgroundColor' => $this->color->getBackgroundColor($row['color_id']),
+ 'borderColor' => $this->color->getBorderColor($row['color_id']),
+ 'textColor' => 'black',
+ 'url' => $this->helper->url('task', 'show', array('task_id' => $row['task_id'], 'project_id' => $row['project_id'])),
+ 'editable' => false,
+ );
+ }
+
+ return $events;
+ }
+
+ /**
* Log start time
*
* @access public
@@ -133,7 +256,7 @@ class SubtaskTimeTracking extends Base
*/
public function updateTaskTimeTracking($task_id)
{
- $result = $this->calculateSubtaskTime($task_id);
+ $result = $this->calculateSubtaskTime($task_id);
if (empty($result['total_spent']) && empty($result['total_estimated'])) {
return true;
diff --git a/app/Model/TaskFilter.php b/app/Model/TaskFilter.php
index b5a90154..31de2795 100644
--- a/app/Model/TaskFilter.php
+++ b/app/Model/TaskFilter.php
@@ -18,6 +18,24 @@ class TaskFilter extends Base
return $this;
}
+ public function excludeTasks(array $task_ids)
+ {
+ $this->query->notin('id', $task_ids);
+ return $this;
+ }
+
+ public function filterByTitle($title)
+ {
+ $this->query->ilike('title', '%'.$title.'%');
+ return $this;
+ }
+
+ public function filterByProjects(array $project_ids)
+ {
+ $this->query->in('project_id', $project_ids);
+ return $this;
+ }
+
public function filterByProject($project_id)
{
if ($project_id > 0) {
@@ -94,6 +112,20 @@ class TaskFilter extends Base
return $this->query->findAll();
}
+ public function toAutoCompletion()
+ {
+ return $this->query->columns('id', 'title')->filter(function(array $results) {
+
+ foreach ($results as &$result) {
+ $result['value'] = $result['title'];
+ $result['label'] = '#'.$result['id'].' - '.$result['title'];
+ }
+
+ return $results;
+
+ })->findAll();
+ }
+
public function toCalendarEvents()
{
$events = array();
diff --git a/app/Model/TaskFinder.php b/app/Model/TaskFinder.php
index 27fa8150..98ece4e1 100644
--- a/app/Model/TaskFinder.php
+++ b/app/Model/TaskFinder.php
@@ -84,6 +84,7 @@ class TaskFinder extends Base
'(SELECT count(*) FROM task_has_files WHERE task_id=tasks.id) AS nb_files',
'(SELECT count(*) FROM task_has_subtasks WHERE task_id=tasks.id) AS nb_subtasks',
'(SELECT count(*) FROM task_has_subtasks WHERE task_id=tasks.id AND status=2) AS nb_completed_subtasks',
+ '(SELECT count(*) FROM ' . TaskLink::TABLE . ' WHERE ' . TaskLink::TABLE . '.task_id = tasks.id) AS nb_links',
'tasks.id',
'tasks.reference',
'tasks.title',
diff --git a/app/Model/TaskLink.php b/app/Model/TaskLink.php
new file mode 100644
index 00000000..c712e5a8
--- /dev/null
+++ b/app/Model/TaskLink.php
@@ -0,0 +1,142 @@
+<?php
+
+namespace Model;
+
+use SimpleValidator\Validator;
+use SimpleValidator\Validators;
+
+/**
+ * TaskLink model
+ *
+ * @package model
+ * @author Olivier Maridat
+ * @author Frederic Guillot
+ */
+class TaskLink extends Base
+{
+ /**
+ * SQL table name
+ *
+ * @var string
+ */
+ const TABLE = 'task_has_links';
+
+ /**
+ * Get a task link
+ *
+ * @access public
+ * @param integer $task_link_id Task link id
+ * @return array
+ */
+ public function getById($task_link_id)
+ {
+ return $this->db->table(self::TABLE)->eq('id', $task_link_id)->findOne();
+ }
+
+ /**
+ * Get all links attached to a task
+ *
+ * @access public
+ * @param integer $task_id Task id
+ * @return array
+ */
+ public function getLinks($task_id)
+ {
+ return $this->db
+ ->table(self::TABLE)
+ ->columns(
+ self::TABLE.'.id',
+ self::TABLE.'.opposite_task_id AS task_id',
+ Link::TABLE.'.label',
+ Task::TABLE.'.title',
+ Task::TABLE.'.is_active',
+ Task::TABLE.'.project_id'
+ )
+ ->eq(self::TABLE.'.task_id', $task_id)
+ ->join(Link::TABLE, 'id', 'link_id')
+ ->join(Task::TABLE, 'id', 'opposite_task_id')
+ ->findAll();
+ }
+
+ /**
+ * Create a new link
+ *
+ * @access public
+ * @param integer $task_id Task id
+ * @param integer $opposite_task_id Opposite task id
+ * @param integer $link_id Link id
+ * @return boolean
+ */
+ public function create($task_id, $opposite_task_id, $link_id)
+ {
+ $this->db->startTransaction();
+
+ // Create the original link
+ $this->db->table(self::TABLE)->insert(array(
+ 'task_id' => $task_id,
+ 'opposite_task_id' => $opposite_task_id,
+ 'link_id' => $link_id,
+ ));
+
+ $link_id = $this->link->getOppositeLinkId($link_id);
+
+ // Create the opposite link
+ $this->db->table(self::TABLE)->insert(array(
+ 'task_id' => $opposite_task_id,
+ 'opposite_task_id' => $task_id,
+ 'link_id' => $link_id,
+ ));
+
+ $this->db->closeTransaction();
+
+ return true;
+ }
+
+ /**
+ * Remove a link between two tasks
+ *
+ * @access public
+ * @param integer $task_link_id
+ * @return boolean
+ */
+ public function remove($task_link_id)
+ {
+ $this->db->startTransaction();
+
+ $link = $this->getById($task_link_id);
+ $link_id = $this->link->getOppositeLinkId($link['link_id']);
+
+ $this->db->table(self::TABLE)->eq('id', $task_link_id)->remove();
+
+ $this->db
+ ->table(self::TABLE)
+ ->eq('opposite_task_id', $link['task_id'])
+ ->eq('task_id', $link['opposite_task_id'])
+ ->eq('link_id', $link_id)->remove();
+
+ $this->db->closeTransaction();
+
+ return true;
+ }
+
+ /**
+ * Validate creation
+ *
+ * @access public
+ * @param array $values Form values
+ * @return array $valid, $errors [0] = Success or not, [1] = List of errors
+ */
+ public function validateCreation(array $values)
+ {
+ $v = new Validator($values, array(
+ new Validators\Required('task_id', t('Field required')),
+ new Validators\Required('link_id', t('Field required')),
+ new Validators\Required('title', t('Field required')),
+ ));
+
+ return array(
+ $v->execute(),
+ $v->getErrors()
+ );
+ }
+}
diff --git a/app/Model/User.php b/app/Model/User.php
index 01be8597..7586f3c4 100644
--- a/app/Model/User.php
+++ b/app/Model/User.php
@@ -29,6 +29,18 @@ class User extends Base
const EVERYBODY_ID = -1;
/**
+ * Return true if the user exists
+ *
+ * @access public
+ * @param integer $user_id User id
+ * @return boolean
+ */
+ public function exists($user_id)
+ {
+ return $this->db->table(self::TABLE)->eq('id', $user_id)->count() === 1;
+ }
+
+ /**
* Get query to fetch all users
*
* @access public
diff --git a/app/Schema/Mysql.php b/app/Schema/Mysql.php
index 24bc2baf..947a62b3 100644
--- a/app/Schema/Mysql.php
+++ b/app/Schema/Mysql.php
@@ -4,13 +4,52 @@ namespace Schema;
use PDO;
use Core\Security;
-
-const VERSION = 45;
+use Model\Link;
+
+const VERSION = 46;
+
+function version_46($pdo)
+{
+ $pdo->exec("CREATE TABLE links (
+ id INT NOT NULL AUTO_INCREMENT,
+ label VARCHAR(255) NOT NULL,
+ opposite_id INT DEFAULT 0,
+ PRIMARY KEY(id),
+ UNIQUE(label)
+ ) ENGINE=InnoDB CHARSET=utf8");
+
+ $pdo->exec("CREATE TABLE task_has_links (
+ id INT NOT NULL AUTO_INCREMENT,
+ link_id INT NOT NULL,
+ task_id INT NOT NULL,
+ opposite_task_id INT NOT NULL,
+ FOREIGN KEY(link_id) REFERENCES links(id) ON DELETE CASCADE,
+ FOREIGN KEY(task_id) REFERENCES tasks(id) ON DELETE CASCADE,
+ FOREIGN KEY(opposite_task_id) REFERENCES tasks(id) ON DELETE CASCADE,
+ PRIMARY KEY(id)
+ ) ENGINE=InnoDB CHARSET=utf8");
+
+ $pdo->exec("CREATE INDEX task_has_links_task_index ON task_has_links(task_id)");
+ $pdo->exec("CREATE UNIQUE INDEX task_has_links_unique ON task_has_links(link_id, task_id, opposite_task_id)");
+
+ $rq = $pdo->prepare('INSERT INTO links (label, opposite_id) VALUES (?, ?)');
+ $rq->execute(array('relates to', 0));
+ $rq->execute(array('blocks', 3));
+ $rq->execute(array('is blocked by', 2));
+ $rq->execute(array('duplicates', 5));
+ $rq->execute(array('is duplicated by', 4));
+ $rq->execute(array('is a child of', 7));
+ $rq->execute(array('is a parent of', 6));
+ $rq->execute(array('targets milestone', 9));
+ $rq->execute(array('is a milestone of', 8));
+ $rq->execute(array('fixes', 11));
+ $rq->execute(array('is fixed by', 10));
+}
function version_45($pdo)
{
$pdo->exec('ALTER TABLE tasks ADD COLUMN date_moved INT DEFAULT 0');
-
+
/* Update tasks.date_moved from project_activities table if tasks.date_moved = null or 0.
* We take max project_activities.date_creation where event_name in task.create','task.move.column
* since creation date is always less than task moves
@@ -122,7 +161,7 @@ function version_38($pdo)
");
$pdo->exec('ALTER TABLE tasks ADD COLUMN swimlane_id INT DEFAULT 0');
- $pdo->exec("ALTER TABLE projects ADD COLUMN default_swimlane VARCHAR(200) DEFAULT '".t('Default swimlane')."'");
+ $pdo->exec("ALTER TABLE projects ADD COLUMN default_swimlane VARCHAR(200) DEFAULT 'Default swimlane'");
$pdo->exec("ALTER TABLE projects ADD COLUMN show_default_swimlane INT DEFAULT 1");
}
diff --git a/app/Schema/Postgres.php b/app/Schema/Postgres.php
index d3fb9fc4..027401ff 100644
--- a/app/Schema/Postgres.php
+++ b/app/Schema/Postgres.php
@@ -4,8 +4,45 @@ namespace Schema;
use PDO;
use Core\Security;
+use Model\Link;
-const VERSION = 26;
+const VERSION = 27;
+
+function version_27($pdo)
+{
+ $pdo->exec('CREATE TABLE links (
+ "id" SERIAL PRIMARY KEY,
+ "label" VARCHAR(255) NOT NULL,
+ "opposite_id" INTEGER DEFAULT 0,
+ UNIQUE("label")
+ )');
+
+ $pdo->exec("CREATE TABLE task_has_links (
+ id SERIAL PRIMARY KEY,
+ link_id INTEGER NOT NULL,
+ task_id INTEGER NOT NULL,
+ opposite_task_id INTEGER NOT NULL,
+ FOREIGN KEY(link_id) REFERENCES links(id) ON DELETE CASCADE,
+ FOREIGN KEY(task_id) REFERENCES tasks(id) ON DELETE CASCADE,
+ FOREIGN KEY(opposite_task_id) REFERENCES tasks(id) ON DELETE CASCADE
+ )");
+
+ $pdo->exec("CREATE INDEX task_has_links_task_index ON task_has_links(task_id)");
+ $pdo->exec("CREATE UNIQUE INDEX task_has_links_unique ON task_has_links(link_id, task_id, opposite_task_id)");
+
+ $rq = $pdo->prepare('INSERT INTO links (label, opposite_id) VALUES (?, ?)');
+ $rq->execute(array('relates to', 0));
+ $rq->execute(array('blocks', 3));
+ $rq->execute(array('is blocked by', 2));
+ $rq->execute(array('duplicates', 5));
+ $rq->execute(array('is duplicated by', 4));
+ $rq->execute(array('is a child of', 7));
+ $rq->execute(array('is a parent of', 6));
+ $rq->execute(array('targets milestone', 9));
+ $rq->execute(array('is a milestone of', 8));
+ $rq->execute(array('fixes', 11));
+ $rq->execute(array('is fixed by', 10));
+}
function version_26($pdo)
{
@@ -66,7 +103,7 @@ function version_24($pdo)
function version_23($pdo)
{
- $pdo->exec('ALTER TABLE columns ADD COLUMN description TEXT');
+ $pdo->exec('ALTER TABLE columns ADD COLUMN description TEXT');
}
function version_22($pdo)
@@ -120,7 +157,7 @@ function version_19($pdo)
");
$pdo->exec('ALTER TABLE tasks ADD COLUMN swimlane_id INTEGER DEFAULT 0');
- $pdo->exec("ALTER TABLE projects ADD COLUMN default_swimlane VARCHAR(200) DEFAULT '".t('Default swimlane')."'");
+ $pdo->exec("ALTER TABLE projects ADD COLUMN default_swimlane VARCHAR(200) DEFAULT 'Default swimlane'");
$pdo->exec("ALTER TABLE projects ADD COLUMN show_default_swimlane BOOLEAN DEFAULT '1'");
}
diff --git a/app/Schema/Sqlite.php b/app/Schema/Sqlite.php
index f027cf91..c6dec33f 100644
--- a/app/Schema/Sqlite.php
+++ b/app/Schema/Sqlite.php
@@ -4,13 +4,50 @@ namespace Schema;
use Core\Security;
use PDO;
-
-const VERSION = 44;
+use Model\Link;
+
+const VERSION = 45;
+
+function version_45($pdo)
+{
+ $pdo->exec("CREATE TABLE links (
+ id INTEGER PRIMARY KEY,
+ label TEXT NOT NULL,
+ opposite_id INTEGER DEFAULT 0,
+ UNIQUE(label)
+ )");
+
+ $pdo->exec("CREATE TABLE task_has_links (
+ id INTEGER PRIMARY KEY,
+ link_id INTEGER NOT NULL,
+ task_id INTEGER NOT NULL,
+ opposite_task_id INTEGER NOT NULL,
+ FOREIGN KEY(link_id) REFERENCES links(id) ON DELETE CASCADE,
+ FOREIGN KEY(task_id) REFERENCES tasks(id) ON DELETE CASCADE,
+ FOREIGN KEY(opposite_task_id) REFERENCES tasks(id) ON DELETE CASCADE
+ )");
+
+ $pdo->exec("CREATE INDEX task_has_links_task_index ON task_has_links(task_id)");
+ $pdo->exec("CREATE UNIQUE INDEX task_has_links_unique ON task_has_links(link_id, task_id, opposite_task_id)");
+
+ $rq = $pdo->prepare('INSERT INTO links (label, opposite_id) VALUES (?, ?)');
+ $rq->execute(array('relates to', 0));
+ $rq->execute(array('blocks', 3));
+ $rq->execute(array('is blocked by', 2));
+ $rq->execute(array('duplicates', 5));
+ $rq->execute(array('is duplicated by', 4));
+ $rq->execute(array('is a child of', 7));
+ $rq->execute(array('is a parent of', 6));
+ $rq->execute(array('targets milestone', 9));
+ $rq->execute(array('is a milestone of', 8));
+ $rq->execute(array('fixes', 11));
+ $rq->execute(array('is fixed by', 10));
+}
function version_44($pdo)
{
$pdo->exec('ALTER TABLE tasks ADD COLUMN date_moved INTEGER DEFAULT 0');
-
+
/* Update tasks.date_moved from project_activities table if tasks.date_moved = null or 0.
* We take max project_activities.date_creation where event_name in task.create','task.move.column
* since creation date is always less than task moves
@@ -66,7 +103,7 @@ function version_42($pdo)
function version_41($pdo)
{
- $pdo->exec('ALTER TABLE columns ADD COLUMN description TEXT');
+ $pdo->exec('ALTER TABLE columns ADD COLUMN description TEXT');
}
function version_40($pdo)
@@ -120,7 +157,7 @@ function version_37($pdo)
");
$pdo->exec('ALTER TABLE tasks ADD COLUMN swimlane_id INTEGER DEFAULT 0');
- $pdo->exec("ALTER TABLE projects ADD COLUMN default_swimlane TEXT DEFAULT '".t('Default swimlane')."'");
+ $pdo->exec("ALTER TABLE projects ADD COLUMN default_swimlane TEXT DEFAULT 'Default swimlane'");
$pdo->exec("ALTER TABLE projects ADD COLUMN show_default_swimlane INTEGER DEFAULT 1");
}
diff --git a/app/ServiceProvider/ClassProvider.php b/app/ServiceProvider/ClassProvider.php
index bee03184..213972ed 100644
--- a/app/ServiceProvider/ClassProvider.php
+++ b/app/ServiceProvider/ClassProvider.php
@@ -24,6 +24,7 @@ class ClassProvider implements ServiceProviderInterface
'DateParser',
'File',
'LastLogin',
+ 'Link',
'Notification',
'Project',
'ProjectActivity',
@@ -41,6 +42,7 @@ class ClassProvider implements ServiceProviderInterface
'TaskExport',
'TaskFinder',
'TaskFilter',
+ 'TaskLink',
'TaskModification',
'TaskPermission',
'TaskPosition',
diff --git a/app/Subscriber/Base.php b/app/Subscriber/Base.php
index 489d49b0..f90d9604 100644
--- a/app/Subscriber/Base.php
+++ b/app/Subscriber/Base.php
@@ -10,6 +10,7 @@ use Pimple\Container;
* @package subscriber
* @author Frederic Guillot
*
+ * @property \Model\Board $board
* @property \Model\Config $config
* @property \Model\Comment $comment
* @property \Model\LastLogin $lastLogin
diff --git a/app/Template/board/filters.php b/app/Template/board/filters.php
index 0e9c4d5f..64a01b8f 100644
--- a/app/Template/board/filters.php
+++ b/app/Template/board/filters.php
@@ -6,6 +6,14 @@
<i class="fa fa-caret-down"></i> <a href="#" class="dropdown-menu"><?= t('Actions') ?></a>
<ul>
<li>
+ <span class="filter-collapse">
+ <i class="fa fa-compress fa-fw"></i> <a href="#" class="filter-collapse-link"><?= t('Collapse tasks') ?></a>
+ </span>
+ <span class="filter-expand" style="display: none">
+ <i class="fa fa-expand fa-fw"></i> <a href="#" class="filter-expand-link"><?= t('Expand tasks') ?></a>
+ </span>
+ </li>
+ <li>
<i class="fa fa-search fa-fw"></i>
<?= $this->a(t('Search'), 'project', 'search', array('project_id' => $project['id'])) ?>
</li>
@@ -21,6 +29,11 @@
<i class="fa fa-calendar fa-fw"></i>
<?= $this->a(t('Calendar'), 'calendar', 'show', array('project_id' => $project['id'])) ?>
</li>
+ <?php if ($project['is_public']): ?>
+ <li>
+ <i class="fa fa-share-alt fa-fw"></i> <?= $this->a(t('Public link'), 'board', 'readonly', array('token' => $project['token']), false, '', '', true) ?>
+ </li>
+ <?php endif ?>
<?php if ($this->acl->isManagerActionAllowed($project['id'])): ?>
<li>
<i class="fa fa-line-chart fa-fw"></i>
diff --git a/app/Template/board/show.php b/app/Template/board/show.php
index 5fbb576c..f1607d26 100644
--- a/app/Template/board/show.php
+++ b/app/Template/board/show.php
@@ -1,28 +1,30 @@
-
-<?php if (isset($not_editable)): ?>
- <table id="board">
-<?php else: ?>
- <table id="board"
- data-project-id="<?= $project['id'] ?>"
- data-check-interval="<?= $board_private_refresh_interval ?>"
- data-save-url="<?= $this->u('board', 'save', array('project_id' => $project['id'])) ?>"
- data-check-url="<?= $this->u('board', 'check', array('project_id' => $project['id'], 'timestamp' => time())) ?>"
- >
-<?php endif ?>
-
-<?php foreach ($swimlanes as $swimlane): ?>
- <?php if (empty($swimlane['columns'])): ?>
- <p class="alert alert-error"><?= t('There is no column in your project!') ?></p>
- <?php break ?>
+<div id="board-container">
+ <?php if (isset($not_editable)): ?>
+ <table id="board">
<?php else: ?>
- <?= $this->render('board/swimlane', array(
- 'project' => $project,
- 'swimlane' => $swimlane,
- 'board_highlight_period' => $board_highlight_period,
- 'categories' => $categories,
- 'hide_swimlane' => count($swimlanes) === 1,
- 'not_editable' => isset($not_editable),
- )) ?>
+ <table id="board"
+ data-project-id="<?= $project['id'] ?>"
+ data-check-interval="<?= $board_private_refresh_interval ?>"
+ data-save-url="<?= $this->u('board', 'save', array('project_id' => $project['id'])) ?>"
+ data-check-url="<?= $this->u('board', 'check', array('project_id' => $project['id'], 'timestamp' => time())) ?>"
+ data-task-creation-url="<?= $this->u('task', 'create', array('project_id' => $project['id'])) ?>"
+ >
<?php endif ?>
-<?php endforeach ?>
-</table>
+
+ <?php foreach ($swimlanes as $swimlane): ?>
+ <?php if (empty($swimlane['columns'])): ?>
+ <p class="alert alert-error"><?= t('There is no column in your project!') ?></p>
+ <?php break ?>
+ <?php else: ?>
+ <?= $this->render('board/swimlane', array(
+ 'project' => $project,
+ 'swimlane' => $swimlane,
+ 'board_highlight_period' => $board_highlight_period,
+ 'categories' => $categories,
+ 'hide_swimlane' => count($swimlanes) === 1,
+ 'not_editable' => isset($not_editable),
+ )) ?>
+ <?php endif ?>
+ <?php endforeach ?>
+ </table>
+</div> \ No newline at end of file
diff --git a/app/Template/board/swimlane.php b/app/Template/board/swimlane.php
index f0a00fc1..08efdd11 100644
--- a/app/Template/board/swimlane.php
+++ b/app/Template/board/swimlane.php
@@ -2,7 +2,7 @@
<?php if (! $hide_swimlane): ?>
<th>
- <?php if ($swimlane['nb_tasks'] > 0): ?>
+ <?php if (! $not_editable && $swimlane['nb_tasks'] > 0): ?>
<a href="#" class="board-swimlane-toggle" data-swimlane-id="<?= $swimlane['id'] ?>">
<i class="fa fa-minus-circle hide-icon-swimlane-<?= $swimlane['id'] ?>"></i>
<i class="fa fa-plus-circle show-icon-swimlane-<?= $swimlane['id'] ?>" style="display: none"></i>
@@ -18,21 +18,21 @@
<?php endif ?>
<?php foreach ($swimlane['columns'] as $column): ?>
- <th>
+ <th class="board-column">
<?php if (! $not_editable): ?>
<div class="board-add-icon">
- <?= $this->a('+', 'task', 'create', array('project_id' => $column['project_id'], 'column_id' => $column['id'], 'swimlane_id' => $swimlane['id']), false, 'task-creation-popover', t('Add a new task')) ?>
+ <?= $this->a('+', 'task', 'create', array('project_id' => $column['project_id'], 'column_id' => $column['id'], 'swimlane_id' => $swimlane['id']), false, 'task-board-popover', t('Add a new task')) ?>
</div>
<?php endif ?>
<?= $this->e($column['title']) ?>
-
+
<?php if (! empty($column['description'])): ?>
- <span class="column-tooltip pull-right" title="<?= $this->markdown($column['description']) ?>">
+ <span class="column-tooltip pull-right" title="<?= $this->markdown($column['description']) ?>">
<i class="fa fa-info-circle"></i>
</span>
<?php endif ?>
-
+
<?php if ($column['task_limit']): ?>
<span title="<?= t('Task limit') ?>" class="task-limit">
(<span id="task-number-column-<?= $column['id'] ?>"><?= $column['nb_tasks'] ?></span>/<?= $this->e($column['task_limit']) ?>)
@@ -65,7 +65,7 @@
<?php endif ?>
<?php foreach ($column['tasks'] as $task): ?>
- <?= $this->render('board/task', array(
+ <?= $this->render($not_editable ? 'board/task_public' : 'board/task', array(
'project' => $project,
'task' => $task,
'categories' => $categories,
diff --git a/app/Template/board/task.php b/app/Template/board/task.php
index 5cad4004..a2a617a6 100644
--- a/app/Template/board/task.php
+++ b/app/Template/board/task.php
@@ -1,35 +1,3 @@
-<?php if ($not_editable): ?>
-
-<div class="task-board color-<?= $task['color_id'] ?> <?= $task['date_modification'] > time() - $board_highlight_period ? 'task-board-recent' : '' ?>">
-
- <?= $this->a('#'.$task['id'], 'task', 'readonly', array('task_id' => $task['id'], 'token' => $project['token'])) ?>
-
- <?php if ($task['reference']): ?>
- <span class="task-board-reference" title="<?= t('Reference') ?>">
- (<?= $task['reference'] ?>)
- </span>
- <?php endif ?>
-
- &nbsp;-&nbsp;
-
- <span class="task-board-user">
- <?php if (! empty($task['owner_id'])): ?>
- <?= t('Assigned to %s', $task['assignee_name'] ?: $task['assignee_username']) ?>
- <?php else: ?>
- <span class="task-board-nobody"><?= t('Nobody assigned') ?></span>
- <?php endif ?>
- </span>
-
- <?php if ($task['score']): ?>
- <span class="task-score"><?= $this->e($task['score']) ?></span>
- <?php endif ?>
-
- <div class="task-board-title">
- <?= $this->a($this->e($task['title']), 'task', 'readonly', array('task_id' => $task['id'], 'token' => $project['token'])) ?>
- </div>
-
-<?php else: ?>
-
<div class="task-board draggable-item color-<?= $task['color_id'] ?> <?= $task['date_modification'] > time() - $board_highlight_period ? 'task-board-recent' : '' ?>"
data-task-id="<?= $task['id'] ?>"
data-owner-id="<?= $task['owner_id'] ?>"
@@ -38,98 +6,57 @@
data-task-url="<?= $this->u('task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>"
title="<?= t('View this task') ?>">
- <ul class="dropdown">
- <li>
- <a href="#" class="dropdown-menu"><?= '#'.$task['id'] ?></a>
- <ul>
- <li><i class="fa fa-user"></i> <?= $this->a(t('Change assignee'), 'board', 'changeAssignee', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'assignee-popover') ?></li>
- <li><i class="fa fa-tag"></i> <?= $this->a(t('Change category'), 'board', 'changeCategory', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'category-popover') ?></li>
- <li><i class="fa fa-align-left"></i> <?= $this->a(t('Change description'), 'task', 'description', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'task-description-popover') ?></li>
- <li><i class="fa fa-pencil-square-o"></i> <?= $this->a(t('Edit this task'), 'task', 'edit', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'task-edit-popover') ?></li>
- <li><i class="fa fa-close"></i> <?= $this->a(t('Close this task'), 'task', 'close', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'confirmation' => 'yes', 'redirect' => 'board'), true) ?></li>
- </li>
- </li>
- </ul>
-
- <?php if ($task['reference']): ?>
- <span class="task-board-reference" title="<?= t('Reference') ?>">
- (<?= $task['reference'] ?>)
- </span>
- <?php endif ?>
-
- <span class="task-board-user <?= $this->userSession->isCurrentUser($task['owner_id']) ? 'task-board-current-user' : '' ?>">
- <?= $this->a(
- (! empty($task['owner_id']) ? t('Assigned to %s', $task['assignee_name'] ?: $task['assignee_username']) : t('Nobody assigned')),
- 'board',
- 'changeAssignee',
- array('task_id' => $task['id'], 'project_id' => $task['project_id']),
- false,
- 'assignee-popover',
- t('Change assignee')
- ) ?>
- </span>
-
- <span title="<?= t('Task age in days')?>" class="task-days-age"><?= t('%dd', floor(time()/86400) - floor($task['date_creation']/86400)) ?></span>
- <span title="<?= t('Days in this column')?>" class="task-days-incolumn"><?= t('%dd', floor(time()/86400) - floor($task['date_moved']/86400)) ?></span>
-
- <?php if ($task['score']): ?>
- <span class="task-score"><?= $this->e($task['score']) ?></span>
- <?php endif ?>
-
- <div class="task-board-title">
- <?= $this->a($this->e($task['title']), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, '', t('View this task')) ?>
+ <div class="task-board-collapsed" style="display: none">
+ <?= $this->a('#'.$task['id'], 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'task-board-collapsed-id') ?>
+ <?= $this->a($this->e($task['title']), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'task-board-collapsed-title') ?>
</div>
-<?php endif ?>
-
-
-<?php if ($task['category_id']): ?>
-<div class="task-board-category-container">
- <span class="task-board-category">
- <?= $this->a(
- $this->inList($task['category_id'], $categories),
- 'board',
- 'changeCategory',
- array('task_id' => $task['id'], 'project_id' => $task['project_id']),
- false,
- 'category-popover',
- t('Change category')
- ) ?>
- </span>
-</div>
-<?php endif ?>
-
-
-<?php if (! empty($task['date_due']) || ! empty($task['nb_files']) || ! empty($task['nb_comments']) || ! empty($task['description']) || ! empty($task['nb_subtasks'])): ?>
-<div class="task-board-footer">
-
- <?php if (! empty($task['date_due'])): ?>
- <div class="task-board-date <?= time() > $task['date_due'] ? 'task-board-date-overdue' : '' ?>">
- <i class="fa fa-calendar"></i>&nbsp;<?= dt('%b %e, %Y', $task['date_due']) ?>
- </div>
- <?php endif ?>
-
- <div class="task-board-icons">
+ <div class="task-board-expanded">
+
+ <ul class="dropdown">
+ <li>
+ <a href="#" class="dropdown-menu"><?= '#'.$task['id'] ?></a>
+ <ul>
+ <li><i class="fa fa-user"></i> <?= $this->a(t('Change assignee'), 'board', 'changeAssignee', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'task-board-popover') ?></li>
+ <li><i class="fa fa-tag"></i> <?= $this->a(t('Change category'), 'board', 'changeCategory', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'task-board-popover') ?></li>
+ <li><i class="fa fa-align-left"></i> <?= $this->a(t('Change description'), 'task', 'description', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'task-board-popover') ?></li>
+ <li><i class="fa fa-pencil-square-o"></i> <?= $this->a(t('Edit this task'), 'task', 'edit', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'task-board-popover') ?></li>
+ <li><i class="fa fa-close"></i> <?= $this->a(t('Close this task'), 'task', 'close', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'redirect' => 'board'), false, 'task-board-popover') ?></li>
+ </ul>
+ </li>
+ </ul>
- <?php if (! empty($task['nb_subtasks'])): ?>
- <span title="<?= t('Sub-Tasks') ?>" class="task-board-tooltip" data-href="<?= $this->u('board', 'subtasks', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>"><?= round($task['nb_completed_subtasks']/$task['nb_subtasks']*100, 0).'%' ?> <i class="fa fa-bars"></i></span>
+ <?php if ($task['reference']): ?>
+ <span class="task-board-reference" title="<?= t('Reference') ?>">
+ (<?= $task['reference'] ?>)
+ </span>
<?php endif ?>
- <?php if (! empty($task['nb_files'])): ?>
- <span title="<?= t('Attachments') ?>" class="task-board-tooltip" data-href="<?= $this->u('board', 'attachments', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>"><?= $task['nb_files'] ?> <i class="fa fa-paperclip"></i></span>
+ <span class="task-board-user <?= $this->userSession->isCurrentUser($task['owner_id']) ? 'task-board-current-user' : '' ?>">
+ <?= $this->a(
+ (! empty($task['owner_id']) ? ($task['assignee_name'] ?: $task['assignee_username']) : t('Nobody assigned')),
+ 'board',
+ 'changeAssignee',
+ array('task_id' => $task['id'], 'project_id' => $task['project_id']),
+ false,
+ 'task-board-popover',
+ t('Change assignee')
+ ) ?>
+ </span>
+
+ <?php if ($task['score']): ?>
+ <span class="task-score"><?= $this->e($task['score']) ?></span>
<?php endif ?>
- <?php if (! empty($task['nb_comments'])): ?>
- <span title="<?= p($task['nb_comments'], t('%d comment', $task['nb_comments']), t('%d comments', $task['nb_comments'])) ?>" class="task-board-tooltip" data-href="<?= $this->u('board', 'comments', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>"><?= $task['nb_comments'] ?> <i class="fa fa-comment-o"></i></span>
- <?php endif ?>
+ <div class="task-board-days">
+ <span title="<?= t('Task age in days')?>" class="task-days-age"><?= $this->getTaskAge($task['date_creation']) ?></span>
+ <span title="<?= t('Days in this column')?>" class="task-days-incolumn"><?= $this->getTaskAge($task['date_moved']) ?></span>
+ </div>
- <?php if (! empty($task['description'])): ?>
- <span title="<?= t('Description') ?>" class="task-board-tooltip" data-href="<?= $this->u('board', 'description', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>">
- <i class="fa fa-file-text-o"></i>
- </span>
- <?php endif ?>
+ <div class="task-board-title">
+ <?= $this->a($this->e($task['title']), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, '', t('View this task')) ?>
+ </div>
+
+ <?= $this->render('board/task_footer', array('task' => $task, 'categories' => $categories)) ?>
</div>
</div>
-<?php endif ?>
-
-</div> \ No newline at end of file
diff --git a/app/Template/board/task_footer.php b/app/Template/board/task_footer.php
new file mode 100644
index 00000000..d413692c
--- /dev/null
+++ b/app/Template/board/task_footer.php
@@ -0,0 +1,45 @@
+<?php if ($task['category_id']): ?>
+<div class="task-board-category-container">
+ <span class="task-board-category">
+ <?= $this->a(
+ $this->inList($task['category_id'], $categories),
+ 'board',
+ 'changeCategory',
+ array('task_id' => $task['id'], 'project_id' => $task['project_id']),
+ false,
+ 'task-board-popover',
+ t('Change category')
+ ) ?>
+ </span>
+</div>
+<?php endif ?>
+
+<div class="task-board-icons">
+ <?php if (! empty($task['date_due'])): ?>
+ <span class="task-board-date <?= time() > $task['date_due'] ? 'task-board-date-overdue' : '' ?>">
+ <i class="fa fa-calendar"></i>&nbsp;<?= dt('%b %e', $task['date_due']) ?>
+ </span>
+ <?php endif ?>
+
+ <?php if (! empty($task['nb_links'])): ?>
+ <span title="<?= t('Links') ?>" class="task-board-tooltip" data-href="<?= $this->u('board', 'tasklinks', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>"><?= $task['nb_links'] ?> <i class="fa fa-code-fork"></i></span>
+ <?php endif ?>
+
+ <?php if (! empty($task['nb_subtasks'])): ?>
+ <span title="<?= t('Sub-Tasks') ?>" class="task-board-tooltip" data-href="<?= $this->u('board', 'subtasks', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>"><?= round($task['nb_completed_subtasks']/$task['nb_subtasks']*100, 0).'%' ?> <i class="fa fa-bars"></i></span>
+ <?php endif ?>
+
+ <?php if (! empty($task['nb_files'])): ?>
+ <span title="<?= t('Attachments') ?>" class="task-board-tooltip" data-href="<?= $this->u('board', 'attachments', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>"><?= $task['nb_files'] ?> <i class="fa fa-paperclip"></i></span>
+ <?php endif ?>
+
+ <?php if (! empty($task['nb_comments'])): ?>
+ <span title="<?= p($task['nb_comments'], t('%d comment', $task['nb_comments']), t('%d comments', $task['nb_comments'])) ?>" class="task-board-tooltip" data-href="<?= $this->u('board', 'comments', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>"><?= $task['nb_comments'] ?> <i class="fa fa-comment-o"></i></span>
+ <?php endif ?>
+
+ <?php if (! empty($task['description'])): ?>
+ <span title="<?= t('Description') ?>" class="task-board-tooltip" data-href="<?= $this->u('board', 'description', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>">
+ <i class="fa fa-file-text-o"></i>
+ </span>
+ <?php endif ?>
+</div> \ No newline at end of file
diff --git a/app/Template/board/task_public.php b/app/Template/board/task_public.php
new file mode 100644
index 00000000..650b956d
--- /dev/null
+++ b/app/Template/board/task_public.php
@@ -0,0 +1,30 @@
+<div class="task-board color-<?= $task['color_id'] ?> <?= $task['date_modification'] > time() - $board_highlight_period ? 'task-board-recent' : '' ?>">
+
+ <?= $this->a('#'.$task['id'], 'task', 'readonly', array('task_id' => $task['id'], 'token' => $project['token'])) ?>
+
+ <?php if ($task['reference']): ?>
+ <span class="task-board-reference" title="<?= t('Reference') ?>">
+ (<?= $task['reference'] ?>)
+ </span>
+ <?php endif ?>
+
+ &nbsp;-&nbsp;
+
+ <span class="task-board-user">
+ <?php if (! empty($task['owner_id'])): ?>
+ <?= t('Assigned to %s', $task['assignee_name'] ?: $task['assignee_username']) ?>
+ <?php else: ?>
+ <span class="task-board-nobody"><?= t('Nobody assigned') ?></span>
+ <?php endif ?>
+ </span>
+
+ <?php if ($task['score']): ?>
+ <span class="task-score"><?= $this->e($task['score']) ?></span>
+ <?php endif ?>
+
+ <div class="task-board-title">
+ <?= $this->a($this->e($task['title']), 'task', 'readonly', array('task_id' => $task['id'], 'token' => $project['token'])) ?>
+ </div>
+
+ <?= $this->render('board/task_footer', array('task' => $task, 'categories' => $categories)) ?>
+</div> \ No newline at end of file
diff --git a/app/Template/board/tasklinks.php b/app/Template/board/tasklinks.php
new file mode 100644
index 00000000..9c4f52ca
--- /dev/null
+++ b/app/Template/board/tasklinks.php
@@ -0,0 +1,15 @@
+<div class="tooltip-tasklinks">
+ <ul>
+ <?php foreach($links as $link): ?>
+ <li>
+ <strong><?= t($link['label']) ?></strong>
+ <?= $this->a(
+ $this->e('#'.$link['task_id'].' - '.$link['title']),
+ 'task', 'show', array('task_id' => $link['task_id'], 'project_id' => $link['project_id']),
+ false,
+ $link['is_active'] ? '' : 'task-link-closed'
+ ) ?>
+ </li>
+ <?php endforeach ?>
+ </ul>
+</div> \ No newline at end of file
diff --git a/app/Template/calendar/show.php b/app/Template/calendar/show.php
index b258e391..cb5a1109 100644
--- a/app/Template/calendar/show.php
+++ b/app/Template/calendar/show.php
@@ -35,7 +35,7 @@
<div id="calendar"
data-project-id="<?= $project['id'] ?>"
data-save-url="<?= $this->u('calendar', 'save', array('project_id' => $project['id'])) ?>"
- data-check-url="<?= $this->u('calendar', 'events', array('project_id' => $project['id'])) ?>"
+ data-check-url="<?= $this->u('calendar', 'project', array('project_id' => $project['id'])) ?>"
data-check-interval="<?= $check_interval ?>"
>
</div>
diff --git a/app/Template/config/about.php b/app/Template/config/about.php
index d96e6bdd..f6474e21 100644
--- a/app/Template/config/about.php
+++ b/app/Template/config/about.php
@@ -1,7 +1,7 @@
<div class="page-header">
<h2><?= t('About') ?></h2>
</div>
-<section class="listing">
+<div class="listing">
<ul>
<li>
<?= t('Official website:') ?>
@@ -12,12 +12,11 @@
<strong><?= APP_VERSION ?></strong>
</li>
</ul>
-</section>
-
+</div>
<div class="page-header">
<h2><?= t('Database') ?></h2>
</div>
-<section class="listing">
+<div class="listing">
<ul>
<li>
<?= t('Database driver:') ?>
@@ -38,4 +37,20 @@
</li>
<?php endif ?>
</ul>
-</section> \ No newline at end of file
+</div>
+<div class="page-header">
+ <h2><?= t('Keyboard shortcuts') ?></h2>
+</div>
+<div class="listing">
+ <h3><?= t('Board view') ?></h3>
+ <ul>
+ <li><?= t('New task') ?> = <strong>n</strong></li>
+ <li><?= t('Expand/collapse tasks') ?> = <strong>s</strong></li>
+ </ul>
+ <h3><?= t('Application') ?></h3>
+ <ul>
+ <li><?= t('Open board switcher') ?> = <strong>b</strong></li>
+ <li><?= t('Close dialog box') ?> = <strong>ESC</strong></li>
+ <li><?= t('Submit a form') ?> = <strong>CTRL+ENTER</strong> <?= t('or') ?> <strong>⌘+ENTER</strong></li>
+ </ul>
+</div> \ No newline at end of file
diff --git a/app/Template/config/sidebar.php b/app/Template/config/sidebar.php
index 8e6fa379..89f2c203 100644
--- a/app/Template/config/sidebar.php
+++ b/app/Template/config/sidebar.php
@@ -11,6 +11,9 @@
<?= $this->a(t('Board settings'), 'config', 'board') ?>
</li>
<li>
+ <?= $this->a(t('Link settings'), 'link', 'index') ?>
+ </li>
+ <li>
<?= $this->a(t('Webhooks'), 'config', 'webhook') ?>
</li>
<li>
diff --git a/app/Template/file/show.php b/app/Template/file/show.php
index 179f5744..298976f6 100644
--- a/app/Template/file/show.php
+++ b/app/Template/file/show.php
@@ -11,7 +11,7 @@
<?= $this->a($this->e($file['name']), 'file', 'download', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'file_id' => $file['id'])) ?>
<span class="task-show-file-actions">
<?php if ($file['is_image']): ?>
- <?= $this->a(t('open'), 'file', 'open', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'file_id' => $file['id']), false, 'file-popover') ?>,
+ <?= $this->a(t('open'), 'file', 'open', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'file_id' => $file['id']), false, 'popover') ?>,
<?php endif ?>
<?= $this->a(t('remove'), 'file', 'confirm', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'file_id' => $file['id'])) ?>
</span>
diff --git a/app/Template/layout.php b/app/Template/layout.php
index 9e85e9ae..ad4c4084 100644
--- a/app/Template/layout.php
+++ b/app/Template/layout.php
@@ -29,13 +29,13 @@
data-login-url="<?= $this->u('user', 'login') ?>"
data-timezone="<?= $this->getTimezone() ?>"
data-js-lang="<?= $this->jsLang() ?>">
-
+
<?php if (isset($no_layout) && $no_layout): ?>
<?= $content_for_layout ?>
<?php else: ?>
<header>
<nav>
- <h1><?= $this->a('<i class="fa fa-home fa-fw"></i>', 'app', 'index', array(), false, 'home-link', t('Dashboard')).' '.$this->summary($this->e($title)) ?></h1>
+ <h1><?= $this->a('K<span>B</span>', 'app', 'index', array(), false, 'logo', t('Dashboard')).' '.$this->summary($this->e($title)) ?></h1>
<ul>
<?php if (isset($board_selector) && ! empty($board_selector)): ?>
<li>
diff --git a/app/Template/link/create.php b/app/Template/link/create.php
new file mode 100644
index 00000000..12589574
--- /dev/null
+++ b/app/Template/link/create.php
@@ -0,0 +1,18 @@
+<div class="page-header">
+ <h2><?= t('Add a new link') ?></h2>
+</div>
+
+<form action="<?= $this->u('link', 'save') ?>" method="post" autocomplete="off">
+
+ <?= $this->formCsrf() ?>
+
+ <?= $this->formLabel(t('Label'), 'label') ?>
+ <?= $this->formText('label', $values, $errors, array('required')) ?>
+
+ <?= $this->formLabel(t('Opposite label'), 'opposite_label') ?>
+ <?= $this->formText('opposite_label', $values, $errors) ?>
+
+ <div class="form-actions">
+ <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/>
+ </div>
+</form> \ No newline at end of file
diff --git a/app/Template/link/edit.php b/app/Template/link/edit.php
new file mode 100644
index 00000000..d9ce280c
--- /dev/null
+++ b/app/Template/link/edit.php
@@ -0,0 +1,21 @@
+<div class="page-header">
+ <h2><?= t('Link modification') ?></h2>
+</div>
+
+<form action="<?= $this->u('link', 'update', array('link_id' => $link['id'])) ?>" method="post" autocomplete="off">
+
+ <?= $this->formCsrf() ?>
+ <?= $this->formHidden('id', $values) ?>
+
+ <?= $this->formLabel(t('Label'), 'label') ?>
+ <?= $this->formText('label', $values, $errors, array('required')) ?>
+
+ <?= $this->formLabel(t('Opposite label'), 'opposite_id') ?>
+ <?= $this->formSelect('opposite_id', $labels, $values, $errors) ?>
+
+ <div class="form-actions">
+ <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/>
+ <?= t('or') ?>
+ <?= $this->a(t('cancel'), 'link', 'index') ?>
+ </div>
+</form> \ No newline at end of file
diff --git a/app/Template/link/index.php b/app/Template/link/index.php
new file mode 100644
index 00000000..90d1c357
--- /dev/null
+++ b/app/Template/link/index.php
@@ -0,0 +1,33 @@
+<div class="page-header">
+ <h2><?= t('Link labels') ?></h2>
+</div>
+<?php if (! empty($links)): ?>
+<table>
+ <tr>
+ <th class="column-70"><?= t('Link labels') ?></th>
+ <th><?= t('Actions') ?></th>
+ </tr>
+ <?php foreach ($links as $link): ?>
+ <tr>
+ <td>
+ <strong><?= t($link['label']) ?></strong>
+
+ <?php if (! empty($link['opposite_label'])): ?>
+ | <?= t($link['opposite_label']) ?>
+ <?php endif ?>
+ </td>
+ <td>
+ <ul>
+ <?= $this->a(t('Edit'), 'link', 'edit', array('link_id' => $link['id'])) ?>
+ <?= t('or') ?>
+ <?= $this->a(t('Remove'), 'link', 'confirm', array('link_id' => $link['id'])) ?>
+ </ul>
+ </td>
+ </tr>
+ <?php endforeach ?>
+</table>
+<?php else: ?>
+ <?= t('There is no link.') ?>
+<?php endif ?>
+
+<?= $this->render('link/create', array('values' => $values, 'errors' => $errors)) ?> \ No newline at end of file
diff --git a/app/Template/link/remove.php b/app/Template/link/remove.php
new file mode 100644
index 00000000..a802feb0
--- /dev/null
+++ b/app/Template/link/remove.php
@@ -0,0 +1,15 @@
+<div class="page-header">
+ <h2><?= t('Remove a link') ?></h2>
+</div>
+
+<div class="confirm">
+ <p class="alert alert-info">
+ <?= t('Do you really want to remove this link: "%s"?', $link['label']) ?>
+ </p>
+
+ <div class="form-actions">
+ <?= $this->a(t('Yes'), 'link', 'remove', array('link_id' => $link['id']), true, 'btn btn-red') ?>
+ <?= t('or') ?>
+ <?= $this->a(t('cancel'), 'link', 'index') ?>
+ </div>
+</div> \ No newline at end of file
diff --git a/app/Template/task/close.php b/app/Template/task/close.php
index 316d58eb..a0a95787 100644
--- a/app/Template/task/close.php
+++ b/app/Template/task/close.php
@@ -8,8 +8,8 @@
</p>
<div class="form-actions">
- <?= $this->a(t('Yes'), 'task', 'close', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'confirmation' => 'yes'), true, 'btn btn-red') ?>
+ <?= $this->a(t('Yes'), 'task', 'close', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'confirmation' => 'yes', 'redirect' => $redirect), true, 'btn btn-red') ?>
<?= t('or') ?>
- <?= $this->a(t('cancel'), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>
+ <?= $this->a(t('cancel'), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'close-popover') ?>
</div>
</div> \ No newline at end of file
diff --git a/app/Template/task/public.php b/app/Template/task/public.php
index 2d95e6db..d7acef9f 100644
--- a/app/Template/task/public.php
+++ b/app/Template/task/public.php
@@ -10,6 +10,13 @@
'is_public' => true
)) ?>
+ <?= $this->render('tasklink/show', array(
+ 'task' => $task,
+ 'links' => $links,
+ 'project' => $project,
+ 'not_editable' => true
+ )) ?>
+
<?= $this->render('subtask/show', array(
'task' => $task,
'subtasks' => $subtasks,
diff --git a/app/Template/task/show.php b/app/Template/task/show.php
index b8243cc6..1ff2ef43 100644
--- a/app/Template/task/show.php
+++ b/app/Template/task/show.php
@@ -1,7 +1,8 @@
<?= $this->render('task/details', array('task' => $task, 'project' => $project)) ?>
<?= $this->render('task/time', array('task' => $task, 'values' => $values, 'date_format' => $date_format, 'date_formats' => $date_formats)) ?>
<?= $this->render('task/show_description', array('task' => $task)) ?>
+<?= $this->render('tasklink/show', array('task' => $task, 'links' => $links)) ?>
<?= $this->render('subtask/show', array('task' => $task, 'subtasks' => $subtasks)) ?>
<?= $this->render('task/timesheet', array('task' => $task)) ?>
<?= $this->render('file/show', array('task' => $task, 'files' => $files)) ?>
-<?= $this->render('task/comments', array('task' => $task, 'comments' => $comments, 'project' => $project)) ?> \ No newline at end of file
+<?= $this->render('task/comments', array('task' => $task, 'comments' => $comments, 'project' => $project)) ?>
diff --git a/app/Template/task/sidebar.php b/app/Template/task/sidebar.php
index e85a1671..f41be14d 100644
--- a/app/Template/task/sidebar.php
+++ b/app/Template/task/sidebar.php
@@ -19,6 +19,9 @@
<?= $this->a(t('Add a sub-task'), 'subtask', 'create', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>
</li>
<li>
+ <?= $this->a(t('Add a link'), 'tasklink', 'create', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>
+ </li>
+ <li>
<?= $this->a(t('Add a comment'), 'comment', 'create', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>
</li>
<li>
diff --git a/app/Template/tasklink/create.php b/app/Template/tasklink/create.php
new file mode 100644
index 00000000..fb438cd8
--- /dev/null
+++ b/app/Template/tasklink/create.php
@@ -0,0 +1,27 @@
+<div class="page-header">
+ <h2><?= t('Add a new link') ?></h2>
+</div>
+
+<form action="<?= $this->u('tasklink', 'save', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>" method="post" autocomplete="off">
+
+ <?= $this->formCsrf() ?>
+ <?= $this->formHidden('task_id', $values) ?>
+ <?= $this->formHidden('opposite_task_id', $values) ?>
+
+ <?= $this->formLabel(t('Label'), 'link_id') ?>
+ <?= $this->formSelect('link_id', $labels, $values, $errors) ?>
+
+ <?= $this->formLabel(t('Task'), 'title') ?>
+ <?= $this->formText(
+ 'title',
+ $values,
+ $errors,
+ array('required', 'data-dst-field="opposite_task_id"', 'data-search-url="'.$this->u('app', 'autocomplete', array('exclude_task_id' => $task['id'])).'"'),
+ 'task-autocomplete') ?>
+
+ <div class="form-actions">
+ <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/>
+ <?= t('or') ?>
+ <?= $this->a(t('cancel'), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>
+ </div>
+</form> \ No newline at end of file
diff --git a/app/Template/tasklink/remove.php b/app/Template/tasklink/remove.php
new file mode 100644
index 00000000..9322ec24
--- /dev/null
+++ b/app/Template/tasklink/remove.php
@@ -0,0 +1,15 @@
+<div class="page-header">
+ <h2><?= t('Remove a link') ?></h2>
+</div>
+
+<div class="confirm">
+ <p class="alert alert-info">
+ <?= t('Do you really want to remove this link with task #%d?', $link['opposite_task_id']) ?>
+ </p>
+
+ <div class="form-actions">
+ <?= $this->a(t('Yes'), 'tasklink', 'remove', array('link_id' => $link['id'], 'task_id' => $task['id'], 'project_id' => $task['project_id']), true, 'btn btn-red') ?>
+ <?= t('or') ?>
+ <?= $this->a(t('cancel'), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>
+ </div>
+</div> \ No newline at end of file
diff --git a/app/Template/tasklink/show.php b/app/Template/tasklink/show.php
new file mode 100644
index 00000000..ca4e4383
--- /dev/null
+++ b/app/Template/tasklink/show.php
@@ -0,0 +1,41 @@
+<?php if (! empty($links)): ?>
+<div class="page-header">
+ <h2><?= t('Links') ?></h2>
+</div>
+<table class="table-fixed" id="links">
+ <tr>
+ <th class="column-30"><?= t('Label') ?></th>
+ <th class="column-60"><?= t('Task') ?></th>
+ <?php if (! isset($not_editable)): ?>
+ <th><?= t('Action') ?></th>
+ <?php endif ?>
+ </tr>
+ <?php foreach ($links as $link): ?>
+ <tr>
+ <td><?= t('This task') ?> <strong><?= t($link['label']) ?></strong></td>
+ <?php if (! isset($not_editable)): ?>
+ <td>
+ <?= $this->a(
+ $this->e('#'.$link['task_id'].' - '.$link['title']),
+ 'task', 'show', array('task_id' => $link['task_id'], 'project_id' => $link['project_id']),
+ false,
+ $link['is_active'] ? '' : 'task-link-closed'
+ ) ?>
+ </td>
+ <td>
+ <?= $this->a(t('Remove'), 'tasklink', 'confirm', array('link_id' => $link['id'], 'task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>
+ </td>
+ <?php else: ?>
+ <td>
+ <?= $this->a(
+ $this->e('#'.$link['task_id'].' - '.$link['title']),
+ 'task', 'readonly', array('task_id' => $link['task_id'], 'token' => $project['token']),
+ false,
+ $link['is_active'] ? '' : 'task-link-closed'
+ ) ?>
+ </td>
+ <?php endif ?>
+ </tr>
+ <?php endforeach ?>
+</table>
+<?php endif ?> \ No newline at end of file
diff --git a/app/functions.php b/app/functions.php
index d45e78e7..439375be 100644
--- a/app/functions.php
+++ b/app/functions.php
@@ -63,6 +63,9 @@ function dt($format, $timestamp)
* @todo Improve this function
* @return mixed
*/
-function p($value, $t1, $t2) {
+function p($value, $t1, $t2)
+{
return $value > 1 ? $t2 : $t1;
}
+
+