Add task links (Merge pull-request #610)

This commit is contained in:
Frederic Guillot 2015-02-13 17:50:20 -05:00
parent 124f7cad28
commit 364382b1b5
38 changed files with 1769 additions and 41 deletions

View File

@ -43,6 +43,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 +55,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

View File

@ -401,6 +401,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->getAll($task['id']),
'task' => $task,
)));
}
/**
* Get subtasks on mouseover
*

View File

@ -87,12 +87,17 @@ class Config extends Base
*
* @access public
*/
public function board()
public function board(array $values = array(), array $errors = array())
{
$this->common('board');
$this->response->html($this->layout('config/board', array(
'default_columns' => implode(', ', $this->board->getDefaultColumns()),
'links' => $this->link->getMergedList(),
'values' => $values + array(
'project_id' => -1
),
'errors' => $errors,
'title' => t('Settings').' > '.t('Board settings'),
)));
}

189
app/Controller/Link.php Normal file
View File

@ -0,0 +1,189 @@
<?php
namespace Controller;
/**
* Link controller
*
* @package controller
* @author Olivier Maridat
*/
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);
if (isset($params['values']['project_id']) && -1 != $params['values']['project_id']) {
return $this->projectLayout($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'), $this->request->getIntegerParam('project_id', -1));
if (! $link) {
$this->notfound();
}
$link['link_id'] = $link[0]['link_id'];
$link['project_id'] = $link[0]['project_id'];
return $link;
}
/**
* Method to get a project
*
* @access protected
* @param integer $project_id Default project id
* @return array
*/
protected function getProject($project_id = -1)
{
$project = array('id' => $project_id);
$project_id = $this->request->getIntegerParam('project_id', $project_id);
if (-1 != $project_id) {
$project = parent::getProject($project_id);
}
return $project;
}
/**
* List of links for a given project
*
* @access public
*/
public function index(array $values = array(), array $errors = array())
{
$project = $this->getProject();
$values['project_id'] = $project['id'];
$values[] = array();
$this->response->html($this->layout('link/index', array(
'links' => $this->link->getMergedList($project['id']),
'values' => $values,
'errors' => $errors,
'project' => $project,
'title' => t('Settings').' &gt; '.t('Board\'s links settings'),
)));
}
/**
* 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)) {
$this->session->flash(t('Link added successfully.'));
$this->response->redirect('?controller=link&action=index&project_id='.$values['project_id']);
}
else {
$this->session->flashError(t('Unable to create your link.'));
}
}
if (!empty($values)) {
$this->link->prepare($values);
}
$this->index($values, $errors);
}
/**
* Edit form
*
* @access public
*/
public function edit(array $values = array(), array $errors = array())
{
$project = $this->getProject();
$this->response->html($this->layout('link/edit', array(
'values' => empty($values) ? $this->getLink() : $values,
'errors' => $errors,
'project' => $project,
'edit' => true,
'title' => t('Links')
)));
}
/**
* 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('?controller=link&action=index&project_id='.$values['project_id']);
}
else {
$this->session->flashError(t('Unable to update your link.'));
}
}
if (!empty($values)) {
$this->link->prepare($values);
}
$this->edit($values, $errors);
}
/**
* Confirmation dialog before removing a link
*
* @access public
*/
public function confirm()
{
$project = $this->getProject();
$link = $this->getLink();
$this->response->html($this->layout('link/remove', array(
'project' => $project,
'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['link_id'])) {
$this->session->flash(t('Link removed successfully.'));
$this->response->redirect('?controller=link&action=index&project_id='.$link['project_id']);
}
else {
$this->session->flashError(t('Unable to remove this link.'));
}
$this->confirm();
}
}

View File

@ -3,6 +3,7 @@
namespace Controller;
use Model\Project as ProjectModel;
use Model\Task as TaskModel;
/**
* Task controller
@ -36,6 +37,7 @@ class Task extends Base
'project' => $project,
'comments' => $this->comment->getAll($task['id']),
'subtasks' => $this->subtask->getAll($task['id']),
'links' => $this->taskLink->getAll($task['id']),
'task' => $task,
'columns_list' => $this->board->getColumnsList($task['project_id']),
'colors_list' => $this->color->getList(),
@ -70,10 +72,13 @@ class Task extends Base
'files' => $this->file->getAll($task['id']),
'comments' => $this->comment->getAll($task['id']),
'subtasks' => $subtasks,
'links' => $this->taskLink->getAll($task['id']),
'task' => $task,
'values' => $values,
'columns_list' => $this->board->getColumnsList($task['project_id']),
'colors_list' => $this->color->getList(),
'link_list' => $this->link->getLinkLabelList($task['project_id'], false),
'task_list' => $this->taskFinder->getList($task['project_id'], TaskModel::STATUS_OPEN, $task['id']),
'date_format' => $this->config->get('application_date_format'),
'date_formats' => $this->dateParser->getAvailableFormats(),
'title' => $task['project_name'].' &gt; '.$task['title'],

160
app/Controller/Tasklink.php Normal file
View File

@ -0,0 +1,160 @@
<?php
namespace Controller;
use Model\Task AS TaskModel;
/**
* TaskLink controller
*
* @package controller
* @author Olivier Maridat
*/
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'],
'another_link' => $this->request->getIntegerParam('another_link', 0)
);
}
$this->response->html($this->taskLayout('tasklink/edit', array(
'values' => $values,
'errors' => $errors,
'link_list' => $this->link->getLinkLabelList($task['project_id']),
'task_list' => $this->taskFinder->getList($task['project_id'], TaskModel::STATUS_OPEN, $task['id']),
'task' => $task,
)));
}
/**
* 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)) {
$this->session->flash(t('Link added successfully.'));
if (isset($values['another_link']) && $values['another_link'] == 1) {
$this->response->redirect('?controller=tasklink&action=create&task_id='.$task['id'].'&project_id='.$task['project_id'].'&another_link=1');
}
$this->response->redirect('?controller=task&action=show&task_id='.$task['id'].'&project_id='.$task['project_id'].'#links');
}
else {
$this->session->flashError(t('Unable to add the link.'));
}
}
$this->create($values, $errors);
}
/**
* Edit form
*
* @access public
*/
public function edit(array $values = array(), array $errors = array())
{
$task = $this->getTask();
$taskLink = $this->getTaskLink();
$this->response->html($this->taskLayout('tasklink/edit', array(
'values' => empty($values) ? $taskLink : $values,
'errors' => $errors,
'link_list' => $this->link->getLinkLabelList($task['project_id'], false),
'task_list' => $this->taskFinder->getList($task['project_id'], TaskModel::STATUS_OPEN, $task['id']),
'link' => $taskLink,
'task' => $task,
'edit' => true,
)));
}
/**
* Update and validate a link
*
* @access public
*/
public function update()
{
$task = $this->getTask();
$values = $this->request->getValues();
list($valid, $errors) = $this->taskLink->validateModification($values);
if ($valid) {
if ($this->taskLink->update($values)) {
$this->session->flash(t('Link updated successfully.'));
$this->response->redirect('?controller=task&action=show&task_id='.$task['id'].'&project_id='.$task['project_id'].'#links');
}
else {
$this->session->flashError(t('Unable to update the link.'));
}
}
$this->edit($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.'));
$this->response->redirect('?controller=task&action=show&task_id='.$task['id'].'&project_id='.$task['project_id'].'#links');
}
else {
$this->session->flashError(t('Unable to remove this link.'));
}
$this->confirm();
}
}

View File

