Form and theme improvements (#513)

This commit is contained in:
Kevin Papst
2019-01-21 13:10:53 +01:00
committed by GitHub
parent 47ac50b4d0
commit a9ece209ae
35 changed files with 234 additions and 292 deletions

1
.gitignore vendored
View File

@@ -9,6 +9,7 @@
templates/invoice/renderer/.~lock* templates/invoice/renderer/.~lock*
/var/data/*.sqlite /var/data/*.sqlite
/var/data/*.sqlite-journal
!/var/data/kimai_test.sqlite !/var/data/kimai_test.sqlite
/var/coverage/ /var/coverage/
/var/cache/* /var/cache/*

View File

@@ -1,165 +0,0 @@
/*
* 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.
*/
@import "~bootstrap-sass/assets/stylesheets/bootstrap/variables";
.error-page {
margin-bottom: 50px;
}
/* ================================ CONTENT ================================ */
.content {
padding: 15px 0;
}
.content-header {
h1 {
small {
display: none;
}
}
}
@media (min-width: $screen-sm-min) {
.content-header {
h1 {
small {
display: inline-block;
}
}
}
.content {
padding: 15px;
}
}
/* ================================ NAVBAR ================================ */
.navbar-nav > li > a.ddt-large {
padding-top: 12px;
padding-bottom: 9px;
}
li.messages-menu ul.menu li .pull-left i {
color: #dd4b39;
}
.navbar-nav>.messages-menu>.dropdown-menu>li .menu>li>a>p {
overflow: hidden;
text-overflow: ellipsis;
}
.navbar-nav .start_record {
text-align: center;
}
/*
.ticktac:hover i.running{
color: #4ff131;
}
li.open .ticktac i.running{
color: #4ff131;
}
.ticktac i.stopped{
color: #fff;
}
.ticktac:hover i.stopped{
color: #4ff131;
}
.user-panel>.info {
left: 40px;
padding-top: 0px;
}
*/
/* ================================ TOOLBAR ================================ */
/* Page based action buttons in the upper right corner of the content area */
.content-header > .breadcrumb {
position: absolute;
float: right;
background: transparent;
}
/* Right sidebar with additional tabs for personal settings and "About Kimai" section */
.control-sidebar {
select {
color: $input-color;
background-color: $input-bg;
}
div.image {
img.img-circle {
max-width: 34px;
}
}
}
/* The filter form is available on most pages above the datatable */
@media (min-width: 768px) {
.toolbar {
form.navbar-form {
font-size: $font-size-base;
.form-control {
display: inline-block;
width: 100%;
vertical-align: middle;
}
}
}
}
/* ================================ SIDEBAR ================================ */
/* Used for the Sidebar UserPanel (which is deactivated right now) */
.user-panel>.info {
left: 80px;
}
/* ================================ FOOTER ================================ */
footer.main-footer {
padding: 5px;
font-size: 80%;
}
/* ================================ PRINT ================================ */
@media print{
.sf-toolbar, .control-sidebar {display: none !important;}
}
/* ================================ INVOICES ================================ */
.invoice {
margin: 0;
}
@media (min-width: $screen-sm-min) {
.invoice {
margin: 10px 25px;
}
}
.page-header-small{
font-size: 14px;
}
div.invoice-address {
margin-top: 30px;
margin-bottom: 30px;
}
table.invoice-records {}
table.invoice-meta th {
padding-right: 40px;
}
table.invoice-sum th {
width: 70%;
}
table.invoice-sum th, table.invoice-sum td {
text-align: right;
}

View File

@@ -35,6 +35,12 @@
width: 100%; width: 100%;
vertical-align: middle; vertical-align: middle;
} }
.control-label {
width: 100%;
}
.input-group {
width: 100%;
}
} }
} }
} }

14
composer.lock generated
View File

