Open comments in board view with a modal dialog instead of tooltip

This commit is contained in:
Frederic Guillot 2017-02-11 17:22:10 -05:00
parent 28052edb22
commit 8bf054a480
26 changed files with 195 additions and 95 deletions

View File

@ -3,6 +3,7 @@ Version 1.0.39 (unreleased)
Improvements: Improvements:
* Open comments in board view with a modal dialog instead of tooltip
* Improve card icons alignment on board * Improve card icons alignment on board
* Adjust modal dialog width on mobile devices * Adjust modal dialog width on mobile devices
* Add priority column in list view * Add priority column in list view

View File

@ -69,22 +69,6 @@ class BoardTooltipController extends BaseController
))); )));
} }
/**
* Display comments during a task mouseover
*
* @access public
*/
public function comments()
{
$task = $this->getTask();
$commentSortingDirection = $this->userMetadataCacheDecorator->get(UserMetadataModel::KEY_COMMENT_SORTING_DIRECTION, 'ASC');
$this->response->html($this->template->render('board/tooltip_comments', array(
'task' => $task,
'comments' => $this->commentModel->getAll($task['id'], $commentSortingDirection)
)));
}
/** /**
* Display task description * Display task description
* *

View File

@ -4,7 +4,6 @@ namespace Kanboard\Controller;
use Kanboard\Core\Controller\AccessForbiddenException; use Kanboard\Core\Controller\AccessForbiddenException;
use Kanboard\Core\Controller\PageNotFoundException; use Kanboard\Core\Controller\PageNotFoundException;
use Kanboard\Model\UserMetadataModel;
/** /**
* Comment Controller * Comment Controller
@ -17,12 +16,12 @@ class CommentController extends BaseController
/** /**
* Get the current comment * Get the current comment
* *
* @access private * @access protected
* @return array * @return array
* @throws PageNotFoundException * @throws PageNotFoundException
* @throws AccessForbiddenException * @throws AccessForbiddenException
*/ */
private function getComment() protected function getComment()
{ {
$comment = $this->commentModel->getById($this->request->getIntegerParam('comment_id')); $comment = $this->commentModel->getById($this->request->getIntegerParam('comment_id'));
@ -77,6 +76,8 @@ class CommentController extends BaseController
{ {
$task = $this->getTask(); $task = $this->getTask();
$values = $this->request->getValues(); $values = $this->request->getValues();
$values['task_id'] = $task['id'];
$values['user_id'] = $this->userSession->getId();
list($valid, $errors) = $this->commentValidator->validateCreation($values); list($valid, $errors) = $this->commentValidator->validateCreation($values);
@ -118,7 +119,6 @@ class CommentController extends BaseController
'errors' => $errors, 'errors' => $errors,
'comment' => $comment, 'comment' => $comment,
'task' => $task, 'task' => $task,
'title' => t('Edit a comment')
))); )));
} }
@ -142,10 +142,11 @@ class CommentController extends BaseController
$this->flash->failure(t('Unable to update your comment.')); $this->flash->failure(t('Unable to update your comment.'));
} }
return $this->response->redirect($this->helper->url->to('TaskViewController', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])), false); $this->response->redirect($this->helper->url->to('TaskViewController', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])), true);
return;
} }
return $this->edit($values, $errors); $this->edit($values, $errors);
} }
/** /**
@ -182,7 +183,7 @@ class CommentController extends BaseController
$this->flash->failure(t('Unable to remove this comment.')); $this->flash->failure(t('Unable to remove this comment.'));
} }
$this->response->redirect($this->helper->url->to('TaskViewController', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), 'comments')); $this->response->redirect($this->helper->url->to('TaskViewController', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), 'comments'), true);
} }
/** /**
@ -193,11 +194,7 @@ class CommentController extends BaseController
public function toggleSorting() public function toggleSorting()
{ {
$task = $this->getTask(); $task = $this->getTask();
$this->helper->comment->toggleSorting();
$oldDirection = $this->userMetadataCacheDecorator->get(UserMetadataModel::KEY_COMMENT_SORTING_DIRECTION, 'ASC');
$newDirection = $oldDirection === 'ASC' ? 'DESC' : 'ASC';
$this->userMetadataCacheDecorator->set(UserMetadataModel::KEY_COMMENT_SORTING_DIRECTION, $newDirection);
$this->response->redirect($this->helper->url->to( $this->response->redirect($this->helper->url->to(
'TaskViewController', 'TaskViewController',

View File

@ -0,0 +1,50 @@
<?php
namespace Kanboard\Controller;
use Kanboard\Model\UserMetadataModel;
/**
* Class CommentListController
*
* @package Kanboard\Controller
* @author Frederic Guillot
*/
class CommentListController extends BaseController
{
public function show()
{
$project = $this->getProject();
$task = $this->getTask();
$commentSortingDirection = $this->userMetadataCacheDecorator->get(UserMetadataModel::KEY_COMMENT_SORTING_DIRECTION, 'ASC');
$this->response->html($this->template->render('comment_list/show', array(
'project' => $project,
'task' => $task,
'comments' => $this->commentModel->getAll($task['id'], $commentSortingDirection),
'editable' => $this->helper->user->hasProjectAccess('CommentController', 'edit', $task['project_id']),
)));
}
public function save()
{
$task = $this->getTask();
$values = $this->request->getValues();
$values['task_id'] = $task['id'];
$values['user_id'] = $this->userSession->getId();
list($valid, ) = $this->commentValidator->validateCreation($values);
if ($valid && $this->commentModel->create($values) !== false) {
$this->flash->success(t('Comment added successfully.'));
}
$this->show();
}
public function toggleSorting()
{
$this->helper->comment->toggleSorting();
$this->show();
}
}

