Merge branch 'master' into project_name_in_task_links
* master: ajout captures pour les vues essai intégration image Append filters instead of replacing value for users and categories dropdowns Do not show empty swimlanes in public view Show complexity sum across all swimlanes Show number of tasks for each column across all swimlanes Fix regression (css) Change swimlane layout to save space on the screen Add the possibility to set/unset max column height (scrolling) settings chapter Add getPluginHomepage() Add page to show the list of plugins Do not use preventDefault() with .close-popover when there is no popover opened Show "Open this task" in dropdown menu for closed tasks Add contributor Update app.css Update popover.css Show assignee on card only when someone is assigned
This commit is contained in:
@@ -27,7 +27,7 @@ class Board extends Base
|
||||
}
|
||||
|
||||
// Display the board with a specific layout
|
||||
$this->response->html($this->template->layout('board/public_view', array(
|
||||
$this->response->html($this->template->layout('board/view_public', array(
|
||||
'project' => $project,
|
||||
'swimlanes' => $this->board->getBoard($project['id']),
|
||||
'title' => $project['name'],
|
||||
@@ -49,7 +49,7 @@ class Board extends Base
|
||||
{
|
||||
$params = $this->getProjectFilters('board', 'show');
|
||||
|
||||
$this->response->html($this->template->layout('board/private_view', array(
|
||||
$this->response->html($this->template->layout('board/view_private', array(
|
||||
'categories_list' => $this->category->getList($params['project']['id'], false),
|
||||
'users_list' => $this->projectPermission->getMemberList($params['project']['id'], false),
|
||||
'swimlanes' => $this->taskFilter->search($params['filters']['search'])->getBoard($params['project']['id']),
|
||||
@@ -136,7 +136,7 @@ class Board extends Base
|
||||
}
|
||||
|
||||
$values = $this->request->getJson();
|
||||
$this->userSession->setFilters($project_id, $values['search']);
|
||||
$this->userSession->setFilters($project_id, empty($values['search']) ? '' : $values['search']);
|
||||
|
||||
$this->response->html($this->renderBoard($project_id));
|
||||
}
|
||||
|
||||
@@ -77,6 +77,19 @@ class Config extends Base
|
||||
)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the plugin page
|
||||
*
|
||||
* @access public
|
||||
*/
|
||||
public function plugins()
|
||||
{
|
||||
$this->response->html($this->layout('config/plugins', array(
|
||||
'plugins' => $this->pluginLoader->plugins,
|
||||
'title' => t('Settings').' > '.t('Plugins'),
|
||||
)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the application settings page
|
||||
*
|
||||
|
||||
@@ -18,36 +18,8 @@ class Taskstatus extends Base
|
||||
public function close()
|
||||
{
|
||||
$task = $this->getTask();
|
||||
$redirect = $this->request->getStringParam('redirect');
|
||||
|
||||
if ($this->request->getStringParam('confirmation') === 'yes') {
|
||||
|
||||
$this->checkCSRFParam();
|
||||
|
||||
if ($this->taskStatus->close($task['id'])) {
|
||||
$this->session->flash(t('Task closed successfully.'));
|
||||
} else {
|
||||
$this->session->flashError(t('Unable to close this task.'));
|
||||
}
|
||||
|
||||
if ($redirect === 'board') {
|
||||
$this->response->redirect($this->helper->url->to('board', 'show', array('project_id' => $task['project_id'])));
|
||||
}
|
||||
|
||||
$this->response->redirect($this->helper->url->to('task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])));
|
||||
}
|
||||
|
||||
if ($this->request->isAjax()) {
|
||||
$this->response->html($this->template->render('task_status/close', array(
|
||||
'task' => $task,
|
||||
'redirect' => $redirect,
|
||||
)));
|
||||
}
|
||||
|
||||
$this->response->html($this->taskLayout('task_status/close', array(
|
||||
'task' => $task,
|
||||
'redirect' => $redirect,
|
||||
)));
|
||||
$this->changeStatus($task, 'close', t('Task closed successfully.'), t('Unable to close this task.'));
|
||||
$this->renderTemplate($task, 'task_status/close');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -58,22 +30,46 @@ class Taskstatus extends Base
|
||||
public function open()
|
||||
{
|
||||
$task = $this->getTask();
|
||||
$redirect = $this->request->getStringParam('redirect');
|
||||
|
||||
$this->changeStatus($task, 'open', t('Task opened successfully.'), t('Unable to open this task.'));
|
||||
$this->renderTemplate($task, 'task_status/open');
|
||||
}
|
||||
|
||||
private function changeStatus(array $task, $method, $success_message, $failure_message)
|
||||
{
|
||||
if ($this->request->getStringParam('confirmation') === 'yes') {
|
||||
|
||||
$this->checkCSRFParam();
|
||||
|
||||
if ($this->taskStatus->open($task['id'])) {
|
||||
$this->session->flash(t('Task opened successfully.'));
|
||||
if ($this->taskStatus->$method($task['id'])) {
|
||||
$this->session->flash($success_message);
|
||||
} else {
|
||||
$this->session->flashError(t('Unable to open this task.'));
|
||||
$this->session->flashError($failure_message);
|
||||
}
|
||||
|
||||
$this->response->redirect($this->helper->url->to('task', 'show', array('project_id' => $task['project_id'], 'task_id' => $task['id'])));
|
||||
if ($this->request->getStringParam('redirect') === 'board') {
|
||||
$this->response->redirect($this->helper->url->to('board', 'show', array('project_id' => $task['project_id'])));
|
||||
}
|
||||
|
||||
$this->response->redirect($this->helper->url->to('task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])));
|
||||
}
|
||||
}
|
||||
|
||||
private function renderTemplate(array $task, $template)
|
||||
{
|
||||
$redirect = $this->request->getStringParam('redirect');
|
||||
|
||||
if ($this->request->isAjax()) {
|
||||
$this->response->html($this->template->render($template, array(
|
||||
'task' => $task,
|
||||
'redirect' => $redirect,
|
||||
)));
|
||||
}
|
||||
|
||||
$this->response->html($this->taskLayout('task_status/open', array(
|
||||
$this->response->html($this->taskLayout($template, array(
|
||||
'task' => $task,
|
||||
'redirect' => $redirect,
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ use Pimple\Container;
|
||||
* @property \Core\ObjectStorage\ObjectStorageInterface $objectStorage
|
||||
* @property \Core\Cache\Cache $memoryCache
|
||||
* @property \Core\Plugin\Hook $hook
|
||||
* @property \Core\Plugin\Loader $pluginLoader
|
||||
* @property \Integration\BitbucketWebhook $bitbucketWebhook
|
||||
* @property \Integration\GithubWebhook $githubWebhook
|
||||
* @property \Integration\GitlabWebhook $gitlabWebhook
|
||||
|
||||
@@ -44,4 +44,69 @@ abstract class Base extends \Core\Base
|
||||
call_user_func($callback, $container);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get plugin name
|
||||
*
|
||||
* This method should be overrided by your Plugin class
|
||||
*
|
||||
* @access public
|
||||
* @return string
|
||||
*/
|
||||
public function getPluginName()
|
||||
{
|
||||
return ucfirst(substr(get_called_class(), 7, -7));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get plugin description
|
||||
*
|
||||
* This method should be overrided by your Plugin class
|
||||
*
|
||||
* @access public
|
||||
* @return string
|
||||
*/
|
||||
public function getPluginDescription()
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get plugin author
|
||||
*
|
||||
* This method should be overrided by your Plugin class
|
||||
*
|
||||
* @access public
|
||||
* @return string
|
||||
*/
|
||||
public function getPluginAuthor()
|
||||
{
|
||||
return '?';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get plugin version
|
||||
*
|
||||
* This method should be overrided by your Plugin class
|
||||
*
|
||||
* @access public
|
||||
* @return string
|
||||
*/
|
||||
public function getPluginVersion()
|
||||
{
|
||||
return '?';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get plugin homepage
|
||||
*
|
||||
* This method should be overrided by your Plugin class
|
||||
*
|
||||
* @access public
|
||||
* @return string
|
||||
*/
|
||||
public function getPluginHomepage()
|
||||
{
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,14 @@ class Loader extends \Core\Base
|
||||
*/
|
||||
const TABLE_SCHEMA = 'plugin_schema_versions';
|
||||
|
||||
/**
|
||||
* Plugin instances
|
||||
*
|
||||
* @access public
|
||||
* @var array
|
||||
*/
|
||||
public $plugins = array();
|
||||
|
||||
/**
|
||||
* Scan plugin folder and load plugins
|
||||
*
|
||||
@@ -55,6 +63,7 @@ class Loader extends \Core\Base
|
||||
Tool::buildDic($this->container, $instance->getClasses());
|
||||
|
||||
$instance->initialize();
|
||||
$this->plugins[] = $instance;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1012,4 +1012,10 @@ return array(
|
||||
// 'Table of contents' => '',
|
||||
// 'Gantt' => '',
|
||||
// 'Help with project permissions' => '',
|
||||
// 'Author' => '',
|
||||
// 'Version' => '',
|
||||
// 'Plugins' => '',
|
||||
// 'There is no plugin loaded.' => '',
|
||||
// 'Set maximum column height' => '',
|
||||
// 'Remove maximum column height' => '',
|
||||
);
|
||||
|
||||
@@ -1012,4 +1012,10 @@ return array(
|
||||
// 'Table of contents' => '',
|
||||
// 'Gantt' => '',
|
||||
// 'Help with project permissions' => '',
|
||||
// 'Author' => '',
|
||||
// 'Version' => '',
|
||||
// 'Plugins' => '',
|
||||
// 'There is no plugin loaded.' => '',
|
||||
// 'Set maximum column height' => '',
|
||||
// 'Remove maximum column height' => '',
|
||||
);
|
||||
|
||||
@@ -1012,4 +1012,10 @@ return array(
|
||||
// 'Table of contents' => '',
|
||||
// 'Gantt' => '',
|
||||
// 'Help with project permissions' => '',
|
||||
// 'Author' => '',
|
||||
// 'Version' => '',
|
||||
// 'Plugins' => '',
|
||||
// 'There is no plugin loaded.' => '',
|
||||
// 'Set maximum column height' => '',
|
||||
// 'Remove maximum column height' => '',
|
||||
);
|
||||
|
||||
@@ -1012,4 +1012,10 @@ return array(
|
||||
'Table of contents' => 'Tabla de contenido',
|
||||
'Gantt' => 'Gantt',
|
||||
'Help with project permissions' => 'Ayuda con permisos del proyecto',
|
||||
// 'Author' => '',
|
||||
// 'Version' => '',
|
||||
// 'Plugins' => '',
|
||||
// 'There is no plugin loaded.' => '',
|
||||
// 'Set maximum column height' => '',
|
||||
// 'Remove maximum column height' => '',
|
||||
);
|
||||
|
||||
@@ -1012,4 +1012,10 @@ return array(
|
||||
// 'Table of contents' => '',
|
||||
// 'Gantt' => '',
|
||||
// 'Help with project permissions' => '',
|
||||
// 'Author' => '',
|
||||
// 'Version' => '',
|
||||
// 'Plugins' => '',
|
||||
// 'There is no plugin loaded.' => '',
|
||||
// 'Set maximum column height' => '',
|
||||
// 'Remove maximum column height' => '',
|
||||
);
|
||||
|
||||
@@ -1014,4 +1014,10 @@ return array(
|
||||
'Table of contents' => 'Table des matières',
|
||||
'Gantt' => 'Gantt',
|
||||
'Help with project permissions' => 'Aide avec les permissions des projets',
|
||||
'Author' => 'Auteur',
|
||||
'Version' => 'Version',
|
||||
'Plugins' => 'Extensions',
|
||||
'There is no plugin loaded.' => 'Il n\'y a aucune extension chargée.',
|
||||
'Set maximum column height' => 'Définir la hauteur max. des colonnes',
|
||||
'Remove maximum column height' => 'Enlever la hauteur max. des colonnes',
|
||||
);
|
||||
|
||||
@@ -1012,4 +1012,10 @@ return array(
|
||||
// 'Table of contents' => '',
|
||||
// 'Gantt' => '',
|
||||
// 'Help with project permissions' => '',
|
||||
// 'Author' => '',
|
||||
// 'Version' => '',
|
||||
// 'Plugins' => '',
|
||||
// 'There is no plugin loaded.' => '',
|
||||
// 'Set maximum column height' => '',
|
||||
// 'Remove maximum column height' => '',
|
||||
);
|
||||
|
||||
@@ -1012,4 +1012,10 @@ return array(
|
||||
'Table of contents' => 'Daftar isi',
|
||||
'Gantt' => 'Gantt',
|
||||
'Help with project permissions' => 'Bantuan dengan izin proyek',
|
||||
// 'Author' => '',
|
||||
// 'Version' => '',
|
||||
// 'Plugins' => '',
|
||||
// 'There is no plugin loaded.' => '',
|
||||
// 'Set maximum column height' => '',
|
||||
// 'Remove maximum column height' => '',
|
||||
);
|
||||
|
||||
@@ -1012,4 +1012,10 @@ return array(
|
||||
// 'Table of contents' => '',
|
||||
// 'Gantt' => '',
|
||||
// 'Help with project permissions' => '',
|
||||
// 'Author' => '',
|
||||
// 'Version' => '',
|
||||
// 'Plugins' => '',
|
||||
// 'There is no plugin loaded.' => '',
|
||||
// 'Set maximum column height' => '',
|
||||
// 'Remove maximum column height' => '',
|
||||
);
|
||||
|
||||
@@ -1012,4 +1012,10 @@ return array(
|
||||
// 'Table of contents' => '',
|
||||
// 'Gantt' => '',
|
||||
// 'Help with project permissions' => '',
|
||||
// 'Author' => '',
|
||||
// 'Version' => '',
|
||||
// 'Plugins' => '',
|
||||
// 'There is no plugin loaded.' => '',
|
||||
// 'Set maximum column height' => '',
|
||||
// 'Remove maximum column height' => '',
|
||||
);
|
||||
|
||||
@@ -1012,4 +1012,10 @@ return array(
|
||||
'Table of contents' => 'Innholdsfortegnelse',
|
||||
'Gantt' => 'Gantt',
|
||||
'Help with project permissions' => 'Hjelp med prosjekttilganger',
|
||||
// 'Author' => '',
|
||||
// 'Version' => '',
|
||||
// 'Plugins' => '',
|
||||
// 'There is no plugin loaded.' => '',
|
||||
// 'Set maximum column height' => '',
|
||||
// 'Remove maximum column height' => '',
|
||||
);
|
||||
|
||||
@@ -1012,4 +1012,10 @@ return array(
|
||||
// 'Table of contents' => '',
|
||||
// 'Gantt' => '',
|
||||
// 'Help with project permissions' => '',
|
||||
// 'Author' => '',
|
||||
// 'Version' => '',
|
||||
// 'Plugins' => '',
|
||||
// 'There is no plugin loaded.' => '',
|
||||
// 'Set maximum column height' => '',
|
||||
// 'Remove maximum column height' => '',
|
||||
);
|
||||
|
||||
@@ -1012,4 +1012,10 @@ return array(
|
||||
// 'Table of contents' => '',
|
||||
// 'Gantt' => '',
|
||||
// 'Help with project permissions' => '',
|
||||
// 'Author' => '',
|
||||
// 'Version' => '',
|
||||
// 'Plugins' => '',
|
||||
// 'There is no plugin loaded.' => '',
|
||||
// 'Set maximum column height' => '',
|
||||
// 'Remove maximum column height' => '',
|
||||
);
|
||||
|
||||
@@ -1012,4 +1012,10 @@ return array(
|
||||
'Table of contents' => 'Índice',
|
||||
'Gantt' => 'Gantt',
|
||||
'Help with project permissions' => 'Ajuda com as permissões dos projetos',
|
||||
// 'Author' => '',
|
||||
// 'Version' => '',
|
||||
// 'Plugins' => '',
|
||||
// 'There is no plugin loaded.' => '',
|
||||
// 'Set maximum column height' => '',
|
||||
// 'Remove maximum column height' => '',
|
||||
);
|
||||
|
||||
@@ -1012,4 +1012,10 @@ return array(
|
||||
'Table of contents' => 'Tabela de conteúdos',
|
||||
'Gantt' => 'Gantt',
|
||||
'Help with project permissions' => 'Ajuda com permissões de projecto',
|
||||
// 'Author' => '',
|
||||
// 'Version' => '',
|
||||
// 'Plugins' => '',
|
||||
// 'There is no plugin loaded.' => '',
|
||||
// 'Set maximum column height' => '',
|
||||
// 'Remove maximum column height' => '',
|
||||
);
|
||||
|
||||
@@ -1012,4 +1012,10 @@ return array(
|
||||
'Table of contents' => 'Сожержание',
|
||||
'Gantt' => 'Гантт',
|
||||
'Help with project permissions' => 'Помощь с правами доступа по проекту',
|
||||
// 'Author' => '',
|
||||
// 'Version' => '',
|
||||
// 'Plugins' => '',
|
||||
// 'There is no plugin loaded.' => '',
|
||||
// 'Set maximum column height' => '',
|
||||
// 'Remove maximum column height' => '',
|
||||
);
|
||||
|
||||
@@ -1012,4 +1012,10 @@ return array(
|
||||
// 'Table of contents' => '',
|
||||
// 'Gantt' => '',
|
||||
// 'Help with project permissions' => '',
|
||||
// 'Author' => '',
|
||||
// 'Version' => '',
|
||||
// 'Plugins' => '',
|
||||
// 'There is no plugin loaded.' => '',
|
||||
// 'Set maximum column height' => '',
|
||||
// 'Remove maximum column height' => '',
|
||||
);
|
||||
|
||||
@@ -1012,4 +1012,10 @@ return array(
|
||||
// 'Table of contents' => '',
|
||||
// 'Gantt' => '',
|
||||
// 'Help with project permissions' => '',
|
||||
// 'Author' => '',
|
||||
// 'Version' => '',
|
||||
// 'Plugins' => '',
|
||||
// 'There is no plugin loaded.' => '',
|
||||
// 'Set maximum column height' => '',
|
||||
// 'Remove maximum column height' => '',
|
||||
);
|
||||
|
||||
@@ -1012,4 +1012,10 @@ return array(
|
||||
// 'Table of contents' => '',
|
||||
// 'Gantt' => '',
|
||||
// 'Help with project permissions' => '',
|
||||
// 'Author' => '',
|
||||
// 'Version' => '',
|
||||
// 'Plugins' => '',
|
||||
// 'There is no plugin loaded.' => '',
|
||||
// 'Set maximum column height' => '',
|
||||
// 'Remove maximum column height' => '',
|
||||
);
|
||||
|
||||
@@ -1012,4 +1012,10 @@ return array(
|
||||
// 'Table of contents' => '',
|
||||
// 'Gantt' => '',
|
||||
// 'Help with project permissions' => '',
|
||||
// 'Author' => '',
|
||||
// 'Version' => '',
|
||||
// 'Plugins' => '',
|
||||
// 'There is no plugin loaded.' => '',
|
||||
// 'Set maximum column height' => '',
|
||||
// 'Remove maximum column height' => '',
|
||||
);
|
||||
|
||||
@@ -1012,4 +1012,10 @@ return array(
|
||||
// 'Table of contents' => '',
|
||||
// 'Gantt' => '',
|
||||
// 'Help with project permissions' => '',
|
||||
// 'Author' => '',
|
||||
// 'Version' => '',
|
||||
// 'Plugins' => '',
|
||||
// 'There is no plugin loaded.' => '',
|
||||
// 'Set maximum column height' => '',
|
||||
// 'Remove maximum column height' => '',
|
||||
);
|
||||
|
||||
@@ -252,16 +252,24 @@ class Board extends Base
|
||||
$swimlanes[$i]['columns'] = $columns;
|
||||
$swimlanes[$i]['nb_columns'] = $nb_columns;
|
||||
$swimlanes[$i]['nb_tasks'] = 0;
|
||||
$swimlanes[$i]['nb_swimlanes'] = $ilen;
|
||||
|
||||
for ($j = 0; $j < $nb_columns; $j++) {
|
||||
|
||||
$column_id = $columns[$j]['id'];
|
||||
$swimlane_id = $swimlanes[$i]['id'];
|
||||
|
||||
if (! isset($swimlanes[0]['columns'][$j]['nb_column_tasks'])) {
|
||||
$swimlanes[0]['columns'][$j]['nb_column_tasks'] = 0;
|
||||
$swimlanes[0]['columns'][$j]['total_score'] = 0;
|
||||
}
|
||||
|
||||
$swimlanes[$i]['columns'][$j]['tasks'] = $callback === null ? $this->taskFinder->getTasksByColumnAndSwimlane($project_id, $column_id, $swimlane_id) : $callback($project_id, $column_id, $swimlane_id);
|
||||
$swimlanes[$i]['columns'][$j]['nb_tasks'] = count($swimlanes[$i]['columns'][$j]['tasks']);
|
||||
$swimlanes[$i]['columns'][$j]['score'] = $this->getColumnSum($swimlanes[$i]['columns'][$j]['tasks'], 'score');
|
||||
$swimlanes[$i]['nb_tasks'] += $swimlanes[$i]['columns'][$j]['nb_tasks'];
|
||||
$swimlanes[0]['columns'][$j]['nb_column_tasks'] += $swimlanes[$i]['columns'][$j]['nb_tasks'];
|
||||
$swimlanes[0]['columns'][$j]['total_score'] += $swimlanes[$i]['columns'][$j]['score'];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace ServiceProvider;
|
||||
|
||||
use Core\Plugin\Loader;
|
||||
use Core\ObjectStorage\FileStorage;
|
||||
use Core\Paginator;
|
||||
use Core\OAuth2;
|
||||
@@ -117,5 +118,7 @@ class ClassProvider implements ServiceProviderInterface
|
||||
$container['objectStorage'] = function() {
|
||||
return new FileStorage(FILES_DIR);
|
||||
};
|
||||
|
||||
$container['pluginLoader'] = new Loader($container);
|
||||
}
|
||||
}
|
||||
|
||||
51
app/Template/board/table_column.php
Normal file
51
app/Template/board/table_column.php
Normal file
@@ -0,0 +1,51 @@
|
||||
<!-- column titles -->
|
||||
<?php ?>
|
||||
<tr>
|
||||
<?php foreach ($swimlane['columns'] as $column): ?>
|
||||
<th class="board-column-header board-column-header-<?= $column['id'] ?>" data-column-id="<?= $column['id'] ?>">
|
||||
|
||||
<!-- column in collapsed mode -->
|
||||
<div class="board-column-collapsed">
|
||||
<span title="<?= t('Task count') ?>" class="board-column-header-task-count" title="<?= t('Show this column') ?>">
|
||||
<span id="task-number-column-<?= $column['id'] ?>"><?= $column['nb_column_tasks'] ?></span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- column in expanded mode -->
|
||||
<div class="board-column-expanded">
|
||||
<?php if (! $not_editable): ?>
|
||||
<div class="board-add-icon">
|
||||
<?= $this->url->link('+', 'taskcreation', 'create', array('project_id' => $column['project_id'], 'column_id' => $column['id'], 'swimlane_id' => $swimlane['id']), false, 'popover', t('Add a new task')) ?>
|
||||
</div>
|
||||
<?php endif ?>
|
||||
|
||||
<span class="board-column-title" data-column-id="<?= $column['id'] ?>" title="<?= t('Hide this column') ?>">
|
||||
<?= $this->e($column['title']) ?>
|
||||
</span>
|
||||
|
||||
<?php if (! $not_editable && ! empty($column['description'])): ?>
|
||||
<span class="tooltip pull-right" title='<?= $this->e($this->text->markdown($column['description'])) ?>'>
|
||||
<i class="fa fa-info-circle"></i>
|
||||
</span>
|
||||
<?php endif ?>
|
||||
|
||||
<?php if (! empty($column['score'])): ?>
|
||||
<span class="pull-right" title="<?= t('Score') ?>">
|
||||
<?= $column['total_score'] ?>
|
||||
</span>
|
||||
<?php endif ?>
|
||||
|
||||
<span title="<?= t('Total number of tasks in this column across all swimlanes') ?>" class="board-column-header-task-count">
|
||||
(<span id="task-number-column-<?= $column['id'] ?>"><?= $column['nb_column_tasks'] ?></span>)
|
||||
</span>
|
||||
|
||||
<?php if ($column['task_limit']): ?>
|
||||
<span title="<?= t('Task limit') ?>">
|
||||
(<span id="task-number-column-<?= $column['id'] ?>"><?= $this->e($column['task_limit']) ?></span>)
|
||||
</span>
|
||||
<?php endif ?>
|
||||
</div>
|
||||
|
||||
</th>
|
||||
<?php endforeach ?>
|
||||
</tr>
|
||||
@@ -13,18 +13,37 @@
|
||||
>
|
||||
<?php endif ?>
|
||||
|
||||
<?php foreach ($swimlanes as $swimlane): ?>
|
||||
<?php foreach ($swimlanes as $index => $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/table_swimlane', array(
|
||||
'project' => $project,
|
||||
'swimlane' => $swimlane,
|
||||
'board_highlight_period' => $board_highlight_period,
|
||||
'hide_swimlane' => count($swimlanes) === 1,
|
||||
'not_editable' => isset($not_editable),
|
||||
)) ?>
|
||||
<?php if ($index === 0): ?>
|
||||
<?= $this->render('board/table_column', array(
|
||||
'swimlane' => $swimlane,
|
||||
'not_editable' => isset($not_editable),
|
||||
)) ?>
|
||||
<?php endif ?>
|
||||
|
||||
<?php if (! ($swimlane['nb_tasks'] === 0 && isset($not_editable))): ?>
|
||||
|
||||
<?php if ($swimlane['nb_swimlanes'] > 1): ?>
|
||||
<?= $this->render('board/table_swimlane', array(
|
||||
'project' => $project,
|
||||
'swimlane' => $swimlane,
|
||||
'not_editable' => isset($not_editable),
|
||||
)) ?>
|
||||
<?php endif ?>
|
||||
|
||||
<?= $this->render('board/table_tasks', array(
|
||||
'project' => $project,
|
||||
'swimlane' => $swimlane,
|
||||
'not_editable' => isset($not_editable),
|
||||
'board_highlight_period' => $board_highlight_period,
|
||||
)) ?>
|
||||
|
||||
<?php endif ?>
|
||||
|
||||
<?php endif ?>
|
||||
<?php endforeach ?>
|
||||
</table>
|
||||
|
||||
@@ -1,104 +1,26 @@
|
||||
<!-- swimlane -->
|
||||
<tr id="swimlane-<?= $swimlane['id'] ?>">
|
||||
<!-- swimlane toggle -->
|
||||
<?php if (! $hide_swimlane): ?>
|
||||
<th class="board-swimlane-header">
|
||||
<?php if (! $not_editable): ?>
|
||||
<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>
|
||||
</a>
|
||||
<th class="board-swimlane-header" colspan="<?= $swimlane['nb_columns'] ?>">
|
||||
<?php if (! $not_editable): ?>
|
||||
<a href="#" class="board-swimlane-toggle" data-swimlane-id="<?= $swimlane['id'] ?>">
|
||||
<i class="fa fa-chevron-circle-up hide-icon-swimlane-<?= $swimlane['id'] ?>" title="<?= t('Collapse swimlane') ?>"></i>
|
||||
<i class="fa fa-chevron-circle-down show-icon-swimlane-<?= $swimlane['id'] ?>" title="<?= t('Expand swimlane') ?>" style="display: none"></i>
|
||||
</a>
|
||||
<?php endif ?>
|
||||
|
||||
<?php if (! empty($swimlane['description'])): ?>
|
||||
<span
|
||||
title="<?= t('Description') ?>"
|
||||
class="tooltip"
|
||||
data-href="<?= $this->url->href('board', 'swimlane', array('swimlane_id' => $swimlane['id'], 'project_id' => $project['id'])) ?>">
|
||||
<i class="fa fa-info-circle"></i>
|
||||
</span>
|
||||
<?php endif ?>
|
||||
<?= $this->e($swimlane['name']) ?>
|
||||
|
||||
<span title="<?= t('Task count') ?>" class="board-column-header-task-count swimlane-task-count-<?= $swimlane['id'] ?>">
|
||||
(<?= $swimlane['nb_tasks'] ?>)
|
||||
</span>
|
||||
|
||||
<span class="board-swimlane-toggle-title show-icon-swimlane-<?= $swimlane['id'] ?>"><?= $this->e($swimlane['name']) ?></span>
|
||||
<?php endif ?>
|
||||
</th>
|
||||
<?php endif ?>
|
||||
|
||||
<!-- column header title -->
|
||||
<?php foreach ($swimlane['columns'] as $column): ?>
|
||||
<th class="board-column-header board-column-header-<?= $column['id'] ?>" data-column-id="<?= $column['id'] ?>">
|
||||
<div class="board-column-collapsed">
|
||||
<span title="<?= t('Task count') ?>" class="board-column-header-task-count" title="<?= t('Show this column') ?>">
|
||||
<span id="task-number-column-<?= $column['id'] ?>"><?= $column['nb_tasks'] ?></span>
|
||||
<?php if (! $not_editable && ! empty($swimlane['description'])): ?>
|
||||
<span
|
||||
title="<?= t('Description') ?>"
|
||||
class="tooltip"
|
||||
data-href="<?= $this->url->href('board', 'swimlane', array('swimlane_id' => $swimlane['id'], 'project_id' => $project['id'])) ?>">
|
||||
<i class="fa fa-info-circle"></i>
|
||||
</span>
|
||||
</div>
|
||||
<div class="board-column-expanded">
|
||||
<?php if (! $not_editable): ?>
|
||||
<div class="board-add-icon">
|
||||
<?= $this->url->link('+', 'taskcreation', 'create', array('project_id' => $column['project_id'], 'column_id' => $column['id'], 'swimlane_id' => $swimlane['id']), false, 'popover', t('Add a new task')) ?>
|
||||
</div>
|
||||
<?php endif ?>
|
||||
<?php endif ?>
|
||||
|
||||
<span class="board-column-title" data-column-id="<?= $column['id'] ?>" title="<?= t('Hide this column') ?>">
|
||||
<?= $this->e($column['title']) ?>
|
||||
</span>
|
||||
|
||||
<?php if (! $not_editable && ! empty($column['description'])): ?>
|
||||
<span class="tooltip pull-right" title='<?= $this->e($this->text->markdown($column['description'])) ?>'>
|
||||
<i class="fa fa-info-circle"></i>
|
||||
</span>
|
||||
<?php endif ?>
|
||||
|
||||
<?php if (! empty($column['score'])): ?>
|
||||
<span class="pull-right" title="<?= t('Score') ?>">
|
||||
<?= $column['score'] ?>
|
||||
</span>
|
||||
<?php endif ?>
|
||||
|
||||
<?php if ($column['task_limit']): ?>
|
||||
<span title="<?= t('Task limit') ?>">
|
||||
(<span id="task-number-column-<?= $column['id'] ?>"><?= $column['nb_tasks'] ?></span>/<?= $this->e($column['task_limit']) ?>)
|
||||
</span>
|
||||
<?php else: ?>
|
||||
<span title="<?= t('Task count') ?>" class="board-column-header-task-count">
|
||||
(<span id="task-number-column-<?= $column['id'] ?>"><?= $column['nb_tasks'] ?></span>)
|
||||
</span>
|
||||
<?php endif ?>
|
||||
</div>
|
||||
<span title="<?= t('Task count') ?>" class="board-column-header-task-count swimlane-task-count-<?= $swimlane['id'] ?>">
|
||||
(<?= $swimlane['nb_tasks'] ?>)
|
||||
</span>
|
||||
</th>
|
||||
<?php endforeach ?>
|
||||
</tr>
|
||||
<tr class="board-swimlane swimlane-row-<?= $swimlane['id'] ?>">
|
||||
|
||||
<!-- swimlane title -->
|
||||
<?php if (! $hide_swimlane): ?>
|
||||
<th class="board-swimlane-title">
|
||||
<?= $this->e($swimlane['name']) ?>
|
||||
</th>
|
||||
<?php endif ?>
|
||||
|
||||
<!-- task list -->
|
||||
<?php foreach ($swimlane['columns'] as $column): ?>
|
||||
<td class="board-column-<?= $column['id'] ?> <?= $column['task_limit'] && $column['nb_tasks'] > $column['task_limit'] ? 'board-task-list-limit' : '' ?>">
|
||||
<div class="board-task-list board-column-expanded" data-column-id="<?= $column['id'] ?>" data-swimlane-id="<?= $swimlane['id'] ?>" data-task-limit="<?= $column['task_limit'] ?>">
|
||||
<?php foreach ($column['tasks'] as $task): ?>
|
||||
<?= $this->render($not_editable ? 'board/task_public' : 'board/task_private', array(
|
||||
'project' => $project,
|
||||
'task' => $task,
|
||||
'board_highlight_period' => $board_highlight_period,
|
||||
'not_editable' => $not_editable,
|
||||
)) ?>
|
||||
<?php endforeach ?>
|
||||
</div>
|
||||
<div class="board-column-collapsed">
|
||||
<div class="board-rotation-wrapper">
|
||||
<div class="board-column-title board-rotation" data-column-id="<?= $column['id'] ?>" title="<?= t('Show this column') ?>">
|
||||
<?= $this->e($column['title']) ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<?php endforeach ?>
|
||||
</tr>
|
||||
31
app/Template/board/table_tasks.php
Normal file
31
app/Template/board/table_tasks.php
Normal file
@@ -0,0 +1,31 @@
|
||||
<!-- task row -->
|
||||
<tr class="board-swimlane swimlane-row-<?= $swimlane['id'] ?>">
|
||||
<?php foreach ($swimlane['columns'] as $column): ?>
|
||||
<td class="
|
||||
board-column-<?= $column['id'] ?>
|
||||
<?= $column['task_limit'] > 0 && $column['nb_tasks'] > $column['task_limit'] ? 'board-task-list-limit' : '' ?>
|
||||
">
|
||||
|
||||
<!-- tasks list -->
|
||||
<div class="board-task-list board-column-expanded" data-column-id="<?= $column['id'] ?>" data-swimlane-id="<?= $swimlane['id'] ?>" data-task-limit="<?= $column['task_limit'] ?>">
|
||||
<?php foreach ($column['tasks'] as $task): ?>
|
||||
<?= $this->render($not_editable ? 'board/task_public' : 'board/task_private', array(
|
||||
'project' => $project,
|
||||
'task' => $task,
|
||||
'board_highlight_period' => $board_highlight_period,
|
||||
'not_editable' => $not_editable,
|
||||
)) ?>
|
||||
<?php endforeach ?>
|
||||
</div>
|
||||
|
||||
<!-- column in collapsed mode (rotated text) -->
|
||||
<div class="board-column-collapsed">
|
||||
<div class="board-rotation-wrapper">
|
||||
<div class="board-column-title board-rotation" data-column-id="<?= $column['id'] ?>" title="<?= t('Show this column') ?>">
|
||||
<i class="fa fa-chevron-circle-up tooltip" title="<?= $this->e($column['title']) ?>"></i> <?= $this->e($column['title']) ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<?php endforeach ?>
|
||||
</tr>
|
||||
@@ -8,6 +8,10 @@
|
||||
<li><i class="fa fa-comment-o fa-fw"></i> <?= $this->url->link(t('Add a comment'), 'comment', 'create', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'popover') ?></li>
|
||||
<li><i class="fa fa-code-fork fa-fw"></i> <?= $this->url->link(t('Add a link'), 'tasklink', 'create', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'popover') ?></li>
|
||||
<li><i class="fa fa-camera fa-fw"></i> <?= $this->url->link(t('Add a screenshot'), 'board', 'screenshot', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'popover') ?></li>
|
||||
<li><i class="fa fa-close fa-fw"></i> <?= $this->url->link(t('Close this task'), 'taskstatus', 'close', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'redirect' => 'board'), false, 'popover') ?></li>
|
||||
<?php if ($task['is_active'] == 1): ?>
|
||||
<li><i class="fa fa-close fa-fw"></i> <?= $this->url->link(t('Close this task'), 'taskstatus', 'close', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'redirect' => 'board'), false, 'popover') ?></li>
|
||||
<?php else: ?>
|
||||
<li><i class="fa fa-check-square-o fa-fw"></i> <?= $this->url->link(t('Open this task'), 'taskstatus', 'open', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'redirect' => 'board'), false, 'popover') ?></li>
|
||||
<?php endif ?>
|
||||
</ul>
|
||||
</span>
|
||||
@@ -9,10 +9,11 @@
|
||||
data-task-url="<?= $this->url->href('task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>">
|
||||
|
||||
<div class="task-board-sort-handle" style="display: none;"><i class="fa fa-arrows-alt"></i></div>
|
||||
<?= $this->render('board/task_menu', array('task' => $task)) ?>
|
||||
|
||||
<?php if ($this->board->isCollapsed($project['id'])): ?>
|
||||
<?php if ($this->board->isCollapsed($task['project_id'])): ?>
|
||||
<div class="task-board-collapsed">
|
||||
<?= $this->render('board/task_menu', array('task' => $task)) ?>
|
||||
|
||||
<?php if (! empty($task['assignee_username'])): ?>
|
||||
<span title="<?= $this->e($task['assignee_name'] ?: $task['assignee_username']) ?>">
|
||||
<?= $this->e($this->user->getInitials($task['assignee_name'] ?: $task['assignee_username'])) ?>
|
||||
@@ -22,6 +23,7 @@
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="task-board-expanded">
|
||||
<?= $this->render('board/task_menu', array('task' => $task)) ?>
|
||||
|
||||
<?php if ($task['reference']): ?>
|
||||
<span class="task-board-reference" title="<?= t('Reference') ?>">
|
||||
@@ -29,9 +31,10 @@
|
||||
</span>
|
||||
<?php endif ?>
|
||||
|
||||
<?php if (! empty($task['owner_id'])): ?>
|
||||
<span class="task-board-user <?= $this->user->isCurrentUser($task['owner_id']) ? 'task-board-current-user' : '' ?>">
|
||||
<?= $this->url->link(
|
||||
(! empty($task['owner_id']) ? ($task['assignee_name'] ?: $task['assignee_username']) : t('Nobody assigned')),
|
||||
$task['assignee_name'] ?: $task['assignee_username'],
|
||||
'board',
|
||||
'changeAssignee',
|
||||
array('task_id' => $task['id'], 'project_id' => $task['project_id']),
|
||||
@@ -40,6 +43,7 @@
|
||||
t('Change assignee')
|
||||
) ?>
|
||||
</span>
|
||||
<?php endif ?>
|
||||
|
||||
<?php if ($task['is_active'] == 1): ?>
|
||||
<div class="task-board-days">
|
||||
|
||||
30
app/Template/config/plugins.php
Normal file
30
app/Template/config/plugins.php
Normal file
@@ -0,0 +1,30 @@
|
||||
<div class="page-header">
|
||||
<h2><?= t('Plugins') ?></h2>
|
||||
</div>
|
||||
|
||||
<?php if (empty($plugins)): ?>
|
||||
<p class="alert"><?= t('There is no plugin loaded.') ?></p>
|
||||
<?php else: ?>
|
||||
<table class="table-stripped">
|
||||
<tr>
|
||||
<th class="column-20"><?= t('Name') ?></th>
|
||||
<th class="column-20"><?= t('Author') ?></th>
|
||||
<th class="column-10"><?= t('Version') ?></th>
|
||||
<th><?= t('Description') ?></th>
|
||||
</tr>
|
||||
|
||||
<?php foreach($plugins as $plugin): ?>
|
||||
<tr>
|
||||
<td>
|
||||
<?php if ($plugin->getPluginHomepage()): ?>
|
||||
<a href="<?= $plugin->getPluginHomepage() ?>" target="_blank" rel="noreferrer"><?= $this->e($plugin->getPluginName()) ?></a>
|
||||
<?php else: ?>
|
||||
<?= $this->e($plugin->getPluginName()) ?>
|
||||
<?php endif ?>
|
||||
</td>
|
||||
<td><?= $this->e($plugin->getPluginAuthor()) ?></td>
|
||||
<td><?= $this->e($plugin->getPluginVersion()) ?></td>
|
||||
<td><?= $this->e($plugin->getPluginDescription()) ?></td>
|
||||
</tr>
|
||||
<?php endforeach ?>
|
||||
<?php endif ?>
|
||||
@@ -4,6 +4,9 @@
|
||||
<li <?= $this->app->getRouterAction() === 'index' ? 'class="active"' : '' ?>>
|
||||
<?= $this->url->link(t('About'), 'config', 'index') ?>
|
||||
</li>
|
||||
<li <?= $this->app->getRouterAction() === 'plugins' ? 'class="active"' : '' ?>>
|
||||
<?= $this->url->link(t('Plugins'), 'config', 'plugins') ?>
|
||||
</li>
|
||||
<li <?= $this->app->getRouterAction() === 'application' ? 'class="active"' : '' ?>>
|
||||
<?= $this->url->link(t('Application settings'), 'config', 'application') ?>
|
||||
</li>
|
||||
|
||||
@@ -21,6 +21,14 @@
|
||||
<i class="fa fa-arrows-h fa-fw"></i> <a href="#" class="filter-toggle-scrolling" title="<?= t('Keyboard shortcut: "%s"', 'c') ?>"><?= t('Horizontal scrolling') ?></a>
|
||||
</span>
|
||||
</li>
|
||||
<li>
|
||||
<span class="filter-max-height" style="display: none">
|
||||
<i class="fa fa-arrows-v fa-fw"></i> <a href="#" class="filter-toggle-height"><?= t('Set maximum column height') ?></a>
|
||||
</span>
|
||||
<span class="filter-min-height">
|
||||
<i class="fa fa-arrows-v fa-fw"></i> <a href="#" class="filter-toggle-height"><?= t('Remove maximum column height') ?></a>
|
||||
</span>
|
||||
</li>
|
||||
<?php endif ?>
|
||||
<?= $this->render('project/dropdown', array('project' => $project)) ?>
|
||||
</ul>
|
||||
@@ -59,10 +67,9 @@
|
||||
<div class="dropdown filters">
|
||||
<i class="fa fa-caret-down"></i> <a href="#" class="dropdown-menu"><?= t('Users') ?></a>
|
||||
<ul>
|
||||
<li><a href="#" class="filter-helper" data-filter="status:open"><?= t('All users') ?></a></li>
|
||||
<li><a href="#" class="filter-helper" data-filter="status:open assignee:nobody"><?= t('Not assigned') ?></a></li>
|
||||
<li><a href="#" class="filter-helper" data-append-filter="assignee:nobody"><?= t('Not assigned') ?></a></li>
|
||||
<?php foreach ($users_list as $user): ?>
|
||||
<li><a href="#" class="filter-helper" data-filter='status:open assignee:"<?= $this->e($user) ?>"'><?= $this->e($user) ?></a></li>
|
||||
<li><a href="#" class="filter-helper" data-append-filter='assignee:"<?= $this->e($user) ?>"'><?= $this->e($user) ?></a></li>
|
||||
<?php endforeach ?>
|
||||
</ul>
|
||||
</div>
|
||||
@@ -72,10 +79,9 @@
|
||||
<div class="dropdown filters">
|
||||
<i class="fa fa-caret-down"></i> <a href="#" class="dropdown-menu"><?= t('Categories') ?></a>
|
||||
<ul>
|
||||
<li><a href="#" class="filter-helper" data-filter="status:open"><?= t('All categories') ?></a></li>
|
||||
<li><a href="#" class="filter-helper" data-filter="status:open category:none"><?= t('No category') ?></a></li>
|
||||
<li><a href="#" class="filter-helper" data-append-filter="category:none"><?= t('No category') ?></a></li>
|
||||
<?php foreach ($categories_list as $category): ?>
|
||||
<li><a href="#" class="filter-helper" data-filter='status:open category:"<?= $this->e($category) ?>"'><?= $this->e($category) ?></a></li>
|
||||
<li><a href="#" class="filter-helper" data-append-filter='category:"<?= $this->e($category) ?>"'><?= $this->e($category) ?></a></li>
|
||||
<?php endforeach ?>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -50,7 +50,7 @@
|
||||
<?= $this->form->hidden('id', $default_swimlane) ?>
|
||||
|
||||
<?= $this->form->label(t('Rename'), 'default_swimlane') ?>
|
||||
<?= $this->form->text('default_swimlane', $default_swimlane, array(), array('autofocus', 'required', 'maxlength="50"')) ?><br/>
|
||||
<?= $this->form->text('default_swimlane', $default_swimlane, array(), array('required', 'maxlength="50"')) ?><br/>
|
||||
|
||||
<?= $this->form->checkbox('show_default_swimlane', t('Show default swimlane'), 1, isset($default_swimlane['show_default_swimlane']) && $default_swimlane['show_default_swimlane'] == 1) ?>
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
<div class="confirm">
|
||||
<p class="alert alert-info">
|
||||
<?= t('Do you really want to close the task "%s" as well as all subtasks?', $this->e($task['title'])) ?>
|
||||
<?= t('Do you really want to close the task "%s" as well as all subtasks?', $task['title']) ?>
|
||||
</p>
|
||||
|
||||
<div class="form-actions">
|
||||
|
||||
@@ -4,12 +4,12 @@
|
||||
|
||||
<div class="confirm">
|
||||
<p class="alert alert-info">
|
||||
<?= t('Do you really want to open this task: "%s"?', $this->e($task['title'])) ?>
|
||||
<?= t('Do you really want to open this task: "%s"?', $task['title']) ?>
|
||||
</p>
|
||||
|
||||
<div class="form-actions">
|
||||
<?= $this->url->link(t('Yes'), 'taskstatus', 'open', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'confirmation' => 'yes'), true, 'btn btn-red') ?>
|
||||
<?= $this->url->link(t('Yes'), 'taskstatus', 'open', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'confirmation' => 'yes', 'redirect' => $redirect), true, 'btn btn-red') ?>
|
||||
<?= t('or') ?>
|
||||
<?= $this->url->link(t('cancel'), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>
|
||||
<?= $this->url->link(t('cancel'), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'close-popover') ?>
|
||||
</div>
|
||||
</div>
|
||||
@@ -33,5 +33,4 @@ if (ENABLE_URL_REWRITE) {
|
||||
require __DIR__.'/routes.php';
|
||||
}
|
||||
|
||||
$plugin = new Core\Plugin\Loader($container);
|
||||
$plugin->scan();
|
||||
$container['pluginLoader']->scan();
|
||||
|
||||
Reference in New Issue
Block a user