@@ -1172,12 +1172,12 @@
"version": "v2.6.2", "version": "v2.6.2",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/doctrine/doctrine2.git", "url": "https://github.com/doctrine/orm.git",
"reference": "d2b4dd71d2a276edd65d0c170375b445f8a4a4a8" "reference": "d2b4dd71d2a276edd65d0c170375b445f8a4a4a8"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/doctrine/doctrine2/zipball/d2b4dd71d2a276edd65d0c170375b445f8a4a4a8", "url": "https://api.github.com/repos/doctrine/orm/zipball/d2b4dd71d2a276edd65d0c170375b445f8a4a4a8",
"reference": "d2b4dd71d2a276edd65d0c170375b445f8a4a4a8", "reference": "d2b4dd71d2a276edd65d0c170375b445f8a4a4a8",
"shasum": "" "shasum": ""
}, },
@@ -2118,16 +2118,16 @@
}, },
{ {
"name": "kevinpapst/adminlte-bundle", "name": "kevinpapst/adminlte-bundle",
"version": "2.1.2", "version": "2.2",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/kevinpapst/AdminLTEBundle.git", "url": "https://github.com/kevinpapst/AdminLTEBundle.git",
"reference": "6a085dbfbf46a24cf21b6a1d54fa0086e6ab4cc9" "reference": "f30230e476eec0b22d1f850b6902488d14e0b95e"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/kevinpapst/AdminLTEBundle/zipball/6a085dbfbf46a24cf21b6a1d54fa0086e6ab4cc9", "url": "https://api.github.com/repos/kevinpapst/AdminLTEBundle/zipball/f30230e476eec0b22d1f850b6902488d14e0b95e",
"reference": "6a085dbfbf46a24cf21b6a1d54fa0086e6ab4cc9", "reference": "f30230e476eec0b22d1f850b6902488d14e0b95e",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@@ -2175,7 +2175,7 @@
} }
], ],
"description": "Admin theme bundle for Symfony 4 based on AdminLTE 2.4.8 with FOSUserBundle support", "description": "Admin theme bundle for Symfony 4 based on AdminLTE 2.4.8 with FOSUserBundle support",
"time": "2018-10-10T11:38:54+00:00" "time": "2019-01-20T16:36:22+00:00"
}, },
{ {
"name": "markbaker/complex", "name": "markbaker/complex",

View File

@@ -2,3 +2,5 @@ twig:
#paths: ['%kernel.project_dir%/templates'] #paths: ['%kernel.project_dir%/templates']
debug: '%kernel.debug%' debug: '%kernel.debug%'
strict_variables: '%kernel.debug%' strict_variables: '%kernel.debug%'
form_themes:
- 'form/kimai-theme.html.twig'

View File

@@ -97,6 +97,10 @@ services:
arguments: arguments:
$durationOnly: "%kimai.timesheet.duration_only%" $durationOnly: "%kimai.timesheet.duration_only%"
App\Form\Extension\SelectWithApiDataExtension:
tags:
- { name: form.type_extension, extended_type: Symfony\Bridge\Doctrine\Form\Type\EntityType }
# ================================================================================ # ================================================================================
# THEME # THEME
# ================================================================================ # ================================================================================

File diff suppressed because one or more lines are too long

View File

@@ -1,6 +1,6 @@
{ {
"build/app.js": "/build/app.js?d823754f6c3293656052", "build/app.js": "/build/app.js?d823754f6c3293656052",
"build/app.css": "/build/app.css?979d88ed847886068fcf3eb0bc3de545", "build/app.css": "/build/app.css?c7fd4b9cec5de89467ba89547d2857aa",
"build/images/blue@2x.png": "/build/images/blue@2x.png?2694acfd", "build/images/blue@2x.png": "/build/images/blue@2x.png?2694acfd",
"build/images/blue.png": "/build/images/blue.png?96f8a905", "build/images/blue.png": "/build/images/blue.png?96f8a905",
"build/fonts/fa-solid-900.woff2": "/build/fonts/fa-solid-900.woff2?e8a92a29", "build/fonts/fa-solid-900.woff2": "/build/fonts/fa-solid-900.woff2?e8a92a29",

View File

@@ -92,6 +92,7 @@ class UserController extends AbstractController
public function createAction(Request $request) public function createAction(Request $request)
{ {
$user = new User(); $user = new User();
$user->setEnabled(true);
$editForm = $this->createEditForm($user); $editForm = $this->createEditForm($user);
$editForm->handleRequest($request); $editForm->handleRequest($request);

View File

@@ -19,7 +19,7 @@ use App\Form\Type\SkinType;
use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType; use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\Form\Extension\Core\Type\NumberType; use Symfony\Component\Form\Extension\Core\Type\MoneyType;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface; use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
use Symfony\Component\Validator\Constraints\Range; use Symfony\Component\Validator\Constraints\Range;
@@ -86,13 +86,13 @@ class UserPreferenceSubscriber implements EventSubscriberInterface
(new UserPreference()) (new UserPreference())
->setName(UserPreference::HOURLY_RATE) ->setName(UserPreference::HOURLY_RATE)
->setValue(0) ->setValue(0)
->setType(NumberType::class) ->setType(MoneyType::class)
->setEnabled($enableHourlyRate) ->setEnabled($enableHourlyRate)
->addConstraint(new Range(['min' => 0])), ->addConstraint(new Range(['min' => 0])),
(new UserPreference()) (new UserPreference())
->setName('language') ->setName('language')
->setValue('en') // TODO fetch from services.yaml ->setValue('en')
->setType(LanguageType::class), ->setType(LanguageType::class),
(new UserPreference()) (new UserPreference())

View File

@@ -17,7 +17,7 @@ use App\Repository\CustomerRepository;
use App\Repository\ProjectRepository; use App\Repository\ProjectRepository;
use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType; use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\Form\Extension\Core\Type\NumberType; use Symfony\Component\Form\Extension\Core\Type\MoneyType;
use Symfony\Component\Form\Extension\Core\Type\TextareaType; use Symfony\Component\Form\Extension\Core\Type\TextareaType;
use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Form\FormBuilderInterface;
@@ -38,10 +38,12 @@ class ActivityEditForm extends AbstractType
$project = null; $project = null;
$customer = null; $customer = null;
$currency = false;
if (null !== $entry->getProject()) { if (null !== $entry->getProject()) {
$project = $entry->getProject(); $project = $entry->getProject();
$customer = $project->getCustomer(); $customer = $project->getCustomer();
$currency = $customer->getCurrency();
} }
$builder $builder
@@ -62,9 +64,10 @@ class ActivityEditForm extends AbstractType
'data' => $customer ? $customer : null, 'data' => $customer ? $customer : null,
'required' => false, 'required' => false,
'mapped' => false, 'mapped' => false,
'attr' => [ 'api_data' => [
'data-related-select' => $this->getBlockPrefix() . '_project', 'select' => 'project',
'data-api-url' => ['get_projects', ['customer' => '-s-']], 'route' => 'get_projects',
'route_params' => ['customer' => '-s-']
], ],
]) ])
->add('project', ProjectType::class, [ ->add('project', ProjectType::class, [
@@ -74,13 +77,15 @@ class ActivityEditForm extends AbstractType
return $repo->builderForEntityType($project, $customer); return $repo->builderForEntityType($project, $customer);
}, },
]) ])
->add('fixedRate', NumberType::class, [ ->add('fixedRate', MoneyType::class, [
'label' => 'label.fixed_rate', 'label' => 'label.fixed_rate',
'required' => false, 'required' => false,
'currency' => $currency,
]) ])
->add('hourlyRate', NumberType::class, [ ->add('hourlyRate', MoneyType::class, [
'label' => 'label.hourly_rate', 'label' => 'label.hourly_rate',
'required' => false, 'required' => false,
'currency' => $currency,
]) ])
// boolean // boolean
->add('visible', YesNoType::class, [ ->add('visible', YesNoType::class, [

View File

@@ -15,7 +15,7 @@ use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\CountryType; use Symfony\Component\Form\Extension\Core\Type\CountryType;
use Symfony\Component\Form\Extension\Core\Type\CurrencyType; use Symfony\Component\Form\Extension\Core\Type\CurrencyType;
use Symfony\Component\Form\Extension\Core\Type\EmailType; use Symfony\Component\Form\Extension\Core\Type\EmailType;
use Symfony\Component\Form\Extension\Core\Type\NumberType; use Symfony\Component\Form\Extension\Core\Type\MoneyType;
use Symfony\Component\Form\Extension\Core\Type\TelType; use Symfony\Component\Form\Extension\Core\Type\TelType;
use Symfony\Component\Form\Extension\Core\Type\TextareaType; use Symfony\Component\Form\Extension\Core\Type\TextareaType;
use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\Form\Extension\Core\Type\TextType;
@@ -34,6 +34,9 @@ class CustomerEditForm extends AbstractType
*/ */
public function buildForm(FormBuilderInterface $builder, array $options) public function buildForm(FormBuilderInterface $builder, array $options)
{ {
/** @var Customer $customer */
$customer = $options['data'];
$builder $builder
->add('name', TextType::class, [ ->add('name', TextType::class, [
'label' => 'label.name', 'label' => 'label.name',
@@ -71,10 +74,12 @@ class CustomerEditForm extends AbstractType
->add('fax', TelType::class, [ ->add('fax', TelType::class, [
'label' => 'label.fax', 'label' => 'label.fax',
'required' => false, 'required' => false,
'attr' => ['icon' => 'fax'],
]) ])
->add('mobile', TelType::class, [ ->add('mobile', TelType::class, [
'label' => 'label.mobile', 'label' => 'label.mobile',
'required' => false, 'required' => false,
'attr' => ['icon' => 'mobile'],
]) ])
->add('mail', EmailType::class, [ ->add('mail', EmailType::class, [
'label' => 'label.email', 'label' => 'label.email',
@@ -87,13 +92,15 @@ class CustomerEditForm extends AbstractType
->add('timezone', TimezoneType::class, [ ->add('timezone', TimezoneType::class, [
'label' => 'label.timezone', 'label' => 'label.timezone',
]) ])
->add('fixedRate', NumberType::class, [ ->add('fixedRate', MoneyType::class, [
'label' => 'label.fixed_rate', 'label' => 'label.fixed_rate',
'required' => false, 'required' => false,
'currency' => $customer->getCurrency() ?? false,
]) ])
->add('hourlyRate', NumberType::class, [ ->add('hourlyRate', MoneyType::class, [
'label' => 'label.hourly_rate', 'label' => 'label.hourly_rate',
'required' => false, 'required' => false,
'currency' => $customer->getCurrency() ?? false,
]) ])
->add('visible', YesNoType::class, [ ->add('visible', YesNoType::class, [
'label' => 'label.visible', 'label' => 'label.visible',

View File

@@ -0,0 +1,85 @@
<?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\Extension;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\AbstractTypeExtension;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
/**
* Support Remote-API calls for Entity select-boxes.
*/
class SelectWithApiDataExtension extends AbstractTypeExtension
{
/**
* @var UrlGeneratorInterface
*/
private $router;
/**
* @param UrlGeneratorInterface $router
*/
public function __construct(UrlGeneratorInterface $router)
{
$this->router = $router;
}
/**
* @return string
*/
public function getExtendedType()
{
return EntityType::class;
}
/**
* @param FormView $view
* @param FormInterface $form
* @param array $options
*/
public function buildView(FormView $view, FormInterface $form, array $options)
{
if (!isset($options['api_data'])) {
return;
}
$apiData = $options['api_data'];
if (!isset($apiData['select'])) {
throw new \InvalidArgumentException('Missing "select" option for "api_data" option for form "' . $form->getName() . '"');
}
if (!isset($apiData['route'])) {
throw new \InvalidArgumentException('Missing "route" option for "api_data" option for form "' . $form->getName() . '"');
}
if (!isset($apiData['route_params'])) {
$apiData['route_params'] = [];
}
$view->vars['attr'] = array_merge($view->vars['attr'], [
'data-related-select' => $form->getParent()->getName() . '_' . $apiData['select'],
'data-api-url' => $this->router->generate($apiData['route'], $apiData['route_params']),
]);
}
/**
* @param OptionsResolver $resolver
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefined(['api_data']);
$resolver->setAllowedTypes('api_data', 'array');
//$resolver->setDefault('api_data', []);
}
}

View File

@@ -17,7 +17,6 @@ use App\Repository\CustomerRepository;
use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType; use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\Form\Extension\Core\Type\MoneyType; use Symfony\Component\Form\Extension\Core\Type\MoneyType;
use Symfony\Component\Form\Extension\Core\Type\NumberType;
use Symfony\Component\Form\Extension\Core\Type\TextareaType; use Symfony\Component\Form\Extension\Core\Type\TextareaType;
use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Form\FormBuilderInterface;
@@ -37,8 +36,11 @@ class ProjectEditForm extends AbstractType
$entry = $options['data']; $entry = $options['data'];
$customer = null; $customer = null;
$currency = false;
if ($entry->getId() !== null) { if ($entry->getId() !== null) {
$customer = $entry->getCustomer(); $customer = $entry->getCustomer();
$currency = $customer->getCurrency();
} }
$builder $builder
@@ -59,18 +61,20 @@ class ProjectEditForm extends AbstractType
return $repo->builderForEntityType($customer); return $repo->builderForEntityType($customer);
}, },
]) ])
->add('fixedRate', NumberType::class, [ ->add('fixedRate', MoneyType::class, [
'label' => 'label.fixed_rate', 'label' => 'label.fixed_rate',
'required' => false, 'required' => false,
'currency' => $currency,
]) ])
->add('hourlyRate', NumberType::class, [ ->add('hourlyRate', MoneyType::class, [
'label' => 'label.hourly_rate', 'label' => 'label.hourly_rate',
'required' => false, 'required' => false,
'currency' => $currency,
]) ])
->add('budget', MoneyType::class, [ ->add('budget', MoneyType::class, [
'label' => 'label.budget', 'label' => 'label.budget',
'currency' => $customer ? $customer->getCurrency() : $builder->getOption('currency'),
'required' => false, 'required' => false,
'currency' => $currency,
]) ])
->add('visible', YesNoType::class, [ ->add('visible', YesNoType::class, [
'label' => 'label.visible', 'label' => 'label.visible',

View File

@@ -20,7 +20,7 @@ use App\Repository\CustomerRepository;
use App\Repository\ProjectRepository; use App\Repository\ProjectRepository;
use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\DateTimeType; use Symfony\Component\Form\Extension\Core\Type\DateTimeType;
use Symfony\Component\Form\Extension\Core\Type\NumberType; use Symfony\Component\Form\Extension\Core\Type\MoneyType;
use Symfony\Component\Form\Extension\Core\Type\TextareaType; use Symfony\Component\Form\Extension\Core\Type\TextareaType;
use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormEvent; use Symfony\Component\Form\FormEvent;
@@ -67,6 +67,7 @@ class TimesheetEditForm extends AbstractType
$activity = null; $activity = null;
$project = null; $project = null;
$customer = null; $customer = null;
$currency = false;
$end = null; $end = null;
if (isset($options['data'])) { if (isset($options['data'])) {
@@ -75,12 +76,16 @@ class TimesheetEditForm extends AbstractType
$activity = $entry->getActivity(); $activity = $entry->getActivity();
$project = $entry->getProject(); $project = $entry->getProject();
$customer = null === $entry->getProject() ? null : $entry->getProject()->getCustomer(); $customer = null === $project ? null : $project->getCustomer();
if (null === $project && null !== $activity) { if (null === $project && null !== $activity) {
$project = $activity->getProject(); $project = $activity->getProject();
} }
if (null !== $customer) {
$currency = $customer->getCurrency();
}
$end = $entry->getEnd(); $end = $entry->getEnd();
} }
@@ -124,10 +129,12 @@ class TimesheetEditForm extends AbstractType
}, },
'data' => $customer ? $customer : '', 'data' => $customer ? $customer : '',
'required' => false, 'required' => false,
'placeholder' => null === $customer ? '' : null,
'mapped' => false, 'mapped' => false,
'attr' => [ 'api_data' => [
'data-related-select' => $this->getBlockPrefix() . '_project', 'select' => 'project',
'data-api-url' => ['get_projects', ['customer' => '-s-']], 'route' => 'get_projects',
'route_params' => ['customer' => '-s-']
], ],
]); ]);
} else { } else {
@@ -148,12 +155,13 @@ class TimesheetEditForm extends AbstractType
'description' => 'Project ID', 'description' => 'Project ID',
], ],
'required' => true, 'required' => true,
'query_builder' => function (ProjectRepository $repo) use ($project) { 'query_builder' => function (ProjectRepository $repo) use ($project, $customer) {
return $repo->builderForEntityType($project); return $repo->builderForEntityType($project, $customer);
}, },
'attr' => [ 'api_data' => [
'data-related-select' => $this->getBlockPrefix() . '_activity', 'select' => 'activity',
'data-api-url' => ['get_activities', ['project' => '-s-']], 'route' => 'get_activities',
'route_params' => ['project' => '-s-']
], ],
])); ]));
@@ -164,8 +172,8 @@ class TimesheetEditForm extends AbstractType
'type' => 'integer', 'type' => 'integer',
'description' => 'Activity ID', 'description' => 'Activity ID',
], ],
'query_builder' => function (ActivityRepository $repo) use ($activity) { 'query_builder' => function (ActivityRepository $repo) use ($activity, $project) {
return $repo->builderForEntityType($activity); return $repo->builderForEntityType($activity, $project);
}, },
]) ])
->add('description', TextareaType::class, [ ->add('description', TextareaType::class, [
@@ -176,13 +184,15 @@ class TimesheetEditForm extends AbstractType
if ($options['include_rate']) { if ($options['include_rate']) {
$builder $builder
->add('fixedRate', NumberType::class, [ ->add('fixedRate', MoneyType::class, [
'label' => 'label.fixed_rate', 'label' => 'label.fixed_rate',
'required' => false, 'required' => false,
'currency' => $currency,
]) ])
->add('hourlyRate', NumberType::class, [ ->add('hourlyRate', MoneyType::class, [
'label' => 'label.hourly_rate', 'label' => 'label.hourly_rate',
'required' => false, 'required' => false,
'currency' => $currency,
]); ]);
} }
@@ -198,9 +208,10 @@ class TimesheetEditForm extends AbstractType
'query_builder' => function (ProjectRepository $repo) use ($customer) { 'query_builder' => function (ProjectRepository $repo) use ($customer) {
return $repo->builderForEntityType(null, $customer); return $repo->builderForEntityType(null, $customer);
}, },
'attr' => [ 'api_data' => [
'data-related-select' => $this->getBlockPrefix() . '_activity', 'select' => 'activity',
'data-api-url' => ['get_activities', ['project' => '-s-']], 'route' => 'get_activities',
'route_params' => ['project' => '-s-']
], ],
]); ]);
} }

View File

@@ -78,10 +78,15 @@ abstract class AbstractToolbarForm extends AbstractType
/** /**
* @param FormBuilderInterface $builder * @param FormBuilderInterface $builder
* @param null|string $label
*/ */
protected function addVisibilityChoice(FormBuilderInterface $builder) protected function addVisibilityChoice(FormBuilderInterface $builder, ?string $label = null)
{ {
$builder->add('visibility', VisibilityType::class, []); $builder->add('visibility', VisibilityType::class, [
'required' => false,
'placeholder' => null,
'label' => $label
]);
} }
/** /**

View File

@@ -36,7 +36,8 @@ class InvoiceToolbarForm extends AbstractToolbarForm
protected function addTemplateChoice(FormBuilderInterface $builder) protected function addTemplateChoice(FormBuilderInterface $builder)
{ {
$builder->add('template', InvoiceTemplateType::class, [ $builder->add('template', InvoiceTemplateType::class, [
'required' => true 'required' => false,
'placeholder' => null,
]); ]);
} }

View File

@@ -40,6 +40,8 @@ class TimesheetToolbarForm extends AbstractToolbarForm
{ {
$builder->add('state', ChoiceType::class, [ $builder->add('state', ChoiceType::class, [
'label' => 'label.entryState', 'label' => 'label.entryState',
'required' => false,
'placeholder' => null,
'choices' => [ 'choices' => [
'entryState.all' => TimesheetQuery::STATE_ALL, 'entryState.all' => TimesheetQuery::STATE_ALL,
'entryState.running' => TimesheetQuery::STATE_RUNNING, 'entryState.running' => TimesheetQuery::STATE_RUNNING,

View File

@@ -24,7 +24,7 @@ class UserToolbarForm extends AbstractToolbarForm
public function buildForm(FormBuilderInterface $builder, array $options) public function buildForm(FormBuilderInterface $builder, array $options)
{ {
$this->addPageSizeChoice($builder); $this->addPageSizeChoice($builder);
$this->addVisibilityChoice($builder); $this->addVisibilityChoice($builder, 'label.active');
$this->addUserRoleChoice($builder); $this->addUserRoleChoice($builder);
} }

View File

@@ -10,7 +10,7 @@
namespace App\Form\Type; namespace App\Form\Type;
use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType; use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\OptionsResolver\OptionsResolver; use Symfony\Component\OptionsResolver\OptionsResolver;
/** /**
@@ -24,9 +24,9 @@ class YesNoType extends AbstractType
public function configureOptions(OptionsResolver $resolver) public function configureOptions(OptionsResolver $resolver)
{ {
$resolver->setDefaults([ $resolver->setDefaults([
'choices' => ['yes' => true, 'no' => false], 'value' => true,
'multiple' => false, 'false_values' => [null, 0, false],
'expanded' => true, 'required' => false,
]); ]);
} }
@@ -35,6 +35,6 @@ class YesNoType extends AbstractType
*/ */
public function getParent() public function getParent()
{ {
return ChoiceType::class; return CheckboxType::class;
} }
} }

View File

@@ -12,6 +12,7 @@ namespace App\Form;
use App\Entity\User; use App\Entity\User;
use App\Form\Type\YesNoType; use App\Form\Type\YesNoType;
use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\EmailType;
use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver; use Symfony\Component\OptionsResolver\OptionsResolver;
@@ -27,26 +28,21 @@ class UserEditType extends AbstractType
public function buildForm(FormBuilderInterface $builder, array $options) public function buildForm(FormBuilderInterface $builder, array $options)
{ {
$builder $builder
// string - length 160
->add('alias', TextType::class, [ ->add('alias', TextType::class, [
'label' => 'label.alias', 'label' => 'label.alias',
'required' => false, 'required' => false,
]) ])
// string - length 50
->add('title', TextType::class, [ ->add('title', TextType::class, [
'label' => 'label.title', 'label' => 'label.title',
'required' => false, 'required' => false,
]) ])
// string - length 255
->add('avatar', TextType::class, [ ->add('avatar', TextType::class, [
'label' => 'label.avatar', 'label' => 'label.avatar',
'required' => false, 'required' => false,
]) ])
// string - length 160 ->add('email', EmailType::class, [
->add('email', TextType::class, [
'label' => 'label.email', 'label' => 'label.email',
]) ])
// boolean
->add('enabled', YesNoType::class, [ ->add('enabled', YesNoType::class, [
'label' => 'label.active', 'label' => 'label.active',
]) ])

View File

@@ -317,19 +317,24 @@ class TimesheetRepository extends AbstractRepository
->setParameter('user', $query->getUser()); ->setParameter('user', $query->getUser());
} }
if (TimesheetQuery::STATE_RUNNING == $query->getState()) {
$qb->andWhere($qb->expr()->isNull('t.end'));
} elseif (TimesheetQuery::STATE_STOPPED == $query->getState()) {
$qb->andWhere($qb->expr()->isNotNull('t.end'));
}
if (null !== $query->getBegin()) { if (null !== $query->getBegin()) {
$qb->andWhere('t.begin >= :begin') $qb->andWhere('t.begin >= :begin')
->setParameter('begin', $query->getBegin()); ->setParameter('begin', $query->getBegin());
} }
if (null !== $query->getEnd()) { if (TimesheetQuery::STATE_RUNNING == $query->getState()) {
$qb->andWhere('t.end <= :end') $qb->andWhere($qb->expr()->isNull('t.end'));
}
if (TimesheetQuery::STATE_STOPPED == $query->getState()) {
$qb->andWhere($qb->expr()->isNotNull('t.end'));
if (null !== $query->getEnd()) {
$qb->andWhere('t.end <= :end')
->setParameter('end', $query->getEnd());
}
} elseif (null !== $query->getBegin()) {
$qb->andWhere('t.begin <= :end')
->setParameter('end', $query->getEnd()); ->setParameter('end', $query->getEnd());
} }

View File

@@ -7,7 +7,6 @@
{% endif %} {% endif %}
</h3> </h3>
</div> </div>
{% form_theme form 'form/kimai-theme.html.twig' %}
{{ form_start(form) }} {{ form_start(form) }}
<div class="box-body"> <div class="box-body">
{{ form_widget(form) }} {{ form_widget(form) }}

View File

@@ -1,15 +1,9 @@
{% extends "@AdminLTE/layout/form-theme.html.twig" %} {% extends "@AdminLTE/layout/form-theme.html.twig" %}
{# Adds the help icon, including a link to the documentation #}
{% block form_label %} {% block form_label %}
{% if form.vars.docu_chapter is defined and form.vars.docu_chapter is not empty %} {% if form.vars.docu_chapter is defined and form.vars.docu_chapter is not empty %}
<a href="{{ path('help_chapter', {'chapter': form.vars.docu_chapter}) }}"><i class="{{ 'help'|icon }}"></i></a> <a href="{{ path('help_chapter', {'chapter': form.vars.docu_chapter}) }}"><i class="{{ 'help'|icon }}"></i></a>
{% endif %} {% endif %}
{{ parent() }} {{ parent() }}
{% endblock form_label %} {% endblock form_label %}
{% block choice_widget_collapsed %}
{% if 'data-api-url' in attr|keys %}
{% set attr = attr|merge({'data-api-url': path(attr['data-api-url']|first, attr['data-api-url']|last)}) %}
{% endif %}
{{ parent() }}
{% endblock %}

View File

@@ -1,25 +0,0 @@
{% extends "@AdminLTE/layout/form-theme.html.twig" %}
{% block date_widget %}
{% if widget == 'single_text' %}
{% if attr.class is defined %}
{% set class = attr.class ~ ' timepicker' %}
{% else %}
{% set class = ' timepicker' %}
{% endif %}
{% set class = class ~ ' datepicker' %}
{% set attr = attr|merge({'class' : class}) %}
{{ block('form_widget_simple') }}
{% else %}
{% set date_pattern = '<div class="row">' ~ date_pattern ~ '</div>'|raw %}
{{ date_pattern|replace({
'{{ year }}' : '<div class="col-xs-4">{{ year }}</div>',
'{{ month }}' : '<div class="col-xs-4">{{ month }}</div>',
'{{ day }}' : '<div class="col-xs-4">{{ day }}</div>',
})|raw|replace({
'{{ year }}': form_widget(form.year),
'{{ month }}': form_widget(form.month),
'{{ day }}': form_widget(form.day),
})|raw }}
{% endif %}
{% endblock %}

View File

@@ -1,11 +1,9 @@
{% macro toolbar(form, collapsible, displayInitial) %} {% macro toolbar(form, collapsible, displayInitial) %}
<div class="toolbar no-print"> <div class="toolbar no-print">
{% if collapsible %}<div class="collapse{% if displayInitial %} collapse.show in{% endif %}" id="{{ collapsible }}">{% endif %} {% if collapsible %}<div class="collapse{% if displayInitial %} collapse.show in{% endif %}" id="{{ collapsible }}">{% endif %}
{% form_theme form 'form/toolbar-theme.html.twig' %} {{ form_start(form, { 'attr': {'class': 'navbar-form'}}) }}
{{ form_start(form, { 'attr': {'class': 'navbar-form'}}) }} {{ form_widget(form) }}
{{ form_widget(form) }} {{ form_end(form) }}
{{ form_end(form) }} {% if collapsible %}</div>{% endif %}
{% if collapsible %}</div>{% endif %} </div>
</div>
{% endmacro %} {% endmacro %}

View File

@@ -240,13 +240,11 @@ class ActivityControllerTest extends ControllerBaseTest
'activity_edit_form' => [ 'activity_edit_form' => [
'name' => '', 'name' => '',
'project' => 0, 'project' => 0,
'visible' => 3,
] ]
], ],
[ [
'#activity_edit_form_name', '#activity_edit_form_name',
'#activity_edit_form_project', '#activity_edit_form_project',
'#activity_edit_form_visible',
] ]
], ],
]; ];

View File

@@ -212,7 +212,6 @@ class CustomerControllerTest extends ControllerBaseTest
[ [
'customer_edit_form' => [ 'customer_edit_form' => [
'name' => '', 'name' => '',
'visible' => 3,
'country' => '00', // TODO why does it not fail? 'country' => '00', // TODO why does it not fail?
'currency' => '00', // TODO why does it not fail? 'currency' => '00', // TODO why does it not fail?
'timezone' => 'XXX' 'timezone' => 'XXX'
@@ -220,7 +219,6 @@ class CustomerControllerTest extends ControllerBaseTest
], ],
[ [
'#customer_edit_form_name', '#customer_edit_form_name',
'#customer_edit_form_visible',
//'#customer_edit_form_country', //'#customer_edit_form_country',
//'#customer_edit_form_currency', //'#customer_edit_form_currency',
'#customer_edit_form_timezone', '#customer_edit_form_timezone',

View File

@@ -236,13 +236,11 @@ class ProjectControllerTest extends ControllerBaseTest
'project_edit_form' => [ 'project_edit_form' => [
'name' => '', 'name' => '',
'customer' => 0, 'customer' => 0,
'visible' => 3,
] ]
], ],
[ [
'#project_edit_form_name', '#project_edit_form_name',
'#project_edit_form_customer', '#project_edit_form_customer',
'#project_edit_form_visible',
] ]
], ],
]; ];

View File

@@ -132,14 +132,12 @@ class UserControllerTest extends ControllerBaseTest
'title' => '34rtwrtewrt', 'title' => '34rtwrtewrt',
'avatar' => 'asdfawer', 'avatar' => 'asdfawer',
'email' => 'ydfbvsdfgs', 'email' => 'ydfbvsdfgs',
'enabled' => '3',
] ]
], ],
[ [
'#user_create_username', '#user_create_username',
'#user_create_plainPassword_first', '#user_create_plainPassword_first',
'#user_create_email', '#user_create_email',
'#user_create_enabled',
] ]
], ],
]; ];