View File

@ -15,6 +15,7 @@ use Pimple\Container;
* @property \Kanboard\Helper\AvatarHelper $avatar * @property \Kanboard\Helper\AvatarHelper $avatar
* @property \Kanboard\Helper\BoardHelper $board * @property \Kanboard\Helper\BoardHelper $board
* @property \Kanboard\Helper\CalendarHelper $calendar * @property \Kanboard\Helper\CalendarHelper $calendar
* @property \Kanboard\Helper\CommentHelper $comment
* @property \Kanboard\Helper\DateHelper $dt * @property \Kanboard\Helper\DateHelper $dt
* @property \Kanboard\Helper\FileHelper $file * @property \Kanboard\Helper\FileHelper $file
* @property \Kanboard\Helper\FormHelper $form * @property \Kanboard\Helper\FormHelper $form

View File

@ -0,0 +1,23 @@
<?php
namespace Kanboard\Helper;
use Kanboard\Core\Base;
use Kanboard\Model\UserMetadataModel;
/**
* Class CommentHelper
*
* @package Kanboard\Helper
* @author Frederic Guillot
*/
class CommentHelper extends Base
{
public function toggleSorting()
{
$oldDirection = $this->userMetadataCacheDecorator->get(UserMetadataModel::KEY_COMMENT_SORTING_DIRECTION, 'ASC');
$newDirection = $oldDirection === 'ASC' ? 'DESC' : 'ASC';
$this->userMetadataCacheDecorator->set(UserMetadataModel::KEY_COMMENT_SORTING_DIRECTION, $newDirection);
}
}

View File

@ -47,10 +47,10 @@ class ModalHelper extends Base
return $this->helper->url->link($html, $controller, $action, $params, false, 'js-modal-large'); return $this->helper->url->link($html, $controller, $action, $params, false, 'js-modal-large');
} }
public function medium($icon, $label, $controller, $action, array $params = array()) public function medium($icon, $label, $controller, $action, array $params = array(), $title = '')
{ {
$html = '<i class="fa fa-'.$icon.' fa-fw js-modal-medium" aria-hidden="true"></i>'.$label; $html = '<i class="fa fa-'.$icon.' fa-fw js-modal-medium" aria-hidden="true"></i>'.$label;
return $this->helper->url->link($html, $controller, $action, $params, false, 'js-modal-medium'); return $this->helper->url->link($html, $controller, $action, $params, false, 'js-modal-medium', $title);
} }
public function small($icon, $label, $controller, $action, array $params = array()) public function small($icon, $label, $controller, $action, array $params = array())

View File

@ -70,7 +70,7 @@ class CommentModel extends Base
UserModel::TABLE.'.avatar_path' UserModel::TABLE.'.avatar_path'
) )
->join(UserModel::TABLE, 'id', 'user_id') ->join(UserModel::TABLE, 'id', 'user_id')
->orderBy(self::TABLE.'.date_modification', $sorting) ->orderBy(self::TABLE.'.date_creation', $sorting)
->eq(self::TABLE.'.task_id', $task_id) ->eq(self::TABLE.'.task_id', $task_id)
->findAll(); ->findAll();
} }

