limit amount of items in calendar drag and drop boxes (#2291)
This commit is contained in:
102
src/Calendar/CalendarService.php
Normal file
102
src/Calendar/CalendarService.php
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -170,6 +170,11 @@ class SystemConfiguration implements SystemBundleConfiguration
|
||||
return (string) $this->find('calendar.slot_duration');
|
||||
}
|
||||
|
||||
public function getCalendarDragAndDropMaxEntries(): int
|
||||
{
|
||||
return (int) $this->find('calendar.dragdrop_amount');
|
||||
}
|
||||
|
||||
// ========== Customer configurations ==========
|
||||
|
||||
public function getCustomerDefaultTimezone(): ?string
|
||||
|
||||
@@ -9,21 +9,11 @@
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Calendar\DragAndDropSource;
|
||||
use App\Calendar\Google;
|
||||
use App\Calendar\GoogleSource;
|
||||
use App\Calendar\RecentActivitiesSource;
|
||||
use App\Calendar\TimesheetEntry;
|
||||
use App\Calendar\CalendarService;
|
||||
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\Utils\Color;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
|
||||
|
||||
/**
|
||||
* Controller used to display calendars.
|
||||
@@ -33,20 +23,17 @@ use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
|
||||
*/
|
||||
class CalendarController extends AbstractController
|
||||
{
|
||||
/**
|
||||
* @var EventDispatcherInterface
|
||||
*/
|
||||
private $dispatcher;
|
||||
private $calendarService;
|
||||
|
||||
public function __construct(EventDispatcherInterface $dispatcher)
|
||||
public function __construct(CalendarService $calendarService)
|
||||
{
|
||||
$this->dispatcher = $dispatcher;
|
||||
$this->calendarService = $calendarService;
|
||||
}
|
||||
|
||||
/**
|
||||
* @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();
|
||||
$factory = $this->getDateTimeFactory();
|
||||
@@ -62,19 +49,24 @@ class CalendarController extends AbstractController
|
||||
'slotDuration' => $configuration->getCalendarSlotDuration(),
|
||||
'timeframeBegin' => $configuration->getCalendarTimeframeBegin(),
|
||||
'timeframeEnd' => $configuration->getCalendarTimeframeEnd(),
|
||||
'dragDropAmount' => $configuration->getCalendarDragAndDropMaxEntries(),
|
||||
];
|
||||
|
||||
$isPunchMode = !$mode->canEditDuration() && !$mode->canEditBegin() && !$mode->canEditEnd();
|
||||
$dragAndDrop = [];
|
||||
|
||||
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', [
|
||||
'config' => $config,
|
||||
'dragAndDrop' => $dragAndDrop,
|
||||
'google' => $this->getGoogleSources($configuration),
|
||||
'google' => $this->calendarService->getGoogleSources($this->getUser()),
|
||||
'now' => $factory->createDateTime(),
|
||||
'defaultStartTime' => $defaultStart->format('h:i:s'),
|
||||
'is_punch_mode' => $isPunchMode,
|
||||
@@ -83,60 +75,4 @@ class CalendarController extends AbstractController
|
||||
'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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,6 +44,7 @@ use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Validator\Constraints\GreaterThanOrEqual;
|
||||
use Symfony\Component\Validator\Constraints\NotBlank;
|
||||
use Symfony\Component\Validator\Constraints\NotNull;
|
||||
use Symfony\Component\Validator\Constraints\Range;
|
||||
use Symfony\Component\Validator\Constraints\Regex;
|
||||
|
||||
/**
|
||||
@@ -582,6 +583,11 @@ final class SystemConfigurationController extends AbstractController
|
||||
->setTranslationDomain('system-configuration')
|
||||
->setType(TextType::class)
|
||||
->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())
|
||||
->setSection(SystemConfigurationModel::SECTION_BRANDING)
|
||||
|
||||
@@ -391,6 +391,19 @@ class Configuration implements ConfigurationInterface
|
||||
->end()
|
||||
->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()
|
||||
;
|
||||
|
||||
|
||||
@@ -23,10 +23,15 @@ final class CalendarDragAndDropSourceEvent extends Event
|
||||
* @var DragAndDropSource[]
|
||||
*/
|
||||
private $sources = [];
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
private $maxEntries = 0;
|
||||
|
||||
public function __construct(User $user)
|
||||
public function __construct(User $user, int $maxEntries)
|
||||
{
|
||||
$this->user = $user;
|
||||
$this->maxEntries = $maxEntries;
|
||||
}
|
||||
|
||||
public function getUser(): User
|
||||
@@ -34,6 +39,11 @@ final class CalendarDragAndDropSourceEvent extends Event
|
||||
return $this->user;
|
||||
}
|
||||
|
||||
public function getMaxEntries(): int
|
||||
{
|
||||
return $this->maxEntries;
|
||||
}
|
||||
|
||||
public function addSource(DragAndDropSource $source): CalendarDragAndDropSourceEvent
|
||||
{
|
||||
$this->sources[] = $source;
|
||||
|
||||
@@ -972,7 +972,7 @@ class TimesheetRepository extends EntityRepository
|
||||
* @return array|mixed
|
||||
* @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();
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
|
||||
{% block main %}
|
||||
<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 %}
|
||||
<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) %}
|
||||
@@ -19,7 +19,7 @@
|
||||
{% block box_body_class %}drag-and-drop-source{% endblock %}
|
||||
{% 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') }}">
|
||||
{% 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') }}">
|
||||
{% 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) }}
|
||||
|
||||
@@ -130,7 +130,8 @@ class AppExtensionTest extends TestCase
|
||||
'api_key' => null,
|
||||
'sources' => [],
|
||||
],
|
||||
'weekends' => true
|
||||
'weekends' => true,
|
||||
'dragdrop_amount' => 10,
|
||||
],
|
||||
'kimai.dashboard' => [],
|
||||
'kimai.widgets' => [],
|
||||
|
||||
@@ -128,6 +128,19 @@ class ConfigurationTest extends TestCase
|
||||
$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()
|
||||
{
|
||||
$this->expectException(InvalidConfigurationException::class);
|
||||
@@ -346,6 +359,7 @@ class ConfigurationTest extends TestCase
|
||||
],
|
||||
],
|
||||
'weekends' => true,
|
||||
'dragdrop_amount' => 10,
|
||||
],
|
||||
'theme' => [
|
||||
'active_warning' => 3,
|
||||
|
||||
@@ -24,7 +24,7 @@ class CalendarDragAndDropSourceEventTest extends TestCase
|
||||
$user = new User();
|
||||
$user->setAlias('foo');
|
||||
|
||||
$sut = new CalendarDragAndDropSourceEvent($user);
|
||||
$sut = new CalendarDragAndDropSourceEvent($user, 10);
|
||||
|
||||
$hello = new TestDragAndDropSource('hello');
|
||||
$tmp1 = new TestDragAndDropSource('foo');
|
||||
@@ -40,6 +40,7 @@ class CalendarDragAndDropSourceEventTest extends TestCase
|
||||
self::assertInstanceOf(CalendarDragAndDropSourceEvent::class, $sut->addSource($tmp3));
|
||||
self::assertCount(4, $sut->getSources());
|
||||
self::assertEquals([$tmp1, $tmp2, $hello, $tmp3], $sut->getSources());
|
||||
self::assertEquals(10, $sut->getMaxEntries());
|
||||
|
||||
self::assertFalse($sut->removeSource(new TestDragAndDropSource('foo')));
|
||||
self::assertTrue($sut->removeSource($hello));
|
||||
|
||||
@@ -314,6 +314,10 @@
|
||||
<source>label.timesheet.rules.long_running_duration</source>
|
||||
<target>Maximale Dauer eines Zeiteintrags in Minuten, bevor das Speichern abgelehnt wird (0 = deaktiviert)</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="label.calendar.dragdrop_amount">
|
||||
<source>label.calendar.dragdrop_amount</source>
|
||||
<target>Anzahl an Einträgen für Drag&Drop (0 = deaktiviert)</target>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
|
||||
@@ -314,6 +314,10 @@
|
||||
<source>label.timesheet.rules.long_running_duration</source>
|
||||
<target>Maximum duration of a timesheet record in minutes before saving is rejected (0 = deactivated)</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="label.calendar.dragdrop_amount">
|
||||
<source>label.calendar.dragdrop_amount</source>
|
||||
<target>Amount of entries for drag&drop (0 = deactivated)</target>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
|
||||
Reference in New Issue
Block a user