Added tag modification from the user interface

This commit is contained in:
Frederic Guillot 2016-06-24 22:10:14 -04:00
parent cead21691e
commit ce367a24fc
No known key found for this signature in database
GPG Key ID: 92D77191BA7FBC99
18 changed files with 527 additions and 2 deletions

View File

@ -0,0 +1,134 @@
<?php
namespace Kanboard\Controller;
use Kanboard\Core\Controller\AccessForbiddenException;
/**
* Class ProjectTagController
*
* @package Kanboard\Controller
* @author Frederic Guillot
*/
class ProjectTagController extends BaseController
{
public function index()
{
$project = $this->getProject();
$this->response->html($this->helper->layout->project('project_tag/index', array(
'project' => $project,
'tags' => $this->tagModel->getAllByProject($project['id']),
'title' => t('Project tags management'),
)));
}
public function create(array $values = array(), array $errors = array())
{
$project = $this->getProject();
if (empty($values)) {
$values['project_id'] = $project['id'];
}
$this->response->html($this->template->render('project_tag/create', array(
'project' => $project,
'values' => $values,
'errors' => $errors,
)));
}
public function save()
{
$project = $this->getProject();
$values = $this->request->getValues();
list($valid, $errors) = $this->tagValidator->validateCreation($values);
if ($valid) {
if ($this->tagModel->create($project['id'], $values['name']) > 0) {
$this->flash->success(t('Tag created successfully.'));
} else {
$this->flash->failure(t('Unable to create this tag.'));
}
$this->response->redirect($this->helper->url->to('ProjectTagController', 'index', array('project_id' => $project['id'])));
} else {
$this->create($values, $errors);
}
}
public function edit(array $values = array(), array $errors = array())
{
$project = $this->getProject();
$tag_id = $this->request->getIntegerParam('tag_id');
$tag = $this->tagModel->getById($tag_id);
if (empty($values)) {
$values = $tag;
}
$this->response->html($this->template->render('project_tag/edit', array(
'project' => $project,
'tag' => $tag,
'values' => $values,
'errors' => $errors,
)));
}
public function update()
{
$project = $this->getProject();
$tag_id = $this->request->getIntegerParam('tag_id');
$tag = $this->tagModel->getById($tag_id);
$values = $this->request->getValues();
list($valid, $errors) = $this->tagValidator->validateModification($values);
if ($tag['project_id'] != $project['id']) {
throw new AccessForbiddenException();
}
if ($valid) {
if ($this->tagModel->update($values['id'], $values['name'])) {
$this->flash->success(t('Tag updated successfully.'));
} else {
$this->flash->failure(t('Unable to update this tag.'));
}
$this->response->redirect($this->helper->url->to('ProjectTagController', 'index', array('project_id' => $project['id'])));
} else {
$this->edit($values, $errors);
}
}
public function confirm()
{
$project = $this->getProject();
$tag_id = $this->request->getIntegerParam('tag_id');
$tag = $this->tagModel->getById($tag_id);
$this->response->html($this->template->render('project_tag/remove', array(
'tag' => $tag,
'project' => $project,
)));
}
public function remove()
{
$this->checkCSRFParam();
$project = $this->getProject();
$tag_id = $this->request->getIntegerParam('tag_id');
$tag = $this->tagModel->getById($tag_id);
if ($tag['project_id'] != $project['id']) {
throw new AccessForbiddenException();
}
if ($this->tagModel->remove($tag_id)) {
$this->flash->success(t('Tag removed successfully.'));
} else {
$this->flash->failure(t('Unable to remove this tag.'));
}
$this->response->redirect($this->helper->url->to('ProjectTagController', 'index', array('project_id' => $project['id'])));
}
}

View File