View File

@ -83,7 +83,8 @@ class AuthenticationProvider implements ServiceProviderInterface
$acl->add('CalendarController', 'save', Role::PROJECT_MEMBER); $acl->add('CalendarController', 'save', Role::PROJECT_MEMBER);
$acl->add('CategoryController', '*', Role::PROJECT_MANAGER); $acl->add('CategoryController', '*', Role::PROJECT_MANAGER);
$acl->add('ColumnController', '*', Role::PROJECT_MANAGER); $acl->add('ColumnController', '*', Role::PROJECT_MANAGER);
$acl->add('CommentController', '*', Role::PROJECT_MEMBER); $acl->add('CommentController', array('create', 'save', 'edit', 'update', 'confirm', 'remove'), Role::PROJECT_MEMBER);
$acl->add('CommentListController', array('save'), Role::PROJECT_MEMBER);
$acl->add('CustomFilterController', '*', Role::PROJECT_MEMBER); $acl->add('CustomFilterController', '*', Role::PROJECT_MEMBER);
$acl->add('ExportController', '*', Role::PROJECT_MANAGER); $acl->add('ExportController', '*', Role::PROJECT_MANAGER);
$acl->add('TaskFileController', array('screenshot', 'create', 'save', 'remove', 'confirm'), Role::PROJECT_MEMBER); $acl->add('TaskFileController', array('screenshot', 'create', 'save', 'remove', 'confirm'), Role::PROJECT_MEMBER);

View File

@ -22,6 +22,7 @@ class HelperProvider implements ServiceProviderInterface
$container['helper']->register('calendar', '\Kanboard\Helper\CalendarHelper'); $container['helper']->register('calendar', '\Kanboard\Helper\CalendarHelper');
$container['helper']->register('asset', '\Kanboard\Helper\AssetHelper'); $container['helper']->register('asset', '\Kanboard\Helper\AssetHelper');
$container['helper']->register('board', '\Kanboard\Helper\BoardHelper'); $container['helper']->register('board', '\Kanboard\Helper\BoardHelper');
$container['helper']->register('comment', '\Kanboard\Helper\CommentHelper');
$container['helper']->register('dt', '\Kanboard\Helper\DateHelper'); $container['helper']->register('dt', '\Kanboard\Helper\DateHelper');
$container['helper']->register('file', '\Kanboard\Helper\FileHelper'); $container['helper']->register('file', '\Kanboard\Helper\FileHelper');
$container['helper']->register('form', '\Kanboard\Helper\FormHelper'); $container['helper']->register('form', '\Kanboard\Helper\FormHelper');

View File

@ -95,8 +95,15 @@
<span title="<?= t('Attachments') ?>" class="tooltip" data-href="<?= $this->url->href('BoardTooltipController', 'attachments', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>"><i class="fa fa-paperclip"></i>&nbsp;<?= $task['nb_files'] ?></span> <span title="<?= t('Attachments') ?>" class="tooltip" data-href="<?= $this->url->href('BoardTooltipController', 'attachments', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>"><i class="fa fa-paperclip"></i>&nbsp;<?= $task['nb_files'] ?></span>
<?php endif ?> <?php endif ?>
<?php if (! empty($task['nb_comments'])): ?> <?php if ($task['nb_comments'] > 0): ?>
<span title="<?= $task['nb_comments'] == 1 ? t('%d comment', $task['nb_comments']) : t('%d comments', $task['nb_comments']) ?>" class="tooltip" data-href="<?= $this->url->href('BoardTooltipController', 'comments', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>"><i class="fa fa-comment-o"></i>&nbsp;<?= $task['nb_comments'] ?></span> <?= $this->modal->medium(
'comments-o',
$task['nb_comments'],
'CommentListController',
'show',
array('task_id' => $task['id'], 'project_id' => $task['project_id']),
$task['nb_comments'] == 1 ? t('%d comment', $task['nb_comments']) : t('%d comments', $task['nb_comments'])
) ?>
<?php endif ?> <?php endif ?>
<?php if (! empty($task['description'])): ?> <?php if (! empty($task['description'])): ?>

View File

