limit amount of items in calendar drag and drop boxes (#2291)

This commit is contained in:
Kevin Papst
2021-07-11 15:03:18 +02:00
committed by GitHub
parent 2583fdf20e
commit a1f90ffaba
13 changed files with 178 additions and 82 deletions

View File

@@ -0,0 +1,102 @@
<?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\Calendar;
use App\Configuration\SystemConfiguration;
use App\Entity\User;
use App\Event\CalendarDragAndDropSourceEvent;
use App\Event\CalendarGoogleSourceEvent;
use App\Event\RecentActivityEvent;
use App\Repository\TimesheetRepository;
use App\Timesheet\DateTimeFactory;
use App\Utils\Color;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
final class CalendarService
{
/**
* @var SystemConfiguration
*/
private $configuration;
/**
* @var TimesheetRepository
*/
private $repository;
/**
* @var EventDispatcherInterface
*/
private $dispatcher;
public function __construct(SystemConfiguration $configuration, TimesheetRepository $repository, EventDispatcherInterface $dispatcher)
{
$this->configuration = $configuration;
$this->repository = $repository;
$this->dispatcher = $dispatcher;
}
/**
* @param User $user
* @return DragAndDropSource[]
* @throws \Exception
*/
public function getDragAndDropResources(User $user): array
{
$maxAmount = $this->configuration->getCalendarDragAndDropMaxEntries();
$event = new CalendarDragAndDropSourceEvent($user, $maxAmount);
if ($maxAmount < 1) {
return [];
}
$data = $this->repository->getRecentActivities(
$user,
DateTimeFactory::createByUser($user)->createDateTime('-1 year'),
$maxAmount
);
$recentActivity = new RecentActivityEvent($user, $data);
$this->dispatcher->dispatch($recentActivity);
$entries = [];
$colorHelper = new Color();
foreach ($recentActivity->getRecentActivities() as $timesheet) {
$entries[] = new TimesheetEntry($timesheet, $colorHelper->getTimesheetColor($timesheet));
}
$event->addSource(new RecentActivitiesSource($entries));
$this->dispatcher->dispatch($event);
return $event->getSources();
}
public function getGoogleSources(User $user): ?Google
{
$apiKey = $this->configuration->getCalendarGoogleApiKey();
if ($apiKey === null) {
return null;
}
$sources = [];
foreach ($this->configuration->getCalendarGoogleSources() as $name => $config) {
$sources[] = new GoogleSource($name, $config['id'], $config['color']);
}
$event = new CalendarGoogleSourceEvent($user);
$this->dispatcher->dispatch($event);
foreach ($event->getSources() as $source) {
$sources[] = $source;
}
return new Google($apiKey, $sources);
}
}

View File

@@ -170,6 +170,11 @@ class SystemConfiguration implements SystemBundleConfiguration
return (string) $this->find('calendar.slot_duration'); return (string) $this->find('calendar.slot_duration');
} }
public function getCalendarDragAndDropMaxEntries(): int
{
return (int) $this->find('calendar.dragdrop_amount');
}
// ========== Customer configurations ========== // ========== Customer configurations ==========
public function getCustomerDefaultTimezone(): ?string public function getCustomerDefaultTimezone(): ?string

View File