@ -0,0 +1,121 @@
<?php
namespace Kanboard\Controller;
use Kanboard\Core\Controller\AccessForbiddenException;
/**
* Class TagController
*
* @package Kanboard\Controller
* @author Frederic Guillot
*/
class TagController extends BaseController
{
public function index()
{
$this->response->html($this->helper->layout->config('tag/index', array(
'tags' => $this->tagModel->getAllByProject(0),
'title' => t('Settings').' &gt; '.t('Global tags management'),
)));
}
public function create(array $values = array(), array $errors = array())
{
if (empty($values)) {
$values['project_id'] = 0;
}
$this->response->html($this->template->render('tag/create', array(
'values' => $values,
'errors' => $errors,
)));
}
public function save()
{
$values = $this->request->getValues();
list($valid, $errors) = $this->tagValidator->validateCreation($values);
if ($valid) {
if ($this->tagModel->create(0, $values['name']) > 0) {
$this->flash->success(t('Tag created successfully.'));
} else {
$this->flash->failure(t('Unable to create this tag.'));
}
$this->response->redirect($this->helper->url->to('TagController', 'index'));
} else {
$this->create($values, $errors);
}
}
public function edit(array $values = array(), array $errors = array())
{
$tag_id = $this->request->getIntegerParam('tag_id');
$tag = $this->tagModel->getById($tag_id);
if (empty($values)) {
$values = $tag;
}
$this->response->html($this->template->render('tag/edit', array(
'tag' => $tag,
'values' => $values,
'errors' => $errors,
)));
}
public function update()
{
$tag_id = $this->request->getIntegerParam('tag_id');
$tag = $this->tagModel->getById($tag_id);
$values = $this->request->getValues();
list($valid, $errors) = $this->tagValidator->validateModification($values);
if ($tag['project_id'] != 0) {
throw new AccessForbiddenException();
}
if ($valid) {
if ($this->tagModel->update($values['id'], $values['name'])) {
$this->flash->success(t('Tag updated successfully.'));
} else {
$this->flash->failure(t('Unable to update this tag.'));
}
$this->response->redirect($this->helper->url->to('TagController', 'index'));
} else {
$this->edit($values, $errors);
}
}
public function confirm()
{
$tag_id = $this->request->getIntegerParam('tag_id');
$tag = $this->tagModel->getById($tag_id);
$this->response->html($this->template->render('tag/remove', array(
'tag' => $tag,
)));
}
public function remove()
{
$this->checkCSRFParam();
$tag_id = $this->request->getIntegerParam('tag_id');
$tag = $this->tagModel->getById($tag_id);
if ($tag['project_id'] != 0) {
throw new AccessForbiddenException();
}
if ($this->tagModel->remove($tag_id)) {
$this->flash->success(t('Tag removed successfully.'));
} else {
$this->flash->failure(t('Unable to remove this tag.'));
}
$this->response->redirect($this->helper->url->to('TagController', 'index'));
}
}

View File

@ -116,14 +116,15 @@ use Pimple\Container;
* @property \Kanboard\Validator\CommentValidator $commentValidator
* @property \Kanboard\Validator\CurrencyValidator $currencyValidator
* @property \Kanboard\Validator\CustomFilterValidator $customFilterValidator
* @property \Kanboard\Validator\ExternalLinkValidator $externalLinkValidator
* @property \Kanboard\Validator\GroupValidator $groupValidator
* @property \Kanboard\Validator\LinkValidator $linkValidator
* @property \Kanboard\Validator\PasswordResetValidator $passwordResetValidator
* @property \Kanboard\Validator\ProjectValidator $projectValidator
* @property \Kanboard\Validator\SubtaskValidator $subtaskValidator
* @property \Kanboard\Validator\SwimlaneValidator $swimlaneValidator
* @property \Kanboard\Validator\TagValidator $tagValidator
* @property \Kanboard\Validator\TaskLinkValidator $taskLinkValidator
* @property \Kanboard\Validator\ExternalLinkValidator $externalLinkValidator
* @property \Kanboard\Validator\TaskValidator $taskValidator
* @property \Kanboard\Validator\UserValidator $userValidator
* @property \Kanboard\Import\TaskImport $taskImport

View File