@ -28,7 +28,7 @@
<?= $this->text->e($this->user->getInitials($task['assignee_name'] ?: $task['assignee_username'])) ?> <?= $this->text->e($this->user->getInitials($task['assignee_name'] ?: $task['assignee_username'])) ?>
</span> - </span> -
<?php endif ?> <?php endif ?>
<?= $this->url->link($this->text->e($task['title']), 'TaskViewController', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'task-board-collapsed-title tooltip', $this->text->e($task['title'])) ?> <?= $this->text->e($task['title']) ?>
</div> </div>
<?php else: ?> <?php else: ?>
<div class="task-board-expanded"> <div class="task-board-expanded">
@ -50,7 +50,7 @@
<?= $this->hook->render('template:board:private:task:before-title', array('task' => $task)) ?> <?= $this->hook->render('template:board:private:task:before-title', array('task' => $task)) ?>
<div class="task-board-title"> <div class="task-board-title">
<?= $this->url->link($this->text->e($task['title']), 'TaskViewController', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, '', t('View this task')) ?> <?= $this->text->e($task['title']) ?>
</div> </div>
<?= $this->hook->render('template:board:private:task:after-title', array('task' => $task)) ?> <?= $this->hook->render('template:board:private:task:after-title', array('task' => $task)) ?>

View File

@ -1,9 +0,0 @@
<div class="tooltip-large">
<?php foreach ($comments as $comment): ?>
<?= $this->render('comment/show', array(
'comment' => $comment,
'task' => $task,
'hide_actions' => true,
)) ?>
<?php endforeach ?>
</div>

View File

@ -9,13 +9,6 @@
<small class="comment-date"><?= t('Created at:') ?> <?= $this->dt->datetime($comment['date_creation']) ?></small> <small class="comment-date"><?= t('Created at:') ?> <?= $this->dt->datetime($comment['date_creation']) ?></small>
<small class="comment-date"><?= t('Updated at:')?> <?= $this->dt->datetime($comment['date_modification']) ?></small> <small class="comment-date"><?= t('Updated at:')?> <?= $this->dt->datetime($comment['date_modification']) ?></small>
</div>
<div class="comment-content">
<div class="markdown">
<?= $this->text->markdown($comment['comment'], isset($is_public) && $is_public) ?>
</div>
</div> </div>
<?php if (! isset($hide_actions)): ?> <?php if (! isset($hide_actions)): ?>
@ -36,4 +29,10 @@
</ul> </ul>
</div> </div>
<?php endif ?> <?php endif ?>
<div class="comment-content">
<div class="markdown">
<?= $this->text->markdown($comment['comment'], isset($is_public) && $is_public) ?>
</div>
</div>
</div> </div>

View File

@ -0,0 +1,8 @@
<div class="page-header">
<h2><?= t('Add a comment') ?></h2>
</div>
<form method="post" action="<?= $this->url->href('CommentListController', 'save', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>" autocomplete="off">
<?= $this->form->csrf() ?>
<?= $this->form->textEditor('comment', array(), array(), array('required' => true)) ?>
<?= $this->modal->submitButtons() ?>
</form>

View File

@ -0,0 +1,27 @@
<div class="page-header">
<h2><?= $this->text->e($task['title']) ?> &gt; <?= t('Comments') ?></h2>
<?php if (!isset($is_public) || !$is_public): ?>
<div class="comment-sorting">
<small>
<?= $this->url->icon('sort', t('change sorting'), 'CommentListController', 'toggleSorting', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'js-modal-replace') ?>
</small>
</div>
<?php endif ?>
</div>
<div class="comments">
<?php foreach ($comments as $comment): ?>
<?= $this->render('comment/show', array(
'comment' => $comment,
'task' => $task,
'project' => $project,
'editable' => $editable,
'is_public' => isset($is_public) && $is_public,
)) ?>
<?php endforeach ?>
<?php if ($editable): ?>
<?= $this->render('comment_list/create', array(
'task' => $task,
)) ?>
<?php endif ?>
</div>

View File

