Release 2.0.11 (#3932)

- added "today" as selector in date-range dropdown
- added feature to prevent auto-select of dropdowns with only one entry
- added hint that no changes were detected in batch update
- added negative invoice sums are possible (e.g. for credit notes)
- fix project list is expanded after submission
- fix invalid date parsing causes 500
- fix: prevent auto-select of activities in export and invoice form (in case only one global activity exists)
- fix team assignments for customer and project were not saved (using API now)
- fix form fieldset with legend styling (e.g. team project assignment)
- fix required meta-field were forced to have a value in batch update
- fix tomselect meta-field was not disabled in batch update
- fix unset internal rate is shown as 0
- fix one minute rounding problem in duration-only mode  with "now" being default time
- fix column width and label for duration-only mode
- tech debt: cleanup invoice template (remove invoice layout)
- tech debt: reorder for simpler comparison with invoice form
- possible BC for devs: remove unused methods from form trait
- bump composer packages (includes new translations for auth screens)
This commit is contained in:
Kevin Papst
2023-03-21 12:42:18 +01:00
committed by GitHub
parent 252652082d
commit 3a5d7a62de
41 changed files with 590 additions and 1288 deletions

View File

@@ -14,9 +14,9 @@ use Doctrine\Common\Collections\Collection;
class MultiUpdateTableDTO
{
/**
* @var iterable<object>|Collection<object>
* @var array<object>|Collection<object>
*/
private iterable|Collection $entities = [];
private array|Collection $entities = [];
/**
* @var string[]
*/
@@ -26,16 +26,16 @@ class MultiUpdateTableDTO
/**
* @return object[]
*/
public function getEntities(): iterable|Collection
public function getEntities(): array|Collection
{
return $this->entities;
}
/**
* @param iterable<object>|Collection<object> $entities
* @param array<object>|Collection<object> $entities
* @return MultiUpdateTableDTO
*/
public function setEntities(iterable|Collection $entities): MultiUpdateTableDTO
public function setEntities(array|Collection $entities): MultiUpdateTableDTO
{
$this->entities = $entities;

View File

@@ -195,7 +195,7 @@ final class TimesheetMultiUpdate extends AbstractType
// meta fields only if at least one exists
if ($entry !== null && $entry->getMetaFields()->count() > 0) {
$builder->add('metaFields', MetaFieldsCollectionType::class);
$builder->add('metaFields', MetaFieldsCollectionType::class, ['fields_required' => false]);
$choices = [];
foreach ($entry->getMetaFields() as $field) {

View File

@@ -1,45 +0,0 @@
<?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\Team;
use App\Form\Type\CustomerType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
final class TeamCustomerForm extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('customers', CustomerType::class, [
'multiple' => true,
'expanded' => true,
'by_reference' => false,
'query_builder_for_user' => false,
'required' => false,
])
;
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => Team::class,
'csrf_protection' => true,
'csrf_field_name' => '_token',
'csrf_token_id' => 'admin_team_customer',
'attr' => [
'data-form-event' => 'kimai.teamUpdate'
],
]);
}
}

View File

@@ -1,46 +0,0 @@
<?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\Team;
use App\Form\Type\ProjectType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
final class TeamProjectForm extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('projects', ProjectType::class, [
'multiple' => true,
'expanded' => false,
'by_reference' => false,
'attr' => ['size' => '20'],
'query_builder_for_user' => false,
'required' => false,
])
;
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => Team::class,
'csrf_protection' => true,
'csrf_field_name' => '_token',
'csrf_token_id' => 'admin_team_project',
'attr' => [
'data-form-event' => 'kimai.teamUpdate'
],
]);
}
}

View File

