improved tag auto-completion and added tag management (#1083)
This commit is contained in:
@@ -13,6 +13,7 @@ Perform EACH version specific task between your version and the new one, otherwi
|
||||
### Possible BC breaks
|
||||
|
||||
- Refactored toolbars and search, plugins needs to be checked
|
||||
- Added `manage_tag` permission for new tag features
|
||||
- Invoices now supports multiple repositories, some method signatures had to be changed (eg. `calculateSumIdentifier()`)
|
||||
|
||||
## [1.2](https://github.com/kevinpapst/kimai2/releases/tag/1.2)
|
||||
|
||||
@@ -20,6 +20,7 @@ export default class KimaiAutocomplete extends KimaiPlugin {
|
||||
}
|
||||
|
||||
init() {
|
||||
this.minChars = this.getContainer().getConfiguration().get('autoComplete');
|
||||
this.activateAutocomplete(this.selector);
|
||||
}
|
||||
|
||||
@@ -59,7 +60,7 @@ export default class KimaiAutocomplete extends KimaiPlugin {
|
||||
search: function () {
|
||||
// custom minLength
|
||||
var term = self.extractLastTag(this.value);
|
||||
if (term.length < 2) {
|
||||
if (term.length < self.minChars) {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
@@ -95,7 +95,7 @@ kimai:
|
||||
TIMESHEET_OTHER: ['view_other_timesheet','start_other_timesheet','stop_other_timesheet','create_other_timesheet','edit_other_timesheet','export_other_timesheet','delete_other_timesheet']
|
||||
PROFILE: ['view_own_profile','edit_own_profile','password_own_profile','preferences_own_profile','api-token_own_profile']
|
||||
PROFILE_OTHER: ['view_other_profile','edit_other_profile','delete_other_profile','password_other_profile','roles_other_profile','preferences_other_profile','api-token_other_profile','teams_other_profile']
|
||||
TAGS: ['view_tag','delete_tag']
|
||||
TAGS: ['view_tag','manage_tag','delete_tag']
|
||||
USER: ['view_user','create_user','delete_user','role_permissions']
|
||||
RATE: ['view_rate_own_timesheet','edit_rate_own_timesheet']
|
||||
RATE_OTHER: ['view_rate_other_timesheet','edit_rate_other_timesheet']
|
||||
|
||||
@@ -14,6 +14,8 @@ nelmio_api_doc:
|
||||
- { alias: ActivityEntity, type: App\Entity\Activity, groups: [Default, Entity, Activity] }
|
||||
- { alias: ActivityMetaField, type: App\Entity\ActivityMeta, groups: [Default, Activity] }
|
||||
- { alias: ActivityCollection, type: App\Entity\Activity, groups: [Default, Collection, Activity] }
|
||||
- { alias: TagEditForm, type: App\Form\API\TagApiEditForm, groups: [Default, Entity, Tag] }
|
||||
- { alias: TagEntity, type: App\Entity\Tag, groups: [Default, Entity, Tag] }
|
||||
- { alias: TimesheetEditForm, type: App\Form\API\TimesheetApiEditForm, groups: [Default, Entity, Timesheet] }
|
||||
- { alias: TimesheetEntity, type: App\Entity\Timesheet, groups: [Default, Entity, Timesheet] }
|
||||
- { alias: TimesheetMeta, type: App\Entity\TimesheetMeta, groups: [Default, Timesheet] }
|
||||
|
||||
1
public/build/app.121b7493.js
Normal file
1
public/build/app.121b7493.js
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -5,7 +5,7 @@
|
||||
"build/runtime.4ee6be68.js",
|
||||
"build/0.a87622f3.js",
|
||||
"build/1.c1bee41f.js",
|
||||
"build/app.75bdc057.js"
|
||||
"build/app.121b7493.js"
|
||||
],
|
||||
"css": [
|
||||
"build/app.eb4ed947.css"
|
||||
@@ -35,7 +35,7 @@
|
||||
"build/runtime.4ee6be68.js": "sha384-xNNrNinl64G3nCUrIskgSjU0mUXXCB9lj6XCSInBTwxSKXk8uTMafnLHtdWdIGtd",
|
||||
"build/0.a87622f3.js": "sha384-ncT/BKhCsqH6jhxwdsSG95m1ei7ZZjeZtzH1262h+OPUU80TSFFE3dt+abcHHMok",
|
||||
"build/1.c1bee41f.js": "sha384-7UVWcP6Hefp2k/CrtGSITKXx4dSZqtvpAiU8WX7dClETkzMewrUjoCRtVXQ5j3KI",
|
||||
"build/app.75bdc057.js": "sha384-a93v/mUzZDopA6To/YJEjOAxgJ6T4H6gmXnGf0ja6Bq4M7ZVDQqqKMX7/9hIhRPM",
|
||||
"build/app.121b7493.js": "sha384-avvNYZprPc37DZ2jHh1hLY+0KdsyHsgQttMZLm4ZQ3/OiwWqUFo9xrqwR6Yn1H7m",
|
||||
"build/app.eb4ed947.css": "sha384-TQ5nEns/+JBxjuHlqGXkiS1qt9Zdzb7tREhc4DPunQkTib/iuv8MfwELt8OPhF10",
|
||||
"build/2.7be60d8d.js": "sha384-txR0QG+838LKYtPQ99Gx4OU7WmgN9J3joZEyGwIskSz74EN1T4/IBVnmNaKiFN1q",
|
||||
"build/chart.0af3f813.js": "sha384-I57c9DtU3AOG2kzKqIZkIu0hi1aGYHRZ5QG4LKC9+9slzJnAMttPGXoL2cQG3m6y",
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"build/1.c1bee41f.js": "build/1.c1bee41f.js",
|
||||
"build/2.7be60d8d.js": "build/2.7be60d8d.js",
|
||||
"build/app.css": "build/app.eb4ed947.css",
|
||||
"build/app.js": "build/app.75bdc057.js",
|
||||
"build/app.js": "build/app.121b7493.js",
|
||||
"build/calendar.css": "build/calendar.b0551848.css",
|
||||
"build/calendar.js": "build/calendar.5839778f.js",
|
||||
"build/chart.js": "build/chart.0af3f813.js",
|
||||
|
||||
@@ -11,6 +11,9 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\API;
|
||||
|
||||
use App\Entity\Tag;
|
||||
use App\Form\API\TagApiEditForm;
|
||||
use App\Form\TagEditForm;
|
||||
use App\Repository\TagRepository;
|
||||
use FOS\RestBundle\Controller\Annotations as Rest;
|
||||
use FOS\RestBundle\Controller\Annotations\RouteResource;
|
||||
@@ -19,7 +22,9 @@ use FOS\RestBundle\View\View;
|
||||
use FOS\RestBundle\View\ViewHandlerInterface;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Swagger\Annotations as SWG;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||
|
||||
/**
|
||||
* @RouteResource("Tag")
|
||||
@@ -58,7 +63,7 @@ class TagController extends BaseApiController
|
||||
* )
|
||||
* )
|
||||
*
|
||||
* @Rest\QueryParam(name="name", requirements="[a-zA-Z0-9 -\.]+", strict=true, nullable=true, description="Search term to filter tag list")
|
||||
* @Rest\QueryParam(name="name", strict=true, nullable=true, description="Search term to filter tag list")
|
||||
*
|
||||
* @return Response
|
||||
*/
|
||||
@@ -69,7 +74,58 @@ class TagController extends BaseApiController
|
||||
$data = $this->repository->findAllTagNames($filter);
|
||||
|
||||
$view = new View($data, 200);
|
||||
$view->getContext()->setGroups(['Default', 'Collection']);
|
||||
$view->getContext()->setGroups(['Default', 'Collection', 'Tag']);
|
||||
|
||||
return $this->viewHandler->handle($view);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new tag
|
||||
*
|
||||
* @SWG\Post(
|
||||
* description="Creates a new tag and returns it afterwards",
|
||||
* @SWG\Response(
|
||||
* response=200,
|
||||
* description="Returns the new created tag",
|
||||
* @SWG\Schema(ref="#/definitions/TagEntity"),
|
||||
* )
|
||||
* )
|
||||
* @SWG\Parameter(
|
||||
* name="body",
|
||||
* in="body",
|
||||
* required=true,
|
||||
* @SWG\Schema(ref="#/definitions/TagEditForm")
|
||||
* )
|
||||
*
|
||||
* @param Request $request
|
||||
* @return Response
|
||||
* @throws \App\Repository\RepositoryException
|
||||
* @throws \Doctrine\ORM\ORMException
|
||||
* @throws \Doctrine\ORM\OptimisticLockException
|
||||
*/
|
||||
public function postAction(Request $request)
|
||||
{
|
||||
if (!$this->isGranted('manage_tag')) {
|
||||
throw new AccessDeniedHttpException('User cannot create tags');
|
||||
}
|
||||
|
||||
$tag = new Tag();
|
||||
|
||||
$form = $this->createForm(TagApiEditForm::class, $tag);
|
||||
|
||||
$form->submit($request->request->all());
|
||||
|
||||
if ($form->isValid()) {
|
||||
$this->repository->saveTag($tag);
|
||||
|
||||
$view = new View($tag, 200);
|
||||
$view->getContext()->setGroups(['Default', 'Entity', 'Tag']);
|
||||
|
||||
return $this->viewHandler->handle($view);
|
||||
}
|
||||
|
||||
$view = new View($form);
|
||||
$view->getContext()->setGroups(['Default', 'Entity', 'Tag']);
|
||||
|
||||
return $this->viewHandler->handle($view);
|
||||
}
|
||||
|
||||
@@ -266,6 +266,11 @@ class SystemConfigurationController extends AbstractController
|
||||
->setLabel('theme.markdown_content')
|
||||
->setType(CheckboxType::class)
|
||||
->setTranslationDomain('system-configuration'),
|
||||
(new Configuration())
|
||||
->setName('theme.autocomplete_chars')
|
||||
->setLabel('theme.autocomplete_chars')
|
||||
->setType(IntegerType::class)
|
||||
->setTranslationDomain('system-configuration'),
|
||||
// FIXME should that be configurable per user?
|
||||
/*
|
||||
(new Configuration())
|
||||
|
||||
@@ -9,9 +9,12 @@
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Entity\Tag;
|
||||
use App\Form\TagEditForm;
|
||||
use App\Form\Toolbar\TagToolbarForm;
|
||||
use App\Repository\Query\TagQuery;
|
||||
use App\Repository\TagRepository;
|
||||
use Doctrine\ORM\ORMException;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
@@ -51,6 +54,68 @@ class TagController extends AbstractController
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/{id}/edit", name="tags_edit", methods={"GET", "POST"})
|
||||
* @Security("is_granted('manage_tag')")
|
||||
*/
|
||||
public function editAction(Tag $tag, TagRepository $repository, Request $request)
|
||||
{
|
||||
$editForm = $this->createForm(TagEditForm::class, $tag, [
|
||||
'action' => $this->generateUrl('tags_edit', ['id' => $tag->getId()]),
|
||||
'method' => 'POST',
|
||||
]);
|
||||
|
||||
$editForm->handleRequest($request);
|
||||
|
||||
if ($editForm->isSubmitted() && $editForm->isValid()) {
|
||||
try {
|
||||
$repository->saveTag($tag);
|
||||
$this->flashSuccess('action.update.success');
|
||||
|
||||
return $this->redirectToRoute('tags');
|
||||
} catch (ORMException $ex) {
|
||||
$this->flashError('action.update.error', ['%reason%' => $ex->getMessage()]);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->render('tags/edit.html.twig', [
|
||||
'tag' => $tag,
|
||||
'form' => $editForm->createView()
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/create", name="tags_create", methods={"GET", "POST"})
|
||||
* @Security("is_granted('manage_tag')")
|
||||
*/
|
||||
public function createAction(TagRepository $repository, Request $request)
|
||||
{
|
||||
$tag = new Tag();
|
||||
|
||||
$editForm = $this->createForm(TagEditForm::class, $tag, [
|
||||
'action' => $this->generateUrl('tags_create'),
|
||||
'method' => 'POST',
|
||||
]);
|
||||
|
||||
$editForm->handleRequest($request);
|
||||
|
||||
if ($editForm->isSubmitted() && $editForm->isValid()) {
|
||||
try {
|
||||
$repository->saveTag($tag);
|
||||
$this->flashSuccess('action.update.success');
|
||||
|
||||
return $this->redirectToRoute('tags');
|
||||
} catch (ORMException $ex) {
|
||||
$this->flashError('action.update.error', ['%reason%' => $ex->getMessage()]);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->render('tags/edit.html.twig', [
|
||||
'tag' => $tag,
|
||||
'form' => $editForm->createView()
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param TagQuery $query
|
||||
* @return \Symfony\Component\Form\FormInterface
|
||||
|
||||
@@ -346,6 +346,9 @@ class Configuration implements ConfigurationInterface
|
||||
->end()
|
||||
->end()
|
||||
->end()
|
||||
->integerNode('autocomplete_chars')
|
||||
->defaultValue(3)
|
||||
->end()
|
||||
->end()
|
||||
;
|
||||
|
||||
|
||||
28
src/Form/API/TagApiEditForm.php
Normal file
28
src/Form/API/TagApiEditForm.php
Normal file
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Form\API;
|
||||
|
||||
use App\Form\TagEditForm;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
class TagApiEditForm extends TagEditForm
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function configureOptions(OptionsResolver $resolver)
|
||||
{
|
||||
parent::configureOptions($resolver);
|
||||
|
||||
$resolver->setDefaults([
|
||||
'csrf_protection' => false,
|
||||
]);
|
||||
}
|
||||
}
|
||||
50
src/Form/TagEditForm.php
Normal file
50
src/Form/TagEditForm.php
Normal file
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Form;
|
||||
|
||||
use App\Entity\Tag;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
class TagEditForm extends AbstractType
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||
{
|
||||
$builder
|
||||
->add('name', TextType::class, [
|
||||
'label' => 'label.name',
|
||||
'attr' => [
|
||||
'autofocus' => 'autofocus'
|
||||
],
|
||||
])
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function configureOptions(OptionsResolver $resolver)
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'data_class' => Tag::class,
|
||||
'csrf_protection' => true,
|
||||
'csrf_field_name' => '_token',
|
||||
'csrf_token_id' => 'tags_edit',
|
||||
'attr' => [
|
||||
'data-form-event' => 'kimai.tagUpdate'
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,18 @@ use Pagerfanta\Pagerfanta;
|
||||
|
||||
class TagRepository extends EntityRepository
|
||||
{
|
||||
/**
|
||||
* @param Tag $tag
|
||||
* @throws ORMException
|
||||
* @throws \Doctrine\ORM\OptimisticLockException
|
||||
*/
|
||||
public function saveTag(Tag $tag)
|
||||
{
|
||||
$entityManager = $this->getEntityManager();
|
||||
$entityManager->persist($tag);
|
||||
$entityManager->flush();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Tag $tag
|
||||
* @throws ORMException
|
||||
|
||||
@@ -203,7 +203,8 @@
|
||||
{
|
||||
locale: '{{ app.request.locale }}',
|
||||
twentyFourHours: {{ 'true'|hour24('false') }},
|
||||
autoReloadDatatable: {% if theme_config('auto_reload_datatable') %}true{% else %}false{% endif %}
|
||||
autoReloadDatatable: {% if theme_config('auto_reload_datatable') %}true{% else %}false{% endif %},
|
||||
autoComplete: {{ theme_config('autocomplete_chars') }}
|
||||
},
|
||||
{
|
||||
'confirm': '{{ 'confirm'|trans }}',
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
|
||||
{% set actions = {'search': {'class': 'search-toggle visible-xs-inline'}} %}
|
||||
|
||||
{% if is_granted('manage_tag') %}
|
||||
{% set actions = actions|merge({'create': {'url': path('tags_create'), 'class': 'modal-ajax-form'}}) %}
|
||||
{% endif %}
|
||||
|
||||
{% set actions = actions|merge({'help': {'url': 'tags.html'|docu_link, 'target': '_blank'}}) %}
|
||||
|
||||
{% set event = trigger('actions.tags', {'actions': actions, 'view': view}) %}
|
||||
@@ -14,14 +18,32 @@
|
||||
|
||||
{% set actions = {} %}
|
||||
|
||||
{% if is_granted('view_other_timesheet') %}
|
||||
{% set actions = actions|merge({'timesheet': path('admin_timesheet', {'tags': tag.name})}) %}
|
||||
{% if tag.id is not empty %}
|
||||
{% if is_granted('manage_tag') %}
|
||||
{% set class = '' %}
|
||||
{% if view != 'edit' %}
|
||||
{% set class = 'modal-ajax-form' %}
|
||||
{% endif %}
|
||||
{% set actions = actions|merge({'edit': {'url': path('tags_edit', {'id': tag.id}), 'class': class}}) %}
|
||||
{% endif %}
|
||||
|
||||
{% if is_granted('view_other_timesheet') %}
|
||||
{% set actions = actions|merge({'timesheet': path('admin_timesheet', {'tags': tag.name})}) %}
|
||||
{% endif %}
|
||||
|
||||
{% if is_granted('delete_tag') %}
|
||||
{% set actions = actions|merge({'trash': {'url': path('delete_tag', {'id' : tag.id}), 'class': 'api-link', 'attr': {'data-event': 'kimai.tagDelete kimai.tagUpdate', 'data-method': 'DELETE', 'data-question': 'confirm.delete', 'data-msg-error': 'action.delete.error', 'data-msg-success': 'action.delete.success'}}}) %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% if is_granted('delete_tag') %}
|
||||
{% set actions = actions|merge({'trash': {'url': path('delete_tag', {'id' : tag.id}), 'class': 'api-link', 'attr': {'data-event': 'kimai.tagDelete kimai.tagUpdate', 'data-method': 'DELETE', 'data-question': 'confirm.delete', 'data-msg-error': 'action.delete.error', 'data-msg-success': 'action.delete.success'}}}) %}
|
||||
{% if view != 'index' %}
|
||||
{% set actions = actions|merge({'back': path('tags')}) %}
|
||||
{% endif %}
|
||||
|
||||
|
||||
{% set event = trigger('actions.tag', {'actions': actions, 'view': view, 'tag': tag}) %}
|
||||
{{ widgets.table_actions(event.payload.actions) }}
|
||||
{% if view == 'index' %}
|
||||
{{ widgets.table_actions(event.payload.actions) }}
|
||||
{% else %}
|
||||
{{ widgets.entity_actions(event.payload.actions) }}
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
14
templates/tags/edit.html.twig
Normal file
14
templates/tags/edit.html.twig
Normal file
@@ -0,0 +1,14 @@
|
||||
{% extends app.request.xmlHttpRequest ? 'form.html.twig' : 'base.html.twig' %}
|
||||
{% import "macros/widgets.html.twig" as widgets %}
|
||||
{% import "tags/actions.html.twig" as actions %}
|
||||
|
||||
{% block page_title %}{{ 'tags.title'|trans({}, 'tags') }}{% endblock %}
|
||||
{% block page_actions %}{{ actions.tag(tag, 'edit') }}{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
{{ include(app.request.xmlHttpRequest ? 'default/_form_modal.html.twig' : 'default/_form.html.twig', {
|
||||
'title': tag.name|default('create'|trans),
|
||||
'form': form,
|
||||
'back': path('tags')
|
||||
}) }}
|
||||
{% endblock %}
|
||||
@@ -21,6 +21,7 @@
|
||||
} %}
|
||||
|
||||
{% set tableName = 'admin_tags' %}
|
||||
{% set manageAllowed = is_granted('manage_tag') %}
|
||||
|
||||
{{ tables.data_table_header(tableName, columns, false, 'kimai.tagUpdate') }}
|
||||
{% for tag in tags %}
|
||||
@@ -29,7 +30,7 @@
|
||||
{% set type = 'warning' %}
|
||||
{% endif %}
|
||||
|
||||
<tr>
|
||||
<tr{% if manageAllowed %} class="modal-ajax-form open-edit" data-href="{{ path('tags_edit', {'id': tag.id}) }}"{% endif %}>
|
||||
<td>{{ tag.name }}</td>
|
||||
<td>{{ widgets.label(tag.amount, type) }}</td>
|
||||
<td class="actions">
|
||||
|
||||
@@ -55,6 +55,35 @@ class TagControllerTest extends APIControllerBaseTest
|
||||
$this->assertEquals(0, count($result));
|
||||
}
|
||||
|
||||
public function testPostAction()
|
||||
{
|
||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
|
||||
$data = [
|
||||
'name' => 'foo',
|
||||
];
|
||||
$this->request($client, '/api/tags', 'POST', [], json_encode($data));
|
||||
$this->assertTrue($client->getResponse()->isSuccessful());
|
||||
|
||||
$result = json_decode($client->getResponse()->getContent(), true);
|
||||
$this->assertIsArray($result);
|
||||
$this->assertStructure($result);
|
||||
$this->assertNotEmpty($result['id']);
|
||||
}
|
||||
|
||||
public function testPostActionWithInvalidUser()
|
||||
{
|
||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
|
||||
$data = [
|
||||
'name' => 'foo',
|
||||
];
|
||||
$this->request($client, '/api/tags', 'POST', [], json_encode($data));
|
||||
$response = $client->getResponse();
|
||||
$this->assertFalse($response->isSuccessful());
|
||||
$this->assertEquals(Response::HTTP_FORBIDDEN, $response->getStatusCode());
|
||||
$json = json_decode($response->getContent(), true);
|
||||
$this->assertEquals('User cannot create tags', $json['message']);
|
||||
}
|
||||
|
||||
public function testPartOfEntries()
|
||||
{
|
||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
|
||||
@@ -90,4 +119,21 @@ class TagControllerTest extends APIControllerBaseTest
|
||||
{
|
||||
$this->assertEntityNotFoundForDelete(User::ROLE_ADMIN, '/api/tags/255', []);
|
||||
}
|
||||
|
||||
protected function assertStructure(array $result, $full = true)
|
||||
{
|
||||
$expectedKeys = [
|
||||
'id', 'name', 'timesheets'
|
||||
];
|
||||
|
||||
if ($full) {
|
||||
$expectedKeys = array_merge($expectedKeys, []);
|
||||
}
|
||||
|
||||
$actual = array_keys($result);
|
||||
sort($actual);
|
||||
sort($expectedKeys);
|
||||
|
||||
$this->assertEquals($expectedKeys, $actual, 'Tag structure does not match');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,4 +60,39 @@ class TagControllerTest extends ControllerBaseTest
|
||||
$this->assertHasDataTable($client);
|
||||
$this->assertDataTableRowCount($client, 'datatable_admin_tags', 2);
|
||||
}
|
||||
|
||||
public function testCreateAction()
|
||||
{
|
||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
|
||||
$this->assertAccessIsGranted($client, '/admin/tags/create');
|
||||
$form = $client->getCrawler()->filter('form[name=tag_edit_form]')->form();
|
||||
$client->submit($form, [
|
||||
'tag_edit_form' => [
|
||||
'name' => 'A tAG Name!',
|
||||
]
|
||||
]);
|
||||
$this->assertIsRedirect($client, $this->createUrl('/admin/tags/'));
|
||||
$client->followRedirect();
|
||||
$this->assertHasDataTable($client);
|
||||
|
||||
$this->request($client, '/admin/tags/11/edit');
|
||||
$editForm = $client->getCrawler()->filter('form[name=tag_edit_form]')->form();
|
||||
$this->assertEquals('A tAG Name!', $editForm->get('tag_edit_form[name]')->getValue());
|
||||
}
|
||||
|
||||
public function testEditAction()
|
||||
{
|
||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
|
||||
$this->assertAccessIsGranted($client, '/admin/tags/1/edit');
|
||||
$form = $client->getCrawler()->filter('form[name=tag_edit_form]')->form();
|
||||
$client->submit($form, [
|
||||
'tag_edit_form' => ['name' => 'Test 2 updated']
|
||||
]);
|
||||
$this->assertIsRedirect($client, $this->createUrl('/admin/tags/'));
|
||||
$client->followRedirect();
|
||||
$this->assertHasDataTable($client);
|
||||
$this->request($client, '/admin/tags/1/edit');
|
||||
$editForm = $client->getCrawler()->filter('form[name=tag_edit_form]')->form();
|
||||
$this->assertEquals('Test 2 updated', $editForm->get('tag_edit_form[name]')->getValue());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -231,6 +231,6 @@ class UserControllerTest extends ControllerBaseTest
|
||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_SUPER_ADMIN);
|
||||
$this->assertAccessIsGranted($client, '/admin/user/permissions');
|
||||
$this->assertHasDataTable($client);
|
||||
$this->assertDataTableRowCount($client, 'datatable_user_admin_permissions', 80);
|
||||
$this->assertDataTableRowCount($client, 'datatable_user_admin_permissions', 81);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,6 +117,7 @@ class AppExtensionTest extends TestCase
|
||||
'title' => null,
|
||||
],
|
||||
'auto_reload_datatable' => false,
|
||||
'autocomplete_chars' => 3,
|
||||
],
|
||||
'kimai.theme.select_type' => null,
|
||||
'kimai.theme.show_about' => true,
|
||||
|
||||
@@ -70,6 +70,10 @@
|
||||
<source>label.theme.select_type</source>
|
||||
<target>Art der Auswahl-Boxen in Formularen</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="label.theme.autocomplete_chars">
|
||||
<source>label.theme.autocomplete_chars</source>
|
||||
<target>Minimale Anzahl Buchstaben für den Start der Autovervollständigung</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="label.calendar.week_numbers">
|
||||
<source>label.calendar.week_numbers</source>
|
||||
<target>Kalenderwochen anzeigen</target>
|
||||
|
||||
@@ -70,6 +70,10 @@
|
||||
<source>label.theme.select_type</source>
|
||||
<target>Type of select-boxes in forms</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="label.theme.autocomplete_chars">
|
||||
<source>label.theme.autocomplete_chars</source>
|
||||
<target>Minimum number of letters to start auto-completion</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="label.calendar.week_numbers">
|
||||
<source>label.calendar.week_numbers</source>
|
||||
<target>Display week numbers</target>
|
||||
|
||||
Reference in New Issue
Block a user