Release 2.34 (#5465)
* fix timing issue in timesheet edit form with deactivated rounding * bump packages * only show update messages for newer plugin versions * replace deprecated method * remove internal from API * remove technical terms from translation * prevent calls to internal symfony methods * helper method to flag entry as modified * support meta-fields in weekly-hourse view * fix running timesheets were deleted in weekly-hourse
This commit is contained in:
409
composer.lock
generated
409
composer.lock
generated
File diff suppressed because it is too large
Load Diff
55
phpstan.neon
55
phpstan.neon
@@ -1907,46 +1907,16 @@ parameters:
|
||||
count: 1
|
||||
path: src/Form/Extension/SelectWithApiDataExtension.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#1 \\$haystack of function stripos expects string, bool\\|float\\|int\\|string given\\.$#"
|
||||
count: 2
|
||||
path: src/Form/Helper/ActivityHelper.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#3 \\$subject of function str_replace expects array\\<string\\>\\|string, bool\\|float\\|int\\|string given\\.$#"
|
||||
count: 1
|
||||
path: src/Form/Helper/ActivityHelper.php
|
||||
|
||||
-
|
||||
message: "#^Property App\\\\Form\\\\Helper\\\\ActivityHelper\\:\\:\\$pattern \\(string\\|null\\) does not accept bool\\|float\\|int\\|string\\|null\\.$#"
|
||||
count: 1
|
||||
path: src/Form/Helper/ActivityHelper.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#1 \\$haystack of function stripos expects string, bool\\|float\\|int\\|string given\\.$#"
|
||||
count: 2
|
||||
path: src/Form/Helper/CustomerHelper.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#3 \\$subject of function str_replace expects array\\<string\\>\\|string, bool\\|float\\|int\\|string given\\.$#"
|
||||
count: 1
|
||||
path: src/Form/Helper/CustomerHelper.php
|
||||
|
||||
-
|
||||
message: "#^Property App\\\\Form\\\\Helper\\\\CustomerHelper\\:\\:\\$pattern \\(string\\|null\\) does not accept bool\\|float\\|int\\|string\\|null\\.$#"
|
||||
count: 1
|
||||
path: src/Form/Helper/CustomerHelper.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#1 \\$haystack of function stripos expects string, bool\\|float\\|int\\|string given\\.$#"
|
||||
count: 2
|
||||
path: src/Form/Helper/ProjectHelper.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#3 \\$subject of function str_replace expects array\\<string\\>\\|string, bool\\|float\\|int\\|string given\\.$#"
|
||||
count: 1
|
||||
path: src/Form/Helper/ProjectHelper.php
|
||||
|
||||
-
|
||||
message: "#^Property App\\\\Form\\\\Helper\\\\ProjectHelper\\:\\:\\$pattern \\(string\\|null\\) does not accept bool\\|float\\|int\\|string\\|null\\.$#"
|
||||
count: 1
|
||||
@@ -3262,11 +3232,6 @@ parameters:
|
||||
count: 1
|
||||
path: src/Ldap/LdapAuthenticator.php
|
||||
|
||||
-
|
||||
message: "#^Instanceof between Symfony\\\\Component\\\\Security\\\\Http\\\\Authenticator\\\\Passport\\\\Passport and Symfony\\\\Component\\\\Security\\\\Http\\\\Authenticator\\\\Passport\\\\Passport will always evaluate to true\\.$#"
|
||||
count: 1
|
||||
path: src/Ldap/LdapCredentialsSubscriber.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Ldap\\\\LdapCredentialsSubscriber\\:\\:onCheckPassport\\(\\) has no return type specified\\.$#"
|
||||
count: 1
|
||||
@@ -3912,26 +3877,6 @@ parameters:
|
||||
count: 1
|
||||
path: src/Timesheet/TimesheetStatisticService.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Timesheet\\\\TrackingMode\\\\AbstractTrackingMode\\:\\:setBeginEndFromRequest\\(\\) has no return type specified\\.$#"
|
||||
count: 1
|
||||
path: src/Timesheet/TrackingMode/AbstractTrackingMode.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Timesheet\\\\TrackingMode\\\\AbstractTrackingMode\\:\\:setFromToFromRequest\\(\\) has no return type specified\\.$#"
|
||||
count: 1
|
||||
path: src/Timesheet/TrackingMode/AbstractTrackingMode.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#1 \\$datetime of class DateTime constructor expects string, mixed given\\.$#"
|
||||
count: 2
|
||||
path: src/Timesheet/TrackingMode/AbstractTrackingMode.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#2 \\$datetime of static method DateTime\\:\\:createFromFormat\\(\\) expects string, mixed given\\.$#"
|
||||
count: 2
|
||||
path: src/Timesheet/TrackingMode/AbstractTrackingMode.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Twig\\\\Configuration\\:\\:__call\\(\\) has no return type specified\\.$#"
|
||||
count: 1
|
||||
|
||||
@@ -23,7 +23,11 @@ use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
final class ValidationFailedExceptionErrorHandler implements SubscribingHandlerInterface
|
||||
{
|
||||
public function __construct(private TranslatorInterface $translator, private FlattenExceptionHandler $exceptionHandler, private Security $security)
|
||||
public function __construct(
|
||||
private readonly TranslatorInterface $translator,
|
||||
private readonly FlattenExceptionHandler $exceptionHandler, // @phpstan-ignore-line
|
||||
private readonly Security $security
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -47,7 +51,7 @@ final class ValidationFailedExceptionErrorHandler implements SubscribingHandlerI
|
||||
public function serializeExceptionToJson(JsonSerializationVisitor $visitor, FlattenException $exception, array $type, Context $context)
|
||||
{
|
||||
if ($exception->getClass() !== ValidationFailedException::class) {
|
||||
return $this->exceptionHandler->serializeToJson($visitor, $exception, $type, $context);
|
||||
return $this->exceptionHandler->serializeToJson($visitor, $exception, $type, $context); // @phpstan-ignore method.internalClass
|
||||
}
|
||||
|
||||
$original = $context->getAttribute('exception');
|
||||
@@ -55,7 +59,7 @@ final class ValidationFailedExceptionErrorHandler implements SubscribingHandlerI
|
||||
return $this->serializeValidationExceptionToJson($visitor, $original, $type, $context);
|
||||
}
|
||||
|
||||
return $this->exceptionHandler->serializeToJson($visitor, $exception, $type, $context);
|
||||
return $this->exceptionHandler->serializeToJson($visitor, $exception, $type, $context); // @phpstan-ignore method.internalClass
|
||||
}
|
||||
|
||||
public function serializeValidationExceptionToJson(JsonSerializationVisitor $visitor, ValidationFailedException $exception, array $type, Context $context)
|
||||
|
||||
@@ -483,25 +483,17 @@ final class SystemConfiguration
|
||||
return (bool) $this->find('theme.avatar_url');
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal will be made private soon after 2.18.0 - do not access this method directly, but through getThemeColors()
|
||||
*/
|
||||
public function getThemeColorChoices(): string
|
||||
{
|
||||
$config = $this->find('theme.color_choices');
|
||||
if (\is_string($config) && $config !== '') {
|
||||
return $config;
|
||||
}
|
||||
|
||||
return 'Silver|#c0c0c0';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public function getThemeColors(): array
|
||||
{
|
||||
$config = explode(',', $this->getThemeColorChoices());
|
||||
$config = $this->find('theme.color_choices');
|
||||
if (!\is_string($config) || $config === '') {
|
||||
return ['Silver' => '#c0c0c0'];
|
||||
}
|
||||
|
||||
$config = explode(',', $config);
|
||||
|
||||
$colors = [];
|
||||
foreach ($config as $item) {
|
||||
|
||||
@@ -17,11 +17,11 @@ final class Constants
|
||||
/**
|
||||
* The current release version
|
||||
*/
|
||||
public const VERSION = '2.33.0';
|
||||
public const VERSION = '2.34.0';
|
||||
/**
|
||||
* The current release: major * 10000 + minor * 100 + patch
|
||||
*/
|
||||
public const VERSION_ID = 23300;
|
||||
public const VERSION_ID = 23400;
|
||||
/**
|
||||
* The software name
|
||||
*/
|
||||
|
||||
@@ -60,10 +60,10 @@ use Twig\Environment;
|
||||
final class InvoiceController extends AbstractController
|
||||
{
|
||||
public function __construct(
|
||||
private ServiceInvoice $service,
|
||||
private InvoiceTemplateRepository $templateRepository,
|
||||
private InvoiceRepository $invoiceRepository,
|
||||
private EventDispatcherInterface $dispatcher
|
||||
private readonly ServiceInvoice $service,
|
||||
private readonly InvoiceTemplateRepository $templateRepository,
|
||||
private readonly InvoiceRepository $invoiceRepository,
|
||||
private readonly EventDispatcherInterface $dispatcher
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -333,7 +333,7 @@ final class InvoiceController extends AbstractController
|
||||
{
|
||||
$invoice = null;
|
||||
|
||||
if (null !== ($id = $request->get('id'))) {
|
||||
if (null !== ($id = $request->query->get('id'))) {
|
||||
$invoice = $this->invoiceRepository->find($id);
|
||||
}
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ final class PluginController extends AbstractController
|
||||
$installed = [];
|
||||
$plugins = $manager->getPlugins();
|
||||
foreach ($plugins as $plugin) {
|
||||
$installed[] = $plugin->getId();
|
||||
$installed[$plugin->getId()] = $plugin;
|
||||
}
|
||||
|
||||
$page = new PageSetup('menu.plugin');
|
||||
@@ -36,18 +36,23 @@ final class PluginController extends AbstractController
|
||||
|
||||
$all = $this->getPluginInformation($client, $cache);
|
||||
$bundles = [];
|
||||
$updates = [];
|
||||
foreach ($all as $item) {
|
||||
if ($item['bundle'] !== null) {
|
||||
$bundles[$item['bundle']] = $item;
|
||||
if (\array_key_exists($item['bundle'], $installed)) {
|
||||
$updates[$item['bundle']] = version_compare($installed[$item['bundle']]->getMetadata()->getVersion(), $item['latest_release']) === -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $this->render('plugin/index.html.twig', [
|
||||
'page_setup' => $page,
|
||||
'plugins' => $plugins,
|
||||
'installed' => $installed,
|
||||
'installed' => array_keys($installed),
|
||||
'extensions' => $all,
|
||||
'bundles' => $bundles,
|
||||
'updates' => $updates,
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Configuration\SystemConfiguration;
|
||||
use App\Event\QuickEntryMetaDisplayEvent;
|
||||
use App\Form\QuickEntryForm;
|
||||
use App\Form\WeekByUserForm;
|
||||
use App\Model\QuickEntryWeek;
|
||||
@@ -19,6 +20,7 @@ use App\Repository\TimesheetRepository;
|
||||
use App\Timesheet\FavoriteRecordService;
|
||||
use App\Timesheet\TimesheetService;
|
||||
use App\Utils\PageSetup;
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
@@ -34,7 +36,8 @@ final class QuickEntryController extends AbstractController
|
||||
private readonly SystemConfiguration $configuration,
|
||||
private readonly TimesheetService $timesheetService,
|
||||
private readonly TimesheetRepository $repository,
|
||||
private readonly FavoriteRecordService $favoriteRecordService
|
||||
private readonly FavoriteRecordService $favoriteRecordService,
|
||||
private readonly EventDispatcherInterface $dispatcher,
|
||||
)
|
||||
{
|
||||
}
|
||||
@@ -149,6 +152,11 @@ final class QuickEntryController extends AbstractController
|
||||
$defaultHour = (int) $defaultBegin->format('H');
|
||||
$defaultMinute = (int) $defaultBegin->format('i');
|
||||
|
||||
// find additional meta-fields exclusively for QuickEntry view
|
||||
$event = new QuickEntryMetaDisplayEvent($query);
|
||||
$this->dispatcher->dispatch($event);
|
||||
$metaFields = $event->getFields();
|
||||
|
||||
$formModel = new QuickEntryWeek($startWeek);
|
||||
|
||||
foreach ($rows as $id => $row) {
|
||||
@@ -168,10 +176,12 @@ final class QuickEntryController extends AbstractController
|
||||
$model->addTimesheet($day['entry']);
|
||||
}
|
||||
}
|
||||
$model->setMetaFields($metaFields);
|
||||
}
|
||||
|
||||
// create prototype model
|
||||
$empty = $formModel->createRow($user);
|
||||
$empty->setMetaFields($metaFields);
|
||||
$empty->markAsPrototype();
|
||||
foreach ($week as $dayId => $day) {
|
||||
$tmp = $this->timesheetService->createNewTimesheet($user);
|
||||
@@ -219,7 +229,11 @@ final class QuickEntryController extends AbstractController
|
||||
foreach ($tmpModel->getTimesheets() as $timesheet) {
|
||||
if ($timesheet->getId() !== null) {
|
||||
$duration = $timesheet->getDuration(false);
|
||||
if ($duration === null || $timesheet->isRunning()) {
|
||||
// previously running timesheets were deleted, which was wrong
|
||||
// so now we distinguish between running timesheets and null duration
|
||||
if ($timesheet->isRunning()) {
|
||||
$saveTimesheets[] = $timesheet;
|
||||
} elseif ($duration === null) {
|
||||
$deleteTimesheets[] = $timesheet;
|
||||
} else {
|
||||
$saveTimesheets[] = $timesheet;
|
||||
@@ -263,6 +277,7 @@ final class QuickEntryController extends AbstractController
|
||||
'page_setup' => $page,
|
||||
'days' => $week,
|
||||
'form' => $form->createView(),
|
||||
'metaColumns' => $metaFields,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -147,9 +147,7 @@ abstract class TimesheetAbstractController extends AbstractController
|
||||
$event = new TimesheetMetaDefinitionEvent($entry);
|
||||
$this->dispatcher->dispatch($event);
|
||||
|
||||
$page = $request->get('page');
|
||||
$page = is_numeric($page) ? (int) $page : 1;
|
||||
$editForm = $this->getEditForm($entry, $page);
|
||||
$editForm = $this->getEditForm($entry);
|
||||
$editForm->handleRequest($request);
|
||||
|
||||
if ($editForm->isSubmitted() && $editForm->isValid()) {
|
||||
@@ -157,7 +155,7 @@ abstract class TimesheetAbstractController extends AbstractController
|
||||
$this->service->updateTimesheet($entry);
|
||||
$this->flashSuccess('action.update.success');
|
||||
|
||||
return $this->redirectToRoute($this->getTimesheetRoute(), ['page' => $request->get('page', 1)]);
|
||||
return $this->redirectToRoute($this->getTimesheetRoute());
|
||||
} catch (\Exception $ex) {
|
||||
$this->flashUpdateException($ex);
|
||||
}
|
||||
@@ -494,14 +492,13 @@ abstract class TimesheetAbstractController extends AbstractController
|
||||
]);
|
||||
}
|
||||
|
||||
private function getEditForm(Timesheet $entry, int $page): FormInterface
|
||||
private function getEditForm(Timesheet $entry): FormInterface
|
||||
{
|
||||
$mode = $this->getTrackingMode();
|
||||
|
||||
return $this->createForm($this->getEditFormClassName(), $entry, [
|
||||
'action' => $this->generateUrl($this->getEditRoute(), [
|
||||
'id' => $entry->getId(),
|
||||
'page' => $page,
|
||||
]),
|
||||
'include_rate' => $this->isGranted('edit_rate', $entry),
|
||||
'include_exported' => $this->isGranted('edit_export', $entry),
|
||||
|
||||
@@ -9,9 +9,6 @@
|
||||
|
||||
namespace App\Doctrine\Behavior;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
interface CreatedAt
|
||||
{
|
||||
public function getCreatedAt(): ?\DateTimeImmutable;
|
||||
|
||||
@@ -25,4 +25,9 @@ trait ModifiedTrait
|
||||
{
|
||||
$this->modifiedAt = $dateTime;
|
||||
}
|
||||
|
||||
public function markAsModified(): void
|
||||
{
|
||||
$this->setModifiedAt(new \DateTimeImmutable('now', new \DateTimeZone('UTC')));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -224,7 +224,7 @@ class Timesheet implements EntityWithMetaFields, ExportableItem, ModifiedAt
|
||||
{
|
||||
$this->tags = new ArrayCollection();
|
||||
$this->meta = new ArrayCollection();
|
||||
$this->setModifiedAt(new \DateTimeImmutable('now', new \DateTimeZone('UTC')));
|
||||
$this->markAsModified();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -611,7 +611,7 @@ class Timesheet implements EntityWithMetaFields, ExportableItem, ModifiedAt
|
||||
{
|
||||
// this needs to be done, otherwise doctrine will not see the item as changed
|
||||
// and the calculators will not run
|
||||
$this->setModifiedAt(new \DateTimeImmutable('now', new \DateTimeZone('UTC')));
|
||||
$this->markAsModified();
|
||||
|
||||
if (null === ($current = $this->getMetaField($meta->getName()))) {
|
||||
$meta->setEntity($this);
|
||||
@@ -659,7 +659,7 @@ class Timesheet implements EntityWithMetaFields, ExportableItem, ModifiedAt
|
||||
$this->id = null;
|
||||
}
|
||||
|
||||
$this->setModifiedAt(new \DateTimeImmutable('now', new \DateTimeZone('UTC')));
|
||||
$this->markAsModified();
|
||||
$this->exported = false;
|
||||
|
||||
$currentMeta = $this->meta;
|
||||
|
||||
25
src/Event/QuickEntryMetaDisplayEvent.php
Normal file
25
src/Event/QuickEntryMetaDisplayEvent.php
Normal file
@@ -0,0 +1,25 @@
|
||||
<?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\Event;
|
||||
|
||||
use App\Repository\Query\TimesheetQuery;
|
||||
|
||||
/**
|
||||
* Dynamically find possible meta fields for the quick-entry screen.
|
||||
*
|
||||
* @method TimesheetQuery getQuery()
|
||||
*/
|
||||
final class QuickEntryMetaDisplayEvent extends AbstractMetaDisplayEvent
|
||||
{
|
||||
public function __construct(TimesheetQuery $query)
|
||||
{
|
||||
parent::__construct($query, 'quick-entry');
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,7 @@ use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
*/
|
||||
final class MenuBuilderSubscriber implements EventSubscriberInterface
|
||||
{
|
||||
public function __construct(private MenuService $menuService)
|
||||
public function __construct(private readonly MenuService $menuService)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -53,7 +53,7 @@ final class MenuBuilderSubscriber implements EventSubscriberInterface
|
||||
$event->addItem($menuEvent->getSystemMenu());
|
||||
}
|
||||
|
||||
$route = $event->getRequest()->get('_route');
|
||||
$route = $event->getRequest()->attributes->get('_route');
|
||||
if (!\is_string($route)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -33,6 +33,18 @@ final class QuickEntryForm extends AbstractType
|
||||
$builder->addModelTransformer(new CallbackTransformer(
|
||||
function ($value) {
|
||||
// page is loaded, nothing to do
|
||||
if (!$value instanceof QuickEntryWeek) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
foreach ($value->getRows() as $row) {
|
||||
foreach ($row->getTimesheets() as $timesheet) {
|
||||
foreach ($timesheet->getMetaFields() as $metaField) {
|
||||
$row->setMetaFieldValue($metaField);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $value;
|
||||
},
|
||||
function (?QuickEntryWeek $value) {
|
||||
@@ -49,6 +61,24 @@ final class QuickEntryForm extends AbstractType
|
||||
foreach ($row->getTimesheets() as $timesheet) {
|
||||
$timesheet->setProject($project);
|
||||
$timesheet->setActivity($activity);
|
||||
foreach ($row->getMetaFields() as $metaField) {
|
||||
if (null === ($name = $metaField->getName())) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (null === $timesheet->getMetaField($name)) {
|
||||
$timesheet->setMetaField($metaField);
|
||||
}
|
||||
|
||||
if (null === ($tmpField = $timesheet->getMetaField($name))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($tmpField->getValue() !== $metaField->getValue()) {
|
||||
$tmpField->setValue($metaField->getValue());
|
||||
$timesheet->markAsModified();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -214,9 +214,9 @@ class TimesheetEditForm extends AbstractType
|
||||
return;
|
||||
}
|
||||
|
||||
// if the user did not change the time, make sure to keep the seconds
|
||||
$seconds = 0;
|
||||
if ($data->getBegin()?->format('H:i') === $time->format('H:i')) {
|
||||
// if the user did not change the time, make sure to keep the seconds (ONLY if the timesheet is already existing)
|
||||
if ($data->getBegin()?->format('H:i') === $time->format('H:i') && $data->getId() !== null) {
|
||||
$seconds = $data->getBegin()->format('s') ?? 0;
|
||||
}
|
||||
|
||||
@@ -276,9 +276,9 @@ class TimesheetEditForm extends AbstractType
|
||||
throw new \Exception('Cannot work with timesheets without start time');
|
||||
}
|
||||
|
||||
// if the user did not change the time, make sure to keep the seconds
|
||||
$seconds = 0;
|
||||
if ($oldEnd !== null && $oldEnd->format('H:i') === $end->format('H:i')) {
|
||||
// if the user did not change the time, make sure to keep the seconds (ONLY if the timesheet is already existing)
|
||||
if ($oldEnd !== null && $oldEnd->format('H:i') === $end->format('H:i') && $timesheet->getId() !== null) {
|
||||
$seconds = $oldEnd->format('s') ?? 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -92,6 +92,8 @@ final class QuickEntryWeekType extends AbstractType
|
||||
};
|
||||
$builder->addEventListener(FormEvents::PRE_SUBMIT, $activityPreSubmitFunction);
|
||||
|
||||
$builder->add('metaFields', MetaFieldsCollectionType::class);
|
||||
|
||||
$builder->add('timesheets', CollectionType::class, [
|
||||
'entry_type' => QuickEntryTimesheetType::class,
|
||||
'label' => false,
|
||||
|
||||
@@ -13,12 +13,11 @@ use App\Entity\User;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
use Symfony\Component\Security\Core\Exception\BadCredentialsException;
|
||||
use Symfony\Component\Security\Http\Authenticator\Passport\Credentials\PasswordCredentials;
|
||||
use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
|
||||
use Symfony\Component\Security\Http\Event\CheckPassportEvent;
|
||||
|
||||
final class LdapCredentialsSubscriber implements EventSubscriberInterface
|
||||
{
|
||||
public function __construct(private LdapManager $ldapManager)
|
||||
public function __construct(private readonly LdapManager $ldapManager)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -40,7 +39,7 @@ final class LdapCredentialsSubscriber implements EventSubscriberInterface
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$passport instanceof Passport || !$passport->hasBadge(PasswordCredentials::class)) {
|
||||
if (!$passport->hasBadge(PasswordCredentials::class)) {
|
||||
throw new \LogicException(\sprintf('LDAP authentication requires a passport containing a user and password credentials, authenticator "%s" does not fulfill these requirements.', \get_class($event->getAuthenticator())));
|
||||
}
|
||||
|
||||
@@ -78,6 +77,6 @@ final class LdapCredentialsSubscriber implements EventSubscriberInterface
|
||||
}
|
||||
|
||||
// make sure that the normal auth process is not triggered
|
||||
$passwordCredentials->markResolved();
|
||||
$passwordCredentials->markResolved(); // @phpstan-ignore method.internal
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,9 +10,12 @@
|
||||
namespace App\Model;
|
||||
|
||||
use App\Entity\Activity;
|
||||
use App\Entity\MetaTableTypeInterface;
|
||||
use App\Entity\Project;
|
||||
use App\Entity\Timesheet;
|
||||
use App\Entity\User;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
@@ -24,9 +27,18 @@ class QuickEntryModel
|
||||
* @var Timesheet[]
|
||||
*/
|
||||
private array $timesheets = [];
|
||||
/**
|
||||
* @var Collection<int, MetaTableTypeInterface>
|
||||
*/
|
||||
private Collection $metaFields;
|
||||
|
||||
public function __construct(private User $user, private ?Project $project = null, private ?Activity $activity = null)
|
||||
public function __construct(
|
||||
private readonly User $user,
|
||||
private ?Project $project = null,
|
||||
private ?Activity $activity = null
|
||||
)
|
||||
{
|
||||
$this->metaFields = new ArrayCollection();
|
||||
}
|
||||
|
||||
public function markAsPrototype(): void
|
||||
@@ -184,4 +196,36 @@ class QuickEntryModel
|
||||
$this->timesheets[] = clone $record;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<int, MetaTableTypeInterface>
|
||||
*/
|
||||
public function getMetaFields(): Collection
|
||||
{
|
||||
return $this->metaFields;
|
||||
}
|
||||
|
||||
public function setMetaFieldValue(MetaTableTypeInterface $metaField): void
|
||||
{
|
||||
// we only update the value for fields which were previously registered
|
||||
// as we do NOT want new fields to show up by accident
|
||||
foreach ($this->metaFields as $field) {
|
||||
if ($field->getName() === $metaField->getName() && $metaField->getValue() !== null) {
|
||||
$field->setValue($metaField->getValue());
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<MetaTableTypeInterface> $metaFields
|
||||
*/
|
||||
public function setMetaFields(array $metaFields): void
|
||||
{
|
||||
$collection = new ArrayCollection();
|
||||
foreach($metaFields as $field) {
|
||||
$collection->add(clone $field);
|
||||
}
|
||||
$this->metaFields = $collection;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ class QuickEntryWeek
|
||||
*/
|
||||
private array $rows = [];
|
||||
|
||||
public function __construct(private \DateTime $startDate)
|
||||
public function __construct(private readonly \DateTime $startDate)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -79,7 +79,7 @@ class QuickEntryWeek
|
||||
$result = strcmp((string) $aName, (string) $bName);
|
||||
}
|
||||
|
||||
return $result < 0 ? -1 : 1;
|
||||
return $result < 0 ? -1 : 1;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -9,9 +9,6 @@
|
||||
|
||||
namespace App\Model;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class TimesheetCountedStatistic implements \JsonSerializable
|
||||
{
|
||||
private int $counter = 0;
|
||||
@@ -34,8 +31,6 @@ class TimesheetCountedStatistic implements \JsonSerializable
|
||||
|
||||
/**
|
||||
* For unified access, used in frontend.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getCounter(): int
|
||||
{
|
||||
@@ -84,8 +79,6 @@ class TimesheetCountedStatistic implements \JsonSerializable
|
||||
|
||||
/**
|
||||
* For unified access, used in frontend.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getValue(): int
|
||||
{
|
||||
@@ -104,8 +97,6 @@ class TimesheetCountedStatistic implements \JsonSerializable
|
||||
|
||||
/**
|
||||
* For unified access, used in frontend.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getDuration(): int
|
||||
{
|
||||
@@ -114,8 +105,6 @@ class TimesheetCountedStatistic implements \JsonSerializable
|
||||
|
||||
/**
|
||||
* For unified access, used in frontend.
|
||||
*
|
||||
* @return float
|
||||
*/
|
||||
public function getRate(): float
|
||||
{
|
||||
@@ -134,8 +123,6 @@ class TimesheetCountedStatistic implements \JsonSerializable
|
||||
|
||||
/**
|
||||
* Returns the total internal rate of all included timesheet records.
|
||||
*
|
||||
* @return float
|
||||
*/
|
||||
public function getInternalRate(): float
|
||||
{
|
||||
|
||||
@@ -324,7 +324,6 @@ class BaseQuery
|
||||
* @template T of BaseQuery
|
||||
* @param T $query
|
||||
* @return T
|
||||
* @internal
|
||||
*/
|
||||
final public function copyTo(BaseQuery $query): BaseQuery
|
||||
{
|
||||
|
||||
@@ -24,7 +24,7 @@ final class SamlAuthenticationSuccessHandler extends DefaultAuthenticationSucces
|
||||
|
||||
protected function determineTargetUrl(Request $request): string
|
||||
{
|
||||
$relayState = $request->get('RelayState');
|
||||
$relayState = $request->request->get('RelayState', $request->query->get('RelayState'));
|
||||
if (\is_scalar($relayState)) {
|
||||
$relayState = (string) $relayState;
|
||||
if ($relayState !== $this->httpUtils->generateUri($request, (string) $this->options['login_path'])) {
|
||||
|
||||
@@ -27,9 +27,9 @@ abstract class AbstractTrackingMode implements TrackingModeInterface
|
||||
$this->setFromToFromRequest($timesheet, $request);
|
||||
}
|
||||
|
||||
protected function setBeginEndFromRequest(Timesheet $entry, Request $request)
|
||||
protected function setBeginEndFromRequest(Timesheet $entry, Request $request): void
|
||||
{
|
||||
$start = $request->get('begin');
|
||||
$start = $request->query->get('begin');
|
||||
if (null === $start) {
|
||||
return;
|
||||
}
|
||||
@@ -42,7 +42,7 @@ abstract class AbstractTrackingMode implements TrackingModeInterface
|
||||
$entry->setBegin($start);
|
||||
|
||||
// only check for an end date if a begin date was given
|
||||
$end = $request->get('end');
|
||||
$end = $request->query->get('end');
|
||||
if (null === $end) {
|
||||
return;
|
||||
}
|
||||
@@ -59,33 +59,33 @@ abstract class AbstractTrackingMode implements TrackingModeInterface
|
||||
$entry->setDuration($end->getTimestamp() - $start->getTimestamp());
|
||||
}
|
||||
|
||||
protected function setFromToFromRequest(Timesheet $entry, Request $request)
|
||||
protected function setFromToFromRequest(Timesheet $entry, Request $request): void
|
||||
{
|
||||
$from = $request->get('from');
|
||||
if (null === $from) {
|
||||
$from = $request->query->get('from');
|
||||
if (!\is_string($from)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$from = new DateTime($from, $this->getTimezone($entry));
|
||||
$entry->setBegin($from);
|
||||
} catch (\Exception $ex) {
|
||||
return;
|
||||
}
|
||||
|
||||
$entry->setBegin($from);
|
||||
|
||||
$to = $request->get('to');
|
||||
if (null === $to) {
|
||||
// only check for an end date if a valid begin date was given
|
||||
$to = $request->query->get('to');
|
||||
if (!\is_string($to)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$to = new DateTime($to, $this->getTimezone($entry));
|
||||
$entry->setEnd($to);
|
||||
} catch (\Exception $ex) {
|
||||
return;
|
||||
}
|
||||
|
||||
$entry->setEnd($to);
|
||||
$entry->setDuration($to->getTimestamp() - $from->getTimestamp());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,12 +63,12 @@ final class ThemeExtension implements RuntimeExtensionInterface
|
||||
|
||||
public function getJavascriptTranslations(): array
|
||||
{
|
||||
$event = new ThemeJavascriptTranslationsEvent(); // @phpstan-ignore new.deprecated
|
||||
$event = new ThemeJavascriptTranslationsEvent(); // @phpstan-ignore new.deprecatedClass
|
||||
|
||||
$this->eventDispatcher->dispatch($event);
|
||||
|
||||
$all = [];
|
||||
foreach ($event->getTranslations() as $key => $translation) {
|
||||
foreach ($event->getTranslations() as $key => $translation) { // @phpstan-ignore method.deprecatedClass
|
||||
$all[$key] = $this->translator->trans($translation[0], [], $translation[1]);
|
||||
}
|
||||
|
||||
|
||||
@@ -34,11 +34,11 @@ final class ForbiddenPolicy implements SecurityPolicyInterface
|
||||
* @param array<string> $forbiddenFunctions
|
||||
*/
|
||||
public function __construct(
|
||||
private array $forbiddenTags = [],
|
||||
private array $forbiddenFilters = [],
|
||||
private readonly array $forbiddenTags = [],
|
||||
private readonly array $forbiddenFilters = [],
|
||||
array $forbiddenMethods = [],
|
||||
private array $forbiddenProperties = [],
|
||||
private array $forbiddenFunctions = []
|
||||
private readonly array $forbiddenProperties = [],
|
||||
private readonly array $forbiddenFunctions = []
|
||||
)
|
||||
{
|
||||
$this->forbiddenMethods = [];
|
||||
@@ -70,7 +70,7 @@ final class ForbiddenPolicy implements SecurityPolicyInterface
|
||||
|
||||
public function checkMethodAllowed($obj, $method): void
|
||||
{
|
||||
if ($obj instanceof Template || $obj instanceof Markup) {
|
||||
if ($obj instanceof Template || $obj instanceof Markup) { // @phpstan-ignore instanceof.internalClass
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -204,7 +204,7 @@ final class InvoicePolicy implements SecurityPolicyInterface
|
||||
|
||||
public function checkMethodAllowed($obj, $method): void
|
||||
{
|
||||
if ($obj instanceof Template || $obj instanceof Markup || $obj instanceof UnicodeString) {
|
||||
if ($obj instanceof Template || $obj instanceof Markup || $obj instanceof UnicodeString) { // @phpstan-ignore instanceof.internalClass
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -27,9 +27,6 @@ use App\WorkingTime\Model\Year;
|
||||
use App\WorkingTime\Model\YearPerUserSummary;
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
|
||||
/**
|
||||
* @internal this API and the entire namespace is experimental: expect changes!
|
||||
*/
|
||||
final class WorkingTimeService
|
||||
{
|
||||
private const LATEST_APPROVAL_PREF = '_latest_approval';
|
||||
|
||||
@@ -35,6 +35,10 @@
|
||||
{{ form_row(form.duration, {row_attr: {class: 'p-0'}}) }}
|
||||
{%- endblock %}
|
||||
|
||||
{% block _quick_entry_form_rows_entry_metaFields_entry_widget %}
|
||||
{{ form_row(form, {row_attr: {class: 'p-0'}}) }}
|
||||
{%- endblock %}
|
||||
|
||||
{% block quick_entry_week_row %}
|
||||
<tr{% with {attr: row_attr|merge({class: (row_attr.class|default('') ~ ' form-group qe-entry-week-row' ~ (not valid ? ' is-invalid'))|trim})} %}{{ block('attributes') }}{% endwith %}>
|
||||
<td>
|
||||
@@ -43,6 +47,11 @@
|
||||
<td>
|
||||
{{ form_row(form.activity, {row_attr: {class: 'p-0'}}) }}
|
||||
</td>
|
||||
{% for meta in form.metaFields|sort((a, b) => a.vars.data.order <=> b.vars.data.order) %}
|
||||
<td>
|
||||
{{ form_row(meta.value, {label: false}) }}
|
||||
</td>
|
||||
{% endfor %}
|
||||
{% for timesheet in form.timesheets %}
|
||||
<td class="text-center{% if timesheet.vars.data.begin is weekend %} weekend{% endif %}{% if timesheet.vars.data.begin is today %} today{% endif %}">
|
||||
{{ form_widget(timesheet) }}
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
</td>
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'description') }}">
|
||||
{{ plugin.metadata.description }}
|
||||
{% if bundles[plugin.id] is defined and bundles[plugin.id]['latest_release'] is defined and bundles[plugin.id]['latest_release'] != plugin.metadata.version %}
|
||||
{% if updates[plugin.id] is defined and updates[plugin.id] == true %}
|
||||
{{ widgets.alert('warning', 'update_available'|trans({'%version%': bundles[plugin.id]['latest_release']}, 'plugins')) }}
|
||||
{% endif %}
|
||||
</td>
|
||||
|
||||
@@ -28,6 +28,9 @@
|
||||
<tr>
|
||||
<th>{{ 'project'|trans }}</th>
|
||||
<th>{{ 'activity'|trans }}</th>
|
||||
{% for field in metaColumns|sort((a, b) => a.order <=> b.order) %}
|
||||
<th style="min-width: 70px">{{ field.label }}</th>
|
||||
{% endfor %}
|
||||
{% for id, week in days %}
|
||||
<th class="text-center{% if week.day is weekend %} weekend{% endif %}{% if week.day is today %} today{% endif %}">
|
||||
{{ week.day|date_weekday }}
|
||||
@@ -43,6 +46,9 @@
|
||||
<tr class="summary">
|
||||
<td>{{ 'stats.durationTotal'|trans }}</td>
|
||||
<td></td>
|
||||
{% for field in metaColumns %}
|
||||
<td></td>
|
||||
{% endfor %}
|
||||
{% for id, week in days %}
|
||||
<td class="text-center" id="qe-totals-day-{{ loop.index0 }}"></td>
|
||||
{% endfor %}
|
||||
|
||||
@@ -136,7 +136,6 @@ class SystemConfigurationTest extends TestCase
|
||||
self::assertEquals('GBP', $sut->find('defaults.customer.currency'));
|
||||
self::assertFalse($sut->find('timesheet.rules.allow_future_times'));
|
||||
self::assertEquals(99, $sut->find('timesheet.active_entries.hard_limit'));
|
||||
self::assertEquals('Maroon|#800000,Brown|#a52a2a,Red|#ff0000,Orange|#ffa500,#ffffff,,|#000000', $sut->getThemeColorChoices());
|
||||
self::assertEquals(['Maroon' => '#800000', 'Brown' => '#a52a2a', 'Red' => '#ff0000', 'Orange' => '#ffa500', '#ffffff' => '#ffffff', '#000000' => '#000000'], $sut->getThemeColors());
|
||||
}
|
||||
|
||||
@@ -160,7 +159,6 @@ class SystemConfigurationTest extends TestCase
|
||||
]);
|
||||
self::assertFalse($sut->find('timesheet.rules.allow_future_times'));
|
||||
self::assertTrue($sut->isSamlActive());
|
||||
self::assertEquals('Silver|#c0c0c0', $sut->getThemeColorChoices());
|
||||
self::assertEquals(['Silver' => '#c0c0c0'], $sut->getThemeColors());
|
||||
self::assertEquals('2020-03-27', $sut->getFinancialYearStart());
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* @covers \App\Event\ThemeJavascriptTranslationsEvent
|
||||
* @group legacy
|
||||
*/
|
||||
class ThemeJavascriptTranslationsEventTest extends TestCase
|
||||
{
|
||||
@@ -21,18 +22,18 @@ class ThemeJavascriptTranslationsEventTest extends TestCase
|
||||
|
||||
public function testDefaultValues(): void
|
||||
{
|
||||
$sut = new ThemeJavascriptTranslationsEvent(); // @phpstan-ignore new.deprecated
|
||||
$sut = new ThemeJavascriptTranslationsEvent(); // @phpstan-ignore new.deprecatedClass
|
||||
|
||||
self::assertCount(self::COUNTER, $sut->getTranslations());
|
||||
self::assertCount(self::COUNTER, $sut->getTranslations()); // @phpstan-ignore method.deprecatedClass
|
||||
}
|
||||
|
||||
public function testGetterAndSetter(): void
|
||||
{
|
||||
$sut = new ThemeJavascriptTranslationsEvent(); // @phpstan-ignore new.deprecated
|
||||
$sut->setTranslation('foo', 'bar');
|
||||
$sut->setTranslation('hello', 'world', 'testing');
|
||||
$sut = new ThemeJavascriptTranslationsEvent(); // @phpstan-ignore new.deprecatedClass
|
||||
$sut->setTranslation('foo', 'bar'); // @phpstan-ignore method.deprecatedClass
|
||||
$sut->setTranslation('hello', 'world', 'testing'); // @phpstan-ignore method.deprecatedClass
|
||||
|
||||
$result = $sut->getTranslations();
|
||||
$result = $sut->getTranslations(); // @phpstan-ignore method.deprecatedClass
|
||||
self::assertCount(self::COUNTER + 2, $result);
|
||||
self::assertArrayHasKey('foo', $result);
|
||||
self::assertEquals(['bar', 'messages'], $result['foo']);
|
||||
|
||||
@@ -180,19 +180,19 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="jmsE4EY" resname="Ceil">
|
||||
<source>Ceil</source>
|
||||
<target>Ceil: Start, Ende und Dauer werden nach oben gerundet</target>
|
||||
<target>Start, Ende und Dauer werden nach oben gerundet</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Y_qmsDz" resname="Closest">
|
||||
<source>Closest</source>
|
||||
<target>Closest: Mathematische Rundung zum nächsten Wert</target>
|
||||
<target>Mathematische Rundung zum nächsten Wert</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="IbERy.5" resname="Default">
|
||||
<source>Default</source>
|
||||
<target>Standard: Start wird nach unten, Ende und Dauer nach oben gerundet</target>
|
||||
<target>Start wird nach unten, Ende und Dauer nach oben gerundet</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="0mjJxEk" resname="Floor">
|
||||
<source>Floor</source>
|
||||
<target>Floor: Start, Ende und Dauer werden nach unten gerundet</target>
|
||||
<target>Start, Ende und Dauer werden nach unten gerundet</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="_0OBfZf" resname="timesheet.default_begin" xml:space="preserve">
|
||||
<source>timesheet.default_begin</source>
|
||||
|
||||
@@ -180,19 +180,19 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="jmsE4EY" resname="Ceil">
|
||||
<source>Ceil</source>
|
||||
<target>Ceil: begin, end and duration will be rounded up</target>
|
||||
<target>Begin, end and duration will be rounded up</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Y_qmsDz" resname="Closest">
|
||||
<source>Closest</source>
|
||||
<target>Closest: mathematical rounding to the nearest value</target>
|
||||
<target>Mathematical rounding to the nearest value</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="IbERy.5" resname="Default">
|
||||
<source>Default</source>
|
||||
<target>Standard: begin will be rounded down, end and duration up</target>
|
||||
<target>Begin will be rounded down, end and duration up</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="0mjJxEk" resname="Floor">
|
||||
<source>Floor</source>
|
||||
<target>Floor: begin, end and duration will be rounded down</target>
|
||||
<target>Begin, end and duration will be rounded down</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="_0OBfZf" resname="timesheet.default_begin">
|
||||
<source>timesheet.default_begin</source>
|
||||
|
||||
Reference in New Issue
Block a user