@ -93,6 +93,29 @@ class TagModel extends Base
->findOneColumn('id');
}
/**
* Return true if the tag exists
*
* @access public
* @param integer $project_id
* @param string $tag
* @param integer $tag_id
* @return boolean
*/
public function exists($project_id, $tag, $tag_id = 0)
{
return $this->db
->table(self::TABLE)
->neq('id', $tag_id)
->beginOr()
->eq('project_id', 0)
->eq('project_id', $project_id)
->closeOr()
->ilike('name', $tag)
->asc('project_id')
->exists();
}
/**
* Return tag id and create a new tag if necessary
*

View File

@ -88,6 +88,7 @@ class AuthenticationProvider implements ServiceProviderInterface
$acl->add('ProjectFileController', '*', Role::PROJECT_MEMBER);
$acl->add('ProjectUserOverviewController', '*', Role::PROJECT_MANAGER);
$acl->add('ProjectStatusController', '*', Role::PROJECT_MANAGER);
$acl->add('ProjectTagController', '*', Role::PROJECT_MANAGER);
$acl->add('SubtaskController', '*', Role::PROJECT_MEMBER);
$acl->add('SubtaskRestrictionController', '*', Role::PROJECT_MEMBER);
$acl->add('SubtaskStatusController', '*', Role::PROJECT_MEMBER);
@ -131,6 +132,7 @@ class AuthenticationProvider implements ServiceProviderInterface
$acl->add('AvatarFileController', 'show', Role::APP_PUBLIC);
$acl->add('ConfigController', '*', Role::APP_ADMIN);
$acl->add('TagController', '*', Role::APP_ADMIN);
$acl->add('PluginController', '*', Role::APP_ADMIN);
$acl->add('CurrencyController', '*', Role::APP_ADMIN);
$acl->add('ProjectGanttController', '*', Role::APP_MANAGER);

View File

@ -99,8 +99,9 @@ class ClassProvider implements ServiceProviderInterface
'ProjectValidator',
'SubtaskValidator',
'SwimlaneValidator',
'TaskValidator',
'TagValidator',
'TaskLinkValidator',
'TaskValidator',
'UserValidator',
),
'Import' => array(

View File

@ -59,6 +59,7 @@ class RouteProvider implements ServiceProviderInterface
$container['route']->addRoute('project/:project_id/duplicate', 'ProjectViewController', 'duplicate');
$container['route']->addRoute('project/:project_id/permissions', 'ProjectPermissionController', 'index');
$container['route']->addRoute('project/:project_id/activity', 'ActivityController', 'project');
$container['route']->addRoute('project/:project_id/tags', 'ProjectTagController', 'index');
// Project Overview
$container['route']->addRoute('project/:project_id/overview', 'ProjectOverviewController', 'show');
@ -174,6 +175,7 @@ class RouteProvider implements ServiceProviderInterface
$container['route']->addRoute('settings/api', 'ConfigController', 'api');
$container['route']->addRoute('settings/links', 'LinkController', 'index');
$container['route']->addRoute('settings/currencies', 'CurrencyController', 'index');
$container['route']->addRoute('settings/tags', 'TagController', 'index');
// Plugins
$container['route']->addRoute('extensions', 'PluginController', 'show');

View File

@ -19,6 +19,9 @@
<li <?= $this->app->checkMenuSelection('ConfigController', 'calendar') ?>>
<?= $this->url->link(t('Calendar settings'), 'ConfigController', 'calendar') ?>
</li>
<li <?= $this->app->checkMenuSelection('TagController', 'index') ?>>
<?= $this->url->link(t('Tags management'), 'TagController', 'index') ?>
</li>
<li <?= $this->app->checkMenuSelection('LinkController') ?>>
<?= $this->url->link(t('Link settings'), 'LinkController', 'index') ?>
</li>

View File

@ -32,6 +32,9 @@
<li <?= $this->app->checkMenuSelection('CategoryController') ?>>
<?= $this->url->link(t('Categories'), 'CategoryController', 'index', array('project_id' => $project['id'])) ?>
</li>
<li <?= $this->app->checkMenuSelection('ProjectTagController') ?>>
<?= $this->url->link(t('Tags'), 'ProjectTagController', 'index', array('project_id' => $project['id'])) ?>
</li>
<?php if ($project['is_private'] == 0): ?>
<li <?= $this->app->checkMenuSelection('ProjectPermissionController') ?>>
<?= $this->url->link(t('Permissions'), 'ProjectPermissionController', 'index', array('project_id' => $project['id'])) ?>

View File

@ -0,0 +1,16 @@
<div class="page-header">
<h2><?= t('Add new tag') ?></h2>
</div>
<form method="post" class="popover-form" action="<?= $this->url->href('ProjectTagController', 'save', array('project_id' => $project['id'])) ?>" autocomplete="off">
<?= $this->form->csrf() ?>
<?= $this->form->hidden('project_id', $values) ?>
<?= $this->form->label(t('Name'), 'name') ?>
<?= $this->form->text('name', $values, $errors, array('autofocus', 'required', 'maxlength="255"')) ?>
<div class="form-actions">
<button type="submit" class="btn btn-blue"><?= t('Save') ?></button>
<?= t('or') ?>
<?= $this->url->link(t('cancel'), 'ProjectTagController', 'index', array('project_id' => $project['id']), false, 'close-popover') ?>
</div>
</form>

View File

@ -0,0 +1,17 @@
<div class="page-header">
<h2><?= t('Edit a tag') ?></h2>
</div>
<form method="post" class="popover-form" action="<?= $this->url->href('ProjectTagController', 'update', array('tag_id' => $tag['id'], 'project_id' => $project['id'])) ?>" autocomplete="off">
<?= $this->form->csrf() ?>
<?= $this->form->hidden('id', $values) ?>
<?= $this->form->hidden('project_id', $values) ?>
<?= $this->form->label(t('Name'), 'name') ?>
<?= $this->form->text('name', $values, $errors, array('autofocus', 'required', 'maxlength="255"')) ?>
<div class="form-actions">
<button type="submit" class="btn btn-blue"><?= t('Save') ?></button>
<?= t('or') ?>
<?= $this->url->link(t('cancel'), 'ProjectTagController', 'index', array(), false, 'close-popover') ?>
</div>
</form>

View File

@ -0,0 +1,31 @@
<div class="page-header">
<h2><?= t('Project tags') ?></h2>
<ul>
<li>
<i class="fa fa-plus" aria-hidden="true"></i>
<?= $this->url->link(t('Add new tag'), 'ProjectTagController', 'create', array('project_id' => $project['id']), false, 'popover') ?>
</li>
</ul>
</div>
<?php if (empty($tags)): ?>
<p class="alert"><?= t('There is no specific tag for this project at the moment.') ?></p>
<?php else: ?>
<table class="table-striped">
<tr>
<th class="column-80"><?= t('Tag') ?></th>
<th><?= t('Action') ?></th>
</tr>
<?php foreach ($tags as $tag): ?>
<tr>
<td><?= $this->text->e($tag['name']) ?></td>
<td>
<i class="fa fa-times" aria-hidden="true"></i>
<?= $this->url->link(t('Remove'), 'ProjectTagController', 'confirm', array('tag_id' => $tag['id'], 'project_id' => $project['id']), false, 'popover') ?>
<i class="fa fa-pencil-square-o" aria-hidden="true"></i>
<?= $this->url->link(t('Edit'), 'ProjectTagController', 'edit', array('tag_id' => $tag['id'], 'project_id' => $project['id']), false, 'popover') ?>
</td>
</tr>
<?php endforeach ?>
</table>
<?php endif ?>

View File

@ -0,0 +1,15 @@
<div class="page-header">
<h2><?= t('Remove a tag') ?></h2>
</div>
<div class="confirm">
<p class="alert alert-info">
<?= t('Do you really want to remove this tag: "%s"?', $tag['name']) ?>
</p>
<div class="form-actions">
<?= $this->url->link(t('Yes'), 'ProjectTagController', 'remove', array('tag_id' => $tag['id'], 'project_id' => $project['id']), true, 'btn btn-red popover-link') ?>
<?= t('or') ?>
<?= $this->url->link(t('cancel'), 'ProjectTagController', 'index', array('project_id' => $project['id']), false, 'close-popover') ?>
</div>
</div>

View File

@ -0,0 +1,16 @@
<div class="page-header">
<h2><?= t('Add new tag') ?></h2>
</div>
<form method="post" class="popover-form" action="<?= $this->url->href('TagController', 'save') ?>" autocomplete="off">
<?= $this->form->csrf() ?>
<?= $this->form->hidden('project_id', $values) ?>
<?= $this->form->label(t('Name'), 'name') ?>
<?= $this->form->text('name', $values, $errors, array('autofocus', 'required', 'maxlength="255"')) ?>
<div class="form-actions">
<button type="submit" class="btn btn-blue"><?= t('Save') ?></button>
<?= t('or') ?>
<?= $this->url->link(t('cancel'), 'TagController', 'index', array(), false, 'close-popover') ?>
</div>
</form>

17
app/Template/tag/edit.php Normal file
View File

@ -0,0 +1,17 @@
<div class="page-header">
<h2><?= t('Edit a tag') ?></h2>
</div>
<form method="post" class="popover-form" action="<?= $this->url->href('TagController', 'update', array('tag_id' => $tag['id'])) ?>" autocomplete="off">
<?= $this->form->csrf() ?>
<?= $this->form->hidden('id', $values) ?>
<?= $this->form->hidden('project_id', $values) ?>
<?= $this->form->label(t('Name'), 'name') ?>
<?= $this->form->text('name', $values, $errors, array('autofocus', 'required', 'maxlength="255"')) ?>
<div class="form-actions">
<button type="submit" class="btn btn-blue"><?= t('Save') ?></button>
<?= t('or') ?>
<?= $this->url->link(t('cancel'), 'TagController', 'index', array(), false, 'close-popover') ?>
</div>
</form>

View File

@ -0,0 +1,31 @@
<div class="page-header">
<h2><?= t('Global tags') ?></h2>
<ul>
<li>
<i class="fa fa-plus" aria-hidden="true"></i>
<?= $this->url->link(t('Add new tag'), 'TagController', 'create', array(), false, 'popover') ?>
</li>
</ul>
</div>
<?php if (empty($tags)): ?>
<p class="alert"><?= t('There is no global tag at the moment.') ?></p>
<?php else: ?>
<table class="table-striped">
<tr>
<th class="column-80"><?= t('Tag') ?></th>
<th><?= t('Action') ?></th>
</tr>
<?php foreach ($tags as $tag): ?>
<tr>
<td><?= $this->text->e($tag['name']) ?></td>
<td>
<i class="fa fa-times" aria-hidden="true"></i>
<?= $this->url->link(t('Remove'), 'TagController', 'confirm', array('tag_id' => $tag['id']), false, 'popover') ?>
<i class="fa fa-pencil-square-o" aria-hidden="true"></i>
<?= $this->url->link(t('Edit'), 'TagController', 'edit', array('tag_id' => $tag['id']), false, 'popover') ?>
</td>
</tr>
<?php endforeach ?>
</table>
<?php endif ?>

View File

@ -0,0 +1,15 @@
<div class="page-header">
<h2><?= t('Remove a tag') ?></h2>
</div>
<div class="confirm">
<p class="alert alert-info">
<?= t('Do you really want to remove this tag: "%s"?', $tag['name']) ?>
</p>
<div class="form-actions">
<?= $this->url->link(t('Yes'), 'TagController', 'remove', array('tag_id' => $tag['id']), true, 'btn btn-red popover-link') ?>
<?= t('or') ?>
<?= $this->url->link(t('cancel'), 'TagController', 'index', array(), false, 'close-popover') ?>
</div>
</div>

View File

@ -0,0 +1,77 @@
<?php
namespace Kanboard\Validator;
use Kanboard\Model\TagModel;
use SimpleValidator\Validator;
use SimpleValidator\Validators;
/**
* Tag Validator
*
* @package Kanboard\Validator
* @author Frederic Guillot
*/
class TagValidator extends BaseValidator
{
/**
* Validate creation
*
* @access public
* @param array $values Form values
* @return array $valid, $errors [0] = Success or not, [1] = List of errors
*/
public function validateCreation(array $values)
{
$v = new Validator($values, $this->commonValidationRules());
$result = $v->execute();
$errors = $v->getErrors();
if ($result && $this->tagModel->exists($values['project_id'], $values['name'])) {
$result = false;
$errors = array('name' => array(t('The name must be unique')));
}
return array($result, $errors);
}
/**
* Validate modification
*
* @access public
* @param array $values Form values
* @return array $valid, $errors [0] = Success or not, [1] = List of errors
*/
public function validateModification(array $values)
{
$rules = array(
new Validators\Required('id', t('Field required')),
);
$v = new Validator($values, array_merge($rules, $this->commonValidationRules()));
$result = $v->execute();
$errors = $v->getErrors();
if ($result && $this->tagModel->exists($values['project_id'], $values['name'], $values['id'])) {
$result = false;
$errors = array('name' => array(t('The name must be unique')));
}
return array($result, $errors);
}
/**
* Common validation rules
*
* @access protected
* @return array
*/
protected function commonValidationRules()
{
return array(
new Validators\Required('project_id', t('Field required')),
new Validators\Required('name', t('Field required')),
new Validators\MaxLength('name', t('The maximum length is %d characters', 255), 255),
);
}
}