@ -26,7 +26,7 @@
'is_public' => true, 'is_public' => true,
)) ?> )) ?>
<?= $this->render('comments/show', array( <?= $this->render('task_comments/show', array(
'task' => $task, 'task' => $task,
'comments' => $comments, 'comments' => $comments,
'project' => $project, 'project' => $project,

View File

@ -54,7 +54,7 @@
<?php if (!empty($comments)): ?> <?php if (!empty($comments)): ?>
<?= $this->hook->render('template:task:show:before-comments', array('task' => $task, 'project' => $project)) ?> <?= $this->hook->render('template:task:show:before-comments', array('task' => $task, 'project' => $project)) ?>
<?= $this->render('comments/show', array( <?= $this->render('task_comments/show', array(
'task' => $task, 'task' => $task,
'comments' => $comments, 'comments' => $comments,
'project' => $project, 'project' => $project,

View File

@ -4,8 +4,5 @@
<?= $this->form->hidden('user_id', $values) ?> <?= $this->form->hidden('user_id', $values) ?>
<?= $this->form->textEditor('comment', $values, $errors, array('required' => true)) ?> <?= $this->form->textEditor('comment', $values, $errors, array('required' => true)) ?>
<?= $this->modal->submitButtons() ?>
<div class="form-actions">
<button type="submit" class="btn btn-blue"><?= t('Save') ?></button>
</div>
</form> </form>

View File

@ -2,7 +2,7 @@
<div class="accordion-title"> <div class="accordion-title">
<h3><a href="#" class="fa accordion-toggle"></a> <?= t('Comments') ?></h3> <h3><a href="#" class="fa accordion-toggle"></a> <?= t('Comments') ?></h3>
</div> </div>
<div class="accordion-content" id="comments"> <div class="accordion-content comments" id="comments">
<?php if (!isset($is_public) || !$is_public): ?> <?php if (!isset($is_public) || !$is_public): ?>
<div class="comment-sorting"> <div class="comment-sorting">
<small> <small>
@ -21,12 +21,8 @@
<?php endforeach ?> <?php endforeach ?>
<?php if ($editable): ?> <?php if ($editable): ?>
<?= $this->render('comments/create', array( <?= $this->render('task_comments/create', array(
'values' => array( 'values' => array(),
'user_id' => $this->user->getId(),
'task_id' => $task['id'],
'project_id' => $task['project_id'],
),
'errors' => array(), 'errors' => array(),
'task' => $task, 'task' => $task,
)) ?> )) ?>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,29 @@
(function () {
function redirectToTaskView(e) {
var ignoreParentElement = KB.dom(e.target).parent('a, .task-board-change-assignee');
if (ignoreParentElement) {
return;
}
var taskElement = KB.dom(e.target).parent('.task-board');
if (taskElement) {
var taskUrl = KB.dom(taskElement).data('taskUrl');
if (taskUrl) {
window.location = taskUrl;
}
}
}
function openEditTask(e) {
var baseElement = KB.dom(e.target).parent('.task-board-change-assignee');
var url = KB.dom(baseElement).data('url');
if (url) {
KB.modal.open(url, 'medium', false);
}
}
KB.onClick('.task-board *', redirectToTaskView);
KB.onClick('.task-board-change-assignee *', openEditTask);
}());

View File

@ -1,20 +0,0 @@
Kanboard.BoardTask = function(app) {
this.app = app;
};
// TODO: rewrite this code
Kanboard.BoardTask.prototype.listen = function() {
var self = this;
$(document).on("click", ".task-board-change-assignee", function(e) {
e.preventDefault();
e.stopPropagation();
KB.modal.open($(this).data("url"), "medium", false);
});
$(document).on("click", ".task-board", function(e) {
if (e.target.tagName != "A" && e.target.tagName != "IMG") {
window.location = $(this).data("task-url");
}
});
};

View File

@ -26,7 +26,6 @@
.comment-title .comment-title
border-bottom: 1px dotted #eee border-bottom: 1px dotted #eee
margin-left: 55px margin-left: 55px
margin-bottom: 10px
.comment-date .comment-date
color: color('light') color: color('light')
@ -34,8 +33,8 @@
.comment-actions .comment-actions
font-size: size('small') font-size: size('small')
margin-left: 55px margin-bottom: 10px
margin-top: 8px text-align: right
li li
display: inline display: inline
a a
@ -47,3 +46,10 @@
.comment-content .comment-content
margin-left: 55px margin-left: 55px
.comments
.text-editor
textarea
height: 90px
.text-editor-preview-area
height: 90px

View File

@ -9,12 +9,14 @@
span span
opacity: 0.5 opacity: 0.5
margin-left: 4px margin-left: 4px
a:hover, span:hover a
opacity: 1.0 &:hover
opacity: 1.0
font-weight: bold
.task-board-icons-row .task-board-icons-row
line-height: 22px line-height: 22px
.task-score .task-score
font-weight: 500 font-weight: bold
.flag-milestone .flag-milestone
color: green color: green