@ -155,6 +155,9 @@ class Helper
*/
public function formValue($values, $name)
{
if (false !== ($pos = strpos($name, '['))) {
$name = substr($name, 0, $pos);
}
if (isset($values->$name)) {
return 'value="'.$this->e($values->$name).'"';
}
@ -581,6 +584,7 @@ class Helper
{
return strpos($haystack, $needle) !== false;
}
/**
* Return a value from a dictionary

View File

@ -688,4 +688,52 @@ return array(
'Task age in days' => 'Age de la tâche en jours',
'Days in this column' => 'Jours dans cette colonne',
'%dd' => '%dj',
'relates to' => '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 la milestone',
'is a milestone o' => 'est une milestone de',
'fixes' => 'corrige',
'is fixed by' => 'est corrigée par',
'Links' => 'Liens',
'Add a link' => 'Ajouter un lien',
'Edit a link' => 'Mettre à jour un lien',
'Remove a link' => 'Supprimer un lien',
'Create another link' => 'Ajouter un autre lien',
'Linked task id' => 'Id de la tâche à lier',
'The exact same link already exists' => 'Un tel lien existe déjà',
'This linked task id doesn\'t exist' => 'Cette tâche n\'existe pas',
'A task can not be linked to itself' => 'Une tâche ne peut être liée à elle-même',
'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.',
'Unable to add the link.' => 'Impossible d\'ajouter ce lien.',
'Unable to update the link.' => 'Impossible de mettre à jour ce lien.',
'Unable to remove this link.' => 'Impossible de supprimer ce lien.',
'Do you really want to remove this link with task #%s?' => 'Voulez-vous vraiment supprimer ce lien avec la tâche #%s ?',
'The link type is required' => 'Le type du lien est obligatoire',
'The linked task id is required' => 'L\'id de la tâche à lier est obligatoire',
'The link id must be an integer' => 'L\'id du lien doit être un entier',
'The related task id must be an integer' => 'L\'id de la tâche à lier doit être un entier',
'Link management' => 'Gestion des liens',
'Add a new link' => 'Ajouter un nouveau lien',
'Add a new link label' => 'Ajouter un nouveau libellé de lien',
'Link modification for the project "%s"' => 'Mettre à jour un lien du projet « %s »',
'Links settings' => 'Paramètres des liens',
'Board\'s links settings' => 'Paramètres des liens du tableau',
'Link labels' => 'Libellés des liens',
'Link Label' => 'Libellé du lien',
'Link Inverse Label' => 'Libellé du lien inverse',
'Example:' => 'Exemple :',
'precedes' => 'précède',
'follows' => 'suit',
'#9 precedes #10' => '#9 précède #10',
'#10 follows #9' => '#10 suit #9',
'and therefore' => 'et donc',
'Do you really want to remove this link: "%s"?' => 'Voulez-vous vraiment supprimer ce lien: « %s » ?',
'You need to add link labels to this project before to link this task to another one.' => 'Il faut ajouter des libellés de liens à ce project avant de pouvoir lier une tâche à une autre.',
);

View File

@ -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

437
app/Model/Link.php Normal file
View File

@ -0,0 +1,437 @@
<?php
namespace Model;
use SimpleValidator\Validator;
use SimpleValidator\Validators;
use PDO;
/**
* Link model
* A link is made of one bidirectional (<->), or two sided (<- and ->) link labels.
*
* @package model
* @author Olivier Maridat
*/
class Link extends Base
{
/**
* SQL table name
*
* @var string
*/
const TABLE = 'link';
const TABLE_LABEL = 'link_label';
/**
* Direction: left to right ->
*
* @var integer
*/
const BEHAVIOUR_LEFT2RIGTH = 0;
/**
* Direction: right to left <-
*
* @var integer
*/
const BEHAVIOUR_RIGHT2LEFT = 1;
/**
* Bidirectional <->
*
* @var integer
*/
const BEHAVIOUR_BOTH = 2;
/**
* Get a link by the id
*
* @access public
* @param integer $link_id
* Link id
* @param integer $project_id
* Specify a project id. Default: -1 to target all projects
* @return array
*/
public function getById($link_id, $project_id = -1)
{
return $this->db->table(self::TABLE)
->eq(self::TABLE . '.link_id', $link_id)
->in('project_id', array(
- 1,
$project_id
))
->join(self::TABLE_LABEL, 'link_id', 'link_id')
->findAll();
}
/**
* Get the id of the inverse link label by a link label id
*
* @access public
* @param integer $link_id
* Link id
* @param integer $link_label_id
* Link label id
* @return integer
*/
public function getInverseLinkLabelId($link_label_id)
{
$sql = 'SELECT
la2.id
FROM ' . self::TABLE_LABEL . ' la1
JOIN '.self::TABLE_LABEL.' la2 ON la2.link_id = la1.link_id AND (la2.behaviour=2 OR la2.id != la1.id)
WHERE la1.id = ?
';
$rq = $this->db->execute($sql, array(
$link_label_id
));
return $rq->fetchColumn(0);
}
/**
* Return all link labels for a given project
*
* @access public
* @param integer $project_id
* Specify a project id. Default: -1 to target all projects
* @return array
*/
public function getLinkLabels($project_id = -1)
{
return $this->db->table(self::TABLE_LABEL)
->in(self::TABLE . '.project_id', array(
- 1,
$project_id
))
->join(self::TABLE, 'link_id', 'link_id')
->asc(self::TABLE_LABEL.'.link_id', 'behaviour')
->columns('id', self::TABLE . '.project_id', self::TABLE_LABEL.'.link_id', 'label', 'behaviour')
->findAll();
}
/**
* Return the list of all link labels
* Used to select a link label
*
* @access public
* @param integer $project_id
* Specify a project id. Default: -1 to target all projects
* @return array
*/
public function getLinkLabelList($project_id = -1)
{
$listing = $this->getLinkLabels($project_id);
$mergedListing = array();
foreach ($listing as $link) {
$suffix = '';
$prefix = '';
if (self::BEHAVIOUR_BOTH == $link['behaviour'] || self::BEHAVIOUR_LEFT2RIGTH == $link['behaviour']) {
$suffix = ' &raquo;';
}
if (self::BEHAVIOUR_BOTH == $link['behaviour'] || self::BEHAVIOUR_RIGHT2LEFT == $link['behaviour']) {
$prefix = '&laquo; ';
}
$mergedListing[$link['id']] = $prefix . t($link['label']) . $suffix;
}
$listing = $mergedListing;
return $listing;
}
/**
* Return the list of all links (label + inverse label)
*
* @access public
* @param integer $project_id
* Specify a project id. Default: -1 to target all projects
* @return array
*/
public function getMergedList($project_id = -1)
{
$listing = $this->getLinkLabels($project_id);
$mergedListing = array();
$current = null;
foreach ($listing as $link) {
if (self::BEHAVIOUR_BOTH == $link['behaviour'] || self::BEHAVIOUR_LEFT2RIGTH == $link['behaviour']) {
$current = $link;
}
else {
$current['label_inverse'] = $link['label'];
}
if (self::BEHAVIOUR_BOTH == $link['behaviour'] || self::BEHAVIOUR_RIGHT2LEFT == $link['behaviour']) {
$mergedListing[] = $current;
$current = null;
}
}
$listing = $mergedListing;
return $listing;
}
/**
* Prepare data before insert/update
*
* @access public
* @param array $values
* Form values
*/
public function prepare(array &$values)
{
// Prepare label 1
$link = array(
'project_id' => $values['project_id']
);
$label1 = array(
'label' => @$values['label'][0],
'behaviour' => (isset($values['behaviour'][0]) || !isset($values['label'][1]) || null == $values['label'][1]) ? self::BEHAVIOUR_BOTH : self::BEHAVIOUR_LEFT2RIGTH
);
$label2 = array(
'label' => @$values['label'][1],
'behaviour' => self::BEHAVIOUR_RIGHT2LEFT
);
if (isset($values['link_id'])) {
$link['link_id'] = $values['link_id'];
$label1['id'] = $values['id'][0];
$label2['id'] = @$values['id'][1];
$label1['link_id'] = $values['link_id'];
$label2['link_id'] = $values['link_id'];
}
$values = $link;
$values[] = $label1;
$values[] = $label2;
return array(
$link,
$label1,
$label2
);
}
/**
* Create a link
*
* @access public
* @param array $values
* Form values
* @return bool integer
*/
public function create(array $values)
{
list ($link, $label1, $label2) = $this->prepare($values);
// Create link
$this->db->startTransaction();
$res = $this->db->table(self::TABLE)->save($link);
if (! $res) {
$this->db->cancelTransaction();
return false;
}
// Create label 1
$label1['link_id'] = $this->db->getConnection()->lastInsertId(self::TABLE);
$res = $this->db->table(self::TABLE_LABEL)->save($label1);
if (! $res) {
$this->db->cancelTransaction();
return false;
}
// Create label 2 if any
if (null != $label2 && self::BEHAVIOUR_BOTH != $label1['behaviour']) {
$label2['link_id'] = $label1['link_id'];
$res = $this->db->table(self::TABLE_LABEL)->save($label2);
if (! $res) {
$this->db->cancelTransaction();
return false;
}
}
$this->db->closeTransaction();
return $res;
}
/**
* Update a link
*
* @access public
* @param array $values
* Form values
* @return bool
*/
public function update(array $values)
{
list($link, $label1, $label2) = $this->prepare($values);
// Update link
$this->db->startTransaction();
$res = $this->db->table(self::TABLE)
->eq('link_id', $link['link_id'])
->save($link);
if (! $res) {
$this->db->cancelTransaction();
return false;
}
// Update label 1
$this->db->startTransaction();
$res = $this->db->table(self::TABLE_LABEL)
->eq('id', $label1['id'])
->save($label1);
if (! $res) {
$this->db->cancelTransaction();
return false;
}
// Update label 2 (if label 1 not bidirectional)
if (null != $label2 && self::BEHAVIOUR_BOTH != $label1['behaviour']) {
// Create
if (! isset($label2['id']) || null == $label2['id']) {
unset($label2['id']);
$res = $this->db->table(self::TABLE_LABEL)->save($label2);
if (! $res) {
$this->db->cancelTransaction();
return false;
}
$label2['id'] = $this->db->getConnection()->lastInsertId(self::TABLE_LABEL);
$this->taskLink->changeLinkLabel($link['link_id'], $label2['id'], true);
}
// Update
else {
$res = $this->db->table(self::TABLE_LABEL)
->eq('id', $label2['id'])
->save($label2);
if (! $res) {
$this->db->cancelTransaction();
return false;
}
}
}
// Remove label 2 (if label 1 bidirectional)
else {
$this->taskLink->changeLinkLabel($link['link_id'], $label1['id']);
$this->db->table(self::TABLE_LABEL)
->eq('link_id', $link['link_id'])
->neq('id', $label1['id'])
->remove();
}
$this->db->closeTransaction();
return $res;
}
/**
* Remove a link
*
* @access public
* @param integer $link_id
* Link id
* @return bool
*/
public function remove($link_id)
{
$this->db->startTransaction();
if (! $this->db->table(self::TABLE)
->eq('link_id', $link_id)
->remove()) {
$this->db->cancelTransaction();
return false;
}
$this->db->closeTransaction();
return true;
}
/**
* Duplicate links from a project to another one, must be executed inside a transaction
*
* @param integer $src_project_id
* Source project id
* @return integer $dst_project_id Destination project id
* @return boolean
*/
public function duplicate($src_project_id, $dst_project_id)
{
$labels = $this->db->table(self::TABLE_LABEL)
->columns(self::TABLE_LABEL.'.link_id', 'label', 'behaviour')
->eq('project_id', $src_project_id)
->join(self::TABLE, 'link_id', 'link_id')
->asc(self::TABLE_LABEL.'.link_id', 'behaviour')
->findAll();
$this->db->startTransaction();
$link = array('project_id' => $dst_project_id);
if (! $this->db->table(self::TABLE)->save($link)) {
$this->db->cancelTransaction();
return false;
}
$link['id'] = $this->db->getConnection()->lastInsertId(self::TABLE);
foreach ($labels as $label) {
$label['link_id'] = $link['id'];
if (! $this->db->table(self::TABLE_LABEL)->save($label)) {
$this->db->cancelTransaction();
return false;
}
}
$this->db->closeTransaction();
return true;
}
/**
* Validate link 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, $this->commonValidationRules());
return array(
$v->execute(),
$v->getErrors()
);
}
/**
* Validate link 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)
{
$rules = array(
new Validators\Required('link_id', t('The id is required')),
// new Validators\Required('id[0]', t('The label id is required'))
);
$v = new Validator($values, array_merge($rules, $this->commonValidationRules()));
return array(
$v->execute(),
$v->getErrors()
);
}
/**
* Common validation rules
*
* @access private
* @return array
*/
private function commonValidationRules()
{
// TODO Update simple-validator to support array in forms
return array(
new Validators\Required('project_id', t('The project id required')),
// new Validators\Required('label[0]', t('The link label is required')),
new Validators\Integer('project_id', t('The project id must be an integer')),
new Validators\Integer('link_id', t('The link id must be an integer')),
// new Validators\Integer('id[0]', t('The link label id must be an integer')),
// new Validators\Integer('id[1]', t('The link label id must be an integer')),
// new Validators\Integer('behaviour[0]', t('The link label id must be an integer')),
// new Validators\Integer('behaviour[1]', t('The link label id must be an integer')),
// new Validators\MaxLength('label[0]', t('The maximum length is %d characters', 200), 200),
// new Validators\MaxLength('label[1]', t('The maximum length is %d characters', 200), 200)
);
}
}

View File

@ -159,6 +159,7 @@ class TaskDuplication extends Base
if ($new_task_id) {
$this->subtask->duplicate($task_id, $new_task_id);
$this->taskLink->duplicate($task_id, $new_task_id);
}
return $new_task_id;

View File

@ -3,6 +3,7 @@
namespace Model;
use PDO;
use Model\TaskLink;
/**
* Task Finder model
@ -84,6 +85,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',
@ -128,6 +130,29 @@ class TaskFinder extends Base
->asc('tasks.position')
->findAll();
}
/**
* Get ids and names of all (limited by $limit) tasks for a given project and status
*
* @access public
* @param integer $project_id Project id
* @param integer $status_id Status id
* @param integer $exclude_id Exclude this task id in the result
* @param integer $limit Number of tasks to list
* @return array
*/
public function getList($project_id, $status_id = Task::STATUS_OPEN, $exclude_id=null, $limit=50)
{
$sql = $this->db
->hashtable(Task::TABLE)
->eq('project_id', $project_id)
->eq('is_active', $status_id)
->limit($limit);
if (null != $exclude_id) {
$sql->neq('id', $exclude_id);
}
return $sql->getAll('id', 'title');
}
/**
* Get all tasks for a given project and status

361
app/Model/TaskLink.php Normal file
View File

@ -0,0 +1,361 @@
<?php
namespace Model;
use Core\Helper;
use SimpleValidator\Validator;
use SimpleValidator\Validators;
use PDO;
/**
* TaskLink model
*
* @package model
* @author Olivier Maridat
*/
class TaskLink extends Base
{
/**
* SQL table name
*
* @var string
*/
const TABLE = 'task_has_links';
/**
* Get a link by the task link id
*
* @access public
* @param integer $task_link_id
* Task link id
* @return array
*/
public function getById($task_link_id)
{
$sql = 'SELECT
tl1.id AS id,
tl1.link_label_id AS link_label_id,
tl1.task_id AS task_id,
tl1.task_inverse_id AS task_inverse_id,
tl2.id AS task_link_inverse_id
FROM ' . self::TABLE . ' tl1
LEFT JOIN ' . Link::TABLE_LABEL . ' l1 ON l1.id = tl1.link_label_id
LEFT JOIN ' . Link::TABLE_LABEL . ' l2 ON l2.link_id = l1.link_id
LEFT JOIN ' . self::TABLE . ' tl2 ON tl2.task_id = tl1.task_inverse_id
AND ( (l1.behaviour = 2 AND tl2.link_label_id = l1.id) OR (tl2.link_label_id = l2.id) )
WHERE tl1.id = ?
';
$rq = $this->db->execute($sql, array(
$task_link_id
));
return $rq->fetch();
}
/**
* Get the id of the inverse task link by a task link id
*
* @access public
* @param integer $link_id
* Task link id
* @return integer
*/
public function getInverseTaskLinkId($task_link_id)
{
$sql = 'SELECT
tl2.id
FROM ' . self::TABLE . ' tl1
LEFT JOIN ' . Link::TABLE_LABEL . ' l1 ON l1.id = tl1.link_label_id
LEFT JOIN ' . Link::TABLE_LABEL . ' l2 ON l2.link_id = l1.link_id
LEFT JOIN ' . self::TABLE . ' tl2 ON tl2.task_id = tl1.task_inverse_id
AND ( (l1.behaviour = 2 AND tl2.link_label_id = l1.id) OR (tl2.link_label_id = l2.id) )
WHERE tl1.id = ?
';
$rq = $this->db->execute($sql, array(
$task_link_id
));
return $rq->fetchColumn(0);
}
/**
* Return all links for a given task
*
* @access public
* @param integer $task_id
* Task id
* @return array
*/
public function getAll($task_id)
{
$sql = 'SELECT
tl1.id,
l.label AS label,
t2.id AS task_inverse_id,
t2.project_id AS task_inverse_project_id,
t2.title AS task_inverse_title,
t2.is_active AS task_inverse_is_active,
t2cat.name AS task_inverse_category
FROM ' . self::TABLE . ' tl1
LEFT JOIN ' . Link::TABLE_LABEL . ' l ON l.id = tl1.link_label_id
LEFT JOIN ' . Task::TABLE . ' t2 ON t2.id = tl1.task_inverse_id
LEFT JOIN ' . Category::TABLE . ' t2cat ON t2cat.id = t2.category_id
WHERE tl1.task_id = ?
ORDER BY l.label, t2cat.name, t2.id
';
$rq = $this->db->execute($sql, array(
$task_id
));
$res = $rq->fetchAll(PDO::FETCH_ASSOC);
return $res;
}
/**
* Prepare data before insert/update
*
* @access public
* @param array $values
* Form values
*/
public function prepare(array &$values)
{
$this->removeFields($values, array(
'another_link'
));
$taskLink1 = array(
'link_label_id' => $values['link_label_id'],
'task_id' => $values['task_id'],
'task_inverse_id' => $values['task_inverse_id']
);
$taskLink2 = array(
'link_label_id' => $this->link->getInverseLinkLabelId($taskLink1['link_label_id']),
'task_id' => $values['task_inverse_id'],
'task_inverse_id' => $values['task_id']
);
if (isset($values['id']) && isset($values['task_link_inverse_id'])) {
$taskLink1['id'] = $values['id'];
$taskLink2['id'] = $values['task_link_inverse_id'];
}
return array(
$taskLink1,
$taskLink2
);
}
/**
* Create a link
*
* @access public
* @param array $values
* Form values
* @return bool integer
*/
public function create(array $values)
{
list($taskLink1, $taskLink2) = $this->prepare($values);
$this->db->startTransaction();
if (! $this->db->table(self::TABLE)->save($taskLink1)) {
$this->db->cancelTransaction();
return false;
}
if (! $this->db->table(self::TABLE)->save($taskLink2)) {
$this->db->cancelTransaction();
return false;
}
$this->db->closeTransaction();
return true;
}
/**
* Update a link
*
* @access public
* @param array $values
* Form values
* @return bool
*/
public function update(array $values)
{
list($taskLink1, $taskLink2) = $this->prepare($values);
$this->db->startTransaction();
if (! $this->db->table(self::TABLE)
->eq('id', $taskLink1['id'])
->save($taskLink1)) {
$this->db->cancelTransaction();
return false;
}
if (! $this->db->table(self::TABLE)
->eq('id', $taskLink2['id'])
->save($taskLink2)) {
$this->db->cancelTransaction();
return false;
}
$this->db->closeTransaction();
return true;
}
/**
* Remove a link
*
* @access public
* @param integer $task_link_id
* Task Link id
* @return bool
*/
public function remove($task_link_id)
{
$task_link_inverse_id = $this->getInverseTaskLinkId($task_link_id);
$this->db->startTransaction();
if (! $this->db->table(self::TABLE)
->eq('id', $task_link_id)
->remove()) {
$this->db->cancelTransaction();
return false;
}
if (! $this->db->table(self::TABLE)
->eq('id', $task_link_inverse_id)
->remove()) {
$this->db->cancelTransaction();
return false;
}
$this->db->closeTransaction();
return true;
}
/**
* Duplicate all links to another task
*
* @access public
* @param integer $src_task_id
* Source task id
* @param integer $dst_task_id
* Destination task id
* @return bool
*/
public function duplicate($src_task_id, $dst_task_id)
{
return $this->db->transaction(function ($db) use($src_task_id, $dst_task_id)
{
$links = $db->table(TaskLink::TABLE)
->columns('link_label_id', 'task_id', 'task_inverse_id')
->eq('task_id', $src_task_id)
->asc('id')
->findAll();
foreach ($links as &$link) {
$link['task_id'] = $dst_task_id;
if (! $db->table(TaskLink::TABLE)
->save($link)) {
return false;
}
}
$links = $db->table(TaskLink::TABLE)
->columns('link_label_id', 'task_id', 'task_inverse_id')
->eq('task_inverse_id', $src_task_id)
->asc('id')
->findAll();
foreach ($links as &$link) {
$link['task_inverse_id'] = $dst_task_id;
if (! $db->table(TaskLink::TABLE)
->save($link)) {
return false;
}
}
});
}
/**
* Move a task link from a link label to an other
*
* @access public
* @param integer $link_id
* Link id
* @param integer $dst_link_label_id
* Destination link label id
* @return bool
*/
public function changeLinkLabel($link_id, $dst_link_label_id, $alternate=false)
{
$taskLinks = $this->db->table(Link::TABLE_LABEL)
->eq('link_id', $link_id)
->neq(Link::TABLE_LABEL.'.id', $dst_link_label_id)
->join(self::TABLE, 'link_label_id', 'id')
->asc(self::TABLE.'.id')
->findAllByColumn(self::TABLE.'.id');
foreach ($taskLinks as $i => $taskLinkId) {
if (null == $taskLinkId || ($alternate && 0 != $i % 2)) {
continue;
}
if (! $this->db->table(self::TABLE)
->eq('id', $taskLinkId)
->save(array('link_label_id' => $dst_link_label_id))) {
return false;
}
}
return true;
}
/**
* Validate link 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, $this->commonValidationRules());
$res = array(
$v->execute(),
$v->getErrors()
);
return $res;
}
/**
* Validate link 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)
{
$rules = array(
new Validators\Required('id', t('The id is required'))
);
$v = new Validator($values, array_merge($rules, $this->commonValidationRules()));
$res = array(
$v->execute(),
$v->getErrors()
);
return $res;
}
/**
* Common validation rules
*
* @access private
* @return array
*/
private function commonValidationRules()
{
return array(
new Validators\Required('link_label_id', t('The link type is required')),
new Validators\Required('task_id', t('The task id is required')),
new Validators\Required('task_inverse_id', t('The linked task id is required')),
new Validators\Integer('id', t('The id must be an integer')),
new Validators\Integer('link_label_id', t('The link id must be an integer')),
new Validators\Integer('task_id', t('The task id must be an integer')),
new Validators\Integer('task_inverse_id', t('The related task id must be an integer')),
new Validators\Integer('task_link_inverse_id', t('The related task link id must be an integer')),
new Validators\NotEquals('task_inverse_id', 'task_id', t('A task can not be linked to itself')),
new Validators\Exists('task_inverse_id', t('This linked task id doesn\'t exist'), $this->db->getConnection(), Task::TABLE, 'id'),
new Validators\Unique(array(
'task_inverse_id',
'link_label_id',
'task_id'
), t('The exact same link already exists'), $this->db->getConnection(), self::TABLE)
);
}
}

View File

@ -4,8 +4,60 @@ namespace Schema;
use PDO;
use Core\Security;
use Model\Link;
const VERSION = 45;
const VERSION = 46;
function version_46($pdo)
{
$pdo->exec("CREATE TABLE link
(
link_id INT NOT NULL AUTO_INCREMENT,
project_id INT NOT NULL DEFAULT -1,
PRIMARY KEY(link_id)
) ENGINE=InnoDB CHARSET=utf8");
$pdo->exec("CREATE TABLE link_label
(
id INT NOT NULL AUTO_INCREMENT,
link_id INT NOT NULL,
label TEXT NOT NULL,
behaviour INT DEFAULT 2,
PRIMARY KEY(id),
FOREIGN KEY(link_id) REFERENCES link(link_id) ON DELETE CASCADE
) ENGINE=InnoDB CHARSET=utf8");
$pdo->exec("CREATE TABLE task_has_links
(
id INT NOT NULL AUTO_INCREMENT,
link_label_id INT NOT NULL,
task_id INT NOT NULL,
task_inverse_id INT NOT NULL,
PRIMARY KEY(id),
FOREIGN KEY(link_label_id) REFERENCES link_label(id) ON DELETE CASCADE,
FOREIGN KEY(task_id) REFERENCES tasks(id) ON DELETE CASCADE,
FOREIGN KEY(task_inverse_id) REFERENCES tasks(id) ON DELETE CASCADE
) 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_label_id, task_id, task_inverse_id)");
$rq = $pdo->prepare('INSERT INTO link (project_id) VALUES (?)');
$rq->execute(array(-1));
$rq->execute(array(-1));
$rq->execute(array(-1));
$rq->execute(array(-1));
$rq->execute(array(-1));
$rq->execute(array(-1));
$rq = $pdo->prepare('INSERT INTO link_label (link_id, label, behaviour) VALUES (?, ?, ?)');
$rq->execute(array(1, t('relates to'), Link::BEHAVIOUR_BOTH));
$rq->execute(array(2, t('blocks'), Link::BEHAVIOUR_LEFT2RIGTH));
$rq->execute(array(2, t('is blocked by'), Link::BEHAVIOUR_RIGHT2LEFT));
$rq->execute(array(3, t('duplicates'), Link::BEHAVIOUR_LEFT2RIGTH));
$rq->execute(array(3, t('is duplicated by'), Link::BEHAVIOUR_RIGHT2LEFT));
$rq->execute(array(4, t('is a child of'), Link::BEHAVIOUR_LEFT2RIGTH));
$rq->execute(array(4, t('is a parent of'), Link::BEHAVIOUR_RIGHT2LEFT));
$rq->execute(array(5, t('targets milestone'), Link::BEHAVIOUR_LEFT2RIGTH));
$rq->execute(array(5, t('is a milestone of'), Link::BEHAVIOUR_RIGHT2LEFT));
$rq->execute(array(6, t('fixes'), Link::BEHAVIOUR_LEFT2RIGTH));
$rq->execute(array(6, t('is fixed by'), Link::BEHAVIOUR_RIGHT2LEFT));
}
function version_45($pdo)
{

View File

@ -4,8 +4,57 @@ namespace Schema;
use PDO;
use Core\Security;
use Model\Link;
const VERSION = 26;
const VERSION = 27;
function version_27($pdo)
{
$pdo->exec("CREATE TABLE link
(
link_id SERIAL PRIMARY KEY,
project_id INTEGER NOT NULL DEFAULT -1
) ENGINE=InnoDB CHARSET=utf8");
$pdo->exec("CREATE TABLE link_label
(
id SERIAL PRIMARY KEY,
link_id INTEGER NOT NULL,
label TEXT NOT NULL,
behaviour INTEGER NOT NULL DEFAULT 2,
FOREIGN KEY(link_id) REFERENCES link(link_id) ON DELETE CASCADE
) ENGINE=InnoDB CHARSET=utf8");
$pdo->exec("CREATE TABLE task_has_links
(
id SERIAL PRIMARY KEY,
link_label_id INTEGER NOT NULL,
task_id INTEGER NOT NULL,
task_inverse_id INTEGER NOT NULL,
FOREIGN KEY(link_label_id) REFERENCES link_label(id) ON DELETE CASCADE,
FOREIGN KEY(task_id) REFERENCES tasks(id) ON DELETE CASCADE,
FOREIGN KEY(task_inverse_id) REFERENCES tasks(id) ON DELETE CASCADE
) 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_label_id, task_id, task_inverse_id)");
$rq = $pdo->prepare('INSERT INTO link (project_id) VALUES (?)');
$rq->execute(array(-1));
$rq->execute(array(-1));
$rq->execute(array(-1));
$rq->execute(array(-1));
$rq->execute(array(-1));
$rq->execute(array(-1));
$rq = $pdo->prepare('INSERT INTO link_label (link_id, label, behaviour) VALUES (?, ?, ?)');
$rq->execute(array(1, t('relates to'), Link::BEHAVIOUR_BOTH));
$rq->execute(array(2, t('blocks'), Link::BEHAVIOUR_LEFT2RIGTH));
$rq->execute(array(2, t('is blocked by'), Link::BEHAVIOUR_RIGHT2LEFT));
$rq->execute(array(3, t('duplicates'), Link::BEHAVIOUR_LEFT2RIGTH));
$rq->execute(array(3, t('is duplicated by'), Link::BEHAVIOUR_RIGHT2LEFT));
$rq->execute(array(4, t('is a child of'), Link::BEHAVIOUR_LEFT2RIGTH));
$rq->execute(array(4, t('is a parent of'), Link::BEHAVIOUR_RIGHT2LEFT));
$rq->execute(array(5, t('targets milestone'), Link::BEHAVIOUR_LEFT2RIGTH));
$rq->execute(array(5, t('is a milestone of'), Link::BEHAVIOUR_RIGHT2LEFT));
$rq->execute(array(6, t('fixes'), Link::BEHAVIOUR_LEFT2RIGTH));
$rq->execute(array(6, t('is fixed by'), Link::BEHAVIOUR_RIGHT2LEFT));
}
function version_26($pdo)
{
@ -66,7 +115,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)

View File

@ -4,8 +4,57 @@ namespace Schema;
use Core\Security;
use PDO;
use Model\Link;
const VERSION = 44;
const VERSION = 45;
function version_45($pdo)
{
$pdo->exec("CREATE TABLE link
(
link_id INTEGER PRIMARY KEY,
project_id INTEGER NOT NULL DEFAULT -1
)");
$pdo->exec("CREATE TABLE link_label
(
id INTEGER PRIMARY KEY,
link_id INTEGER NOT NULL,
label TEXT NOT NULL,
behaviour INTEGER DEFAULT '2',
FOREIGN KEY(link_id) REFERENCES link(link_id) ON DELETE CASCADE
)");
$pdo->exec("CREATE TABLE task_has_links
(
id INTEGER PRIMARY KEY,
link_label_id INTEGER NOT NULL,
task_id INTEGER NOT NULL,
task_inverse_id INTEGER NOT NULL,
FOREIGN KEY(link_label_id) REFERENCES link_label(id) ON DELETE CASCADE,
FOREIGN KEY(task_id) REFERENCES tasks(id) ON DELETE CASCADE,
FOREIGN KEY(task_inverse_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_label_id, task_id, task_inverse_id)");
$rq = $pdo->prepare('INSERT INTO link (project_id) VALUES (?)');
$rq->execute(array(-1));
$rq->execute(array(-1));
$rq->execute(array(-1));
$rq->execute(array(-1));
$rq->execute(array(-1));
$rq->execute(array(-1));
$rq = $pdo->prepare('INSERT INTO link_label (link_id, label, behaviour) VALUES (?, ?, ?)');
$rq->execute(array(1, t('relates to'), Link::BEHAVIOUR_BOTH));
$rq->execute(array(2, t('blocks'), Link::BEHAVIOUR_LEFT2RIGTH));
$rq->execute(array(2, t('is blocked by'), Link::BEHAVIOUR_RIGHT2LEFT));
$rq->execute(array(3, t('duplicates'), Link::BEHAVIOUR_LEFT2RIGTH));
$rq->execute(array(3, t('is duplicated by'), Link::BEHAVIOUR_RIGHT2LEFT));
$rq->execute(array(4, t('is a child of'), Link::BEHAVIOUR_LEFT2RIGTH));
$rq->execute(array(4, t('is a parent of'), Link::BEHAVIOUR_RIGHT2LEFT));
$rq->execute(array(5, t('targets milestone'), Link::BEHAVIOUR_LEFT2RIGTH));
$rq->execute(array(5, t('is a milestone of'), Link::BEHAVIOUR_RIGHT2LEFT));
$rq->execute(array(6, t('fixes'), Link::BEHAVIOUR_LEFT2RIGTH));
$rq->execute(array(6, t('is fixed by'), Link::BEHAVIOUR_RIGHT2LEFT));
}
function version_44($pdo)
{
@ -66,7 +115,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)

View File

@ -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',

View File

@ -100,7 +100,7 @@
<?php endif ?>
<?php if (! empty($task['date_due']) || ! empty($task['nb_files']) || ! empty($task['nb_comments']) || ! empty($task['description']) || ! empty($task['nb_subtasks'])): ?>
<?php if (! empty($task['date_due']) || ! empty($task['nb_files']) || ! empty($task['nb_comments']) || ! empty($task['description']) || ! empty($task['nb_subtasks']) || ! empty($task['nb_links'])): ?>
<div class="task-board-footer">
<?php if (! empty($task['date_due'])): ?>
@ -110,7 +110,10 @@
<?php endif ?>
<div class="task-board-icons">
<?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 ?>
@ -132,4 +135,4 @@
</div>
<?php endif ?>
</div>
</div>

View File

@ -0,0 +1,28 @@
<section class="tooltip-tasklinks">
<div>
<ul>
<?php
$previous_link = null;
foreach ($links as $link): ?>
<?php if (null == $previous_link || $previous_link != $link['label']): ?>
<?php if (null != $previous_link): ?>
</ul>
</li>
<?php endif ?>
<?php $previous_link = $link['label']; ?>
<li><?= t($this->e($link['label'])) ?>
<ul>
<?php endif ?>
<li<?php if (0 == $link['task_inverse_is_active']): ?> class="task-closed"<?php endif ?>>
<?= $this->e($link['task_inverse_category']) ?>
<?= $this->a('#'.$this->e($link['task_inverse_id']).' - '.trim($this->e($link['task_inverse_title'])),
'task',
'show',
array('task_id' => $link['task_inverse_id'], 'project_id' => $link['task_inverse_project_id'])) ?>
</li>
<?php endforeach ?>
</ul>
</li>
</ul>
</div>
</section>

View File

@ -10,6 +10,9 @@
<li>
<?= $this->a(t('Board settings'), 'config', 'board') ?>
</li>
<li>
<?= $this->a(t('Links settings'), 'link', 'index') ?>
</li>
<li>
<?= $this->a(t('Webhooks'), 'config', 'webhook') ?>
</li>

View File

@ -0,0 +1,49 @@
<section id="link-edit-section">
<?php use Model\Link;
if (! isset($edit)): ?>
<h3><?= t('Add a new link label') ?></h3>
<?php else: ?>
<div class="page-header">
<h2><?= t('Edit the link label') ?></h2>
</div>
<?php endif ?>
<form method="post" action="<?= $this->u('link', isset($edit) ? 'update' : 'save', array('project_id' => $project['id'], 'link_id' => @$values['id'])) ?>" autocomplete="off">
<?= $this->formCsrf() ?>
<?php if (isset($edit)): ?>
<?= $this->formHidden('link_id', $values) ?>
<?= $this->formHidden('id[0]', $values[0]) ?>
<?php if (isset($values[1])): ?>
<?= $this->formHidden('id[1]', $values[1]) ?>
<?php endif ?>
<?php endif ?>
<?= $this->formHidden('project_id', $values) ?>
<?= $this->formLabel(t('Link Label'), 'label[0]') ?>
<?= $this->formText('label[0]', $values[0], $errors, array('required', 'autofocus', 'placeholder="'.t('precedes').'"')) ?> &raquo;
<?= $this->formCheckbox('behaviour[0]', t('Bidrectional link label'), Link::BEHAVIOUR_BOTH, (isset($values[0]['behaviour']) && Link::BEHAVIOUR_BOTH == $values[0]['behaviour']), 'behaviour') ?>
<div class="link-inverse-label">
<?= $this->formLabel(t('Link Inverse Label'), 'label[1]') ?>
&laquo; <?= $this->formText('label[1]', isset($values[1]) ? $values[1] : $values, $errors, array('placeholder="'.t('follows').'"')) ?>
</div>
<div class="form-actions">
<input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/>
<?php if (isset($edit)): ?>
<?= t('or') ?>
<?= $this->a(t('cancel'), 'link', 'index', array('project_id' => $project['id'])) ?>
<?php endif ?>
</div>
<?php if (! isset($edit)): ?>
<div class="alert alert-info">
<strong><?= t('Example:') ?></strong>
<i><?= t('#9 precedes #10') ?></i>
<?= t('and therefore') ?>
<i><?= t('#10 follows #9') ?></i>
</div>
<?php endif ?>
</form>
</section>

View File

@ -0,0 +1,30 @@
<div class="page-header">
<h2><?= t('Link labels') ?></h2>
</div>
<section>
<?php if (! empty($links)): ?>
<table>
<tr>
<th width="70%"><?= t('Link labels') ?></th>
<th><?= t('Actions') ?></th>
</tr>
<?php foreach ($links as $link): ?>
<tr>
<td><?= t($this->e($link['label'])) ?><?php if (isset($link['label_inverse']) && !empty($link['label_inverse'])): ?> | <?= t($this->e($link['label_inverse'])) ?><?php endif ?></td>
<td>
<ul>
<?= $this->a(t('Edit'), 'link', 'edit', array('link_id' => $link['link_id'], 'project_id' => $link['project_id'])) ?>
<?= t('or') ?>
<?= $this->a(t('Remove'), 'link', 'confirm', array('link_id' => $link['link_id'], 'project_id' => $link['project_id'])) ?>
</ul>
</td>
</tr>
<?php endforeach ?>
</table>
<?php else: ?>
<?= t('There is no link yet.') ?>
<?php endif ?>
</section>
<?= $this->render('link/edit', array('values' => $values, 'errors' => $errors, 'project' => $project)) ?>

View File

@ -0,0 +1,17 @@
<section id="main">
<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"?', t($link[0]['label']).(isset($link[1]['label']) ? ' | '.t($link[1]['label']) : '')) ?>
</p>
<div class="form-actions">
<?= $this->a(t('Yes'), 'link', 'remove', array('project_id' => $project['id'], 'link_id' => $link[0]['link_id']), true, 'btn btn-red') ?>
<?= t('or') ?>
<?= $this->a(t('cancel'), 'link', 'index', array('project_id' => $project['id'])) ?>
</div>
</div>
</section>

View File

@ -16,6 +16,13 @@
'not_editable' => true
)) ?>
<?= $this->render('tasklink/show', array(
'task' => $task,
'links' => $links,
'project' => $project,
'not_editable' => true
)) ?>
<?= $this->render('task/comments', array(
'task' => $task,
'comments' => $comments,

View File

@ -3,5 +3,6 @@
<?= $this->render('task/show_description', array('task' => $task)) ?>
<?= $this->render('subtask/show', array('task' => $task, 'subtasks' => $subtasks)) ?>
<?= $this->render('task/timesheet', array('task' => $task)) ?>
<?= $this->render('tasklink/show', array('task' => $task, 'links' => $links, 'link_list' => $link_list, 'task_list' => $task_list)) ?>
<?= $this->render('file/show', array('task' => $task, 'files' => $files)) ?>
<?= $this->render('task/comments', array('task' => $task, 'comments' => $comments, 'project' => $project)) ?>
<?= $this->render('task/comments', array('task' => $task, 'comments' => $comments, 'project' => $project)) ?>

View File

@ -18,6 +18,9 @@
<li>
<?= $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>

View File

@ -0,0 +1,53 @@
<div class="page-header">
<?php if (! isset($edit)): ?>
<h2><?= t('Add a link') ?></h2>
<?php else: ?>
<h2><?= t('Edit a link') ?></h2>
<?php endif ?>
</div>
<?php if (!empty($link_list)): ?>
<form method="post" action="<?= $this->u('tasklink', isset($edit) ? 'update' : 'save', array('task_id' => $task['id'], 'link_id' => @$values['id'], 'project_id' => $task['project_id'])) ?>" autocomplete="off">
<?= $this->formCsrf() ?>
<?php if (isset($edit)): ?>
<?= $this->formHidden('id', $values) ?>
<?= $this->formHidden('task_link_inverse_id', $values) ?>
<?php endif ?>
<?= $this->formHidden('task_id', $values) ?>
#<?= $task['id'] ?>
&#160;
<?= $this->formSelect('link_label_id', $link_list, $values, $errors, 'required autofocus') ?>
&#160;
#<?= $this->formNumeric('task_inverse_id', $values, $errors, array('required', 'placeholder="'.t('Task id').'"', 'title="'.t('Linked task id').'"', 'list="task_inverse_ids"')) ?>
<?php if (!empty($task_list)): ?>
<datalist id="task_inverse_ids">
<select>
<?php foreach ($task_list as $task_inverse_id => $task_inverse_title): ?>
<option value="<?= $task_inverse_id ?>">#<?= $task_inverse_id.' '.$task_inverse_title ?></option>
<?php endforeach ?>
</select>
</datalist>
<?php endif ?>
<br/>
<?php if (! isset($edit)): ?>
<?= $this->formCheckbox('another_link', t('Create another link'), 1, isset($values['another_link']) && $values['another_link'] == 1) ?>
<?php endif ?>
<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>
<?php else: ?>
<div class="alert alert-info">
<?= t('You need to add link labels to this project before to link this task to another one.') ?>
<ul>
<li><?= $this->a(t('Add link labels'), 'link', 'index', array('project_id' => $task['project_id'])) ?></li>
</ul>
</div>
<?php endif ?>

View File

@ -0,0 +1,17 @@
<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 #%s?', $link['task_inverse_id']) ?>
<br />
</p>
<div class="form-actions">
<?= $this->a(t('Yes'), 'tasklink', 'remove', array('task_id' => $task['id'], 'link_id' => $link['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>

View File

@ -0,0 +1,68 @@
<?php if (! empty($links)): ?>
<aside id="links" class="task-show-section">
<div class="page-header">
<h2><?= t('Links') ?></h2>
</div>
<table class="link-table">
<tr>
<th><?= t('Label') ?></th>
<th width="70%"><?= t('Task') ?></th>
<?php if (! isset($not_editable)): ?>
<th><?= t('Actions') ?></th>
<?php endif ?>
</tr>
<?php $previous_link = null;
foreach ($links as $link): ?>
<tr>
<td>
<?php if (null == $previous_link || $previous_link != $link['label']):
$previous_link = $link['label']; ?>
<?= t($this->e($link['label'])) ?>
<?php endif ?>
</td>
<td>
<?php if (0 == $link['task_inverse_is_active']): ?><span class="task-closed"><?php endif ?>
<?= $this->e($link['task_inverse_category']) ?>
<?php if (! isset($not_editable)): ?>
<?= $this->a('#'.$this->e($link['task_inverse_id']).' - '.trim($this->e($link['task_inverse_title'])), 'task', 'show', array('task_id' => $link['task_inverse_id'], 'project_id' => $link['task_inverse_project_id'])) ?>
<?php else: ?>
<?= $this->a('#'.$this->e($link['task_inverse_id']).' - '.trim($this->e($link['task_inverse_title'])), 'task', 'readonly', array('task_id' => $link['task_inverse_id'], 'project_id' => $link['task_inverse_project_id'], 'token' => $project['token'])) ?>
<?php endif ?>
<?php if (0 == $link['task_inverse_is_active']): ?></span><?php endif ?>
</td>
<?php if (! isset($not_editable)): ?>
<td>
<ul>
<li><?= $this->a(t('Edit'), 'tasklink', 'edit', array('task_id' => $task['id'], 'link_id' => $link['id'], 'project_id' => $task['project_id'])) ?></li>
<li><?= $this->a(t('Remove'), 'tasklink', 'confirm', array('task_id' => $task['id'], 'link_id' => $link['id'], 'project_id' => $task['project_id'])) ?></li>
</ul>
</td>
<?php endif ?>
</tr>
<?php endforeach ?>
</table>
<?php if (! isset($not_editable) && !empty($link_list)): ?>
<form method="post" action="<?= $this->u('tasklink', 'save', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>" autocomplete="off">
<?= $this->formCsrf() ?>
<?= $this->formHidden('task_id', array('task_id' => $task['id'])) ?>
#<?= $this->e($task['id']) ?>
&#160;
<?= $this->formSelect('link_label_id', $link_list, array(), array(), 'required autofocus') ?>
&#160;
#<?= $this->formNumeric('task_inverse_id', array(), array(), array('required', 'placeholder="'.t('Task id').'"', 'title="'.t('Linked task id').'"', 'list="task_inverse_ids"')) ?>
<?php if (!empty($task_list)): ?>
<datalist id="task_inverse_ids">
<select>
<?php foreach ($task_list as $task_inverse_id => $task_inverse_title): ?>
<option value="<?= $task_inverse_id ?>">#<?= $task_inverse_id.' '.$task_inverse_title ?></option>
<?php endforeach ?>
</select>
</datalist>
<?php endif ?>
<input type="submit" value="<?= t('Add') ?>" class="btn btn-blue"/>
</form>
<?php endif ?>
</aside>
<?php endif ?>

View File

@ -901,6 +901,8 @@ textarea {
select {
max-width: 95%;
paddin-top: 5px;
paddin-bottom: 5px;
}
::-webkit-input-placeholder {
@ -1267,6 +1269,9 @@ div.ui-tooltip {
width: 550px;
}
.tooltip-tasklinks ul{
padding-left: 13px;
}
.column-tooltip {
color: #999;
font-size: 0.95em;
@ -1632,6 +1637,7 @@ a.task-board-nobody {
padding: 3px;
}
<<<<<<< HEAD
.task-days-age {
font-size: 0.8em;
border: #888 1px solid;
@ -1651,6 +1657,12 @@ a.task-board-nobody {
border-top-right-radius: 3px;
border-bottom-right-radius: 3px;
}
=======
.task-closed {
text-decoration: line-through;
}
>>>>>>> 3ba416a8c3c0cbf9119f97de86eb447dbf8a18db
/* comments */
.comment {
margin-bottom: 20px;

View File

@ -55,6 +55,8 @@ textarea {
select {
max-width: 95%;
paddin-top: 5px;
paddin-bottom: 5px;
}
::-webkit-input-placeholder {

View File

@ -225,3 +225,7 @@ a.task-board-nobody {
border-top-right-radius: 3px;
border-bottom-right-radius: 3px;
}
.task-closed {
text-decoration: line-through;
}

View File

@ -64,6 +64,9 @@ div.ui-tooltip {
width: 550px;
}
.tooltip-tasklinks ul{
padding-left: 13px;
}
.column-tooltip {
color: #999;
font-size: 0.95em;

View File

@ -154,6 +154,7 @@ Kanboard.Analytic=function(){jQuery(document).ready(function(){Kanboard.Exists("
Kanboard.Analytic.CFD=function(){return{Init:function(){jQuery.getJSON($("#chart").attr("data-url"),function(a){var c=a.labels,b=a.columns,d=a.metrics;a=[];for(var e=0;e<d.length;e++){var f={};f[c.column]=d[e].column_title;f[c.day]=d[e].day;f[c.total]=d[e].total;a.push(f)}d=dimple.newSvg("#chart","100%",380);a=new dimple.chart(d,a);a.addCategoryAxis("x",c.day).addOrderRule("Date");a.addMeasureAxis("y",c.total);a.addSeries(c.column,dimple.plot.area).addOrderRule(b.reverse());a.addLegend(10,10,500,
30,"left");a.draw()})}}}();Kanboard.Analytic.TaskRepartition=function(){return{Init:function(){jQuery.getJSON($("#chart").attr("data-url"),function(a){var c=a.labels,b=a.metrics;a=[];for(var d=0;d<b.length;d++){var e={};e[c.nb_tasks]=b[d].nb_tasks;e[c.column_title]=b[d].column_title;a.push(e)}b=dimple.newSvg("#chart","100%",350);a=new dimple.chart(b,a);a.addMeasureAxis("p",c.nb_tasks);a.addSeries(c.column_title,dimple.plot.pie).innerRadius="50%";a.addLegend(0,0,100,"100%","left");a.draw()})}}}();
Kanboard.Analytic.UserRepartition=function(){return{Init:function(){jQuery.getJSON($("#chart").attr("data-url"),function(a){var c=a.labels,b=a.metrics;a=[];for(var d=0;d<b.length;d++){var e={};e[c.nb_tasks]=b[d].nb_tasks;e[c.user]=b[d].user;a.push(e)}b=dimple.newSvg("#chart","100%",350);a=new dimple.chart(b,a);a.addMeasureAxis("p",c.nb_tasks);a.addSeries(c.user,dimple.plot.pie).innerRadius="50%";a.addLegend(0,0,100,"100%","left");a.draw()})}}}();
Kanboard.Link=function(){function a(){$(".behaviour").prop("checked")?$(".link-inverse-label").hide():$(".link-inverse-label").show()}jQuery(document).ready(function(){Kanboard.Exists("link-edit-section")&&(a(),$(".behaviour").click(a))})}();
Kanboard.Swimlane=function(){function a(a){$(".swimlane-row-"+a).css("display","none");$(".show-icon-swimlane-"+a).css("display","inline");$(".hide-icon-swimlane-"+a).css("display","none")}function c(){var a="hidden_swimlanes_"+$("#board").data("project-id");return JSON.parse(Kanboard.GetStorageItem(a))||[]}jQuery(document).ajaxComplete(function(){c().map(function(b){a(b)})});jQuery(document).ready(function(){c().map(function(b){a(b)})});jQuery(document).on("click",".board-swimlane-toggle",function(b){b.preventDefault();
b=$(this).data("swimlane-id");if(-1<c().indexOf(b)){var d="hidden_swimlanes_"+$("#board").data("project-id"),e=JSON.parse(Kanboard.GetStorageItem(d))||[],f=e.indexOf(b);-1<f&&e.splice(f,1);Kanboard.SetStorageItem(d,JSON.stringify(e));$(".swimlane-row-"+b).css("display","table-row");$(".show-icon-swimlane-"+b).css("display","none");$(".hide-icon-swimlane-"+b).css("display","inline")}else d="hidden_swimlanes_"+$("#board").data("project-id"),e=JSON.parse(Kanboard.GetStorageItem(d))||[],e.push(b),Kanboard.SetStorageItem(d,
JSON.stringify(e)),a(b)})}();

19
assets/js/src/link.js Normal file
View File

@ -0,0 +1,19 @@
Kanboard.Link = (function() {
function on_change() {
if ($('.behaviour').prop('checked')) {
$('.link-inverse-label').hide();
}
else {
$('.link-inverse-label').show();
}
}
jQuery(document).ready(function() {
if (Kanboard.Exists("link-edit-section")) {
on_change();
$(".behaviour").click(on_change);
}
});
})();

View File

@ -1,25 +1,27 @@
{
"require": {
"ext-mbstring": "*",
"fguillot/simple-validator": "0.0.1",
"swiftmailer/swiftmailer": "@stable",
"fguillot/json-rpc": "0.0.1",
"fguillot/picodb": "dev-master",
"erusev/parsedown": "1.5.1",
"lusitanian/oauth": "0.3.5",
"pimple/pimple": "~3.0",
"symfony/console": "@stable",
"symfony/event-dispatcher": "~2.6",
"fguillot/simpleLogger": "0.0.1"
},
"autoload": {
"psr-0": {"": "app/"},
"files": [
"app/functions.php",
"app/Libs/password.php"
]
},
"require-dev": {
"symfony/stopwatch": "~2.6"
}
}
"require" : {
"ext-mbstring" : "*",
"fguillot/simple-validator" : "0.0.2",
"swiftmailer/swiftmailer" : "@stable",
"fguillot/json-rpc" : "0.0.1",
"fguillot/picodb" : "dev-master",
"erusev/parsedown" : "1.5.1",
"lusitanian/oauth" : "0.3.5",
"pimple/pimple" : "~3.0",
"symfony/console" : "@stable",
"symfony/event-dispatcher" : "~2.6",
"fguillot/simpleLogger" : "0.0.1"
},
"autoload" : {
"psr-0" : {
"" : "app/"
},
"files" : [
"app/functions.php",
"app/Libs/password.php"
]
},
"require-dev" : {
"symfony/stopwatch" : "~2.6"
}
}

13
composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at http://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
"This file is @generated automatically"
],
"hash": "fdd9fc2aa1f8bdbc3e21d06ff0c7b184",
"hash": "7ccc1bcdeca71cacef30015aca5a483a",
"packages": [
{
"name": "erusev/parsedown",
@ -121,16 +121,16 @@
},
{
"name": "fguillot/simple-validator",
"version": "v0.0.1",
"version": "v0.0.2",
"source": {
"type": "git",
"url": "https://github.com/fguillot/simpleValidator.git",
"reference": "3bfa1ef0062906c83824ce8db1219914996d9bd4"
"reference": "9e9502c88ce239901c0fee12a1f504948342ab42"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/fguillot/simpleValidator/zipball/3bfa1ef0062906c83824ce8db1219914996d9bd4",
"reference": "3bfa1ef0062906c83824ce8db1219914996d9bd4",
"url": "https://api.github.com/repos/fguillot/simpleValidator/zipball/9e9502c88ce239901c0fee12a1f504948342ab42",
"reference": "9e9502c88ce239901c0fee12a1f504948342ab42",
"shasum": ""
},
"require": {
@ -154,7 +154,7 @@
],
"description": "The most easy to use validator library for PHP :)",
"homepage": "https://github.com/fguillot/simpleValidator",
"time": "2014-11-25 22:58:14"
"time": "2015-01-21 02:00:33"
},
{
"name": "fguillot/simpleLogger",
@ -564,6 +564,7 @@
"symfony/console": 0
},
"prefer-stable": false,
"prefer-lowest": false,
"platform": {
"ext-mbstring": "*"
},

View File

@ -3,7 +3,7 @@
app_css="base links title table form button alert tooltip header board task comment subtask markdown listing activity dashboard pagination popover confirm sidebar responsive dropdown"
vendor_css="jquery-ui-1.10.4.custom chosen.min fullcalendar.min font-awesome.min"
app_js="base board calendar analytic swimlane dashboard"
app_js="base board calendar analytic link swimlane dashboard"
vendor_js="jquery-1.11.1.min jquery-ui-1.10.4.custom.min jquery.ui.touch-punch.min chosen.jquery.min dropit.min moment.min fullcalendar.min mousetrap.min app.min"
lang_js="da de es fi fr hu it ja pl pt-br ru sv th zh-cn"