View File

@@ -210,8 +210,14 @@ abstract class ControllerBaseTest extends WebTestCase
$this->assertNotNull($field, 'Could not find form field: ' . $name); $this->assertNotNull($field, 'Could not find form field: ' . $name);
$list = $field->nextAll(); $list = $field->nextAll();
$this->assertNotNull($list, 'Form field has no validation message: ' . $name); $this->assertNotNull($list, 'Form field has no validation message: ' . $name);
$validation = $list->filter('li.text-danger'); $validation = $list->filter('li.text-danger');
$this->assertGreaterThanOrEqual(1, count($validation), 'Form field has no validation message: ' . $name); if (count($validation) < 1) {
// decorated form fields with icon have a different html structure, see kimai-theme.html.twig
$classes = $field->parents()->getNode(1)->getAttribute('class');
$this->assertContains('has-feedback', $classes, 'Form field has no validation message: ' . $name);
$this->assertContains('has-error', $classes, 'Form field has no validation message: ' . $name);
}
} }
} }

View File

@@ -83,7 +83,7 @@ class ProfileControllerTest extends ControllerBaseTest
'title' => 'Code Monkey', 'title' => 'Code Monkey',
'avatar' => '/fake/image.jpg', 'avatar' => '/fake/image.jpg',
'email' => 'updated@example.com', 'email' => 'updated@example.com',
'enabled' => 0, 'enabled' => false,
] ]
]); ]);

