Release 2.39 (#5604)

* prepare audit via annotation
* default calendar slot label distance of 1h
+ replace freestyle config with dropdown
* added missing return definition in callbacks
* refactor view name handling
* dispatch calendar view changes and push them into the URL to be able to reload the poage
* bump packages
* fix timezone issue in calendar sum calculation
* fixes #5618 resetRates()
* show expected daily hours in working-contract screen
This commit is contained in:
Kevin Papst
2025-08-30 11:41:17 +02:00
committed by GitHub
parent 4920ea5075
commit a4d658b821
85 changed files with 987 additions and 516 deletions

View File

@@ -35,7 +35,7 @@ final class TimesheetApiEditForm extends TimesheetEditForm
$builder->addEventListener(
FormEvents::PRE_SUBMIT,
function (FormEvent $event) {
function (FormEvent $event): void {
$data = $event->getData();
if (\array_key_exists('billable', $data)) {
$data['billableMode'] = Timesheet::BILLABLE_AUTOMATIC;

View File

@@ -27,7 +27,7 @@ trait ColorTrait
// this code exists only for backward compatibility
$builder->addEventListener(
FormEvents::PRE_SET_DATA,
function (FormEvent $event) use ($required) {
function (FormEvent $event) use ($required): void {
if (!$event->getForm()->getConfig()->hasOption('choices')) {
return;
}

View File

@@ -58,7 +58,7 @@ trait FormTrait
// replaces the project select after submission, to make sure only projects for the selected customer are displayed
$builder->addEventListener(
FormEvents::PRE_SUBMIT,
function (FormEvent $event) use ($builder, $project, $customer, $isNew, $options) {
function (FormEvent $event) use ($builder, $project, $customer, $isNew, $options): void {
/** @var array<string, mixed> $data */
$data = $event->getData();
$customer = \array_key_exists('customer', $data) && $data['customer'] !== '' ? $data['customer'] : null;
@@ -114,7 +114,7 @@ trait FormTrait
// replaces the activity select after submission, to make sure only activities for the selected project are displayed
$builder->addEventListener(
FormEvents::PRE_SUBMIT,
function (FormEvent $event) use ($options) {
function (FormEvent $event) use ($options): void {
/** @var array<string, mixed> $data */
$data = $event->getData();

View File

@@ -94,7 +94,7 @@ final class TimesheetMultiUpdate extends AbstractType
// TODO replace me with FormTrait
$builder->addEventListener(
FormEvents::PRE_SUBMIT,
function (FormEvent $event) use ($project, $customer) {
function (FormEvent $event) use ($project, $customer): void {
$data = $event->getData();
$customer = isset($data['customer']) && !empty($data['customer']) ? $data['customer'] : null;
$project = isset($data['project']) && !empty($data['project']) ? $data['project'] : $project;
@@ -126,7 +126,7 @@ final class TimesheetMultiUpdate extends AbstractType
// TODO replace me with FormTrait
$builder->addEventListener(
FormEvents::PRE_SUBMIT,
function (FormEvent $event) use ($activityOptions) {
function (FormEvent $event) use ($activityOptions): void {
$data = $event->getData();
if (!isset($data['project']) || empty($data['project'])) {
return;

View File

@@ -188,7 +188,7 @@ class TimesheetEditForm extends AbstractType
$builder->addEventListener(
FormEvents::POST_SET_DATA,
function (FormEvent $event) {
function (FormEvent $event): void {
/** @var Timesheet $timesheet */
$timesheet = $event->getData();
$begin = $timesheet->getBegin();
@@ -203,7 +203,7 @@ class TimesheetEditForm extends AbstractType
// map single fields to original datetime object
$builder->addEventListener(
FormEvents::SUBMIT,
function (FormEvent $event) {
function (FormEvent $event): void {
/** @var Timesheet $data */
$data = $event->getData();
@@ -243,7 +243,7 @@ class TimesheetEditForm extends AbstractType
$builder->addEventListener(
FormEvents::POST_SET_DATA,
function (FormEvent $event) {
function (FormEvent $event): void {
/** @var Timesheet|null $data */
$data = $event->getData();
if (null !== $data->getEnd()) {
@@ -255,7 +255,7 @@ class TimesheetEditForm extends AbstractType
// make sure that date & time fields are mapped back to begin & end fields
$builder->addEventListener(
FormEvents::SUBMIT,
function (FormEvent $event) {
function (FormEvent $event): void {
/** @var Timesheet $timesheet */
$timesheet = $event->getData();
$oldEnd = $timesheet->getEnd();
@@ -334,7 +334,7 @@ class TimesheetEditForm extends AbstractType
$builder->addEventListener(
FormEvents::POST_SET_DATA,
function (FormEvent $event) {
function (FormEvent $event): void {
/** @var Timesheet|null $timesheet */
$timesheet = $event->getData();
if (null === $timesheet || ($timesheet instanceof Timesheet && $timesheet->isRunning())) {
@@ -346,7 +346,7 @@ class TimesheetEditForm extends AbstractType
// make sure that duration is mapped back to end field
$builder->addEventListener(
FormEvents::SUBMIT,
function (FormEvent $event) use ($forceApply) {
function (FormEvent $event) use ($forceApply): void {
/** @var Timesheet $timesheet */
$timesheet = $event->getData();

View File

@@ -7,17 +7,24 @@
* file that was distributed with this source code.
*/
namespace App\Form;
namespace App\Form\Toolbar;
use App\Form\Type\CalendarViewType;
use App\Form\Type\DayPickerType;
use App\Form\Type\UserType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
final class CalendarForm extends AbstractType
final class CalendarToolbarForm extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder->add('date', DayPickerType::class, [
'model_timezone' => $options['timezone'],
'view_timezone' => $options['timezone'],
]);
$builder->add('view', CalendarViewType::class, []);
$builder->add('user', UserType::class, [
'required' => false,
'attr' => ['onchange' => 'this.form.submit()']
@@ -28,7 +35,9 @@ final class CalendarForm extends AbstractType
{
$resolver->setDefaults([
'csrf_protection' => false,
'timezone' => date_default_timezone_get(),
'method' => 'GET',
'change_user' => true,
]);
}
}

View File

@@ -98,7 +98,7 @@ trait ToolbarFormTrait
$builder->addEventListener(
FormEvents::PRE_SUBMIT,
function (FormEvent $event) use ($builder, $options, $name, $multiCustomer, $multiProject) {
function (FormEvent $event) use ($builder, $options, $name, $multiCustomer, $multiProject): void {
/** @var array<string, mixed> $data */
$data = $event->getData();
$event->getForm()->add($name, CustomerType::class, array_merge([
@@ -187,7 +187,7 @@ trait ToolbarFormTrait
$builder->addEventListener(
FormEvents::PRE_SUBMIT,
function (FormEvent $event) use ($builder, $options, $name, $multiCustomer, $multiProject, $multiActivity) {
function (FormEvent $event) use ($builder, $options, $name, $multiCustomer, $multiProject, $multiActivity): void {
/** @var array<string, mixed> $data */
$data = $event->getData();
$event->getForm()->add($name, ProjectType::class, array_merge([
@@ -266,7 +266,7 @@ trait ToolbarFormTrait
$builder->addEventListener(
FormEvents::PRE_SUBMIT,
function (FormEvent $event) use ($name, $multiProject, $activityOptions, $options) {
function (FormEvent $event) use ($name, $multiProject, $activityOptions, $options): void {
/** @var array<string, mixed> $data */
$data = $event->getData();
$event->getForm()->add($name, ActivityType::class, array_merge($activityOptions, [

View File

@@ -10,7 +10,9 @@
namespace App\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\CallbackTransformer;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
@@ -20,12 +22,28 @@ final class CalendarViewType extends AbstractType
{
public const DEFAULT_VIEW = 'month';
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder->addModelTransformer(new CallbackTransformer(
function ($transform) {
return match ($transform) {
'agendaDay', 'day' => 'day',
'agendaWeek', 'week' => 'week',
default => self::DEFAULT_VIEW,
};
},
function ($reverseTransform) {
return $reverseTransform;
}
));
}
public function configureOptions(OptionsResolver $resolver): void
{
$choices = [
'month' => 'month',
'agendaWeek' => 'agendaWeek',
'agendaDay' => 'agendaDay',
'agendaWeek' => 'week',
'agendaDay' => 'day',
];
$resolver->setDefaults([

View File

@@ -0,0 +1,43 @@
<?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\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\DateType;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* Form field type to enter a date in HTML5 format, mainly for GET forms.
*/
class DayPickerType extends AbstractType
{
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'label' => 'date',
'widget' => 'single_text',
'html5' => true,
'format' => DateType::HTML5_FORMAT,
'model_timezone' => date_default_timezone_get(),
'view_timezone' => date_default_timezone_get(),
'datepicker' => false,
]);
}
public function getParent(): string
{
return DateType::class;
}
public function getBlockPrefix(): string
{
return 'day';
}
}

View File

@@ -30,7 +30,7 @@ final class EntityMetaDefinitionType extends AbstractType
{
$builder->addEventListener(
FormEvents::PRE_SET_DATA,
function (FormEvent $event) {
function (FormEvent $event): void {
/** @var MetaTableTypeInterface $definition */
$definition = $event->getData();

View File

@@ -147,7 +147,7 @@ final class ExportColumnsType extends AbstractType
{
$builder->addEventListener(
FormEvents::PRE_SUBMIT,
function (FormEvent $event) {
function (FormEvent $event): void {
$data = $event->getData();
if (\is_array($data)) {
$this->ordered = $data; // @phpstan-ignore assign.propertyType
@@ -156,7 +156,7 @@ final class ExportColumnsType extends AbstractType
);
$builder->addEventListener(
FormEvents::SUBMIT,
function (FormEvent $event) {
function (FormEvent $event): void {
$event->setData($this->ordered);
}
);

View File

@@ -27,7 +27,7 @@ final class MetaFieldsCollectionType extends AbstractType
{
$builder->addEventListener(
FormEvents::PRE_SET_DATA,
function (FormEvent $event) use ($options) {
function (FormEvent $event) use ($options): void {
/** @var ArrayCollection<MetaTableTypeInterface> $collection */
$collection = $event->getData();
foreach ($collection as $collectionItem) {

View File

@@ -53,7 +53,7 @@ final class QuickEntryTimesheetType extends AbstractType
$builder->addEventListener(
FormEvents::POST_SET_DATA,
function (FormEvent $event) use ($durationOptions) {
function (FormEvent $event) use ($durationOptions): void {
/** @var Timesheet|null $data */
$data = $event->getData();
if (null === $data || $data->isRunning()) {
@@ -89,7 +89,7 @@ final class QuickEntryTimesheetType extends AbstractType
// make sure that duration is mapped back to end field
$builder->addEventListener(
FormEvents::SUBMIT,
function (FormEvent $event) {
function (FormEvent $event): void {
/** @var Timesheet $data */
$data = $event->getData();
$duration = $data->getDuration(false);

View File

@@ -39,7 +39,7 @@ final class QuickEntryWeekType extends AbstractType
$builder->add('project', ProjectType::class, $projectOptions);
$projectFunction = function (FormEvent $event) use ($projectOptions) {
$projectFunction = function (FormEvent $event) use ($projectOptions): void {
/** @var QuickEntryModel|null $data */
$data = $event->getData();
if ($data === null || $data->getProject() === null) {
@@ -62,7 +62,7 @@ final class QuickEntryWeekType extends AbstractType
$builder->add('activity', ActivityType::class, $activityOptions);
$activityFunction = function (FormEvent $event) use ($activityOptions) {
$activityFunction = function (FormEvent $event) use ($activityOptions): void {
/** @var QuickEntryModel|null $data */
$data = $event->getData();
if ($data === null || $data->getActivity() === null) {
@@ -77,7 +77,7 @@ final class QuickEntryWeekType extends AbstractType
$builder->addEventListener(FormEvents::PRE_SET_DATA, $activityFunction);
// make sure to pre-fill the form, so non-global activities can be loaded for the select project
$activityPreSubmitFunction = function (FormEvent $event) use ($activityOptions) {
$activityPreSubmitFunction = function (FormEvent $event) use ($activityOptions): void {
$data = $event->getData();
if (\is_array($data)) {
@@ -113,7 +113,7 @@ final class QuickEntryWeekType extends AbstractType
],
]);
$builder->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event) use ($options) {
$builder->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event) use ($options): void {
if ($event->getData() === null && $options['prototype_data'] instanceof QuickEntryModel) {
$event->setData(clone $options['prototype_data']);
}
@@ -151,7 +151,7 @@ final class QuickEntryWeekType extends AbstractType
// make sure that duration is mapped back to end field
$builder->addEventListener(
FormEvents::SUBMIT,
function (FormEvent $event) {
function (FormEvent $event): void {
/** @var QuickEntryModel $data */
$data = $event->getData();
$newRecords = $data->getNewTimesheet();

View File

@@ -31,7 +31,7 @@ final class SystemConfigurationType extends AbstractType
{
$builder->addEventListener(
FormEvents::PRE_SET_DATA,
function (FormEvent $event) {
function (FormEvent $event): void {
/** @var Configuration $preference */
$preference = $event->getData();

View File

@@ -35,7 +35,7 @@ final class TagsSelectType extends AbstractType
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder->addEventListener(FormEvents::PRE_SUBMIT, function (FormEvent $event) use ($options) {
$builder->addEventListener(FormEvents::PRE_SUBMIT, function (FormEvent $event) use ($options): void {
/** @var array<string> $tagIds */
$tagIds = $event->getData();

View File

@@ -37,7 +37,7 @@ final class UserPreferenceType extends AbstractType
{
$builder->addEventListener(
FormEvents::PRE_SET_DATA,
function (FormEvent $event) {
function (FormEvent $event): void {
/** @var UserPreference $preference */
$preference = $event->getData();

View File

@@ -29,7 +29,7 @@ final class UserPreferencesCollectionType extends AbstractType
{
$builder->addEventListener(
FormEvents::PRE_SET_DATA,
function (FormEvent $event) {
function (FormEvent $event): void {
/** @var ArrayCollection<UserPreference> $collection */
$collection = $event->getData();
foreach ($collection as $collectionItem) {