@@ -9,21 +9,11 @@
namespace App\Controller; namespace App\Controller;
use App\Calendar\DragAndDropSource; use App\Calendar\CalendarService;
use App\Calendar\Google;
use App\Calendar\GoogleSource;
use App\Calendar\RecentActivitiesSource;
use App\Calendar\TimesheetEntry;
use App\Configuration\SystemConfiguration; use App\Configuration\SystemConfiguration;
use App\Event\CalendarDragAndDropSourceEvent;
use App\Event\CalendarGoogleSourceEvent;
use App\Event\RecentActivityEvent;
use App\Repository\TimesheetRepository;
use App\Timesheet\TrackingModeService; use App\Timesheet\TrackingModeService;
use App\Utils\Color;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security; use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\Routing\Annotation\Route;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
/** /**
* Controller used to display calendars. * Controller used to display calendars.
@@ -33,20 +23,17 @@ use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
*/ */
class CalendarController extends AbstractController class CalendarController extends AbstractController
{ {
/** private $calendarService;
* @var EventDispatcherInterface
*/
private $dispatcher;
public function __construct(EventDispatcherInterface $dispatcher) public function __construct(CalendarService $calendarService)
{ {
$this->dispatcher = $dispatcher; $this->calendarService = $calendarService;
} }
/** /**
* @Route(path="/", name="calendar", methods={"GET"}) * @Route(path="/", name="calendar", methods={"GET"})
*/ */
public function userCalendar(SystemConfiguration $configuration, TrackingModeService $service, TimesheetRepository $repository) public function userCalendar(SystemConfiguration $configuration, TrackingModeService $service)
{ {
$mode = $service->getActiveMode(); $mode = $service->getActiveMode();
$factory = $this->getDateTimeFactory(); $factory = $this->getDateTimeFactory();
@@ -62,19 +49,24 @@ class CalendarController extends AbstractController
'slotDuration' => $configuration->getCalendarSlotDuration(), 'slotDuration' => $configuration->getCalendarSlotDuration(),
'timeframeBegin' => $configuration->getCalendarTimeframeBegin(), 'timeframeBegin' => $configuration->getCalendarTimeframeBegin(),
'timeframeEnd' => $configuration->getCalendarTimeframeEnd(), 'timeframeEnd' => $configuration->getCalendarTimeframeEnd(),
'dragDropAmount' => $configuration->getCalendarDragAndDropMaxEntries(),
]; ];
$isPunchMode = !$mode->canEditDuration() && !$mode->canEditBegin() && !$mode->canEditEnd(); $isPunchMode = !$mode->canEditDuration() && !$mode->canEditBegin() && !$mode->canEditEnd();
$dragAndDrop = []; $dragAndDrop = [];
if ($mode->canEditBegin()) { if ($mode->canEditBegin()) {
$dragAndDrop = $this->getDragAndDropResources($repository); try {
$dragAndDrop = $this->calendarService->getDragAndDropResources($this->getUser());
} catch (\Exception $ex) {
$this->logException($ex);
}
} }
return $this->render('calendar/user.html.twig', [ return $this->render('calendar/user.html.twig', [
'config' => $config, 'config' => $config,
'dragAndDrop' => $dragAndDrop, 'dragAndDrop' => $dragAndDrop,
'google' => $this->getGoogleSources($configuration), 'google' => $this->calendarService->getGoogleSources($this->getUser()),
'now' => $factory->createDateTime(), 'now' => $factory->createDateTime(),
'defaultStartTime' => $defaultStart->format('h:i:s'), 'defaultStartTime' => $defaultStart->format('h:i:s'),
'is_punch_mode' => $isPunchMode, 'is_punch_mode' => $isPunchMode,
@@ -83,60 +75,4 @@ class CalendarController extends AbstractController
'can_edit_duration' => $mode->canEditDuration(), 'can_edit_duration' => $mode->canEditDuration(),
]); ]);
} }
/**
* @return DragAndDropSource[]
*/
private function getDragAndDropResources(TimesheetRepository $repository): array
{
$event = new CalendarDragAndDropSourceEvent($this->getUser());
try {
$data = $repository->getRecentActivities(
$this->getUser(),
$this->getDateTimeFactory()->createDateTime('-1 year'),
10
);
$recentActivity = new RecentActivityEvent($this->getUser(), $data);
$this->dispatcher->dispatch($recentActivity);
$entries = [];
$colorHelper = new Color();
foreach ($recentActivity->getRecentActivities() as $timesheet) {
$entries[] = new TimesheetEntry($timesheet, $colorHelper->getTimesheetColor($timesheet));
}
$event->addSource(new RecentActivitiesSource($entries));
} catch (\Exception $ex) {
$this->logException($ex);
}
$this->dispatcher->dispatch($event);
return $event->getSources();
}
private function getGoogleSources(SystemConfiguration $configuration): ?Google
{
$apiKey = $configuration->getCalendarGoogleApiKey();
if ($apiKey === null) {
return null;
}
$sources = [];
foreach ($configuration->getCalendarGoogleSources() as $name => $config) {
$sources[] = new GoogleSource($name, $config['id'], $config['color']);
}
$event = new CalendarGoogleSourceEvent($this->getUser());
$this->dispatcher->dispatch($event);
foreach ($event->getSources() as $source) {
$sources[] = $source;
}
return new Google($apiKey, $sources);
}
} }

View File