View File

@@ -10,6 +10,10 @@
<source>End date must not be earlier then start date.</source> <source>End date must not be earlier then start date.</source>
<target>Das Enddatum darf nicht vor dem Startdatum liegen.</target> <target>Das Enddatum darf nicht vor dem Startdatum liegen.</target>
</trans-unit> </trans-unit>
<trans-unit id="The begin date cannot be in the future.">
<source>The begin date cannot be in the future.</source>
<target>Das Startdatum darf nicht in der Zukunft liegen.</target>
</trans-unit>
</body> </body>
</file> </file>
</xliff> </xliff>

View File

@@ -10,6 +10,10 @@
<source>End date must not be earlier then start date.</source> <source>End date must not be earlier then start date.</source>
<target>End date must not be earlier then start date.</target> <target>End date must not be earlier then start date.</target>
</trans-unit> </trans-unit>
<trans-unit id="The begin date cannot be in the future.">
<source>The begin date cannot be in the future.</source>
<target>The begin date cannot be in the future.</target>
</trans-unit>
</body> </body>
</file> </file>
</xliff> </xliff>

View File

@@ -1,15 +1,17 @@
# Kimai documentation # Kimai documentation
Welcome to the official Kimai v2 documentation. This is the official Kimai v2 documentation.
We know there is a lot missing right now and would appreciate [any help](https://github.com/kevinpapst/kimai2/pulls) in writing. Most parts of Kimai should be self-explanatory, but if you miss any information please [let us know](https://github.com/kevinpapst/kimai2/issues)
If you need information which is not available right now, [ask us](https://github.com/kevinpapst/kimai2/issues) and we and we will add it as soon as possible.
try to add it as soon as possible.
## User manual ## User manual
For the most parts Kimai usage should be self-explanatory, so we will only cover topics here which were - [Invoices](invoices.md) - export your data to different formats with customizable templates
[requested](https://github.com/kevinpapst/kimai2/issues) by the community. - [Timesheets](timesheet.md) - information about timesheets
- [User and Security](users.md) - docu for user and security topics, like authentication, registration and roles
## Admins and Developers
- [Configurations](configurations.md) - intro into the global application configs - [Configurations](configurations.md) - intro into the global application configs
- [Calendar](calendar.md) - the Timesheet calendar view - [Calendar](calendar.md) - the Timesheet calendar view
@@ -19,11 +21,9 @@ For the most parts Kimai usage should be self-explanatory, so we will only cover
- [Developer](developers.md) - docs about extending Kimai's feature set - [Developer](developers.md) - docs about extending Kimai's feature set
- [API](developers_api.md) - how to use the JSON API - [API](developers_api.md) - how to use the JSON API
- [Theme settings](theme.md) - theme related settings - [Theme settings](theme.md) - theme related settings
- [FAQ](faq.md) - some answers to frequently asked questions
- [Installation](installation.md) - instructions about several installation methods - [Installation](installation.md) - instructions about several installation methods
- [Update Kimai](updates.md) - updating an existing Kimai 2 installation to the latest version
- [Docker](docker.md) - using the built-in Dockerfile for development - [Docker](docker.md) - using the built-in Dockerfile for development
- [Importing data](migration_v1.md) - how to import your timesheets from Kimai v1 - [Importing data](migration_v1.md) - how to import your timesheets from Kimai v1
- [Invoices](invoices.md) - export your data to different formats with customizable templates
- [Timesheets](timesheet.md) - information about timesheets
- [Translations](translations.md) - all about languages and translations - [Translations](translations.md) - all about languages and translations
- [User and Security](users.md) - docu for user and security topics, like authentication, registration and roles - [FAQ](faq.md) - some answers to frequently asked questions