@@ -28,15 +28,15 @@ final class ExportToolbarForm extends AbstractType
$this->addDateRange($builder, ['timezone' => $options['timezone']]);
$this->addCustomerMultiChoice($builder, ['start_date_param' => null, 'end_date_param' => null, 'ignore_date' => true], true);
$this->addProjectMultiChoice($builder, ['ignore_date' => true], true, true);
$this->addActivityMultiChoice($builder, [], true);
$this->addActivitySelect($builder, [], true, true, false);
$this->addTagInputField($builder);
if ($options['include_user']) {
$this->addUsersChoice($builder);
$this->addTeamsChoice($builder);
}
$this->addExportStateChoice($builder);
$this->addTimesheetStateChoice($builder);
$this->addBillableChoice($builder);
$this->addExportStateChoice($builder);
$builder->add('renderer', HiddenType::class, []);
if ($options['include_export']) {
$builder->add('markAsExported', HiddenType::class, [

View File

@@ -25,21 +25,21 @@ final class InvoiceToolbarForm extends AbstractType
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$this->addTemplateChoice($builder);
$this->addDateRange($builder, ['timezone' => $options['timezone']]);
$this->addCustomerMultiChoice($builder, ['required' => false, 'start_date_param' => null, 'end_date_param' => null, 'ignore_date' => true, 'placeholder' => ''], true);
$this->addProjectMultiChoice($builder, ['ignore_date' => true], false, true);
$this->addSearchTermInputField($builder);
$this->addDateRange($builder, ['timezone' => $options['timezone']]);
$this->addCustomerMultiChoice($builder, ['start_date_param' => null, 'end_date_param' => null, 'ignore_date' => true], true);
$this->addProjectMultiChoice($builder, ['ignore_date' => true], true, true);
$this->addActivitySelect($builder, [], true, true, false);
$this->addTagInputField($builder);
if ($options['include_user']) {
$this->addUsersChoice($builder);
$this->addTeamsChoice($builder);
}
$this->addActivityMultiChoice($builder, $options, true);
$this->addTagInputField($builder);
$this->addExportStateChoice($builder);
$builder->add('invoiceDate', DatePickerType::class, [
'required' => true,
]);
$this->addTemplateChoice($builder);
}
protected function addTemplateChoice(FormBuilderInterface $builder): void

View File

@@ -45,14 +45,6 @@ use Symfony\Component\Form\FormEvents;
*/
trait ToolbarFormTrait
{
protected function addUserChoice(FormBuilderInterface $builder): void
{
$builder->add('user', UserType::class, [
'label' => 'user',
'required' => false,
]);
}
protected function addUsersChoice(FormBuilderInterface $builder, string $field = 'users', array $options = []): void
{
$builder->add($field, UserType::class, array_merge([
@@ -67,14 +59,6 @@ trait ToolbarFormTrait
], $options));
}
protected function addTeamChoice(FormBuilderInterface $builder): void
{
$builder->add('team', TeamType::class, [
'label' => 'team',
'required' => false,
]);
}
protected function addTeamsChoice(FormBuilderInterface $builder, string $field = 'teams', array $options = []): void
{
$builder->add($field, TeamType::class, array_merge([
@@ -89,11 +73,6 @@ trait ToolbarFormTrait
], $options));
}
protected function addCustomerChoice(FormBuilderInterface $builder, array $options = [], bool $multiProject = false): void
{
$this->addCustomerSelect($builder, $options, false, $multiProject);
}
protected function addCustomerMultiChoice(FormBuilderInterface $builder, array $options = [], bool $multiProject = false): void
{
$this->addCustomerSelect($builder, $options, true, $multiProject);
@@ -190,16 +169,6 @@ trait ToolbarFormTrait
$builder->add('daterange', DateRangeType::class, $params);
}
protected function addDateRangeChoice(FormBuilderInterface $builder, $allowEmpty = true, $required = false): void
{
$this->addDateRange($builder, [], $allowEmpty, $required);
}
protected function addProjectChoice(FormBuilderInterface $builder, array $options = [], bool $multiCustomer = false, bool $multiActivity = false): void
{
$this->addProjectSelect($builder, $options, false, $multiCustomer, $multiActivity);
}
protected function addProjectMultiChoice(FormBuilderInterface $builder, array $options = [], bool $multiCustomer = false, bool $multiActivity = false): void
{
$this->addProjectSelect($builder, $options, true, $multiCustomer, $multiActivity);
@@ -271,46 +240,45 @@ trait ToolbarFormTrait
);
}
protected function addActivityChoice(FormBuilderInterface $builder, array $options = [], bool $multiProject = false): void
{
$this->addActivitySelect($builder, $options, false, $multiProject);
}
protected function addActivityMultiChoice(FormBuilderInterface $builder, array $options = [], bool $multiProject = false): void
{
$this->addActivitySelect($builder, $options, true, $multiProject);
}
private function addActivitySelect(FormBuilderInterface $builder, array $options = [], bool $multiActivity = false, bool $multiProject = false): void
private function addActivitySelect(FormBuilderInterface $builder, array $options = [], bool $multiActivity = false, bool $multiProject = false, bool $autoFill = true): void
{
$name = 'activity';
if ($multiActivity) {
$name = 'activities';
}
$name = $multiActivity ? 'activities' : 'activity';
// just a fake field for having this field at the right position in the frontend
$builder->add($name, ActivityType::class, [
$activityOptions = [
'required' => false,
'documentation' => [
'type' => 'array',
'items' => ['type' => 'integer', 'description' => 'Activity ID'],
'description' => 'Array of activity IDs',
],
'choices' => [],
'multiple' => $multiActivity,
]);
];
if (!$autoFill) {
$activityOptions['attr'] = [
'data-autoselect' => 'false'
];
}
// just a fake field for having this field at the right position in the frontend
$builder->add($name, ActivityType::class, array_merge($activityOptions, [
'choices' => [],
]));
$builder->addEventListener(
FormEvents::PRE_SUBMIT,
function (FormEvent $event) use ($name, $multiActivity, $multiProject) {
function (FormEvent $event) use ($name, $multiProject, $activityOptions) {
/** @var array<string, mixed> $data */
$data = $event->getData();
$event->getForm()->add($name, ActivityType::class, [
'multiple' => $multiActivity,
'required' => false,
'query_builder' => function (ActivityRepository $repo) use ($data, $multiActivity, $multiProject) {
$event->getForm()->add($name, ActivityType::class, array_merge($activityOptions, [
'query_builder' => function (ActivityRepository $repo) use ($name, $data, $multiProject) {
$query = new ActivityFormTypeQuery();
$name = $multiActivity ? 'activities' : 'activity';
if (\array_key_exists($name, $data) && $data[$name] !== null && $data[$name] !== '') {
// we need to pre-fetch the activities to see if they are global, see ActivityFormTypeQuery::isGlobalsOnly()
$activities = \is_array($data[$name]) ? $data[$name] : [$data[$name]];
@@ -323,9 +291,9 @@ trait ToolbarFormTrait
}
}
$name = $multiProject ? 'projects' : 'project';
if (\array_key_exists($name, $data) && $data[$name] !== null && $data[$name] !== '') {
$projects = \is_array($data[$name]) ? $data[$name] : [$data[$name]];
$projectName = $multiProject ? 'projects' : 'project';
if (\array_key_exists($projectName, $data) && $data[$projectName] !== null && $data[$projectName] !== '') {
$projects = \is_array($data[$projectName]) ? $data[$projectName] : [$data[$projectName]];
foreach ($projects as $project) {
$project = \is_string($project) ? (int) $project : $project;
if (!\is_int($project) && !($project instanceof Project)) {
@@ -337,7 +305,7 @@ trait ToolbarFormTrait
return $repo->getQueryBuilderForFormType($query);
},
]);
]));
}
);
}

View File

@@ -62,6 +62,7 @@ final class DateRangeType extends AbstractType
$factory = DateTimeFactory::createByUser($user);
$view->vars['ranges'] = [
'today' => [$factory->createDateTime('00:00:00'), $factory->createDateTime('23:59:59')],
'yesterday' => [$factory->createDateTime('-1 day 00:00:00'), $factory->createDateTime('-1 day 23:59:59')],
'thisWeek' => [$factory->getStartOfWeek(), $factory->getEndOfWeek()],
'lastWeek' => [$factory->getStartOfWeek('-1 week'), $factory->getEndOfWeek('-1 week')],

View File

@@ -27,7 +27,7 @@ final class MetaFieldsCollectionType extends AbstractType
{
$builder->addEventListener(
FormEvents::PRE_SET_DATA,
function (FormEvent $event) {
function (FormEvent $event) use ($options) {
/** @var ArrayCollection<MetaTableTypeInterface> $collection */
$collection = $event->getData();
foreach ($collection as $collectionItem) {
@@ -42,6 +42,11 @@ final class MetaFieldsCollectionType extends AbstractType
continue;
}
if ($options['fields_required'] !== null) {
// TODO required select-fields can receive an empty value
$collectionItem->setIsRequired((bool) $options['fields_required']);
}
$collection->set($collectionItem->getName(), $collectionItem);
}
},
@@ -57,8 +62,11 @@ final class MetaFieldsCollectionType extends AbstractType
'entry_options' => ['label' => false],
'allow_add' => false,
'allow_delete' => false,
'fields_required' => null,
'label' => false,
]);
$resolver->setAllowedTypes('fields_required', ['null', 'bool']);
}
public function getParent(): string