@@ -44,6 +44,7 @@ use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Validator\Constraints\GreaterThanOrEqual; use Symfony\Component\Validator\Constraints\GreaterThanOrEqual;
use Symfony\Component\Validator\Constraints\NotBlank; use Symfony\Component\Validator\Constraints\NotBlank;
use Symfony\Component\Validator\Constraints\NotNull; use Symfony\Component\Validator\Constraints\NotNull;
use Symfony\Component\Validator\Constraints\Range;
use Symfony\Component\Validator\Constraints\Regex; use Symfony\Component\Validator\Constraints\Regex;
/** /**
@@ -582,6 +583,11 @@ final class SystemConfigurationController extends AbstractController
->setTranslationDomain('system-configuration') ->setTranslationDomain('system-configuration')
->setType(TextType::class) ->setType(TextType::class)
->setConstraints([new Regex(['pattern' => '/[0-2]{1}[0-9]{1}:[0-9]{2}:[0-9]{2}/']), new NotNull()]), ->setConstraints([new Regex(['pattern' => '/[0-2]{1}[0-9]{1}:[0-9]{2}:[0-9]{2}/']), new NotNull()]),
(new Configuration())
->setName('calendar.dragdrop_amount')
->setTranslationDomain('system-configuration')
->setType(IntegerType::class)
->setConstraints([new Range(['min' => 0, 'max' => 20]), new NotNull()]),
]), ]),
(new SystemConfigurationModel()) (new SystemConfigurationModel())
->setSection(SystemConfigurationModel::SECTION_BRANDING) ->setSection(SystemConfigurationModel::SECTION_BRANDING)

View File

@@ -391,6 +391,19 @@ class Configuration implements ConfigurationInterface
->end() ->end()
->end() ->end()
->booleanNode('weekends')->defaultTrue()->end() ->booleanNode('weekends')->defaultTrue()->end()
->integerNode('dragdrop_amount')
->defaultValue(10)
->validate()
->ifTrue(static function ($v) {
if ($v === null || $v < 0 || $v > 20) {
return true;
}
return false;
})
->thenInvalid('The dragdrop_amount must be between 0 and 20')
->end()
->end()
->end() ->end()
; ;

View File

@@ -23,10 +23,15 @@ final class CalendarDragAndDropSourceEvent extends Event
* @var DragAndDropSource[] * @var DragAndDropSource[]
*/ */
private $sources = []; private $sources = [];
/**
* @var int
*/
private $maxEntries = 0;
public function __construct(User $user) public function __construct(User $user, int $maxEntries)
{ {
$this->user = $user; $this->user = $user;
$this->maxEntries = $maxEntries;
} }
public function getUser(): User public function getUser(): User
@@ -34,6 +39,11 @@ final class CalendarDragAndDropSourceEvent extends Event
return $this->user; return $this->user;
} }
public function getMaxEntries(): int
{
return $this->maxEntries;
}
public function addSource(DragAndDropSource $source): CalendarDragAndDropSourceEvent public function addSource(DragAndDropSource $source): CalendarDragAndDropSourceEvent
{ {
$this->sources[] = $source; $this->sources[] = $source;

View File

@@ -972,7 +972,7 @@ class TimesheetRepository extends EntityRepository
* @return array|mixed * @return array|mixed
* @throws \Doctrine\ORM\Query\QueryException * @throws \Doctrine\ORM\Query\QueryException
*/ */
public function getRecentActivities(User $user = null, DateTime $startFrom = null, $limit = 10) public function getRecentActivities(User $user = null, DateTime $startFrom = null, int $limit = 10)
{ {
$qb = $this->getEntityManager()->createQueryBuilder(); $qb = $this->getEntityManager()->createQueryBuilder();

View File

@@ -10,7 +10,7 @@
{% block main %} {% block main %}
<div class="row"> <div class="row">
{% set hasDragAndDrop = (dragAndDrop is not empty and (dragAndDrop|filter(s => s.entries|length > 0)|length > 0)) %} {% set hasDragAndDrop = (config.dragDropAmount > 0 and dragAndDrop is not empty and (dragAndDrop|filter(s => s.entries|length > 0)|length > 0)) %}
{% if hasDragAndDrop %} {% if hasDragAndDrop %}
<div class="hidden-xs col-sm-5 col-md-4 col-lg-3 no-print"> <div class="hidden-xs col-sm-5 col-md-4 col-lg-3 no-print">
{% for source in dragAndDrop|filter(s => s.entries|length > 0) %} {% for source in dragAndDrop|filter(s => s.entries|length > 0) %}
@@ -19,7 +19,7 @@
{% block box_body_class %}drag-and-drop-source{% endblock %} {% block box_body_class %}drag-and-drop-source{% endblock %}
{% block box_body %} {% block box_body %}
<div class="external-events" data-method="{{ source.method }}" data-route="{{ path(source.route, source.routeParams) }}" data-route-replacer="{{ source.routeReplacer|json_encode|e('html_attr') }}"> <div class="external-events" data-method="{{ source.method }}" data-route="{{ path(source.route, source.routeParams) }}" data-route-replacer="{{ source.routeReplacer|json_encode|e('html_attr') }}">
{% for entry in source.entries %} {% for entry in source.entries|slice(0, config.dragDropAmount) %}
<div draggable="true" style="background-color: {{ entry.color }}; color: {{ entry.color|font_contrast }}" class="external-event ui-draggable ui-draggable-handle" data-entry="{{ entry.data|json_encode|e('html_attr') }}"> <div draggable="true" style="background-color: {{ entry.color }}; color: {{ entry.color|font_contrast }}" class="external-event ui-draggable ui-draggable-handle" data-entry="{{ entry.data|json_encode|e('html_attr') }}">
{% if source.blockInclude is not null and entry.blockName is not null and block(entry.blockName, source.blockInclude) is defined %} {% if source.blockInclude is not null and entry.blockName is not null and block(entry.blockName, source.blockInclude) is defined %}
{{ block(entry.blockName, source.blockInclude) }} {{ block(entry.blockName, source.blockInclude) }}

View File

@@ -130,7 +130,8 @@ class AppExtensionTest extends TestCase
'api_key' => null, 'api_key' => null,
'sources' => [], 'sources' => [],
], ],
'weekends' => true 'weekends' => true,
'dragdrop_amount' => 10,
], ],
'kimai.dashboard' => [], 'kimai.dashboard' => [],
'kimai.widgets' => [], 'kimai.widgets' => [],

