Merge branch 'master' of https://github.com/fguillot/kanboard into 1245_bug_with_subtask_timer

This commit is contained in:
Max Kamashev 2015-09-30 09:19:33 +03:00
commit cabcdc9d9e
88 changed files with 1034 additions and 231 deletions

View File

@ -38,6 +38,7 @@ Contributors:
- [Jean-François Magnier](https://github.com/lefakir)
- [Jesusaplsoft](https://github.com/jesusaplsoft)
- [Jesús Marín](https://github.com/alu0100502114)
- [Jules Verhaeren](https://github.com/julesverhaeren)
- [Karol J](https://github.com/dzudek)
- [Kiswa](https://github.com/kiswa)
- [Kralo](https://github.com/kralo)

View File

@ -18,6 +18,12 @@ Core functionalities moved to plugins:
Improvements:
* Append filters instead of replacing value for users and categories dropdowns
* Do not show empty swimlanes in public view
* Change swimlane layout to save space on the screen
* Add the possibility to set/unset max column height (column scrolling)
* Show "Open this task" in dropdown menu for closed tasks
* Show assignee on card only when someone is assigned (hide nobody text)
* Highlight selected item in dropdown menus
* Gantt chart: change bar color according to task progress
* Replace color dropdown by color picker in task forms

View File

@ -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));
}

View File

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

View File

@ -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,
)));
}
}

View File

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

View File

@ -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 '';
}
}

View File