View File

@@ -128,6 +128,19 @@ class ConfigurationTest extends TestCase
$this->assertConfig($config, []); $this->assertConfig($config, []);
} }
public function testValidateCalendarDragDropMaxEntries()
{
$this->expectException(InvalidConfigurationException::class);
$this->expectExceptionMessage('Invalid configuration for path "kimai.calendar.dragdrop_amount": The dragdrop_amount must be between 0 and 20');
$config = $this->getMinConfig();
$config['calendar'] = [
'dragdrop_amount' => 50,
];
$this->assertConfig($config, []);
}
public function testValidateLdapFilterInvalidParenthesisCounter() public function testValidateLdapFilterInvalidParenthesisCounter()
{ {
$this->expectException(InvalidConfigurationException::class); $this->expectException(InvalidConfigurationException::class);
@@ -346,6 +359,7 @@ class ConfigurationTest extends TestCase
], ],
], ],
'weekends' => true, 'weekends' => true,
'dragdrop_amount' => 10,
], ],
'theme' => [ 'theme' => [
'active_warning' => 3, 'active_warning' => 3,

View File

@@ -24,7 +24,7 @@ class CalendarDragAndDropSourceEventTest extends TestCase
$user = new User(); $user = new User();
$user->setAlias('foo'); $user->setAlias('foo');
$sut = new CalendarDragAndDropSourceEvent($user); $sut = new CalendarDragAndDropSourceEvent($user, 10);
$hello = new TestDragAndDropSource('hello'); $hello = new TestDragAndDropSource('hello');
$tmp1 = new TestDragAndDropSource('foo'); $tmp1 = new TestDragAndDropSource('foo');
@@ -40,6 +40,7 @@ class CalendarDragAndDropSourceEventTest extends TestCase
self::assertInstanceOf(CalendarDragAndDropSourceEvent::class, $sut->addSource($tmp3)); self::assertInstanceOf(CalendarDragAndDropSourceEvent::class, $sut->addSource($tmp3));
self::assertCount(4, $sut->getSources()); self::assertCount(4, $sut->getSources());
self::assertEquals([$tmp1, $tmp2, $hello, $tmp3], $sut->getSources()); self::assertEquals([$tmp1, $tmp2, $hello, $tmp3], $sut->getSources());
self::assertEquals(10, $sut->getMaxEntries());
self::assertFalse($sut->removeSource(new TestDragAndDropSource('foo'))); self::assertFalse($sut->removeSource(new TestDragAndDropSource('foo')));
self::assertTrue($sut->removeSource($hello)); self::assertTrue($sut->removeSource($hello));

View File

@@ -314,6 +314,10 @@
<source>label.timesheet.rules.long_running_duration</source> <source>label.timesheet.rules.long_running_duration</source>
<target>Maximale Dauer eines Zeiteintrags in Minuten, bevor das Speichern abgelehnt wird (0 = deaktiviert)</target> <target>Maximale Dauer eines Zeiteintrags in Minuten, bevor das Speichern abgelehnt wird (0 = deaktiviert)</target>
</trans-unit> </trans-unit>
<trans-unit id="label.calendar.dragdrop_amount">
<source>label.calendar.dragdrop_amount</source>
<target>Anzahl an Einträgen für Drag&amp;Drop (0 = deaktiviert)</target>
</trans-unit>
</body> </body>
</file> </file>
</xliff> </xliff>

View File

@@ -314,6 +314,10 @@
<source>label.timesheet.rules.long_running_duration</source> <source>label.timesheet.rules.long_running_duration</source>
<target>Maximum duration of a timesheet record in minutes before saving is rejected (0 = deactivated)</target> <target>Maximum duration of a timesheet record in minutes before saving is rejected (0 = deactivated)</target>
</trans-unit> </trans-unit>
<trans-unit id="label.calendar.dragdrop_amount">
<source>label.calendar.dragdrop_amount</source>
<target>Amount of entries for drag&amp;drop (0 = deactivated)</target>
</trans-unit>
</body> </body>
</file> </file>
</xliff> </xliff>