@ -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;
}
/**

View File

@ -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' => '',
);

View File

@ -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' => '',
);

View File

@ -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' => '',
);

View File

@ -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' => '',
);

View File

@ -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' => '',
);

View File

@ -608,7 +608,7 @@ return array(
'Time Tracking' => 'Feuille de temps',
'You already have one subtask in progress' => 'Vous avez déjà une sous-tâche en progrès',
'Which parts of the project do you want to duplicate?' => 'Quelles parties du projet voulez-vous dupliquer ?',
'Disallow login form' => 'Interdir le formulaire d\'authentification',
'Disallow login form' => 'Interdire le formulaire d\'authentification',
'Bitbucket commit received' => 'Commit reçu via Bitbucket',
'Bitbucket webhooks' => 'Webhook Bitbucket',
'Help on Bitbucket webhooks' => 'Aide sur les webhooks Bitbucket',
@ -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',
);

View File

@ -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' => '',
);

View File

@ -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' => '',
);

View File

@ -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' => '',
);

View File

@ -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' => '',
);

View File

@ -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' => '',
);

View File

@ -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' => '',
);

View File

@ -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' => '',
);

View File

@ -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' => '',
);

View File

@ -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' => '',
);

View File

@ -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' => '',
);

View File

@ -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' => '',
);

View File

@ -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' => '',
);

View File

@ -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' => '',
);

View File

@ -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' => '',
);

View File

@ -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' => '',
);

View File

@ -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'];
}
}

View File

@ -82,13 +82,15 @@ class TaskLink extends Base
Task::TABLE.'.owner_id AS task_assignee_id',
User::TABLE.'.username AS task_assignee_username',
User::TABLE.'.name AS task_assignee_name',
Board::TABLE.'.title AS column_title'
Board::TABLE.'.title AS column_title',
Project::TABLE.'.name AS project_name'
)
->eq(self::TABLE.'.task_id', $task_id)
->join(Link::TABLE, 'id', 'link_id')
->join(Task::TABLE, 'id', 'opposite_task_id')
->join(Board::TABLE, 'id', 'column_id', Task::TABLE)
->join(User::TABLE, 'id', 'owner_id', Task::TABLE)
->join(Project::TABLE, 'id', 'project_id', Task::TABLE)
->asc(Link::TABLE.'.id')
->desc(Board::TABLE.'.position')
->desc(Task::TABLE.'.is_active')

View File

@ -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);
}
}

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

View File

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

View File

@ -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'] ?>&nbsp;
</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>

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

View File

@ -8,6 +8,10 @@
<li><i class="fa fa-comment-o fa-fw"></i>&nbsp;<?= $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>&nbsp;<?= $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>&nbsp;<?= $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>&nbsp;<?= $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>&nbsp;<?= $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>&nbsp;<?= $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>

View File

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

View File

@ -3,6 +3,7 @@
<?php foreach($links as $link): ?>
<li>
<strong><?= t($link['label']) ?></strong>
[<i><?= $link['project_name'] ?></i>]
<?= $this->url->link(
$this->e('#'.$link['task_id'].' - '.$link['title']),
'task', 'show', array('task_id' => $link['task_id'], 'project_id' => $link['project_id']),

View 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 ?>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -6,6 +6,7 @@
<tr>
<th class="column-20"><?= t('Label') ?></th>
<th class="column-30"><?= t('Task') ?></th>
<th class="column-20"><?= t('Project') ?></th>
<th><?= t('Column') ?></th>
<th><?= t('Assignee') ?></th>
<?php if (! isset($not_editable)): ?>
@ -52,6 +53,7 @@
<strong><?= $this->e($link['task_time_estimated']).'h' ?></strong> <?= t('estimated') ?>
<?php endif ?>
</td>
<td><?= $this->e($link['project_name']) ?></td>
<td><?= $this->e($link['column_title']) ?></td>
<td>
<?php if (! empty($link['task_assignee_username'])): ?>

View File

@ -33,5 +33,4 @@ if (ENABLE_URL_REWRITE) {
require __DIR__.'/routes.php';
}
$plugin = new Core\Plugin\Loader($container);
$plugin->scan();
$container['pluginLoader']->scan();

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -61,10 +61,12 @@ td.board-column-task-collapsed {
.board-rotation-wrapper {
position: relative;
padding: 8px 4px;
min-height: 150px;
overflow: auto;
}
.board-rotation {
min-width: 250px;
white-space: nowrap;
-webkit-backface-visibility: hidden;
-webkit-transform: rotate(90deg);
-moz-transform: rotate(90deg);
@ -77,6 +79,10 @@ td.board-column-task-collapsed {
}
/* column header */
.board-column-title {
cursor: pointer;
}
.board-add-icon {
float: left;
padding: 0 5px;
@ -105,26 +111,22 @@ th.board-column-header-collapsed .board-column-header-task-count {
}
/* swimlanes */
th.board-swimlane-header {
width: 120px;
}
a.board-swimlane-toggle {
font-size: 0.95em;
text-decoration: none;
}
.board-swimlane-toggle-title {
font-size: 0.85em;
display: none;
}
.board-swimlane-title {
vertical-align: top;
a.board-swimlane-toggle:hover,
a.board-swimlane-toggle:focus {
color: #000;
text-decoration: none;
border: none;
}
/* board task list */
.board-task-list {
overflow: auto;
min-height: 60px;
}
.board-task-list-limit {

View File

@ -18,6 +18,6 @@
top: 1%;
padding: 15px;
background: #fff;
overflow: scroll;
overflow: auto;
max-height: 85%;
}

View File

@ -42,8 +42,6 @@ a.task-board-collapsed-title {
}
.task-board .dropdown {
float: left;
margin-right: 5px;
font-size: 1.1em;
}

File diff suppressed because one or more lines are too long

View File

@ -8,13 +8,13 @@ Board.prototype.execute = function() {
this.app.swimlane.listen();
this.restoreColumnViewMode();
this.compactView();
this.columnScrolling();
this.poll();
this.keyboardShortcuts();
this.resizeColumnHeight();
this.listen();
this.dragAndDrop();
$(window).resize(this.resizeColumnHeight);
$(window).resize(this.columnScrolling);
};
Board.prototype.poll = function() {
@ -85,7 +85,7 @@ Board.prototype.refresh = function(data) {
this.app.refresh();
this.app.swimlane.refresh();
this.app.swimlane.listen();
this.resizeColumnHeight();
this.columnScrolling();
this.app.hideLoadingIcon();
this.listen();
this.dragAndDrop();
@ -93,22 +93,6 @@ Board.prototype.refresh = function(data) {
this.restoreColumnViewMode();
};
Board.prototype.resizeColumnHeight = function() {
if ($(".board-swimlane").length > 1) {
$(".board-task-list").each(function() {
if ($(this).height() > 500) {
$(this).height(500);
}
else {
$(this).css("min-height", 320); // Min height is the height of the menu dropdown
}
});
}
else {
$(".board-task-list").height($(window).height() - 145);
}
};
Board.prototype.dragAndDrop = function() {
var self = this;
var params = {
@ -155,11 +139,58 @@ Board.prototype.listen = function() {
self.toggleCompactView();
});
$(document).on('click', ".filter-toggle-height", function(e) {
e.preventDefault();
self.toggleColumnScrolling();
});
$(document).on("click", ".board-column-title", function() {
self.toggleColumnViewMode($(this).data("column-id"));
});
};
Board.prototype.toggleColumnScrolling = function() {
var scrolling = localStorage.getItem("column_scroll") || 1;
localStorage.setItem("column_scroll", scrolling == 0 ? 1 : 0);
this.columnScrolling();
};
Board.prototype.columnScrolling = function() {
if (localStorage.getItem("column_scroll") == 0) {
$(".filter-max-height").show();
$(".filter-min-height").hide();
$(".board-task-list").each(function() {
$(this).css("min-height", 80);
$(this).css("height", '');
$(".board-rotation-wrapper").css("min-height", '');
});
}
else {
$(".filter-max-height").hide();
$(".filter-min-height").show();
if ($(".board-swimlane").length > 1) {
$(".board-task-list").each(function() {
if ($(this).height() > 500) {
$(this).css("height", 500);
}
else {
$(this).css("min-height", 320); // Height of the dropdown menu
$(".board-rotation-wrapper").css("min-height", 320);
}
});
}
else {
var height = $(window).height() - 145;
$(".board-task-list").css("height", height);
$(".board-rotation-wrapper").css("min-height", height);
}
}
};
Board.prototype.toggleCompactView = function() {
var scrolling = localStorage.getItem("horizontal_scroll") || 1;
localStorage.setItem("horizontal_scroll", scrolling == 0 ? 1 : 0);
@ -233,7 +264,6 @@ Board.prototype.hideColumn = function(columnId) {
});
$(".board-column-" + columnId + " .board-rotation").each(function() {
var position = $(".board-swimlane").position();
$(this).css("width", $(".board-column-" + columnId + "").height());
});

View File

@ -21,11 +21,14 @@ Popover.prototype.open = function(link) {
};
Popover.prototype.close = function(e) {
if (e) {
e.preventDefault();
}
if (this.isOpen()) {
$('#popover-container').remove();
if (e) {
e.preventDefault();
}
$('#popover-container').remove();
}
};
Popover.prototype.onClick = function(e) {

View File

@ -18,7 +18,13 @@ Search.prototype.listen = function() {
// Filter helper for search
$(document).on("click", ".filter-helper", function (e) {
e.preventDefault();
var filter = $(this).data("filter");
var appendFilter = $(this).data("append-filter");
if (appendFilter) {
filter = $("#form-search").val() + " " + appendFilter;
}
$("#form-search").val(filter);

View File

@ -16,9 +16,8 @@ Swimlane.prototype.expand = function(swimlaneId) {
localStorage.setItem(this.getStorageKey(), JSON.stringify(swimlaneIds));
$('.swimlane-row-' + swimlaneId).css('display', 'table-row');
$('.show-icon-swimlane-' + swimlaneId).css('display', 'none');
$('.hide-icon-swimlane-' + swimlaneId).css('display', 'inline');
$('.swimlane-task-count-' + swimlaneId).css('display', 'inline');
$('.show-icon-swimlane-' + swimlaneId).css('display', 'none');
};
Swimlane.prototype.collapse = function(swimlaneId) {
@ -30,9 +29,8 @@ Swimlane.prototype.collapse = function(swimlaneId) {
}
$('.swimlane-row-' + swimlaneId).css('display', 'none');
$('.show-icon-swimlane-' + swimlaneId).css('display', 'inline');
$('.hide-icon-swimlane-' + swimlaneId).css('display', 'none');
$('.swimlane-task-count-' + swimlaneId).css('display', 'none');
$('.show-icon-swimlane-' + swimlaneId).css('display', 'inline');
};
Swimlane.prototype.isCollapsed = function(swimlaneId) {

33
doc/fr/2fa.markdown Normal file
View File

@ -0,0 +1,33 @@
Authentification à deux facteurs
=========================
Chaque utilisateur peut activer [l'authentification à deux facteurs](http://en.wikipedia.org/wiki/Two_factor_authentication).
Après sêtre connecté, un code à usage unique (6 caractères) est demandé à l'utilisateur pour lui autoriser laccès à Kanboard.
Ce code doit être fourni par un logiciel compatible, généralement installé sur votre smartphone.
Kanboard utilise le [Time-based One-time Password Algorithm](http://en.wikipedia.org/wiki/Time-based_One-time_Password_Algorithm) défini dans la [RFC 6238](http://tools.ietf.org/html/rfc6238).
Il existe de nombreux logiciels compatibles avec le standard TOTP system.
Par exemple, vous pouvez utilisez ces applications libres et open source :
- [Google Authenticator](https://github.com/google/google-authenticator/) (Android, iOS, Blackberry)
- [FreeOTP](https://fedorahosted.org/freeotp/) (Android, iOS)
- [OATH Toolkit](http://www.nongnu.org/oath-toolkit/) (utilitaire en ligne de commande sur Unix/Linux)
Ce système peut fonctionner hors ligne et vous n'avez pas l'obligation d'avoir un téléphone portable.
Paramétrage
-----
1. Allez dans le profil utilisateur.
2. Sur la gauche, cliquez sur **Authentification à deux facteurs** et cochez la case.
3. Une clef secrète est générée pour vous.
![2FA](http://kanboard.net/screenshots/documentation/2fa.png)
- Vous devez sauvegarder votre clef dans votre logiciel TOTP. Si vous utilisez un smartphone, la solution la plus simple est de scanner le QR code avec FreeOTP ou Google Authenticator
- À chaque ouverture de session, un nouveau code sera demandé
- N'oubliez pas de tester votre appareil avant de quitter votre session
Une nouvelle clef est générée à chaque fois que vous activez/désactivez cette fonction

View File

@ -0,0 +1,41 @@
Paramètres de l'application
====================
Certains paramètres de l'application peuvent être modifiés sur la page des paramètres.
Seuls les administrateurs peuvent modifier ces paramètres.
Allez au menu **Paramètres**, puis choisissez **Paramètres de l'application** sur la gauche.
![Paramètres de l'application](http://kanboard.net/screenshots/documentation/application-settings.png)
### URL de l'application
Ce paramètre est utilisé pour les notifications par mail.
Le pied de page du mail contiendra un lien vers la tâche du Kanboard.
### Langue
La langue de l'application peut être modifiée à tout moment.
Elle sera définie pour tous les utilisateurs.
### Fuseau horaire
Par défaut, Kanboard utilise le TUC comme fuseau horaire, mais vous pouvez définir votre propre fuseau horaire.
La liste contient tous les fuseaux horaires pris en charge par votre serveur web.
### Format de date
Format d'entrée utilisé pour les champs de saisie de date, par exemple la date d'échéance pour les tâches.
Kanboard propose 4 différents formats:
- JJ/MM/AAAA
- MM/JJ/AAAA (par défaut)
- AAAA/MM/JJ
- MM.JJ.AAAA
Le format [ISO 8601](http://en.wikipedia.org/wiki/ISO_8601) est toujours accepté (AAAA-MM-JJ ou AAAA_MM_JJ).
### Feuille de style personnalisée
Écrivez votre propre CSS pour remplacer ou améliorer le style par défaut de Kanboard.

View File

@ -0,0 +1,41 @@
Paramètres de l'application
====================
Certains paramètres de l'application peuvent être modifiés sur la page des paramètres.
Seuls les administrateurs peuvent modifier ces paramètres.
Allez au menu **Paramètres**, puis choisissez **Paramètres de l'application** sur la gauche.
![Paramètres de l'application](http://kanboard.net/screenshots/documentation/application-settings.png)
### URL de l'application
Ce paramètre est utilisé pour les notifications par mail.
Le pied de page du mail contiendra un lien vers la tâche du Kanboard.
### Langue
La langue de l'application peut être modifiée à tout moment.
Elle sera définie pour tous les utilisateurs.
### Fuseau horaire
Par défaut, Kanboard utilise le TUC comme fuseau horaire, mais vous pouvez définir votre propre fuseau horaire.
La liste contient tous les fuseaux horaires pris en charge par votre serveur web.
### Format de date
Format d'entrée utilisé pour les champs de saisie de date, par exemple la date d'échéance pour les tâches.
Kanboard propose 4 différents formats:
- JJ/MM/AAAA
- MM/JJ/AAAA (par défaut)
- AAAA/MM/JJ
- MM.JJ.AAAA
Le format [ISO 8601](http://en.wikipedia.org/wiki/ISO_8601) est toujours accepté (AAAA-MM-JJ ou AAAA_MM_JJ).
### Feuille de style personnalisée
Écrivez votre propre CSS pour remplacer ou améliorer le style par défaut de Kanboard.

View File

@ -0,0 +1,24 @@
Paramètres du tableau
==============
Allez dans le menu **Paramètres** puis choisissez *Paramètres du tableau** sur la gauche
![Paramètres du tableau](http://kanboard.net/screenshots/documentation/board-settings.png)
### Mise en avant d'une tâche
Cette fonctionnalité affiche une ombre autour de la tâche lorsqu'une tâche à été déplacée récemment
Initialisez la fonctionnalité à 0 pour la désactiver, par défaut 2 jours (172800 secondes)
Toutes les tâches qui ont été déplacées depuis 2 jours seront entourées d'une ombre
### Intervalle pour rafraîchir un tableau public
Lorsque vous partagez un tableau, la page sera, par défaut, automatiquement rafraîchie toutes les 60 secondes
### Intervalle pour rafraîchir un tableau privé
Lorsque votre navigateur web est ouvert sur un tableau, Kanboard vérifie toutes les 10 secondes si quelque chose à été modifié par un autre utilisateur.
Techniquement, ce processus est fait par Ajax polling.

View File

@ -0,0 +1,43 @@
Paramètres du calendrier
=================
Allez au menu **Paramètres**, puis choisissez **Paramètres du calendrier** sur la gauche.
![Paramètres du calendrier](http://kanboard.net/screenshots/documentation/calendar-settings.png)
il existe deux calendriers distincts dans Kanboard :
- le calendrier du projet
- le calendrier de l'utilisateur, disponible dans le tableau de bord
Le calendrier du projet
----------------
Ce calendrier affiche les tâches avec les dates d'échéance et les tâches selon leur date de création ou de début.
### Afficher les tâches selon leur date de création
- La date de début d'un évènement du calendrier est la date de création de la tâche.
- la date de fin de l'évènement est la date d'achèvement de la tâche.
### Afficher les tâches selon leur date de début
- La date de début d'un évènement du calendrier est la date du démarrage effectif de la tâche.
- Cette date ne peut pas être définie manuellement.
- La date de fin de l'évènement est la date de l'achèvement de la tâche.
- S'il n'existe pas de date de début la tâche ne figurera pas sur le calendrier .
Calendrier de l'utilisateur
-------------
Ce calendrier n'affiche que les tâches assignées à l'utilisateur et de façon facultative des informations sur les sous-tâches.
### Afficher les sous-tâches selon le suivi du temps passé
- Affiche les sous-tâches dans le calendrier d'après les informations recueillies dans l afeuille de suivi du temps.
- Le croisement des données avec l'emploi du temps de l'utilisateur est également calculé.
### Afficher les estimations des sous-tâches (anticipation sur le travail à venir)
- Affiche l'estimation du travail à venir pour les sous-tâches qui ont le statut « à faire » et avec une valeur définie à « estimé ».

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

View File

@ -14,7 +14,7 @@ Créer des projets multi-utilisateurs
Depuis le tableau principal, cliquez sur le lien **Nouveau projet** :
![Formulaire de création de projet](http://kanboard.net/screenshots/documentation/project-creation-form.png)
![Formulaire de création de projet](captures/kanboard-nouveauprojet.png)
C'est vraiment très simple, il vous suffit de trouver un nom pour votre projet !
@ -27,6 +27,6 @@ Créer un projet privé
Depuis le tableau principal, cliquez sur le lien **Nouveau projet privé**.
![Nouveau projet privé](http://kanboard.net/screenshots/documentation/new-private-project.png)
![Nouveau projet privé](captures/kanboard-nouveauprojet-prive.png)
Remarque : les noms de projets doivent être uniques dans toute l'application.

View File

@ -0,0 +1,11 @@
Taux de change des devises
==============
Chaque utilisateur peut avoir un taux horaire prédéfini dans différentes devises
Si vous avez à manipuler plusieurs devises, vous définissez ici le taux en fonction de la devise de référence
Cette fonctionnalité est utilisée pour le calculer le budget du projet
![Currency Rate](http://kanboard.net/screenshots/documentation/currency-rate.png)
Les paramètres pour le taux de change des devises sont situés dans **Paramètres > Taux de change**

View File

@ -43,3 +43,19 @@ Utiliser Kanboard
- [Créer des tâches par email](create-tasks-by-email.markdown)
- [Sous-tâches](subtasks.markdown)
- [Analytique des tâches](analytics-tasks.markdown)
### Travailler avec les utilisateurs
- [Gestion des utilisateurs](user-management.markdown)
- [Notifications](notifications.markdown)
- [Authentification à deux facteurs](2fa.markdown)
### Paramètres
- [Raccourcis clavier](keyboard-shortcuts.markdown)
- [Paramètres de l'application](application-configuration.markdown)
- [Paramètres du projet](project-configuration.markdown)
- [Paramètres du tableau](board-configuration.markdown)
- [Paramètres du calendrier](calendar-configuration.markdown)
- [Paramètres du lien](link-labels.markdown)
- [Taux de change](currency-rate.markdown)

View File

@ -0,0 +1,28 @@
Raccourcis clavier
==================
La disponibilité des raccourcis clavier dépend de la page sur laquelle vous êtes couramment.
Vues par projets (Tableau, Agenda, Liste, Gantt)
--------------------------------------------
- Passer à la vue tableau = **v b** (appuyer sur **v** puis **b**)
- Passer à la vue agenda = **v c**
- Passer à la vue liste = **v l**
- Passer à la vue Gantt = **v g**
Vue tableau
----------
- Nouvelle tâche = **n**
- Étendre / replier une tâche = **s**
- Vue compacte / vue étendue = **c**
Application
-----------
- Ouvrir le changement de tableau = **b**
- Aller au moteur de recherche = **f**
- Restaurer la boîte de recherche = **r**
- Fermer la fenêtre de dialogue = **ESC**
- Soumettre un formulaire = **CTRL+ENTER** ou **⌘+ENTER**

View File

@ -0,0 +1,13 @@
Paramètres des liens
=============
Les relations entre les tâches peuvent être modifiées depuis les paramètres de l'application (**Paramètres > Paramètres des liens**)
![Libellé des liens](http://kanboard.net/screenshots/documentation/link-labels.png)
Chaque nom du libellé peut avoir un nom du libellé opposé.
Si il n'y a pas d'opposé, le nom du libellé sera considéré comme étant bidirectionnel.
![Création d'un libellé de lien](http://kanboard.net/screenshots/documentation/link-label-creation.png)

View File

@ -0,0 +1,30 @@
Notifications
=============
Kanboard est capable d'envoyer des notifications via différents canaux :
- Email
- Jabber/XMPP
- Hipchat
- Slack
En fait, pour Jabber/Hipchat/Slack les notifications sont envoyées dans des salons de discussion car ils sont configurés au niveau projet.
Cependant, les notifications par email sont envoyées à un individu.
Notifications utilisateur
------------------
Chaque utilisateur doit autoriser les notifications dans son profil : **Profil Utilisateur > Notifications par email**. Cette option est désactivée par défaut.
Vous devez, bien sûr, avoir renseigné une adresse email valide dans votre profil et l'application doit être configurée pour envoyer des emails.
![Notifications](http://kanboard.net/screenshots/documentation/notifications.png)
Pour chaque projet dont vous êtes membre, vous pouvez choisir de recevoir des notifications pour :
- Toutes les tâches
- Seulement les tâches qui vous sont assignées
- Seulement les tâches que vous avez créées
- Seulement les tâches que vous avez créées et celles qui vous sont assignées
Vous pouvez aussi sélectionner certain projets, par défaut tous les projets dont vous êtes membre sont sélectionnés.

View File

@ -0,0 +1,42 @@
Paramètres du projet
================
Aller dans le menu **Préférences**; puis choisissez **Paramètres du projet** sur la gauche
![Paramètres du projet](http://kanboard.net/screenshots/documentation/project-settings.png)
###Colonnes par défaut pour les nouveaux projets
Vous pouvez changer le nom des colonnes par défaut.
C'est utile si vous créez toujours des projets comprenant les même colonnes
Chaque nom de colonne doit être séparé par une virgule.
Par défaut, Kanboard utilise les noms de colonne suivants : en attente, prêt, en cours, terminé.
###Catégories par défaut pour les nouveaux projets
Les catégories ne sont pas globales à l'application mais rattachées à un projet.
Chaque projet peut avoir plusieurs catégories.
De plus, si vous créez toujours la même catégorie pour tous vos projets, vous pouvez définir ici la liste des catégories à créer automatiquement
### Autoriser une seule sous-tâche en cours à la fois pour un utilisateur
Lorsque cette option est sélectionnée, un utilisateur ne peut travailler que sur une seule sous-tâche à la fois
Si une autre sous-tâche possède le statut « en cours », l'utilisateur verra cette boite de dialogue :
![Limite des sous-tâches pour l'utilisateur](http://kanboard.net/screenshots/documentation/subtask-user-restriction.png)
### Déclencher automatiquement le suivi du temps pour les sous-tâches
- Si activé, lorsque le statut d'une sous-tâche devient « en cours », le chrono va démarrer automatiquement
- Désactivez cette option si vous n'utilisez pas le suivi du temps.
### Inclure les tâches fermées dans le diagramme de flux cumulé
- Si l'option est activée, les tâches fermées seront incluses dans le diagramme de flux cumulé
- Si l'option est désactivée, seules les tâches ouvertes seront incluses dans le diagramme de flux cumulé
- Cette option affecte la colonne "total" de la table "project_daily_column_stats"

View File

@ -4,7 +4,7 @@ Pour chaque projet, les tâches peuvent être visualisées dans différentes vue
Vue Tableau
----------
![Vue Tableau](http://kanboard.net/screenshots/documentation/board-view.png)
![Vue Tableau](captures/kanboard-board.png)
- Dans cette vue, il est possible de glisser-déposer facilement des tâches d'une colonne à l'autre.
- Il est également possible d'utiliser le raccourci clavier **"v b"** pour afficher la vue Tableau.
@ -19,7 +19,7 @@ Lorsque la limite de tâches est atteinte pour une colonne, l'arrière-plan devi
Vue Agenda
--------------
![Vue Agenda](http://kanboard.net/screenshots/documentation/calendar-view.png)
![Vue Agenda](captures/kanboard-vue-calendrier.png)
- Dans cette vue, il est possible de voir les tâches avec des dates d'échéance.
- Selon les paramètres, il est également possible de voir les tâches en cours.
@ -29,7 +29,7 @@ Vue Agenda
Vue Liste
----------
![Vue liste](http://kanboard.net/screenshots/documentation/list-view.png)
![Vue liste](captures/kanboard-vue-liste.png)
- Dans cette vue, tous les résultats de votre recherche sont affichés dans un tableau.
- Il est également possible d'utiliser le raccourci clavier **"v l"** pour afficher la vue Liste.
@ -37,7 +37,7 @@ Vue Liste
Vue Gantt
----------
![Vue Gantt](http://kanboard.net/screenshots/documentation/gantt-view.png)
![Vue Gantt](captures/kanboard-vue-gantt.png)
- La vue Gantt affiche les tâches dans une fresque horizontale
- Le diagramme utilise la date de début et la date d'échéance pour afficher les tâches

View File

@ -19,7 +19,7 @@ Activer l'accès public
Choisissez votre projet, puis cliquez sur « Accès public » et enfin sur le bouton « Activer l'accès public ».
![Activer l'accès public](http://kanboard.net/screenshots/documentation/project-enable-sharing.png)
![Activer l'accès public](captures/kanboard-acces-prive.png)
Lorsque l'accès public est activé, plusieurs liens sont créés :
@ -27,7 +27,7 @@ Lorsque l'accès public est activé, plusieurs liens sont créés :
- Lien de souscription au fil RSS
- Lien d'abonnement à iCalendar
![Désactiver l'accès public](http://kanboard.net/screenshots/documentation/project-disable-sharing.png)
![Désactiver l'accès public](captures/kanboard-acces-public.png)
Vous pouvez désactiver l'accès public à tout moment.

View File

@ -0,0 +1,81 @@
Gestion des utilisateurs
===============
Rôles au niveau de l'application
------------------------------
Kanboard utilise un système de permissions basique qui reconnaît 3 types d'utilisateurs :
### Administrateur
- Peut accéder à tout
### Administrateur de projets
- Peut créer des projets multi-utilisateurs et privés
- Peut convertir les projets multi-utilisateurs et privés
- Peut voir seulement ses propres projets
- Ne peut pas modifier les paramètres de l'application
- Ne peut pas gérer les utilisateurs
### Utilisateur standard
- Peut créer seulement des projets privés
- Peut voir seulement ses propres projets
- Ne peut pas supprimer de projets
Rôles au niveau des projets
--------------------------
Ces rôles sont liés aux permissions du projet.
### Gestionnaire de projet
- Peut gérer seulement ses propres projets
- Peut accéder aux rapports et à la section budget
### Membre du projet
- Peut pratiquer toutes les opérations quotidiennes sur son projet (créer et déplacer des tâches…)
- Ne peut pas configurer les projets
Remarque : n'importe quel « utilisateur de base » peut être promu « Gestionnaire de projet » pour un projet donné, il n'est pas nécessaire d'être « Administrateur de projets ».
Utilisateurs locaux et distants
----------------------
- Un utilisateur local est un compte qui utilise la base de données pour stocker ses identifiants. Les utilisateurs locaux utilisent le formulaire de connexion pour s'identifier.
- Un utilisateur distant est un compte qui utilise un système externe pour stocker ses identifiants. Par exemple, ce peut être un compte LDAP, Github ou Google. L'authentification de ces utilisateurs peut s'effectuer ou non avec le formulaire de connexion.
Ajouter un nouvel utilisateur
--------------
Pour ajouter un nouvel utilisateur, vous devez être administrateur.
1. Depuis le tableau de bord, allez au menu **Gestion des utilisateurs**
2. Dans la partie haute vous avez un lien **Créer un utilisateur local** ou **Créer un utilisateur distant**
3. Informez les champs de saisie et enregistrez
![Nouvel utilisateur](captures/kanboard-creer-utilisateur.png)
Quand vous créez un **utilisateur local**, vous devez préciser au moins deux valeurs :
- **nom d'utilisateur** : c'est l'identifiant unique de votre utilisateur (login)
- **mot de passe** : le mot de passe de votre utilisateur doit comporter au moins 6 caractères
Pour les **utilisateurs distants**, seul le nom d'utilisateur est obligatoire. Vous pouvez aussi leur associer leur compte Github ou Google si vous connaissez déjà leur id unique.
Modifier des utilisateurs
----------
Quand vous allez au menu **utilisateurs**, vous disposez d'une liste d'utilisateurs. Pour modifier un utilisateur cliquez sur le lien **Modifier**.
- si vous êtes un utilisateur ordinaire, vous ne pouvez modifier que votre propre profil
- vous devez être administrateur pour pouvoir modifier n'importe quel utilisateur
Supprimer des utilisateurs
------------
Depuis le menu **utilisateurs**, cliquez sur le lien **supprimer**. Ce lien n'est visible que si vous êtes administrateur.
Si vous supprimez un utilisateur particulier, **les tâches assignées à cette personne ne lui seront plus assignées** après cette opération.

View File

@ -24,7 +24,7 @@ By default, Kanboard will check if the Apache mode rewrite is enabled.
To avoid the automatic detection of url rewriting from the web server, you can enable this feature in your config file:
```
```php
define('ENABLE_URL_REWRITE', true);
```
@ -34,3 +34,47 @@ When this constant is at `true`:
- If you use another web server than Apache, by example Nginx or Microsoft IIS, you have to configure yourself the url rewriting
Note: Kanboard always fallback to old school urls when it's not configured, this configuration is optional.
Nginx configuration example
---------------------------
In the section `server` of your Nginx config file you can use this example:
```bash
index index.php;
location / {
try_files $uri $uri/ /index.php;
# If Kanboard is under a subfolder
# try_files $uri $uri/ /kanboard/index.php;
}
location ~ \.php$ {
try_files $uri =404;
fastcgi_split_path_info ^(.+\.php)(/.+)$;
fastcgi_pass unix:/var/run/php5-fpm.sock;
fastcgi_index index.php;
include fastcgi_params;
}
# Deny access to the directory data
location ~* /data {
deny all;
return 404;
}
# Deny access to .htaccess
location ~ /\.ht {
deny all;
return 404;
}
```
In your Kanboard `config.php`:
```php
define('ENABLE_URL_REWRITE', true);
```
Adapt the example above according to your own configuration.

View File

@ -70,6 +70,11 @@ Available methods from `Core\Plugin\Base`:
- `initialize()`: Executed when the plugin is loaded
- `getClasses()`: Return all classes that should be stored in the dependency injection container
- `on($event, $callback)`: Listen on internal events
- `getPluginName()`: Should return plugin name
- `getPluginAuthor()`: Should return plugin author
- `getPluginVersion()`: Should return plugin version
- `getPluginDescription()`: Should return plugin description
- `getPluginHomepage()`: Should return plugin Homepage (link)
Your plugin registration class also inherit from `Core\Base`, that means you can access to all classes and methods of Kanboard easily.

View File

@ -39,7 +39,7 @@ These role are related to the project permission.
- Can do any daily operations in their projects (create and move tasks...)
- Cannot configure projects
Note: Any "Standard User" can be promotted "Project Manager" for a given project, they don't necessary need to be "Project Administrator".
Note: Any "Standard User" can be promoted "Project Manager" for a given project, they don't necessary need to be "Project Administrator".
Local and remote users
----------------------

View File

@ -54,11 +54,15 @@ class BoardTest extends Base
$board = $b->getBoard(1);
$this->assertNotEmpty($board);
$this->assertEquals(1, count($board));
$this->assertEquals(5, count($board[0]));
$this->assertTrue(array_key_exists('name', $board[0]));
$this->assertTrue(array_key_exists('columns', $board[0]));
$this->assertTrue(array_key_exists('tasks', $board[0]['columns'][2]));
$this->assertTrue(array_key_exists('title', $board[0]['columns'][2]));
$this->assertEquals(6, count($board[0]));
$this->assertArrayHasKey('name', $board[0]);
$this->assertArrayHasKey('nb_tasks', $board[0]);
$this->assertArrayHasKey('columns', $board[0]);
$this->assertArrayHasKey('tasks', $board[0]['columns'][2]);
$this->assertArrayHasKey('nb_tasks', $board[0]['columns'][2]);
$this->assertArrayHasKey('title', $board[0]['columns'][2]);
$this->assertArrayHasKey('nb_column_tasks', $board[0]['columns'][0]);
$this->assertArrayHasKey('total_score', $board[0]['columns'][0]);
}
public function testGetBoardWithSwimlane()
@ -75,18 +79,26 @@ class BoardTest extends Base
$this->assertEquals(2, $tc->create(array('title' => 'Task #2', 'project_id' => 1, 'column_id' => 3)));
$this->assertEquals(3, $tc->create(array('title' => 'Task #3', 'project_id' => 1, 'column_id' => 2, 'swimlane_id' => 1)));
$this->assertEquals(4, $tc->create(array('title' => 'Task #4', 'project_id' => 1, 'column_id' => 3)));
$this->assertEquals(5, $tc->create(array('title' => 'Task #5', 'project_id' => 1, 'column_id' => 4)));
$this->assertEquals(6, $tc->create(array('title' => 'Task #6', 'project_id' => 1, 'column_id' => 4, 'swimlane_id' => 1)));
$this->assertEquals(5, $tc->create(array('title' => 'Task #5', 'project_id' => 1, 'column_id' => 4, 'score' => 2)));
$this->assertEquals(6, $tc->create(array('title' => 'Task #6', 'project_id' => 1, 'column_id' => 4, 'score' => 3, 'swimlane_id' => 1)));
$board = $b->getBoard(1);
$this->assertNotEmpty($board);
$this->assertEquals(2, count($board));
$this->assertEquals(5, count($board[0]));
$this->assertTrue(array_key_exists('nb_tasks', $board[0]));
$this->assertTrue(array_key_exists('name', $board[0]));
$this->assertTrue(array_key_exists('columns', $board[0]));
$this->assertTrue(array_key_exists('tasks', $board[0]['columns'][2]));
$this->assertTrue(array_key_exists('title', $board[0]['columns'][2]));
$this->assertEquals(6, count($board[0]));
$this->assertArrayHasKey('name', $board[0]);
$this->assertArrayHasKey('nb_tasks', $board[0]);
$this->assertArrayHasKey('columns', $board[0]);
$this->assertArrayHasKey('tasks', $board[0]['columns'][2]);
$this->assertArrayHasKey('nb_tasks', $board[0]['columns'][2]);
$this->assertArrayHasKey('title', $board[0]['columns'][2]);
$this->assertArrayHasKey('nb_column_tasks', $board[0]['columns'][0]);
$this->assertArrayNotHasKey('nb_column_tasks', $board[1]['columns'][0]);
$this->assertArrayNotHasKey('total_score', $board[1]['columns'][0]);
$this->assertArrayHasKey('score', $board[0]['columns'][3]);
$this->assertArrayHasKey('total_score', $board[0]['columns'][3]);
$this->assertEquals(2, $board[0]['columns'][3]['score']);
$this->assertEquals(5, $board[0]['columns'][3]['total_score']);
$task = $tf->getById(1);
$this->assertEquals(1, $task['id']);